#!/usr/bin/env node 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const source = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8'); function extractFunction(name) { const declaration = new RegExp(`^ function ${name}\\(`, 'm'); const start = source.search(declaration); assert(start >= 0, `缺少前端函数 ${name}`); const remaining = source.slice(start + 1); const next = remaining.search(/^ (?:async )?function \w+\(/m); assert(next >= 0, `无法确定前端函数 ${name} 的边界`); return source.slice(start, start + 1 + next); } class Element { constructor(tagName) { this.tagName = tagName; this.children = []; this.textContent = ''; this.attributes = {}; this.listeners = {}; } appendChild(child) { this.children.push(child); return child; } setAttribute(name, value) { this.attributes[name] = value; } addEventListener(name, listener) { this.listeners[name] = listener; } insertAdjacentHTML(position, html) { this.html = html; } } function createHarness() { // 解析、分支选择和直链图片 DOM 均执行生产函数,只替换附件加载及浏览器环境。 return new Function('Element', ` const frames = [], rendered = [], hydrated = []; const document = { createElement: (tagName) => new Element(tagName) }; function requestAnimationFrame(callback) { frames.push(callback); } function renderAttachmentPreviews(attachments) { rendered.push(attachments); return '
'; } function hydrateAttachmentPreviews(wrapper, attachments) { hydrated.push({ wrapper, attachments }); } ${[ 'parseMaybeJsonObject', 'parseCcwebDisplayImageResult', 'createCcwebDisplayImageToolContent', 'createAssistantImageElement', ].map(extractFunction).join('\n')} return { parse: parseCcwebDisplayImageResult, render: createCcwebDisplayImageToolContent, rendered, hydrated, flushFrames() { while (frames.length) frames.shift()(); }, }; `)(Element); } function run() { const harness = createHarness(); const attachment = { id: 'generated-image', filename: 'generated.png', storageState: 'ready' }; const success = { ok: true, image: { url: 'https://example.test/generated.png', title: '生成的图片' }, attachment, }; const textContent = [{ type: 'text', text: JSON.stringify(success) }]; // 与故障会话存档相同的 CallToolResult 结构,包含重复的业务 JSON 和 _meta。 const historicalEnvelope = { content: textContent, structuredContent: success, _meta: null }; const successCases = [ ['直接业务对象', success], ['直接业务 JSON', JSON.stringify(success)], ['历史 MCP 对象', historicalEnvelope], ['历史 MCP JSON', JSON.stringify(historicalEnvelope)], ['仅 structuredContent', { structuredContent: success }], ['structuredContent 为 JSON', { structuredContent: JSON.stringify(success) }], ['仅 content 文本', { content: textContent }], ['content 文本包装的 JSON', JSON.stringify({ content: textContent })], ['缺失结构化结果', { structuredContent: null, content: textContent }], ['结构化结果无效时回退文本', { structuredContent: '无效结果', content: textContent }], ['跳过非业务文本及其他块', { content: [ { type: 'image', data: 'irrelevant' }, { type: 'text', text: '处理中' }, { type: 'text', text: '{invalid json' }, ...textContent, ] }], ]; for (const [label, result] of successCases) { assert.deepEqual(harness.parse(result), success, `${label}应解析为业务结果`); const renderedCount = harness.rendered.length; const node = harness.render({ result, done: true }); assert.equal(node.textContent, '', `${label}不能误报图片失败`); assert.equal(harness.rendered.length, renderedCount + 1, `${label}应进入附件预览分支`); assert.deepEqual(harness.rendered.at(-1), [attachment]); harness.flushFrames(); assert.equal(harness.hydrated.at(-1).wrapper, node); assert.deepEqual(harness.hydrated.at(-1).attachments, [attachment]); } const failure = { ok: false, message: '图片文件不存在' }; for (const result of [failure, JSON.stringify(failure), { structuredContent: failure, content: textContent }, { content: [{ type: 'text', text: JSON.stringify(failure) }] }]) { assert.deepEqual(harness.parse(result), failure, '应保留明确的业务失败'); assert.equal(harness.render({ result, done: true }).textContent, failure.message); } const transportErrors = [ [{ isError: true, content: [{ type: 'text', text: 'MCP 图片工具读取失败' }] }, 'MCP 图片工具读取失败'], [{ isError: true, structuredContent: failure }, failure.message], [{ isError: true, message: '工具调用失败', structuredContent: success }, '工具调用失败'], [{ isError: true, structuredContent: success, content: textContent }, JSON.stringify(success)], [{ isError: true }, '图片渲染失败'], ]; for (const [result, message] of transportErrors) { assert.equal(harness.parse(result).ok, false, 'MCP 错误不能被成功内容覆盖'); assert.equal(harness.render({ result: JSON.stringify(result), done: true }).textContent, message); } const malformed = [null, undefined, '', '{invalid', 'null', 'false', '42', '"text"', [], JSON.stringify([success]), {}, { ok: 'true', attachment }, { content: 'invalid' }, { content: [{ type: 'text', text: 'invalid' }] }, { structuredContent: { structuredContent: success } }]; const cyclic = {}; cyclic.structuredContent = cyclic; malformed.push(cyclic); for (const result of malformed) { assert.equal(harness.parse(result), null, '畸形或循环结果应安全返回空值'); assert.equal(harness.render({ result, done: true }).textContent, '图片渲染失败'); } assert.equal(harness.render({ done: false }).textContent, '正在准备图片…', '未完成调用应显示等待状态'); const inlineResult = { ok: true, image: { url: 'https://example.test/inline.png', title: '直链图片' } }; for (const result of [inlineResult, { structuredContent: inlineResult }, { content: [{ type: 'text', text: JSON.stringify(inlineResult) }] }]) { const node = harness.render({ result: JSON.stringify(result), done: true }); const figure = node.children[0]; assert.equal(figure.tagName, 'figure', '直链结果应进入图片分支'); assert.equal(figure.children[0].children[0].src, inlineResult.image.url); assert.equal(figure.children[0].children[0].alt, inlineResult.image.title); } for (const image of [{}, { url: 'javascript:alert(1)' }]) { assert.equal(harness.render({ result: { ok: true, image }, done: true }).textContent, '图片来源不可用', '无可用图片来源时应保留兜底提示'); } console.log('ccweb_display_image 解析及渲染回归通过'); } if (require.main === module) run(); module.exports = { createHarness, run };