fix: restore T2I text template rendering#7789
Conversation
- keep using {{ text | safe }} instead of text_base64
- inject Shiki runtime by default for T2I templates
- update built-in templates to read markdown from a hidden textarea
- improve WebUI preview sample text and Shiki runtime serving
- add regression tests for template rendering and runtime injection
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The Shiki runtime injection and markdown-source normalization logic is now duplicated in both the frontend (T2ITemplateEditor.vue) and backend (TemplateManager/network_strategy); consider centralizing the regex patterns/IDs and helper behavior in a shared module or at least a single source of truth to reduce drift in future changes.
- TemplateManager._remove_decode_base64_utf8_helper relies on brace-counting across lines and will treat any
decodeBase64Utf8occurrence as the target function; you may want to tighten this to match the function more explicitly (e.g., with start/end markers or a more constrained regex) to avoid accidentally stripping unrelated code blocks.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The Shiki runtime injection and markdown-source normalization logic is now duplicated in both the frontend (T2ITemplateEditor.vue) and backend (TemplateManager/network_strategy); consider centralizing the regex patterns/IDs and helper behavior in a shared module or at least a single source of truth to reduce drift in future changes.
- TemplateManager._remove_decode_base64_utf8_helper relies on brace-counting across lines and will treat any `decodeBase64Utf8` occurrence as the target function; you may want to tighten this to match the function more explicitly (e.g., with start/end markers or a more constrained regex) to avoid accidentally stripping unrelated code blocks.
## Individual Comments
### Comment 1
<location path="astrbot/core/utils/t2i/template_manager.py" line_range="130-139" />
<code_context>
+ return f"{source_element}{content}"
+
+ @staticmethod
+ def _remove_decode_base64_utf8_helper(content: str) -> str:
+ lines = content.splitlines(keepends=True)
+ migrated_lines: list[str] = []
+ index = 0
+ while index < len(lines):
+ line = lines[index]
+ if "function decodeBase64Utf8(base64Text)" not in line:
+ migrated_lines.append(line)
+ index += 1
+ continue
+
+ depth = 0
+ while index < len(lines):
+ depth += lines[index].count("{") - lines[index].count("}")
+ index += 1
+ if depth <= 0:
+ break
+
+ if migrated_lines and not migrated_lines[-1].strip():
+ migrated_lines.pop()
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Function-removal via brace counting is brittle and can strip unrelated code if braces appear in strings/comments.
The current `_remove_decode_base64_utf8_helper` logic assumes every `{`/`}` pair reflects JS block structure. If user templates include braces in strings, comments, or embedded content, the `depth` counter can run past the intended function and remove unrelated code. Consider a more targeted approach (e.g., match the function header and stop at the corresponding closing `}` at the same indentation, or limit scanning to a local region around the match) instead of counting braces across the whole file.
</issue_to_address>
### Comment 2
<location path="astrbot/core/utils/t2i/network_strategy.py" line_range="59-62" />
<code_context>
+ if not runtime:
+ return tmpl_str
+
+ script = (
+ "{% raw %}"
+ f'<script id="{SHIKI_RUNTIME_SCRIPT_ID}">{runtime}</script>'
+ "{% endraw %}"
+ )
+ head_close = re.search(r"</head\s*>", tmpl_str, flags=re.IGNORECASE)
</code_context>
<issue_to_address>
**issue:** Injecting a `{% raw %}`-wrapped script can break templates that already use raw blocks around large regions.
Because this snippet always wraps the runtime in `{% raw %}...{% endraw %}`, it will fail on templates that already have a large `{% raw %}` region, since Jinja doesn’t allow nested raw blocks. Given that `get_shiki_runtime()` already escapes `</script>`, consider dropping the raw wrapper, making it conditional on the presence of `{{`/`{%` in the runtime, or offering a way for callers to opt out when they control the injection point.
</issue_to_address>
### Comment 3
<location path="astrbot/core/utils/t2i/template_manager.py" line_range="63" />
<code_context>
+ f.write(migrated_content)
+
+ @staticmethod
+ def _migrate_legacy_template_content(content: str) -> str:
+ had_legacy_text = bool(
+ re.search(
</code_context>
<issue_to_address>
**issue (complexity):** Consider breaking the migration logic into named helper functions with compiled regex constants so the main method reads as a simple, understandable pipeline of steps.
Consider extracting and naming the individual migration steps and regexes to reduce cognitive load without changing behavior.
For example, `_migrate_legacy_template_content` can be decomposed into small helpers and constants:
```python
# Top-level constants (module scope)
SHIKI_RUNTIME_SCRIPT_RE = re.compile(
r"^[ \t]*<script>\s*\{\{\s*shiki_runtime\s*\|\s*safe\s*\}\}\s*</script>[ \t]*\r?\n?",
flags=re.MULTILINE,
)
LEGACY_TEXT_BASE64_CALL_RE = re.compile(
r'decodeBase64Utf8\("\{\{\s*text_base64\s*\}\}"\)',
)
MARKDOWN_SOURCE_SCRIPT_RE = re.compile(
r"<script\s+id=[\"']markdown-source[\"']\s+type=[\"']text/plain[\"']>\s*\{\{\s*text\s*\|\s*safe\s*\}\}\s*</script>",
flags=re.IGNORECASE,
)
MARKED_SCRIPT_RE = re.compile(
r"^[ \t]*<script\s+src=[\"']https://cdn\.jsdelivr\.net/npm/marked/marked\.min\.js[\"']></script>[ \t]*\r?\n?",
flags=re.MULTILINE,
)
MARKDOWN_SOURCE_ID_RE = re.compile(
r"<[a-z][^>]*\bid=[\"']markdown-source[\"']",
flags=re.IGNORECASE,
)
```
Then the core method becomes a readable pipeline:
```python
@staticmethod
def _migrate_legacy_template_content(content: str) -> str:
has_legacy_text = TemplateManager._has_legacy_text_usage(content)
content = TemplateManager._remove_shiki_runtime_script(content)
content = TemplateManager._rewrite_text_base64_usage(content)
content = TemplateManager._normalize_markdown_source_element(content)
content = TemplateManager._remove_decode_base64_utf8_helper(content)
if has_legacy_text and not TemplateManager._has_markdown_source(content):
content = TemplateManager._insert_markdown_source_element(content)
return content
```
With small helpers that just wrap the existing logic:
```python
@staticmethod
def _has_legacy_text_usage(content: str) -> bool:
return bool(LEGACY_TEXT_BASE64_CALL_RE.search(content))
@staticmethod
def _remove_shiki_runtime_script(content: str) -> str:
return SHIKI_RUNTIME_SCRIPT_RE.sub("", content)
@staticmethod
def _rewrite_text_base64_usage(content: str) -> str:
content = LEGACY_TEXT_BASE64_CALL_RE.sub(
'document.getElementById("markdown-source").value',
content,
)
return content.replace(
'document.getElementById("markdown-source").textContent',
'document.getElementById("markdown-source").value',
)
@staticmethod
def _normalize_markdown_source_element(content: str) -> str:
return MARKDOWN_SOURCE_SCRIPT_RE.sub(
'<textarea id="markdown-source" hidden>{{ text | safe }}</textarea>',
content,
)
```
`_has_markdown_source` and `_insert_markdown_source_element` can also use the compiled regexes and be a bit clearer:
```python
@staticmethod
def _has_markdown_source(content: str) -> bool:
return bool(MARKDOWN_SOURCE_ID_RE.search(content))
@staticmethod
def _insert_markdown_source_element(content: str) -> str:
source_element = ' <textarea id="markdown-source" hidden>{{ text | safe }}</textarea>\n'
marked_script = MARKED_SCRIPT_RE.search(content)
if marked_script:
return (
content[:marked_script.start()]
+ source_element
+ content[marked_script.start():]
)
body_close = re.search(r"</body\s*>", content, flags=re.IGNORECASE)
if body_close:
return (
content[:body_close.start()]
+ source_element
+ content[body_close.start():]
)
return f"{source_element}{content}"
```
For `_remove_decode_base64_utf8_helper`, the brace-counting logic is currently embedded and a bit opaque. At minimum, you can factor it into a named helper and document the assumptions, which makes its intent clearer without changing behavior:
```python
@staticmethod
def _remove_function_block(content: str, signature: str) -> str:
"""
Remove a top-level function by its signature line.
Assumes:
- The function declaration line contains `signature`.
- Braces in the function body are balanced.
- No interfering unmatched braces in strings/comments.
"""
lines = content.splitlines(keepends=True)
migrated: list[str] = []
i = 0
while i < len(lines):
line = lines[i]
if signature not in line:
migrated.append(line)
i += 1
continue
depth = 0
while i < len(lines):
depth += lines[i].count("{") - lines[i].count("}")
i += 1
if depth <= 0:
break
if migrated and not migrated[-1].strip():
migrated.pop()
return "".join(migrated)
@staticmethod
def _remove_decode_base64_utf8_helper(content: str) -> str:
return TemplateManager._remove_function_block(
content,
"function decodeBase64Utf8(base64Text)",
)
```
This keeps all existing behavior but makes each step independently understandable and testable, reducing the perceived complexity in the main migration function.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This pull request refactors the Text-to-Image (T2I) rendering system to use raw text stored in a hidden <textarea> instead of Base64-encoded strings, simplifying template logic and improving performance. It also introduces automatic injection of the Shiki syntax highlighting runtime and includes a migration utility to update legacy user templates. The review feedback highlights a critical security concern regarding the use of the | safe filter inside <textarea> elements, which could lead to XSS or broken rendering if the input contains </textarea>. Additionally, it is recommended to restore the text_base64 variable in the template data to maintain backward compatibility with existing custom templates and plugins.
There was a problem hiding this comment.
Pull request overview
Restores legacy T2I template rendering behavior after #7501 so existing templates using text | safe continue to work, while keeping Shiki highlighting working via default runtime injection.
Changes:
- Switch T2I rendering data back to passing raw
text(instead oftext_base64) and add automatic Shiki runtime injection with legacy placeholder compatibility. - Update built-in T2I templates to read Markdown from a hidden
textareaand remove the previous base64 decode helper/runtime placeholder requirement. - Add dashboard support for serving/building the Shiki runtime asset and add regression tests covering legacy/compat paths.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
astrbot/core/utils/t2i/network_strategy.py |
Restores raw text templating and injects Shiki runtime unless legacy placeholder is present. |
astrbot/core/utils/t2i/template_manager.py |
Migrates user templates away from text_base64 + legacy runtime placeholder to the new textarea-based approach. |
astrbot/core/utils/t2i/template/base.html |
Switches Markdown source to hidden textarea and removes base64 decode helper. |
astrbot/core/utils/t2i/template/astrbot_powershell.html |
Same textarea-based Markdown source change + removal of legacy decode helper/placeholder. |
astrbot/core/utils/t2i/template/astrbot_vitepress.html |
Same textarea-based Markdown source change + removal of legacy decode helper/placeholder. |
dashboard/vite.config.ts |
Adds a Vite plugin to serve/emit /t2i/shiki_runtime.iife.js. |
dashboard/src/components/shared/T2ITemplateEditor.vue |
Improves WebUI preview: normalizes legacy template patterns and injects runtime for preview. |
dashboard/src/i18n/locales/zh-CN/core/shared.json |
Updates preview text to a richer Markdown/KaTeX/code/table test case. |
dashboard/src/i18n/locales/en-US/core/shared.json |
Updates preview text to a richer Markdown/KaTeX/code/table test case. |
dashboard/src/i18n/locales/ru-RU/core/shared.json |
Updates preview text to a richer Markdown/KaTeX/code/table test case. |
tests/unit/test_network_render_strategy.py |
Adds regression tests for raw text passing, runtime injection, and legacy template migration. |
|
惩罚你 |
* fix: restore T2I text template rendering
- keep using {{ text | safe }} instead of text_base64
- inject Shiki runtime by default for T2I templates
- update built-in templates to read markdown from a hidden textarea
- improve WebUI preview sample text and Shiki runtime serving
- add regression tests for template rendering and runtime injection
* fix: prevent injected Shiki runtime from breaking T2I templates
* fix(t2i): restore raw text template rendering
* test(t2i): remove test
* fix(t2i): restore previewText
Modifications / 改动点
#7501 引入了一个破坏性更改,会使用户现有 t2i 模板内的 text | safe 标识无法被识别
本提交将其还原,兼容读取原先用户的
text | safetext_base64恢复为原始text | safe{{ shiki_runtime | safe }}/t2i/shiki_runtime.iife.js静态资源Screenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
/ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txt 和 pyproject.toml 文件相应位置。
😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Restore T2I template rendering to use raw text instead of base64 and make Shiki runtime injection automatic and backward compatible.
Bug Fixes:
text | safeplaceholders instead of relying ontext_base64.Enhancements:
shiki_runtimeplaceholders.Tests: