This commit is contained in:
Jerry 2025-09-10 18:33:32 +08:00
parent 49de188740
commit 42e02fc381
13 changed files with 1 additions and 2172 deletions

View File

@ -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文件管理便于版本控制和团队协作。

View File

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

View File

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: R插件文档
url: https://gitee.com/kyrzy0416/rconsole-plugin
about: 专门为朋友们写的Yunzai-Bot插件专注图片视频分享、生活、健康和学习的插件

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, "");
}
}

View File

@ -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}&timestamp=${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);
}
}
}

View File

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

View File

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

View File

@ -29,7 +29,7 @@ for (let i in files) {
let name = files[i].replace(".js", "");
if (ret[i].status !== "fulfilled") {
logger.error(`载入插件错误:${logger.red(name)}`);
logger.error(`[rc-plugin] 载入插件错误:${logger.red(name)}`);
logger.error(ret[i].reason);
continue;
}