@ppagent/memory 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/dist/index.d.ts +86 -19
  2. package/dist/index.js +292 -211
  3. package/package.json +49 -50
  4. package/llms.txt +0 -845
package/dist/index.js CHANGED
@@ -25,10 +25,9 @@ function resolveConfig(config) {
25
25
  detailMaxTokens: config.detailMaxTokens ?? 2048,
26
26
  summaryMaxTokens: config.summaryMaxTokens ?? 512,
27
27
  conciseMaxTokens: config.conciseMaxTokens ?? 128,
28
- httpTimeoutMs: config.httpTimeoutMs ?? 6e4,
28
+ httpTimeoutMs: config.httpTimeoutMs ?? 3e4,
29
29
  httpMaxRetries: config.httpMaxRetries ?? 2,
30
30
  embeddingBatchSize: config.embeddingBatchSize ?? 20,
31
- embeddingConcurrency: config.embeddingConcurrency ?? 2,
32
31
  maxConcurrentCompressions: config.maxConcurrentCompressions ?? 3,
33
32
  entitySimilarityThreshold: config.entitySimilarityThreshold ?? 0.92,
34
33
  defaultSearchLimit: config.defaultSearchLimit ?? 10,
@@ -43,9 +42,7 @@ function resolveConfig(config) {
43
42
  knowledgeGraphTriggerScore: config.knowledgeGraphTriggerScore ?? 0.78,
44
43
  knowledgeGraphEntityTopK: config.knowledgeGraphEntityTopK ?? 10,
45
44
  knowledgeGraphAnchorTopK: config.knowledgeGraphAnchorTopK ?? 3,
46
- knowledgeGraphHopLimit: config.knowledgeGraphHopLimit ?? 10,
47
- graphExtractConcurrency: config.graphExtractConcurrency ?? 2,
48
- graphBuildTimeoutMs: config.graphBuildTimeoutMs ?? 12e4
45
+ knowledgeGraphHopLimit: config.knowledgeGraphHopLimit ?? 10
49
46
  };
50
47
  }
51
48
 
@@ -229,45 +226,27 @@ var EmbedService = class {
229
226
  const inputs = Array.isArray(texts) ? texts : [texts];
230
227
  const url = `${this.config.embeddingBaseUrl.replace(/\/$/, "")}/embeddings`;
231
228
  const batchSize = Math.max(1, this.config.embeddingBatchSize);
232
- const concurrency = Math.max(1, this.config.embeddingConcurrency);
233
229
  const retryOpts = { timeoutMs: this.config.httpTimeoutMs, maxRetries: this.config.httpMaxRetries };
234
- const batches = [];
230
+ const out = [];
235
231
  for (let i = 0; i < inputs.length; i += batchSize) {
236
- batches.push({ index: batches.length, texts: inputs.slice(i, i + batchSize) });
237
- }
238
- const batchResults = await mapLimit(batches, concurrency, async (batch) => {
232
+ const batch = inputs.slice(i, i + batchSize);
239
233
  const data = await postJsonWithRetry(
240
234
  url,
241
235
  { Authorization: `Bearer ${this.config.embeddingApiKey}` },
242
- { model: this.config.embeddingModel, input: batch.texts },
236
+ { model: this.config.embeddingModel, input: batch },
243
237
  retryOpts,
244
238
  "Embedding API"
245
239
  );
246
- return {
247
- index: batch.index,
248
- vectors: data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding)
249
- };
250
- });
251
- return batchResults.sort((a, b) => a.index - b.index).flatMap((b) => b.vectors);
240
+ const sorted = data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
241
+ out.push(...sorted);
242
+ }
243
+ return out;
252
244
  }
253
245
  async embedOne(text) {
254
246
  const result = await this.embed([text]);
255
247
  return result[0];
256
248
  }
257
249
  };
258
- async function mapLimit(items, limit, mapper) {
259
- const results = new Array(items.length);
260
- let next = 0;
261
- const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
262
- while (true) {
263
- const index = next++;
264
- if (index >= items.length) return;
265
- results[index] = await mapper(items[index], index);
266
- }
267
- });
268
- await Promise.all(workers);
269
- return results;
270
- }
271
250
  function cosineSimilarity(a, b) {
272
251
  let dot = 0;
273
252
  let normA = 0;
@@ -281,8 +260,24 @@ function cosineSimilarity(a, b) {
281
260
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
282
261
  }
283
262
 
263
+ // src/page.util.ts
264
+ function normalizePage(page) {
265
+ const offset = Math.max(0, Math.floor(page.offset ?? 0));
266
+ const limit = page.limit != null && page.limit > 0 ? Math.floor(page.limit) : 0;
267
+ return { offset, limit };
268
+ }
269
+ function paginate(items, page) {
270
+ const total = items.length;
271
+ const { offset, limit } = normalizePage(page);
272
+ const slice = limit > 0 ? items.slice(offset, offset + limit) : items.slice(offset);
273
+ return { items: slice, total, offset, limit };
274
+ }
275
+ function cmpStr(a, b) {
276
+ return a < b ? -1 : a > b ? 1 : 0;
277
+ }
278
+
284
279
  // src/db/grafeo.service.ts
285
- var GrafeoService = class {
280
+ var GrafeoService = class _GrafeoService {
286
281
  config;
287
282
  db;
288
283
  constructor(config) {
@@ -459,16 +454,17 @@ var GrafeoService = class {
459
454
  const r = row;
460
455
  return {
461
456
  type: "relation",
462
- content: `${r["a.name"]} -[${r["type(r)"]}]-> ${r["b.name"]}`,
457
+ content: `${r["fromName"]} -[${r["relType"]}]-> ${r["toName"]}`,
463
458
  score: 1,
464
- meta: { happenedAt: r["r.happenedAt"] }
459
+ meta: { happenedAt: r["happenedAt"] }
465
460
  };
466
461
  });
462
+ const RETURN_CLAUSE = "RETURN a.name AS fromName, type(r) AS relType, b.name AS toName, r.happenedAt AS happenedAt";
467
463
  try {
468
464
  const result = await this.db.execute(
469
465
  `MATCH (a:Entity {name: $name, userId: $userId, chatId: $chatId, kind: $kind})-[r]->(b)
470
466
  WHERE r.chatId = $chatId AND r.userId = $userId
471
- RETURN a.name, type(r), b.name, r.happenedAt LIMIT $limit`,
467
+ ${RETURN_CLAUSE} LIMIT $limit`,
472
468
  { name: entityName, userId, chatId, kind, limit }
473
469
  );
474
470
  return mapRows(result.toArray());
@@ -477,7 +473,7 @@ var GrafeoService = class {
477
473
  try {
478
474
  const result = await this.db.execute(
479
475
  `MATCH (a:Entity {name: $name, userId: $userId, chatId: $chatId, kind: $kind})-[r]->(b)
480
- RETURN a.name, type(r), b.name, r.happenedAt LIMIT $limit`,
476
+ ${RETURN_CLAUSE} LIMIT $limit`,
481
477
  { name: entityName, userId, chatId, kind, limit }
482
478
  );
483
479
  return mapRows(result.toArray());
@@ -500,56 +496,176 @@ var GrafeoService = class {
500
496
  }
501
497
  }
502
498
  // ── 管理面板用:全量读取实体 / 关系 ────────────────────────────────────────────────
503
- /** 全量读取实体节点(管理面板/知识图谱用,剔除体积大的 embedding 字段)*/
504
- async getAllEntities(limit = 1e3) {
499
+ /**
500
+ * 构建按域过滤的内联属性子句(如 ` {userId: $userId, chatId: $chatId}`)。
501
+ * 在 LIMIT 之前生效,避免「先截断再过滤」导致结果不足。
502
+ */
503
+ buildScopeMatch(filter, params) {
504
+ const props = [];
505
+ if (filter?.userId) {
506
+ props.push("userId: $userId");
507
+ params.userId = filter.userId;
508
+ }
509
+ if (filter?.chatId) {
510
+ props.push("chatId: $chatId");
511
+ params.chatId = filter.chatId;
512
+ }
513
+ return props.length > 0 ? ` {${props.join(", ")}}` : "";
514
+ }
515
+ // 关系查询的统一 RETURN 子句:用别名(AS)取列,避免函数表达式 type(r)/id(r) 的默认列名
516
+ // 被归一为 "type(...)"/"id(...)"(按 r["type(r)"] 取会得到 undefined)。id(r) 同时用于 ORDER BY。
517
+ static RELATION_RETURN = `RETURN a.name AS fromName, b.name AS toName, type(r) AS relType,
518
+ r.happenedAt AS happenedAt, r.userId AS userId, r.chatId AS chatId,
519
+ r.sessionId AS sessionId, id(r) AS rid`;
520
+ /** 节点 → Entity 映射(剔除体积大的 embedding 字段)*/
521
+ nodeToEntity(node) {
522
+ const props = node.properties();
523
+ const { embedding: _embedding, name, userId, chatId, sessionId, ...rest } = props;
524
+ const type = (node.labels ?? []).filter((l) => l !== "Entity").join(", ") || "\u672A\u77E5";
525
+ return {
526
+ name: String(name ?? ""),
527
+ type,
528
+ meta: {
529
+ userId: String(userId ?? ""),
530
+ chatId: String(chatId ?? ""),
531
+ sessionId: String(sessionId ?? ""),
532
+ ...rest
533
+ }
534
+ };
535
+ }
536
+ /** 关系查询结果行 → Relation 映射(依赖 RELATION_RETURN 的别名列)*/
537
+ rowToRelation(row) {
538
+ const r = row;
539
+ return {
540
+ from: String(r["fromName"] ?? ""),
541
+ to: String(r["toName"] ?? ""),
542
+ type: String(r["relType"] ?? ""),
543
+ happenedAt: r["happenedAt"] != null ? Number(r["happenedAt"]) : void 0,
544
+ meta: {
545
+ userId: String(r["userId"] ?? ""),
546
+ chatId: String(r["chatId"] ?? ""),
547
+ sessionId: String(r["sessionId"] ?? "")
548
+ }
549
+ };
550
+ }
551
+ /** 统计实体数(可选按域过滤),用于分页 total */
552
+ async countEntities(filter) {
553
+ try {
554
+ const params = {};
555
+ const scope = this.buildScopeMatch(filter, params);
556
+ const res = await this.db.execute(`MATCH (n:Entity${scope}) RETURN count(n) AS cnt`, params);
557
+ return Number(res.toArray()[0]?.cnt ?? 0);
558
+ } catch {
559
+ return 0;
560
+ }
561
+ }
562
+ /** 统计关系数(可选按域过滤),用于分页 total */
563
+ async countRelations(filter) {
505
564
  try {
506
- const result = await this.db.execute(`MATCH (n:Entity) RETURN n LIMIT $limit`, { limit });
565
+ const params = {};
566
+ const scope = this.buildScopeMatch(filter, params);
567
+ const res = await this.db.execute(
568
+ `MATCH (a:Entity${scope})-[r]->(b:Entity) RETURN count(r) AS cnt`,
569
+ params
570
+ );
571
+ return Number(res.toArray()[0]?.cnt ?? 0);
572
+ } catch {
573
+ return 0;
574
+ }
575
+ }
576
+ /**
577
+ * 全量读取实体节点(管理面板/知识图谱用,剔除体积大的 embedding 字段)。
578
+ * 可选按 userId / chatId 过滤(在节点属性上匹配,LIMIT 前生效)。
579
+ * 按节点内部 id 升序,确保「同样条件 → 同样顺序」。
580
+ * 注意:此方法带安全上限 limit,超出会截断;需要准确 total 与翻页请用 pageEntities。
581
+ */
582
+ async getAllEntities(filter, limit = 1e3) {
583
+ try {
584
+ const params = { limit };
585
+ const scope = this.buildScopeMatch(filter, params);
586
+ const result = await this.db.execute(`MATCH (n:Entity${scope}) RETURN n LIMIT $limit`, params);
507
587
  const nodes = result.nodes();
508
- return nodes.map((node) => {
509
- const props = node.properties();
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
- });
588
+ nodes.sort((a, b) => a.id - b.id);
589
+ return nodes.map((node) => this.nodeToEntity(node));
523
590
  } catch {
524
591
  return [];
525
592
  }
526
593
  }
527
- /** 全量读取关系边(管理面板/知识图谱用)*/
528
- async getAllRelations(limit = 2e3) {
594
+ /**
595
+ * 分页读取实体节点:把 ORDER BY id(n) + SKIP/LIMIT 下推到查询层,并用 count 取准确 total。
596
+ * 不受 getAllEntities 安全上限约束。返回当前页 items 与过滤后的 total。
597
+ */
598
+ async pageEntities(filter, page) {
599
+ const { offset, limit } = normalizePage(page);
600
+ try {
601
+ const total = await this.countEntities(filter);
602
+ const effLimit = limit > 0 ? limit : Math.max(0, total - offset);
603
+ if (effLimit <= 0) return { items: [], total };
604
+ const params = {};
605
+ const scope = this.buildScopeMatch(filter, params);
606
+ params.offset = offset;
607
+ params.limit = effLimit;
608
+ const res = await this.db.execute(
609
+ `MATCH (n:Entity${scope}) RETURN id(n) AS nid ORDER BY nid SKIP $offset LIMIT $limit`,
610
+ params
611
+ );
612
+ const items = [];
613
+ for (const row of res.toArray()) {
614
+ const nid = Number(row.nid);
615
+ const node = this.db.getNode(nid);
616
+ if (node) items.push(this.nodeToEntity(node));
617
+ }
618
+ return { items, total };
619
+ } catch {
620
+ return { items: [], total: 0 };
621
+ }
622
+ }
623
+ /**
624
+ * 全量读取关系边(管理面板/知识图谱用)。可选按 userId / chatId 过滤:在源实体节点的域属性上匹配
625
+ *(关系两端与边均同域,故按源节点过滤即可,且节点内联属性匹配在各 grafeo 版本均可靠)。
626
+ * 按 id(r) 升序确保顺序确定。
627
+ * 注意:此方法带安全上限 limit,超出会截断;需要准确 total 与翻页请用 pageRelations。
628
+ */
629
+ async getAllRelations(filter, limit = 2e3) {
529
630
  try {
631
+ const params = { limit };
632
+ const scope = this.buildScopeMatch(filter, params);
530
633
  const result = await this.db.execute(
531
- `MATCH (a:Entity)-[r]->(b:Entity)
532
- RETURN a.name, type(r), b.name, r.happenedAt, r.userId, r.chatId, r.sessionId LIMIT $limit`,
533
- { limit }
634
+ `MATCH (a:Entity${scope})-[r]->(b:Entity)
635
+ ${_GrafeoService.RELATION_RETURN}
636
+ ORDER BY rid LIMIT $limit`,
637
+ params
534
638
  );
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
- });
639
+ return result.toArray().map((row) => this.rowToRelation(row));
549
640
  } catch {
550
641
  return [];
551
642
  }
552
643
  }
644
+ /**
645
+ * 分页读取关系边:把 ORDER BY id(r) + SKIP/LIMIT 下推到查询层,并用 count 取准确 total。
646
+ * 不受 getAllRelations 安全上限约束。返回当前页 items 与过滤后的 total。
647
+ */
648
+ async pageRelations(filter, page) {
649
+ const { offset, limit } = normalizePage(page);
650
+ try {
651
+ const total = await this.countRelations(filter);
652
+ const effLimit = limit > 0 ? limit : Math.max(0, total - offset);
653
+ if (effLimit <= 0) return { items: [], total };
654
+ const params = {};
655
+ const scope = this.buildScopeMatch(filter, params);
656
+ params.offset = offset;
657
+ params.limit = effLimit;
658
+ const res = await this.db.execute(
659
+ `MATCH (a:Entity${scope})-[r]->(b:Entity)
660
+ ${_GrafeoService.RELATION_RETURN}
661
+ ORDER BY rid SKIP $offset LIMIT $limit`,
662
+ params
663
+ );
664
+ return { items: res.toArray().map((row) => this.rowToRelation(row)), total };
665
+ } catch {
666
+ return { items: [], total: 0 };
667
+ }
668
+ }
553
669
  close() {
554
670
  try {
555
671
  this.db.close();
@@ -1051,6 +1167,15 @@ var LanceService = class {
1051
1167
  return [];
1052
1168
  }
1053
1169
  }
1170
+ /** 取某会话的全部消息(按 createdAt 升序),供管理面板分页切片使用 */
1171
+ async getAllMessagesBySession(sessionId) {
1172
+ try {
1173
+ const rows = await this.messagesTable.query().where(eqFilter("session_id", sessionId)).toArray();
1174
+ return rows.sort((a, b) => Number(a.created_at) - Number(b.created_at) || cmpStr(a.message_id, b.message_id)).map(rowToMessage);
1175
+ } catch {
1176
+ return [];
1177
+ }
1178
+ }
1054
1179
  async searchMessages(vector, filter, limit = 10) {
1055
1180
  const q = this.messagesTable.vectorSearch(vector).limit(limit);
1056
1181
  if (filter) q.where(filter);
@@ -1082,8 +1207,7 @@ var LanceService = class {
1082
1207
  const total = rows.length;
1083
1208
  return rows.map((r, idx) => ({
1084
1209
  row: r,
1085
- // 位置分(越前越高)+ recall_count 加成
1086
- score: 1 - idx / total + Math.log1p(Number(r.recall_count)) * 0.1
1210
+ score: total - idx + Math.log1p(Number(r.recall_count))
1087
1211
  })).sort((a, b) => b.score - a.score).map((x) => rowToTopic(x.row));
1088
1212
  }
1089
1213
  } catch {
@@ -1151,7 +1275,7 @@ var LanceService = class {
1151
1275
  async getAllTopics() {
1152
1276
  try {
1153
1277
  const rows = await this.topicsTable.query().toArray();
1154
- return rows.sort((a, b) => Number(b.end_time) - Number(a.end_time)).map(rowToTopic);
1278
+ return rows.sort((a, b) => Number(b.end_time) - Number(a.end_time) || cmpStr(a.summary_id, b.summary_id)).map(rowToTopic);
1155
1279
  } catch {
1156
1280
  return [];
1157
1281
  }
@@ -1324,7 +1448,7 @@ var LanceService = class {
1324
1448
  async getAllDocuments() {
1325
1449
  try {
1326
1450
  const rows = await this.documentsTable.query().toArray();
1327
- return rows.sort((a, b) => Number(b.updated_at) - Number(a.updated_at)).map(rowToDocument);
1451
+ return rows.sort((a, b) => Number(b.updated_at) - Number(a.updated_at) || cmpStr(a.doc_id, b.doc_id)).map(rowToDocument);
1328
1452
  } catch {
1329
1453
  return [];
1330
1454
  }
@@ -1478,7 +1602,7 @@ var LlmService = class {
1478
1602
  return data.choices[0].message;
1479
1603
  }
1480
1604
  // ── 第一步:生成三级摘要(JSON 输出)──────────────────────────────────────────
1481
- async summarizeMessages(messages) {
1605
+ async getSummary(messages) {
1482
1606
  const now = (/* @__PURE__ */ new Date()).toISOString();
1483
1607
  const { detailMaxTokens, summaryMaxTokens, conciseMaxTokens } = this.config;
1484
1608
  const systemPrompt = `\u4F60\u662F\u4E00\u4E2A\u5BF9\u8BDD\u8BB0\u5FC6\u7BA1\u7406\u52A9\u624B\u3002\u5F53\u524D\u7CFB\u7EDF\u65F6\u95F4\uFF1A${now}\u3002
@@ -1500,7 +1624,7 @@ ${formatted}`);
1500
1624
  return { title: parsed.title ?? "", detail: parsed.detail, summary: parsed.summary, concise: parsed.concise };
1501
1625
  }
1502
1626
  // ── 第二步:通过工具调用提取实体和关系 ──────────────────────────────────────
1503
- async extractEntitiesFromMessages(messages) {
1627
+ async extractEntitiesWithTool(messages) {
1504
1628
  const now = (/* @__PURE__ */ new Date()).toISOString();
1505
1629
  const systemPrompt = `\u4F60\u662F\u4E00\u4E2A\u5B9E\u4F53\u5173\u7CFB\u62BD\u53D6\u52A9\u624B\u3002\u5F53\u524D\u7CFB\u7EDF\u65F6\u95F4\uFF1A${now}\u3002
1506
1630
  \u68C0\u67E5\u7ED9\u5B9A\u7684\u5BF9\u8BDD\u5185\u5BB9\uFF0C\u5224\u65AD\u5176\u4E2D\u662F\u5426\u5B58\u5728\u503C\u5F97\u8BB0\u5F55\u7684\u547D\u540D\u5B9E\u4F53\uFF08\u4EBA\u7269\u3001\u7EC4\u7EC7\u3001\u9879\u76EE\u3001\u5730\u70B9\u3001\u6280\u672F\u7B49\uFF09\u548C\u5B83\u4EEC\u4E4B\u95F4\u7684\u5173\u7CFB\u3002
@@ -1528,15 +1652,15 @@ ${formatted}`
1528
1652
  }
1529
1653
  }
1530
1654
  } catch (err) {
1531
- console.error("[LlmService] extractEntitiesFromMessages failed:", err);
1655
+ console.error("[LlmService] extractEntitiesWithTool failed:", err);
1532
1656
  }
1533
1657
  return { entities: [], relations: [] };
1534
1658
  }
1535
1659
  // ── 对外主接口:压缩 + 实体抽取(并发执行两步)────────────────────────────────
1536
1660
  async compress(messages) {
1537
1661
  const [summary, extraction] = await Promise.all([
1538
- this.summarizeMessages(messages),
1539
- this.extractEntitiesFromMessages(messages)
1662
+ this.getSummary(messages),
1663
+ this.extractEntitiesWithTool(messages)
1540
1664
  ]);
1541
1665
  return {
1542
1666
  title: summary.title,
@@ -1688,16 +1812,16 @@ var CompressManager = class {
1688
1812
  this.sessionCache = sessionCache;
1689
1813
  this.semaphore = new Semaphore(config.maxConcurrentCompressions);
1690
1814
  }
1691
- triggerCompress(sessionId, force = false, waitGraph = false) {
1815
+ triggerCompress(sessionId, force = false) {
1692
1816
  const chain = this.sessionChain.get(sessionId) ?? Promise.resolve();
1693
1817
  const next = chain.then(
1694
- () => this.semaphore.run(() => this.doCompress(sessionId, force, waitGraph))
1818
+ () => this.semaphore.run(() => this.doCompress(sessionId, force))
1695
1819
  );
1696
1820
  this.sessionChain.set(sessionId, next.catch(() => {
1697
1821
  }));
1698
1822
  return next;
1699
1823
  }
1700
- async doCompress(sessionId, force, waitGraph) {
1824
+ async doCompress(sessionId, force) {
1701
1825
  const messages = this.sessionCache.getSessionMessages(sessionId);
1702
1826
  if (messages.length === 0) return;
1703
1827
  const entry = this.sessionCache.getEntry(sessionId);
@@ -1706,15 +1830,15 @@ var CompressManager = class {
1706
1830
  const { chatId, userId } = entry.ids;
1707
1831
  const startTime = messages[0].createdAt;
1708
1832
  const endTime = messages[messages.length - 1].createdAt;
1709
- let summary;
1833
+ let compressOutput;
1710
1834
  try {
1711
- summary = await this.llm.summarizeMessages(messages);
1835
+ compressOutput = await this.llm.compress(messages);
1712
1836
  } catch (err) {
1713
1837
  console.error(`[CompressManager] LLM compress failed for session ${sessionId}:`, err);
1714
1838
  return;
1715
1839
  }
1716
- const { title, detail, summary: summaryText, concise } = summary;
1717
- const topicTextForEmbed = [detail, summaryText, concise].find((t) => t.length > 0) ?? "";
1840
+ const { title, detail, summary, concise, entities, relations } = compressOutput;
1841
+ const topicTextForEmbed = [detail, summary, concise].find((t) => t.length > 0) ?? "";
1718
1842
  let topicVector = [];
1719
1843
  try {
1720
1844
  topicVector = await this.embed.embedOne(topicTextForEmbed);
@@ -1728,7 +1852,7 @@ var CompressManager = class {
1728
1852
  chatId,
1729
1853
  title,
1730
1854
  detail,
1731
- summary: summaryText,
1855
+ summary,
1732
1856
  concise,
1733
1857
  startTime,
1734
1858
  endTime,
@@ -1742,6 +1866,9 @@ var CompressManager = class {
1742
1866
  } catch (err) {
1743
1867
  console.error(`[CompressManager] Save topic failed:`, err);
1744
1868
  }
1869
+ if (entities.length > 0 || relations.length > 0) {
1870
+ await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
1871
+ }
1745
1872
  this.sessionCache.clearMessages(sessionId, endTime);
1746
1873
  const [n1, n2, n3] = this.config.topicRatio;
1747
1874
  try {
@@ -1750,19 +1877,6 @@ var CompressManager = class {
1750
1877
  } catch (err) {
1751
1878
  console.error(`[CompressManager] Rebuild history window failed:`, err);
1752
1879
  }
1753
- const graphTask = this.extractAndPersistGraph(messages, sessionId, chatId, userId, endTime);
1754
- if (waitGraph) {
1755
- await withTimeout(graphTask, this.config.graphBuildTimeoutMs, "flushChat graph build");
1756
- } else {
1757
- graphTask.catch((err) => {
1758
- console.error(`[CompressManager] Background graph persist failed:`, err);
1759
- });
1760
- }
1761
- }
1762
- async extractAndPersistGraph(messages, sessionId, chatId, userId, endTime) {
1763
- const { entities, relations } = await this.llm.extractEntitiesFromMessages(messages);
1764
- if (entities.length === 0 && relations.length === 0) return;
1765
- await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
1766
1880
  }
1767
1881
  async persistGraph(rawEntities, rawRelations, sessionId, chatId, userId, messageTime) {
1768
1882
  const entities = rawEntities.map((e) => ({
@@ -1805,13 +1919,6 @@ var CompressManager = class {
1805
1919
  }
1806
1920
  }
1807
1921
  };
1808
- function withTimeout(promise, timeoutMs, label) {
1809
- let timer;
1810
- const timeout = new Promise((_, reject) => {
1811
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
1812
- });
1813
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
1814
- }
1815
1922
 
1816
1923
  // src/manager/fact.cache.ts
1817
1924
  import { v4 as uuidv42 } from "uuid";
@@ -2071,29 +2178,20 @@ var KnowledgeManager = class {
2071
2178
  updatedAt: now
2072
2179
  };
2073
2180
  await this.lance.addDocument(doc);
2074
- const chunkIngest = this.semaphore.run(() => this.ingestChunks(doc, pieces));
2075
- const graphBuild = chunkIngest.then((chunks) => {
2076
- if (!shouldBuildGraph) return;
2077
- return this.buildDocumentGraph(doc, chunks);
2078
- });
2079
- if (opts.wait || opts.waitGraph) {
2080
- await chunkIngest;
2081
- } else {
2082
- chunkIngest.catch((err) => {
2083
- console.error("[KnowledgeManager] background chunk ingest failed:", err);
2084
- });
2085
- }
2086
- if (opts.waitGraph) {
2087
- await withTimeout2(graphBuild, this.config.graphBuildTimeoutMs, "document graph build");
2181
+ const background = this.semaphore.run(
2182
+ () => this.ingestChunksAndGraph(doc, pieces, shouldBuildGraph)
2183
+ );
2184
+ if (opts.wait) {
2185
+ await background;
2088
2186
  } else {
2089
- graphBuild.catch((err) => {
2090
- console.error("[KnowledgeManager] background graph build failed:", err);
2187
+ background.catch((err) => {
2188
+ console.error("[KnowledgeManager] background ingest failed:", err);
2091
2189
  });
2092
2190
  }
2093
2191
  return { docId };
2094
2192
  }
2095
- async ingestChunks(doc, pieces) {
2096
- if (pieces.length === 0) return [];
2193
+ async ingestChunksAndGraph(doc, pieces, buildGraph) {
2194
+ if (pieces.length === 0) return;
2097
2195
  const redundant = this.config.chunkRedundantIds;
2098
2196
  const embedInputs = pieces.map(
2099
2197
  (p) => p.headingPath ? `${p.headingPath}
@@ -2104,7 +2202,7 @@ ${p.content}` : p.content
2104
2202
  vectors = await this.embed.embed(embedInputs);
2105
2203
  } catch (err) {
2106
2204
  console.error("[KnowledgeManager] embed chunks failed:", err);
2107
- return [];
2205
+ return;
2108
2206
  }
2109
2207
  const chunks = pieces.map((p, i) => ({
2110
2208
  chunkId: uuidv43(),
@@ -2121,12 +2219,17 @@ ${p.content}` : p.content
2121
2219
  createdAt: doc.createdAt
2122
2220
  }));
2123
2221
  await this.lance.addChunks(chunks);
2124
- return chunks;
2222
+ if (buildGraph) {
2223
+ await this.buildDocumentGraph(doc, chunks).catch((err) => {
2224
+ console.error("[KnowledgeManager] build graph failed:", err);
2225
+ });
2226
+ }
2125
2227
  }
2126
2228
  async buildDocumentGraph(doc, chunks) {
2127
- const extracted = await mapLimit2(chunks, this.config.graphExtractConcurrency, async (chunk) => {
2229
+ let extractedAny = false;
2230
+ for (const chunk of chunks) {
2128
2231
  const { entities: rawEntities, relations: rawRelations } = await this.llm.extractEntitiesFromText(chunk.content);
2129
- if (rawEntities.length === 0 && rawRelations.length === 0) return null;
2232
+ if (rawEntities.length === 0 && rawRelations.length === 0) continue;
2130
2233
  const meta = {
2131
2234
  userId: doc.userId,
2132
2235
  chatId: doc.chatId,
@@ -2135,47 +2238,46 @@ ${p.content}` : p.content
2135
2238
  chunkId: chunk.chunkId,
2136
2239
  messageTime: doc.createdAt
2137
2240
  };
2138
- const entities2 = rawEntities.map((e) => ({
2241
+ const entities = rawEntities.map((e) => ({
2139
2242
  name: e.name,
2140
2243
  type: e.type,
2141
2244
  meta: { ...meta, ...e.meta }
2142
2245
  }));
2143
- const relations2 = rawRelations.map((r) => ({
2246
+ const relations = rawRelations.map((r) => ({
2144
2247
  from: r.from,
2145
2248
  to: r.to,
2146
2249
  type: r.type,
2147
2250
  happenedAt: r.happenedAt ?? void 0,
2148
2251
  meta: { ...meta, ...r.meta }
2149
2252
  }));
2150
- return { entities: entities2, relations: relations2 };
2151
- });
2152
- const entities = extracted.flatMap((item) => item?.entities ?? []);
2153
- const relations = extracted.flatMap((item) => item?.relations ?? []);
2154
- if (entities.length === 0 && relations.length === 0) return;
2155
- const allNames = [
2156
- .../* @__PURE__ */ new Set([
2157
- ...entities.map((e) => e.name),
2158
- ...relations.flatMap((r) => [r.from, r.to])
2159
- ])
2160
- ];
2161
- if (allNames.length === 0) return;
2162
- const embeddings = /* @__PURE__ */ new Map();
2163
- try {
2164
- const vecs = await this.embed.embed(allNames);
2165
- allNames.forEach((name, i) => embeddings.set(name, vecs[i]));
2166
- } catch (err) {
2167
- console.error("[KnowledgeManager] embed entity names failed:", err);
2168
- return;
2253
+ const allNames = [
2254
+ .../* @__PURE__ */ new Set([
2255
+ ...entities.map((e) => e.name),
2256
+ ...relations.flatMap((r) => [r.from, r.to])
2257
+ ])
2258
+ ];
2259
+ if (allNames.length === 0) continue;
2260
+ const embeddings = /* @__PURE__ */ new Map();
2261
+ try {
2262
+ const vecs = await this.embed.embed(allNames);
2263
+ allNames.forEach((name, i) => embeddings.set(name, vecs[i]));
2264
+ } catch (err) {
2265
+ console.error("[KnowledgeManager] embed entity names failed:", err);
2266
+ continue;
2267
+ }
2268
+ await this.grafeo.upsertEntitiesAndRelations(
2269
+ entities,
2270
+ relations,
2271
+ embeddings,
2272
+ KIND_KNOWLEDGE
2273
+ );
2274
+ extractedAny = true;
2275
+ }
2276
+ if (extractedAny) {
2277
+ await this.lance.updateDocumentGraphFlag(doc.docId, true).catch((err) => {
2278
+ console.error(`[KnowledgeManager] Failed to update graph flag for doc ${doc.docId}:`, err);
2279
+ });
2169
2280
  }
2170
- await this.grafeo.upsertEntitiesAndRelations(
2171
- entities,
2172
- relations,
2173
- embeddings,
2174
- KIND_KNOWLEDGE
2175
- );
2176
- await this.lance.updateDocumentGraphFlag(doc.docId, true).catch((err) => {
2177
- console.error(`[KnowledgeManager] Failed to update graph flag for doc ${doc.docId}:`, err);
2178
- });
2179
2281
  }
2180
2282
  // ── 检索 ───────────────────────────────────────────────────────────────────────
2181
2283
  async searchKnowledge(opts) {
@@ -2293,12 +2395,12 @@ ${p.content}` : p.content
2293
2395
  });
2294
2396
  return true;
2295
2397
  }
2296
- async listDocuments(filter) {
2398
+ async listDocuments(filter, page) {
2297
2399
  let docs = await this.lance.getAllDocuments();
2298
2400
  if (filter?.userId) docs = docs.filter((d) => d.userId === filter.userId);
2299
2401
  if (filter?.chatId) docs = docs.filter((d) => d.chatId === filter.chatId);
2300
2402
  if (filter?.sessionId) docs = docs.filter((d) => d.sessionId === filter.sessionId);
2301
- return docs;
2403
+ return page ? paginate(docs, page) : docs;
2302
2404
  }
2303
2405
  };
2304
2406
  var NO_MATCH = Symbol("no-match");
@@ -2337,26 +2439,6 @@ function inferTitle(markdown) {
2337
2439
  }
2338
2440
  return void 0;
2339
2441
  }
2340
- async function mapLimit2(items, limit, mapper) {
2341
- const results = new Array(items.length);
2342
- let next = 0;
2343
- const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
2344
- while (true) {
2345
- const index = next++;
2346
- if (index >= items.length) return;
2347
- results[index] = await mapper(items[index], index);
2348
- }
2349
- });
2350
- await Promise.all(workers);
2351
- return results;
2352
- }
2353
- function withTimeout2(promise, timeoutMs, label) {
2354
- let timer;
2355
- const timeout = new Promise((_, reject) => {
2356
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
2357
- });
2358
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
2359
- }
2360
2442
 
2361
2443
  // src/manager/session.cache.ts
2362
2444
  var SessionCache = class {
@@ -2571,7 +2653,7 @@ var MemoryManager = class {
2571
2653
  }
2572
2654
  async flushChat(sessionId, opts) {
2573
2655
  const sid = sessionId ?? DEFAULT_SESSION_ID;
2574
- const promise = this.compressManager.triggerCompress(sid, true, opts?.waitGraph === true);
2656
+ const promise = this.compressManager.triggerCompress(sid, true);
2575
2657
  if (opts?.wait) await promise;
2576
2658
  }
2577
2659
  async updateFacts(content, level, userId, chatId, sessionId) {
@@ -2778,33 +2860,28 @@ var MemoryManager = class {
2778
2860
  const n = Math.min(365, Math.max(1, Math.floor(days)));
2779
2861
  return this.lance.trendDaily(n);
2780
2862
  }
2781
- /** 列出会话,可选按 userId / chatId 过滤,按更新时间倒序 */
2782
- listSessions(filter) {
2863
+ listSessions(filter, page) {
2783
2864
  let results = [...this.sessionMap.values()];
2784
2865
  if (filter?.userId) results = results.filter((s) => s.userId === filter.userId);
2785
2866
  if (filter?.chatId) results = results.filter((s) => s.chatId === filter.chatId);
2786
- return results.sort((a, b) => b.updatedAt - a.updatedAt);
2867
+ results.sort((a, b) => b.updatedAt - a.updatedAt || cmpStr(a.sessionId, b.sessionId));
2868
+ return page ? paginate(results, page) : results;
2787
2869
  }
2788
- /** 列出会话内的消息(按时间正序)*/
2789
- async listMessages(sessionId, limit = 100) {
2790
- return this.lance.getLatestMessages(sessionId, limit);
2870
+ async listMessages(sessionId, limitOrPage) {
2871
+ if (typeof limitOrPage === "object") {
2872
+ const all = await this.lance.getAllMessagesBySession(sessionId);
2873
+ return paginate(all, limitOrPage);
2874
+ }
2875
+ return this.lance.getLatestMessages(sessionId, limitOrPage ?? 100);
2791
2876
  }
2792
- /** 列出主题/摘要,可选按 sessionId / userId / chatId 过滤 */
2793
- async listTopics(filter) {
2877
+ async listTopics(filter, page) {
2794
2878
  let topics = await this.lance.getAllTopics();
2795
2879
  if (filter?.sessionId) topics = topics.filter((t) => t.sessionId === filter.sessionId);
2796
2880
  if (filter?.userId) topics = topics.filter((t) => t.userId === filter.userId);
2797
2881
  if (filter?.chatId) topics = topics.filter((t) => t.chatId === filter.chatId);
2798
- return topics;
2882
+ return page ? paginate(topics, page) : topics;
2799
2883
  }
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) {
2884
+ listFacts(filter, page) {
2808
2885
  let facts = this.factCache.all();
2809
2886
  if (filter?.chatId) {
2810
2887
  const chatId = filter.chatId;
@@ -2816,7 +2893,8 @@ var MemoryManager = class {
2816
2893
  facts = facts.filter((f) => f.userId === filter.userId);
2817
2894
  }
2818
2895
  if (filter?.level) facts = facts.filter((f) => f.level === filter.level);
2819
- return facts.sort((a, b) => b.createdAt - a.createdAt);
2896
+ facts.sort((a, b) => b.createdAt - a.createdAt || cmpStr(a.factId, b.factId));
2897
+ return page ? paginate(facts, page) : facts;
2820
2898
  }
2821
2899
  /** 由 chatId 反查所属 userId:优先用会话表映射,兜底用 chat 级 fact 自身。 */
2822
2900
  resolveChatOwner(chatId) {
@@ -2833,13 +2911,17 @@ var MemoryManager = class {
2833
2911
  async deleteFact(factId) {
2834
2912
  return this.factCache.remove(factId);
2835
2913
  }
2836
- /** 列出全部实体(知识图谱节点)*/
2837
- async listEntities() {
2838
- return this.grafeo.getAllEntities();
2914
+ async listEntities(filter, page) {
2915
+ if (!page) return this.grafeo.getAllEntities(filter);
2916
+ const { offset, limit } = normalizePage(page);
2917
+ const { items, total } = await this.grafeo.pageEntities(filter, page);
2918
+ return { items, total, offset, limit };
2839
2919
  }
2840
- /** 列出全部关系(知识图谱边)*/
2841
- async listRelations() {
2842
- return this.grafeo.getAllRelations();
2920
+ async listRelations(filter, page) {
2921
+ if (!page) return this.grafeo.getAllRelations(filter);
2922
+ const { offset, limit } = normalizePage(page);
2923
+ const { items, total } = await this.grafeo.pageRelations(filter, page);
2924
+ return { items, total, offset, limit };
2843
2925
  }
2844
2926
  // ── 知识库 API ────────────────────────────────────────────────────────────────
2845
2927
  /** 摄入一个文档(markdown)。document 落库即返回,切块/embedding/图谱后台执行(wait=true 可等待)。*/
@@ -2858,9 +2940,8 @@ var MemoryManager = class {
2858
2940
  async deleteDocument(docId) {
2859
2941
  return this.knowledgeManager.deleteDocument(docId);
2860
2942
  }
2861
- /** 列出文档,可选按 userId / chatId / sessionId 过滤,按更新时间倒序。*/
2862
- async listDocuments(filter) {
2863
- return this.knowledgeManager.listDocuments(filter);
2943
+ async listDocuments(filter, page) {
2944
+ return page ? this.knowledgeManager.listDocuments(filter, page) : this.knowledgeManager.listDocuments(filter);
2864
2945
  }
2865
2946
  destroy() {
2866
2947
  this.grafeo.close();