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/index.js
CHANGED
|
@@ -916,6 +916,7 @@ function migrateDatabase(database) {
|
|
|
916
916
|
chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
917
917
|
sender_id TEXT NOT NULL,
|
|
918
918
|
sender_name TEXT NOT NULL,
|
|
919
|
+
person_id TEXT REFERENCES persons(id) ON DELETE SET NULL,
|
|
919
920
|
message_type TEXT NOT NULL,
|
|
920
921
|
text TEXT NOT NULL,
|
|
921
922
|
raw_payload_json TEXT NOT NULL,
|
|
@@ -953,6 +954,80 @@ function migrateDatabase(database) {
|
|
|
953
954
|
UNIQUE(chat_id, started_at, ended_at)
|
|
954
955
|
);
|
|
955
956
|
|
|
957
|
+
CREATE TABLE IF NOT EXISTS persons (
|
|
958
|
+
id TEXT PRIMARY KEY,
|
|
959
|
+
primary_name TEXT NOT NULL,
|
|
960
|
+
notes TEXT,
|
|
961
|
+
created_at TEXT NOT NULL,
|
|
962
|
+
updated_at TEXT NOT NULL
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
CREATE TABLE IF NOT EXISTS person_identities (
|
|
966
|
+
id TEXT PRIMARY KEY,
|
|
967
|
+
person_id TEXT NOT NULL REFERENCES persons(id) ON DELETE CASCADE,
|
|
968
|
+
platform TEXT NOT NULL,
|
|
969
|
+
platform_chat_id TEXT NOT NULL,
|
|
970
|
+
external_user_id TEXT NOT NULL,
|
|
971
|
+
external_open_id TEXT,
|
|
972
|
+
external_union_id TEXT,
|
|
973
|
+
external_user_id_raw TEXT,
|
|
974
|
+
display_name TEXT NOT NULL,
|
|
975
|
+
alias TEXT,
|
|
976
|
+
source TEXT NOT NULL CHECK(source IN ('message','feishu_member','manual','inferred')),
|
|
977
|
+
first_seen_at TEXT NOT NULL,
|
|
978
|
+
last_seen_at TEXT NOT NULL,
|
|
979
|
+
UNIQUE(platform, platform_chat_id, external_user_id)
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
CREATE INDEX IF NOT EXISTS person_identities_person_idx ON person_identities(person_id);
|
|
983
|
+
CREATE INDEX IF NOT EXISTS person_identities_lookup_idx ON person_identities(platform, platform_chat_id, external_user_id);
|
|
984
|
+
CREATE INDEX IF NOT EXISTS person_identities_name_idx ON person_identities(display_name, alias);
|
|
985
|
+
|
|
986
|
+
CREATE TABLE IF NOT EXISTS person_profile_entries (
|
|
987
|
+
id TEXT PRIMARY KEY,
|
|
988
|
+
person_id TEXT NOT NULL REFERENCES persons(id) ON DELETE CASCADE,
|
|
989
|
+
category TEXT NOT NULL,
|
|
990
|
+
content TEXT NOT NULL,
|
|
991
|
+
entry_type TEXT NOT NULL CHECK(entry_type IN ('fact','inferred')),
|
|
992
|
+
confidence REAL NOT NULL,
|
|
993
|
+
status TEXT NOT NULL CHECK(status IN ('active','superseded','deleted')),
|
|
994
|
+
source TEXT NOT NULL CHECK(source IN ('dream','explicit_user_request','manual')),
|
|
995
|
+
created_at TEXT NOT NULL,
|
|
996
|
+
updated_at TEXT NOT NULL,
|
|
997
|
+
last_observed_at TEXT NOT NULL
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
CREATE INDEX IF NOT EXISTS person_profile_entries_person_status_idx ON person_profile_entries(person_id, status, updated_at);
|
|
1001
|
+
|
|
1002
|
+
CREATE TABLE IF NOT EXISTS person_profile_evidence (
|
|
1003
|
+
entry_id TEXT NOT NULL REFERENCES person_profile_entries(id) ON DELETE CASCADE,
|
|
1004
|
+
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
1005
|
+
quote TEXT NOT NULL,
|
|
1006
|
+
reason TEXT NOT NULL,
|
|
1007
|
+
PRIMARY KEY (entry_id, message_id, quote)
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
CREATE TABLE IF NOT EXISTS profile_dream_state (
|
|
1011
|
+
platform TEXT NOT NULL,
|
|
1012
|
+
platform_chat_id TEXT NOT NULL,
|
|
1013
|
+
last_message_id TEXT,
|
|
1014
|
+
last_message_sent_at TEXT,
|
|
1015
|
+
updated_at TEXT NOT NULL,
|
|
1016
|
+
PRIMARY KEY (platform, platform_chat_id)
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
CREATE TABLE IF NOT EXISTS profile_dream_runs (
|
|
1020
|
+
id TEXT PRIMARY KEY,
|
|
1021
|
+
platform TEXT NOT NULL,
|
|
1022
|
+
platform_chat_id TEXT NOT NULL,
|
|
1023
|
+
status TEXT NOT NULL CHECK(status IN ('succeeded','failed','skipped')),
|
|
1024
|
+
processed_message_count INTEGER NOT NULL,
|
|
1025
|
+
generated_entry_count INTEGER NOT NULL,
|
|
1026
|
+
error TEXT,
|
|
1027
|
+
started_at TEXT NOT NULL,
|
|
1028
|
+
finished_at TEXT NOT NULL
|
|
1029
|
+
);
|
|
1030
|
+
|
|
956
1031
|
CREATE TABLE IF NOT EXISTS memory_episode_messages (
|
|
957
1032
|
episode_id TEXT NOT NULL REFERENCES memory_episodes(id) ON DELETE CASCADE,
|
|
958
1033
|
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
@@ -1080,6 +1155,11 @@ function migrateDatabase(database) {
|
|
|
1080
1155
|
CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
|
|
1081
1156
|
ON feishu_chat_members(chat_id, user_name);
|
|
1082
1157
|
`);
|
|
1158
|
+
const messageColumns = database.prepare("PRAGMA table_info(messages)").all();
|
|
1159
|
+
if (!messageColumns.some((column) => column.name === "person_id")) {
|
|
1160
|
+
database.prepare("ALTER TABLE messages ADD COLUMN person_id TEXT REFERENCES persons(id) ON DELETE SET NULL").run();
|
|
1161
|
+
}
|
|
1162
|
+
database.prepare("CREATE INDEX IF NOT EXISTS messages_person_idx ON messages(person_id, sent_at)").run();
|
|
1083
1163
|
const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
|
|
1084
1164
|
const ensureCronJobColumn = (name, definition) => {
|
|
1085
1165
|
if (!cronJobColumns.some((column) => column.name === name)) {
|
|
@@ -1789,6 +1869,10 @@ function buildScopeWhere(scope) {
|
|
|
1789
1869
|
clauses.push("c.platform_chat_id = ?");
|
|
1790
1870
|
params.push(scope.platformChatId);
|
|
1791
1871
|
}
|
|
1872
|
+
if (scope?.personId) {
|
|
1873
|
+
clauses.push("m.person_id = ?");
|
|
1874
|
+
params.push(scope.personId);
|
|
1875
|
+
}
|
|
1792
1876
|
return {
|
|
1793
1877
|
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
1794
1878
|
params
|
|
@@ -1833,14 +1917,15 @@ var MessageRepository = class {
|
|
|
1833
1917
|
`
|
|
1834
1918
|
INSERT INTO messages (
|
|
1835
1919
|
id, platform, platform_message_id, chat_id, sender_id, sender_name,
|
|
1836
|
-
message_type, text, raw_payload_json, sent_at, received_at, created_at
|
|
1920
|
+
person_id, message_type, text, raw_payload_json, sent_at, received_at, created_at
|
|
1837
1921
|
)
|
|
1838
1922
|
VALUES (
|
|
1839
1923
|
@id, @platform, @platformMessageId, @chatId, @senderId, @senderName,
|
|
1840
|
-
@messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1924
|
+
@personId, @messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1841
1925
|
)
|
|
1842
1926
|
ON CONFLICT(platform, platform_message_id)
|
|
1843
1927
|
DO UPDATE SET
|
|
1928
|
+
person_id = COALESCE(excluded.person_id, messages.person_id),
|
|
1844
1929
|
message_type = excluded.message_type,
|
|
1845
1930
|
text = excluded.text,
|
|
1846
1931
|
raw_payload_json = excluded.raw_payload_json,
|
|
@@ -1853,6 +1938,7 @@ var MessageRepository = class {
|
|
|
1853
1938
|
chatId,
|
|
1854
1939
|
senderId: input.senderId,
|
|
1855
1940
|
senderName: input.senderName,
|
|
1941
|
+
personId: input.personId ?? null,
|
|
1856
1942
|
messageType: input.messageType,
|
|
1857
1943
|
text: input.text,
|
|
1858
1944
|
rawPayloadJson,
|
|
@@ -1895,6 +1981,7 @@ var MessageRepository = class {
|
|
|
1895
1981
|
m.chat_id AS chatId,
|
|
1896
1982
|
m.sender_id AS senderId,
|
|
1897
1983
|
m.sender_name AS senderName,
|
|
1984
|
+
m.person_id AS personId,
|
|
1898
1985
|
m.sent_at AS sentAt,
|
|
1899
1986
|
c.platform_chat_id AS platformChatId,
|
|
1900
1987
|
c.name AS chatName
|
|
@@ -1917,6 +2004,7 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1917
2004
|
platformMessageId: derivedPlatformMessageId,
|
|
1918
2005
|
senderId: source.senderId,
|
|
1919
2006
|
senderName: source.senderName,
|
|
2007
|
+
personId: source.personId ?? void 0,
|
|
1920
2008
|
messageType: "image_summary",
|
|
1921
2009
|
text: summaryText,
|
|
1922
2010
|
sentAt: source.sentAt,
|
|
@@ -1943,7 +2031,9 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1943
2031
|
1.0 AS score,
|
|
1944
2032
|
m.message_type AS messageType,
|
|
1945
2033
|
c.name AS chatName,
|
|
2034
|
+
m.sender_id AS senderId,
|
|
1946
2035
|
m.sender_name AS senderName,
|
|
2036
|
+
m.person_id AS personId,
|
|
1947
2037
|
m.sent_at AS sentAt
|
|
1948
2038
|
FROM message_chunks mc
|
|
1949
2039
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1964,7 +2054,9 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1964
2054
|
1.0 AS score,
|
|
1965
2055
|
m.message_type AS messageType,
|
|
1966
2056
|
c.name AS chatName,
|
|
2057
|
+
m.sender_id AS senderId,
|
|
1967
2058
|
m.sender_name AS senderName,
|
|
2059
|
+
m.person_id AS personId,
|
|
1968
2060
|
m.sent_at AS sentAt
|
|
1969
2061
|
FROM message_chunks mc
|
|
1970
2062
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1988,7 +2080,9 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1988
2080
|
1.0 AS score,
|
|
1989
2081
|
m.message_type AS messageType,
|
|
1990
2082
|
c.name AS chatName,
|
|
2083
|
+
m.sender_id AS senderId,
|
|
1991
2084
|
m.sender_name AS senderName,
|
|
2085
|
+
m.person_id AS personId,
|
|
1992
2086
|
m.sent_at AS sentAt
|
|
1993
2087
|
FROM message_chunks mc
|
|
1994
2088
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -2004,7 +2098,7 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
2004
2098
|
const excludedIds = options.excludeMessageIds ?? [];
|
|
2005
2099
|
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
2006
2100
|
const scope = buildScopeWhere(options.scope);
|
|
2007
|
-
const
|
|
2101
|
+
const ftsRows = this.database.prepare(
|
|
2008
2102
|
`
|
|
2009
2103
|
SELECT
|
|
2010
2104
|
fts.chunk_id AS chunkId,
|
|
@@ -2014,8 +2108,11 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
2014
2108
|
bm25(message_chunks_fts) * -1 AS score,
|
|
2015
2109
|
m.message_type AS messageType,
|
|
2016
2110
|
c.name AS chatName,
|
|
2111
|
+
m.sender_id AS senderId,
|
|
2017
2112
|
m.sender_name AS senderName,
|
|
2018
|
-
m.
|
|
2113
|
+
m.person_id AS personId,
|
|
2114
|
+
m.sent_at AS sentAt,
|
|
2115
|
+
mc.chunk_index AS chunkIndex
|
|
2019
2116
|
FROM message_chunks_fts fts
|
|
2020
2117
|
JOIN message_chunks mc ON mc.id = fts.chunk_id
|
|
2021
2118
|
JOIN messages m ON m.id = fts.message_id
|
|
@@ -2023,10 +2120,23 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
2023
2120
|
WHERE message_chunks_fts MATCH ?
|
|
2024
2121
|
${excludedWhere}
|
|
2025
2122
|
${scope.where}
|
|
2026
|
-
ORDER BY bm25(message_chunks_fts)
|
|
2123
|
+
ORDER BY bm25(message_chunks_fts), m.sent_at DESC, mc.chunk_index ASC
|
|
2027
2124
|
LIMIT ?
|
|
2028
2125
|
`
|
|
2029
|
-
).all(ftsQuery, ...excludedIds, ...scope.params, limit);
|
|
2126
|
+
).all(ftsQuery, ...excludedIds, ...scope.params, Math.max(limit * 8, limit));
|
|
2127
|
+
const ftsResults = [];
|
|
2128
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
2129
|
+
for (const row of ftsRows) {
|
|
2130
|
+
if (seenMessageIds.has(row.messageId)) {
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
seenMessageIds.add(row.messageId);
|
|
2134
|
+
const { chunkIndex: _chunkIndex, ...result } = row;
|
|
2135
|
+
ftsResults.push(result);
|
|
2136
|
+
if (ftsResults.length >= limit) {
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2030
2140
|
if (ftsResults.length > 0) {
|
|
2031
2141
|
return ftsResults;
|
|
2032
2142
|
}
|
|
@@ -2040,22 +2150,30 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
2040
2150
|
return this.database.prepare(
|
|
2041
2151
|
`
|
|
2042
2152
|
SELECT
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2153
|
+
*
|
|
2154
|
+
FROM (
|
|
2155
|
+
SELECT
|
|
2156
|
+
mc.id AS chunkId,
|
|
2157
|
+
m.id AS messageId,
|
|
2158
|
+
m.platform AS platform,
|
|
2159
|
+
mc.text AS text,
|
|
2160
|
+
0.1 AS score,
|
|
2161
|
+
m.message_type AS messageType,
|
|
2162
|
+
c.name AS chatName,
|
|
2163
|
+
m.sender_id AS senderId,
|
|
2164
|
+
m.sender_name AS senderName,
|
|
2165
|
+
m.person_id AS personId,
|
|
2166
|
+
m.sent_at AS sentAt,
|
|
2167
|
+
ROW_NUMBER() OVER (PARTITION BY m.id ORDER BY mc.chunk_index ASC) AS rowNumber
|
|
2168
|
+
FROM message_chunks mc
|
|
2169
|
+
JOIN messages m ON m.id = mc.message_id
|
|
2170
|
+
JOIN chats c ON c.id = m.chat_id
|
|
2171
|
+
WHERE (${where})
|
|
2172
|
+
${likeExcludedWhere}
|
|
2173
|
+
${scope.where}
|
|
2174
|
+
) ranked
|
|
2175
|
+
WHERE rowNumber = 1
|
|
2176
|
+
ORDER BY sentAt DESC
|
|
2059
2177
|
LIMIT ?
|
|
2060
2178
|
`
|
|
2061
2179
|
).all(...params, ...excludedIds, ...scope.params, limit);
|
|
@@ -2470,19 +2588,20 @@ var HybridRetriever = class {
|
|
|
2470
2588
|
|
|
2471
2589
|
// src/rag/message-retriever.ts
|
|
2472
2590
|
function toEvidenceSource(result) {
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
label: result.senderName,
|
|
2477
|
-
timestamp: result.sentAt
|
|
2478
|
-
};
|
|
2479
|
-
}
|
|
2480
|
-
return {
|
|
2481
|
-
type: "message",
|
|
2482
|
-
label: result.chatName,
|
|
2483
|
-
sender: result.senderName,
|
|
2591
|
+
const source = {
|
|
2592
|
+
type: result.messageType === "file" ? "file" : "message",
|
|
2593
|
+
label: result.messageType === "file" ? result.senderName : result.chatName,
|
|
2484
2594
|
timestamp: result.sentAt
|
|
2485
2595
|
};
|
|
2596
|
+
if (result.messageType !== "file") {
|
|
2597
|
+
source.sender = result.senderName;
|
|
2598
|
+
}
|
|
2599
|
+
source.senderId = result.senderId;
|
|
2600
|
+
source.profileAvailable = Boolean(result.personId);
|
|
2601
|
+
if (result.personId) {
|
|
2602
|
+
source.personId = result.personId;
|
|
2603
|
+
}
|
|
2604
|
+
return source;
|
|
2486
2605
|
}
|
|
2487
2606
|
var MessageFtsRetriever = class {
|
|
2488
2607
|
constructor(messages, options = {}) {
|
|
@@ -2611,6 +2730,9 @@ function toEvidenceSource2(row) {
|
|
|
2611
2730
|
type: "message",
|
|
2612
2731
|
label: row.chatName,
|
|
2613
2732
|
sender: row.senderName,
|
|
2733
|
+
senderId: row.senderId,
|
|
2734
|
+
personId: row.personId,
|
|
2735
|
+
profileAvailable: Boolean(row.personId),
|
|
2614
2736
|
timestamp: row.sentAt
|
|
2615
2737
|
};
|
|
2616
2738
|
}
|
|
@@ -2625,6 +2747,10 @@ function buildScopeWhere3(scope) {
|
|
|
2625
2747
|
clauses.push("c.platform_chat_id = ?");
|
|
2626
2748
|
params.push(scope.platformChatId);
|
|
2627
2749
|
}
|
|
2750
|
+
if (scope?.personId) {
|
|
2751
|
+
clauses.push("m.person_id = ?");
|
|
2752
|
+
params.push(scope.personId);
|
|
2753
|
+
}
|
|
2628
2754
|
return {
|
|
2629
2755
|
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
2630
2756
|
params
|
|
@@ -2675,7 +2801,9 @@ var SqliteVectorStore = class {
|
|
|
2675
2801
|
mc.id AS chunkId,
|
|
2676
2802
|
mc.text AS text,
|
|
2677
2803
|
c.name AS chatName,
|
|
2804
|
+
m.sender_id AS senderId,
|
|
2678
2805
|
m.sender_name AS senderName,
|
|
2806
|
+
m.person_id AS personId,
|
|
2679
2807
|
m.sent_at AS sentAt,
|
|
2680
2808
|
e.embedding_json AS embeddingJson
|
|
2681
2809
|
FROM message_chunk_embeddings e
|
|
@@ -2756,8 +2884,12 @@ async function createAgenticRagSearchTools(input) {
|
|
|
2756
2884
|
new SqliteVectorStore(input.database, { model: input.config.embedding.model })
|
|
2757
2885
|
) : void 0;
|
|
2758
2886
|
const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
|
|
2887
|
+
const tools = createRagSearchTools({ hybrid, messages, episodes, semantic, scope: input.scope });
|
|
2888
|
+
if (input.profileTools && input.profileTools.length > 0) {
|
|
2889
|
+
tools.push(...input.profileTools);
|
|
2890
|
+
}
|
|
2759
2891
|
return {
|
|
2760
|
-
tools
|
|
2892
|
+
tools,
|
|
2761
2893
|
close: () => {
|
|
2762
2894
|
}
|
|
2763
2895
|
};
|
|
@@ -3365,6 +3497,588 @@ function parseExactNumber2(field, min, max) {
|
|
|
3365
3497
|
return value;
|
|
3366
3498
|
}
|
|
3367
3499
|
|
|
3500
|
+
// src/profiles/rag-tools.ts
|
|
3501
|
+
var getProfileInputSchema = {
|
|
3502
|
+
type: "object",
|
|
3503
|
+
properties: {
|
|
3504
|
+
personId: { type: "string", description: "Stable person identifier from retrieved evidence." },
|
|
3505
|
+
senderId: { type: "string", description: "Message sender id when personId is unavailable." },
|
|
3506
|
+
platformChatId: { type: "string", description: "Chat id paired with senderId for profile lookup." },
|
|
3507
|
+
includeEvidence: { type: "boolean", description: "Whether to include evidence snippets in the profile text." },
|
|
3508
|
+
includeInferred: { type: "boolean", description: "Whether to include inferred profile entries." }
|
|
3509
|
+
},
|
|
3510
|
+
additionalProperties: false
|
|
3511
|
+
};
|
|
3512
|
+
var searchMessagesInputSchema = {
|
|
3513
|
+
type: "object",
|
|
3514
|
+
properties: {
|
|
3515
|
+
personId: { type: "string", description: "The stable person identifier whose messages to search." },
|
|
3516
|
+
query: { type: "string", description: "Search query written by the model." },
|
|
3517
|
+
limit: { type: "number", description: "Maximum number of evidence blocks to return." }
|
|
3518
|
+
},
|
|
3519
|
+
required: ["personId", "query"],
|
|
3520
|
+
additionalProperties: false
|
|
3521
|
+
};
|
|
3522
|
+
function readString2(value) {
|
|
3523
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
3524
|
+
}
|
|
3525
|
+
function resolvePersonId(profiles, input) {
|
|
3526
|
+
const raw = typeof input === "object" && input !== null ? input : {};
|
|
3527
|
+
const personId = readString2(raw.personId);
|
|
3528
|
+
if (personId) return personId;
|
|
3529
|
+
const senderId = readString2(raw.senderId);
|
|
3530
|
+
const platformChatId = readString2(raw.platformChatId);
|
|
3531
|
+
if (senderId && platformChatId) {
|
|
3532
|
+
const resolved = profiles.resolvePersonIdForSender({ senderId, platformChatId });
|
|
3533
|
+
if (resolved) return resolved;
|
|
3534
|
+
}
|
|
3535
|
+
throw new Error("personId \u6216 senderId + platformChatId \u5FC5\u987B\u63D0\u4F9B\u3002");
|
|
3536
|
+
}
|
|
3537
|
+
function parseBoolean(input, key, defaultValue) {
|
|
3538
|
+
const value = typeof input === "object" && input !== null ? input[key] : void 0;
|
|
3539
|
+
return typeof value === "boolean" ? value : defaultValue;
|
|
3540
|
+
}
|
|
3541
|
+
function parseQuery(input) {
|
|
3542
|
+
const rawQuery = typeof input === "object" && input !== null && "query" in input ? input.query : void 0;
|
|
3543
|
+
if (typeof rawQuery !== "string") {
|
|
3544
|
+
throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
|
|
3545
|
+
}
|
|
3546
|
+
const query = rawQuery.trim();
|
|
3547
|
+
if (!query) {
|
|
3548
|
+
throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
|
|
3549
|
+
}
|
|
3550
|
+
return query;
|
|
3551
|
+
}
|
|
3552
|
+
function parseLimit(input) {
|
|
3553
|
+
const rawLimit = typeof input === "object" && input !== null && "limit" in input ? input.limit : void 0;
|
|
3554
|
+
const numericLimit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? rawLimit : 5;
|
|
3555
|
+
return Math.min(12, Math.max(1, Math.floor(numericLimit)));
|
|
3556
|
+
}
|
|
3557
|
+
function createGetPersonProfileTool(profiles) {
|
|
3558
|
+
return {
|
|
3559
|
+
name: "get_person_profile",
|
|
3560
|
+
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.",
|
|
3561
|
+
inputSchema: getProfileInputSchema,
|
|
3562
|
+
execute: async (input) => {
|
|
3563
|
+
const personId = resolvePersonId(profiles, input);
|
|
3564
|
+
const includeEvidence = parseBoolean(input, "includeEvidence", false);
|
|
3565
|
+
const includeInferred = parseBoolean(input, "includeInferred", true);
|
|
3566
|
+
const profile = profiles.getPersonProfile(personId, { includeEvidence, includeInferred });
|
|
3567
|
+
if (!profile) {
|
|
3568
|
+
return [];
|
|
3569
|
+
}
|
|
3570
|
+
const aliases = profile.identities.map((identity) => identity.displayName).filter(Boolean).join("\u3001");
|
|
3571
|
+
const entries = profile.entries.map((entry) => {
|
|
3572
|
+
const evidence = includeEvidence && entry.evidence?.length ? `
|
|
3573
|
+
\u8BC1\u636E\uFF1A${entry.evidence.map((item) => `${item.quote}\uFF08${item.reason}\uFF09`).join("\uFF1B")}` : "";
|
|
3574
|
+
return `- [${entry.entryType}] ${entry.category}\uFF1A${entry.content}\uFF08\u7F6E\u4FE1\u5EA6 ${entry.confidence}\uFF0C\u6765\u6E90 ${entry.source}\uFF09${evidence}`;
|
|
3575
|
+
});
|
|
3576
|
+
return [{
|
|
3577
|
+
id: `person_profile:${profile.person.id}`,
|
|
3578
|
+
text: [`\u4EBA\u7269\uFF1A${profile.person.primaryName}`, aliases ? `\u8EAB\u4EFD/\u6635\u79F0\uFF1A${aliases}` : void 0, ...entries].filter(Boolean).join("\n"),
|
|
3579
|
+
score: 1,
|
|
3580
|
+
source: {
|
|
3581
|
+
type: "person_profile",
|
|
3582
|
+
label: profile.person.primaryName,
|
|
3583
|
+
personId: profile.person.id,
|
|
3584
|
+
profileAvailable: true
|
|
3585
|
+
}
|
|
3586
|
+
}];
|
|
3587
|
+
}
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
function createSearchPersonMessagesTool(profiles) {
|
|
3591
|
+
return {
|
|
3592
|
+
name: "search_person_messages",
|
|
3593
|
+
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.",
|
|
3594
|
+
inputSchema: searchMessagesInputSchema,
|
|
3595
|
+
execute: async (input) => {
|
|
3596
|
+
const personId = resolvePersonId(profiles, input);
|
|
3597
|
+
const query = parseQuery(input);
|
|
3598
|
+
const limit = parseLimit(input);
|
|
3599
|
+
const results = profiles.searchPersonMessages(personId, query, limit);
|
|
3600
|
+
return results.map((result) => ({
|
|
3601
|
+
id: result.chunkId,
|
|
3602
|
+
text: result.text,
|
|
3603
|
+
score: result.score,
|
|
3604
|
+
source: {
|
|
3605
|
+
type: result.messageType === "file" ? "file" : "message",
|
|
3606
|
+
label: result.chatName,
|
|
3607
|
+
sender: result.senderName,
|
|
3608
|
+
senderId: result.senderId,
|
|
3609
|
+
timestamp: result.sentAt,
|
|
3610
|
+
personId: result.personId ?? void 0,
|
|
3611
|
+
profileAvailable: Boolean(result.personId)
|
|
3612
|
+
}
|
|
3613
|
+
}));
|
|
3614
|
+
}
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
function createPersonProfileTools({ profiles }) {
|
|
3618
|
+
return [createGetPersonProfileTool(profiles), createSearchPersonMessagesTool(profiles)];
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// src/profiles/repository.ts
|
|
3622
|
+
import crypto5 from "crypto";
|
|
3623
|
+
function nowIso4() {
|
|
3624
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3625
|
+
}
|
|
3626
|
+
function createId(prefix, parts) {
|
|
3627
|
+
return `${prefix}_${crypto5.createHash("sha256").update(parts.join("")).digest("hex").slice(0, 24)}`;
|
|
3628
|
+
}
|
|
3629
|
+
function mapPerson(row) {
|
|
3630
|
+
return {
|
|
3631
|
+
id: row.id,
|
|
3632
|
+
primaryName: row.primaryName,
|
|
3633
|
+
notes: row.notes ?? void 0,
|
|
3634
|
+
createdAt: row.createdAt,
|
|
3635
|
+
updatedAt: row.updatedAt
|
|
3636
|
+
};
|
|
3637
|
+
}
|
|
3638
|
+
var ProfileRepository = class {
|
|
3639
|
+
constructor(database) {
|
|
3640
|
+
this.database = database;
|
|
3641
|
+
}
|
|
3642
|
+
database;
|
|
3643
|
+
resolvePersonForSender(input) {
|
|
3644
|
+
const observedAt = input.observedAt ?? nowIso4();
|
|
3645
|
+
const personId = createId("person", [input.platform, input.platformChatId, input.senderId]);
|
|
3646
|
+
const identityId = createId("identity", [input.platform, input.platformChatId, input.senderId]);
|
|
3647
|
+
const findPerson = this.database.prepare(
|
|
3648
|
+
`
|
|
3649
|
+
SELECT id, primary_name AS primaryName, notes, created_at AS createdAt, updated_at AS updatedAt
|
|
3650
|
+
FROM persons
|
|
3651
|
+
WHERE id = ?
|
|
3652
|
+
`
|
|
3653
|
+
);
|
|
3654
|
+
const transaction = this.database.transaction(() => {
|
|
3655
|
+
this.database.prepare(
|
|
3656
|
+
`
|
|
3657
|
+
INSERT INTO persons (id, primary_name, notes, created_at, updated_at)
|
|
3658
|
+
VALUES (?, ?, NULL, ?, ?)
|
|
3659
|
+
ON CONFLICT(id) DO NOTHING
|
|
3660
|
+
`
|
|
3661
|
+
).run(personId, input.senderName, observedAt, observedAt);
|
|
3662
|
+
this.database.prepare(
|
|
3663
|
+
`
|
|
3664
|
+
INSERT INTO person_identities (
|
|
3665
|
+
id, person_id, platform, platform_chat_id, external_user_id, external_open_id,
|
|
3666
|
+
external_union_id, external_user_id_raw, display_name, alias, source, first_seen_at, last_seen_at
|
|
3667
|
+
) VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, ?, NULL, ?, ?, ?)
|
|
3668
|
+
ON CONFLICT(platform, platform_chat_id, external_user_id)
|
|
3669
|
+
DO UPDATE SET
|
|
3670
|
+
display_name = excluded.display_name,
|
|
3671
|
+
source = excluded.source,
|
|
3672
|
+
last_seen_at = excluded.last_seen_at
|
|
3673
|
+
`
|
|
3674
|
+
).run(identityId, personId, input.platform, input.platformChatId, input.senderId, input.senderId, input.senderName, input.source, observedAt, observedAt);
|
|
3675
|
+
this.database.prepare(
|
|
3676
|
+
`
|
|
3677
|
+
UPDATE persons
|
|
3678
|
+
SET primary_name = ?, updated_at = ?
|
|
3679
|
+
WHERE id = ?
|
|
3680
|
+
`
|
|
3681
|
+
).run(input.senderName, observedAt, personId);
|
|
3682
|
+
return findPerson.get(personId);
|
|
3683
|
+
});
|
|
3684
|
+
return transaction();
|
|
3685
|
+
}
|
|
3686
|
+
listPersons() {
|
|
3687
|
+
const rows = this.database.prepare(
|
|
3688
|
+
`
|
|
3689
|
+
SELECT id, primary_name AS primaryName, notes, created_at AS createdAt, updated_at AS updatedAt
|
|
3690
|
+
FROM persons
|
|
3691
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
3692
|
+
`
|
|
3693
|
+
).all();
|
|
3694
|
+
return rows.map(mapPerson);
|
|
3695
|
+
}
|
|
3696
|
+
getPersonProfile(personId, options = {}) {
|
|
3697
|
+
const personRow = 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
|
+
).get(personId);
|
|
3704
|
+
if (!personRow) {
|
|
3705
|
+
return void 0;
|
|
3706
|
+
}
|
|
3707
|
+
const includeInferred = options.includeInferred ?? true;
|
|
3708
|
+
const entryRows = this.database.prepare(
|
|
3709
|
+
`
|
|
3710
|
+
SELECT
|
|
3711
|
+
id,
|
|
3712
|
+
person_id AS personId,
|
|
3713
|
+
category,
|
|
3714
|
+
content,
|
|
3715
|
+
entry_type AS entryType,
|
|
3716
|
+
confidence,
|
|
3717
|
+
status,
|
|
3718
|
+
source,
|
|
3719
|
+
created_at AS createdAt,
|
|
3720
|
+
updated_at AS updatedAt,
|
|
3721
|
+
last_observed_at AS lastObservedAt
|
|
3722
|
+
FROM person_profile_entries
|
|
3723
|
+
WHERE person_id = ?
|
|
3724
|
+
AND status = 'active'
|
|
3725
|
+
${includeInferred ? "" : "AND entry_type = 'fact'"}
|
|
3726
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
3727
|
+
`
|
|
3728
|
+
).all(personId);
|
|
3729
|
+
const entries = options.includeEvidence ? entryRows.map((entry) => ({ ...entry, evidence: this.getEvidence(entry.id) })) : entryRows;
|
|
3730
|
+
const identities = this.database.prepare(
|
|
3731
|
+
`
|
|
3732
|
+
SELECT
|
|
3733
|
+
platform,
|
|
3734
|
+
platform_chat_id AS platformChatId,
|
|
3735
|
+
external_user_id AS externalUserId,
|
|
3736
|
+
display_name AS displayName,
|
|
3737
|
+
alias,
|
|
3738
|
+
source,
|
|
3739
|
+
first_seen_at AS firstSeenAt,
|
|
3740
|
+
last_seen_at AS lastSeenAt
|
|
3741
|
+
FROM person_identities
|
|
3742
|
+
WHERE person_id = ?
|
|
3743
|
+
ORDER BY last_seen_at DESC, first_seen_at DESC
|
|
3744
|
+
`
|
|
3745
|
+
).all(personId);
|
|
3746
|
+
return {
|
|
3747
|
+
person: mapPerson(personRow),
|
|
3748
|
+
identities,
|
|
3749
|
+
entries
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
upsertProfileEntry(input) {
|
|
3753
|
+
if (input.evidence.length === 0) {
|
|
3754
|
+
throw new Error("Profile entry evidence is required.");
|
|
3755
|
+
}
|
|
3756
|
+
const timestamp = input.observedAt ?? nowIso4();
|
|
3757
|
+
const entryId = createId("profile_entry", [input.personId, input.category, input.content]);
|
|
3758
|
+
const transaction = this.database.transaction(() => {
|
|
3759
|
+
this.database.prepare(
|
|
3760
|
+
`
|
|
3761
|
+
INSERT INTO person_profile_entries (
|
|
3762
|
+
id, person_id, category, content, entry_type, confidence, status, source, created_at, updated_at, last_observed_at
|
|
3763
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
|
|
3764
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3765
|
+
confidence = MAX(person_profile_entries.confidence, excluded.confidence),
|
|
3766
|
+
status = 'active',
|
|
3767
|
+
source = excluded.source,
|
|
3768
|
+
updated_at = excluded.updated_at,
|
|
3769
|
+
last_observed_at = excluded.last_observed_at
|
|
3770
|
+
`
|
|
3771
|
+
).run(entryId, input.personId, input.category, input.content, input.entryType, input.confidence, input.source, timestamp, timestamp, timestamp);
|
|
3772
|
+
const insertEvidence = this.database.prepare(
|
|
3773
|
+
`
|
|
3774
|
+
INSERT INTO person_profile_evidence (entry_id, message_id, quote, reason)
|
|
3775
|
+
VALUES (?, ?, ?, ?)
|
|
3776
|
+
ON CONFLICT(entry_id, message_id, quote) DO UPDATE SET reason = excluded.reason
|
|
3777
|
+
`
|
|
3778
|
+
);
|
|
3779
|
+
for (const evidence of input.evidence) {
|
|
3780
|
+
insertEvidence.run(entryId, evidence.messageId, evidence.quote, evidence.reason);
|
|
3781
|
+
}
|
|
3782
|
+
});
|
|
3783
|
+
transaction();
|
|
3784
|
+
return entryId;
|
|
3785
|
+
}
|
|
3786
|
+
backfillMessagePersons({ limit }) {
|
|
3787
|
+
const rows = this.database.prepare(
|
|
3788
|
+
`
|
|
3789
|
+
SELECT
|
|
3790
|
+
m.id AS id,
|
|
3791
|
+
m.platform AS platform,
|
|
3792
|
+
c.platform_chat_id AS platformChatId,
|
|
3793
|
+
m.sender_id AS senderId,
|
|
3794
|
+
m.sender_name AS senderName,
|
|
3795
|
+
m.sent_at AS sentAt
|
|
3796
|
+
FROM messages m
|
|
3797
|
+
JOIN chats c ON c.id = m.chat_id
|
|
3798
|
+
WHERE m.person_id IS NULL
|
|
3799
|
+
ORDER BY m.sent_at ASC
|
|
3800
|
+
LIMIT ?
|
|
3801
|
+
`
|
|
3802
|
+
).all(limit);
|
|
3803
|
+
const update = this.database.prepare("UPDATE messages SET person_id = ? WHERE id = ?");
|
|
3804
|
+
const transaction = this.database.transaction(() => {
|
|
3805
|
+
for (const row of rows) {
|
|
3806
|
+
const person = this.resolvePersonForSender({
|
|
3807
|
+
platform: row.platform,
|
|
3808
|
+
platformChatId: row.platformChatId,
|
|
3809
|
+
senderId: row.senderId,
|
|
3810
|
+
senderName: row.senderName,
|
|
3811
|
+
source: "inferred",
|
|
3812
|
+
observedAt: row.sentAt
|
|
3813
|
+
});
|
|
3814
|
+
update.run(person.id, row.id);
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
transaction();
|
|
3818
|
+
return { updatedMessages: rows.length };
|
|
3819
|
+
}
|
|
3820
|
+
getDreamState(platform, platformChatId) {
|
|
3821
|
+
return this.database.prepare(
|
|
3822
|
+
`
|
|
3823
|
+
SELECT
|
|
3824
|
+
platform,
|
|
3825
|
+
platform_chat_id AS platformChatId,
|
|
3826
|
+
last_message_id AS lastMessageId,
|
|
3827
|
+
last_message_sent_at AS lastMessageSentAt,
|
|
3828
|
+
updated_at AS updatedAt
|
|
3829
|
+
FROM profile_dream_state
|
|
3830
|
+
WHERE platform = ? AND platform_chat_id = ?
|
|
3831
|
+
`
|
|
3832
|
+
).get(platform, platformChatId);
|
|
3833
|
+
}
|
|
3834
|
+
updateDreamState(input) {
|
|
3835
|
+
this.database.prepare(
|
|
3836
|
+
`
|
|
3837
|
+
INSERT INTO profile_dream_state (platform, platform_chat_id, last_message_id, last_message_sent_at, updated_at)
|
|
3838
|
+
VALUES (?, ?, ?, ?, ?)
|
|
3839
|
+
ON CONFLICT(platform, platform_chat_id)
|
|
3840
|
+
DO UPDATE SET
|
|
3841
|
+
last_message_id = excluded.last_message_id,
|
|
3842
|
+
last_message_sent_at = excluded.last_message_sent_at,
|
|
3843
|
+
updated_at = excluded.updated_at
|
|
3844
|
+
`
|
|
3845
|
+
).run(input.platform, input.platformChatId, input.lastMessageId ?? null, input.lastMessageSentAt ?? null, input.updatedAt);
|
|
3846
|
+
}
|
|
3847
|
+
listMessagesForDream(input) {
|
|
3848
|
+
const afterWhere = input.afterSentAt ? "AND m.sent_at > ?" : "";
|
|
3849
|
+
const params = input.afterSentAt ? [input.platform, input.platformChatId, input.afterSentAt, input.limit] : [input.platform, input.platformChatId, input.limit];
|
|
3850
|
+
return this.database.prepare(
|
|
3851
|
+
`
|
|
3852
|
+
SELECT
|
|
3853
|
+
m.id AS messageId,
|
|
3854
|
+
m.person_id AS personId,
|
|
3855
|
+
m.sender_name AS senderName,
|
|
3856
|
+
m.sent_at AS sentAt,
|
|
3857
|
+
m.text AS text
|
|
3858
|
+
FROM messages m
|
|
3859
|
+
JOIN chats c ON c.id = m.chat_id
|
|
3860
|
+
WHERE m.platform = ?
|
|
3861
|
+
AND c.platform_chat_id = ?
|
|
3862
|
+
AND m.person_id IS NOT NULL
|
|
3863
|
+
${afterWhere}
|
|
3864
|
+
ORDER BY m.sent_at ASC, m.created_at ASC
|
|
3865
|
+
LIMIT ?
|
|
3866
|
+
`
|
|
3867
|
+
).all(...params);
|
|
3868
|
+
}
|
|
3869
|
+
listChatsWithPendingDreamMessages() {
|
|
3870
|
+
return this.database.prepare(
|
|
3871
|
+
`
|
|
3872
|
+
SELECT DISTINCT m.platform AS platform, c.platform_chat_id AS platformChatId
|
|
3873
|
+
FROM messages m
|
|
3874
|
+
JOIN chats c ON c.id = m.chat_id
|
|
3875
|
+
LEFT JOIN profile_dream_state pds ON pds.platform = m.platform AND pds.platform_chat_id = c.platform_chat_id
|
|
3876
|
+
WHERE m.person_id IS NOT NULL
|
|
3877
|
+
AND (pds.last_message_sent_at IS NULL OR m.sent_at > pds.last_message_sent_at)
|
|
3878
|
+
ORDER BY c.platform_chat_id ASC
|
|
3879
|
+
`
|
|
3880
|
+
).all();
|
|
3881
|
+
}
|
|
3882
|
+
recordDreamRun(input) {
|
|
3883
|
+
const id = input.id ?? createId("profile_dream_run", [input.platform, input.platformChatId, input.status, input.startedAt, input.finishedAt, crypto5.randomUUID()]);
|
|
3884
|
+
this.database.prepare(
|
|
3885
|
+
`
|
|
3886
|
+
INSERT INTO profile_dream_runs (
|
|
3887
|
+
id, platform, platform_chat_id, status, processed_message_count, generated_entry_count, error, started_at, finished_at
|
|
3888
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3889
|
+
`
|
|
3890
|
+
).run(
|
|
3891
|
+
id,
|
|
3892
|
+
input.platform,
|
|
3893
|
+
input.platformChatId,
|
|
3894
|
+
input.status,
|
|
3895
|
+
input.processedMessageCount,
|
|
3896
|
+
input.generatedEntryCount,
|
|
3897
|
+
input.error ?? null,
|
|
3898
|
+
input.startedAt,
|
|
3899
|
+
input.finishedAt
|
|
3900
|
+
);
|
|
3901
|
+
return id;
|
|
3902
|
+
}
|
|
3903
|
+
resolvePersonIdForSender(input) {
|
|
3904
|
+
const platform = input.platform ?? "feishu";
|
|
3905
|
+
const row = this.database.prepare(
|
|
3906
|
+
`
|
|
3907
|
+
SELECT person_id AS personId
|
|
3908
|
+
FROM person_identities
|
|
3909
|
+
WHERE platform = ? AND platform_chat_id = ? AND external_user_id = ?
|
|
3910
|
+
LIMIT 1
|
|
3911
|
+
`
|
|
3912
|
+
).get(platform, input.platformChatId, input.senderId);
|
|
3913
|
+
return row?.personId;
|
|
3914
|
+
}
|
|
3915
|
+
searchPersonMessages(personId, query, limit, options = {}) {
|
|
3916
|
+
const cleaned = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter(Boolean);
|
|
3917
|
+
const wrapped = cleaned.map((term) => `"${term}"`).join(" ");
|
|
3918
|
+
if (!wrapped) {
|
|
3919
|
+
return [];
|
|
3920
|
+
}
|
|
3921
|
+
const excludedIds = options.excludeMessageIds ?? [];
|
|
3922
|
+
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
3923
|
+
const rows = this.database.prepare(
|
|
3924
|
+
`
|
|
3925
|
+
SELECT
|
|
3926
|
+
fts.chunk_id AS chunkId,
|
|
3927
|
+
fts.message_id AS messageId,
|
|
3928
|
+
m.platform AS platform,
|
|
3929
|
+
mc.text AS text,
|
|
3930
|
+
bm25(message_chunks_fts) * -1 AS score,
|
|
3931
|
+
m.message_type AS messageType,
|
|
3932
|
+
c.name AS chatName,
|
|
3933
|
+
m.sender_id AS senderId,
|
|
3934
|
+
m.sender_name AS senderName,
|
|
3935
|
+
m.person_id AS personId,
|
|
3936
|
+
m.sent_at AS sentAt,
|
|
3937
|
+
mc.chunk_index AS chunkIndex
|
|
3938
|
+
FROM message_chunks_fts fts
|
|
3939
|
+
JOIN message_chunks mc ON mc.id = fts.chunk_id
|
|
3940
|
+
JOIN messages m ON m.id = fts.message_id
|
|
3941
|
+
JOIN chats c ON c.id = m.chat_id
|
|
3942
|
+
WHERE message_chunks_fts MATCH ?
|
|
3943
|
+
${excludedWhere}
|
|
3944
|
+
AND m.person_id = ?
|
|
3945
|
+
ORDER BY bm25(message_chunks_fts), m.sent_at DESC, mc.chunk_index ASC
|
|
3946
|
+
LIMIT ?
|
|
3947
|
+
`
|
|
3948
|
+
).all(wrapped, ...excludedIds, personId, Math.max(limit * 8, limit));
|
|
3949
|
+
const results = [];
|
|
3950
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
3951
|
+
for (const row of rows) {
|
|
3952
|
+
if (seenMessageIds.has(row.messageId)) {
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
seenMessageIds.add(row.messageId);
|
|
3956
|
+
const { chunkIndex: _chunkIndex, ...result } = row;
|
|
3957
|
+
results.push(result);
|
|
3958
|
+
if (results.length >= limit) {
|
|
3959
|
+
break;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
if (results.length > 0) {
|
|
3963
|
+
return results;
|
|
3964
|
+
}
|
|
3965
|
+
const terms = query.split(/[ ]+/).map((term) => term.trim()).filter((term) => term.length > 0);
|
|
3966
|
+
if (terms.length === 0) {
|
|
3967
|
+
return [];
|
|
3968
|
+
}
|
|
3969
|
+
const where = terms.map(() => "mc.text LIKE ? ESCAPE '\\'").join(" OR ");
|
|
3970
|
+
const params = terms.map((term) => `%${term.replace(/[%_]/g, "\\$&")}%`);
|
|
3971
|
+
const likeExcludedWhere = excludedIds.length > 0 ? `AND m.id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
3972
|
+
return this.database.prepare(
|
|
3973
|
+
`
|
|
3974
|
+
SELECT
|
|
3975
|
+
*
|
|
3976
|
+
FROM (
|
|
3977
|
+
SELECT
|
|
3978
|
+
mc.id AS chunkId,
|
|
3979
|
+
m.id AS messageId,
|
|
3980
|
+
m.platform AS platform,
|
|
3981
|
+
mc.text AS text,
|
|
3982
|
+
0.1 AS score,
|
|
3983
|
+
m.message_type AS messageType,
|
|
3984
|
+
c.name AS chatName,
|
|
3985
|
+
m.sender_id AS senderId,
|
|
3986
|
+
m.sender_name AS senderName,
|
|
3987
|
+
m.person_id AS personId,
|
|
3988
|
+
m.sent_at AS sentAt,
|
|
3989
|
+
ROW_NUMBER() OVER (PARTITION BY m.id ORDER BY mc.chunk_index ASC) AS rowNumber
|
|
3990
|
+
FROM message_chunks mc
|
|
3991
|
+
JOIN messages m ON m.id = mc.message_id
|
|
3992
|
+
JOIN chats c ON c.id = m.chat_id
|
|
3993
|
+
WHERE (${where})
|
|
3994
|
+
${likeExcludedWhere}
|
|
3995
|
+
AND m.person_id = ?
|
|
3996
|
+
) ranked
|
|
3997
|
+
WHERE rowNumber = 1
|
|
3998
|
+
ORDER BY sentAt DESC
|
|
3999
|
+
LIMIT ?
|
|
4000
|
+
`
|
|
4001
|
+
).all(...params, ...excludedIds, personId, limit);
|
|
4002
|
+
}
|
|
4003
|
+
getProfileEntry(entryId) {
|
|
4004
|
+
const row = this.database.prepare(
|
|
4005
|
+
`
|
|
4006
|
+
SELECT
|
|
4007
|
+
id,
|
|
4008
|
+
person_id AS personId,
|
|
4009
|
+
category,
|
|
4010
|
+
content,
|
|
4011
|
+
entry_type AS entryType,
|
|
4012
|
+
confidence,
|
|
4013
|
+
status,
|
|
4014
|
+
source,
|
|
4015
|
+
created_at AS createdAt,
|
|
4016
|
+
updated_at AS updatedAt,
|
|
4017
|
+
last_observed_at AS lastObservedAt
|
|
4018
|
+
FROM person_profile_entries
|
|
4019
|
+
WHERE id = ?
|
|
4020
|
+
`
|
|
4021
|
+
).get(entryId);
|
|
4022
|
+
if (!row) {
|
|
4023
|
+
return void 0;
|
|
4024
|
+
}
|
|
4025
|
+
return { ...row, evidence: this.getEvidence(entryId) };
|
|
4026
|
+
}
|
|
4027
|
+
replaceProfileEntry(input) {
|
|
4028
|
+
if (input.input.evidence.length === 0) {
|
|
4029
|
+
throw new Error("Profile entry evidence is required.");
|
|
4030
|
+
}
|
|
4031
|
+
const timestamp = input.input.observedAt ?? nowIso4();
|
|
4032
|
+
const newEntryId = createId("profile_entry", [input.input.personId, input.input.category, input.input.content]);
|
|
4033
|
+
const transaction = this.database.transaction(() => {
|
|
4034
|
+
this.database.prepare(
|
|
4035
|
+
`
|
|
4036
|
+
INSERT INTO person_profile_entries (
|
|
4037
|
+
id, person_id, category, content, entry_type, confidence, status, source, created_at, updated_at, last_observed_at
|
|
4038
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
|
|
4039
|
+
`
|
|
4040
|
+
).run(newEntryId, input.input.personId, input.input.category, input.input.content, input.input.entryType, input.input.confidence, input.input.source, timestamp, timestamp, timestamp);
|
|
4041
|
+
this.database.prepare(
|
|
4042
|
+
`
|
|
4043
|
+
UPDATE person_profile_entries
|
|
4044
|
+
SET status = 'superseded', updated_at = ?
|
|
4045
|
+
WHERE id = ? AND status = 'active'
|
|
4046
|
+
`
|
|
4047
|
+
).run(timestamp, input.supersedeEntryId);
|
|
4048
|
+
const insertEvidence = this.database.prepare(
|
|
4049
|
+
`
|
|
4050
|
+
INSERT INTO person_profile_evidence (entry_id, message_id, quote, reason)
|
|
4051
|
+
VALUES (?, ?, ?, ?)
|
|
4052
|
+
ON CONFLICT(entry_id, message_id, quote) DO UPDATE SET reason = excluded.reason
|
|
4053
|
+
`
|
|
4054
|
+
);
|
|
4055
|
+
for (const evidence of input.input.evidence) {
|
|
4056
|
+
insertEvidence.run(newEntryId, evidence.messageId, evidence.quote, evidence.reason);
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
transaction();
|
|
4060
|
+
return newEntryId;
|
|
4061
|
+
}
|
|
4062
|
+
markProfileEntryDeleted(entryId) {
|
|
4063
|
+
const timestamp = nowIso4();
|
|
4064
|
+
this.database.prepare("UPDATE person_profile_entries SET status = 'deleted', updated_at = ? WHERE id = ?").run(timestamp, entryId);
|
|
4065
|
+
}
|
|
4066
|
+
personExists(personId) {
|
|
4067
|
+
const row = this.database.prepare("SELECT 1 AS existsFlag FROM persons WHERE id = ? LIMIT 1").get(personId);
|
|
4068
|
+
return Boolean(row);
|
|
4069
|
+
}
|
|
4070
|
+
getEvidence(entryId) {
|
|
4071
|
+
return this.database.prepare(
|
|
4072
|
+
`
|
|
4073
|
+
SELECT entry_id AS entryId, message_id AS messageId, quote, reason
|
|
4074
|
+
FROM person_profile_evidence
|
|
4075
|
+
WHERE entry_id = ?
|
|
4076
|
+
ORDER BY message_id ASC, quote ASC
|
|
4077
|
+
`
|
|
4078
|
+
).all(entryId);
|
|
4079
|
+
}
|
|
4080
|
+
};
|
|
4081
|
+
|
|
3368
4082
|
// src/rag/indexer.ts
|
|
3369
4083
|
var EMBEDDING_INDEX_BATCH_SIZE = 64;
|
|
3370
4084
|
async function indexMessageChunks(input) {
|
|
@@ -3452,12 +4166,12 @@ async function processMessagesNow(input) {
|
|
|
3452
4166
|
}
|
|
3453
4167
|
|
|
3454
4168
|
// src/multimodal/tasks.ts
|
|
3455
|
-
import
|
|
3456
|
-
function
|
|
4169
|
+
import crypto6 from "crypto";
|
|
4170
|
+
function nowIso5() {
|
|
3457
4171
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3458
4172
|
}
|
|
3459
4173
|
function stableId3(sourceMessageId, imageKey) {
|
|
3460
|
-
return
|
|
4174
|
+
return crypto6.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
|
|
3461
4175
|
}
|
|
3462
4176
|
function mapRow(row) {
|
|
3463
4177
|
if (!row) {
|
|
@@ -3485,7 +4199,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3485
4199
|
database;
|
|
3486
4200
|
enqueue(input) {
|
|
3487
4201
|
const id = stableId3(input.sourceMessageId, input.imageKey);
|
|
3488
|
-
const timestamp =
|
|
4202
|
+
const timestamp = nowIso5();
|
|
3489
4203
|
this.database.prepare(
|
|
3490
4204
|
`
|
|
3491
4205
|
INSERT INTO image_multimodal_tasks (
|
|
@@ -3573,7 +4287,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3573
4287
|
updated_at = @updatedAt
|
|
3574
4288
|
WHERE id = @id AND status = 'pending'
|
|
3575
4289
|
`
|
|
3576
|
-
).run({ id, updatedAt:
|
|
4290
|
+
).run({ id, updatedAt: nowIso5() });
|
|
3577
4291
|
if (result.changes === 0) {
|
|
3578
4292
|
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A${id}`);
|
|
3579
4293
|
}
|
|
@@ -3589,7 +4303,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3589
4303
|
updated_at = @updatedAt
|
|
3590
4304
|
WHERE id = @id
|
|
3591
4305
|
`
|
|
3592
|
-
).run({ id, derivedMessageId, updatedAt:
|
|
4306
|
+
).run({ id, derivedMessageId, updatedAt: nowIso5() });
|
|
3593
4307
|
return this.requireById(id);
|
|
3594
4308
|
}
|
|
3595
4309
|
markSkipped(id, reason) {
|
|
@@ -3602,7 +4316,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3602
4316
|
updated_at = @updatedAt
|
|
3603
4317
|
WHERE id = @id
|
|
3604
4318
|
`
|
|
3605
|
-
).run({ id, reason, updatedAt:
|
|
4319
|
+
).run({ id, reason, updatedAt: nowIso5() });
|
|
3606
4320
|
return this.requireById(id);
|
|
3607
4321
|
}
|
|
3608
4322
|
markFailed(id, error, finalFailure) {
|
|
@@ -3615,7 +4329,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3615
4329
|
updated_at = @updatedAt
|
|
3616
4330
|
WHERE id = @id
|
|
3617
4331
|
`
|
|
3618
|
-
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt:
|
|
4332
|
+
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso5() });
|
|
3619
4333
|
return this.requireById(id);
|
|
3620
4334
|
}
|
|
3621
4335
|
getById(id) {
|
|
@@ -3897,7 +4611,7 @@ function createFeishuChatMembersClient(client) {
|
|
|
3897
4611
|
}
|
|
3898
4612
|
|
|
3899
4613
|
// src/rag/qa-logs.ts
|
|
3900
|
-
import
|
|
4614
|
+
import crypto7 from "crypto";
|
|
3901
4615
|
|
|
3902
4616
|
// src/rag/qa-trace.ts
|
|
3903
4617
|
function hasQaTrace(trace) {
|
|
@@ -3937,7 +4651,7 @@ var QaLogRepository = class {
|
|
|
3937
4651
|
create(input) {
|
|
3938
4652
|
const trace = input.trace ?? {};
|
|
3939
4653
|
const record = {
|
|
3940
|
-
id: `qa_${
|
|
4654
|
+
id: `qa_${crypto7.randomUUID()}`,
|
|
3941
4655
|
chatId: input.chatId ?? null,
|
|
3942
4656
|
questionMessageId: input.questionMessageId ?? null,
|
|
3943
4657
|
question: input.question,
|
|
@@ -4117,13 +4831,13 @@ ${block.text}`;
|
|
|
4117
4831
|
function toToolErrorContent(message) {
|
|
4118
4832
|
return JSON.stringify({ ok: false, error: message });
|
|
4119
4833
|
}
|
|
4120
|
-
function
|
|
4834
|
+
function nowIso6() {
|
|
4121
4835
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4122
4836
|
}
|
|
4123
4837
|
function finalizeTrace(trace, status, finalAnswer, startedAtMs) {
|
|
4124
4838
|
return {
|
|
4125
4839
|
...trace,
|
|
4126
|
-
completedAt:
|
|
4840
|
+
completedAt: nowIso6(),
|
|
4127
4841
|
durationMs: Date.now() - startedAtMs,
|
|
4128
4842
|
status,
|
|
4129
4843
|
finalAnswer
|
|
@@ -4180,11 +4894,11 @@ async function runFeishuToolLoop(input) {
|
|
|
4180
4894
|
content: assistantResult.content,
|
|
4181
4895
|
reasoningContent: assistantResult.reasoningContent,
|
|
4182
4896
|
toolCalls: assistantResult.toolCalls,
|
|
4183
|
-
createdAt:
|
|
4897
|
+
createdAt: nowIso6()
|
|
4184
4898
|
});
|
|
4185
4899
|
if (assistantResult.toolCalls.length === 0) {
|
|
4186
4900
|
if (hasRawToolCallMarkup) {
|
|
4187
|
-
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:
|
|
4901
|
+
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() });
|
|
4188
4902
|
break;
|
|
4189
4903
|
}
|
|
4190
4904
|
const answer = assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
@@ -4192,7 +4906,7 @@ async function runFeishuToolLoop(input) {
|
|
|
4192
4906
|
}
|
|
4193
4907
|
for (const toolCall of assistantResult.toolCalls) {
|
|
4194
4908
|
if (toolCallsUsed >= maxToolCalls) {
|
|
4195
|
-
trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt:
|
|
4909
|
+
trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt: nowIso6() });
|
|
4196
4910
|
return { answer: FEISHU_TOOL_LOOP_LIMIT_REACHED, trace: finalizeTrace(trace, "failed", FEISHU_TOOL_LOOP_LIMIT_REACHED, startedAtMs) };
|
|
4197
4911
|
}
|
|
4198
4912
|
toolCallsUsed += 1;
|
|
@@ -4203,7 +4917,7 @@ async function runFeishuToolLoop(input) {
|
|
|
4203
4917
|
name: toolCall.name,
|
|
4204
4918
|
input: toolCall.input,
|
|
4205
4919
|
error: `\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`,
|
|
4206
|
-
createdAt:
|
|
4920
|
+
createdAt: nowIso6()
|
|
4207
4921
|
});
|
|
4208
4922
|
messages.push({
|
|
4209
4923
|
role: "tool",
|
|
@@ -4219,7 +4933,7 @@ async function runFeishuToolLoop(input) {
|
|
|
4219
4933
|
name: toolCall.name,
|
|
4220
4934
|
input: toolCall.input,
|
|
4221
4935
|
content: result,
|
|
4222
|
-
createdAt:
|
|
4936
|
+
createdAt: nowIso6()
|
|
4223
4937
|
});
|
|
4224
4938
|
messages.push({
|
|
4225
4939
|
role: "tool",
|
|
@@ -4233,7 +4947,7 @@ async function runFeishuToolLoop(input) {
|
|
|
4233
4947
|
name: toolCall.name,
|
|
4234
4948
|
input: toolCall.input,
|
|
4235
4949
|
error: message,
|
|
4236
|
-
createdAt:
|
|
4950
|
+
createdAt: nowIso6()
|
|
4237
4951
|
});
|
|
4238
4952
|
messages.push({
|
|
4239
4953
|
role: "tool",
|
|
@@ -4249,11 +4963,11 @@ async function runFeishuToolLoop(input) {
|
|
|
4249
4963
|
{ 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" }
|
|
4250
4964
|
]);
|
|
4251
4965
|
const answer = salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4252
|
-
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:
|
|
4966
|
+
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() });
|
|
4253
4967
|
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4254
4968
|
} catch {
|
|
4255
4969
|
const answer = "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4256
|
-
trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt:
|
|
4970
|
+
trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt: nowIso6() });
|
|
4257
4971
|
return { answer, trace: finalizeTrace(trace, "failed", answer, startedAtMs) };
|
|
4258
4972
|
}
|
|
4259
4973
|
}
|
|
@@ -4360,7 +5074,8 @@ var FeishuQuestionHandler = class {
|
|
|
4360
5074
|
secrets: this.options.secrets,
|
|
4361
5075
|
database: this.options.database,
|
|
4362
5076
|
messages: new MessageRepository(this.options.database),
|
|
4363
|
-
excludeMessageIds: options.excludeMessageIds
|
|
5077
|
+
excludeMessageIds: options.excludeMessageIds,
|
|
5078
|
+
profileTools: createPersonProfileTools({ profiles: new ProfileRepository(this.options.database) })
|
|
4364
5079
|
});
|
|
4365
5080
|
try {
|
|
4366
5081
|
try {
|
|
@@ -4837,7 +5552,8 @@ function createFeishuGateway(options) {
|
|
|
4837
5552
|
secrets: options.secrets,
|
|
4838
5553
|
database: options.cronJobProcessor.database,
|
|
4839
5554
|
messages: new MessageRepository(options.cronJobProcessor.database),
|
|
4840
|
-
scope: { platform: "feishu", platformChatId: job.chatId }
|
|
5555
|
+
scope: { platform: "feishu", platformChatId: job.chatId },
|
|
5556
|
+
profileTools: createPersonProfileTools({ profiles: new ProfileRepository(options.cronJobProcessor.database) })
|
|
4841
5557
|
});
|
|
4842
5558
|
try {
|
|
4843
5559
|
const memberPrompt = formatFeishuMemberPrompt(
|
|
@@ -5097,7 +5813,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
5097
5813
|
};
|
|
5098
5814
|
|
|
5099
5815
|
// src/files/ingest.ts
|
|
5100
|
-
import
|
|
5816
|
+
import crypto8 from "crypto";
|
|
5101
5817
|
import fs12 from "fs/promises";
|
|
5102
5818
|
import path15 from "path";
|
|
5103
5819
|
|
|
@@ -5161,7 +5877,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
5161
5877
|
}
|
|
5162
5878
|
}
|
|
5163
5879
|
function stableStoredName(sourcePath, fileName) {
|
|
5164
|
-
const digest =
|
|
5880
|
+
const digest = crypto8.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
5165
5881
|
return `${digest}-${fileName}`;
|
|
5166
5882
|
}
|
|
5167
5883
|
async function ingestLocalFile(input) {
|
|
@@ -5261,16 +5977,32 @@ function isMultimodalReady(config, secrets) {
|
|
|
5261
5977
|
return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
|
|
5262
5978
|
}
|
|
5263
5979
|
var GatewayIngestor = class {
|
|
5264
|
-
constructor(database) {
|
|
5980
|
+
constructor(database, options = {}) {
|
|
5265
5981
|
this.database = database;
|
|
5982
|
+
this.options = options;
|
|
5266
5983
|
this.messages = new MessageRepository(database);
|
|
5267
5984
|
this.jobs = new FileJobRepository(database);
|
|
5268
5985
|
this.imageTasks = new ImageMultimodalTaskRepository(database);
|
|
5269
5986
|
}
|
|
5270
5987
|
database;
|
|
5988
|
+
options;
|
|
5271
5989
|
messages;
|
|
5272
5990
|
jobs;
|
|
5273
5991
|
imageTasks;
|
|
5992
|
+
enrichWithPerson(input) {
|
|
5993
|
+
const person = this.options.profiles?.resolvePersonForSender({
|
|
5994
|
+
platform: input.platform,
|
|
5995
|
+
platformChatId: input.platformChatId,
|
|
5996
|
+
senderId: input.senderId,
|
|
5997
|
+
senderName: input.senderName,
|
|
5998
|
+
source: "message",
|
|
5999
|
+
observedAt: input.sentAt
|
|
6000
|
+
});
|
|
6001
|
+
return {
|
|
6002
|
+
...input,
|
|
6003
|
+
personId: person?.id
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
5274
6006
|
ingestFeishuEvent(payload) {
|
|
5275
6007
|
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
5276
6008
|
if (!normalized) {
|
|
@@ -5279,12 +6011,13 @@ var GatewayIngestor = class {
|
|
|
5279
6011
|
reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
|
|
5280
6012
|
};
|
|
5281
6013
|
}
|
|
5282
|
-
const
|
|
5283
|
-
const
|
|
6014
|
+
const enriched = this.enrichWithPerson(normalized);
|
|
6015
|
+
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
6016
|
+
const messageId = this.messages.ingest(enriched);
|
|
5284
6017
|
return {
|
|
5285
6018
|
accepted: true,
|
|
5286
6019
|
messageId,
|
|
5287
|
-
message:
|
|
6020
|
+
message: enriched,
|
|
5288
6021
|
duplicate
|
|
5289
6022
|
};
|
|
5290
6023
|
}
|
|
@@ -5298,7 +6031,7 @@ var GatewayIngestor = class {
|
|
|
5298
6031
|
}
|
|
5299
6032
|
const openId = extractFeishuSenderOpenId(normalized);
|
|
5300
6033
|
const senderName = openId ? await input.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
|
|
5301
|
-
const enriched = { ...normalized, senderName };
|
|
6034
|
+
const enriched = this.enrichWithPerson({ ...normalized, senderName });
|
|
5302
6035
|
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
5303
6036
|
const messageId = this.messages.ingest(enriched);
|
|
5304
6037
|
return {
|
|
@@ -5526,7 +6259,7 @@ var MemoryVectorStore = class {
|
|
|
5526
6259
|
};
|
|
5527
6260
|
|
|
5528
6261
|
// src/web/server.ts
|
|
5529
|
-
import
|
|
6262
|
+
import crypto9 from "crypto";
|
|
5530
6263
|
import Fastify from "fastify";
|
|
5531
6264
|
function buildHtml() {
|
|
5532
6265
|
return `<!doctype html>
|
|
@@ -5868,6 +6601,10 @@ function buildHtml() {
|
|
|
5868
6601
|
<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>
|
|
5869
6602
|
<span>\u95EE\u7B54\u65E5\u5FD7</span>
|
|
5870
6603
|
</button>
|
|
6604
|
+
<button class="nav-item" data-view="persons">
|
|
6605
|
+
<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>
|
|
6606
|
+
<span>\u4E2A\u4EBA\u6863\u6848</span>
|
|
6607
|
+
</button>
|
|
5871
6608
|
<button class="nav-item" data-view="settings">
|
|
5872
6609
|
<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>
|
|
5873
6610
|
<span>\u8BBE\u7F6E</span>
|
|
@@ -5899,6 +6636,10 @@ function buildHtml() {
|
|
|
5899
6636
|
<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>
|
|
5900
6637
|
<span>\u4EFB\u52A1</span>
|
|
5901
6638
|
</button>
|
|
6639
|
+
<button class="mobile-nav-item" data-view="persons">
|
|
6640
|
+
<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>
|
|
6641
|
+
<span>\u6863\u6848</span>
|
|
6642
|
+
</button>
|
|
5902
6643
|
<button class="mobile-nav-item" data-view="settings">
|
|
5903
6644
|
<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>
|
|
5904
6645
|
<span>\u8BBE\u7F6E</span>
|
|
@@ -5995,6 +6736,18 @@ function buildHtml() {
|
|
|
5995
6736
|
<div class="content-panel glass"><div id="qa-logs-list"></div></div>
|
|
5996
6737
|
</div>
|
|
5997
6738
|
|
|
6739
|
+
<div class="view" id="view-persons">
|
|
6740
|
+
<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>
|
|
6741
|
+
<div class="content-panel glass" id="persons-list-panel"><div id="persons-list"></div></div>
|
|
6742
|
+
<div class="content-panel glass mt-lg" id="person-profile-panel" style="display:none;">
|
|
6743
|
+
<div class="panel-header">
|
|
6744
|
+
<h2 class="panel-title" id="person-profile-name"></h2>
|
|
6745
|
+
<button class="btn btn-sm" onclick="navigateTo('persons')">\u8FD4\u56DE\u5217\u8868</button>
|
|
6746
|
+
</div>
|
|
6747
|
+
<div id="person-profile-detail"></div>
|
|
6748
|
+
</div>
|
|
6749
|
+
</div>
|
|
6750
|
+
|
|
5998
6751
|
<div class="view" id="view-settings">
|
|
5999
6752
|
<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>
|
|
6000
6753
|
<div class="settings-group glass" id="settings-config"></div>
|
|
@@ -6030,7 +6783,8 @@ function buildHtml() {
|
|
|
6030
6783
|
let allFileJobs = [];
|
|
6031
6784
|
let allCronJobs = [];
|
|
6032
6785
|
let allQaLogs = [];
|
|
6033
|
-
let
|
|
6786
|
+
let allPersons = [];
|
|
6787
|
+
let selectedPersonId = null;
|
|
6034
6788
|
let statusData = null;
|
|
6035
6789
|
|
|
6036
6790
|
function fmt(value) { return value == null || value === "" ? "-" : String(value); }
|
|
@@ -6074,6 +6828,7 @@ function buildHtml() {
|
|
|
6074
6828
|
if (view === "files") renderFilesView();
|
|
6075
6829
|
if (view === "tasks") renderTasksView();
|
|
6076
6830
|
if (view === "qa-logs") renderQaLogsView();
|
|
6831
|
+
if (view === "persons") renderPersonsView();
|
|
6077
6832
|
}
|
|
6078
6833
|
|
|
6079
6834
|
document.querySelectorAll(".nav-item, .mobile-nav-item").forEach(function(el) {
|
|
@@ -6116,6 +6871,55 @@ function buildHtml() {
|
|
|
6116
6871
|
return result;
|
|
6117
6872
|
}
|
|
6118
6873
|
|
|
6874
|
+
function toJsArgument(value) {
|
|
6875
|
+
return escapeHtml(JSON.stringify(fmt(value)));
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
function findEntryEvidenceMessage(entry) {
|
|
6879
|
+
var evidence = entry.evidence || [];
|
|
6880
|
+
return evidence.length > 0 ? evidence[0].messageId : "";
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
function findEntryEvidenceQuote(entry) {
|
|
6884
|
+
var evidence = entry.evidence || [];
|
|
6885
|
+
return evidence.length > 0 ? evidence[0].quote : "";
|
|
6886
|
+
}
|
|
6887
|
+
|
|
6888
|
+
async function correctPersonProfileEntry(personId, entryId, category, content, entryType, confidence, evidenceMessageId, quote) {
|
|
6889
|
+
var nextContent = prompt("\u4FEE\u6B63\u6863\u6848\u5185\u5BB9", content);
|
|
6890
|
+
if (!nextContent || !nextContent.trim()) return;
|
|
6891
|
+
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";
|
|
6892
|
+
try {
|
|
6893
|
+
await postJson("/api/persons/" + encodeURIComponent(personId) + "/profile/entries/" + encodeURIComponent(entryId) + "/correct", {
|
|
6894
|
+
headers: { "content-type": "application/json" },
|
|
6895
|
+
body: JSON.stringify({
|
|
6896
|
+
category: category,
|
|
6897
|
+
content: nextContent.trim(),
|
|
6898
|
+
entryType: entryType,
|
|
6899
|
+
confidence: confidence,
|
|
6900
|
+
evidenceMessageId: evidenceMessageId,
|
|
6901
|
+
quote: quote || content,
|
|
6902
|
+
reason: reason
|
|
6903
|
+
})
|
|
6904
|
+
});
|
|
6905
|
+
showToast("\u6863\u6848\u6761\u76EE\u5DF2\u4FEE\u6B63", "success");
|
|
6906
|
+
await showPersonProfile(personId);
|
|
6907
|
+
} catch (error) {
|
|
6908
|
+
showToast(error instanceof Error ? error.message : String(error), "error");
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
|
|
6912
|
+
async function deletePersonProfileEntry(personId, entryId) {
|
|
6913
|
+
if (!confirm("\u786E\u8BA4\u5220\u9664\u8FD9\u6761\u6863\u6848\u6761\u76EE\uFF1F")) return;
|
|
6914
|
+
try {
|
|
6915
|
+
await deleteJson("/api/persons/" + encodeURIComponent(personId) + "/profile/entries/" + encodeURIComponent(entryId));
|
|
6916
|
+
showToast("\u6863\u6848\u6761\u76EE\u5DF2\u5220\u9664", "success");
|
|
6917
|
+
await showPersonProfile(personId);
|
|
6918
|
+
} catch (error) {
|
|
6919
|
+
showToast(error instanceof Error ? error.message : String(error), "error");
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
|
|
6119
6923
|
function renderMetrics(status) {
|
|
6120
6924
|
var gatewayClass = status.gateway.configured ? "status-dot online" : "status-dot offline";
|
|
6121
6925
|
var gatewayText = status.gateway.connection === "running" ? "\u8FD0\u884C\u4E2D" : (!status.gateway.configured ? "\u672A\u914D\u7F6E" : "\u5F85\u542F\u52A8");
|
|
@@ -6377,6 +7181,96 @@ function buildHtml() {
|
|
|
6377
7181
|
container.innerHTML = html;
|
|
6378
7182
|
}
|
|
6379
7183
|
|
|
7184
|
+
function renderPersonsView() {
|
|
7185
|
+
var listEl = document.getElementById("persons-list");
|
|
7186
|
+
var profilePanel = document.getElementById("person-profile-panel");
|
|
7187
|
+
if (profilePanel) profilePanel.style.display = "none";
|
|
7188
|
+
if (!allPersons || allPersons.length === 0) {
|
|
7189
|
+
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>';
|
|
7190
|
+
return;
|
|
7191
|
+
}
|
|
7192
|
+
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>';
|
|
7193
|
+
for (var i = 0; i < allPersons.length; i++) {
|
|
7194
|
+
var p = allPersons[i];
|
|
7195
|
+
html += '<tr data-view-person="' + escapeHtml(p.id) + '" style="cursor:pointer;">' +
|
|
7196
|
+
'<td><span style="font-weight:500;">' + escapeHtml(p.primaryName) + '</span></td>' +
|
|
7197
|
+
'<td>' + escapeHtml(p.profileEntryCount) + '</td>' +
|
|
7198
|
+
'<td>' + escapeHtml(p.messageCount) + '</td></tr>';
|
|
7199
|
+
}
|
|
7200
|
+
html += '</tbody></table>';
|
|
7201
|
+
listEl.innerHTML = html;
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
async function showPersonProfile(id) {
|
|
7205
|
+
selectedPersonId = id;
|
|
7206
|
+
var listEl = document.getElementById("persons-list-panel");
|
|
7207
|
+
var profilePanel = document.getElementById("person-profile-panel");
|
|
7208
|
+
var detailEl = document.getElementById("person-profile-detail");
|
|
7209
|
+
if (listEl) listEl.style.display = "none";
|
|
7210
|
+
if (profilePanel) profilePanel.style.display = "block";
|
|
7211
|
+
detailEl.innerHTML = '<div class="empty-state">\u6B63\u5728\u52A0\u8F7D\u4E2A\u4EBA\u6863\u6848...</div>';
|
|
7212
|
+
try {
|
|
7213
|
+
var profile = await fetchJson("/api/persons/" + encodeURIComponent(id) + "/profile");
|
|
7214
|
+
var messages = await fetchJson("/api/persons/" + encodeURIComponent(id) + "/messages?limit=50");
|
|
7215
|
+
renderPersonProfile(id, profile, messages.items || []);
|
|
7216
|
+
} catch (error) {
|
|
7217
|
+
detailEl.innerHTML = '<div class="empty-state">\u52A0\u8F7D\u5931\u8D25\uFF1A' + escapeHtml(error instanceof Error ? error.message : String(error)) + '</div>';
|
|
7218
|
+
}
|
|
7219
|
+
}
|
|
7220
|
+
|
|
7221
|
+
function renderPersonProfile(id, profile, messages) {
|
|
7222
|
+
var el = document.getElementById("person-profile-detail");
|
|
7223
|
+
var nameEl = document.getElementById("person-profile-name");
|
|
7224
|
+
if (nameEl) nameEl.textContent = profile.person.primaryName;
|
|
7225
|
+
var html = '';
|
|
7226
|
+
var identities = profile.identities || [];
|
|
7227
|
+
if (identities.length > 0) {
|
|
7228
|
+
html += '<div class="mb-md"><span style="color:var(--text-muted);font-size:13px;">\u8EAB\u4EFD\uFF1A</span> ';
|
|
7229
|
+
html += identities.map(function(id) { return escapeHtml(id.displayName) + ' (' + escapeHtml(id.platform) + ')'; }).join('\uFF0C');
|
|
7230
|
+
html += '</div>';
|
|
7231
|
+
}
|
|
7232
|
+
var entries = profile.entries || [];
|
|
7233
|
+
if (entries.length === 0) {
|
|
7234
|
+
html += '<div class="empty-state">\u8FD8\u6CA1\u6709\u6863\u6848\u6761\u76EE\u3002</div>';
|
|
7235
|
+
} else {
|
|
7236
|
+
html += '<div class="grid-2">';
|
|
7237
|
+
for (var i = 0; i < entries.length; i++) {
|
|
7238
|
+
var entry = entries[i];
|
|
7239
|
+
var evidence = entry.evidence || [];
|
|
7240
|
+
html += '<div class="content-panel" style="background:rgba(255,255,255,0.03);padding:var(--space-md);">' +
|
|
7241
|
+
'<div style="display:flex;gap:var(--space-sm);margin-bottom:var(--space-sm);align-items:center;flex-wrap:wrap;">' +
|
|
7242
|
+
'<span class="tag">' + escapeHtml(entry.category) + '</span>' +
|
|
7243
|
+
'<span class="tag ' + (entry.entryType === 'fact' ? 'tag-success' : 'tag-info') + '">' + escapeHtml(entry.entryType) + '</span>' +
|
|
7244
|
+
'<span style="font-size:12px;color:var(--text-muted);">' + escapeHtml(Math.round(entry.confidence * 100)) + '%</span>' +
|
|
7245
|
+
'<span style="flex:1;"></span>' +
|
|
7246
|
+
'<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>' +
|
|
7247
|
+
'<button class="btn btn-sm btn-danger" onclick="deletePersonProfileEntry(' + toJsArgument(id) + ',' + toJsArgument(entry.id) + ')">\u5220\u9664</button>' +
|
|
7248
|
+
'</div>' +
|
|
7249
|
+
'<div style="font-weight:500;margin-bottom:var(--space-xs);">' + escapeHtml(entry.content) + '</div>';
|
|
7250
|
+
if (evidence.length > 0) {
|
|
7251
|
+
html += '<div style="font-size:12px;color:var(--text-muted);">\u8BC1\u636E\uFF1A' +
|
|
7252
|
+
evidence.map(function(e) { return '<span style="display:block;margin-top:2px;">"' + escapeHtml(e.quote) + '" (' + escapeHtml(e.reason) + ')</span>'; }).join('') +
|
|
7253
|
+
'</div>';
|
|
7254
|
+
}
|
|
7255
|
+
html += '</div>';
|
|
7256
|
+
}
|
|
7257
|
+
html += '</div>';
|
|
7258
|
+
}
|
|
7259
|
+
if (messages && messages.length > 0) {
|
|
7260
|
+
html += '<h3 style="margin:var(--space-lg) 0 var(--space-sm);font-size:16px;">\u6700\u8FD1\u6D88\u606F</h3>';
|
|
7261
|
+
html += '<div class="message-list">';
|
|
7262
|
+
for (var j = 0; j < Math.min(messages.length, 10); j++) {
|
|
7263
|
+
var msg = messages[j];
|
|
7264
|
+
html += '<div class="message-card"><div class="message-meta">' +
|
|
7265
|
+
'<span>' + escapeHtml(formatDateTime(msg.sentAt)) + '</span>' +
|
|
7266
|
+
'<span>' + escapeHtml(displayChatName(msg.chatName, msg.platform)) + '</span>' +
|
|
7267
|
+
'</div><div class="message-text">' + escapeHtml(msg.text) + '</div></div>';
|
|
7268
|
+
}
|
|
7269
|
+
html += '</div>';
|
|
7270
|
+
}
|
|
7271
|
+
el.innerHTML = html;
|
|
7272
|
+
}
|
|
7273
|
+
|
|
6380
7274
|
function renderSettings(status) {
|
|
6381
7275
|
var el = document.getElementById("settings-config");
|
|
6382
7276
|
var html = '<h3 style="font-size:16px;font-weight:600;margin-bottom:var(--space-md);">\u7CFB\u7EDF\u914D\u7F6E</h3>';
|
|
@@ -6407,6 +7301,7 @@ function buildHtml() {
|
|
|
6407
7301
|
await loadSection("/api/file-jobs", function(data) { allFileJobs = data.items || []; });
|
|
6408
7302
|
await loadSection("/api/qa-logs?limit=20", function(data) { allQaLogs = data.items || []; });
|
|
6409
7303
|
await loadSection("/api/cron-jobs", function(data) { allCronJobs = data.items || []; });
|
|
7304
|
+
await loadSection("/api/persons", function(data) { allPersons = data.items || []; });
|
|
6410
7305
|
if (currentView === "messages") renderMessagesView();
|
|
6411
7306
|
if (currentView === "episodes") renderEpisodesView();
|
|
6412
7307
|
if (currentView === "files") renderFilesView();
|
|
@@ -6415,6 +7310,10 @@ function buildHtml() {
|
|
|
6415
7310
|
renderQaLogsView();
|
|
6416
7311
|
if (selectedQaLogId) void showQaLogDetail(selectedQaLogId);
|
|
6417
7312
|
}
|
|
7313
|
+
if (currentView === "persons") {
|
|
7314
|
+
renderPersonsView();
|
|
7315
|
+
if (selectedPersonId) void showPersonProfile(selectedPersonId);
|
|
7316
|
+
}
|
|
6418
7317
|
}
|
|
6419
7318
|
|
|
6420
7319
|
async function processNow() {
|
|
@@ -6441,6 +7340,11 @@ function buildHtml() {
|
|
|
6441
7340
|
void showQaLogDetail(qaLogId);
|
|
6442
7341
|
return;
|
|
6443
7342
|
}
|
|
7343
|
+
var personId = target.dataset.viewPerson || target.closest('[data-view-person]')?.dataset.viewPerson;
|
|
7344
|
+
if (personId) {
|
|
7345
|
+
void showPersonProfile(personId);
|
|
7346
|
+
return;
|
|
7347
|
+
}
|
|
6444
7348
|
var id = target.dataset.deleteCronJob;
|
|
6445
7349
|
if (!id) return;
|
|
6446
7350
|
target.disabled = true;
|
|
@@ -6460,7 +7364,7 @@ function buildHtml() {
|
|
|
6460
7364
|
</body>
|
|
6461
7365
|
</html>`;
|
|
6462
7366
|
}
|
|
6463
|
-
function
|
|
7367
|
+
function parseLimit2(value, fallback, max) {
|
|
6464
7368
|
const rawLimit = Number(value ?? fallback);
|
|
6465
7369
|
return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
|
|
6466
7370
|
}
|
|
@@ -6484,6 +7388,16 @@ function parseCookies(header) {
|
|
|
6484
7388
|
function isAuthorizedWebAction(request, token) {
|
|
6485
7389
|
return parseCookies(request.headers.cookie).chattercatcher_web_token === token;
|
|
6486
7390
|
}
|
|
7391
|
+
function readStringField(input, key) {
|
|
7392
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
|
|
7393
|
+
const value = input[key];
|
|
7394
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
7395
|
+
}
|
|
7396
|
+
function readNumberField(input, key, fallback) {
|
|
7397
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return fallback;
|
|
7398
|
+
const value = input[key];
|
|
7399
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
7400
|
+
}
|
|
6487
7401
|
function toQaLogListItem(log) {
|
|
6488
7402
|
const { trace: _trace, ...item } = log;
|
|
6489
7403
|
return item;
|
|
@@ -6497,11 +7411,12 @@ function createWebApp(config, options = {}) {
|
|
|
6497
7411
|
const fileJobs = new FileJobRepository(database);
|
|
6498
7412
|
const qaLogs = new QaLogRepository(database);
|
|
6499
7413
|
const cronJobs = new CronJobRepository(database);
|
|
7414
|
+
const profiles = new ProfileRepository(database);
|
|
6500
7415
|
let webActionToken = "";
|
|
6501
7416
|
const tokenReady = (async () => {
|
|
6502
7417
|
const secrets = await loadSecrets();
|
|
6503
7418
|
if (!secrets.web.actionToken) {
|
|
6504
|
-
secrets.web.actionToken =
|
|
7419
|
+
secrets.web.actionToken = crypto9.randomBytes(32).toString("hex");
|
|
6505
7420
|
await saveSecrets(secrets);
|
|
6506
7421
|
}
|
|
6507
7422
|
webActionToken = getWebActionToken(secrets);
|
|
@@ -6539,32 +7454,32 @@ function createWebApp(config, options = {}) {
|
|
|
6539
7454
|
items: messages.listChats()
|
|
6540
7455
|
}));
|
|
6541
7456
|
app.get("/api/files", async (request) => {
|
|
6542
|
-
const limit =
|
|
7457
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6543
7458
|
return {
|
|
6544
7459
|
items: messages.listFiles(limit)
|
|
6545
7460
|
};
|
|
6546
7461
|
});
|
|
6547
7462
|
app.get("/api/file-jobs", async (request) => {
|
|
6548
|
-
const limit =
|
|
7463
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6549
7464
|
const status = request.query.status;
|
|
6550
7465
|
return {
|
|
6551
7466
|
items: fileJobs.list(limit, status === "processing" || status === "indexed" || status === "failed" ? { status } : {})
|
|
6552
7467
|
};
|
|
6553
7468
|
});
|
|
6554
7469
|
app.get("/api/messages/recent", async (request) => {
|
|
6555
|
-
const limit =
|
|
7470
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6556
7471
|
return {
|
|
6557
7472
|
items: messages.listRecentMessages(limit)
|
|
6558
7473
|
};
|
|
6559
7474
|
});
|
|
6560
7475
|
app.get("/api/episodes", async (request) => {
|
|
6561
|
-
const limit =
|
|
7476
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6562
7477
|
return {
|
|
6563
7478
|
items: episodes.listRecentEpisodes(limit)
|
|
6564
7479
|
};
|
|
6565
7480
|
});
|
|
6566
7481
|
app.get("/api/qa-logs", async (request) => {
|
|
6567
|
-
const limit =
|
|
7482
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6568
7483
|
return {
|
|
6569
7484
|
items: qaLogs.listRecent(limit).map(toQaLogListItem)
|
|
6570
7485
|
};
|
|
@@ -6579,7 +7494,7 @@ function createWebApp(config, options = {}) {
|
|
|
6579
7494
|
return log;
|
|
6580
7495
|
});
|
|
6581
7496
|
app.get("/api/cron-jobs", async (request) => {
|
|
6582
|
-
const limit =
|
|
7497
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6583
7498
|
return {
|
|
6584
7499
|
items: cronJobs.list(limit)
|
|
6585
7500
|
};
|
|
@@ -6599,6 +7514,105 @@ function createWebApp(config, options = {}) {
|
|
|
6599
7514
|
const ok = cronJobs.deleteByChat(id, job.chatId);
|
|
6600
7515
|
return { ok };
|
|
6601
7516
|
});
|
|
7517
|
+
app.get("/api/persons", async (_request) => {
|
|
7518
|
+
const persons = profiles.listPersons();
|
|
7519
|
+
const items = persons.map((person) => {
|
|
7520
|
+
const profile = profiles.getPersonProfile(person.id, { includeEvidence: false, includeInferred: true });
|
|
7521
|
+
return {
|
|
7522
|
+
id: person.id,
|
|
7523
|
+
primaryName: person.primaryName,
|
|
7524
|
+
profileEntryCount: profile?.entries.length ?? 0,
|
|
7525
|
+
messageCount: database.prepare(
|
|
7526
|
+
"SELECT COUNT(1) AS count FROM messages WHERE person_id = ?"
|
|
7527
|
+
).get(person.id).count
|
|
7528
|
+
};
|
|
7529
|
+
});
|
|
7530
|
+
return { items };
|
|
7531
|
+
});
|
|
7532
|
+
app.get("/api/persons/:id/profile", async (request, reply) => {
|
|
7533
|
+
const id = request.params.id;
|
|
7534
|
+
const profile = profiles.getPersonProfile(id, { includeEvidence: true, includeInferred: true });
|
|
7535
|
+
if (!profile) {
|
|
7536
|
+
reply.code(404);
|
|
7537
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u8BE5\u6210\u5458\u3002" };
|
|
7538
|
+
}
|
|
7539
|
+
return profile;
|
|
7540
|
+
});
|
|
7541
|
+
app.get("/api/persons/:id/messages", async (request) => {
|
|
7542
|
+
const id = request.params.id;
|
|
7543
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
7544
|
+
const rows = database.prepare(
|
|
7545
|
+
`SELECT m.id, m.platform, m.platform_message_id AS platformMessageId, m.sender_id AS senderId,
|
|
7546
|
+
m.sender_name AS senderName, m.message_type AS messageType, m.text, m.sent_at AS sentAt,
|
|
7547
|
+
c.name AS chatName, c.platform_chat_id AS platformChatId
|
|
7548
|
+
FROM messages m
|
|
7549
|
+
JOIN chats c ON c.id = m.chat_id
|
|
7550
|
+
WHERE m.person_id = ?
|
|
7551
|
+
ORDER BY m.sent_at DESC, m.created_at DESC
|
|
7552
|
+
LIMIT ?`
|
|
7553
|
+
).all(id, limit);
|
|
7554
|
+
return { items: rows };
|
|
7555
|
+
});
|
|
7556
|
+
app.post("/api/persons/:id/profile/entries/:entryId/correct", async (request, reply) => {
|
|
7557
|
+
await tokenReady;
|
|
7558
|
+
if (!isAuthorizedWebAction(request, webActionToken)) {
|
|
7559
|
+
reply.code(403);
|
|
7560
|
+
return { ok: false, message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
|
|
7561
|
+
}
|
|
7562
|
+
const { id, entryId } = request.params;
|
|
7563
|
+
const existing = profiles.getProfileEntry(entryId);
|
|
7564
|
+
if (!existing || existing.personId !== id || existing.status !== "active") {
|
|
7565
|
+
reply.code(404);
|
|
7566
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u8BE5\u6863\u6848\u6761\u76EE\u3002" };
|
|
7567
|
+
}
|
|
7568
|
+
const body = request.body;
|
|
7569
|
+
const category = readStringField(body, "category") ?? existing.category;
|
|
7570
|
+
const content = readStringField(body, "content");
|
|
7571
|
+
const entryType = readStringField(body, "entryType") ?? existing.entryType;
|
|
7572
|
+
const evidenceMessageId = readStringField(body, "evidenceMessageId");
|
|
7573
|
+
const quote = readStringField(body, "quote");
|
|
7574
|
+
const reason = readStringField(body, "reason") ?? "\u7528\u6237\u5728 Web UI \u663E\u5F0F\u4FEE\u6B63";
|
|
7575
|
+
if (!content || !evidenceMessageId || !quote) {
|
|
7576
|
+
reply.code(400);
|
|
7577
|
+
return { ok: false, message: "\u4FEE\u6B63\u5FC5\u987B\u5305\u542B content\u3001evidenceMessageId \u548C quote\u3002" };
|
|
7578
|
+
}
|
|
7579
|
+
if (entryType !== "fact" && entryType !== "inferred") {
|
|
7580
|
+
reply.code(400);
|
|
7581
|
+
return { ok: false, message: "entryType \u5FC5\u987B\u662F fact \u6216 inferred\u3002" };
|
|
7582
|
+
}
|
|
7583
|
+
if (category === existing.category && content === existing.content) {
|
|
7584
|
+
reply.code(400);
|
|
7585
|
+
return { ok: false, message: "\u4FEE\u6B63\u5185\u5BB9\u6CA1\u6709\u53D8\u5316\u3002" };
|
|
7586
|
+
}
|
|
7587
|
+
const entryIdNew = profiles.replaceProfileEntry({
|
|
7588
|
+
supersedeEntryId: entryId,
|
|
7589
|
+
input: {
|
|
7590
|
+
personId: id,
|
|
7591
|
+
category,
|
|
7592
|
+
content,
|
|
7593
|
+
entryType,
|
|
7594
|
+
confidence: Math.min(1, Math.max(0, readNumberField(body, "confidence", existing.confidence))),
|
|
7595
|
+
source: "explicit_user_request",
|
|
7596
|
+
evidence: [{ messageId: evidenceMessageId, quote, reason }]
|
|
7597
|
+
}
|
|
7598
|
+
});
|
|
7599
|
+
return { ok: true, entryId: entryIdNew };
|
|
7600
|
+
});
|
|
7601
|
+
app.delete("/api/persons/:id/profile/entries/:entryId", async (request, reply) => {
|
|
7602
|
+
await tokenReady;
|
|
7603
|
+
if (!isAuthorizedWebAction(request, webActionToken)) {
|
|
7604
|
+
reply.code(403);
|
|
7605
|
+
return { ok: false, message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
|
|
7606
|
+
}
|
|
7607
|
+
const { id, entryId } = request.params;
|
|
7608
|
+
const existing = profiles.getProfileEntry(entryId);
|
|
7609
|
+
if (!existing || existing.personId !== id || existing.status !== "active") {
|
|
7610
|
+
reply.code(404);
|
|
7611
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u8BE5\u6863\u6848\u6761\u76EE\u3002" };
|
|
7612
|
+
}
|
|
7613
|
+
profiles.markProfileEntryDeleted(entryId);
|
|
7614
|
+
return { ok: true };
|
|
7615
|
+
});
|
|
6602
7616
|
app.post("/api/process/messages", async (request, reply) => {
|
|
6603
7617
|
await tokenReady;
|
|
6604
7618
|
if (!isAuthorizedWebAction(request, webActionToken)) {
|