@max1874/feishu 0.2.28 → 0.3.1
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/package.json +1 -1
- package/src/accounts.ts +30 -7
- package/src/bot.ts +3 -1
- package/src/channel.ts +90 -23
- package/src/client.ts +13 -15
- package/src/config-schema.ts +16 -0
- package/src/monitor.ts +95 -39
- package/src/types.ts +2 -1
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -21,16 +21,34 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Merge per-account overrides with the base (top-level) feishu config.
|
|
26
|
+
* Account-specific fields (credentials, domain, connectionMode, etc.) override the base.
|
|
27
|
+
*/
|
|
28
|
+
export function mergeFeishuAccountConfig(
|
|
29
|
+
baseCfg: FeishuConfig,
|
|
30
|
+
accountId: string,
|
|
31
|
+
): FeishuConfig {
|
|
32
|
+
if (accountId === DEFAULT_ACCOUNT_ID) return baseCfg;
|
|
33
|
+
const overrides = baseCfg.accounts?.[accountId];
|
|
34
|
+
if (!overrides) return baseCfg;
|
|
35
|
+
// Strip `accounts` from merged result to prevent downstream confusion
|
|
36
|
+
const { accounts: _, ...base } = baseCfg;
|
|
37
|
+
return { ...base, ...overrides };
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
export function resolveFeishuAccount(params: {
|
|
25
41
|
cfg: ClawdbotConfig;
|
|
26
42
|
accountId?: string | null;
|
|
27
43
|
}): ResolvedFeishuAccount {
|
|
28
44
|
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
45
|
+
const accountId = params.accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
|
46
|
+
const merged = feishuCfg ? mergeFeishuAccountConfig(feishuCfg, accountId) : undefined;
|
|
47
|
+
const enabled = merged?.enabled !== false;
|
|
48
|
+
const creds = resolveFeishuCredentials(merged);
|
|
31
49
|
|
|
32
50
|
return {
|
|
33
|
-
accountId
|
|
51
|
+
accountId,
|
|
34
52
|
enabled,
|
|
35
53
|
configured: Boolean(creds),
|
|
36
54
|
appId: creds?.appId,
|
|
@@ -38,12 +56,17 @@ export function resolveFeishuAccount(params: {
|
|
|
38
56
|
};
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
export function listFeishuAccountIds(
|
|
42
|
-
|
|
59
|
+
export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
|
|
60
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
61
|
+
const accounts = feishuCfg?.accounts;
|
|
62
|
+
if (!accounts) return [DEFAULT_ACCOUNT_ID];
|
|
63
|
+
const ids = Object.keys(accounts);
|
|
64
|
+
return ids.length === 0 ? [DEFAULT_ACCOUNT_ID] : ids;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
|
-
export function resolveDefaultFeishuAccountId(
|
|
46
|
-
|
|
67
|
+
export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
|
|
68
|
+
const ids = listFeishuAccountIds(cfg);
|
|
69
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
export function listEnabledFeishuAccounts(cfg: ClawdbotConfig): ResolvedFeishuAccount[] {
|
package/src/bot.ts
CHANGED
|
@@ -490,8 +490,9 @@ export async function handleFeishuMessage(params: {
|
|
|
490
490
|
botOpenId?: string;
|
|
491
491
|
runtime?: RuntimeEnv;
|
|
492
492
|
chatHistories?: Map<string, HistoryEntry[]>;
|
|
493
|
+
accountId?: string;
|
|
493
494
|
}): Promise<void> {
|
|
494
|
-
const { cfg, event, botOpenId, runtime, chatHistories } = params;
|
|
495
|
+
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
|
|
495
496
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
496
497
|
const log = runtime?.log ?? console.log;
|
|
497
498
|
const error = runtime?.error ?? console.error;
|
|
@@ -624,6 +625,7 @@ export async function handleFeishuMessage(params: {
|
|
|
624
625
|
const route = core.channel.routing.resolveAgentRoute({
|
|
625
626
|
cfg: routingCfg,
|
|
626
627
|
channel: "feishu",
|
|
628
|
+
accountId,
|
|
627
629
|
peer: {
|
|
628
630
|
kind: isGroup ? "group" : "dm",
|
|
629
631
|
id: isGroup ? ctx.chatId : ctx.senderOpenId,
|
package/src/channel.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
|
4
|
-
import { resolveFeishuAccount, resolveFeishuCredentials } from "./accounts.js";
|
|
4
|
+
import { resolveFeishuAccount, resolveFeishuCredentials, listFeishuAccountIds, mergeFeishuAccountConfig } from "./accounts.js";
|
|
5
5
|
import { feishuOutbound } from "./outbound.js";
|
|
6
6
|
import { probeFeishu } from "./probe.js";
|
|
7
7
|
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
|
@@ -87,24 +87,78 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
87
87
|
chunkMode: { type: "string", enum: ["length", "newline"] },
|
|
88
88
|
mediaMaxMb: { type: "number", minimum: 0 },
|
|
89
89
|
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
|
90
|
+
accounts: {
|
|
91
|
+
type: "object",
|
|
92
|
+
additionalProperties: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
enabled: { type: "boolean" },
|
|
96
|
+
name: { type: "string" },
|
|
97
|
+
appId: { type: "string" },
|
|
98
|
+
appSecret: { type: "string" },
|
|
99
|
+
encryptKey: { type: "string" },
|
|
100
|
+
verificationToken: { type: "string" },
|
|
101
|
+
domain: { type: "string", enum: ["feishu", "lark"] },
|
|
102
|
+
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
103
|
+
webhookPath: { type: "string" },
|
|
104
|
+
webhookPort: { type: "integer", minimum: 1 },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
90
108
|
},
|
|
91
109
|
},
|
|
92
110
|
},
|
|
93
111
|
config: {
|
|
94
|
-
listAccountIds: () =>
|
|
95
|
-
resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
|
|
96
|
-
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
97
|
-
setAccountEnabled: ({ cfg, enabled }) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...cfg
|
|
103
|
-
|
|
112
|
+
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
|
113
|
+
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
|
114
|
+
defaultAccountId: (cfg) => listFeishuAccountIds(cfg)[0] ?? DEFAULT_ACCOUNT_ID,
|
|
115
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
116
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
117
|
+
// Per-account enable/disable when using named accounts
|
|
118
|
+
if (accountId && accountId !== DEFAULT_ACCOUNT_ID && feishuCfg?.accounts?.[accountId]) {
|
|
119
|
+
return {
|
|
120
|
+
...cfg,
|
|
121
|
+
channels: {
|
|
122
|
+
...cfg.channels,
|
|
123
|
+
feishu: {
|
|
124
|
+
...feishuCfg,
|
|
125
|
+
accounts: {
|
|
126
|
+
...feishuCfg.accounts,
|
|
127
|
+
[accountId]: { ...feishuCfg.accounts[accountId], enabled },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...cfg,
|
|
135
|
+
channels: {
|
|
136
|
+
...cfg.channels,
|
|
137
|
+
feishu: {
|
|
138
|
+
...cfg.channels?.feishu,
|
|
139
|
+
enabled,
|
|
140
|
+
},
|
|
104
141
|
},
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
deleteAccount: ({ cfg }) => {
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
145
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
146
|
+
// Delete a named account
|
|
147
|
+
if (accountId && accountId !== DEFAULT_ACCOUNT_ID && feishuCfg?.accounts) {
|
|
148
|
+
const nextAccounts = { ...feishuCfg.accounts };
|
|
149
|
+
delete nextAccounts[accountId];
|
|
150
|
+
return {
|
|
151
|
+
...cfg,
|
|
152
|
+
channels: {
|
|
153
|
+
...cfg.channels,
|
|
154
|
+
feishu: {
|
|
155
|
+
...feishuCfg,
|
|
156
|
+
accounts: Object.keys(nextAccounts).length > 0 ? nextAccounts : undefined,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Delete the whole channel (default account)
|
|
108
162
|
const next = { ...cfg } as ClawdbotConfig;
|
|
109
163
|
const nextChannels = { ...cfg.channels };
|
|
110
164
|
delete (nextChannels as Record<string, unknown>).feishu;
|
|
@@ -115,8 +169,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
115
169
|
}
|
|
116
170
|
return next;
|
|
117
171
|
},
|
|
118
|
-
isConfigured: (
|
|
119
|
-
Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)),
|
|
172
|
+
isConfigured: (account) => account.configured,
|
|
120
173
|
describeAccount: (account) => ({
|
|
121
174
|
accountId: account.accountId,
|
|
122
175
|
enabled: account.enabled,
|
|
@@ -142,7 +195,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
142
195
|
},
|
|
143
196
|
},
|
|
144
197
|
setup: {
|
|
145
|
-
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
198
|
+
resolveAccountId: (cfg) => listFeishuAccountIds(cfg)[0] ?? DEFAULT_ACCOUNT_ID,
|
|
146
199
|
applyAccountConfig: ({ cfg }) => ({
|
|
147
200
|
...cfg,
|
|
148
201
|
channels: {
|
|
@@ -193,8 +246,11 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
193
246
|
probe: snapshot.probe,
|
|
194
247
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
195
248
|
}),
|
|
196
|
-
probeAccount: async ({ cfg }) =>
|
|
197
|
-
|
|
249
|
+
probeAccount: async ({ cfg, accountId }) => {
|
|
250
|
+
const baseCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
251
|
+
const merged = baseCfg ? mergeFeishuAccountConfig(baseCfg, accountId ?? DEFAULT_ACCOUNT_ID) : baseCfg;
|
|
252
|
+
return probeFeishu(merged);
|
|
253
|
+
},
|
|
198
254
|
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
199
255
|
accountId: account.accountId,
|
|
200
256
|
enabled: account.enabled,
|
|
@@ -210,12 +266,23 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
210
266
|
gateway: {
|
|
211
267
|
startAccount: async (ctx) => {
|
|
212
268
|
const { monitorFeishuProvider } = await import("./monitor.js");
|
|
213
|
-
const
|
|
214
|
-
const
|
|
269
|
+
const baseCfg = ctx.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
270
|
+
const merged = baseCfg ? mergeFeishuAccountConfig(baseCfg, ctx.accountId) : baseCfg;
|
|
271
|
+
const port = merged?.webhookPort ?? null;
|
|
272
|
+
|
|
273
|
+
// Inject merged account config so all downstream reads of cfg.channels.feishu get the right values
|
|
274
|
+
const accountCfg: ClawdbotConfig = {
|
|
275
|
+
...ctx.cfg,
|
|
276
|
+
channels: {
|
|
277
|
+
...ctx.cfg.channels,
|
|
278
|
+
feishu: merged,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
215
282
|
ctx.setStatus({ accountId: ctx.accountId, port });
|
|
216
|
-
ctx.log?.info(`starting feishu provider (mode: ${
|
|
283
|
+
ctx.log?.info(`starting feishu provider (account: ${ctx.accountId}, mode: ${merged?.connectionMode ?? "websocket"})`);
|
|
217
284
|
return monitorFeishuProvider({
|
|
218
|
-
config:
|
|
285
|
+
config: accountCfg,
|
|
219
286
|
runtime: ctx.runtime,
|
|
220
287
|
abortSignal: ctx.abortSignal,
|
|
221
288
|
accountId: ctx.accountId,
|
package/src/client.ts
CHANGED
|
@@ -2,8 +2,8 @@ import * as Lark from "@larksuiteoapi/node-sdk";
|
|
|
2
2
|
import type { FeishuConfig, FeishuDomain } from "./types.js";
|
|
3
3
|
import { resolveFeishuCredentials } from "./accounts.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type CachedEntry = { client: Lark.Client; appId: string; appSecret: string; domain: FeishuDomain };
|
|
6
|
+
const clientCache = new Map<string, CachedEntry>();
|
|
7
7
|
|
|
8
8
|
function resolveDomain(domain: FeishuDomain) {
|
|
9
9
|
return domain === "lark" ? Lark.Domain.Lark : Lark.Domain.Feishu;
|
|
@@ -15,14 +15,10 @@ export function createFeishuClient(cfg: FeishuConfig): Lark.Client {
|
|
|
15
15
|
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
cachedConfig.appSecret === creds.appSecret &&
|
|
23
|
-
cachedConfig.domain === creds.domain
|
|
24
|
-
) {
|
|
25
|
-
return cachedClient;
|
|
18
|
+
const cacheKey = `${creds.appId}:${creds.domain}`;
|
|
19
|
+
const cached = clientCache.get(cacheKey);
|
|
20
|
+
if (cached && cached.appSecret === creds.appSecret) {
|
|
21
|
+
return cached.client;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
const client = new Lark.Client({
|
|
@@ -32,8 +28,7 @@ export function createFeishuClient(cfg: FeishuConfig): Lark.Client {
|
|
|
32
28
|
domain: resolveDomain(creds.domain),
|
|
33
29
|
});
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
cachedConfig = { appId: creds.appId, appSecret: creds.appSecret, domain: creds.domain };
|
|
31
|
+
clientCache.set(cacheKey, { client, appId: creds.appId, appSecret: creds.appSecret, domain: creds.domain });
|
|
37
32
|
|
|
38
33
|
return client;
|
|
39
34
|
}
|
|
@@ -60,7 +55,10 @@ export function createEventDispatcher(cfg: FeishuConfig): Lark.EventDispatcher {
|
|
|
60
55
|
});
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
export function clearClientCache() {
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
export function clearClientCache(accountKey?: string) {
|
|
59
|
+
if (accountKey) {
|
|
60
|
+
clientCache.delete(accountKey);
|
|
61
|
+
} else {
|
|
62
|
+
clientCache.clear();
|
|
63
|
+
}
|
|
66
64
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -79,6 +79,21 @@ export const FeishuGroupSchema = z
|
|
|
79
79
|
})
|
|
80
80
|
.strict();
|
|
81
81
|
|
|
82
|
+
export const FeishuAccountOverrideSchema = z
|
|
83
|
+
.object({
|
|
84
|
+
enabled: z.boolean().optional(),
|
|
85
|
+
name: z.string().optional(),
|
|
86
|
+
appId: z.string().optional(),
|
|
87
|
+
appSecret: z.string().optional(),
|
|
88
|
+
encryptKey: z.string().optional(),
|
|
89
|
+
verificationToken: z.string().optional(),
|
|
90
|
+
domain: FeishuDomainSchema.optional(),
|
|
91
|
+
connectionMode: FeishuConnectionModeSchema.optional(),
|
|
92
|
+
webhookPath: z.string().optional(),
|
|
93
|
+
webhookPort: z.number().int().positive().optional(),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
|
|
82
97
|
export const FeishuConfigSchema = z
|
|
83
98
|
.object({
|
|
84
99
|
enabled: z.boolean().optional(),
|
|
@@ -111,6 +126,7 @@ export const FeishuConfigSchema = z
|
|
|
111
126
|
replyToMode: ReplyToModeSchema, // "all" = always thread (default), "off" = main chat unless already in thread
|
|
112
127
|
replyToModeByChatType: ReplyToModeByChatTypeSchema, // per-chat-type overrides for replyToMode
|
|
113
128
|
dmScope: DmScopeSchema, // override session.dmScope for this channel (default: per-channel-peer)
|
|
129
|
+
accounts: z.record(z.string(), FeishuAccountOverrideSchema.optional()).optional(),
|
|
114
130
|
})
|
|
115
131
|
.strict()
|
|
116
132
|
.superRefine((value, ctx) => {
|
package/src/monitor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
3
3
|
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
4
5
|
import type { FeishuConfig } from "./types.js";
|
|
5
6
|
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
|
6
7
|
import { resolveFeishuCredentials } from "./accounts.js";
|
|
@@ -14,9 +15,35 @@ export type MonitorFeishuOpts = {
|
|
|
14
15
|
accountId?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const wsClients = new Map<string, Lark.WSClient>();
|
|
19
|
+
const httpServers = new Map<string, http.Server>();
|
|
20
|
+
const botOpenIds = new Map<string, string>();
|
|
21
|
+
|
|
22
|
+
// --- Message dedup ---
|
|
23
|
+
// Feishu may deliver the same event multiple times (webhook retries, websocket reconnects).
|
|
24
|
+
// Track seen message_ids for a short window to skip duplicates.
|
|
25
|
+
const DEDUP_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
26
|
+
const seenMessageIds = new Map<string, number>();
|
|
27
|
+
|
|
28
|
+
function isDuplicateMessage(messageId: string | undefined): boolean {
|
|
29
|
+
if (!messageId) return false;
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
|
|
32
|
+
// Lazy cleanup: prune expired entries when map grows
|
|
33
|
+
if (seenMessageIds.size > 200) {
|
|
34
|
+
for (const [id, ts] of seenMessageIds) {
|
|
35
|
+
if (now - ts > DEDUP_TTL_MS) seenMessageIds.delete(id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const seenAt = seenMessageIds.get(messageId);
|
|
40
|
+
if (seenAt !== undefined) {
|
|
41
|
+
if (now - seenAt <= DEDUP_TTL_MS) return true;
|
|
42
|
+
// Expired — treat as new message
|
|
43
|
+
}
|
|
44
|
+
seenMessageIds.set(messageId, now);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
20
47
|
|
|
21
48
|
async function fetchBotOpenId(cfg: FeishuConfig): Promise<string | undefined> {
|
|
22
49
|
try {
|
|
@@ -33,6 +60,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
33
60
|
throw new Error("Config is required for Feishu monitor");
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
const accountId = opts.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
36
64
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
37
65
|
const creds = resolveFeishuCredentials(feishuCfg);
|
|
38
66
|
if (!creds) {
|
|
@@ -43,17 +71,22 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
43
71
|
const error = opts.runtime?.error ?? console.error;
|
|
44
72
|
|
|
45
73
|
if (feishuCfg) {
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
const openId = await fetchBotOpenId(feishuCfg);
|
|
75
|
+
if (openId) {
|
|
76
|
+
botOpenIds.set(accountId, openId);
|
|
77
|
+
} else {
|
|
78
|
+
botOpenIds.delete(accountId);
|
|
79
|
+
}
|
|
80
|
+
log(`feishu[${accountId}]: bot open_id resolved: ${openId ?? "unknown"}`);
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
const connectionMode = feishuCfg?.connectionMode ?? "websocket";
|
|
51
84
|
|
|
52
85
|
if (connectionMode === "websocket") {
|
|
53
|
-
return monitorWebSocket({ cfg, feishuCfg: feishuCfg!, runtime: opts.runtime, abortSignal: opts.abortSignal });
|
|
86
|
+
return monitorWebSocket({ cfg, feishuCfg: feishuCfg!, runtime: opts.runtime, abortSignal: opts.abortSignal, accountId });
|
|
54
87
|
}
|
|
55
88
|
|
|
56
|
-
return monitorWebhook({ cfg, feishuCfg: feishuCfg!, runtime: opts.runtime, abortSignal: opts.abortSignal });
|
|
89
|
+
return monitorWebhook({ cfg, feishuCfg: feishuCfg!, runtime: opts.runtime, abortSignal: opts.abortSignal, accountId });
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
async function monitorWebSocket(params: {
|
|
@@ -61,17 +94,19 @@ async function monitorWebSocket(params: {
|
|
|
61
94
|
feishuCfg: FeishuConfig;
|
|
62
95
|
runtime?: RuntimeEnv;
|
|
63
96
|
abortSignal?: AbortSignal;
|
|
97
|
+
accountId: string;
|
|
64
98
|
}): Promise<void> {
|
|
65
|
-
const { cfg, feishuCfg, runtime, abortSignal } = params;
|
|
99
|
+
const { cfg, feishuCfg, runtime, abortSignal, accountId } = params;
|
|
66
100
|
const log = runtime?.log ?? console.log;
|
|
67
101
|
const error = runtime?.error ?? console.error;
|
|
68
102
|
|
|
69
|
-
log(
|
|
103
|
+
log(`feishu[${accountId}]: starting WebSocket connection...`);
|
|
70
104
|
|
|
71
105
|
const wsClient = createFeishuWSClient(feishuCfg);
|
|
72
|
-
|
|
106
|
+
wsClients.set(accountId, wsClient);
|
|
73
107
|
|
|
74
108
|
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
109
|
+
const botOpenId = botOpenIds.get(accountId);
|
|
75
110
|
|
|
76
111
|
const eventDispatcher = createEventDispatcher(feishuCfg);
|
|
77
112
|
|
|
@@ -79,15 +114,20 @@ async function monitorWebSocket(params: {
|
|
|
79
114
|
"im.message.receive_v1": async (data) => {
|
|
80
115
|
try {
|
|
81
116
|
const event = data as unknown as FeishuMessageEvent;
|
|
117
|
+
if (isDuplicateMessage(event.message?.message_id)) {
|
|
118
|
+
log(`feishu[${accountId}]: skipping duplicate message ${event.message.message_id}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
82
121
|
await handleFeishuMessage({
|
|
83
122
|
cfg,
|
|
84
123
|
event,
|
|
85
124
|
botOpenId,
|
|
86
125
|
runtime,
|
|
87
126
|
chatHistories,
|
|
127
|
+
accountId,
|
|
88
128
|
});
|
|
89
129
|
} catch (err) {
|
|
90
|
-
error(`feishu: error handling message event: ${String(err)}`);
|
|
130
|
+
error(`feishu[${accountId}]: error handling message event: ${String(err)}`);
|
|
91
131
|
}
|
|
92
132
|
},
|
|
93
133
|
"im.message.message_read_v1": async () => {
|
|
@@ -96,30 +136,30 @@ async function monitorWebSocket(params: {
|
|
|
96
136
|
"im.chat.member.bot.added_v1": async (data) => {
|
|
97
137
|
try {
|
|
98
138
|
const event = data as unknown as FeishuBotAddedEvent;
|
|
99
|
-
log(`feishu: bot added to chat ${event.chat_id}`);
|
|
139
|
+
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
|
100
140
|
} catch (err) {
|
|
101
|
-
error(`feishu: error handling bot added event: ${String(err)}`);
|
|
141
|
+
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
|
102
142
|
}
|
|
103
143
|
},
|
|
104
144
|
"im.chat.member.bot.deleted_v1": async (data) => {
|
|
105
145
|
try {
|
|
106
146
|
const event = data as unknown as { chat_id: string };
|
|
107
|
-
log(`feishu: bot removed from chat ${event.chat_id}`);
|
|
147
|
+
log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
|
|
108
148
|
} catch (err) {
|
|
109
|
-
error(`feishu: error handling bot removed event: ${String(err)}`);
|
|
149
|
+
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
|
110
150
|
}
|
|
111
151
|
},
|
|
112
152
|
});
|
|
113
153
|
|
|
114
154
|
return new Promise((resolve, reject) => {
|
|
115
155
|
const cleanup = () => {
|
|
116
|
-
if (
|
|
117
|
-
|
|
156
|
+
if (wsClients.get(accountId) === wsClient) {
|
|
157
|
+
wsClients.delete(accountId);
|
|
118
158
|
}
|
|
119
159
|
};
|
|
120
160
|
|
|
121
161
|
const handleAbort = () => {
|
|
122
|
-
log(
|
|
162
|
+
log(`feishu[${accountId}]: abort signal received, stopping WebSocket client`);
|
|
123
163
|
cleanup();
|
|
124
164
|
resolve();
|
|
125
165
|
};
|
|
@@ -137,7 +177,7 @@ async function monitorWebSocket(params: {
|
|
|
137
177
|
eventDispatcher,
|
|
138
178
|
});
|
|
139
179
|
|
|
140
|
-
log(
|
|
180
|
+
log(`feishu[${accountId}]: WebSocket client started`);
|
|
141
181
|
} catch (err) {
|
|
142
182
|
cleanup();
|
|
143
183
|
abortSignal?.removeEventListener("abort", handleAbort);
|
|
@@ -146,13 +186,22 @@ async function monitorWebSocket(params: {
|
|
|
146
186
|
});
|
|
147
187
|
}
|
|
148
188
|
|
|
149
|
-
export function stopFeishuMonitor(): void {
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
189
|
+
export function stopFeishuMonitor(accountId?: string): void {
|
|
190
|
+
if (accountId) {
|
|
191
|
+
wsClients.delete(accountId);
|
|
192
|
+
botOpenIds.delete(accountId);
|
|
193
|
+
const server = httpServers.get(accountId);
|
|
194
|
+
if (server) {
|
|
195
|
+
server.close();
|
|
196
|
+
httpServers.delete(accountId);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
wsClients.clear();
|
|
200
|
+
botOpenIds.clear();
|
|
201
|
+
for (const server of httpServers.values()) {
|
|
202
|
+
server.close();
|
|
203
|
+
}
|
|
204
|
+
httpServers.clear();
|
|
156
205
|
}
|
|
157
206
|
}
|
|
158
207
|
|
|
@@ -161,32 +210,39 @@ async function monitorWebhook(params: {
|
|
|
161
210
|
feishuCfg: FeishuConfig;
|
|
162
211
|
runtime?: RuntimeEnv;
|
|
163
212
|
abortSignal?: AbortSignal;
|
|
213
|
+
accountId: string;
|
|
164
214
|
}): Promise<void> {
|
|
165
|
-
const { cfg, feishuCfg, runtime, abortSignal } = params;
|
|
215
|
+
const { cfg, feishuCfg, runtime, abortSignal, accountId } = params;
|
|
166
216
|
const log = runtime?.log ?? console.log;
|
|
167
217
|
const error = runtime?.error ?? console.error;
|
|
168
218
|
|
|
169
219
|
const webhookPath = feishuCfg.webhookPath ?? "/feishu/events";
|
|
170
220
|
const webhookPort = feishuCfg.webhookPort ?? 3000;
|
|
171
221
|
|
|
172
|
-
log(`feishu: starting webhook server on port ${webhookPort}, path ${webhookPath}...`);
|
|
222
|
+
log(`feishu[${accountId}]: starting webhook server on port ${webhookPort}, path ${webhookPath}...`);
|
|
173
223
|
|
|
174
224
|
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
225
|
+
const botOpenId = botOpenIds.get(accountId);
|
|
175
226
|
const eventDispatcher = createEventDispatcher(feishuCfg);
|
|
176
227
|
|
|
177
228
|
eventDispatcher.register({
|
|
178
229
|
"im.message.receive_v1": async (data) => {
|
|
179
230
|
try {
|
|
180
231
|
const event = data as unknown as FeishuMessageEvent;
|
|
232
|
+
if (isDuplicateMessage(event.message?.message_id)) {
|
|
233
|
+
log(`feishu[${accountId}]: skipping duplicate message ${event.message.message_id}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
181
236
|
await handleFeishuMessage({
|
|
182
237
|
cfg,
|
|
183
238
|
event,
|
|
184
239
|
botOpenId,
|
|
185
240
|
runtime,
|
|
186
241
|
chatHistories,
|
|
242
|
+
accountId,
|
|
187
243
|
});
|
|
188
244
|
} catch (err) {
|
|
189
|
-
error(`feishu: error handling message event: ${String(err)}`);
|
|
245
|
+
error(`feishu[${accountId}]: error handling message event: ${String(err)}`);
|
|
190
246
|
}
|
|
191
247
|
},
|
|
192
248
|
"im.message.message_read_v1": async () => {
|
|
@@ -195,33 +251,33 @@ async function monitorWebhook(params: {
|
|
|
195
251
|
"im.chat.member.bot.added_v1": async (data) => {
|
|
196
252
|
try {
|
|
197
253
|
const event = data as unknown as FeishuBotAddedEvent;
|
|
198
|
-
log(`feishu: bot added to chat ${event.chat_id}`);
|
|
254
|
+
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
|
199
255
|
} catch (err) {
|
|
200
|
-
error(`feishu: error handling bot added event: ${String(err)}`);
|
|
256
|
+
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
|
201
257
|
}
|
|
202
258
|
},
|
|
203
259
|
"im.chat.member.bot.deleted_v1": async (data) => {
|
|
204
260
|
try {
|
|
205
261
|
const event = data as unknown as { chat_id: string };
|
|
206
|
-
log(`feishu: bot removed from chat ${event.chat_id}`);
|
|
262
|
+
log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
|
|
207
263
|
} catch (err) {
|
|
208
|
-
error(`feishu: error handling bot removed event: ${String(err)}`);
|
|
264
|
+
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
|
209
265
|
}
|
|
210
266
|
},
|
|
211
267
|
});
|
|
212
268
|
|
|
213
269
|
return new Promise((resolve, reject) => {
|
|
214
270
|
const server = http.createServer();
|
|
215
|
-
|
|
271
|
+
httpServers.set(accountId, server);
|
|
216
272
|
|
|
217
273
|
const cleanup = () => {
|
|
218
|
-
if (
|
|
219
|
-
|
|
274
|
+
if (httpServers.get(accountId) === server) {
|
|
275
|
+
httpServers.delete(accountId);
|
|
220
276
|
}
|
|
221
277
|
};
|
|
222
278
|
|
|
223
279
|
const handleAbort = () => {
|
|
224
|
-
log(
|
|
280
|
+
log(`feishu[${accountId}]: abort signal received, stopping webhook server`);
|
|
225
281
|
server.close(() => {
|
|
226
282
|
cleanup();
|
|
227
283
|
resolve();
|
|
@@ -239,7 +295,7 @@ async function monitorWebhook(params: {
|
|
|
239
295
|
server.on("error", (err) => {
|
|
240
296
|
cleanup();
|
|
241
297
|
abortSignal?.removeEventListener("abort", handleAbort);
|
|
242
|
-
error(`feishu: webhook server error: ${String(err)}`);
|
|
298
|
+
error(`feishu[${accountId}]: webhook server error: ${String(err)}`);
|
|
243
299
|
reject(err);
|
|
244
300
|
});
|
|
245
301
|
|
|
@@ -249,7 +305,7 @@ async function monitorWebhook(params: {
|
|
|
249
305
|
}));
|
|
250
306
|
|
|
251
307
|
server.listen(webhookPort, () => {
|
|
252
|
-
log(`feishu: webhook server started on port ${webhookPort}`);
|
|
308
|
+
log(`feishu[${accountId}]: webhook server started on port ${webhookPort}`);
|
|
253
309
|
});
|
|
254
310
|
});
|
|
255
311
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { FeishuConfigSchema, FeishuGroupSchema, z } from "./config-schema.js";
|
|
1
|
+
import type { FeishuConfigSchema, FeishuGroupSchema, FeishuAccountOverrideSchema, z } from "./config-schema.js";
|
|
2
2
|
import type { MentionTarget } from "./mention.js";
|
|
3
3
|
|
|
4
4
|
export type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
|
|
5
5
|
export type FeishuGroupConfig = z.infer<typeof FeishuGroupSchema>;
|
|
6
|
+
export type FeishuAccountOverride = z.infer<typeof FeishuAccountOverrideSchema>;
|
|
6
7
|
|
|
7
8
|
export type FeishuDomain = "feishu" | "lark";
|
|
8
9
|
export type FeishuConnectionMode = "websocket" | "webhook";
|