auto-save 2026-05-11 19:15 (~5)
This commit is contained in:
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"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,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 10:13 (~1)",
|
||||
@@ -3271,6 +3264,13 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-11 19:04 (~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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
RULES.md
2
RULES.md
@@ -21,6 +21,7 @@
|
||||
- 爱马仕前端「设置 → 连接」可自助维护 API 地址 / API Key 并测试连接;「对话 → 存周报」和「设置 → 周报记录」会在本地保存任务描述、上下文片段和最终周报
|
||||
- 爱马仕前端「模型」可维护 AI 模型 Profiles,用于给不同 Agent 绑定不同模型、Provider、Base URL 和服务器端 Key 引用
|
||||
- 爱马仕前端「提供商」可维护 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、共享 Agent/Profile 等 `/feishu/*` 管理接口复用网页登录 cookie,并在前端 401 时走 `/_auth/verify` 静默续期
|
||||
- 当前前端不再启用 Service Worker 静态壳缓存;`sw.js` 仅用于清理旧 `hermes-ui-*` 缓存并注销旧注册
|
||||
@@ -68,5 +69,6 @@
|
||||
- 飞书后台配置所需回调 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` 扩展
|
||||
- Hermes 运行配置接口 `/feishu/hermes-config` 复用飞书桥接反代,要求已登录爱马仕 cookie 且同源请求;会通过 Incus 写入 LXC 配置并重启 `hermes-agent`
|
||||
- Provider Key 管理接口 `/feishu/provider-keys` 复用飞书桥接反代,要求已登录爱马仕 cookie 且同源请求;只允许管理形如 `*_API_KEY` 的 Provider 环境变量,禁止管理飞书和部署控制变量
|
||||
- 当前飞书桥接版本按明文事件回调处理;如果飞书后台开启事件加密,需要先补充解密支持
|
||||
- 主站有 cookie 门禁;nginx 已对 `/feishu/` 单独放行并反代到飞书桥接服务
|
||||
|
||||
@@ -228,6 +228,9 @@ def reload_config_from_env_file() -> None:
|
||||
Config.feishu_app_secret = _env("FEISHU_APP_SECRET")
|
||||
Config.feishu_apps = _load_feishu_apps()
|
||||
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:
|
||||
@@ -1140,6 +1143,125 @@ def is_admin_request(headers: dict[str, str]) -> tuple[bool, int, str]:
|
||||
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:
|
||||
data = http_json(
|
||||
"POST",
|
||||
@@ -1348,6 +1470,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
status, payload = handle_ui_config_get(headers)
|
||||
self.send_json(status, payload)
|
||||
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"})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
@@ -1362,6 +1488,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
status, payload = handle_hermes_config_post(headers, body)
|
||||
elif self.path == "/feishu/ui-config":
|
||||
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":
|
||||
ok, status, message = is_admin_request(headers)
|
||||
if not ok:
|
||||
@@ -1383,6 +1511,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
headers = {key.lower(): value for key, value in self.headers.items()}
|
||||
if self.path.startswith("/feishu/apps/"):
|
||||
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:
|
||||
status, payload = 404, {"code": 404, "msg": "not found"}
|
||||
except Exception as exc:
|
||||
|
||||
140
src/app.js
140
src/app.js
@@ -13,6 +13,7 @@ const LS_TAB = "hermes-ui-active-tab-v1";
|
||||
const LS_WEEKLY_REPORTS = "hermes-ui-weekly-reports-v1";
|
||||
const LS_MODEL_PROFILES = "hermes-ui-model-profiles-v1";
|
||||
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 LEGACY_DEFAULT_MODEL_ID = "gemini-3-pro-preview";
|
||||
|
||||
@@ -666,6 +667,7 @@ function switchTab(name, options = {}) {
|
||||
}
|
||||
if (name === "providers") {
|
||||
refreshHermesConfig();
|
||||
refreshProviderKeys();
|
||||
}
|
||||
if (name === "tools") {
|
||||
refreshTools();
|
||||
@@ -894,6 +896,8 @@ async function testApiConnection() {
|
||||
let _hermesConfigLoaded = false;
|
||||
let _hermesConfigLoading = false;
|
||||
let _hermesConfigSnapshot = null;
|
||||
let _providerKeysLoaded = false;
|
||||
let _providerKeysLoading = false;
|
||||
function setSettingsStatus(id, text, isError = false) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
@@ -910,6 +914,141 @@ function setHermesConfigStatuses(text, isError = false) {
|
||||
setHermesModelStatus(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) {
|
||||
if (_hermesConfigLoading || (_hermesConfigLoaded && !force)) return;
|
||||
@@ -1061,6 +1200,7 @@ async function saveSharedConfig(options = {}) {
|
||||
saveModelProfilesToLS();
|
||||
state.sharedConfigLoaded = true;
|
||||
state.sharedConfigAvailable = true;
|
||||
_providerKeysLoaded = false;
|
||||
syncModelOptionsFromProfiles();
|
||||
renderModelProfiles();
|
||||
renderAgentModelProfileOptions(document.getElementById("agentModelProfile")?.value || "");
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="./icon.svg">
|
||||
<link rel="apple-touch-icon" href="./icon.svg">
|
||||
<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>
|
||||
<body>
|
||||
|
||||
@@ -736,6 +736,48 @@
|
||||
</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>
|
||||
</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><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/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 /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>
|
||||
@@ -1545,6 +1587,6 @@ git push # Gitea kangwan/hermes-glass-ui-personal
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="./app.js?v=20260511-modules-ia-v36"></script>
|
||||
<script src="./app.js?v=20260511-provider-keys-v37"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user