# Ask Helix — 飞书对话通道运维手册

面向运维/管理员。讲清楚这条通道**开在哪、怎么开、出问题怎么查**。

## 一句话架构

飞书**不调用** Helix 的工具。飞书只投递消息事件；Helix 在自己的进程内校验来源、
认定发信人、执行工具、组织回复。钥匙、门卫、账本都在 Helix 侧。

```
员工在飞书发消息
  → 飞书事件推送 POST /api/v1/approval/feishu/callback   (应用级唯一事件订阅 URL)
  → 验签 / 时间戳新鲜度 / nonce 重放闸                    (审批链路既有，逐字节复用)
  → 事件分发：assistant 认领 im.message.receive_v1        (其余事件仍走审批状态机)
  → 立即 200 ACK，后台处理
  → 总闸 assistant.enabled → event_id 去重 → 身份解析 → 限速 → 回复
```

## 为什么复用审批的回调地址

一个飞书自建应用**只有一个**事件订阅 URL。Helix 的这个应用早已把它指向审批回调，
所以新增 `im.message.receive_v1` 订阅后，聊天消息必然落到同一个端点。新开一个
endpoint 不会收到任何事件。分叉发生在全部校验之后，审批事件的处理路径没有变化。

## 开通步骤（飞书开放平台，需要应用管理员）

现有应用：与审批 / 通知 / SSO 同一个自建应用（integration kind `chat_approval`）。
**不要新建应用，不要新建凭证。**

1. 「权限管理」添加并申请：`im:message`（接收与发送单聊、**群组**消息）。
   群里 @机器人 的消息由它投递;建群 / 加人 / 转发 / **解散**另需 `im:chat`
   （2026-09-07 已开通）。**读群里的历史消息**(总结、结案说明、解散前补记录)走
   `im:message`(`GET /im/v1/messages?container_id_type=chat`) —— 缺它的表现是「总结不
   出来」而不是报错(代码如实说「读不到群里的消息」)。机器人自己的身份 `GET /bot/v3/info`
   不需要任何权限。

   解散用 `DELETE /im/v1/chats/{chat_id}`,要求应用是这个群的**群主** —— Helix 自己
   建的群天然满足(建群时机器人是 owner)。**别人拉机器人进去的群永远不会被解散**:
   收尾任务只认 `assistant_ticket_chats` 里的行。

   ⚠️ **接收** 和 **读历史** 是两组不同的权限,而且群和单聊各自一条(2026-09-08
   在生产上逐条核对过):

   | 用途 | 权限 |
   |---|---|
   | 收单聊消息 | `im:message.p2p_msg:readonly`(获取用户发给机器人的单聊消息) |
   | **收群里 @机器人 的消息** | `im:message.group_at_msg:readonly`(接收群聊中@机器人消息事件) |
   | 读会话历史(总结 / 结案说明 / 解散前补记录 / 模型的 `read_this_room`) | `im:message` 或 `im:message:readonly` 或 `im:message.history:readonly` |
   | **群消息的历史还要额外一条** | `im:message.group_msg`(获取群组中所有消息,**敏感权限、需审核**) |
   | 发消息 / 卡片 | `im:message`(发送) |

   读历史还有一条前提:**机器人必须在被查询的那个群里**(Helix 自己建的群天然满足)。
   `GET /bot/v3/info`(机器人自己的 open_id)不需要任何权限。

   「需审核权限」勾选之后要**重新发版本 + 企业管理员审批**才生效 —— 勾了没发版,
   表现和没勾一模一样。
2. 「事件与回调 → 事件配置」确认请求地址为
   `https://helix.pingcap.net/api/v1/approval/feishu/callback`（若已配置则保持不变），
   添加**三个**事件：
   - **接收消息 v2.0 `im.message.receive_v1`** —— 单聊与群里 @机器人 的消息;
   - **机器人被加入群聊 `im.chat.member.bot.added_v1`** —— 机器人进群时说一句自我介绍。
     不订阅它,机器人会静静地待在一个新群里:屋里的人不会去猜「@我 就能问」,而一个
     进了群、不说话、也不解释自己能干什么的机器人对他们是纯粹的噪音;
   - **机器人被移出群聊 `im.chat.member.bot.deleted_v1`** —— 把房间落成已关闭状态。
     不订阅它,台账里那条 `feishu_chat_id` 会一直指向一个机器人已经不在的房间,后面
     **每一次**往它发消息都会失败,而失败原因读起来像飞书故障。

   ⚠️ 这两个成员事件是 2026-09-08「助手进群」那一批的一部分,处理器已经在生产代码里 ——
   **缺的只是这一步订阅**,而缺了它不会报错,只会安静地少两种行为。
3. 「版本管理与发布」创建版本 → 提交企业管理员审批 → 发布。**这一步有外部审批延迟，
   宜早提交。**
4. 确认应用可用范围覆盖全员（推送用途下通常已是全员）。

发布后事件即开始投递；此时 `assistant.enabled` 仍为关闭，机器人不会回话——这是有意的
两段式：先让通道在生产静默跑通，再开产品。

## 开关与参数（平台设置 `platform_settings`）

| Key | 默认 | 说明 |
|---|---|---|
| `assistant.enabled` | 关闭 | 产品总闸。**必须是 JSON 布尔 `true`**，字符串 `"true"` 不放行 |
| `assistant.turns_per_user_per_hour` | 60 | 每人每小时回合上限（5–600） |
| `assistant.history_turns` | 12 | 带进模型的历史轮数（2–40） |
| `assistant.session_ttl_minutes` | 120 | 多久没说话就自动开新话题（5–1440） |

模型内核另有自己的开关 `copilot.enabled` 与百炼/Anthropic 凭证（与网页版 copilot 共用
一处配置）。内核未开或未配置时，助手回一条「AI 助手暂时不可用 + 到门户自助」的确定性
回复，不报错。

关闭总闸后，员工看到的与订阅上线前一致：机器人只推送、不回话。
（实现上事件仍会被助手认领并立即 ACK，只是不产生任何回复；审批链路因此也不会收到聊天事件。）

## 排查

| 现象 | 先看 |
|---|---|
| 员工发消息没有任何回复 | `assistant.enabled` 是否为布尔 `true`；应用版本是否已发布 |
| 回复「先登录一次 Helix」 | 该员工没有 active 的飞书**登录**身份（`identity_external_identities` provider=feishu、`is_login_identity=1`、未软删）。让其用飞书登录一次 Helix |
| 回复「账号已停用」 | `identity_persons.employment_status = 'left'` 或已软删 |
| 同一句话收到两次回复 | Redis 不可用时去重 fail-open（宁可重复也不吞消息）。查 Redis 健康 |
| 审批回调受影响 | 分叉只认领 `im.message.receive_v1`；查审计 `approval.callback.processed` 是否照常 |

| 回复「我还在处理你上一条消息」 | 上一回合还没结束（每 chat 串行）。锁 3 分钟自愈 |
| 员工说「AI 助手暂时不可用」 | `copilot.enabled` 或百炼/Anthropic 凭证未配置 |

## 审计与数据留存

两处，用途不同：

- **审计脊柱** `audit_logs`：每条处理过的消息一行 `assistant.message.received`，
  `detail.outcome` ∈ `answered` / `unavailable` / `failed` / `unbound` / `departed` /
  `mismatch` / `rate_limited` / `unsupported` / `reset` / `busy` / `offered` /
  `card_undelivered`，带 trace_id、工具调用名、provider、耗时。**不含员工消息正文。**
- **对话转写** `assistant_conversations` / `assistant_messages`：完整问答原文 + 每条回复
  调了哪些工具（记录**真实工具名**，不是桥的元工具名）、哪个 provider、耗时、trace_id。
  **进到模型回合的失败也在里面**（模型报错 / 模型不可用）——「助手没回答我」必须可核查。
  注意：在回合之前就被挡下的消息（未绑定 / 已离职 / 身份不一致 / 限速 / 非文本 / 重置 /
  发卡片）**只进审计脊柱、不进转写**——那几种情况没有「对话」可记。会话行按短期记忆窗口
  分段（默认 2 小时没说话就算新会话）。

留存：转写目前无自动清理。需要定期清理时按 `assistant_messages.created_at` 删除，
`assistant_conversations` 为父表（先删子表）。

**管理端页面**：`/admin/audit?tab=assistant`（2026-08-17 起是审计日志的第 4 个 tab；旧的
`/admin/assistant-audit` 302 进去。仅 global_admin + auditor——**不含 management**，
转写里是员工描述问题的原话，比领导层只读层平常看到的治理数据敏感一档）。可按人 / 结果 /
工具名 / 时间窗筛选，点行看完整转写。头条数字里的**「被拒的工具调用」平时应为 0**：>0 表示
有东西试图越过只读边界，页面会给警示。对应 MCP 只读工具
`assistant_list_conversations` / `assistant_get_conversation` / `assistant_usage_stats`。

### 房间的两次不可逆事件（2026-09-08 起）

建群会出现在每个成员的会话列表里；解散会把**整段聊天记录**一起带走。这两件事各留
一行审计，因为台账上的 `close_reason` 对「到期自动清理」和「有人在群里敲了一句解散」
给出同一个答案：

| action | actor | detail |
| --- | --- | --- |
| `assistant.room.opened` | 开群的人（转人工时是系统） | `reason`、`members`、`feishu_chat_id`、工单号 |
| `assistant.room.disbanded` | 解散的人；到期清理 / 孤儿清理是 `system` | `reason` ∈ `requested` / `manual` / `retention` / `ticket_gone` / `bot_removed`、`feishu_chat_id`、工单号 |

开群与随之而来的**指派**共用一个 trace_id —— 事后追「这个群是怎么来的」是一条链，
不是两条互不相连的行。**删失败不记**:一行说「群被解散了」而群还在,比没有这一行更糟。

### 员工能从哪里开群

四个面,同一个端点(`POST /support/tickets/{id}/chat`,幂等):

1. 助手答不上来时那张卡上的「转人工」——最常见的来源;
2. 工单详情页(桌面)`/me/support/tickets/{id}`;
3. 工单详情页(**移动端 H5**)`/m/support/{id}` 与 `/m/admin/support/tickets/{id}` —— 员工基本都在飞书客户端里,这是他最常用的那个面;
4. MCP `open_ticket_chat`(AI 客户端)。

四个面共用一条判据:**求助人本人、这张工单的处理人、或 IT 管理员**。只读档
(auditor / management)读得到工单但开群会被拒(开群会 `assign`)—— 前端也据此
不显示那颗按钮。**已经结束的工单不给开新群**:说一句「还有问题」会重开工单,
重开之后就能再拿到房间。

### 群里的讨论会进模型吗

只在**有人要**的时候,四处:

1. 群里说「总结」(字面命令);
2. 处理人按「我弄完了」(整理结案说明);
3. 收尾任务删房间前保证工单上有过程记录;
4. **模型自己调 `read_this_room`**(2026-09-08 起)—— 员工用一句自然语言问
   「看一下群里的聊天 然后总结一下什么问题」时走的就是这条。

前三处的产物写进工单的备注(`support_ticket_messages.kind='note'`);第四处只进
这一轮的上下文。**模型不可用 / 读不到时什么都不写**,并如实说读不到 —— 一段占位
的假总结会被下一个人当成事实,而一个空结果会被模型读成「大家什么都没说」。

> **为什么第 4 条要单独存在**:这个能力原来只挂在字面命令上,于是一句自然语言
> 落到通用问答,而模型手里没有任何读群的手段 —— 生产上它回的是「我无法查看群聊
> 历史,只能看到当前对话」。那句话对平台是**不准确**的,而一个「我做不到」的回答
> 在平台其实做得到的时候比错误答案更贵:员工据此不会再问第二次,能力从此只对
> 知道咒语的人存在。

群里的**普通问答**和单聊走同一条路（转写进 `assistant_messages`）。房间的聊天记录
本身 Helix 不留副本：解散之后只剩工单上那段记录，这正是「删之前先保证有记录」这条
规矩存在的原因。

### 运营台的读写档位不同

`/admin/assistant-handoff` 的**读**是审计档（global_admin + auditor + 只读的 MCP
`assistant_ticket_chat_overview`），而每一处**写**是 global_admin：改派路由负责人走
`require_route_write`，解散房间走支持域的 `can_act_on_ticket`。审计员看得见这张表、
控件是灰的，页面顶部有一条说清为什么的横幅 —— 一颗按下去必然 403 的控件读起来像
平台坏了。

## 写操作：员工按下确认才发生

助手可以**提案**，不能执行。员工说「转人工 / 开工单」→ 助手用对话内容拟一张工单 → 发一张
飞书卡片 → **点卡片上的「确认提交」原生按钮**（唯一的提交方式;身份来自飞书验签信封，转发出去点不动，不跳浏览器、不需要登录）。「查看完整内容」外链是**纯只读**页，用来看卡片截断掉的全文
→ 才走既有申请流。

- 确认链接**一次性、只对本人**。提案行 **1 小时**后失效（token 与提案同寿（60 分钟），两者一起失效），
  过期后打开会看到「未提交」而不是一个点了必失败的按钮。
- 确认页显示的参数带 sha256 摘要，确认调用必须回显它：中途任何改动都会被拒绝，而不是提交
  一份员工没读过的内容。
- 执行走领域自己的服务（首个动作是 `SupportService.create`），审批、通知、幂等、审计与网页
  提交完全一致。
- **没有对应的 MCP 工具**，这是有意的：让 agent 能替人按确认键会抵消这条路径存在的意义。

排查确认页：

| 现象 | 先看 |
|---|---|
| 「这个确认链接已经失效」 | 已过期（提案 1 小时）、已用过、或当事人已离职/被强制登出 |
| 「内容与你打开时不一致」 | 提案参数在打开后被改动；让员工回飞书重新提一次 |
| 点了确认没反应 | 看 `assistant_action_proposals.status` 与 `result`，失败会记 `failed` + 错误 |

## 群聊（2026-09-08 起）

助手现在也在**群里**应答。判据只有一个:**它被 @ 了**。

> ⚠️ 判据**不是**「飞书把这条消息投给了我们」。投递范围取决于应用当前持有哪些
> 权限(`im:message` 与 `im:message.group_at_msg` 的范围不同),而权限会变 ——
> 一个今天成立的推断,明天多开一个 scope 就变成「机器人开始回答它偷听到的每
> 一句话」。判断留在代码里(`group_chat.addressed_to_bot`),不留在权限配置里。

两种群,两个开关:

| 群 | 是什么 | 开关 | 默认 |
|---|---|---|---|
| 工单群 | Helix 自己为「转人工」建的房间(`assistant_handoffs.feishu_chat_id` 上有它) | `assistant.group_chat_enabled` | **开** |
| 任意群 | 有人把机器人拉进去的房间 | `assistant.group_chat_any_enabled` | **关** |

另有 `assistant.group_turns_per_hour`(默认 120)——**每个群**每小时的回合上限,
与每人上限各自独立:十个人在一个群里各问六句,每人都没超,群超了。

三行都由迁移 632 播种,所以它们在平台设置页里看得见、改得动(幂等:已经存在的
行不覆盖)。

群里的行为约定:

- **答复回到问的那个房间。** 工单群平铺发;任意群走**话题回复**(挂在提问那条
  消息下),否则一屋子人看到的是一堆没有上文的答复。
- **涉及个人记录的内容与所有确认卡一律私聊**,群里只留一句「已私信」。确认卡绑
  本人、只有本人能按,发在群里等于给别人一个必然失败的按钮。
- **「新话题」只作用于私聊线程**:群里发言不会改写 `assistant:active_chat`。
- **裸资产号的回购捷径在群里不触发** —— 那条捷径的前提是「刚才我私下给过你这个
  号」。
- 在工单群里打「转人工」→ 回一句「这个群就是转人工的结果」,**不建第二张工单**。
- 「我不在这儿应答」这句话每个房间 24 小时最多说一次(沉默会被读成机器人坏了,
  每条都回则是刷屏)。

### 工单群里能做什么(2026-09-08 起)

工单群里挂着一张**工单卡**(编号 / 状态 / 处理人 / 优先级 + 三颗按钮),工单在
任何地方变状态它都会原地重画,并在群里留一句「刚刚发生了什么」。

群里 @Helix 可以直接说(字面命令,不经模型):

| 说什么 | 做什么 | 谁能做 |
|---|---|---|
| `状态` | 这单现在什么情况 | 屋里任何人 |
| `认领` / `我来处理` | 接手这张工单 | IT 管理员 / 当前处理人 |
| `转给 @某人` | 换处理人(并把他拉进群) | IT 管理员 / 当前处理人 |
| `拉 @某人 进来` | 把人加进这个群 | 屋里任何人 |
| `优先级 高/紧急/普通/低` | 改优先级 | IT 管理员 / 当前处理人 |
| `总结` | 读一次群 → 四栏小结 → 发群里 **并写进工单** | 屋里任何人 |
| `交接给 @某人` | 总结 + 转派 + 拉他进群 | IT 管理员 / 当前处理人 |
| `解决了 <说明>` | 结案,说明写进工单 | IT 管理员 / 当前处理人 |
| `没解决` / `还有问题` | 已解决的重开;未解决的记一条 | 求助人(管理员亦可) |
| `撤回` | 撤掉这张工单 | 求助人(管理员亦可) |
| `帮助` | 列出以上这些 | 屋里任何人 |

卡片上的按钮按状态给,**最多三颗**:未认领 →「我来处理 / 标为高优先级」;有人
处理 →「我弄完了 / 标为高优先级」;已解决或等回复 →「问题解决了 / 还有问题」;
已关闭或已撤回 → 只剩「刷新」。

> 🔴 **「问题解决了」和「还有问题」只有求助人能按**(管理员亦可)。处理人替他按
> 第一颗,等于自己给自己发合格证。「我弄完了」**不直接结案**:它先在群里发一段
> 结案说明等确认,确认时校验摘要 —— 「你确认的」必须和「写进去的」是同一段。

关单/解决之后群里会出现**三档满意度**卡(只有求助人能评,一张工单一次)。在后台
结案时群里同样会收到它。

> 🔴 **只有 `总结`(以及 `交接`)会去读群历史。** 助手不订阅群消息 —— 每次读取
> 都落一行审计 `assistant.chat.history_read`(哪个房间、多少条、谁让读的,
> **不含正文**)。模型不可用时**不写任何东西**并在群里说明:一段编出来的小结会
> 被下一个人当成事实。小结在群里和在工单上**逐字相同**。

> 🔴 **「在这个房间里」不是授权。** 每一条写命令都重新问一次
> `support.service.can_act_on_ticket`(IT 管理员 **或** 这张工单的处理人)——
> 一个被拉进来看一眼的同事不该因此获得改优先级或转派的权力。同一个判据也管着
> 卡片按钮、后台 HTTP 端点和 MCP 工具。

**开群的三个入口**:助手答案卡上的「💬 转人工」、门户工单详情页的「拉个群聊」、
后台工单页同一颗按钮(以及 MCP `open_ticket_chat`)。一张工单最多一个群,第二次
点拿到的是同一个。工单还没人认领时由求助人开群,对面取**兜底路由的负责人**。

排查:

| 现象 | 先看 |
|---|---|
| 群里那张卡不跟着工单变 | `assistant_ticket_chats.card_message_id` 是否为空(发卡时没拿到 id);日志 `ticket card refresh failed` |
| 点了按钮说「只有 IT 管理员或处理人能改」 | 这张工单的 `assignee_id` 是谁;判据只有那两条 |
| 「拉个群聊」返回 409 | 工单还没人认领,且兜底路由没有指派负责人 —— 在 `/admin/assistant-handoff` 配 |
| 群还在但助手当它是「任意群」 | `assistant_ticket_chats` 里那一行的 `state` 是不是 `closed` |
| 「总结」说读不到历史 | `GET /im/v1/messages` 需要 `im:message`;日志 `could not read room` 带飞书原始错误 |
| 「总结」说 AI 不可用 | `copilot.enabled` 与百炼/Anthropic 凭证 —— 与助手回答共用一处配置 |
| 点「我弄完了」说「结案说明已经不是最新的了」 | 那段小结之后又被新的盖过(摘要校验挡下),重新点一次 |
| 群里评价说「现在评不了」 | 已经评过,或工单还没到 resolved/closed |
| 结了很久的群没被解散 | 任务运行结果里的 `kept_for_record`(记录没落成)与 `failed`(飞书拒绝);保留期是否被设成 0 |
| 「解散群」说「工单还是处理中」 | 终态闸:先把工单关掉或撤回 |

### 群的收尾(2026-09-08 起)

每日 03:20 的 `assistant.ticket_chat_sweep`:

| 群的状态 | 做什么 | 设置 |
|---|---|---|
| 工单已关闭/撤回,且过了保留期 | 补一段过程记录 → 发告别语 → 解散 | `assistant.ticket_chat_retention_days`(默认 7,0=永不) |
| 工单还开着,但长期没动静 | 群里问一句「这件事还要不要」 | `assistant.ticket_chat_idle_nudge_days`(默认 14,0=不问) |
| 工单已被硬删 | 发一句 → 解散 | — |

> 🔴 **删之前工单上必须有过程记录** —— 房间的聊天记录跟着解散一起消失。模型不
> 可用或读不到房间时**留着房间**,并计入运行结果的 `kept_for_record`:那一格
> 和「没到期」在 `disbanded == 0` 上长得一样,而原因完全不同。
>
> 🔴 **不自动关单。** 自动关掉一张还开着的工单,会让「已关闭」不再代表任何东西。
>
> 运行结果的字段:`considered` / `disbanded` / `nudged` / `failed` /
> `kept_for_record` / 两个阈值。**全部尝试都失败**时任务会抛(那说明 `im:chat`
> 权限或凭据坏了),单个失败只留到下一次重试。

手动解散三个入口:群里说「解散群」、门户/后台工单详情的按钮(只在终态出现,
带二次确认)、MCP `disband_ticket_chat`。

### 负责人兜底与群内催办(2026-09-08 起)

路由表多了一列**备份负责人**。拉人之前先问「他今天在吗」:

1. 主负责人今天**没有在批准中的休假**、并且有企业邮箱 → 拉他。
2. 否则拉备份负责人(开场白里会说明原因,员工不会以为拉错了人)。
3. 两个都不行 → 仍然降级成只建工单。

> 🔴 **休假判据与对外可用性接口逐字同源**(`APPROVED` / `PENDING` /
> `PENDING_CANCELLATION`,以及 NULL 保守算「不在」)。**查不到休假记录 = 在** ——
> 休假数据来自 HRIS 同步,可能压根是空的,而一个同步故障不该让所有转人工都降级。
> 主负责人休假而**没有备份**时仍然拉主负责人:一个休假中的负责人好过一个空房间。

**群内 SLA 催办**:`support.ticket_sla_reminder` 每次给处理人发催办卡时,群里也
说一句(催办卡只发给处理人一个人,而求助人就在同一个房间里等着)。**复用那个
job 已有的水位与冷却**,不另立时间表。

催到 `assistant.room_escalate_after_reminders`(默认 3)轮还没动 → 把备份负责人
(退到 IT 管理员)拉进房间,**只一次**,且**不自动改派**。

| 现象 | 先看 |
|---|---|
| 转人工拉的是备份而不是主负责人 | 主负责人今天的 `hr_leave_absences`;或者他没有企业邮箱 |
| 群里没有 SLA 催办 | `support.ticket_sla_reminder` 的 `cooldown_hours` 与工单的 `last_agent_reminded_at` —— 群里那句话与卡片同一条阶梯 |
| 升级没有拉人 | 路由上有没有配备份负责人;备份是不是就是当前处理人(那时是空操作);`assistant_ticket_chats.escalated_at` 是不是已经有值 |

机器人被拉进群 / 被移出群(`im.chat.member.bot.added_v1` / `deleted_v1`)也由助手
认领:进群时自我介绍(该应答的群才发),被移出时记一行审计
`assistant.chat.membership`。

**需要在飞书开放平台补的事件订阅**(与 `im.message.receive_v1` 同一处,加完要发
一个应用版本):

- 接收消息 v2.0 `im.message.receive_v1`(已有)
- **机器人进群 `im.chat.member.bot.added_v1`**
- **机器人被移出群 `im.chat.member.bot.deleted_v1`**

未订阅这两个事件不影响问答,只是没有进群自我介绍、也没有被移出的审计行。

排查:

| 现象 | 先看 |
|---|---|
| 群里 @ 了没反应 | 该群属于哪一类、对应开关是否为 JSON 布尔 `true`;审计 `assistant.message.received` 的 `detail.outcome` 是否为 `not_addressed` / `group_disabled` |
| 群里答的是别人的问题 | `detail.group_kind` 与 `chat_type`;工单群应为 `handoff` |
| 回复跑到私聊去了 | 这条链路只有确认卡与个人记录走私聊,其余都用 `ReplyTarget`;看 `assistant: reply delivery failed to dm:…` 日志 |
| 日志里 `could not resolve bot identity` | `GET /bot/v3/info` 不可用 —— 此时降级成「有任何 @ 就应答」,**没有 @ 仍然不应答** |

## 边界（当前切片）

- 处理单聊与**群聊**文本消息(群聊需 @机器人)。图片、文件回「只看得懂文字」。
- 话题群(`chat_type` 既不是 `p2p` 也不是 `group`)仍然认领后忽略。
- 应用（机器人）发出的消息一律丢弃——机器人回应机器人是死循环的起点。
- 只读工具面 = 网页版 copilot 的三个自我收口工具 + 平台的**只读 MCP 工具全集**（按 `mutates=`
  真值派生，DESTRUCTIVE 与助手自己的对话审计工具排除；数量随平台变化，当前 270+）。工具随平台
  自动生长：新自助流程按 MCP-sync 规则配了 MCP 工具，助手当天就会用。写操作一律走确认卡片，
  聊天面到不了任何一次写。
- 白名单写动作：转人工建单、VPN Full Mode 申请、设备借用/报修。资产申请只开「借用 / 报修」，
  认领与转移归属不从聊天发起。
- **软件坐席暂未纳入**：其逻辑在 HTTP router 与 MCP 工具里各内联一份，需先提取成共享服务函数
  再接入，避免第三份副本漂移。
