chattercatcher 0.1.16 → 0.1.18
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 +820 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +168 -75
- package/dist/index.js +703 -13
- package/dist/index.js.map +1 -1
- package/docs/superpowers/plans/2026-05-02-agentic-rag.md +1013 -0
- package/docs/superpowers/plans/2026-05-02-image-multimodal-memory.md +1888 -0
- package/docs/superpowers/specs/2026-05-02-agentic-rag-design.md +143 -0
- package/docs/superpowers/specs/2026-05-02-image-multimodal-memory-design.md +160 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { input, password, select, confirm, number } from "@inquirer/prompts";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import
|
|
6
|
+
import fs14 from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "chattercatcher",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.18",
|
|
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",
|
|
@@ -102,6 +102,13 @@ var appConfigSchema = z.object({
|
|
|
102
102
|
model: z.string().default(""),
|
|
103
103
|
dimension: z.number().int().positive().nullable().default(null)
|
|
104
104
|
}),
|
|
105
|
+
multimodal: z.preprocess(
|
|
106
|
+
(value) => value ?? {},
|
|
107
|
+
z.object({
|
|
108
|
+
baseUrl: z.string().url().or(z.literal("")).default(""),
|
|
109
|
+
model: z.string().default("")
|
|
110
|
+
})
|
|
111
|
+
),
|
|
105
112
|
storage: z.object({
|
|
106
113
|
dataDir: z.string().default(defaultDataDir)
|
|
107
114
|
}),
|
|
@@ -126,13 +133,20 @@ var appSecretsSchema = z.object({
|
|
|
126
133
|
}),
|
|
127
134
|
embedding: z.object({
|
|
128
135
|
apiKey: z.string().default("")
|
|
129
|
-
})
|
|
136
|
+
}),
|
|
137
|
+
multimodal: z.preprocess(
|
|
138
|
+
(value) => value ?? {},
|
|
139
|
+
z.object({
|
|
140
|
+
apiKey: z.string().default("")
|
|
141
|
+
})
|
|
142
|
+
)
|
|
130
143
|
});
|
|
131
144
|
function createDefaultConfig() {
|
|
132
145
|
return appConfigSchema.parse({
|
|
133
146
|
feishu: {},
|
|
134
147
|
llm: {},
|
|
135
148
|
embedding: {},
|
|
149
|
+
multimodal: {},
|
|
136
150
|
storage: {},
|
|
137
151
|
web: {},
|
|
138
152
|
schedules: {},
|
|
@@ -143,7 +157,8 @@ function createDefaultSecrets() {
|
|
|
143
157
|
return appSecretsSchema.parse({
|
|
144
158
|
feishu: {},
|
|
145
159
|
llm: {},
|
|
146
|
-
embedding: {}
|
|
160
|
+
embedding: {},
|
|
161
|
+
multimodal: {}
|
|
147
162
|
});
|
|
148
163
|
}
|
|
149
164
|
|
|
@@ -478,6 +493,24 @@ function migrateDatabase(database) {
|
|
|
478
493
|
created_at TEXT NOT NULL,
|
|
479
494
|
updated_at TEXT NOT NULL
|
|
480
495
|
);
|
|
496
|
+
|
|
497
|
+
CREATE TABLE IF NOT EXISTS image_multimodal_tasks (
|
|
498
|
+
id TEXT PRIMARY KEY,
|
|
499
|
+
source_message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
500
|
+
platform_message_id TEXT NOT NULL,
|
|
501
|
+
image_key TEXT NOT NULL,
|
|
502
|
+
stored_path TEXT NOT NULL,
|
|
503
|
+
mime_type TEXT NOT NULL,
|
|
504
|
+
status TEXT NOT NULL CHECK(status IN ('pending','running','succeeded','skipped','failed')),
|
|
505
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
506
|
+
last_error TEXT,
|
|
507
|
+
derived_message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
|
508
|
+
created_at TEXT NOT NULL,
|
|
509
|
+
updated_at TEXT NOT NULL,
|
|
510
|
+
UNIQUE(source_message_id, image_key)
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
|
|
481
514
|
`);
|
|
482
515
|
}
|
|
483
516
|
|
|
@@ -894,6 +927,40 @@ function getGatewayStatus(config, secrets) {
|
|
|
894
927
|
function normalizeBaseUrl(baseUrl) {
|
|
895
928
|
return baseUrl.replace(/\/+$/, "");
|
|
896
929
|
}
|
|
930
|
+
function toOpenAIMessage(message) {
|
|
931
|
+
return {
|
|
932
|
+
role: message.role,
|
|
933
|
+
content: message.content,
|
|
934
|
+
...message.toolCallId ? { tool_call_id: message.toolCallId } : {},
|
|
935
|
+
...message.toolCalls ? {
|
|
936
|
+
tool_calls: message.toolCalls.map((toolCall) => ({
|
|
937
|
+
id: toolCall.id,
|
|
938
|
+
type: "function",
|
|
939
|
+
function: {
|
|
940
|
+
name: toolCall.name,
|
|
941
|
+
arguments: JSON.stringify(toolCall.input)
|
|
942
|
+
}
|
|
943
|
+
}))
|
|
944
|
+
} : {}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function toOpenAITool(tool) {
|
|
948
|
+
return {
|
|
949
|
+
type: "function",
|
|
950
|
+
function: {
|
|
951
|
+
name: tool.name,
|
|
952
|
+
description: tool.description,
|
|
953
|
+
parameters: tool.inputSchema
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function parseToolCalls(message) {
|
|
958
|
+
return message?.tool_calls?.map((toolCall) => ({
|
|
959
|
+
id: toolCall.id,
|
|
960
|
+
name: toolCall.function.name,
|
|
961
|
+
input: JSON.parse(toolCall.function.arguments)
|
|
962
|
+
})) ?? [];
|
|
963
|
+
}
|
|
897
964
|
var OpenAICompatibleChatModel = class {
|
|
898
965
|
constructor(options) {
|
|
899
966
|
this.options = options;
|
|
@@ -911,7 +978,7 @@ var OpenAICompatibleChatModel = class {
|
|
|
911
978
|
},
|
|
912
979
|
body: JSON.stringify({
|
|
913
980
|
model: this.options.model,
|
|
914
|
-
messages,
|
|
981
|
+
messages: messages.map(toOpenAIMessage),
|
|
915
982
|
temperature: this.options.temperature ?? 0.2
|
|
916
983
|
})
|
|
917
984
|
});
|
|
@@ -926,6 +993,35 @@ var OpenAICompatibleChatModel = class {
|
|
|
926
993
|
}
|
|
927
994
|
return content;
|
|
928
995
|
}
|
|
996
|
+
async completeWithTools(messages, tools) {
|
|
997
|
+
if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
|
|
998
|
+
throw new Error("LLM \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
|
|
999
|
+
}
|
|
1000
|
+
const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/chat/completions`, {
|
|
1001
|
+
method: "POST",
|
|
1002
|
+
headers: {
|
|
1003
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
1004
|
+
"content-type": "application/json"
|
|
1005
|
+
},
|
|
1006
|
+
body: JSON.stringify({
|
|
1007
|
+
model: this.options.model,
|
|
1008
|
+
messages: messages.map(toOpenAIMessage),
|
|
1009
|
+
tools: tools.map(toOpenAITool),
|
|
1010
|
+
tool_choice: "auto",
|
|
1011
|
+
temperature: this.options.temperature ?? 0.2
|
|
1012
|
+
})
|
|
1013
|
+
});
|
|
1014
|
+
if (!response.ok) {
|
|
1015
|
+
const body = await response.text();
|
|
1016
|
+
throw new Error(`LLM \u8BF7\u6C42\u5931\u8D25\uFF1A${response.status} ${body}`);
|
|
1017
|
+
}
|
|
1018
|
+
const data2 = await response.json();
|
|
1019
|
+
const message = data2.choices?.[0]?.message;
|
|
1020
|
+
return {
|
|
1021
|
+
content: message?.content ?? "",
|
|
1022
|
+
toolCalls: parseToolCalls(message)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
929
1025
|
};
|
|
930
1026
|
var OpenAICompatibleEmbeddingModel = class {
|
|
931
1027
|
constructor(options) {
|
|
@@ -1081,6 +1177,7 @@ var MessageRepository = class {
|
|
|
1081
1177
|
)
|
|
1082
1178
|
ON CONFLICT(platform, platform_message_id)
|
|
1083
1179
|
DO UPDATE SET
|
|
1180
|
+
message_type = excluded.message_type,
|
|
1084
1181
|
text = excluded.text,
|
|
1085
1182
|
raw_payload_json = excluded.raw_payload_json,
|
|
1086
1183
|
received_at = excluded.received_at
|
|
@@ -1125,6 +1222,48 @@ var MessageRepository = class {
|
|
|
1125
1222
|
transaction();
|
|
1126
1223
|
return messageId;
|
|
1127
1224
|
}
|
|
1225
|
+
createImageSummaryMessage(input2) {
|
|
1226
|
+
const source = this.database.prepare(
|
|
1227
|
+
`
|
|
1228
|
+
SELECT
|
|
1229
|
+
m.platform AS platform,
|
|
1230
|
+
m.platform_message_id AS platformMessageId,
|
|
1231
|
+
m.chat_id AS chatId,
|
|
1232
|
+
m.sender_id AS senderId,
|
|
1233
|
+
m.sender_name AS senderName,
|
|
1234
|
+
m.sent_at AS sentAt,
|
|
1235
|
+
c.platform_chat_id AS platformChatId,
|
|
1236
|
+
c.name AS chatName
|
|
1237
|
+
FROM messages m
|
|
1238
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1239
|
+
WHERE m.id = ?
|
|
1240
|
+
`
|
|
1241
|
+
).get(input2.sourceMessageId);
|
|
1242
|
+
if (!source) {
|
|
1243
|
+
throw new Error("\u539F\u59CB\u56FE\u7247\u6D88\u606F\u4E0D\u5B58\u5728\u3002");
|
|
1244
|
+
}
|
|
1245
|
+
const derivedPlatformMessageId = `${source.platformMessageId}:image-summary:${input2.imageKey}`;
|
|
1246
|
+
return this.ingest({
|
|
1247
|
+
platform: source.platform,
|
|
1248
|
+
platformChatId: source.platformChatId,
|
|
1249
|
+
chatName: source.chatName,
|
|
1250
|
+
platformMessageId: derivedPlatformMessageId,
|
|
1251
|
+
senderId: source.senderId,
|
|
1252
|
+
senderName: source.senderName,
|
|
1253
|
+
messageType: "image_summary",
|
|
1254
|
+
text: `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}`,
|
|
1255
|
+
sentAt: source.sentAt,
|
|
1256
|
+
rawPayload: {
|
|
1257
|
+
derivedFromMessageId: input2.sourceMessageId,
|
|
1258
|
+
sourceAttachmentKind: "image",
|
|
1259
|
+
sourceResourceKey: input2.imageKey,
|
|
1260
|
+
multimodalModel: input2.multimodalModel,
|
|
1261
|
+
isMeaningful: true,
|
|
1262
|
+
...input2.reason?.trim() ? { reason: input2.reason.trim() } : {},
|
|
1263
|
+
generatedAt: input2.generatedAt
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1128
1267
|
listRecentMessages(limit = 20) {
|
|
1129
1268
|
return this.database.prepare(
|
|
1130
1269
|
`
|
|
@@ -1444,6 +1583,69 @@ var EpisodeRepository = class {
|
|
|
1444
1583
|
messageIds: window.messages.map((message) => message.id)
|
|
1445
1584
|
};
|
|
1446
1585
|
}
|
|
1586
|
+
async refreshWindowForMessage(input2) {
|
|
1587
|
+
const target = this.database.prepare(
|
|
1588
|
+
`
|
|
1589
|
+
SELECT chat_id AS chatId, sent_at AS sentAt
|
|
1590
|
+
FROM messages
|
|
1591
|
+
WHERE id = ?
|
|
1592
|
+
`
|
|
1593
|
+
).get(input2.messageId);
|
|
1594
|
+
if (!target) {
|
|
1595
|
+
return void 0;
|
|
1596
|
+
}
|
|
1597
|
+
const existingWindow = this.database.prepare(
|
|
1598
|
+
`
|
|
1599
|
+
SELECT e.started_at AS startedAt, e.ended_at AS endedAt
|
|
1600
|
+
FROM messages target
|
|
1601
|
+
JOIN messages source
|
|
1602
|
+
ON source.id = json_extract(target.raw_payload_json, '$.derivedFromMessageId')
|
|
1603
|
+
JOIN memory_episode_messages mem ON mem.message_id = source.id
|
|
1604
|
+
JOIN memory_episodes e ON e.id = mem.episode_id
|
|
1605
|
+
WHERE target.id = ?
|
|
1606
|
+
LIMIT 1
|
|
1607
|
+
`
|
|
1608
|
+
).get(input2.messageId);
|
|
1609
|
+
if (!existingWindow) {
|
|
1610
|
+
return void 0;
|
|
1611
|
+
}
|
|
1612
|
+
const messageTime = toMillis(target.sentAt);
|
|
1613
|
+
const windowStart = toMillis(existingWindow.startedAt);
|
|
1614
|
+
const windowEnd = Math.max(toMillis(existingWindow.endedAt), messageTime);
|
|
1615
|
+
const rows = this.database.prepare(
|
|
1616
|
+
`
|
|
1617
|
+
SELECT
|
|
1618
|
+
m.id,
|
|
1619
|
+
m.chat_id AS chatId,
|
|
1620
|
+
c.name AS chatName,
|
|
1621
|
+
m.sender_name AS senderName,
|
|
1622
|
+
m.text,
|
|
1623
|
+
m.sent_at AS sentAt
|
|
1624
|
+
FROM messages m
|
|
1625
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1626
|
+
WHERE m.chat_id = ?
|
|
1627
|
+
ORDER BY m.sent_at ASC
|
|
1628
|
+
`
|
|
1629
|
+
).all(target.chatId);
|
|
1630
|
+
const windowMessages = rows.filter((message) => {
|
|
1631
|
+
const time = toMillis(message.sentAt);
|
|
1632
|
+
return time >= windowStart && time <= windowEnd;
|
|
1633
|
+
});
|
|
1634
|
+
const first = windowMessages[0];
|
|
1635
|
+
const last = windowMessages.at(-1);
|
|
1636
|
+
if (!first || !last) {
|
|
1637
|
+
return void 0;
|
|
1638
|
+
}
|
|
1639
|
+
const window = {
|
|
1640
|
+
chatId: first.chatId,
|
|
1641
|
+
chatName: first.chatName,
|
|
1642
|
+
startedAt: first.sentAt,
|
|
1643
|
+
endedAt: last.sentAt,
|
|
1644
|
+
messages: windowMessages
|
|
1645
|
+
};
|
|
1646
|
+
const summary = await input2.summarize(window);
|
|
1647
|
+
return this.insertEpisode(window, summary);
|
|
1648
|
+
}
|
|
1447
1649
|
getEpisodeCount() {
|
|
1448
1650
|
const row = this.database.prepare("SELECT count(*) AS count FROM memory_episodes").get();
|
|
1449
1651
|
return row.count;
|
|
@@ -1612,6 +1814,73 @@ var MessageFtsRetriever = class {
|
|
|
1612
1814
|
}
|
|
1613
1815
|
};
|
|
1614
1816
|
|
|
1817
|
+
// src/rag/search-tools.ts
|
|
1818
|
+
var searchInputSchema = {
|
|
1819
|
+
type: "object",
|
|
1820
|
+
properties: {
|
|
1821
|
+
query: { type: "string", description: "Search query written by the model." },
|
|
1822
|
+
limit: { type: "number", description: "Maximum number of evidence blocks to return." }
|
|
1823
|
+
},
|
|
1824
|
+
required: ["query"],
|
|
1825
|
+
additionalProperties: false
|
|
1826
|
+
};
|
|
1827
|
+
function parseSearchInput(input2) {
|
|
1828
|
+
const rawQuery = typeof input2 === "object" && input2 !== null && "query" in input2 ? input2.query : void 0;
|
|
1829
|
+
if (typeof rawQuery !== "string") {
|
|
1830
|
+
throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
|
|
1831
|
+
}
|
|
1832
|
+
const query = rawQuery.trim();
|
|
1833
|
+
if (!query) {
|
|
1834
|
+
throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
|
|
1835
|
+
}
|
|
1836
|
+
const rawLimit = typeof input2 === "object" && input2 !== null && "limit" in input2 ? input2.limit : void 0;
|
|
1837
|
+
const numericLimit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? rawLimit : 5;
|
|
1838
|
+
const limit = Math.min(12, Math.max(1, Math.floor(numericLimit)));
|
|
1839
|
+
return { query, limit };
|
|
1840
|
+
}
|
|
1841
|
+
async function runRetriever(retriever, input2) {
|
|
1842
|
+
const { query, limit } = parseSearchInput(input2);
|
|
1843
|
+
const results = await retriever.retrieve(query);
|
|
1844
|
+
return results.slice(0, limit);
|
|
1845
|
+
}
|
|
1846
|
+
function createSearchTool(name, description, retriever) {
|
|
1847
|
+
return {
|
|
1848
|
+
name,
|
|
1849
|
+
description,
|
|
1850
|
+
inputSchema: searchInputSchema,
|
|
1851
|
+
execute: (input2) => runRetriever(retriever, input2)
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function createRagSearchTools(input2) {
|
|
1855
|
+
const tools = [
|
|
1856
|
+
createSearchTool(
|
|
1857
|
+
"hybrid_search",
|
|
1858
|
+
"Search across all indexed RAG evidence using the default hybrid retrieval strategy.",
|
|
1859
|
+
input2.hybrid
|
|
1860
|
+
),
|
|
1861
|
+
createSearchTool(
|
|
1862
|
+
"search_messages",
|
|
1863
|
+
"Search chat messages only when the answer likely depends on message-level evidence.",
|
|
1864
|
+
input2.messages
|
|
1865
|
+
),
|
|
1866
|
+
createSearchTool(
|
|
1867
|
+
"search_episodes",
|
|
1868
|
+
"Search episode summaries only when the answer likely depends on longer-running context.",
|
|
1869
|
+
input2.episodes
|
|
1870
|
+
)
|
|
1871
|
+
];
|
|
1872
|
+
if (input2.semantic) {
|
|
1873
|
+
tools.push(
|
|
1874
|
+
createSearchTool(
|
|
1875
|
+
"semantic_search",
|
|
1876
|
+
"Search semantic vector evidence only when broader conceptual recall is needed.",
|
|
1877
|
+
input2.semantic
|
|
1878
|
+
)
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
return tools;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1615
1884
|
// src/rag/embedding.ts
|
|
1616
1885
|
function cosineSimilarity(left, right) {
|
|
1617
1886
|
if (left.length === 0 || right.length === 0 || left.length !== right.length) {
|
|
@@ -1766,6 +2035,20 @@ async function createHybridRetriever(input2) {
|
|
|
1766
2035
|
}
|
|
1767
2036
|
};
|
|
1768
2037
|
}
|
|
2038
|
+
async function createAgenticRagSearchTools(input2) {
|
|
2039
|
+
const episodes = new EpisodeFtsRetriever(new EpisodeRepository(input2.database));
|
|
2040
|
+
const messages = new MessageFtsRetriever(input2.messages, { excludeMessageIds: input2.excludeMessageIds });
|
|
2041
|
+
const semantic = hasEmbeddingConfig(input2.config, input2.secrets) ? new VectorRetriever(
|
|
2042
|
+
createEmbeddingModel(input2.config, input2.secrets),
|
|
2043
|
+
new SqliteVectorStore(input2.database, { model: input2.config.embedding.model })
|
|
2044
|
+
) : void 0;
|
|
2045
|
+
const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
|
|
2046
|
+
return {
|
|
2047
|
+
tools: createRagSearchTools({ hybrid, messages, episodes, semantic }),
|
|
2048
|
+
close: () => {
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
1769
2052
|
|
|
1770
2053
|
// src/doctor/checks.ts
|
|
1771
2054
|
function pass(name, message) {
|
|
@@ -2334,6 +2617,268 @@ async function ensureFeishuBotOpenId(config, secrets, options = {}) {
|
|
|
2334
2617
|
// src/feishu/gateway.ts
|
|
2335
2618
|
import * as lark2 from "@larksuiteoapi/node-sdk";
|
|
2336
2619
|
|
|
2620
|
+
// src/multimodal/tasks.ts
|
|
2621
|
+
import crypto4 from "crypto";
|
|
2622
|
+
function nowIso4() {
|
|
2623
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2624
|
+
}
|
|
2625
|
+
function stableId3(sourceMessageId, imageKey) {
|
|
2626
|
+
return crypto4.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
|
|
2627
|
+
}
|
|
2628
|
+
function mapRow(row) {
|
|
2629
|
+
if (!row) {
|
|
2630
|
+
return void 0;
|
|
2631
|
+
}
|
|
2632
|
+
return {
|
|
2633
|
+
id: row.id,
|
|
2634
|
+
sourceMessageId: row.source_message_id,
|
|
2635
|
+
platformMessageId: row.platform_message_id,
|
|
2636
|
+
imageKey: row.image_key,
|
|
2637
|
+
storedPath: row.stored_path,
|
|
2638
|
+
mimeType: row.mime_type,
|
|
2639
|
+
status: row.status,
|
|
2640
|
+
attempts: row.attempts,
|
|
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
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
var ImageMultimodalTaskRepository = class {
|
|
2648
|
+
constructor(database) {
|
|
2649
|
+
this.database = database;
|
|
2650
|
+
}
|
|
2651
|
+
database;
|
|
2652
|
+
enqueue(input2) {
|
|
2653
|
+
const id = stableId3(input2.sourceMessageId, input2.imageKey);
|
|
2654
|
+
const timestamp = nowIso4();
|
|
2655
|
+
this.database.prepare(
|
|
2656
|
+
`
|
|
2657
|
+
INSERT INTO image_multimodal_tasks (
|
|
2658
|
+
id,
|
|
2659
|
+
source_message_id,
|
|
2660
|
+
platform_message_id,
|
|
2661
|
+
image_key,
|
|
2662
|
+
stored_path,
|
|
2663
|
+
mime_type,
|
|
2664
|
+
status,
|
|
2665
|
+
attempts,
|
|
2666
|
+
created_at,
|
|
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
|
+
}
|
|
2706
|
+
return record;
|
|
2707
|
+
}
|
|
2708
|
+
listPending(limit = 10) {
|
|
2709
|
+
const rows = this.database.prepare(
|
|
2710
|
+
`
|
|
2711
|
+
SELECT
|
|
2712
|
+
id,
|
|
2713
|
+
source_message_id,
|
|
2714
|
+
platform_message_id,
|
|
2715
|
+
image_key,
|
|
2716
|
+
stored_path,
|
|
2717
|
+
mime_type,
|
|
2718
|
+
status,
|
|
2719
|
+
attempts,
|
|
2720
|
+
last_error,
|
|
2721
|
+
derived_message_id,
|
|
2722
|
+
created_at,
|
|
2723
|
+
updated_at
|
|
2724
|
+
FROM image_multimodal_tasks
|
|
2725
|
+
WHERE status = 'pending'
|
|
2726
|
+
ORDER BY updated_at ASC
|
|
2727
|
+
LIMIT ?
|
|
2728
|
+
`
|
|
2729
|
+
).all(limit);
|
|
2730
|
+
return rows.map((row) => mapRow(row)).filter((row) => Boolean(row));
|
|
2731
|
+
}
|
|
2732
|
+
markRunning(id) {
|
|
2733
|
+
const result = this.database.prepare(
|
|
2734
|
+
`
|
|
2735
|
+
UPDATE image_multimodal_tasks
|
|
2736
|
+
SET status = 'running',
|
|
2737
|
+
attempts = attempts + 1,
|
|
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(
|
|
2750
|
+
`
|
|
2751
|
+
UPDATE image_multimodal_tasks
|
|
2752
|
+
SET status = 'succeeded',
|
|
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);
|
|
2760
|
+
}
|
|
2761
|
+
markSkipped(id, reason) {
|
|
2762
|
+
this.database.prepare(
|
|
2763
|
+
`
|
|
2764
|
+
UPDATE image_multimodal_tasks
|
|
2765
|
+
SET status = 'skipped',
|
|
2766
|
+
last_error = @reason,
|
|
2767
|
+
derived_message_id = NULL,
|
|
2768
|
+
updated_at = @updatedAt
|
|
2769
|
+
WHERE id = @id
|
|
2770
|
+
`
|
|
2771
|
+
).run({ id, reason, updatedAt: nowIso4() });
|
|
2772
|
+
return this.requireById(id);
|
|
2773
|
+
}
|
|
2774
|
+
markFailed(id, error, finalFailure) {
|
|
2775
|
+
this.database.prepare(
|
|
2776
|
+
`
|
|
2777
|
+
UPDATE image_multimodal_tasks
|
|
2778
|
+
SET status = @status,
|
|
2779
|
+
last_error = @error,
|
|
2780
|
+
derived_message_id = NULL,
|
|
2781
|
+
updated_at = @updatedAt
|
|
2782
|
+
WHERE id = @id
|
|
2783
|
+
`
|
|
2784
|
+
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso4() });
|
|
2785
|
+
return this.requireById(id);
|
|
2786
|
+
}
|
|
2787
|
+
getById(id) {
|
|
2788
|
+
const row = this.database.prepare(
|
|
2789
|
+
`
|
|
2790
|
+
SELECT
|
|
2791
|
+
id,
|
|
2792
|
+
source_message_id,
|
|
2793
|
+
platform_message_id,
|
|
2794
|
+
image_key,
|
|
2795
|
+
stored_path,
|
|
2796
|
+
mime_type,
|
|
2797
|
+
status,
|
|
2798
|
+
attempts,
|
|
2799
|
+
last_error,
|
|
2800
|
+
derived_message_id,
|
|
2801
|
+
created_at,
|
|
2802
|
+
updated_at
|
|
2803
|
+
FROM image_multimodal_tasks
|
|
2804
|
+
WHERE id = ?
|
|
2805
|
+
`
|
|
2806
|
+
).get(id);
|
|
2807
|
+
return mapRow(row);
|
|
2808
|
+
}
|
|
2809
|
+
requireById(id) {
|
|
2810
|
+
const record = this.getById(id);
|
|
2811
|
+
if (!record) {
|
|
2812
|
+
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u4E0D\u5B58\u5728\uFF1A${id}`);
|
|
2813
|
+
}
|
|
2814
|
+
return record;
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
|
|
2818
|
+
// src/multimodal/worker.ts
|
|
2819
|
+
var ImageMultimodalWorker = class {
|
|
2820
|
+
constructor(options) {
|
|
2821
|
+
this.options = options;
|
|
2822
|
+
}
|
|
2823
|
+
options;
|
|
2824
|
+
async processPending(limit = 10) {
|
|
2825
|
+
const result = { processed: 0, succeeded: 0, skipped: 0, failed: 0 };
|
|
2826
|
+
const pending = this.options.tasks.listPending(limit);
|
|
2827
|
+
for (const task of pending) {
|
|
2828
|
+
result.processed += 1;
|
|
2829
|
+
await this.processTask(task, result);
|
|
2830
|
+
}
|
|
2831
|
+
return result;
|
|
2832
|
+
}
|
|
2833
|
+
async processTask(task, result) {
|
|
2834
|
+
let running;
|
|
2835
|
+
try {
|
|
2836
|
+
running = this.options.tasks.markRunning(task.id);
|
|
2837
|
+
} catch (error) {
|
|
2838
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2839
|
+
if (message.startsWith("\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A")) {
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
throw error;
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
const described = await this.options.model.describeImage({
|
|
2846
|
+
imagePath: running.storedPath,
|
|
2847
|
+
mimeType: running.mimeType
|
|
2848
|
+
});
|
|
2849
|
+
if (!described.isMeaningful) {
|
|
2850
|
+
this.options.tasks.markSkipped(running.id, described.reason || "\u591A\u6A21\u6001\u6A21\u578B\u5224\u5B9A\u56FE\u7247\u65E0\u610F\u4E49\u3002");
|
|
2851
|
+
result.skipped += 1;
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
const derivedMessageId = this.options.messages.createImageSummaryMessage({
|
|
2855
|
+
sourceMessageId: running.sourceMessageId,
|
|
2856
|
+
imageKey: running.imageKey,
|
|
2857
|
+
summary: described.summary,
|
|
2858
|
+
reason: described.reason,
|
|
2859
|
+
multimodalModel: this.options.multimodalModelName,
|
|
2860
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2861
|
+
});
|
|
2862
|
+
if (this.options.vectorIndexMessage) {
|
|
2863
|
+
await this.options.vectorIndexMessage(derivedMessageId);
|
|
2864
|
+
}
|
|
2865
|
+
if (this.options.episodes && this.options.summarizeEpisode) {
|
|
2866
|
+
await this.options.episodes.refreshWindowForMessage({
|
|
2867
|
+
messageId: derivedMessageId,
|
|
2868
|
+
windowMs: this.options.config.episodes.windowMinutes * 60 * 1e3,
|
|
2869
|
+
summarize: this.options.summarizeEpisode
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
this.options.tasks.markSucceeded(running.id, derivedMessageId);
|
|
2873
|
+
result.succeeded += 1;
|
|
2874
|
+
} catch (error) {
|
|
2875
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2876
|
+
this.options.tasks.markFailed(running.id, message, running.attempts >= 3);
|
|
2877
|
+
result.failed += 1;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2337
2882
|
// src/rag/citations.ts
|
|
2338
2883
|
function isOpaqueId(value) {
|
|
2339
2884
|
return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
|
|
@@ -2454,12 +2999,99 @@ async function generateGroundedAnswer(input2) {
|
|
|
2454
2999
|
};
|
|
2455
3000
|
}
|
|
2456
3001
|
|
|
2457
|
-
// src/rag/qa-service.ts
|
|
2458
|
-
|
|
2459
|
-
|
|
3002
|
+
// src/rag/agentic-qa-service.ts
|
|
3003
|
+
var DEFAULT_MAX_MODEL_TURNS = 4;
|
|
3004
|
+
var DEFAULT_MAX_TOOL_CALLS = 8;
|
|
3005
|
+
var DEFAULT_MAX_EVIDENCE = 12;
|
|
3006
|
+
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";
|
|
3007
|
+
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";
|
|
3008
|
+
function toToolResultContent(results) {
|
|
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
|
+
);
|
|
3017
|
+
}
|
|
3018
|
+
function toToolErrorContent(message) {
|
|
3019
|
+
return JSON.stringify({ error: message });
|
|
3020
|
+
}
|
|
3021
|
+
function dedupeEvidence(evidence, maxEvidence) {
|
|
3022
|
+
const deduped = [];
|
|
3023
|
+
const seen = /* @__PURE__ */ new Set();
|
|
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;
|
|
3035
|
+
}
|
|
3036
|
+
async function askWithAgenticRag(input2) {
|
|
3037
|
+
if (!input2.model.completeWithTools) {
|
|
3038
|
+
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3039
|
+
}
|
|
3040
|
+
const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3041
|
+
const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
3042
|
+
const maxEvidence = input2.maxEvidence ?? DEFAULT_MAX_EVIDENCE;
|
|
3043
|
+
const messages = [
|
|
3044
|
+
{ role: "system", content: AGENTIC_SYSTEM_PROMPT },
|
|
3045
|
+
{ role: "user", content: input2.question }
|
|
3046
|
+
];
|
|
3047
|
+
const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
|
|
3048
|
+
let evidence = [];
|
|
3049
|
+
let toolCallsUsed = 0;
|
|
3050
|
+
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
3051
|
+
const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
|
|
3052
|
+
messages.push({
|
|
3053
|
+
role: "assistant",
|
|
3054
|
+
content: assistantResult.content,
|
|
3055
|
+
toolCalls: assistantResult.toolCalls
|
|
3056
|
+
});
|
|
3057
|
+
if (assistantResult.toolCalls.length === 0) {
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
3060
|
+
for (const toolCall of assistantResult.toolCalls) {
|
|
3061
|
+
if (toolCallsUsed >= maxToolCalls) {
|
|
3062
|
+
break;
|
|
3063
|
+
}
|
|
3064
|
+
toolCallsUsed += 1;
|
|
3065
|
+
const tool = toolsByName.get(toolCall.name);
|
|
3066
|
+
if (!tool) {
|
|
3067
|
+
messages.push({
|
|
3068
|
+
role: "tool",
|
|
3069
|
+
toolCallId: toolCall.id,
|
|
3070
|
+
content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
|
|
3071
|
+
});
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
try {
|
|
3075
|
+
const results = await tool.execute(toolCall.input);
|
|
3076
|
+
evidence = dedupeEvidence([...evidence, ...results], maxEvidence);
|
|
3077
|
+
messages.push({
|
|
3078
|
+
role: "tool",
|
|
3079
|
+
toolCallId: toolCall.id,
|
|
3080
|
+
content: toToolResultContent(results)
|
|
3081
|
+
});
|
|
3082
|
+
} catch (error) {
|
|
3083
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3084
|
+
messages.push({
|
|
3085
|
+
role: "tool",
|
|
3086
|
+
toolCallId: toolCall.id,
|
|
3087
|
+
content: toToolErrorContent(message)
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
2460
3092
|
if (evidence.length === 0) {
|
|
2461
3093
|
return {
|
|
2462
|
-
answer:
|
|
3094
|
+
answer: NO_EVIDENCE_ANSWER,
|
|
2463
3095
|
citations: []
|
|
2464
3096
|
};
|
|
2465
3097
|
}
|
|
@@ -2577,7 +3209,7 @@ var FeishuQuestionHandler = class {
|
|
|
2577
3209
|
}
|
|
2578
3210
|
const questionMessageId = payload.event?.message?.message_id;
|
|
2579
3211
|
await this.acknowledgeQuestion(decision.chatId, questionMessageId);
|
|
2580
|
-
const {
|
|
3212
|
+
const { tools, close } = await createAgenticRagSearchTools({
|
|
2581
3213
|
config: this.options.config,
|
|
2582
3214
|
secrets: this.options.secrets,
|
|
2583
3215
|
database: this.options.database,
|
|
@@ -2586,9 +3218,9 @@ var FeishuQuestionHandler = class {
|
|
|
2586
3218
|
});
|
|
2587
3219
|
try {
|
|
2588
3220
|
try {
|
|
2589
|
-
const result = await
|
|
3221
|
+
const result = await askWithAgenticRag({
|
|
2590
3222
|
question: decision.question,
|
|
2591
|
-
|
|
3223
|
+
tools,
|
|
2592
3224
|
model: this.options.model
|
|
2593
3225
|
});
|
|
2594
3226
|
const citations = formatCitations(result.citations);
|
|
@@ -2724,6 +3356,7 @@ function createFeishuEventDispatcher(options) {
|
|
|
2724
3356
|
payload,
|
|
2725
3357
|
downloader: options.resourceDownloader,
|
|
2726
3358
|
config: options.config,
|
|
3359
|
+
secrets: options.secrets,
|
|
2727
3360
|
vectorIndexMessage: options.attachmentVectorIndexer
|
|
2728
3361
|
}) : options.ingestor.ingestFeishuEvent(payload);
|
|
2729
3362
|
if (!result.accepted) {
|
|
@@ -2749,6 +3382,23 @@ function createFeishuEventDispatcher(options) {
|
|
|
2749
3382
|
}
|
|
2750
3383
|
if (result.attachment?.downloaded) {
|
|
2751
3384
|
console.log(`\u98DE\u4E66\u9644\u4EF6\u5DF2\u4E0B\u8F7D\uFF1A${result.attachment.downloaded.storedPath}`);
|
|
3385
|
+
if (options.imageMultimodalProcessor && result.attachment.imageTask) {
|
|
3386
|
+
void new ImageMultimodalWorker({
|
|
3387
|
+
config: options.config,
|
|
3388
|
+
messages: new MessageRepository(options.imageMultimodalProcessor.database),
|
|
3389
|
+
tasks: new ImageMultimodalTaskRepository(options.imageMultimodalProcessor.database),
|
|
3390
|
+
model: options.imageMultimodalProcessor.model,
|
|
3391
|
+
multimodalModelName: options.config.multimodal.model,
|
|
3392
|
+
vectorIndexMessage: options.attachmentVectorIndexer
|
|
3393
|
+
}).processPending().then((imageResult) => {
|
|
3394
|
+
console.log(
|
|
3395
|
+
`\u98DE\u4E66\u56FE\u7247\u591A\u6A21\u6001\u5904\u7406\u5B8C\u6210\uFF1Aprocessed=${imageResult.processed}, succeeded=${imageResult.succeeded}, skipped=${imageResult.skipped}, failed=${imageResult.failed}`
|
|
3396
|
+
);
|
|
3397
|
+
}).catch((error) => {
|
|
3398
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3399
|
+
console.error(`\u98DE\u4E66\u56FE\u7247\u591A\u6A21\u6001\u5904\u7406\u5931\u8D25\uFF1A${message}`);
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
2752
3402
|
if (result.attachment.indexedMessageId) {
|
|
2753
3403
|
console.log(`\u98DE\u4E66\u9644\u4EF6\u5DF2\u8FDB\u5165 RAG\uFF1A${result.attachment.indexedMessageId}`);
|
|
2754
3404
|
if (result.attachment.vectorIndexed) {
|
|
@@ -2799,7 +3449,8 @@ function createFeishuGateway(options) {
|
|
|
2799
3449
|
questionHandler: options.questionHandler,
|
|
2800
3450
|
resourceDownloader: options.resourceDownloader,
|
|
2801
3451
|
attachmentVectorIndexer: options.attachmentVectorIndexer,
|
|
2802
|
-
episodeProcessor: options.episodeProcessor
|
|
3452
|
+
episodeProcessor: options.episodeProcessor,
|
|
3453
|
+
imageMultimodalProcessor: options.imageMultimodalProcessor
|
|
2803
3454
|
});
|
|
2804
3455
|
return {
|
|
2805
3456
|
async start() {
|
|
@@ -2881,7 +3532,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
2881
3532
|
};
|
|
2882
3533
|
|
|
2883
3534
|
// src/files/ingest.ts
|
|
2884
|
-
import
|
|
3535
|
+
import crypto5 from "crypto";
|
|
2885
3536
|
import fs11 from "fs/promises";
|
|
2886
3537
|
import path13 from "path";
|
|
2887
3538
|
|
|
@@ -2945,7 +3596,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
2945
3596
|
}
|
|
2946
3597
|
}
|
|
2947
3598
|
function stableStoredName(sourcePath, fileName) {
|
|
2948
|
-
const digest =
|
|
3599
|
+
const digest = crypto5.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
2949
3600
|
return `${digest}-${fileName}`;
|
|
2950
3601
|
}
|
|
2951
3602
|
async function ingestLocalFile(input2) {
|
|
@@ -3183,12 +3834,17 @@ function extractAttachment(message) {
|
|
|
3183
3834
|
}
|
|
3184
3835
|
return candidate;
|
|
3185
3836
|
}
|
|
3837
|
+
function isMultimodalReady(config, secrets) {
|
|
3838
|
+
return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
|
|
3839
|
+
}
|
|
3186
3840
|
var GatewayIngestor = class {
|
|
3187
3841
|
messages;
|
|
3188
3842
|
jobs;
|
|
3843
|
+
imageTasks;
|
|
3189
3844
|
constructor(database) {
|
|
3190
3845
|
this.messages = new MessageRepository(database);
|
|
3191
3846
|
this.jobs = new FileJobRepository(database);
|
|
3847
|
+
this.imageTasks = new ImageMultimodalTaskRepository(database);
|
|
3192
3848
|
}
|
|
3193
3849
|
ingestFeishuEvent(payload) {
|
|
3194
3850
|
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
@@ -3220,6 +3876,23 @@ var GatewayIngestor = class {
|
|
|
3220
3876
|
messageId: result.message.platformMessageId,
|
|
3221
3877
|
attachment
|
|
3222
3878
|
});
|
|
3879
|
+
if (attachment.kind === "image") {
|
|
3880
|
+
const imageTask = isMultimodalReady(input2.config, input2.secrets) ? this.imageTasks.enqueue({
|
|
3881
|
+
sourceMessageId: result.messageId,
|
|
3882
|
+
platformMessageId: result.message.platformMessageId,
|
|
3883
|
+
imageKey: attachment.fileKey,
|
|
3884
|
+
storedPath: downloaded.storedPath,
|
|
3885
|
+
mimeType: attachment.mimeType || "image/jpeg"
|
|
3886
|
+
}) : void 0;
|
|
3887
|
+
return {
|
|
3888
|
+
...result,
|
|
3889
|
+
attachment: {
|
|
3890
|
+
downloaded,
|
|
3891
|
+
...imageTask ? { imageTask } : {},
|
|
3892
|
+
skippedReason: imageTask ? "\u56FE\u7247\u5DF2\u4E0B\u8F7D\uFF0C\u7B49\u5F85\u591A\u6A21\u6001\u540E\u53F0\u5904\u7406\u3002" : "\u56FE\u7247\u5DF2\u4E0B\u8F7D\uFF0C\u4F46\u591A\u6A21\u6001\u672A\u914D\u7F6E\u3002"
|
|
3893
|
+
}
|
|
3894
|
+
};
|
|
3895
|
+
}
|
|
3223
3896
|
if (!isSupportedTextFile(downloaded.storedPath)) {
|
|
3224
3897
|
return {
|
|
3225
3898
|
...result,
|
|
@@ -3355,6 +4028,96 @@ async function startDetachedGateway(input2) {
|
|
|
3355
4028
|
}
|
|
3356
4029
|
}
|
|
3357
4030
|
|
|
4031
|
+
// src/multimodal/openai-compatible.ts
|
|
4032
|
+
import fs13 from "fs/promises";
|
|
4033
|
+
function normalizeBaseUrl2(baseUrl) {
|
|
4034
|
+
return baseUrl.replace(/\/+$/, "");
|
|
4035
|
+
}
|
|
4036
|
+
function buildPrompt(context) {
|
|
4037
|
+
const contextText = context?.trim();
|
|
4038
|
+
return [
|
|
4039
|
+
"\u8BF7\u7406\u89E3\u8FD9\u5F20\u56FE\u7247\uFF0C\u5224\u65AD\u5B83\u662F\u5426\u5305\u542B\u503C\u5F97\u8FDB\u5165\u77E5\u8BC6\u5E93\u548C\u4F1A\u8BDD\u8BB0\u5FC6\u7684\u6709\u610F\u4E49\u4FE1\u606F\u3002",
|
|
4040
|
+
'\u8BF7\u53EA\u8F93\u51FA JSON\uFF0C\u683C\u5F0F\u4E3A {"summary": string, "isMeaningful": boolean, "reason": string}\u3002',
|
|
4041
|
+
"summary \u4F7F\u7528\u7B80\u6D01\u4E2D\u6587\u8F6C\u8FF0\u56FE\u7247\u4E2D\u7684\u5173\u952E\u4FE1\u606F\uFF1B\u65E0\u610F\u4E49\u56FE\u7247\u4E5F\u8981\u7ED9\u51FA\u7B80\u77ED summary\u3002",
|
|
4042
|
+
contextText ? `\u4E0A\u4E0B\u6587\uFF1A${contextText}` : void 0
|
|
4043
|
+
].filter(Boolean).join("\n");
|
|
4044
|
+
}
|
|
4045
|
+
function parseDescribeImageResult(content) {
|
|
4046
|
+
let data2;
|
|
4047
|
+
try {
|
|
4048
|
+
data2 = JSON.parse(content);
|
|
4049
|
+
} catch {
|
|
4050
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u7684 JSON \u65E0\u6CD5\u89E3\u6790\u3002");
|
|
4051
|
+
}
|
|
4052
|
+
if (!data2 || typeof data2 !== "object") {
|
|
4053
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u683C\u5F0F\u4E0D\u6B63\u786E\u3002");
|
|
4054
|
+
}
|
|
4055
|
+
const result = data2;
|
|
4056
|
+
const summary = typeof result.summary === "string" ? result.summary.trim() : "";
|
|
4057
|
+
if (!summary) {
|
|
4058
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u7684 summary \u4E3A\u7A7A\u3002");
|
|
4059
|
+
}
|
|
4060
|
+
if (typeof result.isMeaningful !== "boolean") {
|
|
4061
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u7684 isMeaningful \u4E0D\u662F\u5E03\u5C14\u503C\u3002");
|
|
4062
|
+
}
|
|
4063
|
+
const reason = typeof result.reason === "string" ? result.reason.trim() : "";
|
|
4064
|
+
return {
|
|
4065
|
+
summary,
|
|
4066
|
+
isMeaningful: result.isMeaningful,
|
|
4067
|
+
...reason ? { reason } : {}
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
var OpenAICompatibleMultimodalModel = class {
|
|
4071
|
+
constructor(options) {
|
|
4072
|
+
this.options = options;
|
|
4073
|
+
}
|
|
4074
|
+
options;
|
|
4075
|
+
async describeImage(input2) {
|
|
4076
|
+
if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
|
|
4077
|
+
throw new Error("\u591A\u6A21\u6001\u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
|
|
4078
|
+
}
|
|
4079
|
+
const image = await fs13.readFile(input2.imagePath);
|
|
4080
|
+
const response = await fetch(`${normalizeBaseUrl2(this.options.baseUrl)}/chat/completions`, {
|
|
4081
|
+
method: "POST",
|
|
4082
|
+
headers: {
|
|
4083
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
4084
|
+
"content-type": "application/json"
|
|
4085
|
+
},
|
|
4086
|
+
body: JSON.stringify({
|
|
4087
|
+
model: this.options.model,
|
|
4088
|
+
messages: [
|
|
4089
|
+
{
|
|
4090
|
+
role: "user",
|
|
4091
|
+
content: [
|
|
4092
|
+
{ type: "text", text: buildPrompt(input2.context) },
|
|
4093
|
+
{ type: "image_url", image_url: { url: `data:${input2.mimeType};base64,${image.toString("base64")}` } }
|
|
4094
|
+
]
|
|
4095
|
+
}
|
|
4096
|
+
],
|
|
4097
|
+
response_format: { type: "json_object" },
|
|
4098
|
+
temperature: this.options.temperature ?? 0.2
|
|
4099
|
+
})
|
|
4100
|
+
});
|
|
4101
|
+
if (!response.ok) {
|
|
4102
|
+
const body = await response.text();
|
|
4103
|
+
throw new Error(`\u591A\u6A21\u6001\u8BF7\u6C42\u5931\u8D25\uFF1A${response.status} ${body}`);
|
|
4104
|
+
}
|
|
4105
|
+
const data2 = await response.json();
|
|
4106
|
+
const content = data2.choices?.[0]?.message?.content?.trim();
|
|
4107
|
+
if (!content) {
|
|
4108
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u4E3A\u7A7A\u3002");
|
|
4109
|
+
}
|
|
4110
|
+
return parseDescribeImageResult(content);
|
|
4111
|
+
}
|
|
4112
|
+
};
|
|
4113
|
+
function createMultimodalModel(config, secrets) {
|
|
4114
|
+
return new OpenAICompatibleMultimodalModel({
|
|
4115
|
+
baseUrl: config.multimodal.baseUrl,
|
|
4116
|
+
apiKey: secrets.multimodal.apiKey,
|
|
4117
|
+
model: config.multimodal.model
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
|
|
3358
4121
|
// src/rag/indexer.ts
|
|
3359
4122
|
async function indexMessageChunks(input2) {
|
|
3360
4123
|
const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
|
|
@@ -3433,6 +4196,22 @@ async function processMessagesNow(input2) {
|
|
|
3433
4196
|
};
|
|
3434
4197
|
}
|
|
3435
4198
|
|
|
4199
|
+
// src/rag/qa-service.ts
|
|
4200
|
+
async function askWithRag(input2) {
|
|
4201
|
+
const evidence = await input2.retriever.retrieve(input2.question);
|
|
4202
|
+
if (evidence.length === 0) {
|
|
4203
|
+
return {
|
|
4204
|
+
answer: "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002",
|
|
4205
|
+
citations: []
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
return generateGroundedAnswer({
|
|
4209
|
+
question: input2.question,
|
|
4210
|
+
evidence,
|
|
4211
|
+
model: input2.model
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
|
|
3436
4215
|
// src/update/npm-updater.ts
|
|
3437
4216
|
import { execFile } from "child_process";
|
|
3438
4217
|
import { promisify } from "util";
|
|
@@ -4065,12 +4844,31 @@ async function promptForConfiguration(config, secrets) {
|
|
|
4065
4844
|
llmApiKey: secrets.llm.apiKey
|
|
4066
4845
|
});
|
|
4067
4846
|
config.embedding.model = await input({ message: "Embedding Model", default: config.embedding.model });
|
|
4847
|
+
const multimodalBaseUrl = await input({
|
|
4848
|
+
message: "Multimodal Base URL\uFF08OpenAI-compatible\uFF0C\u53EF\u7559\u7A7A\uFF09",
|
|
4849
|
+
default: config.multimodal.baseUrl
|
|
4850
|
+
});
|
|
4851
|
+
const multimodalApiKey = await password({
|
|
4852
|
+
message: "Multimodal API Key\uFF08\u53EF\u7559\u7A7A\uFF09",
|
|
4853
|
+
mask: "*"
|
|
4854
|
+
});
|
|
4855
|
+
const multimodalModel = await input({
|
|
4856
|
+
message: "Multimodal Model\uFF08\u53EF\u7559\u7A7A\uFF09",
|
|
4857
|
+
default: config.multimodal.model
|
|
4858
|
+
});
|
|
4068
4859
|
const dimension = await number({
|
|
4069
4860
|
message: "Embedding \u7EF4\u5EA6\uFF08\u4E0D\u77E5\u9053\u53EF\u5148\u7559\u7A7A\uFF09",
|
|
4070
4861
|
default: config.embedding.dimension ?? void 0,
|
|
4071
4862
|
required: false
|
|
4072
4863
|
});
|
|
4073
4864
|
config.embedding.dimension = dimension ?? null;
|
|
4865
|
+
config.multimodal = {
|
|
4866
|
+
baseUrl: multimodalBaseUrl,
|
|
4867
|
+
model: multimodalModel
|
|
4868
|
+
};
|
|
4869
|
+
secrets.multimodal = {
|
|
4870
|
+
apiKey: multimodalApiKey || secrets.multimodal.apiKey
|
|
4871
|
+
};
|
|
4074
4872
|
config.web.port = await number({ message: "Web UI \u7AEF\u53E3", default: config.web.port, required: true }) ?? config.web.port;
|
|
4075
4873
|
config.feishu.requireMention = await confirm({
|
|
4076
4874
|
message: "\u7FA4\u804A\u56DE\u7B54\u662F\u5426\u8981\u6C42 @ \u673A\u5668\u4EBA\uFF1F",
|
|
@@ -4098,7 +4896,8 @@ function printSettings(config, secrets) {
|
|
|
4098
4896
|
secrets: {
|
|
4099
4897
|
feishu: { appSecret: maskSecret(secrets.feishu.appSecret) },
|
|
4100
4898
|
llm: { apiKey: maskSecret(secrets.llm.apiKey) },
|
|
4101
|
-
embedding: { apiKey: maskSecret(secrets.embedding.apiKey) }
|
|
4899
|
+
embedding: { apiKey: maskSecret(secrets.embedding.apiKey) },
|
|
4900
|
+
multimodal: { apiKey: maskSecret(secrets.multimodal.apiKey) }
|
|
4102
4901
|
}
|
|
4103
4902
|
},
|
|
4104
4903
|
null,
|
|
@@ -4210,6 +5009,10 @@ async function startGatewayForegroundCommand() {
|
|
|
4210
5009
|
database,
|
|
4211
5010
|
model: createChatModel(config, secrets)
|
|
4212
5011
|
},
|
|
5012
|
+
imageMultimodalProcessor: config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey ? {
|
|
5013
|
+
database,
|
|
5014
|
+
model: createMultimodalModel(config, secrets)
|
|
5015
|
+
} : void 0,
|
|
4213
5016
|
questionHandler: new FeishuQuestionHandler({
|
|
4214
5017
|
config,
|
|
4215
5018
|
secrets,
|
|
@@ -4577,7 +5380,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
|
|
|
4577
5380
|
const config = await loadConfig();
|
|
4578
5381
|
const database = openDatabase(config);
|
|
4579
5382
|
try {
|
|
4580
|
-
const raw = await
|
|
5383
|
+
const raw = await fs14.readFile(options.file, "utf8");
|
|
4581
5384
|
const payload = JSON.parse(raw);
|
|
4582
5385
|
const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
|
|
4583
5386
|
if (!result.accepted) {
|