跳转至

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 个工作量

Manager Dashboard — Sarah's week
─────────────────────────────────
Closed tasks:  3

实际 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