# 文生图与图生图工具设计

> 日期：2026-05-02
> 状态：待实施
> 范围：后端工具层、图片 Provider 层、前端工具渲染、Prompt Builder

## 1. 背景与目标

系统已在模型渠道中配置了火山引擎和 MiniMax 的图片生成模型。需要在 Agent 对话中提供文生图和图生图能力，主要面向电商场景（产品主图、详情图、文章配图），并支持配置专用电商套图 Agent 一次性生成成套图片。

### 目标

- 新增 `text_to_image` 和 `image_to_image` 两个独立工具
- 建立可扩展的图片生成 Provider 层，当前支持火山引擎和 MiniMax，未来可扩展其他渠道
- 扩展 `ToolResultContent` 支持多图返回
- 前端 tool-card 支持单图/多图渲染
- 生成图片转存本地，解决 Provider 临时 URL 过期问题
- Prompt Builder 根据可用工具动态生成附件提示语

### 不做

- 不改造 LLM Provider 层（图片生成走独立工具，不走 chat completions）
- 不新增附件上传能力（现有机制已满足图生图的参考图传递）
- 不做图片编辑器 UI（生成结果在 tool-card 内渲染和预览）

## 2. 架构总览

```
packages/backend/src/modules/netaclaw/
├── image_providers/                    # 新增：图片生成 Provider 层
│   ├── types.ts                        # 统一接口、凭证、错误类型、工厂函数
│   ├── ark.ts                          # 火山引擎（OpenAI SDK images.generate）
│   └── minimax.ts                      # MiniMax（自有 REST API）
├── tools/
│   ├── common.ts                       # 修改：新增 images 类型
│   ├── catalog.ts                      # 修改：注册两个新工具
│   └── builtin/
│       ├── text_to_image.ts            # 新增：文生图工具
│       └── image_to_image.ts           # 新增：图生图工具
├── service/
│   ├── tool_resolver.ts                # 修改：注入凭证和 provider
│   └── image_storage.ts               # 新增：图片转存本地服务
└── runtime/
    └── prompt_builder.ts               # 修改：附件提示语扩展

packages/frontend/src/modules/agent/
├── tools/renderer-registry.ts          # 修改：rawResult 新增 images 类型
└── components/tool-card.vue            # 修改：单图/多图渲染
```

## 3. Provider 层

### 3.1 统一接口

```typescript
// image_providers/types.ts

export interface ImageProviderCredentials {
  baseUrl: string;
  apiKey: string;
  supplier: string;
  modelId: string;
  promptHint: string | null;
  extra?: ToolGovernanceExtra | null;
}

export interface TextToImageParams {
  prompt: string;
  width?: number;
  height?: number;
  aspectRatio?: string;
  n?: number;
  responseFormat?: 'url' | 'base64';
  watermark?: boolean;
  seed?: number;
  extra?: Record<string, unknown>;
}

export interface ImageToImageParams extends TextToImageParams {
  referenceImage: string;
  strength?: number;
}

export interface ImageGenerationResult {
  images: { url?: string; base64?: string; width?: number; height?: number }[];
  model: string;
  provider: string;
}

export interface ImageGenerationProvider {
  readonly id: string;
  textToImage(params: TextToImageParams, creds: ImageProviderCredentials): Promise<ImageGenerationResult>;
  imageToImage(params: ImageToImageParams, creds: ImageProviderCredentials): Promise<ImageGenerationResult>;
}

export class ImageGenerationError extends Error {
  constructor(
    message: string,
    public readonly code: 'content_safety' | 'rate_limit' | 'insufficient_balance' |
                          'invalid_params' | 'timeout' | 'network' | 'unknown',
    public readonly retryable: boolean,
  ) {
    super(message);
  }
}
```

### 3.2 Provider 路由

supplier + baseUrl 联合路由，解决火山引擎 supplier 可能配为 "OpenAI" 的问题：

```typescript
export function getImageProvider(supplier: string, baseUrl: string): ImageGenerationProvider | null {
  const s = supplier.toLowerCase();
  if (s === 'minimax') return providers.get('minimax')!;
  if (s === 'ark' || s === 'volcengine') return providers.get('ark')!;
  if (s === 'openai') {
    if (baseUrl.includes('volces.com') || baseUrl.includes('volcengine')) return providers.get('ark')!;
    if (baseUrl.includes('minimax')) return providers.get('minimax')!;
  }
  return null;
}
```

### 3.3 火山引擎实现 (ark.ts)

- 使用 OpenAI SDK `client.images.generate()`
- 尺寸通过 `size` 字段传递（如 `"1024x1024"`、`"2K"`）
- 图生图通过 `extra_body.image` 传参考图
- 特有参数（watermark 等）通过 `extra_body` 透传
- 60 秒超时保护

### 3.4 MiniMax 实现 (minimax.ts)

- 使用自有 REST API `POST {baseUrl}/v1/image_generation`
- 尺寸通过 `aspect_ratio` 或 `width + height` 传递
- 图生图通过 `subject_reference` 字段传参考图
- 特有参数：`style`（画风）、`prompt_optimizer`（prompt 优化）通过 `extra` 透传
- 响应从 `data.image_urls[]` 归一化到统一结构
- Bearer token 认证
- 60 秒超时保护

### 3.5 扩展新渠道

新增 Provider 只需：
1. 在 `image_providers/` 下新建文件，实现 `ImageGenerationProvider` 接口
2. 在工厂函数 `getImageProvider` 中注册路由规则

## 4. 工具层

### 4.1 text_to_image

```typescript
// tools/builtin/text_to_image.ts

const Params = Type.Object({
  prompt: Type.String({ description: '图片描述，尽量详细具体' }),
  aspectRatio: Type.Optional(Type.String({
    description: '宽高比。可选: 1:1, 16:9, 4:3, 3:2, 2:3, 3:4, 9:16',
  })),
  width: Type.Optional(Type.Integer({ description: '精确宽度（像素），优先级低于 aspectRatio' })),
  height: Type.Optional(Type.Integer({ description: '精确高度（像素）' })),
  n: Type.Optional(Type.Integer({ description: '生成数量，默认 1，最大 9', minimum: 1, maximum: 9 })),
  watermark: Type.Optional(Type.Boolean({ description: '是否添加水印' })),
  seed: Type.Optional(Type.Integer({ description: '随机种子，相同 seed 可复现相近结果' })),
  extra: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
    description: 'Provider 特有参数，如 MiniMax 的 style、prompt_optimizer',
  })),
});

registerSchema({
  name: 'text_to_image',
  toolset: 'vision',
  description: '根据文字描述生成图片，支持指定尺寸、数量、风格等参数。',
  capability: 'multimodal',
  visibility: 'tool',
  isCore: false,
  canDisable: true,
  supportsPromptHint: true,
  requiresModel: true,
});
```

工具描述会拼接 `promptHint`，管理员可在后台配置行业特定的默认提示词。

### 4.2 image_to_image

```typescript
// tools/builtin/image_to_image.ts

const Params = Type.Object({
  prompt: Type.String({ description: '对参考图的修改描述' }),
  referenceImage: Type.String({ description: '参考图片 URL（从用户上传附件获取）' }),
  strength: Type.Optional(Type.Number({
    description: '参考图影响强度 0-1，越大越接近原图', minimum: 0, maximum: 1,
  })),
  aspectRatio: Type.Optional(Type.String({ description: '宽高比' })),
  width: Type.Optional(Type.Integer({ description: '精确宽度（像素）' })),
  height: Type.Optional(Type.Integer({ description: '精确高度（像素）' })),
  n: Type.Optional(Type.Integer({ description: '生成数量，默认 1，最大 9', minimum: 1, maximum: 9 })),
  watermark: Type.Optional(Type.Boolean({ description: '是否添加水印' })),
  seed: Type.Optional(Type.Integer({ description: '随机种子' })),
  extra: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
    description: 'Provider 特有参数',
  })),
});

registerSchema({
  name: 'image_to_image',
  toolset: 'vision',
  description: '基于参考图片生成新图片，支持风格迁移、内容编辑等。传入参考图URL和修改描述。',
  capability: 'multimodal',
  visibility: 'tool',
  isCore: false,
  canDisable: true,
  supportsPromptHint: true,
  requiresModel: true,
});
```

### 4.3 tool_resolver 集成

两个工具在 tool_resolver 中的注入逻辑与 `image_recognize` 对称：

1. 检查 `filteredNames` 是否包含工具名
2. 从 `toolRegistry.getToolModelConfig()` 获取模型渠道配置
3. 从 `modelChannelService.resolveForAgent()` 解析凭证
4. 通过 `getImageProvider(supplier, baseUrl)` 获取 provider 实例
5. 创建工具实例，注入 creds 和 provider
6. 未配置时写入 `disabledReasons`

两个工具可以绑定不同的模型渠道（如文生图用火山引擎，图生图用 MiniMax）。

## 5. 多图返回

### 5.1 后端类型扩展

```typescript
// tools/common.ts

export interface ImageItem {
  url: string;
  mimeType?: string;
  width?: number;
  height?: number;
  seed?: number;
}

export type ToolResultContent =
  | { type: 'text'; text: string }
  | { type: 'json'; data: unknown }
  | { type: 'image'; url: string; mimeType?: string; text?: string;
      width?: number; height?: number; bytes?: number;
      originalWidth?: number; originalHeight?: number;
      originalBytes?: number; resized?: boolean; }
  | { type: 'images'; images: ImageItem[]; text?: string };

export function imagesResult(images: ImageItem[], text?: string): ToolResultContent {
  return { type: 'images', images, text };
}
```

### 5.2 toolResultToText 扩展

```typescript
if (value.type === 'images') {
  const lines = value.images.map((img, i) =>
    `[图${i + 1}] ${img.url}${img.width && img.height ? ` (${img.width}x${img.height})` : ''}`
  );
  const header = value.text || `已生成 ${value.images.length} 张图片`;
  return `${header}\n${lines.join('\n')}`;
}
```

### 5.3 返回策略

- 单图（n=1）：返回 `type: 'image'`，向后兼容
- 多图（n>1）：返回 `type: 'images'`，前端网格渲染

## 6. 图片转存

### 6.1 问题

MiniMax URL 有效期 24 小时，火山引擎类似。历史会话中图片会失效。

### 6.2 方案

新增 `service/image_storage.ts`，工具 execute 完成后立即将临时 URL 下载并转存到本地存储：

```typescript
export class ImageStorageService {
  async persist(tempUrl: string, metadata?: { toolName: string }): Promise<string> {
    const response = await fetch(tempUrl);
    const buffer = Buffer.from(await response.arrayBuffer());
    const ext = this.detectExtension(response.headers.get('content-type'));
    const filename = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
    // 复用现有文件存储基础设施写入本地
    return await this.fileService.saveBuffer(buffer, filename);
  }
}
```

工具内部在 provider 返回后、构造 ToolResultContent 前执行转存，session tree 中存储的是永久本地 URL。

## 7. 前端渲染

### 7.1 renderer-registry 扩展

`ToolRenderSource.rawResult` 新增 `images` 类型：

```typescript
rawResult?: {
  type: 'text' | 'json' | 'image' | 'images';
  // 现有字段...
  images?: { url: string; mimeType?: string; width?: number; height?: number; seed?: number }[];
};
```

### 7.2 message-item.vue 渲染

当前 `message-item.vue` 已有 `rawResult.type === 'image'` 的单图渲染（`<img>` 标签）。需要：

- 将单图渲染升级为 `el-image`，支持点击预览大图
- 新增 `rawResult.type === 'images'` 分支，CSS Grid 网格布局，`el-image` 预览列表联动
- 其他类型保持原有 `<pre>` 文本渲染

多图网格样式：`grid-template-columns: repeat(auto-fill, minmax(140px, 1fr))`，自适应列数。

## 8. Prompt Builder 扩展

### 8.1 改动

`buildLLMMessages` 新增 `toolNames` 参数，附件提示语根据 Agent 实际可用工具动态生成：

- 有 `image_recognize` → 提示"如需分析图片内容，请使用 image_recognize 工具"
- 有 `image_to_image` → 提示"如需基于图片生成新图片，请使用 image_to_image 工具，将图片 URL 作为 referenceImage 参数"
- 两者都有 → 两条提示都输出

避免提到 Agent 没有的工具。

## 9. 错误处理

Provider 层统一抛出 `ImageGenerationError`，包含错误码和是否可重试标记：

| 错误码 | 含义 | 可重试 | Agent 可见信息 |
|--------|------|--------|--------------|
| content_safety | 内容安全拦截 | 否 | "提示词触发内容安全策略，请调整描述" |
| rate_limit | 限流 | 是 | "当前请求过多，请稍后重试" |
| insufficient_balance | 余额不足 | 否 | "模型渠道余额不足" |
| invalid_params | 参数错误 | 否 | 具体参数错误信息 |
| timeout | 超时（60s） | 是 | "生成超时，可尝试降低图片尺寸" |
| network | 网络错误 | 是 | "网络连接失败" |

工具层捕获后返回结构化错误文本，Agent 可据此决策（换 prompt 重试或告知用户）。

## 10. 工具参数控制权

### 10.1 问题

图片生成工具的参数（n、aspectRatio、watermark、width/height 等）存在三个来源：

| 来源 | 设置者 | 例子 |
|------|--------|------|
| 工具管理页 `extra` | 管理员 | 默认 n=1、watermark=true |
| Agent 系统 prompt | 管理员 | "电商主图用 1:1" |
| Agent 运行时 tool_use | Agent（响应用户） | `{ n: 4, aspectRatio: "16:9" }` |

如果管理员在工具管理页设置了 n=1，但用户说"生成 4 张"，Agent 传了 n=4，应该听谁的？

### 10.2 设计原则：Agent 决策优先，管理员设默认值

参数分为两类：

**默认值参数（Agent 可覆盖）**：`n`、`aspectRatio`、`width`、`height`、`watermark`、`seed`、`responseFormat`

管理员在工具管理页的 `extra` 字段中配置默认值。Agent 不传时用默认值，Agent 传了则以 Agent 为准。这些参数在工具编辑页可查看可编辑，标注"默认值，Agent 可覆盖"。

**约束参数（Agent 不可突破）**：`maxN`、`maxWidth`、`maxHeight`

管理员设置硬上限，工具 execute 内部做 clamp。防止 Agent 被用户诱导生成过多或过大图片导致成本失控。这些参数在工具编辑页可查看可编辑，标注"硬上限"。

### 10.3 extra 字段结构

扩展 `netaclaw_tool.extra`，图片工具新增：

```typescript
// ToolGovernanceExtra 扩展
export type ToolGovernanceExtra = {
  allowInSubagent?: boolean;
  workerRoutingStrategy?: ToolWorkerRoutingStrategy;
  // 图片工具新增
  imageDefaults?: {
    n?: number;
    aspectRatio?: string;
    width?: number;
    height?: number;
    watermark?: boolean;
    responseFormat?: 'url' | 'base64';
  };
  imageConstraints?: {
    maxN?: number;          // 默认 9
    maxWidth?: number;      // 默认 2048
    maxHeight?: number;     // 默认 2048
  };
};
```

### 10.4 工具 execute 内部合并逻辑

```typescript
async execute(_id, params) {
  const defaults = creds.extra?.imageDefaults ?? {};
  const constraints = creds.extra?.imageConstraints ?? {};

  const merged = {
    prompt: params.prompt,
    n: Math.min(params.n ?? defaults.n ?? 1, constraints.maxN ?? 9),
    aspectRatio: params.aspectRatio ?? defaults.aspectRatio,
    width: clamp(params.width ?? defaults.width, constraints.maxWidth ?? 2048),
    height: clamp(params.height ?? defaults.height, constraints.maxHeight ?? 2048),
    watermark: params.watermark ?? defaults.watermark ?? false,
    // ...
  };

  const result = await provider.textToImage(merged, creds);
  // ...
}
```

### 10.5 前端工具编辑页

工具编辑抽屉中，当工具名为 `text_to_image` 或 `image_to_image` 时，在模型配置区域下方新增"图片生成配置"区块：

```
┌─ 图片生成配置 ──────────────────────────────┐
│                                              │
│  默认值（Agent 可覆盖）                        │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│  │ 数量: 1  │ │ 比例: 1:1│ │ 水印: ☑     │ │
│  └──────────┘ └──────────┘ └──────────────┘ │
│                                              │
│  硬上限                                       │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│  │最大数量:9│ │最大宽:2048│ │最大高:2048   │ │
│  └──────────┘ └──────────┘ └──────────────┘ │
│                                              │
│  ⓘ 默认值在 Agent 未指定时生效，Agent 可根据   │
│    用户指令覆盖。硬上限不可突破。               │
└──────────────────────────────────────────────┘
```

## 11. 专用电商套图 Agent

通过管理后台 Agent 编辑页配置：

- **系统 Prompt**：电商视觉设计领域知识（平台尺寸规范、构图原则、套图工作流）
- **工具集**：`text_to_image` + `image_to_image` + `image_recognize` + `todo`
- **maxToolRounds**：30（套图需要多轮工具调用）
- **promptHint**：各工具在后台配置行业特定的默认提示词
- **imageDefaults**：工具管理页配置电商常用默认值（如 aspectRatio: "1:1"、watermark: false）
- **imageConstraints**：设置 maxN 防止成本失控

典型流程：用户描述产品 → Agent 规划套图方案 → 逐张生成 → 如有参考图则用图生图 → 汇总输出。

## 12. 完整改动清单

| 层 | 文件 | 改动类型 |
|----|------|---------|
| Provider | `image_providers/types.ts` | 新增 |
| Provider | `image_providers/ark.ts` | 新增 |
| Provider | `image_providers/minimax.ts` | 新增 |
| 工具 | `tools/common.ts` | 修改 |
| 工具 | `tools/builtin/text_to_image.ts` | 新增 |
| 工具 | `tools/builtin/image_to_image.ts` | 新增 |
| 注册 | `tools/catalog.ts` | 修改 |
| 治理 | `tools/manifest.ts` | 修改（ToolGovernanceExtra 扩展） |
| 解析 | `service/tool_resolver.ts` | 修改 |
| 存储 | `service/image_storage.ts` | 新增 |
| Prompt | `runtime/prompt_builder.ts` | 修改 |
| 前端 | `tools/renderer-registry.ts` | 修改 |
| 前端 | `components/message-item.vue` | 修改（图片渲染升级 + 多图支持） |
| 前端 | `views/tools.vue` | 修改（图片生成配置区块） |
