ForcePilot/backend/package/yuxi/channel/extensions/confluence/webhook.py
Kris f943c31ce4 feat(confluence): 新增Confluence渠道插件完整实现
新增了完整的Confluence集成插件,包含以下核心功能:
1.  基础认证与配置管理,支持API Token和OAuth2两种认证方式
2.  评论去重、权限控制与提及解析
3.  知识库检索与页面内容处理
4.  评论收发、编辑删除与流式回复支持
5.  附件与页面标签管理
6.  内容属性存储与AI元数据管理
7.  Webhook事件接收与处理
8.  完整的插件配置与状态检查
2026-05-21 10:43:42 +08:00

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)