mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 04:03:16 +08:00
Merge remote-tracking branch 'upstream/main' into feat/per-bot-agent-preset
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # lib/index.js
This commit is contained in:
commit
a1bdf2c1f8
17 changed files with 438 additions and 144 deletions
|
|
@ -645,6 +645,20 @@ var EN = Object.freeze({
|
||||||
"\u5FAE\u4FE1\u7ED1\u5B9A\u6CA1\u6709\u5B8C\u6210": "WeChat setup did not complete",
|
"\u5FAE\u4FE1\u7ED1\u5B9A\u6CA1\u6709\u5B8C\u6210": "WeChat setup did not complete",
|
||||||
"\u5FAE\u4FE1\u8FDE\u63A5\u6B63\u5E38": "WeChat connection is healthy",
|
"\u5FAE\u4FE1\u8FDE\u63A5\u6B63\u5E38": "WeChat connection is healthy",
|
||||||
"\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA": "WeChat connection is not ready",
|
"\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA": "WeChat connection is not ready",
|
||||||
|
"\u5F53\u524D\u6A21\u578B\u4E0D\u652F\u6301\u56FE\u7247\uFF0C\u8BF7\u7528 /models \u67E5\u770B\u53EF\u7528\u6A21\u578B\uFF0C\u518D\u7528 /model <\u5E8F\u53F7> \u5207\u6362\u540E\u91CD\u53D1\u3002": "The current model does not support images. Use /models to list models, then /model <number> to switch and resend.",
|
||||||
|
"\u56FE\u7247\u8D85\u8FC7\u5BBF\u4E3B\u5141\u8BB8\u7684\u5927\u5C0F\uFF0C\u8BF7\u538B\u7F29\u540E\u91CD\u8BD5\u3002": "The image exceeds the Host size limit. Compress it and try again.",
|
||||||
|
"\u56FE\u7247\u5206\u8FA8\u7387\u8FC7\u9AD8\uFF0C\u8BF7\u538B\u7F29\u540E\u91CD\u8BD5\u3002": "The image resolution is too high. Compress it and try again.",
|
||||||
|
"\u56FE\u7247\u5185\u5BB9\u65E0\u6548\u6216\u683C\u5F0F\u4E0D\u53D7\u652F\u6301\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u3002": "The image is invalid or unsupported. Send it again.",
|
||||||
|
"\u672A\u80FD\u8BFB\u53D6\u56FE\u7247\u5185\u5BB9\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u3002": "The image could not be read. Send it again.",
|
||||||
|
"\u56FE\u7247\u683C\u5F0F\u4E0E\u5B9E\u9645\u5185\u5BB9\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u3002": "The declared image format does not match its content. Send it again.",
|
||||||
|
"\u4E00\u6B21\u53D1\u9001\u7684\u56FE\u7247\u6570\u91CF\u8D85\u8FC7\u5BBF\u4E3B\u9650\u5236\uFF0C\u8BF7\u51CF\u5C11\u540E\u91CD\u8BD5\u3002": "The message exceeds the Host image-count limit. Remove some images and try again.",
|
||||||
|
"\u56FE\u7247\u603B\u5927\u5C0F\u8D85\u8FC7\u5BBF\u4E3B\u9650\u5236\uFF0C\u8BF7\u51CF\u5C11\u56FE\u7247\u6216\u538B\u7F29\u540E\u91CD\u8BD5\u3002": "The images exceed the Host total-size limit. Remove or compress some images and try again.",
|
||||||
|
"\u56FE\u7247\u4E0B\u8F7D\u5730\u5740\u53D1\u751F\u4E86\u91CD\u5B9A\u5411\uFF0C\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u3002": "The image download redirected and cannot be read.",
|
||||||
|
"\u56FE\u7247\u8D85\u8FC7 5 MB\uFF0C\u8BF7\u538B\u7F29\u540E\u91CD\u8BD5\u3002": "The image exceeds 5 MB. Compress it and try again.",
|
||||||
|
"\u4E00\u6B21\u53D1\u9001\u7684\u56FE\u7247\u603B\u5927\u5C0F\u8FC7\u5927\uFF0C\u8BF7\u51CF\u5C11\u56FE\u7247\u6570\u91CF\u6216\u538B\u7F29\u540E\u91CD\u8BD5\u3002": "The images are too large in total. Remove or compress some images and try again.",
|
||||||
|
"\u56FE\u7247\u4E0B\u8F7D\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u540E\u518D\u8BD5\u3002": "The image download failed. Send it again.",
|
||||||
|
"\u6682\u4E0D\u652F\u6301\u8BE5\u56FE\u7247\u683C\u5F0F\uFF0C\u8BF7\u53D1\u9001 JPEG\u3001PNG\u3001WebP \u6216 GIF \u56FE\u7247\u3002": "This image format is unsupported. Send a JPEG, PNG, WebP, or GIF image.",
|
||||||
|
"\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "Message processing failed. Try again later.",
|
||||||
"\u5FAE\u4FE1\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u8D26\u53F7\u5217\u8868": "WeChat did not return a valid account list",
|
"\u5FAE\u4FE1\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u8D26\u53F7\u5217\u8868": "WeChat did not return a valid account list",
|
||||||
"\u5C1A\u672A\u7ED1\u5B9A\u5FAE\u4FE1": "No WeChat account connected yet",
|
"\u5C1A\u672A\u7ED1\u5B9A\u5FAE\u4FE1": "No WeChat account connected yet",
|
||||||
"\u7528\u4E8E\u628A\u5FAE\u4FE1\u673A\u5668\u4EBA\u7ED1\u5B9A\u5230 DeepSeek Harness \u7684\u4E00\u6B21\u6027\u4E8C\u7EF4\u7801": "One-time QR code for connecting a WeChat bot to DeepSeek Harness",
|
"\u7528\u4E8E\u628A\u5FAE\u4FE1\u673A\u5668\u4EBA\u7ED1\u5B9A\u5230 DeepSeek Harness \u7684\u4E00\u6B21\u6027\u4E8C\u7EF4\u7801": "One-time QR code for connecting a WeChat bot to DeepSeek Harness",
|
||||||
|
|
@ -712,6 +726,12 @@ function translateDynamic(text5) {
|
||||||
}
|
}
|
||||||
match = /^二维码剩余 (.+)$/.exec(text5);
|
match = /^二维码剩余 (.+)$/.exec(text5);
|
||||||
if (match) return `QR code expires in ${match[1]}`;
|
if (match) return `QR code expires in ${match[1]}`;
|
||||||
|
match = /^最近一条消息处理失败:(.+)$/.exec(text5);
|
||||||
|
if (match) return `Latest message failed: ${localizeText(match[1])}`;
|
||||||
|
match = /^图片下载失败(HTTP (.+)),请重新发送后再试。$/.exec(text5);
|
||||||
|
if (match) return `The image download failed (HTTP ${match[1]}). Send it again.`;
|
||||||
|
match = /^一次最多只能处理 (\d+) 张图片。$/.exec(text5);
|
||||||
|
if (match) return `A message can contain at most ${match[1]} images.`;
|
||||||
match = /^状态刷新失败:(.+)$/.exec(text5);
|
match = /^状态刷新失败:(.+)$/.exec(text5);
|
||||||
if (match) return `Status refresh failed: ${match[1]}`;
|
if (match) return `Status refresh failed: ${match[1]}`;
|
||||||
match = /^状态自动刷新失败:(.+)$/.exec(text5);
|
match = /^状态自动刷新失败:(.+)$/.exec(text5);
|
||||||
|
|
@ -7654,6 +7674,14 @@ function normalizeTestMessage4(value) {
|
||||||
const code = value.code === "test-target-unavailable" ? "test-target-unavailable" : "test-message-failed";
|
const code = value.code === "test-target-unavailable" ? "test-target-unavailable" : "test-message-failed";
|
||||||
return { sent: false, code };
|
return { sent: false, code };
|
||||||
}
|
}
|
||||||
|
function normalizeMessageError(value) {
|
||||||
|
if (!isRecord6(value)) return null;
|
||||||
|
const code = string(value.code).slice(0, 64);
|
||||||
|
const reason = string(value.reason).slice(0, 128);
|
||||||
|
const message = string(value.message).slice(0, 500);
|
||||||
|
const at = timestamp5(value.at);
|
||||||
|
return code && reason && message && at !== null ? { code, reason, message, at } : null;
|
||||||
|
}
|
||||||
function unwrapRpcResult8(result) {
|
function unwrapRpcResult8(result) {
|
||||||
if (!isRecord6(result) || typeof result.ok !== "boolean") {
|
if (!isRecord6(result) || typeof result.ok !== "boolean") {
|
||||||
throw new Error("\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94");
|
throw new Error("\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94");
|
||||||
|
|
@ -7728,6 +7756,7 @@ function normalizeBot5(value) {
|
||||||
messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
|
messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
|
||||||
messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0)
|
messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0)
|
||||||
},
|
},
|
||||||
|
lastMessageError: normalizeMessageError(value.lastMessageError),
|
||||||
error: isRecord6(value.error) ? {
|
error: isRecord6(value.error) ? {
|
||||||
code: string(value.error.code, "WEIXIN_ACCOUNT_ERROR"),
|
code: string(value.error.code, "WEIXIN_ACCOUNT_ERROR"),
|
||||||
message: string(value.error.message, "\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA")
|
message: string(value.error.message, "\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA")
|
||||||
|
|
@ -8194,6 +8223,10 @@ function AccountCard4({
|
||||||
"div",
|
"div",
|
||||||
{ className: "dxw-accountFooter dim-cardFooter" },
|
{ className: "dxw-accountFooter dim-cardFooter" },
|
||||||
summary ? h2("div", { className: "dxw-summary dim-cardSummary" }, summary) : null,
|
summary ? h2("div", { className: "dxw-summary dim-cardSummary" }, summary) : null,
|
||||||
|
account.lastMessageError ? h2("div", {
|
||||||
|
className: "dxw-summary dim-cardSummary",
|
||||||
|
role: "status"
|
||||||
|
}, `\u6700\u8FD1\u4E00\u6761\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF1A${account.lastMessageError.message}`) : null,
|
||||||
feedback ? h2("div", {
|
feedback ? h2("div", {
|
||||||
className: "dxw-summary dim-cardSummary",
|
className: "dxw-summary dim-cardSummary",
|
||||||
role: "status",
|
role: "status",
|
||||||
|
|
|
||||||
262
lib/index.js
262
lib/index.js
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@xmanrui/dsh-im",
|
"name": "@xmanrui/dsh-im",
|
||||||
"version": "0.17.0",
|
"version": "0.17.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@xmanrui/dsh-im",
|
"name": "@xmanrui/dsh-im",
|
||||||
"version": "0.17.0",
|
"version": "0.17.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tencent-connect/qqbot-connector": "1.2.0",
|
"@tencent-connect/qqbot-connector": "1.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@xmanrui/dsh-im",
|
"name": "@xmanrui/dsh-im",
|
||||||
"version": "0.17.0",
|
"version": "0.17.1",
|
||||||
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,15 @@ function normalizeTestMessage(value) {
|
||||||
return { sent: false, code };
|
return { sent: false, code };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMessageError(value) {
|
||||||
|
if (!isRecord(value)) return null;
|
||||||
|
const code = string(value.code).slice(0, 64);
|
||||||
|
const reason = string(value.reason).slice(0, 128);
|
||||||
|
const message = string(value.message).slice(0, 500);
|
||||||
|
const at = timestamp(value.at);
|
||||||
|
return code && reason && message && at !== null ? { code, reason, message, at } : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function unwrapRpcResult(result) {
|
export function unwrapRpcResult(result) {
|
||||||
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
||||||
throw new Error('微信服务返回了无法识别的响应');
|
throw new Error('微信服务返回了无法识别的响应');
|
||||||
|
|
@ -132,6 +141,7 @@ function normalizeBot(value) {
|
||||||
messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
|
messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
|
||||||
messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
|
messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
|
||||||
},
|
},
|
||||||
|
lastMessageError: normalizeMessageError(value.lastMessageError),
|
||||||
error: isRecord(value.error)
|
error: isRecord(value.error)
|
||||||
? {
|
? {
|
||||||
code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
|
code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,10 @@ export function AccountCard({
|
||||||
}),
|
}),
|
||||||
h('div', { className: 'dxw-accountFooter dim-cardFooter' },
|
h('div', { className: 'dxw-accountFooter dim-cardFooter' },
|
||||||
summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
|
summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
|
||||||
|
account.lastMessageError ? h('div', {
|
||||||
|
className: 'dxw-summary dim-cardSummary',
|
||||||
|
role: 'status',
|
||||||
|
}, `最近一条消息处理失败:${account.lastMessageError.message}`) : null,
|
||||||
feedback ? h('div', {
|
feedback ? h('div', {
|
||||||
className: 'dxw-summary dim-cardSummary',
|
className: 'dxw-summary dim-cardSummary',
|
||||||
role: 'status',
|
role: 'status',
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,20 @@ const EN = Object.freeze({
|
||||||
'微信绑定没有完成': 'WeChat setup did not complete',
|
'微信绑定没有完成': 'WeChat setup did not complete',
|
||||||
'微信连接正常': 'WeChat connection is healthy',
|
'微信连接正常': 'WeChat connection is healthy',
|
||||||
'微信连接未就绪': 'WeChat connection is not ready',
|
'微信连接未就绪': 'WeChat connection is not ready',
|
||||||
|
'当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。': 'The current model does not support images. Use /models to list models, then /model <number> to switch and resend.',
|
||||||
|
'图片超过宿主允许的大小,请压缩后重试。': 'The image exceeds the Host size limit. Compress it and try again.',
|
||||||
|
'图片分辨率过高,请压缩后重试。': 'The image resolution is too high. Compress it and try again.',
|
||||||
|
'图片内容无效或格式不受支持,请重新发送。': 'The image is invalid or unsupported. Send it again.',
|
||||||
|
'未能读取图片内容,请重新发送。': 'The image could not be read. Send it again.',
|
||||||
|
'图片格式与实际内容不一致,请重新发送。': 'The declared image format does not match its content. Send it again.',
|
||||||
|
'一次发送的图片数量超过宿主限制,请减少后重试。': 'The message exceeds the Host image-count limit. Remove some images and try again.',
|
||||||
|
'图片总大小超过宿主限制,请减少图片或压缩后重试。': 'The images exceed the Host total-size limit. Remove or compress some images and try again.',
|
||||||
|
'图片下载地址发生了重定向,暂时无法读取。': 'The image download redirected and cannot be read.',
|
||||||
|
'图片超过 5 MB,请压缩后重试。': 'The image exceeds 5 MB. Compress it and try again.',
|
||||||
|
'一次发送的图片总大小过大,请减少图片数量或压缩后重试。': 'The images are too large in total. Remove or compress some images and try again.',
|
||||||
|
'图片下载失败,请重新发送后再试。': 'The image download failed. Send it again.',
|
||||||
|
'暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。': 'This image format is unsupported. Send a JPEG, PNG, WebP, or GIF image.',
|
||||||
|
'消息处理失败,请稍后重试。': 'Message processing failed. Try again later.',
|
||||||
'微信服务没有返回有效的账号列表': 'WeChat did not return a valid account list',
|
'微信服务没有返回有效的账号列表': 'WeChat did not return a valid account list',
|
||||||
'尚未绑定微信': 'No WeChat account connected yet',
|
'尚未绑定微信': 'No WeChat account connected yet',
|
||||||
'用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码': 'One-time QR code for connecting a WeChat bot to DeepSeek Harness',
|
'用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码': 'One-time QR code for connecting a WeChat bot to DeepSeek Harness',
|
||||||
|
|
@ -528,6 +542,12 @@ function translateDynamic(text) {
|
||||||
}
|
}
|
||||||
match = /^二维码剩余 (.+)$/.exec(text);
|
match = /^二维码剩余 (.+)$/.exec(text);
|
||||||
if (match) return `QR code expires in ${match[1]}`;
|
if (match) return `QR code expires in ${match[1]}`;
|
||||||
|
match = /^最近一条消息处理失败:(.+)$/.exec(text);
|
||||||
|
if (match) return `Latest message failed: ${localizeText(match[1])}`;
|
||||||
|
match = /^图片下载失败(HTTP (.+)),请重新发送后再试。$/.exec(text);
|
||||||
|
if (match) return `The image download failed (HTTP ${match[1]}). Send it again.`;
|
||||||
|
match = /^一次最多只能处理 (\d+) 张图片。$/.exec(text);
|
||||||
|
if (match) return `A message can contain at most ${match[1]} images.`;
|
||||||
match = /^状态刷新失败:(.+)$/.exec(text);
|
match = /^状态刷新失败:(.+)$/.exec(text);
|
||||||
if (match) return `Status refresh failed: ${match[1]}`;
|
if (match) return `Status refresh failed: ${match[1]}`;
|
||||||
match = /^状态自动刷新失败:(.+)$/.exec(text);
|
match = /^状态自动刷新失败:(.+)$/.exec(text);
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export class DiscordApi {
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bot ${this.#token}`,
|
authorization: `Bot ${this.#token}`,
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.17.0)',
|
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.17.1)',
|
||||||
},
|
},
|
||||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||||
signal: requestSignal(signal, timeoutMs),
|
signal: requestSignal(signal, timeoutMs),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,18 @@ export class ImagePromptError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HOST_ATTACHMENT_USER_MESSAGES = Object.freeze({
|
||||||
|
MODEL_DOES_NOT_SUPPORT_IMAGES:
|
||||||
|
'当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。',
|
||||||
|
IMAGE_TOO_LARGE: '图片超过宿主允许的大小,请压缩后重试。',
|
||||||
|
IMAGE_TOO_MANY_PIXELS: '图片分辨率过高,请压缩后重试。',
|
||||||
|
INVALID_IMAGE: '图片内容无效或格式不受支持,请重新发送。',
|
||||||
|
INVALID_IMAGE_BASE64: '未能读取图片内容,请重新发送。',
|
||||||
|
IMAGE_TYPE_MISMATCH: '图片格式与实际内容不一致,请重新发送。',
|
||||||
|
TOO_MANY_IMAGES: '一次发送的图片数量超过宿主限制,请减少后重试。',
|
||||||
|
IMAGES_TOO_LARGE: '图片总大小超过宿主限制,请减少图片或压缩后重试。',
|
||||||
|
});
|
||||||
|
|
||||||
function requestSignal(signal, timeoutMs) {
|
function requestSignal(signal, timeoutMs) {
|
||||||
const timeout = AbortSignal.timeout(timeoutMs);
|
const timeout = AbortSignal.timeout(timeoutMs);
|
||||||
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
||||||
|
|
@ -263,6 +275,25 @@ export async function promptContentForMessage(message, {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function imagePromptUserMessage(error) {
|
/** Return only allowlisted, user-safe image failure details. */
|
||||||
return error instanceof ImagePromptError ? error.userMessage : null;
|
export function imagePromptDiagnostic(error) {
|
||||||
|
if (error instanceof ImagePromptError) {
|
||||||
|
return {
|
||||||
|
code: 'image-prompt-error',
|
||||||
|
reason: error.code,
|
||||||
|
userMessage: error.userMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (error?.code !== 'attachment-error' || typeof error?.details?.reason !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const reason = error.details.reason;
|
||||||
|
const userMessage = Object.hasOwn(HOST_ATTACHMENT_USER_MESSAGES, reason)
|
||||||
|
? HOST_ATTACHMENT_USER_MESSAGES[reason]
|
||||||
|
: null;
|
||||||
|
return userMessage ? { code: 'attachment-error', reason, userMessage } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imagePromptUserMessage(error) {
|
||||||
|
return imagePromptDiagnostic(error)?.userMessage ?? null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ function authenticatedHeaders(token) {
|
||||||
function baseInfo() {
|
function baseInfo() {
|
||||||
return {
|
return {
|
||||||
channel_version: WEIXIN_PROTOCOL_VERSION,
|
channel_version: WEIXIN_PROTOCOL_VERSION,
|
||||||
bot_agent: 'DeepSeekHarness/0.17.0',
|
bot_agent: 'DeepSeekHarness/0.17.1',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,14 @@ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
||||||
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
||||||
import {
|
import {
|
||||||
hasInboundImages,
|
hasInboundImages,
|
||||||
|
imagePromptDiagnostic,
|
||||||
imagePromptUserMessage,
|
imagePromptUserMessage,
|
||||||
promptContentForMessage,
|
promptContentForMessage,
|
||||||
} from '../shared/image-prompt.mjs';
|
} from '../shared/image-prompt.mjs';
|
||||||
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
||||||
|
|
||||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||||
|
const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
|
||||||
|
|
||||||
const HELP_TEXT = [
|
const HELP_TEXT = [
|
||||||
'微信已连接 DeepSeek Harness。',
|
'微信已连接 DeepSeek Harness。',
|
||||||
|
|
@ -69,6 +71,16 @@ function canClaimInteractionReply(message, pending) {
|
||||||
&& nonEmptyString(extractWeixinText(message));
|
&& nonEmptyString(extractWeixinText(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
|
||||||
|
const diagnostic = imagePromptDiagnostic(error);
|
||||||
|
return {
|
||||||
|
code: diagnostic?.code ?? 'message-processing-failed',
|
||||||
|
reason: diagnostic?.reason ?? 'UNKNOWN',
|
||||||
|
message: diagnostic?.userMessage ?? userMessage,
|
||||||
|
at: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createWeixinBridgeStatus() {
|
export function createWeixinBridgeStatus() {
|
||||||
return {
|
return {
|
||||||
messagesReceived: 0,
|
messagesReceived: 0,
|
||||||
|
|
@ -78,6 +90,7 @@ export function createWeixinBridgeStatus() {
|
||||||
lastReplyAt: null,
|
lastReplyAt: null,
|
||||||
lastRejectedAt: null,
|
lastRejectedAt: null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
|
lastMessageError: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,8 +181,9 @@ export class WeixinHarnessBridge {
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
||||||
this.#status.lastError = error?.message ?? String(error);
|
this.#status.lastError = error?.message ?? String(error);
|
||||||
|
this.#status.lastMessageError = safeMessageError(error);
|
||||||
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
||||||
return this.#send(sender, '消息处理失败,请稍后重试。', contextToken, runId)
|
return this.#send(sender, GENERIC_PROCESSING_ERROR, contextToken, runId)
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.#acceptedMessageIds.delete(messageId);
|
this.#acceptedMessageIds.delete(messageId);
|
||||||
|
|
@ -289,6 +303,7 @@ export class WeixinHarnessBridge {
|
||||||
if (reply) await this.#send(sender, reply, contextToken, runId);
|
if (reply) await this.#send(sender, reply, contextToken, runId);
|
||||||
}
|
}
|
||||||
this.#status.lastError = null;
|
this.#status.lastError = null;
|
||||||
|
this.#status.lastMessageError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #process(message, key, { alreadyRecorded = false } = {}) {
|
async #process(message, key, { alreadyRecorded = false } = {}) {
|
||||||
|
|
@ -401,6 +416,7 @@ export class WeixinHarnessBridge {
|
||||||
this.#status.messagesReplied += 1;
|
this.#status.messagesReplied += 1;
|
||||||
this.#status.lastReplyAt = new Date().toISOString();
|
this.#status.lastReplyAt = new Date().toISOString();
|
||||||
this.#status.lastError = null;
|
this.#status.lastError = null;
|
||||||
|
this.#status.lastMessageError = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.code === 'turn-stopped') {
|
if (error?.code === 'turn-stopped') {
|
||||||
await this.#state.markSeen(messageId);
|
await this.#state.markSeen(messageId);
|
||||||
|
|
@ -408,11 +424,13 @@ export class WeixinHarnessBridge {
|
||||||
}
|
}
|
||||||
if (this.#signal?.aborted) return;
|
if (this.#signal?.aborted) return;
|
||||||
this.#status.lastError = error?.message ?? String(error);
|
this.#status.lastError = error?.message ?? String(error);
|
||||||
|
const userMessage = imagePromptUserMessage(error) ?? GENERIC_PROCESSING_ERROR;
|
||||||
|
this.#status.lastMessageError = safeMessageError(error, userMessage);
|
||||||
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
||||||
try {
|
try {
|
||||||
await this.#send(
|
await this.#send(
|
||||||
sender,
|
sender,
|
||||||
imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
|
userMessage,
|
||||||
contextToken,
|
contextToken,
|
||||||
runId,
|
runId,
|
||||||
);
|
);
|
||||||
|
|
@ -526,6 +544,7 @@ export class WeixinHarnessBridge {
|
||||||
});
|
});
|
||||||
this.#clearPendingInteraction(key, pending.interactionId);
|
this.#clearPendingInteraction(key, pending.interactionId);
|
||||||
this.#status.lastError = null;
|
this.#status.lastError = null;
|
||||||
|
this.#status.lastMessageError = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.#signal?.aborted) return;
|
if (this.#signal?.aborted) return;
|
||||||
if (error?.code === 'interaction-not-pending') {
|
if (error?.code === 'interaction-not-pending') {
|
||||||
|
|
@ -719,13 +738,14 @@ export class WeixinHarnessBridge {
|
||||||
async #handleInteractionFailure(message, messageId, error) {
|
async #handleInteractionFailure(message, messageId, error) {
|
||||||
if (this.#signal?.aborted) return;
|
if (this.#signal?.aborted) return;
|
||||||
this.#status.lastError = error?.message ?? String(error);
|
this.#status.lastError = error?.message ?? String(error);
|
||||||
|
this.#status.lastMessageError = safeMessageError(error);
|
||||||
this.#logger.error?.('[dsh-weixin] failed to process an interaction reply:', error);
|
this.#logger.error?.('[dsh-weixin] failed to process an interaction reply:', error);
|
||||||
if (!this.#state.hasSeen(messageId)) {
|
if (!this.#state.hasSeen(messageId)) {
|
||||||
await this.#state.markSeen(messageId).catch(() => undefined);
|
await this.#state.markSeen(messageId).catch(() => undefined);
|
||||||
}
|
}
|
||||||
await this.#send(
|
await this.#send(
|
||||||
nonEmptyString(message?.from_user_id),
|
nonEmptyString(message?.from_user_id),
|
||||||
'消息处理失败,请稍后重试。',
|
GENERIC_PROCESSING_ERROR,
|
||||||
nonEmptyString(message?.context_token) ?? undefined,
|
nonEmptyString(message?.context_token) ?? undefined,
|
||||||
nonEmptyString(message?.run_id) ?? undefined,
|
nonEmptyString(message?.run_id) ?? undefined,
|
||||||
).catch(() => undefined);
|
).catch(() => undefined);
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,20 @@ function safeAccountError(code, message) {
|
||||||
return Object.freeze({ code, message });
|
return Object.freeze({ code, message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publicMessageError(value) {
|
||||||
|
if (!value || typeof value !== 'object'
|
||||||
|
|| typeof value.code !== 'string' || !value.code
|
||||||
|
|| typeof value.reason !== 'string' || !value.reason
|
||||||
|
|| typeof value.message !== 'string' || !value.message
|
||||||
|
|| !Number.isFinite(value.at)) return null;
|
||||||
|
return {
|
||||||
|
code: value.code.slice(0, 64),
|
||||||
|
reason: value.reason.slice(0, 128),
|
||||||
|
message: value.message.slice(0, 500),
|
||||||
|
at: value.at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function activationStageError(code, cause) {
|
function activationStageError(code, cause) {
|
||||||
const error = new Error(`Weixin activation failed during ${code}`, { cause });
|
const error = new Error(`Weixin activation failed during ${code}`, { cause });
|
||||||
error.name = 'WeixinActivationStageError';
|
error.name = 'WeixinActivationStageError';
|
||||||
|
|
@ -351,6 +365,7 @@ export class WeixinController {
|
||||||
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
||||||
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
||||||
},
|
},
|
||||||
|
lastMessageError: publicMessageError(runtimeStatus?.lastMessageError),
|
||||||
error: error ? structuredClone(error) : null,
|
error: error ? structuredClone(error) : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import React from 'react';
|
||||||
import { renderToStaticMarkup } from 'react-dom/server';
|
import { renderToStaticMarkup } from 'react-dom/server';
|
||||||
import TestRenderer from 'react-test-renderer';
|
import TestRenderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
import { en, setImTranslator } from '../../../plugin-src/client/i18n.js';
|
||||||
import { normalizeSnapshot } from '../../../plugin-src/client/channels/weixin/api.js';
|
import { normalizeSnapshot } from '../../../plugin-src/client/channels/weixin/api.js';
|
||||||
import {
|
import {
|
||||||
AccountCard,
|
AccountCard,
|
||||||
|
|
@ -57,6 +58,13 @@ test('Weixin client keeps only the public connection-test result', () => {
|
||||||
state: 'connected',
|
state: 'connected',
|
||||||
configured: true,
|
configured: true,
|
||||||
bot: { name: '微信机器人', accountIdMasked: 'account••••1234' },
|
bot: { name: '微信机器人', accountIdMasked: 'account••••1234' },
|
||||||
|
lastMessageError: {
|
||||||
|
code: 'attachment-error',
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
message: '当前模型不支持图片。',
|
||||||
|
at: 123,
|
||||||
|
providerDetail: 'must-not-cross-client-normalization',
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -64,6 +72,42 @@ test('Weixin client keeps only the public connection-test result', () => {
|
||||||
sent: false,
|
sent: false,
|
||||||
code: 'test-target-unavailable',
|
code: 'test-target-unavailable',
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(snapshot.bots[0].lastMessageError, {
|
||||||
|
code: 'attachment-error',
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
message: '当前模型不支持图片。',
|
||||||
|
at: 123,
|
||||||
|
});
|
||||||
|
assert.doesNotMatch(JSON.stringify(snapshot), /must-not-cross-client-normalization/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Weixin account card shows the latest safe message-processing error', () => {
|
||||||
|
const props = {
|
||||||
|
account: {
|
||||||
|
...account('wx_image', '微信机器人'),
|
||||||
|
lastMessageError: {
|
||||||
|
code: 'attachment-error',
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
message: '当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。',
|
||||||
|
at: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onReconnect() {}, onRequestRemove() {}, onConfirmRemove() {}, onCancelRemove() {},
|
||||||
|
};
|
||||||
|
const markup = renderToStaticMarkup(React.createElement(AccountCard, props));
|
||||||
|
|
||||||
|
assert.match(markup, /最近一条消息处理失败/);
|
||||||
|
assert.match(markup, /当前模型不支持图片/);
|
||||||
|
|
||||||
|
setImTranslator((key) => en[key] ?? key);
|
||||||
|
try {
|
||||||
|
const english = renderToStaticMarkup(React.createElement(AccountCard, props));
|
||||||
|
assert.match(english, /Latest message failed/);
|
||||||
|
assert.match(english, /current model does not support images/i);
|
||||||
|
assert.doesNotMatch(english, /[\p{Script=Han}]/u);
|
||||||
|
} finally {
|
||||||
|
setImTranslator(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Weixin card feedback stays visible without hiding connection errors', () => {
|
test('Weixin card feedback stays visible without hiding connection errors', () => {
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ test('sendText emits the iLink message envelope without reflecting the token in
|
||||||
assert.equal(body.msg.context_token, 'message-context');
|
assert.equal(body.msg.context_token, 'message-context');
|
||||||
assert.equal(body.msg.item_list[0].text_item.text, 'Harness reply');
|
assert.equal(body.msg.item_list[0].text_item.text, 'Harness reply');
|
||||||
assert.equal(body.base_info.channel_version, '2.4.6');
|
assert.equal(body.base_info.channel_version, '2.4.6');
|
||||||
assert.equal(body.base_info.bot_agent, 'DeepSeekHarness/0.17.0');
|
assert.equal(body.base_info.bot_agent, 'DeepSeekHarness/0.17.1');
|
||||||
assert.doesNotMatch(calls[0].init.body, /host-only-token/);
|
assert.doesNotMatch(calls[0].init.body, /host-only-token/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,53 @@ test('Weixin returns a specific retry message when encrypted image loading fails
|
||||||
assert.equal(fixture.seen.has('weixin-image-error'), true);
|
assert.equal(fixture.seen.has('weixin-image-error'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Weixin explains model image rejection and records only safe structured diagnostics', async () => {
|
||||||
|
const fixture = stateFixture();
|
||||||
|
fixture.sessions.set('p2p:owner-user', 'session-image');
|
||||||
|
const sent = [];
|
||||||
|
const status = createWeixinBridgeStatus();
|
||||||
|
const bridge = new WeixinHarnessBridge({
|
||||||
|
api: {
|
||||||
|
inboundImages: () => [{ name: 'image', data: PNG_BYTES }],
|
||||||
|
sendText: async (request) => sent.push(request),
|
||||||
|
},
|
||||||
|
baseUrl: 'https://ilinkai.weixin.qq.com/',
|
||||||
|
token: 'host-token',
|
||||||
|
ownerUserId: 'owner-user',
|
||||||
|
harness: {
|
||||||
|
sessionExists: async () => true,
|
||||||
|
ask: async () => {
|
||||||
|
throw Object.assign(new Error('Model detail at /private/path with provider-token'), {
|
||||||
|
code: 'attachment-error',
|
||||||
|
details: {
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
providerDetail: 'must-not-cross-status-boundary',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: fixture.state,
|
||||||
|
status,
|
||||||
|
logger: { error() {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
await bridge.accept(message('weixin-model-image-error', '', {
|
||||||
|
item_list: [{ type: 2, image_item: { media: {} } }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.match(sent.at(-1).text, /当前模型不支持图片/);
|
||||||
|
assert.match(sent.at(-1).text, /\/models/);
|
||||||
|
assert.equal(fixture.seen.has('weixin-model-image-error'), true);
|
||||||
|
assert.deepEqual(status.lastMessageError, {
|
||||||
|
code: 'attachment-error',
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
message: sent.at(-1).text,
|
||||||
|
at: status.lastMessageError.at,
|
||||||
|
});
|
||||||
|
assert.equal(Number.isFinite(status.lastMessageError.at), true);
|
||||||
|
assert.doesNotMatch(JSON.stringify(status.lastMessageError), /private|provider-token|providerDetail/);
|
||||||
|
});
|
||||||
|
|
||||||
test('Weixin executes /compact for the bound Session without prompting the model', async () => {
|
test('Weixin executes /compact for the bound Session without prompting the model', async () => {
|
||||||
const fixture = stateFixture();
|
const fixture = stateFixture();
|
||||||
fixture.sessions.set('p2p:owner-user', 'session-compact');
|
fixture.sessions.set('p2p:owner-user', 'session-compact');
|
||||||
|
|
@ -1088,6 +1135,7 @@ test('bridge commands are local and internal failures return a generic message',
|
||||||
const fixture = stateFixture();
|
const fixture = stateFixture();
|
||||||
fixture.sessions.set('p2p:owner-user', 'old-session');
|
fixture.sessions.set('p2p:owner-user', 'old-session');
|
||||||
const sent = [];
|
const sent = [];
|
||||||
|
const status = createWeixinBridgeStatus();
|
||||||
const bridge = new WeixinHarnessBridge({
|
const bridge = new WeixinHarnessBridge({
|
||||||
api: { sendText: async (request) => sent.push(request.text) },
|
api: { sendText: async (request) => sent.push(request.text) },
|
||||||
baseUrl: 'https://ilinkai.weixin.qq.com/',
|
baseUrl: 'https://ilinkai.weixin.qq.com/',
|
||||||
|
|
@ -1099,6 +1147,7 @@ test('bridge commands are local and internal failures return a generic message',
|
||||||
ask: async () => { throw new Error('private path /secret and token-shaped detail'); },
|
ask: async () => { throw new Error('private path /secret and token-shaped detail'); },
|
||||||
},
|
},
|
||||||
state: fixture.state,
|
state: fixture.state,
|
||||||
|
status,
|
||||||
logger: { error() {} },
|
logger: { error() {} },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1107,4 +1156,11 @@ test('bridge commands are local and internal failures return a generic message',
|
||||||
await bridge.accept(message('failure', '触发失败'));
|
await bridge.accept(message('failure', '触发失败'));
|
||||||
assert.match(sent.at(-1), /消息处理失败/);
|
assert.match(sent.at(-1), /消息处理失败/);
|
||||||
assert.doesNotMatch(sent.at(-1), /private path|secret|token-shaped/);
|
assert.doesNotMatch(sent.at(-1), /private path|secret|token-shaped/);
|
||||||
|
assert.deepEqual(status.lastMessageError, {
|
||||||
|
code: 'message-processing-failed',
|
||||||
|
reason: 'UNKNOWN',
|
||||||
|
message: '消息处理失败,请稍后重试。',
|
||||||
|
at: status.lastMessageError.at,
|
||||||
|
});
|
||||||
|
assert.doesNotMatch(JSON.stringify(status.lastMessageError), /private path|secret|token-shaped/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function configFixture() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function runtimeFactory({ failStart = false, startError } = {}) {
|
function runtimeFactory({ failStart = false, startError, lastMessageError = null } = {}) {
|
||||||
const runtimes = [];
|
const runtimes = [];
|
||||||
const connectionTests = [];
|
const connectionTests = [];
|
||||||
const createRuntime = async ({ config, token }) => {
|
const createRuntime = async ({ config, token }) => {
|
||||||
|
|
@ -61,6 +61,7 @@ function runtimeFactory({ failStart = false, startError } = {}) {
|
||||||
weixinConnectionState: ready ? 'connected' : 'idle',
|
weixinConnectionState: ready ? 'connected' : 'idle',
|
||||||
harnessReachable: ready,
|
harnessReachable: ready,
|
||||||
lastCheckedAt: ready ? 100 : null,
|
lastCheckedAt: ready ? 100 : null,
|
||||||
|
lastMessageError,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async start() {
|
async start() {
|
||||||
|
|
@ -80,7 +81,15 @@ function runtimeFactory({ failStart = false, startError } = {}) {
|
||||||
test('confirmed QR login stores bot_token only in credentials and starts a redacted account', async () => {
|
test('confirmed QR login stores bot_token only in credentials and starts a redacted account', async () => {
|
||||||
const credentials = credentialsFixture();
|
const credentials = credentialsFixture();
|
||||||
const configs = configFixture();
|
const configs = configFixture();
|
||||||
const runtimes = runtimeFactory();
|
const runtimes = runtimeFactory({
|
||||||
|
lastMessageError: {
|
||||||
|
code: 'attachment-error',
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
message: '当前模型不支持图片。',
|
||||||
|
at: 123,
|
||||||
|
providerDetail: 'must-not-cross-controller-boundary',
|
||||||
|
},
|
||||||
|
});
|
||||||
const controller = new WeixinController({
|
const controller = new WeixinController({
|
||||||
api: {
|
api: {
|
||||||
beginLogin: async ({ localTokens }) => {
|
beginLogin: async ({ localTokens }) => {
|
||||||
|
|
@ -116,6 +125,13 @@ test('confirmed QR login stores bot_token only in credentials and starts a redac
|
||||||
const publicJson = JSON.stringify(controller.status());
|
const publicJson = JSON.stringify(controller.status());
|
||||||
assert.doesNotMatch(publicJson, /private-bot-token|owner-user|account@im\.bot|tokenRef/);
|
assert.doesNotMatch(publicJson, /private-bot-token|owner-user|account@im\.bot|tokenRef/);
|
||||||
assert.equal(controller.status().totals.connected, 1);
|
assert.equal(controller.status().totals.connected, 1);
|
||||||
|
assert.deepEqual(controller.status().bots[0].lastMessageError, {
|
||||||
|
code: 'attachment-error',
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
message: '当前模型不支持图片。',
|
||||||
|
at: 123,
|
||||||
|
});
|
||||||
|
assert.doesNotMatch(publicJson, /must-not-cross-controller-boundary/);
|
||||||
|
|
||||||
await controller.sendConnectionTest(completed.botId);
|
await controller.sendConnectionTest(completed.botId);
|
||||||
assert.equal(runtimes.connectionTests[0].botId, completed.botId);
|
assert.equal(runtimes.connectionTests[0].botId, completed.botId);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import {
|
||||||
ImagePromptError,
|
ImagePromptError,
|
||||||
fetchImageBuffer,
|
fetchImageBuffer,
|
||||||
hasInboundPrompt,
|
hasInboundPrompt,
|
||||||
|
imagePromptDiagnostic,
|
||||||
|
imagePromptUserMessage,
|
||||||
promptContentForMessage,
|
promptContentForMessage,
|
||||||
} from '../src/channels/shared/image-prompt.mjs';
|
} from '../src/channels/shared/image-prompt.mjs';
|
||||||
import { HarnessClient } from '../src/channels/shared/harness-client.mjs';
|
import { HarnessClient } from '../src/channels/shared/harness-client.mjs';
|
||||||
|
|
@ -100,6 +102,49 @@ test('unsupported image bytes receive a channel-safe error', async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Harness attachment failures map only allowlisted reasons to safe channel messages', () => {
|
||||||
|
const modelError = Object.assign(new Error('private provider detail'), {
|
||||||
|
code: 'attachment-error',
|
||||||
|
details: { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES', privatePath: '/secret/model' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(imagePromptDiagnostic(modelError), {
|
||||||
|
code: 'attachment-error',
|
||||||
|
reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
||||||
|
userMessage: '当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。',
|
||||||
|
});
|
||||||
|
assert.doesNotMatch(imagePromptUserMessage(modelError), /private|secret/);
|
||||||
|
|
||||||
|
for (const reason of [
|
||||||
|
'IMAGE_TOO_LARGE',
|
||||||
|
'IMAGE_TOO_MANY_PIXELS',
|
||||||
|
'INVALID_IMAGE',
|
||||||
|
'INVALID_IMAGE_BASE64',
|
||||||
|
'IMAGE_TYPE_MISMATCH',
|
||||||
|
'TOO_MANY_IMAGES',
|
||||||
|
'IMAGES_TOO_LARGE',
|
||||||
|
]) {
|
||||||
|
assert.equal(typeof imagePromptUserMessage({
|
||||||
|
code: 'attachment-error', details: { reason },
|
||||||
|
}), 'string', reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown Harness failures remain generic and cannot access inherited map properties', () => {
|
||||||
|
for (const reason of ['FUTURE_PRIVATE_REASON', 'toString']) {
|
||||||
|
const error = {
|
||||||
|
code: 'attachment-error',
|
||||||
|
details: { reason },
|
||||||
|
message: 'provider token and /private/path',
|
||||||
|
};
|
||||||
|
assert.equal(imagePromptDiagnostic(error), null);
|
||||||
|
assert.equal(imagePromptUserMessage(error), null);
|
||||||
|
}
|
||||||
|
assert.equal(imagePromptUserMessage({
|
||||||
|
code: 'agent-busy',
|
||||||
|
details: { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' },
|
||||||
|
}), null);
|
||||||
|
});
|
||||||
|
|
||||||
test('bounded HTTPS downloads enforce response size before buffering', async () => {
|
test('bounded HTTPS downloads enforce response size before buffering', async () => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const fetchImpl = async (url, options) => {
|
const fetchImpl = async (url, options) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue