chattercatcher 0.1.5 → 0.1.7
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 +3404 -130
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3293 -80
- package/dist/index.js.map +1 -1
- package/docs/superpowers/plans/2026-04-28-gateway-background-start.md +86 -0
- package/package.json +1 -1
- package/dist/chunk-LYT5TS7P.js +0 -3190
- package/dist/chunk-LYT5TS7P.js.map +0 -1
- package/dist/chunk-OEROFIU2.js +0 -107
- package/dist/chunk-OEROFIU2.js.map +0 -1
- package/dist/chunk-WOBPNFFZ.js +0 -29
- package/dist/chunk-WOBPNFFZ.js.map +0 -1
- package/dist/lancedb-store-QHG4PAI4.js +0 -10
- package/dist/lancedb-store-QHG4PAI4.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,127 +1,3372 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
import
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config/paths.ts
|
|
13
|
+
import os2 from "os";
|
|
14
|
+
import path2 from "path";
|
|
15
|
+
function getChatterCatcherHome() {
|
|
16
|
+
return process.env.CHATTERCATCHER_HOME || path2.join(os2.homedir(), ".chattercatcher");
|
|
17
|
+
}
|
|
18
|
+
function resolveHomePath(value) {
|
|
19
|
+
if (value === "~") {
|
|
20
|
+
return os2.homedir();
|
|
21
|
+
}
|
|
22
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
23
|
+
return path2.join(os2.homedir(), value.slice(2));
|
|
24
|
+
}
|
|
25
|
+
return path2.resolve(value);
|
|
26
|
+
}
|
|
27
|
+
function getConfigPath() {
|
|
28
|
+
return path2.join(getChatterCatcherHome(), "config.json");
|
|
29
|
+
}
|
|
30
|
+
function getSecretsPath() {
|
|
31
|
+
return path2.join(getChatterCatcherHome(), "secrets.json");
|
|
32
|
+
}
|
|
33
|
+
var init_paths = __esm({
|
|
34
|
+
"src/config/paths.ts"() {
|
|
35
|
+
"use strict";
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// src/rag/lancedb-store.ts
|
|
40
|
+
var lancedb_store_exports = {};
|
|
41
|
+
__export(lancedb_store_exports, {
|
|
42
|
+
LanceDbVectorStore: () => LanceDbVectorStore,
|
|
43
|
+
getLanceDbPath: () => getLanceDbPath
|
|
44
|
+
});
|
|
45
|
+
import fs6 from "fs/promises";
|
|
46
|
+
import path9 from "path";
|
|
47
|
+
function getLanceDbPath(config) {
|
|
48
|
+
return path9.join(resolveHomePath(config.storage.dataDir), "vector", "lancedb");
|
|
49
|
+
}
|
|
50
|
+
function toRow(record) {
|
|
51
|
+
return {
|
|
52
|
+
id: record.id,
|
|
53
|
+
vector: record.vector,
|
|
54
|
+
text: record.evidence.text,
|
|
55
|
+
source_json: JSON.stringify(record.evidence.source)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function toLanceData(rows) {
|
|
59
|
+
return rows.map((row) => ({
|
|
60
|
+
id: row.id,
|
|
61
|
+
vector: row.vector,
|
|
62
|
+
text: row.text,
|
|
63
|
+
source_json: row.source_json
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
function escapeSqlString(value) {
|
|
67
|
+
return value.replace(/'/g, "''");
|
|
68
|
+
}
|
|
69
|
+
function toEvidence(row) {
|
|
70
|
+
const distance = row._distance ?? 0;
|
|
71
|
+
const vectorScore = 1 / (1 + Math.max(0, distance));
|
|
72
|
+
return {
|
|
73
|
+
id: row.id,
|
|
74
|
+
text: row.text,
|
|
75
|
+
score: vectorScore,
|
|
76
|
+
vectorScore,
|
|
77
|
+
source: JSON.parse(row.source_json)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var DEFAULT_TABLE_NAME, LanceDbVectorStore;
|
|
81
|
+
var init_lancedb_store = __esm({
|
|
82
|
+
"src/rag/lancedb-store.ts"() {
|
|
83
|
+
"use strict";
|
|
84
|
+
init_paths();
|
|
85
|
+
DEFAULT_TABLE_NAME = "message_chunks";
|
|
86
|
+
LanceDbVectorStore = class _LanceDbVectorStore {
|
|
87
|
+
constructor(connection, tableName) {
|
|
88
|
+
this.connection = connection;
|
|
89
|
+
this.tableName = tableName;
|
|
90
|
+
}
|
|
91
|
+
connection;
|
|
92
|
+
tableName;
|
|
93
|
+
static async connect(uri, tableName = DEFAULT_TABLE_NAME) {
|
|
94
|
+
await fs6.mkdir(uri, { recursive: true });
|
|
95
|
+
const lancedb = await import("@lancedb/lancedb");
|
|
96
|
+
const connection = await lancedb.connect(uri);
|
|
97
|
+
return new _LanceDbVectorStore(connection, tableName);
|
|
98
|
+
}
|
|
99
|
+
static async connectFromConfig(config, tableName = DEFAULT_TABLE_NAME) {
|
|
100
|
+
return _LanceDbVectorStore.connect(getLanceDbPath(config), tableName);
|
|
101
|
+
}
|
|
102
|
+
close() {
|
|
103
|
+
this.connection.close();
|
|
104
|
+
}
|
|
105
|
+
async upsert(records) {
|
|
106
|
+
if (records.length === 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const rows = records.map(toRow);
|
|
110
|
+
const data2 = toLanceData(rows);
|
|
111
|
+
const table = await this.ensureTable(data2);
|
|
112
|
+
const ids = rows.map((row) => `'${escapeSqlString(row.id)}'`).join(", ");
|
|
113
|
+
await table.delete(`id IN (${ids})`);
|
|
114
|
+
await table.add(data2);
|
|
115
|
+
}
|
|
116
|
+
async search(vector, limit) {
|
|
117
|
+
const table = await this.openTableIfExists();
|
|
118
|
+
if (!table) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const rows = await table.vectorSearch(vector).limit(limit).toArray();
|
|
122
|
+
return rows.map(toEvidence);
|
|
123
|
+
}
|
|
124
|
+
async count() {
|
|
125
|
+
const table = await this.openTableIfExists();
|
|
126
|
+
if (!table) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
return table.countRows();
|
|
130
|
+
}
|
|
131
|
+
async ensureTable(initialRows) {
|
|
132
|
+
const table = await this.openTableIfExists();
|
|
133
|
+
if (table) {
|
|
134
|
+
return table;
|
|
135
|
+
}
|
|
136
|
+
return this.connection.createTable(this.tableName, initialRows);
|
|
137
|
+
}
|
|
138
|
+
async openTableIfExists() {
|
|
139
|
+
const tableNames = await this.connection.tableNames();
|
|
140
|
+
if (!tableNames.includes(this.tableName)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return this.connection.openTable(this.tableName);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// src/cli.ts
|
|
150
|
+
import { input, password, select, confirm, number } from "@inquirer/prompts";
|
|
151
|
+
import { Command } from "commander";
|
|
152
|
+
import fs14 from "fs/promises";
|
|
153
|
+
|
|
154
|
+
// src/config/store.ts
|
|
155
|
+
import fs from "fs/promises";
|
|
156
|
+
import path3 from "path";
|
|
157
|
+
|
|
158
|
+
// src/config/schema.ts
|
|
159
|
+
import os from "os";
|
|
160
|
+
import path from "path";
|
|
161
|
+
import { z } from "zod";
|
|
162
|
+
function defaultDataDir() {
|
|
163
|
+
return path.join(process.env.CHATTERCATCHER_HOME || path.join(os.homedir(), ".chattercatcher"), "data");
|
|
164
|
+
}
|
|
165
|
+
var appConfigSchema = z.object({
|
|
166
|
+
feishu: z.object({
|
|
167
|
+
domain: z.enum(["feishu", "lark"]).default("feishu"),
|
|
168
|
+
appId: z.string().default(""),
|
|
169
|
+
groupPolicy: z.enum(["open", "allowlist", "disabled"]).default("open"),
|
|
170
|
+
requireMention: z.boolean().default(true)
|
|
171
|
+
}),
|
|
172
|
+
llm: z.object({
|
|
173
|
+
baseUrl: z.string().url().or(z.literal("")).default(""),
|
|
174
|
+
model: z.string().default("")
|
|
175
|
+
}),
|
|
176
|
+
embedding: z.object({
|
|
177
|
+
baseUrl: z.string().url().or(z.literal("")).default(""),
|
|
178
|
+
model: z.string().default(""),
|
|
179
|
+
dimension: z.number().int().positive().nullable().default(null)
|
|
180
|
+
}),
|
|
181
|
+
storage: z.object({
|
|
182
|
+
dataDir: z.string().default(defaultDataDir)
|
|
183
|
+
}),
|
|
184
|
+
web: z.object({
|
|
185
|
+
host: z.string().default("127.0.0.1"),
|
|
186
|
+
port: z.number().int().min(1).max(65535).default(3878)
|
|
187
|
+
}),
|
|
188
|
+
schedules: z.object({
|
|
189
|
+
indexing: z.string().default("*/10 * * * *")
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
var appSecretsSchema = z.object({
|
|
193
|
+
feishu: z.object({
|
|
194
|
+
appSecret: z.string().default("")
|
|
195
|
+
}),
|
|
196
|
+
llm: z.object({
|
|
197
|
+
apiKey: z.string().default("")
|
|
198
|
+
}),
|
|
199
|
+
embedding: z.object({
|
|
200
|
+
apiKey: z.string().default("")
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
function createDefaultConfig() {
|
|
204
|
+
return appConfigSchema.parse({
|
|
205
|
+
feishu: {},
|
|
206
|
+
llm: {},
|
|
207
|
+
embedding: {},
|
|
208
|
+
storage: {},
|
|
209
|
+
web: {},
|
|
210
|
+
schedules: {}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function createDefaultSecrets() {
|
|
214
|
+
return appSecretsSchema.parse({
|
|
215
|
+
feishu: {},
|
|
216
|
+
llm: {},
|
|
217
|
+
embedding: {}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/config/store.ts
|
|
222
|
+
init_paths();
|
|
223
|
+
async function readJsonFile(filePath, fallback) {
|
|
224
|
+
try {
|
|
225
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
226
|
+
return JSON.parse(raw);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error.code === "ENOENT") {
|
|
229
|
+
return fallback;
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function writeJsonFile(filePath, value) {
|
|
235
|
+
await fs.mkdir(path3.dirname(filePath), { recursive: true });
|
|
236
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}
|
|
237
|
+
`, "utf8");
|
|
238
|
+
}
|
|
239
|
+
async function loadConfig() {
|
|
240
|
+
const raw = await readJsonFile(getConfigPath(), createDefaultConfig());
|
|
241
|
+
return appConfigSchema.parse(raw);
|
|
242
|
+
}
|
|
243
|
+
async function saveConfig(config) {
|
|
244
|
+
await writeJsonFile(getConfigPath(), appConfigSchema.parse(config));
|
|
245
|
+
}
|
|
246
|
+
async function loadSecrets() {
|
|
247
|
+
const raw = await readJsonFile(getSecretsPath(), createDefaultSecrets());
|
|
248
|
+
return appSecretsSchema.parse(raw);
|
|
249
|
+
}
|
|
250
|
+
async function saveSecrets(secrets) {
|
|
251
|
+
await writeJsonFile(getSecretsPath(), appSecretsSchema.parse(secrets));
|
|
252
|
+
}
|
|
253
|
+
async function ensureConfigFiles() {
|
|
254
|
+
await fs.mkdir(getChatterCatcherHome(), { recursive: true });
|
|
255
|
+
const config = await loadConfig();
|
|
256
|
+
const secrets = await loadSecrets();
|
|
257
|
+
await saveConfig(config);
|
|
258
|
+
await saveSecrets(secrets);
|
|
259
|
+
return { config, secrets };
|
|
260
|
+
}
|
|
261
|
+
async function resetConfigFiles() {
|
|
262
|
+
await saveConfig(createDefaultConfig());
|
|
263
|
+
await saveSecrets(createDefaultSecrets());
|
|
264
|
+
}
|
|
265
|
+
function maskSecret(value) {
|
|
266
|
+
if (!value) {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
if (value.length <= 8) {
|
|
270
|
+
return "********";
|
|
271
|
+
}
|
|
272
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/config/update.ts
|
|
276
|
+
function applySecretInput(currentValue, nextValue) {
|
|
277
|
+
const trimmed = nextValue?.trim() ?? "";
|
|
278
|
+
return trimmed ? trimmed : currentValue;
|
|
279
|
+
}
|
|
280
|
+
function resolveEmbeddingApiKey(input2) {
|
|
281
|
+
const explicit = applySecretInput(input2.currentEmbeddingKey, input2.nextEmbeddingKey);
|
|
282
|
+
return explicit || input2.llmApiKey;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/cli.ts
|
|
286
|
+
init_paths();
|
|
287
|
+
|
|
288
|
+
// src/data/deletion.ts
|
|
289
|
+
init_paths();
|
|
290
|
+
import fs2 from "fs/promises";
|
|
291
|
+
import path4 from "path";
|
|
292
|
+
function emptyResult(targetType, targetId) {
|
|
293
|
+
return {
|
|
294
|
+
targetType,
|
|
295
|
+
targetId,
|
|
296
|
+
deletedMessages: 0,
|
|
297
|
+
deletedChunks: 0,
|
|
298
|
+
deletedFileJobs: 0,
|
|
299
|
+
deletedChats: 0,
|
|
300
|
+
deletedStoredFiles: [],
|
|
301
|
+
skippedStoredFiles: []
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function parseStoredPathFromRawPayload(rawPayloadJson) {
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(rawPayloadJson);
|
|
307
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const storedPath = parsed.storedPath;
|
|
311
|
+
return typeof storedPath === "string" ? storedPath : null;
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function isInsideDirectory(filePath, directory) {
|
|
317
|
+
const relative = path4.relative(path4.resolve(directory), path4.resolve(filePath));
|
|
318
|
+
return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
319
|
+
}
|
|
320
|
+
async function removeStoredFiles(config, paths) {
|
|
321
|
+
const dataDir = resolveHomePath(config.storage.dataDir);
|
|
322
|
+
const deleted = [];
|
|
323
|
+
const skipped = [];
|
|
324
|
+
const uniquePaths = [...new Set(paths.filter(Boolean).map((item) => path4.resolve(item)))];
|
|
325
|
+
for (const storedPath of uniquePaths) {
|
|
326
|
+
if (!isInsideDirectory(storedPath, dataDir)) {
|
|
327
|
+
skipped.push(storedPath);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
await fs2.rm(storedPath, { force: true });
|
|
332
|
+
deleted.push(storedPath);
|
|
333
|
+
} catch {
|
|
334
|
+
skipped.push(storedPath);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { deleted, skipped };
|
|
338
|
+
}
|
|
339
|
+
function getStoredPathsForMessages(database, messageIds) {
|
|
340
|
+
if (messageIds.length === 0) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
const rows = database.prepare(
|
|
344
|
+
`
|
|
345
|
+
SELECT raw_payload_json AS rawPayloadJson
|
|
346
|
+
FROM messages
|
|
347
|
+
WHERE id IN (${messageIds.map(() => "?").join(", ")})
|
|
348
|
+
`
|
|
349
|
+
).all(...messageIds);
|
|
350
|
+
const fileJobRows = database.prepare(
|
|
351
|
+
`
|
|
352
|
+
SELECT stored_path AS storedPath
|
|
353
|
+
FROM file_jobs
|
|
354
|
+
WHERE message_id IN (${messageIds.map(() => "?").join(", ")})
|
|
355
|
+
`
|
|
356
|
+
).all(...messageIds);
|
|
357
|
+
return [
|
|
358
|
+
...rows.map((row) => parseStoredPathFromRawPayload(row.rawPayloadJson)).filter((item) => Boolean(item)),
|
|
359
|
+
...fileJobRows.map((row) => row.storedPath).filter((item) => Boolean(item))
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
function deleteMessagesByIds(database, messageIds) {
|
|
363
|
+
if (messageIds.length === 0) {
|
|
364
|
+
return {
|
|
365
|
+
deletedMessages: 0,
|
|
366
|
+
deletedChunks: 0,
|
|
367
|
+
deletedFileJobs: 0
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const placeholders = messageIds.map(() => "?").join(", ");
|
|
371
|
+
const deletedChunks = database.prepare(`SELECT COUNT(*) AS count FROM message_chunks WHERE message_id IN (${placeholders})`).get(...messageIds).count;
|
|
372
|
+
const deletedFileJobs = database.prepare(`DELETE FROM file_jobs WHERE message_id IN (${placeholders})`).run(...messageIds).changes;
|
|
373
|
+
database.prepare(`DELETE FROM message_chunks_fts WHERE message_id IN (${placeholders})`).run(...messageIds);
|
|
374
|
+
const deletedMessages = database.prepare(`DELETE FROM messages WHERE id IN (${placeholders})`).run(...messageIds).changes;
|
|
375
|
+
return {
|
|
376
|
+
deletedMessages,
|
|
377
|
+
deletedChunks,
|
|
378
|
+
deletedFileJobs
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async function deleteLocalData(input2) {
|
|
382
|
+
const result = emptyResult(input2.targetType, input2.targetId);
|
|
383
|
+
let storedPaths = [];
|
|
384
|
+
const transaction = input2.database.transaction(() => {
|
|
385
|
+
if (input2.targetType === "chat") {
|
|
386
|
+
const messageIds = input2.database.prepare("SELECT id FROM messages WHERE chat_id = ?").all(input2.targetId).map((row) => row.id);
|
|
387
|
+
storedPaths = getStoredPathsForMessages(input2.database, messageIds);
|
|
388
|
+
const deleted2 = deleteMessagesByIds(input2.database, messageIds);
|
|
389
|
+
result.deletedMessages = deleted2.deletedMessages;
|
|
390
|
+
result.deletedChunks = deleted2.deletedChunks;
|
|
391
|
+
result.deletedFileJobs = deleted2.deletedFileJobs;
|
|
392
|
+
result.deletedChats = input2.database.prepare("DELETE FROM chats WHERE id = ?").run(input2.targetId).changes;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (input2.targetType === "file") {
|
|
396
|
+
const file = input2.database.prepare("SELECT id FROM messages WHERE id = ? AND message_type = 'file'").get(input2.targetId);
|
|
397
|
+
if (!file) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
storedPaths = getStoredPathsForMessages(input2.database, [input2.targetId]);
|
|
402
|
+
const deleted = deleteMessagesByIds(input2.database, [input2.targetId]);
|
|
403
|
+
result.deletedMessages = deleted.deletedMessages;
|
|
404
|
+
result.deletedChunks = deleted.deletedChunks;
|
|
405
|
+
result.deletedFileJobs = deleted.deletedFileJobs;
|
|
406
|
+
});
|
|
407
|
+
transaction();
|
|
408
|
+
const removed = await removeStoredFiles(input2.config, storedPaths);
|
|
409
|
+
result.deletedStoredFiles = removed.deleted;
|
|
410
|
+
result.skippedStoredFiles = removed.skipped;
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/db/database.ts
|
|
415
|
+
init_paths();
|
|
416
|
+
import Database from "better-sqlite3";
|
|
417
|
+
import fs3 from "fs";
|
|
418
|
+
import path5 from "path";
|
|
419
|
+
function getDatabasePath(config) {
|
|
420
|
+
return path5.join(resolveHomePath(config.storage.dataDir), "chattercatcher.db");
|
|
421
|
+
}
|
|
422
|
+
function openDatabase(config) {
|
|
423
|
+
const databasePath = getDatabasePath(config);
|
|
424
|
+
fs3.mkdirSync(path5.dirname(databasePath), { recursive: true });
|
|
425
|
+
const database = new Database(databasePath);
|
|
426
|
+
database.pragma("journal_mode = WAL");
|
|
427
|
+
database.pragma("foreign_keys = ON");
|
|
428
|
+
migrateDatabase(database);
|
|
429
|
+
return database;
|
|
430
|
+
}
|
|
431
|
+
function migrateDatabase(database) {
|
|
432
|
+
database.exec(`
|
|
433
|
+
CREATE TABLE IF NOT EXISTS chats (
|
|
434
|
+
id TEXT PRIMARY KEY,
|
|
435
|
+
platform TEXT NOT NULL,
|
|
436
|
+
platform_chat_id TEXT NOT NULL,
|
|
437
|
+
name TEXT NOT NULL,
|
|
438
|
+
created_at TEXT NOT NULL,
|
|
439
|
+
updated_at TEXT NOT NULL,
|
|
440
|
+
UNIQUE(platform, platform_chat_id)
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
444
|
+
id TEXT PRIMARY KEY,
|
|
445
|
+
platform TEXT NOT NULL,
|
|
446
|
+
platform_message_id TEXT NOT NULL,
|
|
447
|
+
chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
448
|
+
sender_id TEXT NOT NULL,
|
|
449
|
+
sender_name TEXT NOT NULL,
|
|
450
|
+
message_type TEXT NOT NULL,
|
|
451
|
+
text TEXT NOT NULL,
|
|
452
|
+
raw_payload_json TEXT NOT NULL,
|
|
453
|
+
sent_at TEXT NOT NULL,
|
|
454
|
+
received_at TEXT NOT NULL,
|
|
455
|
+
created_at TEXT NOT NULL,
|
|
456
|
+
UNIQUE(platform, platform_message_id)
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
CREATE TABLE IF NOT EXISTS message_chunks (
|
|
460
|
+
id TEXT PRIMARY KEY,
|
|
461
|
+
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
462
|
+
chunk_index INTEGER NOT NULL,
|
|
463
|
+
text TEXT NOT NULL,
|
|
464
|
+
metadata_json TEXT NOT NULL,
|
|
465
|
+
created_at TEXT NOT NULL,
|
|
466
|
+
UNIQUE(message_id, chunk_index)
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS message_chunks_fts USING fts5(
|
|
470
|
+
text,
|
|
471
|
+
chunk_id UNINDEXED,
|
|
472
|
+
message_id UNINDEXED,
|
|
473
|
+
tokenize = 'unicode61'
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
CREATE TABLE IF NOT EXISTS file_jobs (
|
|
477
|
+
id TEXT PRIMARY KEY,
|
|
478
|
+
source_path TEXT NOT NULL,
|
|
479
|
+
stored_path TEXT,
|
|
480
|
+
file_name TEXT NOT NULL,
|
|
481
|
+
status TEXT NOT NULL,
|
|
482
|
+
parser TEXT,
|
|
483
|
+
message_id TEXT,
|
|
484
|
+
bytes INTEGER,
|
|
485
|
+
characters INTEGER,
|
|
486
|
+
warnings_json TEXT NOT NULL DEFAULT '[]',
|
|
487
|
+
error TEXT,
|
|
488
|
+
created_at TEXT NOT NULL,
|
|
489
|
+
updated_at TEXT NOT NULL
|
|
490
|
+
);
|
|
491
|
+
`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/doctor/checks.ts
|
|
495
|
+
init_paths();
|
|
496
|
+
import fs7 from "fs/promises";
|
|
497
|
+
|
|
498
|
+
// src/files/jobs.ts
|
|
499
|
+
import crypto from "crypto";
|
|
500
|
+
import path6 from "path";
|
|
501
|
+
function nowIso() {
|
|
502
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
503
|
+
}
|
|
504
|
+
function stableJobId(sourcePath) {
|
|
505
|
+
return crypto.createHash("sha256").update(path6.resolve(sourcePath)).digest("hex").slice(0, 32);
|
|
506
|
+
}
|
|
507
|
+
function parseWarnings(value) {
|
|
508
|
+
try {
|
|
509
|
+
const parsed = JSON.parse(value);
|
|
510
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
|
|
511
|
+
} catch {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
var FileJobRepository = class {
|
|
516
|
+
constructor(database) {
|
|
517
|
+
this.database = database;
|
|
518
|
+
}
|
|
519
|
+
database;
|
|
520
|
+
start(input2) {
|
|
521
|
+
const id = stableJobId(input2.sourcePath);
|
|
522
|
+
const now = nowIso();
|
|
523
|
+
this.database.prepare(
|
|
524
|
+
`
|
|
525
|
+
INSERT INTO file_jobs (
|
|
526
|
+
id, source_path, file_name, status, warnings_json, created_at, updated_at
|
|
527
|
+
)
|
|
528
|
+
VALUES (@id, @sourcePath, @fileName, 'processing', '[]', @createdAt, @updatedAt)
|
|
529
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
530
|
+
source_path = excluded.source_path,
|
|
531
|
+
file_name = excluded.file_name,
|
|
532
|
+
status = 'processing',
|
|
533
|
+
parser = NULL,
|
|
534
|
+
message_id = NULL,
|
|
535
|
+
bytes = NULL,
|
|
536
|
+
characters = NULL,
|
|
537
|
+
warnings_json = '[]',
|
|
538
|
+
error = NULL,
|
|
539
|
+
updated_at = excluded.updated_at
|
|
540
|
+
`
|
|
541
|
+
).run({
|
|
542
|
+
id,
|
|
543
|
+
sourcePath: path6.resolve(input2.sourcePath),
|
|
544
|
+
fileName: input2.fileName ?? path6.basename(input2.sourcePath),
|
|
545
|
+
createdAt: now,
|
|
546
|
+
updatedAt: now
|
|
547
|
+
});
|
|
548
|
+
return id;
|
|
549
|
+
}
|
|
550
|
+
complete(input2) {
|
|
551
|
+
this.database.prepare(
|
|
552
|
+
`
|
|
553
|
+
UPDATE file_jobs
|
|
554
|
+
SET
|
|
555
|
+
stored_path = @storedPath,
|
|
556
|
+
status = 'indexed',
|
|
557
|
+
parser = @parser,
|
|
558
|
+
message_id = @messageId,
|
|
559
|
+
bytes = @bytes,
|
|
560
|
+
characters = @characters,
|
|
561
|
+
warnings_json = @warningsJson,
|
|
562
|
+
error = NULL,
|
|
563
|
+
updated_at = @updatedAt
|
|
564
|
+
WHERE id = @id
|
|
565
|
+
`
|
|
566
|
+
).run({
|
|
567
|
+
id: input2.id,
|
|
568
|
+
storedPath: input2.storedPath,
|
|
569
|
+
parser: input2.parser,
|
|
570
|
+
messageId: input2.messageId,
|
|
571
|
+
bytes: input2.bytes,
|
|
572
|
+
characters: input2.characters,
|
|
573
|
+
warningsJson: JSON.stringify(input2.warnings),
|
|
574
|
+
updatedAt: nowIso()
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
fail(input2) {
|
|
578
|
+
this.database.prepare(
|
|
579
|
+
`
|
|
580
|
+
UPDATE file_jobs
|
|
581
|
+
SET status = 'failed', error = @error, updated_at = @updatedAt
|
|
582
|
+
WHERE id = @id
|
|
583
|
+
`
|
|
584
|
+
).run({
|
|
585
|
+
id: input2.id,
|
|
586
|
+
error: input2.error,
|
|
587
|
+
updatedAt: nowIso()
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
get(id) {
|
|
591
|
+
return this.listByWhere("WHERE id = ?", [id], 1)[0] ?? null;
|
|
592
|
+
}
|
|
593
|
+
list(limit = 50, options = {}) {
|
|
594
|
+
return options.status ? this.listByWhere("WHERE status = ?", [options.status], limit) : this.listByWhere("", [], limit);
|
|
595
|
+
}
|
|
596
|
+
listByWhere(whereSql, params, limit) {
|
|
597
|
+
const rows = this.database.prepare(
|
|
598
|
+
`
|
|
599
|
+
SELECT
|
|
600
|
+
id,
|
|
601
|
+
source_path AS sourcePath,
|
|
602
|
+
stored_path AS storedPath,
|
|
603
|
+
file_name AS fileName,
|
|
604
|
+
status,
|
|
605
|
+
parser,
|
|
606
|
+
message_id AS messageId,
|
|
607
|
+
bytes,
|
|
608
|
+
characters,
|
|
609
|
+
warnings_json AS warningsJson,
|
|
610
|
+
error,
|
|
611
|
+
created_at AS createdAt,
|
|
612
|
+
updated_at AS updatedAt
|
|
613
|
+
FROM file_jobs
|
|
614
|
+
${whereSql}
|
|
615
|
+
ORDER BY updated_at DESC
|
|
616
|
+
LIMIT ?
|
|
617
|
+
`
|
|
618
|
+
).all(...params, limit);
|
|
619
|
+
return rows.map((row) => ({
|
|
620
|
+
id: row.id,
|
|
621
|
+
sourcePath: row.sourcePath,
|
|
622
|
+
storedPath: row.storedPath ?? void 0,
|
|
623
|
+
fileName: row.fileName,
|
|
624
|
+
status: row.status,
|
|
625
|
+
parser: row.parser ?? void 0,
|
|
626
|
+
messageId: row.messageId ?? void 0,
|
|
627
|
+
bytes: row.bytes ?? void 0,
|
|
628
|
+
characters: row.characters ?? void 0,
|
|
629
|
+
warnings: parseWarnings(row.warningsJson),
|
|
630
|
+
error: row.error ?? void 0,
|
|
631
|
+
createdAt: row.createdAt,
|
|
632
|
+
updatedAt: row.updatedAt
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/gateway/runtime.ts
|
|
638
|
+
init_paths();
|
|
639
|
+
import fs5 from "fs";
|
|
640
|
+
import path8 from "path";
|
|
641
|
+
|
|
642
|
+
// src/logs/reader.ts
|
|
643
|
+
init_paths();
|
|
644
|
+
import fs4 from "fs/promises";
|
|
645
|
+
import { watch } from "fs";
|
|
646
|
+
import path7 from "path";
|
|
647
|
+
function getLogsDirectory() {
|
|
648
|
+
return path7.join(getChatterCatcherHome(), "logs");
|
|
649
|
+
}
|
|
650
|
+
function resolveLogPath(fileName, logsDir = getLogsDirectory()) {
|
|
651
|
+
return path7.isAbsolute(fileName) ? fileName : path7.join(logsDir, fileName);
|
|
652
|
+
}
|
|
653
|
+
function normalizeLineCount(value, fallback = 200) {
|
|
654
|
+
const parsed = Number(value ?? fallback);
|
|
655
|
+
return Number.isFinite(parsed) ? Math.min(Math.max(Math.trunc(parsed), 1), 1e4) : fallback;
|
|
656
|
+
}
|
|
657
|
+
async function listLogFiles(logsDir = getLogsDirectory()) {
|
|
658
|
+
let entries;
|
|
659
|
+
try {
|
|
660
|
+
entries = await fs4.readdir(logsDir, { withFileTypes: true });
|
|
661
|
+
} catch (error) {
|
|
662
|
+
if (error.code === "ENOENT") {
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
const files2 = await Promise.all(
|
|
668
|
+
entries.filter((entry) => entry.isFile() && entry.name.endsWith(".log")).map(async (entry) => {
|
|
669
|
+
const filePath = path7.join(logsDir, entry.name);
|
|
670
|
+
const stats = await fs4.stat(filePath);
|
|
671
|
+
return {
|
|
672
|
+
name: entry.name,
|
|
673
|
+
path: filePath,
|
|
674
|
+
updatedAt: stats.mtime,
|
|
675
|
+
bytes: stats.size
|
|
676
|
+
};
|
|
677
|
+
})
|
|
678
|
+
);
|
|
679
|
+
return files2.sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime());
|
|
680
|
+
}
|
|
681
|
+
function tailLines(content, lines) {
|
|
682
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
683
|
+
const parts = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
|
|
684
|
+
return parts.slice(-lines).join("\n");
|
|
685
|
+
}
|
|
686
|
+
async function readLogTail(input2) {
|
|
687
|
+
const stats = await fs4.stat(input2.filePath);
|
|
688
|
+
const content = await fs4.readFile(input2.filePath, "utf8");
|
|
689
|
+
return {
|
|
690
|
+
file: {
|
|
691
|
+
name: path7.basename(input2.filePath),
|
|
692
|
+
path: input2.filePath,
|
|
693
|
+
updatedAt: stats.mtime,
|
|
694
|
+
bytes: stats.size
|
|
695
|
+
},
|
|
696
|
+
content: tailLines(content, normalizeLineCount(input2.lines))
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function readLatestLogTail(input2 = {}) {
|
|
700
|
+
if (input2.fileName) {
|
|
701
|
+
return readLogTail({
|
|
702
|
+
filePath: resolveLogPath(input2.fileName, input2.logsDir),
|
|
703
|
+
lines: input2.lines
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
const [latest] = await listLogFiles(input2.logsDir);
|
|
707
|
+
if (!latest) {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
return readLogTail({ filePath: latest.path, lines: input2.lines });
|
|
711
|
+
}
|
|
712
|
+
async function followLogFile(input2) {
|
|
713
|
+
let offset = (await fs4.stat(input2.filePath)).size;
|
|
714
|
+
const directory = path7.dirname(input2.filePath);
|
|
715
|
+
const fileName = path7.basename(input2.filePath);
|
|
716
|
+
async function readAppended() {
|
|
717
|
+
const stats = await fs4.stat(input2.filePath);
|
|
718
|
+
if (stats.size < offset) {
|
|
719
|
+
offset = 0;
|
|
720
|
+
}
|
|
721
|
+
if (stats.size === offset) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const handle = await fs4.open(input2.filePath, "r");
|
|
725
|
+
try {
|
|
726
|
+
const length = stats.size - offset;
|
|
727
|
+
const buffer = Buffer.alloc(length);
|
|
728
|
+
await handle.read(buffer, 0, length, offset);
|
|
729
|
+
offset = stats.size;
|
|
730
|
+
input2.onChunk(buffer.toString("utf8"));
|
|
731
|
+
} finally {
|
|
732
|
+
await handle.close();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const watcher = watch(directory, (eventType, changedFileName) => {
|
|
736
|
+
if (eventType !== "change" || changedFileName?.toString() !== fileName) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
void readAppended().catch((error) => {
|
|
740
|
+
input2.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
return () => watcher.close();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/gateway/runtime.ts
|
|
747
|
+
function getGatewayPidPath() {
|
|
748
|
+
return path8.join(getChatterCatcherHome(), "gateway.pid");
|
|
749
|
+
}
|
|
750
|
+
function getGatewayLogPath() {
|
|
751
|
+
return path8.join(getLogsDirectory(), "gateway.log");
|
|
752
|
+
}
|
|
753
|
+
function isProcessRunning(pid) {
|
|
754
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
process.kill(pid, 0);
|
|
759
|
+
return true;
|
|
760
|
+
} catch {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function readGatewayPidRecord(pidFile = getGatewayPidPath()) {
|
|
765
|
+
try {
|
|
766
|
+
const raw = fs5.readFileSync(pidFile, "utf8");
|
|
767
|
+
const parsed = JSON.parse(raw);
|
|
768
|
+
if (!Number.isInteger(parsed.pid) || typeof parsed.startedAt !== "string" || typeof parsed.command !== "string") {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const pid = parsed.pid;
|
|
772
|
+
if (pid === void 0) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
pid,
|
|
777
|
+
startedAt: parsed.startedAt,
|
|
778
|
+
command: parsed.command,
|
|
779
|
+
...typeof parsed.logFile === "string" ? { logFile: parsed.logFile } : {},
|
|
780
|
+
...parsed.mode === "gateway" || parsed.mode === "web" ? { mode: parsed.mode } : {}
|
|
781
|
+
};
|
|
782
|
+
} catch (error) {
|
|
783
|
+
if (error.code === "ENOENT") {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function writeGatewayPidRecord(pidFile = getGatewayPidPath(), record = {
|
|
790
|
+
pid: process.pid,
|
|
791
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
792
|
+
command: process.argv.join(" ")
|
|
793
|
+
}) {
|
|
794
|
+
fs5.mkdirSync(path8.dirname(pidFile), { recursive: true });
|
|
795
|
+
fs5.writeFileSync(pidFile, `${JSON.stringify(record, null, 2)}
|
|
796
|
+
`, "utf8");
|
|
797
|
+
}
|
|
798
|
+
function removeGatewayPidRecord(pidFile = getGatewayPidPath()) {
|
|
799
|
+
try {
|
|
800
|
+
fs5.rmSync(pidFile, { force: true });
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function getGatewayRuntimeState(pidFile = getGatewayPidPath()) {
|
|
805
|
+
const record = readGatewayPidRecord(pidFile);
|
|
806
|
+
const running = record ? isProcessRunning(record.pid) : false;
|
|
807
|
+
return {
|
|
808
|
+
pidFile,
|
|
809
|
+
record,
|
|
810
|
+
running,
|
|
811
|
+
stale: Boolean(record && !running)
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function stopGatewayProcess(pidFile = getGatewayPidPath()) {
|
|
815
|
+
const state = getGatewayRuntimeState(pidFile);
|
|
816
|
+
if (!state.record) {
|
|
817
|
+
return {
|
|
818
|
+
stopped: false,
|
|
819
|
+
message: "Gateway \u6CA1\u6709\u8FD0\u884C\u8BB0\u5F55\u3002"
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (state.stale) {
|
|
823
|
+
removeGatewayPidRecord(pidFile);
|
|
824
|
+
return {
|
|
825
|
+
stopped: false,
|
|
826
|
+
message: `Gateway PID \u6587\u4EF6\u5DF2\u8FC7\u671F\uFF0C\u5DF2\u6E05\u7406\uFF1A${state.record.pid}`
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
if (state.record.pid === process.pid) {
|
|
830
|
+
return {
|
|
831
|
+
stopped: false,
|
|
832
|
+
message: "\u62D2\u7EDD\u505C\u6B62\u5F53\u524D CLI \u8FDB\u7A0B\uFF1B\u8BF7\u5728\u53E6\u4E00\u4E2A\u7EC8\u7AEF\u8FD0\u884C stop\uFF0C\u6216\u6309 Ctrl+C\u3002"
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
process.kill(state.record.pid, "SIGTERM");
|
|
837
|
+
removeGatewayPidRecord(pidFile);
|
|
838
|
+
return {
|
|
839
|
+
stopped: true,
|
|
840
|
+
message: `\u5DF2\u5411 Gateway \u8FDB\u7A0B\u53D1\u9001\u505C\u6B62\u4FE1\u53F7\uFF1Apid=${state.record.pid}`
|
|
841
|
+
};
|
|
842
|
+
} catch (error) {
|
|
843
|
+
return {
|
|
844
|
+
stopped: false,
|
|
845
|
+
message: `\u505C\u6B62 Gateway \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/gateway/index.ts
|
|
851
|
+
function getGatewayStatus(config, secrets) {
|
|
852
|
+
const runtime = getGatewayRuntimeState();
|
|
853
|
+
const configured = Boolean(config.feishu.appId && (!secrets || secrets.feishu.appSecret));
|
|
854
|
+
if (runtime.running && runtime.record) {
|
|
855
|
+
if (runtime.record.mode === "web" && !configured) {
|
|
856
|
+
return {
|
|
857
|
+
configured,
|
|
858
|
+
connection: "running",
|
|
859
|
+
message: `\u672C\u5730 Web UI \u8FDB\u7A0B\u6B63\u5728\u8FD0\u884C\uFF1Apid=${runtime.record.pid}\uFF0CstartedAt=${runtime.record.startedAt}\uFF1B\u98DE\u4E66\u914D\u7F6E\u5C1A\u672A\u5B8C\u6210\u3002`,
|
|
860
|
+
pid: runtime.record.pid,
|
|
861
|
+
pidFile: runtime.pidFile,
|
|
862
|
+
logFile: runtime.record.logFile
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
configured: true,
|
|
867
|
+
connection: "running",
|
|
868
|
+
message: `\u98DE\u4E66 Gateway \u6B63\u5728\u8FD0\u884C\uFF1Apid=${runtime.record.pid}\uFF0CstartedAt=${runtime.record.startedAt}`,
|
|
869
|
+
pid: runtime.record.pid,
|
|
870
|
+
pidFile: runtime.pidFile,
|
|
871
|
+
logFile: runtime.record.logFile
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
if (!config.feishu.appId) {
|
|
875
|
+
return {
|
|
876
|
+
configured: false,
|
|
877
|
+
connection: "not_configured",
|
|
878
|
+
message: "\u5C1A\u672A\u914D\u7F6E\u98DE\u4E66 App ID\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002"
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
if (secrets && !secrets.feishu.appSecret) {
|
|
882
|
+
return {
|
|
883
|
+
configured: false,
|
|
884
|
+
connection: "not_configured",
|
|
885
|
+
message: "\u5C1A\u672A\u914D\u7F6E\u98DE\u4E66 App Secret\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002"
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (runtime.stale && runtime.record) {
|
|
889
|
+
return {
|
|
890
|
+
configured: true,
|
|
891
|
+
connection: "ready_for_start",
|
|
892
|
+
message: `\u98DE\u4E66\u957F\u8FDE\u63A5\u914D\u7F6E\u5DF2\u5C31\u7EEA\uFF1B\u53D1\u73B0\u8FC7\u671F PID \u6587\u4EF6\uFF1Apid=${runtime.record.pid}\u3002\u8FD0\u884C chattercatcher gateway start \u4F1A\u8986\u76D6\u8FD0\u884C\u8BB0\u5F55\u3002`,
|
|
893
|
+
pid: runtime.record.pid,
|
|
894
|
+
pidFile: runtime.pidFile,
|
|
895
|
+
logFile: runtime.record.logFile
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
configured: true,
|
|
900
|
+
connection: "ready_for_start",
|
|
901
|
+
message: "\u98DE\u4E66\u957F\u8FDE\u63A5\u914D\u7F6E\u5DF2\u5C31\u7EEA\u3002\u8FD0\u884C chattercatcher gateway start \u540E\u4F1A\u63A5\u6536 im.message.receive_v1 \u4E8B\u4EF6\u3002",
|
|
902
|
+
pidFile: runtime.pidFile
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/llm/openai-compatible.ts
|
|
907
|
+
function normalizeBaseUrl(baseUrl) {
|
|
908
|
+
return baseUrl.replace(/\/+$/, "");
|
|
909
|
+
}
|
|
910
|
+
var OpenAICompatibleChatModel = class {
|
|
911
|
+
constructor(options) {
|
|
912
|
+
this.options = options;
|
|
913
|
+
}
|
|
914
|
+
options;
|
|
915
|
+
async complete(messages) {
|
|
916
|
+
if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
|
|
917
|
+
throw new Error("LLM \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
|
|
918
|
+
}
|
|
919
|
+
const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/chat/completions`, {
|
|
920
|
+
method: "POST",
|
|
921
|
+
headers: {
|
|
922
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
923
|
+
"content-type": "application/json"
|
|
924
|
+
},
|
|
925
|
+
body: JSON.stringify({
|
|
926
|
+
model: this.options.model,
|
|
927
|
+
messages,
|
|
928
|
+
temperature: this.options.temperature ?? 0.2
|
|
929
|
+
})
|
|
930
|
+
});
|
|
931
|
+
if (!response.ok) {
|
|
932
|
+
const body = await response.text();
|
|
933
|
+
throw new Error(`LLM \u8BF7\u6C42\u5931\u8D25\uFF1A${response.status} ${body}`);
|
|
934
|
+
}
|
|
935
|
+
const data2 = await response.json();
|
|
936
|
+
const content = data2.choices?.[0]?.message?.content?.trim();
|
|
937
|
+
if (!content) {
|
|
938
|
+
throw new Error("LLM \u8FD4\u56DE\u4E3A\u7A7A\u3002");
|
|
939
|
+
}
|
|
940
|
+
return content;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
var OpenAICompatibleEmbeddingModel = class {
|
|
944
|
+
constructor(options) {
|
|
945
|
+
this.options = options;
|
|
946
|
+
}
|
|
947
|
+
options;
|
|
948
|
+
async embed(text) {
|
|
949
|
+
const [vector] = await this.embedBatch([text]);
|
|
950
|
+
return vector ?? [];
|
|
951
|
+
}
|
|
952
|
+
async embedBatch(texts) {
|
|
953
|
+
if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
|
|
954
|
+
throw new Error("Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
|
|
955
|
+
}
|
|
956
|
+
const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/embeddings`, {
|
|
957
|
+
method: "POST",
|
|
958
|
+
headers: {
|
|
959
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
960
|
+
"content-type": "application/json"
|
|
961
|
+
},
|
|
962
|
+
body: JSON.stringify({
|
|
963
|
+
model: this.options.model,
|
|
964
|
+
input: texts
|
|
965
|
+
})
|
|
966
|
+
});
|
|
967
|
+
if (!response.ok) {
|
|
968
|
+
const body = await response.text();
|
|
969
|
+
throw new Error(`Embedding \u8BF7\u6C42\u5931\u8D25\uFF1A${response.status} ${body}`);
|
|
970
|
+
}
|
|
971
|
+
const data2 = await response.json();
|
|
972
|
+
return data2.data?.map((item) => item.embedding ?? []) ?? [];
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
function createChatModel(config, secrets) {
|
|
976
|
+
return new OpenAICompatibleChatModel({
|
|
977
|
+
baseUrl: config.llm.baseUrl,
|
|
978
|
+
apiKey: secrets.llm.apiKey,
|
|
979
|
+
model: config.llm.model
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function createEmbeddingModel(config, secrets) {
|
|
983
|
+
return new OpenAICompatibleEmbeddingModel({
|
|
984
|
+
baseUrl: config.embedding.baseUrl || config.llm.baseUrl,
|
|
985
|
+
apiKey: secrets.embedding.apiKey || secrets.llm.apiKey,
|
|
986
|
+
model: config.embedding.model
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/messages/repository.ts
|
|
991
|
+
import crypto2 from "crypto";
|
|
992
|
+
|
|
993
|
+
// src/messages/chunker.ts
|
|
994
|
+
function chunkText(text, maxChars = 900, overlapChars = 120) {
|
|
995
|
+
const normalized = text.trim().replace(/\s+/g, " ");
|
|
996
|
+
if (!normalized) {
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
if (normalized.length <= maxChars) {
|
|
1000
|
+
return [{ index: 0, text: normalized }];
|
|
1001
|
+
}
|
|
1002
|
+
const chunks = [];
|
|
1003
|
+
let cursor = 0;
|
|
1004
|
+
while (cursor < normalized.length) {
|
|
1005
|
+
const end = Math.min(cursor + maxChars, normalized.length);
|
|
1006
|
+
chunks.push({ index: chunks.length, text: normalized.slice(cursor, end) });
|
|
1007
|
+
if (end === normalized.length) {
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
cursor = Math.max(end - overlapChars, cursor + 1);
|
|
1011
|
+
}
|
|
1012
|
+
return chunks;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/messages/repository.ts
|
|
1016
|
+
function nowIso2() {
|
|
1017
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1018
|
+
}
|
|
1019
|
+
function stableId(parts) {
|
|
1020
|
+
return crypto2.createHash("sha256").update(parts.join("")).digest("hex").slice(0, 32);
|
|
1021
|
+
}
|
|
1022
|
+
function escapeFtsQuery(query) {
|
|
1023
|
+
const terms = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter(Boolean);
|
|
1024
|
+
if (terms.length === 0) {
|
|
1025
|
+
return '""';
|
|
1026
|
+
}
|
|
1027
|
+
return terms.map((term) => `"${term}"`).join(" OR ");
|
|
1028
|
+
}
|
|
1029
|
+
function escapeLikeTerm(term) {
|
|
1030
|
+
return term.replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
1031
|
+
}
|
|
1032
|
+
function buildSearchTerms(query) {
|
|
1033
|
+
const trimmed = query.trim();
|
|
1034
|
+
if (!trimmed) {
|
|
1035
|
+
return [];
|
|
1036
|
+
}
|
|
1037
|
+
const terms = trimmed.split(/\s+/).filter(Boolean);
|
|
1038
|
+
if (terms.length > 1) {
|
|
1039
|
+
return terms;
|
|
1040
|
+
}
|
|
1041
|
+
if (/[\u3400-\u9fff]/.test(trimmed) && trimmed.length > 2) {
|
|
1042
|
+
const cjkTerms = /* @__PURE__ */ new Set([trimmed]);
|
|
1043
|
+
for (let index2 = 0; index2 < trimmed.length - 1; index2 += 1) {
|
|
1044
|
+
cjkTerms.add(trimmed.slice(index2, index2 + 2));
|
|
1045
|
+
}
|
|
1046
|
+
return [...cjkTerms];
|
|
1047
|
+
}
|
|
1048
|
+
return [trimmed];
|
|
1049
|
+
}
|
|
1050
|
+
function parseRawPayload(value) {
|
|
1051
|
+
try {
|
|
1052
|
+
const parsed = JSON.parse(value);
|
|
1053
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1054
|
+
} catch {
|
|
1055
|
+
return {};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
var MessageRepository = class {
|
|
1059
|
+
constructor(database) {
|
|
1060
|
+
this.database = database;
|
|
1061
|
+
}
|
|
1062
|
+
database;
|
|
1063
|
+
ingest(input2) {
|
|
1064
|
+
const createdAt = nowIso2();
|
|
1065
|
+
const chatId = stableId([input2.platform, input2.platformChatId]);
|
|
1066
|
+
const messageId = stableId([input2.platform, input2.platformMessageId]);
|
|
1067
|
+
const rawPayloadJson = JSON.stringify(input2.rawPayload ?? {});
|
|
1068
|
+
const chunks = chunkText(input2.text);
|
|
1069
|
+
const transaction = this.database.transaction(() => {
|
|
1070
|
+
this.database.prepare(
|
|
1071
|
+
`
|
|
1072
|
+
INSERT INTO chats (id, platform, platform_chat_id, name, created_at, updated_at)
|
|
1073
|
+
VALUES (@id, @platform, @platformChatId, @name, @createdAt, @updatedAt)
|
|
1074
|
+
ON CONFLICT(platform, platform_chat_id)
|
|
1075
|
+
DO UPDATE SET name = excluded.name, updated_at = excluded.updated_at
|
|
1076
|
+
`
|
|
1077
|
+
).run({
|
|
1078
|
+
id: chatId,
|
|
1079
|
+
platform: input2.platform,
|
|
1080
|
+
platformChatId: input2.platformChatId,
|
|
1081
|
+
name: input2.chatName,
|
|
1082
|
+
createdAt,
|
|
1083
|
+
updatedAt: createdAt
|
|
1084
|
+
});
|
|
1085
|
+
this.database.prepare(
|
|
1086
|
+
`
|
|
1087
|
+
INSERT INTO messages (
|
|
1088
|
+
id, platform, platform_message_id, chat_id, sender_id, sender_name,
|
|
1089
|
+
message_type, text, raw_payload_json, sent_at, received_at, created_at
|
|
1090
|
+
)
|
|
1091
|
+
VALUES (
|
|
1092
|
+
@id, @platform, @platformMessageId, @chatId, @senderId, @senderName,
|
|
1093
|
+
@messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1094
|
+
)
|
|
1095
|
+
ON CONFLICT(platform, platform_message_id)
|
|
1096
|
+
DO UPDATE SET
|
|
1097
|
+
text = excluded.text,
|
|
1098
|
+
raw_payload_json = excluded.raw_payload_json,
|
|
1099
|
+
received_at = excluded.received_at
|
|
1100
|
+
`
|
|
1101
|
+
).run({
|
|
1102
|
+
id: messageId,
|
|
1103
|
+
platform: input2.platform,
|
|
1104
|
+
platformMessageId: input2.platformMessageId,
|
|
1105
|
+
chatId,
|
|
1106
|
+
senderId: input2.senderId,
|
|
1107
|
+
senderName: input2.senderName,
|
|
1108
|
+
messageType: input2.messageType,
|
|
1109
|
+
text: input2.text,
|
|
1110
|
+
rawPayloadJson,
|
|
1111
|
+
sentAt: input2.sentAt,
|
|
1112
|
+
receivedAt: createdAt,
|
|
1113
|
+
createdAt
|
|
1114
|
+
});
|
|
1115
|
+
this.database.prepare("DELETE FROM message_chunks_fts WHERE message_id = ?").run(messageId);
|
|
1116
|
+
this.database.prepare("DELETE FROM message_chunks WHERE message_id = ?").run(messageId);
|
|
1117
|
+
const insertChunk = this.database.prepare(`
|
|
1118
|
+
INSERT INTO message_chunks (id, message_id, chunk_index, text, metadata_json, created_at)
|
|
1119
|
+
VALUES (@id, @messageId, @chunkIndex, @text, @metadataJson, @createdAt)
|
|
1120
|
+
`);
|
|
1121
|
+
const insertFts = this.database.prepare(`
|
|
1122
|
+
INSERT INTO message_chunks_fts (text, chunk_id, message_id)
|
|
1123
|
+
VALUES (@text, @chunkId, @messageId)
|
|
1124
|
+
`);
|
|
1125
|
+
for (const chunk of chunks) {
|
|
1126
|
+
const chunkId = stableId([messageId, String(chunk.index)]);
|
|
1127
|
+
insertChunk.run({
|
|
1128
|
+
id: chunkId,
|
|
1129
|
+
messageId,
|
|
1130
|
+
chunkIndex: chunk.index,
|
|
1131
|
+
text: chunk.text,
|
|
1132
|
+
metadataJson: JSON.stringify({ sourceType: "message" }),
|
|
1133
|
+
createdAt
|
|
1134
|
+
});
|
|
1135
|
+
insertFts.run({ text: chunk.text, chunkId, messageId });
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
transaction();
|
|
1139
|
+
return messageId;
|
|
1140
|
+
}
|
|
1141
|
+
listRecentMessages(limit = 20) {
|
|
1142
|
+
return this.database.prepare(
|
|
1143
|
+
`
|
|
1144
|
+
SELECT
|
|
1145
|
+
mc.id AS chunkId,
|
|
1146
|
+
m.id AS messageId,
|
|
1147
|
+
m.platform AS platform,
|
|
1148
|
+
mc.text AS text,
|
|
1149
|
+
1.0 AS score,
|
|
1150
|
+
m.message_type AS messageType,
|
|
1151
|
+
c.name AS chatName,
|
|
1152
|
+
m.sender_name AS senderName,
|
|
1153
|
+
m.sent_at AS sentAt
|
|
1154
|
+
FROM message_chunks mc
|
|
1155
|
+
JOIN messages m ON m.id = mc.message_id
|
|
1156
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1157
|
+
ORDER BY m.sent_at DESC
|
|
1158
|
+
LIMIT ?
|
|
1159
|
+
`
|
|
1160
|
+
).all(limit);
|
|
1161
|
+
}
|
|
1162
|
+
listAllMessageChunks(limit = 1e4) {
|
|
1163
|
+
return this.database.prepare(
|
|
1164
|
+
`
|
|
1165
|
+
SELECT
|
|
1166
|
+
mc.id AS chunkId,
|
|
1167
|
+
m.id AS messageId,
|
|
1168
|
+
m.platform AS platform,
|
|
1169
|
+
mc.text AS text,
|
|
1170
|
+
1.0 AS score,
|
|
1171
|
+
m.message_type AS messageType,
|
|
1172
|
+
c.name AS chatName,
|
|
1173
|
+
m.sender_name AS senderName,
|
|
1174
|
+
m.sent_at AS sentAt
|
|
1175
|
+
FROM message_chunks mc
|
|
1176
|
+
JOIN messages m ON m.id = mc.message_id
|
|
1177
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1178
|
+
ORDER BY m.sent_at DESC, mc.chunk_index ASC
|
|
1179
|
+
LIMIT ?
|
|
1180
|
+
`
|
|
1181
|
+
).all(limit);
|
|
1182
|
+
}
|
|
1183
|
+
listMessageChunksByMessageIds(messageIds, limit = 1e4) {
|
|
1184
|
+
if (messageIds.length === 0) {
|
|
1185
|
+
return [];
|
|
1186
|
+
}
|
|
1187
|
+
return this.database.prepare(
|
|
1188
|
+
`
|
|
1189
|
+
SELECT
|
|
1190
|
+
mc.id AS chunkId,
|
|
1191
|
+
m.id AS messageId,
|
|
1192
|
+
m.platform AS platform,
|
|
1193
|
+
mc.text AS text,
|
|
1194
|
+
1.0 AS score,
|
|
1195
|
+
m.message_type AS messageType,
|
|
1196
|
+
c.name AS chatName,
|
|
1197
|
+
m.sender_name AS senderName,
|
|
1198
|
+
m.sent_at AS sentAt
|
|
1199
|
+
FROM message_chunks mc
|
|
1200
|
+
JOIN messages m ON m.id = mc.message_id
|
|
1201
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1202
|
+
WHERE m.id IN (${messageIds.map(() => "?").join(", ")})
|
|
1203
|
+
ORDER BY m.sent_at DESC, mc.chunk_index ASC
|
|
1204
|
+
LIMIT ?
|
|
1205
|
+
`
|
|
1206
|
+
).all(...messageIds, limit);
|
|
1207
|
+
}
|
|
1208
|
+
searchMessages(query, limit = 8, options = {}) {
|
|
1209
|
+
const ftsQuery = escapeFtsQuery(query);
|
|
1210
|
+
const excludedIds = options.excludeMessageIds ?? [];
|
|
1211
|
+
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
1212
|
+
const ftsResults = this.database.prepare(
|
|
1213
|
+
`
|
|
1214
|
+
SELECT
|
|
1215
|
+
fts.chunk_id AS chunkId,
|
|
1216
|
+
fts.message_id AS messageId,
|
|
1217
|
+
m.platform AS platform,
|
|
1218
|
+
mc.text AS text,
|
|
1219
|
+
bm25(message_chunks_fts) * -1 AS score,
|
|
1220
|
+
m.message_type AS messageType,
|
|
1221
|
+
c.name AS chatName,
|
|
1222
|
+
m.sender_name AS senderName,
|
|
1223
|
+
m.sent_at AS sentAt
|
|
1224
|
+
FROM message_chunks_fts fts
|
|
1225
|
+
JOIN message_chunks mc ON mc.id = fts.chunk_id
|
|
1226
|
+
JOIN messages m ON m.id = fts.message_id
|
|
1227
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1228
|
+
WHERE message_chunks_fts MATCH ?
|
|
1229
|
+
${excludedWhere}
|
|
1230
|
+
ORDER BY bm25(message_chunks_fts)
|
|
1231
|
+
LIMIT ?
|
|
1232
|
+
`
|
|
1233
|
+
).all(ftsQuery, ...excludedIds, limit);
|
|
1234
|
+
if (ftsResults.length > 0) {
|
|
1235
|
+
return ftsResults;
|
|
1236
|
+
}
|
|
1237
|
+
const terms = buildSearchTerms(query);
|
|
1238
|
+
if (terms.length === 0) {
|
|
1239
|
+
return [];
|
|
1240
|
+
}
|
|
1241
|
+
const where = terms.map(() => "mc.text LIKE ? ESCAPE '\\'").join(" OR ");
|
|
1242
|
+
const params = terms.map((term) => `%${escapeLikeTerm(term)}%`);
|
|
1243
|
+
const likeExcludedWhere = excludedIds.length > 0 ? `AND m.id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
1244
|
+
return this.database.prepare(
|
|
1245
|
+
`
|
|
1246
|
+
SELECT
|
|
1247
|
+
mc.id AS chunkId,
|
|
1248
|
+
m.id AS messageId,
|
|
1249
|
+
m.platform AS platform,
|
|
1250
|
+
mc.text AS text,
|
|
1251
|
+
0.1 AS score,
|
|
1252
|
+
m.message_type AS messageType,
|
|
1253
|
+
c.name AS chatName,
|
|
1254
|
+
m.sender_name AS senderName,
|
|
1255
|
+
m.sent_at AS sentAt
|
|
1256
|
+
FROM message_chunks mc
|
|
1257
|
+
JOIN messages m ON m.id = mc.message_id
|
|
1258
|
+
JOIN chats c ON c.id = m.chat_id
|
|
1259
|
+
WHERE (${where})
|
|
1260
|
+
${likeExcludedWhere}
|
|
1261
|
+
ORDER BY m.sent_at DESC
|
|
1262
|
+
LIMIT ?
|
|
1263
|
+
`
|
|
1264
|
+
).all(...params, ...excludedIds, limit);
|
|
1265
|
+
}
|
|
1266
|
+
getChatCount() {
|
|
1267
|
+
return this.database.prepare("SELECT COUNT(*) AS count FROM chats").get().count;
|
|
1268
|
+
}
|
|
1269
|
+
getMessageCount() {
|
|
1270
|
+
return this.database.prepare("SELECT COUNT(*) AS count FROM messages").get().count;
|
|
1271
|
+
}
|
|
1272
|
+
hasPlatformMessage(platform, platformMessageId) {
|
|
1273
|
+
const row = this.database.prepare("SELECT 1 AS existsFlag FROM messages WHERE platform = ? AND platform_message_id = ? LIMIT 1").get(platform, platformMessageId);
|
|
1274
|
+
return Boolean(row);
|
|
1275
|
+
}
|
|
1276
|
+
listChats() {
|
|
1277
|
+
return this.database.prepare(
|
|
1278
|
+
`
|
|
1279
|
+
SELECT
|
|
1280
|
+
id,
|
|
1281
|
+
platform,
|
|
1282
|
+
platform_chat_id AS platformChatId,
|
|
1283
|
+
name,
|
|
1284
|
+
created_at AS createdAt,
|
|
1285
|
+
updated_at AS updatedAt
|
|
1286
|
+
FROM chats
|
|
1287
|
+
ORDER BY updated_at DESC
|
|
1288
|
+
`
|
|
1289
|
+
).all();
|
|
1290
|
+
}
|
|
1291
|
+
listFiles(limit = 50) {
|
|
1292
|
+
const rows = this.database.prepare(
|
|
1293
|
+
`
|
|
1294
|
+
SELECT
|
|
1295
|
+
id AS messageId,
|
|
1296
|
+
sender_name AS fileName,
|
|
1297
|
+
raw_payload_json AS rawPayloadJson,
|
|
1298
|
+
length(text) AS characters,
|
|
1299
|
+
created_at AS importedAt
|
|
1300
|
+
FROM messages
|
|
1301
|
+
WHERE message_type = 'file'
|
|
1302
|
+
ORDER BY created_at DESC
|
|
1303
|
+
LIMIT ?
|
|
1304
|
+
`
|
|
1305
|
+
).all(limit);
|
|
1306
|
+
return rows.map((row) => {
|
|
1307
|
+
const payload = parseRawPayload(row.rawPayloadJson);
|
|
1308
|
+
return {
|
|
1309
|
+
messageId: row.messageId,
|
|
1310
|
+
fileName: row.fileName,
|
|
1311
|
+
sourcePath: typeof payload.sourcePath === "string" ? payload.sourcePath : void 0,
|
|
1312
|
+
storedPath: typeof payload.storedPath === "string" ? payload.storedPath : void 0,
|
|
1313
|
+
bytes: typeof payload.bytes === "number" ? payload.bytes : void 0,
|
|
1314
|
+
characters: row.characters,
|
|
1315
|
+
parser: typeof payload.parser === "string" ? payload.parser : void 0,
|
|
1316
|
+
parserWarnings: Array.isArray(payload.parserWarnings) ? payload.parserWarnings.filter((item) => typeof item === "string") : void 0,
|
|
1317
|
+
importedAt: row.importedAt
|
|
1318
|
+
};
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
// src/rag/hybrid-retriever.ts
|
|
1324
|
+
function normalizeScore(score) {
|
|
1325
|
+
if (!Number.isFinite(score)) {
|
|
1326
|
+
return 0;
|
|
1327
|
+
}
|
|
1328
|
+
return Math.max(0, Math.min(1, score));
|
|
1329
|
+
}
|
|
1330
|
+
var HybridRetriever = class {
|
|
1331
|
+
constructor(retrievers, options = {}) {
|
|
1332
|
+
this.retrievers = retrievers;
|
|
1333
|
+
this.options = options;
|
|
1334
|
+
}
|
|
1335
|
+
retrievers;
|
|
1336
|
+
options;
|
|
1337
|
+
async retrieve(question) {
|
|
1338
|
+
const results = await Promise.all(this.retrievers.map((retriever) => retriever.retrieve(question)));
|
|
1339
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1340
|
+
for (const [retrieverIndex, evidenceList] of results.entries()) {
|
|
1341
|
+
for (const evidence of evidenceList) {
|
|
1342
|
+
const existing = merged.get(evidence.id);
|
|
1343
|
+
const weightedScore = normalizeScore(evidence.score) + (this.retrievers.length - retrieverIndex) * 0.01;
|
|
1344
|
+
if (!existing || weightedScore > existing.score) {
|
|
1345
|
+
merged.set(evidence.id, {
|
|
1346
|
+
...evidence,
|
|
1347
|
+
score: weightedScore
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return [...merged.values()].sort((left, right) => right.score - left.score).slice(0, this.options.limit ?? 8);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// src/rag/message-retriever.ts
|
|
1357
|
+
function toEvidenceSource(result) {
|
|
1358
|
+
if (result.messageType === "file") {
|
|
1359
|
+
return {
|
|
1360
|
+
type: "file",
|
|
1361
|
+
label: result.senderName,
|
|
1362
|
+
timestamp: result.sentAt
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return {
|
|
1366
|
+
type: "message",
|
|
1367
|
+
label: result.chatName,
|
|
1368
|
+
sender: result.senderName,
|
|
1369
|
+
timestamp: result.sentAt
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
var MessageFtsRetriever = class {
|
|
1373
|
+
constructor(messages, options = {}) {
|
|
1374
|
+
this.messages = messages;
|
|
1375
|
+
this.options = options;
|
|
1376
|
+
}
|
|
1377
|
+
messages;
|
|
1378
|
+
options;
|
|
1379
|
+
async retrieve(question) {
|
|
1380
|
+
const results = this.messages.searchMessages(question, 8, {
|
|
1381
|
+
excludeMessageIds: this.options.excludeMessageIds
|
|
1382
|
+
});
|
|
1383
|
+
return results.map((result) => ({
|
|
1384
|
+
id: result.chunkId,
|
|
1385
|
+
text: result.text,
|
|
1386
|
+
score: result.score,
|
|
1387
|
+
source: toEvidenceSource(result)
|
|
1388
|
+
}));
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
// src/rag/vector-retriever.ts
|
|
1393
|
+
var VectorRetriever = class {
|
|
1394
|
+
constructor(embedding, store, limit = 8) {
|
|
1395
|
+
this.embedding = embedding;
|
|
1396
|
+
this.store = store;
|
|
1397
|
+
this.limit = limit;
|
|
1398
|
+
}
|
|
1399
|
+
embedding;
|
|
1400
|
+
store;
|
|
1401
|
+
limit;
|
|
1402
|
+
async retrieve(question) {
|
|
1403
|
+
const vector = await this.embedding.embed(question);
|
|
1404
|
+
return this.store.search(vector, this.limit);
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// src/rag/factory.ts
|
|
1409
|
+
function hasEmbeddingConfig(config, secrets) {
|
|
1410
|
+
return Boolean((config.embedding.baseUrl || config.llm.baseUrl) && config.embedding.model && (secrets.embedding.apiKey || secrets.llm.apiKey));
|
|
1411
|
+
}
|
|
1412
|
+
async function createHybridRetriever(input2) {
|
|
1413
|
+
const retrievers = [new MessageFtsRetriever(input2.messages, { excludeMessageIds: input2.excludeMessageIds })];
|
|
1414
|
+
const closers = [];
|
|
1415
|
+
if (hasEmbeddingConfig(input2.config, input2.secrets)) {
|
|
1416
|
+
const { LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
|
|
1417
|
+
const vectorStore = await LanceDbVectorStore2.connectFromConfig(input2.config);
|
|
1418
|
+
retrievers.push(new VectorRetriever(createEmbeddingModel(input2.config, input2.secrets), vectorStore));
|
|
1419
|
+
closers.push(() => vectorStore.close());
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
retriever: new HybridRetriever(retrievers),
|
|
1423
|
+
close: () => {
|
|
1424
|
+
for (const closer of closers) {
|
|
1425
|
+
closer();
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/doctor/checks.ts
|
|
1432
|
+
function pass(name, message) {
|
|
1433
|
+
return { name, status: "pass", message };
|
|
1434
|
+
}
|
|
1435
|
+
function warn(name, message) {
|
|
1436
|
+
return { name, status: "warn", message };
|
|
1437
|
+
}
|
|
1438
|
+
function fail(name, message) {
|
|
1439
|
+
return { name, status: "fail", message };
|
|
1440
|
+
}
|
|
1441
|
+
async function runDoctor(config, secrets, options = {}) {
|
|
1442
|
+
const checks = [];
|
|
1443
|
+
checks.push(await checkHomeDirectory());
|
|
1444
|
+
checks.push(checkFeishu(config, secrets));
|
|
1445
|
+
checks.push(checkLlmConfig(config, secrets));
|
|
1446
|
+
checks.push(checkEmbeddingConfig(config, secrets));
|
|
1447
|
+
checks.push(await checkSqlite(config));
|
|
1448
|
+
checks.push(await checkFilePipeline(config));
|
|
1449
|
+
checks.push(await checkLanceDb(config));
|
|
1450
|
+
checks.push(checkRagPolicy());
|
|
1451
|
+
if (options.online) {
|
|
1452
|
+
checks.push(await checkChatModel(config, secrets));
|
|
1453
|
+
checks.push(await checkEmbeddingModel(config, secrets));
|
|
1454
|
+
}
|
|
1455
|
+
return checks;
|
|
1456
|
+
}
|
|
1457
|
+
async function checkHomeDirectory() {
|
|
1458
|
+
const home = getChatterCatcherHome();
|
|
1459
|
+
try {
|
|
1460
|
+
await fs7.mkdir(home, { recursive: true });
|
|
1461
|
+
await fs7.access(home);
|
|
1462
|
+
return pass("\u914D\u7F6E\u76EE\u5F55", home);
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
return fail("\u914D\u7F6E\u76EE\u5F55", error instanceof Error ? error.message : String(error));
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function checkFeishu(config, secrets) {
|
|
1468
|
+
const status = getGatewayStatus(config, secrets);
|
|
1469
|
+
if (status.configured) {
|
|
1470
|
+
return pass("\u98DE\u4E66 Gateway", status.message);
|
|
1471
|
+
}
|
|
1472
|
+
return warn("\u98DE\u4E66 Gateway", status.message);
|
|
1473
|
+
}
|
|
1474
|
+
function checkLlmConfig(config, secrets) {
|
|
1475
|
+
if (!config.llm.baseUrl || !config.llm.model || !secrets.llm.apiKey) {
|
|
1476
|
+
return warn("LLM \u914D\u7F6E", "\u672A\u914D\u7F6E\u5B8C\u6574\uFF1B@ \u63D0\u95EE\u65F6\u65E0\u6CD5\u751F\u6210\u6A21\u578B\u56DE\u7B54\u3002");
|
|
1477
|
+
}
|
|
1478
|
+
return pass("LLM \u914D\u7F6E", `${config.llm.model} @ ${config.llm.baseUrl}`);
|
|
1479
|
+
}
|
|
1480
|
+
function checkEmbeddingConfig(config, secrets) {
|
|
1481
|
+
if (!hasEmbeddingConfig(config, secrets)) {
|
|
1482
|
+
return warn("Embedding \u914D\u7F6E", "\u672A\u914D\u7F6E\u5B8C\u6574\uFF1BRAG \u4F1A\u4F7F\u7528 SQLite FTS\uFF0C\u65E0\u6CD5\u4F7F\u7528 LanceDB \u8BED\u4E49\u68C0\u7D22\u3002");
|
|
1483
|
+
}
|
|
1484
|
+
return pass("Embedding \u914D\u7F6E", `${config.embedding.model} @ ${config.embedding.baseUrl || config.llm.baseUrl}`);
|
|
1485
|
+
}
|
|
1486
|
+
async function checkSqlite(config) {
|
|
1487
|
+
let database = null;
|
|
1488
|
+
try {
|
|
1489
|
+
database = openDatabase(config);
|
|
1490
|
+
const messages = new MessageRepository(database);
|
|
1491
|
+
return pass("SQLite", `${getDatabasePath(config)}\uFF1Bmessages=${messages.getMessageCount()}`);
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
return fail("SQLite", error instanceof Error ? error.message : String(error));
|
|
1494
|
+
} finally {
|
|
1495
|
+
database?.close();
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
async function checkFilePipeline(config) {
|
|
1499
|
+
let database = null;
|
|
1500
|
+
try {
|
|
1501
|
+
database = openDatabase(config);
|
|
1502
|
+
const messages = new MessageRepository(database);
|
|
1503
|
+
const jobs = new FileJobRepository(database);
|
|
1504
|
+
const fileCount = messages.listFiles(1e6).length;
|
|
1505
|
+
const failedJobs = jobs.list(1e6, { status: "failed" });
|
|
1506
|
+
if (failedJobs.length > 0) {
|
|
1507
|
+
return warn("\u6587\u4EF6\u89E3\u6790", `files=${fileCount}\uFF1Bfailed_jobs=${failedJobs.length}\uFF1B\u53EF\u8FD0\u884C chattercatcher files jobs --status failed \u67E5\u770B\u3002`);
|
|
1508
|
+
}
|
|
1509
|
+
return pass("\u6587\u4EF6\u89E3\u6790", `files=${fileCount}\uFF1Bfailed_jobs=0`);
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
return fail("\u6587\u4EF6\u89E3\u6790", error instanceof Error ? error.message : String(error));
|
|
1512
|
+
} finally {
|
|
1513
|
+
database?.close();
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async function checkLanceDb(config) {
|
|
1517
|
+
let store = null;
|
|
1518
|
+
try {
|
|
1519
|
+
const { getLanceDbPath: getLanceDbPath2, LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
|
|
1520
|
+
store = await LanceDbVectorStore2.connectFromConfig(config);
|
|
1521
|
+
const count = await store.count();
|
|
1522
|
+
return pass("LanceDB", `${getLanceDbPath2(config)}\uFF1Bvectors=${count}`);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
return fail("LanceDB", error instanceof Error ? error.message : String(error));
|
|
1525
|
+
} finally {
|
|
1526
|
+
store?.close();
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function checkRagPolicy() {
|
|
1530
|
+
return pass("RAG \u7B56\u7565", "\u5F3A\u5236\u5148\u68C0\u7D22\u8BC1\u636E\u518D\u56DE\u7B54\uFF1B\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0\u3002");
|
|
1531
|
+
}
|
|
1532
|
+
async function checkChatModel(config, secrets) {
|
|
1533
|
+
if (!config.llm.baseUrl || !config.llm.model || !secrets.llm.apiKey) {
|
|
1534
|
+
return warn("LLM \u8FDE\u901A\u6027", "\u8DF3\u8FC7\uFF1ALLM \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002");
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
const answer = await createChatModel(config, secrets).complete([{ role: "user", content: "Reply with OK only." }]);
|
|
1538
|
+
return pass("LLM \u8FDE\u901A\u6027", answer.slice(0, 80));
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
return fail("LLM \u8FDE\u901A\u6027", error instanceof Error ? error.message : String(error));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
async function checkEmbeddingModel(config, secrets) {
|
|
1544
|
+
if (!hasEmbeddingConfig(config, secrets)) {
|
|
1545
|
+
return warn("Embedding \u8FDE\u901A\u6027", "\u8DF3\u8FC7\uFF1AEmbedding \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002");
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
const vector = await createEmbeddingModel(config, secrets).embed("chattercatcher doctor");
|
|
1549
|
+
if (vector.length === 0) {
|
|
1550
|
+
return fail("Embedding \u8FDE\u901A\u6027", "\u8FD4\u56DE\u5411\u91CF\u4E3A\u7A7A\u3002");
|
|
1551
|
+
}
|
|
1552
|
+
return pass("Embedding \u8FDE\u901A\u6027", `dimension=${vector.length}`);
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
return fail("Embedding \u8FDE\u901A\u6027", error instanceof Error ? error.message : String(error));
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function formatDoctorChecks(checks) {
|
|
1558
|
+
const icon = {
|
|
1559
|
+
pass: "PASS",
|
|
1560
|
+
warn: "WARN",
|
|
1561
|
+
fail: "FAIL"
|
|
1562
|
+
};
|
|
1563
|
+
return checks.map((check) => `[${icon[check.status]}] ${check.name}: ${check.message}`).join("\n");
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/export/data-export.ts
|
|
1567
|
+
init_paths();
|
|
1568
|
+
import fs8 from "fs/promises";
|
|
1569
|
+
import path10 from "path";
|
|
1570
|
+
function parseJsonObject(value) {
|
|
1571
|
+
try {
|
|
1572
|
+
const parsed = JSON.parse(value);
|
|
1573
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1574
|
+
} catch {
|
|
1575
|
+
return {};
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function parseJsonArray(value) {
|
|
1579
|
+
try {
|
|
1580
|
+
const parsed = JSON.parse(value);
|
|
1581
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1582
|
+
} catch {
|
|
1583
|
+
return [];
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function defaultExportPath(config, exportedAt) {
|
|
1587
|
+
const fileName = `chattercatcher-export-${exportedAt.replace(/[:.]/g, "-")}.json`;
|
|
1588
|
+
return path10.join(resolveHomePath(config.storage.dataDir), "exports", fileName);
|
|
1589
|
+
}
|
|
1590
|
+
async function exportLocalData(input2) {
|
|
1591
|
+
const exportedAt = input2.exportedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1592
|
+
const outputPath = path10.resolve(input2.outputPath ?? defaultExportPath(input2.config, exportedAt));
|
|
1593
|
+
const chats = input2.database.prepare(
|
|
1594
|
+
`
|
|
1595
|
+
SELECT
|
|
1596
|
+
id,
|
|
1597
|
+
platform,
|
|
1598
|
+
platform_chat_id AS platformChatId,
|
|
1599
|
+
name,
|
|
1600
|
+
created_at AS createdAt,
|
|
1601
|
+
updated_at AS updatedAt
|
|
1602
|
+
FROM chats
|
|
1603
|
+
ORDER BY updated_at ASC
|
|
1604
|
+
`
|
|
1605
|
+
).all();
|
|
1606
|
+
const messages = input2.database.prepare(
|
|
1607
|
+
`
|
|
1608
|
+
SELECT
|
|
1609
|
+
id,
|
|
1610
|
+
platform,
|
|
1611
|
+
platform_message_id AS platformMessageId,
|
|
1612
|
+
chat_id AS chatId,
|
|
1613
|
+
sender_id AS senderId,
|
|
1614
|
+
sender_name AS senderName,
|
|
1615
|
+
message_type AS messageType,
|
|
1616
|
+
text,
|
|
1617
|
+
raw_payload_json AS rawPayloadJson,
|
|
1618
|
+
sent_at AS sentAt,
|
|
1619
|
+
received_at AS receivedAt,
|
|
1620
|
+
created_at AS createdAt
|
|
1621
|
+
FROM messages
|
|
1622
|
+
ORDER BY sent_at ASC, created_at ASC
|
|
1623
|
+
`
|
|
1624
|
+
).all().map(({ rawPayloadJson, ...message }) => ({
|
|
1625
|
+
...message,
|
|
1626
|
+
rawPayload: parseJsonObject(rawPayloadJson)
|
|
1627
|
+
}));
|
|
1628
|
+
const chunks = input2.database.prepare(
|
|
1629
|
+
`
|
|
1630
|
+
SELECT
|
|
1631
|
+
id,
|
|
1632
|
+
message_id AS messageId,
|
|
1633
|
+
chunk_index AS chunkIndex,
|
|
1634
|
+
text,
|
|
1635
|
+
metadata_json AS metadataJson,
|
|
1636
|
+
created_at AS createdAt
|
|
1637
|
+
FROM message_chunks
|
|
1638
|
+
ORDER BY message_id ASC, chunk_index ASC
|
|
1639
|
+
`
|
|
1640
|
+
).all().map(({ metadataJson, ...chunk }) => ({
|
|
1641
|
+
...chunk,
|
|
1642
|
+
metadata: parseJsonObject(metadataJson)
|
|
1643
|
+
}));
|
|
1644
|
+
const fileJobs = input2.database.prepare(
|
|
1645
|
+
`
|
|
1646
|
+
SELECT
|
|
1647
|
+
id,
|
|
1648
|
+
source_path AS sourcePath,
|
|
1649
|
+
stored_path AS storedPath,
|
|
1650
|
+
file_name AS fileName,
|
|
1651
|
+
status,
|
|
1652
|
+
parser,
|
|
1653
|
+
message_id AS messageId,
|
|
1654
|
+
bytes,
|
|
1655
|
+
characters,
|
|
1656
|
+
warnings_json AS warningsJson,
|
|
1657
|
+
error,
|
|
1658
|
+
created_at AS createdAt,
|
|
1659
|
+
updated_at AS updatedAt
|
|
1660
|
+
FROM file_jobs
|
|
1661
|
+
ORDER BY updated_at ASC
|
|
1662
|
+
`
|
|
1663
|
+
).all().map(({ warningsJson, ...job }) => ({
|
|
1664
|
+
...job,
|
|
1665
|
+
warnings: parseJsonArray(warningsJson).filter((item) => typeof item === "string")
|
|
1666
|
+
}));
|
|
1667
|
+
const payload = {
|
|
1668
|
+
app: "ChatterCatcher",
|
|
1669
|
+
schemaVersion: 1,
|
|
1670
|
+
exportedAt,
|
|
1671
|
+
note: "\u672C\u6587\u4EF6\u53EA\u5305\u542B\u672C\u5730\u77E5\u8BC6\u5E93\u6570\u636E\uFF0C\u4E0D\u5305\u542B API Key\u3001App Secret \u6216 token\u3002",
|
|
1672
|
+
data: {
|
|
1673
|
+
chats,
|
|
1674
|
+
messages,
|
|
1675
|
+
chunks,
|
|
1676
|
+
fileJobs
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
await fs8.mkdir(path10.dirname(outputPath), { recursive: true });
|
|
1680
|
+
await fs8.writeFile(outputPath, `${JSON.stringify(payload, null, 2)}
|
|
1681
|
+
`, "utf8");
|
|
1682
|
+
return {
|
|
1683
|
+
outputPath,
|
|
1684
|
+
chats: chats.length,
|
|
1685
|
+
messages: messages.length,
|
|
1686
|
+
chunks: chunks.length,
|
|
1687
|
+
fileJobs: fileJobs.length
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// src/export/data-restore.ts
|
|
1692
|
+
import fs9 from "fs/promises";
|
|
1693
|
+
import path11 from "path";
|
|
1694
|
+
function asObject(value) {
|
|
1695
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1696
|
+
}
|
|
1697
|
+
function asArray(value) {
|
|
1698
|
+
return Array.isArray(value) ? value.map(asObject) : [];
|
|
1699
|
+
}
|
|
1700
|
+
function asString(value, field) {
|
|
1701
|
+
if (typeof value !== "string") {
|
|
1702
|
+
throw new Error(`\u6062\u590D\u6587\u4EF6\u5B57\u6BB5\u65E0\u6548\uFF1A${field}`);
|
|
1703
|
+
}
|
|
1704
|
+
return value;
|
|
1705
|
+
}
|
|
1706
|
+
function asOptionalString(value) {
|
|
1707
|
+
return typeof value === "string" ? value : null;
|
|
1708
|
+
}
|
|
1709
|
+
function asOptionalNumber(value) {
|
|
1710
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1711
|
+
}
|
|
1712
|
+
function asJson(value, fallback) {
|
|
1713
|
+
return JSON.stringify(value === void 0 ? fallback : value);
|
|
1714
|
+
}
|
|
1715
|
+
function parsePayload(raw) {
|
|
1716
|
+
const parsed = asObject(JSON.parse(raw));
|
|
1717
|
+
const data2 = asObject(parsed.data);
|
|
1718
|
+
const payload = {
|
|
1719
|
+
app: asString(parsed.app, "app"),
|
|
1720
|
+
schemaVersion: typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : NaN,
|
|
1721
|
+
data: {
|
|
1722
|
+
chats: asArray(data2.chats),
|
|
1723
|
+
messages: asArray(data2.messages),
|
|
1724
|
+
chunks: asArray(data2.chunks),
|
|
1725
|
+
fileJobs: asArray(data2.fileJobs)
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
if (payload.app !== "ChatterCatcher" || payload.schemaVersion !== 1) {
|
|
1729
|
+
throw new Error("\u6062\u590D\u6587\u4EF6\u4E0D\u662F ChatterCatcher schemaVersion=1 \u5BFC\u51FA\u3002");
|
|
1730
|
+
}
|
|
1731
|
+
return payload;
|
|
1732
|
+
}
|
|
1733
|
+
function clearDatabase(database) {
|
|
1734
|
+
database.prepare("DELETE FROM message_chunks_fts").run();
|
|
1735
|
+
database.prepare("DELETE FROM message_chunks").run();
|
|
1736
|
+
database.prepare("DELETE FROM file_jobs").run();
|
|
1737
|
+
database.prepare("DELETE FROM messages").run();
|
|
1738
|
+
database.prepare("DELETE FROM chats").run();
|
|
1739
|
+
}
|
|
1740
|
+
async function restoreLocalData(input2) {
|
|
1741
|
+
const inputPath = path11.resolve(input2.inputPath);
|
|
1742
|
+
const payload = parsePayload(await fs9.readFile(inputPath, "utf8"));
|
|
1743
|
+
const mode = input2.replace ? "replace" : "merge";
|
|
1744
|
+
const restore = input2.database.transaction(() => {
|
|
1745
|
+
if (input2.replace) {
|
|
1746
|
+
clearDatabase(input2.database);
|
|
1747
|
+
}
|
|
1748
|
+
const upsertChat = input2.database.prepare(`
|
|
1749
|
+
INSERT INTO chats (id, platform, platform_chat_id, name, created_at, updated_at)
|
|
1750
|
+
VALUES (@id, @platform, @platformChatId, @name, @createdAt, @updatedAt)
|
|
1751
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1752
|
+
platform = excluded.platform,
|
|
1753
|
+
platform_chat_id = excluded.platform_chat_id,
|
|
1754
|
+
name = excluded.name,
|
|
1755
|
+
updated_at = excluded.updated_at
|
|
1756
|
+
`);
|
|
1757
|
+
const upsertMessage = input2.database.prepare(`
|
|
1758
|
+
INSERT INTO messages (
|
|
1759
|
+
id, platform, platform_message_id, chat_id, sender_id, sender_name,
|
|
1760
|
+
message_type, text, raw_payload_json, sent_at, received_at, created_at
|
|
1761
|
+
)
|
|
1762
|
+
VALUES (
|
|
1763
|
+
@id, @platform, @platformMessageId, @chatId, @senderId, @senderName,
|
|
1764
|
+
@messageType, @text, @rawPayloadJson, @sentAt, @receivedAt, @createdAt
|
|
1765
|
+
)
|
|
1766
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1767
|
+
platform = excluded.platform,
|
|
1768
|
+
platform_message_id = excluded.platform_message_id,
|
|
1769
|
+
chat_id = excluded.chat_id,
|
|
1770
|
+
sender_id = excluded.sender_id,
|
|
1771
|
+
sender_name = excluded.sender_name,
|
|
1772
|
+
message_type = excluded.message_type,
|
|
1773
|
+
text = excluded.text,
|
|
1774
|
+
raw_payload_json = excluded.raw_payload_json,
|
|
1775
|
+
sent_at = excluded.sent_at,
|
|
1776
|
+
received_at = excluded.received_at
|
|
1777
|
+
`);
|
|
1778
|
+
const upsertChunk = input2.database.prepare(`
|
|
1779
|
+
INSERT INTO message_chunks (id, message_id, chunk_index, text, metadata_json, created_at)
|
|
1780
|
+
VALUES (@id, @messageId, @chunkIndex, @text, @metadataJson, @createdAt)
|
|
1781
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1782
|
+
message_id = excluded.message_id,
|
|
1783
|
+
chunk_index = excluded.chunk_index,
|
|
1784
|
+
text = excluded.text,
|
|
1785
|
+
metadata_json = excluded.metadata_json
|
|
1786
|
+
`);
|
|
1787
|
+
const insertFts = input2.database.prepare(`
|
|
1788
|
+
INSERT INTO message_chunks_fts (text, chunk_id, message_id)
|
|
1789
|
+
VALUES (@text, @chunkId, @messageId)
|
|
1790
|
+
`);
|
|
1791
|
+
const upsertFileJob = input2.database.prepare(`
|
|
1792
|
+
INSERT INTO file_jobs (
|
|
1793
|
+
id, source_path, stored_path, file_name, status, parser, message_id,
|
|
1794
|
+
bytes, characters, warnings_json, error, created_at, updated_at
|
|
1795
|
+
)
|
|
1796
|
+
VALUES (
|
|
1797
|
+
@id, @sourcePath, @storedPath, @fileName, @status, @parser, @messageId,
|
|
1798
|
+
@bytes, @characters, @warningsJson, @error, @createdAt, @updatedAt
|
|
1799
|
+
)
|
|
1800
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1801
|
+
source_path = excluded.source_path,
|
|
1802
|
+
stored_path = excluded.stored_path,
|
|
1803
|
+
file_name = excluded.file_name,
|
|
1804
|
+
status = excluded.status,
|
|
1805
|
+
parser = excluded.parser,
|
|
1806
|
+
message_id = excluded.message_id,
|
|
1807
|
+
bytes = excluded.bytes,
|
|
1808
|
+
characters = excluded.characters,
|
|
1809
|
+
warnings_json = excluded.warnings_json,
|
|
1810
|
+
error = excluded.error,
|
|
1811
|
+
updated_at = excluded.updated_at
|
|
1812
|
+
`);
|
|
1813
|
+
for (const chat of payload.data.chats) {
|
|
1814
|
+
upsertChat.run({
|
|
1815
|
+
id: asString(chat.id, "chat.id"),
|
|
1816
|
+
platform: asString(chat.platform, "chat.platform"),
|
|
1817
|
+
platformChatId: asString(chat.platformChatId, "chat.platformChatId"),
|
|
1818
|
+
name: asString(chat.name, "chat.name"),
|
|
1819
|
+
createdAt: asString(chat.createdAt, "chat.createdAt"),
|
|
1820
|
+
updatedAt: asString(chat.updatedAt, "chat.updatedAt")
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
for (const message of payload.data.messages) {
|
|
1824
|
+
upsertMessage.run({
|
|
1825
|
+
id: asString(message.id, "message.id"),
|
|
1826
|
+
platform: asString(message.platform, "message.platform"),
|
|
1827
|
+
platformMessageId: asString(message.platformMessageId, "message.platformMessageId"),
|
|
1828
|
+
chatId: asString(message.chatId, "message.chatId"),
|
|
1829
|
+
senderId: asString(message.senderId, "message.senderId"),
|
|
1830
|
+
senderName: asString(message.senderName, "message.senderName"),
|
|
1831
|
+
messageType: asString(message.messageType, "message.messageType"),
|
|
1832
|
+
text: asString(message.text, "message.text"),
|
|
1833
|
+
rawPayloadJson: asJson(message.rawPayload, {}),
|
|
1834
|
+
sentAt: asString(message.sentAt, "message.sentAt"),
|
|
1835
|
+
receivedAt: asString(message.receivedAt, "message.receivedAt"),
|
|
1836
|
+
createdAt: asString(message.createdAt, "message.createdAt")
|
|
1837
|
+
});
|
|
1838
|
+
input2.database.prepare("DELETE FROM message_chunks_fts WHERE message_id = ?").run(asString(message.id, "message.id"));
|
|
1839
|
+
}
|
|
1840
|
+
for (const chunk of payload.data.chunks) {
|
|
1841
|
+
const messageId = asString(chunk.messageId, "chunk.messageId");
|
|
1842
|
+
const chunkId = asString(chunk.id, "chunk.id");
|
|
1843
|
+
const text = asString(chunk.text, "chunk.text");
|
|
1844
|
+
upsertChunk.run({
|
|
1845
|
+
id: chunkId,
|
|
1846
|
+
messageId,
|
|
1847
|
+
chunkIndex: asOptionalNumber(chunk.chunkIndex) ?? 0,
|
|
1848
|
+
text,
|
|
1849
|
+
metadataJson: asJson(chunk.metadata, {}),
|
|
1850
|
+
createdAt: asString(chunk.createdAt, "chunk.createdAt")
|
|
1851
|
+
});
|
|
1852
|
+
insertFts.run({ text, chunkId, messageId });
|
|
1853
|
+
}
|
|
1854
|
+
for (const job of payload.data.fileJobs) {
|
|
1855
|
+
upsertFileJob.run({
|
|
1856
|
+
id: asString(job.id, "fileJob.id"),
|
|
1857
|
+
sourcePath: asString(job.sourcePath, "fileJob.sourcePath"),
|
|
1858
|
+
storedPath: asOptionalString(job.storedPath),
|
|
1859
|
+
fileName: asString(job.fileName, "fileJob.fileName"),
|
|
1860
|
+
status: asString(job.status, "fileJob.status"),
|
|
1861
|
+
parser: asOptionalString(job.parser),
|
|
1862
|
+
messageId: asOptionalString(job.messageId),
|
|
1863
|
+
bytes: asOptionalNumber(job.bytes),
|
|
1864
|
+
characters: asOptionalNumber(job.characters),
|
|
1865
|
+
warningsJson: asJson(Array.isArray(job.warnings) ? job.warnings : [], []),
|
|
1866
|
+
error: asOptionalString(job.error),
|
|
1867
|
+
createdAt: asString(job.createdAt, "fileJob.createdAt"),
|
|
1868
|
+
updatedAt: asString(job.updatedAt, "fileJob.updatedAt")
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
restore();
|
|
1873
|
+
return {
|
|
1874
|
+
inputPath,
|
|
1875
|
+
mode,
|
|
1876
|
+
chats: payload.data.chats.length,
|
|
1877
|
+
messages: payload.data.messages.length,
|
|
1878
|
+
chunks: payload.data.chunks.length,
|
|
1879
|
+
fileJobs: payload.data.fileJobs.length
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/feishu/gateway.ts
|
|
1884
|
+
import * as lark2 from "@larksuiteoapi/node-sdk";
|
|
1885
|
+
|
|
1886
|
+
// src/rag/citations.ts
|
|
1887
|
+
function isOpaqueId(value) {
|
|
1888
|
+
return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
|
|
1889
|
+
}
|
|
1890
|
+
function formatTime(value) {
|
|
1891
|
+
if (!value) {
|
|
1892
|
+
return "\u672A\u77E5\u65F6\u95F4";
|
|
1893
|
+
}
|
|
1894
|
+
const date = new Date(value);
|
|
1895
|
+
if (Number.isNaN(date.getTime())) {
|
|
1896
|
+
return value;
|
|
1897
|
+
}
|
|
1898
|
+
const pad = (input2) => String(input2).padStart(2, "0");
|
|
1899
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1900
|
+
}
|
|
1901
|
+
function formatSpeaker(source) {
|
|
1902
|
+
if (source.type === "file") {
|
|
1903
|
+
return isOpaqueId(source.label) ? "\u6587\u4EF6" : `\u6587\u4EF6 ${source.label}`;
|
|
1904
|
+
}
|
|
1905
|
+
if (source.sender && !isOpaqueId(source.sender)) {
|
|
1906
|
+
return source.sender;
|
|
1907
|
+
}
|
|
1908
|
+
return "\u7FA4\u6210\u5458";
|
|
1909
|
+
}
|
|
1910
|
+
function clipText(value, maxLength) {
|
|
1911
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
1912
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
1913
|
+
}
|
|
1914
|
+
function formatCitation(citation, options = {}) {
|
|
1915
|
+
const maxTextLength = options.maxTextLength ?? 120;
|
|
1916
|
+
const speaker = formatSpeaker(citation.source);
|
|
1917
|
+
const time = formatTime(citation.source.timestamp);
|
|
1918
|
+
const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
|
|
1919
|
+
return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
|
|
1920
|
+
}
|
|
1921
|
+
function formatCitations(citations, options = {}) {
|
|
1922
|
+
return citations.map((citation) => formatCitation(citation, options)).join("\n");
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// src/rag/answer.ts
|
|
1926
|
+
var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
|
|
1927
|
+
var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
|
|
1928
|
+
var SCORE_TIE_THRESHOLD = 0.15;
|
|
1929
|
+
function parseTimestamp(value) {
|
|
1930
|
+
if (!value) {
|
|
1931
|
+
return 0;
|
|
1932
|
+
}
|
|
1933
|
+
const time = Date.parse(value);
|
|
1934
|
+
return Number.isFinite(time) ? time : 0;
|
|
1935
|
+
}
|
|
1936
|
+
function rankEvidenceForPrompt(evidence) {
|
|
1937
|
+
return [...evidence].sort((left, right) => {
|
|
1938
|
+
const scoreDiff = right.score - left.score;
|
|
1939
|
+
if (Math.abs(scoreDiff) > SCORE_TIE_THRESHOLD) {
|
|
1940
|
+
return scoreDiff;
|
|
1941
|
+
}
|
|
1942
|
+
const timeDiff = parseTimestamp(right.source.timestamp) - parseTimestamp(left.source.timestamp);
|
|
1943
|
+
if (timeDiff !== 0) {
|
|
1944
|
+
return timeDiff;
|
|
1945
|
+
}
|
|
1946
|
+
return scoreDiff;
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
function buildEvidencePrompt(question, evidence, options = {}) {
|
|
1950
|
+
if (evidence.length === 0) {
|
|
1951
|
+
throw new Error("RAG evidence is required before answer generation.");
|
|
1952
|
+
}
|
|
1953
|
+
const maxEvidenceBlocks = options.maxEvidenceBlocks ?? DEFAULT_MAX_EVIDENCE_BLOCKS;
|
|
1954
|
+
const maxCharsPerBlock = options.maxCharsPerBlock ?? DEFAULT_MAX_CHARS_PER_BLOCK;
|
|
1955
|
+
const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
|
|
1956
|
+
const citations = selected.map((item, index2) => ({
|
|
1957
|
+
marker: `S${index2 + 1}`,
|
|
1958
|
+
evidenceId: item.id,
|
|
1959
|
+
source: item.source,
|
|
1960
|
+
text: item.text
|
|
1961
|
+
}));
|
|
1962
|
+
const evidenceText = selected.map((item, index2) => {
|
|
1963
|
+
const marker = citations[index2]?.marker;
|
|
1964
|
+
const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
|
|
1965
|
+
const sourceParts = [
|
|
1966
|
+
item.source.label,
|
|
1967
|
+
item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
|
|
1968
|
+
item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
|
|
1969
|
+
item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
|
|
1970
|
+
].filter(Boolean);
|
|
1971
|
+
return `[${marker}]
|
|
1972
|
+
\u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
|
|
1973
|
+
\u5185\u5BB9\uFF1A${clippedText}`;
|
|
1974
|
+
}).join("\n\n");
|
|
1975
|
+
return {
|
|
1976
|
+
citations,
|
|
1977
|
+
messages: [
|
|
1978
|
+
{
|
|
1979
|
+
role: "system",
|
|
1980
|
+
content: "\u4F60\u662F ChatterCatcher \u7684\u95EE\u7B54\u6A21\u5757\u3002\u53EA\u80FD\u6839\u636E\u63D0\u4F9B\u7684\u68C0\u7D22\u8BC1\u636E\u56DE\u7B54\uFF0C\u5FC5\u987B\u7B80\u77ED\u76F4\u63A5\u3002\u4E8B\u5B9E\u6027\u7ED3\u8BBA\u5FC5\u987B\u5F15\u7528 [S1] \u8FD9\u6837\u7684\u6765\u6E90\u6807\u8BB0\u3002\u8BC1\u636E\u4E0D\u8DB3\u65F6\u8BF4\u4E0D\u77E5\u9053\uFF0C\u4E0D\u8981\u731C\u3002\u82E5\u8BC1\u636E\u4E92\u76F8\u77DB\u76FE\uFF0C\u4F18\u5148\u91C7\u7528\u65F6\u95F4\u66F4\u65B0\u4E14\u8868\u8FF0\u660E\u786E\u7684\u8BC1\u636E\uFF1B\u5982\u679C\u8F83\u65B0\u7684\u8BC1\u636E\u53EA\u662F\u8BA8\u8BBA\u3001\u731C\u6D4B\u6216\u4E0D\u786E\u5B9A\u8868\u8FBE\uFF0C\u4E0D\u8981\u628A\u5B83\u5F53\u4F5C\u786E\u5B9A\u66F4\u65B0\u3002"
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
role: "user",
|
|
1984
|
+
content: `\u95EE\u9898\uFF1A${question}
|
|
1985
|
+
|
|
1986
|
+
\u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
|
|
1987
|
+
1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
|
|
1988
|
+
2. \u540C\u4E00\u4E8B\u9879\u51FA\u73B0\u591A\u4E2A\u7248\u672C\u65F6\uFF0C\u9ED8\u8BA4\u8F83\u65B0\u7684\u660E\u786E\u6D88\u606F\u4F18\u5148\u3002
|
|
1989
|
+
3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
|
|
1990
|
+
|
|
1991
|
+
\u68C0\u7D22\u8BC1\u636E\uFF1A
|
|
1992
|
+
${evidenceText}`
|
|
1993
|
+
}
|
|
1994
|
+
]
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
async function generateGroundedAnswer(input2) {
|
|
1998
|
+
const prompt = buildEvidencePrompt(input2.question, input2.evidence);
|
|
1999
|
+
const answer = await input2.model.complete(prompt.messages);
|
|
2000
|
+
return {
|
|
2001
|
+
answer,
|
|
2002
|
+
citations: prompt.citations
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// src/rag/qa-service.ts
|
|
2007
|
+
async function askWithRag(input2) {
|
|
2008
|
+
const evidence = await input2.retriever.retrieve(input2.question);
|
|
2009
|
+
if (evidence.length === 0) {
|
|
2010
|
+
return {
|
|
2011
|
+
answer: "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002",
|
|
2012
|
+
citations: []
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
return generateGroundedAnswer({
|
|
2016
|
+
question: input2.question,
|
|
2017
|
+
evidence,
|
|
2018
|
+
model: input2.model
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// src/feishu/question.ts
|
|
2023
|
+
function parseTextContent(content) {
|
|
2024
|
+
if (!content) {
|
|
2025
|
+
return "";
|
|
2026
|
+
}
|
|
2027
|
+
try {
|
|
2028
|
+
const parsed = JSON.parse(content);
|
|
2029
|
+
return typeof parsed.text === "string" ? parsed.text : "";
|
|
2030
|
+
} catch {
|
|
2031
|
+
return content;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function stripMentions(text, mentions) {
|
|
2035
|
+
let result = text;
|
|
2036
|
+
for (const mention of mentions ?? []) {
|
|
2037
|
+
for (const token of [mention.key, mention.name, mention.name ? `@${mention.name}` : void 0]) {
|
|
2038
|
+
if (token) {
|
|
2039
|
+
result = result.replaceAll(token, " ");
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return result.replace(/@\s*ChatterCatcher/gi, " ").replace(/@/g, " ").replace(/\s+/g, " ").trim();
|
|
2044
|
+
}
|
|
2045
|
+
function isFeishuMessageAddressedToBot(payload) {
|
|
2046
|
+
const message = payload.event?.message;
|
|
2047
|
+
if (!message || message.message_type !== "text") {
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
const mentions = message.mentions ?? [];
|
|
2051
|
+
const text = parseTextContent(message.content);
|
|
2052
|
+
return mentions.length > 0 || /@?ChatterCatcher/i.test(text);
|
|
2053
|
+
}
|
|
2054
|
+
function getFeishuQuestionDecision(payload, config) {
|
|
2055
|
+
const message = payload.event?.message;
|
|
2056
|
+
if (!message?.chat_id || message.message_type !== "text") {
|
|
2057
|
+
return {
|
|
2058
|
+
shouldAnswer: false,
|
|
2059
|
+
reason: "\u4E0D\u662F\u53EF\u56DE\u7B54\u7684\u6587\u672C\u6D88\u606F\u3002"
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
const mentions = message.mentions ?? [];
|
|
2063
|
+
const text = parseTextContent(message.content);
|
|
2064
|
+
const hasMention = isFeishuMessageAddressedToBot(payload);
|
|
2065
|
+
if (config.feishu.requireMention && !hasMention) {
|
|
2066
|
+
return {
|
|
2067
|
+
shouldAnswer: false,
|
|
2068
|
+
reason: "\u7FA4\u804A\u914D\u7F6E\u4E3A\u5FC5\u987B @ \u540E\u56DE\u7B54\u3002"
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
const question = stripMentions(text, mentions);
|
|
2072
|
+
if (!question) {
|
|
2073
|
+
return {
|
|
2074
|
+
shouldAnswer: false,
|
|
2075
|
+
reason: "\u6CA1\u6709\u53EF\u56DE\u7B54\u7684\u95EE\u9898\u6587\u672C\u3002"
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
return {
|
|
2079
|
+
shouldAnswer: true,
|
|
2080
|
+
question,
|
|
2081
|
+
chatId: message.chat_id
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
var FeishuQuestionHandler = class {
|
|
2085
|
+
constructor(options) {
|
|
2086
|
+
this.options = options;
|
|
2087
|
+
}
|
|
2088
|
+
options;
|
|
2089
|
+
async sendResponse(chatId, messageId, text) {
|
|
2090
|
+
if (messageId && this.options.sender.replyTextToMessage) {
|
|
2091
|
+
try {
|
|
2092
|
+
await this.options.sender.replyTextToMessage(messageId, text);
|
|
2093
|
+
return;
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
console.log(`\u98DE\u4E66\u56DE\u590D\u539F\u6D88\u606F\u5931\u8D25\uFF0C\u9000\u56DE\u7FA4\u6D88\u606F\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
await this.options.sender.sendTextToChat(chatId, text);
|
|
2099
|
+
}
|
|
2100
|
+
async acknowledgeQuestion(chatId, messageId) {
|
|
2101
|
+
if (!messageId) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (this.options.sender.addReactionToMessage) {
|
|
2105
|
+
try {
|
|
2106
|
+
await this.options.sender.addReactionToMessage(messageId, this.options.thinkingEmojiType ?? "keyboard");
|
|
2107
|
+
return;
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
console.log(`\u98DE\u4E66\u63D0\u95EE\u8868\u60C5\u53CD\u9988\u5931\u8D25\uFF0C\u6539\u7528\u6587\u5B57\u53CD\u9988\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
await this.sendResponse(chatId, messageId, "\u6536\u5230\uFF0C\u6B63\u5728\u67E5\u3002");
|
|
2113
|
+
}
|
|
2114
|
+
async handle(payload, options = {}) {
|
|
2115
|
+
const decision = getFeishuQuestionDecision(payload, this.options.config);
|
|
2116
|
+
if (!decision.shouldAnswer || !decision.question || !decision.chatId) {
|
|
2117
|
+
return decision;
|
|
2118
|
+
}
|
|
2119
|
+
const questionMessageId = payload.event?.message?.message_id;
|
|
2120
|
+
await this.acknowledgeQuestion(decision.chatId, questionMessageId);
|
|
2121
|
+
const { retriever, close } = await createHybridRetriever({
|
|
2122
|
+
config: this.options.config,
|
|
2123
|
+
secrets: this.options.secrets,
|
|
2124
|
+
messages: new MessageRepository(this.options.database),
|
|
2125
|
+
excludeMessageIds: options.excludeMessageIds
|
|
2126
|
+
});
|
|
2127
|
+
try {
|
|
2128
|
+
try {
|
|
2129
|
+
const result = await askWithRag({
|
|
2130
|
+
question: decision.question,
|
|
2131
|
+
retriever,
|
|
2132
|
+
model: this.options.model
|
|
2133
|
+
});
|
|
2134
|
+
const citations = formatCitations(result.citations);
|
|
2135
|
+
const text = citations ? `${result.answer}
|
|
50
2136
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
2137
|
+
\u5F15\u7528\uFF1A
|
|
2138
|
+
${citations}` : result.answer;
|
|
2139
|
+
await this.sendResponse(decision.chatId, questionMessageId, text);
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2142
|
+
await this.sendResponse(decision.chatId, questionMessageId, `\u6682\u65F6\u65E0\u6CD5\u56DE\u7B54\uFF1A${message}`);
|
|
2143
|
+
}
|
|
2144
|
+
return decision;
|
|
2145
|
+
} finally {
|
|
2146
|
+
close();
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
// src/feishu/sender.ts
|
|
2152
|
+
import * as lark from "@larksuiteoapi/node-sdk";
|
|
2153
|
+
function mapDomain(domain) {
|
|
2154
|
+
return domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu;
|
|
2155
|
+
}
|
|
2156
|
+
var FeishuMessageSender = class _FeishuMessageSender {
|
|
2157
|
+
constructor(client) {
|
|
2158
|
+
this.client = client;
|
|
2159
|
+
}
|
|
2160
|
+
client;
|
|
2161
|
+
static fromConfig(config, secrets) {
|
|
2162
|
+
const client = new lark.Client({
|
|
2163
|
+
appId: config.feishu.appId,
|
|
2164
|
+
appSecret: secrets.feishu.appSecret,
|
|
2165
|
+
domain: mapDomain(config.feishu.domain)
|
|
2166
|
+
});
|
|
2167
|
+
return new _FeishuMessageSender(client);
|
|
2168
|
+
}
|
|
2169
|
+
async sendTextToChat(chatId, text) {
|
|
2170
|
+
const payload = {
|
|
2171
|
+
data: {
|
|
2172
|
+
receive_id: chatId,
|
|
2173
|
+
msg_type: "text",
|
|
2174
|
+
content: JSON.stringify({ text })
|
|
2175
|
+
},
|
|
2176
|
+
params: {
|
|
2177
|
+
receive_id_type: "chat_id"
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
if (this.client.im.v1?.message.create) {
|
|
2181
|
+
await this.client.im.v1.message.create(payload);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (this.client.im.message?.create) {
|
|
2185
|
+
await this.client.im.message.create(payload);
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
{
|
|
2189
|
+
throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
async replyTextToMessage(messageId, text) {
|
|
2193
|
+
const payload = {
|
|
2194
|
+
path: {
|
|
2195
|
+
message_id: messageId
|
|
2196
|
+
},
|
|
2197
|
+
data: {
|
|
2198
|
+
msg_type: "text",
|
|
2199
|
+
content: JSON.stringify({ text })
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
if (this.client.im.v1?.message.reply) {
|
|
2203
|
+
await this.client.im.v1.message.reply(payload);
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
if (this.client.im.message?.reply) {
|
|
2207
|
+
await this.client.im.message.reply(payload);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u56DE\u590D\u63A5\u53E3\u3002");
|
|
2211
|
+
}
|
|
2212
|
+
async addReactionToMessage(messageId, emojiType) {
|
|
2213
|
+
if (!this.client.im.v1?.messageReaction?.create) {
|
|
2214
|
+
throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u8868\u60C5\u56DE\u590D\u63A5\u53E3\u3002");
|
|
2215
|
+
}
|
|
2216
|
+
await this.client.im.v1.messageReaction.create({
|
|
2217
|
+
path: {
|
|
2218
|
+
message_id: messageId
|
|
2219
|
+
},
|
|
2220
|
+
data: {
|
|
2221
|
+
reaction_type: {
|
|
2222
|
+
emoji_type: emojiType
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
// src/feishu/gateway.ts
|
|
2230
|
+
function assertFeishuConfig(config, secrets) {
|
|
2231
|
+
if (!config.feishu.appId || !secrets.feishu.appSecret) {
|
|
2232
|
+
throw new Error("\u98DE\u4E66\u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u5148\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
function createFeishuEventDispatcher(options) {
|
|
2236
|
+
const answeredMessageIds = /* @__PURE__ */ new Set();
|
|
2237
|
+
return new lark2.EventDispatcher({}).register({
|
|
2238
|
+
"im.message.receive_v1": async (data2) => {
|
|
2239
|
+
const payload = { event: data2 };
|
|
2240
|
+
if (options.questionHandler && isFeishuMessageAddressedToBot(payload)) {
|
|
2241
|
+
const platformMessageId = data2?.message?.message_id;
|
|
2242
|
+
if (platformMessageId && answeredMessageIds.has(platformMessageId)) {
|
|
2243
|
+
console.log("\u98DE\u4E66\u63D0\u95EE\u91CD\u590D\u6295\u9012\uFF1A\u5DF2\u8DF3\u8FC7\u56DE\u7B54\u3002");
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const decision = getFeishuQuestionDecision(payload, options.config);
|
|
2247
|
+
if (decision.shouldAnswer) {
|
|
2248
|
+
if (platformMessageId) {
|
|
2249
|
+
answeredMessageIds.add(platformMessageId);
|
|
2250
|
+
}
|
|
2251
|
+
await options.questionHandler.handle(payload);
|
|
2252
|
+
console.log("\u98DE\u4E66\u63D0\u95EE\u5DF2\u56DE\u7B54\uFF1A\u8DF3\u8FC7\u77E5\u8BC6\u5E93\u5165\u5E93\u3002");
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const result = options.resourceDownloader ? await options.ingestor.ingestFeishuEventAndDownloadAttachments({
|
|
2257
|
+
payload,
|
|
2258
|
+
downloader: options.resourceDownloader,
|
|
2259
|
+
config: options.config,
|
|
2260
|
+
vectorIndexMessage: options.attachmentVectorIndexer
|
|
2261
|
+
}) : options.ingestor.ingestFeishuEvent(payload);
|
|
2262
|
+
if (!result.accepted) {
|
|
2263
|
+
console.log(`\u98DE\u4E66\u6D88\u606F\u672A\u5165\u5E93\uFF1A${result.reason}`);
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
console.log(`\u98DE\u4E66\u6D88\u606F\u5DF2\u5165\u5E93\uFF1A${result.messageId}`);
|
|
2267
|
+
if (result.duplicate) {
|
|
2268
|
+
console.log("\u98DE\u4E66\u6D88\u606F\u91CD\u590D\u6295\u9012\uFF1A\u5DF2\u8DF3\u8FC7\u9644\u4EF6\u5904\u7406\u548C\u56DE\u7B54\u3002");
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
if (result.attachment?.downloaded) {
|
|
2272
|
+
console.log(`\u98DE\u4E66\u9644\u4EF6\u5DF2\u4E0B\u8F7D\uFF1A${result.attachment.downloaded.storedPath}`);
|
|
2273
|
+
if (result.attachment.indexedMessageId) {
|
|
2274
|
+
console.log(`\u98DE\u4E66\u9644\u4EF6\u5DF2\u8FDB\u5165 RAG\uFF1A${result.attachment.indexedMessageId}`);
|
|
2275
|
+
if (result.attachment.vectorIndexed) {
|
|
2276
|
+
console.log(
|
|
2277
|
+
`\u98DE\u4E66\u9644\u4EF6\u5411\u91CF\u7D22\u5F15\u5B8C\u6210\uFF1Achunks=${result.attachment.vectorIndexed.chunks}, vectors=${result.attachment.vectorIndexed.vectors}`
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
} else if (result.attachment.skippedReason) {
|
|
2281
|
+
console.log(`\u98DE\u4E66\u9644\u4EF6\u6682\u672A\u8FDB\u5165 RAG\uFF1A${result.attachment.skippedReason}`);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
if (options.questionHandler) {
|
|
2285
|
+
const decision = await options.questionHandler.handle(payload, {
|
|
2286
|
+
excludeMessageIds: result.messageId ? [result.messageId] : []
|
|
2287
|
+
});
|
|
2288
|
+
if (!decision.shouldAnswer) {
|
|
2289
|
+
console.log(`\u98DE\u4E66\u6D88\u606F\u4E0D\u89E6\u53D1\u56DE\u7B54\uFF1A${decision.reason}`);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
function createFeishuGateway(options) {
|
|
2296
|
+
assertFeishuConfig(options.config, options.secrets);
|
|
2297
|
+
const wsClient = options.wsClientFactory?.({
|
|
2298
|
+
appId: options.config.feishu.appId,
|
|
2299
|
+
appSecret: options.secrets.feishu.appSecret,
|
|
2300
|
+
domain: mapDomain(options.config.feishu.domain),
|
|
2301
|
+
onReady: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u5EFA\u7ACB\u3002"),
|
|
2302
|
+
onError: (error) => console.error(`\u98DE\u4E66\u957F\u8FDE\u63A5\u9519\u8BEF\uFF1A${error.message}`),
|
|
2303
|
+
onReconnecting: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u6B63\u5728\u91CD\u8FDE\u3002"),
|
|
2304
|
+
onReconnected: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u91CD\u8FDE\u3002")
|
|
2305
|
+
}) ?? new lark2.WSClient({
|
|
2306
|
+
appId: options.config.feishu.appId,
|
|
2307
|
+
appSecret: options.secrets.feishu.appSecret,
|
|
2308
|
+
domain: mapDomain(options.config.feishu.domain),
|
|
2309
|
+
loggerLevel: lark2.LoggerLevel.info,
|
|
2310
|
+
source: "chattercatcher",
|
|
2311
|
+
onReady: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u5EFA\u7ACB\u3002"),
|
|
2312
|
+
onError: (error) => console.error(`\u98DE\u4E66\u957F\u8FDE\u63A5\u9519\u8BEF\uFF1A${error.message}`),
|
|
2313
|
+
onReconnecting: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u6B63\u5728\u91CD\u8FDE\u3002"),
|
|
2314
|
+
onReconnected: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u91CD\u8FDE\u3002")
|
|
2315
|
+
});
|
|
2316
|
+
const eventDispatcher = createFeishuEventDispatcher({
|
|
2317
|
+
config: options.config,
|
|
2318
|
+
ingestor: options.ingestor,
|
|
2319
|
+
questionHandler: options.questionHandler,
|
|
2320
|
+
resourceDownloader: options.resourceDownloader,
|
|
2321
|
+
attachmentVectorIndexer: options.attachmentVectorIndexer
|
|
2322
|
+
});
|
|
2323
|
+
return {
|
|
2324
|
+
async start() {
|
|
2325
|
+
await wsClient.start({ eventDispatcher });
|
|
2326
|
+
},
|
|
2327
|
+
stop() {
|
|
2328
|
+
wsClient.close({ force: true });
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/feishu/resource-downloader.ts
|
|
2334
|
+
init_paths();
|
|
2335
|
+
import * as lark3 from "@larksuiteoapi/node-sdk";
|
|
2336
|
+
import fs10 from "fs/promises";
|
|
2337
|
+
import path12 from "path";
|
|
2338
|
+
var RESOURCE_TYPE_BY_KIND = {
|
|
2339
|
+
file: "file",
|
|
2340
|
+
image: "image",
|
|
2341
|
+
audio: "audio",
|
|
2342
|
+
media: "media"
|
|
2343
|
+
};
|
|
2344
|
+
var DEFAULT_EXTENSION_BY_KIND = {
|
|
2345
|
+
file: ".bin",
|
|
2346
|
+
image: ".jpg",
|
|
2347
|
+
audio: ".mp3",
|
|
2348
|
+
media: ".mp4"
|
|
2349
|
+
};
|
|
2350
|
+
function sanitizeFileName(value) {
|
|
2351
|
+
const sanitized = value.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_").trim();
|
|
2352
|
+
return sanitized || "feishu-resource";
|
|
2353
|
+
}
|
|
2354
|
+
function buildStoredFileName(input2) {
|
|
2355
|
+
const rawName = input2.attachment.fileName || `${input2.attachment.fileKey}${DEFAULT_EXTENSION_BY_KIND[input2.attachment.kind]}`;
|
|
2356
|
+
return `${input2.messageId}-${sanitizeFileName(rawName)}`;
|
|
2357
|
+
}
|
|
2358
|
+
var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
2359
|
+
constructor(client, dataDir) {
|
|
2360
|
+
this.client = client;
|
|
2361
|
+
this.dataDir = dataDir;
|
|
2362
|
+
}
|
|
2363
|
+
client;
|
|
2364
|
+
dataDir;
|
|
2365
|
+
static fromConfig(config, secrets) {
|
|
2366
|
+
const client = new lark3.Client({
|
|
2367
|
+
appId: config.feishu.appId,
|
|
2368
|
+
appSecret: secrets.feishu.appSecret,
|
|
2369
|
+
domain: mapDomain(config.feishu.domain)
|
|
2370
|
+
});
|
|
2371
|
+
return new _FeishuResourceDownloader(client, resolveHomePath(config.storage.dataDir));
|
|
2372
|
+
}
|
|
2373
|
+
async download(input2) {
|
|
2374
|
+
const resourceType = RESOURCE_TYPE_BY_KIND[input2.attachment.kind];
|
|
2375
|
+
const targetDir = path12.join(this.dataDir, "files", "feishu");
|
|
2376
|
+
await fs10.mkdir(targetDir, { recursive: true });
|
|
2377
|
+
const fileName = buildStoredFileName(input2);
|
|
2378
|
+
const storedPath = path12.join(targetDir, fileName);
|
|
2379
|
+
const payload = {
|
|
2380
|
+
params: { type: resourceType },
|
|
2381
|
+
path: { message_id: input2.messageId, file_key: input2.attachment.fileKey }
|
|
2382
|
+
};
|
|
2383
|
+
const api = this.client.im.v1?.messageResource?.get ?? this.client.im.messageResource?.get;
|
|
2384
|
+
if (!api) {
|
|
2385
|
+
throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301 messageResource.get\uFF0C\u65E0\u6CD5\u4E0B\u8F7D\u6D88\u606F\u8D44\u6E90\u3002");
|
|
2386
|
+
}
|
|
2387
|
+
const response = await api(payload);
|
|
2388
|
+
await response.writeFile(storedPath);
|
|
2389
|
+
return {
|
|
2390
|
+
messageId: input2.messageId,
|
|
2391
|
+
fileKey: input2.attachment.fileKey,
|
|
2392
|
+
fileName,
|
|
2393
|
+
resourceType,
|
|
2394
|
+
storedPath
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2399
|
+
// src/files/ingest.ts
|
|
2400
|
+
init_paths();
|
|
2401
|
+
import crypto3 from "crypto";
|
|
2402
|
+
import fs12 from "fs/promises";
|
|
2403
|
+
import path14 from "path";
|
|
2404
|
+
|
|
2405
|
+
// src/files/parser.ts
|
|
2406
|
+
import fs11 from "fs/promises";
|
|
2407
|
+
import path13 from "path";
|
|
2408
|
+
import mammoth from "mammoth";
|
|
2409
|
+
import { PDFParse } from "pdf-parse";
|
|
2410
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".txt", ".md", ".markdown", ".json", ".csv", ".tsv", ".log"]);
|
|
2411
|
+
var DOCX_EXTENSIONS = /* @__PURE__ */ new Set([".docx"]);
|
|
2412
|
+
var PDF_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
|
|
2413
|
+
function isSupportedParseFile(filePath) {
|
|
2414
|
+
const extension = path13.extname(filePath).toLowerCase();
|
|
2415
|
+
return TEXT_EXTENSIONS.has(extension) || DOCX_EXTENSIONS.has(extension) || PDF_EXTENSIONS.has(extension);
|
|
2416
|
+
}
|
|
2417
|
+
function describeSupportedParseTypes() {
|
|
2418
|
+
return "txt\u3001md\u3001json\u3001csv\u3001tsv\u3001log\u3001docx\u3001pdf";
|
|
2419
|
+
}
|
|
2420
|
+
async function parseFileToText(filePath) {
|
|
2421
|
+
const extension = path13.extname(filePath).toLowerCase();
|
|
2422
|
+
if (TEXT_EXTENSIONS.has(extension)) {
|
|
2423
|
+
return {
|
|
2424
|
+
text: await fs11.readFile(filePath, "utf8"),
|
|
2425
|
+
parser: "text",
|
|
2426
|
+
warnings: []
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
if (DOCX_EXTENSIONS.has(extension)) {
|
|
2430
|
+
const result = await mammoth.extractRawText({ path: filePath });
|
|
2431
|
+
return {
|
|
2432
|
+
text: result.value,
|
|
2433
|
+
parser: "docx",
|
|
2434
|
+
warnings: result.messages.map((message) => message.message)
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
if (PDF_EXTENSIONS.has(extension)) {
|
|
2438
|
+
const buffer = await fs11.readFile(filePath);
|
|
2439
|
+
const parser = new PDFParse({ data: buffer });
|
|
2440
|
+
try {
|
|
2441
|
+
const result = await parser.getText();
|
|
2442
|
+
return {
|
|
2443
|
+
text: result.text,
|
|
2444
|
+
parser: "pdf",
|
|
2445
|
+
warnings: []
|
|
2446
|
+
};
|
|
2447
|
+
} finally {
|
|
2448
|
+
await parser.destroy();
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
throw new Error(`\u6682\u4E0D\u652F\u6301\u8BE5\u6587\u4EF6\u7C7B\u578B\uFF1A${extension || "\u65E0\u6269\u5C55\u540D"}\u3002\u5F53\u524D\u652F\u6301 ${describeSupportedParseTypes()}\u3002`);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/files/ingest.ts
|
|
2455
|
+
function isSupportedTextFile(filePath) {
|
|
2456
|
+
return isSupportedParseFile(filePath);
|
|
2457
|
+
}
|
|
2458
|
+
function ensureSupportedTextFile(filePath) {
|
|
2459
|
+
if (!isSupportedTextFile(filePath)) {
|
|
2460
|
+
const extension = path14.extname(filePath).toLowerCase();
|
|
2461
|
+
throw new Error(`\u6682\u4E0D\u652F\u6301\u8BE5\u6587\u4EF6\u7C7B\u578B\uFF1A${extension || "\u65E0\u6269\u5C55\u540D"}\u3002\u5F53\u524D\u652F\u6301 ${describeSupportedParseTypes()}\u3002`);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
function stableStoredName(sourcePath, fileName) {
|
|
2465
|
+
const digest = crypto3.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
2466
|
+
return `${digest}-${fileName}`;
|
|
2467
|
+
}
|
|
2468
|
+
async function ingestLocalFile(input2) {
|
|
2469
|
+
const sourcePath = path14.resolve(input2.filePath);
|
|
2470
|
+
const fileName = path14.basename(sourcePath);
|
|
2471
|
+
const jobId = input2.jobs?.start({ sourcePath, fileName });
|
|
2472
|
+
try {
|
|
2473
|
+
ensureSupportedTextFile(sourcePath);
|
|
2474
|
+
const stat = await fs12.stat(sourcePath);
|
|
2475
|
+
if (!stat.isFile()) {
|
|
2476
|
+
throw new Error(`\u4E0D\u662F\u6587\u4EF6\uFF1A${sourcePath}`);
|
|
2477
|
+
}
|
|
2478
|
+
const parsed = await parseFileToText(sourcePath);
|
|
2479
|
+
const text = parsed.text.trim();
|
|
2480
|
+
if (!text) {
|
|
2481
|
+
throw new Error(`\u6587\u4EF6\u6CA1\u6709\u53EF\u7D22\u5F15\u6587\u672C\uFF1A${sourcePath}`);
|
|
2482
|
+
}
|
|
2483
|
+
const fileDir = path14.join(resolveHomePath(input2.config.storage.dataDir), "files");
|
|
2484
|
+
await fs12.mkdir(fileDir, { recursive: true });
|
|
2485
|
+
const storedPath = path14.join(fileDir, stableStoredName(sourcePath, fileName));
|
|
2486
|
+
await fs12.copyFile(sourcePath, storedPath);
|
|
2487
|
+
const messageId = input2.messages.ingest({
|
|
2488
|
+
platform: "local-file",
|
|
2489
|
+
platformChatId: "local-files",
|
|
2490
|
+
chatName: "\u6587\u4EF6\u5E93",
|
|
2491
|
+
platformMessageId: sourcePath,
|
|
2492
|
+
senderId: "local-file",
|
|
2493
|
+
senderName: fileName,
|
|
2494
|
+
messageType: "file",
|
|
2495
|
+
text,
|
|
2496
|
+
sentAt: stat.mtime.toISOString(),
|
|
2497
|
+
rawPayload: {
|
|
2498
|
+
sourcePath,
|
|
2499
|
+
storedPath,
|
|
2500
|
+
bytes: stat.size,
|
|
2501
|
+
fileName,
|
|
2502
|
+
parser: parsed.parser,
|
|
2503
|
+
parserWarnings: parsed.warnings
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
input2.jobs?.complete({
|
|
2507
|
+
id: jobId ?? "",
|
|
2508
|
+
storedPath,
|
|
2509
|
+
parser: parsed.parser,
|
|
2510
|
+
messageId,
|
|
2511
|
+
bytes: stat.size,
|
|
2512
|
+
characters: text.length,
|
|
2513
|
+
warnings: parsed.warnings
|
|
2514
|
+
});
|
|
2515
|
+
return {
|
|
2516
|
+
messageId,
|
|
2517
|
+
sourcePath,
|
|
2518
|
+
storedPath,
|
|
2519
|
+
fileName,
|
|
2520
|
+
bytes: stat.size,
|
|
2521
|
+
characters: text.length,
|
|
2522
|
+
parser: parsed.parser,
|
|
2523
|
+
warnings: parsed.warnings,
|
|
2524
|
+
jobId
|
|
2525
|
+
};
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
if (jobId) {
|
|
2528
|
+
input2.jobs?.fail({
|
|
2529
|
+
id: jobId,
|
|
2530
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
throw error;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/feishu/normalize.ts
|
|
2538
|
+
function asObject2(value) {
|
|
2539
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2540
|
+
}
|
|
2541
|
+
function parseContent(content) {
|
|
2542
|
+
if (!content) {
|
|
2543
|
+
return {};
|
|
2544
|
+
}
|
|
2545
|
+
try {
|
|
2546
|
+
return asObject2(JSON.parse(content));
|
|
2547
|
+
} catch {
|
|
2548
|
+
return { text: content };
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
function stringifyUnknown(value) {
|
|
2552
|
+
if (typeof value === "string") {
|
|
2553
|
+
return value;
|
|
2554
|
+
}
|
|
2555
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2556
|
+
return String(value);
|
|
2557
|
+
}
|
|
2558
|
+
return "";
|
|
2559
|
+
}
|
|
2560
|
+
function numberUnknown(value) {
|
|
2561
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2562
|
+
return value;
|
|
2563
|
+
}
|
|
2564
|
+
if (typeof value === "string") {
|
|
2565
|
+
const parsed = Number(value);
|
|
2566
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2567
|
+
}
|
|
2568
|
+
return void 0;
|
|
2569
|
+
}
|
|
2570
|
+
function extractPostText(content) {
|
|
2571
|
+
const post = asObject2(content.post);
|
|
2572
|
+
const zhCn = asObject2(post.zh_cn ?? post["zh-CN"] ?? post.en_us ?? post["en-US"]);
|
|
2573
|
+
const title = stringifyUnknown(zhCn.title);
|
|
2574
|
+
const blocks = Array.isArray(zhCn.content) ? zhCn.content : [];
|
|
2575
|
+
const parts = [];
|
|
2576
|
+
if (title) {
|
|
2577
|
+
parts.push(title);
|
|
2578
|
+
}
|
|
2579
|
+
for (const block of blocks) {
|
|
2580
|
+
if (!Array.isArray(block)) {
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
for (const item of block) {
|
|
2584
|
+
const object = asObject2(item);
|
|
2585
|
+
const text = stringifyUnknown(object.text);
|
|
2586
|
+
if (text) {
|
|
2587
|
+
parts.push(text);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return parts.join(" ").trim();
|
|
2592
|
+
}
|
|
2593
|
+
function extractFeishuAttachment(messageType, content) {
|
|
2594
|
+
if (messageType === "image") {
|
|
2595
|
+
const fileKey2 = stringifyUnknown(content.image_key);
|
|
2596
|
+
return fileKey2 ? { platform: "feishu", kind: "image", fileKey: fileKey2 } : void 0;
|
|
2597
|
+
}
|
|
2598
|
+
if (messageType === "audio") {
|
|
2599
|
+
const fileKey2 = stringifyUnknown(content.file_key);
|
|
2600
|
+
return fileKey2 ? { platform: "feishu", kind: "audio", fileKey: fileKey2 } : void 0;
|
|
2601
|
+
}
|
|
2602
|
+
if (messageType !== "file" && messageType !== "media") {
|
|
2603
|
+
return void 0;
|
|
2604
|
+
}
|
|
2605
|
+
const fileKey = stringifyUnknown(content.file_key);
|
|
2606
|
+
if (!fileKey) {
|
|
2607
|
+
return void 0;
|
|
2608
|
+
}
|
|
2609
|
+
return {
|
|
2610
|
+
platform: "feishu",
|
|
2611
|
+
kind: messageType,
|
|
2612
|
+
fileKey,
|
|
2613
|
+
fileName: stringifyUnknown(content.file_name) || void 0,
|
|
2614
|
+
mimeType: stringifyUnknown(content.mime_type) || void 0,
|
|
2615
|
+
size: numberUnknown(content.file_size ?? content.size)
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
function extractMessageText(messageType, content) {
|
|
2619
|
+
if (messageType === "text") {
|
|
2620
|
+
return stringifyUnknown(content.text).trim();
|
|
2621
|
+
}
|
|
2622
|
+
if (messageType === "post") {
|
|
2623
|
+
return extractPostText(content);
|
|
2624
|
+
}
|
|
2625
|
+
if (messageType === "image") {
|
|
2626
|
+
return `[\u56FE\u7247] ${stringifyUnknown(content.image_key)}`.trim();
|
|
2627
|
+
}
|
|
2628
|
+
if (messageType === "file") {
|
|
2629
|
+
return `[\u6587\u4EF6] ${stringifyUnknown(content.file_name) || stringifyUnknown(content.file_key)}`.trim();
|
|
2630
|
+
}
|
|
2631
|
+
if (messageType === "audio") {
|
|
2632
|
+
return `[\u8BED\u97F3] ${stringifyUnknown(content.file_key)}`.trim();
|
|
2633
|
+
}
|
|
2634
|
+
if (messageType === "media") {
|
|
2635
|
+
return `[\u5A92\u4F53] ${stringifyUnknown(content.file_name) || stringifyUnknown(content.file_key)}`.trim();
|
|
2636
|
+
}
|
|
2637
|
+
const fallback = Object.entries(content).map(([key, value]) => `${key}: ${stringifyUnknown(value)}`).filter((line) => !line.endsWith(": ")).join(" ");
|
|
2638
|
+
return fallback || `[${messageType}]`;
|
|
2639
|
+
}
|
|
2640
|
+
function normalizeTimestamp(createTime) {
|
|
2641
|
+
if (!createTime) {
|
|
2642
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2643
|
+
}
|
|
2644
|
+
const numeric = Number(createTime);
|
|
2645
|
+
if (Number.isFinite(numeric)) {
|
|
2646
|
+
const milliseconds = createTime.length <= 10 ? numeric * 1e3 : numeric;
|
|
2647
|
+
return new Date(milliseconds).toISOString();
|
|
2648
|
+
}
|
|
2649
|
+
const date = new Date(createTime);
|
|
2650
|
+
if (Number.isNaN(date.getTime())) {
|
|
2651
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2652
|
+
}
|
|
2653
|
+
return date.toISOString();
|
|
2654
|
+
}
|
|
2655
|
+
function normalizeFeishuReceiveMessageEvent(payload) {
|
|
2656
|
+
const event = payload.event;
|
|
2657
|
+
const message = event?.message;
|
|
2658
|
+
if (!event || !message?.message_id || !message.chat_id) {
|
|
2659
|
+
return null;
|
|
2660
|
+
}
|
|
2661
|
+
const messageType = message.message_type || "unknown";
|
|
2662
|
+
const content = parseContent(message.content);
|
|
2663
|
+
const text = extractMessageText(messageType, content);
|
|
2664
|
+
if (!text) {
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
const senderId = event.sender?.sender_id?.open_id || event.sender?.sender_id?.user_id || event.sender?.sender_id?.union_id || "unknown";
|
|
2668
|
+
return {
|
|
2669
|
+
platform: "feishu",
|
|
2670
|
+
platformChatId: message.chat_id,
|
|
2671
|
+
chatName: message.chat_id,
|
|
2672
|
+
platformMessageId: message.message_id,
|
|
2673
|
+
senderId,
|
|
2674
|
+
senderName: senderId,
|
|
2675
|
+
messageType,
|
|
2676
|
+
text,
|
|
2677
|
+
sentAt: normalizeTimestamp(message.create_time),
|
|
2678
|
+
rawPayload: {
|
|
2679
|
+
platform: "feishu",
|
|
2680
|
+
raw: payload,
|
|
2681
|
+
content,
|
|
2682
|
+
attachment: extractFeishuAttachment(messageType, content)
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
55
2686
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
2687
|
+
// src/gateway/ingest.ts
|
|
2688
|
+
function extractAttachment(message) {
|
|
2689
|
+
const raw = message.rawPayload;
|
|
2690
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2691
|
+
return void 0;
|
|
2692
|
+
}
|
|
2693
|
+
const attachment = raw.attachment;
|
|
2694
|
+
if (!attachment || typeof attachment !== "object" || Array.isArray(attachment)) {
|
|
2695
|
+
return void 0;
|
|
2696
|
+
}
|
|
2697
|
+
const candidate = attachment;
|
|
2698
|
+
if (candidate.platform !== "feishu" || !candidate.kind || !candidate.fileKey) {
|
|
2699
|
+
return void 0;
|
|
2700
|
+
}
|
|
2701
|
+
return candidate;
|
|
2702
|
+
}
|
|
2703
|
+
var GatewayIngestor = class {
|
|
2704
|
+
messages;
|
|
2705
|
+
jobs;
|
|
2706
|
+
constructor(database) {
|
|
2707
|
+
this.messages = new MessageRepository(database);
|
|
2708
|
+
this.jobs = new FileJobRepository(database);
|
|
2709
|
+
}
|
|
2710
|
+
ingestFeishuEvent(payload) {
|
|
2711
|
+
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
2712
|
+
if (!normalized) {
|
|
2713
|
+
return {
|
|
2714
|
+
accepted: false,
|
|
2715
|
+
reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
const duplicate = this.messages.hasPlatformMessage(normalized.platform, normalized.platformMessageId);
|
|
2719
|
+
const messageId = this.messages.ingest(normalized);
|
|
2720
|
+
return {
|
|
2721
|
+
accepted: true,
|
|
2722
|
+
messageId,
|
|
2723
|
+
message: normalized,
|
|
2724
|
+
duplicate
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
async ingestFeishuEventAndDownloadAttachments(input2) {
|
|
2728
|
+
const result = this.ingestFeishuEvent(input2.payload);
|
|
2729
|
+
if (!result.accepted || !result.messageId || !result.message || result.duplicate) {
|
|
2730
|
+
return result;
|
|
2731
|
+
}
|
|
2732
|
+
const attachment = extractAttachment(result.message);
|
|
2733
|
+
if (!attachment) {
|
|
2734
|
+
return result;
|
|
2735
|
+
}
|
|
2736
|
+
const downloaded = await input2.downloader.download({
|
|
2737
|
+
messageId: result.message.platformMessageId,
|
|
2738
|
+
attachment
|
|
2739
|
+
});
|
|
2740
|
+
if (!isSupportedTextFile(downloaded.storedPath)) {
|
|
2741
|
+
return {
|
|
2742
|
+
...result,
|
|
2743
|
+
attachment: {
|
|
2744
|
+
downloaded,
|
|
2745
|
+
skippedReason: "\u9644\u4EF6\u5DF2\u4E0B\u8F7D\uFF0C\u4F46\u5F53\u524D\u6587\u4EF6\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u89E3\u6790\u3002"
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
const indexedMessageId = await ingestLocalFile({
|
|
2750
|
+
config: input2.config,
|
|
2751
|
+
messages: this.messages,
|
|
2752
|
+
jobs: this.jobs,
|
|
2753
|
+
filePath: downloaded.storedPath
|
|
2754
|
+
}).then((file) => file.messageId);
|
|
2755
|
+
const vectorIndexed = input2.vectorIndexMessage ? await input2.vectorIndexMessage(indexedMessageId) : void 0;
|
|
2756
|
+
return {
|
|
2757
|
+
...result,
|
|
2758
|
+
attachment: {
|
|
2759
|
+
downloaded,
|
|
2760
|
+
indexedMessageId,
|
|
2761
|
+
vectorIndexed
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
122
2764
|
}
|
|
123
2765
|
};
|
|
124
2766
|
|
|
2767
|
+
// src/gateway/detached.ts
|
|
2768
|
+
import { spawn } from "child_process";
|
|
2769
|
+
import fs13 from "fs";
|
|
2770
|
+
import path15 from "path";
|
|
2771
|
+
function buildGatewayForegroundSpawnCommand(argv = process.argv) {
|
|
2772
|
+
const [command = process.execPath, ...rawArgs] = argv;
|
|
2773
|
+
const args = [...rawArgs];
|
|
2774
|
+
while (args.at(-1) === "--foreground") {
|
|
2775
|
+
args.pop();
|
|
2776
|
+
}
|
|
2777
|
+
if (args.at(-1) === "start" && args.at(-2) === "gateway") {
|
|
2778
|
+
args.splice(-2, 2);
|
|
2779
|
+
}
|
|
2780
|
+
return {
|
|
2781
|
+
command,
|
|
2782
|
+
args: [...args, "gateway", "start", "--foreground"]
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
function startDetachedGateway(input2) {
|
|
2786
|
+
const status = getGatewayStatus(input2.config, input2.secrets);
|
|
2787
|
+
const logFile = getGatewayLogPath();
|
|
2788
|
+
if (status.connection === "running") {
|
|
2789
|
+
return {
|
|
2790
|
+
started: false,
|
|
2791
|
+
message: `\u98DE\u4E66 Gateway \u5DF2\u7ECF\u6B63\u5728\u8FD0\u884C\uFF1Apid=${status.pid ?? "unknown"}`,
|
|
2792
|
+
logFile,
|
|
2793
|
+
...status.pid ? { pid: status.pid } : {}
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
fs13.mkdirSync(path15.dirname(logFile), { recursive: true });
|
|
2797
|
+
let out;
|
|
2798
|
+
let err;
|
|
2799
|
+
let stdioClosed = false;
|
|
2800
|
+
const closeStdio = () => {
|
|
2801
|
+
if (stdioClosed) {
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
stdioClosed = true;
|
|
2805
|
+
if (typeof out === "number") {
|
|
2806
|
+
fs13.closeSync(out);
|
|
2807
|
+
}
|
|
2808
|
+
if (typeof err === "number") {
|
|
2809
|
+
fs13.closeSync(err);
|
|
2810
|
+
}
|
|
2811
|
+
};
|
|
2812
|
+
try {
|
|
2813
|
+
out = fs13.openSync(logFile, "a");
|
|
2814
|
+
err = fs13.openSync(logFile, "a");
|
|
2815
|
+
const foreground = buildGatewayForegroundSpawnCommand(input2.argv);
|
|
2816
|
+
const child = spawn(foreground.command, foreground.args, {
|
|
2817
|
+
detached: true,
|
|
2818
|
+
stdio: ["ignore", out, err],
|
|
2819
|
+
windowsHide: true
|
|
2820
|
+
});
|
|
2821
|
+
closeStdio();
|
|
2822
|
+
child.unref();
|
|
2823
|
+
return {
|
|
2824
|
+
started: true,
|
|
2825
|
+
message: `\u5DF2\u5728\u540E\u53F0\u542F\u52A8\u98DE\u4E66 Gateway\uFF1Apid=${child.pid}`,
|
|
2826
|
+
pid: child.pid,
|
|
2827
|
+
logFile
|
|
2828
|
+
};
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
closeStdio();
|
|
2831
|
+
throw error;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/rag/indexer.ts
|
|
2836
|
+
async function indexMessageChunks(input2) {
|
|
2837
|
+
const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
|
|
2838
|
+
if (chunks.length === 0) {
|
|
2839
|
+
return { chunks: 0, vectors: 0 };
|
|
2840
|
+
}
|
|
2841
|
+
const vectors = await input2.embedding.embedBatch(chunks.map((chunk) => chunk.text));
|
|
2842
|
+
const records = [];
|
|
2843
|
+
for (const [index2, chunk] of chunks.entries()) {
|
|
2844
|
+
const vector = vectors[index2];
|
|
2845
|
+
if (!vector || vector.length === 0) {
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
records.push({
|
|
2849
|
+
id: chunk.chunkId,
|
|
2850
|
+
vector,
|
|
2851
|
+
evidence: {
|
|
2852
|
+
id: chunk.chunkId,
|
|
2853
|
+
text: chunk.text,
|
|
2854
|
+
score: 1,
|
|
2855
|
+
source: toEvidenceSource2(chunk)
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
await input2.store.upsert(records);
|
|
2860
|
+
return {
|
|
2861
|
+
chunks: chunks.length,
|
|
2862
|
+
vectors: records.length
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
function toEvidenceSource2(chunk) {
|
|
2866
|
+
if (chunk.messageType === "file") {
|
|
2867
|
+
return {
|
|
2868
|
+
type: "file",
|
|
2869
|
+
label: chunk.senderName,
|
|
2870
|
+
timestamp: chunk.sentAt
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
return {
|
|
2874
|
+
type: "message",
|
|
2875
|
+
label: chunk.chatName,
|
|
2876
|
+
sender: chunk.senderName,
|
|
2877
|
+
timestamp: chunk.sentAt
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// src/rag/manual-index.ts
|
|
2882
|
+
async function processMessagesNow(input2) {
|
|
2883
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2884
|
+
if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
|
|
2885
|
+
return {
|
|
2886
|
+
status: "skipped",
|
|
2887
|
+
reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
|
|
2888
|
+
chunks: 0,
|
|
2889
|
+
vectors: 0,
|
|
2890
|
+
startedAt,
|
|
2891
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
const { LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
|
|
2895
|
+
const vectorStore = await LanceDbVectorStore2.connectFromConfig(input2.config);
|
|
2896
|
+
try {
|
|
2897
|
+
const stats = await indexMessageChunks({
|
|
2898
|
+
messages: new MessageRepository(input2.database),
|
|
2899
|
+
embedding: createEmbeddingModel(input2.config, input2.secrets),
|
|
2900
|
+
store: vectorStore,
|
|
2901
|
+
limit: input2.limit
|
|
2902
|
+
});
|
|
2903
|
+
return {
|
|
2904
|
+
status: "completed",
|
|
2905
|
+
chunks: stats.chunks,
|
|
2906
|
+
vectors: stats.vectors,
|
|
2907
|
+
startedAt,
|
|
2908
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2909
|
+
};
|
|
2910
|
+
} finally {
|
|
2911
|
+
vectorStore.close();
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// src/web/server.ts
|
|
2916
|
+
import Fastify from "fastify";
|
|
2917
|
+
function buildHtml() {
|
|
2918
|
+
return `<!doctype html>
|
|
2919
|
+
<html lang="zh-CN">
|
|
2920
|
+
<head>
|
|
2921
|
+
<meta charset="utf-8" />
|
|
2922
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2923
|
+
<title>ChatterCatcher</title>
|
|
2924
|
+
<style>
|
|
2925
|
+
:root {
|
|
2926
|
+
color-scheme: light;
|
|
2927
|
+
--bg: #f6f5f0;
|
|
2928
|
+
--panel: #ffffff;
|
|
2929
|
+
--text: #1f2933;
|
|
2930
|
+
--muted: #667085;
|
|
2931
|
+
--line: #d9d7cf;
|
|
2932
|
+
--accent: #1f7a5a;
|
|
2933
|
+
--warn: #9a5b13;
|
|
2934
|
+
}
|
|
2935
|
+
* { box-sizing: border-box; }
|
|
2936
|
+
body {
|
|
2937
|
+
margin: 0;
|
|
2938
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2939
|
+
background: var(--bg);
|
|
2940
|
+
color: var(--text);
|
|
2941
|
+
}
|
|
2942
|
+
main { max-width: 1120px; margin: 0 auto; padding: 32px 24px 48px; overflow-x: hidden; }
|
|
2943
|
+
header {
|
|
2944
|
+
display: flex;
|
|
2945
|
+
justify-content: space-between;
|
|
2946
|
+
gap: 20px;
|
|
2947
|
+
align-items: flex-start;
|
|
2948
|
+
padding-bottom: 24px;
|
|
2949
|
+
border-bottom: 1px solid var(--line);
|
|
2950
|
+
}
|
|
2951
|
+
h1 { margin: 0; font-size: 30px; line-height: 1.1; letter-spacing: 0; }
|
|
2952
|
+
h2 { margin: 0 0 12px; font-size: 18px; letter-spacing: 0; }
|
|
2953
|
+
p { margin: 8px 0 0; color: var(--muted); }
|
|
2954
|
+
code { background: #eceae2; border-radius: 4px; padding: 2px 6px; }
|
|
2955
|
+
button {
|
|
2956
|
+
appearance: none;
|
|
2957
|
+
border: 1px solid var(--line);
|
|
2958
|
+
background: var(--panel);
|
|
2959
|
+
color: var(--text);
|
|
2960
|
+
border-radius: 6px;
|
|
2961
|
+
padding: 8px 12px;
|
|
2962
|
+
cursor: pointer;
|
|
2963
|
+
}
|
|
2964
|
+
button:hover { border-color: var(--accent); }
|
|
2965
|
+
.actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
|
|
2966
|
+
.grid {
|
|
2967
|
+
display: grid;
|
|
2968
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
2969
|
+
gap: 12px;
|
|
2970
|
+
margin: 24px 0;
|
|
2971
|
+
}
|
|
2972
|
+
.metric {
|
|
2973
|
+
background: var(--panel);
|
|
2974
|
+
border: 1px solid var(--line);
|
|
2975
|
+
border-radius: 8px;
|
|
2976
|
+
padding: 16px;
|
|
2977
|
+
min-height: 112px;
|
|
2978
|
+
}
|
|
2979
|
+
.label { color: var(--muted); font-size: 13px; }
|
|
2980
|
+
.value { margin-top: 10px; font-size: 22px; font-weight: 650; overflow-wrap: anywhere; line-height: 1.18; }
|
|
2981
|
+
.note { margin-top: 8px; color: var(--muted); font-size: 13px; line-height: 1.45; }
|
|
2982
|
+
.layout {
|
|
2983
|
+
display: grid;
|
|
2984
|
+
grid-template-columns: minmax(0, 1fr) minmax(280px, 380px);
|
|
2985
|
+
gap: 24px;
|
|
2986
|
+
}
|
|
2987
|
+
.layout > * { min-width: 0; }
|
|
2988
|
+
section { padding: 20px 0; border-top: 1px solid var(--line); }
|
|
2989
|
+
section:first-child { border-top: 0; }
|
|
2990
|
+
.message-list { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
2991
|
+
.message-item { padding: 14px 16px; border-bottom: 1px solid var(--line); }
|
|
2992
|
+
.message-item:last-child { border-bottom: 0; }
|
|
2993
|
+
.message-meta { display: flex; flex-wrap: wrap; gap: 8px 14px; color: var(--muted); font-size: 13px; line-height: 1.4; }
|
|
2994
|
+
.message-body { margin-top: 8px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.55; }
|
|
2995
|
+
table { width: 100%; table-layout: fixed; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
2996
|
+
th, td { padding: 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; overflow: hidden; text-overflow: ellipsis; }
|
|
2997
|
+
th { color: var(--muted); font-size: 13px; font-weight: 600; }
|
|
2998
|
+
tr:last-child td { border-bottom: 0; }
|
|
2999
|
+
.message { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3000
|
+
.id-text, .path { display: block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-size: 13px; }
|
|
3001
|
+
.compact-table th:first-child, .compact-table td:first-child { width: 120px; }
|
|
3002
|
+
.compact-table th:nth-child(2), .compact-table td:nth-child(2) { width: 180px; }
|
|
3003
|
+
.status-ok { color: var(--accent); }
|
|
3004
|
+
.status-warn { color: var(--warn); }
|
|
3005
|
+
.empty { color: var(--muted); padding: 18px; background: var(--panel); border: 1px dashed var(--line); border-radius: 8px; }
|
|
3006
|
+
.status-line { margin-top: 10px; font-size: 13px; color: var(--muted); text-align: right; }
|
|
3007
|
+
@media (max-width: 900px) {
|
|
3008
|
+
header, .layout { display: block; }
|
|
3009
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
3010
|
+
header button { margin-top: 16px; }
|
|
3011
|
+
}
|
|
3012
|
+
@media (max-width: 560px) {
|
|
3013
|
+
main { padding: 24px 16px 36px; }
|
|
3014
|
+
.grid { grid-template-columns: 1fr; }
|
|
3015
|
+
}
|
|
3016
|
+
</style>
|
|
3017
|
+
</head>
|
|
3018
|
+
<body>
|
|
3019
|
+
<main>
|
|
3020
|
+
<header>
|
|
3021
|
+
<div>
|
|
3022
|
+
<h1>ChatterCatcher</h1>
|
|
3023
|
+
<p>\u672C\u5730\u4F18\u5148\u7684\u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u3002\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22 RAG \u8BC1\u636E\uFF0C\u4E0D\u5806\u53E0\u5168\u91CF\u4E0A\u4E0B\u6587\u3002</p>
|
|
3024
|
+
</div>
|
|
3025
|
+
<div>
|
|
3026
|
+
<div class="actions">
|
|
3027
|
+
<button id="process-messages" type="button">\u7ACB\u5373\u5904\u7406</button>
|
|
3028
|
+
</div>
|
|
3029
|
+
<div id="action-status" class="status-line"></div>
|
|
3030
|
+
</div>
|
|
3031
|
+
</header>
|
|
3032
|
+
|
|
3033
|
+
<div class="grid" id="metrics"></div>
|
|
3034
|
+
|
|
3035
|
+
<div class="layout">
|
|
3036
|
+
<div>
|
|
3037
|
+
<section>
|
|
3038
|
+
<h2>\u6700\u8FD1\u6D88\u606F</h2>
|
|
3039
|
+
<div id="messages" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
3040
|
+
</section>
|
|
3041
|
+
</div>
|
|
3042
|
+
<aside>
|
|
3043
|
+
<section>
|
|
3044
|
+
<h2>\u7FA4\u804A</h2>
|
|
3045
|
+
<div id="chats" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
3046
|
+
</section>
|
|
3047
|
+
<section>
|
|
3048
|
+
<h2>\u6587\u4EF6\u5E93</h2>
|
|
3049
|
+
<div id="files" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
3050
|
+
</section>
|
|
3051
|
+
<section>
|
|
3052
|
+
<h2>\u89E3\u6790\u4EFB\u52A1</h2>
|
|
3053
|
+
<div id="file-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
3054
|
+
</section>
|
|
3055
|
+
<section>
|
|
3056
|
+
<h2>\u672C\u5730\u64CD\u4F5C</h2>
|
|
3057
|
+
<p><code>chattercatcher settings</code> \u4FEE\u6539\u914D\u7F6E\u3002</p>
|
|
3058
|
+
<p><code>chattercatcher files add <path...></code> \u5BFC\u5165\u6587\u672C\u3001DOCX \u6216 PDF \u6587\u4EF6\u3002</p>
|
|
3059
|
+
<p><code>chattercatcher doctor</code> \u68C0\u67E5\u98DE\u4E66\u3001\u6A21\u578B\u3001RAG \u548C\u672C\u5730\u5B58\u50A8\u3002</p>
|
|
3060
|
+
</section>
|
|
3061
|
+
</aside>
|
|
3062
|
+
</div>
|
|
3063
|
+
</main>
|
|
3064
|
+
<script>
|
|
3065
|
+
const metrics = document.querySelector("#metrics");
|
|
3066
|
+
const messages = document.querySelector("#messages");
|
|
3067
|
+
const chats = document.querySelector("#chats");
|
|
3068
|
+
const files = document.querySelector("#files");
|
|
3069
|
+
const fileJobs = document.querySelector("#file-jobs");
|
|
3070
|
+
const processMessages = document.querySelector("#process-messages");
|
|
3071
|
+
const actionStatus = document.querySelector("#action-status");
|
|
3072
|
+
|
|
3073
|
+
function fmt(value) {
|
|
3074
|
+
return value == null || value === "" ? "-" : String(value);
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
function escapeHtml(value) {
|
|
3078
|
+
return fmt(value)
|
|
3079
|
+
.replaceAll("&", "&")
|
|
3080
|
+
.replaceAll("<", "<")
|
|
3081
|
+
.replaceAll(">", ">")
|
|
3082
|
+
.replaceAll('"', """);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
function isOpaqueId(value) {
|
|
3086
|
+
return /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(fmt(value));
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
function formatDateTime(value) {
|
|
3090
|
+
const date = new Date(value);
|
|
3091
|
+
if (Number.isNaN(date.getTime())) return fmt(value);
|
|
3092
|
+
const pad = (input) => String(input).padStart(2, "0");
|
|
3093
|
+
return [
|
|
3094
|
+
date.getFullYear(),
|
|
3095
|
+
pad(date.getMonth() + 1),
|
|
3096
|
+
pad(date.getDate()),
|
|
3097
|
+
].join("/") + " " + [
|
|
3098
|
+
pad(date.getHours()),
|
|
3099
|
+
pad(date.getMinutes()),
|
|
3100
|
+
pad(date.getSeconds()),
|
|
3101
|
+
].join(":");
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
function displaySender(value) {
|
|
3105
|
+
return isOpaqueId(value) ? "\u7FA4\u6210\u5458" : fmt(value);
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
function displayChatName(value, platform) {
|
|
3109
|
+
if (!isOpaqueId(value)) return fmt(value);
|
|
3110
|
+
return platform === "feishu" ? "\u98DE\u4E66\u7FA4\u804A" : "\u7FA4\u804A";
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
function formatGatewayValue(gateway) {
|
|
3114
|
+
if (gateway.connection === "running") return "\u8FD0\u884C\u4E2D";
|
|
3115
|
+
if (!gateway.configured) return "\u672A\u914D\u7F6E";
|
|
3116
|
+
return "\u5F85\u542F\u52A8";
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function formatGatewayNote(gateway) {
|
|
3120
|
+
if (gateway.connection === "running" && gateway.pid) return "PID " + gateway.pid;
|
|
3121
|
+
return "\u98DE\u4E66\u957F\u8FDE\u63A5";
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
function renderMetrics(status) {
|
|
3125
|
+
const gatewayClass = status.gateway.configured ? "status-ok" : "status-warn";
|
|
3126
|
+
metrics.innerHTML = [
|
|
3127
|
+
["Gateway", formatGatewayValue(status.gateway), formatGatewayNote(status.gateway), gatewayClass],
|
|
3128
|
+
["\u7FA4\u804A", status.data.chats, "\u672C\u5730\u7FA4\u804A\u6570", ""],
|
|
3129
|
+
["\u6D88\u606F", status.data.messages, "\u5DF2\u5165\u5E93\u6D88\u606F", ""],
|
|
3130
|
+
["\u6587\u4EF6", status.data.files, "\u6587\u4EF6\u77E5\u8BC6\u6E90", ""],
|
|
3131
|
+
].map(([label, value, note, extra]) => \`
|
|
3132
|
+
<div class="metric">
|
|
3133
|
+
<div class="label">\${escapeHtml(label)}</div>
|
|
3134
|
+
<div class="value \${extra}">\${escapeHtml(value)}</div>
|
|
3135
|
+
<div class="note">\${escapeHtml(note)}</div>
|
|
3136
|
+
</div>
|
|
3137
|
+
\`).join("");
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
function renderMessages(items) {
|
|
3141
|
+
if (items.length === 0) {
|
|
3142
|
+
messages.className = "empty";
|
|
3143
|
+
messages.textContent = "\u8FD8\u6CA1\u6709\u6D88\u606F\u3002\u542F\u52A8 Gateway \u540E\uFF0C\u7FA4\u804A\u6587\u672C\u4F1A\u8FDB\u5165\u672C\u5730 RAG \u7D22\u5F15\u3002";
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
messages.className = "";
|
|
3147
|
+
messages.innerHTML = \`
|
|
3148
|
+
<div class="message-list">
|
|
3149
|
+
\${items.map((item) => \`
|
|
3150
|
+
<article class="message-item">
|
|
3151
|
+
<div class="message-meta">
|
|
3152
|
+
<span>\${escapeHtml(formatDateTime(item.sentAt))}</span>
|
|
3153
|
+
<span>\${escapeHtml(displaySender(item.senderName))}</span>
|
|
3154
|
+
<span>\${escapeHtml(displayChatName(item.chatName, item.platform))}</span>
|
|
3155
|
+
</div>
|
|
3156
|
+
<div class="message-body">\${escapeHtml(item.text)}</div>
|
|
3157
|
+
</article>
|
|
3158
|
+
\`).join("")}
|
|
3159
|
+
</div>
|
|
3160
|
+
\`;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
function renderChats(items) {
|
|
3164
|
+
if (items.length === 0) {
|
|
3165
|
+
chats.className = "empty";
|
|
3166
|
+
chats.textContent = "\u8FD8\u6CA1\u6709\u7FA4\u804A\u8BB0\u5F55\u3002";
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
chats.className = "";
|
|
3170
|
+
chats.innerHTML = \`
|
|
3171
|
+
<table>
|
|
3172
|
+
<thead><tr><th>\u540D\u79F0</th><th>\u5E73\u53F0</th></tr></thead>
|
|
3173
|
+
<tbody>
|
|
3174
|
+
\${items.map((item) => \`
|
|
3175
|
+
<tr>
|
|
3176
|
+
<td><span class="id-text" title="\${escapeHtml(item.name)}">\${escapeHtml(displayChatName(item.name, item.platform))}</span></td>
|
|
3177
|
+
<td>\${escapeHtml(item.platform)}</td>
|
|
3178
|
+
</tr>
|
|
3179
|
+
\`).join("")}
|
|
3180
|
+
</tbody>
|
|
3181
|
+
</table>
|
|
3182
|
+
\`;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function renderFiles(items) {
|
|
3186
|
+
if (items.length === 0) {
|
|
3187
|
+
files.className = "empty";
|
|
3188
|
+
files.textContent = "\u8FD8\u6CA1\u6709\u6587\u4EF6\u3002\u53EF\u5148\u8FD0\u884C chattercatcher files add <path...> \u5BFC\u5165\u6587\u672C\u3001DOCX \u6216 PDF \u6587\u4EF6\u3002";
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
files.className = "";
|
|
3192
|
+
files.innerHTML = \`
|
|
3193
|
+
<table>
|
|
3194
|
+
<thead><tr><th>\u6587\u4EF6</th><th>\u89E3\u6790\u5668</th><th>\u5B57\u7B26</th></tr></thead>
|
|
3195
|
+
<tbody>
|
|
3196
|
+
\${items.map((item) => \`
|
|
3197
|
+
<tr>
|
|
3198
|
+
<td>
|
|
3199
|
+
<div>\${escapeHtml(item.fileName)}</div>
|
|
3200
|
+
<div class="path" title="\${escapeHtml(item.storedPath)}">\${escapeHtml(item.storedPath)}</div>
|
|
3201
|
+
</td>
|
|
3202
|
+
<td>\${escapeHtml(item.parser || "unknown")}</td>
|
|
3203
|
+
<td>\${escapeHtml(item.characters)}</td>
|
|
3204
|
+
</tr>
|
|
3205
|
+
\`).join("")}
|
|
3206
|
+
</tbody>
|
|
3207
|
+
</table>
|
|
3208
|
+
\`;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
function renderFileJobs(items) {
|
|
3212
|
+
if (items.length === 0) {
|
|
3213
|
+
fileJobs.className = "empty";
|
|
3214
|
+
fileJobs.textContent = "\u8FD8\u6CA1\u6709\u6587\u4EF6\u89E3\u6790\u4EFB\u52A1\u3002";
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
fileJobs.className = "";
|
|
3218
|
+
fileJobs.innerHTML = \`
|
|
3219
|
+
<table>
|
|
3220
|
+
<thead><tr><th>\u4EFB\u52A1</th><th>\u72B6\u6001</th></tr></thead>
|
|
3221
|
+
<tbody>
|
|
3222
|
+
\${items.map((item) => \`
|
|
3223
|
+
<tr>
|
|
3224
|
+
<td>
|
|
3225
|
+
<div>\${escapeHtml(item.fileName)}</div>
|
|
3226
|
+
<div class="path" title="\${escapeHtml(item.id)}">ID: \${escapeHtml(item.id)}</div>
|
|
3227
|
+
<div class="path" title="\${escapeHtml(item.error || item.storedPath)}">\${escapeHtml(item.error || item.storedPath)}</div>
|
|
3228
|
+
</td>
|
|
3229
|
+
<td>\${escapeHtml(item.status)}</td>
|
|
3230
|
+
</tr>
|
|
3231
|
+
\`).join("")}
|
|
3232
|
+
</tbody>
|
|
3233
|
+
</table>
|
|
3234
|
+
\`;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
async function load() {
|
|
3238
|
+
const [status, recent, chatList, fileList, jobList] = await Promise.all([
|
|
3239
|
+
fetch("/api/status").then((response) => response.json()),
|
|
3240
|
+
fetch("/api/messages/recent?limit=20").then((response) => response.json()),
|
|
3241
|
+
fetch("/api/chats").then((response) => response.json()),
|
|
3242
|
+
fetch("/api/files").then((response) => response.json()),
|
|
3243
|
+
fetch("/api/file-jobs").then((response) => response.json()),
|
|
3244
|
+
]);
|
|
3245
|
+
renderMetrics(status);
|
|
3246
|
+
renderMessages(recent.items);
|
|
3247
|
+
renderChats(chatList.items);
|
|
3248
|
+
renderFiles(fileList.items);
|
|
3249
|
+
renderFileJobs(jobList.items);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
async function processNow() {
|
|
3253
|
+
processMessages.disabled = true;
|
|
3254
|
+
actionStatus.textContent = "\u6B63\u5728\u5904\u7406\u6D88\u606F\u7D22\u5F15...";
|
|
3255
|
+
try {
|
|
3256
|
+
const response = await fetch("/api/process/messages", { method: "POST" });
|
|
3257
|
+
const result = await response.json();
|
|
3258
|
+
if (!response.ok) {
|
|
3259
|
+
actionStatus.textContent = result.message || "\u5904\u7406\u5931\u8D25\u3002";
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
if (result.status === "skipped") {
|
|
3264
|
+
actionStatus.textContent = result.reason;
|
|
3265
|
+
} else {
|
|
3266
|
+
actionStatus.textContent = \`\u5904\u7406\u5B8C\u6210\uFF1Achunks=\${result.chunks}, vectors=\${result.vectors}\`;
|
|
3267
|
+
}
|
|
3268
|
+
await load();
|
|
3269
|
+
} catch (error) {
|
|
3270
|
+
actionStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
3271
|
+
} finally {
|
|
3272
|
+
processMessages.disabled = false;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
processMessages.addEventListener("click", () => void processNow());
|
|
3277
|
+
void load();
|
|
3278
|
+
setInterval(() => {
|
|
3279
|
+
if (document.visibilityState === "visible") {
|
|
3280
|
+
void load();
|
|
3281
|
+
}
|
|
3282
|
+
}, 5000);
|
|
3283
|
+
</script>
|
|
3284
|
+
</body>
|
|
3285
|
+
</html>`;
|
|
3286
|
+
}
|
|
3287
|
+
function parseLimit(value, fallback, max) {
|
|
3288
|
+
const rawLimit = Number(value ?? fallback);
|
|
3289
|
+
return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
|
|
3290
|
+
}
|
|
3291
|
+
function createWebApp(config) {
|
|
3292
|
+
const app = Fastify({ logger: false });
|
|
3293
|
+
const database = openDatabase(config);
|
|
3294
|
+
const messages = new MessageRepository(database);
|
|
3295
|
+
const fileJobs = new FileJobRepository(database);
|
|
3296
|
+
app.addHook("onClose", async () => {
|
|
3297
|
+
database.close();
|
|
3298
|
+
});
|
|
3299
|
+
app.get("/api/status", async () => ({
|
|
3300
|
+
app: "ChatterCatcher",
|
|
3301
|
+
gateway: getGatewayStatus(config),
|
|
3302
|
+
data: {
|
|
3303
|
+
chats: messages.getChatCount(),
|
|
3304
|
+
messages: messages.getMessageCount(),
|
|
3305
|
+
files: messages.listFiles(1e3).length
|
|
3306
|
+
},
|
|
3307
|
+
rag: {
|
|
3308
|
+
mode: "required",
|
|
3309
|
+
note: "\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22\u8BC1\u636E\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0\u3002",
|
|
3310
|
+
retrieval: {
|
|
3311
|
+
keyword: "SQLite FTS5",
|
|
3312
|
+
vector: "LanceDB",
|
|
3313
|
+
hybrid: true
|
|
3314
|
+
}
|
|
3315
|
+
},
|
|
3316
|
+
web: config.web
|
|
3317
|
+
}));
|
|
3318
|
+
app.get("/api/chats", async () => ({
|
|
3319
|
+
items: messages.listChats()
|
|
3320
|
+
}));
|
|
3321
|
+
app.get("/api/files", async (request) => {
|
|
3322
|
+
const limit = parseLimit(request.query.limit, 50, 200);
|
|
3323
|
+
return {
|
|
3324
|
+
items: messages.listFiles(limit)
|
|
3325
|
+
};
|
|
3326
|
+
});
|
|
3327
|
+
app.get("/api/file-jobs", async (request) => {
|
|
3328
|
+
const limit = parseLimit(request.query.limit, 50, 200);
|
|
3329
|
+
const status = request.query.status;
|
|
3330
|
+
return {
|
|
3331
|
+
items: fileJobs.list(limit, status === "processing" || status === "indexed" || status === "failed" ? { status } : {})
|
|
3332
|
+
};
|
|
3333
|
+
});
|
|
3334
|
+
app.get("/api/messages/recent", async (request) => {
|
|
3335
|
+
const limit = parseLimit(request.query.limit, 20, 100);
|
|
3336
|
+
return {
|
|
3337
|
+
items: messages.listRecentMessages(limit)
|
|
3338
|
+
};
|
|
3339
|
+
});
|
|
3340
|
+
app.post("/api/process/messages", async (_request, reply) => {
|
|
3341
|
+
try {
|
|
3342
|
+
return await processMessagesNow({
|
|
3343
|
+
config,
|
|
3344
|
+
secrets: await loadSecrets(),
|
|
3345
|
+
database,
|
|
3346
|
+
limit: 1e4
|
|
3347
|
+
});
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
reply.code(500);
|
|
3350
|
+
return {
|
|
3351
|
+
status: "failed",
|
|
3352
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
});
|
|
3356
|
+
app.get("/", async (_request, reply) => {
|
|
3357
|
+
reply.type("text/html; charset=utf-8");
|
|
3358
|
+
return buildHtml();
|
|
3359
|
+
});
|
|
3360
|
+
return app;
|
|
3361
|
+
}
|
|
3362
|
+
async function startWebServer(config) {
|
|
3363
|
+
const app = createWebApp(config);
|
|
3364
|
+
await app.listen({ host: config.web.host, port: config.web.port });
|
|
3365
|
+
const address = app.server.address();
|
|
3366
|
+
const url = typeof address === "string" ? address : `http://${config.web.host}:${address?.port ?? config.web.port}`;
|
|
3367
|
+
console.log(`ChatterCatcher Web UI: ${url}`);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
125
3370
|
// src/cli.ts
|
|
126
3371
|
var program = new Command();
|
|
127
3372
|
async function promptForConfiguration(config, secrets) {
|
|
@@ -184,7 +3429,7 @@ function printSettings(config, secrets) {
|
|
|
184
3429
|
2
|
|
185
3430
|
));
|
|
186
3431
|
}
|
|
187
|
-
program.name("chattercatcher").description("\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u673A\u5668\u4EBA").version(
|
|
3432
|
+
program.name("chattercatcher").description("\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u673A\u5668\u4EBA").version("0.1.5");
|
|
188
3433
|
program.command("setup").description("\u4EA4\u4E92\u5F0F\u521D\u59CB\u5316\u914D\u7F6E").action(async () => {
|
|
189
3434
|
const { config, secrets } = await ensureConfigFiles();
|
|
190
3435
|
await promptForConfiguration(config, secrets);
|
|
@@ -221,19 +3466,33 @@ program.command("doctor").description("\u68C0\u67E5\u672C\u5730\u914D\u7F6E\u300
|
|
|
221
3466
|
console.log(formatDoctorChecks(checks));
|
|
222
3467
|
});
|
|
223
3468
|
var gateway = program.command("gateway").description("\u7BA1\u7406\u672C\u5730\u98DE\u4E66 Gateway");
|
|
224
|
-
async function
|
|
3469
|
+
async function startGatewayForegroundCommand() {
|
|
225
3470
|
const config = await loadConfig();
|
|
226
3471
|
const secrets = await loadSecrets();
|
|
227
3472
|
const status = getGatewayStatus(config, secrets);
|
|
3473
|
+
const pidRecordBase = {
|
|
3474
|
+
pid: process.pid,
|
|
3475
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3476
|
+
command: process.argv.join(" "),
|
|
3477
|
+
logFile: getGatewayLogPath()
|
|
3478
|
+
};
|
|
228
3479
|
if (!status.configured) {
|
|
3480
|
+
writeGatewayPidRecord(void 0, {
|
|
3481
|
+
...pidRecordBase,
|
|
3482
|
+
mode: "web"
|
|
3483
|
+
});
|
|
229
3484
|
console.log(status.message);
|
|
230
3485
|
console.log("\u672C\u5730 Web UI \u4ECD\u4F1A\u542F\u52A8\uFF0C\u65B9\u4FBF\u7EE7\u7EED\u914D\u7F6E\u3002");
|
|
231
3486
|
await startWebServer(config);
|
|
232
3487
|
return;
|
|
233
3488
|
}
|
|
3489
|
+
writeGatewayPidRecord(void 0, {
|
|
3490
|
+
...pidRecordBase,
|
|
3491
|
+
mode: "gateway"
|
|
3492
|
+
});
|
|
234
3493
|
const database = openDatabase(config);
|
|
235
|
-
const { LanceDbVectorStore } = hasEmbeddingConfig(config, secrets) ? await
|
|
236
|
-
const vectorStore =
|
|
3494
|
+
const { LanceDbVectorStore: LanceDbVectorStore2 } = hasEmbeddingConfig(config, secrets) ? await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports)) : { LanceDbVectorStore: null };
|
|
3495
|
+
const vectorStore = LanceDbVectorStore2 ? await LanceDbVectorStore2.connectFromConfig(config) : null;
|
|
237
3496
|
const gatewayRuntime = createFeishuGateway({
|
|
238
3497
|
config,
|
|
239
3498
|
secrets,
|
|
@@ -268,7 +3527,6 @@ async function startGatewayCommand() {
|
|
|
268
3527
|
process.exit(0);
|
|
269
3528
|
});
|
|
270
3529
|
console.log(status.message);
|
|
271
|
-
writeGatewayPidRecord();
|
|
272
3530
|
try {
|
|
273
3531
|
await gatewayRuntime.start();
|
|
274
3532
|
await startWebServer(config);
|
|
@@ -277,12 +3535,28 @@ async function startGatewayCommand() {
|
|
|
277
3535
|
throw error;
|
|
278
3536
|
}
|
|
279
3537
|
}
|
|
3538
|
+
async function startGatewayCommand(options = {}) {
|
|
3539
|
+
if (options.foreground) {
|
|
3540
|
+
await startGatewayForegroundCommand();
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
const config = await loadConfig();
|
|
3544
|
+
const secrets = await loadSecrets();
|
|
3545
|
+
const result = startDetachedGateway({ config, secrets });
|
|
3546
|
+
console.log(result.message);
|
|
3547
|
+
if (result.pid) {
|
|
3548
|
+
console.log(`PID\uFF1A${result.pid}`);
|
|
3549
|
+
}
|
|
3550
|
+
console.log(`\u65E5\u5FD7\u6587\u4EF6\uFF1A${result.logFile}`);
|
|
3551
|
+
console.log("\u67E5\u770B\u65E5\u5FD7\uFF1Achattercatcher logs --follow --file gateway.log");
|
|
3552
|
+
console.log("\u505C\u6B62 Gateway\uFF1Achattercatcher gateway stop");
|
|
3553
|
+
}
|
|
280
3554
|
gateway.command("status").description("\u67E5\u770B Gateway \u72B6\u6001").action(async () => {
|
|
281
3555
|
const config = await loadConfig();
|
|
282
3556
|
const secrets = await loadSecrets();
|
|
283
3557
|
console.log(JSON.stringify(getGatewayStatus(config, secrets), null, 2));
|
|
284
3558
|
});
|
|
285
|
-
gateway.command("start").description("\u542F\u52A8\u98DE\u4E66\u957F\u8FDE\u63A5 Gateway \u548C\u672C\u5730 Web UI").action(startGatewayCommand);
|
|
3559
|
+
gateway.command("start").description("\u542F\u52A8\u98DE\u4E66\u957F\u8FDE\u63A5 Gateway \u548C\u672C\u5730 Web UI").option("--foreground", "\u5728\u5F53\u524D\u7EC8\u7AEF\u4EE5\u524D\u53F0\u6A21\u5F0F\u8FD0\u884C").action(startGatewayCommand);
|
|
286
3560
|
gateway.command("stop").description("\u505C\u6B62 Gateway").action(() => {
|
|
287
3561
|
console.log(stopGatewayProcess().message);
|
|
288
3562
|
});
|
|
@@ -333,13 +3607,13 @@ index.command("status").description("\u67E5\u770B\u7D22\u5F15\u72B6\u6001").acti
|
|
|
333
3607
|
const secrets = await loadSecrets();
|
|
334
3608
|
const database = openDatabase(config);
|
|
335
3609
|
const messages = new MessageRepository(database);
|
|
336
|
-
const { getLanceDbPath, LanceDbVectorStore } = await
|
|
337
|
-
const vectorStore = await
|
|
3610
|
+
const { getLanceDbPath: getLanceDbPath2, LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
|
|
3611
|
+
const vectorStore = await LanceDbVectorStore2.connectFromConfig(config);
|
|
338
3612
|
const vectors = await vectorStore.count();
|
|
339
3613
|
console.log(JSON.stringify(
|
|
340
3614
|
{
|
|
341
3615
|
database: getDatabasePath(config),
|
|
342
|
-
vectorDatabase:
|
|
3616
|
+
vectorDatabase: getLanceDbPath2(config),
|
|
343
3617
|
chats: messages.getChatCount(),
|
|
344
3618
|
messages: messages.getMessageCount(),
|
|
345
3619
|
vectors,
|
|
@@ -364,8 +3638,8 @@ index.command("rebuild").description("\u91CD\u5EFA LanceDB \u5411\u91CF\u7D22\u5
|
|
|
364
3638
|
return;
|
|
365
3639
|
}
|
|
366
3640
|
const database = openDatabase(config);
|
|
367
|
-
const { LanceDbVectorStore } = await
|
|
368
|
-
const vectorStore = await
|
|
3641
|
+
const { LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
|
|
3642
|
+
const vectorStore = await LanceDbVectorStore2.connectFromConfig(config);
|
|
369
3643
|
try {
|
|
370
3644
|
const stats = await indexMessageChunks({
|
|
371
3645
|
messages: new MessageRepository(database),
|
|
@@ -573,7 +3847,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
|
|
|
573
3847
|
const config = await loadConfig();
|
|
574
3848
|
const database = openDatabase(config);
|
|
575
3849
|
try {
|
|
576
|
-
const raw = await
|
|
3850
|
+
const raw = await fs14.readFile(options.file, "utf8");
|
|
577
3851
|
const payload = JSON.parse(raw);
|
|
578
3852
|
const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
|
|
579
3853
|
if (!result.accepted) {
|