chattercatcher 0.1.28 → 0.1.30
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 +432 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +158 -87
- package/dist/index.js +419 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -193,8 +193,12 @@ async function generateCronJobMessage(input) {
|
|
|
193
193
|
if (!input.model.completeWithTools) {
|
|
194
194
|
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
195
195
|
}
|
|
196
|
+
const systemPrompt = input.memberPrompt ? `${SYSTEM_PROMPT}
|
|
197
|
+
|
|
198
|
+
${input.memberPrompt}
|
|
199
|
+
\u751F\u6210\u6D88\u606F\u65F6\u9047\u5230\u4E0A\u8FF0 ID \u65F6\u4F18\u5148\u4F7F\u7528\u5BF9\u5E94\u7FA4\u6635\u79F0\uFF1B\u6CA1\u6709\u6620\u5C04\u65F6\u4FDD\u7559\u539F ID\uFF0C\u4E0D\u8981\u7F16\u9020\u6635\u79F0\u3002` : SYSTEM_PROMPT;
|
|
196
200
|
const messages = [
|
|
197
|
-
{ role: "system", content:
|
|
201
|
+
{ role: "system", content: systemPrompt },
|
|
198
202
|
{ role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
|
|
199
203
|
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input.prompt}` }
|
|
200
204
|
];
|
|
@@ -212,7 +216,7 @@ async function generateCronJobMessage(input) {
|
|
|
212
216
|
for (const call of result.toolCalls) {
|
|
213
217
|
if (toolCallsUsed >= maxToolCalls) {
|
|
214
218
|
return input.model.complete([
|
|
215
|
-
{ role: "system", content:
|
|
219
|
+
{ role: "system", content: systemPrompt },
|
|
216
220
|
{
|
|
217
221
|
role: "user",
|
|
218
222
|
content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
|
|
@@ -363,6 +367,9 @@ var CronJobRepository = class {
|
|
|
363
367
|
const schedule = input.schedule.trim();
|
|
364
368
|
const prompt = input.prompt.trim();
|
|
365
369
|
const imageFileName = input.imageFileName?.trim();
|
|
370
|
+
const mentionTargetName = input.mentionTargetName?.trim();
|
|
371
|
+
const mentionOpenId = input.mentionOpenId?.trim();
|
|
372
|
+
const mentionUserId = input.mentionUserId?.trim();
|
|
366
373
|
if (!isValidCronSchedule(schedule)) {
|
|
367
374
|
throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
|
|
368
375
|
}
|
|
@@ -381,6 +388,9 @@ var CronJobRepository = class {
|
|
|
381
388
|
schedule,
|
|
382
389
|
prompt,
|
|
383
390
|
...imageFileName ? { imageFileName } : {},
|
|
391
|
+
...mentionTargetName ? { mentionTargetName } : {},
|
|
392
|
+
...mentionOpenId ? { mentionOpenId } : {},
|
|
393
|
+
...mentionUserId ? { mentionUserId } : {},
|
|
384
394
|
status: "active",
|
|
385
395
|
nextRunAt: nextRunAt.toISOString(),
|
|
386
396
|
createdAt: now.toISOString(),
|
|
@@ -389,17 +399,22 @@ var CronJobRepository = class {
|
|
|
389
399
|
this.database.prepare(
|
|
390
400
|
`
|
|
391
401
|
INSERT INTO cron_jobs (
|
|
392
|
-
id, chat_id, created_by_open_id, schedule, prompt, image_file_name,
|
|
402
|
+
id, chat_id, created_by_open_id, schedule, prompt, image_file_name,
|
|
403
|
+
mention_target_name, mention_open_id, mention_user_id, status,
|
|
393
404
|
last_run_at, next_run_at, last_error, created_at, updated_at
|
|
394
405
|
)
|
|
395
406
|
VALUES (
|
|
396
|
-
@id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName,
|
|
407
|
+
@id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName,
|
|
408
|
+
@mentionTargetName, @mentionOpenId, @mentionUserId, @status,
|
|
397
409
|
NULL, @nextRunAt, NULL, @createdAt, @updatedAt
|
|
398
410
|
)
|
|
399
411
|
`
|
|
400
412
|
).run({
|
|
401
413
|
...record,
|
|
402
|
-
imageFileName: record.imageFileName ?? null
|
|
414
|
+
imageFileName: record.imageFileName ?? null,
|
|
415
|
+
mentionTargetName: record.mentionTargetName ?? null,
|
|
416
|
+
mentionOpenId: record.mentionOpenId ?? null,
|
|
417
|
+
mentionUserId: record.mentionUserId ?? null
|
|
403
418
|
});
|
|
404
419
|
return record;
|
|
405
420
|
}
|
|
@@ -426,6 +441,9 @@ var CronJobRepository = class {
|
|
|
426
441
|
schedule,
|
|
427
442
|
prompt,
|
|
428
443
|
image_file_name AS imageFileName,
|
|
444
|
+
mention_target_name AS mentionTargetName,
|
|
445
|
+
mention_open_id AS mentionOpenId,
|
|
446
|
+
mention_user_id AS mentionUserId,
|
|
429
447
|
status,
|
|
430
448
|
last_run_at AS lastRunAt,
|
|
431
449
|
next_run_at AS nextRunAt,
|
|
@@ -445,6 +463,9 @@ var CronJobRepository = class {
|
|
|
445
463
|
schedule: row.schedule,
|
|
446
464
|
prompt: row.prompt,
|
|
447
465
|
imageFileName: row.imageFileName ?? void 0,
|
|
466
|
+
mentionTargetName: row.mentionTargetName ?? void 0,
|
|
467
|
+
mentionOpenId: row.mentionOpenId ?? void 0,
|
|
468
|
+
mentionUserId: row.mentionUserId ?? void 0,
|
|
448
469
|
status: row.status,
|
|
449
470
|
lastRunAt: row.lastRunAt ?? void 0,
|
|
450
471
|
nextRunAt: row.nextRunAt,
|
|
@@ -519,6 +540,9 @@ var CronJobRepository = class {
|
|
|
519
540
|
schedule,
|
|
520
541
|
prompt,
|
|
521
542
|
image_file_name AS imageFileName,
|
|
543
|
+
mention_target_name AS mentionTargetName,
|
|
544
|
+
mention_open_id AS mentionOpenId,
|
|
545
|
+
mention_user_id AS mentionUserId,
|
|
522
546
|
status,
|
|
523
547
|
last_run_at AS lastRunAt,
|
|
524
548
|
next_run_at AS nextRunAt,
|
|
@@ -538,6 +562,9 @@ var CronJobRepository = class {
|
|
|
538
562
|
schedule: row.schedule,
|
|
539
563
|
prompt: row.prompt,
|
|
540
564
|
imageFileName: row.imageFileName ?? void 0,
|
|
565
|
+
mentionTargetName: row.mentionTargetName ?? void 0,
|
|
566
|
+
mentionOpenId: row.mentionOpenId ?? void 0,
|
|
567
|
+
mentionUserId: row.mentionUserId ?? void 0,
|
|
541
568
|
status: row.status,
|
|
542
569
|
lastRunAt: row.lastRunAt ?? void 0,
|
|
543
570
|
nextRunAt: row.nextRunAt,
|
|
@@ -567,7 +594,12 @@ function createCronJobScheduler(options) {
|
|
|
567
594
|
for (const job of jobs) {
|
|
568
595
|
try {
|
|
569
596
|
const text = await options.generateMessage(job, startedAt);
|
|
570
|
-
|
|
597
|
+
const sendOptions = job.mentionOpenId && job.mentionTargetName ? { mentions: [{ openId: job.mentionOpenId, name: job.mentionTargetName }] } : void 0;
|
|
598
|
+
if (sendOptions) {
|
|
599
|
+
await options.sendTextToChat(job.chatId, text, sendOptions);
|
|
600
|
+
} else {
|
|
601
|
+
await options.sendTextToChat(job.chatId, text);
|
|
602
|
+
}
|
|
571
603
|
if (job.imageFileName) {
|
|
572
604
|
if (!options.sendImageToChat) {
|
|
573
605
|
throw new Error("\u5F53\u524D\u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\u3002");
|
|
@@ -644,18 +676,27 @@ function createCronJobTools(input) {
|
|
|
644
676
|
imageFileName: {
|
|
645
677
|
type: "string",
|
|
646
678
|
description: "Optional image filename already stored from the current chat, for example om_xxx-image.jpg."
|
|
679
|
+
},
|
|
680
|
+
mentionTargetName: {
|
|
681
|
+
type: "string",
|
|
682
|
+
description: "Optional exact Feishu chat nickname to @ when the scheduled message is sent."
|
|
647
683
|
}
|
|
648
684
|
},
|
|
649
685
|
required: ["schedule", "prompt"],
|
|
650
686
|
additionalProperties: false
|
|
651
687
|
},
|
|
652
688
|
execute: async (rawInput) => {
|
|
689
|
+
const mentionTargetName = readOptionalString(rawInput, "mentionTargetName");
|
|
690
|
+
const mentionTarget = mentionTargetName && input.memberResolver ? await input.memberResolver.resolveUniqueName(input.chatId, mentionTargetName) : null;
|
|
653
691
|
const job = input.repository.create({
|
|
654
692
|
chatId: input.chatId,
|
|
655
693
|
createdByOpenId: input.createdByOpenId,
|
|
656
694
|
schedule: readString(rawInput, "schedule"),
|
|
657
695
|
prompt: readString(rawInput, "prompt"),
|
|
658
|
-
imageFileName: readOptionalString(rawInput, "imageFileName")
|
|
696
|
+
imageFileName: readOptionalString(rawInput, "imageFileName"),
|
|
697
|
+
mentionTargetName,
|
|
698
|
+
mentionOpenId: mentionTarget?.openId,
|
|
699
|
+
mentionUserId: mentionTarget?.userId
|
|
659
700
|
});
|
|
660
701
|
return JSON.stringify({ ok: true, job });
|
|
661
702
|
}
|
|
@@ -974,6 +1015,18 @@ function migrateDatabase(database) {
|
|
|
974
1015
|
|
|
975
1016
|
CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
|
|
976
1017
|
|
|
1018
|
+
CREATE TABLE IF NOT EXISTS feishu_chat_members (
|
|
1019
|
+
chat_id TEXT NOT NULL,
|
|
1020
|
+
open_id TEXT NOT NULL,
|
|
1021
|
+
user_id TEXT,
|
|
1022
|
+
user_name TEXT,
|
|
1023
|
+
updated_at TEXT NOT NULL,
|
|
1024
|
+
PRIMARY KEY (chat_id, open_id)
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
|
|
1028
|
+
ON feishu_chat_members(chat_id, user_name);
|
|
1029
|
+
|
|
977
1030
|
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
978
1031
|
id TEXT PRIMARY KEY,
|
|
979
1032
|
chat_id TEXT NOT NULL,
|
|
@@ -991,11 +1044,29 @@ function migrateDatabase(database) {
|
|
|
991
1044
|
|
|
992
1045
|
CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
|
|
993
1046
|
CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
|
|
1047
|
+
|
|
1048
|
+
CREATE TABLE IF NOT EXISTS feishu_chat_members (
|
|
1049
|
+
chat_id TEXT NOT NULL,
|
|
1050
|
+
open_id TEXT NOT NULL,
|
|
1051
|
+
user_id TEXT,
|
|
1052
|
+
user_name TEXT NOT NULL,
|
|
1053
|
+
updated_at TEXT NOT NULL,
|
|
1054
|
+
PRIMARY KEY (chat_id, open_id)
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
|
|
1058
|
+
ON feishu_chat_members(chat_id, user_name);
|
|
994
1059
|
`);
|
|
995
1060
|
const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1061
|
+
const ensureCronJobColumn = (name, definition) => {
|
|
1062
|
+
if (!cronJobColumns.some((column) => column.name === name)) {
|
|
1063
|
+
database.prepare(`ALTER TABLE cron_jobs ADD COLUMN ${definition}`).run();
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
ensureCronJobColumn("image_file_name", "image_file_name TEXT");
|
|
1067
|
+
ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
|
|
1068
|
+
ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
|
|
1069
|
+
ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
|
|
999
1070
|
}
|
|
1000
1071
|
|
|
1001
1072
|
// src/doctor/checks.ts
|
|
@@ -3619,11 +3690,204 @@ var ImageMultimodalWorker = class {
|
|
|
3619
3690
|
}
|
|
3620
3691
|
};
|
|
3621
3692
|
|
|
3693
|
+
// src/feishu/members.ts
|
|
3694
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
3695
|
+
var FeishuMemberRepository = class {
|
|
3696
|
+
constructor(database) {
|
|
3697
|
+
this.database = database;
|
|
3698
|
+
}
|
|
3699
|
+
database;
|
|
3700
|
+
upsert(record) {
|
|
3701
|
+
this.database.prepare(
|
|
3702
|
+
`
|
|
3703
|
+
INSERT INTO feishu_chat_members (chat_id, open_id, user_id, user_name, updated_at)
|
|
3704
|
+
VALUES (@chatId, @openId, @userId, @userName, @updatedAt)
|
|
3705
|
+
ON CONFLICT(chat_id, open_id)
|
|
3706
|
+
DO UPDATE SET
|
|
3707
|
+
user_id = excluded.user_id,
|
|
3708
|
+
user_name = excluded.user_name,
|
|
3709
|
+
updated_at = excluded.updated_at
|
|
3710
|
+
`
|
|
3711
|
+
).run({
|
|
3712
|
+
chatId: record.chatId,
|
|
3713
|
+
openId: record.openId,
|
|
3714
|
+
userId: record.userId ?? null,
|
|
3715
|
+
userName: record.userName,
|
|
3716
|
+
updatedAt: record.updatedAt
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
get(chatId, openId) {
|
|
3720
|
+
const row = this.database.prepare(
|
|
3721
|
+
`
|
|
3722
|
+
SELECT
|
|
3723
|
+
chat_id AS chatId,
|
|
3724
|
+
open_id AS openId,
|
|
3725
|
+
user_id AS userId,
|
|
3726
|
+
user_name AS userName,
|
|
3727
|
+
updated_at AS updatedAt
|
|
3728
|
+
FROM feishu_chat_members
|
|
3729
|
+
WHERE chat_id = ? AND open_id = ?
|
|
3730
|
+
`
|
|
3731
|
+
).get(chatId, openId);
|
|
3732
|
+
return row ?? null;
|
|
3733
|
+
}
|
|
3734
|
+
listByChat(chatId) {
|
|
3735
|
+
return this.database.prepare(
|
|
3736
|
+
`
|
|
3737
|
+
SELECT
|
|
3738
|
+
chat_id AS chatId,
|
|
3739
|
+
open_id AS openId,
|
|
3740
|
+
user_id AS userId,
|
|
3741
|
+
user_name AS userName,
|
|
3742
|
+
updated_at AS updatedAt
|
|
3743
|
+
FROM feishu_chat_members
|
|
3744
|
+
WHERE chat_id = ?
|
|
3745
|
+
ORDER BY user_name ASC, open_id ASC
|
|
3746
|
+
`
|
|
3747
|
+
).all(chatId);
|
|
3748
|
+
}
|
|
3749
|
+
findUniqueByName(chatId, userName) {
|
|
3750
|
+
const rows = this.database.prepare(
|
|
3751
|
+
`
|
|
3752
|
+
SELECT
|
|
3753
|
+
chat_id AS chatId,
|
|
3754
|
+
open_id AS openId,
|
|
3755
|
+
user_id AS userId,
|
|
3756
|
+
user_name AS userName,
|
|
3757
|
+
updated_at AS updatedAt
|
|
3758
|
+
FROM feishu_chat_members
|
|
3759
|
+
WHERE chat_id = ? AND user_name = ?
|
|
3760
|
+
ORDER BY open_id ASC
|
|
3761
|
+
LIMIT 2
|
|
3762
|
+
`
|
|
3763
|
+
).all(chatId, userName);
|
|
3764
|
+
return rows.length === 1 ? rows[0] : null;
|
|
3765
|
+
}
|
|
3766
|
+
};
|
|
3767
|
+
var FeishuMemberResolver = class {
|
|
3768
|
+
constructor(options) {
|
|
3769
|
+
this.options = options;
|
|
3770
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3771
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
3772
|
+
this.logger = options.logger;
|
|
3773
|
+
}
|
|
3774
|
+
options;
|
|
3775
|
+
now;
|
|
3776
|
+
ttlMs;
|
|
3777
|
+
logger;
|
|
3778
|
+
async resolveOpenIdName(chatId, openId) {
|
|
3779
|
+
const cached = this.options.repository.get(chatId, openId);
|
|
3780
|
+
if (!cached || this.isExpired(cached.updatedAt)) {
|
|
3781
|
+
try {
|
|
3782
|
+
await this.refreshChatMembers(chatId);
|
|
3783
|
+
} catch (error) {
|
|
3784
|
+
this.logger?.warn("Failed to refresh Feishu chat members for open id resolution", {
|
|
3785
|
+
chatId,
|
|
3786
|
+
openId,
|
|
3787
|
+
error
|
|
3788
|
+
});
|
|
3789
|
+
return cached?.userName ?? openId;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return this.options.repository.get(chatId, openId)?.userName ?? openId;
|
|
3793
|
+
}
|
|
3794
|
+
async resolveUniqueName(chatId, userName) {
|
|
3795
|
+
const cached = this.options.repository.findUniqueByName(chatId, userName);
|
|
3796
|
+
if (cached && !this.isExpired(cached.updatedAt)) {
|
|
3797
|
+
return cached;
|
|
3798
|
+
}
|
|
3799
|
+
try {
|
|
3800
|
+
await this.refreshChatMembers(chatId);
|
|
3801
|
+
} catch (error) {
|
|
3802
|
+
this.logger?.warn("Failed to refresh Feishu chat members for unique name resolution", {
|
|
3803
|
+
chatId,
|
|
3804
|
+
userName,
|
|
3805
|
+
error
|
|
3806
|
+
});
|
|
3807
|
+
return cached ?? null;
|
|
3808
|
+
}
|
|
3809
|
+
return this.options.repository.findUniqueByName(chatId, userName);
|
|
3810
|
+
}
|
|
3811
|
+
isExpired(updatedAt) {
|
|
3812
|
+
const updatedAtMs = Date.parse(updatedAt);
|
|
3813
|
+
if (Number.isNaN(updatedAtMs)) {
|
|
3814
|
+
return true;
|
|
3815
|
+
}
|
|
3816
|
+
return this.now().getTime() - updatedAtMs >= this.ttlMs;
|
|
3817
|
+
}
|
|
3818
|
+
async refreshChatMembers(chatId) {
|
|
3819
|
+
const members = await this.options.client.listChatMembers({ chatId, memberIdType: "open_id" });
|
|
3820
|
+
const updatedAt = this.now().toISOString();
|
|
3821
|
+
for (const member of members) {
|
|
3822
|
+
this.options.repository.upsert({
|
|
3823
|
+
chatId,
|
|
3824
|
+
openId: member.openId,
|
|
3825
|
+
userId: member.userId,
|
|
3826
|
+
userName: member.userName,
|
|
3827
|
+
updatedAt
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
};
|
|
3832
|
+
function formatFeishuMemberPrompt(members, limit = 80) {
|
|
3833
|
+
const lines = members.filter((member) => member.userName).slice(0, limit).map((member) => `${member.openId} = ${member.userName}`);
|
|
3834
|
+
return lines.length ? `\u5F53\u524D\u7FA4\u804A\u6210\u5458 ID \u4E0E\u7FA4\u6635\u79F0\u6620\u5C04\uFF1A
|
|
3835
|
+
${lines.join("\n")}` : "";
|
|
3836
|
+
}
|
|
3837
|
+
function createFeishuChatMembersClient(client) {
|
|
3838
|
+
return {
|
|
3839
|
+
async listChatMembers(payload) {
|
|
3840
|
+
const api = client.im.v1?.chatMembers?.get;
|
|
3841
|
+
if (!api) {
|
|
3842
|
+
throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301 chatMembers.get\uFF0C\u65E0\u6CD5\u83B7\u53D6\u7FA4\u6210\u5458\u3002");
|
|
3843
|
+
}
|
|
3844
|
+
const members = [];
|
|
3845
|
+
let pageToken;
|
|
3846
|
+
do {
|
|
3847
|
+
const response = await api({
|
|
3848
|
+
path: { chat_id: payload.chatId },
|
|
3849
|
+
params: {
|
|
3850
|
+
member_id_type: payload.memberIdType,
|
|
3851
|
+
...pageToken ? { page_token: pageToken } : {}
|
|
3852
|
+
}
|
|
3853
|
+
});
|
|
3854
|
+
const items = response.data?.items ?? [];
|
|
3855
|
+
for (const item of items) {
|
|
3856
|
+
if (!item.member_id || !item.name) {
|
|
3857
|
+
continue;
|
|
3858
|
+
}
|
|
3859
|
+
members.push({
|
|
3860
|
+
openId: item.member_id,
|
|
3861
|
+
userId: item.user_id,
|
|
3862
|
+
userName: item.name
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
pageToken = response.data?.has_more ? response.data.page_token : void 0;
|
|
3866
|
+
} while (pageToken);
|
|
3867
|
+
return members;
|
|
3868
|
+
}
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3622
3872
|
// src/rag/qa-logs.ts
|
|
3623
3873
|
import crypto6 from "crypto";
|
|
3624
3874
|
function clampLimit(limit) {
|
|
3625
3875
|
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3626
3876
|
}
|
|
3877
|
+
function mapQaLogRow(row) {
|
|
3878
|
+
return {
|
|
3879
|
+
id: row.id,
|
|
3880
|
+
chatId: row.chat_id,
|
|
3881
|
+
questionMessageId: row.question_message_id,
|
|
3882
|
+
question: row.question,
|
|
3883
|
+
answer: row.answer,
|
|
3884
|
+
citations: JSON.parse(row.citations_json),
|
|
3885
|
+
retrievalDebug: JSON.parse(row.retrieval_debug_json),
|
|
3886
|
+
status: row.status,
|
|
3887
|
+
error: row.error,
|
|
3888
|
+
createdAt: row.created_at
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3627
3891
|
var QaLogRepository = class {
|
|
3628
3892
|
constructor(database) {
|
|
3629
3893
|
this.database = database;
|
|
@@ -3702,18 +3966,29 @@ var QaLogRepository = class {
|
|
|
3702
3966
|
LIMIT ?
|
|
3703
3967
|
`
|
|
3704
3968
|
).all(clampLimit(limit));
|
|
3705
|
-
return rows.map(
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3969
|
+
return rows.map(mapQaLogRow);
|
|
3970
|
+
}
|
|
3971
|
+
listRecentByChat(chatId, limit) {
|
|
3972
|
+
const rows = this.database.prepare(
|
|
3973
|
+
`
|
|
3974
|
+
SELECT
|
|
3975
|
+
id,
|
|
3976
|
+
chat_id,
|
|
3977
|
+
question_message_id,
|
|
3978
|
+
question,
|
|
3979
|
+
answer,
|
|
3980
|
+
citations_json,
|
|
3981
|
+
retrieval_debug_json,
|
|
3982
|
+
status,
|
|
3983
|
+
error,
|
|
3984
|
+
created_at
|
|
3985
|
+
FROM qa_logs
|
|
3986
|
+
WHERE chat_id = ? AND status = 'answered'
|
|
3987
|
+
ORDER BY created_at DESC
|
|
3988
|
+
LIMIT ?
|
|
3989
|
+
`
|
|
3990
|
+
).all(chatId, clampLimit(limit));
|
|
3991
|
+
return rows.map(mapQaLogRow);
|
|
3717
3992
|
}
|
|
3718
3993
|
getCount() {
|
|
3719
3994
|
const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
|
|
@@ -3785,8 +4060,18 @@ async function runFeishuToolLoop(input) {
|
|
|
3785
4060
|
}
|
|
3786
4061
|
const maxModelTurns = input.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3787
4062
|
const maxToolCalls = input.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
4063
|
+
const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
|
|
4064
|
+
if (input.memberPrompt) {
|
|
4065
|
+
systemPromptParts.push(`${input.memberPrompt}
|
|
4066
|
+
\u56DE\u7B54\u4E2D\u9047\u5230\u4E0A\u8FF0 ID \u65F6\u4F18\u5148\u4F7F\u7528\u5BF9\u5E94\u7FA4\u6635\u79F0\uFF1B\u6CA1\u6709\u6620\u5C04\u65F6\u4FDD\u7559\u539F ID\uFF0C\u4E0D\u8981\u7F16\u9020\u6635\u79F0\u3002`);
|
|
4067
|
+
}
|
|
4068
|
+
if (input.conversationContext) {
|
|
4069
|
+
systemPromptParts.push(`${input.conversationContext}
|
|
4070
|
+
\u8FD9\u4E9B\u662F\u5F53\u524D\u7FA4\u804A\u91CC\u6700\u8FD1\u51E0\u8F6E\u4F60\u548C\u7528\u6237\u7684\u95EE\u7B54\uFF0C\u53EA\u4F5C\u4E3A\u7406\u89E3\u7701\u7565\u6307\u4EE3\u548C\u8FDE\u7EED\u8FFD\u95EE\u7684\u4E0A\u4E0B\u6587\uFF1B\u5982\u679C\u4E0E\u68C0\u7D22\u8BC1\u636E\u51B2\u7A81\uFF0C\u4EE5\u68C0\u7D22\u8BC1\u636E\u4E3A\u51C6\u3002`);
|
|
4071
|
+
}
|
|
4072
|
+
const systemPrompt = systemPromptParts.join("\n\n");
|
|
3788
4073
|
const messages = [
|
|
3789
|
-
{ role: "system", content:
|
|
4074
|
+
{ role: "system", content: systemPrompt },
|
|
3790
4075
|
{ role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
|
|
3791
4076
|
\u95EE\u9898\uFF1A${input.question}` }
|
|
3792
4077
|
];
|
|
@@ -3848,6 +4133,13 @@ async function runFeishuToolLoop(input) {
|
|
|
3848
4133
|
return "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
3849
4134
|
}
|
|
3850
4135
|
}
|
|
4136
|
+
function formatConversationContext(records) {
|
|
4137
|
+
const lines = records.slice().reverse().map((record, index) => `\u7B2C ${index + 1} \u8F6E
|
|
4138
|
+
\u7528\u6237\uFF1A${record.question}
|
|
4139
|
+
\u52A9\u624B\uFF1A${record.answer}`);
|
|
4140
|
+
return lines.length ? `\u8FD1\u671F\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1A
|
|
4141
|
+
${lines.join("\n\n")}` : "";
|
|
4142
|
+
}
|
|
3851
4143
|
function isMentionForBot(mention, config) {
|
|
3852
4144
|
if (!config.feishu.botOpenId) {
|
|
3853
4145
|
return false;
|
|
@@ -3898,8 +4190,13 @@ function getFeishuQuestionDecision(payload, config) {
|
|
|
3898
4190
|
var FeishuQuestionHandler = class {
|
|
3899
4191
|
constructor(options) {
|
|
3900
4192
|
this.options = options;
|
|
4193
|
+
this.memberResolver = options.memberResolver;
|
|
3901
4194
|
}
|
|
3902
4195
|
options;
|
|
4196
|
+
memberResolver;
|
|
4197
|
+
setMemberResolver(memberResolver) {
|
|
4198
|
+
this.memberResolver = memberResolver;
|
|
4199
|
+
}
|
|
3903
4200
|
async sendResponse(chatId, messageId, text) {
|
|
3904
4201
|
if (messageId && this.options.sender.replyTextToMessage) {
|
|
3905
4202
|
try {
|
|
@@ -3946,14 +4243,20 @@ var FeishuQuestionHandler = class {
|
|
|
3946
4243
|
const cronTools = createCronJobTools({
|
|
3947
4244
|
repository: new CronJobRepository(this.options.database),
|
|
3948
4245
|
chatId: decision.chatId,
|
|
3949
|
-
createdByOpenId: payload.event?.sender?.sender_id?.open_id
|
|
4246
|
+
createdByOpenId: payload.event?.sender?.sender_id?.open_id,
|
|
4247
|
+
memberResolver: this.memberResolver
|
|
3950
4248
|
});
|
|
3951
4249
|
const allTools = [...tools, ...cronTools];
|
|
4250
|
+
const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
|
|
4251
|
+
const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
|
|
4252
|
+
const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
|
|
3952
4253
|
const answer = await runFeishuToolLoop({
|
|
3953
4254
|
question: decision.question,
|
|
3954
4255
|
now,
|
|
3955
4256
|
tools: allTools,
|
|
3956
|
-
model: this.options.model
|
|
4257
|
+
model: this.options.model,
|
|
4258
|
+
memberPrompt,
|
|
4259
|
+
conversationContext
|
|
3957
4260
|
});
|
|
3958
4261
|
qaLogs.create({
|
|
3959
4262
|
chatId: decision.chatId,
|
|
@@ -4006,6 +4309,15 @@ function extractImageKey(response) {
|
|
|
4006
4309
|
}
|
|
4007
4310
|
throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
|
|
4008
4311
|
}
|
|
4312
|
+
function escapeAtText(value) {
|
|
4313
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4314
|
+
}
|
|
4315
|
+
function formatTextWithMentions(text, options) {
|
|
4316
|
+
const mentions = options?.mentions ?? [];
|
|
4317
|
+
if (mentions.length === 0) return text;
|
|
4318
|
+
const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
|
|
4319
|
+
return `${prefix} ${text}`.trim();
|
|
4320
|
+
}
|
|
4009
4321
|
var FeishuMessageSender = class _FeishuMessageSender {
|
|
4010
4322
|
constructor(client) {
|
|
4011
4323
|
this.client = client;
|
|
@@ -4019,12 +4331,12 @@ var FeishuMessageSender = class _FeishuMessageSender {
|
|
|
4019
4331
|
});
|
|
4020
4332
|
return new _FeishuMessageSender(client);
|
|
4021
4333
|
}
|
|
4022
|
-
async sendTextToChat(chatId, text) {
|
|
4334
|
+
async sendTextToChat(chatId, text, options) {
|
|
4023
4335
|
const payload = {
|
|
4024
4336
|
data: {
|
|
4025
4337
|
receive_id: chatId,
|
|
4026
4338
|
msg_type: "text",
|
|
4027
|
-
content: JSON.stringify({ text })
|
|
4339
|
+
content: JSON.stringify({ text: formatTextWithMentions(text, options) })
|
|
4028
4340
|
},
|
|
4029
4341
|
params: {
|
|
4030
4342
|
receive_id_type: "chat_id"
|
|
@@ -4146,13 +4458,24 @@ function createFeishuEventDispatcher(options) {
|
|
|
4146
4458
|
return;
|
|
4147
4459
|
}
|
|
4148
4460
|
}
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4461
|
+
let result;
|
|
4462
|
+
if (options.resourceDownloader) {
|
|
4463
|
+
result = await options.ingestor.ingestFeishuEventAndDownloadAttachments({
|
|
4464
|
+
payload,
|
|
4465
|
+
downloader: options.resourceDownloader,
|
|
4466
|
+
config: options.config,
|
|
4467
|
+
secrets: options.secrets,
|
|
4468
|
+
vectorIndexMessage: options.attachmentVectorIndexer,
|
|
4469
|
+
memberResolver: options.memberResolver
|
|
4470
|
+
});
|
|
4471
|
+
} else if (options.memberResolver) {
|
|
4472
|
+
result = await options.ingestor.ingestFeishuEventWithMembers({
|
|
4473
|
+
payload,
|
|
4474
|
+
memberResolver: options.memberResolver
|
|
4475
|
+
});
|
|
4476
|
+
} else {
|
|
4477
|
+
result = options.ingestor.ingestFeishuEvent(payload);
|
|
4478
|
+
}
|
|
4156
4479
|
if (!result.accepted) {
|
|
4157
4480
|
console.log(`\u98DE\u4E66\u6D88\u606F\u672A\u5165\u5E93\uFF1A${result.reason}`);
|
|
4158
4481
|
return;
|
|
@@ -4243,10 +4566,20 @@ function createFeishuGateway(options) {
|
|
|
4243
4566
|
onReconnecting: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u6B63\u5728\u91CD\u8FDE\u3002"),
|
|
4244
4567
|
onReconnected: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u91CD\u8FDE\u3002")
|
|
4245
4568
|
});
|
|
4569
|
+
const memberResolver = new FeishuMemberResolver({
|
|
4570
|
+
repository: new FeishuMemberRepository(options.ingestor.database),
|
|
4571
|
+
client: createFeishuChatMembersClient(new lark2.Client({
|
|
4572
|
+
appId: options.config.feishu.appId,
|
|
4573
|
+
appSecret: options.secrets.feishu.appSecret,
|
|
4574
|
+
domain: mapDomain(options.config.feishu.domain)
|
|
4575
|
+
}))
|
|
4576
|
+
});
|
|
4577
|
+
options.questionHandler?.setMemberResolver?.(memberResolver);
|
|
4246
4578
|
const eventDispatcher = createFeishuEventDispatcher({
|
|
4247
4579
|
config: options.config,
|
|
4248
4580
|
secrets: options.secrets,
|
|
4249
4581
|
ingestor: options.ingestor,
|
|
4582
|
+
memberResolver,
|
|
4250
4583
|
questionHandler: options.questionHandler,
|
|
4251
4584
|
resourceDownloader: options.resourceDownloader,
|
|
4252
4585
|
attachmentVectorIndexer: options.attachmentVectorIndexer,
|
|
@@ -4266,7 +4599,7 @@ function createFeishuGateway(options) {
|
|
|
4266
4599
|
}) : void 0);
|
|
4267
4600
|
const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
|
|
4268
4601
|
repository: new CronJobRepository(options.cronJobProcessor.database),
|
|
4269
|
-
sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
|
|
4602
|
+
sendTextToChat: (chatId, text, sendOptions) => options.cronJobProcessor.sender.sendTextToChat(chatId, text, sendOptions),
|
|
4270
4603
|
sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
|
|
4271
4604
|
chatId,
|
|
4272
4605
|
resolveFeishuImagePath(options.config, imageFileName)
|
|
@@ -4280,7 +4613,16 @@ function createFeishuGateway(options) {
|
|
|
4280
4613
|
scope: { platform: "feishu", platformChatId: job.chatId }
|
|
4281
4614
|
});
|
|
4282
4615
|
try {
|
|
4283
|
-
|
|
4616
|
+
const memberPrompt = formatFeishuMemberPrompt(
|
|
4617
|
+
new FeishuMemberRepository(options.cronJobProcessor.database).listByChat(job.chatId)
|
|
4618
|
+
);
|
|
4619
|
+
return await generateCronJobMessage({
|
|
4620
|
+
prompt: job.prompt,
|
|
4621
|
+
model: options.cronJobProcessor.model,
|
|
4622
|
+
tools,
|
|
4623
|
+
now,
|
|
4624
|
+
memberPrompt
|
|
4625
|
+
});
|
|
4284
4626
|
} finally {
|
|
4285
4627
|
close();
|
|
4286
4628
|
}
|
|
@@ -4436,7 +4778,9 @@ function normalizeFeishuReceiveMessageEvent(payload) {
|
|
|
4436
4778
|
if (!text) {
|
|
4437
4779
|
return null;
|
|
4438
4780
|
}
|
|
4439
|
-
const
|
|
4781
|
+
const senderOpenId = event.sender?.sender_id?.open_id;
|
|
4782
|
+
const senderUserId = event.sender?.sender_id?.user_id;
|
|
4783
|
+
const senderId = senderOpenId || senderUserId || event.sender?.sender_id?.union_id || "unknown";
|
|
4440
4784
|
return {
|
|
4441
4785
|
platform: "feishu",
|
|
4442
4786
|
platformChatId: message.chat_id,
|
|
@@ -4451,6 +4795,10 @@ function normalizeFeishuReceiveMessageEvent(payload) {
|
|
|
4451
4795
|
platform: "feishu",
|
|
4452
4796
|
raw: payload,
|
|
4453
4797
|
content,
|
|
4798
|
+
sender: {
|
|
4799
|
+
...senderOpenId ? { openId: senderOpenId } : {},
|
|
4800
|
+
...senderUserId ? { userId: senderUserId } : {}
|
|
4801
|
+
},
|
|
4454
4802
|
attachment: extractFeishuAttachment(messageType, content)
|
|
4455
4803
|
}
|
|
4456
4804
|
};
|
|
@@ -4659,6 +5007,14 @@ async function ingestLocalFile(input) {
|
|
|
4659
5007
|
}
|
|
4660
5008
|
|
|
4661
5009
|
// src/gateway/ingest.ts
|
|
5010
|
+
function extractFeishuSenderOpenId(message) {
|
|
5011
|
+
const raw = message.rawPayload;
|
|
5012
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
5013
|
+
const sender = raw.sender;
|
|
5014
|
+
if (!sender || typeof sender !== "object" || Array.isArray(sender)) return void 0;
|
|
5015
|
+
const openId = sender.openId;
|
|
5016
|
+
return typeof openId === "string" && openId.trim() ? openId.trim() : void 0;
|
|
5017
|
+
}
|
|
4662
5018
|
function extractAttachment(message) {
|
|
4663
5019
|
const raw = message.rawPayload;
|
|
4664
5020
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -4678,14 +5034,16 @@ function isMultimodalReady(config, secrets) {
|
|
|
4678
5034
|
return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
|
|
4679
5035
|
}
|
|
4680
5036
|
var GatewayIngestor = class {
|
|
4681
|
-
messages;
|
|
4682
|
-
jobs;
|
|
4683
|
-
imageTasks;
|
|
4684
5037
|
constructor(database) {
|
|
5038
|
+
this.database = database;
|
|
4685
5039
|
this.messages = new MessageRepository(database);
|
|
4686
5040
|
this.jobs = new FileJobRepository(database);
|
|
4687
5041
|
this.imageTasks = new ImageMultimodalTaskRepository(database);
|
|
4688
5042
|
}
|
|
5043
|
+
database;
|
|
5044
|
+
messages;
|
|
5045
|
+
jobs;
|
|
5046
|
+
imageTasks;
|
|
4689
5047
|
ingestFeishuEvent(payload) {
|
|
4690
5048
|
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
4691
5049
|
if (!normalized) {
|
|
@@ -4703,8 +5061,28 @@ var GatewayIngestor = class {
|
|
|
4703
5061
|
duplicate
|
|
4704
5062
|
};
|
|
4705
5063
|
}
|
|
5064
|
+
async ingestFeishuEventWithMembers(input) {
|
|
5065
|
+
const normalized = normalizeFeishuReceiveMessageEvent(input.payload);
|
|
5066
|
+
if (!normalized) {
|
|
5067
|
+
return {
|
|
5068
|
+
accepted: false,
|
|
5069
|
+
reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
5072
|
+
const openId = extractFeishuSenderOpenId(normalized);
|
|
5073
|
+
const senderName = openId ? await input.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
|
|
5074
|
+
const enriched = { ...normalized, senderName };
|
|
5075
|
+
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
5076
|
+
const messageId = this.messages.ingest(enriched);
|
|
5077
|
+
return {
|
|
5078
|
+
accepted: true,
|
|
5079
|
+
messageId,
|
|
5080
|
+
message: enriched,
|
|
5081
|
+
duplicate
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
4706
5084
|
async ingestFeishuEventAndDownloadAttachments(input) {
|
|
4707
|
-
const result = this.ingestFeishuEvent(input.payload);
|
|
5085
|
+
const result = input.memberResolver ? await this.ingestFeishuEventWithMembers({ payload: input.payload, memberResolver: input.memberResolver }) : this.ingestFeishuEvent(input.payload);
|
|
4708
5086
|
if (!result.accepted || !result.messageId || !result.message || result.duplicate) {
|
|
4709
5087
|
return result;
|
|
4710
5088
|
}
|