chatccc 0.2.116 → 0.2.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -14
- package/package.json +1 -1
- package/src/__tests__/orchestrator.test.ts +1 -1
- package/src/adapters/adapter-interface.ts +7 -0
- package/src/adapters/claude-adapter.ts +498 -495
- package/src/adapters/cursor-adapter.ts +11 -1
- package/src/cards.ts +2 -0
- package/src/config.ts +18 -0
- package/src/orchestrator.ts +160 -15
- package/src/session-chat-binding.ts +2 -0
- package/src/session.ts +12 -2
|
@@ -1,496 +1,499 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// claude-adapter.ts — Claude CLI 适配器
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// 通过直接 spawn claude.exe(不再使用 @anthropic-ai/claude-agent-sdk JS API)
|
|
5
|
-
// 以确保 --mcp-config 参数能正确传递给 CLI 子进程,加载用户 MCP 服务器。
|
|
6
|
-
// =============================================================================
|
|
7
|
-
|
|
8
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
9
|
-
import { createInterface } from "node:readline";
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
-
import { join, dirname } from "node:path";
|
|
12
|
-
import { homedir } from "node:os";
|
|
13
|
-
import { createRequire } from "node:module";
|
|
14
|
-
|
|
15
|
-
import type {
|
|
16
|
-
ToolAdapter,
|
|
17
|
-
UnifiedBlock,
|
|
18
|
-
UnifiedStreamMessage,
|
|
19
|
-
CreateSessionResult,
|
|
20
|
-
SessionInfo,
|
|
21
|
-
ToolPromptOptions,
|
|
22
|
-
} from "./adapter-interface.ts";
|
|
23
|
-
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
24
|
-
import {
|
|
25
|
-
defaultClaudeSessionMetaStore,
|
|
26
|
-
type ClaudeSessionMetaStore,
|
|
27
|
-
} from "./claude-session-meta-store.ts";
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// 常量
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/** 根据当前平台动态解析 claude-agent-sdk 二进制路径 */
|
|
34
|
-
function resolveClaudeBinary(): string {
|
|
35
|
-
const platform = process.platform; // 'win32', 'linux', 'darwin'
|
|
36
|
-
const arch = process.arch; // 'x64', 'arm64'
|
|
37
|
-
|
|
38
|
-
// 通过 Node 模块解析找到 SDK 主包目录,确保测试和生产环境一致
|
|
39
|
-
const _require = createRequire(import.meta.url);
|
|
40
|
-
const sdkMainDir = dirname(_require.resolve("@anthropic-ai/claude-agent-sdk"));
|
|
41
|
-
|
|
42
|
-
// 读取 manifest.json 获取各平台二进制文件名
|
|
43
|
-
let platforms: Record<string, { binary: string }> | null = null;
|
|
44
|
-
try {
|
|
45
|
-
const manifest = JSON.parse(
|
|
46
|
-
readFileSync(join(sdkMainDir, "manifest.json"), "utf-8"),
|
|
47
|
-
) as { platforms?: Record<string, { binary: string }> };
|
|
48
|
-
platforms = manifest.platforms ?? null;
|
|
49
|
-
} catch { /* manifest 缺失时用默认规则推断 */ }
|
|
50
|
-
|
|
51
|
-
// Linux 上需区分 musl / glibc,先检测 musl 变体
|
|
52
|
-
const candidates: string[] = [];
|
|
53
|
-
if (platform === "linux") {
|
|
54
|
-
candidates.push(`${platform}-${arch}-musl`, `${platform}-${arch}`);
|
|
55
|
-
} else {
|
|
56
|
-
candidates.push(`${platform}-${arch}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (const triple of candidates) {
|
|
60
|
-
const binaryName = platforms?.[triple]?.binary
|
|
61
|
-
?? (platform === "win32" ? "claude.exe" : "claude");
|
|
62
|
-
const pkgDir = join(sdkMainDir, "..", `claude-agent-sdk-${triple}`);
|
|
63
|
-
if (!existsSync(pkgDir)) continue;
|
|
64
|
-
const exePath = join(pkgDir, binaryName);
|
|
65
|
-
if (existsSync(exePath)) return exePath;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
throw new Error(
|
|
69
|
-
`No Claude CLI binary found for platform ${platform}-${arch}. ` +
|
|
70
|
-
`Tried: ${candidates.map((c) => `@anthropic-ai/claude-agent-sdk-${c}`).join(", ")}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const CLAUDE_EXE = resolveClaudeBinary();
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// 类型别名(CLI JSONL 消息格式,与旧 SDK 消息格式兼容)
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
interface SdkContentBlock {
|
|
81
|
-
type: string;
|
|
82
|
-
text?: string;
|
|
83
|
-
thinking?: string;
|
|
84
|
-
name?: string;
|
|
85
|
-
input?: unknown;
|
|
86
|
-
tool_use_id?: string;
|
|
87
|
-
content?: unknown;
|
|
88
|
-
is_error?: boolean;
|
|
89
|
-
query?: string;
|
|
90
|
-
[key: string]: unknown;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface SdkMessageLike {
|
|
94
|
-
type?: string;
|
|
95
|
-
subtype?: string;
|
|
96
|
-
message?: { content?: SdkContentBlock[] };
|
|
97
|
-
compact_metadata?: {
|
|
98
|
-
trigger?: "manual" | "auto";
|
|
99
|
-
pre_tokens?: number;
|
|
100
|
-
post_tokens?: number;
|
|
101
|
-
};
|
|
102
|
-
session_id?: string;
|
|
103
|
-
model?: string;
|
|
104
|
-
cwd?: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// ClaudeAdapterOptions
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
|
|
111
|
-
export interface ClaudeAdapterOptions {
|
|
112
|
-
model: string;
|
|
113
|
-
subagentModel?: string;
|
|
114
|
-
effort: string;
|
|
115
|
-
/** Anthropic API Key(选填,留空则不注入环境变量) */
|
|
116
|
-
apiKey?: string;
|
|
117
|
-
/** Anthropic 兼容 API Base URL(选填,留空则不注入环境变量) */
|
|
118
|
-
baseUrl?: string;
|
|
119
|
-
/** 判断字段是否为"不传给 CLI"的占位(空字符串/全空白) */
|
|
120
|
-
isEmpty: (value: string) => boolean;
|
|
121
|
-
/** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
|
|
122
|
-
metaStore?: ClaudeSessionMetaStore;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
// buildCliEnv — 为 CLI 子进程构造 env(仅子进程级别,不污染主进程)
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
|
|
129
|
-
function buildCliEnv(
|
|
130
|
-
subagentModel: string | undefined,
|
|
131
|
-
apiKey: string | undefined,
|
|
132
|
-
baseUrl: string | undefined,
|
|
133
|
-
): Record<string, string | undefined> | undefined {
|
|
134
|
-
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
135
|
-
const apiKeyTrim = (apiKey ?? "").trim();
|
|
136
|
-
const baseUrlTrim = (baseUrl ?? "").trim();
|
|
137
|
-
|
|
138
|
-
if (!subagentModelTrim && !apiKeyTrim && !baseUrlTrim) return undefined;
|
|
139
|
-
|
|
140
|
-
const env: Record<string, string | undefined> = { ...process.env };
|
|
141
|
-
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
142
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
143
|
-
delete env.ANTHROPIC_MODEL;
|
|
144
|
-
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
145
|
-
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
146
|
-
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
147
|
-
delete env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
148
|
-
|
|
149
|
-
if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
|
|
150
|
-
if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
|
|
151
|
-
if (baseUrlTrim) env.ANTHROPIC_BASE_URL = baseUrlTrim;
|
|
152
|
-
|
|
153
|
-
return env;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// MCP 配置读取
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
function readMcpConfigJson(): string | null {
|
|
161
|
-
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
162
|
-
try {
|
|
163
|
-
if (!existsSync(settingsPath)) return null;
|
|
164
|
-
const raw = readFileSync(settingsPath, "utf-8");
|
|
165
|
-
const settings = JSON.parse(raw);
|
|
166
|
-
const mcpServers = settings?.mcpServers;
|
|
167
|
-
if (!mcpServers || Object.keys(mcpServers).length === 0) return null;
|
|
168
|
-
// CLI 期望 {"mcpServers": {...}} 格式,而非裸 mcpServers 值
|
|
169
|
-
return JSON.stringify({ mcpServers });
|
|
170
|
-
} catch {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// 诊断日志
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
function logMcpConfig(): void {
|
|
180
|
-
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
181
|
-
const ts = new Date().toISOString();
|
|
182
|
-
try {
|
|
183
|
-
if (!existsSync(settingsPath)) {
|
|
184
|
-
console.log(`[${ts}] [MCP-DIAG] settings.json not found at ${settingsPath}`);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const raw = readFileSync(settingsPath, "utf-8");
|
|
188
|
-
const settings = JSON.parse(raw);
|
|
189
|
-
const mcpServers = settings?.mcpServers;
|
|
190
|
-
if (!mcpServers || Object.keys(mcpServers).length === 0) {
|
|
191
|
-
console.log(`[${ts}] [MCP-DIAG] No mcpServers configured in settings.json`);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
console.log(`[${ts}] [MCP-DIAG] mcpServers found: ${JSON.stringify(Object.keys(mcpServers))}`);
|
|
195
|
-
for (const [name, cfg] of Object.entries(mcpServers) as [string, { type?: string; command?: string; args?: string[] }][]) {
|
|
196
|
-
console.log(`[${ts}] [MCP-DIAG] ${name}: type=${cfg.type}, command=${cfg.command}, args=${JSON.stringify(cfg.args)}`);
|
|
197
|
-
}
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.log(`[${ts}] [MCP-DIAG] Failed to read settings.json: ${(err as Error).message}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
// normalizeSdkMessage — CLI JSONL 消息 → UnifiedStreamMessage | null
|
|
205
|
-
// (CLI 与 SDK 使用相同 JSONL 格式,导出名保留以兼容现有测试)
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage | null {
|
|
209
|
-
if (
|
|
210
|
-
(msg.type === "assistant" || msg.type === "user") &&
|
|
211
|
-
msg.message?.content
|
|
212
|
-
) {
|
|
213
|
-
const blocks: UnifiedBlock[] = [];
|
|
214
|
-
for (const block of msg.message.content) {
|
|
215
|
-
if (block.type === "thinking" && block.thinking) {
|
|
216
|
-
blocks.push({ type: "thinking", thinking: block.thinking });
|
|
217
|
-
} else if (block.type === "tool_use") {
|
|
218
|
-
blocks.push({
|
|
219
|
-
type: "tool_use",
|
|
220
|
-
id: (block as { id?: string }).id,
|
|
221
|
-
name: block.name ?? "unknown",
|
|
222
|
-
input: block.input,
|
|
223
|
-
});
|
|
224
|
-
} else if (block.type === "tool_result") {
|
|
225
|
-
blocks.push({
|
|
226
|
-
type: "tool_result",
|
|
227
|
-
tool_use_id: block.tool_use_id ?? "",
|
|
228
|
-
content: block.content,
|
|
229
|
-
is_error: block.is_error,
|
|
230
|
-
});
|
|
231
|
-
} else if (block.type === "redacted_thinking") {
|
|
232
|
-
blocks.push({ type: "redacted_thinking" });
|
|
233
|
-
} else if (block.type === "search_result") {
|
|
234
|
-
blocks.push({
|
|
235
|
-
type: "search_result",
|
|
236
|
-
query: block.query ?? "",
|
|
237
|
-
});
|
|
238
|
-
} else if (block.type === "text" && block.text) {
|
|
239
|
-
// 跳过 user 消息中的 text block:--replay-user-messages 会重放
|
|
240
|
-
// 之前的用户消息(含内嵌的 IM skill prompt),这些不应出现在最终回复中
|
|
241
|
-
if (msg.type === "user") continue;
|
|
242
|
-
blocks.push({ type: "text", text: block.text });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return { type: msg.type, blocks };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
249
|
-
const meta = msg.compact_metadata;
|
|
250
|
-
if (!meta) return null;
|
|
251
|
-
return {
|
|
252
|
-
type: "system",
|
|
253
|
-
blocks: [
|
|
254
|
-
{
|
|
255
|
-
type: "compact_boundary",
|
|
256
|
-
trigger: meta.trigger ?? "auto",
|
|
257
|
-
pre_tokens: meta.pre_tokens ?? 0,
|
|
258
|
-
post_tokens: meta.post_tokens,
|
|
259
|
-
},
|
|
260
|
-
],
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
// CLI spawn helpers
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
|
|
271
|
-
function buildCliArgs(
|
|
272
|
-
model: string,
|
|
273
|
-
effort: string,
|
|
274
|
-
isEmpty: (value: string) => boolean,
|
|
275
|
-
mcpConfigJson: string | null,
|
|
276
|
-
extraArgs: string[],
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"
|
|
282
|
-
"--
|
|
283
|
-
"--
|
|
284
|
-
"--
|
|
285
|
-
"--
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
// ---------------------------------------------------------------------------
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
private
|
|
382
|
-
private
|
|
383
|
-
private
|
|
384
|
-
private
|
|
385
|
-
private
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
this.
|
|
391
|
-
this.
|
|
392
|
-
this.
|
|
393
|
-
this.
|
|
394
|
-
this.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
proc.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// claude-adapter.ts — Claude CLI 适配器
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 通过直接 spawn claude.exe(不再使用 @anthropic-ai/claude-agent-sdk JS API)
|
|
5
|
+
// 以确保 --mcp-config 参数能正确传递给 CLI 子进程,加载用户 MCP 服务器。
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ToolAdapter,
|
|
17
|
+
UnifiedBlock,
|
|
18
|
+
UnifiedStreamMessage,
|
|
19
|
+
CreateSessionResult,
|
|
20
|
+
SessionInfo,
|
|
21
|
+
ToolPromptOptions,
|
|
22
|
+
} from "./adapter-interface.ts";
|
|
23
|
+
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
24
|
+
import {
|
|
25
|
+
defaultClaudeSessionMetaStore,
|
|
26
|
+
type ClaudeSessionMetaStore,
|
|
27
|
+
} from "./claude-session-meta-store.ts";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// 常量
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** 根据当前平台动态解析 claude-agent-sdk 二进制路径 */
|
|
34
|
+
function resolveClaudeBinary(): string {
|
|
35
|
+
const platform = process.platform; // 'win32', 'linux', 'darwin'
|
|
36
|
+
const arch = process.arch; // 'x64', 'arm64'
|
|
37
|
+
|
|
38
|
+
// 通过 Node 模块解析找到 SDK 主包目录,确保测试和生产环境一致
|
|
39
|
+
const _require = createRequire(import.meta.url);
|
|
40
|
+
const sdkMainDir = dirname(_require.resolve("@anthropic-ai/claude-agent-sdk"));
|
|
41
|
+
|
|
42
|
+
// 读取 manifest.json 获取各平台二进制文件名
|
|
43
|
+
let platforms: Record<string, { binary: string }> | null = null;
|
|
44
|
+
try {
|
|
45
|
+
const manifest = JSON.parse(
|
|
46
|
+
readFileSync(join(sdkMainDir, "manifest.json"), "utf-8"),
|
|
47
|
+
) as { platforms?: Record<string, { binary: string }> };
|
|
48
|
+
platforms = manifest.platforms ?? null;
|
|
49
|
+
} catch { /* manifest 缺失时用默认规则推断 */ }
|
|
50
|
+
|
|
51
|
+
// Linux 上需区分 musl / glibc,先检测 musl 变体
|
|
52
|
+
const candidates: string[] = [];
|
|
53
|
+
if (platform === "linux") {
|
|
54
|
+
candidates.push(`${platform}-${arch}-musl`, `${platform}-${arch}`);
|
|
55
|
+
} else {
|
|
56
|
+
candidates.push(`${platform}-${arch}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const triple of candidates) {
|
|
60
|
+
const binaryName = platforms?.[triple]?.binary
|
|
61
|
+
?? (platform === "win32" ? "claude.exe" : "claude");
|
|
62
|
+
const pkgDir = join(sdkMainDir, "..", `claude-agent-sdk-${triple}`);
|
|
63
|
+
if (!existsSync(pkgDir)) continue;
|
|
64
|
+
const exePath = join(pkgDir, binaryName);
|
|
65
|
+
if (existsSync(exePath)) return exePath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new Error(
|
|
69
|
+
`No Claude CLI binary found for platform ${platform}-${arch}. ` +
|
|
70
|
+
`Tried: ${candidates.map((c) => `@anthropic-ai/claude-agent-sdk-${c}`).join(", ")}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const CLAUDE_EXE = resolveClaudeBinary();
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// 类型别名(CLI JSONL 消息格式,与旧 SDK 消息格式兼容)
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
interface SdkContentBlock {
|
|
81
|
+
type: string;
|
|
82
|
+
text?: string;
|
|
83
|
+
thinking?: string;
|
|
84
|
+
name?: string;
|
|
85
|
+
input?: unknown;
|
|
86
|
+
tool_use_id?: string;
|
|
87
|
+
content?: unknown;
|
|
88
|
+
is_error?: boolean;
|
|
89
|
+
query?: string;
|
|
90
|
+
[key: string]: unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface SdkMessageLike {
|
|
94
|
+
type?: string;
|
|
95
|
+
subtype?: string;
|
|
96
|
+
message?: { content?: SdkContentBlock[] };
|
|
97
|
+
compact_metadata?: {
|
|
98
|
+
trigger?: "manual" | "auto";
|
|
99
|
+
pre_tokens?: number;
|
|
100
|
+
post_tokens?: number;
|
|
101
|
+
};
|
|
102
|
+
session_id?: string;
|
|
103
|
+
model?: string;
|
|
104
|
+
cwd?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// ClaudeAdapterOptions
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export interface ClaudeAdapterOptions {
|
|
112
|
+
model: string;
|
|
113
|
+
subagentModel?: string;
|
|
114
|
+
effort: string;
|
|
115
|
+
/** Anthropic API Key(选填,留空则不注入环境变量) */
|
|
116
|
+
apiKey?: string;
|
|
117
|
+
/** Anthropic 兼容 API Base URL(选填,留空则不注入环境变量) */
|
|
118
|
+
baseUrl?: string;
|
|
119
|
+
/** 判断字段是否为"不传给 CLI"的占位(空字符串/全空白) */
|
|
120
|
+
isEmpty: (value: string) => boolean;
|
|
121
|
+
/** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
|
|
122
|
+
metaStore?: ClaudeSessionMetaStore;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// buildCliEnv — 为 CLI 子进程构造 env(仅子进程级别,不污染主进程)
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function buildCliEnv(
|
|
130
|
+
subagentModel: string | undefined,
|
|
131
|
+
apiKey: string | undefined,
|
|
132
|
+
baseUrl: string | undefined,
|
|
133
|
+
): Record<string, string | undefined> | undefined {
|
|
134
|
+
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
135
|
+
const apiKeyTrim = (apiKey ?? "").trim();
|
|
136
|
+
const baseUrlTrim = (baseUrl ?? "").trim();
|
|
137
|
+
|
|
138
|
+
if (!subagentModelTrim && !apiKeyTrim && !baseUrlTrim) return undefined;
|
|
139
|
+
|
|
140
|
+
const env: Record<string, string | undefined> = { ...process.env };
|
|
141
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
142
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
143
|
+
delete env.ANTHROPIC_MODEL;
|
|
144
|
+
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
145
|
+
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
146
|
+
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
147
|
+
delete env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
148
|
+
|
|
149
|
+
if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
|
|
150
|
+
if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
|
|
151
|
+
if (baseUrlTrim) env.ANTHROPIC_BASE_URL = baseUrlTrim;
|
|
152
|
+
|
|
153
|
+
return env;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// MCP 配置读取
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
function readMcpConfigJson(): string | null {
|
|
161
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
162
|
+
try {
|
|
163
|
+
if (!existsSync(settingsPath)) return null;
|
|
164
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
165
|
+
const settings = JSON.parse(raw);
|
|
166
|
+
const mcpServers = settings?.mcpServers;
|
|
167
|
+
if (!mcpServers || Object.keys(mcpServers).length === 0) return null;
|
|
168
|
+
// CLI 期望 {"mcpServers": {...}} 格式,而非裸 mcpServers 值
|
|
169
|
+
return JSON.stringify({ mcpServers });
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// 诊断日志
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
function logMcpConfig(): void {
|
|
180
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
181
|
+
const ts = new Date().toISOString();
|
|
182
|
+
try {
|
|
183
|
+
if (!existsSync(settingsPath)) {
|
|
184
|
+
console.log(`[${ts}] [MCP-DIAG] settings.json not found at ${settingsPath}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
188
|
+
const settings = JSON.parse(raw);
|
|
189
|
+
const mcpServers = settings?.mcpServers;
|
|
190
|
+
if (!mcpServers || Object.keys(mcpServers).length === 0) {
|
|
191
|
+
console.log(`[${ts}] [MCP-DIAG] No mcpServers configured in settings.json`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
console.log(`[${ts}] [MCP-DIAG] mcpServers found: ${JSON.stringify(Object.keys(mcpServers))}`);
|
|
195
|
+
for (const [name, cfg] of Object.entries(mcpServers) as [string, { type?: string; command?: string; args?: string[] }][]) {
|
|
196
|
+
console.log(`[${ts}] [MCP-DIAG] ${name}: type=${cfg.type}, command=${cfg.command}, args=${JSON.stringify(cfg.args)}`);
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.log(`[${ts}] [MCP-DIAG] Failed to read settings.json: ${(err as Error).message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// normalizeSdkMessage — CLI JSONL 消息 → UnifiedStreamMessage | null
|
|
205
|
+
// (CLI 与 SDK 使用相同 JSONL 格式,导出名保留以兼容现有测试)
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage | null {
|
|
209
|
+
if (
|
|
210
|
+
(msg.type === "assistant" || msg.type === "user") &&
|
|
211
|
+
msg.message?.content
|
|
212
|
+
) {
|
|
213
|
+
const blocks: UnifiedBlock[] = [];
|
|
214
|
+
for (const block of msg.message.content) {
|
|
215
|
+
if (block.type === "thinking" && block.thinking) {
|
|
216
|
+
blocks.push({ type: "thinking", thinking: block.thinking });
|
|
217
|
+
} else if (block.type === "tool_use") {
|
|
218
|
+
blocks.push({
|
|
219
|
+
type: "tool_use",
|
|
220
|
+
id: (block as { id?: string }).id,
|
|
221
|
+
name: block.name ?? "unknown",
|
|
222
|
+
input: block.input,
|
|
223
|
+
});
|
|
224
|
+
} else if (block.type === "tool_result") {
|
|
225
|
+
blocks.push({
|
|
226
|
+
type: "tool_result",
|
|
227
|
+
tool_use_id: block.tool_use_id ?? "",
|
|
228
|
+
content: block.content,
|
|
229
|
+
is_error: block.is_error,
|
|
230
|
+
});
|
|
231
|
+
} else if (block.type === "redacted_thinking") {
|
|
232
|
+
blocks.push({ type: "redacted_thinking" });
|
|
233
|
+
} else if (block.type === "search_result") {
|
|
234
|
+
blocks.push({
|
|
235
|
+
type: "search_result",
|
|
236
|
+
query: block.query ?? "",
|
|
237
|
+
});
|
|
238
|
+
} else if (block.type === "text" && block.text) {
|
|
239
|
+
// 跳过 user 消息中的 text block:--replay-user-messages 会重放
|
|
240
|
+
// 之前的用户消息(含内嵌的 IM skill prompt),这些不应出现在最终回复中
|
|
241
|
+
if (msg.type === "user") continue;
|
|
242
|
+
blocks.push({ type: "text", text: block.text });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { type: msg.type, blocks };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
249
|
+
const meta = msg.compact_metadata;
|
|
250
|
+
if (!meta) return null;
|
|
251
|
+
return {
|
|
252
|
+
type: "system",
|
|
253
|
+
blocks: [
|
|
254
|
+
{
|
|
255
|
+
type: "compact_boundary",
|
|
256
|
+
trigger: meta.trigger ?? "auto",
|
|
257
|
+
pre_tokens: meta.pre_tokens ?? 0,
|
|
258
|
+
post_tokens: meta.post_tokens,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// CLI spawn helpers
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
function buildCliArgs(
|
|
272
|
+
model: string,
|
|
273
|
+
effort: string,
|
|
274
|
+
isEmpty: (value: string) => boolean,
|
|
275
|
+
mcpConfigJson: string | null,
|
|
276
|
+
extraArgs: string[],
|
|
277
|
+
permissionMode?: "plan" | "ask",
|
|
278
|
+
): string[] {
|
|
279
|
+
const permMode = permissionMode === "plan" ? "plan" : "bypassPermissions";
|
|
280
|
+
const args = [
|
|
281
|
+
"-p",
|
|
282
|
+
"--output-format", "stream-json",
|
|
283
|
+
"--verbose",
|
|
284
|
+
"--setting-sources", "user,project,local",
|
|
285
|
+
"--permission-mode", permMode,
|
|
286
|
+
"--dangerously-skip-permissions",
|
|
287
|
+
"--settings", "{\"maxTurns\":0}",
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
if (!isEmpty(model)) args.push("--model", model);
|
|
291
|
+
if (!isEmpty(effort)) args.push("--effort", effort);
|
|
292
|
+
|
|
293
|
+
// extraArgs(如 prompt "ok" 或 --resume <id>)必须在 --mcp-config 之前,
|
|
294
|
+
// 因为 --mcp-config 接受多个空格分隔的值,会把后续非 flag 参数都当作 MCP 配置
|
|
295
|
+
args.push(...extraArgs);
|
|
296
|
+
|
|
297
|
+
if (mcpConfigJson) args.push("--mcp-config", mcpConfigJson);
|
|
298
|
+
|
|
299
|
+
const ts = new Date().toISOString();
|
|
300
|
+
const safeArgs = args.filter(a => a !== mcpConfigJson);
|
|
301
|
+
console.log(`[${ts}] [CLAUDE-CLI] spawn: ${CLAUDE_EXE} ${safeArgs.join(" ")}`);
|
|
302
|
+
if (mcpConfigJson) {
|
|
303
|
+
console.log(`[${ts}] [CLAUDE-CLI] --mcp-config: ${mcpConfigJson}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return args;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function spawnCli(
|
|
310
|
+
args: string[],
|
|
311
|
+
cwd: string,
|
|
312
|
+
env: Record<string, string | undefined> | undefined,
|
|
313
|
+
pipeStdin: boolean,
|
|
314
|
+
): ChildProcess {
|
|
315
|
+
const proc = spawn(CLAUDE_EXE, args, {
|
|
316
|
+
cwd,
|
|
317
|
+
stdio: [pipeStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
318
|
+
windowsHide: true,
|
|
319
|
+
env: env ?? process.env,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const ts = new Date().toISOString();
|
|
323
|
+
console.log(`[${ts}] [CLAUDE-CLI] spawned, pid=${proc.pid}`);
|
|
324
|
+
|
|
325
|
+
let stderr = "";
|
|
326
|
+
proc.stderr!.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
|
|
327
|
+
proc.on("close", (code) => {
|
|
328
|
+
if (stderr.trim()) {
|
|
329
|
+
const ts2 = new Date().toISOString();
|
|
330
|
+
console.log(`[${ts2}] [CLAUDE-STDERR] exit=${code}: ${stderr.trim().slice(0, 2000)}`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return proc;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function* readJsonLines(
|
|
338
|
+
proc: ChildProcess,
|
|
339
|
+
signal?: AbortSignal,
|
|
340
|
+
): AsyncGenerator<SdkMessageLike> {
|
|
341
|
+
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
342
|
+
const onAbort = () => { rl.close(); };
|
|
343
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
344
|
+
let lineCount = 0;
|
|
345
|
+
try {
|
|
346
|
+
for await (const line of rl) {
|
|
347
|
+
if (signal?.aborted) break;
|
|
348
|
+
lineCount++;
|
|
349
|
+
const trimmed = line.trim();
|
|
350
|
+
if (!trimmed) continue;
|
|
351
|
+
try {
|
|
352
|
+
yield JSON.parse(trimmed) as SdkMessageLike;
|
|
353
|
+
} catch { /* 非 JSON 行静默跳过 */ }
|
|
354
|
+
}
|
|
355
|
+
} finally {
|
|
356
|
+
signal?.removeEventListener("abort", onAbort);
|
|
357
|
+
rl.close();
|
|
358
|
+
const ts = new Date().toISOString();
|
|
359
|
+
console.log(`[${ts}] [CLAUDE-CLI] readJsonLines done: ${lineCount} raw lines`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** 构造 stream-json 格式的 stdin 输入行 */
|
|
364
|
+
function buildStreamJsonInput(text: string): string {
|
|
365
|
+
return JSON.stringify({
|
|
366
|
+
type: "user",
|
|
367
|
+
message: {
|
|
368
|
+
role: "user",
|
|
369
|
+
content: [{ type: "text", text }],
|
|
370
|
+
},
|
|
371
|
+
}) + "\n";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// ClaudeAdapter
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
class ClaudeAdapter implements ToolAdapter {
|
|
379
|
+
readonly displayName = "Claude Code";
|
|
380
|
+
readonly sessionDescPrefix = "Claude Code Session:";
|
|
381
|
+
private model: string;
|
|
382
|
+
private effort: string;
|
|
383
|
+
private subagentModel: string | undefined;
|
|
384
|
+
private apiKey: string | undefined;
|
|
385
|
+
private baseUrl: string | undefined;
|
|
386
|
+
private isEmpty: (value: string) => boolean;
|
|
387
|
+
private metaStore: ClaudeSessionMetaStore;
|
|
388
|
+
|
|
389
|
+
constructor(options: ClaudeAdapterOptions) {
|
|
390
|
+
this.model = options.model;
|
|
391
|
+
this.effort = options.effort;
|
|
392
|
+
this.subagentModel = options.subagentModel;
|
|
393
|
+
this.apiKey = options.apiKey;
|
|
394
|
+
this.baseUrl = options.baseUrl;
|
|
395
|
+
this.isEmpty = options.isEmpty;
|
|
396
|
+
this.metaStore = options.metaStore ?? defaultClaudeSessionMetaStore;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
400
|
+
logMcpConfig();
|
|
401
|
+
const mcpConfigJson = readMcpConfigJson();
|
|
402
|
+
const env = buildCliEnv(this.subagentModel, this.apiKey, this.baseUrl);
|
|
403
|
+
const args = buildCliArgs(
|
|
404
|
+
this.model, this.effort, this.isEmpty, mcpConfigJson,
|
|
405
|
+
["ok"],
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const proc = spawnCli(args, cwd, env, false);
|
|
409
|
+
|
|
410
|
+
for await (const msg of readJsonLines(proc)) {
|
|
411
|
+
if (msg.session_id) {
|
|
412
|
+
const sessionId = msg.session_id;
|
|
413
|
+
await this.metaStore.set(sessionId, { cwd }).catch(() => {});
|
|
414
|
+
await killProcessTree(proc.pid);
|
|
415
|
+
const ts = new Date().toISOString();
|
|
416
|
+
console.log(`[${ts}] [CLAUDE-CLI] createSession: ${sessionId}`);
|
|
417
|
+
return { sessionId };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await killProcessTree(proc.pid);
|
|
422
|
+
throw new Error("No session ID in Claude init event");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async *prompt(
|
|
426
|
+
sessionId: string,
|
|
427
|
+
userText: string,
|
|
428
|
+
cwd: string,
|
|
429
|
+
signal?: AbortSignal,
|
|
430
|
+
options?: ToolPromptOptions,
|
|
431
|
+
): AsyncIterable<UnifiedStreamMessage> {
|
|
432
|
+
const mcpConfigJson = readMcpConfigJson();
|
|
433
|
+
const env = buildCliEnv(this.subagentModel, this.apiKey, this.baseUrl);
|
|
434
|
+
const args = buildCliArgs(
|
|
435
|
+
this.model, this.effort, this.isEmpty, mcpConfigJson,
|
|
436
|
+
["--resume", sessionId, "--input-format", "stream-json", "--replay-user-messages"],
|
|
437
|
+
options?.permissionMode,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const proc = spawnCli(args, cwd, env, true);
|
|
441
|
+
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
442
|
+
|
|
443
|
+
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
444
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
445
|
+
|
|
446
|
+
proc.stdin!.write(buildStreamJsonInput(userText));
|
|
447
|
+
proc.stdin!.end();
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
for await (const raw of readJsonLines(proc, signal)) {
|
|
451
|
+
if (signal?.aborted) break;
|
|
452
|
+
|
|
453
|
+
if (raw.type === "system" && raw.subtype === "init" && raw.session_id) {
|
|
454
|
+
const meta: { cwd?: string; model?: string } = {};
|
|
455
|
+
if (raw.cwd) meta.cwd = raw.cwd;
|
|
456
|
+
if (raw.model) meta.model = raw.model;
|
|
457
|
+
if (Object.keys(meta).length > 0) {
|
|
458
|
+
this.metaStore.set(raw.session_id, meta).catch(() => {});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const normalized = normalizeSdkMessage(raw);
|
|
463
|
+
if (normalized) yield normalized;
|
|
464
|
+
|
|
465
|
+
// result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
|
|
466
|
+
if (raw.type === "result") {
|
|
467
|
+
void killProcessTree(proc.pid);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} finally {
|
|
472
|
+
signal?.removeEventListener("abort", onAbort);
|
|
473
|
+
await killProcessTree(proc.pid);
|
|
474
|
+
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async getSessionInfo(sessionId: string): Promise<SessionInfo | undefined> {
|
|
479
|
+
const meta = await this.metaStore.get(sessionId);
|
|
480
|
+
if (!meta) return { sessionId };
|
|
481
|
+
return meta.model
|
|
482
|
+
? { sessionId, cwd: meta.cwd, model: meta.model }
|
|
483
|
+
: { sessionId, cwd: meta.cwd };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async closeSession(_sessionId: string): Promise<void> {
|
|
487
|
+
// 子进程由 prompt 的 finally 自动 kill
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// 工厂函数
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
|
|
495
|
+
export function createClaudeAdapter(
|
|
496
|
+
options: ClaudeAdapterOptions,
|
|
497
|
+
): ToolAdapter {
|
|
498
|
+
return new ClaudeAdapter(options);
|
|
496
499
|
}
|