From dec91950bcc9b7e7feb9dc69f9569de6a5fd5e34 Mon Sep 17 00:00:00 2001 From: zhx Date: Mon, 31 Mar 2025 13:23:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=89=E8=A3=85=E5=AE=8C=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=90=8E=E8=87=AA=E5=8A=A8=E5=BC=B9=E5=87=BA=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E4=BB=93=E5=BA=93=20README=20=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/star/star_manager.py | 57 ++++- astrbot/dashboard/routes/plugin.py | 8 +- dashboard/package.json | 3 +- dashboard/src/views/ExtensionMarketplace.vue | 255 ++++++++++++++++++- 4 files changed, 310 insertions(+), 13 deletions(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 13d1768ef..75a18317c 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -451,7 +451,34 @@ class PluginManager: # reload the plugin dir_name = os.path.basename(plugin_path) await self.load(specified_dir_name=dir_name) - return plugin_path + + # Get the plugin metadata to return repo info + plugin = self.context.get_registered_star(dir_name) + if not plugin: + # Try to find by other name if directory name doesn't match plugin name + for star in self.context.get_all_stars(): + if star.root_dir_name == dir_name: + plugin = star + break + + # Extract README.md content if exists + readme_content = None + readme_path = os.path.join(plugin_path, "README.md") + if not os.path.exists(readme_path): + readme_path = os.path.join(plugin_path, "readme.md") + + if os.path.exists(readme_path): + try: + with open(readme_path, "r", encoding="utf-8") as f: + readme_content = f.read() + except Exception as e: + logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}") + + plugin_info = None + if plugin: + plugin_info = {"repo": plugin.repo, "readme": readme_content} + + return plugin_info async def uninstall_plugin(self, plugin_name: str): plugin = self.context.get_registered_star(plugin_name) @@ -607,3 +634,31 @@ class PluginManager: logger.warning(f"删除插件压缩包失败: {str(e)}") # await self.reload() await self.load(specified_dir_name=dir_name) + + # Get the plugin metadata to return repo info + plugin = self.context.get_registered_star(dir_name) + if not plugin: + # Try to find by other name if directory name doesn't match plugin name + for star in self.context.get_all_stars(): + if star.root_dir_name == dir_name: + plugin = star + break + + # Extract README.md content if exists + readme_content = None + readme_path = os.path.join(desti_dir, "README.md") + if not os.path.exists(readme_path): + readme_path = os.path.join(desti_dir, "readme.md") + + if os.path.exists(readme_path): + try: + with open(readme_path, "r", encoding="utf-8") as f: + readme_content = f.read() + except Exception as e: + logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}") + + plugin_info = None + if plugin: + plugin_info = {"repo": plugin.repo, "readme": readme_content} + + return plugin_info diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 3ffa1c557..e369ad054 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -211,10 +211,10 @@ class PluginRoute(Route): try: logger.info(f"正在安装插件 {repo_url}") - await self.plugin_manager.install_plugin(repo_url, proxy) + plugin_info = await self.plugin_manager.install_plugin(repo_url, proxy) # self.core_lifecycle.restart() logger.info(f"安装插件 {repo_url} 成功。") - return Response().ok(None, "安装成功。").__dict__ + return Response().ok(plugin_info, "安装成功。").__dict__ except Exception as e: logger.error(traceback.format_exc()) return Response().error(str(e)).__dict__ @@ -233,10 +233,10 @@ class PluginRoute(Route): logger.info(f"正在安装用户上传的插件 {file.filename}") file_path = f"data/temp/{file.filename}" await file.save(file_path) - await self.plugin_manager.install_plugin_from_file(file_path) + plugin_info = await self.plugin_manager.install_plugin_from_file(file_path) # self.core_lifecycle.restart() logger.info(f"安装插件 {file.filename} 成功") - return Response().ok(None, "安装成功。").__dict__ + return Response().ok(plugin_info, "安装成功。").__dict__ except Exception as e: logger.error(traceback.format_exc()) return Response().error(str(e)).__dict__ diff --git a/dashboard/package.json b/dashboard/package.json index 5d76b72d5..a7edd4935 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -21,9 +21,10 @@ "axios-mock-adapter": "^1.22.0", "chance": "1.1.11", "date-fns": "2.30.0", + "highlight.js": "^11.11.1", "js-md5": "^0.8.3", "lodash": "4.17.21", - "marked": "^15.0.6", + "marked": "^15.0.7", "pinia": "2.1.6", "remixicon": "3.5.0", "vee-validate": "4.11.3", diff --git a/dashboard/src/views/ExtensionMarketplace.vue b/dashboard/src/views/ExtensionMarketplace.vue index 3dfaea0d7..9e8587a6f 100644 --- a/dashboard/src/views/ExtensionMarketplace.vue +++ b/dashboard/src/views/ExtensionMarketplace.vue @@ -5,6 +5,9 @@ import AstrBotConfig from '@/components/shared/AstrBotConfig.vue'; import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue'; import axios from 'axios'; import { useCommonStore } from '@/stores/common'; +import { marked } from 'marked'; +import hljs from 'highlight.js'; +import 'highlight.js/styles/github.css'; @@ -209,6 +251,12 @@ export default { statusCode: 0, // 0: loading, 1: success, 2: error, result: "" }, + readmeDialog: { + show: false, + url: null, + content: null, + error: null + }, announcement: "", isListView: true, @@ -327,7 +375,50 @@ export default { }); }, - newExtension() { + async getReadmeUrl(repoUrl) { + // 去掉 repoUrl 末尾的斜杠 + repoUrl = repoUrl.replace(/\/+$/, ''); + + const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/); + if (!match) { + throw new Error("无效的 GitHub 仓库地址"); + } + + const owner = match[1]; + const repo = match[2]; + + const apiUrl = `https://api.github.com/repos/${owner}/${repo}`; + + try { + const res = await fetch(apiUrl); + const data = await res.json(); + + const branch = data?.default_branch || 'master'; + return `${repoUrl}/blob/${branch}/README.md`; + } catch (error) { + console.error("获取默认分支失败,使用 master 作为默认:", error); + return `${repoUrl}/blob/master/README.md`; + } + }, + + async showReadmeDialog(res) { + this.readmeDialog.content = null; + this.readmeDialog.error = null; + if (res?.data?.data?.repo) { + this.readmeDialog.url = await this.getReadmeUrl(res.data.data.repo); + if (res.data.data.readme) { + this.readmeDialog.content = res.data.data.readme; + } else { + this.readmeDialog.error = "插件未提供README文档"; + } + } else { + this.readmeDialog.url = null; + this.readmeDialog.error = "插件没有仓库信息或README文档"; + } + this.readmeDialog.show = true; + }, + + async newExtension() { if (this.extension_url === "" && this.upload_file === null) { this.toast("请填写插件链接或上传插件文件", "error"); return; @@ -347,7 +438,7 @@ export default { headers: { 'Content-Type': 'multipart/form-data' } - }).then((res) => { + }).then(async (res) => { this.loading_ = false; if (res.data.status === "error") { this.onLoadingDialogResult(2, res.data.message, -1); @@ -358,7 +449,8 @@ export default { this.onLoadingDialogResult(1, res.data.message); this.dialog = false; this.getExtensions(); - // this.$refs.wfr.check(); + + await this.showReadmeDialog(res); }).catch((err) => { this.loading_ = false; this.onLoadingDialogResult(2, err, -1); @@ -370,7 +462,7 @@ export default { { url: this.extension_url, proxy: localStorage.getItem('selectedGitHubProxy') || "" - }).then((res) => { + }).then(async (res) => { this.loading_ = false; this.toast(res.data.message, res.data.status === "ok" ? "success" : "error"); if (res.data.status === "error") { @@ -382,7 +474,7 @@ export default { this.onLoadingDialogResult(1, res.data.message); this.dialog = false; this.getExtensions(); - // this.$refs.wfr.check(); + await this.showReadmeDialog(res); }).catch((err) => { this.loading_ = false; this.toast("安装插件失败: " + err, "error"); @@ -412,8 +504,157 @@ export default { } } this.pluginMarketData = notInstalled.concat(installed); - } + }, + openReadmeInNewTab() { + if (this.readmeDialog.url) { + window.open(this.readmeDialog.url, '_blank'); + } + }, + renderMarkdown(content) { + if (!content) return ''; + // Configure marked with highlight.js for syntax highlighting + marked.setOptions({ + highlight: function(code, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(code, { language: lang }).value; + } catch (e) { + console.error(e); + } + } + return hljs.highlightAuto(code).value; + }, + gfm: true, // GitHub Flavored Markdown + breaks: true, // Convert \n to
+ headerIds: true, // Add id attributes to headers + mangle: false // Don't mangle email addresses + }); + return marked(content); + }, }, } - \ No newline at end of file + + + \ No newline at end of file