auto-save 2026-05-09 18:03 (~6)

This commit is contained in:
2026-05-09 18:03:31 +08:00
parent ef06c99d56
commit fb92072f49
6 changed files with 214 additions and 15 deletions

View File

@@ -1,19 +1,5 @@
{ {
"entries": [ "entries": [
{
"files_changed": 1,
"hash": "2c8038a",
"message": "auto-save 2026-05-07 15:51 (~1)",
"ts": "2026-05-07T15:51:12+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "82da6b8",
"message": "auto-save 2026-05-07 15:56 (~1)",
"ts": "2026-05-07T15:56:45+08:00",
"type": "commit"
},
{ {
"files_changed": 1, "files_changed": 1,
"hash": "3d54081", "hash": "3d54081",
@@ -3472,6 +3458,19 @@
"type": "session-heartbeat", "type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 3 项未提交变更 · 最近提交auto-save 2026-05-09 17:52 (~3)", "message": "Codex 会话活跃 · 最近命令codex · 分支 master · 3 项未提交变更 · 最近提交auto-save 2026-05-09 17:52 (~3)",
"files_changed": 3 "files_changed": 3
},
{
"ts": "2026-05-09T17:57:57+08:00",
"type": "commit",
"message": "auto-save 2026-05-09 17:57 (~3)",
"hash": "ef06c99",
"files_changed": 3
},
{
"ts": "2026-05-09T09:58:28Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-09 17:57 (~3)",
"files_changed": 1
} }
] ]
} }

View File

@@ -366,6 +366,25 @@ def split_text(text: str, limit: int = 3800) -> list[str]:
return chunks return chunks
def handle_apps() -> tuple[int, dict[str, Any]]:
apps = []
for app_id, app in Config.feishu_apps.items():
apps.append({
"app_id": app_id,
"callback_url": f"https://hermes.kang-kang.com/feishu/events/{app_id}",
"default_receive_id_type": app.get("default_receive_id_type", "chat_id"),
"has_default_receive_id": bool(app.get("default_receive_id")),
"allowed_chat_ids_count": len(app.get("allowed_chat_ids", set())),
"verification_tokens_count": len(app.get("verification_tokens", [])),
})
return 200, {
"code": 0,
"service": "hermes-feishu-bridge",
"default_app_id": Config.default_feishu_app_id,
"apps": apps,
}
def handle_notify(path: str, headers: dict[str, str], body: dict[str, Any]) -> tuple[int, dict[str, Any]]: def handle_notify(path: str, headers: dict[str, str], body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
expected = Config.notify_token expected = Config.notify_token
if not expected: if not expected:
@@ -404,6 +423,10 @@ class Handler(BaseHTTPRequestHandler):
if self.path == "/health": if self.path == "/health":
self.send_json(200, {"ok": True, "service": "feishu-bridge"}) self.send_json(200, {"ok": True, "service": "feishu-bridge"})
return return
if self.path == "/feishu/apps":
status, payload = handle_apps()
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:

View File

@@ -343,6 +343,7 @@ function switchTab(name) {
if (name === "cron") refreshCron(); if (name === "cron") refreshCron();
if (name === "memory") refreshMemory(); if (name === "memory") refreshMemory();
if (name === "tools") refreshTools(); if (name === "tools") refreshTools();
if (name === "settings") refreshFeishuApps();
if (name === "runs") setTimeout(() => document.getElementById("runPrompt")?.focus(), 50); if (name === "runs") setTimeout(() => document.getElementById("runPrompt")?.focus(), 50);
if (name === "dashboard" && _dashboardDirty) { if (name === "dashboard" && _dashboardDirty) {
// 推迟到下一帧,避免阻塞切换动画 // 推迟到下一帧,避免阻塞切换动画
@@ -353,6 +354,55 @@ function switchTab(name) {
} }
} }
// ---------- 飞书集成 ----------
let _feishuAppsLoading = false;
async function refreshFeishuApps() {
const box = document.getElementById("feishuApps");
if (!box || _feishuAppsLoading) return;
_feishuAppsLoading = true;
box.innerHTML = '<div class="settings-help">正在读取飞书桥接服务...</div>';
try {
const res = await fetch("/feishu/apps", { cache: "no-store" });
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
const apps = Array.isArray(data.apps) ? data.apps : [];
if (!apps.length) {
box.innerHTML = '<div class="settings-help">还没有读取到飞书机器人配置。</div>';
return;
}
box.innerHTML = apps.map(app => {
const appId = escapeHTML(app.app_id || "");
const callbackUrl = escapeHTML(app.callback_url || "");
const isDefault = app.app_id === data.default_app_id;
const tokenCount = Number(app.verification_tokens_count || 0);
return `
<div class="feishu-app-card">
<div class="feishu-app-top">
<div>
<div class="feishu-app-title">${appId}</div>
<div class="feishu-app-meta">${isDefault ? "默认应用" : "独立应用"} · ${tokenCount} 个校验 Token</div>
</div>
<span class="feishu-status">已接入</span>
</div>
<div class="feishu-callback">
<span>${callbackUrl}</span>
<button class="icon-btn-mini" onclick="copyText('${callbackUrl}')" title="复制回调地址">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
<div class="feishu-app-foot">
<span>事件: im.message.receive_v1</span>
<span>通知目标: ${app.has_default_receive_id ? escapeHTML(app.default_receive_id_type || "chat_id") : "按请求传入"}</span>
</div>
</div>`;
}).join("");
} catch (e) {
box.innerHTML = `<div class="settings-help">飞书桥接服务读取失败: ${escapeHTML(e.message || e)}</div>`;
} finally {
_feishuAppsLoading = false;
}
}
// ---------- 带认证续期的 fetch ---------- // ---------- 带认证续期的 fetch ----------
// nginx 的 hermes_auth cookie 默认 24h 过期;过期后 /api/v1/* 全部 401。 // nginx 的 hermes_auth cookie 默认 24h 过期;过期后 /api/v1/* 全部 401。
// 这里在 401 时尝试一次静默续期(浏览器若缓存了 Basic Auth 会自动带上), // 这里在 401 时尝试一次静默续期(浏览器若缓存了 Basic Auth 会自动带上),
@@ -3283,6 +3333,9 @@ function startResearch() {
function openLog() { function openLog() {
toast("日志查看暂未实现,可 SSH 到 Mac mini 查看 ~/.hermes/logs/"); toast("日志查看暂未实现,可 SSH 到 Mac mini 查看 ~/.hermes/logs/");
} }
function copyText(text) {
navigator.clipboard?.writeText(text).then(() => toast("已复制"));
}
function toast(text) { function toast(text) {
const el = document.createElement("div"); const el = document.createElement("div");
el.textContent = text; el.textContent = text;

View File

@@ -1088,6 +1088,31 @@ git push # Gitea kangwan/hermes-glass-ui-personal
</div> </div>
</div> </div>
<!-- 飞书集成 -->
<div class="settings-group wide" id="feishuSettingsGroup">
<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="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/><path d="M8 9h8"/><path d="M8 13h5"/></svg>
</div>
<div>
<div class="settings-group-title">飞书集成</div>
<div class="settings-group-desc">当前已接入的飞书机器人和事件回调地址</div>
</div>
</div>
<div class="settings-group-body">
<div class="feishu-toolbar">
<div class="settings-help">只显示 App ID、回调地址和服务状态Secret 与 Token 只保存在服务器环境文件。</div>
<button class="glass-btn-sm" onclick="refreshFeishuApps()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 15.5-6.3L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15.5 6.3L3 16"/><path d="M3 21v-5h5"/></svg>
刷新
</button>
</div>
<div class="feishu-apps" id="feishuApps">
<div class="settings-help">打开设置页后自动读取飞书桥接服务。</div>
</div>
</div>
</div>
<!-- 关于 --> <!-- 关于 -->
<div class="settings-group wide"> <div class="settings-group wide">
<div class="settings-group-head"> <div class="settings-group-head">

View File

@@ -2296,6 +2296,104 @@ a { color: var(--orange-3); text-decoration: none; }
padding: 10px 16px; padding: 10px 16px;
font-size: 12px; font-size: 12px;
} }
.feishu-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.feishu-toolbar .settings-help { flex: 1 1 280px; }
.feishu-apps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.feishu-app-card {
border: 1px solid var(--line);
background: rgba(255,255,255,0.04);
border-radius: 12px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
[data-theme="light"] .feishu-app-card { background: rgba(15,22,40,0.035); }
.feishu-app-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
min-width: 0;
}
.feishu-app-title {
font-family: "SF Mono", ui-monospace, Menlo, monospace;
font-size: 12px;
font-weight: 800;
color: var(--text);
overflow-wrap: anywhere;
}
.feishu-app-meta {
margin-top: 3px;
font-size: 11px;
color: var(--text-dim2);
}
.feishu-status {
flex: 0 0 auto;
border: 1px solid rgba(80,220,140,0.32);
background: rgba(80,220,140,0.1);
color: #68d391;
border-radius: 999px;
padding: 4px 8px;
font-size: 11px;
font-weight: 800;
}
.feishu-callback {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(0,0,0,0.18);
}
[data-theme="light"] .feishu-callback { background: rgba(15,22,40,0.04); }
.feishu-callback span {
flex: 1 1 auto;
min-width: 0;
font-family: "SF Mono", ui-monospace, Menlo, monospace;
font-size: 11px;
color: var(--text-dim);
overflow-wrap: anywhere;
}
.icon-btn-mini {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid var(--line-strong);
background: rgba(255,105,0,0.08);
color: var(--orange-3);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.icon-btn-mini:hover { background: rgba(255,105,0,0.16); }
.feishu-app-foot {
display: flex;
flex-wrap: wrap;
gap: 8px;
color: var(--text-dim2);
font-size: 11px;
}
.feishu-app-foot span {
padding: 3px 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255,255,255,0.035);
}
.danger-btn { .danger-btn {
border-color: rgba(255,93,122,0.3) !important; border-color: rgba(255,93,122,0.3) !important;
color: var(--err) !important; color: var(--err) !important;

View File

@@ -1,6 +1,6 @@
// 爱马仕 Hermes · 轻量 Service Worker // 爱马仕 Hermes · 轻量 Service Worker
// 静态壳走 network-first拿不到再回退缓存API 直通 // 静态壳走 network-first拿不到再回退缓存API 直通
const CACHE = "hermes-ui-v9"; const CACHE = "hermes-ui-v10";
const ASSETS = [ const ASSETS = [
"./", "./",
"./index.html", "./index.html",
@@ -29,6 +29,7 @@ self.addEventListener("fetch", (e) => {
// API / 鉴权 / skill 索引等动态资源全部直通 // API / 鉴权 / skill 索引等动态资源全部直通
if (url.pathname.startsWith("/api/")) return; if (url.pathname.startsWith("/api/")) return;
if (url.pathname.startsWith("/_auth/")) return; if (url.pathname.startsWith("/_auth/")) return;
if (url.pathname.startsWith("/feishu/")) return;
if (url.pathname.startsWith("/hermes-skills/")) return; if (url.pathname.startsWith("/hermes-skills/")) return;
if (e.request.method !== "GET") return; if (e.request.method !== "GET") return;