Files
MailUI4Agents/plugins/pi-mail-bridge/lib/workspace.js
JianFeeeee 011957ac97 refactor(pi-bridge): A 方案落地 —— 平台兜底值搬回平台侧,workspace.js 恢复逐字节相同
pi 定的 A(2026-09-15)。要点是:**契约的逃逸口不是豁免清单,而是
`resolveWorkspaceCwd(workspace, fallback)` 的第二个参数** —— 平台兜底值本来就该由平台侧传进去。
四个桥对照很清楚:

| 桥       | 平台兜底值在哪                                   | 怎么交给共用函数 |
|----------|--------------------------------------------------|------------------|
| opencode | 自己的 `index.js`                                | 传 `directory`   |
| zcode    | 自己的 `src/index.mjs`(`zcodeSessionFallback`) | 传进去           |
| dsh      | 共用的 `mailSessionFallback`(写死 `.dsh`)      | 直接用           |
| pi       | **原来造在共用模块里**(本次出的错)             | → 现在也传进去   |

⇒ `docs/PLUGIN-CONTRACT.md` 第 1150 行**不用改、也不该加旁路**:pi 只是唯一一个把平台值
造在共用模块里的,挪回平台侧就恢复了规矩。

**改动(与 pi 预测的形状一致)**:`workspace.js` 删掉那 15 行、`pool.mjs`/`worker.mjs`
各改一行 import,外加新文件 `src/paths.mjs`。`git diff --stat` 实测
`15 -` / `3 +-` / `3 +-` —— 没有多余改动。

**为什么新家是 `src/paths.mjs` 而不是 `lib/sandbox.js`**(pi 给了两个选项,我选前者):
`lib/` 按契约是"**候选共用**"目录,把一个 pi 专有文件放进去**正是这次出事的形状** ——
下一个人会问"它为什么不在 `ALL_LIBS` 里"。`src/` 下同名文件不会引起这个问题。
父子同源(worker 的沙箱 rw 由父进程算)由"两边 import 同一个模块"继续满足。

**验收四条(pi 给的,逐条实测)**:
1. `cmp opencode/lib/workspace.js pi/lib/workspace.js` **相同**;
   `check-shared-libs.sh` **exit 0**;`install.sh --check` **exit 0** ✓
2. 测试数**不降**:491 → **496**(+5,见下)✓
3. 给 `piMailFallback` **补测试**(原先一条都没有 —— 这正是当初的不对称:
   四个桥 `npm test` 全绿、只有 `check-shared-libs` 抓得到)→ 新增
   `test/pi-paths.test.mjs` 5 条 ✓
4. 四个调用点改 import 后 diff 只应是 import 行 + 删掉的那 15 行 ✓

**新测试为什么是独立文件**:`test/workspace.test.mjs` 在四个桥里**逐字节相同**
(md5 一致,属共用测试),往里加 pi 专有断言会把共用测试也弄分叉 —— 与 `lib/` 同一条规矩。

**判据含结构断言 + 行为断言**,并已按纪律先验区分力:
· 变异 1(把 `piMailFallback` 塞回共用模块 = 本次分叉的形状)⇒ 结构判据**变红**;
· 变异 2(把 `.pi` 改成 `.dsh`)⇒ 三条行为判据**变红**;
两次变异都用 `cp` 恢复并以 `cmp` 校验一致。

★ 顺带记下 pi 指出的一条:`check-deploy-drift.mjs` 的判据 ① 是我扩到"比全部 133 个文件"的,
所以这次分叉**它能抓到** —— 但 `check-shared-libs` 先红了,说明两道门的分工是对的。
2026-09-15 06:31:18 +08:00

78 lines
3.2 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.

/**
* 邮件寻址里的工作目录(三维地址 name@path.session 的 path 位)。
*
* 这个模块存在的理由是一次真实故障:插件建会话时用的 cwd 是自己拼的
* `~/.dsh/mail-sessions/mail-<uuid>` —— 每封邮件一个全新的空目录。
* DSH 与 opencode 都按 cwd 给会话分组,于是所有邮件会话既不属于任何项目、
* 彼此也不同组,界面上全落进「未分组」。
*
* path 位本来就是「希望它在哪儿干活」,插件只需照用。
*/
import { existsSync, mkdirSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { isAbsolute, join, resolve } from 'node:path';
/**
* 校验寻址里的工作目录,不可用时返回调用方给的兜底。
*
* 决策顺序:
* 1. path 位是一个已存在的目录 → 直接用它(同 path 的多封邮件天然同组)
* 2. path 位非空但目录不存在 → **不创建**,返回兜底
* 3. path 位为空(地址写成 `dsh` 而不带 `@/path`)→ 兜底
*
* 为什么不给不存在的 path 建目录:那等于让一个笔误(`/home/porgram/x`)
* 在磁盘上落下一个真目录,而 Agent 会在里面一无所获地干活 ——
* 用户看到会话建起来了却什么都做不了,比明确落到兜底目录更难排查。
*
* 为什么拒绝相对路径:cwd 的相对基准是 harness 进程的启动目录,
* 那是个与邮件语义无关的量(systemd 下通常是 `/`)。
*
* 兜底由调用方给,因为各平台的兜底不同:opencode 有插件启动时的 directory
* 可用,DSH 没有、只能落到 `~/.dsh/mail-sessions/<会话>`(见 mailSessionFallback)。
*
* @param {string} workspace 事件里的 to_workspace
* @param {string} fallback 不可用时的兜底目录(可为空串 = 交给平台自己决定)
* @returns {{cwd: string, grouped: boolean}} grouped 为真表示落在了寻址指定的目录里
*/
export function resolveWorkspaceCwd(workspace, fallback) {
const raw = typeof workspace === 'string' ? workspace.trim() : '';
const fb = typeof fallback === 'string' ? fallback : '';
if (!raw || !isAbsolute(raw)) return { cwd: fb, grouped: false };
const abs = resolve(raw);
try {
if (existsSync(abs) && statSync(abs).isDirectory()) {
return { cwd: abs, grouped: true };
}
} catch {
// 权限不足等:当作不可用
}
return { cwd: fb, grouped: false };
}
/**
* 没有天然兜底的平台(DSH)用这个:`~/.dsh/mail-sessions/<会话 id>`。
* @param {string} sessionKey 会话标识
* @returns {string}
*/
export function mailSessionFallback(sessionKey) {
return join(homedir(), '.dsh', 'mail-sessions', String(sessionKey || 'default'));
}
/**
* 确保兜底目录存在。寻址指定的目录本来就存在(否则不会被选中),
* 只有兜底目录需要现建。
* @param {string} cwd resolveWorkspaceCwd 的结果
* @param {boolean} grouped 是否落在寻址指定的目录里
*/
export function ensureCwd(cwd, grouped) {
if (grouped || !cwd) return;
try {
mkdirSync(cwd, { recursive: true });
} catch {
// 建不出来就让 harness 自己报错,这里不该吞掉真实原因
}
}