This commit is contained in:
14
Dockerfile
14
Dockerfile
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
4
konabot/docs/user/typst.txt
Normal file
4
konabot/docs/user/typst.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# Typst 渲染
|
||||
|
||||
只需使用 `typst ...` 就可以渲染 Typst 了
|
||||
|
||||
97
konabot/plugins/typst/__init__.py
Normal file
97
konabot/plugins/typst/__init__.py
Normal 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)
|
||||
|
||||
5
konabot/plugins/typst/template.typ
Normal file
5
konabot/plugins/typst/template.typ
Normal 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"))
|
||||
|
||||
Reference in New Issue
Block a user