auto-save 2026-05-11 17:04 (~4)

This commit is contained in:
2026-05-11 17:04:53 +08:00
parent 7a150ae74c
commit 60c44cfaae
4 changed files with 424 additions and 10 deletions

View File

@@ -1026,6 +1026,184 @@ async function refreshUiConfig(options = {}) {
}
}
function modelProfileCard(profile) {
const defaultBadge = profile.isDefault ? '<span class="model-profile-badge">默认</span>' : "";
const disabledBadge = profile.enabled ? "" : '<span class="model-profile-badge off">停用</span>';
return `
<div class="model-profile-card ${profile.isDefault ? "active" : ""}">
<div class="model-profile-main">
<div class="model-profile-name">${escapeHTML(profile.name)}${defaultBadge}${disabledBadge}</div>
<div class="model-profile-model">${escapeHTML(profile.model)}</div>
<div class="model-profile-meta">
<span>${escapeHTML(profile.provider || "auto")}</span>
${profile.baseUrl ? `<span>${escapeHTML(profile.baseUrl)}</span>` : ""}
${profile.apiKeyRef ? `<span>${escapeHTML(profile.apiKeyRef)}</span>` : ""}
</div>
</div>
<div class="model-profile-actions">
<button type="button" onclick="editModelProfile('${escapeHTML(profile.id)}')">编辑</button>
<button type="button" onclick="makeModelProfileDefault('${escapeHTML(profile.id)}')">设默认</button>
<button type="button" onclick="applyModelProfileToRuntime('${escapeHTML(profile.id)}')">应用到运行配置</button>
<button type="button" class="danger" onclick="deleteModelProfile('${escapeHTML(profile.id)}')">删除</button>
</div>
</div>
`;
}
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) {

View File

@@ -396,6 +396,13 @@
<label>模型 ID</label>
<input type="text" id="agentModel" list="modelOptions" placeholder="google/gemini-3.1-pro-preview" autocomplete="off">
</div>
<div class="setting-row setting-row-full">
<label>模型 Profile</label>
<select id="agentModelProfile" onchange="applyAgentModelProfileSelection()">
<option value="">跟随对话默认 / 仅使用模型 ID</option>
</select>
<div class="settings-help">Profile 记录 provider / base URL / API Key 引用;当前请求仍按模型 ID 发给 Hermes API。</div>
</div>
<div class="setting-row setting-row-full">
<label>角色设定 (System Prompt)</label>
<textarea id="agentPrompt" rows="5" placeholder="告诉爱马仕这个智能体的角色、语气、规则、能力边界..."></textarea>
@@ -1200,7 +1207,7 @@ git push # Gitea kangwan/hermes-glass-ui-personal
</div>
<div>
<div class="settings-group-title">AI 模型接入</div>
<div class="settings-group-desc">线上 Hermes agent 默认模型、Provider 和模型网关地址</div>
<div class="settings-group-desc">AI 模型 Profiles 与线上 Hermes agent 默认运行模型</div>
</div>
</div>
<div class="settings-group-body">
@@ -1232,6 +1239,52 @@ git push # Gitea kangwan/hermes-glass-ui-personal
</button>
<div class="settings-help" id="hermesModelStatus">打开设置页后自动读取。</div>
</div>
<div class="settings-subpanel">
<div class="settings-subpanel-head">
<div>
<div class="settings-subtitle">模型 Profiles</div>
<div class="settings-help" id="modelProfilesCount">用于给不同智能体绑定不同模型接入信息。</div>
</div>
<button class="glass-btn-sm" type="button" onclick="clearModelProfileForm()">新增 Profile</button>
</div>
<div class="model-profile-list" id="modelProfilesList">
<div class="settings-help">正在读取服务器共享配置...</div>
</div>
<div class="model-profile-form">
<div class="settings-subtitle" id="modelProfileFormTitle">新增模型 Profile</div>
<div class="settings-grid-3">
<div class="settings-field">
<label for="modelProfileName">显示名</label>
<input type="text" id="modelProfileName" placeholder="例如 OpenRouter Gemini" autocomplete="off">
</div>
<div class="settings-field">
<label for="modelProfileProvider">Provider</label>
<input type="text" id="modelProfileProvider" placeholder="openrouter / openai / anthropic" autocomplete="off">
</div>
<div class="settings-field">
<label for="modelProfileModel">模型 ID</label>
<input type="text" id="modelProfileModel" placeholder="google/gemini-3.1-pro-preview" autocomplete="off">
</div>
<div class="settings-field">
<label for="modelProfileBaseUrl">Base URL</label>
<input type="text" id="modelProfileBaseUrl" placeholder="https://openrouter.ai/api/v1" autocomplete="off">
</div>
<div class="settings-field">
<label for="modelProfileApiKeyRef">API Key 引用</label>
<input type="text" id="modelProfileApiKeyRef" placeholder="OPENROUTER_API_KEY / 服务器环境变量" autocomplete="off">
</div>
<div class="settings-field model-profile-checks">
<label class="settings-checkline"><input type="checkbox" id="modelProfileEnabled" checked> 启用</label>
<label class="settings-checkline"><input type="checkbox" id="modelProfileDefault"> 作为默认</label>
</div>
</div>
<div class="settings-actions">
<button class="glass-btn-sm primary" type="button" onclick="saveModelProfile()">保存 Profile</button>
<button class="glass-btn-sm" type="button" onclick="clearModelProfileForm()">清空表单</button>
<div class="settings-help" id="sharedConfigStatus">共享配置会保存到服务器,供同事端同步。</div>
</div>
</div>
</div>
</div>
</div>
@@ -1253,7 +1306,7 @@ git push # Gitea kangwan/hermes-glass-ui-personal
time:
command: uvx
args: ["mcp-server-time"]'></textarea>
<div class="settings-help">留空会移除 <code>mcp_servers</code>;保存会备份配置并重启 <code>hermes-agent</code></div>
<div class="settings-help">留空会移除 <code>mcp_servers</code>;保存只改工具接入,不再要求同时填写模型配置</div>
</div>
<div class="settings-actions">
<button class="glass-btn-sm" onclick="refreshHermesConfig(true)">读取 MCP 配置</button>

View File

@@ -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;
}