memory-pulse-mcp-server 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +378 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -270,6 +270,93 @@ function deserializeVector(s) {
|
|
|
270
270
|
return Array.from(buffer);
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
// ../core/dist/encoding.js
|
|
274
|
+
var TYPE_IMPORTANCE = {
|
|
275
|
+
decision: 0.9,
|
|
276
|
+
solution: 0.8,
|
|
277
|
+
error: 0.7,
|
|
278
|
+
config: 0.6,
|
|
279
|
+
code: 0.5,
|
|
280
|
+
session: 0.4
|
|
281
|
+
};
|
|
282
|
+
function getTypeImportance(type) {
|
|
283
|
+
return TYPE_IMPORTANCE[type] ?? 0.5;
|
|
284
|
+
}
|
|
285
|
+
function flattenObject(obj, prefix, depth = 0) {
|
|
286
|
+
if (depth > 3)
|
|
287
|
+
return "";
|
|
288
|
+
const parts = [];
|
|
289
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
290
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
291
|
+
if (value === null || value === void 0)
|
|
292
|
+
continue;
|
|
293
|
+
if (typeof value === "string") {
|
|
294
|
+
if (value.length > 500)
|
|
295
|
+
continue;
|
|
296
|
+
parts.push(key, value);
|
|
297
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
298
|
+
parts.push(key, String(value));
|
|
299
|
+
} else if (Array.isArray(value)) {
|
|
300
|
+
for (const item of value) {
|
|
301
|
+
if (typeof item === "string" && item.length <= 200) {
|
|
302
|
+
parts.push(item);
|
|
303
|
+
} else if (typeof item === "number") {
|
|
304
|
+
parts.push(String(item));
|
|
305
|
+
} else if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
306
|
+
parts.push(flattenObject(item, fullKey, depth + 1));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else if (typeof value === "object") {
|
|
310
|
+
parts.push(flattenObject(value, fullKey, depth + 1));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return parts.filter(Boolean).join(" ");
|
|
314
|
+
}
|
|
315
|
+
function extractTextFromContext(rawContext, maxDepth = 3) {
|
|
316
|
+
const texts = [];
|
|
317
|
+
function walk(obj, depth) {
|
|
318
|
+
if (depth > maxDepth)
|
|
319
|
+
return;
|
|
320
|
+
for (const value of Object.values(obj)) {
|
|
321
|
+
if (value === null || value === void 0)
|
|
322
|
+
continue;
|
|
323
|
+
if (typeof value === "string") {
|
|
324
|
+
if (value.length > 0 && value.length <= 1e3) {
|
|
325
|
+
texts.push(value);
|
|
326
|
+
}
|
|
327
|
+
} else if (typeof value === "number") {
|
|
328
|
+
texts.push(String(value));
|
|
329
|
+
} else if (Array.isArray(value)) {
|
|
330
|
+
for (const item of value) {
|
|
331
|
+
if (typeof item === "string" && item.length <= 500) {
|
|
332
|
+
texts.push(item);
|
|
333
|
+
} else if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
334
|
+
walk(item, depth + 1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else if (typeof value === "object") {
|
|
338
|
+
walk(value, depth + 1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
walk(rawContext, 0);
|
|
343
|
+
return texts.join(" ");
|
|
344
|
+
}
|
|
345
|
+
function encodeMemoryText(params) {
|
|
346
|
+
const parts = [params.summary];
|
|
347
|
+
const dataText = flattenObject(params.data);
|
|
348
|
+
if (dataText) {
|
|
349
|
+
parts.push(dataText);
|
|
350
|
+
}
|
|
351
|
+
if (params.rawContext) {
|
|
352
|
+
const contextText = extractTextFromContext(params.rawContext);
|
|
353
|
+
if (contextText) {
|
|
354
|
+
parts.push(contextText);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return parts.filter(Boolean).join(" ");
|
|
358
|
+
}
|
|
359
|
+
|
|
273
360
|
// ../core/dist/keywords.js
|
|
274
361
|
var CHINESE_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
275
362
|
"\u7684",
|
|
@@ -544,7 +631,40 @@ function getTopKeywords(words, limit = 30) {
|
|
|
544
631
|
}
|
|
545
632
|
return Array.from(frequency.entries()).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([word]) => word);
|
|
546
633
|
}
|
|
547
|
-
function
|
|
634
|
+
function extractFromData(data, maxDepth = 2) {
|
|
635
|
+
const keywords = [];
|
|
636
|
+
function walk(obj, depth) {
|
|
637
|
+
if (depth > maxDepth)
|
|
638
|
+
return;
|
|
639
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
640
|
+
if (["id", "createdAt", "updatedAt", "timestamp"].includes(key))
|
|
641
|
+
continue;
|
|
642
|
+
if (key.length >= 2)
|
|
643
|
+
keywords.push(key);
|
|
644
|
+
if (typeof value === "string" && value.length >= 1 && value.length <= 500)
|
|
645
|
+
keywords.push(value);
|
|
646
|
+
if (typeof value === "number" && isFinite(value))
|
|
647
|
+
keywords.push(String(value));
|
|
648
|
+
if (Array.isArray(value)) {
|
|
649
|
+
for (const item of value) {
|
|
650
|
+
if (typeof item === "string" && item.length >= 1 && item.length <= 200)
|
|
651
|
+
keywords.push(item);
|
|
652
|
+
if (typeof item === "number" && isFinite(item))
|
|
653
|
+
keywords.push(String(item));
|
|
654
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
655
|
+
walk(item, depth + 1);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
660
|
+
walk(value, depth + 1);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
walk(data, 0);
|
|
665
|
+
return keywords;
|
|
666
|
+
}
|
|
667
|
+
function extractKeywords(content, rawContext, data) {
|
|
548
668
|
const allKeywords = [];
|
|
549
669
|
const contentText = content || "";
|
|
550
670
|
allKeywords.push(...basicTokenize(contentText));
|
|
@@ -554,6 +674,15 @@ function extractKeywords(content, rawContext) {
|
|
|
554
674
|
allKeywords.push(...basicTokenize(contextText));
|
|
555
675
|
allKeywords.push(...extractSpecialPatterns(contextText));
|
|
556
676
|
}
|
|
677
|
+
if (data && Object.keys(data).length > 0) {
|
|
678
|
+
const dataKeywords = extractFromData(data);
|
|
679
|
+
for (const kw of dataKeywords) {
|
|
680
|
+
allKeywords.push(...basicTokenize(kw));
|
|
681
|
+
allKeywords.push(...extractSpecialPatterns(kw));
|
|
682
|
+
if (kw.length >= 2)
|
|
683
|
+
allKeywords.push(kw);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
557
686
|
const topKeywords = getTopKeywords(allKeywords, 30);
|
|
558
687
|
const specialKeywords = extractSpecialPatterns(contentText + " " + extractTextFromObject(rawContext));
|
|
559
688
|
const uniqueSpecial = [...new Set(specialKeywords)].slice(0, 10);
|
|
@@ -2100,6 +2229,13 @@ var PostgreSQLStorage = class {
|
|
|
2100
2229
|
const id = `mem_${nanoid2()}`;
|
|
2101
2230
|
const timestamp = /* @__PURE__ */ new Date();
|
|
2102
2231
|
const memoryType = params.type || MemoryType.CODE;
|
|
2232
|
+
const keywords = extractKeywords(params.content, params.rawContext, params.data);
|
|
2233
|
+
const fullText = encodeMemoryText({
|
|
2234
|
+
summary: params.content,
|
|
2235
|
+
data: params.data,
|
|
2236
|
+
rawContext: params.rawContext
|
|
2237
|
+
});
|
|
2238
|
+
const importance = getTypeImportance(memoryType);
|
|
2103
2239
|
await this.prisma.memory.create({
|
|
2104
2240
|
data: {
|
|
2105
2241
|
id,
|
|
@@ -2109,17 +2245,28 @@ var PostgreSQLStorage = class {
|
|
|
2109
2245
|
type: memoryType,
|
|
2110
2246
|
tags: params.tags || [],
|
|
2111
2247
|
summary: params.content,
|
|
2112
|
-
// data 存储关键结构化数据(精简版),context 存储完整原始数据(完整版)
|
|
2113
2248
|
data: params.data,
|
|
2114
2249
|
replaces: params.relations?.replaces || [],
|
|
2115
2250
|
relatedTo: params.relations?.relatedTo || [],
|
|
2116
2251
|
impacts: params.relations?.impacts || [],
|
|
2117
2252
|
derivedFrom: params.relations?.derivedFrom || null,
|
|
2118
2253
|
context: params.rawContext,
|
|
2119
|
-
keywords: params.tags || [],
|
|
2120
|
-
fullText
|
|
2254
|
+
keywords: [.../* @__PURE__ */ new Set([...params.tags || [], ...keywords])],
|
|
2255
|
+
fullText,
|
|
2256
|
+
importance
|
|
2121
2257
|
}
|
|
2122
2258
|
});
|
|
2259
|
+
if (params.relations) {
|
|
2260
|
+
const referencedIds = [
|
|
2261
|
+
...params.relations.relatedTo || [],
|
|
2262
|
+
...params.relations.replaces || [],
|
|
2263
|
+
...params.relations.impacts || [],
|
|
2264
|
+
...params.relations.derivedFrom ? [params.relations.derivedFrom] : []
|
|
2265
|
+
];
|
|
2266
|
+
if (referencedIds.length > 0) {
|
|
2267
|
+
await this.onMemoryReferenced(referencedIds);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2123
2270
|
if (params.relations) {
|
|
2124
2271
|
const relationRecords = [];
|
|
2125
2272
|
if (params.relations.relatedTo) {
|
|
@@ -2150,7 +2297,7 @@ var PostgreSQLStorage = class {
|
|
|
2150
2297
|
if (this.cache) {
|
|
2151
2298
|
this.cache.invalidateProject(params.projectId);
|
|
2152
2299
|
}
|
|
2153
|
-
await this.autoRelate(id, params.projectId,
|
|
2300
|
+
await this.autoRelate(id, params.projectId, keywords);
|
|
2154
2301
|
return { id, success: true };
|
|
2155
2302
|
}
|
|
2156
2303
|
/**
|
|
@@ -2160,8 +2307,17 @@ var PostgreSQLStorage = class {
|
|
|
2160
2307
|
const id = `mem_${nanoid2()}`;
|
|
2161
2308
|
const timestamp = /* @__PURE__ */ new Date();
|
|
2162
2309
|
const summary = `[\u51B3\u7B56] ${params.question}`;
|
|
2163
|
-
const
|
|
2164
|
-
const
|
|
2310
|
+
const decisionData = { chosen: params.chosen, reason: params.reason };
|
|
2311
|
+
const rawContext = {
|
|
2312
|
+
question: params.question,
|
|
2313
|
+
analysis: params.analysis,
|
|
2314
|
+
options: params.options,
|
|
2315
|
+
chosen: params.chosen,
|
|
2316
|
+
reason: params.reason
|
|
2317
|
+
};
|
|
2318
|
+
const fullText = encodeMemoryText({ summary, data: decisionData, rawContext });
|
|
2319
|
+
const keywords = extractKeywords(summary, rawContext, decisionData);
|
|
2320
|
+
const importance = getTypeImportance(MemoryType.DECISION);
|
|
2165
2321
|
await this.prisma.memory.create({
|
|
2166
2322
|
data: {
|
|
2167
2323
|
id,
|
|
@@ -2171,20 +2327,15 @@ var PostgreSQLStorage = class {
|
|
|
2171
2327
|
type: MemoryType.DECISION,
|
|
2172
2328
|
tags: params.tags || [],
|
|
2173
2329
|
summary,
|
|
2174
|
-
data:
|
|
2330
|
+
data: decisionData,
|
|
2175
2331
|
replaces: params.relations?.replaces || [],
|
|
2176
2332
|
relatedTo: params.relations?.relatedTo || [],
|
|
2177
2333
|
impacts: params.relations?.impacts || [],
|
|
2178
2334
|
derivedFrom: params.relations?.derivedFrom || null,
|
|
2179
|
-
context:
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
chosen: params.chosen,
|
|
2184
|
-
reason: params.reason
|
|
2185
|
-
},
|
|
2186
|
-
keywords,
|
|
2187
|
-
fullText
|
|
2335
|
+
context: rawContext,
|
|
2336
|
+
keywords: [.../* @__PURE__ */ new Set([...params.tags || [], ...keywords])],
|
|
2337
|
+
fullText,
|
|
2338
|
+
importance
|
|
2188
2339
|
}
|
|
2189
2340
|
});
|
|
2190
2341
|
if (params.relations) {
|
|
@@ -2227,8 +2378,17 @@ var PostgreSQLStorage = class {
|
|
|
2227
2378
|
const id = `mem_${nanoid2()}`;
|
|
2228
2379
|
const timestamp = /* @__PURE__ */ new Date();
|
|
2229
2380
|
const summary = `[\u65B9\u6848] ${params.problem}`;
|
|
2230
|
-
const
|
|
2231
|
-
const
|
|
2381
|
+
const solutionData = { problem: params.problem, solution: params.solution, ...params.artifacts || {} };
|
|
2382
|
+
const rawContext = {
|
|
2383
|
+
problem: params.problem,
|
|
2384
|
+
rootCause: params.rootCause,
|
|
2385
|
+
solution: params.solution,
|
|
2386
|
+
prevention: params.prevention,
|
|
2387
|
+
relatedIssues: params.relatedIssues
|
|
2388
|
+
};
|
|
2389
|
+
const fullText = encodeMemoryText({ summary, data: solutionData, rawContext });
|
|
2390
|
+
const keywords = extractKeywords(summary, rawContext, solutionData);
|
|
2391
|
+
const importance = getTypeImportance(MemoryType.SOLUTION);
|
|
2232
2392
|
await this.prisma.memory.create({
|
|
2233
2393
|
data: {
|
|
2234
2394
|
id,
|
|
@@ -2238,20 +2398,15 @@ var PostgreSQLStorage = class {
|
|
|
2238
2398
|
type: MemoryType.SOLUTION,
|
|
2239
2399
|
tags: params.tags || [],
|
|
2240
2400
|
summary,
|
|
2241
|
-
data:
|
|
2401
|
+
data: solutionData,
|
|
2242
2402
|
replaces: params.relations?.replaces || [],
|
|
2243
2403
|
relatedTo: params.relations?.relatedTo || [],
|
|
2244
2404
|
impacts: params.relations?.impacts || [],
|
|
2245
2405
|
derivedFrom: params.relations?.derivedFrom || null,
|
|
2246
|
-
context:
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
prevention: params.prevention,
|
|
2251
|
-
relatedIssues: params.relatedIssues
|
|
2252
|
-
},
|
|
2253
|
-
keywords,
|
|
2254
|
-
fullText
|
|
2406
|
+
context: rawContext,
|
|
2407
|
+
keywords: [.../* @__PURE__ */ new Set([...params.tags || [], ...keywords])],
|
|
2408
|
+
fullText,
|
|
2409
|
+
importance
|
|
2255
2410
|
}
|
|
2256
2411
|
});
|
|
2257
2412
|
if (params.relations) {
|
|
@@ -2294,8 +2449,20 @@ var PostgreSQLStorage = class {
|
|
|
2294
2449
|
const id = `mem_${nanoid2()}`;
|
|
2295
2450
|
const timestamp = /* @__PURE__ */ new Date();
|
|
2296
2451
|
const summary = `[\u4F1A\u8BDD] ${params.summary}`;
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2452
|
+
const sessionData = {
|
|
2453
|
+
decisions: params.decisions || [],
|
|
2454
|
+
nextSteps: params.nextSteps || [],
|
|
2455
|
+
unfinishedTasks: params.unfinishedTasks || []
|
|
2456
|
+
};
|
|
2457
|
+
const rawContext = {
|
|
2458
|
+
summary: params.summary,
|
|
2459
|
+
decisions: params.decisions,
|
|
2460
|
+
unfinishedTasks: params.unfinishedTasks,
|
|
2461
|
+
nextSteps: params.nextSteps
|
|
2462
|
+
};
|
|
2463
|
+
const fullText = encodeMemoryText({ summary, data: sessionData, rawContext });
|
|
2464
|
+
const keywords = extractKeywords(summary, rawContext, sessionData);
|
|
2465
|
+
const importance = getTypeImportance(MemoryType.SESSION);
|
|
2299
2466
|
await this.prisma.memory.create({
|
|
2300
2467
|
data: {
|
|
2301
2468
|
id,
|
|
@@ -2305,19 +2472,15 @@ var PostgreSQLStorage = class {
|
|
|
2305
2472
|
type: MemoryType.SESSION,
|
|
2306
2473
|
tags: [],
|
|
2307
2474
|
summary,
|
|
2308
|
-
data:
|
|
2475
|
+
data: sessionData,
|
|
2309
2476
|
replaces: params.relations?.replaces || [],
|
|
2310
2477
|
relatedTo: params.relations?.relatedTo || [],
|
|
2311
2478
|
impacts: params.relations?.impacts || [],
|
|
2312
2479
|
derivedFrom: params.relations?.derivedFrom || null,
|
|
2313
|
-
context:
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
nextSteps: params.nextSteps
|
|
2318
|
-
},
|
|
2319
|
-
keywords,
|
|
2320
|
-
fullText
|
|
2480
|
+
context: rawContext,
|
|
2481
|
+
keywords: [...new Set(keywords)],
|
|
2482
|
+
fullText,
|
|
2483
|
+
importance
|
|
2321
2484
|
}
|
|
2322
2485
|
});
|
|
2323
2486
|
if (params.relations) {
|
|
@@ -2354,12 +2517,12 @@ var PostgreSQLStorage = class {
|
|
|
2354
2517
|
return { id, success: true };
|
|
2355
2518
|
}
|
|
2356
2519
|
/**
|
|
2357
|
-
*
|
|
2520
|
+
* 检索记忆(仿人脑:线索触发 + 扩散激活 + 相关性排序)
|
|
2358
2521
|
*
|
|
2359
|
-
*
|
|
2360
|
-
*
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
2522
|
+
* 1. tsvector 全文搜索(AND 优先,降级 OR)
|
|
2523
|
+
* 2. 综合评分 = 文本相关性 * 重要性 * 时间衰减 * 频率增强
|
|
2524
|
+
* 3. 扩散激活:关联记忆参与混合排序
|
|
2525
|
+
* 4. 用进废退:命中时更新 access_count
|
|
2363
2526
|
*/
|
|
2364
2527
|
async recall(filters) {
|
|
2365
2528
|
const startTime = Date.now();
|
|
@@ -2374,26 +2537,38 @@ var PostgreSQLStorage = class {
|
|
|
2374
2537
|
const offset = filters.offset || 0;
|
|
2375
2538
|
const expandRelations = filters.expandRelations !== false;
|
|
2376
2539
|
const relationDepth = filters.relationDepth || 1;
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
let
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
if (
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2540
|
+
const dbStartTime = Date.now();
|
|
2541
|
+
let results = [];
|
|
2542
|
+
let total = 0;
|
|
2543
|
+
if (filters.query && filters.query.trim()) {
|
|
2544
|
+
const tsvResult = await this.tsvectorSearch(filters, limit, offset, "and");
|
|
2545
|
+
if (tsvResult.memories.length > 0) {
|
|
2546
|
+
results = tsvResult.memories;
|
|
2547
|
+
total = tsvResult.total;
|
|
2548
|
+
} else {
|
|
2549
|
+
const orResult = await this.tsvectorSearch(filters, limit, offset, "or");
|
|
2550
|
+
results = orResult.memories;
|
|
2551
|
+
total = orResult.total;
|
|
2386
2552
|
}
|
|
2387
2553
|
if (results.length === 0) {
|
|
2388
|
-
const
|
|
2389
|
-
results =
|
|
2390
|
-
total =
|
|
2554
|
+
const fallback = await this.multiDimensionSearch(filters, limit, offset);
|
|
2555
|
+
results = fallback.memories;
|
|
2556
|
+
total = fallback.total;
|
|
2391
2557
|
}
|
|
2558
|
+
} else {
|
|
2559
|
+
const searchResult = await this.multiDimensionSearch(filters, limit, offset);
|
|
2560
|
+
results = searchResult.memories;
|
|
2561
|
+
total = searchResult.total;
|
|
2392
2562
|
}
|
|
2393
2563
|
let relatedMemories;
|
|
2394
2564
|
if (expandRelations && results.length > 0) {
|
|
2395
2565
|
relatedMemories = await this.expandRelatedMemories(results, relationDepth);
|
|
2396
2566
|
}
|
|
2567
|
+
if (results.length > 0) {
|
|
2568
|
+
const hitIds = results.map((m) => m.meta.id);
|
|
2569
|
+
this.onMemoryAccessed(hitIds).catch(() => {
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2397
2572
|
const dbEndTime = Date.now();
|
|
2398
2573
|
const took = Date.now() - startTime;
|
|
2399
2574
|
const dbTime = dbEndTime - dbStartTime;
|
|
@@ -2415,6 +2590,122 @@ var PostgreSQLStorage = class {
|
|
|
2415
2590
|
}
|
|
2416
2591
|
return result;
|
|
2417
2592
|
}
|
|
2593
|
+
/**
|
|
2594
|
+
* tsvector 全文搜索(核心检索引擎)
|
|
2595
|
+
* 使用 PostgreSQL ts_rank_cd + 重要性 + 时间衰减 + 频率增强 综合评分
|
|
2596
|
+
*/
|
|
2597
|
+
async tsvectorSearch(filters, limit, offset, mode) {
|
|
2598
|
+
const query = filters.query?.trim();
|
|
2599
|
+
if (!query)
|
|
2600
|
+
return { memories: [], total: 0 };
|
|
2601
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
2602
|
+
const operator = mode === "and" ? " & " : " | ";
|
|
2603
|
+
const tsTerms = [];
|
|
2604
|
+
for (const word of words) {
|
|
2605
|
+
if (/[\u4e00-\u9fa5]/.test(word)) {
|
|
2606
|
+
for (const char of word) {
|
|
2607
|
+
if (/[\u4e00-\u9fa5]/.test(char))
|
|
2608
|
+
tsTerms.push(char);
|
|
2609
|
+
}
|
|
2610
|
+
} else {
|
|
2611
|
+
tsTerms.push(word.toLowerCase());
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
if (tsTerms.length === 0)
|
|
2615
|
+
return { memories: [], total: 0 };
|
|
2616
|
+
const tsqueryStr = tsTerms.join(operator);
|
|
2617
|
+
let projectFilter = "";
|
|
2618
|
+
const params = [tsqueryStr, limit, offset];
|
|
2619
|
+
let paramIdx = 4;
|
|
2620
|
+
if (filters.projectIds && filters.projectIds.length > 0) {
|
|
2621
|
+
projectFilter = `AND "projectId" = ANY($${paramIdx})`;
|
|
2622
|
+
params.push(filters.projectIds);
|
|
2623
|
+
paramIdx++;
|
|
2624
|
+
} else if (filters.projectId) {
|
|
2625
|
+
projectFilter = `AND "projectId" = $${paramIdx}`;
|
|
2626
|
+
params.push(filters.projectId);
|
|
2627
|
+
paramIdx++;
|
|
2628
|
+
}
|
|
2629
|
+
let typeFilter = "";
|
|
2630
|
+
if (filters.type) {
|
|
2631
|
+
typeFilter = `AND type = $${paramIdx}`;
|
|
2632
|
+
params.push(filters.type);
|
|
2633
|
+
paramIdx++;
|
|
2634
|
+
}
|
|
2635
|
+
const sql = `
|
|
2636
|
+
SELECT *,
|
|
2637
|
+
ts_rank_cd(tsv, to_tsquery('simple', $1)) AS text_score,
|
|
2638
|
+
(
|
|
2639
|
+
ts_rank_cd(tsv, to_tsquery('simple', $1))
|
|
2640
|
+
* COALESCE(importance, 0.5)
|
|
2641
|
+
* EXP(-0.693 * EXTRACT(EPOCH FROM (NOW() - timestamp)) / (30.0 * 86400.0))
|
|
2642
|
+
* (1.0 + LN(1.0 + COALESCE(access_count, 0) + COALESCE(reference_count, 0)) * 0.2)
|
|
2643
|
+
) AS brain_score
|
|
2644
|
+
FROM memories
|
|
2645
|
+
WHERE tsv @@ to_tsquery('simple', $1)
|
|
2646
|
+
${projectFilter}
|
|
2647
|
+
${typeFilter}
|
|
2648
|
+
ORDER BY brain_score DESC
|
|
2649
|
+
LIMIT $2 OFFSET $3
|
|
2650
|
+
`;
|
|
2651
|
+
const countSql = `
|
|
2652
|
+
SELECT COUNT(*) as total
|
|
2653
|
+
FROM memories
|
|
2654
|
+
WHERE tsv @@ to_tsquery('simple', $1)
|
|
2655
|
+
${projectFilter}
|
|
2656
|
+
${typeFilter}
|
|
2657
|
+
`;
|
|
2658
|
+
try {
|
|
2659
|
+
const [rows, countResult] = await Promise.all([
|
|
2660
|
+
this.prisma.$queryRawUnsafe(sql, ...params),
|
|
2661
|
+
this.prisma.$queryRawUnsafe(countSql, ...params.slice(0, 1).concat(params.slice(3)))
|
|
2662
|
+
]);
|
|
2663
|
+
const memories = rows.map((row) => this.rawRowToMemory(row));
|
|
2664
|
+
const total = Number(countResult[0]?.total || 0);
|
|
2665
|
+
return { memories, total };
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
console.warn("tsvector search failed, falling back:", error.message);
|
|
2668
|
+
return { memories: [], total: 0 };
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* 将原始 SQL 查询行转换为 Memory 对象
|
|
2673
|
+
* 与 rowToMemory 类似,但处理的是原始 SQL 返回的 snake_case 字段
|
|
2674
|
+
*/
|
|
2675
|
+
rawRowToMemory(row) {
|
|
2676
|
+
return {
|
|
2677
|
+
meta: {
|
|
2678
|
+
id: row.id,
|
|
2679
|
+
projectId: row.projectId,
|
|
2680
|
+
sessionId: row.sessionId,
|
|
2681
|
+
timestamp: row.timestamp instanceof Date ? row.timestamp.toISOString() : row.timestamp,
|
|
2682
|
+
type: row.type,
|
|
2683
|
+
tags: row.tags || [],
|
|
2684
|
+
version: row.version,
|
|
2685
|
+
importance: row.importance ?? 0.5,
|
|
2686
|
+
accessCount: row.access_count ?? 0,
|
|
2687
|
+
referenceCount: row.reference_count ?? 0,
|
|
2688
|
+
lastAccessedAt: row.last_accessed_at ? row.last_accessed_at instanceof Date ? row.last_accessed_at.toISOString() : row.last_accessed_at : void 0
|
|
2689
|
+
},
|
|
2690
|
+
content: {
|
|
2691
|
+
summary: row.summary,
|
|
2692
|
+
data: (typeof row.data === "string" ? JSON.parse(row.data) : row.data) || {},
|
|
2693
|
+
rawContext: (typeof row.context === "string" ? JSON.parse(row.context) : row.context) || void 0
|
|
2694
|
+
},
|
|
2695
|
+
relations: {
|
|
2696
|
+
replaces: row.replaces || void 0,
|
|
2697
|
+
relatedTo: row.relatedTo || row.relatedto || void 0,
|
|
2698
|
+
impacts: row.impacts || void 0,
|
|
2699
|
+
derivedFrom: row.derivedFrom || row.derivedfrom || void 0
|
|
2700
|
+
},
|
|
2701
|
+
searchable: {
|
|
2702
|
+
keywords: row.keywords || [],
|
|
2703
|
+
fullText: row.fullText || row.fulltext || ""
|
|
2704
|
+
},
|
|
2705
|
+
createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : row.createdat || row.createdAt || "",
|
|
2706
|
+
updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : row.updatedat || row.updatedAt || ""
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2418
2709
|
/**
|
|
2419
2710
|
* 扩展关联记忆
|
|
2420
2711
|
*
|
|
@@ -2822,7 +3113,11 @@ var PostgreSQLStorage = class {
|
|
|
2822
3113
|
timestamp: row.timestamp.toISOString(),
|
|
2823
3114
|
type: row.type,
|
|
2824
3115
|
tags: row.tags || [],
|
|
2825
|
-
version: row.version
|
|
3116
|
+
version: row.version,
|
|
3117
|
+
importance: row.importance ?? 0.5,
|
|
3118
|
+
accessCount: row.accessCount ?? 0,
|
|
3119
|
+
referenceCount: row.referenceCount ?? 0,
|
|
3120
|
+
lastAccessedAt: row.lastAccessedAt?.toISOString() ?? void 0
|
|
2826
3121
|
},
|
|
2827
3122
|
content: {
|
|
2828
3123
|
summary: row.summary,
|
|
@@ -2892,6 +3187,31 @@ var PostgreSQLStorage = class {
|
|
|
2892
3187
|
/**
|
|
2893
3188
|
* 关闭数据库连接
|
|
2894
3189
|
*/
|
|
3190
|
+
// ========== 用进废退:访问/引用计数 ==========
|
|
3191
|
+
/**
|
|
3192
|
+
* 批量更新访问计数(查询命中时异步调用)
|
|
3193
|
+
*/
|
|
3194
|
+
async onMemoryAccessed(memoryIds) {
|
|
3195
|
+
if (memoryIds.length === 0)
|
|
3196
|
+
return;
|
|
3197
|
+
try {
|
|
3198
|
+
await this.prisma.$executeRawUnsafe(`UPDATE memories SET access_count = access_count + 1, last_accessed_at = NOW() WHERE id = ANY($1)`, memoryIds);
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
console.warn("\u66F4\u65B0\u8BBF\u95EE\u8BA1\u6570\u5931\u8D25:", error.message);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* 批量更新引用计数(被其他记忆引用时调用)
|
|
3205
|
+
*/
|
|
3206
|
+
async onMemoryReferenced(memoryIds) {
|
|
3207
|
+
if (memoryIds.length === 0)
|
|
3208
|
+
return;
|
|
3209
|
+
try {
|
|
3210
|
+
await this.prisma.$executeRawUnsafe(`UPDATE memories SET reference_count = reference_count + 1 WHERE id = ANY($1)`, memoryIds);
|
|
3211
|
+
} catch (error) {
|
|
3212
|
+
console.warn("\u66F4\u65B0\u5F15\u7528\u8BA1\u6570\u5931\u8D25:", error.message);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
2895
3215
|
async close() {
|
|
2896
3216
|
await this.prisma.$disconnect();
|
|
2897
3217
|
}
|