aws-runtime-bridge 1.9.10 → 1.9.21
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 +376 -10
- package/dist/adapter/AcodeSdkAdapter.test.js +155 -8
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +17 -1
- package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/CodexSdkAdapter.js +15 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +56 -4
- 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 +29 -2
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/routes/dashboard.d.ts.map +1 -1
- package/dist/routes/dashboard.js +749 -15
- 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 +405 -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 +17 -3
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +464 -47
- package/dist/routes/terminal.test.js +37 -9
- package/dist/services/agent-event-store.d.ts +60 -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 +8 -0
- package/dist/services/panel-auth.d.ts.map +1 -1
- package/dist/services/panel-auth.js +8 -0
- 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 +49 -3
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +115 -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/builtins/shellOutputDecoder.d.ts +25 -2
- package/package/acode/dist/builtins/shellOutputDecoder.d.ts.map +1 -1
- package/package/acode/dist/builtins/shellOutputDecoder.js +69 -5
- 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 +10 -3
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +33 -11
- 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/http-client.d.ts +11 -0
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +67 -0
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +166 -10
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +933 -41
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +150 -130
- 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 +58 -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 +246 -15
- 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 +106 -2
- 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 +123 -5
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件快照存储服务
|
|
3
|
+
*
|
|
4
|
+
* 借鉴 Claude Code fileHistory 的确定性整文件覆盖方案,替代 git stash 实现稳定快照。
|
|
5
|
+
* - 快照 = 工作区那一刻所有文件的内容哈希副本 + index.json 映射
|
|
6
|
+
* - 还原 = "先清后写"整文件覆盖,零冲突,幂等
|
|
7
|
+
* - 内容哈希去重:相同文件内容跨快照共享一份物理副本
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { promises as fs } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { createLogger } from "../utils/logger.js";
|
|
14
|
+
const log = createLogger("file-snapshot-store");
|
|
15
|
+
// ── 常量 ──────────────────────────────────────────────────────
|
|
16
|
+
/** 快照根目录,可通过环境变量覆盖 */
|
|
17
|
+
function resolveSnapshotRoot() {
|
|
18
|
+
const envRoot = String(process.env.AWS_RUNTIME_SNAPSHOT_ROOT || "").trim();
|
|
19
|
+
if (envRoot)
|
|
20
|
+
return envRoot;
|
|
21
|
+
return path.join(homedir(), ".agentswork", "snapshots");
|
|
22
|
+
}
|
|
23
|
+
/** 快照操作超时(毫秒),默认 120s */
|
|
24
|
+
function resolveSnapshotTimeoutMs() {
|
|
25
|
+
const configured = Number(process.env.AWS_RUNTIME_SNAPSHOT_TIMEOUT_MS);
|
|
26
|
+
if (Number.isFinite(configured) && configured >= 5_000)
|
|
27
|
+
return configured;
|
|
28
|
+
return 120_000;
|
|
29
|
+
}
|
|
30
|
+
/** 快照时排除的目录/文件名(精确匹配首层或任意层级) */
|
|
31
|
+
const EXCLUDED_NAMES = new Set([
|
|
32
|
+
".git",
|
|
33
|
+
"node_modules",
|
|
34
|
+
".agentswork-snapshots",
|
|
35
|
+
"__pycache__",
|
|
36
|
+
".cache",
|
|
37
|
+
".next",
|
|
38
|
+
"target",
|
|
39
|
+
"dist",
|
|
40
|
+
".idea",
|
|
41
|
+
".vscode",
|
|
42
|
+
]);
|
|
43
|
+
/** 单文件最大备份大小(默认 50MB),超过则跳过 */
|
|
44
|
+
const MAX_FILE_SIZE = Number(process.env.AWS_RUNTIME_SNAPSHOT_MAX_FILE_SIZE || 50 * 1024 * 1024);
|
|
45
|
+
/** 单快照最大文件数(防止超大工作区耗尽磁盘) */
|
|
46
|
+
const MAX_FILE_COUNT = Number(process.env.AWS_RUNTIME_SNAPSHOT_MAX_FILES || 50_000);
|
|
47
|
+
// ── 工具函数 ──────────────────────────────────────────────────
|
|
48
|
+
function sha256Short(data) {
|
|
49
|
+
return createHash("sha256").update(data).digest("hex").slice(0, 16);
|
|
50
|
+
}
|
|
51
|
+
function workspaceHash(workspacePath) {
|
|
52
|
+
return sha256Short(path.resolve(workspacePath));
|
|
53
|
+
}
|
|
54
|
+
function snapshotDirFor(snapshotRoot, wsHash, snapshotId) {
|
|
55
|
+
return path.join(snapshotRoot, wsHash, snapshotId);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 判断路径是否应被排除。
|
|
59
|
+
* 主流程:遍历路径段,命中 EXCLUDED_NAMES 则排除。
|
|
60
|
+
*/
|
|
61
|
+
function isExcludedPath(relPath) {
|
|
62
|
+
const parts = relPath.split(path.sep);
|
|
63
|
+
return parts.some((segment) => EXCLUDED_NAMES.has(segment));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 递归遍历工作区,收集所有普通文件的相对路径。
|
|
67
|
+
* 遵守排除规则,不递归排除目录。
|
|
68
|
+
*/
|
|
69
|
+
async function collectWorkspaceFiles(dirPath, basePath, result, fileCountLimit) {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const err = error;
|
|
76
|
+
if (err.code === "ENOENT" || err.code === "EACCES")
|
|
77
|
+
return;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (result.length >= fileCountLimit) {
|
|
82
|
+
log.warn(`[FileSnapshot] 文件数已达上限 ${fileCountLimit},停止遍历`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (EXCLUDED_NAMES.has(entry.name))
|
|
86
|
+
continue;
|
|
87
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
88
|
+
const relPath = path.relative(basePath, fullPath);
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
await collectWorkspaceFiles(fullPath, basePath, result, fileCountLimit);
|
|
91
|
+
}
|
|
92
|
+
else if (entry.isFile()) {
|
|
93
|
+
result.push(relPath);
|
|
94
|
+
}
|
|
95
|
+
// 忽略符号链接和其他特殊文件
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 安全删除文件,忽略 ENOENT
|
|
100
|
+
*/
|
|
101
|
+
async function safeUnlink(filePath) {
|
|
102
|
+
try {
|
|
103
|
+
await fs.unlink(filePath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const err = error;
|
|
107
|
+
if (err.code !== "ENOENT")
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 递归删除空目录
|
|
113
|
+
*/
|
|
114
|
+
async function removeEmptyDirs(dirPath, basePath) {
|
|
115
|
+
if (dirPath === basePath)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
const entries = await fs.readdir(dirPath);
|
|
119
|
+
if (entries.length === 0) {
|
|
120
|
+
await fs.rmdir(dirPath);
|
|
121
|
+
// 尝试递归清理父目录
|
|
122
|
+
await removeEmptyDirs(path.dirname(dirPath), basePath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// 忽略
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── 核心 API ──────────────────────────────────────────────────
|
|
130
|
+
/**
|
|
131
|
+
* 创建工作区文件快照。
|
|
132
|
+
*
|
|
133
|
+
* 算法(借鉴 Claude Code fileHistoryMakeSnapshot):
|
|
134
|
+
* 1. 遍历工作区收集所有文件
|
|
135
|
+
* 2. 对每个文件计算内容哈希,拷贝到快照目录(去重)
|
|
136
|
+
* 3. 写入 index.json 和 meta.json
|
|
137
|
+
*/
|
|
138
|
+
export async function createSnapshot(params) {
|
|
139
|
+
const startTime = Date.now();
|
|
140
|
+
const snapshotRoot = resolveSnapshotRoot();
|
|
141
|
+
const normalizedPath = path.resolve(String(params.workspacePath).trim());
|
|
142
|
+
const wsHash = workspaceHash(normalizedPath);
|
|
143
|
+
// 验证工作区存在
|
|
144
|
+
try {
|
|
145
|
+
const stat = await fs.stat(normalizedPath);
|
|
146
|
+
if (!stat.isDirectory()) {
|
|
147
|
+
return { ok: false, snapshotId: "", snapshotKey: "", fileCount: 0, totalBytes: 0, workspaceHash: wsHash, error: "workspace path is not a directory" };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { ok: false, snapshotId: "", snapshotKey: "", fileCount: 0, totalBytes: 0, workspaceHash: wsHash, error: "workspace path does not exist" };
|
|
152
|
+
}
|
|
153
|
+
const snapshotId = params.snapshotId?.trim() || crypto.randomUUID();
|
|
154
|
+
const snapshotName = params.snapshotName?.trim() || `snapshot-${new Date().toISOString()}`;
|
|
155
|
+
const triggerType = params.triggerType?.trim() || "MANUAL";
|
|
156
|
+
const snapshotKey = `${wsHash}/${snapshotId}`;
|
|
157
|
+
const snapshotDir = snapshotDirFor(snapshotRoot, wsHash, snapshotId);
|
|
158
|
+
const filesDir = path.join(snapshotDir, "files");
|
|
159
|
+
try {
|
|
160
|
+
// 创建目录
|
|
161
|
+
await fs.mkdir(filesDir, { recursive: true });
|
|
162
|
+
// 收集工作区文件
|
|
163
|
+
const files = [];
|
|
164
|
+
await collectWorkspaceFiles(normalizedPath, normalizedPath, files, MAX_FILE_COUNT);
|
|
165
|
+
const index = {};
|
|
166
|
+
let totalBytes = 0;
|
|
167
|
+
let fileCount = 0;
|
|
168
|
+
// 超时保护
|
|
169
|
+
const timeoutMs = resolveSnapshotTimeoutMs();
|
|
170
|
+
const deadline = Date.now() + timeoutMs;
|
|
171
|
+
for (const relPath of files) {
|
|
172
|
+
if (Date.now() > deadline) {
|
|
173
|
+
log.warn(`[FileSnapshot] 快照超时 ${timeoutMs}ms,已备份 ${fileCount}/${files.length} 个文件`);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const fullPath = path.join(normalizedPath, relPath);
|
|
177
|
+
try {
|
|
178
|
+
const stat = await fs.stat(fullPath);
|
|
179
|
+
if (!stat.isFile())
|
|
180
|
+
continue;
|
|
181
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
182
|
+
log.warn(`[FileSnapshot] 跳过大文件: ${relPath} (${(stat.size / 1024 / 1024).toFixed(1)}MB)`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const content = await fs.readFile(fullPath);
|
|
186
|
+
const contentHash = sha256Short(content);
|
|
187
|
+
const backupPath = path.join(filesDir, contentHash);
|
|
188
|
+
// 内容去重:只在物理文件不存在时拷贝
|
|
189
|
+
try {
|
|
190
|
+
await fs.access(backupPath);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
await fs.writeFile(backupPath, content);
|
|
194
|
+
}
|
|
195
|
+
index[relPath] = {
|
|
196
|
+
backupName: contentHash,
|
|
197
|
+
size: stat.size,
|
|
198
|
+
mtime: stat.mtimeMs,
|
|
199
|
+
mode: stat.mode,
|
|
200
|
+
};
|
|
201
|
+
totalBytes += stat.size;
|
|
202
|
+
fileCount++;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const err = error;
|
|
206
|
+
if (err.code === "ENOENT" || err.code === "EACCES") {
|
|
207
|
+
log.warn(`[FileSnapshot] 跳过不可读文件: ${relPath}`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 写入 index.json
|
|
214
|
+
await fs.writeFile(path.join(snapshotDir, "index.json"), JSON.stringify(index, null, 2));
|
|
215
|
+
// 写入 meta.json
|
|
216
|
+
const meta = {
|
|
217
|
+
snapshotId,
|
|
218
|
+
workspacePath: normalizedPath,
|
|
219
|
+
snapshotName,
|
|
220
|
+
triggerType,
|
|
221
|
+
createdAt: new Date().toISOString(),
|
|
222
|
+
fileCount,
|
|
223
|
+
totalBytes,
|
|
224
|
+
workspaceHash: wsHash,
|
|
225
|
+
};
|
|
226
|
+
await fs.writeFile(path.join(snapshotDir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
227
|
+
const elapsed = Date.now() - startTime;
|
|
228
|
+
log.info(`[FileSnapshot] 创建快照完成: ${snapshotKey}, ${fileCount} files, ${(totalBytes / 1024 / 1024).toFixed(1)}MB, ${elapsed}ms`);
|
|
229
|
+
return { ok: true, snapshotId, snapshotKey, fileCount, totalBytes, workspaceHash: wsHash };
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const err = error;
|
|
233
|
+
log.error(`[FileSnapshot] 创建快照失败: ${err.message}`);
|
|
234
|
+
// 清理部分写入的目录
|
|
235
|
+
try {
|
|
236
|
+
await fs.rm(snapshotDir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// 忽略清理错误
|
|
240
|
+
}
|
|
241
|
+
return { ok: false, snapshotId, snapshotKey, fileCount: 0, totalBytes: 0, workspaceHash: wsHash, error: err.message };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 还原工作区到快照状态。
|
|
246
|
+
*
|
|
247
|
+
* 算法(借鉴 Claude Code applySnapshot):
|
|
248
|
+
* 1. 可选:还原前自动创建安全网快照(preRestoreBackup)
|
|
249
|
+
* 2. 读取目标快照 index.json
|
|
250
|
+
* 3. 删除工作区中"快照后新增"的文件
|
|
251
|
+
* 4. 从备份逐文件覆盖,内容相同则跳过
|
|
252
|
+
*/
|
|
253
|
+
export async function restoreSnapshot(params) {
|
|
254
|
+
const startTime = Date.now();
|
|
255
|
+
const snapshotRoot = resolveSnapshotRoot();
|
|
256
|
+
const normalizedPath = path.resolve(String(params.workspacePath).trim());
|
|
257
|
+
const preRestoreBackup = params.preRestoreBackup !== false; // 默认 true
|
|
258
|
+
// 解析 snapshotKey
|
|
259
|
+
const [wsHash, snapshotId] = params.snapshotKey.split("/");
|
|
260
|
+
if (!wsHash || !snapshotId) {
|
|
261
|
+
return { ok: false, filesRestored: 0, filesDeleted: 0, filesSkipped: 0, error: "invalid snapshotKey format" };
|
|
262
|
+
}
|
|
263
|
+
const snapshotDir = snapshotDirFor(snapshotRoot, wsHash, snapshotId);
|
|
264
|
+
const indexPath = path.join(snapshotDir, "index.json");
|
|
265
|
+
// 验证快照存在
|
|
266
|
+
try {
|
|
267
|
+
await fs.access(indexPath);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return { ok: false, filesRestored: 0, filesDeleted: 0, filesSkipped: 0, error: `snapshot not found: ${params.snapshotKey}` };
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
// 安全网:还原前自动快照
|
|
274
|
+
let preRestoreSnapshotKey;
|
|
275
|
+
if (preRestoreBackup) {
|
|
276
|
+
try {
|
|
277
|
+
const guardResult = await createSnapshot({
|
|
278
|
+
workspacePath: normalizedPath,
|
|
279
|
+
snapshotName: `pre-restore-guard-${snapshotId}`,
|
|
280
|
+
triggerType: "AUTO_RESTORE_GUARD",
|
|
281
|
+
});
|
|
282
|
+
if (guardResult.ok) {
|
|
283
|
+
preRestoreSnapshotKey = guardResult.snapshotKey;
|
|
284
|
+
log.info(`[FileSnapshot] 还原前安全网快照: ${preRestoreSnapshotKey}`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
log.warn(`[FileSnapshot] 还原前安全网快照失败: ${guardResult.error}`);
|
|
288
|
+
// 不阻断还原
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
log.warn(`[FileSnapshot] 还原前安全网快照异常: ${error.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// 读取 index
|
|
296
|
+
const indexRaw = await fs.readFile(indexPath, "utf-8");
|
|
297
|
+
const targetIndex = JSON.parse(indexRaw);
|
|
298
|
+
// 确保工作区目录存在
|
|
299
|
+
await fs.mkdir(normalizedPath, { recursive: true });
|
|
300
|
+
// 收集当前工作区文件
|
|
301
|
+
const currentFiles = [];
|
|
302
|
+
await collectWorkspaceFiles(normalizedPath, normalizedPath, currentFiles, MAX_FILE_COUNT * 2);
|
|
303
|
+
const targetRelPaths = new Set(Object.keys(targetIndex));
|
|
304
|
+
let filesDeleted = 0;
|
|
305
|
+
let filesRestored = 0;
|
|
306
|
+
let filesSkipped = 0;
|
|
307
|
+
// 第一步:删除"快照后新增"的文件(存在于当前但不在快照中)
|
|
308
|
+
for (const relPath of currentFiles) {
|
|
309
|
+
if (!targetRelPaths.has(relPath)) {
|
|
310
|
+
const fullPath = path.join(normalizedPath, relPath);
|
|
311
|
+
await safeUnlink(fullPath);
|
|
312
|
+
filesDeleted++;
|
|
313
|
+
// 尝试清理空目录
|
|
314
|
+
const parentDir = path.dirname(fullPath);
|
|
315
|
+
if (parentDir !== normalizedPath) {
|
|
316
|
+
await removeEmptyDirs(parentDir, normalizedPath);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// 第二步:还原目标文件
|
|
321
|
+
const timeoutMs = resolveSnapshotTimeoutMs();
|
|
322
|
+
const deadline = Date.now() + timeoutMs;
|
|
323
|
+
for (const [relPath, entry] of Object.entries(targetIndex)) {
|
|
324
|
+
if (Date.now() > deadline) {
|
|
325
|
+
log.warn(`[FileSnapshot] 还原超时 ${timeoutMs}ms,已还原 ${filesRestored} 个文件`);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
const fullPath = path.join(normalizedPath, relPath);
|
|
329
|
+
const backupPath = path.join(snapshotDir, "files", entry.backupName);
|
|
330
|
+
try {
|
|
331
|
+
// 检查备份文件存在
|
|
332
|
+
try {
|
|
333
|
+
await fs.access(backupPath);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
log.warn(`[FileSnapshot] 备份文件丢失,跳过: ${relPath}`);
|
|
337
|
+
filesSkipped++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// 优化:当前文件内容哈希相同则跳过
|
|
341
|
+
try {
|
|
342
|
+
const currentContent = await fs.readFile(fullPath);
|
|
343
|
+
const currentHash = sha256Short(currentContent);
|
|
344
|
+
if (currentHash === entry.backupName) {
|
|
345
|
+
filesSkipped++;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// 文件不存在或不可读,继续还原
|
|
351
|
+
}
|
|
352
|
+
// 确保父目录存在
|
|
353
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
354
|
+
// 拷贝备份到工作区
|
|
355
|
+
await fs.copyFile(backupPath, fullPath);
|
|
356
|
+
// 恢复权限(Windows 下 chmod 为 no-op)
|
|
357
|
+
try {
|
|
358
|
+
await fs.chmod(fullPath, entry.mode);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Windows 或部分文件系统不支持 chmod,忽略
|
|
362
|
+
}
|
|
363
|
+
filesRestored++;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
log.warn(`[FileSnapshot] 还原文件失败: ${relPath} - ${error.message}`);
|
|
367
|
+
filesSkipped++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const elapsed = Date.now() - startTime;
|
|
371
|
+
log.info(`[FileSnapshot] 还原完成: ${params.snapshotKey}, restored=${filesRestored}, deleted=${filesDeleted}, skipped=${filesSkipped}, ${elapsed}ms`);
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
filesRestored,
|
|
375
|
+
filesDeleted,
|
|
376
|
+
filesSkipped,
|
|
377
|
+
preRestoreSnapshotKey,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
const err = error;
|
|
382
|
+
log.error(`[FileSnapshot] 还原快照失败: ${err.message}`);
|
|
383
|
+
return { ok: false, filesRestored: 0, filesDeleted: 0, filesSkipped: 0, error: err.message };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 列出指定工作区的所有快照
|
|
388
|
+
*/
|
|
389
|
+
export async function listSnapshots(workspacePath) {
|
|
390
|
+
const snapshotRoot = resolveSnapshotRoot();
|
|
391
|
+
const normalizedPath = path.resolve(String(workspacePath).trim());
|
|
392
|
+
const wsHash = workspaceHash(normalizedPath);
|
|
393
|
+
const wsDir = path.join(snapshotRoot, wsHash);
|
|
394
|
+
try {
|
|
395
|
+
await fs.access(wsDir);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
const entries = await fs.readdir(wsDir, { withFileTypes: true });
|
|
401
|
+
const results = [];
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
if (!entry.isDirectory())
|
|
404
|
+
continue;
|
|
405
|
+
const metaPath = path.join(wsDir, entry.name, "meta.json");
|
|
406
|
+
try {
|
|
407
|
+
const metaRaw = await fs.readFile(metaPath, "utf-8");
|
|
408
|
+
const meta = JSON.parse(metaRaw);
|
|
409
|
+
results.push({
|
|
410
|
+
snapshotId: meta.snapshotId,
|
|
411
|
+
snapshotKey: `${wsHash}/${meta.snapshotId}`,
|
|
412
|
+
snapshotName: meta.snapshotName,
|
|
413
|
+
triggerType: meta.triggerType,
|
|
414
|
+
createdAt: meta.createdAt,
|
|
415
|
+
fileCount: meta.fileCount,
|
|
416
|
+
totalBytes: meta.totalBytes,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// 跳过损坏的快照
|
|
421
|
+
log.warn(`[FileSnapshot] 跳过损坏快照: ${entry.name}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// 按创建时间倒序
|
|
425
|
+
results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
426
|
+
return results;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 删除指定快照
|
|
430
|
+
*/
|
|
431
|
+
export async function deleteSnapshot(snapshotKey) {
|
|
432
|
+
const snapshotRoot = resolveSnapshotRoot();
|
|
433
|
+
const [wsHash, snapshotId] = snapshotKey.split("/");
|
|
434
|
+
if (!wsHash || !snapshotId) {
|
|
435
|
+
return { ok: false, error: "invalid snapshotKey format" };
|
|
436
|
+
}
|
|
437
|
+
const snapshotDir = snapshotDirFor(snapshotRoot, wsHash, snapshotId);
|
|
438
|
+
try {
|
|
439
|
+
await fs.access(snapshotDir);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return { ok: false, error: `snapshot not found: ${snapshotKey}` };
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
// 读取 index 获取备份文件列表(引用计数)
|
|
446
|
+
const indexRaw = await fs.readFile(path.join(snapshotDir, "index.json"), "utf-8");
|
|
447
|
+
const index = JSON.parse(indexRaw);
|
|
448
|
+
const backupNames = new Set(Object.values(index).map((e) => e.backupName));
|
|
449
|
+
// 删除快照目录
|
|
450
|
+
await fs.rm(snapshotDir, { recursive: true, force: true });
|
|
451
|
+
// 引用计数:检查同工作区下其他快照是否还引用同一备份文件
|
|
452
|
+
const wsDir = path.join(snapshotRoot, wsHash);
|
|
453
|
+
try {
|
|
454
|
+
const otherSnapshots = await fs.readdir(wsDir, { withFileTypes: true });
|
|
455
|
+
const referencedHashes = new Set();
|
|
456
|
+
for (const other of otherSnapshots) {
|
|
457
|
+
if (!other.isDirectory())
|
|
458
|
+
continue;
|
|
459
|
+
try {
|
|
460
|
+
const otherIndexRaw = await fs.readFile(path.join(wsDir, other.name, "index.json"), "utf-8");
|
|
461
|
+
const otherIndex = JSON.parse(otherIndexRaw);
|
|
462
|
+
for (const entry of Object.values(otherIndex)) {
|
|
463
|
+
referencedHashes.add(entry.backupName);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// 忽略损坏的快照
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// 删除不再被引用的备份文件
|
|
471
|
+
const filesDir = path.join(wsDir, "files");
|
|
472
|
+
try {
|
|
473
|
+
for (const backupName of backupNames) {
|
|
474
|
+
if (!referencedHashes.has(backupName)) {
|
|
475
|
+
await safeUnlink(path.join(filesDir, backupName));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// 忽略清理错误
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// 工作区目录不存在,无需清理
|
|
485
|
+
}
|
|
486
|
+
log.info(`[FileSnapshot] 删除快照: ${snapshotKey}`);
|
|
487
|
+
return { ok: true };
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
const err = error;
|
|
491
|
+
log.error(`[FileSnapshot] 删除快照失败: ${err.message}`);
|
|
492
|
+
return { ok: false, error: err.message };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 获取快照根目录(供诊断/调试使用)
|
|
497
|
+
*/
|
|
498
|
+
export function getSnapshotRootPath() {
|
|
499
|
+
return resolveSnapshotRoot();
|
|
500
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 配置服务
|
|
3
|
+
*
|
|
4
|
+
* 管理 ACode 实例的 AI provider 配置(model/baseURL/apiKey)。
|
|
5
|
+
* 支持三层来源(api > env > file)仲裁,deep-merge 后写入
|
|
6
|
+
* per-instance acode.json 供 ACode runtime 加载。
|
|
7
|
+
*/
|
|
8
|
+
export interface AdapterInstanceAiConfig {
|
|
9
|
+
model?: string;
|
|
10
|
+
baseURL?: string;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
effort?: string;
|
|
13
|
+
}
|
|
14
|
+
interface PrepareOptions {
|
|
15
|
+
/** 来自请求体 instanceAiConfig 的覆盖(api > env > file 中的 "api" 层) */
|
|
16
|
+
apiOverrides?: AdapterInstanceAiConfig;
|
|
17
|
+
agentId: string;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
runtimeBridgeBaseUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
interface PrepareResult {
|
|
22
|
+
instanceConfig: {
|
|
23
|
+
config: AdapterInstanceAiConfig;
|
|
24
|
+
source: {
|
|
25
|
+
model: string;
|
|
26
|
+
baseURL: string;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
effort: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
perInstanceConfigPath: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 脱敏日志中的敏感字段(apiKey / secret / token / password)。
|
|
35
|
+
* 返回新对象,不修改原对象。
|
|
36
|
+
*/
|
|
37
|
+
export declare function redactForLog(obj: Record<string, unknown>): Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* 准备 ACode 实例配置。
|
|
40
|
+
*
|
|
41
|
+
* 主流程:
|
|
42
|
+
* 1. 从 apiOverrides(请求体)、env(环境变量)、file(acode.json)三层收集配置
|
|
43
|
+
* 2. 按 api > env > file 优先级仲裁每个字段
|
|
44
|
+
* 3. 将仲裁结果 deep-merge 写入 per-instance acode.json
|
|
45
|
+
* 4. 返回合并后的 config 和 per-instance 文件路径
|
|
46
|
+
*/
|
|
47
|
+
export declare function prepareAcodeInstanceConfig(options: PrepareOptions): Promise<PrepareResult>;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=instance-ai-config-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-ai-config-service.d.ts","sourceRoot":"","sources":["../../src/services/instance-ai-config-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,cAAc;IACtB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,uBAAuB,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,UAAU,aAAa;IACrB,cAAc,EAAE;QACd,MAAM,EAAE,uBAAuB,CAAC;QAChC,MAAM,EAAE;YACN,KAAK,EAAE,MAAM,CAAC;YACd,OAAO,EAAE,MAAM,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC;YACf,MAAM,EAAE,MAAM,CAAC;SAChB,CAAC;KACH,CAAC;IACF,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AASD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAelF;AA+CD;;;;;;;;GAQG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAgGxB"}
|