feat(portal): make provider/batch-import form fields self-explanatory + auto-fill
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled

Problem: provider manifest form had all-empty fields with cryptic
placeholders, users had to know what model IDs to type.

Fix on /portal/admin/providers.html (Provider Manifest 草稿):
- DISPLAY NAME: datalist of common vendors (OpenAI / DeepSeek /
  硅基流动 / Moonshot / 智谱 / Anthropic / 零一万物 / MiniMax / Qwen / Baichuan / 混元)
- PLATFORM: datalist of common platforms (openai / openai-compatible /
  deepseek / anthropic / gemini / zhipu / moonshot / minimax / qwen / ...)
- SMOKE TEST MODEL: datalist of common smoke models + auto-fills with
  first model from MODELS field if user leaves it empty
- BASE URL PLACEHOLDER: datalist of common base URLs (12 presets)
- MODELS: chip-row of 11 common models (gpt-5.4, gpt-5.4-mini,
  deepseek-chat, MiniMax-M2.7-highspeed, kimi-k2.6, glm-4.6,
  claude-sonnet-4-5, gemini-2.5-pro, qwen3-coder-plus, gpt-4o, o4-mini)
  + clear button. Click chip → append to MODELS field (dedup).
- KEYS textarea: 6 rows + example placeholder (sk-example-1/2/3)

Fix on /portal/admin-batch-import.html (发起导入):
- HOST ID: datalist of common host_ids + hint about loading pack first
- ENTRIES textarea: 6 rows + multi-line hint explaining
  base_url|api_key|model1,model2 format, optional model, batch import

JS change: syncDraftHelperState() in providers.html now auto-fills
smoke_test_model with first model if user hasn't filled it yet.
Also fixed: 2 duplicate copies of syncDraftHelperState (from
earlier batch script restoration) — both now have the new logic.

Verification:
- bash scripts/test/test_tksea_portal_assets.sh → PASS
- bash scripts/test/verify_frontend_smoke.sh → PASS
- browser_console click test: gpt-5.4 + deepseek-chat + kimi-k2.6 chips
  → models='gpt-5.4,deepseek-chat,kimi-k2.6' + smoke='gpt-5.4' auto-fill ✓
- screenshot: /tmp/portal-screenshots/admin-providers-v5.png
This commit is contained in:
phamnazage-jpg
2026-06-03 11:24:54 +08:00
parent 122d6282e1
commit 09f7c07de3
2 changed files with 142 additions and 11 deletions

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -87,7 +87,14 @@
<input id="api-base" type="text" placeholder="https://sub.tksea.top/portal-admin-api">
</label>
<label>Host ID
<input id="host-id" type="text" placeholder="remote43-current-host">
<input id="host-id" type="text" placeholder="remote43-current-host" list="preset-host-id">
<datalist id="preset-host-id">
<option value="remote43-current-host"></option>
<option value="remote43"></option>
<option value="localhost"></option>
<option value="dev-host"></option>
</datalist>
<span class="hint">目标子机的 host_id。要从「现有 host 列表」里选一个;若列表为空则先在 providers.html 加载 pack 目录。</span>
</label>
</div>
@@ -151,9 +158,14 @@
</div>
<label style="margin-top: 12px;">Entries
<textarea id="entries">https://api.example.com/v1|sk-example-1|kimi-k2.6
https://api.other.com/v1|sk-example-2|deepseek-chat,gpt-5.4</textarea>
<span class="hint">格式:`base_url|api_key|requested_model_1,requested_model_2`,模型为空时可省略第三段。</span>
<textarea id="entries" rows="6" placeholder="https://api.example.com/v1|sk-example-1|kimi-k2.6
https://api.other.com/v1|sk-example-2|deepseek-chat,gpt-5.4"></textarea>
<span class="hint">
格式:<code>base_url|api_key|requested_model_1,requested_model_2</code><br>
· 每行一条供应商帐号;多个 key 走多行即可(批量导入)。<br>
· 第三段 <code>requested_model</code> 可省略CRM 会从 host 已发布 provider 自动推断。<br>
· 同 base_url 可合并多行;不同 base_url 走独立 row 即可。
</span>
</label>
<div class="actions">

View File

@@ -197,8 +197,10 @@
</div>
<label>Keys
<textarea id="provider-keys" placeholder="一行一个 key"></textarea>
<span class="hint">一行一个供应商帐号 key。导入页会自动拆成字符串数组传给 CRM。</span>
<textarea id="provider-keys" rows="6" placeholder="sk-example-1
sk-example-2
sk-example-3"></textarea>
<span class="hint">一行一个供应商帐号 key多 key 批量导入)。例如 <code>sk-prod-xxx</code>。导入页会自动拆成字符串数组传给 CRM。</span>
</label>
<div class="actions">
@@ -225,7 +227,21 @@
<span class="hint">根据 display name / base url / models 自动生成,并尽量避免与现有 provider_id 冲突。</span>
</label>
<label>Display Name
<input id="draft-display-name" type="text" placeholder="OpenAI 中转">
<input id="draft-display-name" type="text" placeholder="OpenAI 中转 / DeepSeek / 硅基流动" list="preset-display-name">
<datalist id="preset-display-name">
<option value="OpenAI 中转"></option>
<option value="OpenAI 直连"></option>
<option value="DeepSeek"></option>
<option value="硅基流动 (SiliconFlow)"></option>
<option value="Moonshot Kimi"></option>
<option value="智谱 GLM"></option>
<option value="Anthropic Claude"></option>
<option value="零一万物"></option>
<option value="MiniMax"></option>
<option value="通义千问 Qwen"></option>
<option value="百川 Baichuan"></option>
<option value="腾讯混元"></option>
</datalist>
</label>
</div>
@@ -234,19 +250,74 @@
<div class="field-grid two">
<label>Platform
<input id="draft-platform" type="text" placeholder="openai">
<input id="draft-platform" type="text" placeholder="openai" list="preset-platform">
<datalist id="preset-platform">
<option value="openai"></option>
<option value="openai-compatible"></option>
<option value="deepseek"></option>
<option value="anthropic"></option>
<option value="gemini"></option>
<option value="zhipu"></option>
<option value="moonshot"></option>
<option value="minimax"></option>
<option value="qwen"></option>
<option value="baichuan"></option>
<option value="hunyuan"></option>
<option value="yi"></option>
</datalist>
</label>
<label>Smoke Test Model
<input id="draft-smoke-model" type="text" placeholder="gpt-5.4">
<input id="draft-smoke-model" type="text" placeholder="从 models 自动取第一个" list="preset-smoke-model">
<datalist id="preset-smoke-model">
<option value="gpt-5.4"></option>
<option value="gpt-5.4-mini"></option>
<option value="deepseek-chat"></option>
<option value="MiniMax-M2.7-highspeed"></option>
<option value="claude-sonnet-4-5"></option>
<option value="gemini-2.5-pro"></option>
<option value="kimi-k2.6"></option>
<option value="glm-4.6"></option>
<option value="qwen3-coder-plus"></option>
</datalist>
<span class="hint">建议填一个「最便宜最快」的模型作为健康探针,留空则从下方 Models 取第一个。</span>
</label>
</div>
<div class="field-grid two">
<label>Base URL Placeholder
<input id="draft-base-url" type="text" placeholder="https://api.example.com/v1">
<input id="draft-base-url" type="text" placeholder="https://api.example.com/v1" list="preset-base-url">
<datalist id="preset-base-url">
<option value="https://api.openai.com/v1"></option>
<option value="https://api.deepseek.com/v1"></option>
<option value="https://api.siliconflow.cn/v1"></option>
<option value="https://api.moonshot.cn/v1"></option>
<option value="https://open.bigmodel.cn/api/paas/v4"></option>
<option value="https://api.anthropic.com/v1"></option>
<option value="https://generativelanguage.googleapis.com/v1beta"></option>
<option value="https://api.minimax.chat/v1"></option>
<option value="https://dashscope.aliyuncs.com/compatible-mode/v1"></option>
<option value="https://api.baichuan-ai.com/v1"></option>
<option value="https://hunyuan.tencent.com/v1"></option>
<option value="https://api.lingyiwanwu.com/v1"></option>
</datalist>
</label>
<label>Models
<input id="draft-models" type="text" placeholder="gpt-5.4,gpt-5.4-mini">
<span class="hint">用英文逗号分隔。点击下方常用模型快速加入。</span>
<div class="chip-row" id="model-presets" style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;">
<button type="button" class="chip" data-model="gpt-5.4">+ gpt-5.4</button>
<button type="button" class="chip" data-model="gpt-5.4-mini">+ gpt-5.4-mini</button>
<button type="button" class="chip" data-model="deepseek-chat">+ deepseek-chat</button>
<button type="button" class="chip" data-model="MiniMax-M2.7-highspeed">+ MiniMax-M2.7-highspeed</button>
<button type="button" class="chip" data-model="kimi-k2.6">+ kimi-k2.6</button>
<button type="button" class="chip" data-model="glm-4.6">+ glm-4.6</button>
<button type="button" class="chip" data-model="claude-sonnet-4-5">+ claude-sonnet-4-5</button>
<button type="button" class="chip" data-model="gemini-2.5-pro">+ gemini-2.5-pro</button>
<button type="button" class="chip" data-model="qwen3-coder-plus">+ qwen3-coder-plus</button>
<button type="button" class="chip" data-model="gpt-4o">+ gpt-4o</button>
<button type="button" class="chip" data-model="o4-mini">+ o4-mini</button>
<button type="button" class="ghost chip-clear" data-action="clear-models" style="margin-left:6px;">清空</button>
</div>
</label>
</div>
@@ -557,6 +628,13 @@
if (forceProviderID || state.draftProviderIDAuto || !draftProviderIDInput.value.trim()) {
draftProviderIDInput.value = suggested;
}
// Auto-fill smoke_test_model with first model if user hasn't filled it yet
if (!draftSmokeModelInput.value.trim()) {
const models = parseDraftModels();
if (models.length > 0) {
draftSmokeModelInput.value = models[0];
}
}
}
function rememberLastPublishedTemplate() {
@@ -1441,6 +1519,13 @@
if (forceProviderID || state.draftProviderIDAuto || !draftProviderIDInput.value.trim()) {
draftProviderIDInput.value = suggested;
}
// Auto-fill smoke_test_model with first model if user hasn't filled it yet
if (!draftSmokeModelInput.value.trim()) {
const models = parseDraftModels();
if (models.length > 0) {
draftSmokeModelInput.value = models[0];
}
}
}
function rememberLastPublishedTemplate() {
@@ -2112,6 +2197,40 @@
syncDraftHelperState();
refreshAdminSession().catch(() => {});
renderServerDrafts();
// Wire up the "常用模型" preset chips — click to append to Models field.
// Placed after the main script so the input refs are available.
(function wireModelPresets() {
const wrap = document.getElementById("model-presets");
if (!wrap) return;
wrap.addEventListener("click", (ev) => {
const btn = ev.target.closest("button[data-model]");
if (!btn) return;
ev.preventDefault();
const model = btn.getAttribute("data-model");
const current = (draftModelsInput.value || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (current.includes(model)) {
setStatus(document.getElementById("draft-status"), `模型 ${model} 已在列表中`, "note");
return;
}
current.push(model);
draftModelsInput.value = current.join(",");
syncDraftHelperState(false);
setStatus(document.getElementById("draft-status"), `已加入 ${model}(共 ${current.length} 个)`, "success");
});
const clearBtn = wrap.querySelector('[data-action="clear-models"]');
if (clearBtn) {
clearBtn.addEventListener("click", (ev) => {
ev.preventDefault();
draftModelsInput.value = "";
syncDraftHelperState(false);
setStatus(document.getElementById("draft-status"), "已清空 Models 字段", "note");
});
}
})();
</script>
</body>
</html>