ForcePilot/backend/package/yuxi/channel/extensions/jira/status.py
Kris c5d8e3550e feat(channel): 添加 Jira 渠道扩展
新增 Jira 渠道扩展,支持在 Yuxi 平台中集成 Jira 项目管理渠道。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- outbound: 外发消息与工单管理
- streaming: 流式消息处理
- parse: Jira 内容解析
- format: 消息格式转换
- agent_tools: Agent 工具集成
- security: 安全校验
- dedupe: 消息去重
- loopbreaker: 循环响应防护
- monitor: 渠道状态监控
- status: 会话与工单状态管理
- types: 类型定义
2026-05-21 11:10:54 +08:00

185 lines
6.5 KiB
Python

from __future__ import annotations
import base64
import logging
import httpx
from yuxi.channel.protocols import ChannelAccountSnapshot, StatusProtocol
logger = logging.getLogger(__name__)
class JiraStatus(StatusProtocol):
default_runtime = ChannelAccountSnapshot(account_id="default")
async def probe(self, account: dict | None = None) -> bool:
if not account:
return False
try:
info = await JiraStatus.get_account_info(account)
return info is not None and info.get("accountId") is not None
except Exception:
return False
def build_summary(self, snapshot: object) -> dict:
return {"channel": "jira"}
def build_channel_summary(
self,
account: dict,
config: dict,
default_account_id: str,
snapshot: ChannelAccountSnapshot,
) -> dict:
return {
"channel": "jira",
"account_id": account.get("account_id", default_account_id),
"configured": bool(account.get("site_url") and account.get("email") and account.get("api_token")),
"site_url": account.get("site_url", ""),
"project_key": account.get("project_key", ""),
"running": snapshot.running,
"connected": snapshot.connected,
}
def format_capabilities_probe(self, probe: object) -> list[dict]:
return []
async def audit_account(self, account: dict, timeout_ms: int, config: dict, probe: object | None = None) -> object:
return {"ok": True}
async def build_capabilities_diagnostics(
self,
account: dict,
timeout_ms: int,
config: dict,
probe: object | None = None,
audit: object | None = None,
target: str | None = None,
) -> dict | None:
return None
def build_account_snapshot(
self,
account: dict,
config: dict,
runtime: ChannelAccountSnapshot | None = None,
probe: object | None = None,
audit: object | None = None,
) -> ChannelAccountSnapshot:
return ChannelAccountSnapshot(
account_id=account.get("account_id", ""),
configured=bool(account.get("site_url") and account.get("email") and account.get("api_token")),
enabled=account.get("enabled", True) if isinstance(account.get("enabled"), bool) else True,
probe=probe,
audit=audit,
)
def log_self_id(self, account: dict, config: dict, runtime: object, include_channel_prefix: bool = False) -> None:
pass
def resolve_account_state(
self,
account: dict,
config: dict,
configured: bool,
enabled: bool,
) -> str:
if not configured:
return "unconfigured"
if not enabled:
return "disabled"
return "ready"
def collect_status_issues(self, accounts: list[ChannelAccountSnapshot]) -> list:
return []
@staticmethod
async def probe_account(account: dict) -> bool:
try:
info = await JiraStatus.get_account_info(account)
return info is not None and info.get("accountId") is not None
except Exception:
return False
@staticmethod
async def get_account_info(account: dict) -> dict | None:
site_url = account.get("site_url", "")
email = account.get("email", "")
api_token = account.get("api_token", "")
if not all([site_url, email, api_token]):
return None
credentials = f"{email}:{api_token}"
auth = f"Basic {base64.b64encode(credentials.encode('utf-8')).decode('utf-8')}"
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
resp = await client.get(
f"{site_url}/rest/api/3/myself",
headers={"Authorization": auth, "Accept": "application/json"},
)
if resp.status_code == 200:
data = resp.json()
return {
"accountId": data.get("accountId", ""),
"displayName": data.get("displayName", ""),
"emailAddress": data.get("emailAddress", ""),
"active": data.get("active", False),
}
except Exception:
logger.exception("Jira account info probe failed")
return None
@staticmethod
async def check_webhook_status(account: dict) -> dict | None:
site_url = account.get("site_url", "")
email = account.get("email", "")
api_token = account.get("api_token", "")
webhook_id = account.get("webhook_id", "")
if not all([site_url, email, api_token]):
return None
credentials = f"{email}:{api_token}"
auth = f"Basic {base64.b64encode(credentials.encode('utf-8')).decode('utf-8')}"
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
if webhook_id:
resp = await client.get(
f"{site_url}/rest/api/3/webhook/{webhook_id}",
headers={"Authorization": auth},
)
if resp.status_code == 200:
return resp.json()
resp = await client.get(
f"{site_url}/rest/api/3/webhook",
headers={"Authorization": auth},
)
if resp.status_code == 200:
webhooks = resp.json()
for wh in webhooks:
callback = wh.get("url", "")
if account.get("callback_base_url", "") in callback:
return wh
except Exception:
logger.exception("Jira webhook status check failed")
return None
@staticmethod
def build_summary_data(account: dict, user_info: dict | None = None) -> dict:
summary: dict = {
"account_id": account.get("account_id", ""),
"configured": bool(account.get("site_url") and account.get("email") and account.get("api_token")),
"site_url": account.get("site_url", ""),
"project_key": account.get("project_key", ""),
"comment_visibility": account.get("comment_visibility", "internal"),
}
if user_info:
summary["bot_account_id"] = user_info.get("accountId", "")
summary["display_name"] = user_info.get("displayName", "")
summary["active"] = user_info.get("active", False)
return summary