qwen_agent/skills/developing/mineru/scripts/sinks/slack.py
2026-06-05 14:35:17 +08:00

96 lines
3.3 KiB
Python

"""Slack sink: upload the parsed Markdown as a file via the external-upload flow.
Slack deprecated ``files.upload`` (retired) in favour of a three-step external
upload. Delivery follows that official path:
1. ``files.getUploadURLExternal`` — reserve an upload URL + file id for the
given filename and byte length.
2. ``POST`` the raw bytes to the returned upload URL.
3. ``files.completeUploadExternal`` — finalize the upload, attach it to the
target channel, and post an initial comment.
Images are *not* embedded: Markdown is uploaded as a single ``.md`` file.
"""
from __future__ import annotations
import urllib.parse
from . import _http, _md
from .base import ParsedDoc, Sink, SinkError, SinkResult, register
@register
class SlackSink(Sink):
name = "slack"
requires = ("SLACK_BOT_TOKEN", "SLACK_CHANNEL")
label = "Slack channel (file upload)"
def deliver(self, doc: ParsedDoc) -> SinkResult:
token = self.env("SLACK_BOT_TOKEN")
channel = self.env("SLACK_CHANNEL")
auth = {"Authorization": f"Bearer {token}"}
content = doc.markdown.encode("utf-8")
filename = _md.slugify(doc.title) + ".md"
# Step 1: reserve an external upload URL + file id. This endpoint wants
# form-encoded data, so use http_request and parse the JSON response.
form = urllib.parse.urlencode({
"filename": filename,
"length": len(content),
}).encode("utf-8")
status, raw = _http.http_request(
"POST",
"https://slack.com/api/files.getUploadURLExternal",
headers={**auth, "Content-Type": "application/x-www-form-urlencoded"},
data=form,
)
parsed = _parse_json(raw)
if not parsed.get("ok"):
raise SinkError(parsed.get("error") or f"Slack getUploadURLExternal failed (HTTP {status})")
upload_url = parsed.get("upload_url")
file_id = parsed.get("file_id")
if not upload_url or not file_id:
raise SinkError("Slack did not return an upload URL / file id")
# Step 2: upload the raw bytes to the reserved URL.
up_status, _up_body = _http.http_request(
"POST", upload_url,
headers={"Content-Type": "application/octet-stream"},
data=content,
)
if up_status != 200:
raise SinkError(f"Slack file upload failed (HTTP {up_status})")
# Step 3: finalize the upload into the channel.
status, parsed = _http.request_json(
"POST",
"https://slack.com/api/files.completeUploadExternal",
headers=auth,
payload={
"files": [{"id": file_id, "title": doc.title}],
"channel_id": channel,
"initial_comment": f"Parsed: {doc.title}",
},
)
if not parsed.get("ok"):
raise SinkError(parsed.get("error") or f"Slack completeUploadExternal failed (HTTP {status})")
files = parsed.get("files") or [{}]
url = files[0].get("permalink")
return SinkResult(
sink=self.name, ok=True, url=url,
detail="uploaded .md file (images not embedded)",
)
def _parse_json(raw):
import json
if not raw:
return {}
try:
return json.loads(raw.decode("utf-8"))
except (ValueError, UnicodeDecodeError):
return {}