@modelzen/feishu-codex-bridge 0.4.3 → 0.5.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.
Files changed (2) hide show
  1. package/dist/cli.js +1041 -82
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -805,6 +805,25 @@ var BACKEND_CATALOG = [
805
805
  },
806
806
  // supportedModes undefined ⇒ 全档(qa/write/full)。
807
807
  blurb: "\u80FD\u529B\u6700\u5168\uFF08goal/steer/compact/resume + \u771F\u6C99\u7BB1\u53EA\u8BFB\u6863\uFF09"
808
+ },
809
+ {
810
+ id: "claude-agent",
811
+ agentFamily: "claude",
812
+ displayName: "Claude",
813
+ access: "sdk",
814
+ dep: {
815
+ // SDK 是桥的硬依赖(随桥内置),isBackendDepInstalled 路径①经 bridge 自身
816
+ // node_modules 的 require.resolve 恒命中 → 在 picker 里始终判为「已装」可选。
817
+ kind: "npm-ondemand",
818
+ pkg: "@anthropic-ai/claude-agent-sdk",
819
+ // 实测 SDK + 依赖装到用户目录约 265MB(含自带 Claude Code 运行时);首次下载较久。
820
+ approxSizeMB: 265,
821
+ detectHint: "\u6309\u9700\u4E0B\u8F7D @anthropic-ai/claude-agent-sdk\uFF08\u7EA6 265MB\uFF0C\u590D\u7528\u672C\u673A Claude \u767B\u5F55\u6001\uFF09",
822
+ installCmd: "\u968F\u6865\u5DF2\u5185\u7F6E\uFF08\u5982\u7F3A\u5931\uFF1Anpm i @anthropic-ai/claude-agent-sdk\uFF09"
823
+ },
824
+ // 必须与 ClaudeAgentBackend.supportedModes 完全一致(单测强制)。
825
+ supportedModes: ["qa", "write", "full"],
826
+ blurb: "Claude Code\uFF08SDK \u5185\u7F6E\uFF0C\u590D\u7528\u672C\u673A\u767B\u5F55\uFF1Bqa/write \u8D70 OS \u6C99\u7BB1\uFF0C\u80FD\u529B\u8F83 Codex \u7CBE\u7B80\uFF09"
808
827
  }
809
828
  ];
810
829
  function visibleCatalog() {
@@ -2054,58 +2073,46 @@ var STATIC_MODELS = [
2054
2073
  }
2055
2074
  ];
2056
2075
 
2057
- // src/agent/detect.ts
2058
- async function probeCodexAgent() {
2059
- const entry = BACKEND_CATALOG.find((e) => e.id === "codex-appserver");
2060
- const bin = resolveCodexBin({ force: true });
2061
- const version = bin ? await codexVersionAsync(bin, { force: true }) : null;
2062
- const installed = !!bin && !!version;
2063
- return {
2064
- id: "codex",
2065
- displayName: "Codex",
2066
- installed,
2067
- version,
2068
- installHint: installed ? void 0 : entry.dep.detectHint,
2069
- backends: [
2070
- {
2071
- backendId: "codex-appserver",
2072
- available: installed,
2073
- reason: !bin ? entry.dep.detectHint : !version ? "codex --version \u5931\u8D25" : void 0,
2074
- version,
2075
- supportedModes: entry.supportedModes,
2076
- installable: false
2077
- }
2078
- ]
2079
- };
2080
- }
2081
- async function detectAgents() {
2082
- return Promise.all([probeCodexAgent()]);
2083
- }
2084
- function pickDefaultBackend(agents) {
2085
- const find = (id) => agents.flatMap((a) => a.backends).find((b) => b.backendId === id);
2086
- const pickable = (id) => !catalogById(id)?.hidden && find(id)?.available;
2087
- if (pickable("codex-appserver")) return "codex-appserver";
2088
- return DEFAULT_BACKEND_ID;
2089
- }
2090
- var defaultCache;
2091
- async function effectiveDefaultBackend(opts) {
2092
- if (!opts?.force && defaultCache !== void 0) return defaultCache;
2093
- try {
2094
- defaultCache = pickDefaultBackend(await detectAgents());
2095
- } catch {
2096
- defaultCache = DEFAULT_BACKEND_ID;
2097
- }
2098
- return defaultCache;
2099
- }
2076
+ // src/agent/claude-agent/backend.ts
2077
+ import { randomUUID as randomUUID2 } from "crypto";
2100
2078
 
2101
2079
  // src/agent/backend-loader.ts
2102
2080
  import { createRequire } from "module";
2103
2081
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2104
2082
  import { join as join6 } from "path";
2105
2083
  import { pathToFileURL } from "url";
2084
+ var BackendNotInstalledError = class extends Error {
2085
+ constructor(pkg) {
2086
+ 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`);
2087
+ this.pkg = pkg;
2088
+ this.name = "BackendNotInstalledError";
2089
+ }
2090
+ pkg;
2091
+ };
2106
2092
  function userAnchor() {
2107
2093
  return join6(paths.backendsDir, "__backend_anchor__.cjs");
2108
2094
  }
2095
+ var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND", "ERR_PACKAGE_PATH_NOT_EXPORTED"]);
2096
+ function isNotFound(err) {
2097
+ const code = err?.code;
2098
+ if (code !== void 0 && NOT_FOUND_CODES.has(code)) return true;
2099
+ const msg = err?.message ?? "";
2100
+ return /Failed to (load|resolve) (url|import)/i.test(msg);
2101
+ }
2102
+ async function loadBackendDep(pkg) {
2103
+ try {
2104
+ return await import(pkg);
2105
+ } catch (err) {
2106
+ if (!isNotFound(err)) throw err;
2107
+ }
2108
+ let resolved;
2109
+ try {
2110
+ resolved = createRequire(userAnchor()).resolve(pkg);
2111
+ } catch {
2112
+ throw new BackendNotInstalledError(pkg);
2113
+ }
2114
+ return await import(pathToFileURL(resolved).href);
2115
+ }
2109
2116
  function isBackendDepInstalled(pkg) {
2110
2117
  try {
2111
2118
  createRequire(import.meta.url).resolve(pkg);
@@ -2164,6 +2171,905 @@ function installedBackendVersion(pkg) {
2164
2171
  }
2165
2172
  }
2166
2173
 
2174
+ // src/agent/claude-agent/permission.ts
2175
+ var WRITE_TOOLS = ["Write", "Edit", "NotebookEdit"];
2176
+ var NETWORK_TOOLS = ["WebFetch", "WebSearch"];
2177
+ function permissionOptions(mode, network, cwd) {
2178
+ const tier = mode ?? "full";
2179
+ if (tier === "full") {
2180
+ return { permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true };
2181
+ }
2182
+ const sandbox = {
2183
+ enabled: true,
2184
+ failIfUnavailable: true,
2185
+ // fail-closed: error out instead of running unsandboxed.
2186
+ autoAllowBashIfSandboxed: true,
2187
+ // sandboxed Bash needn't prompt — the sandbox is the gate.
2188
+ allowUnsandboxedCommands: false,
2189
+ // CRITICAL: block the dangerouslyDisableSandbox escape.
2190
+ ...tier === "qa" ? { filesystem: { denyWrite: [cwd] } } : {}
2191
+ };
2192
+ const disallowedTools = [];
2193
+ if (tier === "qa") disallowedTools.push(...WRITE_TOOLS);
2194
+ if (!network) disallowedTools.push(...NETWORK_TOOLS);
2195
+ return {
2196
+ // bypassPermissions = never prompt; the sandbox (above) is the real boundary.
2197
+ permissionMode: "bypassPermissions",
2198
+ allowDangerouslySkipPermissions: true,
2199
+ sandbox,
2200
+ ...disallowedTools.length ? { disallowedTools } : {}
2201
+ };
2202
+ }
2203
+
2204
+ // src/agent/claude-agent/event-map.ts
2205
+ function createTurnMapper(ctx = {}) {
2206
+ let blockSeq = 0;
2207
+ const open3 = /* @__PURE__ */ new Map();
2208
+ let systemEmitted = false;
2209
+ let initModel;
2210
+ function map(msg) {
2211
+ const m = msg;
2212
+ switch (m.type) {
2213
+ case "system":
2214
+ if (m.subtype === "init") {
2215
+ if (typeof m.model === "string") initModel = m.model;
2216
+ if (!systemEmitted) {
2217
+ systemEmitted = true;
2218
+ return [{ type: "system", threadId: String(m.session_id ?? "") }];
2219
+ }
2220
+ }
2221
+ if (m.subtype === "compact_boundary") return [{ type: "context_compacted" }];
2222
+ return [];
2223
+ case "stream_event":
2224
+ return mapStreamEvent(m.event);
2225
+ case "assistant": {
2226
+ const content = m.message?.content ?? [];
2227
+ const out = [];
2228
+ for (const b of content) {
2229
+ if (b.type === "tool_use" || b.type === "server_tool_use") {
2230
+ out.push({
2231
+ type: "tool_use",
2232
+ itemId: String(b.id ?? `tool${++blockSeq}`),
2233
+ title: toolTitle(b.name ?? "\u5DE5\u5177", b.input ?? {}, ctx.cwd),
2234
+ detail: toolDetail(b.name ?? "", b.input ?? {})
2235
+ });
2236
+ }
2237
+ }
2238
+ return out;
2239
+ }
2240
+ case "user": {
2241
+ const content = m.message?.content;
2242
+ if (!Array.isArray(content)) return [];
2243
+ const out = [];
2244
+ for (const b of content) {
2245
+ if (b.type === "tool_result") {
2246
+ out.push({
2247
+ type: "tool_result",
2248
+ itemId: String(b.tool_use_id ?? ""),
2249
+ output: toolResultText(b.content),
2250
+ exitCode: b.is_error ? 1 : 0
2251
+ });
2252
+ }
2253
+ }
2254
+ return out;
2255
+ }
2256
+ case "result": {
2257
+ const out = [];
2258
+ const usage = m.usage;
2259
+ if (usage) {
2260
+ out.push({
2261
+ type: "usage",
2262
+ inputTokens: usage.input_tokens,
2263
+ outputTokens: usage.output_tokens
2264
+ });
2265
+ const used = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
2266
+ if (used > 0) {
2267
+ out.push({ type: "context_usage", usedTokens: used, contextWindow: contextWindowFor(initModel) });
2268
+ }
2269
+ }
2270
+ return out;
2271
+ }
2272
+ // Transient API hiccup the SDK auto-retries — surface a retrying footer
2273
+ // (run-state keeps the ⏹ button and lets the next deltas overwrite it).
2274
+ case "system_api_retry":
2275
+ case "api_retry":
2276
+ return [{ type: "error", message: "\u7F51\u7EDC\u6CE2\u52A8\uFF0C\u6B63\u5728\u91CD\u8BD5\u2026", willRetry: true }];
2277
+ default:
2278
+ return [];
2279
+ }
2280
+ }
2281
+ function mapStreamEvent(ev) {
2282
+ if (!ev || typeof ev.type !== "string") return [];
2283
+ switch (ev.type) {
2284
+ case "content_block_start": {
2285
+ const idx = ev.index;
2286
+ const cb = ev.content_block ?? {};
2287
+ const kind = cb.type === "thinking" || cb.type === "redacted_thinking" ? "thinking" : cb.type === "text" ? "text" : cb.type === "tool_use" || cb.type === "server_tool_use" ? "tool_use" : "other";
2288
+ open3.set(idx, { itemId: `b${++blockSeq}`, kind, acc: "" });
2289
+ return [];
2290
+ }
2291
+ case "content_block_delta": {
2292
+ const b = open3.get(ev.index);
2293
+ if (!b) return [];
2294
+ const d = ev.delta ?? {};
2295
+ if (d.type === "text_delta" && typeof d.text === "string") {
2296
+ b.acc += d.text;
2297
+ return [{ type: "text_delta", itemId: b.itemId, delta: d.text }];
2298
+ }
2299
+ if (d.type === "thinking_delta" && typeof d.thinking === "string") {
2300
+ b.acc += d.thinking;
2301
+ return [{ type: "thinking_delta", itemId: b.itemId, delta: d.thinking }];
2302
+ }
2303
+ return [];
2304
+ }
2305
+ case "content_block_stop": {
2306
+ const idx = ev.index;
2307
+ const b = open3.get(idx);
2308
+ open3.delete(idx);
2309
+ if (!b || !b.acc) return [];
2310
+ if (b.kind === "text") return [{ type: "text", itemId: b.itemId, text: b.acc }];
2311
+ if (b.kind === "thinking") return [{ type: "thinking", itemId: b.itemId, text: b.acc }];
2312
+ return [];
2313
+ }
2314
+ default:
2315
+ return [];
2316
+ }
2317
+ }
2318
+ return { map };
2319
+ }
2320
+ function toolTitle(name, input2, cwd) {
2321
+ const s = (k2) => typeof input2[k2] === "string" ? input2[k2] : "";
2322
+ switch (name) {
2323
+ case "Bash":
2324
+ case "BashOutput":
2325
+ return s("command") || "Shell \u547D\u4EE4";
2326
+ case "Read":
2327
+ return `\u8BFB\u53D6 ${displayPath2(s("file_path"), cwd)}`;
2328
+ case "Write":
2329
+ return `\u5199\u5165 ${displayPath2(s("file_path"), cwd)}`;
2330
+ case "Edit":
2331
+ case "MultiEdit":
2332
+ return `\u7F16\u8F91 ${displayPath2(s("file_path"), cwd)}`;
2333
+ case "NotebookEdit":
2334
+ return `\u7F16\u8F91\u7B14\u8BB0\u672C ${displayPath2(s("notebook_path"), cwd)}`;
2335
+ case "Glob":
2336
+ return `\u67E5\u627E ${s("pattern")}`.trim();
2337
+ case "Grep":
2338
+ return `\u641C\u7D22 ${s("pattern")}`.trim();
2339
+ case "WebFetch":
2340
+ return `\u6293\u53D6\u7F51\u9875 ${s("url")}`.trim();
2341
+ case "WebSearch":
2342
+ return `\u8054\u7F51\u641C\u7D22 ${s("query")}`.trim();
2343
+ case "Task":
2344
+ return `\u5B50\u4EFB\u52A1\uFF1A${s("description") || s("subagent_type") || ""}`.trim();
2345
+ case "TodoWrite":
2346
+ return "\u66F4\u65B0\u5F85\u529E\u6E05\u5355";
2347
+ case "ExitPlanMode":
2348
+ return "\u63D0\u4EA4\u65B9\u6848";
2349
+ default:
2350
+ return name || "\u5DE5\u5177\u8C03\u7528";
2351
+ }
2352
+ }
2353
+ function toolDetail(name, input2) {
2354
+ if (name === "Bash" && typeof input2.description === "string" && input2.description) {
2355
+ return input2.description;
2356
+ }
2357
+ return void 0;
2358
+ }
2359
+ function toolResultText(content) {
2360
+ if (content == null) return void 0;
2361
+ if (typeof content === "string") return content || void 0;
2362
+ if (Array.isArray(content)) {
2363
+ const text = content.map((b) => {
2364
+ const x = b;
2365
+ if (x?.type === "text" && typeof x.text === "string") return x.text;
2366
+ if (x?.type === "image") return "[\u56FE\u7247]";
2367
+ return "";
2368
+ }).join("").trim();
2369
+ return text || void 0;
2370
+ }
2371
+ return void 0;
2372
+ }
2373
+ var PATH_TAIL_MAX2 = 40;
2374
+ function displayPath2(p, cwd) {
2375
+ if (!p) return "\u6587\u4EF6";
2376
+ if (cwd) {
2377
+ const sep2 = cwd.includes("\\") ? "\\" : "/";
2378
+ const root = cwd.endsWith(sep2) ? cwd : cwd + sep2;
2379
+ if (p.startsWith(root) && p.length > root.length) return p.slice(root.length);
2380
+ }
2381
+ if (p.length <= PATH_TAIL_MAX2 || !p.includes("/")) return p;
2382
+ const segs = p.split("/");
2383
+ let out = segs[segs.length - 1] ?? p;
2384
+ for (let i = segs.length - 2; i >= 0; i--) {
2385
+ const cand = `${segs[i]}/${out}`;
2386
+ if (cand.length > PATH_TAIL_MAX2) break;
2387
+ out = cand;
2388
+ }
2389
+ return `\u2026/${out}`;
2390
+ }
2391
+ function contextWindowFor(model) {
2392
+ const id = typeof model === "string" ? model.toLowerCase() : "";
2393
+ if (!id) return null;
2394
+ if (id.includes("1m") || id.includes("[1m]")) return 1e6;
2395
+ return 2e5;
2396
+ }
2397
+ function resultErrorText(m) {
2398
+ const result = m.result;
2399
+ if (typeof result === "string" && result.trim()) return result.trim();
2400
+ const subtype = m.subtype;
2401
+ if (typeof subtype === "string") {
2402
+ if (subtype === "error_max_turns") return "\u5DF2\u8FBE\u5230\u6700\u5927\u8F6E\u6B21\u9650\u5236";
2403
+ if (subtype === "error_max_budget_usd") return "\u5DF2\u8FBE\u5230\u9884\u7B97\u4E0A\u9650";
2404
+ return `\u8FD0\u884C\u51FA\u9519\uFF08${subtype}\uFF09`;
2405
+ }
2406
+ return "\u8FD0\u884C\u51FA\u9519";
2407
+ }
2408
+
2409
+ // src/agent/claude-agent/history.ts
2410
+ function mapSessionSummary(s) {
2411
+ const preview = (s.customTitle || s.summary || s.firstPrompt || "").trim();
2412
+ return {
2413
+ sessionId: s.sessionId,
2414
+ preview,
2415
+ createdAt: msToSec(s.createdAt ?? s.lastModified),
2416
+ updatedAt: msToSec(s.lastModified),
2417
+ name: s.customTitle || void 0
2418
+ };
2419
+ }
2420
+ function msToSec(ms) {
2421
+ return ms && ms > 0 ? Math.floor(ms / 1e3) : 0;
2422
+ }
2423
+ function blocksOf(message) {
2424
+ const content = message?.content;
2425
+ if (typeof content === "string") return [{ type: "text", text: content }];
2426
+ if (Array.isArray(content)) return content;
2427
+ return [];
2428
+ }
2429
+ function isBoilerplateUserText2(text) {
2430
+ const t = text.trimStart();
2431
+ return t.startsWith("<environment_context>") || t.startsWith("# AGENTS.md") || t.startsWith("<system-reminder>") || t.startsWith("Caveat:");
2432
+ }
2433
+ function toolResultText2(content) {
2434
+ if (content == null) return void 0;
2435
+ if (typeof content === "string") return content || void 0;
2436
+ if (Array.isArray(content)) {
2437
+ const text = content.map((b) => {
2438
+ const x = b;
2439
+ return x?.type === "text" && typeof x.text === "string" ? x.text : "";
2440
+ }).join("").trim();
2441
+ return text || void 0;
2442
+ }
2443
+ return void 0;
2444
+ }
2445
+ function foldSessionMessages(messages, maxTurns, cwd) {
2446
+ const turns = [];
2447
+ const toolById = /* @__PURE__ */ new Map();
2448
+ let cur = null;
2449
+ const flush = () => {
2450
+ if (cur && cur._hasContent) {
2451
+ const { _hasContent, ...turn } = cur;
2452
+ void _hasContent;
2453
+ turns.push(turn);
2454
+ }
2455
+ cur = null;
2456
+ };
2457
+ const ensure = () => {
2458
+ if (!cur) cur = { userText: "", assistantText: "", reasoning: "", tools: [], _hasContent: false };
2459
+ return cur;
2460
+ };
2461
+ for (const msg of messages) {
2462
+ const blocks = blocksOf(msg.message);
2463
+ if (msg.type === "user") {
2464
+ const texts = [];
2465
+ let attachedResult = false;
2466
+ for (const b of blocks) {
2467
+ if (b.type === "text" && b.text && !isBoilerplateUserText2(b.text)) texts.push(b.text);
2468
+ else if (b.type === "tool_result" && b.tool_use_id) {
2469
+ const tool = toolById.get(b.tool_use_id);
2470
+ if (tool) {
2471
+ tool.output = toolResultText2(b.content);
2472
+ if (b.is_error) tool.failed = true;
2473
+ attachedResult = true;
2474
+ }
2475
+ }
2476
+ }
2477
+ const userText = texts.join("\n").trim();
2478
+ if (userText) {
2479
+ flush();
2480
+ const t = ensure();
2481
+ t.userText = userText;
2482
+ t._hasContent = true;
2483
+ } else if (attachedResult) {
2484
+ ensure()._hasContent = true;
2485
+ }
2486
+ } else if (msg.type === "assistant") {
2487
+ const t = ensure();
2488
+ for (const b of blocks) {
2489
+ if (b.type === "text" && b.text) {
2490
+ t.assistantText = t.assistantText ? `${t.assistantText}
2491
+
2492
+ ${b.text}` : b.text;
2493
+ t._hasContent = true;
2494
+ } else if (b.type === "thinking" && b.thinking) {
2495
+ t.reasoning = t.reasoning ? `${t.reasoning}
2496
+
2497
+ ${b.thinking}` : b.thinking;
2498
+ t._hasContent = true;
2499
+ } else if ((b.type === "tool_use" || b.type === "server_tool_use") && b.id) {
2500
+ const tool = { title: toolTitle(b.name ?? "\u5DE5\u5177", b.input ?? {}, cwd) };
2501
+ t.tools.push(tool);
2502
+ toolById.set(b.id, tool);
2503
+ t._hasContent = true;
2504
+ }
2505
+ }
2506
+ }
2507
+ }
2508
+ flush();
2509
+ const totalTurns = turns.length;
2510
+ const kept = totalTurns > maxTurns ? turns.slice(totalTurns - maxTurns) : turns;
2511
+ return { turns: kept, totalTurns };
2512
+ }
2513
+
2514
+ // src/agent/claude-agent/thread.ts
2515
+ var COMPACT_TIMEOUT_MS2 = 12e4;
2516
+ var ABORT_ESCALATE_MS = 4e3;
2517
+ var Inbox = class {
2518
+ buf = [];
2519
+ waiters = [];
2520
+ push(v) {
2521
+ const w = this.waiters.shift();
2522
+ if (w) w(v);
2523
+ else this.buf.push(v);
2524
+ }
2525
+ next() {
2526
+ const v = this.buf.shift();
2527
+ if (v !== void 0) return Promise.resolve(v);
2528
+ return new Promise((resolve8) => this.waiters.push(resolve8));
2529
+ }
2530
+ };
2531
+ var PushablePrompt = class {
2532
+ buf = [];
2533
+ waiters = [];
2534
+ closed = false;
2535
+ push(msg) {
2536
+ if (this.closed) return;
2537
+ const w = this.waiters.shift();
2538
+ if (w) w(msg);
2539
+ else this.buf.push(msg);
2540
+ }
2541
+ close() {
2542
+ this.closed = true;
2543
+ while (this.waiters.length) this.waiters.shift()(null);
2544
+ }
2545
+ async *[Symbol.asyncIterator]() {
2546
+ while (true) {
2547
+ const v = this.buf.shift();
2548
+ if (v !== void 0) {
2549
+ yield v;
2550
+ continue;
2551
+ }
2552
+ if (this.closed) return;
2553
+ const next = await new Promise((resolve8) => this.waiters.push(resolve8));
2554
+ if (next == null) return;
2555
+ yield next;
2556
+ }
2557
+ }
2558
+ };
2559
+ function toSdkEffort(e) {
2560
+ if (!e) return void 0;
2561
+ if (e === "none" || e === "minimal") return "low";
2562
+ return e;
2563
+ }
2564
+ function toUserMessage(input2) {
2565
+ const text = input2.text ?? "";
2566
+ return {
2567
+ type: "user",
2568
+ message: { role: "user", content: text },
2569
+ parent_tool_use_id: null
2570
+ };
2571
+ }
2572
+ function goalPrompt(objective) {
2573
+ return [
2574
+ "\u3010\u81EA\u4E3B\u76EE\u6807\u3011\u8BF7\u8FDE\u7EED\u3001\u81EA\u4E3B\u5730\u5B8C\u6210\u4E0B\u9762\u7684\u76EE\u6807\uFF1A\u6309\u9700\u4F7F\u7528\u5DE5\u5177\uFF0C\u4E00\u6B65\u6B65\u505A\u5230\u5B8C\u6210\u4E3A\u6B62\uFF0C",
2575
+ "\u4E2D\u9014\u4E0D\u8981\u505C\u4E0B\u6765\u7B49\u6211\u786E\u8BA4\uFF1B\u5B8C\u6210\u540E\u7528\u4E00\u6BB5\u8BDD\u603B\u7ED3\u505A\u4E86\u4EC0\u4E48\u3002",
2576
+ "",
2577
+ `\u76EE\u6807\uFF1A${objective}`
2578
+ ].join("\n");
2579
+ }
2580
+ function goalStatusFromResult(subtype) {
2581
+ if (subtype === "error_max_budget_usd") return "budgetLimited";
2582
+ return "blocked";
2583
+ }
2584
+ var ClaudeAgentThread = class {
2585
+ sessionId;
2586
+ cwd;
2587
+ model;
2588
+ effort;
2589
+ input = new PushablePrompt();
2590
+ query;
2591
+ abortController = new AbortController();
2592
+ dead = false;
2593
+ interruptRequested = false;
2594
+ /** true while a runGoal() turn is in flight — clearGoal() only hard-stops then. */
2595
+ goalRunning = false;
2596
+ /** true while ANY turn (runStreamed/runGoal) is in flight — gates abort escalation. */
2597
+ turnInFlight = false;
2598
+ escalateTimer;
2599
+ turnSeq = 0;
2600
+ currentTurnId;
2601
+ lastActivityAt = Date.now();
2602
+ sink;
2603
+ constructor(cfg) {
2604
+ this.sessionId = cfg.sessionId;
2605
+ this.cwd = cfg.cwd;
2606
+ this.model = cfg.model;
2607
+ this.effort = cfg.effort;
2608
+ const options = {
2609
+ cwd: cfg.cwd,
2610
+ abortController: this.abortController,
2611
+ includePartialMessages: true,
2612
+ ...cfg.model ? { model: cfg.model } : {},
2613
+ ...toSdkEffort(cfg.effort) ? { effort: toSdkEffort(cfg.effort) } : {},
2614
+ ...cfg.resume ? { resume: cfg.sessionId } : { sessionId: cfg.sessionId },
2615
+ ...cfg.systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: cfg.systemPromptAppend } } : {},
2616
+ // permission tier (permissionMode / sandbox / canUseTool / disallowedTools)
2617
+ ...cfg.permission,
2618
+ stderr: (d) => {
2619
+ const s = String(d).trim();
2620
+ if (s) log.info("agent", "sdk-stderr", { backend: "claude-agent", line: s.slice(0, 300) });
2621
+ }
2622
+ };
2623
+ this.query = cfg.query({ prompt: this.input, options });
2624
+ void this.pump();
2625
+ }
2626
+ /** Single background consumer: route every query message to the current turn. */
2627
+ async pump() {
2628
+ try {
2629
+ for await (const msg of this.query) {
2630
+ this.lastActivityAt = Date.now();
2631
+ this.sink?.({ kind: "msg", msg });
2632
+ }
2633
+ this.dead = true;
2634
+ this.sink?.({ kind: "end" });
2635
+ } catch (err) {
2636
+ this.dead = true;
2637
+ log.fail("agent", err, { backend: "claude-agent", phase: "pump" });
2638
+ this.sink?.({ kind: "error", err });
2639
+ }
2640
+ }
2641
+ runStreamed(input2, turn) {
2642
+ const turnId = `t${++this.turnSeq}`;
2643
+ this.currentTurnId = turnId;
2644
+ this.interruptRequested = false;
2645
+ this.turnInFlight = true;
2646
+ if (turn?.model && turn.model !== this.model) {
2647
+ this.model = turn.model;
2648
+ this.query.setModel(turn.model).catch((err) => log.fail("agent", err, { phase: "setModel" }));
2649
+ }
2650
+ if (turn?.effort) this.effort = turn.effort;
2651
+ const mapper = createTurnMapper({ cwd: this.cwd });
2652
+ const inbox = new Inbox();
2653
+ const mySink = (item) => inbox.push(item);
2654
+ this.sink = mySink;
2655
+ this.input.push(toUserMessage(input2));
2656
+ const self = this;
2657
+ async function* gen() {
2658
+ yield { type: "turn_started", turnId };
2659
+ try {
2660
+ while (true) {
2661
+ const item = await inbox.next();
2662
+ if (item.kind === "end" || item.kind === "error") {
2663
+ if (self.interruptRequested) {
2664
+ yield { type: "done", turnId };
2665
+ return;
2666
+ }
2667
+ const msg2 = item.kind === "error" && item.err instanceof Error ? item.err.message : "Claude \u4F1A\u8BDD\u8FDB\u7A0B\u5DF2\u9000\u51FA";
2668
+ yield { type: "error", message: msg2 || "Claude \u4F1A\u8BDD\u51FA\u9519", willRetry: false };
2669
+ return;
2670
+ }
2671
+ const msg = item.msg;
2672
+ self.lastActivityAt = Date.now();
2673
+ for (const ev of mapper.map(item.msg)) yield ev;
2674
+ if (msg.type === "result") {
2675
+ const ok = msg.subtype === "success" || self.interruptRequested;
2676
+ if (ok) {
2677
+ try {
2678
+ const cu = await self.query.getContextUsage();
2679
+ if (cu && typeof cu.totalTokens === "number" && typeof cu.maxTokens === "number") {
2680
+ yield { type: "context_usage", usedTokens: cu.totalTokens, contextWindow: cu.maxTokens };
2681
+ }
2682
+ } catch (err) {
2683
+ log.fail("agent", err, { backend: "claude-agent", phase: "getContextUsage" });
2684
+ }
2685
+ yield { type: "done", turnId };
2686
+ } else {
2687
+ yield { type: "error", message: resultErrorText(msg), willRetry: false };
2688
+ }
2689
+ return;
2690
+ }
2691
+ }
2692
+ } finally {
2693
+ self.endTurn();
2694
+ if (self.sink === mySink) self.sink = void 0;
2695
+ }
2696
+ }
2697
+ return {
2698
+ events: gen(),
2699
+ turnId: () => self.currentTurnId,
2700
+ lastActivity: () => self.lastActivityAt
2701
+ };
2702
+ }
2703
+ /** End-of-turn bookkeeping: clear the in-flight flag + any pending ⏹ escalation. */
2704
+ endTurn() {
2705
+ this.turnInFlight = false;
2706
+ if (this.escalateTimer) {
2707
+ clearTimeout(this.escalateTimer);
2708
+ this.escalateTimer = void 0;
2709
+ }
2710
+ }
2711
+ /**
2712
+ * /goal —— Claude 没有 codex 那种「目标引擎 + 多轮自动续跑」,但 Claude Code 的
2713
+ * agent loop 本身就能在 ONE query turn 内自主多步跑完一个目标。所以这里把目标当作
2714
+ * 「一个自主轮」:发带自主提示的目标 → 流式跑 → 合成 goal_update 状态(active 起、
2715
+ * complete/blocked/budgetLimited 收)。
2716
+ *
2717
+ * ── 与 codex 的差异(如实)──────────────────────────────────────────────
2718
+ * - codex:N 个自动续跑的 turn(N 张卡片)+ 原生状态机(active/paused/complete/
2719
+ * budgetLimited/usageLimited/blocked)+ token 预算。
2720
+ * - claude:1 个自主 turn(1 张卡片,内部多步)+ 合成状态(仅 active→complete/
2721
+ * blocked/budgetLimited)。无 paused/usageLimited、无预算。
2722
+ * - ⏹ 终止 / 🎯 结束:经 clearGoal() 用 abortController 硬停(spike 实测 ~2s 停,
2723
+ * 且 abort 后会话可 resume 续聊)——比工具执行中途的 interrupt() 可靠。
2724
+ */
2725
+ runGoal(objective) {
2726
+ const turnId = `g${++this.turnSeq}`;
2727
+ this.currentTurnId = turnId;
2728
+ this.interruptRequested = false;
2729
+ this.goalRunning = true;
2730
+ this.turnInFlight = true;
2731
+ const startedAt = Date.now();
2732
+ const mapper = createTurnMapper({ cwd: this.cwd });
2733
+ const inbox = new Inbox();
2734
+ const mySink = (item) => inbox.push(item);
2735
+ this.sink = mySink;
2736
+ this.input.push(toUserMessage({ text: goalPrompt(objective) }));
2737
+ const self = this;
2738
+ async function* gen() {
2739
+ yield { type: "goal_update", status: "active", objective, tokensUsed: 0, timeUsedSeconds: 0, tokenBudget: null };
2740
+ yield { type: "turn_started", turnId };
2741
+ let tokensUsed = 0;
2742
+ const finishGoal = (status) => ({
2743
+ type: "goal_update",
2744
+ status,
2745
+ objective,
2746
+ tokensUsed,
2747
+ timeUsedSeconds: Math.round((Date.now() - startedAt) / 1e3),
2748
+ tokenBudget: null
2749
+ });
2750
+ try {
2751
+ while (true) {
2752
+ const item = await inbox.next();
2753
+ if (item.kind === "end" || item.kind === "error") {
2754
+ if (self.interruptRequested) {
2755
+ yield { type: "done", turnId };
2756
+ } else {
2757
+ const m = item.kind === "error" && item.err instanceof Error ? item.err.message : "Claude \u4F1A\u8BDD\u8FDB\u7A0B\u5DF2\u9000\u51FA";
2758
+ yield finishGoal("blocked");
2759
+ yield { type: "error", message: m, willRetry: false };
2760
+ }
2761
+ return;
2762
+ }
2763
+ const msg = item.msg;
2764
+ self.lastActivityAt = Date.now();
2765
+ for (const ev of mapper.map(item.msg)) {
2766
+ if (ev.type === "usage") tokensUsed = (ev.inputTokens ?? 0) + (ev.outputTokens ?? 0);
2767
+ yield ev;
2768
+ }
2769
+ if (msg.type === "result") {
2770
+ self.goalRunning = false;
2771
+ const status = msg.subtype === "success" || self.interruptRequested ? "complete" : goalStatusFromResult(msg.subtype);
2772
+ try {
2773
+ const cu = await self.query.getContextUsage();
2774
+ if (cu && typeof cu.totalTokens === "number" && typeof cu.maxTokens === "number") {
2775
+ yield { type: "context_usage", usedTokens: cu.totalTokens, contextWindow: cu.maxTokens };
2776
+ }
2777
+ } catch {
2778
+ }
2779
+ yield finishGoal(status);
2780
+ yield { type: "done", turnId };
2781
+ return;
2782
+ }
2783
+ }
2784
+ } finally {
2785
+ self.goalRunning = false;
2786
+ self.endTurn();
2787
+ if (self.sink === mySink) self.sink = void 0;
2788
+ }
2789
+ }
2790
+ return { events: gen(), turnId: () => self.currentTurnId, lastActivity: () => self.lastActivityAt };
2791
+ }
2792
+ /**
2793
+ * Clear/terminate the goal. Claude has no goal engine, so "clear" = hard-stop the
2794
+ * in-flight goal turn via the AbortController (interrupt() can hang mid-tool;
2795
+ * abort reliably stops in ~2s and the session stays resumable, so chat continues
2796
+ * via resolveThread's resume on the next message). No-op when no goal is running
2797
+ * (this is also the orchestrator's idempotent end-of-run cleanup — keep the warm
2798
+ * query alive on natural completion).
2799
+ */
2800
+ async clearGoal() {
2801
+ if (!this.goalRunning) return;
2802
+ this.goalRunning = false;
2803
+ this.interruptRequested = true;
2804
+ this.dead = true;
2805
+ try {
2806
+ this.abortController.abort();
2807
+ } catch {
2808
+ }
2809
+ }
2810
+ async steer() {
2811
+ throw new Error("claude-agent \u540E\u7AEF\u6682\u4E0D\u652F\u6301\u98DE\u884C\u4E2D\u5F15\u5BFC\uFF08steer\uFF09\uFF0C\u5C06\u81EA\u52A8\u6539\u4E3A\u4E0B\u4E00\u8F6E\u53D1\u9001");
2812
+ }
2813
+ /**
2814
+ * Manual /compact. Claude Code's `/compact` IS a real slash command (verified:
2815
+ * `supportedCommands()` lists it, and sending "/compact" as input is intercepted
2816
+ * and executed, not echoed). We send it through the persistent query and drain to
2817
+ * the result; a `compact_boundary`(trigger:manual) means it actually compacted —
2818
+ * "Not enough messages to compact." just ends with no boundary (compacted=false).
2819
+ */
2820
+ async compact() {
2821
+ if (this.dead) throw new Error("Claude \u4F1A\u8BDD\u5DF2\u7ED3\u675F\uFF0C\u65E0\u6CD5\u538B\u7F29");
2822
+ const inbox = new Inbox();
2823
+ const mySink = (item) => inbox.push(item);
2824
+ this.sink = mySink;
2825
+ this.interruptRequested = false;
2826
+ let timer;
2827
+ const timeout = new Promise((resolve8) => {
2828
+ timer = setTimeout(() => resolve8("timeout"), COMPACT_TIMEOUT_MS2);
2829
+ });
2830
+ let compacted = false;
2831
+ try {
2832
+ this.input.push(toUserMessage({ text: "/compact" }));
2833
+ while (true) {
2834
+ const step = await Promise.race([inbox.next(), timeout]);
2835
+ if (step === "timeout") throw new Error(`\u538B\u7F29\u8D85\u65F6\uFF08Claude \u672A\u5728 ${COMPACT_TIMEOUT_MS2 / 1e3}s \u5185\u5B8C\u6210\uFF09`);
2836
+ if (step.kind === "end") throw new Error("Claude \u4F1A\u8BDD\u8FDB\u7A0B\u5DF2\u9000\u51FA");
2837
+ if (step.kind === "error") throw step.err instanceof Error ? step.err : new Error(String(step.err));
2838
+ const msg = step.msg;
2839
+ this.lastActivityAt = Date.now();
2840
+ if (msg.type === "system" && msg.subtype === "compact_boundary") compacted = true;
2841
+ if (msg.type === "result") break;
2842
+ }
2843
+ } finally {
2844
+ if (timer) clearTimeout(timer);
2845
+ if (this.sink === mySink) this.sink = void 0;
2846
+ }
2847
+ let usage = null;
2848
+ try {
2849
+ const cu = await this.query.getContextUsage();
2850
+ if (cu && typeof cu.totalTokens === "number" && typeof cu.maxTokens === "number") {
2851
+ usage = { usedTokens: cu.totalTokens, contextWindow: cu.maxTokens };
2852
+ }
2853
+ } catch {
2854
+ }
2855
+ return { compacted, usage };
2856
+ }
2857
+ async abort(_turnId) {
2858
+ this.interruptRequested = true;
2859
+ try {
2860
+ await this.query.interrupt();
2861
+ } catch (err) {
2862
+ log.fail("agent", err, { backend: "claude-agent", phase: "interrupt" });
2863
+ }
2864
+ if (this.escalateTimer || !this.turnInFlight) return;
2865
+ this.escalateTimer = setTimeout(() => {
2866
+ this.escalateTimer = void 0;
2867
+ if (this.dead || !this.turnInFlight) return;
2868
+ log.info("agent", "interrupt-escalate", { backend: "claude-agent" });
2869
+ this.dead = true;
2870
+ try {
2871
+ this.abortController.abort();
2872
+ } catch {
2873
+ }
2874
+ }, ABORT_ESCALATE_MS);
2875
+ }
2876
+ isAlive() {
2877
+ return !this.dead;
2878
+ }
2879
+ async close() {
2880
+ this.dead = true;
2881
+ this.endTurn();
2882
+ try {
2883
+ this.input.close();
2884
+ } catch {
2885
+ }
2886
+ try {
2887
+ this.abortController.abort();
2888
+ } catch {
2889
+ }
2890
+ try {
2891
+ this.query.close?.();
2892
+ } catch {
2893
+ }
2894
+ }
2895
+ };
2896
+
2897
+ // src/agent/claude-agent/backend.ts
2898
+ var SDK_PKG = "@anthropic-ai/claude-agent-sdk";
2899
+ var sdkPromise;
2900
+ function loadSdk() {
2901
+ sdkPromise ??= loadBackendDep(SDK_PKG);
2902
+ return sdkPromise;
2903
+ }
2904
+ var ClaudeAgentBackend = class {
2905
+ id = "claude-agent";
2906
+ displayName = "Claude";
2907
+ capabilities = {
2908
+ // /goal:goal-like —— 一个自主轮跑完目标 + 合成状态 + abort 硬停可终止续聊
2909
+ // (非 codex 的多轮目标引擎,差异见 thread.runGoal)。
2910
+ goal: true,
2911
+ steer: false,
2912
+ // /compact:Claude Code 原生斜杠命令(发 "/compact" 即触发,见 thread.compact)。
2913
+ compact: true,
2914
+ // resume 历史卡:读 ~/.claude/projects 会话存储(与 `claude -r` 同源,双向可见)。
2915
+ resume: true,
2916
+ approvals: false
2917
+ };
2918
+ // Claude's sandbox supports macOS (Seatbelt) and Linux (bubblewrap), so all
2919
+ // three tiers are offered; qa/write fail-closed at runtime if the sandbox can't
2920
+ // start (permission.ts sets sandbox.failIfUnavailable). See the security delta
2921
+ // documented in permission.ts / CLAUDE_AGENT_PROGRESS.md.
2922
+ supportedModes = ["qa", "write", "full"];
2923
+ async isAvailable() {
2924
+ return (await this.doctor()).ok;
2925
+ }
2926
+ async doctor() {
2927
+ if (!isBackendDepInstalled(SDK_PKG)) {
2928
+ return {
2929
+ ok: false,
2930
+ version: null,
2931
+ installable: true,
2932
+ depState: "not-installed",
2933
+ hint: "\u70B9\u300C\u4E0B\u8F7D\u300D\u5B89\u88C5 Claude Agent SDK\uFF08\u96F6 sudo\uFF0C\u88C5\u5230\u7528\u6237\u76EE\u5F55\uFF09"
2934
+ };
2935
+ }
2936
+ return {
2937
+ ok: true,
2938
+ version: installedBackendVersion(SDK_PKG),
2939
+ location: SDK_PKG,
2940
+ depState: "installed",
2941
+ hint: "\u590D\u7528\u672C\u673A Claude \u767B\u5F55\u6001\uFF08\u672A\u767B\u5F55\u8BF7\u5148 `claude` \u767B\u5F55\uFF0C\u6216\u8BBE\u7F6E ANTHROPIC_API_KEY\uFF09"
2942
+ };
2943
+ }
2944
+ async listModels() {
2945
+ return STATIC_MODELS2;
2946
+ }
2947
+ /** 最近会话(newest first),读 ~/.claude/projects/<cwd-hash> 的 JSONL 存储——
2948
+ * 与 `claude -r` 同源,故能列出本机用 `claude` 手开的会话。绝不抛错(契约)。 */
2949
+ async listThreads(cwd, limit = 15) {
2950
+ try {
2951
+ const sdk = await loadSdk();
2952
+ const sessions = await sdk.listSessions({ dir: cwd, limit });
2953
+ return sessions.map(mapSessionSummary).sort((a, b) => b.updatedAt - a.updatedAt);
2954
+ } catch (err) {
2955
+ log.fail("agent", err, { backend: "claude-agent", phase: "listSessions" });
2956
+ return [];
2957
+ }
2958
+ }
2959
+ /** 某会话的转写摘要(resume 历史卡)——读 getSessionMessages 折叠成 turns,不起会话、
2960
+ * 无 token 成本。绝不抛错(返回空)。 */
2961
+ async readHistory(cwd, sessionId, maxTurns = 10) {
2962
+ try {
2963
+ const sdk = await loadSdk();
2964
+ const messages = await sdk.getSessionMessages(sessionId, { dir: cwd });
2965
+ return foldSessionMessages(messages, maxTurns, cwd);
2966
+ } catch (err) {
2967
+ log.fail("agent", err, { backend: "claude-agent", phase: "getSessionMessages", sessionId });
2968
+ return { turns: [], totalTurns: 0 };
2969
+ }
2970
+ }
2971
+ async startThread(opts) {
2972
+ const sdk = await loadSdk();
2973
+ return new ClaudeAgentThread({
2974
+ sessionId: randomUUID2(),
2975
+ resume: false,
2976
+ cwd: opts.cwd,
2977
+ model: opts.model,
2978
+ effort: opts.effort,
2979
+ permission: permissionOptions(opts.mode, opts.network, opts.cwd),
2980
+ systemPromptAppend: BRIDGE_DEVELOPER_INSTRUCTIONS,
2981
+ query: sdk.query
2982
+ });
2983
+ }
2984
+ async resumeThread(opts) {
2985
+ const sdk = await loadSdk();
2986
+ return new ClaudeAgentThread({
2987
+ sessionId: opts.sessionId,
2988
+ resume: true,
2989
+ cwd: opts.cwd,
2990
+ model: opts.model,
2991
+ effort: opts.effort,
2992
+ permission: permissionOptions(opts.mode, opts.network, opts.cwd),
2993
+ systemPromptAppend: BRIDGE_DEVELOPER_INSTRUCTIONS,
2994
+ query: sdk.query
2995
+ });
2996
+ }
2997
+ };
2998
+ var EFFORTS = ["low", "medium", "high", "xhigh"];
2999
+ var STATIC_MODELS2 = [
3000
+ {
3001
+ id: "claude-opus-4-8",
3002
+ displayName: "Claude Opus 4.8",
3003
+ description: "\u6700\u5F3A\uFF0C\u590D\u6742\u63A8\u7406 / \u957F\u7A0B agentic",
3004
+ supportedEfforts: EFFORTS,
3005
+ defaultEffort: "high",
3006
+ isDefault: true,
3007
+ hidden: false
3008
+ },
3009
+ {
3010
+ id: "claude-sonnet-4-6",
3011
+ displayName: "Claude Sonnet 4.6",
3012
+ description: "\u5747\u8861\uFF0C\u65E5\u5E38\u7F16\u7801",
3013
+ supportedEfforts: EFFORTS,
3014
+ defaultEffort: "medium",
3015
+ isDefault: false,
3016
+ hidden: false
3017
+ },
3018
+ {
3019
+ id: "claude-haiku-4-5",
3020
+ displayName: "Claude Haiku 4.5",
3021
+ description: "\u6700\u5FEB\uFF0C\u8F7B\u91CF\u4EFB\u52A1",
3022
+ supportedEfforts: ["low", "medium", "high"],
3023
+ defaultEffort: "low",
3024
+ isDefault: false,
3025
+ hidden: false
3026
+ }
3027
+ ];
3028
+
3029
+ // src/agent/detect.ts
3030
+ async function probeCodexAgent() {
3031
+ const entry = BACKEND_CATALOG.find((e) => e.id === "codex-appserver");
3032
+ const bin = resolveCodexBin({ force: true });
3033
+ const version = bin ? await codexVersionAsync(bin, { force: true }) : null;
3034
+ const installed = !!bin && !!version;
3035
+ return {
3036
+ id: "codex",
3037
+ displayName: "Codex",
3038
+ installed,
3039
+ version,
3040
+ installHint: installed ? void 0 : entry.dep.detectHint,
3041
+ backends: [
3042
+ {
3043
+ backendId: "codex-appserver",
3044
+ available: installed,
3045
+ reason: !bin ? entry.dep.detectHint : !version ? "codex --version \u5931\u8D25" : void 0,
3046
+ version,
3047
+ supportedModes: entry.supportedModes,
3048
+ installable: false
3049
+ }
3050
+ ]
3051
+ };
3052
+ }
3053
+ async function detectAgents() {
3054
+ return Promise.all([probeCodexAgent()]);
3055
+ }
3056
+ function pickDefaultBackend(agents) {
3057
+ const find = (id) => agents.flatMap((a) => a.backends).find((b) => b.backendId === id);
3058
+ const pickable = (id) => !catalogById(id)?.hidden && find(id)?.available;
3059
+ if (pickable("codex-appserver")) return "codex-appserver";
3060
+ return DEFAULT_BACKEND_ID;
3061
+ }
3062
+ var defaultCache;
3063
+ async function effectiveDefaultBackend(opts) {
3064
+ if (!opts?.force && defaultCache !== void 0) return defaultCache;
3065
+ try {
3066
+ defaultCache = pickDefaultBackend(await detectAgents());
3067
+ } catch {
3068
+ defaultCache = DEFAULT_BACKEND_ID;
3069
+ }
3070
+ return defaultCache;
3071
+ }
3072
+
2167
3073
  // src/agent/installer.ts
2168
3074
  import { mkdir as mkdir4, rm as rm2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
2169
3075
  import { existsSync as existsSync4 } from "fs";
@@ -2345,6 +3251,7 @@ async function rollback(bareName) {
2345
3251
  const target = join8(paths.backendsDir, "node_modules", ...bareName.split("/"));
2346
3252
  await rm2(target, { recursive: true, force: true }).catch(() => void 0);
2347
3253
  await removeBackendsDep(bareName);
3254
+ await rm2(join8(paths.backendsDir, "package-lock.json"), { force: true }).catch(() => void 0);
2348
3255
  }
2349
3256
  async function removeBackendsDep(bareName) {
2350
3257
  const pkgFile = join8(paths.backendsDir, "package.json");
@@ -2366,7 +3273,8 @@ function stripVersion(pkg) {
2366
3273
 
2367
3274
  // src/agent/index.ts
2368
3275
  var REGISTRY = /* @__PURE__ */ new Map([
2369
- ["codex-appserver", () => new CodexAppServerBackend()]
3276
+ ["codex-appserver", () => new CodexAppServerBackend()],
3277
+ ["claude-agent", () => new ClaudeAgentBackend()]
2370
3278
  ]);
2371
3279
  function backendIds() {
2372
3280
  return catalogBackendIds();
@@ -2837,7 +3745,7 @@ import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
2837
3745
 
2838
3746
  // src/project/registry.ts
2839
3747
  import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile5 } from "fs/promises";
2840
- import { randomUUID as randomUUID2 } from "crypto";
3748
+ import { randomUUID as randomUUID3 } from "crypto";
2841
3749
  import { dirname as dirname5 } from "path";
2842
3750
  function defaultNoMention(p) {
2843
3751
  return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
@@ -2882,7 +3790,7 @@ function withLock(fn) {
2882
3790
  }
2883
3791
  async function write(projects) {
2884
3792
  await mkdir5(dirname5(paths.projectsFile), { recursive: true });
2885
- const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID2()}`;
3793
+ const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID3()}`;
2886
3794
  const body = { version: FILE_VERSION2, projects };
2887
3795
  await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
2888
3796
  `, "utf8");
@@ -3281,14 +4189,14 @@ function buildHelpCard(scope, noMention = true, isAdmin2 = false, caps) {
3281
4189
  }
3282
4190
  return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
3283
4191
  }
3284
- function buildWelcomeCard(kind, docUrl, noMention = true, caps) {
4192
+ function buildWelcomeCard(kind, docUrl, noMention = true, caps, agentName = "Codex") {
3285
4193
  const showGoal = caps?.goal ?? true;
3286
4194
  const showCompact = caps?.compact ?? true;
3287
4195
  const showResume = caps?.resume ?? true;
3288
4196
  const goalLine = "\xB7 `/goal <\u76EE\u6807>` \u2192 \u81EA\u4E3B\u591A\u8F6E\u8DD1\u5230\u5B8C\u6210\uFF08\u5361\u4E0A \u23F9 \u7EC8\u6B62 / \u{1F3AF} \u7ED3\u675F\u76EE\u6807\uFF09";
3289
4197
  const ctxLine = showCompact ? "\xB7 `/context` \xB7 `/compact` \u2192 \u770B / \u538B\u7F29\u4E0A\u4E0B\u6587" : "\xB7 `/context` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4";
3290
4198
  const elements = [
3291
- md("\u{1F44B} **\u6B22\u8FCE\u4F7F\u7528 Codex Bridge** \u2014 \u672C\u7FA4\u5DF2\u7ED1\u5B9A\u4E00\u4E2A\u9879\u76EE\u76EE\u5F55\uFF0C\u5728\u7FA4\u91CC\u5C31\u80FD\u9A71\u52A8\u672C\u673A Codex \u5E72\u6D3B\u3002"),
4199
+ md(`\u{1F44B} **\u6B22\u8FCE\u4F7F\u7528 ${agentName} Bridge** \u2014 \u672C\u7FA4\u5DF2\u7ED1\u5B9A\u4E00\u4E2A\u9879\u76EE\u76EE\u5F55\uFF0C\u5728\u7FA4\u91CC\u5C31\u80FD\u9A71\u52A8\u672C\u673A ${agentName} \u5E72\u6D3B\u3002`),
3292
4200
  hr()
3293
4201
  ];
3294
4202
  if (kind === "single") {
@@ -3691,7 +4599,7 @@ function buildNewProjectFormCard(opts = {}) {
3691
4599
  ];
3692
4600
  if (backends.length > 1) {
3693
4601
  formItems.push(
3694
- note("\u{1F9E0} \u540E\u7AEF Agent\uFF08\u521B\u5EFA\u540E**\u56FA\u5B9A\u4E0D\u53EF\u5207\u6362**\uFF1B\u672A\u4E0B\u8F7D\u7684\u4E0D\u5728\u5217\u8868 \u2192 \u53BB Web\u300C\u540E\u7AEF Agent\u300D\u9875\u4E0B\u8F7D\uFF09"),
4602
+ note("\u{1F9E0} \u540E\u7AEF Agent\uFF08\u521B\u5EFA\u540E**\u56FA\u5B9A\u4E0D\u53EF\u5207\u6362**\uFF1B\u6807\u6CE8\u300C\u672A\u4E0B\u8F7D\u300D\u7684\u9700\u5148\u53BB Web\u300C\u540E\u7AEF Agent\u300D\u9875\u4E0B\u8F7D\uFF0C\u9009\u5B83\u4F1A\u63D0\u793A\uFF09"),
3695
4603
  selectMenu({ name: "backend", placeholder: "\u9009\u62E9\u540E\u7AEF Agent", options: backends, initial: backends[0]?.value })
3696
4604
  );
3697
4605
  } else if (backends.length === 1) {
@@ -6896,7 +7804,7 @@ function buildUsageShareCard(data, opts = {}) {
6896
7804
  }
6897
7805
 
6898
7806
  // src/web/discovery.ts
6899
- import { randomUUID as randomUUID3 } from "crypto";
7807
+ import { randomUUID as randomUUID4 } from "crypto";
6900
7808
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
6901
7809
  import { dirname as dirname10 } from "path";
6902
7810
  function publishWebConsole(rec, file = paths.webConsoleFile) {
@@ -6938,7 +7846,7 @@ function stableWebConsoleToken(file = paths.webTokenFile) {
6938
7846
  if (t) return t;
6939
7847
  } catch {
6940
7848
  }
6941
- const token = randomUUID3();
7849
+ const token = randomUUID4();
6942
7850
  try {
6943
7851
  mkdirSync2(dirname10(file), { recursive: true });
6944
7852
  try {
@@ -7070,6 +7978,9 @@ function welcomeCaps(backend) {
7070
7978
  return void 0;
7071
7979
  }
7072
7980
  }
7981
+ function welcomeAgentName(backend) {
7982
+ return backend && catalogById(backend)?.displayName || "Codex";
7983
+ }
7073
7984
  var HELP_DOC_URL = "https://my.feishu.cn/wiki/PZ23wGr7JiKK5RkIG4rcZXzGn5g";
7074
7985
  function sidebarPcUrl(url) {
7075
7986
  return `https://applink.feishu.cn/client/web_url/open?mode=sidebar-semi&url=${encodeURIComponent(url)}`;
@@ -7077,10 +7988,13 @@ function sidebarPcUrl(url) {
7077
7988
  async function onboardGroup(channel, project) {
7078
7989
  const kind = project.kind ?? "multi";
7079
7990
  const chatId = project.chatId;
7991
+ const agentName = welcomeAgentName(project.backend);
7080
7992
  const decorate = (project.origin ?? "created") !== "joined";
7081
7993
  try {
7082
7994
  const noMention = project.noMention ?? defaultNoMention(project);
7083
- const content = JSON.stringify(buildWelcomeCard(kind, HELP_DOC_URL || void 0, noMention, welcomeCaps(project.backend)));
7995
+ const content = JSON.stringify(
7996
+ buildWelcomeCard(kind, HELP_DOC_URL || void 0, noMention, welcomeCaps(project.backend), agentName)
7997
+ );
7084
7998
  const sent = await channel.rawClient.im.v1.message.create({
7085
7999
  params: { receive_id_type: "chat_id" },
7086
8000
  data: { receive_id: chatId, msg_type: "interactive", content }
@@ -7116,7 +8030,7 @@ async function onboardGroup(channel, project) {
7116
8030
  {
7117
8031
  chat_menu_item: {
7118
8032
  action_type: "REDIRECT_LINK",
7119
- name: "\u{1F916} Codex",
8033
+ name: `\u{1F916} ${agentName}`,
7120
8034
  redirect_link: { common_url: HELP_DOC_URL, pc_url: sidebarPcUrl(HELP_DOC_URL) }
7121
8035
  }
7122
8036
  }
@@ -7135,7 +8049,12 @@ async function onboardGroup(channel, project) {
7135
8049
  function assertBackendUsable(backend, mode) {
7136
8050
  if (!backend) return;
7137
8051
  const ok = projectCreatableBackends(mode, isBackendEntryInstalled).some((e) => e.id === backend);
7138
- if (!ok) throw new Error(`\u6240\u9009\u540E\u7AEF\u300C${backend}\u300D\u5F53\u524D\u4E0D\u53EF\u7528\uFF08\u672A\u4E0B\u8F7D\u6216\u4E0D\u652F\u6301\u8BE5\u6743\u9650\u6863\uFF09\uFF0C\u8BF7\u56DE\u5361\u7247\u91CD\u65B0\u9009\u62E9`);
8052
+ if (ok) return;
8053
+ const entry = catalogById(backend);
8054
+ if (entry && isInstallable(entry)) {
8055
+ throw new Error(`\u300C${entry.displayName}\u300D\u5C1A\u672A\u4E0B\u8F7D\u2014\u2014\u8BF7\u5230 Web \u63A7\u5236\u53F0\u300C\u540E\u7AEF Agent\u300D\u9875\u70B9\u300C\u4E0B\u8F7D\u300D\u88C5\u597D\u540E\uFF0C\u518D\u56DE\u5361\u7247\u9009\u7528\u3002`);
8056
+ }
8057
+ throw new Error(`\u6240\u9009\u540E\u7AEF\u300C${backend}\u300D\u5F53\u524D\u4E0D\u53EF\u7528\uFF08\u672A\u4E0B\u8F7D\u6216\u4E0D\u652F\u6301\u8BE5\u6743\u9650\u6863\uFF09\uFF0C\u8BF7\u56DE\u5361\u7247\u91CD\u65B0\u9009\u62E9`);
7139
8058
  }
7140
8059
  async function resolveCwd(name, existingPath) {
7141
8060
  if (existingPath) {
@@ -7228,7 +8147,7 @@ async function leaveChat(channel, chatId) {
7228
8147
 
7229
8148
  // src/bot/session-store.ts
7230
8149
  import { mkdir as mkdir11, readFile as readFile10, rename as rename5, writeFile as writeFile9 } from "fs/promises";
7231
- import { randomUUID as randomUUID4 } from "crypto";
8150
+ import { randomUUID as randomUUID5 } from "crypto";
7232
8151
  import { dirname as dirname11 } from "path";
7233
8152
  var FILE_VERSION3 = 2;
7234
8153
  var LEGACY_V1_SESSION_FIELD = "codexThreadId";
@@ -7266,7 +8185,7 @@ function withLock2(fn) {
7266
8185
  }
7267
8186
  async function write2(sessions) {
7268
8187
  await mkdir11(dirname11(paths.sessionsFile), { recursive: true });
7269
- const tmp = `${paths.sessionsFile}.tmp-${process.pid}-${randomUUID4()}`;
8188
+ const tmp = `${paths.sessionsFile}.tmp-${process.pid}-${randomUUID5()}`;
7270
8189
  const body = { version: FILE_VERSION3, sessions };
7271
8190
  await writeFile9(tmp, `${JSON.stringify(body, null, 2)}
7272
8191
  `, "utf8");
@@ -8134,7 +9053,10 @@ function asTier(v) {
8134
9053
  return v === "qa" || v === "write" || v === "full" ? v : void 0;
8135
9054
  }
8136
9055
  function backendOptionsFor(mode) {
8137
- const opts = projectCreatableBackends(mode, isBackendEntryInstalled).map((e) => ({ label: e.displayName, value: e.id }));
9056
+ const opts = visibleCatalog().filter((e) => !e.supportedModes || e.supportedModes.includes(mode)).map((e) => {
9057
+ const installed = e.id === DEFAULT_BACKEND_ID || isBackendEntryInstalled(e);
9058
+ return { label: installed ? e.displayName : `${e.displayName}\uFF08\u672A\u4E0B\u8F7D\xB7\u53BB\u63A7\u5236\u53F0\u4E0B\u8F7D\uFF09`, value: e.id };
9059
+ });
8138
9060
  return opts.length > 1 ? opts : [];
8139
9061
  }
8140
9062
  function bindModeFor(backend) {
@@ -11252,7 +12174,7 @@ async function doRestart(phase) {
11252
12174
 
11253
12175
  // src/web/server.ts
11254
12176
  import { createServer } from "http";
11255
- import { randomUUID as randomUUID5, timingSafeEqual } from "crypto";
12177
+ import { randomUUID as randomUUID6, timingSafeEqual } from "crypto";
11256
12178
  import { mkdirSync as mkdirSync3, watch } from "fs";
11257
12179
  import { open as open2, stat as stat5 } from "fs/promises";
11258
12180
  import { join as join19 } from "path";
@@ -11793,6 +12715,9 @@ var UI_HTML = `<!doctype html>
11793
12715
  .iconbtn svg { width: 15px; height: 15px; }
11794
12716
  .menu { position: absolute; top: 28px; right: 0; background: #1c1c21; border: 1px solid var(--border-2); border-radius: 10px; padding: 5px; min-width: 152px; box-shadow: var(--shadow-md); z-index: 25; display: none; }
11795
12717
  .menu.open { display: block; }
12718
+ /* \u672B\u884C\u7684\u4E0B\u62C9\u5411\u4E0A\u5C55\u5F00\uFF1A\u8868\u683C .bka-tbl \u7528 overflow:hidden \u505A\u5706\u89D2\uFF0C\u4F1A\u88C1\u6389\u6700\u540E\u4E00\u884C
12719
+ \u5411\u4E0B\u5C55\u5F00\u7684\u83DC\u5355\uFF08\u5982 Claude \u884C\u7684\u300C\u68C0\u67E5\u66F4\u65B0\u300D\u88AB\u5207\u6389\uFF09\u2014\u2014\u672B\u884C\u6539\u4E3A\u5411\u4E0A\u5F39\u51FA\u5373\u53EF\u907F\u5F00\u3002 */
12720
+ .bka-row:last-child .menu { top: auto; bottom: 30px; }
11796
12721
  .mi { display: flex; align-items: center; gap: 9px; padding: 7px 9px; border-radius: 7px; font-size: 12.5px; color: var(--text); cursor: pointer; }
11797
12722
  .mi svg { width: 14px; height: 14px; color: var(--text-2); }
11798
12723
  .mi:hover { background: var(--hover); }
@@ -13925,10 +14850,11 @@ var DEFAULT_WEB_PORT = 51847;
13925
14850
  var COOKIE_NAME = "fcb_console_token";
13926
14851
  var SSE_INITIAL_TAIL_BYTES = 16 * 1024;
13927
14852
  function createWebServer(opts) {
13928
- const token = opts.token ?? randomUUID5();
14853
+ const token = opts.token ?? randomUUID6();
13929
14854
  const html = opts.html ?? UI_HTML;
13930
14855
  const logDir = opts.logDir ?? join19(paths.appDir, "logs");
13931
14856
  const sseCleanups = /* @__PURE__ */ new Set();
14857
+ const installJobs = /* @__PURE__ */ new Map();
13932
14858
  let qrSession = null;
13933
14859
  const server = createServer((req, res) => {
13934
14860
  void handle(req, res).catch((err) => {
@@ -14277,7 +15203,7 @@ function createWebServer(opts) {
14277
15203
  function handleRegisterQrStream(req, res) {
14278
15204
  qrSession?.abort.abort();
14279
15205
  const abort = new AbortController();
14280
- const session = { id: randomUUID5(), abort };
15206
+ const session = { id: randomUUID6(), abort };
14281
15207
  qrSession = session;
14282
15208
  res.writeHead(200, {
14283
15209
  "Content-Type": "text/event-stream",
@@ -14339,35 +15265,68 @@ data: ${JSON.stringify(data)}
14339
15265
  "X-Accel-Buffering": "no"
14340
15266
  });
14341
15267
  res.write(": connected\n\n");
14342
- const heartbeat = setInterval(() => res.write(": ka\n\n"), 15e3);
14343
- const abort = new AbortController();
15268
+ const heartbeat = setInterval(() => {
15269
+ try {
15270
+ res.write(": ka\n\n");
15271
+ } catch {
15272
+ }
15273
+ }, 15e3);
14344
15274
  let ended = false;
14345
- const sendData = (data) => {
14346
- if (ended) return;
14347
- res.write(`data: ${JSON.stringify(data)}
15275
+ const conn = {
15276
+ send: (data) => {
15277
+ if (ended) return;
15278
+ try {
15279
+ res.write(`data: ${JSON.stringify(data)}
14348
15280
 
14349
15281
  `);
15282
+ } catch {
15283
+ }
15284
+ },
15285
+ done: () => {
15286
+ if (ended) return;
15287
+ ended = true;
15288
+ clearInterval(heartbeat);
15289
+ sseCleanups.delete(conn.done);
15290
+ try {
15291
+ res.end();
15292
+ } catch {
15293
+ }
15294
+ }
14350
15295
  };
14351
- const finish = () => {
14352
- if (ended) return;
14353
- ended = true;
14354
- clearInterval(heartbeat);
14355
- sseCleanups.delete(cleanup);
14356
- res.end();
14357
- };
14358
- const cleanup = () => {
14359
- abort.abort();
14360
- finish();
15296
+ sseCleanups.add(conn.done);
15297
+ const key = `${id}|${update ? 1 : 0}`;
15298
+ const running = installJobs.get(key);
15299
+ if (running && !running.settled) {
15300
+ running.buffer.forEach(conn.send);
15301
+ running.conns.add(conn);
15302
+ req.on("close", () => {
15303
+ running.conns.delete(conn);
15304
+ conn.done();
15305
+ });
15306
+ return;
15307
+ }
15308
+ const job = { buffer: [], conns: /* @__PURE__ */ new Set([conn]), abort: new AbortController(), settled: false };
15309
+ installJobs.set(key, job);
15310
+ req.on("close", () => {
15311
+ job.conns.delete(conn);
15312
+ conn.done();
15313
+ });
15314
+ const emit2 = (data) => {
15315
+ if (data.type === "log") job.buffer.push(data);
15316
+ for (const c of [...job.conns]) c.send(data);
14361
15317
  };
14362
- sseCleanups.add(cleanup);
14363
- req.on("close", cleanup);
14364
- void opts.service.installBackend(id, (chunk) => sendData({ type: "log", chunk }), abort.signal, { update }).then((result) => {
14365
- if (result.ok) sendData({ type: "done" });
14366
- else sendData({ type: "error", code: result.aborted ? "aborted" : "install_failed", message: result.tail });
15318
+ void opts.service.installBackend(id, (chunk) => emit2({ type: "log", chunk }), job.abort.signal, { update }).then((result) => {
15319
+ emit2(
15320
+ result.ok ? { type: "done" } : { type: "error", code: result.aborted ? "aborted" : "install_failed", message: result.tail }
15321
+ );
14367
15322
  }).catch((err) => {
14368
15323
  const code = err instanceof NotWiredYetError ? "not_wired_yet" : "unknown";
14369
- sendData({ type: "error", code, message: err instanceof Error ? err.message : String(err) });
14370
- }).finally(finish);
15324
+ emit2({ type: "error", code, message: err instanceof Error ? err.message : String(err) });
15325
+ }).finally(() => {
15326
+ job.settled = true;
15327
+ installJobs.delete(key);
15328
+ for (const c of [...job.conns]) c.done();
15329
+ });
14371
15330
  }
14372
15331
  async function handleDiagnosis(res, botParam) {
14373
15332
  const botId = botParam ?? await defaultBotId();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "qrcode-terminal": "^0.12.0"
35
35
  },
36
36
  "devDependencies": {
37
+ "@anthropic-ai/claude-agent-sdk": "^0.3.178",
37
38
  "@types/cross-spawn": "^6.0.6",
38
39
  "@types/node": "^22.10.0",
39
40
  "@types/qrcode-terminal": "^0.12.2",