/** * 直接加载 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);