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

76 lines
2.4 KiB
Python

"""Linear sink: create an issue from Markdown via the GraphQL API.
Linear's API is GraphQL at ``https://api.linear.app/graphql`` and authenticates
with a raw API key in the ``Authorization`` header (no ``Bearer`` prefix). The
issue description is Markdown; Linear renders inline ``data:`` image URIs, so
local images are read and embedded as base64 data URIs before delivery.
"""
from __future__ import annotations
import base64
from pathlib import Path
from . import _http, _md
from .base import ParsedDoc, Sink, SinkError, SinkResult, register
API = "https://api.linear.app/graphql"
_MUTATION = (
"mutation IssueCreate($input: IssueCreateInput!)"
"{issueCreate(input:$input){success issue{id url identifier}}}"
)
_MIME = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
}
def _data_uri(path: Path) -> str:
mime = _MIME.get(path.suffix.lower(), "image/png")
b64 = base64.b64encode(path.read_bytes()).decode("ascii")
return f"data:{mime};base64,{b64}"
@register
class LinearSink(Sink):
name = "linear"
requires = ("LINEAR_API_KEY", "LINEAR_TEAM_ID")
label = "Linear issue (GraphQL API)"
def deliver(self, doc: ParsedDoc) -> SinkResult:
key = self.env("LINEAR_API_KEY")
team = self.env("LINEAR_TEAM_ID")
headers = {"Authorization": key, "Content-Type": "application/json"}
base_dir = Path(doc.markdown_path).parent if doc.markdown_path else None
images = _md.find_local_images(doc.markdown, base_dir)
mapping = {ref: _data_uri(path) for _alt, ref, path in images}
body = _md.rewrite_images(doc.markdown, mapping)
status, parsed = _http.request_json("POST", API, headers=headers, payload={
"query": _MUTATION,
"variables": {"input": {
"teamId": team,
"title": doc.title,
"description": body,
}},
})
if parsed.get("errors"):
raise SinkError(str(parsed["errors"]))
result = ((parsed.get("data") or {}).get("issueCreate")) or {}
if not result.get("success"):
raise SinkError(f"Linear did not create the issue (HTTP {status})")
issue = result.get("issue") or {}
return SinkResult(
sink=self.name, ok=True,
url=issue.get("url"),
detail=f"{len(mapping)} image(s) inlined",
)