feat(ai): W1-AI-CLOSURE 超级 Sprint — 9 APP 全链路收口 + chat 上下文真激活

Phase 2.3 chat 上下文捕获链路从未真正激活到完整工作:
- 14 处 ai-float-button 补 sourcePage,chat.ts 三分支同步设 pageFilters.contextId
- 后端 page_context 4 层 BUG 修(列名错位 + RLS site_id 未重设)
- xcx_chat filters.pop 破坏 body.page_context 引用 — dict() 浅拷贝隔离
- chat 流式 markdown 实时解析(表格/标题/列表/加粗 + KPI 富卡)
- reference_card KPI 富卡接入 SSE 路径,db 真写入
- 维客线索 source 显示规则:AI 来源用机器人 icon 替代长文字

数据库:
- public.member_retention_clue 加 emoji + runtime_mode + sandbox_instance_id
- biz.ai_run_logs 加 assistant_id + 复合索引
- chk_ai_cache_type CHECK 约束 8 类应用名
- cache_type / app_type 命名统一(app6_note / app7_customer / app8_consolidation)
- 历史 emoji 抽取脚本 44/44 成功

后端 silent failure 修:
- cleanup_service WHERE app_type → cache_type(90 天清理 + 20K 上限重新生效)
- _build_ai_insight 字段错位修复(app4 → app7 + 字段对齐 prompt schema)
- task_manager talkingPoints 改 app5_tactics + tactics 字段
- task_manager aiSuggestion 改取 one_line_summary
- cache_service.CACHE_EXPIRY_DAYS 加 app2a_finance_area
- WS /ws/ai-cache 加 token + JWT + site_id 校验(P0 信息泄露漏洞)
- internal_ai token 改 hmac.compare_digest

工具/文档:
- main.py 加 RotatingFileHandler logs/backend.log + uvicorn /health 过滤
- 新建 utils/clue_category.py(VI 6 类配色 + emoji fallback + source 显示规则)
- 新建 utils/markdown.ts(轻量 md 转 rich-text 解析 + streaming 容错)
- audit + 数据库变更说明 + backlog §七 #14 收口 + #15-#38 残余子任务
- backlog 追加 §十一 App1 参数/MCP/沙箱审计 + §十二 百炼/SQL MCP 主任务线

实地 MCP 走查:14 入口数据层 + 5 代表入口 sourcePage 注入 + customer-detail 全模块 + chat md 渲染 + reference_card 富卡 都已验证。9 项预先 BUG/UX 登记 §七 #29-#38 后续修复。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-05-06 16:39:07 +08:00
parent c9c2bce101
commit 2dfc926f96
56 changed files with 1983 additions and 278 deletions

View File

@@ -0,0 +1,172 @@
-- 2026-05-06
-- W1-AI-CLOSURE 超级 Sprint 组 1 — Schema 修复 + 命名统一
--
-- 背景:
-- AI 9 APP 全链路调研发现以下劣化:
-- 1. emoji 嵌入 summary 字符串(dispatcher.py:582-584),数据库 member_retention_clue
-- 表无独立 emoji 列,违反"字段独立性"哲学
-- 2. member_retention_clue 表无 runtime_mode / sandbox_instance_id,沙箱模式下 App8
-- 写入会污染 prod 视图(其他 7 张 ai_* 表都有这两列,本表是唯一例外)
-- 3. ai_run_logs 缺 assistant_id 列,App4/App5 这种 (assistant, member) 二元任务
-- 失败定位困难
-- 4. cache_type / app_type 双名长期共存:
-- ai_cache.cache_type = app7_customer_analysis / app8_clue_consolidated
-- ai_run_logs.app_type = app7_customer / app8_consolidate
-- 违反"schema 一致性"哲学,统一为应用名(与 prompt 文件名一致):
-- app7_customer / app8_consolidation
--
-- 影响范围:
-- - public.member_retention_clue:加 3 列(emoji + runtime_mode + sandbox_instance_id)
-- - biz.ai_run_logs:加 assistant_id 列 + 复合索引补建
-- - biz.ai_cache + biz.ai_run_logs:cache_type / app_type 命名统一
-- - 后端 dispatcher / cleanup_service / cache_service 代码相应修改(组 2-5)
--
-- 兼容性:
-- - emoji 列默认空字符串,新写入由 dispatcher 移除拼字符串后独立写入(组 3)
-- - runtime_mode / sandbox_instance_id 默认 'live',与其他 ai_* 表一致
-- - 命名 UPDATE 后,旧字符串 'app7_customer_analysis' / 'app8_clue_consolidated' /
-- 'app8_consolidate' 在数据库中绝迹,代码侧必须同步更新
-- - 回填脚本 scripts/ops/backfill_retention_clue_emoji.py 抽取 summary 嵌入的 emoji
-- 到 emoji 列,本迁移不做该回填(脚本走 dry-run + 实跑两步)
--
-- 回滚策略:见末尾"回滚参考"块。
--
-- 验证 SQL(执行后跑):
-- 1. SELECT column_name FROM information_schema.columns
-- WHERE table_schema='public' AND table_name='member_retention_clue'
-- AND column_name IN ('emoji','runtime_mode','sandbox_instance_id');
-- 预期 3 行
-- 2. SELECT column_name FROM information_schema.columns
-- WHERE table_schema='biz' AND table_name='ai_run_logs'
-- AND column_name='assistant_id';
-- 预期 1 行
-- 3. SELECT cache_type, count(*) FROM biz.ai_cache
-- WHERE cache_type IN ('app6_note_analysis','app7_customer_analysis',
-- 'app8_clue_consolidated','app8_consolidate')
-- GROUP BY 1;
-- 预期 0 行
-- 4. SELECT app_type, count(*) FROM biz.ai_run_logs
-- WHERE app_type IN ('app6_note_analysis','app7_customer_analysis',
-- 'app8_consolidate','app8_clue_consolidated')
-- GROUP BY 1;
-- 预期 0 行
-- 5. SELECT runtime_mode, count(*) FROM public.member_retention_clue GROUP BY 1;
-- 预期 'live' 一行覆盖全部历史
BEGIN;
-- ── 1) public.member_retention_clue: 加 emoji + runtime_mode + sandbox_instance_id ──
ALTER TABLE public.member_retention_clue
ADD COLUMN IF NOT EXISTS emoji character varying(8) NOT NULL DEFAULT '';
ALTER TABLE public.member_retention_clue
ADD COLUMN IF NOT EXISTS runtime_mode character varying(20) NOT NULL DEFAULT 'live',
ADD COLUMN IF NOT EXISTS sandbox_instance_id character varying(64) NOT NULL DEFAULT 'live';
UPDATE public.member_retention_clue
SET runtime_mode = 'live', sandbox_instance_id = 'live'
WHERE runtime_mode IS NULL OR sandbox_instance_id IS NULL;
COMMENT ON COLUMN public.member_retention_clue.emoji IS
'维客线索独立 emoji 字段(由 App8 prompt 输出 emoji 字段直接写入,不嵌 summary);本字段于 W1-AI-CLOSURE 引入,历史数据由 backfill_retention_clue_emoji.py 回填。';
COMMENT ON COLUMN public.member_retention_clue.runtime_mode IS
'运行模式:live / sandbox;sandbox 模式写入隔离实例 ID,live 与其他门店共享 prod 视图。';
COMMENT ON COLUMN public.member_retention_clue.sandbox_instance_id IS
'sandbox 模式写入隔离实例 ID;live 模式固定为 live。';
-- ── 2) biz.ai_run_logs: 加 assistant_id 列 + 复合索引 ──
ALTER TABLE biz.ai_run_logs
ADD COLUMN IF NOT EXISTS assistant_id bigint;
COMMENT ON COLUMN biz.ai_run_logs.assistant_id IS
'App4/App5 这类 (assistant, member) 二元关系任务的助教 ID,便于失败定位;App2/App3/App6/App7/App8 类任务为 NULL。';
CREATE INDEX IF NOT EXISTS idx_ai_run_logs_assistant_member
ON biz.ai_run_logs (site_id, assistant_id, member_id, created_at DESC)
WHERE assistant_id IS NOT NULL;
-- ── 3) cache_type / app_type 命名统一(app6 + app7 + app8) ──
-- 双名长期共存违反 schema 一致性,统一为与 prompt 文件名一致的应用名:
-- app6_note_analysis -> app6_note
-- app7_customer_analysis -> app7_customer
-- app8_clue_consolidated / app8_consolidate -> app8_consolidation
-- 注意:cache_type 有 chk_ai_cache_type CHECK 约束,需先 DROP 再 UPDATE 再 ADD 新约束。
ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_type;
UPDATE biz.ai_cache
SET cache_type = 'app6_note'
WHERE cache_type = 'app6_note_analysis';
UPDATE biz.ai_cache
SET cache_type = 'app7_customer'
WHERE cache_type = 'app7_customer_analysis';
UPDATE biz.ai_cache
SET cache_type = 'app8_consolidation'
WHERE cache_type IN ('app8_clue_consolidated', 'app8_consolidate');
UPDATE biz.ai_run_logs
SET app_type = 'app8_consolidation'
WHERE app_type IN ('app8_consolidate', 'app8_clue_consolidated');
UPDATE biz.ai_run_logs
SET app_type = 'app6_note'
WHERE app_type = 'app6_note_analysis';
UPDATE biz.ai_run_logs
SET app_type = 'app7_customer'
WHERE app_type = 'app7_customer_analysis';
-- 注意:ai_run_logs 中 app7 测试库已经是 'app7_customer'(102 行),app6 在测试库
-- 无数据;UPDATE 旧名字若不存在则 0 行影响,幂等安全。
ALTER TABLE biz.ai_cache
ADD CONSTRAINT chk_ai_cache_type
CHECK (cache_type IN (
'app2_finance',
'app2a_finance_area',
'app3_clue',
'app4_analysis',
'app5_tactics',
'app6_note',
'app7_customer',
'app8_consolidation'
));
COMMENT ON CONSTRAINT chk_ai_cache_type ON biz.ai_cache IS
'AI 8 个写缓存的应用类型(app1_chat 走 ai_messages 不进缓存);命名与 prompt 文件名一致。';
-- ── 4) 索引收尾(若旧索引引用旧 cache_type 字符串,无影响 — 索引按当前值重建) ──
COMMIT;
-- =============================================================================
-- 回滚参考(测试库回滚先跑此块,正式库回滚需评估业务影响):
-- =============================================================================
-- BEGIN;
--
-- -- 命名 UPDATE 回滚(注意:旧名字 app8_consolidate vs app8_clue_consolidated 已合并,
-- -- 回滚无法精确还原,只能选其一;以下示例选 ai_cache 的旧描述名)
-- -- ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_type;
-- -- UPDATE biz.ai_run_logs SET app_type = 'app8_consolidate' WHERE app_type = 'app8_consolidation';
-- -- UPDATE biz.ai_run_logs SET app_type = 'app7_customer_analysis' WHERE app_type = 'app7_customer';
-- -- UPDATE biz.ai_run_logs SET app_type = 'app6_note_analysis' WHERE app_type = 'app6_note';
-- -- UPDATE biz.ai_cache SET cache_type = 'app8_clue_consolidated' WHERE cache_type = 'app8_consolidation';
-- -- UPDATE biz.ai_cache SET cache_type = 'app7_customer_analysis' WHERE cache_type = 'app7_customer';
-- -- UPDATE biz.ai_cache SET cache_type = 'app6_note_analysis' WHERE cache_type = 'app6_note';
-- -- ALTER TABLE biz.ai_cache ADD CONSTRAINT chk_ai_cache_type CHECK (cache_type IN
-- -- ('app2_finance','app2a_finance_area','app3_clue','app4_analysis','app5_tactics',
-- -- 'app6_note_analysis','app7_customer_analysis','app8_clue_consolidated'));
--
-- DROP INDEX IF EXISTS biz.idx_ai_run_logs_assistant_member;
-- ALTER TABLE biz.ai_run_logs DROP COLUMN IF EXISTS assistant_id;
--
-- ALTER TABLE public.member_retention_clue
-- DROP COLUMN IF EXISTS sandbox_instance_id,
-- DROP COLUMN IF EXISTS runtime_mode,
-- DROP COLUMN IF EXISTS emoji;
--
-- COMMIT;