@ppagent/memory 0.1.0 → 0.1.2
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.d.ts +231 -122
- package/dist/index.js +410 -160
- package/llms.txt +845 -845
- package/package.json +49 -48
package/dist/index.js
CHANGED
|
@@ -45,7 +45,10 @@ function resolveConfig(config) {
|
|
|
45
45
|
knowledgeGraphAnchorTopK: config.knowledgeGraphAnchorTopK ?? 3,
|
|
46
46
|
knowledgeGraphHopLimit: config.knowledgeGraphHopLimit ?? 10,
|
|
47
47
|
graphExtractConcurrency: config.graphExtractConcurrency ?? 2,
|
|
48
|
-
graphBuildTimeoutMs: config.graphBuildTimeoutMs ?? 12e4
|
|
48
|
+
graphBuildTimeoutMs: config.graphBuildTimeoutMs ?? 12e4,
|
|
49
|
+
autoOptimizeOnInit: config.autoOptimizeOnInit ?? true,
|
|
50
|
+
optimizeVersionRetentionMs: config.optimizeVersionRetentionMs ?? 0,
|
|
51
|
+
restoreConcurrency: config.restoreConcurrency ?? 8
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -165,6 +168,16 @@ import { GrafeoDB } from "@grafeo-db/js";
|
|
|
165
168
|
import { get_encoding } from "@dqbd/tiktoken";
|
|
166
169
|
|
|
167
170
|
// src/llm/http.ts
|
|
171
|
+
function normalizeFetchError(err, timeoutMs, errorLabel) {
|
|
172
|
+
if (isAbortError(err)) {
|
|
173
|
+
return new Error(`${errorLabel} request timed out after ${timeoutMs}ms`, { cause: err });
|
|
174
|
+
}
|
|
175
|
+
if (err instanceof Error) return err;
|
|
176
|
+
return new Error(String(err));
|
|
177
|
+
}
|
|
178
|
+
function isAbortError(err) {
|
|
179
|
+
return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
|
|
180
|
+
}
|
|
168
181
|
function backoffDelay(attempt) {
|
|
169
182
|
return Math.min(1e3 * 2 ** attempt, 8e3);
|
|
170
183
|
}
|
|
@@ -173,7 +186,9 @@ async function postJsonWithRetry(url, headers, body, opts, errorLabel = "HTTP")
|
|
|
173
186
|
let lastErr;
|
|
174
187
|
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
175
188
|
const controller = new AbortController();
|
|
176
|
-
const timer = setTimeout(() =>
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
controller.abort(new Error(`${errorLabel} request timed out after ${opts.timeoutMs}ms`));
|
|
191
|
+
}, opts.timeoutMs);
|
|
177
192
|
let res;
|
|
178
193
|
try {
|
|
179
194
|
res = await fetch(url, {
|
|
@@ -184,12 +199,12 @@ async function postJsonWithRetry(url, headers, body, opts, errorLabel = "HTTP")
|
|
|
184
199
|
});
|
|
185
200
|
} catch (err) {
|
|
186
201
|
clearTimeout(timer);
|
|
187
|
-
lastErr = err;
|
|
202
|
+
lastErr = normalizeFetchError(err, opts.timeoutMs, errorLabel);
|
|
188
203
|
if (attempt < opts.maxRetries) {
|
|
189
204
|
await sleep(backoffDelay(attempt));
|
|
190
205
|
continue;
|
|
191
206
|
}
|
|
192
|
-
throw
|
|
207
|
+
throw lastErr;
|
|
193
208
|
}
|
|
194
209
|
clearTimeout(timer);
|
|
195
210
|
if (res.ok) {
|
|
@@ -281,8 +296,24 @@ function cosineSimilarity(a, b) {
|
|
|
281
296
|
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
282
297
|
}
|
|
283
298
|
|
|
299
|
+
// src/page.util.ts
|
|
300
|
+
function normalizePage(page) {
|
|
301
|
+
const offset = Math.max(0, Math.floor(page.offset ?? 0));
|
|
302
|
+
const limit = page.limit != null && page.limit > 0 ? Math.floor(page.limit) : 0;
|
|
303
|
+
return { offset, limit };
|
|
304
|
+
}
|
|
305
|
+
function paginate(items, page) {
|
|
306
|
+
const total = items.length;
|
|
307
|
+
const { offset, limit } = normalizePage(page);
|
|
308
|
+
const slice = limit > 0 ? items.slice(offset, offset + limit) : items.slice(offset);
|
|
309
|
+
return { items: slice, total, offset, limit };
|
|
310
|
+
}
|
|
311
|
+
function cmpStr(a, b) {
|
|
312
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
284
315
|
// src/db/grafeo.service.ts
|
|
285
|
-
var GrafeoService = class {
|
|
316
|
+
var GrafeoService = class _GrafeoService {
|
|
286
317
|
config;
|
|
287
318
|
db;
|
|
288
319
|
constructor(config) {
|
|
@@ -459,16 +490,17 @@ var GrafeoService = class {
|
|
|
459
490
|
const r = row;
|
|
460
491
|
return {
|
|
461
492
|
type: "relation",
|
|
462
|
-
content: `${r["
|
|
493
|
+
content: `${r["fromName"]} -[${r["relType"]}]-> ${r["toName"]}`,
|
|
463
494
|
score: 1,
|
|
464
|
-
meta: { happenedAt: r["
|
|
495
|
+
meta: { happenedAt: r["happenedAt"] }
|
|
465
496
|
};
|
|
466
497
|
});
|
|
498
|
+
const RETURN_CLAUSE = "RETURN a.name AS fromName, type(r) AS relType, b.name AS toName, r.happenedAt AS happenedAt";
|
|
467
499
|
try {
|
|
468
500
|
const result = await this.db.execute(
|
|
469
501
|
`MATCH (a:Entity {name: $name, userId: $userId, chatId: $chatId, kind: $kind})-[r]->(b)
|
|
470
502
|
WHERE r.chatId = $chatId AND r.userId = $userId
|
|
471
|
-
|
|
503
|
+
${RETURN_CLAUSE} LIMIT $limit`,
|
|
472
504
|
{ name: entityName, userId, chatId, kind, limit }
|
|
473
505
|
);
|
|
474
506
|
return mapRows(result.toArray());
|
|
@@ -477,7 +509,7 @@ var GrafeoService = class {
|
|
|
477
509
|
try {
|
|
478
510
|
const result = await this.db.execute(
|
|
479
511
|
`MATCH (a:Entity {name: $name, userId: $userId, chatId: $chatId, kind: $kind})-[r]->(b)
|
|
480
|
-
|
|
512
|
+
${RETURN_CLAUSE} LIMIT $limit`,
|
|
481
513
|
{ name: entityName, userId, chatId, kind, limit }
|
|
482
514
|
);
|
|
483
515
|
return mapRows(result.toArray());
|
|
@@ -500,56 +532,176 @@ var GrafeoService = class {
|
|
|
500
532
|
}
|
|
501
533
|
}
|
|
502
534
|
// ── 管理面板用:全量读取实体 / 关系 ────────────────────────────────────────────────
|
|
503
|
-
/**
|
|
504
|
-
|
|
535
|
+
/**
|
|
536
|
+
* 构建按域过滤的内联属性子句(如 ` {userId: $userId, chatId: $chatId}`)。
|
|
537
|
+
* 在 LIMIT 之前生效,避免「先截断再过滤」导致结果不足。
|
|
538
|
+
*/
|
|
539
|
+
buildScopeMatch(filter, params) {
|
|
540
|
+
const props = [];
|
|
541
|
+
if (filter?.userId) {
|
|
542
|
+
props.push("userId: $userId");
|
|
543
|
+
params.userId = filter.userId;
|
|
544
|
+
}
|
|
545
|
+
if (filter?.chatId) {
|
|
546
|
+
props.push("chatId: $chatId");
|
|
547
|
+
params.chatId = filter.chatId;
|
|
548
|
+
}
|
|
549
|
+
return props.length > 0 ? ` {${props.join(", ")}}` : "";
|
|
550
|
+
}
|
|
551
|
+
// 关系查询的统一 RETURN 子句:用别名(AS)取列,避免函数表达式 type(r)/id(r) 的默认列名
|
|
552
|
+
// 被归一为 "type(...)"/"id(...)"(按 r["type(r)"] 取会得到 undefined)。id(r) 同时用于 ORDER BY。
|
|
553
|
+
static RELATION_RETURN = `RETURN a.name AS fromName, b.name AS toName, type(r) AS relType,
|
|
554
|
+
r.happenedAt AS happenedAt, r.userId AS userId, r.chatId AS chatId,
|
|
555
|
+
r.sessionId AS sessionId, id(r) AS rid`;
|
|
556
|
+
/** 节点 → Entity 映射(剔除体积大的 embedding 字段)*/
|
|
557
|
+
nodeToEntity(node) {
|
|
558
|
+
const props = node.properties();
|
|
559
|
+
const { embedding: _embedding, name, userId, chatId, sessionId, ...rest } = props;
|
|
560
|
+
const type = (node.labels ?? []).filter((l) => l !== "Entity").join(", ") || "\u672A\u77E5";
|
|
561
|
+
return {
|
|
562
|
+
name: String(name ?? ""),
|
|
563
|
+
type,
|
|
564
|
+
meta: {
|
|
565
|
+
userId: String(userId ?? ""),
|
|
566
|
+
chatId: String(chatId ?? ""),
|
|
567
|
+
sessionId: String(sessionId ?? ""),
|
|
568
|
+
...rest
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/** 关系查询结果行 → Relation 映射(依赖 RELATION_RETURN 的别名列)*/
|
|
573
|
+
rowToRelation(row) {
|
|
574
|
+
const r = row;
|
|
575
|
+
return {
|
|
576
|
+
from: String(r["fromName"] ?? ""),
|
|
577
|
+
to: String(r["toName"] ?? ""),
|
|
578
|
+
type: String(r["relType"] ?? ""),
|
|
579
|
+
happenedAt: r["happenedAt"] != null ? Number(r["happenedAt"]) : void 0,
|
|
580
|
+
meta: {
|
|
581
|
+
userId: String(r["userId"] ?? ""),
|
|
582
|
+
chatId: String(r["chatId"] ?? ""),
|
|
583
|
+
sessionId: String(r["sessionId"] ?? "")
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/** 统计实体数(可选按域过滤),用于分页 total */
|
|
588
|
+
async countEntities(filter) {
|
|
505
589
|
try {
|
|
506
|
-
const
|
|
590
|
+
const params = {};
|
|
591
|
+
const scope = this.buildScopeMatch(filter, params);
|
|
592
|
+
const res = await this.db.execute(`MATCH (n:Entity${scope}) RETURN count(n) AS cnt`, params);
|
|
593
|
+
return Number(res.toArray()[0]?.cnt ?? 0);
|
|
594
|
+
} catch {
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** 统计关系数(可选按域过滤),用于分页 total */
|
|
599
|
+
async countRelations(filter) {
|
|
600
|
+
try {
|
|
601
|
+
const params = {};
|
|
602
|
+
const scope = this.buildScopeMatch(filter, params);
|
|
603
|
+
const res = await this.db.execute(
|
|
604
|
+
`MATCH (a:Entity${scope})-[r]->(b:Entity) RETURN count(r) AS cnt`,
|
|
605
|
+
params
|
|
606
|
+
);
|
|
607
|
+
return Number(res.toArray()[0]?.cnt ?? 0);
|
|
608
|
+
} catch {
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* 全量读取实体节点(管理面板/知识图谱用,剔除体积大的 embedding 字段)。
|
|
614
|
+
* 可选按 userId / chatId 过滤(在节点属性上匹配,LIMIT 前生效)。
|
|
615
|
+
* 按节点内部 id 升序,确保「同样条件 → 同样顺序」。
|
|
616
|
+
* 注意:此方法带安全上限 limit,超出会截断;需要准确 total 与翻页请用 pageEntities。
|
|
617
|
+
*/
|
|
618
|
+
async getAllEntities(filter, limit = 1e3) {
|
|
619
|
+
try {
|
|
620
|
+
const params = { limit };
|
|
621
|
+
const scope = this.buildScopeMatch(filter, params);
|
|
622
|
+
const result = await this.db.execute(`MATCH (n:Entity${scope}) RETURN n LIMIT $limit`, params);
|
|
507
623
|
const nodes = result.nodes();
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const { embedding: _embedding, name, userId, chatId, sessionId, ...rest } = props;
|
|
511
|
-
const type = (node.labels ?? []).filter((l) => l !== "Entity").join(", ") || "\u672A\u77E5";
|
|
512
|
-
return {
|
|
513
|
-
name: String(name ?? ""),
|
|
514
|
-
type,
|
|
515
|
-
meta: {
|
|
516
|
-
userId: String(userId ?? ""),
|
|
517
|
-
chatId: String(chatId ?? ""),
|
|
518
|
-
sessionId: String(sessionId ?? ""),
|
|
519
|
-
...rest
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
});
|
|
624
|
+
nodes.sort((a, b) => a.id - b.id);
|
|
625
|
+
return nodes.map((node) => this.nodeToEntity(node));
|
|
523
626
|
} catch {
|
|
524
627
|
return [];
|
|
525
628
|
}
|
|
526
629
|
}
|
|
527
|
-
/**
|
|
528
|
-
|
|
630
|
+
/**
|
|
631
|
+
* 分页读取实体节点:把 ORDER BY id(n) + SKIP/LIMIT 下推到查询层,并用 count 取准确 total。
|
|
632
|
+
* 不受 getAllEntities 安全上限约束。返回当前页 items 与过滤后的 total。
|
|
633
|
+
*/
|
|
634
|
+
async pageEntities(filter, page) {
|
|
635
|
+
const { offset, limit } = normalizePage(page);
|
|
636
|
+
try {
|
|
637
|
+
const total = await this.countEntities(filter);
|
|
638
|
+
const effLimit = limit > 0 ? limit : Math.max(0, total - offset);
|
|
639
|
+
if (effLimit <= 0) return { items: [], total };
|
|
640
|
+
const params = {};
|
|
641
|
+
const scope = this.buildScopeMatch(filter, params);
|
|
642
|
+
params.offset = offset;
|
|
643
|
+
params.limit = effLimit;
|
|
644
|
+
const res = await this.db.execute(
|
|
645
|
+
`MATCH (n:Entity${scope}) RETURN id(n) AS nid ORDER BY nid SKIP $offset LIMIT $limit`,
|
|
646
|
+
params
|
|
647
|
+
);
|
|
648
|
+
const items = [];
|
|
649
|
+
for (const row of res.toArray()) {
|
|
650
|
+
const nid = Number(row.nid);
|
|
651
|
+
const node = this.db.getNode(nid);
|
|
652
|
+
if (node) items.push(this.nodeToEntity(node));
|
|
653
|
+
}
|
|
654
|
+
return { items, total };
|
|
655
|
+
} catch {
|
|
656
|
+
return { items: [], total: 0 };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* 全量读取关系边(管理面板/知识图谱用)。可选按 userId / chatId 过滤:在源实体节点的域属性上匹配
|
|
661
|
+
*(关系两端与边均同域,故按源节点过滤即可,且节点内联属性匹配在各 grafeo 版本均可靠)。
|
|
662
|
+
* 按 id(r) 升序确保顺序确定。
|
|
663
|
+
* 注意:此方法带安全上限 limit,超出会截断;需要准确 total 与翻页请用 pageRelations。
|
|
664
|
+
*/
|
|
665
|
+
async getAllRelations(filter, limit = 2e3) {
|
|
529
666
|
try {
|
|
667
|
+
const params = { limit };
|
|
668
|
+
const scope = this.buildScopeMatch(filter, params);
|
|
530
669
|
const result = await this.db.execute(
|
|
531
|
-
`MATCH (a:Entity)-[r]->(b:Entity)
|
|
532
|
-
|
|
533
|
-
|
|
670
|
+
`MATCH (a:Entity${scope})-[r]->(b:Entity)
|
|
671
|
+
${_GrafeoService.RELATION_RETURN}
|
|
672
|
+
ORDER BY rid LIMIT $limit`,
|
|
673
|
+
params
|
|
534
674
|
);
|
|
535
|
-
return result.toArray().map((row) =>
|
|
536
|
-
const r = row;
|
|
537
|
-
return {
|
|
538
|
-
from: String(r["a.name"] ?? ""),
|
|
539
|
-
to: String(r["b.name"] ?? ""),
|
|
540
|
-
type: String(r["type(r)"] ?? ""),
|
|
541
|
-
happenedAt: r["r.happenedAt"] != null ? Number(r["r.happenedAt"]) : void 0,
|
|
542
|
-
meta: {
|
|
543
|
-
userId: String(r["r.userId"] ?? ""),
|
|
544
|
-
chatId: String(r["r.chatId"] ?? ""),
|
|
545
|
-
sessionId: String(r["r.sessionId"] ?? "")
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
});
|
|
675
|
+
return result.toArray().map((row) => this.rowToRelation(row));
|
|
549
676
|
} catch {
|
|
550
677
|
return [];
|
|
551
678
|
}
|
|
552
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* 分页读取关系边:把 ORDER BY id(r) + SKIP/LIMIT 下推到查询层,并用 count 取准确 total。
|
|
682
|
+
* 不受 getAllRelations 安全上限约束。返回当前页 items 与过滤后的 total。
|
|
683
|
+
*/
|
|
684
|
+
async pageRelations(filter, page) {
|
|
685
|
+
const { offset, limit } = normalizePage(page);
|
|
686
|
+
try {
|
|
687
|
+
const total = await this.countRelations(filter);
|
|
688
|
+
const effLimit = limit > 0 ? limit : Math.max(0, total - offset);
|
|
689
|
+
if (effLimit <= 0) return { items: [], total };
|
|
690
|
+
const params = {};
|
|
691
|
+
const scope = this.buildScopeMatch(filter, params);
|
|
692
|
+
params.offset = offset;
|
|
693
|
+
params.limit = effLimit;
|
|
694
|
+
const res = await this.db.execute(
|
|
695
|
+
`MATCH (a:Entity${scope})-[r]->(b:Entity)
|
|
696
|
+
${_GrafeoService.RELATION_RETURN}
|
|
697
|
+
ORDER BY rid SKIP $offset LIMIT $limit`,
|
|
698
|
+
params
|
|
699
|
+
);
|
|
700
|
+
return { items: res.toArray().map((row) => this.rowToRelation(row)), total };
|
|
701
|
+
} catch {
|
|
702
|
+
return { items: [], total: 0 };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
553
705
|
close() {
|
|
554
706
|
try {
|
|
555
707
|
this.db.close();
|
|
@@ -863,6 +1015,24 @@ function safeParseObject(s) {
|
|
|
863
1015
|
return {};
|
|
864
1016
|
}
|
|
865
1017
|
}
|
|
1018
|
+
var MESSAGES_INDEXES = [
|
|
1019
|
+
{ column: "message_id" },
|
|
1020
|
+
{ column: "session_id" },
|
|
1021
|
+
{ column: "content", fts: true },
|
|
1022
|
+
{ column: "metadata", fts: true }
|
|
1023
|
+
// 支持 metadata 内容全文检索
|
|
1024
|
+
];
|
|
1025
|
+
var TOPICS_INDEXES = [
|
|
1026
|
+
{ column: "chat_id" },
|
|
1027
|
+
{ column: "detail", fts: true }
|
|
1028
|
+
];
|
|
1029
|
+
var SESSIONS_INDEXES = [{ column: "session_id" }];
|
|
1030
|
+
var DOCUMENTS_INDEXES = [{ column: "doc_id" }];
|
|
1031
|
+
var CHUNKS_INDEXES = [
|
|
1032
|
+
{ column: "doc_id" },
|
|
1033
|
+
{ column: "content", fts: true }
|
|
1034
|
+
// 知识片段混合检索
|
|
1035
|
+
];
|
|
866
1036
|
var LanceService = class {
|
|
867
1037
|
config;
|
|
868
1038
|
conn;
|
|
@@ -875,6 +1045,7 @@ var LanceService = class {
|
|
|
875
1045
|
// Scalar indexes cannot be created on empty tables (LanceDB btree limitation).
|
|
876
1046
|
// These flags defer creation to the first insert.
|
|
877
1047
|
isNewMessagesTable = false;
|
|
1048
|
+
isNewTopicsTable = false;
|
|
878
1049
|
isNewSessionsTable = false;
|
|
879
1050
|
isNewDocumentsTable = false;
|
|
880
1051
|
isNewChunksTable = false;
|
|
@@ -888,7 +1059,6 @@ var LanceService = class {
|
|
|
888
1059
|
if (existingTables.includes(MESSAGES_TABLE)) {
|
|
889
1060
|
this.messagesTable = await this.conn.openTable(MESSAGES_TABLE);
|
|
890
1061
|
await this._ensurePartsColumn();
|
|
891
|
-
await this.ensureScalarIndex(this.messagesTable, "message_id");
|
|
892
1062
|
} else {
|
|
893
1063
|
this.messagesTable = await this.conn.createEmptyTable(
|
|
894
1064
|
MESSAGES_TABLE,
|
|
@@ -904,6 +1074,7 @@ var LanceService = class {
|
|
|
904
1074
|
TOPICS_TABLE,
|
|
905
1075
|
topicsSchema(dim)
|
|
906
1076
|
);
|
|
1077
|
+
this.isNewTopicsTable = true;
|
|
907
1078
|
}
|
|
908
1079
|
if (existingTables.includes(FACTS_TABLE)) {
|
|
909
1080
|
this.factsTable = await this.conn.openTable(FACTS_TABLE);
|
|
@@ -912,43 +1083,83 @@ var LanceService = class {
|
|
|
912
1083
|
}
|
|
913
1084
|
if (existingTables.includes(SESSIONS_TABLE)) {
|
|
914
1085
|
this.sessionsTable = await this.conn.openTable(SESSIONS_TABLE);
|
|
915
|
-
await this.ensureScalarIndex(this.sessionsTable, "session_id");
|
|
916
1086
|
} else {
|
|
917
1087
|
this.sessionsTable = await this.conn.createEmptyTable(SESSIONS_TABLE, sessionsSchema());
|
|
918
1088
|
this.isNewSessionsTable = true;
|
|
919
1089
|
}
|
|
920
1090
|
if (existingTables.includes(DOCUMENTS_TABLE)) {
|
|
921
1091
|
this.documentsTable = await this.conn.openTable(DOCUMENTS_TABLE);
|
|
922
|
-
await this.ensureScalarIndex(this.documentsTable, "doc_id");
|
|
923
1092
|
} else {
|
|
924
1093
|
this.documentsTable = await this.conn.createEmptyTable(DOCUMENTS_TABLE, documentsSchema(dim));
|
|
925
1094
|
this.isNewDocumentsTable = true;
|
|
926
1095
|
}
|
|
927
1096
|
if (existingTables.includes(CHUNKS_TABLE)) {
|
|
928
1097
|
this.chunksTable = await this.conn.openTable(CHUNKS_TABLE);
|
|
929
|
-
await this.ensureScalarIndex(this.chunksTable, "doc_id");
|
|
930
1098
|
} else {
|
|
931
1099
|
this.chunksTable = await this.conn.createEmptyTable(CHUNKS_TABLE, chunksSchema(dim));
|
|
932
1100
|
this.isNewChunksTable = true;
|
|
933
1101
|
}
|
|
934
|
-
await this.
|
|
1102
|
+
await this.ensureIndexes(this.messagesTable, MESSAGES_INDEXES);
|
|
1103
|
+
await this.ensureIndexes(this.topicsTable, TOPICS_INDEXES);
|
|
1104
|
+
await this.ensureIndexes(this.sessionsTable, SESSIONS_INDEXES);
|
|
1105
|
+
await this.ensureIndexes(this.documentsTable, DOCUMENTS_INDEXES);
|
|
1106
|
+
await this.ensureIndexes(this.chunksTable, CHUNKS_INDEXES);
|
|
935
1107
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1108
|
+
/**
|
|
1109
|
+
* 按需补齐索引:先经 listIndices 判存在,缺失的列才 createIndex(且 replace:false)。
|
|
1110
|
+
* createIndex 默认 replace:true 会在每次启动时全量重建索引并提交新表版本——
|
|
1111
|
+
* 这正是历史上「启动越来越慢 + _versions 目录膨胀」的根源,绝不可回退到无条件 createIndex。
|
|
1112
|
+
*/
|
|
1113
|
+
async ensureIndexes(table, specs) {
|
|
1114
|
+
let indexed;
|
|
1115
|
+
try {
|
|
1116
|
+
indexed = new Set((await table.listIndices()).flatMap((i) => i.columns));
|
|
1117
|
+
} catch {
|
|
1118
|
+
indexed = /* @__PURE__ */ new Set();
|
|
1119
|
+
}
|
|
1120
|
+
for (const { column, fts } of specs) {
|
|
1121
|
+
if (indexed.has(column)) continue;
|
|
946
1122
|
try {
|
|
947
|
-
await table.createIndex(
|
|
1123
|
+
await table.createIndex(
|
|
1124
|
+
column,
|
|
1125
|
+
fts ? { config: lancedb.Index.fts(), replace: false } : { replace: false }
|
|
1126
|
+
);
|
|
948
1127
|
} catch {
|
|
949
1128
|
}
|
|
950
1129
|
}
|
|
951
1130
|
}
|
|
1131
|
+
/**
|
|
1132
|
+
* 存储压实:逐表执行碎片合并 + 清理 retentionMs 之前的历史版本。
|
|
1133
|
+
* 嵌入式场景下 LanceDB 不会自动做这件事,长期运行后版本/碎片无限累积会显著拖慢启动与查询。
|
|
1134
|
+
*/
|
|
1135
|
+
async optimizeStorage(retentionMs = 0) {
|
|
1136
|
+
const cutoff = new Date(Date.now() - Math.max(0, retentionMs));
|
|
1137
|
+
const targets = [
|
|
1138
|
+
[MESSAGES_TABLE, this.messagesTable],
|
|
1139
|
+
[TOPICS_TABLE, this.topicsTable],
|
|
1140
|
+
[FACTS_TABLE, this.factsTable],
|
|
1141
|
+
[SESSIONS_TABLE, this.sessionsTable],
|
|
1142
|
+
[DOCUMENTS_TABLE, this.documentsTable],
|
|
1143
|
+
[CHUNKS_TABLE, this.chunksTable]
|
|
1144
|
+
];
|
|
1145
|
+
const results = [];
|
|
1146
|
+
for (const [name, table] of targets) {
|
|
1147
|
+
try {
|
|
1148
|
+
const stats = await table.optimize({ cleanupOlderThan: cutoff });
|
|
1149
|
+
results.push({
|
|
1150
|
+
table: name,
|
|
1151
|
+
fragmentsRemoved: stats.compaction.fragmentsRemoved,
|
|
1152
|
+
fragmentsAdded: stats.compaction.fragmentsAdded,
|
|
1153
|
+
filesRemoved: stats.compaction.filesRemoved,
|
|
1154
|
+
oldVersionsRemoved: stats.prune.oldVersionsRemoved,
|
|
1155
|
+
bytesRemoved: Number(stats.prune.bytesRemoved)
|
|
1156
|
+
});
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
console.warn(`[LanceService] optimize table "${name}" failed:`, err);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return results;
|
|
1162
|
+
}
|
|
952
1163
|
/**
|
|
953
1164
|
* 为存量 messages 表添加 parts 列(如果缺失)。
|
|
954
1165
|
* LanceDB 0.14+ 支持 addColumns;旧版本会 throw,此时 rowToMessage 的 ?? "[]" 兜底。
|
|
@@ -985,24 +1196,20 @@ var LanceService = class {
|
|
|
985
1196
|
} catch {
|
|
986
1197
|
}
|
|
987
1198
|
}
|
|
988
|
-
// LanceDB btree scalar indexes require at least one row.
|
|
989
|
-
// For existing tables (opened in init) this is safe; for new tables we defer via isNew*Table flags.
|
|
990
|
-
async ensureScalarIndex(table, column) {
|
|
991
|
-
try {
|
|
992
|
-
await table.createIndex(column);
|
|
993
|
-
} catch {
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
1199
|
async addMessages(messages) {
|
|
997
1200
|
if (messages.length === 0) return;
|
|
998
1201
|
await this.messagesTable.add(messages.map(messageToRow));
|
|
999
1202
|
if (this.isNewMessagesTable) {
|
|
1000
|
-
await this.
|
|
1203
|
+
await this.ensureIndexes(this.messagesTable, MESSAGES_INDEXES);
|
|
1001
1204
|
this.isNewMessagesTable = false;
|
|
1002
1205
|
}
|
|
1003
1206
|
}
|
|
1004
1207
|
async addTopic(topic) {
|
|
1005
1208
|
await this.topicsTable.add([topicToRow(topic)]);
|
|
1209
|
+
if (this.isNewTopicsTable) {
|
|
1210
|
+
await this.ensureIndexes(this.topicsTable, TOPICS_INDEXES);
|
|
1211
|
+
this.isNewTopicsTable = false;
|
|
1212
|
+
}
|
|
1006
1213
|
}
|
|
1007
1214
|
async updateTopicRecallCount(summaryId, count) {
|
|
1008
1215
|
await this.topicsTable.update({
|
|
@@ -1051,6 +1258,15 @@ var LanceService = class {
|
|
|
1051
1258
|
return [];
|
|
1052
1259
|
}
|
|
1053
1260
|
}
|
|
1261
|
+
/** 取某会话的全部消息(按 createdAt 升序),供管理面板分页切片使用 */
|
|
1262
|
+
async getAllMessagesBySession(sessionId) {
|
|
1263
|
+
try {
|
|
1264
|
+
const rows = await this.messagesTable.query().where(eqFilter("session_id", sessionId)).toArray();
|
|
1265
|
+
return rows.sort((a, b) => Number(a.created_at) - Number(b.created_at) || cmpStr(a.message_id, b.message_id)).map(rowToMessage);
|
|
1266
|
+
} catch {
|
|
1267
|
+
return [];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1054
1270
|
async searchMessages(vector, filter, limit = 10) {
|
|
1055
1271
|
const q = this.messagesTable.vectorSearch(vector).limit(limit);
|
|
1056
1272
|
if (filter) q.where(filter);
|
|
@@ -1082,8 +1298,7 @@ var LanceService = class {
|
|
|
1082
1298
|
const total = rows.length;
|
|
1083
1299
|
return rows.map((r, idx) => ({
|
|
1084
1300
|
row: r,
|
|
1085
|
-
|
|
1086
|
-
score: 1 - idx / total + Math.log1p(Number(r.recall_count)) * 0.1
|
|
1301
|
+
score: total - idx + Math.log1p(Number(r.recall_count))
|
|
1087
1302
|
})).sort((a, b) => b.score - a.score).map((x) => rowToTopic(x.row));
|
|
1088
1303
|
}
|
|
1089
1304
|
} catch {
|
|
@@ -1127,7 +1342,7 @@ var LanceService = class {
|
|
|
1127
1342
|
async insertSession(session) {
|
|
1128
1343
|
await this.sessionsTable.add([sessionToRow(session)]);
|
|
1129
1344
|
if (this.isNewSessionsTable) {
|
|
1130
|
-
await this.
|
|
1345
|
+
await this.ensureIndexes(this.sessionsTable, SESSIONS_INDEXES);
|
|
1131
1346
|
this.isNewSessionsTable = false;
|
|
1132
1347
|
}
|
|
1133
1348
|
}
|
|
@@ -1151,7 +1366,7 @@ var LanceService = class {
|
|
|
1151
1366
|
async getAllTopics() {
|
|
1152
1367
|
try {
|
|
1153
1368
|
const rows = await this.topicsTable.query().toArray();
|
|
1154
|
-
return rows.sort((a, b) => Number(b.end_time) - Number(a.end_time)).map(rowToTopic);
|
|
1369
|
+
return rows.sort((a, b) => Number(b.end_time) - Number(a.end_time) || cmpStr(a.summary_id, b.summary_id)).map(rowToTopic);
|
|
1155
1370
|
} catch {
|
|
1156
1371
|
return [];
|
|
1157
1372
|
}
|
|
@@ -1233,7 +1448,7 @@ var LanceService = class {
|
|
|
1233
1448
|
async addDocument(doc) {
|
|
1234
1449
|
await this.documentsTable.add([documentToRow(doc)]);
|
|
1235
1450
|
if (this.isNewDocumentsTable) {
|
|
1236
|
-
await this.
|
|
1451
|
+
await this.ensureIndexes(this.documentsTable, DOCUMENTS_INDEXES);
|
|
1237
1452
|
this.isNewDocumentsTable = false;
|
|
1238
1453
|
}
|
|
1239
1454
|
}
|
|
@@ -1241,7 +1456,7 @@ var LanceService = class {
|
|
|
1241
1456
|
if (chunks.length === 0) return;
|
|
1242
1457
|
await this.chunksTable.add(chunks.map(chunkToRow));
|
|
1243
1458
|
if (this.isNewChunksTable) {
|
|
1244
|
-
await this.
|
|
1459
|
+
await this.ensureIndexes(this.chunksTable, CHUNKS_INDEXES);
|
|
1245
1460
|
this.isNewChunksTable = false;
|
|
1246
1461
|
}
|
|
1247
1462
|
}
|
|
@@ -1324,7 +1539,7 @@ var LanceService = class {
|
|
|
1324
1539
|
async getAllDocuments() {
|
|
1325
1540
|
try {
|
|
1326
1541
|
const rows = await this.documentsTable.query().toArray();
|
|
1327
|
-
return rows.sort((a, b) => Number(b.updated_at) - Number(a.updated_at)).map(rowToDocument);
|
|
1542
|
+
return rows.sort((a, b) => Number(b.updated_at) - Number(a.updated_at) || cmpStr(a.doc_id, b.doc_id)).map(rowToDocument);
|
|
1328
1543
|
} catch {
|
|
1329
1544
|
return [];
|
|
1330
1545
|
}
|
|
@@ -1364,6 +1579,40 @@ var LanceService = class {
|
|
|
1364
1579
|
}
|
|
1365
1580
|
};
|
|
1366
1581
|
|
|
1582
|
+
// src/manager/semaphore.ts
|
|
1583
|
+
var Semaphore = class {
|
|
1584
|
+
count;
|
|
1585
|
+
queue = [];
|
|
1586
|
+
constructor(max) {
|
|
1587
|
+
this.count = max;
|
|
1588
|
+
}
|
|
1589
|
+
async run(fn) {
|
|
1590
|
+
await this.acquire();
|
|
1591
|
+
try {
|
|
1592
|
+
return await fn();
|
|
1593
|
+
} finally {
|
|
1594
|
+
this.release();
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
acquire() {
|
|
1598
|
+
if (this.count > 0) {
|
|
1599
|
+
this.count--;
|
|
1600
|
+
return Promise.resolve();
|
|
1601
|
+
}
|
|
1602
|
+
return new Promise((resolve) => {
|
|
1603
|
+
this.queue.push(resolve);
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
release() {
|
|
1607
|
+
const next = this.queue.shift();
|
|
1608
|
+
if (next) {
|
|
1609
|
+
next();
|
|
1610
|
+
} else {
|
|
1611
|
+
this.count++;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1367
1616
|
// src/llm/llm.service.ts
|
|
1368
1617
|
var ENTITY_EXTRACTOR_TOOL = {
|
|
1369
1618
|
type: "function",
|
|
@@ -1634,42 +1883,6 @@ ${context}`, false);
|
|
|
1634
1883
|
|
|
1635
1884
|
// src/manager/compress.manager.ts
|
|
1636
1885
|
import { v4 as uuidv4 } from "uuid";
|
|
1637
|
-
|
|
1638
|
-
// src/manager/semaphore.ts
|
|
1639
|
-
var Semaphore = class {
|
|
1640
|
-
count;
|
|
1641
|
-
queue = [];
|
|
1642
|
-
constructor(max) {
|
|
1643
|
-
this.count = max;
|
|
1644
|
-
}
|
|
1645
|
-
async run(fn) {
|
|
1646
|
-
await this.acquire();
|
|
1647
|
-
try {
|
|
1648
|
-
return await fn();
|
|
1649
|
-
} finally {
|
|
1650
|
-
this.release();
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
acquire() {
|
|
1654
|
-
if (this.count > 0) {
|
|
1655
|
-
this.count--;
|
|
1656
|
-
return Promise.resolve();
|
|
1657
|
-
}
|
|
1658
|
-
return new Promise((resolve) => {
|
|
1659
|
-
this.queue.push(resolve);
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
release() {
|
|
1663
|
-
const next = this.queue.shift();
|
|
1664
|
-
if (next) {
|
|
1665
|
-
next();
|
|
1666
|
-
} else {
|
|
1667
|
-
this.count++;
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
};
|
|
1671
|
-
|
|
1672
|
-
// src/manager/compress.manager.ts
|
|
1673
1886
|
var CompressManager = class {
|
|
1674
1887
|
config;
|
|
1675
1888
|
lance;
|
|
@@ -1698,7 +1911,7 @@ var CompressManager = class {
|
|
|
1698
1911
|
return next;
|
|
1699
1912
|
}
|
|
1700
1913
|
async doCompress(sessionId, force, waitGraph) {
|
|
1701
|
-
const messages = this.sessionCache.getSessionMessages(sessionId);
|
|
1914
|
+
const messages = this.sessionCache.getSessionMessages(sessionId).slice();
|
|
1702
1915
|
if (messages.length === 0) return;
|
|
1703
1916
|
const entry = this.sessionCache.getEntry(sessionId);
|
|
1704
1917
|
if (!entry) return;
|
|
@@ -1710,7 +1923,7 @@ var CompressManager = class {
|
|
|
1710
1923
|
try {
|
|
1711
1924
|
summary = await this.llm.summarizeMessages(messages);
|
|
1712
1925
|
} catch (err) {
|
|
1713
|
-
console.error(`[CompressManager] LLM compress failed for session ${sessionId}
|
|
1926
|
+
console.error(`[CompressManager] LLM compress failed for session ${sessionId}: ${formatError(err)}`);
|
|
1714
1927
|
return;
|
|
1715
1928
|
}
|
|
1716
1929
|
const { title, detail, summary: summaryText, concise } = summary;
|
|
@@ -1805,6 +2018,9 @@ var CompressManager = class {
|
|
|
1805
2018
|
}
|
|
1806
2019
|
}
|
|
1807
2020
|
};
|
|
2021
|
+
function formatError(err) {
|
|
2022
|
+
return err instanceof Error ? err.message : String(err);
|
|
2023
|
+
}
|
|
1808
2024
|
function withTimeout(promise, timeoutMs, label) {
|
|
1809
2025
|
let timer;
|
|
1810
2026
|
const timeout = new Promise((_, reject) => {
|
|
@@ -2293,12 +2509,12 @@ ${p.content}` : p.content
|
|
|
2293
2509
|
});
|
|
2294
2510
|
return true;
|
|
2295
2511
|
}
|
|
2296
|
-
async listDocuments(filter) {
|
|
2512
|
+
async listDocuments(filter, page) {
|
|
2297
2513
|
let docs = await this.lance.getAllDocuments();
|
|
2298
2514
|
if (filter?.userId) docs = docs.filter((d) => d.userId === filter.userId);
|
|
2299
2515
|
if (filter?.chatId) docs = docs.filter((d) => d.chatId === filter.chatId);
|
|
2300
2516
|
if (filter?.sessionId) docs = docs.filter((d) => d.sessionId === filter.sessionId);
|
|
2301
|
-
return docs;
|
|
2517
|
+
return page ? paginate(docs, page) : docs;
|
|
2302
2518
|
}
|
|
2303
2519
|
};
|
|
2304
2520
|
var NO_MATCH = Symbol("no-match");
|
|
@@ -2476,27 +2692,62 @@ var MemoryManager = class {
|
|
|
2476
2692
|
this.sessionMap.set(s.sessionId, this.deserializeSession(s));
|
|
2477
2693
|
}
|
|
2478
2694
|
await this.restoreFromStorage();
|
|
2695
|
+
if (this.config.autoOptimizeOnInit) {
|
|
2696
|
+
void this.optimizeStorage().then((results) => {
|
|
2697
|
+
const versions = results.reduce((s, r) => s + r.oldVersionsRemoved, 0);
|
|
2698
|
+
const fragments = results.reduce((s, r) => s + r.fragmentsRemoved, 0);
|
|
2699
|
+
if (versions > 0 || fragments > 0) {
|
|
2700
|
+
console.info(
|
|
2701
|
+
`[MemoryManager] \u540E\u53F0\u5B58\u50A8\u538B\u5B9E\u5B8C\u6210\uFF1A\u6E05\u7406\u5386\u53F2\u7248\u672C ${versions} \u4E2A\uFF0C\u5408\u5E76\u788E\u7247 ${fragments} \u4E2A`
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
}).catch((err) => {
|
|
2705
|
+
console.warn("[MemoryManager] \u540E\u53F0\u5B58\u50A8\u538B\u5B9E\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u670D\u52A1\uFF09:", err);
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* 手动触发存储压实与历史版本清理(init 后台会自动执行一次;上层维护任务也可调用)。
|
|
2711
|
+
* @param retentionMs 保留多久内的历史版本,默认取配置 optimizeVersionRetentionMs
|
|
2712
|
+
*/
|
|
2713
|
+
async optimizeStorage(retentionMs) {
|
|
2714
|
+
return this.lance.optimizeStorage(retentionMs ?? this.config.optimizeVersionRetentionMs);
|
|
2479
2715
|
}
|
|
2480
2716
|
async restoreFromStorage() {
|
|
2481
2717
|
const [n1, n2, n3] = this.config.topicRatio;
|
|
2482
2718
|
const sessionIds = await this.lance.getAllSessionIds();
|
|
2483
2719
|
if (sessionIds.length === 0) return;
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
if (rawMessages.length > 0) {
|
|
2492
|
-
this.sessionCache.addMessages(sessionId, rawMessages, chatId, userId);
|
|
2493
|
-
}
|
|
2494
|
-
if (topicGroups.detail.length + topicGroups.summary.length + topicGroups.concise.length > 0) {
|
|
2495
|
-
this.sessionCache.buildHistoryWindow(sessionId, topicGroups);
|
|
2496
|
-
} else {
|
|
2497
|
-
this.sessionCache.setHistoryWindow(sessionId, "");
|
|
2720
|
+
const topicsCache = /* @__PURE__ */ new Map();
|
|
2721
|
+
const getTopics = (chatId, userId) => {
|
|
2722
|
+
const key = `${chatId}\0${userId}`;
|
|
2723
|
+
let cached = topicsCache.get(key);
|
|
2724
|
+
if (!cached) {
|
|
2725
|
+
cached = this.lance.getRecentTopics(chatId, userId, n1, n2, n3);
|
|
2726
|
+
topicsCache.set(key, cached);
|
|
2498
2727
|
}
|
|
2499
|
-
|
|
2728
|
+
return cached;
|
|
2729
|
+
};
|
|
2730
|
+
const sem = new Semaphore(this.config.restoreConcurrency);
|
|
2731
|
+
await Promise.all(
|
|
2732
|
+
sessionIds.map(
|
|
2733
|
+
(sessionId) => sem.run(async () => {
|
|
2734
|
+
const recentMessages = await this.lance.getLatestMessages(sessionId, 1);
|
|
2735
|
+
if (recentMessages.length === 0) return;
|
|
2736
|
+
const { chatId, userId } = recentMessages[0];
|
|
2737
|
+
const topicGroups = await getTopics(chatId, userId);
|
|
2738
|
+
const n1EndTime = topicGroups.detail.length > 0 ? topicGroups.detail[topicGroups.detail.length - 1].endTime : 0;
|
|
2739
|
+
const rawMessages = n1EndTime > 0 ? await this.lance.getMessagesSince(sessionId, n1EndTime) : await this.lance.getLatestMessages(sessionId, 100);
|
|
2740
|
+
if (rawMessages.length > 0) {
|
|
2741
|
+
this.sessionCache.addMessages(sessionId, rawMessages, chatId, userId);
|
|
2742
|
+
}
|
|
2743
|
+
if (topicGroups.detail.length + topicGroups.summary.length + topicGroups.concise.length > 0) {
|
|
2744
|
+
this.sessionCache.buildHistoryWindow(sessionId, topicGroups);
|
|
2745
|
+
} else {
|
|
2746
|
+
this.sessionCache.setHistoryWindow(sessionId, "");
|
|
2747
|
+
}
|
|
2748
|
+
})
|
|
2749
|
+
)
|
|
2750
|
+
);
|
|
2500
2751
|
}
|
|
2501
2752
|
async updateChat(messages, opts) {
|
|
2502
2753
|
if (messages.length === 0) return;
|
|
@@ -2778,33 +3029,28 @@ var MemoryManager = class {
|
|
|
2778
3029
|
const n = Math.min(365, Math.max(1, Math.floor(days)));
|
|
2779
3030
|
return this.lance.trendDaily(n);
|
|
2780
3031
|
}
|
|
2781
|
-
|
|
2782
|
-
listSessions(filter) {
|
|
3032
|
+
listSessions(filter, page) {
|
|
2783
3033
|
let results = [...this.sessionMap.values()];
|
|
2784
3034
|
if (filter?.userId) results = results.filter((s) => s.userId === filter.userId);
|
|
2785
3035
|
if (filter?.chatId) results = results.filter((s) => s.chatId === filter.chatId);
|
|
2786
|
-
|
|
3036
|
+
results.sort((a, b) => b.updatedAt - a.updatedAt || cmpStr(a.sessionId, b.sessionId));
|
|
3037
|
+
return page ? paginate(results, page) : results;
|
|
2787
3038
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3039
|
+
async listMessages(sessionId, limitOrPage) {
|
|
3040
|
+
if (typeof limitOrPage === "object") {
|
|
3041
|
+
const all = await this.lance.getAllMessagesBySession(sessionId);
|
|
3042
|
+
return paginate(all, limitOrPage);
|
|
3043
|
+
}
|
|
3044
|
+
return this.lance.getLatestMessages(sessionId, limitOrPage ?? 100);
|
|
2791
3045
|
}
|
|
2792
|
-
|
|
2793
|
-
async listTopics(filter) {
|
|
3046
|
+
async listTopics(filter, page) {
|
|
2794
3047
|
let topics = await this.lance.getAllTopics();
|
|
2795
3048
|
if (filter?.sessionId) topics = topics.filter((t) => t.sessionId === filter.sessionId);
|
|
2796
3049
|
if (filter?.userId) topics = topics.filter((t) => t.userId === filter.userId);
|
|
2797
3050
|
if (filter?.chatId) topics = topics.filter((t) => t.chatId === filter.chatId);
|
|
2798
|
-
return topics;
|
|
3051
|
+
return page ? paginate(topics, page) : topics;
|
|
2799
3052
|
}
|
|
2800
|
-
|
|
2801
|
-
* 列出事实,按创建时间倒序。过滤遵循 scope 层级包含语义:
|
|
2802
|
-
* - 按 chatId 过滤时,除该 chat 级 facts 外,还包含该 chat 所属 user 的 user 级 facts
|
|
2803
|
-
* (user 级对其下所有 chat/session 生效)。
|
|
2804
|
-
* - 按 userId 过滤时,返回该 user 名下全部 facts(两级都带 userId)。
|
|
2805
|
-
* - level 过滤在层级过滤之后再叠加。
|
|
2806
|
-
*/
|
|
2807
|
-
listFacts(filter) {
|
|
3053
|
+
listFacts(filter, page) {
|
|
2808
3054
|
let facts = this.factCache.all();
|
|
2809
3055
|
if (filter?.chatId) {
|
|
2810
3056
|
const chatId = filter.chatId;
|
|
@@ -2816,7 +3062,8 @@ var MemoryManager = class {
|
|
|
2816
3062
|
facts = facts.filter((f) => f.userId === filter.userId);
|
|
2817
3063
|
}
|
|
2818
3064
|
if (filter?.level) facts = facts.filter((f) => f.level === filter.level);
|
|
2819
|
-
|
|
3065
|
+
facts.sort((a, b) => b.createdAt - a.createdAt || cmpStr(a.factId, b.factId));
|
|
3066
|
+
return page ? paginate(facts, page) : facts;
|
|
2820
3067
|
}
|
|
2821
3068
|
/** 由 chatId 反查所属 userId:优先用会话表映射,兜底用 chat 级 fact 自身。 */
|
|
2822
3069
|
resolveChatOwner(chatId) {
|
|
@@ -2833,13 +3080,17 @@ var MemoryManager = class {
|
|
|
2833
3080
|
async deleteFact(factId) {
|
|
2834
3081
|
return this.factCache.remove(factId);
|
|
2835
3082
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
3083
|
+
async listEntities(filter, page) {
|
|
3084
|
+
if (!page) return this.grafeo.getAllEntities(filter);
|
|
3085
|
+
const { offset, limit } = normalizePage(page);
|
|
3086
|
+
const { items, total } = await this.grafeo.pageEntities(filter, page);
|
|
3087
|
+
return { items, total, offset, limit };
|
|
2839
3088
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
3089
|
+
async listRelations(filter, page) {
|
|
3090
|
+
if (!page) return this.grafeo.getAllRelations(filter);
|
|
3091
|
+
const { offset, limit } = normalizePage(page);
|
|
3092
|
+
const { items, total } = await this.grafeo.pageRelations(filter, page);
|
|
3093
|
+
return { items, total, offset, limit };
|
|
2843
3094
|
}
|
|
2844
3095
|
// ── 知识库 API ────────────────────────────────────────────────────────────────
|
|
2845
3096
|
/** 摄入一个文档(markdown)。document 落库即返回,切块/embedding/图谱后台执行(wait=true 可等待)。*/
|
|
@@ -2858,9 +3109,8 @@ var MemoryManager = class {
|
|
|
2858
3109
|
async deleteDocument(docId) {
|
|
2859
3110
|
return this.knowledgeManager.deleteDocument(docId);
|
|
2860
3111
|
}
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
return this.knowledgeManager.listDocuments(filter);
|
|
3112
|
+
async listDocuments(filter, page) {
|
|
3113
|
+
return page ? this.knowledgeManager.listDocuments(filter, page) : this.knowledgeManager.listDocuments(filter);
|
|
2864
3114
|
}
|
|
2865
3115
|
destroy() {
|
|
2866
3116
|
this.grafeo.close();
|