auto-save 2026-05-09 16:40 (~7)

This commit is contained in:
2026-05-09 16:40:34 +08:00
parent 211ce9de23
commit af00c61db6
7 changed files with 158 additions and 74 deletions

View File

@@ -1,26 +1,5 @@
{ {
"entries": [ "entries": [
{
"files_changed": 1,
"hash": "e1ac8cd",
"message": "auto-save 2026-05-07 12:46 (~1)",
"ts": "2026-05-07T12:46:08+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "9a789b4",
"message": "auto-save 2026-05-07 12:51 (~1)",
"ts": "2026-05-07T12:51:59+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "d44069d",
"message": "auto-save 2026-05-07 12:57 (~1)",
"ts": "2026-05-07T12:57:51+08:00",
"type": "commit"
},
{ {
"files_changed": 1, "files_changed": 1,
"hash": "8eed9ec", "hash": "8eed9ec",
@@ -3489,6 +3468,25 @@
"message": "auto-save 2026-05-09 16:29 (~1)", "message": "auto-save 2026-05-09 16:29 (~1)",
"hash": "bdb8fe7", "hash": "bdb8fe7",
"files_changed": 1 "files_changed": 1
},
{
"ts": "2026-05-09T16:35:00+08:00",
"type": "commit",
"message": "auto-save 2026-05-09 16:34 (~4)",
"hash": "211ce9d",
"files_changed": 4
},
{
"ts": "2026-05-09T08:35:52Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-09 16:34 (~4)",
"files_changed": 1
},
{
"ts": "2026-05-09T08:38:27Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 2 项未提交变更 · 最近提交auto-save 2026-05-09 16:34 (~4)",
"files_changed": 2
} }
] ]
} }

View File

@@ -8,6 +8,10 @@
{ {
"category" : "Feishu 自建应用", "category" : "Feishu 自建应用",
"entry" : "Hermes Feishu App cli_a974771369bb1bc3" "entry" : "Hermes Feishu App cli_a974771369bb1bc3"
},
{
"category" : "Feishu 自建应用",
"entry" : "Hermes Feishu App cli_a97764e101b95be9"
} }
], ],
"description" : "Fork from 20260414-hermes-glass-ui, 单用户个人 VPS 部署 (hermes.kang-kang.com)", "description" : "Fork from 20260414-hermes-glass-ui, 单用户个人 VPS 部署 (hermes.kang-kang.com)",
@@ -39,6 +43,11 @@
"type" : "backend", "type" : "backend",
"url" : "https:\/\/hermes.kang-kang.com\/feishu\/events" "url" : "https:\/\/hermes.kang-kang.com\/feishu\/events"
}, },
{
"label" : "feishu-events-cli_a97764e101b95be9",
"type" : "backend",
"url" : "https:\/\/hermes.kang-kang.com\/feishu\/events\/cli_a97764e101b95be9"
},
{ {
"label" : "feishu-notify", "label" : "feishu-notify",
"type" : "backend", "type" : "backend",

View File

@@ -15,6 +15,7 @@
`server/feishu_bridge.py` 提供飞书双向桥接: `server/feishu_bridge.py` 提供飞书双向桥接:
- `POST /feishu/events`: 飞书事件回调 → Hermes → 飞书回复 - `POST /feishu/events`: 飞书事件回调 → Hermes → 飞书回复
- `POST /feishu/events/{app_id}`: 多个飞书机器人共用桥服务时按路径区分
- `POST /feishu/notify`: Hermes / 内部系统主动推送到飞书 - `POST /feishu/notify`: Hermes / 内部系统主动推送到飞书
线上已部署为 systemd `hermes-feishu-bridge.service`nginx 已公开 `/feishu/` 反代。凭证通过部署环境变量配置,详见 `server/feishu-bridge.env.example`;不要把 App Secret、Hermes API key 或通知 token 写入仓库。 线上已部署为 systemd `hermes-feishu-bridge.service`nginx 已公开 `/feishu/` 反代。凭证通过部署环境变量配置,详见 `server/feishu-bridge.env.example`;不要把 App Secret、Hermes API key 或通知 token 写入仓库。

View File

@@ -10,6 +10,7 @@
- API / 后端:同域 `/api/v1` 转发到 LXC `hermes-personal` 内的 `hermes-agent:8642` - API / 后端:同域 `/api/v1` 转发到 LXC `hermes-personal` 内的 `hermes-agent:8642`
- 飞书桥接:已部署 systemd `hermes-feishu-bridge.service`,宿主 `127.0.0.1:8787` - 飞书桥接:已部署 systemd `hermes-feishu-bridge.service`,宿主 `127.0.0.1:8787`
- 飞书事件回调https://hermes.kang-kang.com/feishu/events - 飞书事件回调https://hermes.kang-kang.com/feishu/events
- 飞书事件回调(`cli_a97764e101b95be9`https://hermes.kang-kang.com/feishu/events/cli_a97764e101b95be9
- 飞书主动通知https://hermes.kang-kang.com/feishu/notify - 飞书主动通知https://hermes.kang-kang.com/feishu/notify
- 文档 / 解析https://styles.kang-kang.com - 文档 / 解析https://styles.kang-kang.com
- 管理后台:待定 - 管理后台:待定
@@ -32,8 +33,13 @@
- 飞书桥接服务: - 飞书桥接服务:
- `FEISHU_APP_ID` - `FEISHU_APP_ID`
- `FEISHU_APP_SECRET`(敏感,不入库) - `FEISHU_APP_SECRET`(敏感,不入库)
- `FEISHU_APP_ID_2`
- `FEISHU_APP_SECRET_2`(敏感,不入库)
- `FEISHU_VERIFICATION_TOKEN`(可选,建议配置) - `FEISHU_VERIFICATION_TOKEN`(可选,建议配置)
- `FEISHU_VERIFICATION_TOKEN_2`(可选,建议配置)
- `FEISHU_DEFAULT_RECEIVE_ID` / `FEISHU_DEFAULT_RECEIVE_ID_TYPE`(主动通知默认目标) - `FEISHU_DEFAULT_RECEIVE_ID` / `FEISHU_DEFAULT_RECEIVE_ID_TYPE`(主动通知默认目标)
- `FEISHU_DEFAULT_RECEIVE_ID_2` / `FEISHU_DEFAULT_RECEIVE_ID_TYPE_2`(第二应用主动通知默认目标)
- `FEISHU_DEFAULT_APP_ID`
- `FEISHU_NOTIFY_TOKEN`(主动通知内部鉴权,敏感,不入库) - `FEISHU_NOTIFY_TOKEN`(主动通知内部鉴权,敏感,不入库)
- `HERMES_API_BASE` - `HERMES_API_BASE`
- `HERMES_API_KEY`(敏感,不入库) - `HERMES_API_KEY`(敏感,不入库)

View File

@@ -3,7 +3,9 @@
双向桥接服务: 双向桥接服务:
- `POST /feishu/events`:飞书事件回调。收到文本消息后后台调用 Hermes再发回飞书。 - `POST /feishu/events`:飞书事件回调。收到文本消息后后台调用 Hermes再发回飞书。
- `POST /feishu/events/{app_id}`:多个飞书应用共用桥服务时,按路径指定应用。
- `POST /feishu/notify`Hermes 或内部系统主动通知飞书。必须带 `Authorization: Bearer $FEISHU_NOTIFY_TOKEN``X-Hermes-Feishu-Token` - `POST /feishu/notify`Hermes 或内部系统主动通知飞书。必须带 `Authorization: Bearer $FEISHU_NOTIFY_TOKEN``X-Hermes-Feishu-Token`
- `POST /feishu/notify/{app_id}`:主动通知时指定发消息的飞书应用,也可在 JSON 里传 `app_id`
- `GET /health`:健康检查。 - `GET /health`:健康检查。
## 凭证 ## 凭证
@@ -13,7 +15,7 @@
## 飞书后台配置 ## 飞书后台配置
1. 给自建应用开通消息相关权限,例如接收消息事件、获取与发送单聊/群组消息。 1. 给自建应用开通消息相关权限,例如接收消息事件、获取与发送单聊/群组消息。
2. 事件订阅里添加请求地址:`https://hermes.kang-kang.com/feishu/events` 2. 事件订阅里添加请求地址:默认应用用 `https://hermes.kang-kang.com/feishu/events`;其它应用用 `https://hermes.kang-kang.com/feishu/events/{app_id}`
3. 如果启用事件加密,需要先给本服务补充解密支持;当前版本按明文事件回调处理。 3. 如果启用事件加密,需要先给本服务补充解密支持;当前版本按明文事件回调处理。
4. 建议配置 `FEISHU_VERIFICATION_TOKEN`,并保持和飞书后台一致。 4. 建议配置 `FEISHU_VERIFICATION_TOKEN`,并保持和飞书后台一致。
@@ -24,4 +26,9 @@ curl -X POST https://hermes.kang-kang.com/feishu/notify \
-H "Authorization: Bearer $FEISHU_NOTIFY_TOKEN" \ -H "Authorization: Bearer $FEISHU_NOTIFY_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"receive_id_type":"chat_id","receive_id":"oc_xxx","text":"任务完成"}' -d '{"receive_id_type":"chat_id","receive_id":"oc_xxx","text":"任务完成"}'
curl -X POST https://hermes.kang-kang.com/feishu/notify/cli_xxx \
-H "Authorization: Bearer $FEISHU_NOTIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"receive_id_type":"chat_id","receive_id":"oc_xxx","text":"任务完成"}'
``` ```

View File

@@ -3,8 +3,15 @@ FEISHU_APP_SECRET=replace-with-secret
FEISHU_VERIFICATION_TOKEN= FEISHU_VERIFICATION_TOKEN=
FEISHU_DEFAULT_RECEIVE_ID= FEISHU_DEFAULT_RECEIVE_ID=
FEISHU_DEFAULT_RECEIVE_ID_TYPE=chat_id FEISHU_DEFAULT_RECEIVE_ID_TYPE=chat_id
FEISHU_APP_ID_2=cli_yyyyyyyyyyyyyyyy
FEISHU_APP_SECRET_2=replace-with-second-secret
FEISHU_VERIFICATION_TOKEN_2=
FEISHU_DEFAULT_RECEIVE_ID_2=
FEISHU_DEFAULT_RECEIVE_ID_TYPE_2=chat_id
FEISHU_DEFAULT_APP_ID=cli_xxxxxxxxxxxxxxxx
FEISHU_NOTIFY_TOKEN=replace-with-random-internal-token FEISHU_NOTIFY_TOKEN=replace-with-random-internal-token
FEISHU_ALLOWED_CHAT_IDS= FEISHU_ALLOWED_CHAT_IDS=
FEISHU_ALLOWED_CHAT_IDS_2=
FEISHU_REPLY_IN_THREAD=false FEISHU_REPLY_IN_THREAD=false
HERMES_API_BASE=http://127.0.0.1:8642/v1 HERMES_API_BASE=http://127.0.0.1:8642/v1

View File

@@ -15,6 +15,7 @@ import threading
import time import time
import traceback import traceback
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any from typing import Any
@@ -27,19 +28,42 @@ def _env(name: str, default: str = "") -> str:
return os.environ.get(name, default).strip() return os.environ.get(name, default).strip()
def _bool_env(name: str, default: str = "false") -> bool:
return _env(name, default).lower() in {"1", "true", "yes"}
def _csv_set(value: str) -> set[str]:
return {item.strip() for item in value.split(",") if item.strip()}
def _load_feishu_apps() -> dict[str, dict[str, Any]]:
apps: dict[str, dict[str, Any]] = {}
for suffix in ["", *[f"_{idx}" for idx in range(2, 10)]]:
app_id = _env(f"FEISHU_APP_ID{suffix}")
app_secret = _env(f"FEISHU_APP_SECRET{suffix}")
if not app_id and not app_secret:
continue
apps[app_id] = {
"app_id": app_id,
"app_secret": app_secret,
"verification_token": _env(f"FEISHU_VERIFICATION_TOKEN{suffix}"),
"default_receive_id": _env(f"FEISHU_DEFAULT_RECEIVE_ID{suffix}"),
"default_receive_id_type": _env(
f"FEISHU_DEFAULT_RECEIVE_ID_TYPE{suffix}",
"chat_id",
),
"allowed_chat_ids": _csv_set(_env(f"FEISHU_ALLOWED_CHAT_IDS{suffix}")),
}
return apps
class Config: class Config:
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_verification_token = _env("FEISHU_VERIFICATION_TOKEN") feishu_apps = _load_feishu_apps()
feishu_default_receive_id = _env("FEISHU_DEFAULT_RECEIVE_ID") default_feishu_app_id = _env("FEISHU_DEFAULT_APP_ID", feishu_app_id)
feishu_default_receive_id_type = _env("FEISHU_DEFAULT_RECEIVE_ID_TYPE", "chat_id")
notify_token = _env("FEISHU_NOTIFY_TOKEN") notify_token = _env("FEISHU_NOTIFY_TOKEN")
allowed_chat_ids = { reply_in_thread = _bool_env("FEISHU_REPLY_IN_THREAD")
item.strip()
for item in _env("FEISHU_ALLOWED_CHAT_IDS").split(",")
if item.strip()
}
reply_in_thread = _env("FEISHU_REPLY_IN_THREAD", "false").lower() in {"1", "true", "yes"}
hermes_api_base = _env("HERMES_API_BASE", "http://127.0.0.1:8642/v1").rstrip("/") hermes_api_base = _env("HERMES_API_BASE", "http://127.0.0.1:8642/v1").rstrip("/")
hermes_api_key = _env("HERMES_API_KEY") hermes_api_key = _env("HERMES_API_KEY")
@@ -55,20 +79,23 @@ class Config:
class TokenCache: class TokenCache:
def __init__(self) -> None: def __init__(self) -> None:
self._lock = threading.Lock() self._lock = threading.Lock()
self._token = "" self._tokens: dict[str, tuple[str, float]] = {}
self._expires_at = 0.0
def get(self) -> str: def get(self, app_id: str) -> str:
with self._lock: with self._lock:
if self._token and time.time() < self._expires_at - 300: token, expires_at = self._tokens.get(app_id, ("", 0.0))
return self._token if token and time.time() < expires_at - 300:
return token
if not Config.feishu_app_id or not Config.feishu_app_secret: app = Config.feishu_apps.get(app_id)
raise RuntimeError("FEISHU_APP_ID and FEISHU_APP_SECRET are required") if not app:
raise RuntimeError(f"unknown Feishu app_id: {app_id}")
if not app.get("app_id") or not app.get("app_secret"):
raise RuntimeError(f"FEISHU_APP_ID and FEISHU_APP_SECRET are required for {app_id}")
payload = { payload = {
"app_id": Config.feishu_app_id, "app_id": app["app_id"],
"app_secret": Config.feishu_app_secret, "app_secret": app["app_secret"],
} }
data = http_json( data = http_json(
"POST", "POST",
@@ -78,9 +105,10 @@ class TokenCache:
if data.get("code") != 0: if data.get("code") != 0:
raise RuntimeError(f"Feishu token error: {data}") raise RuntimeError(f"Feishu token error: {data}")
self._token = str(data["tenant_access_token"]) token = str(data["tenant_access_token"])
self._expires_at = time.time() + int(data.get("expire", 7200)) expires_at = time.time() + int(data.get("expire", 7200))
return self._token self._tokens[app_id] = (token, expires_at)
return token
token_cache = TokenCache() token_cache = TokenCache()
@@ -131,8 +159,25 @@ def json_text(value: Any) -> str:
return "" return ""
def verify_callback_token(body: dict[str, Any]) -> bool: def resolve_app_id(path: str, body: dict[str, Any] | None = None) -> str:
expected = Config.feishu_verification_token route_app_id = ""
for prefix in ("/feishu/events/", "/feishu/notify/"):
if path.startswith(prefix):
route_app_id = urllib.parse.unquote(path[len(prefix) :].strip("/"))
break
if route_app_id:
return route_app_id
body = body or {}
body_app_id = str(body.get("app_id") or body.get("header", {}).get("app_id") or "").strip()
if body_app_id in Config.feishu_apps:
return body_app_id
return Config.default_feishu_app_id
def verify_callback_token(body: dict[str, Any], app_id: str) -> bool:
app = Config.feishu_apps.get(app_id, {})
expected = app.get("verification_token", "")
if not expected: if not expected:
return True return True
token = body.get("token") or body.get("header", {}).get("token") token = body.get("token") or body.get("header", {}).get("token")
@@ -153,7 +198,11 @@ def remember_event(event_id: str) -> bool:
return True return True
def handle_feishu_event(body: dict[str, Any]) -> tuple[int, dict[str, Any]]: def handle_feishu_event(path: str, body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
app_id = resolve_app_id(path, body)
if app_id not in Config.feishu_apps:
return 404, {"code": 404, "msg": f"unknown Feishu app_id: {app_id}"}
if "encrypt" in body: if "encrypt" in body:
return 400, { return 400, {
"code": 400, "code": 400,
@@ -161,26 +210,27 @@ def handle_feishu_event(body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
} }
if body.get("type") == "url_verification": if body.get("type") == "url_verification":
if not verify_callback_token(body): if not verify_callback_token(body, app_id):
return 403, {"code": 403, "msg": "invalid verification token"} return 403, {"code": 403, "msg": "invalid verification token"}
return 200, {"challenge": body.get("challenge", "")} return 200, {"challenge": body.get("challenge", "")}
if not verify_callback_token(body): if not verify_callback_token(body, app_id):
return 403, {"code": 403, "msg": "invalid verification token"} return 403, {"code": 403, "msg": "invalid verification token"}
event_type = body.get("header", {}).get("event_type") or body.get("event", {}).get("type") event_type = body.get("header", {}).get("event_type") or body.get("event", {}).get("type")
event_id = body.get("header", {}).get("event_id", "") event_id = body.get("header", {}).get("event_id", "")
if event_type != "im.message.receive_v1": if event_type != "im.message.receive_v1":
return 200, {"code": 0, "msg": "ignored"} return 200, {"code": 0, "msg": "ignored"}
if not remember_event(event_id): if not remember_event(f"{app_id}:{event_id}"):
return 200, {"code": 0, "msg": "duplicate"} return 200, {"code": 0, "msg": "duplicate"}
threading.Thread(target=process_message_event, args=(body,), daemon=True).start() threading.Thread(target=process_message_event, args=(app_id, body), daemon=True).start()
return 200, {"code": 0, "msg": "ok"} return 200, {"code": 0, "msg": "ok"}
def process_message_event(body: dict[str, Any]) -> None: def process_message_event(app_id: str, body: dict[str, Any]) -> None:
try: try:
app = Config.feishu_apps[app_id]
event = body.get("event", {}) event = body.get("event", {})
sender = event.get("sender", {}) sender = event.get("sender", {})
if sender.get("sender_type") == "app": if sender.get("sender_type") == "app":
@@ -190,21 +240,22 @@ def process_message_event(body: dict[str, Any]) -> None:
message_type = message.get("message_type") message_type = message.get("message_type")
chat_id = message.get("chat_id", "") chat_id = message.get("chat_id", "")
message_id = message.get("message_id", "") message_id = message.get("message_id", "")
if Config.allowed_chat_ids and chat_id not in Config.allowed_chat_ids: allowed_chat_ids = app.get("allowed_chat_ids", set())
logging.info("ignored message from disallowed chat_id=%s", chat_id) if allowed_chat_ids and chat_id not in allowed_chat_ids:
logging.info("ignored message from disallowed app_id=%s chat_id=%s", app_id, chat_id)
return return
if message_type != "text": if message_type != "text":
send_feishu_text(chat_id, "我现在只支持处理文本消息。", message_id=message_id) send_feishu_text(app_id, chat_id, "我现在只支持处理文本消息。", message_id=message_id)
return return
text = json_text(message.get("content")) text = json_text(message.get("content"))
if not text: if not text:
send_feishu_text(chat_id, "我没有读到文本内容。", message_id=message_id) send_feishu_text(app_id, chat_id, "我没有读到文本内容。", message_id=message_id)
return return
answer = ask_hermes(text, event) answer = ask_hermes(text, event)
send_feishu_text(chat_id, answer, message_id=message_id) send_feishu_text(app_id, chat_id, answer, message_id=message_id)
except Exception: except Exception:
logging.error("failed to process Feishu event:\n%s", traceback.format_exc()) logging.error("failed to process Feishu event:\n%s", traceback.format_exc())
@@ -242,6 +293,7 @@ def ask_hermes(text: str, event: dict[str, Any]) -> str:
def send_feishu_text( def send_feishu_text(
app_id: str,
receive_id: str, receive_id: str,
text: str, text: str,
receive_id_type: str = "chat_id", receive_id_type: str = "chat_id",
@@ -250,7 +302,7 @@ def send_feishu_text(
if not receive_id: if not receive_id:
raise RuntimeError("receive_id is required") raise RuntimeError("receive_id is required")
token = token_cache.get() token = token_cache.get(app_id)
chunks = split_text(text) chunks = split_text(text)
result: dict[str, Any] = {} result: dict[str, Any] = {}
for chunk in chunks: for chunk in chunks:
@@ -288,7 +340,7 @@ def split_text(text: str, limit: int = 3800) -> list[str]:
return chunks return chunks
def handle_notify(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:
return 503, {"code": 503, "msg": "FEISHU_NOTIFY_TOKEN is not configured"} return 503, {"code": 503, "msg": "FEISHU_NOTIFY_TOKEN is not configured"}
@@ -300,18 +352,23 @@ def handle_notify(headers: dict[str, str], body: dict[str, Any]) -> tuple[int, d
if token != expected: if token != expected:
return 401, {"code": 401, "msg": "unauthorized"} return 401, {"code": 401, "msg": "unauthorized"}
app_id = str(body.get("app_id") or resolve_app_id(path, body)).strip()
app = Config.feishu_apps.get(app_id)
if not app:
return 404, {"code": 404, "msg": f"unknown Feishu app_id: {app_id}"}
text = str(body.get("text", "")).strip() text = str(body.get("text", "")).strip()
if not text: if not text:
return 400, {"code": 400, "msg": "text is required"} return 400, {"code": 400, "msg": "text is required"}
receive_id = str(body.get("receive_id") or Config.feishu_default_receive_id).strip() receive_id = str(body.get("receive_id") or app.get("default_receive_id", "")).strip()
receive_id_type = str( receive_id_type = str(
body.get("receive_id_type") or Config.feishu_default_receive_id_type body.get("receive_id_type") or app.get("default_receive_id_type", "chat_id")
).strip() ).strip()
if not receive_id: if not receive_id:
return 400, {"code": 400, "msg": "receive_id is required"} return 400, {"code": 400, "msg": "receive_id is required"}
result = send_feishu_text(receive_id, text, receive_id_type=receive_id_type) result = send_feishu_text(app_id, receive_id, text, receive_id_type=receive_id_type)
return 200, {"code": 0, "msg": "ok", "feishu": result} return 200, {"code": 0, "msg": "ok", "app_id": app_id, "feishu": result}
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
@@ -327,10 +384,10 @@ class Handler(BaseHTTPRequestHandler):
try: try:
body = self.read_json() body = self.read_json()
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": if self.path == "/feishu/events" or self.path.startswith("/feishu/events/"):
status, payload = handle_feishu_event(body) status, payload = handle_feishu_event(self.path, body)
elif self.path == "/feishu/notify": elif self.path == "/feishu/notify" or self.path.startswith("/feishu/notify/"):
status, payload = handle_notify(headers, body) status, payload = handle_notify(self.path, headers, body)
else: else:
status, payload = 404, {"code": 404, "msg": "not found"} status, payload = 404, {"code": 404, "msg": "not found"}
except Exception as exc: except Exception as exc:
@@ -365,16 +422,15 @@ def main() -> None:
level=os.environ.get("LOG_LEVEL", "INFO"), level=os.environ.get("LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(message)s", format="%(asctime)s %(levelname)s %(message)s",
) )
missing = [ missing = []
name if not Config.feishu_apps:
for name, value in { missing.append("FEISHU_APP_ID and FEISHU_APP_SECRET")
"FEISHU_APP_ID": Config.feishu_app_id, for app_id, app in Config.feishu_apps.items():
"FEISHU_APP_SECRET": Config.feishu_app_secret, if not app.get("app_secret"):
}.items() missing.append(f"FEISHU_APP_SECRET for {app_id}")
if not value
]
if missing: if missing:
logging.warning("missing required env: %s", ", ".join(missing)) logging.warning("missing required env: %s", ", ".join(missing))
logging.info("configured Feishu apps: %s", ", ".join(Config.feishu_apps) or "(none)")
httpd = ThreadingHTTPServer(("0.0.0.0", Config.port), Handler) httpd = ThreadingHTTPServer(("0.0.0.0", Config.port), Handler)
logging.info("Feishu bridge listening on :%s", Config.port) logging.info("Feishu bridge listening on :%s", Config.port)
httpd.serve_forever() httpd.serve_forever()