auto-save 2026-05-11 19:15 (~5)

This commit is contained in:
2026-05-11 19:15:34 +08:00
parent 77d800dcac
commit c78f112685
5 changed files with 324 additions and 10 deletions

View File

@@ -1,12 +1,5 @@
{ {
"entries": [ "entries": [
{
"files_changed": 1,
"hash": "a4b2ec6",
"message": "auto-save 2026-05-10 10:13 (~1)",
"ts": "2026-05-10T10:13:19+08:00",
"type": "commit"
},
{ {
"files_changed": 1, "files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-10 10:13 (~1)", "message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-10 10:13 (~1)",
@@ -3271,6 +3264,13 @@
"type": "session-heartbeat", "type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-11 19:04 (~1)", "message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-11 19:04 (~1)",
"files_changed": 1 "files_changed": 1
},
{
"ts": "2026-05-11T19:10:01+08:00",
"type": "commit",
"message": "auto-save 2026-05-11 19:09 (~1)",
"hash": "77d800d",
"files_changed": 1
} }
] ]
} }

View File

@@ -21,6 +21,7 @@
- 爱马仕前端「设置 → 连接」可自助维护 API 地址 / API Key 并测试连接;「对话 → 存周报」和「设置 → 周报记录」会在本地保存任务描述、上下文片段和最终周报 - 爱马仕前端「设置 → 连接」可自助维护 API 地址 / API Key 并测试连接;「对话 → 存周报」和「设置 → 周报记录」会在本地保存任务描述、上下文片段和最终周报
- 爱马仕前端「模型」可维护 AI 模型 Profiles用于给不同 Agent 绑定不同模型、Provider、Base URL 和服务器端 Key 引用 - 爱马仕前端「模型」可维护 AI 模型 Profiles用于给不同 Agent 绑定不同模型、Provider、Base URL 和服务器端 Key 引用
- 爱马仕前端「提供商」可维护 LXC 内 `/opt/hermes-agent/config.yaml``model` 块,保存后重启 Docker `hermes-agent` - 爱马仕前端「提供商」可维护 LXC 内 `/opt/hermes-agent/config.yaml``model` 块,保存后重启 Docker `hermes-agent`
- 爱马仕前端「提供商 → Provider API Keys」可写入 / 清除服务器环境变量里的 AI Provider Key真实 Key 不回显、不入库
- 爱马仕前端「工具 → MCP 工具接入」可维护 LXC 内 `/opt/hermes-agent/config.yaml``mcp_servers` 块,保存后重启 Docker `hermes-agent` - 爱马仕前端「工具 → MCP 工具接入」可维护 LXC 内 `/opt/hermes-agent/config.yaml``mcp_servers` 块,保存后重启 Docker `hermes-agent`
- 飞书、模型、MCP、共享 Agent/Profile 等 `/feishu/*` 管理接口复用网页登录 cookie并在前端 401 时走 `/_auth/verify` 静默续期 - 飞书、模型、MCP、共享 Agent/Profile 等 `/feishu/*` 管理接口复用网页登录 cookie并在前端 401 时走 `/_auth/verify` 静默续期
- 当前前端不再启用 Service Worker 静态壳缓存;`sw.js` 仅用于清理旧 `hermes-ui-*` 缓存并注销旧注册 - 当前前端不再启用 Service Worker 静态壳缓存;`sw.js` 仅用于清理旧 `hermes-ui-*` 缓存并注销旧注册
@@ -68,5 +69,6 @@
- 飞书后台配置所需回调 URL、verification token、notify token 备份:`/root/hermes-feishu-bridge.tokens`mode 600 - 飞书后台配置所需回调 URL、verification token、notify token 备份:`/root/hermes-feishu-bridge.tokens`mode 600
- 飞书自助配置接口 `POST /feishu/apps``DELETE /feishu/apps/{app_id}` 要求已登录爱马仕 cookie 且同源请求;未登录公网请求返回 401后台 Origin 白名单默认包含当前请求 Host可用 `HERMES_ADMIN_ORIGINS` 扩展 - 飞书自助配置接口 `POST /feishu/apps``DELETE /feishu/apps/{app_id}` 要求已登录爱马仕 cookie 且同源请求;未登录公网请求返回 401后台 Origin 白名单默认包含当前请求 Host可用 `HERMES_ADMIN_ORIGINS` 扩展
- Hermes 运行配置接口 `/feishu/hermes-config` 复用飞书桥接反代,要求已登录爱马仕 cookie 且同源请求;会通过 Incus 写入 LXC 配置并重启 `hermes-agent` - Hermes 运行配置接口 `/feishu/hermes-config` 复用飞书桥接反代,要求已登录爱马仕 cookie 且同源请求;会通过 Incus 写入 LXC 配置并重启 `hermes-agent`
- Provider Key 管理接口 `/feishu/provider-keys` 复用飞书桥接反代,要求已登录爱马仕 cookie 且同源请求;只允许管理形如 `*_API_KEY` 的 Provider 环境变量,禁止管理飞书和部署控制变量
- 当前飞书桥接版本按明文事件回调处理;如果飞书后台开启事件加密,需要先补充解密支持 - 当前飞书桥接版本按明文事件回调处理;如果飞书后台开启事件加密,需要先补充解密支持
- 主站有 cookie 门禁nginx 已对 `/feishu/` 单独放行并反代到飞书桥接服务 - 主站有 cookie 门禁nginx 已对 `/feishu/` 单独放行并反代到飞书桥接服务

View File

@@ -228,6 +228,9 @@ def reload_config_from_env_file() -> None:
Config.feishu_app_secret = _env("FEISHU_APP_SECRET") Config.feishu_app_secret = _env("FEISHU_APP_SECRET")
Config.feishu_apps = _load_feishu_apps() Config.feishu_apps = _load_feishu_apps()
Config.default_feishu_app_id = _env("FEISHU_DEFAULT_APP_ID", Config.feishu_app_id) Config.default_feishu_app_id = _env("FEISHU_DEFAULT_APP_ID", Config.feishu_app_id)
Config.hermes_api_base = _env("HERMES_API_BASE", Config.hermes_api_base).rstrip("/")
Config.hermes_api_key = _env("HERMES_API_KEY")
Config.hermes_model = _env("HERMES_MODEL", Config.hermes_model)
def sync_tokens_file() -> None: def sync_tokens_file() -> None:
@@ -1140,6 +1143,125 @@ def is_admin_request(headers: dict[str, str]) -> tuple[bool, int, str]:
return True, 200, "ok" return True, 200, "ok"
COMMON_PROVIDER_KEYS = [
"HERMES_API_KEY",
"OPENROUTER_API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"GOOGLE_API_KEY",
"GEMINI_API_KEY",
"DEEPSEEK_API_KEY",
"XAI_API_KEY",
"GROQ_API_KEY",
"DASHSCOPE_API_KEY",
]
def provider_env_key(raw: Any) -> str:
key = str(raw or "").strip()
if key.startswith("env:"):
key = key[4:].strip()
if not re.match(r"^[A-Z][A-Z0-9_]{1,120}$", key):
return ""
return key
def validate_provider_env_key(raw: Any) -> str:
key = provider_env_key(raw)
if not key:
raise ValueError("env key must look like OPENROUTER_API_KEY")
blocked_prefixes = ("FEISHU_", "HERMES_ADMIN_", "FEISHU_BRIDGE_")
blocked_keys = {"PORT", "PATH", "HOME", "SHELL", "PWD"}
if key in blocked_keys or key.startswith(blocked_prefixes):
raise ValueError("this env key is managed by deployment, not by provider settings")
if not key.endswith("_API_KEY"):
raise ValueError("provider env key must end with _API_KEY")
return key
def provider_key_profiles() -> dict[str, list[str]]:
result: dict[str, list[str]] = {}
try:
config = read_ui_config_file()
except Exception:
logging.warning("provider key profile map unavailable:\n%s", traceback.format_exc())
return result
profiles = config.get("modelProfiles") if isinstance(config.get("modelProfiles"), list) else []
for profile in profiles:
if not isinstance(profile, dict):
continue
key = provider_env_key(profile.get("apiKeyRef"))
if not key:
continue
name = str(profile.get("name") or profile.get("model") or "Profile")[:80]
result.setdefault(key, []).append(name)
return result
def handle_provider_keys_get(headers: dict[str, str]) -> tuple[int, dict[str, Any]]:
ok, status, message = is_admin_request(headers)
if not ok:
return status, {"code": status, "msg": message}
try:
env = parse_env_file(Config.env_file)
profile_map = provider_key_profiles()
keys = set(COMMON_PROVIDER_KEYS) | set(profile_map.keys())
payload = []
for key in sorted(keys):
present = bool(os.environ.get(key) or env.get(key))
payload.append({
"key": key,
"present": present,
"profiles": profile_map.get(key, []),
})
return 200, {"code": 0, "msg": "ok", "keys": payload}
except Exception as exc:
logging.error("failed to read provider keys:\n%s", traceback.format_exc())
return 500, {"code": 500, "msg": str(exc)}
def handle_provider_keys_post(headers: dict[str, str], body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
ok, status, message = is_admin_request(headers)
if not ok:
return status, {"code": status, "msg": message}
try:
key = validate_provider_env_key(body.get("key"))
value = str(body.get("value") or body.get("api_key") or "").strip()
if not value:
raise ValueError("api key value is required")
if "\n" in value or "\r" in value:
raise ValueError("api key value must be a single line")
if len(value) > 10000:
raise ValueError("api key value is too large")
write_env_updates(Config.env_file, {key: value})
reload_config_from_env_file()
logging.info("updated provider env key %s", key)
return 200, {"code": 0, "msg": "ok", "key": {"key": key, "present": True}}
except ValueError as exc:
return 400, {"code": 400, "msg": str(exc)}
except Exception as exc:
logging.error("failed to write provider key:\n%s", traceback.format_exc())
return 500, {"code": 500, "msg": str(exc)}
def handle_provider_keys_delete(path: str, headers: dict[str, str]) -> tuple[int, dict[str, Any]]:
ok, status, message = is_admin_request(headers)
if not ok:
return status, {"code": status, "msg": message}
try:
raw_key = urllib.parse.unquote(path[len("/feishu/provider-keys/") :].strip("/"))
key = validate_provider_env_key(raw_key)
write_env_removals(Config.env_file, {key})
reload_config_from_env_file()
logging.info("removed provider env key %s", key)
return 200, {"code": 0, "msg": "ok", "key": {"key": key, "present": False}}
except ValueError as exc:
return 400, {"code": 400, "msg": str(exc)}
except Exception as exc:
logging.error("failed to remove provider key:\n%s", traceback.format_exc())
return 500, {"code": 500, "msg": str(exc)}
def validate_feishu_credentials(app_id: str, app_secret: str) -> None: def validate_feishu_credentials(app_id: str, app_secret: str) -> None:
data = http_json( data = http_json(
"POST", "POST",
@@ -1348,6 +1470,10 @@ class Handler(BaseHTTPRequestHandler):
status, payload = handle_ui_config_get(headers) status, payload = handle_ui_config_get(headers)
self.send_json(status, payload) self.send_json(status, payload)
return return
if self.path == "/feishu/provider-keys":
status, payload = handle_provider_keys_get(headers)
self.send_json(status, payload)
return
self.send_json(404, {"code": 404, "msg": "not found"}) self.send_json(404, {"code": 404, "msg": "not found"})
def do_POST(self) -> None: def do_POST(self) -> None:
@@ -1362,6 +1488,8 @@ class Handler(BaseHTTPRequestHandler):
status, payload = handle_hermes_config_post(headers, body) status, payload = handle_hermes_config_post(headers, body)
elif self.path == "/feishu/ui-config": elif self.path == "/feishu/ui-config":
status, payload = handle_ui_config_post(headers, body) status, payload = handle_ui_config_post(headers, body)
elif self.path == "/feishu/provider-keys":
status, payload = handle_provider_keys_post(headers, body)
elif self.path == "/feishu/chat/completions": elif self.path == "/feishu/chat/completions":
ok, status, message = is_admin_request(headers) ok, status, message = is_admin_request(headers)
if not ok: if not ok:
@@ -1383,6 +1511,8 @@ class Handler(BaseHTTPRequestHandler):
headers = {key.lower(): value for key, value in self.headers.items()} headers = {key.lower(): value for key, value in self.headers.items()}
if self.path.startswith("/feishu/apps/"): if self.path.startswith("/feishu/apps/"):
status, payload = handle_apps_delete(self.path, headers) status, payload = handle_apps_delete(self.path, headers)
elif self.path.startswith("/feishu/provider-keys/"):
status, payload = handle_provider_keys_delete(self.path, headers)
else: else:
status, payload = 404, {"code": 404, "msg": "not found"} status, payload = 404, {"code": 404, "msg": "not found"}
except Exception as exc: except Exception as exc:

View File

@@ -13,6 +13,7 @@ const LS_TAB = "hermes-ui-active-tab-v1";
const LS_WEEKLY_REPORTS = "hermes-ui-weekly-reports-v1"; const LS_WEEKLY_REPORTS = "hermes-ui-weekly-reports-v1";
const LS_MODEL_PROFILES = "hermes-ui-model-profiles-v1"; const LS_MODEL_PROFILES = "hermes-ui-model-profiles-v1";
const UI_CONFIG_ENDPOINT = "/feishu/ui-config"; const UI_CONFIG_ENDPOINT = "/feishu/ui-config";
const PROVIDER_KEYS_ENDPOINT = "/feishu/provider-keys";
const DEFAULT_MODEL_ID = "google/gemini-3.1-pro-preview"; const DEFAULT_MODEL_ID = "google/gemini-3.1-pro-preview";
const LEGACY_DEFAULT_MODEL_ID = "gemini-3-pro-preview"; const LEGACY_DEFAULT_MODEL_ID = "gemini-3-pro-preview";
@@ -666,6 +667,7 @@ function switchTab(name, options = {}) {
} }
if (name === "providers") { if (name === "providers") {
refreshHermesConfig(); refreshHermesConfig();
refreshProviderKeys();
} }
if (name === "tools") { if (name === "tools") {
refreshTools(); refreshTools();
@@ -894,6 +896,8 @@ async function testApiConnection() {
let _hermesConfigLoaded = false; let _hermesConfigLoaded = false;
let _hermesConfigLoading = false; let _hermesConfigLoading = false;
let _hermesConfigSnapshot = null; let _hermesConfigSnapshot = null;
let _providerKeysLoaded = false;
let _providerKeysLoading = false;
function setSettingsStatus(id, text, isError = false) { function setSettingsStatus(id, text, isError = false) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (!el) return; if (!el) return;
@@ -910,6 +914,141 @@ function setHermesConfigStatuses(text, isError = false) {
setHermesModelStatus(text, isError); setHermesModelStatus(text, isError);
setHermesMcpStatus(text, isError); setHermesMcpStatus(text, isError);
} }
function setProviderKeyStatus(text, isError = false) {
setSettingsStatus("providerKeyStatus", text, isError);
}
function providerKeyBadge(present) {
return present
? '<span class="model-profile-badge">已配置</span>'
: '<span class="model-profile-badge off">未配置</span>';
}
function renderProviderKeys(keys) {
const box = document.getElementById("providerKeysList");
if (!box) return;
if (!Array.isArray(keys) || !keys.length) {
box.innerHTML = '<div class="settings-help">还没有发现可管理的 Provider Key。</div>';
return;
}
box.innerHTML = keys.map(item => {
const key = escapeHTML(item.key || "");
const profiles = Array.isArray(item.profiles) ? item.profiles.filter(Boolean) : [];
const profileText = profiles.length ? profiles.map(escapeHTML).join(" · ") : "尚未被模型 Profile 引用";
const cls = item.present ? "model-profile-card active" : "model-profile-card";
const removeBtn = item.present
? `<button type="button" class="danger" onclick="deleteProviderKey('${key}')">清除</button>`
: "";
return `
<div class="${cls}">
<div class="model-profile-main">
<div class="model-profile-name">
<span>${key}</span>
${providerKeyBadge(!!item.present)}
</div>
<div class="model-profile-model">${profileText}</div>
</div>
<div class="model-profile-actions">
<button type="button" onclick="fillProviderKeyName('${key}')">填入表单</button>
${removeBtn}
</div>
</div>
`;
}).join("");
}
async function refreshProviderKeys(force = false) {
if (_providerKeysLoading || (_providerKeysLoaded && !force)) return;
const box = document.getElementById("providerKeysList");
if (!box) return;
_providerKeysLoading = true;
setProviderKeyStatus("正在读取 Provider Key 状态...");
try {
const res = await apiFetch(PROVIDER_KEYS_ENDPOINT, {
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));
renderProviderKeys(data.keys || []);
_providerKeysLoaded = true;
setProviderKeyStatus("已读取 Key 状态;真实值不会回显。");
} catch (e) {
setProviderKeyStatus("读取失败: " + (e.message || e), true);
} finally {
_providerKeysLoading = false;
}
}
function fillProviderKeyName(key) {
const el = document.getElementById("providerKeyName");
if (el) {
el.value = key || "";
el.focus();
}
}
async function saveProviderKey() {
const key = document.getElementById("providerKeyName")?.value.trim() || "";
const valueEl = document.getElementById("providerKeyValue");
const value = valueEl?.value.trim() || "";
if (!/^[A-Z][A-Z0-9_]{1,120}$/.test(key) || !key.endsWith("_API_KEY")) {
toast("环境变量名需类似 OPENROUTER_API_KEY");
return;
}
if (!value) {
toast("请填写 API Key");
return;
}
const btn = document.getElementById("providerKeySaveBtn");
const oldHTML = btn?.innerHTML;
if (btn) {
btn.disabled = true;
btn.textContent = "保存 Key 中...";
}
setProviderKeyStatus("正在写入服务器环境变量...");
try {
const res = await apiFetch(PROVIDER_KEYS_ENDPOINT, {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, value }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status));
if (valueEl) valueEl.value = "";
_providerKeysLoaded = false;
await refreshProviderKeys(true);
toast("Provider Key 已写入服务器");
} catch (e) {
setProviderKeyStatus("保存失败: " + (e.message || e), true);
toast("保存失败: " + (e.message || e));
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = oldHTML;
}
}
}
async function deleteProviderKey(key) {
if (!key || !confirm("清除服务器环境变量 " + key + "? 已绑定该 Key 的模型 Profile 会停止可用。")) return;
setProviderKeyStatus("正在清除 " + key + "...");
try {
const res = await apiFetch(PROVIDER_KEYS_ENDPOINT + "/" + encodeURIComponent(key), {
method: "DELETE",
credentials: "same-origin",
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status));
_providerKeysLoaded = false;
await refreshProviderKeys(true);
toast("Provider Key 已清除");
} catch (e) {
setProviderKeyStatus("清除失败: " + (e.message || e), true);
toast("清除失败: " + (e.message || e));
}
}
async function refreshHermesConfig(force = false) { async function refreshHermesConfig(force = false) {
if (_hermesConfigLoading || (_hermesConfigLoaded && !force)) return; if (_hermesConfigLoading || (_hermesConfigLoaded && !force)) return;
@@ -1061,6 +1200,7 @@ async function saveSharedConfig(options = {}) {
saveModelProfilesToLS(); saveModelProfilesToLS();
state.sharedConfigLoaded = true; state.sharedConfigLoaded = true;
state.sharedConfigAvailable = true; state.sharedConfigAvailable = true;
_providerKeysLoaded = false;
syncModelOptionsFromProfiles(); syncModelOptionsFromProfiles();
renderModelProfiles(); renderModelProfiles();
renderAgentModelProfileOptions(document.getElementById("agentModelProfile")?.value || ""); renderAgentModelProfileOptions(document.getElementById("agentModelProfile")?.value || "");

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/svg+xml" href="./icon.svg"> <link rel="icon" type="image/svg+xml" href="./icon.svg">
<link rel="apple-touch-icon" href="./icon.svg"> <link rel="apple-touch-icon" href="./icon.svg">
<title>爱马仕 · AI</title> <title>爱马仕 · AI</title>
<link rel="stylesheet" href="./styles.css?v=20260511-modules-ia-v36"> <link rel="stylesheet" href="./styles.css?v=20260511-provider-keys-v37">
</head> </head>
<body> <body>
@@ -736,6 +736,48 @@
</div> </div>
</div> </div>
</div> </div>
<div class="settings-group wide">
<div class="settings-group-head">
<div class="settings-group-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 7a3 3 0 1 1-2.83 4H6l-2 2v3h3l2-2h3.17A3 3 0 1 1 15 7z"/><path d="M17 10h.01"/></svg>
</div>
<div>
<div class="settings-group-title">Provider API Keys</div>
<div class="settings-group-desc">只写入服务器环境变量;前端只保存变量名,不回显真实 Key</div>
</div>
</div>
<div class="settings-group-body">
<div class="settings-subpanel">
<div class="settings-subpanel-head">
<div>
<div class="settings-subtitle">服务器 Key 状态</div>
<div class="settings-help">模型 Profile 的 API Key 引用会匹配这些环境变量。</div>
</div>
<button class="glass-btn-sm" type="button" onclick="refreshProviderKeys(true)">刷新 Key 状态</button>
</div>
<div class="model-profile-list" id="providerKeysList">
<div class="settings-help">打开提供商页后自动读取。</div>
</div>
<div class="model-profile-form">
<div class="settings-grid-3">
<div class="settings-field">
<label for="providerKeyName">环境变量名</label>
<input type="text" id="providerKeyName" placeholder="OPENROUTER_API_KEY" autocomplete="off">
</div>
<div class="settings-field">
<label for="providerKeyValue">API Key</label>
<input type="password" id="providerKeyValue" placeholder="只写入服务器,不回显" autocomplete="off">
</div>
<div class="settings-field model-profile-checks">
<button class="glass-btn-sm primary" id="providerKeySaveBtn" type="button" onclick="saveProviderKey()">保存 Key</button>
</div>
</div>
<div class="settings-help" id="providerKeyStatus">真实 Key 不会保存到浏览器或仓库。</div>
</div>
</div>
</div>
</div>
</div> </div>
</section> </section>
@@ -954,7 +996,7 @@
<tr><td>档案</td><td>前端 CRUD + system prompt 组装</td><td>localStorage (hermes-ui-agents-v1)</td></tr> <tr><td>档案</td><td>前端 CRUD + system prompt 组装</td><td>localStorage (hermes-ui-agents-v1)</td></tr>
<tr><td>工作区</td><td><code>GET /v1/models</code> + localStorage 聚合</td><td>实时 + 本地</td></tr> <tr><td>工作区</td><td><code>GET /v1/models</code> + localStorage 聚合</td><td>实时 + 本地</td></tr>
<tr><td>模型</td><td><code>GET/POST /feishu/ui-config</code></td><td>服务器共享模型 Profiles</td></tr> <tr><td>模型</td><td><code>GET/POST /feishu/ui-config</code></td><td>服务器共享模型 Profiles</td></tr>
<tr><td>提供商</td><td><code>GET/PUT /feishu/hermes-config</code> <code>model</code></td><td>Hermes 运行模型配置</td></tr> <tr><td>提供商</td><td><code>GET/POST /feishu/hermes-config</code> + <code>/feishu/provider-keys</code></td><td>运行模型配置 + Provider Key 状态</td></tr>
<tr><td>技能</td><td><code>GET /hermes-skills/</code> autoindex JSON</td><td>docker cp 快照 (78 个真实 SKILL.md)</td></tr> <tr><td>技能</td><td><code>GET /hermes-skills/</code> autoindex JSON</td><td>docker cp 快照 (78 个真实 SKILL.md)</td></tr>
<tr><td>人格</td><td><code>GET /memory/SOUL.md</code></td><td>systemd timer sync</td></tr> <tr><td>人格</td><td><code>GET /memory/SOUL.md</code></td><td>systemd timer sync</td></tr>
<tr><td>记忆</td><td><code>GET /memory/sessions.json</code> 入口聚合</td><td>systemd timer sync</td></tr> <tr><td>记忆</td><td><code>GET /memory/sessions.json</code> 入口聚合</td><td>systemd timer sync</td></tr>
@@ -1545,6 +1587,6 @@ git push # Gitea kangwan/hermes-glass-ui-personal
</main> </main>
</div> </div>
<script src="./app.js?v=20260511-modules-ia-v36"></script> <script src="./app.js?v=20260511-provider-keys-v37"></script>
</body> </body>
</html> </html>