@zzzpyyy/dsh-bot-feishu 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +92 -2
- package/compatibility.json +12 -0
- package/cordis.patch.example.yml +14 -0
- package/cordis.patch.yml +3 -0
- package/domain-OHZklmI3.js +520 -0
- package/feishu.js +925 -0
- package/package.json +52 -4
package/feishu.js
ADDED
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
import { r as InfoFlowBridge, t as bridgeDomain } from "./domain-OHZklmI3.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
5
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { defineDomain } from "@deepseek-ai/dsh-storage-domain";
|
|
8
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
9
|
+
//#region lib/types/feishu/settings.js
|
|
10
|
+
/** Native versioned settings store; secret values belong to DSH credentials. */
|
|
11
|
+
const botId = z.string().transform(brandString);
|
|
12
|
+
const settingsDomain = defineDomain({
|
|
13
|
+
name: "bot_feishu_settings",
|
|
14
|
+
version: 1,
|
|
15
|
+
tables: {},
|
|
16
|
+
global: {
|
|
17
|
+
schema: z.object({
|
|
18
|
+
bindings: z.array(z.object({
|
|
19
|
+
botId,
|
|
20
|
+
appId: z.string(),
|
|
21
|
+
botOpenId: z.string(),
|
|
22
|
+
robotName: z.string(),
|
|
23
|
+
brand: z.enum(["feishu", "lark"]),
|
|
24
|
+
enabled: z.boolean(),
|
|
25
|
+
secretRef: z.string(),
|
|
26
|
+
allowUsers: z.array(z.string()),
|
|
27
|
+
allowGroups: z.array(z.string()),
|
|
28
|
+
admins: z.array(z.string())
|
|
29
|
+
})),
|
|
30
|
+
groups: z.array(z.object({
|
|
31
|
+
requestId: z.string(),
|
|
32
|
+
title: z.string(),
|
|
33
|
+
botIds: z.array(botId),
|
|
34
|
+
creatorBot: botId,
|
|
35
|
+
owner: z.string(),
|
|
36
|
+
createdAt: z.number(),
|
|
37
|
+
chatId: z.string().nullable(),
|
|
38
|
+
roomId: z.string().transform(brandString).nullable(),
|
|
39
|
+
status: z.enum([
|
|
40
|
+
"creating",
|
|
41
|
+
"uncertain",
|
|
42
|
+
"failed",
|
|
43
|
+
"linking",
|
|
44
|
+
"ready"
|
|
45
|
+
]),
|
|
46
|
+
error: z.string().nullable()
|
|
47
|
+
})).default([]),
|
|
48
|
+
registrations: z.array(z.object({
|
|
49
|
+
botId,
|
|
50
|
+
attemptId: z.string(),
|
|
51
|
+
status: z.enum([
|
|
52
|
+
"starting",
|
|
53
|
+
"waiting",
|
|
54
|
+
"saving",
|
|
55
|
+
"complete",
|
|
56
|
+
"canceled",
|
|
57
|
+
"expired",
|
|
58
|
+
"error",
|
|
59
|
+
"interrupted"
|
|
60
|
+
]),
|
|
61
|
+
message: z.string().nullable()
|
|
62
|
+
}))
|
|
63
|
+
}),
|
|
64
|
+
initial: {
|
|
65
|
+
bindings: [],
|
|
66
|
+
registrations: [],
|
|
67
|
+
groups: []
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region lib/types/feishu/sdk.js
|
|
73
|
+
/** Pinned official Feishu SDK, with lifecycle and wire validation at this seam. */
|
|
74
|
+
const logger = {
|
|
75
|
+
error() {},
|
|
76
|
+
warn() {},
|
|
77
|
+
info() {},
|
|
78
|
+
debug() {},
|
|
79
|
+
trace() {}
|
|
80
|
+
};
|
|
81
|
+
const domain = (brand) => brand === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn";
|
|
82
|
+
function loadSdk(timeoutMs) {
|
|
83
|
+
const require = createRequire(import.meta.url);
|
|
84
|
+
const metadata = require("@larksuiteoapi/node-sdk/package.json");
|
|
85
|
+
if (typeof metadata !== "object" || !metadata || !("version" in metadata) || metadata.version !== "1.75.0") throw new Error("Install @larksuiteoapi/node-sdk@1.75.0 in the Feishu companion");
|
|
86
|
+
const sdk = require("@larksuiteoapi/node-sdk");
|
|
87
|
+
if (typeof sdk !== "object" || !sdk || !("registerApp" in sdk) || typeof sdk.registerApp !== "function") throw new Error("Feishu SDK registration support is missing");
|
|
88
|
+
const loaded = sdk;
|
|
89
|
+
if (!loaded.defaultHttpInstance) throw new Error("Feishu SDK HTTP client is missing");
|
|
90
|
+
loaded.defaultHttpInstance.defaults.timeout = timeoutMs;
|
|
91
|
+
return loaded;
|
|
92
|
+
}
|
|
93
|
+
const record = z.looseObject({});
|
|
94
|
+
function object(value) {
|
|
95
|
+
return record.safeParse(value).data ?? {};
|
|
96
|
+
}
|
|
97
|
+
function text(value) {
|
|
98
|
+
return typeof value === "string" ? value : "";
|
|
99
|
+
}
|
|
100
|
+
function checked(value) {
|
|
101
|
+
const result = object(value);
|
|
102
|
+
if (result.code !== 0) throw new Error("飞书接口未成功,请检查机器人权限、发布状态和网络");
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
async function identity(sdk, appId, appSecret, brand, timeoutMs) {
|
|
106
|
+
const result = checked(await new sdk.Client({
|
|
107
|
+
appId,
|
|
108
|
+
appSecret,
|
|
109
|
+
domain: domain(brand),
|
|
110
|
+
logger
|
|
111
|
+
}).request({
|
|
112
|
+
method: "GET",
|
|
113
|
+
url: "/open-apis/bot/v3/info",
|
|
114
|
+
timeout: timeoutMs
|
|
115
|
+
}));
|
|
116
|
+
const bot = object(result.bot ?? object(result.data).bot);
|
|
117
|
+
const openId = text(bot.open_id), name = text(bot.app_name);
|
|
118
|
+
if (!/^ou_[\w-]+$/.test(openId)) throw new Error("未查询到应用机器人,请在飞书启用机器人能力并发布应用");
|
|
119
|
+
return {
|
|
120
|
+
openId,
|
|
121
|
+
name: name || appId
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Safe, classified errors let the manager distinguish API rejection from an unknown outcome. */
|
|
125
|
+
var GroupCreateError = class extends Error {};
|
|
126
|
+
/** Create a private group; Feishu deduplicates the same uuid + owner for ten hours. */
|
|
127
|
+
async function createGroup(sdk, appId, appSecret, brand, timeoutMs, options) {
|
|
128
|
+
const client = new sdk.Client({
|
|
129
|
+
appId,
|
|
130
|
+
appSecret,
|
|
131
|
+
domain: domain(brand),
|
|
132
|
+
logger
|
|
133
|
+
});
|
|
134
|
+
let response;
|
|
135
|
+
try {
|
|
136
|
+
response = await client.request({
|
|
137
|
+
method: "POST",
|
|
138
|
+
url: "/open-apis/im/v1/chats",
|
|
139
|
+
timeout: timeoutMs,
|
|
140
|
+
params: {
|
|
141
|
+
user_id_type: "open_id",
|
|
142
|
+
set_bot_manager: false,
|
|
143
|
+
uuid: options.requestId
|
|
144
|
+
},
|
|
145
|
+
data: {
|
|
146
|
+
name: options.title,
|
|
147
|
+
owner_id: options.owner,
|
|
148
|
+
user_id_list: [options.owner],
|
|
149
|
+
bot_id_list: options.botAppIds,
|
|
150
|
+
chat_mode: "group",
|
|
151
|
+
chat_type: "private"
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
} catch (cause) {
|
|
155
|
+
const wire = object(object(cause).response);
|
|
156
|
+
if (typeof object(wire.data).code !== "number") throw new Error("建群结果待确认,请使用同一请求继续,勿重复新建");
|
|
157
|
+
response = wire.data;
|
|
158
|
+
}
|
|
159
|
+
const result = object(response);
|
|
160
|
+
if (typeof result.code === "number" && result.code !== 0) {
|
|
161
|
+
if (result.code === 99991672) throw new GroupCreateError("建群机器人缺少 im:chat:create 权限,请在飞书开放平台开通后继续");
|
|
162
|
+
throw new GroupCreateError(`飞书拒绝建群(错误码 ${result.code}),请检查群主、机器人是否在同一企业及应用可用范围`);
|
|
163
|
+
}
|
|
164
|
+
const chatId = text(object(result.data).chat_id);
|
|
165
|
+
if (result.code !== 0 || !/^oc_[\w-]+$/.test(chatId)) throw new Error("未取得群 ID,请使用同一请求继续,勿重复新建");
|
|
166
|
+
return chatId;
|
|
167
|
+
}
|
|
168
|
+
/** Official user-confirmed registration with messaging and group-creation permissions. */
|
|
169
|
+
function register(sdk, options) {
|
|
170
|
+
return sdk.registerApp({
|
|
171
|
+
signal: options.signal,
|
|
172
|
+
source: "dsh-bot",
|
|
173
|
+
createOnly: true,
|
|
174
|
+
onQRCodeReady: options.qr,
|
|
175
|
+
appPreset: {
|
|
176
|
+
name: options.name,
|
|
177
|
+
desc: "DSH Bot 独立 AI 助手"
|
|
178
|
+
},
|
|
179
|
+
addons: {
|
|
180
|
+
preset: false,
|
|
181
|
+
scopes: { tenant: [
|
|
182
|
+
"im:message.p2p_msg:readonly",
|
|
183
|
+
"im:message.group_at_msg:readonly",
|
|
184
|
+
"im:message:send_as_bot",
|
|
185
|
+
"im:chat:create"
|
|
186
|
+
] },
|
|
187
|
+
events: { items: { tenant: ["im.message.receive_v1"] } }
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function qrImage(url) {
|
|
192
|
+
const parsed = new URL(url);
|
|
193
|
+
if (parsed.protocol !== "https:" || !["feishu.cn", "larksuite.com"].some((host) => parsed.hostname === host || parsed.hostname.endsWith(`.${host}`))) throw new Error("飞书返回了无法识别的授权地址");
|
|
194
|
+
return createRequire(import.meta.url)("qrcode").toDataURL(url, {
|
|
195
|
+
width: 280,
|
|
196
|
+
margin: 2,
|
|
197
|
+
errorCorrectionLevel: "M"
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** Text-only ingress, explicit human sender and exact bot mentions. */
|
|
201
|
+
function decodeMessage(event, appId, botOpenId) {
|
|
202
|
+
const body = object(event), sender = object(body.sender), msg = object(body.message);
|
|
203
|
+
const senderId = text(object(sender.sender_id).open_id), messageId = text(msg.message_id), chatId = text(msg.chat_id);
|
|
204
|
+
if (sender.sender_type !== "user" || !senderId || senderId === botOpenId || !messageId || !chatId || msg.message_type !== "text") return null;
|
|
205
|
+
if (msg.chat_type !== "p2p" && msg.chat_type !== "group") return null;
|
|
206
|
+
const mentions = Array.isArray(msg.mentions) ? msg.mentions.map(object) : [];
|
|
207
|
+
const ownMentions = mentions.filter((mention) => object(mention.id).open_id === botOpenId);
|
|
208
|
+
if (msg.chat_type === "group" && !ownMentions.length) return null;
|
|
209
|
+
let content;
|
|
210
|
+
try {
|
|
211
|
+
content = text(object(JSON.parse(text(msg.content))).text);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
for (const mention of msg.chat_type === "group" ? mentions : ownMentions) {
|
|
216
|
+
const key = text(mention.key);
|
|
217
|
+
if (key) content = content.split(key).join("");
|
|
218
|
+
}
|
|
219
|
+
content = content.trim();
|
|
220
|
+
if (!content) return null;
|
|
221
|
+
return {
|
|
222
|
+
conversation: {
|
|
223
|
+
provider: "feishu",
|
|
224
|
+
accountId: appId,
|
|
225
|
+
kind: msg.chat_type === "p2p" ? "private" : "group",
|
|
226
|
+
id: chatId
|
|
227
|
+
},
|
|
228
|
+
message: {
|
|
229
|
+
id: messageId,
|
|
230
|
+
sender: {
|
|
231
|
+
id: senderId,
|
|
232
|
+
name: senderId
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
text: content
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
var FeishuTransport = class {
|
|
239
|
+
config;
|
|
240
|
+
report;
|
|
241
|
+
client;
|
|
242
|
+
socket;
|
|
243
|
+
stopped = false;
|
|
244
|
+
connection;
|
|
245
|
+
handlers = /* @__PURE__ */ new Set();
|
|
246
|
+
constructor(sdk, config, receive, status, report) {
|
|
247
|
+
this.config = config;
|
|
248
|
+
this.report = report;
|
|
249
|
+
const common = {
|
|
250
|
+
appId: config.appId,
|
|
251
|
+
appSecret: config.appSecret,
|
|
252
|
+
domain: domain(config.brand),
|
|
253
|
+
logger
|
|
254
|
+
};
|
|
255
|
+
this.client = new sdk.Client(common);
|
|
256
|
+
this.socket = new sdk.WSClient({
|
|
257
|
+
...common,
|
|
258
|
+
handshakeTimeoutMs: config.timeoutMs,
|
|
259
|
+
onReady: () => {
|
|
260
|
+
if (!this.stopped) status("connected");
|
|
261
|
+
},
|
|
262
|
+
onReconnected: () => {
|
|
263
|
+
if (!this.stopped) status("connected");
|
|
264
|
+
},
|
|
265
|
+
onReconnecting: () => {
|
|
266
|
+
if (!this.stopped) status("connecting");
|
|
267
|
+
},
|
|
268
|
+
onError: () => {
|
|
269
|
+
if (!this.stopped) status("error");
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
const dispatcher = new sdk.EventDispatcher({ logger }).register({ "im.message.receive_v1": async (event) => {
|
|
273
|
+
if (this.stopped) return;
|
|
274
|
+
const input = decodeMessage(event, config.appId, config.botOpenId);
|
|
275
|
+
if (!input) return;
|
|
276
|
+
const task = receive(input).then(() => {}, () => {
|
|
277
|
+
this.report();
|
|
278
|
+
throw new Error("DSH could not persist this message");
|
|
279
|
+
});
|
|
280
|
+
this.handlers.add(task);
|
|
281
|
+
try {
|
|
282
|
+
await task;
|
|
283
|
+
} finally {
|
|
284
|
+
this.handlers.delete(task);
|
|
285
|
+
}
|
|
286
|
+
} });
|
|
287
|
+
this.begin = () => {
|
|
288
|
+
this.connection = this.socket.start({ eventDispatcher: dispatcher }).catch(() => {
|
|
289
|
+
if (!this.stopped) {
|
|
290
|
+
status("error");
|
|
291
|
+
this.report();
|
|
292
|
+
}
|
|
293
|
+
}).finally(() => {
|
|
294
|
+
if (this.stopped) this.socket.close({ force: true });
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
begin;
|
|
299
|
+
start() {
|
|
300
|
+
if (!this.stopped && !this.connection) this.begin();
|
|
301
|
+
}
|
|
302
|
+
async send(delivery) {
|
|
303
|
+
if (this.stopped) throw new Error("Feishu is stopping");
|
|
304
|
+
checked(await this.client.request({
|
|
305
|
+
method: "POST",
|
|
306
|
+
url: "/open-apis/im/v1/messages",
|
|
307
|
+
params: { receive_id_type: "chat_id" },
|
|
308
|
+
timeout: this.config.timeoutMs,
|
|
309
|
+
data: {
|
|
310
|
+
receive_id: delivery.conversation.id,
|
|
311
|
+
msg_type: "text",
|
|
312
|
+
content: JSON.stringify({ text: delivery.text }),
|
|
313
|
+
uuid: createHash("sha256").update(delivery.id).digest("hex").slice(0, 48)
|
|
314
|
+
}
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
async close() {
|
|
318
|
+
this.stopped = true;
|
|
319
|
+
this.socket.close({ force: true });
|
|
320
|
+
await this.connection;
|
|
321
|
+
await Promise.allSettled(this.handlers);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region lib/types/feishu/manager.js
|
|
326
|
+
/** Per-Bot ownership and user-confirmed registration; no platform credentials reach the UI. */
|
|
327
|
+
const active = (status) => [
|
|
328
|
+
"starting",
|
|
329
|
+
"waiting",
|
|
330
|
+
"saving"
|
|
331
|
+
].includes(status);
|
|
332
|
+
function ids(values, prefix) {
|
|
333
|
+
const result = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
334
|
+
if (result.length > 1e3 || result.some((value) => !new RegExp(`^${prefix}[a-zA-Z0-9_-]+$`).test(value))) throw new Error(`请填写 ${prefix} 开头的飞书 ID,用逗号分隔`);
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
function appId(value) {
|
|
338
|
+
const id = value.trim();
|
|
339
|
+
if (!/^cli_[a-zA-Z0-9_-]+$/.test(id)) throw new Error("请填写 cli_ 开头的 App ID");
|
|
340
|
+
return id;
|
|
341
|
+
}
|
|
342
|
+
var FeishuManager = class {
|
|
343
|
+
store;
|
|
344
|
+
runtime;
|
|
345
|
+
state;
|
|
346
|
+
tail = Promise.resolve();
|
|
347
|
+
closed = false;
|
|
348
|
+
jobs = /* @__PURE__ */ new Map();
|
|
349
|
+
statuses = /* @__PURE__ */ new Map();
|
|
350
|
+
constructor(store, runtime) {
|
|
351
|
+
this.store = store;
|
|
352
|
+
this.runtime = runtime;
|
|
353
|
+
this.state = structuredClone(store.get());
|
|
354
|
+
}
|
|
355
|
+
exclusive(action) {
|
|
356
|
+
const next = this.tail.then(action);
|
|
357
|
+
this.tail = next.catch(() => {});
|
|
358
|
+
return next;
|
|
359
|
+
}
|
|
360
|
+
async change(action) {
|
|
361
|
+
const draft = structuredClone(this.state);
|
|
362
|
+
action(draft);
|
|
363
|
+
await this.store.set(draft);
|
|
364
|
+
this.state = draft;
|
|
365
|
+
}
|
|
366
|
+
ensureOpen() {
|
|
367
|
+
if (this.closed) throw new Error("飞书管理器正在关闭");
|
|
368
|
+
}
|
|
369
|
+
get(botId) {
|
|
370
|
+
const item = this.state.bindings.find((value) => value.botId === botId);
|
|
371
|
+
if (!item) throw new Error("请先绑定这个 Bot 的飞书机器人");
|
|
372
|
+
return item;
|
|
373
|
+
}
|
|
374
|
+
view(binding) {
|
|
375
|
+
const { secretRef, ...rest } = binding;
|
|
376
|
+
return {
|
|
377
|
+
...rest,
|
|
378
|
+
allowUsers: [...binding.allowUsers],
|
|
379
|
+
allowGroups: [...binding.allowGroups],
|
|
380
|
+
admins: [...binding.admins],
|
|
381
|
+
credentialsConfigured: !!secretRef,
|
|
382
|
+
...this.statuses.get(binding.botId) ?? {
|
|
383
|
+
status: binding.enabled ? "connecting" : "disabled",
|
|
384
|
+
error: null
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
registration(record) {
|
|
389
|
+
return structuredClone(this.jobs.get(record.attemptId)?.view ?? {
|
|
390
|
+
...record,
|
|
391
|
+
url: null,
|
|
392
|
+
qrDataUrl: null,
|
|
393
|
+
expiresAt: null
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
snapshot() {
|
|
397
|
+
return {
|
|
398
|
+
available: true,
|
|
399
|
+
bindings: this.state.bindings.map((binding) => this.view(binding)),
|
|
400
|
+
registrations: this.state.registrations.map((record) => this.registration(record)),
|
|
401
|
+
groups: structuredClone(this.state.groups)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
connection(botId, status, error = null) {
|
|
405
|
+
this.statuses.set(botId, {
|
|
406
|
+
status,
|
|
407
|
+
error
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
unique(id, botId) {
|
|
411
|
+
if (this.state.bindings.some((binding) => binding.botId !== botId && binding.appId === id)) throw new Error("这个飞书应用已绑定到其他 Bot");
|
|
412
|
+
}
|
|
413
|
+
async secret(binding) {
|
|
414
|
+
const secret = await this.runtime.resolve(binding.secretRef);
|
|
415
|
+
if (!secret) throw new Error("请恢复机器人凭据或重新填写 App Secret");
|
|
416
|
+
return secret;
|
|
417
|
+
}
|
|
418
|
+
async checked(binding, secret) {
|
|
419
|
+
try {
|
|
420
|
+
const identity = await this.runtime.identity(binding.appId, secret, binding.brand);
|
|
421
|
+
return {
|
|
422
|
+
...binding,
|
|
423
|
+
botOpenId: identity.openId,
|
|
424
|
+
robotName: identity.name
|
|
425
|
+
};
|
|
426
|
+
} catch {
|
|
427
|
+
throw new Error("验证失败,请检查凭据、机器人能力、应用发布状态和网络");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async activate(binding) {
|
|
431
|
+
this.connection(binding.botId, "connecting");
|
|
432
|
+
try {
|
|
433
|
+
await this.runtime.start(binding);
|
|
434
|
+
} catch {
|
|
435
|
+
this.connection(binding.botId, "error", "连接启动失败,请验证机器人并重新启用");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async init() {
|
|
439
|
+
if (this.state.registrations.some((record) => active(record.status))) await this.change((state) => {
|
|
440
|
+
for (const record of state.registrations) if (active(record.status)) {
|
|
441
|
+
record.status = "interrupted";
|
|
442
|
+
record.message = "服务重启中断了授权。请先检查飞书后台是否已经创建应用,再决定绑定已有应用或重新扫码。";
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
if (this.state.groups.some((group) => group.status === "creating")) await this.change((state) => {
|
|
446
|
+
for (const group of state.groups) if (group.status === "creating") {
|
|
447
|
+
group.status = "uncertain";
|
|
448
|
+
group.error = "服务重启中断了建群,请继续同一次请求";
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
for (const group of this.state.groups) if (group.chatId) {
|
|
452
|
+
const bindings = group.botIds.map((id) => this.get(id));
|
|
453
|
+
const room = await this.runtime.linkGroup(group, bindings);
|
|
454
|
+
if (group.roomId !== room.id) await this.change((state) => {
|
|
455
|
+
state.groups.find((item) => item.requestId === group.requestId).roomId = room.id;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
for (const binding of this.state.bindings) if (binding.enabled) await this.activate(binding);
|
|
459
|
+
}
|
|
460
|
+
createGroup(request) {
|
|
461
|
+
return this.exclusive(async () => {
|
|
462
|
+
this.ensureOpen();
|
|
463
|
+
const title = request.title.trim(), botIds = [...new Set(request.botIds)].sort();
|
|
464
|
+
if (!/^[a-zA-Z0-9_-]{8,50}$/.test(request.requestId) || !title || title.length > 120) throw new Error("请填写有效群名和建群请求 ID");
|
|
465
|
+
if (!botIds.length || botIds.length > 6 || !botIds.includes(request.creatorBot)) throw new Error("请选择 1–6 个机器人,并指定其中一个建群");
|
|
466
|
+
const previous = this.state.groups.find((group) => group.requestId === request.requestId);
|
|
467
|
+
const creator = this.get(request.creatorBot);
|
|
468
|
+
const owner = request.owner?.trim() || previous?.owner || creator.admins[0] || creator.allowUsers[0] || "";
|
|
469
|
+
if (!/^ou_[a-zA-Z0-9_-]+$/.test(owner)) throw new Error("请填写建群机器人所属应用下的群主 Open ID,或先配置该机器人的管理员");
|
|
470
|
+
if (previous) {
|
|
471
|
+
if (previous.title !== title || previous.creatorBot !== request.creatorBot || previous.owner !== owner || JSON.stringify(previous.botIds) !== JSON.stringify(botIds)) throw new Error("已提交的建群请求不能修改,请继续原请求");
|
|
472
|
+
return this.finishGroup(previous);
|
|
473
|
+
}
|
|
474
|
+
const group = {
|
|
475
|
+
requestId: request.requestId,
|
|
476
|
+
title,
|
|
477
|
+
botIds,
|
|
478
|
+
creatorBot: request.creatorBot,
|
|
479
|
+
owner,
|
|
480
|
+
createdAt: Date.now(),
|
|
481
|
+
chatId: null,
|
|
482
|
+
roomId: null,
|
|
483
|
+
status: "creating",
|
|
484
|
+
error: null
|
|
485
|
+
};
|
|
486
|
+
this.groupBindings(group);
|
|
487
|
+
await this.change((state) => {
|
|
488
|
+
state.groups.push(group);
|
|
489
|
+
});
|
|
490
|
+
return this.finishGroup(group, true);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
resumeGroup(requestId) {
|
|
494
|
+
return this.exclusive(async () => {
|
|
495
|
+
this.ensureOpen();
|
|
496
|
+
const group = this.state.groups.find((item) => item.requestId === requestId);
|
|
497
|
+
if (!group) throw new Error("建群请求不存在");
|
|
498
|
+
return this.finishGroup(group);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
groupBindings(group) {
|
|
502
|
+
const bindings = group.botIds.map((id) => {
|
|
503
|
+
if (!this.runtime.bots().some((bot) => bot.id === id)) throw new Error(`Bot ${id} 不存在`);
|
|
504
|
+
const binding = this.get(id);
|
|
505
|
+
if (!binding.enabled) throw new Error(`请先启用 ${id} 的飞书连接`);
|
|
506
|
+
return binding;
|
|
507
|
+
});
|
|
508
|
+
if (new Set(bindings.map((binding) => binding.brand)).size !== 1) throw new Error("飞书与 Lark 机器人不能混合建群");
|
|
509
|
+
return bindings;
|
|
510
|
+
}
|
|
511
|
+
async finishGroup(saved, firstSubmission = false) {
|
|
512
|
+
const group = structuredClone(saved);
|
|
513
|
+
const bindings = this.groupBindings(group), creator = this.get(group.creatorBot);
|
|
514
|
+
if (group.status === "ready" && group.chatId) return this.runtime.linkGroup(group, bindings);
|
|
515
|
+
const persist = async () => {
|
|
516
|
+
await this.change((state) => {
|
|
517
|
+
state.groups = state.groups.map((item) => item.requestId === group.requestId ? structuredClone(group) : item);
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
if (!group.chatId) {
|
|
521
|
+
const priorUnconfirmed = !firstSubmission && (group.status === "uncertain" || group.status === "creating");
|
|
522
|
+
if (Date.now() - group.createdAt >= 540 * 60 * 1e3) throw new Error("请求去重时限已过,请先在飞书确认建群结果;已有群可通过群 ID 手动接入");
|
|
523
|
+
group.status = "creating";
|
|
524
|
+
group.error = null;
|
|
525
|
+
await persist();
|
|
526
|
+
try {
|
|
527
|
+
group.chatId = await this.runtime.createGroup(creator, await this.secret(creator), {
|
|
528
|
+
requestId: group.requestId,
|
|
529
|
+
title: group.title,
|
|
530
|
+
owner: group.owner,
|
|
531
|
+
botAppIds: bindings.filter((binding) => binding.botId !== creator.botId).map((binding) => binding.appId)
|
|
532
|
+
});
|
|
533
|
+
} catch (cause) {
|
|
534
|
+
group.status = cause instanceof GroupCreateError && !priorUnconfirmed ? "failed" : "uncertain";
|
|
535
|
+
group.error = cause instanceof GroupCreateError ? cause.message : "建群结果待确认,请继续同一次请求;不要重复新建";
|
|
536
|
+
await persist();
|
|
537
|
+
throw new Error(group.error);
|
|
538
|
+
}
|
|
539
|
+
group.status = "linking";
|
|
540
|
+
await persist();
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const chatId = group.chatId;
|
|
544
|
+
await this.change((state) => {
|
|
545
|
+
for (const binding of state.bindings) if (group.botIds.includes(binding.botId) && !binding.allowGroups.includes(chatId)) binding.allowGroups.push(chatId);
|
|
546
|
+
});
|
|
547
|
+
const room = await this.runtime.linkGroup(group, bindings);
|
|
548
|
+
for (const id of group.botIds) {
|
|
549
|
+
await this.runtime.stop(id);
|
|
550
|
+
await this.activate(this.get(id));
|
|
551
|
+
}
|
|
552
|
+
if (group.botIds.some((id) => this.statuses.get(id)?.status === "error")) throw new Error("连接启动失败");
|
|
553
|
+
group.roomId = room.id;
|
|
554
|
+
group.status = "ready";
|
|
555
|
+
group.error = null;
|
|
556
|
+
await persist();
|
|
557
|
+
return room;
|
|
558
|
+
} catch {
|
|
559
|
+
group.status = "linking";
|
|
560
|
+
group.error = "飞书群已创建,接入尚未完成。点击继续完成,不会重复建群。";
|
|
561
|
+
await persist();
|
|
562
|
+
throw new Error(group.error);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
save(request) {
|
|
566
|
+
return this.exclusive(async () => {
|
|
567
|
+
this.ensureOpen();
|
|
568
|
+
if (!this.runtime.bots().some((bot) => bot.id === request.botId)) throw new Error("Bot 不存在");
|
|
569
|
+
if ([...this.jobs.values()].some((job) => job.view.botId === request.botId && !job.done)) throw new Error("请先完成或取消正在进行的扫码授权");
|
|
570
|
+
const previous = this.state.bindings.find((binding) => binding.botId === request.botId);
|
|
571
|
+
const id = appId(request.appId);
|
|
572
|
+
this.unique(id, request.botId);
|
|
573
|
+
if (previous && (previous.appId !== id || previous.brand !== request.brand)) throw new Error("已绑定 Bot 不能直接更换应用;请为新应用创建一个新的 DSH Bot");
|
|
574
|
+
const secret = request.appSecret?.trim() || (previous ? await this.secret(previous) : "");
|
|
575
|
+
if (!secret) throw new Error("首次绑定需要 App Secret");
|
|
576
|
+
const base = {
|
|
577
|
+
botId: request.botId,
|
|
578
|
+
appId: id,
|
|
579
|
+
brand: request.brand,
|
|
580
|
+
botOpenId: "",
|
|
581
|
+
robotName: id,
|
|
582
|
+
enabled: previous?.enabled ?? false,
|
|
583
|
+
secretRef: `DSH_FEISHU_${randomUUID().replaceAll("-", "").toUpperCase()}_SECRET`,
|
|
584
|
+
allowUsers: ids(request.allowUsers, "ou_"),
|
|
585
|
+
allowGroups: ids(request.allowGroups, "oc_"),
|
|
586
|
+
admins: ids(request.admins, "ou_")
|
|
587
|
+
};
|
|
588
|
+
const binding = await this.checked(base, secret);
|
|
589
|
+
await this.runtime.set(binding.secretRef, secret);
|
|
590
|
+
await this.change((state) => {
|
|
591
|
+
state.bindings = [...state.bindings.filter((item) => item.botId !== request.botId), binding];
|
|
592
|
+
});
|
|
593
|
+
await this.runtime.stop(binding.botId);
|
|
594
|
+
if (binding.enabled) await this.activate(binding);
|
|
595
|
+
else this.connection(binding.botId, "disabled");
|
|
596
|
+
return this.view(binding);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
verify(botId) {
|
|
600
|
+
return this.exclusive(async () => {
|
|
601
|
+
this.ensureOpen();
|
|
602
|
+
const current = this.get(botId), verified = await this.checked(current, await this.secret(current));
|
|
603
|
+
await this.change((state) => {
|
|
604
|
+
state.bindings = state.bindings.map((item) => item.botId === botId ? verified : item);
|
|
605
|
+
});
|
|
606
|
+
return this.view(verified);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
toggle(request) {
|
|
610
|
+
return this.exclusive(async () => {
|
|
611
|
+
this.ensureOpen();
|
|
612
|
+
let binding = {
|
|
613
|
+
...this.get(request.botId),
|
|
614
|
+
enabled: request.enabled
|
|
615
|
+
};
|
|
616
|
+
if (binding.enabled) binding = await this.checked(binding, await this.secret(binding));
|
|
617
|
+
await this.change((state) => {
|
|
618
|
+
state.bindings = state.bindings.map((item) => item.botId === binding.botId ? binding : item);
|
|
619
|
+
});
|
|
620
|
+
await this.runtime.stop(binding.botId);
|
|
621
|
+
if (binding.enabled) await this.activate(binding);
|
|
622
|
+
else this.connection(binding.botId, "disabled");
|
|
623
|
+
return this.view(binding);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
register(botId) {
|
|
627
|
+
return this.exclusive(async () => {
|
|
628
|
+
this.ensureOpen();
|
|
629
|
+
const bot = this.runtime.bots().find((item) => item.id === botId);
|
|
630
|
+
if (!bot) throw new Error("Bot 不存在");
|
|
631
|
+
const inFlight = [...this.jobs.values()].find((job) => job.view.botId === botId && !job.done);
|
|
632
|
+
if (inFlight) return structuredClone(inFlight.view);
|
|
633
|
+
for (const [key, job] of this.jobs) if (job.view.botId === botId && job.done) this.jobs.delete(key);
|
|
634
|
+
const pending = this.state.registrations.find((record) => record.botId === botId && active(record.status));
|
|
635
|
+
if (pending) return this.registration(pending);
|
|
636
|
+
if (this.state.bindings.some((binding) => binding.botId === botId)) throw new Error("这个 Bot 已绑定机器人,不能重复创建");
|
|
637
|
+
const attemptId = randomUUID();
|
|
638
|
+
const record = {
|
|
639
|
+
botId,
|
|
640
|
+
attemptId,
|
|
641
|
+
status: "starting",
|
|
642
|
+
message: null
|
|
643
|
+
};
|
|
644
|
+
await this.change((state) => {
|
|
645
|
+
state.registrations = [...state.registrations.filter((item) => item.botId !== botId), record];
|
|
646
|
+
});
|
|
647
|
+
const job = {
|
|
648
|
+
done: false,
|
|
649
|
+
controller: new AbortController(),
|
|
650
|
+
view: {
|
|
651
|
+
...record,
|
|
652
|
+
url: null,
|
|
653
|
+
qrDataUrl: null,
|
|
654
|
+
expiresAt: null
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
this.jobs.set(attemptId, job);
|
|
658
|
+
job.task = this.runRegistration(job, bot.name).finally(() => {
|
|
659
|
+
job.done = true;
|
|
660
|
+
});
|
|
661
|
+
return this.registration(record);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async settle(job, status, message) {
|
|
665
|
+
await this.change((state) => {
|
|
666
|
+
const record = state.registrations.find((item) => item.attemptId === job.view.attemptId);
|
|
667
|
+
if (record) {
|
|
668
|
+
record.status = status;
|
|
669
|
+
record.message = message;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
job.view = {
|
|
673
|
+
...job.view,
|
|
674
|
+
status,
|
|
675
|
+
message,
|
|
676
|
+
url: null,
|
|
677
|
+
qrDataUrl: null,
|
|
678
|
+
expiresAt: null
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
async runRegistration(job, name) {
|
|
682
|
+
try {
|
|
683
|
+
const result = await this.runtime.register({
|
|
684
|
+
name,
|
|
685
|
+
signal: job.controller.signal,
|
|
686
|
+
qr: (info) => {
|
|
687
|
+
if (job.controller.signal.aborted || job.view.status !== "starting") return;
|
|
688
|
+
job.view = {
|
|
689
|
+
...job.view,
|
|
690
|
+
status: "waiting",
|
|
691
|
+
url: null,
|
|
692
|
+
qrDataUrl: null,
|
|
693
|
+
expiresAt: Date.now() + info.expireIn * 1e3
|
|
694
|
+
};
|
|
695
|
+
this.runtime.qrImage(info.url).then((qrDataUrl) => {
|
|
696
|
+
if (job.view.status === "waiting" && !job.controller.signal.aborted) job.view = {
|
|
697
|
+
...job.view,
|
|
698
|
+
url: info.url,
|
|
699
|
+
qrDataUrl
|
|
700
|
+
};
|
|
701
|
+
}).catch(() => {
|
|
702
|
+
job.controller.abort();
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
await this.exclusive(async () => {
|
|
707
|
+
job.view = {
|
|
708
|
+
...job.view,
|
|
709
|
+
status: "saving",
|
|
710
|
+
url: null,
|
|
711
|
+
qrDataUrl: null
|
|
712
|
+
};
|
|
713
|
+
const id = appId(result.client_id);
|
|
714
|
+
this.unique(id, job.view.botId);
|
|
715
|
+
if (!result.client_secret) throw new Error("No application credentials returned");
|
|
716
|
+
const owner = result.user_info?.open_id;
|
|
717
|
+
const allowUsers = owner && /^ou_[\w-]+$/.test(owner) ? [owner] : [];
|
|
718
|
+
const binding = {
|
|
719
|
+
botId: job.view.botId,
|
|
720
|
+
appId: id,
|
|
721
|
+
botOpenId: "",
|
|
722
|
+
robotName: name,
|
|
723
|
+
brand: result.user_info?.tenant_brand === "lark" ? "lark" : "feishu",
|
|
724
|
+
enabled: false,
|
|
725
|
+
secretRef: `DSH_FEISHU_${createHash("sha256").update(job.view.attemptId).digest("hex").toUpperCase()}_SECRET`,
|
|
726
|
+
allowUsers,
|
|
727
|
+
admins: [...allowUsers],
|
|
728
|
+
allowGroups: []
|
|
729
|
+
};
|
|
730
|
+
await this.runtime.set(binding.secretRef, result.client_secret);
|
|
731
|
+
await this.change((state) => {
|
|
732
|
+
state.bindings.push(binding);
|
|
733
|
+
});
|
|
734
|
+
let notice = "机器人已创建并绑定。请核对授权页的权限范围,然后启用连接。";
|
|
735
|
+
try {
|
|
736
|
+
const verified = await this.checked(binding, result.client_secret);
|
|
737
|
+
await this.change((state) => {
|
|
738
|
+
state.bindings = state.bindings.map((item) => item.botId === binding.botId ? verified : item);
|
|
739
|
+
});
|
|
740
|
+
} catch {
|
|
741
|
+
notice = "凭据已安全保存,但机器人暂未就绪。请检查飞书应用发布状态,再点击验证机器人;无需再次创建。";
|
|
742
|
+
}
|
|
743
|
+
await this.settle(job, "complete", notice);
|
|
744
|
+
});
|
|
745
|
+
} catch {
|
|
746
|
+
await this.exclusive(async () => {
|
|
747
|
+
if (job.view.status === "complete" || job.view.status === "canceled") return;
|
|
748
|
+
const expired = job.view.expiresAt !== null && Date.now() >= job.view.expiresAt;
|
|
749
|
+
await this.settle(job, this.closed ? "interrupted" : expired ? "expired" : job.controller.signal.aborted ? "canceled" : "error", this.closed ? "服务已停止,请先检查飞书后台是否已经创建应用。" : expired ? "二维码已过期,可以重新发起扫码。" : job.controller.signal.aborted ? "已取消等待。若已在飞书确认创建,请使用已有应用绑定。" : "创建或保存未完成,请先检查飞书后台;不要重复创建已有应用。");
|
|
750
|
+
}).catch(() => {
|
|
751
|
+
job.view = {
|
|
752
|
+
...job.view,
|
|
753
|
+
status: "error",
|
|
754
|
+
message: "无法保存授权结果,请检查 DSH 存储和飞书后台。",
|
|
755
|
+
url: null,
|
|
756
|
+
qrDataUrl: null
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
cancel(attemptId) {
|
|
762
|
+
return this.exclusive(async () => {
|
|
763
|
+
this.ensureOpen();
|
|
764
|
+
const job = this.jobs.get(attemptId);
|
|
765
|
+
if (!job || !active(job.view.status)) return;
|
|
766
|
+
if (job.view.status === "saving") throw new Error("凭据正在保存,请稍候");
|
|
767
|
+
job.controller.abort();
|
|
768
|
+
await this.settle(job, "canceled", "已取消等待。若已在飞书确认创建,请使用已有应用绑定。");
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
async close() {
|
|
772
|
+
this.closed = true;
|
|
773
|
+
for (const job of this.jobs.values()) job.controller.abort();
|
|
774
|
+
await Promise.allSettled([...this.jobs.values()].map((job) => job.task));
|
|
775
|
+
await this.tail;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region lib/types/feishu.js
|
|
780
|
+
/** Optional Feishu companion, with one independently owned robot per DSH Bot. */
|
|
781
|
+
const name = "bot-feishu";
|
|
782
|
+
const inject = [
|
|
783
|
+
"botMode",
|
|
784
|
+
"storageDomain",
|
|
785
|
+
"credentials"
|
|
786
|
+
];
|
|
787
|
+
const Config = Schema.object({
|
|
788
|
+
maxPending: Schema.natural().min(1).default(200),
|
|
789
|
+
maxQueuePerRoom: Schema.natural().min(1).default(20),
|
|
790
|
+
maxConcurrentRooms: Schema.natural().min(1).default(4),
|
|
791
|
+
maxReceipts: Schema.natural().min(1).default(1e5),
|
|
792
|
+
maxOutbox: Schema.natural().min(20).default(1e4),
|
|
793
|
+
chunkSize: Schema.natural().min(100).max(3e3).default(3e3),
|
|
794
|
+
retryMs: Schema.natural().min(100).default(5e3),
|
|
795
|
+
maxRetryMs: Schema.natural().min(100).default(12e4),
|
|
796
|
+
endpointTimeoutMs: Schema.natural().min(1e3).default(15e3),
|
|
797
|
+
pollMs: Schema.natural().min(100).default(1e3)
|
|
798
|
+
});
|
|
799
|
+
async function apply(ctx, config) {
|
|
800
|
+
const settings = await ctx.storageDomain.open(settingsDomain);
|
|
801
|
+
const connections = /* @__PURE__ */ new Map();
|
|
802
|
+
const stop = async (botId) => {
|
|
803
|
+
const current = connections.get(botId);
|
|
804
|
+
if (current) {
|
|
805
|
+
connections.delete(botId);
|
|
806
|
+
await current.close();
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
const report = () => {
|
|
810
|
+
ctx.logger.warn("Feishu operation failed; check connection status and retained work");
|
|
811
|
+
};
|
|
812
|
+
const resolve = async (ref) => (await ctx.credentials.resolve(credentialRef(ref)))?.value;
|
|
813
|
+
const manager = new FeishuManager(settings.global, {
|
|
814
|
+
bots: () => ctx.botMode.snapshot().bots,
|
|
815
|
+
createGroup: (binding, secret, options) => createGroup(loadSdk(config.endpointTimeoutMs), binding.appId, secret, binding.brand, config.endpointTimeoutMs, options),
|
|
816
|
+
linkGroup: async (group, bindings) => {
|
|
817
|
+
if (!group.chatId) throw new Error("Created chat ID required");
|
|
818
|
+
return ctx.botMode.ensureExternalRoom({
|
|
819
|
+
conversation: {
|
|
820
|
+
provider: "feishu",
|
|
821
|
+
accountId: `group:${group.chatId}`,
|
|
822
|
+
kind: "group",
|
|
823
|
+
id: group.chatId
|
|
824
|
+
},
|
|
825
|
+
title: group.title,
|
|
826
|
+
botIds: bindings.map((binding) => binding.botId),
|
|
827
|
+
defaultBot: group.creatorBot
|
|
828
|
+
});
|
|
829
|
+
},
|
|
830
|
+
resolve,
|
|
831
|
+
set: (ref, value) => ctx.credentials.set(credentialRef(ref), value),
|
|
832
|
+
stop,
|
|
833
|
+
identity: (appId, secret, brand) => identity(loadSdk(config.endpointTimeoutMs), appId, secret, brand, config.endpointTimeoutMs),
|
|
834
|
+
register: (options) => register(loadSdk(config.endpointTimeoutMs), options),
|
|
835
|
+
qrImage,
|
|
836
|
+
start: async (binding) => {
|
|
837
|
+
const appSecret = await resolve(binding.secretRef);
|
|
838
|
+
if (!appSecret || !binding.botOpenId) throw new Error("Robot identity and credentials required");
|
|
839
|
+
for (const chatId of binding.allowGroups) await ctx.botMode.ensureExternalRoom({
|
|
840
|
+
conversation: {
|
|
841
|
+
provider: "feishu",
|
|
842
|
+
accountId: binding.appId,
|
|
843
|
+
kind: "group",
|
|
844
|
+
id: chatId
|
|
845
|
+
},
|
|
846
|
+
title: `飞书 · ${chatId}`,
|
|
847
|
+
botIds: [binding.botId],
|
|
848
|
+
defaultBot: binding.botId
|
|
849
|
+
});
|
|
850
|
+
const queue = await ctx.storageDomain.open(bridgeDomain(`bot_fs_${createHash("sha256").update(`${binding.botId}:${binding.appId}`).digest("hex").slice(0, 28)}`));
|
|
851
|
+
let bridge, transport;
|
|
852
|
+
try {
|
|
853
|
+
transport = new FeishuTransport(loadSdk(config.endpointTimeoutMs), {
|
|
854
|
+
...binding,
|
|
855
|
+
appSecret,
|
|
856
|
+
timeoutMs: config.endpointTimeoutMs
|
|
857
|
+
}, (input) => bridge.receive({
|
|
858
|
+
...input,
|
|
859
|
+
targets: [binding.botId]
|
|
860
|
+
}), (status) => {
|
|
861
|
+
manager.connection(binding.botId, status, status === "error" ? "长连接异常,请检查事件订阅、应用发布状态和网络后重新启用" : null);
|
|
862
|
+
}, report);
|
|
863
|
+
bridge = new InfoFlowBridge(ctx.botMode, queue.global, transport, {
|
|
864
|
+
...config,
|
|
865
|
+
provider: "feishu",
|
|
866
|
+
accountId: binding.appId,
|
|
867
|
+
botIds: [binding.botId],
|
|
868
|
+
defaultBot: binding.botId,
|
|
869
|
+
allowUsers: binding.allowUsers,
|
|
870
|
+
allowGroups: binding.allowGroups,
|
|
871
|
+
admins: binding.admins
|
|
872
|
+
}, report);
|
|
873
|
+
const currentBridge = bridge, currentTransport = transport;
|
|
874
|
+
connections.set(binding.botId, {
|
|
875
|
+
bridge,
|
|
876
|
+
close: async () => {
|
|
877
|
+
await currentTransport.close();
|
|
878
|
+
await currentBridge.close();
|
|
879
|
+
await queue.close();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
transport.start();
|
|
883
|
+
} catch (cause) {
|
|
884
|
+
await transport?.close();
|
|
885
|
+
await bridge?.close();
|
|
886
|
+
await queue.close();
|
|
887
|
+
throw cause;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
const abort = new AbortController();
|
|
892
|
+
const wake = async () => {
|
|
893
|
+
await Promise.all([...connections.values()].map((item) => item.bridge.wake()));
|
|
894
|
+
};
|
|
895
|
+
let feed, timer;
|
|
896
|
+
const detach = ctx.botMode.attachFeishu(manager);
|
|
897
|
+
ctx.effect(() => async () => {
|
|
898
|
+
detach();
|
|
899
|
+
abort.abort();
|
|
900
|
+
if (timer) clearInterval(timer);
|
|
901
|
+
await manager.close();
|
|
902
|
+
await Promise.all([...connections.keys()].map(stop));
|
|
903
|
+
await feed;
|
|
904
|
+
await settings.close();
|
|
905
|
+
}, "feishu: cancel registration, drain handlers and close native queues");
|
|
906
|
+
await manager.init();
|
|
907
|
+
ctx.on("session/event", (session, event) => {
|
|
908
|
+
if (event.type === "approval/asked") for (const { bridge } of connections.values()) bridge.attention(session.id, `approval:${event.data.id}`).catch(report);
|
|
909
|
+
});
|
|
910
|
+
ctx.on("user-questions/request", async (request, next) => {
|
|
911
|
+
if (request.agent) for (const { bridge } of connections.values()) bridge.attention(request.agent.session.id, `question:${createHash("sha256").update(JSON.stringify(request.questions)).digest("hex")}`).catch(report);
|
|
912
|
+
return next();
|
|
913
|
+
}, { prepend: true });
|
|
914
|
+
feed = (async () => {
|
|
915
|
+
for await (const _frame of ctx.botMode.follow(abort.signal)) await wake();
|
|
916
|
+
})().catch(() => {
|
|
917
|
+
if (!abort.signal.aborted) report();
|
|
918
|
+
});
|
|
919
|
+
timer = setInterval(() => {
|
|
920
|
+
wake().catch(report);
|
|
921
|
+
}, config.pollMs);
|
|
922
|
+
timer.unref();
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
export { Config, apply, inject, name };
|