aws-runtime-bridge 1.5.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapter/AdapterRegistry.d.ts +1 -1
- package/dist/adapter/AdapterRegistry.d.ts.map +1 -1
- package/dist/adapter/AdapterRegistry.js +0 -2
- package/dist/adapter/ClaudeSdkAdapter.d.ts +4 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +11 -2
- package/dist/adapter/CodexSdkAdapter.js +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +2 -2
- package/dist/adapter/types.d.ts +10 -0
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/index.js +14 -43
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +9 -1
- package/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +21 -1
- package/dist/routes/file-browser.test.js +9 -0
- package/dist/routes/instance.d.ts +10 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +93 -2
- package/dist/routes/instance.test.js +50 -0
- package/dist/routes/pty.d.ts +107 -0
- package/dist/routes/pty.d.ts.map +1 -0
- package/dist/routes/pty.js +551 -0
- package/dist/routes/pty.test.d.ts +2 -0
- package/dist/routes/pty.test.d.ts.map +1 -0
- package/dist/routes/pty.test.js +82 -0
- package/dist/routes/sessions.d.ts +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +32 -213
- package/dist/routes/terminal.d.ts +32 -3
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +411 -243
- package/dist/routes/terminal.test.js +105 -29
- package/dist/services/agent-process-manager.d.ts +2 -2
- package/dist/services/agent-process-manager.d.ts.map +1 -1
- package/dist/services/agent-process-manager.js +3 -3
- package/dist/services/process-detector.d.ts +2 -4
- package/dist/services/process-detector.d.ts.map +1 -1
- package/dist/services/process-detector.js +9 -16
- package/dist/services/process-registry.d.ts +2 -2
- package/dist/services/process-registry.d.ts.map +1 -1
- package/dist/services/process-registry.js +1 -1
- package/dist/services/session-output.d.ts +15 -5
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +33 -3
- package/dist/services/session-output.test.js +43 -29
- package/dist/services/terminal-persistence.d.ts +9 -0
- package/dist/services/terminal-persistence.d.ts.map +1 -1
- package/dist/services/terminal-persistence.js +20 -0
- package/dist/services/tool-installer.d.ts +10 -0
- package/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +193 -28
- package/dist/services/tool-installer.test.js +46 -1
- package/dist/services/workspace-files.d.ts +14 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +52 -0
- package/dist/services/workspace-files.test.js +85 -1
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/routes/terminal.js
CHANGED
|
@@ -1,22 +1,290 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 终端 API 路由
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 提供 SDK Agent 会话的启动、输入、调整大小和停止功能
|
|
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';
|
|
7
12
|
import { Router } from 'express';
|
|
8
|
-
import pty from 'node-pty';
|
|
9
13
|
import { v4 as uuidv4 } from 'uuid';
|
|
10
14
|
import { adapterRegistry, registerSdkProviders, resolveSdkProviderIdByCommand, } from '../adapter/index.js';
|
|
11
15
|
import { validateToken } from '../middleware/auth.js';
|
|
12
16
|
import { getAgentProcessManager } from '../services/agent-process-manager.js';
|
|
13
|
-
import { findClaudeCodeProcesses, terminateProcessTree, waitForProcessExit } from '../services/process-detector.js';
|
|
14
|
-
import { flushSessionOutput, scheduleOutputFlush, sendQuestionRequest, sendStatus, sessions } from '../services/session-output.js';
|
|
15
17
|
import { enqueueMcpLaunchBinding } from '../services/mcp-launch-binding-queue.js';
|
|
16
|
-
import {
|
|
18
|
+
import { findClaudeCodeProcesses } from '../services/process-detector.js';
|
|
19
|
+
import { sendOutput, sendQuestionRequest, sendStatus } from '../services/session-output.js';
|
|
20
|
+
import { removePersistedSession, removePersistedSessionByAgentId, savePersistedSessions, updatePersistedSessionAutoCommands, upsertPersistedSession, } from '../services/terminal-persistence.js';
|
|
17
21
|
export const terminalRouter = Router();
|
|
18
22
|
// 导出 SDK 会话存储,供其他模块使用(如 sessions.ts)
|
|
19
23
|
export const sdkSessions = new Map();
|
|
24
|
+
const TOOL_RESULT_PREVIEW_MAX_LENGTH = 4000;
|
|
25
|
+
const terminalCommandStates = new Map();
|
|
26
|
+
const DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING = 'gb18030';
|
|
27
|
+
const DEFAULT_TERMINAL_OUTPUT_ENCODING = 'utf-8';
|
|
28
|
+
/**
|
|
29
|
+
* 标准化终端输入:将前端回车符还原为 shell 可执行命令文本。
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeTerminalCommandInput(value) {
|
|
32
|
+
return String(value ?? '').replace(/\r/g, '\n').trim();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 解析终端命令输出解码编码。
|
|
36
|
+
* 主流程:优先使用环境变量覆盖 -> Windows 默认 GB18030 -> 其他平台默认 UTF-8。
|
|
37
|
+
*/
|
|
38
|
+
export function resolveTerminalOutputEncoding(platform = process.platform, env = process.env) {
|
|
39
|
+
const configuredEncoding = env.AWS_TERMINAL_OUTPUT_ENCODING?.trim();
|
|
40
|
+
if (configuredEncoding) {
|
|
41
|
+
return configuredEncoding;
|
|
42
|
+
}
|
|
43
|
+
return platform === 'win32' ? DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING : DEFAULT_TERMINAL_OUTPUT_ENCODING;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 创建终端输出流式解码器,避免多字节中文被 chunk 边界截断。
|
|
47
|
+
*/
|
|
48
|
+
export function createTerminalOutputDecoder(encoding = resolveTerminalOutputEncoding()) {
|
|
49
|
+
try {
|
|
50
|
+
return new TextDecoder(encoding, { fatal: false });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return new TextDecoder(DEFAULT_TERMINAL_OUTPUT_ENCODING, { fatal: false });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 解码终端输出 chunk;end=true 时刷新解码器内部剩余字节。
|
|
58
|
+
*/
|
|
59
|
+
export function decodeTerminalOutputChunk(decoder, chunk, end = false) {
|
|
60
|
+
if (end) {
|
|
61
|
+
return decoder.decode();
|
|
62
|
+
}
|
|
63
|
+
if (!chunk || chunk.length === 0) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
return decoder.decode(chunk, { stream: true });
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 解析 cd 命令目标;返回 undefined 表示不是目录切换命令,null 表示仅查看当前目录。
|
|
70
|
+
*/
|
|
71
|
+
export function parseTerminalDirectoryChangeTarget(command) {
|
|
72
|
+
const match = command.trim().match(/^cd(?:\s+(.*))?$/i);
|
|
73
|
+
if (!match) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const rawTarget = match[1]?.trim();
|
|
77
|
+
if (!rawTarget) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const withoutDriveSwitch = rawTarget.replace(/^\/d\s+/i, '').trim();
|
|
81
|
+
if ((withoutDriveSwitch.startsWith('"') && withoutDriveSwitch.endsWith('"'))
|
|
82
|
+
|| (withoutDriveSwitch.startsWith("'") && withoutDriveSwitch.endsWith("'"))) {
|
|
83
|
+
return withoutDriveSwitch.slice(1, -1);
|
|
84
|
+
}
|
|
85
|
+
return withoutDriveSwitch;
|
|
86
|
+
}
|
|
87
|
+
function getTerminalCommandState(entry) {
|
|
88
|
+
const existing = terminalCommandStates.get(entry.sessionId);
|
|
89
|
+
if (existing) {
|
|
90
|
+
return existing;
|
|
91
|
+
}
|
|
92
|
+
const state = {
|
|
93
|
+
currentDirectory: entry.workspacePath,
|
|
94
|
+
runningProcess: null,
|
|
95
|
+
};
|
|
96
|
+
terminalCommandStates.set(entry.sessionId, state);
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
function stopTerminalCommandProcess(sessionId) {
|
|
100
|
+
const state = terminalCommandStates.get(sessionId);
|
|
101
|
+
if (!state?.runningProcess) {
|
|
102
|
+
terminalCommandStates.delete(sessionId);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
state.runningProcess.kill();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// ignore best-effort cleanup failures; session shutdown must continue
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
state.runningProcess = null;
|
|
113
|
+
terminalCommandStates.delete(sessionId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function appendCommandOutput(entry, output) {
|
|
117
|
+
if (!output) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
entry.seq += 1;
|
|
121
|
+
void sendOutput(entry.agentId, output, entry.sessionId, entry.seq);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 生成显示在终端输出流中的工作目录提示符。
|
|
125
|
+
*/
|
|
126
|
+
export function formatTerminalPrompt(currentDirectory) {
|
|
127
|
+
return `\x1b[36m${currentDirectory}>\x1b[0m `;
|
|
128
|
+
}
|
|
129
|
+
function resolveDirectoryTarget(currentDirectory, target) {
|
|
130
|
+
if (target === '~') {
|
|
131
|
+
return os.homedir();
|
|
132
|
+
}
|
|
133
|
+
return path.resolve(currentDirectory, target);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 处理内置目录切换命令,保持终端后续命令的当前目录连续。
|
|
137
|
+
*/
|
|
138
|
+
async function handleDirectoryChangeCommand(entry, state, command) {
|
|
139
|
+
const target = parseTerminalDirectoryChangeTarget(command);
|
|
140
|
+
if (target === undefined) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (target === null) {
|
|
144
|
+
appendCommandOutput(entry, `${state.currentDirectory}\r\n`);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const nextDirectory = resolveDirectoryTarget(state.currentDirectory, target);
|
|
148
|
+
try {
|
|
149
|
+
const stat = await fs.stat(nextDirectory);
|
|
150
|
+
if (!stat.isDirectory()) {
|
|
151
|
+
appendCommandOutput(entry, `cd: not a directory: ${nextDirectory}\r\n`);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
state.currentDirectory = nextDirectory;
|
|
155
|
+
appendCommandOutput(entry, `${state.currentDirectory}\r\n`);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
160
|
+
appendCommandOutput(entry, `cd: ${message}\r\n`);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 在当前 Agent 工作目录对应的终端上下文中执行命令。
|
|
166
|
+
*/
|
|
167
|
+
async function executeTerminalCommand(sessionId, commandValue) {
|
|
168
|
+
const entry = sdkSessions.get(sessionId);
|
|
169
|
+
if (!entry) {
|
|
170
|
+
throw new Error('SDK session not found');
|
|
171
|
+
}
|
|
172
|
+
const command = normalizeTerminalCommandInput(commandValue);
|
|
173
|
+
const state = getTerminalCommandState(entry);
|
|
174
|
+
if (!command) {
|
|
175
|
+
return { started: false, cwd: state.currentDirectory };
|
|
176
|
+
}
|
|
177
|
+
if (state.runningProcess) {
|
|
178
|
+
throw new Error('A terminal command is already running for this session');
|
|
179
|
+
}
|
|
180
|
+
appendCommandOutput(entry, `${formatTerminalPrompt(state.currentDirectory)}${command}\r\n`);
|
|
181
|
+
const handledDirectoryChange = await handleDirectoryChangeCommand(entry, state, command);
|
|
182
|
+
if (handledDirectoryChange) {
|
|
183
|
+
return { started: false, cwd: state.currentDirectory };
|
|
184
|
+
}
|
|
185
|
+
await sendStatus(entry.agentId, sessionId, 'tool_using', {
|
|
186
|
+
actionType: 'bash',
|
|
187
|
+
actionLabel: '终端命令',
|
|
188
|
+
actionDetail: command,
|
|
189
|
+
});
|
|
190
|
+
const child = spawn(command, {
|
|
191
|
+
cwd: state.currentDirectory,
|
|
192
|
+
shell: true,
|
|
193
|
+
env: buildRuntimeEnv(entry.agentId, entry.workspacePath, process.env, entry.config.envOverrides),
|
|
194
|
+
});
|
|
195
|
+
state.runningProcess = child;
|
|
196
|
+
const stdoutDecoder = createTerminalOutputDecoder();
|
|
197
|
+
const stderrDecoder = createTerminalOutputDecoder();
|
|
198
|
+
child.stdout.on('data', (chunk) => {
|
|
199
|
+
appendCommandOutput(entry, decodeTerminalOutputChunk(stdoutDecoder, chunk));
|
|
200
|
+
});
|
|
201
|
+
child.stderr.on('data', (chunk) => {
|
|
202
|
+
appendCommandOutput(entry, decodeTerminalOutputChunk(stderrDecoder, chunk));
|
|
203
|
+
});
|
|
204
|
+
child.on('error', (error) => {
|
|
205
|
+
appendCommandOutput(entry, `\r\n[Terminal Error] ${error.message}\r\n`);
|
|
206
|
+
});
|
|
207
|
+
child.on('close', (code, signal) => {
|
|
208
|
+
state.runningProcess = null;
|
|
209
|
+
appendCommandOutput(entry, decodeTerminalOutputChunk(stdoutDecoder, undefined, true));
|
|
210
|
+
appendCommandOutput(entry, decodeTerminalOutputChunk(stderrDecoder, undefined, true));
|
|
211
|
+
if (code && code !== 0) {
|
|
212
|
+
appendCommandOutput(entry, `\r\n[exit ${code}]\r\n`);
|
|
213
|
+
}
|
|
214
|
+
else if (signal) {
|
|
215
|
+
appendCommandOutput(entry, `\r\n[signal ${signal}]\r\n`);
|
|
216
|
+
}
|
|
217
|
+
appendCommandOutput(entry, formatTerminalPrompt(state.currentDirectory));
|
|
218
|
+
void sendStatus(entry.agentId, sessionId, 'waiting_input');
|
|
219
|
+
});
|
|
220
|
+
return { started: true, cwd: state.currentDirectory };
|
|
221
|
+
}
|
|
222
|
+
export function buildRuntimeEnv(agentId, workspacePath, baseEnv, envOverrides) {
|
|
223
|
+
const env = {};
|
|
224
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
225
|
+
if (value !== undefined) {
|
|
226
|
+
env[key] = value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (workspacePath) {
|
|
230
|
+
env.AWS_WORKSPACE_PATH = String(workspacePath);
|
|
231
|
+
}
|
|
232
|
+
if (envOverrides) {
|
|
233
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
234
|
+
const normalizedKey = key.trim();
|
|
235
|
+
if (!normalizedKey || value === undefined || value === null) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const normalizedValue = String(value).trim();
|
|
239
|
+
if (!normalizedValue) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
env[normalizedKey] = normalizedValue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
env.AWS_AGENT_ID = String(agentId);
|
|
246
|
+
env.AWS_MCP_CLAIM_LAUNCH_BINDING = 'true';
|
|
247
|
+
return env;
|
|
248
|
+
}
|
|
249
|
+
function formatToolResultForTimeline(result) {
|
|
250
|
+
if (result === undefined || result === null) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
254
|
+
const normalized = text.trim();
|
|
255
|
+
if (!normalized) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
if (normalized.length <= TOOL_RESULT_PREVIEW_MAX_LENGTH) {
|
|
259
|
+
return normalized;
|
|
260
|
+
}
|
|
261
|
+
return `${normalized.slice(0, TOOL_RESULT_PREVIEW_MAX_LENGTH)}\n...(结果过长,已截断)`;
|
|
262
|
+
}
|
|
263
|
+
function isMcpProviderEvent(event) {
|
|
264
|
+
const actionType = event.data.actionType;
|
|
265
|
+
if (actionType === 'mcp') {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
const toolName = String(event.data.toolName || '').toLowerCase();
|
|
269
|
+
return toolName.startsWith('mcp__') || toolName.includes('-mcp__') || toolName.includes('mcp_');
|
|
270
|
+
}
|
|
271
|
+
export function formatSdkOutputEvent(event) {
|
|
272
|
+
if (event.type === 'text_delta' || event.type === 'thinking') {
|
|
273
|
+
return event.data.text || undefined;
|
|
274
|
+
}
|
|
275
|
+
if (event.type === 'error') {
|
|
276
|
+
return event.data.text ? `\r\n[SDK Error] ${event.data.text}\r\n` : undefined;
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
function forwardSdkOutputEvent(event, entry) {
|
|
281
|
+
const output = formatSdkOutputEvent(event);
|
|
282
|
+
if (!output) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
entry.seq += 1;
|
|
286
|
+
void sendOutput(entry.agentId, output, event.sessionId, entry.seq);
|
|
287
|
+
}
|
|
20
288
|
export function resolveSdkProviderId(command) {
|
|
21
289
|
return resolveSdkProviderIdByCommand(command);
|
|
22
290
|
}
|
|
@@ -33,6 +301,19 @@ function wireSdkAdapterEvents(adapter, definition) {
|
|
|
33
301
|
const entry = sdkSessions.get(event.sessionId);
|
|
34
302
|
if (entry) {
|
|
35
303
|
console.log(`[${definition.displayName} Adapter] Event: ${event.type} for session ${event.sessionId}`);
|
|
304
|
+
forwardSdkOutputEvent(event, entry);
|
|
305
|
+
if (event.type === 'tool_use_end' && isMcpProviderEvent(event)) {
|
|
306
|
+
const actionResult = formatToolResultForTimeline(event.data.toolResult);
|
|
307
|
+
if (actionResult) {
|
|
308
|
+
void sendStatus(entry.agentId, event.sessionId, 'tool_result', {
|
|
309
|
+
actionType: 'mcp',
|
|
310
|
+
actionLabel: 'MCP返回',
|
|
311
|
+
actionDetail: event.data.toolName,
|
|
312
|
+
actionId: event.data.toolUseId,
|
|
313
|
+
actionResult,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
36
317
|
if (event.type === 'turn_complete' && event.data.usage) {
|
|
37
318
|
sendStatus(entry.agentId, event.sessionId, 'waiting_input', undefined, event.data.usage);
|
|
38
319
|
}
|
|
@@ -42,7 +323,7 @@ function wireSdkAdapterEvents(adapter, definition) {
|
|
|
42
323
|
const entry = sdkSessions.get(sessionId);
|
|
43
324
|
if (entry) {
|
|
44
325
|
console.log(`[${definition.displayName} Adapter] Status changed: ${status} for agent ${entry.agentId}`, actionInfo || '');
|
|
45
|
-
sendStatus(entry.agentId, sessionId, status, actionInfo);
|
|
326
|
+
void sendStatus(entry.agentId, sessionId, status, actionInfo);
|
|
46
327
|
}
|
|
47
328
|
});
|
|
48
329
|
adapter.on('ask-user-question', (data) => {
|
|
@@ -60,17 +341,16 @@ function wireSdkAdapterEvents(adapter, definition) {
|
|
|
60
341
|
});
|
|
61
342
|
}
|
|
62
343
|
/**
|
|
63
|
-
*
|
|
344
|
+
* 启动 SDK Agent 会话
|
|
64
345
|
* POST /runtime/start
|
|
65
|
-
* 支持 mode: 'pty' | 'sdk'
|
|
66
346
|
*/
|
|
67
347
|
terminalRouter.post('/start', validateToken, async (req, res) => {
|
|
68
|
-
const { agentId, workspacePath,
|
|
348
|
+
const { agentId, workspacePath, mode = 'sdk', } = req.body || {};
|
|
69
349
|
if (!agentId || !workspacePath) {
|
|
70
350
|
res.status(400).json({ error: 'agentId and workspacePath are required' });
|
|
71
351
|
return;
|
|
72
352
|
}
|
|
73
|
-
|
|
353
|
+
enqueueMcpLaunchBinding({
|
|
74
354
|
agentId: String(agentId),
|
|
75
355
|
workspacePath: String(workspacePath),
|
|
76
356
|
serverUrl: req.body?.serverUrl || req.body?.schedulerBaseUrl,
|
|
@@ -78,104 +358,10 @@ terminalRouter.post('/start', validateToken, async (req, res) => {
|
|
|
78
358
|
userId: req.body?.userId,
|
|
79
359
|
schedulerBaseUrl: req.body?.schedulerBaseUrl,
|
|
80
360
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await startSdkSession(req, res);
|
|
84
|
-
return;
|
|
361
|
+
if (mode && mode !== 'sdk') {
|
|
362
|
+
console.warn(`[Runtime] Unsupported runtime mode "${mode}" requested for agent ${agentId}; starting SDK session instead.`);
|
|
85
363
|
}
|
|
86
|
-
|
|
87
|
-
const oldSession = Array.from(sessions.entries()).find(([, value]) => value.agentId === agentId);
|
|
88
|
-
if (oldSession) {
|
|
89
|
-
try {
|
|
90
|
-
oldSession[1].ptyProcess.kill();
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// ignore stale session cleanup errors
|
|
94
|
-
}
|
|
95
|
-
if (oldSession[1].flushTimer) {
|
|
96
|
-
clearTimeout(oldSession[1].flushTimer);
|
|
97
|
-
}
|
|
98
|
-
sessions.delete(oldSession[0]);
|
|
99
|
-
await removePersistedSession(oldSession[0]);
|
|
100
|
-
}
|
|
101
|
-
const sessionId = uuidv4();
|
|
102
|
-
const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
|
|
103
|
-
const shellArgs = process.platform === 'win32' ? ['-NoLogo'] : [];
|
|
104
|
-
const actualCommand = command || 'claude';
|
|
105
|
-
const terminal = pty.spawn(shell, shellArgs, {
|
|
106
|
-
name: 'xterm-color',
|
|
107
|
-
cols: 120,
|
|
108
|
-
rows: 30,
|
|
109
|
-
cwd: workspacePath,
|
|
110
|
-
env: {
|
|
111
|
-
...process.env,
|
|
112
|
-
AWS_AGENT_ID: String(agentId),
|
|
113
|
-
AWS_MCP_CLAIM_LAUNCH_BINDING: 'true',
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
sessions.set(sessionId, {
|
|
117
|
-
agentId,
|
|
118
|
-
workspacePath,
|
|
119
|
-
ptyProcess: terminal,
|
|
120
|
-
outputBuffer: '',
|
|
121
|
-
flushTimer: null,
|
|
122
|
-
flushInFlight: null,
|
|
123
|
-
seq: 0,
|
|
124
|
-
});
|
|
125
|
-
await upsertPersistedSession({
|
|
126
|
-
sessionId,
|
|
127
|
-
agentId,
|
|
128
|
-
workspacePath,
|
|
129
|
-
command: actualCommand,
|
|
130
|
-
startedAt: new Date().toISOString(),
|
|
131
|
-
status: 'running',
|
|
132
|
-
mode: 'pty',
|
|
133
|
-
// ★ PTY 模式:记录 pty 进程的 PID
|
|
134
|
-
pid: terminal.pid,
|
|
135
|
-
});
|
|
136
|
-
// ★ 注册到进程管理器
|
|
137
|
-
const processManager = getAgentProcessManager();
|
|
138
|
-
await processManager.startProcess({
|
|
139
|
-
agentId,
|
|
140
|
-
sessionId,
|
|
141
|
-
pid: terminal.pid,
|
|
142
|
-
mode: 'pty',
|
|
143
|
-
workspacePath,
|
|
144
|
-
command: actualCommand,
|
|
145
|
-
});
|
|
146
|
-
await sendStatus(agentId, sessionId, 'running');
|
|
147
|
-
terminal.onData((data) => {
|
|
148
|
-
const session = sessions.get(sessionId);
|
|
149
|
-
if (!session) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
session.outputBuffer += data;
|
|
153
|
-
scheduleOutputFlush(sessionId);
|
|
154
|
-
});
|
|
155
|
-
terminal.onExit(async () => {
|
|
156
|
-
const session = sessions.get(sessionId);
|
|
157
|
-
if (session?.flushTimer) {
|
|
158
|
-
clearTimeout(session.flushTimer);
|
|
159
|
-
session.flushTimer = null;
|
|
160
|
-
}
|
|
161
|
-
await flushSessionOutput(sessionId);
|
|
162
|
-
sessions.delete(sessionId);
|
|
163
|
-
// 直接删除持久化会话,而不是改成 stopped,避免后续被恢复
|
|
164
|
-
await removePersistedSession(sessionId);
|
|
165
|
-
// ★ 从进程管理器中移除
|
|
166
|
-
const processManager = getAgentProcessManager();
|
|
167
|
-
await processManager.removeProcess(agentId);
|
|
168
|
-
await sendStatus(agentId, null, 'stopped');
|
|
169
|
-
});
|
|
170
|
-
terminal.write(`${actualCommand}\r`);
|
|
171
|
-
res.json({
|
|
172
|
-
sessionId,
|
|
173
|
-
status: 'running',
|
|
174
|
-
agentId,
|
|
175
|
-
workspacePath,
|
|
176
|
-
command: actualCommand,
|
|
177
|
-
mode: 'pty',
|
|
178
|
-
});
|
|
364
|
+
await startSdkSession(req, res);
|
|
179
365
|
});
|
|
180
366
|
/**
|
|
181
367
|
* 启动 SDK 会话
|
|
@@ -184,7 +370,7 @@ terminalRouter.post('/start', validateToken, async (req, res) => {
|
|
|
184
370
|
async function startSdkSession(req, res) {
|
|
185
371
|
const { agentId, workspacePath, command, autoAccept = true, initialPrompt,
|
|
186
372
|
// MCP 配置 - 加载 aws-client-agent-mcp
|
|
187
|
-
mcpConfigPath, extraMcpServers,
|
|
373
|
+
mcpConfigPath, extraMcpServers, envOverrides,
|
|
188
374
|
// 空闲命令配置
|
|
189
375
|
idleInputAutoCommand, nonInputAutoCommand, } = req.body || {};
|
|
190
376
|
let sessionId = null;
|
|
@@ -208,11 +394,7 @@ async function startSdkSession(req, res) {
|
|
|
208
394
|
// MCP 配置:不再默认注入 aws-mcp;由用户在面板中安装/配置
|
|
209
395
|
mcpConfigPath,
|
|
210
396
|
extraMcpServers: extraMcpServers || {},
|
|
211
|
-
envOverrides:
|
|
212
|
-
AWS_AGENT_ID: String(agentId),
|
|
213
|
-
AWS_WORKSPACE_PATH: String(workspacePath),
|
|
214
|
-
AWS_MCP_CLAIM_LAUNCH_BINDING: 'true',
|
|
215
|
-
},
|
|
397
|
+
envOverrides: buildRuntimeEnv(String(agentId), String(workspacePath), process.env, envOverrides),
|
|
216
398
|
};
|
|
217
399
|
// 存储会话信息
|
|
218
400
|
sdkSessions.set(sessionId, {
|
|
@@ -221,6 +403,7 @@ async function startSdkSession(req, res) {
|
|
|
221
403
|
sessionId,
|
|
222
404
|
config,
|
|
223
405
|
providerId, // 记录使用的 provider
|
|
406
|
+
seq: 0,
|
|
224
407
|
});
|
|
225
408
|
// 启动 SDK 会话
|
|
226
409
|
await adapter.startSession(sessionId, config);
|
|
@@ -433,6 +616,7 @@ terminalRouter.post('/sdk/stop', validateToken, async (req, res) => {
|
|
|
433
616
|
const adapter = adapterRegistry.get(providerId);
|
|
434
617
|
await adapter.terminateSession(sessionId);
|
|
435
618
|
sdkSessions.delete(sessionId);
|
|
619
|
+
stopTerminalCommandProcess(sessionId);
|
|
436
620
|
await removePersistedSession(sessionId);
|
|
437
621
|
// ★ 从进程管理器中移除
|
|
438
622
|
const processManager = getAgentProcessManager();
|
|
@@ -479,27 +663,105 @@ terminalRouter.get('/sdk/status/:sessionId', validateToken, (req, res) => {
|
|
|
479
663
|
res.status(500).json({ error: errorMessage });
|
|
480
664
|
}
|
|
481
665
|
});
|
|
482
|
-
// ============ PTY 模式原有路由 ============
|
|
483
666
|
/**
|
|
484
|
-
*
|
|
667
|
+
* 执行当前 Agent 工作目录下的终端命令。
|
|
668
|
+
*
|
|
669
|
+
* 主流程:校验 session/command -> 读取会话 workspacePath -> 在该 cwd 下启动 shell 命令 -> 通过终端输出回调流式返回。
|
|
670
|
+
* POST /runtime/command
|
|
671
|
+
*/
|
|
672
|
+
terminalRouter.post('/command', validateToken, async (req, res) => {
|
|
673
|
+
const { sessionId, command } = req.body || {};
|
|
674
|
+
if (!sessionId || command === undefined || command === null) {
|
|
675
|
+
res.status(400).json({ error: 'sessionId and command are required' });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const result = await executeTerminalCommand(String(sessionId), command);
|
|
680
|
+
res.json({ ok: true, mode: 'terminal', ...result });
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
684
|
+
res.status(500).json({ error: errorMessage });
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
// ============ 兼容终端 UI 的 SDK 路由 ============
|
|
688
|
+
/**
|
|
689
|
+
* 发送消息到 SDK 会话。
|
|
690
|
+
*
|
|
691
|
+
* 主流程:校验 session/input -> 定位 SDK 会话 -> 通过 provider adapter 发送用户消息。
|
|
485
692
|
* POST /runtime/input
|
|
486
693
|
*/
|
|
487
|
-
terminalRouter.post('/input', validateToken, (req, res) => {
|
|
488
|
-
const { sessionId, input } = req.body || {};
|
|
694
|
+
terminalRouter.post('/input', validateToken, async (req, res) => {
|
|
695
|
+
const { sessionId, input, mode } = req.body || {};
|
|
489
696
|
if (!sessionId || input === undefined || input === null) {
|
|
490
697
|
res.status(400).json({ error: 'sessionId and input are required' });
|
|
491
698
|
return;
|
|
492
699
|
}
|
|
493
|
-
const
|
|
494
|
-
if (!
|
|
495
|
-
res.status(404).json({ error: 'session not found' });
|
|
700
|
+
const entry = sdkSessions.get(sessionId);
|
|
701
|
+
if (!entry) {
|
|
702
|
+
res.status(404).json({ error: 'SDK session not found' });
|
|
496
703
|
return;
|
|
497
704
|
}
|
|
498
|
-
|
|
499
|
-
|
|
705
|
+
try {
|
|
706
|
+
const providerId = entry.providerId || 'claude-code';
|
|
707
|
+
const adapter = adapterRegistry.get(providerId);
|
|
708
|
+
if (mode === 'shortcut') {
|
|
709
|
+
if (!adapter.sendShortcutInput) {
|
|
710
|
+
res.status(400).json({ error: 'shortcut input is not supported by this SDK provider' });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
await adapter.sendShortcutInput(sessionId, String(input));
|
|
714
|
+
res.json({ ok: true, mode: 'shortcut' });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
await adapter.sendMessage(sessionId, String(input));
|
|
718
|
+
res.json({ ok: true, mode: 'sdk' });
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
722
|
+
res.status(500).json({ error: errorMessage });
|
|
723
|
+
}
|
|
500
724
|
});
|
|
501
725
|
/**
|
|
502
|
-
*
|
|
726
|
+
* 热更新运行中 SDK 会话的自动命令配置。
|
|
727
|
+
*
|
|
728
|
+
* 主流程:按 agentId 定位 SDK 会话 -> 更新 adapter 内存配置 -> 同步持久化会话字段。
|
|
729
|
+
* POST /runtime/update-auto-commands
|
|
730
|
+
*/
|
|
731
|
+
terminalRouter.post('/update-auto-commands', validateToken, async (req, res) => {
|
|
732
|
+
const { agentId, idleInputAutoCommand = '', nonInputAutoCommand = '' } = req.body || {};
|
|
733
|
+
const normalizedAgentId = String(agentId || '').trim();
|
|
734
|
+
if (!normalizedAgentId) {
|
|
735
|
+
res.status(400).json({ error: 'agentId is required' });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const entry = [...sdkSessions.values()].find((session) => session.agentId === normalizedAgentId);
|
|
739
|
+
const commands = {
|
|
740
|
+
idleInputCommand: String(idleInputAutoCommand || ''),
|
|
741
|
+
nonInputCommand: String(nonInputAutoCommand || ''),
|
|
742
|
+
};
|
|
743
|
+
try {
|
|
744
|
+
let sessionUpdated = false;
|
|
745
|
+
if (entry) {
|
|
746
|
+
const adapter = adapterRegistry.get(entry.providerId || 'claude-code');
|
|
747
|
+
adapter.setIdleCommands?.(entry.sessionId, commands);
|
|
748
|
+
sessionUpdated = true;
|
|
749
|
+
}
|
|
750
|
+
const persistedUpdated = await updatePersistedSessionAutoCommands(normalizedAgentId, {
|
|
751
|
+
idleInputAutoCommand: commands.idleInputCommand,
|
|
752
|
+
nonInputAutoCommand: commands.nonInputCommand,
|
|
753
|
+
});
|
|
754
|
+
res.json({ ok: true, mode: 'sdk', sessionUpdated, persistedUpdated });
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
758
|
+
res.status(500).json({ ok: false, mode: 'sdk', error: errorMessage });
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
/**
|
|
762
|
+
* 兼容终端尺寸调整 API。
|
|
763
|
+
*
|
|
764
|
+
* SDK 会话没有终端尺寸概念;保留该端点用于前端 xterm fit 回调,返回 no-op。
|
|
503
765
|
* POST /runtime/resize
|
|
504
766
|
*/
|
|
505
767
|
terminalRouter.post('/resize', validateToken, (req, res) => {
|
|
@@ -508,16 +770,14 @@ terminalRouter.post('/resize', validateToken, (req, res) => {
|
|
|
508
770
|
res.status(400).json({ error: 'sessionId, cols, rows are required' });
|
|
509
771
|
return;
|
|
510
772
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
res.status(404).json({ error: 'session not found' });
|
|
773
|
+
if (!sdkSessions.has(sessionId)) {
|
|
774
|
+
res.status(404).json({ error: 'SDK session not found' });
|
|
514
775
|
return;
|
|
515
776
|
}
|
|
516
|
-
|
|
517
|
-
res.json({ ok: true, cols: Number(cols), rows: Number(rows) });
|
|
777
|
+
res.json({ ok: true, mode: 'sdk', noop: true, cols: Number(cols), rows: Number(rows) });
|
|
518
778
|
});
|
|
519
779
|
/**
|
|
520
|
-
*
|
|
780
|
+
* 停止 SDK 会话。
|
|
521
781
|
* POST /runtime/stop
|
|
522
782
|
*/
|
|
523
783
|
terminalRouter.post('/stop', validateToken, async (req, res) => {
|
|
@@ -526,133 +786,41 @@ terminalRouter.post('/stop', validateToken, async (req, res) => {
|
|
|
526
786
|
res.status(400).json({ error: 'sessionId is required' });
|
|
527
787
|
return;
|
|
528
788
|
}
|
|
529
|
-
// 1. 检查是否是 SDK 会话
|
|
530
789
|
const sdkEntry = sdkSessions.get(sessionId);
|
|
531
|
-
if (sdkEntry) {
|
|
532
|
-
const agentId = sdkEntry.agentId;
|
|
533
|
-
try {
|
|
534
|
-
// 根据 providerId 获取对应的 adapter
|
|
535
|
-
const providerId = sdkEntry.providerId || 'claude-code';
|
|
536
|
-
const adapter = adapterRegistry.get(providerId);
|
|
537
|
-
await adapter.terminateSession(sessionId);
|
|
538
|
-
sdkSessions.delete(sessionId);
|
|
539
|
-
// 同时按 sessionId 和 agentId 删除,确保持久化会话被清除
|
|
540
|
-
await removePersistedSession(sessionId);
|
|
541
|
-
await removePersistedSessionByAgentId(agentId);
|
|
542
|
-
// ★ 从进程管理器中移除
|
|
543
|
-
const processManager = getAgentProcessManager();
|
|
544
|
-
await processManager.removeProcess(agentId);
|
|
545
|
-
await sendStatus(agentId, null, 'stopped');
|
|
546
|
-
res.json({ ok: true, status: 'stopped', mode: 'sdk' });
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
catch (error) {
|
|
550
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
551
|
-
console.error(`[Runtime] Failed to terminate SDK session ${sessionId}:`, errorMessage);
|
|
552
|
-
res.status(500).json({ ok: false, status: 'stop_failed', mode: 'sdk', error: errorMessage });
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
// 2. 检查是否是 PTY 会话
|
|
557
|
-
const session = sessions.get(sessionId);
|
|
558
|
-
if (!session) {
|
|
559
|
-
// 3. 会话不存在于内存,直接清理持久化记录,避免后续恢复
|
|
790
|
+
if (!sdkEntry) {
|
|
560
791
|
await removePersistedSession(sessionId);
|
|
561
|
-
// 也从进程管理器清理(通过持久化会话查找 agentId)
|
|
562
|
-
try {
|
|
563
|
-
const persistedSessions = await loadPersistedSessions();
|
|
564
|
-
const persistedSession = persistedSessions.find(p => p.sessionId === sessionId);
|
|
565
|
-
if (persistedSession) {
|
|
566
|
-
const processManager = getAgentProcessManager();
|
|
567
|
-
await processManager.removeProcess(persistedSession.agentId);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
// ignore cleanup errors
|
|
572
|
-
}
|
|
573
792
|
res.json({ ok: true, status: 'stopped', mode: 'none' });
|
|
574
793
|
return;
|
|
575
794
|
}
|
|
576
|
-
|
|
577
|
-
const agentId = session.agentId;
|
|
578
|
-
const pid = session.ptyProcess.pid;
|
|
579
|
-
let exited = false;
|
|
580
|
-
let terminated = 0;
|
|
581
|
-
let failed = 0;
|
|
795
|
+
const agentId = sdkEntry.agentId;
|
|
582
796
|
try {
|
|
583
|
-
|
|
584
|
-
|
|
797
|
+
const providerId = sdkEntry.providerId || 'claude-code';
|
|
798
|
+
const adapter = adapterRegistry.get(providerId);
|
|
799
|
+
await adapter.terminateSession(sessionId);
|
|
800
|
+
sdkSessions.delete(sessionId);
|
|
801
|
+
stopTerminalCommandProcess(sessionId);
|
|
802
|
+
await removePersistedSession(sessionId);
|
|
803
|
+
await removePersistedSessionByAgentId(agentId);
|
|
804
|
+
const processManager = getAgentProcessManager();
|
|
805
|
+
await processManager.removeProcess(agentId);
|
|
806
|
+
await sendStatus(agentId, null, 'stopped');
|
|
807
|
+
res.json({ ok: true, status: 'stopped', mode: 'sdk' });
|
|
585
808
|
}
|
|
586
809
|
catch (error) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const result = terminateProcessTree(pid);
|
|
591
|
-
terminated = result.terminated;
|
|
592
|
-
failed = result.failed;
|
|
593
|
-
exited = await waitForProcessExit(pid, 5000);
|
|
594
|
-
}
|
|
595
|
-
if (!exited) {
|
|
596
|
-
await sendStatus(agentId, sessionId, 'stop_failed');
|
|
597
|
-
res.status(500).json({
|
|
598
|
-
ok: false,
|
|
599
|
-
status: 'stop_failed',
|
|
600
|
-
mode: 'pty',
|
|
601
|
-
pid,
|
|
602
|
-
terminated,
|
|
603
|
-
failed,
|
|
604
|
-
error: `process ${pid} is still running after termination attempts`,
|
|
605
|
-
});
|
|
606
|
-
return;
|
|
810
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
811
|
+
console.error(`[Runtime] Failed to terminate SDK session ${sessionId}:`, errorMessage);
|
|
812
|
+
res.status(500).json({ ok: false, status: 'stop_failed', mode: 'sdk', error: errorMessage });
|
|
607
813
|
}
|
|
608
|
-
if (session.flushTimer) {
|
|
609
|
-
clearTimeout(session.flushTimer);
|
|
610
|
-
session.flushTimer = null;
|
|
611
|
-
}
|
|
612
|
-
await flushSessionOutput(sessionId);
|
|
613
|
-
sessions.delete(sessionId);
|
|
614
|
-
// 同时按 sessionId 和 agentId 删除,确保持久化会话被清除
|
|
615
|
-
await removePersistedSession(sessionId);
|
|
616
|
-
await removePersistedSessionByAgentId(agentId);
|
|
617
|
-
// ★ 从进程管理器中移除
|
|
618
|
-
const processManager = getAgentProcessManager();
|
|
619
|
-
await processManager.removeProcess(agentId);
|
|
620
|
-
await sendStatus(agentId, null, 'stopped');
|
|
621
|
-
res.json({ ok: true, status: 'stopped', mode: 'pty' });
|
|
622
814
|
});
|
|
623
815
|
/**
|
|
624
|
-
*
|
|
816
|
+
* 停止所有 SDK 会话
|
|
625
817
|
* POST /runtime/stop-all
|
|
626
|
-
* 用于 aws-mcp-server
|
|
818
|
+
* 用于 aws-mcp-server 重启时清理所有 SDK Agent 会话
|
|
627
819
|
*/
|
|
628
820
|
terminalRouter.post('/stop-all', validateToken, async (_req, res) => {
|
|
629
|
-
const stoppedPty = [];
|
|
630
821
|
const stoppedSdk = [];
|
|
631
822
|
const errors = [];
|
|
632
|
-
// 1. 停止所有
|
|
633
|
-
for (const [sessionId, session] of sessions.entries()) {
|
|
634
|
-
try {
|
|
635
|
-
if (session.flushTimer) {
|
|
636
|
-
clearTimeout(session.flushTimer);
|
|
637
|
-
session.flushTimer = null;
|
|
638
|
-
}
|
|
639
|
-
await flushSessionOutput(sessionId);
|
|
640
|
-
try {
|
|
641
|
-
session.ptyProcess.kill();
|
|
642
|
-
}
|
|
643
|
-
catch {
|
|
644
|
-
// ignore kill errors
|
|
645
|
-
}
|
|
646
|
-
sessions.delete(sessionId);
|
|
647
|
-
stoppedPty.push(sessionId);
|
|
648
|
-
await sendStatus(session.agentId, null, 'stopped');
|
|
649
|
-
}
|
|
650
|
-
catch (error) {
|
|
651
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
652
|
-
errors.push(`PTY ${sessionId}: ${errorMessage}`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
// 2. 停止所有 SDK 会话
|
|
823
|
+
// 1. 停止所有 SDK 会话
|
|
656
824
|
for (const [sessionId, entry] of sdkSessions.entries()) {
|
|
657
825
|
try {
|
|
658
826
|
// 根据 providerId 获取对应的 adapter
|
|
@@ -660,6 +828,7 @@ terminalRouter.post('/stop-all', validateToken, async (_req, res) => {
|
|
|
660
828
|
const adapter = adapterRegistry.get(providerId);
|
|
661
829
|
await adapter.terminateSession(sessionId);
|
|
662
830
|
sdkSessions.delete(sessionId);
|
|
831
|
+
stopTerminalCommandProcess(sessionId);
|
|
663
832
|
stoppedSdk.push(sessionId);
|
|
664
833
|
await sendStatus(entry.agentId, null, 'stopped');
|
|
665
834
|
}
|
|
@@ -668,7 +837,7 @@ terminalRouter.post('/stop-all', validateToken, async (_req, res) => {
|
|
|
668
837
|
errors.push(`SDK ${sessionId}: ${errorMessage}`);
|
|
669
838
|
}
|
|
670
839
|
}
|
|
671
|
-
//
|
|
840
|
+
// 2. 清理持久化状态,避免 stop-all 后还能被恢复
|
|
672
841
|
// 直接清空整个持久化文件,确保不会有残留
|
|
673
842
|
try {
|
|
674
843
|
await savePersistedSessions([]);
|
|
@@ -681,7 +850,6 @@ terminalRouter.post('/stop-all', validateToken, async (_req, res) => {
|
|
|
681
850
|
res.json({
|
|
682
851
|
ok: true,
|
|
683
852
|
stopped: {
|
|
684
|
-
pty: stoppedPty.length,
|
|
685
853
|
sdk: stoppedSdk.length,
|
|
686
854
|
},
|
|
687
855
|
errors: errors.length > 0 ? errors : undefined,
|