Files
ai-image/README.md
T

297 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ai-image
验收记录与当前限制见 [docs/acceptance.md](docs/acceptance.md)。
AI 绘图聚合服务。对外提供统一的多供应商生图能力(文生图 / 图生图),内置 Web 控制台,Agent 可通过 REST API 或 MCP 调用。
---
## 一、快速开始
```bash
pip install -r requirements.txt
# 启动(Web + REST + MCP 同端口)
python main.py
# 仅 MCP stdio(给 MCP 客户端当子进程用)
python main.py --stdio
```
打开 <http://127.0.0.1:8765/> 即可进入控制台。
首次使用:进入「供应商」页填写上游的 Base URL 与 API Key → 「拉取模型」→ 勾选含 `image` 的模型保存 → 到「出图」页输入提示词出图。
---
## 二、当前状态(v2 · 已落地)
### 技术栈
| 层 | 选型 |
|---|---|
| Web 框架 | FastAPI |
| 前端 | 原生 HTML/CSS/JS 静态页(`web/`),与后端同端口同源 |
| 持久化 | SQLite(标准库 `sqlite3`) |
| HTTP 客户端 | httpx |
| 密钥加密 | cryptography / Fernet |
| MCP | fastmcp(stdio + streamable HTTP) |
| 部署 | Docker / docker-compose |
### 目录结构
```
main.py 入口:REST + Web + MCP
app/
config.py 路径与运行配置
db.py SQLite 连接与建表
security.py 密钥加解密与掩码
schemas.py Pydantic 请求模型
providers/
base.py 适配器基类与能力声明
openai_compat.py OpenAI 兼容网关
agnes.py Agnes 原生协议
__init__.py 适配器注册表
services/
providers.py 供应商 / 模型 CRUD 与模型发现
generation.py 出图、落盘、记录
gallery.py output/ 扫描
skill_doc.py SKILL.md 读取
routers/
api_providers.py 供应商与模型 API
api_models.py
api_generate.py
api_gallery.py
api_skills.py
mcp_server.py MCP 工具
web/ 前端静态页
data/ SQLite + 密钥文件(不入库)
output/ 图片输出(画廊数据源)
SKILL.md Agent 使用说明
```
### Web 控制台
| 页面 | 文件 | 功能 |
|---|---|---|
| 出图 | `web/create.html` | 输入提示词、选供应商与模型、选尺寸与数量,直接出图 |
| 画廊 | `web/index.html` | 按画廊(profile)浏览、搜索、进入查看、删除 |
| 生成记录 | `web/records.html` | 查看历史生成任务与失败原因 |
| 供应商 | `web/providers.html` | 增删改供应商、拉取上游模型、勾选启用 |
| 模型 | `web/models.html` | 跨供应商查看模型、启用 / 停用 |
| SKILL.md | `web/skills.html` | 在线查看 Agent 使用说明 |
前端只有一层数据出入口 `web/api.js`,全部走真实 REST;无 mock 数据。
---
## 三、核心机制
### 统一参数体系
所有供应商收敛到同一套入参,内部再翻译成各上游格式:
| 入参 | 类型 | 说明 |
|---|---|---|
| `provider_id` | str | 供应商 ID,界面创建时自定义 |
| `model` | str | 模型 ID,来自上游模型列表 |
| `prompt` | str | 提示词 |
| `size` | str | 统一 `WxH` 像素,如 `1024x1024`;也接受 `1K`/`2K`/`3K`/`4K` |
| `ratio` | str | 仅 Agnes 支持,如 `16:9`;省略时从 `size` 推导 |
| `n` | int | 生成数量(1~4) |
| `images` | list[str] | 图生图参考图(URL 或 Data URI),仅部分供应商支持 |
| `profile_name` | str | 调用方标识,用于输出目录隔离 |
适配器声明自身能力(`supports_image_input`、`size_hint`、`kind`),服务端据此校验,避免把不支持的参数透传给上游。
### 数据模型(SQLite)
**providers**
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | TEXT PK | 用户自定义唯一标识 |
| `name` | TEXT | 显示名 |
| `adapter` | TEXT | `openai_compat` / `agnes` |
| `base_url` | TEXT | 上游地址 |
| `api_key` | TEXT | 上游密钥(Fernet 加密存储,接口返回掩码) |
| `default_model` | TEXT | 默认模型 |
| `enabled` | INTEGER | 是否启用 |
| `note` | TEXT | 备注 |
| `created_at` / `updated_at` | TEXT | 时间戳 |
**models**
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | TEXT PK | `provider_id:model_id` |
| `provider_id` | TEXT | 外键(ON DELETE CASCADE) |
| `model_id` | TEXT | 上游模型标识 |
| `display_name` | TEXT | 显示名 |
| `enabled` | INTEGER | 是否对外暴露 |
| `kind` | TEXT | 类型(`image`) |
| `size_hint` | TEXT | 尺寸提示 |
| `supports_image_input` | INTEGER | 是否支持参考图 |
| `note` | TEXT | 备注 |
| `discovered_at` | TEXT | 最近一次发现时间 |
**generations**
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | TEXT PK | 任务 ID |
| `provider_id` / `provider_name` | TEXT | 实际使用的供应商 |
| `model` | TEXT | 模型 |
| `profile_name` | TEXT | 调用方 |
| `prompt` | TEXT | 提示词 |
| `size` / `ratio` / `n` | — | 入参快照 |
| `status` | TEXT | `success` / `failed` |
| `error` | TEXT | 失败原因 |
| `files` | TEXT(JSON) | 落盘文件名列表 |
| `duration_ms` | INTEGER | 耗时 |
| `created_at` | TEXT | 时间戳 |
### 模型发现
用户在界面填完 API Key 与 Base URL 后点击「拉取模型」,服务端请求上游 `GET {base_url}/models`,筛选规则:**模型 ID 转小写后包含 `image`**。命中的模型写入 `models` 表并标记 `discovered_at`,用户可勾选哪些对外启用(未勾选的自动停用)。拉取失败不阻塞保存,仅提示错误。
两种适配器的发现与出图路径:
| 适配器 | 模型发现 | 出图 |
|---|---|---|
| `openai_compat` | `GET /models` | `POST /images/generations`(`data[0].b64_json` 或 `data[0].url`) |
| `agnes` | `GET /models` | `POST /images/generations`(`size` 档位 + `ratio`,`response_format` 放在 `extra_body` 内) |
### 画廊
`output/` 目录即画廊数据源:
```
output/
├── slime-fellowship/ ← 一个画廊(profile_name)
│ ├── gpt-image-2-1K-0014ef2b.png
│ └── ...
├── smoke-gpt-1/
└── user/ ← 网页端手动出图(固定 profile_name = user)
```
目录名语义 = **谁生成的图片**:Agent 调用时由调用方指定 `profile_name`(默认 `agent`),网页端手动出图固定 `user`;目录不存在时后端自动创建。
| 接口 | 说明 |
|---|---|
| `GET /api/galleries` | 画廊列表:名称、图片数、占用空间、更新时间、封面、涉及的模型 |
| `GET /api/galleries/{profile}` | 某个画廊内的图片(含模型、供应商、时间) |
| `DELETE /api/galleries/{profile}/{filename}` | 删除单张(物理删除 + 同步 `generations` 记录) |
### MCP 对接
Agent 侧可用工具:
| 工具 | 说明 | 顺序 |
|---|---|---|
| `list_providers` | 列出可用供应商及其启用模型数 | 1 |
| `discover_models` | 拉取指定供应商的上游模型,返回 `{ provider_id, total, matched }` | 2(必做) |
| `generate_image` | 出图(文生图 / 图生图) | 3 |
| `list_generations` | 查询生成记录 | 可选 |
| `get_skill_doc` | 获取最新版 SKILL.md 内容 | 任意时刻 |
MCP 只暴露用户在界面中启用的供应商,不泄露未启用配置与密钥。
**Agent 强制流程**:`list_providers()` → `discover_models(provider_id)` → 用返回的 `matched` 出图。
`matched` 为空时,Agent **必须直接结束并回复「没找到有效的模型」**,不得猜测模型名、不得调用出图接口。
### SKILL.md
`GET /api/skills` 返回 SKILL.md 内容(JSON)或原文(`/api/skills/raw`,`text/markdown`)。页面提供在线查看,Agent 通过 MCP 工具 `get_skill_doc` 拉取。
SKILL.md 中**不写任何固定模型名称**:只描述「先拉模型、再出图」的流程与参数说明,模型清单一律来自 `discover_models` 的实时返回。
---
## 四、接口清单
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/health` | 健康检查 |
| GET | `/api/providers` | 供应商列表(密钥掩码) |
| POST | `/api/providers` | 新增 / 更新供应商(`api_key` 留空表示不修改) |
| DELETE | `/api/providers/{id}` | 删除供应商及其模型 |
| POST | `/api/providers/discover` | 手填 Base URL + Key 临时拉模型 |
| POST | `/api/providers/{id}/discover` | 按已保存供应商拉模型并写库 |
| POST | `/api/providers/{id}/models` | 保存勾选的模型,未勾选的自动停用 |
| GET | `/api/models` | 模型列表(可 `?provider_id=`) |
| PATCH | `/api/models/{id}` | 启用 / 停用单个模型 |
| POST | `/api/models/bulk` | 批量启用 / 停用 |
| POST | `/api/generate` | 出图 |
| GET | `/api/generations` | 生成记录 |
| GET | `/api/galleries` | 画廊列表 |
| GET | `/api/galleries/{profile}` | 画廊内图片 |
| DELETE | `/api/galleries/{profile}/{filename}` | 删除图片 |
| GET | `/api/skills` | SKILL.md(JSON) |
| GET | `/api/skills/raw` | SKILL.md 原文(markdown) |
| GET | `/output/{profile}/{filename}` | 图片访问 |
| POST | `/mcp` | MCP streamable HTTP |
---
## 五、配置
`.env`(参考 `.env.example`):
| 变量 | 默认值 | 说明 |
|---|---|---|
| `AI_IMAGE_HOST` | `127.0.0.1` | 监听地址 |
| `AI_IMAGE_PORT` | `8765` | 监听端口 |
| `AI_IMAGE_DATA_DIR` | `data` | SQLite 与密钥文件目录 |
| `AI_IMAGE_OUTPUT_DIR` | `output` | 图片输出根目录 |
| `AI_IMAGE_SECRET_KEY` | 自动生成 | 密钥加密主密钥,不填则落 `data/secret.key` |
**供应商的 API Key 与 Base URL 不再走环境变量**,在控制台填写,服务端加密后存 SQLite。
---
## 六、部署
```bash
docker compose up -d
```
宿主机 `output/` 与 `data/` 挂载进容器,端口映射 `8765:8000`。
---
## 七、MCP 客户端接入
`config.json` 示例:
```json
{
"mcpServers": {
"ai-image": {
"command": "python",
"args": ["d:\\source\\MyProject\\ai-image\\main.py", "--stdio"],
"cwd": "d:\\source\\MyProject\\ai-image",
"env": {}
}
}
}
```
也可用 streamable HTTP 直连:`http://127.0.0.1:8765/mcp`。
## 八、测试
```bash
# 后端回归:标准库 unittest,无需额外测试框架
python -m unittest discover -s tests -p test_app.py -v
# 完整验收:包含 Chromium 浏览器交互、桌面/移动布局、MCP HTTP/stdio
pip install -r requirements-dev.txt
python -m playwright install chromium
python -m unittest discover -s tests -v
```
测试使用临时目录、独立 SQLite、虚拟密钥和本地模拟上游,不读取现有数据库或调用付费生图服务。
浏览器测试会启动临时本地服务,结束后关闭服务并清除临时数据。