ac8935c641
WeWorkFinanceSdk 是 Go c-shared 库、自带 Go runtime, 嵌进 app-server(uvicorn)后台线程跑会 segfault 把主进程带崩(2026-07-17 实测:开 worker 后 app-server core-dump 每 5s 循环)。改成 轮询搬到【独立进程】(Go runtime 独占进程+主线程, 崩了 systemd 只重启本进程、不动 app-server), 拉到卡片/图后 POST app-server 内部端点打信号(set_pending 在 app-server 进程内存, 心跳才 pop 得到)。 - 删 app/core/wx_finance_worker.py(进程内 worker), main lifespan 不再起它 - 新增内部端点 POST /internal/wx-finance/pending(X-Internal-Secret)→ set_pending(复用现有内部端点范式) - 新增 scripts/wx_finance_poller.py(独立 poller:GetChatData 轮询→解密→weapp/image→POST 内部端点) - 新增 deploy/wx-finance-poller.service(独立 systemd 单元, 与 app-server 隔离) - config 加 WX_FINANCE_INTERNAL_URL(poller→app-server 内部地址, 默认 127.0.0.1:8770) - SDK 封装 wx_finance_sdk.py 原样复用(探针已验证 Init/GetChatData 通) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
"""会话存档【独立进程】轮询 poller —— 单独 systemd 服务跑, 与 app-server 隔离。
|
|
|
|
为什么独立进程:WeWorkFinanceSdk 是 Go 编译的 c-shared 库, 自带 Go runtime。嵌进 app-server(uvicorn)
|
|
后台线程里跑会 segfault、把主进程带崩(2026-07-17 实测 core-dump 每 5s 循环)。独立进程让 Go runtime
|
|
独占进程 + 主线程, 崩了 systemd 只重启本 poller、不动 app-server。拉到美团卡片(weapp)/截图(image)
|
|
→ POST app-server 内部端点 /internal/wx-finance/pending 打比价信号(下游心跳→弹窗→比价 完全复用)。
|
|
|
|
手动跑: cd /opt/shaguabijia-app-server && .venv/bin/python scripts/wx_finance_poller.py
|
|
生产: deploy/wx-finance-poller.service 常驻(systemctl enable --now wx-finance-poller)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import signal
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
from app.core.config import settings
|
|
from app.core.logging import setup_logging
|
|
from app.integrations.wx_finance_sdk import WxFinanceError, WxFinanceSdk
|
|
|
|
logger = logging.getLogger("shagua.wx_finance_poller")
|
|
|
|
_stop = False
|
|
|
|
|
|
def _on_signal(_signum: int, _frame: object) -> None:
|
|
global _stop
|
|
_stop = True
|
|
|
|
|
|
def _load_seq() -> int:
|
|
try:
|
|
return int(Path(settings.WX_FINANCE_SEQ_FILE).read_text().strip() or "0")
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def _save_seq(seq: int) -> None:
|
|
try:
|
|
p = Path(settings.WX_FINANCE_SEQ_FILE)
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(str(seq))
|
|
except Exception:
|
|
logger.exception("seq 持久化失败")
|
|
|
|
|
|
def _notify(client: httpx.Client, source: str, kind: str) -> None:
|
|
"""POST 内部端点让 app-server 打信号(set_pending 在 app-server 进程内存)。"""
|
|
url = f"{settings.WX_FINANCE_INTERNAL_URL.rstrip('/')}/internal/wx-finance/pending"
|
|
try:
|
|
resp = client.post(
|
|
url,
|
|
json={"source": source, "kind": kind},
|
|
headers={"X-Internal-Secret": settings.INTERNAL_API_SECRET},
|
|
timeout=10,
|
|
)
|
|
logger.info("notify app-server (%s) → %s", kind, resp.status_code)
|
|
except Exception:
|
|
logger.exception("notify app-server 失败")
|
|
|
|
|
|
def _handle(client: httpx.Client, msg: dict) -> None:
|
|
"""明文消息:外部用户(from≠接收成员)发来的 weapp/image → 通知 app-server 打信号。"""
|
|
msgtype = msg.get("msgtype") or ""
|
|
frm = msg.get("from") or ""
|
|
receiver = settings.WX_FINANCE_RECEIVER_USERID
|
|
if receiver and frm == receiver:
|
|
return # 成员自己发的, 跳过
|
|
if msgtype not in ("weapp", "image"):
|
|
return
|
|
logger.info("命中触发 from=%s type=%s", frm, msgtype)
|
|
_notify(client, "meituan", msgtype)
|
|
|
|
|
|
def main() -> None:
|
|
setup_logging(debug=settings.APP_DEBUG)
|
|
if not settings.wx_finance_configured:
|
|
logger.warning("WX_FINANCE 未配齐(ENABLED/corpid/存档Secret), poller 退出")
|
|
return
|
|
if not settings.INTERNAL_API_SECRET:
|
|
logger.error("INTERNAL_API_SECRET 未配置, poller 无法通知 app-server, 退出")
|
|
return
|
|
signal.signal(signal.SIGTERM, _on_signal)
|
|
signal.signal(signal.SIGINT, _on_signal)
|
|
|
|
sdk = WxFinanceSdk(
|
|
settings.WX_FINANCE_SDK_PATH,
|
|
settings.WX_KF_CORP_ID,
|
|
settings.WX_FINANCE_SECRET,
|
|
Path(settings.WX_FINANCE_PRIVATE_KEY_PATH).read_bytes(),
|
|
)
|
|
seq = _load_seq()
|
|
logger.info(
|
|
"wx_finance poller 启动, 从 seq=%d 轮询, 间隔 %ds", seq, settings.WX_FINANCE_POLL_INTERVAL_SEC
|
|
)
|
|
with httpx.Client() as client:
|
|
while not _stop:
|
|
try:
|
|
msgs = sdk.get_chat_data(seq, limit=1000)
|
|
if msgs:
|
|
logger.info("拉到 %d 条(seq>%d)", len(msgs), seq)
|
|
for item in msgs:
|
|
try:
|
|
plain = sdk.decrypt(item["encrypt_random_key"], item["encrypt_chat_msg"])
|
|
_handle(client, plain)
|
|
except Exception:
|
|
logger.exception("解密/处理单条失败 seq=%s", item.get("seq"))
|
|
seq = max(seq, int(item.get("seq", seq)))
|
|
if msgs:
|
|
_save_seq(seq)
|
|
except WxFinanceError:
|
|
logger.exception("GetChatData 出错")
|
|
except Exception:
|
|
logger.exception("轮询异常")
|
|
# 可打断的 sleep(收到 SIGTERM 尽快退)
|
|
for _ in range(max(1, settings.WX_FINANCE_POLL_INTERVAL_SEC) * 2):
|
|
if _stop:
|
|
break
|
|
time.sleep(0.5)
|
|
sdk.close()
|
|
logger.info("wx_finance poller 已停止")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|