chattercatcher 0.2.5 → 0.2.7

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/cli.js CHANGED
@@ -8,7 +8,7 @@ import fs15 from "fs/promises";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "chattercatcher",
11
- version: "0.2.5",
11
+ version: "0.2.7",
12
12
  description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
13
13
  type: "module",
14
14
  main: "dist/index.js",
@@ -31,7 +31,8 @@ var package_default = {
31
31
  "docs/PRD.md",
32
32
  "docs/TECHNICAL_ARCHITECTURE.md",
33
33
  "README.md",
34
- "AGENTS.md"
34
+ "AGENTS.md",
35
+ "CHANGELOG.md"
35
36
  ],
36
37
  directories: {
37
38
  doc: "docs"
@@ -416,6 +417,7 @@ function migrateDatabase(database) {
416
417
  chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
417
418
  sender_id TEXT NOT NULL,
418
419
  sender_name TEXT NOT NULL,
420
+ person_id TEXT REFERENCES persons(id) ON DELETE SET NULL,
419
421
  message_type TEXT NOT NULL,
420
422
  text TEXT NOT NULL,
421
423
  raw_payload_json TEXT NOT NULL,
@@ -453,6 +455,80 @@ function migrateDatabase(database) {
453
455
  UNIQUE(chat_id, started_at, ended_at)
454
456
  );
455
457
 
458
+ CREATE TABLE IF NOT EXISTS persons (
459
+ id TEXT PRIMARY KEY,
460
+ primary_name TEXT NOT NULL,
461
+ notes TEXT,
462
+ created_at TEXT NOT NULL,
463
+ updated_at TEXT NOT NULL
464
+ );
465
+
466
+ CREATE TABLE IF NOT EXISTS person_identities (
467
+ id TEXT PRIMARY KEY,
468
+ person_id TEXT NOT NULL REFERENCES persons(id) ON DELETE CASCADE,
469
+ platform TEXT NOT NULL,
470
+ platform_chat_id TEXT NOT NULL,
471
+ external_user_id TEXT NOT NULL,
472
+ external_open_id TEXT,
473
+ external_union_id TEXT,
474
+ external_user_id_raw TEXT,
475
+ display_name TEXT NOT NULL,
476
+ alias TEXT,
477
+ source TEXT NOT NULL CHECK(source IN ('message','feishu_member','manual','inferred')),
478
+ first_seen_at TEXT NOT NULL,
479
+ last_seen_at TEXT NOT NULL,
480
+ UNIQUE(platform, platform_chat_id, external_user_id)
481
+ );
482
+
483
+ CREATE INDEX IF NOT EXISTS person_identities_person_idx ON person_identities(person_id);
484
+ CREATE INDEX IF NOT EXISTS person_identities_lookup_idx ON person_identities(platform, platform_chat_id, external_user_id);
485
+ CREATE INDEX IF NOT EXISTS person_identities_name_idx ON person_identities(display_name, alias);
486
+
487
+ CREATE TABLE IF NOT EXISTS person_profile_entries (
488
+ id TEXT PRIMARY KEY,
489
+ person_id TEXT NOT NULL REFERENCES persons(id) ON DELETE CASCADE,
490
+ category TEXT NOT NULL,
491
+ content TEXT NOT NULL,
492
+ entry_type TEXT NOT NULL CHECK(entry_type IN ('fact','inferred')),
493
+ confidence REAL NOT NULL,
494
+ status TEXT NOT NULL CHECK(status IN ('active','superseded','deleted')),
495
+ source TEXT NOT NULL CHECK(source IN ('dream','explicit_user_request','manual')),
496
+ created_at TEXT NOT NULL,
497
+ updated_at TEXT NOT NULL,
498
+ last_observed_at TEXT NOT NULL
499
+ );
500
+
501
+ CREATE INDEX IF NOT EXISTS person_profile_entries_person_status_idx ON person_profile_entries(person_id, status, updated_at);
502
+
503
+ CREATE TABLE IF NOT EXISTS person_profile_evidence (
504
+ entry_id TEXT NOT NULL REFERENCES person_profile_entries(id) ON DELETE CASCADE,
505
+ message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
506
+ quote TEXT NOT NULL,
507
+ reason TEXT NOT NULL,
508
+ PRIMARY KEY (entry_id, message_id, quote)
509
+ );
510
+
511
+ CREATE TABLE IF NOT EXISTS profile_dream_state (
512
+ platform TEXT NOT NULL,
513
+ platform_chat_id TEXT NOT NULL,
514
+ last_message_id TEXT,
515
+ last_message_sent_at TEXT,
516
+ updated_at TEXT NOT NULL,
517
+ PRIMARY KEY (platform, platform_chat_id)
518
+ );
519
+
520
+ CREATE TABLE IF NOT EXISTS profile_dream_runs (
521
+ id TEXT PRIMARY KEY,
522
+ platform TEXT NOT NULL,
523
+ platform_chat_id TEXT NOT NULL,
524
+ status TEXT NOT NULL CHECK(status IN ('succeeded','failed','skipped')),
525
+ processed_message_count INTEGER NOT NULL,
526
+ generated_entry_count INTEGER NOT NULL,
527
+ error TEXT,
528
+ started_at TEXT NOT NULL,
529
+ finished_at TEXT NOT NULL
530
+ );
531
+
456
532
  CREATE TABLE IF NOT EXISTS memory_episode_messages (
457
533
  episode_id TEXT NOT NULL REFERENCES memory_episodes(id) ON DELETE CASCADE,
458
534
  message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
@@ -495,6 +571,7 @@ function migrateDatabase(database) {
495
571
  answer TEXT NOT NULL,
496
572
  citations_json TEXT NOT NULL,
497
573
  retrieval_debug_json TEXT NOT NULL,
574
+ trace_json TEXT NOT NULL DEFAULT '{}',
498
575
  status TEXT NOT NULL CHECK(status IN ('answered','failed')),
499
576
  error TEXT,
500
577
  created_at TEXT NOT NULL
@@ -579,6 +656,11 @@ function migrateDatabase(database) {
579
656
  CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
580
657
  ON feishu_chat_members(chat_id, user_name);
581
658
  `);
659
+ const messageColumns = database.prepare("PRAGMA table_info(messages)").all();
660
+ if (!messageColumns.some((column) => column.name === "person_id")) {
661
+ database.prepare("ALTER TABLE messages ADD COLUMN person_id TEXT REFERENCES persons(id) ON DELETE SET NULL").run();
662
+ }
663
+ database.prepare("CREATE INDEX IF NOT EXISTS messages_person_idx ON messages(person_id, sent_at)").run();
582
664
  const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
583
665
  const ensureCronJobColumn = (name, definition) => {
584
666
  if (!cronJobColumns.some((column) => column.name === name)) {
@@ -589,6 +671,10 @@ function migrateDatabase(database) {
589
671
  ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
590
672
  ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
591
673
  ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
674
+ const qaLogColumns = database.prepare("PRAGMA table_info(qa_logs)").all();
675
+ if (!qaLogColumns.some((column) => column.name === "trace_json")) {
676
+ database.prepare("ALTER TABLE qa_logs ADD COLUMN trace_json TEXT NOT NULL DEFAULT '{}'").run();
677
+ }
592
678
  }
593
679
 
594
680
  // src/doctor/checks.ts
@@ -1284,6 +1370,10 @@ function buildScopeWhere(scope) {
1284
1370
  clauses.push("c.platform_chat_id = ?");
1285
1371
  params.push(scope.platformChatId);
1286
1372
  }
1373
+ if (scope?.personId) {
1374
+ clauses.push("m.person_id = ?");
1375
+ params.push(scope.personId);
1376
+ }
1287
1377
  return {
1288
1378
  where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
1289
1379
  params
@@ -1328,14 +1418,15 @@ var MessageRepository = class {
1328
1418
  `
1329
1419
  INSERT INTO messages (
1330
1420
  id, platform, platform_message_id, chat_id, sender_id, sender_name,
1331
- message_type, text, raw_payload_json, sent_at, received_at, created_at
1421
+ person_id, message_type, text, raw_payload_json, sent_at, received_at, created_at
1332
1422
  )
1333
1423
  VALUES (
1334
1424
  @id, @platform, @platformMessageId, @chatId, @senderId, @senderName,
1335
- @messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
1425
+ @personId, @messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
1336
1426
  )
1337
1427
  ON CONFLICT(platform, platform_message_id)
1338
1428
  DO UPDATE SET
1429
+ person_id = COALESCE(excluded.person_id, messages.person_id),
1339
1430
  message_type = excluded.message_type,
1340
1431
  text = excluded.text,
1341
1432
  raw_payload_json = excluded.raw_payload_json,
@@ -1348,6 +1439,7 @@ var MessageRepository = class {
1348
1439
  chatId,
1349
1440
  senderId: input2.senderId,
1350
1441
  senderName: input2.senderName,
1442
+ personId: input2.personId ?? null,
1351
1443
  messageType: input2.messageType,
1352
1444
  text: input2.text,
1353
1445
  rawPayloadJson,
@@ -1390,6 +1482,7 @@ var MessageRepository = class {
1390
1482
  m.chat_id AS chatId,
1391
1483
  m.sender_id AS senderId,
1392
1484
  m.sender_name AS senderName,
1485
+ m.person_id AS personId,
1393
1486
  m.sent_at AS sentAt,
1394
1487
  c.platform_chat_id AS platformChatId,
1395
1488
  c.name AS chatName
@@ -1412,6 +1505,7 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1412
1505
  platformMessageId: derivedPlatformMessageId,
1413
1506
  senderId: source.senderId,
1414
1507
  senderName: source.senderName,
1508
+ personId: source.personId ?? void 0,
1415
1509
  messageType: "image_summary",
1416
1510
  text: summaryText,
1417
1511
  sentAt: source.sentAt,
@@ -1438,7 +1532,9 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1438
1532
  1.0 AS score,
1439
1533
  m.message_type AS messageType,
1440
1534
  c.name AS chatName,
1535
+ m.sender_id AS senderId,
1441
1536
  m.sender_name AS senderName,
1537
+ m.person_id AS personId,
1442
1538
  m.sent_at AS sentAt
1443
1539
  FROM message_chunks mc
1444
1540
  JOIN messages m ON m.id = mc.message_id
@@ -1459,7 +1555,9 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1459
1555
  1.0 AS score,
1460
1556
  m.message_type AS messageType,
1461
1557
  c.name AS chatName,
1558
+ m.sender_id AS senderId,
1462
1559
  m.sender_name AS senderName,
1560
+ m.person_id AS personId,
1463
1561
  m.sent_at AS sentAt
1464
1562
  FROM message_chunks mc
1465
1563
  JOIN messages m ON m.id = mc.message_id
@@ -1483,7 +1581,9 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1483
1581
  1.0 AS score,
1484
1582
  m.message_type AS messageType,
1485
1583
  c.name AS chatName,
1584
+ m.sender_id AS senderId,
1486
1585
  m.sender_name AS senderName,
1586
+ m.person_id AS personId,
1487
1587
  m.sent_at AS sentAt
1488
1588
  FROM message_chunks mc
1489
1589
  JOIN messages m ON m.id = mc.message_id
@@ -1499,7 +1599,7 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1499
1599
  const excludedIds = options.excludeMessageIds ?? [];
1500
1600
  const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
1501
1601
  const scope = buildScopeWhere(options.scope);
1502
- const ftsResults = this.database.prepare(
1602
+ const ftsRows = this.database.prepare(
1503
1603
  `
1504
1604
  SELECT
1505
1605
  fts.chunk_id AS chunkId,
@@ -1509,8 +1609,11 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1509
1609
  bm25(message_chunks_fts) * -1 AS score,
1510
1610
  m.message_type AS messageType,
1511
1611
  c.name AS chatName,
1612
+ m.sender_id AS senderId,
1512
1613
  m.sender_name AS senderName,
1513
- m.sent_at AS sentAt
1614
+ m.person_id AS personId,
1615
+ m.sent_at AS sentAt,
1616
+ mc.chunk_index AS chunkIndex
1514
1617
  FROM message_chunks_fts fts
1515
1618
  JOIN message_chunks mc ON mc.id = fts.chunk_id
1516
1619
  JOIN messages m ON m.id = fts.message_id
@@ -1518,10 +1621,23 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1518
1621
  WHERE message_chunks_fts MATCH ?
1519
1622
  ${excludedWhere}
1520
1623
  ${scope.where}
1521
- ORDER BY bm25(message_chunks_fts)
1624
+ ORDER BY bm25(message_chunks_fts), m.sent_at DESC, mc.chunk_index ASC
1522
1625
  LIMIT ?
1523
1626
  `
1524
- ).all(ftsQuery, ...excludedIds, ...scope.params, limit);
1627
+ ).all(ftsQuery, ...excludedIds, ...scope.params, Math.max(limit * 8, limit));
1628
+ const ftsResults = [];
1629
+ const seenMessageIds = /* @__PURE__ */ new Set();
1630
+ for (const row of ftsRows) {
1631
+ if (seenMessageIds.has(row.messageId)) {
1632
+ continue;
1633
+ }
1634
+ seenMessageIds.add(row.messageId);
1635
+ const { chunkIndex: _chunkIndex, ...result } = row;
1636
+ ftsResults.push(result);
1637
+ if (ftsResults.length >= limit) {
1638
+ break;
1639
+ }
1640
+ }
1525
1641
  if (ftsResults.length > 0) {
1526
1642
  return ftsResults;
1527
1643
  }
@@ -1535,22 +1651,30 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
1535
1651
  return this.database.prepare(
1536
1652
  `
1537
1653
  SELECT
1538
- mc.id AS chunkId,
1539
- m.id AS messageId,
1540
- m.platform AS platform,
1541
- mc.text AS text,
1542
- 0.1 AS score,
1543
- m.message_type AS messageType,
1544
- c.name AS chatName,
1545
- m.sender_name AS senderName,
1546
- m.sent_at AS sentAt
1547
- FROM message_chunks mc
1548
- JOIN messages m ON m.id = mc.message_id
1549
- JOIN chats c ON c.id = m.chat_id
1550
- WHERE (${where})
1551
- ${likeExcludedWhere}
1552
- ${scope.where}
1553
- ORDER BY m.sent_at DESC
1654
+ *
1655
+ FROM (
1656
+ SELECT
1657
+ mc.id AS chunkId,
1658
+ m.id AS messageId,
1659
+ m.platform AS platform,
1660
+ mc.text AS text,
1661
+ 0.1 AS score,
1662
+ m.message_type AS messageType,
1663
+ c.name AS chatName,
1664
+ m.sender_id AS senderId,
1665
+ m.sender_name AS senderName,
1666
+ m.person_id AS personId,
1667
+ m.sent_at AS sentAt,
1668
+ ROW_NUMBER() OVER (PARTITION BY m.id ORDER BY mc.chunk_index ASC) AS rowNumber
1669
+ FROM message_chunks mc
1670
+ JOIN messages m ON m.id = mc.message_id
1671
+ JOIN chats c ON c.id = m.chat_id
1672
+ WHERE (${where})
1673
+ ${likeExcludedWhere}
1674
+ ${scope.where}
1675
+ ) ranked
1676
+ WHERE rowNumber = 1
1677
+ ORDER BY sentAt DESC
1554
1678
  LIMIT ?
1555
1679
  `
1556
1680
  ).all(...params, ...excludedIds, ...scope.params, limit);
@@ -1965,19 +2089,20 @@ var HybridRetriever = class {
1965
2089
 
1966
2090
  // src/rag/message-retriever.ts
1967
2091
  function toEvidenceSource(result) {
1968
- if (result.messageType === "file") {
1969
- return {
1970
- type: "file",
1971
- label: result.senderName,
1972
- timestamp: result.sentAt
1973
- };
1974
- }
1975
- return {
1976
- type: "message",
1977
- label: result.chatName,
1978
- sender: result.senderName,
2092
+ const source = {
2093
+ type: result.messageType === "file" ? "file" : "message",
2094
+ label: result.messageType === "file" ? result.senderName : result.chatName,
1979
2095
  timestamp: result.sentAt
1980
2096
  };
2097
+ if (result.messageType !== "file") {
2098
+ source.sender = result.senderName;
2099
+ }
2100
+ source.senderId = result.senderId;
2101
+ source.profileAvailable = Boolean(result.personId);
2102
+ if (result.personId) {
2103
+ source.personId = result.personId;
2104
+ }
2105
+ return source;
1981
2106
  }
1982
2107
  var MessageFtsRetriever = class {
1983
2108
  constructor(messages, options = {}) {
@@ -2106,6 +2231,9 @@ function toEvidenceSource2(row) {
2106
2231
  type: "message",
2107
2232
  label: row.chatName,
2108
2233
  sender: row.senderName,
2234
+ senderId: row.senderId,
2235
+ personId: row.personId,
2236
+ profileAvailable: Boolean(row.personId),
2109
2237
  timestamp: row.sentAt
2110
2238
  };
2111
2239
  }
@@ -2120,6 +2248,10 @@ function buildScopeWhere3(scope) {
2120
2248
  clauses.push("c.platform_chat_id = ?");
2121
2249
  params.push(scope.platformChatId);
2122
2250
  }
2251
+ if (scope?.personId) {
2252
+ clauses.push("m.person_id = ?");
2253
+ params.push(scope.personId);
2254
+ }
2123
2255
  return {
2124
2256
  where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
2125
2257
  params
@@ -2170,7 +2302,9 @@ var SqliteVectorStore = class {
2170
2302
  mc.id AS chunkId,
2171
2303
  mc.text AS text,
2172
2304
  c.name AS chatName,
2305
+ m.sender_id AS senderId,
2173
2306
  m.sender_name AS senderName,
2307
+ m.person_id AS personId,
2174
2308
  m.sent_at AS sentAt,
2175
2309
  e.embedding_json AS embeddingJson
2176
2310
  FROM message_chunk_embeddings e
@@ -2251,8 +2385,12 @@ async function createAgenticRagSearchTools(input2) {
2251
2385
  new SqliteVectorStore(input2.database, { model: input2.config.embedding.model })
2252
2386
  ) : void 0;
2253
2387
  const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
2388
+ const tools = createRagSearchTools({ hybrid, messages, episodes, semantic, scope: input2.scope });
2389
+ if (input2.profileTools && input2.profileTools.length > 0) {
2390
+ tools.push(...input2.profileTools);
2391
+ }
2254
2392
  return {
2255
- tools: createRagSearchTools({ hybrid, messages, episodes, semantic, scope: input2.scope }),
2393
+ tools,
2256
2394
  close: () => {
2257
2395
  }
2258
2396
  };
@@ -3402,12 +3540,594 @@ function parseExactNumber2(field, min, max) {
3402
3540
  if (!/^\d+$/.test(field)) {
3403
3541
  return null;
3404
3542
  }
3405
- const value = Number(field);
3406
- if (!Number.isInteger(value) || value < min || value > max) {
3407
- return null;
3543
+ const value = Number(field);
3544
+ if (!Number.isInteger(value) || value < min || value > max) {
3545
+ return null;
3546
+ }
3547
+ return value;
3548
+ }
3549
+
3550
+ // src/profiles/rag-tools.ts
3551
+ var getProfileInputSchema = {
3552
+ type: "object",
3553
+ properties: {
3554
+ personId: { type: "string", description: "Stable person identifier from retrieved evidence." },
3555
+ senderId: { type: "string", description: "Message sender id when personId is unavailable." },
3556
+ platformChatId: { type: "string", description: "Chat id paired with senderId for profile lookup." },
3557
+ includeEvidence: { type: "boolean", description: "Whether to include evidence snippets in the profile text." },
3558
+ includeInferred: { type: "boolean", description: "Whether to include inferred profile entries." }
3559
+ },
3560
+ additionalProperties: false
3561
+ };
3562
+ var searchMessagesInputSchema = {
3563
+ type: "object",
3564
+ properties: {
3565
+ personId: { type: "string", description: "The stable person identifier whose messages to search." },
3566
+ query: { type: "string", description: "Search query written by the model." },
3567
+ limit: { type: "number", description: "Maximum number of evidence blocks to return." }
3568
+ },
3569
+ required: ["personId", "query"],
3570
+ additionalProperties: false
3571
+ };
3572
+ function readString(value) {
3573
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
3574
+ }
3575
+ function resolvePersonId(profiles, input2) {
3576
+ const raw = typeof input2 === "object" && input2 !== null ? input2 : {};
3577
+ const personId = readString(raw.personId);
3578
+ if (personId) return personId;
3579
+ const senderId = readString(raw.senderId);
3580
+ const platformChatId = readString(raw.platformChatId);
3581
+ if (senderId && platformChatId) {
3582
+ const resolved = profiles.resolvePersonIdForSender({ senderId, platformChatId });
3583
+ if (resolved) return resolved;
3584
+ }
3585
+ throw new Error("personId \u6216 senderId + platformChatId \u5FC5\u987B\u63D0\u4F9B\u3002");
3586
+ }
3587
+ function parseBoolean(input2, key, defaultValue) {
3588
+ const value = typeof input2 === "object" && input2 !== null ? input2[key] : void 0;
3589
+ return typeof value === "boolean" ? value : defaultValue;
3590
+ }
3591
+ function parseQuery(input2) {
3592
+ const rawQuery = typeof input2 === "object" && input2 !== null && "query" in input2 ? input2.query : void 0;
3593
+ if (typeof rawQuery !== "string") {
3594
+ throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
3595
+ }
3596
+ const query = rawQuery.trim();
3597
+ if (!query) {
3598
+ throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
3599
+ }
3600
+ return query;
3601
+ }
3602
+ function parseLimit(input2) {
3603
+ const rawLimit = typeof input2 === "object" && input2 !== null && "limit" in input2 ? input2.limit : void 0;
3604
+ const numericLimit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? rawLimit : 5;
3605
+ return Math.min(12, Math.max(1, Math.floor(numericLimit)));
3606
+ }
3607
+ function createGetPersonProfileTool(profiles) {
3608
+ return {
3609
+ name: "get_person_profile",
3610
+ description: "Retrieve an evidence-backed profile for a person. Use this when the question depends on who someone is, their role, preferences, personality, relationships, or recent state.",
3611
+ inputSchema: getProfileInputSchema,
3612
+ execute: async (input2) => {
3613
+ const personId = resolvePersonId(profiles, input2);
3614
+ const includeEvidence = parseBoolean(input2, "includeEvidence", false);
3615
+ const includeInferred = parseBoolean(input2, "includeInferred", true);
3616
+ const profile = profiles.getPersonProfile(personId, { includeEvidence, includeInferred });
3617
+ if (!profile) {
3618
+ return [];
3619
+ }
3620
+ const aliases = profile.identities.map((identity) => identity.displayName).filter(Boolean).join("\u3001");
3621
+ const entries = profile.entries.map((entry) => {
3622
+ const evidence = includeEvidence && entry.evidence?.length ? `
3623
+ \u8BC1\u636E\uFF1A${entry.evidence.map((item) => `${item.quote}\uFF08${item.reason}\uFF09`).join("\uFF1B")}` : "";
3624
+ return `- [${entry.entryType}] ${entry.category}\uFF1A${entry.content}\uFF08\u7F6E\u4FE1\u5EA6 ${entry.confidence}\uFF0C\u6765\u6E90 ${entry.source}\uFF09${evidence}`;
3625
+ });
3626
+ return [{
3627
+ id: `person_profile:${profile.person.id}`,
3628
+ text: [`\u4EBA\u7269\uFF1A${profile.person.primaryName}`, aliases ? `\u8EAB\u4EFD/\u6635\u79F0\uFF1A${aliases}` : void 0, ...entries].filter(Boolean).join("\n"),
3629
+ score: 1,
3630
+ source: {
3631
+ type: "person_profile",
3632
+ label: profile.person.primaryName,
3633
+ personId: profile.person.id,
3634
+ profileAvailable: true
3635
+ }
3636
+ }];
3637
+ }
3638
+ };
3639
+ }
3640
+ function createSearchPersonMessagesTool(profiles) {
3641
+ return {
3642
+ name: "search_person_messages",
3643
+ description: "Search chat messages sent by a specific person only. Use this when the question is explicitly about what a particular person said, or when you need to find messages from a specific person.",
3644
+ inputSchema: searchMessagesInputSchema,
3645
+ execute: async (input2) => {
3646
+ const personId = resolvePersonId(profiles, input2);
3647
+ const query = parseQuery(input2);
3648
+ const limit = parseLimit(input2);
3649
+ const results = profiles.searchPersonMessages(personId, query, limit);
3650
+ return results.map((result) => ({
3651
+ id: result.chunkId,
3652
+ text: result.text,
3653
+ score: result.score,
3654
+ source: {
3655
+ type: result.messageType === "file" ? "file" : "message",
3656
+ label: result.chatName,
3657
+ sender: result.senderName,
3658
+ senderId: result.senderId,
3659
+ timestamp: result.sentAt,
3660
+ personId: result.personId ?? void 0,
3661
+ profileAvailable: Boolean(result.personId)
3662
+ }
3663
+ }));
3664
+ }
3665
+ };
3666
+ }
3667
+ function createPersonProfileTools({ profiles }) {
3668
+ return [createGetPersonProfileTool(profiles), createSearchPersonMessagesTool(profiles)];
3669
+ }
3670
+
3671
+ // src/profiles/repository.ts
3672
+ import crypto5 from "crypto";
3673
+ function nowIso4() {
3674
+ return (/* @__PURE__ */ new Date()).toISOString();
3675
+ }
3676
+ function createId(prefix, parts) {
3677
+ return `${prefix}_${crypto5.createHash("sha256").update(parts.join("")).digest("hex").slice(0, 24)}`;
3678
+ }
3679
+ function mapPerson(row) {
3680
+ return {
3681
+ id: row.id,
3682
+ primaryName: row.primaryName,
3683
+ notes: row.notes ?? void 0,
3684
+ createdAt: row.createdAt,
3685
+ updatedAt: row.updatedAt
3686
+ };
3687
+ }
3688
+ var ProfileRepository = class {
3689
+ constructor(database) {
3690
+ this.database = database;
3691
+ }
3692
+ database;
3693
+ resolvePersonForSender(input2) {
3694
+ const observedAt = input2.observedAt ?? nowIso4();
3695
+ const personId = createId("person", [input2.platform, input2.platformChatId, input2.senderId]);
3696
+ const identityId = createId("identity", [input2.platform, input2.platformChatId, input2.senderId]);
3697
+ const findPerson = this.database.prepare(
3698
+ `
3699
+ SELECT id, primary_name AS primaryName, notes, created_at AS createdAt, updated_at AS updatedAt
3700
+ FROM persons
3701
+ WHERE id = ?
3702
+ `
3703
+ );
3704
+ const transaction = this.database.transaction(() => {
3705
+ this.database.prepare(
3706
+ `
3707
+ INSERT INTO persons (id, primary_name, notes, created_at, updated_at)
3708
+ VALUES (?, ?, NULL, ?, ?)
3709
+ ON CONFLICT(id) DO NOTHING
3710
+ `
3711
+ ).run(personId, input2.senderName, observedAt, observedAt);
3712
+ this.database.prepare(
3713
+ `
3714
+ INSERT INTO person_identities (
3715
+ id, person_id, platform, platform_chat_id, external_user_id, external_open_id,
3716
+ external_union_id, external_user_id_raw, display_name, alias, source, first_seen_at, last_seen_at
3717
+ ) VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, ?, NULL, ?, ?, ?)
3718
+ ON CONFLICT(platform, platform_chat_id, external_user_id)
3719
+ DO UPDATE SET
3720
+ display_name = excluded.display_name,
3721
+ source = excluded.source,
3722
+ last_seen_at = excluded.last_seen_at
3723
+ `
3724
+ ).run(identityId, personId, input2.platform, input2.platformChatId, input2.senderId, input2.senderId, input2.senderName, input2.source, observedAt, observedAt);
3725
+ this.database.prepare(
3726
+ `
3727
+ UPDATE persons
3728
+ SET primary_name = ?, updated_at = ?
3729
+ WHERE id = ?
3730
+ `
3731
+ ).run(input2.senderName, observedAt, personId);
3732
+ return findPerson.get(personId);
3733
+ });
3734
+ return transaction();
3735
+ }
3736
+ listPersons() {
3737
+ const rows = this.database.prepare(
3738
+ `
3739
+ SELECT id, primary_name AS primaryName, notes, created_at AS createdAt, updated_at AS updatedAt
3740
+ FROM persons
3741
+ ORDER BY updated_at DESC, created_at DESC
3742
+ `
3743
+ ).all();
3744
+ return rows.map(mapPerson);
3745
+ }
3746
+ getPersonProfile(personId, options = {}) {
3747
+ const personRow = this.database.prepare(
3748
+ `
3749
+ SELECT id, primary_name AS primaryName, notes, created_at AS createdAt, updated_at AS updatedAt
3750
+ FROM persons
3751
+ WHERE id = ?
3752
+ `
3753
+ ).get(personId);
3754
+ if (!personRow) {
3755
+ return void 0;
3756
+ }
3757
+ const includeInferred = options.includeInferred ?? true;
3758
+ const entryRows = this.database.prepare(
3759
+ `
3760
+ SELECT
3761
+ id,
3762
+ person_id AS personId,
3763
+ category,
3764
+ content,
3765
+ entry_type AS entryType,
3766
+ confidence,
3767
+ status,
3768
+ source,
3769
+ created_at AS createdAt,
3770
+ updated_at AS updatedAt,
3771
+ last_observed_at AS lastObservedAt
3772
+ FROM person_profile_entries
3773
+ WHERE person_id = ?
3774
+ AND status = 'active'
3775
+ ${includeInferred ? "" : "AND entry_type = 'fact'"}
3776
+ ORDER BY updated_at DESC, created_at DESC
3777
+ `
3778
+ ).all(personId);
3779
+ const entries = options.includeEvidence ? entryRows.map((entry) => ({ ...entry, evidence: this.getEvidence(entry.id) })) : entryRows;
3780
+ const identities = this.database.prepare(
3781
+ `
3782
+ SELECT
3783
+ platform,
3784
+ platform_chat_id AS platformChatId,
3785
+ external_user_id AS externalUserId,
3786
+ display_name AS displayName,
3787
+ alias,
3788
+ source,
3789
+ first_seen_at AS firstSeenAt,
3790
+ last_seen_at AS lastSeenAt
3791
+ FROM person_identities
3792
+ WHERE person_id = ?
3793
+ ORDER BY last_seen_at DESC, first_seen_at DESC
3794
+ `
3795
+ ).all(personId);
3796
+ return {
3797
+ person: mapPerson(personRow),
3798
+ identities,
3799
+ entries
3800
+ };
3801
+ }
3802
+ upsertProfileEntry(input2) {
3803
+ if (input2.evidence.length === 0) {
3804
+ throw new Error("Profile entry evidence is required.");
3805
+ }
3806
+ const timestamp = input2.observedAt ?? nowIso4();
3807
+ const entryId = createId("profile_entry", [input2.personId, input2.category, input2.content]);
3808
+ const transaction = this.database.transaction(() => {
3809
+ this.database.prepare(
3810
+ `
3811
+ INSERT INTO person_profile_entries (
3812
+ id, person_id, category, content, entry_type, confidence, status, source, created_at, updated_at, last_observed_at
3813
+ ) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
3814
+ ON CONFLICT(id) DO UPDATE SET
3815
+ confidence = MAX(person_profile_entries.confidence, excluded.confidence),
3816
+ status = 'active',
3817
+ source = excluded.source,
3818
+ updated_at = excluded.updated_at,
3819
+ last_observed_at = excluded.last_observed_at
3820
+ `
3821
+ ).run(entryId, input2.personId, input2.category, input2.content, input2.entryType, input2.confidence, input2.source, timestamp, timestamp, timestamp);
3822
+ const insertEvidence = this.database.prepare(
3823
+ `
3824
+ INSERT INTO person_profile_evidence (entry_id, message_id, quote, reason)
3825
+ VALUES (?, ?, ?, ?)
3826
+ ON CONFLICT(entry_id, message_id, quote) DO UPDATE SET reason = excluded.reason
3827
+ `
3828
+ );
3829
+ for (const evidence of input2.evidence) {
3830
+ insertEvidence.run(entryId, evidence.messageId, evidence.quote, evidence.reason);
3831
+ }
3832
+ });
3833
+ transaction();
3834
+ return entryId;
3835
+ }
3836
+ backfillMessagePersons({ limit }) {
3837
+ const rows = this.database.prepare(
3838
+ `
3839
+ SELECT
3840
+ m.id AS id,
3841
+ m.platform AS platform,
3842
+ c.platform_chat_id AS platformChatId,
3843
+ m.sender_id AS senderId,
3844
+ m.sender_name AS senderName,
3845
+ m.sent_at AS sentAt
3846
+ FROM messages m
3847
+ JOIN chats c ON c.id = m.chat_id
3848
+ WHERE m.person_id IS NULL
3849
+ ORDER BY m.sent_at ASC
3850
+ LIMIT ?
3851
+ `
3852
+ ).all(limit);
3853
+ const update = this.database.prepare("UPDATE messages SET person_id = ? WHERE id = ?");
3854
+ const transaction = this.database.transaction(() => {
3855
+ for (const row of rows) {
3856
+ const person = this.resolvePersonForSender({
3857
+ platform: row.platform,
3858
+ platformChatId: row.platformChatId,
3859
+ senderId: row.senderId,
3860
+ senderName: row.senderName,
3861
+ source: "inferred",
3862
+ observedAt: row.sentAt
3863
+ });
3864
+ update.run(person.id, row.id);
3865
+ }
3866
+ });
3867
+ transaction();
3868
+ return { updatedMessages: rows.length };
3869
+ }
3870
+ getDreamState(platform, platformChatId) {
3871
+ return this.database.prepare(
3872
+ `
3873
+ SELECT
3874
+ platform,
3875
+ platform_chat_id AS platformChatId,
3876
+ last_message_id AS lastMessageId,
3877
+ last_message_sent_at AS lastMessageSentAt,
3878
+ updated_at AS updatedAt
3879
+ FROM profile_dream_state
3880
+ WHERE platform = ? AND platform_chat_id = ?
3881
+ `
3882
+ ).get(platform, platformChatId);
3883
+ }
3884
+ updateDreamState(input2) {
3885
+ this.database.prepare(
3886
+ `
3887
+ INSERT INTO profile_dream_state (platform, platform_chat_id, last_message_id, last_message_sent_at, updated_at)
3888
+ VALUES (?, ?, ?, ?, ?)
3889
+ ON CONFLICT(platform, platform_chat_id)
3890
+ DO UPDATE SET
3891
+ last_message_id = excluded.last_message_id,
3892
+ last_message_sent_at = excluded.last_message_sent_at,
3893
+ updated_at = excluded.updated_at
3894
+ `
3895
+ ).run(input2.platform, input2.platformChatId, input2.lastMessageId ?? null, input2.lastMessageSentAt ?? null, input2.updatedAt);
3896
+ }
3897
+ listMessagesForDream(input2) {
3898
+ const afterWhere = input2.afterSentAt ? "AND m.sent_at > ?" : "";
3899
+ const params = input2.afterSentAt ? [input2.platform, input2.platformChatId, input2.afterSentAt, input2.limit] : [input2.platform, input2.platformChatId, input2.limit];
3900
+ return this.database.prepare(
3901
+ `
3902
+ SELECT
3903
+ m.id AS messageId,
3904
+ m.person_id AS personId,
3905
+ m.sender_name AS senderName,
3906
+ m.sent_at AS sentAt,
3907
+ m.text AS text
3908
+ FROM messages m
3909
+ JOIN chats c ON c.id = m.chat_id
3910
+ WHERE m.platform = ?
3911
+ AND c.platform_chat_id = ?
3912
+ AND m.person_id IS NOT NULL
3913
+ ${afterWhere}
3914
+ ORDER BY m.sent_at ASC, m.created_at ASC
3915
+ LIMIT ?
3916
+ `
3917
+ ).all(...params);
3918
+ }
3919
+ listChatsWithPendingDreamMessages() {
3920
+ return this.database.prepare(
3921
+ `
3922
+ SELECT DISTINCT m.platform AS platform, c.platform_chat_id AS platformChatId
3923
+ FROM messages m
3924
+ JOIN chats c ON c.id = m.chat_id
3925
+ LEFT JOIN profile_dream_state pds ON pds.platform = m.platform AND pds.platform_chat_id = c.platform_chat_id
3926
+ WHERE m.person_id IS NOT NULL
3927
+ AND (pds.last_message_sent_at IS NULL OR m.sent_at > pds.last_message_sent_at)
3928
+ ORDER BY c.platform_chat_id ASC
3929
+ `
3930
+ ).all();
3931
+ }
3932
+ recordDreamRun(input2) {
3933
+ const id = input2.id ?? createId("profile_dream_run", [input2.platform, input2.platformChatId, input2.status, input2.startedAt, input2.finishedAt, crypto5.randomUUID()]);
3934
+ this.database.prepare(
3935
+ `
3936
+ INSERT INTO profile_dream_runs (
3937
+ id, platform, platform_chat_id, status, processed_message_count, generated_entry_count, error, started_at, finished_at
3938
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3939
+ `
3940
+ ).run(
3941
+ id,
3942
+ input2.platform,
3943
+ input2.platformChatId,
3944
+ input2.status,
3945
+ input2.processedMessageCount,
3946
+ input2.generatedEntryCount,
3947
+ input2.error ?? null,
3948
+ input2.startedAt,
3949
+ input2.finishedAt
3950
+ );
3951
+ return id;
3952
+ }
3953
+ resolvePersonIdForSender(input2) {
3954
+ const platform = input2.platform ?? "feishu";
3955
+ const row = this.database.prepare(
3956
+ `
3957
+ SELECT person_id AS personId
3958
+ FROM person_identities
3959
+ WHERE platform = ? AND platform_chat_id = ? AND external_user_id = ?
3960
+ LIMIT 1
3961
+ `
3962
+ ).get(platform, input2.platformChatId, input2.senderId);
3963
+ return row?.personId;
3964
+ }
3965
+ searchPersonMessages(personId, query, limit, options = {}) {
3966
+ const cleaned = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter(Boolean);
3967
+ const wrapped = cleaned.map((term) => `"${term}"`).join(" ");
3968
+ if (!wrapped) {
3969
+ return [];
3970
+ }
3971
+ const excludedIds = options.excludeMessageIds ?? [];
3972
+ const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
3973
+ const rows = this.database.prepare(
3974
+ `
3975
+ SELECT
3976
+ fts.chunk_id AS chunkId,
3977
+ fts.message_id AS messageId,
3978
+ m.platform AS platform,
3979
+ mc.text AS text,
3980
+ bm25(message_chunks_fts) * -1 AS score,
3981
+ m.message_type AS messageType,
3982
+ c.name AS chatName,
3983
+ m.sender_id AS senderId,
3984
+ m.sender_name AS senderName,
3985
+ m.person_id AS personId,
3986
+ m.sent_at AS sentAt,
3987
+ mc.chunk_index AS chunkIndex
3988
+ FROM message_chunks_fts fts
3989
+ JOIN message_chunks mc ON mc.id = fts.chunk_id
3990
+ JOIN messages m ON m.id = fts.message_id
3991
+ JOIN chats c ON c.id = m.chat_id
3992
+ WHERE message_chunks_fts MATCH ?
3993
+ ${excludedWhere}
3994
+ AND m.person_id = ?
3995
+ ORDER BY bm25(message_chunks_fts), m.sent_at DESC, mc.chunk_index ASC
3996
+ LIMIT ?
3997
+ `
3998
+ ).all(wrapped, ...excludedIds, personId, Math.max(limit * 8, limit));
3999
+ const results = [];
4000
+ const seenMessageIds = /* @__PURE__ */ new Set();
4001
+ for (const row of rows) {
4002
+ if (seenMessageIds.has(row.messageId)) {
4003
+ continue;
4004
+ }
4005
+ seenMessageIds.add(row.messageId);
4006
+ const { chunkIndex: _chunkIndex, ...result } = row;
4007
+ results.push(result);
4008
+ if (results.length >= limit) {
4009
+ break;
4010
+ }
4011
+ }
4012
+ if (results.length > 0) {
4013
+ return results;
4014
+ }
4015
+ const terms = query.split(/[  ]+/).map((term) => term.trim()).filter((term) => term.length > 0);
4016
+ if (terms.length === 0) {
4017
+ return [];
4018
+ }
4019
+ const where = terms.map(() => "mc.text LIKE ? ESCAPE '\\'").join(" OR ");
4020
+ const params = terms.map((term) => `%${term.replace(/[%_]/g, "\\$&")}%`);
4021
+ const likeExcludedWhere = excludedIds.length > 0 ? `AND m.id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
4022
+ return this.database.prepare(
4023
+ `
4024
+ SELECT
4025
+ *
4026
+ FROM (
4027
+ SELECT
4028
+ mc.id AS chunkId,
4029
+ m.id AS messageId,
4030
+ m.platform AS platform,
4031
+ mc.text AS text,
4032
+ 0.1 AS score,
4033
+ m.message_type AS messageType,
4034
+ c.name AS chatName,
4035
+ m.sender_id AS senderId,
4036
+ m.sender_name AS senderName,
4037
+ m.person_id AS personId,
4038
+ m.sent_at AS sentAt,
4039
+ ROW_NUMBER() OVER (PARTITION BY m.id ORDER BY mc.chunk_index ASC) AS rowNumber
4040
+ FROM message_chunks mc
4041
+ JOIN messages m ON m.id = mc.message_id
4042
+ JOIN chats c ON c.id = m.chat_id
4043
+ WHERE (${where})
4044
+ ${likeExcludedWhere}
4045
+ AND m.person_id = ?
4046
+ ) ranked
4047
+ WHERE rowNumber = 1
4048
+ ORDER BY sentAt DESC
4049
+ LIMIT ?
4050
+ `
4051
+ ).all(...params, ...excludedIds, personId, limit);
4052
+ }
4053
+ getProfileEntry(entryId) {
4054
+ const row = this.database.prepare(
4055
+ `
4056
+ SELECT
4057
+ id,
4058
+ person_id AS personId,
4059
+ category,
4060
+ content,
4061
+ entry_type AS entryType,
4062
+ confidence,
4063
+ status,
4064
+ source,
4065
+ created_at AS createdAt,
4066
+ updated_at AS updatedAt,
4067
+ last_observed_at AS lastObservedAt
4068
+ FROM person_profile_entries
4069
+ WHERE id = ?
4070
+ `
4071
+ ).get(entryId);
4072
+ if (!row) {
4073
+ return void 0;
4074
+ }
4075
+ return { ...row, evidence: this.getEvidence(entryId) };
4076
+ }
4077
+ replaceProfileEntry(input2) {
4078
+ if (input2.input.evidence.length === 0) {
4079
+ throw new Error("Profile entry evidence is required.");
4080
+ }
4081
+ const timestamp = input2.input.observedAt ?? nowIso4();
4082
+ const newEntryId = createId("profile_entry", [input2.input.personId, input2.input.category, input2.input.content]);
4083
+ const transaction = this.database.transaction(() => {
4084
+ this.database.prepare(
4085
+ `
4086
+ INSERT INTO person_profile_entries (
4087
+ id, person_id, category, content, entry_type, confidence, status, source, created_at, updated_at, last_observed_at
4088
+ ) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
4089
+ `
4090
+ ).run(newEntryId, input2.input.personId, input2.input.category, input2.input.content, input2.input.entryType, input2.input.confidence, input2.input.source, timestamp, timestamp, timestamp);
4091
+ this.database.prepare(
4092
+ `
4093
+ UPDATE person_profile_entries
4094
+ SET status = 'superseded', updated_at = ?
4095
+ WHERE id = ? AND status = 'active'
4096
+ `
4097
+ ).run(timestamp, input2.supersedeEntryId);
4098
+ const insertEvidence = this.database.prepare(
4099
+ `
4100
+ INSERT INTO person_profile_evidence (entry_id, message_id, quote, reason)
4101
+ VALUES (?, ?, ?, ?)
4102
+ ON CONFLICT(entry_id, message_id, quote) DO UPDATE SET reason = excluded.reason
4103
+ `
4104
+ );
4105
+ for (const evidence of input2.input.evidence) {
4106
+ insertEvidence.run(newEntryId, evidence.messageId, evidence.quote, evidence.reason);
4107
+ }
4108
+ });
4109
+ transaction();
4110
+ return newEntryId;
4111
+ }
4112
+ markProfileEntryDeleted(entryId) {
4113
+ const timestamp = nowIso4();
4114
+ this.database.prepare("UPDATE person_profile_entries SET status = 'deleted', updated_at = ? WHERE id = ?").run(timestamp, entryId);
4115
+ }
4116
+ personExists(personId) {
4117
+ const row = this.database.prepare("SELECT 1 AS existsFlag FROM persons WHERE id = ? LIMIT 1").get(personId);
4118
+ return Boolean(row);
3408
4119
  }
3409
- return value;
3410
- }
4120
+ getEvidence(entryId) {
4121
+ return this.database.prepare(
4122
+ `
4123
+ SELECT entry_id AS entryId, message_id AS messageId, quote, reason
4124
+ FROM person_profile_evidence
4125
+ WHERE entry_id = ?
4126
+ ORDER BY message_id ASC, quote ASC
4127
+ `
4128
+ ).all(entryId);
4129
+ }
4130
+ };
3411
4131
 
3412
4132
  // src/rag/indexer.ts
3413
4133
  var EMBEDDING_INDEX_BATCH_SIZE = 64;
@@ -3496,12 +4216,12 @@ async function processMessagesNow(input2) {
3496
4216
  }
3497
4217
 
3498
4218
  // src/multimodal/tasks.ts
3499
- import crypto5 from "crypto";
3500
- function nowIso4() {
4219
+ import crypto6 from "crypto";
4220
+ function nowIso5() {
3501
4221
  return (/* @__PURE__ */ new Date()).toISOString();
3502
4222
  }
3503
4223
  function stableId3(sourceMessageId, imageKey) {
3504
- return crypto5.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
4224
+ return crypto6.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
3505
4225
  }
3506
4226
  function mapRow(row) {
3507
4227
  if (!row) {
@@ -3529,7 +4249,7 @@ var ImageMultimodalTaskRepository = class {
3529
4249
  database;
3530
4250
  enqueue(input2) {
3531
4251
  const id = stableId3(input2.sourceMessageId, input2.imageKey);
3532
- const timestamp = nowIso4();
4252
+ const timestamp = nowIso5();
3533
4253
  this.database.prepare(
3534
4254
  `
3535
4255
  INSERT INTO image_multimodal_tasks (
@@ -3617,7 +4337,7 @@ var ImageMultimodalTaskRepository = class {
3617
4337
  updated_at = @updatedAt
3618
4338
  WHERE id = @id AND status = 'pending'
3619
4339
  `
3620
- ).run({ id, updatedAt: nowIso4() });
4340
+ ).run({ id, updatedAt: nowIso5() });
3621
4341
  if (result.changes === 0) {
3622
4342
  throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A${id}`);
3623
4343
  }
@@ -3633,7 +4353,7 @@ var ImageMultimodalTaskRepository = class {
3633
4353
  updated_at = @updatedAt
3634
4354
  WHERE id = @id
3635
4355
  `
3636
- ).run({ id, derivedMessageId, updatedAt: nowIso4() });
4356
+ ).run({ id, derivedMessageId, updatedAt: nowIso5() });
3637
4357
  return this.requireById(id);
3638
4358
  }
3639
4359
  markSkipped(id, reason) {
@@ -3646,7 +4366,7 @@ var ImageMultimodalTaskRepository = class {
3646
4366
  updated_at = @updatedAt
3647
4367
  WHERE id = @id
3648
4368
  `
3649
- ).run({ id, reason, updatedAt: nowIso4() });
4369
+ ).run({ id, reason, updatedAt: nowIso5() });
3650
4370
  return this.requireById(id);
3651
4371
  }
3652
4372
  markFailed(id, error, finalFailure) {
@@ -3659,7 +4379,7 @@ var ImageMultimodalTaskRepository = class {
3659
4379
  updated_at = @updatedAt
3660
4380
  WHERE id = @id
3661
4381
  `
3662
- ).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso4() });
4382
+ ).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso5() });
3663
4383
  return this.requireById(id);
3664
4384
  }
3665
4385
  getById(id) {
@@ -3941,7 +4661,7 @@ function createFeishuChatMembersClient(client) {
3941
4661
  }
3942
4662
 
3943
4663
  // src/cron/tools.ts
3944
- function readString(input2, key) {
4664
+ function readString2(input2, key) {
3945
4665
  const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
3946
4666
  if (typeof value !== "string" || !value.trim()) {
3947
4667
  throw new Error(`${key} \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
@@ -3993,8 +4713,8 @@ function createCronJobTools(input2) {
3993
4713
  const job = input2.repository.create({
3994
4714
  chatId: input2.chatId,
3995
4715
  createdByOpenId: input2.createdByOpenId,
3996
- schedule: readString(rawInput, "schedule"),
3997
- prompt: readString(rawInput, "prompt"),
4716
+ schedule: readString2(rawInput, "schedule"),
4717
+ prompt: readString2(rawInput, "prompt"),
3998
4718
  imageFileName: readOptionalString(rawInput, "imageFileName"),
3999
4719
  mentionTargetName,
4000
4720
  mentionOpenId: mentionTarget?.openId,
@@ -4024,7 +4744,7 @@ function createCronJobTools(input2) {
4024
4744
  additionalProperties: false
4025
4745
  },
4026
4746
  execute: async (rawInput) => {
4027
- const id = readString(rawInput, "id");
4747
+ const id = readString2(rawInput, "id");
4028
4748
  const ok = input2.repository.deleteByChat(id, input2.chatId);
4029
4749
  return JSON.stringify({
4030
4750
  ok,
@@ -4037,11 +4757,23 @@ function createCronJobTools(input2) {
4037
4757
  }
4038
4758
 
4039
4759
  // src/rag/qa-logs.ts
4040
- import crypto6 from "crypto";
4760
+ import crypto7 from "crypto";
4761
+
4762
+ // src/rag/qa-trace.ts
4763
+ function hasQaTrace(trace) {
4764
+ return Object.keys(trace).length > 0;
4765
+ }
4766
+
4767
+ // src/rag/qa-logs.ts
4041
4768
  function clampLimit(limit) {
4042
4769
  return Math.max(1, Math.min(200, Math.trunc(limit)));
4043
4770
  }
4771
+ function parseTrace(value) {
4772
+ const parsed = JSON.parse(value);
4773
+ return parsed && typeof parsed === "object" ? parsed : {};
4774
+ }
4044
4775
  function mapQaLogRow(row) {
4776
+ const trace = parseTrace(row.trace_json);
4045
4777
  return {
4046
4778
  id: row.id,
4047
4779
  chatId: row.chat_id,
@@ -4050,6 +4782,8 @@ function mapQaLogRow(row) {
4050
4782
  answer: row.answer,
4051
4783
  citations: JSON.parse(row.citations_json),
4052
4784
  retrievalDebug: JSON.parse(row.retrieval_debug_json),
4785
+ trace,
4786
+ hasTrace: hasQaTrace(trace),
4053
4787
  status: row.status,
4054
4788
  error: row.error,
4055
4789
  createdAt: row.created_at
@@ -4061,14 +4795,17 @@ var QaLogRepository = class {
4061
4795
  }
4062
4796
  database;
4063
4797
  create(input2) {
4798
+ const trace = input2.trace ?? {};
4064
4799
  const record = {
4065
- id: `qa_${crypto6.randomUUID()}`,
4800
+ id: `qa_${crypto7.randomUUID()}`,
4066
4801
  chatId: input2.chatId ?? null,
4067
4802
  questionMessageId: input2.questionMessageId ?? null,
4068
4803
  question: input2.question,
4069
4804
  answer: input2.answer,
4070
4805
  citations: input2.citations,
4071
4806
  retrievalDebug: input2.retrievalDebug,
4807
+ trace,
4808
+ hasTrace: hasQaTrace(trace),
4072
4809
  status: input2.status,
4073
4810
  error: input2.error ?? null,
4074
4811
  createdAt: input2.createdAt
@@ -4083,6 +4820,7 @@ var QaLogRepository = class {
4083
4820
  answer,
4084
4821
  citations_json,
4085
4822
  retrieval_debug_json,
4823
+ trace_json,
4086
4824
  status,
4087
4825
  error,
4088
4826
  created_at
@@ -4095,6 +4833,7 @@ var QaLogRepository = class {
4095
4833
  @answer,
4096
4834
  @citationsJson,
4097
4835
  @retrievalDebugJson,
4836
+ @traceJson,
4098
4837
  @status,
4099
4838
  @error,
4100
4839
  @createdAt
@@ -4108,6 +4847,7 @@ var QaLogRepository = class {
4108
4847
  answer: record.answer,
4109
4848
  citationsJson: JSON.stringify(record.citations),
4110
4849
  retrievalDebugJson: JSON.stringify(record.retrievalDebug),
4850
+ traceJson: JSON.stringify(record.trace),
4111
4851
  status: record.status,
4112
4852
  error: record.error,
4113
4853
  createdAt: record.createdAt
@@ -4125,6 +4865,7 @@ var QaLogRepository = class {
4125
4865
  answer,
4126
4866
  citations_json,
4127
4867
  retrieval_debug_json,
4868
+ trace_json,
4128
4869
  status,
4129
4870
  error,
4130
4871
  created_at
@@ -4146,6 +4887,7 @@ var QaLogRepository = class {
4146
4887
  answer,
4147
4888
  citations_json,
4148
4889
  retrieval_debug_json,
4890
+ trace_json,
4149
4891
  status,
4150
4892
  error,
4151
4893
  created_at
@@ -4157,6 +4899,27 @@ var QaLogRepository = class {
4157
4899
  ).all(chatId, clampLimit(limit));
4158
4900
  return rows.map(mapQaLogRow);
4159
4901
  }
4902
+ getById(id) {
4903
+ const row = this.database.prepare(
4904
+ `
4905
+ SELECT
4906
+ id,
4907
+ chat_id,
4908
+ question_message_id,
4909
+ question,
4910
+ answer,
4911
+ citations_json,
4912
+ retrieval_debug_json,
4913
+ trace_json,
4914
+ status,
4915
+ error,
4916
+ created_at
4917
+ FROM qa_logs
4918
+ WHERE id = ?
4919
+ `
4920
+ ).get(id);
4921
+ return row ? mapQaLogRow(row) : null;
4922
+ }
4160
4923
  getCount() {
4161
4924
  const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
4162
4925
  return row.count;
@@ -4214,6 +4977,18 @@ ${block.text}`;
4214
4977
  function toToolErrorContent(message) {
4215
4978
  return JSON.stringify({ ok: false, error: message });
4216
4979
  }
4980
+ function nowIso6() {
4981
+ return (/* @__PURE__ */ new Date()).toISOString();
4982
+ }
4983
+ function finalizeTrace(trace, status, finalAnswer, startedAtMs) {
4984
+ return {
4985
+ ...trace,
4986
+ completedAt: nowIso6(),
4987
+ durationMs: Date.now() - startedAtMs,
4988
+ status,
4989
+ finalAnswer
4990
+ };
4991
+ }
4217
4992
  async function executeFeishuTool(tool, input2) {
4218
4993
  const result = await tool.execute(input2);
4219
4994
  if (isEvidenceBlockArray(result)) {
@@ -4225,6 +5000,13 @@ async function runFeishuToolLoop(input2) {
4225
5000
  if (!input2.model.completeWithTools) {
4226
5001
  throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
4227
5002
  }
5003
+ const startedAtMs = Date.now();
5004
+ const trace = {
5005
+ startedAt: new Date(startedAtMs).toISOString(),
5006
+ modelTurns: [],
5007
+ toolResults: [],
5008
+ fallbacks: []
5009
+ };
4228
5010
  const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
4229
5011
  const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
4230
5012
  const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
@@ -4253,19 +5035,36 @@ async function runFeishuToolLoop(input2) {
4253
5035
  toolCalls: assistantResult.toolCalls,
4254
5036
  reasoningContent: assistantResult.reasoningContent
4255
5037
  });
5038
+ trace.modelTurns?.push({
5039
+ index: turn,
5040
+ content: assistantResult.content,
5041
+ reasoningContent: assistantResult.reasoningContent,
5042
+ toolCalls: assistantResult.toolCalls,
5043
+ createdAt: nowIso6()
5044
+ });
4256
5045
  if (assistantResult.toolCalls.length === 0) {
4257
5046
  if (hasRawToolCallMarkup) {
5047
+ trace.fallbacks?.push({ type: "raw_tool_markup", message: "\u6A21\u578B\u8F93\u51FA\u4E86\u539F\u59CB\u5DE5\u5177\u8C03\u7528\u6807\u8BB0\uFF0C\u8F6C\u5165\u6700\u7EC8\u8865\u6551\u56DE\u7B54\u3002", createdAt: nowIso6() });
4258
5048
  break;
4259
5049
  }
4260
- return assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
5050
+ const answer = assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
5051
+ return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
4261
5052
  }
4262
5053
  for (const toolCall of assistantResult.toolCalls) {
4263
5054
  if (toolCallsUsed >= maxToolCalls) {
4264
- return FEISHU_TOOL_LOOP_LIMIT_REACHED;
5055
+ trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt: nowIso6() });
5056
+ return { answer: FEISHU_TOOL_LOOP_LIMIT_REACHED, trace: finalizeTrace(trace, "failed", FEISHU_TOOL_LOOP_LIMIT_REACHED, startedAtMs) };
4265
5057
  }
4266
5058
  toolCallsUsed += 1;
4267
5059
  const tool = toolsByName.get(toolCall.name);
4268
5060
  if (!tool) {
5061
+ trace.toolResults?.push({
5062
+ toolCallId: toolCall.id,
5063
+ name: toolCall.name,
5064
+ input: toolCall.input,
5065
+ error: `\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`,
5066
+ createdAt: nowIso6()
5067
+ });
4269
5068
  messages.push({
4270
5069
  role: "tool",
4271
5070
  toolCallId: toolCall.id,
@@ -4275,6 +5074,13 @@ async function runFeishuToolLoop(input2) {
4275
5074
  }
4276
5075
  try {
4277
5076
  const result = await executeFeishuTool(tool, toolCall.input);
5077
+ trace.toolResults?.push({
5078
+ toolCallId: toolCall.id,
5079
+ name: toolCall.name,
5080
+ input: toolCall.input,
5081
+ content: result,
5082
+ createdAt: nowIso6()
5083
+ });
4278
5084
  messages.push({
4279
5085
  role: "tool",
4280
5086
  toolCallId: toolCall.id,
@@ -4282,6 +5088,13 @@ async function runFeishuToolLoop(input2) {
4282
5088
  });
4283
5089
  } catch (error) {
4284
5090
  const message = error instanceof Error ? error.message : String(error);
5091
+ trace.toolResults?.push({
5092
+ toolCallId: toolCall.id,
5093
+ name: toolCall.name,
5094
+ input: toolCall.input,
5095
+ error: message,
5096
+ createdAt: nowIso6()
5097
+ });
4285
5098
  messages.push({
4286
5099
  role: "tool",
4287
5100
  toolCallId: toolCall.id,
@@ -4295,9 +5108,13 @@ async function runFeishuToolLoop(input2) {
4295
5108
  ...messages,
4296
5109
  { role: "system", content: "\u8BF7\u57FA\u4E8E\u4EE5\u4E0A\u6240\u6709\u5DE5\u5177\u8FD4\u56DE\u7684\u4FE1\u606F\uFF0C\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\u3002\u4E0D\u8981\u518D\u8C03\u7528\u5DE5\u5177\u3002" }
4297
5110
  ]);
4298
- return salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
5111
+ const answer = salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
5112
+ trace.fallbacks?.push({ type: "salvage_completion", message: "\u5DE5\u5177\u5FAA\u73AF\u7ED3\u675F\u540E\u4F7F\u7528\u65E0\u5DE5\u5177\u8865\u6551\u56DE\u7B54\u3002", createdAt: nowIso6() });
5113
+ return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
4299
5114
  } catch {
4300
- return "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
5115
+ const answer = "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
5116
+ trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt: nowIso6() });
5117
+ return { answer, trace: finalizeTrace(trace, "failed", answer, startedAtMs) };
4301
5118
  }
4302
5119
  }
4303
5120
  function formatConversationContext(records) {
@@ -4403,7 +5220,8 @@ var FeishuQuestionHandler = class {
4403
5220
  secrets: this.options.secrets,
4404
5221
  database: this.options.database,
4405
5222
  messages: new MessageRepository(this.options.database),
4406
- excludeMessageIds: options.excludeMessageIds
5223
+ excludeMessageIds: options.excludeMessageIds,
5224
+ profileTools: createPersonProfileTools({ profiles: new ProfileRepository(this.options.database) })
4407
5225
  });
4408
5226
  try {
4409
5227
  try {
@@ -4417,7 +5235,7 @@ var FeishuQuestionHandler = class {
4417
5235
  const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
4418
5236
  const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
4419
5237
  const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
4420
- const answer = await runFeishuToolLoop({
5238
+ const result = await runFeishuToolLoop({
4421
5239
  question: decision.question,
4422
5240
  now,
4423
5241
  tools: allTools,
@@ -4429,27 +5247,38 @@ var FeishuQuestionHandler = class {
4429
5247
  chatId: decision.chatId,
4430
5248
  questionMessageId,
4431
5249
  question: decision.question,
4432
- answer,
5250
+ answer: result.answer,
4433
5251
  citations: [],
4434
5252
  retrievalDebug: {},
5253
+ trace: result.trace,
4435
5254
  status: "answered",
4436
5255
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4437
5256
  });
4438
- await this.sendResponse(decision.chatId, questionMessageId, answer);
5257
+ await this.sendResponse(decision.chatId, questionMessageId, result.answer);
4439
5258
  } catch (error) {
4440
5259
  const message = error instanceof Error ? error.message : String(error);
5260
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
5261
+ const failedAnswer = `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`;
4441
5262
  qaLogs.create({
4442
5263
  chatId: decision.chatId,
4443
5264
  questionMessageId,
4444
5265
  question: decision.question,
4445
- answer: `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`,
5266
+ answer: failedAnswer,
4446
5267
  citations: [],
4447
5268
  retrievalDebug: {},
5269
+ trace: {
5270
+ startedAt: now.toISOString(),
5271
+ completedAt: failedAt,
5272
+ durationMs: Math.max(0, Date.parse(failedAt) - now.getTime()),
5273
+ status: "failed",
5274
+ finalAnswer: failedAnswer,
5275
+ fallbacks: [{ type: "answer_generation_failed", message, createdAt: failedAt }]
5276
+ },
4448
5277
  status: "failed",
4449
5278
  error: message,
4450
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
5279
+ createdAt: failedAt
4451
5280
  });
4452
- await this.sendResponse(decision.chatId, questionMessageId, `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`);
5281
+ await this.sendResponse(decision.chatId, questionMessageId, failedAnswer);
4453
5282
  }
4454
5283
  return decision;
4455
5284
  } finally {
@@ -4869,7 +5698,8 @@ function createFeishuGateway(options) {
4869
5698
  secrets: options.secrets,
4870
5699
  database: options.cronJobProcessor.database,
4871
5700
  messages: new MessageRepository(options.cronJobProcessor.database),
4872
- scope: { platform: "feishu", platformChatId: job.chatId }
5701
+ scope: { platform: "feishu", platformChatId: job.chatId },
5702
+ profileTools: createPersonProfileTools({ profiles: new ProfileRepository(options.cronJobProcessor.database) })
4873
5703
  });
4874
5704
  try {
4875
5705
  const memberPrompt = formatFeishuMemberPrompt(
@@ -4973,7 +5803,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4973
5803
  };
4974
5804
 
4975
5805
  // src/files/ingest.ts
4976
- import crypto7 from "crypto";
5806
+ import crypto8 from "crypto";
4977
5807
  import fs12 from "fs/promises";
4978
5808
  import path15 from "path";
4979
5809
 
@@ -5037,7 +5867,7 @@ function ensureSupportedTextFile(filePath) {
5037
5867
  }
5038
5868
  }
5039
5869
  function stableStoredName(sourcePath, fileName) {
5040
- const digest = crypto7.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
5870
+ const digest = crypto8.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
5041
5871
  return `${digest}-${fileName}`;
5042
5872
  }
5043
5873
  async function ingestLocalFile(input2) {
@@ -5293,16 +6123,32 @@ function isMultimodalReady(config, secrets) {
5293
6123
  return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
5294
6124
  }
5295
6125
  var GatewayIngestor = class {
5296
- constructor(database) {
6126
+ constructor(database, options = {}) {
5297
6127
  this.database = database;
6128
+ this.options = options;
5298
6129
  this.messages = new MessageRepository(database);
5299
6130
  this.jobs = new FileJobRepository(database);
5300
6131
  this.imageTasks = new ImageMultimodalTaskRepository(database);
5301
6132
  }
5302
6133
  database;
6134
+ options;
5303
6135
  messages;
5304
6136
  jobs;
5305
6137
  imageTasks;
6138
+ enrichWithPerson(input2) {
6139
+ const person = this.options.profiles?.resolvePersonForSender({
6140
+ platform: input2.platform,
6141
+ platformChatId: input2.platformChatId,
6142
+ senderId: input2.senderId,
6143
+ senderName: input2.senderName,
6144
+ source: "message",
6145
+ observedAt: input2.sentAt
6146
+ });
6147
+ return {
6148
+ ...input2,
6149
+ personId: person?.id
6150
+ };
6151
+ }
5306
6152
  ingestFeishuEvent(payload) {
5307
6153
  const normalized = normalizeFeishuReceiveMessageEvent(payload);
5308
6154
  if (!normalized) {
@@ -5311,12 +6157,13 @@ var GatewayIngestor = class {
5311
6157
  reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
5312
6158
  };
5313
6159
  }
5314
- const duplicate = this.messages.hasPlatformMessage(normalized.platform, normalized.platformMessageId);
5315
- const messageId = this.messages.ingest(normalized);
6160
+ const enriched = this.enrichWithPerson(normalized);
6161
+ const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
6162
+ const messageId = this.messages.ingest(enriched);
5316
6163
  return {
5317
6164
  accepted: true,
5318
6165
  messageId,
5319
- message: normalized,
6166
+ message: enriched,
5320
6167
  duplicate
5321
6168
  };
5322
6169
  }
@@ -5330,7 +6177,7 @@ var GatewayIngestor = class {
5330
6177
  }
5331
6178
  const openId = extractFeishuSenderOpenId(normalized);
5332
6179
  const senderName = openId ? await input2.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
5333
- const enriched = { ...normalized, senderName };
6180
+ const enriched = this.enrichWithPerson({ ...normalized, senderName });
5334
6181
  const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
5335
6182
  const messageId = this.messages.ingest(enriched);
5336
6183
  return {
@@ -5599,6 +6446,153 @@ function createMultimodalModel(config, secrets) {
5599
6446
  // src/cli.ts
5600
6447
  import * as lark4 from "@larksuiteoapi/node-sdk";
5601
6448
 
6449
+ // src/profiles/dream.ts
6450
+ function stripJsonFence(value) {
6451
+ const trimmed = value.trim();
6452
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
6453
+ return fenced ? fenced[1].trim() : trimmed;
6454
+ }
6455
+ function parseDreamOutput(value) {
6456
+ const parsed = JSON.parse(stripJsonFence(value));
6457
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.updates)) {
6458
+ throw new Error("Dream output must be a JSON object with updates array.");
6459
+ }
6460
+ return parsed;
6461
+ }
6462
+ function buildPrompts(messages, existingProfiles) {
6463
+ return {
6464
+ system: [
6465
+ "\u4F60\u662F ChatterCatcher \u7684\u4E2A\u4EBA\u6863\u6848 Dream \u5904\u7406\u5668\u3002",
6466
+ "\u53EA\u80FD\u57FA\u4E8E\u8F93\u5165\u6D88\u606F\u63D0\u53D6\u4EBA\u7269\u6863\u6848\u53D8\u5316\u3002",
6467
+ '\u8F93\u51FA\u4E25\u683C JSON\uFF1A{"updates":[{"personId":string,"category":string,"entryType":"fact"|"inferred","content":string,"confidence":number,"evidence":[{"messageId":string,"quote":string,"reason":string}]}]}\u3002',
6468
+ '\u6CA1\u6709\u8DB3\u591F\u8BC1\u636E\u65F6\u8FD4\u56DE {"updates":[]}\u3002'
6469
+ ].join("\n"),
6470
+ user: JSON.stringify({ messages, existingProfiles }, null, 2)
6471
+ };
6472
+ }
6473
+ var ProfileDreamProcessor = class {
6474
+ constructor(input2) {
6475
+ this.input = input2;
6476
+ }
6477
+ input;
6478
+ async processChat(input2) {
6479
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6480
+ const state = this.input.profiles.getDreamState(input2.platform, input2.platformChatId);
6481
+ const messages = this.input.profiles.listMessagesForDream({
6482
+ platform: input2.platform,
6483
+ platformChatId: input2.platformChatId,
6484
+ afterSentAt: state?.lastMessageSentAt,
6485
+ limit: input2.limit ?? 100
6486
+ });
6487
+ if (messages.length === 0) {
6488
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
6489
+ this.input.profiles.recordDreamRun({
6490
+ platform: input2.platform,
6491
+ platformChatId: input2.platformChatId,
6492
+ status: "skipped",
6493
+ processedMessageCount: 0,
6494
+ generatedEntryCount: 0,
6495
+ startedAt,
6496
+ finishedAt
6497
+ });
6498
+ return { status: "skipped", processedMessageCount: 0, generatedEntryCount: 0 };
6499
+ }
6500
+ try {
6501
+ const personIds = [...new Set(messages.map((message) => message.personId))];
6502
+ const existingProfiles = personIds.map((personId) => this.input.profiles.getPersonProfile(personId, { includeEvidence: false, includeInferred: true }));
6503
+ const prompts = buildPrompts(messages, existingProfiles);
6504
+ const raw = await this.input.model.complete([
6505
+ { role: "system", content: prompts.system },
6506
+ { role: "user", content: prompts.user }
6507
+ ]);
6508
+ const output = parseDreamOutput(raw);
6509
+ const messageIds = new Set(messages.map((message) => message.messageId));
6510
+ for (const update of output.updates) {
6511
+ this.validateUpdate(update, messageIds);
6512
+ }
6513
+ for (const update of output.updates) {
6514
+ this.input.profiles.upsertProfileEntry({
6515
+ personId: update.personId,
6516
+ category: update.category,
6517
+ content: update.content,
6518
+ entryType: update.entryType,
6519
+ confidence: update.confidence,
6520
+ source: "dream",
6521
+ evidence: update.evidence,
6522
+ observedAt: messages[messages.length - 1].sentAt
6523
+ });
6524
+ }
6525
+ const lastMessage = messages[messages.length - 1];
6526
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
6527
+ this.input.profiles.updateDreamState({
6528
+ platform: input2.platform,
6529
+ platformChatId: input2.platformChatId,
6530
+ lastMessageId: lastMessage.messageId,
6531
+ lastMessageSentAt: lastMessage.sentAt,
6532
+ updatedAt: finishedAt
6533
+ });
6534
+ this.input.profiles.recordDreamRun({
6535
+ platform: input2.platform,
6536
+ platformChatId: input2.platformChatId,
6537
+ status: "succeeded",
6538
+ processedMessageCount: messages.length,
6539
+ generatedEntryCount: output.updates.length,
6540
+ startedAt,
6541
+ finishedAt
6542
+ });
6543
+ return { status: "succeeded", processedMessageCount: messages.length, generatedEntryCount: output.updates.length };
6544
+ } catch (error) {
6545
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
6546
+ const message = error instanceof Error ? error.message : String(error);
6547
+ this.input.profiles.recordDreamRun({
6548
+ platform: input2.platform,
6549
+ platformChatId: input2.platformChatId,
6550
+ status: "failed",
6551
+ processedMessageCount: messages.length,
6552
+ generatedEntryCount: 0,
6553
+ error: message,
6554
+ startedAt,
6555
+ finishedAt
6556
+ });
6557
+ return { status: "failed", processedMessageCount: messages.length, generatedEntryCount: 0, error: message };
6558
+ }
6559
+ }
6560
+ validateUpdate(update, messageIds) {
6561
+ if (!this.input.profiles.personExists(update.personId)) {
6562
+ throw new Error(`Unknown personId in dream output: ${update.personId}`);
6563
+ }
6564
+ if (update.entryType !== "fact" && update.entryType !== "inferred") {
6565
+ throw new Error("Dream update entryType must be fact or inferred.");
6566
+ }
6567
+ if (typeof update.confidence !== "number" || !Number.isFinite(update.confidence) || update.confidence < 0 || update.confidence > 1) {
6568
+ throw new Error("Dream update confidence must be a number between 0 and 1.");
6569
+ }
6570
+ if (!Array.isArray(update.evidence) || update.evidence.length === 0) {
6571
+ throw new Error("Dream update evidence is required.");
6572
+ }
6573
+ if (typeof update.category !== "string" || !update.category.trim()) {
6574
+ throw new Error("Dream update category is required.");
6575
+ }
6576
+ if (typeof update.content !== "string" || !update.content.trim()) {
6577
+ throw new Error("Dream update content is required.");
6578
+ }
6579
+ for (const evidence of update.evidence) {
6580
+ if (!evidence || typeof evidence !== "object") {
6581
+ throw new Error("Dream update evidence item must be an object.");
6582
+ }
6583
+ if (typeof evidence.messageId !== "string" || !messageIds.has(evidence.messageId)) {
6584
+ throw new Error(`Dream update evidence message is outside the processed batch: ${String(evidence.messageId)}`);
6585
+ }
6586
+ if (typeof evidence.quote !== "string" || !evidence.quote.trim()) {
6587
+ throw new Error("Dream update evidence quote is required.");
6588
+ }
6589
+ if (typeof evidence.reason !== "string" || !evidence.reason.trim()) {
6590
+ throw new Error("Dream update evidence reason is required.");
6591
+ }
6592
+ }
6593
+ }
6594
+ };
6595
+
5602
6596
  // src/rag/answer.ts
5603
6597
  var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
5604
6598
  var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
@@ -5839,7 +6833,7 @@ async function updateChatterCatcher(options) {
5839
6833
  }
5840
6834
 
5841
6835
  // src/web/server.ts
5842
- import crypto8 from "crypto";
6836
+ import crypto9 from "crypto";
5843
6837
  import Fastify from "fastify";
5844
6838
  function buildHtml() {
5845
6839
  return `<!doctype html>
@@ -6181,6 +7175,10 @@ function buildHtml() {
6181
7175
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
6182
7176
  <span>\u95EE\u7B54\u65E5\u5FD7</span>
6183
7177
  </button>
7178
+ <button class="nav-item" data-view="persons">
7179
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
7180
+ <span>\u4E2A\u4EBA\u6863\u6848</span>
7181
+ </button>
6184
7182
  <button class="nav-item" data-view="settings">
6185
7183
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
6186
7184
  <span>\u8BBE\u7F6E</span>
@@ -6212,6 +7210,10 @@ function buildHtml() {
6212
7210
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
6213
7211
  <span>\u4EFB\u52A1</span>
6214
7212
  </button>
7213
+ <button class="mobile-nav-item" data-view="persons">
7214
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
7215
+ <span>\u6863\u6848</span>
7216
+ </button>
6215
7217
  <button class="mobile-nav-item" data-view="settings">
6216
7218
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
6217
7219
  <span>\u8BBE\u7F6E</span>
@@ -6308,6 +7310,18 @@ function buildHtml() {
6308
7310
  <div class="content-panel glass"><div id="qa-logs-list"></div></div>
6309
7311
  </div>
6310
7312
 
7313
+ <div class="view" id="view-persons">
7314
+ <div class="section-header"><div><h1 class="section-title">\u4E2A\u4EBA\u6863\u6848</h1><p class="page-subtitle">\u7FA4\u6210\u5458\u6863\u6848\u4E0E\u4E8B\u5B9E\u4FE1\u606F</p></div></div>
7315
+ <div class="content-panel glass" id="persons-list-panel"><div id="persons-list"></div></div>
7316
+ <div class="content-panel glass mt-lg" id="person-profile-panel" style="display:none;">
7317
+ <div class="panel-header">
7318
+ <h2 class="panel-title" id="person-profile-name"></h2>
7319
+ <button class="btn btn-sm" onclick="navigateTo('persons')">\u8FD4\u56DE\u5217\u8868</button>
7320
+ </div>
7321
+ <div id="person-profile-detail"></div>
7322
+ </div>
7323
+ </div>
7324
+
6311
7325
  <div class="view" id="view-settings">
6312
7326
  <div class="section-header"><div><h1 class="section-title">\u8BBE\u7F6E</h1><p class="page-subtitle">\u7CFB\u7EDF\u914D\u7F6E\u4E0E\u64CD\u4F5C</p></div></div>
6313
7327
  <div class="settings-group glass" id="settings-config"></div>
@@ -6343,12 +7357,16 @@ function buildHtml() {
6343
7357
  let allFileJobs = [];
6344
7358
  let allCronJobs = [];
6345
7359
  let allQaLogs = [];
7360
+ let allPersons = [];
7361
+ let selectedPersonId = null;
6346
7362
  let statusData = null;
6347
7363
 
6348
7364
  function fmt(value) { return value == null || value === "" ? "-" : String(value); }
6349
7365
  function escapeHtml(value) {
6350
7366
  return fmt(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
6351
7367
  }
7368
+ function renderJson(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(JSON.stringify(value, null, 2)) + '</pre>'; }
7369
+ function renderTextBlock(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(value || "") + '</pre>'; }
6352
7370
  function isOpaqueId(value) { return /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(fmt(value)); }
6353
7371
  function formatDateTime(value) {
6354
7372
  var date = new Date(value);
@@ -6384,6 +7402,7 @@ function buildHtml() {
6384
7402
  if (view === "files") renderFilesView();
6385
7403
  if (view === "tasks") renderTasksView();
6386
7404
  if (view === "qa-logs") renderQaLogsView();
7405
+ if (view === "persons") renderPersonsView();
6387
7406
  }
6388
7407
 
6389
7408
  document.querySelectorAll(".nav-item, .mobile-nav-item").forEach(function(el) {
@@ -6426,6 +7445,55 @@ function buildHtml() {
6426
7445
  return result;
6427
7446
  }
6428
7447
 
7448
+ function toJsArgument(value) {
7449
+ return escapeHtml(JSON.stringify(fmt(value)));
7450
+ }
7451
+
7452
+ function findEntryEvidenceMessage(entry) {
7453
+ var evidence = entry.evidence || [];
7454
+ return evidence.length > 0 ? evidence[0].messageId : "";
7455
+ }
7456
+
7457
+ function findEntryEvidenceQuote(entry) {
7458
+ var evidence = entry.evidence || [];
7459
+ return evidence.length > 0 ? evidence[0].quote : "";
7460
+ }
7461
+
7462
+ async function correctPersonProfileEntry(personId, entryId, category, content, entryType, confidence, evidenceMessageId, quote) {
7463
+ var nextContent = prompt("\u4FEE\u6B63\u6863\u6848\u5185\u5BB9", content);
7464
+ if (!nextContent || !nextContent.trim()) return;
7465
+ var reason = prompt("\u4FEE\u6B63\u7406\u7531", "\u7528\u6237\u5728 Web UI \u663E\u5F0F\u4FEE\u6B63") || "\u7528\u6237\u5728 Web UI \u663E\u5F0F\u4FEE\u6B63";
7466
+ try {
7467
+ await postJson("/api/persons/" + encodeURIComponent(personId) + "/profile/entries/" + encodeURIComponent(entryId) + "/correct", {
7468
+ headers: { "content-type": "application/json" },
7469
+ body: JSON.stringify({
7470
+ category: category,
7471
+ content: nextContent.trim(),
7472
+ entryType: entryType,
7473
+ confidence: confidence,
7474
+ evidenceMessageId: evidenceMessageId,
7475
+ quote: quote || content,
7476
+ reason: reason
7477
+ })
7478
+ });
7479
+ showToast("\u6863\u6848\u6761\u76EE\u5DF2\u4FEE\u6B63", "success");
7480
+ await showPersonProfile(personId);
7481
+ } catch (error) {
7482
+ showToast(error instanceof Error ? error.message : String(error), "error");
7483
+ }
7484
+ }
7485
+
7486
+ async function deletePersonProfileEntry(personId, entryId) {
7487
+ if (!confirm("\u786E\u8BA4\u5220\u9664\u8FD9\u6761\u6863\u6848\u6761\u76EE\uFF1F")) return;
7488
+ try {
7489
+ await deleteJson("/api/persons/" + encodeURIComponent(personId) + "/profile/entries/" + encodeURIComponent(entryId));
7490
+ showToast("\u6863\u6848\u6761\u76EE\u5DF2\u5220\u9664", "success");
7491
+ await showPersonProfile(personId);
7492
+ } catch (error) {
7493
+ showToast(error instanceof Error ? error.message : String(error), "error");
7494
+ }
7495
+ }
7496
+
6429
7497
  function renderMetrics(status) {
6430
7498
  var gatewayClass = status.gateway.configured ? "status-dot online" : "status-dot offline";
6431
7499
  var gatewayText = status.gateway.connection === "running" ? "\u8FD0\u884C\u4E2D" : (!status.gateway.configured ? "\u672A\u914D\u7F6E" : "\u5F85\u542F\u52A8");
@@ -6621,13 +7689,158 @@ function buildHtml() {
6621
7689
  for (var i = 0; i < allQaLogs.length; i++) {
6622
7690
  var item = allQaLogs[i];
6623
7691
  var citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
6624
- var statusClass = item.status === 'success' ? 'tag-success' : 'tag-warning';
7692
+ var statusClass = item.status === 'answered' ? 'tag-success' : 'tag-warning';
6625
7693
  html += '<div class="qa-card"><div class="message-meta" style="margin-bottom:var(--space-sm);">' +
6626
7694
  '<span>' + escapeHtml(formatDateTime(item.createdAt)) + '</span>' +
6627
7695
  '<span class="tag ' + statusClass + '">' + escapeHtml(item.status) + '</span>' +
6628
- '<span>' + citationCount + ' \u6761\u5F15\u7528</span></div>' +
7696
+ '<span>' + citationCount + ' \u6761\u5F15\u7528</span>' +
7697
+ '<span class="tag ' + (item.hasTrace ? 'tag-info' : 'tag-warning') + '">' + (item.hasTrace ? '\u6709 trace' : '\u65E0 trace') + '</span></div>' +
6629
7698
  '<div class="qa-question">' + escapeHtml(item.question) + '</div>' +
6630
- '<div class="qa-answer">' + escapeHtml(item.answer) + '</div></div>';
7699
+ '<div class="qa-answer">' + escapeHtml(item.answer) + '</div>' +
7700
+ '<button class="btn btn-sm" style="margin-top:var(--space-sm);" data-view-qa-log="' + escapeHtml(item.id) + '">\u67E5\u770B\u8BE6\u60C5</button>' +
7701
+ '<div id="qa-detail-' + escapeHtml(item.id) + '" style="margin-top:var(--space-md);"></div></div>';
7702
+ }
7703
+ el.innerHTML = html;
7704
+ }
7705
+
7706
+ async function showQaLogDetail(id) {
7707
+ selectedQaLogId = id;
7708
+ var container = document.getElementById("qa-detail-" + id);
7709
+ if (!container) return;
7710
+ container.innerHTML = '<div class="empty-state">\u6B63\u5728\u52A0\u8F7D\u95EE\u7B54\u8BE6\u60C5...</div>';
7711
+ try {
7712
+ var item = await fetchJson("/api/qa-logs/" + encodeURIComponent(id));
7713
+ renderQaLogDetail(item);
7714
+ } catch (error) {
7715
+ container.innerHTML = '<div class="empty-state">\u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25\uFF1A' + escapeHtml(error instanceof Error ? error.message : String(error)) + '</div>';
7716
+ }
7717
+ }
7718
+
7719
+ function renderQaLogDetail(item) {
7720
+ var container = document.getElementById("qa-detail-" + item.id);
7721
+ if (!container) return;
7722
+ var trace = item.trace || {};
7723
+ var html = '<div class="content-panel" style="margin-top:var(--space-sm);background:rgba(255,255,255,0.03);">';
7724
+ html += '<h3 style="font-size:15px;margin-bottom:var(--space-sm);">\u95EE\u7B54\u8BE6\u60C5</h3>';
7725
+ html += '<div class="message-meta" style="margin-bottom:var(--space-sm);"><span>\u72B6\u6001\uFF1A' + escapeHtml(item.status) + '</span><span>\u521B\u5EFA\uFF1A' + escapeHtml(formatDateTime(item.createdAt)) + '</span><span>\u8017\u65F6\uFF1A' + escapeHtml(trace.durationMs == null ? '-' : trace.durationMs + 'ms') + '</span></div>';
7726
+ if (item.error) html += '<div style="color:var(--danger);margin-bottom:var(--space-sm);">\u9519\u8BEF\uFF1A' + escapeHtml(item.error) + '</div>';
7727
+ html += '<div class="qa-question">' + escapeHtml(item.question) + '</div>';
7728
+ html += '<div class="qa-answer" style="margin-bottom:var(--space-md);">' + escapeHtml(item.answer) + '</div>';
7729
+ if (!item.hasTrace) {
7730
+ html += '<div class="empty-state">\u8FD9\u6761\u95EE\u7B54\u6CA1\u6709 trace\uFF0C\u53EF\u80FD\u6765\u81EA\u65E7\u7248\u672C\u8BB0\u5F55\u3002</div></div>';
7731
+ container.innerHTML = html;
7732
+ return;
7733
+ }
7734
+ var turns = trace.modelTurns || [];
7735
+ html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Reasoning</h4>';
7736
+ if (turns.length === 0) html += '<div class="empty-state">\u65E0 reasoningContent</div>';
7737
+ for (var i = 0; i < turns.length; i++) {
7738
+ html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>\u6A21\u578B\u8F6E\u6B21 ' + escapeHtml(turns[i].index) + '</span><span>' + escapeHtml(formatDateTime(turns[i].createdAt)) + '</span></div>' + renderTextBlock(turns[i].reasoningContent || '\u65E0 reasoningContent') + '</div>';
7739
+ }
7740
+ html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u6A21\u578B\u8F6E\u6B21\u4E0E\u5DE5\u5177\u8C03\u7528</h4>';
7741
+ for (var j = 0; j < turns.length; j++) {
7742
+ html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>\u8F6E\u6B21 ' + escapeHtml(turns[j].index) + '</span></div>' + renderTextBlock(turns[j].content || '') + renderJson(turns[j].toolCalls || []) + '</div>';
7743
+ }
7744
+ html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5DE5\u5177\u7ED3\u679C</h4>';
7745
+ var toolResults = trace.toolResults || [];
7746
+ if (toolResults.length === 0) html += '<div class="empty-state">\u6CA1\u6709\u5DE5\u5177\u7ED3\u679C\u3002</div>';
7747
+ for (var k = 0; k < toolResults.length; k++) {
7748
+ html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>' + escapeHtml(toolResults[k].name) + '</span><span>' + escapeHtml(toolResults[k].toolCallId) + '</span><span>' + escapeHtml(formatDateTime(toolResults[k].createdAt)) + '</span></div>' + renderJson(toolResults[k].input) + (toolResults[k].error ? '<div style="color:var(--danger);">' + escapeHtml(toolResults[k].error) + '</div>' : renderTextBlock(toolResults[k].content || '')) + '</div>';
7749
+ }
7750
+ html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5F15\u7528\u4E0E\u68C0\u7D22</h4>' + renderJson({ citations: item.citations || [], retrievalDebug: item.retrievalDebug || {} });
7751
+ html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Fallback</h4>';
7752
+ var fallbacks = trace.fallbacks || [];
7753
+ html += fallbacks.length === 0 ? '<div class="empty-state">\u6CA1\u6709 fallback\u3002</div>' : renderJson(fallbacks);
7754
+ html += '</div>';
7755
+ container.innerHTML = html;
7756
+ }
7757
+
7758
+ function renderPersonsView() {
7759
+ var listEl = document.getElementById("persons-list");
7760
+ var profilePanel = document.getElementById("person-profile-panel");
7761
+ if (profilePanel) profilePanel.style.display = "none";
7762
+ if (!allPersons || allPersons.length === 0) {
7763
+ listEl.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u4E2A\u4EBA\u6863\u6848\u3002\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u4ECE\u804A\u5929\u8BB0\u5F55\u4E2D\u8BC6\u522B\u7FA4\u6210\u5458\u3002</div>';
7764
+ return;
7765
+ }
7766
+ var html = '<table class="data-table"><thead><tr><th>\u6210\u5458</th><th>\u6863\u6848\u6761\u76EE</th><th>\u6D88\u606F\u6570</th></tr></thead><tbody>';
7767
+ for (var i = 0; i < allPersons.length; i++) {
7768
+ var p = allPersons[i];
7769
+ html += '<tr data-view-person="' + escapeHtml(p.id) + '" style="cursor:pointer;">' +
7770
+ '<td><span style="font-weight:500;">' + escapeHtml(p.primaryName) + '</span></td>' +
7771
+ '<td>' + escapeHtml(p.profileEntryCount) + '</td>' +
7772
+ '<td>' + escapeHtml(p.messageCount) + '</td></tr>';
7773
+ }
7774
+ html += '</tbody></table>';
7775
+ listEl.innerHTML = html;
7776
+ }
7777
+
7778
+ async function showPersonProfile(id) {
7779
+ selectedPersonId = id;
7780
+ var listEl = document.getElementById("persons-list-panel");
7781
+ var profilePanel = document.getElementById("person-profile-panel");
7782
+ var detailEl = document.getElementById("person-profile-detail");
7783
+ if (listEl) listEl.style.display = "none";
7784
+ if (profilePanel) profilePanel.style.display = "block";
7785
+ detailEl.innerHTML = '<div class="empty-state">\u6B63\u5728\u52A0\u8F7D\u4E2A\u4EBA\u6863\u6848...</div>';
7786
+ try {
7787
+ var profile = await fetchJson("/api/persons/" + encodeURIComponent(id) + "/profile");
7788
+ var messages = await fetchJson("/api/persons/" + encodeURIComponent(id) + "/messages?limit=50");
7789
+ renderPersonProfile(id, profile, messages.items || []);
7790
+ } catch (error) {
7791
+ detailEl.innerHTML = '<div class="empty-state">\u52A0\u8F7D\u5931\u8D25\uFF1A' + escapeHtml(error instanceof Error ? error.message : String(error)) + '</div>';
7792
+ }
7793
+ }
7794
+
7795
+ function renderPersonProfile(id, profile, messages) {
7796
+ var el = document.getElementById("person-profile-detail");
7797
+ var nameEl = document.getElementById("person-profile-name");
7798
+ if (nameEl) nameEl.textContent = profile.person.primaryName;
7799
+ var html = '';
7800
+ var identities = profile.identities || [];
7801
+ if (identities.length > 0) {
7802
+ html += '<div class="mb-md"><span style="color:var(--text-muted);font-size:13px;">\u8EAB\u4EFD\uFF1A</span> ';
7803
+ html += identities.map(function(id) { return escapeHtml(id.displayName) + ' (' + escapeHtml(id.platform) + ')'; }).join('\uFF0C');
7804
+ html += '</div>';
7805
+ }
7806
+ var entries = profile.entries || [];
7807
+ if (entries.length === 0) {
7808
+ html += '<div class="empty-state">\u8FD8\u6CA1\u6709\u6863\u6848\u6761\u76EE\u3002</div>';
7809
+ } else {
7810
+ html += '<div class="grid-2">';
7811
+ for (var i = 0; i < entries.length; i++) {
7812
+ var entry = entries[i];
7813
+ var evidence = entry.evidence || [];
7814
+ html += '<div class="content-panel" style="background:rgba(255,255,255,0.03);padding:var(--space-md);">' +
7815
+ '<div style="display:flex;gap:var(--space-sm);margin-bottom:var(--space-sm);align-items:center;flex-wrap:wrap;">' +
7816
+ '<span class="tag">' + escapeHtml(entry.category) + '</span>' +
7817
+ '<span class="tag ' + (entry.entryType === 'fact' ? 'tag-success' : 'tag-info') + '">' + escapeHtml(entry.entryType) + '</span>' +
7818
+ '<span style="font-size:12px;color:var(--text-muted);">' + escapeHtml(Math.round(entry.confidence * 100)) + '%</span>' +
7819
+ '<span style="flex:1;"></span>' +
7820
+ '<button class="btn btn-sm" onclick="correctPersonProfileEntry(' + toJsArgument(id) + ',' + toJsArgument(entry.id) + ',' + toJsArgument(entry.category) + ',' + toJsArgument(entry.content) + ',' + toJsArgument(entry.entryType) + ',' + Number(entry.confidence || 0) + ',' + toJsArgument(findEntryEvidenceMessage(entry)) + ',' + toJsArgument(findEntryEvidenceQuote(entry)) + ')">\u4FEE\u6B63</button>' +
7821
+ '<button class="btn btn-sm btn-danger" onclick="deletePersonProfileEntry(' + toJsArgument(id) + ',' + toJsArgument(entry.id) + ')">\u5220\u9664</button>' +
7822
+ '</div>' +
7823
+ '<div style="font-weight:500;margin-bottom:var(--space-xs);">' + escapeHtml(entry.content) + '</div>';
7824
+ if (evidence.length > 0) {
7825
+ html += '<div style="font-size:12px;color:var(--text-muted);">\u8BC1\u636E\uFF1A' +
7826
+ evidence.map(function(e) { return '<span style="display:block;margin-top:2px;">"' + escapeHtml(e.quote) + '" (' + escapeHtml(e.reason) + ')</span>'; }).join('') +
7827
+ '</div>';
7828
+ }
7829
+ html += '</div>';
7830
+ }
7831
+ html += '</div>';
7832
+ }
7833
+ if (messages && messages.length > 0) {
7834
+ html += '<h3 style="margin:var(--space-lg) 0 var(--space-sm);font-size:16px;">\u6700\u8FD1\u6D88\u606F</h3>';
7835
+ html += '<div class="message-list">';
7836
+ for (var j = 0; j < Math.min(messages.length, 10); j++) {
7837
+ var msg = messages[j];
7838
+ html += '<div class="message-card"><div class="message-meta">' +
7839
+ '<span>' + escapeHtml(formatDateTime(msg.sentAt)) + '</span>' +
7840
+ '<span>' + escapeHtml(displayChatName(msg.chatName, msg.platform)) + '</span>' +
7841
+ '</div><div class="message-text">' + escapeHtml(msg.text) + '</div></div>';
7842
+ }
7843
+ html += '</div>';
6631
7844
  }
6632
7845
  el.innerHTML = html;
6633
7846
  }
@@ -6662,11 +7875,19 @@ function buildHtml() {
6662
7875
  await loadSection("/api/file-jobs", function(data) { allFileJobs = data.items || []; });
6663
7876
  await loadSection("/api/qa-logs?limit=20", function(data) { allQaLogs = data.items || []; });
6664
7877
  await loadSection("/api/cron-jobs", function(data) { allCronJobs = data.items || []; });
7878
+ await loadSection("/api/persons", function(data) { allPersons = data.items || []; });
6665
7879
  if (currentView === "messages") renderMessagesView();
6666
7880
  if (currentView === "episodes") renderEpisodesView();
6667
7881
  if (currentView === "files") renderFilesView();
6668
7882
  if (currentView === "tasks") renderTasksView();
6669
- if (currentView === "qa-logs") renderQaLogsView();
7883
+ if (currentView === "qa-logs") {
7884
+ renderQaLogsView();
7885
+ if (selectedQaLogId) void showQaLogDetail(selectedQaLogId);
7886
+ }
7887
+ if (currentView === "persons") {
7888
+ renderPersonsView();
7889
+ if (selectedPersonId) void showPersonProfile(selectedPersonId);
7890
+ }
6670
7891
  }
6671
7892
 
6672
7893
  async function processNow() {
@@ -6688,6 +7909,16 @@ function buildHtml() {
6688
7909
  document.addEventListener("click", async function(event) {
6689
7910
  var target = event.target;
6690
7911
  if (!(target instanceof HTMLElement)) return;
7912
+ var qaLogId = target.dataset.viewQaLog;
7913
+ if (qaLogId) {
7914
+ void showQaLogDetail(qaLogId);
7915
+ return;
7916
+ }
7917
+ var personId = target.dataset.viewPerson || target.closest('[data-view-person]')?.dataset.viewPerson;
7918
+ if (personId) {
7919
+ void showPersonProfile(personId);
7920
+ return;
7921
+ }
6691
7922
  var id = target.dataset.deleteCronJob;
6692
7923
  if (!id) return;
6693
7924
  target.disabled = true;
@@ -6707,7 +7938,7 @@ function buildHtml() {
6707
7938
  </body>
6708
7939
  </html>`;
6709
7940
  }
6710
- function parseLimit(value, fallback, max) {
7941
+ function parseLimit2(value, fallback, max) {
6711
7942
  const rawLimit = Number(value ?? fallback);
6712
7943
  return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
6713
7944
  }
@@ -6731,6 +7962,20 @@ function parseCookies(header) {
6731
7962
  function isAuthorizedWebAction(request, token) {
6732
7963
  return parseCookies(request.headers.cookie).chattercatcher_web_token === token;
6733
7964
  }
7965
+ function readStringField(input2, key) {
7966
+ if (!input2 || typeof input2 !== "object" || Array.isArray(input2)) return void 0;
7967
+ const value = input2[key];
7968
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
7969
+ }
7970
+ function readNumberField(input2, key, fallback) {
7971
+ if (!input2 || typeof input2 !== "object" || Array.isArray(input2)) return fallback;
7972
+ const value = input2[key];
7973
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
7974
+ }
7975
+ function toQaLogListItem(log) {
7976
+ const { trace: _trace, ...item } = log;
7977
+ return item;
7978
+ }
6734
7979
  function createWebApp(config, options = {}) {
6735
7980
  const app = Fastify({ logger: false });
6736
7981
  const database = openDatabase(config);
@@ -6740,11 +7985,12 @@ function createWebApp(config, options = {}) {
6740
7985
  const fileJobs = new FileJobRepository(database);
6741
7986
  const qaLogs = new QaLogRepository(database);
6742
7987
  const cronJobs = new CronJobRepository(database);
7988
+ const profiles = new ProfileRepository(database);
6743
7989
  let webActionToken = "";
6744
7990
  const tokenReady = (async () => {
6745
7991
  const secrets = await loadSecrets();
6746
7992
  if (!secrets.web.actionToken) {
6747
- secrets.web.actionToken = crypto8.randomBytes(32).toString("hex");
7993
+ secrets.web.actionToken = crypto9.randomBytes(32).toString("hex");
6748
7994
  await saveSecrets(secrets);
6749
7995
  }
6750
7996
  webActionToken = getWebActionToken(secrets);
@@ -6782,38 +8028,47 @@ function createWebApp(config, options = {}) {
6782
8028
  items: messages.listChats()
6783
8029
  }));
6784
8030
  app.get("/api/files", async (request) => {
6785
- const limit = parseLimit(request.query.limit, 50, 200);
8031
+ const limit = parseLimit2(request.query.limit, 50, 200);
6786
8032
  return {
6787
8033
  items: messages.listFiles(limit)
6788
8034
  };
6789
8035
  });
6790
8036
  app.get("/api/file-jobs", async (request) => {
6791
- const limit = parseLimit(request.query.limit, 50, 200);
8037
+ const limit = parseLimit2(request.query.limit, 50, 200);
6792
8038
  const status = request.query.status;
6793
8039
  return {
6794
8040
  items: fileJobs.list(limit, status === "processing" || status === "indexed" || status === "failed" ? { status } : {})
6795
8041
  };
6796
8042
  });
6797
8043
  app.get("/api/messages/recent", async (request) => {
6798
- const limit = parseLimit(request.query.limit, 20, 100);
8044
+ const limit = parseLimit2(request.query.limit, 20, 100);
6799
8045
  return {
6800
8046
  items: messages.listRecentMessages(limit)
6801
8047
  };
6802
8048
  });
6803
8049
  app.get("/api/episodes", async (request) => {
6804
- const limit = parseLimit(request.query.limit, 20, 100);
8050
+ const limit = parseLimit2(request.query.limit, 20, 100);
6805
8051
  return {
6806
8052
  items: episodes.listRecentEpisodes(limit)
6807
8053
  };
6808
8054
  });
6809
8055
  app.get("/api/qa-logs", async (request) => {
6810
- const limit = parseLimit(request.query.limit, 20, 100);
8056
+ const limit = parseLimit2(request.query.limit, 20, 100);
6811
8057
  return {
6812
- items: qaLogs.listRecent(limit)
8058
+ items: qaLogs.listRecent(limit).map(toQaLogListItem)
6813
8059
  };
6814
8060
  });
8061
+ app.get("/api/qa-logs/:id", async (request, reply) => {
8062
+ const id = request.params.id;
8063
+ const log = qaLogs.getById(id);
8064
+ if (!log) {
8065
+ reply.code(404);
8066
+ return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u95EE\u7B54\u65E5\u5FD7\u3002" };
8067
+ }
8068
+ return log;
8069
+ });
6815
8070
  app.get("/api/cron-jobs", async (request) => {
6816
- const limit = parseLimit(request.query.limit, 50, 200);
8071
+ const limit = parseLimit2(request.query.limit, 50, 200);
6817
8072
  return {
6818
8073
  items: cronJobs.list(limit)
6819
8074
  };
@@ -6833,6 +8088,105 @@ function createWebApp(config, options = {}) {
6833
8088
  const ok = cronJobs.deleteByChat(id, job.chatId);
6834
8089
  return { ok };
6835
8090
  });
8091
+ app.get("/api/persons", async (_request) => {
8092
+ const persons = profiles.listPersons();
8093
+ const items = persons.map((person) => {
8094
+ const profile = profiles.getPersonProfile(person.id, { includeEvidence: false, includeInferred: true });
8095
+ return {
8096
+ id: person.id,
8097
+ primaryName: person.primaryName,
8098
+ profileEntryCount: profile?.entries.length ?? 0,
8099
+ messageCount: database.prepare(
8100
+ "SELECT COUNT(1) AS count FROM messages WHERE person_id = ?"
8101
+ ).get(person.id).count
8102
+ };
8103
+ });
8104
+ return { items };
8105
+ });
8106
+ app.get("/api/persons/:id/profile", async (request, reply) => {
8107
+ const id = request.params.id;
8108
+ const profile = profiles.getPersonProfile(id, { includeEvidence: true, includeInferred: true });
8109
+ if (!profile) {
8110
+ reply.code(404);
8111
+ return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u8BE5\u6210\u5458\u3002" };
8112
+ }
8113
+ return profile;
8114
+ });
8115
+ app.get("/api/persons/:id/messages", async (request) => {
8116
+ const id = request.params.id;
8117
+ const limit = parseLimit2(request.query.limit, 50, 200);
8118
+ const rows = database.prepare(
8119
+ `SELECT m.id, m.platform, m.platform_message_id AS platformMessageId, m.sender_id AS senderId,
8120
+ m.sender_name AS senderName, m.message_type AS messageType, m.text, m.sent_at AS sentAt,
8121
+ c.name AS chatName, c.platform_chat_id AS platformChatId
8122
+ FROM messages m
8123
+ JOIN chats c ON c.id = m.chat_id
8124
+ WHERE m.person_id = ?
8125
+ ORDER BY m.sent_at DESC, m.created_at DESC
8126
+ LIMIT ?`
8127
+ ).all(id, limit);
8128
+ return { items: rows };
8129
+ });
8130
+ app.post("/api/persons/:id/profile/entries/:entryId/correct", async (request, reply) => {
8131
+ await tokenReady;
8132
+ if (!isAuthorizedWebAction(request, webActionToken)) {
8133
+ reply.code(403);
8134
+ return { ok: false, message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
8135
+ }
8136
+ const { id, entryId } = request.params;
8137
+ const existing = profiles.getProfileEntry(entryId);
8138
+ if (!existing || existing.personId !== id || existing.status !== "active") {
8139
+ reply.code(404);
8140
+ return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u8BE5\u6863\u6848\u6761\u76EE\u3002" };
8141
+ }
8142
+ const body = request.body;
8143
+ const category = readStringField(body, "category") ?? existing.category;
8144
+ const content = readStringField(body, "content");
8145
+ const entryType = readStringField(body, "entryType") ?? existing.entryType;
8146
+ const evidenceMessageId = readStringField(body, "evidenceMessageId");
8147
+ const quote = readStringField(body, "quote");
8148
+ const reason = readStringField(body, "reason") ?? "\u7528\u6237\u5728 Web UI \u663E\u5F0F\u4FEE\u6B63";
8149
+ if (!content || !evidenceMessageId || !quote) {
8150
+ reply.code(400);
8151
+ return { ok: false, message: "\u4FEE\u6B63\u5FC5\u987B\u5305\u542B content\u3001evidenceMessageId \u548C quote\u3002" };
8152
+ }
8153
+ if (entryType !== "fact" && entryType !== "inferred") {
8154
+ reply.code(400);
8155
+ return { ok: false, message: "entryType \u5FC5\u987B\u662F fact \u6216 inferred\u3002" };
8156
+ }
8157
+ if (category === existing.category && content === existing.content) {
8158
+ reply.code(400);
8159
+ return { ok: false, message: "\u4FEE\u6B63\u5185\u5BB9\u6CA1\u6709\u53D8\u5316\u3002" };
8160
+ }
8161
+ const entryIdNew = profiles.replaceProfileEntry({
8162
+ supersedeEntryId: entryId,
8163
+ input: {
8164
+ personId: id,
8165
+ category,
8166
+ content,
8167
+ entryType,
8168
+ confidence: Math.min(1, Math.max(0, readNumberField(body, "confidence", existing.confidence))),
8169
+ source: "explicit_user_request",
8170
+ evidence: [{ messageId: evidenceMessageId, quote, reason }]
8171
+ }
8172
+ });
8173
+ return { ok: true, entryId: entryIdNew };
8174
+ });
8175
+ app.delete("/api/persons/:id/profile/entries/:entryId", async (request, reply) => {
8176
+ await tokenReady;
8177
+ if (!isAuthorizedWebAction(request, webActionToken)) {
8178
+ reply.code(403);
8179
+ return { ok: false, message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
8180
+ }
8181
+ const { id, entryId } = request.params;
8182
+ const existing = profiles.getProfileEntry(entryId);
8183
+ if (!existing || existing.personId !== id || existing.status !== "active") {
8184
+ reply.code(404);
8185
+ return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u8BE5\u6863\u6848\u6761\u76EE\u3002" };
8186
+ }
8187
+ profiles.markProfileEntryDeleted(entryId);
8188
+ return { ok: true };
8189
+ });
6836
8190
  app.post("/api/process/messages", async (request, reply) => {
6837
8191
  await tokenReady;
6838
8192
  if (!isAuthorizedWebAction(request, webActionToken)) {
@@ -7070,7 +8424,7 @@ async function startGatewayForegroundCommand() {
7070
8424
  const gatewayRuntime = createFeishuGateway({
7071
8425
  config,
7072
8426
  secrets,
7073
- ingestor: new GatewayIngestor(database),
8427
+ ingestor: new GatewayIngestor(database, { profiles: new ProfileRepository(database) }),
7074
8428
  resourceDownloader: FeishuResourceDownloader.fromConfig(config, secrets),
7075
8429
  attachmentVectorIndexer: vectorStore ? (messageId) => indexMessageChunks({
7076
8430
  messages: new MessageRepository(database),
@@ -7160,6 +8514,47 @@ web.command("start").description("\u542F\u52A8\u672C\u5730 Web UI").action(async
7160
8514
  const config = await loadConfig();
7161
8515
  await startWebServer(config, { version: package_default.version });
7162
8516
  });
8517
+ var profilesCommand = program.command("profiles").description("\u67E5\u770B\u548C\u7EF4\u62A4\u4E2A\u4EBA\u6863\u6848");
8518
+ profilesCommand.command("list").description("\u5217\u51FA\u4E2A\u4EBA\u6863\u6848").action(async () => {
8519
+ const config = await loadConfig();
8520
+ const database = openDatabase(config);
8521
+ try {
8522
+ const profiles = new ProfileRepository(database);
8523
+ for (const person of profiles.listPersons()) {
8524
+ console.log(`${person.id} ${person.primaryName} ${person.updatedAt}`);
8525
+ }
8526
+ } finally {
8527
+ database.close();
8528
+ }
8529
+ });
8530
+ profilesCommand.command("show").argument("personId").description("\u67E5\u770B\u4E2A\u4EBA\u6863\u6848").action(async (personId) => {
8531
+ const config = await loadConfig();
8532
+ const database = openDatabase(config);
8533
+ try {
8534
+ const profiles = new ProfileRepository(database);
8535
+ const profile = profiles.getPersonProfile(personId, { includeEvidence: true, includeInferred: true });
8536
+ if (!profile) {
8537
+ console.error("\u672A\u627E\u5230\u4E2A\u4EBA\u6863\u6848\u3002");
8538
+ process.exitCode = 1;
8539
+ return;
8540
+ }
8541
+ console.log(JSON.stringify(profile, null, 2));
8542
+ } finally {
8543
+ database.close();
8544
+ }
8545
+ });
8546
+ profilesCommand.command("backfill").description("\u4E3A\u5386\u53F2\u6D88\u606F\u56DE\u586B\u4E2A\u4EBA\u6863\u6848\u5173\u8054").option("--limit <number>", "\u6700\u591A\u56DE\u586B\u6D88\u606F\u6570", "1000").action(async (options) => {
8547
+ const config = await loadConfig();
8548
+ const database = openDatabase(config);
8549
+ try {
8550
+ const profiles = new ProfileRepository(database);
8551
+ const limit = Number(options.limit);
8552
+ const result = profiles.backfillMessagePersons({ limit: Number.isFinite(limit) ? limit : 1e3 });
8553
+ console.log(`\u5DF2\u56DE\u586B ${result.updatedMessages} \u6761\u6D88\u606F\u3002`);
8554
+ } finally {
8555
+ database.close();
8556
+ }
8557
+ });
7163
8558
  var data = program.command("data").description("\u7BA1\u7406\u672C\u5730\u77E5\u8BC6\u5E93\u6570\u636E");
7164
8559
  async function deleteDataCommand(targetType, targetId, options) {
7165
8560
  const shouldDelete = options.yes || await confirm({
@@ -7290,6 +8685,35 @@ processCommand.command("episodes").description("\u7ACB\u5373\u751F\u6210\u4F1A\u
7290
8685
  database.close();
7291
8686
  }
7292
8687
  });
8688
+ processCommand.command("profiles").description("\u5904\u7406\u672A\u603B\u7ED3\u6D88\u606F\u5E76\u66F4\u65B0\u4E2A\u4EBA\u6863\u6848").option("--limit <number>", "\u6BCF\u4E2A\u7FA4\u6700\u591A\u5904\u7406\u6D88\u606F\u6570", "100").action(async (options) => {
8689
+ const config = await loadConfig();
8690
+ const secrets = await loadSecrets();
8691
+ const database = openDatabase(config);
8692
+ try {
8693
+ const profiles = new ProfileRepository(database);
8694
+ profiles.backfillMessagePersons({ limit: 1e4 });
8695
+ const processor = new ProfileDreamProcessor({ profiles, model: createChatModel(config, secrets) });
8696
+ const chats = profiles.listChatsWithPendingDreamMessages();
8697
+ let succeeded = 0;
8698
+ let failed = 0;
8699
+ let skipped = 0;
8700
+ const limit = Number(options.limit);
8701
+ for (const chat of chats) {
8702
+ const result = await processor.processChat({
8703
+ platform: chat.platform,
8704
+ platformChatId: chat.platformChatId,
8705
+ limit: Number.isFinite(limit) ? limit : 100
8706
+ });
8707
+ if (result.status === "succeeded") succeeded += 1;
8708
+ if (result.status === "failed") failed += 1;
8709
+ if (result.status === "skipped") skipped += 1;
8710
+ console.log(`${chat.platformChatId}: ${result.status}, messages=${result.processedMessageCount}, entries=${result.generatedEntryCount}${result.error ? `, error=${result.error}` : ""}`);
8711
+ }
8712
+ console.log(`\u4E2A\u4EBA\u6863\u6848\u5904\u7406\u5B8C\u6210\uFF1A\u6210\u529F ${succeeded}\uFF0C\u5931\u8D25 ${failed}\uFF0C\u8DF3\u8FC7 ${skipped}\u3002`);
8713
+ } finally {
8714
+ database.close();
8715
+ }
8716
+ });
7293
8717
  var files = program.command("files").description("\u7BA1\u7406\u672C\u5730\u6587\u4EF6\u77E5\u8BC6\u6E90");
7294
8718
  files.command("add").description("\u628A\u672C\u5730\u6587\u4EF6\u89E3\u6790\u3001\u4FDD\u5B58\u5230\u6570\u636E\u76EE\u5F55\u5E76\u5199\u5165 RAG \u77E5\u8BC6\u5E93").argument("<paths...>", "\u6587\u4EF6\u8DEF\u5F84\uFF0C\u652F\u6301 txt\u3001md\u3001json\u3001csv\u3001tsv\u3001log\u3001docx\u3001pdf").action(async (paths) => {
7295
8719
  const config = await loadConfig();
@@ -7464,7 +8888,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
7464
8888
  try {
7465
8889
  const raw = await fs15.readFile(options.file, "utf8");
7466
8890
  const payload = JSON.parse(raw);
7467
- const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
8891
+ const result = new GatewayIngestor(database, { profiles: new ProfileRepository(database) }).ingestFeishuEvent(payload);
7468
8892
  if (!result.accepted) {
7469
8893
  console.log(result.reason);
7470
8894
  return;