feat(admin): publish provider drafts into pack repo

This commit is contained in:
phamnazage-jpg
2026-05-28 07:30:02 +08:00
parent 8d7aa925df
commit 3a00f1b859
14 changed files with 948 additions and 16 deletions

View File

@@ -14,7 +14,7 @@
- `tksea-portal/admin/providers.html`
- `https://sub.tksea.top/portal/admin/providers.html`
- 用现有 CRM API 做 pack/provider 浏览、preview-import、import以及 provider manifest 草稿生成
- 当前也可直接调用服务端 `provider_drafts` API把 manifest 草稿持久化到 CRM SQLite并支持更新 / 删除
- 当前也可直接调用服务端 `provider_drafts` API把 manifest 草稿持久化到 CRM SQLite并支持更新 / 删除 / 发布到 pack 仓库
- `tksea-portal/admin/batch-import.html`
- `https://sub.tksea.top/portal/admin/batch-import.html`
- 结构化入口地址,当前跳转到 legacy `admin-batch-import.html`

View File

@@ -292,8 +292,8 @@
<h2>新增模型 / 供应商目录</h2>
<p>
这页负责浏览已安装 pack、选择 provider、调用 <code>preview-import</code> /
<code>import</code>,同时提供 provider manifest 草稿生成。当前版本不直接在浏览器里写入 pack 仓库,
但已经把“新增模型”的准备动作收进同一条操作链
<code>import</code>,同时提供 provider manifest 草稿生成与发布。当前版本已经支持先保存草稿,再经由 CRM
服务端写入 pack/provider 文件并自动提交到仓库
</p>
<div class="cta-row">
<a class="cta primary" href="/portal/admin/providers.html">打开供应商页</a>
@@ -302,7 +302,7 @@
<ul class="list">
<li>
<strong>适用动作</strong>
查看 pack 与 provider、输入 keys 做 preview/import、生成 provider 草稿 JSON
查看 pack 与 provider、输入 keys 做 preview/import、生成 provider 草稿,并一键发布到仓库
</li>
<li>
<strong>默认 API Base</strong>
@@ -342,8 +342,8 @@
</article>
<article class="status-card status-note">
<div class="metric-label">当前边界</div>
<strong>浏览器不直接写 pack 仓库</strong>
<p>新增 provider 模板的最终落盘仍通过仓库提交完成,页面当前先覆盖目录、草稿与导入操作</p>
<strong>浏览器提交到 CRM再由 CRM 写仓库</strong>
<p>页面不会直接拼 Git 命令;所有写 pack/provider 与提交仓库的动作,都统一走 CRM 服务端的发布接口</p>
</article>
<article class="status-card status-caution">
<div class="metric-label">安全前提</div>

View File

@@ -371,12 +371,12 @@
<p class="hero-copy">
这页把“新增模型供应商”和“导入供应商帐号”的前置动作收口在一起。当前版本会先列出
pack 里已经存在的 provider允许直接做 <code>preview-import</code><code>import</code>
如果你要新增 provider 模板,本页也会生成一份 manifest 草稿,方便再落回仓库提交
如果你要新增 provider 模板,本页也支持把草稿保存到 CRM再一键发布成 pack/provider 文件并自动提交到仓库
</p>
<ul class="hero-points">
<li>默认 API Base<code>/portal-admin-api</code></li>
<li>支持同域 Bearer admin token</li>
<li>支持 provider manifest 草稿生成</li>
<li>支持 provider 草稿发布到 pack 仓库</li>
</ul>
</article>
@@ -499,8 +499,8 @@
<article class="card panel" id="manifest-draft">
<h2>Provider Manifest 草稿</h2>
<p class="panel-desc">
这部分不会直接写仓库,只生成一个可复制的 JSON 草稿,解决“新增模型页面完全缺失”的问题
真正落盘仍通过 pack 仓库提交完成。
这部分既能生成与保存 provider 草稿,也能从已保存草稿一键发布到 pack/provider 文件并提交到仓库
页面本身不直接拼 Git 命令,所有写仓库动作都经由 CRM 服务端完成。
</p>
<div class="field-grid two">
@@ -530,10 +530,16 @@
</label>
</div>
<label>发布 Commit Message
<input id="draft-commit-message" type="text" placeholder="feat(pack): publish provider draft openai-zhongzhuan">
<span class="hint">留空时会按 provider_id 自动生成标准 commit message。</span>
</label>
<div class="actions">
<button class="secondary" id="generate-draft-btn">生成草稿</button>
<button class="primary" id="save-draft-btn">保存到服务端</button>
<button class="secondary" id="update-draft-btn">更新草稿</button>
<button class="primary" id="publish-draft-btn">发布到仓库</button>
<button class="ghost" id="delete-draft-btn">删除草稿</button>
<button class="ghost" id="copy-draft-btn">复制 JSON</button>
<button class="ghost" id="refresh-drafts-btn">刷新草稿列表</button>
@@ -615,6 +621,7 @@
const draftSmokeModelInput = document.getElementById("draft-smoke-model");
const draftBaseURLInput = document.getElementById("draft-base-url");
const draftModelsInput = document.getElementById("draft-models");
const draftCommitMessageInput = document.getElementById("draft-commit-message");
function defaultApiBase() {
return `${window.location.origin}/portal-admin-api`;
@@ -688,6 +695,7 @@
draftSmokeModel: draftSmokeModelInput.value.trim(),
draftBaseURL: draftBaseURLInput.value.trim(),
draftModels: draftModelsInput.value.trim(),
draftCommitMessage: draftCommitMessageInput.value.trim(),
}));
syncMetrics();
setStatus(catalogStatus, "本地配置已保存。", "success");
@@ -720,6 +728,7 @@
draftSmokeModelInput.value = payload.draftSmokeModel || "";
draftBaseURLInput.value = payload.draftBaseURL || "";
draftModelsInput.value = payload.draftModels || "";
draftCommitMessageInput.value = payload.draftCommitMessage || "";
} catch (error) {
apiBaseInput.value = defaultApiBase();
}
@@ -816,6 +825,9 @@
draftSmokeModelInput.value = draft.smoke_test_model || "";
draftBaseURLInput.value = draft.base_url || "";
draftModelsInput.value = Array.isArray(draft.supported_models) ? draft.supported_models.join(",") : "";
if (!draftCommitMessageInput.value.trim()) {
draftCommitMessageInput.value = `feat(pack): publish provider draft ${draft.provider_id || ""}`.trim();
}
}
function clearDraftFormSelection() {
@@ -1144,6 +1156,33 @@
}
}
async function publishDraftToRepo() {
const button = document.getElementById("publish-draft-btn");
button.disabled = true;
try {
if (!state.currentDraftID) {
throw new Error("请先从服务端草稿列表选择一条草稿");
}
const result = await requestJSON(`/api/provider-drafts/${encodeURIComponent(state.currentDraftID)}/publish`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
commit_message: draftCommitMessageInput.value.trim(),
}),
});
importResult.textContent = JSON.stringify(result.publish || result, null, 2);
setStatus(
draftStatus,
`已发布到仓库:${result.publish?.provider_path || "-"} · ${result.publish?.pack_version_before || "-"} -> ${result.publish?.pack_version_after || "-"} · ${result.publish?.commit_sha || "-"}`,
"success",
);
} catch (error) {
setStatus(draftStatus, `发布失败:${error.message}`, "danger");
} finally {
button.disabled = false;
}
}
function escapeHTML(value) {
return String(value)
.replaceAll("&", "&amp;")
@@ -1160,6 +1199,7 @@
document.getElementById("generate-draft-btn").addEventListener("click", generateDraft);
document.getElementById("save-draft-btn").addEventListener("click", saveDraftToServer);
document.getElementById("update-draft-btn").addEventListener("click", updateDraftOnServer);
document.getElementById("publish-draft-btn").addEventListener("click", publishDraftToRepo);
document.getElementById("delete-draft-btn").addEventListener("click", deleteDraftOnServer);
document.getElementById("copy-draft-btn").addEventListener("click", copyDraft);
document.getElementById("refresh-drafts-btn").addEventListener("click", loadServerDrafts);