@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -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.0)',
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 instanceof ImagePromptError ? error.userMessage : null;
298
+ return imagePromptDiagnostic(error)?.userMessage ?? null;
268
299
  }
@@ -184,7 +184,7 @@ function authenticatedHeaders(token) {
184
184
  function baseInfo() {
185
185
  return {
186
186
  channel_version: WEIXIN_PROTOCOL_VERSION,
187
- bot_agent: 'DeepSeekHarness/0.17.0',
187
+ bot_agent: 'DeepSeekHarness/0.17.1',
188
188
  };
189
189
  }
190
190
 
@@ -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, '消息处理失败,请稍后重试。', contextToken, runId)
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
- imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
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
  });