精简聊天页面说明并静默恢复草稿

This commit is contained in:
2026-10-30 16:04:40 +08:00
parent eba3a1b562
commit 689d55bcf9
7 changed files with 17 additions and 69 deletions
+1 -1
View File
@@ -266,7 +266,7 @@ data: {"d": 1}
## 当前限制
- 订单查询使用可编辑测试副本,尚未接入实时承运商物流轨迹或 MCP;旧历史中的模拟轨迹保留演示标识。
- 订单查询使用可编辑测试副本,尚未接入实时承运商物流轨迹或 MCP;旧历史卡片继续兼容展示。
- 不预置客服账号;应用内客服登录仍沿用旧账号校验逻辑。客服工作台及接口仍须通过 Nginx 的专用访问密码保护,客户聊天不要求此密码。
- 后台登录尚未建立完整会话鉴权,管理页面和接口缺少登录校验。
- 运费数字由 Python 计算后交给模型组织说明;附加费是否适用仍需结合具体货物和地址核对。
+2 -1
View File
@@ -26,7 +26,7 @@ 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`:`CARD_PREFIX`、`card_to_text`;旧 `extract_tracking_no`、`build_track` 仅兼容旧导入 | `static/js/chat.js`:`fillTrackCard`、`addTrackCard`;新订单卡片由 `order_data.py` 提供,历史模拟轨迹标为演示数据 |
| 卡片协议 | `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 |
@@ -87,6 +87,7 @@ tests/ # 按业务模块组织的回归
- 限流、输入状态由应用实例持有,不能每请求重置或跨测试应用共享。维护 CLI 和纯规则可用标准库加载,包入口延迟导入 Web 依赖。
- 单文件围绕一个职责;超过约 300–400 行时检查职责,而非按行数机械拆分。报价数据保持单一来源;兼容转发不复制字典。页面脚本按页面保留既有执行顺序和全局函数,不擅自改为 ES Modules。
- 配色、字体基线和通用控件放在 `theme.css`,页面布局放在对应 CSS。客户页通过 `body.dark` 覆盖主题令牌;新增可翻译文案加入 `chat.js` 的 `I18N`,静态文案用 `data-i18n`。调整样式后检查桌面、390px 窄屏、侧栏、聊天卡片和客服空/活动状态。
- 客户界面不附加测试/演示声明、底部免责声明、键盘说明、快捷入口副标题或附件准备说明;草稿恢复和发送成功静默处理。加载、录音、人工排队状态及操作失败反馈保留,权限校验继续由服务端执行。
- 客户附件预览由 `clearAttachment` 管理,轨迹展开由 `fillTrackCard` 管理。客服 `drafts` 按 `sid` 保存草稿,发送成功后只清理已确认发送的内容;常用回复仅填入草稿。`renderList` 只在展示数据变化时重绘。
- 客户文字草稿由 `drafts.js` 的 `ChatDrafts.create` 管理,脚本在 `visitor.js` 之后、`chat.js` 之前加载;localStorage 按访客 key 和会话分条存储,sessionStorage 仅记录当前标签页的活动会话。切换前同步保存,输入防抖及页面隐藏时保存;无持久存储时使用页面内存并提示。恢复历史的异步结果受会话版本约束,加载失败禁止继续发送,删除会话同步清除其草稿。草稿不含未发送附件,不跨设备同步。
- 人工申请由服务端以 UTC+8 判断每日 08:00–22:00 营业状态,客户手机时区不参与判断。附件清理保留 `messages` 和 `human_messages` 引用的文件,仅删除超过 7 天的未引用临时文件;读取引用失败时停止清理。附件不存在或不属于当前游客均返回相同的私有、不可缓存 404 页面,破图在聊天中显示可读提示。
+1 -1
View File
@@ -4,7 +4,7 @@ from .db import connect, now_str
from .visitor_service import visitor_id, owns_conversation
WELCOME_TEXT = '您好!欢迎咨询柏悦物流智能客服,可以咨询运费、时效或联系人工客服。订单查询将在客户账号登录并关联后开放。'
WELCOME_TEXT = '您好!欢迎咨询柏悦物流智能客服,可以咨询运费、时效或联系人工客服。'
def new_chat():
+1 -18
View File
@@ -42,7 +42,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.sidebar-footer { border-top: 1px solid var(--border); padding: 15px 7px 0; margin-top: 13px; white-space: nowrap; }
.clear-all-btn { border: 0; color: var(--muted); background: transparent; padding: 5px 0; font-size: 11px; }
.clear-all-btn:hover { color: var(--danger); }
.sidebar-note { display: flex; align-items: center; gap: 7px; margin-top: 10px; color: var(--muted); font-size: 10px; }
.left-panel-hide { width: 0 !important; padding-left: 0 !important; padding-right: 0 !important; visibility: hidden; border: 0; }
.main-chat { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; position: relative; overflow: hidden; }
.chat-title { flex-shrink: 0; min-height: 75px; padding: 16px 26px; display: flex; align-items: center; gap: 13px; border-bottom: 1px solid var(--border); background: var(--surface); z-index: 31; }
@@ -64,7 +63,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.ai-avatar svg { width: 27px; height: 27px; }
.welcome-eyebrow { color: var(--brand-text); font-size: 11px; font-weight: 600; letter-spacing: .06em; margin-bottom: 9px; }
.ai-welcome-card h3 { font-size: clamp(22px, 2.2vw, 30px); line-height: 1.4; letter-spacing: -.035em; font-weight: 650; color: var(--text); }
.ai-welcome-card p { color: var(--secondary); font-size: 13px; line-height: 1.8; margin-top: 10px; }
.suggest-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 28px 0 0; }
.suggest-item { display: flex; align-items: center; gap: 13px; min-height: 83px; padding: 17px; border: 1px solid var(--border); border-radius: 11px; background: var(--surface); color: var(--text); text-align: left; width: 100%; }
.suggest-item:hover { border-color: #9ab6f5; background: var(--brand-soft); box-shadow: 0 3px 12px rgba(49, 95, 213, .04); }
@@ -72,7 +70,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.suggest-icon svg { width: 19px; height: 19px; }
.suggest-copy { min-width: 0; }
.suggest-copy strong { display: block; font-size: 14px; font-weight: 600; }
.suggest-copy small { display: block; color: var(--secondary); font-size: 12px; margin-top: 4px; line-height: 1.6; }
.suggest-arrow { margin-left: auto; color: var(--muted); font-size: 17px; }
.hot-questions { margin-top: 24px; }
.hot-questions h4, .recent-chat h4 { color: var(--secondary); font-size: 12px; font-weight: 600; margin-bottom: 10px; }
@@ -97,7 +94,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.hot-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-bottom: 10px; }
.human-session { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; margin-bottom: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--brand-soft); color: var(--secondary); font-size: 12px; }
.human-session button { flex-shrink: 0; padding: 6px 9px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--brand-text); font-size: 12px; }
.hot-bar-label { font-size: 10px; color: var(--muted); margin-right: 3px; }
.hot-bar .hot-capsule { border: 0; padding: 4px 8px; background: var(--surface-soft); font-size: 10px; }
.input-area { display: flex; align-items: flex-end; gap: 10px; border: 1px solid var(--border); border-radius: 12px; padding: 9px; background: var(--surface); box-shadow: 0 3px 15px rgba(28, 47, 80, .035); }
.input-area:focus-within { border-color: #7f9fe8; box-shadow: 0 0 0 3px var(--brand-soft); }
@@ -114,9 +110,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
#send-button { min-width: 66px; height: 38px; flex-shrink: 0; padding: 0 15px; border: 0; border-radius: 8px; background: var(--brand); color: white; font-size: 12px; font-weight: 600; }
#send-button:hover { background: var(--brand-hover); }
#send-button.stop { background: var(--danger); }
.composer-footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 9px 2px 0; }
.disclaimer, .keyboard-hint { color: var(--muted); font-size: 11px; }
.keyboard-hint { white-space: nowrap; }
.msg-row { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; max-width: 820px; margin: 0 auto 24px; }
.msg-row.user-line { align-items: flex-end; }
.message-author { display: flex; align-items: center; gap: 8px; min-height: 24px; margin-bottom: 4px; color: var(--secondary); font-size: 12px; font-weight: 600; }
@@ -162,7 +155,6 @@ body { padding: 20px; height: 100vh; 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; }
@@ -180,7 +172,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.attachment-file-icon { display: grid; place-items: center; flex-shrink: 0; width: 42px; height: 42px; font-size: 10px; font-weight: 650; border-radius: 7px; background: var(--brand-soft); color: var(--brand-text); }
.attachment-copy { flex: 1; min-width: 0; }
.attachment-copy strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 500; }
.attachment-copy small { display: block; font-size: 10px; color: var(--muted); margin-top: 2px; }
.attachment-remove { flex-shrink: 0; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 7px; color: var(--secondary); background: transparent; font-size: 21px; }
.attachment-remove:hover { color: var(--danger); background: var(--danger-soft); }
.composer-status { font-size: 12px; color: var(--secondary); padding: 7px 3px 0; }
@@ -192,7 +183,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.ai-welcome-card { padding-top: 0; }
.ai-avatar { width: 42px; height: 42px; margin-bottom: 12px; }
.welcome-eyebrow { margin-bottom: 6px; }
.ai-welcome-card p { margin-top: 8px; }
.suggest-grid { margin-top: 20px; gap: 10px; }
.suggest-item { min-height: 72px; padding: 13px 16px; }
.hot-questions { margin-top: 18px; }
@@ -205,7 +195,6 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.composer { width: calc(100% - 50px); }
.service-label { display: none; }
.suggest-item { padding: 13px; gap: 10px; }
.suggest-copy small { font-size: 11px; }
}
@media (max-width: 900px) {
body { padding: 0; position: fixed; inset: 0; width: 100%; overscroll-behavior: none; }
@@ -250,12 +239,10 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.ai-avatar { display: none; }
.ai-welcome-card { padding-top: 12px; }
.ai-welcome-card h3 { font-size: 23px; }
.ai-welcome-card p { max-width: 300px; margin: 10px auto 0; font-size: 12px; }
.suggest-grid { gap: 9px; margin-top: 22px; }
.suggest-item { flex-direction: column; align-items: flex-start; justify-content: flex-start; min-height: 114px; padding: 13px; gap: 9px; }
.suggest-item { flex-direction: column; align-items: flex-start; justify-content: flex-start; min-height: 88px; padding: 13px; gap: 9px; }
.suggest-icon { width: 32px; height: 32px; border-radius: 8px; }
.suggest-copy strong { font-size: 13px; }
.suggest-copy small { font-size: 11px; }
.suggest-arrow { display: none; }
.hot-questions { margin-top: 22px; }
.hot-questions .hot-capsule-wrap { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 3px; }
@@ -264,16 +251,12 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
#recentChatWrap { grid-template-columns: 1fr; gap: 0; }
.composer { width: calc(100% - 24px); padding-bottom: calc(10px + env(safe-area-inset-bottom)); }
.hot-bar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: 5px; }
.hot-bar-label { display: none; }
.hot-bar .hot-capsule { white-space: nowrap; flex-shrink: 0; padding: 6px 9px; font-size: 11px; }
.input-area { gap: 7px; padding: 7px; }
#send-button { min-width: 57px; padding: 0 12px; }
#upload-btn { flex-basis: 36px; }
#input-text { font-size: 16px; }
.attachment-remove { width: 40px; height: 40px; }
.keyboard-hint { display: none; }
.composer-footer { justify-content: center; padding-top: 7px; }
.disclaimer { font-size: 10px; }
.track-text { grid-template-columns: 1fr; gap: 2px; }
.track-place { grid-column: 1; }
.robot-message.track-card { padding: 16px; }
+9 -42
View File
@@ -142,9 +142,7 @@ uploadInput.onchange = function(){
copy.className = "attachment-copy";
const name = document.createElement("strong");
name.textContent = data.name;
const hint = document.createElement("small");
hint.textContent = t("attachmentReady");
copy.append(name, hint);
copy.append(name);
const remove = document.createElement("button");
remove.type = "button";
remove.className = "attachment-remove";
@@ -240,7 +238,7 @@ const I18N = {
thinking: "正在思考中...",
busyRetry: "服务繁忙,请稍后重试",
sessionFail: "会话创建失败,请确认服务已启动",
autoWelcome: "您好!欢迎咨询柏悦物流智能客服,可以咨询运费、时效或联系人工客服。订单查询将在客户账号登录并关联后开放。",
autoWelcome: "您好!欢迎咨询柏悦物流智能客服,可以咨询运费、时效或联系人工客服。",
humanHours: "人工客服工作时间:每天北京时间 8:00-22:00。当前为非工作时间,请留言,工作时间会尽快回复您。",
offlinePrompt: "请输入您的留言:",
offlineSaved: "留言已提交,工作时间(8:00-22:00)会尽快回复您。",
@@ -250,7 +248,6 @@ const I18N = {
humanWaiting: "正在排队,消息将由人工客服接入后回复。您也可以返回 AI 继续咨询。",
humanActive: "人工客服接待中,消息将发送给人工客服。",
returnAI: "返回 AI 问答",
humanSent: "已发送给人工客服",
humanSending: "正在发送给人工客服…",
humanSendFailed: "消息未发送成功,内容已保留,请重试。",
humanConnectFailed: "人工客服连接失败,请稍后重试。",
@@ -258,7 +255,6 @@ const I18N = {
humanReturnFailed: "暂时无法结束人工会话,请重试。",
historyLoading: "正在加载这段会话,请稍候…",
historyLoadFailed: "会话未加载成功,请重新选择该会话重试,或新建对话。",
draftRestored: "已恢复这段会话的未发送草稿。",
draftSaveFailed: "草稿暂时无法保存到当前浏览器,刷新前请保留输入内容。",
attachmentUnavailable: "图片暂时无法访问,请重新发送附件。",
recalled: "该消息已撤回",
@@ -268,7 +264,7 @@ const I18N = {
networkFail: "网络连接中断,请检查网络后重试",
stopped: "已停止生成",
stoppedMark: "(已停止生成)",
inputPh: "请输入问题、订单号或入仓码…(Enter 发送,Shift+Enter 换行)",
inputPh: "输入问题、订单号或入仓码…",
send: "发送",
stop: "停止",
copy: "复制",
@@ -280,17 +276,12 @@ const I18N = {
retryAIOnly: "请先返回 AI 问答,再重答这条消息。",
clickRetry: "点击重试",
welcomeTitle: "物流问题,随时为您解答",
welcomeSub: "咨询运费、时效或联系人工客服。订单查询需关联客户账号。",
welcomeEyebrow: "中欧 · 英国物流服务",
serviceScope: "中欧与英国专线咨询",
serviceTag: "物流咨询服务",
quickLabel: "快捷咨询",
footerTrack: "查订单",
footerQuote: "专线报价",
footerException: "异常处理",
footerHuman: "联系人工",
disclaimer: "报价与时效仅供参考,请以客服最终确认为准",
keyboardHint: "Enter 发送 · Shift + Enter 换行",
inputShort: "输入问题…",
uploading: "附件正在上传,请稍候…",
uploadFailed: "附件上传失败,请重新选择文件",
@@ -307,7 +298,6 @@ const I18N = {
voiceEmpty: "没有识别到文字,请靠近麦克风重试。", voiceFailed: "语音输入失败,请重试或使用文字输入。",
voiceTimeout: "语音处理超时,请重试。", voiceWait: "请等待当前操作完成后,再使用语音输入。",
voiceDraft: "输入框还有未发送内容,请先发送或清空后再使用语音。",
attachmentReady: "已准备好,发送后将附在消息中",
removeAttachment: "移除待发送附件",
you: "您",
assistant: "柏悦物流助手",
@@ -321,15 +311,12 @@ const I18N = {
logisticsHistory: "物流轨迹",
noLogisticsHistory: "暂无已发生的物流节点",
estimatedTracking: "预计节点",
legacyTrackingNotice: "历史模拟轨迹,仅供演示,不代表真实订单。",
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: ["关联客户账号后查询订单与轨迹", "了解线路、费用与预计时效", "咨询延误、签收或理赔问题", "进一步沟通您的物流需求"],
welcomeTip: "您可以直接输入问题,也可以点击下面选项快速提问。",
hot: "热点问题",
recent: "最近对话记录",
sideCollapse: "收起侧栏",
@@ -357,7 +344,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. Ask about rates, transit times or human support. Order lookup will be available after signing in and linking a customer account.",
autoWelcome: "Welcome to Baiyue Logistics. Ask about rates, transit times or human support.",
humanHours: "Our team is available daily, 08:00–22:00 China Standard Time (UTC+8). 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).",
@@ -367,7 +354,6 @@ const I18N = {
humanWaiting: "You are in the queue. A team member will reply after joining, or you can return to AI chat.",
humanActive: "You are chatting with our support team. Messages will be sent to a team member.",
returnAI: "Return to AI chat",
humanSent: "Sent to our support team",
humanSending: "Sending to our support team…",
humanSendFailed: "Message not sent. Your text has been kept. Please retry.",
humanConnectFailed: "Could not connect to our support team. Please try again.",
@@ -375,7 +361,6 @@ const I18N = {
humanReturnFailed: "Could not end the support session. Please try again.",
historyLoading: "Loading this conversation. Please wait…",
historyLoadFailed: "Conversation could not load. Select it again to retry, or start a new chat.",
draftRestored: "Your unsent draft has been restored.",
draftSaveFailed: "Your browser could not save this draft. Keep a copy before refreshing.",
attachmentUnavailable: "Image unavailable. Please upload the attachment again.",
recalled: "This message was recalled",
@@ -385,7 +370,7 @@ const I18N = {
networkFail: "Connection lost, please check your network and retry",
stopped: "Stopped",
stoppedMark: "(Generation stopped)",
inputPh: "Ask a question or enter a tracking number... (Enter to send, Shift+Enter for newline)",
inputPh: "Ask a question or enter an order number…",
send: "Send",
stop: "Stop",
copy: "Copy",
@@ -397,17 +382,12 @@ const I18N = {
retryAIOnly: "Return to AI chat before regenerating this answer.",
clickRetry: "Retry",
welcomeTitle: "Your next shipment, made clearer.",
welcomeSub: "Ask about rates, transit times or human support. Order lookup requires a linked customer account.",
welcomeEyebrow: "EUROPE & UK LOGISTICS",
serviceScope: "Europe & UK freight support",
serviceTag: "Logistics support",
quickLabel: "Quick help",
footerTrack: "Order lookup",
footerQuote: "Get a quote",
footerException: "Parcel issue",
footerHuman: "Human support",
disclaimer: "Quotes and transit times are subject to final confirmation",
keyboardHint: "Enter to send · Shift + Enter for a new line",
inputShort: "Message…",
uploading: "Uploading your attachment…",
uploadFailed: "Upload failed. Please select the file again.",
@@ -426,7 +406,6 @@ const I18N = {
voiceFailed: "Voice input failed. Please try again or type your message.", voiceTimeout: "Voice input timed out. Please try again.",
voiceWait: "Please wait for the current operation to finish before recording.",
voiceDraft: "Please send or clear your existing draft before recording.",
attachmentReady: "Ready to attach to your next message",
removeAttachment: "Remove pending attachment",
you: "You",
assistant: "Baiyue Assistant",
@@ -440,15 +419,12 @@ const I18N = {
logisticsHistory: "Shipment tracking",
noLogisticsHistory: "No actual shipment events available",
estimatedTracking: "Estimated milestones",
legacyTrackingNotice: "Historical simulated tracking for demonstration only; this is not a real order record.",
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: ["Link a customer account to view orders and tracking", "Explore routes, rates and transit times", "Get help with delays, delivery or claims", "Discuss your shipping requirements"],
welcomeTip: "Type your question, or pick a quick option below.",
hot: "Popular Questions",
recent: "Recent Conversations",
sideCollapse: "Collapse sidebar",
@@ -629,7 +605,7 @@ function welcomeHTML(){
const kinds = ["track", "quote", "issue", "human"];
const cards = q.map(function(question, index){
const action = index === 3 ? 'data-human-support' : `data-question="${escHtml(question)}"`;
return `<button type="button" class="suggest-item" ${action}><span class="suggest-icon">${welcomeIcon(kinds[index])}</span><span class="suggest-copy"><strong>${question}</strong><small>${t("quickSubs")[index]}</small></span><span class="suggest-arrow" aria-hidden="true">↗</span></button>`;
return `<button type="button" class="suggest-item" ${action}><span class="suggest-icon">${welcomeIcon(kinds[index])}</span><span class="suggest-copy"><strong>${question}</strong></span><span class="suggest-arrow" aria-hidden="true">↗</span></button>`;
}).join("");
return `
<div class="ai-welcome-card">
@@ -637,7 +613,6 @@ function welcomeHTML(){
<div class="ai-avatar">${welcomeIcon("box")}</div>
<div class="welcome-eyebrow">${t("welcomeEyebrow")}</div>
<h3>${t("welcomeTitle")}</h3>
<p>${t("welcomeSub")}</p>
</div>
<div class="suggest-grid">${cards}</div>
</div>
@@ -690,7 +665,7 @@ newChatBtn.onclick = function(){
if(alreadyNew) inputDom.value = "";
autoGrow();
clearAttachment();
setComposerStatus(inputDom.value ? t('draftRestored') : '');
setComposerStatus('');
chatDom.innerHTML = welcomeHTML();
chatDom.scrollTop = 0;
followLatest = true;
@@ -904,7 +879,7 @@ function openConversation(id){
if(version !== conversationVersion) return;
if(!Array.isArray(msgs)) throw new Error('History failed');
openingConv = null;
setComposerStatus(inputDom.value ? t('draftRestored') : '');
setComposerStatus('');
chatDom.innerHTML = "";
if(!msgs.length){
chatDom.innerHTML = welcomeHTML();
@@ -952,14 +927,6 @@ 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 fields = document.createElement('dl');
fields.className = 'order-data-fields is-collapsed';
@@ -1242,7 +1209,7 @@ async function sendHumanMessage(content){
addMsg(content, true);
if(inputDom.value === draft){ inputDom.value = ''; autoGrow(); }
if(window.__pendingImg === attachment) clearAttachment();
setComposerStatus(t(humanStatus === 'active' ? 'humanSent' : 'humanWaiting'));
setComposerStatus('');
focusComposer();
return true;
}catch(error){
-4
View File
@@ -22,7 +22,6 @@
<div class="history-wrap" id="historyWrap"></div>
<div class="sidebar-footer">
<button type="button" class="clear-all-btn">清空全部历史</button>
<div class="sidebar-note"><span class="status-dot" aria-hidden="true"></span><span data-i18n="serviceScope">中欧与英国专线咨询</span></div>
</div>
</aside>
<button class="sidebar-backdrop" type="button" aria-label="关闭历史侧栏"></button>
@@ -44,14 +43,12 @@
<div class="ai-avatar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 7 8-4 8 4v10l-8 4-8-4Z"/><path d="m4 7 8 4 8-4M12 11v10M8 5l8 4"/></svg></div>
<div class="welcome-eyebrow">中欧 · 英国物流服务</div>
<h3>物流问题,随时为您解答</h3>
<p>咨询运费、时效或联系人工客服。<br>订单查询需登录并关联客户账号。</p>
</div>
</section>
</div>
<footer class="composer">
<div id="human-session" class="human-session" role="status" hidden><span id="human-session-label"></span><button id="return-ai" type="button">返回 AI 问答</button></div>
<div class="composer-toolbar"><div class="hot-bar">
<span class="hot-bar-label" data-i18n="quickLabel">快捷咨询</span>
<button type="button" class="hot-capsule" data-question-index="0" data-i18n="footerTrack">查运单</button>
<button type="button" class="hot-capsule" data-question-index="1" data-i18n="footerQuote">专线报价</button>
<button type="button" class="hot-capsule" data-question-index="2" data-i18n="footerException">异常处理</button>
@@ -66,7 +63,6 @@
<button type="button" id="send-button">发送</button>
</div>
<p id="composer-status" class="composer-status" role="status" hidden></p>
<div class="composer-footer"><span class="disclaimer" data-i18n="disclaimer">AI 回答仅供参考,具体以人工客服回复为准</span><span class="keyboard-hint" data-i18n="keyboardHint">Enter 发送 · Shift + Enter 换行</span></div>
</footer>
<div id="voice-panel" class="voice-panel" role="status" aria-live="polite" hidden><span class="voice-wave" aria-hidden="true">▂ ▆ █ ▆ ▂</span><p id="voice-label"></p><span id="voice-clock"></span></div>
<button type="button" id="to-bottom" class="to-bottom" title="回到底部" aria-label="回到底部">↓</button>
+3 -2
View File
@@ -926,12 +926,13 @@ test('a new conversation draft restores after refresh and moves to the created c
assert.equal(store.read(null),'');assert.equal(store.read(1),'');assert.equal(store.active(),'1');
});
test('quota failure replaces a restored-draft notice and clears after storage recovers', async () => {
test('draft recovery stays silent but quota failure remains visible until storage recovers', async () => {
const history=[{id:9,title:'A',ctime:'now'}],app=await customerPage({human:false,history});
await app.click(app.element('historyWrap').children[0]);
app.element('input-text').value='已保存草稿';app.element('input-text').listeners.input();await app.pageHide();
const restored=await customerPage({human:false,history,storage:app.storage,tabStorage:app.tabStorage});
assert.match(restored.element('composer-status').textContent,/已恢复/);
assert.equal(restored.element('input-text').value,'已保存草稿');
assert.equal(restored.element('composer-status').textContent,'');
const setItem=restored.context.localStorage.setItem;
restored.context.localStorage.setItem=()=>{throw Error('quota');};
restored.element('input-text').value='新的未保存输入';restored.element('input-text').listeners.input();await restored.runTimers(250);