chattercatcher 0.1.19 → 0.1.21
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 +1118 -439
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +226 -129
- package/dist/index.js +1025 -336
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -59,6 +59,12 @@ var appSecretsSchema = z.object({
|
|
|
59
59
|
z.object({
|
|
60
60
|
apiKey: z.string().default("")
|
|
61
61
|
})
|
|
62
|
+
),
|
|
63
|
+
web: z.preprocess(
|
|
64
|
+
(value) => value ?? {},
|
|
65
|
+
z.object({
|
|
66
|
+
actionToken: z.string().default("")
|
|
67
|
+
})
|
|
62
68
|
)
|
|
63
69
|
});
|
|
64
70
|
function createDefaultConfig() {
|
|
@@ -78,7 +84,8 @@ function createDefaultSecrets() {
|
|
|
78
84
|
feishu: {},
|
|
79
85
|
llm: {},
|
|
80
86
|
embedding: {},
|
|
81
|
-
multimodal: {}
|
|
87
|
+
multimodal: {},
|
|
88
|
+
web: {}
|
|
82
89
|
});
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -171,6 +178,490 @@ function resolveEmbeddingApiKey(input) {
|
|
|
171
178
|
return explicit || input.llmApiKey;
|
|
172
179
|
}
|
|
173
180
|
|
|
181
|
+
// src/cron/generator.ts
|
|
182
|
+
var SYSTEM_PROMPT = "\u4F60\u6B63\u5728\u4E3A\u98DE\u4E66\u7FA4\u751F\u6210\u4E00\u6761\u5B9A\u65F6\u6D88\u606F\u3002\u53EF\u4EE5\u5148\u8C03\u7528\u641C\u7D22\u5DE5\u5177\u68C0\u7D22\u672C\u5730\u7FA4\u804A\u77E5\u8BC6\u5E93\u3002\u6700\u7EC8\u8F93\u51FA\u5FC5\u987B\u662F\u53EF\u4EE5\u76F4\u63A5\u53D1\u5230\u7FA4\u91CC\u7684\u7EAF\u6587\u672C\uFF0C\u4E0D\u8981\u8F93\u51FA\u5DE5\u5177\u8C03\u7528\u8BF4\u660E\u3002";
|
|
183
|
+
function evidenceToText(evidence) {
|
|
184
|
+
if (evidence.length === 0) {
|
|
185
|
+
return "\u65E0\u68C0\u7D22\u8BC1\u636E\u3002";
|
|
186
|
+
}
|
|
187
|
+
return evidence.map((item, index) => `${index + 1}. ${item.text}`).join("\n");
|
|
188
|
+
}
|
|
189
|
+
function toolResultContent(results) {
|
|
190
|
+
return JSON.stringify(results.map((item) => ({ id: item.id, text: item.text, score: item.score, source: item.source })));
|
|
191
|
+
}
|
|
192
|
+
async function generateCronJobMessage(input) {
|
|
193
|
+
if (!input.model.completeWithTools) {
|
|
194
|
+
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
195
|
+
}
|
|
196
|
+
const messages = [
|
|
197
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
198
|
+
{ role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
|
|
199
|
+
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input.prompt}` }
|
|
200
|
+
];
|
|
201
|
+
const toolsByName = new Map(input.tools.map((tool) => [tool.name, tool]));
|
|
202
|
+
const evidence = [];
|
|
203
|
+
const maxModelTurns = input.maxModelTurns ?? 3;
|
|
204
|
+
const maxToolCalls = input.maxToolCalls ?? 6;
|
|
205
|
+
let toolCallsUsed = 0;
|
|
206
|
+
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
207
|
+
const result = await input.model.completeWithTools(messages, input.tools);
|
|
208
|
+
messages.push({ role: "assistant", content: result.content, toolCalls: result.toolCalls, reasoningContent: result.reasoningContent });
|
|
209
|
+
if (result.toolCalls.length === 0) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
for (const call of result.toolCalls) {
|
|
213
|
+
if (toolCallsUsed >= maxToolCalls) {
|
|
214
|
+
return input.model.complete([
|
|
215
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
216
|
+
{
|
|
217
|
+
role: "user",
|
|
218
|
+
content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
|
|
219
|
+
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input.prompt}
|
|
220
|
+
|
|
221
|
+
\u8BC1\u636E\uFF1A
|
|
222
|
+
${evidenceToText(evidence)}`
|
|
223
|
+
}
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
toolCallsUsed += 1;
|
|
227
|
+
const tool = toolsByName.get(call.name);
|
|
228
|
+
if (!tool) {
|
|
229
|
+
messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify({ error: `\u672A\u77E5\u5DE5\u5177\uFF1A${call.name}` }) });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const results = await tool.execute(call.input);
|
|
234
|
+
evidence.push(...results);
|
|
235
|
+
messages.push({ role: "tool", toolCallId: call.id, content: toolResultContent(results) });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify({ error: message }) });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return input.model.complete([
|
|
243
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
244
|
+
{
|
|
245
|
+
role: "user",
|
|
246
|
+
content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
|
|
247
|
+
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input.prompt}
|
|
248
|
+
|
|
249
|
+
\u8BC1\u636E\uFF1A
|
|
250
|
+
${evidenceToText(evidence)}`
|
|
251
|
+
}
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/cron/jobs.ts
|
|
256
|
+
import crypto from "crypto";
|
|
257
|
+
|
|
258
|
+
// src/cron/schedule.ts
|
|
259
|
+
function isValidCronSchedule(schedule) {
|
|
260
|
+
return parseCronSchedule(schedule) !== null;
|
|
261
|
+
}
|
|
262
|
+
function matchesCronSchedule(schedule, date) {
|
|
263
|
+
const parsed = parseCronSchedule(schedule);
|
|
264
|
+
if (!parsed) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
return matchesParsedSchedule(parsed, date);
|
|
268
|
+
}
|
|
269
|
+
function getNextCronRun(schedule, after) {
|
|
270
|
+
const parsed = parseCronSchedule(schedule);
|
|
271
|
+
if (!parsed) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const candidate = new Date(after);
|
|
275
|
+
candidate.setSeconds(0, 0);
|
|
276
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
277
|
+
const maxMinutes = 5 * 366 * 24 * 60;
|
|
278
|
+
for (let i = 0; i < maxMinutes; i += 1) {
|
|
279
|
+
if (matchesParsedSchedule(parsed, candidate)) {
|
|
280
|
+
return new Date(candidate);
|
|
281
|
+
}
|
|
282
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
function matchesParsedSchedule(schedule, date) {
|
|
287
|
+
const dayOfMonthMatches = schedule.dayOfMonth.matches(date.getDate());
|
|
288
|
+
const dayOfWeekMatches = schedule.dayOfWeek.matches(date.getDay());
|
|
289
|
+
const dayMatches = schedule.dayOfMonth.wildcard || schedule.dayOfWeek.wildcard ? dayOfMonthMatches && dayOfWeekMatches : dayOfMonthMatches || dayOfWeekMatches;
|
|
290
|
+
return schedule.minute.matches(date.getMinutes()) && schedule.hour.matches(date.getHours()) && dayMatches && schedule.month.matches(date.getMonth() + 1);
|
|
291
|
+
}
|
|
292
|
+
function parseCronSchedule(schedule) {
|
|
293
|
+
const fields = schedule.trim().split(/\s+/);
|
|
294
|
+
if (fields.length !== 5) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const minute = parseMinuteField(fields[0]);
|
|
298
|
+
const hour = parseExactOrWildcardField(fields[1], 0, 23);
|
|
299
|
+
const dayOfMonth = parseExactOrWildcardField(fields[2], 1, 31);
|
|
300
|
+
const month = parseExactOrWildcardField(fields[3], 1, 12);
|
|
301
|
+
const dayOfWeek = parseExactOrWildcardField(fields[4], 0, 6);
|
|
302
|
+
if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return { minute, hour, dayOfMonth, month, dayOfWeek };
|
|
306
|
+
}
|
|
307
|
+
function parseMinuteField(field) {
|
|
308
|
+
if (field === "*") {
|
|
309
|
+
return { wildcard: true, matches: () => true };
|
|
310
|
+
}
|
|
311
|
+
const stepMatch = /^\*\/(\d+)$/.exec(field);
|
|
312
|
+
if (stepMatch) {
|
|
313
|
+
const step = Number(stepMatch[1]);
|
|
314
|
+
if (!Number.isInteger(step) || step <= 0 || step > 59) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return { wildcard: false, matches: (value) => value % step === 0 };
|
|
318
|
+
}
|
|
319
|
+
if (field.includes(",")) {
|
|
320
|
+
const values = field.split(",").map((part) => parseExactNumber(part, 0, 59));
|
|
321
|
+
if (values.some((value) => value === null)) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const allowed = new Set(values);
|
|
325
|
+
return { wildcard: false, matches: (value) => allowed.has(value) };
|
|
326
|
+
}
|
|
327
|
+
const exact = parseExactNumber(field, 0, 59);
|
|
328
|
+
if (exact === null) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return { wildcard: false, matches: (value) => value === exact };
|
|
332
|
+
}
|
|
333
|
+
function parseExactOrWildcardField(field, min, max) {
|
|
334
|
+
if (field === "*") {
|
|
335
|
+
return { wildcard: true, matches: () => true };
|
|
336
|
+
}
|
|
337
|
+
const exact = parseExactNumber(field, min, max);
|
|
338
|
+
if (exact === null) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return { wildcard: false, matches: (value) => value === exact };
|
|
342
|
+
}
|
|
343
|
+
function parseExactNumber(field, min, max) {
|
|
344
|
+
if (!/^\d+$/.test(field)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const value = Number(field);
|
|
348
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/cron/jobs.ts
|
|
355
|
+
var CronJobRepository = class {
|
|
356
|
+
constructor(database, options = {}) {
|
|
357
|
+
this.database = database;
|
|
358
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
359
|
+
}
|
|
360
|
+
database;
|
|
361
|
+
now;
|
|
362
|
+
create(input) {
|
|
363
|
+
const schedule = input.schedule.trim();
|
|
364
|
+
const prompt = input.prompt.trim();
|
|
365
|
+
if (!isValidCronSchedule(schedule)) {
|
|
366
|
+
throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
|
|
367
|
+
}
|
|
368
|
+
if (!prompt) {
|
|
369
|
+
throw new Error("\u5B9A\u65F6\u4EFB\u52A1 prompt \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
370
|
+
}
|
|
371
|
+
const now = this.now();
|
|
372
|
+
const nextRunAt = getNextCronRun(schedule, now);
|
|
373
|
+
if (!nextRunAt) {
|
|
374
|
+
throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
|
|
375
|
+
}
|
|
376
|
+
const record = {
|
|
377
|
+
id: crypto.randomUUID(),
|
|
378
|
+
chatId: input.chatId,
|
|
379
|
+
createdByOpenId: input.createdByOpenId,
|
|
380
|
+
schedule,
|
|
381
|
+
prompt,
|
|
382
|
+
status: "active",
|
|
383
|
+
nextRunAt: nextRunAt.toISOString(),
|
|
384
|
+
createdAt: now.toISOString(),
|
|
385
|
+
updatedAt: now.toISOString()
|
|
386
|
+
};
|
|
387
|
+
this.database.prepare(
|
|
388
|
+
`
|
|
389
|
+
INSERT INTO cron_jobs (
|
|
390
|
+
id, chat_id, created_by_open_id, schedule, prompt, status,
|
|
391
|
+
last_run_at, next_run_at, last_error, created_at, updated_at
|
|
392
|
+
)
|
|
393
|
+
VALUES (
|
|
394
|
+
@id, @chatId, @createdByOpenId, @schedule, @prompt, @status,
|
|
395
|
+
NULL, @nextRunAt, NULL, @createdAt, @updatedAt
|
|
396
|
+
)
|
|
397
|
+
`
|
|
398
|
+
).run(record);
|
|
399
|
+
return record;
|
|
400
|
+
}
|
|
401
|
+
get(id) {
|
|
402
|
+
return this.listByWhere("WHERE id = ?", [id], 1)[0] ?? null;
|
|
403
|
+
}
|
|
404
|
+
list(limit = 100) {
|
|
405
|
+
return this.listByWhere("", [], limit);
|
|
406
|
+
}
|
|
407
|
+
listByChat(chatId, limit = 50) {
|
|
408
|
+
return this.listByWhere(
|
|
409
|
+
"WHERE chat_id = ? AND status = 'active'",
|
|
410
|
+
[chatId],
|
|
411
|
+
limit
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
listDue(now, limit = 20) {
|
|
415
|
+
const rows = this.database.prepare(
|
|
416
|
+
`
|
|
417
|
+
SELECT
|
|
418
|
+
id,
|
|
419
|
+
chat_id AS chatId,
|
|
420
|
+
created_by_open_id AS createdByOpenId,
|
|
421
|
+
schedule,
|
|
422
|
+
prompt,
|
|
423
|
+
status,
|
|
424
|
+
last_run_at AS lastRunAt,
|
|
425
|
+
next_run_at AS nextRunAt,
|
|
426
|
+
last_error AS lastError,
|
|
427
|
+
created_at AS createdAt,
|
|
428
|
+
updated_at AS updatedAt
|
|
429
|
+
FROM cron_jobs
|
|
430
|
+
WHERE status = 'active' AND next_run_at <= ?
|
|
431
|
+
ORDER BY next_run_at ASC, updated_at ASC
|
|
432
|
+
LIMIT ?
|
|
433
|
+
`
|
|
434
|
+
).all(now.toISOString(), limit);
|
|
435
|
+
return rows.map((row) => ({
|
|
436
|
+
id: row.id,
|
|
437
|
+
chatId: row.chatId,
|
|
438
|
+
createdByOpenId: row.createdByOpenId ?? void 0,
|
|
439
|
+
schedule: row.schedule,
|
|
440
|
+
prompt: row.prompt,
|
|
441
|
+
status: row.status,
|
|
442
|
+
lastRunAt: row.lastRunAt ?? void 0,
|
|
443
|
+
nextRunAt: row.nextRunAt,
|
|
444
|
+
lastError: row.lastError ?? void 0,
|
|
445
|
+
createdAt: row.createdAt,
|
|
446
|
+
updatedAt: row.updatedAt
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
deleteByChat(id, chatId) {
|
|
450
|
+
const now = this.now().toISOString();
|
|
451
|
+
const result = this.database.prepare(
|
|
452
|
+
`
|
|
453
|
+
UPDATE cron_jobs
|
|
454
|
+
SET status = 'deleted', updated_at = @updatedAt
|
|
455
|
+
WHERE id = @id AND chat_id = @chatId AND status = 'active'
|
|
456
|
+
`
|
|
457
|
+
).run({ id, chatId, updatedAt: now });
|
|
458
|
+
return result.changes > 0;
|
|
459
|
+
}
|
|
460
|
+
markSuccess(id, ranAt) {
|
|
461
|
+
const job = this.get(id);
|
|
462
|
+
if (!job) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const nextRunAt = getNextCronRun(job.schedule, ranAt);
|
|
466
|
+
if (!nextRunAt) {
|
|
467
|
+
throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
|
|
468
|
+
}
|
|
469
|
+
this.database.prepare(
|
|
470
|
+
`
|
|
471
|
+
UPDATE cron_jobs
|
|
472
|
+
SET last_run_at = @lastRunAt, next_run_at = @nextRunAt, last_error = NULL, updated_at = @updatedAt
|
|
473
|
+
WHERE id = @id AND status = 'active'
|
|
474
|
+
`
|
|
475
|
+
).run({
|
|
476
|
+
id,
|
|
477
|
+
lastRunAt: ranAt.toISOString(),
|
|
478
|
+
nextRunAt: nextRunAt.toISOString(),
|
|
479
|
+
updatedAt: ranAt.toISOString()
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
markFailure(id, error, failedAt) {
|
|
483
|
+
const job = this.get(id);
|
|
484
|
+
if (!job) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const nextRunAt = getNextCronRun(job.schedule, failedAt);
|
|
488
|
+
if (!nextRunAt) {
|
|
489
|
+
throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
|
|
490
|
+
}
|
|
491
|
+
this.database.prepare(
|
|
492
|
+
`
|
|
493
|
+
UPDATE cron_jobs
|
|
494
|
+
SET last_run_at = @lastRunAt, last_error = @lastError, next_run_at = @nextRunAt, updated_at = @updatedAt
|
|
495
|
+
WHERE id = @id AND status = 'active'
|
|
496
|
+
`
|
|
497
|
+
).run({
|
|
498
|
+
id,
|
|
499
|
+
lastRunAt: failedAt.toISOString(),
|
|
500
|
+
lastError: error,
|
|
501
|
+
nextRunAt: nextRunAt.toISOString(),
|
|
502
|
+
updatedAt: failedAt.toISOString()
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
listByWhere(whereSql, params, limit) {
|
|
506
|
+
const rows = this.database.prepare(
|
|
507
|
+
`
|
|
508
|
+
SELECT
|
|
509
|
+
id,
|
|
510
|
+
chat_id AS chatId,
|
|
511
|
+
created_by_open_id AS createdByOpenId,
|
|
512
|
+
schedule,
|
|
513
|
+
prompt,
|
|
514
|
+
status,
|
|
515
|
+
last_run_at AS lastRunAt,
|
|
516
|
+
next_run_at AS nextRunAt,
|
|
517
|
+
last_error AS lastError,
|
|
518
|
+
created_at AS createdAt,
|
|
519
|
+
updated_at AS updatedAt
|
|
520
|
+
FROM cron_jobs
|
|
521
|
+
${whereSql}
|
|
522
|
+
ORDER BY updated_at DESC
|
|
523
|
+
LIMIT ?
|
|
524
|
+
`
|
|
525
|
+
).all(...params, limit);
|
|
526
|
+
return rows.map((row) => ({
|
|
527
|
+
id: row.id,
|
|
528
|
+
chatId: row.chatId,
|
|
529
|
+
createdByOpenId: row.createdByOpenId ?? void 0,
|
|
530
|
+
schedule: row.schedule,
|
|
531
|
+
prompt: row.prompt,
|
|
532
|
+
status: row.status,
|
|
533
|
+
lastRunAt: row.lastRunAt ?? void 0,
|
|
534
|
+
nextRunAt: row.nextRunAt,
|
|
535
|
+
lastError: row.lastError ?? void 0,
|
|
536
|
+
createdAt: row.createdAt,
|
|
537
|
+
updatedAt: row.updatedAt
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/cron/scheduler.ts
|
|
543
|
+
function createCronJobScheduler(options) {
|
|
544
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
545
|
+
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
546
|
+
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
547
|
+
const logger = options.logger ?? console;
|
|
548
|
+
let timer;
|
|
549
|
+
let running = false;
|
|
550
|
+
const runDueNow = async () => {
|
|
551
|
+
if (running) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
running = true;
|
|
555
|
+
const startedAt = now();
|
|
556
|
+
try {
|
|
557
|
+
const jobs = options.repository.listDue(startedAt);
|
|
558
|
+
for (const job of jobs) {
|
|
559
|
+
try {
|
|
560
|
+
const text = await options.generateMessage(job, startedAt);
|
|
561
|
+
await options.sendTextToChat(job.chatId, text);
|
|
562
|
+
options.repository.markSuccess(job.id, startedAt);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
565
|
+
options.repository.markFailure(job.id, message, startedAt);
|
|
566
|
+
logger.error(`CRONJob \u6267\u884C\u5931\u8D25\uFF1A${job.id} ${message}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
} finally {
|
|
570
|
+
running = false;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
return {
|
|
574
|
+
start() {
|
|
575
|
+
if (timer) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
void runDueNow();
|
|
579
|
+
timer = setIntervalFn(() => {
|
|
580
|
+
void runDueNow();
|
|
581
|
+
}, 6e4);
|
|
582
|
+
},
|
|
583
|
+
stop() {
|
|
584
|
+
if (!timer) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
clearIntervalFn(timer);
|
|
588
|
+
timer = void 0;
|
|
589
|
+
},
|
|
590
|
+
runDueNow
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/cron/tools.ts
|
|
595
|
+
function readString(input, key) {
|
|
596
|
+
const value = typeof input === "object" && input !== null && key in input ? input[key] : void 0;
|
|
597
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
598
|
+
throw new Error(`${key} \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
|
|
599
|
+
}
|
|
600
|
+
return value.trim();
|
|
601
|
+
}
|
|
602
|
+
function createCronJobTools(input) {
|
|
603
|
+
return [
|
|
604
|
+
{
|
|
605
|
+
name: "create_cron_job",
|
|
606
|
+
description: "Create a scheduled AI message for the current Feishu chat only. The schedule must be a five-field cron string.",
|
|
607
|
+
inputSchema: {
|
|
608
|
+
type: "object",
|
|
609
|
+
properties: {
|
|
610
|
+
schedule: {
|
|
611
|
+
type: "string",
|
|
612
|
+
description: "Five-field cron schedule, for example 0 9 * * *."
|
|
613
|
+
},
|
|
614
|
+
prompt: {
|
|
615
|
+
type: "string",
|
|
616
|
+
description: "Prompt used later to generate the scheduled message."
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
required: ["schedule", "prompt"],
|
|
620
|
+
additionalProperties: false
|
|
621
|
+
},
|
|
622
|
+
execute: async (rawInput) => {
|
|
623
|
+
const job = input.repository.create({
|
|
624
|
+
chatId: input.chatId,
|
|
625
|
+
createdByOpenId: input.createdByOpenId,
|
|
626
|
+
schedule: readString(rawInput, "schedule"),
|
|
627
|
+
prompt: readString(rawInput, "prompt")
|
|
628
|
+
});
|
|
629
|
+
return JSON.stringify({ ok: true, job });
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "list_cron_jobs",
|
|
634
|
+
description: "List active scheduled AI messages for the current Feishu chat only.",
|
|
635
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
636
|
+
execute: async () => JSON.stringify({ ok: true, jobs: input.repository.listByChat(input.chatId) })
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "delete_cron_job",
|
|
640
|
+
description: "Delete a scheduled AI message by ID, only if it belongs to the current Feishu chat.",
|
|
641
|
+
inputSchema: {
|
|
642
|
+
type: "object",
|
|
643
|
+
properties: {
|
|
644
|
+
id: {
|
|
645
|
+
type: "string",
|
|
646
|
+
description: "Cron job ID returned by create_cron_job or list_cron_jobs."
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
required: ["id"],
|
|
650
|
+
additionalProperties: false
|
|
651
|
+
},
|
|
652
|
+
execute: async (rawInput) => {
|
|
653
|
+
const id = readString(rawInput, "id");
|
|
654
|
+
const ok = input.repository.deleteByChat(id, input.chatId);
|
|
655
|
+
return JSON.stringify({
|
|
656
|
+
ok,
|
|
657
|
+
id,
|
|
658
|
+
message: ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664\u3002" : "\u6CA1\u6709\u627E\u5230\u5F53\u524D\u7FA4\u91CC\u7684\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\u3002"
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
|
|
174
665
|
// src/data/deletion.ts
|
|
175
666
|
import fs2 from "fs/promises";
|
|
176
667
|
import path4 from "path";
|
|
@@ -451,6 +942,23 @@ function migrateDatabase(database) {
|
|
|
451
942
|
);
|
|
452
943
|
|
|
453
944
|
CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
|
|
945
|
+
|
|
946
|
+
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
947
|
+
id TEXT PRIMARY KEY,
|
|
948
|
+
chat_id TEXT NOT NULL,
|
|
949
|
+
created_by_open_id TEXT,
|
|
950
|
+
schedule TEXT NOT NULL,
|
|
951
|
+
prompt TEXT NOT NULL,
|
|
952
|
+
status TEXT NOT NULL CHECK(status IN ('active','deleted')),
|
|
953
|
+
last_run_at TEXT,
|
|
954
|
+
next_run_at TEXT NOT NULL,
|
|
955
|
+
last_error TEXT,
|
|
956
|
+
created_at TEXT NOT NULL,
|
|
957
|
+
updated_at TEXT NOT NULL
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
|
|
961
|
+
CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
|
|
454
962
|
`);
|
|
455
963
|
}
|
|
456
964
|
|
|
@@ -458,13 +966,13 @@ function migrateDatabase(database) {
|
|
|
458
966
|
import fs6 from "fs/promises";
|
|
459
967
|
|
|
460
968
|
// src/files/jobs.ts
|
|
461
|
-
import
|
|
969
|
+
import crypto2 from "crypto";
|
|
462
970
|
import path6 from "path";
|
|
463
971
|
function nowIso() {
|
|
464
972
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
465
973
|
}
|
|
466
974
|
function stableJobId(sourcePath) {
|
|
467
|
-
return
|
|
975
|
+
return crypto2.createHash("sha256").update(path6.resolve(sourcePath)).digest("hex").slice(0, 32);
|
|
468
976
|
}
|
|
469
977
|
function parseWarnings(value) {
|
|
470
978
|
try {
|
|
@@ -881,7 +1389,8 @@ function toOpenAIMessage(message) {
|
|
|
881
1389
|
arguments: JSON.stringify(toolCall.input)
|
|
882
1390
|
}
|
|
883
1391
|
}))
|
|
884
|
-
} : {}
|
|
1392
|
+
} : {},
|
|
1393
|
+
...message.reasoningContent ? { reasoning_content: message.reasoningContent } : {}
|
|
885
1394
|
};
|
|
886
1395
|
}
|
|
887
1396
|
function toOpenAITool(tool) {
|
|
@@ -927,7 +1436,8 @@ var OpenAICompatibleChatModel = class {
|
|
|
927
1436
|
throw new Error(`LLM \u8BF7\u6C42\u5931\u8D25\uFF1A${response.status} ${body}`);
|
|
928
1437
|
}
|
|
929
1438
|
const data = await response.json();
|
|
930
|
-
const
|
|
1439
|
+
const message = data.choices?.[0]?.message;
|
|
1440
|
+
const content = message?.content?.trim();
|
|
931
1441
|
if (!content) {
|
|
932
1442
|
throw new Error("LLM \u8FD4\u56DE\u4E3A\u7A7A\u3002");
|
|
933
1443
|
}
|
|
@@ -959,7 +1469,8 @@ var OpenAICompatibleChatModel = class {
|
|
|
959
1469
|
const message = data.choices?.[0]?.message;
|
|
960
1470
|
return {
|
|
961
1471
|
content: message?.content ?? "",
|
|
962
|
-
toolCalls: parseToolCalls(message)
|
|
1472
|
+
toolCalls: parseToolCalls(message),
|
|
1473
|
+
reasoningContent: message?.reasoning_content ?? void 0
|
|
963
1474
|
};
|
|
964
1475
|
}
|
|
965
1476
|
};
|
|
@@ -1011,7 +1522,7 @@ function createEmbeddingModel(config, secrets) {
|
|
|
1011
1522
|
}
|
|
1012
1523
|
|
|
1013
1524
|
// src/messages/repository.ts
|
|
1014
|
-
import
|
|
1525
|
+
import crypto3 from "crypto";
|
|
1015
1526
|
|
|
1016
1527
|
// src/messages/chunker.ts
|
|
1017
1528
|
function chunkText(text, maxChars = 900, overlapChars = 120) {
|
|
@@ -1040,7 +1551,7 @@ function nowIso2() {
|
|
|
1040
1551
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1041
1552
|
}
|
|
1042
1553
|
function stableId(parts) {
|
|
1043
|
-
return
|
|
1554
|
+
return crypto3.createHash("sha256").update(parts.join("")).digest("hex").slice(0, 32);
|
|
1044
1555
|
}
|
|
1045
1556
|
function escapeFtsQuery(query) {
|
|
1046
1557
|
const terms = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter(Boolean);
|
|
@@ -1070,6 +1581,22 @@ function buildSearchTerms(query) {
|
|
|
1070
1581
|
}
|
|
1071
1582
|
return [trimmed];
|
|
1072
1583
|
}
|
|
1584
|
+
function buildScopeWhere(scope) {
|
|
1585
|
+
const clauses = [];
|
|
1586
|
+
const params = [];
|
|
1587
|
+
if (scope?.platform) {
|
|
1588
|
+
clauses.push("m.platform = ?");
|
|
1589
|
+
params.push(scope.platform);
|
|
1590
|
+
}
|
|
1591
|
+
if (scope?.platformChatId) {
|
|
1592
|
+
clauses.push("c.platform_chat_id = ?");
|
|
1593
|
+
params.push(scope.platformChatId);
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
1597
|
+
params
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1073
1600
|
function parseRawPayload(value) {
|
|
1074
1601
|
try {
|
|
1075
1602
|
const parsed = JSON.parse(value);
|
|
@@ -1275,6 +1802,7 @@ var MessageRepository = class {
|
|
|
1275
1802
|
const ftsQuery = escapeFtsQuery(query);
|
|
1276
1803
|
const excludedIds = options.excludeMessageIds ?? [];
|
|
1277
1804
|
const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
|
|
1805
|
+
const scope = buildScopeWhere(options.scope);
|
|
1278
1806
|
const ftsResults = this.database.prepare(
|
|
1279
1807
|
`
|
|
1280
1808
|
SELECT
|
|
@@ -1293,10 +1821,11 @@ var MessageRepository = class {
|
|
|
1293
1821
|
JOIN chats c ON c.id = m.chat_id
|
|
1294
1822
|
WHERE message_chunks_fts MATCH ?
|
|
1295
1823
|
${excludedWhere}
|
|
1824
|
+
${scope.where}
|
|
1296
1825
|
ORDER BY bm25(message_chunks_fts)
|
|
1297
1826
|
LIMIT ?
|
|
1298
1827
|
`
|
|
1299
|
-
).all(ftsQuery, ...excludedIds, limit);
|
|
1828
|
+
).all(ftsQuery, ...excludedIds, ...scope.params, limit);
|
|
1300
1829
|
if (ftsResults.length > 0) {
|
|
1301
1830
|
return ftsResults;
|
|
1302
1831
|
}
|
|
@@ -1324,10 +1853,11 @@ var MessageRepository = class {
|
|
|
1324
1853
|
JOIN chats c ON c.id = m.chat_id
|
|
1325
1854
|
WHERE (${where})
|
|
1326
1855
|
${likeExcludedWhere}
|
|
1856
|
+
${scope.where}
|
|
1327
1857
|
ORDER BY m.sent_at DESC
|
|
1328
1858
|
LIMIT ?
|
|
1329
1859
|
`
|
|
1330
|
-
).all(...params, ...excludedIds, limit);
|
|
1860
|
+
).all(...params, ...excludedIds, ...scope.params, limit);
|
|
1331
1861
|
}
|
|
1332
1862
|
getChatCount() {
|
|
1333
1863
|
return this.database.prepare("SELECT COUNT(*) AS count FROM chats").get().count;
|
|
@@ -1387,7 +1917,7 @@ var MessageRepository = class {
|
|
|
1387
1917
|
};
|
|
1388
1918
|
|
|
1389
1919
|
// src/episodes/repository.ts
|
|
1390
|
-
import
|
|
1920
|
+
import crypto4 from "crypto";
|
|
1391
1921
|
|
|
1392
1922
|
// src/episodes/sanitizer.ts
|
|
1393
1923
|
var SECRET_PATTERNS = [
|
|
@@ -1414,7 +1944,7 @@ function nowIso3() {
|
|
|
1414
1944
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1415
1945
|
}
|
|
1416
1946
|
function stableId2(parts) {
|
|
1417
|
-
return
|
|
1947
|
+
return crypto4.createHash("sha256").update(parts.join("")).digest("hex").slice(0, 32);
|
|
1418
1948
|
}
|
|
1419
1949
|
function escapeFtsQuery2(query) {
|
|
1420
1950
|
const terms = query.trim().split(/\s+/).map((term) => term.replace(/[^\p{L}\p{N}_-]+/gu, " ").trim()).flatMap((term) => term.split(/\s+/)).filter(Boolean);
|
|
@@ -1427,6 +1957,22 @@ function toMillis(value) {
|
|
|
1427
1957
|
const time = Date.parse(value);
|
|
1428
1958
|
return Number.isFinite(time) ? time : 0;
|
|
1429
1959
|
}
|
|
1960
|
+
function buildScopeWhere2(scope) {
|
|
1961
|
+
const clauses = [];
|
|
1962
|
+
const params = [];
|
|
1963
|
+
if (scope?.platform) {
|
|
1964
|
+
clauses.push("c.platform = ?");
|
|
1965
|
+
params.push(scope.platform);
|
|
1966
|
+
}
|
|
1967
|
+
if (scope?.platformChatId) {
|
|
1968
|
+
clauses.push("c.platform_chat_id = ?");
|
|
1969
|
+
params.push(scope.platformChatId);
|
|
1970
|
+
}
|
|
1971
|
+
return {
|
|
1972
|
+
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
1973
|
+
params
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1430
1976
|
var EpisodeRepository = class {
|
|
1431
1977
|
constructor(database) {
|
|
1432
1978
|
this.database = database;
|
|
@@ -1609,8 +2155,9 @@ var EpisodeRepository = class {
|
|
|
1609
2155
|
`
|
|
1610
2156
|
).all(limit);
|
|
1611
2157
|
}
|
|
1612
|
-
searchEpisodes(query, limit = 8) {
|
|
2158
|
+
searchEpisodes(query, limit = 8, scope) {
|
|
1613
2159
|
const ftsQuery = escapeFtsQuery2(query);
|
|
2160
|
+
const scopeWhere = buildScopeWhere2(scope);
|
|
1614
2161
|
return this.database.prepare(
|
|
1615
2162
|
`
|
|
1616
2163
|
SELECT
|
|
@@ -1638,11 +2185,12 @@ var EpisodeRepository = class {
|
|
|
1638
2185
|
JOIN memory_episodes e ON e.id = fts.episode_id
|
|
1639
2186
|
JOIN chats c ON c.id = e.chat_id
|
|
1640
2187
|
WHERE memory_episodes_fts MATCH ?
|
|
2188
|
+
${scopeWhere.where}
|
|
1641
2189
|
GROUP BY e.id
|
|
1642
2190
|
ORDER BY e.ended_at DESC
|
|
1643
2191
|
LIMIT ?
|
|
1644
2192
|
`
|
|
1645
|
-
).all(ftsQuery, limit).map((row) => {
|
|
2193
|
+
).all(ftsQuery, ...scopeWhere.params, limit).map((row) => {
|
|
1646
2194
|
const item = row;
|
|
1647
2195
|
return {
|
|
1648
2196
|
...item,
|
|
@@ -1672,8 +2220,8 @@ var EpisodeFtsRetriever = class {
|
|
|
1672
2220
|
this.episodes = episodes;
|
|
1673
2221
|
}
|
|
1674
2222
|
episodes;
|
|
1675
|
-
async retrieve(question) {
|
|
1676
|
-
return this.episodes.searchEpisodes(question, 8).map(toEpisodeEvidence);
|
|
2223
|
+
async retrieve(question, scope) {
|
|
2224
|
+
return this.episodes.searchEpisodes(question, 8, scope).map(toEpisodeEvidence);
|
|
1677
2225
|
}
|
|
1678
2226
|
};
|
|
1679
2227
|
|
|
@@ -1699,8 +2247,9 @@ var HybridRetriever = class {
|
|
|
1699
2247
|
}
|
|
1700
2248
|
retrievers;
|
|
1701
2249
|
options;
|
|
1702
|
-
async retrieve(question) {
|
|
1703
|
-
const
|
|
2250
|
+
async retrieve(question, scope) {
|
|
2251
|
+
const effectiveScope = scope ?? this.options.scope;
|
|
2252
|
+
const results = await Promise.all(this.retrievers.map((retriever) => retriever.retrieve(question, effectiveScope)));
|
|
1704
2253
|
const merged = /* @__PURE__ */ new Map();
|
|
1705
2254
|
for (const evidenceList of results) {
|
|
1706
2255
|
for (const evidence of evidenceList) {
|
|
@@ -1741,9 +2290,10 @@ var MessageFtsRetriever = class {
|
|
|
1741
2290
|
}
|
|
1742
2291
|
messages;
|
|
1743
2292
|
options;
|
|
1744
|
-
async retrieve(question) {
|
|
2293
|
+
async retrieve(question, scope) {
|
|
1745
2294
|
const results = this.messages.searchMessages(question, 8, {
|
|
1746
|
-
excludeMessageIds: this.options.excludeMessageIds
|
|
2295
|
+
excludeMessageIds: this.options.excludeMessageIds,
|
|
2296
|
+
scope
|
|
1747
2297
|
});
|
|
1748
2298
|
return results.map((result) => ({
|
|
1749
2299
|
id: result.chunkId,
|
|
@@ -1778,17 +2328,17 @@ function parseSearchInput(input) {
|
|
|
1778
2328
|
const limit = Math.min(12, Math.max(1, Math.floor(numericLimit)));
|
|
1779
2329
|
return { query, limit };
|
|
1780
2330
|
}
|
|
1781
|
-
async function runRetriever(retriever, input) {
|
|
2331
|
+
async function runRetriever(retriever, input, scope) {
|
|
1782
2332
|
const { query, limit } = parseSearchInput(input);
|
|
1783
|
-
const results = await retriever.retrieve(query);
|
|
2333
|
+
const results = await retriever.retrieve(query, scope);
|
|
1784
2334
|
return results.slice(0, limit);
|
|
1785
2335
|
}
|
|
1786
|
-
function createSearchTool(name, description, retriever) {
|
|
2336
|
+
function createSearchTool(name, description, retriever, scope) {
|
|
1787
2337
|
return {
|
|
1788
2338
|
name,
|
|
1789
2339
|
description,
|
|
1790
2340
|
inputSchema: searchInputSchema,
|
|
1791
|
-
execute: (input) => runRetriever(retriever, input)
|
|
2341
|
+
execute: (input) => runRetriever(retriever, input, scope)
|
|
1792
2342
|
};
|
|
1793
2343
|
}
|
|
1794
2344
|
function createRagSearchTools(input) {
|
|
@@ -1796,17 +2346,20 @@ function createRagSearchTools(input) {
|
|
|
1796
2346
|
createSearchTool(
|
|
1797
2347
|
"hybrid_search",
|
|
1798
2348
|
"Search across all indexed RAG evidence using the default hybrid retrieval strategy.",
|
|
1799
|
-
input.hybrid
|
|
2349
|
+
input.hybrid,
|
|
2350
|
+
input.scope
|
|
1800
2351
|
),
|
|
1801
2352
|
createSearchTool(
|
|
1802
2353
|
"search_messages",
|
|
1803
2354
|
"Search chat messages only when the answer likely depends on message-level evidence.",
|
|
1804
|
-
input.messages
|
|
2355
|
+
input.messages,
|
|
2356
|
+
input.scope
|
|
1805
2357
|
),
|
|
1806
2358
|
createSearchTool(
|
|
1807
2359
|
"search_episodes",
|
|
1808
2360
|
"Search episode summaries only when the answer likely depends on longer-running context.",
|
|
1809
|
-
input.episodes
|
|
2361
|
+
input.episodes,
|
|
2362
|
+
input.scope
|
|
1810
2363
|
)
|
|
1811
2364
|
];
|
|
1812
2365
|
if (input.semantic) {
|
|
@@ -1814,7 +2367,8 @@ function createRagSearchTools(input) {
|
|
|
1814
2367
|
createSearchTool(
|
|
1815
2368
|
"semantic_search",
|
|
1816
2369
|
"Search semantic vector evidence only when broader conceptual recall is needed.",
|
|
1817
|
-
input.semantic
|
|
2370
|
+
input.semantic,
|
|
2371
|
+
input.scope
|
|
1818
2372
|
)
|
|
1819
2373
|
);
|
|
1820
2374
|
}
|
|
@@ -1859,6 +2413,22 @@ function toEvidenceSource2(row) {
|
|
|
1859
2413
|
timestamp: row.sentAt
|
|
1860
2414
|
};
|
|
1861
2415
|
}
|
|
2416
|
+
function buildScopeWhere3(scope) {
|
|
2417
|
+
const clauses = [];
|
|
2418
|
+
const params = [];
|
|
2419
|
+
if (scope?.platform) {
|
|
2420
|
+
clauses.push("m.platform = ?");
|
|
2421
|
+
params.push(scope.platform);
|
|
2422
|
+
}
|
|
2423
|
+
if (scope?.platformChatId) {
|
|
2424
|
+
clauses.push("c.platform_chat_id = ?");
|
|
2425
|
+
params.push(scope.platformChatId);
|
|
2426
|
+
}
|
|
2427
|
+
return {
|
|
2428
|
+
where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
|
|
2429
|
+
params
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
1862
2432
|
var SqliteVectorStore = class {
|
|
1863
2433
|
constructor(database, options) {
|
|
1864
2434
|
this.database = database;
|
|
@@ -1893,10 +2463,11 @@ var SqliteVectorStore = class {
|
|
|
1893
2463
|
});
|
|
1894
2464
|
transaction(records);
|
|
1895
2465
|
}
|
|
1896
|
-
async search(vector, limit) {
|
|
2466
|
+
async search(vector, limit, scope) {
|
|
1897
2467
|
if (limit <= 0) {
|
|
1898
2468
|
return [];
|
|
1899
2469
|
}
|
|
2470
|
+
const scopeWhere = buildScopeWhere3(scope);
|
|
1900
2471
|
const rows = this.database.prepare(
|
|
1901
2472
|
`
|
|
1902
2473
|
SELECT
|
|
@@ -1911,8 +2482,9 @@ var SqliteVectorStore = class {
|
|
|
1911
2482
|
JOIN messages m ON m.id = mc.message_id
|
|
1912
2483
|
JOIN chats c ON c.id = m.chat_id
|
|
1913
2484
|
WHERE e.model = ?
|
|
2485
|
+
${scopeWhere.where}
|
|
1914
2486
|
`
|
|
1915
|
-
).all(this.options.model);
|
|
2487
|
+
).all(this.options.model, ...scopeWhere.params);
|
|
1916
2488
|
return rows.flatMap((row) => {
|
|
1917
2489
|
const storedVector = parseEmbeddingJson(row.embeddingJson);
|
|
1918
2490
|
if (storedVector.length === 0) {
|
|
@@ -1944,9 +2516,9 @@ var VectorRetriever = class {
|
|
|
1944
2516
|
embedding;
|
|
1945
2517
|
store;
|
|
1946
2518
|
limit;
|
|
1947
|
-
async retrieve(question) {
|
|
2519
|
+
async retrieve(question, scope) {
|
|
1948
2520
|
const vector = await this.embedding.embed(question);
|
|
1949
|
-
return this.store.search(vector, this.limit);
|
|
2521
|
+
return this.store.search(vector, this.limit, scope);
|
|
1950
2522
|
}
|
|
1951
2523
|
};
|
|
1952
2524
|
|
|
@@ -1967,7 +2539,7 @@ async function createHybridRetriever(input) {
|
|
|
1967
2539
|
retrievers.push(new VectorRetriever(createEmbeddingModel(input.config, input.secrets), vectorStore));
|
|
1968
2540
|
}
|
|
1969
2541
|
return {
|
|
1970
|
-
retriever: new HybridRetriever(retrievers),
|
|
2542
|
+
retriever: new HybridRetriever(retrievers, { scope: input.scope }),
|
|
1971
2543
|
close: () => {
|
|
1972
2544
|
for (const closer of closers) {
|
|
1973
2545
|
closer();
|
|
@@ -1984,7 +2556,7 @@ async function createAgenticRagSearchTools(input) {
|
|
|
1984
2556
|
) : void 0;
|
|
1985
2557
|
const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
|
|
1986
2558
|
return {
|
|
1987
|
-
tools: createRagSearchTools({ hybrid, messages, episodes, semantic }),
|
|
2559
|
+
tools: createRagSearchTools({ hybrid, messages, episodes, semantic, scope: input.scope }),
|
|
1988
2560
|
close: () => {
|
|
1989
2561
|
}
|
|
1990
2562
|
};
|
|
@@ -2489,11 +3061,11 @@ function createIndexingScheduler(options) {
|
|
|
2489
3061
|
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
2490
3062
|
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
2491
3063
|
const logger = options.logger ?? console;
|
|
2492
|
-
const parsed =
|
|
3064
|
+
const parsed = parseCronSchedule2(options.schedule);
|
|
2493
3065
|
let timer;
|
|
2494
3066
|
let running = false;
|
|
2495
3067
|
const runDueNow = async () => {
|
|
2496
|
-
if (!parsed || running || !
|
|
3068
|
+
if (!parsed || running || !matchesParsedSchedule2(parsed, now())) {
|
|
2497
3069
|
return;
|
|
2498
3070
|
}
|
|
2499
3071
|
running = true;
|
|
@@ -2525,25 +3097,25 @@ function createIndexingScheduler(options) {
|
|
|
2525
3097
|
runDueNow
|
|
2526
3098
|
};
|
|
2527
3099
|
}
|
|
2528
|
-
function
|
|
3100
|
+
function matchesParsedSchedule2(schedule, date) {
|
|
2529
3101
|
return schedule.minute(date.getMinutes()) && schedule.hour(date.getHours()) && schedule.dayOfMonth(date.getDate()) && schedule.month(date.getMonth() + 1) && schedule.dayOfWeek(date.getDay());
|
|
2530
3102
|
}
|
|
2531
|
-
function
|
|
3103
|
+
function parseCronSchedule2(schedule) {
|
|
2532
3104
|
const fields = schedule.trim().split(/\s+/);
|
|
2533
3105
|
if (fields.length !== 5) {
|
|
2534
3106
|
return null;
|
|
2535
3107
|
}
|
|
2536
|
-
const minute =
|
|
2537
|
-
const hour =
|
|
2538
|
-
const dayOfMonth =
|
|
2539
|
-
const month =
|
|
2540
|
-
const dayOfWeek =
|
|
3108
|
+
const minute = parseMinuteField2(fields[0]);
|
|
3109
|
+
const hour = parseExactOrWildcardField2(fields[1], 0, 23);
|
|
3110
|
+
const dayOfMonth = parseExactOrWildcardField2(fields[2], 1, 31);
|
|
3111
|
+
const month = parseExactOrWildcardField2(fields[3], 1, 12);
|
|
3112
|
+
const dayOfWeek = parseExactOrWildcardField2(fields[4], 0, 6);
|
|
2541
3113
|
if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
|
|
2542
3114
|
return null;
|
|
2543
3115
|
}
|
|
2544
3116
|
return { minute, hour, dayOfMonth, month, dayOfWeek };
|
|
2545
3117
|
}
|
|
2546
|
-
function
|
|
3118
|
+
function parseMinuteField2(field) {
|
|
2547
3119
|
if (field === "*") {
|
|
2548
3120
|
return () => true;
|
|
2549
3121
|
}
|
|
@@ -2556,30 +3128,30 @@ function parseMinuteField(field) {
|
|
|
2556
3128
|
return (value) => value % step === 0;
|
|
2557
3129
|
}
|
|
2558
3130
|
if (field.includes(",")) {
|
|
2559
|
-
const values = field.split(",").map((part) =>
|
|
3131
|
+
const values = field.split(",").map((part) => parseExactNumber2(part, 0, 59));
|
|
2560
3132
|
if (values.some((value) => value === null)) {
|
|
2561
3133
|
return null;
|
|
2562
3134
|
}
|
|
2563
3135
|
const allowed = new Set(values);
|
|
2564
3136
|
return (value) => allowed.has(value);
|
|
2565
3137
|
}
|
|
2566
|
-
const exact =
|
|
3138
|
+
const exact = parseExactNumber2(field, 0, 59);
|
|
2567
3139
|
if (exact === null) {
|
|
2568
3140
|
return null;
|
|
2569
3141
|
}
|
|
2570
3142
|
return (value) => value === exact;
|
|
2571
3143
|
}
|
|
2572
|
-
function
|
|
3144
|
+
function parseExactOrWildcardField2(field, min, max) {
|
|
2573
3145
|
if (field === "*") {
|
|
2574
3146
|
return () => true;
|
|
2575
3147
|
}
|
|
2576
|
-
const exact =
|
|
3148
|
+
const exact = parseExactNumber2(field, min, max);
|
|
2577
3149
|
if (exact === null) {
|
|
2578
3150
|
return null;
|
|
2579
3151
|
}
|
|
2580
3152
|
return (value) => value === exact;
|
|
2581
3153
|
}
|
|
2582
|
-
function
|
|
3154
|
+
function parseExactNumber2(field, min, max) {
|
|
2583
3155
|
if (!/^\d+$/.test(field)) {
|
|
2584
3156
|
return null;
|
|
2585
3157
|
}
|
|
@@ -2669,12 +3241,12 @@ async function processMessagesNow(input) {
|
|
|
2669
3241
|
}
|
|
2670
3242
|
|
|
2671
3243
|
// src/multimodal/tasks.ts
|
|
2672
|
-
import
|
|
3244
|
+
import crypto5 from "crypto";
|
|
2673
3245
|
function nowIso4() {
|
|
2674
3246
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2675
3247
|
}
|
|
2676
3248
|
function stableId3(sourceMessageId, imageKey) {
|
|
2677
|
-
return
|
|
3249
|
+
return crypto5.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
|
|
2678
3250
|
}
|
|
2679
3251
|
function mapRow(row) {
|
|
2680
3252
|
if (!row) {
|
|
@@ -2923,238 +3495,15 @@ var ImageMultimodalWorker = class {
|
|
|
2923
3495
|
this.options.tasks.markSucceeded(running.id, derivedMessageId);
|
|
2924
3496
|
result.succeeded += 1;
|
|
2925
3497
|
} catch (error) {
|
|
2926
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2927
|
-
this.options.tasks.markFailed(running.id, message, running.attempts >= 3);
|
|
2928
|
-
result.failed += 1;
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
};
|
|
2932
|
-
|
|
2933
|
-
// src/rag/citations.ts
|
|
2934
|
-
function isOpaqueId(value) {
|
|
2935
|
-
return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
|
|
2936
|
-
}
|
|
2937
|
-
function formatTime(value) {
|
|
2938
|
-
if (!value) {
|
|
2939
|
-
return "\u672A\u77E5\u65F6\u95F4";
|
|
2940
|
-
}
|
|
2941
|
-
const date = new Date(value);
|
|
2942
|
-
if (Number.isNaN(date.getTime())) {
|
|
2943
|
-
return value;
|
|
2944
|
-
}
|
|
2945
|
-
const pad = (input) => String(input).padStart(2, "0");
|
|
2946
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
2947
|
-
}
|
|
2948
|
-
function formatSpeaker(source) {
|
|
2949
|
-
if (source.type === "file") {
|
|
2950
|
-
return isOpaqueId(source.label) ? "\u6587\u4EF6" : `\u6587\u4EF6 ${source.label}`;
|
|
2951
|
-
}
|
|
2952
|
-
if (source.sender && !isOpaqueId(source.sender)) {
|
|
2953
|
-
return source.sender;
|
|
2954
|
-
}
|
|
2955
|
-
return "\u7FA4\u6210\u5458";
|
|
2956
|
-
}
|
|
2957
|
-
function clipText(value, maxLength) {
|
|
2958
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
2959
|
-
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
2960
|
-
}
|
|
2961
|
-
function formatCitation(citation, options = {}) {
|
|
2962
|
-
const maxTextLength = options.maxTextLength ?? 120;
|
|
2963
|
-
const speaker = formatSpeaker(citation.source);
|
|
2964
|
-
const time = formatTime(citation.source.timestamp);
|
|
2965
|
-
const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
|
|
2966
|
-
return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
|
|
2967
|
-
}
|
|
2968
|
-
function formatCitations(citations, options = {}) {
|
|
2969
|
-
return citations.map((citation) => formatCitation(citation, options)).join("\n");
|
|
2970
|
-
}
|
|
2971
|
-
|
|
2972
|
-
// src/rag/answer.ts
|
|
2973
|
-
var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
|
|
2974
|
-
var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
|
|
2975
|
-
var SCORE_TIE_THRESHOLD = 0.15;
|
|
2976
|
-
function parseTimestamp(value) {
|
|
2977
|
-
if (!value) {
|
|
2978
|
-
return 0;
|
|
2979
|
-
}
|
|
2980
|
-
const time = Date.parse(value);
|
|
2981
|
-
return Number.isFinite(time) ? time : 0;
|
|
2982
|
-
}
|
|
2983
|
-
function rankEvidenceForPrompt(evidence) {
|
|
2984
|
-
return [...evidence].sort((left, right) => {
|
|
2985
|
-
const scoreDiff = right.score - left.score;
|
|
2986
|
-
if (Math.abs(scoreDiff) > SCORE_TIE_THRESHOLD) {
|
|
2987
|
-
return scoreDiff;
|
|
2988
|
-
}
|
|
2989
|
-
const timeDiff = parseTimestamp(right.source.timestamp) - parseTimestamp(left.source.timestamp);
|
|
2990
|
-
if (timeDiff !== 0) {
|
|
2991
|
-
return timeDiff;
|
|
2992
|
-
}
|
|
2993
|
-
return scoreDiff;
|
|
2994
|
-
});
|
|
2995
|
-
}
|
|
2996
|
-
function buildEvidencePrompt(question, evidence, options = {}) {
|
|
2997
|
-
if (evidence.length === 0) {
|
|
2998
|
-
throw new Error("RAG evidence is required before answer generation.");
|
|
2999
|
-
}
|
|
3000
|
-
const maxEvidenceBlocks = options.maxEvidenceBlocks ?? DEFAULT_MAX_EVIDENCE_BLOCKS;
|
|
3001
|
-
const maxCharsPerBlock = options.maxCharsPerBlock ?? DEFAULT_MAX_CHARS_PER_BLOCK;
|
|
3002
|
-
const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
|
|
3003
|
-
const citations = selected.map((item, index) => ({
|
|
3004
|
-
marker: `S${index + 1}`,
|
|
3005
|
-
evidenceId: item.id,
|
|
3006
|
-
source: item.source,
|
|
3007
|
-
text: item.text
|
|
3008
|
-
}));
|
|
3009
|
-
const evidenceText = selected.map((item, index) => {
|
|
3010
|
-
const marker = citations[index]?.marker;
|
|
3011
|
-
const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
|
|
3012
|
-
const sourceParts = [
|
|
3013
|
-
item.source.label,
|
|
3014
|
-
item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
|
|
3015
|
-
item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
|
|
3016
|
-
item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
|
|
3017
|
-
].filter(Boolean);
|
|
3018
|
-
return `[${marker}]
|
|
3019
|
-
\u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
|
|
3020
|
-
\u5185\u5BB9\uFF1A${clippedText}`;
|
|
3021
|
-
}).join("\n\n");
|
|
3022
|
-
return {
|
|
3023
|
-
citations,
|
|
3024
|
-
messages: [
|
|
3025
|
-
{
|
|
3026
|
-
role: "system",
|
|
3027
|
-
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"
|
|
3028
|
-
},
|
|
3029
|
-
{
|
|
3030
|
-
role: "user",
|
|
3031
|
-
content: `\u95EE\u9898\uFF1A${question}
|
|
3032
|
-
|
|
3033
|
-
\u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
|
|
3034
|
-
1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
|
|
3035
|
-
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
|
|
3036
|
-
3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
|
|
3037
|
-
|
|
3038
|
-
\u68C0\u7D22\u8BC1\u636E\uFF1A
|
|
3039
|
-
${evidenceText}`
|
|
3040
|
-
}
|
|
3041
|
-
]
|
|
3042
|
-
};
|
|
3043
|
-
}
|
|
3044
|
-
async function generateGroundedAnswer(input) {
|
|
3045
|
-
const prompt = buildEvidencePrompt(input.question, input.evidence);
|
|
3046
|
-
const answer = await input.model.complete(prompt.messages);
|
|
3047
|
-
return {
|
|
3048
|
-
answer,
|
|
3049
|
-
citations: prompt.citations
|
|
3050
|
-
};
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
// src/rag/agentic-qa-service.ts
|
|
3054
|
-
var DEFAULT_MAX_MODEL_TURNS = 4;
|
|
3055
|
-
var DEFAULT_MAX_TOOL_CALLS = 8;
|
|
3056
|
-
var DEFAULT_MAX_EVIDENCE = 12;
|
|
3057
|
-
var NO_EVIDENCE_ANSWER = "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002";
|
|
3058
|
-
var AGENTIC_SYSTEM_PROMPT = "\u4F60\u662F\u672C\u5730\u77E5\u8BC6\u4FE1\u606F\u6536\u96C6\u4EE3\u7406\u3002\u4F60\u7684\u804C\u8D23\u662F\u56F4\u7ED5\u7528\u6237\u95EE\u9898\u51B3\u5B9A\u662F\u5426\u8C03\u7528\u641C\u7D22\u5DE5\u5177\u3001\u9009\u62E9\u5408\u9002\u7684\u5DE5\u5177\u548C\u67E5\u8BE2\u8BCD\uFF0C\u5E76\u6839\u636E\u5F53\u524D\u7ED3\u679C\u51B3\u5B9A\u662F\u5426\u7EE7\u7EED\u641C\u7D22\u3002\u4E0D\u8981\u7F16\u9020\u4EFB\u4F55\u8BC1\u636E\u6216\u58F0\u79F0\u770B\u8FC7\u672A\u68C0\u7D22\u5230\u7684\u5185\u5BB9\u3002\u4F60\u7684\u8F93\u51FA\u53EA\u7528\u4E8E\u6536\u96C6\u8BC1\u636E\uFF0C\u6700\u7EC8\u7B54\u6848\u4F1A\u7531\u53E6\u4E00\u4E2A\u57FA\u4E8E\u8BC1\u636E\u7684\u6B65\u9AA4\u751F\u6210\u3002";
|
|
3059
|
-
function toToolResultContent(results) {
|
|
3060
|
-
return JSON.stringify(
|
|
3061
|
-
results.map((item) => ({
|
|
3062
|
-
id: item.id,
|
|
3063
|
-
text: item.text,
|
|
3064
|
-
score: item.score,
|
|
3065
|
-
source: item.source
|
|
3066
|
-
}))
|
|
3067
|
-
);
|
|
3068
|
-
}
|
|
3069
|
-
function toToolErrorContent(message) {
|
|
3070
|
-
return JSON.stringify({ error: message });
|
|
3071
|
-
}
|
|
3072
|
-
function dedupeEvidence(evidence, maxEvidence) {
|
|
3073
|
-
const deduped = [];
|
|
3074
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3075
|
-
for (const item of evidence) {
|
|
3076
|
-
if (seen.has(item.id)) {
|
|
3077
|
-
continue;
|
|
3078
|
-
}
|
|
3079
|
-
seen.add(item.id);
|
|
3080
|
-
deduped.push(item);
|
|
3081
|
-
if (deduped.length >= maxEvidence) {
|
|
3082
|
-
break;
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
return deduped;
|
|
3086
|
-
}
|
|
3087
|
-
async function askWithAgenticRag(input) {
|
|
3088
|
-
if (!input.model.completeWithTools) {
|
|
3089
|
-
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3090
|
-
}
|
|
3091
|
-
const maxModelTurns = input.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3092
|
-
const maxToolCalls = input.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
3093
|
-
const maxEvidence = input.maxEvidence ?? DEFAULT_MAX_EVIDENCE;
|
|
3094
|
-
const messages = [
|
|
3095
|
-
{ role: "system", content: AGENTIC_SYSTEM_PROMPT },
|
|
3096
|
-
{ role: "user", content: input.question }
|
|
3097
|
-
];
|
|
3098
|
-
const toolsByName = new Map(input.tools.map((tool) => [tool.name, tool]));
|
|
3099
|
-
let evidence = [];
|
|
3100
|
-
let toolCallsUsed = 0;
|
|
3101
|
-
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
3102
|
-
const assistantResult = await input.model.completeWithTools(messages, input.tools);
|
|
3103
|
-
messages.push({
|
|
3104
|
-
role: "assistant",
|
|
3105
|
-
content: assistantResult.content,
|
|
3106
|
-
toolCalls: assistantResult.toolCalls
|
|
3107
|
-
});
|
|
3108
|
-
if (assistantResult.toolCalls.length === 0) {
|
|
3109
|
-
break;
|
|
3110
|
-
}
|
|
3111
|
-
for (const toolCall of assistantResult.toolCalls) {
|
|
3112
|
-
if (toolCallsUsed >= maxToolCalls) {
|
|
3113
|
-
break;
|
|
3114
|
-
}
|
|
3115
|
-
toolCallsUsed += 1;
|
|
3116
|
-
const tool = toolsByName.get(toolCall.name);
|
|
3117
|
-
if (!tool) {
|
|
3118
|
-
messages.push({
|
|
3119
|
-
role: "tool",
|
|
3120
|
-
toolCallId: toolCall.id,
|
|
3121
|
-
content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
|
|
3122
|
-
});
|
|
3123
|
-
continue;
|
|
3124
|
-
}
|
|
3125
|
-
try {
|
|
3126
|
-
const results = await tool.execute(toolCall.input);
|
|
3127
|
-
evidence = dedupeEvidence([...evidence, ...results], maxEvidence);
|
|
3128
|
-
messages.push({
|
|
3129
|
-
role: "tool",
|
|
3130
|
-
toolCallId: toolCall.id,
|
|
3131
|
-
content: toToolResultContent(results)
|
|
3132
|
-
});
|
|
3133
|
-
} catch (error) {
|
|
3134
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3135
|
-
messages.push({
|
|
3136
|
-
role: "tool",
|
|
3137
|
-
toolCallId: toolCall.id,
|
|
3138
|
-
content: toToolErrorContent(message)
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
if (evidence.length === 0) {
|
|
3144
|
-
return {
|
|
3145
|
-
answer: NO_EVIDENCE_ANSWER,
|
|
3146
|
-
citations: []
|
|
3147
|
-
};
|
|
3148
|
-
}
|
|
3149
|
-
return generateGroundedAnswer({
|
|
3150
|
-
question: input.question,
|
|
3151
|
-
evidence,
|
|
3152
|
-
model: input.model
|
|
3153
|
-
});
|
|
3154
|
-
}
|
|
3498
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3499
|
+
this.options.tasks.markFailed(running.id, message, running.attempts >= 3);
|
|
3500
|
+
result.failed += 1;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3155
3504
|
|
|
3156
3505
|
// src/rag/qa-logs.ts
|
|
3157
|
-
import
|
|
3506
|
+
import crypto6 from "crypto";
|
|
3158
3507
|
function clampLimit(limit) {
|
|
3159
3508
|
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3160
3509
|
}
|
|
@@ -3165,7 +3514,7 @@ var QaLogRepository = class {
|
|
|
3165
3514
|
database;
|
|
3166
3515
|
create(input) {
|
|
3167
3516
|
const record = {
|
|
3168
|
-
id: `qa_${
|
|
3517
|
+
id: `qa_${crypto6.randomUUID()}`,
|
|
3169
3518
|
chatId: input.chatId ?? null,
|
|
3170
3519
|
questionMessageId: input.questionMessageId ?? null,
|
|
3171
3520
|
question: input.question,
|
|
@@ -3278,6 +3627,77 @@ function stripMentions(text, mentions) {
|
|
|
3278
3627
|
}
|
|
3279
3628
|
return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
|
|
3280
3629
|
}
|
|
3630
|
+
var FEISHU_TOOL_SYSTEM_PROMPT = "\u4F60\u662F\u98DE\u4E66\u7FA4\u804A\u52A9\u624B\u3002\u4F60\u53EF\u4EE5\u5148\u641C\u7D22\u672C\u5730\u77E5\u8BC6\u6765\u56DE\u7B54\u95EE\u9898\uFF1B\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u521B\u5EFA\u3001\u67E5\u770B\u6216\u5220\u9664\u7FA4\u6D88\u606F\u5B9A\u65F6\u4EFB\u52A1\u65F6\uFF0C\u4E5F\u53EF\u4EE5\u8C03\u7528\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u3002\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u53EA\u7BA1\u7406\u5F53\u524D\u7FA4\u804A\uFF0C\u4E0D\u80FD\u8DE8\u7FA4\u64CD\u4F5C\u3002\u82E5\u7528\u6237\u7528\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u65F6\u95F4\uFF0C\u4F60\u9700\u8981\u5148\u5C06\u5176\u8F6C\u6362\u4E3A\u4E94\u5B57\u6BB5 cron \u8868\u8FBE\u5F0F\uFF08\u5206 \u65F6 \u65E5 \u6708 \u5468\uFF09\uFF0C\u518D\u8C03\u7528\u5DE5\u5177\u3002\u5BF9\u4E8E\u4E00\u822C\u95EE\u7B54\uFF0C\u5148\u6309\u9700\u8C03\u7528\u641C\u7D22\u5DE5\u5177\uFF0C\u518D\u57FA\u4E8E\u5DE5\u5177\u8FD4\u56DE\u7684\u8BC1\u636E\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\uFF1B\u82E5\u5F15\u7528\u4E86\u68C0\u7D22\u7ED3\u679C\uFF0C\u8981\u5728\u7B54\u6848\u91CC\u76F4\u63A5\u5199\u51FA\u5F15\u7528\u5185\u5BB9\u3002\u4E0D\u8981\u58F0\u79F0\u5B8C\u6210\u4E86\u672A\u5B9E\u9645\u8C03\u7528\u7684\u64CD\u4F5C\u3002";
|
|
3631
|
+
var DEFAULT_MAX_MODEL_TURNS = 4;
|
|
3632
|
+
var DEFAULT_MAX_TOOL_CALLS = 8;
|
|
3633
|
+
var FEISHU_TOOL_LOOP_FALLBACK = "\u5B9A\u65F6\u4EFB\u52A1\u64CD\u4F5C\u5DF2\u63D0\u4EA4\uFF0C\u4F46\u6A21\u578B\u6CA1\u6709\u751F\u6210\u6700\u7EC8\u56DE\u590D\u3002";
|
|
3634
|
+
var FEISHU_TOOL_LOOP_LIMIT_REACHED = "\u5DE5\u5177\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u5230\u4E0A\u9650\uFF0C\u8BF7\u7F29\u5C0F\u8BF7\u6C42\u540E\u91CD\u8BD5\u3002";
|
|
3635
|
+
function toToolResultContent(value) {
|
|
3636
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
3637
|
+
}
|
|
3638
|
+
function toToolErrorContent(message) {
|
|
3639
|
+
return JSON.stringify({ ok: false, error: message });
|
|
3640
|
+
}
|
|
3641
|
+
async function executeFeishuTool(tool, input) {
|
|
3642
|
+
const result = await tool.execute(input);
|
|
3643
|
+
return toToolResultContent(result);
|
|
3644
|
+
}
|
|
3645
|
+
async function runFeishuToolLoop(input) {
|
|
3646
|
+
if (!input.model.completeWithTools) {
|
|
3647
|
+
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3648
|
+
}
|
|
3649
|
+
const maxModelTurns = input.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3650
|
+
const maxToolCalls = input.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
3651
|
+
const messages = [
|
|
3652
|
+
{ role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
|
|
3653
|
+
{ role: "user", content: input.question }
|
|
3654
|
+
];
|
|
3655
|
+
const toolsByName = new Map(input.tools.map((tool) => [tool.name, tool]));
|
|
3656
|
+
let toolCallsUsed = 0;
|
|
3657
|
+
for (let turn = 0; turn < maxModelTurns; turn += 1) {
|
|
3658
|
+
const assistantResult = await input.model.completeWithTools(messages, input.tools);
|
|
3659
|
+
messages.push({
|
|
3660
|
+
role: "assistant",
|
|
3661
|
+
content: assistantResult.content,
|
|
3662
|
+
toolCalls: assistantResult.toolCalls,
|
|
3663
|
+
reasoningContent: assistantResult.reasoningContent
|
|
3664
|
+
});
|
|
3665
|
+
if (assistantResult.toolCalls.length === 0) {
|
|
3666
|
+
return assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
|
|
3667
|
+
}
|
|
3668
|
+
for (const toolCall of assistantResult.toolCalls) {
|
|
3669
|
+
if (toolCallsUsed >= maxToolCalls) {
|
|
3670
|
+
return FEISHU_TOOL_LOOP_LIMIT_REACHED;
|
|
3671
|
+
}
|
|
3672
|
+
toolCallsUsed += 1;
|
|
3673
|
+
const tool = toolsByName.get(toolCall.name);
|
|
3674
|
+
if (!tool) {
|
|
3675
|
+
messages.push({
|
|
3676
|
+
role: "tool",
|
|
3677
|
+
toolCallId: toolCall.id,
|
|
3678
|
+
content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
|
|
3679
|
+
});
|
|
3680
|
+
continue;
|
|
3681
|
+
}
|
|
3682
|
+
try {
|
|
3683
|
+
const result = await executeFeishuTool(tool, toolCall.input);
|
|
3684
|
+
messages.push({
|
|
3685
|
+
role: "tool",
|
|
3686
|
+
toolCallId: toolCall.id,
|
|
3687
|
+
content: result
|
|
3688
|
+
});
|
|
3689
|
+
} catch (error) {
|
|
3690
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3691
|
+
messages.push({
|
|
3692
|
+
role: "tool",
|
|
3693
|
+
toolCallId: toolCall.id,
|
|
3694
|
+
content: toToolErrorContent(message)
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
return FEISHU_TOOL_LOOP_FALLBACK;
|
|
3700
|
+
}
|
|
3281
3701
|
function isMentionForBot(mention, config) {
|
|
3282
3702
|
if (!config.feishu.botOpenId) {
|
|
3283
3703
|
return false;
|
|
@@ -3372,27 +3792,28 @@ var FeishuQuestionHandler = class {
|
|
|
3372
3792
|
});
|
|
3373
3793
|
try {
|
|
3374
3794
|
try {
|
|
3375
|
-
const
|
|
3795
|
+
const cronTools = createCronJobTools({
|
|
3796
|
+
repository: new CronJobRepository(this.options.database),
|
|
3797
|
+
chatId: decision.chatId,
|
|
3798
|
+
createdByOpenId: payload.event?.sender?.sender_id?.open_id
|
|
3799
|
+
});
|
|
3800
|
+
const allTools = [...tools, ...cronTools];
|
|
3801
|
+
const answer = await runFeishuToolLoop({
|
|
3376
3802
|
question: decision.question,
|
|
3377
|
-
tools,
|
|
3803
|
+
tools: allTools,
|
|
3378
3804
|
model: this.options.model
|
|
3379
3805
|
});
|
|
3380
3806
|
qaLogs.create({
|
|
3381
3807
|
chatId: decision.chatId,
|
|
3382
3808
|
questionMessageId,
|
|
3383
3809
|
question: decision.question,
|
|
3384
|
-
answer
|
|
3385
|
-
citations:
|
|
3386
|
-
retrievalDebug: {
|
|
3810
|
+
answer,
|
|
3811
|
+
citations: [],
|
|
3812
|
+
retrievalDebug: {},
|
|
3387
3813
|
status: "answered",
|
|
3388
3814
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3389
3815
|
});
|
|
3390
|
-
|
|
3391
|
-
const text = citations ? `${result.answer}
|
|
3392
|
-
|
|
3393
|
-
\u5F15\u7528\uFF1A
|
|
3394
|
-
${citations}` : result.answer;
|
|
3395
|
-
await this.sendResponse(decision.chatId, questionMessageId, text);
|
|
3816
|
+
await this.sendResponse(decision.chatId, questionMessageId, answer);
|
|
3396
3817
|
} catch (error) {
|
|
3397
3818
|
const message = error instanceof Error ? error.message : String(error);
|
|
3398
3819
|
qaLogs.create({
|
|
@@ -3638,18 +4059,39 @@ function createFeishuGateway(options) {
|
|
|
3638
4059
|
});
|
|
3639
4060
|
}
|
|
3640
4061
|
}) : void 0);
|
|
4062
|
+
const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
|
|
4063
|
+
repository: new CronJobRepository(options.cronJobProcessor.database),
|
|
4064
|
+
sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
|
|
4065
|
+
generateMessage: async (job, now) => {
|
|
4066
|
+
const { tools, close } = await createAgenticRagSearchTools({
|
|
4067
|
+
config: options.config,
|
|
4068
|
+
secrets: options.secrets,
|
|
4069
|
+
database: options.cronJobProcessor.database,
|
|
4070
|
+
messages: new MessageRepository(options.cronJobProcessor.database),
|
|
4071
|
+
scope: { platform: "feishu", platformChatId: job.chatId }
|
|
4072
|
+
});
|
|
4073
|
+
try {
|
|
4074
|
+
return await generateCronJobMessage({ prompt: job.prompt, model: options.cronJobProcessor.model, tools, now });
|
|
4075
|
+
} finally {
|
|
4076
|
+
close();
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
}) : void 0);
|
|
3641
4080
|
return {
|
|
3642
4081
|
async start() {
|
|
3643
4082
|
try {
|
|
3644
4083
|
await wsClient.start({ eventDispatcher });
|
|
3645
4084
|
indexingScheduler?.start();
|
|
4085
|
+
cronJobScheduler?.start();
|
|
3646
4086
|
} catch (error) {
|
|
3647
4087
|
indexingScheduler?.stop();
|
|
4088
|
+
cronJobScheduler?.stop();
|
|
3648
4089
|
throw formatGatewayStartError(error);
|
|
3649
4090
|
}
|
|
3650
4091
|
},
|
|
3651
4092
|
stop() {
|
|
3652
4093
|
indexingScheduler?.stop();
|
|
4094
|
+
cronJobScheduler?.stop();
|
|
3653
4095
|
wsClient.close({ force: true });
|
|
3654
4096
|
}
|
|
3655
4097
|
};
|
|
@@ -3871,7 +4313,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
|
|
|
3871
4313
|
};
|
|
3872
4314
|
|
|
3873
4315
|
// src/files/ingest.ts
|
|
3874
|
-
import
|
|
4316
|
+
import crypto7 from "crypto";
|
|
3875
4317
|
import fs11 from "fs/promises";
|
|
3876
4318
|
import path13 from "path";
|
|
3877
4319
|
|
|
@@ -3935,7 +4377,7 @@ function ensureSupportedTextFile(filePath) {
|
|
|
3935
4377
|
}
|
|
3936
4378
|
}
|
|
3937
4379
|
function stableStoredName(sourcePath, fileName) {
|
|
3938
|
-
const digest =
|
|
4380
|
+
const digest = crypto7.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
|
3939
4381
|
return `${digest}-${fileName}`;
|
|
3940
4382
|
}
|
|
3941
4383
|
async function ingestLocalFile(input) {
|
|
@@ -4109,6 +4551,126 @@ var GatewayIngestor = class {
|
|
|
4109
4551
|
}
|
|
4110
4552
|
};
|
|
4111
4553
|
|
|
4554
|
+
// src/rag/answer.ts
|
|
4555
|
+
var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
|
|
4556
|
+
var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
|
|
4557
|
+
var SCORE_TIE_THRESHOLD = 0.15;
|
|
4558
|
+
function parseTimestamp(value) {
|
|
4559
|
+
if (!value) {
|
|
4560
|
+
return 0;
|
|
4561
|
+
}
|
|
4562
|
+
const time = Date.parse(value);
|
|
4563
|
+
return Number.isFinite(time) ? time : 0;
|
|
4564
|
+
}
|
|
4565
|
+
function rankEvidenceForPrompt(evidence) {
|
|
4566
|
+
return [...evidence].sort((left, right) => {
|
|
4567
|
+
const scoreDiff = right.score - left.score;
|
|
4568
|
+
if (Math.abs(scoreDiff) > SCORE_TIE_THRESHOLD) {
|
|
4569
|
+
return scoreDiff;
|
|
4570
|
+
}
|
|
4571
|
+
const timeDiff = parseTimestamp(right.source.timestamp) - parseTimestamp(left.source.timestamp);
|
|
4572
|
+
if (timeDiff !== 0) {
|
|
4573
|
+
return timeDiff;
|
|
4574
|
+
}
|
|
4575
|
+
return scoreDiff;
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
function buildEvidencePrompt(question, evidence, options = {}) {
|
|
4579
|
+
if (evidence.length === 0) {
|
|
4580
|
+
throw new Error("RAG evidence is required before answer generation.");
|
|
4581
|
+
}
|
|
4582
|
+
const maxEvidenceBlocks = options.maxEvidenceBlocks ?? DEFAULT_MAX_EVIDENCE_BLOCKS;
|
|
4583
|
+
const maxCharsPerBlock = options.maxCharsPerBlock ?? DEFAULT_MAX_CHARS_PER_BLOCK;
|
|
4584
|
+
const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
|
|
4585
|
+
const citations = selected.map((item, index) => ({
|
|
4586
|
+
marker: `S${index + 1}`,
|
|
4587
|
+
evidenceId: item.id,
|
|
4588
|
+
source: item.source,
|
|
4589
|
+
text: item.text
|
|
4590
|
+
}));
|
|
4591
|
+
const evidenceText = selected.map((item, index) => {
|
|
4592
|
+
const marker = citations[index]?.marker;
|
|
4593
|
+
const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
|
|
4594
|
+
const sourceParts = [
|
|
4595
|
+
item.source.label,
|
|
4596
|
+
item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
|
|
4597
|
+
item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
|
|
4598
|
+
item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
|
|
4599
|
+
].filter(Boolean);
|
|
4600
|
+
return `[${marker}]
|
|
4601
|
+
\u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
|
|
4602
|
+
\u5185\u5BB9\uFF1A${clippedText}`;
|
|
4603
|
+
}).join("\n\n");
|
|
4604
|
+
return {
|
|
4605
|
+
citations,
|
|
4606
|
+
messages: [
|
|
4607
|
+
{
|
|
4608
|
+
role: "system",
|
|
4609
|
+
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"
|
|
4610
|
+
},
|
|
4611
|
+
{
|
|
4612
|
+
role: "user",
|
|
4613
|
+
content: `\u95EE\u9898\uFF1A${question}
|
|
4614
|
+
|
|
4615
|
+
\u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
|
|
4616
|
+
1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
|
|
4617
|
+
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
|
|
4618
|
+
3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
|
|
4619
|
+
|
|
4620
|
+
\u68C0\u7D22\u8BC1\u636E\uFF1A
|
|
4621
|
+
${evidenceText}`
|
|
4622
|
+
}
|
|
4623
|
+
]
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
async function generateGroundedAnswer(input) {
|
|
4627
|
+
const prompt = buildEvidencePrompt(input.question, input.evidence);
|
|
4628
|
+
const answer = await input.model.complete(prompt.messages);
|
|
4629
|
+
return {
|
|
4630
|
+
answer,
|
|
4631
|
+
citations: prompt.citations
|
|
4632
|
+
};
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
// src/rag/citations.ts
|
|
4636
|
+
function isOpaqueId(value) {
|
|
4637
|
+
return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
|
|
4638
|
+
}
|
|
4639
|
+
function formatTime(value) {
|
|
4640
|
+
if (!value) {
|
|
4641
|
+
return "\u672A\u77E5\u65F6\u95F4";
|
|
4642
|
+
}
|
|
4643
|
+
const date = new Date(value);
|
|
4644
|
+
if (Number.isNaN(date.getTime())) {
|
|
4645
|
+
return value;
|
|
4646
|
+
}
|
|
4647
|
+
const pad = (input) => String(input).padStart(2, "0");
|
|
4648
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
4649
|
+
}
|
|
4650
|
+
function formatSpeaker(source) {
|
|
4651
|
+
if (source.type === "file") {
|
|
4652
|
+
return isOpaqueId(source.label) ? "\u6587\u4EF6" : `\u6587\u4EF6 ${source.label}`;
|
|
4653
|
+
}
|
|
4654
|
+
if (source.sender && !isOpaqueId(source.sender)) {
|
|
4655
|
+
return source.sender;
|
|
4656
|
+
}
|
|
4657
|
+
return "\u7FA4\u6210\u5458";
|
|
4658
|
+
}
|
|
4659
|
+
function clipText(value, maxLength) {
|
|
4660
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
4661
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
4662
|
+
}
|
|
4663
|
+
function formatCitation(citation, options = {}) {
|
|
4664
|
+
const maxTextLength = options.maxTextLength ?? 120;
|
|
4665
|
+
const speaker = formatSpeaker(citation.source);
|
|
4666
|
+
const time = formatTime(citation.source.timestamp);
|
|
4667
|
+
const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
|
|
4668
|
+
return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
|
|
4669
|
+
}
|
|
4670
|
+
function formatCitations(citations, options = {}) {
|
|
4671
|
+
return citations.map((citation) => formatCitation(citation, options)).join("\n");
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4112
4674
|
// src/rag/qa-service.ts
|
|
4113
4675
|
async function askWithRag(input) {
|
|
4114
4676
|
const evidence = await input.retriever.retrieve(input.question);
|
|
@@ -4133,7 +4695,7 @@ var MemoryVectorStore = class {
|
|
|
4133
4695
|
this.records.set(record.id, record);
|
|
4134
4696
|
}
|
|
4135
4697
|
}
|
|
4136
|
-
async search(vector, limit) {
|
|
4698
|
+
async search(vector, limit, _scope) {
|
|
4137
4699
|
return [...this.records.values()].map((record) => {
|
|
4138
4700
|
const vectorScore = cosineSimilarity(vector, record.vector);
|
|
4139
4701
|
return {
|
|
@@ -4146,6 +4708,7 @@ var MemoryVectorStore = class {
|
|
|
4146
4708
|
};
|
|
4147
4709
|
|
|
4148
4710
|
// src/web/server.ts
|
|
4711
|
+
import crypto8 from "crypto";
|
|
4149
4712
|
import Fastify from "fastify";
|
|
4150
4713
|
function buildHtml() {
|
|
4151
4714
|
return `<!doctype html>
|
|
@@ -4293,6 +4856,10 @@ function buildHtml() {
|
|
|
4293
4856
|
<h2>\u89E3\u6790\u4EFB\u52A1</h2>
|
|
4294
4857
|
<div id="file-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
4295
4858
|
</section>
|
|
4859
|
+
<section>
|
|
4860
|
+
<h2>\u5B9A\u65F6\u4EFB\u52A1</h2>
|
|
4861
|
+
<div id="cron-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
|
|
4862
|
+
</section>
|
|
4296
4863
|
<section>
|
|
4297
4864
|
<h2>\u672C\u5730\u64CD\u4F5C</h2>
|
|
4298
4865
|
<p><code>chattercatcher settings</code> \u4FEE\u6539\u914D\u7F6E\u3002</p>
|
|
@@ -4309,10 +4876,13 @@ function buildHtml() {
|
|
|
4309
4876
|
const chats = document.querySelector("#chats");
|
|
4310
4877
|
const files = document.querySelector("#files");
|
|
4311
4878
|
const fileJobs = document.querySelector("#file-jobs");
|
|
4879
|
+
const cronJobs = document.querySelector("#cron-jobs");
|
|
4312
4880
|
const qaLogs = document.querySelector("#qa-logs");
|
|
4313
4881
|
const processMessages = document.querySelector("#process-messages");
|
|
4314
4882
|
const actionStatus = document.querySelector("#action-status");
|
|
4315
4883
|
|
|
4884
|
+
let webActionToken = "__WEB_ACTION_TOKEN__";
|
|
4885
|
+
|
|
4316
4886
|
function fmt(value) {
|
|
4317
4887
|
return value == null || value === "" ? "-" : String(value);
|
|
4318
4888
|
}
|
|
@@ -4501,6 +5071,36 @@ function buildHtml() {
|
|
|
4501
5071
|
\`;
|
|
4502
5072
|
}
|
|
4503
5073
|
|
|
5074
|
+
function renderCronJobs(items) {
|
|
5075
|
+
if (items.length === 0) {
|
|
5076
|
+
cronJobs.className = "empty";
|
|
5077
|
+
cronJobs.textContent = "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1\u3002\u53EF\u5728\u98DE\u4E66\u7FA4\u91CC @ \u673A\u5668\u4EBA\u521B\u5EFA\u3002";
|
|
5078
|
+
return;
|
|
5079
|
+
}
|
|
5080
|
+
cronJobs.className = "";
|
|
5081
|
+
cronJobs.innerHTML = \`
|
|
5082
|
+
<table>
|
|
5083
|
+
<thead><tr><th>\u4EFB\u52A1</th><th>\u72B6\u6001</th></tr></thead>
|
|
5084
|
+
<tbody>
|
|
5085
|
+
\${items.map((item) => \`
|
|
5086
|
+
<tr>
|
|
5087
|
+
<td>
|
|
5088
|
+
<div>\${escapeHtml(item.schedule)}</div>
|
|
5089
|
+
<div class="message" title="\${escapeHtml(item.prompt)}">\${escapeHtml(item.prompt)}</div>
|
|
5090
|
+
<div class="path" title="\${escapeHtml(item.id)}">ID: \${escapeHtml(item.id)}</div>
|
|
5091
|
+
<div class="path" title="\${escapeHtml(item.chatId)}">\u7FA4: \${escapeHtml(item.chatId)}</div>
|
|
5092
|
+
<div class="path">\u4E0B\u6B21: \${escapeHtml(formatDateTime(item.nextRunAt))}</div>
|
|
5093
|
+
<div class="path" title="\${escapeHtml(item.lastError || "")}">\${escapeHtml(item.lastError || "")}</div>
|
|
5094
|
+
\${item.status === "active" ? \`<button type="button" data-delete-cron-job="\${escapeHtml(item.id)}">\u5220\u9664</button>\` : ""}
|
|
5095
|
+
</td>
|
|
5096
|
+
<td>\${escapeHtml(item.status)}</td>
|
|
5097
|
+
</tr>
|
|
5098
|
+
\`).join("")}
|
|
5099
|
+
</tbody>
|
|
5100
|
+
</table>
|
|
5101
|
+
\`;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
4504
5104
|
function renderQaLogs(items) {
|
|
4505
5105
|
if (items.length === 0) {
|
|
4506
5106
|
qaLogs.className = "empty";
|
|
@@ -4532,29 +5132,38 @@ function buildHtml() {
|
|
|
4532
5132
|
}
|
|
4533
5133
|
|
|
4534
5134
|
async function load() {
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
5135
|
+
try {
|
|
5136
|
+
const [status, recent, episodeList, chatList, fileList, jobList, qaLogList, cronJobList] = await Promise.all([
|
|
5137
|
+
fetch("/api/status").then((response) => response.json()),
|
|
5138
|
+
fetch("/api/messages/recent?limit=20").then((response) => response.json()),
|
|
5139
|
+
fetch("/api/episodes?limit=10").then((response) => response.json()),
|
|
5140
|
+
fetch("/api/chats").then((response) => response.json()),
|
|
5141
|
+
fetch("/api/files").then((response) => response.json()),
|
|
5142
|
+
fetch("/api/file-jobs").then((response) => response.json()),
|
|
5143
|
+
fetch("/api/qa-logs?limit=10").then((response) => response.json()),
|
|
5144
|
+
fetch("/api/cron-jobs").then((response) => response.json()),
|
|
5145
|
+
]);
|
|
5146
|
+
renderMetrics(status);
|
|
5147
|
+
renderMessages(recent.items);
|
|
5148
|
+
renderEpisodes(episodeList.items);
|
|
5149
|
+
renderChats(chatList.items);
|
|
5150
|
+
renderFiles(fileList.items);
|
|
5151
|
+
renderFileJobs(jobList.items);
|
|
5152
|
+
renderQaLogs(qaLogList.items);
|
|
5153
|
+
renderCronJobs(cronJobList.items);
|
|
5154
|
+
} catch (error) {
|
|
5155
|
+
metrics.innerHTML = '<div class="empty">\u6570\u636E\u52A0\u8F7D\u5931\u8D25\uFF1A' + escapeHtml(error instanceof Error ? error.message : String(error)) + '</div>';
|
|
5156
|
+
}
|
|
4551
5157
|
}
|
|
4552
5158
|
|
|
4553
5159
|
async function processNow() {
|
|
4554
5160
|
processMessages.disabled = true;
|
|
4555
5161
|
actionStatus.textContent = "\u6B63\u5728\u5904\u7406\u6D88\u606F\u7D22\u5F15...";
|
|
4556
5162
|
try {
|
|
4557
|
-
const response = await fetch("/api/process/messages", {
|
|
5163
|
+
const response = await fetch("/api/process/messages", {
|
|
5164
|
+
method: "POST",
|
|
5165
|
+
headers: { "x-chattercatcher-web-token": webActionToken },
|
|
5166
|
+
});
|
|
4558
5167
|
const result = await response.json();
|
|
4559
5168
|
if (!response.ok) {
|
|
4560
5169
|
actionStatus.textContent = result.message || "\u5904\u7406\u5931\u8D25\u3002";
|
|
@@ -4574,6 +5183,28 @@ function buildHtml() {
|
|
|
4574
5183
|
}
|
|
4575
5184
|
}
|
|
4576
5185
|
|
|
5186
|
+
document.addEventListener("click", async (event) => {
|
|
5187
|
+
const target = event.target;
|
|
5188
|
+
if (!(target instanceof HTMLElement)) return;
|
|
5189
|
+
const id = target.dataset.deleteCronJob;
|
|
5190
|
+
if (!id) return;
|
|
5191
|
+
target.setAttribute("disabled", "disabled");
|
|
5192
|
+
actionStatus.textContent = "\u6B63\u5728\u5220\u9664\u5B9A\u65F6\u4EFB\u52A1...";
|
|
5193
|
+
try {
|
|
5194
|
+
const response = await fetch(\`/api/cron-jobs/\${encodeURIComponent(id)}\`, {
|
|
5195
|
+
method: "DELETE",
|
|
5196
|
+
headers: { "x-chattercatcher-web-token": webActionToken },
|
|
5197
|
+
});
|
|
5198
|
+
const result = await response.json();
|
|
5199
|
+
actionStatus.textContent = result.ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664\u3002" : result.message || "\u5220\u9664\u5931\u8D25\u3002";
|
|
5200
|
+
await load();
|
|
5201
|
+
} catch (error) {
|
|
5202
|
+
actionStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
5203
|
+
} finally {
|
|
5204
|
+
target.removeAttribute("disabled");
|
|
5205
|
+
}
|
|
5206
|
+
});
|
|
5207
|
+
|
|
4577
5208
|
processMessages.addEventListener("click", () => void processNow());
|
|
4578
5209
|
void load();
|
|
4579
5210
|
setInterval(() => {
|
|
@@ -4589,6 +5220,16 @@ function parseLimit(value, fallback, max) {
|
|
|
4589
5220
|
const rawLimit = Number(value ?? fallback);
|
|
4590
5221
|
return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
|
|
4591
5222
|
}
|
|
5223
|
+
function getWebActionToken(secrets) {
|
|
5224
|
+
return secrets.web.actionToken;
|
|
5225
|
+
}
|
|
5226
|
+
function readHeader(value) {
|
|
5227
|
+
return Array.isArray(value) ? value[0] : value;
|
|
5228
|
+
}
|
|
5229
|
+
function isAuthorizedWebAction(request, token) {
|
|
5230
|
+
const provided = readHeader(request.headers["x-chattercatcher-web-token"]);
|
|
5231
|
+
return provided === token;
|
|
5232
|
+
}
|
|
4592
5233
|
function createWebApp(config) {
|
|
4593
5234
|
const app = Fastify({ logger: false });
|
|
4594
5235
|
const database = openDatabase(config);
|
|
@@ -4596,30 +5237,44 @@ function createWebApp(config) {
|
|
|
4596
5237
|
const episodes = new EpisodeRepository(database);
|
|
4597
5238
|
const fileJobs = new FileJobRepository(database);
|
|
4598
5239
|
const qaLogs = new QaLogRepository(database);
|
|
5240
|
+
const cronJobs = new CronJobRepository(database);
|
|
5241
|
+
let webActionToken = "";
|
|
5242
|
+
const tokenReady = (async () => {
|
|
5243
|
+
const secrets = await loadSecrets();
|
|
5244
|
+
if (!secrets.web.actionToken) {
|
|
5245
|
+
secrets.web.actionToken = crypto8.randomBytes(32).toString("hex");
|
|
5246
|
+
await saveSecrets(secrets);
|
|
5247
|
+
}
|
|
5248
|
+
webActionToken = getWebActionToken(secrets);
|
|
5249
|
+
})();
|
|
4599
5250
|
app.addHook("onClose", async () => {
|
|
4600
5251
|
database.close();
|
|
4601
5252
|
});
|
|
4602
|
-
app.get("/api/status", async () =>
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
5253
|
+
app.get("/api/status", async () => {
|
|
5254
|
+
await tokenReady;
|
|
5255
|
+
return {
|
|
5256
|
+
app: "ChatterCatcher",
|
|
5257
|
+
gateway: getGatewayStatus(config),
|
|
5258
|
+
data: {
|
|
5259
|
+
chats: messages.getChatCount(),
|
|
5260
|
+
messages: messages.getMessageCount(),
|
|
5261
|
+
episodes: episodes.getEpisodeCount(),
|
|
5262
|
+
files: messages.listFiles(1e3).length,
|
|
5263
|
+
qaLogs: qaLogs.getCount(),
|
|
5264
|
+
cronJobs: cronJobs.list(1e3).length
|
|
5265
|
+
},
|
|
5266
|
+
rag: {
|
|
5267
|
+
mode: "required",
|
|
5268
|
+
note: "\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22\u8BC1\u636E\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0\u3002",
|
|
5269
|
+
retrieval: {
|
|
5270
|
+
keyword: "SQLite FTS5",
|
|
5271
|
+
vector: "SQLite embedding",
|
|
5272
|
+
hybrid: true
|
|
5273
|
+
}
|
|
5274
|
+
},
|
|
5275
|
+
web: config.web
|
|
5276
|
+
};
|
|
5277
|
+
});
|
|
4623
5278
|
app.get("/api/chats", async () => ({
|
|
4624
5279
|
items: messages.listChats()
|
|
4625
5280
|
}));
|
|
@@ -4654,7 +5309,33 @@ function createWebApp(config) {
|
|
|
4654
5309
|
items: qaLogs.listRecent(limit)
|
|
4655
5310
|
};
|
|
4656
5311
|
});
|
|
4657
|
-
app.
|
|
5312
|
+
app.get("/api/cron-jobs", async (request) => {
|
|
5313
|
+
const limit = parseLimit(request.query.limit, 50, 200);
|
|
5314
|
+
return {
|
|
5315
|
+
items: cronJobs.list(limit)
|
|
5316
|
+
};
|
|
5317
|
+
});
|
|
5318
|
+
app.delete("/api/cron-jobs/:id", async (request, reply) => {
|
|
5319
|
+
await tokenReady;
|
|
5320
|
+
if (!isAuthorizedWebAction(request, webActionToken)) {
|
|
5321
|
+
reply.code(403);
|
|
5322
|
+
return { ok: false, message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
|
|
5323
|
+
}
|
|
5324
|
+
const id = request.params.id;
|
|
5325
|
+
const job = cronJobs.get(id);
|
|
5326
|
+
if (!job) {
|
|
5327
|
+
reply.code(404);
|
|
5328
|
+
return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u5B9A\u65F6\u4EFB\u52A1\u3002" };
|
|
5329
|
+
}
|
|
5330
|
+
const ok = cronJobs.deleteByChat(id, job.chatId);
|
|
5331
|
+
return { ok };
|
|
5332
|
+
});
|
|
5333
|
+
app.post("/api/process/messages", async (request, reply) => {
|
|
5334
|
+
await tokenReady;
|
|
5335
|
+
if (!isAuthorizedWebAction(request, webActionToken)) {
|
|
5336
|
+
reply.code(403);
|
|
5337
|
+
return { status: "failed", message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
|
|
5338
|
+
}
|
|
4658
5339
|
try {
|
|
4659
5340
|
return await processMessagesNow({
|
|
4660
5341
|
config,
|
|
@@ -4671,8 +5352,9 @@ function createWebApp(config) {
|
|
|
4671
5352
|
}
|
|
4672
5353
|
});
|
|
4673
5354
|
app.get("/", async (_request, reply) => {
|
|
5355
|
+
await tokenReady;
|
|
4674
5356
|
reply.type("text/html; charset=utf-8");
|
|
4675
|
-
return buildHtml();
|
|
5357
|
+
return buildHtml().replaceAll("__WEB_ACTION_TOKEN__", webActionToken);
|
|
4676
5358
|
});
|
|
4677
5359
|
return app;
|
|
4678
5360
|
}
|
|
@@ -4684,6 +5366,7 @@ async function startWebServer(config) {
|
|
|
4684
5366
|
console.log(`ChatterCatcher Web UI: ${url}`);
|
|
4685
5367
|
}
|
|
4686
5368
|
export {
|
|
5369
|
+
CronJobRepository,
|
|
4687
5370
|
EpisodeRepository,
|
|
4688
5371
|
FeishuMessageSender,
|
|
4689
5372
|
FeishuQuestionHandler,
|
|
@@ -4706,6 +5389,8 @@ export {
|
|
|
4706
5389
|
cosineSimilarity,
|
|
4707
5390
|
createAgenticRagSearchTools,
|
|
4708
5391
|
createChatModel,
|
|
5392
|
+
createCronJobScheduler,
|
|
5393
|
+
createCronJobTools,
|
|
4709
5394
|
createDefaultConfig,
|
|
4710
5395
|
createDefaultSecrets,
|
|
4711
5396
|
createEmbeddingModel,
|
|
@@ -4722,6 +5407,7 @@ export {
|
|
|
4722
5407
|
formatCitation,
|
|
4723
5408
|
formatCitations,
|
|
4724
5409
|
formatDoctorChecks,
|
|
5410
|
+
generateCronJobMessage,
|
|
4725
5411
|
generateGroundedAnswer,
|
|
4726
5412
|
getDatabasePath,
|
|
4727
5413
|
getFeishuQuestionDecision,
|
|
@@ -4729,6 +5415,7 @@ export {
|
|
|
4729
5415
|
getGatewayPidPath,
|
|
4730
5416
|
getGatewayRuntimeState,
|
|
4731
5417
|
getLogsDirectory,
|
|
5418
|
+
getNextCronRun,
|
|
4732
5419
|
hasEmbeddingConfig,
|
|
4733
5420
|
indexMessageChunks,
|
|
4734
5421
|
ingestLocalFile,
|
|
@@ -4736,11 +5423,13 @@ export {
|
|
|
4736
5423
|
isProcessRunning,
|
|
4737
5424
|
isSupportedParseFile,
|
|
4738
5425
|
isSupportedTextFile,
|
|
5426
|
+
isValidCronSchedule,
|
|
4739
5427
|
listLogFiles,
|
|
4740
5428
|
loadConfig,
|
|
4741
5429
|
loadSecrets,
|
|
4742
5430
|
mapDomain,
|
|
4743
5431
|
maskSecret,
|
|
5432
|
+
matchesCronSchedule,
|
|
4744
5433
|
migrateDatabase,
|
|
4745
5434
|
normalizeFeishuReceiveMessageEvent,
|
|
4746
5435
|
normalizeLineCount,
|