auto-save 2026-05-11 17:04 (~4)
This commit is contained in:
184
src/app.js
184
src/app.js
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
179
src/styles.css
179
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user