@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
|
@@ -3,6 +3,8 @@ import * as React from 'react';
|
|
|
3
3
|
import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-binding.js';
|
|
4
4
|
import { h } from '../../i18n.js';
|
|
5
5
|
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
|
6
|
+
import { WorkspaceEditor } from '../../workspace-editor.js';
|
|
7
|
+
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
|
6
8
|
|
|
7
9
|
const Button = React.forwardRef(function Button(
|
|
8
10
|
{ children, kind = 'secondary', className = '', ...props },
|
|
@@ -51,7 +53,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
51
53
|
emptyActionLabel = '填写 Bot Token',
|
|
52
54
|
} = definition;
|
|
53
55
|
|
|
54
|
-
function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
|
56
|
+
function AccountCard({ account, busy, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
|
55
57
|
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
|
56
58
|
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
|
57
59
|
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
|
|
@@ -73,6 +75,11 @@ export function createTokenChannelSettings(definition) {
|
|
|
73
75
|
h('dt', null, '消息通道'), h('dd', null, account.connected ? connectionLabel : '离线')),
|
|
74
76
|
h('div', { className: 'ddt-metric dim-botMetric' },
|
|
75
77
|
h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
|
78
|
+
h(WorkspaceEditor, {
|
|
79
|
+
workspace: account.workspace,
|
|
80
|
+
disabled: Boolean(busy),
|
|
81
|
+
onSave: onWorkspaceSave,
|
|
82
|
+
}),
|
|
76
83
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
77
84
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
78
85
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
@@ -106,6 +113,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
106
113
|
const [busyByBot, setBusyByBot] = React.useState({});
|
|
107
114
|
const [removeTarget, setRemoveTarget] = React.useState(null);
|
|
108
115
|
const mounted = React.useRef(true);
|
|
116
|
+
const workspaceFence = useWorkspaceSnapshotFence();
|
|
109
117
|
|
|
110
118
|
React.useEffect(() => {
|
|
111
119
|
const disposeDingtalk = installDingtalkStyles();
|
|
@@ -124,13 +132,17 @@ export function createTokenChannelSettings(definition) {
|
|
|
124
132
|
}, [rpcCall]);
|
|
125
133
|
|
|
126
134
|
const loadStatus = React.useCallback(async ({ signal, silent = false } = {}) => {
|
|
135
|
+
const workspaceVersion = workspaceFence.beginStatus();
|
|
136
|
+
if (workspaceVersion === null) return;
|
|
127
137
|
if (!silent && mounted.current) setModel((current) => ({ ...current, phase: 'loading', error: null }));
|
|
128
138
|
try {
|
|
129
139
|
const snapshot = api.normalizeSnapshot(await invoke(endpoints.status, {}, signal));
|
|
130
|
-
if (!mounted.current || signal?.aborted
|
|
140
|
+
if (!mounted.current || signal?.aborted
|
|
141
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return;
|
|
131
142
|
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
132
143
|
} catch (error) {
|
|
133
|
-
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
|
|
144
|
+
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
|
|
145
|
+
&& workspaceFence.canCommitStatus(workspaceVersion)) {
|
|
134
146
|
setModel((current) => ({
|
|
135
147
|
...current,
|
|
136
148
|
phase: silent ? current.phase : 'error',
|
|
@@ -138,7 +150,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
138
150
|
}));
|
|
139
151
|
}
|
|
140
152
|
}
|
|
141
|
-
}, [invoke]);
|
|
153
|
+
}, [invoke, workspaceFence]);
|
|
142
154
|
|
|
143
155
|
React.useEffect(() => {
|
|
144
156
|
const controller = new AbortController();
|
|
@@ -160,6 +172,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
160
172
|
}, [loadStatus, model.phase]);
|
|
161
173
|
|
|
162
174
|
const bindCredentials = React.useCallback(async (values) => {
|
|
175
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
163
176
|
setBusy(true);
|
|
164
177
|
setCredentialError(null);
|
|
165
178
|
try {
|
|
@@ -168,30 +181,37 @@ export function createTokenChannelSettings(definition) {
|
|
|
168
181
|
credentialPayload(values),
|
|
169
182
|
));
|
|
170
183
|
if (!mounted.current) return;
|
|
171
|
-
|
|
184
|
+
if (workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
185
|
+
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
186
|
+
}
|
|
172
187
|
setCredentialOpen(false);
|
|
173
188
|
} catch (error) {
|
|
174
189
|
if (mounted.current) setCredentialError(api.presentError(error));
|
|
175
190
|
} finally {
|
|
191
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
192
|
+
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
176
193
|
if (mounted.current) setBusy(false);
|
|
177
194
|
}
|
|
178
|
-
}, [invoke]);
|
|
195
|
+
}, [invoke, loadStatus, workspaceFence]);
|
|
179
196
|
|
|
180
197
|
const botAction = React.useCallback(async (account, operation, endpoint, payload) => {
|
|
198
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
181
199
|
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
|
|
182
200
|
try {
|
|
183
201
|
const snapshot = api.normalizeSnapshot(await invoke(endpoint, payload));
|
|
184
|
-
if (mounted.current) {
|
|
202
|
+
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
185
203
|
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
186
204
|
}
|
|
187
205
|
} finally {
|
|
206
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
207
|
+
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
188
208
|
if (mounted.current) setBusyByBot((current) => {
|
|
189
209
|
const next = { ...current };
|
|
190
210
|
delete next[account.botId];
|
|
191
211
|
return next;
|
|
192
212
|
});
|
|
193
213
|
}
|
|
194
|
-
}, [invoke]);
|
|
214
|
+
}, [invoke, loadStatus, workspaceFence]);
|
|
195
215
|
|
|
196
216
|
const botList = model.bots.length > 0
|
|
197
217
|
? h('section', { className: 'dim-listSection' },
|
|
@@ -208,6 +228,12 @@ export function createTokenChannelSettings(definition) {
|
|
|
208
228
|
endpoints.reconnectBot,
|
|
209
229
|
{ botId: account.botId },
|
|
210
230
|
),
|
|
231
|
+
onWorkspaceSave: (workspace) => botAction(
|
|
232
|
+
account,
|
|
233
|
+
'workspace',
|
|
234
|
+
endpoints.setWorkspace,
|
|
235
|
+
{ botId: account.botId, workspace },
|
|
236
|
+
),
|
|
211
237
|
onRequestRemove: () => setRemoveTarget(account.botId),
|
|
212
238
|
onCancelRemove: () => setRemoveTarget(null),
|
|
213
239
|
onConfirmRemove: async () => {
|
|
@@ -8,6 +8,7 @@ export const WECOM_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, '企业微信机器人', 100),
|
|
84
86
|
appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
|
|
@@ -3,6 +3,8 @@ import * as React from 'react';
|
|
|
3
3
|
import { WecomLogoGlyph } 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
|
WECOM_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 WecomSettingsTab({ 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 WecomSettingsTab({ 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(WECOM_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 WecomSettingsTab({ 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 WecomSettingsTab({ 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 WecomSettingsTab({ rpcCall }) {
|
|
|
266
288
|
{ botId: identity, 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 WecomSettingsTab({ 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' }), '正在申请企业微信二维码…');
|
|
@@ -349,6 +380,12 @@ export function WecomSettingsTab({ rpcCall }) {
|
|
|
349
380
|
busy: busyByBot[account.botId],
|
|
350
381
|
removing: removeTarget === account.botId,
|
|
351
382
|
onReconnect: () => void botAction(account, 'reconnect', WECOM_ENDPOINTS.reconnectBot, { botId: account.botId }),
|
|
383
|
+
onWorkspaceSave: (workspace) => botAction(
|
|
384
|
+
account,
|
|
385
|
+
'workspace',
|
|
386
|
+
WECOM_ENDPOINTS.setWorkspace,
|
|
387
|
+
{ botId: account.botId, workspace },
|
|
388
|
+
),
|
|
352
389
|
onRequestRemove: () => setRemoveTarget(account.botId),
|
|
353
390
|
onCancelRemove: () => setRemoveTarget(null),
|
|
354
391
|
onConfirmRemove: async () => {
|
|
@@ -7,6 +7,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
|
|
|
7
7
|
cancelProvisioning: 'provision.cancel',
|
|
8
8
|
reconnectBot: 'bot.reconnect',
|
|
9
9
|
deleteBot: 'bot.delete',
|
|
10
|
+
setWorkspace: 'bot.workspace.set',
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
|
@@ -103,6 +104,7 @@ function normalizeBot(value) {
|
|
|
103
104
|
state: connected ? 'connected' : state,
|
|
104
105
|
connected,
|
|
105
106
|
configured: value.configured === true,
|
|
107
|
+
workspace: string(value.workspace).slice(0, 4_096),
|
|
106
108
|
bot: {
|
|
107
109
|
name: string(value.bot.name, '微信机器人'),
|
|
108
110
|
accountIdMasked: string(value.bot.accountIdMasked, '已安全保存'),
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
unwrapRpcResult,
|
|
16
16
|
} from './api.js';
|
|
17
17
|
import { createPollScheduler, useAnimationFrameScheduler } from '../../lifecycle.js';
|
|
18
|
+
import { WorkspaceEditor } from '../../workspace-editor.js';
|
|
19
|
+
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
|
18
20
|
import { installWeixinStyles } from './styles.js';
|
|
19
21
|
|
|
20
22
|
export const name = 'weixin-settings';
|
|
@@ -193,7 +195,16 @@ function checkedTime(timestamp) {
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
export function AccountCard({
|
|
198
|
+
export function AccountCard({
|
|
199
|
+
account,
|
|
200
|
+
busy,
|
|
201
|
+
removing,
|
|
202
|
+
onReconnect,
|
|
203
|
+
onWorkspaceSave,
|
|
204
|
+
onRequestRemove,
|
|
205
|
+
onConfirmRemove,
|
|
206
|
+
onCancelRemove,
|
|
207
|
+
}) {
|
|
197
208
|
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
|
198
209
|
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
|
199
210
|
const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
|
|
@@ -211,6 +222,11 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
|
|
211
222
|
h('dd', null, account.connected ? 'iLink 长轮询' : '离线')),
|
|
212
223
|
h('div', { className: 'dxw-metric dim-botMetric' }, h('dt', null, '最近检查'),
|
|
213
224
|
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
|
225
|
+
h(WorkspaceEditor, {
|
|
226
|
+
workspace: account.workspace,
|
|
227
|
+
disabled: Boolean(busy),
|
|
228
|
+
onSave: onWorkspaceSave,
|
|
229
|
+
}),
|
|
214
230
|
h('div', { className: 'dxw-accountFooter dim-cardFooter' },
|
|
215
231
|
summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
|
|
216
232
|
h('div', { className: 'dxw-actions dim-cardActions' },
|
|
@@ -236,6 +252,7 @@ function AccountList(props) {
|
|
|
236
252
|
busy: props.busyByBot[account.botId],
|
|
237
253
|
removing: props.removeTarget === account.botId,
|
|
238
254
|
onReconnect: () => props.onReconnect(account),
|
|
255
|
+
onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
|
|
239
256
|
onRequestRemove: () => props.onRequestRemove(account),
|
|
240
257
|
onConfirmRemove: () => props.onConfirmRemove(account),
|
|
241
258
|
onCancelRemove: props.onCancelRemove,
|
|
@@ -269,8 +286,15 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
269
286
|
const [notice, setNotice] = React.useState('');
|
|
270
287
|
const [now, setNow] = React.useState(() => Date.now());
|
|
271
288
|
const addButtonRef = React.useRef(null);
|
|
289
|
+
const mountedRef = React.useRef(true);
|
|
290
|
+
const workspaceFence = useWorkspaceSnapshotFence();
|
|
272
291
|
const scheduleAnimationFrame = useAnimationFrameScheduler();
|
|
273
292
|
|
|
293
|
+
React.useEffect(() => {
|
|
294
|
+
mountedRef.current = true;
|
|
295
|
+
return () => { mountedRef.current = false; };
|
|
296
|
+
}, []);
|
|
297
|
+
|
|
274
298
|
const announce = React.useCallback((value) => {
|
|
275
299
|
setNotice('');
|
|
276
300
|
scheduleAnimationFrame(() => {
|
|
@@ -285,10 +309,13 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
285
309
|
silent = false,
|
|
286
310
|
restoreProvisioning = false,
|
|
287
311
|
} = {}) => {
|
|
312
|
+
const workspaceVersion = workspaceFence.beginStatus();
|
|
313
|
+
if (workspaceVersion === null || !mountedRef.current) return undefined;
|
|
288
314
|
if (!silent) setModel((current) => ({ ...current, phase: 'loading', error: null }));
|
|
289
315
|
try {
|
|
290
316
|
const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.status, {}, signal));
|
|
291
|
-
if (signal?.aborted
|
|
317
|
+
if (signal?.aborted || !mountedRef.current
|
|
318
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
|
|
292
319
|
setModel({
|
|
293
320
|
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
|
|
294
321
|
revision: snapshot.revision, error: null,
|
|
@@ -302,7 +329,8 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
302
329
|
}
|
|
303
330
|
return snapshot;
|
|
304
331
|
} catch (error) {
|
|
305
|
-
if (signal?.aborted || error?.name === 'AbortError'
|
|
332
|
+
if (signal?.aborted || error?.name === 'AbortError' || !mountedRef.current
|
|
333
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
|
|
306
334
|
setModel((current) => ({
|
|
307
335
|
...current,
|
|
308
336
|
phase: silent && current.phase === 'ready' ? 'ready' : 'error',
|
|
@@ -310,7 +338,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
310
338
|
}));
|
|
311
339
|
return undefined;
|
|
312
340
|
}
|
|
313
|
-
}, [invoke]);
|
|
341
|
+
}, [invoke, workspaceFence]);
|
|
314
342
|
|
|
315
343
|
React.useEffect(() => {
|
|
316
344
|
const controller = new AbortController();
|
|
@@ -466,35 +494,66 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
466
494
|
}, []);
|
|
467
495
|
|
|
468
496
|
const reconnect = React.useCallback(async (account) => {
|
|
497
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
469
498
|
setBotBusy(account.botId, 'reconnect');
|
|
470
499
|
try {
|
|
471
500
|
const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.reconnectBot, { botId: account.botId }));
|
|
472
|
-
|
|
501
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
502
|
+
setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
|
|
503
|
+
}
|
|
473
504
|
const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
|
|
474
505
|
announce(refreshed?.connected ? '微信连接检查完成。' : '微信仍未连接,插件会继续自动重试。');
|
|
475
506
|
} catch (error) {
|
|
476
507
|
announce(`连接检查失败:${presentError(error).message}`);
|
|
477
508
|
} finally {
|
|
509
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
510
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
478
511
|
setBotBusy(account.botId, null);
|
|
479
512
|
}
|
|
480
|
-
}, [announce, invoke, setBotBusy]);
|
|
513
|
+
}, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
514
|
+
|
|
515
|
+
const saveWorkspace = React.useCallback(async (account, workspace) => {
|
|
516
|
+
const workspaceVersion = workspaceFence.beginMutation();
|
|
517
|
+
setBotBusy(account.botId, 'workspace');
|
|
518
|
+
try {
|
|
519
|
+
const snapshot = normalizeSnapshot(await invoke(
|
|
520
|
+
WEIXIN_ENDPOINTS.setWorkspace,
|
|
521
|
+
{ botId: account.botId, workspace },
|
|
522
|
+
));
|
|
523
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(workspaceVersion)) {
|
|
524
|
+
setModel({
|
|
525
|
+
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
|
|
526
|
+
revision: snapshot.revision, error: null,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
} finally {
|
|
530
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
531
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
532
|
+
if (mountedRef.current) setBotBusy(account.botId, null);
|
|
533
|
+
}
|
|
534
|
+
}, [invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
481
535
|
|
|
482
536
|
const remove = React.useCallback(async (account) => {
|
|
537
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
483
538
|
setBotBusy(account.botId, 'delete');
|
|
484
539
|
try {
|
|
485
540
|
const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.deleteBot, {
|
|
486
541
|
botId: account.botId,
|
|
487
542
|
confirm: true,
|
|
488
543
|
}));
|
|
489
|
-
|
|
544
|
+
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
545
|
+
setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
|
|
546
|
+
}
|
|
490
547
|
setRemoveTarget(null);
|
|
491
548
|
announce('微信账号及本机凭据已移除。');
|
|
492
549
|
} catch (error) {
|
|
493
550
|
announce(`移除失败:${presentError(error).message}`);
|
|
494
551
|
} finally {
|
|
552
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
553
|
+
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
495
554
|
setBotBusy(account.botId, null);
|
|
496
555
|
}
|
|
497
|
-
}, [announce, invoke, setBotBusy]);
|
|
556
|
+
}, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
|
|
498
557
|
|
|
499
558
|
let provisionView = null;
|
|
500
559
|
if (provision?.status === 'starting') {
|
|
@@ -554,6 +613,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
554
613
|
busyByBot,
|
|
555
614
|
removeTarget,
|
|
556
615
|
onReconnect: (account) => void reconnect(account),
|
|
616
|
+
onWorkspaceSave: saveWorkspace,
|
|
557
617
|
onRequestRemove: (account) => setRemoveTarget(account.botId),
|
|
558
618
|
onConfirmRemove: (account) => void remove(account),
|
|
559
619
|
onCancelRemove: () => setRemoveTarget(null),
|
|
@@ -7,6 +7,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
|
7
7
|
cancelProvisioning: 'provision.cancel',
|
|
8
8
|
reconnectBot: 'bot.reconnect',
|
|
9
9
|
deleteBot: 'bot.delete',
|
|
10
|
+
setWorkspace: 'bot.workspace.set',
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
const PROVISION_STATES = new Set(['starting', 'pending', 'connecting', 'connected', 'failed', 'cancelled']);
|
|
@@ -80,6 +81,7 @@ function normalizeBot(value) {
|
|
|
80
81
|
botId: id(value.botId),
|
|
81
82
|
connected,
|
|
82
83
|
state: connected ? 'connected' : state,
|
|
84
|
+
workspace: text(value.workspace, '', 4_096),
|
|
83
85
|
bot: {
|
|
84
86
|
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
|
85
87
|
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
|
@@ -3,6 +3,8 @@ import * as React from 'react';
|
|
|
3
3
|
import { WhatsappLogoGlyph } from '../../channel-logos.js';
|
|
4
4
|
import { 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
|
WHATSAPP_ENDPOINTS,
|
|
@@ -159,6 +161,7 @@ export function WhatsappAccountCard({
|
|
|
159
161
|
busy,
|
|
160
162
|
removing,
|
|
161
163
|
onReconnect,
|
|
164
|
+
onWorkspaceSave,
|
|
162
165
|
onRequestRemove,
|
|
163
166
|
onConfirmRemove,
|
|
164
167
|
onCancelRemove,
|
|
@@ -186,6 +189,11 @@ export function WhatsappAccountCard({
|
|
|
186
189
|
h('dd', null, account.connected ? 'WhatsApp Web' : '离线')),
|
|
187
190
|
h('div', { className: 'ddt-metric dim-botMetric' },
|
|
188
191
|
h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
|
192
|
+
h(WorkspaceEditor, {
|
|
193
|
+
workspace: account.workspace,
|
|
194
|
+
disabled: Boolean(busy),
|
|
195
|
+
onSave: onWorkspaceSave,
|
|
196
|
+
}),
|
|
189
197
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
190
198
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
191
199
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
@@ -213,6 +221,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
213
221
|
const [removeTarget, setRemoveTarget] = React.useState(null);
|
|
214
222
|
const [now, setNow] = React.useState(Date.now());
|
|
215
223
|
const mounted = React.useRef(true);
|
|
224
|
+
const workspaceFence = useWorkspaceSnapshotFence();
|
|
216
225
|
const addButtonRef = React.useRef(null);
|
|
217
226
|
|
|
218
227
|
React.useEffect(() => {
|
|
@@ -232,10 +241,13 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
232
241
|
}, [rpcCall]);
|
|
233
242
|
|
|
234
243
|
const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
|
244
|
+
const workspaceVersion = workspaceFence.beginStatus();
|
|
245
|
+
if (workspaceVersion === null) return undefined;
|
|
235
246
|
if (!silent && mounted.current) setModel((current) => ({ ...current, phase: 'loading', error: null }));
|
|
236
247
|
try {
|
|
237
248
|
const snapshot = normalizeSnapshot(await invoke(WHATSAPP_ENDPOINTS.status, {}, signal));
|
|
238
|
-
if (!mounted.current || signal?.aborted
|
|
249
|
+
if (!mounted.current || signal?.aborted
|
|
250
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
|
|
239
251
|
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
240
252
|
if (restore && snapshot.provisioning) setProvision({
|
|
241
253
|
...snapshot.provisioning,
|
|
@@ -243,7 +255,8 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
243
255
|
});
|
|
244
256
|
return snapshot;
|
|
245
257
|
} catch (error) {
|
|
246
|
-
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
|
|
258
|
+
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
|
|
259
|
+
&& workspaceFence.canCommitStatus(workspaceVersion)) {
|
|
247
260
|
setModel((current) => ({
|
|
248
261
|
...current,
|
|
249
262
|
phase: silent ? current.phase : 'error',
|
|
@@ -252,7 +265,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
252
265
|
}
|
|
253
266
|
return undefined;
|
|
254
267
|
}
|
|
255
|
-
}, [invoke]);
|
|
268
|
+
}, [invoke, workspaceFence]);
|
|
256
269
|
|
|
257
270
|
React.useEffect(() => {
|
|
258
271
|
const controller = new AbortController();
|
|
@@ -350,20 +363,23 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
350
363
|
}, [invoke, loadStatus, provision?.attemptId, provision?.status]);
|
|
351
364
|
|
|
352
365
|
const botAction = React.useCallback(async (account, operation, endpoint, payload) => {
|
|
366
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
353
367
|
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
|
|
354
368
|
try {
|
|
355
369
|
const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
|
|
356
|
-
if (mounted.current) {
|
|
370
|
+
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
357
371
|
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
358
372
|
}
|
|
359
373
|
} finally {
|
|
374
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
375
|
+
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
360
376
|
if (mounted.current) setBusyByBot((current) => {
|
|
361
377
|
const next = { ...current };
|
|
362
378
|
delete next[account.botId];
|
|
363
379
|
return next;
|
|
364
380
|
});
|
|
365
381
|
}
|
|
366
|
-
}, [invoke]);
|
|
382
|
+
}, [invoke, loadStatus, workspaceFence]);
|
|
367
383
|
|
|
368
384
|
const botList = model.bots.length > 0
|
|
369
385
|
? h('section', { className: 'dim-listSection' },
|
|
@@ -380,6 +396,12 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
380
396
|
WHATSAPP_ENDPOINTS.reconnectBot,
|
|
381
397
|
{ botId: account.botId },
|
|
382
398
|
),
|
|
399
|
+
onWorkspaceSave: (workspace) => botAction(
|
|
400
|
+
account,
|
|
401
|
+
'workspace',
|
|
402
|
+
WHATSAPP_ENDPOINTS.setWorkspace,
|
|
403
|
+
{ botId: account.botId, workspace },
|
|
404
|
+
),
|
|
383
405
|
onRequestRemove: () => setRemoveTarget(account.botId),
|
|
384
406
|
onCancelRemove: () => setRemoveTarget(null),
|
|
385
407
|
onConfirmRemove: async () => {
|
|
@@ -7,7 +7,8 @@ const EN = Object.freeze({
|
|
|
7
7
|
'IM机器人': 'IM bots',
|
|
8
8
|
'IM机器人设置': 'IM bot settings',
|
|
9
9
|
'IM 渠道': 'IM channels',
|
|
10
|
-
'
|
|
10
|
+
'让聊天机器人轻松接入 DeepSeek Harness': 'Connect IM bots to DeepSeek Harness with ease',
|
|
11
|
+
'帮助与反馈 · 前往 GitHub': 'Help & feedback · Open GitHub',
|
|
11
12
|
'微信': 'WeChat',
|
|
12
13
|
'飞书': 'Feishu',
|
|
13
14
|
'钉钉': 'DingTalk',
|
|
@@ -43,6 +44,19 @@ const EN = Object.freeze({
|
|
|
43
44
|
'已断开': 'Disconnected',
|
|
44
45
|
'消息通道': 'Message channel',
|
|
45
46
|
'最近检查': 'Last checked',
|
|
47
|
+
'当前工作区': 'Current workspace',
|
|
48
|
+
'工作区绝对路径': 'Absolute workspace path',
|
|
49
|
+
'/绝对路径/到/工作区': '/absolute/path/to/workspace',
|
|
50
|
+
'修改': 'Change',
|
|
51
|
+
'保存': 'Save',
|
|
52
|
+
'保存中…': 'Saving…',
|
|
53
|
+
'未设置': 'Not set',
|
|
54
|
+
'工作区修改失败,请重试。': 'Could not update the workspace. Try again.',
|
|
55
|
+
'请输入工作区绝对路径。': 'Enter an absolute workspace path.',
|
|
56
|
+
'工作区必须是绝对路径。': 'The workspace must be an absolute path.',
|
|
57
|
+
'工作区路径不存在。': 'The workspace path does not exist.',
|
|
58
|
+
'工作区路径必须指向一个目录。': 'The workspace path must point to a directory.',
|
|
59
|
+
'找不到要修改的机器人。': 'The bot could not be found.',
|
|
46
60
|
'尚未检查': 'Not checked yet',
|
|
47
61
|
'刚刚': 'Just now',
|
|
48
62
|
'检查连接': 'Check connection',
|