@xmanrui/dsh-im 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +13 -3
- package/lib/index.js +118 -118
- package/package.json +1 -1
- package/plugin-src/client/i18n.js +8 -0
- package/plugin-src/client/index.js +2 -1
- package/plugin-src/client/styles.js +2 -1
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-controller.mjs +56 -12
- package/src/channels/weixin/weixin-runtime.mjs +12 -1
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ const EN = Object.freeze({
|
|
|
9
9
|
'IM 渠道': 'IM channels',
|
|
10
10
|
'让 DeepSeek Harness 触手可及': 'DeepSeek Harness, always within reach',
|
|
11
11
|
'AI Office': 'AI Office',
|
|
12
|
+
'(实验功能)': '(Experimental)',
|
|
12
13
|
'AI Office 设置': 'AI Office settings',
|
|
13
14
|
'AI Office 设置页缺少 RPC 连接': 'AI Office settings are missing an RPC connection',
|
|
14
15
|
'正在读取 AI Office Connector…': 'Loading AI Office Connector…',
|
|
@@ -230,6 +231,13 @@ const EN = Object.freeze({
|
|
|
230
231
|
'这是微信附加的安全确认步骤。配对码只用于本次扫码轮询,不会写入配置或日志。': 'This is an additional WeChat confirmation step. The pairing code is used only for this connection and is never stored.',
|
|
231
232
|
'正在保存凭据并验证 Harness 与微信长轮询。': 'Saving credentials and verifying the WeChat connection.',
|
|
232
233
|
'微信已确认,正在启动消息连接': 'Confirmed in WeChat. Starting the message connection',
|
|
234
|
+
'微信已授权,但无法读取现有登录凭据。请检查 DSH 凭据存储。': 'WeChat was authorized, but the existing login credential could not be read. Check the DSH credential store.',
|
|
235
|
+
'微信已授权,但登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。': 'WeChat was authorized, but the login credential could not be written to the DSH credential store. Check that the store is writable.',
|
|
236
|
+
'微信已授权,但账号配置无法写入本机。请检查 DSH_HOME 目录权限。': 'WeChat was authorized, but the account configuration could not be saved locally. Check the DSH_HOME directory permissions.',
|
|
237
|
+
'微信已授权,但无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。': 'WeChat was authorized, but the account state or workspace could not be initialized. Check DSH_HOME and the workspace directory.',
|
|
238
|
+
'微信已授权,但插件无法连接本机 Harness。请确认 dsh web 已正常启动。': 'WeChat was authorized, but the plugin could not reach the local Harness. Confirm that dsh web is running normally.',
|
|
239
|
+
'微信已授权,但消息连接初始化失败。请查看 dsh web 日志后重试。': 'WeChat was authorized, but the message connection could not be initialized. Check the dsh web logs and try again.',
|
|
240
|
+
'微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。': 'WeChat was authorized, but an unknown error occurred during activation. Check the dsh web logs.',
|
|
233
241
|
'微信已绑定,可以开始向已绑定的机器人发消息。': 'WeChat is connected and ready for messages.',
|
|
234
242
|
'这个微信账号已经绑定并保持在线。': 'This WeChat account is connected and online.',
|
|
235
243
|
'微信账号及本机凭据已移除。': 'The WeChat account and local credentials were removed.',
|
|
@@ -58,7 +58,7 @@ const CHANNELS = Object.freeze([
|
|
|
58
58
|
{ id: 'telegram', label: 'Telegram' },
|
|
59
59
|
{ id: 'discord', label: 'Discord' },
|
|
60
60
|
{ id: 'whatsapp', label: 'WhatsApp' },
|
|
61
|
-
{ id: 'office', label: 'AI Office' },
|
|
61
|
+
{ id: 'office', label: 'AI Office', note: '(实验功能)' },
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
64
|
function WeixinLogo() {
|
|
@@ -176,6 +176,7 @@ export function IMSettingsTab({
|
|
|
176
176
|
h(ChannelLogo, { channel: channel.id }),
|
|
177
177
|
h('span', { className: 'dim-channelCopy' },
|
|
178
178
|
h('strong', null, channel.label),
|
|
179
|
+
channel.note ? h('small', { className: 'dim-channelNote' }, channel.note) : null,
|
|
179
180
|
)))),
|
|
180
181
|
h('div', { className: 'dim-divider', 'aria-hidden': 'true' }),
|
|
181
182
|
h('main', {
|
|
@@ -52,8 +52,9 @@ const CSS = String.raw`
|
|
|
52
52
|
.dim-logoSlack svg { width: 21px; height: 21px; }
|
|
53
53
|
.dim-logoWhatsapp { color: white; background: #25d366; }
|
|
54
54
|
.dim-logoWhatsapp svg { width: 21px; height: 21px; }
|
|
55
|
-
.dim-channelCopy { min-width: 0; display:
|
|
55
|
+
.dim-channelCopy { min-width: 0; display: grid; }
|
|
56
56
|
.dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
|
+
.dim-channelNote { overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 10px; line-height: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
58
|
.dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-border-l1, #eef0f3); }
|
|
58
59
|
.dim-panel { min-width: 0; container-type: inline-size; }
|
|
59
60
|
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dsl-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
|
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.15.0)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -20,6 +20,14 @@ const ACTIVE_ATTEMPT_STATES = new Set([
|
|
|
20
20
|
]);
|
|
21
21
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
|
22
22
|
const QR_TTL_MS = 5 * 60_000;
|
|
23
|
+
const ACTIVATION_ERROR_MESSAGES = Object.freeze({
|
|
24
|
+
'credential-read-failed': '微信已授权,但无法读取现有登录凭据。请检查 DSH 凭据存储。',
|
|
25
|
+
'credential-save-failed': '微信已授权,但登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。',
|
|
26
|
+
'account-config-save-failed': '微信已授权,但账号配置无法写入本机。请检查 DSH_HOME 目录权限。',
|
|
27
|
+
'runtime-prepare-failed': '微信已授权,但无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。',
|
|
28
|
+
'harness-unreachable': '微信已授权,但插件无法连接本机 Harness。请确认 dsh web 已正常启动。',
|
|
29
|
+
'connection-start-failed': '微信已授权,但消息连接初始化失败。请查看 dsh web 日志后重试。',
|
|
30
|
+
});
|
|
23
31
|
|
|
24
32
|
function cleanString(value) {
|
|
25
33
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -54,6 +62,26 @@ function safeAccountError(code, message) {
|
|
|
54
62
|
return Object.freeze({ code, message });
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
function activationStageError(code, cause) {
|
|
66
|
+
const error = new Error(`Weixin activation failed during ${code}`, { cause });
|
|
67
|
+
error.name = 'WeixinActivationStageError';
|
|
68
|
+
error.code = code;
|
|
69
|
+
return error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function publicProvisioningError(error) {
|
|
73
|
+
if (error instanceof WeixinApiError) return safeAccountError(error.code, error.message);
|
|
74
|
+
const message = ACTIVATION_ERROR_MESSAGES[error?.code];
|
|
75
|
+
return message
|
|
76
|
+
? safeAccountError(error.code, message)
|
|
77
|
+
: safeAccountError('activation-unknown-failed', '微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function preserveActivationError(error, fallbackCode) {
|
|
81
|
+
if (error instanceof WeixinApiError || ACTIVATION_ERROR_MESSAGES[error?.code]) return error;
|
|
82
|
+
return activationStageError(fallbackCode, error);
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
export class WeixinController {
|
|
58
86
|
#api;
|
|
59
87
|
#credentials;
|
|
@@ -439,12 +467,7 @@ export class WeixinController {
|
|
|
439
467
|
record.error = safeAccountError('cancelled', '扫码绑定已取消。');
|
|
440
468
|
} else {
|
|
441
469
|
record.state = 'failed';
|
|
442
|
-
record.error =
|
|
443
|
-
error instanceof WeixinApiError ? error.code : 'activation-failed',
|
|
444
|
-
error instanceof WeixinApiError
|
|
445
|
-
? error.message
|
|
446
|
-
: '微信已授权,但无法保存凭据或启动消息连接。',
|
|
447
|
-
);
|
|
470
|
+
record.error = publicProvisioningError(error);
|
|
448
471
|
this.#logger.error?.('[dsh-weixin] provisioning failed:', error);
|
|
449
472
|
}
|
|
450
473
|
} finally {
|
|
@@ -469,13 +492,26 @@ export class WeixinController {
|
|
|
469
492
|
createdAt: previousConfig?.createdAt ?? new Date().toISOString(),
|
|
470
493
|
connectedAt: new Date().toISOString(),
|
|
471
494
|
};
|
|
472
|
-
|
|
495
|
+
let previousToken;
|
|
496
|
+
try {
|
|
497
|
+
previousToken = await this.#credentials.resolve(identity.tokenRef);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
throw activationStageError('credential-read-failed', error);
|
|
500
|
+
}
|
|
473
501
|
|
|
474
502
|
return this.#withBotTransition(identity.botId, async () => {
|
|
475
|
-
await this.#credentials.set(identity.tokenRef, token);
|
|
476
503
|
try {
|
|
504
|
+
try {
|
|
505
|
+
await this.#credentials.set(identity.tokenRef, token);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
throw activationStageError('credential-save-failed', error);
|
|
508
|
+
}
|
|
477
509
|
this.#assertAttemptActive(record);
|
|
478
|
-
|
|
510
|
+
try {
|
|
511
|
+
await this.#configStore.save(config);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
throw activationStageError('account-config-save-failed', error);
|
|
514
|
+
}
|
|
479
515
|
this.#assertAttemptActive(record);
|
|
480
516
|
await this.#startRuntime(config, token);
|
|
481
517
|
this.#assertAttemptActive(record);
|
|
@@ -504,16 +540,24 @@ export class WeixinController {
|
|
|
504
540
|
|
|
505
541
|
async #startRuntime(config, token) {
|
|
506
542
|
await this.#stopRuntime(config.botId);
|
|
507
|
-
|
|
543
|
+
let runtime;
|
|
544
|
+
try {
|
|
545
|
+
runtime = await this.#createRuntime({ botId: config.botId, config, token });
|
|
546
|
+
} catch (error) {
|
|
547
|
+
throw preserveActivationError(error, 'runtime-prepare-failed');
|
|
548
|
+
}
|
|
508
549
|
if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
|
|
509
|
-
throw
|
|
550
|
+
throw activationStageError(
|
|
551
|
+
'runtime-prepare-failed',
|
|
552
|
+
new TypeError('createRuntime returned an invalid Weixin runtime'),
|
|
553
|
+
);
|
|
510
554
|
}
|
|
511
555
|
try {
|
|
512
556
|
await runtime.start();
|
|
513
557
|
this.#runtimes.set(config.botId, runtime);
|
|
514
558
|
} catch (error) {
|
|
515
559
|
await runtime.stop().catch(() => undefined);
|
|
516
|
-
throw error;
|
|
560
|
+
throw preserveActivationError(error, 'connection-start-failed');
|
|
517
561
|
}
|
|
518
562
|
}
|
|
519
563
|
|
|
@@ -25,6 +25,13 @@ function retryableStartError(error) {
|
|
|
25
25
|
&& (error.status === 408 || error.status === 425 || error.status === 429 || error.status >= 500);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function runtimeStartError(code, cause) {
|
|
29
|
+
const error = new Error(`Weixin runtime failed during ${code}`, { cause });
|
|
30
|
+
error.name = 'WeixinRuntimeStartError';
|
|
31
|
+
error.code = code;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
function delay(ms, signal) {
|
|
29
36
|
return new Promise((resolve, reject) => {
|
|
30
37
|
if (signal?.aborted) {
|
|
@@ -117,7 +124,11 @@ export class WeixinRuntime {
|
|
|
117
124
|
this.#status.weixinConnectionState = 'connecting';
|
|
118
125
|
this.#status.lastError = null;
|
|
119
126
|
try {
|
|
120
|
-
|
|
127
|
+
try {
|
|
128
|
+
await this.#harness.ensureRunning();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw runtimeStartError('harness-unreachable', error);
|
|
131
|
+
}
|
|
121
132
|
this.#status.harnessReachable = true;
|
|
122
133
|
await this.#notifyStart();
|
|
123
134
|
this.#abortController = new AbortController();
|