aws-runtime-bridge 1.9.9 → 1.9.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/AcodeSdkAdapter.d.ts +40 -0
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +353 -7
- package/dist/adapter/AcodeSdkAdapter.test.js +155 -8
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +36 -3
- package/dist/adapter/instance-ai-config-resolver.d.ts +49 -0
- package/dist/adapter/instance-ai-config-resolver.d.ts.map +1 -0
- package/dist/adapter/instance-ai-config-resolver.js +46 -0
- package/dist/adapter/types.d.ts +27 -2
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/routes/dashboard.d.ts.map +1 -1
- package/dist/routes/dashboard.js +1092 -8
- package/dist/routes/events.d.ts +7 -0
- package/dist/routes/events.d.ts.map +1 -0
- package/dist/routes/events.js +22 -0
- package/dist/routes/file-browser.d.ts +1 -1
- package/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +25 -9
- package/dist/routes/file-snapshot.d.ts +10 -0
- package/dist/routes/file-snapshot.d.ts.map +1 -0
- package/dist/routes/file-snapshot.js +156 -0
- package/dist/routes/git.d.ts +10 -0
- package/dist/routes/git.d.ts.map +1 -1
- package/dist/routes/git.js +305 -2
- package/dist/routes/git.test.js +39 -2
- package/dist/routes/instance.js +3 -1
- package/dist/routes/memory.d.ts +3 -0
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +3 -0
- package/dist/routes/terminal.d.ts +2 -1
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +378 -41
- package/dist/routes/terminal.test.js +33 -7
- package/dist/services/agent-event-store.d.ts +59 -0
- package/dist/services/agent-event-store.d.ts.map +1 -0
- package/dist/services/agent-event-store.js +68 -0
- package/dist/services/event-delta-pusher.d.ts +40 -0
- package/dist/services/event-delta-pusher.d.ts.map +1 -0
- package/dist/services/event-delta-pusher.js +95 -0
- package/dist/services/fileSnapshotStore.d.ts +96 -0
- package/dist/services/fileSnapshotStore.d.ts.map +1 -0
- package/dist/services/fileSnapshotStore.js +500 -0
- package/dist/services/instance-ai-config-service.d.ts +49 -0
- package/dist/services/instance-ai-config-service.d.ts.map +1 -0
- package/dist/services/instance-ai-config-service.js +174 -0
- package/dist/services/panel-auth.d.ts +23 -0
- package/dist/services/panel-auth.d.ts.map +1 -1
- package/dist/services/panel-auth.js +64 -1
- package/dist/services/ring-buffer.d.ts +67 -0
- package/dist/services/ring-buffer.d.ts.map +1 -0
- package/dist/services/ring-buffer.js +103 -0
- package/dist/services/session-output.d.ts +47 -3
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +112 -2
- package/dist/utils/instance-ai-config-debug-logger.d.ts +37 -0
- package/dist/utils/instance-ai-config-debug-logger.d.ts.map +1 -0
- package/dist/utils/instance-ai-config-debug-logger.js +63 -0
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +48 -4
- package/package/acode/dist/built-in-file-tools.d.ts +1 -1
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
- package/package/acode/dist/built-in-file-tools.js +5 -3
- package/package/acode/dist/builtins/commandArtifacts.d.ts +8 -1
- package/package/acode/dist/builtins/commandArtifacts.d.ts.map +1 -1
- package/package/acode/dist/builtins/commandArtifacts.js +24 -4
- package/package/acode/dist/builtins/shellCommandResult.d.ts +1 -0
- package/package/acode/dist/builtins/shellCommandResult.d.ts.map +1 -1
- package/package/acode/dist/builtins/shellCommandResult.js +3 -3
- package/package/acode/dist/builtins/shellCommandRunner.d.ts +1 -0
- package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +1 -1
- package/package/acode/dist/builtins/shellCommandRunner.js +1 -0
- package/package/acode/dist/cli.js +2 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +156 -5
- package/package/acode/dist/types.d.ts +1 -0
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +5 -0
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +16 -0
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts +136 -0
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -0
- package/package/aws-client-agent-mcp/dist/context-manager.js +473 -0
- package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -0
- package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +6 -0
- package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +1 -0
- package/package/aws-client-agent-mcp/dist/context-manager.test.js +307 -0
- package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -0
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +170 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +947 -9
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +103 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +15 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +62 -2
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +46 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js +164 -12
- package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +8 -0
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js +88 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +7 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
- package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +103 -2
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +27 -2
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +122 -5
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package.json +1 -1
package/dist/routes/terminal.js
CHANGED
|
@@ -16,16 +16,44 @@ import { adapterRegistry, getToolActionInfo, isTransientClipboardPasteFilePath,
|
|
|
16
16
|
import { validateToken } from "../middleware/auth.js";
|
|
17
17
|
import { getAgentProcessManager } from "../services/agent-process-manager.js";
|
|
18
18
|
import { getAiConfigService } from "../services/cc-switch-sdk.js";
|
|
19
|
+
import { prepareAcodeInstanceConfig, redactForLog } from "../services/instance-ai-config-service.js";
|
|
19
20
|
import { enqueueMcpLaunchBinding } from "../services/mcp-launch-binding-queue.js";
|
|
20
21
|
import { findClaudeCodeProcesses, isProcessRunning, } from "../services/process-detector.js";
|
|
21
|
-
import { sendOutput, sendQuestionRequest, sendStatus, } from "../services/session-output.js";
|
|
22
|
+
import { bridgeEventBuffer, sendOutput, sendQuestionRequest, sendStatus, } from "../services/session-output.js";
|
|
23
|
+
import { getAllPushStats, startHeartbeatScheduler } from "../services/event-delta-pusher.js";
|
|
22
24
|
import { findPersistedSessionByAgentId, removePersistedSession, savePersistedSessions, updatePersistedSessionAutoCommands, updatePersistedSessionRuntimeState, upsertPersistedSession, } from "../services/terminal-persistence.js";
|
|
25
|
+
import { getInstanceAiConfigDebugLogger } from "../utils/instance-ai-config-debug-logger.js";
|
|
23
26
|
import { logger as log } from "../utils/logger.js";
|
|
24
27
|
export const terminalRouter = Router();
|
|
25
28
|
// 导出 SDK 会话存储,供其他模块使用(如 sessions.ts)
|
|
26
29
|
export const sdkSessions = new Map();
|
|
27
30
|
const TOOL_RESULT_PREVIEW_MAX_LENGTH = 4000;
|
|
28
31
|
const AI_REQUEST_BODY_MARKER = "\n\n请求体:\n";
|
|
32
|
+
/**
|
|
33
|
+
* 把请求体中的 instanceAiConfig 归一化为 AdapterInstanceAiConfig。
|
|
34
|
+
* 任意字段类型错误或非字符串都返回 undefined,调用方应继续走 env/file fallback。
|
|
35
|
+
*/
|
|
36
|
+
function sanitizeInstanceAiConfig(value) {
|
|
37
|
+
if (value == null)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
40
|
+
return undefined;
|
|
41
|
+
const raw = value;
|
|
42
|
+
const out = {};
|
|
43
|
+
if (typeof raw.model === "string" && raw.model.trim()) {
|
|
44
|
+
out.model = raw.model.trim();
|
|
45
|
+
}
|
|
46
|
+
if (typeof raw.baseURL === "string" && raw.baseURL.trim()) {
|
|
47
|
+
out.baseURL = raw.baseURL.trim();
|
|
48
|
+
}
|
|
49
|
+
if (typeof raw.apiKey === "string" && raw.apiKey.trim()) {
|
|
50
|
+
out.apiKey = raw.apiKey.trim();
|
|
51
|
+
}
|
|
52
|
+
if (typeof raw.effort === "string" && raw.effort.trim()) {
|
|
53
|
+
out.effort = raw.effort.trim();
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
29
57
|
const AI_REQUEST_HEADERS_MARKER = "\n\n请求头:\n";
|
|
30
58
|
const startingAgents = new Set();
|
|
31
59
|
export function resolveRuntimeContextResetSupport(providerId, hasResetContext) {
|
|
@@ -83,9 +111,13 @@ export function resolveStatusChangeUsage(actionInfo) {
|
|
|
83
111
|
const outputTokens = Number.isFinite(usage.outputTokens)
|
|
84
112
|
? usage.outputTokens
|
|
85
113
|
: 0;
|
|
86
|
-
if (inputTokens <= 0 && outputTokens <= 0)
|
|
114
|
+
if (inputTokens <= 0 && outputTokens <= 0 && !usage.contextLimit)
|
|
87
115
|
return undefined;
|
|
88
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
inputTokens,
|
|
118
|
+
outputTokens,
|
|
119
|
+
...(usage.contextLimit ? { contextLimit: usage.contextLimit } : {}),
|
|
120
|
+
};
|
|
89
121
|
}
|
|
90
122
|
export function resolveStatusChangeRuntimeState(status, actionInfo) {
|
|
91
123
|
const normalizedStatus = String(status || "")
|
|
@@ -109,14 +141,14 @@ const TERMINAL_COMMAND_INACTIVITY_TIMEOUT_MS = 60_000;
|
|
|
109
141
|
function padDatePart(value) {
|
|
110
142
|
return String(value).padStart(2, "0");
|
|
111
143
|
}
|
|
112
|
-
export function buildAcodeCommandOutputPath(
|
|
144
|
+
export function buildAcodeCommandOutputPath(agentId, now = new Date()) {
|
|
113
145
|
const year = now.getFullYear();
|
|
114
146
|
const month = padDatePart(now.getMonth() + 1);
|
|
115
147
|
const day = padDatePart(now.getDate());
|
|
116
148
|
const hour = padDatePart(now.getHours());
|
|
117
149
|
const minute = padDatePart(now.getMinutes());
|
|
118
150
|
const timestamp = `${year}${month}${day}-${hour}${minute}${padDatePart(now.getSeconds())}-${String(now.getMilliseconds()).padStart(3, "0")}`;
|
|
119
|
-
return path.join(
|
|
151
|
+
return path.join(os.homedir(), ".acode", "cmd-dist", agentId, `${year}-${month}-${day}`, hour, minute, `${timestamp}-cmd.out`);
|
|
120
152
|
}
|
|
121
153
|
export function countTerminalOutputLines(output) {
|
|
122
154
|
if (!output) {
|
|
@@ -138,7 +170,7 @@ export function formatCommandInactivityTimeoutNotice(timeoutMs = TERMINAL_COMMAN
|
|
|
138
170
|
}
|
|
139
171
|
async function persistTerminalCommandOutput(state) {
|
|
140
172
|
const outputPath = state.outputPath ||
|
|
141
|
-
buildAcodeCommandOutputPath(state.
|
|
173
|
+
buildAcodeCommandOutputPath(state.agentId, state.startedAt);
|
|
142
174
|
state.outputPath = outputPath;
|
|
143
175
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
144
176
|
await fs.writeFile(outputPath, state.chunks.join(""), "utf-8");
|
|
@@ -395,7 +427,7 @@ async function executeTerminalCommand(sessionId, commandValue) {
|
|
|
395
427
|
const stderrDecoder = createTerminalOutputDecoder();
|
|
396
428
|
const usesAcodeCommandOutputPolicy = entry.providerId === "acode";
|
|
397
429
|
const outputSpilloverState = {
|
|
398
|
-
|
|
430
|
+
agentId: entry.agentId,
|
|
399
431
|
startedAt: new Date(),
|
|
400
432
|
chunks: [],
|
|
401
433
|
spilled: false,
|
|
@@ -908,19 +940,11 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
|
|
|
908
940
|
};
|
|
909
941
|
}
|
|
910
942
|
if (event.type === "turn_complete") {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
? {
|
|
914
|
-
actionType: "ai_request",
|
|
915
|
-
actionLabel: "AI响应",
|
|
916
|
-
actionResult: event.data.responseBody,
|
|
917
|
-
usage: event.data.usage,
|
|
918
|
-
}
|
|
919
|
-
: undefined;
|
|
943
|
+
// 方案 A:不设置 "AI响应" action 信息,避免状态卡在 action 展示
|
|
944
|
+
// 只返回干净的 waiting_input 状态,usage 信息保留用于 token 统计
|
|
920
945
|
return {
|
|
921
946
|
status: "waiting_input",
|
|
922
|
-
actionInfo:
|
|
923
|
-
(event.data.usage ? { usage: event.data.usage } : undefined),
|
|
947
|
+
actionInfo: event.data.usage ? { usage: event.data.usage } : undefined,
|
|
924
948
|
};
|
|
925
949
|
}
|
|
926
950
|
if (event.type === "error") {
|
|
@@ -928,15 +952,111 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
|
|
|
928
952
|
}
|
|
929
953
|
return undefined;
|
|
930
954
|
}
|
|
931
|
-
|
|
955
|
+
// Per-session 输出队列:确保 sendOutput HTTP POST 按事件顺序串行到达 Server,
|
|
956
|
+
// 避免并发 POST 因网络时序导致流式文本 chunk 乱序。
|
|
957
|
+
const sdkOutputQueueBySession = new Map();
|
|
958
|
+
/** 小 chunk 积攒缓存:sessionId:sourceEventType → { buffer, seq, timer, sourceEventType, entry },不足 20 字符时积攒最多 500ms 再推送 */
|
|
959
|
+
const sdkOutputSmallChunkAccumulators = new Map();
|
|
960
|
+
/** 小 chunk 积攒阈值:字符数少于此值的 output 事件积攒发送 */
|
|
961
|
+
const SMALL_CHUNK_CHAR_THRESHOLD = 20;
|
|
962
|
+
/** 小 chunk 最大积攒时间(毫秒) */
|
|
963
|
+
const SMALL_CHUNK_MAX_ACCUMULATE_MS = 500;
|
|
964
|
+
function flushSmallChunkAccumulator(sessionId, sourceEventType) {
|
|
965
|
+
const key = sourceEventType
|
|
966
|
+
? `${sessionId}:${sourceEventType}`
|
|
967
|
+
: undefined;
|
|
968
|
+
// 如果指定了 key 则精确刷新;否则回退到按 sessionId 前缀查找并刷新所有(兼容大 chunk 触发的全量刷新)
|
|
969
|
+
let acc;
|
|
970
|
+
let accKey;
|
|
971
|
+
if (key) {
|
|
972
|
+
acc = sdkOutputSmallChunkAccumulators.get(key);
|
|
973
|
+
accKey = key;
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
// 刷新该 sessionId 下的所有 accumulator(可能有多种 sourceEventType)
|
|
977
|
+
for (const [k, v] of sdkOutputSmallChunkAccumulators) {
|
|
978
|
+
if (k.startsWith(sessionId + ":") && v.buffer) {
|
|
979
|
+
flushSmallChunkAccumulator(sessionId, v.sourceEventType);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (!acc || !acc.buffer)
|
|
985
|
+
return;
|
|
986
|
+
acc.timer = null;
|
|
987
|
+
const output = acc.buffer;
|
|
988
|
+
const seq = acc.seq;
|
|
989
|
+
const entry = acc.entry;
|
|
990
|
+
const eventType = acc.sourceEventType;
|
|
991
|
+
acc.buffer = "";
|
|
992
|
+
if (accKey)
|
|
993
|
+
sdkOutputSmallChunkAccumulators.delete(accKey);
|
|
994
|
+
const metadata = {
|
|
995
|
+
sourceEventType: eventType,
|
|
996
|
+
logCategory: "acode",
|
|
997
|
+
};
|
|
998
|
+
const authOptions = {
|
|
999
|
+
runtimeAccessToken: entry.runtimeAccessToken,
|
|
1000
|
+
schedulerBaseUrl: entry.schedulerBaseUrl,
|
|
1001
|
+
};
|
|
1002
|
+
const prev = sdkOutputQueueBySession.get(sessionId) ?? Promise.resolve();
|
|
1003
|
+
const next = prev.then(() => sendOutput(entry.agentId, output, sessionId, seq, metadata, authOptions), () => sendOutput(entry.agentId, output, sessionId, seq, metadata, authOptions));
|
|
1004
|
+
sdkOutputQueueBySession.set(sessionId, next);
|
|
1005
|
+
void next.finally(() => {
|
|
1006
|
+
if (sdkOutputQueueBySession.get(sessionId) === next) {
|
|
1007
|
+
sdkOutputQueueBySession.delete(sessionId);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
function enqueueSdkOutputEvent(event, entry) {
|
|
932
1012
|
const output = formatSdkOutputEvent(event);
|
|
933
1013
|
if (!output) {
|
|
934
1014
|
return;
|
|
935
1015
|
}
|
|
936
1016
|
entry.seq += 1;
|
|
937
|
-
|
|
1017
|
+
const seq = entry.seq;
|
|
1018
|
+
const metadata = buildSdkOutputMetadata(event);
|
|
1019
|
+
const authOptions = {
|
|
938
1020
|
runtimeAccessToken: entry.runtimeAccessToken,
|
|
939
1021
|
schedulerBaseUrl: entry.schedulerBaseUrl,
|
|
1022
|
+
};
|
|
1023
|
+
// 小 chunk 积攒优化:命令输出和 AI 文本增量等小片段不足阈值时积攒再推送
|
|
1024
|
+
if ((event.type === "command_output" ||
|
|
1025
|
+
event.type === "text_delta" ||
|
|
1026
|
+
event.type === "thinking") &&
|
|
1027
|
+
output.length < SMALL_CHUNK_CHAR_THRESHOLD) {
|
|
1028
|
+
const accKey = `${event.sessionId}:${event.type}`;
|
|
1029
|
+
let acc = sdkOutputSmallChunkAccumulators.get(accKey);
|
|
1030
|
+
if (!acc) {
|
|
1031
|
+
acc = {
|
|
1032
|
+
buffer: "",
|
|
1033
|
+
seq: 0,
|
|
1034
|
+
timer: null,
|
|
1035
|
+
entry,
|
|
1036
|
+
sourceEventType: event.type,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
acc.buffer += output;
|
|
1040
|
+
acc.seq = seq;
|
|
1041
|
+
acc.entry = entry;
|
|
1042
|
+
if (!acc.timer) {
|
|
1043
|
+
const capturedSessionId = event.sessionId;
|
|
1044
|
+
const capturedType = event.type;
|
|
1045
|
+
acc.timer = setTimeout(() => flushSmallChunkAccumulator(capturedSessionId, capturedType), SMALL_CHUNK_MAX_ACCUMULATE_MS);
|
|
1046
|
+
}
|
|
1047
|
+
sdkOutputSmallChunkAccumulators.set(accKey, acc);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
// 大 chunk 直接发送(先刷新可能存在的积攒缓存)
|
|
1051
|
+
flushSmallChunkAccumulator(event.sessionId);
|
|
1052
|
+
const prev = sdkOutputQueueBySession.get(event.sessionId) ?? Promise.resolve();
|
|
1053
|
+
const next = prev.then(() => sendOutput(entry.agentId, output, event.sessionId, seq, metadata, authOptions), () => sendOutput(entry.agentId, output, event.sessionId, seq, metadata, authOptions));
|
|
1054
|
+
sdkOutputQueueBySession.set(event.sessionId, next);
|
|
1055
|
+
// 队列空闲时自动清理,避免内存泄漏
|
|
1056
|
+
void next.finally(() => {
|
|
1057
|
+
if (sdkOutputQueueBySession.get(event.sessionId) === next) {
|
|
1058
|
+
sdkOutputQueueBySession.delete(event.sessionId);
|
|
1059
|
+
}
|
|
940
1060
|
});
|
|
941
1061
|
}
|
|
942
1062
|
export function resolveSdkProviderId(command) {
|
|
@@ -948,6 +1068,8 @@ function ensureAdapterInitialized() {
|
|
|
948
1068
|
if (adapterInitialized)
|
|
949
1069
|
return;
|
|
950
1070
|
registerSdkProviders(adapterRegistry, wireSdkAdapterEvents);
|
|
1071
|
+
// 需求 4.2.1 #2:启动全局心跳调度器(每 30s 推送空包检测 Server 连接存活)
|
|
1072
|
+
startHeartbeatScheduler();
|
|
951
1073
|
adapterInitialized = true;
|
|
952
1074
|
}
|
|
953
1075
|
function wireSdkAdapterEvents(adapter, definition) {
|
|
@@ -955,20 +1077,36 @@ function wireSdkAdapterEvents(adapter, definition) {
|
|
|
955
1077
|
const entry = sdkSessions.get(event.sessionId);
|
|
956
1078
|
if (entry) {
|
|
957
1079
|
console.log(`[${definition.displayName} Adapter] Event: ${event.type} for session ${event.sessionId}`);
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1080
|
+
// Skip sendStatus for thinking/text_delta events — the status-change listener
|
|
1081
|
+
// already emits the authoritative thinking status with restored actionInfo,
|
|
1082
|
+
// avoiding duplicate timeline entries in agentEventStore.
|
|
1083
|
+
const isThinkingDelta = event.type === "thinking" || event.type === "text_delta";
|
|
1084
|
+
if (!isThinkingDelta) {
|
|
1085
|
+
const runtimeStatus = resolveRuntimeStatusFromProviderEvent(event, entry.currentRuntimeStatus);
|
|
1086
|
+
if (runtimeStatus) {
|
|
1087
|
+
void sendStatus(entry.agentId, event.sessionId, runtimeStatus.status, runtimeStatus.actionInfo, resolveStatusChangeUsage(runtimeStatus.actionInfo), {
|
|
1088
|
+
runtimeAccessToken: entry.runtimeAccessToken,
|
|
1089
|
+
schedulerBaseUrl: entry.schedulerBaseUrl,
|
|
1090
|
+
},
|
|
1091
|
+
// Pass original timestamp from ACode runtime for correct timeline ordering
|
|
1092
|
+
// Convert ISO string timestamp to epoch milliseconds
|
|
1093
|
+
event.timestamp ? new Date(event.timestamp).getTime() : undefined,
|
|
1094
|
+
// Pass original event type for frontend gray-zone rendering (thinking vs text_delta)
|
|
1095
|
+
event.type);
|
|
1096
|
+
}
|
|
964
1097
|
}
|
|
965
|
-
|
|
1098
|
+
enqueueSdkOutputEvent(event, entry);
|
|
966
1099
|
const toolResultActionInfo = buildToolResultTimelineActionInfo(event);
|
|
967
1100
|
if (toolResultActionInfo) {
|
|
968
1101
|
void sendStatus(entry.agentId, event.sessionId, "tool_result", toolResultActionInfo, undefined, {
|
|
969
1102
|
runtimeAccessToken: entry.runtimeAccessToken,
|
|
970
1103
|
schedulerBaseUrl: entry.schedulerBaseUrl,
|
|
971
|
-
}
|
|
1104
|
+
},
|
|
1105
|
+
// Pass original timestamp from ACode runtime for correct timeline ordering
|
|
1106
|
+
// Convert ISO string timestamp to epoch milliseconds
|
|
1107
|
+
event.timestamp ? new Date(event.timestamp).getTime() : undefined,
|
|
1108
|
+
// Pass original event type (tool_use_end) for timeline sourceEventType
|
|
1109
|
+
event.type);
|
|
972
1110
|
}
|
|
973
1111
|
}
|
|
974
1112
|
});
|
|
@@ -977,18 +1115,45 @@ function wireSdkAdapterEvents(adapter, definition) {
|
|
|
977
1115
|
if (entry) {
|
|
978
1116
|
const runtimeState = resolveStatusChangeRuntimeState(status, actionInfo);
|
|
979
1117
|
entry.currentRuntimeStatus = runtimeState.status;
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1118
|
+
// 当新状态未携带动作且进入 thinking 时,保留上一个 idle 类动作(如 poll_message),
|
|
1119
|
+
// 避免页面刷新后丢失"监听消息"状态而错误显示"思考中"。
|
|
1120
|
+
// 注意:MCP client 发送的 actionType 是 "mcp" 而非 "idle",需要通过 actionDetail 判断。
|
|
1121
|
+
// ★ 关键修复: turn_complete 后进入 waiting_input 时,如果携带的是 ai_request 动作,
|
|
1122
|
+
// 不应该保留之前的 tool_using 动作,避免永久卡在工具调用状态。
|
|
1123
|
+
const prevActionDetail = entry.currentRuntimeActionDetail ?? "";
|
|
1124
|
+
const isPrevPollMessage = prevActionDetail.includes("poll_message") || prevActionDetail.includes("get_message");
|
|
1125
|
+
const isNewAiRequestAction = runtimeState.actionInfo?.actionType === "ai_request";
|
|
1126
|
+
const shouldPreservePrevAction = !isNewAiRequestAction && // 新动作不是 AI 响应
|
|
1127
|
+
(runtimeState.status === "thinking" || runtimeState.status === "waiting_input") && // 状态是 thinking 或 waiting_input
|
|
1128
|
+
(entry.currentRuntimeActionType === "idle" || entry.currentRuntimeActionType === "mcp") && // 之前是 idle/mcp 动作
|
|
1129
|
+
isPrevPollMessage; // 且之前是 poll_message
|
|
1130
|
+
if (runtimeState.actionInfo?.actionType || // 新状态携带了动作
|
|
1131
|
+
!shouldPreservePrevAction // 不满足保留条件
|
|
1132
|
+
) {
|
|
1133
|
+
entry.currentRuntimeActionType = runtimeState.actionInfo?.actionType;
|
|
1134
|
+
entry.currentRuntimeActionLabel = runtimeState.actionInfo?.actionLabel;
|
|
1135
|
+
entry.currentRuntimeActionDetail =
|
|
1136
|
+
runtimeState.actionInfo?.actionDetail;
|
|
1137
|
+
}
|
|
984
1138
|
console.log(`[${definition.displayName} Adapter] Status changed: ${runtimeState.status} for agent ${entry.agentId}`, runtimeState.actionInfo || "");
|
|
985
1139
|
void updatePersistedSessionRuntimeState(sessionId, {
|
|
986
1140
|
runtimeStatus: runtimeState.status,
|
|
987
|
-
runtimeActionType:
|
|
988
|
-
runtimeActionLabel:
|
|
989
|
-
runtimeActionDetail:
|
|
1141
|
+
runtimeActionType: entry.currentRuntimeActionType,
|
|
1142
|
+
runtimeActionLabel: entry.currentRuntimeActionLabel,
|
|
1143
|
+
runtimeActionDetail: entry.currentRuntimeActionDetail,
|
|
990
1144
|
});
|
|
991
|
-
|
|
1145
|
+
// thinking + poll_message 动作保留时,派生恢复用 actionInfo 传给 sendStatus
|
|
1146
|
+
// 支持 actionType 为 "idle" 或 "mcp",通过 actionDetail 中的 poll_message/get_message 判断
|
|
1147
|
+
const restoredActionInfo = runtimeState.actionInfo?.actionType
|
|
1148
|
+
? runtimeState.actionInfo
|
|
1149
|
+
: entry.currentRuntimeActionType
|
|
1150
|
+
? {
|
|
1151
|
+
actionType: entry.currentRuntimeActionType,
|
|
1152
|
+
actionLabel: entry.currentRuntimeActionLabel ?? "",
|
|
1153
|
+
actionDetail: entry.currentRuntimeActionDetail,
|
|
1154
|
+
}
|
|
1155
|
+
: undefined;
|
|
1156
|
+
void sendStatus(entry.agentId, sessionId, runtimeState.status, restoredActionInfo, resolveStatusChangeUsage(runtimeState.actionInfo), {
|
|
992
1157
|
runtimeAccessToken: entry.runtimeAccessToken,
|
|
993
1158
|
schedulerBaseUrl: entry.schedulerBaseUrl,
|
|
994
1159
|
});
|
|
@@ -1008,6 +1173,17 @@ function wireSdkAdapterEvents(adapter, definition) {
|
|
|
1008
1173
|
const entry = sdkSessions.get(data.sessionId);
|
|
1009
1174
|
if (entry) {
|
|
1010
1175
|
console.log(`[${definition.displayName} Adapter] Permission request: ${data.toolName} for agent ${entry.agentId}`);
|
|
1176
|
+
const actionDetail = data.toolInput
|
|
1177
|
+
? redactTransientClipboardPastePaths(JSON.stringify(data.toolInput).slice(0, 400))
|
|
1178
|
+
: undefined;
|
|
1179
|
+
void sendStatus(entry.agentId, data.sessionId, "waiting_input", {
|
|
1180
|
+
actionType: "other",
|
|
1181
|
+
actionLabel: "权限确认",
|
|
1182
|
+
actionDetail: actionDetail || data.toolName,
|
|
1183
|
+
}, undefined, {
|
|
1184
|
+
runtimeAccessToken: entry.runtimeAccessToken,
|
|
1185
|
+
schedulerBaseUrl: entry.schedulerBaseUrl,
|
|
1186
|
+
});
|
|
1011
1187
|
}
|
|
1012
1188
|
});
|
|
1013
1189
|
}
|
|
@@ -1049,7 +1225,24 @@ async function startSdkSession(req, res) {
|
|
|
1049
1225
|
// MCP 配置 - 加载 aws-client-agent-mcp
|
|
1050
1226
|
mcpConfigPath, extraMcpServers, envOverrides,
|
|
1051
1227
|
// 空闲命令配置
|
|
1052
|
-
idleInputAutoCommand, nonInputAutoCommand,
|
|
1228
|
+
idleInputAutoCommand, nonInputAutoCommand,
|
|
1229
|
+
// Per-agent model override(优先于全局配置)
|
|
1230
|
+
model: perAgentModel,
|
|
1231
|
+
// Per-instance AI provider 覆盖(仅 ACode 消费;优先级:api > env > file > global)
|
|
1232
|
+
instanceAiConfig: perInstanceAiConfig, } = req.body || {};
|
|
1233
|
+
const _dbg = getInstanceAiConfigDebugLogger();
|
|
1234
|
+
void _dbg.log({
|
|
1235
|
+
level: "info",
|
|
1236
|
+
category: "launch",
|
|
1237
|
+
message: "startSdkSession: 收到启动请求",
|
|
1238
|
+
source: "bridge",
|
|
1239
|
+
agentId: String(agentId || "").trim() || undefined,
|
|
1240
|
+
details: {
|
|
1241
|
+
reqBodyKeys: Object.keys(req.body || {}),
|
|
1242
|
+
perInstanceAiConfig: perInstanceAiConfig ? redactForLog(perInstanceAiConfig) : null,
|
|
1243
|
+
perAgentModel: perAgentModel ?? null,
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1053
1246
|
let sessionId = null;
|
|
1054
1247
|
const normalizedAgentId = String(agentId || "").trim();
|
|
1055
1248
|
const runtimeAccessToken = String(req.body?.runtimeAccessToken || "").trim() || undefined;
|
|
@@ -1098,7 +1291,6 @@ async function startSdkSession(req, res) {
|
|
|
1098
1291
|
// 根据命令选择对应的 adapter
|
|
1099
1292
|
const normalizedCommand = (command || "acode").toLowerCase();
|
|
1100
1293
|
const providerId = resolveSdkProviderId(command);
|
|
1101
|
-
console.log(`[Runtime] ★★★ SDK启动 ★★★ command="${command}", normalizedCommand="${normalizedCommand}", providerId="${providerId}"`);
|
|
1102
1294
|
const adapter = adapterRegistry.get(providerId);
|
|
1103
1295
|
sessionId = uuidv4();
|
|
1104
1296
|
const savedClaudeConfig = providerId === "claude-code"
|
|
@@ -1107,18 +1299,108 @@ async function startSdkSession(req, res) {
|
|
|
1107
1299
|
const claudeLaunchConfig = providerId === "claude-code"
|
|
1108
1300
|
? buildClaudeCodeLaunchConfig(savedClaudeConfig, envOverrides)
|
|
1109
1301
|
: buildClaudeCodeLaunchConfig(null, envOverrides);
|
|
1302
|
+
// 模型解析优先级(ACode 与非 ACode 不同):
|
|
1303
|
+
//
|
|
1304
|
+
// ACode:模型仲裁收敛到 AcodeSdkAdapter.resolveAcodeProvider → resolveAcodeProviderConfig 纯函数
|
|
1305
|
+
// 优先级:perSessionModel(request) > instanceAiConfig(instance) > acode.json(global) > default
|
|
1306
|
+
// 这里不预解析,将原始输入传递给 adapter
|
|
1307
|
+
//
|
|
1308
|
+
// 非 ACode (claude-code/codex/opencode):
|
|
1309
|
+
// 优先级:perAgentModel(request) > claudeLaunchConfig.model(global)
|
|
1310
|
+
// 通过 ANTHROPIC_MODEL/CODEX_MODEL 环境变量注入
|
|
1311
|
+
const resolvedModel = providerId === "acode"
|
|
1312
|
+
? (perAgentModel ? String(perAgentModel).trim() : undefined)
|
|
1313
|
+
: (() => {
|
|
1314
|
+
const requestModel = perAgentModel ? String(perAgentModel).trim() : undefined;
|
|
1315
|
+
const globalModel = claudeLaunchConfig.model ? String(claudeLaunchConfig.model).trim() : undefined;
|
|
1316
|
+
return requestModel || globalModel;
|
|
1317
|
+
})();
|
|
1318
|
+
void _dbg.log({
|
|
1319
|
+
level: "info",
|
|
1320
|
+
category: "resolve",
|
|
1321
|
+
message: "模型解析结果",
|
|
1322
|
+
source: "bridge",
|
|
1323
|
+
agentId: normalizedAgentId,
|
|
1324
|
+
sessionId: sessionId ?? undefined,
|
|
1325
|
+
details: {
|
|
1326
|
+
perAgentModel: perAgentModel ?? null,
|
|
1327
|
+
globalModel: claudeLaunchConfig.model ?? null,
|
|
1328
|
+
resolvedModel: resolvedModel ?? null,
|
|
1329
|
+
providerId,
|
|
1330
|
+
note: providerId === "acode" ? "adapter handles full priority" : "request > global",
|
|
1331
|
+
},
|
|
1332
|
+
});
|
|
1333
|
+
// 构建 effectiveEnvOverrides:将 resolvedModel 注入各 provider 对应的环境变量
|
|
1334
|
+
const effectiveEnvOverrides = { ...claudeLaunchConfig.envOverrides };
|
|
1335
|
+
if (resolvedModel) {
|
|
1336
|
+
// Claude Code 使用 ANTHROPIC_MODEL
|
|
1337
|
+
effectiveEnvOverrides.ANTHROPIC_MODEL = resolvedModel;
|
|
1338
|
+
// Codex 使用 CODEX_MODEL
|
|
1339
|
+
effectiveEnvOverrides.CODEX_MODEL = resolvedModel;
|
|
1340
|
+
// ACode / OpenCode: 不通过 env var 传递模型
|
|
1341
|
+
}
|
|
1342
|
+
// ACode 实例自配置:解析 api > env > file,deep-merge 全局后写入 per-instance acode.json
|
|
1343
|
+
// 其它 provider 不消费 instanceAiConfig,保持向后兼容
|
|
1344
|
+
// 模型仲裁由 AcodeSdkAdapter.resolveAcodeProvider 统一处理
|
|
1345
|
+
let resolvedInstanceAiConfig;
|
|
1346
|
+
let resolvedMcpConfigPath = mcpConfigPath;
|
|
1347
|
+
if (providerId === "acode") {
|
|
1348
|
+
const mergedInstanceConfig = perInstanceAiConfig || {};
|
|
1349
|
+
const prepared = await prepareAcodeInstanceConfig({
|
|
1350
|
+
apiOverrides: sanitizeInstanceAiConfig(mergedInstanceConfig) ?? undefined,
|
|
1351
|
+
agentId: normalizedAgentId,
|
|
1352
|
+
sessionId: sessionId ?? undefined,
|
|
1353
|
+
runtimeBridgeBaseUrl: requestedSchedulerBaseUrl,
|
|
1354
|
+
});
|
|
1355
|
+
resolvedInstanceAiConfig = prepared.instanceConfig.config;
|
|
1356
|
+
resolvedMcpConfigPath = prepared.perInstanceConfigPath;
|
|
1357
|
+
void _dbg.log({
|
|
1358
|
+
level: "info",
|
|
1359
|
+
category: "apply",
|
|
1360
|
+
message: "prepareAcodeInstanceConfig 完成",
|
|
1361
|
+
source: "bridge",
|
|
1362
|
+
agentId: normalizedAgentId,
|
|
1363
|
+
sessionId: sessionId ?? undefined,
|
|
1364
|
+
details: {
|
|
1365
|
+
sanitizedApiOverrides: sanitizeInstanceAiConfig(mergedInstanceConfig) ? redactForLog(sanitizeInstanceAiConfig(mergedInstanceConfig)) : null,
|
|
1366
|
+
resolvedInstanceAiConfig: resolvedInstanceAiConfig ? redactForLog(resolvedInstanceAiConfig) : null,
|
|
1367
|
+
perInstanceConfigPath: resolvedMcpConfigPath,
|
|
1368
|
+
instanceConfigSources: prepared.instanceConfig.source ?? null,
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1110
1372
|
// 构建SDK配置
|
|
1111
1373
|
const config = {
|
|
1112
1374
|
command: command || "acode",
|
|
1113
1375
|
workingDirectory: workspacePath,
|
|
1376
|
+
agentId: normalizedAgentId,
|
|
1114
1377
|
autoAccept: Boolean(autoAccept),
|
|
1115
1378
|
initialPrompt,
|
|
1116
|
-
model:
|
|
1379
|
+
model: resolvedModel,
|
|
1117
1380
|
// MCP 配置:不再默认注入 aws-mcp;由用户在面板中安装/配置
|
|
1118
|
-
|
|
1381
|
+
// ACode 场景下,per-instance acode.json 已由 instance-ai-config-service 写入
|
|
1382
|
+
mcpConfigPath: resolvedMcpConfigPath,
|
|
1383
|
+
// ACode 实例自配置:来源与值会写入 d:/Instance-ai-configuration-debug.log
|
|
1384
|
+
...(resolvedInstanceAiConfig
|
|
1385
|
+
? { instanceAiConfig: resolvedInstanceAiConfig }
|
|
1386
|
+
: {}),
|
|
1119
1387
|
extraMcpServers: extraMcpServers || {},
|
|
1120
|
-
envOverrides: buildRuntimeEnv(String(agentId), String(workspacePath), process.env,
|
|
1388
|
+
envOverrides: buildRuntimeEnv(String(agentId), String(workspacePath), process.env, effectiveEnvOverrides),
|
|
1121
1389
|
};
|
|
1390
|
+
void _dbg.log({
|
|
1391
|
+
level: "info",
|
|
1392
|
+
category: "launch",
|
|
1393
|
+
message: "传递给 Adapter 的完整 AdapterSessionConfig",
|
|
1394
|
+
source: "bridge",
|
|
1395
|
+
agentId: normalizedAgentId,
|
|
1396
|
+
sessionId,
|
|
1397
|
+
details: {
|
|
1398
|
+
adapterModel: config.model ?? null,
|
|
1399
|
+
adapterInstanceAiConfig: config.instanceAiConfig ? redactForLog(config.instanceAiConfig) : null,
|
|
1400
|
+
adapterMcpConfigPath: config.mcpConfigPath ?? null,
|
|
1401
|
+
},
|
|
1402
|
+
});
|
|
1403
|
+
await _dbg.flush();
|
|
1122
1404
|
// 存储会话信息
|
|
1123
1405
|
sdkSessions.set(sessionId, {
|
|
1124
1406
|
agentId: normalizedAgentId,
|
|
@@ -1784,3 +2066,58 @@ terminalRouter.post("/stop-all", validateToken, async (_req, res) => {
|
|
|
1784
2066
|
errors: errors.length > 0 ? errors : undefined,
|
|
1785
2067
|
});
|
|
1786
2068
|
});
|
|
2069
|
+
// ============ 桥接侧事件查询 ============
|
|
2070
|
+
/**
|
|
2071
|
+
* 查询最近 N 条桥接侧本地事件。
|
|
2072
|
+
*
|
|
2073
|
+
* 用于前端面板或调试工具获取最近的状态/输出事件快照。
|
|
2074
|
+
* GET /runtime/events/last/:n
|
|
2075
|
+
*/
|
|
2076
|
+
terminalRouter.get("/events/last/:n", validateToken, (req, res) => {
|
|
2077
|
+
const nParam = parseInt(req.params.n, 10);
|
|
2078
|
+
if (!Number.isFinite(nParam) || nParam < 1) {
|
|
2079
|
+
res.status(400).json({ error: ":n must be a positive integer" });
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const entries = bridgeEventBuffer.getLastN(nParam);
|
|
2083
|
+
res.json({
|
|
2084
|
+
ok: true,
|
|
2085
|
+
count: entries.length,
|
|
2086
|
+
entries,
|
|
2087
|
+
});
|
|
2088
|
+
});
|
|
2089
|
+
/**
|
|
2090
|
+
* 查询自指定 index(不含)之后的桥接侧本地事件。
|
|
2091
|
+
*
|
|
2092
|
+
* 增量轮询端点:客户端保存上一次看到的 index,
|
|
2093
|
+
* 下次传入该 index 仅获取增量事件,适用于实时刷新场景。
|
|
2094
|
+
* GET /runtime/events/since/:index
|
|
2095
|
+
*/
|
|
2096
|
+
terminalRouter.get("/events/since/:index", validateToken, (req, res) => {
|
|
2097
|
+
const indexParam = parseInt(req.params.index, 10);
|
|
2098
|
+
if (!Number.isFinite(indexParam) || indexParam < 0) {
|
|
2099
|
+
res.status(400).json({ error: ":index must be a non-negative integer" });
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
const slice = bridgeEventBuffer.getSinceIndex(indexParam);
|
|
2103
|
+
res.json({
|
|
2104
|
+
ok: true,
|
|
2105
|
+
entries: slice.entries,
|
|
2106
|
+
nextIndex: slice.nextIndex,
|
|
2107
|
+
oldestIndex: slice.oldestIndex,
|
|
2108
|
+
dataLoss: slice.oldestIndex > indexParam + 1,
|
|
2109
|
+
});
|
|
2110
|
+
});
|
|
2111
|
+
/**
|
|
2112
|
+
* 获取 Bridge 侧事件推送统计(需求 4.2.1 #3)。
|
|
2113
|
+
* 返回每个 Agent 的待推送数、累计成功/失败、连续失败次数、最近延迟等。
|
|
2114
|
+
* GET /runtime/events/push-stats
|
|
2115
|
+
*/
|
|
2116
|
+
terminalRouter.get("/events/push-stats", validateToken, (_req, res) => {
|
|
2117
|
+
const stats = getAllPushStats();
|
|
2118
|
+
res.json({
|
|
2119
|
+
ok: true,
|
|
2120
|
+
count: stats.length,
|
|
2121
|
+
agents: stats,
|
|
2122
|
+
});
|
|
2123
|
+
});
|
|
@@ -293,6 +293,19 @@ describe("SDK provider event runtime status mapping", () => {
|
|
|
293
293
|
actionId: "tool-idle-1",
|
|
294
294
|
});
|
|
295
295
|
});
|
|
296
|
+
it("turn_complete returns clean waiting_input without action info (A方案)", () => {
|
|
297
|
+
// 验证: turn_complete 后返回干净的 waiting_input 状态,不设置 "AI响应" action
|
|
298
|
+
// 这样可以避免状态卡在 "AI响应" 显示
|
|
299
|
+
const state = resolveRuntimeStatusFromProviderEvent(event("turn_complete", {
|
|
300
|
+
responseBody: '{"choices":[{"message":{"content":"done"}}]}',
|
|
301
|
+
usage: { inputTokens: 100, outputTokens: 50 },
|
|
302
|
+
}));
|
|
303
|
+
expect(state?.status).toBe("waiting_input");
|
|
304
|
+
// 方案 A:不再设置 ai_request action,只保留 usage
|
|
305
|
+
expect(state?.actionInfo?.actionType).toBeUndefined();
|
|
306
|
+
expect(state?.actionInfo?.actionLabel).toBeUndefined();
|
|
307
|
+
expect(state?.actionInfo?.usage).toEqual({ inputTokens: 100, outputTokens: 50 });
|
|
308
|
+
});
|
|
296
309
|
});
|
|
297
310
|
describe("terminal command helpers", () => {
|
|
298
311
|
it("expands a whole mini-term clipboard paste file path into SDK input content", async () => {
|
|
@@ -339,9 +352,9 @@ describe("terminal command helpers", () => {
|
|
|
339
352
|
expect(countTerminalOutputLines("one\ntwo\n")).toBe(2);
|
|
340
353
|
expect(countTerminalOutputLines("one\r\ntwo\rthree")).toBe(3);
|
|
341
354
|
});
|
|
342
|
-
it("builds ACode command output spillover paths under
|
|
343
|
-
const outputPath = buildAcodeCommandOutputPath(
|
|
344
|
-
expect(outputPath).toBe(path.join(
|
|
355
|
+
it("builds ACode command output spillover paths under ~/.acode/cmd-dist/<agentId>/", () => {
|
|
356
|
+
const outputPath = buildAcodeCommandOutputPath("agent-abc-123", new Date(2026, 1, 3, 4, 5, 6, 7));
|
|
357
|
+
expect(outputPath).toBe(path.join(os.homedir(), ".acode", "cmd-dist", "agent-abc-123", "2026-02-03", "04", "05", "20260203-040506-007-cmd.out"));
|
|
345
358
|
});
|
|
346
359
|
it("formats ACode long-output and inactivity notices for terminal users", () => {
|
|
347
360
|
expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 300)).toContain("命令输出超过 300 行");
|
|
@@ -399,7 +412,7 @@ describe("SDK status usage forwarding", () => {
|
|
|
399
412
|
actionLabel: "Calling MCP",
|
|
400
413
|
})).toBeUndefined();
|
|
401
414
|
});
|
|
402
|
-
it("
|
|
415
|
+
it("returns waiting_input without action info on turn_complete (A方案:避免状态卡住)", () => {
|
|
403
416
|
const runtimeStatus = resolveRuntimeStatusFromProviderEvent({
|
|
404
417
|
type: "turn_complete",
|
|
405
418
|
sessionId: "session-1",
|
|
@@ -410,16 +423,29 @@ describe("SDK status usage forwarding", () => {
|
|
|
410
423
|
finishReason: "stop",
|
|
411
424
|
},
|
|
412
425
|
});
|
|
426
|
+
// 方案 A:不再设置 "AI响应" action 信息,只保留 usage 用于 token 统计
|
|
413
427
|
expect(runtimeStatus).toEqual({
|
|
414
428
|
status: "waiting_input",
|
|
415
429
|
actionInfo: {
|
|
416
|
-
actionType: "ai_request",
|
|
417
|
-
actionLabel: "AI响应",
|
|
418
|
-
actionResult: '{"choices":[{"message":{"content":"done"}}]}',
|
|
419
430
|
usage: { inputTokens: 12, outputTokens: 4 },
|
|
420
431
|
},
|
|
421
432
|
});
|
|
422
433
|
});
|
|
434
|
+
it("returns waiting_input without action info when turn_complete has no usage", () => {
|
|
435
|
+
const runtimeStatus = resolveRuntimeStatusFromProviderEvent({
|
|
436
|
+
type: "turn_complete",
|
|
437
|
+
sessionId: "session-1",
|
|
438
|
+
timestamp: new Date(0).toISOString(),
|
|
439
|
+
data: {
|
|
440
|
+
responseBody: '{"choices":[{"message":{"content":"done"}}]}',
|
|
441
|
+
finishReason: "stop",
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
expect(runtimeStatus).toEqual({
|
|
445
|
+
status: "waiting_input",
|
|
446
|
+
actionInfo: undefined,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
423
449
|
it("includes file paths and commands in tool status action details", () => {
|
|
424
450
|
expect(resolveRuntimeStatusFromProviderEvent({
|
|
425
451
|
type: "tool_use_start",
|