添加 Typst 支持
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-01-18 12:25:17 +08:00
parent c44e29a907
commit 1eb7e62cfe
5 changed files with 122 additions and 0 deletions

View File

@ -1,3 +1,16 @@
FROM alpine:latest AS artifacts
RUN apk add --no-cache curl xz
WORKDIR /tmp
RUN mkdir -p /artifacts
RUN curl -L -o typst.tar.xz "https://github.com/typst/typst/releases/download/v0.14.2/typst-x86_64-unknown-linux-musl.tar.xz" \
&& tar -xJf typst.tar.xz \
&& mv typst-x86_64-unknown-linux-musl/typst /artifacts
RUN chmod -R +x /artifacts/
FROM python:3.13-slim AS base
ENV VIRTUAL_ENV=/app/.venv \
@ -38,6 +51,7 @@ RUN uv sync --no-install-project
FROM base AS runtime
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --from=artifacts /artifacts/ /usr/local/bin/
WORKDIR /app

View File

@ -5,6 +5,7 @@ FONTS_PATH = ASSETS_PATH / "fonts"
SRC_PATH = Path(__file__).resolve().parent.parent
DATA_PATH = SRC_PATH.parent / "data"
TMP_PATH = DATA_PATH / "tmp"
LOG_PATH = DATA_PATH / "logs"
CONFIG_PATH = DATA_PATH / "config"
@ -21,4 +22,5 @@ if not LOG_PATH.exists():
LOG_PATH.mkdir()
CONFIG_PATH.mkdir(exist_ok=True)
TMP_PATH.mkdir(exist_ok=True)

View File

@ -0,0 +1,4 @@
# Typst 渲染
只需使用 `typst ...` 就可以渲染 Typst 了

View File

@ -0,0 +1,97 @@
import asyncio
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import cast
from loguru import logger
from nonebot import on_command
from nonebot.adapters import Event, Bot
from nonebot_plugin_alconna import UniMessage, UniMsg
from nonebot.adapters.onebot.v11.event import MessageEvent as OB11MessageEvent
from nonebot.adapters.onebot.v11.bot import Bot as OB11Bot
from nonebot.adapters.onebot.v11.message import Message as OB11Message
from konabot.common.longtask import DepLongTaskTarget
from konabot.common.path import TMP_PATH
TEMPLATE_PATH = Path(__file__).parent / "template.typ"
TEMPLATE = TEMPLATE_PATH.read_text()
def render_sync(code: str) -> bytes:
with TemporaryDirectory(dir=TMP_PATH) as tmpdirname:
temp_dir = Path(tmpdirname).resolve()
temp_typ = temp_dir / "page.typ"
temp_typ.write_text(TEMPLATE + "\n\n" + code)
cmd = ["typst", "compile", temp_typ.name, "--format", "png", "--root", temp_dir]
result = subprocess.run(
cmd, capture_output=True, text=True, cwd=temp_dir.resolve(), timeout=50
)
logger.info(
f"渲染了 Typst "
f"STDOUT={result.stdout} "
f"STDERR={result.stderr} "
f"RETURNCODE={result.returncode}"
)
if result.returncode != 0:
raise subprocess.CalledProcessError(
result.returncode, cmd, result.stdout, result.stderr
)
result_png = temp_dir / "page.png"
if not result_png.exists():
raise FileNotFoundError("Typst 没有输出图片文件")
return result_png.read_bytes()
async def render(code: str) -> bytes:
task = asyncio.to_thread(lambda: render_sync(code))
return await task
cmd = on_command("typst")
@cmd.handle()
async def _(evt: Event, bot: Bot, msg: UniMsg, target: DepLongTaskTarget):
typst_code = ""
if isinstance(evt, OB11MessageEvent):
if evt.reply is not None:
typst_code = evt.reply.message.extract_plain_text()
else:
for seg in evt.get_message():
if seg.type == 'reply':
msgid = seg.get('id')
if msgid is not None:
msg2data = await cast(OB11Bot, bot).get_msg(message_id=msgid)
typst_code = OB11Message(msg2data.get("message")).extract_plain_text()
typst_code += msg.extract_plain_text().removeprefix("typst").strip()
if len(typst_code) == 0:
return
try:
res = await render(typst_code)
except FileNotFoundError as e:
await target.send_message("渲染出错:内部错误")
raise e from e
except subprocess.CalledProcessError as e:
await target.send_message("渲染出错,以下是输出消息:\n\n" + e.stderr)
return
except TimeoutError:
await target.send_message("渲染出错:渲染超时")
return
except PermissionError as e:
await target.send_message("渲染出错:内部错误")
raise e from e
await target.send_message(UniMessage.image(raw=res), at=False)

View File

@ -0,0 +1,5 @@
#import "@preview/cetz:0.4.2"
#set page(width: auto, height: auto, margin: (x: 10pt, y: 10pt))
#set text(font: ("Noto Sans CJK SC"))