chattercatcher 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -0
- package/CHANGELOG.md +52 -0
- package/README.md +60 -14
- package/dist/cli.js +1514 -90
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +163 -3
- package/dist/index.js +1283 -77
- package/dist/index.js.map +1 -1
- package/docs/DEVELOPMENT_PLAN.md +45 -1
- package/docs/PRD.md +14 -0
- package/docs/TECHNICAL_ARCHITECTURE.md +117 -0
- package/package.json +3 -2
package/dist/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,
|
|
@@ -995,6 +1070,7 @@ function migrateDatabase(database) {
|
|
|
995
1070
|
answer TEXT NOT NULL,
|
|
996
1071
|
citations_json TEXT NOT NULL,
|
|
997
1072
|
retrieval_debug_json TEXT NOT NULL,
|
|
1073
|
+
trace_json TEXT NOT NULL DEFAULT '{}',
|
|
998
1074
|
status TEXT NOT NULL CHECK(status IN ('answered','failed')),
|
|
999
1075
|
error TEXT,
|
|
1000
1076
|
created_at TEXT NOT NULL
|
|
@@ -1079,6 +1155,11 @@ function migrateDatabase(database) {
|
|
|
1079
1155
|
CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
|
|
1080
1156
|
ON feishu_chat_members(chat_id, user_name);
|
|
1081
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();
|
|
1082
1163
|
const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
|
|
1083
1164
|
const ensureCronJobColumn = (name, definition) => {
|
|
1084
1165
|
if (!cronJobColumns.some((column) => column.name === name)) {
|
|
@@ -1089,6 +1170,10 @@ function migrateDatabase(database) {
|
|
|
1089
1170
|
ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
|
|
1090
1171
|
ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
|
|
1091
1172
|
ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
|
|
1173
|
+
const qaLogColumns = database.prepare("PRAGMA table_info(qa_logs)").all();
|
|
1174
|
+
if (!qaLogColumns.some((column) => column.name === "trace_json")) {
|
|
1175
|
+
database.prepare("ALTER TABLE qa_logs ADD COLUMN trace_json TEXT NOT NULL DEFAULT '{}'").run();
|
|
1176
|
+
}
|
|
1092
1177
|
}
|
|
1093
1178
|
|
|
1094
1179
|
// src/doctor/checks.ts
|
|
@@ -1784,6 +1869,10 @@ function buildScopeWhere(scope) {
|
|
|
1784
1869
|
clauses.push("c.platform_chat_id = ?");
|
|
1785
1870
|
params.push(scope.platformChatId);
|
|
1786
1871
|
}
|
|
1872
|
+
if (scope?.personId) {
|
|
1873
|
+
clauses.push("m.person_id = ?");
|
|
1874
|
+
params.push(scope.personId);
|
|
1875
|
+
}
|
|
1787
1876
|
return {
|
|
1788
1877
|
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
1789
1878
|
params
|
|
@@ -1828,14 +1917,15 @@ var MessageRepository = class {
|
|
|
1828
1917
|
`
|
|
1829
1918
|
INSERT INTO messages (
|
|
1830
1919
|
id, platform, platform_message_id, chat_id, sender_id, sender_name,
|
|
1831
|
-
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
|
|
1832
1921
|
)
|
|
1833
1922
|
VALUES (
|
|
1834
1923
|
@id, @platform, @platformMessageId, @chatId, @senderId, @senderName,
|
|
1835
|
-
@messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1924
|
+
@personId, @messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1836
1925
|
)
|
|
1837
1926
|
ON CONFLICT(platform, platform_message_id)
|
|
1838
1927
|
DO UPDATE SET
|
|
1928
|
+
person_id = COALESCE(excluded.person_id, messages.person_id),
|
|
1839
1929
|
message_type = excluded.message_type,
|
|
1840
1930
|
text = excluded.text,
|
|
1841
1931
|
raw_payload_json = excluded.raw_payload_json,
|
|
@@ -1848,6 +1938,7 @@ var MessageRepository = class {
|
|
|
1848
1938
|
chatId,
|
|
1849
1939
|
senderId: input.senderId,
|
|
1850
1940
|
senderName: input.senderName,
|
|
1941
|
+
personId: input.personId ?? null,
|
|
1851
1942
|
messageType: input.messageType,
|
|
1852
1943
|
text: input.text,
|
|
1853
1944
|
rawPayloadJson,
|
|
@@ -1890,6 +1981,7 @@ var MessageRepository = class {
|
|
|
1890
1981
|
m.chat_id AS chatId,
|
|
1891
1982
|
m.sender_id AS senderId,
|
|
1892
1983
|
m.sender_name AS senderName,
|
|
1984
|
+
m.person_id AS personId,
|
|
1893
1985
|
m.sent_at AS sentAt,
|
|
1894
1986
|
c.platform_chat_id AS platformChatId,
|
|
1895
1987
|
c.name AS chatName
|
|
@@ -1912,6 +2004,7 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1912
2004
|
platformMessageId: derivedPlatformMessageId,
|
|
1913
2005
|
senderId: source.senderId,
|
|
1914
2006
|
senderName: source.senderName,
|
|
2007
|
+
personId: source.personId ?? void 0,
|
|
1915
2008
|
messageType: "image_summary",
|
|
1916
2009
|
text: summaryText,
|
|
1917
2010
|
sentAt: source.sentAt,
|
|
@@ -1938,7 +2031,9 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1938
2031
|
1.0 AS score,
|
|
1939
2032
|
m.message_type AS messageType,
|
|
1940
2033
|
c.name AS chatName,
|
|
2034
|
+
m.sender_id AS senderId,
|
|
1941
2035
|
m.sender_name AS senderName,
|
|
2036
|
+
m.person_id AS personId,
|
|
1942
2037
|
m.sent_at AS sentAt
|
|
1943
2038
|
FROM message_chunks mc
|
|
1944
2039
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1959,7 +2054,9 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1959
2054
|
1.0 AS score,
|
|
1960
2055
|
m.message_type AS messageType,
|
|
1961
2056
|
c.name AS chatName,
|
|
2057
|
+
m.sender_id AS senderId,
|
|
1962
2058
|
m.sender_name AS senderName,
|
|
2059
|
+
m.person_id AS personId,
|
|
1963
2060
|
m.sent_at AS sentAt
|
|
1964
2061
|
FROM message_chunks mc
|
|
1965
2062
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1983,7 +2080,9 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1983
2080
|
1.0 AS score,
|
|
1984
2081
|
m.message_type AS messageType,
|
|
1985
2082
|
c.name AS chatName,
|
|
2083
|
+
m.sender_id AS senderId,
|
|
1986
2084
|
m.sender_name AS senderName,
|
|
2085
|
+
m.person_id AS personId,
|
|
1987
2086
|
m.sent_at AS sentAt
|
|
1988
2087
|
FROM message_chunks mc
|
|
1989
2088
|
JOIN messages m ON m.id = mc.message_id
|
|
@@ -1999,7 +2098,7 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
1999
2098
|
const excludedIds = options.excludeMessageIds ?? [];
|
|
2000
2099
|
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
2001
2100
|
const scope = buildScopeWhere(options.scope);
|
|
2002
|
-
const
|
|
2101
|
+
const ftsRows = this.database.prepare(
|
|
2003
2102
|
`
|
|
2004
2103
|
SELECT
|
|
2005
2104
|
fts.chunk_id AS chunkId,
|
|
@@ -2009,8 +2108,11 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
2009
2108
|
bm25(message_chunks_fts) * -1 AS score,
|
|
2010
2109
|
m.message_type AS messageType,
|
|
2011
2110
|
c.name AS chatName,
|
|
2111
|
+
m.sender_id AS senderId,
|
|
2012
2112
|
m.sender_name AS senderName,
|
|
2013
|
-
m.
|
|
2113
|
+
m.person_id AS personId,
|
|
2114
|
+
m.sent_at AS sentAt,
|
|
2115
|
+
mc.chunk_index AS chunkIndex
|
|
2014
2116
|
FROM message_chunks_fts fts
|
|
2015
2117
|
JOIN message_chunks mc ON mc.id = fts.chunk_id
|
|
2016
2118
|
JOIN messages m ON m.id = fts.message_id
|
|
@@ -2018,10 +2120,23 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
2018
2120
|
WHERE message_chunks_fts MATCH ?
|
|
2019
2121
|
${excludedWhere}
|
|
2020
2122
|
${scope.where}
|
|
2021
|
-
ORDER BY bm25(message_chunks_fts)
|
|
2123
|
+
ORDER BY bm25(message_chunks_fts), m.sent_at DESC, mc.chunk_index ASC
|
|
2022
2124
|
LIMIT ?
|
|
2023
2125
|
`
|
|
2024
|
-
).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
|
+
}
|
|
2025
2140
|
if (ftsResults.length > 0) {
|
|
2026
2141
|
return ftsResults;
|
|
2027
2142
|
}
|
|
@@ -2035,22 +2150,30 @@ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
|
|
|
2035
2150
|
return this.database.prepare(
|
|
2036
2151
|
`
|
|
2037
2152
|
SELECT
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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
|
|
2054
2177
|
LIMIT ?
|
|
2055
2178
|
`
|
|
2056
2179
|
).all(...params, ...excludedIds, ...scope.params, limit);
|
|
@@ -2465,19 +2588,20 @@ var HybridRetriever = class {
|
|
|
2465
2588
|
|
|
2466
2589
|
// src/rag/message-retriever.ts
|
|
2467
2590
|
function toEvidenceSource(result) {
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
label: result.senderName,
|
|
2472
|
-
timestamp: result.sentAt
|
|
2473
|
-
};
|
|
2474
|
-
}
|
|
2475
|
-
return {
|
|
2476
|
-
type: "message",
|
|
2477
|
-
label: result.chatName,
|
|
2478
|
-
sender: result.senderName,
|
|
2591
|
+
const source = {
|
|
2592
|
+
type: result.messageType === "file" ? "file" : "message",
|
|
2593
|
+
label: result.messageType === "file" ? result.senderName : result.chatName,
|
|
2479
2594
|
timestamp: result.sentAt
|
|
2480
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;
|
|
2481
2605
|
}
|
|
2482
2606
|
var MessageFtsRetriever = class {
|
|
2483
2607
|
constructor(messages, options = {}) {
|
|
@@ -2606,6 +2730,9 @@ function toEvidenceSource2(row) {
|
|
|
2606
2730
|
type: "message",
|
|
2607
2731
|
label: row.chatName,
|
|
2608
2732
|
sender: row.senderName,
|
|
2733
|
+
senderId: row.senderId,
|
|
2734
|
+
personId: row.personId,
|
|
2735
|
+
profileAvailable: Boolean(row.personId),
|
|
2609
2736
|
timestamp: row.sentAt
|
|
2610
2737
|
};
|
|
2611
2738
|
}
|
|
@@ -2620,6 +2747,10 @@ function buildScopeWhere3(scope) {
|
|
|
2620
2747
|
clauses.push("c.platform_chat_id = ?");
|
|
2621
2748
|
params.push(scope.platformChatId);
|
|
2622
2749
|
}
|
|
2750
|
+
if (scope?.personId) {
|
|
2751
|
+
clauses.push("m.person_id = ?");
|
|
2752
|
+
params.push(scope.personId);
|
|
2753
|
+
}
|
|
2623
2754
|
return {
|
|
2624
2755
|
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
2625
2756
|
params
|
|
@@ -2670,7 +2801,9 @@ var SqliteVectorStore = class {
|
|
|
2670
2801
|
mc.id AS chunkId,
|
|
2671
2802
|
mc.text AS text,
|
|
2672
2803
|
c.name AS chatName,
|
|
2804
|
+
m.sender_id AS senderId,
|
|
2673
2805
|
m.sender_name AS senderName,
|
|
2806
|
+
m.person_id AS personId,
|
|
2674
2807
|
m.sent_at AS sentAt,
|
|
2675
2808
|
e.embedding_json AS embeddingJson
|
|
2676
2809
|
FROM message_chunk_embeddings e
|
|
@@ -2751,8 +2884,12 @@ async function createAgenticRagSearchTools(input) {
|
|
|
2751
2884
|
new SqliteVectorStore(input.database, { model: input.config.embedding.model })
|
|
2752
2885
|
) : void 0;
|
|
2753
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
|
+
}
|
|
2754
2891
|
return {
|
|
2755
|
-
tools
|
|
2892
|
+
tools,
|
|
2756
2893
|
close: () => {
|
|
2757
2894
|
}
|
|
2758
2895
|
};
|
|
@@ -3360,6 +3497,588 @@ function parseExactNumber2(field, min, max) {
|
|
|
3360
3497
|
return value;
|
|
3361
3498
|
}
|
|
3362
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
|
+
|
|
3363
4082
|
// src/rag/indexer.ts
|
|
3364
4083
|
var EMBEDDING_INDEX_BATCH_SIZE = 64;
|
|
3365
4084
|
async function indexMessageChunks(input) {
|
|
@@ -3447,12 +4166,12 @@ async function processMessagesNow(input) {
|
|
|
3447
4166
|
}
|
|
3448
4167
|
|
|
3449
4168
|
// src/multimodal/tasks.ts
|
|
3450
|
-
import
|
|
3451
|
-
function
|
|
4169
|
+
import crypto6 from "crypto";
|
|
4170
|
+
function nowIso5() {
|
|
3452
4171
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3453
4172
|
}
|
|
3454
4173
|
function stableId3(sourceMessageId, imageKey) {
|
|
3455
|
-
return
|
|
4174
|
+
return crypto6.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
|
|
3456
4175
|
}
|
|
3457
4176
|
function mapRow(row) {
|
|
3458
4177
|
if (!row) {
|
|
@@ -3480,7 +4199,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3480
4199
|
database;
|
|
3481
4200
|
enqueue(input) {
|
|
3482
4201
|
const id = stableId3(input.sourceMessageId, input.imageKey);
|
|
3483
|
-
const timestamp =
|
|
4202
|
+
const timestamp = nowIso5();
|
|
3484
4203
|
this.database.prepare(
|
|
3485
4204
|
`
|
|
3486
4205
|
INSERT INTO image_multimodal_tasks (
|
|
@@ -3568,7 +4287,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3568
4287
|
updated_at = @updatedAt
|
|
3569
4288
|
WHERE id = @id AND status = 'pending'
|
|
3570
4289
|
`
|
|
3571
|
-
).run({ id, updatedAt:
|
|
4290
|
+
).run({ id, updatedAt: nowIso5() });
|
|
3572
4291
|
if (result.changes === 0) {
|
|
3573
4292
|
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A${id}`);
|
|
3574
4293
|
}
|
|
@@ -3584,7 +4303,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3584
4303
|
updated_at = @updatedAt
|
|
3585
4304
|
WHERE id = @id
|
|
3586
4305
|
`
|
|
3587
|
-
).run({ id, derivedMessageId, updatedAt:
|
|
4306
|
+
).run({ id, derivedMessageId, updatedAt: nowIso5() });
|
|
3588
4307
|
return this.requireById(id);
|
|
3589
4308
|
}
|
|
3590
4309
|
markSkipped(id, reason) {
|
|
@@ -3597,7 +4316,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3597
4316
|
updated_at = @updatedAt
|
|
3598
4317
|
WHERE id = @id
|
|
3599
4318
|
`
|
|
3600
|
-
).run({ id, reason, updatedAt:
|
|
4319
|
+
).run({ id, reason, updatedAt: nowIso5() });
|
|
3601
4320
|
return this.requireById(id);
|
|
3602
4321
|
}
|
|
3603
4322
|
markFailed(id, error, finalFailure) {
|
|
@@ -3610,7 +4329,7 @@ var ImageMultimodalTaskRepository = class {
|
|
|
3610
4329
|
updated_at = @updatedAt
|
|
3611
4330
|
WHERE id = @id
|
|
3612
4331
|
`
|
|
3613
|
-
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt:
|
|
4332
|
+
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso5() });
|
|
3614
4333
|
return this.requireById(id);
|
|
3615
4334
|
}
|
|
3616
4335
|
getById(id) {
|
|
@@ -3892,11 +4611,23 @@ function createFeishuChatMembersClient(client) {
|
|
|
3892
4611
|
}
|
|
3893
4612
|
|
|
3894
4613
|
// src/rag/qa-logs.ts
|
|
3895
|
-
import
|
|
4614
|
+
import crypto7 from "crypto";
|
|
4615
|
+
|
|
4616
|
+
// src/rag/qa-trace.ts
|
|
4617
|
+
function hasQaTrace(trace) {
|
|
4618
|
+
return Object.keys(trace).length > 0;
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
// src/rag/qa-logs.ts
|
|
3896
4622
|
function clampLimit(limit) {
|
|
3897
4623
|
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3898
4624
|
}
|
|
4625
|
+
function parseTrace(value) {
|
|
4626
|
+
const parsed = JSON.parse(value);
|
|
4627
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4628
|
+
}
|
|
3899
4629
|
function mapQaLogRow(row) {
|
|
4630
|
+
const trace = parseTrace(row.trace_json);
|
|
3900
4631
|
return {
|
|
3901
4632
|
id: row.id,
|
|
3902
4633
|
chatId: row.chat_id,
|
|
@@ -3905,6 +4636,8 @@ function mapQaLogRow(row) {
|
|
|
3905
4636
|
answer: row.answer,
|
|
3906
4637
|
citations: JSON.parse(row.citations_json),
|
|
3907
4638
|
retrievalDebug: JSON.parse(row.retrieval_debug_json),
|
|
4639
|
+
trace,
|
|
4640
|
+
hasTrace: hasQaTrace(trace),
|
|
3908
4641
|
status: row.status,
|
|
3909
4642
|
error: row.error,
|
|
3910
4643
|
createdAt: row.created_at
|
|
@@ -3916,14 +4649,17 @@ var QaLogRepository = class {
|
|
|
3916
4649
|
}
|
|
3917
4650
|
database;
|
|
3918
4651
|
create(input) {
|
|
4652
|
+
const trace = input.trace ?? {};
|
|
3919
4653
|
const record = {
|
|
3920
|
-
id: `qa_${
|
|
4654
|
+
id: `qa_${crypto7.randomUUID()}`,
|
|
3921
4655
|
chatId: input.chatId ?? null,
|
|
3922
4656
|
questionMessageId: input.questionMessageId ?? null,
|
|
3923
4657
|
question: input.question,
|
|
3924
4658
|
answer: input.answer,
|
|
3925
4659
|
citations: input.citations,
|
|
3926
4660
|
retrievalDebug: input.retrievalDebug,
|
|
4661
|
+
trace,
|
|
4662
|
+
hasTrace: hasQaTrace(trace),
|
|
3927
4663
|
status: input.status,
|
|
3928
4664
|
error: input.error ?? null,
|
|
3929
4665
|
createdAt: input.createdAt
|
|
@@ -3938,6 +4674,7 @@ var QaLogRepository = class {
|
|
|
3938
4674
|
answer,
|
|
3939
4675
|
citations_json,
|
|
3940
4676
|
retrieval_debug_json,
|
|
4677
|
+
trace_json,
|
|
3941
4678
|
status,
|
|
3942
4679
|
error,
|
|
3943
4680
|
created_at
|
|
@@ -3950,6 +4687,7 @@ var QaLogRepository = class {
|
|
|
3950
4687
|
@answer,
|
|
3951
4688
|
@citationsJson,
|
|
3952
4689
|
@retrievalDebugJson,
|
|
4690
|
+
@traceJson,
|
|
3953
4691
|
@status,
|
|
3954
4692
|
@error,
|
|
3955
4693
|
@createdAt
|
|
@@ -3963,6 +4701,7 @@ var QaLogRepository = class {
|
|
|
3963
4701
|
answer: record.answer,
|
|
3964
4702
|
citationsJson: JSON.stringify(record.citations),
|
|
3965
4703
|
retrievalDebugJson: JSON.stringify(record.retrievalDebug),
|
|
4704
|
+
traceJson: JSON.stringify(record.trace),
|
|
3966
4705
|
status: record.status,
|
|
3967
4706
|
error: record.error,
|
|
3968
4707
|
createdAt: record.createdAt
|
|
@@ -3980,6 +4719,7 @@ var QaLogRepository = class {
|
|
|
3980
4719
|
answer,
|
|
3981
4720
|
citations_json,
|
|
3982
4721
|
retrieval_debug_json,
|
|
4722
|
+
trace_json,
|
|
3983
4723
|
status,
|
|
3984
4724
|
error,
|
|
3985
4725
|
created_at
|
|
@@ -4001,6 +4741,7 @@ var QaLogRepository = class {
|
|
|
4001
4741
|
answer,
|
|
4002
4742
|
citations_json,
|
|
4003
4743
|
retrieval_debug_json,
|
|
4744
|
+
trace_json,
|
|
4004
4745
|
status,
|
|
4005
4746
|
error,
|
|
4006
4747
|
created_at
|
|
@@ -4012,6 +4753,27 @@ var QaLogRepository = class {
|
|
|
4012
4753
|
).all(chatId, clampLimit(limit));
|
|
4013
4754
|
return rows.map(mapQaLogRow);
|
|
4014
4755
|
}
|
|
4756
|
+
getById(id) {
|
|
4757
|
+
const row = this.database.prepare(
|
|
4758
|
+
`
|
|
4759
|
+
SELECT
|
|
4760
|
+
id,
|
|
4761
|
+
chat_id,
|
|
4762
|
+
question_message_id,
|
|
4763
|
+
question,
|
|
4764
|
+
answer,
|
|
4765
|
+
citations_json,
|
|
4766
|
+
retrieval_debug_json,
|
|
4767
|
+
trace_json,
|
|
4768
|
+
status,
|
|
4769
|
+
error,
|
|
4770
|
+
created_at
|
|
4771
|
+
FROM qa_logs
|
|
4772
|
+
WHERE id = ?
|
|
4773
|
+
`
|
|
4774
|
+
).get(id);
|
|
4775
|
+
return row ? mapQaLogRow(row) : null;
|
|
4776
|
+
}
|
|
4015
4777
|
getCount() {
|
|
4016
4778
|
const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
|
|
4017
4779
|
return row.count;
|
|
@@ -4069,6 +4831,18 @@ ${block.text}`;
|
|
|
4069
4831
|
function toToolErrorContent(message) {
|
|
4070
4832
|
return JSON.stringify({ ok: false, error: message });
|
|
4071
4833
|
}
|
|
4834
|
+
function nowIso6() {
|
|
4835
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4836
|
+
}
|
|
4837
|
+
function finalizeTrace(trace, status, finalAnswer, startedAtMs) {
|
|
4838
|
+
return {
|
|
4839
|
+
...trace,
|
|
4840
|
+
completedAt: nowIso6(),
|
|
4841
|
+
durationMs: Date.now() - startedAtMs,
|
|
4842
|
+
status,
|
|
4843
|
+
finalAnswer
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4072
4846
|
async function executeFeishuTool(tool, input) {
|
|
4073
4847
|
const result = await tool.execute(input);
|
|
4074
4848
|
if (isEvidenceBlockArray(result)) {
|
|
@@ -4080,6 +4854,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4080
4854
|
if (!input.model.completeWithTools) {
|
|
4081
4855
|
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
4082
4856
|
}
|
|
4857
|
+
const startedAtMs = Date.now();
|
|
4858
|
+
const trace = {
|
|
4859
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
4860
|
+
modelTurns: [],
|
|
4861
|
+
toolResults: [],
|
|
4862
|
+
fallbacks: []
|
|
4863
|
+
};
|
|
4083
4864
|
const maxModelTurns = input.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
4084
4865
|
const maxToolCalls = input.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
4085
4866
|
const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
|
|
@@ -4108,19 +4889,36 @@ async function runFeishuToolLoop(input) {
|
|
|
4108
4889
|
toolCalls: assistantResult.toolCalls,
|
|
4109
4890
|
reasoningContent: assistantResult.reasoningContent
|
|
4110
4891
|
});
|
|
4892
|
+
trace.modelTurns?.push({
|
|
4893
|
+
index: turn,
|
|
4894
|
+
content: assistantResult.content,
|
|
4895
|
+
reasoningContent: assistantResult.reasoningContent,
|
|
4896
|
+
toolCalls: assistantResult.toolCalls,
|
|
4897
|
+
createdAt: nowIso6()
|
|
4898
|
+
});
|
|
4111
4899
|
if (assistantResult.toolCalls.length === 0) {
|
|
4112
4900
|
if (hasRawToolCallMarkup) {
|
|
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() });
|
|
4113
4902
|
break;
|
|
4114
4903
|
}
|
|
4115
|
-
|
|
4904
|
+
const answer = assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
4905
|
+
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4116
4906
|
}
|
|
4117
4907
|
for (const toolCall of assistantResult.toolCalls) {
|
|
4118
4908
|
if (toolCallsUsed >= maxToolCalls) {
|
|
4119
|
-
|
|
4909
|
+
trace.fallbacks?.push({ type: "tool_limit", message: FEISHU_TOOL_LOOP_LIMIT_REACHED, createdAt: nowIso6() });
|
|
4910
|
+
return { answer: FEISHU_TOOL_LOOP_LIMIT_REACHED, trace: finalizeTrace(trace, "failed", FEISHU_TOOL_LOOP_LIMIT_REACHED, startedAtMs) };
|
|
4120
4911
|
}
|
|
4121
4912
|
toolCallsUsed += 1;
|
|
4122
4913
|
const tool = toolsByName.get(toolCall.name);
|
|
4123
4914
|
if (!tool) {
|
|
4915
|
+
trace.toolResults?.push({
|
|
4916
|
+
toolCallId: toolCall.id,
|
|
4917
|
+
name: toolCall.name,
|
|
4918
|
+
input: toolCall.input,
|
|
4919
|
+
error: `\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`,
|
|
4920
|
+
createdAt: nowIso6()
|
|
4921
|
+
});
|
|
4124
4922
|
messages.push({
|
|
4125
4923
|
role: "tool",
|
|
4126
4924
|
toolCallId: toolCall.id,
|
|
@@ -4130,6 +4928,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4130
4928
|
}
|
|
4131
4929
|
try {
|
|
4132
4930
|
const result = await executeFeishuTool(tool, toolCall.input);
|
|
4931
|
+
trace.toolResults?.push({
|
|
4932
|
+
toolCallId: toolCall.id,
|
|
4933
|
+
name: toolCall.name,
|
|
4934
|
+
input: toolCall.input,
|
|
4935
|
+
content: result,
|
|
4936
|
+
createdAt: nowIso6()
|
|
4937
|
+
});
|
|
4133
4938
|
messages.push({
|
|
4134
4939
|
role: "tool",
|
|
4135
4940
|
toolCallId: toolCall.id,
|
|
@@ -4137,6 +4942,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4137
4942
|
});
|
|
4138
4943
|
} catch (error) {
|
|
4139
4944
|
const message = error instanceof Error ? error.message : String(error);
|
|
4945
|
+
trace.toolResults?.push({
|
|
4946
|
+
toolCallId: toolCall.id,
|
|
4947
|
+
name: toolCall.name,
|
|
4948
|
+
input: toolCall.input,
|
|
4949
|
+
error: message,
|
|
4950
|
+
createdAt: nowIso6()
|
|
4951
|
+
});
|
|
4140
4952
|
messages.push({
|
|
4141
4953
|
role: "tool",
|
|
4142
4954
|
toolCallId: toolCall.id,
|
|
@@ -4150,9 +4962,13 @@ async function runFeishuToolLoop(input) {
|
|
|
4150
4962
|
...messages,
|
|
4151
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" }
|
|
4152
4964
|
]);
|
|
4153
|
-
|
|
4965
|
+
const answer = salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
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() });
|
|
4967
|
+
return { answer, trace: finalizeTrace(trace, "answered", answer, startedAtMs) };
|
|
4154
4968
|
} catch {
|
|
4155
|
-
|
|
4969
|
+
const answer = "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
4970
|
+
trace.fallbacks?.push({ type: "answer_generation_failed", message: answer, createdAt: nowIso6() });
|
|
4971
|
+
return { answer, trace: finalizeTrace(trace, "failed", answer, startedAtMs) };
|
|
4156
4972
|
}
|
|
4157
4973
|
}
|
|
4158
4974
|
function formatConversationContext(records) {
|
|
@@ -4258,7 +5074,8 @@ var FeishuQuestionHandler = class {
|
|
|
4258
5074
|
secrets: this.options.secrets,
|
|
4259
5075
|
database: this.options.database,
|
|
4260
5076
|
messages: new MessageRepository(this.options.database),
|
|
4261
|
-
excludeMessageIds: options.excludeMessageIds
|
|
5077
|
+
excludeMessageIds: options.excludeMessageIds,
|
|
5078
|
+
profileTools: createPersonProfileTools({ profiles: new ProfileRepository(this.options.database) })
|
|
4262
5079
|
});
|
|
4263
5080
|
try {
|
|
4264
5081
|
try {
|
|
@@ -4272,7 +5089,7 @@ var FeishuQuestionHandler = class {
|
|
|
4272
5089
|
const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
|
|
4273
5090
|
const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
|
|
4274
5091
|
const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
|
|
4275
|
-
const
|
|
5092
|
+
const result = await runFeishuToolLoop({
|
|
4276
5093
|
question: decision.question,
|
|
4277
5094
|
now,
|
|
4278
5095
|
tools: allTools,
|
|
@@ -4284,27 +5101,38 @@ var FeishuQuestionHandler = class {
|
|
|
4284
5101
|
chatId: decision.chatId,
|
|
4285
5102
|
questionMessageId,
|
|
4286
5103
|
question: decision.question,
|
|
4287
|
-
answer,
|
|
5104
|
+
answer: result.answer,
|
|
4288
5105
|
citations: [],
|
|
4289
5106
|
retrievalDebug: {},
|
|
5107
|
+
trace: result.trace,
|
|
4290
5108
|
status: "answered",
|
|
4291
5109
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4292
5110
|
});
|
|
4293
|
-
await this.sendResponse(decision.chatId, questionMessageId, answer);
|
|
5111
|
+
await this.sendResponse(decision.chatId, questionMessageId, result.answer);
|
|
4294
5112
|
} catch (error) {
|
|
4295
5113
|
const message = error instanceof Error ? error.message : String(error);
|
|
5114
|
+
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5115
|
+
const failedAnswer = `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`;
|
|
4296
5116
|
qaLogs.create({
|
|
4297
5117
|
chatId: decision.chatId,
|
|
4298
5118
|
questionMessageId,
|
|
4299
5119
|
question: decision.question,
|
|
4300
|
-
answer:
|
|
5120
|
+
answer: failedAnswer,
|
|
4301
5121
|
citations: [],
|
|
4302
5122
|
retrievalDebug: {},
|
|
5123
|
+
trace: {
|
|
5124
|
+
startedAt: now.toISOString(),
|
|
5125
|
+
completedAt: failedAt,
|
|
5126
|
+
durationMs: Math.max(0, Date.parse(failedAt) - now.getTime()),
|
|
5127
|
+
status: "failed",
|
|
5128
|
+
finalAnswer: failedAnswer,
|
|
5129
|
+
fallbacks: [{ type: "answer_generation_failed", message, createdAt: failedAt }]
|
|
5130
|
+
},
|
|
4303
5131
|
status: "failed",
|
|
4304
5132
|
error: message,
|
|
4305
|
-
createdAt:
|
|
5133
|
+
createdAt: failedAt
|
|
4306
5134
|
});
|
|
4307
|
-
await this.sendResponse(decision.chatId, questionMessageId,
|
|
5135
|
+
await this.sendResponse(decision.chatId, questionMessageId, failedAnswer);
|
|
4308
5136
|
}
|
|
4309
5137
|
return decision;
|
|
4310
5138
|
} finally {
|
|
@@ -4724,7 +5552,8 @@ function createFeishuGateway(options) {
|
|
|
4724
5552
|
secrets: options.secrets,
|
|
4725
5553
|
database: options.cronJobProcessor.database,
|
|
4726
5554
|
messages: new MessageRepository(options.cronJobProcessor.database),
|
|
4727
|
-
scope: { platform: "feishu", platformChatId: job.chatId }
|
|
5555
|
+
scope: { platform: "feishu", platformChatId: job.chatId },
|
|
5556
|
+
profileTools: createPersonProfileTools({ profiles: new ProfileRepository(options.cronJobProcessor.database) })
|
|
4728
5557
|
});
|
|
4729
5558
|
try {
|
|
4730
5559
|
const memberPrompt = formatFeishuMemberPrompt(
|
|
@@ -4984,7 +5813,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
4984
5813
|
};
|
|
4985
5814
|
|
|
4986
5815
|
// src/files/ingest.ts
|
|
4987
|
-
import
|
|
5816
|
+
import crypto8 from "crypto";
|
|
4988
5817
|
import fs12 from "fs/promises";
|
|
4989
5818
|
import path15 from "path";
|
|
4990
5819
|
|
|
@@ -5048,7 +5877,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
5048
5877
|
}
|
|
5049
5878
|
}
|
|
5050
5879
|
function stableStoredName(sourcePath, fileName) {
|
|
5051
|
-
const digest =
|
|
5880
|
+
const digest = crypto8.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
5052
5881
|
return `${digest}-${fileName}`;
|
|
5053
5882
|
}
|
|
5054
5883
|
async function ingestLocalFile(input) {
|
|
@@ -5148,16 +5977,32 @@ function isMultimodalReady(config, secrets) {
|
|
|
5148
5977
|
return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
|
|
5149
5978
|
}
|
|
5150
5979
|
var GatewayIngestor = class {
|
|
5151
|
-
constructor(database) {
|
|
5980
|
+
constructor(database, options = {}) {
|
|
5152
5981
|
this.database = database;
|
|
5982
|
+
this.options = options;
|
|
5153
5983
|
this.messages = new MessageRepository(database);
|
|
5154
5984
|
this.jobs = new FileJobRepository(database);
|
|
5155
5985
|
this.imageTasks = new ImageMultimodalTaskRepository(database);
|
|
5156
5986
|
}
|
|
5157
5987
|
database;
|
|
5988
|
+
options;
|
|
5158
5989
|
messages;
|
|
5159
5990
|
jobs;
|
|
5160
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
|
+
}
|
|
5161
6006
|
ingestFeishuEvent(payload) {
|
|
5162
6007
|
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
5163
6008
|
if (!normalized) {
|
|
@@ -5166,12 +6011,13 @@ var GatewayIngestor = class {
|
|
|
5166
6011
|
reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
|
|
5167
6012
|
};
|
|
5168
6013
|
}
|
|
5169
|
-
const
|
|
5170
|
-
const
|
|
6014
|
+
const enriched = this.enrichWithPerson(normalized);
|
|
6015
|
+
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
6016
|
+
const messageId = this.messages.ingest(enriched);
|
|
5171
6017
|
return {
|
|
5172
6018
|
accepted: true,
|
|
5173
6019
|
messageId,
|
|
5174
|
-
message:
|
|
6020
|
+
message: enriched,
|
|
5175
6021
|
duplicate
|
|
5176
6022
|
};
|
|
5177
6023
|
}
|
|
@@ -5185,7 +6031,7 @@ var GatewayIngestor = class {
|
|
|
5185
6031
|
}
|
|
5186
6032
|
const openId = extractFeishuSenderOpenId(normalized);
|
|
5187
6033
|
const senderName = openId ? await input.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
|
|
5188
|
-
const enriched = { ...normalized, senderName };
|
|
6034
|
+
const enriched = this.enrichWithPerson({ ...normalized, senderName });
|
|
5189
6035
|
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
5190
6036
|
const messageId = this.messages.ingest(enriched);
|
|
5191
6037
|
return {
|
|
@@ -5413,7 +6259,7 @@ var MemoryVectorStore = class {
|
|
|
5413
6259
|
};
|
|
5414
6260
|
|
|
5415
6261
|
// src/web/server.ts
|
|
5416
|
-
import
|
|
6262
|
+
import crypto9 from "crypto";
|
|
5417
6263
|
import Fastify from "fastify";
|
|
5418
6264
|
function buildHtml() {
|
|
5419
6265
|
return `<!doctype html>
|
|
@@ -5755,6 +6601,10 @@ function buildHtml() {
|
|
|
5755
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>
|
|
5756
6602
|
<span>\u95EE\u7B54\u65E5\u5FD7</span>
|
|
5757
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>
|
|
5758
6608
|
<button class="nav-item" data-view="settings">
|
|
5759
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>
|
|
5760
6610
|
<span>\u8BBE\u7F6E</span>
|
|
@@ -5786,6 +6636,10 @@ function buildHtml() {
|
|
|
5786
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>
|
|
5787
6637
|
<span>\u4EFB\u52A1</span>
|
|
5788
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>
|
|
5789
6643
|
<button class="mobile-nav-item" data-view="settings">
|
|
5790
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>
|
|
5791
6645
|
<span>\u8BBE\u7F6E</span>
|
|
@@ -5882,6 +6736,18 @@ function buildHtml() {
|
|
|
5882
6736
|
<div class="content-panel glass"><div id="qa-logs-list"></div></div>
|
|
5883
6737
|
</div>
|
|
5884
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
|
+
|
|
5885
6751
|
<div class="view" id="view-settings">
|
|
5886
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>
|
|
5887
6753
|
<div class="settings-group glass" id="settings-config"></div>
|
|
@@ -5917,12 +6783,16 @@ function buildHtml() {
|
|
|
5917
6783
|
let allFileJobs = [];
|
|
5918
6784
|
let allCronJobs = [];
|
|
5919
6785
|
let allQaLogs = [];
|
|
6786
|
+
let allPersons = [];
|
|
6787
|
+
let selectedPersonId = null;
|
|
5920
6788
|
let statusData = null;
|
|
5921
6789
|
|
|
5922
6790
|
function fmt(value) { return value == null || value === "" ? "-" : String(value); }
|
|
5923
6791
|
function escapeHtml(value) {
|
|
5924
6792
|
return fmt(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
5925
6793
|
}
|
|
6794
|
+
function renderJson(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(JSON.stringify(value, null, 2)) + '</pre>'; }
|
|
6795
|
+
function renderTextBlock(value) { return '<pre style="white-space:pre-wrap;overflow:auto;max-height:320px;">' + escapeHtml(value || "") + '</pre>'; }
|
|
5926
6796
|
function isOpaqueId(value) { return /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(fmt(value)); }
|
|
5927
6797
|
function formatDateTime(value) {
|
|
5928
6798
|
var date = new Date(value);
|
|
@@ -5958,6 +6828,7 @@ function buildHtml() {
|
|
|
5958
6828
|
if (view === "files") renderFilesView();
|
|
5959
6829
|
if (view === "tasks") renderTasksView();
|
|
5960
6830
|
if (view === "qa-logs") renderQaLogsView();
|
|
6831
|
+
if (view === "persons") renderPersonsView();
|
|
5961
6832
|
}
|
|
5962
6833
|
|
|
5963
6834
|
document.querySelectorAll(".nav-item, .mobile-nav-item").forEach(function(el) {
|
|
@@ -6000,6 +6871,55 @@ function buildHtml() {
|
|
|
6000
6871
|
return result;
|
|
6001
6872
|
}
|
|
6002
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
|
+
|
|
6003
6923
|
function renderMetrics(status) {
|
|
6004
6924
|
var gatewayClass = status.gateway.configured ? "status-dot online" : "status-dot offline";
|
|
6005
6925
|
var gatewayText = status.gateway.connection === "running" ? "\u8FD0\u884C\u4E2D" : (!status.gateway.configured ? "\u672A\u914D\u7F6E" : "\u5F85\u542F\u52A8");
|
|
@@ -6195,13 +7115,158 @@ function buildHtml() {
|
|
|
6195
7115
|
for (var i = 0; i < allQaLogs.length; i++) {
|
|
6196
7116
|
var item = allQaLogs[i];
|
|
6197
7117
|
var citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
|
|
6198
|
-
var statusClass = item.status === '
|
|
7118
|
+
var statusClass = item.status === 'answered' ? 'tag-success' : 'tag-warning';
|
|
6199
7119
|
html += '<div class="qa-card"><div class="message-meta" style="margin-bottom:var(--space-sm);">' +
|
|
6200
7120
|
'<span>' + escapeHtml(formatDateTime(item.createdAt)) + '</span>' +
|
|
6201
7121
|
'<span class="tag ' + statusClass + '">' + escapeHtml(item.status) + '</span>' +
|
|
6202
|
-
'<span>' + citationCount + ' \u6761\u5F15\u7528</span
|
|
7122
|
+
'<span>' + citationCount + ' \u6761\u5F15\u7528</span>' +
|
|
7123
|
+
'<span class="tag ' + (item.hasTrace ? 'tag-info' : 'tag-warning') + '">' + (item.hasTrace ? '\u6709 trace' : '\u65E0 trace') + '</span></div>' +
|
|
6203
7124
|
'<div class="qa-question">' + escapeHtml(item.question) + '</div>' +
|
|
6204
|
-
'<div class="qa-answer">' + escapeHtml(item.answer) + '</div
|
|
7125
|
+
'<div class="qa-answer">' + escapeHtml(item.answer) + '</div>' +
|
|
7126
|
+
'<button class="btn btn-sm" style="margin-top:var(--space-sm);" data-view-qa-log="' + escapeHtml(item.id) + '">\u67E5\u770B\u8BE6\u60C5</button>' +
|
|
7127
|
+
'<div id="qa-detail-' + escapeHtml(item.id) + '" style="margin-top:var(--space-md);"></div></div>';
|
|
7128
|
+
}
|
|
7129
|
+
el.innerHTML = html;
|
|
7130
|
+
}
|
|
7131
|
+
|
|
7132
|
+
async function showQaLogDetail(id) {
|
|
7133
|
+
selectedQaLogId = id;
|
|
7134
|
+
var container = document.getElementById("qa-detail-" + id);
|
|
7135
|
+
if (!container) return;
|
|
7136
|
+
container.innerHTML = '<div class="empty-state">\u6B63\u5728\u52A0\u8F7D\u95EE\u7B54\u8BE6\u60C5...</div>';
|
|
7137
|
+
try {
|
|
7138
|
+
var item = await fetchJson("/api/qa-logs/" + encodeURIComponent(id));
|
|
7139
|
+
renderQaLogDetail(item);
|
|
7140
|
+
} catch (error) {
|
|
7141
|
+
container.innerHTML = '<div class="empty-state">\u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25\uFF1A' + escapeHtml(error instanceof Error ? error.message : String(error)) + '</div>';
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7144
|
+
|
|
7145
|
+
function renderQaLogDetail(item) {
|
|
7146
|
+
var container = document.getElementById("qa-detail-" + item.id);
|
|
7147
|
+
if (!container) return;
|
|
7148
|
+
var trace = item.trace || {};
|
|
7149
|
+
var html = '<div class="content-panel" style="margin-top:var(--space-sm);background:rgba(255,255,255,0.03);">';
|
|
7150
|
+
html += '<h3 style="font-size:15px;margin-bottom:var(--space-sm);">\u95EE\u7B54\u8BE6\u60C5</h3>';
|
|
7151
|
+
html += '<div class="message-meta" style="margin-bottom:var(--space-sm);"><span>\u72B6\u6001\uFF1A' + escapeHtml(item.status) + '</span><span>\u521B\u5EFA\uFF1A' + escapeHtml(formatDateTime(item.createdAt)) + '</span><span>\u8017\u65F6\uFF1A' + escapeHtml(trace.durationMs == null ? '-' : trace.durationMs + 'ms') + '</span></div>';
|
|
7152
|
+
if (item.error) html += '<div style="color:var(--danger);margin-bottom:var(--space-sm);">\u9519\u8BEF\uFF1A' + escapeHtml(item.error) + '</div>';
|
|
7153
|
+
html += '<div class="qa-question">' + escapeHtml(item.question) + '</div>';
|
|
7154
|
+
html += '<div class="qa-answer" style="margin-bottom:var(--space-md);">' + escapeHtml(item.answer) + '</div>';
|
|
7155
|
+
if (!item.hasTrace) {
|
|
7156
|
+
html += '<div class="empty-state">\u8FD9\u6761\u95EE\u7B54\u6CA1\u6709 trace\uFF0C\u53EF\u80FD\u6765\u81EA\u65E7\u7248\u672C\u8BB0\u5F55\u3002</div></div>';
|
|
7157
|
+
container.innerHTML = html;
|
|
7158
|
+
return;
|
|
7159
|
+
}
|
|
7160
|
+
var turns = trace.modelTurns || [];
|
|
7161
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Reasoning</h4>';
|
|
7162
|
+
if (turns.length === 0) html += '<div class="empty-state">\u65E0 reasoningContent</div>';
|
|
7163
|
+
for (var i = 0; i < turns.length; i++) {
|
|
7164
|
+
html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>\u6A21\u578B\u8F6E\u6B21 ' + escapeHtml(turns[i].index) + '</span><span>' + escapeHtml(formatDateTime(turns[i].createdAt)) + '</span></div>' + renderTextBlock(turns[i].reasoningContent || '\u65E0 reasoningContent') + '</div>';
|
|
7165
|
+
}
|
|
7166
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u6A21\u578B\u8F6E\u6B21\u4E0E\u5DE5\u5177\u8C03\u7528</h4>';
|
|
7167
|
+
for (var j = 0; j < turns.length; j++) {
|
|
7168
|
+
html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>\u8F6E\u6B21 ' + escapeHtml(turns[j].index) + '</span></div>' + renderTextBlock(turns[j].content || '') + renderJson(turns[j].toolCalls || []) + '</div>';
|
|
7169
|
+
}
|
|
7170
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5DE5\u5177\u7ED3\u679C</h4>';
|
|
7171
|
+
var toolResults = trace.toolResults || [];
|
|
7172
|
+
if (toolResults.length === 0) html += '<div class="empty-state">\u6CA1\u6709\u5DE5\u5177\u7ED3\u679C\u3002</div>';
|
|
7173
|
+
for (var k = 0; k < toolResults.length; k++) {
|
|
7174
|
+
html += '<div style="margin-bottom:var(--space-sm);"><div class="message-meta"><span>' + escapeHtml(toolResults[k].name) + '</span><span>' + escapeHtml(toolResults[k].toolCallId) + '</span><span>' + escapeHtml(formatDateTime(toolResults[k].createdAt)) + '</span></div>' + renderJson(toolResults[k].input) + (toolResults[k].error ? '<div style="color:var(--danger);">' + escapeHtml(toolResults[k].error) + '</div>' : renderTextBlock(toolResults[k].content || '')) + '</div>';
|
|
7175
|
+
}
|
|
7176
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">\u5F15\u7528\u4E0E\u68C0\u7D22</h4>' + renderJson({ citations: item.citations || [], retrievalDebug: item.retrievalDebug || {} });
|
|
7177
|
+
html += '<h4 style="margin:var(--space-md) 0 var(--space-sm);">Fallback</h4>';
|
|
7178
|
+
var fallbacks = trace.fallbacks || [];
|
|
7179
|
+
html += fallbacks.length === 0 ? '<div class="empty-state">\u6CA1\u6709 fallback\u3002</div>' : renderJson(fallbacks);
|
|
7180
|
+
html += '</div>';
|
|
7181
|
+
container.innerHTML = html;
|
|
7182
|
+
}
|
|
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>';
|
|
6205
7270
|
}
|
|
6206
7271
|
el.innerHTML = html;
|
|
6207
7272
|
}
|
|
@@ -6236,11 +7301,19 @@ function buildHtml() {
|
|
|
6236
7301
|
await loadSection("/api/file-jobs", function(data) { allFileJobs = data.items || []; });
|
|
6237
7302
|
await loadSection("/api/qa-logs?limit=20", function(data) { allQaLogs = data.items || []; });
|
|
6238
7303
|
await loadSection("/api/cron-jobs", function(data) { allCronJobs = data.items || []; });
|
|
7304
|
+
await loadSection("/api/persons", function(data) { allPersons = data.items || []; });
|
|
6239
7305
|
if (currentView === "messages") renderMessagesView();
|
|
6240
7306
|
if (currentView === "episodes") renderEpisodesView();
|
|
6241
7307
|
if (currentView === "files") renderFilesView();
|
|
6242
7308
|
if (currentView === "tasks") renderTasksView();
|
|
6243
|
-
if (currentView === "qa-logs")
|
|
7309
|
+
if (currentView === "qa-logs") {
|
|
7310
|
+
renderQaLogsView();
|
|
7311
|
+
if (selectedQaLogId) void showQaLogDetail(selectedQaLogId);
|
|
7312
|
+
}
|
|
7313
|
+
if (currentView === "persons") {
|
|
7314
|
+
renderPersonsView();
|
|
7315
|
+
if (selectedPersonId) void showPersonProfile(selectedPersonId);
|
|
7316
|
+
}
|
|
6244
7317
|
}
|
|
6245
7318
|
|
|
6246
7319
|
async function processNow() {
|
|
@@ -6262,6 +7335,16 @@ function buildHtml() {
|
|
|
6262
7335
|
document.addEventListener("click", async function(event) {
|
|
6263
7336
|
var target = event.target;
|
|
6264
7337
|
if (!(target instanceof HTMLElement)) return;
|
|
7338
|
+
var qaLogId = target.dataset.viewQaLog;
|
|
7339
|
+
if (qaLogId) {
|
|
7340
|
+
void showQaLogDetail(qaLogId);
|
|
7341
|
+
return;
|
|
7342
|
+
}
|
|
7343
|
+
var personId = target.dataset.viewPerson || target.closest('[data-view-person]')?.dataset.viewPerson;
|
|
7344
|
+
if (personId) {
|
|
7345
|
+
void showPersonProfile(personId);
|
|
7346
|
+
return;
|
|
7347
|
+
}
|
|
6265
7348
|
var id = target.dataset.deleteCronJob;
|
|
6266
7349
|
if (!id) return;
|
|
6267
7350
|
target.disabled = true;
|
|
@@ -6281,7 +7364,7 @@ function buildHtml() {
|
|
|
6281
7364
|
</body>
|
|
6282
7365
|
</html>`;
|
|
6283
7366
|
}
|
|
6284
|
-
function
|
|
7367
|
+
function parseLimit2(value, fallback, max) {
|
|
6285
7368
|
const rawLimit = Number(value ?? fallback);
|
|
6286
7369
|
return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
|
|
6287
7370
|
}
|
|
@@ -6305,6 +7388,20 @@ function parseCookies(header) {
|
|
|
6305
7388
|
function isAuthorizedWebAction(request, token) {
|
|
6306
7389
|
return parseCookies(request.headers.cookie).chattercatcher_web_token === token;
|
|
6307
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
|
+
}
|
|
7401
|
+
function toQaLogListItem(log) {
|
|
7402
|
+
const { trace: _trace, ...item } = log;
|
|
7403
|
+
return item;
|
|
7404
|
+
}
|
|
6308
7405
|
function createWebApp(config, options = {}) {
|
|
6309
7406
|
const app = Fastify({ logger: false });
|
|
6310
7407
|
const database = openDatabase(config);
|
|
@@ -6314,11 +7411,12 @@ function createWebApp(config, options = {}) {
|
|
|
6314
7411
|
const fileJobs = new FileJobRepository(database);
|
|
6315
7412
|
const qaLogs = new QaLogRepository(database);
|
|
6316
7413
|
const cronJobs = new CronJobRepository(database);
|
|
7414
|
+
const profiles = new ProfileRepository(database);
|
|
6317
7415
|
let webActionToken = "";
|
|
6318
7416
|
const tokenReady = (async () => {
|
|
6319
7417
|
const secrets = await loadSecrets();
|
|
6320
7418
|
if (!secrets.web.actionToken) {
|
|
6321
|
-
secrets.web.actionToken =
|
|
7419
|
+
secrets.web.actionToken = crypto9.randomBytes(32).toString("hex");
|
|
6322
7420
|
await saveSecrets(secrets);
|
|
6323
7421
|
}
|
|
6324
7422
|
webActionToken = getWebActionToken(secrets);
|
|
@@ -6356,38 +7454,47 @@ function createWebApp(config, options = {}) {
|
|
|
6356
7454
|
items: messages.listChats()
|
|
6357
7455
|
}));
|
|
6358
7456
|
app.get("/api/files", async (request) => {
|
|
6359
|
-
const limit =
|
|
7457
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6360
7458
|
return {
|
|
6361
7459
|
items: messages.listFiles(limit)
|
|
6362
7460
|
};
|
|
6363
7461
|
});
|
|
6364
7462
|
app.get("/api/file-jobs", async (request) => {
|
|
6365
|
-
const limit =
|
|
7463
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6366
7464
|
const status = request.query.status;
|
|
6367
7465
|
return {
|
|
6368
7466
|
items: fileJobs.list(limit, status === "processing" || status === "indexed" || status === "failed" ? { status } : {})
|
|
6369
7467
|
};
|
|
6370
7468
|
});
|
|
6371
7469
|
app.get("/api/messages/recent", async (request) => {
|
|
6372
|
-
const limit =
|
|
7470
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6373
7471
|
return {
|
|
6374
7472
|
items: messages.listRecentMessages(limit)
|
|
6375
7473
|
};
|
|
6376
7474
|
});
|
|
6377
7475
|
app.get("/api/episodes", async (request) => {
|
|
6378
|
-
const limit =
|
|
7476
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6379
7477
|
return {
|
|
6380
7478
|
items: episodes.listRecentEpisodes(limit)
|
|
6381
7479
|
};
|
|
6382
7480
|
});
|
|
6383
7481
|
app.get("/api/qa-logs", async (request) => {
|
|
6384
|
-
const limit =
|
|
7482
|
+
const limit = parseLimit2(request.query.limit, 20, 100);
|
|
6385
7483
|
return {
|
|
6386
|
-
items: qaLogs.listRecent(limit)
|
|
7484
|
+
items: qaLogs.listRecent(limit).map(toQaLogListItem)
|
|
6387
7485
|
};
|
|
6388
7486
|
});
|
|
7487
|
+
app.get("/api/qa-logs/:id", async (request, reply) => {
|
|
7488
|
+
const id = request.params.id;
|
|
7489
|
+
const log = qaLogs.getById(id);
|
|
7490
|
+
if (!log) {
|
|
7491
|
+
reply.code(404);
|
|
7492
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u95EE\u7B54\u65E5\u5FD7\u3002" };
|
|
7493
|
+
}
|
|
7494
|
+
return log;
|
|
7495
|
+
});
|
|
6389
7496
|
app.get("/api/cron-jobs", async (request) => {
|
|
6390
|
-
const limit =
|
|
7497
|
+
const limit = parseLimit2(request.query.limit, 50, 200);
|
|
6391
7498
|
return {
|
|
6392
7499
|
items: cronJobs.list(limit)
|
|
6393
7500
|
};
|
|
@@ -6407,6 +7514,105 @@ function createWebApp(config, options = {}) {
|
|
|
6407
7514
|
const ok = cronJobs.deleteByChat(id, job.chatId);
|
|
6408
7515
|
return { ok };
|
|
6409
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
|
+
});
|
|
6410
7616
|
app.post("/api/process/messages", async (request, reply) => {
|
|
6411
7617
|
await tokenReady;
|
|
6412
7618
|
if (!isAuthorizedWebAction(request, webActionToken)) {
|