-- 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;