mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
fix(channels): clarify unsupported image model errors
This commit is contained in:
parent
c3b3a29f27
commit
5e5c7e46c5
12 changed files with 436 additions and 142 deletions
|
|
@ -6,6 +6,7 @@ import React from 'react';
|
|||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
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 {
|
||||
AccountCard,
|
||||
|
|
@ -57,6 +58,13 @@ test('Weixin client keeps only the public connection-test result', () => {
|
|||
state: 'connected',
|
||||
configured: true,
|
||||
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,
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const fixture = stateFixture();
|
||||
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();
|
||||
fixture.sessions.set('p2p:owner-user', 'old-session');
|
||||
const sent = [];
|
||||
const status = createWeixinBridgeStatus();
|
||||
const bridge = new WeixinHarnessBridge({
|
||||
api: { sendText: async (request) => sent.push(request.text) },
|
||||
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'); },
|
||||
},
|
||||
state: fixture.state,
|
||||
status,
|
||||
logger: { error() {} },
|
||||
});
|
||||
|
||||
|
|
@ -1107,4 +1156,11 @@ test('bridge commands are local and internal failures return a generic message',
|
|||
await bridge.accept(message('failure', '触发失败'));
|
||||
assert.match(sent.at(-1), /消息处理失败/);
|
||||
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 connectionTests = [];
|
||||
const createRuntime = async ({ config, token }) => {
|
||||
|
|
@ -61,6 +61,7 @@ function runtimeFactory({ failStart = false, startError } = {}) {
|
|||
weixinConnectionState: ready ? 'connected' : 'idle',
|
||||
harnessReachable: ready,
|
||||
lastCheckedAt: ready ? 100 : null,
|
||||
lastMessageError,
|
||||
};
|
||||
},
|
||||
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 () => {
|
||||
const credentials = credentialsFixture();
|
||||
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({
|
||||
api: {
|
||||
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());
|
||||
assert.doesNotMatch(publicJson, /private-bot-token|owner-user|account@im\.bot|tokenRef/);
|
||||
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);
|
||||
assert.equal(runtimes.connectionTests[0].botId, completed.botId);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
ImagePromptError,
|
||||
fetchImageBuffer,
|
||||
hasInboundPrompt,
|
||||
imagePromptDiagnostic,
|
||||
imagePromptUserMessage,
|
||||
promptContentForMessage,
|
||||
} from '../src/channels/shared/image-prompt.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 () => {
|
||||
let cancelled = false;
|
||||
const fetchImpl = async (url, options) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue