diff --git a/webui/src/pages/SettingsPage.svelte b/webui/src/pages/SettingsPage.svelte index 5582373..2406e8f 100644 --- a/webui/src/pages/SettingsPage.svelte +++ b/webui/src/pages/SettingsPage.svelte @@ -6,20 +6,42 @@ let { notify } = $props(); let tab = $state("config"); let content = $state(""); + let original = $state(""); let path = $state(""); let loading = $state(true); + let saving = $state(false); + let reloadStatus = $state(null); + let pollTimer = null; + + const isConfig = $derived(tab === "config"); const title = $derived(tab === "config" ? "运行配置" : tab === "user" ? "用户档案" : "Agent 行为准则"); + const isDirty = $derived(content !== original); + + const jsonParse = $derived.by(() => { + if (!isConfig || !content.trim()) return { value: null, error: null }; + try { return { value: JSON.parse(content), error: null }; } catch (e) { return { value: null, error: e.message }; } + }); + const jsonError = $derived(jsonParse.error); + const outlineKeys = $derived(jsonParse.value ? Object.keys(jsonParse.value) : []); + + const phaseBadge = $derived.by(() => { + if (!reloadStatus) return "ok"; + const map = { steady: "ok", pending: "run", preparing: "run", activating: "run", failed: "fail" }; + return map[reloadStatus.phase] || "ok"; + }); async function load() { loading = true; try { - if (tab === "config") { + if (isConfig) { const result = await api("/api/config"); content = JSON.stringify(result.config, null, 2); + original = content; path = result.path; } else { const result = await api(`/api/profiles/${tab}`); content = result.content; + original = content; path = result.path; } } catch (caught) { notify(caught.message, true); } @@ -27,22 +49,67 @@ } async function save() { + if (isConfig && jsonError) return; + saving = true; try { - if (tab === "config") { - let config; - try { config = JSON.parse(content); } catch (caught) { throw new Error(`JSON 格式错误:${caught.message}`); } + if (isConfig) { + const config = JSON.parse(content); const result = await api("/api/config", { method: "PUT", body: JSON.stringify({ config }) }); content = JSON.stringify(result.config, null, 2); - notify("配置已保存,请重启 Gateway 使其生效"); + original = content; + notify("配置已保存"); } else { await api(`/api/profiles/${tab}`, { method: "PUT", body: JSON.stringify({ content }) }); + original = content; notify("助手档案已保存"); } } catch (caught) { notify(caught.message, true); } + finally { saving = false; } } - function changeTab(value) { tab = value; load(); } - onMount(load); + async function saveAndReload() { + if (jsonError) return; + saving = true; + try { + const config = JSON.parse(content); + const result = await api("/api/config", { method: "PUT", body: JSON.stringify({ config }) }); + content = JSON.stringify(result.config, null, 2); + original = content; + const reload = await api("/api/config/reload", { method: "POST" }); + notify(reload.message || "配置已保存并触发热重载"); + pollReloadStatus(); + } catch (caught) { notify(caught.message, true); } + finally { saving = false; } + } + + function discard() { content = original; } + + async function pollReloadStatus() { + try { reloadStatus = await api("/api/config/reload/status"); } catch {} + } + + function startPolling() { + stopPolling(); + pollReloadStatus(); + pollTimer = setInterval(pollReloadStatus, 3000); + } + + function stopPolling() { + if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } + } + + function changeTab(value) { + tab = value; + stopPolling(); + load(); + if (value === "config") startPolling(); + } + + onMount(() => { + load(); + startPolling(); + return stopPolling; + });
@@ -52,10 +119,84 @@ config.jsonUSER.mdAGENTS.md -
-
{title}{path}
- -
{#if tab === "config"}API Key 等敏感字段显示为 ********,保持不变即可保留原值。配置保存后需重启 Gateway 生效。{:else}Markdown 内容会在后续新会话和上下文构建中供 PicoBot 使用。{/if}
-
+ + {#if isConfig} +
+
+
+
{title}{path}
+
+ {#if isDirty}未保存{/if} + + + +
+
+ + {#if jsonError}
JSON 格式错误:{jsonError}
{/if} +
API Key 等敏感字段显示为 ********,保持不变即可保留原值。
+
+ + +
+ {:else} +
+
+
{title}{path}
+
+ {#if isDirty}未保存{/if} + +
+
+ +
Markdown 内容会在后续新会话和上下文构建中供 PicoBot 使用。
+
+ {/if}
+ +