@xmanrui/dsh-im 0.17.0 → 0.17.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/lib/client.js +33 -0
- package/lib/index.js +135 -135
- package/package.json +1 -1
- package/plugin-src/client/channels/weixin/api.js +10 -0
- package/plugin-src/client/channels/weixin/index.js +4 -0
- package/plugin-src/client/i18n.js +20 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/shared/image-prompt.mjs +32 -1
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +23 -3
- package/src/channels/weixin/weixin-controller.mjs +15 -0
package/package.json
CHANGED
|
@@ -45,6 +45,15 @@ function normalizeTestMessage(value) {
|
|
|
45
45
|
return { sent: false, code };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function normalizeMessageError(value) {
|
|
49
|
+
if (!isRecord(value)) return null;
|
|
50
|
+
const code = string(value.code).slice(0, 64);
|
|
51
|
+
const reason = string(value.reason).slice(0, 128);
|
|
52
|
+
const message = string(value.message).slice(0, 500);
|
|
53
|
+
const at = timestamp(value.at);
|
|
54
|
+
return code && reason && message && at !== null ? { code, reason, message, at } : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export function unwrapRpcResult(result) {
|
|
49
58
|
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
|
50
59
|
throw new Error('微信服务返回了无法识别的响应');
|
|
@@ -128,6 +137,7 @@ function normalizeBot(value) {
|
|
|
128
137
|
messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
|
|
129
138
|
messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
|
|
130
139
|
},
|
|
140
|
+
lastMessageError: normalizeMessageError(value.lastMessageError),
|
|
131
141
|
error: isRecord(value.error)
|
|
132
142
|
? {
|
|
133
143
|
code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
|
|
@@ -230,6 +230,10 @@ export function AccountCard({
|
|
|
230
230
|
}),
|
|
231
231
|
h('div', { className: 'dxw-accountFooter dim-cardFooter' },
|
|
232
232
|
summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
|
|
233
|
+
account.lastMessageError ? h('div', {
|
|
234
|
+
className: 'dxw-summary dim-cardSummary',
|
|
235
|
+
role: 'status',
|
|
236
|
+
}, `最近一条消息处理失败:${account.lastMessageError.message}`) : null,
|
|
233
237
|
feedback ? h('div', {
|
|
234
238
|
className: 'dxw-summary dim-cardSummary',
|
|
235
239
|
role: 'status',
|
|
@@ -449,6 +449,20 @@ const EN = Object.freeze({
|
|
|
449
449
|
'微信绑定没有完成': 'WeChat setup did not complete',
|
|
450
450
|
'微信连接正常': 'WeChat connection is healthy',
|
|
451
451
|
'微信连接未就绪': 'WeChat connection is not ready',
|
|
452
|
+
'当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。': 'The current model does not support images. Use /models to list models, then /model <number> to switch and resend.',
|
|
453
|
+
'图片超过宿主允许的大小,请压缩后重试。': 'The image exceeds the Host size limit. Compress it and try again.',
|
|
454
|
+
'图片分辨率过高,请压缩后重试。': 'The image resolution is too high. Compress it and try again.',
|
|
455
|
+
'图片内容无效或格式不受支持,请重新发送。': 'The image is invalid or unsupported. Send it again.',
|
|
456
|
+
'未能读取图片内容,请重新发送。': 'The image could not be read. Send it again.',
|
|
457
|
+
'图片格式与实际内容不一致,请重新发送。': 'The declared image format does not match its content. Send it again.',
|
|
458
|
+
'一次发送的图片数量超过宿主限制,请减少后重试。': 'The message exceeds the Host image-count limit. Remove some images and try again.',
|
|
459
|
+
'图片总大小超过宿主限制,请减少图片或压缩后重试。': 'The images exceed the Host total-size limit. Remove or compress some images and try again.',
|
|
460
|
+
'图片下载地址发生了重定向,暂时无法读取。': 'The image download redirected and cannot be read.',
|
|
461
|
+
'图片超过 5 MB,请压缩后重试。': 'The image exceeds 5 MB. Compress it and try again.',
|
|
462
|
+
'一次发送的图片总大小过大,请减少图片数量或压缩后重试。': 'The images are too large in total. Remove or compress some images and try again.',
|
|
463
|
+
'图片下载失败,请重新发送后再试。': 'The image download failed. Send it again.',
|
|
464
|
+
'暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。': 'This image format is unsupported. Send a JPEG, PNG, WebP, or GIF image.',
|
|
465
|
+
'消息处理失败,请稍后重试。': 'Message processing failed. Try again later.',
|
|
452
466
|
'微信服务没有返回有效的账号列表': 'WeChat did not return a valid account list',
|
|
453
467
|
'尚未绑定微信': 'No WeChat account connected yet',
|
|
454
468
|
'用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码': 'One-time QR code for connecting a WeChat bot to DeepSeek Harness',
|
|
@@ -522,6 +536,12 @@ function translateDynamic(text) {
|
|
|
522
536
|
}
|
|
523
537
|
match = /^二维码剩余 (.+)$/.exec(text);
|
|
524
538
|
if (match) return `QR code expires in ${match[1]}`;
|
|
539
|
+
match = /^最近一条消息处理失败:(.+)$/.exec(text);
|
|
540
|
+
if (match) return `Latest message failed: ${localizeText(match[1])}`;
|
|
541
|
+
match = /^图片下载失败(HTTP (.+)),请重新发送后再试。$/.exec(text);
|
|
542
|
+
if (match) return `The image download failed (HTTP ${match[1]}). Send it again.`;
|
|
543
|
+
match = /^一次最多只能处理 (\d+) 张图片。$/.exec(text);
|
|
544
|
+
if (match) return `A message can contain at most ${match[1]} images.`;
|
|
525
545
|
match = /^状态刷新失败:(.+)$/.exec(text);
|
|
526
546
|
if (match) return `Status refresh failed: ${match[1]}`;
|
|
527
547
|
match = /^状态自动刷新失败:(.+)$/.exec(text);
|
|
@@ -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.17.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.17.1)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -13,6 +13,18 @@ export class ImagePromptError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const HOST_ATTACHMENT_USER_MESSAGES = Object.freeze({
|
|
17
|
+
MODEL_DOES_NOT_SUPPORT_IMAGES:
|
|
18
|
+
'当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。',
|
|
19
|
+
IMAGE_TOO_LARGE: '图片超过宿主允许的大小,请压缩后重试。',
|
|
20
|
+
IMAGE_TOO_MANY_PIXELS: '图片分辨率过高,请压缩后重试。',
|
|
21
|
+
INVALID_IMAGE: '图片内容无效或格式不受支持,请重新发送。',
|
|
22
|
+
INVALID_IMAGE_BASE64: '未能读取图片内容,请重新发送。',
|
|
23
|
+
IMAGE_TYPE_MISMATCH: '图片格式与实际内容不一致,请重新发送。',
|
|
24
|
+
TOO_MANY_IMAGES: '一次发送的图片数量超过宿主限制,请减少后重试。',
|
|
25
|
+
IMAGES_TOO_LARGE: '图片总大小超过宿主限制,请减少图片或压缩后重试。',
|
|
26
|
+
});
|
|
27
|
+
|
|
16
28
|
function requestSignal(signal, timeoutMs) {
|
|
17
29
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
18
30
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
@@ -263,6 +275,25 @@ export async function promptContentForMessage(message, {
|
|
|
263
275
|
return content;
|
|
264
276
|
}
|
|
265
277
|
|
|
278
|
+
/** Return only allowlisted, user-safe image failure details. */
|
|
279
|
+
export function imagePromptDiagnostic(error) {
|
|
280
|
+
if (error instanceof ImagePromptError) {
|
|
281
|
+
return {
|
|
282
|
+
code: 'image-prompt-error',
|
|
283
|
+
reason: error.code,
|
|
284
|
+
userMessage: error.userMessage,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (error?.code !== 'attachment-error' || typeof error?.details?.reason !== 'string') {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const reason = error.details.reason;
|
|
291
|
+
const userMessage = Object.hasOwn(HOST_ATTACHMENT_USER_MESSAGES, reason)
|
|
292
|
+
? HOST_ATTACHMENT_USER_MESSAGES[reason]
|
|
293
|
+
: null;
|
|
294
|
+
return userMessage ? { code: 'attachment-error', reason, userMessage } : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
266
297
|
export function imagePromptUserMessage(error) {
|
|
267
|
-
return error
|
|
298
|
+
return imagePromptDiagnostic(error)?.userMessage ?? null;
|
|
268
299
|
}
|
|
@@ -23,12 +23,14 @@ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
|
23
23
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
24
24
|
import {
|
|
25
25
|
hasInboundImages,
|
|
26
|
+
imagePromptDiagnostic,
|
|
26
27
|
imagePromptUserMessage,
|
|
27
28
|
promptContentForMessage,
|
|
28
29
|
} from '../shared/image-prompt.mjs';
|
|
29
30
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
30
31
|
|
|
31
32
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
33
|
+
const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
|
|
32
34
|
|
|
33
35
|
const HELP_TEXT = [
|
|
34
36
|
'微信已连接 DeepSeek Harness。',
|
|
@@ -69,6 +71,16 @@ function canClaimInteractionReply(message, pending) {
|
|
|
69
71
|
&& nonEmptyString(extractWeixinText(message));
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
|
|
75
|
+
const diagnostic = imagePromptDiagnostic(error);
|
|
76
|
+
return {
|
|
77
|
+
code: diagnostic?.code ?? 'message-processing-failed',
|
|
78
|
+
reason: diagnostic?.reason ?? 'UNKNOWN',
|
|
79
|
+
message: diagnostic?.userMessage ?? userMessage,
|
|
80
|
+
at: Date.now(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
export function createWeixinBridgeStatus() {
|
|
73
85
|
return {
|
|
74
86
|
messagesReceived: 0,
|
|
@@ -78,6 +90,7 @@ export function createWeixinBridgeStatus() {
|
|
|
78
90
|
lastReplyAt: null,
|
|
79
91
|
lastRejectedAt: null,
|
|
80
92
|
lastError: null,
|
|
93
|
+
lastMessageError: null,
|
|
81
94
|
};
|
|
82
95
|
}
|
|
83
96
|
|
|
@@ -168,8 +181,9 @@ export class WeixinHarnessBridge {
|
|
|
168
181
|
).catch((error) => {
|
|
169
182
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
170
183
|
this.#status.lastError = error?.message ?? String(error);
|
|
184
|
+
this.#status.lastMessageError = safeMessageError(error);
|
|
171
185
|
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
|
172
|
-
return this.#send(sender,
|
|
186
|
+
return this.#send(sender, GENERIC_PROCESSING_ERROR, contextToken, runId)
|
|
173
187
|
.catch(() => undefined);
|
|
174
188
|
}).finally(() => {
|
|
175
189
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -289,6 +303,7 @@ export class WeixinHarnessBridge {
|
|
|
289
303
|
if (reply) await this.#send(sender, reply, contextToken, runId);
|
|
290
304
|
}
|
|
291
305
|
this.#status.lastError = null;
|
|
306
|
+
this.#status.lastMessageError = null;
|
|
292
307
|
}
|
|
293
308
|
|
|
294
309
|
async #process(message, key, { alreadyRecorded = false } = {}) {
|
|
@@ -401,6 +416,7 @@ export class WeixinHarnessBridge {
|
|
|
401
416
|
this.#status.messagesReplied += 1;
|
|
402
417
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
403
418
|
this.#status.lastError = null;
|
|
419
|
+
this.#status.lastMessageError = null;
|
|
404
420
|
} catch (error) {
|
|
405
421
|
if (error?.code === 'turn-stopped') {
|
|
406
422
|
await this.#state.markSeen(messageId);
|
|
@@ -408,11 +424,13 @@ export class WeixinHarnessBridge {
|
|
|
408
424
|
}
|
|
409
425
|
if (this.#signal?.aborted) return;
|
|
410
426
|
this.#status.lastError = error?.message ?? String(error);
|
|
427
|
+
const userMessage = imagePromptUserMessage(error) ?? GENERIC_PROCESSING_ERROR;
|
|
428
|
+
this.#status.lastMessageError = safeMessageError(error, userMessage);
|
|
411
429
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
412
430
|
try {
|
|
413
431
|
await this.#send(
|
|
414
432
|
sender,
|
|
415
|
-
|
|
433
|
+
userMessage,
|
|
416
434
|
contextToken,
|
|
417
435
|
runId,
|
|
418
436
|
);
|
|
@@ -526,6 +544,7 @@ export class WeixinHarnessBridge {
|
|
|
526
544
|
});
|
|
527
545
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
528
546
|
this.#status.lastError = null;
|
|
547
|
+
this.#status.lastMessageError = null;
|
|
529
548
|
} catch (error) {
|
|
530
549
|
if (this.#signal?.aborted) return;
|
|
531
550
|
if (error?.code === 'interaction-not-pending') {
|
|
@@ -719,13 +738,14 @@ export class WeixinHarnessBridge {
|
|
|
719
738
|
async #handleInteractionFailure(message, messageId, error) {
|
|
720
739
|
if (this.#signal?.aborted) return;
|
|
721
740
|
this.#status.lastError = error?.message ?? String(error);
|
|
741
|
+
this.#status.lastMessageError = safeMessageError(error);
|
|
722
742
|
this.#logger.error?.('[dsh-weixin] failed to process an interaction reply:', error);
|
|
723
743
|
if (!this.#state.hasSeen(messageId)) {
|
|
724
744
|
await this.#state.markSeen(messageId).catch(() => undefined);
|
|
725
745
|
}
|
|
726
746
|
await this.#send(
|
|
727
747
|
nonEmptyString(message?.from_user_id),
|
|
728
|
-
|
|
748
|
+
GENERIC_PROCESSING_ERROR,
|
|
729
749
|
nonEmptyString(message?.context_token) ?? undefined,
|
|
730
750
|
nonEmptyString(message?.run_id) ?? undefined,
|
|
731
751
|
).catch(() => undefined);
|
|
@@ -62,6 +62,20 @@ function safeAccountError(code, message) {
|
|
|
62
62
|
return Object.freeze({ code, message });
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function publicMessageError(value) {
|
|
66
|
+
if (!value || typeof value !== 'object'
|
|
67
|
+
|| typeof value.code !== 'string' || !value.code
|
|
68
|
+
|| typeof value.reason !== 'string' || !value.reason
|
|
69
|
+
|| typeof value.message !== 'string' || !value.message
|
|
70
|
+
|| !Number.isFinite(value.at)) return null;
|
|
71
|
+
return {
|
|
72
|
+
code: value.code.slice(0, 64),
|
|
73
|
+
reason: value.reason.slice(0, 128),
|
|
74
|
+
message: value.message.slice(0, 500),
|
|
75
|
+
at: value.at,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
function activationStageError(code, cause) {
|
|
66
80
|
const error = new Error(`Weixin activation failed during ${code}`, { cause });
|
|
67
81
|
error.name = 'WeixinActivationStageError';
|
|
@@ -351,6 +365,7 @@ export class WeixinController {
|
|
|
351
365
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
|
352
366
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
353
367
|
},
|
|
368
|
+
lastMessageError: publicMessageError(runtimeStatus?.lastMessageError),
|
|
354
369
|
error: error ? structuredClone(error) : null,
|
|
355
370
|
};
|
|
356
371
|
});
|