@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.
- package/dist/cli.js +1041 -82
- 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/
|
|
2058
|
-
|
|
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
|
|
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}-${
|
|
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(
|
|
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\
|
|
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
|
|
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 =
|
|
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(
|
|
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:
|
|
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 (
|
|
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
|
|
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}-${
|
|
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 =
|
|
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
|
|
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 ??
|
|
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:
|
|
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(() =>
|
|
14343
|
-
|
|
15268
|
+
const heartbeat = setInterval(() => {
|
|
15269
|
+
try {
|
|
15270
|
+
res.write(": ka\n\n");
|
|
15271
|
+
} catch {
|
|
15272
|
+
}
|
|
15273
|
+
}, 15e3);
|
|
14344
15274
|
let ended = false;
|
|
14345
|
-
const
|
|
14346
|
-
|
|
14347
|
-
|
|
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
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
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
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
|
|
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
|
-
|
|
14370
|
-
}).finally(
|
|
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.
|
|
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",
|