chattercatcher 0.1.18 → 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 +395 -84
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +388 -82
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/docs/superpowers/plans/2026-05-02-agentic-rag.md +0 -1013
- package/docs/superpowers/plans/2026-05-02-image-multimodal-memory.md +0 -1888
- package/docs/superpowers/specs/2026-05-02-agentic-rag-design.md +0 -143
- package/docs/superpowers/specs/2026-05-02-image-multimodal-memory-design.md +0 -160
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs14 from "fs/promises";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "chattercatcher",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.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
|
],
|
|
@@ -478,6 +480,22 @@ function migrateDatabase(database) {
|
|
|
478
480
|
CREATE INDEX IF NOT EXISTS message_chunk_embeddings_model_idx
|
|
479
481
|
ON message_chunk_embeddings(model, dimension);
|
|
480
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
|
+
|
|
481
499
|
CREATE TABLE IF NOT EXISTS file_jobs (
|
|
482
500
|
id TEXT PRIMARY KEY,
|
|
483
501
|
source_path TEXT NOT NULL,
|
|
@@ -2617,6 +2635,191 @@ async function ensureFeishuBotOpenId(config, secrets, options = {}) {
|
|
|
2617
2635
|
// src/feishu/gateway.ts
|
|
2618
2636
|
import * as lark2 from "@larksuiteoapi/node-sdk";
|
|
2619
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
|
+
|
|
2620
2823
|
// src/multimodal/tasks.ts
|
|
2621
2824
|
import crypto4 from "crypto";
|
|
2622
2825
|
function nowIso4() {
|
|
@@ -3102,6 +3305,108 @@ async function askWithAgenticRag(input2) {
|
|
|
3102
3305
|
});
|
|
3103
3306
|
}
|
|
3104
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
|
+
|
|
3105
3410
|
// src/feishu/question.ts
|
|
3106
3411
|
function parseTextContent(content) {
|
|
3107
3412
|
if (!content) {
|
|
@@ -3208,6 +3513,7 @@ var FeishuQuestionHandler = class {
|
|
|
3208
3513
|
return decision;
|
|
3209
3514
|
}
|
|
3210
3515
|
const questionMessageId = payload.event?.message?.message_id;
|
|
3516
|
+
const qaLogs = new QaLogRepository(this.options.database);
|
|
3211
3517
|
await this.acknowledgeQuestion(decision.chatId, questionMessageId);
|
|
3212
3518
|
const { tools, close } = await createAgenticRagSearchTools({
|
|
3213
3519
|
config: this.options.config,
|
|
@@ -3223,6 +3529,16 @@ var FeishuQuestionHandler = class {
|
|
|
3223
3529
|
tools,
|
|
3224
3530
|
model: this.options.model
|
|
3225
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
|
+
});
|
|
3226
3542
|
const citations = formatCitations(result.citations);
|
|
3227
3543
|
const text = citations ? `${result.answer}
|
|
3228
3544
|
|
|
@@ -3231,6 +3547,17 @@ ${citations}` : result.answer;
|
|
|
3231
3547
|
await this.sendResponse(decision.chatId, questionMessageId, text);
|
|
3232
3548
|
} catch (error) {
|
|
3233
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
|
+
});
|
|
3234
3561
|
await this.sendResponse(decision.chatId, questionMessageId, `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`);
|
|
3235
3562
|
}
|
|
3236
3563
|
return decision;
|
|
@@ -3452,15 +3779,29 @@ function createFeishuGateway(options) {
|
|
|
3452
3779
|
episodeProcessor: options.episodeProcessor,
|
|
3453
3780
|
imageMultimodalProcessor: options.imageMultimodalProcessor
|
|
3454
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);
|
|
3455
3793
|
return {
|
|
3456
3794
|
async start() {
|
|
3457
3795
|
try {
|
|
3458
3796
|
await wsClient.start({ eventDispatcher });
|
|
3797
|
+
indexingScheduler?.start();
|
|
3459
3798
|
} catch (error) {
|
|
3799
|
+
indexingScheduler?.stop();
|
|
3460
3800
|
throw formatGatewayStartError(error);
|
|
3461
3801
|
}
|
|
3462
3802
|
},
|
|
3463
3803
|
stop() {
|
|
3804
|
+
indexingScheduler?.stop();
|
|
3464
3805
|
wsClient.close({ force: true });
|
|
3465
3806
|
}
|
|
3466
3807
|
};
|
|
@@ -3532,7 +3873,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
3532
3873
|
};
|
|
3533
3874
|
|
|
3534
3875
|
// src/files/ingest.ts
|
|
3535
|
-
import
|
|
3876
|
+
import crypto6 from "crypto";
|
|
3536
3877
|
import fs11 from "fs/promises";
|
|
3537
3878
|
import path13 from "path";
|
|
3538
3879
|
|
|
@@ -3596,7 +3937,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
3596
3937
|
}
|
|
3597
3938
|
}
|
|
3598
3939
|
function stableStoredName(sourcePath, fileName) {
|
|
3599
|
-
const digest =
|
|
3940
|
+
const digest = crypto6.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
3600
3941
|
return `${digest}-${fileName}`;
|
|
3601
3942
|
}
|
|
3602
3943
|
async function ingestLocalFile(input2) {
|
|
@@ -4118,84 +4459,6 @@ function createMultimodalModel(config, secrets) {
|
|
|
4118
4459
|
});
|
|
4119
4460
|
}
|
|
4120
4461
|
|
|
4121
|
-
// src/rag/indexer.ts
|
|
4122
|
-
async function indexMessageChunks(input2) {
|
|
4123
|
-
const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
|
|
4124
|
-
if (chunks.length === 0) {
|
|
4125
|
-
return { chunks: 0, vectors: 0 };
|
|
4126
|
-
}
|
|
4127
|
-
const vectors = await input2.embedding.embedBatch(chunks.map((chunk) => chunk.text));
|
|
4128
|
-
const records = [];
|
|
4129
|
-
for (const [index2, chunk] of chunks.entries()) {
|
|
4130
|
-
const vector = vectors[index2];
|
|
4131
|
-
if (!vector || vector.length === 0) {
|
|
4132
|
-
continue;
|
|
4133
|
-
}
|
|
4134
|
-
records.push({
|
|
4135
|
-
id: chunk.chunkId,
|
|
4136
|
-
vector,
|
|
4137
|
-
evidence: {
|
|
4138
|
-
id: chunk.chunkId,
|
|
4139
|
-
text: chunk.text,
|
|
4140
|
-
score: 1,
|
|
4141
|
-
source: toEvidenceSource3(chunk)
|
|
4142
|
-
}
|
|
4143
|
-
});
|
|
4144
|
-
}
|
|
4145
|
-
await input2.store.upsert(records);
|
|
4146
|
-
return {
|
|
4147
|
-
chunks: chunks.length,
|
|
4148
|
-
vectors: records.length
|
|
4149
|
-
};
|
|
4150
|
-
}
|
|
4151
|
-
function toEvidenceSource3(chunk) {
|
|
4152
|
-
if (chunk.messageType === "file") {
|
|
4153
|
-
return {
|
|
4154
|
-
type: "file",
|
|
4155
|
-
label: chunk.senderName,
|
|
4156
|
-
timestamp: chunk.sentAt
|
|
4157
|
-
};
|
|
4158
|
-
}
|
|
4159
|
-
return {
|
|
4160
|
-
type: "message",
|
|
4161
|
-
label: chunk.chatName,
|
|
4162
|
-
sender: chunk.senderName,
|
|
4163
|
-
timestamp: chunk.sentAt
|
|
4164
|
-
};
|
|
4165
|
-
}
|
|
4166
|
-
|
|
4167
|
-
// src/rag/manual-index.ts
|
|
4168
|
-
async function processMessagesNow(input2) {
|
|
4169
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4170
|
-
if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
|
|
4171
|
-
return {
|
|
4172
|
-
status: "skipped",
|
|
4173
|
-
reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
|
|
4174
|
-
chunks: 0,
|
|
4175
|
-
vectors: 0,
|
|
4176
|
-
startedAt,
|
|
4177
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4178
|
-
};
|
|
4179
|
-
}
|
|
4180
|
-
const vectorStore = new SqliteVectorStore(input2.database, {
|
|
4181
|
-
model: input2.config.embedding.model
|
|
4182
|
-
});
|
|
4183
|
-
const embedding = input2.embedding ?? createEmbeddingModel(input2.config, input2.secrets);
|
|
4184
|
-
const stats = await indexMessageChunks({
|
|
4185
|
-
messages: new MessageRepository(input2.database),
|
|
4186
|
-
embedding,
|
|
4187
|
-
store: vectorStore,
|
|
4188
|
-
limit: input2.limit
|
|
4189
|
-
});
|
|
4190
|
-
return {
|
|
4191
|
-
status: "completed",
|
|
4192
|
-
chunks: stats.chunks,
|
|
4193
|
-
vectors: stats.vectors,
|
|
4194
|
-
startedAt,
|
|
4195
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4196
|
-
};
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
4462
|
// src/rag/qa-service.ts
|
|
4200
4463
|
async function askWithRag(input2) {
|
|
4201
4464
|
const evidence = await input2.retriever.retrieve(input2.question);
|
|
@@ -4444,6 +4707,10 @@ function buildHtml() {
|
|
|
4444
4707
|
<h2>\u4F1A\u8BDD\u8BB0\u5FC6</h2>
|
|
4445
4708
|
<div id="episodes" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
4446
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>
|
|
4447
4714
|
</div>
|
|
4448
4715
|
<aside>
|
|
4449
4716
|
<section>
|
|
@@ -4474,6 +4741,7 @@ function buildHtml() {
|
|
|
4474
4741
|
const chats = document.querySelector("#chats");
|
|
4475
4742
|
const files = document.querySelector("#files");
|
|
4476
4743
|
const fileJobs = document.querySelector("#file-jobs");
|
|
4744
|
+
const qaLogs = document.querySelector("#qa-logs");
|
|
4477
4745
|
const processMessages = document.querySelector("#process-messages");
|
|
4478
4746
|
const actionStatus = document.querySelector("#action-status");
|
|
4479
4747
|
|
|
@@ -4665,14 +4933,45 @@ function buildHtml() {
|
|
|
4665
4933
|
\`;
|
|
4666
4934
|
}
|
|
4667
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
|
+
|
|
4668
4966
|
async function load() {
|
|
4669
|
-
const [status, recent, episodeList, chatList, fileList, jobList] = await Promise.all([
|
|
4967
|
+
const [status, recent, episodeList, chatList, fileList, jobList, qaLogList] = await Promise.all([
|
|
4670
4968
|
fetch("/api/status").then((response) => response.json()),
|
|
4671
4969
|
fetch("/api/messages/recent?limit=20").then((response) => response.json()),
|
|
4672
4970
|
fetch("/api/episodes?limit=10").then((response) => response.json()),
|
|
4673
4971
|
fetch("/api/chats").then((response) => response.json()),
|
|
4674
4972
|
fetch("/api/files").then((response) => response.json()),
|
|
4675
4973
|
fetch("/api/file-jobs").then((response) => response.json()),
|
|
4974
|
+
fetch("/api/qa-logs?limit=10").then((response) => response.json()),
|
|
4676
4975
|
]);
|
|
4677
4976
|
renderMetrics(status);
|
|
4678
4977
|
renderMessages(recent.items);
|
|
@@ -4680,6 +4979,7 @@ function buildHtml() {
|
|
|
4680
4979
|
renderChats(chatList.items);
|
|
4681
4980
|
renderFiles(fileList.items);
|
|
4682
4981
|
renderFileJobs(jobList.items);
|
|
4982
|
+
renderQaLogs(qaLogList.items);
|
|
4683
4983
|
}
|
|
4684
4984
|
|
|
4685
4985
|
async function processNow() {
|
|
@@ -4727,6 +5027,7 @@ function createWebApp(config) {
|
|
|
4727
5027
|
const messages = new MessageRepository(database);
|
|
4728
5028
|
const episodes = new EpisodeRepository(database);
|
|
4729
5029
|
const fileJobs = new FileJobRepository(database);
|
|
5030
|
+
const qaLogs = new QaLogRepository(database);
|
|
4730
5031
|
app.addHook("onClose", async () => {
|
|
4731
5032
|
database.close();
|
|
4732
5033
|
});
|
|
@@ -4737,7 +5038,8 @@ function createWebApp(config) {
|
|
|
4737
5038
|
chats: messages.getChatCount(),
|
|
4738
5039
|
messages: messages.getMessageCount(),
|
|
4739
5040
|
episodes: episodes.getEpisodeCount(),
|
|
4740
|
-
files: messages.listFiles(1e3).length
|
|
5041
|
+
files: messages.listFiles(1e3).length,
|
|
5042
|
+
qaLogs: qaLogs.getCount()
|
|
4741
5043
|
},
|
|
4742
5044
|
rag: {
|
|
4743
5045
|
mode: "required",
|
|
@@ -4778,6 +5080,12 @@ function createWebApp(config) {
|
|
|
4778
5080
|
items: episodes.listRecentEpisodes(limit)
|
|
4779
5081
|
};
|
|
4780
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
|
+
});
|
|
4781
5089
|
app.post("/api/process/messages", async (_request, reply) => {
|
|
4782
5090
|
try {
|
|
4783
5091
|
return await processMessagesNow({
|
|
@@ -5013,6 +5321,9 @@ async function startGatewayForegroundCommand() {
|
|
|
5013
5321
|
database,
|
|
5014
5322
|
model: createMultimodalModel(config, secrets)
|
|
5015
5323
|
} : void 0,
|
|
5324
|
+
indexingProcessor: {
|
|
5325
|
+
database
|
|
5326
|
+
},
|
|
5016
5327
|
questionHandler: new FeishuQuestionHandler({
|
|
5017
5328
|
config,
|
|
5018
5329
|
secrets,
|