diff --git a/.memory/worklog.json b/.memory/worklog.json index 01f5f92..47a5fe2 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,18 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "f4789ab", - "message": "auto-save 2026-05-10 07:28 (~1)", - "ts": "2026-05-10T07:28:02+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 07:28 (~1)", - "ts": "2026-05-09T23:28:29Z", - "type": "session-heartbeat" - }, { "files_changed": 1, "hash": "a72c5bb", @@ -3259,6 +3246,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-11 15:35 (~5)", "files_changed": 1 + }, + { + "ts": "2026-05-11T15:41:29+08:00", + "type": "commit", + "message": "auto-save 2026-05-11 15:41 (~1)", + "hash": "a3cc734", + "files_changed": 1 + }, + { + "ts": "2026-05-11T07:46:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 6 项未提交变更 · 最近提交:auto-save 2026-05-11 15:41 (~1)", + "files_changed": 6 } ] } diff --git a/RULES.md b/RULES.md index ecfaff0..dc0b001 100644 --- a/RULES.md +++ b/RULES.md @@ -19,8 +19,8 @@ - 爱马仕前端「仪表盘」同步了上游 Hermes 的快捷入口板块,个人版展示主站、API、飞书机器人列表、文档/解析入口 - 爱马仕前端「仪表盘」活动热力图已重做为带摘要、月份标尺、紧凑格子和细分色阶的活动卡片 - 爱马仕前端「设置 → 连接」可自助维护 API 地址 / API Key 并测试连接;「对话 → 存周报」和「设置 → 周报记录」会在本地保存任务描述、上下文片段和最终周报 -- 爱马仕前端「设置 → 模型与 MCP」可读取 / 写入 LXC 内 `/opt/hermes-agent/config.yaml`,保存后重启 Docker `hermes-agent` -- 当前前端静态壳缓存版本:`hermes-ui-v26` +- 爱马仕前端「设置 → AI 模型接入」和「设置 → MCP 工具接入」可分别维护 LXC 内 `/opt/hermes-agent/config.yaml` 的 `model` 与 `mcp_servers` 块,保存后重启 Docker `hermes-agent` +- 当前前端静态壳缓存版本:`hermes-ui-v27` - 文档 / 解析:https://styles.kang-kang.com - 管理后台:待定 - 代码仓:https://git.kang-kang.com/kangwan/hermes-glass-ui-personal diff --git a/src/app.js b/src/app.js index 2e7d49a..7d7204d 100644 --- a/src/app.js +++ b/src/app.js @@ -125,15 +125,29 @@ function saveSettings(options = {}) { pingBackend(); } +function ensureModelChoice(modelValue, labelValue = "") { + const model = (modelValue || "").trim(); + if (!model) return; + const label = (labelValue || model).trim(); + const pick = document.getElementById("modelPick"); + if (pick && !Array.from(pick.options).some(item => item.value === model)) { + pick.appendChild(new Option(label, model)); + } + const list = document.getElementById("modelOptions"); + if (list && !Array.from(list.options).some(item => item.value === model)) { + const option = document.createElement("option"); + option.value = model; + option.label = label; + list.appendChild(option); + } +} + function syncModelPick(modelValue) { const model = (modelValue || state.model || "").trim(); const pick = document.getElementById("modelPick"); if (!pick || !model) return; + ensureModelChoice(model); let option = Array.from(pick.options).find(item => item.value === model); - if (!option) { - option = new Option(model, model); - pick.appendChild(option); - } pick.value = model; state.model = model; const stat = document.getElementById("statModel"); @@ -596,19 +610,30 @@ async function testApiConnection() { let _hermesConfigLoaded = false; let _hermesConfigLoading = false; -function setHermesConfigStatus(text, isError = false) { - const el = document.getElementById("hermesConfigStatus"); +let _hermesConfigSnapshot = null; +function setSettingsStatus(id, text, isError = false) { + const el = document.getElementById(id); if (!el) return; el.textContent = text; el.style.color = isError ? "var(--err)" : ""; } +function setHermesModelStatus(text, isError = false) { + setSettingsStatus("hermesModelStatus", text, isError); +} +function setHermesMcpStatus(text, isError = false) { + setSettingsStatus("hermesMcpStatus", text, isError); +} +function setHermesConfigStatuses(text, isError = false) { + setHermesModelStatus(text, isError); + setHermesMcpStatus(text, isError); +} async function refreshHermesConfig(force = false) { if (_hermesConfigLoading || (_hermesConfigLoaded && !force)) return; const modelEl = document.getElementById("hermesModelDefault"); if (!modelEl) return; _hermesConfigLoading = true; - setHermesConfigStatus("正在读取线上配置..."); + setHermesConfigStatuses("正在读取线上配置..."); try { const res = await fetch("/feishu/hermes-config", { credentials: "same-origin", @@ -622,63 +647,146 @@ async function refreshHermesConfig(force = false) { document.getElementById("hermesModelProvider").value = model.provider || ""; document.getElementById("hermesModelBaseUrl").value = model.base_url || ""; document.getElementById("mcpServersYaml").value = config.mcp_servers_yaml || ""; + _hermesConfigSnapshot = { + model: { + default: model.default || "", + provider: model.provider || "", + base_url: model.base_url || "", + }, + mcp_servers_yaml: config.mcp_servers_yaml || "", + }; if (model.default) syncModelPick(model.default); _hermesConfigLoaded = true; - setHermesConfigStatus("已读取线上配置" + (config.lxc ? " · " + config.lxc : "")); + const suffix = config.lxc ? " · " + config.lxc : ""; + setHermesModelStatus("已读取模型配置" + suffix); + setHermesMcpStatus("已读取 MCP 配置" + suffix); } catch (e) { - setHermesConfigStatus("读取失败: " + (e.message || e), true); + setHermesConfigStatuses("读取失败: " + (e.message || e), true); } finally { _hermesConfigLoading = false; } } -async function saveHermesConfig() { +function readModelConfigFields() { const modelDefault = document.getElementById("hermesModelDefault")?.value.trim() || ""; const provider = document.getElementById("hermesModelProvider")?.value.trim() || "openrouter"; const baseUrl = document.getElementById("hermesModelBaseUrl")?.value.trim() || ""; - const mcpServersYaml = document.getElementById("mcpServersYaml")?.value || ""; - if (!modelDefault) { + return { default: modelDefault, provider, base_url: baseUrl }; +} + +function snapshotModelOrFields() { + const model = _hermesConfigSnapshot?.model || {}; + if (model.default) return { ...model }; + const fields = readModelConfigFields(); + if (fields.default) return fields; + return { + default: state.model || "gemini-3-pro-preview", + provider: fields.provider || "openrouter", + base_url: fields.base_url || "", + }; +} + +async function postHermesRuntimeConfig(payload) { + const res = await fetch("/feishu/hermes-config", { + method: "POST", + credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status)); + return data.config || {}; +} + +async function saveModelConfig() { + const model = readModelConfigFields(); + if (!model.default) { toast("默认模型不能为空"); return; } - if (!confirm("保存后会重启线上 Hermes agent,当前正在生成的任务可能中断。继续吗?")) return; - const btn = document.getElementById("hermesConfigSaveBtn"); + if (!confirm("保存 AI 模型接入配置后会重启线上 Hermes agent,当前正在生成的任务可能中断。继续吗?")) return; + const btn = document.getElementById("hermesModelSaveBtn"); const oldHTML = btn?.innerHTML; if (btn) { btn.disabled = true; - btn.textContent = "保存并重启中..."; + btn.textContent = "保存模型中..."; } - setHermesConfigStatus("正在写入 config.yaml 并重启 Hermes agent..."); + setHermesModelStatus("正在写入 model 配置并重启 Hermes agent..."); try { - const res = await fetch("/feishu/hermes-config", { - method: "POST", - credentials: "same-origin", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: { - default: modelDefault, - provider, - base_url: baseUrl, - }, - mcp_servers_yaml: mcpServersYaml, - restart: true, - }), + const saved = await postHermesRuntimeConfig({ + model, + mcp_servers_yaml: _hermesConfigSnapshot ? _hermesConfigSnapshot.mcp_servers_yaml : (document.getElementById("mcpServersYaml")?.value || ""), + restart: true, }); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status)); - const saved = data.config || {}; const savedModel = saved.model || {}; if (savedModel.default) syncModelPick(savedModel.default); - document.getElementById("mcpServersYaml").value = saved.mcp_servers_yaml || ""; + _hermesConfigSnapshot = { + model: { + default: savedModel.default || model.default, + provider: savedModel.provider || model.provider, + base_url: savedModel.base_url || model.base_url, + }, + mcp_servers_yaml: _hermesConfigSnapshot ? _hermesConfigSnapshot.mcp_servers_yaml : (saved.mcp_servers_yaml || ""), + }; _hermesConfigLoaded = false; - setHermesConfigStatus("已保存并重启 · 备份 " + (saved.backup || "已创建")); - toast("模型与 MCP 配置已生效"); + setHermesModelStatus("模型配置已保存并重启 · 备份 " + (saved.backup || "已创建")); + toast("AI 模型接入配置已生效"); setTimeout(() => { pingBackend(); refreshDashboard(); }, 1800); } catch (e) { - setHermesConfigStatus("保存失败: " + (e.message || e), true); + setHermesModelStatus("保存失败: " + (e.message || e), true); + toast("保存失败: " + (e.message || e)); + } finally { + if (btn) { + btn.disabled = false; + btn.innerHTML = oldHTML; + } + } +} + +async function saveMcpConfig() { + const mcpServersYaml = document.getElementById("mcpServersYaml")?.value || ""; + const model = snapshotModelOrFields(); + if (!model.default) { + toast("先读取或填写默认模型"); + return; + } + if (!confirm("保存 MCP 工具接入配置后会重启线上 Hermes agent,当前正在生成的任务可能中断。继续吗?")) return; + const btn = document.getElementById("hermesMcpSaveBtn"); + const oldHTML = btn?.innerHTML; + if (btn) { + btn.disabled = true; + btn.textContent = "保存 MCP 中..."; + } + setHermesMcpStatus("正在写入 mcp_servers 配置并重启 Hermes agent..."); + try { + const saved = await postHermesRuntimeConfig({ + model, + mcp_servers_yaml: mcpServersYaml, + restart: true, + }); + const savedModel = saved.model || model; + if (savedModel.default) syncModelPick(savedModel.default); + document.getElementById("mcpServersYaml").value = saved.mcp_servers_yaml || ""; + _hermesConfigSnapshot = { + model: { + default: savedModel.default || model.default, + provider: savedModel.provider || model.provider, + base_url: savedModel.base_url || model.base_url, + }, + mcp_servers_yaml: saved.mcp_servers_yaml || "", + }; + _hermesConfigLoaded = false; + setHermesMcpStatus("MCP 配置已保存并重启 · 备份 " + (saved.backup || "已创建")); + toast("MCP 工具接入配置已生效"); + setTimeout(() => { + pingBackend(); + refreshDashboard(); + }, 1800); + } catch (e) { + setHermesMcpStatus("保存失败: " + (e.message || e), true); toast("保存失败: " + (e.message || e)); } finally { if (btn) { diff --git a/src/index.html b/src/index.html index d477c96..9df1be1 100644 --- a/src/index.html +++ b/src/index.html @@ -11,7 +11,7 @@ 爱马仕 · AI - + @@ -308,10 +308,17 @@
+ + + + + +
@@ -386,12 +393,8 @@
- - + +
@@ -1189,21 +1192,21 @@ git push # Gitea kangwan/hermes-glass-ui-personal
- +
-
模型与 MCP
-
线上 Hermes agent 的默认模型和 MCP server 配置
+
AI 模型接入
+
线上 Hermes agent 的默认模型、Provider 和模型网关地址
- +
@@ -1215,6 +1218,35 @@ git push # Gitea kangwan/hermes-glass-ui-personal
+
+
+ + +
+
+
+ + +
打开设置页后自动读取。
+
+
+
+ + +
+
+
+ +
+
+
MCP 工具接入
+
外部工具、知识库和服务调用配置
+
+
+