diff --git a/app/api/chat.py b/app/api/chat.py
index 597cf17..2952da1 100644
--- a/app/api/chat.py
+++ b/app/api/chat.py
@@ -327,17 +327,38 @@ def list_sessions_api(
request: Request,
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
+ status: str | None = Query(
+ None,
+ description="按状态筛选:active / closed;缺省返回全部",
+ pattern="^(active|closed)$",
+ ),
auth: AuthContext = Depends(get_auth_context),
) -> dict:
"""当前登录人的会话列表(created_at 倒序分页;agent_type 经 X-Agent-Type 头指定)。"""
agent_type = _resolve_agent_type(request)
_assert_chat_entry(auth, agent_type)
items, total = _session_repo().list_sessions(
- actor_id=auth.actor_id, agent_type=agent_type, limit=limit, offset=offset
+ actor_id=auth.actor_id,
+ agent_type=agent_type,
+ limit=limit,
+ offset=offset,
+ status=status,
)
return {"items": items, "total": total, "limit": limit, "offset": offset}
+@router.post("/sessions/close-all")
+def close_all_sessions_api(
+ request: Request,
+ auth: AuthContext = Depends(get_auth_context),
+) -> dict:
+ """关闭当前 actor 在本 Agent 线上的全部 active 会话(不清消息,仅侧栏不可续聊)。"""
+ agent_type = _resolve_agent_type(request)
+ _assert_chat_entry(auth, agent_type)
+ closed = _session_repo().close_all_active(actor_id=auth.actor_id, agent_type=agent_type)
+ return {"closed_count": closed, "agent_type": agent_type}
+
+
@router.get("/sessions/{session_id}/messages")
def list_messages_api(
session_id: str,
diff --git a/app/config/database.py b/app/config/database.py
index 24f6cbf..2ea3dde 100644
--- a/app/config/database.py
+++ b/app/config/database.py
@@ -33,8 +33,8 @@ def _redis_kwargs() -> dict:
return {
"decode_responses": True,
"protocol": 2,
- "socket_timeout": 5,
- "socket_connect_timeout": 3,
+ "socket_timeout": 1,
+ "socket_connect_timeout": 0.5,
}
diff --git a/app/repository/session_repository.py b/app/repository/session_repository.py
index 8db1738..01c5974 100644
--- a/app/repository/session_repository.py
+++ b/app/repository/session_repository.py
@@ -34,20 +34,37 @@ class SessionRepository:
return dict(row) if row else None
def list_sessions(
- self, *, actor_id: str, agent_type: str, limit: int = 20, offset: int = 0
+ self,
+ *,
+ actor_id: str,
+ agent_type: str,
+ limit: int = 20,
+ offset: int = 0,
+ status: str | None = None,
) -> tuple[list[dict[str, Any]], int]:
"""前端会话列表(方案 B):仅本人 + 本 Agent 线,created_at 倒序分页。
返回 (items, total);total 供前端分页器。id 倒序兜底同秒并发建的
会话排序稳定(created_at 精度秒级时并列)。datetime 统一转 str——
sqlite 返 str、MySQL 返 datetime,响应体跨库同构。
+
+ status 可选:active / closed;缺省返回全部状态(兼容旧客户端)。
"""
where = "WHERE actor_id = :actor AND agent_type = :atype"
+ params: dict[str, Any] = {
+ "actor": actor_id,
+ "atype": agent_type,
+ "lim": limit,
+ "off": offset,
+ }
+ if status is not None:
+ where += " AND status = :status"
+ params["status"] = status
with self._engine.connect() as conn:
total = int(
conn.execute(
text(f"SELECT COUNT(*) FROM agent_session {where}"),
- {"actor": actor_id, "atype": agent_type},
+ params,
).scalar_one()
)
rows = conn.execute(
@@ -60,7 +77,7 @@ class SessionRepository:
LIMIT :lim OFFSET :off
"""
),
- {"actor": actor_id, "atype": agent_type, "lim": limit, "off": offset},
+ params,
).mappings().all()
items = [
{
@@ -93,6 +110,18 @@ class SessionRepository:
)
return result.rowcount > 0
+ def close_all_active(self, *, actor_id: str, agent_type: str) -> int:
+ """关闭该 actor 在某 Agent 线上的全部 active 会话(前端「清空历史」)。"""
+ with self._engine.begin() as conn:
+ result = conn.execute(
+ text(
+ "UPDATE agent_session SET status = 'closed', closed_at = CURRENT_TIMESTAMP"
+ " WHERE actor_id = :actor AND agent_type = :atype AND status = 'active'"
+ ),
+ {"actor": actor_id, "atype": agent_type},
+ )
+ return int(result.rowcount)
+
def create_session(
self,
*,
diff --git a/app/service/customer_prompts.py b/app/service/customer_prompts.py
index 8726ea8..9902e39 100644
--- a/app/service/customer_prompts.py
+++ b/app/service/customer_prompts.py
@@ -133,6 +133,12 @@ _SAVE_NOTE_KW = (
"帮我记住", "你要记",
)
+# 问候/闲聊快路由(不调 LLM;避免 DeepSeek 不可用时「你好」落入 fallback)
+_GREETING_KW = (
+ "你好", "您好", "你好呀", "您好呀", "嗨", "hello", "hi",
+ "在吗", "在不在", "早上好", "下午好", "晚上好", "哈喽",
+)
+
def keyword_route(message: str) -> tuple[str, str] | None:
"""关键词快速路由。返回 (intent, preset_reply);无命中返回 None。
@@ -144,6 +150,10 @@ def keyword_route(message: str) -> tuple[str, str] | None:
if any(k in msg for k in _TRANSFER_KEYWORDS):
return ("transfer_human", "")
+ stripped = msg.strip().lower()
+ if stripped in {"hi", "hello"} or any(k in msg for k in _GREETING_KW):
+ return ("chit_chat", "")
+
# 拒绝类(边界外请求)
if any(k in msg for k in _REJECT_ADVICE_KW):
return ("reject", "advice")
@@ -377,6 +387,12 @@ FALLBACK_TEXT = (
"3. 拨打客服热线 95XXX 或回复「转人工」"
)
+# LLM 不可用时的闲聊降级(仍走 chit_chat 意图,避免误用 FALLBACK 话术)
+CHITCHAT_DEGRADED_TEXT = (
+ "您好,我是您的财富助手。您可以问我「我的持仓」「最近流水」或「我能买什么产品」,"
+ "也可以直接描述您的问题。"
+)
+
# 数据查询工具调用失败时的兜底
DATA_ERROR_TEXT = "暂时无法查询您的数据,请稍后再试,或回复「转人工」联系客服人员。"
diff --git a/app/service/customer_service.py b/app/service/customer_service.py
index 2f9f1f0..4bacf73 100644
--- a/app/service/customer_service.py
+++ b/app/service/customer_service.py
@@ -24,6 +24,7 @@ from langgraph.graph import END, StateGraph
from app.config.settings import settings
from app.model.schemas import AuthContext
from app.service.customer_prompts import (
+ CHITCHAT_DEGRADED_TEXT,
CHITCHAT_SYSTEM,
CHITCHAT_USER_TEMPLATE,
DATA_ERROR_TEXT,
@@ -351,9 +352,9 @@ def chitchat(state: CustomerState) -> CustomerState:
)
reply = content.strip()
if not reply:
- return {"reply": FALLBACK_TEXT, "intent": "fallback"}
+ return {"reply": CHITCHAT_DEGRADED_TEXT, "intent": "chit_chat"}
except Exception:
- return {"reply": FALLBACK_TEXT, "intent": "fallback"}
+ return {"reply": CHITCHAT_DEGRADED_TEXT, "intent": "chit_chat"}
reply, need_transfer = finalize_sanitized_reply(reply, intent=state.get("intent"))
if need_transfer:
diff --git a/app/service/visitor_prompts.py b/app/service/visitor_prompts.py
index dec8552..5436c1d 100644
--- a/app/service/visitor_prompts.py
+++ b/app/service/visitor_prompts.py
@@ -89,3 +89,7 @@ FALLBACK_TEXT = (
"3. 拨打客服热线 95XXX\n"
'4. 回复"转人工"联系客服人员'
)
+
+CHITCHAT_DEGRADED_TEXT = (
+ "您好,欢迎咨询。您可以问我产品规则、开户流程等,或回复「转人工」联系客服。"
+)
diff --git a/app/service/visitor_service.py b/app/service/visitor_service.py
index 3909cd4..950bac7 100644
--- a/app/service/visitor_service.py
+++ b/app/service/visitor_service.py
@@ -101,6 +101,35 @@ _REJECT_REALTIME_KW = (
"当前价格", "最新行情",
)
+# FAQ 关键词:LLM 分类失败时仍可走 RAG(开户/账户操作类)
+_FAQ_KEYWORDS = (
+ "开户", "开账户", "办理开户", "开户资料", "开户材料",
+ "需要什么资料", "需要哪些资料", "需要什么材料", "需要哪些材料",
+ "身份认证", "实名认证", "风险测评", "风评问卷", "忘记密码", "重置密码",
+)
+
+
+def _visitor_memory_prompt(session_id: str, kind: str) -> str:
+ """读取游客短期记忆;Redis 不可用时降级为空(与 recall_memory 口径一致)。"""
+ try:
+ return VisitorMemoryService().as_prompt_text(session_id, kind)
+ except Exception:
+ return ""
+
+
+def _degraded_reply_from_rag(rag_context: str) -> str | None:
+ """LLM 不可用时,从 RAG 首片段提取可读答案。"""
+ text = (rag_context or "").strip()
+ if not text:
+ return None
+ parts = text.split("\n", 1)
+ if len(parts) < 2:
+ return None
+ body = parts[1].strip()
+ if len(body) < 8:
+ return None
+ return body[:400]
+
def recall_memory(state: VisitorState) -> VisitorState:
"""节点 1:从 Redis 读取两类短期记忆(Redis 不可用则降级为空)。"""
@@ -138,6 +167,9 @@ def intent_classify(state: VisitorState) -> VisitorState:
if any(k in msg for k in _REJECT_REALTIME_KW):
return {"intent": "reject", "reply": REJECT_REALTIME}
+ if any(k in msg for k in _FAQ_KEYWORDS):
+ return {"intent": "faq"}
+
# DeepSeek 分类
try:
llm = _build_llm()
@@ -191,7 +223,7 @@ def generate(state: VisitorState) -> VisitorState:
try:
llm = _build_llm()
- mem_text = VisitorMemoryService().as_prompt_text(state["session_id"], "consult")
+ mem_text = _visitor_memory_prompt(state["session_id"], "consult")
user_prompt = GENERATE_USER_TEMPLATE.format(
rag_context=state["rag_context"],
memory=mem_text,
@@ -203,7 +235,10 @@ def generate(state: VisitorState) -> VisitorState:
])
reply = resp.content.strip()
except Exception:
- return {"reply": FALLBACK_TEXT, "intent": "fallback"}
+ degraded = _degraded_reply_from_rag(state.get("rag_context", ""))
+ if not degraded:
+ return {"reply": FALLBACK_TEXT, "intent": "fallback"}
+ reply = degraded
# 合规护栏(1B:命中不自动 transfer)
reply, need_transfer = finalize_sanitized_reply(reply, intent=state.get("intent"))
@@ -222,7 +257,7 @@ def chitchat(state: VisitorState) -> VisitorState:
"""节点 5:闲聊生成。"""
try:
llm = _build_llm()
- mem_text = VisitorMemoryService().as_prompt_text(state["session_id"], "chitchat")
+ mem_text = _visitor_memory_prompt(state["session_id"], "chitchat")
user_prompt = CHITCHAT_USER_TEMPLATE.format(
memory=mem_text,
message=state["message"],
diff --git a/docs/course/index.html b/docs/course/index.html
index b634b2d..5867bb3 100644
--- a/docs/course/index.html
+++ b/docs/course/index.html
@@ -123,13 +123,19 @@
diff --git a/docs/course/jinrong-module-analyst/_base.html b/docs/course/jinrong-module-analyst/_base.html
index 92db22f..8794330 100644
--- a/docs/course/jinrong-module-analyst/_base.html
+++ b/docs/course/jinrong-module-analyst/_base.html
@@ -33,6 +33,7 @@
+
diff --git a/docs/course/jinrong-module-analyst/index.html b/docs/course/jinrong-module-analyst/index.html
index 99a71ec..9b6b1ca 100644
--- a/docs/course/jinrong-module-analyst/index.html
+++ b/docs/course/jinrong-module-analyst/index.html
@@ -33,6 +33,7 @@
+
@@ -194,9 +195,10 @@
@@ -303,11 +305,11 @@
消歧与生成
-
_detect_ambiguity 命中则 _clarify 返回 suggestions;否则 _generate_sql。
+
_detect_ambiguity → clarify;否则 模板填参 或 _generate_sql。
执行与缓存
-
validate → execute_readonly;Redis 按权限指纹 + SQL hash 缓存结果。
+
validate → execute_readonly;Redis 权限指纹 + SQL + 表世代;写交易/预警/L3 后 bump。
解读与留痕
@@ -315,7 +317,7 @@
- 返回体 AnalystResponse: answer + table + sql + meta(exec_ms, cache_hit, cost_est)+ disclaimer + status(success / degrade / clarify)。
+ 返回体 AnalystResponse: answer + table + sql + meta(exec_ms, template_hit, cache_hit, cost_est)+ disclaimer + status(success / degrade / clarify)。
@@ -460,6 +462,117 @@
+
+
+
+
模块 5 · D-06
+
模板填参 + 结果缓存
写侧 bump 失效
+
+ 问数两层「快路径」:① 同形态问题命中 published 模板,跳过 LLM 写 SQL(template_service.py)
+ ② 执行结果进 Redis,键里带表世代——交易/预警/L3 写后 bump,不等 TTL 过期。
+
+
+
+
两层 D-06 各管什么
+
+
+ | 层 | 入口 | 答辩句 |
+
+
+
+ | 模板缓存 |
+ match_template → 填 :days 等 |
+ 「客户+总数」→ template_hit: true,省 LLM 写 SQL |
+
+
+ | 结果缓存 |
+ cache_service.py 权限指纹 + SQL hash + 世代 |
+ 「同 SQL 同权限」秒回;写侧 bump 后旧键自然 miss |
+
+
+ | 写侧失效 |
+ analyst_cache_invalidate 交易/预警/L3 后 |
+ 「演示里刚模拟交易,问数不会还显示旧汇总」 |
+
+
+
+
+ 前端问数页会展示 模板命中 / 结果缓存 标签(读 meta.template_hit · cache_hit)。
+
+
+
+
+
+
+
analyst_agent 决策顺序(简化)
+
clarify? → return suggestions
+template match? → fill SQL → validate → execute
+else LLM generate_sql → validate → execute (cache?)
+guardrail → persist analytics_query_log
+
+
+
白话
+
+
像预制菜 + 冰箱贴保质期:常问题型直接拿模板填天数;跑完的结果贴冰箱,但 Core 有新交易就撕掉旧标签(bump)。
+
种子:scripts/agent/seed-analyst-query-templates.sql · 一键演示:prepare_all.ps1。
+
+
+
+
+
+
+
群聊:模板 vs LLM vs 缓存
+
+
+
+
问数页
+
+
AnalystQueryPage
+
POST「近7天申购金额」
+
+
+
+
template
+
+
template_service
+
关键词 + schema 命中 → 填 :days=7 → 跳过 LLM
+
+
+
+
cache
+
+
cache_service
+
世代未变 → cache_hit;刚 bump → 重算再写 Redis
+
+
+
+
+
+
+
+
+
AI 说「问数缓存就是 Redis TTL 10 分钟」,你怎么纠正?
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/course/jinrong-module-analyst/modules/02-pipeline.html b/docs/course/jinrong-module-analyst/modules/02-pipeline.html
index dee8548..88f66ab 100644
--- a/docs/course/jinrong-module-analyst/modules/02-pipeline.html
+++ b/docs/course/jinrong-module-analyst/modules/02-pipeline.html
@@ -13,9 +13,10 @@
diff --git a/docs/course/jinrong-module-analyst/modules/03-agent-api.html b/docs/course/jinrong-module-analyst/modules/03-agent-api.html
index d7a9ca5..93cab1f 100644
--- a/docs/course/jinrong-module-analyst/modules/03-agent-api.html
+++ b/docs/course/jinrong-module-analyst/modules/03-agent-api.html
@@ -17,11 +17,11 @@
消歧与生成
-
_detect_ambiguity 命中则 _clarify 返回 suggestions;否则 _generate_sql。
+
_detect_ambiguity → clarify;否则 模板填参 或 _generate_sql。
执行与缓存
-
validate → execute_readonly;Redis 按权限指纹 + SQL hash 缓存结果。
+
validate → execute_readonly;Redis 权限指纹 + SQL + 表世代;写交易/预警/L3 后 bump。
解读与留痕
@@ -29,7 +29,7 @@
- 返回体 AnalystResponse: answer + table + sql + meta(exec_ms, cache_hit, cost_est)+ disclaimer + status(success / degrade / clarify)。
+ 返回体 AnalystResponse: answer + table + sql + meta(exec_ms, template_hit, cache_hit, cost_est)+ disclaimer + status(success / degrade / clarify)。
diff --git a/docs/course/jinrong-module-analyst/modules/05-d06-cache.html b/docs/course/jinrong-module-analyst/modules/05-d06-cache.html
new file mode 100644
index 0000000..ef7f080
--- /dev/null
+++ b/docs/course/jinrong-module-analyst/modules/05-d06-cache.html
@@ -0,0 +1,111 @@
+
+
+
模块 5 · D-06
+
模板填参 + 结果缓存
写侧 bump 失效
+
+ 问数两层「快路径」:① 同形态问题命中 published 模板,跳过 LLM 写 SQL(template_service.py)
+ ② 执行结果进 Redis,键里带表世代——交易/预警/L3 写后 bump,不等 TTL 过期。
+
+
+
+
两层 D-06 各管什么
+
+
+ | 层 | 入口 | 答辩句 |
+
+
+
+ | 模板缓存 |
+ match_template → 填 :days 等 |
+ 「客户+总数」→ template_hit: true,省 LLM 写 SQL |
+
+
+ | 结果缓存 |
+ cache_service.py 权限指纹 + SQL hash + 世代 |
+ 「同 SQL 同权限」秒回;写侧 bump 后旧键自然 miss |
+
+
+ | 写侧失效 |
+ analyst_cache_invalidate 交易/预警/L3 后 |
+ 「演示里刚模拟交易,问数不会还显示旧汇总」 |
+
+
+
+
+ 前端问数页会展示 模板命中 / 结果缓存 标签(读 meta.template_hit · cache_hit)。
+
+
+
+
+
+
+
analyst_agent 决策顺序(简化)
+
clarify? → return suggestions
+template match? → fill SQL → validate → execute
+else LLM generate_sql → validate → execute (cache?)
+guardrail → persist analytics_query_log
+
+
+
白话
+
+
像预制菜 + 冰箱贴保质期:常问题型直接拿模板填天数;跑完的结果贴冰箱,但 Core 有新交易就撕掉旧标签(bump)。
+
种子:scripts/agent/seed-analyst-query-templates.sql · 一键演示:prepare_all.ps1。
+
+
+
+
+
+
+
群聊:模板 vs LLM vs 缓存
+
+
+
+
问数页
+
+
AnalystQueryPage
+
POST「近7天申购金额」
+
+
+
+
template
+
+
template_service
+
关键词 + schema 命中 → 填 :days=7 → 跳过 LLM
+
+
+
+
cache
+
+
cache_service
+
世代未变 → cache_hit;刚 bump → 重算再写 Redis
+
+
+
+
+
+
+
+
+
AI 说「问数缓存就是 Redis TTL 10 分钟」,你怎么纠正?
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/course/jinrong-module-frontend/index.html b/docs/course/jinrong-module-frontend/index.html
index 5a3ac2c..5fbf8bf 100644
--- a/docs/course/jinrong-module-frontend/index.html
+++ b/docs/course/jinrong-module-frontend/index.html
@@ -354,20 +354,24 @@
模块 4 · 未接线
菜单有 ≠ API 已接
Chat 还有 B 方案三端点
- 前端 P0 主链路已真接,但和后端课对照,仍有页面占位、风控/平台按钮缺失、SSE 行为等缝。
- 改 URL 进别人工作台,前端藏菜单拦不住,后端才是闸。
+ 前端 P0 主链路已真接(含风控三页 · 问数标签 · Dashboard hooks),但仍有分析对话占位、看板无钻取等缝。
+ 开发态若 Dashboard「先闪错再正常」,多半是 StrictMode 双请求——已用 useAsyncSequence 丢弃过期响应。
-
前端未全接清单(2026-09)
+
仍开放 / 已修清单(2026-09-10)
-
风控
-
AML scan · 适当性校验按钮 · 台账高级筛选 — 部分 API 有,UI 简版或未接。
+
风控 UI
+
台账筛选 · 适当性校验 · AML 扫描页已接 REST;高级运营能力仍简版。
问数
-
AnalystChatShell 占位 · 真问数在 AnalystQueryPage + dashboard/assets。
+
AnalystChatShell 占位 · 真问数在 AnalystQueryPage(模板/缓存标签 + dashboard/assets)。
+
+
+
竞态修复
+
useAsyncSequence 用于 Dashboard hooks、ChatPanel、风控/客户列表页等。
游客
diff --git a/docs/course/jinrong-module-frontend/modules/04-unwired.html b/docs/course/jinrong-module-frontend/modules/04-unwired.html
index 4f8f2d8..11df890 100644
--- a/docs/course/jinrong-module-frontend/modules/04-unwired.html
+++ b/docs/course/jinrong-module-frontend/modules/04-unwired.html
@@ -3,20 +3,24 @@
模块 4 · 未接线
菜单有 ≠ API 已接
Chat 还有 B 方案三端点
- 前端 P0 主链路已真接,但和后端课对照,仍有页面占位、风控/平台按钮缺失、SSE 行为等缝。
- 改 URL 进别人工作台,前端藏菜单拦不住,后端才是闸。
+ 前端 P0 主链路已真接(含风控三页 · 问数标签 · Dashboard hooks),但仍有分析对话占位、看板无钻取等缝。
+ 开发态若 Dashboard「先闪错再正常」,多半是 StrictMode 双请求——已用 useAsyncSequence 丢弃过期响应。
-
前端未全接清单(2026-09)
+
仍开放 / 已修清单(2026-09-10)
-
风控
-
AML scan · 适当性校验按钮 · 台账高级筛选 — 部分 API 有,UI 简版或未接。
+
风控 UI
+
台账筛选 · 适当性校验 · AML 扫描页已接 REST;高级运营能力仍简版。
问数
-
AnalystChatShell 占位 · 真问数在 AnalystQueryPage + dashboard/assets。
+
AnalystChatShell 占位 · 真问数在 AnalystQueryPage(模板/缓存标签 + dashboard/assets)。
+
+
+
竞态修复
+
useAsyncSequence 用于 Dashboard hooks、ChatPanel、风控/客户列表页等。
游客
diff --git a/docs/course/jinrong-module-risk/index.html b/docs/course/jinrong-module-risk/index.html
index 8cf9f7e..b68180d 100644
--- a/docs/course/jinrong-module-risk/index.html
+++ b/docs/course/jinrong-module-risk/index.html
@@ -49,7 +49,7 @@
风控专员 Demo
STAFF-30001 带
risk_officer +
risk_demo 角色:
能看预警台账、处置、跑 AML 扫描,还能在模拟交易页触发规则引擎。
本地登录页一键切换,改 JWT 角色后需
重新登录。
- 仓库基线
merger ·
python -m pytest →
795 passed。
+ 仓库基线
merger ·
python -m pytest →
804 passed。
@@ -409,7 +409,7 @@
- 仓库现状(2026-09-10):STAFF-30001 含 risk_demo;python -m pytest → 795 passed。
+ 仓库现状(2026-09-10):STAFF-30001 含 risk_demo;python -m pytest → 804 passed。
diff --git a/docs/course/jinrong-module-risk/modules/01-capabilities.html b/docs/course/jinrong-module-risk/modules/01-capabilities.html
index f4ed541..291610a 100644
--- a/docs/course/jinrong-module-risk/modules/01-capabilities.html
+++ b/docs/course/jinrong-module-risk/modules/01-capabilities.html
@@ -6,7 +6,7 @@
风控专员 Demo
STAFF-30001 带
risk_officer +
risk_demo 角色:
能看预警台账、处置、跑 AML 扫描,还能在模拟交易页触发规则引擎。
本地登录页一键切换,改 JWT 角色后需
重新登录。
- 仓库基线
merger ·
python -m pytest →
795 passed。
+ 仓库基线
merger ·
python -m pytest →
804 passed。
diff --git a/docs/course/jinrong-module-risk/modules/03-tools-rest.html b/docs/course/jinrong-module-risk/modules/03-tools-rest.html
index 117211a..65cd4f1 100644
--- a/docs/course/jinrong-module-risk/modules/03-tools-rest.html
+++ b/docs/course/jinrong-module-risk/modules/03-tools-rest.html
@@ -128,7 +128,7 @@
- 仓库现状(2026-09-10):STAFF-30001 含 risk_demo;python -m pytest → 795 passed。
+ 仓库现状(2026-09-10):STAFF-30001 含 risk_demo;python -m pytest → 804 passed。
diff --git a/docs/course/jinrong-overview/_base.html b/docs/course/jinrong-overview/_base.html
index 5bed6c0..ef84c0e 100644
--- a/docs/course/jinrong-overview/_base.html
+++ b/docs/course/jinrong-overview/_base.html
@@ -36,6 +36,7 @@
+
diff --git a/docs/course/jinrong-overview/index.html b/docs/course/jinrong-overview/index.html
index 4dc4faf..7b5b230 100644
--- a/docs/course/jinrong-overview/index.html
+++ b/docs/course/jinrong-overview/index.html
@@ -36,6 +36,7 @@
+
@@ -411,17 +412,17 @@
模块 5 · 现状与下一步
现在做到哪?
怎么本地跑起来
- 分支 merger · 测试基线 774 pytest · 前端 19 Vitest · Redis Docker 6380。
- 文档入口:docs/memory/MEMORY.md §0 交接清单。
+ 分支 merger · 测试基线 804 pytest · 前端 19 Vitest · Redis Docker 6380。
+ 文档入口:docs/memory/MEMORY.md §0 · 答辩提纲 docs/答辩/答辩知识点清单.md · 滚动版见模块 8。
-
已完成(2026-09-09 快照)
+
已完成(2026-09-10 快照)
✓ 平台 API v0.1
客户/产品/顾问/合规只读 REST
-
✓ 四 Agent 对话 + SSE
含 customer 客服线分流
-
✓ 问数 S3 + P2
问数页 · 看板 metrics · 资产沉淀 UI
-
✓ 风控全链路
台账 · 模拟交易 · 规则 FR-1~10
+
✓ 四 Agent 对话 + SSE
客服 Wave3 · 游客 1B · customer 14 节点
+
✓ 问数 S3 + D-06
模板填参 · 结果缓存 + 写侧 bump · 问数 UI 标签
+
✓ 风控 + 前端 P0
台账/模拟/AML 页 · prepare_all.ps1 灌库
@@ -439,11 +440,11 @@
本地启动(最小路径)
1pip install -r requirements.txt
-
2.\scripts\core\reset.ps1 灌 Core 模拟库
+
2.\scripts\demo\prepare_all.ps1 或 .\scripts\core\reset.ps1
3.\scripts\dev\start-redis.ps1 → 6380
4uvicorn app.main:app --reload :8000
5cd web && npm run dev → :5173 代理 API
-
6python -m pytest 验收 774 绿
+
6python -m pytest 验收 804 绿
改后端后: 若浏览器问数 404,多半是 :8000 的 uvicorn 没重启,OpenAPI 里还缺新路由。
@@ -476,10 +477,10 @@
-
想继续深入? 打开
课程中心,按角色进入 7 门模块深潜课(客户 / 顾问 / 问数 / 风控 / 平台 API / 前端 / 共用底座)。
+
想继续深入? 模块 8 有
答辩 5~8 分钟动线;或打开
课程中心 按角色深潜。
-
+
@@ -583,7 +584,7 @@
已实现(别重复造轮子)
✓平台 API v0.1 · 四 Agent 对话 + customer SSE
-
✓问数 S3 + dashboard/assets · 风控台账 + 模拟交易
+
✓问数 S3 + D-06 模板/结果缓存 · 风控台账 + 模拟交易 + 前端四页
✓客服 14 节点 + 游客 9 节点 · R-02 网关阻断
@@ -600,8 +601,8 @@
A-03 话术草稿复核 · A-05 合规巡检台 · A-06 跟进草稿入库 — prompt 有约束,无前端/表流程。
-
前端未接线
-
AML scan UI · 平台适当性按钮 · 风控台账高级筛选 — 后端部分已有,web/ 未全接。
+
前端仍简版
+
风控 P1 已接(筛选/适当性/AML)· 看板无钻取 · 分析 Chat 仍占位 · 平台适当性按钮部分未接。
运维 cron 无 UI
@@ -666,6 +667,132 @@
+
+
+
+
模块 8 · 答辩动线
+
5~8 分钟 Demo
+ 必背铁律
+
+ 对照 docs/答辩/答辩知识点清单.md 的滚动版:先讲清四角色不互调 LLM,再按动线演示,最后主动说边界。
+ 基线 804 pytest · 19 Vitest · 分支 merger。
+
+
+
+
30 秒总架构(开口即说)
+
+
+
分层
+
浏览器 → FastAPI → 编排 service → tool/repository → 双库 MySQL + Redis + Milvus
+
+
+
三条入口
+
Chat + X-Agent-Type · 客服 customer 14 节点 · 问数 /api/analyst/chat(无 Agent 头)
+
+
+
铁律
+
L0 画像不可覆盖 · 仅 R-02 可阻断交易 · 风控不自动冻户 · 审计只 INSERT
+
+
+
+
+
+
推荐演示顺序(动画)
+
+
+
🖥登录/Dashboard
+
💬客户线
+
🛡风控线
+
📊问数线
+
+
灌库:scripts/demo/prepare_all.ps1(含 Core reset + 问数模板种子)
+
+
+
+
+
+
+
+
+
群聊:评委问「四个 Agent 为什么不互相打电话?」
+
+
+
+
评委
+
+
评委
+
风控 Agent 能不能直接让客服 Agent 去安抚客户?
+
+
+
+
你
+
+
答辩口径
+
不互调 LLM。跨线靠表(如 risk_alert)和平台只读 API;审计按 Agent 边界留痕,避免一条链路里混处置权限。
+
+
+
+
+
你
+
+
答辩口径
+
交易写 Core → 风控引擎出单进 agent 库;客服查持仓走 Core RO Tool,不读风控写侧。问数只统计,不能调处置 API。
+
+
+
+
+
+
+
+
+
诚实边界(主动说加分)
+
+ - L0 是模拟 Core,不是真银行托管库
+ - 分析对话页占位,真 NL2SQL 在问数工作台(D-09 多轮未做)
+ - 看板无钻取 · 行情 Phase B 草案未接
+ - 知识库 T-21 脚本入库,上传 API 一期不做
+
+
+
+
+
评委:「刷新 Dashboard 会先红一下再正常,是不是没做错误处理?」
+
+
+
+
+
+
+
+
+
+
+
+
+
全文提纲: 仓库内
docs/答辩/答辩知识点清单.md · 深潜按角色打开
课程中心。
+
+
+
+
diff --git a/docs/course/jinrong-overview/modules/05-status.html b/docs/course/jinrong-overview/modules/05-status.html
index 9574d8b..2199b85 100644
--- a/docs/course/jinrong-overview/modules/05-status.html
+++ b/docs/course/jinrong-overview/modules/05-status.html
@@ -3,17 +3,17 @@
模块 5 · 现状与下一步
现在做到哪?
怎么本地跑起来
- 分支 merger · 测试基线 774 pytest · 前端 19 Vitest · Redis Docker 6380。
- 文档入口:docs/memory/MEMORY.md §0 交接清单。
+ 分支 merger · 测试基线 804 pytest · 前端 19 Vitest · Redis Docker 6380。
+ 文档入口:docs/memory/MEMORY.md §0 · 答辩提纲 docs/答辩/答辩知识点清单.md · 滚动版见模块 8。
-
已完成(2026-09-09 快照)
+
已完成(2026-09-10 快照)
✓ 平台 API v0.1
客户/产品/顾问/合规只读 REST
-
✓ 四 Agent 对话 + SSE
含 customer 客服线分流
-
✓ 问数 S3 + P2
问数页 · 看板 metrics · 资产沉淀 UI
-
✓ 风控全链路
台账 · 模拟交易 · 规则 FR-1~10
+
✓ 四 Agent 对话 + SSE
客服 Wave3 · 游客 1B · customer 14 节点
+
✓ 问数 S3 + D-06
模板填参 · 结果缓存 + 写侧 bump · 问数 UI 标签
+
✓ 风控 + 前端 P0
台账/模拟/AML 页 · prepare_all.ps1 灌库
@@ -31,11 +31,11 @@
本地启动(最小路径)
1pip install -r requirements.txt
-
2.\scripts\core\reset.ps1 灌 Core 模拟库
+
2.\scripts\demo\prepare_all.ps1 或 .\scripts\core\reset.ps1
3.\scripts\dev\start-redis.ps1 → 6380
4uvicorn app.main:app --reload :8000
5cd web && npm run dev → :5173 代理 API
-
6python -m pytest 验收 774 绿
+
6python -m pytest 验收 804 绿
改后端后: 若浏览器问数 404,多半是 :8000 的 uvicorn 没重启,OpenAPI 里还缺新路由。
@@ -68,10 +68,10 @@
-
想继续深入? 打开
课程中心,按角色进入 7 门模块深潜课(客户 / 顾问 / 问数 / 风控 / 平台 API / 前端 / 共用底座)。
+
想继续深入? 模块 8 有
答辩 5~8 分钟动线;或打开
课程中心 按角色深潜。
-
+
diff --git a/docs/course/jinrong-overview/modules/07-not-implemented.html b/docs/course/jinrong-overview/modules/07-not-implemented.html
index 587e6cd..47a219c 100644
--- a/docs/course/jinrong-overview/modules/07-not-implemented.html
+++ b/docs/course/jinrong-overview/modules/07-not-implemented.html
@@ -11,7 +11,7 @@
已实现(别重复造轮子)
✓平台 API v0.1 · 四 Agent 对话 + customer SSE
-
✓问数 S3 + dashboard/assets · 风控台账 + 模拟交易
+
✓问数 S3 + D-06 模板/结果缓存 · 风控台账 + 模拟交易 + 前端四页
✓客服 14 节点 + 游客 9 节点 · R-02 网关阻断
@@ -28,8 +28,8 @@
A-03 话术草稿复核 · A-05 合规巡检台 · A-06 跟进草稿入库 — prompt 有约束,无前端/表流程。
-
前端未接线
-
AML scan UI · 平台适当性按钮 · 风控台账高级筛选 — 后端部分已有,web/ 未全接。
+
前端仍简版
+
风控 P1 已接(筛选/适当性/AML)· 看板无钻取 · 分析 Chat 仍占位 · 平台适当性按钮部分未接。
运维 cron 无 UI
diff --git a/docs/course/jinrong-overview/modules/08-defense.html b/docs/course/jinrong-overview/modules/08-defense.html
new file mode 100644
index 0000000..a21dad8
--- /dev/null
+++ b/docs/course/jinrong-overview/modules/08-defense.html
@@ -0,0 +1,126 @@
+
+
+
模块 8 · 答辩动线
+
5~8 分钟 Demo
+ 必背铁律
+
+ 对照 docs/答辩/答辩知识点清单.md 的滚动版:先讲清四角色不互调 LLM,再按动线演示,最后主动说边界。
+ 基线 804 pytest · 19 Vitest · 分支 merger。
+
+
+
+
30 秒总架构(开口即说)
+
+
+
分层
+
浏览器 → FastAPI → 编排 service → tool/repository → 双库 MySQL + Redis + Milvus
+
+
+
三条入口
+
Chat + X-Agent-Type · 客服 customer 14 节点 · 问数 /api/analyst/chat(无 Agent 头)
+
+
+
铁律
+
L0 画像不可覆盖 · 仅 R-02 可阻断交易 · 风控不自动冻户 · 审计只 INSERT
+
+
+
+
+
+
推荐演示顺序(动画)
+
+
+
🖥登录/Dashboard
+
💬客户线
+
🛡风控线
+
📊问数线
+
+
灌库:scripts/demo/prepare_all.ps1(含 Core reset + 问数模板种子)
+
+
+
+
+
+
+
+
+
群聊:评委问「四个 Agent 为什么不互相打电话?」
+
+
+
+
评委
+
+
评委
+
风控 Agent 能不能直接让客服 Agent 去安抚客户?
+
+
+
+
你
+
+
答辩口径
+
不互调 LLM。跨线靠表(如 risk_alert)和平台只读 API;审计按 Agent 边界留痕,避免一条链路里混处置权限。
+
+
+
+
+
你
+
+
答辩口径
+
交易写 Core → 风控引擎出单进 agent 库;客服查持仓走 Core RO Tool,不读风控写侧。问数只统计,不能调处置 API。
+
+
+
+
+
+
+
+
+
诚实边界(主动说加分)
+
+ - L0 是模拟 Core,不是真银行托管库
+ - 分析对话页占位,真 NL2SQL 在问数工作台(D-09 多轮未做)
+ - 看板无钻取 · 行情 Phase B 草案未接
+ - 知识库 T-21 脚本入库,上传 API 一期不做
+
+
+
+
+
评委:「刷新 Dashboard 会先红一下再正常,是不是没做错误处理?」
+
+
+
+
+
+
+
+
+
+
+
+
+
全文提纲: 仓库内
docs/答辩/答辩知识点清单.md · 深潜按角色打开
课程中心。
+
+
+
+
+
diff --git a/docs/frontend/FRONTEND-HANDOFF.md b/docs/frontend/FRONTEND-HANDOFF.md
index 8fad44f..83d9c6a 100644
--- a/docs/frontend/FRONTEND-HANDOFF.md
+++ b/docs/frontend/FRONTEND-HANDOFF.md
@@ -163,6 +163,7 @@ Tailwind 负责:
- 理财师显示客户数、总 AUM 和最大客户 fallback;客户市值降序、客户与 Chat 链接可用。
- 分析员显示产品数、上涨/下跌/平盘;按日变化绝对值排序,行情/问数/Chat 链接可用。
- 风控显示待审数量和 API disclaimer;无预警时显示 Empty,不出现假图表或假表格;有预警时显示图表、处置和助手链接。
+- **四角色 Chat(含客户 SSE)**:…侧栏每条可删除(调用 close API);侧栏**仅展示 active**;**清空历史会话** 调用 `POST /api/chat/sessions/close-all`(消息仍留库,仅关闭续聊)。
- API 错误态显示 code、message、trace ID 和重试;loading 态保留 shell 与 skeleton。
- 检查正收益红色、负收益绿色、零值灰色。
@@ -182,6 +183,7 @@ Tailwind 负责:
- 不要在页面里新增硬编码颜色或随意 spacing。
- 不要删除共享组件而不检查 downstream agent 的引用。
- 不要修改 `AuthState`、`jinrong.auth` localStorage key 或登录默认落点。
+- 对话续聊用 `sessionStorage` 键 `jinrong.chat.activeSession.{agentType}`(与登录 key 无关);勿在流式 POST 省略 `session_id`。
- 不要把表格的 sorting、pagination、row action 逻辑重写成页面外的副本。
- 修改后至少运行:
@@ -193,3 +195,25 @@ npm run lint
```
当前已知 lint warning 主要来自既有 hooks 的 `set-state-in-effect` 和 React Fast Refresh 对“组件文件同时导出常量”的提示;它们不是编译错误。Vite 可能提示主 chunk 超过 500 kB,属于当前依赖包体积提示。
+
+## 10. 看板地图与加载(2026-09-10)
+
+**有可视化,入口是角色 home,不是单独 BI 产品:**
+
+| 角色 | 路由 | 图表 |
+| --- | --- | --- |
+| 客户 | `#/app/customer/home` | 资产结构饼图 · 持仓盈亏柱图 · 表 |
+| 理财师 | `#/app/advisor/home` | 名下客户 AUM 分布 |
+| 分析 | `#/app/analyst/home` | 市场/产品概览 |
+| 风控 | `#/app/risk/home` | 预警类型分布 |
+| 问数 | `#/app/analytics/query` | MetricCard(`/api/analyst/dashboard`)· 无钻取(D-12 未做) |
+
+**为何每次进页都转圈:**
+
+- **Redis 不缓存**平台 REST(持仓/净值/预警);只服务 Chat 窗口、问数 D-06、限流、L3。
+- 每个页面 **mount 时 `loading=true` 全量重拉**;离开路由 state 丢弃(无 React Query)。
+- `fetchNavMap` 对**每个 product_id 单独请求** `/nav`,持仓/行情页请求次数多。
+
+**明日优化方向(见 `TODO.md` §2026-09-11):** 批量净值 API · 客户端 stale-while-revalidate · 可选 Dashboard 短 TTL 缓存(后端)。
+
+**客户「产品趋势」:** 客户助手 Chat 仅 **C-05 最新净值**;走势/预测 reject。统计类趋势走 **问数工作台** + customer **`self` 域**(`数据分析Agent-合并说明.md` §9.6)。分析对话页仍为占位。
diff --git a/docs/memory/ITERATION.md b/docs/memory/ITERATION.md
index 5708118..5672d41 100644
--- a/docs/memory/ITERATION.md
+++ b/docs/memory/ITERATION.md
@@ -25,4 +25,8 @@
| 2026-09-10 | **数据分析 Agent 护栏/口径迭代**(cherry-pick `445cfbb` 功能层)· guardrail Decimal/编号/时间窗 · 流水 N-01 消歧 · battery 脚本改 `issue_dev_token` | 远程 `data-analysis-agent` 未合 merger | guardrail / dict_service / wave6 测试 / `数据分析Agent-代码迭代.md` |
| 2026-09-10 | **数据分析 D-06 缓存**:结果缓存表世代 + 写侧 bump(trade/alert/L3)· **模板填参**(`template_service`)· 问数页 template/cache 标签 · `804 pytest` | 用户拍板 D-06 必须 · PII 脱敏暂缓 | cache_service / template_service / analyst_agent / web |
| 2026-09-10 | **风控前端 P1 + 演示**:台账筛选 · 适当性/AML 页 · `prepare_all.ps1` · 深潜课模块 7 | 2026-09-10 拍板收口 | web/risk · scripts/demo · docs/course |
-| 2026-09-10 | **`battery_report.json` 不入库** | 本地跑分产物 | `.gitignore` |
+| 2026-09-10 | **客户/共用 ChatPanel**:流式请求补传 `session_id`(续聊不再每轮新建会话)· `sessionStorage` 刷新恢复 · 会话侧栏 8 条分页 · 固定高度布局 | E2E 反馈 CHAT-1/2 | web · 整体测试交接 · MEMORY |
+| 2026-09-10 | **前端 E2E 问题清单收口**:`AuthProvider` 修登录崩溃/退出死循环 · Redis `socket_connect_timeout` 0.5s · 预警筛选防抖 · I18N formatter · 对话 Markdown 粗体 · A11Y `index.html` · AntD `
` | `docs/整体测试/前端整体测试交接.md` 登记项 | web · `database.py` · MEMORY |
+| 2026-09-10 | **答辩/课程**:`docs/答辩/答辩知识点清单.md` · 总览模块 8 · 问数课 D-06 模块 5 · 导览中心更新 | 用户答辩准备 | docs/course · MEMORY |
+| 2026-09-10 | **客服问候修复**:「你好」关键词 → `chit_chat` · LLM 失败 `CHITCHAT_DEGRADED_TEXT`(非 FALLBACK)· 游客问候快路由 | 用户反馈 Chat 无法回答问候 | customer_prompts · customer_service · visitor · tests |
+| 2026-09-10 | **产品口径落账**:Redis≠Dashboard 缓存 · 四角色 home 有图无 BI · 客户趋势=问数 self 域非 Chat 曲线(§9.6) | 用户问答澄清 | MEMORY §0/§3 · TODO §2026-09-11 · FRONTEND-HANDOFF §10 |
diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md
index 44099de..ce55b7c 100644
--- a/docs/memory/MEMORY.md
+++ b/docs/memory/MEMORY.md
@@ -9,7 +9,7 @@
**项目是什么:** 金融四 Agent(客户财富 / 代理人 / 数据分析 / 风控)共用数据层与合规底座;**不**互调 LLM,跨 Agent 走 L1/L2/L3 画像与预警表。
-**当前进度:** 需求与表设计已定 · **风控 + 平台 API + 客服 S2 Wave3 + 数据分析 S3/P2/D-06 + 前端四角色 P0 Demo** · **804 pytest** · **19 Vitest** · **Redis @ 6380** · **`merger` 工作区未 commit**
+**当前进度:** 需求与表设计已定 · **风控 + 平台 API + 客服 S2 Wave3 + 数据分析 S3/P2/D-06 + 前端四角色 P0 Demo** · **804 pytest** · **22 Vitest** · **Redis @ 6380** · **`merger` 工作区未 commit**
**工作分支:** 团队开发在 **`merger`**;历史 `risk-control-agent` 交付冻结。
@@ -41,14 +41,14 @@
| `app/utils/` | **基本就绪** | trace(trace_id+request_id 双 contextvar)/ desensitize / db(引擎工厂)/ response(统一错误体+4xx/500 handler)/ exceptions(含 ApiError)已实现;logger 占位 |
| `app/config/database.py` | **已实现** | MySQL 双引擎 + **Redis 单例**(`REDIS_URL` · **RESP2 `protocol=2`** 兼容 Docker Redis 7 / 旧 Windows Redis 3) |
| `app/config/settings.py` | **已实现** | 双库 + risk_* 阈值 + JWT + **customer/visitor/profile** 字段 + `kb_root_dir` |
-| `app/service/customer_service.py` `visitor_service.py` | **已实现(S2 + Wave3 部分)** | 客服 14 节点 · **C-04 阈值** · **C-05 nav_query** · **C-11 匹配说明** · 游客 9 节点;共用 `sanitize_postprocess.finalize_sanitized_reply`(1B);RAG **fin_* 三库** |
+| `app/service/customer_service.py` `visitor_service.py` | **已实现(S2 + Wave3 + 问候快路由)** | 客服 14 节点 · **C-04 阈值** · **C-05 nav_query** · **C-11 匹配说明** · **「你好」等问候关键词 → chit_chat;LLM 失败闲聊降级 `CHITCHAT_DEGRADED_TEXT`** · 游客 9 节点;共用 `sanitize_postprocess`(1B);RAG **fin_* 三库** |
| `app/service/threshold_service.py` | **已实现(C-04)** | L1 `threshold_pref_summary` → `customer_threshold_config`;持仓查询加权盈亏 vs 阈值 → `customer_notify_log` |
| `app/utils/sanitize_postprocess.py` | **已实现(1B 共用)** | 数据查询 intent(含 nav_query)违禁 → 回退 fact_text;RAG/闲聊 → COMPLIANCE_REJECT;均不自动 transfer |
| `scripts/core/*.sql` + `reset.ps1` | **已实现** | Core 模拟库 DDL + 种子 |
| `scripts/agent/` `scripts/demo/` `scripts/dev/` | **已实现** | AML 种子 · **`prepare_all.ps1` 一键灌库** · `seed-analyst-query-templates.sql`(模板缓存)· `run_query_battery.py`(**不入库**)· `start-redis.ps1` |
| `scripts/sync/*.py` | **已实现** | 归属同步 + Neo4j 全图 |
| `tests/` | **已实现** | **804 用例** 1 skipped(Wave6 template/cache + Wave3 customer + 1B/R1)
-| `docs/course/` | **交互课程集** | 导览中心 + 风控深潜 **7 模块**(含模块 7 写侧并发)
+| `docs/course/` | **交互课程集** | 导览中心 + 总览 **8 模块**(含模块 8 答辩动线)+ 问数 **5 模块**(D-06)+ 风控深潜 **7 模块**(含写侧并发)· 提纲 `docs/答辩/答辩知识点清单.md`
| `docs/PRD/PRD-风控监测Agent.md` | **已冻结(v1.1)** | 风控 PRD v1.0 + v1.1 追加 FR-8/9/10(§4A)+ 规则表附录 |
| `docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md` | **已定稿** | C4~C6 编码依据(经独立 AI 评审修订闭环);分支/进度速览另见项目根 `交接文档.md` |
| `docs/项目框架设计/合并注意事项-风控模块并入main.md` | **AL-09 已执行(2026-09-08)** | 合并接线完成;接缝见《风控Agent模块边界与合并接缝标注.md》 |
@@ -58,7 +58,7 @@
| `docs/项目框架设计/接口契约-代销平台API-v0.2-行情扩展草案.md` | **草案(2026-09-09)** | `nav-snapshot` · `sync_market_nav` · 前端产品行情 Phase B |
| `docs/项目框架设计/表设计/` | 已定 | Agent 共用 11 表 + agent 专用 SQL |
| `docs/项目框架设计/Core模拟底座/` | 已定 | 无真实 Core 时的 L0 方案 |
-| `web/` | **P0 Demo 齐备(2026-09-10)** | 四角色 Dashboard/Chat · 问数工作台(**模板/缓存标签**)· 资产沉淀 · 风控台账筛选+适当性+AML+模拟交易 · 平台行情 · **分析对话仍为轻量占位**(NL2SQL 走问数页) |
+| `web/` | **P0 Demo 齐备 + E2E/Chat 缺陷已修(2026-09-10)** | 四角色 Dashboard/Chat · **`ChatPanel` 流式续聊 `session_id` + 刷新恢复** · 会话侧栏分页 · 固定面板高度 · 清单见 `docs/整体测试/前端整体测试交接.md` |
**本地 bootstrap(首次):** 完整步骤与前置说明见 `FLOW.md` §0(权威),速览:
@@ -77,10 +77,14 @@
**AL-09 合并后架构(一句话):** 宿主 `gateway/` + 模块 `deps.py` **双栈并存**;对外登录/token **统一**;chat/risk 均走模块鉴权;接缝 S2 用 `auth_adapter`。
-**下一步(见 TODO):** git commit `merger` · 20 题 battery 实跑 · 客服手工验收/TEST-LOG · 接口契约发群 · analyst Q17/Q7 挂账 · D-09/N-03/N-07 未做
+**下一步(见 TODO · §2026-09-11 接续):** git commit `merger`(含问候修复)· 前端加载体验(batch nav / 客户端缓存)· 产品口径文档已落 MEMORY §3 · battery · 客服验收 · D-09/D-12 往后排
+
+**⚠️ 前端 E2E 未跑测试(2026-09-10):** 该轮走查**只做到「25 路由能渲染 + 无 console/HTTP 错误」,不是逐功能详测** —— 数字/图表/交互正确性、**接口错误态**、对话多轮与会话管理、适当性通过路径、交易拦截路径等**均未验证**(CHAT-1~4 就是这样漏掉的)。**未跑清单见 `TODO.md` §「前端 E2E · 未跑测试」**,报告见 `docs/整体测试/前端整体测试交接.md`。
**Redis(2026-09-09):** 推荐 **Docker** `jinrong-redis`(`redis:7-alpine`)· 宿主机 **6380** → 容器 6379 · `.env` `REDIS_URL=redis://127.0.0.1:6380/0` · 客户端 **RESP2**(`database.py` / `redis_gateway.py`)。
+**Redis 不加速页面看板(2026-09-10 澄清):** Redis 管 **Chat 会话窗口 / 问数 D-06 结果 / 限流 / L3 热读**;**平台 GET(持仓/净值/预警列表)与 Dashboard hooks 每次进页仍打 MySQL**,前端无跨路由缓存 · 净值 **`fetchNavMap` 按产品 N 次 HTTP** → 体感「每页都转圈」见 `TODO.md` §2026-09-11 · `FRONTEND-HANDOFF.md` §10。
+
**前端启动(`web/`):** `npm install` → `npm run dev`(`5173`,`/api` 代理 `8000`)· 验收四 demo 账号见 **`docs/frontend/FRONTEND-HANDOFF.md` §8** · 改 UI 后跑 `npm run build && npm run test && npm run lint`。
**平台 API 硬规则(2026-09-08 拍板):** 路由 **A · 按业务域**(`/api/customers` 等);与 Agent 功能重复时 **以本平台 API 为准**;命名见契约 §2;合并时统一改 canonical 路径。
@@ -166,6 +170,7 @@ audit_log 等审计表(只 INSERT)
| **C-11** | 「我能买什么/匹配产品」→ `suitability_check` + Core 可购列表;**「推荐稳赚/买什么好」仍 reject** | `customer_prompts._ELIGIBLE_PRODUCTS_KW` |
| **C-07** | 风评查询走 Core;**「重新测评/重做风评」** → 引导 App/网点(Agent 内不做问卷) | `customer_prompts._RISK_KW` |
| **C-08** | 仅 L1 槽位 `investment.allocation_target`(13 槽);**无自动偏离检测/调仓** | `profile_slots.py` |
+| **客户「趋势/走势」** | **客户 Chat**:仅 **C-05 最新净值快照** + 持仓/流水;**走势预测/实时盘口 reject**;**无 Chat 内净值历史曲线 Tool** · **统计类趋势**(近 N 日笔数、结构描述)→ **问数** `POST /api/analyst/chat` · **`self` 域**(拍板 `数据分析Agent-合并说明.md` §9.6 · 尾注「AI 分析有风险」)· **D-12 看板钻取未做** | 问数 ≠ 客户助手 · Phase B 行情 sync 未做 |
| **L0 优先** | 抽槽与 L0 撞车**永远听 L0**;L1 只 enrich 措辞 | `profile_slots` D7 |
------
@@ -181,7 +186,7 @@ Core 模拟:scripts/core/reset.ps1 · 文档 docs/项目框架设计/Core模
启动:uvicorn app.main:app --reload → GET /health
Redis:`docker compose up -d redis` · `REDIS_URL=redis://127.0.0.1:6380/0` · `scripts/dev/start-redis.ps1`
测试:python -m pytest(**804 绿**;集成需本机 MySQL + AML + 风控演示数据)
-前端:cd web && npm run dev · npm run build/test/lint(**19** Vitest)· 四角色 Demo 见 `docs/frontend/FRONTEND-HANDOFF.md` §8
+前端:cd web && npm run dev · npm run build/test/lint(**22** Vitest)· 四角色 Demo 见 `docs/frontend/FRONTEND-HANDOFF.md` §8
问数模板:mysql … < scripts/agent/seed-analyst-query-templates.sql
风控一键灌库:.\scripts\demo\prepare_all.ps1
运维/演示脚本:scripts/demo/subscribe_alerts.py(订阅推送演示)· rebuild_alerts.py TRD-xxx(引擎异常补偿重放)
diff --git a/docs/memory/REQUIREMENTS.md b/docs/memory/REQUIREMENTS.md
index 215890a..c997bca 100644
--- a/docs/memory/REQUIREMENTS.md
+++ b/docs/memory/REQUIREMENTS.md
@@ -60,7 +60,7 @@
| C-02 | 产品规则咨询 | fin_product RAG + 免责 | **已实现** · Wave1~2 | — |
| C-03 | 政策/FAQ | fin_policy / fin_faq RAG | **已实现** · Wave1~2 | — |
| C-04 | 亏损阈值提醒 | `customer_threshold_config` + 持仓查询追加提醒 | **已实现(2026-09-10)** · `threshold_service` | ~~T-40~~ |
-| C-05 | 产品净值 | Core 最新净值快照;禁止实时盘口 | **已实现(2026-09-10)** · `nav_query`;「实时净值」reject | ~~T-40~~ |
+| C-05 | 产品净值 | Core 最新净值快照;禁止实时盘口 | **已实现(2026-09-10)** · `nav_query`;「实时净值」reject · **无 Chat 历史走势曲线**;统计趋势见问数 self 域(合并说明 §9.6) | ~~T-40~~ |
| C-07 | 风评查询 / 重测引导 | Core L0;重测引导 App/网点 | **部分已实现** · 无 Agent 内问卷 | T-41 |
| C-08 | 配置目标 | L1 槽 `allocation_target` | **部分已实现** · 无偏离检测/调仓 | T-41 |
| C-11 | 适当性匹配说明 | 非营销推荐;联动 R-02 | **部分已实现** · 「我能买什么」→ suitability;「推荐稳赚」仍 reject | T-42 |
diff --git a/docs/memory/TODO.md b/docs/memory/TODO.md
index 6e472a1..dff7153 100644
--- a/docs/memory/TODO.md
+++ b/docs/memory/TODO.md
@@ -9,7 +9,7 @@
### 本批次 · 优先收尾(推荐顺序)
-- [ ] **git commit `merger`**:Wave3 + analyst D-06/模板缓存 + 风控前端/灌库/深潜课7 + 问数 UI 标签 + 文档(你确认后执行)
+- [ ] **git commit `merger`**:Wave3 + analyst D-06/模板缓存 + 风控前端/灌库/深潜课7 + 问数 UI 标签 + **客服问候快路由/闲聊降级** + 答辩清单/课程 + 文档(你确认后执行)
- [x] **风控深潜课改版(方案 1 · 7 模块 · 读者 dev)**:模块 1–7 已写(含写侧并发)· `jinrong-module-risk/index.html` 已拼接
- [ ] **数据分析 · 20 题 live battery**:`uvicorn` + DeepSeek → `python scripts/dev/run_query_battery.py` → 本地写 `scripts/dev/battery_report.json`(**已拍板:不入库** · 见 `.gitignore`)
- [ ] **客服 · 手工验收**:`docs/memory/tests/2026-09-10-customer-1b-r1/MANUAL-CHECKLIST.md`(CUST-9527 持仓/sanitize/R1 种子)
@@ -17,6 +17,22 @@
- [ ] **测试包签核**:`TEST-LOG-2026-09-10-CS-001` → 模块负责人 zhangyong 确认
- [ ] **接口契约发群稿**:`docs/项目管理/接口契约发群-2026-09-09.md`(复制到群即完成)
- [ ] **口径字典/问数模板种子**:本机执行 `seed-analyst-metric-dict.sql` · **`seed-analyst-query-templates.sql`**(若未灌)
+- [ ] **前端 E2E 补齐未跑测试**(见下方「前端 E2E · 未跑测试」;2026-09-10 走查只做到「路由能渲染」,**不是逐功能详测**)
+
+### 2026-09-11 接续(2026-09-10 晚对话落账 · 明天干)
+
+> 工作区已有改动 · **未 commit**。先读 `MEMORY.md` §0(Redis≠页面缓存)· §3 客户「趋势」行。
+
+**Tonight 已做**
+
+- [x] `docs/答辩/答辩知识点清单.md` + 课程对齐(总览模块 8 · 问数 D-06 模块 5)
+- [x] 客服「你好」→ 问候 `keyword_route` + `CHITCHAT_DEGRADED_TEXT`(`customer_service` / `visitor_service` · 单测 `test_greeting_chitchat_without_llm`)
+
+**明天优先**
+
+- [ ] **前端进页转圈**:batch 净值 API 或合并请求 · 客户端 stale-while-revalidate(见 `FRONTEND-HANDOFF.md` §10)
+- [ ] **看板演示口径**:四角色 `/app/{role}/home` 有图表;无 BI 大屏 · D-12 钻取仍 open
+- [ ] **客户持仓产品趋势(若要做)**:拍板 Chat 历史 nav Tool vs 问数引导 · 现状 §9.6 仅问数 `self` 域
### 需拍板(定了再开任务)
@@ -54,6 +70,33 @@
- [x] **问数结果标签**:`AnalystQueryPage` 展示 `template_hit` / `cache_hit`(2026-09-10)
- [ ] **分析对话与问数合一** / **看板钻取** — 规格 D-09/D-12,往后排
- [ ] **Vitest 补测**(可选):`useChatPanel` · `api/analyst.ts` mock
+
+### 前端 E2E · 未跑测试(2026-09-10 走查缺口)
+
+> ⚠️ **口径说明:** 2026-09-10 那轮 E2E **不是逐功能详测**。25 条路由的断言只是「页面渲染出来 + 无 console error + 无 4xx/5xx」,
+> **页面上的数字、图表、交互是否正确一律未验证**。下面这些是**明确没跑**的,**复测前不要当作已验证**。
+> 报告与脚本:`docs/整体测试/前端整体测试交接.md` · 脚手架在仓库外 `C:/Users/Windows/e2e-jinrong/`。
+> **反例佐证:** CHAT-1~4 这四条真实缺陷是**后来别人发现的**,走查时完全漏掉 —— 因为对话线每条只发了 1 句就收工。
+
+**P1**
+
+- [ ] **对话模块详测**(最优先 · CHAT-1~4 已修需回归):多轮连发 · F5 刷新保持同一 session · 会话侧栏分页 · 会话切换/删除/关闭 · 并发发送 · 超长消息 · 断流重连
+- [ ] **接口错误态**:主动打断后端(停 uvicorn / 改 proxy 返 500 / 制造超时),逐页验证 `ApiErrorResult` 是否给出 code / message / trace ID + 可重试 —— 这是 `FRONTEND-HANDOFF.md` §8 白纸黑字的验收项,**本轮完全未验证**
+- [ ] **数据正确性**:拿后端原始响应逐项核对前端渲染值(持仓市值/盈亏、AUM、预警条数、仪表指标卡)—— 本轮**一次都没对过**
+- [ ] **业务边界路径**:
+ - 适当性:只跑了 A-1 阻断,**通过路径未测**
+ - 模拟交易:只跑了放行,**风控拦截路径未测**
+ - 预警处置:只提交一次,**其余状态流转 / 校验失败未测**
+ - AML 扫描:只全量跑一次,**未核对结果正确性**
+
+**P2**
+
+- [ ] **图表正确性**:`@ant-design/charts` 数据映射、空数据、单点、极值 —— 本轮只确认「渲染出来没报错」
+- [ ] **排序/分页正确性**:只确认「点了不崩」,**未验证排序真的有序、翻页真的换内容**
+- [ ] **资产沉淀**:只确认表单渲染,**从未提交过一条**
+- [ ] **客户档案**:只看到 5 个字段(customer_id / display_name / risk_code / risk_expires_at / phone_masked),其余字段渲染未验证(另见 OBS-2 后端 `occupation:"???"` 乱码)
+- [ ] **掩码持久化**:眼睛按钮状态跨路由 / 刷新 / 重登录的行为未全量验证
+- [ ] **PERF-1 复测**:Redis 起到 6380 后重测接口耗时 —— 当前记录的 **2.05s 是 Redis 未起环境下**的测量值,环境修好后数字会变
- [x] **simulate 交易 UI**:风控菜单「模拟交易」+ `RiskSimulatePage` + `api/simulate.ts`(演示说明 · A-3/A-1 预设)
- [x] **接口契约发群稿**(见 `docs/项目管理/接口契约发群-2026-09-09.md` · 复制到群即完成)
diff --git a/docs/整体测试/前端整体测试交接.md b/docs/整体测试/前端整体测试交接.md
new file mode 100644
index 0000000..8cfa536
--- /dev/null
+++ b/docs/整体测试/前端整体测试交接.md
@@ -0,0 +1,373 @@
+# JinRong 前端整体测试交接
+
+> **修复状态(2026-09-10 晚):** 仓库已合入 P0~P2 修复 —— `AuthProvider`(BUG-1/2)· Redis 连接超时 0.5s(PERF-1 降级)· 预警客户 ID 防抖 · `displayLabels`/`formatDateTime` · 对话 `**` 粗体 · 登录重入 · `index.html` lang/title · Ant Design `` · 分析对话 Banner。**CHAT-1/2(2026-09-10 夜):** 流式续聊传 `session_id` + `sessionStorage` 恢复当前会话 · 侧栏分页 · 固定 640px 面板高度。建议复测:客户助手连发 2 轮 → F5 → 仍 1 条会话。E2E 脚本仍在 `C:/Users/Windows/e2e-jinrong/`,BUG-1 修好后可删 `login()` 内 `reload`。
+
+> 交接对象:接手修复的工程师 / 下一轮测试的人
+> 测试日期:2026-09-10 | 分支:`merger`(含当时未提交改动)
+> 测试范围:`http://localhost:5173` 全部 25 条路由 × 4 个角色,真实浏览器端到端
+
+---
+
+## 0. 一句话结论
+
+主路径是**断的**:全新浏览器第一次点演示账号登录必定白屏崩溃,点「退出」会把标签页转死。
+两个都是 `web/src/App.tsx` 里同一处 auth 下发方式导致的,**一处修改可同时修掉**。
+其余为性能(全站 +2s)、展示中文化与交互细节问题,不影响可访问,但影响观感。
+
+---
+
+## 1. 测试范围与覆盖
+
+| 维度 | 覆盖内容 |
+| --- | --- |
+| 路由 | 全部 25 条 × 4 角色(客户 / 理财师 / 分析员 / 风控) |
+| 主流程 | 登录(4 账号)、退出、侧栏折叠展开、菜单高亮、actor ID 复制、面包屑 |
+| 业务 | 持仓、交易明细、产品行情、名下客户、问数工作台、资产沉淀、预警台账、适当性校验、AML 扫描、模拟交易、预警处置弹窗 |
+| 对话 | 4 条 SSE 对话线(客户 / 顾问 / 风控 / 分析)+ 登录页游客试聊 |
+| 边界 | 未登录深链、未知路由、连点提交、刷新保持、空输入 |
+| 响应式 | 1280 / 1024 / 768 / 320px 四档 |
+| 可访问性 | 标签标题、`lang`、`h1` 层级、图片 `alt`、按钮名称、键盘焦点环 |
+
+**结论:25 条路由全部可渲染,无未捕获异常、无 4xx/5xx、无请求失败。** 阻断问题集中在登录/退出这一对状态切换上。
+
+---
+
+## 2. 环境与已知环境因素
+
+| 组件 | 地址 | 备注 |
+| --- | --- | --- |
+| 前端 Vite | `http://localhost:5173` | `HashRouter`,URL 形如 `/#/app/customer/home` |
+| 后端 FastAPI | `http://127.0.0.1:8000` | Vite 代理 `/api` |
+| **Redis** | `.env` 配的是 `127.0.0.1:6380` | ⚠️ **该端口是关的**,见 PERF-1 |
+
+> ⚠️ **复测前先确认 Redis 已启动。** 本次测试期间 6380 无 Redis(本机 6379 上是另一个实例),
+> 这直接造成了 PERF-1。**PERF-1 的测量结果在 Redis 正常后会显著变化**,届时需要复测一次。
+
+复现失败时先排除环境因素:
+
+```bash
+cd web && npm install && npm run dev # 前端
+uvicorn app.main:app --reload # 后端
+redis-cli -p 6380 ping # 应回 PONG,不通就是 PERF-1 的成因
+```
+
+---
+
+## 3. 问题清单(按优先级)
+
+| 编号 | 级别 | 问题 | 位置 | 状态 |
+| --- | --- | --- | --- | --- |
+| BUG-1 | 🔴 阻断 | 首次登录整站崩溃,白屏「应用加载失败」 | `web/src/App.tsx` → **`AuthProvider`** | ✅ 已修 |
+| BUG-2 | 🔴 阻断 | 点「退出」标签页卡死(无限重定向) | 同上 | ✅ 已修 |
+| PERF-1 | 🟠 高 | 每个已鉴权接口固定慢 ~2s,全站 2~6s 才出数据 | `.env` / 鉴权热路径 | ⚠️ 环境仍须起 Redis;代码已降连接超时 |
+| BUG-3 | 🟠 高 | 筛选框无防抖,敲 9 个字发 9 个请求 | `RiskAlertsPage.tsx:177` | ✅ 已修 |
+| I18N-1 | 🟡 中 | 交易明细「日期」显示原始 ISO | `CustomerTradesPage.tsx:42` | ✅ 已修 |
+| I18N-2 | 🟡 中 | 预警台账「创建时间」显示带微秒时间戳 | `RiskAlertsPage.tsx` 列定义 | ✅ 已修 |
+| I18N-3 | 🟡 中 | 后端英文枚举直接露出(6 处) | 各表格列 | ✅ 主要列已映射 |
+| I18N-4 | 🟡 中 | 风控首页大指标硬编码英文枚举 | `RiskAlertsDashboard.tsx:70` | ✅ 已修 |
+| I18N-5 | 🟡 中 | 问数工作台指标卡标题是英文 key | `AnalystQueryPage.tsx:100` | ✅ 已修 |
+| UX-1 | 🟡 中 | AI 回复的 Markdown 未渲染,`**` 直接显示 | 各对话气泡 | ✅ 粗体已渲染 |
+| UX-2 | 🟡 中 | 连点登录按钮发 2 次请求 | `LoginPage` | ✅ 已修 |
+| UX-3 | 🟡 中 | 「分析对话」查数答不了,与问数工作台定位冲突 | 分析对话页 | ℹ️ 仍占位;Banner 已加强 |
+| A11Y-1 | 🟡 中 | 标签页标题是 Vite 默认 `web` | `web/index.html` | ✅ 已修 |
+| A11Y-2 | 🟡 中 | `` 与全中文界面不符 | `web/index.html` | ✅ 已修 |
+| A11Y-3 | 🟡 中 | 登录页有两个 `` | `LoginPage` | ✅ 侧栏改为 h2 |
+| OBS-1 | 🟡 中 | 常驻 AntD 弃用告警;`message` 静态调用不继承主题 | 全局 | ⚠️ 已包 ``;弃用 API 未全量替换 |
+| OBS-2 | ⚪ 低 | 后端种子数据乱码 `occupation:"???"` | 尚未暴露到界面 | 未修 |
+| CHAT-1 | 🔴 高 | **流式对话每轮新建 session**(刷新后同一轮聊天拆成多条会话) | `useChatPanel` 未传 `session_id` 给 `postChatStream` | ✅ 已修 |
+| CHAT-2 | 🟡 中 | 会话列表撑高整页;应固定对话框高度 + 列表分页 | `ChatPanel.tsx` | ✅ 已修 |
+| CHAT-3 | 🟡 中 | 对话区过矮 · 用户气泡字色被 AntD 盖掉 · 无删除入口 | `ChatPanel.tsx` | ✅ 已修(加高 + 白字 + 侧栏/当前删除) |
+| CHAT-4 | 🟠 高 | **已关闭会话**仍出现在侧栏,再点删除 → 409 删不掉 | 列表未筛 active | ✅ 侧栏仅 active(服务端+客户端双滤)+ close-all **或** 逐条 close 兜底 + **须重启 uvicorn** |
+
+---
+
+## 4. 复现步骤与定位
+
+### BUG-1 🔴 首次登录整站崩溃
+
+**复现:**
+
+1. 打开浏览器开发者工具 → Application → Local Storage → 清空(或直接用无痕窗口)
+2. 访问 `http://localhost:5173/#/login`
+3. 点任意「Demo」演示账号按钮
+4. 页面顶部闪现角色欢迎语,随即整页变为
+ `应用加载失败 / Cannot read properties of null (reading 'roleLabel') / 刷新页面`
+
+**复现率 100%。对没登录过的用户,这就是默认路径。**
+
+| 场景 | 结果 |
+| --- | --- |
+| 全新会话 → 点演示账号登录 | ❌ 崩溃 |
+| 崩溃后手动刷新一次 | ✅ 正常 |
+| 已有会话时冷启动 | ✅ 正常 |
+
+**定位 —— `web/src/App.tsx`:**
+
+```tsx
+function AppRoutes() {
+ const auth = loadAuth() // ← 只在 AppRoutes 渲染时读一次
+ return (
+
+ {/* ← 导航时重新 loadAuth(),拿到新值 */}
+ {/* ← 这个 prop 在 element 构建期就求值并冻死了 null */}
+
+ } />
+```
+
+`AppRoutes` 不订阅 router context,`navigate()` 不会让它重渲染。所以首次登录时
+`` 这个 element 仍带着**登录前那次渲染**捕获的 `auth = null`;
+而 `RequireAuth` 在导航时重新 `loadAuth()` 拿到非 null 于是放行 →
+`AppLayout` 收到 `null` → `auth.roleLabel` 抛错 → 根级 ErrorBoundary 接管。
+
+**建议改法:** 不要在 element 构建期求值。把 auth 提到 React state(`AuthProvider` + `useContext`),
+或让 `RequireAuth` 读到 auth 后通过 render-prop / context 下发给子组件。
+
+> ⚠️ 注意 `FRONTEND-HANDOFF.md` §9 明确写着「不要修改 `AuthState`、`jinrong.auth` localStorage key」——
+> 本次修复**不需要动 key、也不需要动 `AuthState` 结构**,只改 auth 的**传递方式**,与那条约束不冲突。
+
+---
+
+### BUG-2 🔴 点「退出」把标签页卡死
+
+**复现:**
+
+1. 任一角色登录成功
+2. 点右上角「退出」
+3. 页面变全白,**浏览器标签页 CPU 跑满、完全无响应**,只能强制关闭标签页
+
+**实测证据(决定性):** 点击「退出」后对 `history.replaceState` 计数:
+
+```
+点击前: 1
+点击后 +0.5s: 1,297
+点击后 +2.0s: 28,257 ← 仍在持续增长
+点击后 +3.0s: 页面 evaluate 无响应(主线程被占满)
+```
+
+**根因(与 BUG-1 同源):** `logout()` 执行 `clearAuth()` + `navigate('/login')`,但
+
+- `/login` 的 element 是「auth 非 null」时构建的 ``
+- `/app` 的 element 里 `RequireAuth` 现场 `loadAuth()` 得到 `null` → 返回 ``
+
+两者互相 ``,形成**无限重定向循环**。每轮 `` 走独立 effect + `replaceState`,
+React 不会抛 "Maximum update depth exceeded",所以**没有任何错误提示,只是静默地把标签页转死**。
+
+> `docs/frontend/FRONTEND-HANDOFF.md` §8 把「退出后回到登录页」列为验收项,
+> 说明这是**回归缺陷**,不是设计如此。
+
+---
+
+### PERF-1 🟠 每个已鉴权接口固定慢 ~2 秒
+
+**实测(连打 3 次结果一致):**
+
+| 接口 | 耗时 |
+| --- | --- |
+| `POST /api/auth/login` | 0.014s |
+| `GET /api/products`(**不带 token**) | 0.066s |
+| `GET /api/products`(带 token) | **2.05s** |
+| `GET /api/customers/CUST-9527/holdings` | **2.06s** |
+| `GET /api/risk/alerts` | **2.06s** |
+| `GET /api/analyst/dashboard` | **2.10s** |
+
+**根因:** `.env` 配 `REDIS_URL=redis://127.0.0.1:6380/0`,6380 上没有 Redis。
+`auth_service.is_revoked()` 在**每个请求的鉴权路径**上同步查 jti 黑名单,连不上时 fail-open 放行 ——
+**但连接失败本身要耗时 ~2.4 秒**:
+
+```
+redis err after 2.45s: ConnectionError Error 10061 connecting to 127.0.0.1:6380
+```
+
+于是「每个接口 +2s」被累加到每次页面加载:客户「我的持仓」要 ~5s 才脱离骨架屏,
+「交易明细」要 ~5s 才出 2 行数据。**整个 Demo 用起来就是「卡」**,很容易被误认为页面坏了。
+
+**建议:**
+
+1. 环境上按 README 起 Redis(**先做这步,再复测性能**);
+2. 代码上给鉴权热路径的 Redis 查询加**熔断**:首次连接失败后 N 秒内直接 fail-open 不再重试;
+ 或把 `socket_connect_timeout` 从 3s 降到 0.3s 量级。
+
+否则任何一次 Redis 抖动都会放大成全站慢 —— 这是 fail-open 设计没有配套超时预算的典型问题。
+
+---
+
+### BUG-3 🟠 筛选输入框没有防抖
+
+`web/src/pages/risk/RiskAlertsPage.tsx:177` 附近,「客户 ID」的 `onChange` 直接 `setCustomerId`,
+该 state 是 `load` 的 `useCallback` 依赖 → `useEffect` 每次重跑。
+
+**实测:** 逐字输入 `CUST-1002`(9 个字符)→ **发出 9 次 `/api/risk/alerts`**。
+
+叠加 PERF-1 后,用户每敲一个字都要等 2 秒、页面反复重渲染。
+(`useAsyncSequence` 只挡住过期写入,挡不住请求风暴。)
+
+**建议:** 加 300ms 防抖,或改成回车 / 失焦才提交。
+
+---
+
+### I18N-1 ~ I18N-5 展示层
+
+| 编号 | 现象 | 位置 |
+| --- | --- | --- |
+| I18N-1 | 交易明细「日期」渲染成 `2026-08-15T14:20:00`(列宽 120px 还截断) | `CustomerTradesPage.tsx:42`,`dataIndex: 'traded_at'` 无 render |
+| I18N-2 | 预警台账「创建时间」渲染成 `2026-09-10T16:54:50.398000`(180px 放不下) | `RiskAlertsPage.tsx` 列定义 |
+| I18N-3 | 英文枚举漏出:`aml` / `large_amount` / `pending_review` / `confirmed_normal` / `subscribe` / `confirmed` / `money` / `bond` / `stock` / `mixed` | 各表格列 |
+| I18N-4 | 风控首页大指标 `mainLabel="pending_review 总数"` | `RiskAlertsDashboard.tsx:70` |
+| I18N-5 | 问数工作台指标卡标题是英文 key:`CUSTOMERS 33` / `HOLDINGS 7290960` / `PENDING 1` / `DICT_COUNT 0` | `AnalystQueryPage.tsx:100` `label={key}` |
+
+> **I18N-3 / I18N-5 的修法很直接:页面里已经有中文映射了。**
+> 例如 `RiskAlertsPage` 的筛选下拉就是 `{value:'aml', label:'AML'}`,表格列只是没复用。
+> I18N-5 更明显 —— 同页面下面那行「可用维度:客户总数 · 总持仓规模 · 今日交易笔数与金额 ·
+> 待处理预警数 · 口径字典资产数」**已经是中文**,说明后端 `cards` 字段本就是给这里用的,只是没接上。
+>
+> 建议抽一个公共 formatter(时间 / 枚举 → 中文)放 `src/utils`,按 `FRONTEND-HANDOFF.md` §6.3 补纯函数测试。
+
+---
+
+### UX-1 ~ UX-3
+
+- **UX-1 Markdown 未渲染**:所有对话线都用 `` 原样输出,用户看到成排 `**`。
+ 实测片段:`当前共有 **1 条**待处理预警`、`1. **有效身份证件** 2. **本人银行卡**`。
+- **UX-2 连点登录发 2 次请求**:`LoginPage.handleLogin` 无重入保护,`