Skip to content

feat: add documentation and changelog viewing features #7252

Open
VanillaNahida wants to merge 7 commits intoAstrBotDevs:masterfrom
VanillaNahida:feat/add-plugin-readme-buttom
Open

feat: add documentation and changelog viewing features #7252
VanillaNahida wants to merge 7 commits intoAstrBotDevs:masterfrom
VanillaNahida:feat/add-plugin-readme-buttom

Conversation

@VanillaNahida
Copy link
Copy Markdown
Contributor

@VanillaNahida VanillaNahida commented Apr 1, 2026

What's change:

  1. 插件市场支持直接查看插件README文档
  2. 支持直接查看已安装插件的更新日志而不是更新后才能看
  3. 添加作者主页超链跳转

Modifications / 改动点

 M astrbot/dashboard/routes/plugin.py
 M dashboard/src/components/extension/MarketPluginCard.vue
 M dashboard/src/components/shared/ExtensionCard.vue
 M dashboard/src/components/shared/ReadmeDialog.vue
 M dashboard/src/i18n/locales/en-US/features/extension.json
 M dashboard/src/i18n/locales/ru-RU/features/extension.json
 M dashboard/src/i18n/locales/zh-CN/features/extension.json
 M dashboard/src/views/extension/InstalledPluginsTab.vue
 M dashboard/src/views/extension/MarketPluginsTab.vue
  1. 修改了plugin.py, 添加了获取远程README文档的函数,可以实现在未安装插件的时候直接在WebUI内查看插件文档
  2. WebUI新增了对应的组件
  3. i18n 对应字段更新
  4. 添加作者主页超链跳转,点击作者名字即可跳转到GitHub主页
  • This is NOT a breaking change. / 这不是一个破坏性变更。

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

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.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Add support for viewing plugin documentation from the marketplace and changelogs for installed plugins directly in the dashboard.

New Features:

  • Allow fetching plugin README content from remote repositories when available, falling back to local files.
  • Expose a new dashboard action to view plugin documentation for marketplace plugins before installation.
  • Add menu actions to view changelogs for installed plugins without requiring an update first.

Enhancements:

  • Extend the shared README dialog to accept repository URLs and load content accordingly.
  • Update extension-related localization strings to cover the new documentation and changelog actions.

@auto-assign auto-assign bot requested review from Soulter and anka-afk April 1, 2026 01:18
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Apr 1, 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:

  • In _fetch_remote_readme, consider validating that repo_url actually points to a GitHub repository (e.g., checking the host) before constructing raw.githubusercontent.com URLs, or explicitly handling/early-returning for non-GitHub URLs to avoid confusing failures.
  • The _fetch_remote_readme HTTP calls currently have no timeout and create a new aiohttp.ClientSession per invocation; adding a reasonable timeout and reusing a session/connector would help avoid hanging requests and reduce overhead under load.
  • When trying to resolve the README in _fetch_remote_readme, you currently hardcode main and master branches; you might want to fall back to the repository's default branch via the GitHub API or make the branch configurable to better handle repos using other default branch names.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_fetch_remote_readme`, consider validating that `repo_url` actually points to a GitHub repository (e.g., checking the host) before constructing `raw.githubusercontent.com` URLs, or explicitly handling/early-returning for non-GitHub URLs to avoid confusing failures.
- The `_fetch_remote_readme` HTTP calls currently have no timeout and create a new `aiohttp.ClientSession` per invocation; adding a reasonable timeout and reusing a session/connector would help avoid hanging requests and reduce overhead under load.
- When trying to resolve the README in `_fetch_remote_readme`, you currently hardcode `main` and `master` branches; you might want to fall back to the repository's default branch via the GitHub API or make the branch configurable to better handle repos using other default branch names.

## Individual Comments

### Comment 1
<location path="dashboard/src/components/extension/MarketPluginCard.vue" line_range="23-26" />
<code_context>
 });

-const emit = defineEmits(["install"]);
+const emit = defineEmits(["install", "viewReadme"]);

 const normalizePlatformList = (platforms) => {
</code_context>
<issue_to_address>
**issue (bug_risk):** The emitted event name `viewReadme` does not match the parent listener `@view-readme`, which will prevent the handler from firing.

In Vue 3, custom event names are case-sensitive and are not auto-converted between camelCase and kebab-case. Here you emit `"viewReadme"`, but the parent listens with `@view-readme`, so the handler will never run.

Use a single naming style for the event in both places, e.g.:
- Kebab-case: `defineEmits(["view-readme"])` and `emit("view-readme", plugin)`
- CamelCase: keep `emit("viewReadme", plugin)` and change the parent to `@viewReadme="viewReadme"`.
</issue_to_address>

### Comment 2
<location path="astrbot/dashboard/routes/plugin.py" line_range="755-757" />
<code_context>
+        logger.debug(f"正在获取插件 {plugin_name} 的README文件内容, repo: {repo_url}")

+        # 如果提供了 repo_url,优先从远程获取
+        if repo_url:
+            try:
+                readme_content = await self._fetch_remote_readme(repo_url)
+                if readme_content:
+                    return (
+                        Response()
+                        .ok({"content": readme_content}, "成功获取README内容")
+                        .__dict__
+                    )
+                else:
+                    return Response().error("无法从远程仓库获取README文件").__dict__
+            except Exception as e:
+                logger.error(f"从远程获取README失败: {traceback.format_exc()}")
+                return Response().error(f"获取README失败: {e!s}").__dict__
+
+        # 否则从本地获取
</code_context>
<issue_to_address>
**🚨 suggestion (security):** The API response exposes raw exception messages to clients, which can leak internal details.

In the remote README error path, `Response().error(f"获取README失败: {e!s}")` returns the raw exception message to clients. Instead, keep detailed info (including `e` and stack traces) in logs only, and return a generic, static error message and/or error code in the API response.

```suggestion
            except Exception as e:
                logger.error(f"从远程获取README失败: {traceback.format_exc()}")
                # 不将具体异常信息暴露给客户端,仅返回通用错误提示
                return Response().error("获取README失败,请稍后重试").__dict__
```
</issue_to_address>

### Comment 3
<location path="astrbot/dashboard/routes/plugin.py" line_range="818-824" />
<code_context>
+        # 支持格式: https://github.com/owner/repo 或 https://github.com/owner/repo.git
+        repo_url = repo_url.rstrip("/").replace(".git", "")
+
+        # 提取 owner 和 repo
+        parts = repo_url.split("/")
+        if len(parts) < 2:
+            return None
+
+        owner = parts[-2]
+        repo = parts[-1]
+
+        # 尝试多种README文件名
</code_context>
<issue_to_address>
**suggestion (bug_risk):** GitHub URL parsing is very permissive and may derive incorrect owner/repo for non-standard URLs.

The logic currently takes `parts[-2]`/`parts[-1]` for `owner`/`repo`, so it will also treat URLs like `https://example.com/foo/bar` or `https://github.com/owner` as valid, leading to incorrect `owner`/`repo` and 404s against `raw.githubusercontent.com`.

It would be safer to:
- Check that the hostname is `github.com` (or another explicitly supported host), and
- Require at least two path segments after the domain before extracting `owner` and `repo`.

If validation fails, consider returning `None` (or a specific error) so callers can clearly distinguish an invalid repo URL from a missing README.

Suggested implementation:

```python
    async def _fetch_remote_readme(self, repo_url: str) -> str | None:
        """从远程GitHub仓库获取README内容"""
        # 解析GitHub仓库URL
        # 支持格式: https://github.com/owner/repo 或 https://github.com/owner/repo.git
        # 先去掉末尾的斜杠和 .git 后缀,避免影响解析
        repo_url = repo_url.rstrip("/")
        if repo_url.endswith(".git"):
            repo_url = repo_url[:-4]

        # 使用 urlparse 严格解析 URL,校验域名和路径
        parsed = urlparse(repo_url)

        # 仅支持 GitHub 仓库链接
        if parsed.netloc.lower() != "github.com":
            return None

        # 提取路径中的 owner 和 repo,要求至少有两个段
        path_parts = [part for part in parsed.path.strip("/").split("/") if part]
        if len(path_parts) < 2:
            return None

        owner, repo = path_parts[0], path_parts[1]

```

To make this compile, ensure that `urlparse` is imported at the top of `astrbot/dashboard/routes/plugin.py`, for example:

<<<<<<< SEARCH
import ssl
=======
import ssl
from urllib.parse import urlparse
>>>>>>> REPLACE

Adjust the exact SEARCH block for the imports to match your existing import section if it differs.
</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 on lines +23 to 26
const emit = defineEmits(["install", "viewReadme"]);

const normalizePlatformList = (platforms) => {
if (!Array.isArray(platforms)) return [];
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.

issue (bug_risk): The emitted event name viewReadme does not match the parent listener @view-readme, which will prevent the handler from firing.

In Vue 3, custom event names are case-sensitive and are not auto-converted between camelCase and kebab-case. Here you emit "viewReadme", but the parent listens with @view-readme, so the handler will never run.

Use a single naming style for the event in both places, e.g.:

  • Kebab-case: defineEmits(["view-readme"]) and emit("view-readme", plugin)
  • CamelCase: keep emit("viewReadme", plugin) and change the parent to @viewReadme="viewReadme".

Comment on lines +755 to +757
except Exception as e:
logger.error(f"从远程获取README失败: {traceback.format_exc()}")
return Response().error(f"获取README失败: {e!s}").__dict__
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.

🚨 suggestion (security): The API response exposes raw exception messages to clients, which can leak internal details.

In the remote README error path, Response().error(f"获取README失败: {e!s}") returns the raw exception message to clients. Instead, keep detailed info (including e and stack traces) in logs only, and return a generic, static error message and/or error code in the API response.

Suggested change
except Exception as e:
logger.error(f"从远程获取README失败: {traceback.format_exc()}")
return Response().error(f"获取README失败: {e!s}").__dict__
except Exception as e:
logger.error(f"从远程获取README失败: {traceback.format_exc()}")
# 不将具体异常信息暴露给客户端,仅返回通用错误提示
return Response().error("获取README失败,请稍后重试").__dict__

@dosubot dosubot bot added area:webui The bug / feature is about webui(dashboard) of astrbot. feature:plugin The bug / feature is about AstrBot plugin system. labels Apr 1, 2026
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 implements functionality to fetch and display plugin README files from remote GitHub repositories. Key changes include a new backend method for retrieving remote content and frontend updates to provide 'View Documentation' and 'View Changelog' actions. Feedback focuses on improving the robustness of the repository URL parsing and ensuring that empty README files are handled correctly by checking for non-null content rather than truthiness.

"""从远程GitHub仓库获取README内容"""
# 解析GitHub仓库URL
# 支持格式: https://github.com/owner/repo 或 https://github.com/owner/repo.git
repo_url = repo_url.rstrip("/").replace(".git", "")
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.

high

使用 .replace(".git", "") 会替换 URL 中所有出现的 .git 字符串,这在仓库名本身包含 .git 时(例如 git-tools)会导致解析出的仓库名错误。建议使用 .removesuffix(".git"),它仅移除末尾的后缀,且在 Python 3.9+ 中可用。

Suggested change
repo_url = repo_url.rstrip("/").replace(".git", "")
repo_url = repo_url.rstrip("/").removesuffix(".git")

if repo_url:
try:
readme_content = await self._fetch_remote_readme(repo_url)
if readme_content:
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.

medium

if readme_content: 在 README 文件存在但内容为空(空字符串)时会评估为 False,从而导致返回错误响应。建议使用 if readme_content is not None: 来准确判断是否成功获取到了内容,以支持空 README 文件的显示。

Suggested change
if readme_content:
if readme_content is not None:

@VanillaNahida
Copy link
Copy Markdown
Contributor Author

@anka-afk @Soulter

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:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant