工作台总设计 · 附录 B · 微信数据平台盘点(normalize)
~/wechat-data/normalize/(docs / etl / serve / views / ops)后的盘点。全程只读;未打开任何 */plain/ 与 *.db;未抄任何消息内容、姓名、wxid。核心一句话:这是一个纯读 / 纯分析平台。源微信库神圣只读,产出库
normalize.db 是 SQLite 单文件,对外只有一个 stdlib http.server 写的只读 API。系统内不存在任何向微信发消息的通路。实体清单
层级(docs/schema-doc.md + etl/run_etl_full.py):Layer 1 = 每次全量 rebuild 被 DROP 重建;Layer 2 = AI 建议层受保护禁 drop;Layer 3 = 运营表整表 carry。命名规范(docs/schema-naming.md):_v1 基表 / _f_v1 过滤视图 / _d_v1 反范式视图 / _safe_v1 投影视图 / _final_v1 裁决视图。
A1 身份与会话骨架(Layer 1)
| 实体 | 类型 | 关键列 | 来源 | 已知问题 |
|---|---|---|---|---|
party_v1 | 基表 | global_party_id(PK, sha256(wxid)[:16])、raw_wxid_hidden(PII)、display_name、display_status、display_source、remark_latest、nick_name_latest、first_seen_ts | etl/build_party.py | 三个 Restricted PII 列禁止进业务视图 |
sender_account_v1 | 基表 | (global_party_id,account,shard) PK、sender_id_by_shard、msg_count、per-account remark/nick | ETL | 含 PII,隐私规范明令业务视图不得引用;但 workbench_customer_card_v1 的 R6-49 版本 JOIN 了它 |
message_v1 | 基表 | PK(account,shard,thread_id,msg_local_id);sort_seq/server_seq/ts_utc/ts_jst_date;sender_global_party_id、sender_display_name_snapshot、sender_role_snapshot、direction、local_type、status、message_content、content_status、media_type、media_path、quoted_message_id、recalled_message_id、voice_text、lineage 四列 | etl/extract_messages.py + enrich_bronze.py | message_content/voice_text = Restricted;image_ocr_text 需下轮 rebuild 才回填 |
msg_normalized_v1_cache | 物化缓存 | 与 message_v1 同构 | etl/populate_cache.py | conversation_v1 走 message_v1 不走 cache |
thread_v1 | 基表 | PK(account,shard,thread_id)、thread_type(private/chatroom/openim/self_only)、counterparty_global_party_id、owner_global_party_id、chat_room_username、first/last_msg_ts、msg_count | etl/derive_threads.py | counterparty 语义见 docs/counterparty-canonical-v1.md:"global" 只指 wxid 假名,不是跨号合人;daemon 增量新 private 线程曾永久 NULL(50 条) |
thread_membership_v1 / thread_speaker_v1 | 基表 | 当前成员 / 历史发言者 | etl/build_thread_membership.py | — |
thread_facts_v1 | 派生基表 | msg_count_7d/30d/90d、last_msg_ts、last_customer_msg_ts、last_employee_reply_ts、days_since_last_employee_reply、has_unanswered_customer_msg、first_response_seconds、sla_compliant + V2 增列 last_customer_msg_content、last_employee_reply_content | etl/compute_lifecycle.py + compute_sla.py | 工作台队列的真正心脏;两个 *_content 列由 ALTER 动态加,不在 schema-doc 里 |
role_assignment_v1 | 基表 | role_final、role_suggested、confidence、source_layer、factors_json、is_locked/locked_by/locked_at、superseded_at | etl/classify_roles.py | Layer3 vendor 恒 conf=0.85、Layer4 不送审 → audit_queue_v1 无自然入口 |
audit_queue_v1 | 基表 | 灰区人审队列 | classify | 名存实亡(无入口) |
avatar_asset_v1 | 基表 | 头像 URL/缓存/状态 | avatar_daemon | 3 条毒循环 42+ 天 |
account_lineage_v1 | 基表 | account、status、owner_wxid_canonical、supersedes/superseded_by、live_since_ts、frozen_at_ts | etl/seed_account_lineage.py | — |
remark_avatar_change_log_v1 | 日志 | 只存 old/new 哈希 | ETL | carry-whole |
normalize_run_v1 / normalize_manifest_v1 / dq_result_v1 | 基表 | run/build 元数据、DQ 结果(7 项检查) | etl/dq_runner.py | — |
event_log_v1 / subscriber_v1 / delivery_attempt_v1 | 基表 | webhook 事件总线:8 种事件含 customer_unanswered、dq_alert、role_change | serve/events.py | 出站 webhook(HMAC),不是微信通路 |
api_token_v1 | 基表 | name、token_hash、scope CHECK(read/read_staff/admin)、active、last_used_at、employee_id、scope_json | r6_20 | scope_json.account_filter 有解析函数但路由层未接线 |
bronze_message_raw_v1 | 基表 | decoder 对齐冻结 schema | r6_50 | 尚未进生产库 |
A2 客户业务层
| 实体 | 类型 | 说明 |
|---|---|---|
lx_customer_profile_v1 | 基表 | customer_tier(VVIP/CIP/VIP/普通)、tier_source、trip_start/end_date、destination、hotel、agency、parse_status;由 etl/parse_tiers.py 从备注文本解析 |
lx_customer_lifecycle_v1 | 基表 | lead_age_days、is_active_lead、is_trip_in_progress、is_post_trip、is_repeat_customer、last_customer_msg_ts、last_employee_reply_ts、days_since_last_employee_reply;derived_daemon 每 30 min |
customer_v1 | 视图 | party + role 过滤 customer;列里没有 role_current |
customer_global_v1 | 视图 | 同上 + role_current/ts_created_utc/ts_updated_utc |
customer_f_v1 | 视图 | party JOIN role_current WHERE role='customer' |
customer_dashboard_d_v1 / lx_customer_dashboard_v1 | 视图 | party+profile+lifecycle+avatar 反范式;工作台 customer_card 实际读的是 lx_customer_dashboard_v1 |
customer_api_safe_v1 | 视图 | 6 列安全投影。已悬空(见 G) |
workbench_customer_card_v1 | 视图 | 工作台客户卡。已悬空 |
customer_tier_suggestion_v1 / customer_tier_human_override_v1 | Layer2 基表 | 复合键 (account_id,customer_wxid,valid_from_ts_utc),双时态 |
customer_tier_final_v1 | 视图 | COALESCE(override, suggestion) + final_source in (human/ai) |
customer_notes_v1 | Layer3 基表 | note_type、visibility(默认 staff_only)、body、author_employee_id。无任何 API 写入口 |
customer_relationships_v1 | Layer3 基表 | party_a/party_b/relationship_type/source/confidence |
deleted_party_tombstone_v1 | Layer3 基表 | 由 serve/delete_customer.py 按需建,不在 R6 migration 清单里——G 项悬空的根因 |
A3 行程 / 任务 / SLA(Layer 3,全部 carry)
trip_v1:customer_global_party_idFK、destination、start/end_date、nights、adults/children、status(默认 inquiry)、source_thread_idtrip_internal_v1:hidden_profit_cny、revenue_cny、cost_breakdown_json、attention_level、escalation_owner_employee_id、is_loss_leader(Restricted)quote_v1:trip_idFK、version、total_cny、pricing_breakdown_json、statustask_v1:title/description/priority(默认3)/status/assigned_to_employee_id/due_ts_utcsla_v1:tier×metric×percentile唯一。种子只有 4 行 first_response p50:VVIP 300s / CIP 600s / VIP 900s / default 900semployee_binding_v1:employee_id、global_party_id、display_name、tier、accounts_json、email、active。列漂移:migration 9 列,生产手工 ALTER 成 11 列employee_v1/vendor_v1:party+role 过滤视图;role_current_v1:最新未 superseded 的 role;thread_member_v1:membership ∪ speaker
A4 AI 层(Layer 2,六表受保护)
| 实体 | 说明 | 谁在写 |
|---|---|---|
message_reply_suggestion_v1 | suggestion_id、account、thread_id、reply_to_msg_sort_seq、suggested_reply_text、suggested_reply_type(text/template/quick_reply)、confidence、reasoning、status(pending/accepted/rejected/expired)、reviewed_by | 全仓无任何 INSERT。生产者不存在 |
ai_artifact_v1 | artifact_type/source_type/source_id/content_text/content_json/model_version/confidence + 双时态 | 无写入者 |
ai_action_log_v1 | action_type/target_type/target_id/action_detail_json/result_status/error_message | 无写入者 |
thread_agent_assignment_v1 | assigned_agent_type(human / ai_auto / ai_assisted / none)、assigned_agent_id、assignment_reason、双时态 | 无写入者 |
A5 治理 / 隐私 / DQ
pii_columns_v1:PII 列注册表,14 条种子,sensitivity_level、masking_fn。种子里 7 条指向不存在的列(注册表与真实 schema 不一致)pii_access_log_v1:列级访问审计。serve/auth.py:log_pii_access()已实现,路由层没有任何调用点dq_freshness_v1:per-accountlag_seconds/is_stale,被/v1/health读取
A6 请求中提到但不存在的实体
customer_intent_v1、data_health_v1、除 lx_view_sales_response_v1 外的 role_*_v1、employee_*_v1(除 employee_v1 / employee_binding_v1)全仓零命中。设计工作台时不要假设它们存在。
A7 销售队列视图族(关键风险)
| 视图 | 引用处 | DDL 位置 |
|---|---|---|
lx_view_b1_customer_profile | serve/queries.py:410 | 全仓无 DDL |
lx_view_b3_sales_queue | serve/queries.py:450 | 全仓无 DDL |
lx_view_s2_followup_sla | serve/queries.py:495, 525 | 全仓无 DDL |
lx_view_sales_response_v1 | 仅 SQL 文件自身 | views/lx_view_sales_response_v1.sql,不在 R6_MIGRATIONS 清单内,ETL 不自动应用 |
internal_brand_account_v1、file_ocr | parity_gate CARRY_EXACT | 无 DDL,靠 _carry_log_layers() 从 prod sqlite_master 动态复刻 |
lx_customer_employee_v1、account_shift_window_v1 | lx_view_sales_response_v1.sql:138,179 | 无 DDL |
→ /v1/lx/* 四个端点依赖的三个视图,定义只存在于活库里(或根本不存在),代码仓无法复现。工作台立项前必须先考古的第一件事。
API 清单
B1 openapi.yaml 的 12 个端点(全部 GET)
| # | 端点 | 参数 | 响应 | scope |
|---|---|---|---|---|
| 1 | GET /v1/health | — | {ok, integrity_check, msg_count, last_msg_ts} + per-account 新鲜度 | read |
| 2 | GET /v1/health/deep | — | {ok, integrity_check, foreign_key_violations} | read_staff |
| 3 | GET /v1/accounts | — | 账号谱系列表 | read |
| 4 | GET /v1/customers | tier、active_only、account、limit(50)、offset | 分页客户列表 | read |
| 5 | GET /v1/customers/{id} | path | 客户详情 / 404 | read |
| 6 | GET /v1/threads/unanswered | min_days(1)、account、limit | 未回线程 | read |
| 7 | GET /v1/threads/{account}/{thread_id}/messages | limit、before_ts | 按时间正序消息 | read |
| 8 | GET /v1/threads/{account}/{thread_id}/workbench | include(默认 messages,customer_card,trip,tasks,suggestions,artifacts)、msg_limit | 复合对象 | read_staff |
| 9 | GET /v1/media/avatar/{party_id} | path | JPEG 或占位 SVG | read_staff |
| 10 | GET /v1/media/{account}/{thread_id}/{message_id} | path | 媒体文件 | read_staff |
| 11 | GET /v1/artifacts/{artifact_id} | path | artifact 内容 | read_staff |
| 12 | GET /v1/reports/weekly/{week} | format=json/md | 周报 | read |
B2 未记录但代码里真实存在的 6 条(serve/api.py:86-130)
GET /v1/openapi.yaml · GET /v1/lx/customer_profile(读 lx_view_b1_customer_profile)· GET /v1/lx/sales_queue(读 lx_view_b3_sales_queue)· GET /v1/lx/unanswered(读 lx_view_s2_followup_sla,按 minutes_waiting DESC)· GET /v1/lx/unanswered_by_employee · DELETE /v1/customers/{id}(admin,12 张表级联删除 + tombstone)。POST /v1/tokens 出现在文档里,但 do_POST 无条件 405——实际不存在。
B3 servers / hosts —— 三个端口互相矛盾
openapi 写 127.0.0.1:8765 + https://workbench.luxingtravel.com;api.py 默认 8765;实际 launchd 是 --port 9000,且未传 --require-auth;round6 文档写 8023。两点要害:生产 API 进程没开鉴权(require_auth=False 时整条 do_GET 不校验 token,read_staff 形同虚设,DELETE 例外);只绑 127.0.0.1,无 HTTPS、无对外监听,仓里没有 nginx/反代配置。
B4 写接口盘点
除 DELETE /v1/customers/{id} 外,全平台零写端点。 没有 notes 写入、task 创建/流转、assignment 变更、suggestion 采纳/驳回、回复发送。鉴权:Bearer token sha256 存 hash,三档 SCOPE_RANK = {read:1, read_staff:2, admin:3};CORS 白名单 localhost:5173 / :3000 / workbench.luxingtravel.com;错误格式统一 {error, code}。
工作台逻辑
C1 销售队列怎么算(三条并存、互不统一)
(a) thread_facts_v1.has_unanswered_customer_msg——最底层事实位,derived_daemon 每 1800s 刷;/v1/threads/unanswered 走它。 (b) /v1/lx/* 三兄弟——读三个无 DDL 的 lx_view_*,排序 has_unanswered DESC, last_customer_msg_ts DESC / minutes_waiting DESC;assigned_employee_gpi + assignee_confidence 说明视图内已做员工归属推断。 (c) lx_view_sales_response_v1(星星 spec + 阿麦实施,粒度 = account × customer_gpi)——没有任何 API 消费它,但业务逻辑最完整:wait_during_working_hours_min(14 天生成器 × account_shift_window_v1 班次窗口,只累计工作时段等待);is_closing_phrase(约 40 项收尾语白名单);is_explicit_closure(8 条流失措辞);follow_up_bucket 七态:noise → closed → no_action → urgent(VVIP 10 min / CIP 15 / VIP 15 / 其他 30)→ elegant_24h → elegant_48h → cold_lead(>7d)→ monitoring。 注意这套 tier 阈值(分钟)与 sla_v1 种子(秒)是两套独立数字,工作台要先统一。
C2 客户卡怎么组装(serve/workbench.py)
get_workbench() 按 include 拼 6 段:customer_card ← lx_customer_dashboard_v1;trip ← trip_v1(confirmed/booked/in_progress 最新 1 条);tasks ← task_v1(open/in_progress);suggestions ← message_reply_suggestion_v1 全量倒序(不过滤 status);artifacts ← ai_artifact_v1(valid_to IS NULL);messages ← message_v1 倒查后正序。 workbench_customer_card_v1 视图(r6_49)输出 tier、lifecycle_stage、active_trip_id、open_tasks_count、total_trips、total_revenue_cny、常量 leak_guard_marker='⛔CROSS_ACCOUNT_DATA_FORBIDDEN',配套 assert_account_scope.py 防跨号泄漏。但当前没有任何代码读它。
C3 回复建议怎么生成
没有生成。 建表、carry、读取、单测都有,零 INSERT。docs/HANDOFF.md:530:"LLM classifier not implemented";round6 把 "Real LLM integration" 列为 R7+ 候选。表结构为将来预留(model_version、confidence、reasoning、reviewed_by、status 四态)。
C4 workbench.luxingtravel.com 是什么
尚不存在的前端域名占位符。只出现在 openapi servers、CORS 白名单、契约文档。仓内无任何前端代码。round6 R7+ 候选首条:"Web frontend — deferred to AI OS + real dev team per Ling msg 224"。
能否回写微信
不能。全平台不存在任何向微信发送消息的机制。 grep send_message|send_reply|自动回复|wechaty|itchat|appium|osascript.*WeChat|模拟点击 仅命中 2 处内部字段回填。文档明文:源库 ?mode=ro;serve/api.py:234 POST not supported on read API。 唯一与"回复"沾边的红线是 docs/counterparty-canonical-v1.md §2,约束的是上下文组装:thread_v1.counterparty_global_party_id 是唯一可进对客回复组装的身份层;跨号合成画像 internal_person_overlay_v1 必须零 FK/零 view 通向 reply 组装链路;reply 组装只许 JOIN ON (account, party_id) 双键。 结论:若工作台要做"一键回复",发送侧需要一个当前完全不存在的新组件,并且必须先过 counterparty 双锁与内阁红线。
治理与人员
Lane(2026-07-07 Ling 裁):拓拓 arch 架构裁决 · 阿麦 impl / 生产写唯一执行人(lx01 单写者)· 星星 业务判决 / 回归 / human_review 签名 / lx_view_sales_response_v1 spec 作者 / 内阁 LXCEO · 锐锐 on-call forensic · Ling 创始人 founder hard 10 条 + 最终裁决。 内阁(cabinet):docs/cabinet_briefing_sop.md——每个 OMO 任务以"内阁简报包"开场:founder hard 10 条、客户 tier 锁死 4 档(VVIP/CIP/VIP/普通,无 UHNW)、4 账号身份(dmd/luxingtravel/zhenguo LIVE,lux 历史,no silent merge)、PII(wxid 不得进业务视图)、升级矩阵、trust-but-verify、产物闸。 守门:parity_gate(判据 1-11,prod-ref 必须静态 .bak)· employee parity guard(seed 真源 FINAL103,变更=星星签→拓拓 ratify→阿麦执行)· human_review 人工终审(is_locked=1 活过任何 refresh)· freshness monitor · enforce_display_contract(扫 7 个业务视图 TEXT 列的 wxid_%/@chatroom)。 freeze flags:etl/FREEZE-NO-FULL-REFRESH-BEFORE-D3a.md 存在则拒跑全量;unfreeze-precheck.sh 四件全真才放行。 核心原则:full-diff > enumerate-known | staff/客户身份 = business fact | 红线靠结构守非纪律守 | guard 必 fail-loud | 承重写 = 备份→执行→验证 + owner 双闸。
数据规模与账号
账号 4 个:dmd、lux(已退役,并入 luxingtravel)、luxingtravel、zhenguo;LIVE 三个。硬约束 lux ≠ luxingtravel,禁止静默跨号合并。 体量(2026-07-07):message_v1 3.29M · party_v1 38.7K · thread_v1 48K · customer 35.9K / employee 103 / vendor 180+ · file_ocr 287K · voice 41K+。 ETL:源(plain decode·只读) → bronze → normalize.db → enrich.db → serving;全量 11 stage 原子 swap;R6 migration 26 个 SQL;carry 三件套。 节奏(ops/launchd/ 7 个服务):etl_incremental_daemon 常驻 3s 轮询;derived_daemon 30 min;avatar_daemon 常驻;etl_full 周日 02:00;backup 每日 03:15(异地 LG01JP);logrotate 04:00;dq_weekly 周一 05:00;api KeepAlive。 备份纪律:活库禁止裸 copy,唯一正确 sqlite3 normalize.db ".backup"。
已知债务与坑
P1 业务面正确性
- 悬空视图族(最高优先):
customer_api_safe_v1与workbench_customer_card_v1的 WHERE 结尾引用deleted_party_tombstone_v1,该表不在 migration 清单里,新建/rebuild 的库一查即炸。消费面已绕行(workbench 读lx_customer_dashboard_v1),所以线上没炸,但两个"官方"视图是死代码。修法 = 重建视图去 tombstone 引用。
附加坑(本次新发现):/v1/lx/*依赖的lx_view_b1/b3/s2在代码仓完全没有 DDL;lx_customer_employee_v1、account_shift_window_v1也无 DDL。一次全量 rebuild 后/v1/lx/*与lx_view_sales_response_v1会集体消失。 - 灰区机制死路:
audit_queue_v1无自然入口。 _merge_ocrserving 联动:image_ocr_text要下次 rebuild 才回填。
P2 管线健康
- avatar_daemon 毒循环(patch 已备候点头)。5. S2b 周期慢 cycle:真凶是 06-23 起 lx01 机器级 I/O 退化(放大器候选 opencode 820% CPU、llama-server 常驻 ~26GB),contact-delete detector HOLD 87 轮、659 待裁删除积压。6.
updated_at命名违规。7. classify 双实现。8. 活库前提型测试污染单元套件。
P3 与 sweep
9–13:daemon 冷扫优化、备份命名、媒体回填 23.88%→80%、附件回填、/tmp 依赖。S1 LOCKED_GPIDS_7 死常量;S2 硬码账号清单 ≥6 处(加第 4 个号漏改任何一处 = 静默不入库零报错);S3 CARRY_EXACT 只查行数。 其他:employee_binding_v1 列漂移;pii_columns_v1 7 条指向不存在的列;log_pii_access() / resolve_account_filter() 零调用;docs/schema-doc.md 已 stale(5-14 生成)。
隐私规范要点(V0.1,Accepted 2026-05-12)
核心:raw wxid 永不出现在业务视图;每个人用假名 global_party_id;显示名 remark > nick_name > 兜底"未命名客户(首次添加 YYYY-MM-DD)"。 Restricted PII:party_v1.raw_wxid_hidden/remark_latest/nick_name_latest、sender_account_v1.remark_in_account/nick_name_in_account、message_v1.message_content/voice_text。 角色矩阵:Business user 只读业务视图;Dashboard app 只读子集;ETL 读写内部表、源库只读;Developer 只读合成数据;DBA 只读生产。 七个业务视图不得含 raw_wxid_hidden、encrypt_username、sender_id_by_shard、chat_room_username。 外送模型:spec 里没有任何一条授权数据出给外部 LLM;§4.3 业务仪表盘只消费聚合指标不消费原文;今天事实上零外送。→ Ling 9-14 已裁"聊天原文可送 DeepSeek 或其他 agent",规范要同步补一条。 审计:normalize_run_v1、audit_queue_v1、变更日志只存哈希、显示契约巡检;R6 补 pii_access_log_v1 + pii_columns_v1 但未接线。合规闸(§8)发版前全自动测试。
给工作台设计的五条结论
- 写侧必须新建:notes / tasks / assignment / suggestion 采纳全部只有表没有接口。
- 发送侧完全不存在,且受 counterparty 双锁红线约束,属需要 Ling 裁决的新 scope。
- 先做视图考古:
lx_view_b1/b3/s2无 DDL、两个官方视图悬空——数据底座一半不可复现。 - 鉴权是纸面的:生产 API 未开
--require-auth;pii_access_log_v1有表无调用。 - 两套 SLA 数字要统一:
sla_v1(秒)vslx_view_sales_response_v1(分钟)。