新增了完整的Confluence集成插件,包含以下核心功能: 1. 基础认证与配置管理,支持API Token和OAuth2两种认证方式 2. 评论去重、权限控制与提及解析 3. 知识库检索与页面内容处理 4. 评论收发、编辑删除与流式回复支持 5. 附件与页面标签管理 6. 内容属性存储与AI元数据管理 7. Webhook事件接收与处理 8. 完整的插件配置与状态检查
147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
import json
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.confluence.types import InboundConfluenceComment, PageContext
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SUPPORTED_EVENTS = {
|
|
"page_created", "page_updated", "page_removed",
|
|
"blog_created", "blog_updated",
|
|
"comment_created", "comment_updated", "comment_removed",
|
|
"attachment_created", "attachment_updated", "attachment_removed",
|
|
"label_added", "label_removed",
|
|
"page_moved", "page_restored", "page_trashed",
|
|
"content_created", "content_updated", "content_trashed", "content_restored",
|
|
"space_created", "space_updated", "space_removed",
|
|
}
|
|
|
|
|
|
class ConfluenceWebhookReceiver:
|
|
def __init__(self, on_page_changed=None, on_comment=None):
|
|
self._on_page_changed = on_page_changed
|
|
self._on_comment = on_comment
|
|
|
|
def parse_webhook(self, body: bytes, headers: dict) -> dict | None:
|
|
try:
|
|
payload = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
logger.warning("Invalid webhook JSON payload")
|
|
return None
|
|
|
|
event_type = payload.get("eventType", "")
|
|
if event_type not in SUPPORTED_EVENTS:
|
|
logger.debug("Ignored webhook event type: %s", event_type)
|
|
return None
|
|
|
|
if event_type.startswith("comment_"):
|
|
return self._parse_comment_event(payload)
|
|
|
|
if event_type.startswith(("attachment_", "label_")):
|
|
return self._parse_resource_event(payload, event_type)
|
|
|
|
page_id = payload.get("page", {}).get("id")
|
|
page_title = payload.get("page", {}).get("title", "")
|
|
space_key = payload.get("space", {}).get("key", "")
|
|
|
|
if not page_id:
|
|
logger.warning("Webhook payload missing page.id")
|
|
return None
|
|
|
|
return {
|
|
"event_type": event_type,
|
|
"page_id": page_id,
|
|
"page_title": page_title,
|
|
"space_key": space_key,
|
|
"user_account_id": payload.get("userAccountId", ""),
|
|
"timestamp": payload.get("timestamp", ""),
|
|
}
|
|
|
|
def _parse_comment_event(self, payload: dict) -> dict | None:
|
|
comment = payload.get("comment", {})
|
|
comment_id = comment.get("id")
|
|
if not comment_id:
|
|
return None
|
|
|
|
container = payload.get("container") or payload.get("page") or {}
|
|
author = comment.get("createdBy", {}) or comment.get("lastUpdatedBy", {})
|
|
|
|
page_ctx = PageContext(
|
|
page_id=container.get("id", ""),
|
|
page_title=container.get("title", ""),
|
|
space_key=payload.get("space", {}).get("key", ""),
|
|
)
|
|
|
|
inbound = InboundConfluenceComment(
|
|
comment_id=comment_id,
|
|
author_id=author.get("accountId", ""),
|
|
author_name=author.get("displayName", ""),
|
|
body_text=self._extract_comment_body_text(comment),
|
|
container_id=container.get("id", ""),
|
|
parent_comment_id=comment.get("parentCommentId"),
|
|
page_context=page_ctx,
|
|
)
|
|
|
|
return {
|
|
"event_type": payload.get("eventType", ""),
|
|
"comment": inbound,
|
|
"timestamp": payload.get("timestamp", ""),
|
|
}
|
|
|
|
@staticmethod
|
|
def _extract_comment_body_text(comment: dict) -> str:
|
|
body = comment.get("body", {})
|
|
view_value = body.get("view", {}).get("value", "")
|
|
if view_value:
|
|
import re
|
|
clean = re.sub(r"<[^>]+>", "", view_value)
|
|
clean = re.sub(r"&[a-z]+;", " ", clean)
|
|
return re.sub(r"\s+", " ", clean).strip()
|
|
return ""
|
|
|
|
def _parse_resource_event(self, payload: dict, event_type: str) -> dict | None:
|
|
if event_type.startswith("attachment_"):
|
|
resource = payload.get("attachment", {})
|
|
return {
|
|
"event_type": event_type,
|
|
"attachment_id": resource.get("id"),
|
|
"filename": resource.get("title"),
|
|
"page_id": payload.get("page", {}).get("id"),
|
|
"timestamp": payload.get("timestamp", ""),
|
|
}
|
|
|
|
if event_type.startswith("label_"):
|
|
return {
|
|
"event_type": event_type,
|
|
"label_name": payload.get("label", {}).get("name"),
|
|
"page_id": payload.get("page", {}).get("id"),
|
|
"timestamp": payload.get("timestamp", ""),
|
|
}
|
|
|
|
return None
|
|
|
|
async def handle_page_event(self, event: dict, account_id: str = "default"):
|
|
if self._on_page_changed is None:
|
|
return
|
|
|
|
page_ctx = PageContext(
|
|
page_id=event["page_id"],
|
|
page_title=event["page_title"],
|
|
space_key=event["space_key"],
|
|
)
|
|
|
|
await self._on_page_changed(
|
|
event_type=event["event_type"],
|
|
page_context=page_ctx,
|
|
user_account_id=event["user_account_id"],
|
|
account_id=account_id,
|
|
)
|
|
|
|
async def handle_comment_event(self, event: dict, account_id: str = "default"):
|
|
if self._on_comment is None:
|
|
return
|
|
comment = event.get("comment")
|
|
if comment is None:
|
|
return
|
|
await self._on_comment(comment, account_id=account_id)
|