chattercatcher 0.1.17 → 0.1.19
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 +947 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +145 -74
- package/dist/index.js +833 -85
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/docs/superpowers/plans/2026-05-02-agentic-rag.md +0 -1013
- package/docs/superpowers/specs/2026-05-02-agentic-rag-design.md +0 -143
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.19",
|
|
12
12
|
description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
|
|
13
13
|
type: "module",
|
|
14
14
|
main: "dist/index.js",
|
|
@@ -27,7 +27,9 @@ var package_default = {
|
|
|
27
27
|
files: [
|
|
28
28
|
"assets",
|
|
29
29
|
"dist",
|
|
30
|
-
"docs",
|
|
30
|
+
"docs/DEVELOPMENT_PLAN.md",
|
|
31
|
+
"docs/PRD.md",
|
|
32
|
+
"docs/TECHNICAL_ARCHITECTURE.md",
|
|
31
33
|
"README.md",
|
|
32
34
|
"AGENTS.md"
|
|
33
35
|
],
|
|
@@ -102,6 +104,13 @@ var appConfigSchema = z.object({
|
|
|
102
104
|
model: z.string().default(""),
|
|
103
105
|
dimension: z.number().int().positive().nullable().default(null)
|
|
104
106
|
}),
|
|
107
|
+
multimodal: z.preprocess(
|
|
108
|
+
(value) => value ?? {},
|
|
109
|
+
z.object({
|
|
110
|
+
baseUrl: z.string().url().or(z.literal("")).default(""),
|
|
111
|
+
model: z.string().default("")
|
|
112
|
+
})
|
|
113
|
+
),
|
|
105
114
|
storage: z.object({
|
|
106
115
|
dataDir: z.string().default(defaultDataDir)
|
|
107
116
|
}),
|
|
@@ -126,13 +135,20 @@ var appSecretsSchema = z.object({
|
|
|
126
135
|
}),
|
|
127
136
|
embedding: z.object({
|
|
128
137
|
apiKey: z.string().default("")
|
|
129
|
-
})
|
|
138
|
+
}),
|
|
139
|
+
multimodal: z.preprocess(
|
|
140
|
+
(value) => value ?? {},
|
|
141
|
+
z.object({
|
|
142
|
+
apiKey: z.string().default("")
|
|
143
|
+
})
|
|
144
|
+
)
|
|
130
145
|
});
|
|
131
146
|
function createDefaultConfig() {
|
|
132
147
|
return appConfigSchema.parse({
|
|
133
148
|
feishu: {},
|
|
134
149
|
llm: {},
|
|
135
150
|
embedding: {},
|
|
151
|
+
multimodal: {},
|
|
136
152
|
storage: {},
|
|
137
153
|
web: {},
|
|
138
154
|
schedules: {},
|
|
@@ -143,7 +159,8 @@ function createDefaultSecrets() {
|
|
|
143
159
|
return appSecretsSchema.parse({
|
|
144
160
|
feishu: {},
|
|
145
161
|
llm: {},
|
|
146
|
-
embedding: {}
|
|
162
|
+
embedding: {},
|
|
163
|
+
multimodal: {}
|
|
147
164
|
});
|
|
148
165
|
}
|
|
149
166
|
|
|
@@ -463,6 +480,22 @@ function migrateDatabase(database) {
|
|
|
463
480
|
CREATE INDEX IF NOT EXISTS message_chunk_embeddings_model_idx
|
|
464
481
|
ON message_chunk_embeddings(model, dimension);
|
|
465
482
|
|
|
483
|
+
CREATE TABLE IF NOT EXISTS qa_logs (
|
|
484
|
+
id TEXT PRIMARY KEY,
|
|
485
|
+
chat_id TEXT,
|
|
486
|
+
question_message_id TEXT,
|
|
487
|
+
question TEXT NOT NULL,
|
|
488
|
+
answer TEXT NOT NULL,
|
|
489
|
+
citations_json TEXT NOT NULL,
|
|
490
|
+
retrieval_debug_json TEXT NOT NULL,
|
|
491
|
+
status TEXT NOT NULL CHECK(status IN ('answered','failed')),
|
|
492
|
+
error TEXT,
|
|
493
|
+
created_at TEXT NOT NULL
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
CREATE INDEX IF NOT EXISTS qa_logs_created_at_idx ON qa_logs(created_at);
|
|
497
|
+
CREATE INDEX IF NOT EXISTS qa_logs_chat_idx ON qa_logs(chat_id, created_at);
|
|
498
|
+
|
|
466
499
|
CREATE TABLE IF NOT EXISTS file_jobs (
|
|
467
500
|
id TEXT PRIMARY KEY,
|
|
468
501
|
source_path TEXT NOT NULL,
|
|
@@ -478,6 +511,24 @@ function migrateDatabase(database) {
|
|
|
478
511
|
created_at TEXT NOT NULL,
|
|
479
512
|
updated_at TEXT NOT NULL
|
|
480
513
|
);
|
|
514
|
+
|
|
515
|
+
CREATE TABLE IF NOT EXISTS image_multimodal_tasks (
|
|
516
|
+
id TEXT PRIMARY KEY,
|
|
517
|
+
source_message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
518
|
+
platform_message_id TEXT NOT NULL,
|
|
519
|
+
image_key TEXT NOT NULL,
|
|
520
|
+
stored_path TEXT NOT NULL,
|
|
521
|
+
mime_type TEXT NOT NULL,
|
|
522
|
+
status TEXT NOT NULL CHECK(status IN ('pending','running','succeeded','skipped','failed')),
|
|
523
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
524
|
+
last_error TEXT,
|
|
525
|
+
derived_message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
|
526
|
+
created_at TEXT NOT NULL,
|
|
527
|
+
updated_at TEXT NOT NULL,
|
|
528
|
+
UNIQUE(source_message_id, image_key)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
|
|
481
532
|
`);
|
|
482
533
|
}
|
|
483
534
|
|
|
@@ -1144,6 +1195,7 @@ var MessageRepository = class {
|
|
|
1144
1195
|
)
|
|
1145
1196
|
ON CONFLICT(platform, platform_message_id)
|
|
1146
1197
|
DO UPDATE SET
|
|
1198
|
+
message_type = excluded.message_type,
|
|
1147
1199
|
text = excluded.text,
|
|
1148
1200
|
raw_payload_json = excluded.raw_payload_json,
|
|
1149
1201
|
received_at = excluded.received_at
|
|
@@ -1188,6 +1240,48 @@ var MessageRepository = class {
|
|
|
1188
1240
|
transaction();
|
|
1189
1241
|
return messageId;
|
|
1190
1242
|
}
|
|
1243
|
+
createImageSummaryMessage(input2) {
|
|
1244
|
+
const source = this.database.prepare(
|
|
1245
|
+
`
|
|
1246
|
+
SELECT
|
|
1247
|
+
m.platform AS platform,
|
|
1248
|
+
m.platform_message_id AS platformMessageId,
|
|
1249
|
+
m.chat_id AS chatId,
|
|
1250
|
+
m.sender_id AS senderId,
|
|
1251
|
+
m.sender_name AS senderName,
|
|
1252
|
+
m.sent_at AS sentAt,
|
|
1253
|
+
c.platform_chat_id AS platformChatId,
|
|
1254
|
+
c.name AS chatName
|
|
1255
|
+
FROM messages m
|
|
1256
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1257
|
+
WHERE m.id = ?
|
|
1258
|
+
`
|
|
1259
|
+
).get(input2.sourceMessageId);
|
|
1260
|
+
if (!source) {
|
|
1261
|
+
throw new Error("\u539F\u59CB\u56FE\u7247\u6D88\u606F\u4E0D\u5B58\u5728\u3002");
|
|
1262
|
+
}
|
|
1263
|
+
const derivedPlatformMessageId = `${source.platformMessageId}:image-summary:${input2.imageKey}`;
|
|
1264
|
+
return this.ingest({
|
|
1265
|
+
platform: source.platform,
|
|
1266
|
+
platformChatId: source.platformChatId,
|
|
1267
|
+
chatName: source.chatName,
|
|
1268
|
+
platformMessageId: derivedPlatformMessageId,
|
|
1269
|
+
senderId: source.senderId,
|
|
1270
|
+
senderName: source.senderName,
|
|
1271
|
+
messageType: "image_summary",
|
|
1272
|
+
text: `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}`,
|
|
1273
|
+
sentAt: source.sentAt,
|
|
1274
|
+
rawPayload: {
|
|
1275
|
+
derivedFromMessageId: input2.sourceMessageId,
|
|
1276
|
+
sourceAttachmentKind: "image",
|
|
1277
|
+
sourceResourceKey: input2.imageKey,
|
|
1278
|
+
multimodalModel: input2.multimodalModel,
|
|
1279
|
+
isMeaningful: true,
|
|
1280
|
+
...input2.reason?.trim() ? { reason: input2.reason.trim() } : {},
|
|
1281
|
+
generatedAt: input2.generatedAt
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1191
1285
|
listRecentMessages(limit = 20) {
|
|
1192
1286
|
return this.database.prepare(
|
|
1193
1287
|
`
|
|
@@ -1507,6 +1601,69 @@ var EpisodeRepository = class {
|
|
|
1507
1601
|
messageIds: window.messages.map((message) => message.id)
|
|
1508
1602
|
};
|
|
1509
1603
|
}
|
|
1604
|
+
async refreshWindowForMessage(input2) {
|
|
1605
|
+
const target = this.database.prepare(
|
|
1606
|
+
`
|
|
1607
|
+
SELECT chat_id AS chatId, sent_at AS sentAt
|
|
1608
|
+
FROM messages
|
|
1609
|
+
WHERE id = ?
|
|
1610
|
+
`
|
|
1611
|
+
).get(input2.messageId);
|
|
1612
|
+
if (!target) {
|
|
1613
|
+
return void 0;
|
|
1614
|
+
}
|
|
1615
|
+
const existingWindow = this.database.prepare(
|
|
1616
|
+
`
|
|
1617
|
+
SELECT e.started_at AS startedAt, e.ended_at AS endedAt
|
|
1618
|
+
FROM messages target
|
|
1619
|
+
JOIN messages source
|
|
1620
|
+
ON source.id = json_extract(target.raw_payload_json, '$.derivedFromMessageId')
|
|
1621
|
+
JOIN memory_episode_messages mem ON mem.message_id = source.id
|
|
1622
|
+
JOIN memory_episodes e ON e.id = mem.episode_id
|
|
1623
|
+
WHERE target.id = ?
|
|
1624
|
+
LIMIT 1
|
|
1625
|
+
`
|
|
1626
|
+
).get(input2.messageId);
|
|
1627
|
+
if (!existingWindow) {
|
|
1628
|
+
return void 0;
|
|
1629
|
+
}
|
|
1630
|
+
const messageTime = toMillis(target.sentAt);
|
|
1631
|
+
const windowStart = toMillis(existingWindow.startedAt);
|
|
1632
|
+
const windowEnd = Math.max(toMillis(existingWindow.endedAt), messageTime);
|
|
1633
|
+
const rows = this.database.prepare(
|
|
1634
|
+
`
|
|
1635
|
+
SELECT
|
|
1636
|
+
m.id,
|
|
1637
|
+
m.chat_id AS chatId,
|
|
1638
|
+
c.name AS chatName,
|
|
1639
|
+
m.sender_name AS senderName,
|
|
1640
|
+
m.text,
|
|
1641
|
+
m.sent_at AS sentAt
|
|
1642
|
+
FROM messages m
|
|
1643
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1644
|
+
WHERE m.chat_id = ?
|
|
1645
|
+
ORDER BY m.sent_at ASC
|
|
1646
|
+
`
|
|
1647
|
+
).all(target.chatId);
|
|
1648
|
+
const windowMessages = rows.filter((message) => {
|
|
1649
|
+
const time = toMillis(message.sentAt);
|
|
1650
|
+
return time >= windowStart && time <= windowEnd;
|
|
1651
|
+
});
|
|
1652
|
+
const first = windowMessages[0];
|
|
1653
|
+
const last = windowMessages.at(-1);
|
|
1654
|
+
if (!first || !last) {
|
|
1655
|
+
return void 0;
|
|
1656
|
+
}
|
|
1657
|
+
const window = {
|
|
1658
|
+
chatId: first.chatId,
|
|
1659
|
+
chatName: first.chatName,
|
|
1660
|
+
startedAt: first.sentAt,
|
|
1661
|
+
endedAt: last.sentAt,
|
|
1662
|
+
messages: windowMessages
|
|
1663
|
+
};
|
|
1664
|
+
const summary = await input2.summarize(window);
|
|
1665
|
+
return this.insertEpisode(window, summary);
|
|
1666
|
+
}
|
|
1510
1667
|
getEpisodeCount() {
|
|
1511
1668
|
const row = this.database.prepare("SELECT count(*) AS count FROM memory_episodes").get();
|
|
1512
1669
|
return row.count;
|
|
@@ -2478,6 +2635,453 @@ async function ensureFeishuBotOpenId(config, secrets, options = {}) {
|
|
|
2478
2635
|
// src/feishu/gateway.ts
|
|
2479
2636
|
import * as lark2 from "@larksuiteoapi/node-sdk";
|
|
2480
2637
|
|
|
2638
|
+
// src/gateway/indexing-scheduler.ts
|
|
2639
|
+
function createIndexingScheduler(options) {
|
|
2640
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2641
|
+
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
2642
|
+
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
2643
|
+
const logger = options.logger ?? console;
|
|
2644
|
+
const parsed = parseCronSchedule(options.schedule);
|
|
2645
|
+
let timer;
|
|
2646
|
+
let running = false;
|
|
2647
|
+
const runDueNow = async () => {
|
|
2648
|
+
if (!parsed || running || !matchesParsedSchedule(parsed, now())) {
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
running = true;
|
|
2652
|
+
try {
|
|
2653
|
+
await options.work();
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2656
|
+
logger.error(`\u5B9A\u65F6\u6D88\u606F\u7D22\u5F15\u5931\u8D25\uFF1A${message}`);
|
|
2657
|
+
} finally {
|
|
2658
|
+
running = false;
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
return {
|
|
2662
|
+
start() {
|
|
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
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
function matchesParsedSchedule(schedule, date) {
|
|
2681
|
+
return schedule.minute(date.getMinutes()) && schedule.hour(date.getHours()) && schedule.dayOfMonth(date.getDate()) && schedule.month(date.getMonth() + 1) && schedule.dayOfWeek(date.getDay());
|
|
2682
|
+
}
|
|
2683
|
+
function parseCronSchedule(schedule) {
|
|
2684
|
+
const fields = schedule.trim().split(/\s+/);
|
|
2685
|
+
if (fields.length !== 5) {
|
|
2686
|
+
return null;
|
|
2687
|
+
}
|
|
2688
|
+
const minute = parseMinuteField(fields[0]);
|
|
2689
|
+
const hour = parseExactOrWildcardField(fields[1], 0, 23);
|
|
2690
|
+
const dayOfMonth = parseExactOrWildcardField(fields[2], 1, 31);
|
|
2691
|
+
const month = parseExactOrWildcardField(fields[3], 1, 12);
|
|
2692
|
+
const dayOfWeek = parseExactOrWildcardField(fields[4], 0, 6);
|
|
2693
|
+
if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
|
|
2694
|
+
return null;
|
|
2695
|
+
}
|
|
2696
|
+
return { minute, hour, dayOfMonth, month, dayOfWeek };
|
|
2697
|
+
}
|
|
2698
|
+
function parseMinuteField(field) {
|
|
2699
|
+
if (field === "*") {
|
|
2700
|
+
return () => true;
|
|
2701
|
+
}
|
|
2702
|
+
const stepMatch = /^\*\/(\d+)$/.exec(field);
|
|
2703
|
+
if (stepMatch) {
|
|
2704
|
+
const step = Number(stepMatch[1]);
|
|
2705
|
+
if (!Number.isInteger(step) || step <= 0 || step > 59) {
|
|
2706
|
+
return null;
|
|
2707
|
+
}
|
|
2708
|
+
return (value) => value % step === 0;
|
|
2709
|
+
}
|
|
2710
|
+
if (field.includes(",")) {
|
|
2711
|
+
const values = field.split(",").map((part) => parseExactNumber(part, 0, 59));
|
|
2712
|
+
if (values.some((value) => value === null)) {
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
const allowed = new Set(values);
|
|
2716
|
+
return (value) => allowed.has(value);
|
|
2717
|
+
}
|
|
2718
|
+
const exact = parseExactNumber(field, 0, 59);
|
|
2719
|
+
if (exact === null) {
|
|
2720
|
+
return null;
|
|
2721
|
+
}
|
|
2722
|
+
return (value) => value === exact;
|
|
2723
|
+
}
|
|
2724
|
+
function parseExactOrWildcardField(field, min, max) {
|
|
2725
|
+
if (field === "*") {
|
|
2726
|
+
return () => true;
|
|
2727
|
+
}
|
|
2728
|
+
const exact = parseExactNumber(field, min, max);
|
|
2729
|
+
if (exact === null) {
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
return (value) => value === exact;
|
|
2733
|
+
}
|
|
2734
|
+
function parseExactNumber(field, min, max) {
|
|
2735
|
+
if (!/^\d+$/.test(field)) {
|
|
2736
|
+
return null;
|
|
2737
|
+
}
|
|
2738
|
+
const value = Number(field);
|
|
2739
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
return value;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// src/rag/indexer.ts
|
|
2746
|
+
async function indexMessageChunks(input2) {
|
|
2747
|
+
const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
|
|
2748
|
+
if (chunks.length === 0) {
|
|
2749
|
+
return { chunks: 0, vectors: 0 };
|
|
2750
|
+
}
|
|
2751
|
+
const vectors = await input2.embedding.embedBatch(chunks.map((chunk) => chunk.text));
|
|
2752
|
+
const records = [];
|
|
2753
|
+
for (const [index2, chunk] of chunks.entries()) {
|
|
2754
|
+
const vector = vectors[index2];
|
|
2755
|
+
if (!vector || vector.length === 0) {
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
records.push({
|
|
2759
|
+
id: chunk.chunkId,
|
|
2760
|
+
vector,
|
|
2761
|
+
evidence: {
|
|
2762
|
+
id: chunk.chunkId,
|
|
2763
|
+
text: chunk.text,
|
|
2764
|
+
score: 1,
|
|
2765
|
+
source: toEvidenceSource3(chunk)
|
|
2766
|
+
}
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
await input2.store.upsert(records);
|
|
2770
|
+
return {
|
|
2771
|
+
chunks: chunks.length,
|
|
2772
|
+
vectors: records.length
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
function toEvidenceSource3(chunk) {
|
|
2776
|
+
if (chunk.messageType === "file") {
|
|
2777
|
+
return {
|
|
2778
|
+
type: "file",
|
|
2779
|
+
label: chunk.senderName,
|
|
2780
|
+
timestamp: chunk.sentAt
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
return {
|
|
2784
|
+
type: "message",
|
|
2785
|
+
label: chunk.chatName,
|
|
2786
|
+
sender: chunk.senderName,
|
|
2787
|
+
timestamp: chunk.sentAt
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// src/rag/manual-index.ts
|
|
2792
|
+
async function processMessagesNow(input2) {
|
|
2793
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2794
|
+
if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
|
|
2795
|
+
return {
|
|
2796
|
+
status: "skipped",
|
|
2797
|
+
reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
|
|
2798
|
+
chunks: 0,
|
|
2799
|
+
vectors: 0,
|
|
2800
|
+
startedAt,
|
|
2801
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
const vectorStore = new SqliteVectorStore(input2.database, {
|
|
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
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/multimodal/tasks.ts
|
|
2824
|
+
import crypto4 from "crypto";
|
|
2825
|
+
function nowIso4() {
|
|
2826
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2827
|
+
}
|
|
2828
|
+
function stableId3(sourceMessageId, imageKey) {
|
|
2829
|
+
return crypto4.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
|
|
2830
|
+
}
|
|
2831
|
+
function mapRow(row) {
|
|
2832
|
+
if (!row) {
|
|
2833
|
+
return void 0;
|
|
2834
|
+
}
|
|
2835
|
+
return {
|
|
2836
|
+
id: row.id,
|
|
2837
|
+
sourceMessageId: row.source_message_id,
|
|
2838
|
+
platformMessageId: row.platform_message_id,
|
|
2839
|
+
imageKey: row.image_key,
|
|
2840
|
+
storedPath: row.stored_path,
|
|
2841
|
+
mimeType: row.mime_type,
|
|
2842
|
+
status: row.status,
|
|
2843
|
+
attempts: row.attempts,
|
|
2844
|
+
...row.last_error ? { lastError: row.last_error } : {},
|
|
2845
|
+
...row.derived_message_id ? { derivedMessageId: row.derived_message_id } : {},
|
|
2846
|
+
createdAt: row.created_at,
|
|
2847
|
+
updatedAt: row.updated_at
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
var ImageMultimodalTaskRepository = class {
|
|
2851
|
+
constructor(database) {
|
|
2852
|
+
this.database = database;
|
|
2853
|
+
}
|
|
2854
|
+
database;
|
|
2855
|
+
enqueue(input2) {
|
|
2856
|
+
const id = stableId3(input2.sourceMessageId, input2.imageKey);
|
|
2857
|
+
const timestamp = nowIso4();
|
|
2858
|
+
this.database.prepare(
|
|
2859
|
+
`
|
|
2860
|
+
INSERT INTO image_multimodal_tasks (
|
|
2861
|
+
id,
|
|
2862
|
+
source_message_id,
|
|
2863
|
+
platform_message_id,
|
|
2864
|
+
image_key,
|
|
2865
|
+
stored_path,
|
|
2866
|
+
mime_type,
|
|
2867
|
+
status,
|
|
2868
|
+
attempts,
|
|
2869
|
+
created_at,
|
|
2870
|
+
updated_at
|
|
2871
|
+
)
|
|
2872
|
+
VALUES (
|
|
2873
|
+
@id,
|
|
2874
|
+
@sourceMessageId,
|
|
2875
|
+
@platformMessageId,
|
|
2876
|
+
@imageKey,
|
|
2877
|
+
@storedPath,
|
|
2878
|
+
@mimeType,
|
|
2879
|
+
'pending',
|
|
2880
|
+
0,
|
|
2881
|
+
@createdAt,
|
|
2882
|
+
@updatedAt
|
|
2883
|
+
)
|
|
2884
|
+
ON CONFLICT(source_message_id, image_key)
|
|
2885
|
+
DO UPDATE SET
|
|
2886
|
+
platform_message_id = excluded.platform_message_id,
|
|
2887
|
+
stored_path = excluded.stored_path,
|
|
2888
|
+
mime_type = excluded.mime_type,
|
|
2889
|
+
status = 'pending',
|
|
2890
|
+
attempts = 0,
|
|
2891
|
+
last_error = NULL,
|
|
2892
|
+
derived_message_id = NULL,
|
|
2893
|
+
updated_at = excluded.updated_at
|
|
2894
|
+
`
|
|
2895
|
+
).run({
|
|
2896
|
+
id,
|
|
2897
|
+
sourceMessageId: input2.sourceMessageId,
|
|
2898
|
+
platformMessageId: input2.platformMessageId,
|
|
2899
|
+
imageKey: input2.imageKey,
|
|
2900
|
+
storedPath: input2.storedPath,
|
|
2901
|
+
mimeType: input2.mimeType,
|
|
2902
|
+
createdAt: timestamp,
|
|
2903
|
+
updatedAt: timestamp
|
|
2904
|
+
});
|
|
2905
|
+
const record = this.getById(id);
|
|
2906
|
+
if (!record) {
|
|
2907
|
+
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u5199\u5165\u5931\u8D25\uFF1A${id}`);
|
|
2908
|
+
}
|
|
2909
|
+
return record;
|
|
2910
|
+
}
|
|
2911
|
+
listPending(limit = 10) {
|
|
2912
|
+
const rows = this.database.prepare(
|
|
2913
|
+
`
|
|
2914
|
+
SELECT
|
|
2915
|
+
id,
|
|
2916
|
+
source_message_id,
|
|
2917
|
+
platform_message_id,
|
|
2918
|
+
image_key,
|
|
2919
|
+
stored_path,
|
|
2920
|
+
mime_type,
|
|
2921
|
+
status,
|
|
2922
|
+
attempts,
|
|
2923
|
+
last_error,
|
|
2924
|
+
derived_message_id,
|
|
2925
|
+
created_at,
|
|
2926
|
+
updated_at
|
|
2927
|
+
FROM image_multimodal_tasks
|
|
2928
|
+
WHERE status = 'pending'
|
|
2929
|
+
ORDER BY updated_at ASC
|
|
2930
|
+
LIMIT ?
|
|
2931
|
+
`
|
|
2932
|
+
).all(limit);
|
|
2933
|
+
return rows.map((row) => mapRow(row)).filter((row) => Boolean(row));
|
|
2934
|
+
}
|
|
2935
|
+
markRunning(id) {
|
|
2936
|
+
const result = this.database.prepare(
|
|
2937
|
+
`
|
|
2938
|
+
UPDATE image_multimodal_tasks
|
|
2939
|
+
SET status = 'running',
|
|
2940
|
+
attempts = attempts + 1,
|
|
2941
|
+
last_error = NULL,
|
|
2942
|
+
updated_at = @updatedAt
|
|
2943
|
+
WHERE id = @id AND status = 'pending'
|
|
2944
|
+
`
|
|
2945
|
+
).run({ id, updatedAt: nowIso4() });
|
|
2946
|
+
if (result.changes === 0) {
|
|
2947
|
+
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A${id}`);
|
|
2948
|
+
}
|
|
2949
|
+
return this.requireById(id);
|
|
2950
|
+
}
|
|
2951
|
+
markSucceeded(id, derivedMessageId) {
|
|
2952
|
+
this.database.prepare(
|
|
2953
|
+
`
|
|
2954
|
+
UPDATE image_multimodal_tasks
|
|
2955
|
+
SET status = 'succeeded',
|
|
2956
|
+
last_error = NULL,
|
|
2957
|
+
derived_message_id = @derivedMessageId,
|
|
2958
|
+
updated_at = @updatedAt
|
|
2959
|
+
WHERE id = @id
|
|
2960
|
+
`
|
|
2961
|
+
).run({ id, derivedMessageId, updatedAt: nowIso4() });
|
|
2962
|
+
return this.requireById(id);
|
|
2963
|
+
}
|
|
2964
|
+
markSkipped(id, reason) {
|
|
2965
|
+
this.database.prepare(
|
|
2966
|
+
`
|
|
2967
|
+
UPDATE image_multimodal_tasks
|
|
2968
|
+
SET status = 'skipped',
|
|
2969
|
+
last_error = @reason,
|
|
2970
|
+
derived_message_id = NULL,
|
|
2971
|
+
updated_at = @updatedAt
|
|
2972
|
+
WHERE id = @id
|
|
2973
|
+
`
|
|
2974
|
+
).run({ id, reason, updatedAt: nowIso4() });
|
|
2975
|
+
return this.requireById(id);
|
|
2976
|
+
}
|
|
2977
|
+
markFailed(id, error, finalFailure) {
|
|
2978
|
+
this.database.prepare(
|
|
2979
|
+
`
|
|
2980
|
+
UPDATE image_multimodal_tasks
|
|
2981
|
+
SET status = @status,
|
|
2982
|
+
last_error = @error,
|
|
2983
|
+
derived_message_id = NULL,
|
|
2984
|
+
updated_at = @updatedAt
|
|
2985
|
+
WHERE id = @id
|
|
2986
|
+
`
|
|
2987
|
+
).run({ id, status: finalFailure ? "failed" : "pending", error, updatedAt: nowIso4() });
|
|
2988
|
+
return this.requireById(id);
|
|
2989
|
+
}
|
|
2990
|
+
getById(id) {
|
|
2991
|
+
const row = this.database.prepare(
|
|
2992
|
+
`
|
|
2993
|
+
SELECT
|
|
2994
|
+
id,
|
|
2995
|
+
source_message_id,
|
|
2996
|
+
platform_message_id,
|
|
2997
|
+
image_key,
|
|
2998
|
+
stored_path,
|
|
2999
|
+
mime_type,
|
|
3000
|
+
status,
|
|
3001
|
+
attempts,
|
|
3002
|
+
last_error,
|
|
3003
|
+
derived_message_id,
|
|
3004
|
+
created_at,
|
|
3005
|
+
updated_at
|
|
3006
|
+
FROM image_multimodal_tasks
|
|
3007
|
+
WHERE id = ?
|
|
3008
|
+
`
|
|
3009
|
+
).get(id);
|
|
3010
|
+
return mapRow(row);
|
|
3011
|
+
}
|
|
3012
|
+
requireById(id) {
|
|
3013
|
+
const record = this.getById(id);
|
|
3014
|
+
if (!record) {
|
|
3015
|
+
throw new Error(`\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u4E0D\u5B58\u5728\uFF1A${id}`);
|
|
3016
|
+
}
|
|
3017
|
+
return record;
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
|
|
3021
|
+
// src/multimodal/worker.ts
|
|
3022
|
+
var ImageMultimodalWorker = class {
|
|
3023
|
+
constructor(options) {
|
|
3024
|
+
this.options = options;
|
|
3025
|
+
}
|
|
3026
|
+
options;
|
|
3027
|
+
async processPending(limit = 10) {
|
|
3028
|
+
const result = { processed: 0, succeeded: 0, skipped: 0, failed: 0 };
|
|
3029
|
+
const pending = this.options.tasks.listPending(limit);
|
|
3030
|
+
for (const task of pending) {
|
|
3031
|
+
result.processed += 1;
|
|
3032
|
+
await this.processTask(task, result);
|
|
3033
|
+
}
|
|
3034
|
+
return result;
|
|
3035
|
+
}
|
|
3036
|
+
async processTask(task, result) {
|
|
3037
|
+
let running;
|
|
3038
|
+
try {
|
|
3039
|
+
running = this.options.tasks.markRunning(task.id);
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3042
|
+
if (message.startsWith("\u56FE\u7247\u591A\u6A21\u6001\u4EFB\u52A1\u72B6\u6001\u65E0\u6CD5\u66F4\u65B0\uFF1A")) {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
throw error;
|
|
3046
|
+
}
|
|
3047
|
+
try {
|
|
3048
|
+
const described = await this.options.model.describeImage({
|
|
3049
|
+
imagePath: running.storedPath,
|
|
3050
|
+
mimeType: running.mimeType
|
|
3051
|
+
});
|
|
3052
|
+
if (!described.isMeaningful) {
|
|
3053
|
+
this.options.tasks.markSkipped(running.id, described.reason || "\u591A\u6A21\u6001\u6A21\u578B\u5224\u5B9A\u56FE\u7247\u65E0\u610F\u4E49\u3002");
|
|
3054
|
+
result.skipped += 1;
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
const derivedMessageId = this.options.messages.createImageSummaryMessage({
|
|
3058
|
+
sourceMessageId: running.sourceMessageId,
|
|
3059
|
+
imageKey: running.imageKey,
|
|
3060
|
+
summary: described.summary,
|
|
3061
|
+
reason: described.reason,
|
|
3062
|
+
multimodalModel: this.options.multimodalModelName,
|
|
3063
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3064
|
+
});
|
|
3065
|
+
if (this.options.vectorIndexMessage) {
|
|
3066
|
+
await this.options.vectorIndexMessage(derivedMessageId);
|
|
3067
|
+
}
|
|
3068
|
+
if (this.options.episodes && this.options.summarizeEpisode) {
|
|
3069
|
+
await this.options.episodes.refreshWindowForMessage({
|
|
3070
|
+
messageId: derivedMessageId,
|
|
3071
|
+
windowMs: this.options.config.episodes.windowMinutes * 60 * 1e3,
|
|
3072
|
+
summarize: this.options.summarizeEpisode
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
this.options.tasks.markSucceeded(running.id, derivedMessageId);
|
|
3076
|
+
result.succeeded += 1;
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3079
|
+
this.options.tasks.markFailed(running.id, message, running.attempts >= 3);
|
|
3080
|
+
result.failed += 1;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
|
|
2481
3085
|
// src/rag/citations.ts
|
|
2482
3086
|
function isOpaqueId(value) {
|
|
2483
3087
|
return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
|
|
@@ -2701,6 +3305,108 @@ async function askWithAgenticRag(input2) {
|
|
|
2701
3305
|
});
|
|
2702
3306
|
}
|
|
2703
3307
|
|
|
3308
|
+
// src/rag/qa-logs.ts
|
|
3309
|
+
import crypto5 from "crypto";
|
|
3310
|
+
function clampLimit(limit) {
|
|
3311
|
+
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3312
|
+
}
|
|
3313
|
+
var QaLogRepository = class {
|
|
3314
|
+
constructor(database) {
|
|
3315
|
+
this.database = database;
|
|
3316
|
+
}
|
|
3317
|
+
database;
|
|
3318
|
+
create(input2) {
|
|
3319
|
+
const record = {
|
|
3320
|
+
id: `qa_${crypto5.randomUUID()}`,
|
|
3321
|
+
chatId: input2.chatId ?? null,
|
|
3322
|
+
questionMessageId: input2.questionMessageId ?? null,
|
|
3323
|
+
question: input2.question,
|
|
3324
|
+
answer: input2.answer,
|
|
3325
|
+
citations: input2.citations,
|
|
3326
|
+
retrievalDebug: input2.retrievalDebug,
|
|
3327
|
+
status: input2.status,
|
|
3328
|
+
error: input2.error ?? null,
|
|
3329
|
+
createdAt: input2.createdAt
|
|
3330
|
+
};
|
|
3331
|
+
this.database.prepare(
|
|
3332
|
+
`
|
|
3333
|
+
INSERT INTO qa_logs (
|
|
3334
|
+
id,
|
|
3335
|
+
chat_id,
|
|
3336
|
+
question_message_id,
|
|
3337
|
+
question,
|
|
3338
|
+
answer,
|
|
3339
|
+
citations_json,
|
|
3340
|
+
retrieval_debug_json,
|
|
3341
|
+
status,
|
|
3342
|
+
error,
|
|
3343
|
+
created_at
|
|
3344
|
+
)
|
|
3345
|
+
VALUES (
|
|
3346
|
+
@id,
|
|
3347
|
+
@chatId,
|
|
3348
|
+
@questionMessageId,
|
|
3349
|
+
@question,
|
|
3350
|
+
@answer,
|
|
3351
|
+
@citationsJson,
|
|
3352
|
+
@retrievalDebugJson,
|
|
3353
|
+
@status,
|
|
3354
|
+
@error,
|
|
3355
|
+
@createdAt
|
|
3356
|
+
)
|
|
3357
|
+
`
|
|
3358
|
+
).run({
|
|
3359
|
+
id: record.id,
|
|
3360
|
+
chatId: record.chatId,
|
|
3361
|
+
questionMessageId: record.questionMessageId,
|
|
3362
|
+
question: record.question,
|
|
3363
|
+
answer: record.answer,
|
|
3364
|
+
citationsJson: JSON.stringify(record.citations),
|
|
3365
|
+
retrievalDebugJson: JSON.stringify(record.retrievalDebug),
|
|
3366
|
+
status: record.status,
|
|
3367
|
+
error: record.error,
|
|
3368
|
+
createdAt: record.createdAt
|
|
3369
|
+
});
|
|
3370
|
+
return record;
|
|
3371
|
+
}
|
|
3372
|
+
listRecent(limit) {
|
|
3373
|
+
const rows = this.database.prepare(
|
|
3374
|
+
`
|
|
3375
|
+
SELECT
|
|
3376
|
+
id,
|
|
3377
|
+
chat_id,
|
|
3378
|
+
question_message_id,
|
|
3379
|
+
question,
|
|
3380
|
+
answer,
|
|
3381
|
+
citations_json,
|
|
3382
|
+
retrieval_debug_json,
|
|
3383
|
+
status,
|
|
3384
|
+
error,
|
|
3385
|
+
created_at
|
|
3386
|
+
FROM qa_logs
|
|
3387
|
+
ORDER BY created_at DESC
|
|
3388
|
+
LIMIT ?
|
|
3389
|
+
`
|
|
3390
|
+
).all(clampLimit(limit));
|
|
3391
|
+
return rows.map((row) => ({
|
|
3392
|
+
id: row.id,
|
|
3393
|
+
chatId: row.chat_id,
|
|
3394
|
+
questionMessageId: row.question_message_id,
|
|
3395
|
+
question: row.question,
|
|
3396
|
+
answer: row.answer,
|
|
3397
|
+
citations: JSON.parse(row.citations_json),
|
|
3398
|
+
retrievalDebug: JSON.parse(row.retrieval_debug_json),
|
|
3399
|
+
status: row.status,
|
|
3400
|
+
error: row.error,
|
|
3401
|
+
createdAt: row.created_at
|
|
3402
|
+
}));
|
|
3403
|
+
}
|
|
3404
|
+
getCount() {
|
|
3405
|
+
const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
|
|
3406
|
+
return row.count;
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
|
|
2704
3410
|
// src/feishu/question.ts
|
|
2705
3411
|
function parseTextContent(content) {
|
|
2706
3412
|
if (!content) {
|
|
@@ -2807,6 +3513,7 @@ var FeishuQuestionHandler = class {
|
|
|
2807
3513
|
return decision;
|
|
2808
3514
|
}
|
|
2809
3515
|
const questionMessageId = payload.event?.message?.message_id;
|
|
3516
|
+
const qaLogs = new QaLogRepository(this.options.database);
|
|
2810
3517
|
await this.acknowledgeQuestion(decision.chatId, questionMessageId);
|
|
2811
3518
|
const { tools, close } = await createAgenticRagSearchTools({
|
|
2812
3519
|
config: this.options.config,
|
|
@@ -2822,6 +3529,16 @@ var FeishuQuestionHandler = class {
|
|
|
2822
3529
|
tools,
|
|
2823
3530
|
model: this.options.model
|
|
2824
3531
|
});
|
|
3532
|
+
qaLogs.create({
|
|
3533
|
+
chatId: decision.chatId,
|
|
3534
|
+
questionMessageId,
|
|
3535
|
+
question: decision.question,
|
|
3536
|
+
answer: result.answer,
|
|
3537
|
+
citations: result.citations,
|
|
3538
|
+
retrievalDebug: { evidenceCount: result.citations.length },
|
|
3539
|
+
status: "answered",
|
|
3540
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3541
|
+
});
|
|
2825
3542
|
const citations = formatCitations(result.citations);
|
|
2826
3543
|
const text = citations ? `${result.answer}
|
|
2827
3544
|
|
|
@@ -2830,6 +3547,17 @@ ${citations}` : result.answer;
|
|
|
2830
3547
|
await this.sendResponse(decision.chatId, questionMessageId, text);
|
|
2831
3548
|
} catch (error) {
|
|
2832
3549
|
const message = error instanceof Error ? error.message : String(error);
|
|
3550
|
+
qaLogs.create({
|
|
3551
|
+
chatId: decision.chatId,
|
|
3552
|
+
questionMessageId,
|
|
3553
|
+
question: decision.question,
|
|
3554
|
+
answer: `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`,
|
|
3555
|
+
citations: [],
|
|
3556
|
+
retrievalDebug: {},
|
|
3557
|
+
status: "failed",
|
|
3558
|
+
error: message,
|
|
3559
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3560
|
+
});
|
|
2833
3561
|
await this.sendResponse(decision.chatId, questionMessageId, `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`);
|
|
2834
3562
|
}
|
|
2835
3563
|
return decision;
|
|
@@ -2955,6 +3683,7 @@ function createFeishuEventDispatcher(options) {
|
|
|
2955
3683
|
payload,
|
|
2956
3684
|
downloader: options.resourceDownloader,
|
|
2957
3685
|
config: options.config,
|
|
3686
|
+
secrets: options.secrets,
|
|
2958
3687
|
vectorIndexMessage: options.attachmentVectorIndexer
|
|
2959
3688
|
}) : options.ingestor.ingestFeishuEvent(payload);
|
|
2960
3689
|
if (!result.accepted) {
|
|
@@ -2980,6 +3709,23 @@ function createFeishuEventDispatcher(options) {
|
|
|
2980
3709
|
}
|
|
2981
3710
|
if (result.attachment?.downloaded) {
|
|
2982
3711
|
console.log(`\u98DE\u4E66\u9644\u4EF6\u5DF2\u4E0B\u8F7D\uFF1A${result.attachment.downloaded.storedPath}`);
|
|
3712
|
+
if (options.imageMultimodalProcessor && result.attachment.imageTask) {
|
|
3713
|
+
void new ImageMultimodalWorker({
|
|
3714
|
+
config: options.config,
|
|
3715
|
+
messages: new MessageRepository(options.imageMultimodalProcessor.database),
|
|
3716
|
+
tasks: new ImageMultimodalTaskRepository(options.imageMultimodalProcessor.database),
|
|
3717
|
+
model: options.imageMultimodalProcessor.model,
|
|
3718
|
+
multimodalModelName: options.config.multimodal.model,
|
|
3719
|
+
vectorIndexMessage: options.attachmentVectorIndexer
|
|
3720
|
+
}).processPending().then((imageResult) => {
|
|
3721
|
+
console.log(
|
|
3722
|
+
`\u98DE\u4E66\u56FE\u7247\u591A\u6A21\u6001\u5904\u7406\u5B8C\u6210\uFF1Aprocessed=${imageResult.processed}, succeeded=${imageResult.succeeded}, skipped=${imageResult.skipped}, failed=${imageResult.failed}`
|
|
3723
|
+
);
|
|
3724
|
+
}).catch((error) => {
|
|
3725
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3726
|
+
console.error(`\u98DE\u4E66\u56FE\u7247\u591A\u6A21\u6001\u5904\u7406\u5931\u8D25\uFF1A${message}`);
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
2983
3729
|
if (result.attachment.indexedMessageId) {
|
|
2984
3730
|
console.log(`\u98DE\u4E66\u9644\u4EF6\u5DF2\u8FDB\u5165 RAG\uFF1A${result.attachment.indexedMessageId}`);
|
|
2985
3731
|
if (result.attachment.vectorIndexed) {
|
|
@@ -3030,17 +3776,32 @@ function createFeishuGateway(options) {
|
|
|
3030
3776
|
questionHandler: options.questionHandler,
|
|
3031
3777
|
resourceDownloader: options.resourceDownloader,
|
|
3032
3778
|
attachmentVectorIndexer: options.attachmentVectorIndexer,
|
|
3033
|
-
episodeProcessor: options.episodeProcessor
|
|
3779
|
+
episodeProcessor: options.episodeProcessor,
|
|
3780
|
+
imageMultimodalProcessor: options.imageMultimodalProcessor
|
|
3034
3781
|
});
|
|
3782
|
+
const indexingScheduler = options.indexingScheduler ?? (options.indexingProcessor ? createIndexingScheduler({
|
|
3783
|
+
schedule: options.config.schedules.indexing,
|
|
3784
|
+
work: async () => {
|
|
3785
|
+
await processMessagesNow({
|
|
3786
|
+
config: options.config,
|
|
3787
|
+
secrets: options.secrets,
|
|
3788
|
+
database: options.indexingProcessor.database,
|
|
3789
|
+
limit: 1e4
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
}) : void 0);
|
|
3035
3793
|
return {
|
|
3036
3794
|
async start() {
|
|
3037
3795
|
try {
|
|
3038
3796
|
await wsClient.start({ eventDispatcher });
|
|
3797
|
+
indexingScheduler?.start();
|
|
3039
3798
|
} catch (error) {
|
|
3799
|
+
indexingScheduler?.stop();
|
|
3040
3800
|
throw formatGatewayStartError(error);
|
|
3041
3801
|
}
|
|
3042
3802
|
},
|
|
3043
3803
|
stop() {
|
|
3804
|
+
indexingScheduler?.stop();
|
|
3044
3805
|
wsClient.close({ force: true });
|
|
3045
3806
|
}
|
|
3046
3807
|
};
|
|
@@ -3112,7 +3873,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
3112
3873
|
};
|
|
3113
3874
|
|
|
3114
3875
|
// src/files/ingest.ts
|
|
3115
|
-
import
|
|
3876
|
+
import crypto6 from "crypto";
|
|
3116
3877
|
import fs11 from "fs/promises";
|
|
3117
3878
|
import path13 from "path";
|
|
3118
3879
|
|
|
@@ -3176,7 +3937,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
3176
3937
|
}
|
|
3177
3938
|
}
|
|
3178
3939
|
function stableStoredName(sourcePath, fileName) {
|
|
3179
|
-
const digest =
|
|
3940
|
+
const digest = crypto6.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
3180
3941
|
return `${digest}-${fileName}`;
|
|
3181
3942
|
}
|
|
3182
3943
|
async function ingestLocalFile(input2) {
|
|
@@ -3414,12 +4175,17 @@ function extractAttachment(message) {
|
|
|
3414
4175
|
}
|
|
3415
4176
|
return candidate;
|
|
3416
4177
|
}
|
|
4178
|
+
function isMultimodalReady(config, secrets) {
|
|
4179
|
+
return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
|
|
4180
|
+
}
|
|
3417
4181
|
var GatewayIngestor = class {
|
|
3418
4182
|
messages;
|
|
3419
4183
|
jobs;
|
|
4184
|
+
imageTasks;
|
|
3420
4185
|
constructor(database) {
|
|
3421
4186
|
this.messages = new MessageRepository(database);
|
|
3422
4187
|
this.jobs = new FileJobRepository(database);
|
|
4188
|
+
this.imageTasks = new ImageMultimodalTaskRepository(database);
|
|
3423
4189
|
}
|
|
3424
4190
|
ingestFeishuEvent(payload) {
|
|
3425
4191
|
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
@@ -3451,6 +4217,23 @@ var GatewayIngestor = class {
|
|
|
3451
4217
|
messageId: result.message.platformMessageId,
|
|
3452
4218
|
attachment
|
|
3453
4219
|
});
|
|
4220
|
+
if (attachment.kind === "image") {
|
|
4221
|
+
const imageTask = isMultimodalReady(input2.config, input2.secrets) ? this.imageTasks.enqueue({
|
|
4222
|
+
sourceMessageId: result.messageId,
|
|
4223
|
+
platformMessageId: result.message.platformMessageId,
|
|
4224
|
+
imageKey: attachment.fileKey,
|
|
4225
|
+
storedPath: downloaded.storedPath,
|
|
4226
|
+
mimeType: attachment.mimeType || "image/jpeg"
|
|
4227
|
+
}) : void 0;
|
|
4228
|
+
return {
|
|
4229
|
+
...result,
|
|
4230
|
+
attachment: {
|
|
4231
|
+
downloaded,
|
|
4232
|
+
...imageTask ? { imageTask } : {},
|
|
4233
|
+
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"
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
3454
4237
|
if (!isSupportedTextFile(downloaded.storedPath)) {
|
|
3455
4238
|
return {
|
|
3456
4239
|
...result,
|
|
@@ -3586,82 +4369,94 @@ async function startDetachedGateway(input2) {
|
|
|
3586
4369
|
}
|
|
3587
4370
|
}
|
|
3588
4371
|
|
|
3589
|
-
// src/
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
4372
|
+
// src/multimodal/openai-compatible.ts
|
|
4373
|
+
import fs13 from "fs/promises";
|
|
4374
|
+
function normalizeBaseUrl2(baseUrl) {
|
|
4375
|
+
return baseUrl.replace(/\/+$/, "");
|
|
4376
|
+
}
|
|
4377
|
+
function buildPrompt(context) {
|
|
4378
|
+
const contextText = context?.trim();
|
|
4379
|
+
return [
|
|
4380
|
+
"\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",
|
|
4381
|
+
'\u8BF7\u53EA\u8F93\u51FA JSON\uFF0C\u683C\u5F0F\u4E3A {"summary": string, "isMeaningful": boolean, "reason": string}\u3002',
|
|
4382
|
+
"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",
|
|
4383
|
+
contextText ? `\u4E0A\u4E0B\u6587\uFF1A${contextText}` : void 0
|
|
4384
|
+
].filter(Boolean).join("\n");
|
|
4385
|
+
}
|
|
4386
|
+
function parseDescribeImageResult(content) {
|
|
4387
|
+
let data2;
|
|
4388
|
+
try {
|
|
4389
|
+
data2 = JSON.parse(content);
|
|
4390
|
+
} catch {
|
|
4391
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u7684 JSON \u65E0\u6CD5\u89E3\u6790\u3002");
|
|
3594
4392
|
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
for (const [index2, chunk] of chunks.entries()) {
|
|
3598
|
-
const vector = vectors[index2];
|
|
3599
|
-
if (!vector || vector.length === 0) {
|
|
3600
|
-
continue;
|
|
3601
|
-
}
|
|
3602
|
-
records.push({
|
|
3603
|
-
id: chunk.chunkId,
|
|
3604
|
-
vector,
|
|
3605
|
-
evidence: {
|
|
3606
|
-
id: chunk.chunkId,
|
|
3607
|
-
text: chunk.text,
|
|
3608
|
-
score: 1,
|
|
3609
|
-
source: toEvidenceSource3(chunk)
|
|
3610
|
-
}
|
|
3611
|
-
});
|
|
4393
|
+
if (!data2 || typeof data2 !== "object") {
|
|
4394
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u683C\u5F0F\u4E0D\u6B63\u786E\u3002");
|
|
3612
4395
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
};
|
|
3618
|
-
}
|
|
3619
|
-
function toEvidenceSource3(chunk) {
|
|
3620
|
-
if (chunk.messageType === "file") {
|
|
3621
|
-
return {
|
|
3622
|
-
type: "file",
|
|
3623
|
-
label: chunk.senderName,
|
|
3624
|
-
timestamp: chunk.sentAt
|
|
3625
|
-
};
|
|
4396
|
+
const result = data2;
|
|
4397
|
+
const summary = typeof result.summary === "string" ? result.summary.trim() : "";
|
|
4398
|
+
if (!summary) {
|
|
4399
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u7684 summary \u4E3A\u7A7A\u3002");
|
|
3626
4400
|
}
|
|
4401
|
+
if (typeof result.isMeaningful !== "boolean") {
|
|
4402
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u7684 isMeaningful \u4E0D\u662F\u5E03\u5C14\u503C\u3002");
|
|
4403
|
+
}
|
|
4404
|
+
const reason = typeof result.reason === "string" ? result.reason.trim() : "";
|
|
3627
4405
|
return {
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
timestamp: chunk.sentAt
|
|
4406
|
+
summary,
|
|
4407
|
+
isMeaningful: result.isMeaningful,
|
|
4408
|
+
...reason ? { reason } : {}
|
|
3632
4409
|
};
|
|
3633
4410
|
}
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3638
|
-
if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
|
|
3639
|
-
return {
|
|
3640
|
-
status: "skipped",
|
|
3641
|
-
reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
|
|
3642
|
-
chunks: 0,
|
|
3643
|
-
vectors: 0,
|
|
3644
|
-
startedAt,
|
|
3645
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3646
|
-
};
|
|
4411
|
+
var OpenAICompatibleMultimodalModel = class {
|
|
4412
|
+
constructor(options) {
|
|
4413
|
+
this.options = options;
|
|
3647
4414
|
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
4415
|
+
options;
|
|
4416
|
+
async describeImage(input2) {
|
|
4417
|
+
if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
|
|
4418
|
+
throw new Error("\u591A\u6A21\u6001\u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
|
|
4419
|
+
}
|
|
4420
|
+
const image = await fs13.readFile(input2.imagePath);
|
|
4421
|
+
const response = await fetch(`${normalizeBaseUrl2(this.options.baseUrl)}/chat/completions`, {
|
|
4422
|
+
method: "POST",
|
|
4423
|
+
headers: {
|
|
4424
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
4425
|
+
"content-type": "application/json"
|
|
4426
|
+
},
|
|
4427
|
+
body: JSON.stringify({
|
|
4428
|
+
model: this.options.model,
|
|
4429
|
+
messages: [
|
|
4430
|
+
{
|
|
4431
|
+
role: "user",
|
|
4432
|
+
content: [
|
|
4433
|
+
{ type: "text", text: buildPrompt(input2.context) },
|
|
4434
|
+
{ type: "image_url", image_url: { url: `data:${input2.mimeType};base64,${image.toString("base64")}` } }
|
|
4435
|
+
]
|
|
4436
|
+
}
|
|
4437
|
+
],
|
|
4438
|
+
response_format: { type: "json_object" },
|
|
4439
|
+
temperature: this.options.temperature ?? 0.2
|
|
4440
|
+
})
|
|
4441
|
+
});
|
|
4442
|
+
if (!response.ok) {
|
|
4443
|
+
const body = await response.text();
|
|
4444
|
+
throw new Error(`\u591A\u6A21\u6001\u8BF7\u6C42\u5931\u8D25\uFF1A${response.status} ${body}`);
|
|
4445
|
+
}
|
|
4446
|
+
const data2 = await response.json();
|
|
4447
|
+
const content = data2.choices?.[0]?.message?.content?.trim();
|
|
4448
|
+
if (!content) {
|
|
4449
|
+
throw new Error("\u591A\u6A21\u6001\u6A21\u578B\u8FD4\u56DE\u4E3A\u7A7A\u3002");
|
|
4450
|
+
}
|
|
4451
|
+
return parseDescribeImageResult(content);
|
|
4452
|
+
}
|
|
4453
|
+
};
|
|
4454
|
+
function createMultimodalModel(config, secrets) {
|
|
4455
|
+
return new OpenAICompatibleMultimodalModel({
|
|
4456
|
+
baseUrl: config.multimodal.baseUrl,
|
|
4457
|
+
apiKey: secrets.multimodal.apiKey,
|
|
4458
|
+
model: config.multimodal.model
|
|
3657
4459
|
});
|
|
3658
|
-
return {
|
|
3659
|
-
status: "completed",
|
|
3660
|
-
chunks: stats.chunks,
|
|
3661
|
-
vectors: stats.vectors,
|
|
3662
|
-
startedAt,
|
|
3663
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3664
|
-
};
|
|
3665
4460
|
}
|
|
3666
4461
|
|
|
3667
4462
|
// src/rag/qa-service.ts
|
|
@@ -3912,6 +4707,10 @@ function buildHtml() {
|
|
|
3912
4707
|
<h2>\u4F1A\u8BDD\u8BB0\u5FC6</h2>
|
|
3913
4708
|
<div id="episodes" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
3914
4709
|
</section>
|
|
4710
|
+
<section>
|
|
4711
|
+
<h2>\u95EE\u7B54\u65E5\u5FD7</h2>
|
|
4712
|
+
<div id="qa-logs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
4713
|
+
</section>
|
|
3915
4714
|
</div>
|
|
3916
4715
|
<aside>
|
|
3917
4716
|
<section>
|
|
@@ -3942,6 +4741,7 @@ function buildHtml() {
|
|
|
3942
4741
|
const chats = document.querySelector("#chats");
|
|
3943
4742
|
const files = document.querySelector("#files");
|
|
3944
4743
|
const fileJobs = document.querySelector("#file-jobs");
|
|
4744
|
+
const qaLogs = document.querySelector("#qa-logs");
|
|
3945
4745
|
const processMessages = document.querySelector("#process-messages");
|
|
3946
4746
|
const actionStatus = document.querySelector("#action-status");
|
|
3947
4747
|
|
|
@@ -4133,14 +4933,45 @@ function buildHtml() {
|
|
|
4133
4933
|
\`;
|
|
4134
4934
|
}
|
|
4135
4935
|
|
|
4936
|
+
function renderQaLogs(items) {
|
|
4937
|
+
if (items.length === 0) {
|
|
4938
|
+
qaLogs.className = "empty";
|
|
4939
|
+
qaLogs.textContent = "\u8FD8\u6CA1\u6709\u95EE\u7B54\u65E5\u5FD7\u3002";
|
|
4940
|
+
return;
|
|
4941
|
+
}
|
|
4942
|
+
qaLogs.className = "";
|
|
4943
|
+
const rows = items.map((item) => {
|
|
4944
|
+
const citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
|
|
4945
|
+
return [
|
|
4946
|
+
'<article class="message-item">',
|
|
4947
|
+
' <div class="message-meta">',
|
|
4948
|
+
" <span>" + escapeHtml(formatDateTime(item.createdAt)) + "</span>",
|
|
4949
|
+
" <span>" + escapeHtml(item.status) + "</span>",
|
|
4950
|
+
" <span>" + escapeHtml(citationCount) + " \u6761\u5F15\u7528</span>",
|
|
4951
|
+
" </div>",
|
|
4952
|
+
" <div class="message-body"><strong>\u95EE\uFF1A</strong>" + escapeHtml(item.question) + "</div>",
|
|
4953
|
+
" <div class="message-body"><strong>\u7B54\uFF1A</strong>" + escapeHtml(item.answer) + "</div>",
|
|
4954
|
+
"</article>",
|
|
4955
|
+
].join("
|
|
4956
|
+
");
|
|
4957
|
+
});
|
|
4958
|
+
qaLogs.innerHTML = [
|
|
4959
|
+
'<div class="message-list">',
|
|
4960
|
+
rows.join(""),
|
|
4961
|
+
"</div>",
|
|
4962
|
+
].join("
|
|
4963
|
+
");
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4136
4966
|
async function load() {
|
|
4137
|
-
const [status, recent, episodeList, chatList, fileList, jobList] = await Promise.all([
|
|
4967
|
+
const [status, recent, episodeList, chatList, fileList, jobList, qaLogList] = await Promise.all([
|
|
4138
4968
|
fetch("/api/status").then((response) => response.json()),
|
|
4139
4969
|
fetch("/api/messages/recent?limit=20").then((response) => response.json()),
|
|
4140
4970
|
fetch("/api/episodes?limit=10").then((response) => response.json()),
|
|
4141
4971
|
fetch("/api/chats").then((response) => response.json()),
|
|
4142
4972
|
fetch("/api/files").then((response) => response.json()),
|
|
4143
4973
|
fetch("/api/file-jobs").then((response) => response.json()),
|
|
4974
|
+
fetch("/api/qa-logs?limit=10").then((response) => response.json()),
|
|
4144
4975
|
]);
|
|
4145
4976
|
renderMetrics(status);
|
|
4146
4977
|
renderMessages(recent.items);
|
|
@@ -4148,6 +4979,7 @@ function buildHtml() {
|
|
|
4148
4979
|
renderChats(chatList.items);
|
|
4149
4980
|
renderFiles(fileList.items);
|
|
4150
4981
|
renderFileJobs(jobList.items);
|
|
4982
|
+
renderQaLogs(qaLogList.items);
|
|
4151
4983
|
}
|
|
4152
4984
|
|
|
4153
4985
|
async function processNow() {
|
|
@@ -4195,6 +5027,7 @@ function createWebApp(config) {
|
|
|
4195
5027
|
const messages = new MessageRepository(database);
|
|
4196
5028
|
const episodes = new EpisodeRepository(database);
|
|
4197
5029
|
const fileJobs = new FileJobRepository(database);
|
|
5030
|
+
const qaLogs = new QaLogRepository(database);
|
|
4198
5031
|
app.addHook("onClose", async () => {
|
|
4199
5032
|
database.close();
|
|
4200
5033
|
});
|
|
@@ -4205,7 +5038,8 @@ function createWebApp(config) {
|
|
|
4205
5038
|
chats: messages.getChatCount(),
|
|
4206
5039
|
messages: messages.getMessageCount(),
|
|
4207
5040
|
episodes: episodes.getEpisodeCount(),
|
|
4208
|
-
files: messages.listFiles(1e3).length
|
|
5041
|
+
files: messages.listFiles(1e3).length,
|
|
5042
|
+
qaLogs: qaLogs.getCount()
|
|
4209
5043
|
},
|
|
4210
5044
|
rag: {
|
|
4211
5045
|
mode: "required",
|
|
@@ -4246,6 +5080,12 @@ function createWebApp(config) {
|
|
|
4246
5080
|
items: episodes.listRecentEpisodes(limit)
|
|
4247
5081
|
};
|
|
4248
5082
|
});
|
|
5083
|
+
app.get("/api/qa-logs", async (request) => {
|
|
5084
|
+
const limit = parseLimit(request.query.limit, 20, 100);
|
|
5085
|
+
return {
|
|
5086
|
+
items: qaLogs.listRecent(limit)
|
|
5087
|
+
};
|
|
5088
|
+
});
|
|
4249
5089
|
app.post("/api/process/messages", async (_request, reply) => {
|
|
4250
5090
|
try {
|
|
4251
5091
|
return await processMessagesNow({
|
|
@@ -4312,12 +5152,31 @@ async function promptForConfiguration(config, secrets) {
|
|
|
4312
5152
|
llmApiKey: secrets.llm.apiKey
|
|
4313
5153
|
});
|
|
4314
5154
|
config.embedding.model = await input({ message: "Embedding Model", default: config.embedding.model });
|
|
5155
|
+
const multimodalBaseUrl = await input({
|
|
5156
|
+
message: "Multimodal Base URL\uFF08OpenAI-compatible\uFF0C\u53EF\u7559\u7A7A\uFF09",
|
|
5157
|
+
default: config.multimodal.baseUrl
|
|
5158
|
+
});
|
|
5159
|
+
const multimodalApiKey = await password({
|
|
5160
|
+
message: "Multimodal API Key\uFF08\u53EF\u7559\u7A7A\uFF09",
|
|
5161
|
+
mask: "*"
|
|
5162
|
+
});
|
|
5163
|
+
const multimodalModel = await input({
|
|
5164
|
+
message: "Multimodal Model\uFF08\u53EF\u7559\u7A7A\uFF09",
|
|
5165
|
+
default: config.multimodal.model
|
|
5166
|
+
});
|
|
4315
5167
|
const dimension = await number({
|
|
4316
5168
|
message: "Embedding \u7EF4\u5EA6\uFF08\u4E0D\u77E5\u9053\u53EF\u5148\u7559\u7A7A\uFF09",
|
|
4317
5169
|
default: config.embedding.dimension ?? void 0,
|
|
4318
5170
|
required: false
|
|
4319
5171
|
});
|
|
4320
5172
|
config.embedding.dimension = dimension ?? null;
|
|
5173
|
+
config.multimodal = {
|
|
5174
|
+
baseUrl: multimodalBaseUrl,
|
|
5175
|
+
model: multimodalModel
|
|
5176
|
+
};
|
|
5177
|
+
secrets.multimodal = {
|
|
5178
|
+
apiKey: multimodalApiKey || secrets.multimodal.apiKey
|
|
5179
|
+
};
|
|
4321
5180
|
config.web.port = await number({ message: "Web UI \u7AEF\u53E3", default: config.web.port, required: true }) ?? config.web.port;
|
|
4322
5181
|
config.feishu.requireMention = await confirm({
|
|
4323
5182
|
message: "\u7FA4\u804A\u56DE\u7B54\u662F\u5426\u8981\u6C42 @ \u673A\u5668\u4EBA\uFF1F",
|
|
@@ -4345,7 +5204,8 @@ function printSettings(config, secrets) {
|
|
|
4345
5204
|
secrets: {
|
|
4346
5205
|
feishu: { appSecret: maskSecret(secrets.feishu.appSecret) },
|
|
4347
5206
|
llm: { apiKey: maskSecret(secrets.llm.apiKey) },
|
|
4348
|
-
embedding: { apiKey: maskSecret(secrets.embedding.apiKey) }
|
|
5207
|
+
embedding: { apiKey: maskSecret(secrets.embedding.apiKey) },
|
|
5208
|
+
multimodal: { apiKey: maskSecret(secrets.multimodal.apiKey) }
|
|
4349
5209
|
}
|
|
4350
5210
|
},
|
|
4351
5211
|
null,
|
|
@@ -4457,6 +5317,13 @@ async function startGatewayForegroundCommand() {
|
|
|
4457
5317
|
database,
|
|
4458
5318
|
model: createChatModel(config, secrets)
|
|
4459
5319
|
},
|
|
5320
|
+
imageMultimodalProcessor: config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey ? {
|
|
5321
|
+
database,
|
|
5322
|
+
model: createMultimodalModel(config, secrets)
|
|
5323
|
+
} : void 0,
|
|
5324
|
+
indexingProcessor: {
|
|
5325
|
+
database
|
|
5326
|
+
},
|
|
4460
5327
|
questionHandler: new FeishuQuestionHandler({
|
|
4461
5328
|
config,
|
|
4462
5329
|
secrets,
|
|
@@ -4824,7 +5691,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
|
|
|
4824
5691
|
const config = await loadConfig();
|
|
4825
5692
|
const database = openDatabase(config);
|
|
4826
5693
|
try {
|
|
4827
|
-
const raw = await
|
|
5694
|
+
const raw = await fs14.readFile(options.file, "utf8");
|
|
4828
5695
|
const payload = JSON.parse(raw);
|
|
4829
5696
|
const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
|
|
4830
5697
|
if (!result.accepted) {
|