-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: add documentation and changelog viewing features #7252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9b28ed0
7997411
d5e1e6d
3625d65
bdacdf6
d655d3a
7e2d0c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||
| from datetime import datetime, timezone | ||||||||||||||||
| from pathlib import Path | ||||||||||||||||
| from urllib.parse import urlparse | ||||||||||||||||
|
|
||||||||||||||||
| import aiohttp | ||||||||||||||||
| import certifi | ||||||||||||||||
|
|
@@ -737,8 +738,26 @@ async def on_plugin(self): | |||||||||||||||
|
|
||||||||||||||||
| async def get_plugin_readme(self): | ||||||||||||||||
| plugin_name = request.args.get("name") | ||||||||||||||||
| logger.debug(f"正在获取插件 {plugin_name} 的README文件内容") | ||||||||||||||||
| repo_url = request.args.get("repo") | ||||||||||||||||
| 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__ | ||||||||||||||||
|
Comment on lines
+756
to
+758
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,
Suggested change
|
||||||||||||||||
|
|
||||||||||||||||
| # 否则从本地获取 | ||||||||||||||||
| if not plugin_name: | ||||||||||||||||
| logger.warning("插件名称为空") | ||||||||||||||||
| return Response().error("插件名称不能为空").__dict__ | ||||||||||||||||
|
|
@@ -791,6 +810,53 @@ async def get_plugin_readme(self): | |||||||||||||||
| logger.error(f"/api/plugin/readme: {traceback.format_exc()}") | ||||||||||||||||
| return Response().error(f"读取README文件失败: {e!s}").__dict__ | ||||||||||||||||
|
|
||||||||||||||||
| 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 | ||||||||||||||||
| repo_url = repo_url.rstrip("/").removesuffix(".git") | ||||||||||||||||
|
|
||||||||||||||||
| # 使用 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] | ||||||||||||||||
|
|
||||||||||||||||
| # 尝试多种README文件名 | ||||||||||||||||
| readme_names = ["README.md", "readme.md", "README.MD", "Readme.md"] | ||||||||||||||||
|
|
||||||||||||||||
| ssl_context = ssl.create_default_context(cafile=certifi.where()) | ||||||||||||||||
| connector = aiohttp.TCPConnector(ssl=ssl_context) | ||||||||||||||||
|
|
||||||||||||||||
| async with aiohttp.ClientSession( | ||||||||||||||||
| trust_env=True, connector=connector, timeout=aiohttp.ClientTimeout(total=10) | ||||||||||||||||
| ) as session: | ||||||||||||||||
| # 尝试从不同分支获取 | ||||||||||||||||
| branches = ["main", "master"] | ||||||||||||||||
| for branch in branches: | ||||||||||||||||
| for readme_name in readme_names: | ||||||||||||||||
| # 使用GitHub raw content URL | ||||||||||||||||
| raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{readme_name}" | ||||||||||||||||
| try: | ||||||||||||||||
| async with session.get(raw_url) as response: | ||||||||||||||||
| if response.status == 200: | ||||||||||||||||
| content = await response.text() | ||||||||||||||||
| logger.debug(f"成功从 {raw_url} 获取README") | ||||||||||||||||
| return content | ||||||||||||||||
| except Exception as e: | ||||||||||||||||
| logger.debug(f"从 {raw_url} 获取失败: {e}") | ||||||||||||||||
| continue | ||||||||||||||||
|
|
||||||||||||||||
| return None | ||||||||||||||||
|
|
||||||||||||||||
| async def get_plugin_changelog(self): | ||||||||||||||||
| """获取插件更新日志 | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ const props = defineProps({ | |
| }, | ||
| }); | ||
|
|
||
| const emit = defineEmits(["install"]); | ||
| const emit = defineEmits(["install", "viewReadme"]); | ||
|
|
||
| const normalizePlatformList = (platforms) => { | ||
| if (!Array.isArray(platforms)) return []; | ||
|
Comment on lines
+23
to
26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): The emitted event name In Vue 3, custom event names are case-sensitive and are not auto-converted between camelCase and kebab-case. Here you emit Use a single naming style for the event in both places, e.g.:
|
||
|
|
@@ -35,6 +35,30 @@ const handleInstall = (plugin) => { | |
| emit("install", plugin); | ||
| }; | ||
|
|
||
| const handleViewReadme = (plugin) => { | ||
| emit("viewReadme", plugin); | ||
| }; | ||
|
|
||
| // 从 repo URL 提取作者主页链接 | ||
| const authorHomepageUrl = computed(() => { | ||
| const repoUrl = props.plugin?.repo; | ||
| if (!repoUrl) return null; | ||
|
|
||
| try { | ||
| // 解析 GitHub URL,提取 owner | ||
| const url = new URL(repoUrl); | ||
| if (url.hostname.toLowerCase() !== 'github.com') return null; | ||
|
|
||
| const pathParts = url.pathname.split('/').filter(p => p); | ||
| if (pathParts.length < 1) return null; | ||
|
|
||
| const owner = pathParts[0]; | ||
| return `https://github.com/${owner}`; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }); | ||
|
|
||
| </script> | ||
|
|
||
| <template> | ||
|
|
@@ -98,6 +122,22 @@ const handleInstall = (plugin) => { | |
| > | ||
| {{ plugin.author }} | ||
| </a> | ||
| <a | ||
| v-else-if="authorHomepageUrl" | ||
| :href="authorHomepageUrl" | ||
| target="_blank" | ||
| @click.stop | ||
| class="text-subtitle-2 font-weight-medium" | ||
| style=" | ||
| text-decoration: none; | ||
| color: rgb(var(--v-theme-primary)); | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| " | ||
| > | ||
| {{ plugin.author }} | ||
| </a> | ||
| <span | ||
| v-else | ||
| class="text-subtitle-2 font-weight-medium" | ||
|
|
@@ -198,6 +238,18 @@ const handleInstall = (plugin) => { | |
| </v-list> | ||
| </v-menu> | ||
| <v-spacer></v-spacer> | ||
| <v-btn | ||
| v-if="plugin?.repo" | ||
| color="info" | ||
| size="small" | ||
| variant="tonal" | ||
| class="market-action-btn" | ||
| @click="handleViewReadme(plugin)" | ||
| style="height: 32px" | ||
| > | ||
| <v-icon icon="mdi-file-document-outline" start size="small"></v-icon> | ||
| {{ tm("buttons.viewDocs") }} | ||
| </v-btn> | ||
| <v-btn | ||
| v-if="plugin?.repo" | ||
| color="secondary" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if readme_content:在 README 文件存在但内容为空(空字符串)时会评估为False,从而导致返回错误响应。建议使用if readme_content is not None:来准确判断是否成功获取到了内容,以支持空 README 文件的显示。