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/AGENTS.md +10 -0
- package/CHANGELOG.md +52 -0
- package/README.md +60 -14
- package/dist/cli.js +1514 -90
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +163 -3
- package/dist/index.js +1283 -77
- package/dist/index.js.map +1 -1
- package/docs/DEVELOPMENT_PLAN.md +45 -1
- package/docs/PRD.md +14 -0
- package/docs/TECHNICAL_ARCHITECTURE.md +117 -0
- package/package.json +3 -2
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
3500
|
-
function
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
3997
|
-
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 =
|
|
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
|
|
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_${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
5279
|
+
createdAt: failedAt
|
|
4451
5280
|
});
|
|
4452
|
-
await this.sendResponse(decision.chatId, questionMessageId,
|
|
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
|
|
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 =
|
|
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
|
|
5315
|
-
const
|
|
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:
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
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 === '
|
|
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
|
|
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
|
|
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")
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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;
|