# 多模态图片识别工具 & 工具模型分类 & 对话附件上传 设计

> 日期：2026-04-26
> 状态：待实施

## 概述

在工具管理系统中引入三个能力：
1. **工具模型分类**：区分"需要大模型配置"和"不需要大模型配置"的工具，支持工具级别的模型渠道绑定
2. **图片识别工具**：新增 `image_recognize` 多模态工具，支持 URL 和 base64 两种图片输入，通过配置的多模态模型进行图片分析
3. **对话附件上传**：Agent 对话页面支持上传图片/视频/PDF 等附件，支持多文件、首尾帧，为后续文生图/图生图/视频生成预留扩展

## 一、数据层变更

### netaclaw_tool 表新增字段

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `requiresModel` | int | 0 | 是否需要大模型配置（0否 1是） |
| `modelChannelId` | int | null | 关联模型渠道ID（指向 netaclaw_model_channel.id） |
| `modelId` | varchar(100) | null | 关联模型ID（渠道内具体模型名） |

在 `NetaClawToolEntity` 中新增对应字段。

### Catalog Schema 扩展

`registerSchema()` 的 schema 对象新增 `requiresModel?: boolean` 字段，工具注册时声明是否需要模型。
`syncCatalogToDb()` 同步时将此字段写入 DB。

## 二、图片识别工具实现

### 文件位置
`packages/backend/src/modules/netaclaw/tools/builtin/image_recognize.ts`

### 工具元信息
- name: `image_recognize`
- label: `图片识别`
- toolset: `vision`
- capability: `multimodal`
- requiresModel: `true`
- visibility: `tool`
- isCore: false
- canDisable: true

### 依赖注入模式

遵循项目现有的**工厂函数 + 闭包**模式。现有工厂函数接收的是**运行时数据**（provider 实例、凭证），不是 service 对象。image_recognize 也应如此：

```typescript
// image_recognize.ts — 工厂函数接收已解析的运行时凭证
export function createImageRecognizeTool(credentials: {
  baseUrl: string; apiKey: string; supplier: string;
  modelId: string; promptHint: string | null;
}): AnyAgentTool {
  return {
    name: 'image_recognize',
    async execute(id, params) {
      // 通过闭包直接访问已解析的凭证，无需再查 DB
      const provider = getProvider(supplierToProvider[credentials.supplier]);
      // ... 调用 provider.chat()
    },
  };
}
```

在 `tool_resolver.ts` 的 `resolve()` 方法中，先查询 DB 获取凭证，再传给工厂函数：

```typescript
// tool_resolver.ts resolve() 方法中
if (filteredNames.includes('image_recognize')) {
  const toolConfig = await this.toolRegistry.getToolModelConfig('image_recognize');
  if (toolConfig) {
    const channel = await this.modelChannelService.resolveForAgent(toolConfig.modelChannelId);
    if (channel) {
      runtimeTools.push(createImageRecognizeTool({
        ...channel, modelId: toolConfig.modelId, promptHint: toolConfig.promptHint,
      }));
    }
  }
  // 若未配置或渠道不可用，工具不进入 runtimeTools（LLM 不会看到该工具）
}
```

**关键决策：模型未配置时在 resolve 阶段排除工具**，避免 LLM 尝试调用必然失败的工具。

### 参数 Schema (TypeBox)

```typescript
{
  image: Type.String({ description: '图片URL或base64编码字符串' }),
  prompt: Type.Optional(Type.String({ description: '分析提示词，不传则使用工具默认提示词' }))
}
```

### 默认系统提示词

工具内置一个高质量默认提示词，存储在 `promptHint` 字段中，前端可编辑覆写：

```
你是一个专业的图像分析助手。请按以下步骤分析图片：

1. **图像分类**：首先识别图片类型（如：身份证、驾驶证、行驶证、营业执照、发票、商品图片、截图、照片、表格、图表、手写文字、印刷文字等）。

2. **结构化提取**：根据图片类型，提取关键信息：
   - 证件类：提取所有字段（姓名、证件号、有效期、地址等）
   - 票据类：提取金额、日期、项目明细等
   - 商品类：提取品名、规格、价格、品牌等
   - 表格/图表类：提取数据结构和关键数值
   - 其他类：详细描述画面内容

3. **详细描述**：对图片内容进行全面、详细的文字描述，不遗漏任何可见信息。包括文字内容、图形元素、颜色、布局等。

4. **质量评估**：简要说明图片清晰度、是否有遮挡或模糊区域。

请以结构化格式输出分析结果。
```

### 执行流程

1. 工厂函数通过闭包直接访问已解析的凭证（baseUrl、apiKey、supplier、modelId、promptHint）
2. 判断 `image` 参数：URL（http/https 开头）或 base64
3. 读取闭包中的 `promptHint` 作为系统提示词，若用户传了 `prompt` 参数则追加
4. 通过项目现有的 LLM provider 层调用模型（`getProvider(supplierToProvider[supplier])` → `provider.chat()`），不直接用 fetch
5. 构造 OpenAI 兼容的 multimodal messages（火山引擎 volcengine supplier 映射到 openai provider）：
   ```json
   {
     "model": "doubao-seed-2-0-pro-260215",
     "messages": [{
       "role": "user",
       "content": [
         { "type": "text", "text": "<promptHint + prompt>" },
         { "type": "image_url", "image_url": { "url": "<url或data:image/...;base64,...>" } }
       ]
     }]
   }
   ```
6. 返回 `textResult(response.choices[0].message.content)`

## 三、后端接口变更

### tool controller 变更

- `page` 接口 `pageQueryOp` 新增 `requiresModel` 到 `fieldEq`
- `update` 接口自动支持（CoolController 的 update 会更新所有传入字段）

### tool_registry service 变更

- `syncCatalogToDb()` 同步时处理 `requiresModel` 字段
- `page()` 方法支持 `requiresModel` 过滤
- 新增 `getToolModelConfig(toolName)` 方法：
  ```typescript
  async getToolModelConfig(toolName: string): Promise<{ modelChannelId: number; modelId: string } | null> {
    const tool = await this.toolEntity.findOneBy({ name: toolName });
    if (!tool?.modelChannelId || !tool?.modelId) return null;
    return { modelChannelId: tool.modelChannelId, modelId: tool.modelId };
  }
  ```

### tool_resolver.ts 变更

在 `resolve()` 方法中，为 `image_recognize` 工具创建实例时注入依赖（与 memory/delegate 工具同模式）。

## 四、前端工具管理页改造

### 筛选栏

新增"模型依赖"下拉筛选：全部 / 需要模型 / 不需要模型

### 表格列

新增"模型配置"列：
- `requiresModel === 0`：显示 `-`
- `requiresModel === 1` 且已配置：显示模型名称（如 `doubao-seed-2-0-pro-260215`）
- `requiresModel === 1` 且未配置：显示橙色 `el-tag type="warning"` "未配置"

### 编辑抽屉

当 `requiresModel === 1` 时，在编辑抽屉中显示"模型配置"区域（`el-divider` 分隔）：
- 模型渠道下拉（调用 `model_channel` 的 `allModels` 接口，筛选含 multimodal 能力模型的渠道）
- 模型选择下拉（联动，显示所选渠道下 capability 为 multimodal 的模型）
- 默认提示词文本框（`el-input type="textarea" :rows="8"`，编辑 `promptHint` 字段）

## 五、Agent 对话附件上传

### 架构决策：主 Agent LLM 如何感知附件

主 Agent LLM 可能是纯文本模型（如 DeepSeek），无法直接"看到"图片。采用 **metadata + prompt_builder 注入方案**：

1. 用户消息 `content` 保持纯净，不污染原始文本
2. 附件信息存储在 message 的 `metadata.attachments` 中
3. 在 `prompt_builder` 层构造 LLM messages 时，检查 metadata.attachments，将附件信息作为**独立的 system/user message** 注入到 messages 数组中：

```
[系统注入的附件提示]
用户上传了以下附件：
- 图片: photo.jpg (URL: https://xxx/photo.jpg)
- PDF: report.pdf (URL: https://xxx/report.pdf)
如需分析图片内容，请使用 image_recognize 工具，传入图片URL。
```

这样避免了"后端注入 + 前端过滤"的脆弱双向耦合，content 字段始终是用户原始输入。

### 协议层扩展

`gateway/protocol.ts` 中 ClientChatMessage 新增 `attachments` 字段：

```typescript
interface ChatAttachment {
  id: string;
  type: 'image' | 'video' | 'pdf' | 'document' | 'other';
  url: string;
  name: string;
  size: number;
  mimeType: string;
  role?: 'start_frame' | 'end_frame';
}
```

### 消息存储

`netaclaw_message.metadata` JSON 中存储：
```json
{ "attachments": [...] }
```

### 前端组件化设计

将附件功能拆分为独立组件，不膨胀现有 ChatComposer：

```
components/chat/
├── ChatComposer.vue          (现有，slim 协调器，新增 attachments prop 传递)
├── ChatAttachmentButton.vue   (新增，附件按钮 + 文件选择器)
├── ChatAttachmentPreview.vue  (新增，附件预览区，缩略图/删除/首尾帧标记)
├── MessageAttachments.vue     (新增，消息气泡中的附件展示)
└── ...
```

#### ChatAttachmentButton.vue
- 回形针图标按钮，点击打开文件选择器
- 接受类型：图片(jpg/png/gif/webp/bmp)、视频(mp4/mov/avi)、PDF、文档(doc/docx/xls/xlsx)
- 支持多选
- emit: `@select(files: File[])`

#### ChatAttachmentPreview.vue
- 横向滚动的附件预览条，位于输入框上方
- 每个附件显示：缩略图（图片）/ 文件图标（其他）+ 文件名 + 大小 + 删除按钮
- 图片：显示缩略图（前端 `URL.createObjectURL` 或上传后的 URL）
- 视频：显示视频图标 + 文件名
- 右键菜单：标记为"首帧" / "尾帧"（设置 `role` 字段）
- 上传进度条（每个文件独立进度）
- props: `attachments: ChatAttachment[]`
- emit: `@remove(id)`, `@update-role(id, role)`

#### ChatComposer.vue 改造
- 在 textarea 上方条件渲染 `ChatAttachmentPreview`
- 在 textarea 左侧添加 `ChatAttachmentButton`
- 支持拖拽上传（`@dragover` + `@drop` 事件）
- 支持粘贴上传（`@paste` 事件，检测 `clipboardData.files`）
- 新增 `attachments` ref，发送时随消息一起 emit

#### MessageAttachments.vue
- 在 `message-item.vue` 中，当 `metadata.attachments` 存在时渲染
- 图片：网格布局缩略图，点击 `el-image-viewer` 放大
- 视频：视频缩略图 + 播放图标
- PDF/文档：文件图标 + 文件名 + 大小，点击新窗口打开

### 上传流程

1. 用户选择/拖拽/粘贴文件
2. 前端调用 `/admin/base/comm/upload` 上传到 Space（复用现有上传基础设施）
3. 上传成功后获得文件 URL，构造 `ChatAttachment` 对象
4. 添加到 `attachments` 数组，显示预览
5. 用户点击发送 → `attachments` 随消息通过 WebSocket 发送（只携带 URL，不携带 base64 数据）
6. 后端将附件信息存入 message metadata

### ConversationList 消息展示

在 `message-item.vue` 的用户消息气泡中：
- 检查 `metadata.attachments`，若存在则渲染 `MessageAttachments` 组件
- content 字段始终是用户原始输入，无需过滤

## 六、影响范围

### 新增文件
- `packages/backend/src/modules/netaclaw/tools/builtin/image_recognize.ts` — 图片识别工具
- `packages/frontend/src/modules/agent/components/chat/ChatAttachmentButton.vue` — 附件按钮
- `packages/frontend/src/modules/agent/components/chat/ChatAttachmentPreview.vue` — 附件预览
- `packages/frontend/src/modules/agent/components/chat/MessageAttachments.vue` — 消息附件展示

### 修改文件
- `packages/backend/src/modules/netaclaw/entity/tool.ts` — 新增 3 个字段
- `packages/backend/src/modules/netaclaw/tools/catalog.ts` — schema 类型扩展 + import 新工具
- `packages/backend/src/modules/netaclaw/service/tool_registry.ts` — 同步、查询、模型配置方法
- `packages/backend/src/modules/netaclaw/service/tool_resolver.ts` — 注入 image_recognize 工具依赖
- `packages/backend/src/modules/netaclaw/controller/admin/tool.ts` — 筛选参数
- `packages/frontend/src/modules/agent/views/tools.vue` — 筛选、表格列、编辑抽屉
- `packages/backend/src/modules/netaclaw/gateway/protocol.ts` — ChatAttachment 类型 + ClientChatMessage 扩展
- `packages/backend/src/modules/netaclaw/gateway/server.ts` — 附件消息处理（存 metadata，不改 content）
- `packages/backend/src/modules/netaclaw/service/prompt_builder.ts` — 附件信息注入到 LLM messages
- `packages/frontend/src/modules/agent/components/chat/ChatComposer.vue` — 集成附件按钮/预览/拖拽/粘贴
- `packages/frontend/src/modules/agent/components/message-item.vue` — 渲染 MessageAttachments
- `packages/frontend/src/modules/agent/types/index.d.ts` — ChatAttachment 类型 + WSClientMessage 扩展
- `packages/frontend/src/modules/agent/hooks/websocket.ts` — 消息发送携带 attachments

### 不需要修改
- model_channel 相关代码（复用现有 resolveForAgent + allModels 接口）
- tool_resolver.ts 的 capability 匹配逻辑（已存在，无需改动）
- Agent entity（工具模型配置在工具级别）

### 安全注意事项
- 后端文件上传接口需验证 MIME 白名单和文件大小限制
- API Key 不得出现在工具执行日志或前端返回的 tool_result 事件中
- 附件 URL 应有访问控制（签名 URL 或 token 验证）
