auto-save 2026-05-11 14:39 (~6)

This commit is contained in:
2026-05-11 14:39:31 +08:00
parent 93118d4ce1
commit 5247dfd0dc
6 changed files with 168 additions and 19 deletions

View File

@@ -170,8 +170,48 @@ def write_env_updates(path: str, updates: dict[str, str]) -> None:
os.replace(tmp, path)
def write_env_removals(path: str, remove_keys: set[str], updates: dict[str, str] | None = None) -> None:
updates = updates or {}
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].strip()
if key in remove_keys:
continue
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 suffix in app_suffixes():
for prefix in (
"FEISHU_APP_ID",
"FEISHU_APP_SECRET",
"FEISHU_VERIFICATION_TOKEN",
"FEISHU_DEFAULT_RECEIVE_ID",
"FEISHU_DEFAULT_RECEIVE_ID_TYPE",
"FEISHU_ALLOWED_CHAT_IDS",
):
key = f"{prefix}{suffix}"
if key not in values:
os.environ.pop(key, None)
if "FEISHU_DEFAULT_APP_ID" not in values:
os.environ.pop("FEISHU_DEFAULT_APP_ID", None)
for key, value in values.items():
os.environ[key] = value
Config.feishu_app_id = _env("FEISHU_APP_ID")
@@ -415,7 +455,7 @@ def send_feishu_text(
result: dict[str, Any] = {}
for chunk in chunks:
content = json.dumps({"text": chunk}, ensure_ascii=False)
if Config.reply_in_thread and message_id:
if message_id:
url = f"{FEISHU_API_BASE}/im/v1/messages/{message_id}/reply"
payload = {"msg_type": "text", "content": content}
else:
@@ -535,6 +575,48 @@ def upsert_feishu_app(body: dict[str, Any]) -> dict[str, Any]:
}
def delete_feishu_app(app_id: str) -> dict[str, Any]:
app_id = app_id.strip()
if not APP_ID_RE.match(app_id):
raise ValueError("App ID must look like cli_xxx")
env = parse_env_file(Config.env_file)
suffix = ""
for candidate in app_suffixes():
if env.get(f"FEISHU_APP_ID{candidate}") == app_id:
suffix = candidate
break
else:
raise KeyError(app_id)
remove_keys = {
f"FEISHU_APP_ID{suffix}",
f"FEISHU_APP_SECRET{suffix}",
f"FEISHU_VERIFICATION_TOKEN{suffix}",
f"FEISHU_DEFAULT_RECEIVE_ID{suffix}",
f"FEISHU_DEFAULT_RECEIVE_ID_TYPE{suffix}",
f"FEISHU_ALLOWED_CHAT_IDS{suffix}",
}
updates: dict[str, str] = {}
current_default_app_id = env.get("FEISHU_DEFAULT_APP_ID") or env.get("FEISHU_APP_ID", "")
if current_default_app_id == app_id:
remaining_app_ids = [
env.get(f"FEISHU_APP_ID{candidate}", "")
for candidate in app_suffixes()
if candidate != suffix and env.get(f"FEISHU_APP_ID{candidate}", "")
]
remove_keys.add("FEISHU_DEFAULT_APP_ID")
if remaining_app_ids:
updates["FEISHU_DEFAULT_APP_ID"] = remaining_app_ids[0]
write_env_removals(Config.env_file, remove_keys, updates)
sync_tokens_file()
reload_config_from_env_file()
token_cache.drop(app_id)
return {"app_id": 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:
@@ -547,6 +629,23 @@ def handle_apps_admin(headers: dict[str, str], body: dict[str, Any]) -> tuple[in
return 200, {"code": 0, "msg": "ok", "app": app}
def handle_apps_delete(path: str, headers: dict[str, str]) -> tuple[int, dict[str, Any]]:
ok, status, message = is_admin_request(headers)
if not ok:
return status, {"code": status, "msg": message}
route = urllib.parse.urlparse(path).path
app_id = urllib.parse.unquote(route[len("/feishu/apps/") :].strip("/"))
try:
app = delete_feishu_app(app_id)
except ValueError as exc:
return 400, {"code": 400, "msg": str(exc)}
except KeyError:
return 404, {"code": 404, "msg": f"unknown Feishu app_id: {app_id}"}
logging.info("deleted 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]]:
expected = Config.notify_token
if not expected:
@@ -608,6 +707,18 @@ class Handler(BaseHTTPRequestHandler):
status, payload = 500, {"code": 500, "msg": str(exc)}
self.send_json(status, payload)
def do_DELETE(self) -> None:
try:
headers = {key.lower(): value for key, value in self.headers.items()}
if self.path.startswith("/feishu/apps/"):
status, payload = handle_apps_delete(self.path, headers)
else:
status, payload = 404, {"code": 404, "msg": "not found"}
except Exception as exc:
logging.error("request failed:\n%s", traceback.format_exc())
status, payload = 500, {"code": 500, "msg": str(exc)}
self.send_json(status, payload)
def read_json(self) -> dict[str, Any]:
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length).decode("utf-8") if length else "{}"