chattercatcher 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1452 -461
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +234 -129
- package/dist/index.js +1324 -328
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/docs/superpowers/plans/2026-05-02-agentic-rag.md +0 -1013
- package/docs/superpowers/plans/2026-05-02-image-multimodal-memory.md +0 -1888
- package/docs/superpowers/specs/2026-05-02-agentic-rag-design.md +0 -143
- package/docs/superpowers/specs/2026-05-02-image-multimodal-memory-design.md +0 -160
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs14 from "fs/promises";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "chattercatcher",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.20",
|
|
12
12
|
description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
|
|
13
13
|
type: "module",
|
|
14
14
|
main: "dist/index.js",
|
|
@@ -27,7 +27,9 @@ var package_default = {
|
|
|
27
27
|
files: [
|
|
28
28
|
"assets",
|
|
29
29
|
"dist",
|
|
30
|
-
"docs",
|
|
30
|
+
"docs/DEVELOPMENT_PLAN.md",
|
|
31
|
+
"docs/PRD.md",
|
|
32
|
+
"docs/TECHNICAL_ARCHITECTURE.md",
|
|
31
33
|
"README.md",
|
|
32
34
|
"AGENTS.md"
|
|
33
35
|
],
|
|
@@ -139,6 +141,12 @@ var appSecretsSchema = z.object({
|
|
|
139
141
|
z.object({
|
|
140
142
|
apiKey: z.string().default("")
|
|
141
143
|
})
|
|
144
|
+
),
|
|
145
|
+
web: z.preprocess(
|
|
146
|
+
(value) => value ?? {},
|
|
147
|
+
z.object({
|
|
148
|
+
actionToken: z.string().default("")
|
|
149
|
+
})
|
|
142
150
|
)
|
|
143
151
|
});
|
|
144
152
|
function createDefaultConfig() {
|
|
@@ -158,7 +166,8 @@ function createDefaultSecrets() {
|
|
|
158
166
|
feishu: {},
|
|
159
167
|
llm: {},
|
|
160
168
|
embedding: {},
|
|
161
|
-
multimodal: {}
|
|
169
|
+
multimodal: {},
|
|
170
|
+
web: {}
|
|
162
171
|
});
|
|
163
172
|
}
|
|
164
173
|
|
|
@@ -478,6 +487,22 @@ function migrateDatabase(database) {
|
|
|
478
487
|
CREATE INDEX IF NOT EXISTS message_chunk_embeddings_model_idx
|
|
479
488
|
ON message_chunk_embeddings(model, dimension);
|
|
480
489
|
|
|
490
|
+
CREATE TABLE IF NOT EXISTS qa_logs (
|
|
491
|
+
id TEXT PRIMARY KEY,
|
|
492
|
+
chat_id TEXT,
|
|
493
|
+
question_message_id TEXT,
|
|
494
|
+
question TEXT NOT NULL,
|
|
495
|
+
answer TEXT NOT NULL,
|
|
496
|
+
citations_json TEXT NOT NULL,
|
|
497
|
+
retrieval_debug_json TEXT NOT NULL,
|
|
498
|
+
status TEXT NOT NULL CHECK(status IN ('answered','failed')),
|
|
499
|
+
error TEXT,
|
|
500
|
+
created_at TEXT NOT NULL
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
CREATE INDEX IF NOT EXISTS qa_logs_created_at_idx ON qa_logs(created_at);
|
|
504
|
+
CREATE INDEX IF NOT EXISTS qa_logs_chat_idx ON qa_logs(chat_id, created_at);
|
|
505
|
+
|
|
481
506
|
CREATE TABLE IF NOT EXISTS file_jobs (
|
|
482
507
|
id TEXT PRIMARY KEY,
|
|
483
508
|
source_path TEXT NOT NULL,
|
|
@@ -511,6 +536,23 @@ function migrateDatabase(database) {
|
|
|
511
536
|
);
|
|
512
537
|
|
|
513
538
|
CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
|
|
539
|
+
|
|
540
|
+
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
541
|
+
id TEXT PRIMARY KEY,
|
|
542
|
+
chat_id TEXT NOT NULL,
|
|
543
|
+
created_by_open_id TEXT,
|
|
544
|
+
schedule TEXT NOT NULL,
|
|
545
|
+
prompt TEXT NOT NULL,
|
|
546
|
+
status TEXT NOT NULL CHECK(status IN ('active','deleted')),
|
|
547
|
+
last_run_at TEXT,
|
|
548
|
+
next_run_at TEXT NOT NULL,
|
|
549
|
+
last_error TEXT,
|
|
550
|
+
created_at TEXT NOT NULL,
|
|
551
|
+
updated_at TEXT NOT NULL
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
|
|
555
|
+
CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
|
|
514
556
|
`);
|
|
515
557
|
}
|
|
516
558
|
|
|
@@ -1130,6 +1172,22 @@ function buildSearchTerms(query) {
|
|
|
1130
1172
|
}
|
|
1131
1173
|
return [trimmed];
|
|
1132
1174
|
}
|
|
1175
|
+
function buildScopeWhere(scope) {
|
|
1176
|
+
const clauses = [];
|
|
1177
|
+
const params = [];
|
|
1178
|
+
if (scope?.platform) {
|
|
1179
|
+
clauses.push("m.platform = ?");
|
|
1180
|
+
params.push(scope.platform);
|
|
1181
|
+
}
|
|
1182
|
+
if (scope?.platformChatId) {
|
|
1183
|
+
clauses.push("c.platform_chat_id = ?");
|
|
1184
|
+
params.push(scope.platformChatId);
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
1188
|
+
params
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1133
1191
|
function parseRawPayload(value) {
|
|
1134
1192
|
try {
|
|
1135
1193
|
const parsed = JSON.parse(value);
|
|
@@ -1335,6 +1393,7 @@ var MessageRepository = class {
|
|
|
1335
1393
|
const ftsQuery = escapeFtsQuery(query);
|
|
1336
1394
|
const excludedIds = options.excludeMessageIds ?? [];
|
|
1337
1395
|
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
1396
|
+
const scope = buildScopeWhere(options.scope);
|
|
1338
1397
|
const ftsResults = this.database.prepare(
|
|
1339
1398
|
`
|
|
1340
1399
|
SELECT
|
|
@@ -1353,10 +1412,11 @@ var MessageRepository = class {
|
|
|
1353
1412
|
JOIN chats c ON c.id = m.chat_id
|
|
1354
1413
|
WHERE message_chunks_fts MATCH ?
|
|
1355
1414
|
${excludedWhere}
|
|
1415
|
+
${scope.where}
|
|
1356
1416
|
ORDER BY bm25(message_chunks_fts)
|
|
1357
1417
|
LIMIT ?
|
|
1358
1418
|
`
|
|
1359
|
-
).all(ftsQuery, ...excludedIds, limit);
|
|
1419
|
+
).all(ftsQuery, ...excludedIds, ...scope.params, limit);
|
|
1360
1420
|
if (ftsResults.length > 0) {
|
|
1361
1421
|
return ftsResults;
|
|
1362
1422
|
}
|
|
@@ -1384,10 +1444,11 @@ var MessageRepository = class {
|
|
|
1384
1444
|
JOIN chats c ON c.id = m.chat_id
|
|
1385
1445
|
WHERE (${where})
|
|
1386
1446
|
${likeExcludedWhere}
|
|
1447
|
+
${scope.where}
|
|
1387
1448
|
ORDER BY m.sent_at DESC
|
|
1388
1449
|
LIMIT ?
|
|
1389
1450
|
`
|
|
1390
|
-
).all(...params, ...excludedIds, limit);
|
|
1451
|
+
).all(...params, ...excludedIds, ...scope.params, limit);
|
|
1391
1452
|
}
|
|
1392
1453
|
getChatCount() {
|
|
1393
1454
|
return this.database.prepare("SELECT COUNT(*) AS count FROM chats").get().count;
|
|
@@ -1487,6 +1548,22 @@ function toMillis(value) {
|
|
|
1487
1548
|
const time = Date.parse(value);
|
|
1488
1549
|
return Number.isFinite(time) ? time : 0;
|
|
1489
1550
|
}
|
|
1551
|
+
function buildScopeWhere2(scope) {
|
|
1552
|
+
const clauses = [];
|
|
1553
|
+
const params = [];
|
|
1554
|
+
if (scope?.platform) {
|
|
1555
|
+
clauses.push("c.platform = ?");
|
|
1556
|
+
params.push(scope.platform);
|
|
1557
|
+
}
|
|
1558
|
+
if (scope?.platformChatId) {
|
|
1559
|
+
clauses.push("c.platform_chat_id = ?");
|
|
1560
|
+
params.push(scope.platformChatId);
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
1564
|
+
params
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1490
1567
|
var EpisodeRepository = class {
|
|
1491
1568
|
constructor(database) {
|
|
1492
1569
|
this.database = database;
|
|
@@ -1669,8 +1746,9 @@ var EpisodeRepository = class {
|
|
|
1669
1746
|
`
|
|
1670
1747
|
).all(limit);
|
|
1671
1748
|
}
|
|
1672
|
-
searchEpisodes(query, limit = 8) {
|
|
1749
|
+
searchEpisodes(query, limit = 8, scope) {
|
|
1673
1750
|
const ftsQuery = escapeFtsQuery2(query);
|
|
1751
|
+
const scopeWhere = buildScopeWhere2(scope);
|
|
1674
1752
|
return this.database.prepare(
|
|
1675
1753
|
`
|
|
1676
1754
|
SELECT
|
|
@@ -1698,11 +1776,12 @@ var EpisodeRepository = class {
|
|
|
1698
1776
|
JOIN memory_episodes e ON e.id = fts.episode_id
|
|
1699
1777
|
JOIN chats c ON c.id = e.chat_id
|
|
1700
1778
|
WHERE memory_episodes_fts MATCH ?
|
|
1779
|
+
${scopeWhere.where}
|
|
1701
1780
|
GROUP BY e.id
|
|
1702
1781
|
ORDER BY e.ended_at DESC
|
|
1703
1782
|
LIMIT ?
|
|
1704
1783
|
`
|
|
1705
|
-
).all(ftsQuery, limit).map((row) => {
|
|
1784
|
+
).all(ftsQuery, ...scopeWhere.params, limit).map((row) => {
|
|
1706
1785
|
const item = row;
|
|
1707
1786
|
return {
|
|
1708
1787
|
...item,
|
|
@@ -1732,8 +1811,8 @@ var EpisodeFtsRetriever = class {
|
|
|
1732
1811
|
this.episodes = episodes;
|
|
1733
1812
|
}
|
|
1734
1813
|
episodes;
|
|
1735
|
-
async retrieve(question) {
|
|
1736
|
-
return this.episodes.searchEpisodes(question, 8).map(toEpisodeEvidence);
|
|
1814
|
+
async retrieve(question, scope) {
|
|
1815
|
+
return this.episodes.searchEpisodes(question, 8, scope).map(toEpisodeEvidence);
|
|
1737
1816
|
}
|
|
1738
1817
|
};
|
|
1739
1818
|
|
|
@@ -1759,8 +1838,9 @@ var HybridRetriever = class {
|
|
|
1759
1838
|
}
|
|
1760
1839
|
retrievers;
|
|
1761
1840
|
options;
|
|
1762
|
-
async retrieve(question) {
|
|
1763
|
-
const
|
|
1841
|
+
async retrieve(question, scope) {
|
|
1842
|
+
const effectiveScope = scope ?? this.options.scope;
|
|
1843
|
+
const results = await Promise.all(this.retrievers.map((retriever) => retriever.retrieve(question, effectiveScope)));
|
|
1764
1844
|
const merged = /* @__PURE__ */ new Map();
|
|
1765
1845
|
for (const evidenceList of results) {
|
|
1766
1846
|
for (const evidence of evidenceList) {
|
|
@@ -1801,9 +1881,10 @@ var MessageFtsRetriever = class {
|
|
|
1801
1881
|
}
|
|
1802
1882
|
messages;
|
|
1803
1883
|
options;
|
|
1804
|
-
async retrieve(question) {
|
|
1884
|
+
async retrieve(question, scope) {
|
|
1805
1885
|
const results = this.messages.searchMessages(question, 8, {
|
|
1806
|
-
excludeMessageIds: this.options.excludeMessageIds
|
|
1886
|
+
excludeMessageIds: this.options.excludeMessageIds,
|
|
1887
|
+
scope
|
|
1807
1888
|
});
|
|
1808
1889
|
return results.map((result) => ({
|
|
1809
1890
|
id: result.chunkId,
|
|
@@ -1838,17 +1919,17 @@ function parseSearchInput(input2) {
|
|
|
1838
1919
|
const limit = Math.min(12, Math.max(1, Math.floor(numericLimit)));
|
|
1839
1920
|
return { query, limit };
|
|
1840
1921
|
}
|
|
1841
|
-
async function runRetriever(retriever, input2) {
|
|
1922
|
+
async function runRetriever(retriever, input2, scope) {
|
|
1842
1923
|
const { query, limit } = parseSearchInput(input2);
|
|
1843
|
-
const results = await retriever.retrieve(query);
|
|
1924
|
+
const results = await retriever.retrieve(query, scope);
|
|
1844
1925
|
return results.slice(0, limit);
|
|
1845
1926
|
}
|
|
1846
|
-
function createSearchTool(name, description, retriever) {
|
|
1927
|
+
function createSearchTool(name, description, retriever, scope) {
|
|
1847
1928
|
return {
|
|
1848
1929
|
name,
|
|
1849
1930
|
description,
|
|
1850
1931
|
inputSchema: searchInputSchema,
|
|
1851
|
-
execute: (input2) => runRetriever(retriever, input2)
|
|
1932
|
+
execute: (input2) => runRetriever(retriever, input2, scope)
|
|
1852
1933
|
};
|
|
1853
1934
|
}
|
|
1854
1935
|
function createRagSearchTools(input2) {
|
|
@@ -1856,17 +1937,20 @@ function createRagSearchTools(input2) {
|
|
|
1856
1937
|
createSearchTool(
|
|
1857
1938
|
"hybrid_search",
|
|
1858
1939
|
"Search across all indexed RAG evidence using the default hybrid retrieval strategy.",
|
|
1859
|
-
input2.hybrid
|
|
1940
|
+
input2.hybrid,
|
|
1941
|
+
input2.scope
|
|
1860
1942
|
),
|
|
1861
1943
|
createSearchTool(
|
|
1862
1944
|
"search_messages",
|
|
1863
1945
|
"Search chat messages only when the answer likely depends on message-level evidence.",
|
|
1864
|
-
input2.messages
|
|
1946
|
+
input2.messages,
|
|
1947
|
+
input2.scope
|
|
1865
1948
|
),
|
|
1866
1949
|
createSearchTool(
|
|
1867
1950
|
"search_episodes",
|
|
1868
1951
|
"Search episode summaries only when the answer likely depends on longer-running context.",
|
|
1869
|
-
input2.episodes
|
|
1952
|
+
input2.episodes,
|
|
1953
|
+
input2.scope
|
|
1870
1954
|
)
|
|
1871
1955
|
];
|
|
1872
1956
|
if (input2.semantic) {
|
|
@@ -1874,7 +1958,8 @@ function createRagSearchTools(input2) {
|
|
|
1874
1958
|
createSearchTool(
|
|
1875
1959
|
"semantic_search",
|
|
1876
1960
|
"Search semantic vector evidence only when broader conceptual recall is needed.",
|
|
1877
|
-
input2.semantic
|
|
1961
|
+
input2.semantic,
|
|
1962
|
+
input2.scope
|
|
1878
1963
|
)
|
|
1879
1964
|
);
|
|
1880
1965
|
}
|
|
@@ -1919,6 +2004,22 @@ function toEvidenceSource2(row) {
|
|
|
1919
2004
|
timestamp: row.sentAt
|
|
1920
2005
|
};
|
|
1921
2006
|
}
|
|
2007
|
+
function buildScopeWhere3(scope) {
|
|
2008
|
+
const clauses = [];
|
|
2009
|
+
const params = [];
|
|
2010
|
+
if (scope?.platform) {
|
|
2011
|
+
clauses.push("m.platform = ?");
|
|
2012
|
+
params.push(scope.platform);
|
|
2013
|
+
}
|
|
2014
|
+
if (scope?.platformChatId) {
|
|
2015
|
+
clauses.push("c.platform_chat_id = ?");
|
|
2016
|
+
params.push(scope.platformChatId);
|
|
2017
|
+
}
|
|
2018
|
+
return {
|
|
2019
|
+
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
2020
|
+
params
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
1922
2023
|
var SqliteVectorStore = class {
|
|
1923
2024
|
constructor(database, options) {
|
|
1924
2025
|
this.database = database;
|
|
@@ -1953,10 +2054,11 @@ var SqliteVectorStore = class {
|
|
|
1953
2054
|
});
|
|
1954
2055
|
transaction(records);
|
|
1955
2056
|
}
|
|
1956
|
-
async search(vector, limit) {
|
|
2057
|
+
async search(vector, limit, scope) {
|
|
1957
2058
|
if (limit <= 0) {
|
|
1958
2059
|
return [];
|
|
1959
2060
|
}
|
|
2061
|
+
const scopeWhere = buildScopeWhere3(scope);
|
|
1960
2062
|
const rows = this.database.prepare(
|
|
1961
2063
|
`
|
|
1962
2064
|
SELECT
|
|
@@ -1971,8 +2073,9 @@ var SqliteVectorStore = class {
|
|
|
1971
2073
|
JOIN messages m ON m.id = mc.message_id
|
|
1972
2074
|
JOIN chats c ON c.id = m.chat_id
|
|
1973
2075
|
WHERE e.model = ?
|
|
2076
|
+
${scopeWhere.where}
|
|
1974
2077
|
`
|
|
1975
|
-
).all(this.options.model);
|
|
2078
|
+
).all(this.options.model, ...scopeWhere.params);
|
|
1976
2079
|
return rows.flatMap((row) => {
|
|
1977
2080
|
const storedVector = parseEmbeddingJson(row.embeddingJson);
|
|
1978
2081
|
if (storedVector.length === 0) {
|
|
@@ -2004,9 +2107,9 @@ var VectorRetriever = class {
|
|
|
2004
2107
|
embedding;
|
|
2005
2108
|
store;
|
|
2006
2109
|
limit;
|
|
2007
|
-
async retrieve(question) {
|
|
2110
|
+
async retrieve(question, scope) {
|
|
2008
2111
|
const vector = await this.embedding.embed(question);
|
|
2009
|
-
return this.store.search(vector, this.limit);
|
|
2112
|
+
return this.store.search(vector, this.limit, scope);
|
|
2010
2113
|
}
|
|
2011
2114
|
};
|
|
2012
2115
|
|
|
@@ -2027,7 +2130,7 @@ async function createHybridRetriever(input2) {
|
|
|
2027
2130
|
retrievers.push(new VectorRetriever(createEmbeddingModel(input2.config, input2.secrets), vectorStore));
|
|
2028
2131
|
}
|
|
2029
2132
|
return {
|
|
2030
|
-
retriever: new HybridRetriever(retrievers),
|
|
2133
|
+
retriever: new HybridRetriever(retrievers, { scope: input2.scope }),
|
|
2031
2134
|
close: () => {
|
|
2032
2135
|
for (const closer of closers) {
|
|
2033
2136
|
closer();
|
|
@@ -2044,7 +2147,7 @@ async function createAgenticRagSearchTools(input2) {
|
|
|
2044
2147
|
) : void 0;
|
|
2045
2148
|
const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
|
|
2046
2149
|
return {
|
|
2047
|
-
tools: createRagSearchTools({ hybrid, messages, episodes, semantic }),
|
|
2150
|
+
tools: createRagSearchTools({ hybrid, messages, episodes, semantic, scope: input2.scope }),
|
|
2048
2151
|
close: () => {
|
|
2049
2152
|
}
|
|
2050
2153
|
};
|
|
@@ -2617,182 +2720,773 @@ async function ensureFeishuBotOpenId(config, secrets, options = {}) {
|
|
|
2617
2720
|
// src/feishu/gateway.ts
|
|
2618
2721
|
import * as lark2 from "@larksuiteoapi/node-sdk";
|
|
2619
2722
|
|
|
2620
|
-
// src/
|
|
2723
|
+
// src/cron/jobs.ts
|
|
2621
2724
|
import crypto4 from "crypto";
|
|
2622
|
-
|
|
2623
|
-
|
|
2725
|
+
|
|
2726
|
+
// src/cron/schedule.ts
|
|
2727
|
+
function isValidCronSchedule(schedule) {
|
|
2728
|
+
return parseCronSchedule(schedule) !== null;
|
|
2624
2729
|
}
|
|
2625
|
-
function
|
|
2626
|
-
|
|
2730
|
+
function getNextCronRun(schedule, after) {
|
|
2731
|
+
const parsed = parseCronSchedule(schedule);
|
|
2732
|
+
if (!parsed) {
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2735
|
+
const candidate = new Date(after);
|
|
2736
|
+
candidate.setSeconds(0, 0);
|
|
2737
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
2738
|
+
const maxMinutes = 5 * 366 * 24 * 60;
|
|
2739
|
+
for (let i = 0; i < maxMinutes; i += 1) {
|
|
2740
|
+
if (matchesParsedSchedule(parsed, candidate)) {
|
|
2741
|
+
return new Date(candidate);
|
|
2742
|
+
}
|
|
2743
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
2744
|
+
}
|
|
2745
|
+
return null;
|
|
2627
2746
|
}
|
|
2628
|
-
function
|
|
2629
|
-
|
|
2630
|
-
|
|
2747
|
+
function matchesParsedSchedule(schedule, date) {
|
|
2748
|
+
const dayOfMonthMatches = schedule.dayOfMonth.matches(date.getDate());
|
|
2749
|
+
const dayOfWeekMatches = schedule.dayOfWeek.matches(date.getDay());
|
|
2750
|
+
const dayMatches = schedule.dayOfMonth.wildcard || schedule.dayOfWeek.wildcard ? dayOfMonthMatches && dayOfWeekMatches : dayOfMonthMatches || dayOfWeekMatches;
|
|
2751
|
+
return schedule.minute.matches(date.getMinutes()) && schedule.hour.matches(date.getHours()) && dayMatches && schedule.month.matches(date.getMonth() + 1);
|
|
2752
|
+
}
|
|
2753
|
+
function parseCronSchedule(schedule) {
|
|
2754
|
+
const fields = schedule.trim().split(/\s+/);
|
|
2755
|
+
if (fields.length !== 5) {
|
|
2756
|
+
return null;
|
|
2631
2757
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
...row.last_error ? { lastError: row.last_error } : {},
|
|
2642
|
-
...row.derived_message_id ? { derivedMessageId: row.derived_message_id } : {},
|
|
2643
|
-
createdAt: row.created_at,
|
|
2644
|
-
updatedAt: row.updated_at
|
|
2645
|
-
};
|
|
2758
|
+
const minute = parseMinuteField(fields[0]);
|
|
2759
|
+
const hour = parseExactOrWildcardField(fields[1], 0, 23);
|
|
2760
|
+
const dayOfMonth = parseExactOrWildcardField(fields[2], 1, 31);
|
|
2761
|
+
const month = parseExactOrWildcardField(fields[3], 1, 12);
|
|
2762
|
+
const dayOfWeek = parseExactOrWildcardField(fields[4], 0, 6);
|
|
2763
|
+
if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
return { minute, hour, dayOfMonth, month, dayOfWeek };
|
|
2646
2767
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2768
|
+
function parseMinuteField(field) {
|
|
2769
|
+
if (field === "*") {
|
|
2770
|
+
return { wildcard: true, matches: () => true };
|
|
2771
|
+
}
|
|
2772
|
+
const stepMatch = /^\*\/(\d+)$/.exec(field);
|
|
2773
|
+
if (stepMatch) {
|
|
2774
|
+
const step = Number(stepMatch[1]);
|
|
2775
|
+
if (!Number.isInteger(step) || step <= 0 || step > 59) {
|
|
2776
|
+
return null;
|
|
2777
|
+
}
|
|
2778
|
+
return { wildcard: false, matches: (value) => value % step === 0 };
|
|
2779
|
+
}
|
|
2780
|
+
if (field.includes(",")) {
|
|
2781
|
+
const values = field.split(",").map((part) => parseExactNumber(part, 0, 59));
|
|
2782
|
+
if (values.some((value) => value === null)) {
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
const allowed = new Set(values);
|
|
2786
|
+
return { wildcard: false, matches: (value) => allowed.has(value) };
|
|
2787
|
+
}
|
|
2788
|
+
const exact = parseExactNumber(field, 0, 59);
|
|
2789
|
+
if (exact === null) {
|
|
2790
|
+
return null;
|
|
2791
|
+
}
|
|
2792
|
+
return { wildcard: false, matches: (value) => value === exact };
|
|
2793
|
+
}
|
|
2794
|
+
function parseExactOrWildcardField(field, min, max) {
|
|
2795
|
+
if (field === "*") {
|
|
2796
|
+
return { wildcard: true, matches: () => true };
|
|
2797
|
+
}
|
|
2798
|
+
const exact = parseExactNumber(field, min, max);
|
|
2799
|
+
if (exact === null) {
|
|
2800
|
+
return null;
|
|
2801
|
+
}
|
|
2802
|
+
return { wildcard: false, matches: (value) => value === exact };
|
|
2803
|
+
}
|
|
2804
|
+
function parseExactNumber(field, min, max) {
|
|
2805
|
+
if (!/^\d+$/.test(field)) {
|
|
2806
|
+
return null;
|
|
2807
|
+
}
|
|
2808
|
+
const value = Number(field);
|
|
2809
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
2810
|
+
return null;
|
|
2811
|
+
}
|
|
2812
|
+
return value;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/cron/jobs.ts
|
|
2816
|
+
var CronJobRepository = class {
|
|
2817
|
+
constructor(database, options = {}) {
|
|
2649
2818
|
this.database = database;
|
|
2819
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2650
2820
|
}
|
|
2651
2821
|
database;
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
const
|
|
2822
|
+
now;
|
|
2823
|
+
create(input2) {
|
|
2824
|
+
const schedule = input2.schedule.trim();
|
|
2825
|
+
const prompt = input2.prompt.trim();
|
|
2826
|
+
if (!isValidCronSchedule(schedule)) {
|
|
2827
|
+
throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
|
|
2828
|
+
}
|
|
2829
|
+
if (!prompt) {
|
|
2830
|
+
throw new Error("\u5B9A\u65F6\u4EFB\u52A1 prompt \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
2831
|
+
}
|
|
2832
|
+
const now = this.now();
|
|
2833
|
+
const nextRunAt = getNextCronRun(schedule, now);
|
|
2834
|
+
if (!nextRunAt) {
|
|
2835
|
+
throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
|
|
2836
|
+
}
|
|
2837
|
+
const record = {
|
|
2838
|
+
id: crypto4.randomUUID(),
|
|
2839
|
+
chatId: input2.chatId,
|
|
2840
|
+
createdByOpenId: input2.createdByOpenId,
|
|
2841
|
+
schedule,
|
|
2842
|
+
prompt,
|
|
2843
|
+
status: "active",
|
|
2844
|
+
nextRunAt: nextRunAt.toISOString(),
|
|
2845
|
+
createdAt: now.toISOString(),
|
|
2846
|
+
updatedAt: now.toISOString()
|
|
2847
|
+
};
|
|
2655
2848
|
this.database.prepare(
|
|
2656
2849
|
`
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
updated_at
|
|
2668
|
-
)
|
|
2669
|
-
VALUES (
|
|
2670
|
-
@id,
|
|
2671
|
-
@sourceMessageId,
|
|
2672
|
-
@platformMessageId,
|
|
2673
|
-
@imageKey,
|
|
2674
|
-
@storedPath,
|
|
2675
|
-
@mimeType,
|
|
2676
|
-
'pending',
|
|
2677
|
-
0,
|
|
2678
|
-
@createdAt,
|
|
2679
|
-
@updatedAt
|
|
2680
|
-
)
|
|
2681
|
-
ON CONFLICT(source_message_id, image_key)
|
|
2682
|
-
DO UPDATE SET
|
|
2683
|
-
platform_message_id = excluded.platform_message_id,
|
|
2684
|
-
stored_path = excluded.stored_path,
|
|
2685
|
-
mime_type = excluded.mime_type,
|
|
2686
|
-
status = 'pending',
|
|
2687
|
-
attempts = 0,
|
|
2688
|
-
last_error = NULL,
|
|
2689
|
-
derived_message_id = NULL,
|
|
2690
|
-
updated_at = excluded.updated_at
|
|
2691
|
-
`
|
|
2692
|
-
).run({
|
|
2693
|
-
id,
|
|
2694
|
-
sourceMessageId: input2.sourceMessageId,
|
|
2695
|
-
platformMessageId: input2.platformMessageId,
|
|
2696
|
-
imageKey: input2.imageKey,
|
|
2697
|
-
storedPath: input2.storedPath,
|
|
2698
|
-
mimeType: input2.mimeType,
|
|
2699
|
-
createdAt: timestamp,
|
|
2700
|
-
updatedAt: timestamp
|
|
2701
|
-
});
|
|
2702
|
-
const record = this.getById(id);
|
|
2703
|
-
if (!record) {
|
|
2704
|
-
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u5199\u5165\u5931\u8D25\uFF1A${id}`);
|
|
2705
|
-
}
|
|
2850
|
+
INSERT INTO cron_jobs (
|
|
2851
|
+
id, chat_id, created_by_open_id, schedule, prompt, status,
|
|
2852
|
+
last_run_at, next_run_at, last_error, created_at, updated_at
|
|
2853
|
+
)
|
|
2854
|
+
VALUES (
|
|
2855
|
+
@id, @chatId, @createdByOpenId, @schedule, @prompt, @status,
|
|
2856
|
+
NULL, @nextRunAt, NULL, @createdAt, @updatedAt
|
|
2857
|
+
)
|
|
2858
|
+
`
|
|
2859
|
+
).run(record);
|
|
2706
2860
|
return record;
|
|
2707
2861
|
}
|
|
2708
|
-
|
|
2862
|
+
get(id) {
|
|
2863
|
+
return this.listByWhere("WHERE id = ?", [id], 1)[0] ?? null;
|
|
2864
|
+
}
|
|
2865
|
+
list(limit = 100) {
|
|
2866
|
+
return this.listByWhere("", [], limit);
|
|
2867
|
+
}
|
|
2868
|
+
listByChat(chatId, limit = 50) {
|
|
2869
|
+
return this.listByWhere(
|
|
2870
|
+
"WHERE chat_id = ? AND status = 'active'",
|
|
2871
|
+
[chatId],
|
|
2872
|
+
limit
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
listDue(now, limit = 20) {
|
|
2709
2876
|
const rows = this.database.prepare(
|
|
2710
2877
|
`
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2878
|
+
SELECT
|
|
2879
|
+
id,
|
|
2880
|
+
chat_id AS chatId,
|
|
2881
|
+
created_by_open_id AS createdByOpenId,
|
|
2882
|
+
schedule,
|
|
2883
|
+
prompt,
|
|
2884
|
+
status,
|
|
2885
|
+
last_run_at AS lastRunAt,
|
|
2886
|
+
next_run_at AS nextRunAt,
|
|
2887
|
+
last_error AS lastError,
|
|
2888
|
+
created_at AS createdAt,
|
|
2889
|
+
updated_at AS updatedAt
|
|
2890
|
+
FROM cron_jobs
|
|
2891
|
+
WHERE status = 'active' AND next_run_at <= ?
|
|
2892
|
+
ORDER BY next_run_at ASC, updated_at ASC
|
|
2893
|
+
LIMIT ?
|
|
2894
|
+
`
|
|
2895
|
+
).all(now.toISOString(), limit);
|
|
2896
|
+
return rows.map((row) => ({
|
|
2897
|
+
id: row.id,
|
|
2898
|
+
chatId: row.chatId,
|
|
2899
|
+
createdByOpenId: row.createdByOpenId ?? void 0,
|
|
2900
|
+
schedule: row.schedule,
|
|
2901
|
+
prompt: row.prompt,
|
|
2902
|
+
status: row.status,
|
|
2903
|
+
lastRunAt: row.lastRunAt ?? void 0,
|
|
2904
|
+
nextRunAt: row.nextRunAt,
|
|
2905
|
+
lastError: row.lastError ?? void 0,
|
|
2906
|
+
createdAt: row.createdAt,
|
|
2907
|
+
updatedAt: row.updatedAt
|
|
2908
|
+
}));
|
|
2731
2909
|
}
|
|
2732
|
-
|
|
2910
|
+
deleteByChat(id, chatId) {
|
|
2911
|
+
const now = this.now().toISOString();
|
|
2733
2912
|
const result = this.database.prepare(
|
|
2734
2913
|
`
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
last_error = NULL,
|
|
2739
|
-
updated_at = @updatedAt
|
|
2740
|
-
WHERE id = @id AND status = 'pending'
|
|
2741
|
-
`
|
|
2742
|
-
).run({ id, updatedAt: nowIso4() });
|
|
2743
|
-
if (result.changes === 0) {
|
|
2744
|
-
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A${id}`);
|
|
2745
|
-
}
|
|
2746
|
-
return this.requireById(id);
|
|
2747
|
-
}
|
|
2748
|
-
markSucceeded(id, derivedMessageId) {
|
|
2749
|
-
this.database.prepare(
|
|
2914
|
+
UPDATE cron_jobs
|
|
2915
|
+
SET status = 'deleted', updated_at = @updatedAt
|
|
2916
|
+
WHERE id = @id AND chat_id = @chatId AND status = 'active'
|
|
2750
2917
|
`
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
last_error = NULL,
|
|
2754
|
-
derived_message_id = @derivedMessageId,
|
|
2755
|
-
updated_at = @updatedAt
|
|
2756
|
-
WHERE id = @id
|
|
2757
|
-
`
|
|
2758
|
-
).run({ id, derivedMessageId, updatedAt: nowIso4() });
|
|
2759
|
-
return this.requireById(id);
|
|
2918
|
+
).run({ id, chatId, updatedAt: now });
|
|
2919
|
+
return result.changes > 0;
|
|
2760
2920
|
}
|
|
2761
|
-
|
|
2921
|
+
markSuccess(id, ranAt) {
|
|
2922
|
+
const job = this.get(id);
|
|
2923
|
+
if (!job) {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
const nextRunAt = getNextCronRun(job.schedule, ranAt);
|
|
2927
|
+
if (!nextRunAt) {
|
|
2928
|
+
throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
|
|
2929
|
+
}
|
|
2762
2930
|
this.database.prepare(
|
|
2763
2931
|
`
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2932
|
+
UPDATE cron_jobs
|
|
2933
|
+
SET last_run_at = @lastRunAt, next_run_at = @nextRunAt, last_error = NULL, updated_at = @updatedAt
|
|
2934
|
+
WHERE id = @id AND status = 'active'
|
|
2935
|
+
`
|
|
2936
|
+
).run({
|
|
2937
|
+
id,
|
|
2938
|
+
lastRunAt: ranAt.toISOString(),
|
|
2939
|
+
nextRunAt: nextRunAt.toISOString(),
|
|
2940
|
+
updatedAt: ranAt.toISOString()
|
|
2941
|
+
});
|
|
2773
2942
|
}
|
|
2774
|
-
|
|
2943
|
+
markFailure(id, error, failedAt) {
|
|
2944
|
+
const job = this.get(id);
|
|
2945
|
+
if (!job) {
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
const nextRunAt = getNextCronRun(job.schedule, failedAt);
|
|
2949
|
+
if (!nextRunAt) {
|
|
2950
|
+
throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
|
|
2951
|
+
}
|
|
2775
2952
|
this.database.prepare(
|
|
2776
2953
|
`
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2954
|
+
UPDATE cron_jobs
|
|
2955
|
+
SET last_run_at = @lastRunAt, last_error = @lastError, next_run_at = @nextRunAt, updated_at = @updatedAt
|
|
2956
|
+
WHERE id = @id AND status = 'active'
|
|
2957
|
+
`
|
|
2958
|
+
).run({
|
|
2959
|
+
id,
|
|
2960
|
+
lastRunAt: failedAt.toISOString(),
|
|
2961
|
+
lastError: error,
|
|
2962
|
+
nextRunAt: nextRunAt.toISOString(),
|
|
2963
|
+
updatedAt: failedAt.toISOString()
|
|
2964
|
+
});
|
|
2786
2965
|
}
|
|
2787
|
-
|
|
2788
|
-
const
|
|
2966
|
+
listByWhere(whereSql, params, limit) {
|
|
2967
|
+
const rows = this.database.prepare(
|
|
2789
2968
|
`
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2969
|
+
SELECT
|
|
2970
|
+
id,
|
|
2971
|
+
chat_id AS chatId,
|
|
2972
|
+
created_by_open_id AS createdByOpenId,
|
|
2973
|
+
schedule,
|
|
2974
|
+
prompt,
|
|
2975
|
+
status,
|
|
2976
|
+
last_run_at AS lastRunAt,
|
|
2977
|
+
next_run_at AS nextRunAt,
|
|
2978
|
+
last_error AS lastError,
|
|
2979
|
+
created_at AS createdAt,
|
|
2980
|
+
updated_at AS updatedAt
|
|
2981
|
+
FROM cron_jobs
|
|
2982
|
+
${whereSql}
|
|
2983
|
+
ORDER BY updated_at DESC
|
|
2984
|
+
LIMIT ?
|
|
2985
|
+
`
|
|
2986
|
+
).all(...params, limit);
|
|
2987
|
+
return rows.map((row) => ({
|
|
2988
|
+
id: row.id,
|
|
2989
|
+
chatId: row.chatId,
|
|
2990
|
+
createdByOpenId: row.createdByOpenId ?? void 0,
|
|
2991
|
+
schedule: row.schedule,
|
|
2992
|
+
prompt: row.prompt,
|
|
2993
|
+
status: row.status,
|
|
2994
|
+
lastRunAt: row.lastRunAt ?? void 0,
|
|
2995
|
+
nextRunAt: row.nextRunAt,
|
|
2996
|
+
lastError: row.lastError ?? void 0,
|
|
2997
|
+
createdAt: row.createdAt,
|
|
2998
|
+
updatedAt: row.updatedAt
|
|
2999
|
+
}));
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
|
|
3003
|
+
// src/cron/generator.ts
|
|
3004
|
+
var SYSTEM_PROMPT = "\u4F60\u6B63\u5728\u4E3A\u98DE\u4E66\u7FA4\u751F\u6210\u4E00\u6761\u5B9A\u65F6\u6D88\u606F\u3002\u53EF\u4EE5\u5148\u8C03\u7528\u641C\u7D22\u5DE5\u5177\u68C0\u7D22\u672C\u5730\u7FA4\u804A\u77E5\u8BC6\u5E93\u3002\u6700\u7EC8\u8F93\u51FA\u5FC5\u987B\u662F\u53EF\u4EE5\u76F4\u63A5\u53D1\u5230\u7FA4\u91CC\u7684\u7EAF\u6587\u672C\uFF0C\u4E0D\u8981\u8F93\u51FA\u5DE5\u5177\u8C03\u7528\u8BF4\u660E\u3002";
|
|
3005
|
+
function evidenceToText(evidence) {
|
|
3006
|
+
if (evidence.length === 0) {
|
|
3007
|
+
return "\u65E0\u68C0\u7D22\u8BC1\u636E\u3002";
|
|
3008
|
+
}
|
|
3009
|
+
return evidence.map((item, index2) => `${index2 + 1}. ${item.text}`).join("\n");
|
|
3010
|
+
}
|
|
3011
|
+
function toolResultContent(results) {
|
|
3012
|
+
return JSON.stringify(results.map((item) => ({ id: item.id, text: item.text, score: item.score, source: item.source })));
|
|
3013
|
+
}
|
|
3014
|
+
async function generateCronJobMessage(input2) {
|
|
3015
|
+
if (!input2.model.completeWithTools) {
|
|
3016
|
+
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3017
|
+
}
|
|
3018
|
+
const messages = [
|
|
3019
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
3020
|
+
{ role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
|
|
3021
|
+
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}` }
|
|
3022
|
+
];
|
|
3023
|
+
const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
|
|
3024
|
+
const evidence = [];
|
|
3025
|
+
const maxModelTurns = input2.maxModelTurns ?? 3;
|
|
3026
|
+
const maxToolCalls = input2.maxToolCalls ?? 6;
|
|
3027
|
+
let toolCallsUsed = 0;
|
|
3028
|
+
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
3029
|
+
const result = await input2.model.completeWithTools(messages, input2.tools);
|
|
3030
|
+
messages.push({ role: "assistant", content: result.content, toolCalls: result.toolCalls });
|
|
3031
|
+
if (result.toolCalls.length === 0) {
|
|
3032
|
+
break;
|
|
3033
|
+
}
|
|
3034
|
+
for (const call of result.toolCalls) {
|
|
3035
|
+
if (toolCallsUsed >= maxToolCalls) {
|
|
3036
|
+
return input2.model.complete([
|
|
3037
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
3038
|
+
{
|
|
3039
|
+
role: "user",
|
|
3040
|
+
content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
|
|
3041
|
+
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}
|
|
3042
|
+
|
|
3043
|
+
\u8BC1\u636E\uFF1A
|
|
3044
|
+
${evidenceToText(evidence)}`
|
|
3045
|
+
}
|
|
3046
|
+
]);
|
|
3047
|
+
}
|
|
3048
|
+
toolCallsUsed += 1;
|
|
3049
|
+
const tool = toolsByName.get(call.name);
|
|
3050
|
+
if (!tool) {
|
|
3051
|
+
messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify({ error: `\u672A\u77E5\u5DE5\u5177\uFF1A${call.name}` }) });
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3054
|
+
try {
|
|
3055
|
+
const results = await tool.execute(call.input);
|
|
3056
|
+
evidence.push(...results);
|
|
3057
|
+
messages.push({ role: "tool", toolCallId: call.id, content: toolResultContent(results) });
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3060
|
+
messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify({ error: message }) });
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
return input2.model.complete([
|
|
3065
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
3066
|
+
{
|
|
3067
|
+
role: "user",
|
|
3068
|
+
content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
|
|
3069
|
+
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}
|
|
3070
|
+
|
|
3071
|
+
\u8BC1\u636E\uFF1A
|
|
3072
|
+
${evidenceToText(evidence)}`
|
|
3073
|
+
}
|
|
3074
|
+
]);
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// src/cron/scheduler.ts
|
|
3078
|
+
function createCronJobScheduler(options) {
|
|
3079
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3080
|
+
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
3081
|
+
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
3082
|
+
const logger = options.logger ?? console;
|
|
3083
|
+
let timer;
|
|
3084
|
+
let running = false;
|
|
3085
|
+
const runDueNow = async () => {
|
|
3086
|
+
if (running) {
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
running = true;
|
|
3090
|
+
const startedAt = now();
|
|
3091
|
+
try {
|
|
3092
|
+
const jobs = options.repository.listDue(startedAt);
|
|
3093
|
+
for (const job of jobs) {
|
|
3094
|
+
try {
|
|
3095
|
+
const text = await options.generateMessage(job, startedAt);
|
|
3096
|
+
await options.sendTextToChat(job.chatId, text);
|
|
3097
|
+
options.repository.markSuccess(job.id, startedAt);
|
|
3098
|
+
} catch (error) {
|
|
3099
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3100
|
+
options.repository.markFailure(job.id, message, startedAt);
|
|
3101
|
+
logger.error(`CRONJob \u6267\u884C\u5931\u8D25\uFF1A${job.id} ${message}`);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
} finally {
|
|
3105
|
+
running = false;
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
3108
|
+
return {
|
|
3109
|
+
start() {
|
|
3110
|
+
if (timer) {
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
void runDueNow();
|
|
3114
|
+
timer = setIntervalFn(() => {
|
|
3115
|
+
void runDueNow();
|
|
3116
|
+
}, 6e4);
|
|
3117
|
+
},
|
|
3118
|
+
stop() {
|
|
3119
|
+
if (!timer) {
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
clearIntervalFn(timer);
|
|
3123
|
+
timer = void 0;
|
|
3124
|
+
},
|
|
3125
|
+
runDueNow
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// src/gateway/indexing-scheduler.ts
|
|
3130
|
+
function createIndexingScheduler(options) {
|
|
3131
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3132
|
+
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
3133
|
+
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
3134
|
+
const logger = options.logger ?? console;
|
|
3135
|
+
const parsed = parseCronSchedule2(options.schedule);
|
|
3136
|
+
let timer;
|
|
3137
|
+
let running = false;
|
|
3138
|
+
const runDueNow = async () => {
|
|
3139
|
+
if (!parsed || running || !matchesParsedSchedule2(parsed, now())) {
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
running = true;
|
|
3143
|
+
try {
|
|
3144
|
+
await options.work();
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3147
|
+
logger.error(`\u5B9A\u65F6\u6D88\u606F\u7D22\u5F15\u5931\u8D25\uFF1A${message}`);
|
|
3148
|
+
} finally {
|
|
3149
|
+
running = false;
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
return {
|
|
3153
|
+
start() {
|
|
3154
|
+
if (!parsed || timer) {
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
timer = setIntervalFn(() => {
|
|
3158
|
+
void runDueNow();
|
|
3159
|
+
}, 6e4);
|
|
3160
|
+
},
|
|
3161
|
+
stop() {
|
|
3162
|
+
if (!timer) {
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
clearIntervalFn(timer);
|
|
3166
|
+
timer = void 0;
|
|
3167
|
+
},
|
|
3168
|
+
runDueNow
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
function matchesParsedSchedule2(schedule, date) {
|
|
3172
|
+
return schedule.minute(date.getMinutes()) && schedule.hour(date.getHours()) && schedule.dayOfMonth(date.getDate()) && schedule.month(date.getMonth() + 1) && schedule.dayOfWeek(date.getDay());
|
|
3173
|
+
}
|
|
3174
|
+
function parseCronSchedule2(schedule) {
|
|
3175
|
+
const fields = schedule.trim().split(/\s+/);
|
|
3176
|
+
if (fields.length !== 5) {
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
const minute = parseMinuteField2(fields[0]);
|
|
3180
|
+
const hour = parseExactOrWildcardField2(fields[1], 0, 23);
|
|
3181
|
+
const dayOfMonth = parseExactOrWildcardField2(fields[2], 1, 31);
|
|
3182
|
+
const month = parseExactOrWildcardField2(fields[3], 1, 12);
|
|
3183
|
+
const dayOfWeek = parseExactOrWildcardField2(fields[4], 0, 6);
|
|
3184
|
+
if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
|
|
3185
|
+
return null;
|
|
3186
|
+
}
|
|
3187
|
+
return { minute, hour, dayOfMonth, month, dayOfWeek };
|
|
3188
|
+
}
|
|
3189
|
+
function parseMinuteField2(field) {
|
|
3190
|
+
if (field === "*") {
|
|
3191
|
+
return () => true;
|
|
3192
|
+
}
|
|
3193
|
+
const stepMatch = /^\*\/(\d+)$/.exec(field);
|
|
3194
|
+
if (stepMatch) {
|
|
3195
|
+
const step = Number(stepMatch[1]);
|
|
3196
|
+
if (!Number.isInteger(step) || step <= 0 || step > 59) {
|
|
3197
|
+
return null;
|
|
3198
|
+
}
|
|
3199
|
+
return (value) => value % step === 0;
|
|
3200
|
+
}
|
|
3201
|
+
if (field.includes(",")) {
|
|
3202
|
+
const values = field.split(",").map((part) => parseExactNumber2(part, 0, 59));
|
|
3203
|
+
if (values.some((value) => value === null)) {
|
|
3204
|
+
return null;
|
|
3205
|
+
}
|
|
3206
|
+
const allowed = new Set(values);
|
|
3207
|
+
return (value) => allowed.has(value);
|
|
3208
|
+
}
|
|
3209
|
+
const exact = parseExactNumber2(field, 0, 59);
|
|
3210
|
+
if (exact === null) {
|
|
3211
|
+
return null;
|
|
3212
|
+
}
|
|
3213
|
+
return (value) => value === exact;
|
|
3214
|
+
}
|
|
3215
|
+
function parseExactOrWildcardField2(field, min, max) {
|
|
3216
|
+
if (field === "*") {
|
|
3217
|
+
return () => true;
|
|
3218
|
+
}
|
|
3219
|
+
const exact = parseExactNumber2(field, min, max);
|
|
3220
|
+
if (exact === null) {
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
return (value) => value === exact;
|
|
3224
|
+
}
|
|
3225
|
+
function parseExactNumber2(field, min, max) {
|
|
3226
|
+
if (!/^\d+$/.test(field)) {
|
|
3227
|
+
return null;
|
|
3228
|
+
}
|
|
3229
|
+
const value = Number(field);
|
|
3230
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
3231
|
+
return null;
|
|
3232
|
+
}
|
|
3233
|
+
return value;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
// src/rag/indexer.ts
|
|
3237
|
+
async function indexMessageChunks(input2) {
|
|
3238
|
+
const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
|
|
3239
|
+
if (chunks.length === 0) {
|
|
3240
|
+
return { chunks: 0, vectors: 0 };
|
|
3241
|
+
}
|
|
3242
|
+
const vectors = await input2.embedding.embedBatch(chunks.map((chunk) => chunk.text));
|
|
3243
|
+
const records = [];
|
|
3244
|
+
for (const [index2, chunk] of chunks.entries()) {
|
|
3245
|
+
const vector = vectors[index2];
|
|
3246
|
+
if (!vector || vector.length === 0) {
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
records.push({
|
|
3250
|
+
id: chunk.chunkId,
|
|
3251
|
+
vector,
|
|
3252
|
+
evidence: {
|
|
3253
|
+
id: chunk.chunkId,
|
|
3254
|
+
text: chunk.text,
|
|
3255
|
+
score: 1,
|
|
3256
|
+
source: toEvidenceSource3(chunk)
|
|
3257
|
+
}
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
await input2.store.upsert(records);
|
|
3261
|
+
return {
|
|
3262
|
+
chunks: chunks.length,
|
|
3263
|
+
vectors: records.length
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
function toEvidenceSource3(chunk) {
|
|
3267
|
+
if (chunk.messageType === "file") {
|
|
3268
|
+
return {
|
|
3269
|
+
type: "file",
|
|
3270
|
+
label: chunk.senderName,
|
|
3271
|
+
timestamp: chunk.sentAt
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
return {
|
|
3275
|
+
type: "message",
|
|
3276
|
+
label: chunk.chatName,
|
|
3277
|
+
sender: chunk.senderName,
|
|
3278
|
+
timestamp: chunk.sentAt
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
// src/rag/manual-index.ts
|
|
3283
|
+
async function processMessagesNow(input2) {
|
|
3284
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3285
|
+
if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
|
|
3286
|
+
return {
|
|
3287
|
+
status: "skipped",
|
|
3288
|
+
reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
|
|
3289
|
+
chunks: 0,
|
|
3290
|
+
vectors: 0,
|
|
3291
|
+
startedAt,
|
|
3292
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
const vectorStore = new SqliteVectorStore(input2.database, {
|
|
3296
|
+
model: input2.config.embedding.model
|
|
3297
|
+
});
|
|
3298
|
+
const embedding = input2.embedding ?? createEmbeddingModel(input2.config, input2.secrets);
|
|
3299
|
+
const stats = await indexMessageChunks({
|
|
3300
|
+
messages: new MessageRepository(input2.database),
|
|
3301
|
+
embedding,
|
|
3302
|
+
store: vectorStore,
|
|
3303
|
+
limit: input2.limit
|
|
3304
|
+
});
|
|
3305
|
+
return {
|
|
3306
|
+
status: "completed",
|
|
3307
|
+
chunks: stats.chunks,
|
|
3308
|
+
vectors: stats.vectors,
|
|
3309
|
+
startedAt,
|
|
3310
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
// src/multimodal/tasks.ts
|
|
3315
|
+
import crypto5 from "crypto";
|
|
3316
|
+
function nowIso4() {
|
|
3317
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3318
|
+
}
|
|
3319
|
+
function stableId3(sourceMessageId, imageKey) {
|
|
3320
|
+
return crypto5.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
|
|
3321
|
+
}
|
|
3322
|
+
function mapRow(row) {
|
|
3323
|
+
if (!row) {
|
|
3324
|
+
return void 0;
|
|
3325
|
+
}
|
|
3326
|
+
return {
|
|
3327
|
+
id: row.id,
|
|
3328
|
+
sourceMessageId: row.source_message_id,
|
|
3329
|
+
platformMessageId: row.platform_message_id,
|
|
3330
|
+
imageKey: row.image_key,
|
|
3331
|
+
storedPath: row.stored_path,
|
|
3332
|
+
mimeType: row.mime_type,
|
|
3333
|
+
status: row.status,
|
|
3334
|
+
attempts: row.attempts,
|
|
3335
|
+
...row.last_error ? { lastError: row.last_error } : {},
|
|
3336
|
+
...row.derived_message_id ? { derivedMessageId: row.derived_message_id } : {},
|
|
3337
|
+
createdAt: row.created_at,
|
|
3338
|
+
updatedAt: row.updated_at
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
var ImageMultimodalTaskRepository = class {
|
|
3342
|
+
constructor(database) {
|
|
3343
|
+
this.database = database;
|
|
3344
|
+
}
|
|
3345
|
+
database;
|
|
3346
|
+
enqueue(input2) {
|
|
3347
|
+
const id = stableId3(input2.sourceMessageId, input2.imageKey);
|
|
3348
|
+
const timestamp = nowIso4();
|
|
3349
|
+
this.database.prepare(
|
|
3350
|
+
`
|
|
3351
|
+
INSERT INTO image_multimodal_tasks (
|
|
3352
|
+
id,
|
|
3353
|
+
source_message_id,
|
|
3354
|
+
platform_message_id,
|
|
3355
|
+
image_key,
|
|
3356
|
+
stored_path,
|
|
3357
|
+
mime_type,
|
|
3358
|
+
status,
|
|
3359
|
+
attempts,
|
|
3360
|
+
created_at,
|
|
3361
|
+
updated_at
|
|
3362
|
+
)
|
|
3363
|
+
VALUES (
|
|
3364
|
+
@id,
|
|
3365
|
+
@sourceMessageId,
|
|
3366
|
+
@platformMessageId,
|
|
3367
|
+
@imageKey,
|
|
3368
|
+
@storedPath,
|
|
3369
|
+
@mimeType,
|
|
3370
|
+
'pending',
|
|
3371
|
+
0,
|
|
3372
|
+
@createdAt,
|
|
3373
|
+
@updatedAt
|
|
3374
|
+
)
|
|
3375
|
+
ON CONFLICT(source_message_id, image_key)
|
|
3376
|
+
DO UPDATE SET
|
|
3377
|
+
platform_message_id = excluded.platform_message_id,
|
|
3378
|
+
stored_path = excluded.stored_path,
|
|
3379
|
+
mime_type = excluded.mime_type,
|
|
3380
|
+
status = 'pending',
|
|
3381
|
+
attempts = 0,
|
|
3382
|
+
last_error = NULL,
|
|
3383
|
+
derived_message_id = NULL,
|
|
3384
|
+
updated_at = excluded.updated_at
|
|
3385
|
+
`
|
|
3386
|
+
).run({
|
|
3387
|
+
id,
|
|
3388
|
+
sourceMessageId: input2.sourceMessageId,
|
|
3389
|
+
platformMessageId: input2.platformMessageId,
|
|
3390
|
+
imageKey: input2.imageKey,
|
|
3391
|
+
storedPath: input2.storedPath,
|
|
3392
|
+
mimeType: input2.mimeType,
|
|
3393
|
+
createdAt: timestamp,
|
|
3394
|
+
updatedAt: timestamp
|
|
3395
|
+
});
|
|
3396
|
+
const record = this.getById(id);
|
|
3397
|
+
if (!record) {
|
|
3398
|
+
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u5199\u5165\u5931\u8D25\uFF1A${id}`);
|
|
3399
|
+
}
|
|
3400
|
+
return record;
|
|
3401
|
+
}
|
|
3402
|
+
listPending(limit = 10) {
|
|
3403
|
+
const rows = this.database.prepare(
|
|
3404
|
+
`
|
|
3405
|
+
SELECT
|
|
3406
|
+
id,
|
|
3407
|
+
source_message_id,
|
|
3408
|
+
platform_message_id,
|
|
3409
|
+
image_key,
|
|
3410
|
+
stored_path,
|
|
3411
|
+
mime_type,
|
|
3412
|
+
status,
|
|
3413
|
+
attempts,
|
|
3414
|
+
last_error,
|
|
3415
|
+
derived_message_id,
|
|
3416
|
+
created_at,
|
|
3417
|
+
updated_at
|
|
3418
|
+
FROM image_multimodal_tasks
|
|
3419
|
+
WHERE status = 'pending'
|
|
3420
|
+
ORDER BY updated_at ASC
|
|
3421
|
+
LIMIT ?
|
|
3422
|
+
`
|
|
3423
|
+
).all(limit);
|
|
3424
|
+
return rows.map((row) => mapRow(row)).filter((row) => Boolean(row));
|
|
3425
|
+
}
|
|
3426
|
+
markRunning(id) {
|
|
3427
|
+
const result = this.database.prepare(
|
|
3428
|
+
`
|
|
3429
|
+
UPDATE image_multimodal_tasks
|
|
3430
|
+
SET status = 'running',
|
|
3431
|
+
attempts = attempts + 1,
|
|
3432
|
+
last_error = NULL,
|
|
3433
|
+
updated_at = @updatedAt
|
|
3434
|
+
WHERE id = @id AND status = 'pending'
|
|
3435
|
+
`
|
|
3436
|
+
).run({ id, updatedAt: nowIso4() });
|
|
3437
|
+
if (result.changes === 0) {
|
|
3438
|
+
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A${id}`);
|
|
3439
|
+
}
|
|
3440
|
+
return this.requireById(id);
|
|
3441
|
+
}
|
|
3442
|
+
markSucceeded(id, derivedMessageId) {
|
|
3443
|
+
this.database.prepare(
|
|
3444
|
+
`
|
|
3445
|
+
UPDATE image_multimodal_tasks
|
|
3446
|
+
SET status = 'succeeded',
|
|
3447
|
+
last_error = NULL,
|
|
3448
|
+
derived_message_id = @derivedMessageId,
|
|
3449
|
+
updated_at = @updatedAt
|
|
3450
|
+
WHERE id = @id
|
|
3451
|
+
`
|
|
3452
|
+
).run({ id, derivedMessageId, updatedAt: nowIso4() });
|
|
3453
|
+
return this.requireById(id);
|
|
3454
|
+
}
|
|
3455
|
+
markSkipped(id, reason) {
|
|
3456
|
+
this.database.prepare(
|
|
3457
|
+
`
|
|
3458
|
+
UPDATE image_multimodal_tasks
|
|
3459
|
+
SET status = 'skipped',
|
|
3460
|
+
last_error = @reason,
|
|
3461
|
+
derived_message_id = NULL,
|
|
3462
|
+
updated_at = @updatedAt
|
|
3463
|
+
WHERE id = @id
|
|
3464
|
+
`
|
|
3465
|
+
).run({ id, reason, updatedAt: nowIso4() });
|
|
3466
|
+
return this.requireById(id);
|
|
3467
|
+
}
|
|
3468
|
+
markFailed(id, error, finalFailure) {
|
|
3469
|
+
this.database.prepare(
|
|
3470
|
+
`
|
|
3471
|
+
UPDATE image_multimodal_tasks
|
|
3472
|
+
SET status = @status,
|
|
3473
|
+
last_error = @error,
|
|
3474
|
+
derived_message_id = NULL,
|
|
3475
|
+
updated_at = @updatedAt
|
|
3476
|
+
WHERE id = @id
|
|
3477
|
+
`
|
|
3478
|
+
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso4() });
|
|
3479
|
+
return this.requireById(id);
|
|
3480
|
+
}
|
|
3481
|
+
getById(id) {
|
|
3482
|
+
const row = this.database.prepare(
|
|
3483
|
+
`
|
|
3484
|
+
SELECT
|
|
3485
|
+
id,
|
|
3486
|
+
source_message_id,
|
|
3487
|
+
platform_message_id,
|
|
3488
|
+
image_key,
|
|
3489
|
+
stored_path,
|
|
2796
3490
|
mime_type,
|
|
2797
3491
|
status,
|
|
2798
3492
|
attempts,
|
|
@@ -2879,173 +3573,228 @@ var ImageMultimodalWorker = class {
|
|
|
2879
3573
|
}
|
|
2880
3574
|
};
|
|
2881
3575
|
|
|
2882
|
-
// src/
|
|
2883
|
-
function
|
|
2884
|
-
|
|
3576
|
+
// src/cron/tools.ts
|
|
3577
|
+
function readString(input2, key) {
|
|
3578
|
+
const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
|
|
3579
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
3580
|
+
throw new Error(`${key} \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
|
|
3581
|
+
}
|
|
3582
|
+
return value.trim();
|
|
2885
3583
|
}
|
|
2886
|
-
function
|
|
2887
|
-
|
|
2888
|
-
|
|
3584
|
+
function createCronJobTools(input2) {
|
|
3585
|
+
return [
|
|
3586
|
+
{
|
|
3587
|
+
name: "create_cron_job",
|
|
3588
|
+
description: "Create a scheduled AI message for the current Feishu chat only. The schedule must be a five-field cron string.",
|
|
3589
|
+
inputSchema: {
|
|
3590
|
+
type: "object",
|
|
3591
|
+
properties: {
|
|
3592
|
+
schedule: {
|
|
3593
|
+
type: "string",
|
|
3594
|
+
description: "Five-field cron schedule, for example 0 9 * * *."
|
|
3595
|
+
},
|
|
3596
|
+
prompt: {
|
|
3597
|
+
type: "string",
|
|
3598
|
+
description: "Prompt used later to generate the scheduled message."
|
|
3599
|
+
}
|
|
3600
|
+
},
|
|
3601
|
+
required: ["schedule", "prompt"],
|
|
3602
|
+
additionalProperties: false
|
|
3603
|
+
},
|
|
3604
|
+
execute: async (rawInput) => {
|
|
3605
|
+
const job = input2.repository.create({
|
|
3606
|
+
chatId: input2.chatId,
|
|
3607
|
+
createdByOpenId: input2.createdByOpenId,
|
|
3608
|
+
schedule: readString(rawInput, "schedule"),
|
|
3609
|
+
prompt: readString(rawInput, "prompt")
|
|
3610
|
+
});
|
|
3611
|
+
return JSON.stringify({ ok: true, job });
|
|
3612
|
+
}
|
|
3613
|
+
},
|
|
3614
|
+
{
|
|
3615
|
+
name: "list_cron_jobs",
|
|
3616
|
+
description: "List active scheduled AI messages for the current Feishu chat only.",
|
|
3617
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
3618
|
+
execute: async () => JSON.stringify({ ok: true, jobs: input2.repository.listByChat(input2.chatId) })
|
|
3619
|
+
},
|
|
3620
|
+
{
|
|
3621
|
+
name: "delete_cron_job",
|
|
3622
|
+
description: "Delete a scheduled AI message by ID, only if it belongs to the current Feishu chat.",
|
|
3623
|
+
inputSchema: {
|
|
3624
|
+
type: "object",
|
|
3625
|
+
properties: {
|
|
3626
|
+
id: {
|
|
3627
|
+
type: "string",
|
|
3628
|
+
description: "Cron job ID returned by create_cron_job or list_cron_jobs."
|
|
3629
|
+
}
|
|
3630
|
+
},
|
|
3631
|
+
required: ["id"],
|
|
3632
|
+
additionalProperties: false
|
|
3633
|
+
},
|
|
3634
|
+
execute: async (rawInput) => {
|
|
3635
|
+
const id = readString(rawInput, "id");
|
|
3636
|
+
const ok = input2.repository.deleteByChat(id, input2.chatId);
|
|
3637
|
+
return JSON.stringify({
|
|
3638
|
+
ok,
|
|
3639
|
+
id,
|
|
3640
|
+
message: ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664\u3002" : "\u6CA1\u6709\u627E\u5230\u5F53\u524D\u7FA4\u91CC\u7684\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\u3002"
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
];
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// src/rag/qa-logs.ts
|
|
3648
|
+
import crypto6 from "crypto";
|
|
3649
|
+
function clampLimit(limit) {
|
|
3650
|
+
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3651
|
+
}
|
|
3652
|
+
var QaLogRepository = class {
|
|
3653
|
+
constructor(database) {
|
|
3654
|
+
this.database = database;
|
|
2889
3655
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
3656
|
+
database;
|
|
3657
|
+
create(input2) {
|
|
3658
|
+
const record = {
|
|
3659
|
+
id: `qa_${crypto6.randomUUID()}`,
|
|
3660
|
+
chatId: input2.chatId ?? null,
|
|
3661
|
+
questionMessageId: input2.questionMessageId ?? null,
|
|
3662
|
+
question: input2.question,
|
|
3663
|
+
answer: input2.answer,
|
|
3664
|
+
citations: input2.citations,
|
|
3665
|
+
retrievalDebug: input2.retrievalDebug,
|
|
3666
|
+
status: input2.status,
|
|
3667
|
+
error: input2.error ?? null,
|
|
3668
|
+
createdAt: input2.createdAt
|
|
3669
|
+
};
|
|
3670
|
+
this.database.prepare(
|
|
3671
|
+
`
|
|
3672
|
+
INSERT INTO qa_logs (
|
|
3673
|
+
id,
|
|
3674
|
+
chat_id,
|
|
3675
|
+
question_message_id,
|
|
3676
|
+
question,
|
|
3677
|
+
answer,
|
|
3678
|
+
citations_json,
|
|
3679
|
+
retrieval_debug_json,
|
|
3680
|
+
status,
|
|
3681
|
+
error,
|
|
3682
|
+
created_at
|
|
3683
|
+
)
|
|
3684
|
+
VALUES (
|
|
3685
|
+
@id,
|
|
3686
|
+
@chatId,
|
|
3687
|
+
@questionMessageId,
|
|
3688
|
+
@question,
|
|
3689
|
+
@answer,
|
|
3690
|
+
@citationsJson,
|
|
3691
|
+
@retrievalDebugJson,
|
|
3692
|
+
@status,
|
|
3693
|
+
@error,
|
|
3694
|
+
@createdAt
|
|
3695
|
+
)
|
|
3696
|
+
`
|
|
3697
|
+
).run({
|
|
3698
|
+
id: record.id,
|
|
3699
|
+
chatId: record.chatId,
|
|
3700
|
+
questionMessageId: record.questionMessageId,
|
|
3701
|
+
question: record.question,
|
|
3702
|
+
answer: record.answer,
|
|
3703
|
+
citationsJson: JSON.stringify(record.citations),
|
|
3704
|
+
retrievalDebugJson: JSON.stringify(record.retrievalDebug),
|
|
3705
|
+
status: record.status,
|
|
3706
|
+
error: record.error,
|
|
3707
|
+
createdAt: record.createdAt
|
|
3708
|
+
});
|
|
3709
|
+
return record;
|
|
2893
3710
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3711
|
+
listRecent(limit) {
|
|
3712
|
+
const rows = this.database.prepare(
|
|
3713
|
+
`
|
|
3714
|
+
SELECT
|
|
3715
|
+
id,
|
|
3716
|
+
chat_id,
|
|
3717
|
+
question_message_id,
|
|
3718
|
+
question,
|
|
3719
|
+
answer,
|
|
3720
|
+
citations_json,
|
|
3721
|
+
retrieval_debug_json,
|
|
3722
|
+
status,
|
|
3723
|
+
error,
|
|
3724
|
+
created_at
|
|
3725
|
+
FROM qa_logs
|
|
3726
|
+
ORDER BY created_at DESC
|
|
3727
|
+
LIMIT ?
|
|
3728
|
+
`
|
|
3729
|
+
).all(clampLimit(limit));
|
|
3730
|
+
return rows.map((row) => ({
|
|
3731
|
+
id: row.id,
|
|
3732
|
+
chatId: row.chat_id,
|
|
3733
|
+
questionMessageId: row.question_message_id,
|
|
3734
|
+
question: row.question,
|
|
3735
|
+
answer: row.answer,
|
|
3736
|
+
citations: JSON.parse(row.citations_json),
|
|
3737
|
+
retrievalDebug: JSON.parse(row.retrieval_debug_json),
|
|
3738
|
+
status: row.status,
|
|
3739
|
+
error: row.error,
|
|
3740
|
+
createdAt: row.created_at
|
|
3741
|
+
}));
|
|
2900
3742
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
3743
|
+
getCount() {
|
|
3744
|
+
const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
|
|
3745
|
+
return row.count;
|
|
2903
3746
|
}
|
|
2904
|
-
|
|
2905
|
-
}
|
|
2906
|
-
function clipText(value, maxLength) {
|
|
2907
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
2908
|
-
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
2909
|
-
}
|
|
2910
|
-
function formatCitation(citation, options = {}) {
|
|
2911
|
-
const maxTextLength = options.maxTextLength ?? 120;
|
|
2912
|
-
const speaker = formatSpeaker(citation.source);
|
|
2913
|
-
const time = formatTime(citation.source.timestamp);
|
|
2914
|
-
const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
|
|
2915
|
-
return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
|
|
2916
|
-
}
|
|
2917
|
-
function formatCitations(citations, options = {}) {
|
|
2918
|
-
return citations.map((citation) => formatCitation(citation, options)).join("\n");
|
|
2919
|
-
}
|
|
3747
|
+
};
|
|
2920
3748
|
|
|
2921
|
-
// src/
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3749
|
+
// src/feishu/question.ts
|
|
3750
|
+
function parseTextContent(content) {
|
|
3751
|
+
if (!content) {
|
|
3752
|
+
return "";
|
|
3753
|
+
}
|
|
3754
|
+
try {
|
|
3755
|
+
const parsed = JSON.parse(content);
|
|
3756
|
+
return typeof parsed.text === "string" ? parsed.text : "";
|
|
3757
|
+
} catch {
|
|
3758
|
+
return content;
|
|
2928
3759
|
}
|
|
2929
|
-
const time = Date.parse(value);
|
|
2930
|
-
return Number.isFinite(time) ? time : 0;
|
|
2931
3760
|
}
|
|
2932
|
-
function
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
if (timeDiff !== 0) {
|
|
2940
|
-
return timeDiff;
|
|
3761
|
+
function stripMentions(text, mentions) {
|
|
3762
|
+
let result = text;
|
|
3763
|
+
for (const mention of mentions ?? []) {
|
|
3764
|
+
for (const token of [mention.key, mention.name, mention.name ? `@${mention.name}` : void 0]) {
|
|
3765
|
+
if (token) {
|
|
3766
|
+
result = result.replaceAll(token, " ");
|
|
3767
|
+
}
|
|
2941
3768
|
}
|
|
2942
|
-
return scoreDiff;
|
|
2943
|
-
});
|
|
2944
|
-
}
|
|
2945
|
-
function buildEvidencePrompt(question, evidence, options = {}) {
|
|
2946
|
-
if (evidence.length === 0) {
|
|
2947
|
-
throw new Error("RAG evidence is required before answer generation.");
|
|
2948
3769
|
}
|
|
2949
|
-
|
|
2950
|
-
const maxCharsPerBlock = options.maxCharsPerBlock ?? DEFAULT_MAX_CHARS_PER_BLOCK;
|
|
2951
|
-
const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
|
|
2952
|
-
const citations = selected.map((item, index2) => ({
|
|
2953
|
-
marker: `S${index2 + 1}`,
|
|
2954
|
-
evidenceId: item.id,
|
|
2955
|
-
source: item.source,
|
|
2956
|
-
text: item.text
|
|
2957
|
-
}));
|
|
2958
|
-
const evidenceText = selected.map((item, index2) => {
|
|
2959
|
-
const marker = citations[index2]?.marker;
|
|
2960
|
-
const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
|
|
2961
|
-
const sourceParts = [
|
|
2962
|
-
item.source.label,
|
|
2963
|
-
item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
|
|
2964
|
-
item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
|
|
2965
|
-
item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
|
|
2966
|
-
].filter(Boolean);
|
|
2967
|
-
return `[${marker}]
|
|
2968
|
-
\u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
|
|
2969
|
-
\u5185\u5BB9\uFF1A${clippedText}`;
|
|
2970
|
-
}).join("\n\n");
|
|
2971
|
-
return {
|
|
2972
|
-
citations,
|
|
2973
|
-
messages: [
|
|
2974
|
-
{
|
|
2975
|
-
role: "system",
|
|
2976
|
-
content: "\u4F60\u662F ChatterCatcher \u7684\u95EE\u7B54\u6A21\u5757\u3002\u53EA\u80FD\u6839\u636E\u63D0\u4F9B\u7684\u68C0\u7D22\u8BC1\u636E\u56DE\u7B54\uFF0C\u5FC5\u987B\u7B80\u77ED\u76F4\u63A5\u3002\u4E8B\u5B9E\u6027\u7ED3\u8BBA\u5FC5\u987B\u5F15\u7528 [S1] \u8FD9\u6837\u7684\u6765\u6E90\u6807\u8BB0\u3002\u8BC1\u636E\u4E0D\u8DB3\u65F6\u8BF4\u4E0D\u77E5\u9053\uFF0C\u4E0D\u8981\u731C\u3002\u82E5\u8BC1\u636E\u4E92\u76F8\u77DB\u76FE\uFF0C\u4F18\u5148\u91C7\u7528\u65F6\u95F4\u66F4\u65B0\u4E14\u8868\u8FF0\u660E\u786E\u7684\u8BC1\u636E\uFF1B\u5982\u679C\u8F83\u65B0\u7684\u8BC1\u636E\u53EA\u662F\u8BA8\u8BBA\u3001\u731C\u6D4B\u6216\u4E0D\u786E\u5B9A\u8868\u8FBE\uFF0C\u4E0D\u8981\u628A\u5B83\u5F53\u4F5C\u786E\u5B9A\u66F4\u65B0\u3002"
|
|
2977
|
-
},
|
|
2978
|
-
{
|
|
2979
|
-
role: "user",
|
|
2980
|
-
content: `\u95EE\u9898\uFF1A${question}
|
|
2981
|
-
|
|
2982
|
-
\u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
|
|
2983
|
-
1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
|
|
2984
|
-
2. \u540C\u4E00\u4E8B\u9879\u51FA\u73B0\u591A\u4E2A\u7248\u672C\u65F6\uFF0C\u9ED8\u8BA4\u8F83\u65B0\u7684\u660E\u786E\u6D88\u606F\u4F18\u5148\u3002
|
|
2985
|
-
3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
|
|
2986
|
-
|
|
2987
|
-
\u68C0\u7D22\u8BC1\u636E\uFF1A
|
|
2988
|
-
${evidenceText}`
|
|
2989
|
-
}
|
|
2990
|
-
]
|
|
2991
|
-
};
|
|
2992
|
-
}
|
|
2993
|
-
async function generateGroundedAnswer(input2) {
|
|
2994
|
-
const prompt = buildEvidencePrompt(input2.question, input2.evidence);
|
|
2995
|
-
const answer = await input2.model.complete(prompt.messages);
|
|
2996
|
-
return {
|
|
2997
|
-
answer,
|
|
2998
|
-
citations: prompt.citations
|
|
2999
|
-
};
|
|
3770
|
+
return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
|
|
3000
3771
|
}
|
|
3001
|
-
|
|
3002
|
-
// src/rag/agentic-qa-service.ts
|
|
3772
|
+
var FEISHU_TOOL_SYSTEM_PROMPT = "\u4F60\u662F\u98DE\u4E66\u7FA4\u804A\u52A9\u624B\u3002\u4F60\u53EF\u4EE5\u5148\u641C\u7D22\u672C\u5730\u77E5\u8BC6\u6765\u56DE\u7B54\u95EE\u9898\uFF1B\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u521B\u5EFA\u3001\u67E5\u770B\u6216\u5220\u9664\u7FA4\u6D88\u606F\u5B9A\u65F6\u4EFB\u52A1\u65F6\uFF0C\u4E5F\u53EF\u4EE5\u8C03\u7528\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u3002\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u53EA\u7BA1\u7406\u5F53\u524D\u7FA4\u804A\uFF0C\u4E0D\u80FD\u8DE8\u7FA4\u64CD\u4F5C\u3002\u82E5\u7528\u6237\u7528\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u65F6\u95F4\uFF0C\u4F60\u9700\u8981\u5148\u5C06\u5176\u8F6C\u6362\u4E3A\u4E94\u5B57\u6BB5 cron \u8868\u8FBE\u5F0F\uFF08\u5206 \u65F6 \u65E5 \u6708 \u5468\uFF09\uFF0C\u518D\u8C03\u7528\u5DE5\u5177\u3002\u5BF9\u4E8E\u4E00\u822C\u95EE\u7B54\uFF0C\u5148\u6309\u9700\u8C03\u7528\u641C\u7D22\u5DE5\u5177\uFF0C\u518D\u57FA\u4E8E\u5DE5\u5177\u8FD4\u56DE\u7684\u8BC1\u636E\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\uFF1B\u82E5\u5F15\u7528\u4E86\u68C0\u7D22\u7ED3\u679C\uFF0C\u8981\u5728\u7B54\u6848\u91CC\u76F4\u63A5\u5199\u51FA\u5F15\u7528\u5185\u5BB9\u3002\u4E0D\u8981\u58F0\u79F0\u5B8C\u6210\u4E86\u672A\u5B9E\u9645\u8C03\u7528\u7684\u64CD\u4F5C\u3002";
|
|
3003
3773
|
var DEFAULT_MAX_MODEL_TURNS = 4;
|
|
3004
3774
|
var DEFAULT_MAX_TOOL_CALLS = 8;
|
|
3005
|
-
var
|
|
3006
|
-
var
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
return JSON.stringify(
|
|
3010
|
-
results.map((item) => ({
|
|
3011
|
-
id: item.id,
|
|
3012
|
-
text: item.text,
|
|
3013
|
-
score: item.score,
|
|
3014
|
-
source: item.source
|
|
3015
|
-
}))
|
|
3016
|
-
);
|
|
3775
|
+
var FEISHU_TOOL_LOOP_FALLBACK = "\u5B9A\u65F6\u4EFB\u52A1\u64CD\u4F5C\u5DF2\u63D0\u4EA4\uFF0C\u4F46\u6A21\u578B\u6CA1\u6709\u751F\u6210\u6700\u7EC8\u56DE\u590D\u3002";
|
|
3776
|
+
var FEISHU_TOOL_LOOP_LIMIT_REACHED = "\u5DE5\u5177\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u5230\u4E0A\u9650\uFF0C\u8BF7\u7F29\u5C0F\u8BF7\u6C42\u540E\u91CD\u8BD5\u3002";
|
|
3777
|
+
function toToolResultContent(value) {
|
|
3778
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
3017
3779
|
}
|
|
3018
3780
|
function toToolErrorContent(message) {
|
|
3019
|
-
return JSON.stringify({ error: message });
|
|
3781
|
+
return JSON.stringify({ ok: false, error: message });
|
|
3020
3782
|
}
|
|
3021
|
-
function
|
|
3022
|
-
const
|
|
3023
|
-
|
|
3024
|
-
for (const item of evidence) {
|
|
3025
|
-
if (seen.has(item.id)) {
|
|
3026
|
-
continue;
|
|
3027
|
-
}
|
|
3028
|
-
seen.add(item.id);
|
|
3029
|
-
deduped.push(item);
|
|
3030
|
-
if (deduped.length >= maxEvidence) {
|
|
3031
|
-
break;
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
return deduped;
|
|
3783
|
+
async function executeFeishuTool(tool, input2) {
|
|
3784
|
+
const result = await tool.execute(input2);
|
|
3785
|
+
return toToolResultContent(result);
|
|
3035
3786
|
}
|
|
3036
|
-
async function
|
|
3787
|
+
async function runFeishuToolLoop(input2) {
|
|
3037
3788
|
if (!input2.model.completeWithTools) {
|
|
3038
3789
|
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3039
3790
|
}
|
|
3040
3791
|
const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3041
3792
|
const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
3042
|
-
const maxEvidence = input2.maxEvidence ?? DEFAULT_MAX_EVIDENCE;
|
|
3043
3793
|
const messages = [
|
|
3044
|
-
{ role: "system", content:
|
|
3794
|
+
{ role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
|
|
3045
3795
|
{ role: "user", content: input2.question }
|
|
3046
3796
|
];
|
|
3047
3797
|
const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
|
|
3048
|
-
let evidence = [];
|
|
3049
3798
|
let toolCallsUsed = 0;
|
|
3050
3799
|
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
3051
3800
|
const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
|
|
@@ -3055,11 +3804,11 @@ async function askWithAgenticRag(input2) {
|
|
|
3055
3804
|
toolCalls: assistantResult.toolCalls
|
|
3056
3805
|
});
|
|
3057
3806
|
if (assistantResult.toolCalls.length === 0) {
|
|
3058
|
-
|
|
3807
|
+
return assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
3059
3808
|
}
|
|
3060
3809
|
for (const toolCall of assistantResult.toolCalls) {
|
|
3061
3810
|
if (toolCallsUsed >= maxToolCalls) {
|
|
3062
|
-
|
|
3811
|
+
return FEISHU_TOOL_LOOP_LIMIT_REACHED;
|
|
3063
3812
|
}
|
|
3064
3813
|
toolCallsUsed += 1;
|
|
3065
3814
|
const tool = toolsByName.get(toolCall.name);
|
|
@@ -3072,12 +3821,11 @@ async function askWithAgenticRag(input2) {
|
|
|
3072
3821
|
continue;
|
|
3073
3822
|
}
|
|
3074
3823
|
try {
|
|
3075
|
-
const
|
|
3076
|
-
evidence = dedupeEvidence([...evidence, ...results], maxEvidence);
|
|
3824
|
+
const result = await executeFeishuTool(tool, toolCall.input);
|
|
3077
3825
|
messages.push({
|
|
3078
3826
|
role: "tool",
|
|
3079
3827
|
toolCallId: toolCall.id,
|
|
3080
|
-
content:
|
|
3828
|
+
content: result
|
|
3081
3829
|
});
|
|
3082
3830
|
} catch (error) {
|
|
3083
3831
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -3089,41 +3837,7 @@ async function askWithAgenticRag(input2) {
|
|
|
3089
3837
|
}
|
|
3090
3838
|
}
|
|
3091
3839
|
}
|
|
3092
|
-
|
|
3093
|
-
return {
|
|
3094
|
-
answer: NO_EVIDENCE_ANSWER,
|
|
3095
|
-
citations: []
|
|
3096
|
-
};
|
|
3097
|
-
}
|
|
3098
|
-
return generateGroundedAnswer({
|
|
3099
|
-
question: input2.question,
|
|
3100
|
-
evidence,
|
|
3101
|
-
model: input2.model
|
|
3102
|
-
});
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
// src/feishu/question.ts
|
|
3106
|
-
function parseTextContent(content) {
|
|
3107
|
-
if (!content) {
|
|
3108
|
-
return "";
|
|
3109
|
-
}
|
|
3110
|
-
try {
|
|
3111
|
-
const parsed = JSON.parse(content);
|
|
3112
|
-
return typeof parsed.text === "string" ? parsed.text : "";
|
|
3113
|
-
} catch {
|
|
3114
|
-
return content;
|
|
3115
|
-
}
|
|
3116
|
-
}
|
|
3117
|
-
function stripMentions(text, mentions) {
|
|
3118
|
-
let result = text;
|
|
3119
|
-
for (const mention of mentions ?? []) {
|
|
3120
|
-
for (const token of [mention.key, mention.name, mention.name ? `@${mention.name}` : void 0]) {
|
|
3121
|
-
if (token) {
|
|
3122
|
-
result = result.replaceAll(token, " ");
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
}
|
|
3126
|
-
return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
|
|
3840
|
+
return FEISHU_TOOL_LOOP_FALLBACK;
|
|
3127
3841
|
}
|
|
3128
3842
|
function isMentionForBot(mention, config) {
|
|
3129
3843
|
if (!config.feishu.botOpenId) {
|
|
@@ -3208,6 +3922,7 @@ var FeishuQuestionHandler = class {
|
|
|
3208
3922
|
return decision;
|
|
3209
3923
|
}
|
|
3210
3924
|
const questionMessageId = payload.event?.message?.message_id;
|
|
3925
|
+
const qaLogs = new QaLogRepository(this.options.database);
|
|
3211
3926
|
await this.acknowledgeQuestion(decision.chatId, questionMessageId);
|
|
3212
3927
|
const { tools, close } = await createAgenticRagSearchTools({
|
|
3213
3928
|
config: this.options.config,
|
|
@@ -3218,19 +3933,41 @@ var FeishuQuestionHandler = class {
|
|
|
3218
3933
|
});
|
|
3219
3934
|
try {
|
|
3220
3935
|
try {
|
|
3221
|
-
const
|
|
3936
|
+
const cronTools = createCronJobTools({
|
|
3937
|
+
repository: new CronJobRepository(this.options.database),
|
|
3938
|
+
chatId: decision.chatId,
|
|
3939
|
+
createdByOpenId: payload.event?.sender?.sender_id?.open_id
|
|
3940
|
+
});
|
|
3941
|
+
const allTools = [...tools, ...cronTools];
|
|
3942
|
+
const answer = await runFeishuToolLoop({
|
|
3222
3943
|
question: decision.question,
|
|
3223
|
-
tools,
|
|
3944
|
+
tools: allTools,
|
|
3224
3945
|
model: this.options.model
|
|
3225
3946
|
});
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3947
|
+
qaLogs.create({
|
|
3948
|
+
chatId: decision.chatId,
|
|
3949
|
+
questionMessageId,
|
|
3950
|
+
question: decision.question,
|
|
3951
|
+
answer,
|
|
3952
|
+
citations: [],
|
|
3953
|
+
retrievalDebug: {},
|
|
3954
|
+
status: "answered",
|
|
3955
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3956
|
+
});
|
|
3957
|
+
await this.sendResponse(decision.chatId, questionMessageId, answer);
|
|
3232
3958
|
} catch (error) {
|
|
3233
3959
|
const message = error instanceof Error ? error.message : String(error);
|
|
3960
|
+
qaLogs.create({
|
|
3961
|
+
chatId: decision.chatId,
|
|
3962
|
+
questionMessageId,
|
|
3963
|
+
question: decision.question,
|
|
3964
|
+
answer: `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`,
|
|
3965
|
+
citations: [],
|
|
3966
|
+
retrievalDebug: {},
|
|
3967
|
+
status: "failed",
|
|
3968
|
+
error: message,
|
|
3969
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3970
|
+
});
|
|
3234
3971
|
await this.sendResponse(decision.chatId, questionMessageId, `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`);
|
|
3235
3972
|
}
|
|
3236
3973
|
return decision;
|
|
@@ -3452,15 +4189,50 @@ function createFeishuGateway(options) {
|
|
|
3452
4189
|
episodeProcessor: options.episodeProcessor,
|
|
3453
4190
|
imageMultimodalProcessor: options.imageMultimodalProcessor
|
|
3454
4191
|
});
|
|
4192
|
+
const indexingScheduler = options.indexingScheduler ?? (options.indexingProcessor ? createIndexingScheduler({
|
|
4193
|
+
schedule: options.config.schedules.indexing,
|
|
4194
|
+
work: async () => {
|
|
4195
|
+
await processMessagesNow({
|
|
4196
|
+
config: options.config,
|
|
4197
|
+
secrets: options.secrets,
|
|
4198
|
+
database: options.indexingProcessor.database,
|
|
4199
|
+
limit: 1e4
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
}) : void 0);
|
|
4203
|
+
const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
|
|
4204
|
+
repository: new CronJobRepository(options.cronJobProcessor.database),
|
|
4205
|
+
sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
|
|
4206
|
+
generateMessage: async (job, now) => {
|
|
4207
|
+
const { tools, close } = await createAgenticRagSearchTools({
|
|
4208
|
+
config: options.config,
|
|
4209
|
+
secrets: options.secrets,
|
|
4210
|
+
database: options.cronJobProcessor.database,
|
|
4211
|
+
messages: new MessageRepository(options.cronJobProcessor.database),
|
|
4212
|
+
scope: { platform: "feishu", platformChatId: job.chatId }
|
|
4213
|
+
});
|
|
4214
|
+
try {
|
|
4215
|
+
return await generateCronJobMessage({ prompt: job.prompt, model: options.cronJobProcessor.model, tools, now });
|
|
4216
|
+
} finally {
|
|
4217
|
+
close();
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}) : void 0);
|
|
3455
4221
|
return {
|
|
3456
4222
|
async start() {
|
|
3457
4223
|
try {
|
|
3458
4224
|
await wsClient.start({ eventDispatcher });
|
|
4225
|
+
indexingScheduler?.start();
|
|
4226
|
+
cronJobScheduler?.start();
|
|
3459
4227
|
} catch (error) {
|
|
4228
|
+
indexingScheduler?.stop();
|
|
4229
|
+
cronJobScheduler?.stop();
|
|
3460
4230
|
throw formatGatewayStartError(error);
|
|
3461
4231
|
}
|
|
3462
4232
|
},
|
|
3463
4233
|
stop() {
|
|
4234
|
+
indexingScheduler?.stop();
|
|
4235
|
+
cronJobScheduler?.stop();
|
|
3464
4236
|
wsClient.close({ force: true });
|
|
3465
4237
|
}
|
|
3466
4238
|
};
|
|
@@ -3532,7 +4304,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
3532
4304
|
};
|
|
3533
4305
|
|
|
3534
4306
|
// src/files/ingest.ts
|
|
3535
|
-
import
|
|
4307
|
+
import crypto7 from "crypto";
|
|
3536
4308
|
import fs11 from "fs/promises";
|
|
3537
4309
|
import path13 from "path";
|
|
3538
4310
|
|
|
@@ -3596,7 +4368,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
3596
4368
|
}
|
|
3597
4369
|
}
|
|
3598
4370
|
function stableStoredName(sourcePath, fileName) {
|
|
3599
|
-
const digest =
|
|
4371
|
+
const digest = crypto7.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
3600
4372
|
return `${digest}-${fileName}`;
|
|
3601
4373
|
}
|
|
3602
4374
|
async function ingestLocalFile(input2) {
|
|
@@ -4118,81 +4890,84 @@ function createMultimodalModel(config, secrets) {
|
|
|
4118
4890
|
});
|
|
4119
4891
|
}
|
|
4120
4892
|
|
|
4121
|
-
// src/rag/
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4893
|
+
// src/rag/answer.ts
|
|
4894
|
+
var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
|
|
4895
|
+
var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
|
|
4896
|
+
var SCORE_TIE_THRESHOLD = 0.15;
|
|
4897
|
+
function parseTimestamp(value) {
|
|
4898
|
+
if (!value) {
|
|
4899
|
+
return 0;
|
|
4126
4900
|
}
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4901
|
+
const time = Date.parse(value);
|
|
4902
|
+
return Number.isFinite(time) ? time : 0;
|
|
4903
|
+
}
|
|
4904
|
+
function rankEvidenceForPrompt(evidence) {
|
|
4905
|
+
return [...evidence].sort((left, right) => {
|
|
4906
|
+
const scoreDiff = right.score - left.score;
|
|
4907
|
+
if (Math.abs(scoreDiff) > SCORE_TIE_THRESHOLD) {
|
|
4908
|
+
return scoreDiff;
|
|
4133
4909
|
}
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
score: 1,
|
|
4141
|
-
source: toEvidenceSource3(chunk)
|
|
4142
|
-
}
|
|
4143
|
-
});
|
|
4144
|
-
}
|
|
4145
|
-
await input2.store.upsert(records);
|
|
4146
|
-
return {
|
|
4147
|
-
chunks: chunks.length,
|
|
4148
|
-
vectors: records.length
|
|
4149
|
-
};
|
|
4910
|
+
const timeDiff = parseTimestamp(right.source.timestamp) - parseTimestamp(left.source.timestamp);
|
|
4911
|
+
if (timeDiff !== 0) {
|
|
4912
|
+
return timeDiff;
|
|
4913
|
+
}
|
|
4914
|
+
return scoreDiff;
|
|
4915
|
+
});
|
|
4150
4916
|
}
|
|
4151
|
-
function
|
|
4152
|
-
if (
|
|
4153
|
-
|
|
4154
|
-
type: "file",
|
|
4155
|
-
label: chunk.senderName,
|
|
4156
|
-
timestamp: chunk.sentAt
|
|
4157
|
-
};
|
|
4917
|
+
function buildEvidencePrompt(question, evidence, options = {}) {
|
|
4918
|
+
if (evidence.length === 0) {
|
|
4919
|
+
throw new Error("RAG evidence is required before answer generation.");
|
|
4158
4920
|
}
|
|
4921
|
+
const maxEvidenceBlocks = options.maxEvidenceBlocks ?? DEFAULT_MAX_EVIDENCE_BLOCKS;
|
|
4922
|
+
const maxCharsPerBlock = options.maxCharsPerBlock ?? DEFAULT_MAX_CHARS_PER_BLOCK;
|
|
4923
|
+
const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
|
|
4924
|
+
const citations = selected.map((item, index2) => ({
|
|
4925
|
+
marker: `S${index2 + 1}`,
|
|
4926
|
+
evidenceId: item.id,
|
|
4927
|
+
source: item.source,
|
|
4928
|
+
text: item.text
|
|
4929
|
+
}));
|
|
4930
|
+
const evidenceText = selected.map((item, index2) => {
|
|
4931
|
+
const marker = citations[index2]?.marker;
|
|
4932
|
+
const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
|
|
4933
|
+
const sourceParts = [
|
|
4934
|
+
item.source.label,
|
|
4935
|
+
item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
|
|
4936
|
+
item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
|
|
4937
|
+
item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
|
|
4938
|
+
].filter(Boolean);
|
|
4939
|
+
return `[${marker}]
|
|
4940
|
+
\u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
|
|
4941
|
+
\u5185\u5BB9\uFF1A${clippedText}`;
|
|
4942
|
+
}).join("\n\n");
|
|
4159
4943
|
return {
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4944
|
+
citations,
|
|
4945
|
+
messages: [
|
|
4946
|
+
{
|
|
4947
|
+
role: "system",
|
|
4948
|
+
content: "\u4F60\u662F ChatterCatcher \u7684\u95EE\u7B54\u6A21\u5757\u3002\u53EA\u80FD\u6839\u636E\u63D0\u4F9B\u7684\u68C0\u7D22\u8BC1\u636E\u56DE\u7B54\uFF0C\u5FC5\u987B\u7B80\u77ED\u76F4\u63A5\u3002\u4E8B\u5B9E\u6027\u7ED3\u8BBA\u5FC5\u987B\u5F15\u7528 [S1] \u8FD9\u6837\u7684\u6765\u6E90\u6807\u8BB0\u3002\u8BC1\u636E\u4E0D\u8DB3\u65F6\u8BF4\u4E0D\u77E5\u9053\uFF0C\u4E0D\u8981\u731C\u3002\u82E5\u8BC1\u636E\u4E92\u76F8\u77DB\u76FE\uFF0C\u4F18\u5148\u91C7\u7528\u65F6\u95F4\u66F4\u65B0\u4E14\u8868\u8FF0\u660E\u786E\u7684\u8BC1\u636E\uFF1B\u5982\u679C\u8F83\u65B0\u7684\u8BC1\u636E\u53EA\u662F\u8BA8\u8BBA\u3001\u731C\u6D4B\u6216\u4E0D\u786E\u5B9A\u8868\u8FBE\uFF0C\u4E0D\u8981\u628A\u5B83\u5F53\u4F5C\u786E\u5B9A\u66F4\u65B0\u3002"
|
|
4949
|
+
},
|
|
4950
|
+
{
|
|
4951
|
+
role: "user",
|
|
4952
|
+
content: `\u95EE\u9898\uFF1A${question}
|
|
4953
|
+
|
|
4954
|
+
\u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
|
|
4955
|
+
1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
|
|
4956
|
+
2. \u540C\u4E00\u4E8B\u9879\u51FA\u73B0\u591A\u4E2A\u7248\u672C\u65F6\uFF0C\u9ED8\u8BA4\u8F83\u65B0\u7684\u660E\u786E\u6D88\u606F\u4F18\u5148\u3002
|
|
4957
|
+
3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
|
|
4958
|
+
|
|
4959
|
+
\u68C0\u7D22\u8BC1\u636E\uFF1A
|
|
4960
|
+
${evidenceText}`
|
|
4961
|
+
}
|
|
4962
|
+
]
|
|
4164
4963
|
};
|
|
4165
4964
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4170
|
-
if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
|
|
4171
|
-
return {
|
|
4172
|
-
status: "skipped",
|
|
4173
|
-
reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
|
|
4174
|
-
chunks: 0,
|
|
4175
|
-
vectors: 0,
|
|
4176
|
-
startedAt,
|
|
4177
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4178
|
-
};
|
|
4179
|
-
}
|
|
4180
|
-
const vectorStore = new SqliteVectorStore(input2.database, {
|
|
4181
|
-
model: input2.config.embedding.model
|
|
4182
|
-
});
|
|
4183
|
-
const embedding = input2.embedding ?? createEmbeddingModel(input2.config, input2.secrets);
|
|
4184
|
-
const stats = await indexMessageChunks({
|
|
4185
|
-
messages: new MessageRepository(input2.database),
|
|
4186
|
-
embedding,
|
|
4187
|
-
store: vectorStore,
|
|
4188
|
-
limit: input2.limit
|
|
4189
|
-
});
|
|
4965
|
+
async function generateGroundedAnswer(input2) {
|
|
4966
|
+
const prompt = buildEvidencePrompt(input2.question, input2.evidence);
|
|
4967
|
+
const answer = await input2.model.complete(prompt.messages);
|
|
4190
4968
|
return {
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
vectors: stats.vectors,
|
|
4194
|
-
startedAt,
|
|
4195
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4969
|
+
answer,
|
|
4970
|
+
citations: prompt.citations
|
|
4196
4971
|
};
|
|
4197
4972
|
}
|
|
4198
4973
|
|
|
@@ -4212,6 +4987,42 @@ async function askWithRag(input2) {
|
|
|
4212
4987
|
});
|
|
4213
4988
|
}
|
|
4214
4989
|
|
|
4990
|
+
// src/rag/citations.ts
|
|
4991
|
+
function isOpaqueId(value) {
|
|
4992
|
+
return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
|
|
4993
|
+
}
|
|
4994
|
+
function formatTime(value) {
|
|
4995
|
+
if (!value) {
|
|
4996
|
+
return "\u672A\u77E5\u65F6\u95F4";
|
|
4997
|
+
}
|
|
4998
|
+
const date = new Date(value);
|
|
4999
|
+
if (Number.isNaN(date.getTime())) {
|
|
5000
|
+
return value;
|
|
5001
|
+
}
|
|
5002
|
+
const pad = (input2) => String(input2).padStart(2, "0");
|
|
5003
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
5004
|
+
}
|
|
5005
|
+
function formatSpeaker(source) {
|
|
5006
|
+
if (source.type === "file") {
|
|
5007
|
+
return isOpaqueId(source.label) ? "\u6587\u4EF6" : `\u6587\u4EF6 ${source.label}`;
|
|
5008
|
+
}
|
|
5009
|
+
if (source.sender && !isOpaqueId(source.sender)) {
|
|
5010
|
+
return source.sender;
|
|
5011
|
+
}
|
|
5012
|
+
return "\u7FA4\u6210\u5458";
|
|
5013
|
+
}
|
|
5014
|
+
function clipText(value, maxLength) {
|
|
5015
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
5016
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
5017
|
+
}
|
|
5018
|
+
function formatCitation(citation, options = {}) {
|
|
5019
|
+
const maxTextLength = options.maxTextLength ?? 120;
|
|
5020
|
+
const speaker = formatSpeaker(citation.source);
|
|
5021
|
+
const time = formatTime(citation.source.timestamp);
|
|
5022
|
+
const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
|
|
5023
|
+
return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
|
|
5024
|
+
}
|
|
5025
|
+
|
|
4215
5026
|
// src/update/npm-updater.ts
|
|
4216
5027
|
import { execFile } from "child_process";
|
|
4217
5028
|
import { promisify } from "util";
|
|
@@ -4315,6 +5126,7 @@ async function updateChatterCatcher(options) {
|
|
|
4315
5126
|
}
|
|
4316
5127
|
|
|
4317
5128
|
// src/web/server.ts
|
|
5129
|
+
import crypto8 from "crypto";
|
|
4318
5130
|
import Fastify from "fastify";
|
|
4319
5131
|
function buildHtml() {
|
|
4320
5132
|
return `<!doctype html>
|
|
@@ -4444,6 +5256,10 @@ function buildHtml() {
|
|
|
4444
5256
|
<h2>\u4F1A\u8BDD\u8BB0\u5FC6</h2>
|
|
4445
5257
|
<div id="episodes" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
4446
5258
|
</section>
|
|
5259
|
+
<section>
|
|
5260
|
+
<h2>\u95EE\u7B54\u65E5\u5FD7</h2>
|
|
5261
|
+
<div id="qa-logs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
5262
|
+
</section>
|
|
4447
5263
|
</div>
|
|
4448
5264
|
<aside>
|
|
4449
5265
|
<section>
|
|
@@ -4458,6 +5274,10 @@ function buildHtml() {
|
|
|
4458
5274
|
<h2>\u89E3\u6790\u4EFB\u52A1</h2>
|
|
4459
5275
|
<div id="file-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
4460
5276
|
</section>
|
|
5277
|
+
<section>
|
|
5278
|
+
<h2>\u5B9A\u65F6\u4EFB\u52A1</h2>
|
|
5279
|
+
<div id="cron-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
5280
|
+
</section>
|
|
4461
5281
|
<section>
|
|
4462
5282
|
<h2>\u672C\u5730\u64CD\u4F5C</h2>
|
|
4463
5283
|
<p><code>chattercatcher settings</code> \u4FEE\u6539\u914D\u7F6E\u3002</p>
|
|
@@ -4474,9 +5294,13 @@ function buildHtml() {
|
|
|
4474
5294
|
const chats = document.querySelector("#chats");
|
|
4475
5295
|
const files = document.querySelector("#files");
|
|
4476
5296
|
const fileJobs = document.querySelector("#file-jobs");
|
|
5297
|
+
const cronJobs = document.querySelector("#cron-jobs");
|
|
5298
|
+
const qaLogs = document.querySelector("#qa-logs");
|
|
4477
5299
|
const processMessages = document.querySelector("#process-messages");
|
|
4478
5300
|
const actionStatus = document.querySelector("#action-status");
|
|
4479
5301
|
|
|
5302
|
+
let webActionToken = "__WEB_ACTION_TOKEN__";
|
|
5303
|
+
|
|
4480
5304
|
function fmt(value) {
|
|
4481
5305
|
return value == null || value === "" ? "-" : String(value);
|
|
4482
5306
|
}
|
|
@@ -4665,14 +5489,76 @@ function buildHtml() {
|
|
|
4665
5489
|
\`;
|
|
4666
5490
|
}
|
|
4667
5491
|
|
|
5492
|
+
function renderCronJobs(items) {
|
|
5493
|
+
if (items.length === 0) {
|
|
5494
|
+
cronJobs.className = "empty";
|
|
5495
|
+
cronJobs.textContent = "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1\u3002\u53EF\u5728\u98DE\u4E66\u7FA4\u91CC @ \u673A\u5668\u4EBA\u521B\u5EFA\u3002";
|
|
5496
|
+
return;
|
|
5497
|
+
}
|
|
5498
|
+
cronJobs.className = "";
|
|
5499
|
+
cronJobs.innerHTML = \`
|
|
5500
|
+
<table>
|
|
5501
|
+
<thead><tr><th>\u4EFB\u52A1</th><th>\u72B6\u6001</th></tr></thead>
|
|
5502
|
+
<tbody>
|
|
5503
|
+
\${items.map((item) => \`
|
|
5504
|
+
<tr>
|
|
5505
|
+
<td>
|
|
5506
|
+
<div>\${escapeHtml(item.schedule)}</div>
|
|
5507
|
+
<div class="message" title="\${escapeHtml(item.prompt)}">\${escapeHtml(item.prompt)}</div>
|
|
5508
|
+
<div class="path" title="\${escapeHtml(item.id)}">ID: \${escapeHtml(item.id)}</div>
|
|
5509
|
+
<div class="path" title="\${escapeHtml(item.chatId)}">\u7FA4: \${escapeHtml(item.chatId)}</div>
|
|
5510
|
+
<div class="path">\u4E0B\u6B21: \${escapeHtml(formatDateTime(item.nextRunAt))}</div>
|
|
5511
|
+
<div class="path" title="\${escapeHtml(item.lastError || "")}">\${escapeHtml(item.lastError || "")}</div>
|
|
5512
|
+
\${item.status === "active" ? \`<button type="button" data-delete-cron-job="\${escapeHtml(item.id)}">\u5220\u9664</button>\` : ""}
|
|
5513
|
+
</td>
|
|
5514
|
+
<td>\${escapeHtml(item.status)}</td>
|
|
5515
|
+
</tr>
|
|
5516
|
+
\`).join("")}
|
|
5517
|
+
</tbody>
|
|
5518
|
+
</table>
|
|
5519
|
+
\`;
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
function renderQaLogs(items) {
|
|
5523
|
+
if (items.length === 0) {
|
|
5524
|
+
qaLogs.className = "empty";
|
|
5525
|
+
qaLogs.textContent = "\u8FD8\u6CA1\u6709\u95EE\u7B54\u65E5\u5FD7\u3002";
|
|
5526
|
+
return;
|
|
5527
|
+
}
|
|
5528
|
+
qaLogs.className = "";
|
|
5529
|
+
const rows = items.map((item) => {
|
|
5530
|
+
const citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
|
|
5531
|
+
return [
|
|
5532
|
+
'<article class="message-item">',
|
|
5533
|
+
' <div class="message-meta">',
|
|
5534
|
+
" <span>" + escapeHtml(formatDateTime(item.createdAt)) + "</span>",
|
|
5535
|
+
" <span>" + escapeHtml(item.status) + "</span>",
|
|
5536
|
+
" <span>" + escapeHtml(citationCount) + " \u6761\u5F15\u7528</span>",
|
|
5537
|
+
" </div>",
|
|
5538
|
+
" <div class="message-body"><strong>\u95EE\uFF1A</strong>" + escapeHtml(item.question) + "</div>",
|
|
5539
|
+
" <div class="message-body"><strong>\u7B54\uFF1A</strong>" + escapeHtml(item.answer) + "</div>",
|
|
5540
|
+
"</article>",
|
|
5541
|
+
].join("
|
|
5542
|
+
");
|
|
5543
|
+
});
|
|
5544
|
+
qaLogs.innerHTML = [
|
|
5545
|
+
'<div class="message-list">',
|
|
5546
|
+
rows.join(""),
|
|
5547
|
+
"</div>",
|
|
5548
|
+
].join("
|
|
5549
|
+
");
|
|
5550
|
+
}
|
|
5551
|
+
|
|
4668
5552
|
async function load() {
|
|
4669
|
-
const [status, recent, episodeList, chatList, fileList, jobList] = await Promise.all([
|
|
5553
|
+
const [status, recent, episodeList, chatList, fileList, jobList, qaLogList, cronJobList] = await Promise.all([
|
|
4670
5554
|
fetch("/api/status").then((response) => response.json()),
|
|
4671
5555
|
fetch("/api/messages/recent?limit=20").then((response) => response.json()),
|
|
4672
5556
|
fetch("/api/episodes?limit=10").then((response) => response.json()),
|
|
4673
5557
|
fetch("/api/chats").then((response) => response.json()),
|
|
4674
5558
|
fetch("/api/files").then((response) => response.json()),
|
|
4675
5559
|
fetch("/api/file-jobs").then((response) => response.json()),
|
|
5560
|
+
fetch("/api/qa-logs?limit=10").then((response) => response.json()),
|
|
5561
|
+
fetch("/api/cron-jobs").then((response) => response.json()),
|
|
4676
5562
|
]);
|
|
4677
5563
|
renderMetrics(status);
|
|
4678
5564
|
renderMessages(recent.items);
|
|
@@ -4680,13 +5566,18 @@ function buildHtml() {
|
|
|
4680
5566
|
renderChats(chatList.items);
|
|
4681
5567
|
renderFiles(fileList.items);
|
|
4682
5568
|
renderFileJobs(jobList.items);
|
|
5569
|
+
renderQaLogs(qaLogList.items);
|
|
5570
|
+
renderCronJobs(cronJobList.items);
|
|
4683
5571
|
}
|
|
4684
5572
|
|
|
4685
5573
|
async function processNow() {
|
|
4686
5574
|
processMessages.disabled = true;
|
|
4687
5575
|
actionStatus.textContent = "\u6B63\u5728\u5904\u7406\u6D88\u606F\u7D22\u5F15...";
|
|
4688
5576
|
try {
|
|
4689
|
-
const response = await fetch("/api/process/messages", {
|
|
5577
|
+
const response = await fetch("/api/process/messages", {
|
|
5578
|
+
method: "POST",
|
|
5579
|
+
headers: { "x-chattercatcher-web-token": webActionToken },
|
|
5580
|
+
});
|
|
4690
5581
|
const result = await response.json();
|
|
4691
5582
|
if (!response.ok) {
|
|
4692
5583
|
actionStatus.textContent = result.message || "\u5904\u7406\u5931\u8D25\u3002";
|
|
@@ -4706,6 +5597,28 @@ function buildHtml() {
|
|
|
4706
5597
|
}
|
|
4707
5598
|
}
|
|
4708
5599
|
|
|
5600
|
+
document.addEventListener("click", async (event) => {
|
|
5601
|
+
const target = event.target;
|
|
5602
|
+
if (!(target instanceof HTMLElement)) return;
|
|
5603
|
+
const id = target.dataset.deleteCronJob;
|
|
5604
|
+
if (!id) return;
|
|
5605
|
+
target.setAttribute("disabled", "disabled");
|
|
5606
|
+
actionStatus.textContent = "\u6B63\u5728\u5220\u9664\u5B9A\u65F6\u4EFB\u52A1...";
|
|
5607
|
+
try {
|
|
5608
|
+
const response = await fetch(\`/api/cron-jobs/\${encodeURIComponent(id)}\`, {
|
|
5609
|
+
method: "DELETE",
|
|
5610
|
+
headers: { "x-chattercatcher-web-token": webActionToken },
|
|
5611
|
+
});
|
|
5612
|
+
const result = await response.json();
|
|
5613
|
+
actionStatus.textContent = result.ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664\u3002" : result.message || "\u5220\u9664\u5931\u8D25\u3002";
|
|
5614
|
+
await load();
|
|
5615
|
+
} catch (error) {
|
|
5616
|
+
actionStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
5617
|
+
} finally {
|
|
5618
|
+
target.removeAttribute("disabled");
|
|
5619
|
+
}
|
|
5620
|
+
});
|
|
5621
|
+
|
|
4709
5622
|
processMessages.addEventListener("click", () => void processNow());
|
|
4710
5623
|
void load();
|
|
4711
5624
|
setInterval(() => {
|
|
@@ -4714,6 +5627,7 @@ function buildHtml() {
|
|
|
4714
5627
|
}
|
|
4715
5628
|
}, 5000);
|
|
4716
5629
|
</script>
|
|
5630
|
+
<script src="/app.js"></script>
|
|
4717
5631
|
</body>
|
|
4718
5632
|
</html>`;
|
|
4719
5633
|
}
|
|
@@ -4721,35 +5635,65 @@ function parseLimit(value, fallback, max) {
|
|
|
4721
5635
|
const rawLimit = Number(value ?? fallback);
|
|
4722
5636
|
return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
|
|
4723
5637
|
}
|
|
5638
|
+
function getWebActionToken(secrets) {
|
|
5639
|
+
return secrets.web.actionToken;
|
|
5640
|
+
}
|
|
5641
|
+
function readHeader(value) {
|
|
5642
|
+
return Array.isArray(value) ? value[0] : value;
|
|
5643
|
+
}
|
|
5644
|
+
function isAuthorizedWebAction(request, token) {
|
|
5645
|
+
const provided = readHeader(request.headers["x-chattercatcher-web-token"]);
|
|
5646
|
+
return provided === token;
|
|
5647
|
+
}
|
|
5648
|
+
function extractInlineScript(html) {
|
|
5649
|
+
const match = /<script>([\s\S]*)<\/script>/.exec(html);
|
|
5650
|
+
return match?.[1] ?? "";
|
|
5651
|
+
}
|
|
4724
5652
|
function createWebApp(config) {
|
|
4725
5653
|
const app = Fastify({ logger: false });
|
|
4726
5654
|
const database = openDatabase(config);
|
|
4727
5655
|
const messages = new MessageRepository(database);
|
|
4728
5656
|
const episodes = new EpisodeRepository(database);
|
|
4729
5657
|
const fileJobs = new FileJobRepository(database);
|
|
5658
|
+
const qaLogs = new QaLogRepository(database);
|
|
5659
|
+
const cronJobs = new CronJobRepository(database);
|
|
5660
|
+
let webActionToken = "";
|
|
5661
|
+
const tokenReady = (async () => {
|
|
5662
|
+
const secrets = await loadSecrets();
|
|
5663
|
+
if (!secrets.web.actionToken) {
|
|
5664
|
+
secrets.web.actionToken = crypto8.randomBytes(32).toString("hex");
|
|
5665
|
+
await saveSecrets(secrets);
|
|
5666
|
+
}
|
|
5667
|
+
webActionToken = getWebActionToken(secrets);
|
|
5668
|
+
})();
|
|
4730
5669
|
app.addHook("onClose", async () => {
|
|
4731
5670
|
database.close();
|
|
4732
5671
|
});
|
|
4733
|
-
app.get("/api/status", async () =>
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
5672
|
+
app.get("/api/status", async () => {
|
|
5673
|
+
await tokenReady;
|
|
5674
|
+
return {
|
|
5675
|
+
app: "ChatterCatcher",
|
|
5676
|
+
gateway: getGatewayStatus(config),
|
|
5677
|
+
data: {
|
|
5678
|
+
chats: messages.getChatCount(),
|
|
5679
|
+
messages: messages.getMessageCount(),
|
|
5680
|
+
episodes: episodes.getEpisodeCount(),
|
|
5681
|
+
files: messages.listFiles(1e3).length,
|
|
5682
|
+
qaLogs: qaLogs.getCount(),
|
|
5683
|
+
cronJobs: cronJobs.list(1e3).length
|
|
5684
|
+
},
|
|
5685
|
+
rag: {
|
|
5686
|
+
mode: "required",
|
|
5687
|
+
note: "\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22\u8BC1\u636E\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0\u3002",
|
|
5688
|
+
retrieval: {
|
|
5689
|
+
keyword: "SQLite FTS5",
|
|
5690
|
+
vector: "SQLite embedding",
|
|
5691
|
+
hybrid: true
|
|
5692
|
+
}
|
|
5693
|
+
},
|
|
5694
|
+
web: config.web
|
|
5695
|
+
};
|
|
5696
|
+
});
|
|
4753
5697
|
app.get("/api/chats", async () => ({
|
|
4754
5698
|
items: messages.listChats()
|
|
4755
5699
|
}));
|
|
@@ -4778,7 +5722,39 @@ function createWebApp(config) {
|
|
|
4778
5722
|
items: episodes.listRecentEpisodes(limit)
|
|
4779
5723
|
};
|
|
4780
5724
|
});
|
|
4781
|
-
app.
|
|
5725
|
+
app.get("/api/qa-logs", async (request) => {
|
|
5726
|
+
const limit = parseLimit(request.query.limit, 20, 100);
|
|
5727
|
+
return {
|
|
5728
|
+
items: qaLogs.listRecent(limit)
|
|
5729
|
+
};
|
|
5730
|
+
});
|
|
5731
|
+
app.get("/api/cron-jobs", async (request) => {
|
|
5732
|
+
const limit = parseLimit(request.query.limit, 50, 200);
|
|
5733
|
+
return {
|
|
5734
|
+
items: cronJobs.list(limit)
|
|
5735
|
+
};
|
|
5736
|
+
});
|
|
5737
|
+
app.delete("/api/cron-jobs/:id", async (request, reply) => {
|
|
5738
|
+
await tokenReady;
|
|
5739
|
+
if (!isAuthorizedWebAction(request, webActionToken)) {
|
|
5740
|
+
reply.code(403);
|
|
5741
|
+
return { ok: false, message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
|
|
5742
|
+
}
|
|
5743
|
+
const id = request.params.id;
|
|
5744
|
+
const job = cronJobs.get(id);
|
|
5745
|
+
if (!job) {
|
|
5746
|
+
reply.code(404);
|
|
5747
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u5B9A\u65F6\u4EFB\u52A1\u3002" };
|
|
5748
|
+
}
|
|
5749
|
+
const ok = cronJobs.deleteByChat(id, job.chatId);
|
|
5750
|
+
return { ok };
|
|
5751
|
+
});
|
|
5752
|
+
app.post("/api/process/messages", async (request, reply) => {
|
|
5753
|
+
await tokenReady;
|
|
5754
|
+
if (!isAuthorizedWebAction(request, webActionToken)) {
|
|
5755
|
+
reply.code(403);
|
|
5756
|
+
return { status: "failed", message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
|
|
5757
|
+
}
|
|
4782
5758
|
try {
|
|
4783
5759
|
return await processMessagesNow({
|
|
4784
5760
|
config,
|
|
@@ -4794,6 +5770,11 @@ function createWebApp(config) {
|
|
|
4794
5770
|
};
|
|
4795
5771
|
}
|
|
4796
5772
|
});
|
|
5773
|
+
app.get("/app.js", async (_request, reply) => {
|
|
5774
|
+
await tokenReady;
|
|
5775
|
+
reply.type("application/javascript; charset=utf-8");
|
|
5776
|
+
return extractInlineScript(buildHtml()).replaceAll("__WEB_ACTION_TOKEN__", webActionToken);
|
|
5777
|
+
});
|
|
4797
5778
|
app.get("/", async (_request, reply) => {
|
|
4798
5779
|
reply.type("text/html; charset=utf-8");
|
|
4799
5780
|
return buildHtml();
|
|
@@ -4993,6 +5974,8 @@ async function startGatewayForegroundCommand() {
|
|
|
4993
5974
|
mode: "gateway"
|
|
4994
5975
|
});
|
|
4995
5976
|
const database = openDatabase(config);
|
|
5977
|
+
const chatModel = createChatModel(config, secrets);
|
|
5978
|
+
const sender = FeishuMessageSender.fromConfig(config, secrets);
|
|
4996
5979
|
const vectorStore = hasEmbeddingConfig(config, secrets) ? new SqliteVectorStore(database, { model: config.embedding.model }) : null;
|
|
4997
5980
|
const gatewayRuntime = createFeishuGateway({
|
|
4998
5981
|
config,
|
|
@@ -5007,18 +5990,26 @@ async function startGatewayForegroundCommand() {
|
|
|
5007
5990
|
}) : void 0,
|
|
5008
5991
|
episodeProcessor: {
|
|
5009
5992
|
database,
|
|
5010
|
-
model:
|
|
5993
|
+
model: chatModel
|
|
5011
5994
|
},
|
|
5012
5995
|
imageMultimodalProcessor: config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey ? {
|
|
5013
5996
|
database,
|
|
5014
5997
|
model: createMultimodalModel(config, secrets)
|
|
5015
5998
|
} : void 0,
|
|
5999
|
+
indexingProcessor: {
|
|
6000
|
+
database
|
|
6001
|
+
},
|
|
6002
|
+
cronJobProcessor: {
|
|
6003
|
+
database,
|
|
6004
|
+
model: chatModel,
|
|
6005
|
+
sender
|
|
6006
|
+
},
|
|
5016
6007
|
questionHandler: new FeishuQuestionHandler({
|
|
5017
6008
|
config,
|
|
5018
6009
|
secrets,
|
|
5019
6010
|
database,
|
|
5020
|
-
sender
|
|
5021
|
-
model:
|
|
6011
|
+
sender,
|
|
6012
|
+
model: chatModel
|
|
5022
6013
|
})
|
|
5023
6014
|
});
|
|
5024
6015
|
const cleanup = () => {
|