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

158 lines
5.3 KiB
Python

from __future__ import annotations
import logging
import httpx
logger = logging.getLogger(__name__)
GRAPHQL_ENDPOINT = "https://api.github.com/graphql"
class GitHubGraphQLClient:
def __init__(self, token_getter):
self._get_token = token_getter
async def _execute(self, query: str, variables: dict | None = None) -> dict:
token = await self._get_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
}
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
resp = await client.post(
GRAPHQL_ENDPOINT,
json={"query": query, "variables": variables or {}},
headers=headers,
)
resp.raise_for_status()
return resp.json()
async def get_discussion(self, owner: str, repo: str, number: int) -> dict | None:
query = """
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
discussion(number: $number) {
id
number
title
body
url
createdAt
category { name }
author { login }
comments(first: 50) {
nodes {
id
body
author { login }
createdAt
}
}
}
}
}
"""
result = await self._execute(query, {"owner": owner, "repo": repo, "number": number})
data = result.get("data", {})
repo_data = data.get("repository", {})
discussion = repo_data.get("discussion")
return discussion
async def get_discussion_categories(self, owner: str, repo: str) -> list[dict]:
query = """
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
discussionCategories(first: 50) {
nodes {
id
name
emoji
}
}
}
}
"""
result = await self._execute(query, {"owner": owner, "repo": repo})
data = result.get("data", {})
repo_data = data.get("repository", {})
cats = repo_data.get("discussionCategories", {})
return cats.get("nodes", [])
async def create_discussion(
self, repository_id: str, category_id: str, title: str, body: str,
) -> dict | None:
mutation = """
mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
createDiscussion(input: {
repositoryId: $repoId,
categoryId: $categoryId,
title: $title,
body: $body
}) {
discussion { id number url title }
}
}
"""
result = await self._execute(mutation, {
"repoId": repository_id,
"categoryId": category_id,
"title": title,
"body": body,
})
if "errors" in result:
logger.error("GraphQL createDiscussion error: %s", result["errors"])
return None
return result.get("data", {}).get("createDiscussion", {}).get("discussion")
async def close_discussion(self, discussion_node_id: str, reason: str = "RESOLVED") -> dict | None:
mutation = """
mutation($discussionId: ID!, $reason: DiscussionCloseReason!) {
closeDiscussion(input: { discussionId: $discussionId, reason: $reason }) {
discussion { id number closed }
}
}
"""
result = await self._execute(mutation, {
"discussionId": discussion_node_id,
"reason": reason,
})
if "errors" in result:
logger.error("GraphQL closeDiscussion error: %s", result["errors"])
return None
return result.get("data", {}).get("closeDiscussion", {}).get("discussion")
async def update_discussion(
self, discussion_node_id: str, title: str | None = None, body: str | None = None,
) -> dict | None:
mutation = """
mutation($discussionId: ID!, $title: String, $body: String) {
updateDiscussion(input: { discussionId: $discussionId, title: $title, body: $body }) {
discussion { id number title body url }
}
}
"""
result = await self._execute(mutation, {
"discussionId": discussion_node_id,
"title": title,
"body": body,
})
if "errors" in result:
logger.error("GraphQL updateDiscussion error: %s", result["errors"])
return None
return result.get("data", {}).get("updateDiscussion", {}).get("discussion")
async def delete_discussion(self, discussion_node_id: str) -> dict:
mutation = """
mutation($discussionId: ID!) {
deleteDiscussion(input: { discussionId: $discussionId }) {
discussion { id }
}
}
"""
result = await self._execute(mutation, {"discussionId": discussion_node_id})
if "errors" in result:
logger.error("GraphQL deleteDiscussion error: %s", result["errors"])
return {"success": False, "error": result["errors"][0].get("message", "GraphQL error")}
return {"success": True}