Add hub AI config tab with sync to instances and deploy secret bootstrap.
Wire bootstrap_deploy_secrets into setup_env.sh (one-time HUB_BRIDGE_TOKEN, FLASK_SECRET_KEY, HUB_SESSION_SECRET). Remove AI section from instance env UI; hub saves OPENAI settings to all four .env files. SSO unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4347,6 +4347,111 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderHubAiSyncStatus(sync) {
|
||||
const el = document.getElementById("hub-ai-sync-status");
|
||||
if (!el) return;
|
||||
if (!sync) {
|
||||
el.textContent = "";
|
||||
return;
|
||||
}
|
||||
if (sync.all_synced) {
|
||||
el.textContent = "三所实例 AI 配置已与中控同步";
|
||||
el.classList.remove("is-err");
|
||||
return;
|
||||
}
|
||||
const parts = [];
|
||||
const inst = sync.instances || {};
|
||||
Object.keys(inst).forEach((ex) => {
|
||||
const row = inst[ex];
|
||||
if (row && !row.ok) parts.push(`${ex} 不一致`);
|
||||
});
|
||||
el.textContent = parts.length ? `未完全同步:${parts.join("、")}` : "同步状态未知";
|
||||
el.classList.add("is-err");
|
||||
}
|
||||
|
||||
function renderHubAiEnvFields(fields) {
|
||||
const box = document.getElementById("hub-ai-env-fields");
|
||||
if (!box) return;
|
||||
box.innerHTML = (fields || [])
|
||||
.map((f) => {
|
||||
const wide = f.key === "OPENAI_API_BASE" || f.key === "OLLAMA_API" ? " field-wide" : "";
|
||||
const inputType = f.sensitive ? "password" : f.type === "int" ? "number" : "text";
|
||||
const placeholder = f.sensitive && f.has_value ? f.masked || "****" : f.note || "";
|
||||
const value = f.sensitive ? "" : esc(f.current || f.default || "");
|
||||
const note = f.note ? `<span class="field-note">${esc(f.note)}</span>` : "";
|
||||
return `<div class="field${wide}">
|
||||
<label for="hub-ai-${esc(f.key)}">${esc(f.label || f.key)}${note}</label>
|
||||
<input id="hub-ai-${esc(f.key)}" class="hub-ai-field" data-ai-key="${esc(f.key)}" type="${inputType}" value="${value}" placeholder="${esc(placeholder)}" autocomplete="off" />
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function loadHubAiEnvSettings() {
|
||||
const box = document.getElementById("hub-ai-env-fields");
|
||||
if (!box) return;
|
||||
try {
|
||||
const r = await apiFetch("/api/settings/ai-env");
|
||||
const j = await r.json();
|
||||
if (!r.ok) throw new Error(j.detail || "加载失败");
|
||||
renderHubAiEnvFields(j.fields || []);
|
||||
renderHubAiSyncStatus(j.sync_status);
|
||||
} catch (e) {
|
||||
box.innerHTML = `<span class="err">${esc(String(e.message || e))}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function collectHubAiEnvValues() {
|
||||
const values = {};
|
||||
document.querySelectorAll(".hub-ai-field[data-ai-key]").forEach((el) => {
|
||||
values[el.dataset.aiKey] = el.value;
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
async function saveHubAiEnv() {
|
||||
const status = document.getElementById("hub-ai-env-save-status");
|
||||
const setStatus = (msg, err) => {
|
||||
if (!status) return;
|
||||
status.textContent = msg || "";
|
||||
status.classList.toggle("is-err", !!err);
|
||||
};
|
||||
setStatus("保存并同步中…");
|
||||
try {
|
||||
const r = await apiFetch("/api/settings/ai-env", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ values: collectHubAiEnvValues(), restart: true }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!r.ok) throw new Error(j.detail || j.msg || "保存失败");
|
||||
if (!j.changed || !Object.keys(j.changed).length) {
|
||||
setStatus("未修改(与当前配置相同)");
|
||||
showToast("未修改");
|
||||
return;
|
||||
}
|
||||
setStatus("已同步三所,服务重启中…");
|
||||
await waitHubHealth();
|
||||
setStatus("AI 配置已保存并同步至三所实例");
|
||||
showToast("AI 配置已保存并同步");
|
||||
await loadHubAiEnvSettings();
|
||||
} catch (e) {
|
||||
setStatus(String(e.message || e), true);
|
||||
showToast(String(e.message || e), true);
|
||||
}
|
||||
}
|
||||
|
||||
function initHubAiEnvSettings() {
|
||||
const btn = document.getElementById("hub-ai-env-save-btn");
|
||||
if (btn && btn.dataset.bound !== "1") {
|
||||
btn.dataset.bound = "1";
|
||||
btn.addEventListener("click", () => {
|
||||
void saveHubAiEnv();
|
||||
});
|
||||
}
|
||||
void loadHubAiEnvSettings();
|
||||
}
|
||||
|
||||
function macroDatetimeLocalToApi(v) {
|
||||
if (!v) return "";
|
||||
return String(v).trim().replace("T", " ").slice(0, 16);
|
||||
@@ -4491,6 +4596,7 @@
|
||||
loadSettingsMetaLine();
|
||||
initMacroCalendarSettings();
|
||||
initHubPasswordSettings();
|
||||
initHubAiEnvSettings();
|
||||
loadMacroCalendarUI();
|
||||
loadSettings().then((data) => {
|
||||
syncDisplayPrefsUI(data);
|
||||
|
||||
Reference in New Issue
Block a user