@coreyuan/vector-mind 1.0.53 → 1.1.0

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.
Files changed (140) hide show
  1. package/README.md +99 -72
  2. package/admin-panel/README.md +77 -0
  3. package/admin-panel/dist/client/assets/index-Bzyjdhh2.css +1 -0
  4. package/admin-panel/dist/client/assets/index-Ph73ewzt.js +9 -0
  5. package/admin-panel/dist/client/index.html +14 -0
  6. package/admin-panel/package.json +25 -0
  7. package/admin-panel/server/index.mjs +1424 -0
  8. package/admin-panel/server/security.mjs +117 -0
  9. package/admin-panel/server/security.test.mjs +113 -0
  10. package/admin-panel/server/storage.mjs +49 -0
  11. package/admin-panel/server/storage.test.mjs +36 -0
  12. package/dist/builtin-conventions.js +1 -1
  13. package/dist/builtin-conventions.js.map +1 -1
  14. package/dist/builtin-instructions.d.ts +1 -1
  15. package/dist/builtin-instructions.js +1 -1
  16. package/dist/builtin-instructions.js.map +1 -1
  17. package/dist/checkpoint-snapshot.d.ts +56 -0
  18. package/dist/checkpoint-snapshot.js +411 -0
  19. package/dist/checkpoint-snapshot.js.map +1 -0
  20. package/dist/config.d.ts +4 -1
  21. package/dist/config.js +25 -1
  22. package/dist/config.js.map +1 -1
  23. package/dist/context-governance.d.ts +56 -0
  24. package/dist/context-governance.js +176 -0
  25. package/dist/context-governance.js.map +1 -0
  26. package/dist/database-runtime.d.ts +6 -1
  27. package/dist/database-runtime.js +555 -127
  28. package/dist/database-runtime.js.map +1 -1
  29. package/dist/development-warnings.d.ts +2 -1
  30. package/dist/development-warnings.js +92 -22
  31. package/dist/development-warnings.js.map +1 -1
  32. package/dist/file-indexing.d.ts +2 -1
  33. package/dist/file-indexing.js +127 -47
  34. package/dist/file-indexing.js.map +1 -1
  35. package/dist/grep.js +4 -0
  36. package/dist/grep.js.map +1 -1
  37. package/dist/index.js +8 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/large-file-split.d.ts +53 -8
  40. package/dist/large-file-split.js +294 -75
  41. package/dist/large-file-split.js.map +1 -1
  42. package/dist/memory-maintenance/compaction.d.ts +7 -0
  43. package/dist/memory-maintenance/compaction.js +133 -0
  44. package/dist/memory-maintenance/compaction.js.map +1 -0
  45. package/dist/memory-maintenance/context.d.ts +37 -0
  46. package/dist/memory-maintenance/context.js +52 -0
  47. package/dist/memory-maintenance/context.js.map +1 -0
  48. package/dist/memory-maintenance/index-prune.d.ts +22 -0
  49. package/dist/memory-maintenance/index-prune.js +243 -0
  50. package/dist/memory-maintenance/index-prune.js.map +1 -0
  51. package/dist/memory-maintenance/metrics.d.ts +5 -0
  52. package/dist/memory-maintenance/metrics.js +30 -0
  53. package/dist/memory-maintenance/metrics.js.map +1 -0
  54. package/dist/memory-maintenance/pending.d.ts +4 -0
  55. package/dist/memory-maintenance/pending.js +74 -0
  56. package/dist/memory-maintenance/pending.js.map +1 -0
  57. package/dist/memory-maintenance/purge.d.ts +7 -0
  58. package/dist/memory-maintenance/purge.js +115 -0
  59. package/dist/memory-maintenance/purge.js.map +1 -0
  60. package/dist/memory-maintenance/runner.d.ts +5 -0
  61. package/dist/memory-maintenance/runner.js +182 -0
  62. package/dist/memory-maintenance/runner.js.map +1 -0
  63. package/dist/memory-maintenance/sqlite.d.ts +10 -0
  64. package/dist/memory-maintenance/sqlite.js +67 -0
  65. package/dist/memory-maintenance/sqlite.js.map +1 -0
  66. package/dist/memory-maintenance/types.d.ts +87 -0
  67. package/dist/memory-maintenance/types.js +2 -0
  68. package/dist/memory-maintenance/types.js.map +1 -0
  69. package/dist/memory-maintenance.d.ts +5 -80
  70. package/dist/memory-maintenance.js +3 -536
  71. package/dist/memory-maintenance.js.map +1 -1
  72. package/dist/memory-mutations.d.ts +3 -3
  73. package/dist/memory-mutations.js +109 -43
  74. package/dist/memory-mutations.js.map +1 -1
  75. package/dist/memory-recall.d.ts +3 -1
  76. package/dist/memory-recall.js +33 -1
  77. package/dist/memory-recall.js.map +1 -1
  78. package/dist/operation-scope.js +42 -12
  79. package/dist/operation-scope.js.map +1 -1
  80. package/dist/path-containment.d.ts +5 -0
  81. package/dist/path-containment.js +64 -0
  82. package/dist/path-containment.js.map +1 -0
  83. package/dist/path-filters.d.ts +1 -1
  84. package/dist/path-filters.js +7 -6
  85. package/dist/path-filters.js.map +1 -1
  86. package/dist/path-rules.d.ts +1 -0
  87. package/dist/path-rules.js +23 -1
  88. package/dist/path-rules.js.map +1 -1
  89. package/dist/pending-changes.d.ts +5 -0
  90. package/dist/pending-changes.js +30 -16
  91. package/dist/pending-changes.js.map +1 -1
  92. package/dist/project-files.d.ts +1 -0
  93. package/dist/project-files.js +19 -10
  94. package/dist/project-files.js.map +1 -1
  95. package/dist/root.d.ts +4 -1
  96. package/dist/root.js +34 -4
  97. package/dist/root.js.map +1 -1
  98. package/dist/rtk-integrity.d.ts +5 -0
  99. package/dist/rtk-integrity.js +35 -0
  100. package/dist/rtk-integrity.js.map +1 -0
  101. package/dist/rtk-shim.js +22 -9
  102. package/dist/rtk-shim.js.map +1 -1
  103. package/dist/rtk-tools.d.ts +8 -0
  104. package/dist/rtk-tools.js +59 -42
  105. package/dist/rtk-tools.js.map +1 -1
  106. package/dist/server-instructions.js +8 -88
  107. package/dist/server-instructions.js.map +1 -1
  108. package/dist/tool-catalog.d.ts +2 -0
  109. package/dist/tool-catalog.js +77 -24
  110. package/dist/tool-catalog.js.map +1 -1
  111. package/dist/tool-handlers/context-recovery.js +79 -54
  112. package/dist/tool-handlers/context-recovery.js.map +1 -1
  113. package/dist/tool-handlers/large-files.js +615 -41
  114. package/dist/tool-handlers/large-files.js.map +1 -1
  115. package/dist/tool-handlers/memory-diagnostics.js +240 -45
  116. package/dist/tool-handlers/memory-diagnostics.js.map +1 -1
  117. package/dist/tool-handlers/memory.js +167 -22
  118. package/dist/tool-handlers/memory.js.map +1 -1
  119. package/dist/tool-handlers/operations.js +28 -8
  120. package/dist/tool-handlers/operations.js.map +1 -1
  121. package/dist/tool-handlers/requirement-status.d.ts +5 -0
  122. package/dist/tool-handlers/requirement-status.js +267 -0
  123. package/dist/tool-handlers/requirement-status.js.map +1 -0
  124. package/dist/tool-handlers/requirements.js +851 -115
  125. package/dist/tool-handlers/requirements.js.map +1 -1
  126. package/dist/tool-handlers.d.ts +4 -0
  127. package/dist/tool-handlers.js +439 -6
  128. package/dist/tool-handlers.js.map +1 -1
  129. package/dist/tool-output.d.ts +71 -2
  130. package/dist/tool-output.js +56 -13
  131. package/dist/tool-output.js.map +1 -1
  132. package/dist/tool-schemas.d.ts +243 -0
  133. package/dist/tool-schemas.js +112 -26
  134. package/dist/tool-schemas.js.map +1 -1
  135. package/dist/types.d.ts +10 -1
  136. package/docs/capability-matrix.md +23 -17
  137. package/package.json +21 -5
  138. package/skills/vector-mind-autopilot/SKILL.md +82 -23
  139. package/skills/vector-mind-autopilot/references/claude-project-instructions.md +14 -11
  140. package/skills/vector-mind-autopilot/references/universal-system-prompt.md +24 -16
@@ -0,0 +1,1424 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import fsp from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+
10
+ import express from "express";
11
+ import Database from "better-sqlite3";
12
+
13
+ import { ADMIN_TOKEN_HEADER, createAdminSecurityPolicy, evaluateAdminRequest } from "./security.mjs";
14
+ import { atomicWriteTextFile, projectPathIdentity } from "./storage.mjs";
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const ROOT_DIR = path.resolve(__dirname, "..");
19
+ const CLIENT_DIR = path.join(ROOT_DIR, "client");
20
+ const DIST_DIR = path.join(ROOT_DIR, "dist", "client");
21
+
22
+ const PORT = Number.parseInt(process.env.VECTORMIND_ADMIN_PORT ?? "16860", 10) || 16860;
23
+ const HOST = process.env.VECTORMIND_ADMIN_HOST?.trim() || "127.0.0.1";
24
+ const ADMIN_SECURITY = createAdminSecurityPolicy({
25
+ host: HOST,
26
+ port: PORT,
27
+ configuredToken: process.env.VECTORMIND_ADMIN_TOKEN,
28
+ });
29
+ const ADMIN_TOKEN = ADMIN_SECURITY.token;
30
+ const STORAGE_DIR = path.join(os.homedir(), ".vectormind-admin");
31
+ const INDEX_FILE = path.join(STORAGE_DIR, "projects.json");
32
+ const CURRENT_PROJECT_ROOT = path.resolve(ROOT_DIR, "..");
33
+
34
+ const SKIP_SCAN_DIRS = new Set([
35
+ ".git",
36
+ ".hg",
37
+ ".svn",
38
+ ".cache",
39
+ ".codex",
40
+ ".vscode",
41
+ ".vs",
42
+ ".vectormind",
43
+ "node_modules",
44
+ "dist",
45
+ "build",
46
+ "out",
47
+ "target",
48
+ "bin",
49
+ "obj",
50
+ "coverage",
51
+ "AppData",
52
+ ]);
53
+
54
+ function ensureStorage() {
55
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
56
+ if (!fs.existsSync(INDEX_FILE)) {
57
+ writeIndex({
58
+ version: 1,
59
+ createdAt: new Date().toISOString(),
60
+ updatedAt: new Date().toISOString(),
61
+ projects: [],
62
+ });
63
+ }
64
+ }
65
+
66
+ function safeJsonParse(text, fallback) {
67
+ try {
68
+ return JSON.parse(text);
69
+ } catch {
70
+ return fallback;
71
+ }
72
+ }
73
+
74
+ function readIndex() {
75
+ ensureStorage();
76
+ const fallback = {
77
+ version: 1,
78
+ createdAt: new Date().toISOString(),
79
+ updatedAt: new Date().toISOString(),
80
+ projects: [],
81
+ };
82
+ const parsed = safeJsonParse(fs.readFileSync(INDEX_FILE, "utf8"), fallback);
83
+ return {
84
+ version: 1,
85
+ createdAt: parsed.createdAt ?? fallback.createdAt,
86
+ updatedAt: parsed.updatedAt ?? fallback.updatedAt,
87
+ projects: Array.isArray(parsed.projects) ? parsed.projects : [],
88
+ };
89
+ }
90
+
91
+ function writeIndex(index) {
92
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
93
+ const normalized = {
94
+ version: 1,
95
+ createdAt: index.createdAt ?? new Date().toISOString(),
96
+ updatedAt: new Date().toISOString(),
97
+ projects: Array.isArray(index.projects) ? index.projects : [],
98
+ };
99
+ atomicWriteTextFile(INDEX_FILE, `${JSON.stringify(normalized, null, 2)}\n`);
100
+ return normalized;
101
+ }
102
+
103
+ function expandPath(input) {
104
+ const raw = String(input ?? "").trim().replace(/^["']|["']$/g, "");
105
+ if (!raw) return "";
106
+ const withEnv = raw.replace(/%([^%]+)%/g, (_, key) => process.env[key] ?? `%${key}%`);
107
+ if (withEnv === "~") return os.homedir();
108
+ if (withEnv.startsWith(`~${path.sep}`) || withEnv.startsWith("~/") || withEnv.startsWith("~\\")) {
109
+ return path.join(os.homedir(), withEnv.slice(2));
110
+ }
111
+ return withEnv;
112
+ }
113
+
114
+ function normalizeProjectPath(input) {
115
+ const expanded = expandPath(input);
116
+ if (!expanded) return "";
117
+ return path.resolve(expanded);
118
+ }
119
+
120
+ function realDirectoryPath(projectPath) {
121
+ const normalized = normalizeProjectPath(projectPath);
122
+ if (!normalized) {
123
+ const error = new Error("请输入项目路径。");
124
+ error.status = 400;
125
+ throw error;
126
+ }
127
+ if (!fs.existsSync(normalized)) {
128
+ const error = new Error(`项目路径不存在:${normalized}`);
129
+ error.status = 400;
130
+ throw error;
131
+ }
132
+ const stat = fs.statSync(normalized);
133
+ if (!stat.isDirectory()) {
134
+ const error = new Error(`项目路径不是文件夹:${normalized}`);
135
+ error.status = 400;
136
+ throw error;
137
+ }
138
+ try {
139
+ return fs.realpathSync.native(normalized);
140
+ } catch {
141
+ return normalized;
142
+ }
143
+ }
144
+
145
+ function projectIdFor(projectPath) {
146
+ return `prj_${crypto.createHash("sha1").update(projectPathIdentity(projectPath)).digest("hex").slice(0, 12)}`;
147
+ }
148
+
149
+ function folderNameFor(projectPath) {
150
+ return path.basename(projectPath) || projectPath;
151
+ }
152
+
153
+ function toProjectRecord(input, previous = null) {
154
+ const realPath = realDirectoryPath(input.path);
155
+ const now = new Date().toISOString();
156
+ return {
157
+ id: previous?.id ?? projectIdFor(realPath),
158
+ name: String(input.name ?? previous?.name ?? folderNameFor(realPath)).trim() || folderNameFor(realPath),
159
+ folderName: folderNameFor(realPath),
160
+ path: realPath,
161
+ createdAt: previous?.createdAt ?? now,
162
+ updatedAt: now,
163
+ lastOpenedAt: previous?.lastOpenedAt ?? null,
164
+ };
165
+ }
166
+
167
+ function upsertProject(input) {
168
+ const index = readIndex();
169
+ const candidatePath = realDirectoryPath(input.path);
170
+ const id = input.id ?? projectIdFor(candidatePath);
171
+ const existingIndex = index.projects.findIndex(
172
+ (p) => p.id === id || projectPathIdentity(String(p.path ?? "")) === projectPathIdentity(candidatePath),
173
+ );
174
+ const previous = existingIndex >= 0 ? index.projects[existingIndex] : { id };
175
+ const record = toProjectRecord({ ...input, path: candidatePath }, previous);
176
+ if (existingIndex >= 0) index.projects[existingIndex] = record;
177
+ else index.projects.unshift(record);
178
+ writeIndex(index);
179
+ return record;
180
+ }
181
+
182
+ function formatBytes(bytes) {
183
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
184
+ const units = ["B", "KB", "MB", "GB"];
185
+ let size = bytes;
186
+ let unitIndex = 0;
187
+ while (size >= 1024 && unitIndex < units.length - 1) {
188
+ size /= 1024;
189
+ unitIndex += 1;
190
+ }
191
+ return `${size >= 10 || unitIndex === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[unitIndex]}`;
192
+ }
193
+
194
+ function fileInfo(filePath) {
195
+ try {
196
+ const stat = fs.statSync(filePath);
197
+ return {
198
+ exists: true,
199
+ size: stat.size,
200
+ sizeText: formatBytes(stat.size),
201
+ modifiedAt: stat.mtime.toISOString(),
202
+ };
203
+ } catch {
204
+ return { exists: false, size: 0, sizeText: "0 B", modifiedAt: null };
205
+ }
206
+ }
207
+
208
+ function tableExists(db, tableName) {
209
+ return !!db
210
+ .prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name = ? LIMIT 1")
211
+ .get(tableName);
212
+ }
213
+
214
+ function columnExists(db, tableName, columnName) {
215
+ if (!tableExists(db, tableName)) return false;
216
+ try {
217
+ return db.prepare(`PRAGMA table_info(${tableName})`).all().some((row) => row.name === columnName);
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ function safeAll(db, sql, params = []) {
224
+ try {
225
+ return db.prepare(sql).all(...params);
226
+ } catch {
227
+ return [];
228
+ }
229
+ }
230
+
231
+ function safeGet(db, sql, params = []) {
232
+ try {
233
+ return db.prepare(sql).get(...params);
234
+ } catch {
235
+ return undefined;
236
+ }
237
+ }
238
+
239
+ function safeCount(db, tableName, where = "", params = []) {
240
+ if (!tableExists(db, tableName)) return 0;
241
+ const row = safeGet(db, `SELECT COUNT(*) AS count FROM ${tableName} ${where}`, params);
242
+ return Number(row?.count ?? 0);
243
+ }
244
+
245
+ function parseMaybeJson(value) {
246
+ if (!value || typeof value !== "string") return null;
247
+ try {
248
+ return JSON.parse(value);
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+
254
+ function previewText(text, max = 180) {
255
+ const compact = String(text ?? "").replace(/\s+/g, " ").trim();
256
+ return compact.length > max ? `${compact.slice(0, max - 1)}…` : compact;
257
+ }
258
+
259
+ function hasCorruptQuestionRuns(text) {
260
+ return /[??]{3,}/.test(String(text ?? ""));
261
+ }
262
+
263
+ function isMostlyCorruptText(text) {
264
+ const raw = String(text ?? "").trim();
265
+ if (!raw) return false;
266
+ if (!hasCorruptQuestionRuns(raw)) return false;
267
+ const compact = raw.replace(/\s+/g, "");
268
+ const questionCount = (compact.match(/[??]/g) ?? []).length;
269
+ return questionCount / Math.max(1, compact.length) > 0.22;
270
+ }
271
+
272
+ function semanticFallbackForCorruptText(text, fallback = "未识别记忆") {
273
+ const raw = String(text ?? "");
274
+ const parts = [];
275
+ if (/memory maintenance/i.test(raw)) parts.push("记忆维护");
276
+ if (/WAL\s+checkpoint/i.test(raw)) parts.push("WAL checkpoint");
277
+ if (/dry-run/i.test(raw)) parts.push("dry-run");
278
+ if (/checkpoint/i.test(raw) && !parts.some((part) => /checkpoint/i.test(part))) parts.push("checkpoint");
279
+ if (parts.length) return parts.join(" / ");
280
+ return fallback;
281
+ }
282
+
283
+ function cleanDisplayText(text, fallback = "未识别记忆", max = 180) {
284
+ const raw = String(text ?? "").replace(/\s+/g, " ").trim();
285
+ if (!raw) return fallback;
286
+ if (isMostlyCorruptText(raw)) return previewText(semanticFallbackForCorruptText(raw, fallback), max);
287
+ const cleaned = raw
288
+ .replace(/[??]{3,}/g, "…")
289
+ .replace(/\s+/g, " ")
290
+ .replace(/(?:…\s*){2,}/g, "…")
291
+ .trim();
292
+ if (!cleaned || cleaned === "…") return fallback;
293
+ return previewText(cleaned, max);
294
+ }
295
+
296
+ function normalizeForGrouping(text) {
297
+ return String(text ?? "")
298
+ .replace(/\s+/g, " ")
299
+ .replace(/[??]{3,}/g, "?")
300
+ .trim();
301
+ }
302
+
303
+ function groupChangeLogRows(rows) {
304
+ const groups = new Map();
305
+ for (const row of rows) {
306
+ const rowFiles = parseChangeLogFiles(row);
307
+ const intentKey = normalizeForGrouping(row.intent_summary);
308
+ let aggregateFiles = null;
309
+ try {
310
+ aggregateFiles = row.files_json ? JSON.parse(row.files_json) : null;
311
+ } catch {
312
+ aggregateFiles = null;
313
+ }
314
+ const key = Array.isArray(aggregateFiles)
315
+ ? `aggregate:${row.id}`
316
+ : `${row.req_id ?? ""}|${row.timestamp ?? ""}|${intentKey}`;
317
+ const existing =
318
+ groups.get(key) ??
319
+ {
320
+ id: row.id,
321
+ ids: [],
322
+ req_id: row.req_id,
323
+ intent_summary: row.intent_summary,
324
+ timestamp: row.timestamp,
325
+ files: [],
326
+ };
327
+ existing.ids.push(row.id);
328
+ for (const filePath of rowFiles) {
329
+ if (filePath && !existing.files.includes(filePath)) existing.files.push(filePath);
330
+ }
331
+ groups.set(key, existing);
332
+ }
333
+ return [...groups.values()];
334
+ }
335
+
336
+ function parseChangeLogFiles(row) {
337
+ if (row?.files_json) {
338
+ try {
339
+ const parsed = JSON.parse(row.files_json);
340
+ if (Array.isArray(parsed)) {
341
+ const files = parsed
342
+ .map((item) => (item && typeof item === "object" ? item.file_path : null))
343
+ .filter((filePath) => typeof filePath === "string" && filePath && filePath !== "(multiple)");
344
+ if (files.length) return files;
345
+ }
346
+ } catch {
347
+ // fall back to file_path
348
+ }
349
+ }
350
+ return row?.file_path && row.file_path !== "(multiple)" ? [row.file_path] : [];
351
+ }
352
+
353
+ function summarizeFiles(files) {
354
+ const unique = [...new Set((files ?? []).filter(Boolean))];
355
+ if (!unique.length) return "";
356
+ if (unique.length === 1) return unique[0];
357
+ const shown = unique.slice(0, 4).join("、");
358
+ const suffix = unique.length > 4 ? " 等" : "";
359
+ return `涉及 ${unique.length} 个文件:${shown}${suffix}`;
360
+ }
361
+
362
+ function memoryItemFiles(row) {
363
+ const metadata = parseMaybeJson(row?.metadata_json) ?? {};
364
+ if (!Array.isArray(metadata.files)) return [];
365
+ return metadata.files
366
+ .map((item) => (item && typeof item === "object" ? item.file_path : null))
367
+ .filter((filePath) => typeof filePath === "string" && filePath && filePath !== "(multiple)" && filePath !== "(unspecified)");
368
+ }
369
+
370
+ function memoryLogTitle(row) {
371
+ if (row.kind === "change_intent") return cleanDisplayText(row.content, "记录改动意图", 132);
372
+ if (row.kind === "requirement") return cleanDisplayText(row.title, "需求记录", 120);
373
+ return cleanDisplayText(row.title || mapMemoryKind(row.kind), mapMemoryKind(row.kind), 120);
374
+ }
375
+
376
+ function memoryLogDetail(row) {
377
+ if (row.kind === "change_intent") {
378
+ const files = memoryItemFiles(row);
379
+ const filesText = summarizeFiles(files);
380
+ if (filesText) return filesText;
381
+ return cleanDisplayText(row.title, "所属需求", 160);
382
+ }
383
+ return cleanDisplayText(row.content, "暂无可读记忆内容", 160);
384
+ }
385
+
386
+ function numericSetValue(value) {
387
+ const number = Number(value);
388
+ return Number.isFinite(number) && number > 0 ? number : null;
389
+ }
390
+
391
+ function buildMemoryLogMirrorKeys(memoryRows) {
392
+ const requirementReqIds = new Set();
393
+ const changeLogIds = new Set();
394
+ const changeIntentKeys = new Set();
395
+
396
+ for (const row of memoryRows) {
397
+ const reqId = numericSetValue(row.req_id);
398
+ if (row.kind === "requirement" && reqId !== null) {
399
+ requirementReqIds.add(reqId);
400
+ continue;
401
+ }
402
+
403
+ if (row.kind !== "change_intent") continue;
404
+ if (reqId !== null) {
405
+ changeIntentKeys.add(`${reqId}|${normalizeForGrouping(row.content)}`);
406
+ }
407
+
408
+ const metadata = parseMaybeJson(row.metadata_json) ?? {};
409
+ const changeLogId = numericSetValue(metadata.change_log_id);
410
+ if (changeLogId !== null) changeLogIds.add(changeLogId);
411
+ if (Array.isArray(metadata.source_change_ids)) {
412
+ for (const id of metadata.source_change_ids) {
413
+ const parsed = numericSetValue(id);
414
+ if (parsed !== null) changeLogIds.add(parsed);
415
+ }
416
+ }
417
+ }
418
+
419
+ return { requirementReqIds, changeLogIds, changeIntentKeys };
420
+ }
421
+
422
+ function mapMemoryKind(kind) {
423
+ const labels = {
424
+ project_summary: "项目摘要",
425
+ requirement: "需求",
426
+ change_intent: "改动意图",
427
+ note: "备注",
428
+ decision: "决策",
429
+ convention: "规则",
430
+ checkpoint: "检查点",
431
+ memory_compaction: "压缩",
432
+ file_chunk: "代码块",
433
+ fix_pattern: "修复模式",
434
+ };
435
+ return labels[kind] ?? kind ?? "记忆";
436
+ }
437
+
438
+ function memoryLevelFor(kind) {
439
+ if (["decision", "convention", "project_summary", "requirement"].includes(kind)) return "hit";
440
+ if (["fix_pattern", "memory_compaction", "checkpoint"].includes(kind)) return "warn";
441
+ if (["file_chunk"].includes(kind)) return "muted";
442
+ return "info";
443
+ }
444
+
445
+ function guardLevelForSeverity(severity) {
446
+ if (severity === "critical" || severity === "warn") return "warn";
447
+ if (severity === "ok") return "hit";
448
+ return "info";
449
+ }
450
+
451
+ function kindLabelForGuard(kind) {
452
+ const labels = {
453
+ fix_pattern: "回归防护",
454
+ memory_compaction: "记忆维护",
455
+ checkpoint: "上下文检查点",
456
+ };
457
+ return labels[kind] ?? mapMemoryKind(kind);
458
+ }
459
+
460
+ function categoryForGuardEventType(eventType) {
461
+ if (String(eventType).includes("large_file")) return "大文件";
462
+ if (String(eventType).includes("scope")) return "需求边界";
463
+ if (String(eventType).includes("requirement")) return "需求映射";
464
+ if (String(eventType).includes("cross_project")) return "项目隔离";
465
+ if (String(eventType).includes("operation")) return "操作防护";
466
+ if (String(eventType).includes("warning")) return "开发防护";
467
+ return "MCP 防护";
468
+ }
469
+
470
+ function statusForGuardSeverity(severity) {
471
+ if (severity === "critical") return "已拦截";
472
+ if (severity === "warn") return "已提醒";
473
+ return "已辅助";
474
+ }
475
+
476
+ function buildMcpGuardLogs({ guardRows = [] }) {
477
+ const logs = [];
478
+ for (const row of guardRows.slice(0, 50)) {
479
+ logs.push({
480
+ id: `guard-event-${row.id}`,
481
+ at: row.created_at,
482
+ title: cleanDisplayText(row.title, "MCP 防护事件", 140),
483
+ detail: cleanDisplayText(row.detail, row.tool_name ? `工具:${row.tool_name}` : "MCP 已介入", 200),
484
+ status: statusForGuardSeverity(row.severity),
485
+ level: guardLevelForSeverity(row.severity),
486
+ category: categoryForGuardEventType(row.event_type),
487
+ });
488
+ }
489
+
490
+ const deduped = new Map();
491
+ for (const log of logs) {
492
+ const key = `${log.id}`;
493
+ if (!deduped.has(key)) deduped.set(key, log);
494
+ }
495
+ return [...deduped.values()]
496
+ .filter((row) => row.at)
497
+ .sort((a, b) => String(b.at).localeCompare(String(a.at)))
498
+ .slice(0, 36);
499
+ }
500
+
501
+ function withDb(project, readFn) {
502
+ const dbPath = path.join(project.path, ".vectormind", "vectormind.db");
503
+ if (!fs.existsSync(dbPath)) return readFn(null, dbPath);
504
+ let db = null;
505
+ try {
506
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
507
+ db.pragma("query_only = ON");
508
+ return readFn(db, dbPath);
509
+ } finally {
510
+ try {
511
+ db?.close();
512
+ } catch {
513
+ // ignore
514
+ }
515
+ }
516
+ }
517
+
518
+ function readProjectMemory(project) {
519
+ const dbPath = path.join(project.path, ".vectormind", "vectormind.db");
520
+ const dbMain = fileInfo(dbPath);
521
+ const wal = fileInfo(`${dbPath}-wal`);
522
+ const shm = fileInfo(`${dbPath}-shm`);
523
+ const totalBytes = dbMain.size + wal.size + shm.size;
524
+
525
+ const base = {
526
+ dbPresent: dbMain.exists,
527
+ dbPath,
528
+ dbSize: totalBytes,
529
+ dbSizeText: formatBytes(totalBytes),
530
+ dbModifiedAt: dbMain.modifiedAt,
531
+ counts: {
532
+ requirements: 0,
533
+ activeRequirements: 0,
534
+ completedRequirements: 0,
535
+ memoryItems: 0,
536
+ pendingChanges: 0,
537
+ changeLogs: 0,
538
+ symbols: 0,
539
+ decisions: 0,
540
+ conventions: 0,
541
+ notes: 0,
542
+ checkpoints: 0,
543
+ },
544
+ kindCounts: [],
545
+ appliedMemory: [],
546
+ commandLogs: [],
547
+ memoryLogs: [],
548
+ health: {
549
+ hitRate: 0,
550
+ conflictCount: 0,
551
+ lastAppliedAt: null,
552
+ watcherHint: "读取本地 SQLite 记忆库",
553
+ },
554
+ };
555
+
556
+ let auditForGuardLogs = null;
557
+ try {
558
+ auditForGuardLogs = readMemoryAudit(project);
559
+ } catch {
560
+ auditForGuardLogs = null;
561
+ }
562
+
563
+ return withDb(project, (db) => {
564
+ if (!db) return base;
565
+
566
+ const counts = {
567
+ requirements: safeCount(db, "requirements"),
568
+ activeRequirements: safeCount(db, "requirements", "WHERE status = 'active'"),
569
+ completedRequirements: safeCount(db, "requirements", "WHERE status = 'completed'"),
570
+ memoryItems: safeCount(db, "memory_items"),
571
+ pendingChanges: safeCount(db, "pending_changes"),
572
+ changeLogs: safeCount(db, "change_logs"),
573
+ symbols: safeCount(db, "symbols"),
574
+ decisions: safeCount(db, "memory_items", "WHERE kind = 'decision'"),
575
+ conventions: safeCount(db, "memory_items", "WHERE kind = 'convention'"),
576
+ notes: safeCount(db, "memory_items", "WHERE kind = 'note'"),
577
+ checkpoints: safeCount(db, "memory_items", "WHERE kind = 'checkpoint'"),
578
+ };
579
+
580
+ const kindCounts = safeAll(
581
+ db,
582
+ `SELECT kind, COUNT(*) AS count
583
+ FROM memory_items
584
+ GROUP BY kind
585
+ ORDER BY count DESC, kind ASC`,
586
+ ).map((row) => ({
587
+ kind: row.kind,
588
+ label: mapMemoryKind(row.kind),
589
+ count: Number(row.count ?? 0),
590
+ }));
591
+
592
+ const memoryRows = safeAll(
593
+ db,
594
+ `SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, created_at, updated_at
595
+ FROM memory_items
596
+ WHERE kind IN ('project_summary','requirement','change_intent','note','decision','convention','checkpoint','memory_compaction','fix_pattern')
597
+ ORDER BY updated_at DESC, id DESC
598
+ LIMIT 120`,
599
+ );
600
+
601
+ const activeRequirements = safeAll(
602
+ db,
603
+ `SELECT id, title, status, context_data, created_at, updated_at
604
+ FROM requirements
605
+ WHERE status = 'active'
606
+ ORDER BY updated_at DESC, created_at DESC, id DESC
607
+ LIMIT 12`,
608
+ );
609
+
610
+ const recentRequirements = safeAll(
611
+ db,
612
+ `SELECT id, title, status, context_data, created_at, updated_at
613
+ FROM requirements
614
+ ORDER BY updated_at DESC, created_at DESC, id DESC
615
+ LIMIT 20`,
616
+ );
617
+
618
+ const hasAggregateChangeColumns =
619
+ columnExists(db, "change_logs", "files_json") && columnExists(db, "change_logs", "file_count");
620
+ const changeRows = hasAggregateChangeColumns
621
+ ? safeAll(
622
+ db,
623
+ `SELECT id, req_id, file_path, intent_summary, files_json, file_count, timestamp
624
+ FROM change_logs
625
+ ORDER BY timestamp DESC, id DESC
626
+ LIMIT 40`,
627
+ )
628
+ : safeAll(
629
+ db,
630
+ `SELECT id, req_id, file_path, intent_summary, NULL AS files_json, 1 AS file_count, timestamp
631
+ FROM change_logs
632
+ ORDER BY timestamp DESC, id DESC
633
+ LIMIT 40`,
634
+ );
635
+
636
+ const pendingRows = safeAll(
637
+ db,
638
+ `SELECT file_path, last_event, updated_at
639
+ FROM pending_changes
640
+ ORDER BY updated_at DESC
641
+ LIMIT 20`,
642
+ );
643
+ const groupedChangeRows = groupChangeLogRows(changeRows);
644
+ const guardRows = safeAll(
645
+ db,
646
+ `SELECT id, tool_name, event_type, severity, title, detail, metadata_json, created_at
647
+ FROM mcp_guard_events
648
+ ORDER BY created_at DESC, id DESC
649
+ LIMIT 80`,
650
+ );
651
+
652
+ const appliedMemory = [
653
+ ...activeRequirements.map((row) => ({
654
+ id: `req-${row.id}`,
655
+ kind: "requirement",
656
+ label: "活跃需求",
657
+ title: cleanDisplayText(row.title, "未命名活跃需求", 96),
658
+ preview: cleanDisplayText(row.context_data || row.title, "暂无可读需求上下文", 220),
659
+ updatedAt: row.updated_at ?? row.created_at,
660
+ level: "hit",
661
+ })),
662
+ ...memoryRows
663
+ .filter((row) => ["project_summary", "decision", "convention", "note", "fix_pattern"].includes(row.kind))
664
+ .slice(0, 18)
665
+ .map((row) => ({
666
+ id: `mem-${row.id}`,
667
+ kind: row.kind,
668
+ label: mapMemoryKind(row.kind),
669
+ title: cleanDisplayText(row.title || mapMemoryKind(row.kind), mapMemoryKind(row.kind), 96),
670
+ preview: cleanDisplayText(row.content, "暂无可读记忆内容", 180),
671
+ filePath: row.file_path,
672
+ updatedAt: row.updated_at,
673
+ metadata: parseMaybeJson(row.metadata_json),
674
+ level: memoryLevelFor(row.kind),
675
+ })),
676
+ ].slice(0, 24);
677
+
678
+ const commandLogs = buildMcpGuardLogs({
679
+ guardRows,
680
+ audit: auditForGuardLogs,
681
+ pendingRows,
682
+ activeRequirements,
683
+ memoryRows,
684
+ dbModifiedAt: dbMain.modifiedAt,
685
+ });
686
+ const memoryMirrorKeys = buildMemoryLogMirrorKeys(memoryRows);
687
+ const changeRowsForMemoryLogs = groupedChangeRows.filter((row) => {
688
+ if (memoryMirrorKeys.changeLogIds.has(Number(row.id))) return false;
689
+ const reqId = numericSetValue(row.req_id);
690
+ if (reqId !== null) {
691
+ const key = `${reqId}|${normalizeForGrouping(row.intent_summary)}`;
692
+ if (memoryMirrorKeys.changeIntentKeys.has(key)) return false;
693
+ }
694
+ return true;
695
+ });
696
+ const requirementRowsForMemoryLogs = recentRequirements.filter((row) => {
697
+ const reqId = numericSetValue(row.id);
698
+ return reqId === null || !memoryMirrorKeys.requirementReqIds.has(reqId);
699
+ });
700
+
701
+ const memoryLogs = [
702
+ ...memoryRows.map((row) => ({
703
+ id: `memlog-${row.id}`,
704
+ at: row.updated_at ?? row.created_at,
705
+ title: memoryLogTitle(row),
706
+ detail: memoryLogDetail(row),
707
+ status: mapMemoryKind(row.kind),
708
+ level: memoryLevelFor(row.kind),
709
+ category: row.kind,
710
+ })),
711
+ ...changeRowsForMemoryLogs.map((row) => ({
712
+ id: `change-${row.id}`,
713
+ at: row.timestamp,
714
+ title: cleanDisplayText(row.intent_summary, "记录改动意图", 120),
715
+ detail: summarizeFiles(row.files),
716
+ status: "改动意图",
717
+ level: "info",
718
+ category: "change_intent",
719
+ })),
720
+ ...requirementRowsForMemoryLogs.map((row) => ({
721
+ id: `requirement-${row.id}`,
722
+ at: row.updated_at ?? row.created_at,
723
+ title: cleanDisplayText(row.title, row.status === "active" ? "活跃需求" : "历史需求", 120),
724
+ detail: row.status === "active" ? "当前活跃需求" : "历史需求",
725
+ status: row.status === "active" ? "需求" : "历史",
726
+ level: row.status === "active" ? "hit" : "muted",
727
+ category: "requirement",
728
+ })),
729
+ ]
730
+ .filter((row) => row.at)
731
+ .sort((a, b) => String(b.at).localeCompare(String(a.at)))
732
+ .slice(0, 80);
733
+
734
+ const hitBase = counts.decisions + counts.conventions + counts.activeRequirements;
735
+ const denominator = Math.max(1, hitBase + counts.pendingChanges);
736
+ const hitRate = Math.max(0, Math.min(100, (hitBase / denominator) * 100));
737
+
738
+ return {
739
+ ...base,
740
+ counts,
741
+ kindCounts,
742
+ appliedMemory,
743
+ commandLogs,
744
+ memoryLogs,
745
+ health: {
746
+ hitRate: Number(hitRate.toFixed(1)),
747
+ conflictCount: 0,
748
+ lastAppliedAt: memoryLogs[0]?.at ?? commandLogs[0]?.at ?? dbMain.modifiedAt,
749
+ watcherHint: "读取本地 SQLite 记忆库",
750
+ },
751
+ };
752
+ });
753
+ }
754
+
755
+ function summarizeProject(project) {
756
+ const memory = readProjectMemory(project);
757
+ return {
758
+ ...project,
759
+ exists: fs.existsSync(project.path),
760
+ memory: {
761
+ dbPresent: memory.dbPresent,
762
+ dbPath: memory.dbPath,
763
+ dbSizeText: memory.dbSizeText,
764
+ dbModifiedAt: memory.dbModifiedAt,
765
+ counts: memory.counts,
766
+ health: memory.health,
767
+ },
768
+ };
769
+ }
770
+
771
+ function addCurrentProjectIfEmpty() {
772
+ const index = readIndex();
773
+ if (index.projects.length > 0) return;
774
+ if (fs.existsSync(path.join(CURRENT_PROJECT_ROOT, ".vectormind", "vectormind.db"))) {
775
+ upsertProject({
776
+ name: "VectorMind-MCP",
777
+ path: CURRENT_PROJECT_ROOT,
778
+ });
779
+ }
780
+ }
781
+
782
+ function boundedInteger(value, fallback, min, max) {
783
+ const parsed = Number(value);
784
+ return Number.isFinite(parsed) ? Math.max(min, Math.min(Math.trunc(parsed), max)) : fallback;
785
+ }
786
+
787
+ async function discoverMemoryProjects(rootPath, options = {}) {
788
+ const root = realDirectoryPath(rootPath);
789
+ const maxDepth = boundedInteger(options.maxDepth, 5, 1, 12);
790
+ const maxDirs = boundedInteger(options.maxDirs, 8000, 100, 50000);
791
+ const found = [];
792
+ const queue = [{ dir: root, depth: 0 }];
793
+ let cursor = 0;
794
+ let scanned = 0;
795
+
796
+ while (cursor < queue.length && scanned < maxDirs) {
797
+ const current = queue[cursor];
798
+ cursor += 1;
799
+ scanned += 1;
800
+
801
+ const dbPath = path.join(current.dir, ".vectormind", "vectormind.db");
802
+ try {
803
+ await fsp.access(dbPath, fs.constants.F_OK);
804
+ found.push(current.dir);
805
+ continue;
806
+ } catch {
807
+ // Continue traversing when this directory does not contain a memory database.
808
+ }
809
+
810
+ if (current.depth >= maxDepth) continue;
811
+
812
+ let entries = [];
813
+ try {
814
+ entries = await fsp.readdir(current.dir, { withFileTypes: true });
815
+ } catch {
816
+ continue;
817
+ }
818
+
819
+ for (const entry of entries) {
820
+ if (!entry.isDirectory()) continue;
821
+ if (SKIP_SCAN_DIRS.has(entry.name)) continue;
822
+ if (entry.name.startsWith("$")) continue;
823
+ queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
824
+ }
825
+ }
826
+
827
+ const unique = [...new Map(found.map((p) => [projectPathIdentity(p), p])).values()];
828
+ return { root, scanned, found: unique };
829
+ }
830
+
831
+ const AUDIT_IGNORED_LIKE_PATTERNS = [
832
+ ".tmp/%",
833
+ "%/.tmp/%",
834
+ ".tmp-%",
835
+ ".tmp-%/%",
836
+ "%/.tmp-%",
837
+ "%/.tmp-%/%",
838
+ "_tmp-%",
839
+ "_tmp-%/%",
840
+ "%/_tmp-%",
841
+ "%/_tmp-%/%",
842
+ "_tmp_%",
843
+ "_tmp_%/%",
844
+ "%/_tmp_%",
845
+ "%/_tmp_%/%",
846
+ "node_modules/%",
847
+ "%/node_modules/%",
848
+ "dist/%",
849
+ "%/dist/%",
850
+ "build/%",
851
+ "%/build/%",
852
+ ".vectormind/%",
853
+ "%/.vectormind/%",
854
+ ];
855
+
856
+ function ignoredPathWhereSql(alias = "") {
857
+ const prefix = alias ? `${alias}.` : "";
858
+ return AUDIT_IGNORED_LIKE_PATTERNS
859
+ .map(() => `LOWER(REPLACE(${prefix}file_path, '\\', '/')) LIKE ?`)
860
+ .join(" OR ");
861
+ }
862
+
863
+ function readMemoryAudit(project) {
864
+ const dbPath = path.join(project.path, ".vectormind", "vectormind.db");
865
+ const dbMain = fileInfo(dbPath);
866
+ const base = {
867
+ ok: true,
868
+ dbPresent: dbMain.exists,
869
+ dbPath,
870
+ checkedAt: new Date().toISOString(),
871
+ needsRepair: false,
872
+ severity: "ok",
873
+ summary: "未发现需要修复的记忆结构问题",
874
+ counts: {},
875
+ schema: {
876
+ hasAggregateChangeColumns: false,
877
+ hasSyncedFileStates: false,
878
+ },
879
+ duplicateChangeLogs: {
880
+ groups: 0,
881
+ rowsInGroups: 0,
882
+ removableRows: 0,
883
+ topGroups: [],
884
+ },
885
+ duplicateChangeIntents: {
886
+ groups: 0,
887
+ rowsInGroups: 0,
888
+ removableRows: 0,
889
+ topGroups: [],
890
+ },
891
+ ignoredPathNoise: {
892
+ changeLogs: 0,
893
+ indexedMemory: 0,
894
+ symbols: 0,
895
+ },
896
+ lastRepair: null,
897
+ };
898
+
899
+ return withDb(project, (db) => {
900
+ if (!db) {
901
+ return {
902
+ ...base,
903
+ severity: "missing",
904
+ summary: "未发现 .vectormind/vectormind.db",
905
+ };
906
+ }
907
+
908
+ const counts = {
909
+ requirements: safeCount(db, "requirements"),
910
+ changeLogs: safeCount(db, "change_logs"),
911
+ memoryItems: safeCount(db, "memory_items"),
912
+ memoryItemsFts: safeCount(db, "memory_items_fts"),
913
+ syncedFileStates: safeCount(db, "synced_file_states"),
914
+ pendingChanges: safeCount(db, "pending_changes"),
915
+ symbols: safeCount(db, "symbols"),
916
+ };
917
+
918
+ const hasAggregateChangeColumns =
919
+ columnExists(db, "change_logs", "files_json") && columnExists(db, "change_logs", "file_count");
920
+ const hasSyncedFileStates = tableExists(db, "synced_file_states");
921
+
922
+ const duplicateChangeLogs = tableExists(db, "change_logs")
923
+ ? safeGet(
924
+ db,
925
+ `SELECT COUNT(*) AS groups, COALESCE(SUM(row_count), 0) AS rowsInGroups
926
+ FROM (
927
+ SELECT COUNT(*) AS row_count
928
+ FROM change_logs
929
+ GROUP BY req_id, timestamp, intent_summary
930
+ HAVING COUNT(*) > 1
931
+ AND COUNT(DISTINCT COALESCE(file_path, '')) > 1
932
+ AND SUM(CASE
933
+ WHEN json_type(CASE WHEN json_valid(files_json) THEN files_json ELSE 'null' END) = 'array' THEN 1
934
+ ELSE 0
935
+ END) = 0
936
+ )`,
937
+ )
938
+ : { groups: 0, rowsInGroups: 0 };
939
+
940
+ const duplicateChangeIntents = tableExists(db, "memory_items")
941
+ ? safeGet(
942
+ db,
943
+ `SELECT COUNT(*) AS groups, COALESCE(SUM(row_count), 0) AS rowsInGroups
944
+ FROM (
945
+ SELECT COUNT(*) AS row_count
946
+ FROM memory_items
947
+ WHERE kind = 'change_intent'
948
+ GROUP BY req_id, content
949
+ HAVING COUNT(*) > 1
950
+ AND COUNT(DISTINCT COALESCE(file_path, '')) > 1
951
+ AND SUM(CASE
952
+ WHEN json_type(
953
+ CASE WHEN json_valid(COALESCE(metadata_json, '{}')) THEN COALESCE(metadata_json, '{}') ELSE '{}' END,
954
+ '$.files'
955
+ ) = 'array' THEN 1
956
+ ELSE 0
957
+ END) = 0
958
+ )`,
959
+ )
960
+ : { groups: 0, rowsInGroups: 0 };
961
+
962
+ const duplicateChangeLogTopGroups = tableExists(db, "change_logs")
963
+ ? safeAll(
964
+ db,
965
+ `WITH grouped AS (
966
+ SELECT req_id, timestamp, intent_summary, COUNT(*) AS row_count, COUNT(DISTINCT COALESCE(file_path, '')) AS file_count
967
+ FROM change_logs
968
+ GROUP BY req_id, timestamp, intent_summary
969
+ HAVING COUNT(*) > 1
970
+ AND COUNT(DISTINCT COALESCE(file_path, '')) > 1
971
+ AND SUM(CASE
972
+ WHEN json_type(CASE WHEN json_valid(files_json) THEN files_json ELSE 'null' END) = 'array' THEN 1
973
+ ELSE 0
974
+ END) = 0
975
+ )
976
+ SELECT
977
+ req_id,
978
+ timestamp,
979
+ substr(intent_summary, 1, 180) AS preview,
980
+ row_count AS rows,
981
+ file_count AS files,
982
+ (
983
+ SELECT GROUP_CONCAT(file_path, ' | ')
984
+ FROM (
985
+ SELECT file_path
986
+ FROM change_logs AS c
987
+ WHERE c.req_id IS grouped.req_id
988
+ AND c.timestamp IS grouped.timestamp
989
+ AND c.intent_summary IS grouped.intent_summary
990
+ ORDER BY c.id ASC
991
+ LIMIT 6
992
+ )
993
+ ) AS sampleFiles
994
+ FROM grouped
995
+ ORDER BY row_count DESC, timestamp DESC
996
+ LIMIT 8`,
997
+ )
998
+ : [];
999
+
1000
+ const duplicateChangeIntentTopGroups = tableExists(db, "memory_items")
1001
+ ? safeAll(
1002
+ db,
1003
+ `WITH grouped AS (
1004
+ SELECT req_id, content, COUNT(*) AS row_count, COUNT(DISTINCT COALESCE(file_path, '')) AS file_count
1005
+ FROM memory_items
1006
+ WHERE kind = 'change_intent'
1007
+ GROUP BY req_id, content
1008
+ HAVING COUNT(*) > 1
1009
+ AND COUNT(DISTINCT COALESCE(file_path, '')) > 1
1010
+ AND SUM(CASE
1011
+ WHEN json_type(
1012
+ CASE WHEN json_valid(COALESCE(metadata_json, '{}')) THEN COALESCE(metadata_json, '{}') ELSE '{}' END,
1013
+ '$.files'
1014
+ ) = 'array' THEN 1
1015
+ ELSE 0
1016
+ END) = 0
1017
+ )
1018
+ SELECT
1019
+ req_id,
1020
+ substr(content, 1, 180) AS preview,
1021
+ row_count AS rows,
1022
+ file_count AS files,
1023
+ (
1024
+ SELECT GROUP_CONCAT(file_path, ' | ')
1025
+ FROM (
1026
+ SELECT file_path
1027
+ FROM memory_items AS m
1028
+ WHERE m.kind = 'change_intent'
1029
+ AND m.req_id IS grouped.req_id
1030
+ AND m.content IS grouped.content
1031
+ ORDER BY m.id ASC
1032
+ LIMIT 6
1033
+ )
1034
+ ) AS sampleFiles
1035
+ FROM grouped
1036
+ ORDER BY row_count DESC
1037
+ LIMIT 8`,
1038
+ )
1039
+ : [];
1040
+
1041
+ const ignoredWhere = ignoredPathWhereSql();
1042
+ const ignoredPathNoise = {
1043
+ changeLogs: tableExists(db, "change_logs")
1044
+ ? safeCount(db, "change_logs", `WHERE file_path IS NOT NULL AND (${ignoredWhere})`, AUDIT_IGNORED_LIKE_PATTERNS)
1045
+ : 0,
1046
+ indexedMemory: tableExists(db, "memory_items")
1047
+ ? safeCount(db, "memory_items", `WHERE file_path IS NOT NULL AND (${ignoredWhere})`, AUDIT_IGNORED_LIKE_PATTERNS)
1048
+ : 0,
1049
+ symbols: tableExists(db, "symbols")
1050
+ ? safeCount(db, "symbols", `WHERE file_path IS NOT NULL AND (${ignoredWhere})`, AUDIT_IGNORED_LIKE_PATTERNS)
1051
+ : 0,
1052
+ };
1053
+
1054
+ const lastRepairRow = tableExists(db, "meta_kv")
1055
+ ? safeGet(
1056
+ db,
1057
+ `SELECT value, updated_at
1058
+ FROM meta_kv
1059
+ WHERE key = 'maintenance:dedupe_change_records:last_run'
1060
+ LIMIT 1`,
1061
+ )
1062
+ : null;
1063
+ const lastRepair = lastRepairRow
1064
+ ? {
1065
+ updatedAt: lastRepairRow.updated_at,
1066
+ value: parseMaybeJson(lastRepairRow.value),
1067
+ }
1068
+ : null;
1069
+
1070
+ const duplicateChangeLogGroups = Number(duplicateChangeLogs?.groups ?? 0);
1071
+ const duplicateChangeLogRows = Number(duplicateChangeLogs?.rowsInGroups ?? 0);
1072
+ const duplicateChangeIntentGroups = Number(duplicateChangeIntents?.groups ?? 0);
1073
+ const duplicateChangeIntentRows = Number(duplicateChangeIntents?.rowsInGroups ?? 0);
1074
+ const schemaNeedsRepair = !hasAggregateChangeColumns || !hasSyncedFileStates;
1075
+ const duplicateNeedsRepair = duplicateChangeLogGroups > 0 || duplicateChangeIntentGroups > 0;
1076
+ const needsRepair = schemaNeedsRepair || duplicateNeedsRepair;
1077
+ const severity = duplicateChangeLogRows > 1000 ? "critical" : duplicateNeedsRepair ? "warn" : schemaNeedsRepair ? "info" : "ok";
1078
+ const summary = duplicateNeedsRepair
1079
+ ? `发现 ${duplicateChangeLogGroups} 组重复改动日志、${duplicateChangeIntentGroups} 组重复改动记忆`
1080
+ : schemaNeedsRepair
1081
+ ? "数据库仍是旧结构,建议执行一次安全迁移"
1082
+ : "记忆结构正常,未发现重复改动日志";
1083
+
1084
+ return {
1085
+ ...base,
1086
+ counts,
1087
+ schema: {
1088
+ hasAggregateChangeColumns,
1089
+ hasSyncedFileStates,
1090
+ },
1091
+ duplicateChangeLogs: {
1092
+ groups: duplicateChangeLogGroups,
1093
+ rowsInGroups: duplicateChangeLogRows,
1094
+ removableRows: Math.max(0, duplicateChangeLogRows - duplicateChangeLogGroups),
1095
+ topGroups: duplicateChangeLogTopGroups,
1096
+ },
1097
+ duplicateChangeIntents: {
1098
+ groups: duplicateChangeIntentGroups,
1099
+ rowsInGroups: duplicateChangeIntentRows,
1100
+ removableRows: Math.max(0, duplicateChangeIntentRows - duplicateChangeIntentGroups),
1101
+ topGroups: duplicateChangeIntentTopGroups,
1102
+ },
1103
+ ignoredPathNoise,
1104
+ lastRepair,
1105
+ needsRepair,
1106
+ severity,
1107
+ summary,
1108
+ };
1109
+ });
1110
+ }
1111
+
1112
+ async function backupSqliteDatabase(dbPath) {
1113
+ const backupDir = path.join(path.dirname(dbPath), "backups");
1114
+ await fsp.mkdir(backupDir, { recursive: true });
1115
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
1116
+ const backupPath = path.join(backupDir, `vectormind-before-admin-repair-${stamp}.db`);
1117
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
1118
+ try {
1119
+ if (typeof db.backup === "function") {
1120
+ await db.backup(backupPath);
1121
+ } else {
1122
+ await fsp.copyFile(dbPath, backupPath);
1123
+ }
1124
+ } finally {
1125
+ db.close();
1126
+ }
1127
+ return backupPath;
1128
+ }
1129
+
1130
+ async function repairProjectMemory(project) {
1131
+ const dbPath = path.join(project.path, ".vectormind", "vectormind.db");
1132
+ if (!fs.existsSync(dbPath)) {
1133
+ const error = new Error("未发现 .vectormind/vectormind.db,无法修复");
1134
+ error.status = 404;
1135
+ throw error;
1136
+ }
1137
+
1138
+ const before = readMemoryAudit(project);
1139
+ const backupPath = await backupSqliteDatabase(dbPath);
1140
+ const runtimePath = path.join(CURRENT_PROJECT_ROOT, "dist", "database-runtime.js");
1141
+ if (!fs.existsSync(runtimePath)) {
1142
+ const error = new Error("核心修复模块尚未构建,请先运行 npm run build");
1143
+ error.status = 500;
1144
+ throw error;
1145
+ }
1146
+
1147
+ const runtimeModule = await import(`${pathToFileURL(runtimePath).href}?t=${Date.now()}`);
1148
+ const runtime = runtimeModule.openDatabaseRuntime(project.path);
1149
+ try {
1150
+ runtime.db.close();
1151
+ } catch {
1152
+ // ignore close errors
1153
+ }
1154
+
1155
+ const after = readMemoryAudit(project);
1156
+ return {
1157
+ ok: true,
1158
+ backupPath,
1159
+ before,
1160
+ after,
1161
+ memory: readProjectMemory(project),
1162
+ };
1163
+ }
1164
+
1165
+ function sendError(res, err) {
1166
+ const status = Number(err?.status ?? 500);
1167
+ res.status(status).json({
1168
+ ok: false,
1169
+ error: err?.message ?? "请求失败",
1170
+ });
1171
+ }
1172
+
1173
+ const app = express();
1174
+ app.disable("x-powered-by");
1175
+ app.use(express.json({ limit: "1mb" }));
1176
+
1177
+ app.use("/api", (req, res, next) => {
1178
+ const adminAuth = evaluateAdminRequest({
1179
+ policy: ADMIN_SECURITY,
1180
+ path: req.path,
1181
+ origin: req.get("origin"),
1182
+ hostHeader: req.get("host"),
1183
+ protocol: req.protocol,
1184
+ remoteAddress: req.socket.remoteAddress,
1185
+ providedToken: req.get(ADMIN_TOKEN_HEADER),
1186
+ });
1187
+ req.adminAuth = adminAuth;
1188
+ if (!adminAuth.originAllowed) {
1189
+ return res.status(403).json({ ok: false, error: "管理面板仅接受同源请求。" });
1190
+ }
1191
+ if (!adminAuth.authorized) {
1192
+ return res.status(403).json({ ok: false, error: "管理面板会话令牌无效。" });
1193
+ }
1194
+ return next();
1195
+ });
1196
+
1197
+ app.get("/api/config", (req, res) => {
1198
+ ensureStorage();
1199
+ const automaticSession = req.adminAuth.exposeSessionToken;
1200
+ const authenticated = automaticSession || req.adminAuth.tokenValid;
1201
+ const config = {
1202
+ ok: true,
1203
+ host: HOST,
1204
+ port: PORT,
1205
+ platform: process.platform,
1206
+ authentication: {
1207
+ mode: ADMIN_SECURITY.mode,
1208
+ authenticated,
1209
+ requiresToken: ADMIN_SECURITY.mode === "explicit",
1210
+ },
1211
+ };
1212
+ if (automaticSession) config.sessionToken = ADMIN_TOKEN;
1213
+ if (authenticated) {
1214
+ Object.assign(config, {
1215
+ storageDir: STORAGE_DIR,
1216
+ indexFile: INDEX_FILE,
1217
+ homeDir: os.homedir(),
1218
+ currentProjectRoot: CURRENT_PROJECT_ROOT,
1219
+ });
1220
+ }
1221
+ res.json(config);
1222
+ });
1223
+
1224
+ app.get("/api/health", (_req, res) => {
1225
+ res.json({
1226
+ ok: true,
1227
+ service: "vectormind-admin-panel",
1228
+ status: "running",
1229
+ port: PORT,
1230
+ time: new Date().toISOString(),
1231
+ });
1232
+ });
1233
+
1234
+ app.get("/api/projects", (_req, res) => {
1235
+ try {
1236
+ addCurrentProjectIfEmpty();
1237
+ const index = readIndex();
1238
+ res.json({
1239
+ ok: true,
1240
+ storage: {
1241
+ dir: STORAGE_DIR,
1242
+ file: INDEX_FILE,
1243
+ updatedAt: index.updatedAt,
1244
+ },
1245
+ projects: index.projects.map(summarizeProject),
1246
+ });
1247
+ } catch (err) {
1248
+ sendError(res, err);
1249
+ }
1250
+ });
1251
+
1252
+ app.post("/api/projects", (req, res) => {
1253
+ try {
1254
+ const record = upsertProject(req.body ?? {});
1255
+ res.status(201).json({ ok: true, project: summarizeProject(record) });
1256
+ } catch (err) {
1257
+ sendError(res, err);
1258
+ }
1259
+ });
1260
+
1261
+ app.patch("/api/projects/:id", (req, res) => {
1262
+ try {
1263
+ const index = readIndex();
1264
+ const idx = index.projects.findIndex((p) => p.id === req.params.id);
1265
+ if (idx < 0) {
1266
+ const error = new Error("项目索引不存在。");
1267
+ error.status = 404;
1268
+ throw error;
1269
+ }
1270
+ const current = index.projects[idx];
1271
+ const nextInput = {
1272
+ id: current.id,
1273
+ name: req.body?.name ?? current.name,
1274
+ path: req.body?.path ?? current.path,
1275
+ };
1276
+ index.projects[idx] = toProjectRecord(nextInput, current);
1277
+ writeIndex(index);
1278
+ res.json({ ok: true, project: summarizeProject(index.projects[idx]) });
1279
+ } catch (err) {
1280
+ sendError(res, err);
1281
+ }
1282
+ });
1283
+
1284
+ app.delete("/api/projects/:id", (req, res) => {
1285
+ try {
1286
+ const index = readIndex();
1287
+ const before = index.projects.length;
1288
+ index.projects = index.projects.filter((p) => p.id !== req.params.id);
1289
+ if (index.projects.length === before) {
1290
+ const error = new Error("项目索引不存在。");
1291
+ error.status = 404;
1292
+ throw error;
1293
+ }
1294
+ writeIndex(index);
1295
+ res.json({ ok: true });
1296
+ } catch (err) {
1297
+ sendError(res, err);
1298
+ }
1299
+ });
1300
+
1301
+ app.get("/api/projects/:id/memory", (req, res) => {
1302
+ try {
1303
+ const index = readIndex();
1304
+ const project = index.projects.find((p) => p.id === req.params.id);
1305
+ if (!project) {
1306
+ const error = new Error("项目索引不存在。");
1307
+ error.status = 404;
1308
+ throw error;
1309
+ }
1310
+ project.lastOpenedAt = new Date().toISOString();
1311
+ project.updatedAt = project.updatedAt ?? project.lastOpenedAt;
1312
+ writeIndex(index);
1313
+ res.json({
1314
+ ok: true,
1315
+ project,
1316
+ memory: readProjectMemory(project),
1317
+ });
1318
+ } catch (err) {
1319
+ sendError(res, err);
1320
+ }
1321
+ });
1322
+
1323
+ app.get("/api/projects/:id/memory/audit", (req, res) => {
1324
+ try {
1325
+ const index = readIndex();
1326
+ const project = index.projects.find((p) => p.id === req.params.id);
1327
+ if (!project) {
1328
+ const error = new Error("项目索引不存在");
1329
+ error.status = 404;
1330
+ throw error;
1331
+ }
1332
+ res.json({ ok: true, audit: readMemoryAudit(project) });
1333
+ } catch (err) {
1334
+ sendError(res, err);
1335
+ }
1336
+ });
1337
+
1338
+ app.post("/api/projects/:id/memory/repair", async (req, res) => {
1339
+ try {
1340
+ const index = readIndex();
1341
+ const project = index.projects.find((p) => p.id === req.params.id);
1342
+ if (!project) {
1343
+ const error = new Error("项目索引不存在");
1344
+ error.status = 404;
1345
+ throw error;
1346
+ }
1347
+ const result = await repairProjectMemory(project);
1348
+ res.json(result);
1349
+ } catch (err) {
1350
+ sendError(res, err);
1351
+ }
1352
+ });
1353
+
1354
+ app.post("/api/projects/discover", async (req, res) => {
1355
+ try {
1356
+ const root = req.body?.root || os.homedir();
1357
+ const result = await discoverMemoryProjects(root, {
1358
+ maxDepth: req.body?.maxDepth,
1359
+ maxDirs: req.body?.maxDirs,
1360
+ });
1361
+ const records = result.found.map((projectPath) => upsertProject({ path: projectPath }));
1362
+ res.json({
1363
+ ok: true,
1364
+ root: result.root,
1365
+ scanned: result.scanned,
1366
+ found: result.found.length,
1367
+ projects: records.map(summarizeProject),
1368
+ });
1369
+ } catch (err) {
1370
+ sendError(res, err);
1371
+ }
1372
+ });
1373
+
1374
+ async function attachFrontend() {
1375
+ const development = process.argv.includes("--development") || process.env.NODE_ENV === "development";
1376
+
1377
+ if (development) {
1378
+ const { createServer } = await import("vite");
1379
+ const vite = await createServer({
1380
+ root: CLIENT_DIR,
1381
+ appType: "spa",
1382
+ server: {
1383
+ middlewareMode: true,
1384
+ hmr: {
1385
+ port: PORT + 1,
1386
+ },
1387
+ },
1388
+ });
1389
+ app.use(vite.middlewares);
1390
+ app.use(async (req, res, next) => {
1391
+ if (req.method !== "GET" || req.path.startsWith("/api")) return next();
1392
+ try {
1393
+ const template = await fsp.readFile(path.join(CLIENT_DIR, "index.html"), "utf8");
1394
+ const html = await vite.transformIndexHtml(req.originalUrl, template);
1395
+ res.status(200).type("html").send(html);
1396
+ } catch (err) {
1397
+ vite.ssrFixStacktrace(err);
1398
+ next(err);
1399
+ }
1400
+ });
1401
+ return;
1402
+ }
1403
+
1404
+ app.use(express.static(DIST_DIR));
1405
+ app.use(async (req, res, next) => {
1406
+ if (req.method !== "GET" || req.path.startsWith("/api")) return next();
1407
+ try {
1408
+ res.sendFile(path.join(DIST_DIR, "index.html"));
1409
+ } catch (err) {
1410
+ next(err);
1411
+ }
1412
+ });
1413
+ }
1414
+
1415
+ ensureStorage();
1416
+ await attachFrontend();
1417
+
1418
+ const server = app.listen(PORT, HOST, () => {
1419
+ console.log(`VectorMind 管理面板运行中: http://${HOST}:${PORT}`);
1420
+ console.log(`认证模式: ${ADMIN_SECURITY.mode === "automatic" ? "回环自动会话" : "显式令牌"}`);
1421
+ console.log(`项目索引文件: ${INDEX_FILE}`);
1422
+ });
1423
+
1424
+ globalThis.__vectormindAdminServer = server;