From fb92072f498084650c37810d0cc11649feabbeca Mon Sep 17 00:00:00 2001 From: kang Date: Sat, 9 May 2026 18:03:31 +0800 Subject: [PATCH] auto-save 2026-05-09 18:03 (~6) --- .memory/worklog.json | 27 ++++++------ server/feishu_bridge.py | 23 ++++++++++ src/app.js | 53 ++++++++++++++++++++++ src/index.html | 25 +++++++++++ src/styles.css | 98 +++++++++++++++++++++++++++++++++++++++++ src/sw.js | 3 +- 6 files changed, 214 insertions(+), 15 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index dbd6dd2..79075c3 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,19 +1,5 @@ { "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, "hash": "3d54081", @@ -3472,6 +3458,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 3 项未提交变更 · 最近提交:auto-save 2026-05-09 17:52 (~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 } ] } diff --git a/server/feishu_bridge.py b/server/feishu_bridge.py index a68edb0..c2e129e 100644 --- a/server/feishu_bridge.py +++ b/server/feishu_bridge.py @@ -366,6 +366,25 @@ def split_text(text: str, limit: int = 3800) -> list[str]: 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]]: expected = Config.notify_token if not expected: @@ -404,6 +423,10 @@ class Handler(BaseHTTPRequestHandler): if self.path == "/health": self.send_json(200, {"ok": True, "service": "feishu-bridge"}) 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"}) def do_POST(self) -> None: diff --git a/src/app.js b/src/app.js index eba1d26..e30aec9 100644 --- a/src/app.js +++ b/src/app.js @@ -343,6 +343,7 @@ function switchTab(name) { if (name === "cron") refreshCron(); if (name === "memory") refreshMemory(); if (name === "tools") refreshTools(); + if (name === "settings") refreshFeishuApps(); if (name === "runs") setTimeout(() => document.getElementById("runPrompt")?.focus(), 50); 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 = '
正在读取飞书桥接服务...
'; + 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 = '
还没有读取到飞书机器人配置。
'; + 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 ` +
+
+
+
${appId}
+
${isDefault ? "默认应用" : "独立应用"} · ${tokenCount} 个校验 Token
+
+ 已接入 +
+
+ ${callbackUrl} + +
+
+ 事件: im.message.receive_v1 + 通知目标: ${app.has_default_receive_id ? escapeHTML(app.default_receive_id_type || "chat_id") : "按请求传入"} +
+
`; + }).join(""); + } catch (e) { + box.innerHTML = `
飞书桥接服务读取失败: ${escapeHTML(e.message || e)}
`; + } finally { + _feishuAppsLoading = false; + } +} + // ---------- 带认证续期的 fetch ---------- // nginx 的 hermes_auth cookie 默认 24h 过期;过期后 /api/v1/* 全部 401。 // 这里在 401 时尝试一次静默续期(浏览器若缓存了 Basic Auth 会自动带上), @@ -3283,6 +3333,9 @@ function startResearch() { function openLog() { toast("日志查看暂未实现,可 SSH 到 Mac mini 查看 ~/.hermes/logs/"); } +function copyText(text) { + navigator.clipboard?.writeText(text).then(() => toast("已复制")); +} function toast(text) { const el = document.createElement("div"); el.textContent = text; diff --git a/src/index.html b/src/index.html index d04dcba..7b89d4a 100644 --- a/src/index.html +++ b/src/index.html @@ -1088,6 +1088,31 @@ git push # Gitea kangwan/hermes-glass-ui-personal + +
+
+
+ +
+
+
飞书集成
+
当前已接入的飞书机器人和事件回调地址
+
+
+
+
+
只显示 App ID、回调地址和服务状态;Secret 与 Token 只保存在服务器环境文件。
+ +
+
+
打开设置页后自动读取飞书桥接服务。
+
+
+
+
diff --git a/src/styles.css b/src/styles.css index b0bb306..c77b13f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2296,6 +2296,104 @@ a { color: var(--orange-3); text-decoration: none; } padding: 10px 16px; 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 { border-color: rgba(255,93,122,0.3) !important; color: var(--err) !important; diff --git a/src/sw.js b/src/sw.js index c2ae6b2..c65b786 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,6 +1,6 @@ // 爱马仕 Hermes · 轻量 Service Worker // 静态壳走 network-first(拿不到再回退缓存),API 直通 -const CACHE = "hermes-ui-v9"; +const CACHE = "hermes-ui-v10"; const ASSETS = [ "./", "./index.html", @@ -29,6 +29,7 @@ self.addEventListener("fetch", (e) => { // API / 鉴权 / skill 索引等动态资源全部直通 if (url.pathname.startsWith("/api/")) return; if (url.pathname.startsWith("/_auth/")) return; + if (url.pathname.startsWith("/feishu/")) return; if (url.pathname.startsWith("/hermes-skills/")) return; if (e.request.method !== "GET") return;