feat: expose image generation to page agents

This commit is contained in:
john
2026-07-19 18:39:46 +08:00
parent 5b88f8ece5
commit 9cf12e3786
10 changed files with 220 additions and 10 deletions
+66
View File
@@ -40,6 +40,8 @@ const PRIVATE_DATA_MAX_BYTES = Number(process.env.PRIVATE_DATA_MAX_BYTES ?? 20 *
const PRIVATE_DATA_QUERY_TIMEOUT_MS = Number(process.env.PRIVATE_DATA_QUERY_TIMEOUT_MS ?? 5000);
const PRIVATE_DATA_MAX_ROWS = Number(process.env.PRIVATE_DATA_MAX_ROWS ?? 200);
const PRIVATE_DATA_USER_ID = process.env.PRIVATE_DATA_USER_ID?.trim();
const AGENT_API_BASE_URL = process.env.MINDSPACE_AGENT_API_BASE_URL?.trim();
const INTERNAL_AGENT_SECRET = process.env.MINDSPACE_INTERNAL_AGENT_SECRET?.trim();
const allowedToolsEnv = process.env.ALLOWED_TOOLS?.trim();
const ALLOWED_TOOLS = allowedToolsEnv ? new Set(allowedToolsEnv.split(',').map((s) => s.trim())) : null;
@@ -121,6 +123,39 @@ function runGenerateDocxScript({ outputPath, title, sections }) {
return { bytes: size, stdout: String(stdout ?? '').trim() };
}
async function generateMindSpaceImage({ purpose, prompt, negativePrompt, idempotencyKey }) {
if (!AGENT_API_BASE_URL || !INTERNAL_AGENT_SECRET || !PRIVATE_DATA_USER_ID) {
throw new Error('generate_image: Memind 内部生图入口未配置');
}
const endpoint = new URL('agent/mindspace_image_generate', `${AGENT_API_BASE_URL.replace(/\/+$/, '')}/`);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
accept: 'application/json',
authorization: `Bearer ${INTERNAL_AGENT_SECRET}`,
'content-type': 'application/json',
...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
},
body: JSON.stringify({
user_id: PRIVATE_DATA_USER_ID,
purpose,
prompt,
negative_prompt: negativePrompt,
}),
signal: AbortSignal.timeout(Number(process.env.IMAGE_MAKE_GENERATION_TIMEOUT_MS ?? 600_000)),
});
let body = null;
try {
body = await response.json();
} catch {
// Safe error below intentionally excludes the response body.
}
if (!response.ok && !body?.fallback) {
throw new Error(`generate_image: Memind 返回 ${response.status}`);
}
return body?.data ?? body ?? { ok: false, fallback: true, code: 'empty_response' };
}
const ALL_TOOLS = [
{
name: 'read_file',
@@ -195,6 +230,25 @@ const ALL_TOOLS = [
required: ['html_path'],
},
},
{
name: 'generate_image',
description:
'通过平台 image_make 服务生成页面图片并直接写入当前用户 MindSpace。仅在后台开关开启时生效;失败时继续原页面流程。返回 canonical publicUrl 与 workspaceRelativePath。',
inputSchema: {
type: 'object',
properties: {
purpose: {
type: 'string',
enum: ['inline_image', 'hero', 'card_cover', 'feed_cover'],
description: '图片用途',
},
prompt: { type: 'string', description: '图片描述,画面内不要包含文字、水印或二维码' },
negative_prompt: { type: 'string', description: '可选负面描述' },
idempotency_key: { type: 'string', description: '可选幂等键;同一页面同一用途应复用' },
},
required: ['purpose', 'prompt'],
},
},
{
name: 'generate_docx',
description:
@@ -534,6 +588,18 @@ async function callTool(name, args) {
fs.mkdirSync(abs, { recursive: true });
return [{ type: 'text', text: `已创建目录 ${args.path}` }];
}
case 'generate_image': {
const purpose = String(args.purpose ?? '').trim();
const prompt = String(args.prompt ?? '').trim();
if (!prompt) throw new Error('generate_image: prompt 不能为空');
const result = await generateMindSpaceImage({
purpose,
prompt,
negativePrompt: String(args.negative_prompt ?? '').trim(),
idempotencyKey: String(args.idempotency_key ?? '').trim(),
});
return [{ type: 'text', text: JSON.stringify(result, null, 2) }];
}
case 'generate_long_image': {
const htmlPath = String(args.html_path ?? args.path ?? '').trim();
if (!htmlPath.toLowerCase().endsWith('.html')) {