auto-save 2026-05-09 18:20 (~6)
This commit is contained in:
@@ -1,26 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "fb18cba",
|
|
||||||
"message": "auto-save 2026-05-07 16:24 (~1)",
|
|
||||||
"ts": "2026-05-07T16:24:27+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "8820687",
|
|
||||||
"message": "auto-save 2026-05-07 16:29 (~1)",
|
|
||||||
"ts": "2026-05-07T16:29:58+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "60c49c9",
|
|
||||||
"message": "auto-save 2026-05-07 16:35 (~1)",
|
|
||||||
"ts": "2026-05-07T16:35:31+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"files_changed": 1,
|
"files_changed": 1,
|
||||||
"hash": "f808cae",
|
"hash": "f808cae",
|
||||||
@@ -3469,6 +3448,25 @@
|
|||||||
"message": "auto-save 2026-05-09 18:09 (~3)",
|
"message": "auto-save 2026-05-09 18:09 (~3)",
|
||||||
"hash": "2828de0",
|
"hash": "2828de0",
|
||||||
"files_changed": 3
|
"files_changed": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-09T18:14:35+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-09 18:14 (~1)",
|
||||||
|
"hash": "a64489f",
|
||||||
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-09T10:15:53Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 2 项未提交变更 · 最近提交:auto-save 2026-05-09 18:14 (~1)",
|
||||||
|
"files_changed": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-09T10:18:28Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 6 项未提交变更 · 最近提交:auto-save 2026-05-09 18:14 (~1)",
|
||||||
|
"files_changed": 6
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -23,6 +24,7 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
||||||
|
APP_ID_RE = re.compile(r"^cli_[A-Za-z0-9]+$")
|
||||||
|
|
||||||
|
|
||||||
def _env(name: str, default: str = "") -> str:
|
def _env(name: str, default: str = "") -> str:
|
||||||
@@ -41,9 +43,13 @@ def _csv_list(value: str) -> list[str]:
|
|||||||
return [item.strip() for item in value.split(",") if item.strip()]
|
return [item.strip() for item in value.split(",") if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def app_suffixes() -> list[str]:
|
||||||
|
return ["", *[f"_{idx}" for idx in range(2, 21)]]
|
||||||
|
|
||||||
|
|
||||||
def _load_feishu_apps() -> dict[str, dict[str, Any]]:
|
def _load_feishu_apps() -> dict[str, dict[str, Any]]:
|
||||||
apps: dict[str, dict[str, Any]] = {}
|
apps: dict[str, dict[str, Any]] = {}
|
||||||
for suffix in ["", *[f"_{idx}" for idx in range(2, 10)]]:
|
for suffix in app_suffixes():
|
||||||
app_id = _env(f"FEISHU_APP_ID{suffix}")
|
app_id = _env(f"FEISHU_APP_ID{suffix}")
|
||||||
app_secret = _env(f"FEISHU_APP_SECRET{suffix}")
|
app_secret = _env(f"FEISHU_APP_SECRET{suffix}")
|
||||||
if not app_id and not app_secret:
|
if not app_id and not app_secret:
|
||||||
@@ -63,6 +69,8 @@ def _load_feishu_apps() -> dict[str, dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
env_file = _env("FEISHU_BRIDGE_ENV_FILE", "/etc/hermes-feishu-bridge.env")
|
||||||
|
tokens_file = _env("FEISHU_BRIDGE_TOKENS_FILE", "/root/hermes-feishu-bridge.tokens")
|
||||||
feishu_app_id = _env("FEISHU_APP_ID")
|
feishu_app_id = _env("FEISHU_APP_ID")
|
||||||
feishu_app_secret = _env("FEISHU_APP_SECRET")
|
feishu_app_secret = _env("FEISHU_APP_SECRET")
|
||||||
feishu_apps = _load_feishu_apps()
|
feishu_apps = _load_feishu_apps()
|
||||||
@@ -115,12 +123,86 @@ class TokenCache:
|
|||||||
self._tokens[app_id] = (token, expires_at)
|
self._tokens[app_id] = (token, expires_at)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
def drop(self, app_id: str) -> None:
|
||||||
|
with self._lock:
|
||||||
|
self._tokens.pop(app_id, None)
|
||||||
|
|
||||||
|
|
||||||
token_cache = TokenCache()
|
token_cache = TokenCache()
|
||||||
seen_events: dict[str, float] = {}
|
seen_events: dict[str, float] = {}
|
||||||
seen_lock = threading.Lock()
|
seen_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_env_file(path: str) -> dict[str, str]:
|
||||||
|
values: dict[str, str] = {}
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return values
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
for raw in fh:
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
values[key.strip()] = value.strip()
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def write_env_updates(path: str, updates: dict[str, str]) -> None:
|
||||||
|
lines: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
for raw in fh.read().splitlines():
|
||||||
|
if "=" in raw and not raw.lstrip().startswith("#"):
|
||||||
|
key = raw.split("=", 1)[0]
|
||||||
|
if key in updates:
|
||||||
|
lines.append(f"{key}={updates[key]}")
|
||||||
|
seen.add(key)
|
||||||
|
continue
|
||||||
|
lines.append(raw)
|
||||||
|
for key, value in updates.items():
|
||||||
|
if key not in seen:
|
||||||
|
lines.append(f"{key}={value}")
|
||||||
|
tmp = f"{path}.tmp-{os.getpid()}"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write("\n".join(lines) + "\n")
|
||||||
|
os.chmod(tmp, 0o600)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
|
||||||
|
|
||||||
|
def reload_config_from_env_file() -> None:
|
||||||
|
values = parse_env_file(Config.env_file)
|
||||||
|
for key, value in values.items():
|
||||||
|
os.environ[key] = value
|
||||||
|
Config.feishu_app_id = _env("FEISHU_APP_ID")
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_tokens_file() -> None:
|
||||||
|
env = parse_env_file(Config.env_file)
|
||||||
|
lines: list[str] = []
|
||||||
|
notify_token = env.get("FEISHU_NOTIFY_TOKEN")
|
||||||
|
for suffix in app_suffixes():
|
||||||
|
app_id = env.get(f"FEISHU_APP_ID{suffix}", "")
|
||||||
|
if not app_id:
|
||||||
|
continue
|
||||||
|
label = suffix or ""
|
||||||
|
lines.append(f"FEISHU_CALLBACK_URL{label}=https://hermes.kang-kang.com/feishu/events/{app_id}")
|
||||||
|
lines.append(f"FEISHU_APP_ID{label}={app_id}")
|
||||||
|
token = env.get(f"FEISHU_VERIFICATION_TOKEN{suffix}", "")
|
||||||
|
if token:
|
||||||
|
lines.append(f"FEISHU_VERIFICATION_TOKEN{label}={token}")
|
||||||
|
if notify_token:
|
||||||
|
lines.append(f"FEISHU_NOTIFY_TOKEN={notify_token}")
|
||||||
|
tmp = f"{Config.tokens_file}.tmp-{os.getpid()}"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write("\n".join(lines) + "\n")
|
||||||
|
os.chmod(tmp, 0o600)
|
||||||
|
os.replace(tmp, Config.tokens_file)
|
||||||
|
|
||||||
|
|
||||||
def http_json(
|
def http_json(
|
||||||
method: str,
|
method: str,
|
||||||
url: str,
|
url: str,
|
||||||
@@ -385,6 +467,86 @@ def handle_apps() -> tuple[int, dict[str, Any]]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin_request(headers: dict[str, str]) -> tuple[bool, int, str]:
|
||||||
|
cookie = headers.get("cookie", "")
|
||||||
|
if "hermes_auth=ok" not in cookie:
|
||||||
|
return False, 401, "login required"
|
||||||
|
origin = headers.get("origin", "")
|
||||||
|
referer = headers.get("referer", "")
|
||||||
|
allowed = "https://hermes.kang-kang.com"
|
||||||
|
if origin and origin != allowed:
|
||||||
|
return False, 403, "invalid origin"
|
||||||
|
if not origin and referer and not referer.startswith(allowed + "/"):
|
||||||
|
return False, 403, "invalid referer"
|
||||||
|
return True, 200, "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_feishu_credentials(app_id: str, app_secret: str) -> None:
|
||||||
|
data = http_json(
|
||||||
|
"POST",
|
||||||
|
f"{FEISHU_API_BASE}/auth/v3/tenant_access_token/internal",
|
||||||
|
{"app_id": app_id, "app_secret": app_secret},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if data.get("code") != 0:
|
||||||
|
raise ValueError(f"Feishu credential validation failed: {data.get('msg') or data}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_app_suffix(env: dict[str, str], app_id: str) -> str:
|
||||||
|
for suffix in app_suffixes():
|
||||||
|
if env.get(f"FEISHU_APP_ID{suffix}") == app_id:
|
||||||
|
return suffix
|
||||||
|
for suffix in app_suffixes():
|
||||||
|
if not env.get(f"FEISHU_APP_ID{suffix}") and not env.get(f"FEISHU_APP_SECRET{suffix}"):
|
||||||
|
return suffix
|
||||||
|
raise RuntimeError("no free Feishu app slot; supported slots are FEISHU_APP_ID through FEISHU_APP_ID_20")
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_feishu_app(body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
app_id = str(body.get("app_id", "")).strip()
|
||||||
|
app_secret = str(body.get("app_secret", "")).strip()
|
||||||
|
verification_token = str(body.get("verification_token", "")).strip()
|
||||||
|
if not APP_ID_RE.match(app_id):
|
||||||
|
raise ValueError("App ID must look like cli_xxx")
|
||||||
|
if len(app_secret) < 16:
|
||||||
|
raise ValueError("App Secret is too short")
|
||||||
|
if len(verification_token) < 16:
|
||||||
|
raise ValueError("Verification Token is too short")
|
||||||
|
|
||||||
|
validate_feishu_credentials(app_id, app_secret)
|
||||||
|
|
||||||
|
env = parse_env_file(Config.env_file)
|
||||||
|
suffix = find_app_suffix(env, app_id)
|
||||||
|
updates = {
|
||||||
|
f"FEISHU_APP_ID{suffix}": app_id,
|
||||||
|
f"FEISHU_APP_SECRET{suffix}": app_secret,
|
||||||
|
f"FEISHU_VERIFICATION_TOKEN{suffix}": verification_token,
|
||||||
|
}
|
||||||
|
if not env.get(f"FEISHU_DEFAULT_RECEIVE_ID_TYPE{suffix}"):
|
||||||
|
updates[f"FEISHU_DEFAULT_RECEIVE_ID_TYPE{suffix}"] = "chat_id"
|
||||||
|
write_env_updates(Config.env_file, updates)
|
||||||
|
sync_tokens_file()
|
||||||
|
reload_config_from_env_file()
|
||||||
|
token_cache.drop(app_id)
|
||||||
|
return {
|
||||||
|
"app_id": app_id,
|
||||||
|
"callback_url": f"https://hermes.kang-kang.com/feishu/events/{app_id}",
|
||||||
|
"slot": suffix or "_1",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_apps_admin(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:
|
||||||
|
app = upsert_feishu_app(body)
|
||||||
|
except ValueError as exc:
|
||||||
|
return 400, {"code": 400, "msg": str(exc)}
|
||||||
|
logging.info("upserted Feishu app from Hermes settings app_id=%s", app["app_id"])
|
||||||
|
return 200, {"code": 0, "msg": "ok", "app": app}
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -435,6 +597,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
headers = {key.lower(): value for key, value in self.headers.items()}
|
headers = {key.lower(): value for key, value in self.headers.items()}
|
||||||
if self.path == "/feishu/events" or self.path.startswith("/feishu/events/"):
|
if self.path == "/feishu/events" or self.path.startswith("/feishu/events/"):
|
||||||
status, payload = handle_feishu_event(self.path, body)
|
status, payload = handle_feishu_event(self.path, body)
|
||||||
|
elif self.path == "/feishu/apps":
|
||||||
|
status, payload = handle_apps_admin(headers, body)
|
||||||
elif self.path == "/feishu/notify" or self.path.startswith("/feishu/notify/"):
|
elif self.path == "/feishu/notify" or self.path.startswith("/feishu/notify/"):
|
||||||
status, payload = handle_notify(self.path, headers, body)
|
status, payload = handle_notify(self.path, headers, body)
|
||||||
else:
|
else:
|
||||||
|
|||||||
48
src/app.js
48
src/app.js
@@ -356,6 +356,7 @@ function switchTab(name) {
|
|||||||
|
|
||||||
// ---------- 飞书集成 ----------
|
// ---------- 飞书集成 ----------
|
||||||
let _feishuAppsLoading = false;
|
let _feishuAppsLoading = false;
|
||||||
|
let _feishuLastApps = [];
|
||||||
async function refreshFeishuApps() {
|
async function refreshFeishuApps() {
|
||||||
const box = document.getElementById("feishuApps");
|
const box = document.getElementById("feishuApps");
|
||||||
if (!box || _feishuAppsLoading) return;
|
if (!box || _feishuAppsLoading) return;
|
||||||
@@ -366,6 +367,7 @@ async function refreshFeishuApps() {
|
|||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const apps = Array.isArray(data.apps) ? data.apps : [];
|
const apps = Array.isArray(data.apps) ? data.apps : [];
|
||||||
|
_feishuLastApps = apps;
|
||||||
if (!apps.length) {
|
if (!apps.length) {
|
||||||
box.innerHTML = '<div class="settings-help">还没有读取到飞书机器人配置。</div>';
|
box.innerHTML = '<div class="settings-help">还没有读取到飞书机器人配置。</div>';
|
||||||
return;
|
return;
|
||||||
@@ -403,6 +405,52 @@ async function refreshFeishuApps() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveFeishuApp(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const appIdEl = document.getElementById("feishuAppId");
|
||||||
|
const secretEl = document.getElementById("feishuAppSecret");
|
||||||
|
const tokenEl = document.getElementById("feishuVerifyToken");
|
||||||
|
const app_id = appIdEl?.value.trim() || "";
|
||||||
|
const app_secret = secretEl?.value.trim() || "";
|
||||||
|
const verification_token = tokenEl?.value.trim() || "";
|
||||||
|
if (!/^cli_[A-Za-z0-9]+$/.test(app_id)) {
|
||||||
|
toast("App ID 格式不对");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app_secret.length < 16 || verification_token.length < 16) {
|
||||||
|
toast("Secret / Token 太短");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const submit = event.target.querySelector("button[type='submit']");
|
||||||
|
const oldText = submit?.textContent;
|
||||||
|
if (submit) {
|
||||||
|
submit.disabled = true;
|
||||||
|
submit.textContent = "正在保存...";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch("/feishu/apps", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ app_id, app_secret, verification_token }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status));
|
||||||
|
secretEl.value = "";
|
||||||
|
tokenEl.value = "";
|
||||||
|
toast("飞书机器人已保存");
|
||||||
|
await refreshFeishuApps();
|
||||||
|
if (data.app?.callback_url) copyText(data.app.callback_url);
|
||||||
|
} catch (e) {
|
||||||
|
toast("保存失败: " + (e.message || e));
|
||||||
|
} finally {
|
||||||
|
if (submit) {
|
||||||
|
submit.disabled = false;
|
||||||
|
submit.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>添加 / 更新机器人';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 带认证续期的 fetch ----------
|
// ---------- 带认证续期的 fetch ----------
|
||||||
// nginx 的 hermes_auth cookie 默认 24h 过期;过期后 /api/v1/* 全部 401。
|
// nginx 的 hermes_auth cookie 默认 24h 过期;过期后 /api/v1/* 全部 401。
|
||||||
// 这里在 401 时尝试一次静默续期(浏览器若缓存了 Basic Auth 会自动带上),
|
// 这里在 401 时尝试一次静默续期(浏览器若缓存了 Basic Auth 会自动带上),
|
||||||
|
|||||||
@@ -1110,6 +1110,27 @@ git push # Gitea kangwan/hermes-glass-ui-personal
|
|||||||
<div class="feishu-apps" id="feishuApps">
|
<div class="feishu-apps" id="feishuApps">
|
||||||
<div class="settings-help">打开设置页后自动读取飞书桥接服务。</div>
|
<div class="settings-help">打开设置页后自动读取飞书桥接服务。</div>
|
||||||
</div>
|
</div>
|
||||||
|
<form class="feishu-form" id="feishuAddForm" onsubmit="saveFeishuApp(event)">
|
||||||
|
<div class="settings-field">
|
||||||
|
<label for="feishuAppId">App ID</label>
|
||||||
|
<input type="text" id="feishuAppId" placeholder="cli_xxxxxxxxxxxxxxxx" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label for="feishuAppSecret">App Secret</label>
|
||||||
|
<input type="password" id="feishuAppSecret" placeholder="只写入服务器,不保存在浏览器" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label for="feishuVerifyToken">Verification Token</label>
|
||||||
|
<input type="password" id="feishuVerifyToken" placeholder="飞书加密策略里的 Verification Token" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="feishu-form-actions">
|
||||||
|
<button class="glass-btn-sm primary" type="submit">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
|
||||||
|
添加 / 更新机器人
|
||||||
|
</button>
|
||||||
|
<div class="settings-help">保存前会校验 App ID / Secret;成功后复制回调地址到飞书事件配置。</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2394,6 +2394,23 @@ a { color: var(--orange-3); text-decoration: none; }
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255,255,255,0.035);
|
background: rgba(255,255,255,0.035);
|
||||||
}
|
}
|
||||||
|
.feishu-form {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 16px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.feishu-form-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.feishu-form-actions .glass-btn-sm {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.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;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// 爱马仕 Hermes · 轻量 Service Worker
|
// 爱马仕 Hermes · 轻量 Service Worker
|
||||||
// 静态壳走 network-first(拿不到再回退缓存),API 直通
|
// 静态壳走 network-first(拿不到再回退缓存),API 直通
|
||||||
const CACHE = "hermes-ui-v10";
|
const CACHE = "hermes-ui-v11";
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
"./",
|
"./",
|
||||||
"./index.html",
|
"./index.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user