fmode-image-analysis-smoke.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const http = require('http');
  4. const os = require('os');
  5. const path = require('path');
  6. const { analyzeFmodeImage } = require('../mcp/src/tools/fmode-image-analysis');
  7. async function main() {
  8. const noToken = await analyzeFmodeImage({
  9. images: [tinyPngDataUrl()],
  10. prompt: '识别图片内容',
  11. allowEnvToken: false
  12. });
  13. assert(noToken.status === 'needs_token', 'missing token should return needs_token');
  14. assert(Array.isArray(noToken.errors) && noToken.errors.length === 0, 'missing token should keep errors empty');
  15. assert(!/Bearer\s+/i.test(JSON.stringify(noToken)), 'missing token output should not leak bearer headers');
  16. assert(/settings\.json|Session Token|凭证/.test(noToken.assistantMessage), 'missing token output should explain credential recovery');
  17. assert(!/apig-pay|balance=fmodeapi/.test(noToken.assistantMessage), 'missing token output should not show a payment link');
  18. const mock = await startMockFmode();
  19. try {
  20. const out = path.join(os.tmpdir(), `fmode-image-analysis-smoke-${Date.now()}`);
  21. const result = await analyzeFmodeImage({
  22. baseUrl: mock.url,
  23. path: 'v1/chat/completions',
  24. fmodeToken: 'mock-platform-token',
  25. images: [tinyPngDataUrl()],
  26. prompt: '请用中文识别图片中的文字和主体。',
  27. output: out,
  28. allowEnvToken: false
  29. });
  30. assert(result.status === 'ok', 'mock image analysis should return ok');
  31. assert(result.summary.model === 'doubao-seed-2-0-pro-260215', 'default model should be doubao-seed-2-0-pro-260215');
  32. assert(result.assistantMessage.includes('Fmode 图片分析结果'), 'assistant message should include result heading');
  33. assert(result.data.analysis.includes('测试图片'), 'analysis text should be parsed from mock response');
  34. assert(result.files.length === 2 && result.files.every(file => fs.existsSync(file)), 'output files should be written');
  35. const request = mock.requests[0];
  36. assert(request.headers.authorization === 'Bearer mock-platform-token', 'mock request should include platform token');
  37. assert(request.body.model === 'doubao-seed-2-0-pro-260215', 'request should use the configured default model');
  38. assert(Array.isArray(request.body.messages), 'request should use OpenAI-compatible messages');
  39. const userMessage = request.body.messages.find(item => item.role === 'user');
  40. assert(Array.isArray(userMessage.content), 'user content should be multimodal array');
  41. assert(userMessage.content.some(item => item.type === 'image_url'), 'user content should include image_url');
  42. assert(!JSON.stringify(result).includes('mock-platform-token'), 'public result should not leak token');
  43. } finally {
  44. await mock.close();
  45. }
  46. console.log('fmode image analysis smoke ok');
  47. }
  48. function startMockFmode() {
  49. const requests = [];
  50. const server = http.createServer((req, res) => {
  51. if (!req.url.startsWith('/v1/chat/completions')) {
  52. res.writeHead(404, { 'content-type': 'application/json' });
  53. res.end(JSON.stringify({ error: 'not_found' }));
  54. return;
  55. }
  56. let body = '';
  57. req.on('data', chunk => { body += chunk; });
  58. req.on('end', () => {
  59. requests.push({
  60. headers: req.headers,
  61. body: body ? JSON.parse(body) : {}
  62. });
  63. res.writeHead(200, { 'content-type': 'application/json' });
  64. res.end(JSON.stringify({
  65. choices: [
  66. {
  67. finish_reason: 'stop',
  68. message: {
  69. content: '这是一张测试图片,可见一个小色块;未发现明显风险。'
  70. }
  71. }
  72. ],
  73. usage: {
  74. prompt_tokens: 12,
  75. completion_tokens: 18,
  76. total_tokens: 30
  77. }
  78. }));
  79. });
  80. });
  81. return new Promise(resolve => {
  82. server.listen(0, '127.0.0.1', () => {
  83. resolve({
  84. url: `http://127.0.0.1:${server.address().port}`,
  85. requests,
  86. close: () => new Promise(done => server.close(done))
  87. });
  88. });
  89. });
  90. }
  91. function tinyPngDataUrl() {
  92. return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=';
  93. }
  94. function assert(condition, message) {
  95. if (!condition) throw new Error(message);
  96. }
  97. main().catch(error => {
  98. console.error(error && error.stack ? error.stack : String(error));
  99. process.exit(1);
  100. });