diff --git a/SKILL.md b/SKILL.md index c49b80c..bbfc36d 100644 --- a/SKILL.md +++ b/SKILL.md @@ -60,7 +60,7 @@ | `ratio` | str | 仅 Agnes 支持,如 `16:9`;省略时从 `size` 推导 | | `n` | int | 生成数量(1~4) | | `images` | list[str] | 图生图参考图(URL 或 Data URI),仅部分供应商支持 | -| `profile_name` | str | 调用方标识,决定落盘目录 | +| `profile_name` | str | 画廊标识;建议显式传调用 Agent 的名称,例如 `codex`,决定落盘目录 | ## 落盘规则 @@ -75,7 +75,7 @@ "size": "1024x1024", "n": 1, "images": ["https://example.com/reference.png"], - "profile_name": "agent" + "profile_name": "codex" } ``` @@ -87,7 +87,8 @@ output/ ``` - 网页端手动出图固定 `profile_name = user` → `output/user/` -- Agent 调用时由调用方指定 `profile_name`,默认 `agent` +- Agent 调用时建议显式将 `profile_name` 设为自己的名称(例如 `codex`),图片进入同名画廊。 +- 服务端不会自动识别 Agent 身份;省略 `profile_name` 时 MCP 使用通用默认值 `agent`,它不代表具体 Agent 的名字。 - 目录不存在时后端自动创建 - 允许字符:字母、数字、下划线、短横线(1~64 位) diff --git a/app/mcp_server.py b/app/mcp_server.py index ca1a95f..f190c8a 100644 --- a/app/mcp_server.py +++ b/app/mcp_server.py @@ -64,7 +64,11 @@ def generate_image( images: list[str] | None = None, profile_name: str = "agent", ) -> dict[str, Any]: - """第 3 步:出图。model 必须是 discover_models 返回的 matched 之一。""" + """第 3 步:出图。model 必须是 discover_models 返回的 matched 之一。 + + profile_name 是调用方的画廊标识,建议显式传入 Agent 名称(如 codex)。 + 服务端不会自动识别 Agent 身份;未传时使用通用画廊 agent。 + """ try: result = generation_service.generate( provider_id=provider, diff --git a/app/services/gallery.py b/app/services/gallery.py index c6efda6..69f997a 100644 --- a/app/services/gallery.py +++ b/app/services/gallery.py @@ -42,6 +42,7 @@ def _meta_by_file() -> dict[tuple[str, str], dict[str, Any]]: "model": row["model"], "provider_name": row["provider_name"], "created_at": row["created_at"], + "prompt": row["prompt"], }, ) return index @@ -108,6 +109,7 @@ def list_gallery(profile_name: str) -> list[dict[str, Any]]: "bytes": stat.st_size, "model": info.get("model", "—"), "provider_name": info.get("provider_name", "—"), + "prompt": info.get("prompt", ""), "created_at": info.get("created_at") or datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M"), } diff --git a/docs/acceptance.md b/docs/acceptance.md index d7d2e46..5dc707b 100644 --- a/docs/acceptance.md +++ b/docs/acceptance.md @@ -5,7 +5,7 @@ ## 结论与依据 六个页面均已实现,具备真实 REST 后端、SQLite 持久化、两种供应商适配器及 MCP 接口。 -本次修复后,隔离环境中的 26 项自动化测试通过,包括 23 项后端回归和 3 项浏览器/MCP 集成测试。 +本次修复后,隔离环境中的 27 项自动化测试通过,包括 24 项后端回归和 3 项浏览器/MCP 集成测试。 按项目说明验收,供应商配置、模型管理、文生图、文件落盘、画廊、生成记录、文档和 MCP 构成的核心使用流程已完整落地。图生图通过 REST/MCP 提供;Web 端目前定位为文生图控制台。 @@ -26,6 +26,7 @@ | 失败处理 | 上游错误、下载失败、无效 Base64、空结果、部分文件失败回滚 | 通过 | | 输入限制 | 数量、空提示词、尺寸、无效路径、停用配置、未知模型、参考图能力、异常上游 JSON | 通过 | | 默认模型 | 优先选择配置模型,不可用时回退至已启用模型 | 通过 | +| 预览提示词 | 读取生成记录中的提示词,保留换行、安全显示特殊字符,无记录的旧图片隐藏提示词 | 通过 | | 画廊与记录 | 浏览、详情、删除、同名文件跨画廊隔离、成功/失败筛选 | 通过 | | MCP | HTTP 与 stdio 初始化、工具枚举、文档调用 | 通过 | | 布局与脚本 | 1440px/390px 六页面无横向溢出,操作流程无未捕获脚本错误 | 通过 | diff --git a/tests/test_app.py b/tests/test_app.py index 745809b..473c056 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -73,6 +73,18 @@ class AppTests(unittest.TestCase): self.assertEqual(self.client.delete("/api/galleries/user/" + image["filename"]).status_code, 200) self.assertEqual(self.client.get(image["url"]).status_code, 404) + def test_gallery_prompt_and_legacy_image(self): + prompt = "第一行提示词\n" + with patch.object(ProviderAdapter, "_request", side_effect=self.upstream): + result = self.client.post("/api/generate", json={**self.payload, "prompt": prompt, + "profile_name": "prompt-preview"}) + result.raise_for_status() + filename = result.json()["images"][0]["filename"] + (config.OUTPUT_DIR / "prompt-preview" / "legacy.png").write_bytes(PNG) + items = {item["filename"]: item for item in self.client.get("/api/galleries/prompt-preview").json()} + self.assertEqual(items[filename]["prompt"], prompt) + self.assertEqual(items["legacy.png"]["prompt"], "") + def test_disabled_or_unknown_model_never_calls_upstream(self): for model, enabled, status in (("missing-image", True, 404), ("test-image", False, 400)): providers.set_model_enabled("test:test-image", enabled) diff --git a/tests/test_browser.py b/tests/test_browser.py index ef30911..2b28d79 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -1,6 +1,7 @@ """真实浏览器 + 本地模拟上游的端到端验收,不访问付费服务。""" import asyncio +import base64 import json import os import socket @@ -107,7 +108,8 @@ class BrowserTests(unittest.TestCase): page.goto(self.url + "/create.html") expect(page.locator("#model")).to_have_value("vendor/test-image") - page.locator("#prompt").fill("a test image") + prompt = "a test image\n" + page.locator("#prompt").fill(prompt) page.locator("#count").select_option("2") page.locator("#generateBtn").click() expect(page.locator("#resultStatus")).to_contain_text("生成成功 · 2 张") @@ -132,6 +134,9 @@ class BrowserTests(unittest.TestCase): page.locator(".folder-card").click() expect(page.locator("#galleryGrid img")).to_have_count(2) page.locator("#galleryGrid .gal-item").first.click() + expect(page.locator("#lbPrompt")).to_be_visible() + expect(page.locator("#lbPromptText")).to_have_text(prompt) + self.assertFalse(page.evaluate("window.promptInjected === true")) page.on("dialog", lambda dialog: dialog.accept()) page.locator("#lbDelete").click() expect(page.locator("#galleryGrid img")).to_have_count(1) @@ -141,7 +146,7 @@ class BrowserTests(unittest.TestCase): expect(page.locator("#mdView")).to_contain_text("调用流程") page.locator("#rawBtn").click() expect(page.locator("#rawView")).to_be_visible() - for width in (1440, 390): + for width in (2560, 1440, 390): page.set_viewport_size({"width": width, "height": 900}) for name in ("index", "create", "providers", "models", "records", "skills"): page.goto(self.url + f"/{name}.html") @@ -149,12 +154,25 @@ class BrowserTests(unittest.TestCase): page.wait_for_load_state("networkidle") overflow = page.evaluate("document.documentElement.scrollWidth > innerWidth") self.assertFalse(overflow, f"{name} 页面在 {width}px 出现横向溢出") + content = page.locator(".app-content").bounding_box() + body = page.locator(".page-body").bounding_box() + self.assertAlmostEqual(body["width"], content["width"], delta=1, + msg=f"{name} 页面在 {width}px 未铺满内容区域") page.goto(self.url + "/providers.html") page.locator('[data-act="del"]').click() page.locator("#confirmDeleteBtn").click() expect(page.locator("#providerList")).not_to_contain_text("浏览器测试供应商") self.assertEqual(errors, []) + legacy = Path(self.runtime.name) / "output" / "legacy-preview" + legacy.mkdir(exist_ok=True) + (legacy / "legacy.png").write_bytes(base64.b64decode(PNG)) + page.goto(self.url + "/index.html") + page.locator(".folder-card").filter(has_text="legacy-preview").click() + page.locator("#galleryGrid .gal-item").first.click() + expect(page.locator("#lightbox")).to_have_class("lightbox open") + expect(page.locator("#lbPrompt")).to_be_hidden() + expect(page.locator("#lbPromptText")).to_be_empty() browser.close() def test_provider_state_and_default(self): diff --git a/web/index.html b/web/index.html index b2130e0..c1d8346 100644 --- a/web/index.html +++ b/web/index.html @@ -165,6 +165,10 @@
+
@@ -301,6 +305,9 @@ "供应商 " + esc(g.provider_name) + "" + "大小 " + fmtBytes(g.bytes) + "" + "时间 " + esc(g.created_at) + ""; + const prompt = typeof g.prompt === "string" ? g.prompt : ""; + $("#lbPromptText").textContent = prompt; + $("#lbPrompt").classList.toggle("hidden", !prompt.trim()); $("#lightbox").classList.add("open"); } diff --git a/web/styles.css b/web/styles.css index 55ecd37..295e662 100644 --- a/web/styles.css +++ b/web/styles.css @@ -152,7 +152,7 @@ body[data-page="skills"] .app-nav a[data-nav="skills"] .nav-ico { color: var(--p .crumb h1 { font-size: 18px; } .top-actions { display: flex; align-items: center; gap: 8px; } -.page-body { padding: 24px; max-width: 1440px; width: 100%; } +.page-body { padding: 24px; width: 100%; } /* --------------------------------------------------------------- Animations */ @keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } @@ -378,7 +378,7 @@ table.tbl tbody tr:last-child td { border-bottom: none; } display: none; align-items: center; justify-content: center; z-index: 70; padding: 32px; } .lightbox.open { display: flex; } -.lb-inner { max-width: 1100px; width: 100%; display: flex; flex-direction: column; gap: 12px; animation: rise .22s ease both; } +.lb-inner { max-width: 1100px; width: 100%; max-height: calc(100vh - 64px); overflow-y: auto; display: flex; flex-direction: column; gap: 12px; animation: rise .22s ease both; } .lb-stage { display: flex; align-items: center; justify-content: center; gap: 16px; } .lb-stage img { max-height: 74vh; max-width: 100%; border-radius: var(--r-md); box-shadow: var(--sh-lg); } .lb-nav { @@ -389,6 +389,9 @@ table.tbl tbody tr:last-child td { border-bottom: none; } .lb-nav:hover { background: var(--surface-2); border-color: #3a4553; } .lb-info { display: flex; gap: 18px; flex-wrap: wrap; font-size: 12px; color: var(--text-sub); } .lb-info b { color: var(--text); font-weight: 600; } +.lb-prompt { padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); font-size: 12px; line-height: 1.7; } +.lb-prompt b { display: block; margin-bottom: 6px; color: var(--text); } +#lbPromptText { max-height: 160px; overflow-y: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text-sub); } /* ------------------------------------------------------------------ Statbox */ .stat { padding: 14px 16px; }