chattercatcher 0.1.19 → 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 +1102 -422
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +224 -129
- package/dist/index.js +1002 -312
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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",
|
|
@@ -141,6 +141,12 @@ var appSecretsSchema = z.object({
|
|
|
141
141
|
z.object({
|
|
142
142
|
apiKey: z.string().default("")
|
|
143
143
|
})
|
|
144
|
+
),
|
|
145
|
+
web: z.preprocess(
|
|
146
|
+
(value) => value ?? {},
|
|
147
|
+
z.object({
|
|
148
|
+
actionToken: z.string().default("")
|
|
149
|
+
})
|
|
144
150
|
)
|
|
145
151
|
});
|
|
146
152
|
function createDefaultConfig() {
|
|
@@ -160,7 +166,8 @@ function createDefaultSecrets() {
|
|
|
160
166
|
feishu: {},
|
|
161
167
|
llm: {},
|
|
162
168
|
embedding: {},
|
|
163
|
-
multimodal: {}
|
|
169
|
+
multimodal: {},
|
|
170
|
+
web: {}
|
|
164
171
|
});
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -529,6 +536,23 @@ function migrateDatabase(database) {
|
|
|
529
536
|
);
|
|
530
537
|
|
|
531
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);
|
|
532
556
|
`);
|
|
533
557
|
}
|
|
534
558
|
|
|
@@ -1148,6 +1172,22 @@ function buildSearchTerms(query) {
|
|
|
1148
1172
|
}
|
|
1149
1173
|
return [trimmed];
|
|
1150
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
|
+
}
|
|
1151
1191
|
function parseRawPayload(value) {
|
|
1152
1192
|
try {
|
|
1153
1193
|
const parsed = JSON.parse(value);
|
|
@@ -1353,6 +1393,7 @@ var MessageRepository = class {
|
|
|
1353
1393
|
const ftsQuery = escapeFtsQuery(query);
|
|
1354
1394
|
const excludedIds = options.excludeMessageIds ?? [];
|
|
1355
1395
|
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
1396
|
+
const scope = buildScopeWhere(options.scope);
|
|
1356
1397
|
const ftsResults = this.database.prepare(
|
|
1357
1398
|
`
|
|
1358
1399
|
SELECT
|
|
@@ -1371,10 +1412,11 @@ var MessageRepository = class {
|
|
|
1371
1412
|
JOIN chats c ON c.id = m.chat_id
|
|
1372
1413
|
WHERE message_chunks_fts MATCH ?
|
|
1373
1414
|
${excludedWhere}
|
|
1415
|
+
${scope.where}
|
|
1374
1416
|
ORDER BY bm25(message_chunks_fts)
|
|
1375
1417
|
LIMIT ?
|
|
1376
1418
|
`
|
|
1377
|
-
).all(ftsQuery, ...excludedIds, limit);
|
|
1419
|
+
).all(ftsQuery, ...excludedIds, ...scope.params, limit);
|
|
1378
1420
|
if (ftsResults.length > 0) {
|
|
1379
1421
|
return ftsResults;
|
|
1380
1422
|
}
|
|
@@ -1402,10 +1444,11 @@ var MessageRepository = class {
|
|
|
1402
1444
|
JOIN chats c ON c.id = m.chat_id
|
|
1403
1445
|
WHERE (${where})
|
|
1404
1446
|
${likeExcludedWhere}
|
|
1447
|
+
${scope.where}
|
|
1405
1448
|
ORDER BY m.sent_at DESC
|
|
1406
1449
|
LIMIT ?
|
|
1407
1450
|
`
|
|
1408
|
-
).all(...params, ...excludedIds, limit);
|
|
1451
|
+
).all(...params, ...excludedIds, ...scope.params, limit);
|
|
1409
1452
|
}
|
|
1410
1453
|
getChatCount() {
|
|
1411
1454
|
return this.database.prepare("SELECT COUNT(*) AS count FROM chats").get().count;
|
|
@@ -1505,6 +1548,22 @@ function toMillis(value) {
|
|
|
1505
1548
|
const time = Date.parse(value);
|
|
1506
1549
|
return Number.isFinite(time) ? time : 0;
|
|
1507
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
|
+
}
|
|
1508
1567
|
var EpisodeRepository = class {
|
|
1509
1568
|
constructor(database) {
|
|
1510
1569
|
this.database = database;
|
|
@@ -1687,8 +1746,9 @@ var EpisodeRepository = class {
|
|
|
1687
1746
|
`
|
|
1688
1747
|
).all(limit);
|
|
1689
1748
|
}
|
|
1690
|
-
searchEpisodes(query, limit = 8) {
|
|
1749
|
+
searchEpisodes(query, limit = 8, scope) {
|
|
1691
1750
|
const ftsQuery = escapeFtsQuery2(query);
|
|
1751
|
+
const scopeWhere = buildScopeWhere2(scope);
|
|
1692
1752
|
return this.database.prepare(
|
|
1693
1753
|
`
|
|
1694
1754
|
SELECT
|
|
@@ -1716,11 +1776,12 @@ var EpisodeRepository = class {
|
|
|
1716
1776
|
JOIN memory_episodes e ON e.id = fts.episode_id
|
|
1717
1777
|
JOIN chats c ON c.id = e.chat_id
|
|
1718
1778
|
WHERE memory_episodes_fts MATCH ?
|
|
1779
|
+
${scopeWhere.where}
|
|
1719
1780
|
GROUP BY e.id
|
|
1720
1781
|
ORDER BY e.ended_at DESC
|
|
1721
1782
|
LIMIT ?
|
|
1722
1783
|
`
|
|
1723
|
-
).all(ftsQuery, limit).map((row) => {
|
|
1784
|
+
).all(ftsQuery, ...scopeWhere.params, limit).map((row) => {
|
|
1724
1785
|
const item = row;
|
|
1725
1786
|
return {
|
|
1726
1787
|
...item,
|
|
@@ -1750,8 +1811,8 @@ var EpisodeFtsRetriever = class {
|
|
|
1750
1811
|
this.episodes = episodes;
|
|
1751
1812
|
}
|
|
1752
1813
|
episodes;
|
|
1753
|
-
async retrieve(question) {
|
|
1754
|
-
return this.episodes.searchEpisodes(question, 8).map(toEpisodeEvidence);
|
|
1814
|
+
async retrieve(question, scope) {
|
|
1815
|
+
return this.episodes.searchEpisodes(question, 8, scope).map(toEpisodeEvidence);
|
|
1755
1816
|
}
|
|
1756
1817
|
};
|
|
1757
1818
|
|
|
@@ -1777,8 +1838,9 @@ var HybridRetriever = class {
|
|
|
1777
1838
|
}
|
|
1778
1839
|
retrievers;
|
|
1779
1840
|
options;
|
|
1780
|
-
async retrieve(question) {
|
|
1781
|
-
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)));
|
|
1782
1844
|
const merged = /* @__PURE__ */ new Map();
|
|
1783
1845
|
for (const evidenceList of results) {
|
|
1784
1846
|
for (const evidence of evidenceList) {
|
|
@@ -1819,9 +1881,10 @@ var MessageFtsRetriever = class {
|
|
|
1819
1881
|
}
|
|
1820
1882
|
messages;
|
|
1821
1883
|
options;
|
|
1822
|
-
async retrieve(question) {
|
|
1884
|
+
async retrieve(question, scope) {
|
|
1823
1885
|
const results = this.messages.searchMessages(question, 8, {
|
|
1824
|
-
excludeMessageIds: this.options.excludeMessageIds
|
|
1886
|
+
excludeMessageIds: this.options.excludeMessageIds,
|
|
1887
|
+
scope
|
|
1825
1888
|
});
|
|
1826
1889
|
return results.map((result) => ({
|
|
1827
1890
|
id: result.chunkId,
|
|
@@ -1856,17 +1919,17 @@ function parseSearchInput(input2) {
|
|
|
1856
1919
|
const limit = Math.min(12, Math.max(1, Math.floor(numericLimit)));
|
|
1857
1920
|
return { query, limit };
|
|
1858
1921
|
}
|
|
1859
|
-
async function runRetriever(retriever, input2) {
|
|
1922
|
+
async function runRetriever(retriever, input2, scope) {
|
|
1860
1923
|
const { query, limit } = parseSearchInput(input2);
|
|
1861
|
-
const results = await retriever.retrieve(query);
|
|
1924
|
+
const results = await retriever.retrieve(query, scope);
|
|
1862
1925
|
return results.slice(0, limit);
|
|
1863
1926
|
}
|
|
1864
|
-
function createSearchTool(name, description, retriever) {
|
|
1927
|
+
function createSearchTool(name, description, retriever, scope) {
|
|
1865
1928
|
return {
|
|
1866
1929
|
name,
|
|
1867
1930
|
description,
|
|
1868
1931
|
inputSchema: searchInputSchema,
|
|
1869
|
-
execute: (input2) => runRetriever(retriever, input2)
|
|
1932
|
+
execute: (input2) => runRetriever(retriever, input2, scope)
|
|
1870
1933
|
};
|
|
1871
1934
|
}
|
|
1872
1935
|
function createRagSearchTools(input2) {
|
|
@@ -1874,17 +1937,20 @@ function createRagSearchTools(input2) {
|
|
|
1874
1937
|
createSearchTool(
|
|
1875
1938
|
"hybrid_search",
|
|
1876
1939
|
"Search across all indexed RAG evidence using the default hybrid retrieval strategy.",
|
|
1877
|
-
input2.hybrid
|
|
1940
|
+
input2.hybrid,
|
|
1941
|
+
input2.scope
|
|
1878
1942
|
),
|
|
1879
1943
|
createSearchTool(
|
|
1880
1944
|
"search_messages",
|
|
1881
1945
|
"Search chat messages only when the answer likely depends on message-level evidence.",
|
|
1882
|
-
input2.messages
|
|
1946
|
+
input2.messages,
|
|
1947
|
+
input2.scope
|
|
1883
1948
|
),
|
|
1884
1949
|
createSearchTool(
|
|
1885
1950
|
"search_episodes",
|
|
1886
1951
|
"Search episode summaries only when the answer likely depends on longer-running context.",
|
|
1887
|
-
input2.episodes
|
|
1952
|
+
input2.episodes,
|
|
1953
|
+
input2.scope
|
|
1888
1954
|
)
|
|
1889
1955
|
];
|
|
1890
1956
|
if (input2.semantic) {
|
|
@@ -1892,7 +1958,8 @@ function createRagSearchTools(input2) {
|
|
|
1892
1958
|
createSearchTool(
|
|
1893
1959
|
"semantic_search",
|
|
1894
1960
|
"Search semantic vector evidence only when broader conceptual recall is needed.",
|
|
1895
|
-
input2.semantic
|
|
1961
|
+
input2.semantic,
|
|
1962
|
+
input2.scope
|
|
1896
1963
|
)
|
|
1897
1964
|
);
|
|
1898
1965
|
}
|
|
@@ -1937,6 +2004,22 @@ function toEvidenceSource2(row) {
|
|
|
1937
2004
|
timestamp: row.sentAt
|
|
1938
2005
|
};
|
|
1939
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
|
+
}
|
|
1940
2023
|
var SqliteVectorStore = class {
|
|
1941
2024
|
constructor(database, options) {
|
|
1942
2025
|
this.database = database;
|
|
@@ -1971,10 +2054,11 @@ var SqliteVectorStore = class {
|
|
|
1971
2054
|
});
|
|
1972
2055
|
transaction(records);
|
|
1973
2056
|
}
|
|
1974
|
-
async search(vector, limit) {
|
|
2057
|
+
async search(vector, limit, scope) {
|
|
1975
2058
|
if (limit <= 0) {
|
|
1976
2059
|
return [];
|
|
1977
2060
|
}
|
|
2061
|
+
const scopeWhere = buildScopeWhere3(scope);
|
|
1978
2062
|
const rows = this.database.prepare(
|
|
1979
2063
|
`
|
|
1980
2064
|
SELECT
|
|
@@ -1989,8 +2073,9 @@ var SqliteVectorStore = class {
|
|
|
1989
2073
|
JOIN messages m ON m.id = mc.message_id
|
|
1990
2074
|
JOIN chats c ON c.id = m.chat_id
|
|
1991
2075
|
WHERE e.model = ?
|
|
2076
|
+
${scopeWhere.where}
|
|
1992
2077
|
`
|
|
1993
|
-
).all(this.options.model);
|
|
2078
|
+
).all(this.options.model, ...scopeWhere.params);
|
|
1994
2079
|
return rows.flatMap((row) => {
|
|
1995
2080
|
const storedVector = parseEmbeddingJson(row.embeddingJson);
|
|
1996
2081
|
if (storedVector.length === 0) {
|
|
@@ -2022,9 +2107,9 @@ var VectorRetriever = class {
|
|
|
2022
2107
|
embedding;
|
|
2023
2108
|
store;
|
|
2024
2109
|
limit;
|
|
2025
|
-
async retrieve(question) {
|
|
2110
|
+
async retrieve(question, scope) {
|
|
2026
2111
|
const vector = await this.embedding.embed(question);
|
|
2027
|
-
return this.store.search(vector, this.limit);
|
|
2112
|
+
return this.store.search(vector, this.limit, scope);
|
|
2028
2113
|
}
|
|
2029
2114
|
};
|
|
2030
2115
|
|
|
@@ -2045,7 +2130,7 @@ async function createHybridRetriever(input2) {
|
|
|
2045
2130
|
retrievers.push(new VectorRetriever(createEmbeddingModel(input2.config, input2.secrets), vectorStore));
|
|
2046
2131
|
}
|
|
2047
2132
|
return {
|
|
2048
|
-
retriever: new HybridRetriever(retrievers),
|
|
2133
|
+
retriever: new HybridRetriever(retrievers, { scope: input2.scope }),
|
|
2049
2134
|
close: () => {
|
|
2050
2135
|
for (const closer of closers) {
|
|
2051
2136
|
closer();
|
|
@@ -2062,7 +2147,7 @@ async function createAgenticRagSearchTools(input2) {
|
|
|
2062
2147
|
) : void 0;
|
|
2063
2148
|
const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
|
|
2064
2149
|
return {
|
|
2065
|
-
tools: createRagSearchTools({ hybrid, messages, episodes, semantic }),
|
|
2150
|
+
tools: createRagSearchTools({ hybrid, messages, episodes, semantic, scope: input2.scope }),
|
|
2066
2151
|
close: () => {
|
|
2067
2152
|
}
|
|
2068
2153
|
};
|
|
@@ -2635,50 +2720,35 @@ async function ensureFeishuBotOpenId(config, secrets, options = {}) {
|
|
|
2635
2720
|
// src/feishu/gateway.ts
|
|
2636
2721
|
import * as lark2 from "@larksuiteoapi/node-sdk";
|
|
2637
2722
|
|
|
2638
|
-
// src/
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
} finally {
|
|
2658
|
-
running = false;
|
|
2723
|
+
// src/cron/jobs.ts
|
|
2724
|
+
import crypto4 from "crypto";
|
|
2725
|
+
|
|
2726
|
+
// src/cron/schedule.ts
|
|
2727
|
+
function isValidCronSchedule(schedule) {
|
|
2728
|
+
return parseCronSchedule(schedule) !== null;
|
|
2729
|
+
}
|
|
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);
|
|
2659
2742
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
if (!parsed || timer) {
|
|
2664
|
-
return;
|
|
2665
|
-
}
|
|
2666
|
-
timer = setIntervalFn(() => {
|
|
2667
|
-
void runDueNow();
|
|
2668
|
-
}, 6e4);
|
|
2669
|
-
},
|
|
2670
|
-
stop() {
|
|
2671
|
-
if (!timer) {
|
|
2672
|
-
return;
|
|
2673
|
-
}
|
|
2674
|
-
clearIntervalFn(timer);
|
|
2675
|
-
timer = void 0;
|
|
2676
|
-
},
|
|
2677
|
-
runDueNow
|
|
2678
|
-
};
|
|
2743
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
2744
|
+
}
|
|
2745
|
+
return null;
|
|
2679
2746
|
}
|
|
2680
2747
|
function matchesParsedSchedule(schedule, date) {
|
|
2681
|
-
|
|
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);
|
|
2682
2752
|
}
|
|
2683
2753
|
function parseCronSchedule(schedule) {
|
|
2684
2754
|
const fields = schedule.trim().split(/\s+/);
|
|
@@ -2697,7 +2767,7 @@ function parseCronSchedule(schedule) {
|
|
|
2697
2767
|
}
|
|
2698
2768
|
function parseMinuteField(field) {
|
|
2699
2769
|
if (field === "*") {
|
|
2700
|
-
return () => true;
|
|
2770
|
+
return { wildcard: true, matches: () => true };
|
|
2701
2771
|
}
|
|
2702
2772
|
const stepMatch = /^\*\/(\d+)$/.exec(field);
|
|
2703
2773
|
if (stepMatch) {
|
|
@@ -2705,7 +2775,7 @@ function parseMinuteField(field) {
|
|
|
2705
2775
|
if (!Number.isInteger(step) || step <= 0 || step > 59) {
|
|
2706
2776
|
return null;
|
|
2707
2777
|
}
|
|
2708
|
-
return (value) => value % step === 0;
|
|
2778
|
+
return { wildcard: false, matches: (value) => value % step === 0 };
|
|
2709
2779
|
}
|
|
2710
2780
|
if (field.includes(",")) {
|
|
2711
2781
|
const values = field.split(",").map((part) => parseExactNumber(part, 0, 59));
|
|
@@ -2713,23 +2783,23 @@ function parseMinuteField(field) {
|
|
|
2713
2783
|
return null;
|
|
2714
2784
|
}
|
|
2715
2785
|
const allowed = new Set(values);
|
|
2716
|
-
return (value) => allowed.has(value);
|
|
2786
|
+
return { wildcard: false, matches: (value) => allowed.has(value) };
|
|
2717
2787
|
}
|
|
2718
2788
|
const exact = parseExactNumber(field, 0, 59);
|
|
2719
2789
|
if (exact === null) {
|
|
2720
2790
|
return null;
|
|
2721
2791
|
}
|
|
2722
|
-
return (value) => value === exact;
|
|
2792
|
+
return { wildcard: false, matches: (value) => value === exact };
|
|
2723
2793
|
}
|
|
2724
2794
|
function parseExactOrWildcardField(field, min, max) {
|
|
2725
2795
|
if (field === "*") {
|
|
2726
|
-
return () => true;
|
|
2796
|
+
return { wildcard: true, matches: () => true };
|
|
2727
2797
|
}
|
|
2728
2798
|
const exact = parseExactNumber(field, min, max);
|
|
2729
2799
|
if (exact === null) {
|
|
2730
2800
|
return null;
|
|
2731
2801
|
}
|
|
2732
|
-
return (value) => value === exact;
|
|
2802
|
+
return { wildcard: false, matches: (value) => value === exact };
|
|
2733
2803
|
}
|
|
2734
2804
|
function parseExactNumber(field, min, max) {
|
|
2735
2805
|
if (!/^\d+$/.test(field)) {
|
|
@@ -2742,110 +2812,531 @@ function parseExactNumber(field, min, max) {
|
|
|
2742
2812
|
return value;
|
|
2743
2813
|
}
|
|
2744
2814
|
|
|
2745
|
-
// src/
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2815
|
+
// src/cron/jobs.ts
|
|
2816
|
+
var CronJobRepository = class {
|
|
2817
|
+
constructor(database, options = {}) {
|
|
2818
|
+
this.database = database;
|
|
2819
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2750
2820
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
const
|
|
2755
|
-
|
|
2756
|
-
|
|
2821
|
+
database;
|
|
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");
|
|
2757
2828
|
}
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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
|
+
};
|
|
2848
|
+
this.database.prepare(
|
|
2849
|
+
`
|
|
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);
|
|
2860
|
+
return record;
|
|
2861
|
+
}
|
|
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) {
|
|
2876
|
+
const rows = this.database.prepare(
|
|
2877
|
+
`
|
|
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
|
+
}));
|
|
2909
|
+
}
|
|
2910
|
+
deleteByChat(id, chatId) {
|
|
2911
|
+
const now = this.now().toISOString();
|
|
2912
|
+
const result = this.database.prepare(
|
|
2913
|
+
`
|
|
2914
|
+
UPDATE cron_jobs
|
|
2915
|
+
SET status = 'deleted', updated_at = @updatedAt
|
|
2916
|
+
WHERE id = @id AND chat_id = @chatId AND status = 'active'
|
|
2917
|
+
`
|
|
2918
|
+
).run({ id, chatId, updatedAt: now });
|
|
2919
|
+
return result.changes > 0;
|
|
2920
|
+
}
|
|
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
|
+
}
|
|
2930
|
+
this.database.prepare(
|
|
2931
|
+
`
|
|
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()
|
|
2767
2941
|
});
|
|
2768
2942
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
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
|
+
}
|
|
2952
|
+
this.database.prepare(
|
|
2953
|
+
`
|
|
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
|
+
});
|
|
2782
2965
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2966
|
+
listByWhere(whereSql, params, limit) {
|
|
2967
|
+
const rows = this.database.prepare(
|
|
2968
|
+
`
|
|
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
|
+
}));
|
|
2803
3000
|
}
|
|
2804
|
-
|
|
2805
|
-
model: input2.config.embedding.model
|
|
2806
|
-
});
|
|
2807
|
-
const embedding = input2.embedding ?? createEmbeddingModel(input2.config, input2.secrets);
|
|
2808
|
-
const stats = await indexMessageChunks({
|
|
2809
|
-
messages: new MessageRepository(input2.database),
|
|
2810
|
-
embedding,
|
|
2811
|
-
store: vectorStore,
|
|
2812
|
-
limit: input2.limit
|
|
2813
|
-
});
|
|
2814
|
-
return {
|
|
2815
|
-
status: "completed",
|
|
2816
|
-
chunks: stats.chunks,
|
|
2817
|
-
vectors: stats.vectors,
|
|
2818
|
-
startedAt,
|
|
2819
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2820
|
-
};
|
|
2821
|
-
}
|
|
3001
|
+
};
|
|
2822
3002
|
|
|
2823
|
-
// src/
|
|
2824
|
-
|
|
2825
|
-
function
|
|
2826
|
-
|
|
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");
|
|
2827
3010
|
}
|
|
2828
|
-
function
|
|
2829
|
-
return
|
|
3011
|
+
function toolResultContent(results) {
|
|
3012
|
+
return JSON.stringify(results.map((item) => ({ id: item.id, text: item.text, score: item.score, source: item.source })));
|
|
2830
3013
|
}
|
|
2831
|
-
function
|
|
2832
|
-
if (!
|
|
2833
|
-
|
|
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");
|
|
2834
3017
|
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
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
|
+
};
|
|
2849
3340
|
}
|
|
2850
3341
|
var ImageMultimodalTaskRepository = class {
|
|
2851
3342
|
constructor(database) {
|
|
@@ -3082,231 +3573,79 @@ var ImageMultimodalWorker = class {
|
|
|
3082
3573
|
}
|
|
3083
3574
|
};
|
|
3084
3575
|
|
|
3085
|
-
// src/
|
|
3086
|
-
function
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
if (!value) {
|
|
3091
|
-
return "\u672A\u77E5\u65F6\u95F4";
|
|
3092
|
-
}
|
|
3093
|
-
const date = new Date(value);
|
|
3094
|
-
if (Number.isNaN(date.getTime())) {
|
|
3095
|
-
return value;
|
|
3096
|
-
}
|
|
3097
|
-
const pad = (input2) => String(input2).padStart(2, "0");
|
|
3098
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
3099
|
-
}
|
|
3100
|
-
function formatSpeaker(source) {
|
|
3101
|
-
if (source.type === "file") {
|
|
3102
|
-
return isOpaqueId(source.label) ? "\u6587\u4EF6" : `\u6587\u4EF6 ${source.label}`;
|
|
3103
|
-
}
|
|
3104
|
-
if (source.sender && !isOpaqueId(source.sender)) {
|
|
3105
|
-
return source.sender;
|
|
3106
|
-
}
|
|
3107
|
-
return "\u7FA4\u6210\u5458";
|
|
3108
|
-
}
|
|
3109
|
-
function clipText(value, maxLength) {
|
|
3110
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
3111
|
-
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
3112
|
-
}
|
|
3113
|
-
function formatCitation(citation, options = {}) {
|
|
3114
|
-
const maxTextLength = options.maxTextLength ?? 120;
|
|
3115
|
-
const speaker = formatSpeaker(citation.source);
|
|
3116
|
-
const time = formatTime(citation.source.timestamp);
|
|
3117
|
-
const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
|
|
3118
|
-
return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
|
|
3119
|
-
}
|
|
3120
|
-
function formatCitations(citations, options = {}) {
|
|
3121
|
-
return citations.map((citation) => formatCitation(citation, options)).join("\n");
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
// src/rag/answer.ts
|
|
3125
|
-
var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
|
|
3126
|
-
var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
|
|
3127
|
-
var SCORE_TIE_THRESHOLD = 0.15;
|
|
3128
|
-
function parseTimestamp(value) {
|
|
3129
|
-
if (!value) {
|
|
3130
|
-
return 0;
|
|
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`);
|
|
3131
3581
|
}
|
|
3132
|
-
|
|
3133
|
-
return Number.isFinite(time) ? time : 0;
|
|
3582
|
+
return value.trim();
|
|
3134
3583
|
}
|
|
3135
|
-
function
|
|
3136
|
-
return [
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
|
|
3155
|
-
const citations = selected.map((item, index2) => ({
|
|
3156
|
-
marker: `S${index2 + 1}`,
|
|
3157
|
-
evidenceId: item.id,
|
|
3158
|
-
source: item.source,
|
|
3159
|
-
text: item.text
|
|
3160
|
-
}));
|
|
3161
|
-
const evidenceText = selected.map((item, index2) => {
|
|
3162
|
-
const marker = citations[index2]?.marker;
|
|
3163
|
-
const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
|
|
3164
|
-
const sourceParts = [
|
|
3165
|
-
item.source.label,
|
|
3166
|
-
item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
|
|
3167
|
-
item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
|
|
3168
|
-
item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
|
|
3169
|
-
].filter(Boolean);
|
|
3170
|
-
return `[${marker}]
|
|
3171
|
-
\u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
|
|
3172
|
-
\u5185\u5BB9\uFF1A${clippedText}`;
|
|
3173
|
-
}).join("\n\n");
|
|
3174
|
-
return {
|
|
3175
|
-
citations,
|
|
3176
|
-
messages: [
|
|
3177
|
-
{
|
|
3178
|
-
role: "system",
|
|
3179
|
-
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"
|
|
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
|
|
3180
3603
|
},
|
|
3181
|
-
{
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
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
|
|
3188
|
-
3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
|
|
3189
|
-
|
|
3190
|
-
\u68C0\u7D22\u8BC1\u636E\uFF1A
|
|
3191
|
-
${evidenceText}`
|
|
3192
|
-
}
|
|
3193
|
-
]
|
|
3194
|
-
};
|
|
3195
|
-
}
|
|
3196
|
-
async function generateGroundedAnswer(input2) {
|
|
3197
|
-
const prompt = buildEvidencePrompt(input2.question, input2.evidence);
|
|
3198
|
-
const answer = await input2.model.complete(prompt.messages);
|
|
3199
|
-
return {
|
|
3200
|
-
answer,
|
|
3201
|
-
citations: prompt.citations
|
|
3202
|
-
};
|
|
3203
|
-
}
|
|
3204
|
-
|
|
3205
|
-
// src/rag/agentic-qa-service.ts
|
|
3206
|
-
var DEFAULT_MAX_MODEL_TURNS = 4;
|
|
3207
|
-
var DEFAULT_MAX_TOOL_CALLS = 8;
|
|
3208
|
-
var DEFAULT_MAX_EVIDENCE = 12;
|
|
3209
|
-
var NO_EVIDENCE_ANSWER = "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002";
|
|
3210
|
-
var AGENTIC_SYSTEM_PROMPT = "\u4F60\u662F\u672C\u5730\u77E5\u8BC6\u4FE1\u606F\u6536\u96C6\u4EE3\u7406\u3002\u4F60\u7684\u804C\u8D23\u662F\u56F4\u7ED5\u7528\u6237\u95EE\u9898\u51B3\u5B9A\u662F\u5426\u8C03\u7528\u641C\u7D22\u5DE5\u5177\u3001\u9009\u62E9\u5408\u9002\u7684\u5DE5\u5177\u548C\u67E5\u8BE2\u8BCD\uFF0C\u5E76\u6839\u636E\u5F53\u524D\u7ED3\u679C\u51B3\u5B9A\u662F\u5426\u7EE7\u7EED\u641C\u7D22\u3002\u4E0D\u8981\u7F16\u9020\u4EFB\u4F55\u8BC1\u636E\u6216\u58F0\u79F0\u770B\u8FC7\u672A\u68C0\u7D22\u5230\u7684\u5185\u5BB9\u3002\u4F60\u7684\u8F93\u51FA\u53EA\u7528\u4E8E\u6536\u96C6\u8BC1\u636E\uFF0C\u6700\u7EC8\u7B54\u6848\u4F1A\u7531\u53E6\u4E00\u4E2A\u57FA\u4E8E\u8BC1\u636E\u7684\u6B65\u9AA4\u751F\u6210\u3002";
|
|
3211
|
-
function toToolResultContent(results) {
|
|
3212
|
-
return JSON.stringify(
|
|
3213
|
-
results.map((item) => ({
|
|
3214
|
-
id: item.id,
|
|
3215
|
-
text: item.text,
|
|
3216
|
-
score: item.score,
|
|
3217
|
-
source: item.source
|
|
3218
|
-
}))
|
|
3219
|
-
);
|
|
3220
|
-
}
|
|
3221
|
-
function toToolErrorContent(message) {
|
|
3222
|
-
return JSON.stringify({ error: message });
|
|
3223
|
-
}
|
|
3224
|
-
function dedupeEvidence(evidence, maxEvidence) {
|
|
3225
|
-
const deduped = [];
|
|
3226
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3227
|
-
for (const item of evidence) {
|
|
3228
|
-
if (seen.has(item.id)) {
|
|
3229
|
-
continue;
|
|
3230
|
-
}
|
|
3231
|
-
seen.add(item.id);
|
|
3232
|
-
deduped.push(item);
|
|
3233
|
-
if (deduped.length >= maxEvidence) {
|
|
3234
|
-
break;
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
return deduped;
|
|
3238
|
-
}
|
|
3239
|
-
async function askWithAgenticRag(input2) {
|
|
3240
|
-
if (!input2.model.completeWithTools) {
|
|
3241
|
-
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3242
|
-
}
|
|
3243
|
-
const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3244
|
-
const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
3245
|
-
const maxEvidence = input2.maxEvidence ?? DEFAULT_MAX_EVIDENCE;
|
|
3246
|
-
const messages = [
|
|
3247
|
-
{ role: "system", content: AGENTIC_SYSTEM_PROMPT },
|
|
3248
|
-
{ role: "user", content: input2.question }
|
|
3249
|
-
];
|
|
3250
|
-
const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
|
|
3251
|
-
let evidence = [];
|
|
3252
|
-
let toolCallsUsed = 0;
|
|
3253
|
-
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
3254
|
-
const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
|
|
3255
|
-
messages.push({
|
|
3256
|
-
role: "assistant",
|
|
3257
|
-
content: assistantResult.content,
|
|
3258
|
-
toolCalls: assistantResult.toolCalls
|
|
3259
|
-
});
|
|
3260
|
-
if (assistantResult.toolCalls.length === 0) {
|
|
3261
|
-
break;
|
|
3262
|
-
}
|
|
3263
|
-
for (const toolCall of assistantResult.toolCalls) {
|
|
3264
|
-
if (toolCallsUsed >= maxToolCalls) {
|
|
3265
|
-
break;
|
|
3266
|
-
}
|
|
3267
|
-
toolCallsUsed += 1;
|
|
3268
|
-
const tool = toolsByName.get(toolCall.name);
|
|
3269
|
-
if (!tool) {
|
|
3270
|
-
messages.push({
|
|
3271
|
-
role: "tool",
|
|
3272
|
-
toolCallId: toolCall.id,
|
|
3273
|
-
content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
|
|
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")
|
|
3274
3610
|
});
|
|
3275
|
-
|
|
3611
|
+
return JSON.stringify({ ok: true, job });
|
|
3276
3612
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
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"
|
|
3291
3641
|
});
|
|
3292
3642
|
}
|
|
3293
3643
|
}
|
|
3294
|
-
|
|
3295
|
-
if (evidence.length === 0) {
|
|
3296
|
-
return {
|
|
3297
|
-
answer: NO_EVIDENCE_ANSWER,
|
|
3298
|
-
citations: []
|
|
3299
|
-
};
|
|
3300
|
-
}
|
|
3301
|
-
return generateGroundedAnswer({
|
|
3302
|
-
question: input2.question,
|
|
3303
|
-
evidence,
|
|
3304
|
-
model: input2.model
|
|
3305
|
-
});
|
|
3644
|
+
];
|
|
3306
3645
|
}
|
|
3307
3646
|
|
|
3308
3647
|
// src/rag/qa-logs.ts
|
|
3309
|
-
import
|
|
3648
|
+
import crypto6 from "crypto";
|
|
3310
3649
|
function clampLimit(limit) {
|
|
3311
3650
|
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3312
3651
|
}
|
|
@@ -3317,7 +3656,7 @@ var QaLogRepository = class {
|
|
|
3317
3656
|
database;
|
|
3318
3657
|
create(input2) {
|
|
3319
3658
|
const record = {
|
|
3320
|
-
id: `qa_${
|
|
3659
|
+
id: `qa_${crypto6.randomUUID()}`,
|
|
3321
3660
|
chatId: input2.chatId ?? null,
|
|
3322
3661
|
questionMessageId: input2.questionMessageId ?? null,
|
|
3323
3662
|
question: input2.question,
|
|
@@ -3430,6 +3769,76 @@ function stripMentions(text, mentions) {
|
|
|
3430
3769
|
}
|
|
3431
3770
|
return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
|
|
3432
3771
|
}
|
|
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";
|
|
3773
|
+
var DEFAULT_MAX_MODEL_TURNS = 4;
|
|
3774
|
+
var DEFAULT_MAX_TOOL_CALLS = 8;
|
|
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);
|
|
3779
|
+
}
|
|
3780
|
+
function toToolErrorContent(message) {
|
|
3781
|
+
return JSON.stringify({ ok: false, error: message });
|
|
3782
|
+
}
|
|
3783
|
+
async function executeFeishuTool(tool, input2) {
|
|
3784
|
+
const result = await tool.execute(input2);
|
|
3785
|
+
return toToolResultContent(result);
|
|
3786
|
+
}
|
|
3787
|
+
async function runFeishuToolLoop(input2) {
|
|
3788
|
+
if (!input2.model.completeWithTools) {
|
|
3789
|
+
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3790
|
+
}
|
|
3791
|
+
const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3792
|
+
const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
3793
|
+
const messages = [
|
|
3794
|
+
{ role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
|
|
3795
|
+
{ role: "user", content: input2.question }
|
|
3796
|
+
];
|
|
3797
|
+
const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
|
|
3798
|
+
let toolCallsUsed = 0;
|
|
3799
|
+
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
3800
|
+
const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
|
|
3801
|
+
messages.push({
|
|
3802
|
+
role: "assistant",
|
|
3803
|
+
content: assistantResult.content,
|
|
3804
|
+
toolCalls: assistantResult.toolCalls
|
|
3805
|
+
});
|
|
3806
|
+
if (assistantResult.toolCalls.length === 0) {
|
|
3807
|
+
return assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
3808
|
+
}
|
|
3809
|
+
for (const toolCall of assistantResult.toolCalls) {
|
|
3810
|
+
if (toolCallsUsed >= maxToolCalls) {
|
|
3811
|
+
return FEISHU_TOOL_LOOP_LIMIT_REACHED;
|
|
3812
|
+
}
|
|
3813
|
+
toolCallsUsed += 1;
|
|
3814
|
+
const tool = toolsByName.get(toolCall.name);
|
|
3815
|
+
if (!tool) {
|
|
3816
|
+
messages.push({
|
|
3817
|
+
role: "tool",
|
|
3818
|
+
toolCallId: toolCall.id,
|
|
3819
|
+
content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
|
|
3820
|
+
});
|
|
3821
|
+
continue;
|
|
3822
|
+
}
|
|
3823
|
+
try {
|
|
3824
|
+
const result = await executeFeishuTool(tool, toolCall.input);
|
|
3825
|
+
messages.push({
|
|
3826
|
+
role: "tool",
|
|
3827
|
+
toolCallId: toolCall.id,
|
|
3828
|
+
content: result
|
|
3829
|
+
});
|
|
3830
|
+
} catch (error) {
|
|
3831
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3832
|
+
messages.push({
|
|
3833
|
+
role: "tool",
|
|
3834
|
+
toolCallId: toolCall.id,
|
|
3835
|
+
content: toToolErrorContent(message)
|
|
3836
|
+
});
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
return FEISHU_TOOL_LOOP_FALLBACK;
|
|
3841
|
+
}
|
|
3433
3842
|
function isMentionForBot(mention, config) {
|
|
3434
3843
|
if (!config.feishu.botOpenId) {
|
|
3435
3844
|
return false;
|
|
@@ -3524,27 +3933,28 @@ var FeishuQuestionHandler = class {
|
|
|
3524
3933
|
});
|
|
3525
3934
|
try {
|
|
3526
3935
|
try {
|
|
3527
|
-
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({
|
|
3528
3943
|
question: decision.question,
|
|
3529
|
-
tools,
|
|
3944
|
+
tools: allTools,
|
|
3530
3945
|
model: this.options.model
|
|
3531
3946
|
});
|
|
3532
3947
|
qaLogs.create({
|
|
3533
3948
|
chatId: decision.chatId,
|
|
3534
3949
|
questionMessageId,
|
|
3535
3950
|
question: decision.question,
|
|
3536
|
-
answer
|
|
3537
|
-
citations:
|
|
3538
|
-
retrievalDebug: {
|
|
3951
|
+
answer,
|
|
3952
|
+
citations: [],
|
|
3953
|
+
retrievalDebug: {},
|
|
3539
3954
|
status: "answered",
|
|
3540
3955
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3541
3956
|
});
|
|
3542
|
-
|
|
3543
|
-
const text = citations ? `${result.answer}
|
|
3544
|
-
|
|
3545
|
-
\u5F15\u7528\uFF1A
|
|
3546
|
-
${citations}` : result.answer;
|
|
3547
|
-
await this.sendResponse(decision.chatId, questionMessageId, text);
|
|
3957
|
+
await this.sendResponse(decision.chatId, questionMessageId, answer);
|
|
3548
3958
|
} catch (error) {
|
|
3549
3959
|
const message = error instanceof Error ? error.message : String(error);
|
|
3550
3960
|
qaLogs.create({
|
|
@@ -3790,18 +4200,39 @@ function createFeishuGateway(options) {
|
|
|
3790
4200
|
});
|
|
3791
4201
|
}
|
|
3792
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);
|
|
3793
4221
|
return {
|
|
3794
4222
|
async start() {
|
|
3795
4223
|
try {
|
|
3796
4224
|
await wsClient.start({ eventDispatcher });
|
|
3797
4225
|
indexingScheduler?.start();
|
|
4226
|
+
cronJobScheduler?.start();
|
|
3798
4227
|
} catch (error) {
|
|
3799
4228
|
indexingScheduler?.stop();
|
|
4229
|
+
cronJobScheduler?.stop();
|
|
3800
4230
|
throw formatGatewayStartError(error);
|
|
3801
4231
|
}
|
|
3802
4232
|
},
|
|
3803
4233
|
stop() {
|
|
3804
4234
|
indexingScheduler?.stop();
|
|
4235
|
+
cronJobScheduler?.stop();
|
|
3805
4236
|
wsClient.close({ force: true });
|
|
3806
4237
|
}
|
|
3807
4238
|
};
|
|
@@ -3873,7 +4304,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
3873
4304
|
};
|
|
3874
4305
|
|
|
3875
4306
|
// src/files/ingest.ts
|
|
3876
|
-
import
|
|
4307
|
+
import crypto7 from "crypto";
|
|
3877
4308
|
import fs11 from "fs/promises";
|
|
3878
4309
|
import path13 from "path";
|
|
3879
4310
|
|
|
@@ -3937,7 +4368,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
3937
4368
|
}
|
|
3938
4369
|
}
|
|
3939
4370
|
function stableStoredName(sourcePath, fileName) {
|
|
3940
|
-
const digest =
|
|
4371
|
+
const digest = crypto7.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
3941
4372
|
return `${digest}-${fileName}`;
|
|
3942
4373
|
}
|
|
3943
4374
|
async function ingestLocalFile(input2) {
|
|
@@ -4459,6 +4890,87 @@ function createMultimodalModel(config, secrets) {
|
|
|
4459
4890
|
});
|
|
4460
4891
|
}
|
|
4461
4892
|
|
|
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;
|
|
4900
|
+
}
|
|
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;
|
|
4909
|
+
}
|
|
4910
|
+
const timeDiff = parseTimestamp(right.source.timestamp) - parseTimestamp(left.source.timestamp);
|
|
4911
|
+
if (timeDiff !== 0) {
|
|
4912
|
+
return timeDiff;
|
|
4913
|
+
}
|
|
4914
|
+
return scoreDiff;
|
|
4915
|
+
});
|
|
4916
|
+
}
|
|
4917
|
+
function buildEvidencePrompt(question, evidence, options = {}) {
|
|
4918
|
+
if (evidence.length === 0) {
|
|
4919
|
+
throw new Error("RAG evidence is required before answer generation.");
|
|
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");
|
|
4943
|
+
return {
|
|
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
|
+
]
|
|
4963
|
+
};
|
|
4964
|
+
}
|
|
4965
|
+
async function generateGroundedAnswer(input2) {
|
|
4966
|
+
const prompt = buildEvidencePrompt(input2.question, input2.evidence);
|
|
4967
|
+
const answer = await input2.model.complete(prompt.messages);
|
|
4968
|
+
return {
|
|
4969
|
+
answer,
|
|
4970
|
+
citations: prompt.citations
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4462
4974
|
// src/rag/qa-service.ts
|
|
4463
4975
|
async function askWithRag(input2) {
|
|
4464
4976
|
const evidence = await input2.retriever.retrieve(input2.question);
|
|
@@ -4475,6 +4987,42 @@ async function askWithRag(input2) {
|
|
|
4475
4987
|
});
|
|
4476
4988
|
}
|
|
4477
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
|
+
|
|
4478
5026
|
// src/update/npm-updater.ts
|
|
4479
5027
|
import { execFile } from "child_process";
|
|
4480
5028
|
import { promisify } from "util";
|
|
@@ -4578,6 +5126,7 @@ async function updateChatterCatcher(options) {
|
|
|
4578
5126
|
}
|
|
4579
5127
|
|
|
4580
5128
|
// src/web/server.ts
|
|
5129
|
+
import crypto8 from "crypto";
|
|
4581
5130
|
import Fastify from "fastify";
|
|
4582
5131
|
function buildHtml() {
|
|
4583
5132
|
return `<!doctype html>
|
|
@@ -4725,6 +5274,10 @@ function buildHtml() {
|
|
|
4725
5274
|
<h2>\u89E3\u6790\u4EFB\u52A1</h2>
|
|
4726
5275
|
<div id="file-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
4727
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>
|
|
4728
5281
|
<section>
|
|
4729
5282
|
<h2>\u672C\u5730\u64CD\u4F5C</h2>
|
|
4730
5283
|
<p><code>chattercatcher settings</code> \u4FEE\u6539\u914D\u7F6E\u3002</p>
|
|
@@ -4741,10 +5294,13 @@ function buildHtml() {
|
|
|
4741
5294
|
const chats = document.querySelector("#chats");
|
|
4742
5295
|
const files = document.querySelector("#files");
|
|
4743
5296
|
const fileJobs = document.querySelector("#file-jobs");
|
|
5297
|
+
const cronJobs = document.querySelector("#cron-jobs");
|
|
4744
5298
|
const qaLogs = document.querySelector("#qa-logs");
|
|
4745
5299
|
const processMessages = document.querySelector("#process-messages");
|
|
4746
5300
|
const actionStatus = document.querySelector("#action-status");
|
|
4747
5301
|
|
|
5302
|
+
let webActionToken = "__WEB_ACTION_TOKEN__";
|
|
5303
|
+
|
|
4748
5304
|
function fmt(value) {
|
|
4749
5305
|
return value == null || value === "" ? "-" : String(value);
|
|
4750
5306
|
}
|
|
@@ -4933,6 +5489,36 @@ function buildHtml() {
|
|
|
4933
5489
|
\`;
|
|
4934
5490
|
}
|
|
4935
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
|
+
|
|
4936
5522
|
function renderQaLogs(items) {
|
|
4937
5523
|
if (items.length === 0) {
|
|
4938
5524
|
qaLogs.className = "empty";
|
|
@@ -4964,7 +5550,7 @@ function buildHtml() {
|
|
|
4964
5550
|
}
|
|
4965
5551
|
|
|
4966
5552
|
async function load() {
|
|
4967
|
-
const [status, recent, episodeList, chatList, fileList, jobList, qaLogList] = await Promise.all([
|
|
5553
|
+
const [status, recent, episodeList, chatList, fileList, jobList, qaLogList, cronJobList] = await Promise.all([
|
|
4968
5554
|
fetch("/api/status").then((response) => response.json()),
|
|
4969
5555
|
fetch("/api/messages/recent?limit=20").then((response) => response.json()),
|
|
4970
5556
|
fetch("/api/episodes?limit=10").then((response) => response.json()),
|
|
@@ -4972,6 +5558,7 @@ function buildHtml() {
|
|
|
4972
5558
|
fetch("/api/files").then((response) => response.json()),
|
|
4973
5559
|
fetch("/api/file-jobs").then((response) => response.json()),
|
|
4974
5560
|
fetch("/api/qa-logs?limit=10").then((response) => response.json()),
|
|
5561
|
+
fetch("/api/cron-jobs").then((response) => response.json()),
|
|
4975
5562
|
]);
|
|
4976
5563
|
renderMetrics(status);
|
|
4977
5564
|
renderMessages(recent.items);
|
|
@@ -4980,13 +5567,17 @@ function buildHtml() {
|
|
|
4980
5567
|
renderFiles(fileList.items);
|
|
4981
5568
|
renderFileJobs(jobList.items);
|
|
4982
5569
|
renderQaLogs(qaLogList.items);
|
|
5570
|
+
renderCronJobs(cronJobList.items);
|
|
4983
5571
|
}
|
|
4984
5572
|
|
|
4985
5573
|
async function processNow() {
|
|
4986
5574
|
processMessages.disabled = true;
|
|
4987
5575
|
actionStatus.textContent = "\u6B63\u5728\u5904\u7406\u6D88\u606F\u7D22\u5F15...";
|
|
4988
5576
|
try {
|
|
4989
|
-
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
|
+
});
|
|
4990
5581
|
const result = await response.json();
|
|
4991
5582
|
if (!response.ok) {
|
|
4992
5583
|
actionStatus.textContent = result.message || "\u5904\u7406\u5931\u8D25\u3002";
|
|
@@ -5006,6 +5597,28 @@ function buildHtml() {
|
|
|
5006
5597
|
}
|
|
5007
5598
|
}
|
|
5008
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
|
+
|
|
5009
5622
|
processMessages.addEventListener("click", () => void processNow());
|
|
5010
5623
|
void load();
|
|
5011
5624
|
setInterval(() => {
|
|
@@ -5014,6 +5627,7 @@ function buildHtml() {
|
|
|
5014
5627
|
}
|
|
5015
5628
|
}, 5000);
|
|
5016
5629
|
</script>
|
|
5630
|
+
<script src="/app.js"></script>
|
|
5017
5631
|
</body>
|
|
5018
5632
|
</html>`;
|
|
5019
5633
|
}
|
|
@@ -5021,6 +5635,20 @@ function parseLimit(value, fallback, max) {
|
|
|
5021
5635
|
const rawLimit = Number(value ?? fallback);
|
|
5022
5636
|
return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
|
|
5023
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
|
+
}
|
|
5024
5652
|
function createWebApp(config) {
|
|
5025
5653
|
const app = Fastify({ logger: false });
|
|
5026
5654
|
const database = openDatabase(config);
|
|
@@ -5028,30 +5656,44 @@ function createWebApp(config) {
|
|
|
5028
5656
|
const episodes = new EpisodeRepository(database);
|
|
5029
5657
|
const fileJobs = new FileJobRepository(database);
|
|
5030
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
|
+
})();
|
|
5031
5669
|
app.addHook("onClose", async () => {
|
|
5032
5670
|
database.close();
|
|
5033
5671
|
});
|
|
5034
|
-
app.get("/api/status", async () =>
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
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
|
+
});
|
|
5055
5697
|
app.get("/api/chats", async () => ({
|
|
5056
5698
|
items: messages.listChats()
|
|
5057
5699
|
}));
|
|
@@ -5086,7 +5728,33 @@ function createWebApp(config) {
|
|
|
5086
5728
|
items: qaLogs.listRecent(limit)
|
|
5087
5729
|
};
|
|
5088
5730
|
});
|
|
5089
|
-
app.
|
|
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
|
+
}
|
|
5090
5758
|
try {
|
|
5091
5759
|
return await processMessagesNow({
|
|
5092
5760
|
config,
|
|
@@ -5102,6 +5770,11 @@ function createWebApp(config) {
|
|
|
5102
5770
|
};
|
|
5103
5771
|
}
|
|
5104
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
|
+
});
|
|
5105
5778
|
app.get("/", async (_request, reply) => {
|
|
5106
5779
|
reply.type("text/html; charset=utf-8");
|
|
5107
5780
|
return buildHtml();
|
|
@@ -5301,6 +5974,8 @@ async function startGatewayForegroundCommand() {
|
|
|
5301
5974
|
mode: "gateway"
|
|
5302
5975
|
});
|
|
5303
5976
|
const database = openDatabase(config);
|
|
5977
|
+
const chatModel = createChatModel(config, secrets);
|
|
5978
|
+
const sender = FeishuMessageSender.fromConfig(config, secrets);
|
|
5304
5979
|
const vectorStore = hasEmbeddingConfig(config, secrets) ? new SqliteVectorStore(database, { model: config.embedding.model }) : null;
|
|
5305
5980
|
const gatewayRuntime = createFeishuGateway({
|
|
5306
5981
|
config,
|
|
@@ -5315,7 +5990,7 @@ async function startGatewayForegroundCommand() {
|
|
|
5315
5990
|
}) : void 0,
|
|
5316
5991
|
episodeProcessor: {
|
|
5317
5992
|
database,
|
|
5318
|
-
model:
|
|
5993
|
+
model: chatModel
|
|
5319
5994
|
},
|
|
5320
5995
|
imageMultimodalProcessor: config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey ? {
|
|
5321
5996
|
database,
|
|
@@ -5324,12 +5999,17 @@ async function startGatewayForegroundCommand() {
|
|
|
5324
5999
|
indexingProcessor: {
|
|
5325
6000
|
database
|
|
5326
6001
|
},
|
|
6002
|
+
cronJobProcessor: {
|
|
6003
|
+
database,
|
|
6004
|
+
model: chatModel,
|
|
6005
|
+
sender
|
|
6006
|
+
},
|
|
5327
6007
|
questionHandler: new FeishuQuestionHandler({
|
|
5328
6008
|
config,
|
|
5329
6009
|
secrets,
|
|
5330
6010
|
database,
|
|
5331
|
-
sender
|
|
5332
|
-
model:
|
|
6011
|
+
sender,
|
|
6012
|
+
model: chatModel
|
|
5333
6013
|
})
|
|
5334
6014
|
});
|
|
5335
6015
|
const cleanup = () => {
|