feat(portal): make provider/batch-import form fields self-explanatory + auto-fill
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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user