ForcePilot/backend/package/yuxi/channel/extensions/github/auth.py
Kris 702fcd4c33 feat(github): 新增GitHub渠道插件完整实现
新增了完整的GitHub集成插件,包含认证、会话管理、Webhook处理、消息收发、反应支持等核心功能,支持Issues、Discussions、Pull Request的交互与通知。
2026-05-21 10:48:42 +08:00

94 lines
3.1 KiB
Python

from __future__ import annotations
import logging
import time
import httpx
import jwt
logger = logging.getLogger(__name__)
JWT_EXPIRY_SECONDS = 600
TOKEN_REFRESH_MARGIN_SECONDS = 120
INSTALLATION_TOKEN_TTL = 3600
class GitHubAuth:
def __init__(self, app_id: int, private_key: str):
self.app_id = app_id
self.private_key = private_key
self._token_cache: dict[int, tuple[str, float]] = {}
def _generate_jwt(self) -> str:
now = int(time.time())
payload = {
"iat": now - 60,
"exp": now + JWT_EXPIRY_SECONDS,
"iss": str(self.app_id),
}
return jwt.encode(payload, self.private_key, algorithm="RS256")
async def get_installation_token(self, installation_id: int) -> str:
now = time.time()
cached = self._token_cache.get(installation_id)
if cached and now < cached[1] - TOKEN_REFRESH_MARGIN_SECONDS:
return cached[0]
jwt_token = self._generate_jwt()
headers = {
"Authorization": f"Bearer {jwt_token}",
"Accept": "application/vnd.github+json",
}
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
resp = await client.post(
f"https://api.github.com/app/installations/{installation_id}/access_tokens",
headers=headers,
)
resp.raise_for_status()
data = resp.json()
token = data["token"]
expires_at_str = data.get("expires_at", "")
expiry = now + INSTALLATION_TOKEN_TTL
if expires_at_str:
try:
from datetime import datetime as dt
parsed = dt.fromisoformat(expires_at_str.replace("Z", "+00:00"))
expiry = parsed.timestamp()
except (ValueError, OSError):
pass
self._token_cache[installation_id] = (token, expiry)
return token
def clear_cache(self):
self._token_cache.clear()
async def verify_credentials(account: dict, timeout_seconds: float = 10.0) -> dict | None:
auth = GitHubAuth(account["app_id"], account["private_key"])
try:
token = await auth.get_installation_token(account["installation_id"])
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
}
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout_seconds)) as client:
resp = await client.get("https://api.github.com/rate_limit", headers=headers)
resp.raise_for_status()
rate_data = resp.json()
core = rate_data.get("resources", {}).get("core", {})
return {
"ok": True,
"rate_limit": core.get("limit", 5000),
"rate_remaining": core.get("remaining", 5000),
"rate_reset": core.get("reset", 0),
}
except httpx.HTTPStatusError as e:
logger.warning("GitHub credential verification failed: HTTP %d", e.response.status_code)
return None
except Exception:
logger.exception("GitHub credential verification error")
return None