@xmanrui/dsh-im 4.19.2 → 4.20.1
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 +79 -1
- package/README.md +79 -1
- package/lib/client.js +594 -217
- package/lib/index.js +279 -276
- package/package.json +9 -1
- package/plugin-src/client/channels/weixin/api.js +35 -17
- package/plugin-src/client/channels/weixin/connection-error.js +68 -0
- package/plugin-src/client/channels/weixin/index.js +36 -12
- package/plugin-src/client/i18n.js +2 -0
- package/plugin-src/host/channels/shared/startup.mjs +7 -4
- package/plugin-src/host/channels/weixin/connection-supervisor.mjs +13 -1
- package/plugin-src/host/channels/weixin/index.mjs +12 -3
- package/plugin-src/host/channels/weixin/production.mjs +53 -3
- package/plugin-src/host/channels/weixin/rpc.mjs +22 -17
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-menu.mjs +8 -4
- package/src/channels/feishu/bridge.mjs +44 -13
- package/src/channels/qq/qq-bridge.mjs +12 -4
- package/src/channels/qq/qq-menu.mjs +11 -8
- package/src/channels/shared/bot-workspace-store.mjs +532 -40
- package/src/channels/shared/command-catalog.mjs +5 -0
- package/src/channels/shared/compact-command.mjs +14 -4
- package/src/channels/shared/control-command.mjs +1 -1
- package/src/channels/shared/deferred-delivery-coordinator.mjs +1 -1
- package/src/channels/shared/history-command.mjs +1 -1
- package/src/channels/shared/i18n-en/shared-a.mjs +41 -0
- package/src/channels/shared/i18n-en/wecom.mjs +1 -1
- package/src/channels/shared/i18n-en/weixin.mjs +2 -0
- package/src/channels/shared/model-command.mjs +5 -3
- package/src/channels/shared/workspace-command.mjs +114 -9
- package/src/channels/shared/workspace-session.mjs +55 -5
- package/src/channels/telegram/telegram-runtime.mjs +1 -1
- package/src/channels/wecom/wecom-bridge.mjs +14 -46
- package/src/channels/wecom/wecom-runtime.mjs +1 -1
- package/src/channels/weixin/connection-error.en.mjs +116 -0
- package/src/channels/weixin/connection-error.mjs +204 -0
- package/src/channels/weixin/diagnostic-details.mjs +40 -0
- package/src/channels/weixin/state-store.mjs +4 -3
- package/src/channels/weixin/weixin-api.mjs +20 -8
- package/src/channels/weixin/weixin-bridge.mjs +3 -2
- package/src/channels/weixin/weixin-controller.mjs +133 -104
- package/src/channels/weixin/weixin-runtime.mjs +35 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.20.1",
|
|
4
4
|
"description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -56,6 +56,14 @@
|
|
|
56
56
|
{
|
|
57
57
|
"name": "grloper",
|
|
58
58
|
"url": "https://github.com/grloper"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "baijian",
|
|
62
|
+
"url": "https://github.com/baijian"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "lyzhu86",
|
|
66
|
+
"url": "https://github.com/lyzhu86"
|
|
59
67
|
}
|
|
60
68
|
],
|
|
61
69
|
"license": "MIT",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeWeixinDiagnosticDetails } from '../../../../src/channels/weixin/diagnostic-details.mjs';
|
|
1
2
|
import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
|
|
2
3
|
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
|
3
4
|
import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
|
|
@@ -57,13 +58,38 @@ function normalizeTestMessage(value) {
|
|
|
57
58
|
return { sent: false, code };
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
export function normalizeConnectionError(value, fallbackCode = 'WEIXIN_ERROR', fallbackMessage = '微信操作失败,请稍后重试') {
|
|
62
|
+
const details = normalizeWeixinDiagnosticDetails(value?.details);
|
|
63
|
+
return {
|
|
64
|
+
code: string(value?.code, fallbackCode).slice(0, 100),
|
|
65
|
+
message: string(value?.message, fallbackMessage).slice(0, 500),
|
|
66
|
+
...(Object.keys(details).length ? { details } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function managementRequestError(cause, operation) {
|
|
71
|
+
if (cause?.name === 'AbortError') return cause;
|
|
72
|
+
const error = new Error('无法完成微信管理请求,请检查 DSH 连接后重新读取状态。');
|
|
73
|
+
error.code = 'weixin-management-unreachable';
|
|
74
|
+
error.details = normalizeWeixinDiagnosticDetails({ operation, stage: 'management.request', occurredAt: new Date().toISOString() });
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function invalidResponse() {
|
|
79
|
+
const error = new Error('DSH 微信管理接口返回了无法识别的响应,请重新读取状态。');
|
|
80
|
+
error.code = 'weixin-management-invalid-response';
|
|
81
|
+
error.details = { stage: 'management.request', occurredAt: new Date().toISOString() };
|
|
82
|
+
return error;
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
export function unwrapRpcResult(result) {
|
|
61
86
|
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
|
62
|
-
throw
|
|
87
|
+
throw invalidResponse();
|
|
63
88
|
}
|
|
64
89
|
if (!result.ok) {
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
if (!isRecord(result.error)) throw invalidResponse();
|
|
91
|
+
const visible = normalizeConnectionError(result.error, 'WEIXIN_RPC_ERROR', '微信操作失败');
|
|
92
|
+
const error = Object.assign(new Error(visible.message), visible);
|
|
67
93
|
throw error;
|
|
68
94
|
}
|
|
69
95
|
return result.value;
|
|
@@ -93,7 +119,7 @@ export function safeVerificationUrl(value) {
|
|
|
93
119
|
|
|
94
120
|
export function normalizeProvisioning(value) {
|
|
95
121
|
if (!isRecord(value) || !string(value.attemptId)) {
|
|
96
|
-
throw
|
|
122
|
+
throw invalidResponse();
|
|
97
123
|
}
|
|
98
124
|
const status = PROVISION_STATES.has(value.status) ? value.status : 'failed';
|
|
99
125
|
const result = {
|
|
@@ -110,10 +136,7 @@ export function normalizeProvisioning(value) {
|
|
|
110
136
|
if (string(value.botId)) result.botId = string(value.botId);
|
|
111
137
|
if (value.alreadyConnected === true) result.alreadyConnected = true;
|
|
112
138
|
if (isRecord(value.error)) {
|
|
113
|
-
result.error =
|
|
114
|
-
code: string(value.error.code, 'WEIXIN_PROVISION_FAILED'),
|
|
115
|
-
message: string(value.error.message, '微信绑定没有完成'),
|
|
116
|
-
};
|
|
139
|
+
result.error = normalizeConnectionError(value.error, 'WEIXIN_PROVISION_FAILED', '微信绑定没有完成');
|
|
117
140
|
}
|
|
118
141
|
return result;
|
|
119
142
|
}
|
|
@@ -150,17 +173,14 @@ function normalizeBot(value) {
|
|
|
150
173
|
},
|
|
151
174
|
lastMessageError: normalizeLastMessageError(value.lastMessageError),
|
|
152
175
|
error: isRecord(value.error)
|
|
153
|
-
?
|
|
154
|
-
code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
|
|
155
|
-
message: string(value.error.message, '微信连接未就绪'),
|
|
156
|
-
}
|
|
176
|
+
? normalizeConnectionError(value.error, 'WEIXIN_ACCOUNT_ERROR', '微信连接未就绪')
|
|
157
177
|
: null,
|
|
158
178
|
};
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
export function normalizeSnapshot(value) {
|
|
162
182
|
if (!isRecord(value) || !Array.isArray(value.bots)) {
|
|
163
|
-
throw
|
|
183
|
+
throw invalidResponse();
|
|
164
184
|
}
|
|
165
185
|
const bots = value.bots.map(normalizeBot).filter(Boolean);
|
|
166
186
|
return {
|
|
@@ -174,16 +194,14 @@ export function normalizeSnapshot(value) {
|
|
|
174
194
|
},
|
|
175
195
|
provisioning: value.provisioning ? normalizeProvisioning(value.provisioning) : null,
|
|
176
196
|
testMessage: normalizeTestMessage(value.testMessage),
|
|
197
|
+
warnings: Array.isArray(value.warnings) ? value.warnings.filter(isRecord).slice(0, 8).map(error => normalizeConnectionError(error)) : [],
|
|
177
198
|
agentPresetCatalog: normalizeAgentPresetCatalog(value.agentPresetCatalog),
|
|
178
199
|
modelCatalog: normalizeModelCatalog(value.modelCatalog),
|
|
179
200
|
};
|
|
180
201
|
}
|
|
181
202
|
|
|
182
203
|
export function presentError(error) {
|
|
183
|
-
return
|
|
184
|
-
code: string(error?.code, 'WEIXIN_ERROR'),
|
|
185
|
-
message: string(error?.message, '微信操作失败,请稍后重试'),
|
|
186
|
-
};
|
|
204
|
+
return normalizeConnectionError(error);
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
export function formatRemaining(milliseconds) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { h, localizeText } from '../../i18n.js';
|
|
3
|
+
import { normalizeConnectionError } from './api.js';
|
|
4
|
+
|
|
5
|
+
const STAGE_LABELS = {
|
|
6
|
+
'startup.load': '加载微信配置', 'qr.begin': '申请二维码', 'qr.encode': '生成二维码图片', 'qr.poll': '查询扫码状态',
|
|
7
|
+
'qr.verify': '提交配对码', 'qr.cancel': '取消绑定', 'credential.read': '读取登录凭据', 'credential.save': '保存登录凭据',
|
|
8
|
+
'credential.remove': '移除登录凭据', 'account.save': '保存账号配置', 'account.remove': '移除账号配置',
|
|
9
|
+
'state.load': '读取账号状态', 'state.write': '保存账号状态', 'state.cleanup': '清理账号状态',
|
|
10
|
+
'workspace.write': '保存工作区设置', 'workspace.cleanup': '清理工作区设置', 'runtime.prepare': '准备消息连接',
|
|
11
|
+
activation: '激活微信账号', 'harness.check': '检查 DSH 宿主', 'connection.start': '启动微信连接',
|
|
12
|
+
'connection.poll': '同步微信消息', 'connection.stop': '停止微信连接', 'status.read': '读取连接状态',
|
|
13
|
+
rollback: '恢复原账号状态', 'management.request': '访问 DSH 管理接口',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function provisioningErrorTitle(error) {
|
|
17
|
+
const stage = error?.details?.stage;
|
|
18
|
+
if (stage === 'qr.begin' || stage === 'qr.encode') return '无法生成微信二维码';
|
|
19
|
+
if (stage === 'qr.poll') return '查询微信扫码状态失败';
|
|
20
|
+
if (stage === 'management.request') return '无法完成微信管理请求';
|
|
21
|
+
return '微信没有绑定完成';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatWeixinDiagnostic(value) {
|
|
25
|
+
const error = normalizeConnectionError(value);
|
|
26
|
+
const details = error.details ?? {};
|
|
27
|
+
return [
|
|
28
|
+
localizeText(error.message), details.hint ? localizeText(details.hint) : null,
|
|
29
|
+
localizeText('错误码') + ': ' + error.code,
|
|
30
|
+
...['operation', 'stage', 'reason', 'httpStatus', 'providerCode', 'resource', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
31
|
+
.filter(field => details[field] !== undefined)
|
|
32
|
+
.map(field => `${field}: ${details[field]}`),
|
|
33
|
+
].filter(Boolean).join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function WeixinConnectionError({ error: value, warning = false }) {
|
|
37
|
+
const error = normalizeConnectionError(value);
|
|
38
|
+
const details = error.details ?? {};
|
|
39
|
+
const [copyState, setCopyState] = React.useState(null);
|
|
40
|
+
React.useEffect(() => setCopyState(null), [details.referenceId, error.code, error.message]);
|
|
41
|
+
const copy = async () => {
|
|
42
|
+
try {
|
|
43
|
+
if (typeof globalThis.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable');
|
|
44
|
+
await globalThis.navigator.clipboard.writeText(formatWeixinDiagnostic(error));
|
|
45
|
+
setCopyState('copied');
|
|
46
|
+
} catch { setCopyState('manual'); }
|
|
47
|
+
};
|
|
48
|
+
const fields = [
|
|
49
|
+
['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
|
|
50
|
+
['底层原因', details.reason], ['HTTP 状态', details.httpStatus], ['微信返回码', details.providerCode],
|
|
51
|
+
['参考号', details.referenceId], ['发生时间', details.occurredAt], ['插件版本', details.pluginVersion],
|
|
52
|
+
].filter(([, text]) => text !== undefined && text !== '');
|
|
53
|
+
return h('div', { className: 'dxw-summary dim-cardSummary', 'data-weixin-diagnostic': true, role: warning ? 'status' : undefined },
|
|
54
|
+
h('p', null, error.message),
|
|
55
|
+
details.hint ? h('p', null, details.hint) : null,
|
|
56
|
+
h('details', { style: { marginTop: 8 } },
|
|
57
|
+
h('summary', { style: { cursor: 'pointer' } }, '诊断详情'),
|
|
58
|
+
h('dl', { style: { display: 'grid', gridTemplateColumns: 'auto minmax(0, 1fr)', gap: '4px 12px', margin: '10px 0' } },
|
|
59
|
+
...fields.flatMap(([label, text]) => [h('dt', { key: `${label}-label` }, label),
|
|
60
|
+
React.createElement('dd', { key: label, style: { margin: 0, overflowWrap: 'anywhere' } }, String(text))])),
|
|
61
|
+
!details.referenceId ? h('p', null, '未取得 Host 诊断参考号。') : null,
|
|
62
|
+
h('div', { className: 'dim-viewActions' },
|
|
63
|
+
h('button', { type: 'button', className: 'dxw-button', onClick: copy }, copyState === 'copied' ? '诊断信息已复制' : '复制诊断信息')),
|
|
64
|
+
copyState === 'manual' ? h('div', null,
|
|
65
|
+
h('p', null, '无法访问剪贴板,请选择并复制以下诊断信息。'),
|
|
66
|
+
React.createElement('textarea', { readOnly: true, value: formatWeixinDiagnostic(error), rows: 7,
|
|
67
|
+
'aria-label': localizeText('诊断信息'), style: { width: '100%', boxSizing: 'border-box', marginTop: 8 } })) : null));
|
|
68
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WeixinConnectionError, provisioningErrorTitle } from './connection-error.js';
|
|
1
2
|
import { BotName } from '../../bot-alias.js';
|
|
2
3
|
import * as React from 'react';
|
|
3
4
|
|
|
@@ -7,6 +8,7 @@ import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
|
|
|
7
8
|
import { h } from '../../i18n.js';
|
|
8
9
|
import {
|
|
9
10
|
WEIXIN_ENDPOINTS,
|
|
11
|
+
managementRequestError,
|
|
10
12
|
formatRemaining,
|
|
11
13
|
normalizeProvisioning,
|
|
12
14
|
normalizeSnapshot,
|
|
@@ -189,9 +191,8 @@ function ProvisionError({ provision, busy, onRetry, onClose }) {
|
|
|
189
191
|
const error = provision.error ?? { code: 'WEIXIN_PROVISION_FAILED', message: '微信绑定没有完成' };
|
|
190
192
|
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
|
191
193
|
h('div', { className: 'dxw-error dim-inlineError', role: 'alert' },
|
|
192
|
-
h('h3', null, provision.status === 'expired' ? '二维码已过期' :
|
|
193
|
-
h(
|
|
194
|
-
h('span', { className: 'dxw-errorCode' }, error.code),
|
|
194
|
+
h('h3', null, provision.status === 'expired' ? '二维码已过期' : provisioningErrorTitle(error)),
|
|
195
|
+
h(WeixinConnectionError, { error }),
|
|
195
196
|
h('div', { className: 'dxw-actions dim-viewActions' },
|
|
196
197
|
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
|
197
198
|
h(Button, { onClick: onClose, disabled: busy }, '关闭'))));
|
|
@@ -283,7 +284,8 @@ export function AccountCard({
|
|
|
283
284
|
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
|
|
284
285
|
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
|
285
286
|
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
|
|
286
|
-
|
|
287
|
+
account.error ? h(WeixinConnectionError, { error: account.error })
|
|
288
|
+
: summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
|
|
287
289
|
account.lastMessageError ? h(LastMessageErrorSummary, {
|
|
288
290
|
className: 'dxw-summary',
|
|
289
291
|
error: account.lastMessageError,
|
|
@@ -292,7 +294,7 @@ export function AccountCard({
|
|
|
292
294
|
className: 'dxw-summary dim-cardFeedback',
|
|
293
295
|
role: 'status',
|
|
294
296
|
'aria-live': 'polite',
|
|
295
|
-
}, feedback) : null)),
|
|
297
|
+
}, typeof feedback === 'string' ? feedback : h(WeixinConnectionError, { error: feedback })) : null)),
|
|
296
298
|
),
|
|
297
299
|
),
|
|
298
300
|
removing ? h('div', { className: 'dxw-confirm dim-confirm', role: 'alertdialog' },
|
|
@@ -358,6 +360,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
358
360
|
const [feedbackByBot, setFeedbackByBot] = React.useState({});
|
|
359
361
|
const [removeTarget, setRemoveTarget] = React.useState(null);
|
|
360
362
|
const [notice, setNotice] = React.useState('');
|
|
363
|
+
const [operationWarnings, setOperationWarnings] = React.useState([]);
|
|
361
364
|
const [now, setNow] = React.useState(() => Date.now());
|
|
362
365
|
const addButtonRef = React.useRef(null);
|
|
363
366
|
const mountedRef = React.useRef(true);
|
|
@@ -376,7 +379,13 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
376
379
|
}, 'announcement');
|
|
377
380
|
}, [scheduleAnimationFrame]);
|
|
378
381
|
const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
|
|
379
|
-
|
|
382
|
+
let result;
|
|
383
|
+
try { result = await rpcCall(endpoint, payload, signal); }
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (signal?.aborted) throw error;
|
|
386
|
+
throw managementRequestError(error, endpoint);
|
|
387
|
+
}
|
|
388
|
+
return unwrapRpcResult(result);
|
|
380
389
|
}, [rpcCall]);
|
|
381
390
|
const loadStatus = React.useCallback(async ({
|
|
382
391
|
signal,
|
|
@@ -606,12 +615,12 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
606
615
|
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
|
|
607
616
|
}
|
|
608
617
|
announce(feedback);
|
|
609
|
-
} catch {
|
|
610
|
-
const feedback =
|
|
618
|
+
} catch (error) {
|
|
619
|
+
const feedback = presentError(error);
|
|
611
620
|
if (mountedRef.current) {
|
|
612
621
|
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
|
|
613
622
|
}
|
|
614
|
-
announce(feedback);
|
|
623
|
+
announce(feedback.message);
|
|
615
624
|
} finally {
|
|
616
625
|
const shouldRefresh = workspaceFence.endMutation();
|
|
617
626
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
@@ -622,6 +631,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
622
631
|
const saveWorkspace = React.useCallback(async (account, workspace) => {
|
|
623
632
|
const workspaceVersion = workspaceFence.beginMutation();
|
|
624
633
|
setBotBusy(account.botId, 'workspace');
|
|
634
|
+
setFeedbackByBot(current => ({ ...current, [account.botId]: null }));
|
|
625
635
|
try {
|
|
626
636
|
const snapshot = normalizeSnapshot(await invoke(
|
|
627
637
|
WEIXIN_ENDPOINTS.setWorkspace,
|
|
@@ -635,6 +645,9 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
635
645
|
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
|
636
646
|
});
|
|
637
647
|
}
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (mountedRef.current) setFeedbackByBot(current => ({ ...current, [account.botId]: presentError(error) }));
|
|
650
|
+
throw error;
|
|
638
651
|
} finally {
|
|
639
652
|
const shouldRefresh = workspaceFence.endMutation();
|
|
640
653
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
@@ -645,6 +658,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
645
658
|
const saveBotSetting = React.useCallback(async (account, operation, endpoint, payload) => {
|
|
646
659
|
const snapshotVersion = workspaceFence.beginMutation();
|
|
647
660
|
setBotBusy(account.botId, operation);
|
|
661
|
+
setFeedbackByBot(current => ({ ...current, [account.botId]: null }));
|
|
648
662
|
try {
|
|
649
663
|
const snapshot = normalizeSnapshot(await invoke(
|
|
650
664
|
endpoint,
|
|
@@ -658,6 +672,9 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
658
672
|
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
|
659
673
|
});
|
|
660
674
|
}
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (mountedRef.current) setFeedbackByBot(current => ({ ...current, [account.botId]: presentError(error) }));
|
|
677
|
+
throw error;
|
|
661
678
|
} finally {
|
|
662
679
|
const shouldRefresh = workspaceFence.endMutation();
|
|
663
680
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
@@ -668,6 +685,8 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
668
685
|
const remove = React.useCallback(async (account) => {
|
|
669
686
|
const snapshotVersion = workspaceFence.beginMutation();
|
|
670
687
|
setBotBusy(account.botId, 'delete');
|
|
688
|
+
setOperationWarnings([]);
|
|
689
|
+
setFeedbackByBot(current => ({ ...current, [account.botId]: null }));
|
|
671
690
|
try {
|
|
672
691
|
const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.deleteBot, {
|
|
673
692
|
botId: account.botId,
|
|
@@ -680,10 +699,13 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
680
699
|
modelCatalog: snapshot.modelCatalog ?? current.modelCatalog,
|
|
681
700
|
}));
|
|
682
701
|
}
|
|
702
|
+
setOperationWarnings(snapshot.warnings);
|
|
683
703
|
setRemoveTarget(null);
|
|
684
704
|
announce('微信账号及本机凭据已移除。');
|
|
685
705
|
} catch (error) {
|
|
686
|
-
|
|
706
|
+
const failure = presentError(error);
|
|
707
|
+
if (mountedRef.current) setFeedbackByBot(current => ({ ...current, [account.botId]: failure }));
|
|
708
|
+
announce(failure.message);
|
|
687
709
|
} finally {
|
|
688
710
|
const shouldRefresh = workspaceFence.endMutation();
|
|
689
711
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
|
@@ -731,8 +753,10 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
731
753
|
addButtonRef,
|
|
732
754
|
}),
|
|
733
755
|
h('div', { className: 'dxw-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
|
756
|
+
...operationWarnings.map((error, index) => h(WeixinConnectionError, { key: error.details?.referenceId ?? index, error, warning: true })),
|
|
734
757
|
model.error && model.phase === 'ready'
|
|
735
|
-
? h('div', { className: 'dxw-statusNotice dim-statusNotice' },
|
|
758
|
+
? h('div', { className: 'dxw-statusNotice dim-statusNotice' },
|
|
759
|
+
h('p', null, '状态读取失败,以下是上次读取的状态。'), h(WeixinConnectionError, { error: model.error }))
|
|
736
760
|
: null,
|
|
737
761
|
model.phase === 'loading'
|
|
738
762
|
? h(LoadingView)
|
|
@@ -740,7 +764,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
740
764
|
? h('div', { className: 'dxw-card dim-surfaceCard' },
|
|
741
765
|
h('div', { className: 'dxw-error dim-inlineError' },
|
|
742
766
|
h('h3', null, '无法读取微信状态'),
|
|
743
|
-
h(
|
|
767
|
+
h(WeixinConnectionError, { error: model.error }),
|
|
744
768
|
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
|
745
769
|
: h(React.Fragment, null,
|
|
746
770
|
provisionView,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import weixinDiagnostics from '../../src/channels/weixin/connection-error.en.mjs';
|
|
1
2
|
import * as React from 'react';
|
|
2
3
|
|
|
3
4
|
export const IM_LOCALE_NAMESPACE = 'dsh-im';
|
|
4
5
|
|
|
5
6
|
const EN = Object.freeze({
|
|
7
|
+
...weixinDiagnostics,
|
|
6
8
|
'$locale': 'en',
|
|
7
9
|
'修改别名': 'Edit alias',
|
|
8
10
|
'关闭修改别名': 'Close alias editor',
|
|
@@ -26,7 +26,7 @@ function followHostLanguage(ctx, channel, controller, logger) {
|
|
|
26
26
|
|
|
27
27
|
/** Mount the native management RPC before any fallible production initialization. */
|
|
28
28
|
export async function installProductionChannel(ctx, config, {
|
|
29
|
-
channel, rpcChannel, createProduction, createHandler,
|
|
29
|
+
channel, rpcChannel, createProduction, createHandler, reportStartupError,
|
|
30
30
|
}) {
|
|
31
31
|
let startupError = publicChannelInitializing(channel);
|
|
32
32
|
let handler = async () => ({ ok: false, error: startupError });
|
|
@@ -56,12 +56,15 @@ export async function installProductionChannel(ctx, config, {
|
|
|
56
56
|
followHostLanguage(ctx, channel, production.controller, logger);
|
|
57
57
|
handler = readyHandler;
|
|
58
58
|
} catch (error) {
|
|
59
|
-
startupError =
|
|
60
|
-
|
|
59
|
+
startupError = reportStartupError
|
|
60
|
+
? reportStartupError(error, false)
|
|
61
|
+
: publicChannelStartupError(channel, error);
|
|
62
|
+
if (!reportStartupError) logger.error?.(`[dsh-im] failed to activate ${channel}; management RPC remains available`, error);
|
|
61
63
|
try {
|
|
62
64
|
await closeProduction();
|
|
63
65
|
} catch (cleanupError) {
|
|
64
|
-
|
|
66
|
+
if (reportStartupError) reportStartupError(cleanupError, true);
|
|
67
|
+
else logger.error?.(`[dsh-im] failed to close partially initialized ${channel} resources`, cleanupError);
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
return disposeRpc;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createWeixinDiagnostics } from '../../../../src/channels/weixin/connection-error.mjs';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000, 5_000, 10_000, 30_000]);
|
|
2
4
|
|
|
3
5
|
function retryDelays(value) {
|
|
@@ -10,6 +12,7 @@ export class ConnectionSupervisor {
|
|
|
10
12
|
#controller;
|
|
11
13
|
#harness;
|
|
12
14
|
#logger;
|
|
15
|
+
#diagnostics;
|
|
13
16
|
#retryDelays;
|
|
14
17
|
#healthyIntervalMs;
|
|
15
18
|
#setTimeout;
|
|
@@ -26,6 +29,7 @@ export class ConnectionSupervisor {
|
|
|
26
29
|
controller,
|
|
27
30
|
harness,
|
|
28
31
|
logger = console,
|
|
32
|
+
diagnostics,
|
|
29
33
|
retryDelaysMs,
|
|
30
34
|
healthyIntervalMs = 15_000,
|
|
31
35
|
setTimeoutImpl = setTimeout,
|
|
@@ -40,6 +44,7 @@ export class ConnectionSupervisor {
|
|
|
40
44
|
this.#controller = controller;
|
|
41
45
|
this.#harness = harness;
|
|
42
46
|
this.#logger = logger;
|
|
47
|
+
this.#diagnostics = diagnostics ?? createWeixinDiagnostics({ logger });
|
|
43
48
|
this.#retryDelays = retryDelays(retryDelaysMs);
|
|
44
49
|
this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0
|
|
45
50
|
? healthyIntervalMs
|
|
@@ -116,7 +121,14 @@ export class ConnectionSupervisor {
|
|
|
116
121
|
if (this.#closed) return;
|
|
117
122
|
const delayMs = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)];
|
|
118
123
|
this.#retryIndex += 1;
|
|
119
|
-
this.#
|
|
124
|
+
if (typeof this.#controller.reportRestoreFailure === 'function') {
|
|
125
|
+
try { await this.#controller.reportRestoreFailure(error); }
|
|
126
|
+
catch {
|
|
127
|
+
this.#diagnostics.report(error, { operation: 'connection.restore', stage: 'harness.check', code: 'harness-check-unknown-failed', automatic: true });
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
this.#diagnostics.report(error, { operation: 'connection.restore', stage: 'harness.check', code: 'harness-check-unknown-failed', automatic: true });
|
|
131
|
+
}
|
|
120
132
|
this.#schedule(delayMs);
|
|
121
133
|
}
|
|
122
134
|
}
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
2
|
import { createWeixinRpcHandler, installWeixinRpc, WEIXIN_RPC_CHANNEL } from './rpc.mjs';
|
|
3
3
|
import { installProductionChannel } from '../shared/startup.mjs';
|
|
4
|
+
import { publicChannelStartupError } from '../shared/startup-error.mjs';
|
|
5
|
+
import { createWeixinDiagnostics } from '../../../../src/channels/weixin/connection-error.mjs';
|
|
4
6
|
|
|
5
7
|
export const name = 'dsh-weixin-host';
|
|
6
8
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
7
9
|
|
|
8
10
|
export async function apply(ctx, config = {}) {
|
|
11
|
+
const logger = typeof ctx.logger === 'function' ? ctx.logger('dsh-weixin') : (ctx.logger ?? console);
|
|
12
|
+
const diagnostics = createWeixinDiagnostics({ logger });
|
|
13
|
+
const rpcOptions = { ...config.rpcOptions, logger, diagnostics };
|
|
9
14
|
if (config?.controller) {
|
|
10
|
-
return installWeixinRpc(ctx, config.controller,
|
|
15
|
+
return installWeixinRpc(ctx, config.controller, rpcOptions, config.rpcAuthority);
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
return installProductionChannel(ctx, config, {
|
|
14
19
|
channel: 'weixin',
|
|
15
20
|
rpcChannel: WEIXIN_RPC_CHANNEL,
|
|
16
|
-
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
17
|
-
createHandler: controller => createWeixinRpcHandler(controller,
|
|
21
|
+
createProduction: () => createProductionController(ctx, config, { ...config.internals, diagnostics }),
|
|
22
|
+
createHandler: controller => createWeixinRpcHandler(controller, rpcOptions),
|
|
23
|
+
reportStartupError: (error, warning) => diagnostics.report(error, {
|
|
24
|
+
operation: 'startup', stage: warning ? 'connection.stop' : 'startup.load', warning,
|
|
25
|
+
code: publicChannelStartupError('weixin', error).code,
|
|
26
|
+
}).publicError,
|
|
18
27
|
});
|
|
19
28
|
}
|
|
20
29
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../../../../src/channels/weixin/weixin-api.mjs';
|
|
12
12
|
import { WeixinController } from '../../../../src/channels/weixin/weixin-controller.mjs';
|
|
13
13
|
import { WeixinRuntime } from '../../../../src/channels/weixin/weixin-runtime.mjs';
|
|
14
|
+
import { createWeixinDiagnostics, knownWeixinErrorCode, weixinStageError } from '../../../../src/channels/weixin/connection-error.mjs';
|
|
14
15
|
import {
|
|
15
16
|
BotWorkspaceStore,
|
|
16
17
|
createBotWorkspaceScope,
|
|
@@ -44,6 +45,33 @@ function pluginPaths(config) {
|
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// Keep shared workspace lifecycle semantics; expose its fallible persistence only to Weixin.
|
|
49
|
+
function diagnosticWorkspaces(store) {
|
|
50
|
+
const writes = new Set(['ensure', 'setWorkspace', 'setModel', 'setAgentPreset', 'setAlias', 'setContextEnhancement', 'setAccessPolicy', 'flushPendingRemoval']);
|
|
51
|
+
const removals = new Set(['retireAfterConfigCommit', 'finishRemoval']);
|
|
52
|
+
return new Proxy(store, {
|
|
53
|
+
get(target, property) {
|
|
54
|
+
const value = Reflect.get(target, property, target);
|
|
55
|
+
if (typeof value !== 'function') return value;
|
|
56
|
+
if (!writes.has(property) && !removals.has(property)) return value.bind(target);
|
|
57
|
+
return (...args) => {
|
|
58
|
+
const failure = error => {
|
|
59
|
+
throw knownWeixinErrorCode(error?.code) ? error
|
|
60
|
+
: weixinStageError(removals.has(property) ? 'workspace-cleanup-failed' : 'workspace-save-failed', error);
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
const result = value.apply(target, args);
|
|
64
|
+
// flushPendingRemoval must remain synchronous when no cleanup is pending.
|
|
65
|
+
return result?.then ? result.then(value => {
|
|
66
|
+
if (removals.has(property) && value?.error) failure(value.error);
|
|
67
|
+
return value;
|
|
68
|
+
}, failure) : result;
|
|
69
|
+
} catch (error) { return failure(error); }
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
48
76
|
if (!ctx?.credentials) throw new TypeError('dsh-weixin requires ctx.credentials');
|
|
49
77
|
const connection = harnessConnection(ctx, config);
|
|
@@ -58,13 +86,14 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
58
86
|
const logger = typeof ctx.logger === 'function'
|
|
59
87
|
? ctx.logger('dsh-weixin')
|
|
60
88
|
: (ctx.logger ?? console);
|
|
89
|
+
const diagnostics = internals.diagnostics ?? createWeixinDiagnostics({ logger });
|
|
61
90
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
62
91
|
const paths = pluginPaths(config);
|
|
63
92
|
const configStore = await new ConfigStore(paths.config).load();
|
|
64
93
|
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
65
94
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
66
|
-
const workspaces = internals.workspaces
|
|
67
|
-
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
95
|
+
const workspaces = diagnosticWorkspaces(internals.workspaces
|
|
96
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load());
|
|
68
97
|
const configuredBots = configStore.list();
|
|
69
98
|
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
|
|
70
99
|
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
|
|
@@ -115,6 +144,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
115
144
|
credentials: ctx.credentials,
|
|
116
145
|
configStore: observedConfigStore,
|
|
117
146
|
logger,
|
|
147
|
+
diagnostics,
|
|
118
148
|
createRuntime: async ({ botId, config: accountConfig, token }) => {
|
|
119
149
|
const state = await stateFor(botId);
|
|
120
150
|
await workspaces.ensure(botId, {
|
|
@@ -125,6 +155,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
125
155
|
botId, workspaces, state, agentPresetCatalog,
|
|
126
156
|
});
|
|
127
157
|
return new Runtime({
|
|
158
|
+
diagnostics,
|
|
128
159
|
api,
|
|
129
160
|
config: accountConfig,
|
|
130
161
|
token,
|
|
@@ -158,16 +189,35 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
158
189
|
}
|
|
159
190
|
},
|
|
160
191
|
});
|
|
161
|
-
const
|
|
192
|
+
const workspaceController = createWorkspaceAwareController(coreController, {
|
|
162
193
|
workspaces,
|
|
163
194
|
stateFor,
|
|
164
195
|
agentPresetCatalog,
|
|
165
196
|
modelCatalog,
|
|
166
197
|
});
|
|
198
|
+
const controller = new Proxy(workspaceController, {
|
|
199
|
+
get(target, property) {
|
|
200
|
+
if (property === 'deleteBot') return async (...args) => {
|
|
201
|
+
const existed = Boolean(configStore.get(args[0]));
|
|
202
|
+
try { return await target.deleteBot(...args); }
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (!existed || configStore.get(args[0])) throw error;
|
|
205
|
+
// The shared workspace wrapper can fail cleaning up after the account commit.
|
|
206
|
+
const warning = diagnostics.report(weixinStageError('workspace-cleanup-failed', error), {
|
|
207
|
+
operation: 'bot.delete', botId: args[0], warning: true,
|
|
208
|
+
}).publicError;
|
|
209
|
+
try { return { ...await target.status(), warnings: [warning] }; }
|
|
210
|
+
catch (readError) { throw weixinStageError('status-read-failed', readError, 'status.read'); }
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
return Reflect.get(target, property);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
167
216
|
const supervisor = createSupervisor({
|
|
168
217
|
controller,
|
|
169
218
|
harness,
|
|
170
219
|
logger,
|
|
220
|
+
diagnostics,
|
|
171
221
|
retryDelaysMs: config.retryDelaysMs,
|
|
172
222
|
healthyIntervalMs: config.healthyIntervalMs,
|
|
173
223
|
}).start();
|