@xmanrui/dsh-im 0.18.0 → 1.0.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/README.en.md +12 -1
- package/README.md +12 -1
- package/lib/client.js +341 -37
- package/lib/index.js +162 -156
- package/package.json +1 -1
- package/plugin-src/client/channels/feishu/api.js +23 -5
- package/plugin-src/client/channels/feishu/index.js +291 -39
- package/plugin-src/client/channels/feishu/styles.js +46 -0
- package/plugin-src/client/i18n.js +50 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +5 -2
- package/plugin-src/host/channels/feishu/production.mjs +7 -2
- package/plugin-src/host/channels/feishu/rpc.mjs +61 -7
- package/plugin-src/host/channels/qq/production.mjs +5 -2
- package/plugin-src/host/channels/shared/production.mjs +5 -2
- package/plugin-src/host/channels/slack/production.mjs +5 -2
- package/plugin-src/host/channels/wecom/production.mjs +5 -2
- package/plugin-src/host/channels/weixin/production.mjs +5 -2
- package/plugin-src/host/channels/whatsapp/production.mjs +5 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +11 -1
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +40 -5
- package/src/channels/feishu/feishu-cards.mjs +4 -0
- package/src/channels/feishu/feishu-runtime.mjs +14 -0
- package/src/channels/feishu/group-message-permission-manager.mjs +71 -0
- package/src/channels/feishu/group-response-mode.mjs +15 -0
- package/src/channels/feishu/multi-bot-controller.mjs +258 -9
- package/src/channels/feishu/plugin-config-store.mjs +3 -0
- package/src/channels/feishu/repair-manager.mjs +17 -3
- package/src/channels/qq/qq-bridge.mjs +11 -1
- package/src/channels/shared/bot-workspace-store.mjs +75 -4
- package/src/channels/shared/preset-command.mjs +305 -0
- package/src/channels/shared/text-harness-bridge.mjs +11 -1
- package/src/channels/telegram/telegram-api.mjs +31 -0
- package/src/channels/telegram/telegram-runtime.mjs +27 -1
- package/src/channels/wecom/wecom-bridge.mjs +11 -1
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +11 -1
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|
|
14
14
|
status: "connection.status",
|
|
15
15
|
beginProvisioning: "provision.begin",
|
|
16
16
|
beginCallbackRepair: "bot.callback-repair.begin",
|
|
17
|
+
beginGroupMessagePermission: "bot.group-message-permission.begin",
|
|
17
18
|
pollProvisioning: "provision.poll",
|
|
18
19
|
cancelProvisioning: "provision.cancel",
|
|
19
20
|
bindCredentials: "bot.bind-credentials",
|
|
@@ -22,6 +23,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|
|
22
23
|
deleteBot: "bot.delete",
|
|
23
24
|
setWorkspace: "bot.workspace.set",
|
|
24
25
|
setAgentPreset: "bot.preset.set",
|
|
26
|
+
setGroupResponseMode: "bot.group-response-mode.set",
|
|
25
27
|
// Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
|
|
26
28
|
testConnection: "connection.test",
|
|
27
29
|
disconnect: "connection.disconnect",
|
|
@@ -30,6 +32,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|
|
30
32
|
export const FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
|
|
31
33
|
PROVISION: "provision",
|
|
32
34
|
CALLBACK_REPAIR: "callback_repair",
|
|
35
|
+
GROUP_MESSAGE_PERMISSION: "group_message_permission",
|
|
33
36
|
});
|
|
34
37
|
|
|
35
38
|
const CONNECTION_STATES = new Set([
|
|
@@ -70,6 +73,10 @@ function optionalTimestamp(value) {
|
|
|
70
73
|
return undefined;
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
export function normalizeGroupResponseMode(value) {
|
|
77
|
+
return value === "all" ? "all" : "mention";
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
function clamp(value, min, max, fallback) {
|
|
74
81
|
return typeof value === "number" && Number.isFinite(value)
|
|
75
82
|
? Math.min(max, Math.max(min, value))
|
|
@@ -77,9 +84,18 @@ function clamp(value, min, max, fallback) {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
function normalizeRegistrationOperation(value) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
if (value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR) {
|
|
88
|
+
return FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
|
89
|
+
}
|
|
90
|
+
if (value === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION) {
|
|
91
|
+
return FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
|
92
|
+
}
|
|
93
|
+
return FEISHU_REGISTRATION_OPERATIONS.PROVISION;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isTargetedAppUpdate(operation) {
|
|
97
|
+
return operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
|
|
98
|
+
|| operation === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
export function unwrapRpcResult(result) {
|
|
@@ -112,8 +128,8 @@ export function normalizeProvisioning(value, now = Date.now()) {
|
|
|
112
128
|
const expireIn = clamp(source.expireIn, 1, 60 * 60, 5 * 60);
|
|
113
129
|
const operation = normalizeRegistrationOperation(source.operation);
|
|
114
130
|
const botId = optionalString(source.botId);
|
|
115
|
-
if (operation
|
|
116
|
-
throw new Error("
|
|
131
|
+
if (isTargetedAppUpdate(operation) && !botId) {
|
|
132
|
+
throw new Error("飞书服务返回的应用更新信息缺少 botId");
|
|
117
133
|
}
|
|
118
134
|
return {
|
|
119
135
|
attemptId,
|
|
@@ -186,6 +202,8 @@ export function normalizeBotConnection(value, fallbackBotId) {
|
|
|
186
202
|
configured: value.configured !== false,
|
|
187
203
|
workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
|
|
188
204
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
|
205
|
+
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
|
|
206
|
+
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
|
189
207
|
bot: normalizeBot(value.bot),
|
|
190
208
|
health: normalizeHealth(value.health, connected),
|
|
191
209
|
error: normalizeError(value.error),
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
FEISHU_RPC_CHANNEL,
|
|
10
10
|
formatRemaining,
|
|
11
11
|
normalizeBotsSnapshot,
|
|
12
|
+
normalizeGroupResponseMode,
|
|
12
13
|
normalizePollResult,
|
|
13
14
|
normalizeProvisioning,
|
|
14
15
|
presentError,
|
|
@@ -28,11 +29,20 @@ export const name = "feishu-settings";
|
|
|
28
29
|
export const inject = ["slots", "connection"];
|
|
29
30
|
|
|
30
31
|
const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
|
32
|
+
const GROUP_MESSAGE_PERMISSION_OPERATION = FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
|
31
33
|
|
|
32
34
|
function isCallbackRepair(value) {
|
|
33
35
|
return value?.operation === CALLBACK_REPAIR_OPERATION;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
function isGroupMessagePermission(value) {
|
|
39
|
+
return value?.operation === GROUP_MESSAGE_PERMISSION_OPERATION;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isTargetedAppUpdate(value) {
|
|
43
|
+
return isCallbackRepair(value) || isGroupMessagePermission(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
|
|
37
47
|
return h("svg", {
|
|
38
48
|
width: size,
|
|
@@ -241,6 +251,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
241
251
|
const expired = provision.expired === true || remaining === 0;
|
|
242
252
|
const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
|
|
243
253
|
const repairing = isCallbackRepair(provision);
|
|
254
|
+
const grantingGroupMessages = isGroupMessagePermission(provision);
|
|
244
255
|
const botName = provision.botName ?? "此机器人";
|
|
245
256
|
|
|
246
257
|
React.useEffect(() => setImageFailed(false), [qrSource]);
|
|
@@ -254,7 +265,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
254
265
|
src: qrSource,
|
|
255
266
|
alt: repairing
|
|
256
267
|
? `用于修复${botName}卡片按钮的一次性授权二维码`
|
|
257
|
-
:
|
|
268
|
+
: grantingGroupMessages
|
|
269
|
+
? `用于为${botName}开通群消息权限的一次性授权二维码`
|
|
270
|
+
: "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
|
|
258
271
|
onError: () => setImageFailed(true),
|
|
259
272
|
})
|
|
260
273
|
: h("div", { className: "bxf-qrFallback dim-qrFallback" },
|
|
@@ -278,21 +291,35 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
278
291
|
h("div", { className: "bxf-qrCopy dim-qrCopy" },
|
|
279
292
|
h("div", { className: "bxf-stateLabel dim-stateLabel" },
|
|
280
293
|
h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
|
|
281
|
-
h("span", null, repairing
|
|
294
|
+
h("span", null, repairing
|
|
295
|
+
? `正在修复「${botName}」`
|
|
296
|
+
: grantingGroupMessages
|
|
297
|
+
? `正在为「${botName}」开通群消息权限`
|
|
298
|
+
: "正在添加新机器人")),
|
|
282
299
|
h("h3", null, expired
|
|
283
300
|
? "刷新二维码后继续"
|
|
284
|
-
: repairing
|
|
301
|
+
: repairing
|
|
302
|
+
? "使用飞书扫码修复卡片按钮"
|
|
303
|
+
: grantingGroupMessages
|
|
304
|
+
? "使用飞书确认群消息权限"
|
|
305
|
+
: "使用飞书扫码创建机器人"),
|
|
285
306
|
h("p", null, repairing
|
|
286
307
|
? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
|
|
287
|
-
:
|
|
308
|
+
: grantingGroupMessages
|
|
309
|
+
? "扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。"
|
|
310
|
+
: "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
|
|
288
311
|
h("ol", { className: "bxf-steps dim-steps" },
|
|
289
312
|
h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
|
|
290
313
|
h("li", null, repairing
|
|
291
314
|
? "核对现有应用名称,并确认只新增卡片回调"
|
|
292
|
-
:
|
|
315
|
+
: grantingGroupMessages
|
|
316
|
+
? "核对现有应用,并确认“获取群组中所有消息”权限"
|
|
317
|
+
: "核对应用名称与权限范围,并确认创建"),
|
|
293
318
|
h("li", null, repairing
|
|
294
319
|
? "保持本页打开,等待卡片按钮修复完成"
|
|
295
|
-
:
|
|
320
|
+
: grantingGroupMessages
|
|
321
|
+
? "保持本页打开,等待权限生效并自动切换响应方式"
|
|
322
|
+
: "保持本页打开,等待新机器人的长连接就绪")),
|
|
296
323
|
h("div", { className: "bxf-actions dim-viewActions" },
|
|
297
324
|
expired
|
|
298
325
|
? h(Button, {
|
|
@@ -307,7 +334,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
307
334
|
!expired
|
|
308
335
|
? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
|
|
309
336
|
: null,
|
|
310
|
-
h(Button, { onClick: onCancel, disabled: busy }, repairing
|
|
337
|
+
h(Button, { onClick: onCancel, disabled: busy }, repairing
|
|
338
|
+
? "取消修复"
|
|
339
|
+
: grantingGroupMessages ? "取消授权" : "取消添加")),
|
|
311
340
|
),
|
|
312
341
|
),
|
|
313
342
|
);
|
|
@@ -316,34 +345,50 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
316
345
|
function ProvisionProgress({ phase, provision, onCancel, busy }) {
|
|
317
346
|
const connecting = phase === "connecting";
|
|
318
347
|
const repairing = isCallbackRepair(provision);
|
|
348
|
+
const grantingGroupMessages = isGroupMessagePermission(provision);
|
|
319
349
|
return h("div", {
|
|
320
350
|
className: "bxf-card bxf-provisionCard dim-surfaceCard dim-loadingView",
|
|
321
351
|
"aria-busy": "true",
|
|
322
352
|
},
|
|
323
353
|
h("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
|
324
354
|
h("h3", null, connecting
|
|
325
|
-
? repairing
|
|
326
|
-
|
|
355
|
+
? repairing
|
|
356
|
+
? "已确认,正在完成卡片按钮修复"
|
|
357
|
+
: grantingGroupMessages
|
|
358
|
+
? "已确认,正在启用全部消息模式"
|
|
359
|
+
: "已确认,正在连接新机器人"
|
|
360
|
+
: repairing
|
|
361
|
+
? "正在准备修复二维码"
|
|
362
|
+
: grantingGroupMessages ? "正在准备权限授权二维码" : "正在准备授权二维码"),
|
|
327
363
|
h("p", null, connecting
|
|
328
364
|
? repairing
|
|
329
365
|
? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
|
330
|
-
:
|
|
366
|
+
: grantingGroupMessages
|
|
367
|
+
? "权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
|
368
|
+
: "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
|
|
331
369
|
: repairing
|
|
332
370
|
? "正在为现有飞书应用申请一次性更新二维码,请稍候。"
|
|
333
|
-
:
|
|
371
|
+
: grantingGroupMessages
|
|
372
|
+
? "正在为现有飞书应用申请群消息权限二维码,请稍候。"
|
|
373
|
+
: "正在向飞书申请一次性授权二维码,请稍候。"),
|
|
334
374
|
connecting && onCancel
|
|
335
375
|
? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
|
|
336
|
-
h(Button, { onClick: onCancel, disabled: busy }, repairing
|
|
376
|
+
h(Button, { onClick: onCancel, disabled: busy }, repairing
|
|
377
|
+
? "取消修复"
|
|
378
|
+
: grantingGroupMessages ? "取消授权" : "取消添加"))
|
|
337
379
|
: null,
|
|
338
380
|
);
|
|
339
381
|
}
|
|
340
382
|
|
|
341
383
|
function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
|
|
342
384
|
const repairing = isCallbackRepair(provision);
|
|
385
|
+
const grantingGroupMessages = isGroupMessagePermission(provision);
|
|
343
386
|
return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
|
344
387
|
h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
|
|
345
388
|
h("div", null,
|
|
346
|
-
h("h3", null, repairing
|
|
389
|
+
h("h3", null, repairing
|
|
390
|
+
? "卡片按钮没有修复完成"
|
|
391
|
+
: grantingGroupMessages ? "群消息权限没有开通完成" : "新机器人没有添加完成"),
|
|
347
392
|
h("p", null, error.message),
|
|
348
393
|
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
|
349
394
|
h("div", { className: "bxf-actions dim-viewActions" },
|
|
@@ -413,10 +458,95 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
|
|
|
413
458
|
);
|
|
414
459
|
}
|
|
415
460
|
|
|
461
|
+
function GroupResponseModeEditor({
|
|
462
|
+
value,
|
|
463
|
+
permissionGranted = false,
|
|
464
|
+
disabled = false,
|
|
465
|
+
authorizationDisabled = false,
|
|
466
|
+
onSave,
|
|
467
|
+
onAuthorize,
|
|
468
|
+
}) {
|
|
469
|
+
const current = normalizeGroupResponseMode(value);
|
|
470
|
+
const [saving, setSaving] = React.useState(false);
|
|
471
|
+
const [authorizing, setAuthorizing] = React.useState(false);
|
|
472
|
+
const [error, setError] = React.useState(null);
|
|
473
|
+
|
|
474
|
+
const change = async (event) => {
|
|
475
|
+
const next = normalizeGroupResponseMode(event.target.value);
|
|
476
|
+
if (next === current || saving || disabled) return;
|
|
477
|
+
setSaving(true);
|
|
478
|
+
setError(null);
|
|
479
|
+
try {
|
|
480
|
+
await onSave?.(next);
|
|
481
|
+
} catch (cause) {
|
|
482
|
+
setError(cause?.message ?? "群聊响应方式修改失败,请重试。");
|
|
483
|
+
} finally {
|
|
484
|
+
setSaving(false);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const authorize = async () => {
|
|
489
|
+
if (current !== "all" || saving || authorizing || disabled || authorizationDisabled) return;
|
|
490
|
+
setAuthorizing(true);
|
|
491
|
+
setError(null);
|
|
492
|
+
try {
|
|
493
|
+
await onAuthorize?.();
|
|
494
|
+
} catch (cause) {
|
|
495
|
+
setError(cause?.message ?? "群消息权限授权失败,请重试。");
|
|
496
|
+
} finally {
|
|
497
|
+
setAuthorizing(false);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return h("div", { className: "bxf-responseMode dim-responseMode" },
|
|
502
|
+
h("div", { className: "bxf-responseModeHeader dim-responseModeHeader" },
|
|
503
|
+
h("span", null, "群聊响应方式"),
|
|
504
|
+
saving || authorizing
|
|
505
|
+
? h("span", { className: "bxf-responseModeStatus dim-responseModeStatus" },
|
|
506
|
+
saving ? "保存中…" : "正在准备授权…")
|
|
507
|
+
: null),
|
|
508
|
+
h("select", {
|
|
509
|
+
className: "bxf-responseModeSelect dim-responseModeSelect",
|
|
510
|
+
value: current,
|
|
511
|
+
disabled: disabled || saving,
|
|
512
|
+
"aria-label": "群聊响应方式",
|
|
513
|
+
onChange: (event) => { void change(event); },
|
|
514
|
+
},
|
|
515
|
+
h("option", { value: "mention" }, "仅在 @机器人时响应(推荐)"),
|
|
516
|
+
h("option", { value: "all" }, "响应所有群消息"),
|
|
517
|
+
),
|
|
518
|
+
h("small", { className: "bxf-responseModeHelp dim-responseModeHelp" },
|
|
519
|
+
current === "mention"
|
|
520
|
+
? permissionGranted
|
|
521
|
+
? "私聊始终响应;群聊仅处理明确 @当前机器人的消息。群消息权限已开通,再次切换无需授权。"
|
|
522
|
+
: "私聊始终响应;群聊仅处理明确 @当前机器人的消息。选择全部消息后会打开飞书官方授权流程。"
|
|
523
|
+
: permissionGranted
|
|
524
|
+
? "已开通“获取群组中所有消息”权限(im:message.group_msg);机器人会处理群聊中的所有可见消息。"
|
|
525
|
+
: "尚未确认“获取群组中所有消息”权限,请完成飞书授权。"),
|
|
526
|
+
current === "all"
|
|
527
|
+
? h("div", { className: "bxf-responseModePermissionAction dim-responseModePermissionAction" },
|
|
528
|
+
h(Button, {
|
|
529
|
+
className: "bxf-responseModePermissionButton",
|
|
530
|
+
size: "small",
|
|
531
|
+
disabled: disabled || authorizationDisabled || saving || authorizing,
|
|
532
|
+
"aria-busy": authorizing ? "true" : undefined,
|
|
533
|
+
"aria-label": permissionGranted ? "重新授权群消息权限" : "授权群消息权限",
|
|
534
|
+
onClick: () => { void authorize(); },
|
|
535
|
+
}, authorizing ? "正在准备…" : permissionGranted ? "重新授权" : "去授权"))
|
|
536
|
+
: null,
|
|
537
|
+
error ? h("p", {
|
|
538
|
+
className: "bxf-responseModeError dim-responseModeError",
|
|
539
|
+
role: "alert",
|
|
540
|
+
}, error) : null,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
416
544
|
export function BotCard({
|
|
417
545
|
connection,
|
|
418
546
|
busy,
|
|
419
547
|
repairDisabled,
|
|
548
|
+
provisionContent,
|
|
549
|
+
provisionRef,
|
|
420
550
|
actionError,
|
|
421
551
|
testNotice,
|
|
422
552
|
removing,
|
|
@@ -424,6 +554,8 @@ export function BotCard({
|
|
|
424
554
|
onRepairCallback,
|
|
425
555
|
onWorkspaceSave,
|
|
426
556
|
onAgentPresetSave,
|
|
557
|
+
onGroupResponseModeSave,
|
|
558
|
+
onGroupMessagePermissionAuthorize,
|
|
427
559
|
onRequestRemove,
|
|
428
560
|
onConfirmRemove,
|
|
429
561
|
onCancelRemove,
|
|
@@ -479,6 +611,23 @@ export function BotCard({
|
|
|
479
611
|
disabled: Boolean(busy),
|
|
480
612
|
onSave: onAgentPresetSave,
|
|
481
613
|
}),
|
|
614
|
+
h(GroupResponseModeEditor, {
|
|
615
|
+
value: connection.groupResponseMode,
|
|
616
|
+
permissionGranted: connection.groupMessagePermissionGranted,
|
|
617
|
+
disabled: Boolean(busy),
|
|
618
|
+
authorizationDisabled: repairDisabled,
|
|
619
|
+
onSave: onGroupResponseModeSave,
|
|
620
|
+
onAuthorize: onGroupMessagePermissionAuthorize,
|
|
621
|
+
}),
|
|
622
|
+
provisionContent
|
|
623
|
+
? h("section", {
|
|
624
|
+
className: "bxf-botProvision dim-botProvision",
|
|
625
|
+
"aria-label": `${bot.name}的飞书授权流程`,
|
|
626
|
+
"data-provision-for": connection.botId,
|
|
627
|
+
ref: provisionRef,
|
|
628
|
+
tabIndex: -1,
|
|
629
|
+
}, provisionContent)
|
|
630
|
+
: null,
|
|
482
631
|
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
|
483
632
|
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
|
484
633
|
summary) : null,
|
|
@@ -526,9 +675,14 @@ function BotList(props) {
|
|
|
526
675
|
h(BotCard, {
|
|
527
676
|
connection: bot,
|
|
528
677
|
busy: props.busyByBot[bot.botId]
|
|
529
|
-
?? (
|
|
530
|
-
&& props.provisioning.botId === bot.botId ?
|
|
678
|
+
?? (isTargetedAppUpdate(props.provisioning)
|
|
679
|
+
&& props.provisioning.botId === bot.botId ? props.provisioning.operation : undefined),
|
|
531
680
|
repairDisabled: Boolean(props.provisioning),
|
|
681
|
+
provisionContent: isTargetedAppUpdate(props.provisioning)
|
|
682
|
+
&& props.provisioning.botId === bot.botId
|
|
683
|
+
? props.provisionContent
|
|
684
|
+
: null,
|
|
685
|
+
provisionRef: props.provisionRef,
|
|
532
686
|
actionError: props.errorsByBot[bot.botId],
|
|
533
687
|
testNotice: props.testNoticesByBot[bot.botId],
|
|
534
688
|
removing: props.removeTargetId === bot.botId,
|
|
@@ -536,6 +690,8 @@ function BotList(props) {
|
|
|
536
690
|
onRepairCallback: () => props.onRepairCallback(bot),
|
|
537
691
|
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
|
538
692
|
onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
|
|
693
|
+
onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
|
|
694
|
+
onGroupMessagePermissionAuthorize: () => props.onGroupMessagePermissionAuthorize(bot),
|
|
539
695
|
onRequestRemove: () => props.onRequestRemove(bot),
|
|
540
696
|
onConfirmRemove: () => props.onConfirmRemove(bot),
|
|
541
697
|
onCancelRemove: props.onCancelRemove,
|
|
@@ -616,6 +772,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
616
772
|
const [focusBotId, setFocusBotId] = React.useState(null);
|
|
617
773
|
const cardRefs = React.useRef(new Map());
|
|
618
774
|
const removeButtonRefs = React.useRef(new Map());
|
|
775
|
+
const targetedProvisionRef = React.useRef(null);
|
|
619
776
|
const addButtonRef = React.useRef(null);
|
|
620
777
|
const mountedRef = React.useRef(true);
|
|
621
778
|
const workspaceFence = useWorkspaceSnapshotFence();
|
|
@@ -706,15 +863,30 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
706
863
|
setFocusBotId(null);
|
|
707
864
|
}, [focusBotId, model.bots]);
|
|
708
865
|
|
|
866
|
+
const targetedProvisionFocusKey = isTargetedAppUpdate(model.provisioning)
|
|
867
|
+
? `${model.provisioning.botId}:${model.provisioning.attemptId ?? "preparing"}:${model.provisioning.phase}`
|
|
868
|
+
: null;
|
|
869
|
+
React.useEffect(() => {
|
|
870
|
+
if (!targetedProvisionFocusKey) return;
|
|
871
|
+
scheduleAnimationFrame(() => {
|
|
872
|
+
const node = targetedProvisionRef.current;
|
|
873
|
+
if (!node) return;
|
|
874
|
+
node.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
|
|
875
|
+
node.focus?.({ preventScroll: true });
|
|
876
|
+
}, "targeted-provision-focus");
|
|
877
|
+
}, [scheduleAnimationFrame, targetedProvisionFocusKey]);
|
|
878
|
+
|
|
709
879
|
const startProvisioning = React.useCallback(async ({
|
|
710
880
|
replace = false,
|
|
711
881
|
operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
|
|
712
882
|
bot,
|
|
713
883
|
} = {}) => {
|
|
714
884
|
const repairing = operation === CALLBACK_REPAIR_OPERATION;
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
|
|
885
|
+
const grantingGroupMessages = operation === GROUP_MESSAGE_PERMISSION_OPERATION;
|
|
886
|
+
const targetedUpdate = repairing || grantingGroupMessages;
|
|
887
|
+
const botId = targetedUpdate ? bot?.botId ?? model.provisioning?.botId : undefined;
|
|
888
|
+
const botName = targetedUpdate ? bot?.bot?.name ?? model.provisioning?.botName : undefined;
|
|
889
|
+
if (targetedUpdate && !botId) return;
|
|
718
890
|
setCredentialOpen(false);
|
|
719
891
|
setCredentialError(null);
|
|
720
892
|
setProvisionBusy(true);
|
|
@@ -742,13 +914,20 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
742
914
|
// Continue with begin. It is the source of truth for the new attempt.
|
|
743
915
|
}
|
|
744
916
|
}
|
|
917
|
+
const endpoint = repairing
|
|
918
|
+
? FEISHU_ENDPOINTS.beginCallbackRepair
|
|
919
|
+
: grantingGroupMessages
|
|
920
|
+
? FEISHU_ENDPOINTS.beginGroupMessagePermission
|
|
921
|
+
: FEISHU_ENDPOINTS.beginProvisioning;
|
|
745
922
|
const provision = normalizeProvisioning(await invoke(
|
|
746
|
-
|
|
747
|
-
|
|
923
|
+
endpoint,
|
|
924
|
+
targetedUpdate ? { botId } : { locale: "zh-CN" },
|
|
748
925
|
));
|
|
749
|
-
if (
|
|
750
|
-
&& (provision.operation !==
|
|
751
|
-
throw new Error(
|
|
926
|
+
if (targetedUpdate
|
|
927
|
+
&& (provision.operation !== operation || provision.botId !== botId)) {
|
|
928
|
+
throw new Error(grantingGroupMessages
|
|
929
|
+
? "飞书服务返回了不匹配的群消息权限二维码"
|
|
930
|
+
: "飞书服务返回了不匹配的卡片修复二维码");
|
|
752
931
|
}
|
|
753
932
|
const timestamp = Date.now();
|
|
754
933
|
setNow(timestamp);
|
|
@@ -764,7 +943,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
764
943
|
}));
|
|
765
944
|
announce(repairing
|
|
766
945
|
? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
|
|
767
|
-
:
|
|
946
|
+
: grantingGroupMessages
|
|
947
|
+
? `${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。`
|
|
948
|
+
: "授权二维码已生成,请使用飞书扫码。");
|
|
768
949
|
} catch (error) {
|
|
769
950
|
setModel((current) => ({
|
|
770
951
|
...current,
|
|
@@ -815,7 +996,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
815
996
|
const activeProvision = model.provisioning;
|
|
816
997
|
const attemptId = activeProvision?.attemptId;
|
|
817
998
|
const repairing = isCallbackRepair(activeProvision);
|
|
818
|
-
const
|
|
999
|
+
const grantingGroupMessages = isGroupMessagePermission(activeProvision);
|
|
1000
|
+
const targetedUpdate = isTargetedAppUpdate(activeProvision);
|
|
1001
|
+
const targetBot = targetedUpdate
|
|
819
1002
|
? model.bots.find((bot) => bot.botId === activeProvision?.botId)
|
|
820
1003
|
: undefined;
|
|
821
1004
|
setProvisionBusy(true);
|
|
@@ -823,8 +1006,8 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
823
1006
|
const result = attemptId
|
|
824
1007
|
? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId }))
|
|
825
1008
|
: null;
|
|
826
|
-
if (
|
|
827
|
-
if (result.operation !==
|
|
1009
|
+
if (targetedUpdate && result) {
|
|
1010
|
+
if (result.operation !== activeProvision.operation
|
|
828
1011
|
|| result.botId !== activeProvision.botId) {
|
|
829
1012
|
throw new Error("飞书服务返回了不匹配的注册进度");
|
|
830
1013
|
}
|
|
@@ -841,23 +1024,29 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
841
1024
|
},
|
|
842
1025
|
}
|
|
843
1026
|
: current);
|
|
844
|
-
announce(
|
|
1027
|
+
announce(grantingGroupMessages
|
|
1028
|
+
? "权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
|
1029
|
+
: "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
|
|
845
1030
|
return;
|
|
846
1031
|
}
|
|
847
1032
|
if (result.status === "connected") {
|
|
848
1033
|
const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "机器人";
|
|
849
1034
|
setModel((current) => ({ ...current, provisioning: null }));
|
|
850
|
-
announce(
|
|
1035
|
+
announce(grantingGroupMessages
|
|
1036
|
+
? `${targetBotName}已开通群消息权限,并启用“响应所有群消息”。`
|
|
1037
|
+
: `${targetBotName}的卡片按钮已修复。`);
|
|
851
1038
|
if (activeProvision.botId) setFocusBotId(activeProvision.botId);
|
|
852
1039
|
await loadStatus({ silent: true, restoreProvisioning: false });
|
|
853
1040
|
return;
|
|
854
1041
|
}
|
|
855
1042
|
}
|
|
856
1043
|
setModel((current) => ({ ...current, provisioning: null }));
|
|
857
|
-
announce(repairing
|
|
1044
|
+
announce(repairing
|
|
1045
|
+
? "已取消卡片按钮修复。"
|
|
1046
|
+
: grantingGroupMessages ? "已取消群消息权限授权。" : "已取消添加机器人。");
|
|
858
1047
|
await loadStatus({ silent: true, restoreProvisioning: false });
|
|
859
1048
|
scheduleAnimationFrame(() => {
|
|
860
|
-
if (
|
|
1049
|
+
if (targetedUpdate && activeProvision.botId) {
|
|
861
1050
|
cardRefs.current.get(activeProvision.botId)?.focus();
|
|
862
1051
|
} else {
|
|
863
1052
|
addButtonRef.current?.focus();
|
|
@@ -914,7 +1103,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
914
1103
|
controller.signal,
|
|
915
1104
|
));
|
|
916
1105
|
if (result.operation !== provision.operation
|
|
917
|
-
|| (
|
|
1106
|
+
|| (isTargetedAppUpdate(provision) && result.botId !== provision.botId)) {
|
|
918
1107
|
throw new Error("飞书服务返回了不匹配的注册进度");
|
|
919
1108
|
}
|
|
920
1109
|
if (result.status === "connected") {
|
|
@@ -923,7 +1112,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
923
1112
|
if (!snapshot) {
|
|
924
1113
|
throw new Error(isCallbackRepair(provision)
|
|
925
1114
|
? "卡片按钮已更新,但暂时无法确认机器人连接状态"
|
|
926
|
-
:
|
|
1115
|
+
: isGroupMessagePermission(provision)
|
|
1116
|
+
? "群消息权限已更新,但暂时无法确认机器人连接状态"
|
|
1117
|
+
: "机器人已经创建,但暂时无法确认连接状态");
|
|
927
1118
|
}
|
|
928
1119
|
if (!targetBot?.connected) {
|
|
929
1120
|
setModel((current) => current.provisioning?.attemptId === provision.attemptId
|
|
@@ -934,15 +1125,21 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
934
1125
|
setModel((current) => ({ ...current, provisioning: null }));
|
|
935
1126
|
announce(isCallbackRepair(provision)
|
|
936
1127
|
? `${targetBot.bot.name}的卡片按钮已修复。`
|
|
937
|
-
:
|
|
938
|
-
? `${targetBot.bot.name}
|
|
939
|
-
:
|
|
1128
|
+
: isGroupMessagePermission(provision)
|
|
1129
|
+
? `${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。`
|
|
1130
|
+
: targetBot
|
|
1131
|
+
? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
|
|
1132
|
+
: "新飞书机器人已连接,可以开始聊天。");
|
|
940
1133
|
if (result.botId) setFocusBotId(result.botId);
|
|
941
1134
|
return;
|
|
942
1135
|
}
|
|
943
1136
|
if (result.status === "failed") {
|
|
944
1137
|
const error = new Error(result.message
|
|
945
|
-
?? (isCallbackRepair(provision)
|
|
1138
|
+
?? (isCallbackRepair(provision)
|
|
1139
|
+
? "飞书卡片按钮修复失败"
|
|
1140
|
+
: isGroupMessagePermission(provision)
|
|
1141
|
+
? "飞书群消息权限开通失败"
|
|
1142
|
+
: "飞书应用创建失败"));
|
|
946
1143
|
error.code = "FEISHU_PROVISION_FAILED";
|
|
947
1144
|
throw error;
|
|
948
1145
|
}
|
|
@@ -1101,6 +1298,56 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
1101
1298
|
}
|
|
1102
1299
|
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
|
1103
1300
|
|
|
1301
|
+
const authorizeGroupMessages = React.useCallback(async (connection) => {
|
|
1302
|
+
const { botId } = connection;
|
|
1303
|
+
if (model.provisioning) {
|
|
1304
|
+
throw new Error("请先完成当前飞书授权操作,再开通群消息权限。");
|
|
1305
|
+
}
|
|
1306
|
+
setRemoveTargetId(null);
|
|
1307
|
+
setBotError(botId, null);
|
|
1308
|
+
setTestNoticesByBot((current) => {
|
|
1309
|
+
const next = { ...current };
|
|
1310
|
+
delete next[botId];
|
|
1311
|
+
return next;
|
|
1312
|
+
});
|
|
1313
|
+
await startProvisioning({
|
|
1314
|
+
operation: GROUP_MESSAGE_PERMISSION_OPERATION,
|
|
1315
|
+
bot: connection,
|
|
1316
|
+
});
|
|
1317
|
+
}, [model.provisioning, setBotError, startProvisioning]);
|
|
1318
|
+
|
|
1319
|
+
const saveGroupResponseMode = React.useCallback(async (connection, groupResponseMode) => {
|
|
1320
|
+
const { botId } = connection;
|
|
1321
|
+
if (groupResponseMode === "all" && connection.groupMessagePermissionGranted !== true) {
|
|
1322
|
+
await authorizeGroupMessages(connection);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
1326
|
+
setBotBusy(botId, "group-response-mode");
|
|
1327
|
+
setBotError(botId, null);
|
|
1328
|
+
try {
|
|
1329
|
+
const snapshot = normalizeBotsSnapshot(await invoke(
|
|
1330
|
+
FEISHU_ENDPOINTS.setGroupResponseMode,
|
|
1331
|
+
{ botId, groupResponseMode },
|
|
1332
|
+
));
|
|
1333
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
1334
|
+
mergeSnapshot(snapshot);
|
|
1335
|
+
}
|
|
1336
|
+
} finally {
|
|
1337
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
1338
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
1339
|
+
if (mountedRef.current) setBotBusy(botId, null);
|
|
1340
|
+
}
|
|
1341
|
+
}, [
|
|
1342
|
+
invoke,
|
|
1343
|
+
authorizeGroupMessages,
|
|
1344
|
+
loadStatus,
|
|
1345
|
+
mergeSnapshot,
|
|
1346
|
+
setBotBusy,
|
|
1347
|
+
setBotError,
|
|
1348
|
+
workspaceFence,
|
|
1349
|
+
]);
|
|
1350
|
+
|
|
1104
1351
|
const requestRemove = React.useCallback((connection) => {
|
|
1105
1352
|
setRemoveTargetId(connection.botId);
|
|
1106
1353
|
}, []);
|
|
@@ -1138,6 +1385,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
1138
1385
|
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
|
|
1139
1386
|
|
|
1140
1387
|
const provision = model.provisioning;
|
|
1388
|
+
const targetedProvisioning = isTargetedAppUpdate(provision);
|
|
1141
1389
|
const provisionBot = provision?.botId
|
|
1142
1390
|
? model.bots.find((bot) => bot.botId === provision.botId)
|
|
1143
1391
|
?? { botId: provision.botId, bot: { name: provision.botName ?? "此机器人" } }
|
|
@@ -1163,7 +1411,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
1163
1411
|
provisionContent = h(ProvisionProgress, {
|
|
1164
1412
|
phase: "connecting",
|
|
1165
1413
|
provision,
|
|
1166
|
-
onCancel:
|
|
1414
|
+
onCancel: isTargetedAppUpdate(provision) ? undefined : () => void cancelProvisioning(),
|
|
1167
1415
|
busy: provisionBusy,
|
|
1168
1416
|
});
|
|
1169
1417
|
} else if (provision?.phase === "error") {
|
|
@@ -1238,7 +1486,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
1238
1486
|
})
|
|
1239
1487
|
: h(React.Fragment, null,
|
|
1240
1488
|
credentialContent,
|
|
1241
|
-
provisionContent,
|
|
1489
|
+
targetedProvisioning ? null : provisionContent,
|
|
1242
1490
|
model.bots.length === 0 && !provision && !credentialOpen
|
|
1243
1491
|
? h(EmptyView, { onStart: () => void startProvisioning(), busy: provisionBusy })
|
|
1244
1492
|
: null,
|
|
@@ -1250,10 +1498,14 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
1250
1498
|
testNoticesByBot,
|
|
1251
1499
|
removeTargetId,
|
|
1252
1500
|
provisioning: provision,
|
|
1501
|
+
provisionContent,
|
|
1502
|
+
provisionRef: targetedProvisionRef,
|
|
1253
1503
|
onReconnect: (bot) => void reconnectOneBot(bot),
|
|
1254
1504
|
onRepairCallback: repairCallback,
|
|
1255
1505
|
onWorkspaceSave: saveWorkspace,
|
|
1256
1506
|
onAgentPresetSave: saveAgentPreset,
|
|
1507
|
+
onGroupResponseModeSave: saveGroupResponseMode,
|
|
1508
|
+
onGroupMessagePermissionAuthorize: authorizeGroupMessages,
|
|
1257
1509
|
onRequestRemove: requestRemove,
|
|
1258
1510
|
onConfirmRemove: (bot) => void confirmRemove(bot),
|
|
1259
1511
|
onCancelRemove: cancelRemove,
|