feat: show generation prompts in previews and fix fluid page layout

This commit is contained in:
2026-11-04 01:19:42 +08:00
parent bef8af53ed
commit 5f3cebcb38
8 changed files with 57 additions and 9 deletions
+4 -3
View File
@@ -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 位)
+5 -1
View File
@@ -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,
+2
View File
@@ -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"),
}
+2 -1
View File
@@ -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 六页面无横向溢出,操作流程无未捕获脚本错误 | 通过 |
+12
View File
@@ -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<script>保持原样显示</script>"
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)
+20 -2
View File
@@ -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<script>window.promptInjected = true</script>"
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):
+7
View File
@@ -165,6 +165,10 @@
<button class="lb-nav" id="lbNext"><span class="ico" data-icon="chevronRight"></span></button>
</div>
<div class="lb-info" id="lbInfo"></div>
<div class="lb-prompt hidden" id="lbPrompt">
<b>提示词</b>
<div id="lbPromptText"></div>
</div>
<div style="display:flex;justify-content:flex-end">
<button class="btn btn-secondary btn-sm" id="lbDelete">删除图片</button>
<button class="btn btn-secondary btn-sm" id="lbClose">关闭</button>
@@ -301,6 +305,9 @@
"<span><b>供应商</b> " + esc(g.provider_name) + "</span>" +
"<span><b>大小</b> " + fmtBytes(g.bytes) + "</span>" +
"<span><b>时间</b> " + esc(g.created_at) + "</span>";
const prompt = typeof g.prompt === "string" ? g.prompt : "";
$("#lbPromptText").textContent = prompt;
$("#lbPrompt").classList.toggle("hidden", !prompt.trim());
$("#lightbox").classList.add("open");
}
+5 -2
View File
@@ -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; }