mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-14 08:09:19 +00:00
💩
This commit is contained in:
parent
49de188740
commit
42e02fc381
160
.github/GITHUB-BOT-SETUP.md
vendored
160
.github/GITHUB-BOT-SETUP.md
vendored
@ -1,160 +0,0 @@
|
|||||||
# GitHub Bot 自动回复设置指南
|
|
||||||
|
|
||||||
本项目已集成GitHub Actions驱动的自动回复机器人,可以自动处理Issues和Pull Requests。
|
|
||||||
|
|
||||||
## 🚀 功能特性
|
|
||||||
|
|
||||||
### ✅ 已实现功能
|
|
||||||
- **Issue自动回复**: 根据标题关键词智能回复
|
|
||||||
- **PR自动回复**: 欢迎新贡献者并提供审核清单
|
|
||||||
- **智能标签**: 根据内容自动添加相关标签
|
|
||||||
- **定期状态更新**: 每周检查并更新长期无响应的issue
|
|
||||||
- **生成周报**: 统计项目活动数据
|
|
||||||
|
|
||||||
### 🤖 自动回复类型
|
|
||||||
|
|
||||||
#### Issue回复模板
|
|
||||||
- 🐛 **Bug报告**: 引导用户提供复现步骤和环境信息
|
|
||||||
- 🚀 **功能建议**: 说明评估流程和时间安排
|
|
||||||
- 📖 **文档问题**: 承诺改进文档质量
|
|
||||||
- 👋 **通用回复**: 感谢反馈并说明处理流程
|
|
||||||
|
|
||||||
#### PR回复模板
|
|
||||||
- 🎉 **欢迎贡献者**: 感谢贡献并说明审核流程
|
|
||||||
- 📋 **审核清单**: 提醒代码规范、测试、文档等要求
|
|
||||||
- 📊 **PR统计**: 显示修改文件数和PR类型
|
|
||||||
- 🏷️ **自动标签**: 根据内容和规模自动分类
|
|
||||||
|
|
||||||
## 📋 设置步骤
|
|
||||||
|
|
||||||
### 1. 创建所需标签
|
|
||||||
|
|
||||||
在仓库的 **Issues** → **Labels** 页面创建以下标签:
|
|
||||||
|
|
||||||
#### 基础标签
|
|
||||||
```
|
|
||||||
bug - 🐛 - #d73a49 - Bug报告
|
|
||||||
enhancement - ✨ - #a2eeef - 功能增强
|
|
||||||
documentation - 📖 - #0075ca - 文档相关
|
|
||||||
question - ❓ - #d876e3 - 问题咨询
|
|
||||||
performance - ⚡ - #fbca04 - 性能优化
|
|
||||||
security - 🔒 - #b60205 - 安全相关
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 优先级标签
|
|
||||||
```
|
|
||||||
priority/high - 🔴 - #b60205 - 高优先级
|
|
||||||
priority/medium - 🟡 - #fbca04 - 中优先级
|
|
||||||
priority/low - 🟢 - #0e8a16 - 低优先级
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PR大小标签
|
|
||||||
```
|
|
||||||
size/small - S - #28a745 - 小型PR (≤3文件)
|
|
||||||
size/medium - M - #ffc107 - 中型PR (4-10文件)
|
|
||||||
size/large - L - #dc3545 - 大型PR (>10文件)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 平台标签
|
|
||||||
```
|
|
||||||
platform/windows - 🪟 - #0078d4 - Windows平台
|
|
||||||
platform/linux - 🐧 - #f89820 - Linux平台
|
|
||||||
platform/mac - 🍎 - #999999 - macOS平台
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 组件标签
|
|
||||||
```
|
|
||||||
component/bilibili - 📺 - #00a1d6 - B站相关
|
|
||||||
component/tiktok - 🎵 - #ff0050 - 抖音相关
|
|
||||||
component/youtube - ▶️ - #ff0000 - YouTube相关
|
|
||||||
component/music - 🎵 - #1db954 - 音乐功能
|
|
||||||
component/summary - 📄 - #6f42c1 - 链接总结功能
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 状态标签
|
|
||||||
```
|
|
||||||
stale - ⏰ - #795548 - 长期无响应
|
|
||||||
needs-response - 💬 - #8e44ad - 需要回复
|
|
||||||
refactor - ♻️ - #00d4aa - 重构相关
|
|
||||||
test - 🧪 - #17becf - 测试相关
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 启用GitHub Actions
|
|
||||||
|
|
||||||
1. 进入仓库 **Settings** → **Actions** → **General**
|
|
||||||
2. 选择 **Allow all actions and reusable workflows**
|
|
||||||
3. 在 **Workflow permissions** 部分选择 **Read and write permissions**
|
|
||||||
4. 勾选 **Allow GitHub Actions to create and approve pull requests**
|
|
||||||
|
|
||||||
### 3. 创建里程碑(可选)
|
|
||||||
|
|
||||||
为bug自动分配功能创建里程碑:
|
|
||||||
1. 进入 **Issues** → **Milestones**
|
|
||||||
2. 创建如 `v1.1.0`、`v1.2.0` 等版本里程碑
|
|
||||||
3. bot会自动将bug分配到最近的开放里程碑
|
|
||||||
|
|
||||||
### 4. 测试设置
|
|
||||||
|
|
||||||
创建一个测试issue验证:
|
|
||||||
1. 标题包含 "bug" 或 "功能" 关键词
|
|
||||||
2. 查看是否收到自动回复
|
|
||||||
3. 检查是否自动添加了相关标签
|
|
||||||
|
|
||||||
## 🔧 自定义配置
|
|
||||||
|
|
||||||
### 修改回复模板
|
|
||||||
|
|
||||||
编辑 `.github/workflows/` 目录下的workflow文件:
|
|
||||||
- `auto-reply-issues.yml` - Issue回复模板
|
|
||||||
- `auto-reply-prs.yml` - PR回复模板
|
|
||||||
- `auto-label.yml` - 标签规则
|
|
||||||
- `status-update.yml` - 定期更新规则
|
|
||||||
|
|
||||||
### 调整触发条件
|
|
||||||
|
|
||||||
修改workflow文件中的 `on` 部分:
|
|
||||||
```yaml
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, labeled] # 添加labeled触发
|
|
||||||
pull_request:
|
|
||||||
types: [opened, edited] # 添加edited触发
|
|
||||||
```
|
|
||||||
|
|
||||||
### 修改定时任务
|
|
||||||
|
|
||||||
在 `status-update.yml` 中调整cron表达式:
|
|
||||||
```yaml
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 2 * * *' # 每天凌晨2点运行
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 监控和维护
|
|
||||||
|
|
||||||
### 查看运行日志
|
|
||||||
1. 进入 **Actions** 页面
|
|
||||||
2. 点击具体的workflow运行记录
|
|
||||||
3. 查看详细日志和错误信息
|
|
||||||
|
|
||||||
### 常见问题排查
|
|
||||||
- **权限错误**: 检查Actions权限设置
|
|
||||||
- **标签不存在**: 确保创建了所有必需的标签
|
|
||||||
- **API限制**: GitHub API有频率限制,大量操作时可能触发
|
|
||||||
|
|
||||||
### 性能优化建议
|
|
||||||
- 限制单次处理的issue数量(当前设置为50)
|
|
||||||
- 合理设置触发条件避免重复运行
|
|
||||||
- 定期清理过时的workflow运行记录
|
|
||||||
|
|
||||||
## 🎯 最佳实践
|
|
||||||
|
|
||||||
1. **渐进式部署**: 先在测试仓库验证功能
|
|
||||||
2. **定期审查**: 每月检查自动回复效果和用户反馈
|
|
||||||
3. **模板优化**: 根据项目特点调整回复内容
|
|
||||||
4. **标签管理**: 保持标签体系简洁清晰
|
|
||||||
5. **用户反馈**: 收集用户对自动回复的意见
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
📝 **注意**: 此bot完全基于GitHub Actions,无需外部服务器,维护成本极低。所有配置都通过workflow文件管理,便于版本控制和团队协作。
|
|
107
.github/ISSUE_TEMPLATE/bug.yml
vendored
107
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -1,107 +0,0 @@
|
|||||||
name: Bug 反馈
|
|
||||||
description: 当你在代码中发现了一个 Bug,导致应用崩溃或抛出异常,或者有一个组件存在问题,或者某些地方看起来不对劲。
|
|
||||||
title: "[Bug]: "
|
|
||||||
labels: ["bug"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
感谢对项目的支持与关注。在提出问题之前,请确保你已查看相关开发或使用文档:
|
|
||||||
- https://gitee.com/kyrzy0416/rconsole-plugin
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: 这个问题是否已经存在?
|
|
||||||
options:
|
|
||||||
- label: 我已经搜索过现有的问题 (https://gitee.com/kyrzy0416/rconsole-plugin/issues)
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 如何复现
|
|
||||||
description: 请详细告诉我们如何复现你遇到的问题,如涉及代码,可提供一个最小代码示例,并使用反引号```附上它
|
|
||||||
placeholder: |
|
|
||||||
1. ...
|
|
||||||
2. ...
|
|
||||||
3. ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 预期结果
|
|
||||||
description: 请告诉我们你预期会发生什么。
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 实际结果
|
|
||||||
description: 请告诉我们实际发生了什么。
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 截图或视频
|
|
||||||
description: 如果可以的话,上传任何关于 bug 的截图。
|
|
||||||
value: |
|
|
||||||
[在这里上传图片]
|
|
||||||
- type: dropdown
|
|
||||||
id: severity
|
|
||||||
attributes:
|
|
||||||
label: 问题严重程度
|
|
||||||
description: 这个bug对使用的影响程度?
|
|
||||||
options:
|
|
||||||
- 低 - 轻微影响,有变通方案
|
|
||||||
- 中 - 影响正常使用,但不阻塞
|
|
||||||
- 高 - 严重影响使用或导致崩溃
|
|
||||||
- 紧急 - 完全无法使用,需立即修复
|
|
||||||
default: 1
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: yunzai
|
|
||||||
attributes:
|
|
||||||
label: 使用的本体
|
|
||||||
description: 你当前正在使用的崽?
|
|
||||||
options:
|
|
||||||
- Miao
|
|
||||||
- Trss
|
|
||||||
- 其他
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: systemType
|
|
||||||
attributes:
|
|
||||||
label: 系统类型
|
|
||||||
description: 你当前正在使用的系统类型?
|
|
||||||
options:
|
|
||||||
- Windows
|
|
||||||
- Linux
|
|
||||||
- Mac
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: linuxType
|
|
||||||
attributes:
|
|
||||||
label: Linux类型(如果是)
|
|
||||||
description: 你当前正在使用的Linux是什么系统?
|
|
||||||
options:
|
|
||||||
- Debian
|
|
||||||
- Ubuntu
|
|
||||||
- Arch
|
|
||||||
- CentOS
|
|
||||||
- 其他
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: dropdown
|
|
||||||
id: component
|
|
||||||
attributes:
|
|
||||||
label: 相关功能模块
|
|
||||||
description: 此bug涉及哪个功能模块?
|
|
||||||
options:
|
|
||||||
- Bilibili下载
|
|
||||||
- 抖音/TikTok下载
|
|
||||||
- YouTube下载
|
|
||||||
- 音乐功能
|
|
||||||
- 链接总结
|
|
||||||
- 其他功能
|
|
||||||
- 不确定
|
|
||||||
validations:
|
|
||||||
required: false
|
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,5 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: R插件文档
|
|
||||||
url: https://gitee.com/kyrzy0416/rconsole-plugin
|
|
||||||
about: 专门为朋友们写的Yunzai-Bot插件,专注图片视频分享、生活、健康和学习的插件!
|
|
69
.github/ISSUE_TEMPLATE/feature.yml
vendored
69
.github/ISSUE_TEMPLATE/feature.yml
vendored
@ -1,69 +0,0 @@
|
|||||||
name: 功能建议
|
|
||||||
description: 对本项目提出一个功能建议
|
|
||||||
title: "[功能建议]: "
|
|
||||||
labels: ["enhancement"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
感谢提出功能建议,我们将仔细考虑!
|
|
||||||
- type: textarea
|
|
||||||
id: related-problem
|
|
||||||
attributes:
|
|
||||||
label: 你的功能建议是否和某个问题相关?
|
|
||||||
description: 清晰并简洁地描述问题是什么,例如,当我...时,我总是感到困扰。
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: desired-solution
|
|
||||||
attributes:
|
|
||||||
label: 你希望看到什么解决方案?
|
|
||||||
description: 清晰并简洁地描述你希望发生的事情。
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: 你考虑过哪些替代方案?
|
|
||||||
description: 清晰并简洁地描述你考虑过的任何替代解决方案或功能。
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: additional-context
|
|
||||||
attributes:
|
|
||||||
label: 你有其他上下文或截图吗?
|
|
||||||
description: 在此处添加有关功能请求的任何其他上下文或截图。
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: dropdown
|
|
||||||
id: feature_type
|
|
||||||
attributes:
|
|
||||||
label: 功能类型
|
|
||||||
description: 这个功能建议属于哪个类别?
|
|
||||||
options:
|
|
||||||
- 新平台支持 (如新的视频网站)
|
|
||||||
- 现有功能增强
|
|
||||||
- 用户体验改进
|
|
||||||
- 性能优化
|
|
||||||
- 安全相关
|
|
||||||
- 其他
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: priority_suggestion
|
|
||||||
attributes:
|
|
||||||
label: 建议优先级
|
|
||||||
description: 你认为这个功能的重要程度?
|
|
||||||
options:
|
|
||||||
- 低 - 有了更好,没有也可以
|
|
||||||
- 中 - 比较有用的功能
|
|
||||||
- 高 - 非常需要的功能
|
|
||||||
default: 1
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: 意向参与贡献
|
|
||||||
options:
|
|
||||||
- label: 我有意向参与具体功能的开发实现并将代码贡献回到上游社区
|
|
||||||
required: false
|
|
140
.github/workflows/auto-label.yml
vendored
140
.github/workflows/auto-label.yml
vendored
@ -1,140 +0,0 @@
|
|||||||
name: Auto Label Issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Add Labels Based on Content
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const title = context.payload.issue.title.toLowerCase();
|
|
||||||
const body = context.payload.issue.body?.toLowerCase() || '';
|
|
||||||
const labels = [];
|
|
||||||
|
|
||||||
// 根据关键词自动添加标签
|
|
||||||
if (title.includes('bug') || title.includes('错误') || title.includes('问题') ||
|
|
||||||
body.includes('bug') || body.includes('错误') || body.includes('异常')) {
|
|
||||||
labels.push('bug');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.includes('feature') || title.includes('功能') || title.includes('建议') ||
|
|
||||||
title.includes('enhancement') || body.includes('功能') || body.includes('建议')) {
|
|
||||||
labels.push('enhancement');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.includes('doc') || title.includes('文档') || title.includes('说明') ||
|
|
||||||
body.includes('文档') || body.includes('说明') || body.includes('document')) {
|
|
||||||
labels.push('documentation');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.includes('help') || title.includes('求助') || title.includes('question') ||
|
|
||||||
title.includes('如何') || title.includes('怎么') || body.includes('求助')) {
|
|
||||||
labels.push('question');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.includes('性能') || title.includes('performance') || title.includes('优化') ||
|
|
||||||
body.includes('性能') || body.includes('慢') || body.includes('优化')) {
|
|
||||||
labels.push('performance');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.includes('安全') || title.includes('security') ||
|
|
||||||
body.includes('安全') || body.includes('漏洞')) {
|
|
||||||
labels.push('security');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据关键词和表单内容添加优先级标签
|
|
||||||
if (title.includes('紧急') || title.includes('urgent') || title.includes('critical') ||
|
|
||||||
body.includes('紧急') || body.includes('严重') || body.includes('紧急 - 完全无法使用') ||
|
|
||||||
body.includes('高 - 严重影响使用')) {
|
|
||||||
labels.push('priority/high');
|
|
||||||
} else if (title.includes('minor') || title.includes('小') || body.includes('小问题') ||
|
|
||||||
body.includes('低 - 轻微影响')) {
|
|
||||||
labels.push('priority/low');
|
|
||||||
} else {
|
|
||||||
labels.push('priority/medium');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据平台关键词添加标签
|
|
||||||
if (title.includes('windows') || body.includes('windows')) {
|
|
||||||
labels.push('platform/windows');
|
|
||||||
}
|
|
||||||
if (title.includes('linux') || body.includes('linux')) {
|
|
||||||
labels.push('platform/linux');
|
|
||||||
}
|
|
||||||
if (title.includes('mac') || title.includes('darwin') || body.includes('mac')) {
|
|
||||||
labels.push('platform/mac');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据组件关键词和表单选择添加标签
|
|
||||||
if (title.includes('bilibili') || title.includes('bili') || body.includes('bilibili') ||
|
|
||||||
body.includes('Bilibili下载')) {
|
|
||||||
labels.push('component/bilibili');
|
|
||||||
}
|
|
||||||
if (title.includes('tiktok') || title.includes('抖音') || body.includes('tiktok') ||
|
|
||||||
body.includes('抖音/TikTok下载')) {
|
|
||||||
labels.push('component/tiktok');
|
|
||||||
}
|
|
||||||
if (title.includes('youtube') || body.includes('youtube') || body.includes('YouTube下载')) {
|
|
||||||
labels.push('component/youtube');
|
|
||||||
}
|
|
||||||
if (title.includes('music') || title.includes('音乐') || body.includes('音乐') ||
|
|
||||||
body.includes('音乐功能')) {
|
|
||||||
labels.push('component/music');
|
|
||||||
}
|
|
||||||
if (title.includes('总结') || title.includes('summary') || body.includes('链接总结')) {
|
|
||||||
labels.push('component/summary');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labels.length > 0) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
labels: labels
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Added labels: ${labels.join(', ')}`);
|
|
||||||
|
|
||||||
- name: Add Milestone for Bugs
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const labels = context.payload.issue.labels.map(label => label.name);
|
|
||||||
const title = context.payload.issue.title.toLowerCase();
|
|
||||||
|
|
||||||
// 为bug类issue自动分配到下个版本里程碑
|
|
||||||
if (labels.includes('bug') || title.includes('bug') || title.includes('错误')) {
|
|
||||||
try {
|
|
||||||
// 获取所有开放的里程碑
|
|
||||||
const { data: milestones } = await github.rest.issues.listMilestones({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'open',
|
|
||||||
sort: 'due_on',
|
|
||||||
direction: 'asc'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (milestones.length > 0) {
|
|
||||||
// 分配到最近的里程碑
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
milestone: milestones[0].number
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Assigned to milestone: ${milestones[0].title}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('No milestones found or error assigning milestone');
|
|
||||||
}
|
|
||||||
}
|
|
81
.github/workflows/auto-reply-issues.yml
vendored
81
.github/workflows/auto-reply-issues.yml
vendored
@ -1,81 +0,0 @@
|
|||||||
name: Auto Reply to Issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-reply:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Reply to Issue
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const issueNumber = context.issue.number;
|
|
||||||
const issueTitle = context.payload.issue.title;
|
|
||||||
const issueAuthor = context.payload.issue.user.login;
|
|
||||||
|
|
||||||
// 根据标题关键词判断类型
|
|
||||||
let replyMessage = '';
|
|
||||||
let emoji = '👋';
|
|
||||||
|
|
||||||
if (issueTitle.toLowerCase().includes('bug') || issueTitle.toLowerCase().includes('错误') || issueTitle.toLowerCase().includes('问题')) {
|
|
||||||
emoji = '🐛';
|
|
||||||
replyMessage = `${emoji} Hi @${issueAuthor}!
|
|
||||||
|
|
||||||
感谢您报告这个bug!我们已经收到您的反馈,会尽快调查并修复。
|
|
||||||
|
|
||||||
为了更好地帮助您解决问题,请确保提供:
|
|
||||||
- [ ] 详细的问题描述
|
|
||||||
- [ ] 复现步骤
|
|
||||||
- [ ] 运行环境信息
|
|
||||||
- [ ] 相关的错误日志
|
|
||||||
|
|
||||||
我们会尽快回复您!`;
|
|
||||||
} else if (issueTitle.toLowerCase().includes('feature') || issueTitle.toLowerCase().includes('功能') || issueTitle.toLowerCase().includes('建议')) {
|
|
||||||
emoji = '🚀';
|
|
||||||
replyMessage = `${emoji} Hi @${issueAuthor}!
|
|
||||||
|
|
||||||
感谢您的功能建议!我们很高兴收到新的想法和建议。
|
|
||||||
|
|
||||||
我们会仔细评估您的建议:
|
|
||||||
- [ ] 评估技术可行性
|
|
||||||
- [ ] 分析对现有功能的影响
|
|
||||||
- [ ] 确定开发优先级
|
|
||||||
|
|
||||||
如果您有更多细节或使用场景,欢迎补充!`;
|
|
||||||
} else if (issueTitle.toLowerCase().includes('doc') || issueTitle.toLowerCase().includes('文档') || issueTitle.toLowerCase().includes('说明')) {
|
|
||||||
emoji = '📖';
|
|
||||||
replyMessage = `${emoji} Hi @${issueAuthor}!
|
|
||||||
|
|
||||||
感谢您关注文档改进!清晰的文档对项目非常重要。
|
|
||||||
|
|
||||||
我们会:
|
|
||||||
- [ ] 审查当前文档内容
|
|
||||||
- [ ] 补充缺失的说明
|
|
||||||
- [ ] 优化文档结构
|
|
||||||
|
|
||||||
您的反馈很有价值!`;
|
|
||||||
} else {
|
|
||||||
replyMessage = `${emoji} Hi @${issueAuthor}!
|
|
||||||
|
|
||||||
感谢您提交issue!我们已经收到您的反馈。
|
|
||||||
|
|
||||||
我们会尽快处理您的请求:
|
|
||||||
- [ ] 分析问题内容
|
|
||||||
- [ ] 确定处理方案
|
|
||||||
- [ ] 及时反馈进展
|
|
||||||
|
|
||||||
如有任何疑问,随时与我们联系!`;
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
body: replyMessage
|
|
||||||
});
|
|
94
.github/workflows/auto-reply-prs.yml
vendored
94
.github/workflows/auto-reply-prs.yml
vendored
@ -1,94 +0,0 @@
|
|||||||
name: Auto Reply to Pull Requests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-reply:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Reply to PR
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const prNumber = context.issue.number;
|
|
||||||
const prTitle = context.payload.pull_request.title;
|
|
||||||
const prAuthor = context.payload.pull_request.user.login;
|
|
||||||
const prFiles = context.payload.pull_request.changed_files;
|
|
||||||
|
|
||||||
let replyMessage = `🎉 Hi @${prAuthor}!
|
|
||||||
|
|
||||||
感谢您的贡献!您的Pull Request已经提交,我们会尽快进行代码审核。
|
|
||||||
|
|
||||||
## 📋 审核清单
|
|
||||||
期间请确保以下项目已完成:
|
|
||||||
- [ ] 代码符合项目规范和风格
|
|
||||||
- [ ] 已测试新功能或修复
|
|
||||||
- [ ] 更新了相关文档(如有需要)
|
|
||||||
- [ ] 提交信息清晰明确
|
|
||||||
|
|
||||||
## 📊 PR 统计
|
|
||||||
- 📁 修改文件数:${prFiles}
|
|
||||||
- 🏷️ PR类型:${prTitle.toLowerCase().includes('fix') ? '🐛 Bug修复' :
|
|
||||||
prTitle.toLowerCase().includes('feat') ? '✨ 新功能' :
|
|
||||||
prTitle.toLowerCase().includes('doc') ? '📖 文档更新' :
|
|
||||||
prTitle.toLowerCase().includes('refactor') ? '♻️ 重构' : '🔧 其他改进'}
|
|
||||||
|
|
||||||
## 🔄 下一步
|
|
||||||
我们会在1-3个工作日内完成初步审核。如有问题会及时与您沟通。
|
|
||||||
|
|
||||||
再次感谢您对项目的贡献! 🙏`;
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body: replyMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
- name: Add PR Labels
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const prTitle = context.payload.pull_request.title.toLowerCase();
|
|
||||||
const labels = [];
|
|
||||||
|
|
||||||
// 根据PR标题自动添加标签
|
|
||||||
if (prTitle.includes('fix') || prTitle.includes('修复')) {
|
|
||||||
labels.push('bug');
|
|
||||||
}
|
|
||||||
if (prTitle.includes('feat') || prTitle.includes('功能') || prTitle.includes('add')) {
|
|
||||||
labels.push('enhancement');
|
|
||||||
}
|
|
||||||
if (prTitle.includes('doc') || prTitle.includes('文档')) {
|
|
||||||
labels.push('documentation');
|
|
||||||
}
|
|
||||||
if (prTitle.includes('refactor') || prTitle.includes('重构')) {
|
|
||||||
labels.push('refactor');
|
|
||||||
}
|
|
||||||
if (prTitle.includes('test') || prTitle.includes('测试')) {
|
|
||||||
labels.push('test');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据文件数量添加大小标签
|
|
||||||
const fileCount = context.payload.pull_request.changed_files;
|
|
||||||
if (fileCount <= 3) {
|
|
||||||
labels.push('size/small');
|
|
||||||
} else if (fileCount <= 10) {
|
|
||||||
labels.push('size/medium');
|
|
||||||
} else {
|
|
||||||
labels.push('size/large');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labels.length > 0) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
labels: labels
|
|
||||||
});
|
|
||||||
}
|
|
150
.github/workflows/status-update.yml
vendored
150
.github/workflows/status-update.yml
vendored
@ -1,150 +0,0 @@
|
|||||||
name: Weekly Status Update
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 10 * * 1' # 每周一上午10点(UTC时间)
|
|
||||||
workflow_dispatch: # 允许手动触发
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
status-update:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Update Stale Issues
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { data: issues } = await github.rest.issues.listForRepo({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'open',
|
|
||||||
sort: 'updated',
|
|
||||||
direction: 'asc',
|
|
||||||
per_page: 50
|
|
||||||
});
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
let updatedCount = 0;
|
|
||||||
|
|
||||||
for (const issue of issues) {
|
|
||||||
// 跳过PR
|
|
||||||
if (issue.pull_request) continue;
|
|
||||||
|
|
||||||
const lastUpdated = new Date(issue.updated_at);
|
|
||||||
const daysSinceUpdate = Math.floor((now - lastUpdated) / (1000 * 60 * 60 * 24));
|
|
||||||
const daysSinceCreated = Math.floor((now - new Date(issue.created_at)) / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
const hasStaleLabel = issue.labels.some(label => label.name === 'stale');
|
|
||||||
const hasNeedsResponseLabel = issue.labels.some(label => label.name === 'needs-response');
|
|
||||||
const isPriorityHigh = issue.labels.some(label => label.name === 'priority/high');
|
|
||||||
|
|
||||||
// 7天无更新且不是高优先级的issue
|
|
||||||
if (daysSinceUpdate >= 7 && !hasStaleLabel && !isPriorityHigh) {
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
body: `👋 这个issue已经一周没有更新了。
|
|
||||||
|
|
||||||
如果问题仍然存在,请提供更多信息:
|
|
||||||
- 是否还能复现?
|
|
||||||
- 有没有新的错误信息?
|
|
||||||
- 是否尝试了其他解决方案?
|
|
||||||
|
|
||||||
如果问题已解决,欢迎关闭此issue。如果30天内没有响应,此issue将被自动关闭。`
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
labels: ['stale']
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 30天无更新的stale issue自动关闭
|
|
||||||
else if (daysSinceUpdate >= 30 && hasStaleLabel) {
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
body: `🤖 此issue因长期无响应被自动关闭。
|
|
||||||
|
|
||||||
如果问题仍然存在,请创建新的issue并提供详细信息。感谢您的理解!`
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
state: 'closed',
|
|
||||||
state_reason: 'not_planned'
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要回复的issue提醒
|
|
||||||
else if (daysSinceUpdate >= 3 && hasNeedsResponseLabel) {
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
body: `⏰ 提醒:此issue等待回复已超过3天。
|
|
||||||
|
|
||||||
我们会尽快处理,感谢您的耐心等待!`
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Updated ${updatedCount} issues`);
|
|
||||||
|
|
||||||
- name: Generate Weekly Report
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const oneWeekAgo = new Date();
|
|
||||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
||||||
|
|
||||||
// 获取本周的issues
|
|
||||||
const { data: newIssues } = await github.rest.search.issuesAndPullRequests({
|
|
||||||
q: `repo:${context.repo.owner}/${context.repo.repo} is:issue created:>=${oneWeekAgo.toISOString().split('T')[0]}`,
|
|
||||||
sort: 'created',
|
|
||||||
order: 'desc'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取本周的PRs
|
|
||||||
const { data: newPRs } = await github.rest.search.issuesAndPullRequests({
|
|
||||||
q: `repo:${context.repo.owner}/${context.repo.repo} is:pr created:>=${oneWeekAgo.toISOString().split('T')[0]}`,
|
|
||||||
sort: 'created',
|
|
||||||
order: 'desc'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取本周关闭的issues
|
|
||||||
const { data: closedIssues } = await github.rest.search.issuesAndPullRequests({
|
|
||||||
q: `repo:${context.repo.owner}/${context.repo.repo} is:issue closed:>=${oneWeekAgo.toISOString().split('T')[0]}`,
|
|
||||||
sort: 'updated',
|
|
||||||
order: 'desc'
|
|
||||||
});
|
|
||||||
|
|
||||||
const report = `📊 **本周活动报告** (${oneWeekAgo.toISOString().split('T')[0]} ~ ${new Date().toISOString().split('T')[0]})
|
|
||||||
|
|
||||||
## 📈 统计数据
|
|
||||||
- 🎯 新增 Issues: ${newIssues.total_count}
|
|
||||||
- 🔀 新增 PRs: ${newPRs.total_count}
|
|
||||||
- ✅ 关闭 Issues: ${closedIssues.total_count}
|
|
||||||
|
|
||||||
## 🏷️ Issue 分类
|
|
||||||
${newIssues.items.length > 0 ? newIssues.items.slice(0, 5).map(issue =>
|
|
||||||
\`- [\${issue.title}](\${issue.html_url}) - \${issue.labels.map(l => l.name).join(', ') || '未分类'}\`
|
|
||||||
).join('\\n') : '本周暂无新issue'}
|
|
||||||
|
|
||||||
感谢所有贡献者的参与! 🙏`;
|
|
||||||
|
|
||||||
console.log(report);
|
|
218
apps/query.js
218
apps/query.js
@ -1,218 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
// 常量
|
|
||||||
import { CAT_LIMIT, COMMON_USER_AGENT } from "../constants/constant.js";
|
|
||||||
|
|
||||||
export class query extends plugin {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
name: "R插件查询类",
|
|
||||||
dsc: "R插件查询相关指令",
|
|
||||||
event: "message.group",
|
|
||||||
priority: 500,
|
|
||||||
rule: [
|
|
||||||
{
|
|
||||||
reg: "^#医药查询(.*)$",
|
|
||||||
fnc: "doctor",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#cat$",
|
|
||||||
fnc: "cat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#推荐软件$",
|
|
||||||
fnc: "softwareRecommended",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#买家秀$",
|
|
||||||
fnc: "buyerShow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#累了$",
|
|
||||||
fnc: "cospro",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#竹白(.*)",
|
|
||||||
fnc: "zhubaiSearch",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async doctor(e) {
|
|
||||||
const keyword = e.msg.replace("#医药查询", "").trim();
|
|
||||||
const url = `https://server.dayi.org.cn/api/search2?keyword=${ keyword }&pageNo=1&pageSize=10`;
|
|
||||||
try {
|
|
||||||
const res = await fetch(url)
|
|
||||||
.then(resp => resp.json())
|
|
||||||
.then(resp => resp.list);
|
|
||||||
let msg = [];
|
|
||||||
for (let element of res) {
|
|
||||||
const title = this.removeTag(element.title);
|
|
||||||
const thumbnail = element?.thumbnail || element?.auditDoctor?.thumbnail;
|
|
||||||
const doctor = `\n\n👨⚕️ 医生信息:${ element?.auditDoctor?.name } - ${ element?.auditDoctor?.clinicProfessional } - ${ element?.auditDoctor?.eduProfessional } - ${ element?.auditDoctor?.institutionName } - ${ element?.auditDoctor?.institutionLevel } - ${ element?.auditDoctor?.departmentName }`
|
|
||||||
const template = `📌 ${ title } - ${ element.secondTitle }${ element?.auditDoctor ? doctor : '' }\n\n📝 简介:${ element.introduction }`;
|
|
||||||
if (thumbnail) {
|
|
||||||
msg.push({
|
|
||||||
message: [segment.image(thumbnail), { type: "text", text: template, }],
|
|
||||||
nickname: e.sender.card || e.user_id,
|
|
||||||
user_id: e.user_id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
msg.push({
|
|
||||||
message: {
|
|
||||||
type: "text",
|
|
||||||
text: template,
|
|
||||||
},
|
|
||||||
nickname: e.sender.card || e.user_id,
|
|
||||||
user_id: e.user_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.reply(await Bot.makeForwardMsg(msg));
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async cat(e) {
|
|
||||||
const [shibes, cats] = await Promise.allSettled([
|
|
||||||
fetch(`https://shibe.online/api/cats?count=${ CAT_LIMIT }`).then(data => data.json()),
|
|
||||||
fetch(`https://api.thecatapi.com/v1/images/search?limit=${ CAT_LIMIT }`).then(data =>
|
|
||||||
data.json(),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const shibeUrls = shibes.status === "fulfilled" ? shibes.value : [];
|
|
||||||
const catUrls = cats.status === "fulfilled" ? cats.value.map(item => item.url) : [];
|
|
||||||
const reqRes = [...shibeUrls, ...catUrls];
|
|
||||||
|
|
||||||
e.reply("涩图也不看了,就看猫是吧");
|
|
||||||
|
|
||||||
const images = reqRes.map(item => ({
|
|
||||||
message: segment.image(item),
|
|
||||||
nickname: this.e.sender.card || this.e.user_id,
|
|
||||||
user_id: this.e.user_id,
|
|
||||||
}));
|
|
||||||
e.reply(await Bot.makeForwardMsg(images));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async softwareRecommended(e) {
|
|
||||||
// 接口
|
|
||||||
const urls = [
|
|
||||||
"https://www.ghxi.com/ghapi?type=query&n=pc",
|
|
||||||
"https://www.ghxi.com/ghapi?type=query&n=and",
|
|
||||||
];
|
|
||||||
// 一起请求
|
|
||||||
const promises = urls.map(url =>
|
|
||||||
fetch(url)
|
|
||||||
.then(resp => resp.json())
|
|
||||||
.catch(err => logger.error(err)),
|
|
||||||
);
|
|
||||||
const results = await Promise.allSettled(promises);
|
|
||||||
const msg = results
|
|
||||||
.filter(result => result.status === "fulfilled") // 只保留已解决的 Promise
|
|
||||||
.flatMap(result =>
|
|
||||||
result.value.data.list.map(element => {
|
|
||||||
const template = `推荐软件:${ element.title }\n地址:${ element.url }\n`;
|
|
||||||
return {
|
|
||||||
message: { type: "text", text: template },
|
|
||||||
nickname: e.sender.card || e.user_id,
|
|
||||||
user_id: e.user_id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 异步操作
|
|
||||||
e.reply(await Bot.makeForwardMsg(msg));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async buyerShow(e) {
|
|
||||||
const p1 = fetch("https://api.vvhan.com/api/tao").then(resp => resp.url);
|
|
||||||
const p2 = fetch("https://api.uomg.com/api/rand.img3?format=json")
|
|
||||||
.then(resp => resp.json())
|
|
||||||
.then(resp => resp.imgurl);
|
|
||||||
|
|
||||||
const results = await Promise.allSettled([p1, p2]);
|
|
||||||
const images = results
|
|
||||||
.filter(result => result.status === "fulfilled")
|
|
||||||
.map(result => result.value);
|
|
||||||
|
|
||||||
for (const img of images) {
|
|
||||||
e.reply(segment.image(img));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async cospro(e) {
|
|
||||||
let [res1, res2] = (
|
|
||||||
await Promise.allSettled([
|
|
||||||
fetch("https://imgapi.cn/cos2.php?return=jsonpro").then(resp => resp.json()),
|
|
||||||
fetch("https://imgapi.cn/cos.php?return=jsonpro").then(resp => resp.json()),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.filter(result => result.status === "fulfilled")
|
|
||||||
.map(result => result.value);
|
|
||||||
let req = [...res1.imgurls, ...res2.imgurls];
|
|
||||||
e.reply("哪天克火掉一定是在这个群里面...");
|
|
||||||
let images = req.map(item => ({
|
|
||||||
message: segment.image(encodeURI(item)),
|
|
||||||
nickname: this.e.sender.card || this.e.user_id,
|
|
||||||
user_id: this.e.user_id,
|
|
||||||
}));
|
|
||||||
e.reply(await Bot.makeForwardMsg(images));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 竹白百科
|
|
||||||
async zhubaiSearch(e) {
|
|
||||||
const keyword = e.msg.replace("#竹白", "").trim();
|
|
||||||
if (keyword === "") {
|
|
||||||
e.reply("请输入想了解的内容,例如:#竹白 javascript");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
await axios
|
|
||||||
.post(
|
|
||||||
"https://open.zhubai.wiki/a/zb/s/ep/",
|
|
||||||
{
|
|
||||||
content: 1,
|
|
||||||
keyword: keyword,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(async resp => {
|
|
||||||
const res = resp.data.data;
|
|
||||||
const content = res
|
|
||||||
.sort((a, b) => b.luSort - a.luSort)
|
|
||||||
.map(item => {
|
|
||||||
const { pn, pa, zn, lu, pu, pq, aa, hl } = item;
|
|
||||||
const template = `标题:${ pn }\n${ pa }\n期刊:${ zn }\n发布日期距今:${ lu }\n链接1:${ pu }\n链接2:${ pq }\n\n 大致描述:${ hl
|
|
||||||
.join("\n")
|
|
||||||
.replace(/<\/?font[^>]*>/g, "") }`;
|
|
||||||
return {
|
|
||||||
message: [segment.image(aa), template],
|
|
||||||
nickname: this.e.sender.card || this.e.user_id,
|
|
||||||
user_id: this.e.user_id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
e.reply(await Bot.makeForwardMsg(content));
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除标签
|
|
||||||
removeTag(title) {
|
|
||||||
const titleRex = /<[^>]+>/g;
|
|
||||||
return title.replace(titleRex, "");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,706 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import { formatTime, toGBorTB } from '../utils/other.js'
|
|
||||||
import puppeteer from "../../../lib/puppeteer/puppeteer.js";
|
|
||||||
import PickSongList from "../model/pick-song.js";
|
|
||||||
import NeteaseMusicInfo from '../model/neteaseMusicInfo.js'
|
|
||||||
import { NETEASE_API_CN, NETEASE_SONG_DOWNLOAD, NETEASE_TEMP_API } from "../constants/tools.js";
|
|
||||||
import { COMMON_USER_AGENT, REDIS_YUNZAI_ISOVERSEA, REDIS_YUNZAI_SONGINFO, REDIS_YUNZAI_CLOUDSONGLIST } from "../constants/constant.js";
|
|
||||||
import { downloadAudio, retryAxiosReq } from "../utils/common.js";
|
|
||||||
import { redisExistKey, redisGetKey, redisSetKey } from "../utils/redis-util.js";
|
|
||||||
import { checkAndRemoveFile, checkFileExists, splitPaths } from "../utils/file.js";
|
|
||||||
import { sendMusicCard, getGroupFileUrl, getReplyMsg } from "../utils/yunzai-util.js";
|
|
||||||
import config from "../model/config.js";
|
|
||||||
import FormData from 'form-data';
|
|
||||||
import NodeID3 from 'node-id3';
|
|
||||||
|
|
||||||
let FileSuffix = 'flac'
|
|
||||||
|
|
||||||
export class songRequest extends plugin {
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
name: "R插件点歌",
|
|
||||||
dsc: "实现快捷点歌",
|
|
||||||
priority: 300,
|
|
||||||
rule: [
|
|
||||||
{
|
|
||||||
reg: '^#点歌|#听[1-9][0-9]*|#听[1-9]*$',
|
|
||||||
fnc: 'pickSong'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#播放(.*)",
|
|
||||||
fnc: "playSong"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#?上传$",
|
|
||||||
fnc: "upLoad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: '^#?我的云盘$|#rnc|#RNC',
|
|
||||||
fnc: 'myCloud',
|
|
||||||
permission: 'master'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: '^#?云盘更新|#?更新云盘$',
|
|
||||||
fnc: 'songCloudUpdate',
|
|
||||||
permission: 'master'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: '^#?上传云盘|#?上传网盘$|#rnu|#RNU',
|
|
||||||
fnc: 'uploadCloud',
|
|
||||||
permission: 'master'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: '^#?清除云盘缓存$',
|
|
||||||
fnc: 'cleanCloudData',
|
|
||||||
permission: 'master'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: '^#?文件上传云盘$|#?群文件上传云盘$|#rngu|#RNGU',
|
|
||||||
fnc: 'getLatestDocument',
|
|
||||||
permission: 'master'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
this.toolsConfig = config.getConfig("tools");
|
|
||||||
// 加载网易云Cookie
|
|
||||||
this.neteaseCookie = this.toolsConfig.neteaseCookie
|
|
||||||
// 加载是否转化群语音
|
|
||||||
this.isSendVocal = this.toolsConfig.isSendVocal
|
|
||||||
// 加载是否自建服务器
|
|
||||||
this.useLocalNeteaseAPI = this.toolsConfig.useLocalNeteaseAPI
|
|
||||||
// 加载自建服务器API
|
|
||||||
this.neteaseCloudAPIServer = this.toolsConfig.neteaseCloudAPIServer
|
|
||||||
// 加载网易云解析最高音质
|
|
||||||
this.neteaseCloudAudioQuality = this.toolsConfig.neteaseCloudAudioQuality
|
|
||||||
// 加载识别前缀
|
|
||||||
this.identifyPrefix = this.toolsConfig.identifyPrefix;
|
|
||||||
// 加载是否开启网易云点歌功能
|
|
||||||
this.useNeteaseSongRequest = this.toolsConfig.useNeteaseSongRequest
|
|
||||||
// 加载点歌列表长度
|
|
||||||
this.songRequestMaxList = this.toolsConfig.songRequestMaxList
|
|
||||||
// 视频保存路径
|
|
||||||
this.defaultPath = this.toolsConfig.defaultPath;
|
|
||||||
// uid
|
|
||||||
this.uid = this.toolsConfig.neteaseUserId
|
|
||||||
}
|
|
||||||
|
|
||||||
async pickSong(e) {
|
|
||||||
// 判断功能是否开启
|
|
||||||
if (!this.useNeteaseSongRequest) {
|
|
||||||
logger.info('当前未开启网易云点歌')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 获取自定义API
|
|
||||||
const autoSelectNeteaseApi = await this.pickApi()
|
|
||||||
// 只在群里可以使用
|
|
||||||
let group_id = e.group_id
|
|
||||||
if (!group_id) return
|
|
||||||
// 初始化
|
|
||||||
let songInfo = await redisGetKey(REDIS_YUNZAI_SONGINFO) || []
|
|
||||||
const saveId = songInfo.findIndex(item => item.group_id === e.group_id)
|
|
||||||
let musicDate = { 'group_id': group_id, data: [] }
|
|
||||||
// 获取搜索歌曲列表信息
|
|
||||||
let detailUrl = autoSelectNeteaseApi + "/song/detail?ids={}&time=" + Date.now() //歌曲详情API
|
|
||||||
if (e.msg.replace(/\s+/g, "").match(/点歌(.+)/)) {
|
|
||||||
const songKeyWord = e.msg.replace(/\s+/g, "").match(/点歌(.+)/)[1].replace(/[^\w\u4e00-\u9fa5]/g, '')
|
|
||||||
// 获取云盘歌单列表
|
|
||||||
const cloudSongList = await this.getCloudSong()
|
|
||||||
// 搜索云盘歌单并进行搜索
|
|
||||||
const matchedSongs = cloudSongList.filter(({ songName, singerName }) =>
|
|
||||||
songName.includes(songKeyWord) || singerName.includes(songKeyWord) || songName == songKeyWord || singerName == songKeyWord
|
|
||||||
);
|
|
||||||
// 计算列表数
|
|
||||||
let songListCount = matchedSongs.length >= this.songRequestMaxList ? this.songRequestMaxList : matchedSongs.length
|
|
||||||
let searchCount = this.songRequestMaxList - songListCount
|
|
||||||
for (let i = 0; i < songListCount; i++) {
|
|
||||||
musicDate.data.push({
|
|
||||||
'id': matchedSongs[i].id,
|
|
||||||
'songName': matchedSongs[i].songName,
|
|
||||||
'singerName': matchedSongs[i].singerName,
|
|
||||||
'duration': matchedSongs[i].duration
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let searchUrl = autoSelectNeteaseApi + '/search?keywords={}&limit=' + searchCount//搜索API
|
|
||||||
searchUrl = searchUrl.replace("{}", songKeyWord)
|
|
||||||
await axios.get(searchUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT
|
|
||||||
},
|
|
||||||
}).then(async res => {
|
|
||||||
if (res.data.result.songs || musicDate.data[0]) {
|
|
||||||
try {
|
|
||||||
for (const info of res.data.result.songs) {
|
|
||||||
musicDate.data.push({
|
|
||||||
'id': info.id,
|
|
||||||
'songName': info.name,
|
|
||||||
'singerName': info.artists[0]?.name,
|
|
||||||
'duration': formatTime(info.duration)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.info('并未获取云服务歌曲')
|
|
||||||
}
|
|
||||||
const ids = musicDate.data.map(item => item.id).join(',');
|
|
||||||
detailUrl = detailUrl.replace("{}", ids)
|
|
||||||
await axios.get(detailUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT
|
|
||||||
},
|
|
||||||
}).then(res => {
|
|
||||||
let imgList = {}
|
|
||||||
for (let i = 0; i < res.data.songs.length; i++) {
|
|
||||||
if (res.data.songs[i].al.picUrl.includes('109951169484091680.jpg')) {
|
|
||||||
imgList[res.data.songs[i].id] = 'def'
|
|
||||||
} else {
|
|
||||||
imgList[res.data.songs[i].id] = res.data.songs[i].al.picUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < musicDate.data.length; i++) {
|
|
||||||
const songId = musicDate.data[i].id;
|
|
||||||
if (imgList[songId]) {
|
|
||||||
musicDate.data[i].cover = imgList[songId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (saveId == -1) {
|
|
||||||
songInfo.push(musicDate)
|
|
||||||
} else {
|
|
||||||
songInfo[saveId] = musicDate
|
|
||||||
}
|
|
||||||
await redisSetKey(REDIS_YUNZAI_SONGINFO, songInfo)
|
|
||||||
const data = await new PickSongList(e).getData(musicDate.data)
|
|
||||||
let img = await puppeteer.screenshot("pick-song", data);
|
|
||||||
e.reply(img);
|
|
||||||
} else {
|
|
||||||
e.reply('暂未找到你想听的歌哦~')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (await redisGetKey(REDIS_YUNZAI_SONGINFO) != []) {
|
|
||||||
if (e.msg.replace(/\s+/g, "").match(/^#听(\d+)/)) {
|
|
||||||
const pickNumber = e.msg.replace(/\s+/g, "").match(/^#听(\d+)/)[1] - 1
|
|
||||||
let group_id = e.group_id
|
|
||||||
if (!group_id) return
|
|
||||||
let songInfo = await redisGetKey(REDIS_YUNZAI_SONGINFO)
|
|
||||||
const saveId = songInfo.findIndex(item => item.group_id === e.group_id)
|
|
||||||
const AUTO_NETEASE_SONG_DOWNLOAD = autoSelectNeteaseApi + "/song/url/v1?id={}&level=" + this.neteaseCloudAudioQuality;
|
|
||||||
const pickSongUrl = AUTO_NETEASE_SONG_DOWNLOAD.replace("{}", songInfo[saveId].data[pickNumber].id)
|
|
||||||
const songWikiUrl = autoSelectNeteaseApi + '/song/wiki/summary?id=' + songInfo[saveId].data[pickNumber].id
|
|
||||||
const statusUrl = autoSelectNeteaseApi + '/login/status' //用户状态API
|
|
||||||
const isCkExpired = await this.checkCooike(statusUrl)
|
|
||||||
// // 请求netease数据
|
|
||||||
this.neteasePlay(e, pickSongUrl, songWikiUrl, songInfo[saveId].data, pickNumber, isCkExpired)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 播放策略
|
|
||||||
async playSong(e) {
|
|
||||||
if (!this.useNeteaseSongRequest) {
|
|
||||||
logger.info('当前未开启网易云点歌')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 只在群里可以使用
|
|
||||||
let group_id = e.group_id
|
|
||||||
if (!group_id) return
|
|
||||||
const autoSelectNeteaseApi = await this.pickApi()
|
|
||||||
let songInfo = []
|
|
||||||
// 获取搜索歌曲列表信息
|
|
||||||
const AUTO_NETEASE_SONG_DOWNLOAD = autoSelectNeteaseApi + "/song/url/v1?id={}&level=" + this.neteaseCloudAudioQuality;
|
|
||||||
let searchUrl = autoSelectNeteaseApi + '/search?keywords={}&limit=1' //搜索API
|
|
||||||
let detailUrl = autoSelectNeteaseApi + "/song/detail?ids={}" //歌曲详情API
|
|
||||||
if (e.msg.replace(/\s+/g, "").match(/播放(.+)/)) {
|
|
||||||
const songKeyWord = e.msg.replace(/\s+/g, "").match(/播放(.+)/)[1]
|
|
||||||
searchUrl = searchUrl.replace("{}", songKeyWord)
|
|
||||||
await axios.get(searchUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT
|
|
||||||
},
|
|
||||||
}).then(async res => {
|
|
||||||
if (res.data.result.songs) {
|
|
||||||
for (const info of res.data.result.songs) {
|
|
||||||
songInfo.push({
|
|
||||||
'id': info.id,
|
|
||||||
'songName': info.name,
|
|
||||||
'singerName': info.artists[0]?.name,
|
|
||||||
'duration': formatTime(info.duration)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const ids = songInfo.map(item => item.id).join(',');
|
|
||||||
detailUrl = detailUrl.replace("{}", ids)
|
|
||||||
await axios.get(detailUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT
|
|
||||||
},
|
|
||||||
}).then(res => {
|
|
||||||
for (let i = 0; i < res.data.songs.length; i++) {
|
|
||||||
songInfo[i].cover = res.data.songs[i].al.picUrl
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const pickSongUrl = AUTO_NETEASE_SONG_DOWNLOAD.replace("{}", songInfo[0].id)
|
|
||||||
const statusUrl = autoSelectNeteaseApi + '/login/status' //用户状态API
|
|
||||||
const songWikiUrl = autoSelectNeteaseApi + '/song/wiki/summary?id=' + songInfo[0].id
|
|
||||||
const isCkExpired = await this.checkCooike(statusUrl)
|
|
||||||
this.neteasePlay(e, pickSongUrl, songWikiUrl, songInfo, 0, isCkExpired)
|
|
||||||
} else {
|
|
||||||
e.reply('暂未找到你想听的歌哦~')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 获取云盘信息
|
|
||||||
async myCloud(e) {
|
|
||||||
const autoSelectNeteaseApi = await this.pickApi()
|
|
||||||
const cloudUrl = autoSelectNeteaseApi + '/user/cloud'
|
|
||||||
// 云盘数据API
|
|
||||||
await axios.get(cloudUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
"Cookie": this.neteaseCookie
|
|
||||||
},
|
|
||||||
}).then(res => {
|
|
||||||
const cloudData = {
|
|
||||||
'songCount': res.data.count,
|
|
||||||
'useSize': toGBorTB(res.data.size),
|
|
||||||
'cloudSize': toGBorTB(res.data.maxSize)
|
|
||||||
}
|
|
||||||
e.reply(`云盘数据\n歌曲数量:${cloudData.songCount}\n云盘容量:${cloudData.cloudSize}\n已使用容量:${cloudData.useSize}\n数据可能有延迟`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新云盘
|
|
||||||
async songCloudUpdate(e) {
|
|
||||||
try {
|
|
||||||
await this.cleanCloudData()
|
|
||||||
await this.getCloudSong(e, true)
|
|
||||||
try {
|
|
||||||
await e?.reply('更新成功')
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('trss又拉屎了?')
|
|
||||||
}
|
|
||||||
await this.myCloud(e)
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('更新云盘失败', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传音频文件
|
|
||||||
async upLoad(e) {
|
|
||||||
let msg = await getReplyMsg(e);
|
|
||||||
const musicUrlReg = /(http:|https:)\/\/music.163.com\/song\/media\/outer\/url\?id=(\d+)/;
|
|
||||||
const musicUrlReg2 = /(http:|https:)\/\/y.music.163.com\/m\/song\?(.*)&id=(\d+)/;
|
|
||||||
const musicUrlReg3 = /(http:|https:)\/\/music.163.com\/m\/song\/(\d+)/;
|
|
||||||
const id =
|
|
||||||
musicUrlReg2.exec(msg.message[0].data.data)?.[3] ||
|
|
||||||
musicUrlReg.exec(msg.message[0].data.data)?.[2] ||
|
|
||||||
musicUrlReg3.exec(msg.message[0].data.data)?.[2] ||
|
|
||||||
/(?<!user)id=(\d+)/.exec(msg.message[0].data.data)?.[1] || "";
|
|
||||||
const title = msg.message[0].data.data.match(/"title":"([^"]+)"/)[1]
|
|
||||||
const desc = msg.message[0].data.data.match(/"desc":"([^"]+)"/)[1]
|
|
||||||
if (id === "") return
|
|
||||||
let path = this.getCurDownloadPath(e) + '/' + desc + '-' + title + '.' + FileSuffix
|
|
||||||
try {
|
|
||||||
// 上传群文件
|
|
||||||
await this.uploadGroupFile(e, path);
|
|
||||||
// 删除文件
|
|
||||||
await checkAndRemoveFile(path);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传云盘
|
|
||||||
async uploadCloud(e) {
|
|
||||||
let msg = await getReplyMsg(e)
|
|
||||||
const autoSelectNeteaseApi = await this.pickApi()
|
|
||||||
const musicUrlReg = /(http:|https:)\/\/music.163.com\/song\/media\/outer\/url\?id=(\d+)/;
|
|
||||||
const musicUrlReg2 = /(http:|https:)\/\/y.music.163.com\/m\/song\?(.*)&id=(\d+)/;
|
|
||||||
const musicUrlReg3 = /(http:|https:)\/\/music.163.com\/m\/song\/(\d+)/;
|
|
||||||
const id =
|
|
||||||
musicUrlReg2.exec(msg.message[0].data.data)?.[3] ||
|
|
||||||
musicUrlReg.exec(msg.message[0].data.data)?.[2] ||
|
|
||||||
musicUrlReg3.exec(msg.message[0].data.data)?.[2] ||
|
|
||||||
/(?<!user)id=(\d+)/.exec(msg.message[0].data.data)[1] || "";
|
|
||||||
const title = msg.message[0].data.data.match(/"title":"([^"]+)"/)[1]
|
|
||||||
const desc = msg.message[0].data.data.match(/"desc":"([^"]+)"/)[1]
|
|
||||||
if (id === "") return
|
|
||||||
let path = this.getCurDownloadPath(e) + '/' + desc + '-' + title + '.' + FileSuffix
|
|
||||||
const tryUpload = async () => {
|
|
||||||
let formData = new FormData();
|
|
||||||
formData.append('songFile', fs.createReadStream(path));
|
|
||||||
const headers = {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
'Cookie': this.neteaseCookie,
|
|
||||||
};
|
|
||||||
const updateUrl = `${autoSelectNeteaseApi}/cloud?time=${Date.now()}`;
|
|
||||||
try {
|
|
||||||
const res = await axios({
|
|
||||||
method: 'post',
|
|
||||||
url: updateUrl,
|
|
||||||
headers: headers,
|
|
||||||
data: formData,
|
|
||||||
});
|
|
||||||
if (res.data.code == 200) {
|
|
||||||
let matchUrl = `${autoSelectNeteaseApi}/cloud/match?uid=${this.uid}&sid=${res.data.privateCloud.songId}&asid=${id}`;
|
|
||||||
try {
|
|
||||||
const matchRes = await axios.get(matchUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
"Cookie": this.neteaseCookie
|
|
||||||
},
|
|
||||||
});
|
|
||||||
logger.info('歌曲信息匹配成功');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('歌曲信息匹配错误', error);
|
|
||||||
}
|
|
||||||
this.songCloudUpdate(e);
|
|
||||||
return res;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw new Error('上传失败,响应不正确');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await retryAxiosReq(() => tryUpload())
|
|
||||||
await checkAndRemoveFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取云盘歌单
|
|
||||||
async getCloudSong(e, cloudUpdate = false) {
|
|
||||||
let songList = await redisGetKey(REDIS_YUNZAI_CLOUDSONGLIST) || []
|
|
||||||
if (!songList[0] || cloudUpdate) {
|
|
||||||
const autoSelectNeteaseApi = await this.pickApi();
|
|
||||||
const limit = 100;
|
|
||||||
let offset = 0;
|
|
||||||
let cloudUrl = autoSelectNeteaseApi + `/user/cloud?limit=${limit}&offset=${offset}×tamp=${Date.now()}`;
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const res = await axios.get(cloudUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
"Cookie": this.neteaseCookie
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const songs = res.data.data.map(({ songId, songName, artist }) => ({
|
|
||||||
'songName': songName,
|
|
||||||
'id': songId,
|
|
||||||
'singerName': artist || '喵喵~',
|
|
||||||
'duration': '云盘'
|
|
||||||
}));
|
|
||||||
songList.push(...songs);
|
|
||||||
if (!res.data.hasMore) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
offset += limit;
|
|
||||||
cloudUrl = autoSelectNeteaseApi + `/user/cloud?limit=${limit}&offset=${offset}`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取歌单失败", error);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await redisSetKey(REDIS_YUNZAI_CLOUDSONGLIST, songList)
|
|
||||||
return songList;
|
|
||||||
} else {
|
|
||||||
return songList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 群文件上传云盘
|
|
||||||
async getLatestDocument(e) {
|
|
||||||
const autoSelectNeteaseApi = await this.pickApi();
|
|
||||||
let { cleanPath, file_id } = await getGroupFileUrl(e);
|
|
||||||
// Napcat 解决方案
|
|
||||||
if (cleanPath.startsWith("https")) {
|
|
||||||
const fileIdMatch = file_id.match(/\.(.*?)\.(\w+)$/);
|
|
||||||
const songName = fileIdMatch[1]; // 提取的歌曲名称
|
|
||||||
const fileFormat = fileIdMatch[2]; // 提取的文件格式
|
|
||||||
// 检测文件是否存在 已提升性能
|
|
||||||
if (await checkFileExists(cleanPath)) {
|
|
||||||
// 如果文件已存在
|
|
||||||
logger.mark(`[R插件][云盘] 上传路径审计:已存在下载文件`);
|
|
||||||
cleanPath = `${this.getCurDownloadPath(e)}/${songName}.${fileFormat}`;
|
|
||||||
} else {
|
|
||||||
// 如果文件不存在
|
|
||||||
logger.mark(`[R插件][云盘] 上传路径审计:不存在下载文件,将进行下载...`);
|
|
||||||
cleanPath = await downloadAudio(cleanPath, this.getCurDownloadPath(e), songName, "manual", fileFormat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info(`[R插件][云盘] 上传路径审计: ${cleanPath}`);
|
|
||||||
// 使用 splitPaths 提取信息
|
|
||||||
const [{ dir: dirPath, fileName, extension, baseFileName }] = splitPaths(cleanPath);
|
|
||||||
// 文件名拆解为两部分
|
|
||||||
const parts = baseFileName.trim().match(/^([\s\S]+)\s*-\s*([\s\S]+)$/);
|
|
||||||
// 命令不规范检测
|
|
||||||
if (parts == null || parts.length < 2) {
|
|
||||||
logger.warn("[R插件][云盘] 上传路径审计:命名不规范");
|
|
||||||
e.reply("请规范上传文件的命名:歌手-歌名,例如:梁静茹-勇气");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 生成新文件名
|
|
||||||
const newFileName = `${dirPath}/${parts[2].trim()}${extension}`;
|
|
||||||
// 进行元数据编辑
|
|
||||||
if (parts) {
|
|
||||||
const tags = {
|
|
||||||
title: parts[2].replace(/^\s+|\s+$/g, ''),
|
|
||||||
artist: parts[1].replace(/^\s+|\s+$/g, '')
|
|
||||||
};
|
|
||||||
// 写入元数据
|
|
||||||
let success = NodeID3.write(tags, cleanPath)
|
|
||||||
if (fs.existsSync(newFileName)) {
|
|
||||||
logger.info(`音频已存在`);
|
|
||||||
fs.unlinkSync(newFileName);
|
|
||||||
}
|
|
||||||
// 文件重命名
|
|
||||||
fs.renameSync(cleanPath, newFileName)
|
|
||||||
if (success) logger.info('写入元数据成功')
|
|
||||||
} else {
|
|
||||||
logger.info('未按照标准命名')
|
|
||||||
}
|
|
||||||
// 上传请求
|
|
||||||
const tryUpload = async () => {
|
|
||||||
let formData = new FormData()
|
|
||||||
await formData.append('songFile', fs.createReadStream(newFileName))
|
|
||||||
const headers = {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
'Cookie': this.neteaseCookie,
|
|
||||||
};
|
|
||||||
const updateUrl = autoSelectNeteaseApi + `/cloud?time=${Date.now()}`
|
|
||||||
try {
|
|
||||||
const res = await axios({
|
|
||||||
method: 'post',
|
|
||||||
url: updateUrl,
|
|
||||||
headers: headers,
|
|
||||||
data: formData,
|
|
||||||
});
|
|
||||||
this.songCloudUpdate(e);
|
|
||||||
return res;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 重试
|
|
||||||
await retryAxiosReq(() => tryUpload())
|
|
||||||
checkAndRemoveFile(newFileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 清除缓存
|
|
||||||
async cleanCloudData(e) {
|
|
||||||
await redisSetKey(REDIS_YUNZAI_CLOUDSONGLIST, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否海外服务器
|
|
||||||
async isOverseasServer() {
|
|
||||||
// 如果第一次使用没有值就设置
|
|
||||||
if (!(await redisExistKey(REDIS_YUNZAI_ISOVERSEA))) {
|
|
||||||
await redisSetKey(REDIS_YUNZAI_ISOVERSEA, {
|
|
||||||
os: false,
|
|
||||||
})
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 如果有就取出来
|
|
||||||
return (await redisGetKey(REDIS_YUNZAI_ISOVERSEA)).os;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API选择
|
|
||||||
async pickApi() {
|
|
||||||
const isOversea = await this.isOverseasServer();
|
|
||||||
let autoSelectNeteaseApi
|
|
||||||
if (this.useLocalNeteaseAPI) {
|
|
||||||
// 使用自建 API
|
|
||||||
return autoSelectNeteaseApi = this.neteaseCloudAPIServer
|
|
||||||
} else {
|
|
||||||
// 自动选择 API
|
|
||||||
return autoSelectNeteaseApi = isOversea ? NETEASE_SONG_DOWNLOAD : NETEASE_API_CN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测cooike活性
|
|
||||||
async checkCooike(statusUrl) {
|
|
||||||
let status
|
|
||||||
await axios.get(statusUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
"Cookie": this.neteaseCookie
|
|
||||||
},
|
|
||||||
}).then(async res => {
|
|
||||||
const userInfo = res.data.data.profile
|
|
||||||
await config.updateField("tools", "neteaseUserId", res.data.data.profile.userId);
|
|
||||||
if (userInfo) {
|
|
||||||
logger.info('[R插件][ncm-Cookie检测]ck活着,使用ck进行高音质下载')
|
|
||||||
status = true
|
|
||||||
} else {
|
|
||||||
logger.info('[R插件][ncm-Cookie检测]ck失效,将启用临时接口下载')
|
|
||||||
status = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
||||||
// 网易云音乐下载策略
|
|
||||||
neteasePlay(e, pickSongUrl, songWikiUrl, songInfo, pickNumber = 0, isCkExpired) {
|
|
||||||
axios.get(pickSongUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
"Cookie": this.neteaseCookie
|
|
||||||
},
|
|
||||||
}).then(async resp => {
|
|
||||||
// 国内解决方案,替换API后这里也需要修改
|
|
||||||
|
|
||||||
// 英转中字典匹配
|
|
||||||
const translationDict = {
|
|
||||||
'standard': '标准',
|
|
||||||
'higher': '较高',
|
|
||||||
'exhigh': '极高',
|
|
||||||
'lossless': '无损',
|
|
||||||
'hires': 'Hi-Res',
|
|
||||||
'jyeffect': '高清环绕声',
|
|
||||||
'sky': '沉浸环绕声',
|
|
||||||
'dolby': '杜比全景声',
|
|
||||||
'jymaster': '超清母带'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 英转中
|
|
||||||
function translateToChinese(word) {
|
|
||||||
return translationDict[word] || word; // 如果找不到对应翻译,返回原词
|
|
||||||
}
|
|
||||||
|
|
||||||
// 字节转MB
|
|
||||||
function bytesToMB(sizeInBytes) {
|
|
||||||
const sizeInMB = sizeInBytes / (1024 * 1024); // 1 MB = 1024 * 1024 bytes
|
|
||||||
return sizeInMB.toFixed(2); // 保留两位小数
|
|
||||||
}
|
|
||||||
let url = await resp.data.data?.[0]?.url || null;
|
|
||||||
const AudioLevel = translateToChinese(resp.data.data?.[0]?.level)
|
|
||||||
const AudioSize = bytesToMB(resp.data.data?.[0]?.size)
|
|
||||||
|
|
||||||
// 获取歌曲标题
|
|
||||||
let title = songInfo[pickNumber].singerName + '-' + songInfo[pickNumber].songName
|
|
||||||
let typelist = []
|
|
||||||
// 歌曲百科API
|
|
||||||
await axios.get(songWikiUrl, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
// "Cookie": this.neteaseCookie
|
|
||||||
},
|
|
||||||
}).then(res => {
|
|
||||||
const wikiData = res.data.data.blocks[1]?.creatives || []
|
|
||||||
if (wikiData[0]) {
|
|
||||||
typelist.push(wikiData[0].resources[0].uiElement.mainTitle.title)
|
|
||||||
// 防止数据过深出错
|
|
||||||
const recTags = wikiData[1]
|
|
||||||
if (recTags.resources[0]) {
|
|
||||||
for (let i = 0; i < Math.min(3, recTags.resources.length); i++) {
|
|
||||||
if (recTags.resources[i] && recTags.resources[i].uiElement && recTags.resources[i].uiElement.mainTitle.title) {
|
|
||||||
typelist.push(recTags.resources[i].uiElement.mainTitle.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (recTags.uiElement.textLinks[0].text) typelist.push(recTags.uiElement.textLinks[0].text)
|
|
||||||
}
|
|
||||||
if (wikiData[2].uiElement.mainTitle.title == 'BPM') {
|
|
||||||
typelist.push('BPM ' + wikiData[2].uiElement.textLinks[0].text)
|
|
||||||
} else {
|
|
||||||
typelist.push(wikiData[2].uiElement.textLinks[0].text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
typelist.push(AudioLevel)
|
|
||||||
})
|
|
||||||
let musicInfo = {
|
|
||||||
'cover': songInfo[pickNumber].cover,
|
|
||||||
'songName': songInfo[pickNumber].songName,
|
|
||||||
'singerName': songInfo[pickNumber].singerName,
|
|
||||||
'size': AudioSize + ' MB',
|
|
||||||
'musicType': typelist
|
|
||||||
}
|
|
||||||
// 一般这个情况是VIP歌曲 (如果没有url或者是国内,公用接口暂时不可用,必须自建并且ck可用状态才能进行高质量解析)
|
|
||||||
if (!isCkExpired || url == null) {
|
|
||||||
url = await this.musicTempApi(e, musicInfo, title);
|
|
||||||
} else {
|
|
||||||
// 拥有ck,并且有效,直接进行解析
|
|
||||||
let audioInfo = AudioLevel;
|
|
||||||
if (AudioLevel == '杜比全景声') {
|
|
||||||
audioInfo += '\n(杜比下载文件为MP4,编码格式为AC-4,需要设备支持才可播放)';
|
|
||||||
}
|
|
||||||
const data = await new NeteaseMusicInfo(e).getData(musicInfo)
|
|
||||||
let img = await puppeteer.screenshot("neteaseMusicInfo", data);
|
|
||||||
e.reply(img);
|
|
||||||
}
|
|
||||||
// 动态判断后缀名
|
|
||||||
let musicExt = resp.data.data?.[0]?.type
|
|
||||||
FileSuffix = musicExt
|
|
||||||
// 下载音乐
|
|
||||||
downloadAudio(url, this.getCurDownloadPath(e), title, 'follow', musicExt).then(async path => {
|
|
||||||
try {
|
|
||||||
// 发送卡片
|
|
||||||
await sendMusicCard(e, '163', songInfo[pickNumber].id)
|
|
||||||
} catch (error) {
|
|
||||||
if (error.message) {
|
|
||||||
logger.error("发送卡片错误错误:", error.message, '发送群语音');
|
|
||||||
} else {
|
|
||||||
logger.error("发送卡片错误错误,请查看控制台报错,将发送群语音")
|
|
||||||
logger.error(error)
|
|
||||||
}
|
|
||||||
// 发送群文件
|
|
||||||
await this.uploadGroupFile(e, path);
|
|
||||||
// 发送语音
|
|
||||||
if (musicExt != 'mp4' && this.isSendVocal) {
|
|
||||||
await e.reply(segment.record(path));
|
|
||||||
}
|
|
||||||
// 删除文件
|
|
||||||
await checkAndRemoveFile(path);
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
logger.error(`下载音乐失败,错误信息为: ${err}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async musicTempApi(e, musicInfo, title) {
|
|
||||||
let musicReqApi = NETEASE_TEMP_API;
|
|
||||||
// 临时接口,title经过变换后搜索到的音乐质量提升
|
|
||||||
const vipMusicData = await axios.get(musicReqApi.replace("{}", title.replace("-", " ")), {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": COMMON_USER_AGENT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const url = vipMusicData.data?.music_url
|
|
||||||
const id = vipMusicData.data?.id ?? vipMusicData.data?.data?.quality ?? vipMusicData.data?.pay;
|
|
||||||
musicInfo.size = id
|
|
||||||
musicInfo.musicType = musicInfo.musicType.slice(0, -1)
|
|
||||||
const data = await new NeteaseMusicInfo(e).getData(musicInfo)
|
|
||||||
let img = await puppeteer.screenshot("neteaseMusicInfo", data);
|
|
||||||
e.reply(img);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前发送人/群的下载路径
|
|
||||||
* @param e Yunzai 机器人事件
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getCurDownloadPath(e) {
|
|
||||||
return `${this.defaultPath}${e.group_id || e.user_id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传到群文件
|
|
||||||
* @param e 交互事件
|
|
||||||
* @param path 上传的文件所在路径
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
|
||||||
async uploadGroupFile(e, path) {
|
|
||||||
// 判断是否是ICQQ
|
|
||||||
if (e.bot?.sendUni) {
|
|
||||||
await e.group.fs.upload(path);
|
|
||||||
} else {
|
|
||||||
await e.group.sendFile(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,221 +0,0 @@
|
|||||||
import schedule from 'node-schedule';
|
|
||||||
import { REDIS_YUNZAI_ISOVERSEA, REDIS_YUNZAI_WHITELIST } from "../constants/constant.js";
|
|
||||||
import config from "../model/config.js";
|
|
||||||
import { deleteFolderRecursive, readCurrentDir } from "../utils/file.js";
|
|
||||||
import { redisExistAndGetKey, redisGetKey, redisSetKey } from "../utils/redis-util.js";
|
|
||||||
|
|
||||||
// 自动清理定时
|
|
||||||
const autotime = config.getConfig("tools").autoclearTrashtime;
|
|
||||||
// 视频保存路径
|
|
||||||
const defaultPath = config.getConfig("tools").defaultPath;
|
|
||||||
|
|
||||||
export class switchers extends plugin {
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
name: "R插件开关类",
|
|
||||||
dsc: "内含一些和Redis相关的开关类",
|
|
||||||
priority: 300,
|
|
||||||
rule: [
|
|
||||||
{
|
|
||||||
reg: "^#设置海外解析$",
|
|
||||||
fnc: "setOversea",
|
|
||||||
permission: "master",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^清理垃圾$",
|
|
||||||
fnc: "clearTrash",
|
|
||||||
permission: "master",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#设置R信任用户(.*)",
|
|
||||||
fnc: "setWhiteList",
|
|
||||||
permission: "master",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#R信任用户$",
|
|
||||||
fnc: "getWhiteList",
|
|
||||||
permission: "master",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#查询R信任用户(.*)",
|
|
||||||
fnc: "searchWhiteList",
|
|
||||||
permission: "master",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: "^#删除R信任用户(.*)",
|
|
||||||
fnc: "deleteWhiteList",
|
|
||||||
permission: "master",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置海外模式
|
|
||||||
* @param e
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async setOversea(e) {
|
|
||||||
try {
|
|
||||||
// 查看当前设置
|
|
||||||
let os = (await redisGetKey(REDIS_YUNZAI_ISOVERSEA))?.os;
|
|
||||||
// 如果是第一次
|
|
||||||
if (os === undefined) {
|
|
||||||
await redisSetKey(REDIS_YUNZAI_ISOVERSEA, { os: false });
|
|
||||||
os = false;
|
|
||||||
}
|
|
||||||
// 设置
|
|
||||||
os = ~os;
|
|
||||||
await redisSetKey(REDIS_YUNZAI_ISOVERSEA, { os });
|
|
||||||
e.reply(`当前服务器:${ os ? '海外服务器' : '国内服务器' }`);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
e.reply(`设置海外模式时发生错误: ${ err.message }`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手动清理垃圾
|
|
||||||
* @param e
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async clearTrash(e) {
|
|
||||||
try {
|
|
||||||
const { dataClearFileLen, rTempFileLen } = await autoclearTrash();
|
|
||||||
e.reply(`手动清理垃圾完成:\n` +
|
|
||||||
`- 清理了${ dataClearFileLen }个垃圾文件\n` +
|
|
||||||
`- 清理了${ rTempFileLen }个群临时文件`);
|
|
||||||
} catch (err) {
|
|
||||||
e.reply(`手动清理垃圾时发生错误: ${ err.message }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置解析信任用户
|
|
||||||
* @param e
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async setWhiteList(e) {
|
|
||||||
try {
|
|
||||||
let trustUserId = e?.reply_id !== undefined ? (await e.getReply()).user_id : e.msg.replace("#设置R信任用户", "").trim();
|
|
||||||
trustUserId = trustUserId.toString();
|
|
||||||
// 用户ID检测
|
|
||||||
if (!trustUserId) {
|
|
||||||
e.reply("无效的R信任用户");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let whiteList = await redisExistAndGetKey(REDIS_YUNZAI_WHITELIST) || [];
|
|
||||||
// 重复检测
|
|
||||||
if (whiteList.includes(trustUserId)) {
|
|
||||||
e.reply("R信任用户已存在,无须添加!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
whiteList.push(trustUserId);
|
|
||||||
// 放置到Redis里
|
|
||||||
await redisSetKey(REDIS_YUNZAI_WHITELIST, whiteList);
|
|
||||||
e.reply(`成功添加R信任用户:${ trustUserId }`);
|
|
||||||
} catch (err) {
|
|
||||||
e.reply(`设置R信任用户时发生错误: ${ err.message }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取信任用户名单
|
|
||||||
* @param e
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async getWhiteList(e) {
|
|
||||||
try {
|
|
||||||
let whiteList = await redisExistAndGetKey(REDIS_YUNZAI_WHITELIST) || [];
|
|
||||||
const message = `R信任用户列表:\n${ whiteList.join(",\n") }`;
|
|
||||||
if (this.e.isGroup) {
|
|
||||||
await Bot.pickUser(this.e.user_id).sendMsg(await this.e.runtime.common.makeForwardMsg(this.e, message));
|
|
||||||
await this.reply('R插件的信任用户名单已发送至您的私信了~');
|
|
||||||
} else {
|
|
||||||
await e.reply(await makeForwardMsg(this.e, message));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
e.reply(`获取R信任用户时发生错误: ${ err.message }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询某个用户是否是信任用户
|
|
||||||
* @param e
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async searchWhiteList(e) {
|
|
||||||
try {
|
|
||||||
let trustUserId = e?.reply_id !== undefined ? (await e.getReply()).user_id : e.msg.replace("#查询R信任用户", "").trim();
|
|
||||||
let whiteList = await redisExistAndGetKey(REDIS_YUNZAI_WHITELIST) || [];
|
|
||||||
const isInWhiteList = whiteList.includes(trustUserId);
|
|
||||||
e.reply(isInWhiteList ? `✅ ${ trustUserId }已经是R插件的信任用户哦~` : `⚠️ ${ trustUserId }不是R插件的信任用户哦~`);
|
|
||||||
} catch (err) {
|
|
||||||
e.reply(`查询R信任用户时发生错误: ${ err.message }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除信任用户
|
|
||||||
* @param e
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async deleteWhiteList(e) {
|
|
||||||
try {
|
|
||||||
let trustUserId = e?.reply_id !== undefined ? (await e.getReply()).user_id : e.msg.replace("#删除R信任用户", "").trim();
|
|
||||||
// 校准不是string的用户
|
|
||||||
let whiteList = (await redisExistAndGetKey(REDIS_YUNZAI_WHITELIST))?.map(item => item.toString()) || [];
|
|
||||||
// 重复检测
|
|
||||||
if (!whiteList.includes(trustUserId)) {
|
|
||||||
e.reply("R信任用户不存在,无须删除!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
whiteList = whiteList.filter(item => item !== trustUserId);
|
|
||||||
// 放置到Redis里
|
|
||||||
await redisSetKey(REDIS_YUNZAI_WHITELIST, whiteList);
|
|
||||||
e.reply(`成功删除R信任用户:${ trustUserId }`);
|
|
||||||
} catch (err) {
|
|
||||||
e.reply(`删除R信任用户时发生错误: ${ err.message }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理垃圾文件
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
|
||||||
async function autoclearTrash() {
|
|
||||||
const dataDirectory = "./data/";
|
|
||||||
try {
|
|
||||||
const files = await readCurrentDir(dataDirectory);
|
|
||||||
let dataClearFileLen = 0;
|
|
||||||
for (const file of files) {
|
|
||||||
if (/^[0-9a-f]{32}$/.test(file)) {
|
|
||||||
await fs.promises.unlink(dataDirectory + file);
|
|
||||||
dataClearFileLen++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rTempFileLen = await deleteFolderRecursive(defaultPath);
|
|
||||||
return { dataClearFileLen, rTempFileLen };
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoclear(time) {
|
|
||||||
schedule.scheduleJob(time, async function () {
|
|
||||||
try {
|
|
||||||
const { dataClearFileLen, rTempFileLen } = await autoclearTrash();
|
|
||||||
logger.info(`自动清理垃圾完成:\n` +
|
|
||||||
`- 清理了${ dataClearFileLen }个垃圾文件\n` +
|
|
||||||
`- 清理了${ rTempFileLen }个群临时文件`);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`自动清理垃圾时发生错误: ${ err.message }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动清理垃圾
|
|
||||||
autoclear(autotime);
|
|
220
apps/update.js
220
apps/update.js
@ -1,220 +0,0 @@
|
|||||||
// 主库
|
|
||||||
import Version from "../model/version.js";
|
|
||||||
import config from "../model/config.js";
|
|
||||||
import puppeteer from "../../../lib/puppeteer/puppeteer.js";
|
|
||||||
import lodash from "lodash";
|
|
||||||
import YAML from "yaml";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
import { exec, execSync } from "node:child_process";
|
|
||||||
import { copyFiles, deleteFolderRecursive, readCurrentDir } from "../utils/file.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理插件更新1
|
|
||||||
*/
|
|
||||||
export class Update extends plugin {
|
|
||||||
static pluginName = (() => {
|
|
||||||
const packageJsonPath = path.join('./plugins', 'rconsole-plugin', 'package.json');
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
||||||
return packageJson.name;
|
|
||||||
})();
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
name: "R插件更新插件",
|
|
||||||
dsc: "R插件更新插件代码",
|
|
||||||
event: "message",
|
|
||||||
priority: 4000,
|
|
||||||
rule: [
|
|
||||||
{
|
|
||||||
reg: "^#*R(插件)?版本$",
|
|
||||||
fnc: "version",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
/** 命令正则匹配 */
|
|
||||||
reg: "^#*R(插件)?(强制更新|更新)$",
|
|
||||||
/** 执行方法 */
|
|
||||||
fnc: "rconsoleUpdate",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.versionData = config.getConfig("version");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* rule - 插件版本信息
|
|
||||||
*/
|
|
||||||
async version() {
|
|
||||||
const data = await new Version(this.e).getData(this.versionData.slice(0, 3));
|
|
||||||
let img = await puppeteer.screenshot("version", data);
|
|
||||||
this.e.reply(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新主程序
|
|
||||||
* @param e
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async rconsoleUpdate(e) {
|
|
||||||
if (!e.isMaster) {
|
|
||||||
await e.reply("您无权操作");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isForce = !!e.msg.includes("强制");
|
|
||||||
|
|
||||||
// 保存配置文件
|
|
||||||
await copyFiles(`./plugins/${Update.pluginName}/config`, "./temp/rconsole-update-tmp", ['tools.yaml']);
|
|
||||||
|
|
||||||
let command = `git -C ./plugins/${Update.pluginName}/ pull --no-rebase`;
|
|
||||||
if (isForce) {
|
|
||||||
command = `git -C ./plugins/${Update.pluginName}/ checkout . && ${command}`;
|
|
||||||
}
|
|
||||||
this.oldCommitId = await this.getCommitId(Update.pluginName);
|
|
||||||
await e.reply("正在执行更新操作,请稍等");
|
|
||||||
|
|
||||||
let ret = await this.execSync(command);
|
|
||||||
if (ret.error) {
|
|
||||||
e.reply(`更新失败!重试一下!`);
|
|
||||||
await this.gitErr(ret.error, ret.stdout);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const time = await this.getTime(Update.pluginName);
|
|
||||||
if (/Already up|已经是最新/g.test(ret.stdout)) {
|
|
||||||
e.reply(`R插件已经是最新: ${this.versionData[0].version}`);
|
|
||||||
} else {
|
|
||||||
this.isUp = true;
|
|
||||||
e.reply(`R插件更新成功,最后更新时间:${time}`);
|
|
||||||
e.reply(await this.getLog(Update.pluginName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取配置文件比对更新
|
|
||||||
const confFiles = await readCurrentDir("./temp/rconsole-update-tmp");
|
|
||||||
for (let confFile of confFiles) {
|
|
||||||
await this.compareAndUpdateYaml(
|
|
||||||
`./temp/rconsole-update-tmp/${confFile}`,
|
|
||||||
`./plugins/${Update.pluginName}/config/${confFile}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 删除临时文件
|
|
||||||
await deleteFolderRecursive("./temp/rconsole-update-tmp");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCommitId(pluginName) {
|
|
||||||
// let cm = 'git rev-parse --short HEAD'
|
|
||||||
const command = `git -C ./plugins/${pluginName}/ rev-parse --short HEAD`;
|
|
||||||
let commitId = execSync(command, { encoding: "utf-8" });
|
|
||||||
commitId = lodash.trim(commitId);
|
|
||||||
return commitId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async execSync(cmd) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
exec(cmd, { windowsHide: true }, (error, stdout, stderr) => {
|
|
||||||
resolve({ error, stdout, stderr });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTime(pluginName) {
|
|
||||||
const cm = `cd ./plugins/${pluginName}/ && git log -1 --oneline --pretty=format:"%cd" --date=format:"%m-%d %H:%M"`;
|
|
||||||
let time = "";
|
|
||||||
try {
|
|
||||||
time = execSync(cm, { encoding: "utf-8" });
|
|
||||||
time = lodash.trim(time);
|
|
||||||
} catch (error) {
|
|
||||||
time = "获取时间失败";
|
|
||||||
}
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLog(pluginName) {
|
|
||||||
let cm =
|
|
||||||
'git log -20 --oneline --pretty=format:"%h||[%cd] %s" --date=format:"%m-%d %H:%M"';
|
|
||||||
if (pluginName) {
|
|
||||||
cm = `cd ./plugins/${pluginName}/ && ${cm}`;
|
|
||||||
}
|
|
||||||
let logAll;
|
|
||||||
try {
|
|
||||||
logAll = execSync(cm, { encoding: "utf-8" });
|
|
||||||
} catch (error) {
|
|
||||||
this.reply(error.toString(), true);
|
|
||||||
}
|
|
||||||
if (!logAll) return false;
|
|
||||||
logAll = logAll.split("\n");
|
|
||||||
let log = [];
|
|
||||||
for (let str of logAll) {
|
|
||||||
str = str.split("||");
|
|
||||||
if (str[0] === this.oldCommitId) break;
|
|
||||||
if (str[1].includes("Merge branch")) continue;
|
|
||||||
log.push(str[1]);
|
|
||||||
}
|
|
||||||
let line = log.length;
|
|
||||||
log = log.join("\n");
|
|
||||||
if (log.length <= 0) return "";
|
|
||||||
logger.info(`${pluginName || "Yunzai-Bot"}更新日志,共${line}条\n${log}`);
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
async gitErr(err, stdout) {
|
|
||||||
let msg = "更新失败!";
|
|
||||||
let errMsg = err.toString();
|
|
||||||
stdout = stdout.toString();
|
|
||||||
if (errMsg.includes("Timed out")) {
|
|
||||||
await this.reply(msg + `\n连接超时:${errMsg.match(/'(.+?)'/g)[0].replace(/'/g, "")}`);
|
|
||||||
} else if (/Failed to connect|unable to access/g.test(errMsg)) {
|
|
||||||
await this.reply(msg + `\n连接失败:${errMsg.match(/'(.+?)'/g)[0].replace(/'/g, "")}`);
|
|
||||||
} else if (errMsg.includes("be overwritten by merge")) {
|
|
||||||
await this.reply(
|
|
||||||
msg +
|
|
||||||
`存在冲突:\n${errMsg}\n` +
|
|
||||||
"请解决冲突后再更新,或者执行#R强制更新,放弃本地修改",
|
|
||||||
);
|
|
||||||
} else if (stdout.includes("CONFLICT")) {
|
|
||||||
await this.reply([
|
|
||||||
msg + "存在冲突\n",
|
|
||||||
errMsg,
|
|
||||||
stdout,
|
|
||||||
"\n请解决冲突后再更新,或者执行#R强制更新,放弃本地修改",
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
await this.reply([errMsg, stdout]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async compareAndUpdateYaml(sourcePath, updatedPath) {
|
|
||||||
try {
|
|
||||||
// Step 1 & 2: Read and parse YAML files
|
|
||||||
const sourceContent = await fs.readFileSync(sourcePath, 'utf8');
|
|
||||||
const updatedContent = await fs.readFileSync(updatedPath, 'utf8');
|
|
||||||
const sourceObj = YAML.parse(sourceContent);
|
|
||||||
const updatedObj = YAML.parse(updatedContent);
|
|
||||||
|
|
||||||
// Step 3: Compare objects and merge changes
|
|
||||||
Object.keys(updatedObj).forEach(key => {
|
|
||||||
if (!sourceObj.hasOwnProperty(key)) {
|
|
||||||
sourceObj[key] = updatedObj[key]; // Add new keys with updated values
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(sourceObj).forEach(key => {
|
|
||||||
if (!updatedObj.hasOwnProperty(key)) {
|
|
||||||
delete sourceObj[key]; // Remove keys not present in updated object
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 4 & 5: Convert object back to YAML
|
|
||||||
const newYamlContent = YAML.stringify(sourceObj);
|
|
||||||
|
|
||||||
// Step 6: Write the updated YAML back to the updatedPath
|
|
||||||
await fs.writeFileSync(updatedPath, newYamlContent, 'utf8');
|
|
||||||
|
|
||||||
logger.info(`[R插件更新配置文件记录]${updatedPath}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
2
index.js
2
index.js
@ -29,7 +29,7 @@ for (let i in files) {
|
|||||||
let name = files[i].replace(".js", "");
|
let name = files[i].replace(".js", "");
|
||||||
|
|
||||||
if (ret[i].status !== "fulfilled") {
|
if (ret[i].status !== "fulfilled") {
|
||||||
logger.error(`载入插件错误:${logger.red(name)}`);
|
logger.error(`[rc-plugin] 载入插件错误:${logger.red(name)}`);
|
||||||
logger.error(ret[i].reason);
|
logger.error(ret[i].reason);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user