team-anya 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +38 -0
  2. package/apps/server/dist/broker/cc-broker.js +267 -0
  3. package/apps/server/dist/cli.js +296 -0
  4. package/apps/server/dist/config.js +78 -0
  5. package/apps/server/dist/daemon.js +51 -0
  6. package/apps/server/dist/franky/context-builder.js +161 -0
  7. package/apps/server/dist/franky/franky-mcp-server.js +110 -0
  8. package/apps/server/dist/franky/franky-orchestrator.js +629 -0
  9. package/apps/server/dist/franky/index.js +5 -0
  10. package/apps/server/dist/franky/topic-router.js +16 -0
  11. package/apps/server/dist/gateway/chat-sync.js +135 -0
  12. package/apps/server/dist/gateway/command-router.js +116 -0
  13. package/apps/server/dist/gateway/commands/cancel.js +32 -0
  14. package/apps/server/dist/gateway/commands/help.js +16 -0
  15. package/apps/server/dist/gateway/commands/index.js +26 -0
  16. package/apps/server/dist/gateway/commands/restart.js +43 -0
  17. package/apps/server/dist/gateway/commands/status.js +34 -0
  18. package/apps/server/dist/gateway/commands/tasks.js +33 -0
  19. package/apps/server/dist/gateway/feishu-sender.js +508 -0
  20. package/apps/server/dist/gateway/feishu-ws.js +353 -0
  21. package/apps/server/dist/gateway/health-monitor.js +154 -0
  22. package/apps/server/dist/gateway/http.js +1064 -0
  23. package/apps/server/dist/gateway/media-downloader.js +182 -0
  24. package/apps/server/dist/gateway/message-events.js +10 -0
  25. package/apps/server/dist/gateway/message-intake.js +72 -0
  26. package/apps/server/dist/gateway/message-queue.js +118 -0
  27. package/apps/server/dist/gateway/session-reader.js +142 -0
  28. package/apps/server/dist/gateway/ws-push.js +115 -0
  29. package/apps/server/dist/loid/brain.js +121 -0
  30. package/apps/server/dist/loid/clarifier.js +162 -0
  31. package/apps/server/dist/loid/context-builder.js +462 -0
  32. package/apps/server/dist/loid/mcp-server.js +119 -0
  33. package/apps/server/dist/loid/memory-settler.js +189 -0
  34. package/apps/server/dist/loid/opportunity-manager.js +148 -0
  35. package/apps/server/dist/loid/profile-updater.js +179 -0
  36. package/apps/server/dist/loid/project-registry.js +192 -0
  37. package/apps/server/dist/loid/reporter.js +148 -0
  38. package/apps/server/dist/loid/schemas.js +117 -0
  39. package/apps/server/dist/loid/self-calibrator.js +314 -0
  40. package/apps/server/dist/loid/session-manager.js +472 -0
  41. package/apps/server/dist/loid/session.js +276 -0
  42. package/apps/server/dist/main.js +528 -0
  43. package/apps/server/dist/tracing/index.js +2 -0
  44. package/apps/server/dist/tracing/trace-context.js +92 -0
  45. package/apps/server/dist/types/message.js +2 -0
  46. package/apps/server/dist/yor/yor-mcp-server.js +107 -0
  47. package/apps/server/dist/yor/yor-orchestrator.js +248 -0
  48. package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
  49. package/apps/web/dist/assets/index-Dnb9LGZd.js +798 -0
  50. package/apps/web/dist/index.html +13 -0
  51. package/package.json +42 -0
  52. package/packages/cc-client/dist/claude-code-backend.js +792 -0
  53. package/packages/cc-client/dist/index.js +2 -0
  54. package/packages/cc-client/package.json +11 -0
  55. package/packages/core/dist/constants.js +60 -0
  56. package/packages/core/dist/errors.js +35 -0
  57. package/packages/core/dist/index.js +9 -0
  58. package/packages/core/dist/office-init.js +190 -0
  59. package/packages/core/dist/repo-cache.js +70 -0
  60. package/packages/core/dist/scope/checker.js +114 -0
  61. package/packages/core/dist/scope/defaults.js +55 -0
  62. package/packages/core/dist/scope/index.js +3 -0
  63. package/packages/core/dist/state-machine.js +86 -0
  64. package/packages/core/dist/types/audit.js +12 -0
  65. package/packages/core/dist/types/backend.js +2 -0
  66. package/packages/core/dist/types/commitment.js +17 -0
  67. package/packages/core/dist/types/communication.js +18 -0
  68. package/packages/core/dist/types/index.js +9 -0
  69. package/packages/core/dist/types/opportunity.js +27 -0
  70. package/packages/core/dist/types/org.js +26 -0
  71. package/packages/core/dist/types/task.js +46 -0
  72. package/packages/core/dist/types/workspace.js +39 -0
  73. package/packages/core/dist/workspace-manager.js +314 -0
  74. package/packages/core/package.json +10 -0
  75. package/packages/db/dist/client.js +69 -0
  76. package/packages/db/dist/index.js +756 -0
  77. package/packages/db/dist/schema/audit-events.js +13 -0
  78. package/packages/db/dist/schema/cc-sessions.js +14 -0
  79. package/packages/db/dist/schema/chats.js +35 -0
  80. package/packages/db/dist/schema/commitments.js +18 -0
  81. package/packages/db/dist/schema/communication-events.js +14 -0
  82. package/packages/db/dist/schema/index.js +14 -0
  83. package/packages/db/dist/schema/message-log.js +20 -0
  84. package/packages/db/dist/schema/opportunities.js +23 -0
  85. package/packages/db/dist/schema/org.js +36 -0
  86. package/packages/db/dist/schema/projects.js +23 -0
  87. package/packages/db/dist/schema/tasks.js +51 -0
  88. package/packages/db/dist/schema/topics.js +22 -0
  89. package/packages/db/dist/schema/trace-spans.js +19 -0
  90. package/packages/db/dist/schema/workspaces.js +15 -0
  91. package/packages/db/package.json +12 -0
  92. package/packages/db/src/migrations/0000_baseline.sql +251 -0
  93. package/packages/db/src/migrations/0001_workspaces.sql +19 -0
  94. package/packages/db/src/migrations/0002_workspace_parent.sql +1 -0
  95. package/packages/db/src/migrations/0003_chat_context.sql +3 -0
  96. package/packages/db/src/migrations/meta/_journal.json +34 -0
  97. package/packages/mcp-tools/dist/index.js +41 -0
  98. package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
  99. package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
  100. package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
  101. package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
  102. package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
  103. package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
  104. package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
  105. package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
  106. package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
  107. package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
  108. package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
  109. package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
  110. package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
  111. package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
  112. package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
  113. package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
  114. package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
  115. package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
  116. package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
  117. package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
  118. package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
  119. package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
  120. package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
  121. package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
  122. package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
  123. package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
  124. package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
  125. package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
  126. package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +206 -0
  127. package/packages/mcp-tools/dist/layer2/loid/task-escalate-to-topic.js +170 -0
  128. package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +45 -0
  129. package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
  130. package/packages/mcp-tools/dist/layer2/loid/topic-create.js +60 -0
  131. package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
  132. package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
  133. package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
  134. package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +28 -0
  135. package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
  136. package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
  137. package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
  138. package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
  139. package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +203 -0
  140. package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
  141. package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
  142. package/packages/mcp-tools/dist/layer3/channel-send.js +75 -0
  143. package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
  144. package/packages/mcp-tools/dist/registry.js +911 -0
  145. package/packages/mcp-tools/package.json +13 -0
@@ -0,0 +1,792 @@
1
+ import { spawn, execSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFileSync, writeFileSync, appendFileSync, unlinkSync, existsSync, mkdirSync, } from "node:fs";
4
+ import { createServer } from "node:http";
5
+ import { join } from "node:path";
6
+ import { WebSocketServer, WebSocket } from "ws";
7
+ import { ConnectionState } from "@team-anya/core";
8
+ export class ClaudeCodeBackend {
9
+ proc = null;
10
+ ws = null;
11
+ wsServer = null;
12
+ httpServer = null;
13
+ cliSessionId = null;
14
+ config = null;
15
+ verbose;
16
+ // 事件回调
17
+ toolRequestHandler = null;
18
+ resultHandler = null;
19
+ progressHandler = null;
20
+ stateChangeHandler = null;
21
+ sessionIdHandler = null;
22
+ // system/init 信息(异步更新,供外部查询)
23
+ initMessage = null;
24
+ // 消息队列:CLI WS 未连接时暂存待发消息
25
+ pendingMessages = [];
26
+ // .mcp.json 管理:记录原始内容用于恢复
27
+ mcpJsonPath = null;
28
+ mcpJsonBackup = null; // null = 文件原本不存在
29
+ mcpJsonCreated = false;
30
+ // 原始对话日志:收集每次 prompt 执行期间的所有 NDJSON 消息
31
+ rawConversationLog = [];
32
+ // 日志文件路径
33
+ logFile = null;
34
+ logDirEnsured = false;
35
+ // 限流信息:rate_limit_event 到达时暂存,供后续 result 处理时使用
36
+ rateLimitInfo = null;
37
+ constructor(options) {
38
+ this.verbose = options?.verbose ?? false;
39
+ this.logFile = options?.logFile ?? null;
40
+ }
41
+ /** 确保日志文件的父目录存在(仅首次调用时创建) */
42
+ ensureLogDir() {
43
+ if (this.logDirEnsured || !this.logFile)
44
+ return;
45
+ try {
46
+ const dir = this.logFile.substring(0, this.logFile.lastIndexOf("/"));
47
+ if (dir)
48
+ mkdirSync(dir, { recursive: true });
49
+ }
50
+ catch {
51
+ // 创建失败不阻断
52
+ }
53
+ this.logDirEnsured = true;
54
+ }
55
+ /** 格式化可读时间:2026-03-02 15:30:45 */
56
+ static formatTime(date) {
57
+ const Y = date.getFullYear();
58
+ const M = String(date.getMonth() + 1).padStart(2, "0");
59
+ const D = String(date.getDate()).padStart(2, "0");
60
+ const h = String(date.getHours()).padStart(2, "0");
61
+ const m = String(date.getMinutes()).padStart(2, "0");
62
+ const s = String(date.getSeconds()).padStart(2, "0");
63
+ return `${Y}-${M}-${D} ${h}:${m}:${s}`;
64
+ }
65
+ /**
66
+ * 日志输出:
67
+ * - error: 始终输出到控制台 stderr + 写文件
68
+ * - warn/info: 如果配置了 logFile 则写文件,否则输出到控制台
69
+ */
70
+ log(level, ...args) {
71
+ if (level === "info" && !this.verbose)
72
+ return;
73
+ const ts = ClaudeCodeBackend.formatTime(new Date());
74
+ const prefix = `[cc-backend ${ts}]`;
75
+ if (level === "error") {
76
+ console.error(prefix, ...args);
77
+ this.appendToLogFile(prefix, ...args);
78
+ }
79
+ else if (this.logFile) {
80
+ // warn/info 日志写入文件,不输出到控制台
81
+ this.appendToLogFile(prefix, ...args);
82
+ }
83
+ else if (level === "warn") {
84
+ console.warn(prefix, ...args);
85
+ }
86
+ else {
87
+ console.log(prefix, ...args);
88
+ }
89
+ }
90
+ /**
91
+ * 记录原始 WS 消息到日志文件(JSON 格式,含可读时间)
92
+ */
93
+ logRawMessage(direction, msg) {
94
+ if (!this.logFile)
95
+ return;
96
+ this.ensureLogDir();
97
+ try {
98
+ const now = new Date();
99
+ const entry = JSON.stringify({
100
+ time: ClaudeCodeBackend.formatTime(now),
101
+ ts: now.getTime(),
102
+ dir: direction,
103
+ msg,
104
+ });
105
+ appendFileSync(this.logFile, entry + "\n");
106
+ }
107
+ catch {
108
+ // 日志写入失败不影响主流程
109
+ }
110
+ }
111
+ appendToLogFile(...parts) {
112
+ if (!this.logFile)
113
+ return;
114
+ this.ensureLogDir();
115
+ try {
116
+ const line = parts
117
+ .map((p) => (typeof p === "string" ? p : JSON.stringify(p)))
118
+ .join(" ");
119
+ appendFileSync(this.logFile, line + "\n");
120
+ }
121
+ catch {
122
+ // 日志写入失败不影响主流程
123
+ }
124
+ }
125
+ async connect(config) {
126
+ this.config = config;
127
+ this.initMessage = null;
128
+ this.cliSessionId = null;
129
+ this.pendingMessages = [];
130
+ this.stateChangeHandler?.(ConnectionState.CONNECTING);
131
+ this.log("info", "开始连接, binary:", config.binary ?? "claude", "workingDir:", config.workingDir);
132
+ const port = await this.findFreePort();
133
+ this.log("info", `WS 服务监听端口: ${port}`);
134
+ // 1. 启动 WebSocket Server,等待 CLI 反连
135
+ const wsReady = new Promise((resolve, reject) => {
136
+ this.httpServer = createServer();
137
+ this.wsServer = new WebSocketServer({ server: this.httpServer });
138
+ this.wsServer.on("connection", (ws, req) => {
139
+ this.log("warn", `CLI WebSocket 已连接, url=${req.url}, headers=${JSON.stringify(req.headers)}`);
140
+ this.ws = ws;
141
+ this.setupMessageHandler(ws);
142
+ resolve(ws);
143
+ });
144
+ this.httpServer.on("listening", () => {
145
+ this.log("warn", `HTTP server 已启动, 监听 127.0.0.1:${port}`);
146
+ });
147
+ this.httpServer.on("request", (req) => {
148
+ this.log("warn", `[HTTP] 收到请求: ${req.method} ${req.url}`);
149
+ });
150
+ this.httpServer.on("upgrade", (req) => {
151
+ this.log("warn", `[HTTP] 收到 upgrade 请求: ${req.url}, headers=${JSON.stringify(req.headers)}`);
152
+ });
153
+ this.httpServer.listen(port, "127.0.0.1");
154
+ this.httpServer.on("error", (err) => {
155
+ this.log("error", "HTTP server 错误:", err.message);
156
+ reject(err);
157
+ });
158
+ });
159
+ // 2. 启动 Claude Code CLI 子进程
160
+ const sessionId = randomUUID();
161
+ const sdkUrl = `ws://127.0.0.1:${port}/ws/cli/${sessionId}`;
162
+ const args = [
163
+ "--sdk-url",
164
+ sdkUrl,
165
+ "--print",
166
+ "--output-format",
167
+ "stream-json",
168
+ "--input-format",
169
+ "stream-json",
170
+ "--verbose",
171
+ ];
172
+ if (config.model)
173
+ args.push("--model", config.model);
174
+ if (config.resumeSessionId)
175
+ args.push("--resume", config.resumeSessionId);
176
+ if (config.systemPrompt)
177
+ args.push("--system-prompt", config.systemPrompt);
178
+ if (config.maxTurns)
179
+ args.push("--max-turns", String(config.maxTurns));
180
+ if (config.dangerouslySkipPermissions) {
181
+ args.push("--dangerously-skip-permissions");
182
+ }
183
+ else if (config.permissionMode) {
184
+ args.push("--permission-mode", config.permissionMode);
185
+ }
186
+ args.push("-p", ""); // headless 模式占位(参照 companion)
187
+ // MCP servers:写入 .mcp.json 到 workingDir(CLI 从 cwd 读取项目级配置)
188
+ if (config.mcpServers && config.mcpServers.length > 0) {
189
+ this.writeMcpJson(config.workingDir, config.mcpServers);
190
+ }
191
+ // 参照 companion: 用 which 解析 CLI 绝对路径
192
+ let binary = config.binary ?? "claude";
193
+ if (!binary.startsWith("/")) {
194
+ try {
195
+ binary = execSync(`which ${binary}`, { encoding: "utf-8" }).trim();
196
+ this.log("info", `解析 CLI 路径: ${binary}`);
197
+ }
198
+ catch {
199
+ this.log("warn", `无法解析 ${binary} 绝对路径,使用原值`);
200
+ }
201
+ }
202
+ this.log("error", `启动 CLI: ${binary} ${args.join(" ")} | cwd=${config.workingDir}`);
203
+ // 参照 companion (cli-launcher.ts:285-289):
204
+ // companion 设置 CLAUDECODE=1 标识 SDK 模式,但它是独立进程。
205
+ // 当从 Claude Code 会话内启动时,需要清理所有 CLAUDECODE/CLAUDE_CODE_*
206
+ // 环境变量避免嵌套检测。--sdk-url 本身已足够标识 SDK 模式。
207
+ const cleanEnv = {};
208
+ for (const [key, val] of Object.entries(process.env)) {
209
+ if (key === "CLAUDECODE" || key.startsWith("CLAUDE_CODE_"))
210
+ continue;
211
+ cleanEnv[key] = val;
212
+ }
213
+ // 二次清洗:config.env 可能把 CLAUDECODE/CLAUDE_CODE_* 又注入回来
214
+ const mergedEnv = {
215
+ ...cleanEnv,
216
+ ...config.env,
217
+ };
218
+ for (const key of Object.keys(mergedEnv)) {
219
+ if (key === "CLAUDECODE" || key.startsWith("CLAUDE_CODE_")) {
220
+ delete mergedEnv[key];
221
+ }
222
+ }
223
+ this.proc = spawn(binary, args, {
224
+ cwd: config.workingDir,
225
+ env: mergedEnv,
226
+ stdio: ["pipe", "pipe", "pipe"],
227
+ });
228
+ // 收集 stdout/stderr 用于超时诊断
229
+ const stdoutChunks = [];
230
+ const stderrChunks = [];
231
+ this.proc.stdout?.on("data", (data) => {
232
+ const text = data.toString().trim();
233
+ if (text)
234
+ stdoutChunks.push(text);
235
+ });
236
+ this.proc.stderr?.on("data", (data) => {
237
+ const text = data.toString().trim();
238
+ if (text)
239
+ stderrChunks.push(text);
240
+ this.log("warn", "CLI stderr:", text);
241
+ });
242
+ this.proc.on("error", (err) => {
243
+ this.log("error", "CLI 进程错误:", err.message);
244
+ this.stateChangeHandler?.(ConnectionState.ERROR);
245
+ });
246
+ // 进程提前退出时,让 wsReady 立即 reject 而非等到超时
247
+ this.proc.on("exit", (code, signal) => {
248
+ this.log("info", `CLI 进程退出, code=${code}, signal=${signal}`);
249
+ });
250
+ const earlyExit = new Promise((_, reject) => {
251
+ this.proc.on("exit", (code, signal) => {
252
+ const stdout = stdoutChunks.join("\n");
253
+ const stderr = stderrChunks.join("\n");
254
+ reject(new Error(`CLI 进程在连接前退出 (code=${code}, signal=${signal})` +
255
+ (stdout ? `\nstdout:\n${stdout}` : "") +
256
+ (stderr ? `\nstderr:\n${stderr}` : "")));
257
+ });
258
+ });
259
+ // 3. 等待 CLI 反连(不等待 system/init,参照 companion 非阻塞模式)
260
+ const connectTimeout = config.timeout?.connect ?? 30_000;
261
+ this.log("warn", `等待 CLI 反连, 超时: ${connectTimeout}ms`);
262
+ // 超时时附带进程状态和 stdout/stderr 便于诊断
263
+ const timeoutWithDiag = new Promise((_, reject) => {
264
+ setTimeout(() => {
265
+ const alive = this.proc && this.proc.exitCode === null && !this.proc.killed;
266
+ const stdout = stdoutChunks.join("\n");
267
+ const stderr = stderrChunks.join("\n");
268
+ const processState = alive
269
+ ? "运行中"
270
+ : `已退出 (code=${this.proc?.exitCode}, signal=${this.proc?.signalCode})`;
271
+ const diag = [
272
+ `CLI 连接超时 (${connectTimeout}ms)`,
273
+ `进程状态: ${processState}`,
274
+ stdout
275
+ ? `stdout (最后 500 字符):\n${stdout.slice(-500)}`
276
+ : "stdout: (空)",
277
+ stderr ? `stderr:\n${stderr}` : "stderr: (空)",
278
+ ].join("\n");
279
+ // 先输出日志确保诊断信息可见(不依赖上层 Error 序列化)
280
+ this.log("error", `CLI 连接超时诊断: 进程=${processState}, stdout=${stdout.length}字符, stderr=${stderr.length}字符`);
281
+ if (stderr)
282
+ this.log("error", `CLI stderr 内容: ${stderr}`);
283
+ if (stdout)
284
+ this.log("error", `CLI stdout 尾部: ${stdout.slice(-500)}`);
285
+ reject(new Error(diag));
286
+ }, connectTimeout);
287
+ });
288
+ this.ws = await Promise.race([wsReady, earlyExit, timeoutWithDiag]);
289
+ // system/init 到达时会异步更新 cliSessionId(在 routeMessage 中处理)
290
+ this.log("warn", `连接就绪, sessionId=${sessionId} (system/init 将异步更新)`);
291
+ this.stateChangeHandler?.(ConnectionState.CONNECTED);
292
+ return {
293
+ sessionId,
294
+ backendType: "claude-code",
295
+ model: config.model ?? "unknown",
296
+ tools: [],
297
+ capabilities: {
298
+ supportsResume: true,
299
+ supportsStreaming: true,
300
+ supportsToolApproval: true,
301
+ supportsModelSwitch: true,
302
+ },
303
+ };
304
+ }
305
+ async sendPrompt(prompt, _options) {
306
+ this.stateChangeHandler?.(ConnectionState.EXECUTING);
307
+ // 重置原始对话日志(每次 sendPrompt 开始新的收集周期)
308
+ this.rawConversationLog = [];
309
+ // 参照 companion (ws-bridge.ts:848-854)
310
+ const msg = {
311
+ type: "user",
312
+ message: { role: "user", content: prompt },
313
+ parent_tool_use_id: null,
314
+ session_id: this.cliSessionId ?? "",
315
+ };
316
+ const ndjson = JSON.stringify(msg);
317
+ // 记录发出的消息
318
+ this.rawConversationLog.push({
319
+ direction: "sent",
320
+ timestamp: new Date().toISOString(),
321
+ message: msg,
322
+ });
323
+ this.logRawMessage("sent", msg);
324
+ // this.log(
325
+ // "info",
326
+ // `[WS 发] sendPrompt: ${ndjson.slice(0, 500)}${ndjson.length > 500 ? "...(截断)" : ""}`,
327
+ // );
328
+ this.sendToCLI(ndjson);
329
+ }
330
+ async interrupt() {
331
+ // 参照 companion (ws-bridge.ts:899-905)
332
+ const msg = {
333
+ type: "control_request",
334
+ request_id: randomUUID(),
335
+ request: { subtype: "interrupt" },
336
+ };
337
+ const ndjson = JSON.stringify(msg);
338
+ this.rawConversationLog.push({
339
+ direction: "sent",
340
+ timestamp: new Date().toISOString(),
341
+ message: msg,
342
+ });
343
+ this.logRawMessage("sent", msg);
344
+ // this.log("info", `[WS 发] interrupt: ${ndjson}`);
345
+ this.sendToCLI(ndjson);
346
+ }
347
+ async resume(sessionId) {
348
+ await this.dispose();
349
+ if (!this.config)
350
+ throw new Error("No config available for resume");
351
+ // 参照 companion (cli-launcher.ts:219-224): 用 cliSessionId 恢复会话
352
+ return this.connect({
353
+ ...this.config,
354
+ resumeSessionId: sessionId,
355
+ });
356
+ }
357
+ getCapabilities() {
358
+ return {
359
+ supportsResume: true,
360
+ supportsStreaming: true,
361
+ supportsToolApproval: true,
362
+ supportsModelSwitch: true,
363
+ };
364
+ }
365
+ async dispose() {
366
+ // 参照 companion (cli-launcher.ts:507-532): SIGTERM → 等待 → SIGKILL
367
+ if (this.proc) {
368
+ const proc = this.proc;
369
+ const alreadyExited = proc.exitCode !== null || proc.signalCode !== null;
370
+ if (!alreadyExited) {
371
+ proc.kill("SIGTERM");
372
+ await new Promise((resolve) => {
373
+ const timeout = setTimeout(() => {
374
+ proc.kill("SIGKILL");
375
+ resolve();
376
+ }, 5_000);
377
+ proc.on("exit", () => {
378
+ clearTimeout(timeout);
379
+ resolve();
380
+ });
381
+ });
382
+ }
383
+ this.proc = null;
384
+ }
385
+ this.ws?.close();
386
+ this.ws = null;
387
+ this.wsServer?.close();
388
+ this.wsServer = null;
389
+ this.httpServer?.close();
390
+ this.httpServer = null;
391
+ this.restoreMcpJson();
392
+ this.stateChangeHandler?.(ConnectionState.DISCONNECTED);
393
+ }
394
+ onToolRequest(handler) {
395
+ this.toolRequestHandler = handler;
396
+ }
397
+ onResult(handler) {
398
+ this.resultHandler = handler;
399
+ }
400
+ onProgress(handler) {
401
+ this.progressHandler = handler;
402
+ }
403
+ onStateChange(handler) {
404
+ this.stateChangeHandler = handler;
405
+ }
406
+ onSessionId(handler) {
407
+ this.sessionIdHandler = handler;
408
+ }
409
+ getCliSessionId() {
410
+ return this.cliSessionId;
411
+ }
412
+ // ── 私有方法 ──
413
+ /**
414
+ * 统一发送入口:WS 未就绪时消息入队,就绪时直接发送。
415
+ * 参照 companion 非阻塞队列模式。
416
+ */
417
+ sendToCLI(ndjson) {
418
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
419
+ this.log("warn", `[WS] CLI 未就绪 (ws=${this.ws ? `readyState=${this.ws.readyState}` : "null"}),消息入队 (queue=${this.pendingMessages.length + 1})`);
420
+ this.pendingMessages.push(ndjson);
421
+ return;
422
+ }
423
+ // this.log(
424
+ // "warn",
425
+ // `[WS 发] ${ndjson.slice(0, 500)}${ndjson.length > 500 ? "...(截断)" : ""}`,
426
+ // );
427
+ this.ws.send(ndjson + "\n");
428
+ }
429
+ setupMessageHandler(ws) {
430
+ // WS 连接建立后立即刷新排队消息
431
+ if (this.pendingMessages.length > 0) {
432
+ this.log("info", `[WS] 刷新 ${this.pendingMessages.length} 条排队消息`);
433
+ const queued = this.pendingMessages.splice(0);
434
+ for (const ndjson of queued) {
435
+ this.sendToCLI(ndjson);
436
+ }
437
+ }
438
+ let buffer = "";
439
+ ws.on("message", (data) => {
440
+ const raw = data.toString();
441
+ this.log("info", `[WS 收] ${raw.slice(0, 5000)}${raw.length > 5000 ? "...(截断)" : ""}`);
442
+ buffer += raw;
443
+ const lines = buffer.split("\n");
444
+ buffer = lines.pop() ?? "";
445
+ for (const line of lines) {
446
+ if (!line.trim())
447
+ continue;
448
+ try {
449
+ const msg = JSON.parse(line);
450
+ this.routeMessage(msg);
451
+ }
452
+ catch (err) {
453
+ this.log("warn", `[WS 收] JSON 解析失败: ${err.message}, raw=${line.slice(0, 200)}`);
454
+ }
455
+ }
456
+ });
457
+ ws.on("close", (code, reason) => {
458
+ this.log("warn", `[WS] 连接关闭, code=${code}, reason=${reason.toString()}`);
459
+ });
460
+ ws.on("error", (err) => {
461
+ this.log("error", `[WS] 连接错误:`, err.message);
462
+ });
463
+ ws.on("ping", () => {
464
+ // this.log("warn", "[WS] 收到 ping");
465
+ });
466
+ ws.on("pong", () => {
467
+ // this.log("warn", "[WS] 收到 pong");
468
+ });
469
+ }
470
+ /**
471
+ * 参照 companion (ws-bridge.ts:550-591): 按 type 路由 CLI 消息
472
+ */
473
+ routeMessage(msg) {
474
+ // 收到任何消息即证明连接存活,通知 BackendManager 重置心跳计数
475
+ this.progressHandler?.({ type: "heartbeat", timestamp: Date.now() });
476
+ // 记录收到的原始消息到对话日志 + 写入日志文件
477
+ this.rawConversationLog.push({
478
+ direction: "received",
479
+ timestamp: new Date().toISOString(),
480
+ message: msg,
481
+ });
482
+ this.logRawMessage("recv", msg);
483
+ const msgType = `${msg.type}${msg.subtype ? "/" + msg.subtype : ""}`;
484
+ this.log("info", `[路由] type=${msgType}${msg.session_id ? ` sid=${msg.session_id}` : ""}${msg.hook_name ? ` hook=${msg.hook_name}` : ""}`);
485
+ switch (msg.type) {
486
+ case "system":
487
+ // 参照 companion (ws-bridge.ts:594-641): 只有 subtype=init 才是初始化消息
488
+ // 异步更新 cliSessionId 和 initMessage,不阻塞连接流程
489
+ if (msg.subtype === "init") {
490
+ this.log("info", `[路由] system/init 详情: model=${msg.model}, tools=${JSON.stringify(msg.tools ?? [])}`);
491
+ this.initMessage = msg;
492
+ this.cliSessionId = msg.session_id ?? this.cliSessionId;
493
+ if (this.cliSessionId) {
494
+ this.sessionIdHandler?.(this.cliSessionId);
495
+ }
496
+ }
497
+ break;
498
+ case "assistant":
499
+ this.log("info", `[路由] assistant 内容块数: ${msg.message?.content?.length ?? 0}`);
500
+ this.handleAssistant(msg);
501
+ break;
502
+ case "stream_event":
503
+ this.handleStreamEvent(msg.event);
504
+ break;
505
+ case "result": {
506
+ const preview = typeof msg.result === "string"
507
+ ? msg.result.slice(0, 200)
508
+ : JSON.stringify(msg.result ?? null).slice(0, 200);
509
+ this.log("info", `[路由] result: is_error=${msg.is_error}, turns=${msg.num_turns}, preview=${preview}`);
510
+ this.handleResult(msg);
511
+ break;
512
+ }
513
+ case "control_request":
514
+ this.log("info", `[路由] control_request: subtype=${msg.request?.subtype}, tool=${msg.request?.tool_name ?? "N/A"}`);
515
+ if (msg.request?.subtype === "can_use_tool") {
516
+ this.handleToolApproval(msg);
517
+ }
518
+ break;
519
+ case "tool_progress":
520
+ this.progressHandler?.({
521
+ type: "tool_progress",
522
+ toolName: msg.tool_name ?? "unknown",
523
+ output: msg.output ?? "",
524
+ });
525
+ break;
526
+ case "rate_limit_event": {
527
+ const info = msg.rate_limit_info;
528
+ if (info?.status === "rejected") {
529
+ this.log("warn", `[路由] 触发限流: status=${info.status}, type=${info?.rateLimitType}, resetsAt=${info?.resetsAt}`);
530
+ this.rateLimitInfo = {
531
+ resetsAt: info?.resetsAt ?? 0,
532
+ rateLimitType: info?.rateLimitType ?? "unknown",
533
+ };
534
+ }
535
+ else {
536
+ this.log("info", `[路由] 限流状态正常: status=${info?.status}, type=${info?.rateLimitType}`);
537
+ }
538
+ break;
539
+ }
540
+ case "keep_alive":
541
+ case "user":
542
+ // 静默消费(user 是 CLI 回传的 tool_use_result)
543
+ break;
544
+ default:
545
+ this.log("warn", `[路由] 未知消息类型: ${msg.type}, keys=${Object.keys(msg).join(",")}`);
546
+ break;
547
+ }
548
+ }
549
+ handleAssistant(msg) {
550
+ if (msg.message?.content) {
551
+ for (const block of msg.message.content) {
552
+ if (block.type === "text") {
553
+ this.progressHandler?.({ type: "text_chunk", text: block.text });
554
+ }
555
+ else if (block.type === "thinking") {
556
+ this.progressHandler?.({ type: "thinking", text: block.thinking });
557
+ }
558
+ }
559
+ }
560
+ }
561
+ handleStreamEvent(event) {
562
+ if (!event)
563
+ return;
564
+ if (event.type === "content_block_delta") {
565
+ if (event.delta?.type === "text_delta") {
566
+ this.progressHandler?.({ type: "text_chunk", text: event.delta.text });
567
+ }
568
+ }
569
+ }
570
+ handleResult(msg) {
571
+ const result = {
572
+ status: msg.is_error ? "error" : "success",
573
+ resultText: msg.result,
574
+ errors: msg.errors,
575
+ metrics: {
576
+ durationMs: msg.duration_ms ?? 0,
577
+ numTurns: msg.num_turns ?? 0,
578
+ totalCostUsd: msg.total_cost_usd,
579
+ linesAdded: msg.total_lines_added,
580
+ linesRemoved: msg.total_lines_removed,
581
+ },
582
+ // 附加完整的原始对话日志
583
+ rawConversation: [...this.rawConversationLog],
584
+ };
585
+ // 特殊处理 max_turns
586
+ if (msg.subtype === "error_max_turns") {
587
+ result.status = "max_turns";
588
+ }
589
+ // 特殊处理限流:rate_limit_event 先于 result 到达,信息暂存在 rateLimitInfo
590
+ if (this.rateLimitInfo) {
591
+ result.status = "rate_limited";
592
+ result.rateLimitInfo = this.rateLimitInfo;
593
+ this.rateLimitInfo = null;
594
+ }
595
+ this.resultHandler?.(result);
596
+ this.stateChangeHandler?.(ConnectionState.CONNECTED);
597
+ }
598
+ /**
599
+ * 始终自动放行的工具列表(纯控制流,无安全风险)
600
+ */
601
+ static AUTO_APPROVE_TOOLS = new Set([
602
+ "TaskCreate",
603
+ "TaskUpdate",
604
+ "TaskList",
605
+ "TaskGet",
606
+ "SendMessage",
607
+ ]);
608
+ /**
609
+ * headless 模式下需要拒绝的交互式工具(需要终端用户在场)
610
+ */
611
+ static HEADLESS_DENY_TOOLS = new Set([
612
+ "AskUserQuestion",
613
+ "EnterPlanMode",
614
+ "ExitPlanMode",
615
+ ]);
616
+ /**
617
+ * 参照 companion (ws-bridge.ts:714-733 + 858-896): 工具审批流
618
+ */
619
+ async handleToolApproval(msg) {
620
+ const req = {
621
+ requestId: msg.request_id,
622
+ toolName: msg.request.tool_name ?? "unknown",
623
+ input: msg.request.input ?? {},
624
+ description: msg.request.description,
625
+ toolUseId: msg.request.tool_use_id ?? "",
626
+ };
627
+ let response;
628
+ if (ClaudeCodeBackend.HEADLESS_DENY_TOOLS.has(req.toolName)) {
629
+ // headless 模式下拒绝交互式工具
630
+ this.log("warn", `[审批] 拒绝交互式工具: ${req.toolName}(headless 模式无终端用户)`);
631
+ response = { requestId: req.requestId, behavior: "deny", reason: `${req.toolName} requires interactive terminal user, not available in headless mode. Use MCP tools (channel.send, task.dispatch, etc.) instead.` };
632
+ }
633
+ else if (ClaudeCodeBackend.AUTO_APPROVE_TOOLS.has(req.toolName)) {
634
+ // 安全工具自动放行,无需走 ScopeChecker
635
+ this.log("info", `[审批] 自动放行: ${req.toolName}`);
636
+ response = { requestId: req.requestId, behavior: "allow" };
637
+ }
638
+ else if (!this.toolRequestHandler) {
639
+ // 无 handler 时默认放行(兼容 --dangerously-skip-permissions 模式)
640
+ this.log("warn", `[审批] 无 handler,默认放行: ${req.toolName}`);
641
+ response = { requestId: req.requestId, behavior: "allow" };
642
+ }
643
+ else {
644
+ response = await this.toolRequestHandler(req);
645
+ }
646
+ const reply = JSON.stringify({
647
+ type: "control_response",
648
+ response: {
649
+ subtype: "success",
650
+ request_id: req.requestId,
651
+ response: response.behavior === "allow"
652
+ ? {
653
+ behavior: "allow",
654
+ updatedInput: response.updatedInput ?? {},
655
+ }
656
+ : {
657
+ behavior: "deny",
658
+ message: response.reason ?? "denied by tool request handler",
659
+ },
660
+ },
661
+ });
662
+ // 记录发出的 tool approval 响应
663
+ const replyMsg = JSON.parse(reply);
664
+ this.rawConversationLog.push({
665
+ direction: "sent",
666
+ timestamp: new Date().toISOString(),
667
+ message: replyMsg,
668
+ });
669
+ this.logRawMessage("sent", replyMsg);
670
+ // this.log(
671
+ // "info",
672
+ // `[WS 发] toolApproval: tool=${req.toolName}, behavior=${response.behavior}`,
673
+ // );
674
+ this.sendToCLI(reply);
675
+ }
676
+ findFreePort() {
677
+ return new Promise((resolve, reject) => {
678
+ const server = createServer();
679
+ server.listen(0, "127.0.0.1", () => {
680
+ const addr = server.address();
681
+ if (addr && typeof addr === "object") {
682
+ const port = addr.port;
683
+ server.close(() => resolve(port));
684
+ }
685
+ else {
686
+ reject(new Error("Failed to get port"));
687
+ }
688
+ });
689
+ });
690
+ }
691
+ withTimeout(promise, ms, message) {
692
+ return new Promise((resolve, reject) => {
693
+ const timer = setTimeout(() => reject(new Error(message)), ms);
694
+ promise.then((val) => {
695
+ clearTimeout(timer);
696
+ resolve(val);
697
+ }, (err) => {
698
+ clearTimeout(timer);
699
+ reject(err);
700
+ });
701
+ });
702
+ }
703
+ /**
704
+ * 从 workingDir 向上查找 git root
705
+ */
706
+ /**
707
+ * 将 MCP server 配置写入目标目录的 .mcp.json
708
+ * 如果文件已存在,合并我们的 server 条目并备份原始内容
709
+ */
710
+ writeMcpJson(workingDir, mcpServers) {
711
+ // 确保目录存在
712
+ if (!existsSync(workingDir)) {
713
+ mkdirSync(workingDir, { recursive: true });
714
+ }
715
+ this.mcpJsonPath = join(workingDir, ".mcp.json");
716
+ // 构建我们的 server 条目
717
+ const ourServers = {};
718
+ for (const s of mcpServers) {
719
+ if (s.url) {
720
+ ourServers[s.name] = {
721
+ type: "http",
722
+ url: s.url,
723
+ ...(s.headers ? { headers: s.headers } : {}),
724
+ };
725
+ }
726
+ else if (s.command) {
727
+ ourServers[s.name] = {
728
+ type: "stdio",
729
+ command: s.command,
730
+ ...(s.args ? { args: s.args } : {}),
731
+ };
732
+ }
733
+ }
734
+ // 读取已有文件(如有)
735
+ let existing = {};
736
+ if (existsSync(this.mcpJsonPath)) {
737
+ try {
738
+ const raw = readFileSync(this.mcpJsonPath, "utf-8");
739
+ existing = JSON.parse(raw);
740
+ this.mcpJsonBackup = raw;
741
+ this.mcpJsonCreated = false;
742
+ this.log("info", `.mcp.json 已存在,合并 ${Object.keys(ourServers).length} 个 server`);
743
+ }
744
+ catch {
745
+ this.mcpJsonBackup = null;
746
+ this.mcpJsonCreated = true;
747
+ }
748
+ }
749
+ else {
750
+ this.mcpJsonBackup = null;
751
+ this.mcpJsonCreated = true;
752
+ }
753
+ // 合并写入
754
+ const merged = {
755
+ ...existing,
756
+ mcpServers: {
757
+ ...(existing.mcpServers ?? {}),
758
+ ...ourServers,
759
+ },
760
+ };
761
+ writeFileSync(this.mcpJsonPath, JSON.stringify(merged, null, 2), "utf-8");
762
+ this.log("info", `.mcp.json 已写入: ${this.mcpJsonPath}, servers: ${Object.keys(ourServers).join(", ")}`);
763
+ }
764
+ /**
765
+ * 恢复或删除 .mcp.json
766
+ */
767
+ restoreMcpJson() {
768
+ if (!this.mcpJsonPath)
769
+ return;
770
+ try {
771
+ if (this.mcpJsonCreated) {
772
+ // 文件是我们创建的 → 直接删除
773
+ if (existsSync(this.mcpJsonPath)) {
774
+ unlinkSync(this.mcpJsonPath);
775
+ this.log("info", `.mcp.json 已删除: ${this.mcpJsonPath}`);
776
+ }
777
+ }
778
+ else if (this.mcpJsonBackup !== null) {
779
+ // 文件原本存在 → 恢复原始内容
780
+ writeFileSync(this.mcpJsonPath, this.mcpJsonBackup, "utf-8");
781
+ this.log("info", `.mcp.json 已恢复: ${this.mcpJsonPath}`);
782
+ }
783
+ }
784
+ catch (err) {
785
+ this.log("warn", `.mcp.json 清理失败:`, err.message);
786
+ }
787
+ this.mcpJsonPath = null;
788
+ this.mcpJsonBackup = null;
789
+ this.mcpJsonCreated = false;
790
+ }
791
+ }
792
+ //# sourceMappingURL=claude-code-backend.js.map