diff --git a/.memory/worklog.json b/.memory/worklog.json index dddbb4f..5ecb7cf 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,12 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "3b0d2b2", - "message": "auto-save 2026-05-10 08:27 (~1)", - "ts": "2026-05-10T08:27:21+08:00", - "type": "commit" - }, { "files_changed": 1, "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 08:27 (~1)", @@ -3263,6 +3256,13 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 3 项未提交变更 · 最近提交:auto-save 2026-05-11 16:53 (~2)", "files_changed": 3 + }, + { + "ts": "2026-05-11T16:59:20+08:00", + "type": "commit", + "message": "auto-save 2026-05-11 16:59 (~3)", + "hash": "7a150ae", + "files_changed": 3 } ] } diff --git a/src/app.js b/src/app.js index a8e9b13..4432a62 100644 --- a/src/app.js +++ b/src/app.js @@ -1026,6 +1026,184 @@ async function refreshUiConfig(options = {}) { } } +function modelProfileCard(profile) { + const defaultBadge = profile.isDefault ? '默认' : ""; + const disabledBadge = profile.enabled ? "" : '停用'; + return ` +
+
+
${escapeHTML(profile.name)}${defaultBadge}${disabledBadge}
+
${escapeHTML(profile.model)}
+
+ ${escapeHTML(profile.provider || "auto")} + ${profile.baseUrl ? `${escapeHTML(profile.baseUrl)}` : ""} + ${profile.apiKeyRef ? `${escapeHTML(profile.apiKeyRef)}` : ""} +
+
+
+ + + + +
+
+ `; +} + +function renderModelProfiles() { + state.modelProfiles = normalizeModelProfiles(state.modelProfiles); + const list = document.getElementById("modelProfilesList"); + if (list) { + list.innerHTML = state.modelProfiles.map(modelProfileCard).join(""); + } + const active = activeModelProfile(); + const count = document.getElementById("modelProfilesCount"); + if (count) count.textContent = `${state.modelProfiles.length} 个模型 Profile · 默认 ${active?.name || "未设置"}`; + renderAgentModelProfileOptions(document.getElementById("agentModelProfile")?.value || ""); +} + +function clearModelProfileForm() { + state.editingModelProfileId = null; + const fields = { + modelProfileName: "", + modelProfileProvider: "openrouter", + modelProfileModel: "", + modelProfileBaseUrl: "", + modelProfileApiKeyRef: "", + }; + for (const [id, value] of Object.entries(fields)) { + const el = document.getElementById(id); + if (el) el.value = value; + } + const enabled = document.getElementById("modelProfileEnabled"); + const isDefault = document.getElementById("modelProfileDefault"); + if (enabled) enabled.checked = true; + if (isDefault) isDefault.checked = false; + const title = document.getElementById("modelProfileFormTitle"); + if (title) title.textContent = "新增模型 Profile"; +} + +function editModelProfile(id) { + const profile = modelProfileById(id); + if (!profile) return; + state.editingModelProfileId = id; + const values = { + modelProfileName: profile.name, + modelProfileProvider: profile.provider, + modelProfileModel: profile.model, + modelProfileBaseUrl: profile.baseUrl, + modelProfileApiKeyRef: profile.apiKeyRef, + }; + for (const [fieldId, value] of Object.entries(values)) { + const el = document.getElementById(fieldId); + if (el) el.value = value || ""; + } + const enabled = document.getElementById("modelProfileEnabled"); + const isDefault = document.getElementById("modelProfileDefault"); + if (enabled) enabled.checked = profile.enabled !== false; + if (isDefault) isDefault.checked = !!profile.isDefault; + const title = document.getElementById("modelProfileFormTitle"); + if (title) title.textContent = "编辑模型 Profile"; +} + +async function saveModelProfile() { + const name = document.getElementById("modelProfileName")?.value.trim() || ""; + const provider = document.getElementById("modelProfileProvider")?.value.trim() || "openrouter"; + const model = document.getElementById("modelProfileModel")?.value.trim() || ""; + const baseUrl = document.getElementById("modelProfileBaseUrl")?.value.trim() || ""; + const apiKeyRef = document.getElementById("modelProfileApiKeyRef")?.value.trim() || ""; + const enabled = document.getElementById("modelProfileEnabled")?.checked !== false; + const isDefault = !!document.getElementById("modelProfileDefault")?.checked; + if (!model) { + toast("请填写模型 ID"); + return; + } + if (baseUrl && !/^https?:\/\//i.test(baseUrl)) { + toast("Base URL 必须以 http:// 或 https:// 开头"); + return; + } + const id = state.editingModelProfileId || makeId("model"); + const existing = modelProfileById(id); + const next = normalizeModelProfile({ + id, + name: name || model, + provider, + model, + baseUrl, + apiKeyRef, + enabled, + isDefault, + createdAt: existing?.createdAt || Date.now(), + updatedAt: Date.now(), + }); + state.modelProfiles = state.modelProfiles.filter(profile => profile.id !== id); + if (next.isDefault) { + state.modelProfiles = state.modelProfiles.map(profile => ({ ...profile, isDefault: false })); + } + state.modelProfiles.push(next); + state.modelProfiles = normalizeModelProfiles(state.modelProfiles); + syncModelOptionsFromProfiles(); + renderModelProfiles(); + clearModelProfileForm(); + try { + await saveSharedConfig({ silent: true }); + toast("模型 Profile 已保存到服务器"); + } catch (error) { + setSharedConfigStatus("模型 Profile 保存失败: " + (error.message || error), true); + toast("模型 Profile 暂存本机,服务器保存失败"); + } +} + +async function makeModelProfileDefault(id) { + const profile = modelProfileById(id); + if (!profile) return; + state.modelProfiles = normalizeModelProfiles(state.modelProfiles.map(item => ({ + ...item, + isDefault: item.id === id, + }))); + syncModelOptionsFromProfiles(); + renderModelProfiles(); + try { + await saveSharedConfig({ silent: true }); + toast("默认模型 Profile 已更新"); + } catch (error) { + setSharedConfigStatus("默认模型 Profile 保存失败: " + (error.message || error), true); + } +} + +function applyModelProfileToRuntime(id) { + const profile = modelProfileById(id); + if (!profile) return; + document.getElementById("hermesModelDefault").value = profile.model || ""; + document.getElementById("hermesModelProvider").value = profile.provider || ""; + document.getElementById("hermesModelBaseUrl").value = profile.baseUrl || ""; + toast("已填入运行配置,点击“保存模型并重启”后生效"); +} + +async function deleteModelProfile(id) { + if (state.modelProfiles.length <= 1) { + toast("至少保留一个模型 Profile"); + return; + } + if (!confirm("删除这个模型 Profile? 已绑定的智能体会回退到模型 ID。")) return; + const wasDefault = modelProfileById(id)?.isDefault; + state.modelProfiles = state.modelProfiles.filter(profile => profile.id !== id); + if (wasDefault && state.modelProfiles[0]) state.modelProfiles[0].isDefault = true; + for (const agent of Object.values(state.agents)) { + if (agent.modelProfileId === id) agent.modelProfileId = ""; + } + state.modelProfiles = normalizeModelProfiles(state.modelProfiles); + syncModelOptionsFromProfiles(); + renderModelProfiles(); + renderAgents(); + try { + await saveSharedConfig({ silent: true }); + toast("模型 Profile 已删除"); + } catch (error) { + setSharedConfigStatus("模型 Profile 删除保存失败: " + (error.message || error), true); + } +} + async function saveModelConfig() { const model = readModelConfigFields(); if (!model.default) { @@ -1047,7 +1225,7 @@ async function saveModelConfig() { restart: true, }); const savedModel = saved.model || {}; - if (savedModel.default) syncModelPick(savedModel.default, savedModel.provider || model.provider || ""); + if (savedModel.default) syncModelPick(savedModel.default, savedModel.provider || ""); _hermesConfigSnapshot = { model: { default: savedModel.default || model.default, @@ -4159,6 +4337,7 @@ function exportData() { conversations: state.conversations, weeklyReports: state.weeklyReports, agents: state.agents, + modelProfiles: normalizeModelProfiles(state.modelProfiles), customSkills: state.customSkills, flows: state.flows, theme: localStorage.getItem(LS_THEME) || "dark", @@ -4189,6 +4368,7 @@ function importData(event) { saveWeeklyReports(); } if (data.agents) state.agents = data.agents; + if (Array.isArray(data.modelProfiles)) state.modelProfiles = normalizeModelProfiles(data.modelProfiles); if (data.customSkills) { state.customSkills = data.customSkills; saveCustomSkillsToLS(); } if (data.flows) { state.flows = data.flows; saveFlowsToLS(); } if (data.settings) { @@ -4208,6 +4388,7 @@ function importData(event) { } saveConversations(); saveAgents(); + saveSharedConfig({ silent: true }).catch(() => {}); // 重新挑一条活动对话 const ids = sortedConvoIds(); state.activeId = ids[0] || null; @@ -4215,6 +4396,7 @@ function importData(event) { renderSidebar(); renderChat(); renderAgents(); + renderModelProfiles(); refreshDashboard(); toast("已导入"); } catch (e) { diff --git a/src/index.html b/src/index.html index f505e30..ecefdbb 100644 --- a/src/index.html +++ b/src/index.html @@ -396,6 +396,13 @@ +
+ + +
Profile 记录 provider / base URL / API Key 引用;当前请求仍按模型 ID 发给 Hermes API。
+
@@ -1200,7 +1207,7 @@ git push # Gitea kangwan/hermes-glass-ui-personal
AI 模型接入
-
线上 Hermes agent 的默认模型、Provider 和模型网关地址
+
AI 模型 Profiles 与线上 Hermes agent 默认运行模型
@@ -1232,6 +1239,52 @@ git push # Gitea kangwan/hermes-glass-ui-personal
打开设置页后自动读取。
+
+
+
+
模型 Profiles
+
用于给不同智能体绑定不同模型接入信息。
+
+ +
+
+
正在读取服务器共享配置...
+
+
+
新增模型 Profile
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
共享配置会保存到服务器,供同事端同步。
+
+
+
@@ -1253,7 +1306,7 @@ git push # Gitea kangwan/hermes-glass-ui-personal time: command: uvx args: ["mcp-server-time"]'> -
留空会移除 mcp_servers;保存会备份配置并重启 hermes-agent
+
留空会移除 mcp_servers;保存只改工具接入,不再要求同时填写模型配置。
diff --git a/src/styles.css b/src/styles.css index c01100a..af7bd2f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2532,6 +2532,7 @@ 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 select, .settings-field textarea { width: 100%; max-width: 100%; @@ -2558,10 +2559,142 @@ a { color: var(--orange-3); text-decoration: none; } font-family: "SF Mono", ui-monospace, Menlo, monospace; } .settings-field input:focus, +.settings-field select:focus, .settings-field textarea:focus { border-color: var(--orange); box-shadow: 0 0 0 3px rgba(255,105,0,0.12); } +.settings-subpanel { + border: 1px solid var(--line); + border-radius: 12px; + background: rgba(255,255,255,0.025); + padding: 14px; + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; +} +.settings-subpanel-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + min-width: 0; +} +.settings-subtitle { + font-size: 13px; + font-weight: 800; + color: var(--text); + line-height: 1.35; +} +.model-profile-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 10px; +} +.model-profile-card { + border: 1px solid var(--line); + border-radius: 10px; + background: rgba(255,255,255,0.035); + padding: 12px; + min-width: 0; + display: flex; + flex-direction: column; + gap: 10px; +} +.model-profile-card.active { + border-color: rgba(255,122,26,0.55); + background: rgba(255,122,26,0.08); +} +.model-profile-main { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} +.model-profile-name { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; + font-size: 13px; + font-weight: 800; + color: var(--text); +} +.model-profile-model { + font-family: "SF Mono", ui-monospace, Menlo, monospace; + font-size: 12px; + color: var(--text-dim); + overflow-wrap: anywhere; +} +.model-profile-meta { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.model-profile-meta span, +.model-profile-badge { + border: 1px solid var(--line); + border-radius: 999px; + padding: 3px 7px; + font-size: 10px; + color: var(--text-dim2); + max-width: 100%; + overflow-wrap: anywhere; +} +.model-profile-badge { + color: #1a0f08; + background: var(--orange); + border-color: transparent; +} +.model-profile-badge.off { + color: var(--text-dim2); + background: rgba(255,255,255,0.06); + border-color: var(--line); +} +.model-profile-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.model-profile-actions button { + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(255,255,255,0.04); + color: var(--text-dim); + padding: 6px 9px; + cursor: pointer; + font-size: 11px; +} +.model-profile-actions button:hover { + color: var(--text); + border-color: var(--line-strong); +} +.model-profile-actions button.danger:hover { + color: var(--err); + border-color: rgba(255,93,122,0.45); +} +.model-profile-form { + border-top: 1px solid var(--line); + padding-top: 14px; + display: flex; + flex-direction: column; + gap: 12px; +} +.model-profile-checks { + justify-content: center; +} +.settings-checkline { + display: flex; + align-items: center; + gap: 8px; + min-height: 32px; + color: var(--text-dim); + cursor: pointer; +} +.settings-checkline input { + width: auto; +} .settings-help { font-size: 11px; color: var(--text-dim2); @@ -4041,3 +4174,49 @@ a { color: var(--orange-3); text-decoration: none; } justify-content: stretch; } } + +.boot-issue { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 9999; + width: min(420px, calc(100vw - 28px)); + max-height: min(420px, calc(100vh - 28px)); + overflow: auto; + padding: 14px; + border: 1px solid rgba(255,93,122,0.45); + border-radius: 12px; + background: rgba(22, 24, 28, 0.96); + box-shadow: 0 18px 60px rgba(0,0,0,0.35); + color: var(--text); +} +.boot-issue-title { + font-size: 13px; + font-weight: 800; + color: var(--err); + margin-bottom: 6px; +} +.boot-issue-msg { + font-size: 12px; + line-height: 1.5; + color: var(--text-dim); + overflow-wrap: anywhere; +} +.boot-issue button { + margin-top: 10px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(255,255,255,0.05); + color: var(--text); + padding: 7px 10px; + cursor: pointer; +} +.boot-issue details { + margin-top: 10px; + font-size: 11px; + color: var(--text-dim2); +} +.boot-issue pre { + white-space: pre-wrap; + overflow-wrap: anywhere; +}