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
+
用于给不同智能体绑定不同模型接入信息。
+
+
+
+
+
+
@@ -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;
+}