chattercatcher 0.2.6 → 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 +1313 -81
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +163 -3
- package/dist/index.js +1087 -73
- 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,
|
|
@@ -580,6 +656,11 @@ function migrateDatabase(database) {
|
|
|
580
656
|
CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
|
|
581
657
|
ON feishu_chat_members(chat_id, user_name);
|
|
582
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();
|
|
583
664
|
const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
|
|
584
665
|
const ensureCronJobColumn = (name, definition) => {
|
|
585
666
|
if (!cronJobColumns.some((column) => column.name === name)) {
|
|
@@ -1289,6 +1370,10 @@ function buildScopeWhere(scope) {
|
|
|
1289
1370
|
clauses.push("c.platform_chat_id = ?");
|
|
1290
1371
|
params.push(scope.platformChatId);
|
|
1291
1372
|
}
|
|
1373
|
+
if (scope?.personId) {
|
|
1374
|
+
clauses.push("m.person_id = ?");
|
|
1375
|
+
params.push(scope.personId);
|
|
1376
|
+
}
|
|
1292
1377
|
return {
|
|
1293
1378
|
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
1294
1379
|
params
|
|
@@ -1333,14 +1418,15 @@ var MessageRepository = class {
|
|
|
1333
1418
|
`
|
|
1334
1419
|
INSERT INTO messages (
|
|
1335
1420
|
id, platform, platform_message_id, chat_id, sender_id, sender_name,
|
|
1336
|
-
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
|
|
1337
1422
|
)
|
|
1338
1423
|
VALUES (
|
|
1339
1424
|
@id, @platform, @platformMessageId, @chatId, @senderId, @senderName,
|
|
1340
|
-
@messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1425
|
+
@personId, @messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1341
1426
|
)
|
|
1342
1427
|
ON CONFLICT(platform, platform_message_id)
|
|
1343
1428
|
DO UPDATE SET
|
|
1429
|
+
person_id = COALESCE(excluded.person_id, messages.person_id),
|
|
1344
1430
|
message_type = excluded.message_type,
|
|
1345
1431
|
text = excluded.text,
|
|
1346
1432
|
raw_payload_json = excluded.raw_payload_json,
|
|
@@ -1353,6 +1439,7 @@ var MessageRepository = class {
|
|
|
1353
1439
|
chatId,
|
|
1354
1440
|
senderId: input2.senderId,
|
|
1355
1441
|
senderName: input2.senderName,
|
|
1442
|
+
personId: input2.personId ?? null,
|
|
1356
1443
|
messageType: input2.messageType,
|
|
1357
1444
|
text: input2.text,
|
|
1358
1445
|
rawPayloadJson,
|
|
@@ -1395,6 +1482,7 @@ var MessageRepository = class {
|
|
|
1395
1482
|
m.chat_id AS chatId,
|
|
1396
1483
|
m.sender_id AS senderId,
|
|
1397
1484
|
m.sender_name AS senderName,
|
|
1485
|
+
m.person_id AS personId,
|
|
1398
1486
|
m.sent_at AS sentAt,
|
|
1399
1487
|
c.platform_chat_id AS platformChatId,
|
|
1400
1488
|
c.name AS chatName
|
|
@@ -1417,6 +1505,7 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1417
1505
|
platformMessageId: derivedPlatformMessageId,
|
|
1418
1506
|
senderId: source.senderId,
|
|
1419
1507
|
senderName: source.senderName,
|
|
1508
|
+
personId: source.personId ?? void 0,
|
|
1420
1509
|
messageType: "image_summary",
|
|
1421
1510
|
text: summaryText,
|
|
1422
1511
|
sentAt: source.sentAt,
|
|
@@ -1443,7 +1532,9 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1443
1532
|
1.0 AS score,
|
|
1444
1533
|
m.message_type AS messageType,
|
|
1445
1534
|
c.name AS chatName,
|
|
1535
|
+
m.sender_id AS senderId,
|
|
1446
1536
|
m.sender_name AS senderName,
|
|
1537
|
+
m.person_id AS personId,
|
|
1447
1538
|
m.sent_at AS sentAt
|
|
1448
1539
|
FROM message_chunks mc
|
|
1449
1540
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1464,7 +1555,9 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1464
1555
|
1.0 AS score,
|
|
1465
1556
|
m.message_type AS messageType,
|
|
1466
1557
|
c.name AS chatName,
|
|
1558
|
+
m.sender_id AS senderId,
|
|
1467
1559
|
m.sender_name AS senderName,
|
|
1560
|
+
m.person_id AS personId,
|
|
1468
1561
|
m.sent_at AS sentAt
|
|
1469
1562
|
FROM message_chunks mc
|
|
1470
1563
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1488,7 +1581,9 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1488
1581
|
1.0 AS score,
|
|
1489
1582
|
m.message_type AS messageType,
|
|
1490
1583
|
c.name AS chatName,
|
|
1584
|
+
m.sender_id AS senderId,
|
|
1491
1585
|
m.sender_name AS senderName,
|
|
1586
|
+
m.person_id AS personId,
|
|
1492
1587
|
m.sent_at AS sentAt
|
|
1493
1588
|
FROM message_chunks mc
|
|
1494
1589
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1504,7 +1599,7 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1504
1599
|
const excludedIds = options.excludeMessageIds ?? [];
|
|
1505
1600
|
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
1506
1601
|
const scope = buildScopeWhere(options.scope);
|
|
1507
|
-
const
|
|
1602
|
+
const ftsRows = this.database.prepare(
|
|
1508
1603
|
`
|
|
1509
1604
|
SELECT
|
|
1510
1605
|
fts.chunk_id AS chunkId,
|
|
@@ -1514,8 +1609,11 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1514
1609
|
bm25(message_chunks_fts) * -1 AS score,
|
|
1515
1610
|
m.message_type AS messageType,
|
|
1516
1611
|
c.name AS chatName,
|
|
1612
|
+
m.sender_id AS senderId,
|
|
1517
1613
|
m.sender_name AS senderName,
|
|
1518
|
-
m.
|
|
1614
|
+
m.person_id AS personId,
|
|
1615
|
+
m.sent_at AS sentAt,
|
|
1616
|
+
mc.chunk_index AS chunkIndex
|
|
1519
1617
|
FROM message_chunks_fts fts
|
|
1520
1618
|
JOIN message_chunks mc ON mc.id = fts.chunk_id
|
|
1521
1619
|
JOIN messages m ON m.id = fts.message_id
|
|
@@ -1523,10 +1621,23 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1523
1621
|
WHERE message_chunks_fts MATCH ?
|
|
1524
1622
|
${excludedWhere}
|
|
1525
1623
|
${scope.where}
|
|
1526
|
-
ORDER BY bm25(message_chunks_fts)
|
|
1624
|
+
ORDER BY bm25(message_chunks_fts), m.sent_at DESC, mc.chunk_index ASC
|
|
1527
1625
|
LIMIT ?
|
|
1528
1626
|
`
|
|
1529
|
-
).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
|
+
}
|
|
1530
1641
|
if (ftsResults.length > 0) {
|
|
1531
1642
|
return ftsResults;
|
|
1532
1643
|
}
|
|
@@ -1540,22 +1651,30 @@ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}
|
|
|
1540
1651
|
return this.database.prepare(
|
|
1541
1652
|
`
|
|
1542
1653
|
SELECT
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
|
1559
1678
|
LIMIT ?
|
|
1560
1679
|
`
|
|
1561
1680
|
).all(...params, ...excludedIds, ...scope.params, limit);
|
|
@@ -1970,19 +2089,20 @@ var HybridRetriever = class {
|
|
|
1970
2089
|
|
|
1971
2090
|
// src/rag/message-retriever.ts
|
|
1972
2091
|
function toEvidenceSource(result) {
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
label: result.senderName,
|
|
1977
|
-
timestamp: result.sentAt
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
1980
|
-
return {
|
|
1981
|
-
type: "message",
|
|
1982
|
-
label: result.chatName,
|
|
1983
|
-
sender: result.senderName,
|
|
2092
|
+
const source = {
|
|
2093
|
+
type: result.messageType === "file" ? "file" : "message",
|
|
2094
|
+
label: result.messageType === "file" ? result.senderName : result.chatName,
|
|
1984
2095
|
timestamp: result.sentAt
|
|
1985
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;
|
|
1986
2106
|
}
|
|
1987
2107
|
var MessageFtsRetriever = class {
|
|
1988
2108
|
constructor(messages, options = {}) {
|
|
@@ -2111,6 +2231,9 @@ function toEvidenceSource2(row) {
|
|
|
2111
2231
|
type: "message",
|
|
2112
2232
|
label: row.chatName,
|
|
2113
2233
|
sender: row.senderName,
|
|
2234
|
+
senderId: row.senderId,
|
|
2235
|
+
personId: row.personId,
|
|
2236
|
+
profileAvailable: Boolean(row.personId),
|
|
2114
2237
|
timestamp: row.sentAt
|
|
2115
2238
|
};
|
|
2116
2239
|
}
|
|
@@ -2125,6 +2248,10 @@ function buildScopeWhere3(scope) {
|
|
|
2125
2248
|
clauses.push("c.platform_chat_id = ?");
|
|
2126
2249
|
params.push(scope.platformChatId);
|
|
2127
2250
|
}
|
|
2251
|
+
if (scope?.personId) {
|
|
2252
|
+
clauses.push("m.person_id = ?");
|
|
2253
|
+
params.push(scope.personId);
|
|
2254
|
+
}
|
|
2128
2255
|
return {
|
|
2129
2256
|
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
2130
2257
|
params
|
|
@@ -2175,7 +2302,9 @@ var SqliteVectorStore = class {
|
|
|
2175
2302
|
mc.id AS chunkId,
|
|
2176
2303
|
mc.text AS text,
|
|
2177
2304
|
c.name AS chatName,
|
|
2305
|
+
m.sender_id AS senderId,
|
|
2178
2306
|
m.sender_name AS senderName,
|
|
2307
|
+
m.person_id AS personId,
|
|
2179
2308
|
m.sent_at AS sentAt,
|
|
2180
2309
|
e.embedding_json AS embeddingJson
|
|
2181
2310
|
FROM message_chunk_embeddings e
|
|
@@ -2256,8 +2385,12 @@ async function createAgenticRagSearchTools(input2) {
|
|
|
2256
2385
|
new SqliteVectorStore(input2.database, { model: input2.config.embedding.model })
|
|
2257
2386
|
) : void 0;
|
|
2258
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
|
+
}
|
|
2259
2392
|
return {
|
|
2260
|
-
tools
|
|
2393
|
+
tools,
|
|
2261
2394
|
close: () => {
|
|
2262
2395
|
}
|
|
2263
2396
|
};
|
|
@@ -3414,6 +3547,588 @@ function parseExactNumber2(field, min, max) {
|
|
|
3414
3547
|
return value;
|
|
3415
3548
|
}
|
|
3416
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);
|
|
4119
|
+
}
|
|
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
|
+
};
|
|
4131
|
+
|
|
3417
4132
|
// src/rag/indexer.ts
|
|
3418
4133
|
var EMBEDDING_INDEX_BATCH_SIZE = 64;
|
|
3419
4134
|
async function indexMessageChunks(input2) {
|
|
@@ -3501,12 +4216,12 @@ async function processMessagesNow(input2) {
|
|
|
3501
4216
|
}
|
|
3502
4217
|
|
|
3503
4218
|
// src/multimodal/tasks.ts
|
|
3504
|
-
import
|
|
3505
|
-
function
|
|
4219
|
+
import crypto6 from "crypto";
|
|
4220
|
+
function nowIso5() {
|
|
3506
4221
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3507
4222
|
}
|
|
3508
4223
|
function stableId3(sourceMessageId, imageKey) {
|
|
3509
|
-
return
|
|
4224
|
+
return crypto6.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
|
|
3510
4225
|
}
|
|
3511
4226
|
function mapRow(row) {
|
|
3512
4227
|
if (!row) {
|
|
@@ -3534,7 +4249,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3534
4249
|
database;
|
|
3535
4250
|
enqueue(input2) {
|
|
3536
4251
|
const id = stableId3(input2.sourceMessageId, input2.imageKey);
|
|
3537
|
-
const timestamp =
|
|
4252
|
+
const timestamp = nowIso5();
|
|
3538
4253
|
this.database.prepare(
|
|
3539
4254
|
`
|
|
3540
4255
|
INSERT INTO image_multimodal_tasks (
|
|
@@ -3622,7 +4337,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3622
4337
|
updated_at = @updatedAt
|
|
3623
4338
|
WHERE id = @id AND status = 'pending'
|
|
3624
4339
|
`
|
|
3625
|
-
).run({ id, updatedAt:
|
|
4340
|
+
).run({ id, updatedAt: nowIso5() });
|
|
3626
4341
|
if (result.changes === 0) {
|
|
3627
4342
|
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A${id}`);
|
|
3628
4343
|
}
|
|
@@ -3638,7 +4353,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3638
4353
|
updated_at = @updatedAt
|
|
3639
4354
|
WHERE id = @id
|
|
3640
4355
|
`
|
|
3641
|
-
).run({ id, derivedMessageId, updatedAt:
|
|
4356
|
+
).run({ id, derivedMessageId, updatedAt: nowIso5() });
|
|
3642
4357
|
return this.requireById(id);
|
|
3643
4358
|
}
|
|
3644
4359
|
markSkipped(id, reason) {
|
|
@@ -3651,7 +4366,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3651
4366
|
updated_at = @updatedAt
|
|
3652
4367
|
WHERE id = @id
|
|
3653
4368
|
`
|
|
3654
|
-
).run({ id, reason, updatedAt:
|
|
4369
|
+
).run({ id, reason, updatedAt: nowIso5() });
|
|
3655
4370
|
return this.requireById(id);
|
|
3656
4371
|
}
|
|
3657
4372
|
markFailed(id, error, finalFailure) {
|
|
@@ -3664,7 +4379,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3664
4379
|
updated_at = @updatedAt
|
|
3665
4380
|
WHERE id = @id
|
|
3666
4381
|
`
|
|
3667
|
-
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt:
|
|
4382
|
+
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso5() });
|
|
3668
4383
|
return this.requireById(id);
|
|
3669
4384
|
}
|
|
3670
4385
|
getById(id) {
|
|
@@ -3946,7 +4661,7 @@ function createFeishuChatMembersClient(client) {
|
|
|
3946
4661
|
}
|
|
3947
4662
|
|
|
3948
4663
|
// src/cron/tools.ts
|
|
3949
|
-
function
|
|
4664
|
+
function readString2(input2, key) {
|
|
3950
4665
|
const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
|
|
3951
4666
|
if (typeof value !== "string" || !value.trim()) {
|
|
3952
4667
|
throw new Error(`${key} \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
|
|
@@ -3998,8 +4713,8 @@ function createCronJobTools(input2) {
|
|
|
3998
4713
|
const job = input2.repository.create({
|
|
3999
4714
|
chatId: input2.chatId,
|
|
4000
4715
|
createdByOpenId: input2.createdByOpenId,
|
|
4001
|
-
schedule:
|
|
4002
|
-
prompt:
|
|
4716
|
+
schedule: readString2(rawInput, "schedule"),
|
|
4717
|
+
prompt: readString2(rawInput, "prompt"),
|
|
4003
4718
|
imageFileName: readOptionalString(rawInput, "imageFileName"),
|
|
4004
4719
|
mentionTargetName,
|
|
4005
4720
|
mentionOpenId: mentionTarget?.openId,
|
|
@@ -4029,7 +4744,7 @@ function createCronJobTools(input2) {
|
|
|
4029
4744
|
additionalProperties: false
|
|
4030
4745
|
},
|
|
4031
4746
|
execute: async (rawInput) => {
|
|
4032
|
-
const id =
|
|
4747
|
+
const id = readString2(rawInput, "id");
|
|
4033
4748
|
const ok = input2.repository.deleteByChat(id, input2.chatId);
|
|
4034
4749
|
return JSON.stringify({
|
|
4035
4750
|
ok,
|
|
@@ -4042,7 +4757,7 @@ function createCronJobTools(input2) {
|
|
|
4042
4757
|
}
|
|
4043
4758
|
|
|
4044
4759
|
// src/rag/qa-logs.ts
|
|
4045
|
-
import
|
|
4760
|
+
import crypto7 from "crypto";
|
|
4046
4761
|
|
|
4047
4762
|
// src/rag/qa-trace.ts
|
|
4048
4763
|
function hasQaTrace(trace) {
|
|
@@ -4082,7 +4797,7 @@ var QaLogRepository = class {
|
|
|
4082
4797
|
create(input2) {
|
|
4083
4798
|
const trace = input2.trace ?? {};
|
|
4084
4799
|
const record = {
|
|
4085
|
-
id: `qa_${
|
|
4800
|
+
id: `qa_${crypto7.randomUUID()}`,
|
|
4086
4801
|
chatId: input2.chatId ?? null,
|
|
4087
4802
|
questionMessageId: input2.questionMessageId ?? null,
|
|
4088
4803
|
question: input2.question,
|
|
@@ -4262,13 +4977,13 @@ ${block.text}`;
|
|
|
4262
4977
|
function toToolErrorContent(message) {
|
|
4263
4978
|
return JSON.stringify({ ok: false, error: message });
|
|
4264
4979
|
}
|
|
4265
|
-
function
|
|
4980
|
+
function nowIso6() {
|
|
4266
4981
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4267
4982
|
}
|
|
4268
4983
|
function finalizeTrace(trace, status, finalAnswer, startedAtMs) {
|
|
4269
4984
|
return {
|
|
4270
4985
|
...trace,
|
|
4271
|
-
completedAt:
|
|
4986
|
+
completedAt: nowIso6(),
|
|
4272
4987
|
durationMs: Date.now() - startedAtMs,
|
|
4273
4988
|
status,
|
|
4274
4989
|
finalAnswer
|
|
@@ -4325,11 +5040,11 @@ async function runFeishuToolLoop(input2) {
|
|
|
4325
5040
|
content: assistantResult.content,
|
|
4326
5041
|
reasoningContent: assistantResult.reasoningContent,
|
|
4327
5042
|
toolCalls: assistantResult.toolCalls,
|
|
4328
|
-
createdAt:
|
|
5043
|
+
createdAt: nowIso6()
|
|
4329
5044
|
});
|
|
4330
5045
|
if (assistantResult.toolCalls.length === 0) {
|
|
4331
5046
|
if (hasRawToolCallMarkup) {
|
|
4332
|
-
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:
|
|
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() });
|
|
4333
5048
|
break;
|
|
4334
5049
|
}
|
|
4335
5050
|
const answer = assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
@@ -4337,7 +5052,7 @@ async function runFeishuToolLoop(input2) {
|
|
|
4337
5052
|
}
|
|
4338
5053
|
for (const toolCall of assistantResult.toolCalls) {
|
|
4339
5054
|
if (toolCallsUsed >= maxToolCalls) {
|
|
4340
|
-
trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt:
|
|
5055
|
+
trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt: nowIso6() });
|
|
4341
5056
|
return { answer: FEISHU_TOOL_LOOP_LIMIT_REACHED, trace: finalizeTrace(trace, "failed", FEISHU_TOOL_LOOP_LIMIT_REACHED, startedAtMs) };
|
|
4342
5057
|
}
|
|
4343
5058
|
toolCallsUsed += 1;
|
|
@@ -4348,7 +5063,7 @@ async function runFeishuToolLoop(input2) {
|
|
|
4348
5063
|
name: toolCall.name,
|
|
4349
5064
|
input: toolCall.input,
|
|
4350
5065
|
error: `\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`,
|
|
4351
|
-
createdAt:
|
|
5066
|
+
createdAt: nowIso6()
|
|
4352
5067
|
});
|
|
4353
5068
|
messages.push({
|
|
4354
5069
|
role: "tool",
|
|
@@ -4364,7 +5079,7 @@ async function runFeishuToolLoop(input2) {
|
|
|
4364
5079
|
name: toolCall.name,
|
|
4365
5080
|
input: toolCall.input,
|
|
4366
5081
|
content: result,
|
|
4367
|
-
createdAt:
|
|
5082
|
+
createdAt: nowIso6()
|
|
4368
5083
|
});
|
|
4369
5084
|
messages.push({
|
|
4370
5085
|
role: "tool",
|
|
@@ -4378,7 +5093,7 @@ async function runFeishuToolLoop(input2) {
|
|
|
4378
5093
|
name: toolCall.name,
|
|
4379
5094
|
input: toolCall.input,
|
|
4380
5095
|
error: message,
|
|
4381
|
-
createdAt:
|
|
5096
|
+
createdAt: nowIso6()
|
|
4382
5097
|
});
|
|
4383
5098
|
messages.push({
|
|
4384
5099
|
role: "tool",
|
|
@@ -4394,11 +5109,11 @@ async function runFeishuToolLoop(input2) {
|
|
|
4394
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" }
|
|
4395
5110
|
]);
|
|
4396
5111
|
const answer = salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4397
|
-
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:
|
|
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() });
|
|
4398
5113
|
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4399
5114
|
} catch {
|
|
4400
5115
|
const answer = "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4401
|
-
trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt:
|
|
5116
|
+
trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt: nowIso6() });
|
|
4402
5117
|
return { answer, trace: finalizeTrace(trace, "failed", answer, startedAtMs) };
|
|
4403
5118
|
}
|
|
4404
5119
|
}
|
|
@@ -4505,7 +5220,8 @@ var FeishuQuestionHandler = class {
|
|
|
4505
5220
|
secrets: this.options.secrets,
|
|
4506
5221
|
database: this.options.database,
|
|
4507
5222
|
messages: new MessageRepository(this.options.database),
|
|
4508
|
-
excludeMessageIds: options.excludeMessageIds
|
|
5223
|
+
excludeMessageIds: options.excludeMessageIds,
|
|
5224
|
+
profileTools: createPersonProfileTools({ profiles: new ProfileRepository(this.options.database) })
|
|
4509
5225
|
});
|
|
4510
5226
|
try {
|
|
4511
5227
|
try {
|
|
@@ -4982,7 +5698,8 @@ function createFeishuGateway(options) {
|
|
|
4982
5698
|
secrets: options.secrets,
|
|
4983
5699
|
database: options.cronJobProcessor.database,
|
|
4984
5700
|
messages: new MessageRepository(options.cronJobProcessor.database),
|
|
4985
|
-
scope: { platform: "feishu", platformChatId: job.chatId }
|
|
5701
|
+
scope: { platform: "feishu", platformChatId: job.chatId },
|
|
5702
|
+
profileTools: createPersonProfileTools({ profiles: new ProfileRepository(options.cronJobProcessor.database) })
|
|
4986
5703
|
});
|
|
4987
5704
|
try {
|
|
4988
5705
|
const memberPrompt = formatFeishuMemberPrompt(
|
|
@@ -5086,7 +5803,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
5086
5803
|
};
|
|
5087
5804
|
|
|
5088
5805
|
// src/files/ingest.ts
|
|
5089
|
-
import
|
|
5806
|
+
import crypto8 from "crypto";
|
|
5090
5807
|
import fs12 from "fs/promises";
|
|
5091
5808
|
import path15 from "path";
|
|
5092
5809
|
|
|
@@ -5150,7 +5867,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
5150
5867
|
}
|
|
5151
5868
|
}
|
|
5152
5869
|
function stableStoredName(sourcePath, fileName) {
|
|
5153
|
-
const digest =
|
|
5870
|
+
const digest = crypto8.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
5154
5871
|
return `${digest}-${fileName}`;
|
|
5155
5872
|
}
|
|
5156
5873
|
async function ingestLocalFile(input2) {
|
|
@@ -5406,16 +6123,32 @@ function isMultimodalReady(config, secrets) {
|
|
|
5406
6123
|
return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
|
|
5407
6124
|
}
|
|
5408
6125
|
var GatewayIngestor = class {
|
|
5409
|
-
constructor(database) {
|
|
6126
|
+
constructor(database, options = {}) {
|
|
5410
6127
|
this.database = database;
|
|
6128
|
+
this.options = options;
|
|
5411
6129
|
this.messages = new MessageRepository(database);
|
|
5412
6130
|
this.jobs = new FileJobRepository(database);
|
|
5413
6131
|
this.imageTasks = new ImageMultimodalTaskRepository(database);
|
|
5414
6132
|
}
|
|
5415
6133
|
database;
|
|
6134
|
+
options;
|
|
5416
6135
|
messages;
|
|
5417
6136
|
jobs;
|
|
5418
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
|
+
}
|
|
5419
6152
|
ingestFeishuEvent(payload) {
|
|
5420
6153
|
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
5421
6154
|
if (!normalized) {
|
|
@@ -5424,12 +6157,13 @@ var GatewayIngestor = class {
|
|
|
5424
6157
|
reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
|
|
5425
6158
|
};
|
|
5426
6159
|
}
|
|
5427
|
-
const
|
|
5428
|
-
const
|
|
6160
|
+
const enriched = this.enrichWithPerson(normalized);
|
|
6161
|
+
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
6162
|
+
const messageId = this.messages.ingest(enriched);
|
|
5429
6163
|
return {
|
|
5430
6164
|
accepted: true,
|
|
5431
6165
|
messageId,
|
|
5432
|
-
message:
|
|
6166
|
+
message: enriched,
|
|
5433
6167
|
duplicate
|
|
5434
6168
|
};
|
|
5435
6169
|
}
|
|
@@ -5443,7 +6177,7 @@ var GatewayIngestor = class {
|
|
|
5443
6177
|
}
|
|
5444
6178
|
const openId = extractFeishuSenderOpenId(normalized);
|
|
5445
6179
|
const senderName = openId ? await input2.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
|
|
5446
|
-
const enriched = { ...normalized, senderName };
|
|
6180
|
+
const enriched = this.enrichWithPerson({ ...normalized, senderName });
|
|
5447
6181
|
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
5448
6182
|
const messageId = this.messages.ingest(enriched);
|
|
5449
6183
|
return {
|
|
@@ -5712,6 +6446,153 @@ function createMultimodalModel(config, secrets) {
|
|
|
5712
6446
|
// src/cli.ts
|
|
5713
6447
|
import * as lark4 from "@larksuiteoapi/node-sdk";
|
|
5714
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
|
+
|
|
5715
6596
|
// src/rag/answer.ts
|
|
5716
6597
|
var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
|
|
5717
6598
|
var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
|
|
@@ -5952,7 +6833,7 @@ async function updateChatterCatcher(options) {
|
|
|
5952
6833
|
}
|
|
5953
6834
|
|
|
5954
6835
|
// src/web/server.ts
|
|
5955
|
-
import
|
|
6836
|
+
import crypto9 from "crypto";
|
|
5956
6837
|
import Fastify from "fastify";
|
|
5957
6838
|
function buildHtml() {
|
|
5958
6839
|
return `<!doctype html>
|
|
@@ -6294,6 +7175,10 @@ function buildHtml() {
|
|
|
6294
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>
|
|
6295
7176
|
<span>\u95EE\u7B54\u65E5\u5FD7</span>
|
|
6296
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>
|
|
6297
7182
|
<button class="nav-item" data-view="settings">
|
|
6298
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>
|
|
6299
7184
|
<span>\u8BBE\u7F6E</span>
|
|
@@ -6325,6 +7210,10 @@ function buildHtml() {
|
|
|
6325
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>
|
|
6326
7211
|
<span>\u4EFB\u52A1</span>
|
|
6327
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>
|
|
6328
7217
|
<button class="mobile-nav-item" data-view="settings">
|
|
6329
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>
|
|
6330
7219
|
<span>\u8BBE\u7F6E</span>
|
|
@@ -6421,6 +7310,18 @@ function buildHtml() {
|
|
|
6421
7310
|
<div class="content-panel glass"><div id="qa-logs-list"></div></div>
|
|
6422
7311
|
</div>
|
|
6423
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
|
+
|
|
6424
7325
|
<div class="view" id="view-settings">
|
|
6425
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>
|
|
6426
7327
|
<div class="settings-group glass" id="settings-config"></div>
|
|
@@ -6456,7 +7357,8 @@ function buildHtml() {
|
|
|
6456
7357
|
let allFileJobs = [];
|
|
6457
7358
|
let allCronJobs = [];
|
|
6458
7359
|
let allQaLogs = [];
|
|
6459
|
-
let
|
|
7360
|
+
let allPersons = [];
|
|
7361
|
+
let selectedPersonId = null;
|
|
6460
7362
|
let statusData = null;
|
|
6461
7363
|
|
|
6462
7364
|
function fmt(value) { return value == null || value === "" ? "-" : String(value); }
|
|
@@ -6500,6 +7402,7 @@ function buildHtml() {
|
|
|
6500
7402
|
if (view === "files") renderFilesView();
|
|
6501
7403
|
if (view === "tasks") renderTasksView();
|
|
6502
7404
|
if (view === "qa-logs") renderQaLogsView();
|
|
7405
|
+
if (view === "persons") renderPersonsView();
|
|
6503
7406
|
}
|
|
6504
7407
|
|
|
6505
7408
|
document.querySelectorAll(".nav-item, .mobile-nav-item").forEach(function(el) {
|
|
@@ -6542,6 +7445,55 @@ function buildHtml() {
|
|
|
6542
7445
|
return result;
|
|
6543
7446
|
}
|
|
6544
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
|
+
|
|
6545
7497
|
function renderMetrics(status) {
|
|
6546
7498
|
var gatewayClass = status.gateway.configured ? "status-dot online" : "status-dot offline";
|
|
6547
7499
|
var gatewayText = status.gateway.connection === "running" ? "\u8FD0\u884C\u4E2D" : (!status.gateway.configured ? "\u672A\u914D\u7F6E" : "\u5F85\u542F\u52A8");
|
|
@@ -6803,6 +7755,96 @@ function buildHtml() {
|
|
|
6803
7755
|
container.innerHTML = html;
|
|
6804
7756
|
}
|
|
6805
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>';
|
|
7844
|
+
}
|
|
7845
|
+
el.innerHTML = html;
|
|
7846
|
+
}
|
|
7847
|
+
|
|
6806
7848
|
function renderSettings(status) {
|
|
6807
7849
|
var el = document.getElementById("settings-config");
|
|
6808
7850
|
var html = '<h3 style="font-size:16px;font-weight:600;margin-bottom:var(--space-md);">\u7CFB\u7EDF\u914D\u7F6E</h3>';
|
|
@@ -6833,6 +7875,7 @@ function buildHtml() {
|
|
|
6833
7875
|
await loadSection("/api/file-jobs", function(data) { allFileJobs = data.items || []; });
|
|
6834
7876
|
await loadSection("/api/qa-logs?limit=20", function(data) { allQaLogs = data.items || []; });
|
|
6835
7877
|
await loadSection("/api/cron-jobs", function(data) { allCronJobs = data.items || []; });
|
|
7878
|
+
await loadSection("/api/persons", function(data) { allPersons = data.items || []; });
|
|
6836
7879
|
if (currentView === "messages") renderMessagesView();
|
|
6837
7880
|
if (currentView === "episodes") renderEpisodesView();
|
|
6838
7881
|
if (currentView === "files") renderFilesView();
|
|
@@ -6841,6 +7884,10 @@ function buildHtml() {
|
|
|
6841
7884
|
renderQaLogsView();
|
|
6842
7885
|
if (selectedQaLogId) void showQaLogDetail(selectedQaLogId);
|
|
6843
7886
|
}
|
|
7887
|
+
if (currentView === "persons") {
|
|
7888
|
+
renderPersonsView();
|
|
7889
|
+
if (selectedPersonId) void showPersonProfile(selectedPersonId);
|
|
7890
|
+
}
|
|
6844
7891
|
}
|
|
6845
7892
|
|
|
6846
7893
|
async function processNow() {
|
|
@@ -6867,6 +7914,11 @@ function buildHtml() {
|
|
|
6867
7914
|
void showQaLogDetail(qaLogId);
|
|
6868
7915
|
return;
|
|
6869
7916
|
}
|
|
7917
|
+
var personId = target.dataset.viewPerson || target.closest('[data-view-person]')?.dataset.viewPerson;
|
|
7918
|
+
if (personId) {
|
|
7919
|
+
void showPersonProfile(personId);
|
|
7920
|
+
return;
|
|
7921
|
+
}
|
|
6870
7922
|
var id = target.dataset.deleteCronJob;
|
|
6871
7923
|
if (!id) return;
|
|
6872
7924
|
target.disabled = true;
|
|
@@ -6886,7 +7938,7 @@ function buildHtml() {
|
|
|
6886
7938
|
</body>
|
|
6887
7939
|
</html>`;
|
|
6888
7940
|
}
|
|
6889
|
-
function
|
|
7941
|
+
function parseLimit2(value, fallback, max) {
|
|
6890
7942
|
const rawLimit = Number(value ?? fallback);
|
|
6891
7943
|
return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
|
|
6892
7944
|
}
|
|
@@ -6910,6 +7962,16 @@ function parseCookies(header) {
|
|
|
6910
7962
|
function isAuthorizedWebAction(request, token) {
|
|
6911
7963
|
return parseCookies(request.headers.cookie).chattercatcher_web_token === token;
|
|
6912
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
|
+
}
|
|
6913
7975
|
function toQaLogListItem(log) {
|
|
6914
7976
|
const { trace: _trace, ...item } = log;
|
|
6915
7977
|
return item;
|
|
@@ -6923,11 +7985,12 @@ function createWebApp(config, options = {}) {
|
|
|
6923
7985
|
const fileJobs = new FileJobRepository(database);
|
|
6924
7986
|
const qaLogs = new QaLogRepository(database);
|
|
6925
7987
|
const cronJobs = new CronJobRepository(database);
|
|
7988
|
+
const profiles = new ProfileRepository(database);
|
|
6926
7989
|
let webActionToken = "";
|
|
6927
7990
|
const tokenReady = (async () => {
|
|
6928
7991
|
const secrets = await loadSecrets();
|
|
6929
7992
|
if (!secrets.web.actionToken) {
|
|
6930
|
-
secrets.web.actionToken =
|
|
7993
|
+
secrets.web.actionToken = crypto9.randomBytes(32).toString("hex");
|
|
6931
7994
|
await saveSecrets(secrets);
|
|
6932
7995
|
}
|
|
6933
7996
|
webActionToken = getWebActionToken(secrets);
|
|
@@ -6965,32 +8028,32 @@ function createWebApp(config, options = {}) {
|
|
|
6965
8028
|
items: messages.listChats()
|
|
6966
8029
|
}));
|
|
6967
8030
|
app.get("/api/files", async (request) => {
|
|
6968
|
-
const limit =
|
|
8031
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6969
8032
|
return {
|
|
6970
8033
|
items: messages.listFiles(limit)
|
|
6971
8034
|
};
|
|
6972
8035
|
});
|
|
6973
8036
|
app.get("/api/file-jobs", async (request) => {
|
|
6974
|
-
const limit =
|
|
8037
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6975
8038
|
const status = request.query.status;
|
|
6976
8039
|
return {
|
|
6977
8040
|
items: fileJobs.list(limit, status === "processing" || status === "indexed" || status === "failed" ? { status } : {})
|
|
6978
8041
|
};
|
|
6979
8042
|
});
|
|
6980
8043
|
app.get("/api/messages/recent", async (request) => {
|
|
6981
|
-
const limit =
|
|
8044
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6982
8045
|
return {
|
|
6983
8046
|
items: messages.listRecentMessages(limit)
|
|
6984
8047
|
};
|
|
6985
8048
|
});
|
|
6986
8049
|
app.get("/api/episodes", async (request) => {
|
|
6987
|
-
const limit =
|
|
8050
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6988
8051
|
return {
|
|
6989
8052
|
items: episodes.listRecentEpisodes(limit)
|
|
6990
8053
|
};
|
|
6991
8054
|
});
|
|
6992
8055
|
app.get("/api/qa-logs", async (request) => {
|
|
6993
|
-
const limit =
|
|
8056
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6994
8057
|
return {
|
|
6995
8058
|
items: qaLogs.listRecent(limit).map(toQaLogListItem)
|
|
6996
8059
|
};
|
|
@@ -7005,7 +8068,7 @@ function createWebApp(config, options = {}) {
|
|
|
7005
8068
|
return log;
|
|
7006
8069
|
});
|
|
7007
8070
|
app.get("/api/cron-jobs", async (request) => {
|
|
7008
|
-
const limit =
|
|
8071
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
7009
8072
|
return {
|
|
7010
8073
|
items: cronJobs.list(limit)
|
|
7011
8074
|
};
|
|
@@ -7025,6 +8088,105 @@ function createWebApp(config, options = {}) {
|
|
|
7025
8088
|
const ok = cronJobs.deleteByChat(id, job.chatId);
|
|
7026
8089
|
return { ok };
|
|
7027
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
|
+
});
|
|
7028
8190
|
app.post("/api/process/messages", async (request, reply) => {
|
|
7029
8191
|
await tokenReady;
|
|
7030
8192
|
if (!isAuthorizedWebAction(request, webActionToken)) {
|
|
@@ -7262,7 +8424,7 @@ async function startGatewayForegroundCommand() {
|
|
|
7262
8424
|
const gatewayRuntime = createFeishuGateway({
|
|
7263
8425
|
config,
|
|
7264
8426
|
secrets,
|
|
7265
|
-
ingestor: new GatewayIngestor(database),
|
|
8427
|
+
ingestor: new GatewayIngestor(database, { profiles: new ProfileRepository(database) }),
|
|
7266
8428
|
resourceDownloader: FeishuResourceDownloader.fromConfig(config, secrets),
|
|
7267
8429
|
attachmentVectorIndexer: vectorStore ? (messageId) => indexMessageChunks({
|
|
7268
8430
|
messages: new MessageRepository(database),
|
|
@@ -7352,6 +8514,47 @@ web.command("start").description("\u542F\u52A8\u672C\u5730 Web UI").action(async
|
|
|
7352
8514
|
const config = await loadConfig();
|
|
7353
8515
|
await startWebServer(config, { version: package_default.version });
|
|
7354
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
|
+
});
|
|
7355
8558
|
var data = program.command("data").description("\u7BA1\u7406\u672C\u5730\u77E5\u8BC6\u5E93\u6570\u636E");
|
|
7356
8559
|
async function deleteDataCommand(targetType, targetId, options) {
|
|
7357
8560
|
const shouldDelete = options.yes || await confirm({
|
|
@@ -7482,6 +8685,35 @@ processCommand.command("episodes").description("\u7ACB\u5373\u751F\u6210\u4F1A\u
|
|
|
7482
8685
|
database.close();
|
|
7483
8686
|
}
|
|
7484
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
|
+
});
|
|
7485
8717
|
var files = program.command("files").description("\u7BA1\u7406\u672C\u5730\u6587\u4EF6\u77E5\u8BC6\u6E90");
|
|
7486
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) => {
|
|
7487
8719
|
const config = await loadConfig();
|
|
@@ -7656,7 +8888,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
|
|
|
7656
8888
|
try {
|
|
7657
8889
|
const raw = await fs15.readFile(options.file, "utf8");
|
|
7658
8890
|
const payload = JSON.parse(raw);
|
|
7659
|
-
const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
|
|
8891
|
+
const result = new GatewayIngestor(database, { profiles: new ProfileRepository(database) }).ingestFeishuEvent(payload);
|
|
7660
8892
|
if (!result.accepted) {
|
|
7661
8893
|
console.log(result.reason);
|
|
7662
8894
|
return;
|