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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* MCP Server 实现
|
|
3
3
|
* 提供 MCP Tools 供 AI Agent 调用
|
|
4
4
|
*/
|
|
5
|
+
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -11,13 +12,25 @@ import { MCP_SERVER_NAME, MCP_SERVER_VERSION, REGISTER_MAX_RETRIES, REGISTER_RET
|
|
|
11
12
|
import { logger } from "./logger.js";
|
|
12
13
|
import { MCP_TOOLS } from "./mcp-tools.js";
|
|
13
14
|
import { MessageBuffer } from "./message-buffer.js";
|
|
15
|
+
import { ContextManager } from "./context-manager.js";
|
|
14
16
|
import { MemoryCapacityError, MemoryStore, } from "./memory-store.js";
|
|
15
17
|
import { ServerMemoryStore } from "./server-memory-store.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* poll_message 单次 HTTP 长轮询的默认服务端阻塞等待时间:240 秒(4 分钟)。
|
|
20
|
+
* 配合 callToolOnce 的 600 秒客户端超时,每次 HTTP 请求可阻塞最多 4 分钟,
|
|
21
|
+
* 大幅减少轮询次数,同时避免 30s 短轮询在服务端排队时频繁触发 120s 超时。
|
|
22
|
+
* 可通过环境变量 AWS_POLL_MESSAGE_BLOCK_WAIT_MS 覆盖。
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_POLL_MESSAGE_BLOCK_WAIT_MS = 240_000;
|
|
25
|
+
/**
|
|
26
|
+
* poll_message 底层 HTTP 请求的安全超时参考值:600 秒(10 分钟)。
|
|
27
|
+
* 用于 clamp blockWaitMs,确保服务端阻塞时间不会超过客户端单次 HTTP 调用的超时时间。
|
|
28
|
+
* 与 http-client.ts 中的 POLL_MESSAGE_ONCE_TIMEOUT_MS 对齐。
|
|
29
|
+
*/
|
|
30
|
+
const POLL_MESSAGE_LONGPOLL_HTTP_TIMEOUT_MS = 600_000;
|
|
19
31
|
const DEFAULT_MCP_HTTP_REQUEST_TIMEOUT_MS = 120_000;
|
|
20
32
|
const POLL_MESSAGE_HTTP_TIMEOUT_BUFFER_MS = 1_000;
|
|
33
|
+
const DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS = 5_000;
|
|
21
34
|
const PATH_UNSAFE_CHARACTERS = new Set([
|
|
22
35
|
"<",
|
|
23
36
|
">",
|
|
@@ -35,7 +48,92 @@ export class McpServer {
|
|
|
35
48
|
messageBuffer;
|
|
36
49
|
memoryStore;
|
|
37
50
|
roleMemoryStore;
|
|
51
|
+
sessionContextMemoryStore;
|
|
38
52
|
memoryStoresShareFile;
|
|
53
|
+
initialRoleName = "";
|
|
54
|
+
/** 上下文管理器 (v1.8.0) */
|
|
55
|
+
contextManager;
|
|
56
|
+
/**
|
|
57
|
+
* 内存级去重缓存:记录本轮已通知过的 listable memory 文件变更状态。
|
|
58
|
+
* key = memoryId,value = 已通知时刻各绑定文件的 MD5 复合哈希。
|
|
59
|
+
* 文件再次变更(MD5 变化)时仍会触发新通知。
|
|
60
|
+
* 进程重启后自动清空(届时重新检测)。
|
|
61
|
+
*/
|
|
62
|
+
notifiedFileChangeMemoryIds = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* 本地缓存 listable 记忆(task_list/recently_memory)的文件 MD5 map。
|
|
65
|
+
* 服务端(Java 后端)不持久化 file_md5_map 字段,listByType 返回的记录该字段为 undefined。
|
|
66
|
+
* 必须本地缓存才能让 checkMemoriesFileChanges 有正确的旧 MD5 做对比。
|
|
67
|
+
*/
|
|
68
|
+
listableMd5Cache = new Map();
|
|
69
|
+
/** 后台 Memory 文件变更检测定时器 (v1.9.0) */
|
|
70
|
+
memoryFileCheckInterval = null;
|
|
71
|
+
/**
|
|
72
|
+
* 收到 Profile 后同步角色存储路径。
|
|
73
|
+
* 当服务端返回的 roleName 与进程初始化时不一致时,重建 roleMemoryStore 指向正确的目录。
|
|
74
|
+
*/
|
|
75
|
+
syncRoleMemoryStore(profile) {
|
|
76
|
+
if (!profile.roleName) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const expectedRoleDir = McpServer.sanitizeRoleDirectoryName(profile.roleName);
|
|
80
|
+
if (expectedRoleDir === this.initialRoleName) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
logger.info(`[McpServer] 角色目录与初始值不一致,重建 roleMemoryStore: "${this.initialRoleName}" -> "${expectedRoleDir}"`);
|
|
84
|
+
this.roleMemoryStore = new MemoryStore(McpServer.buildRoleMemoryStorePath(expectedRoleDir));
|
|
85
|
+
this.initialRoleName = expectedRoleDir;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 规范化绑定的文件路径。
|
|
89
|
+
* 项目约定以下前缀都表示工作区根路径:
|
|
90
|
+
* ./xxx → ./xxx (已是相对路径)
|
|
91
|
+
* /xxx → ./xxx (/ 视为工作区根路径)
|
|
92
|
+
* @/xxx → ./xxx (@/ 视为工作区根路径别名)
|
|
93
|
+
* xxx → ./xxx (裸路径视为当前工作区相对)
|
|
94
|
+
*/
|
|
95
|
+
normalizeBoundFilePath(filePath) {
|
|
96
|
+
const trimmed = String(filePath || "").trim();
|
|
97
|
+
if (!trimmed)
|
|
98
|
+
return trimmed;
|
|
99
|
+
// 去掉 @/ 前缀(@ 视为工作区根路径)
|
|
100
|
+
const noAt = trimmed.startsWith("@/") ? trimmed.substring(1) : trimmed;
|
|
101
|
+
if (noAt.startsWith("./"))
|
|
102
|
+
return noAt;
|
|
103
|
+
// /xxx 和 xxx 都统一成 ./xxx
|
|
104
|
+
return `./${noAt.replace(/^\//, "")}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 规范化绑定的文件路径数组。
|
|
108
|
+
*/
|
|
109
|
+
normalizeBoundFiles(files) {
|
|
110
|
+
if (!files || !Array.isArray(files))
|
|
111
|
+
return files;
|
|
112
|
+
return files.map(f => this.normalizeBoundFilePath(String(f)));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 验证绑定文件是否存在。
|
|
116
|
+
* 主流程:规范化后的路径 -> 相对于工作区根路径解析 -> 逐文件检查是否存在 -> 汇总缺失列表并抛出错误。
|
|
117
|
+
*/
|
|
118
|
+
async validateBoundFilesExist(boundFiles) {
|
|
119
|
+
if (!boundFiles || boundFiles.length === 0)
|
|
120
|
+
return;
|
|
121
|
+
const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
|
|
122
|
+
const { access } = await import('node:fs/promises');
|
|
123
|
+
const missingFiles = [];
|
|
124
|
+
for (const filePath of boundFiles) {
|
|
125
|
+
try {
|
|
126
|
+
const resolvedPath = path.resolve(workspacePath, filePath);
|
|
127
|
+
await access(resolvedPath);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
missingFiles.push(filePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (missingFiles.length > 0) {
|
|
134
|
+
throw new Error(`绑定的文件不存在,请确认文件路径正确后再重新提交:\n${missingFiles.map(f => ` - ${f}`).join('\n')}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
39
137
|
/**
|
|
40
138
|
* 记忆写入成功后的 MCP 响应 DTO。
|
|
41
139
|
* 主流程:保留定位与版本元数据,刻意不包含 content,避免 create/update 回显大段提交内容。
|
|
@@ -46,19 +144,50 @@ export class McpServer {
|
|
|
46
144
|
uri: record.uri,
|
|
47
145
|
domain: record.domain,
|
|
48
146
|
path: record.path,
|
|
147
|
+
title: record.title,
|
|
49
148
|
memory_type: record.memory_type,
|
|
50
149
|
createdAt: record.createdAt,
|
|
51
150
|
updatedAt: record.updatedAt,
|
|
52
151
|
version: record.version,
|
|
152
|
+
keywords: record.keywords,
|
|
153
|
+
bound_files: record.bound_files,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 返回完整的 Memory 记录(包含元数据)。
|
|
158
|
+
* Memory 增强功能 (v1.6.0)
|
|
159
|
+
*/
|
|
160
|
+
toMemoryRecordWithMetadata(record) {
|
|
161
|
+
return {
|
|
162
|
+
id: record.id,
|
|
163
|
+
uri: record.uri,
|
|
164
|
+
domain: record.domain,
|
|
165
|
+
path: record.path,
|
|
166
|
+
title: record.title,
|
|
167
|
+
content: record.content,
|
|
168
|
+
memory_type: record.memory_type,
|
|
169
|
+
createdAt: record.createdAt,
|
|
170
|
+
updatedAt: record.updatedAt,
|
|
171
|
+
version: record.version,
|
|
172
|
+
// Memory 增强字段
|
|
173
|
+
keywords: record.keywords,
|
|
174
|
+
bound_files: record.bound_files,
|
|
175
|
+
file_md5_map: record.file_md5_map,
|
|
176
|
+
update_status: record.update_status,
|
|
177
|
+
updating_agent_id: record.updating_agent_id,
|
|
178
|
+
last_check_time: record.last_check_time,
|
|
53
179
|
};
|
|
54
180
|
}
|
|
55
181
|
constructor() {
|
|
56
182
|
this.agentClient = new AgentClient();
|
|
57
183
|
this.messageBuffer = new MessageBuffer();
|
|
184
|
+
this.initialRoleName = McpServer.resolveRoleMemoryDirectoryName();
|
|
58
185
|
const memoryStorePaths = McpServer.resolveMemoryStorePaths();
|
|
59
186
|
this.memoryStore = new ServerMemoryStore(this.agentClient);
|
|
60
187
|
this.roleMemoryStore = new MemoryStore(memoryStorePaths.rolePath);
|
|
188
|
+
this.sessionContextMemoryStore = new MemoryStore(path.join(path.dirname(memoryStorePaths.rolePath), "..", "session_context"));
|
|
61
189
|
this.memoryStoresShareFile = false;
|
|
190
|
+
this.contextManager = new ContextManager(this.sessionContextMemoryStore);
|
|
62
191
|
this.server = new Server({
|
|
63
192
|
name: MCP_SERVER_NAME,
|
|
64
193
|
version: MCP_SERVER_VERSION,
|
|
@@ -197,6 +326,16 @@ export class McpServer {
|
|
|
197
326
|
return await this.handleRemoveTaskListMemory(args);
|
|
198
327
|
case "archive_recently_memory":
|
|
199
328
|
return await this.handleArchiveRecentlyMemory(args);
|
|
329
|
+
// Memory 增强功能工具 (v1.6.0)
|
|
330
|
+
case "list_memories_by_file":
|
|
331
|
+
return await this.handleListMemoriesByFile(args);
|
|
332
|
+
case "abandon_memory_update":
|
|
333
|
+
return await this.handleAbandonMemoryUpdate(args);
|
|
334
|
+
// 上下文压缩工具 (v1.8.0)
|
|
335
|
+
case "get_context_stats":
|
|
336
|
+
return await this.handleGetContextStats(args);
|
|
337
|
+
case "compact_context":
|
|
338
|
+
return await this.handleCompactContext(args);
|
|
200
339
|
default:
|
|
201
340
|
throw new Error(`Unknown tool: ${name}`);
|
|
202
341
|
}
|
|
@@ -206,6 +345,7 @@ export class McpServer {
|
|
|
206
345
|
"task_list",
|
|
207
346
|
"recently_memory",
|
|
208
347
|
"memory",
|
|
348
|
+
"session_context",
|
|
209
349
|
];
|
|
210
350
|
static MEMORY_TOOL_NAMES = new Set([
|
|
211
351
|
"create_memory",
|
|
@@ -217,14 +357,33 @@ export class McpServer {
|
|
|
217
357
|
"get_memory_stats",
|
|
218
358
|
"remove_task_list_memory",
|
|
219
359
|
"archive_recently_memory",
|
|
360
|
+
// Memory 增强功能工具 (v1.6.0)
|
|
361
|
+
"list_memories_by_file",
|
|
362
|
+
"abandon_memory_update",
|
|
220
363
|
]);
|
|
221
364
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
365
|
+
* 从 startPath 向上逐级查找 .agentswork 目录。
|
|
366
|
+
* 主流程:从 startPath 开始检查是否存在 .agentswork -> 递归父目录直到根目录 -> 如果找到返回完整路径,否则在 startPath 创建。
|
|
224
367
|
*/
|
|
225
|
-
static
|
|
226
|
-
|
|
227
|
-
|
|
368
|
+
static findAgentsworkDirSync(startPath) {
|
|
369
|
+
let current = path.resolve(startPath);
|
|
370
|
+
while (true) {
|
|
371
|
+
if (fs.existsSync(path.join(current, ".agentswork"))) {
|
|
372
|
+
return path.join(current, ".agentswork");
|
|
373
|
+
}
|
|
374
|
+
const parent = path.dirname(current);
|
|
375
|
+
if (parent === current)
|
|
376
|
+
break; // 到达根目录
|
|
377
|
+
current = parent;
|
|
378
|
+
}
|
|
379
|
+
return path.join(path.resolve(startPath), ".agentswork");
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 清理角色名中的非法路径字符,返回安全的目录名。
|
|
383
|
+
*/
|
|
384
|
+
static sanitizeRoleDirectoryName(roleName) {
|
|
385
|
+
const safeRoleName = String(roleName || "")
|
|
386
|
+
.trim()
|
|
228
387
|
.split("")
|
|
229
388
|
.map((character) => PATH_UNSAFE_CHARACTERS.has(character) || character.charCodeAt(0) < 32
|
|
230
389
|
? "_"
|
|
@@ -234,6 +393,21 @@ export class McpServer {
|
|
|
234
393
|
.trim();
|
|
235
394
|
return safeRoleName || "default-role";
|
|
236
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* 解析角色级记忆目录名。
|
|
398
|
+
* 主流程:优先使用 AWS_ROLE_NAME,清理路径分隔符等非法字符,避免不同角色的长期 memory 混写。
|
|
399
|
+
*/
|
|
400
|
+
static resolveRoleMemoryDirectoryName() {
|
|
401
|
+
return McpServer.sanitizeRoleDirectoryName(process.env.AWS_ROLE_NAME || "");
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 根据 roleName 构建永久记忆存储路径。
|
|
405
|
+
*/
|
|
406
|
+
static buildRoleMemoryStorePath(roleName) {
|
|
407
|
+
const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
|
|
408
|
+
const agentsworkDir = McpServer.findAgentsworkDirSync(workspacePath);
|
|
409
|
+
return path.join(agentsworkDir, "memory", "memory", McpServer.sanitizeRoleDirectoryName(roleName));
|
|
410
|
+
}
|
|
237
411
|
/**
|
|
238
412
|
* 解析记忆存储路径。
|
|
239
413
|
* 主流程:task_list/recently_memory 走 server 数据库;长期 memory 默认写入项目 .agentswork/memory/memory/<roleName>/<domain>.json。
|
|
@@ -244,10 +418,11 @@ export class McpServer {
|
|
|
244
418
|
return { instancePath: configuredPath, rolePath: configuredPath };
|
|
245
419
|
}
|
|
246
420
|
const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
|
|
421
|
+
const agentsworkDir = McpServer.findAgentsworkDirSync(workspacePath);
|
|
247
422
|
const instancePath = String(process.env.AWS_INSTANCE_MEMORY_STORE_PATH || "").trim() ||
|
|
248
|
-
path.join(
|
|
423
|
+
path.join(agentsworkDir, "memory", "server-managed");
|
|
249
424
|
const rolePath = String(process.env.AWS_ROLE_MEMORY_STORE_PATH || "").trim() ||
|
|
250
|
-
path.join(
|
|
425
|
+
path.join(agentsworkDir, "memory", "memory", McpServer.resolveRoleMemoryDirectoryName());
|
|
251
426
|
return { instancePath, rolePath };
|
|
252
427
|
}
|
|
253
428
|
getRequiredString(args, key) {
|
|
@@ -265,6 +440,10 @@ export class McpServer {
|
|
|
265
440
|
const value = args[key];
|
|
266
441
|
return typeof value === "number" ? value : undefined;
|
|
267
442
|
}
|
|
443
|
+
getOptionalStringArray(args, key) {
|
|
444
|
+
const value = args[key];
|
|
445
|
+
return Array.isArray(value) ? value.map((v) => String(v)) : undefined;
|
|
446
|
+
}
|
|
268
447
|
getOptionalMemoryType(args) {
|
|
269
448
|
const value = args.memory_type;
|
|
270
449
|
if (value === undefined) {
|
|
@@ -353,6 +532,7 @@ export class McpServer {
|
|
|
353
532
|
...roleStats.by_type.memory,
|
|
354
533
|
count: roleStats.by_type.memory.count,
|
|
355
534
|
},
|
|
535
|
+
session_context: instanceStats.by_type.session_context,
|
|
356
536
|
},
|
|
357
537
|
};
|
|
358
538
|
}
|
|
@@ -372,6 +552,42 @@ export class McpServer {
|
|
|
372
552
|
logger.warn("[McpServer] 上报合并 memory 统计失败,已保留本地记忆写入结果:", error);
|
|
373
553
|
}
|
|
374
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* 同步本地永久记忆到服务端。
|
|
557
|
+
* 主流程:读取本地 roleMemoryStore 中所有 memory 类型记录 -> 逐条写入服务端 memoryStore -> 已存在的记录静默跳过。
|
|
558
|
+
*/
|
|
559
|
+
async syncLocalPermanentMemoryToServer() {
|
|
560
|
+
try {
|
|
561
|
+
const memories = await this.roleMemoryStore.listByType("memory");
|
|
562
|
+
if (memories.length === 0)
|
|
563
|
+
return;
|
|
564
|
+
let syncedCount = 0;
|
|
565
|
+
for (const memory of memories) {
|
|
566
|
+
try {
|
|
567
|
+
await this.memoryStore.createMemory({
|
|
568
|
+
memory_type: "memory",
|
|
569
|
+
uri: memory.uri,
|
|
570
|
+
title: memory.title,
|
|
571
|
+
content: memory.content,
|
|
572
|
+
keywords: memory.keywords,
|
|
573
|
+
bound_files: memory.bound_files,
|
|
574
|
+
});
|
|
575
|
+
syncedCount++;
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
// If already exists on server, skip
|
|
579
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
580
|
+
if (!errMsg.includes("already exists")) {
|
|
581
|
+
logger.warn(`[McpServer] Failed to sync permanent memory ${memory.uri}:`, e);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
logger.info(`[McpServer] Synced ${syncedCount}/${memories.length} permanent memories to server`);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
logger.error("[McpServer] Failed to sync permanent memories to server:", error);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
375
591
|
/**
|
|
376
592
|
* 生成安全的工具状态上报参数。
|
|
377
593
|
* 主流程:memory 工具只上报元数据,避免 content/query/old_string/new_string/append 离开本地记忆边界。
|
|
@@ -396,6 +612,7 @@ export class McpServer {
|
|
|
396
612
|
if (name === "search_memory") {
|
|
397
613
|
sanitized.queryProvided =
|
|
398
614
|
typeof args.query === "string" && args.query.length > 0;
|
|
615
|
+
sanitized.keyword_mode = args.keyword_mode;
|
|
399
616
|
}
|
|
400
617
|
return sanitized;
|
|
401
618
|
}
|
|
@@ -404,13 +621,42 @@ export class McpServer {
|
|
|
404
621
|
* 主流程:校验必填字段 -> 交给 MemoryStore 规范化 URI 和类型 -> 返回不含 content 的写入结果。
|
|
405
622
|
*/
|
|
406
623
|
async handleCreateMemory(args) {
|
|
624
|
+
const boundFiles = this.normalizeBoundFiles(this.getOptionalStringArray(args, "bound_files"));
|
|
625
|
+
// 验证绑定文件是否存在,避免创建无有效文件绑定的记忆
|
|
626
|
+
await this.validateBoundFilesExist(boundFiles);
|
|
627
|
+
// 创建时即计算绑定文件的 MD5 基线,避免首次文件变更检测产生假阳性
|
|
628
|
+
let fileMd5Map;
|
|
629
|
+
let lastCheckTime;
|
|
630
|
+
if (boundFiles && boundFiles.length > 0) {
|
|
631
|
+
fileMd5Map = {};
|
|
632
|
+
for (const f of boundFiles) {
|
|
633
|
+
const md5 = await this.calculateFileMd5(f);
|
|
634
|
+
if (md5)
|
|
635
|
+
fileMd5Map[f] = md5;
|
|
636
|
+
}
|
|
637
|
+
lastCheckTime = new Date().toISOString();
|
|
638
|
+
}
|
|
407
639
|
const input = {
|
|
408
640
|
memory_type: this.parseMemoryType(this.getRequiredString(args, "memory_type")),
|
|
409
641
|
uri: this.getRequiredString(args, "uri"),
|
|
642
|
+
title: this.getOptionalString(args, "title"),
|
|
410
643
|
content: this.getRequiredString(args, "content"),
|
|
644
|
+
keywords: this.getOptionalStringArray(args, "keywords"),
|
|
645
|
+
bound_files: boundFiles,
|
|
646
|
+
file_md5_map: fileMd5Map,
|
|
647
|
+
last_check_time: lastCheckTime,
|
|
411
648
|
};
|
|
412
649
|
const created = await this.getMemoryStoreForType(input.memory_type).createMemory(input);
|
|
413
650
|
await this.reportCombinedMemoryStats();
|
|
651
|
+
// Dual-write permanent memory to server
|
|
652
|
+
if (input.memory_type === "memory") {
|
|
653
|
+
try {
|
|
654
|
+
await this.memoryStore.createMemory(input).catch(() => { });
|
|
655
|
+
}
|
|
656
|
+
catch (e) {
|
|
657
|
+
logger.warn("[McpServer] Failed to sync permanent memory to server:", e);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
414
660
|
return this.toMemoryWriteResult(created);
|
|
415
661
|
}
|
|
416
662
|
/**
|
|
@@ -444,16 +690,59 @@ export class McpServer {
|
|
|
444
690
|
const input = {
|
|
445
691
|
uri: this.getRequiredString(args, "uri"),
|
|
446
692
|
memory_type: this.getOptionalMemoryType(args),
|
|
693
|
+
title: this.getOptionalString(args, "title"),
|
|
447
694
|
old_string: this.getOptionalString(args, "old_string"),
|
|
448
695
|
new_string: this.getOptionalString(args, "new_string"),
|
|
449
696
|
append,
|
|
697
|
+
keywords: this.getOptionalStringArray(args, "keywords"),
|
|
698
|
+
bound_files: this.normalizeBoundFiles(this.getOptionalStringArray(args, "bound_files")),
|
|
450
699
|
};
|
|
451
700
|
const target = await this.findMemoryStoreForMutation(input);
|
|
701
|
+
// 读取当前记忆以获取 bound_files,用于 MD5 重算
|
|
702
|
+
const existingForMd5 = await target.store.readMemory({
|
|
703
|
+
uri: input.uri,
|
|
704
|
+
memory_type: target.memory_type ?? input.memory_type,
|
|
705
|
+
});
|
|
706
|
+
// 当显式传入新 bound_files 时验证文件是否存在
|
|
707
|
+
if (input.bound_files !== undefined) {
|
|
708
|
+
await this.validateBoundFilesExist(input.bound_files);
|
|
709
|
+
}
|
|
710
|
+
const effectiveBoundFiles = input.bound_files ?? existingForMd5?.bound_files;
|
|
711
|
+
// 重新计算绑定文件的 MD5 map,确保更新后 MD5 与文件实际内容一致
|
|
712
|
+
let fileMd5Map;
|
|
713
|
+
let lastCheckTime;
|
|
714
|
+
if (effectiveBoundFiles && effectiveBoundFiles.length > 0) {
|
|
715
|
+
fileMd5Map = {};
|
|
716
|
+
for (const f of effectiveBoundFiles) {
|
|
717
|
+
const md5 = await this.calculateFileMd5(f);
|
|
718
|
+
if (md5)
|
|
719
|
+
fileMd5Map[f] = md5;
|
|
720
|
+
}
|
|
721
|
+
lastCheckTime = new Date().toISOString();
|
|
722
|
+
}
|
|
452
723
|
const updated = await target.store.updateMemory({
|
|
453
724
|
...input,
|
|
454
725
|
memory_type: target.memory_type ?? input.memory_type,
|
|
726
|
+
file_md5_map: fileMd5Map,
|
|
727
|
+
last_check_time: lastCheckTime,
|
|
455
728
|
});
|
|
456
729
|
await this.reportCombinedMemoryStats();
|
|
730
|
+
// Dual-write permanent memory update to server(同步 MD5 map 避免服务端重复检测)
|
|
731
|
+
if (target.memory_type === "memory") {
|
|
732
|
+
try {
|
|
733
|
+
await this.memoryStore
|
|
734
|
+
.updateMemory({
|
|
735
|
+
...input,
|
|
736
|
+
memory_type: "memory",
|
|
737
|
+
file_md5_map: fileMd5Map,
|
|
738
|
+
last_check_time: lastCheckTime,
|
|
739
|
+
})
|
|
740
|
+
.catch(() => { });
|
|
741
|
+
}
|
|
742
|
+
catch (e) {
|
|
743
|
+
logger.warn("[McpServer] Failed to sync permanent memory update to server:", e);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
457
746
|
return this.toMemoryWriteResult(updated);
|
|
458
747
|
}
|
|
459
748
|
/**
|
|
@@ -472,19 +761,60 @@ export class McpServer {
|
|
|
472
761
|
});
|
|
473
762
|
if (deleted) {
|
|
474
763
|
await this.reportCombinedMemoryStats();
|
|
764
|
+
// Dual-write permanent memory delete to server
|
|
765
|
+
if (target.memory_type === "memory") {
|
|
766
|
+
try {
|
|
767
|
+
await this.memoryStore
|
|
768
|
+
.deleteMemory({ uri: input.uri, memory_type: "memory" })
|
|
769
|
+
.catch(() => { });
|
|
770
|
+
}
|
|
771
|
+
catch (e) {
|
|
772
|
+
logger.warn("[McpServer] Failed to sync permanent memory delete to server:", e);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
475
775
|
}
|
|
476
776
|
return { deleted };
|
|
477
777
|
}
|
|
478
778
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
779
|
+
* 搜索本地记忆与子 Agent 记忆(父子 Agent 可见性)。
|
|
780
|
+
* 主流程:解析 scope -> own(默认)仅搜自己 -> children仅搜子Agent的file-scoped记忆 -> all合并。
|
|
781
|
+
* 子 Agent 判定规则:同项目且工作区路径为当前路径子目录。
|
|
481
782
|
*/
|
|
482
783
|
async handleSearchMemory(args) {
|
|
784
|
+
const rawScope = this.getOptionalString(args, "scope");
|
|
785
|
+
const scope = rawScope === "children" || rawScope === "all" ? rawScope : "own";
|
|
786
|
+
const rawKeywordMode = this.getOptionalString(args, "keyword_mode");
|
|
787
|
+
const keywordMode = rawKeywordMode === "all" ? "all" : rawKeywordMode === "any" ? "any" : undefined;
|
|
483
788
|
const input = {
|
|
484
789
|
query: this.getRequiredString(args, "query"),
|
|
485
790
|
limit: this.getOptionalNumber(args, "limit"),
|
|
486
791
|
memory_type: this.getOptionalMemoryType(args),
|
|
792
|
+
keywords: this.getOptionalStringArray(args, "keywords"),
|
|
793
|
+
keyword_mode: keywordMode,
|
|
794
|
+
scope,
|
|
487
795
|
};
|
|
796
|
+
// scope='children': 仅搜索子 Agent 的 file-scoped 记忆(通过 server 代理)
|
|
797
|
+
if (scope === "children") {
|
|
798
|
+
return await this.memoryStore.searchMemory(input);
|
|
799
|
+
}
|
|
800
|
+
// 搜索自己的记忆(scope='own' 默认行为)
|
|
801
|
+
const ownResult = await this.searchOwnMemories(input);
|
|
802
|
+
// scope='own': 仅返回自己的结果
|
|
803
|
+
if (scope === "own") {
|
|
804
|
+
return ownResult;
|
|
805
|
+
}
|
|
806
|
+
// scope='all': 合并自己的记忆与子 Agent 的 file-scoped 记忆
|
|
807
|
+
const childResult = await this.memoryStore.searchMemory({
|
|
808
|
+
...input,
|
|
809
|
+
scope: "children",
|
|
810
|
+
});
|
|
811
|
+
return this.mergeSearchResults(ownResult, childResult, input.limit);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* 搜索自己工作区的所有记忆。
|
|
815
|
+
* 具体逻辑:按 memory_type 分流 -> 无类型时合并 roleMemoryStore(永久)与 memoryStore(task_list/recently)。
|
|
816
|
+
*/
|
|
817
|
+
async searchOwnMemories(input) {
|
|
488
818
|
if (input.memory_type) {
|
|
489
819
|
return await this.getMemoryStoreForType(input.memory_type).searchMemory(input);
|
|
490
820
|
}
|
|
@@ -510,6 +840,26 @@ export class McpServer {
|
|
|
510
840
|
.slice(0, limit);
|
|
511
841
|
return { query: roleResult.query || instanceResult.query, results };
|
|
512
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* 合并两组搜索结果,按 uri 去重、更新时间排序。
|
|
845
|
+
* 具体逻辑:own 结果优先 -> 补充 child 结果中 uri 不重复的 -> 排序 -> 截断到 limit。
|
|
846
|
+
*/
|
|
847
|
+
mergeSearchResults(own, child, limit) {
|
|
848
|
+
const seen = new Set();
|
|
849
|
+
const maxResults = typeof limit === "number" && limit > 0 ? limit : 10;
|
|
850
|
+
const merged = [...own.results, ...child.results]
|
|
851
|
+
.filter((record) => {
|
|
852
|
+
const key = record.uri;
|
|
853
|
+
if (seen.has(key)) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
seen.add(key);
|
|
857
|
+
return true;
|
|
858
|
+
})
|
|
859
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
860
|
+
.slice(0, maxResults);
|
|
861
|
+
return { query: own.query || child.query, results: merged };
|
|
862
|
+
}
|
|
513
863
|
/**
|
|
514
864
|
* 列出短期类型记忆。
|
|
515
865
|
* 主流程:只允许 task_list/recently_memory,由 MemoryStore 阻止长期 memory 的全量列出。
|
|
@@ -566,7 +916,10 @@ export class McpServer {
|
|
|
566
916
|
const archived = await this.roleMemoryStore.createMemory({
|
|
567
917
|
memory_type: "memory",
|
|
568
918
|
uri: record.uri,
|
|
919
|
+
title: record.title,
|
|
569
920
|
content: record.content,
|
|
921
|
+
keywords: record.keywords,
|
|
922
|
+
bound_files: record.bound_files,
|
|
570
923
|
});
|
|
571
924
|
await this.memoryStore.deleteMemory({
|
|
572
925
|
uri: record.uri,
|
|
@@ -575,12 +928,231 @@ export class McpServer {
|
|
|
575
928
|
await this.reportCombinedMemoryStats();
|
|
576
929
|
return archived;
|
|
577
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* 按文件查询绑定了该文件的 Memory
|
|
933
|
+
* Memory 增强功能 (v1.6.0)
|
|
934
|
+
*/
|
|
935
|
+
/**
|
|
936
|
+
* 按文件路径查询 Memory(支持 scope 跨 Agent 搜索)。
|
|
937
|
+
* 主流程:解析scope -> own(默认)仅自己 -> children通过server按path_prefix匹配子Agent -> all合并。
|
|
938
|
+
* 对于永久记忆(type=memory)同时查询本地roleMemoryStore作为补充。
|
|
939
|
+
*/
|
|
940
|
+
async handleListMemoriesByFile(args) {
|
|
941
|
+
const rawPath = this.getRequiredString(args, "file_path");
|
|
942
|
+
const normalizedPath = this.normalizeBoundFilePath(rawPath);
|
|
943
|
+
const memoryType = this.getOptionalMemoryType(args);
|
|
944
|
+
const rawScope = this.getOptionalString(args, "scope");
|
|
945
|
+
const scope = rawScope === "children" || rawScope === "all" ? rawScope : "own";
|
|
946
|
+
// scope='children': 仅通过 server 搜索子 Agent(带 path_prefix 自动匹配)
|
|
947
|
+
if (scope === "children") {
|
|
948
|
+
const childResult = await this.memoryStore.listByFile(normalizedPath, {
|
|
949
|
+
memory_type: memoryType,
|
|
950
|
+
scope: "children",
|
|
951
|
+
});
|
|
952
|
+
return {
|
|
953
|
+
file_path: normalizedPath,
|
|
954
|
+
count: childResult.length,
|
|
955
|
+
memories: childResult.map(m => this.toMemoryRecordWithMetadata(m)),
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
// 搜索自己的记忆
|
|
959
|
+
const ownMemories = await this.collectOwnMemoriesByFile(normalizedPath, memoryType);
|
|
960
|
+
if (scope === "own") {
|
|
961
|
+
return {
|
|
962
|
+
file_path: normalizedPath,
|
|
963
|
+
count: ownMemories.length,
|
|
964
|
+
memories: ownMemories.map(m => this.toMemoryRecordWithMetadata(m)),
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
// scope='all': 合并自己的 + 子 Agent 的
|
|
968
|
+
const childResult = await this.memoryStore.listByFile(normalizedPath, {
|
|
969
|
+
memory_type: memoryType,
|
|
970
|
+
scope: "children",
|
|
971
|
+
});
|
|
972
|
+
const seen = new Set(ownMemories.map(m => m.id));
|
|
973
|
+
const merged = [...ownMemories];
|
|
974
|
+
for (const m of childResult) {
|
|
975
|
+
if (!seen.has(m.id)) {
|
|
976
|
+
merged.push(m);
|
|
977
|
+
seen.add(m.id);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
merged.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
981
|
+
return {
|
|
982
|
+
file_path: normalizedPath,
|
|
983
|
+
count: merged.length,
|
|
984
|
+
memories: merged.map(m => this.toMemoryRecordWithMetadata(m)),
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* 按文件路径搜索本 Agent 的记忆。
|
|
989
|
+
* 同时查询临时记忆(server)和永久记忆(local roleMemoryStore)。
|
|
990
|
+
*/
|
|
991
|
+
async collectOwnMemoriesByFile(filePath, memoryType) {
|
|
992
|
+
const serverResult = await this.memoryStore.listByFile(filePath, {
|
|
993
|
+
memory_type: memoryType,
|
|
994
|
+
});
|
|
995
|
+
let localResult = [];
|
|
996
|
+
// 补充查询本地永久记忆(server 可能未同步)
|
|
997
|
+
if (!memoryType || memoryType === "memory") {
|
|
998
|
+
localResult = await this.roleMemoryStore.listByFile(filePath, "memory");
|
|
999
|
+
}
|
|
1000
|
+
// 去重合并
|
|
1001
|
+
const seen = new Set(serverResult.map(m => `${m.memory_type}:${m.uri}`));
|
|
1002
|
+
const merged = [...serverResult];
|
|
1003
|
+
for (const m of localResult) {
|
|
1004
|
+
const key = `${m.memory_type}:${m.uri}`;
|
|
1005
|
+
if (!seen.has(key)) {
|
|
1006
|
+
merged.push(m);
|
|
1007
|
+
seen.add(key);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return merged;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* 放弃 Memory 更新状态
|
|
1014
|
+
* Memory 增强功能 (v1.6.0)
|
|
1015
|
+
*/
|
|
1016
|
+
async handleAbandonMemoryUpdate(args) {
|
|
1017
|
+
const uri = this.getRequiredString(args, "uri");
|
|
1018
|
+
const memoryType = this.getOptionalMemoryType(args);
|
|
1019
|
+
const record = await this.memoryStore.readMemory({ uri, memory_type: memoryType });
|
|
1020
|
+
if (!record) {
|
|
1021
|
+
throw new Error(`Memory not found: ${uri}`);
|
|
1022
|
+
}
|
|
1023
|
+
// 清除 updating 状态
|
|
1024
|
+
await this.memoryStore.updateMemory({
|
|
1025
|
+
uri: record.uri,
|
|
1026
|
+
memory_type: record.memory_type,
|
|
1027
|
+
});
|
|
1028
|
+
return {
|
|
1029
|
+
success: true,
|
|
1030
|
+
uri: record.uri,
|
|
1031
|
+
message: "Memory 更新状态已清除",
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
// ============ 上下文压缩工具处理 (v1.8.0) ============
|
|
1035
|
+
/**
|
|
1036
|
+
* 获取当前会话的上下文统计。
|
|
1037
|
+
* 主流程:从 profile 获取模型名称 -> 通过模型名查找上下文窗口 -> 调用 ContextManager 获取统计 -> 返回。
|
|
1038
|
+
*/
|
|
1039
|
+
async handleGetContextStats(args) {
|
|
1040
|
+
const profile = await this.agentClient.getProfile().catch(() => null);
|
|
1041
|
+
const sessionId = args.sessionId ?? profile?.runtimeSessionId ?? "default";
|
|
1042
|
+
const modelName = profile?.prompt ? this.guessModelName(profile) : "unknown";
|
|
1043
|
+
const modelInfo = {
|
|
1044
|
+
provider: modelName.split("/")[0] || "unknown",
|
|
1045
|
+
model: modelName,
|
|
1046
|
+
contextLimit: 0, // ContextManager 内部会通过 lookupModelLimits 查找
|
|
1047
|
+
};
|
|
1048
|
+
const stats = await this.contextManager.getStats(sessionId, modelInfo);
|
|
1049
|
+
// 如果 contextLimit 仍为 0,用查找表的默认值
|
|
1050
|
+
if (stats.contextLimit === 0) {
|
|
1051
|
+
const limits = this.contextManager.lookupModelLimits(modelName);
|
|
1052
|
+
return { ...stats, contextLimit: limits.contextLimit, remaining: limits.contextLimit - stats.totalUsed, usagePercent: Math.round((stats.totalUsed / limits.contextLimit) * 100) };
|
|
1053
|
+
}
|
|
1054
|
+
return stats;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* 从 profile 猜测当前使用的模型名称。
|
|
1058
|
+
* 主流程:读取 prompt 中的模型信息 -> 回退到 runtimeCliCommand 中的提示 -> 最终回退到环境变量或默认值。
|
|
1059
|
+
*/
|
|
1060
|
+
guessModelName(profile) {
|
|
1061
|
+
// 尝试从 prompt 中提取模型名(prompt 可能包含 @model 标记)
|
|
1062
|
+
const promptMatch = profile.prompt?.match(/@model\s+(\S+)/i);
|
|
1063
|
+
if (promptMatch?.[1])
|
|
1064
|
+
return promptMatch[1];
|
|
1065
|
+
// 回退到 CLI 命令中的线索
|
|
1066
|
+
if (profile.runtimeMode)
|
|
1067
|
+
return profile.runtimeMode;
|
|
1068
|
+
// 默认值
|
|
1069
|
+
return process.env.AWS_DEFAULT_MODEL || "gpt-4o";
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* 处理上下文压缩请求。
|
|
1073
|
+
* 主流程:解析策略 -> 获取消息历史 -> 执行压缩 -> 保存结果 -> 返回。
|
|
1074
|
+
*/
|
|
1075
|
+
async handleCompactContext(args) {
|
|
1076
|
+
const profile = await this.agentClient.getProfile().catch(() => null);
|
|
1077
|
+
const sessionId = profile?.runtimeSessionId ?? "default";
|
|
1078
|
+
const strategy = args.strategy ?? "auto";
|
|
1079
|
+
const keepLastTurns = args.keepLastTurns ?? 3;
|
|
1080
|
+
// 获取消息历史
|
|
1081
|
+
const messages = await this.fetchRecentMessages();
|
|
1082
|
+
switch (strategy) {
|
|
1083
|
+
case "prune": {
|
|
1084
|
+
const result = this.contextManager.prune(messages, keepLastTurns);
|
|
1085
|
+
await this.contextManager.saveCompactionResult(sessionId, result);
|
|
1086
|
+
return result;
|
|
1087
|
+
}
|
|
1088
|
+
case "summarize": {
|
|
1089
|
+
// summarize 需要 Agent 自己调用 LLM 来完成
|
|
1090
|
+
// 这里先执行 prune 释放部分空间,然后返回需要 Agent 进行 summarize
|
|
1091
|
+
const pruneResult = this.contextManager.prune(messages, keepLastTurns);
|
|
1092
|
+
await this.contextManager.saveCompactionResult(sessionId, pruneResult);
|
|
1093
|
+
return {
|
|
1094
|
+
...pruneResult,
|
|
1095
|
+
strategy: "summarize",
|
|
1096
|
+
summary: await this.contextManager.getCompactionSummary(sessionId) ?? undefined,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
case "auto":
|
|
1100
|
+
default: {
|
|
1101
|
+
// 先尝试 prune
|
|
1102
|
+
const pruneResult = this.contextManager.prune(messages, keepLastTurns);
|
|
1103
|
+
if (pruneResult.freedTokens >= 20_000) {
|
|
1104
|
+
await this.contextManager.saveCompactionResult(sessionId, pruneResult);
|
|
1105
|
+
return pruneResult;
|
|
1106
|
+
}
|
|
1107
|
+
// prune 释放不够,回退到 summarize
|
|
1108
|
+
return {
|
|
1109
|
+
...pruneResult,
|
|
1110
|
+
strategy: "summarize",
|
|
1111
|
+
summary: await this.contextManager.getCompactionSummary(sessionId) ?? undefined,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* 获取最近的消息历史用于压缩。
|
|
1118
|
+
* 主流程:获取未读消息 + 历史消息 -> 合并排序 -> 返回。
|
|
1119
|
+
*/
|
|
1120
|
+
async fetchRecentMessages() {
|
|
1121
|
+
const [dmHistory, groupHistory] = await Promise.all([
|
|
1122
|
+
this.agentClient.getDmHistory(undefined, 200).catch(() => ({ messages: [], count: 0 })),
|
|
1123
|
+
this.agentClient.getGroupHistory(200).catch(() => ({ messages: [], count: 0 })),
|
|
1124
|
+
]);
|
|
1125
|
+
const messages = [
|
|
1126
|
+
...dmHistory.messages,
|
|
1127
|
+
...groupHistory.messages,
|
|
1128
|
+
];
|
|
1129
|
+
messages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
1130
|
+
return messages;
|
|
1131
|
+
}
|
|
578
1132
|
async handleSendGroup(args) {
|
|
579
1133
|
if (!args.content || typeof args.content !== "string") {
|
|
580
1134
|
throw new Error("content is required");
|
|
581
1135
|
}
|
|
1136
|
+
// 记录 token 消耗 (v1.8.0)
|
|
1137
|
+
this.recordMessageTokenUsage(args.content).catch((err) => logger.warn("[McpServer] Failed to record token usage", err));
|
|
582
1138
|
return await this.agentClient.sendGroupMessage(args.content, args.projectName, args.roomId);
|
|
583
1139
|
}
|
|
1140
|
+
/**
|
|
1141
|
+
* 记录消息发送产生的 token 消耗。
|
|
1142
|
+
* 主流程:估算内容 token -> 获取会话 ID -> 记录到 ContextManager。
|
|
1143
|
+
*/
|
|
1144
|
+
async recordMessageTokenUsage(content) {
|
|
1145
|
+
try {
|
|
1146
|
+
const profile = await this.agentClient.getProfile().catch(() => null);
|
|
1147
|
+
if (!profile?.runtimeSessionId)
|
|
1148
|
+
return;
|
|
1149
|
+
const estimatedTokens = this.contextManager.estimateTokens(content);
|
|
1150
|
+
await this.contextManager.recordUsage(profile.runtimeSessionId, { input: 0, output: estimatedTokens });
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
// 非关键功能,静默失败
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
584
1156
|
async handleHireColleague(args) {
|
|
585
1157
|
if (!args.workspacePath || typeof args.workspacePath !== "string") {
|
|
586
1158
|
throw new Error("workspacePath is required");
|
|
@@ -605,6 +1177,8 @@ export class McpServer {
|
|
|
605
1177
|
if (!args.content || typeof args.content !== "string") {
|
|
606
1178
|
throw new Error("content is required");
|
|
607
1179
|
}
|
|
1180
|
+
// 记录 token 消耗 (v1.8.0)
|
|
1181
|
+
this.recordMessageTokenUsage(args.content).catch((err) => logger.warn("[McpServer] Failed to record token usage", err));
|
|
608
1182
|
return await this.agentClient.sendDirectMessage(args.targetId, args.content, args.waitReply, args.replyToCallId, args.timeoutMs);
|
|
609
1183
|
}
|
|
610
1184
|
async handleCreateTaskInstanceComment(args) {
|
|
@@ -616,10 +1190,28 @@ export class McpServer {
|
|
|
616
1190
|
});
|
|
617
1191
|
}
|
|
618
1192
|
async handlePollMessage() {
|
|
1193
|
+
// 首次非阻塞预取:使用 get_dm_messages 而非 poll_message,
|
|
1194
|
+
// 避免进入服务端 MVC 异步线程池排队(poll_message 始终走异步池),
|
|
1195
|
+
// 确保快速返回,不影响后续 waitForMessagesWithPoll 的 resolver 注册。
|
|
1196
|
+
// 流程线任务由后续的 waitForMessagesWithPoll → 后台长轮询负责补拉。
|
|
619
1197
|
await this.hydrateUnreadMessagesFromServer({
|
|
620
1198
|
blockIfEmpty: false,
|
|
621
|
-
includePipelineTasks:
|
|
1199
|
+
includePipelineTasks: false,
|
|
622
1200
|
});
|
|
1201
|
+
// Memory 文件变更检测通知(优先级最高)
|
|
1202
|
+
// 后台定时器启用时由定时器负责推送,poll_message 不再重复检查
|
|
1203
|
+
if (!this.memoryFileCheckInterval) {
|
|
1204
|
+
const fileChangeNotification = await this.buildMemoryFileChangeNotification();
|
|
1205
|
+
if (fileChangeNotification) {
|
|
1206
|
+
const messages = this.messageBuffer.get();
|
|
1207
|
+
messages.directMessages.unshift(fileChangeNotification);
|
|
1208
|
+
return {
|
|
1209
|
+
directMessages: messages.directMessages,
|
|
1210
|
+
groupMessages: messages.groupMessages,
|
|
1211
|
+
fromBuffer: true,
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
623
1215
|
const immediateMemoryNotification = await this.buildMemoryCleanupNotification();
|
|
624
1216
|
if (immediateMemoryNotification) {
|
|
625
1217
|
const messages = this.messageBuffer.get();
|
|
@@ -630,8 +1222,14 @@ export class McpServer {
|
|
|
630
1222
|
fromBuffer: true,
|
|
631
1223
|
};
|
|
632
1224
|
}
|
|
633
|
-
await this.
|
|
634
|
-
|
|
1225
|
+
const messages = await this.waitForMessagesWithPoll();
|
|
1226
|
+
// 后台定时器启用时跳过,避免与定时器推送重复
|
|
1227
|
+
if (!this.memoryFileCheckInterval) {
|
|
1228
|
+
const fileChangeNotification2 = await this.buildMemoryFileChangeNotification();
|
|
1229
|
+
if (fileChangeNotification2) {
|
|
1230
|
+
messages.directMessages.unshift(fileChangeNotification2);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
635
1233
|
const memoryNotification = await this.buildMemoryCleanupNotification();
|
|
636
1234
|
if (memoryNotification) {
|
|
637
1235
|
messages.directMessages.unshift(memoryNotification);
|
|
@@ -642,41 +1240,39 @@ export class McpServer {
|
|
|
642
1240
|
fromBuffer: true,
|
|
643
1241
|
};
|
|
644
1242
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1243
|
+
async waitForMessagesWithPoll() {
|
|
1244
|
+
// 如果缓冲区已有消息(来自上层的非阻塞补拉),立即返回
|
|
1245
|
+
const buffered = this.messageBuffer.get();
|
|
1246
|
+
if (buffered.directMessages.length > 0 || buffered.groupMessages.length > 0) {
|
|
1247
|
+
return buffered;
|
|
1248
|
+
}
|
|
1249
|
+
// 后台长轮询补拉:作为 WebSocket 之外的第二消息通道,不阻塞 poll 的 resolver 注册
|
|
1250
|
+
// 长轮询返回的消息会入 buffer → tryDispatch(),若 poll 已注册 resolver 则被立即唤醒
|
|
1251
|
+
const blockTimeoutMs = this.resolvePollMessageBlockWaitMs();
|
|
1252
|
+
this.hydrateUnreadMessagesIfNeeded({
|
|
1253
|
+
blockIfEmpty: true,
|
|
1254
|
+
blockTimeoutMs,
|
|
1255
|
+
includePipelineTasks: true,
|
|
1256
|
+
}).catch((error) => {
|
|
1257
|
+
logger.warn("[McpServer] 后台 HTTP 长轮询补拉失败:", error);
|
|
1258
|
+
});
|
|
1259
|
+
// 永久阻塞等待 WebSocket 推送的消息
|
|
1260
|
+
// WebSocket 消息到达时,push() → tryDispatch() 会解析此 Promise
|
|
1261
|
+
// 不传递超时参数,确保不会因超时返回空消息而重试
|
|
1262
|
+
return await this.messageBuffer.poll();
|
|
663
1263
|
}
|
|
664
1264
|
resolvePollMessageBlockWaitMs() {
|
|
665
1265
|
const blockWaitMs = this.parsePositiveIntegerEnv("AWS_POLL_MESSAGE_BLOCK_WAIT_MS", DEFAULT_POLL_MESSAGE_BLOCK_WAIT_MS);
|
|
666
|
-
|
|
1266
|
+
// poll_message 走 callToolOnce 专用通道,HTTP 超时使用 POLL_MESSAGE_LONGPOLL_HTTP_TIMEOUT_MS
|
|
1267
|
+
// 与 http-client.ts 中的 callToolOnce 默认超时对齐,不再引用通用的 AWS_MCP_HTTP_REQUEST_TIMEOUT_MS
|
|
1268
|
+
const httpTimeoutMs = POLL_MESSAGE_LONGPOLL_HTTP_TIMEOUT_MS;
|
|
667
1269
|
const maxSafeBlockWaitMs = Math.max(1, httpTimeoutMs - POLL_MESSAGE_HTTP_TIMEOUT_BUFFER_MS);
|
|
668
1270
|
if (blockWaitMs >= httpTimeoutMs) {
|
|
669
|
-
logger.warn(`[McpServer] AWS_POLL_MESSAGE_BLOCK_WAIT_MS=${blockWaitMs}ms 不应大于等于
|
|
1271
|
+
logger.warn(`[McpServer] AWS_POLL_MESSAGE_BLOCK_WAIT_MS=${blockWaitMs}ms 不应大于等于 poll_message HTTP 超时 ${httpTimeoutMs}ms,已夹紧为 ${maxSafeBlockWaitMs}ms`);
|
|
670
1272
|
return maxSafeBlockWaitMs;
|
|
671
1273
|
}
|
|
672
1274
|
return blockWaitMs;
|
|
673
1275
|
}
|
|
674
|
-
resolvePollMessageEmptyRetryDelayMs() {
|
|
675
|
-
return this.parsePositiveIntegerEnv("AWS_POLL_MESSAGE_EMPTY_RETRY_DELAY_MS", DEFAULT_POLL_MESSAGE_EMPTY_RETRY_DELAY_MS);
|
|
676
|
-
}
|
|
677
|
-
resolvePollMessageErrorRetryDelayMs() {
|
|
678
|
-
return this.parsePositiveIntegerEnv("AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS", DEFAULT_POLL_MESSAGE_ERROR_RETRY_DELAY_MS);
|
|
679
|
-
}
|
|
680
1276
|
/**
|
|
681
1277
|
* 解析正整数环境变量。
|
|
682
1278
|
* 主流程:读取环境变量 -> 转成整数 -> 非法值使用默认值。
|
|
@@ -743,6 +1339,249 @@ export class McpServer {
|
|
|
743
1339
|
return null;
|
|
744
1340
|
}
|
|
745
1341
|
}
|
|
1342
|
+
/**
|
|
1343
|
+
* 构建 Memory 文件变更检测通知。
|
|
1344
|
+
* Memory 增强功能 (v1.6.0)
|
|
1345
|
+
*
|
|
1346
|
+
* 检测流程:
|
|
1347
|
+
* 1. 检查本 Agent 标记为 updating 但未处理的 Memory
|
|
1348
|
+
* 2. 检查短期记忆、近期记忆绑定的文件是否变更
|
|
1349
|
+
* 3. 如果没有变更,检查永久记忆的文件变更,标记最多 5 个为 updating
|
|
1350
|
+
*/
|
|
1351
|
+
async buildMemoryFileChangeNotification() {
|
|
1352
|
+
try {
|
|
1353
|
+
const agentId = this.getCurrentAgentId();
|
|
1354
|
+
if (!agentId) {
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
// 步骤1: 检查是否有 pending 的 updating memories(角色本地永久记忆),
|
|
1358
|
+
// 同时检测绑定文件是否再次变更(用户可能在 Agent 处理前又改了文件)
|
|
1359
|
+
const permanentList = await this.roleMemoryStore.listByType('memory');
|
|
1360
|
+
const updatingMemories = permanentList.filter(m => m.update_status === 'updating' && m.updating_agent_id === agentId);
|
|
1361
|
+
if (updatingMemories.length > 0) {
|
|
1362
|
+
// 对 updating 记忆也检测文件是否再次变更
|
|
1363
|
+
const updatingChanges = await this.checkMemoriesFileChanges(updatingMemories);
|
|
1364
|
+
if (updatingChanges.length > 0) {
|
|
1365
|
+
// 有新的文件变更,更新 MD5 map 持久化,发出新变更通知
|
|
1366
|
+
for (const change of updatingChanges) {
|
|
1367
|
+
const memory = updatingMemories.find(m => m.id === change.memoryId);
|
|
1368
|
+
if (memory) {
|
|
1369
|
+
await this.roleMemoryStore.updateMemory({
|
|
1370
|
+
uri: memory.uri,
|
|
1371
|
+
memory_type: 'memory',
|
|
1372
|
+
update_status: 'updating',
|
|
1373
|
+
updating_agent_id: agentId,
|
|
1374
|
+
file_md5_map: memory.file_md5_map,
|
|
1375
|
+
last_check_time: memory.last_check_time,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const selectedCount = Math.min(updatingChanges.length, 5);
|
|
1380
|
+
const changeDetails = updatingChanges
|
|
1381
|
+
.slice(0, 5)
|
|
1382
|
+
.map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
|
|
1383
|
+
.join('\n');
|
|
1384
|
+
return {
|
|
1385
|
+
msgId: `memory-file-change-${Date.now()}`,
|
|
1386
|
+
senderId: "MCP-System",
|
|
1387
|
+
senderName: "系统",
|
|
1388
|
+
receiverId: agentId,
|
|
1389
|
+
content: `检测到以下 ${selectedCount} 个处于更新中的 Memory 绑定的文件再次变更,已更新 MD5 记录:\n\n${changeDetails}\n\n请调用 read_memory 查看最新内容并调用 update_memory 更新。`,
|
|
1390
|
+
requireReply: false,
|
|
1391
|
+
prompt: "这是一条文件变更提醒,您之前标记为更新中的记忆绑定的文件又有新变更,请处理。",
|
|
1392
|
+
timestamp: new Date().toISOString(),
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
const memoryList = updatingMemories
|
|
1396
|
+
.slice(0, 5)
|
|
1397
|
+
.map(m => `- ${m.uri} (文件: ${m.bound_files?.join(', ') || '未知'})`)
|
|
1398
|
+
.join('\n');
|
|
1399
|
+
return {
|
|
1400
|
+
msgId: `memory-update-pending-${Date.now()}`,
|
|
1401
|
+
senderId: "MCP-System",
|
|
1402
|
+
senderName: "系统",
|
|
1403
|
+
receiverId: agentId,
|
|
1404
|
+
content: `您有以下 Memory 正处于更新中状态但尚未完成更新:\n\n${memoryList}\n\n请继续完成更新工作。`,
|
|
1405
|
+
requireReply: false,
|
|
1406
|
+
prompt: "这是一条更新提醒,您之前已标记这些记忆为更新中,请继续处理。",
|
|
1407
|
+
timestamp: new Date().toISOString(),
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
// 步骤2: 检查短期记忆和近期记忆(服务端存储)
|
|
1411
|
+
const recentlyMemories = await this.memoryStore.listByType('recently_memory');
|
|
1412
|
+
const taskListMemories = await this.memoryStore.listByType('task_list');
|
|
1413
|
+
const listableMemories = [...recentlyMemories, ...taskListMemories];
|
|
1414
|
+
// 用本地 MD5 缓存补全 listable 记忆(服务端可能不持久化 file_md5_map 字段)
|
|
1415
|
+
// 确保 checkMemoriesFileChanges 有正确的旧 MD5 做比对而非始终以空字典开始
|
|
1416
|
+
for (const m of listableMemories) {
|
|
1417
|
+
if (!m.file_md5_map) {
|
|
1418
|
+
const cached = this.listableMd5Cache.get(m.id);
|
|
1419
|
+
if (cached) {
|
|
1420
|
+
m.file_md5_map = cached;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const listableChanges = await this.checkMemoriesFileChanges(listableMemories);
|
|
1425
|
+
// 使用复合 key(memoryId + 当前各绑定文件 MD5)过滤已通知变更,
|
|
1426
|
+
// 同一条记忆文件再次变更(MD5 变化)时仍能触发新通知
|
|
1427
|
+
const newListableChanges = listableChanges.filter(c => {
|
|
1428
|
+
const memory = listableMemories.find(m => m.id === c.memoryId);
|
|
1429
|
+
const md5Map = memory?.file_md5_map;
|
|
1430
|
+
const changeKey = this.buildMemoryChangeNotificationKey(c.memoryId, md5Map);
|
|
1431
|
+
return this.notifiedFileChangeMemoryIds.get(c.memoryId) !== changeKey;
|
|
1432
|
+
});
|
|
1433
|
+
if (newListableChanges.length > 0) {
|
|
1434
|
+
// 记录当前状态的复合键,下次对比时若 MD5 变化则重新通知
|
|
1435
|
+
// 同时将 MD5 map 写入本地缓存,弥补服务端不持久化 file_md5_map 的问题
|
|
1436
|
+
for (const c of newListableChanges) {
|
|
1437
|
+
const memory = listableMemories.find(m => m.id === c.memoryId);
|
|
1438
|
+
const md5Map = memory?.file_md5_map;
|
|
1439
|
+
this.notifiedFileChangeMemoryIds.set(c.memoryId, this.buildMemoryChangeNotificationKey(c.memoryId, md5Map));
|
|
1440
|
+
if (memory?.file_md5_map) {
|
|
1441
|
+
this.listableMd5Cache.set(c.memoryId, memory.file_md5_map);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
const changeDetails = newListableChanges
|
|
1445
|
+
.map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
|
|
1446
|
+
.join('\n');
|
|
1447
|
+
return {
|
|
1448
|
+
msgId: `memory-file-change-${Date.now()}`,
|
|
1449
|
+
senderId: "MCP-System",
|
|
1450
|
+
senderName: "系统",
|
|
1451
|
+
receiverId: agentId,
|
|
1452
|
+
content: `检测到以下 Memory 绑定的文件已变更,请及时更新:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。`,
|
|
1453
|
+
requireReply: false,
|
|
1454
|
+
prompt: "这是一条文件变更提醒,自动生成,无需持久化。",
|
|
1455
|
+
timestamp: new Date().toISOString(),
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
// 步骤3: 检查永久记忆(角色本地存储)
|
|
1459
|
+
const normalPermanentMemories = permanentList.filter(m => m.bound_files && m.bound_files.length > 0 &&
|
|
1460
|
+
(m.update_status === 'normal' || !m.update_status));
|
|
1461
|
+
const permanentChanges = await this.checkMemoriesFileChanges(normalPermanentMemories);
|
|
1462
|
+
if (permanentChanges.length > 0) {
|
|
1463
|
+
// 最多选取 5 个
|
|
1464
|
+
const selectedChanges = permanentChanges.slice(0, 5);
|
|
1465
|
+
// 标记为 updating 状态,同时持久化当前 MD5 map,
|
|
1466
|
+
// 确保后续检测能正确对比文件变更,而非每次都视为"已变更"
|
|
1467
|
+
for (const change of selectedChanges) {
|
|
1468
|
+
const memory = permanentList.find(m => m.id === change.memoryId);
|
|
1469
|
+
if (memory) {
|
|
1470
|
+
await this.roleMemoryStore.updateMemory({
|
|
1471
|
+
uri: memory.uri,
|
|
1472
|
+
memory_type: 'memory',
|
|
1473
|
+
update_status: 'updating',
|
|
1474
|
+
updating_agent_id: agentId,
|
|
1475
|
+
// checkMemoriesFileChanges 已更新 in-memory 对象中的 MD5 map
|
|
1476
|
+
file_md5_map: memory.file_md5_map,
|
|
1477
|
+
last_check_time: memory.last_check_time,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const changeDetails = selectedChanges
|
|
1482
|
+
.map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
|
|
1483
|
+
.join('\n');
|
|
1484
|
+
return {
|
|
1485
|
+
msgId: `memory-file-change-${Date.now()}`,
|
|
1486
|
+
senderId: "MCP-System",
|
|
1487
|
+
senderName: "系统",
|
|
1488
|
+
receiverId: agentId,
|
|
1489
|
+
content: `检测到以下 ${selectedChanges.length} 个永久记忆绑定的文件已变更,已为您标记为更新中:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。更新完成后状态将自动恢复为正常。`,
|
|
1490
|
+
requireReply: false,
|
|
1491
|
+
prompt: "这是一条文件变更提醒,已自动标记更新状态,请处理。",
|
|
1492
|
+
timestamp: new Date().toISOString(),
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
logger.warn("[McpServer] 构建 Memory 文件变更检测通知失败:", error);
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* 检查一组 Memory 的文件变更
|
|
1504
|
+
* Memory 增强功能 (v1.6.0)
|
|
1505
|
+
*/
|
|
1506
|
+
async checkMemoriesFileChanges(memories) {
|
|
1507
|
+
const changes = [];
|
|
1508
|
+
for (const memory of memories) {
|
|
1509
|
+
if (!memory.bound_files || memory.bound_files.length === 0) {
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
try {
|
|
1513
|
+
const changedFiles = [];
|
|
1514
|
+
const newMd5Map = {};
|
|
1515
|
+
const oldMd5Map = memory.file_md5_map || {};
|
|
1516
|
+
for (const filePath of memory.bound_files) {
|
|
1517
|
+
try {
|
|
1518
|
+
const currentMd5 = await this.calculateFileMd5(filePath);
|
|
1519
|
+
if (currentMd5 === null) {
|
|
1520
|
+
// 文件不存在或无法读取
|
|
1521
|
+
changedFiles.push(`${filePath} (文件不存在或无法访问)`);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
newMd5Map[filePath] = currentMd5;
|
|
1525
|
+
const oldMd5 = oldMd5Map[filePath];
|
|
1526
|
+
if (!oldMd5 || oldMd5 !== currentMd5) {
|
|
1527
|
+
changedFiles.push(filePath);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
catch (error) {
|
|
1531
|
+
logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
|
|
1532
|
+
changedFiles.push(`${filePath} (MD5计算失败)`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (changedFiles.length > 0) {
|
|
1536
|
+
changes.push({
|
|
1537
|
+
memoryId: memory.id,
|
|
1538
|
+
memoryUri: memory.uri,
|
|
1539
|
+
changedFiles,
|
|
1540
|
+
});
|
|
1541
|
+
// 更新 MD5 映射
|
|
1542
|
+
memory.file_md5_map = newMd5Map;
|
|
1543
|
+
memory.last_check_time = new Date().toISOString();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
catch (error) {
|
|
1547
|
+
logger.warn(`[McpServer] 检查 Memory ${memory.id} 文件变更失败:`, error);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return changes;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* 计算文件 MD5
|
|
1554
|
+
* Memory 增强功能 (v1.6.0)
|
|
1555
|
+
*/
|
|
1556
|
+
async calculateFileMd5(filePath) {
|
|
1557
|
+
try {
|
|
1558
|
+
const { createReadStream } = await import('node:fs');
|
|
1559
|
+
const { createHash } = await import('node:crypto');
|
|
1560
|
+
return new Promise((resolve) => {
|
|
1561
|
+
const hash = createHash('md5');
|
|
1562
|
+
const stream = createReadStream(filePath);
|
|
1563
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
1564
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
1565
|
+
stream.on('error', () => resolve(null));
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* 构建 Memory 文件变更通知的复合去重键。
|
|
1575
|
+
* 基于 memoryId + 当前各绑定文件的 MD5 生成,当任意绑定文件内容变化时复合键也随之变化,
|
|
1576
|
+
* 从而确保真实文件变更不会因简单 memoryId 去重而静默吞掉。
|
|
1577
|
+
*/
|
|
1578
|
+
buildMemoryChangeNotificationKey(memoryId, md5Map) {
|
|
1579
|
+
if (!md5Map || Object.keys(md5Map).length === 0) {
|
|
1580
|
+
return memoryId;
|
|
1581
|
+
}
|
|
1582
|
+
const sorted = Object.entries(md5Map).sort(([a], [b]) => a.localeCompare(b));
|
|
1583
|
+
return `${memoryId}:${sorted.map(([k, v]) => `${k}=${v}`).join('|')}`;
|
|
1584
|
+
}
|
|
746
1585
|
async handleGetMessage() {
|
|
747
1586
|
await this.hydrateUnreadMessagesIfNeeded();
|
|
748
1587
|
const messages = this.messageBuffer.get();
|
|
@@ -949,8 +1788,54 @@ export class McpServer {
|
|
|
949
1788
|
}
|
|
950
1789
|
// 后台注册:不阻塞 MCP 工具暴露,Agent 可立即接收工具调用
|
|
951
1790
|
this.autoRegisterInBackground();
|
|
1791
|
+
// 启动后台 Memory 文件变更检测
|
|
1792
|
+
this.startMemoryFileWatcher();
|
|
952
1793
|
logger.info("[McpServer] MCP Server 已就绪,工具已可用");
|
|
953
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* 停止 McpServer,清理后台定时器。
|
|
1797
|
+
* 主流程:清理文件变更检测定时器 -> 停止 Agent 客户端消息循环。
|
|
1798
|
+
*/
|
|
1799
|
+
stop() {
|
|
1800
|
+
this.stopMemoryFileWatcher();
|
|
1801
|
+
this.agentClient.stop();
|
|
1802
|
+
logger.info("[McpServer] MCP Server 已停止");
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* 启动后台 Memory 文件变更检测定时器。
|
|
1806
|
+
* 主流程:读取 AWS_MEMORY_FILE_CHECK_INTERVAL_MS -> 启动 setInterval -> 每次触发时检查绑定文件 MD5 变更 -> 有变更时直接推入消息缓冲区。
|
|
1807
|
+
* 后台定时器启用后,poll_message 不再重复检查,避免重复推送通知。
|
|
1808
|
+
*/
|
|
1809
|
+
startMemoryFileWatcher() {
|
|
1810
|
+
const intervalMs = this.parsePositiveIntegerEnv("AWS_MEMORY_FILE_CHECK_INTERVAL_MS", DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS);
|
|
1811
|
+
if (intervalMs <= 0) {
|
|
1812
|
+
logger.info("[McpServer] 后台文件变更检测已禁用(AWS_MEMORY_FILE_CHECK_INTERVAL_MS=0),将回退到 poll_message 时检测");
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
this.memoryFileCheckInterval = setInterval(async () => {
|
|
1816
|
+
try {
|
|
1817
|
+
const notification = await this.buildMemoryFileChangeNotification();
|
|
1818
|
+
if (notification) {
|
|
1819
|
+
this.messageBuffer.push(notification);
|
|
1820
|
+
logger.info("[McpServer] 后台文件变更检测: 已推送 Memory 变更通知到消息缓冲区");
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
catch (error) {
|
|
1824
|
+
logger.warn("[McpServer] 后台文件变更检测失败:", error);
|
|
1825
|
+
}
|
|
1826
|
+
}, intervalMs);
|
|
1827
|
+
logger.info(`[McpServer] 后台文件变更检测已启动,间隔: ${intervalMs}ms(可通过 AWS_MEMORY_FILE_CHECK_INTERVAL_MS 环境变量调整,设为 0 禁用)`);
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* 停止后台 Memory 文件变更检测定时器。
|
|
1831
|
+
*/
|
|
1832
|
+
stopMemoryFileWatcher() {
|
|
1833
|
+
if (this.memoryFileCheckInterval) {
|
|
1834
|
+
clearInterval(this.memoryFileCheckInterval);
|
|
1835
|
+
this.memoryFileCheckInterval = null;
|
|
1836
|
+
logger.info("[McpServer] 后台文件变更检测已停止");
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
954
1839
|
async autoRegister() {
|
|
955
1840
|
const agentConfig = this.agentClient.getAgentConfig();
|
|
956
1841
|
logger.info(`[McpServer] 角色: "${agentConfig.roleName}"`);
|
|
@@ -971,6 +1856,11 @@ export class McpServer {
|
|
|
971
1856
|
statusReporter.reportWaitingInput();
|
|
972
1857
|
}
|
|
973
1858
|
await this.reportCombinedMemoryStats();
|
|
1859
|
+
// 如果是新创建的角色,将本地永久记忆同步到服务端
|
|
1860
|
+
if (result.isExisting === false) {
|
|
1861
|
+
logger.info("[McpServer] 新角色创建,同步本地永久记忆到服务端...");
|
|
1862
|
+
await this.syncLocalPermanentMemoryToServer();
|
|
1863
|
+
}
|
|
974
1864
|
return;
|
|
975
1865
|
}
|
|
976
1866
|
catch (error) {
|
|
@@ -1026,6 +1916,7 @@ export class McpServer {
|
|
|
1026
1916
|
// 收到 Profile 信息后,存储到消息缓冲区
|
|
1027
1917
|
logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
|
|
1028
1918
|
this.messageBuffer.setProfileMessage(profile);
|
|
1919
|
+
this.syncRoleMemoryStore(profile);
|
|
1029
1920
|
},
|
|
1030
1921
|
});
|
|
1031
1922
|
return;
|
|
@@ -1036,6 +1927,7 @@ export class McpServer {
|
|
|
1036
1927
|
// 收到 Profile 信息后,存储到消息缓冲区
|
|
1037
1928
|
logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
|
|
1038
1929
|
this.messageBuffer.setProfileMessage(profile);
|
|
1930
|
+
this.syncRoleMemoryStore(profile);
|
|
1039
1931
|
},
|
|
1040
1932
|
});
|
|
1041
1933
|
}
|