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
+
+
已接入
+
+
+
+
`;
+ }).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
+
+
+
+
+
+
飞书集成
+
当前已接入的飞书机器人和事件回调地址
+
+
+
+
+
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;