Files
MailUI4Agents/deploy/verify-ext-parent-path.mjs
JianFeeeee be9bfd3cd1 chore(deploy)★★: 固定端口监听扩展的失效防护 + SSE 投递审计脚本
## 一次真实事故(2026-10-03)

pi-web 会话里**所有**子 agent 都启动失败,报错

    Error: listen EADDRINUSE: address already in use 127.0.0.1:14010

这个扩展把端口写死,于是「端口被占」直接变成**全部启动失败**,
没有任何降级路径。这次提交把那条路径变成可测的。

- `check-listen-portability.mjs`(360 行):主判据
- `mutate-listen-portability.py`(124 行):变异体 —— 确认判据抓得住
- `verify-ext-parent-path.mjs` / `verify-ext-port-taken.mjs`:
  两条独立路径。**"端口已被占用"那一条之前从没真正跑完过** ——
  它原先被塞在一条 `node -e` 里,进程在「await 端口探测」那步被 30s
  超时掐断,而它恰恰是唯一会决定「子进程会不会被打死」的分支。

## scripts/sse-delivery-audit.sh(298 行)

从网关 access log 判定「SSE 那批邮件到底投没投出去」,**只读**。

为什么必须进版本库而不是 `.tmp/`:`.tmp/` 在 .gitignore 里,
写在 `.tmp/stress-test.sh` 的判据下一轮就没了 ——
而 `docs/DEBTS.json` 里 `stress-sse-delivered-but-silent-no-assertion`
那笔债会长期挂着。**判据和账本必须同寿命。**

## .gitignore

补 `/tmp/`:blob 阈值测试会生成 13MB 级 .bin
(`client/electron/test/tmp/att-threshold-test/`),可再生,不该进版本库
(本机实测 25MB)。
2026-10-05 09:26:59 +08:00

81 lines
3.4 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.

/**
* 直接加载 pi-a2a / pi-acp 扩展(像 pi 用 jiti 那样),验证**根进程路径**:
* 端口空闲 ⇒ 真的绑定成功并能应答。
*
* 为什么需要它:sessiond 是启动时加载扩展的,它内存里仍是**旧代码**,
* 所以本轮改动在父进程路径上没有被真实跑过。而父进程正是「端口空闲」
* 那条分支 —— 不测就等于没验。
*/
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const AGENT = "/root/.pi/agent";
const jitiMod = require(`${AGENT}/npm/node_modules/jiti/lib/jiti.mjs`);
const createJiti = jitiMod.createJiti ?? jitiMod.default ?? jitiMod;
const target = process.argv[2]; // pi-a2a | pi-acp
const port = process.argv[3];
const which = target === "pi-acp" ? "__piAcpServer" : "__piA2aServer";
const currentKey = target === "pi-acp" ? "__piAcpCurrent" : "__piA2aCurrent";
// 选一个空闲端口:让扩展的端口探测走「未被占用」分支
process.env.PI_A2A_PORT = port;
process.env.PI_ACP_PORT = port;
delete process.env.PI_SUBAGENT_CHILD; // 明确不是子进程
delete process.env.PI_SUBAGENT_PARENT_DEPTH;
const file = `${AGENT}/extensions/${target}/index.ts`;
const jiti = createJiti(import.meta.url, { interopDefault: true, fsCache: false, moduleCache: false });
// jiti.import 已把 default 解开;不要再 .default 取一次(那是 undefined)
const factory = (await jiti.import(file)).default;
// 最小 ExtensionAPI 桩:扩展工厂只需要它来登记工具/事件
const registered = [];
const pi = {
registerTool: (t) => registered.push(t.name),
on: () => {},
sendUserMessage: () => {},
sendMessage: () => {},
};
factory(pi);
// ensureServer 现在是 async,等它把端口绑定做完
await new Promise((r) => setTimeout(r, 700));
const srv = globalThis[which];
if (!srv) {
console.log(`RESULT=NO_SERVER ${target}(未创建 server —— 端口被占用路径或子进程路径)`);
process.exit(2);
}
const addr = srv.address();
if (!addr) {
console.log(`RESULT=NOT_LISTENING ${target}(server 已建但未绑定 —— 不该发生)`);
process.exit(3);
}
// 真发一个请求,确认服务真的能用。
// ⚠️ 两个扩展的应答形状**故意不同**,不能用同一个断言:
// pi-a2a 实现了 A2A Agent Card(GET / 返回 {name:"Pi",...})
// pi-acp 没有 card 端点;它对无 method 的请求走 400,对已知 method 才应答。
// 第一版我对两者都断言 body.name === "Pi",pi-acp 报 BAD_CARD ——
// 查证后是**测试的假设错了**,不是代码错(acp 本来就没这个端点)。
const res = await fetch(`http://127.0.0.1:${addr.port}/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(target === "pi-acp"
? { jsonrpc: "2.0", id: 1, method: "session/cancel", params: { sessionId: "x" } }
: { jsonrpc: "2.0", id: 1, method: "noop" }),
});
const body = await res.json();
const okShape = target === "pi-acp"
? Boolean(body.result && "sessionId" in body.result)
: Boolean(body.result && body.result.message);
if (!okShape) {
console.log(`RESULT=BAD_RESPONSE ${target} ${JSON.stringify(body).slice(0, 200)}`);
process.exit(4);
}
console.log(`RESULT=LISTENING ${target} port=${addr.port} http=${res.status} shape=ok`);
console.log(`TOOLS=${registered.join(",")}`);
srv.close();
process.exit(0);