@coreyuan/vector-mind 1.0.54 → 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.
- package/README.md +99 -72
- package/admin-panel/README.md +77 -0
- package/admin-panel/dist/client/assets/index-Bzyjdhh2.css +1 -0
- package/admin-panel/dist/client/assets/index-Ph73ewzt.js +9 -0
- package/admin-panel/dist/client/index.html +14 -0
- package/admin-panel/package.json +25 -0
- package/admin-panel/server/index.mjs +1424 -0
- package/admin-panel/server/security.mjs +117 -0
- package/admin-panel/server/security.test.mjs +113 -0
- package/admin-panel/server/storage.mjs +49 -0
- package/admin-panel/server/storage.test.mjs +36 -0
- package/dist/checkpoint-snapshot.d.ts +56 -0
- package/dist/checkpoint-snapshot.js +411 -0
- package/dist/checkpoint-snapshot.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/context-governance.d.ts +56 -0
- package/dist/context-governance.js +176 -0
- package/dist/context-governance.js.map +1 -0
- package/dist/database-runtime.d.ts +3 -1
- package/dist/database-runtime.js +232 -127
- package/dist/database-runtime.js.map +1 -1
- package/dist/development-warnings.d.ts +2 -1
- package/dist/development-warnings.js +90 -20
- package/dist/development-warnings.js.map +1 -1
- package/dist/file-indexing.d.ts +2 -1
- package/dist/file-indexing.js +127 -47
- package/dist/file-indexing.js.map +1 -1
- package/dist/grep.js +4 -0
- package/dist/grep.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/large-file-split.d.ts +52 -8
- package/dist/large-file-split.js +290 -76
- package/dist/large-file-split.js.map +1 -1
- package/dist/memory-maintenance/compaction.js +3 -2
- package/dist/memory-maintenance/compaction.js.map +1 -1
- package/dist/memory-maintenance/metrics.d.ts +1 -1
- package/dist/memory-maintenance/metrics.js +11 -3
- package/dist/memory-maintenance/metrics.js.map +1 -1
- package/dist/memory-maintenance/purge.js +1 -1
- package/dist/memory-maintenance/purge.js.map +1 -1
- package/dist/memory-maintenance/runner.d.ts +1 -0
- package/dist/memory-maintenance/runner.js +15 -3
- package/dist/memory-maintenance/runner.js.map +1 -1
- package/dist/memory-maintenance/types.d.ts +1 -0
- package/dist/memory-mutations.d.ts +3 -3
- package/dist/memory-mutations.js +109 -43
- package/dist/memory-mutations.js.map +1 -1
- package/dist/memory-recall.js +16 -1
- package/dist/memory-recall.js.map +1 -1
- package/dist/operation-scope.js +42 -12
- package/dist/operation-scope.js.map +1 -1
- package/dist/path-containment.d.ts +5 -0
- package/dist/path-containment.js +64 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-filters.d.ts +1 -1
- package/dist/path-filters.js +7 -6
- package/dist/path-filters.js.map +1 -1
- package/dist/pending-changes.d.ts +5 -0
- package/dist/pending-changes.js +26 -16
- package/dist/pending-changes.js.map +1 -1
- package/dist/project-files.d.ts +1 -0
- package/dist/project-files.js +19 -10
- package/dist/project-files.js.map +1 -1
- package/dist/root.d.ts +4 -1
- package/dist/root.js +34 -4
- package/dist/root.js.map +1 -1
- package/dist/rtk-integrity.d.ts +5 -0
- package/dist/rtk-integrity.js +35 -0
- package/dist/rtk-integrity.js.map +1 -0
- package/dist/rtk-shim.js +22 -9
- package/dist/rtk-shim.js.map +1 -1
- package/dist/rtk-tools.d.ts +8 -0
- package/dist/rtk-tools.js +59 -42
- package/dist/rtk-tools.js.map +1 -1
- package/dist/server-instructions.js +8 -88
- package/dist/server-instructions.js.map +1 -1
- package/dist/tool-catalog.d.ts +2 -0
- package/dist/tool-catalog.js +76 -23
- package/dist/tool-catalog.js.map +1 -1
- package/dist/tool-handlers/context-recovery.js +79 -54
- package/dist/tool-handlers/context-recovery.js.map +1 -1
- package/dist/tool-handlers/large-files.js +600 -44
- package/dist/tool-handlers/large-files.js.map +1 -1
- package/dist/tool-handlers/memory-diagnostics.js +240 -45
- package/dist/tool-handlers/memory-diagnostics.js.map +1 -1
- package/dist/tool-handlers/memory.js +167 -22
- package/dist/tool-handlers/memory.js.map +1 -1
- package/dist/tool-handlers/operations.js +28 -8
- package/dist/tool-handlers/operations.js.map +1 -1
- package/dist/tool-handlers/requirement-status.d.ts +5 -0
- package/dist/tool-handlers/requirement-status.js +267 -0
- package/dist/tool-handlers/requirement-status.js.map +1 -0
- package/dist/tool-handlers/requirements.js +786 -97
- package/dist/tool-handlers/requirements.js.map +1 -1
- package/dist/tool-handlers.d.ts +4 -0
- package/dist/tool-handlers.js +183 -10
- package/dist/tool-handlers.js.map +1 -1
- package/dist/tool-output.d.ts +32 -1
- package/dist/tool-output.js +31 -12
- package/dist/tool-output.js.map +1 -1
- package/dist/tool-schemas.d.ts +235 -0
- package/dist/tool-schemas.js +98 -26
- package/dist/tool-schemas.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/docs/capability-matrix.md +23 -17
- package/package.json +19 -6
- package/skills/vector-mind-autopilot/SKILL.md +82 -23
- package/skills/vector-mind-autopilot/references/claude-project-instructions.md +14 -11
- 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;
|