diff --git a/AGENTS.md b/AGENTS.md
index 8711274..b300ea8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,7 +9,7 @@ Use `am.py` for CLI/WSGI, `logistics_chat/` for business code, and `static/css/`
| AI | `logistics_chat/chat_service.py`: `create_chat_stream`; `logistics_chat/ai_client.py`: `post_completion` |
| Voice input | `static/js/voice.js`: `ChatVoice.create`; `logistics_chat/speech_service.py`: `transcribe_recording`, `baidu_transcribe`; `logistics_chat/routes/speech.py` |
| Guest identity | `static/js/visitor.js`: bootstrap and cookie recovery; `logistics_chat/visitor_service.py`: owner checks; `routes/visitor.py`: customer human support |
-| Customer login / overview | `logistics_chat/customer_service.py`: sessions, default customer bootstrap, order conversation binding; `routes/customer.py`; `static/js/customer.js`: login and logout; `static/js/overview.js`: automatic scoped overview; `order_listing.py`: `read_overview` |
+| Customer login / overview | `logistics_chat/customer_service.py`: sessions, default customer bootstrap, order conversation binding; `routes/customer.py`; `static/js/customer.js`: login and logout; `order_listing.py`: `read_overview`, `answer_overview` for chat replies |
| Order lookup UI | `static/js/order_lookup.js`: `OrderLookup.create`; `chat.js`: `showOrderLookup`, `fillTrackCard`; `support.js`: `copyText` |
| Chat drafts | `static/js/drafts.js`: `ChatDrafts.create`; `chat.js`: `flushDraft`, `restoreDraft`, startup recovery |
| Knowledge | `logistics_chat/knowledge.py`: `knowledge_list`, `search_knowledge`; `docs/knowledge.md` |
diff --git a/docs/architecture.md b/docs/architecture.md
index 935f7d5..41d0fb3 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -153,7 +153,7 @@ tests/ # 按业务模块组织的回归
`order_test_data.exception_reason` 是人工明确登记、允许客户查看的物流异常原因;非空表示该票有异常标记,空值表示未标记,不推断货物正常。取消、审核驳回和无轨迹均不自动标为物流异常。可直接维护此字段,不增加管理页面;旧快照未提供该字段时即使显式覆盖也保留已有原因,显式提供空字符串才清除。后续MCP适配返回相同事实即可。
-登录成功或恢复有效登录时,`overview.js` 自动请求既有 `/api/customer/session?overview=1` 并展开可收起的订单概览。普通会话状态查询保持原响应契约,游客响应不含订单事实。`read_overview` 复用 `scoped_orders`,在同一只读快照内计算全部订单、业务状态 PROCESSING 的履约中订单、明确异常数量和履约中订单的实际节点分布。最多5条重点订单优先列异常,其余按更新时间列履约中订单。错误状态保留重试入口,退出或换账号立即清空概览,迟到响应受请求版本和客户身份约束。概览查询不创建聊天消息,查看计数及入仓码复用现有查询入口。
+用户询问“订单概览”“我的订单情况”时,`answer_overview` 在现有聊天流中回复,沿用订单事实的登录校验与会话保存。`read_overview` 复用 `scoped_orders`,在同一只读快照内计算全部订单、业务状态 PROCESSING 的履约中订单、明确异常数量和履约中订单的实际节点分布。最多5条重点订单优先列异常,其余按更新时间列履约中订单。结果仅出现在聊天消息及对应历史中,登录和恢复登录只更新身份状态。
语音反馈使用输入框上方的绝对定位浮层,不占输入区布局高度,4秒后收起。切换对话清除反馈,同一手势失败后须松开才能重新录音,Pointer、Touch 与键盘入口遵循同一规则。
diff --git a/logistics_chat/customer_service.py b/logistics_chat/customer_service.py
index 56f64bf..331a203 100644
--- a/logistics_chat/customer_service.py
+++ b/logistics_chat/customer_service.py
@@ -3,7 +3,6 @@
import hashlib
import json
import secrets
-import sqlite3
import time
from flask import g, has_app_context, jsonify, request
@@ -135,20 +134,8 @@ def bind_order_conversation(conv_id):
connection.close()
-def session_status(include_overview=False):
- result={'ok':True, 'authenticated':bool(g.customer), 'user':g.customer}
- if include_overview and g.customer:
- from .visitor_service import customer_tenant_ids
- from .order_listing import read_overview
- tenants=customer_tenant_ids()
- if not tenants:
- result['overview_error']='unlinked'
- else:
- try:
- result['overview']=read_overview(tenants)
- except sqlite3.Error:
- result['overview_error']='unavailable'
- return result
+def session_status():
+ return {'ok':True, 'authenticated':bool(g.customer), 'user':g.customer}
def order_login_response():
diff --git a/logistics_chat/order_data.py b/logistics_chat/order_data.py
index ccdbf71..f5abab3 100644
--- a/logistics_chat/order_data.py
+++ b/logistics_chat/order_data.py
@@ -335,6 +335,9 @@ def answer_query(message, conv_id):
tenants = customer_tenant_ids()
from .order_explanations import general_explanation, detail_question, explain_order
if not references:
+ from .order_listing import answer_overview
+ overview=answer_overview(message,tenants)
+ if overview is not None:return overview
explanation=general_explanation(message)
if explanation:return {'t':explanation}
count_intent = parse_count_query(message) if not references else None
diff --git a/logistics_chat/order_listing.py b/logistics_chat/order_listing.py
index bba5556..c189536 100644
--- a/logistics_chat/order_listing.py
+++ b/logistics_chat/order_listing.py
@@ -112,6 +112,29 @@ def read_overview(tenants):
connection.close()
+def answer_overview(message,tenants):
+ question=re.sub(r'[\s,,。.!!??]','',message)
+ chinese=re.fullmatch(r'(?:请|帮我|请帮我)?(?:看一下|看看|查看|查询|显示)?(?:一下)?'
+ r'(?:(?:我|当前账号)的?|当前|现在)?(?:订单|运单)(?:概览|概况|总体情况|整体情况|情况|汇总)(?:呢|吧|吗)?',question)
+ english=re.fullmatch(r'(?:please\s+)?(?:(?:show|give)\s+(?:me\s+)?)?(?:my\s+)?(?:order|shipment)s?\s+(?:overview|summary)',message.strip(' .!?'),re.I)
+ if not chinese and not english:return None
+ if not tenants:
+ from .customer_service import order_login_response
+ return order_login_response()
+ try:
+ data=read_overview(tenants)
+ except sqlite3.Error:
+ return {'t':'订单信息暂时无法读取,请稍后重试。'}
+ lines=[f'您当前共有 {data["total"]} 票订单,履约中 {data["processing"]} 票,异常订单 {data["exceptions"]} 票。']
+ if data['stages']:
+ lines.append('履约中订单的当前实际节点:')
+ lines.extend(f'- {STAGE_LABELS[stage["code"]]}:{stage["count"]}票' for stage in data['stages'])
+ if data['orders']:
+ lines.append('重点订单(异常优先,最多5票):')
+ lines.extend(row_text(order) for order in data['orders'])
+ return {'t':'\n'.join(lines),'_customer_data':True}
+
+
def read_orders(tenants,query,page=1,size=5):
"""One scoped snapshot; each order occupies one actual-node bucket."""
if not valid_filters(query):raise ValueError('Explicit filters required')
diff --git a/logistics_chat/routes/customer.py b/logistics_chat/routes/customer.py
index ddd90db..1bd8546 100644
--- a/logistics_chat/routes/customer.py
+++ b/logistics_chat/routes/customer.py
@@ -9,7 +9,7 @@ bp = Blueprint('customer', __name__)
@bp.get('/api/customer/session')
def session():
- return customer_service.session_status(include_overview=request.args.get('overview')=='1')
+ return customer_service.session_status()
@bp.post('/api/customer/login')
diff --git a/static/css/chat.css b/static/css/chat.css
index 6ed07c7..26b5684 100644
--- a/static/css/chat.css
+++ b/static/css/chat.css
@@ -305,82 +305,3 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
.msg-action-btn { min-height: 40px; }
.to-bottom { right: 15px; }
}
-.customer-overview {
- flex-shrink: 0;
- max-height: 36%;
- margin: 12px 32px 0;
- overflow-y: auto;
- border: 1px solid var(--border);
- border-radius: 12px;
- background: var(--surface);
-}
-.customer-overview > summary {
- display: flex;
- align-items: center;
- gap: 12px;
- position: sticky;
- top: 0;
- z-index: 1;
- min-height: 44px;
- padding: 10px 16px;
- background: var(--surface);
- cursor: pointer;
- list-style: none;
-}
-.customer-overview > summary::-webkit-details-marker { display: none; }
-.customer-overview > summary::after { content: '⌄'; color: var(--muted); }
-.customer-overview[open] > summary::after { transform: rotate(180deg); }
-#customer-overview-title { flex-shrink: 0; font-weight: 650; }
-#customer-overview-summary {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- text-align: right;
- font-size: 12px;
- color: var(--secondary);
-}
-.customer-overview-body { padding: 0 16px 12px; }
-.overview-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
-.overview-metric {
- display: flex;
- flex-direction: column;
- min-width: 0;
- padding: 8px;
- border: 0;
- border-radius: 8px;
- background: var(--surface-soft);
- color: var(--text);
- text-align: left;
-}
-.overview-metric strong { font-size: 21px; line-height: 1.3; }
-.overview-metric span { color: var(--secondary); font-size: 12px; }
-.overview-metric:last-child strong { color: var(--danger); }
-.overview-stages { display: flex; flex-wrap: wrap; gap: 6px 12px; margin: 10px 0; color: var(--secondary); font-size: 12px; }
-.overview-orders { display: grid; gap: 6px; }
-.overview-order {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 2px 10px;
- min-width: 0;
- min-height: 44px;
- padding: 8px 10px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: var(--surface);
- color: var(--text);
- text-align: left;
-}
-.overview-reference { overflow-wrap: anywhere; font-size: 13px; }
-.overview-stage { max-width: 180px; color: var(--secondary); font-size: 12px; text-align: right; }
-.overview-reason { grid-column: 1 / -1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--danger); font-size: 12px; }
-.overview-order.has-exception { border-left: 3px solid var(--danger); }
-.overview-refresh { margin-top: 8px; padding: 4px 0; border: 0; background: transparent; color: var(--brand-text); font-size: 12px; }
-.overview-empty { color: var(--secondary); font-size: 13px; }
-@media (max-width: 720px) {
- .customer-overview { max-height: 32%; margin: 8px 12px 0; }
- .customer-overview > summary { padding: 9px 12px; gap: 8px; }
- .customer-overview-body { padding: 0 12px 10px; }
- .overview-stage { max-width: 120px; }
-}
diff --git a/static/js/chat.js b/static/js/chat.js
index e6c985f..b467668 100644
--- a/static/js/chat.js
+++ b/static/js/chat.js
@@ -43,7 +43,6 @@ let historyRequestVersion = 0;
let followLatest = true;
let voiceInput = null;
let customerLogin = null;
-let customerOverview = null;
let orderLookup = null;
let lookupRequestVersion = 0;
let customerRecovery = null;
@@ -314,33 +313,6 @@ const I18N = {
footerQuote: "美国报价",
footerException: "异常订单",
exceptionQuery: "我有多少个异常订单?",
- overviewTitle: "订单概览",
- overviewTotal: "全部订单",
- overviewProcessing: "履约中",
- overviewExceptions: "异常订单",
- overviewLoading: "正在查询订单…",
- overviewUnlinked: "当前账号尚未关联订单",
- overviewFailed: "订单概览暂时无法加载",
- overviewRefresh: "刷新概览",
- overviewEmpty: "当前没有履约中或已标记异常的订单。",
- overviewAllQuery: "列出我的全部订单",
- overviewProcessingQuery: "列出履约中的订单",
- overviewStageLabels: {
- DOMESTIC_RECEIVED: "已入国内仓",
- LOADING_COMPLETED: "已完成装柜",
- DEPARTED_WAREHOUSE: "已离开国内仓库",
- AT_ORIGIN_PORT: "已进入起运港",
- LOADED_ON_VESSEL: "已装船",
- DEPARTED_ORIGIN_PORT: "已开船",
- IN_TRANSIT: "海运途中",
- ARRIVED_DESTINATION_PORT: "已到目的港",
- DISCHARGED_FROM_VESSEL: "已卸船、未清关",
- PICKUP_READY: "已清关、可提柜",
- PICKED_UP: "已提柜",
- EMPTY_RETURNED: "已还柜",
- NO_ACTUAL: "暂无实际节点",
- MIXED: "多柜进度不同"
- },
footerHuman: "联系人工",
inputShort: "输入入仓码…",
uploading: "附件正在上传,请稍候…",
@@ -458,33 +430,6 @@ const I18N = {
footerQuote: "US quote",
footerException: "Exception orders",
exceptionQuery: "How many exception orders do I have?",
- overviewTitle: "Order overview",
- overviewTotal: "All orders",
- overviewProcessing: "In progress",
- overviewExceptions: "Exceptions",
- overviewLoading: "Loading orders…",
- overviewUnlinked: "No orders are linked to this account",
- overviewFailed: "Order overview could not load",
- overviewRefresh: "Refresh overview",
- overviewEmpty: "No orders are in progress or flagged with an exception.",
- overviewAllQuery: "List all my orders",
- overviewProcessingQuery: "List my processing orders",
- overviewStageLabels: {
- DOMESTIC_RECEIVED: "Received at origin warehouse",
- LOADING_COMPLETED: "Container loaded",
- DEPARTED_WAREHOUSE: "Left origin warehouse",
- AT_ORIGIN_PORT: "At origin port",
- LOADED_ON_VESSEL: "Loaded on vessel",
- DEPARTED_ORIGIN_PORT: "Vessel departed",
- IN_TRANSIT: "In transit",
- ARRIVED_DESTINATION_PORT: "At destination port",
- DISCHARGED_FROM_VESSEL: "Discharged, awaiting clearance",
- PICKUP_READY: "Cleared, ready for pickup",
- PICKED_UP: "Container picked up",
- EMPTY_RETURNED: "Empty container returned",
- NO_ACTUAL: "No actual tracking events",
- MIXED: "Containers at different stages"
- },
footerHuman: "Human support",
inputShort: "Inbound code…",
uploading: "Uploading your attachment…",
@@ -581,7 +526,6 @@ function applyLanguage(lang){
if(tg) tg.title = tg.getAttribute('aria-expanded') === 'false' ? dict.sideExpand : dict.sideCollapse;
updateHumanMode();
if(voiceInput) voiceInput.refresh();
- if(customerOverview) customerOverview.render();
// 欢迎页(若当前显示的是欢迎页则重绘)
if(chatDom.querySelector(".ai-welcome-card")){
@@ -2129,15 +2073,9 @@ voiceInput = ChatVoice.create({
if(!await doSend()) voiceInput.setMode(false);
}
});
-customerOverview = CustomerOverview.create({fetch:fetch,text:t,
- onQuery:function(question){window.askNewTopic(question);},
- onLookup:function(order,button){showOrderLookup(order.inbound_code || order.order_no,order.inbound_code ? 'inbound' : 'order',button);},
- onExpired:function(){if(customerLogin) customerLogin.refresh();}
-});
customerLogin = CustomerLogin.create({fetch:fetch, text:t,
onOpen:function(){ if(voiceInput) voiceInput.cancel(); if(isNarrow()){sideOpen=false;renderSidebar();} syncChatViewport(); },
onChange:function(user,previous){
- customerOverview.setUser(user);
if(previous === undefined) return;
if(previous && (!user || user.id !== previous.id)){
const from = conversationVersion;
diff --git a/static/js/overview.js b/static/js/overview.js
deleted file mode 100644
index b3f41c8..0000000
--- a/static/js/overview.js
+++ /dev/null
@@ -1,130 +0,0 @@
-// Display a fresh, customer-scoped order overview after login or session recovery.
-var CustomerOverview = {
- create(options){
- const panel=document.getElementById('customer-overview');
- const title=document.getElementById('customer-overview-title');
- const summary=document.getElementById('customer-overview-summary');
- const body=document.getElementById('customer-overview-body');
- let user=null;
- let overview=null;
- let failure='';
- let loading=false;
- let requestVersion=0;
- let controller=null;
- panel.hidden=true;
-
- function element(tag,className,text){
- const node=document.createElement(tag);
- if(className) node.className=className;
- if(text !== undefined) node.textContent=text;
- return node;
- }
-
- function queryButton(label,question,className){
- const button=element('button',className,label);
- button.type='button';
- button.onclick=function(){
- panel.open=false;
- options.onQuery(question);
- };
- return button;
- }
-
- function render(){
- title.textContent=options.text('overviewTitle');
- body.innerHTML='';
- panel.hidden=!user;
- if(!user){summary.textContent='';return;}
- if(!overview){
- const key=loading ? 'overviewLoading' : failure==='unlinked' ? 'overviewUnlinked' : 'overviewFailed';
- summary.textContent=options.text(key);
- }else{
- summary.textContent=options.text('overviewProcessing')+' '+overview.processing+' · '+options.text('overviewExceptions')+' '+overview.exceptions;
- const metrics=element('div','overview-metrics');
- for(const [key,count,question] of [
- ['overviewTotal',overview.total,options.text('overviewAllQuery')],
- ['overviewProcessing',overview.processing,options.text('overviewProcessingQuery')],
- ['overviewExceptions',overview.exceptions,options.text('exceptionQuery')]
- ]){
- const button=queryButton('',question,'overview-metric');
- button.append(element('strong','',String(count)),element('span','',options.text(key)));
- metrics.appendChild(button);
- }
- body.appendChild(metrics);
- const stages=element('div','overview-stages');
- const labels=options.text('overviewStageLabels');
- for(const stage of overview.stages){
- stages.appendChild(element('span','',labels[stage.code]+' '+stage.count));
- }
- body.appendChild(stages);
- const orders=element('div','overview-orders');
- for(const order of overview.orders){
- const button=element('button','overview-order');
- button.type='button';
- const reference=order.inbound_code || order.order_no;
- button.appendChild(element('strong','overview-reference',reference));
- button.appendChild(element('span','overview-stage',labels[order.stage]));
- if(order.exception_reason){
- button.classList.add('has-exception');
- button.appendChild(element('span','overview-reason',order.exception_reason));
- }
- button.onclick=function(){options.onLookup(order,button);};
- orders.appendChild(button);
- }
- if(!overview.orders.length) orders.appendChild(element('p','overview-empty',options.text('overviewEmpty')));
- body.appendChild(orders);
- }
- const refresh=element('button','overview-refresh',options.text(loading ? 'overviewLoading' : 'overviewRefresh'));
- refresh.type='button';
- refresh.disabled=loading;
- refresh.onclick=refreshOverview;
- body.appendChild(refresh);
- }
-
- async function refreshOverview(){
- if(!user || loading) return;
- const token=++requestVersion;
- const customerId=user.id;
- controller=new AbortController();
- const activeController=controller;
- const timer=setTimeout(()=>activeController.abort(),10000);
- loading=true;
- failure='';
- render();
- try{
- const response=await options.fetch('/api/customer/session?overview=1',{signal:activeController.signal});
- const result=await response.json();
- if(token!==requestVersion || !user || user.id!==customerId) return;
- if(!response.ok || !result.ok) throw new Error('Overview unavailable');
- if(!result.authenticated || !result.user || result.user.id!==customerId){
- setUser(null);
- if(options.onExpired) options.onExpired();
- return;
- }
- overview=result.overview || null;
- failure=result.overview_error || (overview ? '' : 'unavailable');
- }catch(error){
- if(token===requestVersion){overview=null;failure='unavailable';}
- }finally{
- clearTimeout(timer);
- if(token===requestVersion){controller=null;loading=false;render();}
- }
- }
-
- function setUser(next){
- if(user && next && user.id===next.id) return;
- requestVersion+=1;
- if(controller) controller.abort();
- controller=null;
- user=next;
- overview=null;
- failure='';
- loading=false;
- panel.open=Boolean(user);
- render();
- if(user) refreshOverview();
- }
-
- return {setUser:setUser,render:render};
- }
-};
diff --git a/templates/index.html b/templates/index.html
index 8bcd534..ed2900f 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -38,10 +38,6 @@
- 订单概览
-
-