Audit Timeline Unification — Task Audit 分裂修复¶
Date: 2026-05-08 · Status: Design final · Audience: PM / 设计 / 工程师
Detail spec(工程师实施时看): callytics-infrastructure/.claude/specs/2026-05-08-audit-timeline-unification-design.md
1. TL;DR — 修一个 audit 分裂 bug¶
Sarah 是前台,跟客户 Maria 跟了一周(2 通电话 + 1 条 note + 帮 reopen 一次过期 task)。周三早上 9 点 AI cron 扫到 Maria "7 天没回应",自动关掉 Sarah 之前跟过的 7 个 task。
Manager dashboard 查 "Sarah 这周关了几个 task" — SQL 只读 task_events.actor_user_id='sarah',看到 3 个。实际 Sarah touched 12 个(3 自关 + 7 AI 兜底 + 2 reopen)。9 个工作量永远拿不到 credit。
根因:retaintive 6 个 entity 的 audit 已经统一写一张 contact_timeline 表(call / message / lead / contact 都已对齐),只有 task entity 的 staff actions 写到一张孤立的 task_events 表 — 这是 2026-04 时另一个工程师独立加的,当时不知道 contact_timeline 已经存在。本次把 task_events 删掉,task audit 回到 contact_timeline,顺便补 audit transparency 字段(actor + AI metadata)。
工作量: 2 PR / ~430 LOC / ~1 周(test 环境直接 cut over)。
📌 本次范围明确收紧:之前曾考虑顺手补 schema gap(consent / funnel / UTM / owner / retention 等 61 字段),经审计与 retaintive 现有 product-design 冲突,本次全部不做。详见 §6 Out of Scope。
2. 涉及哪些表¶
3 张 Neon 表,改前 → 改后:
| 表 | 现在 | 改后 | 改动 |
|---|---|---|---|
contact_timeline |
5 entity 共用 audit(缺 task staff) | 6 entity 全部 audit + AI 透明度字段 | +7 字段 |
task_events |
孤立的 task staff audit 表 | DROP(test 直接砍,prod 90 天 archive) | 删表 |
tasks |
close/reopen/note 写 task_events | 改写到 contact_timeline | 0 字段改 |
总改动:1 张表 +7 字段,3 个新 enum 值(task.note_updated / task.due_at_changed / contact.consent_changed),1 张表 DROP。
3. Sarah 的 12 个 task — 改前 vs 改后¶
改前 — Manager 看不到 9 个工作量¶
实际 Sarah touched 12 个(3 自关 + 7 AI 兜底 + 2 reopen)。taskevents 表只在 Sarah click close 时写入,AI 兜底关掉的 7 行写到 contacttimeline,dashboard SQL 不 query 那张表。
改后 — 完整工作量¶
Manager Dashboard — Sarah's week
─────────────────────────────────
Touched tasks: 12
├─ Self-closed: 3
├─ AI auto-closed: 7 (Sarah had touched these earlier)
└─ Reopened: 2
Task 详情页 Activity tab — 完整时间线¶
改前:Activity tab 缺片段(call event 在 calls 表,taskevents 只显示 Sarah click,AI 关闭在 contacttimeline 但 task 详情页不显示)。
改后:全部 event 在同一表,按时间排序:
✨ Mon 09:15 AI 创建 task
理由: 客户说 "我考虑一下"
📞 Mon 14:20 Sarah 打了一个外呼电话(5m 23s) ← 之前看不到(call 在 calls 表)
📝 Tue 17:30 Sarah 加了 note "已留言" ← 之前在 task_events
⚙️ Wed 09:00 AI 兜底关闭 ← 之前在 contact_timeline 但 task 详情页不显示
理由: 客户 7 天没回应
AI 看了过去 7 天的 3 通电话 + 2 条短信
Prompt 版本: flow3-2026-04-15
AI confidence: 87%
💡 Sarah 2 小时前还在跟这客户 ← 新加 staff context
4. 加哪些字段 — contact_timeline +7¶
-- WHO did it (polymorphic — 是 staff 还是 AI 服务还是第三方系统)
actor_subject_id text NULL -- staff UUID / 'contacts_analyzer' / 'mindbody'
-- WHERE FROM (channel split)
actor_source_type text NOT NULL DEFAULT 'unknown' -- 'human_ui' | 'human_api' | 'service' | 'integration' | 'import' | 'unknown'
actor_source_system text NULL -- 'studio_web' | 'contacts_analyzer' | 'mindbody'
-- AI forensic typed columns(让 "AI 为什么这么决定" 答得出来)
ai_run_id text NULL
ai_prompt_version text NULL
ai_model_used text NULL
ai_confidence numeric NULL
modified_fields text[] NULL -- AI 改了哪些字段
加 3 个 enum 值:task.note_updated / task.due_at_changed / contact.consent_changed
为什么这么设计:actor_subject_id(谁)+ actor_source_type(从哪个 channel)是双维度。Future 接 Mindbody / Zapier integration 时不需要再改 schema。AI 透明度 5 字段是为了 "AI 为什么这么决定" 答得出来 — manager / staff / 客户都可能问。
5. 业界对比 — 抄什么¶
| 维度 | 业界做法 | retaintive 选择 | 理由 |
|---|---|---|---|
| AI auto-close task | Keepme/Zenoti: AI propose, human approve | ✅ 保留 AI autonomy | retaintive 业务模型不同 — SA 一天 50 通电话不可能审 AI 建议 |
| AI 决策 transparency | HubSpot Spring 2026 audit card | ✅ 抄(promptVersion / modelUsed / confidence) | Trust = transparency |
| Actor + source 双维度 | Salesforce / WorkOS standard | ✅ 抄 | Future-proof 接 integration |
| Cross-entity timeline | Keepme 3-pane / Zenoti 客户 360 | ✅ 已有(contact_timeline) | - |
6. Out of Scope — 经审计与现有 product-design 冲突的 schema gap¶
之前曾考虑趁本次 migration 顺手补 6 类业界标准 schema gap(共 61 字段)。经对照 retaintive product-design folder(contacts-spec / tasks-field-design / lead-funnel-status / dashboard-spec / rules-spec / product-positioning),全部不做。Why-not 列在这里给 future reader,防止再起同类想法:
| 砍掉的 bucket | 我之前提议加的 | 为什么不做 |
|---|---|---|
| Consent 8+13 字段 | sms/email/callconsentstatus × 3 + optedoutat × 2 + consentsource / consentcapturedat / lastoptoutkeyword + 新表 consent_events 13 字段 | contacts.doNotContact boolean + doNotContactUpdatedBy 已覆盖 DNC 语义(contacts-spec.md L48, L67)。retaintive 不发 outbound SMS marketing(Mindbody/Glofox 才做),无 STOP keyword 业务消费方。13 字段 consent_events 是 Twilio enterprise schema,无监管要求 |
| Funnel timestamps 7 字段 | leadcreatedat / firstresponseat / bookedtourat / showedat / trialedat / convertedat / lostat | contacts.createdAt 已是。first_response_at 可 derive from calls(contacts-spec.md L95)。4 个关键 timestamp(booked/showed/trialed/converted)依赖 Mindbody POS / 门店签到 / 课程管理,V1 不接(lead-funnel-status.md L23 + contacts-spec.md L78 + lead-temperature-v1.md L27-29 三处显式声明)— 加了等于永远 NULL。dashboard-spec §2.5 用 tasks.close_result + closed_at 算转化率,不用 contact 级 timestamp |
| Owner / SLA 3 字段 | owneruserid / ownerassignedat / sladueat | retaintive 是 task-driven 模型,不是 owner-driven CRM。tasks.due_at 已是 SLA(tasks-field-design.md L38)。tasks.closedByStaffName/Id(L41-42)已是 staff attribution。"Lead owner" 是 HubSpot/Salesforce 概念,product-positioning.md 明确 "Add to, not Replace" Mindbody — 不抢 CRM 业务 |
| UTM Attribution 9 字段 | utmsource/medium/campaign/term/content + gclid/fbclid + landingpageurl/referrerurl | retaintive 不做 ads attribution(product-positioning.md)/ 不 host landing page(通过 webhook 接 lead,不知道客户从哪来)。drill-down-analysis.md 提"渠道维度"是 industry framework,不是 V1 schema spec |
| Recording consent 3 字段 | recordingconsentstatus / recordingdisclosureplayed / recordingretentionexpires_at | rules-spec.md §6 唯一合规要求是 TCPA quiet hours(9PM-8AM)+ doNotContact boolean,0 处提 recording consent。retaintive 是平台 CI 服务,recording consent 通常是 platform-level 决策不是 per-call field |
| Retention + Redaction + Spam 共 18 字段 | retentionexpiresat × 3 + piiredactedat × 3 + redactionstatus × 3 + spamscore × 3 + quarantinestatus × 3 + quarantinereason × 3 | product-design 0 处 retention policy 章节。spamscore 是 carrier-level(Twilio/SendGrid),retaintive 通过 RingCentral 接 calls,carrier spam scoring 不暴露给 application layer。lead-tracker-spec.md §一已有 Malformed/Duplicated 分类,无 spamscore 业务消费方 |
核心 lesson:retaintive 是 CI+BI+AI 智能层(product-positioning.md "Add to, not Replace" Mindbody),不是 CRM。schema 抄 HubSpot/Salesforce/Twilio 标准 = frame error。任何 schema 改动必须先对照 product-design folder 验证是否真有业务消费方,而不是从业界 schema 倒推。
如果 future 真要做这些 feature(e.g. Mindbody integration 接通后做 funnel timestamp / 启动 outbound SMS marketing 后做 STOP keyword),schema 那时再加成本极低 — only-add 字段不需要 data migration。
7. 工作量 + PR Sequence¶
2 PR 拆分(~430 LOC / ~1 周)¶
| PR | Repo | LOC | 内容 |
|---|---|---|---|
| PR 1 | callytics-common | ~150 | contact_timeline schema:actor + AI typed cols + 3 enum 值 + helper buildContactTimelineInsertSQL() |
| PR 2 | studio-website-monorepo | ~280 | task close/reopen/note 改写到 contacttimeline + 新加 postpone endpoint + events.ts reader 切到 contacttimeline + 删 log-event.ts |
Test 环境直接 cut over,无 dual-write 观察期(Peter ack 2026-05-08)。
工程师实施时改哪些文件¶
| Repo | Files |
|---|---|
| callytics-common | src/db/schema/contact-timeline.ts(actor + AI typed cols + 3 enum)· helper buildContactTimelineInsertSQL() |
| studio-website-monorepo | apps/api/src/routes/tasks/close.ts / reopen.ts / note.ts 改写 · 新加 postpone.ts · events.ts reader 切到 contact_timeline + response shape mapping · 删 apps/api/src/routes/tasks/log-event.ts |
| callytics-infrastructure(可选,跟 PR 2 协同) | lambda/contacts-analyzer/src/infrastructure/neon-repository.ts AI metadata 字段填入(若不在 PR 2 范围,延后单独 PR) |
Backfill¶
历史 task_events 表 24 行(test 数据全是 noise:"3" / "123" / "去问题"),可弃。Production 数据 90 天 archive 后删表(单独决策)。
8. 风险 + 下一步¶
风险 + 应对¶
| 风险 | 应对 |
|---|---|
| 前端响应格式变了 → 前端炸 | API server-side mapping,响应 JSON shape 保持不变 |
| AI 之前的 task event 跟新的 staff event 重复 | Idempotency key 设计避免重复 |
| Peter 反对删 task_events | ✅ 已 sync ack(2026-05-08) |
| Test wipe 丢 24 行 task_events | Test 数据全是 noise,可弃 |
| Production rollout 单独决策 | 本次只 test cut over,prod archive 90 天 + migrate 是单独 epic |
下一步¶
- ⏳ Peter sync ack 后启动 PR 1(callytics-common,1 天)
- 🟢 PR 2 跟 PR 1 平行(~3-4 天)
- ⏳ Test 环境 cut over + 验证(2 天)
- ⏳ Production rollout 单独决策
Out of Scope — Follow-up Issues¶
| Issue | 内容 | 阻塞 |
|---|---|---|
| #816 | Task reassign(schema + endpoint + UI) | role-based authz 系统 |
| #817 | Passive view tracking | 多账号 auth + 真 user requirement |
| #818 | Audit retention policy + partitioning | 法务/客户合规对齐 |
9. References¶
Internal:
- Detail spec: callytics-infrastructure/.claude/specs/2026-05-08-audit-timeline-unification-design.md — full schema + decision log + verify SQL
- Product design folder(本次 schema gap 砍掉的依据): docs/product-design/
Product design references(why-not 章节引用):
- docs/product-design/contacts-feature/contacts-spec.md L48, L67-70, L78, L95
- docs/product-design/tasks-feature/tasks-field-design.md L38, L41-42
- docs/product-design/lead-tracker-feature/lead-funnel-status.md L23
- docs/product-design/lead-tracker-feature/lead-temperature-v1.md L27-29
- docs/product-design/dashboard-feature/dashboard-spec.md §2.5
- docs/product-design/rules-feature/rules-spec.md §6
- docs/product-design/product-architecture/product-positioning.md "Add to, not Replace"
GitHub: - Parent: callytics-infrastructure#778 - Follow-ups: #816 reassign · #817 view · #818 retention
Industry: - HubSpot Spring 2026 audit cards - Multi-Tenant Audit Logging Mistakes