From daa65fe8778736ac246f8adb2fec7007812ab0fe Mon Sep 17 00:00:00 2001 From: boluofan <40459801+boluofan@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:00:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BD=91=E7=BB=9C=E6=AD=8C=E5=8D=95?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=8A=9F=E8=83=BD=E6=9B=B4=E6=96=B0=20(#690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 增加【插件订阅源】配置及管理 2. 修复【WYY】插件无法使用bug 3. 将开放接口与插件源融合展示,方便用户指定搜索源 --- .gitignore | 1 + package-lock.json | 10 + package.json | 1 + xiaomusic/api/routers/plugin.py | 48 +- xiaomusic/js_plugin_manager.py | 203 ++++++- xiaomusic/js_plugin_runner.js | 3 +- xiaomusic/online_music.py | 161 +++++- xiaomusic/static/onlineSearch/config.js | 2 +- xiaomusic/static/onlineSearch/favicon.ico | Bin 0 -> 16958 bytes xiaomusic/static/onlineSearch/index.html | 7 +- xiaomusic/static/onlineSearch/setting.html | 591 ++++++++++++++------- 11 files changed, 797 insertions(+), 230 deletions(-) create mode 100644 xiaomusic/static/onlineSearch/favicon.ico diff --git a/.gitignore b/.gitignore index 11d2edc..efb1376 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,4 @@ xiaomusic.log.txt* node_modules reference/ .aone_copilot/ +.idea/ diff --git a/package-lock.json b/package-lock.json index 2b77c17..f9d8e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "axios": "^1.6.0", + "big-integer": "^1.6.52", "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "dayjs": "^1.11.10", @@ -33,6 +34,15 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", diff --git a/package.json b/package.json index c7284d7..26f800f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "xiaomusic/js_plugin_runner.js", "dependencies": { "axios": "^1.6.0", + "big-integer": "^1.6.52", "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "dayjs": "^1.11.10", diff --git a/xiaomusic/api/routers/plugin.py b/xiaomusic/api/routers/plugin.py index 859c6e9..0a62075 100644 --- a/xiaomusic/api/routers/plugin.py +++ b/xiaomusic/api/routers/plugin.py @@ -116,11 +116,19 @@ async def upload_js_plugin( plugin_dir = xiaomusic.js_plugin_manager.plugins_dir os.makedirs(plugin_dir, exist_ok=True) + # 校验命名是否是保留字段【ALL/all/OpenAPI】,是的话抛错 + sys_files = ["ALL.js", "all.js", "OpenAPI.js", "OPENAPI.js"] + if file.filename in sys_files: + raise HTTPException( + status_code=409, + detail=f"插件名非法,不能命名为: {sys_files} ,请修改后再上传!", + ) file_path = os.path.join(plugin_dir, file.filename) # 校验是否已存在同名js插件 存在则提示,停止上传 if os.path.exists(file_path): raise HTTPException( - status_code=409, detail=f"插件 {file.filename} 已存在,请重命名后再上传" + status_code=409, + detail=f"插件 {file.filename} 已存在,请重命名后再上传!", ) file_path = os.path.join(plugin_dir, file.filename) @@ -142,6 +150,9 @@ async def upload_js_plugin( return {"success": False, "error": str(e)} +# ----------------------------开放接口相关函数--------------------------------------- + + @router.get("/api/openapi/load") def get_openapi_info(Verifcation=Depends(verification)): """获取开放接口配置信息""" @@ -172,3 +183,38 @@ async def update_openapi_url(request: Request, Verifcation=Depends(verification) return xiaomusic.js_plugin_manager.update_openapi_url(search_url) except Exception as e: return {"success": False, "error": str(e)} + + +# ----------------------------插件源接口--------------------------------------- + + +@router.get("/api/plugin-source/load") +def get_plugin_source_info(Verifcation=Depends(verification)): + """获取插件源配置信息""" + try: + plugin_source = xiaomusic.js_plugin_manager.get_plugin_source() + return {"success": True, "data": plugin_source} + except Exception as e: + return {"success": False, "error": str(e)} + + +@router.post("/api/plugin-source/refresh") +def refresh_plugin_source(Verifcation=Depends(verification)): + """更新订阅源""" + try: + return xiaomusic.js_plugin_manager.refresh_plugin_source() + except Exception as e: + return {"success": False, "error": str(e)} + + +@router.post("/api/plugin-source/updateUrl") +async def update_plugin_source(request: Request, Verifcation=Depends(verification)): + """更新插件源地址""" + try: + request_json = await request.json() + source_url = request_json.get("source_url") + if not request_json or "source_url" not in request_json: + return {"success": False, "error": "Missing 'search_url' in request body"} + return xiaomusic.js_plugin_manager.update_plugin_source_url(source_url) + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/xiaomusic/js_plugin_manager.py b/xiaomusic/js_plugin_manager.py index e639064..c184470 100644 --- a/xiaomusic/js_plugin_manager.py +++ b/xiaomusic/js_plugin_manager.py @@ -286,6 +286,182 @@ class JSPluginManager: self.log.error(f"Failed to update OpenAPI config: {e}") return {"success": False, "error": str(e)} + def get_plugin_source(self) -> dict[str, Any]: + """获取插件源配置信息 + Returns: + Dict[str, Any]: 包含 OpenAPI 配置信息的字典,包括启用状态和搜索 URL + """ + try: + # 读取配置文件中的 OpenAPI 配置信息 + config_data = self._get_config_data() + if config_data: + # 返回 openapi_info 配置项 + return config_data.get("plugin_source", {}) + else: + return {"enabled": False} + except Exception as e: + self.log.error(f"Failed to read plugin source info from config: {e}") + return {} + + def refresh_plugin_source(self) -> dict[str, Any]: + """更新订阅源""" + try: + if os.path.exists(self.plugins_config_path): + with open(self.plugins_config_path, encoding="utf-8") as f: + config_data = json.load(f) + plugin_source = config_data.get("plugin_source", {}) + source_url = plugin_source.get("source_url", "") + if source_url: + import requests + + # 请求源地址 + response = requests.get(source_url, timeout=30) + response.raise_for_status() # 抛出HTTP错误 + # 解析响应JSON + json_data = response.json() + # 校验响应格式 - 检查是否包含 plugins 数组 + if not isinstance(json_data, dict) or "plugins" not in json_data: + return {"success": False, "error": "无效订阅源!"} + plugins_array = json_data["plugins"] + if not isinstance(plugins_array, list): + return {"success": False, "error": "无效订阅源!"} + # 写入插件文本 + self.download_and_save_plugin(plugins_array) + # 使缓存失效 + self._invalidate_config_cache() + self.reload_plugins() + return {"success": True} + else: + return {"success": False, "error": "未找到配置订阅源!"} + else: + return {"success": False} + except Exception as e: + self.log.error(f"Failed to toggle OpenAPI config: {e}") + return {"success": False, "error": str(e)} + + def download_and_save_plugin(self, plugins_array: list) -> bool: + """下载并保存插件数组中的所有插件 + + Args: + plugins_array: 插件信息列表,格式如 [{"name": "plugin_name", "url": "plugin_url", "version": "version"}, ...] + + Returns: + bool: 所有插件下载保存是否全部成功 + """ + if not plugins_array or not isinstance(plugins_array, list): + self.log.warning("Empty or invalid plugins array provided") + return False + + all_success = True + + for plugin_info in plugins_array: + if ( + not isinstance(plugin_info, dict) + or "name" not in plugin_info + or "url" not in plugin_info + ): + self.log.warning(f"Invalid plugin entry: {plugin_info}") + all_success = False + continue + + plugin_name = plugin_info["name"] + plugin_url = plugin_info["url"] + + if not plugin_name or not plugin_url: + self.log.warning(f"Invalid plugin entry: {plugin_name} -> {plugin_url}") + all_success = False + continue + + # 调用单个插件下载方法 + success = self.download_single_plugin(plugin_name, plugin_url) + if not success: + all_success = False + self.log.error(f"Failed to download plugin: {plugin_name}") + + return all_success + + def download_single_plugin(self, plugin_name: str, plugin_url: str) -> bool: + """下载并保存单个插件 + + Args: + plugin_name: 插件名称 + plugin_url: 插件下载URL + + Returns: + bool: 下载保存是否成功 + """ + import requests + + # 检查插件名称是否合法 + sys_files = ["ALL", "all", "OpenAPI", "OPENAPI"] + if plugin_name in sys_files: + self.log.error(f"Plugin name {plugin_name} is reserved and cannot be used") + return False + + # 创建插件目录 + os.makedirs(self.plugins_dir, exist_ok=True) + + # 生成文件路径 + plugin_filename = f"{plugin_name}.js" + file_path = os.path.join(self.plugins_dir, plugin_filename) + + # 检查是否已存在同名插件 + if os.path.exists(file_path): + self.log.warning(f"Plugin {plugin_name} already exists, will overwrite") + + try: + # 下载插件内容 + response = requests.get(plugin_url, timeout=30) + response.raise_for_status() + + # 验证下载的内容是否为有效的JS代码(简单检查是否以有意义的JS字符开头) + content = response.text.strip() + if not content: + self.log.error(f"Downloaded plugin {plugin_name} has empty content") + return False + + # 保存插件文件 + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + + self.log.info(f"Successfully downloaded and saved plugin: {plugin_name}") + + # 更新插件配置 + self.update_plugin_config(plugin_name, plugin_filename) + return True + + except requests.exceptions.RequestException as e: + self.log.error( + f"Failed to download plugin {plugin_name} from {plugin_url}: {e}" + ) + return False + except Exception as e: + self.log.error(f"Failed to save plugin {plugin_name}: {e}") + return False + + def update_plugin_source_url(self, source_url: str) -> dict[str, Any]: + """更新开放接口地址""" + try: + if os.path.exists(self.plugins_config_path): + with open(self.plugins_config_path, encoding="utf-8") as f: + config_data = json.load(f) + + plugin_source = config_data.get("plugin_source", {}) + plugin_source["source_url"] = source_url + config_data["plugin_source"] = plugin_source + + with open(self.plugins_config_path, "w", encoding="utf-8") as f: + json.dump(config_data, f, ensure_ascii=False, indent=2) + + # 使缓存失效 + self._invalidate_config_cache() + return {"success": True} + else: + return {"success": False} + except Exception as e: + self.log.error(f"Failed to update plugin source config: {e}") + return {"success": False, "error": str(e)} + """----------------------------------------------------------------------""" def _get_config_data(self): @@ -332,9 +508,12 @@ class JSPluginManager: base_config = { "account": "", "password": "", + "auto_add_song": True, + "aiapi_info": {"enabled": False, "api_key": ""}, "enabled_plugins": [], - "plugins_info": [], "openapi_info": {"enabled": False, "search_url": ""}, + "plugin_source": {"source_url": ""}, + "plugins_info": [], } with open(self.plugins_config_path, "w", encoding="utf-8") as f: json.dump(base_config, f, ensure_ascii=False, indent=2) @@ -423,6 +602,7 @@ class JSPluginManager: """刷新插件列表,强制重新加载配置数据""" # 强制使缓存失效,重新加载配置 self._invalidate_config_cache() + self.reload_plugins() # 返回最新的插件列表 return self.get_plugin_list() @@ -464,7 +644,13 @@ class JSPluginManager: # 读取配置文件中的启用插件列表 config_data = self._get_config_data() if config_data: - return config_data.get("enabled_plugins", []) + enabled_plugins = config_data.get("enabled_plugins", []) + # 追加开放接口名称 + openapi_info = config_data.get("openapi_info", {}) + enabled_openapi = openapi_info.get("enabled", False) + if enabled_openapi and "OpenAPI" not in enabled_plugins: + enabled_plugins.insert(0, "OpenAPI") + return enabled_plugins else: return [] except Exception as e: @@ -554,7 +740,8 @@ class JSPluginManager: # 构造请求参数 params = {"type": "aggregateSearch", "keyword": keyword, "limit": limit} # 使用aiohttp发起异步HTTP GET请求 - async with aiohttp.ClientSession() as session: + connector = aiohttp.TCPConnector(ssl=False) # 跳过 SSL 验证 + async with aiohttp.ClientSession(connector=connector) as session: async with session.get( url, params=params, timeout=aiohttp.ClientTimeout(total=10) ) as response: @@ -667,7 +854,6 @@ class JSPluginManager: # 获取待处理的数据列表 data_list = result_data["data"] - self.log.info(f"列表信息::{data_list}") # 预计算平台权重,启用插件列表中的前9个插件有权重,排名越靠前权重越高 enabled_plugins = self.get_enabled_plugins() plugin_weights = {p: 9 - i for i, p in enumerate(enabled_plugins[:9])} @@ -708,8 +894,11 @@ class JSPluginManager: artist_score = 800 elif ar in artist: artist_score = 600 - - platform_bonus = plugin_weights.get(platform, 0) + # 开放接口的平台权重最高 20 + if platform.startswith("OpenAPI-"): + platform_bonus = 20 + else: + platform_bonus = plugin_weights.get(platform, 0) return title_score + artist_score + platform_bonus sorted_data = sorted(data_list, key=calculate_match_score, reverse=True) @@ -1120,7 +1309,7 @@ class JSPluginManager: self.plugins.clear() # 重新加载插件 self._load_plugins() - self.log.info("Plugins reloaded successfully") + self.log.info(f"最新插件信息:{self.plugins}") def update_plugin_config(self, plugin_name: str, plugin_file: str): """更新插件配置文件""" diff --git a/xiaomusic/js_plugin_runner.js b/xiaomusic/js_plugin_runner.js index 53e9f8b..3b266f3 100644 --- a/xiaomusic/js_plugin_runner.js +++ b/xiaomusic/js_plugin_runner.js @@ -195,7 +195,8 @@ class PluginRunner { 'he': require('he'), 'dayjs': require('dayjs'), 'cheerio': require('cheerio'), - 'qs': require('qs') + 'qs': require('qs'), + 'big-integer': require('big-integer') }; const safeRequire = (moduleName) => { diff --git a/xiaomusic/online_music.py b/xiaomusic/online_music.py index 4ccfdf5..b1a09f5 100644 --- a/xiaomusic/online_music.py +++ b/xiaomusic/online_music.py @@ -78,14 +78,85 @@ class OnlineMusicService: if not self.js_plugin_manager: return {"success": False, "error": "JS Plugin Manager not available"} - # 初始化 artist 变量 - artist = "" - # 解析关键词,可能通过AI或直接分割 - parsed_keyword, parsed_artist = await self._parse_keyword_with_ai(keyword) - keyword = parsed_keyword or keyword - artist = parsed_artist or artist + # 解析关键词和艺术家 + keyword, artist = await self._parse_keyword_and_artist(keyword) + # 获取API配置信息 openapi_info = self.js_plugin_manager.get_openapi_info() + + if plugin == "all": + # 并发执行插件搜索和OpenAPI搜索 + return await self._execute_concurrent_searches( + keyword, artist, page, limit, openapi_info + ) + elif plugin == "OpenAPI": + # OpenAPI搜索 + return await self._execute_openapi_search(openapi_info, keyword, artist) + else: + # 插件在线搜索 + return await self._execute_plugin_search( + plugin, keyword, artist, page, limit + ) + + async def _parse_keyword_and_artist(self, keyword): + """解析关键词和艺术家""" + parsed_keyword, parsed_artist = await self._parse_keyword_with_ai(keyword) + keyword = parsed_keyword or keyword + artist = parsed_artist or "" + return keyword, artist + + async def _execute_concurrent_searches( + self, keyword, artist, page, limit, openapi_info + ): + """执行并发搜索 - 插件和OpenAPI""" + tasks = [] + + # 插件在线搜索任务 + plugin_task = asyncio.create_task( + self.get_music_list_mf( + "all", keyword=keyword, artist=artist, page=page, limit=limit + ) + ) + tasks.append(plugin_task) + + # OpenAPI搜索任务(只有在配置正确时才创建) + if ( + openapi_info.get("enabled", False) + and openapi_info.get("search_url", "") != "" + ): + openapi_task = asyncio.create_task( + self.js_plugin_manager.openapi_search( + url=openapi_info.get("search_url"), keyword=keyword, artist=artist + ) + ) + tasks.append(openapi_task) + + # 并发执行任务 + results = await asyncio.gather(*tasks, return_exceptions=True) + + plugin_result = results[0] + openapi_result = results[1] if len(results) > 1 else None + + # 处理异常情况 + plugin_result = self._handle_search_exception(plugin_result, "插件") + openapi_result = self._handle_search_exception(openapi_result, "OpenAPI") + + # 合并结果 + combined_result = self._merge_search_results( + plugin_result, openapi_result, keyword, artist, limit + ) + combined_result["artist"] = artist or "佚名" + return combined_result + + def _handle_search_exception(self, result, source_name): + """处理搜索异常""" + if result and isinstance(result, Exception): + self.log.error(f"{source_name}搜索发生异常: {result}") + return {"success": False, "error": str(result)} + return result + + async def _execute_openapi_search(self, openapi_info, keyword, artist): + """执行OpenAPI搜索""" if ( openapi_info.get("enabled", False) and openapi_info.get("search_url", "") != "" @@ -94,17 +165,71 @@ class OnlineMusicService: result_data = await self.js_plugin_manager.openapi_search( url=openapi_info.get("search_url"), keyword=keyword, artist=artist ) - result_data["isOpenAPI"] = True else: - # 插件在线搜索 - result_data = await self.get_music_list_mf( - plugin, keyword=keyword, artist=artist, page=page, limit=limit - ) - result_data["isOpenAPI"] = False - # 将歌手名当作附加值,用于歌手搜索 + return {"success": False, "error": "OpenAPI未启用或配置错误"} + result_data["artist"] = artist or "佚名" return result_data + async def _execute_plugin_search(self, plugin, keyword, artist, page, limit): + """执行插件搜索""" + result_data = await self.get_music_list_mf( + plugin, keyword=keyword, artist=artist, page=page, limit=limit + ) + result_data["artist"] = artist or "佚名" + return result_data + + def _merge_search_results( + self, plugin_result, openapi_result, keyword, artist, limit + ): + merged_data = [] + sources = {} + + # 先处理 OpenAPI 结果 + if openapi_result and openapi_result.get("success"): + openapi_data = openapi_result.get("data", []) + if openapi_data: + for item in openapi_data: + item["source"] = "openapi" + merged_data.extend(openapi_data) + if "sources" in openapi_result: + sources.update(openapi_result["sources"]) + + # 再处理插件结果 + if plugin_result and plugin_result.get("success"): + plugin_data = plugin_result.get("data", []) + if plugin_data: + for item in plugin_data: + item["source"] = "plugin" + merged_data.extend(plugin_data) + sources.update(plugin_result.get("sources", {})) + + # 如果都没有成功结果,返回错误 + if not plugin_result.get("success") and not ( + openapi_result and openapi_result.get("success") + ): + # 优先返回第一个错误 + error_result = ( + plugin_result if not plugin_result.get("success") else openapi_result + ) + return error_result + + # 优化合并后的结果 + optimized_result = self.js_plugin_manager.optimize_search_results( + {"data": merged_data}, + search_keyword=keyword, + limit=limit, + search_artist=artist, + ) + + return { + "success": True, + "data": optimized_result.get("data", []), + "total": len(optimized_result.get("data", [])), + "sources": sources, + "merged": True, # 标识这是合并结果 + } + async def get_music_list_mf( self, plugin="all", keyword="", artist="", page=1, limit=20, **kwargs ): @@ -216,7 +341,8 @@ class OnlineMusicService: song_name = result.get("name", "") artist = result.get("artist", "") # 构建新的关键词 - keyword = _build_keyword(song_name, artist) + # keyword = _build_keyword(song_name, artist) + keyword = song_name self.log.info(f"AI提取到的信息: {result}") return keyword, artist @@ -725,9 +851,12 @@ class OnlineMusicService: return {"success": False, "error": str(e)} @staticmethod - async def get_real_url_of_openapi(url: str, timeout: int = 10) -> str: + async def _make_request_with_validation( + url: str, timeout: int, convert_m4s: bool = False + ) -> str: """ - 通过服务端代理获取开放接口真实的音乐播放URL,避免CORS问题 + 通用的URL请求和验证方法 + Args: url (str): 原始音乐URL timeout (int): 请求超时时间(秒) diff --git a/xiaomusic/static/onlineSearch/config.js b/xiaomusic/static/onlineSearch/config.js index 43f4328..a7f88be 100644 --- a/xiaomusic/static/onlineSearch/config.js +++ b/xiaomusic/static/onlineSearch/config.js @@ -1,6 +1,6 @@ // config.js window.appConfig = { // TODO 版本号 - version: "1.0.4", + version: "1.0.5", // 其他配置项可继续添加 }; diff --git a/xiaomusic/static/onlineSearch/favicon.ico b/xiaomusic/static/onlineSearch/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b6346d0110b0ea18cdbbf620bf254c42b864d2b0 GIT binary patch literal 16958 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 + 在线音乐搜索 @@ -853,7 +854,6 @@ let currentLyrics = []; // 存储解析后的歌词 let lyricLines = []; // 存储歌词DOM元素 let songList = []; // 存储搜索到的歌曲列表 - let isOpenAPI = false; // 是否为开放接口 // 页面加载时获取插件列表 window.onload = function() { @@ -981,7 +981,6 @@ const response = await fetch(`/api/search/online?keyword=${encodeURIComponent(keyword)}&plugin=${plugin}`); const data = await response.json(); if (data.success) { - isOpenAPI = data.isOpenAPI; displayResults(data.data); } else { resultsDiv.innerHTML = `
搜索失败: ${data.error}
`; @@ -1117,7 +1116,7 @@ async function webPlayMusic(mediaItem) { try { let playUrl; - if (mediaItem && isOpenAPI) { + if (mediaItem && mediaItem.isOpenAPI) { // playUrl = await sniffRealMusicUrl(mediaItem.url); playUrl = mediaItem.url; } else { @@ -1248,7 +1247,7 @@ } try { let lrcText - if (mediaItem && isOpenAPI) {//在线接口 + if (mediaItem && mediaItem.isOpenAPI) {//在线接口 // 调用OpenApi接口 GET获取歌词 const response = await fetch(mediaItem.lrc, { method: 'GET', diff --git a/xiaomusic/static/onlineSearch/setting.html b/xiaomusic/static/onlineSearch/setting.html index 4de7d85..d6d3590 100644 --- a/xiaomusic/static/onlineSearch/setting.html +++ b/xiaomusic/static/onlineSearch/setting.html @@ -2,26 +2,220 @@ + 在线搜索配置 @@ -334,8 +389,23 @@ - -
插件配置
+ +
插件源配置
+
+
+
+
+
插件源地址:
+
+
+
+ + + +
+
+
+
加载中...
@@ -355,10 +425,117 @@ versionSpan.textContent = `v${window.appConfig.version}`; } } - //加载插件 + //加载数据 loadPlugins(); loadOpenApiConfig(); + loadPluginSource(); }); + + /*============================插件源相关函数=================================*/ + // 加载 OpenAPI 配置 + async function loadPluginSource() { + const container = document.getElementById('plugin-source'); + try { + const response = await fetch('/api/plugin-source/load'); + const data = await response.json(); + if (data.success) { + displayPluginSource(data.data); + } else { + container.innerHTML = `
加载失败: ${data.error}
`; + } + } catch (error) { + container.innerHTML = `
加载出错: ${error.message}
`; + } + } + + // 显示 OpenAPI 配置 + function displayPluginSource(config) { + document.getElementById('source-url').textContent = config.source_url || ''; + const editButtonElement = document.getElementById('edit-source-btn'); + if (config.source_url && config.source_url.length > 0) { + editButtonElement.style.display = 'inline-block'; + } + } + + + // 刷新订阅源 + async function refreshPluginSource() { + try { + const urlElement = document.getElementById('source-url'); + const currentUrl = urlElement.textContent; + if (!currentUrl) { + alert('请先设置接口地址!'); + return; + } + if (!confirm(`确定要刷新订阅源吗?相同名称插件将被覆盖!`)) { + return; + } + const response = await fetch('/api/plugin-source/refresh', { + method: 'POST' + }); + const data = await response.json(); + + if (data.success) { + // 操作成功,重新加载插件列表 + await loadPlugins(); + } else { + alert(`切换失败: ${data.error}`); + } + } catch (error) { + alert(`操作出错: ${error.message}`); + } + } + + // 编辑插件订阅源地址 + function editPluginSource() { + const urlElement = document.getElementById('source-url'); + const currentUrl = urlElement.textContent; + const newUrl = prompt('请输入新的插件源地址:', currentUrl); + + // 检查用户是否点击了取消 + if (newUrl === null) { + // 用户点击了取消,不执行任何操作 + return; + } + + // 检查用户是否输入了空字符串 + if (newUrl.trim() === '') { + alert('插件源地址不能为空!'); + return; + } + + // 校验URL格式 + const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/; + if (urlPattern.test(newUrl)) { + // 更新地址 + updatePluginSource(newUrl); + } else { + alert('请输入有效的插件源地址!'); + } + } + + // 更新OpenAPI地址 + async function updatePluginSource(newUrl) { + try { + const response = await fetch('/api/plugin-source/updateUrl', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ source_url: newUrl }) + }); + const data = await response.json(); + + if (data.success) { + // 更新成功,重新加载插件列表 + await loadPluginSource(); + } else { + alert(`更新失败: ${data.error}`); + } + } catch (error) { + alert(`更新出错: ${error.message}`); + } + } /*============================开放接口函数=================================*/ // 加载 OpenAPI 配置 async function loadOpenApiConfig() { @@ -432,16 +609,30 @@ const urlElement = document.getElementById('openapi-url'); const currentUrl = urlElement.textContent; const newUrl = prompt('请输入新的接口地址:', currentUrl); - // 校验newUrl格式 + + // 检查用户是否点击了取消 + if (newUrl === null) { + // 用户点击了取消,不执行任何操作 + return; + } + + // 检查用户是否输入了空字符串 + if (newUrl.trim() === '') { + alert('接口地址不能为空!'); + return; + } + + // 校验URL格式 const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/; - if (newUrl && urlPattern.test(newUrl)) { + if (urlPattern.test(newUrl)) { // 更新接口地址 updateOpenApiUrl(newUrl); - }else { + } else { alert('请输入有效的接口地址!'); } } + // 更新OpenAPI地址 async function updateOpenApiUrl(newUrl) { try {