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
|
FROM python:3.13-slim AS base
|
||||||
|
|
||||||
ENV VIRTUAL_ENV=/app/.venv \
|
ENV VIRTUAL_ENV=/app/.venv \
|
||||||
@ -38,6 +51,7 @@ RUN uv sync --no-install-project
|
|||||||
FROM base AS runtime
|
FROM base AS runtime
|
||||||
|
|
||||||
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||||
|
COPY --from=artifacts /artifacts/ /usr/local/bin/
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ FONTS_PATH = ASSETS_PATH / "fonts"
|
|||||||
|
|
||||||
SRC_PATH = Path(__file__).resolve().parent.parent
|
SRC_PATH = Path(__file__).resolve().parent.parent
|
||||||
DATA_PATH = SRC_PATH.parent / "data"
|
DATA_PATH = SRC_PATH.parent / "data"
|
||||||
|
TMP_PATH = DATA_PATH / "tmp"
|
||||||
LOG_PATH = DATA_PATH / "logs"
|
LOG_PATH = DATA_PATH / "logs"
|
||||||
CONFIG_PATH = DATA_PATH / "config"
|
CONFIG_PATH = DATA_PATH / "config"
|
||||||
|
|
||||||
@ -21,4 +22,5 @@ if not LOG_PATH.exists():
|
|||||||
LOG_PATH.mkdir()
|
LOG_PATH.mkdir()
|
||||||
|
|
||||||
CONFIG_PATH.mkdir(exist_ok=True)
|
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