@xmanrui/dsh-im 0.3.0 → 0.5.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.md +64 -0
- package/assets/logo-icon.png +0 -0
- package/assets/logo.png +0 -0
- package/lib/client.js +765 -295
- package/lib/index.js +112 -110
- package/package.json +2 -1
- package/plugin-src/client/build.mjs +6 -1
- package/plugin-src/client/channels/dingtalk/api.js +2 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -14
- package/plugin-src/client/channels/feishu/api.js +2 -0
- package/plugin-src/client/channels/feishu/index.js +70 -22
- package/plugin-src/client/channels/qq/api.js +2 -0
- package/plugin-src/client/channels/qq/index.js +45 -8
- package/plugin-src/client/channels/shared/token-api.js +2 -0
- package/plugin-src/client/channels/shared/token-channel.js +34 -8
- package/plugin-src/client/channels/wecom/api.js +2 -0
- package/plugin-src/client/channels/wecom/index.js +45 -8
- package/plugin-src/client/channels/weixin/api.js +2 -0
- package/plugin-src/client/channels/weixin/index.js +68 -8
- package/plugin-src/client/channels/whatsapp/api.js +2 -0
- package/plugin-src/client/channels/whatsapp/index.js +27 -5
- package/plugin-src/client/i18n.js +15 -1
- package/plugin-src/client/index.js +28 -1
- package/plugin-src/client/styles.js +32 -9
- package/plugin-src/client/workspace-editor.js +96 -0
- package/plugin-src/client/workspace-snapshot-fence.js +28 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
- package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
- package/plugin-src/host/channels/feishu/production.mjs +42 -5
- package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
- package/plugin-src/host/channels/qq/production.mjs +48 -22
- package/plugin-src/host/channels/qq/rpc.mjs +16 -2
- package/plugin-src/host/channels/shared/production.mjs +48 -22
- package/plugin-src/host/channels/shared/rpc.mjs +15 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
- package/plugin-src/host/channels/slack/production.mjs +48 -23
- package/plugin-src/host/channels/slack/rpc.mjs +16 -0
- package/plugin-src/host/channels/wecom/production.mjs +49 -23
- package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
- package/plugin-src/host/channels/weixin/production.mjs +31 -11
- package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
- package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
- package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +22 -11
- package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
- package/src/channels/dingtalk/harness-client.mjs +4 -3
- package/src/channels/dingtalk/state-store.mjs +5 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +36 -16
- package/src/channels/feishu/harness-client.mjs +6 -5
- package/src/channels/feishu/state-store.mjs +5 -0
- package/src/channels/qq/qq-bridge.mjs +25 -15
- package/src/channels/qq/qq-controller.mjs +8 -1
- package/src/channels/qq/state-store.mjs +5 -0
- package/src/channels/shared/bot-workspace-store.mjs +584 -0
- package/src/channels/shared/conversation-state-store.mjs +5 -0
- package/src/channels/shared/text-harness-bridge.mjs +23 -13
- package/src/channels/shared/workspace-command.mjs +26 -0
- package/src/channels/shared/workspace-session.mjs +44 -0
- package/src/channels/wecom/state-store.mjs +5 -0
- package/src/channels/wecom/wecom-bridge.mjs +22 -13
- package/src/channels/wecom/wecom-controller.mjs +8 -1
- package/src/channels/weixin/harness-client.mjs +6 -5
- package/src/channels/weixin/state-store.mjs +5 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +16 -6
- package/src/channels/weixin/weixin-controller.mjs +6 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "通过扫码、App Manifest或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Slack、Telegram、Discord和WhatsApp)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"./package.json": "./package.json"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
+
"assets",
|
|
26
27
|
"bin",
|
|
27
28
|
"cordis.patch.yml",
|
|
28
29
|
"lib",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
@@ -8,6 +8,8 @@ const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
|
8
8
|
const packageRoot = resolve(sourceDirectory, '../..');
|
|
9
9
|
const outputPath = resolve(packageRoot, 'lib/client.js');
|
|
10
10
|
const loaderId = process.env.DSH_IM_CLIENT_ID ?? '@xmanrui/dsh-im';
|
|
11
|
+
const logoPath = resolve(packageRoot, 'assets/logo-icon.png');
|
|
12
|
+
const logoDataUrl = `data:image/png;base64,${(await readFile(logoPath)).toString('base64')}`;
|
|
11
13
|
|
|
12
14
|
const result = await build({
|
|
13
15
|
entryPoints: [resolve(sourceDirectory, 'index.js')],
|
|
@@ -16,6 +18,9 @@ const result = await build({
|
|
|
16
18
|
platform: 'browser',
|
|
17
19
|
target: ['chrome100'],
|
|
18
20
|
external: ['react'],
|
|
21
|
+
define: {
|
|
22
|
+
'globalThis.__DSH_IM_LOGO_DATA_URL__': JSON.stringify(logoDataUrl),
|
|
23
|
+
},
|
|
19
24
|
write: false,
|
|
20
25
|
minify: process.env.NODE_ENV === 'production',
|
|
21
26
|
legalComments: 'none',
|
|
@@ -8,6 +8,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
|
|
|
8
8
|
bindCredentials: 'bot.bind-credentials',
|
|
9
9
|
reconnectBot: 'bot.reconnect',
|
|
10
10
|
deleteBot: 'bot.delete',
|
|
11
|
+
setWorkspace: 'bot.workspace.set',
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
|
@@ -145,6 +146,7 @@ function normalizeBot(value) {
|
|
|
145
146
|
state,
|
|
146
147
|
connected,
|
|
147
148
|
configured: value.configured !== false,
|
|
149
|
+
workspace: optionalString(value.workspace, 4_096) ?? '',
|
|
148
150
|
bot: {
|
|
149
151
|
name: optionalString(bot.name, 100) ?? '钉钉机器人',
|
|
150
152
|
clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? '已安全保存',
|
|
@@ -2,6 +2,8 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
|
4
4
|
import { h } from '../../i18n.js';
|
|
5
|
+
import { WorkspaceEditor } from '../../workspace-editor.js';
|
|
6
|
+
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
|
5
7
|
import {
|
|
6
8
|
DINGTALK_ENDPOINTS,
|
|
7
9
|
DINGTALK_RPC_CHANNEL,
|
|
@@ -208,6 +210,7 @@ export function AccountCard({
|
|
|
208
210
|
busy,
|
|
209
211
|
removing,
|
|
210
212
|
onReconnect,
|
|
213
|
+
onWorkspaceSave,
|
|
211
214
|
onRequestRemove,
|
|
212
215
|
onConfirmRemove,
|
|
213
216
|
onCancelRemove,
|
|
@@ -231,6 +234,11 @@ export function AccountCard({
|
|
|
231
234
|
h('dd', null, account.connected ? 'Stream 长连接' : '离线')),
|
|
232
235
|
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'),
|
|
233
236
|
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
|
237
|
+
h(WorkspaceEditor, {
|
|
238
|
+
workspace: account.workspace,
|
|
239
|
+
disabled: Boolean(busy),
|
|
240
|
+
onSave: onWorkspaceSave,
|
|
241
|
+
}),
|
|
234
242
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
235
243
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
236
244
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
@@ -256,6 +264,7 @@ function AccountList(props) {
|
|
|
256
264
|
busy: props.busyByBot[account.botId],
|
|
257
265
|
removing: props.removeTarget === account.botId,
|
|
258
266
|
onReconnect: () => props.onReconnect(account),
|
|
267
|
+
onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
|
|
259
268
|
onRequestRemove: () => props.onRequestRemove(account),
|
|
260
269
|
onConfirmRemove: () => props.onConfirmRemove(account),
|
|
261
270
|
onCancelRemove: props.onCancelRemove,
|
|
@@ -279,6 +288,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
279
288
|
const addButtonRef = React.useRef(null);
|
|
280
289
|
const mountedRef = React.useRef(true);
|
|
281
290
|
const statusRequestRef = React.useRef(0);
|
|
291
|
+
const workspaceFence = useWorkspaceSnapshotFence();
|
|
282
292
|
const noticeFrameRef = React.useRef(null);
|
|
283
293
|
const focusFrameRef = React.useRef(null);
|
|
284
294
|
|
|
@@ -335,11 +345,14 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
335
345
|
restoreProvisioning = false,
|
|
336
346
|
} = {}) => {
|
|
337
347
|
if (!mountedRef.current || signal?.aborted) return undefined;
|
|
348
|
+
const workspaceVersion = workspaceFence.beginStatus();
|
|
349
|
+
if (workspaceVersion === null) return undefined;
|
|
338
350
|
const requestId = statusRequestRef.current + 1;
|
|
339
351
|
statusRequestRef.current = requestId;
|
|
340
352
|
const canCommit = () => mountedRef.current
|
|
341
353
|
&& !signal?.aborted
|
|
342
|
-
&& statusRequestRef.current === requestId
|
|
354
|
+
&& statusRequestRef.current === requestId
|
|
355
|
+
&& workspaceFence.canCommitStatus(workspaceVersion);
|
|
343
356
|
if (!silent && canCommit()) {
|
|
344
357
|
setModel((current) => ({ ...current, phase: 'loading', error: null }));
|
|
345
358
|
}
|
|
@@ -373,7 +386,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
373
386
|
}));
|
|
374
387
|
return undefined;
|
|
375
388
|
}
|
|
376
|
-
}, [invoke]);
|
|
389
|
+
}, [invoke, workspaceFence]);
|
|
377
390
|
|
|
378
391
|
React.useEffect(() => {
|
|
379
392
|
const controller = new AbortController();
|
|
@@ -450,6 +463,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
450
463
|
|
|
451
464
|
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
|
452
465
|
if (!mountedRef.current) return;
|
|
466
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
453
467
|
setBusy(true);
|
|
454
468
|
setCredentialError(null);
|
|
455
469
|
try {
|
|
@@ -458,21 +472,25 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
458
472
|
{ clientId: identity, clientSecret: secret },
|
|
459
473
|
));
|
|
460
474
|
if (!mountedRef.current) return;
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
475
|
+
if (workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
476
|
+
setModel({
|
|
477
|
+
phase: 'ready',
|
|
478
|
+
bots: snapshot.bots,
|
|
479
|
+
totals: snapshot.totals,
|
|
480
|
+
revision: snapshot.revision,
|
|
481
|
+
error: null,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
468
484
|
setCredentialOpen(false);
|
|
469
485
|
announce('钉钉机器人凭据已绑定。');
|
|
470
486
|
} catch (error) {
|
|
471
487
|
if (mountedRef.current) setCredentialError(presentError(error));
|
|
472
488
|
} finally {
|
|
489
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
490
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
473
491
|
if (mountedRef.current) setBusy(false);
|
|
474
492
|
}
|
|
475
|
-
}, [announce, invoke]);
|
|
493
|
+
}, [announce, invoke, loadStatus, workspaceFence]);
|
|
476
494
|
|
|
477
495
|
const cancelProvisioning = React.useCallback(async () => {
|
|
478
496
|
if (!mountedRef.current) return;
|
|
@@ -575,12 +593,20 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
575
593
|
|
|
576
594
|
const runBotAction = React.useCallback(async ({ account, operation, endpoint, payload, success }) => {
|
|
577
595
|
if (!mountedRef.current) return undefined;
|
|
596
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
578
597
|
setBotBusy(account.botId, operation);
|
|
579
598
|
try {
|
|
580
|
-
await invoke(endpoint, payload);
|
|
581
|
-
if (!mountedRef.current) return undefined;
|
|
582
|
-
const snapshot = await loadStatus({ silent: true, restoreProvisioning: false });
|
|
599
|
+
const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
|
|
583
600
|
if (!mountedRef.current) return undefined;
|
|
601
|
+
if (workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
602
|
+
setModel({
|
|
603
|
+
phase: 'ready',
|
|
604
|
+
bots: snapshot.bots,
|
|
605
|
+
totals: snapshot.totals,
|
|
606
|
+
revision: snapshot.revision,
|
|
607
|
+
error: null,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
584
610
|
announce(typeof success === 'function' ? success(snapshot) : success);
|
|
585
611
|
return snapshot;
|
|
586
612
|
} catch (error) {
|
|
@@ -588,9 +614,11 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
588
614
|
announce(`操作失败:${presentError(error).message}`);
|
|
589
615
|
return undefined;
|
|
590
616
|
} finally {
|
|
617
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
618
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true, restoreProvisioning: false });
|
|
591
619
|
if (mountedRef.current) setBotBusy(account.botId, null);
|
|
592
620
|
}
|
|
593
|
-
}, [announce, invoke, loadStatus, setBotBusy]);
|
|
621
|
+
}, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
594
622
|
|
|
595
623
|
const reconnect = React.useCallback((account) => runBotAction({
|
|
596
624
|
account,
|
|
@@ -602,6 +630,30 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
602
630
|
: '钉钉仍未连接,插件会继续自动重试。',
|
|
603
631
|
}), [runBotAction]);
|
|
604
632
|
|
|
633
|
+
const saveWorkspace = React.useCallback(async (account, workspace) => {
|
|
634
|
+
const workspaceVersion = workspaceFence.beginMutation();
|
|
635
|
+
setBotBusy(account.botId, 'workspace');
|
|
636
|
+
try {
|
|
637
|
+
const snapshot = normalizeSnapshot(await invoke(
|
|
638
|
+
DINGTALK_ENDPOINTS.setWorkspace,
|
|
639
|
+
{ botId: account.botId, workspace },
|
|
640
|
+
));
|
|
641
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(workspaceVersion)) {
|
|
642
|
+
setModel({
|
|
643
|
+
phase: 'ready',
|
|
644
|
+
bots: snapshot.bots,
|
|
645
|
+
totals: snapshot.totals,
|
|
646
|
+
revision: snapshot.revision,
|
|
647
|
+
error: null,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
} finally {
|
|
651
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
652
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
653
|
+
if (mountedRef.current) setBotBusy(account.botId, null);
|
|
654
|
+
}
|
|
655
|
+
}, [invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
656
|
+
|
|
605
657
|
const remove = React.useCallback(async (account) => {
|
|
606
658
|
const snapshot = await runBotAction({
|
|
607
659
|
account,
|
|
@@ -688,6 +740,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
688
740
|
busyByBot,
|
|
689
741
|
removeTarget,
|
|
690
742
|
onReconnect: (account) => void reconnect(account),
|
|
743
|
+
onWorkspaceSave: saveWorkspace,
|
|
691
744
|
onRequestRemove: (account) => setRemoveTarget(account.botId),
|
|
692
745
|
onConfirmRemove: (account) => void remove(account),
|
|
693
746
|
onCancelRemove: () => setRemoveTarget(null),
|
|
@@ -17,6 +17,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|
|
17
17
|
reconnectBot: "bot.reconnect",
|
|
18
18
|
disconnectBot: "bot.disconnect",
|
|
19
19
|
deleteBot: "bot.delete",
|
|
20
|
+
setWorkspace: "bot.workspace.set",
|
|
20
21
|
// Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
|
|
21
22
|
testConnection: "connection.test",
|
|
22
23
|
disconnect: "connection.disconnect",
|
|
@@ -159,6 +160,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
|
|
|
159
160
|
state: authoritativeState(value.state, connected),
|
|
160
161
|
connected,
|
|
161
162
|
configured: value.configured !== false,
|
|
163
|
+
workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
|
|
162
164
|
bot: normalizeBot(value.bot),
|
|
163
165
|
health: normalizeHealth(value.health, connected),
|
|
164
166
|
error: normalizeError(value.error),
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
unwrapRpcResult,
|
|
15
15
|
} from "./api.js";
|
|
16
16
|
import { useAnimationFrameScheduler } from "../../lifecycle.js";
|
|
17
|
+
import { WorkspaceEditor } from "../../workspace-editor.js";
|
|
18
|
+
import { useWorkspaceSnapshotFence } from "../../workspace-snapshot-fence.js";
|
|
17
19
|
import { installFeishuStyles } from "./styles.js";
|
|
18
20
|
|
|
19
21
|
export const name = "feishu-settings";
|
|
@@ -364,6 +366,7 @@ export function BotCard({
|
|
|
364
366
|
actionError,
|
|
365
367
|
removing,
|
|
366
368
|
onReconnect,
|
|
369
|
+
onWorkspaceSave,
|
|
367
370
|
onRequestRemove,
|
|
368
371
|
onConfirmRemove,
|
|
369
372
|
onCancelRemove,
|
|
@@ -409,6 +412,11 @@ export function BotCard({
|
|
|
409
412
|
h("div", { className: "bxf-metric dim-botMetric" }, h("dt", null, "最近检查"),
|
|
410
413
|
h("dd", null, formatCheckedTime(health.lastCheckedAt))),
|
|
411
414
|
),
|
|
415
|
+
h(WorkspaceEditor, {
|
|
416
|
+
workspace: connection.workspace,
|
|
417
|
+
disabled: Boolean(busy),
|
|
418
|
+
onSave: onWorkspaceSave,
|
|
419
|
+
}),
|
|
412
420
|
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
|
413
421
|
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
|
414
422
|
summary) : null,
|
|
@@ -448,6 +456,7 @@ function BotList(props) {
|
|
|
448
456
|
actionError: props.errorsByBot[bot.botId],
|
|
449
457
|
removing: props.removeTargetId === bot.botId,
|
|
450
458
|
onReconnect: () => props.onReconnect(bot),
|
|
459
|
+
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
|
451
460
|
onRequestRemove: () => props.onRequestRemove(bot),
|
|
452
461
|
onConfirmRemove: () => props.onConfirmRemove(bot),
|
|
453
462
|
onCancelRemove: props.onCancelRemove,
|
|
@@ -525,8 +534,15 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
525
534
|
const cardRefs = React.useRef(new Map());
|
|
526
535
|
const removeButtonRefs = React.useRef(new Map());
|
|
527
536
|
const addButtonRef = React.useRef(null);
|
|
537
|
+
const mountedRef = React.useRef(true);
|
|
538
|
+
const workspaceFence = useWorkspaceSnapshotFence();
|
|
528
539
|
const scheduleAnimationFrame = useAnimationFrameScheduler();
|
|
529
540
|
|
|
541
|
+
React.useEffect(() => {
|
|
542
|
+
mountedRef.current = true;
|
|
543
|
+
return () => { mountedRef.current = false; };
|
|
544
|
+
}, []);
|
|
545
|
+
|
|
530
546
|
const announce = React.useCallback((message) => {
|
|
531
547
|
setAnnouncement("");
|
|
532
548
|
scheduleAnimationFrame(() => {
|
|
@@ -548,23 +564,27 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
548
564
|
}, []);
|
|
549
565
|
|
|
550
566
|
const loadStatus = React.useCallback(async ({ signal, silent = false, restoreProvisioning = false } = {}) => {
|
|
567
|
+
const workspaceVersion = workspaceFence.beginStatus();
|
|
568
|
+
if (workspaceVersion === null || !mountedRef.current) return undefined;
|
|
551
569
|
if (!silent) setPageBusy(true);
|
|
552
570
|
try {
|
|
553
571
|
const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.status, {}, signal));
|
|
554
|
-
if (signal?.aborted
|
|
572
|
+
if (signal?.aborted || !mountedRef.current
|
|
573
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
|
|
555
574
|
mergeSnapshot(snapshot, { restoreProvisioning });
|
|
556
575
|
return snapshot;
|
|
557
576
|
} catch (error) {
|
|
558
|
-
if (signal?.aborted || error?.name === "AbortError"
|
|
577
|
+
if (signal?.aborted || error?.name === "AbortError" || !mountedRef.current
|
|
578
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
|
|
559
579
|
const presented = presentError(error);
|
|
560
580
|
setModel((current) => current.phase === "loading" || !silent
|
|
561
581
|
? { ...current, phase: "error", pageError: presented }
|
|
562
582
|
: { ...current, statusError: presented });
|
|
563
583
|
return undefined;
|
|
564
584
|
} finally {
|
|
565
|
-
if (!silent && !signal?.aborted) setPageBusy(false);
|
|
585
|
+
if (!silent && !signal?.aborted && mountedRef.current) setPageBusy(false);
|
|
566
586
|
}
|
|
567
|
-
}, [invoke, mergeSnapshot]);
|
|
587
|
+
}, [invoke, mergeSnapshot, workspaceFence]);
|
|
568
588
|
|
|
569
589
|
React.useEffect(() => {
|
|
570
590
|
const controller = new AbortController();
|
|
@@ -645,6 +665,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
645
665
|
}, [announce, invoke, model.provisioning?.attemptId]);
|
|
646
666
|
|
|
647
667
|
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
|
668
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
648
669
|
setCredentialBusy(true);
|
|
649
670
|
setCredentialError(null);
|
|
650
671
|
try {
|
|
@@ -652,15 +673,19 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
652
673
|
FEISHU_ENDPOINTS.bindCredentials,
|
|
653
674
|
{ appId: identity, appSecret: secret },
|
|
654
675
|
));
|
|
655
|
-
|
|
676
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
677
|
+
mergeSnapshot(snapshot);
|
|
678
|
+
}
|
|
656
679
|
setCredentialOpen(false);
|
|
657
680
|
announce("飞书机器人凭据已绑定。");
|
|
658
681
|
} catch (error) {
|
|
659
682
|
setCredentialError(presentError(error));
|
|
660
683
|
} finally {
|
|
684
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
685
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
661
686
|
setCredentialBusy(false);
|
|
662
687
|
}
|
|
663
|
-
}, [announce, invoke, mergeSnapshot]);
|
|
688
|
+
}, [announce, invoke, loadStatus, mergeSnapshot, workspaceFence]);
|
|
664
689
|
|
|
665
690
|
const cancelProvisioning = React.useCallback(async () => {
|
|
666
691
|
const attemptId = model.provisioning?.attemptId;
|
|
@@ -798,11 +823,14 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
798
823
|
|
|
799
824
|
const reconnectOneBot = React.useCallback(async (connection) => {
|
|
800
825
|
const { botId, bot } = connection;
|
|
826
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
801
827
|
setBotBusy(botId, "reconnect");
|
|
802
828
|
setBotError(botId, null);
|
|
803
829
|
try {
|
|
804
830
|
const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.reconnectBot, { botId }));
|
|
805
|
-
|
|
831
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
832
|
+
mergeSnapshot(snapshot);
|
|
833
|
+
}
|
|
806
834
|
const refreshed = snapshot.bots.find((item) => item.botId === botId);
|
|
807
835
|
if (!refreshed?.connected) {
|
|
808
836
|
const error = new Error(
|
|
@@ -818,9 +846,31 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
818
846
|
setBotError(botId, error);
|
|
819
847
|
announce(`${bot.name}操作失败,请查看机器人状态。`);
|
|
820
848
|
} finally {
|
|
849
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
850
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
821
851
|
setBotBusy(botId, null);
|
|
822
852
|
}
|
|
823
|
-
}, [announce, invoke, mergeSnapshot, setBotBusy, setBotError]);
|
|
853
|
+
}, [announce, invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
|
854
|
+
|
|
855
|
+
const saveWorkspace = React.useCallback(async (connection, workspace) => {
|
|
856
|
+
const { botId } = connection;
|
|
857
|
+
const workspaceVersion = workspaceFence.beginMutation();
|
|
858
|
+
setBotBusy(botId, "workspace");
|
|
859
|
+
setBotError(botId, null);
|
|
860
|
+
try {
|
|
861
|
+
const snapshot = normalizeBotsSnapshot(await invoke(
|
|
862
|
+
FEISHU_ENDPOINTS.setWorkspace,
|
|
863
|
+
{ botId, workspace },
|
|
864
|
+
));
|
|
865
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(workspaceVersion)) {
|
|
866
|
+
mergeSnapshot(snapshot);
|
|
867
|
+
}
|
|
868
|
+
} finally {
|
|
869
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
870
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
871
|
+
if (mountedRef.current) setBotBusy(botId, null);
|
|
872
|
+
}
|
|
873
|
+
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
|
824
874
|
|
|
825
875
|
const requestRemove = React.useCallback((connection) => {
|
|
826
876
|
setRemoveTargetId(connection.botId);
|
|
@@ -834,32 +884,29 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
834
884
|
|
|
835
885
|
const confirmRemove = React.useCallback(async (connection) => {
|
|
836
886
|
const { botId, bot } = connection;
|
|
887
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
837
888
|
setBotBusy(botId, "delete");
|
|
838
889
|
setBotError(botId, null);
|
|
839
890
|
try {
|
|
840
|
-
await invoke(
|
|
891
|
+
const snapshot = normalizeBotsSnapshot(await invoke(
|
|
892
|
+
FEISHU_ENDPOINTS.deleteBot,
|
|
893
|
+
{ botId, confirm: true },
|
|
894
|
+
));
|
|
841
895
|
setRemoveTargetId(null);
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
...current,
|
|
846
|
-
bots,
|
|
847
|
-
totals: {
|
|
848
|
-
configured: bots.length,
|
|
849
|
-
connected: bots.filter((item) => item.connected).length,
|
|
850
|
-
},
|
|
851
|
-
};
|
|
852
|
-
});
|
|
896
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
897
|
+
mergeSnapshot(snapshot);
|
|
898
|
+
}
|
|
853
899
|
announce(`${bot.name}已从此 DeepSeek Harness 移除;飞书开放平台中的应用未被删除。`);
|
|
854
|
-
await loadStatus({ silent: true });
|
|
855
900
|
scheduleAnimationFrame(() => addButtonRef.current?.focus(), "focus");
|
|
856
901
|
} catch (error) {
|
|
857
902
|
setBotError(botId, error);
|
|
858
903
|
announce(`${bot.name}移除失败,请重试。`);
|
|
859
904
|
} finally {
|
|
905
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
906
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
860
907
|
setBotBusy(botId, null);
|
|
861
908
|
}
|
|
862
|
-
}, [announce, invoke, loadStatus, scheduleAnimationFrame, setBotBusy, setBotError]);
|
|
909
|
+
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
|
|
863
910
|
|
|
864
911
|
const provision = model.provisioning;
|
|
865
912
|
let provisionContent = null;
|
|
@@ -950,6 +997,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
950
997
|
errorsByBot,
|
|
951
998
|
removeTargetId,
|
|
952
999
|
onReconnect: (bot) => void reconnectOneBot(bot),
|
|
1000
|
+
onWorkspaceSave: saveWorkspace,
|
|
953
1001
|
onRequestRemove: requestRemove,
|
|
954
1002
|
onConfirmRemove: (bot) => void confirmRemove(bot),
|
|
955
1003
|
onCancelRemove: cancelRemove,
|
|
@@ -8,6 +8,7 @@ export const QQ_ENDPOINTS = Object.freeze({
|
|
|
8
8
|
bindCredentials: 'bot.bind-credentials',
|
|
9
9
|
reconnectBot: 'bot.reconnect',
|
|
10
10
|
deleteBot: 'bot.delete',
|
|
11
|
+
setWorkspace: 'bot.workspace.set',
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
|
|
@@ -79,6 +80,7 @@ function normalizeBot(value) {
|
|
|
79
80
|
botId: id(value.botId),
|
|
80
81
|
connected,
|
|
81
82
|
state: connected ? 'connected' : state,
|
|
83
|
+
workspace: text(value.workspace, '', 4_096),
|
|
82
84
|
bot: {
|
|
83
85
|
name: text(value.bot?.name, 'QQ机器人', 100),
|
|
84
86
|
appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
|
|
@@ -3,6 +3,8 @@ import * as React from 'react';
|
|
|
3
3
|
import { QqLogoGlyph } from '../../channel-logos.js';
|
|
4
4
|
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
|
5
5
|
import { h } from '../../i18n.js';
|
|
6
|
+
import { WorkspaceEditor } from '../../workspace-editor.js';
|
|
7
|
+
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
|
6
8
|
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
|
7
9
|
import {
|
|
8
10
|
QQ_ENDPOINTS,
|
|
@@ -146,7 +148,16 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
|
|
146
148
|
h(Button, { kind: 'danger', onClick: onConfirm, disabled: busy }, busy ? '正在移除…' : '确认移除接入')));
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
export function AccountCard({
|
|
151
|
+
export function AccountCard({
|
|
152
|
+
account,
|
|
153
|
+
busy,
|
|
154
|
+
removing,
|
|
155
|
+
onReconnect,
|
|
156
|
+
onWorkspaceSave,
|
|
157
|
+
onRequestRemove,
|
|
158
|
+
onConfirmRemove,
|
|
159
|
+
onCancelRemove,
|
|
160
|
+
}) {
|
|
150
161
|
const tone = account.connected ? 'success' : account.state === 'error' ? 'error' : 'warning';
|
|
151
162
|
const stateLabel = account.connected ? '运行正常' : account.state === 'connecting' ? '正在连接' : '连接未就绪';
|
|
152
163
|
const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
|
|
@@ -162,6 +173,11 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
|
|
162
173
|
h('dl', { className: 'ddt-metrics dim-botMetrics' },
|
|
163
174
|
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '消息通道'), h('dd', null, account.connected ? 'WebSocket 长连接' : '离线')),
|
|
164
175
|
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
|
176
|
+
h(WorkspaceEditor, {
|
|
177
|
+
workspace: account.workspace,
|
|
178
|
+
disabled: Boolean(busy),
|
|
179
|
+
onSave: onWorkspaceSave,
|
|
180
|
+
}),
|
|
165
181
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
166
182
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
167
183
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
@@ -182,6 +198,7 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
182
198
|
const [credentialError, setCredentialError] = React.useState(null);
|
|
183
199
|
const [now, setNow] = React.useState(Date.now());
|
|
184
200
|
const mounted = React.useRef(true);
|
|
201
|
+
const workspaceFence = useWorkspaceSnapshotFence();
|
|
185
202
|
const addButtonRef = React.useRef(null);
|
|
186
203
|
|
|
187
204
|
React.useEffect(() => {
|
|
@@ -201,10 +218,13 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
201
218
|
}, [rpcCall]);
|
|
202
219
|
|
|
203
220
|
const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
|
221
|
+
const workspaceVersion = workspaceFence.beginStatus();
|
|
222
|
+
if (workspaceVersion === null) return undefined;
|
|
204
223
|
if (!silent && mounted.current) setModel((current) => ({ ...current, phase: 'loading', error: null }));
|
|
205
224
|
try {
|
|
206
225
|
const snapshot = normalizeSnapshot(await invoke(QQ_ENDPOINTS.status, {}, signal));
|
|
207
|
-
if (!mounted.current || signal?.aborted
|
|
226
|
+
if (!mounted.current || signal?.aborted
|
|
227
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
|
|
208
228
|
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
209
229
|
if (restore && snapshot.provisioning) setProvision({
|
|
210
230
|
...snapshot.provisioning,
|
|
@@ -212,12 +232,13 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
212
232
|
});
|
|
213
233
|
return snapshot;
|
|
214
234
|
} catch (error) {
|
|
215
|
-
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
|
|
235
|
+
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
|
|
236
|
+
&& workspaceFence.canCommitStatus(workspaceVersion)) {
|
|
216
237
|
setModel((current) => ({ ...current, phase: silent ? current.phase : 'error', error: presentError(error) }));
|
|
217
238
|
}
|
|
218
239
|
return undefined;
|
|
219
240
|
}
|
|
220
|
-
}, [invoke]);
|
|
241
|
+
}, [invoke, workspaceFence]);
|
|
221
242
|
|
|
222
243
|
React.useEffect(() => {
|
|
223
244
|
const controller = new AbortController();
|
|
@@ -258,6 +279,7 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
258
279
|
}, [invoke, provision?.attemptId]);
|
|
259
280
|
|
|
260
281
|
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
|
282
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
261
283
|
setBusy(true);
|
|
262
284
|
setCredentialError(null);
|
|
263
285
|
try {
|
|
@@ -266,14 +288,18 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
266
288
|
{ appId: identity, appSecret: secret },
|
|
267
289
|
));
|
|
268
290
|
if (!mounted.current) return;
|
|
269
|
-
|
|
291
|
+
if (workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
292
|
+
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
293
|
+
}
|
|
270
294
|
setCredentialOpen(false);
|
|
271
295
|
} catch (error) {
|
|
272
296
|
if (mounted.current) setCredentialError(presentError(error));
|
|
273
297
|
} finally {
|
|
298
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
299
|
+
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
274
300
|
if (mounted.current) setBusy(false);
|
|
275
301
|
}
|
|
276
|
-
}, [invoke]);
|
|
302
|
+
}, [invoke, loadStatus, workspaceFence]);
|
|
277
303
|
|
|
278
304
|
const closeProvision = React.useCallback(async () => {
|
|
279
305
|
setBusy(true);
|
|
@@ -318,17 +344,22 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
318
344
|
}, [invoke, loadStatus, provision?.attemptId, provision?.pollIntervalMs, provision?.status]);
|
|
319
345
|
|
|
320
346
|
const botAction = React.useCallback(async (account, operation, endpoint, payload) => {
|
|
347
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
321
348
|
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
|
|
322
349
|
try {
|
|
323
350
|
const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
|
|
324
|
-
if (mounted.current
|
|
351
|
+
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
352
|
+
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
353
|
+
}
|
|
325
354
|
return snapshot;
|
|
326
355
|
} finally {
|
|
356
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
357
|
+
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
327
358
|
if (mounted.current) setBusyByBot((current) => {
|
|
328
359
|
const next = { ...current }; delete next[account.botId]; return next;
|
|
329
360
|
});
|
|
330
361
|
}
|
|
331
|
-
}, [invoke]);
|
|
362
|
+
}, [invoke, loadStatus, workspaceFence]);
|
|
332
363
|
|
|
333
364
|
let provisionView = null;
|
|
334
365
|
if (provision?.status === 'starting') provisionView = h('div', { className: 'ddt-card ddt-loading dim-surfaceCard' }, h('div', { className: 'ddt-spinner' }), '正在申请 QQ 二维码…');
|
|
@@ -349,6 +380,12 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
349
380
|
busy: busyByBot[account.botId],
|
|
350
381
|
removing: removeTarget === account.botId,
|
|
351
382
|
onReconnect: () => void botAction(account, 'reconnect', QQ_ENDPOINTS.reconnectBot, { botId: account.botId }),
|
|
383
|
+
onWorkspaceSave: (workspace) => botAction(
|
|
384
|
+
account,
|
|
385
|
+
'workspace',
|
|
386
|
+
QQ_ENDPOINTS.setWorkspace,
|
|
387
|
+
{ botId: account.botId, workspace },
|
|
388
|
+
),
|
|
352
389
|
onRequestRemove: () => setRemoveTarget(account.botId),
|
|
353
390
|
onCancelRemove: () => setRemoveTarget(null),
|
|
354
391
|
onConfirmRemove: async () => {
|
|
@@ -24,6 +24,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
|
|
|
24
24
|
bindCredentials: 'bot.bind-credentials',
|
|
25
25
|
reconnectBot: 'bot.reconnect',
|
|
26
26
|
deleteBot: 'bot.delete',
|
|
27
|
+
setWorkspace: 'bot.workspace.set',
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
export function createTokenChannelApi(channel, connectionSummary) {
|
|
@@ -47,6 +48,7 @@ export function createTokenChannelApi(channel, connectionSummary) {
|
|
|
47
48
|
botId: id(value.botId),
|
|
48
49
|
connected,
|
|
49
50
|
state: connected ? 'connected' : state,
|
|
51
|
+
workspace: text(value.workspace, '', 4_096),
|
|
50
52
|
bot: {
|
|
51
53
|
name: text(value.bot?.name, `${channel}机器人`, 100),
|
|
52
54
|
username: text(value.bot?.username, '', 100),
|