#!/usr/bin/env node const fs = require('fs'); const http = require('http'); const os = require('os'); const path = require('path'); const { analyzeFmodeImage } = require('../mcp/src/tools/fmode-image-analysis'); async function main() { const noToken = await analyzeFmodeImage({ images: [tinyPngDataUrl()], prompt: '识别图片内容', allowEnvToken: false }); assert(noToken.status === 'needs_token', 'missing token should return needs_token'); assert(Array.isArray(noToken.errors) && noToken.errors.length === 0, 'missing token should keep errors empty'); assert(!/Bearer\s+/i.test(JSON.stringify(noToken)), 'missing token output should not leak bearer headers'); assert(/apig-pay/.test(noToken.assistantMessage), 'missing token output should include recharge link'); const mock = await startMockFmode(); try { const out = path.join(os.tmpdir(), `fmode-image-analysis-smoke-${Date.now()}`); const result = await analyzeFmodeImage({ baseUrl: mock.url, path: 'v1/chat/completions', fmodeToken: 'mock-platform-token', images: [tinyPngDataUrl()], prompt: '请用中文识别图片中的文字和主体。', output: out, allowEnvToken: false }); assert(result.status === 'ok', 'mock image analysis should return ok'); assert(result.summary.model === 'doubao-seed-2-0-pro-260215', 'default model should be doubao-seed-2-0-pro-260215'); assert(result.assistantMessage.includes('Fmode 图片分析结果'), 'assistant message should include result heading'); assert(result.data.analysis.includes('测试图片'), 'analysis text should be parsed from mock response'); assert(result.files.length === 2 && result.files.every(file => fs.existsSync(file)), 'output files should be written'); const request = mock.requests[0]; assert(request.headers.authorization === 'Bearer mock-platform-token', 'mock request should include platform token'); assert(request.body.model === 'doubao-seed-2-0-pro-260215', 'request should use the configured default model'); assert(Array.isArray(request.body.messages), 'request should use OpenAI-compatible messages'); const userMessage = request.body.messages.find(item => item.role === 'user'); assert(Array.isArray(userMessage.content), 'user content should be multimodal array'); assert(userMessage.content.some(item => item.type === 'image_url'), 'user content should include image_url'); assert(!JSON.stringify(result).includes('mock-platform-token'), 'public result should not leak token'); } finally { await mock.close(); } console.log('fmode image analysis smoke ok'); } function startMockFmode() { const requests = []; const server = http.createServer((req, res) => { if (!req.url.startsWith('/v1/chat/completions')) { res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'not_found' })); return; } let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { requests.push({ headers: req.headers, body: body ? JSON.parse(body) : {} }); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ choices: [ { finish_reason: 'stop', message: { content: '这是一张测试图片,可见一个小色块;未发现明显风险。' } } ], usage: { prompt_tokens: 12, completion_tokens: 18, total_tokens: 30 } })); }); }); return new Promise(resolve => { server.listen(0, '127.0.0.1', () => { resolve({ url: `http://127.0.0.1:${server.address().port}`, requests, close: () => new Promise(done => server.close(done)) }); }); }); } function tinyPngDataUrl() { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='; } function assert(condition, message) { if (!condition) throw new Error(message); } main().catch(error => { console.error(error && error.stack ? error.stack : String(error)); process.exit(1); });