docs: 日历子系统 + 寻址发现工具组 + 权限死锁的排查记录
PHASE7-REMAINING 新增日历一节:三层分离的理由、修掉的六个真问题
(含每一个的现场取证与判据)、前端三个易错点、验证清单、未做的缺口。
写成「可核对的规格 + 踩坑理由」而不是叙事:这些 bug 的共同点是
**不报错**(模板烤死时间、{time} 渲染成 UTC、每 tick 重发、附件从未落盘、
TRIGGER 往返断开、PG schema 缺表),下一个人只有知道判据才能避开。
README 把插件工具表从「六个」更新到十一个(现在 homeagent 是十五个),
并说明寻址发现那一组解决的是**猜地址** —— 生产上真的发生过一个 Agent
猜了 opencode@/home,投递成功但那不是它的工作目录,那封邮件静默变成了
一条平行会话的开端。
PLUGIN-CONTRACT 补 B-8(权限询问转邮件)的判据表与三平台差异。
This commit is contained in:
@ -16,6 +16,7 @@
|
||||
|---|---|---|---|
|
||||
| `plugins/opencode-mail-bridge/` | opencode | `@opencode-ai/plugin` | JavaScript |
|
||||
| `plugins/dsh-mail-bridge/` | DeepSeek Harness | Cordis | TypeScript |
|
||||
| `plugins/pi-mail-bridge/` | pi | pi SDK (`createAgentSession`) | JavaScript (ESM) |
|
||||
|
||||
第八、九节是这两次适配的实现细节与踩坑记录 —— 规范部分(一至七节)与平台无关。
|
||||
|
||||
@ -112,7 +113,7 @@ name@path.session
|
||||
|
||||
### 能力自检脚本
|
||||
|
||||
接入前先回答这七个问题。任何一个答不出来,先去读平台文档,不要开始写代码:
|
||||
接入前先回答这八个问题。任何一个答不出来,先去读平台文档,不要开始写代码:
|
||||
|
||||
```
|
||||
1. 我怎么在不通过 UI 的情况下让某个会话跑一轮? → C-1
|
||||
@ -330,14 +331,79 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一
|
||||
| B-8.1 | `relay_key` 用平台的权限 id;平台不给 id 时用 `会话:工具:callId` 拼一个 | MUST |
|
||||
| B-8.2 | 转发失败 → 让位给平台本地 UI,不要占着钩子 | MUST |
|
||||
| B-8.3 | 同一次询问重复触发只产生一封邮件(服务端按 `relay_key` 幂等) | MUST |
|
||||
| B-8.4 | 邮件正文带足够上下文(工具名、参数摘要),让人能判断 | SHOULD |
|
||||
| B-8.4 | 邮件正文带足够上下文(工具名、参数摘要、**触发这次询问的任务与派活人**),让人能判断 | SHOULD |
|
||||
| B-8.5 | 权限询问**不消耗配额** | MUST |
|
||||
| B-8.6 | **不传 `to`** —— 决策人由服务端解析 | MUST |
|
||||
| B-8.7 | 平台支持「永久允许」时,选项里必须给出「一直同意」并**真的记住它** | MUST |
|
||||
| B-8.8 | 免批的作用域是 **(会话, 工具名)**;决策文本判定用 `lib/permission-grants.js` | MUST |
|
||||
|
||||
> **B-8.1 为什么必须是平台的 id**:服务端会随决策事件把 `relay_key` 回传,
|
||||
> 插件重启丢了内存映射也能对上(`B-4.2`)。自己生成的随机 id 重启后就对不上了。
|
||||
>
|
||||
> **B-8.5 为什么不收费**:人不点头 Agent 就动不了,对它收费等于收「求人费」。
|
||||
> 这里的 `relay_key` 只用于幂等,不是配额豁免的凭证。
|
||||
>
|
||||
> **B-8.6 为什么不能自己定决策人**:插件手边最自然的候选是「来信人」
|
||||
> (`mailContexts` 里的 `replyTo`),但来信人可能是**另一个 Agent** ——
|
||||
> Agent 把任务分派给自己或同伴的另一条会话时,权限邮件就发给了 Agent 自己。
|
||||
> 后果是**死锁而不是报错**:Agent 不可能在 Web 界面上点「同意」,服务端的
|
||||
> 用户推送又投进一个不存在的通道(没有任何人被提醒),于是插件里那个
|
||||
> `await` 永不 resolve —— 会话永久挂死,没有超时、没有日志、没有回信。
|
||||
>
|
||||
> 决策人必须由服务端定:它按 **会话 owner → 线索里最近的人类 → 无人可问则
|
||||
> 409** 解析,那是唯一能看到整条线索的地方。插件只有本地那点上下文,
|
||||
> 猜不出「这条 Agent 链最初是谁派的活」。
|
||||
>
|
||||
> 收到 409(整条链上没有人类)时按 `B-8.2` 处理 —— 服务端已经判定没人可问,
|
||||
> 继续等下去就是死锁。
|
||||
>
|
||||
> **B-8.4 为什么要带派活人**:决策人未必是这条会话的参与者。Agent 转派出来的
|
||||
> 会话,人从没见过它,只给一句「是否允许执行 bash」无从判断 —— 得知道这活是
|
||||
> 谁派的、为的什么事。
|
||||
|
||||
#### B-8.7 / B-8.8 免批(「一直同意」)
|
||||
|
||||
默认语义是**每次都问**。这在「跑一条命令看看」时是对的,在「审查这个工程」
|
||||
时是灾难 —— 实测同一条 pi 会话被问了 **15 次 bash**,人点了 15 次「同意」,
|
||||
全是同一类操作。
|
||||
|
||||
三个平台的原生能力不同,决定了免批状态该由谁持有:
|
||||
|
||||
| 平台 | 原生三态 | 免批由谁记 | 选项 |
|
||||
|---|---|---|---|
|
||||
| opencode | `once` / `always` / `reject` | **平台**(回 `response:"always"`) | 同意 / 一直同意 / 拒绝 |
|
||||
| pi | 只有 block / 放行 | **桥**(`createGrantStore()`) | 同意 / 一直同意 / 拒绝 |
|
||||
| DSH | `allowed-once` / `rejected`(无 always) | 不提供 | 同意 / 拒绝 |
|
||||
|
||||
> **平台能记就让平台记**:opencode 的 `always` 是它自己的权限模型的一部分,
|
||||
> 桥再存一份就有两个真相来源(`I-1`)。pi 的钩子只能答"拦/不拦",没有地方
|
||||
> 表达"以后别问了",这时桥持有状态是唯一选择。
|
||||
>
|
||||
> **DSH 为什么干脆不给这个选项**:它的 `ApprovalOutcome` 只有
|
||||
> `allowed-once | rejected | cancelled | unavailable`。桥自己记的话,DSH 侧
|
||||
> 仍会每次调 `approval/request`,而桥不问就答 `allowed-once` —— 那是用插件
|
||||
> 内存**覆盖**平台的审批策略,且这份策略没人能审计。给不出的能力就不要在
|
||||
> 界面上摆一个按钮(摆了又不生效比没有更糟,见下)。
|
||||
>
|
||||
> **作用域为什么是 (会话, 工具名)**:人看到的那句「是否允许执行 bash?」
|
||||
> 就是在这个粒度上提的问,授权范围不该超出提问范围。
|
||||
> - 放宽到全局 → 人为「审查 llmsproxy」批准的 bash,会静默授权另一个
|
||||
> 发件人派来的另一条任务。那不是他批准的东西。
|
||||
> - 收紧到 `toolCallId` → 等于没有免批。
|
||||
>
|
||||
> **换模型重开会话必须撤销**(`revokeSession`):授权是人对**那次**上下文的
|
||||
> 判断,新会话重跑一遍提示,不该继承上一条的授权。
|
||||
>
|
||||
> **只在内存里是有意的**:长期免批该由平台自己的 settings 管
|
||||
> (pi 的 `settings.json`、opencode 的 permission 配置)。让守护进程的内存
|
||||
> 变成事实上的安全策略,没人能审计,重启后又悄悄消失。
|
||||
>
|
||||
> **判定为什么必须用共用模块**:`isAlwaysDecision` 用的是**精确匹配**。
|
||||
> 写成 `/^同意/` 之类的前缀正则会让「同意」也判成 always ——
|
||||
> 人点一次单次授权,后面所有命令都不再问,这是把单次授权静默升级成永久授权。
|
||||
> 反向的坑同样真实:opencode 侧的三态映射一度以 `: "once"` 兜底,
|
||||
> 于是任何意外文本(空串、旧选项、服务端将来新增的选项)都会**放行**一次
|
||||
> 没人批准的操作 —— 兜底必须落在 `reject` 一侧(`N-9`)。
|
||||
|
||||
### B-9 关停(MUST)
|
||||
|
||||
@ -365,6 +431,17 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一
|
||||
| **T-5** | `forward_mail` | MAY | 转发(走完整三维寻址,它是一条新线索) |
|
||||
| **T-6** | `connect_to_server` | MAY | 登记密钥并注册,方便首次接入 |
|
||||
| **T-7** | ~~`request_permission`~~ | **MUST NOT** | 见 `I-1`:改挂平台权限钩子 |
|
||||
| **T-8** | `suggest_address` | SHOULD | 查可用收件人/工作目录/会话;**发信前应先调** |
|
||||
| **T-9** | `list_contacts` | SHOULD | 列出自己参与过的全部会话及可投递地址 |
|
||||
| **T-10** | `session_participants` | MAY | 列出某条会话的参与方(用于回给抄送方或转达) |
|
||||
| **T-11** | `read_thread` | MAY | 查看某封邮件所在线索的完整往来 |
|
||||
| **T-12** | `read_mail` | SHOULD | 读一封邮件的完整内容(含参与方地址与 reply_address) |
|
||||
| **T-13** | `propose_alias`(T-2 参数) | MAY | 模型在 `send_mail` 的 `propose_alias` 字段提议改名 |
|
||||
|
||||
> **为什么 T-8 到 T-12 都是 SHOULD/MAY 而不是 MUST**:它们解决的是静默投递错误
|
||||
> (`suggest_address`)和信息不足(`list_contacts` / `read_mail` / `read_thread`),
|
||||
> 但都不影响主链路(收信 → 起会话 → 自动回信)。一个最小可行插件只注册 T-1 和 T-2
|
||||
> 就能跑通主链路,上面这些是让它「不猜地址、不漏抄送方」的增强。
|
||||
|
||||
### T-1 `read_inbox` 的三条硬规则
|
||||
|
||||
@ -386,6 +463,52 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一
|
||||
模型可能忘了调(危险操作直接执行),也可能在不需要时乱调(每一步都问人)。
|
||||
平台的权限钩子拦下的那一次是事实。见 `I-1`、`B-8`。
|
||||
|
||||
### T-8 ~ T-12 寻址发现工具(`lib/discovery.js`)
|
||||
|
||||
这五个工具的渲染逻辑全部在 `lib/discovery.js`(三平台共用),与平台 SDK 无关。
|
||||
它们解决的是**静默投递错误**:模型猜一个地址(如 `opencode@/home`),投递成功
|
||||
但那不是 opencode 的工作目录,静默变成了一条平行会话 —— 发件人以为在续谈,其实
|
||||
在跟一条空会话说话。`suggest_address` 用模型从候选列表里选而不是猜,彻底杜绝了
|
||||
这种错误。
|
||||
|
||||
| # | 工具 | 渲染函数 | 服务端端点 |
|
||||
|---|---|---|---|
|
||||
| T-8 | `suggest_address` | `renderNameSuggestions` / `renderPathSuggestions` / `renderSessionSuggestions` | `GET /agent/contacts/suggest` |
|
||||
| T-9 | `list_contacts` | `renderContacts` | `GET /agent/contacts` |
|
||||
| T-10 | `session_participants` | `renderParticipants` | `GET /agent/sessions/{id}/participants` |
|
||||
| T-11 | `read_thread` | `renderThread` | `GET /agent/mail/{id}/thread` |
|
||||
| T-12 | `read_mail` | 内联渲染(含参与方地址与 reply_address) | `GET /agent/mail/{id}` |
|
||||
|
||||
> **`read_mail` 的 `reply_address` 必须回显**:它告诉模型「发件人的可投递地址是什么」。
|
||||
> 不回显的话模型只能用原始地址续谈,但那个地址的 session 位可能是默认的,回过去
|
||||
> 不一定落到同一条线索里。
|
||||
|
||||
### T-13 `propose_alias`(T-2 `send_mail` 的可选参数)
|
||||
|
||||
`propose_alias` 让模型在干完活后提议一个更贴切的会话别名(例如从邮件主题
|
||||
`排查登录问题` 改成更精确的 `fix-session-cookie-leak`)。
|
||||
|
||||
| # | 规则 | 违反后果 |
|
||||
|---|---|---|
|
||||
| T-13.1 | 标记格式是 HTML 注释 `<!-- agentmail:rename-session alias="x" reason="y" -->` | 服务端正则匹配不上,提议静默消失 |
|
||||
| T-13.2 | 标记必须由**共用库** `lib/rename-proposal.js` 构造 | 各平台各写一遍拼接,少个空格就失效 |
|
||||
| T-13.3 | 别名不合法时不追加标记(`isProposableAlias` 返回 false) | 发一个服务端匹配得上却校验失败的标记,白白浪费一轮 |
|
||||
| T-13.4 | 回显**服务端返回的** `rename_proposed`,不是本地提议的值 | 服务端跑过 `normalizeAlias`(非法字符换 `-`、`new` 变 `session-new`),回显本地值会让模型拿一个不存在的名字寻址 |
|
||||
| T-13.5 | 回显时必须说明「等人确认,生效前继续用原别名」 | 模型以为改名已生效,接着用新别名当地址发信 —— 那个别名此刻还不存在 |
|
||||
|
||||
### T-3 / T-4 附件参数命名差异
|
||||
|
||||
两个共享参数在三个平台的工具里名称不一致(历史原因,已锁定):
|
||||
|
||||
| 参数语义 | opencode | dsh / pi |
|
||||
|---|---|---|
|
||||
| 要上传的本地文件路径 | `path` | `file_path` |
|
||||
| 下载后的保存路径 | `save_to` | `save_path` |
|
||||
| 自定义展示文件名 | `filename` | `filename`(新增) |
|
||||
|
||||
这些是**工具参数**,不是 API 字段,因此与服务端协议无关。但新插件实现时应参照
|
||||
自己所在平台已有插件的命名,避免同一平台内两个工具参数风格不一致。
|
||||
|
||||
---
|
||||
|
||||
## 四、降级语义 / Degradation
|
||||
@ -810,6 +933,7 @@ GET /api/v1/attachments/{id}
|
||||
| **N-10** | 修改模型输出的文本内容 | `I-4`:插件只搬运 |
|
||||
| **N-11** | 首次 SSE 连接带 `Last-Event-ID` | 会重投历史邮件 |
|
||||
| **N-12** | 让共用模块依赖平台 SDK | 那是它们能跨平台共用的前提 |
|
||||
| **N-13** | 给 `/permission/request` 传 `to`(尤其是来信人) | 来信人可能是 Agent,权限邮件发给它必然死锁(`B-8.6`) |
|
||||
|
||||
### 共用模块必须逐字节相同
|
||||
|
||||
@ -824,6 +948,10 @@ GET /api/v1/attachments/{id}
|
||||
| `workspace.js` | 寻址 path 位 → 可用的 cwd |
|
||||
| `model-scope.js` | 模型目录整理 + 降级顺序 + 失败报告 |
|
||||
| `catchup.js` | 离线期间积压邮件的补投选择 |
|
||||
| `addressing.js` | 三维地址的拆分与校验 |
|
||||
| `discovery.js` | 寻址发现工具的渲染(`T-8`~`T-12`) |
|
||||
| `rename-proposal.js` | 会话改名标记的构造与回执文案(`T-13`) |
|
||||
| `permission-grants.js` | 权限决策文本判定 + 免批授权表(`B-8.7` / `B-8.8`) |
|
||||
|
||||
> **为什么必须逐字节相同而不是「行为一致」**:一侧改了另一侧没改,两个平台的行为
|
||||
> 会悄悄分叉 —— 同一封邮件在 A 平台标了已读、在 B 平台没标,而两处代码看起来都
|
||||
@ -914,6 +1042,15 @@ GET /api/v1/attachments/{id}
|
||||
|
||||
[ ] 模型主动调 send_mail 回信的那一轮
|
||||
→ 只有一封邮件,没有额外的自动转发(B-5.3)
|
||||
|
||||
[ ] 模型带 propose_alias 发信(T-13)
|
||||
→ 入库正文里**没有** agentmail:rename-session 标记(已被剥掉)
|
||||
→ mails.rename_alias / rename_reason 记下了提议
|
||||
→ GET /sessions/{id}/rename-proposal 返回该提议
|
||||
→ 人接受(PUT /sessions/{id}/alias)后别名真的改了,alias_source 变 manual
|
||||
→ 接受后 rename-proposal 返回 null(提示条不再反复弹)
|
||||
→ 新别名可寻址:<name>@<目录>.<新别名> 落进同一条会话
|
||||
→ 桥的后续命名同步**不覆盖** manual 别名
|
||||
```
|
||||
|
||||
### 7.4 工作目录
|
||||
@ -956,6 +1093,25 @@ GET /api/v1/attachments/{id}
|
||||
→ 平台侧那次工具调用继续执行(D-2 / B-4.1)
|
||||
[ ] 重复触发同一次询问(事件重放)
|
||||
→ 只产生一封邮件(W-6 幂等)
|
||||
|
||||
[ ] **Agent 自己给自己派活的会话里触发权限询问**(B-8.6 / N-13)
|
||||
造:让 Agent 用 send_mail 发给自己 <name>@<目录>.new,正文要求它跑 bash
|
||||
→ 权限邮件的 to_name 是**人**(会话 owner 或线索里最近的人类),不是 Agent
|
||||
sqlite3 "SELECT to_name FROM mails WHERE mail_type='permission_request' …"
|
||||
→ 正文里带得出「触发任务」与「任务来自」(B-8.4)
|
||||
→ 整条链上确实没有人类时:服务端返回 409,插件让位给本地 UI,
|
||||
**不是**无声挂起(日志里要能看到让位那一行)
|
||||
|
||||
[ ] **「一直同意」真的免批**(B-8.7 / B-8.8)
|
||||
造:一封信里要求连续三次单独调用 bash
|
||||
→ 第一次弹权限,点「一直同意」
|
||||
→ 后两次**不再产生邮件**:
|
||||
sqlite3 "SELECT COUNT(*) FROM permission_requests WHERE session_id='…'"
|
||||
→ 应为 1,不是 3
|
||||
→ 换个工具(write)仍会问一次 —— 授权不跨工具
|
||||
→ 另一条会话的 bash 仍会问 —— 授权不跨会话
|
||||
反例(修复前的真实数据):同一条会话 15 封 permission_request,
|
||||
result 全是「同意」,界面上那个「一直同意」按钮点了等于没点
|
||||
```
|
||||
|
||||
### 7.7 上报
|
||||
@ -1001,6 +1157,10 @@ GET /api/v1/attachments/{id}
|
||||
| 模型目录 | `client.config.providers()`(`models` 是**对象**) | `ctx.llm.listProviders()` + `listModels()` | `modelRuntime.getAvailable()`(**不是** `getModels()`:1221 条里只有 1 条能用) |
|
||||
| 别名来源 | `session.slug`(创建时就有) | 模型标题派生 | **邮件主题派生**(SDK 会话没有平台标题,见下) |
|
||||
| 日志可见性 | `console.error` | `console.error`(`ctx.logger` 不进 journalctl) | `console.error` |
|
||||
| 工作区分组 | `session.create({directory})` 自带 | `ctx.get('workspaceRegistry')` → `create(cwd)` + `attachSession()` | 按 cwd 自动分目录(`~/.pi/agent/sessions/--tmp-x--/`),无需注册 |
|
||||
| 工具参数命名 | `path` / `save_to` | `file_path` / `save_path` | `file_path` / `save_path` |
|
||||
| 权限三态 | 原生 `once`/`always`/`reject` | **只有** `allowed-once`/`rejected` | 只有 block/放行 |
|
||||
| 「一直同意」 | 给,平台自己记 | **不给**(表达不了,桥代劳会覆盖平台策略) | 给,桥用 `createGrantStore()` 记 |
|
||||
|
||||
> **pi 为什么是守护进程而不是扩展**:pi 扩展被加载进**一条已经存在的**会话,
|
||||
> 那条会话的 cwd 由启动 pi 的人决定。而 `B-3.1` 要求每封邮件的 `to_workspace`
|
||||
@ -1163,6 +1323,83 @@ socat 被 `Requires` 带停后再没起来。加 `PartOf` 后又发现 `start`
|
||||
同理不要用 `list(cwd)`:桥的进程 cwd 与会话 cwd 无关,按前者过滤会漏掉
|
||||
所有真正在干活的会话。
|
||||
|
||||
### 9.14 改名提议的标记拼错是**静默失败**
|
||||
|
||||
`<!-- agentmail:rename-session alias="x" reason="y" -->` 由服务端正则解析
|
||||
(`gateway/internal/handler/rename_proposal.go`)。少个空格、把双引号写成单引号、
|
||||
或把 `reason=""` 写成空属性 —— 邮件照常发出,提议凭空消失,而模型以为自己提过了,
|
||||
在后续对话里当作已完成的事引用。
|
||||
|
||||
> 这就是为什么拼接必须收在共用库 `lib/rename-proposal.js`(`T-13.2`):
|
||||
> 三平台各写一遍的话,任一处的偏差都不会有人发现。
|
||||
> `test/rename-proposal.test.mjs` 里内置了一份服务端正则的等价实现来验证生成结果。
|
||||
|
||||
### 9.15 换 Gateway 地址后必须清 `lastEventID` 并重连 SSE
|
||||
|
||||
守护进程形态的插件(pi)提供 `connect_to_server` 时有个额外义务:改完
|
||||
`baseURL` 要**重建 SSE 长连**,因为旧长连仍连着旧地址。
|
||||
|
||||
更微妙的是断点:`lastEventID` 是**旧** Gateway 环形缓冲里的序号,拿它去问新
|
||||
Gateway 会命中一段完全无关的历史(或被拒),收到的事件属于别人的会话。
|
||||
`GatewayClient.reconfigure()` 在地址变化时清空它,于是重连以「首次连接」姿态
|
||||
(不带 `Last-Event-ID`,见 `N-11`)进行。
|
||||
|
||||
插件形态(opencode/dsh)没有这个问题:它们的 SSE 由宿主生命周期管,
|
||||
`connect_to_server` 只改配置,下次宿主重连时自然用新值。
|
||||
|
||||
### 9.16 把来信人当权限决策人 → 会话永久挂死
|
||||
|
||||
插件手边最自然的决策人候选是「来信人」(`mailContexts` 里的 `replyTo`),
|
||||
pi 侧一度就是这么传的。它在人给 Agent 派活时看起来完全正确,直到
|
||||
**Agent 给 Agent 派活**:
|
||||
|
||||
```
|
||||
jianf → pi(会话 A) pi 把子任务分给自己(会话 B)
|
||||
会话 B 触发 bash 权限询问
|
||||
replyTo = "pi" → 权限邮件发给 pi 自己
|
||||
```
|
||||
|
||||
后果不是报错而是**死锁**,而且是三重静默:
|
||||
|
||||
1. Agent 不可能在 Web 界面上点「同意」
|
||||
2. 服务端 `SendToUser("pi")` 投进一个不存在的用户通道 —— 没有任何人被提醒
|
||||
3. 插件里 `await new Promise(...)` 永不 resolve —— 没有超时、没有日志、没有回信
|
||||
|
||||
会话就那么停在那里,从外面看像「模型在思考」。实测发生过:两条会话排了任务,
|
||||
只有一条在干活,另一条的最后动静是一封 `to_name=pi` 的 `permission_request`。
|
||||
|
||||
> 修法见 `B-8.6` / `N-13`:**不传 `to`**,决策人由服务端沿会话上溯解析。
|
||||
> 服务端是唯一能看到整条线索的地方;插件只有本地那点上下文,猜不出
|
||||
> 「这条 Agent 链最初是谁派的活」。
|
||||
>
|
||||
> 排查这类问题的第一个动作:
|
||||
> `SELECT to_name FROM mails WHERE mail_type='permission_request'` ——
|
||||
> 出现 Agent 名就是它。
|
||||
|
||||
### 9.17 摆一个「一直同意」按钮却不实现它
|
||||
|
||||
比不给这个选项更糟的是给了但不生效。pi 侧一度在 `options` 里放了
|
||||
`['同意','一直同意','拒绝']`,放行正则也认 `一直同意` ——
|
||||
但**没有任何地方记住它**,所以点完下一条命令照样来一封邮件。
|
||||
|
||||
生产数据(修复前):
|
||||
|
||||
```
|
||||
同一条 pi 会话被问了 15 次 bash
|
||||
permission_requests.result 全是「同意」—— 一次「一直同意」都没有
|
||||
```
|
||||
|
||||
人不是不想点,是点了发现没用,于是退回去一条一条点「同意」。
|
||||
|
||||
> 判断一个平台该不该提供这个选项,看它的钩子能不能表达"以后别问了":
|
||||
> - opencode 能(`response:"always"`)→ 给,且让平台记
|
||||
> - pi 不能(钩子只回 block/放行)→ 给,桥自己记(`B-8.8`)
|
||||
> - DSH 不能且不该由桥代劳(会覆盖平台审批策略)→ 不给
|
||||
>
|
||||
> 自查:在 `options` 里加任何选项之前,先在代码里搜一遍这个选项的文本,
|
||||
> 看它除了"被展示"之外还出现在哪里。只出现在 `options` 和一条放行正则里,
|
||||
> 就是这个坑。
|
||||
|
||||
---
|
||||
|
||||
## 附:文档关系
|
||||
|
||||
Reference in New Issue
Block a user