cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)

This commit is contained in:
host committed 2026-10-26 01:23:21 +08:00
1 parent b97c34465f
commit b74d99e959
17 files changed
+3420

No files matched your search

+238
View File
@@ -0,0 +1,238 @@
"""外部集成 webhook 与推送通知模块(云端架构实施)。
零第三方依赖,只用 Python 标准库。
本模块只做两件事:
1. 入站 webhook 的签名校验与事件解析(GitHub、Slack)。
2. 出站状态通知的占位分发(webhook + Slack)。
边界(不要超出本模块宣称的范围):
- Web Push / APNs / FCM 明确是后续需要证书的一步,本版只有
webhook + Slack 占位,不要编造已接通手机推送。
- 密钥走环境变量是调用方的事:本模块的校验函数只接受参数,
不自己读生产密钥,也不把任何密钥写入文件。
- 本模块不联网、不调用真实 GitHub/Slack、不跑真实模型回合。
"""
import hashlib
import hmac
import sqlite3
import time
__all__ = [
"verify_github_signature",
"parse_github_pr_comment",
"verify_slack_signature",
"parse_slack_command",
"SlackUserMap",
"ALLOWED_NOTIFICATION_EVENTS",
"NotificationService",
]
def verify_github_signature(secret, payload_bytes, signature_header):
"""校验 GitHub webhook 签名。
signature_header 形如 ``"sha256=<hex>"``,用
``hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()``
比对,比较用 ``hmac.compare_digest``。缺 secret 或缺 header 返回 False。
"""
if not secret or not signature_header:
return False
if isinstance(payload_bytes, str):
payload_bytes = payload_bytes.encode("utf-8")
if not isinstance(payload_bytes, (bytes, bytearray)):
return False
if not isinstance(signature_header, str):
return False
try:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"), bytes(payload_bytes), hashlib.sha256
).hexdigest()
except (AttributeError, TypeError, ValueError):
return False
return hmac.compare_digest(expected, signature_header)
def parse_github_pr_comment(payload):
"""解析 GitHub issue_comment 事件,返回 fix-review 触发点或 None。
这是 PR 评论 → fix-review 的触发点,真正执行 fix-review 由控制面调
host.py 完成,本函数只解析不执行。
只有当 action == "created" 且 comment body 里含 "/fix-review" 时返回::
{"type": "fix_review", "repo": ..., "pr_number": ...,
"comment": ..., "author": ...}
其中 repo 取 payload repository full_name,pr_number 取 issue number,
comment 取 body,author 取 comment user login。其他情况返回 None。
"""
if not isinstance(payload, dict):
return None
if payload.get("action") != "created":
return None
comment = payload.get("comment")
if not isinstance(comment, dict):
return None
body = comment.get("body")
if not isinstance(body, str) or "/fix-review" not in body:
return None
repository = payload.get("repository") or {}
issue = payload.get("issue") or {}
user = comment.get("user") or {}
return {
"type": "fix_review",
"repo": repository.get("full_name"),
"pr_number": issue.get("number"),
"comment": body,
"author": user.get("login"),
}
def verify_slack_signature(signing_secret, timestamp, body, signature_header):
"""校验 Slack 请求签名(防重放)。
Slack 规则:basestring = ``f"v0:{timestamp}:{body}"``,hmac sha256,
header 形如 ``"v0=<hex>"``。时间戳与当前时间差超过 300 秒返回 False
(防重放);缺参返回 False。
"""
if not signing_secret or timestamp is None or body is None:
return False
if not signature_header:
return False
try:
ts_int = int(timestamp)
except (TypeError, ValueError):
return False
if abs(time.time() - ts_int) > 300:
return False
try:
if isinstance(body, (bytes, bytearray)):
basestring = b"v0:" + str(ts_int).encode("utf-8") + b":" + bytes(body)
expected = "v0=" + hmac.new(
signing_secret.encode("utf-8"), basestring, hashlib.sha256
).hexdigest()
else:
basestring = f"v0:{timestamp}:{body}"
expected = "v0=" + hmac.new(
signing_secret.encode("utf-8"),
basestring.encode("utf-8"),
hashlib.sha256,
).hexdigest()
except (AttributeError, TypeError, ValueError):
return False
if not isinstance(signature_header, str):
return False
return hmac.compare_digest(expected, signature_header)
def parse_slack_command(text):
"""解析 Slack 斜杠命令。
- ``"/codex 做一个加法函数"`` ->
``{"command": "codex", "prompt": "做一个加法函数"}``
- ``"/codex status <任务名>"`` ->
``{"command": "status", "task": "<任务名>"}``
- 空或非 ``/codex`` 开头 -> ``{"command": None, "prompt": text}``
"""
if not text or not isinstance(text, str):
return {"command": None, "prompt": text}
stripped = text.strip()
if stripped == "/codex" or stripped.startswith("/codex "):
rest = stripped[len("/codex"):].strip()
if rest == "status" or rest.startswith("status "):
task = rest[len("status"):].strip()
return {"command": "status", "task": task}
return {"command": "codex", "prompt": rest}
return {"command": None, "prompt": text}
class SlackUserMap:
"""Slack 用户 ID 到内部 user_id 的映射(SQLite)。"""
def __init__(self, db_path):
self.db_path = db_path
self._conn = sqlite3.connect(db_path)
self._conn.execute(
"CREATE TABLE IF NOT EXISTS slack_users ("
"slack_id TEXT PRIMARY KEY, "
"user_id TEXT, "
"created_at REAL)"
)
self._conn.commit()
def map(self, slack_id, user_id):
"""把 slack_id 映射到 user_id(已存在则覆盖)。"""
self._conn.execute(
"INSERT OR REPLACE INTO slack_users "
"(slack_id, user_id, created_at) VALUES (?, ?, ?)",
(slack_id, user_id, time.time()),
)
self._conn.commit()
def lookup(self, slack_id):
"""返回 slack_id 对应的 user_id,未映射返回 None。"""
row = self._conn.execute(
"SELECT user_id FROM slack_users WHERE slack_id = ?",
(slack_id,),
).fetchone()
return row[0] if row else None
def close(self):
"""关闭数据库连接(可选,单测/退出时用)。"""
self._conn.close()
# 只发状态变化的事件集合:进行中的状态(如 running)不触发通知。
ALLOWED_NOTIFICATION_EVENTS = {
"completed",
"failed",
"goal-unmet",
"merge-failed",
"review-requested",
}
class NotificationService:
"""状态通知分发(webhook + Slack 占位)。
Web Push / APNs / FCM 明确是后续需要证书的一步,本版只有 webhook +
Slack 占位,不要编造已接通手机推送。
sender 都是可调用对象,单测可注入假 sender,默认 None 时不发、
不报错。sender 被调用时收到一个 dict payload::
{"event_type": ..., "task_id": ..., "project_id": ..., "detail": ...}
"""
def __init__(self, webhook_sender=None, slack_sender=None):
self.webhook_sender = webhook_sender
self.slack_sender = slack_sender
def notify(self, event_type, task_id, project_id, detail=""):
"""发送一条状态通知。
event_type 不在 ALLOWED_NOTIFICATION_EVENTS 里时返回
``{"sent": False, "reason": "event not allowed"}`` 且不调用 sender;
否则依次调 webhook_sender 和 slack_sender(不为 None 的才调),
返回 ``{"sent": True, "channels": [...已调的通道名...]}``。
"""
if event_type not in ALLOWED_NOTIFICATION_EVENTS:
return {"sent": False, "reason": "event not allowed"}
payload = {
"event_type": event_type,
"task_id": task_id,
"project_id": project_id,
"detail": detail,
}
channels = []
if self.webhook_sender is not None:
self.webhook_sender(payload)
channels.append("webhook")
if self.slack_sender is not None:
self.slack_sender(payload)
channels.append("slack")
return {"sent": True, "channels": channels}