mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-13 23:59: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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user