@xmanrui/dsh-im 0.9.0 → 0.11.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 +7 -3
- package/README.md +7 -3
- package/lib/client.js +378 -27
- package/lib/index.js +134 -131
- package/package.json +1 -1
- package/plugin-src/client/channels/dingtalk/api.js +19 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -10
- package/plugin-src/client/channels/feishu/index.js +35 -5
- package/plugin-src/client/channels/qq/api.js +21 -0
- package/plugin-src/client/channels/qq/index.js +39 -1
- package/plugin-src/client/channels/shared/token-channel.js +29 -3
- package/plugin-src/client/channels/wecom/api.js +11 -0
- package/plugin-src/client/channels/wecom/index.js +71 -1
- package/plugin-src/client/channels/weixin/api.js +11 -0
- package/plugin-src/client/channels/weixin/index.js +40 -4
- package/plugin-src/client/channels/whatsapp/index.js +41 -2
- package/plugin-src/client/i18n.js +13 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +29 -3
- package/plugin-src/host/channels/feishu/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +34 -3
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/qq/rpc.mjs +35 -2
- package/plugin-src/host/channels/shared/production.mjs +1 -1
- package/plugin-src/host/channels/shared/rpc.mjs +25 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -1
- package/plugin-src/host/channels/slack/rpc.mjs +28 -2
- package/plugin-src/host/channels/wecom/production.mjs +1 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +31 -2
- package/plugin-src/host/channels/weixin/production.mjs +1 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +29 -3
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +31 -3
- package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +160 -20
- package/src/channels/dingtalk/dingtalk-controller.mjs +18 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +21 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +54 -1
- package/src/channels/feishu/bridge.mjs +45 -18
- package/src/channels/feishu/feishu-runtime.mjs +45 -0
- package/src/channels/feishu/message-utils.mjs +227 -6
- package/src/channels/feishu/multi-bot-controller.mjs +21 -0
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +107 -20
- package/src/channels/qq/qq-controller.mjs +18 -0
- package/src/channels/qq/qq-runtime.mjs +26 -0
- package/src/channels/shared/bot-workspace-store.mjs +2 -0
- package/src/channels/shared/connection-test.mjs +54 -0
- package/src/channels/shared/harness-client.mjs +14 -8
- package/src/channels/shared/image-prompt.mjs +268 -0
- package/src/channels/shared/text-harness-bridge.mjs +62 -16
- package/src/channels/shared/token-bot-controller.mjs +21 -0
- package/src/channels/shared/workspace-session.mjs +2 -1
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +95 -0
- package/src/channels/slack/slack-controller.mjs +19 -0
- package/src/channels/slack/slack-runtime.mjs +34 -3
- package/src/channels/telegram/telegram-api.mjs +37 -0
- package/src/channels/telegram/telegram-runtime.mjs +81 -9
- package/src/channels/wecom/wecom-bridge.mjs +160 -23
- package/src/channels/wecom/wecom-controller.mjs +18 -0
- package/src/channels/wecom/wecom-runtime.mjs +18 -0
- package/src/channels/weixin/weixin-api.mjs +98 -2
- package/src/channels/weixin/weixin-bridge.mjs +52 -19
- package/src/channels/weixin/weixin-controller.mjs +18 -0
- package/src/channels/weixin/weixin-runtime.mjs +25 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +20 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +160 -1
package/package.json
CHANGED
|
@@ -85,6 +85,16 @@ function normalizeError(value, fallbackCode, fallbackMessage) {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function normalizeTestMessage(value) {
|
|
89
|
+
if (!isRecord(value)) return null;
|
|
90
|
+
if (value.sent === true) return { sent: true };
|
|
91
|
+
if (value.sent !== false) return null;
|
|
92
|
+
const code = value.code === 'test-target-unavailable'
|
|
93
|
+
? 'test-target-unavailable'
|
|
94
|
+
: 'test-message-failed';
|
|
95
|
+
return { sent: false, code };
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
export function unwrapRpcResult(result) {
|
|
89
99
|
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
|
90
100
|
throw new Error('钉钉服务返回了无法识别的响应');
|
|
@@ -189,9 +199,18 @@ export function normalizeSnapshot(value) {
|
|
|
189
199
|
connected: bots.filter((bot) => bot.connected).length,
|
|
190
200
|
},
|
|
191
201
|
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
|
|
202
|
+
testMessage: normalizeTestMessage(source.testMessage),
|
|
192
203
|
};
|
|
193
204
|
}
|
|
194
205
|
|
|
206
|
+
export function connectionTestFeedback(result) {
|
|
207
|
+
if (result?.sent === true) return '钉钉连接检查完成,测试消息已发送。';
|
|
208
|
+
if (result?.code === 'test-target-unavailable') {
|
|
209
|
+
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
|
210
|
+
}
|
|
211
|
+
return result ? '钉钉连接检查完成,但测试消息发送失败。' : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
195
214
|
export function presentError(error) {
|
|
196
215
|
return {
|
|
197
216
|
code: safeErrorCode(error?.code, 'DINGTALK_ERROR'),
|
|
@@ -7,6 +7,7 @@ import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
|
|
7
7
|
import {
|
|
8
8
|
DINGTALK_ENDPOINTS,
|
|
9
9
|
DINGTALK_RPC_CHANNEL,
|
|
10
|
+
connectionTestFeedback,
|
|
10
11
|
formatRemaining,
|
|
11
12
|
normalizeProvisioning,
|
|
12
13
|
normalizeSnapshot,
|
|
@@ -208,6 +209,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
|
|
208
209
|
export function AccountCard({
|
|
209
210
|
account,
|
|
210
211
|
busy,
|
|
212
|
+
feedback,
|
|
211
213
|
removing,
|
|
212
214
|
onReconnect,
|
|
213
215
|
onWorkspaceSave,
|
|
@@ -241,6 +243,10 @@ export function AccountCard({
|
|
|
241
243
|
}),
|
|
242
244
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
243
245
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
246
|
+
feedback ? h('div', {
|
|
247
|
+
className: 'ddt-summary dim-cardSummary',
|
|
248
|
+
role: 'status',
|
|
249
|
+
}, feedback) : null,
|
|
244
250
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
245
251
|
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
|
|
246
252
|
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
|
@@ -262,6 +268,7 @@ function AccountList(props) {
|
|
|
262
268
|
h(AccountCard, {
|
|
263
269
|
account,
|
|
264
270
|
busy: props.busyByBot[account.botId],
|
|
271
|
+
feedback: props.feedbackByBot[account.botId]?.message,
|
|
265
272
|
removing: props.removeTarget === account.botId,
|
|
266
273
|
onReconnect: () => props.onReconnect(account),
|
|
267
274
|
onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
|
|
@@ -280,6 +287,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
280
287
|
const [provision, setProvision] = React.useState(null);
|
|
281
288
|
const [busy, setBusy] = React.useState(false);
|
|
282
289
|
const [busyByBot, setBusyByBot] = React.useState({});
|
|
290
|
+
const [feedbackByBot, setFeedbackByBot] = React.useState({});
|
|
283
291
|
const [removeTarget, setRemoveTarget] = React.useState(null);
|
|
284
292
|
const [credentialOpen, setCredentialOpen] = React.useState(false);
|
|
285
293
|
const [credentialError, setCredentialError] = React.useState(null);
|
|
@@ -325,6 +333,22 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
325
333
|
}
|
|
326
334
|
}, []);
|
|
327
335
|
|
|
336
|
+
const discardStaleFeedback = React.useCallback((snapshot) => {
|
|
337
|
+
const botsById = new Map(snapshot.bots.map((bot) => [bot.botId, bot]));
|
|
338
|
+
setFeedbackByBot((current) => {
|
|
339
|
+
let changed = false;
|
|
340
|
+
const next = { ...current };
|
|
341
|
+
for (const [botId, feedback] of Object.entries(next)) {
|
|
342
|
+
const bot = botsById.get(botId);
|
|
343
|
+
if (!bot || (feedback.clearWhenDisconnected && (!bot.connected || bot.error))) {
|
|
344
|
+
delete next[botId];
|
|
345
|
+
changed = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return changed ? next : current;
|
|
349
|
+
});
|
|
350
|
+
}, []);
|
|
351
|
+
|
|
328
352
|
const focusAddButton = React.useCallback(() => {
|
|
329
353
|
if (!mountedRef.current) return;
|
|
330
354
|
if (focusFrameRef.current !== null) window.cancelAnimationFrame(focusFrameRef.current);
|
|
@@ -366,6 +390,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
366
390
|
revision: snapshot.revision,
|
|
367
391
|
error: null,
|
|
368
392
|
});
|
|
393
|
+
discardStaleFeedback(snapshot);
|
|
369
394
|
if (restoreProvisioning && snapshot.provisioning) {
|
|
370
395
|
setProvision((current) => !current || current.attemptId === snapshot.provisioning.attemptId
|
|
371
396
|
? {
|
|
@@ -386,7 +411,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
386
411
|
}));
|
|
387
412
|
return undefined;
|
|
388
413
|
}
|
|
389
|
-
}, [invoke, workspaceFence]);
|
|
414
|
+
}, [discardStaleFeedback, invoke, workspaceFence]);
|
|
390
415
|
|
|
391
416
|
React.useEffect(() => {
|
|
392
417
|
const controller = new AbortController();
|
|
@@ -480,6 +505,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
480
505
|
revision: snapshot.revision,
|
|
481
506
|
error: null,
|
|
482
507
|
});
|
|
508
|
+
discardStaleFeedback(snapshot);
|
|
483
509
|
}
|
|
484
510
|
setCredentialOpen(false);
|
|
485
511
|
announce('钉钉机器人凭据已绑定。');
|
|
@@ -490,7 +516,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
490
516
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
491
517
|
if (mountedRef.current) setBusy(false);
|
|
492
518
|
}
|
|
493
|
-
}, [announce, invoke, loadStatus, workspaceFence]);
|
|
519
|
+
}, [announce, discardStaleFeedback, invoke, loadStatus, workspaceFence]);
|
|
494
520
|
|
|
495
521
|
const cancelProvisioning = React.useCallback(async () => {
|
|
496
522
|
if (!mountedRef.current) return;
|
|
@@ -595,6 +621,13 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
595
621
|
if (!mountedRef.current) return undefined;
|
|
596
622
|
const snapshotVersion = workspaceFence.beginMutation();
|
|
597
623
|
setBotBusy(account.botId, operation);
|
|
624
|
+
if (operation === 'reconnect') {
|
|
625
|
+
setFeedbackByBot((current) => {
|
|
626
|
+
const next = { ...current };
|
|
627
|
+
delete next[account.botId];
|
|
628
|
+
return next;
|
|
629
|
+
});
|
|
630
|
+
}
|
|
598
631
|
try {
|
|
599
632
|
const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
|
|
600
633
|
if (!mountedRef.current) return undefined;
|
|
@@ -606,28 +639,50 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
606
639
|
revision: snapshot.revision,
|
|
607
640
|
error: null,
|
|
608
641
|
});
|
|
642
|
+
discardStaleFeedback(snapshot);
|
|
643
|
+
}
|
|
644
|
+
const successMessage = typeof success === 'function' ? success(snapshot) : success;
|
|
645
|
+
if (operation === 'reconnect') {
|
|
646
|
+
setFeedbackByBot((current) => ({
|
|
647
|
+
...current,
|
|
648
|
+
[account.botId]: {
|
|
649
|
+
message: successMessage,
|
|
650
|
+
clearWhenDisconnected: snapshot.testMessage?.sent === true,
|
|
651
|
+
},
|
|
652
|
+
}));
|
|
609
653
|
}
|
|
610
|
-
announce(
|
|
654
|
+
announce(successMessage);
|
|
611
655
|
return snapshot;
|
|
612
656
|
} catch (error) {
|
|
613
657
|
if (!mountedRef.current) return undefined;
|
|
614
|
-
|
|
658
|
+
const failureMessage = operation === 'reconnect'
|
|
659
|
+
? '连接检查失败,请稍后重试。'
|
|
660
|
+
: `操作失败:${presentError(error).message}`;
|
|
661
|
+
if (operation === 'reconnect') {
|
|
662
|
+
setFeedbackByBot((current) => ({
|
|
663
|
+
...current,
|
|
664
|
+
[account.botId]: { message: failureMessage, clearWhenDisconnected: false },
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
announce(failureMessage);
|
|
615
668
|
return undefined;
|
|
616
669
|
} finally {
|
|
617
670
|
const shouldRefresh = workspaceFence.endMutation();
|
|
618
671
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true, restoreProvisioning: false });
|
|
619
672
|
if (mountedRef.current) setBotBusy(account.botId, null);
|
|
620
673
|
}
|
|
621
|
-
}, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
674
|
+
}, [announce, discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
622
675
|
|
|
623
676
|
const reconnect = React.useCallback((account) => runBotAction({
|
|
624
677
|
account,
|
|
625
678
|
operation: 'reconnect',
|
|
626
679
|
endpoint: DINGTALK_ENDPOINTS.reconnectBot,
|
|
627
|
-
payload: { botId: account.botId },
|
|
628
|
-
success: (snapshot) =>
|
|
629
|
-
|
|
630
|
-
|
|
680
|
+
payload: { botId: account.botId, sendTest: true },
|
|
681
|
+
success: (snapshot) => {
|
|
682
|
+
const refreshed = snapshot?.bots.find((bot) => bot.botId === account.botId);
|
|
683
|
+
if (!refreshed?.connected) return '钉钉仍未连接,插件会继续自动重试。';
|
|
684
|
+
return connectionTestFeedback(snapshot.testMessage) ?? '钉钉连接检查完成。';
|
|
685
|
+
},
|
|
631
686
|
}), [runBotAction]);
|
|
632
687
|
|
|
633
688
|
const saveWorkspace = React.useCallback(async (account, workspace) => {
|
|
@@ -646,13 +701,14 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
646
701
|
revision: snapshot.revision,
|
|
647
702
|
error: null,
|
|
648
703
|
});
|
|
704
|
+
discardStaleFeedback(snapshot);
|
|
649
705
|
}
|
|
650
706
|
} finally {
|
|
651
707
|
const shouldRefresh = workspaceFence.endMutation();
|
|
652
708
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
653
709
|
if (mountedRef.current) setBotBusy(account.botId, null);
|
|
654
710
|
}
|
|
655
|
-
}, [invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
711
|
+
}, [discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
656
712
|
|
|
657
713
|
const remove = React.useCallback(async (account) => {
|
|
658
714
|
const snapshot = await runBotAction({
|
|
@@ -738,6 +794,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
738
794
|
? h(AccountList, {
|
|
739
795
|
bots: model.bots,
|
|
740
796
|
busyByBot,
|
|
797
|
+
feedbackByBot,
|
|
741
798
|
removeTarget,
|
|
742
799
|
onReconnect: (account) => void reconnect(account),
|
|
743
800
|
onWorkspaceSave: saveWorkspace,
|
|
@@ -330,6 +330,16 @@ function formatCheckedTime(timestamp) {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
function connectionTestNotice(value) {
|
|
334
|
+
if (value?.testMessage?.sent === true) {
|
|
335
|
+
return '测试消息已发送,请到飞书会话中确认。';
|
|
336
|
+
}
|
|
337
|
+
if (value?.testMessage?.code === 'test-target-unavailable') {
|
|
338
|
+
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
|
339
|
+
}
|
|
340
|
+
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
|
|
341
|
+
}
|
|
342
|
+
|
|
333
343
|
function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
|
|
334
344
|
const cancelRef = React.useRef(null);
|
|
335
345
|
const idPart = bot.botId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
@@ -364,6 +374,7 @@ export function BotCard({
|
|
|
364
374
|
connection,
|
|
365
375
|
busy,
|
|
366
376
|
actionError,
|
|
377
|
+
testNotice,
|
|
367
378
|
removing,
|
|
368
379
|
onReconnect,
|
|
369
380
|
onWorkspaceSave,
|
|
@@ -420,6 +431,10 @@ export function BotCard({
|
|
|
420
431
|
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
|
421
432
|
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
|
422
433
|
summary) : null,
|
|
434
|
+
testNotice ? h("div", {
|
|
435
|
+
className: "bxf-healthSummary dim-cardSummary",
|
|
436
|
+
role: "status",
|
|
437
|
+
}, testNotice) : null,
|
|
423
438
|
h("div", { className: "bxf-actions bxf-botActions dim-cardActions" },
|
|
424
439
|
h(Button, {
|
|
425
440
|
className: "dim-cardAction", onClick: onReconnect,
|
|
@@ -454,6 +469,7 @@ function BotList(props) {
|
|
|
454
469
|
connection: bot,
|
|
455
470
|
busy: props.busyByBot[bot.botId],
|
|
456
471
|
actionError: props.errorsByBot[bot.botId],
|
|
472
|
+
testNotice: props.testNoticesByBot[bot.botId],
|
|
457
473
|
removing: props.removeTargetId === bot.botId,
|
|
458
474
|
onReconnect: () => props.onReconnect(bot),
|
|
459
475
|
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
|
@@ -527,6 +543,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
527
543
|
const [credentialError, setCredentialError] = React.useState(null);
|
|
528
544
|
const [busyByBot, setBusyByBot] = React.useState({});
|
|
529
545
|
const [errorsByBot, setErrorsByBot] = React.useState({});
|
|
546
|
+
const [testNoticesByBot, setTestNoticesByBot] = React.useState({});
|
|
530
547
|
const [removeTargetId, setRemoveTargetId] = React.useState(null);
|
|
531
548
|
const [announcement, setAnnouncement] = React.useState("");
|
|
532
549
|
const [now, setNow] = React.useState(() => Date.now());
|
|
@@ -826,8 +843,14 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
826
843
|
const snapshotVersion = workspaceFence.beginMutation();
|
|
827
844
|
setBotBusy(botId, "reconnect");
|
|
828
845
|
setBotError(botId, null);
|
|
846
|
+
setTestNoticesByBot((current) => {
|
|
847
|
+
const next = { ...current };
|
|
848
|
+
delete next[botId];
|
|
849
|
+
return next;
|
|
850
|
+
});
|
|
829
851
|
try {
|
|
830
|
-
const
|
|
852
|
+
const value = await invoke(FEISHU_ENDPOINTS.reconnectBot, { botId, sendTest: true });
|
|
853
|
+
const snapshot = normalizeBotsSnapshot(value);
|
|
831
854
|
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
832
855
|
mergeSnapshot(snapshot);
|
|
833
856
|
}
|
|
@@ -839,12 +862,18 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
839
862
|
error.code = refreshed?.error?.code ?? "FEISHU_BOT_OFFLINE";
|
|
840
863
|
throw error;
|
|
841
864
|
}
|
|
842
|
-
|
|
865
|
+
const testNotice = connectionTestNotice(value);
|
|
866
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
867
|
+
setTestNoticesByBot((current) => ({ ...current, [botId]: testNotice }));
|
|
868
|
+
}
|
|
869
|
+
announce(testNotice ?? (connection.connected
|
|
843
870
|
? `${bot.name}连接检查完成。`
|
|
844
|
-
: `${bot.name}已重新连接。`);
|
|
871
|
+
: `${bot.name}已重新连接。`));
|
|
845
872
|
} catch (error) {
|
|
846
|
-
|
|
847
|
-
|
|
873
|
+
const failure = new Error("连接检查失败,请稍后重试。");
|
|
874
|
+
failure.code = error?.code;
|
|
875
|
+
setBotError(botId, failure);
|
|
876
|
+
announce(failure.message);
|
|
848
877
|
} finally {
|
|
849
878
|
const shouldRefresh = workspaceFence.endMutation();
|
|
850
879
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
@@ -995,6 +1024,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
995
1024
|
bots: model.bots,
|
|
996
1025
|
busyByBot,
|
|
997
1026
|
errorsByBot,
|
|
1027
|
+
testNoticesByBot,
|
|
998
1028
|
removeTargetId,
|
|
999
1029
|
onReconnect: (bot) => void reconnectOneBot(bot),
|
|
1000
1030
|
onWorkspaceSave: saveWorkspace,
|
|
@@ -13,6 +13,7 @@ export const QQ_ENDPOINTS = Object.freeze({
|
|
|
13
13
|
|
|
14
14
|
const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
|
|
15
15
|
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
|
16
|
+
const TEST_MESSAGE_CODES = new Set(['test-target-unavailable', 'test-message-failed']);
|
|
16
17
|
const QR_DATA_URL = /^data:image\/(?:png|webp);base64,[a-z\d+/]+={0,2}$/i;
|
|
17
18
|
|
|
18
19
|
function isRecord(value) {
|
|
@@ -96,18 +97,38 @@ function normalizeBot(value) {
|
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function normalizeTestMessage(value) {
|
|
101
|
+
if (!isRecord(value) || typeof value.sent !== 'boolean') return undefined;
|
|
102
|
+
if (value.sent) return { sent: true };
|
|
103
|
+
const code = text(value.code, 'test-message-failed', 80);
|
|
104
|
+
return {
|
|
105
|
+
sent: false,
|
|
106
|
+
code: TEST_MESSAGE_CODES.has(code) ? code : 'test-message-failed',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
export function normalizeSnapshot(value) {
|
|
100
111
|
const source = isRecord(value?.snapshot) ? value.snapshot : value;
|
|
101
112
|
if (!isRecord(source) || !Array.isArray(source.bots)) throw new Error('QQ 服务没有返回有效的机器人列表');
|
|
102
113
|
const bots = source.bots.map(normalizeBot).filter(Boolean);
|
|
114
|
+
const testMessage = normalizeTestMessage(source.testMessage);
|
|
103
115
|
return {
|
|
104
116
|
revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
|
|
105
117
|
bots,
|
|
106
118
|
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
|
107
119
|
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
|
|
120
|
+
...(testMessage ? { testMessage } : {}),
|
|
108
121
|
};
|
|
109
122
|
}
|
|
110
123
|
|
|
124
|
+
export function connectionTestFeedback(result) {
|
|
125
|
+
if (result?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
|
|
126
|
+
if (result?.code === 'test-target-unavailable') {
|
|
127
|
+
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
|
128
|
+
}
|
|
129
|
+
return result ? '连接检查完成,但测试消息发送失败。' : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
export function presentError(error) {
|
|
112
133
|
return {
|
|
113
134
|
code: text(error?.code, 'QQ_ERROR', 80),
|
|
@@ -9,6 +9,7 @@ import { installDingtalkStyles } from '../dingtalk/styles.js';
|
|
|
9
9
|
import {
|
|
10
10
|
QQ_ENDPOINTS,
|
|
11
11
|
QQ_RPC_CHANNEL,
|
|
12
|
+
connectionTestFeedback,
|
|
12
13
|
formatRemaining,
|
|
13
14
|
normalizeProvisioning,
|
|
14
15
|
normalizeSnapshot,
|
|
@@ -151,6 +152,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
|
|
151
152
|
export function AccountCard({
|
|
152
153
|
account,
|
|
153
154
|
busy,
|
|
155
|
+
feedback,
|
|
154
156
|
removing,
|
|
155
157
|
onReconnect,
|
|
156
158
|
onWorkspaceSave,
|
|
@@ -180,6 +182,11 @@ export function AccountCard({
|
|
|
180
182
|
}),
|
|
181
183
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
182
184
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
185
|
+
feedback ? h('div', {
|
|
186
|
+
className: 'ddt-summary dim-cardSummary',
|
|
187
|
+
role: 'status',
|
|
188
|
+
'aria-live': 'polite',
|
|
189
|
+
}, feedback) : null,
|
|
183
190
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
184
191
|
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
|
185
192
|
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
|
|
@@ -193,6 +200,7 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
193
200
|
const [provision, setProvision] = React.useState(null);
|
|
194
201
|
const [busy, setBusy] = React.useState(false);
|
|
195
202
|
const [busyByBot, setBusyByBot] = React.useState({});
|
|
203
|
+
const [feedbackByBot, setFeedbackByBot] = React.useState({});
|
|
196
204
|
const [removeTarget, setRemoveTarget] = React.useState(null);
|
|
197
205
|
const [credentialOpen, setCredentialOpen] = React.useState(false);
|
|
198
206
|
const [credentialError, setCredentialError] = React.useState(null);
|
|
@@ -361,6 +369,35 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
361
369
|
}
|
|
362
370
|
}, [invoke, loadStatus, workspaceFence]);
|
|
363
371
|
|
|
372
|
+
const reconnect = React.useCallback(async (account) => {
|
|
373
|
+
setFeedbackByBot((current) => {
|
|
374
|
+
const next = { ...current };
|
|
375
|
+
delete next[account.botId];
|
|
376
|
+
return next;
|
|
377
|
+
});
|
|
378
|
+
try {
|
|
379
|
+
const snapshot = await botAction(
|
|
380
|
+
account,
|
|
381
|
+
'reconnect',
|
|
382
|
+
QQ_ENDPOINTS.reconnectBot,
|
|
383
|
+
{ botId: account.botId, sendTest: true },
|
|
384
|
+
);
|
|
385
|
+
if (mounted.current) {
|
|
386
|
+
setFeedbackByBot((current) => ({
|
|
387
|
+
...current,
|
|
388
|
+
[account.botId]: connectionTestFeedback(snapshot?.testMessage),
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
if (mounted.current) {
|
|
393
|
+
setFeedbackByBot((current) => ({
|
|
394
|
+
...current,
|
|
395
|
+
[account.botId]: '连接检查失败,请稍后重试。',
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}, [botAction]);
|
|
400
|
+
|
|
364
401
|
let provisionView = null;
|
|
365
402
|
if (provision?.status === 'starting') provisionView = h('div', { className: 'ddt-card ddt-loading dim-surfaceCard' }, h('div', { className: 'ddt-spinner' }), '正在申请 QQ 二维码…');
|
|
366
403
|
else if (['pending', 'refreshing'].includes(provision?.status)) provisionView = h(QrPanel, {
|
|
@@ -378,8 +415,9 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
378
415
|
h('li', { key: account.botId }, h(AccountCard, {
|
|
379
416
|
account,
|
|
380
417
|
busy: busyByBot[account.botId],
|
|
418
|
+
feedback: feedbackByBot[account.botId],
|
|
381
419
|
removing: removeTarget === account.botId,
|
|
382
|
-
onReconnect: () => void
|
|
420
|
+
onReconnect: () => void reconnect(account),
|
|
383
421
|
onWorkspaceSave: (workspace) => botAction(
|
|
384
422
|
account,
|
|
385
423
|
'workspace',
|
|
@@ -30,6 +30,14 @@ function checkedTime(value) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function connectionTestNotice(value) {
|
|
34
|
+
if (value?.testMessage?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
|
|
35
|
+
if (value?.testMessage?.code === 'test-target-unavailable') {
|
|
36
|
+
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
|
37
|
+
}
|
|
38
|
+
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export function createTokenChannelSettings(definition) {
|
|
34
42
|
const {
|
|
35
43
|
channel,
|
|
@@ -53,7 +61,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
53
61
|
emptyActionLabel = '填写 Bot Token',
|
|
54
62
|
} = definition;
|
|
55
63
|
|
|
56
|
-
function AccountCard({ account, busy, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
|
64
|
+
function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
|
57
65
|
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
|
58
66
|
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
|
59
67
|
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
|
|
@@ -82,6 +90,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
82
90
|
}),
|
|
83
91
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
84
92
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
93
|
+
testNotice ? h('div', { className: 'ddt-summary dim-cardSummary', role: 'status' }, testNotice) : null,
|
|
85
94
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
86
95
|
h(Button, {
|
|
87
96
|
className: 'dim-cardAction',
|
|
@@ -111,6 +120,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
111
120
|
const [credentialError, setCredentialError] = React.useState(null);
|
|
112
121
|
const [busy, setBusy] = React.useState(false);
|
|
113
122
|
const [busyByBot, setBusyByBot] = React.useState({});
|
|
123
|
+
const [testNoticeByBot, setTestNoticeByBot] = React.useState({});
|
|
114
124
|
const [removeTarget, setRemoveTarget] = React.useState(null);
|
|
115
125
|
const mounted = React.useRef(true);
|
|
116
126
|
const workspaceFence = useWorkspaceSnapshotFence();
|
|
@@ -198,10 +208,25 @@ export function createTokenChannelSettings(definition) {
|
|
|
198
208
|
const snapshotVersion = workspaceFence.beginMutation();
|
|
199
209
|
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
|
|
200
210
|
try {
|
|
201
|
-
const
|
|
211
|
+
const value = await invoke(endpoint, payload);
|
|
212
|
+
const snapshot = api.normalizeSnapshot(value);
|
|
202
213
|
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
203
214
|
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
204
215
|
}
|
|
216
|
+
if (mounted.current && operation === 'reconnect') {
|
|
217
|
+
setTestNoticeByBot((current) => ({
|
|
218
|
+
...current,
|
|
219
|
+
[account.botId]: connectionTestNotice(value),
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (operation !== 'reconnect') throw error;
|
|
224
|
+
if (mounted.current) {
|
|
225
|
+
setTestNoticeByBot((current) => ({
|
|
226
|
+
...current,
|
|
227
|
+
[account.botId]: '连接检查失败,请稍后重试。',
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
205
230
|
} finally {
|
|
206
231
|
const shouldRefresh = workspaceFence.endMutation();
|
|
207
232
|
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
@@ -221,12 +246,13 @@ export function createTokenChannelSettings(definition) {
|
|
|
221
246
|
h('li', { key: account.botId }, h(AccountCard, {
|
|
222
247
|
account,
|
|
223
248
|
busy: busyByBot[account.botId],
|
|
249
|
+
testNotice: testNoticeByBot[account.botId],
|
|
224
250
|
removing: removeTarget === account.botId,
|
|
225
251
|
onReconnect: () => void botAction(
|
|
226
252
|
account,
|
|
227
253
|
'reconnect',
|
|
228
254
|
endpoints.reconnectBot,
|
|
229
|
-
{ botId: account.botId },
|
|
255
|
+
{ botId: account.botId, sendTest: true },
|
|
230
256
|
),
|
|
231
257
|
onWorkspaceSave: (workspace) => botAction(
|
|
232
258
|
account,
|
|
@@ -34,6 +34,16 @@ function timestamp(value) {
|
|
|
34
34
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function normalizeTestMessage(value) {
|
|
38
|
+
if (!isRecord(value)) return null;
|
|
39
|
+
if (value.sent === true) return { sent: true };
|
|
40
|
+
if (value.sent !== false) return null;
|
|
41
|
+
const code = value.code === 'test-target-unavailable'
|
|
42
|
+
? 'test-target-unavailable'
|
|
43
|
+
: 'test-message-failed';
|
|
44
|
+
return { sent: false, code };
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
export function unwrapRpcResult(result) {
|
|
38
48
|
if (!isRecord(result) || typeof result.ok !== 'boolean') throw new Error('企业微信服务返回了无法识别的响应');
|
|
39
49
|
if (!result.ok) {
|
|
@@ -105,6 +115,7 @@ export function normalizeSnapshot(value) {
|
|
|
105
115
|
bots,
|
|
106
116
|
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
|
107
117
|
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
|
|
118
|
+
testMessage: normalizeTestMessage(source.testMessage),
|
|
108
119
|
};
|
|
109
120
|
}
|
|
110
121
|
|