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/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs15 from "fs/promises";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "chattercatcher",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.30",
|
|
12
12
|
description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
|
|
13
13
|
type: "module",
|
|
14
14
|
main: "dist/index.js",
|
|
@@ -537,6 +537,18 @@ function migrateDatabase(database) {
|
|
|
537
537
|
|
|
538
538
|
CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
|
|
539
539
|
|
|
540
|
+
CREATE TABLE IF NOT EXISTS feishu_chat_members (
|
|
541
|
+
chat_id TEXT NOT NULL,
|
|
542
|
+
open_id TEXT NOT NULL,
|
|
543
|
+
user_id TEXT,
|
|
544
|
+
user_name TEXT,
|
|
545
|
+
updated_at TEXT NOT NULL,
|
|
546
|
+
PRIMARY KEY (chat_id, open_id)
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
|
|
550
|
+
ON feishu_chat_members(chat_id, user_name);
|
|
551
|
+
|
|
540
552
|
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
541
553
|
id TEXT PRIMARY KEY,
|
|
542
554
|
chat_id TEXT NOT NULL,
|
|
@@ -554,11 +566,29 @@ function migrateDatabase(database) {
|
|
|
554
566
|
|
|
555
567
|
CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
|
|
556
568
|
CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
|
|
569
|
+
|
|
570
|
+
CREATE TABLE IF NOT EXISTS feishu_chat_members (
|
|
571
|
+
chat_id TEXT NOT NULL,
|
|
572
|
+
open_id TEXT NOT NULL,
|
|
573
|
+
user_id TEXT,
|
|
574
|
+
user_name TEXT NOT NULL,
|
|
575
|
+
updated_at TEXT NOT NULL,
|
|
576
|
+
PRIMARY KEY (chat_id, open_id)
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
|
|
580
|
+
ON feishu_chat_members(chat_id, user_name);
|
|
557
581
|
`);
|
|
558
582
|
const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
583
|
+
const ensureCronJobColumn = (name, definition) => {
|
|
584
|
+
if (!cronJobColumns.some((column) => column.name === name)) {
|
|
585
|
+
database.prepare(`ALTER TABLE cron_jobs ADD COLUMN ${definition}`).run();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
ensureCronJobColumn("image_file_name", "image_file_name TEXT");
|
|
589
|
+
ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
|
|
590
|
+
ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
|
|
591
|
+
ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
|
|
562
592
|
}
|
|
563
593
|
|
|
564
594
|
// src/doctor/checks.ts
|
|
@@ -2901,6 +2931,9 @@ var CronJobRepository = class {
|
|
|
2901
2931
|
const schedule = input2.schedule.trim();
|
|
2902
2932
|
const prompt = input2.prompt.trim();
|
|
2903
2933
|
const imageFileName = input2.imageFileName?.trim();
|
|
2934
|
+
const mentionTargetName = input2.mentionTargetName?.trim();
|
|
2935
|
+
const mentionOpenId = input2.mentionOpenId?.trim();
|
|
2936
|
+
const mentionUserId = input2.mentionUserId?.trim();
|
|
2904
2937
|
if (!isValidCronSchedule(schedule)) {
|
|
2905
2938
|
throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
|
|
2906
2939
|
}
|
|
@@ -2919,6 +2952,9 @@ var CronJobRepository = class {
|
|
|
2919
2952
|
schedule,
|
|
2920
2953
|
prompt,
|
|
2921
2954
|
...imageFileName ? { imageFileName } : {},
|
|
2955
|
+
...mentionTargetName ? { mentionTargetName } : {},
|
|
2956
|
+
...mentionOpenId ? { mentionOpenId } : {},
|
|
2957
|
+
...mentionUserId ? { mentionUserId } : {},
|
|
2922
2958
|
status: "active",
|
|
2923
2959
|
nextRunAt: nextRunAt.toISOString(),
|
|
2924
2960
|
createdAt: now.toISOString(),
|
|
@@ -2927,17 +2963,22 @@ var CronJobRepository = class {
|
|
|
2927
2963
|
this.database.prepare(
|
|
2928
2964
|
`
|
|
2929
2965
|
INSERT INTO cron_jobs (
|
|
2930
|
-
id, chat_id, created_by_open_id, schedule, prompt, image_file_name,
|
|
2966
|
+
id, chat_id, created_by_open_id, schedule, prompt, image_file_name,
|
|
2967
|
+
mention_target_name, mention_open_id, mention_user_id, status,
|
|
2931
2968
|
last_run_at, next_run_at, last_error, created_at, updated_at
|
|
2932
2969
|
)
|
|
2933
2970
|
VALUES (
|
|
2934
|
-
@id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName,
|
|
2971
|
+
@id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName,
|
|
2972
|
+
@mentionTargetName, @mentionOpenId, @mentionUserId, @status,
|
|
2935
2973
|
NULL, @nextRunAt, NULL, @createdAt, @updatedAt
|
|
2936
2974
|
)
|
|
2937
2975
|
`
|
|
2938
2976
|
).run({
|
|
2939
2977
|
...record,
|
|
2940
|
-
imageFileName: record.imageFileName ?? null
|
|
2978
|
+
imageFileName: record.imageFileName ?? null,
|
|
2979
|
+
mentionTargetName: record.mentionTargetName ?? null,
|
|
2980
|
+
mentionOpenId: record.mentionOpenId ?? null,
|
|
2981
|
+
mentionUserId: record.mentionUserId ?? null
|
|
2941
2982
|
});
|
|
2942
2983
|
return record;
|
|
2943
2984
|
}
|
|
@@ -2964,6 +3005,9 @@ var CronJobRepository = class {
|
|
|
2964
3005
|
schedule,
|
|
2965
3006
|
prompt,
|
|
2966
3007
|
image_file_name AS imageFileName,
|
|
3008
|
+
mention_target_name AS mentionTargetName,
|
|
3009
|
+
mention_open_id AS mentionOpenId,
|
|
3010
|
+
mention_user_id AS mentionUserId,
|
|
2967
3011
|
status,
|
|
2968
3012
|
last_run_at AS lastRunAt,
|
|
2969
3013
|
next_run_at AS nextRunAt,
|
|
@@ -2983,6 +3027,9 @@ var CronJobRepository = class {
|
|
|
2983
3027
|
schedule: row.schedule,
|
|
2984
3028
|
prompt: row.prompt,
|
|
2985
3029
|
imageFileName: row.imageFileName ?? void 0,
|
|
3030
|
+
mentionTargetName: row.mentionTargetName ?? void 0,
|
|
3031
|
+
mentionOpenId: row.mentionOpenId ?? void 0,
|
|
3032
|
+
mentionUserId: row.mentionUserId ?? void 0,
|
|
2986
3033
|
status: row.status,
|
|
2987
3034
|
lastRunAt: row.lastRunAt ?? void 0,
|
|
2988
3035
|
nextRunAt: row.nextRunAt,
|
|
@@ -3057,6 +3104,9 @@ var CronJobRepository = class {
|
|
|
3057
3104
|
schedule,
|
|
3058
3105
|
prompt,
|
|
3059
3106
|
image_file_name AS imageFileName,
|
|
3107
|
+
mention_target_name AS mentionTargetName,
|
|
3108
|
+
mention_open_id AS mentionOpenId,
|
|
3109
|
+
mention_user_id AS mentionUserId,
|
|
3060
3110
|
status,
|
|
3061
3111
|
last_run_at AS lastRunAt,
|
|
3062
3112
|
next_run_at AS nextRunAt,
|
|
@@ -3076,6 +3126,9 @@ var CronJobRepository = class {
|
|
|
3076
3126
|
schedule: row.schedule,
|
|
3077
3127
|
prompt: row.prompt,
|
|
3078
3128
|
imageFileName: row.imageFileName ?? void 0,
|
|
3129
|
+
mentionTargetName: row.mentionTargetName ?? void 0,
|
|
3130
|
+
mentionOpenId: row.mentionOpenId ?? void 0,
|
|
3131
|
+
mentionUserId: row.mentionUserId ?? void 0,
|
|
3079
3132
|
status: row.status,
|
|
3080
3133
|
lastRunAt: row.lastRunAt ?? void 0,
|
|
3081
3134
|
nextRunAt: row.nextRunAt,
|
|
@@ -3101,8 +3154,12 @@ async function generateCronJobMessage(input2) {
|
|
|
3101
3154
|
if (!input2.model.completeWithTools) {
|
|
3102
3155
|
throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
|
|
3103
3156
|
}
|
|
3157
|
+
const systemPrompt = input2.memberPrompt ? `${SYSTEM_PROMPT}
|
|
3158
|
+
|
|
3159
|
+
${input2.memberPrompt}
|
|
3160
|
+
\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;
|
|
3104
3161
|
const messages = [
|
|
3105
|
-
{ role: "system", content:
|
|
3162
|
+
{ role: "system", content: systemPrompt },
|
|
3106
3163
|
{ role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
|
|
3107
3164
|
\u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}` }
|
|
3108
3165
|
];
|
|
@@ -3120,7 +3177,7 @@ async function generateCronJobMessage(input2) {
|
|
|
3120
3177
|
for (const call of result.toolCalls) {
|
|
3121
3178
|
if (toolCallsUsed >= maxToolCalls) {
|
|
3122
3179
|
return input2.model.complete([
|
|
3123
|
-
{ role: "system", content:
|
|
3180
|
+
{ role: "system", content: systemPrompt },
|
|
3124
3181
|
{
|
|
3125
3182
|
role: "user",
|
|
3126
3183
|
content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
|
|
@@ -3179,7 +3236,12 @@ function createCronJobScheduler(options) {
|
|
|
3179
3236
|
for (const job of jobs) {
|
|
3180
3237
|
try {
|
|
3181
3238
|
const text = await options.generateMessage(job, startedAt);
|
|
3182
|
-
|
|
3239
|
+
const sendOptions = job.mentionOpenId && job.mentionTargetName ? { mentions: [{ openId: job.mentionOpenId, name: job.mentionTargetName }] } : void 0;
|
|
3240
|
+
if (sendOptions) {
|
|
3241
|
+
await options.sendTextToChat(job.chatId, text, sendOptions);
|
|
3242
|
+
} else {
|
|
3243
|
+
await options.sendTextToChat(job.chatId, text);
|
|
3244
|
+
}
|
|
3183
3245
|
if (job.imageFileName) {
|
|
3184
3246
|
if (!options.sendImageToChat) {
|
|
3185
3247
|
throw new Error("\u5F53\u524D\u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\u3002");
|
|
@@ -3677,6 +3739,185 @@ var ImageMultimodalWorker = class {
|
|
|
3677
3739
|
}
|
|
3678
3740
|
};
|
|
3679
3741
|
|
|
3742
|
+
// src/feishu/members.ts
|
|
3743
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
3744
|
+
var FeishuMemberRepository = class {
|
|
3745
|
+
constructor(database) {
|
|
3746
|
+
this.database = database;
|
|
3747
|
+
}
|
|
3748
|
+
database;
|
|
3749
|
+
upsert(record) {
|
|
3750
|
+
this.database.prepare(
|
|
3751
|
+
`
|
|
3752
|
+
INSERT INTO feishu_chat_members (chat_id, open_id, user_id, user_name, updated_at)
|
|
3753
|
+
VALUES (@chatId, @openId, @userId, @userName, @updatedAt)
|
|
3754
|
+
ON CONFLICT(chat_id, open_id)
|
|
3755
|
+
DO UPDATE SET
|
|
3756
|
+
user_id = excluded.user_id,
|
|
3757
|
+
user_name = excluded.user_name,
|
|
3758
|
+
updated_at = excluded.updated_at
|
|
3759
|
+
`
|
|
3760
|
+
).run({
|
|
3761
|
+
chatId: record.chatId,
|
|
3762
|
+
openId: record.openId,
|
|
3763
|
+
userId: record.userId ?? null,
|
|
3764
|
+
userName: record.userName,
|
|
3765
|
+
updatedAt: record.updatedAt
|
|
3766
|
+
});
|
|
3767
|
+
}
|
|
3768
|
+
get(chatId, openId) {
|
|
3769
|
+
const row = this.database.prepare(
|
|
3770
|
+
`
|
|
3771
|
+
SELECT
|
|
3772
|
+
chat_id AS chatId,
|
|
3773
|
+
open_id AS openId,
|
|
3774
|
+
user_id AS userId,
|
|
3775
|
+
user_name AS userName,
|
|
3776
|
+
updated_at AS updatedAt
|
|
3777
|
+
FROM feishu_chat_members
|
|
3778
|
+
WHERE chat_id = ? AND open_id = ?
|
|
3779
|
+
`
|
|
3780
|
+
).get(chatId, openId);
|
|
3781
|
+
return row ?? null;
|
|
3782
|
+
}
|
|
3783
|
+
listByChat(chatId) {
|
|
3784
|
+
return this.database.prepare(
|
|
3785
|
+
`
|
|
3786
|
+
SELECT
|
|
3787
|
+
chat_id AS chatId,
|
|
3788
|
+
open_id AS openId,
|
|
3789
|
+
user_id AS userId,
|
|
3790
|
+
user_name AS userName,
|
|
3791
|
+
updated_at AS updatedAt
|
|
3792
|
+
FROM feishu_chat_members
|
|
3793
|
+
WHERE chat_id = ?
|
|
3794
|
+
ORDER BY user_name ASC, open_id ASC
|
|
3795
|
+
`
|
|
3796
|
+
).all(chatId);
|
|
3797
|
+
}
|
|
3798
|
+
findUniqueByName(chatId, userName) {
|
|
3799
|
+
const rows = this.database.prepare(
|
|
3800
|
+
`
|
|
3801
|
+
SELECT
|
|
3802
|
+
chat_id AS chatId,
|
|
3803
|
+
open_id AS openId,
|
|
3804
|
+
user_id AS userId,
|
|
3805
|
+
user_name AS userName,
|
|
3806
|
+
updated_at AS updatedAt
|
|
3807
|
+
FROM feishu_chat_members
|
|
3808
|
+
WHERE chat_id = ? AND user_name = ?
|
|
3809
|
+
ORDER BY open_id ASC
|
|
3810
|
+
LIMIT 2
|
|
3811
|
+
`
|
|
3812
|
+
).all(chatId, userName);
|
|
3813
|
+
return rows.length === 1 ? rows[0] : null;
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
3816
|
+
var FeishuMemberResolver = class {
|
|
3817
|
+
constructor(options) {
|
|
3818
|
+
this.options = options;
|
|
3819
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3820
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
3821
|
+
this.logger = options.logger;
|
|
3822
|
+
}
|
|
3823
|
+
options;
|
|
3824
|
+
now;
|
|
3825
|
+
ttlMs;
|
|
3826
|
+
logger;
|
|
3827
|
+
async resolveOpenIdName(chatId, openId) {
|
|
3828
|
+
const cached = this.options.repository.get(chatId, openId);
|
|
3829
|
+
if (!cached || this.isExpired(cached.updatedAt)) {
|
|
3830
|
+
try {
|
|
3831
|
+
await this.refreshChatMembers(chatId);
|
|
3832
|
+
} catch (error) {
|
|
3833
|
+
this.logger?.warn("Failed to refresh Feishu chat members for open id resolution", {
|
|
3834
|
+
chatId,
|
|
3835
|
+
openId,
|
|
3836
|
+
error
|
|
3837
|
+
});
|
|
3838
|
+
return cached?.userName ?? openId;
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
return this.options.repository.get(chatId, openId)?.userName ?? openId;
|
|
3842
|
+
}
|
|
3843
|
+
async resolveUniqueName(chatId, userName) {
|
|
3844
|
+
const cached = this.options.repository.findUniqueByName(chatId, userName);
|
|
3845
|
+
if (cached && !this.isExpired(cached.updatedAt)) {
|
|
3846
|
+
return cached;
|
|
3847
|
+
}
|
|
3848
|
+
try {
|
|
3849
|
+
await this.refreshChatMembers(chatId);
|
|
3850
|
+
} catch (error) {
|
|
3851
|
+
this.logger?.warn("Failed to refresh Feishu chat members for unique name resolution", {
|
|
3852
|
+
chatId,
|
|
3853
|
+
userName,
|
|
3854
|
+
error
|
|
3855
|
+
});
|
|
3856
|
+
return cached ?? null;
|
|
3857
|
+
}
|
|
3858
|
+
return this.options.repository.findUniqueByName(chatId, userName);
|
|
3859
|
+
}
|
|
3860
|
+
isExpired(updatedAt) {
|
|
3861
|
+
const updatedAtMs = Date.parse(updatedAt);
|
|
3862
|
+
if (Number.isNaN(updatedAtMs)) {
|
|
3863
|
+
return true;
|
|
3864
|
+
}
|
|
3865
|
+
return this.now().getTime() - updatedAtMs >= this.ttlMs;
|
|
3866
|
+
}
|
|
3867
|
+
async refreshChatMembers(chatId) {
|
|
3868
|
+
const members = await this.options.client.listChatMembers({ chatId, memberIdType: "open_id" });
|
|
3869
|
+
const updatedAt = this.now().toISOString();
|
|
3870
|
+
for (const member of members) {
|
|
3871
|
+
this.options.repository.upsert({
|
|
3872
|
+
chatId,
|
|
3873
|
+
openId: member.openId,
|
|
3874
|
+
userId: member.userId,
|
|
3875
|
+
userName: member.userName,
|
|
3876
|
+
updatedAt
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
};
|
|
3881
|
+
function formatFeishuMemberPrompt(members, limit = 80) {
|
|
3882
|
+
const lines = members.filter((member) => member.userName).slice(0, limit).map((member) => `${member.openId} = ${member.userName}`);
|
|
3883
|
+
return lines.length ? `\u5F53\u524D\u7FA4\u804A\u6210\u5458 ID \u4E0E\u7FA4\u6635\u79F0\u6620\u5C04\uFF1A
|
|
3884
|
+
${lines.join("\n")}` : "";
|
|
3885
|
+
}
|
|
3886
|
+
function createFeishuChatMembersClient(client) {
|
|
3887
|
+
return {
|
|
3888
|
+
async listChatMembers(payload) {
|
|
3889
|
+
const api = client.im.v1?.chatMembers?.get;
|
|
3890
|
+
if (!api) {
|
|
3891
|
+
throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301 chatMembers.get\uFF0C\u65E0\u6CD5\u83B7\u53D6\u7FA4\u6210\u5458\u3002");
|
|
3892
|
+
}
|
|
3893
|
+
const members = [];
|
|
3894
|
+
let pageToken;
|
|
3895
|
+
do {
|
|
3896
|
+
const response = await api({
|
|
3897
|
+
path: { chat_id: payload.chatId },
|
|
3898
|
+
params: {
|
|
3899
|
+
member_id_type: payload.memberIdType,
|
|
3900
|
+
...pageToken ? { page_token: pageToken } : {}
|
|
3901
|
+
}
|
|
3902
|
+
});
|
|
3903
|
+
const items = response.data?.items ?? [];
|
|
3904
|
+
for (const item of items) {
|
|
3905
|
+
if (!item.member_id || !item.name) {
|
|
3906
|
+
continue;
|
|
3907
|
+
}
|
|
3908
|
+
members.push({
|
|
3909
|
+
openId: item.member_id,
|
|
3910
|
+
userId: item.user_id,
|
|
3911
|
+
userName: item.name
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
pageToken = response.data?.has_more ? response.data.page_token : void 0;
|
|
3915
|
+
} while (pageToken);
|
|
3916
|
+
return members;
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3680
3921
|
// src/cron/tools.ts
|
|
3681
3922
|
function readString(input2, key) {
|
|
3682
3923
|
const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
|
|
@@ -3715,18 +3956,27 @@ function createCronJobTools(input2) {
|
|
|
3715
3956
|
imageFileName: {
|
|
3716
3957
|
type: "string",
|
|
3717
3958
|
description: "Optional image filename already stored from the current chat, for example om_xxx-image.jpg."
|
|
3959
|
+
},
|
|
3960
|
+
mentionTargetName: {
|
|
3961
|
+
type: "string",
|
|
3962
|
+
description: "Optional exact Feishu chat nickname to @ when the scheduled message is sent."
|
|
3718
3963
|
}
|
|
3719
3964
|
},
|
|
3720
3965
|
required: ["schedule", "prompt"],
|
|
3721
3966
|
additionalProperties: false
|
|
3722
3967
|
},
|
|
3723
3968
|
execute: async (rawInput) => {
|
|
3969
|
+
const mentionTargetName = readOptionalString(rawInput, "mentionTargetName");
|
|
3970
|
+
const mentionTarget = mentionTargetName && input2.memberResolver ? await input2.memberResolver.resolveUniqueName(input2.chatId, mentionTargetName) : null;
|
|
3724
3971
|
const job = input2.repository.create({
|
|
3725
3972
|
chatId: input2.chatId,
|
|
3726
3973
|
createdByOpenId: input2.createdByOpenId,
|
|
3727
3974
|
schedule: readString(rawInput, "schedule"),
|
|
3728
3975
|
prompt: readString(rawInput, "prompt"),
|
|
3729
|
-
imageFileName: readOptionalString(rawInput, "imageFileName")
|
|
3976
|
+
imageFileName: readOptionalString(rawInput, "imageFileName"),
|
|
3977
|
+
mentionTargetName,
|
|
3978
|
+
mentionOpenId: mentionTarget?.openId,
|
|
3979
|
+
mentionUserId: mentionTarget?.userId
|
|
3730
3980
|
});
|
|
3731
3981
|
return JSON.stringify({ ok: true, job });
|
|
3732
3982
|
}
|
|
@@ -3769,6 +4019,20 @@ import crypto6 from "crypto";
|
|
|
3769
4019
|
function clampLimit(limit) {
|
|
3770
4020
|
return Math.max(1, Math.min(200, Math.trunc(limit)));
|
|
3771
4021
|
}
|
|
4022
|
+
function mapQaLogRow(row) {
|
|
4023
|
+
return {
|
|
4024
|
+
id: row.id,
|
|
4025
|
+
chatId: row.chat_id,
|
|
4026
|
+
questionMessageId: row.question_message_id,
|
|
4027
|
+
question: row.question,
|
|
4028
|
+
answer: row.answer,
|
|
4029
|
+
citations: JSON.parse(row.citations_json),
|
|
4030
|
+
retrievalDebug: JSON.parse(row.retrieval_debug_json),
|
|
4031
|
+
status: row.status,
|
|
4032
|
+
error: row.error,
|
|
4033
|
+
createdAt: row.created_at
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
3772
4036
|
var QaLogRepository = class {
|
|
3773
4037
|
constructor(database) {
|
|
3774
4038
|
this.database = database;
|
|
@@ -3847,18 +4111,29 @@ var QaLogRepository = class {
|
|
|
3847
4111
|
LIMIT ?
|
|
3848
4112
|
`
|
|
3849
4113
|
).all(clampLimit(limit));
|
|
3850
|
-
return rows.map(
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
4114
|
+
return rows.map(mapQaLogRow);
|
|
4115
|
+
}
|
|
4116
|
+
listRecentByChat(chatId, limit) {
|
|
4117
|
+
const rows = this.database.prepare(
|
|
4118
|
+
`
|
|
4119
|
+
SELECT
|
|
4120
|
+
id,
|
|
4121
|
+
chat_id,
|
|
4122
|
+
question_message_id,
|
|
4123
|
+
question,
|
|
4124
|
+
answer,
|
|
4125
|
+
citations_json,
|
|
4126
|
+
retrieval_debug_json,
|
|
4127
|
+
status,
|
|
4128
|
+
error,
|
|
4129
|
+
created_at
|
|
4130
|
+
FROM qa_logs
|
|
4131
|
+
WHERE chat_id = ? AND status = 'answered'
|
|
4132
|
+
ORDER BY created_at DESC
|
|
4133
|
+
LIMIT ?
|
|
4134
|
+
`
|
|
4135
|
+
).all(chatId, clampLimit(limit));
|
|
4136
|
+
return rows.map(mapQaLogRow);
|
|
3862
4137
|
}
|
|
3863
4138
|
getCount() {
|
|
3864
4139
|
const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
|
|
@@ -3930,8 +4205,18 @@ async function runFeishuToolLoop(input2) {
|
|
|
3930
4205
|
}
|
|
3931
4206
|
const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
|
|
3932
4207
|
const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
4208
|
+
const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
|
|
4209
|
+
if (input2.memberPrompt) {
|
|
4210
|
+
systemPromptParts.push(`${input2.memberPrompt}
|
|
4211
|
+
\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`);
|
|
4212
|
+
}
|
|
4213
|
+
if (input2.conversationContext) {
|
|
4214
|
+
systemPromptParts.push(`${input2.conversationContext}
|
|
4215
|
+
\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`);
|
|
4216
|
+
}
|
|
4217
|
+
const systemPrompt = systemPromptParts.join("\n\n");
|
|
3933
4218
|
const messages = [
|
|
3934
|
-
{ role: "system", content:
|
|
4219
|
+
{ role: "system", content: systemPrompt },
|
|
3935
4220
|
{ role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
|
|
3936
4221
|
\u95EE\u9898\uFF1A${input2.question}` }
|
|
3937
4222
|
];
|
|
@@ -3993,6 +4278,13 @@ async function runFeishuToolLoop(input2) {
|
|
|
3993
4278
|
return "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
3994
4279
|
}
|
|
3995
4280
|
}
|
|
4281
|
+
function formatConversationContext(records) {
|
|
4282
|
+
const lines = records.slice().reverse().map((record, index2) => `\u7B2C ${index2 + 1} \u8F6E
|
|
4283
|
+
\u7528\u6237\uFF1A${record.question}
|
|
4284
|
+
\u52A9\u624B\uFF1A${record.answer}`);
|
|
4285
|
+
return lines.length ? `\u8FD1\u671F\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1A
|
|
4286
|
+
${lines.join("\n\n")}` : "";
|
|
4287
|
+
}
|
|
3996
4288
|
function isMentionForBot(mention, config) {
|
|
3997
4289
|
if (!config.feishu.botOpenId) {
|
|
3998
4290
|
return false;
|
|
@@ -4043,8 +4335,13 @@ function getFeishuQuestionDecision(payload, config) {
|
|
|
4043
4335
|
var FeishuQuestionHandler = class {
|
|
4044
4336
|
constructor(options) {
|
|
4045
4337
|
this.options = options;
|
|
4338
|
+
this.memberResolver = options.memberResolver;
|
|
4046
4339
|
}
|
|
4047
4340
|
options;
|
|
4341
|
+
memberResolver;
|
|
4342
|
+
setMemberResolver(memberResolver) {
|
|
4343
|
+
this.memberResolver = memberResolver;
|
|
4344
|
+
}
|
|
4048
4345
|
async sendResponse(chatId, messageId, text) {
|
|
4049
4346
|
if (messageId && this.options.sender.replyTextToMessage) {
|
|
4050
4347
|
try {
|
|
@@ -4091,14 +4388,20 @@ var FeishuQuestionHandler = class {
|
|
|
4091
4388
|
const cronTools = createCronJobTools({
|
|
4092
4389
|
repository: new CronJobRepository(this.options.database),
|
|
4093
4390
|
chatId: decision.chatId,
|
|
4094
|
-
createdByOpenId: payload.event?.sender?.sender_id?.open_id
|
|
4391
|
+
createdByOpenId: payload.event?.sender?.sender_id?.open_id,
|
|
4392
|
+
memberResolver: this.memberResolver
|
|
4095
4393
|
});
|
|
4096
4394
|
const allTools = [...tools, ...cronTools];
|
|
4395
|
+
const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
|
|
4396
|
+
const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
|
|
4397
|
+
const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
|
|
4097
4398
|
const answer = await runFeishuToolLoop({
|
|
4098
4399
|
question: decision.question,
|
|
4099
4400
|
now,
|
|
4100
4401
|
tools: allTools,
|
|
4101
|
-
model: this.options.model
|
|
4402
|
+
model: this.options.model,
|
|
4403
|
+
memberPrompt,
|
|
4404
|
+
conversationContext
|
|
4102
4405
|
});
|
|
4103
4406
|
qaLogs.create({
|
|
4104
4407
|
chatId: decision.chatId,
|
|
@@ -4151,6 +4454,15 @@ function extractImageKey(response) {
|
|
|
4151
4454
|
}
|
|
4152
4455
|
throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
|
|
4153
4456
|
}
|
|
4457
|
+
function escapeAtText(value) {
|
|
4458
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4459
|
+
}
|
|
4460
|
+
function formatTextWithMentions(text, options) {
|
|
4461
|
+
const mentions = options?.mentions ?? [];
|
|
4462
|
+
if (mentions.length === 0) return text;
|
|
4463
|
+
const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
|
|
4464
|
+
return `${prefix} ${text}`.trim();
|
|
4465
|
+
}
|
|
4154
4466
|
var FeishuMessageSender = class _FeishuMessageSender {
|
|
4155
4467
|
constructor(client) {
|
|
4156
4468
|
this.client = client;
|
|
@@ -4164,12 +4476,12 @@ var FeishuMessageSender = class _FeishuMessageSender {
|
|
|
4164
4476
|
});
|
|
4165
4477
|
return new _FeishuMessageSender(client);
|
|
4166
4478
|
}
|
|
4167
|
-
async sendTextToChat(chatId, text) {
|
|
4479
|
+
async sendTextToChat(chatId, text, options) {
|
|
4168
4480
|
const payload = {
|
|
4169
4481
|
data: {
|
|
4170
4482
|
receive_id: chatId,
|
|
4171
4483
|
msg_type: "text",
|
|
4172
|
-
content: JSON.stringify({ text })
|
|
4484
|
+
content: JSON.stringify({ text: formatTextWithMentions(text, options) })
|
|
4173
4485
|
},
|
|
4174
4486
|
params: {
|
|
4175
4487
|
receive_id_type: "chat_id"
|
|
@@ -4291,13 +4603,24 @@ function createFeishuEventDispatcher(options) {
|
|
|
4291
4603
|
return;
|
|
4292
4604
|
}
|
|
4293
4605
|
}
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4606
|
+
let result;
|
|
4607
|
+
if (options.resourceDownloader) {
|
|
4608
|
+
result = await options.ingestor.ingestFeishuEventAndDownloadAttachments({
|
|
4609
|
+
payload,
|
|
4610
|
+
downloader: options.resourceDownloader,
|
|
4611
|
+
config: options.config,
|
|
4612
|
+
secrets: options.secrets,
|
|
4613
|
+
vectorIndexMessage: options.attachmentVectorIndexer,
|
|
4614
|
+
memberResolver: options.memberResolver
|
|
4615
|
+
});
|
|
4616
|
+
} else if (options.memberResolver) {
|
|
4617
|
+
result = await options.ingestor.ingestFeishuEventWithMembers({
|
|
4618
|
+
payload,
|
|
4619
|
+
memberResolver: options.memberResolver
|
|
4620
|
+
});
|
|
4621
|
+
} else {
|
|
4622
|
+
result = options.ingestor.ingestFeishuEvent(payload);
|
|
4623
|
+
}
|
|
4301
4624
|
if (!result.accepted) {
|
|
4302
4625
|
console.log(`\u98DE\u4E66\u6D88\u606F\u672A\u5165\u5E93\uFF1A${result.reason}`);
|
|
4303
4626
|
return;
|
|
@@ -4388,10 +4711,20 @@ function createFeishuGateway(options) {
|
|
|
4388
4711
|
onReconnecting: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u6B63\u5728\u91CD\u8FDE\u3002"),
|
|
4389
4712
|
onReconnected: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u91CD\u8FDE\u3002")
|
|
4390
4713
|
});
|
|
4714
|
+
const memberResolver = new FeishuMemberResolver({
|
|
4715
|
+
repository: new FeishuMemberRepository(options.ingestor.database),
|
|
4716
|
+
client: createFeishuChatMembersClient(new lark2.Client({
|
|
4717
|
+
appId: options.config.feishu.appId,
|
|
4718
|
+
appSecret: options.secrets.feishu.appSecret,
|
|
4719
|
+
domain: mapDomain(options.config.feishu.domain)
|
|
4720
|
+
}))
|
|
4721
|
+
});
|
|
4722
|
+
options.questionHandler?.setMemberResolver?.(memberResolver);
|
|
4391
4723
|
const eventDispatcher = createFeishuEventDispatcher({
|
|
4392
4724
|
config: options.config,
|
|
4393
4725
|
secrets: options.secrets,
|
|
4394
4726
|
ingestor: options.ingestor,
|
|
4727
|
+
memberResolver,
|
|
4395
4728
|
questionHandler: options.questionHandler,
|
|
4396
4729
|
resourceDownloader: options.resourceDownloader,
|
|
4397
4730
|
attachmentVectorIndexer: options.attachmentVectorIndexer,
|
|
@@ -4411,7 +4744,7 @@ function createFeishuGateway(options) {
|
|
|
4411
4744
|
}) : void 0);
|
|
4412
4745
|
const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
|
|
4413
4746
|
repository: new CronJobRepository(options.cronJobProcessor.database),
|
|
4414
|
-
sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
|
|
4747
|
+
sendTextToChat: (chatId, text, sendOptions) => options.cronJobProcessor.sender.sendTextToChat(chatId, text, sendOptions),
|
|
4415
4748
|
sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
|
|
4416
4749
|
chatId,
|
|
4417
4750
|
resolveFeishuImagePath(options.config, imageFileName)
|
|
@@ -4425,7 +4758,16 @@ function createFeishuGateway(options) {
|
|
|
4425
4758
|
scope: { platform: "feishu", platformChatId: job.chatId }
|
|
4426
4759
|
});
|
|
4427
4760
|
try {
|
|
4428
|
-
|
|
4761
|
+
const memberPrompt = formatFeishuMemberPrompt(
|
|
4762
|
+
new FeishuMemberRepository(options.cronJobProcessor.database).listByChat(job.chatId)
|
|
4763
|
+
);
|
|
4764
|
+
return await generateCronJobMessage({
|
|
4765
|
+
prompt: job.prompt,
|
|
4766
|
+
model: options.cronJobProcessor.model,
|
|
4767
|
+
tools,
|
|
4768
|
+
now,
|
|
4769
|
+
memberPrompt
|
|
4770
|
+
});
|
|
4429
4771
|
} finally {
|
|
4430
4772
|
close();
|
|
4431
4773
|
}
|
|
@@ -4783,7 +5125,9 @@ function normalizeFeishuReceiveMessageEvent(payload) {
|
|
|
4783
5125
|
if (!text) {
|
|
4784
5126
|
return null;
|
|
4785
5127
|
}
|
|
4786
|
-
const
|
|
5128
|
+
const senderOpenId = event.sender?.sender_id?.open_id;
|
|
5129
|
+
const senderUserId = event.sender?.sender_id?.user_id;
|
|
5130
|
+
const senderId = senderOpenId || senderUserId || event.sender?.sender_id?.union_id || "unknown";
|
|
4787
5131
|
return {
|
|
4788
5132
|
platform: "feishu",
|
|
4789
5133
|
platformChatId: message.chat_id,
|
|
@@ -4798,12 +5142,24 @@ function normalizeFeishuReceiveMessageEvent(payload) {
|
|
|
4798
5142
|
platform: "feishu",
|
|
4799
5143
|
raw: payload,
|
|
4800
5144
|
content,
|
|
5145
|
+
sender: {
|
|
5146
|
+
...senderOpenId ? { openId: senderOpenId } : {},
|
|
5147
|
+
...senderUserId ? { userId: senderUserId } : {}
|
|
5148
|
+
},
|
|
4801
5149
|
attachment: extractFeishuAttachment(messageType, content)
|
|
4802
5150
|
}
|
|
4803
5151
|
};
|
|
4804
5152
|
}
|
|
4805
5153
|
|
|
4806
5154
|
// src/gateway/ingest.ts
|
|
5155
|
+
function extractFeishuSenderOpenId(message) {
|
|
5156
|
+
const raw = message.rawPayload;
|
|
5157
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
5158
|
+
const sender = raw.sender;
|
|
5159
|
+
if (!sender || typeof sender !== "object" || Array.isArray(sender)) return void 0;
|
|
5160
|
+
const openId = sender.openId;
|
|
5161
|
+
return typeof openId === "string" && openId.trim() ? openId.trim() : void 0;
|
|
5162
|
+
}
|
|
4807
5163
|
function extractAttachment(message) {
|
|
4808
5164
|
const raw = message.rawPayload;
|
|
4809
5165
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -4823,14 +5179,16 @@ function isMultimodalReady(config, secrets) {
|
|
|
4823
5179
|
return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
|
|
4824
5180
|
}
|
|
4825
5181
|
var GatewayIngestor = class {
|
|
4826
|
-
messages;
|
|
4827
|
-
jobs;
|
|
4828
|
-
imageTasks;
|
|
4829
5182
|
constructor(database) {
|
|
5183
|
+
this.database = database;
|
|
4830
5184
|
this.messages = new MessageRepository(database);
|
|
4831
5185
|
this.jobs = new FileJobRepository(database);
|
|
4832
5186
|
this.imageTasks = new ImageMultimodalTaskRepository(database);
|
|
4833
5187
|
}
|
|
5188
|
+
database;
|
|
5189
|
+
messages;
|
|
5190
|
+
jobs;
|
|
5191
|
+
imageTasks;
|
|
4834
5192
|
ingestFeishuEvent(payload) {
|
|
4835
5193
|
const normalized = normalizeFeishuReceiveMessageEvent(payload);
|
|
4836
5194
|
if (!normalized) {
|
|
@@ -4848,8 +5206,28 @@ var GatewayIngestor = class {
|
|
|
4848
5206
|
duplicate
|
|
4849
5207
|
};
|
|
4850
5208
|
}
|
|
5209
|
+
async ingestFeishuEventWithMembers(input2) {
|
|
5210
|
+
const normalized = normalizeFeishuReceiveMessageEvent(input2.payload);
|
|
5211
|
+
if (!normalized) {
|
|
5212
|
+
return {
|
|
5213
|
+
accepted: false,
|
|
5214
|
+
reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
|
|
5215
|
+
};
|
|
5216
|
+
}
|
|
5217
|
+
const openId = extractFeishuSenderOpenId(normalized);
|
|
5218
|
+
const senderName = openId ? await input2.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
|
|
5219
|
+
const enriched = { ...normalized, senderName };
|
|
5220
|
+
const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
|
|
5221
|
+
const messageId = this.messages.ingest(enriched);
|
|
5222
|
+
return {
|
|
5223
|
+
accepted: true,
|
|
5224
|
+
messageId,
|
|
5225
|
+
message: enriched,
|
|
5226
|
+
duplicate
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
4851
5229
|
async ingestFeishuEventAndDownloadAttachments(input2) {
|
|
4852
|
-
const result = this.ingestFeishuEvent(input2.payload);
|
|
5230
|
+
const result = input2.memberResolver ? await this.ingestFeishuEventWithMembers({ payload: input2.payload, memberResolver: input2.memberResolver }) : this.ingestFeishuEvent(input2.payload);
|
|
4853
5231
|
if (!result.accepted || !result.messageId || !result.message || result.duplicate) {
|
|
4854
5232
|
return result;
|
|
4855
5233
|
}
|
|
@@ -5104,6 +5482,9 @@ function createMultimodalModel(config, secrets) {
|
|
|
5104
5482
|
});
|
|
5105
5483
|
}
|
|
5106
5484
|
|
|
5485
|
+
// src/cli.ts
|
|
5486
|
+
import * as lark4 from "@larksuiteoapi/node-sdk";
|
|
5487
|
+
|
|
5107
5488
|
// src/rag/answer.ts
|
|
5108
5489
|
var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
|
|
5109
5490
|
var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
|
|
@@ -6201,6 +6582,14 @@ async function startGatewayForegroundCommand() {
|
|
|
6201
6582
|
const database = openDatabase(config);
|
|
6202
6583
|
const chatModel = createChatModel(config, secrets);
|
|
6203
6584
|
const sender = FeishuMessageSender.fromConfig(config, secrets);
|
|
6585
|
+
const memberResolver = new FeishuMemberResolver({
|
|
6586
|
+
repository: new FeishuMemberRepository(database),
|
|
6587
|
+
client: createFeishuChatMembersClient(new lark4.Client({
|
|
6588
|
+
appId: config.feishu.appId,
|
|
6589
|
+
appSecret: secrets.feishu.appSecret,
|
|
6590
|
+
domain: mapDomain(config.feishu.domain)
|
|
6591
|
+
}))
|
|
6592
|
+
});
|
|
6204
6593
|
const vectorStore = hasEmbeddingConfig(config, secrets) ? new SqliteVectorStore(database, { model: config.embedding.model }) : null;
|
|
6205
6594
|
const gatewayRuntime = createFeishuGateway({
|
|
6206
6595
|
config,
|
|
@@ -6233,6 +6622,7 @@ async function startGatewayForegroundCommand() {
|
|
|
6233
6622
|
config,
|
|
6234
6623
|
secrets,
|
|
6235
6624
|
database,
|
|
6625
|
+
memberResolver,
|
|
6236
6626
|
sender,
|
|
6237
6627
|
model: chatModel
|
|
6238
6628
|
})
|