Skip to content

feat: support local README language switching in plugin docs preview - #9797

Open
yuluo-feather wants to merge 1 commit into
AstrBotDevs:masterfrom
yuluo-feather:feat/plugin-readme-lang
Open

feat: support local README language switching in plugin docs preview#9797
yuluo-feather wants to merge 1 commit into
AstrBotDevs:masterfrom
yuluo-feather:feat/plugin-readme-lang

Conversation

@yuluo-feather

@yuluo-feather yuluo-feather commented Aug 24, 2026

Copy link
Copy Markdown

Motivation / 动机

插件详情页「文档预览」只渲染插件目录的 README.md。双语插件的 README 顶部通常有语言切换链接(如 README.zh-CN.md),在 WebUI 预览中点击会直接新开标签页跳转到 GitHub,用户被迫离开管理界面才能阅读其他语言版本;在内网、无法访问 GitHub 的环境下则完全不可用。该功能已通过 Issue #9794 与作者讨论。

Modifications / 改动点

  • 后端:GET /api/v1/plugins/readmeGET /api/v1/plugins/{plugin_id}/readme 增加可选 file 参数;PluginService.get_plugin_readme 支持按文件名读取(白名单 README*.md + 禁止路径穿越,指定语言文件不存在时回退默认 README.md,不改变旧行为)

  • 前端:PluginDetailPage.vuerenderMarkdown 拦截以 README*.md 结尾的相对链接,点击时请求本地对应文件并原地重渲染,不再跳转 GitHub;市场来源(远端 URL)行为不变

  • 同步更新 OpenAPI 生成类型(types.gen.ts)与 v1.ts API 封装

  • This is NOT a breaking change. / 这不是一个破坏性变更。

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

  • 后端:ast.parse 语法校验通过(plugins.py 1514 行 / plugin_service.py 2077 行)
  • 前端:本地 npm run build 通过 — vue-tsc --noEmit && vite build✓ built in 44.21s
  • patch 在干净 clone 上 git apply 干净应用,前后逐文件比对一致;最终 5 files changed, 61 insertions(+), 6 deletions(-)

Checklist / 检查清单

Summary by Sourcery

Support in-place local language switching for plugin README previews while preserving remote marketplace link behavior.

New Features:

  • Enable plugin README previews to switch between local language-specific README files without leaving the WebUI.

Bug Fixes:

  • Fall back to the default README.md when a requested README variant is unavailable while preserving existing behavior.

Enhancements:

  • Restrict README file selection to safe README*.md filenames and support the optional file parameter across the plugin README API and generated frontend client.

Summary by Sourcery

Support local language switching for plugin README previews without requiring users to leave the WebUI.

New Features:

  • Enable local in-place switching between language-specific plugin README files in the WebUI preview.
  • Add optional README file selection to the plugin README API and frontend client.

Bug Fixes:

  • Fall back to the default README.md when a requested README variant is unavailable.

Enhancements:

  • Restrict selectable README files to safe README*.md paths and preserve remote marketplace link behavior.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:webui The bug / feature is about webui(dashboard) of astrbot. feature:plugin The bug / feature is about AstrBot plugin system. labels Aug 24, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 4 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="dashboard/src/views/extension/PluginDetailPage.vue" line_range="590" />
<code_context>
+    }
+    // README 语言切换链接(相对路径 README*.md):预览内本地切换,不跳转
+    const fileName = href.split("/").pop() || "";
+    if (/^README[^/]*\.md$/i.test(fileName)) {
+      link.addEventListener("click", (e) => {
+        e.preventDefault();
+        void switchReadmeFile(fileName);
+      });
     }
</code_context>
<issue_to_address>
**issue (broader_impact):** README language links in market-sourced remote READMEs are intercepted and prevented from navigating, then `switchReadmeFile` returns immediately because `isMarketDetail` is true, so those links become non-functional instead of retaining the previous GitHub/remote behavior.

**Triggers:** When a market plugin README contains a relative link to another README*.md file.

**Suggested fix:** Only install the local-switch click handler when `!isMarketDetail.value`, or allow the handler to preserve the remote link behavior for market details.

```suggestion
    if (!isMarketDetail.value && /^README[^/]*\.md$/i.test(fileName)) {
```
</issue_to_address>

### Comment 2
<location path="astrbot/dashboard/services/plugin_service.py" line_range="1940-1945" />
<code_context>
+            or not (file.lower().startswith("readme") and file.lower().endswith(".md"))
+        ):
+            raise PluginServiceError("非法的 README 文件名")
+        readme_path = plugin_dir / file

+        if not readme_path.is_file():
+            # 指定语言文件不存在时回退默认 README.md,行为与旧版一致
+            readme_path = plugin_dir / "README.md"
</code_context>
<issue_to_address>
**🚨 issue (security):** The filename checks reject textual path traversal but do not reject symlinks: `readme_path.is_file()` and `read_text()` follow a README*.md symlink outside the plugin directory, allowing the endpoint to return arbitrary readable files targeted by a plugin-provided symlink.

**Triggers:** When an installed plugin directory contains a symlink named README*.md pointing outside that directory.

**Suggested fix:** Resolve the candidate path and require it to remain under the resolved plugin directory, or reject symlinks before reading.

```suggestion
        plugin_dir = plugin_dir.resolve()
        readme_path = (plugin_dir / file).resolve()

        if plugin_dir not in readme_path.parents:
            raise PluginServiceError("非法的 README 文件名")
        if not readme_path.is_file():
            # 指定语言文件不存在时回退默认 README.md,行为与旧版一致
            readme_path = (plugin_dir / "README.md").resolve()
        if plugin_dir not in readme_path.parents:
            raise PluginServiceError("非法的 README 文件名")
        if not readme_path.is_file():
```
</issue_to_address>

### Comment 3
<location path="dashboard/src/views/extension/PluginDetailPage.vue" line_range="728-738" />
<code_context>
+  readmeError.value = "";
+  readmeEmpty.value = false;
+  try {
+    const res = await pluginApi.readme(plugin.name, fileName);
+    if (res.data.status !== "ok") {
+      readmeError.value = res.data.message || tm("messages.operationFailed");
+      return;
+    }
+    const content = res.data.data?.content || "";
+    if (!content) {
+      readmeError.value = `${fileName} 内容为空`;
+      return;
+    }
+    readmeFile.value = fileName;
+    renderedReadme.value = renderMarkdown(content);
+  } catch (err) {
</code_context>
<issue_to_address>
**issue (bug_risk):** Concurrent README clicks can complete out of order, allowing an older response to overwrite `renderedReadme` and `readmeFile` after a newer selection has already completed, so the displayed language does not necessarily match the last link clicked.

**Triggers:** When the user clicks two different README language links before the first request finishes.

**Suggested fix:** Track a request sequence or selected filename and discard responses that are no longer current.
</issue_to_address>

### Comment 4
<location path="astrbot/dashboard/api/plugins.py" line_range="820-821" />
<code_context>
     service: PluginService = Depends(get_service),
 ):
     return await _run_service(
-        lambda: service.get_plugin_readme(plugin_id),
+        lambda: service.get_plugin_readme(plugin_id, file or "README.md"),
         log_label="/api/plugin/readme",
     )
</code_context>
<issue_to_address>
**issue (bug_risk):** The existing dashboard API test monkeypatches `get_plugin_readme` with a one-argument callable, but this route now always calls it with two positional arguments even when no `file` query parameter is supplied, causing that test request to raise `TypeError` and return the generic service error response.

**Triggers:** When the existing `test_fastapi_v1_dashboard.py` endpoint test runs with its current one-argument monkeypatch.

**Suggested fix:** Update the test double to accept the new optional argument, or have the route call the legacy one-argument form when `file` is absent.
</issue_to_address>

Sourcery assessment

Approval pending. 4 findings to address first.

Blocking findings: dashboard/src/views/extension/PluginDetailPage.vue:590, astrbot/dashboard/services/plugin_service.py:1945, dashboard/src/views/extension/PluginDetailPage.vue:738, astrbot/dashboard/api/plugins.py:821


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 dashboard/src/views/extension/PluginDetailPage.vue Outdated
Comment thread astrbot/dashboard/services/plugin_service.py Outdated
Comment thread dashboard/src/views/extension/PluginDetailPage.vue
Comment thread astrbot/dashboard/api/plugins.py
@yuluo-feather
yuluo-feather force-pushed the feat/plugin-readme-lang branch from 317a2fc to e3caca0 Compare August 24, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:webui The bug / feature is about webui(dashboard) of astrbot. feature:plugin The bug / feature is about AstrBot plugin system. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 插件文档预览支持语言切换(README 语言链接点击不跳 GitHub)

1 participant