From f4e87b0c02f90b264e0407f80fb8bc4cd1ea96e5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 29 Oct 2026 18:43:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=E5=8E=86=E5=8F=B2=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=97=B6=E9=97=B4=E5=B9=B6=E6=81=A2=E5=A4=8D=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=8D=A1=E7=89=87=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 2 +- static/js/chat.js | 28 ++++++++++++++++++---------- tests/frontend/chat.test.cjs | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8b5757c..0a6ba00 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -108,7 +108,7 @@ tests/ # 按业务模块组织的回归 - `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 表、字段、角色值和历史记录继续可读。切换话题或历史时取消旧流和待上传附件;等待建会话期间的新草稿不能被旧发送清空。历史刷新失败保留已有列表,迟到的刷新不覆盖新结果。删除会话同步移除其人工会话及消息,迟到的模型写入不能生成孤立消息。 +- 历史卡片以 `__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` 访问。 - 计费档位、单位换算和舍入保持原样。`shipping_intent_with_context` 在报价追问中保留重量、尺寸、渠道和区域,遇到新的完整问题停止回溯;英国使用英国渠道和默认比价组合。明确提到美国等未覆盖目的地时不套用欧洲费率,英国按方报价不套用欧洲仓价;指定国家或仓点没有匹配时 `calc_warehouse_card` 返回空。`calc_freight.total` 仅含基础运费;附加费尚未统一并入。知识文档与 `knowledge_list` 同步维护。 diff --git a/static/js/chat.js b/static/js/chat.js index 8206321..53a44ab 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -807,12 +807,13 @@ function openConversation(id){ msgs.forEach(m=>{ if(m.role === "assistant" && m.content.indexOf(CARD_PREFIX) === 0){ try{ - addTrackCard(JSON.parse(m.content.slice(CARD_PREFIX.length))); + const bubble = addTrackCard(JSON.parse(m.content.slice(CARD_PREFIX.length)), m.ctime); + attachActions(bubble, m.content, '', false); }catch(e){ - addMsg(m.content, false); + addMsg(m.content, false, undefined, m.ctime); } }else{ - const b = addMsg(m.content, m.role === "user"); + const b = addMsg(m.content, m.role === "user", undefined, m.ctime); if(m.role === "assistant" && b) attachActions(b, m.content, "", false); } }); @@ -925,12 +926,23 @@ function createMessageRow(isUser, authorRole){ return row; } -function addTrackCard(card){ +function appendMessageTime(row, timeStr){ + const ts = document.createElement('div'); + ts.className = 'msg-time'; + const stored = typeof timeStr === 'string' && timeStr.match(/(?:^|[T\s])(\d{2}:\d{2})/); + const now = new Date(); + ts.textContent = stored ? stored[1] : String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0'); + if(timeStr) ts.title = timeStr; + row.appendChild(ts); +} + +function addTrackCard(card, timeStr){ const row = createMessageRow(false); const bubble = document.createElement("div"); bubble.className = "robot-message track-card"; fillTrackCard(bubble, card); row.appendChild(bubble); + appendMessageTime(row, timeStr); chatDom.appendChild(row); scrollChatToEnd(); return bubble; @@ -1180,7 +1192,7 @@ function startHumanPoll(){ humanPollTimer = pollTimer; } -function addMsg(content, isUser, authorRole){ +function addMsg(content, isUser, authorRole, timeStr){ if(chatDom.querySelector('.ai-welcome-card')) chatDom.innerHTML = ""; const rowDiv = createMessageRow(isUser, authorRole); let robotMsg; @@ -1236,11 +1248,7 @@ function addMsg(content, isUser, authorRole){ rowDiv.appendChild(robotMsg); } // 时间戳 - const ts = document.createElement("div"); - ts.className = "msg-time"; - const d = new Date(); - ts.textContent = String(d.getHours()).padStart(2,'0') + ":" + String(d.getMinutes()).padStart(2,'0'); - rowDiv.appendChild(ts); + appendMessageTime(rowDiv, timeStr); chatDom.appendChild(rowDiv); scrollChatToEnd(Boolean(isUser)); if(!isUser){ diff --git a/tests/frontend/chat.test.cjs b/tests/frontend/chat.test.cjs index 0fa4e1f..c95ac90 100644 --- a/tests/frontend/chat.test.cjs +++ b/tests/frontend/chat.test.cjs @@ -278,6 +278,20 @@ test('a failed history deletion preserves the active conversation for the next m assert.equal(app.calls.find(call => call.url === '/stream_chat').body.conv_id,9); }); +test('history text and order cards preserve their original times and card actions', async () => { + const app=await customerPage({human:false,history:[{id:9,title:'历史时间',ctime:'2026-10-28 07:12'}]}); + app.hooks.messages=() => response([ + {role:'user',content:'FL202610290001',ctime:'2026-10-28 07:12'}, + {role:'assistant',content:'__CARD__'+JSON.stringify({kind:'order',no:'FL202610290001',status:'履约中',fields:[],nodes:[],summary:'订单概要'}),ctime:'2026-10-28 07:13'}, + {role:'assistant',content:'后续补充回复',ctime:'2026-10-28 07:14'} + ]); + await app.click(app.element('historyWrap').children[0]); + const times=app.element('chatDom').querySelectorAll('.msg-time'); + assert.deepEqual(times.map(item => item.textContent),['07:12','07:13','07:14']); + assert.equal(times[0].title,'2026-10-28 07:12'); + assert.ok(app.element('chatDom').querySelector('.track-card').querySelector('.msg-actions')); +}); + test('recalling an earlier reply preserves newer replies in the customer log', async () => { const app = await customerPage(); app.setState({status:'active', messages:[{id:1, side:'agent', content:'早先的回复'}, {id:2, side:'agent', content:'最新回复'}]});