feat: 支持 MCP 图片内联渲染
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SERVER_INFO = {
|
||||
name: 'ccweb',
|
||||
@@ -10,6 +12,20 @@ const SERVER_INFO = {
|
||||
};
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
name: 'ccweb_display_image',
|
||||
description: '在当前会话的助手气泡中显示图片。支持 http/https 图片地址、data:image/*;base64 图片或本地图片绝对路径(最大 10 MB)。',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
source: { type: 'string', description: '图片 URL、data:image/*;base64,... 或本地绝对路径。' },
|
||||
alt: { type: 'string', maxLength: 240, description: '图片替代文本。' },
|
||||
title: { type: 'string', maxLength: 240, description: '图片标题。' },
|
||||
},
|
||||
required: ['source'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ccweb_list_conversations',
|
||||
description: '列出当前 ccweb 中可投递消息的对话。只返回 ID、标题、Agent、运行状态和更新时间,不返回对话正文。',
|
||||
@@ -257,6 +273,31 @@ const TOOLS = [
|
||||
},
|
||||
];
|
||||
|
||||
function imageMimeFromPath(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return ({ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif' })[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
function prepareImagePayload(args = {}) {
|
||||
const source = String(args.source || '').trim();
|
||||
if (!source) return { ok: false, code: 'missing_source', message: 'source 不能为空。' };
|
||||
if (/^https?:\/\//i.test(source)) return { ok: true, image: { type: 'image', url: source, mimeType: '', alt: String(args.alt || ''), title: String(args.title || '') } };
|
||||
if (/^data:image\//i.test(source)) {
|
||||
const match = source.match(/^data:(image\/[\w.+-]+);base64,([\s\S]+)$/i);
|
||||
if (!match) return { ok: false, code: 'invalid_data_image', message: '仅支持 base64 编码的 data:image 图片。' };
|
||||
if (Buffer.byteLength(match[2], 'base64') > 10 * 1024 * 1024) return { ok: false, code: 'image_too_large', message: '图片不能超过 10 MB。' };
|
||||
return { ok: true, image: { type: 'image', data: match[2], mimeType: match[1], alt: String(args.alt || ''), title: String(args.title || '') } };
|
||||
}
|
||||
if (!path.isAbsolute(source)) return { ok: false, code: 'invalid_local_path', message: '本地图片必须使用绝对路径。' };
|
||||
try {
|
||||
const stat = fs.statSync(source);
|
||||
if (!stat.isFile() || stat.size > 10 * 1024 * 1024) return { ok: false, code: 'image_too_large', message: '本地图片不存在或超过 10 MB。' };
|
||||
const mimeType = imageMimeFromPath(source);
|
||||
if (!mimeType.startsWith('image/')) return { ok: false, code: 'unsupported_image_type', message: '不支持该本地图片格式。' };
|
||||
return { ok: true, image: { type: 'image', data: fs.readFileSync(source).toString('base64'), mimeType, alt: String(args.alt || path.basename(source)), title: String(args.title || path.basename(source)) } };
|
||||
} catch { return { ok: false, code: 'image_read_failed', message: '无法读取本地图片。' }; }
|
||||
}
|
||||
|
||||
function writeMessage(message) {
|
||||
process.stdout.write(`${JSON.stringify(message)}\n`);
|
||||
}
|
||||
@@ -452,7 +493,7 @@ function runStdioServer() {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { TOOLS, runStdioServer };
|
||||
module.exports = { TOOLS, prepareImagePayload, runStdioServer };
|
||||
|
||||
if (require.main === module) {
|
||||
runStdioServer();
|
||||
|
||||
Reference in New Issue
Block a user