mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
feat: support image prompts across IM channels
This commit is contained in:
parent
99a877c640
commit
65c842c045
40 changed files with 3806 additions and 274 deletions
183
test/image-bridge.test.mjs
Normal file
183
test/image-bridge.test.mjs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { TextHarnessBridge } from '../src/channels/shared/text-harness-bridge.mjs';
|
||||
|
||||
const PNG_1X1 = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||
'base64',
|
||||
);
|
||||
|
||||
function memoryState() {
|
||||
const sessions = new Map();
|
||||
const seen = new Set();
|
||||
return {
|
||||
sessionFor: (key) => sessions.get(key) ?? null,
|
||||
setSession: async (key, value) => sessions.set(key, value),
|
||||
clearSession: async (key) => sessions.delete(key),
|
||||
hasSeen: (id) => seen.has(id),
|
||||
markSeen: async (id) => seen.add(id),
|
||||
};
|
||||
}
|
||||
|
||||
function bridgeFixture() {
|
||||
const sent = [];
|
||||
const prompts = [];
|
||||
const bridge = new TextHarnessBridge({
|
||||
descriptor: { key: 'test', label: 'Test' },
|
||||
bot: { sendText: async (_target, text) => sent.push(text) },
|
||||
harness: {
|
||||
ensureRunning: async () => true,
|
||||
sessionExists: async () => true,
|
||||
createSession: async () => 'session-image',
|
||||
ask: async (_sessionId, prompt) => {
|
||||
prompts.push(prompt);
|
||||
return '识别成功';
|
||||
},
|
||||
},
|
||||
state: memoryState(),
|
||||
});
|
||||
return { bridge, sent, prompts };
|
||||
}
|
||||
|
||||
test('the shared bridge sends image and caption content to Harness', async () => {
|
||||
const { bridge, sent, prompts } = bridgeFixture();
|
||||
let loads = 0;
|
||||
await bridge.accept({
|
||||
messageId: 'image-1',
|
||||
senderId: 'user-1',
|
||||
kind: 'direct',
|
||||
conversationId: 'user-1',
|
||||
content: '图中是什么?',
|
||||
images: [{ async load() { loads += 1; return PNG_1X1; } }],
|
||||
replyTarget: {},
|
||||
});
|
||||
|
||||
assert.equal(loads, 1);
|
||||
assert.deepEqual(prompts, [[
|
||||
{ type: 'text', text: '图中是什么?' },
|
||||
{ type: 'image', mediaType: 'image/png', data: PNG_1X1.toString('base64') },
|
||||
]]);
|
||||
assert.deepEqual(sent, ['识别成功']);
|
||||
});
|
||||
|
||||
test('unaddressed group images are rejected before their bytes are downloaded', async () => {
|
||||
const { bridge, sent, prompts } = bridgeFixture();
|
||||
let loads = 0;
|
||||
await bridge.accept({
|
||||
messageId: 'image-2',
|
||||
senderId: 'user-1',
|
||||
kind: 'group',
|
||||
conversationId: 'group-1',
|
||||
content: '',
|
||||
addressed: false,
|
||||
images: [{ async load() { loads += 1; return PNG_1X1; } }],
|
||||
replyTarget: {},
|
||||
});
|
||||
|
||||
assert.equal(loads, 0);
|
||||
assert.deepEqual(prompts, []);
|
||||
assert.deepEqual(sent, []);
|
||||
});
|
||||
|
||||
test('image validation failures receive a specific safe reply', async () => {
|
||||
const { bridge, sent, prompts } = bridgeFixture();
|
||||
await bridge.accept({
|
||||
messageId: 'image-3',
|
||||
senderId: 'user-1',
|
||||
kind: 'direct',
|
||||
conversationId: 'user-1',
|
||||
content: '',
|
||||
images: [{ data: Buffer.from('not an image') }],
|
||||
replyTarget: {},
|
||||
});
|
||||
|
||||
assert.deepEqual(prompts, []);
|
||||
assert.deepEqual(sent, ['暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。']);
|
||||
});
|
||||
|
||||
test('an image caption cannot answer a pending Harness question', async () => {
|
||||
const sent = [];
|
||||
const questionSent = Promise.withResolvers();
|
||||
const answered = Promise.withResolvers();
|
||||
let interactionResult;
|
||||
let imageLoads = 0;
|
||||
const bridge = new TextHarnessBridge({
|
||||
descriptor: { key: 'test', label: 'Test' },
|
||||
bot: {
|
||||
async sendText(_target, text) {
|
||||
sent.push(text);
|
||||
if (text.includes('请选择环境')) questionSent.resolve();
|
||||
},
|
||||
},
|
||||
harness: {
|
||||
ensureRunning: async () => true,
|
||||
sessionExists: async () => true,
|
||||
createSession: async () => 'session-question',
|
||||
async ask(_sessionId, prompt, options) {
|
||||
assert.equal(prompt, '开始测试');
|
||||
await options.onInteraction({
|
||||
kind: 'question',
|
||||
interactionId: 'question-1',
|
||||
rpcId: 'question-1',
|
||||
sessionId: 'session-question',
|
||||
payload: {
|
||||
questions: [{
|
||||
id: 'environment',
|
||||
question: '请选择环境',
|
||||
options: [{ label: '生产环境' }],
|
||||
}],
|
||||
},
|
||||
async respond(result) {
|
||||
interactionResult = result;
|
||||
answered.resolve();
|
||||
return { accepted: true };
|
||||
},
|
||||
});
|
||||
await answered.promise;
|
||||
return '已完成';
|
||||
},
|
||||
},
|
||||
state: memoryState(),
|
||||
});
|
||||
|
||||
const original = bridge.accept({
|
||||
messageId: 'question-start',
|
||||
senderId: 'user-1',
|
||||
kind: 'direct',
|
||||
conversationId: 'user-1',
|
||||
content: '开始测试',
|
||||
replyTarget: {},
|
||||
});
|
||||
await questionSent.promise;
|
||||
|
||||
await bridge.accept({
|
||||
messageId: 'question-image',
|
||||
senderId: 'user-1',
|
||||
kind: 'direct',
|
||||
conversationId: 'user-1',
|
||||
content: '生产环境',
|
||||
images: [{ async load() { imageLoads += 1; return PNG_1X1; } }],
|
||||
replyTarget: {},
|
||||
});
|
||||
assert.equal(imageLoads, 0);
|
||||
assert.equal(interactionResult, undefined);
|
||||
assert.equal(sent.at(-1), '请用文字回答当前问题。');
|
||||
|
||||
await bridge.accept({
|
||||
messageId: 'question-answer',
|
||||
senderId: 'user-1',
|
||||
kind: 'direct',
|
||||
conversationId: 'user-1',
|
||||
content: '生产环境',
|
||||
replyTarget: {},
|
||||
});
|
||||
await original;
|
||||
assert.deepEqual(interactionResult, {
|
||||
ok: true,
|
||||
value: {
|
||||
sessionId: 'session-question',
|
||||
answer: { answers: [{ id: 'environment', selected: ['生产环境'] }] },
|
||||
},
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue