> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Prompt 中 Task 部分的待解决问题

> **Historical research / Superseded（2026-07-15）**：本文的问题发现（尤其 typeCategory ↔ closeResult drift、attempt 与 terminal Outcome 混淆）仍有参考价值，但字段、mapping 和方案不再具有规范效力。现行设计见 [Task System Design](/product-design/v3/tasks-feature/task-domain-lifecycle.md)。
>
> **来源**：逐句分析现有和更新的Prompt中task部分的逻辑, 并对照最新schema字段得出。

***

## 问题 1：`unreachable` / `neglected`定义中的次数阈值脱离时间窗，逻辑不通

contact-analyzer 把 12 个 `leadStatus` 按客户轨迹分成 3 个 Phase：

- **Phase 1（往前推进，7 个）**：`new` / `attempted` / `connected` / `booked` / `showed` / `trialed` / `converted`
- **Phase 2（交谈过但卡住，3 个）**：`bad_timing` / `not_interested` / `lost_contact`
- **Phase 3（从未接通，2 个）**：`unreachable` / `neglected`

Phase 3 用纯次数判定——`neglected`（\<3 次）/ `unreachable`（≥3 次），**没有任何时间约束**。次数脱离时间没有约束力，举两例：

- 3 通电话打在 **10 分钟内** vs 摊在 **一周里**，都判 `unreachable`
- 1 次尝试，对**刚进来 1 小时**的 lead 和**进来 3 周**的 lead，都判 `neglected`

**两版 prompt 都有这个问题**——pipeline-2（部署版 `prompt-builder.ts`）和 oliver-proposals 2026-05 提案版这段一字不差。**不是 Oliver 引入的，部署版原来就这样**。

### 建议

`unreachable` / `neglected`的次数阈值必须带时间锚才有意义。最自然的做法是**把 `temperature` 重新启用**——"客户随时间衰减的冷热度"本来就是它的语义,让它真正承担"距上次尝试 > N 天"这种约束,Phase 3 / `neglected` 用它来界定时间。

***

## 问题 2：task status 命名漂移——`open` → `pending`

### 现状

contact-analyzer prompt 原文（[L552](oliver-proposals/2026-05-four-prompt-revision/04-contact-analyzer-system-prompt.txt)）：

> Tasks have **binary `status`** (**`pending`** or **`closed`**) at the database level.

数据库 schema、所有 prompt（oliver 2026-05 提案 / pipeline-2 部署版 / prompt-eval v1b-20260504 历史版本）、SQL 查询里**全部用 `pending` / `closed`**。

但产品一直叫 "open"**——这个**漂移没有任何记录，也没经过产品确认\*\*。

### 这不是 Oliver 2026-05 引入的

Schema 早期 V1 就把 status 限定为 `pending` / `closed`（见 archive 的 `task-system-v1.md` 和 `task-system-schema-investigation.md`）。当前 `tasks.ts` 用 Drizzle 的 `{ enum: TASK_STATUS }` 约束（TS 类型层——status 字段本身**没有**显式 DB `CHECK`，只有 `typeCategory` / `closeResult` 才加了 `check()`）。后续所有 prompt / 代码都被迫跟随。**Oliver 没动这个字段，只是按现有 schema 写的**。

### 状态

待团队对齐:**到底是 `pending` 还是 `open`?**

***

## 问题3：typeCategory ↔ closeResult 现状梳理

隐含 3 个问题(可能后续抽出独立 issue)

- **closeResult 是统一池,没按 typeCategory 隔离** —— prompt 让 AI 自己选,schema 不强制 "lead\_follow\_up 不能用 `cancel_saved`" 这种业务约束。AI 配错,代码层没硬挡。
- **没有 typeCategory ↔ closeResult 的合法映射表** —— 现在只能从 prompt 规则反推,没有 schema 层的 mapping enforcement。
- **`booked` / `cancelled` 已于 2026-05-23 加入 DB enum**（callytics-infrastructure #980），这是 Task V2 enum 演进历史；当前 V3 selected Target 的语义与迁移见 [Task V3 Product / Domain Contract §16.2](/product-design/v3/tasks-feature/task-domain-lifecycle.md#162-legacy-closeresult-怎样迁移)。

### typeCategory 有 9 种

| 谁建                                | typeCategory           | 干啥                       |
| --------------------------------- | ---------------------- | ------------------------ |
| **lead-tracking 系统**(非 AI,SLA 驱动) | `lead_outreach`        | Lead 进系统时首次 5 min SLA 联系 |
| **contact-analyzer(AI)**          | `lead_follow_up`       | 推新 lead 预约/转化            |
|                                   | `booked_not_converted` | 推已预约 lead 完成转化           |
|                                   | `cancellation_risk`    | 挽留要取消的会员                 |
|                                   | `retention`            | 解决投诉/服务质量问题              |
|                                   | `upgrade`              | 推会员升级                    |
|                                   | `renewal`              | 恢复冻结/付款                  |
|                                   | `referral`             | 推荐活动/特别促销                |
|                                   | `win_back`             | 挽回前会员                    |

### closeResult 目标态:20 个(12 原有 + ★ 8 新增)

先按"现有 / 建议新增"分，性质（🟢正向 / 🔴负向 / ⚪中性）降为表内一列。

#### 现有 12 个（backend `TASK_CLOSE_RESULT` 已支持）

| closeResult         | 性质    | 含义                                                                                                 |
| ------------------- | ----- | -------------------------------------------------------------------------------------------------- |
| `attempted`         | 🟢 正向 | `lead_outreach` 的正向结果, 已发起联系                                                                       |
| `converted`         | 🟢 正向 | `lead_follow_up` / `booked_not_converted` 的正向结果, lead 转化成会员                                        |
| `cancel_saved`      | 🟢 正向 | `cancellation_risk` 的正向结果, 挽留成功                                                                    |
| `issue_resolved`    | 🟢 正向 | `retention`的正向结果, 投诉/问题解决                                                                          |
| `upgraded`          | 🟢 正向 | `upgrade`的正向结果, 升级成功                                                                               |
| `renewed`           | 🟢 正向 | `renewal`的正向结果, 续费/付款恢复                                                                            |
| `referral_obtained` | 🟢 正向 | `referral`的正向结果, 推荐达成                                                                              |
| `win_back`          | 🟢 正向 | `win_back`的正向结果, 前会员回归                                                                             |
| `already_member`    | 🔴 负向 | `lead_outreach` / `lead_follow_up` / `booked_not_converted` / `win_back` 通用负向——发现对方已是会员，task 目标不成立 |
| `do_not_contact`    | 🔴 负向 | DNC / STOP（所有 typeCategory 通用负向）                                                                   |
| `wrong_number`      | 🔴 负向 | 号码不通 / 联系不上（所有 typeCategory 通用负向）                                                                  |
| `other`             | ⚪ 中性  | 无明确类别（通用中性；closeResult=other 时 closeNote 必填）                                                       |

#### 建议新增 8 个（★ backend enum 还未支持）

| closeResult               | 性质    | 含义                                                                     | 来源                                                 |
| ------------------------- | ----- | ---------------------------------------------------------------------- | -------------------------------------------------- |
| **`unable_to_reach`** ★   | 🔴 负向 | 达到最大尝试次数仍联系不上，`lead_outreach` 的负向结果                                    | \[Final schema §2.3 line 161]（依赖问题 1 修复时间约束后才可靠触发） |
| **`booked`** ★            | 🟢 正向 | lead 订了 intro 课，是 `lead_follow_up` 的正向结果                               | Oliver 已提                                          |
| **`lead_declined`** ★     | 🔴 负向 | 客户明确拒绝预约/转化(leadStatus 变 `paused` / `terminal`)，`lead_follow_up` 的负向结果 | 全新提案（英文名待定）                                        |
| **`cancelled`** ★         | 🔴 负向 | 取消已发生，`cancellation_risk` 的负向结果                                        | Oliver 已提                                          |
| **`upgrade_declined`** ★  | 🔴 负向 | 客户明确拒绝升级，`upgrade` 的负向结果                                               | 全新提案（英文名待定，备选 `not_upgraded`）                      |
| **`referral_declined`** ★ | 🔴 负向 | 客户拒绝推荐，`referral` 的负向结果                                                | 全新提案（英文名待定，备选 `no_referral`）                       |
| **`renewal_declined`** ★  | 🔴 负向 | 客户明确表示不再续费 / 不恢复付款，`renewal` 的负向结果                                     | 全新提案（英文名待定，备选 `not_renewed`）                       |
| **`win_back_declined`** ★ | 🔴 负向 | 前会员明确表示不再回归，`win_back` 的负向结果                                           | 全新提案（英文名待定，备选 `not_won_back`）                      |

### 基于以上, 建议建立typeCategory ↔ closeResult 映射

下表只列 typeCategory-specific 的正负向。`do_not_contact` / `wrong_number`(通用负向)、`other`(通用中性)对所有 typeCategory 都适用，不重复列。

| typeCategory           | 🟢 正向               | 🔴 负向                                        |
| ---------------------- | ------------------- | -------------------------------------------- |
| `lead_outreach`        | `attempted`(完成首次外联) | **`unable_to_reach`** ★ / `already_member`   |
| `lead_follow_up`       | **`booked`** ★      | **`lead_declined`** ★ / `already_member`     |
| `booked_not_converted` | `converted`         | `already_member`                             |
| `cancellation_risk`    | `cancel_saved`      | **`cancelled`** ★                            |
| `retention`            | `issue_resolved`    | —                                            |
| `upgrade`              | `upgraded`          | **`upgrade_declined`** ★                     |
| `renewal`              | `renewed`           | **`renewal_declined`** ★                     |
| `referral`             | `referral_obtained` | **`referral_declined`** ★                    |
| `win_back`             | `win_back`          | **`win_back_declined`** ★ / `already_member` |

★ = 新增，backend `TASK_CLOSE_RESULT` enum 还未支持。

> 注 1：`lead_declined` 的底层触发 = contact 的 `lifecycleState` 变成 `paused` / `terminal`（当前 `lead_follow_up` 随之关闭）；其中 `paused` 牵扯问题 7。
>
> 注 2：`unable_to_reach` 触发条件依赖 `unreachable` leadStatus（≥N 次真实尝试后仍未接通）。但 `unreachable` 目前只看次数不看时间窗（见问题 1），导致 `unable_to_reach` 可能被过早触发——问题 1 不修，这个负向结果就不可靠。

### 设计说明

**`attempted` 的语义 —— 专门给 `lead_outreach` 用**

`attempted` 表示员工完成了首次 SLA 外联任务（打了那通电话）—— 这是 `lead_outreach` 的"正向 = 任务完成"。

**不给其他 typeCategory 用**。Oliver 在两处对 `attempted` 的用法对这个词的语义理解有偏差：

- **\[SECTION 7（line 529-530）]**：禁止把 cancellation / complaint / manager-callback / upgrade / referral / win-back task 在"一次未接通后"关成 `attempted`——这里 `attempted` 不适用于cancellation / complaint / manager-callback / upgrade / referral / win-back task, 这段描述没必要.
- **\[SECTION 4 Billing/payment recovery 模板（line 359-360）]**：推荐在"付款链接已发出但付款未确认"时 close as `attempted`——这里 `attempted` 被当作"完成了一次有意义的行动（发出链接）"

这进一步支持映射表的设计：`attempted` 专属 `lead_outreach`（首次外联完成的正向结果），其他 typeCategory 各有专属的负向 closeResult，不再用 `attempted` 兜底。

> 如果 `attempted` 这词容易引起歧义（"尝试"听起来像负向），可以考虑改名为 **`lead_outreached`** —— 直接表达"首次外联已完成"。详见问题 4。

**为啥 1 个 typeCategory 没有对应的负向结果？**

设计原则:**如果一个任务无法界定负向结果, 但是在没有收到明确否定信号的情况下就需要继续跟下去的话, 那么这个任务需要员工手动用other来关闭, 并填入具体关闭原因, 或者有通用的负向结果出现, 也可以关闭.**。

具体到 1 个没有 typeCategory-specific 负向的:

| typeCategory | 为啥不需要明确负向                       |
| ------------ | ------------------------------- |
| `retention`  | 投诉 / 问题不解决就一直 open，除非客户"明确放弃投诉" |

> 注：`booked_not_converted` 虽然没有"客户拒绝"的明确负向，但有 `already_member` 兜底（发现对方已是会员，task 目标不成立）。

### 8 个新增 closeResult 待 backend 同步

| closeResult         | 适用 typeCategory     | 状态                                             |
| ------------------- | ------------------- | ---------------------------------------------- |
| `unable_to_reach`   | `lead_outreach`     | 全新提案；依赖问题 1 修复时间约束后才可靠触发                       |
| `booked`            | `lead_follow_up`    | Oliver 2026-05 已写入 prompt 规则，等 backend enum 加上 |
| `lead_declined`     | `lead_follow_up`    | 全新提案，英文命名待团队拍板                                 |
| `cancelled`         | `cancellation_risk` | 全新提案，英文命名待团队拍板                                 |
| `upgrade_declined`  | `upgrade`           | 全新提案，英文命名待团队拍板（备选 `not_upgraded`）              |
| `referral_declined` | `referral`          | 全新提案，英文命名待团队拍板（备选 `no_referral`）               |
| `renewal_declined`  | `renewal`           | 全新提案，英文命名待团队拍板（备选 `not_renewed`）               |
| `win_back_declined` | `win_back`          | 全新提案，英文命名待团队拍板（备选 `not_won_back`）              |

加完后 closeResult enum 从 **11 → 20**。

***

## 问题 4：`attempted` 不应删除，建议改名

`attempted` 专属于 `lead_outreach` 的**正向**关闭结果，语义是"首次外联已完成"（staff 完成了 SLA 任务，打出了那通电话）。

Final schema 提议删掉 `attempted`，是因为误解了其语义（以为它是进展事件而非 task 完成结果）。实际上 `lead_outreach` task 的正向结束就是"首次外联已打出"，没有这个 closeResult，`lead_outreach` task 就没有正向出口。

`attempted`（正向）和 `unable_to_reach`（负向）是并列关系，不是替换关系：

| closeResult                         | 性质    | 触发条件             |
| ----------------------------------- | ----- | ---------------- |
| `attempted`（建议改名 `lead_outreached`） | 🟢 正向 | 首次外联已完成，不论接没接通   |
| `unable_to_reach` ★                 | 🔴 负向 | 达到最大尝试次数，从未接通，放弃 |

**建议**：保留语义，改名为 `lead_outreached`，明确表达"首次外联已完成"，避免被误解成"只是尝试过但没有结论"。

***

## 问题 5：Oliver 引入 in-place UPDATE 现状梳理

### 现状：目前是 close + create 循环，不是真 UPDATE

现行 spec（[task-lifecycle.md](/product-design/v1/tasks-feature/task-lifecycle.md)）第 6.1 节写得很清楚：

> Contact Analysis 每次运行时，**先 auto-close 该客户的旧 task，再根据最新分析决定是否创建新 task**

也就是说：**AI 不修改老 task，而是关掉老的、建新的**。

### Oliver 的 UPDATE 改的是 task 的哪些字段

从 Oliver prompt 的 OUTPUT JSON SCHEMA 看:

```jsonc
{
  "action": "update",
  "taskId": "uuid",           // 引用已有 task
  "typeCategory": "...",      // 必填但不能改,只是回填确认
  "priority": "high | medium | low (可选)",
  "suggestedActions": "[...] (可选)",
  "reason": "≤500 字"
}
```

**只能改 2 个字段（至少给 1 个）：**

| 字段                 | 改了的连锁影响                                                        |
| ------------------ | -------------------------------------------------------------- |
| `priority`         | 间接影响 `dueAt`——系统按 priority 推算时间窗（high=4h, medium=24h, low=72h） |
| `suggestedActions` | 给员工的具体话术指引换了                                                   |

**不能改：** `typeCategory`（那是 task 身份）、`taskId`、`createdAt`。

### 但 Oliver 这个 UPDATE 的字段集**不全**——补全后应该是这样

#### 直接 mutate 现有列（5 个）

| 字段                   | 谁改                   | 怎么改                                                                  |
| -------------------- | -------------------- | -------------------------------------------------------------------- |
| `updatedAt`          | ORM 自动               | Drizzle `$onUpdate` 自动刷新                                             |
| `priority`           | Oliver schema 允许     | high / medium / low                                                  |
| `suggestedActions`   | Oliver schema 允许     | 完整替换 jsonb                                                           |
| `dueAt`              | derive 自动算           | priority 改了就重算（high=4h / medium=24h / low=72h）。**前提：员工没手动 Adjust 过** |
| `actionNeededReason` | **Oliver 漏了，语义上必须改** | suggestedActions 变了，"为啥要跟进"的理由也得跟着变，不然两者对不上                          |

#### task\_events 事件（用现有 audit log 表，不加新列）

AI update / staff 编辑 task 应该沿用这套：

| event\_type             | 啥时候发                                   | payload（进 metadata jsonb）                                                                                                                                      |
| ----------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `task.updated_by_ai`    | contact-analyzer 跑出 `action: update` 时 | `{ fromPriority, toPriority, fromActions, toActions, fromActionNeededReason, toActionNeededReason, reason, runId }` + `actor_staff_name='ai-contact-analyzer'` |
| `task.updated_by_staff` | 员工编辑 priority / suggestedActions 时     | `{ fromPriority, toPriority, fromActions, toActions }` + `actor_user_id` / `actor_staff_name`                                                                  |
| `task.due_at_changed`   | 员工 Adjust dueAt 时                      | **目前在 `contact_timeline`**——和 task\_events 的归属不一致，是 alignment 缺口（见下）                                                                                           |

**为啥不加 `lastAiUpdateAt` / `updatedBy` / `lastAiUpdateRunId` 列?**

- update 可发生 N 次 → 加专用列每次 overwrite，丢历史；`task_events` 天然保留
- close 加专用列因为是终态（每 task 1 次，写完不变）；update 是可重复事件，不该比照 close
- "上次 AI 更新时间" / "谁更新" / "哪次 run" 全部从 task\_events 查：

```sql
SELECT * FROM task_events 
WHERE task_id = ? AND event_type LIKE 'task.updated_by_%'
ORDER BY created_at DESC
```

前端用 computed API field 算"**未读 AI 更新数**"（自上次员工查看以来的事件数），渲染卡片角标。

**📝 alignment 缺口：`task.due_at_changed` 当前在 `contact_timeline`，不在 `task_events`**

两张表设计上的语义分工（已对照 schema `task-events.ts` / `contact-timeline.ts`）：

| 表                  | 设计意图的范围                                                                                                          |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `task_events`      | 单个 task 的 audit log（`task.created` / `task.reopened` / `task.note_edited` 等；event\_type 无 DB 约束，由 studio-api 定义） |
| `contact_timeline` | 跨实体的客户活动流（call / sms / lead / task 等都进；event\_type 是 enum 约束）                                                    |

⚠️ **但实际 schema 里两表的 task 事件有重叠，分工并不干净**：`task.created` 两张表都有，`contact_timeline` 还另有 `task.status_changed` / `task.updated`。所以下面"该搬"的方向成立，但别把它当成"两表本来泾渭分明、只有 `due_at_changed` 跑偏"——task 事件本来就两边都落。

`task.due_at_changed` 只在 `contact_timeline` 的 enum 里（`task_events` 没有这个值），进 `contact_timeline` 是历史决策。严格按"单 task 审计"语义它更该在 `task_events`，但要不要补 alignment PR 搬过来，是另一个待决策项。

#### ⚠️ 冲突边界：不该 AI 直接覆盖的

| 字段                 | 冲突情况                                                                                              |
| ------------------ | ------------------------------------------------------------------------------------------------- |
| `dueAt`            | 员工**手动 Adjust 过**（如"客户说下周二再打"）→ AI 重算 priority 时**不该覆盖**员工安排。需要 `dueAt_manually_adjusted=true` 标记 |
| `suggestedActions` | 员工在卡片上**编辑过 / 增删过** → AI 不该直接覆盖。可能需要 `actions_locked_by_staff` 标记                                 |

[task-lifecycle.md §五](/product-design/v1/tasks-feature/task-lifecycle.md) 已经在讲员工手动 Adjust 时留过"AI 重建覆盖员工调整"的 caveat，UPDATE 机制也得处理同一类问题。

#### ❌ 不该在 UPDATE 里改的（11 个）

| 字段                                                                                                                      | 为啥不该改                                                                        |
| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `actionNeeded`（bool）                                                                                                    | 改 true → false 等于关 task，应该走 CLOSE action                                     |
| `status`                                                                                                                | 同上，改状态走 CLOSE                                                                |
| `typeCategory` / `taskType` / `sourceType`                                                                              | task 身份，改了就不是同一件事                                                            |
| 所有 close 字段（`closedAt` / `closedByStaffName` / `closedByStaffId` / `closeType` / `closeResult` / `closeNote`）           | task 还 open，这些字段就该是空                                                         |
| 系统标识（`taskId` / `contactPhone` / `franchiseId` / `accountId` / `storePhone` / `storeId` / `sourceLeadId` / `createdAt`） | 强不变（注：schema 里是 `accountId`，不是 `siteId`；store 隔离键是 `storePhone` / `storeId`） |

### 总结

| 类别              | 数量 | 列表                                                                                            |
| --------------- | -- | --------------------------------------------------------------------------------------------- |
| **直接改**         | 5  | `updatedAt`（自动）、`priority`、`suggestedActions`、`dueAt`（derive）、`actionNeededReason`（Oliver 漏了） |
| **timeline 事件** | 3  | `task.updated_by_ai` / `task.updated_by_staff` / `task.due_at_changed`（现有，不变）                 |
| **冲突防护标志**      | 2  | `dueAt_manually_adjusted`、`actions_locked_by_staff`                                           |
| **绝对不该改**       | 11 | 身份字段 + 状态字段 + close 字段                                                                        |
| **不加新列**        | 0  | ~~`updatedBy` / `lastAiUpdateAt` / `lastAiUpdateRunId`~~（沿用 dueAt 模式，全走 timeline）             |

***

## 问题 6：`suggestedActions` 模板清单混入"建不建任务"的判断——字段职责错位

### 拆分后状态（2026-06-03 核查）

Oliver 将 contact 和 task 拆成两个 prompt 后：

- **\[04-contact-profile]** ✅ 已处理干净：`suggestedActions` 只作为 contact 级信号输出，无"建不建 task"判断，无 `no human action` 渠道选项。
- **\[05-task-decision]** ❌ 问题仍然存在：SECTION 4 的 OTF 模板清单原封不动保留了以下问题（见下方现状分析）。

**需要修改的是 05-task-decision SECTION 4，不是 04-contact-profile。**

### 现状（05-task-decision SECTION 4）

\[SECTION 4（line 287-379）]的 OTF 模板清单列了 8 个场景，但其中 2 个放错了位置。

机制分三层：

- **`suggestedActions`**（task\_issues 表 jsonb 列）= 做什么 action
- **`actionNeeded`**（task\_issues 表 boolean 列）= 要不要跟进
- **建 / 关 / 改 task** = `taskDecisions[]` 里的 `action` 字段——不是数据库列

三者职责分开，但 SECTION 4 把"建不建任务"混进了 suggestedActions 模板：

| 模板场景                           | 位置              | 它实际在说            | 本该归谁管                                                                      |
| ------------------------------ | --------------- | ---------------- | -------------------------------------------------------------------------- |
| **Already booked**             | \[line 335-338] | "不要建或保留人工任务"     | `taskDecisions[]` 不输出 `create`（或输出 `close`）；与 `suggestedActions` 无关        |
| **Do not contact / no action** | \[line 375-378] | "不要建跟进建议，关掉相关任务" | 由 `doNotContact` 触发，走 `taskDecisions[]` 输出 `close`；与 `suggestedActions` 无关 |

剩下 6 个场景（新 lead、问价、取消风险、账单、投诉、课后跟进）才是 `suggestedActions` 真正该管的内容。

### `no human action` 被列为必填渠道

\[SECTION 4 line 298]要求每条 `suggestedActions` 元素必须包含 `channel`（可选值为 `call` / `SMS` / `manager callback` / `no human action`），选项包含 `no human action`。但"不需要人工"和"这条建议存在"本身冲突——应该走 `actionNeeded=false` + `suggestedActions=[]`，根本不会有这条建议。

### 内容重复

"Already booked 不建 task"（SECTION 4 line 337）和 SECTION 5 CREATE RULES（line 404）重复写了同一条规则。

### 建议（修改 05-task-decision SECTION 4）

1. 从模板清单删掉 "Already booked" 和 "Do not contact / no action" 两个场景，只留 6 个真正的 `suggestedActions` 场景
2. channel 选项从 `call / SMS / manager callback / no human action` 改为 `call / SMS / manager callback`
3. SECTION 4 删掉的场景由 SECTION 5 的 DO NOT create 规则覆盖，不重复

***

## 问题 7：`paused` 缺少和 `terminal` 对等的 task 停止联动，且与状态定义自相矛盾

### 设计意图

`lead_follow_up`类 task应该一直跟，直到对应 contact 的 `contacts.lifecycleState` 变成 `paused` 或 `terminal` 才停。

### 现状：`terminal` 写全了，`paused` 没写

prompt 对 `terminal` 有三重保险：

| 位置          | 规则                                                                                    |
| ----------- | ------------------------------------------------------------------------------------- |
| prompt L402 | `lifecycleState = terminal` → `actionNeeded` 必须 false                                 |
| prompt L610 | `lead_follow_up` 创建条件明确排除 terminal（`unreachable` / `lost_contact` / `not_interested`） |

但 `paused` 只有状态**定义**里一句泛泛的"paused = stop proactive outreach, wait for reactivation"（prompt L98）。上面三条硬规则（actionNeeded RULE / DO NOT create / lead\_follow\_up 创建条件）**全都只写 terminal，没写 paused**。

### 两个具体后果

1. **自相矛盾**：定义说 paused 要"停止主动外联"，但 actionNeeded 的 RULE 只拦 terminal——prompt **没有任何规则阻止 paused 状态下 `actionNeeded=true`**。定义说停、规则没拦。
2. **已存在 task 无人管**：唯一映射到 paused 的 leadStatus 是 `bad_timing`。`bad_timing` 不在 `lead_follow_up` 创建条件里，所以**不会新建**；但"已经存在的 `lead_follow_up` task，客户变成 `bad_timing`(paused) 之后要关闭task"——prompt **完全没提及**。

### 建议（含一个待决策）

把 `paused` 补进 actionNeeded RULE 和 task 停止逻辑。

***

## 问题 8：一个 contact 可以有多个 task？

### 背景

当前设计（schema + Oliver prompt）允许一个 contact 同时有多个 open task，每个 typeCategory 最多 1 个。例如一个 member 可以同时有 `retention`（投诉未解决）和 `upgrade`（想升级）两个 task。

Oliver SECTION 2 Constraints 原文（\[line 111-113]）：

> Max 1 pending task per typeCategory per contact.
> If the same typeCategory is already pending, update it, close it, or leave it unchanged. Never create a duplicate.

即：同一个 typeCategory 不能重复建，但不同 typeCategory 可以并存——Oliver 认为一个 contact 同时有 `retention` + `upgrade` 两个 pending task 完全合法。

### 产品提案

> 一个 contact 最多有 **1 个 open task**（task = 一次通话的容器）。有多个需求不新建第二个 task，而是在这个 task 里新增一个 issue。每个 issue 对应一个 typeCategory，各自独立管理状态和 closeResult。
>
> **业务理由**：给同一个客户打两个电话是浪费，一通电话应该把所有事情一起说清楚。

Oliver 那条去重规则平移到新结构：**同一个 task 里，同一个 typeCategory 最多 1 个 open issue**。原文：

> Max 1 pending task per typeCategory per contact.
> If the same typeCategory is already pending, update it, close it, or leave it unchanged. Never create a duplicate.

新结构下有两层约束：

- **1 个 contact 最多 1 个 open task**（新增）
- **1 个 task 里同一个 typeCategory 最多 1 个 open issue**（Oliver 原规则平移）

### 数据结构设计（关系型，对照当前 schema）

> 下表基于 2026-05 当时的 `tasks` snapshot，不能当作当前 schema 或批准后的 Target schema。当前 V3 contract 中的 source snapshot 与 Target delta 分别见 [§9.1](/product-design/v3/tasks-feature/task-domain-lifecycle.md#91-current可直接复用的基础) 和 [§9.3](/product-design/v3/tasks-feature/task-domain-lifecycle.md#93-tasks-expand-schema)；引用前仍须核对 live code。

#### `tasks` 表（联系人维度，1 task per contact）

保留联系人身份、整体生命周期、关闭元数据：

| 字段                  | 类型                   | 说明                                                                            |
| ------------------- | -------------------- | ----------------------------------------------------------------------------- |
| `taskId`            | uuid PK              | 不变                                                                            |
| `contactPhone`      | text NOT NULL        | 唯一约束改为 `(contactPhone, status='pending')` — 每个 contact 最多 1 个 pending task    |
| `franchiseId`       | text NOT NULL        | 审计列，不变                                                                        |
| `accountId`         | text NOT NULL        | 审计列，不变                                                                        |
| `storePhone`        | text NULL            | 最近通话的店端电话，不变                                                                  |
| `storeId`           | text NULL            | 不变                                                                            |
| `status`            | text NOT NULL        | `pending` \| `closed`（沿用当前命名）；由所有 issues 推导：所有 issues closed → task 自动 closed |
| `priority`          | text NULL            | 派生自所有 pending issues 中最高的那个                                                   |
| `dueAt`             | timestamptz NULL     | 派生自所有 pending issues 中最早的那个                                                   |
| `closedAt`          | timestamptz NULL     | 整体关闭时间                                                                        |
| `closeType`         | text NULL            | `auto_closed` \| `manual_closed`，整体关闭方式                                       |
| `closedByStaffName` | text NULL            | 手动关整个 task 的员工姓名                                                              |
| `closedByStaffId`   | uuid NULL            | 手动关整个 task 的员工 ID                                                             |
| `note`              | text NULL            | task 级自由备注，不变                                                                 |
| `createdAt`         | timestamptz NOT NULL | 不变                                                                            |
| `updatedAt`         | timestamptz NOT NULL | 不变                                                                            |

**从 tasks 表移出、下沉到 task\_issues 的字段**：`taskType` / `typeCategory` / `actionNeeded` / `actionNeededReason` / `suggestedActions` / `sourceType` / `sourceLeadId` / `contactAnalysisRunId` / `closeResult` / `closeNote` / `closedAt`（issue 级）/ `closedByStaffName`（issue 级）/ `closedByStaffId`（issue 级）/ `closeType`（issue 级）

#### `task_issues` 表（类别维度，N issues per task）

| 字段                     | 类型                   | 说明                                                                 |
| ---------------------- | -------------------- | ------------------------------------------------------------------ |
| `issueId`              | uuid PK              | 主键                                                                 |
| `taskId`               | uuid FK → tasks      | 所属 task                                                            |
| `contactPhone`         | text NOT NULL        | 冗余，避免 JOIN                                                         |
| `taskType`             | text NOT NULL        | `lead_outreach` \| `follow_up`，从 typeCategory 派生，随 typeCategory 下沉 |
| `typeCategory`         | text NOT NULL        | 单值，每条 issue 一个类别（8 值，不含 `lead_outreach`）                           |
| `status`               | text NOT NULL        | `pending` \| `closed`，每个 issue 独立                                  |
| `priority`             | text NULL            | `high` \| `medium` \| `low`，每个 issue 独立                            |
| `suggestedActions`     | jsonb NULL           | `{action, reason, priority, priorityReason}[]`，每个 issue 独立         |
| `actionNeeded`         | boolean NULL         | 每个 issue 独立                                                        |
| `actionNeededReason`   | text NULL            | 每个 issue 独立                                                        |
| `sourceType`           | text NOT NULL        | `lead` \| `contact_analysis` \| `manual`，每个 issue 独立创建入口           |
| `sourceLeadId`         | text NULL            | 仅 sourceType=lead 时填，每个 issue 独立                                   |
| `contactAnalysisRunId` | uuid NULL            | AI 批次 ID，每个 issue 独立                                               |
| `dueAt`                | timestamptz NULL     | 每个 issue 独立截止时间                                                    |
| `closeResult`          | text NULL            | 每个 issue 独立关闭结果（见问题 3 映射表）                                         |
| `closeNote`            | text NULL            | closeResult=other 时必填                                              |
| `closedAt`             | timestamptz NULL     | 每个 issue 独立关闭时间                                                    |
| `closeType`            | text NULL            | `auto_closed` \| `manual_closed`，每个 issue 独立                       |
| `closedByStaffName`    | text NULL            | 每个 issue 独立                                                        |
| `closedByStaffId`      | uuid NULL            | 每个 issue 独立（V1 为 NULL）                                             |
| `createdAt`            | timestamptz NOT NULL | 不变                                                                 |
| `updatedAt`            | timestamptz NOT NULL | 不变                                                                 |

**唯一约束**：`(taskId, typeCategory, status='pending')` — 同一个 task 里同一个 typeCategory 最多 1 个 pending issue。

#### task 关闭逻辑

```
所有 task_issues.status = 'closed'  →  tasks.status 自动变 'closed'
员工手动关整个 task                  →  未 closed 的 issues 批量标 closeResult='other'，closeNote 必填
```

### 影响面（给 teammate 评估）

| 维度            | 影响                                                                                                                                                                                                                              |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Schema 迁移** | 新增 `task_issues` 表；`tasks` 表移除 `typeCategory` / `suggestedActions` / `actionNeeded` / `actionNeededReason` / `closeResult` / `closeNote`（下沉到 issues）；唯一约束从 `(contactPhone, typeCategory)` 改为 `(contactPhone, status='pending')` |
| **存量数据**      | 现有每条 task 需迁移为 1 个 task + 1 个 issue                                                                                                                                                                                             |
| **Prompt 改动** | taskDecisions 的 action 粒度变成 issue 级；Oliver SECTION 3 的每类规则对应 issue 操作，结构可复用                                                                                                                                                     |
| **问题 3 映射表**  | typeCategory ↔ closeResult 映射不变，closeResult 写在 issue 上而非 task 上                                                                                                                                                                 |
| **analytics** | `task_issues` 是标准关系表，`typeCategory` 可直接作为分析维度                                                                                                                                                                                   |

### 状态

待 teammate 评估 schema 迁移成本。

***

## 问题 9：taskDecisions `action` 字段缺 3 个值，prompt 需补全

### 现状

`action` 是 `taskDecisions[]` 每个元素里的一个字段，值决定后端对 tasks 表做什么操作。

Oliver [SECTION 10（line 650-699）](oliver-proposals/2026-05-four-prompt-revision/05-task-decision-system-prompt.txt)目前只定义了 3 种 action shape：`create` / `close` / `update`。

2026-05 Final proposal 当时列出 6 种 Task V2 action；当前 V3 command surface 见 [Task V3 Product / Domain Contract §10.2](/product-design/v3/tasks-feature/task-domain-lifecycle.md#102-commands)。

### 6 种 action

| action            | 含义                      |
| ----------------- | ----------------------- |
| `create_open`     | 新建一个 pending task/issue |
| `create_closed`   | 新建并立即关闭（当场办成，留记录）       |
| `record_progress` | 记录一次进展事件，不关 task        |
| `update`          | 更新任务信息（优先级 / 建议变了）      |
| `close`           | 关闭现有 pending task/issue |
| `no_op`           | 明确表示"看过了，不需要动"          |

### 需要做的事

prompt 补全 3 个缺失的 action（`create_closed` / `record_progress` / `no_op`）之前，需要结合 schema 和后端实现规格，为**全部 6 种 action** 正式定义：

1. 写哪张表（`tasks` / `task_issues` / `task_progress_events` / 不写库）
2. 必须带哪些字段
3. 不能带哪些字段

不能只靠 prompt 推断——字段规格必须先在 schema / backend 层拍板，再同步进 prompt。

其中 `record_progress` vs `update` 的边界需要特别说清楚：两者都针对已有 task，但 `record_progress` 是"发生了一件事（打电话没接、留了语音），task 本身不变"，`update` 是"task 的信息需要调整（优先级变了、建议换了）"——这个区别必须写进 prompt，否则 AI 会混用。

***

## 问题 10：`record_progress` 启用时 progressType 枚举待统一

### 现状

Oliver 的正式输出（\[SECTION 10 line 650-699]没有 `record_progress`。他在 \[SECTION 11 line 703-724]写了一段**未来提案**，标明"Recommended future backend support / Use this only after backend support exists"，里面设想了 `record_progress` 的 JSON shape 及其 `progressType` 枚举（8 值）。

目前 `record_progress` 两边都没有落地，但将来启用时，Oliver 提案的 8 值和 Final schema 的 6 值需要统一：

|                            | 来源                                                                                                                                                               | 值                                                                                                                                      |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **Oliver 提案**              | \[SECTION 11 line 703-724]                                                                                                                                       | `called` / `texted` / `left_voicemail` / `no_answer` / `payment_link_sent` / `form_sent` / `manager_callback_promised` / `other`（8 值）  |
| **2026-05 Final proposal** | Historical；当前 V3 contract 见 [Task V3 Product / Domain Contract §6.4](/product-design/v3/tasks-feature/task-domain-lifecycle.md#64-activity-fields-与-live-values) | `no_answer` / `left_voicemail` / `text_sent` / `callback_requested` / `follow_up_scheduled` / `customer_considering`（Task V2 6 values） |

两套值**名字不一样、数量不一样**：

### 两套逻辑分析

**Oliver 的 8 值——"staff 做了什么"（动作日志）**：记录员工执行了哪个动作，纯行为记录，不管结果。

**Final schema 的 6 值——"事情进展到什么状态"（状态日志）**：记录这次互动后客户侧/进展处于什么状态。

### 关键发现：`record_progress`要和`update`区别开来, 但是两套都有值会和 `update` 交联

`record_progress` 和 `update` 的边界必须清晰——`record_progress` 只记录"发生了什么"，task 本身不变；`update` 改变 task 的 priority / suggestedActions / dueAt。两者不能因同一个理由同时触发。

以下是两套全部值的逐一分析（Oliver 8 值 + schema 6 值，去重后 11 个独立概念）：

| 值                           | 来源                    | 是否和 update 交联 | 分析                                                                            |
| --------------------------- | --------------------- | ------------- | ----------------------------------------------------------------------------- |
| `no_answer`                 | 两套都有                  | ✅ 安全          | 打了没接，task 不变，纯记录尝试次数                                                          |
| `left_voicemail`            | 两套都有                  | ✅ 安全          | 留了语音，task 不变，纯记录                                                              |
| `texted` / `text_sent`      | Oliver / schema（同一概念） | ✅ 安全          | 发了短信，task 不变，纯记录                                                              |
| `called`                    | Oliver only           | ✅ 安全          | 打了电话（有人接但无实质进展），task 不变，纯记录                                                   |
| `payment_link_sent`         | Oliver only           | ⚠️ 边界模糊       | 发了付款链接后，suggestedActions 可能要更新为"跟进是否已付"→ 可能需要同时 `update`                      |
| `form_sent`                 | Oliver only           | ⚠️ 边界模糊       | 发了表单（如取消表单）后，task 可能应该直接 `close`（cancellation proceeding）而不是 record\_progress |
| `callback_requested`        | schema only           | ⚠️ 边界模糊       | 客户要求回拨，dueAt 和 suggestedActions 可能需要跟着变 → 可能同时需要 `update`                     |
| `customer_considering`      | schema only           | ⚠️ 边界模糊       | 客户在考虑中，priority 和 suggestedActions 可能要调整 → 边界不清                               |
| `manager_callback_promised` | Oliver only           | ❌ 和 update 交联 | 承诺了经理回电 → priority 必须升 high，suggestedActions 必须更新 → 该走 `update`，不是记录          |
| `follow_up_scheduled`       | schema only           | ❌ 和 update 交联 | 安排了跟进时间 → dueAt 必须跟着变 → 该走 `update`                                           |
| `other`                     | Oliver only           | —             | 兜底值，语义依赖备注，本身不引发 task 变化，但使用场景不明确                                             |

**结论**：

- **安全用于 `record_progress`（4 个）**：`no_answer` / `left_voicemail` / `text_sent` / `called`
- **应该走 `update` 或 `close`，不该进 `record_progress`（2 个）**：`manager_callback_promised` / `follow_up_scheduled`
- **边界模糊，需要讨论拍板（4 个）**：`payment_link_sent` / `form_sent` / `callback_requested` / `customer_considering`

### 待拍板

progressType 最终枚举应**只保留不引发任务信息变化的事件**，从两套合并后剔除交联值，建议从 4 个安全值出发，再决定边界模糊的 4 个是否纳入（以及纳入后如何防止和 `update` 混用）。

### 状态

待团队拍板 progressType 最终枚举，再同步进 prompt 和 schema。

***

## 问题 11：SECTION 9 优先级判断混入"要不要建 task"的规则

### 现状

\[SECTION 9 PRIORITY JUDGMENT（line 630-642）] 在描述 `low` 优先级时写了两类内容：

**第一类（正确）**：`low` 的使用条件——"有明确但不紧的后续动作"，这是优先级判断应该做的事。

**第二类（放错位置）**："以下情况不要建 low 优先级 task"：

- 例行预约确认
- 确认短信
- intake / waiver 表单提醒
- 到店提示
- no-answer / 满信箱 / 无意义语音（无高意向内容）

### 问题所在

第二类规则实际上不是在说"优先级怎么定"，而是在说"这些场景根本不该建 task"——这是 **CREATE 决策**，属于 \[SECTION 5 CREATE RULES（line 382-410）]的职责范围。

两个 section 的职责边界：

| Section                         | 应该回答的问题               |
| ------------------------------- | --------------------- |
| **SECTION 5 CREATE RULES**      | 什么情况下建 task / 不建 task |
| **SECTION 9 PRIORITY JUDGMENT** | 已经决定要建 task 时，优先级怎么定  |

把"不建 task"的规则写进优先级章节，会让 AI 在两个地方找到关于"建不建"的判断，逻辑分散，容易漏读。

### 与问题 6 的关系

问题 6 是 `suggestedActions` 模板区混入了"建不建 task"判断；本问题是 SECTION 9 优先级区混入了同类判断。**根因相同**：把"不建 task"的规则分散写在多个 section，而不是集中在 SECTION 5 CREATE RULES 一处。

### 建议

把 SECTION 9 里"Do not create low-priority tasks for..."的列表移到 SECTION 5 CREATE RULES 的"Do not create a task when"列表下，SECTION 9 只保留优先级判断逻辑。这样 AI 找"建不建 task"只看一个地方。

***

## 问题 12："configured outreach threshold" 概念与已有设计前后矛盾

### 出现位置

\[05-task-decision] 中两处使用了 "threshold" 概念：

- **SECTION 4 New lead 模板（line 326-327）**："close the task when the customer books or the **configured outreach threshold** is exhausted"
- **SECTION 3 renewal 规则（line 255）**："keep/update until **threshold** or manual close"

### 问题

产品决策是**所有阈值写死为常量**，当前没有自定义阈值的功能。\[04-contact-profile]里已全部硬编码为 3：

- `unreachable` = 至少尝试 **3** 次从未接通
- `neglected` = 少于 **3** 次
- Lead attempt threshold = **3** 次

05-task-decision 用"**configured** outreach threshold"——"configured" 暗示这是个可配置参数，但这个功能根本不存在。同一套 prompt 里一边写死 3，一边用"configured threshold"，前后矛盾，会让人误以为阈值可以调整。

### 建议

删掉"configured outreach threshold"这个概念，统一改为直接写明"3 次尝试后仍无回应"，或改用 `unable_to_reach` closeResult（见问题 4）作为触发条件，和 04-contact-profile 保持一致。

***

## 问题 13：员工行动指导中涉及关闭/更新的逻辑必须与 AI 规则一致

### 设计原则

`suggestedActions` 是写给员工看的行动指引，但部分模板里包含了"什么时候关 task"或"什么时候更新 task"的描述。这些描述必须与 AI 的对应规则完全一致：

- **关闭条件** → 必须与 \[SECTION 7 CLOSE RULES]对应
- **更新条件** → 必须与 \[SECTION 6 UPDATE RULES]对应

如果 `suggestedActions` 里的关闭/更新条件与 SECTION 6/7 不一致，会导致员工行为和 AI 判断相互矛盾——员工以为可以关，AI 却不认这个理由；或者员工按 AI 的规则操作，却在 `suggestedActions` 里找不到对应指引。

### 已发现的不一致

| 模板                                               | suggestedActions 里的描述                                           | SECTION 7 对应规则                                                     | 是否一致         |
| ------------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------ | ------------ |
| New lead（SECTION 4 line 326-327）                 | "close when customer books **or outreach threshold exhausted**" | customer books → `booked` ✅；threshold exhausted → **无对应规则**        | ❌ 缺口         |
| Billing/payment recovery（SECTION 4 line 359-360） | "close as `attempted` when policy allows"                       | `renewal` 的 close 规则用 `renewed` / `renewal_declined`，无 `attempted` | ❌ 不一致（见问题 3） |

### 建议

审查 SECTION 4 所有 8 个 OTF 模板，逐一核对其中提到的关闭/更新条件与 SECTION 6 / SECTION 7 的对应关系。不一致的地方二选一：

1. 修改 `suggestedActions` 模板，改用 SECTION 7 里正确的 closeResult
2. 在 SECTION 7 里补充对应的关闭规则

***

## 问题 14：DNC 时 task 关闭由谁执行

### 两版冲突

**原版（合并版）**：DNC 时 AI 输出 `taskDecisions=[]`（空数组），不枚举任何 close 指令。原因是当时采用 close + create 循环——每次分析都先批量关掉旧 task，再建新的，DNC 就是"不建新的"，所以 AI 什么都不用做，系统处理。

**Oliver 新版（\[SECTION 7 line 514-516]）**：改为 AI 输出 close 指令，引用每个 pending task 的 taskId，用 `closeResult=do_not_contact` 逐个关闭。原因是新版不再批量关，AI 需要明确指定要关哪个 task。

Oliver 的逻辑（AI 枚举 taskId）在技术上可行——输入里的 PENDING TASKS 本来就是按 contactPhone 查出来的，AI 能看到该 contact 所有的 pending task。但这增加了 prompt 的复杂度：AI 需要识别所有 pending task 并逐个输出 close 指令，而不是简单地让系统处理。

**但在"1 contact 最多 1 task"的设计下（见问题 8），这个问题自动消解**：

- 一个 contact 最多 1 个 pending task
- DNC 时系统只需要用 `contactPhone` 就能找到并关闭这个唯一的 task
- AI 不需要枚举 taskId，只需要输出 `doNotContact=true`
- 不存在"漏关某个 task"的风险，因为最多只有 1 个

### 建议

采用系统负责关闭的方式：AI 只需设置 `doNotContact=true`，系统根据 `contactPhone` 自动关闭该 contact 的 pending task，`taskDecisions` 不需要输出 close 指令。这与"1 contact 最多 1 task"设计配合，是最简洁且无漏关风险的方案。
