diff --git a/.github/GITHUB-BOT-SETUP.md b/.github/GITHUB-BOT-SETUP.md deleted file mode 100644 index 3eb0d70..0000000 --- a/.github/GITHUB-BOT-SETUP.md +++ /dev/null @@ -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文件管理,便于版本控制和团队协作。 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml deleted file mode 100644 index 438af2e..0000000 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 40129ec..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: R插件文档 - url: https://gitee.com/kyrzy0416/rconsole-plugin - about: 专门为朋友们写的Yunzai-Bot插件,专注图片视频分享、生活、健康和学习的插件! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml deleted file mode 100644 index 07e7960..0000000 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml deleted file mode 100644 index ea62475..0000000 --- a/.github/workflows/auto-label.yml +++ /dev/null @@ -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'); - } - } \ No newline at end of file diff --git a/.github/workflows/auto-reply-issues.yml b/.github/workflows/auto-reply-issues.yml deleted file mode 100644 index 7fa0469..0000000 --- a/.github/workflows/auto-reply-issues.yml +++ /dev/null @@ -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 - }); \ No newline at end of file diff --git a/.github/workflows/auto-reply-prs.yml b/.github/workflows/auto-reply-prs.yml deleted file mode 100644 index 44bbc49..0000000 --- a/.github/workflows/auto-reply-prs.yml +++ /dev/null @@ -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 - }); - } \ No newline at end of file diff --git a/.github/workflows/status-update.yml b/.github/workflows/status-update.yml deleted file mode 100644 index 4d073d0..0000000 --- a/.github/workflows/status-update.yml +++ /dev/null @@ -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); \ No newline at end of file diff --git a/apps/query.js b/apps/query.js deleted file mode 100644 index 8041ba5..0000000 --- a/apps/query.js +++ /dev/null @@ -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, ""); - } -} diff --git a/apps/songRequest.js b/apps/songRequest.js deleted file mode 100644 index ca12e89..0000000 --- a/apps/songRequest.js +++ /dev/null @@ -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] || - /(? { - 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} - */ - async uploadGroupFile(e, path) { - // 判断是否是ICQQ - if (e.bot?.sendUni) { - await e.group.fs.upload(path); - } else { - await e.group.sendFile(path); - } - } -} diff --git a/apps/switchers.js b/apps/switchers.js deleted file mode 100644 index 50a6519..0000000 --- a/apps/switchers.js +++ /dev/null @@ -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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ -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); diff --git a/apps/update.js b/apps/update.js deleted file mode 100644 index a701c6a..0000000 --- a/apps/update.js +++ /dev/null @@ -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} - */ - 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); - } - } -} diff --git a/index.js b/index.js index 8a5f3e9..bbae75e 100644 --- a/index.js +++ b/index.js @@ -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; }