新增 Jira 渠道扩展,支持在 Yuxi 平台中集成 Jira 项目管理渠道。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息与工单管理 - streaming: 流式消息处理 - parse: Jira 内容解析 - format: 消息格式转换 - agent_tools: Agent 工具集成 - security: 安全校验 - dedupe: 消息去重 - loopbreaker: 循环响应防护 - monitor: 渠道状态监控 - status: 会话与工单状态管理 - types: 类型定义
214 lines
8.3 KiB
Python
214 lines
8.3 KiB
Python
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import logging
|
||
import os
|
||
|
||
from yuxi.channel.protocols import ConfigProtocol
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class JiraConfigAdapter(ConfigProtocol):
|
||
def __init__(self):
|
||
self._config: dict = {}
|
||
|
||
@property
|
||
def _jira_cfg(self) -> dict:
|
||
return self._config.get("channels", {}).get("jira", {})
|
||
|
||
def list_account_ids(self, config: dict) -> list[str]:
|
||
self._config = config
|
||
accounts = self._jira_cfg.get("accounts", {})
|
||
if accounts:
|
||
return list(accounts.keys())
|
||
if self._env_credentials_exist():
|
||
return ["default"]
|
||
return []
|
||
|
||
async def resolve_account(self, account_id: str) -> dict:
|
||
return self._build_account(account_id)
|
||
|
||
def is_configured(self, account: dict) -> bool:
|
||
return bool(account.get("site_url") and account.get("email") and account.get("api_token"))
|
||
|
||
def is_enabled(self, account: dict, config: dict | None = None) -> bool:
|
||
return account.get("enabled", True)
|
||
|
||
def disabled_reason(self, account: dict, config: dict | None = None) -> str:
|
||
return "" if self.is_enabled(account, config) else "账户已禁用"
|
||
|
||
def unconfigured_reason(self, account: dict, config: dict | None = None) -> str:
|
||
return "" if self.is_configured(account) else "未配置 site_url/email/api_token"
|
||
|
||
def describe_account(self, account: dict, config: dict | None = None) -> dict:
|
||
return {
|
||
"account_id": account.get("account_id", ""),
|
||
"name": account.get("name", ""),
|
||
"site_url": account.get("site_url", ""),
|
||
"project_key": account.get("project_key", ""),
|
||
"auth_method": account.get("auth_method", "api_token"),
|
||
"bot_display_name": account.get("bot_display_name", ""),
|
||
"comment_visibility": account.get("comment_visibility", "internal"),
|
||
"configured": self.is_configured(account),
|
||
}
|
||
|
||
def inspect_account(self, config: dict, account_id: str | None = None) -> dict:
|
||
return {}
|
||
|
||
def default_account_id(self, config: dict | None = None) -> str:
|
||
return self._jira_cfg.get("defaultAccount", "default")
|
||
|
||
def set_account_enabled(self, config: dict, account_id: str, enabled: bool) -> dict:
|
||
return config
|
||
|
||
def delete_account(self, config: dict, account_id: str) -> dict:
|
||
return config
|
||
|
||
def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str | int] | None:
|
||
return None
|
||
|
||
def format_allow_from(self, config: dict, account_id: str | None, allow_from: list[str | int]) -> list[str]:
|
||
return [str(e) for e in allow_from]
|
||
|
||
def has_configured_state(self, config: dict) -> bool:
|
||
return bool(self._jira_cfg)
|
||
|
||
def has_persisted_auth_state(self, config: dict) -> bool:
|
||
return True
|
||
|
||
def resolve_default_to(self, config: dict, account_id: str | None = None) -> str | None:
|
||
return None
|
||
|
||
@staticmethod
|
||
def credential_fingerprint(site_url: str, email: str, api_token: str) -> str:
|
||
return hashlib.sha256(f"{site_url}:{email}:{api_token}".encode()).hexdigest()[:8]
|
||
|
||
@staticmethod
|
||
def _env_credentials_exist() -> bool:
|
||
return all(
|
||
[
|
||
os.environ.get("JIRA_SITE_URL", ""),
|
||
os.environ.get("JIRA_EMAIL", ""),
|
||
os.environ.get("JIRA_API_TOKEN", ""),
|
||
]
|
||
)
|
||
|
||
def _build_account(self, account_id: str) -> dict:
|
||
jira_cfg = self._jira_cfg
|
||
accounts = jira_cfg.get("accounts", {})
|
||
account_raw = accounts.get(account_id, {}) if account_id != "default" else jira_cfg
|
||
|
||
def _get(key: str, default=None):
|
||
return account_raw.get(key, jira_cfg.get(key, default))
|
||
|
||
site_url = os.environ.get("JIRA_SITE_URL", "")
|
||
email = os.environ.get("JIRA_EMAIL", "")
|
||
api_token = os.environ.get("JIRA_API_TOKEN", "")
|
||
|
||
if not site_url:
|
||
site_url = _get("siteUrl", "")
|
||
if not email:
|
||
email = _get("email", "")
|
||
if not api_token:
|
||
api_token = _get("apiToken", "")
|
||
|
||
token_source = (
|
||
"env"
|
||
if (site_url and email and api_token and os.environ.get("JIRA_SITE_URL"))
|
||
else "config"
|
||
if _get("apiToken")
|
||
else "none"
|
||
)
|
||
|
||
return {
|
||
"account_id": account_id,
|
||
"site_url": site_url.rstrip("/") if site_url else "",
|
||
"email": email,
|
||
"api_token": api_token,
|
||
"auth_method": _get("authMethod", "api_token"),
|
||
"project_key": _get("projectKey", ""),
|
||
"jql_filter": _get("jqlFilter", ""),
|
||
"callback_base_url": _get("callbackBaseUrl", ""),
|
||
"webhook_secret": _get("webhookSecret", ""),
|
||
"webhook_id": _get("webhookId", ""),
|
||
"bot_account_id": _get("botAccountId", ""),
|
||
"bot_display_name": _get("botDisplayName", ""),
|
||
"comment_visibility": _get("commentVisibility", "internal"),
|
||
"streaming_mode": _get("streamingMode", "block"),
|
||
"allowed_projects": _get("allowedProjects", []),
|
||
"enabled": account_raw.get("enabled", True),
|
||
"name": account_raw.get("name", account_id),
|
||
"token_source": token_source,
|
||
}
|
||
|
||
def config_schema(self) -> dict:
|
||
return {
|
||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||
"type": "object",
|
||
"title": "Jira 渠道配置",
|
||
"properties": {
|
||
"siteUrl": {
|
||
"type": "string",
|
||
"title": "Jira Site URL",
|
||
"description": "Jira Cloud URL,如 https://your-domain.atlassian.net",
|
||
"format": "uri",
|
||
},
|
||
"email": {
|
||
"type": "string",
|
||
"title": "Email",
|
||
"description": "Jira 账号邮箱(API Token 方式必填)",
|
||
"format": "email",
|
||
},
|
||
"apiToken": {
|
||
"type": "string",
|
||
"title": "API Token",
|
||
"description": "在 https://id.atlassian.com/manage/api-tokens 生成",
|
||
"x-ui-password": True,
|
||
},
|
||
"projectKey": {
|
||
"type": "string",
|
||
"title": "Project Key",
|
||
"description": "Bot 作用于哪个 Jira Project,如 SUPPORT",
|
||
},
|
||
"allowedProjects": {
|
||
"type": "array",
|
||
"title": "允许响应的项目",
|
||
"description": "Bot 可以响应的 Project Key 列表,留空则响应所有",
|
||
"items": {"type": "string"},
|
||
},
|
||
"jqlFilter": {
|
||
"type": "string",
|
||
"title": "JQL 过滤",
|
||
"description": "Webhook 事件的 JQL 过滤条件,默认 project={projectKey}",
|
||
},
|
||
"callbackBaseUrl": {
|
||
"type": "string",
|
||
"title": "Webhook 回调地址",
|
||
"description": "ForcePilot 的公网可达 URL,如 https://forcepilot.example.com",
|
||
"format": "uri",
|
||
},
|
||
"commentVisibility": {
|
||
"type": "string",
|
||
"title": "默认评论可见性",
|
||
"enum": ["internal", "public"],
|
||
"default": "internal",
|
||
"description": "internal=仅内部可见,public=客户可见(JSM)",
|
||
},
|
||
"streamingMode": {
|
||
"type": "string",
|
||
"title": "流式输出模式",
|
||
"enum": ["off", "block"],
|
||
"default": "block",
|
||
"description": "block=占位评论+定时全量更新",
|
||
},
|
||
"webhookSecret": {
|
||
"type": "string",
|
||
"title": "Webhook Secret",
|
||
"description": "Jira Webhook Secret Token,用于 HMAC-SHA256 签名验证",
|
||
"x-ui-password": True,
|
||
},
|
||
},
|
||
"required": ["siteUrl", "email", "apiToken", "callbackBaseUrl"],
|
||
}
|