@modelzen/feishu-codex-bridge 0.3.12-test.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ function bridgeVersion() {
15
15
  }
16
16
 
17
17
  // src/cli/commands/doctor.ts
18
- import { existsSync as existsSync6 } from "fs";
18
+ import { existsSync as existsSync5 } from "fs";
19
19
  import { homedir as homedir2 } from "os";
20
20
  import { join as join9 } from "path";
21
21
 
@@ -67,8 +67,9 @@ var paths = {
67
67
  keystoreSaltFile: join(appDir, ".keystore.salt"),
68
68
  npmCacheDir: join(appDir, "npm-cache"),
69
69
  /**
70
- * 按需重后端(claude-sdk 等 npm-ondemand 包)私装目录:一个扁平
70
+ * 按需后端(npm-ondemand 包)私装目录:一个扁平
71
71
  * `~/.feishu-codex-bridge/backends/node_modules` 放所有按需后端的 npm 包。
72
+ * (通用基础设施;当前内置后端 codex 是 external-cli,不落此目录,保留以备将来。)
72
73
  * 永远在用户 HOME 下、用户可写(零 sudo/brew),与全局包目录的权限死结解耦。
73
74
  * 解析靠 createRequire(backendsDir/...).resolve(见 agent/backend-loader);
74
75
  * 安装靠 `npm install --prefix backendsDir`(见 agent/installer)。 */
@@ -134,14 +135,6 @@ function getMaxConcurrentRuns(cfg) {
134
135
  function getPendingPolicy(cfg) {
135
136
  return cfg.preferences?.pendingPolicy === "queue" ? "queue" : "steer";
136
137
  }
137
- function getAcpCommand(cfg) {
138
- const raw = cfg.preferences?.acpCommand;
139
- if (!raw || typeof raw !== "object" || typeof raw.command !== "string" || !raw.command.trim()) {
140
- return void 0;
141
- }
142
- const args = Array.isArray(raw.args) ? raw.args.filter((a) => typeof a === "string") : [];
143
- return { command: raw.command, args };
144
- }
145
138
  var RUN_IDLE_TIMEOUT_MIN_SEC = 10;
146
139
  var RUN_IDLE_TIMEOUT_MAX_SEC = 3600;
147
140
  function getRunIdleTimeoutMs(cfg) {
@@ -336,44 +329,6 @@ import crossSpawn from "cross-spawn";
336
329
  function spawnProcess(command, args = [], options = {}) {
337
330
  return crossSpawn(command, [...args], { windowsHide: true, ...options });
338
331
  }
339
- async function killProcessGroup(pid, hasExited, opts = {}) {
340
- if (pid === void 0) return;
341
- const isWin = opts.isWindows ?? process.platform === "win32";
342
- const kill = opts.kill ?? ((t, s) => void process.kill(t, s));
343
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
344
- const graceMs = opts.graceMs ?? 4e3;
345
- const pollMs = opts.pollMs ?? 200;
346
- if (isWin) {
347
- const taskkill = opts.taskkill ?? ((p) => {
348
- spawnProcess("taskkill", ["/pid", String(p), "/T", "/F"], { stdio: "ignore" }).on("error", () => void 0);
349
- });
350
- try {
351
- taskkill(pid);
352
- } catch {
353
- }
354
- return;
355
- }
356
- const groupSignal = (signal) => {
357
- try {
358
- kill(-pid, signal);
359
- return true;
360
- } catch (err) {
361
- const code = err.code;
362
- if (code === "ESRCH") return false;
363
- try {
364
- kill(pid, signal);
365
- } catch {
366
- }
367
- return true;
368
- }
369
- };
370
- if (!groupSignal("SIGTERM")) return;
371
- for (let waited = 0; waited < graceMs; waited += pollMs) {
372
- await sleep(pollMs);
373
- if (hasExited()) return;
374
- }
375
- groupSignal("SIGKILL");
376
- }
377
332
  function spawnProcessSync(command, args = [], options = {}) {
378
333
  return crossSpawn.sync(command, [...args], { windowsHide: true, ...options });
379
334
  }
@@ -843,46 +798,6 @@ var BACKEND_CATALOG = [
843
798
  },
844
799
  // supportedModes undefined ⇒ 全档(qa/write/full)。
845
800
  blurb: "\u80FD\u529B\u6700\u5168\uFF08goal/steer/compact/resume + \u771F\u6C99\u7BB1\u53EA\u8BFB\u6863\uFF09"
846
- },
847
- {
848
- id: "claude-sdk",
849
- agentFamily: "claude",
850
- displayName: "Claude\uFF08Agent SDK\uFF09",
851
- access: "sdk",
852
- dep: {
853
- kind: "npm-ondemand",
854
- pkg: "@anthropic-ai/claude-agent-sdk",
855
- // version 省略 ⇒ latest:下载/更新都取最新(让「检查更新」有意义;坏发布风险由上游控)。
856
- approxSizeMB: 224,
857
- detectHint: "\u672A\u5B89\u88C5 @anthropic-ai/claude-agent-sdk\uFF08\u5728\u63A7\u5236\u53F0\u70B9\u300C\u4E0B\u8F7D\u300D\u5373\u6309\u9700\u88C5\u5230\u7528\u6237\u76EE\u5F55\uFF09",
858
- installCmd: "\u5728 Web \u63A7\u5236\u53F0\u70B9\u300C\u4E0B\u8F7D Claude SDK\u300D\uFF08\u7EA6 224M\uFF0C\u6309\u9700\u88C5\u5230\u7528\u6237\u76EE\u5F55\uFF09"
859
- },
860
- supportedModes: ["full"],
861
- blurb: "\u5F00\u7BB1\u5373\u7528\uFF08SDK \u81EA\u5E26 Claude Code \u4E8C\u8FDB\u5236\uFF0C\u7EA6 224M\uFF0C\u6309\u9700\u4E0B\u8F7D\uFF09",
862
- hidden: true
863
- // 用户不可见闸:claude 后端尚未完整验证,先全隐藏(验证后删此行点亮)
864
- },
865
- {
866
- id: "claude-acp",
867
- agentFamily: "claude",
868
- displayName: "Claude\uFF08\u8BA2\u9605\xB7ACP\uFF09",
869
- access: "acp",
870
- dep: {
871
- kind: "npm-ondemand",
872
- pkg: "claude-pty-acp",
873
- binName: "claude-pty-acp",
874
- // version 省略 ⇒ latest:claude-pty-acp 是本项目自管的适配器、迭代快,跟 latest 比
875
- // pin 死省去每次发版改 catalog 的摩擦(坏发布的风险由我们自己控)。
876
- // node-pty 多数平台走 prebuilds(darwin/win 预编译、无需现场编译),可按需装到用户目录;
877
- // 仍需本机 claude CLI 已登录(适配器把交互式 Claude Code 暴露成 ACP agent → 走订阅)。
878
- approxSizeMB: 75,
879
- detectHint: "\u672A\u5B89\u88C5 claude-pty-acp\uFF08\u5728\u63A7\u5236\u53F0\u70B9\u300C\u4E0B\u8F7D\u300D\u5373\u6309\u9700\u88C5\u5230\u7528\u6237\u76EE\u5F55\uFF1B\u53E6\u9700\u672C\u673A claude \u5DF2\u767B\u5F55\uFF09",
880
- installCmd: "\u5728 Web \u63A7\u5236\u53F0\u70B9\u300C\u4E0B\u8F7D Claude \u8BA2\u9605\u9002\u914D\u5668\u300D\uFF08\u7EA6 75M\xB7node-pty \u591A\u5E73\u53F0\u514D\u7F16\u8BD1\uFF09\uFF1B\u6216 npm i -g claude-pty-acp"
881
- },
882
- supportedModes: ["full"],
883
- blurb: "\u8D70\u8BA2\u9605\u8BA1\u8D39\uFF08\u4E0D\u70E7 SDK credit\uFF09\uFF0C\u6309\u9700\u4E0B\u8F7D\u9002\u914D\u5668 + \u9700\u672C\u673A claude \u5DF2\u767B\u5F55",
884
- hidden: true
885
- // 用户不可见闸:claude 后端尚未完整验证,先全隐藏(验证后删此行点亮)
886
801
  }
887
802
  ];
888
803
  function visibleCatalog() {
@@ -2000,1443 +1915,137 @@ var CodexAppServerBackend = class {
2000
1915
  { timeoutMs: READ_HISTORY_TIMEOUT_MS }
2001
1916
  );
2002
1917
  const thread = res.thread;
2003
- const all = (Array.isArray(thread?.turns) ? thread.turns : []).map(mapTurn).filter((t) => t.userText || t.assistantText || t.tools.length);
2004
- const totalTurns = all.length;
2005
- const turns = totalTurns > maxTurns ? all.slice(totalTurns - maxTurns) : all;
2006
- return {
2007
- turns,
2008
- totalTurns,
2009
- name: thread?.name ?? void 0,
2010
- preview: thread?.preview ?? void 0,
2011
- createdAt: thread?.createdAt,
2012
- updatedAt: thread?.updatedAt
2013
- };
2014
- } catch (err) {
2015
- log.fail("agent", err, { phase: "thread/read", sessionId });
2016
- return empty;
2017
- }
2018
- }
2019
- async startThread(opts) {
2020
- const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
2021
- const client = await this.spawn(opts.cwd);
2022
- const res = await client.request("thread/start", {
2023
- cwd: opts.cwd,
2024
- approvalPolicy: APPROVAL_POLICY,
2025
- ...sandbox,
2026
- developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
2027
- ...opts.model ? { model: opts.model } : {}
2028
- });
2029
- return new CodexThread(client, res.thread.id, opts.model, opts.effort);
2030
- }
2031
- async resumeThread(opts) {
2032
- const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
2033
- const client = await this.spawn(opts.cwd);
2034
- const res = await client.request("thread/resume", {
2035
- threadId: opts.sessionId,
2036
- cwd: opts.cwd,
2037
- approvalPolicy: APPROVAL_POLICY,
2038
- ...sandbox,
2039
- developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
2040
- ...opts.model ? { model: opts.model } : {}
2041
- });
2042
- return new CodexThread(client, res.thread.id, opts.model, opts.effort);
2043
- }
2044
- async spawn(cwd) {
2045
- const bin = resolveCodexBin();
2046
- if (!bin) throw new Error("codex CLI not found (set CODEX_BIN or install @openai/codex)");
2047
- const warmed = takeWarmClient(bin);
2048
- void refillWarmPool();
2049
- if (warmed) return warmed;
2050
- const client = new AppServerClient({ bin, cwd });
2051
- await client.connect();
2052
- return client;
2053
- }
2054
- };
2055
- function isBoilerplateUserText(text) {
2056
- const t = text.trimStart();
2057
- return t.startsWith("<environment_context>") || t.startsWith("# AGENTS.md instructions");
2058
- }
2059
- function mapTurn(turn) {
2060
- const userParts = [];
2061
- const assistantParts = [];
2062
- const reasoningParts = [];
2063
- const tools = [];
2064
- for (const item of turn.items ?? []) {
2065
- switch (item.type) {
2066
- case "userMessage": {
2067
- const text = item.content.map((c) => c.type === "text" ? c.text : c.type === "mention" ? `@${c.name}` : "").join("").trim();
2068
- if (text && !isBoilerplateUserText(text)) userParts.push(text);
2069
- break;
2070
- }
2071
- case "agentMessage":
2072
- if (item.text.trim()) assistantParts.push(item.text);
2073
- break;
2074
- case "reasoning": {
2075
- const r = (item.content.length ? item.content : item.summary).join("\n").trim();
2076
- if (r) reasoningParts.push(r);
2077
- break;
2078
- }
2079
- case "commandExecution":
2080
- tools.push({
2081
- title: item.command,
2082
- output: item.aggregatedOutput ?? void 0,
2083
- exitCode: item.exitCode,
2084
- failed: item.status === "failed" || item.status === "declined" || (item.exitCode ?? 0) !== 0
2085
- });
2086
- break;
2087
- case "fileChange":
2088
- tools.push({ title: "\u7F16\u8F91\u6587\u4EF6", failed: item.status === "failed" || item.status === "declined" });
2089
- break;
2090
- case "webSearch":
2091
- tools.push({ title: `\u8054\u7F51\u641C\u7D22\uFF1A${item.query}` });
2092
- break;
2093
- case "mcpToolCall":
2094
- tools.push({ title: `${item.server} / ${item.tool}`, failed: item.status === "failed" || Boolean(item.error) });
2095
- break;
2096
- case "dynamicToolCall":
2097
- tools.push({ title: item.tool, failed: item.status === "failed" || item.success === false });
2098
- break;
2099
- // plan / contextCompaction / review-mode / image* — omitted from the digest
2100
- default:
2101
- break;
2102
- }
2103
- }
2104
- return {
2105
- userText: userParts.join("\n\n"),
2106
- assistantText: assistantParts.join("\n\n"),
2107
- reasoning: reasoningParts.join("\n\n"),
2108
- tools,
2109
- startedAt: turn.startedAt ?? void 0
2110
- };
2111
- }
2112
- function mapModel(m) {
2113
- return {
2114
- id: m.id,
2115
- displayName: m.displayName ?? m.id,
2116
- description: m.description ?? "",
2117
- hidden: m.hidden ?? false,
2118
- isDefault: m.isDefault ?? false,
2119
- supportedEfforts: (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort),
2120
- defaultEffort: m.defaultReasoningEffort ?? "medium"
2121
- };
2122
- }
2123
- var STATIC_MODELS = [
2124
- {
2125
- id: "gpt-5.5",
2126
- displayName: "GPT-5.5",
2127
- description: "\u9ED8\u8BA4\u6A21\u578B",
2128
- hidden: false,
2129
- isDefault: true,
2130
- supportedEfforts: ["low", "medium", "high"],
2131
- defaultEffort: "medium"
2132
- }
2133
- ];
2134
-
2135
- // src/agent/claude-sdk/backend.ts
2136
- import { randomUUID as randomUUID2 } from "crypto";
2137
-
2138
- // src/agent/backend-loader.ts
2139
- import { createRequire } from "module";
2140
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2141
- import { join as join6 } from "path";
2142
- import { pathToFileURL } from "url";
2143
- var BackendNotInstalledError = class extends Error {
2144
- constructor(pkg) {
2145
- super(`\u540E\u7AEF\u4F9D\u8D56\u300C${pkg}\u300D\u672A\u5B89\u88C5\uFF08bridge \u81EA\u8EAB\u4E0E\u7528\u6237\u79C1\u88C5\u76EE\u5F55\u5747\u672A\u627E\u5230\uFF09`);
2146
- this.pkg = pkg;
2147
- this.name = "BackendNotInstalledError";
2148
- }
2149
- pkg;
2150
- };
2151
- function userAnchor() {
2152
- return join6(paths.backendsDir, "__backend_anchor__.cjs");
2153
- }
2154
- var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND", "ERR_PACKAGE_PATH_NOT_EXPORTED"]);
2155
- function isNotFound(err) {
2156
- const code = err?.code;
2157
- if (code !== void 0 && NOT_FOUND_CODES.has(code)) return true;
2158
- const msg = err?.message ?? "";
2159
- return /Failed to (load|resolve) (url|import)/i.test(msg);
2160
- }
2161
- async function loadBackendDep(pkg) {
2162
- try {
2163
- return await import(pkg);
2164
- } catch (err) {
2165
- if (!isNotFound(err)) throw err;
2166
- }
2167
- let resolved;
2168
- try {
2169
- resolved = createRequire(userAnchor()).resolve(pkg);
2170
- } catch {
2171
- throw new BackendNotInstalledError(pkg);
2172
- }
2173
- return await import(pathToFileURL(resolved).href);
2174
- }
2175
- function isBackendDepInstalled(pkg) {
2176
- try {
2177
- createRequire(import.meta.url).resolve(pkg);
2178
- return true;
2179
- } catch {
2180
- }
2181
- try {
2182
- createRequire(userAnchor()).resolve(pkg);
2183
- return true;
2184
- } catch {
2185
- return false;
2186
- }
2187
- }
2188
- function backendsBinPath(binName) {
2189
- const dir = join6(paths.backendsDir, "node_modules", ".bin");
2190
- const candidates = process.platform === "win32" ? [join6(dir, `${binName}.cmd`), join6(dir, binName)] : [join6(dir, binName)];
2191
- return candidates.find((p) => existsSync3(p)) ?? null;
2192
- }
2193
- function isBackendBinInstalled(binName) {
2194
- return backendsBinPath(binName) !== null;
2195
- }
2196
- function isBackendEntryInstalled(entry) {
2197
- const { kind, binName, pkg } = entry.dep;
2198
- if (kind === "external-cli") return false;
2199
- if (binName) return isBackendBinInstalled(binName);
2200
- return pkg ? isBackendDepInstalled(pkg) : false;
2201
- }
2202
- function isBackendInstalledInUserDir(entry) {
2203
- const { binName, pkg } = entry.dep;
2204
- if (binName) return isBackendBinInstalled(binName);
2205
- if (!pkg) return false;
2206
- try {
2207
- createRequire(userAnchor()).resolve(pkg);
2208
- return true;
2209
- } catch {
2210
- return false;
2211
- }
2212
- }
2213
- function installedBackendVersion(pkg) {
2214
- const readVer = (file) => {
2215
- try {
2216
- const j = JSON.parse(readFileSync2(file, "utf8"));
2217
- return typeof j.version === "string" ? j.version : null;
2218
- } catch {
2219
- return null;
2220
- }
2221
- };
2222
- const inUser = join6(paths.backendsDir, "node_modules", ...pkg.split("/"), "package.json");
2223
- const v1 = readVer(inUser);
2224
- if (v1) return v1;
2225
- try {
2226
- const resolved = createRequire(import.meta.url).resolve(`${pkg}/package.json`);
2227
- return readVer(resolved);
2228
- } catch {
2229
- return null;
2230
- }
2231
- }
2232
-
2233
- // src/agent/claude-sdk/event-map.ts
2234
- function toolTitle(name, input2) {
2235
- const i = input2 && typeof input2 === "object" ? input2 : {};
2236
- const s = (v) => typeof v === "string" ? v : "";
2237
- switch (name) {
2238
- case "Bash":
2239
- return { title: s(i.command) || "Bash", detail: s(i.description) || void 0 };
2240
- case "Read":
2241
- return { title: `\u8BFB\u53D6 ${s(i.file_path)}`.trim() };
2242
- case "Write":
2243
- return { title: `\u5199\u5165 ${s(i.file_path)}`.trim() };
2244
- case "Edit":
2245
- return { title: `\u7F16\u8F91 ${s(i.file_path)}`.trim() };
2246
- case "Glob":
2247
- case "Grep":
2248
- return { title: `${name}: ${s(i.pattern)}`.trim() };
2249
- case "WebSearch":
2250
- return { title: `\u8054\u7F51\u641C\u7D22\uFF1A${s(i.query)}` };
2251
- case "WebFetch":
2252
- return { title: `\u6293\u53D6\u7F51\u9875\uFF1A${s(i.url)}` };
2253
- case "Task":
2254
- return { title: `\u5B50\u4EFB\u52A1\uFF1A${s(i.description) || "(\u540E\u53F0\u4EE3\u7406)"}` };
2255
- case "TodoWrite":
2256
- return { title: "\u66F4\u65B0\u5F85\u529E\u6E05\u5355" };
2257
- default:
2258
- return { title: name };
2259
- }
2260
- }
2261
- function toolResultText(content) {
2262
- if (typeof content === "string") return content || void 0;
2263
- if (!Array.isArray(content)) return void 0;
2264
- const text = content.map((c) => c && c.type === "text" && typeof c.text === "string" ? c.text : "").filter(Boolean).join("\n");
2265
- return text || void 0;
2266
- }
2267
- var ClaudeEventMapper = class {
2268
- constructor(turnId) {
2269
- this.turnId = turnId;
2270
- }
2271
- turnId;
2272
- /** message id from the latest message_start — keys stream-delta itemIds */
2273
- currentMsgId = "msg";
2274
- /** Map one SDK message to zero-or-more normalized events (pure per message). */
2275
- map(msg) {
2276
- switch (msg.type) {
2277
- case "stream_event":
2278
- return this.mapStreamEvent(msg);
2279
- case "assistant":
2280
- return this.mapAssistant(msg);
2281
- case "user":
2282
- return this.mapUser(msg);
2283
- case "system":
2284
- return this.mapSystem(msg);
2285
- case "result":
2286
- return this.mapResult(msg);
2287
- default:
2288
- return [];
2289
- }
2290
- }
2291
- mapStreamEvent(msg) {
2292
- if (msg.parent_tool_use_id) return [];
2293
- const ev = msg.event;
2294
- switch (ev.type) {
2295
- case "message_start":
2296
- if (ev.message?.id) this.currentMsgId = ev.message.id;
2297
- return [];
2298
- case "content_block_delta": {
2299
- const itemId = `${this.currentMsgId}:${ev.index ?? 0}`;
2300
- if (ev.delta?.type === "text_delta" && ev.delta.text) {
2301
- return [{ type: "text_delta", itemId, delta: ev.delta.text }];
2302
- }
2303
- if (ev.delta?.type === "thinking_delta" && ev.delta.thinking) {
2304
- return [{ type: "thinking_delta", itemId, delta: ev.delta.thinking }];
2305
- }
2306
- return [];
2307
- }
2308
- default:
2309
- return [];
2310
- }
2311
- }
2312
- mapAssistant(msg) {
2313
- if (msg.parent_tool_use_id) return [];
2314
- const msgId = msg.message.id ?? this.currentMsgId;
2315
- const content = msg.message.content;
2316
- if (typeof content === "string") {
2317
- return content ? [{ type: "text", itemId: `${msgId}:0`, text: content }] : [];
2318
- }
2319
- if (!Array.isArray(content)) return [];
2320
- const out = [];
2321
- content.forEach((block, i) => {
2322
- switch (block.type) {
2323
- case "text": {
2324
- const text = block.text;
2325
- if (text) out.push({ type: "text", itemId: `${msgId}:${i}`, text });
2326
- break;
2327
- }
2328
- case "thinking": {
2329
- const text = block.thinking;
2330
- if (text) out.push({ type: "thinking", itemId: `${msgId}:${i}`, text });
2331
- break;
2332
- }
2333
- case "tool_use": {
2334
- const t = block;
2335
- const { title, detail } = toolTitle(t.name, t.input);
2336
- out.push({ type: "tool_use", itemId: t.id, title, detail });
2337
- break;
2338
- }
2339
- default:
2340
- break;
2341
- }
2342
- });
2343
- return out;
2344
- }
2345
- mapUser(msg) {
2346
- if (msg.parent_tool_use_id) return [];
2347
- const content = msg.message.content;
2348
- if (!Array.isArray(content)) return [];
2349
- const out = [];
2350
- for (const block of content) {
2351
- if (block.type !== "tool_result") continue;
2352
- const r = block;
2353
- out.push({
2354
- type: "tool_result",
2355
- itemId: r.tool_use_id,
2356
- output: toolResultText(r.content),
2357
- // run-state marks failure iff exitCode != null && !== 0; the API only
2358
- // gives a boolean, so map is_error → 1 (and success → undefined).
2359
- exitCode: r.is_error ? 1 : void 0
2360
- });
2361
- }
2362
- return out;
2363
- }
2364
- mapSystem(msg) {
2365
- if (msg.subtype === "init" && msg.session_id) {
2366
- return [{ type: "system", threadId: msg.session_id }];
2367
- }
2368
- if (msg.subtype === "api_retry") {
2369
- return [
2370
- {
2371
- type: "error",
2372
- message: `API \u77AC\u65AD\uFF0C\u91CD\u8BD5\u4E2D\uFF08${msg.attempt ?? "?"} / ${msg.max_retries ?? "?"}\uFF09`,
2373
- willRetry: true
2374
- }
2375
- ];
2376
- }
2377
- return [];
2378
- }
2379
- mapResult(msg) {
2380
- const u = msg.usage;
2381
- const usedTokens = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0);
2382
- const usage = { type: "usage", inputTokens: usedTokens, outputTokens: u?.output_tokens ?? 0 };
2383
- const out = [usage];
2384
- const window = maxContextWindow(msg.modelUsage);
2385
- if (window || usedTokens) out.push({ type: "context_usage", usedTokens, contextWindow: window });
2386
- if (msg.subtype === "success" && !msg.is_error) {
2387
- out.push({ type: "done", turnId: this.turnId });
2388
- return out;
2389
- }
2390
- const detail = msg.errors?.filter(Boolean).join("\uFF1B") || msg.result || msg.subtype;
2391
- out.push({ type: "error", message: `Claude \u8FD0\u884C\u5931\u8D25\uFF1A${detail}`, willRetry: false });
2392
- return out;
2393
- }
2394
- };
2395
- function maxContextWindow(modelUsage) {
2396
- if (!modelUsage) return null;
2397
- let max = 0;
2398
- for (const mu of Object.values(modelUsage)) {
2399
- if (typeof mu?.contextWindow === "number" && mu.contextWindow > max) max = mu.contextWindow;
2400
- }
2401
- return max > 0 ? max : null;
2402
- }
2403
-
2404
- // src/agent/claude-sdk/backend.ts
2405
- var SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
2406
- var STARTUP_PROBE_MS = 1500;
2407
- var CAPABILITIES = {
2408
- goal: false,
2409
- steer: false,
2410
- compact: true,
2411
- resume: false,
2412
- approvals: false
2413
- };
2414
- var COMPACT_TIMEOUT_MS2 = 9e4;
2415
- var STATIC_MODELS2 = [
2416
- {
2417
- id: "sonnet",
2418
- displayName: "Claude Sonnet",
2419
- description: "Claude Code \u9ED8\u8BA4\u6A21\u578B\uFF08\u522B\u540D\uFF0C\u7531 CLI \u89E3\u6790\u5230\u5F53\u524D\u6700\u65B0 Sonnet\uFF09",
2420
- hidden: false,
2421
- isDefault: true,
2422
- supportedEfforts: [],
2423
- defaultEffort: "medium"
2424
- },
2425
- {
2426
- id: "opus",
2427
- displayName: "Claude Opus",
2428
- description: "\u66F4\u5F3A\u63A8\u7406\uFF08\u8BA2\u9605\u7528\u91CF\u6D88\u8017\u66F4\u5FEB\uFF09",
2429
- hidden: false,
2430
- isDefault: false,
2431
- supportedEfforts: [],
2432
- defaultEffort: "medium"
2433
- }
2434
- ];
2435
- var AsyncQueue2 = class {
2436
- items = [];
2437
- waiters = [];
2438
- closed = false;
2439
- push(item) {
2440
- if (this.closed) return;
2441
- const w = this.waiters.shift();
2442
- if (w) w({ value: item, done: false });
2443
- else this.items.push(item);
2444
- }
2445
- close() {
2446
- this.closed = true;
2447
- while (this.waiters.length) this.waiters.shift()({ value: void 0, done: true });
2448
- }
2449
- async *[Symbol.asyncIterator]() {
2450
- while (true) {
2451
- if (this.items.length) {
2452
- yield this.items.shift();
2453
- continue;
2454
- }
2455
- if (this.closed) return;
2456
- const next = await new Promise((resolve8) => this.waiters.push(resolve8));
2457
- if (next.done) return;
2458
- yield next.value;
2459
- }
2460
- }
2461
- };
2462
- function notSupported(what) {
2463
- return new Error(`Claude \u540E\u7AEF\u6682\u4E0D\u652F\u6301${what}\uFF08codex \u4E13\u5C5E\u80FD\u529B\uFF0C\u5DF2\u6309\u80FD\u529B\u5B88\u536B\u62D2\u7EDD\uFF09`);
2464
- }
2465
- var ClaudeSdkThread = class {
2466
- constructor(opts, resumeSessionId) {
2467
- this.opts = opts;
2468
- this.sessionId = resumeSessionId ?? randomUUID2();
2469
- this.resuming = resumeSessionId !== void 0;
2470
- }
2471
- opts;
2472
- /** New thread: bridge-assigned session UUID, forced onto the CLI via
2473
- * `--session-id` so the handle exists BEFORE any turn runs (stream-json mode
2474
- * emits no init until the first input — see STARTUP_PROBE_MS). Resumed
2475
- * thread: the PRIOR session's id — the SDK `resume:` continues the SAME id
2476
- * (probed: no fork), so the persisted SessionRecord stays valid as-is. */
2477
- sessionId;
2478
- /** resume mode (connect passes `resume:` instead of pre-assigning the id) */
2479
- resuming;
2480
- input = new AsyncQueue2();
2481
- q;
2482
- iter;
2483
- /** the startup probe's in-flight read — the FIRST message of turn 1 arrives
2484
- * on this promise; nextMessage() consumes it exactly once. */
2485
- pendingNext;
2486
- child;
2487
- childExited = false;
2488
- closedByUs = false;
2489
- turnSeq = 0;
2490
- currentTurnId;
2491
- /** Turns that ended locally (⏹ / idle watchdog) WITHOUT consuming their
2492
- * terminal `result` leave it in the shared stream; the next turn must eat
2493
- * exactly that many stale results (and everything before them) or it would
2494
- * read a premature `done`. Mirrors the codex compact()-drain rationale. */
2495
- staleResults = 0;
2496
- /** Spawn the CLI (via query()) and probe that it came up. */
2497
- async connect() {
2498
- const { query } = await loadBackendDep(SDK_PACKAGE);
2499
- const options = {
2500
- cwd: this.opts.cwd,
2501
- ...this.opts.model ? { model: this.opts.model } : {},
2502
- // New thread: pre-assign the session id (CLI `--session-id`, probed
2503
- // accepted) so the AgentThread handle is real from t0 without waiting for
2504
- // any message. Resume: SDK-native `resume:` loads the prior conversation
2505
- // and CONTINUES the same session id (probed on-machine: id stable across
2506
- // resumes, context recalled — no forkSession, so nothing drifts).
2507
- ...this.resuming ? { resume: this.sessionId } : { extraArgs: { "session-id": this.sessionId } },
2508
- // TODO(权限映射): full 档专用 — see the module doc. qa/write were already
2509
- // rejected in startThread(); 'full' matches codex's danger-full-access.
2510
- permissionMode: "bypassPermissions",
2511
- allowDangerouslySkipPermissions: true,
2512
- // token 级增量(SDKPartialAssistantMessage)→ 飞书卡片打字机
2513
- includePartialMessages: true,
2514
- // Claude Code 本体行为(工具/代理循环)+ 桥的两条输出约定(与 codex 的
2515
- // developerInstructions 同一段文案,见 ../bridge-instructions)。
2516
- systemPrompt: { type: "preset", preset: "claude_code", append: BRIDGE_DEVELOPER_INSTRUCTIONS },
2517
- // 项目级配置(CLAUDE.md / .claude/settings.json)照常生效 —— 与 codex 读
2518
- // AGENTS.md 的行为对齐;用户/全局配置不读,避免把 owner 个人配置带进群聊。
2519
- settingSources: ["project"],
2520
- // 所有子进程统一走 cross-spawn 封装(Windows .cmd shim / EINVAL 修复),
2521
- // 这是仓库的硬约束(platform/spawn)。child 引用同时喂 isAlive()。
2522
- spawnClaudeCodeProcess: (spawnOpts) => {
2523
- const child = spawnProcess(spawnOpts.command, spawnOpts.args, {
2524
- cwd: spawnOpts.cwd,
2525
- env: spawnOpts.env,
2526
- stdio: ["pipe", "pipe", "pipe"],
2527
- // forwarded signal: fires AFTER the SDK's graceful stdin-EOF window
2528
- signal: spawnOpts.signal
2529
- });
2530
- this.child = child;
2531
- log.info("agent", "claude-spawn", { pid: child.pid ?? null, cwd: this.opts.cwd });
2532
- child.stderr?.on("data", (d) => {
2533
- const line = d.toString("utf8").trim();
2534
- if (line) log.warn("agent", "claude-stderr", { line: line.slice(0, 200) });
2535
- });
2536
- child.on("exit", (code, signal) => {
2537
- this.childExited = true;
2538
- log.info("agent", "claude-exit", { pid: child.pid ?? null, code, signal });
2539
- });
2540
- child.on("error", () => {
2541
- this.childExited = true;
2542
- });
2543
- return child;
2544
- }
2545
- };
2546
- this.q = query({ prompt: this.input[Symbol.asyncIterator](), options });
2547
- this.iter = this.q[Symbol.asyncIterator]();
2548
- const first = this.iter.next();
2549
- this.pendingNext = first;
2550
- const verdict = await Promise.race([
2551
- first.then(
2552
- (step) => step.done ? "exited" : step,
2553
- () => "failed"
2554
- ),
2555
- new Promise((resolve8) => {
2556
- const t = setTimeout(() => resolve8("silent"), STARTUP_PROBE_MS);
2557
- first.then(
2558
- () => clearTimeout(t),
2559
- () => clearTimeout(t)
2560
- );
2561
- })
2562
- ]);
2563
- if (verdict === "failed" || verdict === "exited") {
2564
- const reason = await first.then(
2565
- () => "Claude CLI \u542F\u52A8\u540E\u7ACB\u5373\u9000\u51FA",
2566
- (e) => e instanceof Error ? e.message : String(e)
2567
- );
2568
- await this.close().catch(() => void 0);
2569
- throw new Error(`Claude \u540E\u7AEF\u542F\u52A8\u5931\u8D25\uFF1A${reason}\uFF08\u8BF7\u68C0\u67E5 claude \u767B\u5F55\u6001 / ANTHROPIC_API_KEY\uFF09`);
2570
- }
2571
- if (verdict !== "silent") {
2572
- const m = verdict.value;
2573
- if (m.type === "result" && m.is_error) {
2574
- this.pendingNext = void 0;
2575
- await this.close().catch(() => void 0);
2576
- const what = m.result || m.subtype || "error result";
2577
- throw new Error(
2578
- `Claude \u540E\u7AEF\u542F\u52A8\u5931\u8D25\uFF1A${what}${this.resuming ? "\uFF08\u5F85\u6062\u590D\u7684\u4F1A\u8BDD\u53EF\u80FD\u5DF2\u4E0D\u5B58\u5728\uFF09" : ""}`
2579
- );
2580
- }
2581
- }
2582
- }
2583
- /** Read the next SDK message, consuming the startup probe's pending read first. */
2584
- nextMessage() {
2585
- const p = this.pendingNext ?? this.iter.next();
2586
- this.pendingNext = void 0;
2587
- return p;
2588
- }
2589
- runStreamed(input2, _turn) {
2590
- const self = this;
2591
- let lastActivityAt = Date.now();
2592
- const turnId = `claude-turn-${++this.turnSeq}-${randomUUID2().slice(0, 8)}`;
2593
- const mapper = new ClaudeEventMapper(turnId);
2594
- this.currentTurnId = turnId;
2595
- this.pushUser(input2);
2596
- async function* gen() {
2597
- yield { type: "turn_started", turnId };
2598
- let sawTerminal = false;
2599
- try {
2600
- while (true) {
2601
- let step;
2602
- try {
2603
- step = await self.nextMessage();
2604
- } catch (err) {
2605
- const m = err instanceof Error ? err.message : String(err);
2606
- yield { type: "error", message: `Claude \u8FDB\u7A0B\u5F02\u5E38\uFF1A${m}`, willRetry: false };
2607
- sawTerminal = true;
2608
- return;
2609
- }
2610
- if (step.done) {
2611
- yield { type: "error", message: "Claude \u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF08\u5D29\u6E83\u6216\u88AB\u5173\u95ED\uFF09", willRetry: false };
2612
- sawTerminal = true;
2613
- return;
2614
- }
2615
- lastActivityAt = Date.now();
2616
- const raw = step.value;
2617
- if (self.staleResults > 0) {
2618
- if (raw.type === "result") self.staleResults--;
2619
- continue;
2620
- }
2621
- for (const ev of mapper.map(raw)) {
2622
- yield ev;
2623
- if (ev.type === "done" || ev.type === "error" && !ev.willRetry) {
2624
- sawTerminal = true;
2625
- return;
2626
- }
2627
- }
2628
- }
2629
- } finally {
2630
- if (!sawTerminal && self.isAlive()) self.staleResults++;
2631
- }
2632
- }
2633
- return { events: gen(), turnId: () => self.currentTurnId, lastActivity: () => lastActivityAt };
2634
- }
2635
- pushUser(input2) {
2636
- let text = input2.text ?? "";
2637
- if (input2.images?.length) {
2638
- const lines = input2.images.map((p) => `- ${p}`).join("\n");
2639
- text = `${text}
2640
-
2641
- [\u7528\u6237\u968F\u6D88\u606F\u53D1\u6765 ${input2.images.length} \u5F20\u56FE\u7247\uFF0C\u5DF2\u4FDD\u5B58\u4E3A\u672C\u5730\u6587\u4EF6\uFF0C\u8BF7\u7528 Read \u5DE5\u5177\u67E5\u770B\uFF1A]
2642
- ${lines}`;
2643
- }
2644
- this.input.push({
2645
- type: "user",
2646
- message: { role: "user", content: text },
2647
- parent_tool_use_id: null,
2648
- session_id: this.sessionId
2649
- });
2650
- }
2651
- runGoal(_objective) {
2652
- throw notSupported(" /goal \u81EA\u6CBB\u76EE\u6807");
2653
- }
2654
- async clearGoal() {
2655
- }
2656
- async steer(_input, _expectedTurnId) {
2657
- throw notSupported("\u8FD0\u884C\u4E2D\u5F15\u5BFC\uFF08steer\uFF09\uFF0C\u6D88\u606F\u5C06\u6392\u961F\u4E3A\u4E0B\u4E00\u8F6E");
2658
- }
2659
- async abort(_turnId) {
2660
- await this.q.interrupt();
2661
- }
2662
- /**
2663
- * Manual /compact via the SDK. Idle-only (handle-message reserves the session
2664
- * before calling — no turn owns the shared iterator). Pushes the "/compact"
2665
- * slash command into the streaming input; the CLI runs compaction and emits
2666
- * (probed on-machine 2026-06, streaming-input mode):
2667
- * system{subtype:'status', status:'compacting'}
2668
- * system{subtype:'status', compact_result:'success'|'failed', compact_error?}
2669
- * system{subtype:'compact_boundary', compact_metadata:{post_tokens}} // success only
2670
- * …optional re-init/assistant…
2671
- * result{subtype:'success'} // turn terminal
2672
- * We read THROUGH the terminal `result` so the shared stream is left clean for
2673
- * the next turn (a leftover result would read as a premature `done`). The
2674
- * "Not enough messages to compact" failure is benign — context is already
2675
- * small — and maps to compacted:false (codex's nothing-to-compact parity), not
2676
- * an error card.
2677
- */
2678
- async compact() {
2679
- if (!this.isAlive()) throw new Error("Claude \u4F1A\u8BDD\u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF0C\u65E0\u6CD5\u538B\u7F29");
2680
- this.input.push({
2681
- type: "user",
2682
- message: { role: "user", content: "/compact" },
2683
- parent_tool_use_id: null,
2684
- session_id: this.sessionId
2685
- });
2686
- const deadline = Date.now() + COMPACT_TIMEOUT_MS2;
2687
- let compactedOk = false;
2688
- let failedErr;
2689
- let sawResult = false;
2690
- while (Date.now() < deadline && !sawResult) {
2691
- let step;
2692
- try {
2693
- step = await this.nextMessage();
2694
- } catch (err) {
2695
- throw new Error(`Claude \u538B\u7F29\u4E2D\u8FDB\u7A0B\u5F02\u5E38\uFF1A${err instanceof Error ? err.message : String(err)}`);
2696
- }
2697
- if (step.done) throw new Error("Claude \u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF08\u538B\u7F29\u672A\u5B8C\u6210\uFF09");
2698
- const m = step.value;
2699
- if (this.staleResults > 0 && m.type === "result") {
2700
- this.staleResults--;
2701
- continue;
2702
- }
2703
- if (m.type === "system" && m.subtype === "compact_boundary") {
2704
- compactedOk = true;
2705
- } else if (m.type === "system" && m.subtype === "status" && m.compact_result === "failed") {
2706
- failedErr = m.compact_error;
2707
- } else if (m.type === "result") {
2708
- sawResult = true;
2709
- }
2710
- }
2711
- if (!sawResult && !compactedOk) {
2712
- throw new Error(`Claude \u538B\u7F29\u8D85\u65F6\uFF08\u672A\u5728 ${COMPACT_TIMEOUT_MS2 / 1e3}s \u5185\u5B8C\u6210\uFF09`);
2713
- }
2714
- if (failedErr && !/not enough messages/i.test(failedErr)) throw new Error(`\u538B\u7F29\u5931\u8D25\uFF1A${failedErr}`);
2715
- return { compacted: compactedOk, usage: null };
2716
- }
2717
- isAlive() {
2718
- return !this.childExited && !this.closedByUs;
2719
- }
2720
- async close() {
2721
- this.closedByUs = true;
2722
- this.input.close();
2723
- try {
2724
- this.q.close();
2725
- } catch {
2726
- }
2727
- }
2728
- };
2729
- var ClaudeSdkBackend = class {
2730
- id = "claude-sdk";
2731
- displayName = "Claude Code (Agent SDK)";
2732
- capabilities = CAPABILITIES;
2733
- /** 权限映射未实现(见模块注释 TODO):仅「完全访问」档。声明给后端切换 UI 提前
2734
- * 拦截;硬守卫仍是 {@link assertFullMode}(startThread/resumeThread,fail-closed)。 */
2735
- supportedModes = ["full"];
2736
- async isAvailable() {
2737
- return (await this.doctor()).ok;
2738
- }
2739
- async doctor() {
2740
- try {
2741
- await loadBackendDep(SDK_PACKAGE);
2742
- return { ok: true, version: null, location: SDK_PACKAGE, depState: "installed" };
2743
- } catch (err) {
2744
- if (err instanceof BackendNotInstalledError) {
2745
- return {
2746
- ok: false,
2747
- version: null,
2748
- hint: "\u672A\u5B89\u88C5 Claude SDK \u2014\u2014 \u5728\u63A7\u5236\u53F0\u70B9\u300C\u4E0B\u8F7D\u300D\u5373\u6309\u9700\u88C5\u5230\u7528\u6237\u76EE\u5F55\uFF08\u7EA6 224M\uFF09",
2749
- installable: true,
2750
- depState: "not-installed"
2751
- };
2752
- }
2753
- const msg = err instanceof Error ? err.message : String(err);
2754
- return { ok: false, version: null, hint: `Claude SDK \u52A0\u8F7D\u5931\u8D25\uFF1A${msg}` };
2755
- }
2756
- }
2757
- async listModels() {
2758
- return STATIC_MODELS2;
2759
- }
2760
- async listThreads(_cwd, _limit) {
2761
- throw notSupported(" /resume \u5386\u53F2\u4F1A\u8BDD");
2762
- }
2763
- async readHistory(_cwd, _sessionId, _maxTurns) {
2764
- return { turns: [], totalTurns: 0 };
2765
- }
2766
- async startThread(opts) {
2767
- assertFullMode(opts);
2768
- const thread = new ClaudeSdkThread(sanitizeClaudeModel(opts));
2769
- await thread.connect();
2770
- return thread;
2771
- }
2772
- async resumeThread(opts) {
2773
- assertFullMode(opts);
2774
- const thread = new ClaudeSdkThread(sanitizeClaudeModel(opts), opts.sessionId);
2775
- await thread.connect();
2776
- return thread;
2777
- }
2778
- };
2779
- var KNOWN_MODEL_IDS = new Set(STATIC_MODELS2.map((m) => m.id));
2780
- function sanitizeClaudeModel(opts) {
2781
- if (!opts.model || KNOWN_MODEL_IDS.has(opts.model)) return opts;
2782
- log.warn("agent", "claude-model-ignored", { model: opts.model });
2783
- return { ...opts, model: void 0 };
2784
- }
2785
- function assertFullMode(opts) {
2786
- if ((opts.mode ?? "full") !== "full") {
2787
- throw new Error(
2788
- "Claude \u540E\u7AEF\u76EE\u524D\u4EC5\u652F\u6301\u300C\u5B8C\u5168\u8BBF\u95EE\u300D\u6743\u9650\u6863\uFF1Aqa/write \u6863\u5230 Claude \u6743\u9650\u6A21\u5F0F\u7684\u6620\u5C04\u5C1A\u672A\u5B9E\u73B0\uFF0C\u5DF2\u62D2\u7EDD\u542F\u52A8\uFF08\u7EDD\u4E0D\u9759\u9ED8\u964D\u7EA7\uFF09\u3002\u8BF7\u628A\u9879\u76EE\u6743\u9650\u6863\u5207\u56DE\u300C\u5B8C\u5168\u8BBF\u95EE\u300D\u6216\u6539\u7528 codex \u540E\u7AEF\u3002"
2789
- );
2790
- }
2791
- }
2792
-
2793
- // src/agent/acp/backend.ts
2794
- import { randomUUID as randomUUID3 } from "crypto";
2795
- import { existsSync as existsSync4 } from "fs";
2796
- import { Readable, Writable } from "stream";
2797
-
2798
- // src/agent/native-helpers.ts
2799
- import { chmod as chmod4, readdir as readdir2 } from "fs/promises";
2800
- import { join as join7 } from "path";
2801
- async function fixNativeHelperPerms(rootDir = paths.backendsDir) {
2802
- const nodePtyDirs = [
2803
- join7(rootDir, "node_modules", "node-pty"),
2804
- join7(rootDir, "node_modules", "claude-pty-acp", "node_modules", "node-pty")
2805
- ];
2806
- let fixed = 0;
2807
- for (const npDir of nodePtyDirs) {
2808
- const prebuilds = join7(npDir, "prebuilds");
2809
- let plats;
2810
- try {
2811
- plats = await readdir2(prebuilds, { withFileTypes: true });
2812
- } catch {
2813
- continue;
2814
- }
2815
- for (const p of plats) {
2816
- if (!p.isDirectory()) continue;
2817
- const helper = join7(prebuilds, p.name, "spawn-helper");
2818
- try {
2819
- await chmod4(helper, 493);
2820
- fixed++;
2821
- } catch {
2822
- }
2823
- }
2824
- }
2825
- if (fixed > 0) log.info("agent", "native-helper-perms-fixed", { count: fixed });
2826
- }
2827
-
2828
- // src/agent/acp/event-map.ts
2829
- function toolContentText(content) {
2830
- if (!Array.isArray(content)) return void 0;
2831
- const parts = [];
2832
- for (const c of content) {
2833
- if (c.type === "content" && c.content?.type === "text" && typeof c.content.text === "string") {
2834
- if (c.content.text) parts.push(c.content.text);
2835
- } else if (c.type === "diff" && typeof c.path === "string") {
2836
- parts.push(`[diff] ${c.path}`);
2837
- }
2838
- }
2839
- const text = parts.join("\n");
2840
- return text || void 0;
2841
- }
2842
- function renderPlan(entries) {
2843
- return entries.map((e) => {
2844
- const mark = e.status === "completed" ? "\u2705" : e.status === "in_progress" ? "\u25B6\uFE0F" : "\u2B1C";
2845
- return `${mark} ${e.content}`;
2846
- }).join("\n");
2847
- }
2848
- var AcpEventMapper = class {
2849
- constructor(turnId) {
2850
- this.turnId = turnId;
2851
- }
2852
- turnId;
2853
- textSeq = 0;
2854
- textOpen = false;
2855
- textMsgId;
2856
- thinkingSeq = 0;
2857
- thinkingOpen = false;
2858
- planStarted = false;
2859
- /** Map one session/update to zero-or-more normalized events. */
2860
- map(update) {
2861
- switch (update.sessionUpdate) {
2862
- case "agent_message_chunk":
2863
- return this.mapText(update);
2864
- case "agent_thought_chunk":
2865
- return this.mapThinking(update);
2866
- case "tool_call":
2867
- return this.mapToolCall(update);
2868
- case "tool_call_update":
2869
- return this.mapToolCallUpdate(update);
2870
- case "plan":
2871
- return this.mapPlan(update);
2872
- case "usage_update": {
2873
- const u = update;
2874
- return [{ type: "context_usage", usedTokens: u.used, contextWindow: u.size ?? null }];
2875
- }
2876
- default:
2877
- return [];
2878
- }
2879
- }
2880
- /** Map the prompt response's stopReason (+optional usage) to the terminal events. */
2881
- finish(stopReason, usage) {
2882
- const out = [];
2883
- if (usage && (usage.inputTokens !== void 0 || usage.outputTokens !== void 0)) {
2884
- out.push({
2885
- type: "usage",
2886
- // cache 读写也是模型真实消费的上下文 —— 与 claude-sdk 的口径一致。
2887
- inputTokens: (usage.inputTokens ?? 0) + (usage.cachedReadTokens ?? 0) + (usage.cachedWriteTokens ?? 0),
2888
- outputTokens: usage.outputTokens ?? 0
2889
- });
2890
- }
2891
- switch (stopReason) {
2892
- case "refusal":
2893
- out.push({ type: "error", message: "ACP \u540E\u7AEF\u62D2\u7EDD\u7EE7\u7EED\u672C\u8F6E\uFF08refusal\uFF09", willRetry: false });
2894
- break;
2895
- case "max_tokens":
2896
- case "max_turn_requests":
2897
- out.push({ type: "error", message: `ACP \u540E\u7AEF\u56E0\u4E0A\u9650\u63D0\u524D\u505C\u6B62\uFF08${stopReason}\uFF09\uFF0C\u56DE\u590D\u53EF\u80FD\u4E0D\u5B8C\u6574`, willRetry: false });
2898
- break;
2899
- default:
2900
- out.push({ type: "done", turnId: this.turnId });
2901
- break;
2902
- }
2903
- return out;
2904
- }
2905
- mapText(chunk) {
2906
- if (chunk.content.type !== "text" || !chunk.content.text) return [];
2907
- const msgId = chunk.messageId ?? void 0;
2908
- if (!this.textOpen || msgId !== void 0 && msgId !== this.textMsgId) {
2909
- this.textSeq++;
2910
- this.textOpen = true;
2911
- this.textMsgId = msgId;
2912
- }
2913
- return [{ type: "text_delta", itemId: `${this.turnId}:text:${this.textSeq}`, delta: chunk.content.text }];
2914
- }
2915
- mapThinking(chunk) {
2916
- if (chunk.content.type !== "text" || !chunk.content.text) return [];
2917
- if (!this.thinkingOpen) {
2918
- this.thinkingSeq++;
2919
- this.thinkingOpen = true;
2920
- }
2921
- return [
2922
- { type: "thinking_delta", itemId: `${this.turnId}:thinking:${this.thinkingSeq}`, delta: chunk.content.text }
2923
- ];
2924
- }
2925
- mapToolCall(call) {
2926
- this.textOpen = false;
2927
- this.thinkingOpen = false;
2928
- const out = [
2929
- { type: "tool_use", itemId: call.toolCallId, title: call.title || call.kind || "\u5DE5\u5177\u8C03\u7528" }
2930
- ];
2931
- if (call.status === "completed" || call.status === "failed") {
2932
- out.push({
2933
- type: "tool_result",
2934
- itemId: call.toolCallId,
2935
- output: toolContentText(call.content),
2936
- exitCode: call.status === "failed" ? 1 : void 0
2937
- });
2938
- }
2939
- return out;
2940
- }
2941
- mapToolCallUpdate(update) {
2942
- if (update.status !== "completed" && update.status !== "failed") return [];
2943
- return [
2944
- {
2945
- type: "tool_result",
2946
- itemId: update.toolCallId,
2947
- output: toolContentText(update.content),
2948
- exitCode: update.status === "failed" ? 1 : void 0
2949
- }
2950
- ];
2951
- }
2952
- mapPlan(plan) {
2953
- if (!plan.entries.length) return [];
2954
- const itemId = `${this.turnId}:plan`;
2955
- const out = [];
2956
- if (!this.planStarted) {
2957
- this.planStarted = true;
2958
- this.textOpen = false;
2959
- this.thinkingOpen = false;
2960
- out.push({ type: "tool_use", itemId, title: "\u4EFB\u52A1\u8BA1\u5212" });
2961
- }
2962
- out.push({ type: "tool_result", itemId, output: renderPlan(plan.entries) });
2963
- return out;
2964
- }
2965
- };
2966
-
2967
- // src/agent/acp/backend.ts
2968
- var ACP_SERVER_BIN = "claude-pty-acp";
2969
- var INITIALIZE_TIMEOUT_MS = 1e4;
2970
- var SESSION_TIMEOUT_MS = 9e4;
2971
- var DOCTOR_TIMEOUT_MS = 5e3;
2972
- var IS_WIN2 = process.platform === "win32";
2973
- var CAPABILITIES2 = {
2974
- goal: false,
2975
- steer: false,
2976
- compact: false,
2977
- resume: false,
2978
- approvals: false
2979
- };
2980
- var STATIC_MODELS3 = [
2981
- {
2982
- id: "claude-acp-default",
2983
- displayName: "Claude Code\uFF08\u8BA2\u9605\uFF09",
2984
- description: "\u6A21\u578B\u7531 claude-pty-acp \u80CC\u540E\u7684\u672C\u673A Claude Code \u914D\u7F6E\u51B3\u5B9A\uFF08ACP \u4E0D\u652F\u6301\u5207\u6362\u6A21\u578B\uFF09",
2985
- hidden: false,
2986
- isDefault: true,
2987
- supportedEfforts: [],
2988
- defaultEffort: "medium"
2989
- }
2990
- ];
2991
- var AsyncQueue3 = class {
2992
- items = [];
2993
- waiters = [];
2994
- closed = false;
2995
- push(item) {
2996
- if (this.closed) return;
2997
- const w = this.waiters.shift();
2998
- if (w) w({ value: item, done: false });
2999
- else this.items.push(item);
3000
- }
3001
- close() {
3002
- this.closed = true;
3003
- while (this.waiters.length) this.waiters.shift()({ value: void 0, done: true });
3004
- }
3005
- async *[Symbol.asyncIterator]() {
3006
- while (true) {
3007
- if (this.items.length) {
3008
- yield this.items.shift();
3009
- continue;
3010
- }
3011
- if (this.closed) return;
3012
- const next = await new Promise((resolve8) => this.waiters.push(resolve8));
3013
- if (next.done) return;
3014
- yield next.value;
3015
- }
3016
- }
3017
- };
3018
- function notSupported2(what) {
3019
- return new Error(`ACP \u540E\u7AEF\u6682\u4E0D\u652F\u6301${what}\uFF08codex \u4E13\u5C5E\u80FD\u529B\uFF0C\u5DF2\u6309\u80FD\u529B\u5B88\u536B\u62D2\u7EDD\uFF09`);
3020
- }
3021
- function extractDataDetails(data) {
3022
- if (data == null) return void 0;
3023
- if (typeof data === "string") return data || void 0;
3024
- if (typeof data === "object") {
3025
- const d = data;
3026
- if (typeof d.details === "string" && d.details) return d.details;
3027
- if (typeof d.message === "string" && d.message) return d.message;
3028
- try {
3029
- const s = JSON.stringify(data);
3030
- return s && s !== "{}" ? s : void 0;
3031
- } catch {
3032
- return void 0;
3033
- }
3034
- }
3035
- return void 0;
3036
- }
3037
- function errMsg(err) {
3038
- if (err && typeof err === "object") {
3039
- const e = err;
3040
- const base = typeof e.message === "string" && e.message ? e.message : err instanceof Error ? err.message : String(err);
3041
- const details = extractDataDetails(e.data);
3042
- const codeStr = typeof e.code === "number" ? `code ${e.code}` : void 0;
3043
- const extra = [codeStr, details && !base.includes(details) ? details : void 0].filter(Boolean).join("\uFF1B");
3044
- return extra ? `${base}\uFF08${extra}\uFF09` : base;
3045
- }
3046
- return err instanceof Error ? err.message : String(err);
3047
- }
3048
- function startupHint(raw) {
3049
- if (/posix_spawnp failed/i.test(raw)) {
3050
- return "\n\u53EF\u80FD\u539F\u56E0\uFF1Anode-pty \u7684 spawn-helper \u4E22\u4E86\u53EF\u6267\u884C\u4F4D\uFF08\u5DF2\u5728\u5B89\u88C5/\u542F\u52A8\u65F6\u81EA\u52A8 chmod \u4FEE\u590D\u2014\u2014\u82E5\u4ECD\u62A5\u9519\uFF0C\u91CD\u65B0\u4E0B\u8F7D claude-acp \u540E\u7AEF\uFF09\uFF0C\u6216\u672C\u673A\u627E\u4E0D\u5230\u53EF\u6267\u884C\u7684 claude\u3002";
3051
- }
3052
- if (/\bENOENT\b/i.test(raw)) {
3053
- return "\n\u627E\u4E0D\u5230\u53EF\u6267\u884C\u6587\u4EF6\uFF1A\u786E\u8BA4 claude \u5DF2\u5B89\u88C5\u4E14\u5728 PATH \u4E0A\u3002\u82E5\u4F60\u7684 `claude` \u662F shell \u51FD\u6570/\u522B\u540D\uFF08\u5B50\u8FDB\u7A0B\u770B\u4E0D\u5230\uFF09\uFF0C\u628A\u73AF\u5883\u53D8\u91CF CC_CLAUDE_BIN \u6307\u5411\u771F\u5B9E claude \u4E8C\u8FDB\u5236\uFF0C\u6216\u5728\u9879\u76EE\u504F\u597D\u91CC\u8BBE acpCommand\u3002";
3054
- }
3055
- return "";
3056
- }
3057
- var nativeHelperFixOnce;
3058
- function withTimeout(p, ms, what) {
3059
- return new Promise((resolve8, reject) => {
3060
- const t = setTimeout(() => reject(new Error(`${what} \u8D85\u65F6\uFF08${Math.round(ms / 1e3)}s\uFF09`)), ms);
3061
- p.then(
3062
- (v) => {
3063
- clearTimeout(t);
3064
- resolve8(v);
3065
- },
3066
- (e) => {
3067
- clearTimeout(t);
3068
- reject(e);
3069
- }
3070
- );
3071
- });
3072
- }
3073
- var pathBinCache = null;
3074
- async function resolveAcpCommand(opts) {
3075
- const cfg = await loadConfig().catch(() => ({}));
3076
- const override = getAcpCommand(cfg);
3077
- if (override) return override;
3078
- const onDemand = backendsBinPath(ACP_SERVER_BIN);
3079
- if (onDemand) return { command: onDemand, args: [] };
3080
- if (!opts?.force && pathBinCache && existsSync4(pathBinCache)) {
3081
- return { command: pathBinCache, args: [] };
3082
- }
3083
- pathBinCache = await whichAsync(ACP_SERVER_BIN);
3084
- return pathBinCache ? { command: pathBinCache, args: [] } : null;
3085
- }
3086
- function whichAsync(cmd) {
3087
- return new Promise((resolve8) => {
3088
- let child;
3089
- try {
3090
- child = spawnProcess(IS_WIN2 ? "where" : "which", [cmd], { stdio: ["ignore", "pipe", "ignore"] });
3091
- } catch {
3092
- resolve8(null);
3093
- return;
3094
- }
3095
- let stdout = "";
3096
- child.stdout?.setEncoding("utf8");
3097
- child.stdout?.on("data", (d) => {
3098
- stdout += d;
3099
- });
3100
- child.on("error", () => resolve8(null));
3101
- child.on("close", (code) => {
3102
- if (code !== 0) {
3103
- resolve8(null);
3104
- return;
3105
- }
3106
- const first = stdout.split("\n").map((l) => l.trim()).find(Boolean);
3107
- resolve8(first && existsSync4(first) ? first : null);
3108
- });
3109
- });
3110
- }
3111
- var AcpThread = class {
3112
- constructor(opts, server, resumeSessionId) {
3113
- this.opts = opts;
3114
- this.server = server;
3115
- this.resumeSessionId = resumeSessionId;
3116
- this.instructionsSent = resumeSessionId !== void 0;
3117
- }
3118
- opts;
3119
- server;
3120
- /** server 在 session/new 时分配(resume 则是既有 id),connect() 后才有值。 */
3121
- _sessionId = "";
3122
- resumeSessionId;
3123
- conn;
3124
- child;
3125
- childExited = false;
3126
- closedByUs = false;
3127
- active;
3128
- turnSeq = 0;
3129
- currentTurnId;
3130
- lastActivityAt = Date.now();
3131
- /** 桥接输出约定只随首条用户消息发一次(ACP 没有 developer/system 注入通道);
3132
- * resume 的会话在它新建的那次已经发过。 */
3133
- instructionsSent;
3134
- get sessionId() {
3135
- return this._sessionId;
3136
- }
3137
- /** Spawn the ACP server, handshake, and create/load the session. */
3138
- async connect() {
3139
- const sdk = await import("@agentclientprotocol/sdk");
3140
- await (nativeHelperFixOnce ??= fixNativeHelperPerms().catch(() => void 0));
3141
- const child = spawnProcess(this.server.command, this.server.args, {
3142
- stdio: ["pipe", "pipe", "pipe"],
3143
- // POSIX:自成进程组,close 时整组杀。claude-pty-acp 的 bin 用 `npx tsx` 起
3144
- // (npx→tsx→node→claude 一棵树),npm/npx 不转发信号给孙子——只 kill child
3145
- // 外壳会把 node/claude 留成孤儿(实测过满地 claude-pty-acp 孤儿树)。
3146
- detached: !IS_WIN2
3147
- });
3148
- this.child = child;
3149
- log.info("agent", "acp-spawn", {
3150
- pid: child.pid ?? null,
3151
- command: this.server.command,
3152
- cwd: this.opts.cwd
3153
- });
3154
- child.stderr?.on("data", (d) => {
3155
- const line = d.toString("utf8").trim();
3156
- if (line) log.warn("agent", "acp-stderr", { line: line.slice(0, 2e3) });
3157
- });
3158
- child.on("exit", (code, signal) => {
3159
- this.childExited = true;
3160
- log.info("agent", "acp-exit", { pid: child.pid ?? null, code, signal });
3161
- this.failActive(`ACP server \u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF08code=${code ?? "-"} signal=${signal ?? "-"}\uFF09`);
3162
- });
3163
- child.on("error", (err) => {
3164
- this.childExited = true;
3165
- this.failActive(`ACP server \u542F\u52A8\u5931\u8D25\uFF1A${errMsg(err)}`);
3166
- });
3167
- if (!child.stdin || !child.stdout) {
3168
- await this.close().catch(() => void 0);
3169
- throw new Error("ACP server \u5B50\u8FDB\u7A0B\u7F3A\u5C11 stdio \u7BA1\u9053");
3170
- }
3171
- const stream2 = sdk.ndJsonStream(
3172
- Writable.toWeb(child.stdin),
3173
- Readable.toWeb(child.stdout)
3174
- );
3175
- this.conn = new sdk.ClientSideConnection(() => this.clientHandler(), stream2);
3176
- try {
3177
- const init = await withTimeout(
3178
- this.conn.initialize({
3179
- protocolVersion: sdk.PROTOCOL_VERSION,
3180
- clientInfo: { name: "feishu-codex-bridge", version: bridgeVersion() },
3181
- // fs/terminal 全 false:server 不得反向读写 bridge 侧文件系统/起终端
3182
- // (协议保证:能力 false 时 agent MUST NOT 调对应方法)。
3183
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
3184
- }),
3185
- INITIALIZE_TIMEOUT_MS,
3186
- "ACP initialize"
3187
- );
3188
- if (this.resumeSessionId !== void 0) {
3189
- if (init.agentCapabilities?.loadSession !== true) {
3190
- throw new Error("\u8BE5 ACP server \u4E0D\u652F\u6301\u4F1A\u8BDD\u6062\u590D\uFF08loadSession \u80FD\u529B\u672A\u5BA3\u544A\uFF09");
3191
- }
3192
- await withTimeout(
3193
- this.conn.loadSession({ sessionId: this.resumeSessionId, cwd: this.opts.cwd, mcpServers: [] }),
3194
- SESSION_TIMEOUT_MS,
3195
- "ACP session/load"
3196
- );
3197
- this._sessionId = this.resumeSessionId;
3198
- } else {
3199
- const res = await withTimeout(
3200
- this.conn.newSession({ cwd: this.opts.cwd, mcpServers: [] }),
3201
- SESSION_TIMEOUT_MS,
3202
- "ACP session/new"
3203
- );
3204
- this._sessionId = res.sessionId;
3205
- }
3206
- } catch (err) {
3207
- await this.close().catch(() => void 0);
3208
- const detail = errMsg(err);
3209
- const resumeNote = this.resumeSessionId !== void 0 ? "\uFF08\u5F85\u6062\u590D\u7684\u4F1A\u8BDD\u53EF\u80FD\u5DF2\u4E0D\u5B58\u5728\uFF09" : "";
3210
- throw new Error(`ACP \u540E\u7AEF\u542F\u52A8\u5931\u8D25\uFF1A${detail}${resumeNote}${startupHint(detail)}`);
3211
- }
3212
- }
3213
- /** The ACP Client the server calls back into(必选两方法;fs/terminal 不实现)。 */
3214
- clientHandler() {
3215
- return {
3216
- sessionUpdate: async (n) => this.onSessionUpdate(n),
3217
- requestPermission: async (req) => this.onRequestPermission(req)
3218
- };
3219
- }
3220
- onSessionUpdate(n) {
3221
- this.lastActivityAt = Date.now();
3222
- const a = this.active;
3223
- if (!a || a.settled) return;
3224
- for (const ev of a.mapper.map(n.update)) a.queue.push(ev);
3225
- }
3226
- onRequestPermission(req) {
3227
- const a = this.active;
3228
- const title = req.toolCall?.title || req.toolCall?.toolCallId || "(unknown tool)";
3229
- if (!a || a.settled || a.aborted) {
3230
- return { outcome: { outcome: "cancelled" } };
3231
- }
3232
- const allow = req.options.find((o) => o.kind === "allow_once") ?? req.options.find((o) => o.kind?.startsWith("allow"));
3233
- if (allow) {
3234
- log.warn("agent", "acp-auto-approve", { note: `\u26A0\uFE0F ACP \u540E\u7AEF\u81EA\u52A8\u6279\u51C6\u4E86 ${title}`, optionId: allow.optionId });
3235
- return { outcome: { outcome: "selected", optionId: allow.optionId } };
3236
- }
3237
- const reject = req.options.find((o) => o.kind?.startsWith("reject"));
3238
- log.warn("agent", "acp-auto-reject", { note: `ACP \u6743\u9650\u8BF7\u6C42\u65E0 allow \u9009\u9879\uFF0C\u5DF2\u62D2\u7EDD\uFF1A${title}` });
3239
- if (reject) return { outcome: { outcome: "selected", optionId: reject.optionId } };
3240
- return { outcome: { outcome: "cancelled" } };
3241
- }
3242
- runStreamed(input2, _turn) {
3243
- const turnId = `acp-turn-${++this.turnSeq}-${randomUUID3().slice(0, 8)}`;
3244
- const mapper = new AcpEventMapper(turnId);
3245
- const queue = new AsyncQueue3();
3246
- const active = { turnId, queue, mapper, aborted: false, settled: false };
3247
- this.active = active;
3248
- this.currentTurnId = turnId;
3249
- this.lastActivityAt = Date.now();
3250
- this.conn.prompt({ sessionId: this._sessionId, prompt: [{ type: "text", text: this.buildPromptText(input2) }] }).then(
3251
- (res) => this.settle(active, mapper.finish(res.stopReason, res.usage ?? void 0)),
3252
- (err) => this.settle(active, [{ type: "error", message: `ACP \u540E\u7AEF\u8FD0\u884C\u5931\u8D25\uFF1A${errMsg(err)}`, willRetry: false }])
3253
- );
3254
- async function* gen() {
3255
- yield { type: "turn_started", turnId };
3256
- for await (const ev of queue) yield ev;
3257
- }
3258
- return { events: gen(), turnId: () => this.currentTurnId, lastActivity: () => this.lastActivityAt };
3259
- }
3260
- /** Settle a turn exactly once: flush terminal events, close the stream. */
3261
- settle(active, events) {
3262
- if (active.settled) return;
3263
- active.settled = true;
3264
- this.lastActivityAt = Date.now();
3265
- for (const ev of events) active.queue.push(ev);
3266
- active.queue.close();
3267
- if (this.active === active) this.active = void 0;
3268
- }
3269
- /** server 进程死亡/启动失败时把在飞 turn 立即收尾(turn 永不悬挂)。 */
3270
- failActive(message) {
3271
- const a = this.active;
3272
- if (a) this.settle(a, [{ type: "error", message, willRetry: false }]);
3273
- }
3274
- buildPromptText(input2) {
3275
- let text = input2.text ?? "";
3276
- if (input2.images?.length) {
3277
- const lines = input2.images.map((p) => `- ${p}`).join("\n");
3278
- text = `${text}
3279
-
3280
- [\u7528\u6237\u968F\u6D88\u606F\u53D1\u6765 ${input2.images.length} \u5F20\u56FE\u7247\uFF0C\u5DF2\u4FDD\u5B58\u4E3A\u672C\u5730\u6587\u4EF6\uFF0C\u8BF7\u7528 Read \u5DE5\u5177\u67E5\u770B\uFF1A]
3281
- ${lines}`;
3282
- }
3283
- if (!this.instructionsSent) {
3284
- this.instructionsSent = true;
3285
- text = `[\u98DE\u4E66\u6865\u7CFB\u7EDF\u7EA6\u5B9A\uFF08\u975E\u7528\u6237\u6D88\u606F\uFF09]
3286
- ${BRIDGE_DEVELOPER_INSTRUCTIONS}
3287
- [/\u98DE\u4E66\u6865\u7CFB\u7EDF\u7EA6\u5B9A]
3288
-
3289
- ${text}`;
3290
- }
3291
- return text;
3292
- }
3293
- runGoal(_objective) {
3294
- throw notSupported2(" /goal \u81EA\u6CBB\u76EE\u6807");
3295
- }
3296
- async clearGoal() {
3297
- }
3298
- async steer(_input, _expectedTurnId) {
3299
- throw notSupported2("\u8FD0\u884C\u4E2D\u5F15\u5BFC\uFF08steer\uFF09\uFF0C\u6D88\u606F\u5C06\u6392\u961F\u4E3A\u4E0B\u4E00\u8F6E");
3300
- }
3301
- async abort(turnId) {
3302
- const a = this.active;
3303
- if (a && a.turnId === turnId) a.aborted = true;
3304
- try {
3305
- await this.conn.cancel({ sessionId: this._sessionId });
3306
- } catch {
3307
- }
3308
- }
3309
- async compact() {
3310
- throw notSupported2(" /compact \u624B\u52A8\u538B\u7F29");
3311
- }
3312
- isAlive() {
3313
- return !this.childExited && !this.closedByUs;
3314
- }
3315
- async close() {
3316
- this.closedByUs = true;
3317
- try {
3318
- this.child?.stdin?.end();
3319
- } catch {
3320
- }
3321
- await killProcessGroup(this.child?.pid, () => this.childExited);
3322
- }
3323
- };
3324
- var AcpBackend = class {
3325
- /** `serverCommand`:undefined → 自动解析(配置覆盖 → PATH);显式对象 → 直接
3326
- * 使用(单测 mock server / probe 脚本注入);显式 null → 视为未找到(单测)。 */
3327
- constructor(serverCommand) {
3328
- this.serverCommand = serverCommand;
3329
- }
3330
- serverCommand;
3331
- id = "claude-acp";
3332
- displayName = "Claude\uFF08\u8BA2\u9605\xB7ACP\uFF09";
3333
- capabilities = CAPABILITIES2;
3334
- /** 仅「完全访问」档:bridge 隔着 ACP 无法保证对端的只读/写界约束(qa/write 的
3335
- * 读限制 codex 靠内核沙箱兜底,ACP server 给不了等价保证)。声明给切换 UI 提前
3336
- * 拦截;硬守卫是 {@link assertFullMode}(startThread/resumeThread,fail-closed)。 */
3337
- supportedModes = ["full"];
3338
- /** doctor 握手探活缓存(成功才缓存;force 重探)。key = 完整命令行。 */
3339
- doctorCache = /* @__PURE__ */ new Map();
3340
- async resolveServer(force) {
3341
- if (this.serverCommand !== void 0) return this.serverCommand;
3342
- return resolveAcpCommand({ force });
3343
- }
3344
- async isAvailable() {
3345
- return (await this.doctor()).ok;
3346
- }
3347
- async doctor(opts) {
3348
- const server = await this.resolveServer(opts?.force).catch(() => null);
3349
- if (!server) {
1918
+ const all = (Array.isArray(thread?.turns) ? thread.turns : []).map(mapTurn).filter((t) => t.userText || t.assistantText || t.tools.length);
1919
+ const totalTurns = all.length;
1920
+ const turns = totalTurns > maxTurns ? all.slice(totalTurns - maxTurns) : all;
3350
1921
  return {
3351
- ok: false,
3352
- version: null,
3353
- hint: '\u672A\u68C0\u6D4B\u5230 claude-pty-acp\uFF1Anpm i -g claude-pty-acp\uFF0C\u6216\u5728\u914D\u7F6E preferences.acpCommand \u6307\u5B9A\u542F\u52A8\u547D\u4EE4\uFF08\u5982 {"command":"node","args":["/path/claude-pty-acp/dist/index.js"]}\uFF09'
1922
+ turns,
1923
+ totalTurns,
1924
+ name: thread?.name ?? void 0,
1925
+ preview: thread?.preview ?? void 0,
1926
+ createdAt: thread?.createdAt,
1927
+ updatedAt: thread?.updatedAt
3354
1928
  };
1929
+ } catch (err) {
1930
+ log.fail("agent", err, { phase: "thread/read", sessionId });
1931
+ return empty;
3355
1932
  }
3356
- const key = [server.command, ...server.args].join(" ");
3357
- if (!opts?.force) {
3358
- const hit = this.doctorCache.get(key);
3359
- if (hit) return hit;
3360
- }
3361
- const probe = await handshakeProbe(server, key);
3362
- if (probe.ok) this.doctorCache.set(key, probe);
3363
- return probe;
3364
- }
3365
- async listModels() {
3366
- return STATIC_MODELS3;
3367
- }
3368
- async listThreads(_cwd, _limit) {
3369
- throw notSupported2(" /resume \u5386\u53F2\u4F1A\u8BDD");
3370
- }
3371
- async readHistory(_cwd, _sessionId, _maxTurns) {
3372
- return { turns: [], totalTurns: 0 };
3373
1933
  }
3374
1934
  async startThread(opts) {
3375
- assertFullMode2(opts);
3376
- const server = await this.resolveServer();
3377
- if (!server) throw new Error(noServerError());
3378
- const thread = new AcpThread(opts, server);
3379
- await thread.connect();
3380
- return thread;
1935
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1936
+ const client = await this.spawn(opts.cwd);
1937
+ const res = await client.request("thread/start", {
1938
+ cwd: opts.cwd,
1939
+ approvalPolicy: APPROVAL_POLICY,
1940
+ ...sandbox,
1941
+ developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1942
+ ...opts.model ? { model: opts.model } : {}
1943
+ });
1944
+ return new CodexThread(client, res.thread.id, opts.model, opts.effort);
3381
1945
  }
3382
1946
  async resumeThread(opts) {
3383
- assertFullMode2(opts);
3384
- const server = await this.resolveServer();
3385
- if (!server) throw new Error(noServerError());
3386
- const thread = new AcpThread(opts, server, opts.sessionId);
3387
- await thread.connect();
3388
- return thread;
1947
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1948
+ const client = await this.spawn(opts.cwd);
1949
+ const res = await client.request("thread/resume", {
1950
+ threadId: opts.sessionId,
1951
+ cwd: opts.cwd,
1952
+ approvalPolicy: APPROVAL_POLICY,
1953
+ ...sandbox,
1954
+ developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1955
+ ...opts.model ? { model: opts.model } : {}
1956
+ });
1957
+ return new CodexThread(client, res.thread.id, opts.model, opts.effort);
1958
+ }
1959
+ async spawn(cwd) {
1960
+ const bin = resolveCodexBin();
1961
+ if (!bin) throw new Error("codex CLI not found (set CODEX_BIN or install @openai/codex)");
1962
+ const warmed = takeWarmClient(bin);
1963
+ void refillWarmPool();
1964
+ if (warmed) return warmed;
1965
+ const client = new AppServerClient({ bin, cwd });
1966
+ await client.connect();
1967
+ return client;
3389
1968
  }
3390
1969
  };
3391
- function noServerError() {
3392
- return "\u672A\u68C0\u6D4B\u5230 claude-pty-acp\uFF08ACP server\uFF09\u3002\u8BF7 npm i -g claude-pty-acp\uFF0C\u6216\u5728\u914D\u7F6E preferences.acpCommand \u6307\u5B9A\u542F\u52A8\u547D\u4EE4\u3002";
1970
+ function isBoilerplateUserText(text) {
1971
+ const t = text.trimStart();
1972
+ return t.startsWith("<environment_context>") || t.startsWith("# AGENTS.md instructions");
3393
1973
  }
3394
- async function handshakeProbe(server, location) {
3395
- let child;
3396
- try {
3397
- const sdk = await import("@agentclientprotocol/sdk");
3398
- child = spawnProcess(server.command, server.args, { stdio: ["pipe", "pipe", "ignore"], detached: !IS_WIN2 });
3399
- child.on("error", () => void 0);
3400
- if (!child.stdin || !child.stdout) throw new Error("\u5B50\u8FDB\u7A0B\u7F3A\u5C11 stdio \u7BA1\u9053");
3401
- const stream2 = sdk.ndJsonStream(
3402
- Writable.toWeb(child.stdin),
3403
- Readable.toWeb(child.stdout)
3404
- );
3405
- const conn = new sdk.ClientSideConnection(
3406
- () => ({
3407
- sessionUpdate: async () => void 0,
3408
- requestPermission: async () => ({ outcome: { outcome: "cancelled" } })
3409
- }),
3410
- stream2
3411
- );
3412
- const init = await withTimeout(
3413
- conn.initialize({
3414
- protocolVersion: sdk.PROTOCOL_VERSION,
3415
- clientInfo: { name: "feishu-codex-bridge", version: bridgeVersion() },
3416
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
3417
- }),
3418
- DOCTOR_TIMEOUT_MS,
3419
- "ACP initialize"
3420
- );
3421
- return { ok: true, version: init.agentInfo?.version ?? null, location };
3422
- } catch (err) {
3423
- return {
3424
- ok: false,
3425
- version: null,
3426
- location,
3427
- hint: `\u547D\u4EE4\u5DF2\u627E\u5230\u4F46 ACP \u63E1\u624B\u5931\u8D25\uFF1A${errMsg(err)}\uFF08\u786E\u8BA4\u5B83\u662F\u4E00\u4E2A ACP server\uFF1B\u5FC5\u8981\u65F6\u5728\u914D\u7F6E preferences.acpCommand \u4FEE\u6B63\uFF09`
3428
- };
3429
- } finally {
3430
- void killProcessGroup(child?.pid, () => child?.exitCode != null, { graceMs: 2e3 });
1974
+ function mapTurn(turn) {
1975
+ const userParts = [];
1976
+ const assistantParts = [];
1977
+ const reasoningParts = [];
1978
+ const tools = [];
1979
+ for (const item of turn.items ?? []) {
1980
+ switch (item.type) {
1981
+ case "userMessage": {
1982
+ const text = item.content.map((c) => c.type === "text" ? c.text : c.type === "mention" ? `@${c.name}` : "").join("").trim();
1983
+ if (text && !isBoilerplateUserText(text)) userParts.push(text);
1984
+ break;
1985
+ }
1986
+ case "agentMessage":
1987
+ if (item.text.trim()) assistantParts.push(item.text);
1988
+ break;
1989
+ case "reasoning": {
1990
+ const r = (item.content.length ? item.content : item.summary).join("\n").trim();
1991
+ if (r) reasoningParts.push(r);
1992
+ break;
1993
+ }
1994
+ case "commandExecution":
1995
+ tools.push({
1996
+ title: item.command,
1997
+ output: item.aggregatedOutput ?? void 0,
1998
+ exitCode: item.exitCode,
1999
+ failed: item.status === "failed" || item.status === "declined" || (item.exitCode ?? 0) !== 0
2000
+ });
2001
+ break;
2002
+ case "fileChange":
2003
+ tools.push({ title: "\u7F16\u8F91\u6587\u4EF6", failed: item.status === "failed" || item.status === "declined" });
2004
+ break;
2005
+ case "webSearch":
2006
+ tools.push({ title: `\u8054\u7F51\u641C\u7D22\uFF1A${item.query}` });
2007
+ break;
2008
+ case "mcpToolCall":
2009
+ tools.push({ title: `${item.server} / ${item.tool}`, failed: item.status === "failed" || Boolean(item.error) });
2010
+ break;
2011
+ case "dynamicToolCall":
2012
+ tools.push({ title: item.tool, failed: item.status === "failed" || item.success === false });
2013
+ break;
2014
+ // plan / contextCompaction / review-mode / image* — omitted from the digest
2015
+ default:
2016
+ break;
2017
+ }
3431
2018
  }
2019
+ return {
2020
+ userText: userParts.join("\n\n"),
2021
+ assistantText: assistantParts.join("\n\n"),
2022
+ reasoning: reasoningParts.join("\n\n"),
2023
+ tools,
2024
+ startedAt: turn.startedAt ?? void 0
2025
+ };
3432
2026
  }
3433
- function assertFullMode2(opts) {
3434
- if ((opts.mode ?? "full") !== "full") {
3435
- throw new Error(
3436
- "ACP \u540E\u7AEF\u76EE\u524D\u4EC5\u652F\u6301\u300C\u5B8C\u5168\u8BBF\u95EE\u300D\u6743\u9650\u6863\uFF1Abridge \u65E0\u6CD5\u4FDD\u8BC1 ACP server \u4FA7\u7684\u53EA\u8BFB\u7EA6\u675F\uFF0C\u5DF2\u62D2\u7EDD\u542F\u52A8\uFF08\u7EDD\u4E0D\u9759\u9ED8\u964D\u7EA7\uFF09\u3002\u8BF7\u628A\u9879\u76EE\u6743\u9650\u6863\u5207\u56DE\u300C\u5B8C\u5168\u8BBF\u95EE\u300D\u6216\u6539\u7528 codex \u540E\u7AEF\u3002"
3437
- );
3438
- }
2027
+ function mapModel(m) {
2028
+ return {
2029
+ id: m.id,
2030
+ displayName: m.displayName ?? m.id,
2031
+ description: m.description ?? "",
2032
+ hidden: m.hidden ?? false,
2033
+ isDefault: m.isDefault ?? false,
2034
+ supportedEfforts: (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort),
2035
+ defaultEffort: m.defaultReasoningEffort ?? "medium"
2036
+ };
3439
2037
  }
2038
+ var STATIC_MODELS = [
2039
+ {
2040
+ id: "gpt-5.5",
2041
+ displayName: "GPT-5.5",
2042
+ description: "\u9ED8\u8BA4\u6A21\u578B",
2043
+ hidden: false,
2044
+ isDefault: true,
2045
+ supportedEfforts: ["low", "medium", "high"],
2046
+ defaultEffort: "medium"
2047
+ }
2048
+ ];
3440
2049
 
3441
2050
  // src/agent/detect.ts
3442
2051
  async function probeCodexAgent() {
@@ -3462,50 +2071,13 @@ async function probeCodexAgent() {
3462
2071
  ]
3463
2072
  };
3464
2073
  }
3465
- async function probeClaudeAgent() {
3466
- const sdkEntry = BACKEND_CATALOG.find((e) => e.id === "claude-sdk");
3467
- const acpEntry = BACKEND_CATALOG.find((e) => e.id === "claude-acp");
3468
- const sdkInstalled = sdkEntry.dep.pkg ? isBackendDepInstalled(sdkEntry.dep.pkg) : false;
3469
- const acpCmd = await resolveAcpCommand({ force: true }).catch(() => null);
3470
- const acpInstalled = !!acpCmd;
3471
- const installed = sdkInstalled || acpInstalled;
3472
- return {
3473
- id: "claude",
3474
- displayName: "Claude",
3475
- installed,
3476
- version: null,
3477
- installHint: installed ? void 0 : sdkEntry.dep.detectHint,
3478
- backends: [
3479
- {
3480
- backendId: "claude-sdk",
3481
- available: sdkInstalled,
3482
- reason: sdkInstalled ? void 0 : sdkEntry.dep.detectHint,
3483
- version: null,
3484
- supportedModes: sdkEntry.supportedModes,
3485
- installable: !sdkInstalled
3486
- // npm-ondemand 未装 → 可一键下载
3487
- },
3488
- {
3489
- backendId: "claude-acp",
3490
- available: acpInstalled,
3491
- reason: acpInstalled ? void 0 : acpEntry.dep.detectHint,
3492
- version: null,
3493
- supportedModes: acpEntry.supportedModes,
3494
- installable: !acpInstalled
3495
- // npm-ondemand(bin 类)未装 → 可一键下载
3496
- }
3497
- ]
3498
- };
3499
- }
3500
2074
  async function detectAgents() {
3501
- return Promise.all([probeCodexAgent(), probeClaudeAgent()]);
2075
+ return Promise.all([probeCodexAgent()]);
3502
2076
  }
3503
2077
  function pickDefaultBackend(agents) {
3504
2078
  const find = (id) => agents.flatMap((a) => a.backends).find((b) => b.backendId === id);
3505
2079
  const pickable = (id) => !catalogById(id)?.hidden && find(id)?.available;
3506
2080
  if (pickable("codex-appserver")) return "codex-appserver";
3507
- if (pickable("claude-sdk")) return "claude-sdk";
3508
- if (pickable("claude-acp")) return "claude-acp";
3509
2081
  return DEFAULT_BACKEND_ID;
3510
2082
  }
3511
2083
  var defaultCache;
@@ -3519,10 +2091,105 @@ async function effectiveDefaultBackend(opts) {
3519
2091
  return defaultCache;
3520
2092
  }
3521
2093
 
2094
+ // src/agent/backend-loader.ts
2095
+ import { createRequire } from "module";
2096
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2097
+ import { join as join6 } from "path";
2098
+ import { pathToFileURL } from "url";
2099
+ function userAnchor() {
2100
+ return join6(paths.backendsDir, "__backend_anchor__.cjs");
2101
+ }
2102
+ function isBackendDepInstalled(pkg) {
2103
+ try {
2104
+ createRequire(import.meta.url).resolve(pkg);
2105
+ return true;
2106
+ } catch {
2107
+ }
2108
+ try {
2109
+ createRequire(userAnchor()).resolve(pkg);
2110
+ return true;
2111
+ } catch {
2112
+ return false;
2113
+ }
2114
+ }
2115
+ function backendsBinPath(binName) {
2116
+ const dir = join6(paths.backendsDir, "node_modules", ".bin");
2117
+ const candidates = process.platform === "win32" ? [join6(dir, `${binName}.cmd`), join6(dir, binName)] : [join6(dir, binName)];
2118
+ return candidates.find((p) => existsSync3(p)) ?? null;
2119
+ }
2120
+ function isBackendBinInstalled(binName) {
2121
+ return backendsBinPath(binName) !== null;
2122
+ }
2123
+ function isBackendEntryInstalled(entry) {
2124
+ const { kind, binName, pkg } = entry.dep;
2125
+ if (kind === "external-cli") return false;
2126
+ if (binName) return isBackendBinInstalled(binName);
2127
+ return pkg ? isBackendDepInstalled(pkg) : false;
2128
+ }
2129
+ function isBackendInstalledInUserDir(entry) {
2130
+ const { binName, pkg } = entry.dep;
2131
+ if (binName) return isBackendBinInstalled(binName);
2132
+ if (!pkg) return false;
2133
+ try {
2134
+ createRequire(userAnchor()).resolve(pkg);
2135
+ return true;
2136
+ } catch {
2137
+ return false;
2138
+ }
2139
+ }
2140
+ function installedBackendVersion(pkg) {
2141
+ const readVer = (file) => {
2142
+ try {
2143
+ const j = JSON.parse(readFileSync2(file, "utf8"));
2144
+ return typeof j.version === "string" ? j.version : null;
2145
+ } catch {
2146
+ return null;
2147
+ }
2148
+ };
2149
+ const inUser = join6(paths.backendsDir, "node_modules", ...pkg.split("/"), "package.json");
2150
+ const v1 = readVer(inUser);
2151
+ if (v1) return v1;
2152
+ try {
2153
+ const resolved = createRequire(import.meta.url).resolve(`${pkg}/package.json`);
2154
+ return readVer(resolved);
2155
+ } catch {
2156
+ return null;
2157
+ }
2158
+ }
2159
+
3522
2160
  // src/agent/installer.ts
3523
2161
  import { mkdir as mkdir4, rm as rm2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
3524
- import { existsSync as existsSync5 } from "fs";
2162
+ import { existsSync as existsSync4 } from "fs";
3525
2163
  import { join as join8 } from "path";
2164
+
2165
+ // src/agent/native-helpers.ts
2166
+ import { chmod as chmod4, readdir as readdir2 } from "fs/promises";
2167
+ import { join as join7 } from "path";
2168
+ async function fixNativeHelperPerms(rootDir = paths.backendsDir) {
2169
+ const nodePtyDirs = [join7(rootDir, "node_modules", "node-pty")];
2170
+ let fixed = 0;
2171
+ for (const npDir of nodePtyDirs) {
2172
+ const prebuilds = join7(npDir, "prebuilds");
2173
+ let plats;
2174
+ try {
2175
+ plats = await readdir2(prebuilds, { withFileTypes: true });
2176
+ } catch {
2177
+ continue;
2178
+ }
2179
+ for (const p of plats) {
2180
+ if (!p.isDirectory()) continue;
2181
+ const helper = join7(prebuilds, p.name, "spawn-helper");
2182
+ try {
2183
+ await chmod4(helper, 493);
2184
+ fixed++;
2185
+ } catch {
2186
+ }
2187
+ }
2188
+ }
2189
+ if (fixed > 0) log.info("agent", "native-helper-perms-fixed", { count: fixed });
2190
+ }
2191
+
2192
+ // src/agent/installer.ts
3526
2193
  var NPM = "npm";
3527
2194
  async function latestNpmVersion(pkg, timeoutMs = 8e3) {
3528
2195
  const bare = stripVersion(pkg);
@@ -3567,7 +2234,7 @@ var BACKENDS_PACKAGE_JSON = JSON.stringify(
3567
2234
  async function ensureBackendsDir() {
3568
2235
  await mkdir4(paths.backendsDir, { recursive: true });
3569
2236
  const pkgFile = join8(paths.backendsDir, "package.json");
3570
- if (!existsSync5(pkgFile)) {
2237
+ if (!existsSync4(pkgFile)) {
3571
2238
  await writeFile4(pkgFile, `${BACKENDS_PACKAGE_JSON}
3572
2239
  `, "utf8");
3573
2240
  }
@@ -3692,14 +2359,7 @@ function stripVersion(pkg) {
3692
2359
 
3693
2360
  // src/agent/index.ts
3694
2361
  var REGISTRY = /* @__PURE__ */ new Map([
3695
- ["codex-appserver", () => new CodexAppServerBackend()],
3696
- // Claude Code via the official Agent SDK — minimal slice (capability-guarded:
3697
- // goal/steer/compact/resume off). The SDK itself loads lazily inside the
3698
- // backend, so registering it here costs nothing for codex-only deployments.
3699
- ["claude-sdk", () => new ClaudeSdkBackend()],
3700
- // Claude Code via ACP(spawn claude-pty-acp,订阅计费路径)。ACP SDK 同样在
3701
- // backend 内懒加载;server 命令解析见 acp/backend(配置覆盖 → PATH → doctor 提示)。
3702
- ["claude-acp", () => new AcpBackend()]
2362
+ ["codex-appserver", () => new CodexAppServerBackend()]
3703
2363
  ]);
3704
2364
  function backendIds() {
3705
2365
  return catalogBackendIds();
@@ -3727,7 +2387,7 @@ async function runDoctor() {
3727
2387
  }
3728
2388
  const codexAuth = join9(process.env.CODEX_HOME ?? join9(homedir2(), ".codex"), "auth.json");
3729
2389
  checks.push(
3730
- existsSync6(codexAuth) ? { name: "codex \u767B\u5F55", ok: true, detail: codexAuth } : { name: "codex \u767B\u5F55", ok: false, detail: "\u672A\u767B\u5F55\uFF0C\u8FD0\u884C `codex login`" }
2390
+ existsSync5(codexAuth) ? { name: "codex \u767B\u5F55", ok: true, detail: codexAuth } : { name: "codex \u767B\u5F55", ok: false, detail: "\u672A\u767B\u5F55\uFF0C\u8FD0\u884C `codex login`" }
3731
2391
  );
3732
2392
  const larkVer = tryExec("lark-cli", ["--version"]);
3733
2393
  checks.push(
@@ -3736,7 +2396,7 @@ async function runDoctor() {
3736
2396
  const reg = await ensureRegistry();
3737
2397
  const cur = currentBot(reg);
3738
2398
  if (cur) useBotDir(cur.appId);
3739
- if (cur && existsSync6(paths.configFile)) {
2399
+ if (cur && existsSync5(paths.configFile)) {
3740
2400
  checks.push({
3741
2401
  name: "bridge \u914D\u7F6E",
3742
2402
  ok: true,
@@ -3751,7 +2411,7 @@ async function runDoctor() {
3751
2411
  detail: "\u672A\u914D\u7F6E\uFF0C\u8FD0\u884C `feishu-codex-bridge run`\uFF08\u6216 `bot init`\uFF09\u626B\u7801\u521B\u5EFA"
3752
2412
  });
3753
2413
  }
3754
- if (cur && existsSync6(paths.configFile)) {
2414
+ if (cur && existsSync5(paths.configFile)) {
3755
2415
  checks.push(await checkEventSubscription());
3756
2416
  }
3757
2417
  console.log("\n\u{1FA7A} feishu-codex-bridge \u81EA\u68C0\n");
@@ -3964,10 +2624,8 @@ async function ensureAnyAgent() {
3964
2624
  const rule = "-".repeat(64);
3965
2625
  console.error(`
3966
2626
  ${rule}`);
3967
- console.error("\u26A0\uFE0F \u672A\u68C0\u6D4B\u5230\u4EFB\u4F55\u53EF\u7528 agent \u540E\u7AEF\uFF08codex / claude \u90FD\u6CA1\u5C31\u7EEA\uFF09\u2014\u2014\u4ECD\u4F1A\u542F\u52A8\uFF0C\u4F46\u7FA4\u91CC\u53D1\u6D88\u606F\u4F1A\u62A5\u540E\u7AEF\u4E0D\u53EF\u7528\u3002");
3968
- console.error(" \u9009\u4E00\u4E2A\u88C5\u4E0A\uFF1A");
3969
- console.error(" \u2022 codex\uFF08\u80FD\u529B\u6700\u5168\uFF09\uFF1Anpm i -g @openai/codex\uFF0C\u7136\u540E codex login");
3970
- console.error(" \u2022 Claude SDK\uFF08\u5F00\u7BB1\u5373\u7528\uFF0C\u7EA6 224M\uFF09\uFF1A\u542F\u52A8\u540E\u5728 Web \u63A7\u5236\u53F0\u70B9\u300C\u4E0B\u8F7D Claude SDK\u300D");
2627
+ console.error("\u26A0\uFE0F \u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 codex \u540E\u7AEF\u2014\u2014\u4ECD\u4F1A\u542F\u52A8\uFF0C\u4F46\u7FA4\u91CC\u53D1\u6D88\u606F\u4F1A\u62A5\u540E\u7AEF\u4E0D\u53EF\u7528\u3002");
2628
+ console.error(" \u88C5\u4E0A codex\uFF1Anpm i -g @openai/codex\uFF0C\u7136\u540E codex login");
3971
2629
  console.error(" \u88C5\u597D\u540E\u7528 `feishu-codex-bridge doctor` \u81EA\u68C0\u3002");
3972
2630
  console.error(`${rule}
3973
2631
  `);
@@ -4172,7 +2830,7 @@ import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
4172
2830
 
4173
2831
  // src/project/registry.ts
4174
2832
  import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile5 } from "fs/promises";
4175
- import { randomUUID as randomUUID4 } from "crypto";
2833
+ import { randomUUID as randomUUID2 } from "crypto";
4176
2834
  import { dirname as dirname5 } from "path";
4177
2835
  function defaultNoMention(p) {
4178
2836
  return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
@@ -4217,7 +2875,7 @@ function withLock(fn) {
4217
2875
  }
4218
2876
  async function write(projects) {
4219
2877
  await mkdir5(dirname5(paths.projectsFile), { recursive: true });
4220
- const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID4()}`;
2878
+ const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID2()}`;
4221
2879
  const body = { version: FILE_VERSION2, projects };
4222
2880
  await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
4223
2881
  `, "utf8");
@@ -4481,7 +3139,7 @@ function buildModelCard(state) {
4481
3139
  }
4482
3140
  } else {
4483
3141
  elements.push(hr(), md(`\u5F53\u524D\u6A21\u578B\uFF1A**${curLabel}**`));
4484
- elements.push(note("\u8BE5\u540E\u7AEF\u4E0D\u652F\u6301\u5728\u6B64\u5207\u6362\u6A21\u578B\u6216\u63A8\u7406\u5F3A\u5EA6 \u2014\u2014 \u7531\u8BA2\u9605\u7248 Claude Code \u81EA\u8EAB\u914D\u7F6E\u51B3\u5B9A\u3002"));
3142
+ elements.push(note("\u8BE5\u540E\u7AEF\u4E0D\u652F\u6301\u5728\u6B64\u5207\u6362\u6A21\u578B\u6216\u63A8\u7406\u5F3A\u5EA6\u3002"));
4485
3143
  }
4486
3144
  if (state.note) elements.push(note(state.note));
4487
3145
  return card(elements, { summary: "\u6A21\u578B\u8BBE\u7F6E" });
@@ -4964,9 +3622,7 @@ function buildNewProjectFormCard(opts = {}) {
4964
3622
  selectMenu({ name: "backend", placeholder: "\u9009\u62E9\u540E\u7AEF Agent", options: backends, initial: backends[0]?.value })
4965
3623
  );
4966
3624
  } else if (backends.length === 1) {
4967
- formItems.push(
4968
- note(`\u{1F9E0} \u540E\u7AEF Agent\uFF1A**${backends[0].label}**\uFF08\u521B\u5EFA\u540E\u56FA\u5B9A\u3002\u4E0B\u8F7D\u66F4\u591A\u540E\u7AEF\u5230 Web\u300C\u540E\u7AEF Agent\u300D\u9875\u5373\u53EF\u5728\u6B64\u9009\u62E9\uFF09`)
4969
- );
3625
+ formItems.push(note(`\u{1F9E0} \u540E\u7AEF Agent\uFF1A**${backends[0].label}**\uFF08\u521B\u5EFA\u540E\u56FA\u5B9A\uFF09`));
4970
3626
  }
4971
3627
  formItems.push(
4972
3628
  note("\u9009\u7FA4\u7C7B\u578B(\u76F4\u63A5\u70B9\u5BF9\u5E94\u6309\u94AE\u521B\u5EFA)\uFF1A\u{1F465} \u591A\u8BDD\u9898\u7FA4 = @\u6211\u5F00\u8BDD\u9898\u3001\u6BCF\u8BDD\u9898\u72EC\u7ACB\u4F1A\u8BDD\uFF1B\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4 = \u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\u3001\u8FDE\u7EED\u4E0A\u4E0B\u6587\u3002"),
@@ -4992,9 +3648,7 @@ function buildJoinGroupFormCard(opts) {
4992
3648
  ];
4993
3649
  if (backends.length > 1) {
4994
3650
  formItems.push(
4995
- note(
4996
- "\u{1F9E0} \u540E\u7AEF Agent\uFF08\u7ED1\u5B9A\u540E**\u56FA\u5B9A\u4E0D\u53EF\u5207\u6362**\uFF09\u3002\u9ED8\u8BA4 **Codex** \u4EE5\u300C\u53EA\u8BFB\u300D\u6863\u7ED1\u5B9A\uFF08\u5916\u90E8\u7FA4\u5B89\u5168\uFF09\uFF1B\u9009 **Claude** \u7CFB\u5219\u4EE5\u300C\u5B8C\u5168\u8BBF\u95EE\u300D\u6863\u7ED1\u5B9A\uFF08Claude \u6682\u4E0D\u652F\u6301\u53EA\u8BFB\u6863\uFF09\u2014\u2014 \u7ED9\u81EA\u5DF1\u7684\u5185\u90E8\u7FA4\u7528 Claude \u65F6\u76F4\u63A5\u9009\u5373\u53EF\u3002"
4997
- ),
3651
+ note("\u{1F9E0} \u540E\u7AEF Agent\uFF08\u7ED1\u5B9A\u540E**\u56FA\u5B9A\u4E0D\u53EF\u5207\u6362**\uFF09\u3002\u9ED8\u8BA4 **Codex** \u4EE5\u300C\u53EA\u8BFB\u300D\u6863\u7ED1\u5B9A\uFF08\u5916\u90E8\u7FA4\u5B89\u5168\uFF09\u3002"),
4998
3652
  selectMenu({ name: "backend", placeholder: "\u9009\u62E9\u540E\u7AEF Agent", options: backends, initial: backends[0]?.value })
4999
3653
  );
5000
3654
  } else if (backends.length === 1) {
@@ -6904,13 +5558,13 @@ async function uploadBuffer(channel, buffer) {
6904
5558
  }
6905
5559
 
6906
5560
  // src/service/update.ts
6907
- import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
5561
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
6908
5562
  import { dirname as dirname9, join as join14, resolve as resolve5 } from "path";
6909
5563
  import { fileURLToPath as fileURLToPath4 } from "url";
6910
5564
 
6911
5565
  // src/service/launchd.ts
6912
5566
  import { spawn as spawn2, spawnSync } from "child_process";
6913
- import { existsSync as existsSync7 } from "fs";
5567
+ import { existsSync as existsSync6 } from "fs";
6914
5568
  import { mkdir as mkdir7, rm as rm3, writeFile as writeFile6 } from "fs/promises";
6915
5569
  import { homedir as homedir3, userInfo as userInfo2 } from "os";
6916
5570
  import { dirname as dirname7, join as join11, resolve as resolve4 } from "path";
@@ -7055,7 +5709,7 @@ async function uninstallLaunchd() {
7055
5709
  }
7056
5710
  }
7057
5711
  async function restartLaunchd() {
7058
- if (!existsSync7(launchAgentPlistPath())) {
5712
+ if (!existsSync6(launchAgentPlistPath())) {
7059
5713
  throw new Error(`launchd service \u672A\u5B89\u88C5\uFF1A${launchAgentPlistPath()}`);
7060
5714
  }
7061
5715
  if (isLoaded()) {
@@ -7073,7 +5727,7 @@ function statusLaunchd() {
7073
5727
  const parsed = parseLaunchdStatus(raw);
7074
5728
  return {
7075
5729
  platformName: "launchd (macOS)",
7076
- installed: existsSync7(launchAgentPlistPath()),
5730
+ installed: existsSync6(launchAgentPlistPath()),
7077
5731
  running: result.ok,
7078
5732
  servicePath: launchAgentPlistPath(),
7079
5733
  stdoutPath: serviceStdoutPath(),
@@ -7278,7 +5932,7 @@ function killService() {
7278
5932
 
7279
5933
  // src/service/systemd.ts
7280
5934
  import { spawnSync as spawnSync3 } from "child_process";
7281
- import { existsSync as existsSync8 } from "fs";
5935
+ import { existsSync as existsSync7 } from "fs";
7282
5936
  import { mkdir as mkdir9, rm as rm4, writeFile as writeFile8 } from "fs/promises";
7283
5937
  import { homedir as homedir4 } from "os";
7284
5938
  import { dirname as dirname8, join as join13 } from "path";
@@ -7382,7 +6036,7 @@ function statusSystemd() {
7382
6036
  };
7383
6037
  }
7384
6038
  function unitExists() {
7385
- return existsSync8(systemdUnitPath());
6039
+ return existsSync7(systemdUnitPath());
7386
6040
  }
7387
6041
  function systemdActive() {
7388
6042
  const r = spawnSync3("systemctl", ["--user", "is-active", SYSTEMD_UNIT_NAME], {
@@ -7457,7 +6111,7 @@ function packageName() {
7457
6111
  return pkgJson().name ?? "@modelzen/feishu-codex-bridge";
7458
6112
  }
7459
6113
  function isDevSource() {
7460
- return existsSync9(join14(pkgRoot(), ".git"));
6114
+ return existsSync8(join14(pkgRoot(), ".git"));
7461
6115
  }
7462
6116
  function isNewer(a, b) {
7463
6117
  const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -8099,7 +6753,7 @@ function buildUsageShareCard(data, opts = {}) {
8099
6753
 
8100
6754
  // src/project/lifecycle.ts
8101
6755
  import { mkdir as mkdir10 } from "fs/promises";
8102
- import { existsSync as existsSync10 } from "fs";
6756
+ import { existsSync as existsSync9 } from "fs";
8103
6757
  import { isAbsolute as isAbsolute2, join as join16, resolve as resolve6 } from "path";
8104
6758
 
8105
6759
  // src/project/git-info.ts
@@ -8277,7 +6931,7 @@ function assertBackendUsable(backend, mode) {
8277
6931
  async function resolveCwd(name, existingPath) {
8278
6932
  if (existingPath) {
8279
6933
  const cwd2 = isAbsolute2(existingPath) ? existingPath : resolve6(existingPath);
8280
- if (!existsSync10(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
6934
+ if (!existsSync9(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
8281
6935
  return { cwd: cwd2, blank: false };
8282
6936
  }
8283
6937
  const cwd = join16(paths.projectsRootDir, name);
@@ -8365,7 +7019,7 @@ async function leaveChat(channel, chatId) {
8365
7019
 
8366
7020
  // src/bot/session-store.ts
8367
7021
  import { mkdir as mkdir11, readFile as readFile10, rename as rename5, writeFile as writeFile9 } from "fs/promises";
8368
- import { randomUUID as randomUUID5 } from "crypto";
7022
+ import { randomUUID as randomUUID3 } from "crypto";
8369
7023
  import { dirname as dirname10 } from "path";
8370
7024
  var FILE_VERSION3 = 2;
8371
7025
  var LEGACY_V1_SESSION_FIELD = "codexThreadId";
@@ -8403,7 +7057,7 @@ function withLock2(fn) {
8403
7057
  }
8404
7058
  async function write2(sessions) {
8405
7059
  await mkdir11(dirname10(paths.sessionsFile), { recursive: true });
8406
- const tmp = `${paths.sessionsFile}.tmp-${process.pid}-${randomUUID5()}`;
7060
+ const tmp = `${paths.sessionsFile}.tmp-${process.pid}-${randomUUID3()}`;
8407
7061
  const body = { version: FILE_VERSION3, sessions };
8408
7062
  await writeFile9(tmp, `${JSON.stringify(body, null, 2)}
8409
7063
  `, "utf8");
@@ -9331,7 +7985,7 @@ function pickIdleSessions(keys, touchedAt, isBusy, idleMs, now) {
9331
7985
  function createOrchestrator(channel, cfg, fallbackCwd) {
9332
7986
  const backends = /* @__PURE__ */ new Map();
9333
7987
  function backendFor(id) {
9334
- const key = id ?? DEFAULT_BACKEND_ID;
7988
+ const key = id && catalogById(id) ? id : DEFAULT_BACKEND_ID;
9335
7989
  let be = backends.get(key);
9336
7990
  if (!be) {
9337
7991
  be = createBackend(key);
@@ -9345,7 +7999,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
9345
7999
  async function denyGoal(msg, inThread) {
9346
8000
  await channel.send(
9347
8001
  msg.chatId,
9348
- { markdown: "\u5F53\u524D\u540E\u7AEF\u4E0D\u652F\u6301 `/goal` \u81EA\u6CBB\u76EE\u6807\uFF08codex \u4E13\u5C5E\u80FD\u529B\uFF09\u3002\u76F4\u63A5 @\u6211\u63CF\u8FF0\u4EFB\u52A1\u5373\u53EF \u2014\u2014 Claude \u6BCF\u8F6E\u672C\u5C31\u4F1A\u81EA\u4E3B\u591A\u6B65\u9AA4\u8DD1\u5230\u5B8C\u6210\u3002" },
8002
+ { markdown: "\u5F53\u524D\u540E\u7AEF\u4E0D\u652F\u6301 `/goal` \u81EA\u6CBB\u76EE\u6807\u3002\u76F4\u63A5 @\u6211\u63CF\u8FF0\u4EFB\u52A1\u5373\u53EF\u3002" },
9349
8003
  { replyTo: msg.messageId, replyInThread: inThread }
9350
8004
  ).catch(() => void 0);
9351
8005
  }
@@ -12200,7 +10854,7 @@ function isAlive(pid) {
12200
10854
  }
12201
10855
 
12202
10856
  // src/web/discovery.ts
12203
- import { randomUUID as randomUUID6 } from "crypto";
10857
+ import { randomUUID as randomUUID4 } from "crypto";
12204
10858
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
12205
10859
  import { dirname as dirname12 } from "path";
12206
10860
  function publishWebConsole(rec, file = paths.webConsoleFile) {
@@ -12237,7 +10891,7 @@ function stableWebConsoleToken(file = paths.webTokenFile) {
12237
10891
  if (t) return t;
12238
10892
  } catch {
12239
10893
  }
12240
- const token = randomUUID6();
10894
+ const token = randomUUID4();
12241
10895
  try {
12242
10896
  mkdirSync2(dirname12(file), { recursive: true });
12243
10897
  try {
@@ -12372,7 +11026,7 @@ async function doRestart(phase) {
12372
11026
 
12373
11027
  // src/web/server.ts
12374
11028
  import { createServer } from "http";
12375
- import { randomUUID as randomUUID7, timingSafeEqual } from "crypto";
11029
+ import { randomUUID as randomUUID5, timingSafeEqual } from "crypto";
12376
11030
  import { mkdirSync as mkdirSync3, watch } from "fs";
12377
11031
  import { open as open2, stat as stat5 } from "fs/promises";
12378
11032
  import { join as join19 } from "path";
@@ -13501,7 +12155,7 @@ ${UI_PURE_JS}
13501
12155
  title.appendChild(document.createTextNode('Codex '));
13502
12156
  title.appendChild(el('span', 'em', 'Bridge'));
13503
12157
  hero.appendChild(title);
13504
- hero.appendChild(el('div', 'home-sub', '\u628A\u98DE\u4E66\u63A5\u5230\u672C\u673A\u7684 Codex / Claude \u2014\u2014 \u4E00\u4E2A\u7FA4\u4E00\u4E2A\u9879\u76EE\uFF0C@\u4E00\u53E5\u8BDD\u5C31\u5F00\u5E72\u3002\u591A\u673A\u5668\u4EBA\u3001\u591A\u540E\u7AEF\uFF0C\u5168\u7A0B\u8DD1\u5728\u4F60\u8FD9\u53F0\u673A\u5668\u4E0A\uFF0C\u79C1\u6709\u53EF\u63A7\u3002'));
12158
+ hero.appendChild(el('div', 'home-sub', '\u628A\u98DE\u4E66\u63A5\u5230\u672C\u673A\u7684 Codex \u2014\u2014 \u4E00\u4E2A\u7FA4\u4E00\u4E2A\u9879\u76EE\uFF0C@\u4E00\u53E5\u8BDD\u5C31\u5F00\u5E72\u3002\u591A\u673A\u5668\u4EBA\u3001\u591A\u7FA4\u5E76\u884C\uFF0C\u5168\u7A0B\u8DD1\u5728\u4F60\u8FD9\u53F0\u673A\u5668\u4E0A\uFF0C\u79C1\u6709\u53EF\u63A7\u3002'));
13505
12159
  var ctaRow = el('div', 'home-cta-row');
13506
12160
  var primary = el('button', 'btn primary home-cta', hasBots ? '\u8FDB\u5165\u63A7\u5236\u53F0' : '\u2795 \u626B\u7801\u521B\u5EFA\u7B2C\u4E00\u4E2A\u673A\u5668\u4EBA');
13507
12161
  primary.onclick = function () { if (hasBots) go({ tab: 'overview' }); else tryAddBot(); };
@@ -13529,7 +12183,7 @@ ${UI_PURE_JS}
13529
12183
  // \u8DD1\u9A6C\u706F\uFF08gsap.com \u62DB\u724C\uFF09\uFF1A\u540C\u4E00\u7EC4\u8BCD\u590D\u5236\u4E24\u4EFD\u9996\u5C3E\u76F8\u63A5\uFF0CGSAP \u628A\u6574\u6761\u5411\u5DE6\u65E0\u7F1D\u5E73\u79FB\u3002
13530
12184
  var marquee = el('div', 'marquee');
13531
12185
  var track = el('div', 'marquee-track');
13532
- var words = ['CODEX', 'CLAUDE CODE', '\u8BA2\u9605 \xB7 ACP', '\u4E00\u7FA4\u4E00\u9879\u76EE', '@ \u4E00\u53E5\u8BDD\u5F00\u5E72', '\u591A\u540E\u7AEF\u53EF\u63D2\u62D4', '\u79C1\u6709\u53EF\u63A7', '127.0.0.1'];
12186
+ var words = ['CODEX', '\u4E00\u7FA4\u4E00\u9879\u76EE', '\u8BDD\u9898\u5373\u4F1A\u8BDD', '@ \u4E00\u53E5\u8BDD\u5F00\u5E72', '\u6D41\u5F0F\u5361\u7247', '\u79C1\u6709\u53EF\u63A7', '127.0.0.1'];
13533
12187
  for (var mq = 0; mq < 2; mq++) words.forEach(function (wd) { track.appendChild(el('span', null, wd)); });
13534
12188
  marquee.appendChild(track);
13535
12189
  home.appendChild(marquee);
@@ -13537,7 +12191,7 @@ ${UI_PURE_JS}
13537
12191
  var feats = el('div', 'home-feats');
13538
12192
  [
13539
12193
  { i: 'bot', t: '\u4E00\u7FA4\u4E00\u9879\u76EE', d: '\u628A\u673A\u5668\u4EBA\u62C9\u8FDB\u7FA4\uFF0C\u7FA4\u5C31\u662F\u9879\u76EE\uFF1B@\u5B83\u63D0\u9700\u6C42\uFF0C\u5B83\u5728\u7ED1\u5B9A\u7684\u76EE\u5F55\u91CC\u5E72\u6D3B\u3002' },
13540
- { i: 'cube', t: '\u591A\u540E\u7AEF\u53EF\u63D2\u62D4', d: 'Codex / Claude SDK / \u8BA2\u9605\xB7ACP\uFF0C\u6309\u9700\u4E0B\u8F7D\u3001\u968F\u65F6\u66F4\u65B0\u6216\u5378\u8F7D\uFF0C\u5404\u9879\u76EE\u5404\u9009\u5404\u7684\u3002' },
12194
+ { i: 'cube', t: '\u8F7B\u6838\u5FC3\xB7\u672C\u673A Codex', d: '\u53EA\u4F9D\u8D56\u672C\u673A\u7684 Codex CLI\uFF0C\u4E0D\u6346\u4EFB\u4F55\u91CD\u8FD0\u884C\u65F6\uFF1B\u6BCF\u4F1A\u8BDD\u72EC\u7ACB\u8FDB\u7A0B\uFF0C\u5361\u6B7B\u6709 watchdog \u515C\u5E95\u3002' },
13541
12195
  { i: 'shield', t: '\u79C1\u6709\u53EF\u63A7', d: '\u5168\u7A0B\u8DD1\u5728\u4F60\u8FD9\u53F0\u673A\u5668\u4E0A\uFF0C\u51ED\u636E\u8FDB\u672C\u5730\u52A0\u5BC6\u5E93\uFF0C\u63A7\u5236\u53F0\u53EA\u542C 127.0.0.1\u3002' },
13542
12196
  ].forEach(function (f) {
13543
12197
  var c = el('div', 'glass home-feat');
@@ -15045,7 +13699,7 @@ var DEFAULT_WEB_PORT = 51847;
15045
13699
  var COOKIE_NAME = "fcb_console_token";
15046
13700
  var SSE_INITIAL_TAIL_BYTES = 16 * 1024;
15047
13701
  function createWebServer(opts) {
15048
- const token = opts.token ?? randomUUID7();
13702
+ const token = opts.token ?? randomUUID5();
15049
13703
  const html = opts.html ?? UI_HTML;
15050
13704
  const logDir = opts.logDir ?? join19(paths.appDir, "logs");
15051
13705
  const sseCleanups = /* @__PURE__ */ new Set();
@@ -15397,7 +14051,7 @@ function createWebServer(opts) {
15397
14051
  function handleRegisterQrStream(req, res) {
15398
14052
  qrSession?.abort.abort();
15399
14053
  const abort = new AbortController();
15400
- const session = { id: randomUUID7(), abort };
14054
+ const session = { id: randomUUID5(), abort };
15401
14055
  qrSession = session;
15402
14056
  res.writeHead(200, {
15403
14057
  "Content-Type": "text/event-stream",