@modelzen/feishu-codex-bridge 0.4.2 → 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 +1298 -181
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -127,6 +127,13 @@ function secretKeyForApp(appId) {
127
127
  function getShowToolCalls(cfg) {
128
128
  return cfg.preferences?.showToolCalls !== false;
129
129
  }
130
+ function getModelDisplay(cfg) {
131
+ const v = cfg.preferences?.showModel;
132
+ if (v === "running" || v === "always" || v === "off") return v;
133
+ if (v === true) return "always";
134
+ if (v === false) return "off";
135
+ return "running";
136
+ }
130
137
  function getMaxConcurrentRuns(cfg) {
131
138
  const raw = cfg.preferences?.maxConcurrentRuns;
132
139
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) return 10;
@@ -798,6 +805,25 @@ var BACKEND_CATALOG = [
798
805
  },
799
806
  // supportedModes undefined ⇒ 全档(qa/write/full)。
800
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"
801
827
  }
802
828
  ];
803
829
  function visibleCatalog() {
@@ -2043,7 +2069,960 @@ var STATIC_MODELS = [
2043
2069
  hidden: false,
2044
2070
  isDefault: true,
2045
2071
  supportedEfforts: ["low", "medium", "high"],
2046
- defaultEffort: "medium"
2072
+ defaultEffort: "medium"
2073
+ }
2074
+ ];
2075
+
2076
+ // src/agent/claude-agent/backend.ts
2077
+ import { randomUUID as randomUUID2 } from "crypto";
2078
+
2079
+ // src/agent/backend-loader.ts
2080
+ import { createRequire } from "module";
2081
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2082
+ import { join as join6 } from "path";
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
+ };
2092
+ function userAnchor() {
2093
+ return join6(paths.backendsDir, "__backend_anchor__.cjs");
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
+ }
2116
+ function isBackendDepInstalled(pkg) {
2117
+ try {
2118
+ createRequire(import.meta.url).resolve(pkg);
2119
+ return true;
2120
+ } catch {
2121
+ }
2122
+ try {
2123
+ createRequire(userAnchor()).resolve(pkg);
2124
+ return true;
2125
+ } catch {
2126
+ return false;
2127
+ }
2128
+ }
2129
+ function backendsBinPath(binName) {
2130
+ const dir = join6(paths.backendsDir, "node_modules", ".bin");
2131
+ const candidates = process.platform === "win32" ? [join6(dir, `${binName}.cmd`), join6(dir, binName)] : [join6(dir, binName)];
2132
+ return candidates.find((p) => existsSync3(p)) ?? null;
2133
+ }
2134
+ function isBackendBinInstalled(binName) {
2135
+ return backendsBinPath(binName) !== null;
2136
+ }
2137
+ function isBackendEntryInstalled(entry) {
2138
+ const { kind, binName, pkg } = entry.dep;
2139
+ if (kind === "external-cli") return false;
2140
+ if (binName) return isBackendBinInstalled(binName);
2141
+ return pkg ? isBackendDepInstalled(pkg) : false;
2142
+ }
2143
+ function isBackendInstalledInUserDir(entry) {
2144
+ const { binName, pkg } = entry.dep;
2145
+ if (binName) return isBackendBinInstalled(binName);
2146
+ if (!pkg) return false;
2147
+ try {
2148
+ createRequire(userAnchor()).resolve(pkg);
2149
+ return true;
2150
+ } catch {
2151
+ return false;
2152
+ }
2153
+ }
2154
+ function installedBackendVersion(pkg) {
2155
+ const readVer = (file) => {
2156
+ try {
2157
+ const j = JSON.parse(readFileSync2(file, "utf8"));
2158
+ return typeof j.version === "string" ? j.version : null;
2159
+ } catch {
2160
+ return null;
2161
+ }
2162
+ };
2163
+ const inUser = join6(paths.backendsDir, "node_modules", ...pkg.split("/"), "package.json");
2164
+ const v1 = readVer(inUser);
2165
+ if (v1) return v1;
2166
+ try {
2167
+ const resolved = createRequire(import.meta.url).resolve(`${pkg}/package.json`);
2168
+ return readVer(resolved);
2169
+ } catch {
2170
+ return null;
2171
+ }
2172
+ }
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
2047
3026
  }
2048
3027
  ];
2049
3028
 
@@ -2091,72 +3070,6 @@ async function effectiveDefaultBackend(opts) {
2091
3070
  return defaultCache;
2092
3071
  }
2093
3072
 
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
-
2160
3073
  // src/agent/installer.ts
2161
3074
  import { mkdir as mkdir4, rm as rm2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
2162
3075
  import { existsSync as existsSync4 } from "fs";
@@ -2338,6 +3251,7 @@ async function rollback(bareName) {
2338
3251
  const target = join8(paths.backendsDir, "node_modules", ...bareName.split("/"));
2339
3252
  await rm2(target, { recursive: true, force: true }).catch(() => void 0);
2340
3253
  await removeBackendsDep(bareName);
3254
+ await rm2(join8(paths.backendsDir, "package-lock.json"), { force: true }).catch(() => void 0);
2341
3255
  }
2342
3256
  async function removeBackendsDep(bareName) {
2343
3257
  const pkgFile = join8(paths.backendsDir, "package.json");
@@ -2359,7 +3273,8 @@ function stripVersion(pkg) {
2359
3273
 
2360
3274
  // src/agent/index.ts
2361
3275
  var REGISTRY = /* @__PURE__ */ new Map([
2362
- ["codex-appserver", () => new CodexAppServerBackend()]
3276
+ ["codex-appserver", () => new CodexAppServerBackend()],
3277
+ ["claude-agent", () => new ClaudeAgentBackend()]
2363
3278
  ]);
2364
3279
  function backendIds() {
2365
3280
  return catalogBackendIds();
@@ -2830,7 +3745,7 @@ import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
2830
3745
 
2831
3746
  // src/project/registry.ts
2832
3747
  import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile5 } from "fs/promises";
2833
- import { randomUUID as randomUUID2 } from "crypto";
3748
+ import { randomUUID as randomUUID3 } from "crypto";
2834
3749
  import { dirname as dirname5 } from "path";
2835
3750
  function defaultNoMention(p) {
2836
3751
  return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
@@ -2875,7 +3790,7 @@ function withLock(fn) {
2875
3790
  }
2876
3791
  async function write(projects) {
2877
3792
  await mkdir5(dirname5(paths.projectsFile), { recursive: true });
2878
- const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID2()}`;
3793
+ const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID3()}`;
2879
3794
  const body = { version: FILE_VERSION2, projects };
2880
3795
  await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
2881
3796
  `, "utf8");
@@ -3274,14 +4189,14 @@ function buildHelpCard(scope, noMention = true, isAdmin2 = false, caps) {
3274
4189
  }
3275
4190
  return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
3276
4191
  }
3277
- function buildWelcomeCard(kind, docUrl, noMention = true, caps) {
4192
+ function buildWelcomeCard(kind, docUrl, noMention = true, caps, agentName = "Codex") {
3278
4193
  const showGoal = caps?.goal ?? true;
3279
4194
  const showCompact = caps?.compact ?? true;
3280
4195
  const showResume = caps?.resume ?? true;
3281
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";
3282
4197
  const ctxLine = showCompact ? "\xB7 `/context` \xB7 `/compact` \u2192 \u770B / \u538B\u7F29\u4E0A\u4E0B\u6587" : "\xB7 `/context` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4";
3283
4198
  const elements = [
3284
- 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`),
3285
4200
  hr()
3286
4201
  ];
3287
4202
  if (kind === "single") {
@@ -3347,6 +4262,7 @@ var DM = {
3347
4262
  rmDo: "dm.rmDo",
3348
4263
  rmCancel: "dm.rmCancel",
3349
4264
  setTools: "dm.set.tools",
4265
+ setShowModel: "dm.set.showModel",
3350
4266
  setWatchdog: "dm.set.watchdog",
3351
4267
  // 假死超时「自定义…」:watchdogCustom 打开输入卡,watchdogCustomSubmit 保存任意秒数
3352
4268
  watchdogCustom: "dm.set.watchdog.custom",
@@ -3503,6 +4419,17 @@ function buildUpdateCard(state) {
3503
4419
  );
3504
4420
  }
3505
4421
  }
4422
+ function buildReconnectCard(conn) {
4423
+ const template = conn === "connected" ? "green" : "orange";
4424
+ return card(
4425
+ [
4426
+ md(`\u957F\u8FDE\u63A5\u72B6\u6001\uFF1A**${conn}**`),
4427
+ note("SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8BF7\u5728\u7EC8\u7AEF\u91CD\u8DD1 `feishu-codex-bridge run`\uFF08\u524D\u53F0\uFF09\u6216 `feishu-codex-bridge restart`\uFF08\u540E\u53F0\u5B88\u62A4\uFF09\u3002"),
4428
+ backToMenu()
4429
+ ],
4430
+ { header: { title: "\u{1F504} \u957F\u8FDE\u63A5", template } }
4431
+ );
4432
+ }
3506
4433
  function connLabel(state) {
3507
4434
  switch (state) {
3508
4435
  case "connected":
@@ -3672,7 +4599,7 @@ function buildNewProjectFormCard(opts = {}) {
3672
4599
  ];
3673
4600
  if (backends.length > 1) {
3674
4601
  formItems.push(
3675
- 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"),
3676
4603
  selectMenu({ name: "backend", placeholder: "\u9009\u62E9\u540E\u7AEF Agent", options: backends, initial: backends[0]?.value })
3677
4604
  );
3678
4605
  } else if (backends.length === 1) {
@@ -3809,38 +4736,78 @@ function optionRow(label, actionId, current, opts) {
3809
4736
  actions(opts.map((o) => button(o.label, { a: actionId, v: o.value }, o.value === current ? "primary" : "default")))
3810
4737
  ];
3811
4738
  }
4739
+ function settingSection(title) {
4740
+ return { tag: "markdown", content: `**${title}**`, text_size: "notation", text_color: "grey" };
4741
+ }
4742
+ function settingItem(name, desc, actionId, current, opts) {
4743
+ return [
4744
+ md(`**${name}**`),
4745
+ note(desc),
4746
+ actions(opts.map((o) => button(o.label, { a: actionId, v: o.value }, o.value === current ? "primary" : "default")))
4747
+ ];
4748
+ }
3812
4749
  function buildSettingsCard(cfg) {
3813
4750
  const watchdogSec = cfg.preferences?.runIdleTimeoutSeconds ?? 120;
3814
4751
  return card(
3815
4752
  [
3816
- md("**\u5168\u5C40\u8BBE\u7F6E**\uFF08\u7BA1\u7406\u5458\uFF09"),
3817
- ...optionRow("\u{1F527} \u5DE5\u5177\u8C03\u7528", DM.setTools, getShowToolCalls(cfg) ? "on" : "off", [
3818
- { label: "\u663E\u793A", value: "on" },
3819
- { label: "\u9690\u85CF", value: "off" }
3820
- ]),
3821
- md(`\u23F1 \u5047\u6B7B\u8D85\u65F6\uFF08\u5F53\u524D **${watchdogSec === 0 ? "\u5173\u95ED" : `${watchdogSec} \u79D2`}**\uFF09`),
4753
+ settingSection("\u{1F4E4} \u8F93\u51FA\u5C55\u793A"),
4754
+ ...settingItem(
4755
+ "\u{1F527} \u5DE5\u5177\u8C03\u7528",
4756
+ "\u8F93\u51FA\u65F6\u663E\u793A\u6267\u884C\u7684\u547D\u4EE4 / \u5DE5\u5177\u8C03\u7528\uFF1B\u5173\u6389\u53EA\u770B\u6700\u7EC8\u56DE\u7B54\u3002",
4757
+ DM.setTools,
4758
+ getShowToolCalls(cfg) ? "on" : "off",
4759
+ [
4760
+ { label: "\u663E\u793A", value: "on" },
4761
+ { label: "\u9690\u85CF", value: "off" }
4762
+ ]
4763
+ ),
4764
+ ...settingItem(
4765
+ "\u{1F9E0} \u6A21\u578B\u663E\u793A",
4766
+ "\u6BCF\u6761\u56DE\u590D\u53F3\u4E0B\u89D2\u663E\u793A\u300C\u6A21\u578B \xB7 \u63A8\u7406\u5F3A\u5EA6\u300D\u3002\u4EC5\u8F93\u51FA\u65F6\uFF1D\u53EA\u5728\u751F\u6210\u4E2D\u663E\u793A\uFF1B\u59CB\u7EC8\uFF1D\u751F\u6210\u5B8C\u540E\u5361\u7247\u4E5F\u4FDD\u7559\u3002",
4767
+ DM.setShowModel,
4768
+ getModelDisplay(cfg),
4769
+ [
4770
+ { label: "\u5173\u95ED", value: "off" },
4771
+ { label: "\u4EC5\u8F93\u51FA\u65F6", value: "running" },
4772
+ { label: "\u59CB\u7EC8", value: "always" }
4773
+ ]
4774
+ ),
4775
+ hr(),
4776
+ settingSection("\u23F1 \u8FD0\u884C\u63A7\u5236"),
4777
+ md(`**\u23F1 \u5047\u6B7B\u8D85\u65F6** \xB7 \u5F53\u524D **${watchdogSec === 0 ? "\u5173\u95ED" : `${watchdogSec} \u79D2`}**`),
4778
+ note("\u591A\u4E45\u6CA1\u6709\u4EFB\u4F55\u8F93\u51FA\u5C31\u81EA\u52A8\u7EC8\u6B62\u672C\u8F6E\uFF08\u9632\u5361\u6B7B\uFF09\u3002"),
3822
4779
  actions([
3823
4780
  ...[0, 120, 300].map(
3824
4781
  (v) => button(v === 0 ? "\u5173\u95ED" : `${v}\u79D2`, { a: DM.setWatchdog, v: String(v) }, v === watchdogSec ? "primary" : "default")
3825
4782
  ),
3826
4783
  button("\u81EA\u5B9A\u4E49\u2026", { a: DM.watchdogCustom })
3827
4784
  ]),
3828
- ...optionRow("\u{1F4E5} \u8FD0\u884C\u4E2D\u65B0\u6D88\u606F", DM.setPending, getPendingPolicy(cfg), [
3829
- { label: "\u5F15\u5BFC", value: "steer" },
3830
- { label: "\u6392\u961F", value: "queue" }
3831
- ]),
3832
- ...optionRow("\u26A1 \u5E76\u53D1\u4E0A\u9650", DM.setConcurrency, String(getMaxConcurrentRuns(cfg)), [
3833
- { label: "1", value: "1" },
3834
- { label: "5", value: "5" },
3835
- { label: "10", value: "10" },
3836
- { label: "20", value: "20" }
3837
- ]),
3838
- note("\u26A1 \u5E76\u53D1\u6C60\u4E3A**\u6240\u6709\u7FA4/\u8BDD\u9898\u5168\u5C40\u5171\u4EAB**\uFF1A\u6EE1\u4E86\u65B0\u4EFB\u52A1\u6309\u5148\u6765\u540E\u5230\u6392\u961F\uFF08\u6392\u961F\u5361\u53EF\u89C1\u3001\u53EF \u23F9 \u53D6\u6D88\uFF09\u3002"),
3839
- note("\u26A0\uFE0F \u5E76\u53D1\u4E0A\u9650 \u6539\u540E\u9700**\u91CD\u542F**\u751F\u6548\uFF1B\u5176\u4F59\u8BBE\u7F6E\uFF08\u542B\u5047\u6B7B\u8D85\u65F6\uFF09\u5373\u65F6\u751F\u6548\uFF0C\u6240\u6709\u7FA4\u7ACB\u5373\u5957\u7528\u3002"),
4785
+ ...settingItem(
4786
+ "\u{1F4E5} \u8FD0\u884C\u4E2D\u6765\u65B0\u6D88\u606F",
4787
+ "\u6B63\u5728\u8DD1\u65F6\u4F60\u53C8\u53D1\u6D88\u606F\uFF1A\u5F15\u5BFC\uFF1D\u63D2\u8FDB\u5F53\u524D\u8F6E\u7EA0\u504F\uFF1B\u6392\u961F\uFF1D\u7B49\u8FD9\u8F6E\u8DD1\u5B8C\u518D\u5904\u7406\u3002",
4788
+ DM.setPending,
4789
+ getPendingPolicy(cfg),
4790
+ [
4791
+ { label: "\u5F15\u5BFC", value: "steer" },
4792
+ { label: "\u6392\u961F", value: "queue" }
4793
+ ]
4794
+ ),
4795
+ ...settingItem(
4796
+ "\u26A1 \u5E76\u53D1\u4E0A\u9650",
4797
+ "\u6240\u6709\u7FA4 / \u8BDD\u9898\u5168\u5C40\u540C\u65F6\u6700\u591A\u8DD1\u51E0\u4E2A\uFF0C\u6EE1\u4E86\u6392\u961F\uFF08\u6392\u961F\u5361\u53EF \u23F9 \u53D6\u6D88\uFF09\u3002\u6539\u540E\u9700\u91CD\u542F\u751F\u6548\u3002",
4798
+ DM.setConcurrency,
4799
+ String(getMaxConcurrentRuns(cfg)),
4800
+ [
4801
+ { label: "1", value: "1" },
4802
+ { label: "5", value: "5" },
4803
+ { label: "10", value: "10" },
4804
+ { label: "20", value: "20" }
4805
+ ]
4806
+ ),
3840
4807
  hr(),
3841
4808
  actions([button("\u{1F46E} \u7BA1\u7406\u5458", { a: DM.admins }), button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
3842
4809
  ],
3843
- { header: { title: "\u2699\uFE0F \u8BBE\u7F6E", template: "blue" } }
4810
+ { header: { title: "\u2699\uFE0F \u5168\u5C40\u8BBE\u7F6E", template: "blue" } }
3844
4811
  );
3845
4812
  }
3846
4813
  function buildWatchdogCustomCard(cfg) {
@@ -4904,24 +5871,6 @@ function buildRunCard(rc) {
4904
5871
  }
4905
5872
  function renderRunning(state, rc) {
4906
5873
  const elements = [];
4907
- if (rc.cardKey && rc.goalControls) {
4908
- if (rc.goalEnding) {
4909
- elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")], CONTROLS_EID));
4910
- elements.push(noteMd("_\u{1F3AF} \u76EE\u6807\u5DF2\u89E3\u9664\uFF0C\u672C\u8F6E\u8F93\u51FA\u5B8C\u6210\u540E\u505C\u6B62_"));
4911
- } else {
4912
- elements.push(
4913
- actions(
4914
- [
4915
- button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger"),
4916
- button("\u{1F3AF} \u7ED3\u675F\u76EE\u6807", { a: RC.endGoal, m: rc.cardKey }, "default")
4917
- ],
4918
- CONTROLS_EID
4919
- )
4920
- );
4921
- }
4922
- } else if (rc.cardKey && !rc.hideStop) {
4923
- elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")], CONTROLS_EID));
4924
- }
4925
5874
  const reasoning = reasoningContent(state);
4926
5875
  if (reasoning) elements.push(reasoningPanel(reasoning, state.reasoningActive));
4927
5876
  const showTools = rc.showTools !== false;
@@ -4937,9 +5886,30 @@ function renderRunning(state, rc) {
4937
5886
  if (tools.length > 0) elements.push(...renderToolGroup(tools, false));
4938
5887
  const answer = textParts.join("\n\n");
4939
5888
  if (answer) elements.push(mdStream(answer, ANSWER_EID));
4940
- if (state.footer) elements.push(footerStatus(state.footer));
5889
+ const mEl = modelEl(rc);
5890
+ if (state.footer && mEl) elements.push(splitRow(footerStatus(state.footer), mEl));
5891
+ else if (state.footer) elements.push(footerStatus(state.footer));
5892
+ else if (mEl) elements.push(mEl);
4941
5893
  const gauge = gaugeEl(state);
4942
5894
  if (gauge) elements.push(gauge);
5895
+ if (rc.cardKey && rc.goalControls) {
5896
+ if (rc.goalEnding) {
5897
+ elements.push(noteMd("_\u{1F3AF} \u76EE\u6807\u5DF2\u89E3\u9664\uFF0C\u672C\u8F6E\u8F93\u51FA\u5B8C\u6210\u540E\u505C\u6B62_"));
5898
+ elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")], CONTROLS_EID));
5899
+ } else {
5900
+ elements.push(
5901
+ actions(
5902
+ [
5903
+ button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger"),
5904
+ button("\u{1F3AF} \u7ED3\u675F\u76EE\u6807", { a: RC.endGoal, m: rc.cardKey }, "default")
5905
+ ],
5906
+ CONTROLS_EID
5907
+ )
5908
+ );
5909
+ }
5910
+ } else if (rc.cardKey && !rc.hideStop) {
5911
+ elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")], CONTROLS_EID));
5912
+ }
4943
5913
  return elements;
4944
5914
  }
4945
5915
  function renderTerminal(state, rc) {
@@ -4977,6 +5947,8 @@ function renderTerminal(state, rc) {
4977
5947
  }
4978
5948
  const gauge = gaugeEl(state);
4979
5949
  if (gauge) elements.push(gauge);
5950
+ const mEl = rc.modelOnTerminal ? modelEl(rc) : null;
5951
+ if (mEl) elements.push(mEl);
4980
5952
  return elements;
4981
5953
  }
4982
5954
  function errorAdvice(msg) {
@@ -5098,9 +6070,35 @@ function collapsedToolSummary(tools, finalized) {
5098
6070
  body: tools.map((t) => `- ${toolHeaderText(t)}`).join("\n")
5099
6071
  });
5100
6072
  }
6073
+ function footerStatusText(status) {
6074
+ return status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : status === "retrying" ? "\u26A0\uFE0F \u77AC\u65AD\uFF0C\u81EA\u52A8\u91CD\u8BD5\u4E2D\u2026" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
6075
+ }
5101
6076
  function footerStatus(status) {
5102
- const text = status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : status === "retrying" ? "\u26A0\uFE0F \u77AC\u65AD\uFF0C\u81EA\u52A8\u91CD\u8BD5\u4E2D\u2026" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
5103
- return noteMd(text);
6077
+ return noteMd(footerStatusText(status));
6078
+ }
6079
+ var EFFORT_TIER = {
6080
+ none: { label: "\u65E0", color: "grey" },
6081
+ minimal: { label: "\u6781\u7B80", color: "grey" },
6082
+ low: { label: "\u4F4E", color: "yellow" },
6083
+ medium: { label: "\u4E2D", color: "green" },
6084
+ high: { label: "\u9AD8", color: "violet" },
6085
+ xhigh: { label: "\u6781\u9AD8", color: "purple" }
6086
+ };
6087
+ function modelEffortMd(model, effort) {
6088
+ if (!effort) return model;
6089
+ const t = EFFORT_TIER[effort];
6090
+ if (!t) return `${model} \xB7 ${effort}`;
6091
+ return `${model} \xB7 <font color='${t.color}'>${t.label}</font>`;
6092
+ }
6093
+ function modelEl(rc) {
6094
+ if (!rc.model) return null;
6095
+ return {
6096
+ tag: "markdown",
6097
+ content: modelEffortMd(rc.model, rc.effort),
6098
+ text_size: "notation",
6099
+ text_color: "grey",
6100
+ text_align: "right"
6101
+ };
5104
6102
  }
5105
6103
  function summaryText(state) {
5106
6104
  if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
@@ -6806,7 +7804,7 @@ function buildUsageShareCard(data, opts = {}) {
6806
7804
  }
6807
7805
 
6808
7806
  // src/web/discovery.ts
6809
- import { randomUUID as randomUUID3 } from "crypto";
7807
+ import { randomUUID as randomUUID4 } from "crypto";
6810
7808
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
6811
7809
  import { dirname as dirname10 } from "path";
6812
7810
  function publishWebConsole(rec, file = paths.webConsoleFile) {
@@ -6848,7 +7846,7 @@ function stableWebConsoleToken(file = paths.webTokenFile) {
6848
7846
  if (t) return t;
6849
7847
  } catch {
6850
7848
  }
6851
- const token = randomUUID3();
7849
+ const token = randomUUID4();
6852
7850
  try {
6853
7851
  mkdirSync2(dirname10(file), { recursive: true });
6854
7852
  try {
@@ -6980,6 +7978,9 @@ function welcomeCaps(backend) {
6980
7978
  return void 0;
6981
7979
  }
6982
7980
  }
7981
+ function welcomeAgentName(backend) {
7982
+ return backend && catalogById(backend)?.displayName || "Codex";
7983
+ }
6983
7984
  var HELP_DOC_URL = "https://my.feishu.cn/wiki/PZ23wGr7JiKK5RkIG4rcZXzGn5g";
6984
7985
  function sidebarPcUrl(url) {
6985
7986
  return `https://applink.feishu.cn/client/web_url/open?mode=sidebar-semi&url=${encodeURIComponent(url)}`;
@@ -6987,10 +7988,13 @@ function sidebarPcUrl(url) {
6987
7988
  async function onboardGroup(channel, project) {
6988
7989
  const kind = project.kind ?? "multi";
6989
7990
  const chatId = project.chatId;
7991
+ const agentName = welcomeAgentName(project.backend);
6990
7992
  const decorate = (project.origin ?? "created") !== "joined";
6991
7993
  try {
6992
7994
  const noMention = project.noMention ?? defaultNoMention(project);
6993
- 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
+ );
6994
7998
  const sent = await channel.rawClient.im.v1.message.create({
6995
7999
  params: { receive_id_type: "chat_id" },
6996
8000
  data: { receive_id: chatId, msg_type: "interactive", content }
@@ -7026,7 +8030,7 @@ async function onboardGroup(channel, project) {
7026
8030
  {
7027
8031
  chat_menu_item: {
7028
8032
  action_type: "REDIRECT_LINK",
7029
- name: "\u{1F916} Codex",
8033
+ name: `\u{1F916} ${agentName}`,
7030
8034
  redirect_link: { common_url: HELP_DOC_URL, pc_url: sidebarPcUrl(HELP_DOC_URL) }
7031
8035
  }
7032
8036
  }
@@ -7045,7 +8049,12 @@ async function onboardGroup(channel, project) {
7045
8049
  function assertBackendUsable(backend, mode) {
7046
8050
  if (!backend) return;
7047
8051
  const ok = projectCreatableBackends(mode, isBackendEntryInstalled).some((e) => e.id === backend);
7048
- 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`);
7049
8058
  }
7050
8059
  async function resolveCwd(name, existingPath) {
7051
8060
  if (existingPath) {
@@ -7138,7 +8147,7 @@ async function leaveChat(channel, chatId) {
7138
8147
 
7139
8148
  // src/bot/session-store.ts
7140
8149
  import { mkdir as mkdir11, readFile as readFile10, rename as rename5, writeFile as writeFile9 } from "fs/promises";
7141
- import { randomUUID as randomUUID4 } from "crypto";
8150
+ import { randomUUID as randomUUID5 } from "crypto";
7142
8151
  import { dirname as dirname11 } from "path";
7143
8152
  var FILE_VERSION3 = 2;
7144
8153
  var LEGACY_V1_SESSION_FIELD = "codexThreadId";
@@ -7176,7 +8185,7 @@ function withLock2(fn) {
7176
8185
  }
7177
8186
  async function write2(sessions) {
7178
8187
  await mkdir11(dirname11(paths.sessionsFile), { recursive: true });
7179
- const tmp = `${paths.sessionsFile}.tmp-${process.pid}-${randomUUID4()}`;
8188
+ const tmp = `${paths.sessionsFile}.tmp-${process.pid}-${randomUUID5()}`;
7180
8189
  const body = { version: FILE_VERSION3, sessions };
7181
8190
  await writeFile9(tmp, `${JSON.stringify(body, null, 2)}
7182
8191
  `, "utf8");
@@ -8044,7 +9053,10 @@ function asTier(v) {
8044
9053
  return v === "qa" || v === "write" || v === "full" ? v : void 0;
8045
9054
  }
8046
9055
  function backendOptionsFor(mode) {
8047
- 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
+ });
8048
9060
  return opts.length > 1 ? opts : [];
8049
9061
  }
8050
9062
  function bindModeFor(backend) {
@@ -9034,6 +10046,35 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
9034
10046
  }
9035
10047
  return buildProjectListCard(projects, byChat);
9036
10048
  };
10049
+ const buildDoctorInfo = async () => {
10050
+ const codexProbe = await backend.doctor({ force: true });
10051
+ const app = cfg.accounts.app;
10052
+ const secret = await getSecret(secretKeyForApp(app.id)).catch(() => void 0);
10053
+ const scopeCheck = secret ? await validateAppCredentials(app.id, secret, app.tenant).catch(() => void 0) : void 0;
10054
+ const missingScopes = scopeCheck?.missingScopes;
10055
+ const missingJoinScopes = scopeCheck?.missingJoinScopes;
10056
+ return {
10057
+ codexOk: codexProbe.ok,
10058
+ codexVer: codexProbe.version,
10059
+ conn: channel.getConnectionStatus?.()?.state ?? "unknown",
10060
+ bridgeVer: bridgeVersion(),
10061
+ node: process.version,
10062
+ platform: `${process.platform}-${process.arch}`,
10063
+ logStdout: serviceStdoutPath(),
10064
+ logStderr: serviceStderrPath(),
10065
+ configFile: paths.configFile,
10066
+ missingScopes,
10067
+ // 缺失时预选缺失项(精准开通);查不到/全开通时预选全部必需 scope 供核对。
10068
+ scopeGrantUrl: buildScopeGrantUrl(
10069
+ app.id,
10070
+ app.tenant,
10071
+ missingScopes && missingScopes.length ? missingScopes : void 0
10072
+ ),
10073
+ missingJoinScopes,
10074
+ // 「加入存量群」按钮恒预选这两项 opt-in scope(它们不在必需清单里)。
10075
+ joinScopeGrantUrl: buildScopeGrantUrl(app.id, app.tenant, JOIN_GROUP_SCOPES)
10076
+ };
10077
+ };
9037
10078
  dispatcher.on(DM.menu, ({ evt }) => {
9038
10079
  if (dmAdmin(evt.operator?.openId)) freshMenu(evt);
9039
10080
  }).on(DM.newProject, ({ evt }) => {
@@ -9098,41 +10139,15 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
9098
10139
  if (dmAdmin(evt.operator?.openId)) await patch(evt, buildSettingsCard(cfg));
9099
10140
  }).on(DM.doctor, async ({ evt }) => {
9100
10141
  if (!dmAdmin(evt.operator?.openId)) return;
9101
- const codexProbe = await backend.doctor({ force: true });
9102
- const app = cfg.accounts.app;
9103
- const secret = await getSecret(secretKeyForApp(app.id)).catch(() => void 0);
9104
- const scopeCheck = secret ? await validateAppCredentials(app.id, secret, app.tenant).catch(() => void 0) : void 0;
9105
- const missingScopes = scopeCheck?.missingScopes;
9106
- const missingJoinScopes = scopeCheck?.missingJoinScopes;
9107
- const info = {
9108
- codexOk: codexProbe.ok,
9109
- codexVer: codexProbe.version,
9110
- conn: channel.getConnectionStatus?.()?.state ?? "unknown",
9111
- bridgeVer: bridgeVersion(),
9112
- node: process.version,
9113
- platform: `${process.platform}-${process.arch}`,
9114
- logStdout: serviceStdoutPath(),
9115
- logStderr: serviceStderrPath(),
9116
- configFile: paths.configFile,
9117
- missingScopes,
9118
- // 缺失时预选缺失项(精准开通);查不到/全开通时预选全部必需 scope 供核对。
9119
- scopeGrantUrl: buildScopeGrantUrl(
9120
- app.id,
9121
- app.tenant,
9122
- missingScopes && missingScopes.length ? missingScopes : void 0
9123
- ),
9124
- missingJoinScopes,
9125
- // 「加入存量群」按钮恒预选这两项 opt-in scope(它们不在必需清单里)。
9126
- joinScopeGrantUrl: buildScopeGrantUrl(app.id, app.tenant, JOIN_GROUP_SCOPES)
9127
- };
9128
- await sendManagedCard(channel, evt.chatId, buildDoctorCard(info), evt.messageId).catch(
10142
+ await sendManagedCard(channel, evt.chatId, buildDoctorCard(await buildDoctorInfo()), evt.messageId).catch(
9129
10143
  (err) => log.fail("console", err, { cmd: "doctor" })
9130
10144
  );
9131
10145
  }).on(DM.reconnect, async ({ evt }) => {
9132
10146
  if (!dmAdmin(evt.operator?.openId)) return;
9133
10147
  const conn = channel.getConnectionStatus?.()?.state ?? "unknown";
9134
- await channel.send(evt.chatId, { markdown: `\u{1F504} \u957F\u8FDE\u63A5\u72B6\u6001\uFF1A**${conn}**
9135
- SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8BF7\u5728\u7EC8\u7AEF\u91CD\u8DD1 \`feishu-codex-bridge run\`\uFF08\u524D\u53F0\uFF09\u6216 \`feishu-codex-bridge restart\`\uFF08\u540E\u53F0\u5B88\u62A4\uFF09\u3002` }, { replyTo: evt.messageId }).catch(() => void 0);
10148
+ await sendManagedCard(channel, evt.chatId, buildReconnectCard(conn), evt.messageId).catch(
10149
+ (err) => log.fail("console", err, { cmd: "reconnect" })
10150
+ );
9136
10151
  }).on(DM.update, ({ evt }) => {
9137
10152
  if (!dmAdmin(evt.operator?.openId)) return;
9138
10153
  void (async () => {
@@ -9239,6 +10254,10 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
9239
10254
  });
9240
10255
  }).on(DM.setTools, ({ evt, value }) => {
9241
10256
  applyPref(evt, (p) => p.showToolCalls = value.v === "on");
10257
+ }).on(DM.setShowModel, ({ evt, value }) => {
10258
+ applyPref(evt, (p) => {
10259
+ p.showModel = value.v === "running" ? "running" : value.v === "always" ? "always" : "off";
10260
+ });
9242
10261
  }).on(DM.setWatchdog, ({ evt, value }) => {
9243
10262
  const n = Number(value.v);
9244
10263
  if (Number.isFinite(n)) applyPref(evt, (p) => p.runIdleTimeoutSeconds = n);
@@ -9553,6 +10572,7 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
9553
10572
  firstRec = void 0;
9554
10573
  const turnModel = rec?.model ?? opts.model;
9555
10574
  const turnEffort = rec?.effort ?? opts.effort;
10575
+ const modelDisp = getModelDisplay(cfg);
9556
10576
  const run = opts.thread.runStreamed(turnInput, { model: turnModel, effort: turnEffort });
9557
10577
  const turnStartAt = Date.now();
9558
10578
  state.run = run;
@@ -9562,7 +10582,9 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
9562
10582
  const rc = {
9563
10583
  rs: render.snapshot(),
9564
10584
  requesterOpenId: opts.requesterOpenId,
9565
- showTools: render.showTools
10585
+ showTools: render.showTools,
10586
+ // 模型显示档位:footnote 本轮 model·推理强度;always 档终态卡也保留。
10587
+ ...modelDisp !== "off" && turnModel ? { model: turnModel, effort: turnEffort, modelOnTerminal: modelDisp === "always" } : {}
9566
10588
  };
9567
10589
  const adoptThreadId = async (messageId) => {
9568
10590
  if (activeKey.startsWith("pending:")) {
@@ -9841,7 +10863,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
9841
10863
  const startTurn = () => {
9842
10864
  const render = new RunRender();
9843
10865
  render.showTools = getShowToolCalls(cfg);
9844
- const rc = { rs: render.snapshot(), requesterOpenId: opts.requesterOpenId, showTools: render.showTools, goalControls: true };
10866
+ const goalModelDisp = getModelDisplay(cfg);
10867
+ const rc = {
10868
+ rs: render.snapshot(),
10869
+ requesterOpenId: opts.requesterOpenId,
10870
+ showTools: render.showTools,
10871
+ goalControls: true,
10872
+ ...goalModelDisp !== "off" && opts.model ? { model: opts.model, effort: opts.effort, modelOnTerminal: goalModelDisp === "always" } : {}
10873
+ };
9845
10874
  return { render, rc, stream: null, cardMsgId: null };
9846
10875
  };
9847
10876
  const ensureCard = async (ctx) => {
@@ -10208,9 +11237,60 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
10208
11237
  }).catch(() => void 0);
10209
11238
  return;
10210
11239
  }
10211
- await sendManagedCard(channel, op, buildDmMenuCard({ webConsoleUrl: webConsoleUrl(), version: bridgeVersion() }), void 0, false, "open_id").catch(
10212
- (err) => log.fail("console", err, { cmd: "menu-card" })
10213
- );
11240
+ const sendDm = (c) => sendManagedCard(channel, op, c, void 0, false, "open_id");
11241
+ try {
11242
+ switch (evt.eventKey) {
11243
+ case DM.newProject:
11244
+ await sendDm(buildNewProjectFormCard({ backends: backendOptionsFor("full") }));
11245
+ break;
11246
+ case DM.projects:
11247
+ await sendDm(await renderProjectList());
11248
+ break;
11249
+ case DM.settings:
11250
+ await sendDm(buildSettingsCard(cfg));
11251
+ break;
11252
+ case DM.doctor:
11253
+ await sendDm(buildDoctorCard(await buildDoctorInfo()));
11254
+ break;
11255
+ case DM.reconnect:
11256
+ await sendDm(buildReconnectCard(channel.getConnectionStatus?.()?.state ?? "unknown"));
11257
+ break;
11258
+ case DM.usage: {
11259
+ const { messageId } = await sendDm(buildUsageCard({ phase: "loading" }));
11260
+ let state;
11261
+ try {
11262
+ state = { phase: "ready", data: await fetchUsageBundle(false) };
11263
+ } catch (e) {
11264
+ log.fail("console", e, { phase: "usage", via: "menu" });
11265
+ state = {
11266
+ phase: "error",
11267
+ kind: e instanceof UsageError ? e.kind : "transient",
11268
+ message: e instanceof Error ? e.message : String(e)
11269
+ };
11270
+ }
11271
+ const ok = await updateManagedCard(channel, messageId, buildUsageCard(state)).catch(() => false);
11272
+ if (!ok) await sendDm(buildUsageCard(state));
11273
+ break;
11274
+ }
11275
+ case DM.update: {
11276
+ const { messageId } = await sendDm(buildUpdateCard({ phase: "checking" }));
11277
+ const current = currentVersion();
11278
+ const latest = await latestVersion().catch(() => null);
11279
+ const hasUpdate = !!latest && isNewer(latest, current);
11280
+ log.info("console", "update-check", { current, latest, hasUpdate, via: "menu" });
11281
+ await updateManagedCard(
11282
+ channel,
11283
+ messageId,
11284
+ buildUpdateCard({ phase: "checked", current, latest, hasUpdate, dev: isDevSource() })
11285
+ ).catch((e) => log.fail("console", e, { phase: "update-check", via: "menu" }));
11286
+ break;
11287
+ }
11288
+ default:
11289
+ await sendDm(buildDmMenuCard({ webConsoleUrl: webConsoleUrl(), version: bridgeVersion() }));
11290
+ }
11291
+ } catch (err) {
11292
+ log.fail("console", err, { cmd: "menu-card", key: evt.eventKey });
11293
+ }
10214
11294
  };
10215
11295
  const reaper = setInterval(() => {
10216
11296
  const now = Date.now();
@@ -11094,7 +12174,7 @@ async function doRestart(phase) {
11094
12174
 
11095
12175
  // src/web/server.ts
11096
12176
  import { createServer } from "http";
11097
- import { randomUUID as randomUUID5, timingSafeEqual } from "crypto";
12177
+ import { randomUUID as randomUUID6, timingSafeEqual } from "crypto";
11098
12178
  import { mkdirSync as mkdirSync3, watch } from "fs";
11099
12179
  import { open as open2, stat as stat5 } from "fs/promises";
11100
12180
  import { join as join19 } from "path";
@@ -11635,6 +12715,9 @@ var UI_HTML = `<!doctype html>
11635
12715
  .iconbtn svg { width: 15px; height: 15px; }
11636
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; }
11637
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; }
11638
12721
  .mi { display: flex; align-items: center; gap: 9px; padding: 7px 9px; border-radius: 7px; font-size: 12.5px; color: var(--text); cursor: pointer; }
11639
12722
  .mi svg { width: 14px; height: 14px; color: var(--text-2); }
11640
12723
  .mi:hover { background: var(--hover); }
@@ -13767,10 +14850,11 @@ var DEFAULT_WEB_PORT = 51847;
13767
14850
  var COOKIE_NAME = "fcb_console_token";
13768
14851
  var SSE_INITIAL_TAIL_BYTES = 16 * 1024;
13769
14852
  function createWebServer(opts) {
13770
- const token = opts.token ?? randomUUID5();
14853
+ const token = opts.token ?? randomUUID6();
13771
14854
  const html = opts.html ?? UI_HTML;
13772
14855
  const logDir = opts.logDir ?? join19(paths.appDir, "logs");
13773
14856
  const sseCleanups = /* @__PURE__ */ new Set();
14857
+ const installJobs = /* @__PURE__ */ new Map();
13774
14858
  let qrSession = null;
13775
14859
  const server = createServer((req, res) => {
13776
14860
  void handle(req, res).catch((err) => {
@@ -14119,7 +15203,7 @@ function createWebServer(opts) {
14119
15203
  function handleRegisterQrStream(req, res) {
14120
15204
  qrSession?.abort.abort();
14121
15205
  const abort = new AbortController();
14122
- const session = { id: randomUUID5(), abort };
15206
+ const session = { id: randomUUID6(), abort };
14123
15207
  qrSession = session;
14124
15208
  res.writeHead(200, {
14125
15209
  "Content-Type": "text/event-stream",
@@ -14181,35 +15265,68 @@ data: ${JSON.stringify(data)}
14181
15265
  "X-Accel-Buffering": "no"
14182
15266
  });
14183
15267
  res.write(": connected\n\n");
14184
- const heartbeat = setInterval(() => res.write(": ka\n\n"), 15e3);
14185
- const abort = new AbortController();
15268
+ const heartbeat = setInterval(() => {
15269
+ try {
15270
+ res.write(": ka\n\n");
15271
+ } catch {
15272
+ }
15273
+ }, 15e3);
14186
15274
  let ended = false;
14187
- const sendData = (data) => {
14188
- if (ended) return;
14189
- 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)}
14190
15280
 
14191
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
+ }
14192
15295
  };
14193
- const finish = () => {
14194
- if (ended) return;
14195
- ended = true;
14196
- clearInterval(heartbeat);
14197
- sseCleanups.delete(cleanup);
14198
- res.end();
14199
- };
14200
- const cleanup = () => {
14201
- abort.abort();
14202
- 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);
14203
15317
  };
14204
- sseCleanups.add(cleanup);
14205
- req.on("close", cleanup);
14206
- void opts.service.installBackend(id, (chunk) => sendData({ type: "log", chunk }), abort.signal, { update }).then((result) => {
14207
- if (result.ok) sendData({ type: "done" });
14208
- 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
+ );
14209
15322
  }).catch((err) => {
14210
15323
  const code = err instanceof NotWiredYetError ? "not_wired_yet" : "unknown";
14211
- sendData({ type: "error", code, message: err instanceof Error ? err.message : String(err) });
14212
- }).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
+ });
14213
15330
  }
14214
15331
  async function handleDiagnosis(res, botParam) {
14215
15332
  const botId = botParam ?? await defaultBotId();