feat: complete image console and add verified REST and MCP workflows
This commit is contained in:
+387
@@ -0,0 +1,387 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>出图 · ai-image</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<script src="api.js"></script>
|
||||
<script src="components.js"></script>
|
||||
</head>
|
||||
<body data-page="create">
|
||||
|
||||
<aside class="app-nav">
|
||||
<div class="app-brand">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
<span class="brand-text"><b>ai-image</b><small>绘图聚合控制台</small></span>
|
||||
</div>
|
||||
<nav class="nav-list">
|
||||
<p class="nav-group">工作台</p>
|
||||
<a class="nav-item" data-nav="create" href="create.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3-1.9 5.8L4 10.7l6.1 1.9L12 18.4l1.9-5.8 6.1-1.9-6.1-1.9Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg></span>出图</a>
|
||||
<a class="nav-item" data-nav="gallery" href="index.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg></span>画廊</a>
|
||||
<a class="nav-item" data-nav="records" href="records.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg></span>生成记录</a>
|
||||
<p class="nav-group">配置</p>
|
||||
<a class="nav-item" data-nav="providers" href="providers.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v3a6 6 0 0 1-12 0V8Z"/></svg></span>供应商</a>
|
||||
<a class="nav-item" data-nav="models" href="models.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"/><path d="m7 16.5-4.74-2.85"/><path d="m7 16.5 5-3"/><path d="M7 16.5v5.17"/><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"/><path d="m17 16.5-5-3"/><path d="m17 16.5 4.74-2.85"/><path d="M17 16.5v5.17"/><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"/><path d="M12 8 7.26 5.15"/><path d="m12 8 4.74-2.85"/><path d="M12 13.5V8"/></svg></span>模型</a>
|
||||
<p class="nav-group">Agent</p>
|
||||
<a class="nav-item" data-nav="skills" href="skills.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg></span>SKILL.md</a>
|
||||
</nav>
|
||||
<div class="nav-foot">
|
||||
<span class="dot dot-ok"></span>服务在线 <span class="mono">:8765</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="app-content">
|
||||
<header class="top-bar">
|
||||
<div class="crumb">
|
||||
<span class="crumb-sec">工作台</span>
|
||||
<span class="crumb-sep">/</span>
|
||||
<h1 id="pageTitle">出图</h1>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<span class="tag tag-info mono">output/user/</span>
|
||||
<a class="btn btn-secondary btn-sm" href="index.html">查看画廊</a>
|
||||
<a class="btn btn-sm" href="providers.html">接入供应商</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="page-body">
|
||||
|
||||
<div class="split">
|
||||
<!-- ==================== 左:出图表单 ==================== -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>生成图片</h3>
|
||||
<div class="sub">网页端出图固定写入 <code>output/user/</code>,目录不存在时自动创建</div>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<span class="tag" id="profileTag">profile_name = user</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="field" id="fieldPrompt">
|
||||
<label>提示词 <span class="req">*</span></label>
|
||||
<textarea id="prompt" rows="5" placeholder="描述你想要的画面,例如:a luminous floating city above a misty canyon at sunrise, cinematic realism, volumetric light"></textarea>
|
||||
<div class="err-msg">请填写提示词</div>
|
||||
<div class="hint">建议包含主体 + 风格 + 光线 + 画幅关键词,中英文均可</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="field" id="fieldProvider">
|
||||
<label>供应商 <span class="req">*</span></label>
|
||||
<select id="provider"></select>
|
||||
<div class="err-msg">请选择供应商</div>
|
||||
</div>
|
||||
<div class="field" id="fieldModel">
|
||||
<label>模型 <span class="req">*</span></label>
|
||||
<select id="model"></select>
|
||||
<div class="err-msg">请选择模型</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="field">
|
||||
<label>尺寸</label>
|
||||
<select id="size">
|
||||
<option value="1024x1024">1024x1024(1:1)</option>
|
||||
<option value="2048x2048">2048x2048(1:1 · 高清)</option>
|
||||
<option value="1312x736">1312x736(16:9)</option>
|
||||
<option value="1152x864">1152x864(4:3)</option>
|
||||
<option value="2048x1152">2048x1152(16:9 · 高清)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>数量</label>
|
||||
<select id="count">
|
||||
<option value="1">1 张</option>
|
||||
<option value="2">2 张</option>
|
||||
<option value="4">4 张</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar" style="margin-bottom:0">
|
||||
<button class="btn btn-lg" id="generateBtn"><span class="ico" data-icon="sparkles"></span>开始生成</button>
|
||||
<button class="btn btn-secondary btn-sm" id="discoverBtn"><span class="ico" data-icon="refresh"></span>拉取上游模型</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="dim mono" id="modelCount">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>本次结果</h3>
|
||||
<div class="sub">生成成功后可在下方查看,并已自动加入 <code>output/user/</code> 画廊</div>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<span class="tag tag-success" id="resultStatus">尚未生成</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="gal-grid" id="resultGrid"></div>
|
||||
<div class="empty" id="resultEmpty">
|
||||
<span class="ico" data-icon="image"></span>
|
||||
<h4>还没有生成结果</h4>
|
||||
<p>填写提示词并选择可用模型后点击「开始生成」。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 右:说明与最近产出 ==================== -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<h3>落盘规则</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="code-box"><code>output/
|
||||
└── user/ ← 网页端手动出图
|
||||
├── gpt-image-2-1K-0014ef2b.png
|
||||
└── ...</code></div>
|
||||
<ul class="md" style="padding-left:18px;margin:12px 0 0">
|
||||
<li>output/ 下的<b>文件夹名 = 谁生成的</b>,网页端固定为 <code>user</code></li>
|
||||
<li>Agent 调用时由调用方指定 <code>profile_name</code></li>
|
||||
<li>目录不存在时后端自动创建</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="card-head">
|
||||
<h3>可用模型</h3>
|
||||
<div class="top-actions">
|
||||
<span class="tag tag-info" id="modelSource">来自实时拉取</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="model-list" id="modelList"></div>
|
||||
<div class="empty hidden" id="modelEmpty">
|
||||
<span class="ico" data-icon="alert"></span>
|
||||
<h4>没找到有效的模型</h4>
|
||||
<p>请先接入供应商并拉取上游模型清单。</p>
|
||||
<a class="btn btn-secondary btn-sm" href="providers.html">接入供应商</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="card-head">
|
||||
<h3>user 画廊最近产出</h3>
|
||||
<div class="top-actions">
|
||||
<a class="btn btn-ghost btn-sm" href="index.html">全部</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="gal-grid" id="recentGrid"></div>
|
||||
<div class="empty hidden" id="recentEmpty">
|
||||
<span class="ico" data-icon="images"></span>
|
||||
<h4>output/user/ 还是空的</h4>
|
||||
<p>生成一张图试试。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const $ = (s) => document.querySelector(s);
|
||||
let providers = [];
|
||||
let models = [];
|
||||
let recent = [];
|
||||
|
||||
|
||||
function setError(fieldSel, on, msg) {
|
||||
const f = $(fieldSel);
|
||||
f.classList.toggle("has-error", on);
|
||||
if (msg) f.querySelector(".err-msg").textContent = msg;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------- 供应商 / 模型 */
|
||||
function enabledModels(providerId) {
|
||||
return models.filter(
|
||||
(m) => m.enabled && m.kind === "image" && (!providerId || m.provider_id === providerId)
|
||||
&& providers.some((p) => p.id === m.provider_id && p.enabled)
|
||||
);
|
||||
}
|
||||
|
||||
function renderProviderOptions() {
|
||||
const sel = $("#provider");
|
||||
sel.innerHTML = providers
|
||||
.map((p) => '<option value="' + esc(p.id) + '"' + (p.enabled ? "" : " disabled") + ">" +
|
||||
esc(p.name) + (p.enabled ? "" : "(已停用)") + "</option>")
|
||||
.join("");
|
||||
if (!providers.length) sel.innerHTML = '<option value="">(无可用供应商)</option>';
|
||||
const first = providers.find((p) => p.enabled);
|
||||
sel.value = first ? first.id : "";
|
||||
}
|
||||
|
||||
function renderModelOptions() {
|
||||
const pid = $("#provider").value;
|
||||
const list = enabledModels(pid);
|
||||
const sel = $("#model");
|
||||
sel.innerHTML = list.length
|
||||
? list
|
||||
.map((m) => '<option value="' + esc(m.model_id) + '">' + esc(m.display_name || m.model_id) + " · " + esc(m.model_id) + "</option>")
|
||||
.join("")
|
||||
: '<option value="">(该供应商暂无可用模型)</option>';
|
||||
$("#modelCount").textContent = list.length + " 个可用模型";
|
||||
const preferred = providers.find((p) => p.id === pid)?.default_model;
|
||||
if (list.some((m) => m.model_id === preferred)) sel.value = preferred;
|
||||
}
|
||||
|
||||
function renderModelList() {
|
||||
const list = enabledModels();
|
||||
const box = $("#modelList");
|
||||
box.innerHTML = "";
|
||||
$("#modelEmpty").classList.toggle("hidden", list.length !== 0);
|
||||
list.forEach((m) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "model-row";
|
||||
const p = providers.find((x) => x.id === m.provider_id);
|
||||
row.innerHTML =
|
||||
'<div style="flex:1;min-width:0">' +
|
||||
'<div class="mono" style="font-size:12px">' + esc(m.model_id) + "</div>" +
|
||||
'<div class="hint">' + esc(p ? p.name : m.provider_id) + " · " + esc(m.size_hint || "—") + "</div>" +
|
||||
"</div>" +
|
||||
'<span class="tag tag-success">可用</span>';
|
||||
box.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadProviders() {
|
||||
providers = await window.API.listProviders();
|
||||
renderProviderOptions();
|
||||
renderModelOptions();
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
models = await window.API.listModels();
|
||||
renderModelOptions();
|
||||
renderModelList();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ 拉取上游模型 */
|
||||
$("#discoverBtn").addEventListener("click", async () => {
|
||||
const pid = $("#provider").value;
|
||||
if (!pid) {
|
||||
toast("请先选择供应商", "err");
|
||||
return;
|
||||
}
|
||||
const btn = $("#discoverBtn");
|
||||
if (btn.disabled) return;
|
||||
btn.disabled = true;
|
||||
btn.classList.add("is-loading");
|
||||
try {
|
||||
const res = await window.API.discoverProviderModels(pid);
|
||||
await loadModels();
|
||||
if (!res.matched || !res.matched.length) {
|
||||
toast("没找到有效的模型", "err");
|
||||
$("#modelSource").textContent = "未命中模型";
|
||||
return;
|
||||
}
|
||||
$("#modelSource").textContent = "实时拉取 · " + res.matched.length + " 个";
|
||||
toast("拉取到 " + res.matched.length + " 个可用模型");
|
||||
} catch (e) {
|
||||
toast(e.message, "err");
|
||||
} finally {
|
||||
btn.classList.remove("is-loading");
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------------------------------------------------------- 生成 */
|
||||
$("#generateBtn").addEventListener("click", async () => {
|
||||
const prompt = $("#prompt").value.trim();
|
||||
const provider_id = $("#provider").value;
|
||||
const model = $("#model").value;
|
||||
const size = $("#size").value;
|
||||
const n = parseInt($("#count").value, 10);
|
||||
|
||||
setError("#fieldPrompt", !prompt);
|
||||
setError("#fieldProvider", !provider_id);
|
||||
setError("#fieldModel", !model, "请选择模型");
|
||||
if (!prompt || !provider_id || !model) return;
|
||||
|
||||
const btn = $("#generateBtn");
|
||||
if (btn.disabled) return;
|
||||
btn.disabled = true;
|
||||
btn.classList.add("is-loading");
|
||||
$("#resultStatus").textContent = "生成中…";
|
||||
$("#resultStatus").className = "tag tag-warn";
|
||||
|
||||
try {
|
||||
const res = await window.API.generateImage({
|
||||
provider_id: provider_id,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
size: size,
|
||||
n: n,
|
||||
});
|
||||
renderResult(res);
|
||||
$("#resultStatus").textContent = "生成成功 · " + res.images.length + " 张";
|
||||
$("#resultStatus").className = "tag tag-success";
|
||||
toast("已保存到 " + res.output_dir);
|
||||
await loadRecent();
|
||||
} catch (e) {
|
||||
$("#resultStatus").textContent = "生成失败";
|
||||
$("#resultStatus").className = "tag tag-danger";
|
||||
toast(e.message, "err");
|
||||
} finally {
|
||||
btn.classList.remove("is-loading");
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function renderResult(res) {
|
||||
const grid = $("#resultGrid");
|
||||
grid.innerHTML = "";
|
||||
$("#resultEmpty").classList.toggle("hidden", res.images.length !== 0);
|
||||
res.images.forEach((img) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "gal-item";
|
||||
el.innerHTML =
|
||||
'<img src="' + esc(img.url) + '" alt="' + esc(img.filename) + '" />' +
|
||||
'<span class="tag gal-tag">' + esc(res.generation.model) + "</span>" +
|
||||
'<div class="gal-meta"><b>' + esc(img.filename) + "</b><span>" +
|
||||
fmtBytes(img.bytes) + " · " + esc(res.generation.created_at) + "</span></div>";
|
||||
grid.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------- 最近产出 */
|
||||
async function loadRecent() {
|
||||
const items = await window.API.listGallery("user");
|
||||
recent = items.slice(0, 6);
|
||||
const grid = $("#recentGrid");
|
||||
grid.innerHTML = "";
|
||||
$("#recentEmpty").classList.toggle("hidden", recent.length !== 0);
|
||||
recent.forEach((g) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "gal-item";
|
||||
el.innerHTML =
|
||||
'<img src="' + esc(g.url) + '" alt="' + esc(g.filename) + '" loading="lazy" />' +
|
||||
'<div class="gal-meta"><b>' + esc(g.filename) + "</b><span>" + esc(g.created_at) + "</span></div>";
|
||||
grid.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
$("#provider").addEventListener("change", renderModelOptions);
|
||||
|
||||
paintIcons();
|
||||
runTask(async () => {
|
||||
await loadProviders();
|
||||
await loadModels();
|
||||
await loadRecent();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user