diff --git a/.env.example b/.env.example index 03a36c9..43feefd 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,5 @@ # The process environment takes precedence, including an explicitly empty value. # Keep real credentials in .env; never put them in this example or source files. DEEPSEEK_API_KEY= +# Optional fixed tenant scope for the internal order test dataset. Empty = all imported test rows. +ORDER_DATA_TENANT_ID= diff --git a/AGENTS.md b/AGENTS.md index 62a1581..c809fd9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ Use `am.py` for CLI/WSGI, `logistics_chat/` for business code, and `static/css/` | Knowledge | `logistics_chat/knowledge.py`: `knowledge_list`, `search_knowledge`; `docs/knowledge.md` | | Pricing | `logistics_chat/pricing.py`: `RATES`, `calc_freight`; `logistics_chat/rates_extra.py` | | Tracking | `logistics_chat/tracking.py`: `extract_tracking_no`, `build_track`, `CARD_PREFIX` | +| Order test data | `logistics_chat/order_data.py`: `lookup_order`, `answer_query`, `import_records`; `scripts/import_order_test_data.py` | | Persistence | `logistics_chat/db.py`: `init_db`, `save_message`; `logistics_chat/history_service.py` | | Human support | `logistics_chat/human_service.py`: lifecycle, transfer, notes, recall; `logistics_chat/routes/human.py` | | Business / files | `logistics_chat/business_service.py`, `logistics_chat/file_service.py`; `logistics_chat/routes/` | diff --git a/README.md b/README.md index 1ac1227..2bf5f17 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - **知识库优先**:优先使用本地知识库回答,减少模型编造 - **追问兜底**:当前问题没命中关键词时,用上一句提问去检索 - **运费试算**:识别重量/尺寸/渠道,按报价表阶梯单价计算并展示完整明细 -- **运单号查询**:发送 8~20 位运单号,返回轨迹卡片;先展示最近节点,可展开全部轨迹 +- **订单查询**:按订单号、入仓码、Shipment ID 查询本地订单测试表;展示概要、申报重量/体积和业务状态历史,不生成模拟运输轨迹 - **历史会话**:会话保存到 SQLite,支持打开、搜索、删除 - **交互**:停止生成、复制(纯文本)、重答、多行输入、回到底部 - **中英文切换**:右上角「中/EN」按钮或快捷键 `Ctrl + L` @@ -132,7 +132,8 @@ baiyue-logistics-chat/ │ ├── knowledge.py 知识与关键词检索 │ ├── pricing.py 运费解析与计算 │ ├── rates_extra.py 扩展报价唯一数据源 -│ ├── tracking.py 模拟轨迹与卡片协议 +│ ├── tracking.py 卡片协议与旧演示函数兼容 +│ ├── order_data.py 本地订单测试表、精确查询与快照导入 │ ├── ai_client.py 模型 HTTP、压缩和追问 │ ├── chat_service.py 问答与 SSE 编排 │ ├── history_service.py 会话 CRUD @@ -238,13 +239,23 @@ data: {"d": 1} ## 数据库与运行数据 -首次启动会自动创建 `chat_history`、`messages`、`feedback`、`human_sessions`、`human_messages`、`ratings`、`agents`、`orders` 和 `tickets` 九张表,并兼容已有数据,不预置账号或演示数据。本地入口使用根目录 `history.db`,Linux 部署工厂另存至专用数据目录。客服账号需由管理员在实际环境中配置。会话备注保存在 `human_sessions.note`;消息撤回事件通过 `human_messages.recalled_id` 指向原消息,轮询客户端按编号更新。运单卡片以 `__CARD__{json}` 形式保存,前端据此恢复展示。 +订单测试数据保存在聊天服务运行时 SQLite 的 `order_test_data` 表中,与灰度业务库独立,可直接通过 SQLite 工具新增或修改。聊天每次重新查表;这份副本不会自动同步灰度业务库。没有新增管理页面和增删改接口。内部验收可查询已导入的测试样本;配置 `ORDER_DATA_TENANT_ID` 可以在服务器侧固定租户范围,不能将其视为客户登录授权。 + +快照 JSON 放仓库外的私有 artifacts 目录,使用明确路径导入: + +```powershell +.\.venv\Scripts\python.exe scripts/import_order_test_data.py --snapshot "D:\private\orders.snapshot.json" --db "D:\data\history.db" +``` + +脚本默认保留已有记录,避免覆盖手工调整;确需覆盖时显式加 `--replace`。`order_no` 与 `tenant_id` 组成唯一键;`gross_weight_kg`、`volume_cbm`、`package_count` 是申报汇总,`history_json` 是形如 `[{"time":"2026-10-29 10:00","status":"SUBMITTED"}]` 的业务状态列表。子订单未提供独立货物数据时保持空值。快照、运行数据库和凭据不提交 Git。后续 MCP 接口开放后替换 `order_data.lookup_order` 的取数实现即可保留聊天契约。 + +首次启动会自动创建 `chat_history`、`messages`、`feedback`、`human_sessions`、`human_messages`、`ratings`、`agents`、`orders`、`tickets` 和 `order_test_data` 十张表,并兼容已有数据,不预置账号或订单数据。本地入口使用根目录 `history.db`,Linux 部署工厂另存至专用数据目录。客服账号需由管理员在实际环境中配置。会话备注保存在 `human_sessions.note`;消息撤回事件通过 `human_messages.recalled_id` 指向原消息,轮询客户端按编号更新。订单卡片以 `__CARD__{json}` 形式保存,前端据此恢复展示。 普通导入 `am.py` 或 `logistics_chat` 不创建运行数据。显式调用 `create_app()` 或访问 WSGI 兼容入口 `am.app` 时,默认初始化 SQLite、创建正常运行日志、备份数据库并清理修改时间超过 7 天的上传文件。测试须事先配置临时数据库、附件、日志和备份目录;`INITIALIZE_RUNTIME=False` 可禁止自动初始化,此时调用方负责准备 schema。历史会话的自动清理仍已关闭。 ## 当前限制 -- 运单轨迹为模拟数据,尚未接入真实物流查询接口。 +- 订单查询使用可编辑测试副本,尚未接入实时承运商物流轨迹或 MCP;旧历史中的模拟轨迹保留演示标识。 - 不预置客服账号;应用内客服登录仍沿用旧账号校验逻辑。公网自用验收可按部署指南使用全站访问密码进入工作台。 - 后台登录尚未建立完整会话鉴权,管理页面和接口缺少登录校验。 - 运费数字由 Python 计算后交给模型组织说明;附加费是否适用仍需结合具体货物和地址核对。 diff --git a/docs/architecture.md b/docs/architecture.md index 0b6a101..a17f260 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -26,7 +26,8 @@ PowerShell 下把目录和 `-g '*.py'` 分开传给 `rg`。报价字典可能整 | 历史接口 | `logistics_chat/history_service.py`:`new_chat`、`get_history`、`get_messages`、`del_history`、`clear_all`;对应 `routes/history.py` | `static/js/chat.js`:`ensureConv`、`loadHistory`、`openConversation` | | 知识检索 | `logistics_chat/knowledge.py`:`knowledge_list`、`search_knowledge` | [knowledge.md](knowledge.md) 与代码同步,程序不读取该文档 | | 运费与附加费 | `logistics_chat/pricing.py`:`RATES`、`parse_shipping_intent`、`dims_to_cm`、`pick_rate`、`calc_freight`、`freight_reply`、`calc_warehouse_card`、`detect_surcharge`、`check_island` | `logistics_chat/rates_extra.py` 为唯一扩展数据源,根目录同名文件仅兼容转发 | -| 运单轨迹 | `logistics_chat/tracking.py`:`extract_tracking_no`、`build_track`、`CARD_PREFIX`、`card_to_text` | `static/js/chat.js`:`fillTrackCard`、`addTrackCard`;模拟节点时间取当前时刻,回归对照需固定时钟 | +| 卡片协议 | `logistics_chat/tracking.py`:`CARD_PREFIX`、`card_to_text`;旧 `extract_tracking_no`、`build_track` 仅兼容旧导入 | `static/js/chat.js`:`fillTrackCard`、`addTrackCard`;新订单卡片由 `order_data.py` 提供,历史模拟轨迹标为演示数据 | +| 订单测试表 | `logistics_chat/order_data.py`:`init_order_table`、`import_records`、`lookup_order`、`answer_query` | SQLite `order_test_data` 为可编辑快照;按订单号、入仓码、Shipment ID 精确查表。将来 MCP 在取数层替换 `lookup_order`,不让模型拼 SQL | | AI 与 SSE | `logistics_chat/chat_service.py`:`create_chat_stream`、`sse`;`ai_client.py`:`post_completion`、`compress_answer`、`gen_followups` | `routes/chat.py`:`stream_chat`;客户端 `doSend`、`renderError`、`renderFollowups` | | 人工会话 | `logistics_chat/human_service.py`:`human_*`(含转接、备注、撤回);对应 `routes/human.py` | 输入状态在 `app.extensions['human_typing']`;客户 `startHumanPoll`;客服 `loadQueue`、`poll`、`sendReply`;人工申请每天 8:00–22:00 | | 附件 | `logistics_chat/file_service.py`:`upload_file`、`cleanup_uploads`;`routes/files.py`:`uploaded_file` | 扩展名/大小/目录由配置提供;客户端 `addMsg`,客服端 `appendMsg` | @@ -49,6 +50,7 @@ logistics_chat/ ├── pricing.py # 运费规则、解析、换算、附加费说明 ├── rates_extra.py # 扩展报价数据 ├── tracking.py # 单号识别、模拟轨迹、卡片协议 +├── order_data.py # 可编辑订单测试表、精确查询、快照导入与卡片事实 ├── ai_client.py # 模型 HTTP、压缩与追问 ├── chat_service.py # 问答编排与 SSE 帧 ├── history_service.py # 历史会话 CRUD @@ -103,14 +105,16 @@ tests/ # 按业务模块组织的回归 ## 必须保留的接口与数据约定 - SSE 字段为 `t`、`e`、`card`、`replace`、`suggest`、`d`,保留流式输出、停止、异常及中断保存。 -- `chat_service.create_chat_stream` 在识别运单号时忽略 `[图片]/uploads/...` 和 `[文件]/uploads/...` 路径,避免附件文件名中的时间戳误触发查询;存储与请求仍保留原始附件标记和 URL。 +- `chat_service.create_chat_stream` 的订单查询使用本地 `order_test_data`;忽略附件路径和重量/日期,保留标识中的后缀,不再调用 `build_track` 生成模拟轨迹。旧模拟函数仅兼容旧导入,不进入聊天查询路径。未知单号、多条匹配和查表失败明确说明,禁止回退模型编造状态。 +- `CARD_PREFIX` 新增 `kind=order`:仅包含白名单概要、申报货物汇总和订单业务状态历史,不包含姓名、地址、内部备注或操作人。子单没有独立货物数据时不继承父单总重量;COMPLETED 只表示头程订单完成,不表示快递签收。订单追问只在同一 `conv_id` 内复用最近单号,并重新查表取得手工修改后的值。 +- 订单数据仅为内部验收副本:`ORDER_DATA_TENANT_ID` 可固定服务器端租户范围,空值允许查询内部已导入样本。它不代替将来客户登录授权。没有管理页面或对外增删改接口;表数据在运行时 SQLite 内维护,导入脚本默认保留已有记录,只有显式 `--replace` 覆盖。源库连接凭据和快照文件均在仓库外,业务库只读,不随服务查询访问源库。 - 历史卡片以 `__CARD__{json}` 保存;原 SQLite 表、字段、角色值和历史记录继续可读。 - 人工申请使用 `conv_id`,返回 `session_id`,后续用 `sid`。状态为 `waiting/active/closed`,消息方向为 `customer/agent/system`。转接将 active 改为 waiting,列表只允许主动接入。备注保存在 `human_sessions.note`;撤回将原客服消息改为系统提示并追加带 `recalled_id` 的系统事件,轮询按原消息编号更新,重复撤回不追加事件。 - 新建会话保存一条 assistant 欢迎语并返回 `welcome`,前端在首次用户消息之前显示。首次提问判定只统计 user 消息。服务评价用 `ratings` 保存,并汇总到管理统计。Excel 导出依赖 `openpyxl`,原 CSV 可用 `?format=csv` 访问。 - 计费档位、单位换算和舍入保持原样。`calc_freight.total` 仅含基础运费;附加费尚未统一并入。知识文档与 `knowledge_list` 同步维护。 - 上传 URL 保留 `/uploads/`,默认 10MB,扩展名配置保持不变。原附件保留本地、继续忽略,目录迁移不另建一份数据。 - `agents`、`orders`、`tickets` 在应用初始化时建空表;兼容已有独立 id 列的 agents 表,不预置账号或演示数据。 -- 后台鉴权和会话隔离仍不完整,轨迹为模拟数据。Linux 公网验收用 Nginx 全站密码隔离访问,不代表已修复面向客户开放所需的应用权限。 +- 后台鉴权和会话隔离仍不完整,订单表为内部测试副本,不提供实时物流轨迹。Linux 公网验收用 Nginx 全站密码隔离访问,不代表已修复面向客户开放所需的应用权限。 ## 验证与维护 diff --git a/logistics_chat/chat_service.py b/logistics_chat/chat_service.py index c04494e..cda9309 100644 --- a/logistics_chat/chat_service.py +++ b/logistics_chat/chat_service.py @@ -10,7 +10,8 @@ from .db import connect, last_user_question, load_context, save_message from .knowledge import search_knowledge from .pricing import (parse_shipping_intent, detect_dim_unit, freight_reply, calc_warehouse_card, detect_surcharge, surcharge_reply, check_island) -from .tracking import CARD_PREFIX, extract_tracking_no, build_track +from .tracking import CARD_PREFIX +from .order_data import answer_query @@ -25,14 +26,15 @@ def create_chat_stream(msg, conv_id): return (sse({'e': '问题不能为空'}), {}) if '[图片]' in msg and len(msg) < 300: return (sse({'t': '收到您的图片,请描述一下您的问题。'}) + sse({'d': 1}), {}) - track_no = extract_tracking_no(re.sub(r'\[(?:图片|文件)\]/uploads/\S+', '', msg)) - if track_no: - card = build_track(track_no) + order_answer = answer_query(msg, conv_id) + if order_answer is not None: def track_stream(): save_message(conv_id, 'user', msg) - yield sse({'card': card}) - save_message(conv_id, 'assistant', CARD_PREFIX + json.dumps(card, ensure_ascii=False)) + yield sse(order_answer) + content = (CARD_PREFIX + json.dumps(order_answer['card'], ensure_ascii=False) + if 'card' in order_answer else order_answer['t']) + save_message(conv_id, 'assistant', content) yield sse({'d': 1}) return (track_stream(), {'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) if not get_setting('DEEPSEEK_API_KEY'): diff --git a/logistics_chat/config.py b/logistics_chat/config.py index 2587a14..a850cdf 100644 --- a/logistics_chat/config.py +++ b/logistics_chat/config.py @@ -20,6 +20,7 @@ def default_config(): 'BACKUP_DIR': ROOT_DIR / 'backups', 'LOG_DIR': ROOT_DIR / 'logs', 'DEEPSEEK_API_KEY': (api_key or '').strip(), + 'ORDER_DATA_TENANT_ID': str(os.environ.get('ORDER_DATA_TENANT_ID', local_config.get('ORDER_DATA_TENANT_ID')) or '').strip(), 'MODEL_URL': 'https://api.deepseek.com/v1/chat/completions', 'MODEL_NAME': 'deepseek-chat', 'MAX_CONTEXT': 4, diff --git a/logistics_chat/db.py b/logistics_chat/db.py index 3fce7e1..340eb41 100644 --- a/logistics_chat/db.py +++ b/logistics_chat/db.py @@ -126,6 +126,8 @@ def init_db(): status TEXT ) ''') + from .order_data import init_order_table + init_order_table(conn) old = cur.execute('SELECT id, title FROM chat_history WHERE id NOT IN (SELECT DISTINCT conv_id FROM messages)').fetchall() for cid, title in old: cur.execute('INSERT INTO messages(conv_id, role, content, ctime) VALUES (?,?,?,?)', (cid, 'user', title, now_str())) diff --git a/logistics_chat/history_service.py b/logistics_chat/history_service.py index 95042fb..6222fc3 100644 --- a/logistics_chat/history_service.py +++ b/logistics_chat/history_service.py @@ -3,7 +3,7 @@ from .db import connect, now_str -WELCOME_TEXT = '您好!欢迎咨询柏悦物流智能客服,请问有什么可以帮您?输入运单号可查轨迹,或点击下方快捷选项。' +WELCOME_TEXT = '您好!欢迎咨询柏悦物流智能客服,请问有什么可以帮您?输入订单号、入仓码或 Shipment ID 可查订单,或点击下方快捷选项。' def new_chat(): diff --git a/logistics_chat/order_data.py b/logistics_chat/order_data.py new file mode 100644 index 0000000..d312255 --- /dev/null +++ b/logistics_chat/order_data.py @@ -0,0 +1,181 @@ +"""Local, editable order test data. This repository can later be replaced by MCP.""" + +import json +import re +import sqlite3 +from datetime import datetime + +from .config import get_setting + + +FIELDS = ('source_order_id', 'tenant_id', 'order_no', 'inbound_code', 'shipment_id', + 'status', 'country_code', 'transport_mode', 'warehouse_code', 'order_role', + 'parent_order_no', 'package_count', 'gross_weight_kg', 'volume_cbm', + 'history_json', 'data_note', 'source_updated_at', 'snapshot_at') +STATUS_LABELS = {'DRAFT': '草稿', 'SUBMITTED': '已提交', 'APPROVED': '审核通过', + 'ACCEPTED': '已接单', 'REJECTED': '审核驳回', 'PROCESSING': '履约中', + 'COMPLETED': '头程订单已完成', 'CANCELLED': '已取消'} +FIELD_LABELS = {'inbound':'入仓码', 'shipment':'Shipment ID', 'country':'目的国', + 'warehouse':'目的仓', 'transport':'运输方式', 'packages':'件数', + 'weight':'申报毛重', 'volume':'申报体积', 'role':'订单层级', + 'parent':'父订单号', 'updated':'来源更新时间'} +IDENTIFIER = re.compile(r'(? 1: + return {'t':'请一次提供一个订单号、入仓码或 Shipment ID,避免查错订单。'} + reference = references[0] if references else None + if not reference and conv_id and re.search(r'这(?:个|票|单)|刚才|状态|重量|几件|体积|目的地|运费|费用', message): + from .db import connect + connection = connect() + try: + recent = connection.execute('SELECT role,content FROM messages WHERE conv_id=? ORDER BY id DESC LIMIT 12', (conv_id,)).fetchall() + finally: + connection.close() + for role, content in recent: + if role == 'user': + earlier_references = extract_reference(content) + if earlier_references: + reference = earlier_references[0] if len(earlier_references) == 1 else None + break + if role == 'assistant' and content.startswith('__CARD__'): + try: + previous = json.loads(content[8:]) + if previous.get('kind') == 'order': + reference = previous['no'] + break + except (ValueError, KeyError, TypeError): + pass + if not reference: + if re.search(r'查.*(?:订单|运单|物流轨迹)|订单状态|shipment\s*id|(?:列出|搜索|所有|全部|多少).*(?:订单|入仓)|订单.*(?:多少|列表)', message, re.I): + return {'t':'请提供订单号、入仓码或 Shipment ID,我会查询本地订单测试表中的记录。'} + return None + try: + matches = lookup_order(reference) + except OrderDataUnavailable: + return {'t':'订单测试表暂时无法读取,请稍后重试;当前无法确认该订单状态。'} + if not matches: + return {'t':f'订单测试表中未找到“{reference}”。请核对订单号、入仓码或 Shipment ID,或联系人工客服。'} + if len(matches) > 1: + return {'t':'该标识匹配到多个订单,请提供该订单唯一的订单号或入仓码。'} + if not references and re.search(r'运费|费用|多少钱|价格', message): + return {'t':'这份订单测试数据没有实际运费字段,无法据此确认该订单费用,请联系人工客服核实。'} + return {'card': build_card(matches[0])} diff --git a/logistics_chat/tracking.py b/logistics_chat/tracking.py index 0687166..978aaed 100644 --- a/logistics_chat/tracking.py +++ b/logistics_chat/tracking.py @@ -13,7 +13,9 @@ def card_to_text(content): return content try: card = json.loads(content[len(CARD_PREFIX):]) - return f"(已为运单 {card.get('no', '')} 查询过轨迹,当前状态:{card.get('status', '')})" + if card.get('kind') == 'order': + return '(订单测试数据副本,非实时运输轨迹)' + card.get('summary', '') + return f"(历史演示轨迹,非真实物流信息:单号 {card.get('no', '')},演示状态:{card.get('status', '')})" except Exception: return "(已为该运单查询过轨迹)" @@ -57,7 +59,7 @@ def extract_tracking_no(text): return best def build_track(no): - """生成固定的模拟轨迹:同一个单号每次查都一样""" + """Legacy demo helper for old imports; chat queries use order_test_data.""" rnd = random.Random(no) steps = [ ("已揽收", "义乌仓"), diff --git a/scripts/import_order_test_data.py b/scripts/import_order_test_data.py new file mode 100644 index 0000000..e7cbdc1 --- /dev/null +++ b/scripts/import_order_test_data.py @@ -0,0 +1,32 @@ +"""Import an explicit, private order snapshot into the chat SQLite database.""" + +import argparse +import json +from pathlib import Path +import sqlite3 +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from logistics_chat.order_data import import_records + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--snapshot', type=Path, required=True) + parser.add_argument('--db', type=Path, required=True) + parser.add_argument('--replace', action='store_true', help='Replace existing rows, including manual edits') + args = parser.parse_args() + if not args.snapshot.is_file() or args.snapshot.stat().st_size > 10*1024*1024: + parser.error('Snapshot must be an existing JSON file smaller than 10 MB') + records = json.loads(args.snapshot.read_text(encoding='utf-8-sig')) + if isinstance(records, dict): records = records.get('orders') + if not isinstance(records, list) or len(records) > 3000: + parser.error('Snapshot must contain a list of at most 3000 orders') + if not args.db.parent.is_dir(): parser.error('Database parent directory must exist') + with sqlite3.connect(args.db) as connection: + result = import_records(connection, records, replace=args.replace) + print(json.dumps(result)) + + +if __name__ == '__main__': + main() diff --git a/static/css/chat.css b/static/css/chat.css index 8321367..f2e198b 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -151,6 +151,12 @@ body { padding: 20px; height: 100dvh; overflow: hidden; } .track-expand { width: 100%; margin-top: 3px; padding: 7px 10px; border: 0; border-radius: 7px; background: var(--surface-soft); color: var(--brand-text); font-size: 12px; } .track-expand:hover { background: var(--brand-soft); } .track-card .msg-actions { margin-top: 8px; padding-top: 8px; } +.order-data-notice { margin: 12px 0; padding: 10px; border-radius: 7px; background: var(--brand-soft); color: var(--secondary); font-size: 12px; line-height: 1.8; } +.order-data-fields { display: grid; grid-template-columns: minmax(90px, auto) minmax(0, 1fr); gap: 7px 16px; margin: 14px 0; font-size: 13px; } +.order-data-fields dt { color: var(--muted); } +.order-data-fields dd { margin: 0; overflow-wrap: anywhere; } +.order-snapshot-time { color: var(--muted); font-size: 11px; margin: 10px 0; } +.order-history-title { margin: 18px 0 10px; font-size: 13px; font-weight: 600; } .to-bottom { display: none; position: absolute; right: 24px; bottom: calc(var(--composer-height, 150px) + 12px); z-index: 4; width: 38px; height: 38px; border-radius: 50%; border: 1px solid var(--border); background: var(--surface); color: var(--secondary); box-shadow: var(--shadow); } .to-bottom.show { display: block; } .to-bottom:hover { color: var(--brand-text); border-color: var(--brand); } diff --git a/static/js/chat.js b/static/js/chat.js index 1c5db12..45e8aa4 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -178,7 +178,7 @@ const I18N = { thinking: "正在思考中...", busyRetry: "服务繁忙,请稍后重试", sessionFail: "会话创建失败,请确认服务已启动", - autoWelcome: "您好!欢迎咨询柏悦物流智能客服,请问有什么可以帮您?输入运单号可查轨迹,或点击下方快捷选项。", + autoWelcome: "您好!欢迎咨询柏悦物流智能客服,请问有什么可以帮您?输入订单号、入仓码或 Shipment ID 可查订单,或点击下方快捷选项。", humanHours: "人工客服工作时间:每天 8:00-22:00。当前为非工作时间,请留言,工作时间会尽快回复您。", offlinePrompt: "请输入您的留言:", offlineSaved: "留言已提交,工作时间(8:00-22:00)会尽快回复您。", @@ -202,7 +202,7 @@ const I18N = { networkFail: "网络连接中断,请检查网络后重试", stopped: "已停止生成", stoppedMark: "(已停止生成)", - inputPh: "请输入您的问题或运单号...(Enter 发送,Shift+Enter 换行)", + inputPh: "请输入问题、订单号或入仓码…(Enter 发送,Shift+Enter 换行)", send: "发送", stop: "停止", copy: "复制", @@ -210,18 +210,18 @@ const I18N = { retry: "重答", clickRetry: "点击重试", welcomeTitle: "物流问题,随时为您解答", - welcomeSub: "查询物流轨迹、了解专线报价,或联系人工客服。", + welcomeSub: "查询订单状态、了解专线报价,或联系人工客服。", welcomeEyebrow: "中欧 · 英国物流服务", serviceScope: "中欧与英国专线咨询", serviceTag: "物流咨询服务", quickLabel: "快捷咨询", - footerTrack: "查运单", + footerTrack: "查订单", footerQuote: "专线报价", footerException: "异常处理", footerHuman: "联系人工", disclaimer: "报价与时效仅供参考,请以客服最终确认为准", keyboardHint: "Enter 发送 · Shift + Enter 换行", - inputShort: "输入问题或运单号…", + inputShort: "输入问题、订单号或入仓码…", uploading: "附件正在上传,请稍候…", uploadFailed: "附件上传失败,请重新选择文件", serviceUnavailable: "服务暂时不可用,请稍后重试,或联系人工客服。", @@ -231,9 +231,19 @@ const I18N = { assistant: "柏悦物流助手", humanAgent: "人工客服", trackingNo: "运单号", + orderNo: "订单号", + orderDataNotice: "本地订单测试数据副本。业务状态不代表快递签收,未提供的运输节点不作推断。", + orderHistory: "订单业务状态历史", + noOrderHistory: "未提供订单状态历史", + legacyTrackingNotice: "历史模拟轨迹,仅供演示,不代表真实订单。", + orderSnapshot: "数据导入时间", + order_inbound: "入仓码", order_shipment: "Shipment ID", order_country: "目的国", + order_warehouse: "目的仓", order_transport: "运输方式", order_packages: "件数", + order_weight: "申报毛重", order_volume: "申报体积", order_role: "订单层级", + order_parent: "父订单号", order_updated: "来源更新时间", moreTracking: "展开全部轨迹", lessTracking: "收起较早轨迹", - quickSubs: ["输入运单号,查看运输进度", "了解线路、费用与预计时效", "咨询延误、签收或理赔问题", "进一步沟通您的物流需求"], + quickSubs: ["输入订单号、入仓码或 Shipment ID", "了解线路、费用与预计时效", "咨询延误、签收或理赔问题", "进一步沟通您的物流需求"], welcomeTip: "您可以直接输入问题,也可以点击下面选项快速提问。", hot: "热点问题", recent: "最近对话记录", @@ -243,7 +253,7 @@ const I18N = { langTip: "中英文切换(Ctrl+L)", toBottom: "回到底部", quick: [ - "帮我查询物流轨迹", "欧洲海运专线报价", "包裹异常处理", "转接人工客服" + "查询订单状态", "欧洲海运专线报价", "包裹异常处理", "转接人工客服" ], hotList: [ "如何查询物流轨迹?", "欧洲海运时效是多久?", "包裹显示异常签收怎么办?", "如何申请理赔?" @@ -262,7 +272,7 @@ const I18N = { thinking: "Thinking...", busyRetry: "Service busy, please try again later", sessionFail: "Failed to create session, please check the service is running", - autoWelcome: "Welcome to Baiyue Logistics. How can we help? Enter a tracking number or choose a quick option below.", + autoWelcome: "Welcome to Baiyue Logistics. Enter an order number, inbound code or Shipment ID, or choose a quick option below.", humanHours: "Our team is available daily, 08:00–22:00. Please leave a message and we will reply during service hours.", offlinePrompt: "Please leave your message:", offlineSaved: "Message submitted. Our team will reply during service hours (08:00–22:00).", @@ -299,7 +309,7 @@ const I18N = { serviceScope: "Europe & UK freight support", serviceTag: "Logistics support", quickLabel: "Quick help", - footerTrack: "Tracking", + footerTrack: "Order lookup", footerQuote: "Get a quote", footerException: "Parcel issue", footerHuman: "Human support", @@ -315,6 +325,16 @@ const I18N = { assistant: "Baiyue Assistant", humanAgent: "Support agent", trackingNo: "Tracking number", + orderNo: "Order number", + orderDataNotice: "Editable local test data. Order workflow status does not confirm delivery. Missing transport events are not inferred.", + orderHistory: "Order workflow history", + noOrderHistory: "No order status history provided", + legacyTrackingNotice: "Historical simulated tracking for demonstration only; this is not a real order record.", + orderSnapshot: "Snapshot imported", + order_inbound: "Inbound code", order_shipment: "Shipment ID", order_country: "Destination country", + order_warehouse: "Warehouse", order_transport: "Transport", order_packages: "Packages", + order_weight: "Declared gross weight", order_volume: "Declared volume", order_role: "Order level", + order_parent: "Parent order", order_updated: "Source updated", moreTracking: "View full tracking history", lessTracking: "Show recent updates", quickSubs: ["Check the latest shipment progress", "Explore routes, rates and transit times", "Get help with delays, delivery or claims", "Discuss your shipping requirements"], @@ -765,11 +785,13 @@ const CARD_PREFIX = "__CARD__"; // 把轨迹数据画成时间轴 function fillTrackCard(el, card){ el.innerHTML = ""; + const isOrder = card.kind === 'order'; const head = document.createElement("div"); head.className = "track-head"; const no = document.createElement("div"); no.className = "track-no"; - no.innerHTML = '' + t("trackingNo") + "" + escHtml(card.no); + const numberLabel = isOrder ? 'orderNo' : 'trackingNo'; + no.innerHTML = '' + t(numberLabel) + "" + escHtml(card.no); const st = document.createElement("div"); st.className = "track-status"; st.textContent = card.status; @@ -777,6 +799,50 @@ function fillTrackCard(el, card){ head.appendChild(st); el.appendChild(head); + if(!isOrder){ + const legacyNotice = document.createElement('p'); + legacyNotice.className = 'order-data-notice'; + legacyNotice.dataset.i18n = 'legacyTrackingNotice'; + legacyNotice.textContent = t('legacyTrackingNotice'); + el.appendChild(legacyNotice); + } + + if(isOrder){ + const notice = document.createElement('p'); + notice.className = 'order-data-notice'; + notice.textContent = t('orderDataNotice') + (card.notice ? ' ' + card.notice : ''); + el.appendChild(notice); + const fields = document.createElement('dl'); + fields.className = 'order-data-fields'; + (card.fields || []).forEach(function(field){ + const label = document.createElement('dt'); + label.dataset.i18n = 'order_'+field.key; + label.textContent = t('order_'+field.key) || field.key; + const value = document.createElement('dd'); + value.textContent = field.value; + fields.append(label, value); + }); + el.appendChild(fields); + if(card.snapshot_at){ + const snapshot = document.createElement('p'); + snapshot.className = 'order-snapshot-time'; + snapshot.textContent = t('orderSnapshot')+':'+card.snapshot_at; + el.appendChild(snapshot); + } + const title = document.createElement('p'); + title.className = 'order-history-title'; + title.dataset.i18n = 'orderHistory'; + title.textContent = t('orderHistory'); + el.appendChild(title); + if(!(card.nodes || []).length){ + const empty = document.createElement('p'); + empty.className = 'order-snapshot-time'; + empty.dataset.i18n = 'noOrderHistory'; + empty.textContent = t('noOrderHistory'); + el.appendChild(empty); + } + } + const timeline = document.createElement("div"); timeline.className = "track-timeline"; el.appendChild(timeline); @@ -1179,6 +1245,7 @@ function mdToPlain(md){ if(md.indexOf(CARD_PREFIX) === 0){ try{ const card = JSON.parse(md.slice(CARD_PREFIX.length)); + if(card.kind === 'order') return card.summary || card.no; const lines = ["运单号 " + card.no + "(" + (card.status || "") + ")"]; (card.nodes || []).forEach(function(n){ lines.push(n.time + " " + n.text + (n.place ? " " + n.place : "")); @@ -1428,7 +1495,7 @@ async function doSend(retryText){ if(obj.card){ aiBubble.classList.add("track-card"); fillTrackCard(aiBubble, obj.card); - aiBubble.dataset.raw = "运单号 " + obj.card.no + "(" + (obj.card.status || "") + ")\n" + + aiBubble.dataset.raw = obj.card.kind === 'order' ? obj.card.summary : "运单号 " + obj.card.no + "(" + (obj.card.status || "") + ")\n" + (obj.card.nodes || []).map(function(n){ return n.time + " " + n.text + " " + (n.place || ""); }).join("\n"); firstChunk = false; scrollChatToEnd(); diff --git a/templates/index.html b/templates/index.html index d39ba6f..2d5ce35 100644 --- a/templates/index.html +++ b/templates/index.html @@ -44,7 +44,7 @@
查询物流轨迹、了解专线报价,或联系人工客服。
从这里开始您的物流咨询。
查询订单状态、了解专线报价,或联系人工客服。
从这里开始您的物流咨询。