feature: 新增UI支持 (#105)
* 增加环境变量处理类 * 完善node-handler.js * 完善node-handler.js * 完善vercel-handler.js * 增加netlify-handler * 修改netlify-handler.js * 完善netlify-handler.js * 增加cloudflare-handler.js * 完善cloudflare-handler.js * 完善cloudflare-handler.js * 新增edgeone-handler.js * edgeone和vercel增加部署函数 * 增加部署平台环境变量配置指南 * 优化小点 * 增加UI界面,非真实数据,main.js待后续拆分 * 增加当前版本和最新版本通过后端接口获取 * 修复云端部署的问题 * 前端使用分离文件模块 * 前端使用分离文件模块 * 在html和script上使用globals全局变量信息 * 使用 Language Injection 注释,支持语法高亮、补全、格式化、跳转 * 清理ui无用文件 * 优化缓存清理和重新部署确认弹窗UI * 修改调试api * 使用正式调试api返回数据 * 完善接口调试实际接口数据 * 日志查看调用真实数据 * 修改成系统配置 * 合并冲突 * 配置预览使用真实数据 * 提取log接口函数到system-api * ui上增加API端点展示 * 系统配置里的环境变量使用真实数据 * 删除添加配置按钮,优化变量值输入框 * 系统设置环境变量从原始环境变量获取 * 增删改环境变量对接真实数据 * 修复ui请求报401的问题 * 优化deployPlatform * 数字类型的环境变量增加最小值和最大值限制 * 重新部署对接真实接口 * 修复云服务部署 * 日志中隐藏敏感字符 * 清理缓存打通真实数据接口 * 将main.js中的css提取到css文件中 * 增加ADMIN_TOKEN环境变量 * 完善admin_token访问权限控制 * 增加NODE_TLS_REJECT_UNAUTHORIZED,用于控制在建立 HTTPS 连接时是否验证服务器的 SSL/TLS 证书 * 所有alert都替换成自定义样式弹窗 * 增加部署平台的检查,如果没有配置部署相关的环境变量,也提示需要配置 * 没有token的情况下访问不了日志查看和接口调试 * 增加推送弹幕模块 * 修复推送弹幕 * 推送弹幕界面当点击一个anime获取到剧集列表后直接跳转到剧集列表处 * 弹幕推送ui适配手机样式 * 推送弹幕中的按钮修改为点击后加载转圈 * 推送校验地址挪到推送按钮 * 所有确认弹窗都改成适配ui样式 * 优化API端点显示 * 修复如果是默认token则用admin_token访问不了的问题 * 抽出css到css文件 * 修改handler-factory.js,将各个import的语句挪到switch相对应的case里,延迟引入 * 测试下 * 再测试下 * 尝试解决cloudflare和edgeone部署失败 * 再次尝试解决cloudflare和edgeone部署失败 * 再次尝试解决cloudflare和edgeone部署失败+1 * 再次尝试解决cloudflare和edgeone部署失败+2 * 再次尝试解决cloudflare和edgeone部署失败+3 * 再次尝试解决cloudflare和edgeone部署失败+4 * 修复弹幕推送跨域问题 * 修改标题等小细节 * 修复一些小细节 * 添加ui界面的readme * 将handlers下的README挪到UI目录下的README * 修改readme * readme增加目录指引 * 刷新目录结构 * main.js中的init里的非函数抽出函数 * 拆分main.js,单独拆出preview.js * 抽出日志查看模块 * 提取出接口调试部分 * 提取出弹幕推送模块 * 抽出系统配置模块js * 更新项目结构 * 配置预览优化显示 * 系统配置变量值背景色修改 * 修改右上角的五个按钮布局 * 修复token非默认值的情况下api端点直接显示了token的问题 * 修复在非默认值TOKEN的情况下访问url api端点显示了token的问题 * 修改README * 增加日志打印deployPlatform * 调试vercel参数 * 修复vercel平台部署被判断为cloudfalre的问题 * 修复/api/config获取在没有权限的情况下不应该返回私密信息的问题 * 修复api/config接口的权限问题 * 修复日志查看、接口调试和推送弹幕模块默认token场景下没有token也能访问 * 优化currentToken获取 * main.js中部分代码挪到其他js * 测试vercel部署方式判断 * 修复部署平台参数的问题 * 修复部署平台的几个参数问题 * 修复没有环境变量的情况下设置失败的问题 * 挪动修改环境变量代码 * 修改环境变量描述 * 修改变量的值和描述的字体颜色 * 修改cloudflare才是需要DEPLOY_PLATFROM_ACCOUNT的平台 * 添加环境变量修改后说明 * 修改重新部署时长 * 修改版本号
This commit is contained in:
1 parent
55da0c489e
commit
995d5f10e0
31 files changed
+5490
-121
No files matched your search
@@ -21,6 +21,11 @@
|
||||
# 默认值:87654321
|
||||
TOKEN=87654321
|
||||
|
||||
# 系统管理访问令牌(用于系统管理功能)
|
||||
# 如果未配置此值,则无法访问系统管理功能,需要先配置后在URL中填入此token才能打开系统管理
|
||||
# 默认值:空(不启用系统管理访问控制)
|
||||
# ADMIN_TOKEN=
|
||||
|
||||
# 其他服务器地址(第三方弹幕服务器)
|
||||
# 默认值:https://api.danmu.icu
|
||||
OTHER_SERVER=https://api.danmu.icu
|
||||
@@ -244,3 +249,20 @@ MAX_LAST_SELECT_MAP=100
|
||||
# 默认值:空
|
||||
# UPSTASH_REDIS_REST_TOKEN=
|
||||
|
||||
# ==================== 部署平台API接口信息 配置 ====================
|
||||
|
||||
# 部署账号ID,调用部署服务API需要,配置后可使用UI界面配置服务
|
||||
# 默认值:空
|
||||
#DEPLOY_PLATFROM_ACCOUNT=
|
||||
|
||||
# 部署项目名称,调用部署服务API需要,配置后可使用UI界面配置服务
|
||||
# 默认值:空
|
||||
#DEPLOY_PLATFROM_PROJECT=
|
||||
|
||||
# 部署平台token,调用部署服务API需要,配置后可使用UI界面配置服务
|
||||
# 默认值:空
|
||||
#DEPLOY_PLATFROM_TOKEN=
|
||||
|
||||
# 在建立 HTTPS 连接时是否验证服务器的 SSL/TLS 证书,0表示忽略
|
||||
# 默认值:1
|
||||
# NODE_TLS_REJECT_UNAUTHORIZED=1
|
||||
@@ -27,6 +27,24 @@ LogVar 弹幕 API 服务器
|
||||
|
||||
> 请不要在国内媒体平台宣传本项目!
|
||||
|
||||
# 目录
|
||||
|
||||
- [功能](#功能)
|
||||
- [前置条件](#前置条件)
|
||||
- [本地运行](#本地运行)
|
||||
- [使用 Docker 运行](#使用-docker-运行)
|
||||
- [Docker 一键启动 【推荐】](#docker-一键启动-推荐)
|
||||
- [部署到 Vercel 【推荐】](#部署到-vercel-推荐)
|
||||
- [部署到 Netlify 【推荐】](#部署到-netlify-推荐)
|
||||
- [部署到 腾讯云 edgeone pages](#部署到-腾讯云-edgeone-pages)
|
||||
- [部署到 Cloudflare](#部署到-cloudflare)
|
||||
- [API食用指南](#api食用指南)
|
||||
- [环境变量列表](#环境变量列表)
|
||||
- [采集源及对应平台列表](#采集源及对应平台列表)
|
||||
- [项目结构](#项目结构)
|
||||
- [注意事项](#注意事项)
|
||||
- [贡献者](#贡献者)
|
||||
|
||||
## 功能
|
||||
- **API 接口**:
|
||||
- `GET /api/v2/search/anime?keyword=${queryTitle}`:根据关键字搜索动漫。
|
||||
@@ -57,6 +75,12 @@ LogVar 弹幕 API 服务器
|
||||
- 转换弹幕颜色为白色或彩色(`CONVERT_COLOR`)
|
||||
- 解决部分播放器不支持顶部/底部弹幕和彩色弹幕的问题
|
||||
- **弹幕限制数量**:支持通过环境变量配置等间隔采样弹幕数量。
|
||||
- **UI界面-后台配置管理系统**:支持通过UI执行一些操作(详细见 [UI 系统使用说明](https://github.com/huangxd-/danmu_api/tree/main/danmu_api/ui/README.md) ),包括:
|
||||
- 配置预览
|
||||
- 日志查看
|
||||
- 接口调试
|
||||
- 推送弹幕
|
||||
- 系统管理
|
||||
|
||||
## 前置条件
|
||||
- Node.js(v18.0.0 或更高版本;理论兼容更低版本,请自行测试)
|
||||
@@ -268,12 +292,12 @@ LogVar 弹幕 API 服务器
|
||||
> cf部署可能不稳定,推荐用vercel/netlify部署。
|
||||
|
||||
## API食用指南
|
||||
支持 forward/senplayer/hills/小幻/yamby/eplayerx/afusekt/uz影视/dscloud/lenna/danmaku-anywhere 等支持弹幕API的播放器。
|
||||
支持 forward/senplayer/hills/小幻/yamby/eplayerx/afusekt/uz影视/dscloud/lenna/danmaku-anywhere/omnibox 等支持弹幕API的播放器。
|
||||
|
||||
配合 dd-danmaku 扩展新增对 Emby Web 端弹幕的支持,具体使用方法参考 [PR #98](https://github.com/huangxd-/danmu_api/pull/98) 。
|
||||
|
||||
以`senplayer`为例:
|
||||
1. 获取到部署之后的API地址,如 `http://192.168.1.7:9321/87654321` ,其中`87654321`是默认token(默认为87654321的情况下也可以不带token),如果有自定义环境变量TOKEN,请替换成相应的token
|
||||
1. 获取到部署之后的API地址,如 `http://192.168.1.7:9321/87654321` ,其中`87654321`是默认token(默认为87654321的情况下也可以不带token),如果有自定义环境变量TOKEN,请替换成相应的token;API地址也可直接在UI界面上点击API端点直接复制
|
||||
2. 将API地址填入自定义弹幕API,在`设置 - 弹幕设置 - 自定义弹幕API`
|
||||
3. 播放界面点击`弹幕按钮 - 搜索弹幕`,选择你的弹幕API,会根据标题进行搜索,等待一段时间,选择剧集就行。
|
||||
<img src="https://i.mji.rip/2025/09/14/1dae193008f23e507d3cc3733a92f0a1.jpeg" style="width:400px" />
|
||||
@@ -327,6 +351,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
|
||||
| 变量名称 | 描述 |
|
||||
| ----------- | ----------- |
|
||||
| TOKEN | 【可选】自定义用户token,不填默认为`87654321` |
|
||||
| ADMIN_TOKEN | 【可选】系统管理访问令牌,如果未配置此值,则无法访问系统管理功能,需要先配置后在URL中填入此token才能打开系统管理 |
|
||||
| OTHER_SERVER | 【可选】兜底第三方弹幕服务器,不填默认为`https://api.danmu.icu`,其他可选:`https://fc.lyz05.cn`,`https://dmku.hls.one`,`https://se.678.ooo`,`https://danmu.56uxi.com`,`https://dm.lxlad.com` |
|
||||
| VOD_SERVERS | 【可选】VOD服务器列表,支持多个服务器并发查询,格式:`名称@URL,名称@URL,...`,示例:`金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top`,不填默认为`金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top` |
|
||||
| VOD_RETURN_MODE | 【可选】VOD返回模式,可选值:`all`(返回所有站点结果)、`fastest`(只返回最快的站点结果),默认为`fastest`。当配置多个VOD站点时,`all`模式会返回所有站点的结果(结果较多),`fastest`模式只返回首先响应成功的站点结果(结果较少,避免重复) |
|
||||
@@ -356,12 +381,16 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
|
||||
| MAX_LAST_SELECT_MAP | 【可选】最后选择映射缓存大小限制,默认为`100`,lastSelectMap最多保存的条目数,超过限制时删除最早的条目(FIFO),用于存储查询关键字上次选择的animeId |
|
||||
| UPSTASH_REDIS_REST_URL | 【可选】Upstash redis url,需配合UPSTASH_REDIS_REST_TOKEN使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` |
|
||||
| UPSTASH_REDIS_REST_TOKEN | 【可选】Upstash redis token,需配合UPSTASH_REDIS_REST_URL使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` |
|
||||
| DEPLOY_PLATFROM_ACCOUNT | 【可选】部署账号ID,调用部署服务API需要,配置后可使用UI界面配置服务,不同部署平台获取方式可查看 [部署平台环境变量配置指南](https://github.com/huangxd-/danmu_api/tree/main/danmu_api/ui/README.md#部署平台环境变量配置指南) ,docker部署和本地node部署并不需要配置 |
|
||||
| DEPLOY_PLATFROM_PROJECT | 【可选】部署项目名称,调用部署服务API需要,配置后可使用UI界面配置服务,不同部署平台获取方式可查看 [部署平台环境变量配置指南](https://github.com/huangxd-/danmu_api/tree/main/danmu_api/ui/README.md#部署平台环境变量配置指南) ,docker部署和本地node部署并不需要配置 |
|
||||
| DEPLOY_PLATFROM_TOKEN | 【可选】部署平台token,调用部署服务API需要,配置后可使用UI界面配置服务,不同部署平台获取方式可查看 [部署平台环境变量配置指南](https://github.com/huangxd-/danmu_api/tree/main/danmu_api/ui/README.md#部署平台环境变量配置指南) ,docker部署和本地node部署并不需要配置 |
|
||||
| NODE_TLS_REJECT_UNAUTHORIZED | 【可选】在建立 HTTPS 连接时是否验证服务器的 SSL/TLS 证书,0表示忽略,默认为1 |
|
||||
|
||||
```regex
|
||||
# EPISODE_TITLE_FILTER 默认值
|
||||
(特别|惊喜|纳凉)?企划|合伙人手记|超前(营业|vlog)?|速览|vlog|reaction|纯享|加更(版|篇)?|抢先(看|版|集|篇)?|抢鲜|预告|花絮(独家)?|特辑|彩蛋|专访|幕后(故事|花絮|独家)?|直播(陪看|回顾)?|未播(片段)?|衍生|番外|会员(专享|加长|尊享|专属|版)?|片花|精华|看点|速看|解读|影评|解说|吐槽|盘点|拍摄花絮|制作花絮|幕后花絮|未播花絮|独家花絮|花絮特辑|先导预告|终极预告|正式预告|官方预告|彩蛋片段|删减片段|未播片段|番外彩蛋|精彩片段|精彩看点|精彩回顾|精彩集锦|看点解析|看点预告|NG镜头|NG花絮|番外篇|番外特辑|制作特辑|拍摄特辑|幕后特辑|导演特辑|演员特辑|片尾曲|插曲|高光回顾|背景音乐|OST|音乐MV|歌曲MV|前季回顾|剧情回顾|往期回顾|内容总结|剧情盘点|精选合集|剪辑合集|混剪视频|独家专访|演员访谈|导演访谈|主创访谈|媒体采访|发布会采访|采访|陪看(记)?|试看版|短剧|精编|Plus|独家版|特别版|短片|发布会|解忧局|走心局|火锅局|巅峰时刻|坞里都知道|福持目标坞民|.{3,}篇|(?!.*(入局|破冰局|做局)).{2,}局|观察室|上班那点事儿|周top|赛段|直拍|REACTION|VLOG|全纪录|开播|先导|总宣|展演|集锦|旅行日记|精彩分享|剧情揭秘
|
||||
|
||||
# 如果你想新增过滤词,请自定义EPISODE_TITLE_FILTER,示例如下,每个词用'|'隔开,增加的词都会追加到默认值后面
|
||||
# 如果你想自定义过滤词,请新增EPISODE_TITLE_FILTER环境变量,示例如下,每个词用'|'隔开,也可参照默认值填写
|
||||
测试|test
|
||||
```
|
||||
|
||||
@@ -410,66 +439,91 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
|
||||
danmu_api/
|
||||
├── .github/
|
||||
│ └── workflows/
|
||||
│ ├── docker-image.yml
|
||||
│ └── sync_fork.yml # vercel自动同步配置文件
|
||||
├── danmu_api/
|
||||
│ └── apis/
|
||||
│ └── dandan-api.js # 弹弹play兼容接口函数
|
||||
│ └── configs/
|
||||
│ ├── envs.js # 环境变量处理脚本
|
||||
│ └── globals.js # 全局变量处理脚本
|
||||
│ └── models/
|
||||
│ └── dandan-model.js # 弹弹play数据模型
|
||||
│ └── sources/
|
||||
│ ├── bahamut.js # 巴哈姆特源
|
||||
│ ├── base.js # 弹幕源获取基类
|
||||
│ ├── bilibili.js # b站源
|
||||
│ ├── dandan.js # 弹弹play源
|
||||
│ ├── douban.js # 豆瓣源
|
||||
│ ├── hanjutv.js # 韩剧TV源
|
||||
│ ├── iqiyi.js # 爱奇艺源
|
||||
│ ├── kan360.js # 360看源
|
||||
│ ├── mango.js # 芒果TV源
|
||||
│ ├── other.js # 第三方弹幕服务器
|
||||
│ ├── renren.js # 人人视频源
|
||||
│ ├── tencent.js # 腾讯视频源
|
||||
│ ├── tmdb.js # TMDB源
|
||||
│ ├── vod.js # vod源
|
||||
│ └── youku.js # 优酷源
|
||||
│ └── utils/
|
||||
│ ├── cache-util.js # 缓存数据处理工具
|
||||
│ ├── codec-util.js # 编解码工具
|
||||
│ ├── common-util.js # 通用工具
|
||||
│ ├── danmu-util.js # 弹幕处理工具
|
||||
│ ├── douban-util.js # 豆瓣API请求工具
|
||||
│ ├── http-util.js # 请求工具
|
||||
│ ├── imdb-util.js # IMDB API请求工具
|
||||
│ ├── log-util.js # 日志工具
|
||||
│ ├── redis-util.js # redis工具
|
||||
│ ├── time-util.js # 时间日期工具
|
||||
│ ├── tmdb-util.js # TMDB API请求处理工具
|
||||
│ └── zh-util.js # 中文繁简转换工具
|
||||
│ ├── esm-shim.js # Node.js低版本兼容层
|
||||
│ ├── server.js # 本地node启动脚本
|
||||
│ ├── worker.js # 主 API 服务器代码
|
||||
│ └── worker.test.js # 测试文件
|
||||
├── netlify/
|
||||
│ └── functions/
|
||||
│ └── api.js # netlify 中间处理逻辑
|
||||
├── node-functions/
|
||||
│ ├── [[...path]]..js # edgeone pages 所有路由跳转指向index
|
||||
│ └── index.js # edgeone pages 中间处理逻辑
|
||||
├── .env.example # .env 配置文件示例
|
||||
├── config.yaml.example # YAML 配置文件示例(无法编辑 .env 时使用)
|
||||
│ ├── docker-image.yml # Docker镜像构建和推送工作流
|
||||
│ └── sync_fork.yml # Fork同步上游仓库工作流(vercel自动同步配置文件)
|
||||
├── .env.example # .env 配置文件示例
|
||||
├── .gitignore
|
||||
├── config.yaml.example # YAML 配置文件示例(无法编辑 .env 时使用)
|
||||
├── Dockerfile
|
||||
├── edgeone.json # edgeone pages 配置文件
|
||||
├── edgeone.json # edgeone pages 配置文件
|
||||
├── LICENSE
|
||||
├── netlify.toml # netlify 配置文件
|
||||
├── netlify.toml # netlify 配置文件
|
||||
├── package.json
|
||||
├── README.md
|
||||
├── vercel.json # vercel 配置文件
|
||||
└── wrangler.toml # cloudflare worker 配置文件
|
||||
├── vercel.json # vercel 配置文件
|
||||
├── wrangler.toml # cloudflare worker 配置文件
|
||||
├── danmu_api/
|
||||
│ ├── esm-shim.js # Node.js低版本兼容层
|
||||
│ ├── server.js # 本地node启动脚本
|
||||
│ ├── worker.js # 主 API 服务器代码
|
||||
│ ├── worker.test.js # 测试文件
|
||||
│ ├── apis/
|
||||
│ │ ├── dandan-api.js # 弹弹play兼容接口函数
|
||||
│ │ ├── env-api.js # 环境变量接口函数
|
||||
│ │ └── system-api.js # 系统管理接口函数
|
||||
│ ├── configs/
|
||||
│ │ ├── envs.js # 环境变量处理脚本
|
||||
│ │ └── globals.js # 全局变量处理脚本
|
||||
│ │ └── handlers/ # 部署平台API调用及环境变量处理类
|
||||
│ │ ├── base-handler.js
|
||||
│ │ ├── cloudflare-handler.js
|
||||
│ │ ├── edgeone-handler.js
|
||||
│ │ ├── handler-factory.js
|
||||
│ │ ├── netlify-handler.js
|
||||
│ │ ├── node-handler.js
|
||||
│ │ └── vercel-handler.js
|
||||
│ ├── models/
|
||||
│ │ └── dandan-model.js # 弹弹play数据模型
|
||||
│ ├── sources/
|
||||
│ │ ├── bahamut.js # 巴哈姆特源
|
||||
│ │ ├── base.js # 弹幕源获取基类
|
||||
│ │ ├── bilibili.js # b站源
|
||||
│ │ ├── dandan.js # 弹弹play源
|
||||
│ │ ├── douban.js # 豆瓣源
|
||||
│ │ ├── hanjutv.js # 韩剧TV源
|
||||
│ │ ├── iqiyi.js # 爱奇艺源
|
||||
│ │ ├── kan360.js # 360看源
|
||||
│ │ ├── mango.js # 芒果TV源
|
||||
│ │ ├── other.js # 第三方弹幕服务器
|
||||
│ │ ├── renren.js # 人人视频源
|
||||
│ │ ├── tencent.js # 腾讯视频源
|
||||
│ │ ├── tmdb.js # TMDB源
|
||||
│ │ ├── vod.js # vod源
|
||||
│ │ └── youku.js # 优酷源
|
||||
│ ├── ui/
|
||||
│ │ ├── README.md # UI系统使用说明
|
||||
│ │ ├── template.js # UI模板文件
|
||||
│ │ ├── css/
|
||||
│ │ │ ├── base.css.js # 基础样式
|
||||
│ │ │ ├── components.css.js # 组件样式
|
||||
│ │ │ ├── forms.css.js # 表单样式
|
||||
│ │ │ └── responsive.css.js # 响应式样式
|
||||
│ │ └── js/
|
||||
│ │ ├── apitest.js # API测试脚本
|
||||
│ │ ├── logview.js # 日志查看脚本
|
||||
│ │ ├── main.js # UI主脚本
|
||||
│ │ ├── preview.js # 预览功能脚本
|
||||
│ │ ├── pushdanmu.js # 推送弹幕脚本
|
||||
│ │ └── systemsettings.js # 系统设置脚本
|
||||
│ └── utils/
|
||||
│ ├── cache-util.js # 缓存数据处理工具
|
||||
│ ├── codec-util.js # 编解码工具
|
||||
│ ├── common-util.js # 通用工具
|
||||
│ ├── danmu-util.js # 弹幕处理工具
|
||||
│ ├── douban-util.js # 豆瓣API请求工具
|
||||
│ ├── http-util.js # 请求工具
|
||||
│ ├── imdb-util.js # IMDB API请求工具
|
||||
│ ├── log-util.js # 日志工具
|
||||
│ ├── redis-util.js # redis工具
|
||||
│ ├── time-util.js # 时间日期工具
|
||||
│ ├── tmdb-util.js # TMDB API请求处理工具
|
||||
│ └── zh-util.js # 中文繁简转换工具
|
||||
├── netlify/
|
||||
│ └── functions/
|
||||
│ └── api.js # netlify 中间处理逻辑
|
||||
└── node-functions/
|
||||
├── [[...path]]..js # edgeone pages 所有路由跳转指向index
|
||||
└── index.js # edgeone pages 中间处理逻辑
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
# 默认值:87654321
|
||||
TOKEN: "87654321"
|
||||
|
||||
# 系统管理访问令牌(用于系统管理功能)
|
||||
# 如果未配置此值,则无法访问系统管理功能,需要先配置后在URL中填入此token才能打开系统管理
|
||||
# 默认值:空(不启用系统管理访问控制)
|
||||
# ADMIN_TOKEN:
|
||||
|
||||
# 其他服务器地址(第三方弹幕服务器)
|
||||
# 默认值:https://api.danmu.icu
|
||||
OTHER_SERVER: "https://api.danmu.icu"
|
||||
@@ -238,3 +243,20 @@ MAX_LAST_SELECT_MAP: 100
|
||||
# 默认值:空
|
||||
# UPSTASH_REDIS_REST_TOKEN: ""
|
||||
|
||||
# ==================== 部署平台API接口信息 配置 ====================
|
||||
|
||||
# 部署账号ID,调用部署服务API需要,配置后可使用UI界面配置服务
|
||||
# 默认值:空
|
||||
#DEPLOY_PLATFROM_ACCOUNT: ""
|
||||
|
||||
# 部署项目名称,调用部署服务API需要,配置后可使用UI界面配置服务
|
||||
# 默认值:空
|
||||
#DEPLOY_PLATFROM_PROJECT: ""
|
||||
|
||||
# 部署平台token,调用部署服务API需要,配置后可使用UI界面配置服务
|
||||
# 默认值:空
|
||||
#DEPLOY_PLATFROM_TOKEN: ""
|
||||
|
||||
# 在建立 HTTPS 连接时是否验证服务器的 SSL/TLS 证书,0表示忽略
|
||||
# 默认值:1
|
||||
# NODE_TLS_REJECT_UNAUTHORIZED: 1
|
||||
@@ -0,0 +1,96 @@
|
||||
import { jsonResponse } from '../utils/http-util.js';
|
||||
import { HandlerFactory } from '../configs/handlers/handler-factory.js';
|
||||
import { globals } from '../configs/globals.js';
|
||||
|
||||
/**
|
||||
* 处理设置环境变量的请求
|
||||
*/
|
||||
export async function handleSetEnv(request) {
|
||||
try {
|
||||
const { key, value } = await request.json();
|
||||
|
||||
if (!key) {
|
||||
return jsonResponse({ success: false, message: '缺少环境变量名称' }, 400);
|
||||
}
|
||||
|
||||
// 获取当前部署平台
|
||||
const deployPlatform = globals.deployPlatform;
|
||||
|
||||
// 根据部署平台获取相应的handler
|
||||
const handler = await HandlerFactory.getHandler(deployPlatform);
|
||||
|
||||
// 调用handler的setEnv方法
|
||||
const result = await handler.setEnv(key, value);
|
||||
|
||||
if (result) {
|
||||
return jsonResponse({ success: true, message: `环境变量 ${key} 设置成功` });
|
||||
} else {
|
||||
return jsonResponse({ success: false, message: `环境变量 ${key} 设置失败` }, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置环境变量失败:', error);
|
||||
return jsonResponse({ success: false, message: `设置环境变量失败: ${error.message}` }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理添加环境变量的请求
|
||||
*/
|
||||
export async function handleAddEnv(request) {
|
||||
try {
|
||||
const { key, value } = await request.json();
|
||||
|
||||
if (!key) {
|
||||
return jsonResponse({ success: false, message: '缺少环境变量名称' }, 400);
|
||||
}
|
||||
|
||||
// 获取当前部署平台
|
||||
const deployPlatform = globals.deployPlatform ;
|
||||
|
||||
// 根据部署平台获取相应的handler
|
||||
const handler = await HandlerFactory.getHandler(deployPlatform);
|
||||
|
||||
// 调用handler的addEnv方法
|
||||
const result = await handler.addEnv(key, value);
|
||||
|
||||
if (result) {
|
||||
return jsonResponse({ success: true, message: `环境变量 ${key} 添加成功` });
|
||||
} else {
|
||||
return jsonResponse({ success: false, message: `环境变量 ${key} 添加失败` }, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加环境变量失败:', error);
|
||||
return jsonResponse({ success: false, message: `添加环境变量失败: ${error.message}` }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除环境变量的请求
|
||||
*/
|
||||
export async function handleDelEnv(request) {
|
||||
try {
|
||||
const { key } = await request.json();
|
||||
|
||||
if (!key) {
|
||||
return jsonResponse({ success: false, message: '缺少环境变量名称' }, 400);
|
||||
}
|
||||
|
||||
// 获取当前部署平台
|
||||
const deployPlatform = globals.deployPlatform;
|
||||
|
||||
// 根据部署平台获取相应的handler
|
||||
const handler = await HandlerFactory.getHandler(deployPlatform);
|
||||
|
||||
// 调用handler的delEnv方法
|
||||
const result = await handler.delEnv(key);
|
||||
|
||||
if (result) {
|
||||
return jsonResponse({ success: true, message: `环境变量 ${key} 删除成功` });
|
||||
} else {
|
||||
return jsonResponse({ success: false, message: `环境变量 ${key} 删除失败` }, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除环境变量失败:', error);
|
||||
return jsonResponse({ success: false, message: `删除环境变量失败: ${error.message}` }, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { globals } from "../configs/globals.js";
|
||||
import { jsonResponse } from "../utils/http-util.js";
|
||||
import { HTML_TEMPLATE } from "../ui/template.js";
|
||||
import { formatLogMessage, log } from "../utils/log-util.js";
|
||||
import { HandlerFactory } from "../configs/handlers/handler-factory.js";
|
||||
|
||||
export function handleUI() {
|
||||
return new Response(HTML_TEMPLATE.replace("globals.currentToken", globals.currentToken), {
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function handleConfig(hasPermission = false) {
|
||||
// 获取环境变量配置
|
||||
const envVarConfig = globals.envVarConfig;
|
||||
|
||||
// 分类环境变量
|
||||
const categorizedVars = {
|
||||
api: [],
|
||||
source: [],
|
||||
match: [],
|
||||
danmu: [],
|
||||
cache: [],
|
||||
system: []
|
||||
};
|
||||
|
||||
// 获取所有环境变量 - 这是用于配置预览的
|
||||
const previewEnvVars = {
|
||||
...globals.accessedEnvVars,
|
||||
localCacheValid: globals.localCacheValid,
|
||||
redisValid: globals.redisValid,
|
||||
deployPlatform: globals.deployPlatform
|
||||
};
|
||||
|
||||
// 将环境变量按分类组织 - 使用原始环境变量进行分类,但保持预览格式
|
||||
Object.keys(previewEnvVars).forEach(key => {
|
||||
const varConfig = envVarConfig[key] || { category: 'system', type: 'text', description: '未分类配置项' };
|
||||
const category = varConfig.category || 'system';
|
||||
|
||||
categorizedVars[category].push({
|
||||
key: key,
|
||||
value: previewEnvVars[key].value || previewEnvVars[key], // 如果是新格式则取value字段,否则直接使用原值
|
||||
type: previewEnvVars[key].type || varConfig.type || 'text', // 如果是新格式则取type字段,否则使用配置中的type或默认text
|
||||
description: varConfig.description || '无描述',
|
||||
options: previewEnvVars[key].options || varConfig.options // 如果是新格式则取options字段
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否配置了ADMIN_TOKEN
|
||||
const adminToken = globals.adminToken || '';
|
||||
const hasAdminToken = adminToken.trim() !== '';
|
||||
|
||||
// 准备原始环境变量,无权限时也需要脱敏
|
||||
let originalEnvVars = { ...globals.originalEnvVars };
|
||||
if (!hasPermission || globals.currentToken !== globals.adminToken) {
|
||||
Object.keys(originalEnvVars).forEach(key => {
|
||||
if (globals.currentToken !== globals.token || key !== "TOKEN") {
|
||||
if (key in previewEnvVars && /^\*+$/.test(previewEnvVars[key])) {
|
||||
originalEnvVars[key] = previewEnvVars[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
message: "Welcome to the LogVar Danmu API server",
|
||||
version: globals.VERSION,
|
||||
envs: previewEnvVars, // 配置预览使用
|
||||
categorizedEnvVars: categorizedVars,
|
||||
envVarConfig: envVarConfig,
|
||||
originalEnvVars: originalEnvVars, // 系统设置使用原始环境变量(已脱敏)
|
||||
hasAdminToken: hasAdminToken, // 添加admin token配置状态
|
||||
repository: "https://github.com/huangxd-/danmu_api.git",
|
||||
description: "一个人人都能部署的基于 js 的弹幕 API 服务器,支持爱优腾芒哔人韩巴弹幕直接获取,兼容弹弹play的搜索、详情查询和弹幕获取接口规范,并提供日志记录,支持vercel/netlify/edgeone/cloudflare/docker/claw等部署方式,不用提前下载弹幕,没有nas或小鸡也能一键部署。",
|
||||
notice: "本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。有问题提issue或私信机器人都ok,TG MSG ROBOT: [https://t.me/ddjdd_bot]; 推荐加互助群咨询,TG GROUP: [https://t.me/logvar_danmu_group]; 关注频道获取最新更新内容,TG CHANNEL: [https://t.me/logvar_danmu_channel]。"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重新部署请求
|
||||
* @returns {Response} 部署操作结果
|
||||
*/
|
||||
export async function handleDeploy() {
|
||||
try {
|
||||
const deployPlatform = globals.deployPlatform;
|
||||
log("info", `[server] Deployment request received for platform: ${deployPlatform}`);
|
||||
|
||||
// 如果是 Node 部署,直接返回成功,因为 Node 环境不需要重新部署
|
||||
if (deployPlatform.toLowerCase() === 'node') {
|
||||
log("info", `[server] Node/Docker deployment - no redeployment needed, config changes take effect automatically`);
|
||||
return jsonResponse({ success: true, message: "Node/Docker deployment - configuration changes take effect automatically" }, 200);
|
||||
}
|
||||
|
||||
// 对于其他平台(如 Cloudflare、Vercel、Netlify 等),使用相应的 Handler 触发部署
|
||||
const handler = await HandlerFactory.getHandler(deployPlatform);
|
||||
if (!handler) {
|
||||
log("error", `[server] No handler found for platform: ${deployPlatform}`);
|
||||
return jsonResponse({ success: false, message: `No handler found for platform: ${deployPlatform}` }, 400);
|
||||
}
|
||||
|
||||
// 调用 handler 的 deploy 方法
|
||||
const deployResult = await handler.deploy();
|
||||
if (deployResult) {
|
||||
log("info", `[server] Deployment triggered successfully for platform: ${deployPlatform}`);
|
||||
return jsonResponse({ success: true, message: "Deployment triggered successfully" }, 200);
|
||||
} else {
|
||||
log("error", `[server] Failed to trigger deployment for platform: ${deployPlatform}`);
|
||||
return jsonResponse({ success: false, message: "Failed to trigger deployment" }, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
log("error", `[server] Deployment error: ${error.message}`);
|
||||
return jsonResponse({ success: false, message: `Deployment failed: ${error.message}` }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理获取日志的请求
|
||||
* @returns {Response} 包含日志文本的响应
|
||||
*/
|
||||
export function handleLogs() {
|
||||
const logText = globals.logBuffer
|
||||
.map(
|
||||
(log) =>
|
||||
`[${log.timestamp}] ${log.level}: ${formatLogMessage(log.message)}`
|
||||
)
|
||||
.join("\n");
|
||||
return new Response(logText, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理清除日志的请求
|
||||
* @returns {Response} 表示操作成功的响应
|
||||
*/
|
||||
export function handleClearLogs() {
|
||||
globals.logBuffer = [];
|
||||
return jsonResponse({ success: true, message: "Logs cleared" }, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理清理缓存的请求
|
||||
* @returns {Response} 表示操作成功的响应
|
||||
*/
|
||||
export async function handleClearCache() {
|
||||
try {
|
||||
// 清理 globals 中的缓存数据
|
||||
globals.animes = [];
|
||||
globals.episodeIds = [];
|
||||
globals.episodeNum = 10001; // 重置为初始值
|
||||
globals.lastSelectMap = new Map(); // 重新创建 Map 对象
|
||||
|
||||
// 清理搜索和弹幕缓存
|
||||
globals.searchCache = new Map();
|
||||
globals.commentCache = new Map();
|
||||
globals.requestHistory = new Map();
|
||||
|
||||
log("info", `[server] Memory cache cleared successfully`);
|
||||
|
||||
// 同步清理本地缓存和Redis缓存
|
||||
try {
|
||||
// 如果本地缓存有效,更新本地缓存
|
||||
if (globals.localCacheValid) {
|
||||
const { updateLocalCaches } = await import("../utils/cache-util.js");
|
||||
await updateLocalCaches();
|
||||
log("info", `[server] Local cache cleared successfully`);
|
||||
}
|
||||
} catch (localError) {
|
||||
log("warn", `[server] Local cache may not be available: ${localError.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果Redis有效,更新Redis缓存
|
||||
if (globals.redisValid) {
|
||||
const { updateRedisCaches } = await import("../utils/redis-util.js");
|
||||
await updateRedisCaches();
|
||||
log("info", `[server] Redis cache cleared successfully`);
|
||||
}
|
||||
} catch (redisError) {
|
||||
log("warn", `[server] Redis may not be available: ${redisError.message}`);
|
||||
}
|
||||
|
||||
log("info", `[server] All caches cleared successfully`);
|
||||
return jsonResponse({ success: true, message: "Cache cleared successfully", clearedItems: {
|
||||
animes: 0,
|
||||
episodeIds: 0,
|
||||
episodeNum: 10001,
|
||||
lastSelectMap: 0,
|
||||
searchCache: 0,
|
||||
commentCache: 0,
|
||||
requestHistory: 0
|
||||
}}, 200);
|
||||
} catch (error) {
|
||||
log("error", `[server] Cache clear failed: ${error.message}`);
|
||||
return jsonResponse({ success: false, message: `Cache clear failed: ${error.message}` }, 500);
|
||||
}
|
||||
}
|
||||
+78
-15
@@ -6,6 +6,7 @@ export class Envs {
|
||||
static env;
|
||||
|
||||
// 记录获取过的环境变量
|
||||
static originalEnvVars = new Map();
|
||||
static accessedEnvVars = new Map();
|
||||
|
||||
static VOD_ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq']; // vod允许的播放平台
|
||||
@@ -23,10 +24,13 @@ export class Envs {
|
||||
let value;
|
||||
if (typeof this.env !== 'undefined' && this.env[key]) {
|
||||
value = this.env[key];
|
||||
this.originalEnvVars.set(key, value);
|
||||
} else if (typeof process !== 'undefined' && process.env?.[key]) {
|
||||
value = process.env[key];
|
||||
this.originalEnvVars.set(key, value);
|
||||
} else {
|
||||
value = defaultValue;
|
||||
this.originalEnvVars.set(key, "");
|
||||
}
|
||||
|
||||
let parsedValue;
|
||||
@@ -75,10 +79,9 @@ export class Envs {
|
||||
|
||||
/**
|
||||
* 解析 VOD 服务器配置
|
||||
* @param {Object} env 环境对象
|
||||
* @returns {Array} 服务器列表
|
||||
*/
|
||||
static resolveVodServers(env) {
|
||||
static resolveVodServers() {
|
||||
const defaultVodServers = '金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top';
|
||||
let vodServersConfig = this.get('VOD_SERVERS', defaultVodServers, 'string');
|
||||
|
||||
@@ -102,11 +105,9 @@ export class Envs {
|
||||
|
||||
/**
|
||||
* 解析源排序
|
||||
* @param {Object} env 环境对象
|
||||
* @param {string} deployPlatform 部署平台
|
||||
* @returns {Array} 源排序数组
|
||||
*/
|
||||
static resolveSourceOrder(env, deployPlatform) {
|
||||
static resolveSourceOrder() {
|
||||
let sourceOrder = this.get('SOURCE_ORDER', '360,vod,renren,hanjutv', 'string');
|
||||
|
||||
const orderArr = sourceOrder
|
||||
@@ -121,10 +122,9 @@ export class Envs {
|
||||
|
||||
/**
|
||||
* 解析平台排序
|
||||
* @param {Object} env 环境对象
|
||||
* @returns {Array} 平台排序数组
|
||||
*/
|
||||
static resolvePlatformOrder(env) {
|
||||
static resolvePlatformOrder() {
|
||||
const orderArr = this.get('PLATFORM_ORDER', '', 'string')
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
@@ -137,10 +137,9 @@ export class Envs {
|
||||
|
||||
/**
|
||||
* 解析剧集标题过滤正则
|
||||
* @param {Object} env 环境对象
|
||||
* @returns {RegExp} 过滤正则表达式
|
||||
*/
|
||||
static resolveEpisodeTitleFilter(env) {
|
||||
static resolveEpisodeTitleFilter() {
|
||||
const defaultFilter = '(特别|惊喜|纳凉)?企划|合伙人手记|超前(营业|vlog)?|速览|vlog|reaction|纯享|加更(版|篇)?|抢先(看|版|集|篇)?|抢鲜|预告|花絮(独家)?|' +
|
||||
'特辑|彩蛋|专访|幕后(故事|花絮|独家)?|直播(陪看|回顾)?|未播(片段)?|衍生|番外|会员(专享|加长|尊享|专属|版)?|片花|精华|看点|速看|解读|影评|解说|吐槽|盘点|拍摄花絮|制作花絮|幕后花絮|未播花絮|独家花絮|' +
|
||||
'花絮特辑|先导预告|终极预告|正式预告|官方预告|彩蛋片段|删减片段|未播片段|番外彩蛋|精彩片段|精彩看点|精彩回顾|精彩集锦|看点解析|看点预告|' +
|
||||
@@ -164,7 +163,14 @@ export class Envs {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析弹幕转换颜色
|
||||
* 获取记录的原始环境变量 JSON
|
||||
* @returns {Map<any, any>} JSON 字符串
|
||||
*/
|
||||
static getOriginalEnvVars() {
|
||||
return this.originalEnvVars;
|
||||
}
|
||||
|
||||
/** 解析弹幕转换颜色
|
||||
* @returns {string} 弹幕转换颜色
|
||||
*/
|
||||
static resolveConvertColor() {
|
||||
@@ -187,21 +193,73 @@ export class Envs {
|
||||
* @param {string} deployPlatform 部署平台
|
||||
* @returns {Object} 配置对象
|
||||
*/
|
||||
static load(env = {}, deployPlatform = 'node') {
|
||||
static load(env = {}) {
|
||||
this.env = env;
|
||||
|
||||
// 环境变量分类和描述映射
|
||||
const envVarConfig = {
|
||||
// API配置
|
||||
'TOKEN': { category: 'api', type: 'text', description: 'API访问令牌' },
|
||||
'ADMIN_TOKEN': { category: 'api', type: 'text', description: '系统管理访问令牌' },
|
||||
'RATE_LIMIT_MAX_REQUESTS': { category: 'api', type: 'number', description: '限流配置:1分钟内最大请求次数,0表示不限流,默认3', min: 0, max: 50 },
|
||||
|
||||
// 源配置
|
||||
'SOURCE_ORDER': { category: 'source', type: 'multi-select', options: this.ALLOWED_SOURCES, description: '源排序配置,默认360,vod,renren,hanjutv' },
|
||||
'OTHER_SERVER': { category: 'source', type: 'text', description: '第三方弹幕服务器,默认https://api.danmu.icu' },
|
||||
'VOD_SERVERS': { category: 'source', type: 'text', description: 'VOD站点配置,格式:名称@URL,名称@URL,默认金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top' },
|
||||
'VOD_RETURN_MODE': { category: 'source', type: 'select', options: ['all', 'fastest'], description: 'VOD返回模式:all(所有站点)或 fastest(最快的站点),默认fastest' },
|
||||
'VOD_REQUEST_TIMEOUT': { category: 'source', type: 'number', description: 'VOD请求超时时间,默认10000', min: 5000, max: 30000 },
|
||||
'BILIBILI_COOKIE': { category: 'source', type: 'text', description: 'B站Cookie' },
|
||||
'YOUKU_CONCURRENCY': { category: 'source', type: 'number', description: '优酷并发配置,默认8', min: 1, max: 16 },
|
||||
|
||||
// 匹配配置
|
||||
'PLATFORM_ORDER': { category: 'match', type: 'multi-select', options: this.ALLOWED_PLATFORMS, description: '平台排序配置' },
|
||||
'EPISODE_TITLE_FILTER': { category: 'match', type: 'text', description: '剧集标题过滤规则' },
|
||||
'ENABLE_EPISODE_FILTER': { category: 'match', type: 'boolean', description: '集标题过滤开关' },
|
||||
'STRICT_TITLE_MATCH': { category: 'match', type: 'boolean', description: '严格标题匹配模式' },
|
||||
'TITLE_TO_CHINESE': { category: 'match', type: 'boolean', description: '外语标题转换中文开关' },
|
||||
|
||||
// 弹幕配置
|
||||
'BLOCKED_WORDS': { category: 'danmu', type: 'text', description: '屏蔽词列表' },
|
||||
'GROUP_MINUTE': { category: 'danmu', type: 'number', description: '分钟内合并去重(0表示不去重),默认1', min: 0, max: 30 },
|
||||
'DANMU_LIMIT': { category: 'danmu', type: 'number', description: '弹幕数量限制,单位为k,即千:默认 0,表示不限制弹幕数', min: 0, max: 100 },
|
||||
'DANMU_SIMPLIFIED': { category: 'danmu', type: 'boolean', description: '弹幕繁体转简体开关' },
|
||||
'CONVERT_TOP_BOTTOM_TO_SCROLL': { category: 'danmu', type: 'boolean', description: '顶部/底部弹幕转换为浮动弹幕' },
|
||||
'CONVERT_COLOR': { category: 'danmu', type: 'select', options: ['default', 'white', 'color'], description: '弹幕转换颜色配置' },
|
||||
'DANMU_OUTPUT_FORMAT': { category: 'danmu', type: 'select', options: ['json', 'xml'], description: '弹幕输出格式,默认json' },
|
||||
|
||||
// 缓存配置
|
||||
'SEARCH_CACHE_MINUTES': { category: 'cache', type: 'number', description: '搜索结果缓存时间(分钟),默认1', min: 1, max: 120 },
|
||||
'COMMENT_CACHE_MINUTES': { category: 'cache', type: 'number', description: '弹幕缓存时间(分钟),默认1', min: 1, max: 120 },
|
||||
'REMEMBER_LAST_SELECT': { category: 'cache', type: 'boolean', description: '记住手动选择结果' },
|
||||
'MAX_LAST_SELECT_MAP': { category: 'cache', type: 'number', description: '记住上次选择映射缓存大小限制', min: 10, max: 1000 },
|
||||
'UPSTASH_REDIS_REST_URL': { category: 'cache', type: 'text', description: 'Upstash Redis请求链接' },
|
||||
'UPSTASH_REDIS_REST_TOKEN': { category: 'cache', type: 'text', description: 'Upstash Redis访问令牌' },
|
||||
|
||||
// 系统配置
|
||||
'PROXY_URL': { category: 'system', type: 'text', description: '代理/反代地址' },
|
||||
'TMDB_API_KEY': { category: 'system', type: 'text', description: 'TMDB API密钥' },
|
||||
'LOG_LEVEL': { category: 'system', type: 'select', options: ['debug', 'info', 'warn', 'error'], description: '日志级别配置' },
|
||||
'DEPLOY_PLATFROM_ACCOUNT': { category: 'system', type: 'text', description: '部署平台账号ID' },
|
||||
'DEPLOY_PLATFROM_PROJECT': { category: 'system', type: 'text', description: '部署平台项目名称' },
|
||||
'DEPLOY_PLATFROM_TOKEN': { category: 'system', type: 'text', description: '部署平台访问令牌' },
|
||||
'NODE_TLS_REJECT_UNAUTHORIZED': { category: 'system', type: 'number', description: '在建立 HTTPS 连接时是否验证服务器的 SSL/TLS 证书,0表示忽略' },
|
||||
};
|
||||
|
||||
return {
|
||||
vodAllowedPlatforms: this.VOD_ALLOWED_PLATFORMS,
|
||||
allowedPlatforms: this.ALLOWED_PLATFORMS,
|
||||
token: this.get('TOKEN', '87654321', 'string', true), // token,默认为87654321
|
||||
adminToken: this.get('ADMIN_TOKEN', '', 'string', true), // admin token,用于系统管理访问控制
|
||||
sourceOrderArr: this.resolveSourceOrder(), // 源排序
|
||||
otherServer: this.get('OTHER_SERVER', 'https://api.danmu.icu', 'string'), // 第三方弹幕服务器
|
||||
vodServers: this.resolveVodServers(env), // vod站点配置,格式:名称@URL,名称@URL
|
||||
vodServers: this.resolveVodServers(), // vod站点配置,格式:名称@URL,名称@URL
|
||||
vodReturnMode: this.get('VOD_RETURN_MODE', 'fastest', 'string').toLowerCase(), // vod返回模式:all(所有站点)或 fastest(最快的站点)
|
||||
vodRequestTimeout: this.get('VOD_REQUEST_TIMEOUT', '10000', 'string'), // vod超时时间(默认10秒)
|
||||
bilibliCookie: this.get('BILIBILI_COOKIE', '', 'string', true), // b站cookie
|
||||
youkuConcurrency: Math.min(this.get('YOUKU_CONCURRENCY', 8, 'number'), 16), // 优酷并发配置
|
||||
sourceOrderArr: this.resolveSourceOrder(env, deployPlatform), // 源排序
|
||||
platformOrderArr: this.resolvePlatformOrder(env), // 自动匹配优选平台
|
||||
episodeTitleFilter: this.resolveEpisodeTitleFilter(env), // 剧集标题正则过滤
|
||||
platformOrderArr: this.resolvePlatformOrder(), // 自动匹配优选平台
|
||||
episodeTitleFilter: this.resolveEpisodeTitleFilter(), // 剧集标题正则过滤
|
||||
blockedWords: this.get('BLOCKED_WORDS', '', 'string'), // 屏蔽词列表
|
||||
groupMinute: Math.min(this.get('GROUP_MINUTE', 1, 'number'), 30), // 分钟内合并去重(默认 1,最大值30,0表示不去重)
|
||||
danmuLimit: this.get('DANMU_LIMIT', 0, 'number'), // 等间隔采样限制弹幕总数,单位为k,即千:默认 0,表示不限制弹幕数,若改为5,弹幕总数在超过5000的情况下会将弹幕数控制在5000
|
||||
@@ -222,6 +280,11 @@ export class Envs {
|
||||
titleToChinese: this.get('TITLE_TO_CHINESE', false, 'boolean'), // 外语标题转换中文开关
|
||||
rememberLastSelect: this.get('REMEMBER_LAST_SELECT', true, 'boolean'), // 是否记住手动选择结果,用于match自动匹配时优选上次的选择(默认 true,记住)
|
||||
MAX_LAST_SELECT_MAP: this.get('MAX_LAST_SELECT_MAP', 100, 'number'), // 记住上次选择映射缓存大小限制(默认 100)
|
||||
deployPlatformAccount: this.get('DEPLOY_PLATFROM_ACCOUNT', '', 'string', true), // 部署平台账号ID配置(默认空)
|
||||
deployPlatformProject: this.get('DEPLOY_PLATFROM_PROJECT', '', 'string', true), // 部署平台项目名称配置(默认空)
|
||||
deployPlatformToken: this.get('DEPLOY_PLATFROM_TOKEN', '', 'string', true), // 部署平台项目名称配置(默认空)
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: this.get('NODE_TLS_REJECT_UNAUTHORIZED', 1, 'number'), // 在建立 HTTPS 连接时是否验证服务器的 SSL/TLS 证书,0表示忽略,默认为1
|
||||
envVarConfig: envVarConfig // 环境变量分类和描述映射
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,13 @@ import { Envs } from './envs.js';
|
||||
*/
|
||||
export const Globals = {
|
||||
// 缓存环境变量
|
||||
env: {},
|
||||
envs: {},
|
||||
originalEnvVars: {},
|
||||
accessedEnvVars: {},
|
||||
|
||||
// 静态常量
|
||||
VERSION: '1.8.1',
|
||||
VERSION: '1.9.0',
|
||||
MAX_LOGS: 500, // 日志存储,最多保存 500 行
|
||||
MAX_ANIMES: 100,
|
||||
|
||||
@@ -34,15 +36,29 @@ export const Globals = {
|
||||
},
|
||||
searchCache: new Map(), // 搜索结果缓存,存储格式:{ keyword: { results, timestamp } }
|
||||
commentCache: new Map(), // 弹幕缓存,存储格式:{ videoUrl: { comments, timestamp } }
|
||||
deployPlatform: '', // 部署平台配置
|
||||
currentToken: '', // 标识当前可用token
|
||||
|
||||
/**
|
||||
* 初始化全局变量,加载环境变量依赖
|
||||
* @param {Object} env 环境对象
|
||||
* @param {string} deployPlatform 部署平台
|
||||
* @returns {Object} 全局配置对象
|
||||
*/
|
||||
init(env = {}, deployPlatform = 'node') {
|
||||
this.envs = Envs.load(env, deployPlatform);
|
||||
init(env = {}) {
|
||||
this.env = env;
|
||||
this.envs = Envs.load(this.env);
|
||||
this.originalEnvVars = Object.fromEntries(Envs.getOriginalEnvVars());
|
||||
this.accessedEnvVars = Object.fromEntries(Envs.getAccessedEnvVars());
|
||||
return this.getConfig();
|
||||
},
|
||||
|
||||
/**
|
||||
* 重新初始化全局变量,加载环境变量依赖
|
||||
* @returns {Object} 全局配置对象
|
||||
*/
|
||||
reInit() {
|
||||
this.envs = Envs.load(this.env);
|
||||
this.originalEnvVars = Object.fromEntries(Envs.getOriginalEnvVars());
|
||||
this.accessedEnvVars = Object.fromEntries(Envs.getAccessedEnvVars());
|
||||
return this.getConfig();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { globals } from '../globals.js';
|
||||
import { log } from "../../utils/log-util.js";
|
||||
|
||||
// =====================
|
||||
// 环境变量处理基类
|
||||
// =====================
|
||||
|
||||
export default class BaseHandler {
|
||||
constructor() {
|
||||
// 构造函数,初始化通用配置
|
||||
}
|
||||
|
||||
// 更新本地环境变量
|
||||
updateLocalEnv(key, value) {
|
||||
// 立即更新 (避免重新加载文件的开销)
|
||||
if (typeof globals.env !== 'undefined') {
|
||||
globals.env[key] = value;
|
||||
} else if (typeof process !== 'undefined') {
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
// 重新初始化全局配置
|
||||
globals.reInit();
|
||||
|
||||
log("info", `[server] ✓ Environment variable updated successfully: ${key}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
delLocalEnv(key) {
|
||||
// 删除
|
||||
if (typeof globals.env !== 'undefined' && globals.env[key]) {
|
||||
delete globals.env[key];
|
||||
} else if (typeof process !== 'undefined' && process.env?.[key]) {
|
||||
delete process.env[key];
|
||||
}
|
||||
|
||||
// 重新初始化全局配置
|
||||
globals.reInit();
|
||||
|
||||
log("info", `[server] ✓ Environment variable deleted successfully: ${key}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取所有环境变量
|
||||
getAllEnv() {
|
||||
// 获取原始环境变量
|
||||
const originalEnvVars = globals.originalEnvVars;
|
||||
|
||||
// 获取环境变量配置信息
|
||||
const envVarConfig = globals.envVarConfig;
|
||||
|
||||
// 构建带类型信息的环境变量对象
|
||||
const envWithTypes = {};
|
||||
|
||||
// 遍历所有环境变量
|
||||
for (const [key, value] of Object.entries(originalEnvVars)) {
|
||||
// 获取该环境变量的配置信息
|
||||
const config = envVarConfig[key] || { category: 'system', type: 'text', description: '未分类配置项' };
|
||||
|
||||
// 构建带类型信息的对象
|
||||
envWithTypes[key] = {
|
||||
value: value,
|
||||
category: config.category,
|
||||
type: config.type,
|
||||
description: config.description,
|
||||
options: config.options // 仅对 select 和 multi-select 类型有效
|
||||
};
|
||||
}
|
||||
|
||||
return envWithTypes;
|
||||
}
|
||||
|
||||
// 获取某个环境变量
|
||||
getEnv(key) {
|
||||
return this.getAllEnv()[key];
|
||||
}
|
||||
|
||||
// 设置环境变量的值
|
||||
async setEnv(key, value) {
|
||||
throw new Error("Method 'setEnv' must be implemented");
|
||||
}
|
||||
|
||||
// 添加环境变量
|
||||
async addEnv(key, value) {
|
||||
throw new Error("Method 'addEnv' must be implemented");
|
||||
}
|
||||
|
||||
// 删除环境变量
|
||||
async delEnv(key) {
|
||||
throw new Error("Method 'delEnv' must be implemented");
|
||||
}
|
||||
|
||||
// 校验必填参数
|
||||
async checkParams(accountId, projectId, token) {
|
||||
throw new Error("Method 'checkParams' must be implemented");
|
||||
}
|
||||
|
||||
// 部署
|
||||
async deploy(accountId, projectId, token) {
|
||||
throw new Error("Method 'deploy' must be implemented");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import BaseHandler from "./base-handler.js";
|
||||
import { globals } from '../globals.js';
|
||||
import { log } from "../../utils/log-util.js";
|
||||
import { httpGet, httpPatch } from "../../utils/http-util.js";
|
||||
|
||||
// =====================
|
||||
// Cloudflare环境变量处理类
|
||||
// =====================
|
||||
|
||||
export class CloudflareHandler extends BaseHandler {
|
||||
API_URL = 'https://api.cloudflare.com';
|
||||
|
||||
async _getAllEnvs(accountId, projectId, token) {
|
||||
const url = `${this.API_URL}/client/v4/accounts/${accountId}/workers/scripts/${projectId}/settings`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
};
|
||||
const res = await httpGet(url, options);
|
||||
if (res && res?.data && res?.data?.result && res?.data?.result?.bindings) {
|
||||
return res?.data?.result?.bindings;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置/删除环境变量
|
||||
* @param {Array} envs 环境变量数组(每个元素 {name, text, type})
|
||||
* @param {string} key 变量名
|
||||
* @param {string|null|undefined} value
|
||||
* - 字符串:新增或修改为该值
|
||||
* @returns {Array} 返回处理后的 envs(直接修改原数组并返回)
|
||||
*/
|
||||
_setEnv(envs, key, value) {
|
||||
// 查找已有项的索引
|
||||
const idx = envs.findIndex(item => item.name === key);
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
// 删除:value 为 null/undefined 时删除该变量
|
||||
if (idx !== -1) {
|
||||
envs.splice(idx, 1);
|
||||
}
|
||||
} else if (idx !== -1) {
|
||||
// 已存在 → 修改
|
||||
envs[idx].text = value;
|
||||
} else {
|
||||
// 不存在 → 新增
|
||||
envs.push({
|
||||
name: key,
|
||||
text: value,
|
||||
type: "plain_text"
|
||||
});
|
||||
}
|
||||
|
||||
return envs;
|
||||
}
|
||||
|
||||
async setEnv(key, value) {
|
||||
try {
|
||||
// 获取所有环境变量
|
||||
const envs = await this._getAllEnvs(globals.deployPlatformAccount, globals.deployPlatformProject, globals.deployPlatformToken);
|
||||
if (envs === null) {
|
||||
log("error", '[server] ✗ Failed to set environment variable: envs is null');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新云端环境变量
|
||||
const url = `${this.API_URL}/client/v4/accounts/${globals.deployPlatformAccount}/workers/scripts/${globals.deployPlatformProject}/settings`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}` },
|
||||
};
|
||||
const formData = new FormData();
|
||||
const settings = {
|
||||
bindings: this._setEnv(envs, key, value == null ? null : value.toString())
|
||||
};
|
||||
formData.append(
|
||||
"settings",
|
||||
new Blob([JSON.stringify(settings)], { type: "application/json" }),
|
||||
"settings.json"
|
||||
);
|
||||
await httpPatch(url, formData, options);
|
||||
|
||||
return this.updateLocalEnv(key, value);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to set environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async addEnv(key, value) {
|
||||
// addEnv 和 setEnv 在这个场景下逻辑相同,只是不存在该key
|
||||
return await this.setEnv(key, value);
|
||||
}
|
||||
|
||||
async delEnv(key) {
|
||||
// addEnv 和 setEnv 在这个场景下逻辑相同,只是value设置为null
|
||||
await this.setEnv(key, null);
|
||||
return this.delLocalEnv(key);
|
||||
}
|
||||
|
||||
async checkParams(accountId, projectId, token) {
|
||||
try {
|
||||
await this._getAllEnvs(accountId, projectId, token);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("error", 'checkParams failed! accountId, projectId or token is not valid:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async deploy() {
|
||||
log("log", 'After modifying the environment variables on the cloudflare platform, it will be automatically deployed.');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import BaseHandler from "./base-handler.js";
|
||||
import { globals } from '../globals.js';
|
||||
import { log } from "../../utils/log-util.js";
|
||||
import { httpPost } from "../../utils/http-util.js";
|
||||
|
||||
// =====================
|
||||
// Edgeone环境变量处理类
|
||||
// =====================
|
||||
|
||||
export class EdgeoneHandler extends BaseHandler {
|
||||
API_URL = 'https://pages-api.cloud.tencent.com/v1';
|
||||
|
||||
async _httpPost(token, data) {
|
||||
const url = `${this.API_URL}`;
|
||||
const options = {
|
||||
headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},
|
||||
};
|
||||
return await httpPost(url, JSON.stringify(data), options);
|
||||
}
|
||||
|
||||
async _getAllEnvs(projectId, token) {
|
||||
const data = {
|
||||
Action: "ModifyPagesProjectEnvs",
|
||||
ProjectId: projectId
|
||||
};
|
||||
const res = await this._httpPost(token, data);
|
||||
|
||||
if (res?.data?.Code !== 0) return null;
|
||||
|
||||
const list = res?.data.Data?.Response?.EnvVars ?? [];
|
||||
|
||||
return Object.fromEntries(list.map(({Key, Value}) => [Key, Value]));
|
||||
}
|
||||
|
||||
async setEnv(key, value) {
|
||||
try {
|
||||
// 更新云端环境变量
|
||||
const data = {
|
||||
Action: "ModifyPagesProjectEnvs",
|
||||
ProjectId: globals.deployPlatformProject,
|
||||
EnvVars: [
|
||||
{
|
||||
Key: key,
|
||||
Value: value.toString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await this._httpPost(globals.deployPlatformToken, data);
|
||||
|
||||
return this.updateLocalEnv(key, value);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to set environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async addEnv(key, value) {
|
||||
// addEnv 和 setEnv 在这个场景下逻辑相同
|
||||
return await this.setEnv(key, value);
|
||||
}
|
||||
|
||||
async delEnv(key) {
|
||||
try {
|
||||
// 删除云端环境变量
|
||||
const data = {
|
||||
Action: "DeletePagesProjectEnvs",
|
||||
ProjectId: globals.deployPlatformProject,
|
||||
EnvVars: [key]
|
||||
};
|
||||
|
||||
await this._httpPost(globals.deployPlatformToken, data);
|
||||
|
||||
return this.delLocalEnv(key);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to del environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async checkParams(accountId, projectId, token) {
|
||||
try {
|
||||
await this._getAllEnvs(projectId, token);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("error", 'checkParams failed! accountId, projectId or token is not valid:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async deploy() {
|
||||
try {
|
||||
// 触发云端部署
|
||||
const data = {
|
||||
Action: "CreatePagesDeployment",
|
||||
ProjectId: globals.deployPlatformProject,
|
||||
RepoBranch: "main",
|
||||
ViaMeta: "Github",
|
||||
Provider: "Github"
|
||||
};
|
||||
|
||||
const res = await this._httpPost(globals.deployPlatformToken, data);
|
||||
if (res?.data?.Code !== 0) {
|
||||
log("error", '[server] ✗ Failed to deploy:', JSON.stringify(res?.data));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to deploy:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Handler工厂类 - 根据部署平台返回相应的Handler实例
|
||||
*/
|
||||
export class HandlerFactory {
|
||||
static async getHandler(deployPlatform) {
|
||||
switch (deployPlatform?.toLowerCase()) {
|
||||
case 'cloudflare':
|
||||
const { CloudflareHandler } = await import('./cloudflare-handler.js');
|
||||
return new CloudflareHandler();
|
||||
case 'vercel':
|
||||
const { VercelHandler } = await import('./vercel-handler.js');
|
||||
return new VercelHandler();
|
||||
case 'netlify':
|
||||
const { NetlifyHandler } = await import('./netlify-handler.js');
|
||||
return new NetlifyHandler();
|
||||
case 'edgeone':
|
||||
const { EdgeoneHandler } = await import('./edgeone-handler.js');
|
||||
return new EdgeoneHandler();
|
||||
case 'node':
|
||||
const { NodeHandler } = await import(['./node-handler', '.js'].join(''));
|
||||
return new NodeHandler();
|
||||
default:
|
||||
// 默认返回NodeHandler,适用于本地开发或无法识别的平台
|
||||
const { NodeHandler: DefaultNodeHandler } = await import(['./node-handler', '.js'].join(''));
|
||||
return new DefaultNodeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的平台列表
|
||||
*/
|
||||
static getSupportedPlatforms() {
|
||||
return ['cloudflare', 'vercel', 'netlify', 'edgeone', 'node'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import BaseHandler from "./base-handler.js";
|
||||
import { globals } from '../globals.js';
|
||||
import { log } from "../../utils/log-util.js";
|
||||
import { httpGet, httpPost, httpDelete, httpPut } from "../../utils/http-util.js";
|
||||
|
||||
// =====================
|
||||
// Netlify环境变量处理类
|
||||
// =====================
|
||||
|
||||
export class NetlifyHandler extends BaseHandler {
|
||||
API_URL = 'https://api.netlify.com';
|
||||
|
||||
async _getAllEnvs(accountId, projectId, token) {
|
||||
const url = `${this.API_URL}/api/v1/accounts/${accountId}/env?site_id=${projectId}`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
};
|
||||
return await httpGet(url, options);
|
||||
}
|
||||
|
||||
async setEnv(key, value) {
|
||||
try {
|
||||
// 更新云端环境变量
|
||||
const url = `${this.API_URL}/api/v1/accounts/${globals.deployPlatformAccount}/env/${key}?site_id=${globals.deployPlatformProject}`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
const data = {
|
||||
key: key,
|
||||
values: [
|
||||
{ context: 'all', value: value.toString() },
|
||||
]
|
||||
};
|
||||
await httpPut(url, JSON.stringify(data), options);
|
||||
|
||||
return this.updateLocalEnv(key, value);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to set environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async addEnv(key, value) {
|
||||
try {
|
||||
// 更新云端环境变量
|
||||
const url = `${this.API_URL}/api/v1/accounts/${globals.deployPlatformAccount}/env?site_id=${globals.deployPlatformProject}`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
const data = [{
|
||||
key: key,
|
||||
values: [
|
||||
{ context: 'all', value: value },
|
||||
],
|
||||
}];
|
||||
await httpPost(url, JSON.stringify(data), options);
|
||||
|
||||
return this.updateLocalEnv(key, value);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to add environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async delEnv(key) {
|
||||
try {
|
||||
// 更新云端环境变量
|
||||
const url = `${this.API_URL}/api/v1/accounts/${globals.deployPlatformAccount}/env/${key}?site_id=${globals.deployPlatformProject}`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
await httpDelete(url, options);
|
||||
|
||||
return this.delLocalEnv(key);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to add environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async checkParams(accountId, projectId, token) {
|
||||
try {
|
||||
await this._getAllEnvs(accountId, projectId, token);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("error", 'checkParams failed! accountId, projectId or token is not valid:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async deploy() {
|
||||
try {
|
||||
// 触发云端部署
|
||||
const url = `${this.API_URL}/api/v1/sites/${globals.deployPlatformProject}/builds`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
const data = {};
|
||||
|
||||
const res = await httpPost(url, JSON.stringify(data), options);
|
||||
|
||||
if (!res?.data?.id) {
|
||||
log("error", '[server] ✗ Failed to deploy:', JSON.stringify(res?.data));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to deploy:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import BaseHandler from "./base-handler.js";
|
||||
import { log } from "../../utils/log-util.js";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import yaml from 'js-yaml';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// =====================
|
||||
// Node环境变量处理类
|
||||
// =====================
|
||||
|
||||
export class NodeHandler extends BaseHandler {
|
||||
/**
|
||||
* 在本地配置文件中设置环境变量
|
||||
*/
|
||||
updateConfigValue(key, value) {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const envPath = path.join(__dirname, '..', '..', '..', '.env');
|
||||
const yamlPath = path.join(__dirname, '..', '..', '..', 'config.yaml');
|
||||
|
||||
const envExists = fs.existsSync(envPath);
|
||||
const yamlExists = fs.existsSync(yamlPath);
|
||||
|
||||
if (!envExists && !yamlExists) {
|
||||
throw new Error('Neither .env nor config.yaml found');
|
||||
}
|
||||
|
||||
let updated = false;
|
||||
|
||||
try {
|
||||
// 更新 .env 文件
|
||||
if (envExists) {
|
||||
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||
const lines = envContent.split('\n');
|
||||
let keyFound = false;
|
||||
|
||||
// 查找并更新现有键
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const match = trimmed.match(/^([^=]+)=/);
|
||||
if (match && match[1].trim() === key) {
|
||||
lines[i] = `${key}=${value}`;
|
||||
keyFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果键不存在,添加到文件末尾
|
||||
if (!keyFound) {
|
||||
if (lines[lines.length - 1] !== '') {
|
||||
lines.push('');
|
||||
}
|
||||
lines.push(`${key}=${value}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(envPath, lines.join('\n'), 'utf8');
|
||||
log("info", `[server] Updated ${key} in .env`);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// 更新 config.yaml 文件
|
||||
if (yamlExists) {
|
||||
const yamlContent = fs.readFileSync(yamlPath, 'utf8');
|
||||
let yamlConfig = yaml.load(yamlContent) || {};
|
||||
|
||||
// 将扁平的环境变量键转换为嵌套对象路径
|
||||
const keys = key.toLowerCase().split('_');
|
||||
let current = yamlConfig;
|
||||
|
||||
// 遍历到倒数第二层
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!current[k] || typeof current[k] !== 'object') {
|
||||
current[k] = {};
|
||||
}
|
||||
current = current[k];
|
||||
}
|
||||
|
||||
// 设置最后一层的值,尝试智能转换类型
|
||||
const lastKey = keys[keys.length - 1];
|
||||
let parsedValue = value;
|
||||
|
||||
// 尝试转换为数字
|
||||
if (!isNaN(value) && value !== '') {
|
||||
parsedValue = Number(value);
|
||||
}
|
||||
// 尝试转换为布尔值
|
||||
else if (value === 'true') {
|
||||
parsedValue = true;
|
||||
} else if (value === 'false') {
|
||||
parsedValue = false;
|
||||
}
|
||||
|
||||
current[lastKey] = parsedValue;
|
||||
|
||||
// 写回文件
|
||||
fs.writeFileSync(yamlPath, yaml.dump(yamlConfig), 'utf8');
|
||||
log("info", `[server] Updated ${key} in config.yaml`);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
return updated;
|
||||
} catch (error) {
|
||||
log("error", '[server] Error updating configuration:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置环境变量并重新初始化全局配置
|
||||
*/
|
||||
async setEnv(key, value) {
|
||||
log("info", '[server] Setting environment variable:', key, '=', value);
|
||||
|
||||
try {
|
||||
// 更新配置文件
|
||||
const updated = this.updateConfigValue(key, value);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error('Failed to update configuration files');
|
||||
}
|
||||
|
||||
return this.updateLocalEnv(key, value);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to set environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的环境变量
|
||||
*/
|
||||
async addEnv(key, value) {
|
||||
// addEnv 和 setEnv 在这个场景下逻辑相同
|
||||
return await this.setEnv(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除环境变量
|
||||
*/
|
||||
async delEnv(key) {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const envPath = path.join(__dirname, '..', '..', '..', '.env');
|
||||
const yamlPath = path.join(__dirname, '..', '..', '..', 'config.yaml');
|
||||
|
||||
let deleted = false;
|
||||
|
||||
try {
|
||||
// 从 .env 文件删除
|
||||
if (fs.existsSync(envPath)) {
|
||||
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||
const lines = envContent.split('\n');
|
||||
const filteredLines = lines.filter(line => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) return true;
|
||||
const match = trimmed.match(/^([^=]+)=/);
|
||||
return !(match && match[1].trim() === key);
|
||||
});
|
||||
|
||||
fs.writeFileSync(envPath, filteredLines.join('\n'), 'utf8');
|
||||
log("info", `[server] Deleted ${key} from .env`);
|
||||
deleted = true;
|
||||
}
|
||||
|
||||
// 从 config.yaml 文件删除
|
||||
if (fs.existsSync(yamlPath)) {
|
||||
const yamlContent = fs.readFileSync(yamlPath, 'utf8');
|
||||
let yamlConfig = yaml.load(yamlContent) || {};
|
||||
|
||||
const keys = key.toLowerCase().split('_');
|
||||
let current = yamlConfig;
|
||||
|
||||
// 遍历到倒数第二层
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!current[k]) return;
|
||||
current = current[k];
|
||||
}
|
||||
|
||||
// 删除最后一层的键
|
||||
const lastKey = keys[keys.length - 1];
|
||||
if (current && lastKey in current) {
|
||||
delete current[lastKey];
|
||||
fs.writeFileSync(yamlPath, yaml.dump(yamlConfig), 'utf8');
|
||||
log("info", `[server] Deleted ${key} from config.yaml`);
|
||||
deleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
return this.delLocalEnv(key);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to delete environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import BaseHandler from "./base-handler.js";
|
||||
import { globals } from '../globals.js';
|
||||
import { log } from "../../utils/log-util.js";
|
||||
import { httpGet, httpPost, httpPatch, httpDelete } from "../../utils/http-util.js";
|
||||
|
||||
// =====================
|
||||
// Vercel环境变量处理类
|
||||
// =====================
|
||||
|
||||
export class VercelHandler extends BaseHandler {
|
||||
API_URL = 'https://api.vercel.com';
|
||||
|
||||
async _getAllEnvs(projectId, token) {
|
||||
const url = `${this.API_URL}/v10/projects/${projectId}/env`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
};
|
||||
const res = await httpGet(url, options);
|
||||
const allEnv = res.data.envs;
|
||||
return allEnv;
|
||||
}
|
||||
|
||||
async _getEevId(key) {
|
||||
try {
|
||||
const allEnv = await this._getAllEnvs(globals.deployPlatformProject, globals.deployPlatformToken);
|
||||
|
||||
// 查找第一个 key 字段匹配的元素
|
||||
const envItem = allEnv.find(env => env.key === key);
|
||||
|
||||
// 如果找到了,返回 id
|
||||
if (envItem) {
|
||||
return envItem;
|
||||
} else {
|
||||
// 如果没有找到匹配的元素,返回一个合适的值(比如 null)
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to _getEevId:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async setEnv(key, value) {
|
||||
// 首先获取云端环境变量
|
||||
const envItem = await this._getEevId(key);
|
||||
if (!envItem) {
|
||||
log("error", '[server] Error setEnv: 没有找到对应的环境变量!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新云端环境变量
|
||||
const url = `${this.API_URL}/v9/projects/${globals.deployPlatformProject}/env/${envItem.id}`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
const data = {
|
||||
key: key,
|
||||
value: value.toString(),
|
||||
target: envItem.target,
|
||||
type: envItem.type,
|
||||
};
|
||||
await httpPatch(url, JSON.stringify(data), options);
|
||||
|
||||
return this.updateLocalEnv(key, value);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to set environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async addEnv(key, value) {
|
||||
try {
|
||||
// 更新云端环境变量
|
||||
const url = `${this.API_URL}/v10/projects/${globals.deployPlatformProject}/env`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
const data = {
|
||||
key: key,
|
||||
value: value.toString(),
|
||||
target: [
|
||||
'production',
|
||||
'preview',
|
||||
'development',
|
||||
],
|
||||
type: 'encrypted',
|
||||
};
|
||||
await httpPost(url, JSON.stringify(data), options);
|
||||
|
||||
return this.updateLocalEnv(key, value);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to add environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async delEnv(key) {
|
||||
// 首先获取云端环境变量
|
||||
const envItem = await this._getEevId(key);
|
||||
if (!envItem) {
|
||||
log("error", '[server] Error setEnv: 没有找到对应的环境变量!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新云端环境变量
|
||||
const url = `${this.API_URL}/v9/projects/${globals.deployPlatformProject}/env/${envItem.id}`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
await httpDelete(url, options);
|
||||
|
||||
return this.delLocalEnv(key);
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to add environment variable:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async checkParams(accountId, projectId, token) {
|
||||
try {
|
||||
await this._getAllEnvs(projectId, token);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("error", 'checkParams failed! projectId or token is not valid:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async deploy() {
|
||||
try {
|
||||
// 获取上一次部署id
|
||||
const urlList = `${this.API_URL}/v6/deployments?projectId=${globals.deployPlatformProject}&limit=1`;
|
||||
const optionsList = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}` },
|
||||
};
|
||||
const resList = await httpGet(urlList, optionsList);
|
||||
|
||||
let lastestDeployId;
|
||||
if (resList?.data && resList?.data?.deployments && resList?.data?.deployments.length > 0) {
|
||||
lastestDeployId = resList.data.deployments[0].uid;
|
||||
} else {
|
||||
log("error", '[server] ✗ Failed to deploy:', JSON.stringify(resList?.data));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 触发云端部署
|
||||
const url = `${this.API_URL}/v13/deployments`;
|
||||
const options = {
|
||||
headers: { Authorization: `Bearer ${globals.deployPlatformToken}`, 'Content-Type': 'application/json' },
|
||||
};
|
||||
const data = {
|
||||
name: globals.deployPlatformProject,
|
||||
deploymentId: lastestDeployId,
|
||||
target: "production"
|
||||
};
|
||||
|
||||
const res = await httpPost(url, JSON.stringify(data), options);
|
||||
|
||||
if (!res?.data?.id) {
|
||||
log("error", '[server] ✗ Failed to deploy:', JSON.stringify(res?.data));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("error", '[server] ✗ Failed to deploy:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
# 目录
|
||||
|
||||
- [UI 系统使用说明](#ui-系统使用说明)
|
||||
- [功能概览](#功能概览)
|
||||
- [访问方式](#访问方式)
|
||||
- [功能详解](#功能详解)
|
||||
- [1. 配置预览](#1-配置预览)
|
||||
- [2. 日志查看](#2-日志查看)
|
||||
- [3. 接口调试](#3-接口调试)
|
||||
- [4. 推送弹幕](#4-推送弹幕)
|
||||
- [5. 系统配置](#5-系统配置)
|
||||
- [安全说明](#安全说明)
|
||||
- [部署平台支持](#部署平台支持)
|
||||
- [部署平台环境变量配置指南](#部署平台环境变量配置指南)
|
||||
- [各平台变量获取详细步骤](#各平台变量获取详细步骤)
|
||||
- [1. Vercel 平台](#1-vercel-平台)
|
||||
- [2. Netlify 平台](#2-netlify-平台)
|
||||
- [3. EdgeOne (腾讯云 Pages) 平台](#3-edgeone-腾讯云-pages-平台)
|
||||
- [4. Cloudflare 平台](#4-cloudflare-平台)
|
||||
- [常见问题](#常见问题)
|
||||
- [API 端点](#api-端点)
|
||||
- [故障排除](#故障排除)
|
||||
|
||||
# UI 系统使用说明
|
||||
|
||||
本项目包含一个基于 Web 的用户界面,用于管理和配置弹幕 API 服务。以下是 UI 系统的详细使用说明。
|
||||
|
||||
## 功能概览
|
||||
|
||||
UI 系统提供了以下主要功能模块:
|
||||
|
||||
1. **配置预览** - 查看当前生效的环境变量配置
|
||||
2. **日志查看** - 实时查看系统运行日志
|
||||
3. **接口调试** - 测试和调试 API 接口
|
||||
4. **推送弹幕** - 向播放器推送弹幕
|
||||
5. **系统配置** - 环境变量配置和系统管理
|
||||
|
||||
## 访问方式
|
||||
|
||||
UI 系统需要通过在 URL 中添加 TOKEN 来访问,以确保安全性:
|
||||
|
||||
- 普通功能访问:`http://your-domain/{TOKEN}`
|
||||
- 系统管理功能:`http://your-domain/{ADMIN_TOKEN}`
|
||||
|
||||
## 功能详解
|
||||
|
||||
### 1. 配置预览
|
||||
|
||||
在配置预览页面,您可以:
|
||||
|
||||
- 查看当前生效的环境变量配置
|
||||
- 了解各配置项的当前值和描述
|
||||
- 配置按类别分组显示(API配置、源配置、匹配配置等)
|
||||
|
||||
### 2. 日志查看
|
||||
|
||||
日志查看页面提供:
|
||||
|
||||
- 实时日志监控
|
||||
- 刷新日志功能
|
||||
- 清空日志功能(需要 ADMIN_TOKEN)
|
||||
|
||||
### 3. 接口调试
|
||||
|
||||
接口调试功能允许您:
|
||||
|
||||
- 选择不同的 API 接口进行测试
|
||||
- 输入接口参数
|
||||
- 发送请求并查看响应结果
|
||||
- 支持的接口包括:
|
||||
- 搜索动漫 - `/api/v2/search/anime`
|
||||
- 搜索剧集 - `/api/v2/search/episodes`
|
||||
- 匹配动漫 - `/api/v2/match`
|
||||
- 获取番剧详情 - `/api/v2/bangumi/:animeId`
|
||||
- 获取弹幕 - `/api/v2/comment/:commentId`
|
||||
|
||||
### 4. 推送弹幕
|
||||
|
||||
推送弹幕功能支持:
|
||||
|
||||
- 向 OK 影视等播放器推送弹幕
|
||||
- 搜索动漫并选择剧集
|
||||
- 推送地址格式如:`http://127.0.0.1:9978/action?do=refresh&type=danmaku&path=`
|
||||
- 需要在同一局域网或使用公网 IP
|
||||
|
||||
### 5. 系统配置
|
||||
|
||||
系统配置页面包含:
|
||||
|
||||
#### 环境变量管理
|
||||
- **API配置** - API 相关设置
|
||||
- **源配置** - 数据源配置
|
||||
- **匹配配置** - 匹配算法设置
|
||||
- **弹幕配置** - 弹幕相关参数
|
||||
- **缓存配置** - 缓存策略设置
|
||||
- **系统配置** - 系统级配置
|
||||
|
||||
#### 配置项类型
|
||||
- **文本** - 普通文本输入
|
||||
- **布尔值** - 开关选择
|
||||
- **数字** - 数字滚轮输入 (带范围限制)
|
||||
- **单选** - 从预定义选项中选择
|
||||
- **多选** - 选择多个选项并可拖动排序
|
||||
|
||||
#### 系统管理功能
|
||||
- **清理缓存** - 清除系统缓存(需要 ADMIN_TOKEN)
|
||||
- **重新部署** - 重新部署系统(需要 ADMIN_TOKEN)
|
||||
|
||||
## 安全说明
|
||||
|
||||
- 访问 UI 系统需要在 URL 中配置 TOKEN
|
||||
- 系统管理功能(日志查看、环境变量配置等)需要 ADMIN_TOKEN
|
||||
- 确保 TOKEN 和 ADMIN_TOKEN 的安全性
|
||||
|
||||
## 部署平台支持
|
||||
|
||||
系统支持多种部署平台:
|
||||
|
||||
- Node.js
|
||||
- Vercel
|
||||
- Netlify
|
||||
- Cloudflare
|
||||
- Docker
|
||||
- EdgeOne
|
||||
|
||||
根据部署平台的不同,可能需要配置额外的环境变量。
|
||||
|
||||
## 部署平台环境变量配置指南
|
||||
|
||||
### 平台与所需变量对照表
|
||||
|
||||
| 平台 | DEPLOY_PLATFROM_ACCOUNT | DEPLOY_PLATFROM_PROJECT | DEPLOY_PLATFROM_TOKEN |
|
||||
|------|----------------------|----------------------|---------------------|
|
||||
| Vercel | ❌ | ✅ | ✅ |
|
||||
| Netlify | ✅ | ✅ | ✅ |
|
||||
| EdgeOne | ❌ | ✅ | ✅ |
|
||||
| Cloudflare | ✅ | ✅ | ✅ |
|
||||
| Node.js | ❌ | ❌ | ❌ |
|
||||
| Docker | ❌ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
### 各平台变量获取详细步骤
|
||||
|
||||
#### 1. Vercel 平台
|
||||
|
||||
#### 需要的变量
|
||||
- `DEPLOY_PLATFROM_PROJECT`: 项目 ID
|
||||
- `DEPLOY_PLATFROM_TOKEN`: API Token
|
||||
|
||||
#### 获取步骤
|
||||
|
||||
**获取 Project ID (`DEPLOY_PLATFROM_PROJECT`)**
|
||||
|
||||
1. 登录 [Vercel Dashboard](https://vercel.com/dashboard)
|
||||
2. 选择你的项目
|
||||
3. 进入项目后,点击 **Settings** 标签
|
||||
4. 在左侧菜单中选择 **General**
|
||||
5. 向下滚动找到 **Project ID** 部分
|
||||
6. 复制显示的项目 ID(格式类似: `prj_xxxxxxxxxxxx`)
|
||||
|
||||
**获取 API Token (`DEPLOY_PLATFROM_TOKEN`)**
|
||||
|
||||
1. 点击右上角头像,选择 **Settings**
|
||||
2. 在左侧菜单中选择 **Tokens**
|
||||
3. 点击 **Create Token** 按钮
|
||||
4. 输入 Token 名称(如: `environment-variables-api`)
|
||||
5. 选择 **Scope**:
|
||||
- 可以选择 **Full Account** 或特定项目
|
||||
- 建议选择特定项目以提高安全性
|
||||
6. 设置过期时间(可选)
|
||||
7. 点击 **Create** 创建 Token
|
||||
8. **立即复制并保存** Token(只显示一次)
|
||||
|
||||
---
|
||||
|
||||
### 2. Netlify 平台
|
||||
|
||||
#### 需要的变量
|
||||
- `DEPLOY_PLATFROM_ACCOUNT`: 账户 ID
|
||||
- `DEPLOY_PLATFROM_PROJECT`: 站点 ID
|
||||
- `DEPLOY_PLATFROM_TOKEN`: Personal Access Token
|
||||
|
||||
#### 获取步骤
|
||||
|
||||
**获取 Account ID (`DEPLOY_PLATFROM_ACCOUNT`)**
|
||||
|
||||
1. 登录 [Netlify Dashboard](https://app.netlify.com/)
|
||||
2. 点击左下角头像,选择 **User settings**
|
||||
3. 点击 **Team settings** 可以看到你的 Account Slug
|
||||
4. 或者在左侧菜单选择 **Applications**
|
||||
5. 在 API 端点中可以找到 Account ID
|
||||
|
||||
**获取 Site ID (`DEPLOY_PLATFROM_PROJECT`)**
|
||||
|
||||
1. 在 Netlify Dashboard 中选择你的项目
|
||||
2. 进入项目后,点击 **Project configuration**
|
||||
3. 在 **General** > **Project details** 部分
|
||||
4. 找到 **Project information** 下的 **Project ID**
|
||||
5. 复制显示的站点 ID(格式类似: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)
|
||||
|
||||
**获取 Personal Access Token (`DEPLOY_PLATFROM_TOKEN`)**
|
||||
|
||||
1. 点击左下角头像,选择 **User settings**
|
||||
2. 在左侧菜单中选择 **Applications**
|
||||
3. 滚动到 **Personal access tokens** 部分
|
||||
4. 点击 **New access token** 按钮
|
||||
5. 输入 Token 描述(如: `Environment Variables API`)
|
||||
6. 点击 **Generate token**
|
||||
7. **立即复制并保存** Token(只显示一次)
|
||||
|
||||
---
|
||||
|
||||
### 3. EdgeOne (腾讯云 Pages) 平台
|
||||
|
||||
#### 需要的变量
|
||||
- `DEPLOY_PLATFROM_PROJECT`: 项目 ID
|
||||
- `DEPLOY_PLATFROM_TOKEN`: API 密钥
|
||||
|
||||
#### 获取步骤
|
||||
|
||||
**获取 Project ID (`DEPLOY_PLATFROM_PROJECT`)**
|
||||
|
||||
1. 登录 [腾讯云 EdgeOne 控制台](https://console.cloud.tencent.com/edgeone)
|
||||
2. 进入 **Pages** 服务
|
||||
3. 选择你的项目
|
||||
4. 在URL可以看到项目 ID(格式类似: `pages-xxxxxxxxxxxx`)
|
||||
|
||||
**获取 API 密钥 (`DEPLOY_PLATFROM_TOKEN`)**
|
||||
|
||||
1. 登录 [腾讯云 EdgeOne 控制台](https://console.cloud.tencent.com/edgeone)
|
||||
2. 进入 **Pages** 服务
|
||||
3. 选择 **API Token** 标签页
|
||||
4. 点击 **创建 API Token** 按钮
|
||||
5. 输入描述和过期时间,点击提交后复制相应Token
|
||||
|
||||
---
|
||||
|
||||
### 4. Cloudflare 平台
|
||||
|
||||
#### 需要的变量
|
||||
- `DEPLOY_PLATFROM_ACCOUNT`: 账户 ID
|
||||
- `DEPLOY_PLATFROM_PROJECT`: Workers 脚本名称
|
||||
- `DEPLOY_PLATFROM_TOKEN`: API Token
|
||||
|
||||
#### 获取步骤
|
||||
|
||||
**获取 Account ID (`DEPLOY_PLATFROM_ACCOUNT`)**
|
||||
|
||||
1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
||||
2. 在右侧可以看到 **Account ID**
|
||||
3. 或者点击任意域名,在右侧栏可以找到 **Account ID**
|
||||
4. 复制该 ID(格式类似: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
|
||||
|
||||
**获取 Workers 脚本名称 (`DEPLOY_PLATFROM_PROJECT`)**
|
||||
|
||||
1. 在 Cloudflare Dashboard 左侧菜单选择 **Workers & Pages**
|
||||
2. 找到你的 Workers 脚本
|
||||
3. 脚本名称就是列表中显示的名称
|
||||
4. 或者点击进入脚本详情,在 URL 中可以看到脚本名称
|
||||
|
||||
**获取 API Token (`DEPLOY_PLATFROM_TOKEN`)**
|
||||
|
||||
1. 点击右上角头像,选择 **配置文件**
|
||||
2. 在左侧菜单选择 **API Tokens**
|
||||
3. 点击 **Create Token** 按钮
|
||||
4. 可以选择模板或自定义:
|
||||
- 选择 **Edit Cloudflare Workers** 模板
|
||||
- 或创建 **Custom token**
|
||||
5. 配置权限:
|
||||
- **Account** > **Workers Scripts** > **Edit**
|
||||
- 选择特定账户
|
||||
6. (可选)设置 IP 限制和 TTL
|
||||
7. 点击 **Continue to summary**
|
||||
8. 确认后点击 **Create Token**
|
||||
9. **立即复制并保存** Token(只显示一次)
|
||||
|
||||
---
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Q: Token 创建后忘记复制怎么办?**
|
||||
A: 大多数平台的 Token 只显示一次,如果忘记复制需要删除后重新创建。
|
||||
|
||||
## API 端点
|
||||
|
||||
在 UI 页面顶部显示当前 API 端点,可点击复制到剪贴板。
|
||||
|
||||
## 故障排除
|
||||
|
||||
- 如果无法访问功能页面,请检查 URL 中的 TOKEN 是否正确
|
||||
- 如果系统管理功能不可用,请确认 ADMIN_TOKEN 等环境变量是否已配置
|
||||
- 如遇到其他问题,请查看系统日志获取更多信息
|
||||
@@ -0,0 +1,165 @@
|
||||
// language=CSS
|
||||
export const baseCssContent = /* css */ `
|
||||
/* 基础样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
overflow: hidden; /* 关键:防止溢出 */
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 100%; /* 或 90% 留点内边距更好看 */
|
||||
height: 100%;
|
||||
object-fit: cover; /* 图片会被裁剪成正方形,充满容器 */
|
||||
/* object-fit: contain; /* 如果想完整显示图片(会留白)就用这个 */
|
||||
border-radius: 12px; /* 让图片也跟随圆角 */
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.update-badge {
|
||||
background: #ffd700;
|
||||
color: #333;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
animation: pulse 2s infinite;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* footer 样式 */
|
||||
.footer {
|
||||
padding: 20px;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* footer 文字样式 */
|
||||
.footer-text {
|
||||
color: #ffeb3b;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* footer 链接样式 */
|
||||
.footer-links {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
margin: 0 10px;
|
||||
text-decoration: none;
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #ffeb3b; /* 可选,增加 hover 效果 */
|
||||
}
|
||||
|
||||
/* GitHub 链接特定样式 */
|
||||
.github-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.github-icon {
|
||||
width: 14px;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,710 @@
|
||||
// language=CSS
|
||||
export const componentsCssContent = /* css */ `
|
||||
/* 组件样式 */
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
background: white;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* 环境变量样式 */
|
||||
.env-categories {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
padding: 10px 20px;
|
||||
background: #f0f0f0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.env-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.env-item {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.env-item .env-info {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.env-item .env-info strong {
|
||||
color: #667eea;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.env-item .env-info > div.text-dark-gray {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
background-color: #f1f3f5;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.env-item .env-info span {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.env-item .env-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.env-info {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.env-info strong {
|
||||
color: #667eea;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.env-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-success::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.btn-success:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* 预览区域 */
|
||||
.preview-area {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #667eea;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.preview-item .preview-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.preview-item .preview-key {
|
||||
font-weight: bold;
|
||||
color: #67eea;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.preview-item .preview-value {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
background-color: #f8f9fa;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
color: #333; /* 更黑的字体颜色 */
|
||||
font-weight: bold; /* 加粗显示 */
|
||||
}
|
||||
|
||||
/* 日志样式 */
|
||||
.log-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.5); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 8px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-entry.info { color: #4fc3f7; }
|
||||
.log-entry.warn { color: #ffb74d; }
|
||||
.log-entry.error { color: #e57373; }
|
||||
.log-entry.success { color: #81c784; }
|
||||
|
||||
|
||||
|
||||
/* 表单帮助文本 */
|
||||
.form-help {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* API调试样式 */
|
||||
.api-selector {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.api-params {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.api-response {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* XML响应样式 */
|
||||
.api-response.xml {
|
||||
color: #88ccff;
|
||||
}
|
||||
|
||||
/* JSON高亮样式 */
|
||||
.json-response {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.json-response .key {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.json-response .string {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.json-response .number {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.json-response .boolean {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.json-response .null {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.json-response .undefined {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.error-response {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
/* 模态框 */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
color: #667eea;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.confirmation-list {
|
||||
padding-left: 20px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.confirmation-list li {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.confirmation-list li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
color: #667eea;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 值类型标识 */
|
||||
.value-type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 确认模态框样式 */
|
||||
.confirmation-list {
|
||||
padding-left: 20px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.confirmation-list li {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.confirmation-list li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 25px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.value-type-badge.multi {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
.progress-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: rgba(0,0,0,0.1);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
width: 0;
|
||||
transition: width 0.3s;
|
||||
box-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
/* 加载提示 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
.loading-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 5px solid #f3f3f3;
|
||||
border-top: 5px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.loading-spinner-small {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #ffffff;
|
||||
border-top: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading-detail {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 通用内联样式类 */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #bbb; /* 更淡的字体颜色 */
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.text-dark-gray {
|
||||
color: #333; /* 更黑的字体颜色 */
|
||||
font-weight: bold; /* 加粗显示 */
|
||||
}
|
||||
|
||||
.text-purple {
|
||||
color: #67eea;
|
||||
}
|
||||
|
||||
.text-yellow-gold {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.padding-20 {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.margin-bottom-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.margin-top-3 {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.margin-top-15 {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.font-size-12 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.margin-bottom-15 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.text-monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* 推送弹幕相关样式 */
|
||||
.anime-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.anime-item {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anime-item-img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.anime-title {
|
||||
margin: 8px 0 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.episode-list-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.episode-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.episode-item-content {
|
||||
display: inline-block;
|
||||
width: calc(100% - 100px);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.episode-push-btn {
|
||||
width: 80px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,318 @@
|
||||
// language=CSS
|
||||
export const formsCssContent = /* css */ `
|
||||
/* 表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
font-family: 'Courier New', Consolas, monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 开关按钮 */
|
||||
.switch-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 26px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #667eea;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 数字滚轮 */
|
||||
.number-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.number-display {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.number-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.number-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid #667eea;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.number-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.number-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.number-range {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.number-range input[type="range"] {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #ddd;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.number-range input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.number-range input[type="range"]::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 标签选择 */
|
||||
.tag-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tag-option {
|
||||
padding: 10px 20px;
|
||||
background: #f0f0f0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-option:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.tag-option.selected {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.tag-option.selected:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
/* 多选标签 */
|
||||
.multi-select-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.selected-tags {
|
||||
min-height: 60px;
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.selected-tags.empty::before {
|
||||
content: '拖动或点击下方选项添加...';
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.selected-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.selected-tag:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.selected-tag.dragging {
|
||||
opacity: 0.5;
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
.selected-tag .tag-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selected-tag .remove-btn {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.selected-tag .remove-btn:hover {
|
||||
background: rgba(255,255,255,0.5);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.available-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.available-tag {
|
||||
padding: 8px 16px;
|
||||
background: #f0f0f0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.available-tag:hover {
|
||||
background: #e0e0e0;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.available-tag.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.available-tag.disabled:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
background: #e8eaf6 !important;
|
||||
border-color: #667eea !important;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,68 @@
|
||||
// language=CSS
|
||||
export const responsiveCssContent = /* css */ `
|
||||
/* 响应式样式 */
|
||||
@media (max-width: 768px) {
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.logo-title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
font-size: 11px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
flex: 1 1 calc(33.333% - 5px);
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
padding: 8px 5px;
|
||||
white-space: nowrap;
|
||||
min-width: 70px;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.env-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.env-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,230 @@
|
||||
// language=JavaScript
|
||||
export const apitestJsContent = /* javascript */ `
|
||||
// API 配置
|
||||
const apiConfigs = {
|
||||
searchAnime: {
|
||||
name: '搜索动漫',
|
||||
method: 'GET',
|
||||
path: '/api/v2/search/anime',
|
||||
params: [
|
||||
{ name: 'keyword', label: '关键词', type: 'text', required: true, placeholder: '示例: 生万物' }
|
||||
]
|
||||
},
|
||||
searchEpisodes: {
|
||||
name: '搜索剧集',
|
||||
method: 'GET',
|
||||
path: '/api/v2/search/episodes',
|
||||
params: [
|
||||
{ name: 'anime', label: '动漫名称', type: 'text', required: true, placeholder: '示例: 生万物' }
|
||||
]
|
||||
},
|
||||
matchAnime: {
|
||||
name: '匹配动漫',
|
||||
method: 'POST',
|
||||
path: '/api/v2/match',
|
||||
params: [
|
||||
{ name: 'fileName', label: '文件名', type: 'text', required: true, placeholder: '示例: 生万物 S02E08, 无忧渡.S02E08.2160p.WEB-DL.H265.DDP.5.1, 爱情公寓.ipartment.2009.S02E08.H.265.25fps.mkv, 亲爱的X S02E08, 宇宙Marry Me? S02E08' }
|
||||
]
|
||||
},
|
||||
getBangumi: {
|
||||
name: '获取番剧详情',
|
||||
method: 'GET',
|
||||
path: '/api/v2/bangumi/:animeId',
|
||||
params: [
|
||||
{ name: 'animeId', label: '动漫ID', type: 'text', required: true, placeholder: '示例: 236379' }
|
||||
]
|
||||
},
|
||||
getComment: {
|
||||
name: '获取弹幕',
|
||||
method: 'GET',
|
||||
path: '/api/v2/comment/:commentId',
|
||||
params: [
|
||||
{ name: 'commentId', label: '弹幕ID', type: 'text', required: true, placeholder: '示例: 10009' },
|
||||
{ name: 'format', label: '格式', type: 'select', required: false, placeholder: '可选: json或xml', options: ['json', 'xml'] }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// 接口调试相关
|
||||
function loadApiParams() {
|
||||
const select = document.getElementById('api-select');
|
||||
const apiKey = select.value;
|
||||
const paramsDiv = document.getElementById('api-params');
|
||||
const formDiv = document.getElementById('params-form');
|
||||
|
||||
if (!apiKey) {
|
||||
paramsDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const config = apiConfigs[apiKey];
|
||||
paramsDiv.style.display = 'block';
|
||||
|
||||
if (config.params.length === 0) {
|
||||
formDiv.innerHTML = '<p class="text-gray">此接口无需参数</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
formDiv.innerHTML = config.params.map(param => {
|
||||
if (param.type === 'select') {
|
||||
// 为select类型参数添加默认选项
|
||||
let optionsHtml = '<option value="">-- 请选择 --</option>';
|
||||
if (param.options) {
|
||||
optionsHtml += param.options.map(opt => \`<option value="\${opt}">\${opt}</option>\`).join('');
|
||||
}
|
||||
return \`
|
||||
<div class="form-group">
|
||||
<label>\${param.label}\${param.required ? ' *' : ''}</label>
|
||||
<select id="param-\${param.name}">
|
||||
\${optionsHtml}
|
||||
</select>
|
||||
\${param.placeholder ? \`<div class="form-help">\${param.placeholder}</div>\` : ''}
|
||||
</div>
|
||||
\`;
|
||||
}
|
||||
// 使用placeholder属性显示示例参数
|
||||
const placeholder = param.placeholder ? param.placeholder : "请输入" + param.label;
|
||||
return \`
|
||||
<div class="form-group">
|
||||
<label>\${param.label}\${param.required ? ' *' : ''}</label>
|
||||
<input type="\${param.type}" id="param-\${param.name}" placeholder="\${placeholder}" \${param.required ? 'required' : ''}>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function testApi() {
|
||||
const select = document.getElementById('api-select');
|
||||
const apiKey = select.value;
|
||||
const sendButton = document.querySelector('#api-params .btn-success'); // 获取发送请求按钮
|
||||
|
||||
if (!apiKey) {
|
||||
addLog('请先选择接口', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置按钮为加载状态
|
||||
const originalText = sendButton.innerHTML;
|
||||
sendButton.innerHTML = '<span class="loading-spinner-small"></span>';
|
||||
sendButton.disabled = true;
|
||||
|
||||
const config = apiConfigs[apiKey];
|
||||
const params = {};
|
||||
|
||||
config.params.forEach(param => {
|
||||
const value = document.getElementById(\`param-\${param.name}\`).value;
|
||||
if (value) params[param.name] = value;
|
||||
});
|
||||
|
||||
addLog(\`调用接口: \${config.name} (\${config.method} \${config.path})\`, 'info');
|
||||
addLog(\`请求参数: \${JSON.stringify(params)}\`, 'info');
|
||||
|
||||
// 构建请求URL
|
||||
let url = config.path;
|
||||
|
||||
// 检查是否为路径参数接口
|
||||
const isPathParameterApi = config.path.includes(':');
|
||||
|
||||
if (isPathParameterApi) {
|
||||
// 处理路径参数接口 (/api/v2/comment 和 /api/v2/bangumi)
|
||||
// 先分离路径参数和查询参数
|
||||
const pathParams = {};
|
||||
const queryParams = {};
|
||||
|
||||
// 分类参数
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
// 检查参数是否为路径参数
|
||||
if (config.path.includes(':' + key)) {
|
||||
pathParams[key] = value;
|
||||
} else {
|
||||
// 其他参数作为查询参数
|
||||
queryParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 替换路径参数
|
||||
for (const [key, value] of Object.entries(pathParams)) {
|
||||
url = url.replace(':' + key, encodeURIComponent(value));
|
||||
}
|
||||
|
||||
// 添加查询参数
|
||||
if (config.method === 'GET' && Object.keys(queryParams).length > 0) {
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
url = url + '?' + queryString;
|
||||
}
|
||||
} else {
|
||||
// 保持原来的逻辑,用于 search/anime 等接口
|
||||
if (config.method === 'GET') {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
url = url + '?' + queryString;
|
||||
}
|
||||
}
|
||||
|
||||
// 配置请求选项
|
||||
const requestOptions = {
|
||||
method: config.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (config.method === 'POST') {
|
||||
requestOptions.body = JSON.stringify(params);
|
||||
}
|
||||
|
||||
// 发送真实API请求
|
||||
fetch(buildApiUrl(url), requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error! status: \${response.status}\`);
|
||||
}
|
||||
|
||||
// 检查format参数以确定如何处理响应
|
||||
const formatParam = params.format || 'json';
|
||||
|
||||
if (formatParam.toLowerCase() === 'xml') {
|
||||
// 对于XML格式,返回文本内容
|
||||
return response.text().then(text => ({
|
||||
data: text,
|
||||
format: 'xml'
|
||||
}));
|
||||
} else {
|
||||
// 对于JSON格式或其他情况,返回JSON对象
|
||||
return response.json().then(json => ({
|
||||
data: json,
|
||||
format: 'json'
|
||||
}));
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
// 显示响应结果
|
||||
document.getElementById('api-response-container').style.display = 'block';
|
||||
|
||||
if (result.format === 'xml') {
|
||||
// 显示XML响应
|
||||
document.getElementById('api-response').textContent = result.data;
|
||||
document.getElementById('api-response').className = 'api-response xml'; // 使用XML专用样式类
|
||||
} else {
|
||||
// 显示JSON响应
|
||||
document.getElementById('api-response').className = 'json-response';
|
||||
document.getElementById('api-response').innerHTML = highlightJSON(result.data);
|
||||
}
|
||||
|
||||
addLog('接口调用成功', 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
// 处理错误
|
||||
const errorMessage = \`API请求失败: \${error.message}\`;
|
||||
document.getElementById('api-response-container').style.display = 'block';
|
||||
document.getElementById('api-response').textContent = errorMessage;
|
||||
// 添加错误信息的CSS类
|
||||
document.getElementById('api-response').className = 'error-response';
|
||||
addLog(errorMessage, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
sendButton.innerHTML = originalText;
|
||||
sendButton.disabled = false;
|
||||
});
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,122 @@
|
||||
// language=JavaScript
|
||||
export const logviewJsContent = /* javascript */ `
|
||||
// 日志相关
|
||||
function addLog(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logs.push({ timestamp, message, type });
|
||||
if (logs.length > 100) logs.shift();
|
||||
renderLogs();
|
||||
}
|
||||
|
||||
function renderLogs() {
|
||||
const container = document.getElementById('log-container');
|
||||
container.innerHTML = logs.map(log =>
|
||||
\`<div class="log-entry \${log.type}">[\${log.timestamp}] \${log.message}</div>\`
|
||||
).join('');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// 从API获取真实日志数据
|
||||
async function fetchRealLogs() {
|
||||
try {
|
||||
// 日志查看使用普通token访问,不需要admin token
|
||||
const response = await fetch(buildApiUrl('/api/logs')); // 不使用admin token
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error! status: \${response.status}\`);
|
||||
}
|
||||
const logText = await response.text();
|
||||
// 解析日志文本为数组
|
||||
const logLines = logText.split('\\n').filter(line => line.trim() !== '');
|
||||
// 转换为logs数组格式
|
||||
logs = logLines.map(line => {
|
||||
// 解析日志行,提取时间戳、级别和消息
|
||||
const match = line.match(/\\[([^\\]]+)\\] (\\w+): (.*)/);
|
||||
if (match) {
|
||||
return {
|
||||
timestamp: match[1],
|
||||
type: match[2],
|
||||
message: match[3]
|
||||
};
|
||||
}
|
||||
// 如果无法解析,返回原始行
|
||||
return {
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
type: 'info',
|
||||
message: line
|
||||
};
|
||||
});
|
||||
renderLogs();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch logs:', error);
|
||||
addLog(\`获取日志失败: \${error.message}\`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshLogs() {
|
||||
// 从API获取真实日志数据
|
||||
fetchRealLogs();
|
||||
}
|
||||
|
||||
async function clearLogs() {
|
||||
// 检查部署平台配置
|
||||
const configCheck = await checkDeployPlatformConfig();
|
||||
if (!configCheck.success) {
|
||||
customAlert(configCheck.message);
|
||||
return;
|
||||
}
|
||||
|
||||
customConfirm('确定要清空所有日志吗?', '清空确认').then(async confirmed => {
|
||||
if (confirmed) {
|
||||
try {
|
||||
const response = await fetch(buildApiUrl('/api/logs/clear', true), { // 使用admin token
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error! status: \${response.status}\`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// 清空前端显示的日志
|
||||
logs = [];
|
||||
renderLogs();
|
||||
addLog('日志已清空', 'warn');
|
||||
} else {
|
||||
addLog(\`清空日志失败: \${result.message}\`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear logs:', error);
|
||||
addLog(\`清空日志失败: \${error.message}\`, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// JSON高亮函数
|
||||
function highlightJSON(obj) {
|
||||
let json = JSON.stringify(obj, null, 2);
|
||||
// 转义HTML特殊字符
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// 高亮JSON语法
|
||||
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||
let cls = 'number';
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'key';
|
||||
} else {
|
||||
cls = 'string';
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'boolean';
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'null';
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>';
|
||||
});
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,390 @@
|
||||
// language=JavaScript
|
||||
export const mainJsContent = /* javascript */ `
|
||||
// 自定义弹窗组件
|
||||
function createCustomAlert() {
|
||||
// 检查是否已存在自定义弹窗元素
|
||||
if (document.getElementById('custom-alert-overlay')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建弹窗HTML元素
|
||||
const alertHTML = '<div class="modal" id="custom-alert-overlay"><div class="modal-content" id="custom-alert-content"><div class="modal-header"><h3 id="custom-alert-title">提示</h3><button class="close-btn" id="custom-alert-close">×</button></div><div class="modal-body"><p id="custom-alert-message"></p></div><div class="modal-footer"><button class="btn btn-primary" id="custom-alert-confirm">确定</button></div></div></div>';
|
||||
|
||||
// 添加到body
|
||||
document.body.insertAdjacentHTML('beforeend', alertHTML);
|
||||
|
||||
// 获取元素
|
||||
const overlay = document.getElementById('custom-alert-overlay');
|
||||
const closeBtn = document.getElementById('custom-alert-close');
|
||||
const confirmBtn = document.getElementById('custom-alert-confirm');
|
||||
|
||||
// 关闭弹窗函数
|
||||
function closeAlert() {
|
||||
overlay.classList.remove('active');
|
||||
// 重置标题和消息
|
||||
document.getElementById('custom-alert-title').textContent = '提示';
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
closeBtn.addEventListener('click', closeAlert);
|
||||
confirmBtn.addEventListener('click', closeAlert);
|
||||
|
||||
// 点击遮罩层关闭弹窗
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) {
|
||||
closeAlert();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 自定义alert函数
|
||||
function customAlert(message, title = '提示') {
|
||||
// 确保弹窗元素已创建
|
||||
createCustomAlert();
|
||||
|
||||
// 获取元素
|
||||
const overlay = document.getElementById('custom-alert-overlay');
|
||||
const titleElement = document.getElementById('custom-alert-title');
|
||||
const messageElement = document.getElementById('custom-alert-message');
|
||||
|
||||
// 设置标题和消息
|
||||
titleElement.textContent = title;
|
||||
messageElement.textContent = message;
|
||||
|
||||
// 显示弹窗
|
||||
overlay.classList.add('active');
|
||||
}
|
||||
|
||||
// 自定义confirm函数(如果需要)
|
||||
function customConfirm(message, title = '确认') {
|
||||
return new Promise((resolve) => {
|
||||
// 确保弹窗元素已创建
|
||||
createCustomAlert();
|
||||
|
||||
// 获取元素
|
||||
const overlay = document.getElementById('custom-alert-overlay');
|
||||
const titleElement = document.getElementById('custom-alert-title');
|
||||
const messageElement = document.getElementById('custom-alert-message');
|
||||
const confirmBtn = document.getElementById('custom-alert-confirm');
|
||||
|
||||
// 移除之前的事件监听器(如果有)
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
|
||||
// 设置标题和消息
|
||||
titleElement.textContent = title;
|
||||
messageElement.textContent = message;
|
||||
|
||||
// 确定按钮事件
|
||||
newConfirmBtn.addEventListener('click', () => {
|
||||
overlay.classList.remove('active');
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// 关闭按钮事件
|
||||
document.getElementById('custom-alert-close').addEventListener('click', () => {
|
||||
overlay.classList.remove('active');
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
// 点击遮罩层关闭
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) {
|
||||
overlay.classList.remove('active');
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 显示弹窗
|
||||
overlay.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化自定义弹窗
|
||||
document.addEventListener('DOMContentLoaded', createCustomAlert);
|
||||
|
||||
// 数据存储
|
||||
let envVariables = {};
|
||||
let currentCategory = 'api'; // 默认分类改为api
|
||||
let editingKey = null;
|
||||
let logs = []; // 保留本地日志数组,用于UI显示
|
||||
|
||||
// 版本信息
|
||||
let currentVersion = '';
|
||||
let latestVersion = '';
|
||||
let currentToken = 'globals.currentToken';
|
||||
let currentAdminToken = ''; // admin token,用于系统管理
|
||||
let originalToken = '';
|
||||
|
||||
// 构建带token的API请求路径
|
||||
function buildApiUrl(path, isSystemPath = false) {
|
||||
// 如果是系统管理路径且有admin token,则使用admin token
|
||||
if (isSystemPath && currentAdminToken && currentAdminToken.trim() !== '' && currentAdminToken.trim() !== '*'.repeat(currentAdminToken.length)) {
|
||||
return '/' + currentAdminToken + path;
|
||||
}
|
||||
// 否则使用普通token
|
||||
return (currentToken ? '/' + currentToken : "") + path;
|
||||
}
|
||||
|
||||
// 从API加载真实环境变量数据
|
||||
function loadEnvVariables() {
|
||||
// 从API获取真实配置数据
|
||||
fetch(buildApiUrl('/api/config', true))
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
// 从配置中获取admin token
|
||||
currentAdminToken = config.originalEnvVars?.ADMIN_TOKEN || '';
|
||||
|
||||
originalToken = config.originalEnvVars?.TOKEN || '';
|
||||
|
||||
// 使用从API获取的原始环境变量,用于系统设置
|
||||
const originalEnvVars = config.originalEnvVars || {};
|
||||
|
||||
// 重新组织数据结构以适配现有UI
|
||||
envVariables = {};
|
||||
|
||||
// 将原始环境变量转换为UI所需格式
|
||||
// 这里需要将原始环境变量按类别组织
|
||||
Object.keys(originalEnvVars).forEach(key => {
|
||||
// 从envVarConfig获取配置信息
|
||||
const varConfig = config.envVarConfig?.[key] || { category: 'system', type: 'text', description: '未分类配置项' };
|
||||
const category = varConfig.category || 'system';
|
||||
|
||||
// 如果该分类不存在,创建它
|
||||
if (!envVariables[category]) {
|
||||
envVariables[category] = [];
|
||||
}
|
||||
|
||||
// 添加到对应分类,包含完整的配置信息
|
||||
envVariables[category].push({
|
||||
key: key,
|
||||
value: originalEnvVars[key],
|
||||
description: varConfig.description || '',
|
||||
type: varConfig.type || 'text',
|
||||
min: varConfig.min,
|
||||
max: varConfig.max,
|
||||
options: varConfig.options || [] // 仅对 select 和 multi-select 类型有效
|
||||
});
|
||||
});
|
||||
|
||||
// 渲染环境变量列表
|
||||
renderEnvList();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load env variables:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新API端点信息
|
||||
function updateApiEndpoint() {
|
||||
return fetch(buildApiUrl('/api/config', true))
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
// 获取当前页面的协议、主机和端口
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
const token = config.originalEnvVars?.TOKEN || '87654321'; // 默认token值
|
||||
const adminToken = config.originalEnvVars?.ADMIN_TOKEN;
|
||||
|
||||
// 获取URL路径并提取token
|
||||
const urlPath = window.location.pathname;
|
||||
const pathParts = urlPath.split('/').filter(part => part !== '');
|
||||
const urlToken = pathParts.length > 0 ? pathParts[0] : '';
|
||||
let apiToken = '********';
|
||||
|
||||
// 判断是否使用默认token
|
||||
if (token === '87654321') {
|
||||
// 如果是默认token,则显示真实token
|
||||
apiToken = token;
|
||||
} else {
|
||||
// 如果不是默认token,则检查URL中的token是否匹配,匹配则显示真实token,否则显示星号
|
||||
if (urlToken === token || (adminToken !== "" && urlToken === adminToken)) {
|
||||
apiToken = token; // 更新全局token变量
|
||||
}
|
||||
}
|
||||
|
||||
// 构造API端点URL
|
||||
const apiEndpoint = protocol + '//' + host + '/' + apiToken;
|
||||
const apiEndpointElement = document.getElementById('api-endpoint');
|
||||
if (apiEndpointElement) {
|
||||
apiEndpointElement.textContent = apiEndpoint;
|
||||
}
|
||||
return config; // 返回配置信息,以便链式调用
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取配置信息失败:', error);
|
||||
// 出错时显示默认值
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
const apiEndpoint = protocol + '//' + host + '/********';
|
||||
const apiEndpointElement = document.getElementById('api-endpoint');
|
||||
if (apiEndpointElement) {
|
||||
apiEndpointElement.textContent = apiEndpoint;
|
||||
}
|
||||
throw error; // 抛出错误,以便调用者可以处理
|
||||
});
|
||||
}
|
||||
|
||||
function getDockerVersion() {
|
||||
const url = "https://img.shields.io/docker/v/logvar/danmu-api?sort=semver";
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(svgContent => {
|
||||
// 使用正则表达式从 SVG 中提取版本号
|
||||
const versionMatch = svgContent.match(/version<\\/text><text.*?>(v[\\d\\.]+)/);
|
||||
|
||||
if (versionMatch && versionMatch[1]) {
|
||||
console.log("Version:", versionMatch[1]);
|
||||
const latestVersionElement = document.getElementById('latest-version');
|
||||
if (latestVersionElement) {
|
||||
latestVersionElement.textContent = versionMatch[1];
|
||||
}
|
||||
} else {
|
||||
console.log("Version not found");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching the SVG:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// 切换导航
|
||||
function switchSection(section) {
|
||||
// 检查是否尝试访问受token保护的section(日志查看、接口调试、系统配置需要token访问)
|
||||
if (section === 'logs' || section === 'api' || section === 'env' || section === 'push') {
|
||||
// 获取URL路径并提取token
|
||||
const urlPath = window.location.pathname;
|
||||
const pathParts = urlPath.split('/').filter(part => part !== '');
|
||||
const urlToken = pathParts.length > 0 ? pathParts[0] : '';
|
||||
|
||||
// 检查URL中是否有token
|
||||
if (!urlToken && originalToken !== "87654321") {
|
||||
// 提示用户需要在URL中配置TOKEN
|
||||
setTimeout(() => {
|
||||
// 获取当前页面的协议、主机和端口
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
customAlert('请在URL中配置相应的TOKEN以访问此功能!\\n\\n访问方式:' + protocol + '//' + host + '/{TOKEN}');
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是系统配置页面,还需要检查是否配置了ADMIN_TOKEN且URL中的token等于currentAdminToken
|
||||
if (section === 'env') {
|
||||
// 检查部署平台配置
|
||||
checkDeployPlatformConfig().then(result => {
|
||||
if (!result.success) {
|
||||
// 如果配置检查不通过,只显示提示,不切换页面
|
||||
setTimeout(() => {
|
||||
customAlert(result.message);
|
||||
}, 100);
|
||||
} else {
|
||||
// 如果配置检查通过,才切换到env页面
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||||
|
||||
document.getElementById(\`\${section}-section\`).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
addLog(\`切换到\${section === 'env' ? '环境变量' : section === 'preview' ? '配置预览' : section === 'logs' ? '日志查看' : section === 'push' ? '推送弹幕' : '接口调试'}模块\`, 'info');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 对于日志查看、接口调试和推送弹幕页面,只要URL中有token就可以访问
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||||
|
||||
document.getElementById(\`\${section}-section\`).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
addLog(\`切换到\${section === 'env' ? '环境变量' : section === 'preview' ? '配置预览' : section === 'logs' ? '日志查看' : section === 'push' ? '推送弹幕' : '接口调试'}模块\`, 'info');
|
||||
}
|
||||
} else {
|
||||
// 对于非受保护页面(如配置预览),正常切换
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||||
|
||||
document.getElementById(\`\${section}-section\`).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
addLog(\`切换到\${section === 'env' ? '环境变量' : section === 'preview' ? '配置预览' : section === 'logs' ? '日志查看' : section === 'push' ? '推送弹幕' : '接口调试'}模块\`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换类别
|
||||
function switchCategory(category) {
|
||||
currentCategory = category;
|
||||
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
renderEnvList();
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal() {
|
||||
document.getElementById('env-modal').classList.remove('active');
|
||||
|
||||
// 重置表单字段状态
|
||||
document.getElementById('env-category').disabled = false;
|
||||
document.getElementById('env-key').readOnly = false;
|
||||
document.getElementById('value-type').disabled = false;
|
||||
document.getElementById('env-description').readOnly = false;
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化时获取一次日志
|
||||
async function init() {
|
||||
try {
|
||||
await updateApiEndpoint(); // 等待API端点更新完成
|
||||
getDockerVersion();
|
||||
// 从API获取配置信息,包括检查是否有admin token
|
||||
await fetchAndSetConfig();
|
||||
|
||||
// 检查并处理管理员令牌
|
||||
checkAndHandleAdminToken();
|
||||
|
||||
loadEnvVariables(); // 从API加载真实环境变量数据
|
||||
renderEnvList();
|
||||
renderPreview();
|
||||
addLog('系统初始化完成', 'success');
|
||||
// 获取真实日志数据
|
||||
fetchRealLogs();
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error);
|
||||
addLog('系统初始化失败: ' + error.message, 'error');
|
||||
// 即使初始化失败,也要尝试获取日志
|
||||
fetchRealLogs();
|
||||
}
|
||||
}
|
||||
|
||||
// 复制API端点到剪贴板
|
||||
function copyApiEndpoint() {
|
||||
const apiEndpointElement = document.getElementById('api-endpoint');
|
||||
if (apiEndpointElement) {
|
||||
const apiEndpoint = apiEndpointElement.textContent;
|
||||
navigator.clipboard.writeText(apiEndpoint)
|
||||
.then(() => {
|
||||
// 临时改变显示文本以提供反馈
|
||||
const originalText = apiEndpointElement.textContent;
|
||||
apiEndpointElement.textContent = '已复制!';
|
||||
apiEndpointElement.style.color = '#ff6b6b';
|
||||
|
||||
// 2秒后恢复原始文本
|
||||
setTimeout(() => {
|
||||
apiEndpointElement.textContent = originalText;
|
||||
apiEndpointElement.style.color = '#4CAF50';
|
||||
}, 2000);
|
||||
|
||||
addLog('API端点已复制到剪贴板: ' + apiEndpoint, 'success');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
customAlert('复制失败: ' + err);
|
||||
addLog('复制API端点失败: ' + err, 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 页面加载完成后初始化
|
||||
init();
|
||||
`;
|
||||
@@ -0,0 +1,55 @@
|
||||
// language=JavaScript
|
||||
export const previewJsContent = /* javascript */ `
|
||||
// 渲染配置预览
|
||||
function renderPreview() {
|
||||
const preview = document.getElementById('preview-area');
|
||||
|
||||
// 从API获取真实配置数据
|
||||
fetch('/api/config')
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
// 使用从API获取的分类环境变量
|
||||
const categorizedVars = config.categorizedEnvVars || {};
|
||||
|
||||
// 渲染预览内容
|
||||
let html = '';
|
||||
|
||||
Object.keys(categorizedVars).forEach(category => {
|
||||
const items = categorizedVars[category];
|
||||
if (items && items.length > 0) {
|
||||
html += \`<h3 class="text-purple margin-bottom-10">\${getCategoryName(category)}</h3>\`;
|
||||
items.forEach(item => {
|
||||
html += \`
|
||||
<div class="preview-item">
|
||||
<div class="preview-item-content">
|
||||
<div class="preview-key"><strong>\${item.key}</strong></div>
|
||||
<div class="preview-value">\${item.value}</div>
|
||||
</div>
|
||||
\${item.description ? \`<div class="text-gray font-size-12 margin-top-3">\${item.description}</div>\` : ''}
|
||||
</div>
|
||||
\`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
preview.innerHTML = html || '<p class="text-gray">暂无配置</p>';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load config for preview:', error);
|
||||
preview.innerHTML = '<p class="text-red">加载配置失败: ' + error.message + '</p>';
|
||||
});
|
||||
}
|
||||
|
||||
// 获取类别名称
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
api: '🔗 API配置',
|
||||
source: '📜 源配置',
|
||||
match: '🔍 匹配配置',
|
||||
danmu: '🔣 弹幕配置',
|
||||
cache: '💾 缓存配置',
|
||||
system: '⚙️ 系统配置'
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,180 @@
|
||||
// language=JavaScript
|
||||
export const pushDanmuJsContent = /* javascript */ `
|
||||
// 推送弹幕功能相关
|
||||
|
||||
// 搜索动漫用于推送
|
||||
function searchAnimeForPush() {
|
||||
const keyword = document.getElementById('push-search-keyword').value.trim();
|
||||
const pushUrl = document.getElementById('push-url').value.trim();
|
||||
const searchBtn = document.querySelector('#push-section .btn-primary');
|
||||
|
||||
if (!keyword) {
|
||||
customAlert('请输入搜索关键字');
|
||||
return;
|
||||
}
|
||||
|
||||
// 搜索时不再校验pushUrl是否为空,只在推送时校验
|
||||
|
||||
// 添加加载状态
|
||||
const originalText = searchBtn.textContent;
|
||||
searchBtn.innerHTML = '<span class="loading-spinner-small"></span>';
|
||||
searchBtn.disabled = true;
|
||||
|
||||
// 构建搜索API请求URL
|
||||
const searchUrl = buildApiUrl('/api/v2/search/anime?keyword=' + encodeURIComponent(keyword));
|
||||
|
||||
addLog(\`开始搜索动漫: \${keyword}\`, 'info');
|
||||
|
||||
// 发送搜索请求
|
||||
fetch(searchUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error! status: \${response.status}\`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success && data.animes.length > 0) {
|
||||
displayAnimeListForPush(data.animes, pushUrl);
|
||||
} else {
|
||||
document.getElementById('push-anime-list').style.display = 'none';
|
||||
document.getElementById('push-episode-list').style.display = 'none';
|
||||
customAlert('未找到相关动漫');
|
||||
addLog('未找到相关动漫', 'warn');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('搜索动漫失败:', error);
|
||||
customAlert('搜索动漫失败: ' + error.message);
|
||||
addLog('搜索动漫失败: ' + error.message, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
searchBtn.innerHTML = originalText;
|
||||
searchBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 展示动漫列表用于推送
|
||||
function displayAnimeListForPush(animes, pushUrl) {
|
||||
const container = document.getElementById('push-anime-list');
|
||||
let html = '<h3>搜索结果</h3><div class="anime-grid">';
|
||||
|
||||
animes.forEach(anime => {
|
||||
const imageUrl = anime.imageUrl || 'https://placehold.co/150x200?text=No+Image';
|
||||
html += \`
|
||||
<div class="anime-item" onclick="getBangumiForPush(\${anime.animeId}, '\${pushUrl}')">
|
||||
<img src="\${imageUrl}" alt="\${anime.animeTitle}" referrerpolicy="no-referrer" class="anime-item-img">
|
||||
<h4 class="anime-title">\${anime.animeTitle} - 共\${anime.episodeCount}集</h4>
|
||||
</div>
|
||||
\`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
container.style.display = 'block';
|
||||
|
||||
addLog(\`显示 \${animes.length} 个动漫结果\`, 'info');
|
||||
}
|
||||
|
||||
// 获取番剧详情用于推送
|
||||
function getBangumiForPush(animeId, pushUrl) {
|
||||
const bangumiUrl = buildApiUrl('/api/v2/bangumi/' + animeId);
|
||||
|
||||
addLog(\`获取番剧详情: \${animeId}\`, 'info');
|
||||
|
||||
fetch(bangumiUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error! status: \${response.status}\`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success && data.bangumi && data.bangumi.episodes) {
|
||||
displayEpisodeListForPush(data.bangumi.animeTitle, data.bangumi.episodes, pushUrl);
|
||||
} else {
|
||||
customAlert('该动漫暂无剧集信息');
|
||||
addLog('该动漫暂无剧集信息', 'warn');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取番剧详情失败:', error);
|
||||
customAlert('获取番剧详情失败: ' + error.message);
|
||||
addLog('获取番剧详情失败: ' + error.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 展示剧集列表用于推送
|
||||
function displayEpisodeListForPush(animeTitle, episodes, pushUrl) {
|
||||
const container = document.getElementById('push-episode-list');
|
||||
let html = \`<h3>剧集列表</h3><h4 class="text-yellow-gold">\${animeTitle}</h4><div class="episode-list-container">\`;
|
||||
|
||||
episodes.forEach(episode => {
|
||||
// 生成弹幕URL
|
||||
const commentUrl = window.location.origin + buildApiUrl('/api/v2/comment/' + episode.episodeId + '?format=xml');
|
||||
html += \`
|
||||
<div class="episode-item">
|
||||
<div class="episode-item-content">
|
||||
<strong>第\${episode.episodeNumber}集</strong> - \${episode.episodeTitle || '无标题'}
|
||||
</div>
|
||||
<button class="btn btn-success btn-sm episode-push-btn" onclick="pushDanmu('\${pushUrl}', '\${commentUrl}', '\${episode.episodeTitle || '第' + episode.episodeNumber + '集'}')">推送</button>
|
||||
</div>
|
||||
\`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
container.style.display = 'block';
|
||||
|
||||
addLog(\`显示 \${episodes.length} 个剧集\`, 'info');
|
||||
|
||||
// 自动滚动到剧集列表处
|
||||
setTimeout(() => {
|
||||
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// 推送弹幕
|
||||
async function pushDanmu(pushUrl, commentUrl, episodeTitle) {
|
||||
// 获取推送按钮元素
|
||||
const pushButton = event.target; // 通过事件对象获取当前点击的按钮
|
||||
|
||||
// 校验推送地址是否为空
|
||||
if (!pushUrl || pushUrl.trim() === '') {
|
||||
customAlert('请输入推送地址');
|
||||
if (pushButton) {
|
||||
pushButton.innerHTML = '推送';
|
||||
pushButton.disabled = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pushButton) {
|
||||
const originalText = pushButton.innerHTML;
|
||||
pushButton.innerHTML = '<span class="loading-spinner-small"></span>';
|
||||
pushButton.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 向推送地址发送弹幕数据
|
||||
const pushResponse = await fetch(pushUrl + encodeURIComponent(commentUrl), {
|
||||
method: 'GET',
|
||||
mode: 'no-cors', // 由于跨域限制,使用no-cors模式
|
||||
});
|
||||
|
||||
customAlert('弹幕推送成功!' + episodeTitle);
|
||||
addLog('弹幕推送成功 - ' + episodeTitle, 'success');
|
||||
} catch (error) {
|
||||
console.error('推送弹幕失败:', error);
|
||||
customAlert('推送弹幕失败: ' + error.message);
|
||||
addLog('推送弹幕失败: ' + error.message, 'error');
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
if (pushButton) {
|
||||
pushButton.innerHTML = '推送';
|
||||
pushButton.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,889 @@
|
||||
// language=JavaScript
|
||||
export const systemSettingsJsContent = /* javascript */ `
|
||||
// 显示清理缓存确认模态框
|
||||
function showClearCacheModal() {
|
||||
document.getElementById('clear-cache-modal').classList.add('active');
|
||||
}
|
||||
|
||||
// 隐藏清理缓存确认模态框
|
||||
function hideClearCacheModal() {
|
||||
document.getElementById('clear-cache-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
// 确认清理缓存
|
||||
async function confirmClearCache() {
|
||||
// 检查部署平台配置
|
||||
const configCheck = await checkDeployPlatformConfig();
|
||||
if (!configCheck.success) {
|
||||
hideClearCacheModal();
|
||||
customAlert(configCheck.message);
|
||||
return;
|
||||
}
|
||||
|
||||
hideClearCacheModal();
|
||||
showLoading('正在清理缓存...', '清除中,请稍候');
|
||||
addLog('开始清理缓存', 'info');
|
||||
|
||||
try {
|
||||
// 调用真实的清理缓存API
|
||||
const response = await fetch(buildApiUrl('/api/cache/clear', true), { // 使用admin token
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
updateLoadingText('清理完成', '缓存已成功清除');
|
||||
addLog('缓存清理完成', 'success');
|
||||
addLog('✅ 缓存清理成功!已清理: ' + JSON.stringify(result.clearedItems), 'success');
|
||||
} else {
|
||||
updateLoadingText('清理失败', '请查看日志了解详情');
|
||||
addLog('缓存清理失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
updateLoadingText('清理失败', '网络错误或服务不可用');
|
||||
addLog('缓存清理请求失败: ' + error.message, 'error');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示重新部署确认模态框
|
||||
function showDeploySystemModal() {
|
||||
document.getElementById('deploy-system-modal').classList.add('active');
|
||||
}
|
||||
|
||||
// 隐藏重新部署确认模态框
|
||||
function hideDeploySystemModal() {
|
||||
document.getElementById('deploy-system-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
// 确认重新部署系统
|
||||
function confirmDeploySystem() {
|
||||
// 检查部署平台配置
|
||||
checkDeployPlatformConfig().then(configCheck => {
|
||||
if (!configCheck.success) {
|
||||
hideDeploySystemModal();
|
||||
customAlert(configCheck.message);
|
||||
return;
|
||||
}
|
||||
|
||||
hideDeploySystemModal();
|
||||
showLoading('准备部署...', '正在检查系统状态');
|
||||
addLog('===== 开始系统部署 =====', 'info');
|
||||
|
||||
// 获取当前部署平台
|
||||
fetch(buildApiUrl('/api/config', true))
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
const deployPlatform = config.envs.deployPlatform || 'node';
|
||||
addLog(\`检测到部署平台: \${deployPlatform}\`, 'info');
|
||||
|
||||
if (deployPlatform.toLowerCase() === 'node') {
|
||||
// Node部署不需要重新部署
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
addLog('===== 部署完成 =====', 'success');
|
||||
addLog('Node部署模式,环境变量已生效', 'info');
|
||||
addLog('✅ Node部署模式 - 在Node部署模式下,环境变量修改后会自动生效,无需重新部署。系统已更新配置', 'success');
|
||||
}, 150);
|
||||
} else {
|
||||
// 调用真实的部署API
|
||||
fetch(buildApiUrl('/api/deploy', true), { // 使用admin token
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
addLog('云端部署触发成功', 'success');
|
||||
// 模拟云端部署过程
|
||||
simulateDeployProcess();
|
||||
} else {
|
||||
hideLoading();
|
||||
addLog(\`云端部署失败: \${result.message}\`, 'error');
|
||||
addLog(\`❌ 云端部署失败: \${result.message}\`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
addLog(\`云端部署请求失败: \${error.message}\`, 'error');
|
||||
addLog(\`❌ 云端部署请求失败: \${error.message}\`, 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
addLog(\`获取部署平台信息失败: \${error.message}\`, 'error');
|
||||
console.error('获取部署平台信息失败:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 模拟云端部署过程
|
||||
function simulateDeployProcess() {
|
||||
let progress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
progress += Math.random() * 8;
|
||||
if (progress >= 100) {
|
||||
progress = 10;
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
updateProgress(progress);
|
||||
}, 300);
|
||||
|
||||
// 模拟部署步骤
|
||||
const steps = [
|
||||
{ delay: 100, text: '检查环境变量...', detail: '验证配置文件', log: '配置文件验证通过' },
|
||||
{ delay: 5000, text: '触发云端部署...', detail: '部署到当前平台', log: '云端部署已触发' },
|
||||
{ delay: 9500, text: '构建项目...', detail: '云端构建中', log: '云端构建完成' },
|
||||
{ delay: 5000, text: '部署更新...', detail: '发布到生产环境', log: '更新已部署' },
|
||||
{ delay: 5500, text: '服务重启...', detail: '应用新配置', log: '服务已重启' },
|
||||
{ delay: 5000, text: '健康检查...', detail: '验证服务状态', log: '所有服务运行正常' },
|
||||
];
|
||||
|
||||
steps.forEach(step => {
|
||||
setTimeout(() => {
|
||||
updateLoadingText(step.text, step.detail);
|
||||
addLog(step.log, 'success');
|
||||
}, step.delay);
|
||||
});
|
||||
|
||||
// 部署后检查服务是否可用
|
||||
setTimeout(() => {
|
||||
checkDeploymentStatus();
|
||||
}, 900); // 延长延迟以确保模拟部署过程完成
|
||||
}
|
||||
|
||||
// 检查部署状态,每隔5秒请求/api/logs接口直到请求成功
|
||||
function checkDeploymentStatus() {
|
||||
const checkInterval = setInterval(() => {
|
||||
updateLoadingText('部署完成,检查服务状态...', '正在请求 /api/logs 接口');
|
||||
addLog('正在检查服务状态...', 'info');
|
||||
|
||||
fetch(buildApiUrl('/api/logs'))
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// 请求成功,停止检查
|
||||
clearInterval(checkInterval);
|
||||
// 更新加载状态而不是立即隐藏
|
||||
updateLoadingText('部署成功!', '服务已重启并正常运行');
|
||||
addLog('===== 部署完成 =====', 'success');
|
||||
addLog('部署版本: ' + latestVersion, 'info');
|
||||
addLog('系统已更新并重启', 'success');
|
||||
|
||||
// 部署完成后再次确认,访问/api/logs接口来确认部署完成
|
||||
confirmDeploymentByLogs();
|
||||
} else {
|
||||
addLog('服务检查中 - 状态码: ' + response.status, 'info');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
addLog('服务检查中 - 连接失败: ' + error.message, 'info');
|
||||
});
|
||||
}, 500); // 每5秒检查一次
|
||||
}
|
||||
|
||||
// 部署完成后通过访问/api/logs接口来确认部署完成
|
||||
function confirmDeploymentByLogs() {
|
||||
// 部署完成后的确认检查
|
||||
let confirmationAttempts = 0;
|
||||
const maxAttempts = 3; // 最多尝试3次确认部署完成
|
||||
|
||||
const confirmationInterval = setInterval(() => {
|
||||
confirmationAttempts++;
|
||||
updateLoadingText('部署完成确认中...', '正在确认部署完成 (' + confirmationAttempts + '/' + maxAttempts + ')');
|
||||
addLog('部署完成确认 - 尝试 ' + confirmationAttempts + '/' + maxAttempts, 'info');
|
||||
|
||||
fetch(buildApiUrl('/api/logs'))
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// 请求成功,停止确认检查
|
||||
clearInterval(confirmationInterval);
|
||||
// 显示成功信息后延迟隐藏加载遮罩
|
||||
updateLoadingText('部署确认成功!', '服务已重启并正常运行');
|
||||
addLog('部署确认成功 - /api/logs 接口访问正常', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
// 显示成功弹窗
|
||||
customAlert('🎉 部署成功!云端部署已完成,服务已重启,配置已生效');
|
||||
addLog('🎉 部署成功!云端部署已完成,服务已重启,配置已生效', 'success');
|
||||
}, 200);
|
||||
} else if (confirmationAttempts >= maxAttempts) {
|
||||
// 达到最大尝试次数,停止确认检查
|
||||
clearInterval(confirmationInterval);
|
||||
updateLoadingText('部署确认完成', '服务已重启');
|
||||
addLog('部署确认完成 - 已达到最大尝试次数', 'warn');
|
||||
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
// 显示成功弹窗
|
||||
customAlert('🎉 部署成功!云端部署已完成,服务已重启,配置已生效');
|
||||
addLog('🎉 部署成功!云端部署已完成,服务已重启,配置已生效', 'success');
|
||||
}, 200);
|
||||
} else {
|
||||
addLog('部署确认中 - 状态码: ' + response.status, 'info');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (confirmationAttempts >= maxAttempts) {
|
||||
// 达到最大尝试次数,停止确认检查
|
||||
clearInterval(confirmationInterval);
|
||||
updateLoadingText('部署确认完成', '服务已重启');
|
||||
addLog('部署确认完成 - 已达到最大尝试次数', 'warn');
|
||||
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
// 显示成功弹窗
|
||||
customAlert('🎉 部署成功!云端部署已完成,服务已重启,配置已生效');
|
||||
addLog('🎉 部署成功!云端部署已完成,服务已重启,配置已生效', 'success');
|
||||
}, 200);
|
||||
} else {
|
||||
addLog('部署确认中 - 连接失败: ' + error.message, 'info');
|
||||
}
|
||||
});
|
||||
}, 5000); // 每5秒检查一次,用于确认部署完成
|
||||
}
|
||||
|
||||
// 检查URL中的token是否与currentAdminToken匹配
|
||||
function checkAdminToken() {
|
||||
// 获取URL路径并提取token
|
||||
const urlPath = window.location.pathname;
|
||||
const pathParts = urlPath.split('/').filter(part => part !== '');
|
||||
const urlToken = pathParts.length > 0 ? pathParts[0] : currentToken; // 如果没有路径段,使用默认token
|
||||
|
||||
// 检查是否配置了ADMIN_TOKEN且URL中的token等于currentAdminToken
|
||||
return currentAdminToken && currentAdminToken.trim() !== '' && urlToken === currentAdminToken;
|
||||
}
|
||||
|
||||
// 检查部署平台相关配置
|
||||
async function checkDeployPlatformConfig() {
|
||||
// 首先检查是否配置了ADMIN_TOKEN
|
||||
if (!checkAdminToken()) {
|
||||
// 获取当前页面的协议、主机和端口
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
return { success: false, message: '请先配置ADMIN_TOKEN环境变量并使用正确的token访问以启用系统部署功能!\\n\\n访问方式:' + protocol + '//' + host + '/{ADMIN_TOKEN}' };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(buildApiUrl('/api/config', true));
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP error! status: ' + response.status);
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
const deployPlatform = config.envs.deployPlatform || 'node';
|
||||
|
||||
// 如果是node部署平台,只需要检查ADMIN_TOKEN
|
||||
if (deployPlatform.toLowerCase() === 'node') {
|
||||
return { success: true, message: 'Node部署平台,仅需配置ADMIN_TOKEN' };
|
||||
}
|
||||
|
||||
// 对于其他部署平台,收集所有缺失的环境变量
|
||||
const missingVars = [];
|
||||
const deployPlatformProject = config.originalEnvVars.DEPLOY_PLATFROM_PROJECT;
|
||||
const deployPlatformToken = config.originalEnvVars.DEPLOY_PLATFROM_TOKEN;
|
||||
const deployPlatformAccount = config.originalEnvVars.DEPLOY_PLATFROM_ACCOUNT;
|
||||
|
||||
if (!deployPlatformProject || deployPlatformProject.trim() === '') {
|
||||
missingVars.push('DEPLOY_PLATFROM_PROJECT');
|
||||
}
|
||||
|
||||
if (!deployPlatformToken || deployPlatformToken.trim() === '') {
|
||||
missingVars.push('DEPLOY_PLATFROM_TOKEN');
|
||||
}
|
||||
|
||||
// 对于netlify和cloudflare部署平台,还需要检查DEPLOY_PLATFROM_ACCOUNT
|
||||
if (deployPlatform.toLowerCase() === 'netlify' || deployPlatform.toLowerCase() === 'cloudflare') {
|
||||
if (!deployPlatformAccount || deployPlatformAccount.trim() === '') {
|
||||
missingVars.push('DEPLOY_PLATFROM_ACCOUNT');
|
||||
}
|
||||
}
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
const missingVarsStr = missingVars.join('、');
|
||||
return { success: false, message: '部署平台为' + deployPlatform + ',请配置以下缺失的环境变量:' + missingVarsStr };
|
||||
}
|
||||
|
||||
return { success: true, message: deployPlatform + '部署平台配置完整' };
|
||||
} catch (error) {
|
||||
console.error('检查部署平台配置失败:', error);
|
||||
return { success: false, message: '检查部署平台配置失败: ' + error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取并设置配置信息
|
||||
async function fetchAndSetConfig() {
|
||||
const config = await fetch(buildApiUrl('/api/config', true)).then(response => response.json());
|
||||
const hasAdminToken = config.hasAdminToken;
|
||||
currentAdminToken = config.originalEnvVars?.ADMIN_TOKEN || '';
|
||||
return config;
|
||||
}
|
||||
|
||||
// 检查并处理管理员令牌
|
||||
function checkAndHandleAdminToken() {
|
||||
if (!checkAdminToken()) {
|
||||
// 禁用系统配置按钮并添加提示
|
||||
const envNavBtn = document.getElementById('env-nav-btn');
|
||||
if (envNavBtn) {
|
||||
envNavBtn.title = '请先配置ADMIN_TOKEN并使用正确的admin token访问以启用系统管理功能';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染值输入控件
|
||||
function renderValueInput(item) {
|
||||
const container = document.getElementById('value-input-container');
|
||||
const type = item ? item.type : document.getElementById('value-type').value;
|
||||
const value = item ? item.value : '';
|
||||
|
||||
if (type === 'boolean') {
|
||||
// 布尔开关
|
||||
const checked = value === 'true' || value === true;
|
||||
container.innerHTML = \`
|
||||
<label>值</label>
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="bool-value" \${checked ? 'checked' : ''}>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="switch-label" id="bool-label">\${checked ? '启用' : '禁用'}</span>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
document.getElementById('bool-value').addEventListener('change', function(e) {
|
||||
document.getElementById('bool-label').textContent = e.target.checked ? '启用' : '禁用';
|
||||
});
|
||||
|
||||
} else if (type === 'number') {
|
||||
// 数字滚轮
|
||||
const min = item && item.min !== undefined ? item.min : 1;
|
||||
const max = item && item.max !== undefined ? item.max : 100;
|
||||
const currentValue = value || min;
|
||||
|
||||
container.innerHTML = \`
|
||||
<label>值 (\${min}-\${max})</label>
|
||||
<div class="number-picker">
|
||||
<div class="number-controls">
|
||||
<button type="button" class="number-btn" onclick="adjustNumber(1)">▲</button>
|
||||
<button type="button" class="number-btn" onclick="adjustNumber(-1)">▼</button>
|
||||
</div>
|
||||
<div class="number-display" id="num-value">\${currentValue}</div>
|
||||
</div>
|
||||
<div class="number-range">
|
||||
<input type="range" id="num-slider" min="\${min}" max="\${max}" value="\${currentValue}"
|
||||
oninput="updateNumberDisplay(this.value)">
|
||||
</div>
|
||||
\`;
|
||||
|
||||
} else if (type === 'select') {
|
||||
// 标签选择
|
||||
const options = item && item.options ? item.options : ['option1', 'option2', 'option3'];
|
||||
const optionsInput = item ? '' : \`
|
||||
<div class="form-group margin-bottom-15">
|
||||
<label>可选项 (逗号分隔)</label>
|
||||
<input type="text" id="select-options" placeholder="例如: debug,info,warn,error"
|
||||
value="\${options.join(',')}" onchange="updateTagOptions()">
|
||||
</div>
|
||||
\`;
|
||||
|
||||
container.innerHTML = \`
|
||||
\${optionsInput}
|
||||
<label>选择值</label>
|
||||
<div class="tag-selector" id="tag-selector">
|
||||
\${options.map(opt => \`
|
||||
<div class="tag-option \${opt === value ? 'selected' : ''}"
|
||||
data-value="\${opt}" onclick="selectTag(this)">
|
||||
\${opt}
|
||||
</div>
|
||||
\`).join('')}
|
||||
</div>
|
||||
\`;
|
||||
|
||||
} else if (type === 'multi-select') {
|
||||
// 多选标签(可拖动排序)
|
||||
const options = item && item.options ? item.options : ['option1', 'option2', 'option3', 'option4'];
|
||||
// 确保value是字符串类型后再进行split操作
|
||||
const stringValue = typeof value === 'string' ? value : String(value || '');
|
||||
const selectedValues = stringValue ? stringValue.split(',').map(v => v.trim()).filter(v => v) : [];
|
||||
|
||||
const optionsInput = item ? '' : \`
|
||||
<div class="form-group margin-bottom-15">
|
||||
<label>可选项 (逗号分隔)</label>
|
||||
<input type="text" id="multi-options" placeholder="例如: auth,payment,analytics"
|
||||
value="\${options.join(',')}" onchange="updateMultiOptions()">
|
||||
</div>
|
||||
\`;
|
||||
|
||||
container.innerHTML = \`
|
||||
\${optionsInput}
|
||||
<label>已选择 (拖动调整顺序)</label>
|
||||
<div class="multi-select-container">
|
||||
<div class="selected-tags \${selectedValues.length === 0 ? 'empty' : ''}" id="selected-tags">
|
||||
\${selectedValues.map(val => \`
|
||||
<div class="selected-tag" draggable="true" data-value="\${val}">
|
||||
<span class="tag-text">\${val}</span>
|
||||
<button type="button" class="remove-btn" onclick="removeSelectedTag(this)">×</button>
|
||||
</div>
|
||||
\`).join('')}
|
||||
</div>
|
||||
<label>可选项 (点击添加)</label>
|
||||
<div class="available-tags" id="available-tags">
|
||||
\${options.map(opt => {
|
||||
const isSelected = selectedValues.includes(opt);
|
||||
return \`
|
||||
<div class="available-tag \${isSelected ? 'disabled' : ''}"
|
||||
data-value="\${opt}" onclick="addSelectedTag(this)">
|
||||
\${opt}
|
||||
</div>
|
||||
\`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 设置拖动事件
|
||||
setupDragAndDrop();
|
||||
|
||||
} else {
|
||||
// 文本输入
|
||||
// 如果值太长,使用textarea而不是input
|
||||
if (value && value.length > 50) {
|
||||
// 计算行数,每行约50个字符
|
||||
const rows = Math.min(Math.max(Math.ceil(value.length / 50), 3), 10); // 最少3行,最多10行
|
||||
container.innerHTML = \`
|
||||
<label>变量值 *</label>
|
||||
<textarea id="text-value" placeholder="例如: localhost" rows="\${rows}" class="text-monospace">\${value}</textarea>
|
||||
\`;
|
||||
} else {
|
||||
container.innerHTML = \`
|
||||
<label>变量值 *</label>
|
||||
<input type="text" id="text-value" placeholder="例如: localhost" value="\${value}" required>
|
||||
\`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调整数字
|
||||
function adjustNumber(delta) {
|
||||
const display = document.getElementById('num-value');
|
||||
const slider = document.getElementById('num-slider');
|
||||
let value = parseInt(display.textContent) + delta;
|
||||
|
||||
value = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), value));
|
||||
|
||||
display.textContent = value;
|
||||
slider.value = value;
|
||||
}
|
||||
|
||||
// 更新数字显示
|
||||
function updateNumberDisplay(value) {
|
||||
document.getElementById('num-value').textContent = value;
|
||||
}
|
||||
|
||||
// 选择标签
|
||||
function selectTag(element) {
|
||||
document.querySelectorAll('.tag-option').forEach(el => el.classList.remove('selected'));
|
||||
element.classList.add('selected');
|
||||
}
|
||||
|
||||
// 更新标签选项
|
||||
function updateTagOptions() {
|
||||
const input = document.getElementById('select-options');
|
||||
const options = input.value.split(',').map(s => s.trim()).filter(s => s);
|
||||
const container = document.getElementById('tag-selector');
|
||||
|
||||
container.innerHTML = options.map(opt => \`
|
||||
<div class="tag-option" data-value="\${opt}" onclick="selectTag(this)">
|
||||
\${opt}
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 添加已选标签
|
||||
function addSelectedTag(element) {
|
||||
if (element.classList.contains('disabled')) return;
|
||||
|
||||
const value = element.dataset.value;
|
||||
const container = document.getElementById('selected-tags');
|
||||
|
||||
// 移除empty类
|
||||
container.classList.remove('empty');
|
||||
|
||||
// 创建新标签
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'selected-tag';
|
||||
tag.draggable = true;
|
||||
tag.dataset.value = value;
|
||||
tag.innerHTML = \`
|
||||
<span class="tag-text">\${value}</span>
|
||||
<button type="button" class="remove-btn" onclick="removeSelectedTag(this)">×</button>
|
||||
\`;
|
||||
|
||||
container.appendChild(tag);
|
||||
|
||||
// 禁用可选项
|
||||
element.classList.add('disabled');
|
||||
|
||||
// 重新设置拖动事件
|
||||
setupDragAndDrop();
|
||||
}
|
||||
|
||||
// 移除已选标签
|
||||
function removeSelectedTag(button) {
|
||||
const tag = button.parentElement;
|
||||
const value = tag.dataset.value;
|
||||
const container = document.getElementById('selected-tags');
|
||||
|
||||
// 移除标签
|
||||
tag.remove();
|
||||
|
||||
// 如果没有标签了,添加empty类
|
||||
if (container.children.length === 0) {
|
||||
container.classList.add('empty');
|
||||
}
|
||||
|
||||
// 启用对应的可选项
|
||||
const availableTag = document.querySelector(\`.available-tag[data-value="\${value}"]\`);
|
||||
if (availableTag) {
|
||||
availableTag.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新多选选项
|
||||
function updateMultiOptions() {
|
||||
const input = document.getElementById('multi-options');
|
||||
const options = input.value.split(',').map(s => s.trim()).filter(s => s);
|
||||
const selectedValues = Array.from(document.querySelectorAll('.selected-tag'))
|
||||
.map(el => el.dataset.value);
|
||||
|
||||
const container = document.getElementById('available-tags');
|
||||
container.innerHTML = options.map(opt => {
|
||||
const isSelected = selectedValues.includes(opt);
|
||||
return \`
|
||||
<div class="available-tag \${isSelected ? 'disabled' : ''}"
|
||||
data-value="\${opt}" onclick="addSelectedTag(this)">
|
||||
\${opt}
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 设置拖放功能
|
||||
let draggedElement = null;
|
||||
|
||||
function setupDragAndDrop() {
|
||||
const container = document.getElementById('selected-tags');
|
||||
const tags = container.querySelectorAll('.selected-tag');
|
||||
|
||||
tags.forEach(tag => {
|
||||
tag.addEventListener('dragstart', handleDragStart);
|
||||
tag.addEventListener('dragend', handleDragEnd);
|
||||
tag.addEventListener('dragover', handleDragOver);
|
||||
tag.addEventListener('drop', handleDrop);
|
||||
tag.addEventListener('dragenter', handleDragEnter);
|
||||
tag.addEventListener('dragleave', handleDragLeave);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragStart(e) {
|
||||
draggedElement = this;
|
||||
this.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
function handleDragEnd(e) {
|
||||
this.classList.remove('dragging');
|
||||
document.querySelectorAll('.selected-tag').forEach(tag => {
|
||||
tag.classList.remove('drag-over');
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleDragEnter(e) {
|
||||
if (this !== draggedElement) {
|
||||
this.classList.add('drag-over');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragLeave(e) {
|
||||
this.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
if (draggedElement !== this) {
|
||||
const container = document.getElementById('selected-tags');
|
||||
const allTags = Array.from(container.querySelectorAll('.selected-tag'));
|
||||
const draggedIndex = allTags.indexOf(draggedElement);
|
||||
const targetIndex = allTags.indexOf(this);
|
||||
|
||||
if (draggedIndex < targetIndex) {
|
||||
this.parentNode.insertBefore(draggedElement, this.nextSibling);
|
||||
} else {
|
||||
this.parentNode.insertBefore(draggedElement, this);
|
||||
}
|
||||
}
|
||||
|
||||
this.classList.remove('drag-over');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 显示加载遮罩
|
||||
function showLoading(text, detail) {
|
||||
document.getElementById('loading-text').textContent = text;
|
||||
document.getElementById('loading-detail').textContent = detail;
|
||||
document.getElementById('loading-overlay').classList.add('active');
|
||||
document.getElementById('progress-container').classList.add('active');
|
||||
updateProgress(0);
|
||||
}
|
||||
|
||||
// 隐藏加载遮罩
|
||||
function hideLoading() {
|
||||
document.getElementById('loading-overlay').classList.remove('active');
|
||||
setTimeout(() => {
|
||||
document.getElementById('progress-container').classList.remove('active');
|
||||
updateProgress(0);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 更新加载文本
|
||||
function updateLoadingText(text, detail) {
|
||||
document.getElementById('loading-text').textContent = text;
|
||||
document.getElementById('loading-detail').textContent = detail;
|
||||
}
|
||||
|
||||
// 更新进度条
|
||||
function updateProgress(percent) {
|
||||
document.getElementById('progress-bar').style.width = percent + '%';
|
||||
}
|
||||
|
||||
// 渲染环境变量列表
|
||||
function renderEnvList() {
|
||||
const list = document.getElementById('env-list');
|
||||
const items = envVariables[currentCategory] || [];
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<p class="text-gray padding-20 text-center">暂无配置项</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map((item, index) => {
|
||||
const typeLabel = item.type === 'boolean' ? '布尔' :
|
||||
item.type === 'number' ? '数字' :
|
||||
item.type === 'select' ? '单选' :
|
||||
item.type === 'multi-select' ? '多选' : '文本';
|
||||
const badgeClass = item.type === 'multi-select' ? 'multi' : '';
|
||||
|
||||
return \`
|
||||
<div class="env-item">
|
||||
<div class="env-info">
|
||||
<strong>\${item.key}<span class="value-type-badge \${badgeClass}">\${typeLabel}</span></strong>
|
||||
<div class="text-dark-gray">\${item.value}</div>
|
||||
<div class="text-gray font-size-12 margin-top-3">\${item.description || '无描述'}</div>
|
||||
</div>
|
||||
<div class="env-actions">
|
||||
<button class="btn btn-primary" onclick="editEnv(\${index})">编辑</button>
|
||||
<button class="btn btn-danger" onclick="deleteEnv(\${index})">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 编辑环境变量
|
||||
function editEnv(index) {
|
||||
const item = envVariables[currentCategory][index];
|
||||
const editButton = event.target; // 获取当前点击的编辑按钮
|
||||
|
||||
// 设置按钮为加载状态
|
||||
const originalText = editButton.innerHTML;
|
||||
editButton.innerHTML = '<span class="loading-spinner-small"></span>';
|
||||
editButton.disabled = true;
|
||||
|
||||
editingKey = index;
|
||||
document.getElementById('modal-title').textContent = '编辑配置项';
|
||||
document.getElementById('env-category').value = currentCategory;
|
||||
document.getElementById('env-key').value = item.key;
|
||||
document.getElementById('env-description').value = item.description || '';
|
||||
document.getElementById('value-type').value = item.type || 'text';
|
||||
|
||||
// 设置字段为只读(编辑模式下)
|
||||
document.getElementById('env-category').disabled = true;
|
||||
document.getElementById('env-key').readOnly = true;
|
||||
document.getElementById('value-type').disabled = true;
|
||||
document.getElementById('env-description').readOnly = true;
|
||||
|
||||
// 渲染对应的值输入控件
|
||||
renderValueInput(item);
|
||||
|
||||
document.getElementById('env-modal').classList.add('active');
|
||||
|
||||
// 恢复按钮状态(在实际场景中,这会在编辑完成后发生,比如在保存后或取消后)
|
||||
// 为了演示,这里立即恢复按钮状态,实际使用中应该在适当的地方恢复按钮状态
|
||||
editButton.innerHTML = originalText;
|
||||
editButton.disabled = false;
|
||||
}
|
||||
|
||||
// 删除环境变量
|
||||
function deleteEnv(index) {
|
||||
customConfirm('确定要删除这个配置项吗?', '删除确认').then(confirmed => {
|
||||
if (confirmed) {
|
||||
const item = envVariables[currentCategory][index];
|
||||
const key = item.key;
|
||||
const deleteButton = event.target; // 获取当前点击的删除按钮
|
||||
|
||||
// 设置按钮为加载状态
|
||||
const originalText = deleteButton.innerHTML;
|
||||
deleteButton.innerHTML = '<span class="loading-spinner-small"></span>';
|
||||
deleteButton.disabled = true;
|
||||
|
||||
// 调用API删除环境变量
|
||||
fetch(buildApiUrl('/api/env/del'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ key })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
// 从本地数据中删除
|
||||
envVariables[currentCategory].splice(index, 1);
|
||||
renderEnvList();
|
||||
renderPreview();
|
||||
addLog(\`删除配置项: \${key}\`, 'warn');
|
||||
} else {
|
||||
addLog(\`删除配置项失败: \${result.message}\`, 'error');
|
||||
addLog(\`❌ 删除配置项失败: \${result.message}\`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
addLog(\`删除配置项失败: \${error.message}\`, 'error');
|
||||
addLog(\`❌ 删除配置项失败: \${error.message}\`, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
deleteButton.innerHTML = originalText;
|
||||
deleteButton.disabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
document.getElementById('env-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const category = document.getElementById('env-category').value;
|
||||
const key = document.getElementById('env-key').value.trim();
|
||||
const description = document.getElementById('env-description').value.trim();
|
||||
const type = document.getElementById('value-type').value;
|
||||
|
||||
// 根据类型获取值
|
||||
let value, itemData;
|
||||
|
||||
if (type === 'boolean') {
|
||||
value = document.getElementById('bool-value').checked ? 'true' : 'false';
|
||||
itemData = { key, value, description, type };
|
||||
} else if (type === 'number') {
|
||||
value = document.getElementById('num-value').textContent;
|
||||
const min = parseInt(document.getElementById('num-slider').min);
|
||||
const max = parseInt(document.getElementById('num-slider').max);
|
||||
itemData = { key, value, description, type, min, max };
|
||||
} else if (type === 'select') {
|
||||
const selected = document.querySelector('.tag-option.selected');
|
||||
value = selected ? selected.dataset.value : '';
|
||||
const options = Array.from(document.querySelectorAll('.tag-option')).map(el => el.dataset.value);
|
||||
itemData = { key, value, description, type, options };
|
||||
} else if (type === 'multi-select') {
|
||||
const selectedTags = Array.from(document.querySelectorAll('.selected-tag'))
|
||||
.map(el => el.dataset.value);
|
||||
value = selectedTags.join(',');
|
||||
const options = Array.from(document.querySelectorAll('.available-tag')).map(el => el.dataset.value);
|
||||
itemData = { key, value, description, type, options };
|
||||
} else {
|
||||
value = document.getElementById('text-value').value.trim();
|
||||
itemData = { key, value, description, type };
|
||||
}
|
||||
|
||||
// 调用API更新环境变量 - 先尝试set接口,失败则调用add接口
|
||||
try {
|
||||
// 首先尝试使用set接口更新
|
||||
let response = await fetch(buildApiUrl('/api/env/set'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
|
||||
let result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
// 如果set接口失败,尝试使用add接口
|
||||
response = await fetch(buildApiUrl('/api/env/add'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
|
||||
result = await response.json();
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// 更新本地数据
|
||||
if (!envVariables[category]) {
|
||||
envVariables[category] = [];
|
||||
}
|
||||
|
||||
if (editingKey !== null) {
|
||||
envVariables[currentCategory][editingKey] = itemData;
|
||||
addLog(\`更新配置项: \${key} = \${value}\`, 'success');
|
||||
} else {
|
||||
envVariables[category].push(itemData);
|
||||
addLog(\`添加配置项: \${key} = \${value}\`, 'success');
|
||||
}
|
||||
|
||||
if (category !== currentCategory) {
|
||||
currentCategory = category;
|
||||
document.querySelectorAll('.category-btn').forEach((btn, i) => {
|
||||
btn.classList.toggle('active', ['api', 'source', 'match', 'danmu', 'cache', 'system'][i] === category);
|
||||
});
|
||||
}
|
||||
|
||||
renderEnvList();
|
||||
renderPreview();
|
||||
closeModal();
|
||||
} else {
|
||||
addLog(\`操作失败: \${result.message}\`, 'error');
|
||||
addLog(\`❌ 操作失败: \${result.message}\`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(\`更新环境变量失败: \${error.message}\`, 'error');
|
||||
addLog(\`❌ 更新环境变量失败: \${error.message}\`, 'error');
|
||||
}
|
||||
});
|
||||
`;
|
||||
@@ -0,0 +1,298 @@
|
||||
import { globals } from "../configs/globals.js";
|
||||
import { baseCssContent } from "./css/base.css.js";
|
||||
import { componentsCssContent } from "./css/components.css.js";
|
||||
import { formsCssContent } from "./css/forms.css.js";
|
||||
import { responsiveCssContent } from "./css/responsive.css.js";
|
||||
import { mainJsContent } from "./js/main.js";
|
||||
import { previewJsContent } from "./js/preview.js";
|
||||
import { logviewJsContent } from "./js/logview.js";
|
||||
import { apitestJsContent } from "./js/apitest.js";
|
||||
import { pushDanmuJsContent } from "./js/pushdanmu.js";
|
||||
import { systemSettingsJsContent } from "./js/systemsettings.js";
|
||||
|
||||
// language=HTML
|
||||
export const HTML_TEMPLATE = /* html */ `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LogVar弹幕API</title>
|
||||
<style>${baseCssContent}</style>
|
||||
<style>${componentsCssContent}</style>
|
||||
<style>${formsCssContent}</style>
|
||||
<style>${responsiveCssContent}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-container" id="progress-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="logo-title-container">
|
||||
<div class="logo"><img src="https://i.mji.rip/2025/09/27/eedc7b701c0fa5c1f7c175b22f441ad9.jpeg" width="500"/></div>
|
||||
<h1>LogVar弹幕API</h1>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<span class="version-badge">当前版本: <span id="current-version">v${globals.version}</span></span>
|
||||
<span class="update-badge" id="update-badge">
|
||||
🎉 最新版本: <span id="latest-version">加载中...</span>
|
||||
</span>
|
||||
<span class="api-endpoint-badge">
|
||||
API端点: <span id="api-endpoint" title="点击复制API端点" style="cursor: pointer; color: #4CAF50; font-weight: bold;" onclick="copyApiEndpoint()">加载中...</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn active" onclick="switchSection('preview')">配置预览</button>
|
||||
<button class="nav-btn" onclick="switchSection('logs')">日志查看</button>
|
||||
<button class="nav-btn" onclick="switchSection('api')">接口调试</button>
|
||||
<button class="nav-btn" onclick="switchSection('push')">推送弹幕</button>
|
||||
<button class="nav-btn" onclick="switchSection('env')" id="env-nav-btn">系统配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- 配置预览 -->
|
||||
<div class="section active" id="preview-section">
|
||||
<h2>配置预览</h2>
|
||||
<p style="color: #666; margin-bottom: 20px;">当前生效的环境变量配置</p>
|
||||
<div class="preview-area" id="preview-area"></div>
|
||||
</div>
|
||||
|
||||
<!-- 日志查看 -->
|
||||
<div class="section" id="logs-section">
|
||||
<h2>日志查看</h2>
|
||||
<div class="log-controls">
|
||||
<div>
|
||||
<button class="btn btn-primary" onclick="refreshLogs()">🔄 刷新日志</button>
|
||||
<button class="btn btn-danger" onclick="clearLogs()">🗑️ 清空日志</button>
|
||||
</div>
|
||||
<span style="color: #666;">实时日志监控</span>
|
||||
</div>
|
||||
<div class="log-container" id="log-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- 接口调试 -->
|
||||
<div class="section" id="api-section">
|
||||
<h2>接口调试</h2>
|
||||
<div class="api-selector">
|
||||
<div class="form-group">
|
||||
<label>选择接口</label>
|
||||
<select id="api-select" onchange="loadApiParams()">
|
||||
<option value="">请选择接口</option>
|
||||
<option value="searchAnime">搜索动漫 - /api/v2/search/anime</option>
|
||||
<option value="searchEpisodes">搜索剧集 - /api/v2/search/episodes</option>
|
||||
<option value="matchAnime">匹配动漫 - /api/v2/match</option>
|
||||
<option value="getBangumi">获取番剧详情 - /api/v2/bangumi/:animeId</option>
|
||||
<option value="getComment">获取弹幕 - /api/v2/comment/:commentId</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-params" id="api-params" style="display: none;">
|
||||
<h3 style="margin-bottom: 15px;">接口参数</h3>
|
||||
<div id="params-form"></div>
|
||||
<button class="btn btn-success" onclick="testApi()">发送请求</button>
|
||||
</div>
|
||||
|
||||
<div id="api-response-container" style="display: none;">
|
||||
<h3 style="margin: 20px 0 10px;">响应结果</h3>
|
||||
<div class="api-response" id="api-response"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推送弹幕 -->
|
||||
<div class="section" id="push-section">
|
||||
<h2>推送弹幕</h2>
|
||||
<p style="color: #666; margin-bottom: 15px;">支持OK影视等播放器,两端需要在同一局域网或使用公网ip,推送地址格式如 http://127.0.0.1:9978/action?do=refresh&type=danmaku&path=</p>
|
||||
<div class="push-controls" style="margin-bottom: 20px;">
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label>推送地址</label>
|
||||
<input type="text" id="push-url" placeholder="请输入推送地址,例如: http://127.0.0.1:9978/action?do=refresh&type=danmaku&path=" style="width: 100%; padding: 8px; margin-top: 5px;">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label>搜索关键字</label>
|
||||
<div style="margin-top: 5px;">
|
||||
<input type="text" id="push-search-keyword" placeholder="请输入搜索关键字" style="width: calc(100% - 100px); padding: 8px; display: inline-block;">
|
||||
<button class="btn btn-primary" onclick="searchAnimeForPush()" style="width: 80px; display: inline-block; margin-left: 10px;">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="push-anime-list" class="anime-list" style="display: none;"></div>
|
||||
<div id="push-episode-list" class="episode-list" style="display: none; margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 系统配置 -->
|
||||
<div class="section" id="env-section">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 10px;">
|
||||
<div>
|
||||
<h2 style="margin: 0;">环境变量配置</h2>
|
||||
<p style="margin: 5px 0 0 0; color: #666; font-size: 0.9em;">vercel/netlify/edgeone平台修改变量后需要重新部署</p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<button class="btn btn-danger" onclick="showClearCacheModal()" title="清理系统缓存">
|
||||
🗑️ 清理缓存
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="showDeploySystemModal()" title="重新部署系统">
|
||||
🚀 重新部署
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 清理缓存确认模态框 -->
|
||||
<div class="modal" id="clear-cache-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>确认清理缓存</h3>
|
||||
<button class="close-btn" onclick="hideClearCacheModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="margin-bottom: 20px;">确定要清理所有缓存吗?这将清除:</p>
|
||||
<ul class="confirmation-list">
|
||||
<li>动漫搜索缓存 (animes)</li>
|
||||
<li>剧集ID缓存 (episodeIds)</li>
|
||||
<li>剧集编号缓存 (episodeNum)</li>
|
||||
<li>最后选择映射缓存 (lastSelectMap)</li>
|
||||
<li>搜索结果缓存</li>
|
||||
<li>弹幕内容缓存</li>
|
||||
<li>请求历史记录</li>
|
||||
</ul>
|
||||
<p style="color: #666; margin-top: 20px;">清理后可能需要重新登录</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-success" onclick="confirmClearCache()">确认清理</button>
|
||||
<button class="btn btn-danger" onclick="hideClearCacheModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重新部署确认模态框 -->
|
||||
<div class="modal" id="deploy-system-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>确认重新部署</h3>
|
||||
<button class="close-btn" onclick="hideDeploySystemModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="margin-bottom: 20px;">确定要重新部署系统吗?</p>
|
||||
<div class="warning-box">
|
||||
<p style="margin: 0;">部署过程中:</p>
|
||||
<ul class="confirmation-list">
|
||||
<li>系统将短暂不可用</li>
|
||||
<li>所有配置将重新加载</li>
|
||||
<li>服务将自动重启</li>
|
||||
</ul>
|
||||
<p style="margin-top: 10px;">预计耗时:30-90秒</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-success" onclick="confirmDeploySystem()">确认部署</button>
|
||||
<button class="btn btn-danger" onclick="hideDeploySystemModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="env-categories">
|
||||
<button class="category-btn active" onclick="switchCategory('api')">🔗 API配置</button>
|
||||
<button class="category-btn" onclick="switchCategory('source')">📜 源配置</button>
|
||||
<button class="category-btn" onclick="switchCategory('match')">🔍 匹配配置</button>
|
||||
<button class="category-btn" onclick="switchCategory('danmu')">🔣 弹幕配置</button>
|
||||
<button class="category-btn" onclick="switchCategory('cache')">💾 缓存配置</button>
|
||||
<button class="category-btn" onclick="switchCategory('system')">⚙️ 系统配置</button>
|
||||
</div>
|
||||
|
||||
<div class="env-list" id="env-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载遮罩层 -->
|
||||
<div class="loading-overlay" id="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text" id="loading-text">正在处理...</div>
|
||||
<div class="loading-detail" id="loading-detail">请稍候</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模态框 -->
|
||||
<div class="modal" id="env-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-title">编辑配置项</h3>
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<form id="env-form">
|
||||
<div class="form-group">
|
||||
<label>变量类别</label>
|
||||
<select id="env-category" disabled>
|
||||
<option value="api">🔗 API配置</option>
|
||||
<option value="source">📜 源配置</option>
|
||||
<option value="match">🔍 匹配配置</option>
|
||||
<option value="danmu">🔣 弹幕配置</option>
|
||||
<option value="cache">💾 缓存配置</option>
|
||||
<option value="system">⚙️ 系统配置</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>变量名</label>
|
||||
<input type="text" id="env-key" placeholder="例如: DB_HOST" required readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>值类型</label>
|
||||
<select id="value-type" onchange="renderValueInput()" disabled>
|
||||
<option value="text">文本</option>
|
||||
<option value="boolean">布尔值</option>
|
||||
<option value="number">数字 (1-100)</option>
|
||||
<option value="select">单选</option>
|
||||
<option value="multi-select">多选 (可排序)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="value-input-container">
|
||||
<!-- 动态渲染的值输入控件 -->
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<textarea id="env-description" placeholder="配置项说明" readonly></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="submit" class="btn btn-success" style="flex: 1;">保存</button>
|
||||
<button type="button" class="btn btn-danger" onclick="closeModal()" style="flex: 1;">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目声明 -->
|
||||
<footer class="footer">
|
||||
<p class="footer-text">
|
||||
一个人人都能部署的基于 js 的弹幕 API 服务器,支持爱优腾芒哔人韩巴弹幕直接获取,兼容弹弹play的搜索、详情查询和弹幕获取接口规范,并提供日志记录,支持vercel/netlify/edgeone/cloudflare/docker/claw等部署方式,不用提前下载弹幕,没有nas或小鸡也能一键部署。
|
||||
</p>
|
||||
<p class="footer-text">本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。</p>
|
||||
<p class="footer-links">
|
||||
<a href="https://t.me/ddjdd_bot" target="_blank" class="footer-link">💬 TG MSG ROBOT</a>
|
||||
<a href="https://t.me/logvar_danmu_group" target="_blank" class="footer-link">👥 TG GROUP</a>
|
||||
<a href="https://t.me/logvar_danmu_channel" target="_blank" class="footer-link">📢 TG CHANNEL</a>
|
||||
<a href="https://github.com/huangxd-/danmu_api" target="_blank" class="footer-link github-link">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg" alt="GitHub" class="github-icon">
|
||||
GitHub Repo
|
||||
</a>
|
||||
</p>
|
||||
<p>有问题提issue或私信机器人都ok</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
${mainJsContent}
|
||||
${previewJsContent}
|
||||
${logviewJsContent}
|
||||
${apitestJsContent}
|
||||
${pushDanmuJsContent}
|
||||
${systemSettingsJsContent}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
@@ -309,7 +309,7 @@ export function cleanupExpiredIPs(currentTime) {
|
||||
}
|
||||
|
||||
// 获取当前文件目录的兼容方式
|
||||
function getDirname() {
|
||||
export function getDirname() {
|
||||
if (typeof __dirname !== 'undefined') {
|
||||
// CommonJS 环境 (Vercel)
|
||||
return __dirname;
|
||||
|
||||
@@ -199,11 +199,14 @@ export async function httpPost(url, body, options = {}) {
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const data = await response.text();
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
log("error", `[请求模拟] response data: `, data);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.text();
|
||||
let parsedData;
|
||||
try {
|
||||
parsedData = JSON.parse(data); // 尝试将文本解析为 JSON
|
||||
@@ -260,6 +263,79 @@ export async function httpPost(url, body, options = {}) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 HTTP 请求函数(模拟环境返回结构)
|
||||
* @param {string} method - HTTP 方法
|
||||
* @param {string} url - 请求地址
|
||||
* @param {any} [body] - 请求体(可选)
|
||||
* @param {object} [options] - 选项
|
||||
* @param {object} [options.headers] - 请求头
|
||||
* @param {object} [options.params] - 查询参数(暂未实现)
|
||||
* @param {boolean} [options.allow_redirects=true] - 是否允许重定向(暂未实现)
|
||||
* @returns {Promise<{data: any, status: number, headers: Record<string, string>}>}
|
||||
*/
|
||||
async function httpRequestMethod(method, url, body, options = {}) {
|
||||
log("info", `[请求模拟] HTTP ${method}: ${url}`);
|
||||
|
||||
const { headers = {}, params, allow_redirects = true } = options;
|
||||
|
||||
const fetchOptions = {
|
||||
method,
|
||||
headers: { ...headers },
|
||||
};
|
||||
|
||||
// 只有在 body 存在时才设置(DELETE 通常无 body)
|
||||
if (body !== undefined && body !== null) {
|
||||
fetchOptions.body = body;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, fetchOptions);
|
||||
const textData = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
log("error", `[请求模拟] response data: `, textData);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
let parsedData;
|
||||
try {
|
||||
parsedData = JSON.parse(textData);
|
||||
} catch (e) {
|
||||
parsedData = textData;
|
||||
}
|
||||
|
||||
return {
|
||||
data: parsedData,
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
};
|
||||
} catch (error) {
|
||||
log("error", `[请求模拟] 请求失败:`, error.message);
|
||||
log("error", '详细诊断:');
|
||||
log("error", '- URL:', url);
|
||||
log("error", '- 错误类型:', error.name);
|
||||
log("error", '- 消息:', error.message);
|
||||
if (error.cause) {
|
||||
log("error", '- 码:', error.cause?.code);
|
||||
log("error", '- 原因:', error.cause?.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpPatch(url, body, options = {}) {
|
||||
return httpRequestMethod('PATCH', url, body, options);
|
||||
}
|
||||
|
||||
export async function httpPut(url, body, options = {}) {
|
||||
return httpRequestMethod('PUT', url, body, options);
|
||||
}
|
||||
|
||||
export async function httpDelete(url, options = {}) {
|
||||
return httpRequestMethod('DELETE', url, undefined, options); // DELETE 不传 body
|
||||
}
|
||||
|
||||
export async function getPageTitle(url) {
|
||||
try {
|
||||
// 使用 httpGet 获取网页内容
|
||||
|
||||
@@ -12,18 +12,57 @@ export function log(level, ...args) {
|
||||
return; // 日志级别不符合,不输出
|
||||
}
|
||||
|
||||
const message = args
|
||||
// 处理日志参数,隐藏敏感信息
|
||||
const processedArgs = args.map(arg => {
|
||||
if (typeof arg === "object") {
|
||||
// 如果是对象,转换为字符串并隐藏敏感信息
|
||||
const jsonString = JSON.stringify(arg);
|
||||
return hideSensitiveInfo(jsonString);
|
||||
} else {
|
||||
// 如果是字符串,直接隐藏敏感信息
|
||||
return typeof arg === 'string' ? hideSensitiveInfo(arg) : arg;
|
||||
}
|
||||
});
|
||||
|
||||
const message = processedArgs
|
||||
.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : arg))
|
||||
.join(" ");
|
||||
|
||||
// 获取上海时区时间(UTC+8)
|
||||
const now = new Date();
|
||||
const shanghaiTime = new Date(now.getTime() + (8 * 60 * 60 * 1000));
|
||||
const shanghaiTime = new Date(now.getTime() + (8 * 60 * 1000));
|
||||
const timestamp = shanghaiTime.toISOString().replace('Z', '+08:00');
|
||||
|
||||
globals.logBuffer.push({ timestamp, level, message });
|
||||
if (globals.logBuffer.length > globals.MAX_LOGS) globals.logBuffer.shift();
|
||||
console[level](...args);
|
||||
console[level](...processedArgs);
|
||||
}
|
||||
|
||||
// 隐藏敏感信息的辅助函数
|
||||
function hideSensitiveInfo(message) {
|
||||
let processedMessage = message;
|
||||
|
||||
// 从 globals.originalEnvVars 获取被加密的环境变量值并替换
|
||||
// 通过比较 globals.originalEnvVars 和 globals.accessedEnvVars 来识别加密变量
|
||||
// 如果 originalEnvVars 中的值与 accessedEnvVars 中的值不同,且 accessedEnvVars 中是星号,则说明该变量被加密
|
||||
if (globals.originalEnvVars && globals.accessedEnvVars) {
|
||||
for (const [envVar, originalValue] of Object.entries(globals.originalEnvVars)) {
|
||||
const accessedValue = globals.accessedEnvVars[envVar];
|
||||
|
||||
// 检查是否为加密变量:原始值存在、访问值存在、访问值是星号且与原始值不同
|
||||
if (originalValue && typeof originalValue === 'string' && originalValue.length > 0 &&
|
||||
accessedValue && typeof accessedValue === 'string' &&
|
||||
accessedValue.match(/^\*+$/) && accessedValue.length === originalValue.length) {
|
||||
// 这是一个加密变量,需要隐藏
|
||||
const mask = '*'.repeat(originalValue.length);
|
||||
// 创建正则表达式来匹配环境变量值,使用全局替换
|
||||
const regex = new RegExp(originalValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
|
||||
processedMessage = processedMessage.replace(regex, mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processedMessage;
|
||||
}
|
||||
|
||||
export function formatLogMessage(message) {
|
||||
@@ -33,4 +72,4 @@ export function formatLogMessage(message) {
|
||||
} catch {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
-38
@@ -5,17 +5,20 @@ import { getRedisCaches, judgeRedisValid } from "./utils/redis-util.js";
|
||||
import { cleanupExpiredIPs, findUrlById, getCommentCache, getLocalCaches, judgeLocalCacheValid } from "./utils/cache-util.js";
|
||||
import { formatDanmuResponse } from "./utils/danmu-util.js";
|
||||
import { getBangumi, getComment, getCommentByUrl, matchAnime, searchAnime, searchEpisodes } from "./apis/dandan-api.js";
|
||||
import { handleConfig, handleUI, handleLogs, handleClearLogs, handleDeploy, handleClearCache } from "./apis/system-api.js";
|
||||
import { handleSetEnv, handleAddEnv, handleDelEnv } from "./apis/env-api.js";
|
||||
|
||||
let globals;
|
||||
|
||||
async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
// 加载全局变量和环境变量配置
|
||||
globals = Globals.init(env, deployPlatform);
|
||||
globals = Globals.init(env);
|
||||
|
||||
const url = new URL(req.url);
|
||||
let path = url.pathname;
|
||||
const method = req.method;
|
||||
|
||||
globals.deployPlatform = deployPlatform;
|
||||
if (deployPlatform === "node") {
|
||||
await judgeLocalCacheValid(path, deployPlatform);
|
||||
}
|
||||
@@ -25,6 +28,21 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
log("info", `request path: ${path}`);
|
||||
log("info", `client ip: ${clientIp}`);
|
||||
|
||||
// --- 校验 token ---
|
||||
const parts = path.split("/").filter(Boolean); // 去掉空段
|
||||
|
||||
const knownApiPaths = ["api", "v1", "v2", "search", "match", "bangumi", "comment"];
|
||||
|
||||
const firstPart = parts[0] || "";
|
||||
const isDefaultToken = globals.token === "87654321";
|
||||
const isValidToken = firstPart === globals.token || firstPart === globals.adminToken;
|
||||
|
||||
globals.currentToken =
|
||||
isValidToken ? firstPart :
|
||||
isDefaultToken && (firstPart === "87654321" || knownApiPaths.includes(firstPart)) ?
|
||||
(firstPart === "87654321" ? firstPart : "87654321") :
|
||||
"";
|
||||
|
||||
if (deployPlatform === "node" && globals.localCacheValid && path !== "/favicon.ico" && path !== "/robots.txt") {
|
||||
await getLocalCaches();
|
||||
}
|
||||
@@ -32,25 +50,9 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
await getRedisCaches();
|
||||
}
|
||||
|
||||
function handleHomepage() {
|
||||
log("info", "Accessed homepage with repository information");
|
||||
return jsonResponse({
|
||||
message: "Welcome to the LogVar Danmu API server",
|
||||
version: globals.VERSION,
|
||||
envs: {
|
||||
...globals.accessedEnvVars,
|
||||
localCacheValid: globals.localCacheValid,
|
||||
redisValid: globals.redisValid
|
||||
},
|
||||
repository: "https://github.com/huangxd-/danmu_api.git",
|
||||
description: "一个人人都能部署的基于 js 的弹幕 API 服务器,支持爱优腾芒哔人韩巴弹幕直接获取,兼容弹弹play的搜索、详情查询和弹幕获取接口规范,并提供日志记录,支持vercel/netlify/edgeone/cloudflare/docker/claw等部署方式,不用提前下载弹幕,没有nas或小鸡也能一键部署。",
|
||||
notice: "本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。有问题提issue或私信机器人都ok,TG MSG ROBOT: [https://t.me/ddjdd_bot]; 推荐加互助群咨询,TG GROUP: [https://t.me/logvar_danmu_group]; 关注频道获取最新更新内容,TG CHANNEL: [https://t.me/logvar_danmu_channel]。"
|
||||
});
|
||||
}
|
||||
|
||||
// GET /
|
||||
if (path === "/" && method === "GET") {
|
||||
return handleHomepage();
|
||||
return handleUI();
|
||||
}
|
||||
|
||||
if (path === "/favicon.ico" || path === "/robots.txt" || method === "OPTIONS") {
|
||||
@@ -64,20 +66,18 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
});
|
||||
}
|
||||
|
||||
// --- 校验 token ---
|
||||
const parts = path.split("/").filter(Boolean); // 去掉空段
|
||||
|
||||
// 如果 token 是默认值 87654321
|
||||
if (globals.token === "87654321") {
|
||||
// 检查第一段是否是已知的 API 路径(不是 token)
|
||||
const knownApiPaths = ["api", "v1", "v2", "search", "match", "bangumi", "comment"];
|
||||
|
||||
if (parts.length > 0) {
|
||||
// 如果第一段是正确的默认 token
|
||||
if (parts[0] === "87654321") {
|
||||
if (parts[0] === "87654321" || parts[0] === globals.adminToken) {
|
||||
// 移除 token,继续处理
|
||||
path = "/" + parts.slice(1).join("/");
|
||||
} else if (!knownApiPaths.includes(parts[0])) {
|
||||
// 对于 /api/config 路径,我们允许无 token 访问,但返回有限信息
|
||||
if (path === "/api/config" && method === "GET") {
|
||||
return handleConfig(false); // 无权限
|
||||
}
|
||||
// 第一段不是已知的 API 路径,可能是错误的 token
|
||||
// 返回 401
|
||||
log("error", `Invalid token in path: ${path}`);
|
||||
@@ -90,7 +90,11 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
}
|
||||
} else {
|
||||
// token 不是默认值,必须严格校验
|
||||
if (parts.length < 1 || parts[0] !== globals.token) {
|
||||
if (parts.length < 1 || (parts[0] !== globals.token && parts[0] !== globals.adminToken)) {
|
||||
// 对于 /api/config 路径,如果使用默认 token,我们允许无 token 访问,但返回有限信息
|
||||
if (path === "/api/config" && method === "GET") {
|
||||
return handleConfig(false); // 无权限
|
||||
}
|
||||
log("error", `Invalid or missing token in path: ${path}`);
|
||||
return jsonResponse(
|
||||
{ errorCode: 401, success: false, errorMessage: "Unauthorized" },
|
||||
@@ -101,10 +105,16 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
path = "/" + parts.slice(1).join("/");
|
||||
}
|
||||
|
||||
// GET /api/config - 获取配置信息 (需要 token)
|
||||
if (path === "/api/config" && method === "GET") {
|
||||
return handleConfig(true); // 有权限
|
||||
}
|
||||
|
||||
log("info", path);
|
||||
|
||||
// 智能处理API路径前缀,确保最终有一个正确的 /api/v2
|
||||
if (path !== "/" && path !== "/api/logs") {
|
||||
if (path !== "/" && path !== "/api/logs" && !path.startsWith('/api/env')
|
||||
&& !path.startsWith('/api/deploy') && !path.startsWith('/api/cache')) {
|
||||
log("info", `[Path Check] Starting path normalization for: "${path}"`);
|
||||
const pathBeforeCleanup = path; // 保存清理前的路径检查是否修改
|
||||
|
||||
@@ -124,7 +134,8 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
|
||||
// 2. 补全:如果路径缺少前缀(例如请求原始路径为 /search/anime),则补全
|
||||
const pathBeforePrefixCheck = path;
|
||||
if (!path.startsWith('/api/v2') && path !== '/' && !path.startsWith('/api/logs')) {
|
||||
if (!path.startsWith('/api/v2') && path !== '/' && !path.startsWith('/api/logs')
|
||||
&& !path.startsWith('/api/env') && !path.startsWith('/api/env') && !path.startsWith('/api/cache')) {
|
||||
log("info", `[Path Check] Path is missing /api/v2 prefix. Adding...`);
|
||||
path = '/api/v2' + path;
|
||||
}
|
||||
@@ -139,7 +150,7 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
|
||||
// GET /
|
||||
if (path === "/" && method === "GET") {
|
||||
return handleHomepage();
|
||||
return handleUI();
|
||||
}
|
||||
|
||||
// GET /api/v2/search/anime
|
||||
@@ -279,19 +290,52 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
|
||||
// GET /api/logs
|
||||
if (path === "/api/logs" && method === "GET") {
|
||||
const logText = globals.logBuffer
|
||||
.map(
|
||||
(log) =>
|
||||
`[${log.timestamp}] ${log.level}: ${formatLogMessage(log.message)}`
|
||||
)
|
||||
.join("\n");
|
||||
return new Response(logText, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
||||
return handleLogs();
|
||||
}
|
||||
|
||||
// POST /api/logs/clear
|
||||
if (path === "/api/logs/clear" && method === "POST") {
|
||||
return handleClearLogs();
|
||||
}
|
||||
|
||||
// POST /api/env/set - 设置环境变量
|
||||
if (path === "/api/env/set" && method === "POST") {
|
||||
return handleSetEnv(req);
|
||||
}
|
||||
|
||||
// POST /api/env/add - 添加环境变量
|
||||
if (path === "/api/env/add" && method === "POST") {
|
||||
return handleAddEnv(req);
|
||||
}
|
||||
|
||||
// POST /api/env/del - 删除环境变量
|
||||
if (path === "/api/env/del" && method === "POST") {
|
||||
return handleDelEnv(req);
|
||||
}
|
||||
|
||||
// POST /api/deploy - 重新部署
|
||||
if (path === "/api/deploy" && method === "POST") {
|
||||
return handleDeploy();
|
||||
}
|
||||
|
||||
// POST /api/cache/clear - 清理缓存
|
||||
if (path === "/api/cache/clear" && method === "POST") {
|
||||
return handleClearCache();
|
||||
}
|
||||
|
||||
return jsonResponse({ message: "Not found" }, 404);
|
||||
}
|
||||
|
||||
|
||||
function isRunningOnVercel() {
|
||||
if (typeof process === 'undefined' || !process.env) {
|
||||
return false;
|
||||
}
|
||||
return !!(
|
||||
process.env.VERCEL ||
|
||||
process.env.VERCEL_ENV ||
|
||||
process.env.VERCEL_URL
|
||||
);
|
||||
}
|
||||
|
||||
// --- Cloudflare Workers 入口 ---
|
||||
export default {
|
||||
@@ -299,7 +343,7 @@ export default {
|
||||
// 获取客户端的真实 IP
|
||||
const clientIp = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || 'unknown';
|
||||
|
||||
return handleRequest(request, env, "cloudflare", clientIp);
|
||||
return handleRequest(request, env, isRunningOnVercel() ? "vercel" : "cloudflare", clientIp);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ import MangoSource from "./sources/mango.js";
|
||||
import BilibiliSource from "./sources/bilibili.js";
|
||||
import YoukuSource from "./sources/youku.js";
|
||||
import OtherSource from "./sources/other.js";
|
||||
import { NodeHandler } from "./configs/handlers/node-handler.js";
|
||||
import { VercelHandler } from "./configs/handlers/vercel-handler.js";
|
||||
import { NetlifyHandler } from "./configs/handlers/netlify-handler.js";
|
||||
import { CloudflareHandler } from "./configs/handlers/cloudflare-handler.js";
|
||||
import { EdgeoneHandler } from "./configs/handlers/edgeone-handler.js";
|
||||
|
||||
// Mock Request class for testing
|
||||
class MockRequest {
|
||||
@@ -300,4 +305,177 @@ test('worker.js API endpoints', async (t) => {
|
||||
// const res = await getTMDBChineseTitle("Blood River", 1, 4);
|
||||
// assert(res === "暗河传", `Expected res === "暗河传", but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试获取全部环境变量
|
||||
// await t.test('Config getAllEnv', async () => {
|
||||
// const handler = new NodeHandler();
|
||||
// const res = handler.getAllEnv();
|
||||
// assert(Number(res.DANMU_LIMIT) === 0, `Expected Number(res.DANMU_LIMIT) === 0, but got ${Number(res.DANMU_LIMIT)}`);
|
||||
// });
|
||||
|
||||
// // 测试获取某个环境变量
|
||||
// await t.test('Config getEnv', async () => {
|
||||
// const handler = new NodeHandler();
|
||||
// const res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 0, `Expected Number(res) === 0, but got ${Number(res)}`);
|
||||
// });
|
||||
|
||||
// // 测试Node设置环境变量
|
||||
// await t.test('Node Config setEnv', async () => {
|
||||
// const handler = new NodeHandler();
|
||||
// let res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 0, `Expected Number(res) === 0, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 1);
|
||||
// res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 1, `Expected Number(res) === 1, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 0);
|
||||
// });
|
||||
//
|
||||
// // 测试Node添加和删除环境变量
|
||||
// await t.test('Node Config addEnv and del Env', async () => {
|
||||
// const handler = new NodeHandler();
|
||||
// await handler.addEnv("UPSTASH_REDIS_REST_TOKEN", "xxxx");
|
||||
// let res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "xxxx", `Expected res === "xxxx", but got ${res}`);
|
||||
// await handler.delEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "", `Expected res === "", but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Vercel设置环境变量
|
||||
// await t.test('Vercel Config setEnv', async () => {
|
||||
// const handler = new VercelHandler();
|
||||
// let res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 0, `Expected Number(res) === 0, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 1);
|
||||
// res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 1, `Expected Number(res) === 1, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 0);
|
||||
// });
|
||||
//
|
||||
// // 测试Vercel添加和删除环境变量
|
||||
// await t.test('Vercel Config addEnv and del Env', async () => {
|
||||
// const handler = new VercelHandler();
|
||||
// await handler.addEnv("UPSTASH_REDIS_REST_TOKEN", "xxxx");
|
||||
// let res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "xxxx", `Expected res === "xxxx", but got ${res}`);
|
||||
// await handler.delEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "", `Expected res === "", but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Vercel项目变量是否生效
|
||||
// await t.test('Vercel Check Params', async () => {
|
||||
// const handler = new VercelHandler();
|
||||
// const res = await handler.checkParams("", "", "");
|
||||
// assert(res, `Expected res is true, but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Vercel触发部署
|
||||
// await t.test('Vercel deploy', async () => {
|
||||
// const handler = new VercelHandler();
|
||||
// const res = await handler.deploy();
|
||||
// assert(res, `Expected res is true, but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Netlify设置环境变量
|
||||
// await t.test('Netlify Config setEnv', async () => {
|
||||
// const handler = new NetlifyHandler();
|
||||
// let res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 0, `Expected Number(res) === 0, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 1);
|
||||
// res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 1, `Expected Number(res) === 1, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 0);
|
||||
// });
|
||||
//
|
||||
// // 测试Netlify添加和删除环境变量
|
||||
// await t.test('Netlify Config addEnv and del Env', async () => {
|
||||
// const handler = new NetlifyHandler();
|
||||
// await handler.addEnv("UPSTASH_REDIS_REST_TOKEN", "xxxx");
|
||||
// let res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "xxxx", `Expected res === "xxxx", but got ${res}`);
|
||||
// await handler.delEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "", `Expected res === "", but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Netlify项目变量是否生效
|
||||
// await t.test('Netlify Check Params', async () => {
|
||||
// const handler = new NetlifyHandler();
|
||||
// const res = await handler.checkParams("", "", "");
|
||||
// assert(res, `Expected res is true, but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Netlify触发部署
|
||||
// await t.test('Netlify deploy', async () => {
|
||||
// const handler = new NetlifyHandler();
|
||||
// const res = await handler.deploy();
|
||||
// assert(res, `Expected res is true, but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Cloudflare设置环境变量
|
||||
// await t.test('Cloudflare Config setEnv', async () => {
|
||||
// const handler = new CloudflareHandler();
|
||||
// let res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 0, `Expected Number(res) === 0, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 1);
|
||||
// res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 1, `Expected Number(res) === 1, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 0);
|
||||
// });
|
||||
|
||||
// // 测试Cloudflare添加和删除环境变量
|
||||
// await t.test('Cloudflare Config addEnv and del Env', async () => {
|
||||
// const handler = new CloudflareHandler();
|
||||
// await handler.addEnv("UPSTASH_REDIS_REST_TOKEN", "xxxx");
|
||||
// let res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "xxxx", `Expected res === "xxxx", but got ${res}`);
|
||||
// await handler.delEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// res = handler.getEnv("UPSTASH_REDIS_REST_TOKEN");
|
||||
// assert(res === "", `Expected res === "", but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Cloudflare项目变量是否生效
|
||||
// await t.test('Cloudflare Check Params', async () => {
|
||||
// const handler = new CloudflareHandler();
|
||||
// const res = await handler.checkParams("", "", "");
|
||||
// assert(res, `Expected res is true, but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Edgeone设置环境变量
|
||||
// await t.test('Edgeone Config setEnv', async () => {
|
||||
// const handler = new EdgeoneHandler();
|
||||
// let res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 0, `Expected Number(res) === 0, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 1);
|
||||
// res = handler.getEnv("DANMU_LIMIT");
|
||||
// assert(Number(res) === 1, `Expected Number(res) === 1, but got ${Number(res)}`);
|
||||
// await handler.setEnv("DANMU_LIMIT", 0);
|
||||
// });
|
||||
|
||||
// // 测试Edgeone添加和删除环境变量
|
||||
// await t.test('Edgeone Config addEnv and del Env', async () => {
|
||||
// const handler = new EdgeoneHandler();
|
||||
// await handler.addEnv("PROXY_URL", "xxxx");
|
||||
// let res = handler.getEnv("PROXY_URL");
|
||||
// assert(res === "xxxx", `Expected res === "xxxx", but got ${res}`);
|
||||
// await handler.delEnv("PROXY_URL");
|
||||
// res = handler.getEnv("PROXY_URL");
|
||||
// assert(res === "", `Expected res === "", but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Edgeone项目变量是否生效
|
||||
// await t.test('Edgeone Check Params', async () => {
|
||||
// const handler = new EdgeoneHandler();
|
||||
// const res = await handler.checkParams("", "", "");
|
||||
// assert(res, `Expected res is true, but got ${res}`);
|
||||
// });
|
||||
|
||||
// // 测试Edgeone触发部署
|
||||
// await t.test('Edgeone deploy', async () => {
|
||||
// const handler = new EdgeoneHandler();
|
||||
// const res = await handler.deploy();
|
||||
// assert(res, `Expected res is true, but got ${res}`);
|
||||
// });
|
||||
});
|
||||
Reference in new issue
Block a user