from __future__ import annotations import logging from datetime import datetime from yuxi.channel.extensions.jira.parse import adf_to_plain_text logger = logging.getLogger(__name__) class JiraMonitor: def __init__(self, config: dict): self._config = config def convert_webhook_to_unified(self, payload: dict, account_id: str) -> dict | None: webhook_event = payload.get("webhookEvent", "") comment = payload.get("comment", {}) issue = payload.get("issue", {}) changelog = payload.get("changelog", {}) issue_key = issue.get("key", "") # 处理无 issue 对象的事件(如 issue_deleted 可能以不同结构传递) if not issue_key: issue_key = self._extract_issue_key(payload) if not issue_key: return None # 针对新增事件类型的特殊处理 if webhook_event == "jira:issue_deleted": return self._convert_issue_deleted(payload, account_id, issue_key) if webhook_event in ("attachment_created", "attachment_deleted"): return self._convert_attachment_event(payload, account_id, issue_key, webhook_event) comment_id = str(comment.get("id", "")) if comment else "" comment_body_adf = comment.get("body", {}) if comment else {} comment_body_plain = adf_to_plain_text(comment_body_adf) author = comment.get("author", {}) if comment else payload.get("user", {}) author_account_id = author.get("accountId", "") author_display_name = author.get("displayName", "") fields = issue.get("fields", {}) issue_type = fields.get("issuetype", {}) priority = fields.get("priority", {}) status = fields.get("status", {}) project = fields.get("project", {}) properties = comment.get("properties", []) if comment else [] timestamp = None created = comment.get("created", "") or issue.get("fields", {}).get("created", "") if created: try: timestamp = datetime.fromisoformat(created.replace("Z", "+00:00")) except ValueError: pass return { "msg_id": f"jira:{issue_key}:{comment_id}" if comment_id else f"jira:{issue_key}:{webhook_event}", "channel_type": "jira", "account_id": account_id, "content": comment_body_plain or fields.get("summary", ""), "message_type": "TEXT", "media_urls": [], "sender": { "id": f"jira:{author_account_id}", "display_name": author_display_name or f"Jira:{author_account_id}", "kind": "GROUP", }, "group": { "id": issue_key, "kind": "thread", "name": fields.get("summary", ""), }, "timestamp": timestamp, "reply_to_id": None, "thread_id": issue_key, "thread_parent_id": issue_key, "raw_payload": payload, "metadata": { "webhook_event": webhook_event, "issue_key": issue_key, "issue_id": issue.get("id", ""), "issue_type": issue_type.get("name", ""), "issue_status": status.get("name", ""), "issue_priority": priority.get("name", ""), "project_key": project.get("key", ""), "project_name": project.get("name", ""), "comment_id": comment_id, "comment_visibility": "internal" if _is_internal_comment(properties) else "public", "is_ai_comment": _has_property(properties, "ai.agent"), "author_account_id": author_account_id, "changelog_items": changelog.get("items", []), }, } def convert_to_response_payload(self, unified_msg: dict) -> dict: return { "content": unified_msg.get("content", ""), "issue_key": unified_msg.get("metadata", {}).get("issue_key", ""), "comment_id": unified_msg.get("metadata", {}).get("comment_id", ""), "project_key": unified_msg.get("metadata", {}).get("project_key", ""), } def _extract_issue_key(self, payload: dict) -> str: """从无 issue 对象的事件 payload 中提取 issue key.""" # issue_deleted 事件中 issue 信息可能在不同位置 issue = payload.get("issue", {}) if issue.get("key"): return issue["key"] # 某些事件中可能在 payload 根级别 return payload.get("issue_key", "") def _convert_issue_deleted(self, payload: dict, account_id: str, issue_key: str) -> dict: user = payload.get("user", {}) author_account_id = user.get("accountId", "") author_display_name = user.get("displayName", "") return { "msg_id": f"jira:{issue_key}:issue_deleted", "channel_type": "jira", "account_id": account_id, "content": f"Issue {issue_key} 已被删除", "message_type": "EVENT", "media_urls": [], "sender": { "id": f"jira:{author_account_id}", "display_name": author_display_name or f"Jira:{author_account_id}", "kind": "GROUP", }, "group": { "id": issue_key, "kind": "thread", "name": f"Issue {issue_key}", }, "reply_to_id": None, "thread_id": issue_key, "thread_parent_id": issue_key, "raw_payload": payload, "metadata": { "webhook_event": "jira:issue_deleted", "issue_key": issue_key, }, } def _convert_attachment_event(self, payload: dict, account_id: str, issue_key: str, webhook_event: str) -> dict: attachment = payload.get("attachment", {}) filename = attachment.get("filename", "unknown") user = payload.get("user", {}) author_account_id = user.get("accountId", "") author_display_name = user.get("displayName", "") return { "msg_id": f"jira:{issue_key}:{webhook_event}:{attachment.get('id', '')}", "channel_type": "jira", "account_id": account_id, "content": f"{'新增' if webhook_event == 'attachment_created' else '删除'}附件: {filename}", "message_type": "EVENT", "media_urls": [attachment.get("content", "")] if attachment.get("content") else [], "sender": { "id": f"jira:{author_account_id}", "display_name": author_display_name or f"Jira:{author_account_id}", "kind": "GROUP", }, "group": { "id": issue_key, "kind": "thread", "name": f"Issue {issue_key}", }, "reply_to_id": None, "thread_id": issue_key, "thread_parent_id": issue_key, "raw_payload": payload, "metadata": { "webhook_event": webhook_event, "issue_key": issue_key, "attachment_id": attachment.get("id"), "attachment_filename": filename, "attachment_size": attachment.get("size"), }, } def _is_internal_comment(properties: list[dict]) -> bool: for prop in properties: if prop.get("key") == "sd.public.comment": value = prop.get("value", {}) if isinstance(value, dict) and value.get("internal") is True: return True return False def _has_property(properties: list[dict], key: str) -> bool: for prop in properties: if prop.get("key") == key: return True return False