auto-save 2026-05-09 18:03 (~6)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
53
src/app.js
53
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 = '<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 ----------
|
||||
// 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;
|
||||
|
||||
@@ -1088,6 +1088,31 @@ git push # Gitea kangwan/hermes-glass-ui-personal
|
||||
</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-head">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user