@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/README.md +23 -17
- package/dist/cli.js +251 -1597
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +1 -3
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
|
|
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
|
-
*
|
|
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
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
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
|
-
|
|
3376
|
-
const
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
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
|
-
|
|
3384
|
-
const
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
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
|
|
3392
|
-
|
|
1970
|
+
function isBoilerplateUserText(text) {
|
|
1971
|
+
const t = text.trimStart();
|
|
1972
|
+
return t.startsWith("<environment_context>") || t.startsWith("# AGENTS.md instructions");
|
|
3393
1973
|
}
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
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
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
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()
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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 &&
|
|
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 &&
|
|
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\
|
|
3968
|
-
console.error(" \
|
|
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
|
|
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}-${
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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}-${
|
|
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
|
|
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\
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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', '
|
|
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: '\
|
|
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 ??
|
|
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:
|
|
14054
|
+
const session = { id: randomUUID5(), abort };
|
|
15401
14055
|
qrSession = session;
|
|
15402
14056
|
res.writeHead(200, {
|
|
15403
14057
|
"Content-Type": "text/event-stream",
|