@xmanrui/dsh-im 0.14.0 → 0.16.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/lib/client.js +246 -46
- package/lib/index.js +137 -131
- package/package.json +1 -1
- package/plugin-src/client/channels/feishu/api.js +23 -1
- package/plugin-src/client/channels/feishu/index.js +235 -46
- package/plugin-src/client/channels/feishu/styles.js +2 -0
- package/plugin-src/client/i18n.js +39 -0
- package/plugin-src/client/index.js +2 -1
- package/plugin-src/client/styles.js +2 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +146 -12
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +723 -1
- package/src/channels/feishu/feishu-cards.mjs +155 -0
- package/src/channels/feishu/feishu-runtime.mjs +198 -0
- package/src/channels/feishu/multi-bot-controller.mjs +357 -2
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/feishu/repair-manager.mjs +109 -0
- package/src/channels/shared/workspace-command.mjs +2 -2
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-controller.mjs +56 -12
- package/src/channels/weixin/weixin-runtime.mjs +12 -1
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ export const FEISHU_RPC_CHANNEL = "/feishu";
|
|
|
11
11
|
export const FEISHU_ENDPOINTS = Object.freeze({
|
|
12
12
|
status: "connection.status",
|
|
13
13
|
beginProvisioning: "provision.begin",
|
|
14
|
+
beginCallbackRepair: "bot.callback-repair.begin",
|
|
14
15
|
pollProvisioning: "provision.poll",
|
|
15
16
|
cancelProvisioning: "provision.cancel",
|
|
16
17
|
bindCredentials: "bot.bind-credentials",
|
|
@@ -23,6 +24,11 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|
|
23
24
|
disconnect: "connection.disconnect",
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
export const FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
|
|
28
|
+
PROVISION: "provision",
|
|
29
|
+
CALLBACK_REPAIR: "callback_repair",
|
|
30
|
+
});
|
|
31
|
+
|
|
26
32
|
const CONNECTION_STATES = new Set([
|
|
27
33
|
"disconnected",
|
|
28
34
|
"offline",
|
|
@@ -67,6 +73,12 @@ function clamp(value, min, max, fallback) {
|
|
|
67
73
|
: fallback;
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
function normalizeRegistrationOperation(value) {
|
|
77
|
+
return value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
|
|
78
|
+
? FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
|
|
79
|
+
: FEISHU_REGISTRATION_OPERATIONS.PROVISION;
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
export function unwrapRpcResult(result) {
|
|
71
83
|
if (!isRecord(result) || typeof result.ok !== "boolean") {
|
|
72
84
|
throw new Error("飞书服务返回了无法识别的响应");
|
|
@@ -88,16 +100,25 @@ export function normalizeProvisioning(value, now = Date.now()) {
|
|
|
88
100
|
?? optionalString(source.provisioningId);
|
|
89
101
|
const verificationUrl = optionalString(source.verificationUrl);
|
|
90
102
|
const qrCodeDataUrl = optionalString(source.qrCodeDataUrl);
|
|
91
|
-
|
|
103
|
+
const submitted = source.submitted === true;
|
|
104
|
+
if (!attemptId || (!verificationUrl && !qrCodeDataUrl && !submitted)) {
|
|
92
105
|
throw new Error("飞书服务返回的二维码信息不完整");
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
const explicitExpiry = optionalTimestamp(source.expiresAt);
|
|
96
109
|
const expireIn = clamp(source.expireIn, 1, 60 * 60, 5 * 60);
|
|
110
|
+
const operation = normalizeRegistrationOperation(source.operation);
|
|
111
|
+
const botId = optionalString(source.botId);
|
|
112
|
+
if (operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR && !botId) {
|
|
113
|
+
throw new Error("飞书服务返回的修复信息缺少 botId");
|
|
114
|
+
}
|
|
97
115
|
return {
|
|
98
116
|
attemptId,
|
|
117
|
+
operation,
|
|
118
|
+
botId,
|
|
99
119
|
verificationUrl,
|
|
100
120
|
qrCodeDataUrl,
|
|
121
|
+
submitted,
|
|
101
122
|
expiresAt: explicitExpiry ?? now + expireIn * 1000,
|
|
102
123
|
pollIntervalMs: clamp(source.pollIntervalMs, 800, 10_000, 1_800),
|
|
103
124
|
};
|
|
@@ -296,6 +317,7 @@ export function normalizePollResult(value) {
|
|
|
296
317
|
|
|
297
318
|
const normalized = {
|
|
298
319
|
status,
|
|
320
|
+
operation: normalizeRegistrationOperation(value.operation),
|
|
299
321
|
botId: optionalString(value.botId),
|
|
300
322
|
message: optionalString(value.error?.message) ?? optionalString(value.message),
|
|
301
323
|
connection: undefined,
|
|
@@ -5,6 +5,7 @@ import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from "../.
|
|
|
5
5
|
import { h } from "../../i18n.js";
|
|
6
6
|
import {
|
|
7
7
|
FEISHU_ENDPOINTS,
|
|
8
|
+
FEISHU_REGISTRATION_OPERATIONS,
|
|
8
9
|
FEISHU_RPC_CHANNEL,
|
|
9
10
|
formatRemaining,
|
|
10
11
|
normalizeBotsSnapshot,
|
|
@@ -21,6 +22,12 @@ import { installFeishuStyles } from "./styles.js";
|
|
|
21
22
|
export const name = "feishu-settings";
|
|
22
23
|
export const inject = ["slots", "connection"];
|
|
23
24
|
|
|
25
|
+
const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
|
26
|
+
|
|
27
|
+
function isCallbackRepair(value) {
|
|
28
|
+
return value?.operation === CALLBACK_REPAIR_OPERATION;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
|
|
25
32
|
return h("svg", {
|
|
26
33
|
width: size,
|
|
@@ -197,7 +204,18 @@ function safeVerificationHref(value) {
|
|
|
197
204
|
if (!value) return undefined;
|
|
198
205
|
try {
|
|
199
206
|
const url = new URL(value);
|
|
200
|
-
return url.protocol === "https:"
|
|
207
|
+
return url.protocol === "https:"
|
|
208
|
+
&& [
|
|
209
|
+
"accounts.feishu.cn",
|
|
210
|
+
"accounts.larksuite.com",
|
|
211
|
+
"open.feishu.cn",
|
|
212
|
+
"open.larksuite.com",
|
|
213
|
+
].includes(url.hostname)
|
|
214
|
+
&& !url.port
|
|
215
|
+
&& !url.username
|
|
216
|
+
&& !url.password
|
|
217
|
+
? url.toString()
|
|
218
|
+
: undefined;
|
|
201
219
|
} catch {
|
|
202
220
|
return undefined;
|
|
203
221
|
}
|
|
@@ -217,6 +235,8 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
217
235
|
const remaining = Math.max(0, provision.expiresAt - now);
|
|
218
236
|
const expired = provision.expired === true || remaining === 0;
|
|
219
237
|
const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
|
|
238
|
+
const repairing = isCallbackRepair(provision);
|
|
239
|
+
const botName = provision.botName ?? "此机器人";
|
|
220
240
|
|
|
221
241
|
React.useEffect(() => setImageFailed(false), [qrSource]);
|
|
222
242
|
|
|
@@ -225,9 +245,11 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
225
245
|
h("div", { className: "bxf-qrColumn dim-qrColumn" },
|
|
226
246
|
h("div", { className: "bxf-qrFrame dim-qrFrame" },
|
|
227
247
|
qrSource && !imageFailed
|
|
228
|
-
|
|
248
|
+
? h("img", {
|
|
229
249
|
src: qrSource,
|
|
230
|
-
alt:
|
|
250
|
+
alt: repairing
|
|
251
|
+
? `用于修复${botName}卡片按钮的一次性授权二维码`
|
|
252
|
+
: "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
|
|
231
253
|
onError: () => setImageFailed(true),
|
|
232
254
|
})
|
|
233
255
|
: h("div", { className: "bxf-qrFallback dim-qrFallback" },
|
|
@@ -251,13 +273,21 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
251
273
|
h("div", { className: "bxf-qrCopy dim-qrCopy" },
|
|
252
274
|
h("div", { className: "bxf-stateLabel dim-stateLabel" },
|
|
253
275
|
h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
|
|
254
|
-
h("span", null, "正在添加新机器人")),
|
|
255
|
-
h("h3", null, expired
|
|
256
|
-
|
|
276
|
+
h("span", null, repairing ? `正在修复「${botName}」` : "正在添加新机器人")),
|
|
277
|
+
h("h3", null, expired
|
|
278
|
+
? "刷新二维码后继续"
|
|
279
|
+
: repairing ? "使用飞书扫码修复卡片按钮" : "使用飞书扫码创建机器人"),
|
|
280
|
+
h("p", null, repairing
|
|
281
|
+
? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
|
|
282
|
+
: "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
|
|
257
283
|
h("ol", { className: "bxf-steps dim-steps" },
|
|
258
284
|
h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
|
|
259
|
-
h("li", null,
|
|
260
|
-
|
|
285
|
+
h("li", null, repairing
|
|
286
|
+
? "核对现有应用名称,并确认只新增卡片回调"
|
|
287
|
+
: "核对应用名称与权限范围,并确认创建"),
|
|
288
|
+
h("li", null, repairing
|
|
289
|
+
? "保持本页打开,等待卡片按钮修复完成"
|
|
290
|
+
: "保持本页打开,等待新机器人的长连接就绪")),
|
|
261
291
|
h("div", { className: "bxf-actions dim-viewActions" },
|
|
262
292
|
expired
|
|
263
293
|
? h(Button, {
|
|
@@ -272,35 +302,43 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
|
|
272
302
|
!expired
|
|
273
303
|
? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
|
|
274
304
|
: null,
|
|
275
|
-
h(Button, { onClick: onCancel, disabled: busy }, "取消添加")),
|
|
305
|
+
h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加")),
|
|
276
306
|
),
|
|
277
307
|
),
|
|
278
308
|
);
|
|
279
309
|
}
|
|
280
310
|
|
|
281
|
-
function ProvisionProgress({ phase, onCancel, busy }) {
|
|
311
|
+
function ProvisionProgress({ phase, provision, onCancel, busy }) {
|
|
282
312
|
const connecting = phase === "connecting";
|
|
313
|
+
const repairing = isCallbackRepair(provision);
|
|
283
314
|
return h("div", {
|
|
284
315
|
className: "bxf-card bxf-provisionCard dim-surfaceCard dim-loadingView",
|
|
285
316
|
"aria-busy": "true",
|
|
286
317
|
},
|
|
287
318
|
h("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
|
288
|
-
h("h3", null, connecting
|
|
319
|
+
h("h3", null, connecting
|
|
320
|
+
? repairing ? "已确认,正在完成卡片按钮修复" : "已确认,正在连接新机器人"
|
|
321
|
+
: repairing ? "正在准备修复二维码" : "正在准备授权二维码"),
|
|
289
322
|
h("p", null, connecting
|
|
290
|
-
?
|
|
291
|
-
|
|
292
|
-
|
|
323
|
+
? repairing
|
|
324
|
+
? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
|
325
|
+
: "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
|
|
326
|
+
: repairing
|
|
327
|
+
? "正在为现有飞书应用申请一次性更新二维码,请稍候。"
|
|
328
|
+
: "正在向飞书申请一次性授权二维码,请稍候。"),
|
|
329
|
+
connecting && onCancel
|
|
293
330
|
? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
|
|
294
|
-
h(Button, { onClick: onCancel, disabled: busy }, "取消添加"))
|
|
331
|
+
h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加"))
|
|
295
332
|
: null,
|
|
296
333
|
);
|
|
297
334
|
}
|
|
298
335
|
|
|
299
|
-
function ProvisionError({ error, onRetry, onCancel, busy }) {
|
|
336
|
+
function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
|
|
337
|
+
const repairing = isCallbackRepair(provision);
|
|
300
338
|
return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
|
301
339
|
h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
|
|
302
340
|
h("div", null,
|
|
303
|
-
h("h3", null, "新机器人没有添加完成"),
|
|
341
|
+
h("h3", null, repairing ? "卡片按钮没有修复完成" : "新机器人没有添加完成"),
|
|
304
342
|
h("p", null, error.message),
|
|
305
343
|
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
|
306
344
|
h("div", { className: "bxf-actions dim-viewActions" },
|
|
@@ -373,10 +411,12 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
|
|
|
373
411
|
export function BotCard({
|
|
374
412
|
connection,
|
|
375
413
|
busy,
|
|
414
|
+
repairDisabled,
|
|
376
415
|
actionError,
|
|
377
416
|
testNotice,
|
|
378
417
|
removing,
|
|
379
418
|
onReconnect,
|
|
419
|
+
onRepairCallback,
|
|
380
420
|
onWorkspaceSave,
|
|
381
421
|
onRequestRemove,
|
|
382
422
|
onConfirmRemove,
|
|
@@ -441,6 +481,13 @@ export function BotCard({
|
|
|
441
481
|
disabled: Boolean(busy), "aria-busy": busy === "reconnect" ? "true" : undefined,
|
|
442
482
|
"aria-label": `${connected ? "检查连接" : "重试连接"}${bot.name}`,
|
|
443
483
|
}, busy === "reconnect" ? (connected ? "检查中…" : "正在连接…") : connected ? "检查连接" : "重试连接"),
|
|
484
|
+
h(Button, {
|
|
485
|
+
className: "bxf-repairButton dim-cardAction",
|
|
486
|
+
onClick: onRepairCallback,
|
|
487
|
+
disabled: Boolean(busy) || repairDisabled,
|
|
488
|
+
"aria-busy": busy === "callback-repair" ? "true" : undefined,
|
|
489
|
+
"aria-label": `修复${bot.name}的卡片按钮`,
|
|
490
|
+
}, busy === "callback-repair" ? "等待扫码…" : "修复卡片按钮"),
|
|
444
491
|
h(Button, {
|
|
445
492
|
className: "dim-cardAction", kind: "danger", onClick: onRequestRemove,
|
|
446
493
|
disabled: Boolean(busy), ref: removeButtonRef,
|
|
@@ -467,11 +514,15 @@ function BotList(props) {
|
|
|
467
514
|
props.bots.map((bot) => h("li", { key: bot.botId },
|
|
468
515
|
h(BotCard, {
|
|
469
516
|
connection: bot,
|
|
470
|
-
busy: props.busyByBot[bot.botId]
|
|
517
|
+
busy: props.busyByBot[bot.botId]
|
|
518
|
+
?? (isCallbackRepair(props.provisioning)
|
|
519
|
+
&& props.provisioning.botId === bot.botId ? "callback-repair" : undefined),
|
|
520
|
+
repairDisabled: Boolean(props.provisioning),
|
|
471
521
|
actionError: props.errorsByBot[bot.botId],
|
|
472
522
|
testNotice: props.testNoticesByBot[bot.botId],
|
|
473
523
|
removing: props.removeTargetId === bot.botId,
|
|
474
524
|
onReconnect: () => props.onReconnect(bot),
|
|
525
|
+
onRepairCallback: () => props.onRepairCallback(bot),
|
|
475
526
|
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
|
476
527
|
onRequestRemove: () => props.onRequestRemove(bot),
|
|
477
528
|
onConfirmRemove: () => props.onConfirmRemove(bot),
|
|
@@ -507,11 +558,12 @@ export function mergeFeishuSnapshotState(
|
|
|
507
558
|
if (snapshot.revision > 0 && current.revision > snapshot.revision) return current;
|
|
508
559
|
let provisioning = current.provisioning;
|
|
509
560
|
if (!provisioning && restoreProvisioning && snapshot.provisioning) {
|
|
561
|
+
const submitted = snapshot.provisioning.submitted === true;
|
|
510
562
|
provisioning = {
|
|
511
|
-
phase: snapshot.state === "connecting" ? "connecting" : "qr",
|
|
563
|
+
phase: submitted || snapshot.state === "connecting" ? "connecting" : "qr",
|
|
512
564
|
...snapshot.provisioning,
|
|
513
565
|
durationMs: Math.max(1, snapshot.provisioning.expiresAt - now),
|
|
514
|
-
expired: snapshot.provisioning.expiresAt <= now,
|
|
566
|
+
expired: !submitted && snapshot.provisioning.expiresAt <= now,
|
|
515
567
|
};
|
|
516
568
|
}
|
|
517
569
|
return {
|
|
@@ -640,7 +692,15 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
640
692
|
setFocusBotId(null);
|
|
641
693
|
}, [focusBotId, model.bots]);
|
|
642
694
|
|
|
643
|
-
const startProvisioning = React.useCallback(async ({
|
|
695
|
+
const startProvisioning = React.useCallback(async ({
|
|
696
|
+
replace = false,
|
|
697
|
+
operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
|
|
698
|
+
bot,
|
|
699
|
+
} = {}) => {
|
|
700
|
+
const repairing = operation === CALLBACK_REPAIR_OPERATION;
|
|
701
|
+
const botId = repairing ? bot?.botId ?? model.provisioning?.botId : undefined;
|
|
702
|
+
const botName = repairing ? bot?.bot?.name ?? model.provisioning?.botName : undefined;
|
|
703
|
+
if (repairing && !botId) return;
|
|
644
704
|
setCredentialOpen(false);
|
|
645
705
|
setCredentialError(null);
|
|
646
706
|
setProvisionBusy(true);
|
|
@@ -649,16 +709,33 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
649
709
|
setModel((current) => ({
|
|
650
710
|
...current,
|
|
651
711
|
phase: current.phase === "loading" ? "ready" : current.phase,
|
|
652
|
-
provisioning: {
|
|
712
|
+
provisioning: {
|
|
713
|
+
phase: "creating",
|
|
714
|
+
operation,
|
|
715
|
+
...(botId ? { botId } : {}),
|
|
716
|
+
...(botName ? { botName } : {}),
|
|
717
|
+
},
|
|
653
718
|
}));
|
|
654
719
|
try {
|
|
655
720
|
if (replace && previousAttemptId) {
|
|
656
|
-
|
|
721
|
+
// A Host restart intentionally drops its in-memory registration map.
|
|
722
|
+
// Replacing a stale browser attempt must still be able to start a new
|
|
723
|
+
// authoritative attempt; both controller start paths already
|
|
724
|
+
// supersede/deduplicate a still-live registration safely.
|
|
725
|
+
try {
|
|
726
|
+
await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId: previousAttemptId });
|
|
727
|
+
} catch {
|
|
728
|
+
// Continue with begin. It is the source of truth for the new attempt.
|
|
729
|
+
}
|
|
657
730
|
}
|
|
658
731
|
const provision = normalizeProvisioning(await invoke(
|
|
659
|
-
FEISHU_ENDPOINTS.beginProvisioning,
|
|
660
|
-
{ locale: "zh-CN" },
|
|
732
|
+
repairing ? FEISHU_ENDPOINTS.beginCallbackRepair : FEISHU_ENDPOINTS.beginProvisioning,
|
|
733
|
+
repairing ? { botId } : { locale: "zh-CN" },
|
|
661
734
|
));
|
|
735
|
+
if (repairing
|
|
736
|
+
&& (provision.operation !== CALLBACK_REPAIR_OPERATION || provision.botId !== botId)) {
|
|
737
|
+
throw new Error("飞书服务返回了不匹配的卡片修复二维码");
|
|
738
|
+
}
|
|
662
739
|
const timestamp = Date.now();
|
|
663
740
|
setNow(timestamp);
|
|
664
741
|
setModel((current) => ({
|
|
@@ -666,20 +743,36 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
666
743
|
provisioning: {
|
|
667
744
|
phase: "qr",
|
|
668
745
|
...provision,
|
|
746
|
+
...(botName ? { botName } : {}),
|
|
669
747
|
durationMs: Math.max(1, provision.expiresAt - timestamp),
|
|
670
748
|
expired: false,
|
|
671
749
|
},
|
|
672
750
|
}));
|
|
673
|
-
announce(
|
|
751
|
+
announce(repairing
|
|
752
|
+
? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
|
|
753
|
+
: "授权二维码已生成,请使用飞书扫码。");
|
|
674
754
|
} catch (error) {
|
|
675
755
|
setModel((current) => ({
|
|
676
756
|
...current,
|
|
677
|
-
provisioning: {
|
|
757
|
+
provisioning: {
|
|
758
|
+
phase: "error",
|
|
759
|
+
operation,
|
|
760
|
+
...(botId ? { botId } : {}),
|
|
761
|
+
...(botName ? { botName } : {}),
|
|
762
|
+
...(replace && previousAttemptId ? { attemptId: previousAttemptId } : {}),
|
|
763
|
+
error: presentError(error),
|
|
764
|
+
},
|
|
678
765
|
}));
|
|
679
766
|
} finally {
|
|
680
767
|
setProvisionBusy(false);
|
|
681
768
|
}
|
|
682
|
-
}, [
|
|
769
|
+
}, [
|
|
770
|
+
announce,
|
|
771
|
+
invoke,
|
|
772
|
+
model.provisioning?.attemptId,
|
|
773
|
+
model.provisioning?.botId,
|
|
774
|
+
model.provisioning?.botName,
|
|
775
|
+
]);
|
|
683
776
|
|
|
684
777
|
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
|
685
778
|
const snapshotVersion = workspaceFence.beginMutation();
|
|
@@ -705,23 +798,71 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
705
798
|
}, [announce, invoke, loadStatus, mergeSnapshot, workspaceFence]);
|
|
706
799
|
|
|
707
800
|
const cancelProvisioning = React.useCallback(async () => {
|
|
708
|
-
const
|
|
801
|
+
const activeProvision = model.provisioning;
|
|
802
|
+
const attemptId = activeProvision?.attemptId;
|
|
803
|
+
const repairing = isCallbackRepair(activeProvision);
|
|
804
|
+
const targetBot = repairing
|
|
805
|
+
? model.bots.find((bot) => bot.botId === activeProvision?.botId)
|
|
806
|
+
: undefined;
|
|
709
807
|
setProvisionBusy(true);
|
|
710
808
|
try {
|
|
711
|
-
|
|
809
|
+
const result = attemptId
|
|
810
|
+
? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId }))
|
|
811
|
+
: null;
|
|
812
|
+
if (repairing && result) {
|
|
813
|
+
if (result.operation !== CALLBACK_REPAIR_OPERATION
|
|
814
|
+
|| result.botId !== activeProvision.botId) {
|
|
815
|
+
throw new Error("飞书服务返回了不匹配的注册进度");
|
|
816
|
+
}
|
|
817
|
+
if (result.status === "connecting") {
|
|
818
|
+
setModel((current) => current.provisioning?.attemptId === attemptId
|
|
819
|
+
? {
|
|
820
|
+
...current,
|
|
821
|
+
provisioning: {
|
|
822
|
+
...current.provisioning,
|
|
823
|
+
...(result.provisioning ?? {}),
|
|
824
|
+
phase: "connecting",
|
|
825
|
+
submitted: true,
|
|
826
|
+
expired: false,
|
|
827
|
+
},
|
|
828
|
+
}
|
|
829
|
+
: current);
|
|
830
|
+
announce("配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (result.status === "connected") {
|
|
834
|
+
const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "机器人";
|
|
835
|
+
setModel((current) => ({ ...current, provisioning: null }));
|
|
836
|
+
announce(`${targetBotName}的卡片按钮已修复。`);
|
|
837
|
+
if (activeProvision.botId) setFocusBotId(activeProvision.botId);
|
|
838
|
+
await loadStatus({ silent: true, restoreProvisioning: false });
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
712
842
|
setModel((current) => ({ ...current, provisioning: null }));
|
|
713
|
-
announce("已取消添加机器人。");
|
|
843
|
+
announce(repairing ? "已取消卡片按钮修复。" : "已取消添加机器人。");
|
|
714
844
|
await loadStatus({ silent: true, restoreProvisioning: false });
|
|
715
|
-
scheduleAnimationFrame(() =>
|
|
845
|
+
scheduleAnimationFrame(() => {
|
|
846
|
+
if (repairing && activeProvision.botId) {
|
|
847
|
+
cardRefs.current.get(activeProvision.botId)?.focus();
|
|
848
|
+
} else {
|
|
849
|
+
addButtonRef.current?.focus();
|
|
850
|
+
}
|
|
851
|
+
}, "focus");
|
|
716
852
|
} catch (error) {
|
|
717
853
|
setModel((current) => ({
|
|
718
854
|
...current,
|
|
719
|
-
provisioning: {
|
|
855
|
+
provisioning: {
|
|
856
|
+
...activeProvision,
|
|
857
|
+
phase: "error",
|
|
858
|
+
attemptId,
|
|
859
|
+
error: presentError(error),
|
|
860
|
+
},
|
|
720
861
|
}));
|
|
721
862
|
} finally {
|
|
722
863
|
setProvisionBusy(false);
|
|
723
864
|
}
|
|
724
|
-
}, [announce, invoke, loadStatus, model.provisioning
|
|
865
|
+
}, [announce, invoke, loadStatus, model.bots, model.provisioning, scheduleAnimationFrame]);
|
|
725
866
|
|
|
726
867
|
const countdownAttemptId = model.provisioning?.attemptId;
|
|
727
868
|
const countdownPhase = model.provisioning?.phase;
|
|
@@ -758,27 +899,36 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
758
899
|
{ attemptId: provision.attemptId },
|
|
759
900
|
controller.signal,
|
|
760
901
|
));
|
|
902
|
+
if (result.operation !== provision.operation
|
|
903
|
+
|| (isCallbackRepair(provision) && result.botId !== provision.botId)) {
|
|
904
|
+
throw new Error("飞书服务返回了不匹配的注册进度");
|
|
905
|
+
}
|
|
761
906
|
if (result.status === "connected") {
|
|
762
907
|
const snapshot = await loadStatus({ signal: controller.signal, silent: true, restoreProvisioning: false });
|
|
763
|
-
const
|
|
908
|
+
const targetBot = snapshot?.bots.find((bot) => bot.botId === result.botId);
|
|
764
909
|
if (!snapshot) {
|
|
765
|
-
throw new Error(
|
|
910
|
+
throw new Error(isCallbackRepair(provision)
|
|
911
|
+
? "卡片按钮已更新,但暂时无法确认机器人连接状态"
|
|
912
|
+
: "机器人已经创建,但暂时无法确认连接状态");
|
|
766
913
|
}
|
|
767
|
-
if (!
|
|
914
|
+
if (!targetBot?.connected) {
|
|
768
915
|
setModel((current) => current.provisioning?.attemptId === provision.attemptId
|
|
769
916
|
? { ...current, provisioning: { ...current.provisioning, phase: "connecting" } }
|
|
770
917
|
: current);
|
|
771
918
|
return;
|
|
772
919
|
}
|
|
773
920
|
setModel((current) => ({ ...current, provisioning: null }));
|
|
774
|
-
announce(
|
|
775
|
-
? `${
|
|
776
|
-
:
|
|
921
|
+
announce(isCallbackRepair(provision)
|
|
922
|
+
? `${targetBot.bot.name}的卡片按钮已修复。`
|
|
923
|
+
: targetBot
|
|
924
|
+
? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
|
|
925
|
+
: "新飞书机器人已连接,可以开始聊天。");
|
|
777
926
|
if (result.botId) setFocusBotId(result.botId);
|
|
778
927
|
return;
|
|
779
928
|
}
|
|
780
929
|
if (result.status === "failed") {
|
|
781
|
-
const error = new Error(result.message
|
|
930
|
+
const error = new Error(result.message
|
|
931
|
+
?? (isCallbackRepair(provision) ? "飞书卡片按钮修复失败" : "飞书应用创建失败"));
|
|
782
932
|
error.code = "FEISHU_PROVISION_FAILED";
|
|
783
933
|
throw error;
|
|
784
934
|
}
|
|
@@ -806,6 +956,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
806
956
|
? {
|
|
807
957
|
...current,
|
|
808
958
|
provisioning: {
|
|
959
|
+
...current.provisioning,
|
|
809
960
|
phase: "error",
|
|
810
961
|
attemptId: provision.attemptId,
|
|
811
962
|
error: presentError(error),
|
|
@@ -838,6 +989,21 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
838
989
|
});
|
|
839
990
|
}, []);
|
|
840
991
|
|
|
992
|
+
const repairCallback = React.useCallback((connection) => {
|
|
993
|
+
if (model.provisioning) return;
|
|
994
|
+
setRemoveTargetId(null);
|
|
995
|
+
setBotError(connection.botId, null);
|
|
996
|
+
setTestNoticesByBot((current) => {
|
|
997
|
+
const next = { ...current };
|
|
998
|
+
delete next[connection.botId];
|
|
999
|
+
return next;
|
|
1000
|
+
});
|
|
1001
|
+
void startProvisioning({
|
|
1002
|
+
operation: CALLBACK_REPAIR_OPERATION,
|
|
1003
|
+
bot: connection,
|
|
1004
|
+
});
|
|
1005
|
+
}, [model.provisioning, setBotError, startProvisioning]);
|
|
1006
|
+
|
|
841
1007
|
const reconnectOneBot = React.useCallback(async (connection) => {
|
|
842
1008
|
const { botId, bot } = connection;
|
|
843
1009
|
const snapshotVersion = workspaceFence.beginMutation();
|
|
@@ -938,27 +1104,48 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
938
1104
|
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
|
|
939
1105
|
|
|
940
1106
|
const provision = model.provisioning;
|
|
1107
|
+
const provisionBot = provision?.botId
|
|
1108
|
+
? model.bots.find((bot) => bot.botId === provision.botId)
|
|
1109
|
+
?? { botId: provision.botId, bot: { name: provision.botName ?? "此机器人" } }
|
|
1110
|
+
: undefined;
|
|
1111
|
+
const restartProvisioning = ({ replace = false } = {}) => startProvisioning({
|
|
1112
|
+
replace,
|
|
1113
|
+
operation: provision?.operation ?? FEISHU_REGISTRATION_OPERATIONS.PROVISION,
|
|
1114
|
+
bot: provisionBot,
|
|
1115
|
+
});
|
|
941
1116
|
let provisionContent = null;
|
|
942
1117
|
if (provision?.phase === "creating") {
|
|
943
|
-
provisionContent = h(ProvisionProgress, {
|
|
1118
|
+
provisionContent = h(ProvisionProgress, {
|
|
1119
|
+
phase: "creating", provision, busy: provisionBusy,
|
|
1120
|
+
});
|
|
944
1121
|
} else if (provision?.phase === "qr") {
|
|
945
1122
|
provisionContent = h(QrPane, {
|
|
946
1123
|
provision, now,
|
|
947
|
-
onRefresh: () => void
|
|
1124
|
+
onRefresh: () => void restartProvisioning({ replace: true }),
|
|
948
1125
|
onCancel: () => void cancelProvisioning(),
|
|
949
1126
|
busy: provisionBusy || model.phase !== "ready",
|
|
950
1127
|
});
|
|
951
1128
|
} else if (provision?.phase === "connecting") {
|
|
952
1129
|
provisionContent = h(ProvisionProgress, {
|
|
953
1130
|
phase: "connecting",
|
|
954
|
-
|
|
1131
|
+
provision,
|
|
1132
|
+
onCancel: isCallbackRepair(provision) ? undefined : () => void cancelProvisioning(),
|
|
955
1133
|
busy: provisionBusy,
|
|
956
1134
|
});
|
|
957
1135
|
} else if (provision?.phase === "error") {
|
|
958
1136
|
provisionContent = h(ProvisionError, {
|
|
959
1137
|
error: provision.error,
|
|
960
|
-
|
|
961
|
-
|
|
1138
|
+
provision,
|
|
1139
|
+
onRetry: () => void restartProvisioning({ replace: Boolean(provision.attemptId) }),
|
|
1140
|
+
onCancel: () => {
|
|
1141
|
+
const targetBotId = provision.botId;
|
|
1142
|
+
setModel((current) => ({ ...current, provisioning: null }));
|
|
1143
|
+
void loadStatus({ silent: true, restoreProvisioning: false });
|
|
1144
|
+
scheduleAnimationFrame(() => {
|
|
1145
|
+
if (targetBotId) cardRefs.current.get(targetBotId)?.focus();
|
|
1146
|
+
else addButtonRef.current?.focus();
|
|
1147
|
+
}, "focus");
|
|
1148
|
+
},
|
|
962
1149
|
busy: provisionBusy,
|
|
963
1150
|
});
|
|
964
1151
|
}
|
|
@@ -1026,7 +1213,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
1026
1213
|
errorsByBot,
|
|
1027
1214
|
testNoticesByBot,
|
|
1028
1215
|
removeTargetId,
|
|
1216
|
+
provisioning: provision,
|
|
1029
1217
|
onReconnect: (bot) => void reconnectOneBot(bot),
|
|
1218
|
+
onRepairCallback: repairCallback,
|
|
1030
1219
|
onWorkspaceSave: saveWorkspace,
|
|
1031
1220
|
onRequestRemove: requestRemove,
|
|
1032
1221
|
onConfirmRemove: (bot) => void confirmRemove(bot),
|
|
@@ -412,6 +412,8 @@ const CSS = String.raw`
|
|
|
412
412
|
.bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
|
|
413
413
|
.bxf-botActions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
|
|
414
414
|
.bxf-botActions .bxf-button { flex: none; white-space: nowrap; }
|
|
415
|
+
.bxf-botActions .bxf-repairButton { color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 35%, var(--dsw-alias-border-l2, #dee0e3)); }
|
|
416
|
+
.bxf-botActions .bxf-repairButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
|
|
415
417
|
|
|
416
418
|
.bxf-confirm {
|
|
417
419
|
border-top: 1px solid var(--dsw-alias-border-l2, #dee0e3);
|
|
@@ -9,6 +9,7 @@ const EN = Object.freeze({
|
|
|
9
9
|
'IM 渠道': 'IM channels',
|
|
10
10
|
'让 DeepSeek Harness 触手可及': 'DeepSeek Harness, always within reach',
|
|
11
11
|
'AI Office': 'AI Office',
|
|
12
|
+
'(实验功能)': '(Experimental)',
|
|
12
13
|
'AI Office 设置': 'AI Office settings',
|
|
13
14
|
'AI Office 设置页缺少 RPC 连接': 'AI Office settings are missing an RPC connection',
|
|
14
15
|
'正在读取 AI Office Connector…': 'Loading AI Office Connector…',
|
|
@@ -196,6 +197,27 @@ const EN = Object.freeze({
|
|
|
196
197
|
'保持本页打开,等待新机器人的长连接就绪': 'Keep this page open until the bot connection is ready',
|
|
197
198
|
'在飞书中打开': 'Open in Feishu',
|
|
198
199
|
'取消添加': 'Cancel',
|
|
200
|
+
'使用飞书扫码修复卡片按钮': 'Scan with Feishu to repair card buttons',
|
|
201
|
+
'扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the card-button callback. It does not create a new app. This bot reconnects briefly after confirmation; other bots are not affected.',
|
|
202
|
+
'核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
|
|
203
|
+
'保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
|
|
204
|
+
'取消修复': 'Cancel repair',
|
|
205
|
+
'已确认,正在完成卡片按钮修复': 'Confirmed. Finishing card-button repair',
|
|
206
|
+
'正在准备修复二维码': 'Preparing the repair QR code',
|
|
207
|
+
'配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The update was submitted. Verifying the card callback and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
|
|
208
|
+
'正在为现有飞书应用申请一次性更新二维码,请稍候。': 'Requesting a one-time update QR code for the existing Feishu app…',
|
|
209
|
+
'卡片按钮没有修复完成': 'Card-button repair did not finish',
|
|
210
|
+
'修复卡片按钮': 'Repair card buttons',
|
|
211
|
+
'等待扫码…': 'Waiting for scan…',
|
|
212
|
+
'飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
|
|
213
|
+
'飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
|
|
214
|
+
'飞书服务返回了不匹配的注册进度': 'Feishu returned registration progress for a different operation',
|
|
215
|
+
'此机器人': 'this bot',
|
|
216
|
+
'${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
|
|
217
|
+
'${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
|
|
218
|
+
'已取消卡片按钮修复。': 'Card-button repair was cancelled.',
|
|
219
|
+
'卡片按钮已更新,但暂时无法确认机器人连接状态': 'The card callback was updated, but the bot connection could not be confirmed yet',
|
|
220
|
+
'飞书卡片按钮修复失败': 'Could not repair the Feishu card buttons',
|
|
199
221
|
'已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
|
|
200
222
|
'正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。': 'Saving credentials and checking the new bot connection. Existing bots will not be interrupted.',
|
|
201
223
|
'正在向飞书申请一次性授权二维码,请稍候。': 'Requesting a one-time authorization QR code from Feishu…',
|
|
@@ -230,6 +252,13 @@ const EN = Object.freeze({
|
|
|
230
252
|
'这是微信附加的安全确认步骤。配对码只用于本次扫码轮询,不会写入配置或日志。': 'This is an additional WeChat confirmation step. The pairing code is used only for this connection and is never stored.',
|
|
231
253
|
'正在保存凭据并验证 Harness 与微信长轮询。': 'Saving credentials and verifying the WeChat connection.',
|
|
232
254
|
'微信已确认,正在启动消息连接': 'Confirmed in WeChat. Starting the message connection',
|
|
255
|
+
'微信已授权,但无法读取现有登录凭据。请检查 DSH 凭据存储。': 'WeChat was authorized, but the existing login credential could not be read. Check the DSH credential store.',
|
|
256
|
+
'微信已授权,但登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。': 'WeChat was authorized, but the login credential could not be written to the DSH credential store. Check that the store is writable.',
|
|
257
|
+
'微信已授权,但账号配置无法写入本机。请检查 DSH_HOME 目录权限。': 'WeChat was authorized, but the account configuration could not be saved locally. Check the DSH_HOME directory permissions.',
|
|
258
|
+
'微信已授权,但无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。': 'WeChat was authorized, but the account state or workspace could not be initialized. Check DSH_HOME and the workspace directory.',
|
|
259
|
+
'微信已授权,但插件无法连接本机 Harness。请确认 dsh web 已正常启动。': 'WeChat was authorized, but the plugin could not reach the local Harness. Confirm that dsh web is running normally.',
|
|
260
|
+
'微信已授权,但消息连接初始化失败。请查看 dsh web 日志后重试。': 'WeChat was authorized, but the message connection could not be initialized. Check the dsh web logs and try again.',
|
|
261
|
+
'微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。': 'WeChat was authorized, but an unknown error occurred during activation. Check the dsh web logs.',
|
|
233
262
|
'微信已绑定,可以开始向已绑定的机器人发消息。': 'WeChat is connected and ready for messages.',
|
|
234
263
|
'这个微信账号已经绑定并保持在线。': 'This WeChat account is connected and online.',
|
|
235
264
|
'微信账号及本机凭据已移除。': 'The WeChat account and local credentials were removed.',
|
|
@@ -473,6 +502,16 @@ function translateDynamic(text) {
|
|
|
473
502
|
if (match) return `Remove “${match[1]}” from DeepSeek Harness?`;
|
|
474
503
|
match = /^从 DeepSeek Harness 移除(.+)$/.exec(text);
|
|
475
504
|
if (match) return `Remove ${match[1]} from DeepSeek Harness`;
|
|
505
|
+
match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
|
|
506
|
+
if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
|
|
507
|
+
match = /^正在修复「(.+)」$/.exec(text);
|
|
508
|
+
if (match) return `Repairing “${match[1]}”`;
|
|
509
|
+
match = /^修复(.+)的卡片按钮$/.exec(text);
|
|
510
|
+
if (match) return `Repair card buttons for ${match[1]}`;
|
|
511
|
+
match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
|
|
512
|
+
if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
|
|
513
|
+
match = /^(.+)的卡片按钮已修复。$/.exec(text);
|
|
514
|
+
if (match) return `Card buttons repaired for ${match[1]}.`;
|
|
476
515
|
match = /^(检查连接|重试连接)(.+)$/.exec(text);
|
|
477
516
|
if (match) return `${localizeText(match[1])} ${match[2]}`;
|
|
478
517
|
match = /^移除(.+)$/.exec(text);
|