diff --git a/.memory/worklog.json b/.memory/worklog.json index 41cd316..343be68 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,11 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 06:46 (~1)", - "ts": "2026-05-09T22:48:29Z", - "type": "session-heartbeat" - }, { "files_changed": 1, "hash": "e433c1c", @@ -3256,6 +3250,13 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-11 14:45 (~1)", "files_changed": 1 + }, + { + "ts": "2026-05-11T14:50:39+08:00", + "type": "commit", + "message": "auto-save 2026-05-11 14:50 (~4)", + "hash": "d95aed8", + "files_changed": 4 } ] } diff --git a/RULES.md b/RULES.md index e63161c..efe7fdb 100644 --- a/RULES.md +++ b/RULES.md @@ -19,7 +19,8 @@ - 爱马仕前端「仪表盘」同步了上游 Hermes 的快捷入口板块,个人版展示主站、API、飞书机器人列表、文档/解析入口 - 爱马仕前端「仪表盘」活动热力图已重做为带摘要、月份标尺、紧凑格子和细分色阶的活动卡片 - 爱马仕前端「设置 → 连接」可自助维护 API 地址 / API Key 并测试连接;「对话 → 存周报」和「设置 → 周报记录」会在本地保存任务描述、上下文片段和最终周报 -- 当前前端静态壳缓存版本:`hermes-ui-v17` +- 爱马仕前端「设置 → 模型与 MCP」可读取 / 写入 LXC 内 `/opt/hermes-agent/config.yaml`,保存后重启 Docker `hermes-agent` +- 当前前端静态壳缓存版本:`hermes-ui-v18` - 文档 / 解析:https://styles.kang-kang.com - 管理后台:待定 - 代码仓:https://git.kang-kang.com/kangwan/hermes-glass-ui-personal @@ -63,5 +64,6 @@ - 线上飞书桥接环境:`/etc/hermes-feishu-bridge.env`,mode 600 - 飞书后台配置所需回调 URL、verification token、notify token 备份:`/root/hermes-feishu-bridge.tokens`,mode 600 - 飞书自助配置接口 `POST /feishu/apps`、`DELETE /feishu/apps/{app_id}` 要求已登录爱马仕 cookie 且同源请求;未登录公网请求返回 401 +- Hermes 运行配置接口 `/feishu/hermes-config` 复用飞书桥接反代,要求已登录爱马仕 cookie 且同源请求;会通过 Incus 写入 LXC 配置并重启 `hermes-agent` - 当前飞书桥接版本按明文事件回调处理;如果飞书后台开启事件加密,需要先补充解密支持 - 主站有 cookie 门禁;nginx 已对 `/feishu/` 单独放行并反代到飞书桥接服务 diff --git a/src/app.js b/src/app.js index 0ef7b5d..2e7d49a 100644 --- a/src/app.js +++ b/src/app.js @@ -594,6 +594,100 @@ async function testApiConnection() { } } +let _hermesConfigLoaded = false; +let _hermesConfigLoading = false; +function setHermesConfigStatus(text, isError = false) { + const el = document.getElementById("hermesConfigStatus"); + if (!el) return; + el.textContent = text; + el.style.color = isError ? "var(--err)" : ""; +} + +async function refreshHermesConfig(force = false) { + if (_hermesConfigLoading || (_hermesConfigLoaded && !force)) return; + const modelEl = document.getElementById("hermesModelDefault"); + if (!modelEl) return; + _hermesConfigLoading = true; + setHermesConfigStatus("正在读取线上配置..."); + try { + const res = await fetch("/feishu/hermes-config", { + credentials: "same-origin", + cache: "no-store", + }); + const data = await res.json().catch(() => ({})); + if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status)); + const config = data.config || {}; + const model = config.model || {}; + document.getElementById("hermesModelDefault").value = model.default || ""; + document.getElementById("hermesModelProvider").value = model.provider || ""; + document.getElementById("hermesModelBaseUrl").value = model.base_url || ""; + document.getElementById("mcpServersYaml").value = config.mcp_servers_yaml || ""; + if (model.default) syncModelPick(model.default); + _hermesConfigLoaded = true; + setHermesConfigStatus("已读取线上配置" + (config.lxc ? " · " + config.lxc : "")); + } catch (e) { + setHermesConfigStatus("读取失败: " + (e.message || e), true); + } finally { + _hermesConfigLoading = false; + } +} + +async function saveHermesConfig() { + 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) { + toast("默认模型不能为空"); + return; + } + if (!confirm("保存后会重启线上 Hermes agent,当前正在生成的任务可能中断。继续吗?")) return; + const btn = document.getElementById("hermesConfigSaveBtn"); + const oldHTML = btn?.innerHTML; + if (btn) { + btn.disabled = true; + btn.textContent = "保存并重启中..."; + } + setHermesConfigStatus("正在写入 config.yaml 并重启 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 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 || ""; + _hermesConfigLoaded = false; + setHermesConfigStatus("已保存并重启 · 备份 " + (saved.backup || "已创建")); + toast("模型与 MCP 配置已生效"); + setTimeout(() => { + pingBackend(); + refreshDashboard(); + }, 1800); + } catch (e) { + setHermesConfigStatus("保存失败: " + (e.message || e), true); + toast("保存失败: " + (e.message || e)); + } finally { + if (btn) { + btn.disabled = false; + btn.innerHTML = oldHTML; + } + } +} + async function fetchIP() { const el = document.getElementById("statIP"); if (el) el.textContent = location.hostname; diff --git a/src/styles.css b/src/styles.css index 438ce3a..f9eeb53 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2504,6 +2504,14 @@ a { color: var(--orange-3); text-decoration: none; } gap: 16px; min-width: 0; } +.settings-grid-3 { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} +@media (max-width: 900px) { + .settings-grid-3 { grid-template-columns: 1fr; } +} .settings-field { display: flex; @@ -2526,7 +2534,8 @@ a { color: var(--orange-3); text-decoration: none; } } .settings-field.toggle-field > div:first-child { flex: 1 1 200px; min-width: 0; } .settings-field input[type="text"], -.settings-field input[type="password"] { +.settings-field input[type="password"], +.settings-field textarea { width: 100%; max-width: 100%; box-sizing: border-box; @@ -2540,7 +2549,14 @@ a { color: var(--orange-3); text-decoration: none; } outline: none; transition: border-color 0.2s, box-shadow 0.2s; } -.settings-field input:focus { +.settings-field textarea { + min-height: 156px; + resize: vertical; + line-height: 1.55; + font-family: "SF Mono", ui-monospace, Menlo, monospace; +} +.settings-field input:focus, +.settings-field textarea:focus { border-color: var(--orange); box-shadow: 0 0 0 3px rgba(255,105,0,0.12); } diff --git a/src/sw.js b/src/sw.js index 9c4d179..af096a0 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,6 +1,6 @@ // 爱马仕 Hermes · 轻量 Service Worker // 静态壳走 network-first(拿不到再回退缓存),API 直通 -const CACHE = "hermes-ui-v17"; +const CACHE = "hermes-ui-v18"; const ASSETS = [ "./", "./index.html",