aws-runtime-bridge 1.9.45 → 1.9.51
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 +6 -0
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +20 -4
- package/dist/adapter/CodexSdkAdapter.js +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +1 -1
- package/dist/index.js +4 -0
- package/dist/routes/file-diff-rollback.d.ts +70 -0
- package/dist/routes/file-diff-rollback.d.ts.map +1 -0
- package/dist/routes/file-diff-rollback.js +301 -0
- package/dist/routes/task-context.d.ts +8 -0
- package/dist/routes/task-context.d.ts.map +1 -0
- package/dist/routes/task-context.js +78 -0
- package/dist/services/workspaceTaskChangeWatcher.d.ts +87 -0
- package/dist/services/workspaceTaskChangeWatcher.d.ts.map +1 -0
- package/dist/services/workspaceTaskChangeWatcher.js +483 -0
- package/package/acode/dist/built-in-file-tools.d.ts +9 -1
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
- package/package/acode/dist/built-in-file-tools.js +86 -5
- package/package/acode/dist/mcpToolNames.js +1 -1
- package/package/acode/dist/runtime.d.ts +36 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +83 -2
- package/package/acode/dist/types.d.ts +6 -0
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +3 -7
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +10 -16
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +7 -2
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +4 -21
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +0 -4
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +44 -55
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +0 -11
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package.json +3 -1
|
@@ -43,6 +43,12 @@ type ACodeRuntimeConstructor = new (config: {
|
|
|
43
43
|
memory?: string;
|
|
44
44
|
env?: Record<string, string>;
|
|
45
45
|
subAgentManager?: SubAgentManager;
|
|
46
|
+
onFileChange?: (record: {
|
|
47
|
+
filePath: string;
|
|
48
|
+
operation: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
49
|
+
beforeContent: string | null;
|
|
50
|
+
afterContent: string | null;
|
|
51
|
+
}) => void | Promise<void>;
|
|
46
52
|
}) => ACodeRuntimeInstance;
|
|
47
53
|
export declare function toAcodeEsmImportSpecifier(modulePath: string): string;
|
|
48
54
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/AcodeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"AcodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/AcodeSdkAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,6 +21,7 @@ import { getInstanceAiConfigDebugLogger } from "../utils/instance-ai-config-debu
|
|
|
21
21
|
import { redactForLog } from "../services/instance-ai-config-service.js";
|
|
22
22
|
import { subAgentManager } from "../services/sub-agent-manager-instance.js";
|
|
23
23
|
import { resolveAcodeProviderConfig, } from "./instance-ai-config-resolver.js";
|
|
24
|
+
import { recordAgentFileChange } from "../services/workspaceTaskChangeWatcher.js";
|
|
24
25
|
import { isMessagePollingIdleCommand, isPollMessageToolDescriptor, } from "./acode-tool-names.js";
|
|
25
26
|
import { IdlePollLoop } from "./idle-poll-loop.js";
|
|
26
27
|
import { getCanonicalMcpDetail, getToolActionInfo } from "./types.js";
|
|
@@ -58,7 +59,7 @@ const ACODE_SUBMIT_TIMEOUT_MS = 120000; // 120秒(2分钟)
|
|
|
58
59
|
// 兜底超时:mcp_status 缓存能力后,等待 skill_status 的最长时间。
|
|
59
60
|
// 超时后单独发出 MCP 能力清单,避免 skill_status 事件缺失导致"暂无能力清单"。
|
|
60
61
|
const CAPABILITIES_FALLBACK_MS = 3000;
|
|
61
|
-
const ACODE_MESSAGE_POLLING_IDLE_PROMPT = "系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。";
|
|
62
|
+
const ACODE_MESSAGE_POLLING_IDLE_PROMPT = "系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。 调用 submit_task_result 时请一并传入 changeLog(本次处理的文件/内容变更日志,包含 operation 与 path),用于 AI 回退。";
|
|
62
63
|
function getAcodePackageEntryPath() {
|
|
63
64
|
return path.resolve(getReleasedAcodeEntryPath());
|
|
64
65
|
}
|
|
@@ -659,6 +660,7 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
659
660
|
...(config.subAgentModel ? { subAgentModel: config.subAgentModel } : {}),
|
|
660
661
|
...(config.subAgentEffort ? { subAgentEffort: config.subAgentEffort } : {}),
|
|
661
662
|
...(config.instanceAiConfig?.effort ? { effort: config.instanceAiConfig.effort } : {}),
|
|
663
|
+
onFileChange: (record) => recordAgentFileChange(record),
|
|
662
664
|
});
|
|
663
665
|
const debugLogger = getInstanceAiConfigDebugLogger();
|
|
664
666
|
void debugLogger.log({
|
|
@@ -1799,13 +1801,27 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1799
1801
|
currentEntry.watchdogLastDataFlowTime = now;
|
|
1800
1802
|
return;
|
|
1801
1803
|
}
|
|
1802
|
-
//
|
|
1803
|
-
|
|
1804
|
+
// ★ 看门狗只在 thinking 状态工作:检测 ai_request 发出后 provider 流挂死。
|
|
1805
|
+
// tool_using 状态下完全不触发看门狗——每个工具都有自己的超时机制:
|
|
1806
|
+
// - MCP 普通工具:60s(DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS)
|
|
1807
|
+
// - 内置文件工具/Bash:fallbackTimeoutMs / 自带 timeout_ms
|
|
1808
|
+
// - ask_user_question:question_request 事件取消看门狗,依赖用户回答
|
|
1809
|
+
// - await_sub_agents:无超时,依赖子 agent 自己的 runtime 看门狗
|
|
1810
|
+
// 若在 tool_using 触发看门狗,abort() 即使能中断工具,也会打断 agent 正在进行的
|
|
1811
|
+
// 合法长时间操作(如等待子 agent 完成),违反"看门狗不应影响 agent 动作"原则。
|
|
1812
|
+
// 同时重置计时基准,避免 tool_using 期间累积时间带到下一次 thinking 误触发。
|
|
1813
|
+
if (status === "tool_using") {
|
|
1814
|
+
currentEntry.watchdogLastDataFlowTime = now;
|
|
1815
|
+
currentEntry.watchdogSubmitStartTime = now;
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
// 其他非 thinking 状态(compacting/error 等)不计时
|
|
1819
|
+
if (status !== "thinking") {
|
|
1804
1820
|
return;
|
|
1805
1821
|
}
|
|
1806
1822
|
const submitElapsed = now - (currentEntry.watchdogSubmitStartTime || now);
|
|
1807
1823
|
const dataFlowElapsed = now - (currentEntry.watchdogLastDataFlowTime || now);
|
|
1808
|
-
// 数据流超时(40s 无流式数据)—
|
|
1824
|
+
// 数据流超时(40s 无流式数据)— 仅 thinking 状态触发,比 submit 超时(120s)更短,优先触发
|
|
1809
1825
|
if (dataFlowElapsed >= ACODE_DATA_FLOW_STREAM_WATCHDOG_MS) {
|
|
1810
1826
|
console.warn(`[ACode] CRITICAL: Data flow watchdog triggered after ${Math.round(dataFlowElapsed / 1000)}s ` +
|
|
1811
1827
|
`(source: ${source}, session: ${sessionId}, status: ${status}). ` +
|
|
@@ -822,7 +822,7 @@ export class CodexSdkAdapter extends EventEmitter {
|
|
|
822
822
|
toIdlePrompt(command) {
|
|
823
823
|
const trimmed = command.trim();
|
|
824
824
|
if (this.isMessagePollingIdleCommand(trimmed)) {
|
|
825
|
-
return '系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。';
|
|
825
|
+
return '系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。 调用 submit_task_result 时请一并传入 changeLog(本次处理的文件/内容变更日志,包含 operation 与 path),用于 AI 回退。';
|
|
826
826
|
}
|
|
827
827
|
return trimmed;
|
|
828
828
|
}
|
|
@@ -865,7 +865,7 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
865
865
|
toIdlePrompt(command) {
|
|
866
866
|
const trimmed = command.trim();
|
|
867
867
|
if (this.isMessagePollingIdleCommand(trimmed)) {
|
|
868
|
-
return '系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。';
|
|
868
|
+
return '系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。 调用 submit_task_result 时请一并传入 changeLog(本次处理的文件/内容变更日志,包含 operation 与 path),用于 AI 回退。';
|
|
869
869
|
}
|
|
870
870
|
return trimmed;
|
|
871
871
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,8 @@ import { fileBrowserRouter } from "./routes/file-browser.js";
|
|
|
26
26
|
import { aiSourcesRouter } from "./routes/ai-sources.js";
|
|
27
27
|
import { processesRouter } from "./routes/processes.js";
|
|
28
28
|
import { systemMetricsRouter } from "./routes/system-metrics.js";
|
|
29
|
+
import { taskContextRouter } from "./routes/task-context.js";
|
|
30
|
+
import { fileDiffRollbackRouter } from "./routes/file-diff-rollback.js";
|
|
29
31
|
import { attachPtyWebSocketServer, closeAllPtySessions, ptyRouter, ptySessions, } from "./routes/pty.js";
|
|
30
32
|
import { runtimeBindingRouter, logRuntimeBindingStartupState, } from "./routes/runtime-binding.js";
|
|
31
33
|
import { dashboardRouter } from "./routes/dashboard.js";
|
|
@@ -309,6 +311,8 @@ app.use("/runtime", memoryRouter);
|
|
|
309
311
|
app.use("/runtime", acodeTodosRouter);
|
|
310
312
|
app.use("/runtime", subAgentsRouter);
|
|
311
313
|
app.use("/runtime", aiSourcesRouter);
|
|
314
|
+
app.use("/runtime", taskContextRouter);
|
|
315
|
+
app.use("/runtime", fileDiffRollbackRouter);
|
|
312
316
|
app.use("/pty", ptyRouter);
|
|
313
317
|
app.use("/", dashboardRouter);
|
|
314
318
|
app.use("/api/file-browser", fileBrowserRouter);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件差异回滚路由
|
|
3
|
+
*
|
|
4
|
+
* 根据变更记录的反向操作执行文件回滚。
|
|
5
|
+
* 与全量快照还原不同的是,只还原被修改过的文件。
|
|
6
|
+
*
|
|
7
|
+
* 核心流程(两步走):
|
|
8
|
+
* 1. 内存预检:在内存中按修改时间逆序模拟所有反向操作,验证完整性。不触碰文件系统。
|
|
9
|
+
* - CREATE:检查文件是否存在,准备删除
|
|
10
|
+
* - UPDATE:读取 diff patch,读取当前文件内容,在内存中应用反向 patch 验证是否成功
|
|
11
|
+
* - DELETE:读取删除前的完整内容,准备重建
|
|
12
|
+
* 2. 原子执行:预检全部通过后,批量写入文件系统;任一预检失败则返回 previewFailed=true,
|
|
13
|
+
* 由调用方(前端)弹出"跳过不通过项 / 文件全量覆盖"二选一对话框。
|
|
14
|
+
* skipFailed=true 时仅执行通过项(partial);否则不写盘。
|
|
15
|
+
*
|
|
16
|
+
* 反向操作映射:
|
|
17
|
+
* CREATE → DELETE(删除创建的文件)
|
|
18
|
+
* UPDATE → 应用反向 diff patch(精确撤销修改的部分,保留用户后续改动);
|
|
19
|
+
* 若补丁无法精确应用(上下文不匹配=冲突),报为预检失败,不静默整文件覆盖
|
|
20
|
+
* DELETE → 重新创建被删除的文件(根据 cacheKey 从 bridge 本地缓存读取)
|
|
21
|
+
*/
|
|
22
|
+
export declare const fileDiffRollbackRouter: import("express-serve-static-core").Router;
|
|
23
|
+
export interface ReverseOperation {
|
|
24
|
+
/** 原始操作类型 */
|
|
25
|
+
originalOperation: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
26
|
+
/** 文件路径(相对于工作区) */
|
|
27
|
+
relativePath: string;
|
|
28
|
+
/** bridge 本地缓存 key,UPDATE/DELETE 时用于读取变更前内容或 diff */
|
|
29
|
+
cacheKey: string | null;
|
|
30
|
+
/** 原始创建时间(用于日志与排序) */
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
export interface DiffRollbackRequest {
|
|
34
|
+
workspacePath: string;
|
|
35
|
+
operations: ReverseOperation[];
|
|
36
|
+
/**
|
|
37
|
+
* 预检部分失败时的处理策略。
|
|
38
|
+
* - 缺省/ false:任一预检失败则直接返回 previewFailed,不写盘(由调用方决定后续动作)。
|
|
39
|
+
* - true:仅执行通过预检的操作(preview.plan 中的项),跳过失败项,
|
|
40
|
+
* 返回 partial:true,让调用方可以"跳过不通过项"而非整工作区快照覆盖。
|
|
41
|
+
*/
|
|
42
|
+
skipFailed?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** 单条操作的执行状态 */
|
|
45
|
+
export interface OperationDetail {
|
|
46
|
+
relativePath: string;
|
|
47
|
+
originalOperation: string;
|
|
48
|
+
/** 当前阶段状态 */
|
|
49
|
+
status: 'preview_passed' | 'preview_failed' | 'success' | 'failed' | 'skipped';
|
|
50
|
+
/** 计划执行的操作描述 */
|
|
51
|
+
action?: string;
|
|
52
|
+
/** 失败或跳过的原因 */
|
|
53
|
+
reason?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface DiffRollbackResult {
|
|
56
|
+
ok: boolean;
|
|
57
|
+
restored: number;
|
|
58
|
+
skipped: number;
|
|
59
|
+
errors: string[];
|
|
60
|
+
/** 当 ok=false 且未执行写盘时为 true */
|
|
61
|
+
previewFailed?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* 部分执行标志:skipFailed=true 且仅有部分操作通过预检时,
|
|
64
|
+
* 已对通过项执行写盘、跳过失败项,返回 partial:true。
|
|
65
|
+
*/
|
|
66
|
+
partial?: boolean;
|
|
67
|
+
/** 每条操作的详细状态 */
|
|
68
|
+
details: OperationDetail[];
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=file-diff-rollback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-diff-rollback.d.ts","sourceRoot":"","sources":["../../src/routes/file-diff-rollback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAWH,eAAO,MAAM,sBAAsB,4CAAW,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,aAAa;IACb,iBAAiB,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAClD,mBAAmB;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,sBAAsB;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,gBAAgB;AAChB,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa;IACb,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC/E,gBAAgB;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,+BAA+B;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB;IAChB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B"}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件差异回滚路由
|
|
3
|
+
*
|
|
4
|
+
* 根据变更记录的反向操作执行文件回滚。
|
|
5
|
+
* 与全量快照还原不同的是,只还原被修改过的文件。
|
|
6
|
+
*
|
|
7
|
+
* 核心流程(两步走):
|
|
8
|
+
* 1. 内存预检:在内存中按修改时间逆序模拟所有反向操作,验证完整性。不触碰文件系统。
|
|
9
|
+
* - CREATE:检查文件是否存在,准备删除
|
|
10
|
+
* - UPDATE:读取 diff patch,读取当前文件内容,在内存中应用反向 patch 验证是否成功
|
|
11
|
+
* - DELETE:读取删除前的完整内容,准备重建
|
|
12
|
+
* 2. 原子执行:预检全部通过后,批量写入文件系统;任一预检失败则返回 previewFailed=true,
|
|
13
|
+
* 由调用方(前端)弹出"跳过不通过项 / 文件全量覆盖"二选一对话框。
|
|
14
|
+
* skipFailed=true 时仅执行通过项(partial);否则不写盘。
|
|
15
|
+
*
|
|
16
|
+
* 反向操作映射:
|
|
17
|
+
* CREATE → DELETE(删除创建的文件)
|
|
18
|
+
* UPDATE → 应用反向 diff patch(精确撤销修改的部分,保留用户后续改动);
|
|
19
|
+
* 若补丁无法精确应用(上下文不匹配=冲突),报为预检失败,不静默整文件覆盖
|
|
20
|
+
* DELETE → 重新创建被删除的文件(根据 cacheKey 从 bridge 本地缓存读取)
|
|
21
|
+
*/
|
|
22
|
+
import { promises as fs } from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { Router } from 'express';
|
|
25
|
+
import * as Diff from 'diff';
|
|
26
|
+
import { createLogger } from '../utils/logger.js';
|
|
27
|
+
import { validateToken } from '../middleware/auth.js';
|
|
28
|
+
import { readBeforeContentFromCache, readDiffFromCache } from '../services/workspaceTaskChangeWatcher.js';
|
|
29
|
+
const log = createLogger('file-diff-rollback');
|
|
30
|
+
export const fileDiffRollbackRouter = Router();
|
|
31
|
+
/**
|
|
32
|
+
* 内存预检:在内存中按时间逆序模拟所有反向操作,生成执行计划。
|
|
33
|
+
* 不实际触碰文件系统。
|
|
34
|
+
*
|
|
35
|
+
* 关键:使用内存文件状态跟踪(fileContentMap),每条回滚操作基于前一条执行后的内容,
|
|
36
|
+
* 确保同一文件多次 UPDATE 时链式回滚正确(D→C→B→A)。
|
|
37
|
+
*/
|
|
38
|
+
async function previewRollback(workspacePath, operations) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
const plan = [];
|
|
41
|
+
const details = [];
|
|
42
|
+
const normalizedWorkspace = path.resolve(String(workspacePath).trim());
|
|
43
|
+
// 按 createdAt 逆序排列(最新的变更最先回滚)
|
|
44
|
+
const sortedOps = [...operations].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
45
|
+
// 内存中的文件内容跟踪:key = relativePath,value = 当前内容(null = 文件不存在)
|
|
46
|
+
// 首次访问某文件时从磁盘读取,后续操作基于内存中的最新状态
|
|
47
|
+
const fileContentMap = new Map();
|
|
48
|
+
const fileExistedMap = new Map();
|
|
49
|
+
for (const op of sortedOps) {
|
|
50
|
+
const fullPath = path.join(normalizedWorkspace, op.relativePath);
|
|
51
|
+
const relativePath = op.relativePath.replace(/\\/g, '/');
|
|
52
|
+
// 安全检查:路径不能跳出工作区
|
|
53
|
+
const resolvedPath = path.resolve(fullPath);
|
|
54
|
+
if (!resolvedPath.startsWith(normalizedWorkspace)) {
|
|
55
|
+
const reason = `Path escape detected: ${relativePath}`;
|
|
56
|
+
errors.push(reason);
|
|
57
|
+
details.push({ relativePath, originalOperation: op.originalOperation, status: 'preview_failed', reason });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// 获取当前内存中的文件状态(首次从磁盘读取)
|
|
61
|
+
if (!fileContentMap.has(relativePath)) {
|
|
62
|
+
let diskContent = null;
|
|
63
|
+
let diskExists = false;
|
|
64
|
+
try {
|
|
65
|
+
diskContent = await fs.readFile(fullPath, 'utf-8');
|
|
66
|
+
diskExists = true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
diskExists = false;
|
|
70
|
+
}
|
|
71
|
+
fileContentMap.set(relativePath, diskContent);
|
|
72
|
+
fileExistedMap.set(relativePath, diskExists);
|
|
73
|
+
}
|
|
74
|
+
switch (op.originalOperation) {
|
|
75
|
+
case 'CREATE': {
|
|
76
|
+
// 逆操作:删除文件
|
|
77
|
+
plan.push({ type: 'DELETE', fullPath, relativePath, originalOperation: 'CREATE' });
|
|
78
|
+
details.push({ relativePath, originalOperation: 'CREATE', status: 'preview_passed', action: '删除文件' });
|
|
79
|
+
// 更新内存状态:文件已删除
|
|
80
|
+
fileContentMap.set(relativePath, null);
|
|
81
|
+
fileExistedMap.set(relativePath, false);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'UPDATE': {
|
|
85
|
+
if (!op.cacheKey) {
|
|
86
|
+
const reason = `缺少 cacheKey,无法读取 diff 补丁`;
|
|
87
|
+
errors.push(`UPDATE rollback failed: ${reason} for ${relativePath}`);
|
|
88
|
+
details.push({ relativePath, originalOperation: 'UPDATE', status: 'preview_failed', reason });
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const currentContent = fileContentMap.get(relativePath) ?? null;
|
|
92
|
+
if (currentContent === null) {
|
|
93
|
+
const reason = `文件当前不存在,无法应用反向 diff 补丁`;
|
|
94
|
+
errors.push(`UPDATE rollback failed: ${reason} for ${relativePath}`);
|
|
95
|
+
details.push({ relativePath, originalOperation: 'UPDATE', status: 'preview_failed', reason });
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
// 尝试用 diff patch 精确回滚。
|
|
99
|
+
// 关键设计:applyPatch 失败时不再静默回退到 beforeContent(那会丢弃用户/其他任务
|
|
100
|
+
// 对该文件的所有改动),而是报为 preview_failed,让前端弹出
|
|
101
|
+
// "跳过不通过项 / 文件全量覆盖" 二选一对话框。
|
|
102
|
+
const patch = await readDiffFromCache(normalizedWorkspace, op.cacheKey);
|
|
103
|
+
if (patch === null) {
|
|
104
|
+
const reason = `本地缓存中找不到 diff 补丁 (cacheKey=${op.cacheKey}),无法精确回滚`;
|
|
105
|
+
errors.push(`UPDATE rollback failed: ${reason} for ${relativePath}`);
|
|
106
|
+
details.push({ relativePath, originalOperation: 'UPDATE', status: 'preview_failed', reason });
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
let rollbackContent = null;
|
|
110
|
+
try {
|
|
111
|
+
const parsed = Diff.parsePatch(patch);
|
|
112
|
+
const reversed = Diff.reversePatch(parsed);
|
|
113
|
+
const result = Diff.applyPatch(currentContent, reversed);
|
|
114
|
+
if (result !== false) {
|
|
115
|
+
rollbackContent = result;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// 反向 diff 补丁无法精确应用(上下文不匹配),
|
|
119
|
+
// 说明文件已被其他任务或用户修改了 AI 改动的同一区域,存在冲突。
|
|
120
|
+
const reason = `反向 diff 补丁无法精确应用:文件当前内容与 AI 变更后的预期上下文不匹配,可能已被其他任务或用户修改了相同区域,存在冲突`;
|
|
121
|
+
errors.push(`UPDATE rollback failed: ${reason} for ${relativePath}`);
|
|
122
|
+
details.push({ relativePath, originalOperation: 'UPDATE', status: 'preview_failed', reason });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
const reason = `反向 diff 补丁应用异常: ${err.message}`;
|
|
128
|
+
errors.push(`UPDATE rollback failed: ${reason} for ${relativePath}`);
|
|
129
|
+
details.push({ relativePath, originalOperation: 'UPDATE', status: 'preview_failed', reason });
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
plan.push({ type: 'WRITE', fullPath, relativePath, originalOperation: 'UPDATE', content: rollbackContent });
|
|
133
|
+
details.push({ relativePath, originalOperation: 'UPDATE', status: 'preview_passed', action: '应用反向 diff 补丁' });
|
|
134
|
+
// 更新内存状态:文件内容已回滚
|
|
135
|
+
fileContentMap.set(relativePath, rollbackContent);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'DELETE': {
|
|
139
|
+
if (!op.cacheKey) {
|
|
140
|
+
const reason = `缺少 cacheKey,无法读取删除前的文件内容`;
|
|
141
|
+
errors.push(`DELETE rollback failed: ${reason} for ${relativePath}`);
|
|
142
|
+
details.push({ relativePath, originalOperation: 'DELETE', status: 'preview_failed', reason });
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
const content = await readBeforeContentFromCache(normalizedWorkspace, op.cacheKey);
|
|
146
|
+
if (content === null) {
|
|
147
|
+
const reason = `本地缓存中找不到删除前的文件内容 (cacheKey=${op.cacheKey})`;
|
|
148
|
+
errors.push(`DELETE rollback failed: ${reason} for ${relativePath}`);
|
|
149
|
+
details.push({ relativePath, originalOperation: 'DELETE', status: 'preview_failed', reason });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
plan.push({ type: 'WRITE', fullPath, relativePath, originalOperation: 'DELETE', content });
|
|
153
|
+
details.push({ relativePath, originalOperation: 'DELETE', status: 'preview_passed', action: '重建文件' });
|
|
154
|
+
// 更新内存状态:文件已重建
|
|
155
|
+
fileContentMap.set(relativePath, content);
|
|
156
|
+
fileExistedMap.set(relativePath, true);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
default: {
|
|
160
|
+
const reason = `未知的操作类型 "${op.originalOperation}"`;
|
|
161
|
+
errors.push(`${reason} for ${relativePath}`);
|
|
162
|
+
details.push({ relativePath, originalOperation: op.originalOperation, status: 'preview_failed', reason });
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { ok: errors.length === 0, errors, plan, details };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 批量执行回滚计划。
|
|
171
|
+
* 返回每条操作的执行状态详情。
|
|
172
|
+
*/
|
|
173
|
+
async function executePlan(plan, previewDetails) {
|
|
174
|
+
let restored = 0;
|
|
175
|
+
let skipped = 0;
|
|
176
|
+
const errors = [];
|
|
177
|
+
// 以预检 details 为基础,更新执行阶段状态
|
|
178
|
+
const details = previewDetails.map((d) => ({ ...d }));
|
|
179
|
+
for (const item of plan) {
|
|
180
|
+
const detail = details.find((d) => d.relativePath === item.relativePath && d.status === 'preview_passed');
|
|
181
|
+
try {
|
|
182
|
+
if (item.type === 'DELETE') {
|
|
183
|
+
await fs.unlink(item.fullPath);
|
|
184
|
+
log.info(`Rollback executed: deleted created file ${item.relativePath}`);
|
|
185
|
+
if (detail) {
|
|
186
|
+
detail.status = 'success';
|
|
187
|
+
detail.action = '删除文件';
|
|
188
|
+
}
|
|
189
|
+
restored++;
|
|
190
|
+
}
|
|
191
|
+
else if (item.type === 'WRITE') {
|
|
192
|
+
await fs.mkdir(path.dirname(item.fullPath), { recursive: true });
|
|
193
|
+
await fs.writeFile(item.fullPath, item.content, 'utf-8');
|
|
194
|
+
log.info(`Rollback executed: restored file ${item.relativePath}`);
|
|
195
|
+
if (detail) {
|
|
196
|
+
detail.status = 'success';
|
|
197
|
+
detail.action = item.originalOperation === 'UPDATE' ? '应用反向 diff 补丁' : '重建文件';
|
|
198
|
+
}
|
|
199
|
+
restored++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const reason = `${err.message}`;
|
|
204
|
+
const msg = `Rollback failed for ${item.relativePath}: ${reason}`;
|
|
205
|
+
errors.push(msg);
|
|
206
|
+
log.warn(msg);
|
|
207
|
+
if (detail) {
|
|
208
|
+
detail.status = 'failed';
|
|
209
|
+
detail.reason = reason;
|
|
210
|
+
}
|
|
211
|
+
skipped++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { restored, skipped, errors, details };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 差异回滚预检接口
|
|
218
|
+
* POST /runtime/file-diff-rollback/preview
|
|
219
|
+
*
|
|
220
|
+
* 仅做内存模拟,不触碰文件系统。
|
|
221
|
+
* 返回每条操作的预检状态(preview_passed / preview_failed / skipped)及原因。
|
|
222
|
+
*/
|
|
223
|
+
fileDiffRollbackRouter.post('/file-diff-rollback/preview', validateToken, async (req, res) => {
|
|
224
|
+
const { workspacePath, operations } = req.body;
|
|
225
|
+
if (!workspacePath || !Array.isArray(operations)) {
|
|
226
|
+
res.status(400).json({ error: 'workspacePath and operations (array) are required' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const { ok, errors, plan, details } = await previewRollback(workspacePath, operations);
|
|
230
|
+
res.json({
|
|
231
|
+
ok,
|
|
232
|
+
errors,
|
|
233
|
+
plan: plan.map((p) => ({ type: p.type, relativePath: p.relativePath })),
|
|
234
|
+
totalOperations: operations.length,
|
|
235
|
+
plannedOperations: plan.length,
|
|
236
|
+
details,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
/**
|
|
240
|
+
* 执行文件差异回滚
|
|
241
|
+
* POST /runtime/file-diff-rollback/execute
|
|
242
|
+
*
|
|
243
|
+
* 第一步:内存预检(previewRollback)。
|
|
244
|
+
* 第二步:若预检全部通过,则批量原子写入;若任一预检失败,直接返回错误,不写盘。
|
|
245
|
+
*
|
|
246
|
+
* 调用方在收到 previewFailed=true 时,可提示用户选择使用文件快照进行完整覆盖。
|
|
247
|
+
* 无论成功失败,都返回每条操作的详细状态。
|
|
248
|
+
*/
|
|
249
|
+
fileDiffRollbackRouter.post('/file-diff-rollback/execute', validateToken, async (req, res) => {
|
|
250
|
+
const { workspacePath, operations, skipFailed } = req.body;
|
|
251
|
+
if (!workspacePath || !Array.isArray(operations)) {
|
|
252
|
+
res.status(400).json({ error: 'workspacePath and operations (array) are required' });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// ── 第一步:内存预检 ──
|
|
256
|
+
const preview = await previewRollback(workspacePath, operations);
|
|
257
|
+
if (!preview.ok) {
|
|
258
|
+
// 策略:仅执行通过预检的项(跳过失败项)。
|
|
259
|
+
// preview.plan 只装通过项,且链式预演中失败项不会污染 fileContentMap,
|
|
260
|
+
// 因此通过项预计算出的目标内容天然保留了失败项的改动 —— 直接执行 plan 即等价于"跳过不通过项"。
|
|
261
|
+
if (skipFailed && preview.plan.length > 0) {
|
|
262
|
+
const execResult = await executePlan(preview.plan, preview.details);
|
|
263
|
+
const failedCount = preview.details.filter((d) => d.status === 'preview_failed').length;
|
|
264
|
+
log.warn(`Diff rollback partial execute: passed=${execResult.restored}, skippedFailed=${failedCount}, ioSkipped=${execResult.skipped}`);
|
|
265
|
+
const result = {
|
|
266
|
+
ok: execResult.errors.length === 0,
|
|
267
|
+
restored: execResult.restored,
|
|
268
|
+
skipped: execResult.skipped + failedCount,
|
|
269
|
+
errors: execResult.errors,
|
|
270
|
+
previewFailed: false,
|
|
271
|
+
partial: true,
|
|
272
|
+
details: execResult.details,
|
|
273
|
+
};
|
|
274
|
+
res.json(result);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const result = {
|
|
278
|
+
ok: false,
|
|
279
|
+
restored: 0,
|
|
280
|
+
skipped: operations.length,
|
|
281
|
+
errors: preview.errors,
|
|
282
|
+
previewFailed: true,
|
|
283
|
+
details: preview.details,
|
|
284
|
+
};
|
|
285
|
+
log.warn(`Diff rollback preview failed, no files touched: errors=${preview.errors.length}`);
|
|
286
|
+
res.json(result);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// ── 第二步:批量原子执行 ──
|
|
290
|
+
const execResult = await executePlan(preview.plan, preview.details);
|
|
291
|
+
const result = {
|
|
292
|
+
ok: execResult.errors.length === 0,
|
|
293
|
+
restored: execResult.restored,
|
|
294
|
+
skipped: execResult.skipped,
|
|
295
|
+
errors: execResult.errors,
|
|
296
|
+
previewFailed: false,
|
|
297
|
+
details: execResult.details,
|
|
298
|
+
};
|
|
299
|
+
log.info(`Diff rollback completed: restored=${execResult.restored}, skipped=${execResult.skipped}, errors=${execResult.errors.length}`);
|
|
300
|
+
res.json(result);
|
|
301
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-context.d.ts","sourceRoot":"","sources":["../../src/routes/task-context.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,eAAO,MAAM,iBAAiB,4CAAW,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务上下文路由
|
|
3
|
+
*
|
|
4
|
+
* 提供调度中心设置/清除 Agent 当前活跃任务上下文的能力。
|
|
5
|
+
* 文件变更监听服务据此将变更自动归属到正确的任务。
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
import { validateToken } from '../middleware/auth.js';
|
|
10
|
+
import { setTaskContext, clearTaskContext, getTaskContext, startWatching, stopWatching, } from '../services/workspaceTaskChangeWatcher.js';
|
|
11
|
+
const log = createLogger('task-context');
|
|
12
|
+
export const taskContextRouter = Router();
|
|
13
|
+
/**
|
|
14
|
+
* 设置活跃任务上下文 + 启动工作区文件监听
|
|
15
|
+
* POST /runtime/task-context
|
|
16
|
+
*
|
|
17
|
+
* Body:
|
|
18
|
+
* {
|
|
19
|
+
* agentId: string,
|
|
20
|
+
* taskInstanceId: string,
|
|
21
|
+
* taskNodeId: string,
|
|
22
|
+
* tenantId: string,
|
|
23
|
+
* workspacePath: string,
|
|
24
|
+
* projectName: string
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
taskContextRouter.post('/task-context', validateToken, async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const { agentId, taskInstanceId, taskNodeId, tenantId, workspacePath, projectName } = req.body || {};
|
|
30
|
+
if (!agentId || !taskInstanceId || !taskNodeId || !tenantId || !workspacePath) {
|
|
31
|
+
res.status(400).json({ error: 'agentId, taskInstanceId, taskNodeId, tenantId, workspacePath are required' });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// 启动工作区监听
|
|
35
|
+
await startWatching(String(workspacePath).trim());
|
|
36
|
+
// 设置任务上下文
|
|
37
|
+
setTaskContext({
|
|
38
|
+
agentId: String(agentId).trim(),
|
|
39
|
+
taskInstanceId: String(taskInstanceId).trim(),
|
|
40
|
+
taskNodeId: String(taskNodeId).trim(),
|
|
41
|
+
tenantId: String(tenantId).trim(),
|
|
42
|
+
workspacePath: String(workspacePath).trim(),
|
|
43
|
+
projectName: String(projectName || '').trim(),
|
|
44
|
+
});
|
|
45
|
+
log.info(`Task context set via API: agent=${agentId}, taskInstance=${taskInstanceId}`);
|
|
46
|
+
res.json({ ok: true });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const err = error;
|
|
50
|
+
log.error(`Failed to set task context: ${err.message}`);
|
|
51
|
+
res.status(500).json({ error: err.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* 清除活跃任务上下文 + 停止工作区文件监听
|
|
56
|
+
* DELETE /runtime/task-context
|
|
57
|
+
*/
|
|
58
|
+
taskContextRouter.delete('/task-context', validateToken, async (_req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
await stopWatching();
|
|
61
|
+
clearTaskContext();
|
|
62
|
+
log.info('Task context cleared via API');
|
|
63
|
+
res.json({ ok: true });
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const err = error;
|
|
67
|
+
log.error(`Failed to clear task context: ${err.message}`);
|
|
68
|
+
res.status(500).json({ error: err.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* 获取当前任务上下文
|
|
73
|
+
* GET /runtime/task-context
|
|
74
|
+
*/
|
|
75
|
+
taskContextRouter.get('/task-context', validateToken, async (_req, res) => {
|
|
76
|
+
const ctx = getTaskContext();
|
|
77
|
+
res.json({ ok: true, context: ctx });
|
|
78
|
+
});
|