1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2026-03-15 08:13:16 +08:00

feat: 网络歌单插件功能更新 (#690)

1. 增加【插件订阅源】配置及管理 
2. 修复【WYY】插件无法使用bug
3. 将开放接口与插件源融合展示,方便用户指定搜索源
This commit is contained in:
boluofan
2026-01-16 13:00:14 +08:00
committed by GitHub
parent 84dd9aa882
commit daa65fe877
11 changed files with 797 additions and 230 deletions

1
.gitignore vendored
View File

@@ -173,3 +173,4 @@ xiaomusic.log.txt*
node_modules
reference/
.aone_copilot/
.idea/

10
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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)}

View File

@@ -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):
"""更新插件配置文件"""

View File

@@ -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) => {

View File

@@ -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): 请求超时时间(秒)

View File

@@ -1,6 +1,6 @@
// config.js
window.appConfig = {
// TODO 版本号
version: "1.0.4",
version: "1.0.5",
// 其他配置项可继续添加
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -2,6 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="./favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
<title>在线音乐搜索</title>
<!-- ... 公共配置 ... -->
@@ -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 = `<div class="error">搜索失败: ${data.error}</div>`;
@@ -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',

View File

@@ -2,26 +2,220 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="./favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线搜索配置</title>
<!-- ... 公共配置 ... -->
<script src="./config.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
-webkit-overflow-scrolling: touch;
overscroll-behavior: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
-webkit-overflow-scrolling: touch;
overscroll-behavior: none;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.version-info {
position: absolute;
top: 10px;
left: 20px;
color: white;
font-size: 14px;
background: rgba(255, 255, 255, 0.2);
padding: 4px 8px;
border-radius: 4px;
z-index: 10;
}
/* 调整 header 内容的 padding避免与版本信息重叠 */
.header {
background: #31c27c;
color: white;
padding: 20px 20px 20px 60px; /* 左边增加留白 */
text-align: center;
position: relative;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
text-align: center;
padding: 40px;
color: #d32f2f;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
}
/* 插件设置专用样式 */
.plugins-section {
padding: 20px;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-top: 15px;
margin-bottom: 15px;
color: #333;
padding-bottom: 10px;
}
.border-bottom {
margin-top: 15px;
margin-bottom: 15px;
border-bottom: 2px solid #e0e0e0;
/*加粗*/
padding-bottom: 10px;
}
.plugin-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.plugin-item {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.plugin-info {
flex: 1;
}
.plugin-name {
font-weight: bold;
font-size: 16px;
margin-bottom: 5px;
}
.plugin-status {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-top: 5px; /* 添加上边距 */
}
.status-enabled {
background: #e8f5e9;
color: #4caf50;
}
.status-disabled {
background: #ffebee;
color: #f44336;
}
.plugin-details {
font-size: 13px;
color: #666;
}
.plugin-actions {
display: flex;
gap: 10px;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
}
.enable-btn {
background: #4caf50;
color: white;
}
.enable-btn:hover {
background: #45a049;
}
.disable-btn {
background: #f44336;
color: white;
}
.disable-btn:hover {
background: #da190b;
}
.uninstall-btn {
background: gray;
color: white;
}
.uninstall-btn:hover {
background: dimgray;
}
.refresh-btn {
display: block;
margin: 20px auto;
padding: 10px 20px;
background: #31c27c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover {
background: #28a869;
}
/* 顶部按钮的样式 */
.header-buttons {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 15px;
flex-wrap: wrap;
width: 100%; /* 确保容器占满可用宽度 */
}
.header-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #fff;
color: #31c27c;
border: 1px solid #31c27c;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
transition: all 0.2s ease;
}
.header-btn:hover {
background: #f0f0f0;
}
.material-icons {
font-size: 20px;
}
.edit-btn {
background: #2196f3;
color: white;
}
.edit-btn:hover {
background: #1976d2;
}
/* 移动端适配 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
max-width: 100%;
margin: 0;
border-radius: 0;
min-height: 100vh;
}
.version-info {
position: absolute;
@@ -48,167 +242,6 @@
text-align: center;
padding: 40px;
color: #666;
}
.error {
text-align: center;
padding: 40px;
color: #d32f2f;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
}
/* 插件设置专用样式 */
.plugins-section {
padding: 20px;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-top: 15px;
margin-bottom: 15px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.plugin-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.plugin-item {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.plugin-info {
flex: 1;
}
.plugin-name {
font-weight: bold;
font-size: 16px;
margin-bottom: 5px;
}
.plugin-status {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-top: 5px; /* 添加上边距 */
}
.status-enabled {
background: #e8f5e9;
color: #4caf50;
}
.status-disabled {
background: #ffebee;
color: #f44336;
}
.plugin-details {
font-size: 13px;
color: #666;
}
.plugin-actions {
display: flex;
gap: 10px;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
}
.enable-btn {
background: #4caf50;
color: white;
}
.enable-btn:hover {
background: #45a049;
}
.disable-btn {
background: #f44336;
color: white;
}
.disable-btn:hover {
background: #da190b;
}
.uninstall-btn {
background: gray;
color: white;
}
.uninstall-btn:hover {
background: dimgray;
}
.refresh-btn {
display: block;
margin: 20px auto;
padding: 10px 20px;
background: #31c27c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover {
background: #28a869;
}
/* 顶部按钮的样式 */
.header-buttons {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 15px;
flex-wrap: wrap;
width: 100%; /* 确保容器占满可用宽度 */
}
.header-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #fff;
color: #31c27c;
border: 1px solid #31c27c;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
transition: all 0.2s ease;
}
.header-btn:hover {
background: #f0f0f0;
}
.material-icons {
font-size: 20px;
}
.edit-btn {
background: #2196f3;
color: white;
}
.edit-btn:hover {
background: #1976d2;
}
/* 移动端适配 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
max-width: 100%;
margin: 0;
border-radius: 0;
min-height: 100vh;
}
.header {
padding: 15px 10px;
@@ -254,30 +287,52 @@
}
}
}
/* 小屏设备进一步优化 */
@media (max-width: 480px) {
.header-buttons {
flex-wrap: wrap;
gap: 6px;
}
.header-btn {
flex: 0 0 calc(50% - 3px); /* 每行2个按钮 */
min-width: 90px;
max-width: 120px;
padding: 8px 10px;
font-size: 11px;
}
.plugin-item {
flex-direction: column;
align-items: stretch;
padding: 12px;
}
/* 防止动画影响性能 */
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
animation: none !important;
}
.plugin-info {
margin-bottom: 10px;
}
.plugin-actions {
flex-direction: row;
justify-content: flex-end;
gap: 8px;
}
.action-btn {
padding: 6px 10px;
font-size: 12px;
min-width: auto;
}
}
/* 小屏设备进一步优化 */
@media (max-width: 480px) {
.header-buttons {
flex-wrap: wrap;
gap: 6px;
}
.header-btn {
flex: 0 0 calc(50% - 3px); /* 每行2个按钮 */
min-width: 90px;
max-width: 120px;
padding: 8px 10px;
font-size: 11px;
}
}
/* 防止动画影响性能 */
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
animation: none !important;
}
}
</style>
</head>
<body>
@@ -334,8 +389,23 @@
</div>
</div>
<!-- 插件配置 内容 -->
<div class="section-title">插件配置</div>
<!-- 插件配置 -->
<div class="section-title">插件配置</div>
<div id="plugin-source">
<div class="plugin-item">
<div class="plugin-info">
<div class="plugin-details">
<div><strong>插件源地址: </strong> <span id="source-url"></span></div>
</div>
</div>
<div class="plugin-actions">
<button class="action-btn enable-btn" id="refresh-source-btn" onclick="refreshPluginSource()">更新订阅</button>
<!-- 新增编辑按钮 -->
<button class="action-btn edit-btn" id="edit-source-btn" onclick="editPluginSource()" style="display: inline-block;">编辑</button>
</div>
</div>
</div>
<div class="border-bottom"></div>
<div id="plugins-container">
<div class="loading">加载中...</div>
</div>
@@ -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 = `<div class="error">加载失败: ${data.error}</div>`;
}
} catch (error) {
container.innerHTML = `<div class="error">加载出错: ${error.message}</div>`;
}
}
// 显示 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 {