Files
MailUI4Agents/plugins/opencode-mail-bridge/test/permission-note.test.mjs
JianFeeeee 1ec88866ac fix(permission): 另外四家桥的"人类说明"缺口 —— 三家修、一家本来就有
承接 453f451(pi 桥)。用户批准后把同款缺口在其余四家逐一核对:
**zcode 本来就有**(`说明:${decision.note}`),homeagent / dsh / opencode 三家缺。

## homeagent(Go,能完整修)

SSE 事件结构里**根本没有 Note 字段**(json 里只有 decision/decided_by)⇒ 备注在
解码那一步就没了。补上字段,并把提示词抽成纯函数 `permissionDecisionPrompt(evt)`,
加了判据(说明必须出现 + 反向对照:无说明/空白说明不得凭空造出说明段)。
构建(`go build -buildmode=plugin`)后 install 到
`/home/newqqagent/plugins/homeagent-mail-bridge/plugin.bin` 并重启,已核验部署件
含新符号(`grep -a`,中文用 strings 查是查不到的)。

## dsh / opencode(平台回执放不下理由 → 分两步)

两家的审批回执都是**三态字符串**:DSH `ApprovalOutcome` 只有
allowed-once / rejected / cancelled / unavailable,openCode 只有 once / always / reject
—— **没有地方放人类的说明**。所以:

1. 提示词("你之前发起的权限请求已有结论:…")统一走 `permissionPrompt(data)`,
   带上 `用户的说明:…`。dsh 原有**三处**内联文案(续谈/新会话/通知投递),
   措辞分叉正是这类信息漏掉的地方 —— 判据直接钉"只有一处拼这句话"。
2. 带说明的决策**另投一趟通知**,让模型在会话里看到理由。代价是多一轮;比悄悄
   丢掉人的指令轻(原缺陷就是丢了指令,模型把同一条命令换写法又问一遍,连问 9 次)。
3. 决策回执不再被当成"新任务"(内容已随 permission_decision 交付),并记下
   `decision_mail_id` 防重复 —— 与 pi 桥同源。

判据:dsh / opencode 各 5 条(含"拿缺陷时的源码形态喂进来必须判红"的自检)。

## 部署与代价

- dsh → 快照 20260914-081456、opencode → 20260914-081516、homeagent → 新 plugin.bin,
  三家的服务 active 且心跳/连接已核。
- 重启 dsh 时它正在"续谈"一封邮件(08:10:45 日志)——事后核对:那一轮**已回完**
  (faad0037 的 parent = 4919aa88),没有丢活。
- 套件:dsh 372、opencode 323、homeagent go test ok、zcode 382 全绿。
2026-09-14 08:17:23 +08:00

43 lines
1.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 「人类的说明必须到达模型」—— opencode 桥的接线判据2026-09-13 线上缺陷)。
*
* 与 dsh 桥同源:`note` 在 SSE 回包里没人读,模型只拿到「决策」一个词。
* openCode 的权限回执response: once/always/reject**放不下理由**,所以有说明时
* 另外投一趟通知 —— 这条接线也必须被钉住,否则"修了"会随重构悄悄消失。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = readFileSync(join(HERE, '..', 'index.js'), 'utf8');
test('权限结论的提示词带上了人类的说明', () => {
const fn = SRC.match(/function permissionPrompt\(data\) \{[\s\S]*?\n\}/);
assert.ok(fn, 'permissionPrompt 必须存在');
assert.match(fn[0], /note/);
assert.match(fn[0], /用户的说明/);
});
test('★ 只有一处拼"已有结论"', () => {
assert.equal([...SRC.matchAll(/你之前发起的权限请求已有结论/g)].length, 1);
assert.match(SRC, /permissionPrompt\(data\)/);
});
test('回执之后:有说明就另投一趟通知', () => {
assert.match(SRC, /data\.note\.trim\(\)/);
assert.match(SRC, /deliverMail\(client, directory, data, "permission"\)/);
});
test('决策回执不再被当成新任务', () => {
assert.match(SRC, /mail_type === "permission_decision"/);
});
test('★ 判据自检:旧形态必须判红', () => {
const old = '? `你之前发起的权限请求已有结论:${data.decision}(决策人:${data.decided_by || "用户"})。请据此继续后续工作。`';
assert.equal(/用户的说明/.test(old), false, '旧形态没有说明段');
assert.equal([...old.matchAll(/你之前发起的权限请求已有结论/g)].length, 1);
});