feat: isolate release source for installers and provisioning
Release / Build & Release (push) Canceled after 0s

This commit is contained in:
kory
2026-10-02 15:25:19 +08:00
parent ea7aebd276
commit f084ac5bae
10 changed files with 144 additions and 39 deletions
+13
View File
@@ -54,6 +54,7 @@ func (p *Panel) Handler() http.Handler {
mux.HandleFunc("/panel/api/mcp/test", p.protected(p.testMCP))
mux.HandleFunc("/panel/api/security/settings", p.protected(p.securitySettings))
mux.HandleFunc("/panel/api/security/settings/update", p.protected(p.updateSecuritySettings))
mux.HandleFunc("/panel/api/security/device-credentials/provision", p.protected(p.provisionDeviceCredential))
mux.HandleFunc("/panel/api/notifications/webhooks", p.protected(p.webhookTargets))
mux.HandleFunc("/panel/api/notifications/webhook/update", p.protected(p.updateWebhookTarget))
mux.HandleFunc("/panel/api/notifications/webhook/delete", p.protected(p.deleteWebhookTarget))
@@ -65,6 +66,7 @@ func (p *Panel) Handler() http.Handler {
type securityPanelRunner interface {
SecuritySettingsJSON(http.ResponseWriter, *http.Request)
UpdateSecuritySettingsJSON(http.ResponseWriter, *http.Request)
ProvisionDeviceCredentialJSON(http.ResponseWriter, *http.Request)
}
type mcpPanelRunner interface {
@@ -121,6 +123,17 @@ func (p *Panel) updateSecuritySettings(w http.ResponseWriter, r *http.Request) {
http.Error(w, "security API unavailable", 501)
}
func (p *Panel) provisionDeviceCredential(w http.ResponseWriter, r *http.Request) {
if p.AdminToken != "" {
r.Header.Set("X-Admin-Token", p.AdminToken)
}
if v, ok := p.Runner.(securityPanelRunner); ok {
v.ProvisionDeviceCredentialJSON(w, r)
return
}
http.Error(w, "security API unavailable", 501)
}
func (p *Panel) testMCP(w http.ResponseWriter, r *http.Request) {
if v, ok := p.Runner.(mcpPanelRunner); ok {
v.TestMCPJSON(w, r)
+19
View File
@@ -213,6 +213,25 @@ $("#mcpTest").onclick=async()=>{
$("#mcpTransport").onchange=()=>{const stdio=$("#mcpTransport").value==="stdio";$("#mcpCommand").parentElement.style.display=stdio?"":"none";$("#mcpArgs").parentElement.style.display=stdio?"":"none";$("#mcpUrl").parentElement.style.display=stdio?"none":""};
loadMCPServices().catch(console.error);
async function provisionDeviceCredential(){
const result=$("#deviceProvisionResult");
const status=$("#deviceProvisionStatus");
const name=($("#provisionDeviceName").value||"").trim();
if(!name){status.textContent="请输入节点名称。";return}
try{
const x=await api("/security/device-credentials/provision",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:name})});
$("#deviceProvisionToken").value=x.token||"";
$("#deviceProvisionCommand").value=x.install_command||"";
result.hidden=false;
status.textContent=name+" 接入凭证已生成。安装时把 Token 填入 Agent Token。";
}catch(e){status.textContent="生成失败: "+e.message}
}
$("#deviceProvisionNew").onclick=()=>{$("#deviceProvisionEditor").hidden=false;$("#deviceProvisionResult").hidden=true;$("#deviceProvisionStatus").textContent="";$("#provisionDeviceName").focus()};
$("#deviceProvisionCancel").onclick=()=>{$("#deviceProvisionEditor").hidden=true};
$("#deviceProvisionCreate").onclick=()=>provisionDeviceCredential();
$("#deviceProvisionToken").onclick=e=>e.target.select();
$("#deviceProvisionCommand").onclick=e=>e.target.select();
async function loadSecuritySettings(){const x=await api("/security/settings"),nodes=x.nodes||[],effective=x.effective||{},configured=new Map(nodes.map(n=>[n.node,n.mode])),devices=await api("/devices");$("#globalSecurity").value=x.global||"medium";$("#securityNodes").innerHTML=devices.map(d=>{const mode=configured.get(d.name)||"inherit",eff=effective[d.name]||x.global||"medium";return `<tr><td>${esc(d.name)}</td><td><select data-security-node="${esc(d.name)}"><option value="inherit" ${mode==='inherit'?'selected':''}>跟随全局</option><option value="low" ${mode==='low'?'selected':''}>低 · Low</option><option value="medium" ${mode==='medium'?'selected':''}>中 · Medium</option><option value="high" ${mode==='high'?'selected':''}>高 · High</option></select></td><td>${esc(eff)}</td><td><button type="button" onclick="saveNodeSecurity('${esc(d.name)}')">保存</button></td></tr>`}).join("")}
async function saveNodeSecurity(node){const el=document.querySelector(`[data-security-node="${CSS.escape(node)}"]`);if(!el)return;try{await api("/security/settings/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({node,mode:el.value})});$("#securityStatus").textContent=`${node} 策略已保存。`;await loadSecuritySettings()}catch(e){$("#securityStatus").textContent="保存失败: "+e.message}}
$("#saveGlobalSecurity").onclick=async()=>{try{await api("/security/settings/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({global:$("#globalSecurity").value})});$("#securityStatus").textContent="全局安全策略已保存。";await loadSecuritySettings()}catch(e){$("#securityStatus").textContent="保存失败: "+e.message}};$("#securityRefresh").onclick=()=>loadSecuritySettings().catch(console.error);loadSecuritySettings().catch(console.error);
+18 -1
View File
@@ -1,5 +1,22 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><script>(function(){try{var t=localStorage.getItem("vpc_theme");if(t!=="light"&&t!=="dark")t=matchMedia("(prefers-color-scheme:light)").matches?"light":"dark";document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme="dark"}})();</script><title>VPS-Commander</title><link rel="stylesheet" href="/static/app.css"></head><body><header><div class="brand-wrap"><img src="/static/icon.png" alt="logo" class="brand-logo"><div><strong>VPS-Commander</strong><span>Private Infrastructure Console</span></div></div><div class="header-actions"><button id="themeToggle" type="button" class="btn-icon" title="切换主题" aria-label="切换主题"><svg class="theme-svg svg-sun" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg><svg class="theme-svg svg-moon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></button><button id="logout" type="button">退出</button></div></header><main><section class="grid"><article class="card wide"><div class="cardhead"><h2>设备看板</h2><button id="refresh" type="button">刷新</button></div><div id="devices" class="devices"></div></article><article class="card"><h2>Actions 向导</h2><p>当前 Hub 的 OpenAPI Schema:</p><div class="actions"><button id="copySchema" type="button">复制 Schema 地址</button><button id="copyPrompt" type="button">复制 System Prompt</button><button id="rotateKey" type="button">轮换 API Key</button></div><pre id="schemaHint">/openapi.json</pre><pre id="newKey"></pre></article><article class="card wide"><div class="mcp-toolbar"><h2>MCP</h2><button id="mcpNew" type="button">新增 MCP</button></div><div id="mcpList" class="mcp-grid"></div><div id="mcpEditor" hidden><div class="mcp-form"><label>服务标识<input id="mcpId" placeholder="docker"></label><label>显示名称<input id="mcpName" placeholder="Docker 容器管理"></label><label>传输类型<select id="mcpTransport"><option value="stdio">命令行 / stdio</option><option value="http">HTTP / Streamable HTTP</option><option value="sse">SSE</option></select></label><label>启用<select id="mcpEnabled"><option value="1">启用</option><option value="0">停用</option></select></label><label class="full">Command<input id="mcpCommand" placeholder="docker-mcp"></label><label class="full">Arguments(JSON 数组)<textarea id="mcpArgs">[]</textarea></label><label class="full">URL<input id="mcpUrl" placeholder="http://127.0.0.1:8080/mcp"></label><label class="full">Headers(JSON 对象,可选)<textarea id="mcpHeaders">{}</textarea></label><label class="full">环境变量(JSON 对象,可选)<textarea id="mcpEnv">{}</textarea></label><label>生效范围<select id="mcpScope"><option value="all">全部节点</option><option value="custom">指定节点</option></select></label><div><span>指定节点</span><div id="mcpNodes" class="mcp-nodes"></div></div><label>测试节点<select id="mcpTestNode"><option value="local">Hub 本机</option></select></label></div><div class="mcp-actions"><button id="mcpSave" type="button">保存并热更新</button><button id="mcpTest" type="button">测试连通性</button><button id="mcpCancel" type="button">取消</button></div><div id="mcpStatus" class="mcp-status"></div><div id="mcpTestResult" class="mcp-test-result" hidden></div></div></article>
<article class="card wide"><div class="cardhead"><h2>快速控制台</h2><select id="device"></select></div><textarea id="command" placeholder="输入 Shell 命令,例如 uname -a"></textarea><div><button id="run" type="button">执行</button></div><pre id="output"></pre></article><article class="card wide"><div class="cardhead"><h2>安全</h2><button id="securityRefresh" type="button">刷新</button></div><div class="security-grid"><label>全局安全等级<select id="globalSecurity"><option value="low">低 · Low</option><option value="medium">中 · Medium</option><option value="high">高 · High</option></select></label><button id="saveGlobalSecurity" type="button">保存全局策略</button></div><div class="tablewrap"><table><thead><tr><th>节点</th><th>单机策略</th><th>有效策略</th><th>操作</th></tr></thead><tbody id="securityNodes"></tbody></table></div><p id="securityStatus"></p></article><article class="card wide">
<article class="card wide"><div class="cardhead"><h2>快速控制台</h2><select id="device"></select></div><textarea id="command" placeholder="输入 Shell 命令,例如 uname -a"></textarea><div><button id="run" type="button">执行</button></div><pre id="output"></pre></article><article class="card wide"><div class="cardhead"><h2>设备接入</h2><button id="deviceProvisionNew" type="button">添加设备</button></div>
<div id="deviceProvisionEditor" hidden>
<div class="mcp-form">
<label>节点名称<input id="provisionDeviceName" placeholder="例如 hk-vps-01"></label>
</div>
<div class="mcp-actions">
<button id="deviceProvisionCreate" type="button">生成接入凭证</button>
<button id="deviceProvisionCancel" type="button">取消</button>
</div>
<div id="deviceProvisionStatus" class="mcp-status"></div>
<div id="deviceProvisionResult" hidden>
<label>Agent Token<input id="deviceProvisionToken" readonly></label>
<label>一键安装命令<textarea id="deviceProvisionCommand" readonly></textarea></label>
<p>Token 只在这里明文显示;Hub 仅保存 Token 哈希。</p>
</div>
</div>
</article>
<article class="card wide"><div class="cardhead"><h2>安全</h2><button id="securityRefresh" type="button">刷新</button></div><div class="security-grid"><label>全局安全等级<select id="globalSecurity"><option value="low">低 · Low</option><option value="medium">中 · Medium</option><option value="high">高 · High</option></select></label><button id="saveGlobalSecurity" type="button">保存全局策略</button></div><div class="tablewrap"><table><thead><tr><th>节点</th><th>单机策略</th><th>有效策略</th><th>操作</th></tr></thead><tbody id="securityNodes"></tbody></table></div><p id="securityStatus"></p></article><article class="card wide">
<div class="mcp-toolbar">
<h2>通知</h2>
<div style="display:flex;gap:8px">