Skip to content

fix: restore T2I text template rendering#7789

Merged
RC-CHN merged 5 commits intoAstrBotDevs:masterfrom
camera-2018:fix-t2i-rendering
Apr 27, 2026
Merged

fix: restore T2I text template rendering#7789
RC-CHN merged 5 commits intoAstrBotDevs:masterfrom
camera-2018:fix-t2i-rendering

Conversation

@camera-2018
Copy link
Copy Markdown
Contributor

@camera-2018 camera-2018 commented Apr 25, 2026

Modifications / 改动点

#7501 引入了一个破坏性更改,会使用户现有 t2i 模板内的 text | safe 标识无法被识别

本提交将其还原,兼容读取原先用户的text | safe

  • 将后端 T2I 渲染数据从 text_base64 恢复为原始 text | safe
  • 默认注入内置 Shiki runtime,不再要求模板显式写入 {{ shiki_runtime | safe }}
  • 更新内置 T2I 模板,通过隐藏 textarea 读取 Markdown,避免代码块反引号破坏解析
  • WebUI dev/build 预览通过vite配置提供 /t2i/shiki_runtime.iife.js 静态资源
  • 将 zh-CN、en-US、ru-RU 的 T2I 预览文本改为包含 Markdown、表格、公式、代码块的测试用例
  • 新增回归测试,覆盖原始文本渲染、runtime 注入、旧占位符兼容和模板兼容性

Screenshots or Test Results / 运行截图或测试结果

image image image image

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:

  • Ensure T2I templates correctly render existing text | safe placeholders instead of relying on text_base64.
  • Handle legacy user templates by migrating old text and Shiki runtime patterns to the new format without breaking existing setups.
  • Fix Shiki runtime escaping so injected scripts survive Jinja rendering and do not break HTML.

Enhancements:

  • Inject the Shiki runtime into T2I templates automatically when needed, while still honoring legacy shiki_runtime placeholders.
  • Refine built-in T2I templates to read markdown from a hidden textarea, improving compatibility with complex markdown blocks.
  • Improve the T2I template editor preview with a richer default markdown example demonstrating headings, tables, formulas, and code blocks.
  • Expose the Shiki runtime bundle as a static asset in dev and build via a dedicated Vite plugin.

Tests:

  • Add unit tests covering raw text passing in network render strategy, builtin template behavior, legacy template migration, Shiki runtime injection, and custom template rendering paths.

- 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
Copilot AI review requested due to automatic review settings April 25, 2026 08:38
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Apr 25, 2026
@dosubot dosubot Bot added area:core The bug / feature is about astrbot's core, backend area:webui The bug / feature is about webui(dashboard) of astrbot. labels Apr 25, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/utils/t2i/template_manager.py Outdated
Comment thread astrbot/core/utils/t2i/network_strategy.py Outdated
Comment thread astrbot/core/utils/t2i/template_manager.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread astrbot/core/utils/t2i/network_strategy.py
Comment thread astrbot/core/utils/t2i/network_strategy.py
Comment thread astrbot/core/utils/t2i/template/astrbot_powershell.html
Comment thread astrbot/core/utils/t2i/template/astrbot_vitepress.html
Comment thread astrbot/core/utils/t2i/template/base.html
Comment thread astrbot/core/utils/t2i/template_manager.py Outdated
Comment thread astrbot/core/utils/t2i/template_manager.py Outdated
Comment thread dashboard/src/components/shared/T2ITemplateEditor.vue
Comment thread dashboard/src/components/shared/T2ITemplateEditor.vue
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 of text_base64) and add automatic Shiki runtime injection with legacy placeholder compatibility.
  • Update built-in T2I templates to read Markdown from a hidden textarea and 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.

Comment thread astrbot/core/utils/t2i/template_manager.py Outdated
Comment thread dashboard/vite.config.ts Outdated
Comment thread dashboard/src/components/shared/T2ITemplateEditor.vue Outdated
Comment thread astrbot/core/utils/t2i/template/base.html
Comment thread astrbot/core/utils/t2i/template/astrbot_powershell.html
Comment thread astrbot/core/utils/t2i/template/astrbot_vitepress.html
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Apr 27, 2026
@RC-CHN
Copy link
Copy Markdown
Member

RC-CHN commented Apr 27, 2026

惩罚你

@RC-CHN RC-CHN merged commit 72f4e74 into AstrBotDevs:master Apr 27, 2026
21 checks passed
LIghtJUNction pushed a commit that referenced this pull request Apr 28, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend area:webui The bug / feature is about webui(dashboard) of astrbot. lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants