Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 61 additions & 6 deletions astrbot/core/utils/t2i/network_strategy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import base64
import logging
import random
import re
from functools import lru_cache
from pathlib import Path

Expand All @@ -15,6 +15,11 @@
from . import RenderStrategy

ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img"
SHIKI_RUNTIME_SCRIPT_ID = "astrbot-t2i-shiki-runtime"
SHIKI_RUNTIME_TEMPLATE_PATTERN = re.compile(r"\{\{\s*shiki_runtime\s*\|\s*safe\s*\}\}")
JINJA_SYNTAX_PATTERN = re.compile(r"\{[{%#]")
JINJA_RAW_OPEN_PATTERN = re.compile(r"{%-?\s*raw\s*-?%}")
JINJA_RAW_CLOSE_PATTERN = re.compile(r"{%-?\s*endraw\s*-?%}")

logger = logging.getLogger("astrbot")

Expand All @@ -41,7 +46,49 @@ def get_shiki_runtime() -> str:
)
return ""

return runtime.replace("</script", "<\\/script")
return re.sub(r"</(script)", r"<\/\1", runtime, flags=re.IGNORECASE)


def _is_inside_jinja_raw_block(tmpl_str: str, index: int) -> bool:
raw_open_index = -1
for match in JINJA_RAW_OPEN_PATTERN.finditer(tmpl_str, 0, index):
raw_open_index = match.start()

raw_close_index = -1
for match in JINJA_RAW_CLOSE_PATTERN.finditer(tmpl_str, 0, index):
raw_close_index = match.start()

return raw_open_index > raw_close_index


def _wrap_runtime_for_jinja(tmpl_str: str, script: str, index: int) -> str:
if not JINJA_SYNTAX_PATTERN.search(script) or _is_inside_jinja_raw_block(
tmpl_str,
index,
):
return script

return f"{{% raw %}}{script}{{% endraw %}}"


def inject_shiki_runtime(tmpl_str: str) -> str:
if SHIKI_RUNTIME_SCRIPT_ID in tmpl_str or SHIKI_RUNTIME_TEMPLATE_PATTERN.search(
tmpl_str,
):
return tmpl_str

runtime = get_shiki_runtime()
if not runtime:
return tmpl_str

script = f'<script id="{SHIKI_RUNTIME_SCRIPT_ID}">{runtime}</script>'
head_close = re.search(r"</head\s*>", tmpl_str, flags=re.IGNORECASE)
if head_close:
script = _wrap_runtime_for_jinja(tmpl_str, script, head_close.start())
return f"{tmpl_str[: head_close.start()]} {script}\n{tmpl_str[head_close.start() :]}"

script = _wrap_runtime_for_jinja(tmpl_str, script, 0)
return f"{script}\n{tmpl_str}"


class NetworkRenderStrategy(RenderStrategy):
Expand Down Expand Up @@ -101,11 +148,17 @@ async def render_custom_template(
options: dict | None = None,
) -> str:
"""使用自定义文转图模板"""
default_options = {"full_page": True, "type": "jpeg", "quality": 40}
default_options = {
"full_page": True,
"type": "jpeg",
"quality": 40,
}
if options:
default_options |= options

tmpl_data = {"shiki_runtime": get_shiki_runtime()} | tmpl_data
if SHIKI_RUNTIME_TEMPLATE_PATTERN.search(tmpl_str):
tmpl_data = {"shiki_runtime": get_shiki_runtime()} | tmpl_data
tmpl_str = inject_shiki_runtime(tmpl_str)
post_data = {
"tmpl": tmpl_str,
"json": return_url,
Expand Down Expand Up @@ -158,9 +211,11 @@ async def render(
if not template_name:
template_name = "base"
tmpl_str = await self.get_template(name=template_name)
text_base64 = base64.b64encode(text.encode("utf-8")).decode("ascii")
return await self.render_custom_template(
tmpl_str,
{"text_base64": text_base64, "version": f"v{VERSION}"},
{
"text": text,
"version": f"v{VERSION}",
},
return_url,
)
18 changes: 2 additions & 16 deletions astrbot/core/utils/t2i/template/astrbot_powershell.html
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,14 @@
<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>
<textarea id="markdown-source" hidden>{{ text | safe }}</textarea>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
const source = document.getElementById("markdown-source").value;

contentElement.innerHTML = marked.parse(source);

Expand All @@ -198,20 +198,6 @@
});
}

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>
Expand Down
19 changes: 2 additions & 17 deletions astrbot/core/utils/t2i/template/astrbot_vitepress.html
Original file line number Diff line number Diff line change
Expand Up @@ -415,14 +415,14 @@ <h1 id="heroTitle">AstrBot Docs</h1>
<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>
<textarea id="markdown-source" hidden>{{ text | safe }}</textarea>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
const source = document.getElementById("markdown-source").value;

marked.setOptions({
gfm: true,
Expand All @@ -446,21 +446,6 @@ <h1 id="heroTitle">AstrBot Docs</h1>
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;")
Expand Down
18 changes: 2 additions & 16 deletions astrbot/core/utils/t2i/template/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,14 @@
</div>
<article style="margin-top: 32px" id="content"></article>

<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>
<textarea id="markdown-source" hidden>{{ text | safe }}</textarea>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
const source = document.getElementById("markdown-source").value;

contentElement.innerHTML = marked.parse(source);

Expand All @@ -266,20 +266,6 @@
});
}

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>
Expand Down
77 changes: 71 additions & 6 deletions dashboard/src/components/shared/T2ITemplateEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -298,17 +298,82 @@ const syncPreviewVersion = async () => {

const previewData = computed(() => ({
text: tm('t2iTemplateEditor.previewText') || '这是一个示例文本,用于预览模板效果。\n\n这里可以包含多行文本,支持换行和各种格式。',
version: previewVersion.value
version: previewVersion.value
}))

const injectShikiRuntime = (content) => {
if (content.includes('astrbot-t2i-shiki-runtime')) {
return content
}

const runtimeScript = getShikiRuntimeScript()
const headClose = content.search(/<\/head\s*>/i)
if (headClose >= 0) {
return `${content.slice(0, headClose)} ${runtimeScript}\n${content.slice(headClose)}`
}

return `${runtimeScript}\n${content}`
}

const getShikiRuntimeScript = () => '<script id="astrbot-t2i-shiki-runtime" src="/t2i/shiki_runtime.iife.js"></scr' + 'ipt>'

const hasMarkdownSource = (content) => /<[^>]+\bid=["']markdown-source["']/i.test(content)

const insertMarkdownSource = (content) => {
const sourceElement = ' <textarea id="markdown-source" hidden>{{ text | safe }}</textarea>\n'
const markedScript = content.search(/^[ \t]*<script\s+src=["']https:\/\/cdn\.jsdelivr\.net\/npm\/marked\/marked\.min\.js["']><\/script>[ \t]*\r?\n?/im)
if (markedScript >= 0) {
return `${content.slice(0, markedScript)}${sourceElement}${content.slice(markedScript)}`
}

const bodyClose = content.search(/<\/body\s*>/i)
if (bodyClose >= 0) {
return `${content.slice(0, bodyClose)}${sourceElement}${content.slice(bodyClose)}`
}

return `${sourceElement}${content}`
}

const normalizeMarkdownSource = (content) => {
let normalized = content.replace(
/<script\s+id=["']markdown-source["']\s+type=["']text\/plain["']>\s*\{\{\s*text\s*\|\s*safe\s*\}\}\s*<\/script>/gi,
'<textarea id="markdown-source" hidden>{{ text | safe }}</textarea>'
)

normalized = normalized.replace(
/decodeBase64Utf8\("\{\{\s*text_base64\s*\}\}"\)/g,
'document.getElementById("markdown-source").value'
)
normalized = normalized.replace(
/document\.getElementById\(["']markdown-source["']\)\.textContent/g,
'document.getElementById("markdown-source").value'
)

if (/\{\{\s*text_base64\s*\}\}/.test(normalized) && !hasMarkdownSource(normalized)) {
normalized = insertMarkdownSource(normalized)
}

return normalized
}

const previewContent = computed(() => {
try {
let content = templateContent.value
content = content.replace(/\{\{\s*text\s*\|\s*safe\s*\}\}/g, previewData.value.text)
content = content.replace(/\{\{\s*version\s*\}\}/g, previewData.value.version)
return content
let content = normalizeMarkdownSource(templateContent.value)
content = content.replace(/\{\{\s*text\s*\|\s*safe\s*\}\}/g, () => previewData.value.text)
content = content.replace(/\{\{\s*version\s*\}\}/g, () => previewData.value.version)
let usedLegacyShikiPlaceholder = false
content = content.replace(/<script\b[^>]*>\s*\{\{\s*shiki_runtime\s*\|\s*safe\s*\}\}\s*<\/script>/gi, () => {
usedLegacyShikiPlaceholder = true
return getShikiRuntimeScript()
})
content = content.replace(/\{\{\s*shiki_runtime\s*\|\s*safe\s*\}\}/g, () => {
usedLegacyShikiPlaceholder = true
return getShikiRuntimeScript()
})
return usedLegacyShikiPlaceholder ? content : injectShikiRuntime(content)
} catch (error) {
return `<div style="color: red; padding: 20px;">模板渲染错误: ${error.message}</div>`
const errorMessage = error instanceof Error ? error.message : String(error)
return `<div style="color: red; padding: 20px;">模板渲染错误: ${errorMessage}</div>`
}
})

Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/i18n/locales/en-US/core/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"livePreview": "Live Preview (may differ)",
"refreshPreview": "Refresh Preview",
"previewText": "This is a sample text used to preview the template output.\n\nIt can contain multiple lines and various formatting.",
"syntaxHint": "Supports jinja2 syntax. Available variables: text | safe (text to render), version (AstrBot version)",
"syntaxHint": "Supports jinja2 syntax. Available variables: text | safe (text to render), version (AstrBot version). If the preview test content does not load or looks wrong, click Reset Base to restore the default template.",
"saveAndApply": "Save and Apply Current Template",
"confirmReset": "Confirm Reset",
"confirmResetMessage": "Are you sure you want to reset the 'base' template to default content? Any unsaved changes in the editor will be lost. This action cannot be undone.",
Expand Down
4 changes: 2 additions & 2 deletions dashboard/src/i18n/locales/ru-RU/core/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"livePreview": "Предпросмотр (может отличаться)",
"refreshPreview": "Обновить",
"previewText": "Это пример текста для предпросмотра результата шаблона.\n\nОн может содержать несколько строк и различные форматы.",
"syntaxHint": "Поддерживается синтаксис jinja2. Переменные: text | safe (текст для рендеринга), version (версия AstrBot)",
"syntaxHint": "Поддерживается синтаксис jinja2. Переменные: text | safe (текст для рендеринга), version (версия AstrBot). Если тестовый текст предпросмотра не загружается или выглядит неверно, нажмите «Сбросить 'base'», чтобы восстановить шаблон по умолчанию.",
"saveAndApply": "Сохранить и применить текущий шаблон",
"confirmReset": "Подтверждение сброса",
"confirmResetMessage": "Вы уверены, что хотите сбросить шаблон 'base' до значений по умолчанию? Все несохраненные изменения будут потеряны. Это действие необратимо.",
Expand All @@ -109,4 +109,4 @@
"confirmAction": "Подтверждение действия",
"confirmApplyMessage": "Вы уверены, что хотите сохранить изменения в '{name}' и сделать его активным шаблоном?"
}
}
}
2 changes: 1 addition & 1 deletion dashboard/src/i18n/locales/zh-CN/core/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"livePreview": "实时预览(可能有差异)",
"refreshPreview": "刷新预览",
"previewText": "这是一个示例文本,用于预览模板效果。\n\n这里可以包含多行文本,支持换行和各种格式。",
"syntaxHint": "支持 jinja2 语法。可用变量:text | safe(要渲染的文本), version(AstrBot 版本)",
"syntaxHint": "支持 jinja2 语法。可用变量:text | safe(要渲染的文本), version(AstrBot 版本)。如果预览测试内容未加载或显示异常,请点击“重置Base”恢复默认模板。",
"saveAndApply": "保存应用当前编辑模板",
"confirmReset": "确认重置",
"confirmResetMessage": "确定要将 'base' 模板恢复为默认内容吗?当前编辑器中的任何未保存更改将丢失。此操作无法撤销。",
Expand Down
Loading
Loading