aws-runtime-bridge 1.7.50 → 1.8.1

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 (65) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +30 -3
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/AcodeSdkAdapter.js +461 -105
  4. package/dist/adapter/AcodeSdkAdapter.test.js +1282 -93
  5. package/dist/adapter/ClaudeSdkAdapter.test.js +31 -8
  6. package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
  7. package/dist/adapter/CodexSdkAdapter.js +2 -0
  8. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  9. package/dist/adapter/OpencodeSdkAdapter.js +2 -0
  10. package/dist/adapter/SdkProviderSpi.d.ts.map +1 -1
  11. package/dist/adapter/adapter.test.js +2 -2
  12. package/dist/adapter/idle-poll-loop.d.ts +4 -4
  13. package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
  14. package/dist/adapter/idle-poll-loop.js +3 -3
  15. package/dist/adapter/idle-poll-loop.test.js +15 -1
  16. package/dist/adapter/types.d.ts +25 -6
  17. package/dist/adapter/types.d.ts.map +1 -1
  18. package/dist/adapter/types.js +226 -111
  19. package/dist/adapter/types.test.js +10 -3
  20. package/dist/routes/ai-sources.js +5 -1
  21. package/dist/routes/ai-sources.test.js +21 -0
  22. package/dist/routes/instance.d.ts.map +1 -1
  23. package/dist/routes/instance.js +6 -7
  24. package/dist/routes/runtime-binding.js +2 -2
  25. package/dist/routes/terminal.d.ts +22 -5
  26. package/dist/routes/terminal.d.ts.map +1 -1
  27. package/dist/routes/terminal.js +522 -170
  28. package/dist/routes/terminal.test.js +497 -208
  29. package/dist/services/mcp-launch-binding-queue.d.ts +1 -0
  30. package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
  31. package/dist/services/mcp-launch-binding-queue.js +8 -2
  32. package/dist/services/mcp-launch-binding-queue.test.js +47 -4
  33. package/dist/services/session-output.d.ts +14 -6
  34. package/dist/services/session-output.d.ts.map +1 -1
  35. package/dist/services/session-output.js +29 -14
  36. package/dist/services/session-output.test.js +93 -39
  37. package/package/acode/dist/built-in-file-tools.d.ts +1 -1
  38. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  39. package/package/acode/dist/built-in-file-tools.js +388 -50
  40. package/package/acode/dist/runtime.d.ts +11 -3
  41. package/package/acode/dist/runtime.d.ts.map +1 -1
  42. package/package/acode/dist/runtime.js +902 -190
  43. package/package/acode/dist/types.d.ts +18 -5
  44. package/package/acode/dist/types.d.ts.map +1 -1
  45. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  46. package/package/aws-client-agent-mcp/dist/mcp-server.js +3 -2
  47. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  48. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +11 -9
  49. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  50. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +1 -1
  51. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +1 -0
  52. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +1 -1
  53. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +2 -0
  54. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
  55. package/package/aws-client-agent-mcp/dist/status-reporter.js +31 -3
  56. package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
  57. package/package/aws-client-agent-mcp/dist/types.d.ts +1 -1
  58. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  59. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  60. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  61. package/package/aws-client-agent-mcp/dist/websocket-client.js +2 -0
  62. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  63. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +43 -0
  64. package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +1 -1
  65. package/package.json +1 -1
@@ -4,44 +4,91 @@
4
4
  * 提供 SDK Agent 会话的启动、输入、调整大小和停止功能
5
5
  * 终端 UI 仅作为 SDK Agent 的消息输入/状态输出载体
6
6
  */
7
- import { spawn } from 'node:child_process';
8
- import { promises as fs } from 'node:fs';
9
- import os from 'node:os';
10
- import path from 'node:path';
11
- import { TextDecoder } from 'node:util';
12
- import { Router } from 'express';
13
- import { v4 as uuidv4 } from 'uuid';
14
- import { adapterRegistry, getToolActionInfo, isTransientClipboardPasteFilePath, redactTransientClipboardPastePaths, registerSdkProviders, resolveSdkProviderIdByCommand, } from '../adapter/index.js';
15
- import { validateToken } from '../middleware/auth.js';
16
- import { getAgentProcessManager } from '../services/agent-process-manager.js';
17
- import { getAiConfigService } from '../services/cc-switch-sdk.js';
18
- import { enqueueMcpLaunchBinding } from '../services/mcp-launch-binding-queue.js';
19
- import { findClaudeCodeProcesses, isProcessRunning } from '../services/process-detector.js';
20
- import { sendOutput, sendQuestionRequest, sendStatus } from '../services/session-output.js';
21
- import { findPersistedSessionByAgentId, removePersistedSession, savePersistedSessions, updatePersistedSessionAutoCommands, updatePersistedSessionRuntimeState, upsertPersistedSession, } from '../services/terminal-persistence.js';
7
+ import { spawn } from "node:child_process";
8
+ import { promises as fs } from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import { TextDecoder } from "node:util";
12
+ import { Router } from "express";
13
+ import { v4 as uuidv4 } from "uuid";
14
+ import { adapterRegistry, getToolActionInfo, isTransientClipboardPasteFilePath, redactTransientClipboardPastePaths, registerSdkProviders, resolveSdkProviderIdByCommand, } from "../adapter/index.js";
15
+ import { validateToken } from "../middleware/auth.js";
16
+ import { getAgentProcessManager } from "../services/agent-process-manager.js";
17
+ import { getAiConfigService } from "../services/cc-switch-sdk.js";
18
+ import { enqueueMcpLaunchBinding } from "../services/mcp-launch-binding-queue.js";
19
+ import { findClaudeCodeProcesses, isProcessRunning, } from "../services/process-detector.js";
20
+ import { sendOutput, sendQuestionRequest, sendStatus, } from "../services/session-output.js";
21
+ import { findPersistedSessionByAgentId, removePersistedSession, savePersistedSessions, updatePersistedSessionAutoCommands, updatePersistedSessionRuntimeState, upsertPersistedSession, } from "../services/terminal-persistence.js";
22
22
  export const terminalRouter = Router();
23
23
  // 导出 SDK 会话存储,供其他模块使用(如 sessions.ts)
24
24
  export const sdkSessions = new Map();
25
25
  const TOOL_RESULT_PREVIEW_MAX_LENGTH = 4000;
26
+ const AI_REQUEST_BODY_MARKER = "\n\n请求体:\n";
26
27
  const startingAgents = new Set();
27
28
  export function resolveStatusChangeUsage(actionInfo) {
28
29
  const usage = actionInfo?.usage;
29
30
  if (!usage)
30
31
  return undefined;
31
- const inputTokens = Number.isFinite(usage.inputTokens) ? usage.inputTokens : 0;
32
- const outputTokens = Number.isFinite(usage.outputTokens) ? usage.outputTokens : 0;
32
+ const inputTokens = Number.isFinite(usage.inputTokens)
33
+ ? usage.inputTokens
34
+ : 0;
35
+ const outputTokens = Number.isFinite(usage.outputTokens)
36
+ ? usage.outputTokens
37
+ : 0;
33
38
  if (inputTokens <= 0 && outputTokens <= 0)
34
39
  return undefined;
35
40
  return { inputTokens, outputTokens };
36
41
  }
37
42
  const terminalCommandStates = new Map();
38
- const DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING = 'gb18030';
39
- const DEFAULT_TERMINAL_OUTPUT_ENCODING = 'utf-8';
43
+ const DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING = "gb18030";
44
+ const DEFAULT_TERMINAL_OUTPUT_ENCODING = "utf-8";
45
+ const TERMINAL_COMMAND_OUTPUT_SPILLOVER_LINE_LIMIT = 100;
46
+ const TERMINAL_COMMAND_INACTIVITY_TIMEOUT_MS = 60_000;
47
+ function padDatePart(value) {
48
+ return String(value).padStart(2, "0");
49
+ }
50
+ export function buildAcodeCommandOutputPath(workspacePath, now = new Date()) {
51
+ const year = now.getFullYear();
52
+ const month = padDatePart(now.getMonth() + 1);
53
+ const day = padDatePart(now.getDate());
54
+ const hour = padDatePart(now.getHours());
55
+ const minute = padDatePart(now.getMinutes());
56
+ const timestamp = `${year}${month}${day}-${hour}${minute}${padDatePart(now.getSeconds())}-${String(now.getMilliseconds()).padStart(3, "0")}`;
57
+ return path.join(workspacePath, ".agentswork", "acode", "cmd-dist", `${year}-${month}-${day}`, hour, minute, `${timestamp}-cmd.out`);
58
+ }
59
+ export function countTerminalOutputLines(output) {
60
+ if (!output) {
61
+ return 0;
62
+ }
63
+ const normalized = output.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
64
+ const withoutTrailingLineBreak = normalized.endsWith("\n")
65
+ ? normalized.slice(0, -1)
66
+ : normalized;
67
+ return withoutTrailingLineBreak
68
+ ? withoutTrailingLineBreak.split("\n").length
69
+ : 1;
70
+ }
71
+ export function formatCommandOutputSpilloverNotice(outputPath, lineLimit = TERMINAL_COMMAND_OUTPUT_SPILLOVER_LINE_LIMIT) {
72
+ return `\r\n[ACode] 命令输出超过 ${lineLimit} 行,完整输出已写入: ${outputPath}\r\n[ACode] Agent 可自行读取该文件,或在该文件中搜索需要的内容。\r\n`;
73
+ }
74
+ export function formatCommandInactivityTimeoutNotice(timeoutMs = TERMINAL_COMMAND_INACTIVITY_TIMEOUT_MS) {
75
+ return `\r\n[ACode] 命令超过 ${Math.round(timeoutMs / 1000)} 秒没有新的输出,已停止执行并保留此前输出。\r\n`;
76
+ }
77
+ async function persistTerminalCommandOutput(state) {
78
+ const outputPath = state.outputPath ||
79
+ buildAcodeCommandOutputPath(state.workspacePath, state.startedAt);
80
+ state.outputPath = outputPath;
81
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
82
+ await fs.writeFile(outputPath, state.chunks.join(""), "utf-8");
83
+ return outputPath;
84
+ }
40
85
  /**
41
86
  * 标准化终端输入:将前端回车符还原为 shell 可执行命令文本。
42
87
  */
43
88
  export function normalizeTerminalCommandInput(value) {
44
- return String(value ?? '').replace(/\r/g, '\n').trim();
89
+ return String(value ?? "")
90
+ .replace(/\r/g, "\n")
91
+ .trim();
45
92
  }
46
93
  /**
47
94
  * 解析终端命令输出解码编码。
@@ -52,7 +99,9 @@ export function resolveTerminalOutputEncoding(platform = process.platform, env =
52
99
  if (configuredEncoding) {
53
100
  return configuredEncoding;
54
101
  }
55
- return platform === 'win32' ? DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING : DEFAULT_TERMINAL_OUTPUT_ENCODING;
102
+ return platform === "win32"
103
+ ? DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING
104
+ : DEFAULT_TERMINAL_OUTPUT_ENCODING;
56
105
  }
57
106
  /**
58
107
  * 创建终端输出流式解码器,避免多字节中文被 chunk 边界截断。
@@ -73,7 +122,7 @@ export function decodeTerminalOutputChunk(decoder, chunk, end = false) {
73
122
  return decoder.decode();
74
123
  }
75
124
  if (!chunk || chunk.length === 0) {
76
- return '';
125
+ return "";
77
126
  }
78
127
  return decoder.decode(chunk, { stream: true });
79
128
  }
@@ -89,9 +138,9 @@ export function parseTerminalDirectoryChangeTarget(command) {
89
138
  if (!rawTarget) {
90
139
  return null;
91
140
  }
92
- const withoutDriveSwitch = rawTarget.replace(/^\/d\s+/i, '').trim();
93
- if ((withoutDriveSwitch.startsWith('"') && withoutDriveSwitch.endsWith('"'))
94
- || (withoutDriveSwitch.startsWith("'") && withoutDriveSwitch.endsWith("'"))) {
141
+ const withoutDriveSwitch = rawTarget.replace(/^\/d\s+/i, "").trim();
142
+ if ((withoutDriveSwitch.startsWith('"') && withoutDriveSwitch.endsWith('"')) ||
143
+ (withoutDriveSwitch.startsWith("'") && withoutDriveSwitch.endsWith("'"))) {
95
144
  return withoutDriveSwitch.slice(1, -1);
96
145
  }
97
146
  return withoutDriveSwitch;
@@ -117,7 +166,7 @@ function findActiveSdkSessionByAgentId(agentId) {
117
166
  return undefined;
118
167
  }
119
168
  async function buildExistingSessionResponse(entry) {
120
- const adapter = adapterRegistry.get(entry.providerId || 'claude-code');
169
+ const adapter = adapterRegistry.get(entry.providerId || "claude-code");
121
170
  const status = resolveSdkSessionDisplayStatus(adapter.getSessionStatus(entry.sessionId), entry.currentRuntimeStatus);
122
171
  return {
123
172
  sessionId: entry.sessionId,
@@ -129,7 +178,7 @@ async function buildExistingSessionResponse(entry) {
129
178
  agentId: entry.agentId,
130
179
  workspacePath: entry.workspacePath,
131
180
  command: entry.config.command,
132
- mode: 'sdk',
181
+ mode: "sdk",
133
182
  providerSessionId: adapter.getProviderSessionId(entry.sessionId),
134
183
  reused: true,
135
184
  };
@@ -139,12 +188,12 @@ async function buildExistingSessionResponse(entry) {
139
188
  * 主流程:优先使用 adapter 当前细粒度状态,其次使用路由内存记录的最近状态,最后才兼容旧会话回退 running。
140
189
  */
141
190
  export function resolveSdkSessionDisplayStatus(adapterStatus, currentRuntimeStatus) {
142
- const normalizedAdapterStatus = String(adapterStatus || '').trim();
191
+ const normalizedAdapterStatus = String(adapterStatus || "").trim();
143
192
  if (normalizedAdapterStatus) {
144
193
  return normalizedAdapterStatus;
145
194
  }
146
- const normalizedRuntimeStatus = String(currentRuntimeStatus || '').trim();
147
- return normalizedRuntimeStatus || 'running';
195
+ const normalizedRuntimeStatus = String(currentRuntimeStatus || "").trim();
196
+ return normalizedRuntimeStatus || "running";
148
197
  }
149
198
  /**
150
199
  * Decide whether a persisted SDK session can be reused for a start request.
@@ -152,15 +201,15 @@ export function resolveSdkSessionDisplayStatus(adapterStatus, currentRuntimeStat
152
201
  */
153
202
  export function evaluatePersistedSessionReuse(session, isPidRunning = isProcessRunning) {
154
203
  if (!session.pid) {
155
- return { reusable: false, reason: 'missing-pid' };
204
+ return { reusable: false, reason: "missing-pid" };
156
205
  }
157
206
  if (!isPidRunning(session.pid)) {
158
- return { reusable: false, reason: 'dead-pid' };
207
+ return { reusable: false, reason: "dead-pid" };
159
208
  }
160
- if (resolveSdkProviderId(session.command) === 'acode') {
161
- return { reusable: false, reason: 'in-process-provider' };
209
+ if (resolveSdkProviderId(session.command) === "acode") {
210
+ return { reusable: false, reason: "in-process-provider" };
162
211
  }
163
- return { reusable: true, reason: 'active-pid' };
212
+ return { reusable: true, reason: "active-pid" };
164
213
  }
165
214
  function stopTerminalCommandProcess(sessionId) {
166
215
  const state = terminalCommandStates.get(sessionId);
@@ -193,7 +242,7 @@ export function formatTerminalPrompt(currentDirectory) {
193
242
  return `\x1b[36m${currentDirectory}>\x1b[0m `;
194
243
  }
195
244
  function resolveDirectoryTarget(currentDirectory, target) {
196
- if (target === '~') {
245
+ if (target === "~") {
197
246
  return os.homedir();
198
247
  }
199
248
  return path.resolve(currentDirectory, target);
@@ -233,7 +282,7 @@ async function handleDirectoryChangeCommand(entry, state, command) {
233
282
  async function executeTerminalCommand(sessionId, commandValue) {
234
283
  const entry = sdkSessions.get(sessionId);
235
284
  if (!entry) {
236
- throw new Error('SDK session not found');
285
+ throw new Error("SDK session not found");
237
286
  }
238
287
  const command = normalizeTerminalCommandInput(commandValue);
239
288
  const state = getTerminalCommandState(entry);
@@ -241,12 +290,12 @@ async function executeTerminalCommand(sessionId, commandValue) {
241
290
  return { started: false, cwd: state.currentDirectory };
242
291
  }
243
292
  if (state.runningProcess) {
244
- throw new Error('A terminal command is already running for this session');
293
+ throw new Error("A terminal command is already running for this session");
245
294
  }
246
295
  const displayCommand = redactTransientClipboardPastePaths(command);
247
296
  appendCommandOutput(entry, `${formatTerminalPrompt(state.currentDirectory)}${displayCommand}\r\n`);
248
297
  if (isTransientClipboardPasteFilePath(command)) {
249
- appendCommandOutput(entry, '[Terminal Warning] 检测到剪贴板临时 paste 文件路径,已跳过执行。请粘贴文件内容或使用显式读取命令。\r\n');
298
+ appendCommandOutput(entry, "[Terminal Warning] 检测到剪贴板临时 paste 文件路径,已跳过执行。请粘贴文件内容或使用显式读取命令。\r\n");
250
299
  appendCommandOutput(entry, formatTerminalPrompt(state.currentDirectory));
251
300
  return { started: false, cwd: state.currentDirectory };
252
301
  }
@@ -254,9 +303,9 @@ async function executeTerminalCommand(sessionId, commandValue) {
254
303
  if (handledDirectoryChange) {
255
304
  return { started: false, cwd: state.currentDirectory };
256
305
  }
257
- await sendStatus(entry.agentId, sessionId, 'tool_using', {
258
- actionType: 'bash',
259
- actionLabel: '终端命令',
306
+ await sendStatus(entry.agentId, sessionId, "tool_using", {
307
+ actionType: "bash",
308
+ actionLabel: "终端命令",
260
309
  actionDetail: displayCommand,
261
310
  });
262
311
  const child = spawn(command, {
@@ -267,19 +316,81 @@ async function executeTerminalCommand(sessionId, commandValue) {
267
316
  state.runningProcess = child;
268
317
  const stdoutDecoder = createTerminalOutputDecoder();
269
318
  const stderrDecoder = createTerminalOutputDecoder();
270
- child.stdout.on('data', (chunk) => {
271
- appendCommandOutput(entry, decodeTerminalOutputChunk(stdoutDecoder, chunk));
319
+ const usesAcodeCommandOutputPolicy = entry.providerId === "acode";
320
+ const outputSpilloverState = {
321
+ workspacePath: entry.workspacePath,
322
+ startedAt: new Date(),
323
+ chunks: [],
324
+ spilled: false,
325
+ };
326
+ let inactivityTimer;
327
+ const clearInactivityTimer = () => {
328
+ if (inactivityTimer) {
329
+ clearTimeout(inactivityTimer);
330
+ inactivityTimer = undefined;
331
+ }
332
+ };
333
+ const resetInactivityTimer = () => {
334
+ clearInactivityTimer();
335
+ inactivityTimer = setTimeout(() => {
336
+ if (state.runningProcess === child) {
337
+ appendCommandOutput(entry, formatCommandInactivityTimeoutNotice());
338
+ child.kill();
339
+ }
340
+ }, TERMINAL_COMMAND_INACTIVITY_TIMEOUT_MS);
341
+ };
342
+ const appendProcessOutput = (output, trackActivity = true) => {
343
+ if (!output) {
344
+ return;
345
+ }
346
+ if (usesAcodeCommandOutputPolicy) {
347
+ outputSpilloverState.chunks.push(output);
348
+ }
349
+ if (usesAcodeCommandOutputPolicy && trackActivity) {
350
+ resetInactivityTimer();
351
+ }
352
+ if (usesAcodeCommandOutputPolicy && outputSpilloverState.spilled) {
353
+ return;
354
+ }
355
+ if (usesAcodeCommandOutputPolicy &&
356
+ countTerminalOutputLines(outputSpilloverState.chunks.join("")) >
357
+ TERMINAL_COMMAND_OUTPUT_SPILLOVER_LINE_LIMIT) {
358
+ outputSpilloverState.spilled = true;
359
+ void persistTerminalCommandOutput(outputSpilloverState)
360
+ .then((outputPath) => {
361
+ appendCommandOutput(entry, formatCommandOutputSpilloverNotice(outputPath));
362
+ })
363
+ .catch((error) => {
364
+ const message = error instanceof Error ? error.message : String(error);
365
+ appendCommandOutput(entry, `\r\n[ACode] 命令输出落盘失败: ${message}\r\n`);
366
+ });
367
+ return;
368
+ }
369
+ appendCommandOutput(entry, output);
370
+ };
371
+ if (usesAcodeCommandOutputPolicy) {
372
+ resetInactivityTimer();
373
+ }
374
+ child.stdout.on("data", (chunk) => {
375
+ appendProcessOutput(decodeTerminalOutputChunk(stdoutDecoder, chunk));
272
376
  });
273
- child.stderr.on('data', (chunk) => {
274
- appendCommandOutput(entry, decodeTerminalOutputChunk(stderrDecoder, chunk));
377
+ child.stderr.on("data", (chunk) => {
378
+ appendProcessOutput(decodeTerminalOutputChunk(stderrDecoder, chunk));
275
379
  });
276
- child.on('error', (error) => {
380
+ child.on("error", (error) => {
277
381
  appendCommandOutput(entry, `\r\n[Terminal Error] ${error.message}\r\n`);
278
382
  });
279
- child.on('close', (code, signal) => {
383
+ child.on("close", (code, signal) => {
384
+ clearInactivityTimer();
280
385
  state.runningProcess = null;
281
- appendCommandOutput(entry, decodeTerminalOutputChunk(stdoutDecoder, undefined, true));
282
- appendCommandOutput(entry, decodeTerminalOutputChunk(stderrDecoder, undefined, true));
386
+ appendProcessOutput(decodeTerminalOutputChunk(stdoutDecoder, undefined, true), false);
387
+ appendProcessOutput(decodeTerminalOutputChunk(stderrDecoder, undefined, true), false);
388
+ if (outputSpilloverState.spilled) {
389
+ void persistTerminalCommandOutput(outputSpilloverState).catch((error) => {
390
+ const message = error instanceof Error ? error.message : String(error);
391
+ appendCommandOutput(entry, `\r\n[ACode] 命令输出最终落盘失败: ${message}\r\n`);
392
+ });
393
+ }
283
394
  if (code && code !== 0) {
284
395
  appendCommandOutput(entry, `\r\n[exit ${code}]\r\n`);
285
396
  }
@@ -287,7 +398,7 @@ async function executeTerminalCommand(sessionId, commandValue) {
287
398
  appendCommandOutput(entry, `\r\n[signal ${signal}]\r\n`);
288
399
  }
289
400
  appendCommandOutput(entry, formatTerminalPrompt(state.currentDirectory));
290
- void sendStatus(entry.agentId, sessionId, 'waiting_input');
401
+ void sendStatus(entry.agentId, sessionId, "waiting_input");
291
402
  });
292
403
  return { started: true, cwd: state.currentDirectory };
293
404
  }
@@ -315,7 +426,7 @@ export function buildRuntimeEnv(agentId, workspacePath, baseEnv, envOverrides) {
315
426
  }
316
427
  }
317
428
  env.AWS_AGENT_ID = String(agentId);
318
- env.AWS_MCP_CLAIM_LAUNCH_BINDING = 'true';
429
+ env.AWS_MCP_CLAIM_LAUNCH_BINDING = "true";
319
430
  return env;
320
431
  }
321
432
  function stripWrappingQuotes(value) {
@@ -340,7 +451,7 @@ export async function resolveSdkInputMessage(input) {
340
451
  if (!path.isAbsolute(filePath)) {
341
452
  return message;
342
453
  }
343
- return fs.readFile(filePath, 'utf-8');
454
+ return fs.readFile(filePath, "utf-8");
344
455
  }
345
456
  /**
346
457
  * 主流程:将已持久化的 ClaudeCode AI 配置合并到启动配置,再让请求级覆盖保持最高优先级。
@@ -363,9 +474,9 @@ export function buildClaudeCodeLaunchConfig(savedClaudeConfig, requestEnvOverrid
363
474
  envOverrides[normalizedKey] = normalizedValue;
364
475
  }
365
476
  }
366
- const model = envOverrides.ANTHROPIC_MODEL?.trim()
367
- || envOverrides.CODEX_MODEL?.trim()
368
- || undefined;
477
+ const model = envOverrides.ANTHROPIC_MODEL?.trim() ||
478
+ envOverrides.CODEX_MODEL?.trim() ||
479
+ undefined;
369
480
  return { envOverrides, model };
370
481
  }
371
482
  /**
@@ -385,7 +496,7 @@ function formatToolResultForTimeline(result) {
385
496
  if (result === undefined || result === null) {
386
497
  return undefined;
387
498
  }
388
- const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
499
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
389
500
  const normalized = text.trim();
390
501
  if (!normalized) {
391
502
  return undefined;
@@ -397,18 +508,20 @@ function formatToolResultForTimeline(result) {
397
508
  }
398
509
  function isMcpProviderEvent(event) {
399
510
  const actionType = event.data.actionType;
400
- if (actionType === 'mcp') {
511
+ if (actionType === "mcp") {
401
512
  return true;
402
513
  }
403
- const toolName = String(event.data.toolName || '').toLowerCase();
404
- return toolName.startsWith('mcp__') || toolName.includes('-mcp__') || toolName.includes('mcp_');
514
+ const toolName = String(event.data.toolName || "").toLowerCase();
515
+ return (toolName.startsWith("mcp__") ||
516
+ toolName.includes("-mcp__") ||
517
+ toolName.includes("mcp_"));
405
518
  }
406
519
  /**
407
520
  * 将 SDK 工具结束事件转换为时间线返回数据状态。
408
521
  * 主流程:格式化工具结果 -> 保留 MCP 专用展示 -> 其它工具按动作类型追加到对应时间线事件。
409
522
  */
410
523
  export function buildToolResultTimelineActionInfo(event) {
411
- if (event.type !== 'tool_use_end') {
524
+ if (event.type !== "tool_use_end") {
412
525
  return undefined;
413
526
  }
414
527
  const actionResult = formatToolResultForTimeline(event.data.toolResult ?? event.data.actionResult);
@@ -417,13 +530,13 @@ export function buildToolResultTimelineActionInfo(event) {
417
530
  }
418
531
  const inferredAction = getToolActionInfo(event.data.toolName, event.data.toolInput);
419
532
  const actionType = event.data.actionType || inferredAction.actionType;
420
- if (actionType === 'idle') {
533
+ if (actionType === "idle") {
421
534
  return undefined;
422
535
  }
423
- if (actionType === 'mcp' && isMcpProviderEvent(event)) {
536
+ if (actionType === "mcp" && isMcpProviderEvent(event)) {
424
537
  return {
425
- actionType: 'mcp',
426
- actionLabel: 'MCP返回',
538
+ actionType: "mcp",
539
+ actionLabel: "MCP返回",
427
540
  actionDetail: event.data.actionDetail || event.data.toolName,
428
541
  actionId: event.data.actionId || event.data.toolUseId,
429
542
  actionResult,
@@ -432,17 +545,224 @@ export function buildToolResultTimelineActionInfo(event) {
432
545
  return {
433
546
  actionType,
434
547
  actionLabel: event.data.actionLabel || inferredAction.actionLabel,
435
- actionDetail: event.data.actionDetail || inferredAction.actionDetail || event.data.toolName,
548
+ actionDetail: event.data.actionDetail ||
549
+ inferredAction.actionDetail ||
550
+ event.data.toolName,
436
551
  actionId: event.data.actionId || event.data.toolUseId,
437
552
  actionResult,
438
553
  };
439
554
  }
440
555
  export function formatSdkOutputEvent(event) {
441
- if (event.type === 'text_delta' || event.type === 'thinking') {
556
+ if (event.type === "ai_request") {
557
+ const model = event.data.model ? ` ${event.data.model}` : "";
558
+ const preview = event.data.lastUserMessagePreview
559
+ ? `:${event.data.lastUserMessagePreview}`
560
+ : "";
561
+ return `\r\n[ACode] 正在请求模型${model}${preview}\r\n`;
562
+ }
563
+ if (event.type === "text_delta") {
442
564
  return event.data.text || undefined;
443
565
  }
444
- if (event.type === 'error') {
445
- return event.data.text ? `\r\n[SDK Error] ${event.data.text}\r\n` : undefined;
566
+ if (event.type === "thinking") {
567
+ return event.data.text || undefined;
568
+ }
569
+ if (event.type === "error") {
570
+ return event.data.text
571
+ ? `\r\n[SDK Error] ${event.data.text}\r\n`
572
+ : undefined;
573
+ }
574
+ return undefined;
575
+ }
576
+ export function buildSdkOutputMetadata(event) {
577
+ const logMessage = formatSdkLogMessage(event);
578
+ return {
579
+ sourceEventType: event.type,
580
+ logCategory: "acode",
581
+ ...(logMessage
582
+ ? {
583
+ logLevel: event.type === "error" ? "error" : "info",
584
+ logMessage,
585
+ }
586
+ : {}),
587
+ };
588
+ }
589
+ function formatSdkLogMessage(event) {
590
+ if (event.type === "ai_request") {
591
+ const model = event.data.model || "unknown-model";
592
+ const endpoint = event.data.endpoint ? ` @ ${event.data.endpoint}` : "";
593
+ const toolCount = typeof event.data.toolCount === "number"
594
+ ? `, tools=${event.data.toolCount}`
595
+ : "";
596
+ return `AI request started: ${model}${endpoint}${toolCount}`;
597
+ }
598
+ if (event.type === "tool_use_start") {
599
+ return formatSdkToolLogMessage(event, "开始");
600
+ }
601
+ if (event.type === "tool_use_end") {
602
+ return formatSdkToolLogMessage(event, "完成");
603
+ }
604
+ if (event.type === "turn_complete") {
605
+ const finishReason = event.data.finishReason
606
+ ? `, finish=${event.data.finishReason}`
607
+ : "";
608
+ return `Turn completed${finishReason}`;
609
+ }
610
+ if (event.type === "session_complete") {
611
+ return `Session completed: exitCode=${event.data.exitCode ?? 0}`;
612
+ }
613
+ if (event.type === "error") {
614
+ return event.data.text || "ACode runtime error";
615
+ }
616
+ return undefined;
617
+ }
618
+ function formatSdkToolLogMessage(event, phaseLabel) {
619
+ const actionInfo = getToolActionInfo(event.data.toolName, event.data.toolInput);
620
+ const actionLabel = event.data.actionLabel || actionInfo.actionLabel || "调用工具";
621
+ const detail = resolveSdkToolLogDetail(event, actionInfo);
622
+ return detail
623
+ ? `${phaseLabel}${actionLabel}: ${detail}`
624
+ : `${phaseLabel}${actionLabel}: ${event.data.actionDetail || event.data.toolName || "tool"}`;
625
+ }
626
+ function resolveSdkToolLogDetail(event, actionInfo) {
627
+ const input = event.data.toolInput;
628
+ if (actionInfo.actionType === "bash") {
629
+ return getCommandDisplayDetail(input) || event.data.actionDetail || actionInfo.actionDetail;
630
+ }
631
+ if (actionInfo.actionType === "read_file" ||
632
+ actionInfo.actionType === "edit_file" ||
633
+ actionInfo.actionType === "write_file") {
634
+ return getFileDisplayDetail(input) || event.data.actionDetail || actionInfo.actionDetail;
635
+ }
636
+ return event.data.actionDetail || actionInfo.actionDetail;
637
+ }
638
+ function getCommandDisplayDetail(input) {
639
+ const command = firstDisplayString(input?.command, input?.cmd);
640
+ if (command)
641
+ return command;
642
+ if (Array.isArray(input?.argv)) {
643
+ const argv = input.argv
644
+ .map((item) => String(item ?? "").trim())
645
+ .filter(Boolean)
646
+ .join(" ");
647
+ return argv ? redactTransientClipboardPastePaths(argv) : undefined;
648
+ }
649
+ return firstDisplayString(input?.args);
650
+ }
651
+ function getFileDisplayDetail(input) {
652
+ const paths = displayStringList(input?.paths);
653
+ if (paths.length > 0) {
654
+ return paths.length === 1
655
+ ? paths[0]
656
+ : `${paths.length}个文件: ${paths.join(", ")}`;
657
+ }
658
+ return firstDisplayString(input?.file_path, input?.filePath, input?.path, input?.target_file, input?.targetFile);
659
+ }
660
+ function firstDisplayString(...values) {
661
+ for (const value of values) {
662
+ if (typeof value === "string" && value.trim()) {
663
+ return redactTransientClipboardPastePaths(value.trim());
664
+ }
665
+ if (Array.isArray(value)) {
666
+ const first = displayStringList(value)[0];
667
+ if (first)
668
+ return first;
669
+ }
670
+ }
671
+ return undefined;
672
+ }
673
+ function displayStringList(value) {
674
+ if (!Array.isArray(value))
675
+ return [];
676
+ return value
677
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
678
+ .filter(Boolean)
679
+ .map((item) => redactTransientClipboardPastePaths(item));
680
+ }
681
+ function formatAiRequestDetail(data) {
682
+ const lines = [
683
+ data.model ? `模型: ${data.model}` : undefined,
684
+ data.endpoint ? `接口: ${data.endpoint}` : undefined,
685
+ typeof data.stream === "boolean"
686
+ ? `流式: ${data.stream ? "是" : "否"}`
687
+ : undefined,
688
+ typeof data.messageCount === "number"
689
+ ? `上下文消息: ${data.messageCount}`
690
+ : undefined,
691
+ typeof data.toolCount === "number"
692
+ ? `可用工具: ${data.toolCount}`
693
+ : undefined,
694
+ data.lastUserMessagePreview
695
+ ? `最后请求: ${data.lastUserMessagePreview}`
696
+ : undefined,
697
+ ].filter((line) => Boolean(line));
698
+ const base = lines.join("\n") || "ACode 正在发送 AI 请求";
699
+ return data.requestBody
700
+ ? `${base}${AI_REQUEST_BODY_MARKER}${data.requestBody}`
701
+ : base;
702
+ }
703
+ /**
704
+ * 主干流程:把 provider 事件转换为面板运行状态。
705
+ *
706
+ * text_delta 表示助手正在生成;thinking 表示推理/内部过程。
707
+ * tool_use_start 表示正在执行工具;turn_complete 才表示真正空闲。
708
+ */
709
+ export function resolveRuntimeStatusFromProviderEvent(event) {
710
+ if (event.type === "ai_request") {
711
+ return {
712
+ status: "thinking",
713
+ actionInfo: {
714
+ actionType: "ai_request",
715
+ actionLabel: "AI请求",
716
+ actionDetail: formatAiRequestDetail(event.data),
717
+ },
718
+ };
719
+ }
720
+ if (event.type === "thinking" || event.type === "text_delta") {
721
+ return { status: "thinking" };
722
+ }
723
+ if (event.type === "tool_use_start") {
724
+ const inferredAction = getToolActionInfo(event.data.toolName, event.data.toolInput);
725
+ const actionDetail = resolveSdkToolLogDetail(event, inferredAction);
726
+ if (event.data.actionType === "idle" ||
727
+ inferredAction.actionType === "idle") {
728
+ return {
729
+ status: "waiting_input",
730
+ actionInfo: {
731
+ actionType: event.data.actionType || inferredAction.actionType,
732
+ actionLabel: event.data.actionLabel || inferredAction.actionLabel,
733
+ actionDetail: actionDetail || event.data.actionDetail || inferredAction.actionDetail || event.data.toolName,
734
+ actionId: event.data.actionId || event.data.toolUseId,
735
+ },
736
+ };
737
+ }
738
+ return {
739
+ status: "tool_using",
740
+ actionInfo: {
741
+ actionType: event.data.actionType || inferredAction.actionType,
742
+ actionLabel: event.data.actionLabel || inferredAction.actionLabel,
743
+ actionDetail: actionDetail || event.data.actionDetail || inferredAction.actionDetail || event.data.toolName,
744
+ actionId: event.data.actionId || event.data.toolUseId,
745
+ },
746
+ };
747
+ }
748
+ if (event.type === "turn_complete") {
749
+ const responseActionInfo = event.data
750
+ .responseBody
751
+ ? {
752
+ actionType: "ai_request",
753
+ actionLabel: "AI响应",
754
+ actionResult: event.data.responseBody,
755
+ usage: event.data.usage,
756
+ }
757
+ : undefined;
758
+ return {
759
+ status: "waiting_input",
760
+ actionInfo: responseActionInfo ||
761
+ (event.data.usage ? { usage: event.data.usage } : undefined),
762
+ };
763
+ }
764
+ if (event.type === "error") {
765
+ return { status: "error" };
446
766
  }
447
767
  return undefined;
448
768
  }
@@ -452,7 +772,7 @@ function forwardSdkOutputEvent(event, entry) {
452
772
  return;
453
773
  }
454
774
  entry.seq += 1;
455
- void sendOutput(entry.agentId, output, event.sessionId, entry.seq);
775
+ void sendOutput(entry.agentId, output, event.sessionId, entry.seq, buildSdkOutputMetadata(event), { runtimeAccessToken: entry.runtimeAccessToken });
456
776
  }
457
777
  export function resolveSdkProviderId(command) {
458
778
  return resolveSdkProviderIdByCommand(command);
@@ -466,45 +786,52 @@ function ensureAdapterInitialized() {
466
786
  adapterInitialized = true;
467
787
  }
468
788
  function wireSdkAdapterEvents(adapter, definition) {
469
- adapter.on('event', (event) => {
789
+ adapter.on("event", (event) => {
470
790
  const entry = sdkSessions.get(event.sessionId);
471
791
  if (entry) {
472
792
  console.log(`[${definition.displayName} Adapter] Event: ${event.type} for session ${event.sessionId}`);
793
+ const runtimeStatus = resolveRuntimeStatusFromProviderEvent(event);
794
+ if (runtimeStatus) {
795
+ void sendStatus(entry.agentId, event.sessionId, runtimeStatus.status, runtimeStatus.actionInfo, resolveStatusChangeUsage(runtimeStatus.actionInfo), { runtimeAccessToken: entry.runtimeAccessToken });
796
+ }
473
797
  forwardSdkOutputEvent(event, entry);
474
798
  const toolResultActionInfo = buildToolResultTimelineActionInfo(event);
475
799
  if (toolResultActionInfo) {
476
- void sendStatus(entry.agentId, event.sessionId, 'tool_result', toolResultActionInfo);
477
- }
478
- if (event.type === 'turn_complete' && event.data.usage) {
479
- sendStatus(entry.agentId, event.sessionId, 'waiting_input', undefined, resolveStatusChangeUsage({ usage: event.data.usage }));
800
+ void sendStatus(entry.agentId, event.sessionId, "tool_result", toolResultActionInfo, undefined, {
801
+ runtimeAccessToken: entry.runtimeAccessToken,
802
+ });
480
803
  }
481
804
  }
482
805
  });
483
- adapter.on('status-change', (sessionId, status, actionInfo) => {
806
+ adapter.on("status-change", (sessionId, status, actionInfo) => {
484
807
  const entry = sdkSessions.get(sessionId);
485
808
  if (entry) {
486
809
  entry.currentRuntimeStatus = status;
487
810
  entry.currentRuntimeActionType = actionInfo?.actionType;
488
811
  entry.currentRuntimeActionLabel = actionInfo?.actionLabel;
489
812
  entry.currentRuntimeActionDetail = actionInfo?.actionDetail;
490
- console.log(`[${definition.displayName} Adapter] Status changed: ${status} for agent ${entry.agentId}`, actionInfo || '');
813
+ console.log(`[${definition.displayName} Adapter] Status changed: ${status} for agent ${entry.agentId}`, actionInfo || "");
491
814
  void updatePersistedSessionRuntimeState(sessionId, {
492
815
  runtimeStatus: status,
493
816
  runtimeActionType: actionInfo?.actionType,
494
817
  runtimeActionLabel: actionInfo?.actionLabel,
495
818
  runtimeActionDetail: actionInfo?.actionDetail,
496
819
  });
497
- void sendStatus(entry.agentId, sessionId, status, actionInfo, resolveStatusChangeUsage(actionInfo));
820
+ void sendStatus(entry.agentId, sessionId, status, actionInfo, resolveStatusChangeUsage(actionInfo), {
821
+ runtimeAccessToken: entry.runtimeAccessToken,
822
+ });
498
823
  }
499
824
  });
500
- adapter.on('ask-user-question', (data) => {
825
+ adapter.on("ask-user-question", (data) => {
501
826
  const entry = sdkSessions.get(data.sessionId);
502
827
  if (entry) {
503
828
  console.log(`[${definition.displayName} Adapter] AskUserQuestion for agent ${entry.agentId}`);
504
- void sendQuestionRequest(entry.agentId, data.sessionId, Array.isArray(data.questions) ? data.questions : []);
829
+ void sendQuestionRequest(entry.agentId, data.sessionId, Array.isArray(data.questions) ? data.questions : [], {
830
+ runtimeAccessToken: entry.runtimeAccessToken,
831
+ });
505
832
  }
506
833
  });
507
- adapter.on('permission-request', (data) => {
834
+ adapter.on("permission-request", (data) => {
508
835
  const entry = sdkSessions.get(data.sessionId);
509
836
  if (entry) {
510
837
  console.log(`[${definition.displayName} Adapter] Permission request: ${data.toolName} for agent ${entry.agentId}`);
@@ -515,10 +842,10 @@ function wireSdkAdapterEvents(adapter, definition) {
515
842
  * 启动 SDK Agent 会话
516
843
  * POST /runtime/start
517
844
  */
518
- terminalRouter.post('/start', validateToken, async (req, res) => {
519
- const { agentId, workspacePath, mode = 'sdk', } = req.body || {};
845
+ terminalRouter.post("/start", validateToken, async (req, res) => {
846
+ const { agentId, workspacePath, mode = "sdk" } = req.body || {};
520
847
  if (!agentId || !workspacePath) {
521
- res.status(400).json({ error: 'agentId and workspacePath are required' });
848
+ res.status(400).json({ error: "agentId and workspacePath are required" });
522
849
  return;
523
850
  }
524
851
  enqueueMcpLaunchBinding({
@@ -529,7 +856,7 @@ terminalRouter.post('/start', validateToken, async (req, res) => {
529
856
  userId: req.body?.userId,
530
857
  schedulerBaseUrl: req.body?.schedulerBaseUrl,
531
858
  });
532
- if (mode && mode !== 'sdk') {
859
+ if (mode && mode !== "sdk") {
533
860
  console.warn(`[Runtime] Unsupported runtime mode "${mode}" requested for agent ${agentId}; starting SDK session instead.`);
534
861
  }
535
862
  await startSdkSession(req, res);
@@ -545,11 +872,12 @@ async function startSdkSession(req, res) {
545
872
  // 空闲命令配置
546
873
  idleInputAutoCommand, nonInputAutoCommand, } = req.body || {};
547
874
  let sessionId = null;
548
- const normalizedAgentId = String(agentId || '').trim();
875
+ const normalizedAgentId = String(agentId || "").trim();
876
+ const runtimeAccessToken = String(req.body?.runtimeAccessToken || "").trim() || undefined;
549
877
  try {
550
878
  ensureAdapterInitialized();
551
879
  if (!normalizedAgentId) {
552
- res.status(400).json({ error: 'agentId is required' });
880
+ res.status(400).json({ error: "agentId is required" });
553
881
  return;
554
882
  }
555
883
  const activeSession = findActiveSdkSessionByAgentId(normalizedAgentId);
@@ -561,8 +889,8 @@ async function startSdkSession(req, res) {
561
889
  if (persistedSession) {
562
890
  const reuseDecision = evaluatePersistedSessionReuse(persistedSession);
563
891
  if (!reuseDecision.reusable) {
564
- console.warn(`[Runtime] Ignoring stale persisted SDK session for agentId=${normalizedAgentId}, `
565
- + `sessionId=${persistedSession.sessionId}, reason=${reuseDecision.reason}`);
892
+ console.warn(`[Runtime] Ignoring stale persisted SDK session for agentId=${normalizedAgentId}, ` +
893
+ `sessionId=${persistedSession.sessionId}, reason=${reuseDecision.reason}`);
566
894
  await removePersistedSession(persistedSession.sessionId);
567
895
  }
568
896
  else {
@@ -572,7 +900,7 @@ async function startSdkSession(req, res) {
572
900
  agentId: persistedSession.agentId,
573
901
  workspacePath: persistedSession.workspacePath,
574
902
  command: persistedSession.command,
575
- mode: persistedSession.mode || 'sdk',
903
+ mode: persistedSession.mode || "sdk",
576
904
  pid: persistedSession.pid,
577
905
  reused: true,
578
906
  persistedOnly: true,
@@ -581,26 +909,28 @@ async function startSdkSession(req, res) {
581
909
  }
582
910
  }
583
911
  if (startingAgents.has(normalizedAgentId)) {
584
- res.status(409).json({ error: 'SDK session is already starting for this agent' });
912
+ res
913
+ .status(409)
914
+ .json({ error: "SDK session is already starting for this agent" });
585
915
  return;
586
916
  }
587
917
  startingAgents.add(normalizedAgentId);
588
918
  // 根据命令选择对应的 adapter
589
- const normalizedCommand = (command || 'acode').toLowerCase();
919
+ const normalizedCommand = (command || "acode").toLowerCase();
590
920
  const providerId = resolveSdkProviderId(command);
591
921
  // ★ 特征日志:明确输出选择的 provider
592
922
  console.log(`[Runtime] ★★★ SDK启动 ★★★ command="${command}", normalizedCommand="${normalizedCommand}", providerId="${providerId}"`);
593
923
  const adapter = adapterRegistry.get(providerId);
594
924
  sessionId = uuidv4();
595
- const savedClaudeConfig = providerId === 'claude-code'
925
+ const savedClaudeConfig = providerId === "claude-code"
596
926
  ? await readSavedClaudeCodeLaunchConfig()
597
927
  : null;
598
- const claudeLaunchConfig = providerId === 'claude-code'
928
+ const claudeLaunchConfig = providerId === "claude-code"
599
929
  ? buildClaudeCodeLaunchConfig(savedClaudeConfig, envOverrides)
600
930
  : buildClaudeCodeLaunchConfig(null, envOverrides);
601
931
  // 构建SDK配置
602
932
  const config = {
603
- command: command || 'acode',
933
+ command: command || "acode",
604
934
  workingDirectory: workspacePath,
605
935
  autoAccept: Boolean(autoAccept),
606
936
  initialPrompt,
@@ -618,22 +948,30 @@ async function startSdkSession(req, res) {
618
948
  config,
619
949
  providerId, // 记录使用的 provider
620
950
  seq: 0,
951
+ runtimeAccessToken,
621
952
  });
622
953
  // 启动 SDK 会话
623
954
  await adapter.startSession(sessionId, config);
955
+ // 尽早注入空闲命令,避免后续 PID 探测/进程注册延迟 poll_message 空闲轮询。
956
+ if (idleInputAutoCommand || nonInputAutoCommand) {
957
+ adapter.setIdleCommands?.(sessionId, {
958
+ idleInputCommand: idleInputAutoCommand || "",
959
+ nonInputCommand: nonInputAutoCommand || "",
960
+ });
961
+ }
624
962
  // ★ 尝试获取 SDK 进程的 PID(用于孤儿进程检测)
625
963
  // SDK 不直接暴露 PID,通过进程检测服务查找匹配的进程
626
964
  let sdkPid;
627
965
  try {
628
966
  // 等待一小段时间让进程启动
629
- await new Promise(resolve => setTimeout(resolve, 500));
967
+ await new Promise((resolve) => setTimeout(resolve, 500));
630
968
  // 尝试通过 adapter 获取 PID(如果支持)
631
969
  sdkPid = adapter.getSessionPid?.(sessionId);
632
970
  // 如果 adapter 不支持,尝试通过进程检测服务查找
633
971
  if (!sdkPid) {
634
972
  const processes = findClaudeCodeProcesses();
635
973
  // 查找匹配 agentId 或工作目录的进程
636
- const matched = processes.find(p => p.agentId === agentId ||
974
+ const matched = processes.find((p) => p.agentId === agentId ||
637
975
  p.cwd === workspacePath ||
638
976
  p.command.includes(workspacePath));
639
977
  if (matched) {
@@ -653,9 +991,9 @@ async function startSdkSession(req, res) {
653
991
  workspacePath,
654
992
  command: config.command,
655
993
  startedAt: new Date().toISOString(),
656
- status: 'running',
994
+ status: "running",
657
995
  runtimeStatus: startedStatus,
658
- mode: 'sdk',
996
+ mode: "sdk",
659
997
  pid: sdkPid,
660
998
  providerSessionId: adapter.getProviderSessionId(sessionId),
661
999
  });
@@ -666,18 +1004,11 @@ async function startSdkSession(req, res) {
666
1004
  agentId: normalizedAgentId,
667
1005
  sessionId,
668
1006
  pid: sdkPid,
669
- mode: 'sdk',
1007
+ mode: "sdk",
670
1008
  workspacePath,
671
1009
  command: config.command,
672
1010
  });
673
1011
  }
674
- // 存储空闲命令配置(用于SDK模式的空闲检测)
675
- if (idleInputAutoCommand || nonInputAutoCommand) {
676
- adapter.setIdleCommands?.(sessionId, {
677
- idleInputCommand: idleInputAutoCommand || '',
678
- nonInputCommand: nonInputAutoCommand || '',
679
- });
680
- }
681
1012
  res.json({
682
1013
  sessionId,
683
1014
  status: startedStatus,
@@ -685,7 +1016,7 @@ async function startSdkSession(req, res) {
685
1016
  agentId,
686
1017
  workspacePath,
687
1018
  command: config.command,
688
- mode: 'sdk',
1019
+ mode: "sdk",
689
1020
  pid: sdkPid, // ★ 返回 PID 信息
690
1021
  });
691
1022
  }
@@ -695,7 +1026,9 @@ async function startSdkSession(req, res) {
695
1026
  await removePersistedSession(sessionId);
696
1027
  }
697
1028
  const errorMessage = error instanceof Error ? error.message : String(error);
698
- res.status(500).json({ error: `Failed to start SDK session: ${errorMessage}` });
1029
+ res
1030
+ .status(500)
1031
+ .json({ error: `Failed to start SDK session: ${errorMessage}` });
699
1032
  }
700
1033
  finally {
701
1034
  if (normalizedAgentId) {
@@ -708,20 +1041,20 @@ async function startSdkSession(req, res) {
708
1041
  * 发送消息到 SDK 会话
709
1042
  * POST /runtime/sdk/message
710
1043
  */
711
- terminalRouter.post('/sdk/message', validateToken, async (req, res) => {
1044
+ terminalRouter.post("/sdk/message", validateToken, async (req, res) => {
712
1045
  const { sessionId, message } = req.body || {};
713
1046
  if (!sessionId || !message) {
714
- res.status(400).json({ error: 'sessionId and message are required' });
1047
+ res.status(400).json({ error: "sessionId and message are required" });
715
1048
  return;
716
1049
  }
717
1050
  const entry = sdkSessions.get(sessionId);
718
1051
  if (!entry) {
719
- res.status(404).json({ error: 'SDK session not found' });
1052
+ res.status(404).json({ error: "SDK session not found" });
720
1053
  return;
721
1054
  }
722
1055
  try {
723
1056
  // 根据 providerId 获取对应的 adapter
724
- const providerId = entry.providerId || 'claude-code';
1057
+ const providerId = entry.providerId || "claude-code";
725
1058
  const adapter = adapterRegistry.get(providerId);
726
1059
  await adapter.sendMessage(sessionId, message);
727
1060
  res.json({ ok: true });
@@ -735,20 +1068,20 @@ terminalRouter.post('/sdk/message', validateToken, async (req, res) => {
735
1068
  * 回答 AskUserQuestion
736
1069
  * POST /runtime/sdk/answer
737
1070
  */
738
- terminalRouter.post('/sdk/answer', validateToken, async (req, res) => {
1071
+ terminalRouter.post("/sdk/answer", validateToken, async (req, res) => {
739
1072
  const { sessionId, answers } = req.body || {};
740
1073
  if (!sessionId || !answers) {
741
- res.status(400).json({ error: 'sessionId and answers are required' });
1074
+ res.status(400).json({ error: "sessionId and answers are required" });
742
1075
  return;
743
1076
  }
744
1077
  const entry = sdkSessions.get(sessionId);
745
1078
  if (!entry) {
746
- res.status(404).json({ error: 'SDK session not found' });
1079
+ res.status(404).json({ error: "SDK session not found" });
747
1080
  return;
748
1081
  }
749
1082
  try {
750
1083
  // 根据 providerId 获取对应的 adapter
751
- const providerId = entry.providerId || 'claude-code';
1084
+ const providerId = entry.providerId || "claude-code";
752
1085
  const adapter = adapterRegistry.get(providerId);
753
1086
  if (adapter.sendQuestionAnswer) {
754
1087
  await adapter.sendQuestionAnswer(sessionId, answers);
@@ -764,20 +1097,20 @@ terminalRouter.post('/sdk/answer', validateToken, async (req, res) => {
764
1097
  * 确认权限请求
765
1098
  * POST /runtime/sdk/confirm
766
1099
  */
767
- terminalRouter.post('/sdk/confirm', validateToken, async (req, res) => {
1100
+ terminalRouter.post("/sdk/confirm", validateToken, async (req, res) => {
768
1101
  const { sessionId, accept } = req.body || {};
769
1102
  if (!sessionId) {
770
- res.status(400).json({ error: 'sessionId is required' });
1103
+ res.status(400).json({ error: "sessionId is required" });
771
1104
  return;
772
1105
  }
773
1106
  const entry = sdkSessions.get(sessionId);
774
1107
  if (!entry) {
775
- res.status(404).json({ error: 'SDK session not found' });
1108
+ res.status(404).json({ error: "SDK session not found" });
776
1109
  return;
777
1110
  }
778
1111
  try {
779
1112
  // 根据 providerId 获取对应的 adapter
780
- const providerId = entry.providerId || 'claude-code';
1113
+ const providerId = entry.providerId || "claude-code";
781
1114
  const adapter = adapterRegistry.get(providerId);
782
1115
  await adapter.sendConfirmation(sessionId, Boolean(accept));
783
1116
  res.json({ ok: true });
@@ -791,20 +1124,20 @@ terminalRouter.post('/sdk/confirm', validateToken, async (req, res) => {
791
1124
  * 中止当前轮次
792
1125
  * POST /runtime/sdk/abort
793
1126
  */
794
- terminalRouter.post('/sdk/abort', validateToken, async (req, res) => {
1127
+ terminalRouter.post("/sdk/abort", validateToken, async (req, res) => {
795
1128
  const { sessionId } = req.body || {};
796
1129
  if (!sessionId) {
797
- res.status(400).json({ error: 'sessionId is required' });
1130
+ res.status(400).json({ error: "sessionId is required" });
798
1131
  return;
799
1132
  }
800
1133
  const entry = sdkSessions.get(sessionId);
801
1134
  if (!entry) {
802
- res.status(404).json({ error: 'SDK session not found' });
1135
+ res.status(404).json({ error: "SDK session not found" });
803
1136
  return;
804
1137
  }
805
1138
  try {
806
1139
  // 根据 providerId 获取对应的 adapter
807
- const providerId = entry.providerId || 'claude-code';
1140
+ const providerId = entry.providerId || "claude-code";
808
1141
  const adapter = adapterRegistry.get(providerId);
809
1142
  await adapter.abortCurrentTurn(sessionId);
810
1143
  res.json({ ok: true });
@@ -818,20 +1151,20 @@ terminalRouter.post('/sdk/abort', validateToken, async (req, res) => {
818
1151
  * 停止 SDK 会话
819
1152
  * POST /runtime/sdk/stop
820
1153
  */
821
- terminalRouter.post('/sdk/stop', validateToken, async (req, res) => {
1154
+ terminalRouter.post("/sdk/stop", validateToken, async (req, res) => {
822
1155
  const { sessionId } = req.body || {};
823
1156
  if (!sessionId) {
824
- res.status(400).json({ error: 'sessionId is required' });
1157
+ res.status(400).json({ error: "sessionId is required" });
825
1158
  return;
826
1159
  }
827
1160
  const entry = sdkSessions.get(sessionId);
828
1161
  if (!entry) {
829
- res.json({ ok: true, status: 'stopped' });
1162
+ res.json({ ok: true, status: "stopped" });
830
1163
  return;
831
1164
  }
832
1165
  try {
833
1166
  // 根据 providerId 获取对应的 adapter
834
- const providerId = entry.providerId || 'claude-code';
1167
+ const providerId = entry.providerId || "claude-code";
835
1168
  const adapter = adapterRegistry.get(providerId);
836
1169
  await adapter.terminateSession(sessionId);
837
1170
  sdkSessions.delete(sessionId);
@@ -840,8 +1173,10 @@ terminalRouter.post('/sdk/stop', validateToken, async (req, res) => {
840
1173
  // ★ 从进程管理器中移除
841
1174
  const processManager = getAgentProcessManager();
842
1175
  await processManager.removeProcess(entry.agentId);
843
- await sendStatus(entry.agentId, null, 'stopped');
844
- res.json({ ok: true, status: 'stopped' });
1176
+ await sendStatus(entry.agentId, null, "stopped", undefined, undefined, {
1177
+ runtimeAccessToken: entry.runtimeAccessToken,
1178
+ });
1179
+ res.json({ ok: true, status: "stopped" });
845
1180
  }
846
1181
  catch (error) {
847
1182
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -852,20 +1187,20 @@ terminalRouter.post('/sdk/stop', validateToken, async (req, res) => {
852
1187
  * 获取 SDK 会话状态
853
1188
  * GET /runtime/sdk/status/:sessionId
854
1189
  */
855
- terminalRouter.get('/sdk/status/:sessionId', validateToken, (req, res) => {
1190
+ terminalRouter.get("/sdk/status/:sessionId", validateToken, (req, res) => {
856
1191
  const sessionId = req.params.sessionId;
857
1192
  if (!sessionId) {
858
- res.status(400).json({ error: 'sessionId is required' });
1193
+ res.status(400).json({ error: "sessionId is required" });
859
1194
  return;
860
1195
  }
861
1196
  const entry = sdkSessions.get(sessionId);
862
1197
  if (!entry) {
863
- res.status(404).json({ error: 'SDK session not found' });
1198
+ res.status(404).json({ error: "SDK session not found" });
864
1199
  return;
865
1200
  }
866
1201
  try {
867
1202
  // 根据 providerId 获取对应的 adapter
868
- const providerId = entry.providerId || 'claude-code';
1203
+ const providerId = entry.providerId || "claude-code";
869
1204
  const adapter = adapterRegistry.get(providerId);
870
1205
  const status = adapter.getSessionStatus(sessionId);
871
1206
  const conversation = adapter.getConversation(sessionId);
@@ -888,15 +1223,15 @@ terminalRouter.get('/sdk/status/:sessionId', validateToken, (req, res) => {
888
1223
  * 主流程:校验 session/command -> 读取会话 workspacePath -> 在该 cwd 下启动 shell 命令 -> 通过终端输出回调流式返回。
889
1224
  * POST /runtime/command
890
1225
  */
891
- terminalRouter.post('/command', validateToken, async (req, res) => {
1226
+ terminalRouter.post("/command", validateToken, async (req, res) => {
892
1227
  const { sessionId, command } = req.body || {};
893
1228
  if (!sessionId || command === undefined || command === null) {
894
- res.status(400).json({ error: 'sessionId and command are required' });
1229
+ res.status(400).json({ error: "sessionId and command are required" });
895
1230
  return;
896
1231
  }
897
1232
  try {
898
1233
  const result = await executeTerminalCommand(String(sessionId), command);
899
- res.json({ ok: true, mode: 'terminal', ...result });
1234
+ res.json({ ok: true, mode: "terminal", ...result });
900
1235
  }
901
1236
  catch (error) {
902
1237
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -910,31 +1245,33 @@ terminalRouter.post('/command', validateToken, async (req, res) => {
910
1245
  * 主流程:校验 session/input -> 定位 SDK 会话 -> 通过 provider adapter 发送用户消息。
911
1246
  * POST /runtime/input
912
1247
  */
913
- terminalRouter.post('/input', validateToken, async (req, res) => {
1248
+ terminalRouter.post("/input", validateToken, async (req, res) => {
914
1249
  const { sessionId, input, mode } = req.body || {};
915
1250
  if (!sessionId || input === undefined || input === null) {
916
- res.status(400).json({ error: 'sessionId and input are required' });
1251
+ res.status(400).json({ error: "sessionId and input are required" });
917
1252
  return;
918
1253
  }
919
1254
  const entry = sdkSessions.get(sessionId);
920
1255
  if (!entry) {
921
- res.status(404).json({ error: 'SDK session not found' });
1256
+ res.status(404).json({ error: "SDK session not found" });
922
1257
  return;
923
1258
  }
924
1259
  try {
925
- const providerId = entry.providerId || 'claude-code';
1260
+ const providerId = entry.providerId || "claude-code";
926
1261
  const adapter = adapterRegistry.get(providerId);
927
- if (mode === 'shortcut') {
1262
+ if (mode === "shortcut") {
928
1263
  if (!adapter.sendShortcutInput) {
929
- res.status(400).json({ error: 'shortcut input is not supported by this SDK provider' });
1264
+ res.status(400).json({
1265
+ error: "shortcut input is not supported by this SDK provider",
1266
+ });
930
1267
  return;
931
1268
  }
932
1269
  await adapter.sendShortcutInput(sessionId, String(input));
933
- res.json({ ok: true, mode: 'shortcut' });
1270
+ res.json({ ok: true, mode: "shortcut" });
934
1271
  return;
935
1272
  }
936
1273
  await adapter.sendMessage(sessionId, await resolveSdkInputMessage(input));
937
- res.json({ ok: true, mode: 'sdk' });
1274
+ res.json({ ok: true, mode: "sdk" });
938
1275
  }
939
1276
  catch (error) {
940
1277
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -947,22 +1284,22 @@ terminalRouter.post('/input', validateToken, async (req, res) => {
947
1284
  * 主流程:按 agentId 定位 SDK 会话 -> 更新 adapter 内存配置 -> 同步持久化会话字段。
948
1285
  * POST /runtime/update-auto-commands
949
1286
  */
950
- terminalRouter.post('/update-auto-commands', validateToken, async (req, res) => {
951
- const { agentId, idleInputAutoCommand = '', nonInputAutoCommand = '' } = req.body || {};
952
- const normalizedAgentId = String(agentId || '').trim();
1287
+ terminalRouter.post("/update-auto-commands", validateToken, async (req, res) => {
1288
+ const { agentId, idleInputAutoCommand = "", nonInputAutoCommand = "", } = req.body || {};
1289
+ const normalizedAgentId = String(agentId || "").trim();
953
1290
  if (!normalizedAgentId) {
954
- res.status(400).json({ error: 'agentId is required' });
1291
+ res.status(400).json({ error: "agentId is required" });
955
1292
  return;
956
1293
  }
957
1294
  const entry = [...sdkSessions.values()].find((session) => session.agentId === normalizedAgentId);
958
1295
  const commands = {
959
- idleInputCommand: String(idleInputAutoCommand || ''),
960
- nonInputCommand: String(nonInputAutoCommand || ''),
1296
+ idleInputCommand: String(idleInputAutoCommand || ""),
1297
+ nonInputCommand: String(nonInputAutoCommand || ""),
961
1298
  };
962
1299
  try {
963
1300
  let sessionUpdated = false;
964
1301
  if (entry) {
965
- const adapter = adapterRegistry.get(entry.providerId || 'claude-code');
1302
+ const adapter = adapterRegistry.get(entry.providerId || "claude-code");
966
1303
  adapter.setIdleCommands?.(entry.sessionId, commands);
967
1304
  sessionUpdated = true;
968
1305
  }
@@ -970,11 +1307,11 @@ terminalRouter.post('/update-auto-commands', validateToken, async (req, res) =>
970
1307
  idleInputAutoCommand: commands.idleInputCommand,
971
1308
  nonInputAutoCommand: commands.nonInputCommand,
972
1309
  });
973
- res.json({ ok: true, mode: 'sdk', sessionUpdated, persistedUpdated });
1310
+ res.json({ ok: true, mode: "sdk", sessionUpdated, persistedUpdated });
974
1311
  }
975
1312
  catch (error) {
976
1313
  const errorMessage = error instanceof Error ? error.message : String(error);
977
- res.status(500).json({ ok: false, mode: 'sdk', error: errorMessage });
1314
+ res.status(500).json({ ok: false, mode: "sdk", error: errorMessage });
978
1315
  }
979
1316
  });
980
1317
  /**
@@ -983,37 +1320,43 @@ terminalRouter.post('/update-auto-commands', validateToken, async (req, res) =>
983
1320
  * SDK 会话没有终端尺寸概念;保留该端点用于前端 xterm fit 回调,返回 no-op。
984
1321
  * POST /runtime/resize
985
1322
  */
986
- terminalRouter.post('/resize', validateToken, (req, res) => {
1323
+ terminalRouter.post("/resize", validateToken, (req, res) => {
987
1324
  const { sessionId, cols, rows } = req.body || {};
988
1325
  if (!sessionId || !cols || !rows) {
989
- res.status(400).json({ error: 'sessionId, cols, rows are required' });
1326
+ res.status(400).json({ error: "sessionId, cols, rows are required" });
990
1327
  return;
991
1328
  }
992
1329
  if (!sdkSessions.has(sessionId)) {
993
- res.status(404).json({ error: 'SDK session not found' });
1330
+ res.status(404).json({ error: "SDK session not found" });
994
1331
  return;
995
1332
  }
996
- res.json({ ok: true, mode: 'sdk', noop: true, cols: Number(cols), rows: Number(rows) });
1333
+ res.json({
1334
+ ok: true,
1335
+ mode: "sdk",
1336
+ noop: true,
1337
+ cols: Number(cols),
1338
+ rows: Number(rows),
1339
+ });
997
1340
  });
998
1341
  /**
999
1342
  * 停止 SDK 会话。
1000
1343
  * POST /runtime/stop
1001
1344
  */
1002
- terminalRouter.post('/stop', validateToken, async (req, res) => {
1345
+ terminalRouter.post("/stop", validateToken, async (req, res) => {
1003
1346
  const { sessionId } = req.body || {};
1004
1347
  if (!sessionId) {
1005
- res.status(400).json({ error: 'sessionId is required' });
1348
+ res.status(400).json({ error: "sessionId is required" });
1006
1349
  return;
1007
1350
  }
1008
1351
  const sdkEntry = sdkSessions.get(sessionId);
1009
1352
  if (!sdkEntry) {
1010
1353
  await removePersistedSession(sessionId);
1011
- res.json({ ok: true, status: 'stopped', mode: 'none' });
1354
+ res.json({ ok: true, status: "stopped", mode: "none" });
1012
1355
  return;
1013
1356
  }
1014
1357
  const agentId = sdkEntry.agentId;
1015
1358
  try {
1016
- const providerId = sdkEntry.providerId || 'claude-code';
1359
+ const providerId = sdkEntry.providerId || "claude-code";
1017
1360
  const adapter = adapterRegistry.get(providerId);
1018
1361
  await adapter.terminateSession(sessionId);
1019
1362
  sdkSessions.delete(sessionId);
@@ -1021,13 +1364,20 @@ terminalRouter.post('/stop', validateToken, async (req, res) => {
1021
1364
  await removePersistedSession(sessionId);
1022
1365
  const processManager = getAgentProcessManager();
1023
1366
  await processManager.removeProcess(agentId);
1024
- await sendStatus(agentId, null, 'stopped');
1025
- res.json({ ok: true, status: 'stopped', mode: 'sdk' });
1367
+ await sendStatus(agentId, null, "stopped", undefined, undefined, {
1368
+ runtimeAccessToken: sdkEntry.runtimeAccessToken,
1369
+ });
1370
+ res.json({ ok: true, status: "stopped", mode: "sdk" });
1026
1371
  }
1027
1372
  catch (error) {
1028
1373
  const errorMessage = error instanceof Error ? error.message : String(error);
1029
1374
  console.error(`[Runtime] Failed to terminate SDK session ${sessionId}:`, errorMessage);
1030
- res.status(500).json({ ok: false, status: 'stop_failed', mode: 'sdk', error: errorMessage });
1375
+ res.status(500).json({
1376
+ ok: false,
1377
+ status: "stop_failed",
1378
+ mode: "sdk",
1379
+ error: errorMessage,
1380
+ });
1031
1381
  }
1032
1382
  });
1033
1383
  /**
@@ -1035,20 +1385,22 @@ terminalRouter.post('/stop', validateToken, async (req, res) => {
1035
1385
  * POST /runtime/stop-all
1036
1386
  * 用于 aws-mcp-server 重启时清理所有 SDK Agent 会话
1037
1387
  */
1038
- terminalRouter.post('/stop-all', validateToken, async (_req, res) => {
1388
+ terminalRouter.post("/stop-all", validateToken, async (_req, res) => {
1039
1389
  const stoppedSdk = [];
1040
1390
  const errors = [];
1041
1391
  // 1. 停止所有 SDK 会话
1042
1392
  for (const [sessionId, entry] of sdkSessions.entries()) {
1043
1393
  try {
1044
1394
  // 根据 providerId 获取对应的 adapter
1045
- const providerId = entry.providerId || 'claude-code';
1395
+ const providerId = entry.providerId || "claude-code";
1046
1396
  const adapter = adapterRegistry.get(providerId);
1047
1397
  await adapter.terminateSession(sessionId);
1048
1398
  sdkSessions.delete(sessionId);
1049
1399
  stopTerminalCommandProcess(sessionId);
1050
1400
  stoppedSdk.push(sessionId);
1051
- await sendStatus(entry.agentId, null, 'stopped');
1401
+ await sendStatus(entry.agentId, null, "stopped", undefined, undefined, {
1402
+ runtimeAccessToken: entry.runtimeAccessToken,
1403
+ });
1052
1404
  }
1053
1405
  catch (error) {
1054
1406
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -1059,7 +1411,7 @@ terminalRouter.post('/stop-all', validateToken, async (_req, res) => {
1059
1411
  // 直接清空整个持久化文件,确保不会有残留
1060
1412
  try {
1061
1413
  await savePersistedSessions([]);
1062
- console.log('[Runtime] 已清空所有持久化会话');
1414
+ console.log("[Runtime] 已清空所有持久化会话");
1063
1415
  }
1064
1416
  catch (error) {
1065
1417
  const errorMessage = error instanceof Error ? error.message : String(error);