feat: enable shiki highlighting for t2i templates and add a template (#7501)

* fix: enable shiki highlighting for t2i templates

* fix: t2i templates cr

* feat: add new t2i template astrbot_vitepress.html
This commit is contained in:
若月千鸮
2026-04-14 16:46:32 +08:00
committed by GitHub
parent 207eb34ba2
commit 625eab223f
8 changed files with 1300 additions and 257 deletions

View File

@@ -1,6 +1,9 @@
import asyncio
import base64
import logging
import random
from functools import lru_cache
from pathlib import Path
import aiohttp
@@ -16,6 +19,31 @@ ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img"
logger = logging.getLogger("astrbot")
@lru_cache(maxsize=1)
def get_shiki_runtime() -> str:
runtime_path = (
Path(__file__).resolve().parent / "template" / "shiki_runtime.iife.js"
)
if not runtime_path.exists():
logger.error(
"T2I Shiki runtime not found at %s. Run `cd dashboard && pnpm run build:t2i-shiki-runtime` to regenerate it. Continuing without code highlighting.",
runtime_path,
)
return ""
try:
runtime = runtime_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as err:
logger.warning(
"Failed to load T2I Shiki runtime from %s: %s. Continuing without code highlighting.",
runtime_path,
err,
)
return ""
return runtime.replace("</script", "<\\/script")
class NetworkRenderStrategy(RenderStrategy):
def __init__(self, base_url: str | None = None) -> None:
super().__init__()
@@ -77,6 +105,7 @@ class NetworkRenderStrategy(RenderStrategy):
if options:
default_options |= options
tmpl_data = {"shiki_runtime": get_shiki_runtime()} | tmpl_data
post_data = {
"tmpl": tmpl_str,
"json": return_url,
@@ -129,9 +158,9 @@ class NetworkRenderStrategy(RenderStrategy):
if not template_name:
template_name = "base"
tmpl_str = await self.get_template(name=template_name)
text = text.replace("`", "\\`")
text_base64 = base64.b64encode(text.encode("utf-8")).decode("ascii")
return await self.render_custom_template(
tmpl_str,
{"text": text, "version": f"v{VERSION}"},
{"text_base64": text_base64, "version": f"v{VERSION}"},
return_url,
)

View File

@@ -2,20 +2,15 @@
<html>
<head>
<meta charset="utf-8"/>
<title>Astrbot PowerShell {{ version }} </title>
<title>Astrbot PowerShell {{ version }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/common.min.js"></script>
<script>hljs.highlightAll();</script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"
onload="renderMathInElement(document.getElementById('content'),{delimiters: [{left: '$$', right: '$$', display: true},{left: '$', right: '$', display: false}]});"></script>
<style>
:root {
--bg-color: #010409;
--text-color: #e6edf3;
--title-bar-color: #161b22;
--title-text-color: #e6edf3;
--font-family: 'Consolas', 'Microsoft YaHei Mono', 'Dengxian Mono', 'Courier New', monospace;
--font-family: "Consolas", "Microsoft YaHei Mono", "Dengxian Mono", "Courier New", monospace;
--glow-color: rgba(200, 220, 255, 0.7);
}
@@ -36,7 +31,6 @@
padding: 0;
line-height: 1.6;
font-size: 18px;
/* The CRT glow effect from the image */
text-shadow: 0 0 15px var(--glow-color), 0 0 7px rgba(255, 255, 255, 1);
position: relative;
overflow: hidden;
@@ -63,9 +57,9 @@
color: var(--title-text-color);
font-size: 16px;
border-bottom: 1px solid #30363d;
text-shadow: none; /* No glow for title bar */
text-shadow: none;
}
.header .title {
font-weight: bold;
font-size: 28px;
@@ -78,13 +72,10 @@
main {
padding: 1rem 1.5rem;
position: relative;
z-index: 1;
}
#content {
/* min-width and max-width removed as per request */
}
/* --- Markdown Styles adjusted for terminal look --- */
h1, h2, h3, h4, h5, h6 {
line-height: 1.4;
margin-top: 20px;
@@ -144,7 +135,16 @@
font-size: 100%;
background-color: transparent;
border-radius: 0;
text-shadow: none; /* Disable glow inside code blocks for clarity */
text-shadow: none;
}
pre.shiki {
padding: 1rem;
}
pre.shiki > code,
pre.shiki span {
text-shadow: none;
}
a {
@@ -165,9 +165,8 @@
</style>
</head>
<body>
<div class="header">
<span class="title">> Astrbot PowerShell</span>
<span class="title">&gt; Astrbot PowerShell</span>
<span class="version">{{ version }}</span>
</div>
@@ -175,10 +174,45 @@
<div id="content"></div>
</main>
<script>{{ shiki_runtime | safe }}</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"></script>
<script>
document.getElementById('content').innerHTML = marked.parse(`{{ text | safe }}`);
</script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
contentElement.innerHTML = marked.parse(source);
if (window.AstrBotT2IShiki) {
window.AstrBotT2IShiki.highlightAllCodeBlocks(contentElement, "github-dark");
}
if (window.renderMathInElement) {
window.renderMathInElement(contentElement, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false }
]
});
}
function decodeBase64Utf8(base64Text) {
const binary = window.atob(base64Text || "");
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (window.TextDecoder) {
return new TextDecoder().decode(bytes);
}
let fallback = "";
bytes.forEach((byte) => {
fallback += String.fromCharCode(byte);
});
return decodeURIComponent(escape(fallback));
}
})();
</script>
</body>
</html>
</html>

View File

@@ -0,0 +1,552 @@
<!doctype html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="utf-8"/>
<title>AstrBot Docs {{ version }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
<style>
:root {
--vp-c-bg: #1b1b1f;
--vp-c-bg-soft: #202127;
--vp-c-bg-alt: #161618;
--vp-c-bg-elv: #202127;
--vp-c-border: #3c3f44;
--vp-c-divider: #2e2e32;
--vp-c-gutter: #000000;
--vp-c-text-1: #dfdfd6;
--vp-c-text-2: #98989f;
--vp-c-text-3: #6a6a71;
--vp-c-brand-1: #a8b1ff;
--vp-c-brand-2: #5c73e7;
--vp-c-brand-3: #3e63dd;
--vp-c-brand-soft: rgba(100, 108, 255, 0.16);
--vp-c-default-soft: rgba(101, 117, 133, 0.16);
--vp-code-bg: var(--vp-c-default-soft);
--vp-code-block-bg: var(--vp-c-bg-alt);
--vp-code-line-height: 1.7;
--vp-code-font-size: 0.875em;
--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.07), 0 1px 4px rgba(0, 0, 0, 0.07);
--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
--vp-layout-max-width: 1440px;
--vp-nav-height: 64px;
--vp-radius: 12px;
--vp-font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--vp-font-family-cjk: "Inter4CJK", -apple-system, BlinkMacSystemFont, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--vp-code-font-family: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;
}
* {
box-sizing: border-box;
}
html {
background: var(--vp-c-bg);
}
body {
margin: 0;
min-height: 100vh;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family);
background: linear-gradient(180deg, rgba(27, 27, 31, 0.96), rgba(27, 27, 31, 1));
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body:lang(zh),
body:lang(ja) {
font-family: var(--vp-font-family-cjk);
}
a {
color: var(--vp-c-brand-1);
text-decoration: underline;
text-underline-offset: 2px;
transition: color 0.25s, opacity 0.25s;
}
a:hover {
color: var(--vp-c-brand-2);
}
#app {
max-width: var(--vp-layout-max-width);
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.vp-nav {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--vp-nav-height);
padding: 0 32px;
backdrop-filter: blur(18px);
background: rgba(27, 27, 31, 0.9);
border-bottom: 1px solid var(--vp-c-gutter);
}
.vp-brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
font-size: 20px;
letter-spacing: -0.01em;
}
.vp-brand-logo {
width: 28px;
height: 28px;
object-fit: contain;
filter: drop-shadow(0 6px 16px rgba(62, 99, 221, 0.24));
}
.vp-brand-name {
color: #ffffff;
}
.vp-nav-actions {
display: flex;
align-items: center;
gap: 12px;
color: var(--vp-c-text-2);
font-size: 14px;
}
.vp-search {
display: none;
}
.vp-search kbd {
display: none;
}
.vp-shell {
padding: 42px 32px 56px;
flex: 1 0 auto;
}
.vp-main {
min-width: 0;
}
.vp-content-frame {
max-width: 980px;
margin: 0 auto;
}
.vp-hero {
display: block;
margin-bottom: 36px;
}
.vp-hero.is-hidden {
display: none;
}
.vp-hero-copy h1 {
margin: 0;
font-size: 48px;
line-height: 1.05;
letter-spacing: -0.04em;
color: #ffffff;
}
.vp-hero-copy p {
max-width: 720px;
margin: 16px 0 0;
font-size: 18px;
line-height: 1.78;
color: var(--vp-c-text-2);
}
.vp-doc {
color: var(--vp-c-text-1);
font-size: 16px;
line-height: 28px;
}
.vp-doc > *:first-child {
margin-top: 0;
}
.vp-doc h1,
.vp-doc h2,
.vp-doc h3,
.vp-doc h4 {
position: relative;
scroll-margin-top: 100px;
color: #ffffff;
font-weight: 600;
letter-spacing: -0.02em;
}
.vp-doc h1 {
margin: 0 0 20px;
font-size: 32px;
line-height: 40px;
}
.vp-doc h2 {
margin: 48px 0 16px;
padding-top: 24px;
font-size: 24px;
line-height: 32px;
border-top: 1px solid var(--vp-c-divider);
}
.vp-doc h3 {
margin: 32px 0 0;
font-size: 20px;
line-height: 28px;
}
.vp-doc p,
.vp-doc ul,
.vp-doc ol,
.vp-doc table,
.vp-doc blockquote,
.vp-doc [class*="language-"],
.vp-doc .math {
margin: 16px 0;
}
.vp-doc strong {
color: #ffffff;
}
.vp-doc code {
font-family: var(--vp-code-font-family);
padding: 3px 6px;
border-radius: 4px;
color: var(--vp-c-brand-1);
background: var(--vp-code-bg);
}
.vp-doc pre code {
padding: 0;
background: transparent;
color: inherit;
}
.vp-doc [class*="language-"],
.vp-block {
position: relative;
overflow: hidden;
background-color: var(--vp-code-block-bg);
transition: background-color 0.5s;
border-radius: 8px;
}
.vp-doc [class*="language-"] > span.lang,
.vp-block > span.lang {
position: absolute;
top: 2px;
right: 8px;
z-index: 2;
font-size: 12px;
font-weight: 500;
user-select: none;
color: var(--vp-c-text-2);
background: transparent;
transition: color 0.4s, opacity 0.4s;
}
.vp-doc [class*="language-"] pre.shiki,
.vp-doc [class*="language-"] pre,
.vp-block pre.shiki,
.vp-block pre {
margin: 0;
padding: 20px 0;
border: 0;
border-radius: 8px;
overflow-x: auto;
background: var(--vp-code-block-bg) !important;
line-height: var(--vp-code-line-height);
font-size: var(--vp-code-font-size);
}
.vp-doc [class*="language-"] code,
.vp-block code {
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
}
.vp-doc [class*="language-"] pre.shiki code,
.vp-doc [class*="language-"] pre code,
.vp-block pre.shiki code,
.vp-block pre code {
font-family: var(--vp-code-font-family);
display: block;
width: fit-content;
min-width: 100%;
padding: 0 24px;
font-size: 14px;
color: inherit;
}
.vp-doc [class*="language-"] pre.shiki,
.vp-block pre.shiki {
color: var(--shiki-dark, #e1e4e8) !important;
background-color: var(--shiki-dark-bg, var(--vp-code-block-bg)) !important;
}
.vp-doc [class*="language-"] pre.shiki > code,
.vp-doc [class*="language-"] pre.shiki span,
.vp-block pre.shiki > code,
.vp-block pre.shiki span {
text-shadow: none;
}
.vp-doc blockquote {
margin: 16px 0;
padding-left: 16px;
border-left: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.vp-doc blockquote p {
margin: 0;
font-size: 16px;
}
.vp-doc hr {
height: 1px;
border: 0;
margin: 38px 0;
background: var(--vp-c-divider);
}
.vp-doc img {
max-width: 100%;
display: block;
margin: 22px auto;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
box-shadow: var(--vp-shadow-3);
}
.vp-doc ul,
.vp-doc ol {
padding-left: 1.35rem;
color: var(--vp-c-text-1);
}
.vp-doc li {
margin: 8px 0;
}
.vp-doc table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border-radius: 8px;
border: 1px solid var(--vp-c-border);
background: rgba(255, 255, 255, 0.02);
}
.vp-doc thead {
background: rgba(255, 255, 255, 0.04);
}
.vp-doc th,
.vp-doc td {
padding: 8px 16px;
border-bottom: 1px solid var(--vp-c-divider);
text-align: left;
}
.vp-doc tr:last-child td {
border-bottom: 0;
}
.vp-footer {
height: 72px;
padding: 0 32px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 72px;
color: var(--vp-c-text-3);
font-size: 14px;
border-top: 1px solid var(--vp-c-gutter);
}
@media (max-width: 1180px) {
.vp-shell {
padding-inline: 28px;
}
}
</style>
</head>
<body>
<div id="app">
<header class="vp-nav">
<div class="vp-brand">
<img class="vp-brand-logo" src="https://cf.s3.soulter.top/astrbot-logo.svg" alt="AstrBot logo" />
<div class="vp-brand-name">AstrBot</div>
</div>
<div class="vp-nav-actions">
<span>{{ version }}</span>
</div>
</header>
<div class="vp-shell">
<main class="vp-main">
<div class="vp-content-frame">
<div class="vp-hero is-hidden" id="heroBlock">
<div class="vp-hero-copy">
<h1 id="heroTitle">AstrBot Docs</h1>
<p id="heroLead">将长文本内容整理为单页文档,参考 VitePress 默认主题的深色配色、正文排版与代码块节奏,适合技术说明与发布页。</p>
</div>
</div>
<article class="vp-doc" id="content"></article>
</div>
</main>
</div>
<footer class="vp-footer">Rendered by AstrBot {{ version }}</footer>
</div>
<script>{{ shiki_runtime | safe }}</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"></script>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
marked.setOptions({
gfm: true,
breaks: false,
});
contentElement.innerHTML = marked.parse(source);
assignHeadingIds(contentElement);
enhanceCodeBlocks(contentElement);
if (window.renderMathInElement) {
window.renderMathInElement(contentElement, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false }
]
});
}
const headings = collectHeadings(contentElement);
populateHero(contentElement, headings);
function decodeBase64Utf8(base64Text) {
const binary = window.atob(base64Text || "");
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (window.TextDecoder) {
return new TextDecoder().decode(bytes);
}
let fallback = "";
bytes.forEach((byte) => {
fallback += String.fromCharCode(byte);
});
return decodeURIComponent(escape(fallback));
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function assignHeadingIds(root) {
Array.from(root.querySelectorAll("h1, h2, h3")).forEach((heading, index) => {
heading.id = `section-${index + 1}`;
});
}
function collectHeadings(root) {
return Array.from(root.querySelectorAll("h1, h2, h3")).map((heading) => ({
element: heading,
id: heading.id,
level: Number(heading.tagName.slice(1)),
text: heading.textContent.trim(),
}));
}
function populateHero(root, headings) {
const heroBlock = document.getElementById("heroBlock");
const heroTitle = document.getElementById("heroTitle");
const heroLead = document.getElementById("heroLead");
const firstHeading = headings.find((heading) => heading.level === 1) || headings[0];
if (!firstHeading) {
return;
}
heroBlock.classList.remove("is-hidden");
const leadParagraph = firstHeading.element.nextElementSibling;
const title = firstHeading.text;
heroTitle.textContent = title;
if (leadParagraph && leadParagraph.tagName === "P") {
heroLead.textContent = leadParagraph.textContent.trim();
leadParagraph.remove();
}
if (firstHeading.element.parentElement === root) {
firstHeading.element.remove();
}
}
function extractLanguage(codeElement) {
const className = codeElement.className || "";
const match = className.match(/language-([\\w+#.-]+)/i);
return match ? match[1] : "";
}
function enhanceCodeBlocks(root) {
const blocks = Array.from(root.querySelectorAll("pre > code")).map((codeElement) => ({
rawLanguage: extractLanguage(codeElement),
displayLanguage: extractLanguage(codeElement).trim().split(/\\s+/, 1)[0].toLowerCase(),
}));
if (window.AstrBotT2IShiki) {
window.AstrBotT2IShiki.highlightAllCodeBlocks(root, "github-dark");
}
Array.from(root.querySelectorAll("pre")).forEach((preElement, index) => {
if (preElement.parentElement && preElement.parentElement.classList.contains("vp-code-block")) {
return;
}
const block = blocks[index] || { displayLanguage: "" };
const wrapper = document.createElement("div");
wrapper.className = `language-${block.displayLanguage || "text"}`;
if (block.displayLanguage) {
wrapper.innerHTML = `<span class="lang">${escapeHtml(block.displayLanguage)}</span>`;
}
preElement.replaceWith(wrapper);
wrapper.appendChild(preElement);
});
}
})();
</script>
</body>
</html>

View File

@@ -2,246 +2,285 @@
<html>
<head>
<meta charset="utf-8"/>
<title>AstrBot {{ version }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
<link rel="stylesheet" href="/path/to/styles/default.min.css">
<script src="/path/to/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"
onload="renderMathInElement(document.getElementById('content'),{delimiters: [{left: '$$', right: '$$', display: true},{left: '$', right: '$', display: false}]});"></script>
<style>
#content {
min-width: 200px;
max-width: 85%;
margin: 0 auto;
padding: 2rem 1em 1em;
}
body {
word-break: break-word;
line-height: 1.75;
font-weight: 400;
font-size: 32px;
margin: 0;
padding: 0;
overflow-x: hidden;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.5;
margin-top: 35px;
margin-bottom: 10px;
padding-bottom: 5px;
}
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
margin-top: -1.5rem;
margin-bottom: 1rem;
}
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
content: "#";
display: inline-block;
color: #3eaf7c;
padding-right: 0.23em;
}
h1 {
position: relative;
font-size: 2.5rem;
margin-bottom: 5px;
}
h1::before {
font-size: 2.5rem;
}
h2 {
padding-bottom: 0.5rem;
font-size: 2.2rem;
border-bottom: 1px solid #ececec;
}
h3 {
font-size: 1.5rem;
padding-bottom: 0;
}
h4 {
font-size: 1.25rem;
}
h5 {
font-size: 1rem;
}
h6 {
margin-top: 5px;
}
p {
line-height: inherit;
margin-top: 22px;
margin-bottom: 22px;
}
strong {
color: #3eaf7c;
}
img {
max-width: 100%;
border-radius: 2px;
display: block;
margin: auto;
border: 3px solid rgba(62, 175, 124, 0.2);
}
hr {
border-top: 1px solid #3eaf7c;
border-bottom: none;
border-left: none;
border-right: none;
margin-top: 32px;
margin-bottom: 32px;
}
code {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
word-break: break-word;
overflow-x: auto;
padding: 0.2rem 0.5rem;
margin: 0;
color: #3eaf7c;
font-size: 0.85em;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
}
pre {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
overflow: auto;
position: relative;
line-height: 1.75;
border-radius: 6px;
border: 2px solid #3eaf7c;
}
pre > code {
font-size: 12px;
padding: 15px 12px;
margin: 0;
word-break: normal;
display: block;
overflow-x: auto;
color: #333;
background: #f8f8f8;
}
pre.shiki {
padding: 15px 12px;
}
pre.shiki > code {
padding: 0;
background: transparent !important;
color: inherit;
font-size: 12px;
}
a {
font-weight: 500;
text-decoration: none;
color: #3eaf7c;
}
a:hover, a:active {
border-bottom: 1.5px solid #3eaf7c;
}
a:before {
content: "⇲";
}
table {
display: inline-block !important;
font-size: 12px;
width: auto;
max-width: 100%;
overflow: auto;
border: solid 1px #3eaf7c;
}
thead {
background: #3eaf7c;
color: #fff;
text-align: left;
}
tr:nth-child(2n) {
background-color: rgba(62, 175, 124, 0.2);
}
th, td {
padding: 12px 7px;
line-height: 24px;
}
td {
min-width: 120px;
}
blockquote {
color: #666;
padding: 1px 23px;
margin: 22px 0;
border-left: 0.5rem solid rgba(62, 175, 124, 0.6);
border-color: #42b983;
background-color: #f8f8f8;
}
blockquote::after {
display: block;
content: "";
}
blockquote > p {
margin: 10px 0;
}
details {
border: none;
outline: none;
border-left: 4px solid #3eaf7c;
padding-left: 10px;
margin-left: 4px;
}
details summary {
cursor: pointer;
border: none;
outline: none;
background: white;
margin: 0 -17px;
}
details summary::-webkit-details-marker {
color: #3eaf7c;
}
ol, ul {
padding-left: 28px;
}
ol li, ul li {
margin-bottom: 0;
list-style: inherit;
}
ol li .task-list-item, ul li .task-list-item {
list-style: none;
}
ol li .task-list-item ul, ul li .task-list-item ul, ol li .task-list-item ol, ul li .task-list-item ol {
margin-top: 0;
}
ol ul, ul ul, ol ol, ul ol {
margin-top: 3px;
}
ol li {
padding-left: 6px;
}
ol li::marker {
color: #3eaf7c;
}
ul li {
list-style: none;
}
ul li:before {
content: "•";
margin-right: 4px;
color: #3eaf7c;
}
@media (max-width: 720px) {
h1 {
font-size: 24px;
}
h2 {
font-size: 20px;
}
h3 {
font-size: 18px;
}
}
</style>
</head>
<body>
<div style="background-color: #3276dc; color: #fff; font-size: 64px; ">
<div style="background-color: #3276dc; color: #fff; font-size: 64px;">
<span style="font-weight: bold; margin-left: 16px"># AstrBot</span>
<span>{{ version }}</span>
</div>
<article style="margin-top: 32px" id="content"></article>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.getElementById('content').innerHTML = marked.parse(`{{ text | safe}}`);
</script>
<script>{{ shiki_runtime | safe }}</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"></script>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
contentElement.innerHTML = marked.parse(source);
if (window.AstrBotT2IShiki) {
window.AstrBotT2IShiki.highlightAllCodeBlocks(contentElement, "github-light");
}
if (window.renderMathInElement) {
window.renderMathInElement(contentElement, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false }
]
});
}
function decodeBase64Utf8(base64Text) {
const binary = window.atob(base64Text || "");
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (window.TextDecoder) {
return new TextDecoder().decode(bytes);
}
let fallback = "";
bytes.forEach((byte) => {
fallback += String.fromCharCode(byte);
});
return decodeURIComponent(escape(fallback));
}
})();
</script>
</body>
</html>
<style>
#content {
min-width: 200px;
max-width: 85%;
margin: 0 auto;
padding: 2rem 1em 1em;
}
body {
word-break: break-word;
line-height: 1.75;
font-weight: 400;
font-size: 32px;
margin: 0;
padding: 0;
overflow-x: hidden;
color: #333;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.5;
margin-top: 35px;
margin-bottom: 10px;
padding-bottom: 5px;
}
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
margin-top: -1.5rem;
margin-bottom: 1rem;
}
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
content: "#";
display: inline-block;
color: #3eaf7c;
padding-right: 0.23em;
}
h1 {
position: relative;
font-size: 2.5rem;
margin-bottom: 5px;
}
h1::before {
font-size: 2.5rem;
}
h2 {
padding-bottom: 0.5rem;
font-size: 2.2rem;
border-bottom: 1px solid #ececec;
}
h3 {
font-size: 1.5rem;
padding-bottom: 0;
}
h4 {
font-size: 1.25rem;
}
h5 {
font-size: 1rem;
}
h6 {
margin-top: 5px;
}
p {
line-height: inherit;
margin-top: 22px;
margin-bottom: 22px;
}
strong {
color: #3eaf7c;
}
img {
max-width: 100%;
border-radius: 2px;
display: block;
margin: auto;
border: 3px solid rgba(62, 175, 124, 0.2);
}
hr {
border-top: 1px solid #3eaf7c;
border-bottom: none;
border-left: none;
border-right: none;
margin-top: 32px;
margin-bottom: 32px;
}
code {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
word-break: break-word;
overflow-x: auto;
padding: 0.2rem 0.5rem;
margin: 0;
color: #3eaf7c;
font-size: 0.85em;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
}
pre {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
overflow: auto;
position: relative;
line-height: 1.75;
border-radius: 6px;
border: 2px solid #3eaf7c;
}
pre > code {
font-size: 12px;
padding: 15px 12px;
margin: 0;
word-break: normal;
display: block;
overflow-x: auto;
color: #333;
background: #f8f8f8;
}
a {
font-weight: 500;
text-decoration: none;
color: #3eaf7c;
}
a:hover, a:active {
border-bottom: 1.5px solid #3eaf7c;
}
a:before {
content: "⇲";
}
table {
display: inline-block !important;
font-size: 12px;
width: auto;
max-width: 100%;
overflow: auto;
border: solid 1px #3eaf7c;
}
thead {
background: #3eaf7c;
color: #fff;
text-align: left;
}
tr:nth-child(2n) {
background-color: rgba(62, 175, 124, 0.2);
}
th, td {
padding: 12px 7px;
line-height: 24px;
}
td {
min-width: 120px;
}
blockquote {
color: #666;
padding: 1px 23px;
margin: 22px 0;
border-left: 0.5rem solid rgba(62, 175, 124, 0.6);
border-color: #42b983;
background-color: #f8f8f8;
}
blockquote::after {
display: block;
content: "";
}
blockquote > p {
margin: 10px 0;
}
details {
border: none;
outline: none;
border-left: 4px solid #3eaf7c;
padding-left: 10px;
margin-left: 4px;
}
details summary {
cursor: pointer;
border: none;
outline: none;
background: white;
margin: 0px -17px;
}
details summary::-webkit-details-marker {
color: #3eaf7c;
}
ol, ul {
padding-left: 28px;
}
ol li, ul li {
margin-bottom: 0;
list-style: inherit;
}
ol li .task-list-item, ul li .task-list-item {
list-style: none;
}
ol li .task-list-item ul, ul li .task-list-item ul, ol li .task-list-item ol, ul li .task-list-item ol {
margin-top: 0;
}
ol ul, ul ul, ol ol, ul ol {
margin-top: 3px;
}
ol li {
padding-left: 6px;
}
ol li::marker {
color: #3eaf7c;
}
ul li {
list-style: none;
}
ul li:before {
content: "•";
margin-right: 4px;
color: #3eaf7c;
}
@media (max-width: 720px) {
h1 {
font-size: 24px;
}
h2 {
font-size: 20px;
}
h3 {
font-size: 18px;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,11 @@ class TemplateManager:
所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。
"""
CORE_TEMPLATES = ["base.html", "astrbot_powershell.html"]
CORE_TEMPLATES = [
"base.html",
"astrbot_powershell.html",
"astrbot_vitepress.html",
]
def __init__(self) -> None:
self.builtin_template_dir = os.path.join(

View File

@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite --host",
"subset-icons": "node scripts/subset-mdi-font.mjs",
"build:t2i-shiki-runtime": "node scripts/build-t2i-shiki-runtime.mjs",
"build": "vue-tsc --noEmit && vite build",
"build-stage": "vue-tsc --noEmit && vite build --base=/vue/free/stage/",
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",

View File

@@ -0,0 +1,232 @@
import { createRequire } from "node:module";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { build } from "vite";
const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dashboardRoot = path.resolve(__dirname, "..");
const runtimeOutputFile = path.resolve(
dashboardRoot,
"..",
"astrbot",
"core",
"utils",
"t2i",
"template",
"shiki_runtime.iife.js",
);
const shikiRequire = createRequire(require.resolve("shiki/package.json"));
const languageSpecs = [
["bash", "bash"],
["css", "css"],
["html", "html"],
["javascript", "javascript"],
["json", "json"],
["jsx", "jsx"],
["markdown", "markdown"],
["powershell", "powershell"],
["python", "python"],
["sql", "sql"],
["tsx", "tsx"],
["typescript", "typescript"],
["xml", "xml"],
["yaml", "yaml"],
];
const themeSpecs = [
["github-light", "github-light"],
["github-dark", "github-dark"],
];
// Shiki exposes plain text as a built-in special language, so we keep it
// in the supported language list without importing a package for it.
const builtInLanguageSpecs = ["text"];
const languageAliases = {
bat: "powershell",
cjs: "javascript",
console: "bash",
cts: "typescript",
dockerfile: "bash",
env: "bash",
htm: "html",
js: "javascript",
md: "markdown",
mjs: "javascript",
mts: "typescript",
plain: "text",
plaintext: "text",
ps1: "powershell",
pwsh: "powershell",
py: "python",
shell: "bash",
shellscript: "bash",
sh: "bash",
svg: "xml",
text: "text",
ts: "typescript",
txt: "text",
vue: "html",
xhtml: "html",
xml: "xml",
yml: "yaml",
zsh: "bash",
};
function resolveShikiModule(specifier) {
return pathToFileURL(shikiRequire.resolve(specifier)).href;
}
function buildVirtualSource() {
const shikiImport = JSON.stringify(
pathToFileURL(require.resolve("shiki")).href,
);
const languageImports = languageSpecs
.map(
([, packageName], index) =>
`import lang${index} from ${JSON.stringify(resolveShikiModule(`@shikijs/langs/${packageName}`))};`,
)
.join("\n");
const themeImports = themeSpecs
.map(
([, packageName], index) =>
`import theme${index} from ${JSON.stringify(resolveShikiModule(`@shikijs/themes/${packageName}`))};`,
)
.join("\n");
const supportedLanguages = [
...builtInLanguageSpecs,
...languageSpecs.map(([runtimeName]) => runtimeName),
];
return `import { createHighlighterCoreSync, createJavaScriptRegexEngine } from ${shikiImport};
${languageImports}
${themeImports}
const highlighter = createHighlighterCoreSync({
engine: createJavaScriptRegexEngine(),
langs: [${languageSpecs.map((_, index) => `...lang${index}`).join(", ")}],
themes: [${themeSpecs.map((_, index) => `theme${index}`).join(", ")}],
});
const supportedLanguages = new Set(${JSON.stringify(supportedLanguages)});
const languageAliases = ${JSON.stringify(languageAliases)};
const supportedThemes = new Set(${JSON.stringify(themeSpecs.map(([theme]) => theme))});
function normalizeLanguage(language) {
const normalized = String(language || "").trim().toLowerCase();
if (!normalized) {
return "text";
}
if (normalized in languageAliases) {
return languageAliases[normalized];
}
return supportedLanguages.has(normalized) ? normalized : "text";
}
function normalizeTheme(theme) {
const normalized = String(theme || "").trim();
return supportedThemes.has(normalized) ? normalized : "github-light";
}
function extractLanguage(codeElement) {
const className = codeElement.className || "";
const match = className.match(/language-([\\w+#.-]+)/i);
return match ? match[1] : "";
}
function renderCodeToHtml(code, language, theme) {
const normalizedTheme = normalizeTheme(theme);
try {
return highlighter.codeToHtml(String(code || ""), {
lang: normalizeLanguage(language),
theme: normalizedTheme,
});
} catch (error) {
console.warn("Failed to render T2I code block with Shiki.", error);
return highlighter.codeToHtml(String(code || ""), {
lang: "text",
theme: normalizedTheme,
});
}
}
function highlightAllCodeBlocks(root, theme) {
if (!root) {
return;
}
root.querySelectorAll("pre > code").forEach((codeElement) => {
const preElement = codeElement.parentElement;
if (!preElement || preElement.classList.contains("shiki")) {
return;
}
preElement.outerHTML = renderCodeToHtml(
codeElement.textContent || "",
extractLanguage(codeElement),
theme,
);
});
}
window.AstrBotT2IShiki = Object.freeze({
highlightAllCodeBlocks,
normalizeLanguage,
renderCodeToHtml,
});
`;
}
async function main() {
const tempDir = mkdtempSync(path.join(tmpdir(), "astrbot-t2i-shiki-runtime-"));
const entryPath = path.join(tempDir, "entry.mjs");
writeFileSync(entryPath, buildVirtualSource(), "utf-8");
try {
await build({
configFile: false,
logLevel: "info",
publicDir: false,
build: {
chunkSizeWarningLimit: 1500,
cssCodeSplit: false,
emptyOutDir: false,
lib: {
entry: entryPath,
fileName: () => path.basename(runtimeOutputFile),
formats: ["iife"],
name: "AstrBotT2IShikiRuntime",
},
minify: "esbuild",
outDir: path.dirname(runtimeOutputFile),
rollupOptions: {
output: {
inlineDynamicImports: true,
},
},
sourcemap: false,
target: "es2018",
},
});
} finally {
rmSync(tempDir, { force: true, recursive: true });
}
console.log(`Built ${runtimeOutputFile}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});