# 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 ``` 打开 即可进入控制台。 首次使用:进入「供应商」页填写上游的 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、虚拟密钥和本地模拟上游,不读取现有数据库或调用付费生图服务。 浏览器测试会启动临时本地服务,结束后关闭服务并清除临时数据。