mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-11 23:40:24 +00:00
Compare commits
2 Commits
feature/re
...
pnpm-new
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c8e825eba | ||
|
|
c9c6942564 |
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug 反馈
|
||||
description: 报告可能的 NapCat 异常行为
|
||||
title: "[BUG] "
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -10,10 +10,6 @@ body:
|
||||
在提交新的 Bug 反馈前,请确保您:
|
||||
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
||||
* 不与现有的某一 issue 重复
|
||||
* **不接受因发送不当内容而导致的问题报告**
|
||||
- 包括但不限于:多媒体发送失败、转发消息失败、消息被拦截等因 18+ 内容、违规内容或触发风控的问题
|
||||
- 提交 issue 前,请确认您发送的多媒体内容、链接、文本等均为正常合规内容,不会触发平台风控机制
|
||||
- 因违规内容导致的问题,一律不予受理
|
||||
- type: input
|
||||
id: system-version
|
||||
attributes:
|
||||
@@ -34,7 +30,7 @@ body:
|
||||
id: napcat-version
|
||||
attributes:
|
||||
label: NapCat 版本
|
||||
description: 可在 WebUI 的「系统信息」页中找到
|
||||
description: 可在 LiteLoaderQQNT 的设置页或是 QQNT 的设置页侧栏中找到
|
||||
placeholder: 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
60
.github/ISSUE_TEMPLATE/feat_request.yml
vendored
60
.github/ISSUE_TEMPLATE/feat_request.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: Feat 请求
|
||||
description: 提交新的 NapCat 功能或改进建议
|
||||
title: '[FEAT] '
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
欢迎来到 NapCat 的 Issue Tracker!请填写以下表格来提交功能请求。
|
||||
在提交新的功能请求前,请确保您:
|
||||
* 已经搜索了现有的 issues,并且没有找到类似的建议
|
||||
* 不与现有的某一 issue 重复
|
||||
- type: input
|
||||
id: system-version
|
||||
attributes:
|
||||
label: 系统版本
|
||||
description: 运行 QQNT 的系统版本
|
||||
placeholder: Windows 11 24H2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: qqnt-version
|
||||
attributes:
|
||||
label: QQNT 版本
|
||||
description: 可在 QQNT 的「关于」的设置页中找到
|
||||
placeholder: 9.9.16-29927
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: napcat-version
|
||||
attributes:
|
||||
label: NapCat 版本
|
||||
description: 可在 WebUI 的「系统信息」页中找到
|
||||
placeholder: 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: 功能描述
|
||||
description: 请详细描述你希望添加的功能或改进
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-reason
|
||||
attributes:
|
||||
label: 需求背景与理由
|
||||
description: 请说明为什么需要这个功能,解决了什么问题
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-expected
|
||||
attributes:
|
||||
label: 期望的实现方式或效果
|
||||
description: 请描述你期望的功能实现方式或最终效果
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: 其他补充信息
|
||||
description: 你还想补充什么?
|
||||
43
.github/prompt/default.md
vendored
43
.github/prompt/default.md
vendored
@@ -1,43 +0,0 @@
|
||||
# {VERSION}
|
||||
[使用文档](https://napneko.github.io/)
|
||||
|
||||
## Windows 一键包
|
||||
我们为提供了的轻量化一键部署方案
|
||||
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
||||
|
||||
你可以下载
|
||||
|
||||
NapCat.Shell.Windows.OneKey.zip (无头)
|
||||
|
||||
启动后可自动化部署一键包,教程参考使用文档安装部分
|
||||
|
||||
## 警告
|
||||
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
|
||||
**默认WebUi密钥为随机密码 控制台查看**
|
||||
|
||||
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
|
||||
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
|
||||
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
|
||||
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
|
||||
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
|
||||
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
|
||||
## 如果WinX64缺少运行库或者xxx.dll?
|
||||
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
||||
|
||||
## 更新
|
||||
|
||||
### 🐛 修复
|
||||
1. 修复 WebUI 主题配置在有未保存更改时卸载组件导致字体重置的问题 (ae42eed6)
|
||||
|
||||
### ✨ 新增
|
||||
1. 文件上传相关接口(UploadGroupFile/UploadPrivateFile)新增 `upload_file` 参数支持 (91e0839e)
|
||||
2. 消息发送逻辑支持 PTT(语音)元素过滤,确保语音消息正确独立发送 (47983e29)
|
||||
|
||||
### 🔧 优化
|
||||
1. 优化合并转发消息(GetForwardMsg)的获取与解析逻辑,提高兼容性 (334c4233)
|
||||
2. 改进消息发送方法中发送者 UIN 的处理逻辑 (71bb4f68)
|
||||
3. 增强 WebUI 系统信息界面中对构建产物的处理与展示 (cb061890)
|
||||
|
||||
---
|
||||
|
||||
**完整更新日志**: [v4.10.6...v4.10.7](https://github.com/NapNeko/NapCatQQ/compare/v4.10.6...v4.10.7)
|
||||
111
.github/prompt/release_note_prompt.txt
vendored
111
.github/prompt/release_note_prompt.txt
vendored
@@ -1,111 +0,0 @@
|
||||
# NapCat Release Note Generator
|
||||
|
||||
你是 NapCat 项目的发布说明生成器。请根据提供的 commit 列表生成标准格式的发布说明。
|
||||
|
||||
## 核心规则
|
||||
|
||||
1. **版本号**:第一行必须是 `# {VERSION}`,使用用户提供的版本号,如果版本号是小写 v 开头(如 v4.10.2),必须转换为大写 V(如 V4.10.2)
|
||||
2. **语言**:全部使用简体中文
|
||||
3. **格式**:严格按照下方模板输出,不要添加额外的 markdown 格式
|
||||
|
||||
## Commit 分析规则
|
||||
|
||||
将 commit 分类为以下类型:
|
||||
- 🐛 **修复**:bug fix、修复、fix 相关
|
||||
- ✨ **新增**:新功能、feat、add 相关
|
||||
- 🔧 **优化**:优化、重构、refactor、improve、perf 相关
|
||||
- 📦 **依赖**:deps、依赖更新(通常可以忽略或合并)
|
||||
- 🔨 **构建**:ci、build、workflow 相关(通常可以忽略)
|
||||
|
||||
## 合并和筛选
|
||||
|
||||
- **合并相似项**:同一功能的多个 commit 合并为一条
|
||||
- **忽略琐碎项**:合并冲突、格式化、typo 等可忽略
|
||||
- **控制数量**:最终保持 5-15 条更新要点
|
||||
- **保留 commit hash**:每条末尾附上短 hash,格式 `(a1b2c3d)`
|
||||
|
||||
## 输出模板 - 必须严格遵守以下格式
|
||||
|
||||
```
|
||||
# {VERSION}
|
||||
[使用文档](https://napneko.github.io/)
|
||||
|
||||
## Windows 一键包
|
||||
我们为提供了的轻量化一键部署方案
|
||||
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
||||
|
||||
你可以下载
|
||||
|
||||
NapCat.Shell.Windows.OneKey.zip (无头)
|
||||
|
||||
启动后可自动化部署一键包,教程参考使用文档安装部分
|
||||
|
||||
## 警告
|
||||
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
|
||||
**默认WebUi密钥为随机密码 控制台查看**
|
||||
|
||||
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
|
||||
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
|
||||
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
|
||||
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
|
||||
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
|
||||
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
|
||||
## 如果WinX64缺少运行库或者xxx.dll?
|
||||
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
||||
|
||||
## 更新
|
||||
|
||||
### 🐛 修复
|
||||
1. 修复 xxx 问题 (a1b2c3d)
|
||||
2. 修复 yyy 崩溃 (b2c3d4e)
|
||||
|
||||
### ✨ 新增
|
||||
1. 新增 xxx 功能 (c3d4e5f)
|
||||
2. 支持 yyy 特性 (d4e5f6g)
|
||||
|
||||
### 🔧 优化
|
||||
1. 优化 xxx 性能 (e5f6g7h)
|
||||
2. 重构 yyy 模块 (f6g7h8i)
|
||||
|
||||
---
|
||||
|
||||
**完整更新日志**: [{PREV_VERSION}...{VERSION}](https://github.com/NapNeko/NapCatQQ/compare/{PREV_VERSION}...{VERSION})
|
||||
```
|
||||
|
||||
**格式要求 - 务必严格遵守:**
|
||||
- "Windows 一键包"部分的文本必须完全一致,不要修改任何措辞
|
||||
- "警告"部分必须包含所有 QQ 版本下载链接,保持原有格式
|
||||
- "如果WinX64缺少运行库或者xxx.dll?"这一行必须保持原样
|
||||
- QQ 版本号和下载链接保持不变(40990 版本)
|
||||
- 只有"## 更新"部分下面的内容需要根据实际 commit 生成
|
||||
|
||||
## 重要约束
|
||||
|
||||
1. 如果某个分类没有内容,则完全省略该分类
|
||||
2. 不要编造不存在的更新
|
||||
3. 保持简洁,每条更新控制在一行内
|
||||
4. 使用用户友好的语言,避免过于技术化的描述
|
||||
5. 重大变更(Breaking Changes)需要在注意事项中加粗提示
|
||||
|
||||
## 文件变化分析
|
||||
|
||||
用户会提供文件变化统计和具体代码diff,帮助你理解变更内容:
|
||||
|
||||
### 目录含义
|
||||
- `packages/napcat-core/` → 核心功能、消息处理、QQ接口
|
||||
- `packages/napcat-onebot/` → OneBot 协议实现、API、事件
|
||||
- `packages/napcat-webui-backend/` → WebUI 后端接口
|
||||
- `packages/napcat-webui-frontend/` → WebUI 前端界面
|
||||
- `packages/napcat-shell/` → Shell 启动器
|
||||
|
||||
### 代码diff阅读指南
|
||||
- `+` 开头的行是新增代码
|
||||
- `-` 开头的行是删除代码
|
||||
- 关注函数名、类名的变化来理解功能变更
|
||||
- 关注 `fix`、`bug`、`error` 等关键词识别修复项
|
||||
- 关注 `add`、`new`、`feature` 等关键词识别新功能
|
||||
- 忽略纯重构(代码移动但功能不变)和格式化变更
|
||||
|
||||
### 截断说明
|
||||
- 如果看到 `[... 已截断 ...]`,表示内容过长被截断
|
||||
- 根据已有信息推断完整变更意图即可
|
||||
231
.github/scripts/lib/comment.ts
vendored
231
.github/scripts/lib/comment.ts
vendored
@@ -1,231 +0,0 @@
|
||||
/**
|
||||
* 构建状态评论模板
|
||||
*/
|
||||
|
||||
export const COMMENT_MARKER = '<!-- napcat-pr-build -->';
|
||||
|
||||
export type BuildStatus = 'success' | 'failure' | 'cancelled' | 'pending' | 'unknown';
|
||||
|
||||
export interface BuildTarget {
|
||||
name: string;
|
||||
status: BuildStatus;
|
||||
error?: string;
|
||||
downloadUrl?: string; // Artifact 直接下载链接
|
||||
}
|
||||
|
||||
// ============== 辅助函数 ==============
|
||||
|
||||
function formatSha (sha: string): string {
|
||||
return sha && sha.length >= 7 ? sha.substring(0, 7) : sha || 'unknown';
|
||||
}
|
||||
|
||||
function escapeCodeBlock (text: string): string {
|
||||
// 替换 ``` 为转义形式,避免破坏 Markdown 代码块
|
||||
return text.replace(/```/g, '\\`\\`\\`');
|
||||
}
|
||||
|
||||
function getTimeString (): string {
|
||||
return new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||
}
|
||||
|
||||
// ============== 状态图标 ==============
|
||||
|
||||
export function getStatusIcon (status: BuildStatus): string {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return '✅ 成功';
|
||||
case 'pending':
|
||||
return '⏳ 构建中...';
|
||||
case 'cancelled':
|
||||
return '⚪ 已取消';
|
||||
case 'failure':
|
||||
return '❌ 失败';
|
||||
default:
|
||||
return '❓ 未知';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusEmoji (status: BuildStatus): string {
|
||||
switch (status) {
|
||||
case 'success': return '✅';
|
||||
case 'pending': return '⏳';
|
||||
case 'cancelled': return '⚪';
|
||||
case 'failure': return '❌';
|
||||
default: return '❓';
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 构建中评论 ==============
|
||||
|
||||
export function generateBuildingComment (prSha: string, targets: string[]): string {
|
||||
const time = getTimeString();
|
||||
const shortSha = formatSha(prSha);
|
||||
|
||||
const lines: string[] = [
|
||||
COMMENT_MARKER,
|
||||
'',
|
||||
'<div align="center">',
|
||||
'',
|
||||
'# 🔨 NapCat 构建中',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'</div>',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📦 构建目标',
|
||||
'',
|
||||
'| 包名 | 状态 | 说明 |',
|
||||
'| :--- | :---: | :--- |',
|
||||
...targets.map(name => `| \`${name}\` | ⏳ | 正在构建... |`),
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📋 构建信息',
|
||||
'',
|
||||
`| 项目 | 值 |`,
|
||||
`| :--- | :--- |`,
|
||||
`| 📝 提交 | \`${shortSha}\` |`,
|
||||
`| 🕐 开始时间 | ${time} |`,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> ⏳ **构建进行中,请稍候...**',
|
||||
'>',
|
||||
'> 构建完成后将自动更新此评论',
|
||||
'',
|
||||
'</div>',
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ============== 构建结果评论 ==============
|
||||
|
||||
export function generateResultComment (
|
||||
targets: BuildTarget[],
|
||||
prSha: string,
|
||||
runId: string,
|
||||
repository: string,
|
||||
version?: string
|
||||
): string {
|
||||
const runUrl = `https://github.com/${repository}/actions/runs/${runId}`;
|
||||
const shortSha = formatSha(prSha);
|
||||
const time = getTimeString();
|
||||
|
||||
const allSuccess = targets.every(t => t.status === 'success');
|
||||
const anyCancelled = targets.some(t => t.status === 'cancelled');
|
||||
const anyFailure = targets.some(t => t.status === 'failure');
|
||||
|
||||
// 状态徽章
|
||||
let statusBadge: string;
|
||||
let headerTitle: string;
|
||||
if (allSuccess) {
|
||||
statusBadge = '';
|
||||
headerTitle = '# ✅ NapCat 构建成功';
|
||||
} else if (anyCancelled && !anyFailure) {
|
||||
statusBadge = '';
|
||||
headerTitle = '# ⚪ NapCat 构建已取消';
|
||||
} else {
|
||||
statusBadge = '';
|
||||
headerTitle = '# ❌ NapCat 构建失败';
|
||||
}
|
||||
|
||||
const downloadLink = (target: BuildTarget) => {
|
||||
if (target.status !== 'success') return '—';
|
||||
if (target.downloadUrl) {
|
||||
return `[📥 下载](${target.downloadUrl})`;
|
||||
}
|
||||
return `[📥 下载](${runUrl}#artifacts)`;
|
||||
};
|
||||
|
||||
const lines: string[] = [
|
||||
COMMENT_MARKER,
|
||||
'',
|
||||
'<div align="center">',
|
||||
'',
|
||||
headerTitle,
|
||||
'',
|
||||
statusBadge,
|
||||
'',
|
||||
'</div>',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📦 构建产物',
|
||||
'',
|
||||
'| 包名 | 状态 | 下载 |',
|
||||
'| :--- | :---: | :---: |',
|
||||
...targets.map(t => `| \`${t.name}\` | ${getStatusEmoji(t.status)} ${t.status === 'success' ? '成功' : t.status === 'failure' ? '失败' : t.status === 'cancelled' ? '已取消' : '未知'} | ${downloadLink(t)} |`),
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📋 构建信息',
|
||||
'',
|
||||
`| 项目 | 值 |`,
|
||||
`| :--- | :--- |`,
|
||||
...(version ? [`| 🏷️ 版本号 | \`${version}\` |`] : []),
|
||||
`| 📝 提交 | \`${shortSha}\` |`,
|
||||
`| 🔗 构建日志 | [查看详情](${runUrl}) |`,
|
||||
`| 🕐 完成时间 | ${time} |`,
|
||||
];
|
||||
|
||||
// 添加错误详情
|
||||
const failedTargets = targets.filter(t => t.status === 'failure' && t.error);
|
||||
if (failedTargets.length > 0) {
|
||||
lines.push('', '---', '', '## ⚠️ 错误详情', '');
|
||||
for (const target of failedTargets) {
|
||||
lines.push(
|
||||
`<details>`,
|
||||
`<summary>🔴 <b>${target.name}</b> 构建错误</summary>`,
|
||||
'',
|
||||
'```',
|
||||
escapeCodeBlock(target.error!),
|
||||
'```',
|
||||
'',
|
||||
'</details>',
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加底部提示
|
||||
lines.push('---', '');
|
||||
if (allSuccess) {
|
||||
lines.push(
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> 🎉 **所有构建均已成功完成!**',
|
||||
'>',
|
||||
'> 点击上方下载链接获取构建产物进行测试',
|
||||
'',
|
||||
'</div>'
|
||||
);
|
||||
} else if (anyCancelled && !anyFailure) {
|
||||
lines.push(
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> ⚪ **构建已被取消**',
|
||||
'>',
|
||||
'> 可能是由于新的提交触发了新的构建',
|
||||
'',
|
||||
'</div>'
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> ⚠️ **部分构建失败**',
|
||||
'>',
|
||||
'> 请查看上方错误详情或点击构建日志查看完整输出',
|
||||
'',
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
189
.github/scripts/lib/github.ts
vendored
189
.github/scripts/lib/github.ts
vendored
@@ -1,189 +0,0 @@
|
||||
/**
|
||||
* GitHub API 工具库
|
||||
*/
|
||||
|
||||
import { appendFileSync } from 'node:fs';
|
||||
|
||||
// ============== 类型定义 ==============
|
||||
|
||||
export interface PullRequest {
|
||||
number: number;
|
||||
state: string;
|
||||
head: {
|
||||
sha: string;
|
||||
ref: string;
|
||||
repo: {
|
||||
full_name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
owner: {
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Artifact {
|
||||
id: number;
|
||||
name: string;
|
||||
size_in_bytes: number;
|
||||
archive_download_url: string;
|
||||
}
|
||||
|
||||
// ============== GitHub API Client ==========================
|
||||
|
||||
export class GitHubAPI {
|
||||
private token: string;
|
||||
private baseUrl = 'https://api.github.com';
|
||||
|
||||
constructor (token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
private async request<T> (endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async getPullRequest (owner: string, repo: string, pullNumber: number): Promise<PullRequest> {
|
||||
return this.request<PullRequest>(`/repos/${owner}/${repo}/pulls/${pullNumber}`);
|
||||
}
|
||||
|
||||
async getCollaboratorPermission (owner: string, repo: string, username: string): Promise<string> {
|
||||
const data = await this.request<{ permission: string; }>(
|
||||
`/repos/${owner}/${repo}/collaborators/${username}/permission`
|
||||
);
|
||||
return data.permission;
|
||||
}
|
||||
|
||||
async getRepository (owner: string, repo: string): Promise<Repository> {
|
||||
return this.request(`/repos/${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async checkOrgMembership (org: string, username: string): Promise<boolean> {
|
||||
try {
|
||||
await this.request(`/orgs/${org}/members/${username}`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getRunArtifacts (owner: string, repo: string, runId: string): Promise<Artifact[]> {
|
||||
const data = await this.request<{ artifacts: Artifact[]; }>(
|
||||
`/repos/${owner}/${repo}/actions/runs/${runId}/artifacts`
|
||||
);
|
||||
return data.artifacts;
|
||||
}
|
||||
|
||||
async createComment (owner: string, repo: string, issueNumber: number, body: string): Promise<void> {
|
||||
await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ body }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async findComment (owner: string, repo: string, issueNumber: number, marker: string): Promise<number | null> {
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
|
||||
while (page <= 10) { // 最多检查 1000 条评论
|
||||
const comments = await this.request<Array<{ id: number, body: string; }>>(
|
||||
`/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`
|
||||
);
|
||||
|
||||
if (comments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const found = comments.find(c => c.body.includes(marker));
|
||||
if (found) {
|
||||
return found.id;
|
||||
}
|
||||
|
||||
if (comments.length < perPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async updateComment (owner: string, repo: string, commentId: number, body: string): Promise<void> {
|
||||
await this.request(`/repos/${owner}/${repo}/issues/comments/${commentId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ body }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdateComment (
|
||||
owner: string,
|
||||
repo: string,
|
||||
issueNumber: number,
|
||||
body: string,
|
||||
marker: string
|
||||
): Promise<void> {
|
||||
const existingId = await this.findComment(owner, repo, issueNumber, marker);
|
||||
if (existingId) {
|
||||
await this.updateComment(owner, repo, existingId, body);
|
||||
console.log(`✓ Updated comment #${existingId}`);
|
||||
} else {
|
||||
await this.createComment(owner, repo, issueNumber, body);
|
||||
console.log('✓ Created new comment');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Output 工具 ==============
|
||||
|
||||
export function setOutput (name: string, value: string): void {
|
||||
const outputFile = process.env.GITHUB_OUTPUT;
|
||||
if (outputFile) {
|
||||
appendFileSync(outputFile, `${name}=${value}\n`);
|
||||
}
|
||||
console.log(` ${name}=${value}`);
|
||||
}
|
||||
|
||||
export function setMultilineOutput (name: string, value: string): void {
|
||||
const outputFile = process.env.GITHUB_OUTPUT;
|
||||
if (outputFile) {
|
||||
const delimiter = `EOF_${Date.now()}`;
|
||||
appendFileSync(outputFile, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 环境变量工具 ==============
|
||||
|
||||
export function getEnv (name: string, required: true): string;
|
||||
export function getEnv (name: string, required?: false): string | undefined;
|
||||
export function getEnv (name: string, required = false): string | undefined {
|
||||
const value = process.env[name];
|
||||
if (required && !value) {
|
||||
throw new Error(`Environment variable ${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getRepository (): { owner: string, repo: string; } {
|
||||
const repository = getEnv('GITHUB_REPOSITORY', true);
|
||||
const [owner, repo] = repository.split('/');
|
||||
return { owner, repo };
|
||||
}
|
||||
36
.github/scripts/pr-build-building.ts
vendored
36
.github/scripts/pr-build-building.ts
vendored
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* PR Build - 更新构建中状态评论
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - PR_NUMBER: PR 编号
|
||||
* - PR_SHA: PR 提交 SHA
|
||||
*/
|
||||
|
||||
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
|
||||
import { generateBuildingComment, COMMENT_MARKER } from './lib/comment.ts';
|
||||
|
||||
const BUILD_TARGETS = ['NapCat.Framework', 'NapCat.Shell'];
|
||||
|
||||
async function main (): Promise<void> {
|
||||
console.log('🔨 Updating building status comment\n');
|
||||
|
||||
const token = getEnv('GITHUB_TOKEN', true);
|
||||
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
|
||||
const prSha = getEnv('PR_SHA', true);
|
||||
const { owner, repo } = getRepository();
|
||||
|
||||
console.log(`PR: #${prNumber}`);
|
||||
console.log(`SHA: ${prSha}`);
|
||||
console.log(`Repo: ${owner}/${repo}\n`);
|
||||
|
||||
const github = new GitHubAPI(token);
|
||||
const comment = generateBuildingComment(prSha, BUILD_TARGETS);
|
||||
|
||||
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
206
.github/scripts/pr-build-check.ts
vendored
206
.github/scripts/pr-build-check.ts
vendored
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* PR Build Check Script
|
||||
* 检查 PR 构建触发条件和用户权限
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - GITHUB_EVENT_NAME: 事件名称
|
||||
* - GITHUB_EVENT_PATH: 事件 payload 文件路径
|
||||
* - GITHUB_REPOSITORY: 仓库名称 (owner/repo)
|
||||
* - GITHUB_OUTPUT: 输出文件路径
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { GitHubAPI, getEnv, getRepository, setOutput } from './lib/github.ts';
|
||||
import type { PullRequest } from './lib/github.ts';
|
||||
|
||||
// ============== 类型定义 ==============
|
||||
|
||||
interface GitHubPayload {
|
||||
pull_request?: PullRequest;
|
||||
issue?: {
|
||||
number: number;
|
||||
pull_request?: object;
|
||||
};
|
||||
comment?: {
|
||||
body: string;
|
||||
user: { login: string; };
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckResult {
|
||||
should_build: boolean;
|
||||
pr_number?: number;
|
||||
pr_sha?: string;
|
||||
pr_head_repo?: string;
|
||||
pr_head_ref?: string;
|
||||
}
|
||||
|
||||
// ============== 权限检查 ==============
|
||||
|
||||
async function checkUserPermission (
|
||||
github: GitHubAPI,
|
||||
owner: string,
|
||||
repo: string,
|
||||
username: string
|
||||
): Promise<boolean> {
|
||||
// 方法1:检查仓库协作者权限
|
||||
try {
|
||||
const permission = await github.getCollaboratorPermission(owner, repo, username);
|
||||
if (['admin', 'write', 'maintain'].includes(permission)) {
|
||||
console.log(`✓ User ${username} has ${permission} permission`);
|
||||
return true;
|
||||
}
|
||||
console.log(`✗ User ${username} has ${permission} permission (insufficient)`);
|
||||
} catch (e) {
|
||||
console.log(`✗ Failed to get collaborator permission: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
// 方法2:检查组织成员身份
|
||||
try {
|
||||
const repoInfo = await github.getRepository(owner, repo);
|
||||
if (repoInfo.owner.type === 'Organization') {
|
||||
const isMember = await github.checkOrgMembership(owner, username);
|
||||
if (isMember) {
|
||||
console.log(`✓ User ${username} is organization member`);
|
||||
return true;
|
||||
}
|
||||
console.log(`✗ User ${username} is not organization member`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`✗ Failed to check org membership: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============== 事件处理 ==============
|
||||
|
||||
function handlePullRequestTarget (payload: GitHubPayload): CheckResult {
|
||||
const pr = payload.pull_request;
|
||||
|
||||
if (!pr) {
|
||||
console.log('✗ No pull_request in payload');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
if (pr.state !== 'open') {
|
||||
console.log(`✗ PR is not open (state: ${pr.state})`);
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
console.log(`✓ PR #${pr.number} is open, triggering build`);
|
||||
return {
|
||||
should_build: true,
|
||||
pr_number: pr.number,
|
||||
pr_sha: pr.head.sha,
|
||||
pr_head_repo: pr.head.repo.full_name,
|
||||
pr_head_ref: pr.head.ref,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleIssueComment (
|
||||
payload: GitHubPayload,
|
||||
github: GitHubAPI,
|
||||
owner: string,
|
||||
repo: string
|
||||
): Promise<CheckResult> {
|
||||
const { issue, comment } = payload;
|
||||
|
||||
if (!issue || !comment) {
|
||||
console.log('✗ No issue or comment in payload');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
// 检查是否是 PR 的评论
|
||||
if (!issue.pull_request) {
|
||||
console.log('✗ Comment is not on a PR');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
// 检查是否是 /build 命令
|
||||
if (!comment.body.trim().startsWith('/build')) {
|
||||
console.log('✗ Comment is not a /build command');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
console.log(`→ /build command from @${comment.user.login}`);
|
||||
|
||||
// 获取 PR 详情
|
||||
const pr = await github.getPullRequest(owner, repo, issue.number);
|
||||
|
||||
// 检查 PR 状态
|
||||
if (pr.state !== 'open') {
|
||||
console.log(`✗ PR is not open (state: ${pr.state})`);
|
||||
await github.createComment(owner, repo, issue.number, '⚠️ 此 PR 已关闭,无法触发构建。');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
// 检查用户权限
|
||||
const username = comment.user.login;
|
||||
const hasPermission = await checkUserPermission(github, owner, repo, username);
|
||||
|
||||
if (!hasPermission) {
|
||||
console.log(`✗ User ${username} has no permission`);
|
||||
await github.createComment(
|
||||
owner,
|
||||
repo,
|
||||
issue.number,
|
||||
`⚠️ @${username} 您没有权限使用 \`/build\` 命令,仅仓库协作者或组织成员可使用。`
|
||||
);
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
console.log(`✓ Build triggered by @${username}`);
|
||||
return {
|
||||
should_build: true,
|
||||
pr_number: issue.number,
|
||||
pr_sha: pr.head.sha,
|
||||
pr_head_repo: pr.head.repo.full_name,
|
||||
pr_head_ref: pr.head.ref,
|
||||
};
|
||||
}
|
||||
|
||||
// ============== 主函数 ==============
|
||||
|
||||
async function main (): Promise<void> {
|
||||
console.log('🔍 PR Build Check\n');
|
||||
|
||||
const token = getEnv('GITHUB_TOKEN', true);
|
||||
const eventName = getEnv('GITHUB_EVENT_NAME', true);
|
||||
const eventPath = getEnv('GITHUB_EVENT_PATH', true);
|
||||
const { owner, repo } = getRepository();
|
||||
|
||||
console.log(`Event: ${eventName}`);
|
||||
console.log(`Repository: ${owner}/${repo}\n`);
|
||||
|
||||
const payload = JSON.parse(readFileSync(eventPath, 'utf-8')) as GitHubPayload;
|
||||
const github = new GitHubAPI(token);
|
||||
|
||||
let result: CheckResult;
|
||||
|
||||
switch (eventName) {
|
||||
case 'pull_request_target':
|
||||
result = handlePullRequestTarget(payload);
|
||||
break;
|
||||
case 'issue_comment':
|
||||
result = await handleIssueComment(payload, github, owner, repo);
|
||||
break;
|
||||
default:
|
||||
console.log(`✗ Unsupported event: ${eventName}`);
|
||||
result = { should_build: false };
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
console.log('\n=== Outputs ===');
|
||||
setOutput('should_build', String(result.should_build));
|
||||
setOutput('pr_number', String(result.pr_number ?? ''));
|
||||
setOutput('pr_sha', result.pr_sha ?? '');
|
||||
setOutput('pr_head_repo', result.pr_head_repo ?? '');
|
||||
setOutput('pr_head_ref', result.pr_head_ref ?? '');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
90
.github/scripts/pr-build-result.ts
vendored
90
.github/scripts/pr-build-result.ts
vendored
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* PR Build - 更新构建结果评论
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - PR_NUMBER: PR 编号
|
||||
* - PR_SHA: PR 提交 SHA
|
||||
* - RUN_ID: GitHub Actions Run ID
|
||||
* - NAPCAT_VERSION: 构建版本号
|
||||
* - FRAMEWORK_STATUS: Framework 构建状态
|
||||
* - FRAMEWORK_ERROR: Framework 构建错误信息
|
||||
* - SHELL_STATUS: Shell 构建状态
|
||||
* - SHELL_ERROR: Shell 构建错误信息
|
||||
*/
|
||||
|
||||
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
|
||||
import { generateResultComment, COMMENT_MARKER } from './lib/comment.ts';
|
||||
import type { BuildTarget, BuildStatus } from './lib/comment.ts';
|
||||
|
||||
function parseStatus (value: string | undefined): BuildStatus {
|
||||
if (value === 'success' || value === 'failure' || value === 'cancelled') {
|
||||
return value;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
async function main (): Promise<void> {
|
||||
console.log('📝 Updating build result comment\n');
|
||||
|
||||
const token = getEnv('GITHUB_TOKEN', true);
|
||||
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
|
||||
const prSha = getEnv('PR_SHA') || 'unknown';
|
||||
const runId = getEnv('RUN_ID', true);
|
||||
const version = getEnv('NAPCAT_VERSION') || '';
|
||||
const { owner, repo } = getRepository();
|
||||
|
||||
const frameworkStatus = parseStatus(getEnv('FRAMEWORK_STATUS'));
|
||||
const frameworkError = getEnv('FRAMEWORK_ERROR');
|
||||
const shellStatus = parseStatus(getEnv('SHELL_STATUS'));
|
||||
const shellError = getEnv('SHELL_ERROR');
|
||||
|
||||
console.log(`PR: #${prNumber}`);
|
||||
console.log(`SHA: ${prSha}`);
|
||||
console.log(`Version: ${version}`);
|
||||
console.log(`Run: ${runId}`);
|
||||
console.log(`Framework: ${frameworkStatus}${frameworkError ? ` (${frameworkError})` : ''}`);
|
||||
console.log(`Shell: ${shellStatus}${shellError ? ` (${shellError})` : ''}\n`);
|
||||
|
||||
const github = new GitHubAPI(token);
|
||||
const repository = `${owner}/${repo}`;
|
||||
|
||||
// 获取 artifacts 列表,生成直接下载链接
|
||||
const artifactMap: Record<string, string> = {};
|
||||
try {
|
||||
const artifacts = await github.getRunArtifacts(owner, repo, runId);
|
||||
console.log(`Found ${artifacts.length} artifacts`);
|
||||
for (const artifact of artifacts) {
|
||||
// 生成直接下载链接:https://github.com/{owner}/{repo}/actions/runs/{run_id}/artifacts/{artifact_id}
|
||||
const downloadUrl = `https://github.com/${repository}/actions/runs/${runId}/artifacts/${artifact.id}`;
|
||||
artifactMap[artifact.name] = downloadUrl;
|
||||
console.log(` - ${artifact.name}: ${downloadUrl}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Warning: Failed to get artifacts: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
const targets: BuildTarget[] = [
|
||||
{
|
||||
name: 'NapCat.Framework',
|
||||
status: frameworkStatus,
|
||||
error: frameworkError,
|
||||
downloadUrl: artifactMap['NapCat.Framework'],
|
||||
},
|
||||
{
|
||||
name: 'NapCat.Shell',
|
||||
status: shellStatus,
|
||||
error: shellError,
|
||||
downloadUrl: artifactMap['NapCat.Shell'],
|
||||
},
|
||||
];
|
||||
|
||||
const comment = generateResultComment(targets, prSha, runId, repository, version);
|
||||
|
||||
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
149
.github/scripts/pr-build-run.ts
vendored
149
.github/scripts/pr-build-run.ts
vendored
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* PR Build Runner
|
||||
* 执行构建步骤
|
||||
*
|
||||
* 用法: node pr-build-run.ts <target>
|
||||
* target: framework | shell
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, renameSync, unlinkSync } from 'node:fs';
|
||||
import { setOutput } from './lib/github.ts';
|
||||
|
||||
type BuildTarget = 'framework' | 'shell';
|
||||
|
||||
interface BuildStep {
|
||||
name: string;
|
||||
command: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
// ============== 构建步骤 ==============
|
||||
|
||||
function getCommonSteps (): BuildStep[] {
|
||||
return [
|
||||
{
|
||||
name: 'Install pnpm',
|
||||
command: 'npm i -g pnpm',
|
||||
errorMessage: 'Failed to install pnpm',
|
||||
},
|
||||
{
|
||||
name: 'Install dependencies',
|
||||
command: 'pnpm i',
|
||||
errorMessage: 'Failed to install dependencies',
|
||||
},
|
||||
{
|
||||
name: 'Type check',
|
||||
command: 'pnpm run typecheck',
|
||||
errorMessage: 'Type check failed',
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
command: 'pnpm test',
|
||||
errorMessage: 'Tests failed',
|
||||
},
|
||||
{
|
||||
name: 'Build WebUI',
|
||||
command: 'pnpm --filter napcat-webui-frontend run build',
|
||||
errorMessage: 'WebUI build failed',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getTargetSteps (target: BuildTarget): BuildStep[] {
|
||||
if (target === 'framework') {
|
||||
return [
|
||||
{
|
||||
name: 'Build Framework',
|
||||
command: 'pnpm run build:framework',
|
||||
errorMessage: 'Framework build failed',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: 'Build Shell',
|
||||
command: 'pnpm run build:shell',
|
||||
errorMessage: 'Shell build failed',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============== 执行器 ==============
|
||||
|
||||
function runStep (step: BuildStep): boolean {
|
||||
console.log(`\n::group::${step.name}`);
|
||||
console.log(`> ${step.command}\n`);
|
||||
|
||||
try {
|
||||
execSync(step.command, {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
|
||||
});
|
||||
console.log('::endgroup::');
|
||||
console.log(`✓ ${step.name}`);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
console.log('::endgroup::');
|
||||
console.log(`✗ ${step.name}`);
|
||||
setOutput('error', step.errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function postBuild (target: BuildTarget): void {
|
||||
const srcDir = target === 'framework'
|
||||
? 'packages/napcat-framework/dist'
|
||||
: 'packages/napcat-shell/dist';
|
||||
const destDir = target === 'framework' ? 'framework-dist' : 'shell-dist';
|
||||
|
||||
console.log(`\n→ Moving ${srcDir} to ${destDir}`);
|
||||
|
||||
if (!existsSync(srcDir)) {
|
||||
throw new Error(`Build output not found: ${srcDir}`);
|
||||
}
|
||||
|
||||
renameSync(srcDir, destDir);
|
||||
|
||||
// Install production dependencies
|
||||
console.log('→ Installing production dependencies');
|
||||
execSync('npm install --omit=dev', {
|
||||
cwd: destDir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
|
||||
});
|
||||
|
||||
// Remove package-lock.json
|
||||
const lockFile = `${destDir}/package-lock.json`;
|
||||
if (existsSync(lockFile)) {
|
||||
unlinkSync(lockFile);
|
||||
}
|
||||
|
||||
console.log(`✓ Build output ready at ${destDir}`);
|
||||
}
|
||||
|
||||
// ============== 主函数 ==============
|
||||
|
||||
function main (): void {
|
||||
const target = process.argv[2] as BuildTarget;
|
||||
|
||||
if (!target || !['framework', 'shell'].includes(target)) {
|
||||
console.error('Usage: node pr-build-run.ts <framework|shell>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🔨 Building NapCat.${target === 'framework' ? 'Framework' : 'Shell'}\n`);
|
||||
|
||||
const steps = [...getCommonSteps(), ...getTargetSteps(target)];
|
||||
|
||||
for (const step of steps) {
|
||||
if (!runStep(step)) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
postBuild(target);
|
||||
console.log('\n✅ Build completed successfully!');
|
||||
}
|
||||
|
||||
main();
|
||||
83
.github/workflows/auto-release.yml
vendored
83
.github/workflows/auto-release.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: Auto Release Docker
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
shell-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger NapCat-Docker docker-publish workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCat-Docker/actions/workflows/docker-publish.yml/dispatches \
|
||||
-d '{"ref":"main"}'
|
||||
framework-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger NapCat-Framework-Docker docker-publish workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCat.Docker.Framework/actions/workflows/docker-image.yml/dispatches \
|
||||
-d '{"ref":"main"}'
|
||||
appimage-shell-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Get Latest NapCat Version
|
||||
id: get_version
|
||||
run: |
|
||||
# 获取当前仓库的最新 tag
|
||||
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# 输出调试信息
|
||||
echo "Debug: Latest NapCat Version is ${latest_tag}"
|
||||
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||
- name: Trigger Release NapCat AppImage Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
NAPCAT_VERSION: ${{ env.latest_tag }}
|
||||
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
|
||||
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
|
||||
run: |
|
||||
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_version_x86_64=${QQ_VERSION_X86_64}, qq_version_arm64=${QQ_VERSION_ARM64}"
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCatAppImageBuild/actions/workflows/release.yml/dispatches \
|
||||
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_version_x86_64\":\"${QQ_VERSION_X86_64}\",\"qq_version_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
||||
node-shell-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Get Latest NapCat Version
|
||||
id: get_version
|
||||
run: |
|
||||
# 获取当前仓库的最新 tag
|
||||
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# 输出调试信息
|
||||
echo "Debug: Latest NapCat Version is ${latest_tag}"
|
||||
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||
- name: Trigger Release NapCat AppImage Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
NAPCAT_VERSION: ${{ env.latest_tag }}
|
||||
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
|
||||
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
|
||||
run: |
|
||||
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/release.yml/dispatches \
|
||||
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
||||
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build NapCat Artifacts
|
||||
name: "Build Action"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -8,87 +8,45 @@ on:
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
Build-Framework:
|
||||
Build-LiteLoader:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Generate Version
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-main.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
- name: Build NapCat.Framework
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm run typecheck || exit 1
|
||||
pnpm test || exit 1
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:framework
|
||||
mv packages/napcat-framework/dist framework-dist
|
||||
cd framework-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
cd packages/napcat-framework/dist
|
||||
pnpm install --production
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: framework-dist
|
||||
path: packages/napcat-framework/dist
|
||||
Build-Shell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Generate Version
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-main.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
- name: Build NapCat.Shell
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm run typecheck || exit 1
|
||||
pnpm test || exit 1
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:shell
|
||||
mv packages/napcat-shell/dist shell-dist
|
||||
cd shell-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
cd packages/napcat-shell/dist
|
||||
pnpm install --production
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: shell-dist
|
||||
path: packages/napcat-shell/dist
|
||||
|
||||
303
.github/workflows/pr-build.yml
vendored
303
.github/workflows/pr-build.yml
vendored
@@ -1,303 +0,0 @@
|
||||
# =============================================================================
|
||||
# PR 构建工作流
|
||||
# =============================================================================
|
||||
# 功能:
|
||||
# 1. 在 PR 提交时自动构建 Framework 和 Shell 包
|
||||
# 2. 支持通过 /build 命令手动触发构建(仅协作者/组织成员)
|
||||
# 3. 在 PR 中发布构建状态评论,并持续更新(不会重复创建)
|
||||
# 4. 支持 Fork PR 的构建(使用 pull_request_target 获取写权限)
|
||||
#
|
||||
# 安全说明:
|
||||
# - 使用 pull_request_target 事件,在 base 分支上下文运行
|
||||
# - 构建脚本始终从 base 分支 checkout,避免恶意 PR 篡改脚本
|
||||
# - PR 代码单独 checkout 到 workspace 目录
|
||||
# =============================================================================
|
||||
|
||||
name: PR Build
|
||||
|
||||
# =============================================================================
|
||||
# 触发条件
|
||||
# =============================================================================
|
||||
on:
|
||||
# PR 事件:打开、同步(新推送)、重新打开时触发
|
||||
# 注意:使用 pull_request_target 而非 pull_request,以便对 Fork PR 有写权限
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
# Issue 评论事件:用于响应 /build 命令
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# =============================================================================
|
||||
# 权限配置
|
||||
# =============================================================================
|
||||
permissions:
|
||||
contents: read # 读取仓库内容
|
||||
pull-requests: write # 写入 PR 评论
|
||||
issues: write # 写入 Issue 评论(/build 命令响应)
|
||||
actions: read # 读取 Actions 信息(获取构建日志链接)
|
||||
|
||||
# =============================================================================
|
||||
# 并发控制
|
||||
# =============================================================================
|
||||
# 同一 PR 的多次构建会取消之前未完成的构建,避免资源浪费
|
||||
# 注意:只有在 should_build=true 时才会进入实际构建流程,
|
||||
# issue_comment 事件如果不是 /build 命令,会在 check-build 阶段快速退出,
|
||||
# 不会取消正在进行的构建(因为 cancel-in-progress 只影响同 group 的后续任务)
|
||||
concurrency:
|
||||
# 使用不同的 group 策略:
|
||||
# - pull_request_target: 使用 PR 号
|
||||
# - issue_comment: 只有确认是 /build 命令时才使用 PR 号,否则使用 run_id(不冲突)
|
||||
group: pr-build-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/build') && github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# =============================================================================
|
||||
# 任务定义
|
||||
# =============================================================================
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 1: 检查构建条件
|
||||
# ---------------------------------------------------------------------------
|
||||
# 判断是否应该触发构建:
|
||||
# - pull_request_target 事件:总是触发
|
||||
# - issue_comment 事件:检查是否为 /build 命令,且用户有权限
|
||||
# ---------------------------------------------------------------------------
|
||||
check-build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }} # 是否应该构建
|
||||
pr_number: ${{ steps.check.outputs.pr_number }} # PR 编号
|
||||
pr_sha: ${{ steps.check.outputs.pr_sha }} # PR 最新提交 SHA
|
||||
pr_head_repo: ${{ steps.check.outputs.pr_head_repo }} # PR 源仓库(用于 Fork)
|
||||
pr_head_ref: ${{ steps.check.outputs.pr_head_ref }} # PR 源分支
|
||||
steps:
|
||||
# 仅 checkout 脚本目录,加快速度
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
# 使用 Node.js 24 以支持原生 TypeScript 执行
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 执行检查脚本,判断是否触发构建
|
||||
- name: Check trigger condition
|
||||
id: check
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: node --experimental-strip-types .github/scripts/pr-build-check.ts
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 2: 更新评论为"构建中"状态
|
||||
# ---------------------------------------------------------------------------
|
||||
# 在 PR 中创建或更新评论,显示构建正在进行中
|
||||
# ---------------------------------------------------------------------------
|
||||
update-comment-building:
|
||||
needs: check-build
|
||||
if: needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 更新 PR 评论,显示构建中状态
|
||||
- name: Update building comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
|
||||
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
|
||||
run: node --experimental-strip-types .github/scripts/pr-build-building.ts
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 3: 构建 Framework 包
|
||||
# ---------------------------------------------------------------------------
|
||||
# 执行 napcat-framework 的构建流程
|
||||
# ---------------------------------------------------------------------------
|
||||
build-framework:
|
||||
needs: [check-build, update-comment-building]
|
||||
if: needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
status: ${{ steps.build.outcome }} # 构建结果:success/failure
|
||||
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
|
||||
version: ${{ steps.version.outputs.version }} # 构建版本号
|
||||
steps:
|
||||
# 【安全】先从 base 分支 checkout 构建脚本
|
||||
# 这样即使 PR 中修改了脚本,也不会被执行
|
||||
- name: Checkout scripts from base
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
path: _scripts
|
||||
|
||||
# 将 PR 代码 checkout 到单独的 workspace 目录
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.check-build.outputs.pr_head_repo }}
|
||||
ref: ${{ needs.check-build.outputs.pr_sha }}
|
||||
path: workspace
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 获取最新 release tag 并生成版本号
|
||||
- name: Generate Version
|
||||
id: version
|
||||
working-directory: workspace
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${{ needs.check-build.outputs.pr_sha }}"
|
||||
SHORT_SHA="${SHORT_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-pr.${{ needs.check-build.outputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
|
||||
# 执行构建,使用 base 分支的脚本处理 workspace 中的代码
|
||||
- name: Build
|
||||
id: build
|
||||
working-directory: workspace
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts framework
|
||||
continue-on-error: true # 允许失败,后续更新评论时处理
|
||||
|
||||
# 构建成功时上传产物
|
||||
- name: Upload Artifact
|
||||
if: steps.build.outcome == 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: workspace/framework-dist
|
||||
retention-days: 7 # 保留 7 天
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 4: 构建 Shell 包
|
||||
# ---------------------------------------------------------------------------
|
||||
# 执行 napcat-shell 的构建流程(与 Framework 并行执行)
|
||||
# ---------------------------------------------------------------------------
|
||||
build-shell:
|
||||
needs: [check-build, update-comment-building]
|
||||
if: needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
status: ${{ steps.build.outcome }} # 构建结果:success/failure
|
||||
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
|
||||
version: ${{ steps.version.outputs.version }} # 构建版本号
|
||||
steps:
|
||||
# 【安全】先从 base 分支 checkout 构建脚本
|
||||
- name: Checkout scripts from base
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
path: _scripts
|
||||
|
||||
# 将 PR 代码 checkout 到单独的 workspace 目录
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.check-build.outputs.pr_head_repo }}
|
||||
ref: ${{ needs.check-build.outputs.pr_sha }}
|
||||
path: workspace
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 获取最新 release tag 并生成版本号
|
||||
- name: Generate Version
|
||||
id: version
|
||||
working-directory: workspace
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${{ needs.check-build.outputs.pr_sha }}"
|
||||
SHORT_SHA="${SHORT_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-pr.${{ needs.check-build.outputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
|
||||
# 执行构建
|
||||
- name: Build
|
||||
id: build
|
||||
working-directory: workspace
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts shell
|
||||
continue-on-error: true
|
||||
|
||||
# 构建成功时上传产物
|
||||
- name: Upload Artifact
|
||||
if: steps.build.outcome == 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: workspace/shell-dist
|
||||
retention-days: 7 # 保留 7 天
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 5: 更新评论为构建结果
|
||||
# ---------------------------------------------------------------------------
|
||||
# 汇总所有构建结果,更新 PR 评论显示最终状态
|
||||
# 使用 always() 确保即使构建失败/取消也会执行
|
||||
# ---------------------------------------------------------------------------
|
||||
update-comment-result:
|
||||
needs: [check-build, update-comment-building, build-framework, build-shell]
|
||||
if: always() && needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 更新评论,显示构建结果和下载链接
|
||||
- name: Update result comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
|
||||
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
# 构建版本号
|
||||
NAPCAT_VERSION: ${{ needs.build-framework.outputs.version || needs.build-shell.outputs.version || '' }}
|
||||
# 获取构建状态,如果 job 被跳过则标记为 cancelled
|
||||
FRAMEWORK_STATUS: ${{ needs.build-framework.outputs.status || 'cancelled' }}
|
||||
FRAMEWORK_ERROR: ${{ needs.build-framework.outputs.error }}
|
||||
SHELL_STATUS: ${{ needs.build-shell.outputs.status || 'cancelled' }}
|
||||
SHELL_ERROR: ${{ needs.build-shell.outputs.error }}
|
||||
run: node --experimental-strip-types .github/scripts/pr-build-result.ts
|
||||
408
.github/workflows/release.yml
vendored
408
.github/workflows/release.yml
vendored
@@ -1,51 +1,12 @@
|
||||
name: Release NapCat
|
||||
name: "Build Release"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions: write-all
|
||||
|
||||
env:
|
||||
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
|
||||
OPENROUTER_MODEL: "copilot/ant/gemini-3-flash-preview"
|
||||
RELEASE_NAME: "NapCat"
|
||||
|
||||
jobs:
|
||||
# 验证版本号格式
|
||||
validate-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
valid: ${{ steps.check.outputs.valid }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
steps:
|
||||
- name: Validate semantic version
|
||||
id: check
|
||||
run: |
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "Checking tag: $TAG"
|
||||
|
||||
# 语义化版本正则表达式
|
||||
# 支持: v1.0.0, v1.0.0-beta, v1.0.0-rc.1, v1.0.0-alpha.1+build.123
|
||||
SEMVER_REGEX="^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"
|
||||
|
||||
if [[ "$TAG" =~ $SEMVER_REGEX ]]; then
|
||||
echo "✅ Valid semantic version: $TAG"
|
||||
echo "valid=true" >> $GITHUB_OUTPUT
|
||||
echo "version=$TAG" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ Invalid version format: $TAG"
|
||||
echo "Expected format: vX.Y.Z or vX.Y.Z-prerelease"
|
||||
echo "Examples: v1.0.0, v1.2.3-beta, v2.0.0-rc.1"
|
||||
echo "valid=false" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Build-Framework:
|
||||
needs: validate-version
|
||||
if: needs.validate-version.outputs.valid == 'true'
|
||||
Build-LiteLoader:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
@@ -55,26 +16,19 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Build NapCat.Framework
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:framework
|
||||
mv packages/napcat-framework/dist framework-dist
|
||||
cd framework-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
cd packages/napcat-framework/dist
|
||||
pnpm install --production
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: framework-dist
|
||||
path: packages/napcat-framework/dist
|
||||
|
||||
Build-Shell:
|
||||
needs: validate-version
|
||||
if: needs.validate-version.outputs.valid == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
@@ -84,359 +38,45 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Build NapCat.Shell
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:shell
|
||||
mv packages/napcat-shell/dist shell-dist
|
||||
cd shell-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
cd packages/napcat-shell/dist
|
||||
pnpm install --production
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: shell-dist
|
||||
Download-QNX64:
|
||||
needs: Build-Shell
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Setup tools
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y aria2 unzip zip p7zip-full curl jq
|
||||
|
||||
- name: Download QQ x64, Node.js and Assemble NapCat.Shell.Windows.Node.zip
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TMPDIR=$(mktemp -d)
|
||||
cd "$TMPDIR"
|
||||
|
||||
# -----------------------------
|
||||
# 1) 下载 QQ x64
|
||||
# -----------------------------
|
||||
# JS_URL="https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
|
||||
# JS_URL="https://slave.docadan488.workers.dev/proxy?url=https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
|
||||
# NT_URL=$(curl -fsSL "$JS_URL" | grep -oP '"ntDownloadX64Url"\s*:\s*"\K[^"]+')
|
||||
NT_URL="https://dldir1v6.qq.com/qqfile/qq/QQNT/eb263b35/QQ9.9.23.42086_x64.exe"
|
||||
QQ_ZIP="$(basename "$NT_URL")"
|
||||
aria2c -x16 -s16 -k1M -o "$QQ_ZIP" "$NT_URL"
|
||||
|
||||
QQ_EXTRACT="$TMPDIR/qq_extracted"
|
||||
mkdir -p "$QQ_EXTRACT"
|
||||
7z x -y -o"$QQ_EXTRACT" "$QQ_ZIP" >/dev/null
|
||||
|
||||
# -----------------------------
|
||||
# 2) 下载 Node.js Windows x64 zip 22.11.0
|
||||
# -----------------------------
|
||||
NODE_VER="22.11.0"
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VER/node-v$NODE_VER-win-x64.zip"
|
||||
NODE_ZIP="node-v$NODE_VER-win-x64.zip"
|
||||
aria2c -x1 -s1 -k1M -o "$NODE_ZIP" "$NODE_URL"
|
||||
|
||||
NODE_EXTRACT="$TMPDIR/node_extracted"
|
||||
mkdir -p "$NODE_EXTRACT"
|
||||
unzip -q "$NODE_ZIP" -d "$NODE_EXTRACT"
|
||||
|
||||
# -----------------------------
|
||||
# 3) 创建输出目录
|
||||
# -----------------------------
|
||||
OUT_DIR="$GITHUB_WORKSPACE/NapCat.Shell.Windows.Node"
|
||||
mkdir -p "$OUT_DIR/NapCat.Shell.Windows.Node"
|
||||
|
||||
# -----------------------------
|
||||
# 4) 解压 NapCat.Shell.zip 到 napcat
|
||||
# -----------------------------
|
||||
cp -a "$GITHUB_WORKSPACE/artifacts/NapCat.Shell/." "$OUT_DIR/napcat/"
|
||||
|
||||
# -----------------------------
|
||||
# 5) 拷贝 QQ 文件到 NapCat.Shell.Windows.Node
|
||||
# -----------------------------
|
||||
QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node")
|
||||
for name in "${QQ_TARGETS[@]}"; do
|
||||
find "$QQ_EXTRACT" -iname "$name" -exec cp -a {} "$OUT_DIR" \; || true
|
||||
done
|
||||
|
||||
# -----------------------------
|
||||
# 6) 拷贝仓库文件 napcat.bat 和 index.js
|
||||
# -----------------------------
|
||||
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/napcat.bat" "$OUT_DIR/" || true
|
||||
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/index.js" "$OUT_DIR/" || true
|
||||
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/QQNT.dll" "$OUT_DIR/" || true
|
||||
# -----------------------------
|
||||
# 7) 拷贝 Node.exe 到 NapCat.Shell.Windows.Node
|
||||
# -----------------------------
|
||||
cp -a "$NODE_EXTRACT/node-v$NODE_VER-win-x64/node.exe" "$OUT_DIR/" || true
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell.Windows.Node
|
||||
path: NapCat.Shell.Windows.Node
|
||||
path: packages/napcat-shell/dist
|
||||
|
||||
release-napcat:
|
||||
needs: [Build-Framework, Build-Shell, Download-QNX64]
|
||||
needs: [Build-LiteLoader, Build-Shell]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download Artifacts
|
||||
- name: Download All Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Download NapCat.Shell.Windows.OneKey.zip
|
||||
- name: Compress subdirectories
|
||||
run: |
|
||||
curl -L -o NapCat.Shell.Windows.OneKey.zip https://github.com/NapNeko/NapCatResource/raw/main/NapCat.Shell.Windows.OneKey.zip
|
||||
|
||||
- name: Zip Artifacts
|
||||
run: |
|
||||
cd artifacts
|
||||
[ -d NapCat.Framework ] && (cd NapCat.Framework && zip -qr ../../NapCat.Framework.zip .)
|
||||
[ -d NapCat.Shell ] && (cd NapCat.Shell && zip -qr ../../NapCat.Shell.zip .)
|
||||
[ -d NapCat.Shell.Windows.Node ] && (cd NapCat.Shell.Windows.Node && zip -qr ../../NapCat.Shell.Windows.Node.zip .)
|
||||
cd ..
|
||||
|
||||
- name: Generate release note via OpenRouter
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENROUTER_API_URL: ${{ env.OPENROUTER_API_URL }}
|
||||
OPENROUTER_MODEL: ${{ env.OPENROUTER_MODEL }}
|
||||
GITHUB_OWNER: "NapNeko" # 替换成你的 repo owner
|
||||
GITHUB_REPO: "NapCatQQ" # 替换成你的 repo 名
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# 当前 tag
|
||||
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "Current tag: $CURRENT_TAG"
|
||||
|
||||
# 从 GitHub API 获取 tag 列表
|
||||
TAGS_JSON=$(curl -s "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/tags?per_page=100")
|
||||
TAGS=( $(echo "$TAGS_JSON" | jq -r '.[].name' | sort -V) )
|
||||
|
||||
# 找到上一个 tag
|
||||
PREV_TAG=""
|
||||
for i in "${!TAGS[@]}"; do
|
||||
if [ "${TAGS[$i]}" = "$CURRENT_TAG" ]; then
|
||||
if [ $i -gt 0 ]; then
|
||||
PREV_TAG="${TAGS[$((i-1))]}"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "⚠️ Could not find previous tag for $CURRENT_TAG, using first commit"
|
||||
PREV_TAG=$(git rev-list --max-parents=0 HEAD | head -1)
|
||||
fi
|
||||
|
||||
echo "Previous tag: $PREV_TAG"
|
||||
|
||||
# 强制拉取上一个 tag 和当前 tag
|
||||
git fetch origin "refs/tags/$PREV_TAG:refs/tags/$PREV_TAG" --force || true
|
||||
git fetch origin "refs/tags/$CURRENT_TAG:refs/tags/$CURRENT_TAG" --force || true
|
||||
|
||||
# 获取 commit,使用更清晰的格式
|
||||
# 格式: <type>: <subject> (<hash>)
|
||||
COMMITS=$(git log --pretty=format:'- %s (%h)' "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null || git log --pretty=format:'- %s (%h)' -20)
|
||||
|
||||
echo "Commit list from $PREV_TAG to $CURRENT_TAG:"
|
||||
echo "$COMMITS"
|
||||
|
||||
# 获取文件变化统计
|
||||
echo "Getting file change statistics..."
|
||||
FILE_STATS=$(git diff --stat "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null || echo "")
|
||||
|
||||
# 获取总体统计(最后一行)
|
||||
SUMMARY_LINE=$(echo "$FILE_STATS" | tail -1)
|
||||
echo "Summary: $SUMMARY_LINE"
|
||||
|
||||
# 获取每个文件的变化(去掉最后一行汇总)
|
||||
# 截断过长的输出(最多50个文件,每行最多80字符)
|
||||
FILE_CHANGES=$(echo "$FILE_STATS" | head -n -1 | head -50 | cut -c1-80)
|
||||
|
||||
# 如果文件变化太多,进一步精简:只保留主要目录的变化
|
||||
FILE_COUNT=$(echo "$FILE_STATS" | head -n -1 | wc -l)
|
||||
if [ "$FILE_COUNT" -gt 50 ]; then
|
||||
echo "Too many files ($FILE_COUNT), grouping by directory..."
|
||||
# 按目录分组统计
|
||||
DIR_STATS=$(git diff --stat "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | head -n -1 | \
|
||||
sed 's/|.*//g' | \
|
||||
awk -F'/' '{if(NF>1) print $1"/"$2; else print $1}' | \
|
||||
sort | uniq -c | sort -rn | head -20)
|
||||
FILE_CHANGES="[按目录分组统计 - 共 $FILE_COUNT 个文件变更]
|
||||
$DIR_STATS"
|
||||
fi
|
||||
|
||||
echo "File changes:"
|
||||
echo "$FILE_CHANGES"
|
||||
|
||||
# 获取具体代码变化(关键文件的diff)
|
||||
echo "Getting code diff for key files..."
|
||||
|
||||
# 定义关键目录(优先展示这些目录的变化)
|
||||
KEY_DIRS="packages/napcat-core packages/napcat-onebot packages/napcat-webui-backend"
|
||||
|
||||
# 获取变更的关键文件列表(排除测试、配置等)
|
||||
# 使用 || true 防止 grep 无匹配时返回非零退出码
|
||||
KEY_FILES=$(git diff --name-only "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | \
|
||||
grep -E "^packages/napcat-(core|onebot|webui-backend|shell)/" || true | \
|
||||
grep -E "\.(ts|js)$" || true | \
|
||||
grep -v -E "(test|spec|\.d\.ts|config)" || true | \
|
||||
head -15) || true
|
||||
|
||||
CODE_DIFF=""
|
||||
DIFF_CHAR_LIMIT=6000 # 总diff字符限制
|
||||
CURRENT_CHARS=0
|
||||
|
||||
if [ -n "$KEY_FILES" ]; then
|
||||
for file in $KEY_FILES; do
|
||||
if [ "$CURRENT_CHARS" -ge "$DIFF_CHAR_LIMIT" ]; then
|
||||
CODE_DIFF="$CODE_DIFF
|
||||
[... 更多文件变化已截断 ...]"
|
||||
break
|
||||
fi
|
||||
|
||||
# 获取单个文件的diff,限制每个文件最多50行
|
||||
FILE_DIFF=$(git diff "$PREV_TAG".."$CURRENT_TAG" -- "$file" 2>/dev/null | head -50) || true
|
||||
FILE_DIFF_LEN=${#FILE_DIFF}
|
||||
|
||||
# 如果单个文件diff超过1500字符,截断
|
||||
if [ "$FILE_DIFF_LEN" -gt 1500 ]; then
|
||||
FILE_DIFF=$(echo "$FILE_DIFF" | head -c 1500)
|
||||
FILE_DIFF="$FILE_DIFF
|
||||
[... 文件 $file 变化已截断 ...]"
|
||||
fi
|
||||
|
||||
if [ -n "$FILE_DIFF" ]; then
|
||||
CODE_DIFF="$CODE_DIFF
|
||||
|
||||
### $file
|
||||
\`\`\`diff
|
||||
$FILE_DIFF
|
||||
\`\`\`"
|
||||
CURRENT_CHARS=$((CURRENT_CHARS + FILE_DIFF_LEN))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 如果没有关键文件变化,获取前5个变更文件的diff
|
||||
if [ -z "$CODE_DIFF" ]; then
|
||||
echo "No key files changed, getting top changed files..."
|
||||
TOP_FILES=$(git diff --name-only "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | \
|
||||
grep -E "\.(ts|js|yml|md)$" | head -5) || true
|
||||
|
||||
if [ -n "$TOP_FILES" ]; then
|
||||
for file in $TOP_FILES; do
|
||||
FILE_DIFF=$(git diff "$PREV_TAG".."$CURRENT_TAG" -- "$file" 2>/dev/null | head -30) || true
|
||||
if [ -n "$FILE_DIFF" ] && [ ${#FILE_DIFF} -lt 1000 ]; then
|
||||
CODE_DIFF="$CODE_DIFF
|
||||
|
||||
### $file
|
||||
\`\`\`diff
|
||||
$FILE_DIFF
|
||||
\`\`\`"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# 如果仍然没有代码变化,添加说明
|
||||
if [ -z "$CODE_DIFF" ]; then
|
||||
CODE_DIFF="[本次更新主要涉及配置文件和文档变更,无核心代码变化]"
|
||||
fi
|
||||
|
||||
echo "Code diff preview:"
|
||||
echo "$CODE_DIFF" | head -50
|
||||
|
||||
# 读取 prompt
|
||||
PROMPT_FILE=".github/prompt/release_note_prompt.txt"
|
||||
SYSTEM_PROMPT=$(<"$PROMPT_FILE")
|
||||
|
||||
# 构建用户内容,传递更多上下文(包含文件变化和代码diff)
|
||||
USER_CONTENT="当前版本: $CURRENT_TAG
|
||||
上一版本: $PREV_TAG
|
||||
|
||||
## 提交列表
|
||||
$COMMITS
|
||||
|
||||
## 文件变化统计
|
||||
$SUMMARY_LINE
|
||||
|
||||
## 变更文件列表
|
||||
$FILE_CHANGES
|
||||
|
||||
## 关键代码变化
|
||||
$CODE_DIFF"
|
||||
|
||||
# 构建请求 JSON,增加 max_tokens 以获取更完整的输出
|
||||
BODY=$(jq -n \
|
||||
--arg system "$SYSTEM_PROMPT" \
|
||||
--arg user "$USER_CONTENT" \
|
||||
--arg model "$OPENROUTER_MODEL" \
|
||||
'{model: $model, messages:[{role:"system", content:$system},{role:"user", content:$user}], temperature:0.2, max_tokens:1500}')
|
||||
|
||||
echo "=== OpenRouter request body ==="
|
||||
echo "$BODY" | jq .
|
||||
|
||||
# 调用 OpenRouter
|
||||
if RESPONSE=$(curl -s -X POST "$OPENROUTER_API_URL" \
|
||||
-H "Authorization: Bearer $OPENAI_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY"); then
|
||||
echo "=== raw response ==="
|
||||
echo "$RESPONSE"
|
||||
echo "=== OpenRouter raw response ==="
|
||||
if echo "$RESPONSE" | jq . >/dev/null 2>&1; then
|
||||
echo "$RESPONSE" | jq .
|
||||
else
|
||||
echo "jq failed to parse response"
|
||||
fi
|
||||
|
||||
# 提取生成内容
|
||||
RELEASE_BODY=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // .choices[0].text // ""' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$RELEASE_BODY" ]; then
|
||||
echo "❌ OpenRouter failed to generate release note, using default.md"
|
||||
# 替换默认模板中的版本占位符
|
||||
sed "s/{VERSION}/$CURRENT_TAG/g" .github/prompt/default.md > CHANGELOG.md
|
||||
else
|
||||
# 后处理:确保版本号正确,并添加比较链接
|
||||
echo -e "$RELEASE_BODY" > CHANGELOG.md
|
||||
# 替换可能的占位符
|
||||
sed -i "s/{VERSION}/$CURRENT_TAG/g" CHANGELOG.md
|
||||
sed -i "s/{PREV_VERSION}/$PREV_TAG/g" CHANGELOG.md
|
||||
fi
|
||||
else
|
||||
echo "❌ Curl failed, using default.md"
|
||||
sed "s/{VERSION}/$CURRENT_TAG/g" .github/prompt/default.md > CHANGELOG.md
|
||||
fi
|
||||
echo "=== generated release note ==="
|
||||
cat CHANGELOG.md
|
||||
cd ./NapCat.Shell/
|
||||
zip -q -r NapCat.Shell.zip *
|
||||
cd ..
|
||||
cd ./NapCat.Framework/
|
||||
zip -q -r NapCat.Framework.zip *
|
||||
cd ..
|
||||
rm ./NapCat.Shell.zip -rf
|
||||
rm ./NapCat.Framework.zip -rf
|
||||
mv ./NapCat.Shell/NapCat.Shell.zip ./
|
||||
mv ./NapCat.Framework/NapCat.Framework.zip ./
|
||||
|
||||
- name: Create Release Draft and Upload Artifacts
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: NapCat ${{ github.ref_name }}
|
||||
name: NapCat
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body_path: CHANGELOG.md
|
||||
body: Automated release artifact (no version detection)
|
||||
files: |
|
||||
NapCat.Shell.Windows.Node.zip
|
||||
NapCat.Framework.zip
|
||||
NapCat.Shell.zip
|
||||
NapCat.Shell.Windows.OneKey.zip
|
||||
draft: true
|
||||
draft: true
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,4 +16,4 @@ checkVersion.sh
|
||||
bun.lockb
|
||||
tests/run/
|
||||
guild1.db-wal
|
||||
guild1.db-shm
|
||||
guild1.db-shm
|
||||
|
||||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "调试程序",
|
||||
"command": "pnpm run dev:shell",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
.vscode/settings.json
vendored
35
.vscode/settings.json
vendored
@@ -1,37 +1,2 @@
|
||||
{
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
".env.universal": ".env.*",
|
||||
"vite.config.ts": "vite*.ts",
|
||||
"README.md": "CODE_OF_CONDUCT.md, RELEASES.md, CONTRIBUTING.md, CHANGELOG.md, SECURITY.md",
|
||||
"tsconfig.json": "tsconfig.*.json, env.d.ts",
|
||||
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
|
||||
},
|
||||
"css.customData": [
|
||||
".vscode/tailwindcss.json"
|
||||
],
|
||||
"editor.detectIndentation": false,
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": false,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"files.autoSave": "onFocusChange",
|
||||
"javascript.preferences.quoteStyle": "single",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"javascript.format.semicolons": "insert",
|
||||
"typescript.format.semicolons": "insert",
|
||||
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||
"typescript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||
"typescript.format.insertSpaceAfterConstructor": true,
|
||||
"javascript.format.insertSpaceAfterConstructor": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"typescript.disableAutomaticTypeAcquisition": true
|
||||
}
|
||||
55
.vscode/tailwindcss.json
vendored
55
.vscode/tailwindcss.json
vendored
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -43,7 +43,7 @@ _Modern protocol-side framework implemented based on NTQQ._
|
||||
|
||||
**首次使用**请务必查看如下文档看使用教程
|
||||
|
||||
> 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
|
||||
> 项目非盈利,对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
|
||||
|
||||
## Link
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import neostandard from 'neostandard';
|
||||
|
||||
/** 尾随逗号 */
|
||||
const commaDangle = val => {
|
||||
if (val?.rules?.['@stylistic/comma-dangle']?.[0] === 'warn') {
|
||||
const rule = val?.rules?.['@stylistic/comma-dangle']?.[1];
|
||||
Object.keys(rule).forEach(key => {
|
||||
rule[key] = 'always-multiline';
|
||||
});
|
||||
val.rules['@stylistic/comma-dangle'][1] = rule;
|
||||
}
|
||||
|
||||
/** 三元表达式 */
|
||||
if (val?.rules?.['@stylistic/indent']) {
|
||||
val.rules['@stylistic/indent'][2] = {
|
||||
...val.rules?.['@stylistic/indent']?.[2],
|
||||
flatTernaryExpressions: true,
|
||||
offsetTernaryExpressions: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** 支持下划线 - 禁用 camelcase 规则 */
|
||||
if (val?.rules?.camelcase) {
|
||||
val.rules.camelcase = 'off';
|
||||
}
|
||||
|
||||
/** 未使用的变量强制报错 */
|
||||
if (val?.rules?.['@typescript-eslint/no-unused-vars']) {
|
||||
val.rules['@typescript-eslint/no-unused-vars'] = ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}];
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
/** 忽略的文件 */
|
||||
const ignores = [
|
||||
'node_modules',
|
||||
'**/dist/**',
|
||||
'launcher',
|
||||
];
|
||||
|
||||
const options = neostandard({
|
||||
ts: true,
|
||||
ignores,
|
||||
semi: true, // 强制使用分号
|
||||
}).map(commaDangle);
|
||||
|
||||
export default options;
|
||||
1457
package-lock.json
generated
Normal file
1457
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -2,31 +2,19 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"version": "4.9.42",
|
||||
"scripts": {
|
||||
"build:shell": "pnpm --filter napcat-shell run build || exit 1",
|
||||
"build:shell:dev": "pnpm --filter napcat-shell run build:dev || exit 1",
|
||||
"build:framework": "pnpm --filter napcat-framework run build || exit 1",
|
||||
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1",
|
||||
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1",
|
||||
"typecheck": "pnpm -r --if-present run typecheck",
|
||||
"test": "pnpm --filter napcat-test run test",
|
||||
"test:ui": "pnpm --filter napcat-test run test:ui",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"@vitest/ui": "^4.0.9",
|
||||
"eslint": "^9.39.1",
|
||||
"neostandard": "^0.12.2",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-cp": "^6.0.3",
|
||||
"vitest": "^4.0.9"
|
||||
"vite-plugin-cp": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"silk-wasm": "^3.6.1",
|
||||
"express": "^5.0.0",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts"
|
||||
@@ -16,8 +13,14 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"compressing": "^1.10.1",
|
||||
"json5": "^2.2.3",
|
||||
"ajv": "^8.13.0",
|
||||
"file-type": "^21.0.0"
|
||||
"file-type": "^21.0.0",
|
||||
"napcat-image-size": "workspace:*",
|
||||
"napcat-core": "workspace:*",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
|
||||
20
packages/napcat-common/src/audio-worker.ts
Normal file
20
packages/napcat-common/src/audio-worker.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { encode } from 'silk-wasm';
|
||||
import { parentPort } from 'worker_threads';
|
||||
|
||||
export interface EncodeArgs {
|
||||
input: ArrayBufferView | ArrayBuffer
|
||||
sampleRate: number
|
||||
}
|
||||
export function recvTask<T> (cb: (taskData: T) => Promise<unknown>) {
|
||||
parentPort?.on('message', async (taskData: T) => {
|
||||
try {
|
||||
const ret = await cb(taskData);
|
||||
parentPort?.postMessage(ret);
|
||||
} catch (error: unknown) {
|
||||
parentPort?.postMessage({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
}
|
||||
recvTask<EncodeArgs>(async ({ input, sampleRate }) => {
|
||||
return await encode(input, sampleRate);
|
||||
});
|
||||
84
packages/napcat-common/src/audio.ts
Normal file
84
packages/napcat-common/src/audio.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import fsPromise from 'fs/promises';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
|
||||
import { LogWrapper } from '@/napcat-common/log';
|
||||
import { EncodeArgs } from '@/napcat-common/audio-worker';
|
||||
import { FFmpegService } from '@/napcat-common/ffmpeg';
|
||||
import { runTask } from './worker';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
||||
|
||||
function getWorkerPath () {
|
||||
// return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
|
||||
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs');
|
||||
}
|
||||
|
||||
async function guessDuration (pttPath: string, logger: LogWrapper) {
|
||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
|
||||
logger.log('通过文件大小估算语音的时长:', duration);
|
||||
return duration;
|
||||
}
|
||||
|
||||
async function handleWavFile (
|
||||
file: Buffer,
|
||||
filePath: string,
|
||||
pcmPath: string
|
||||
): Promise<{ input: Buffer; sampleRate: number }> {
|
||||
const { fmt } = getWavFileInfo(file);
|
||||
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
|
||||
const result = await FFmpegService.convert(filePath, pcmPath);
|
||||
return { input: await fsPromise.readFile(pcmPath), sampleRate: result.sampleRate };
|
||||
}
|
||||
return { input: file, sampleRate: fmt.sampleRate };
|
||||
}
|
||||
|
||||
export async function encodeSilk (filePath: string, TEMP_DIR: string, logger: LogWrapper) {
|
||||
try {
|
||||
const file = await fsPromise.readFile(filePath);
|
||||
const pttPath = path.join(TEMP_DIR, randomUUID());
|
||||
if (!isSilk(file)) {
|
||||
logger.log(`语音文件${filePath}需要转换成silk`);
|
||||
const pcmPath = `${pttPath}.pcm`;
|
||||
// const { input, sampleRate } = isWav(file) ? await handleWavFile(file, filePath, pcmPath): { input: await FFmpegService.convert(filePath, pcmPath) ? await fsPromise.readFile(pcmPath) : Buffer.alloc(0), sampleRate: 24000 };
|
||||
let input: Buffer;
|
||||
let sampleRate: number;
|
||||
if (isWav(file)) {
|
||||
const result = await handleWavFile(file, filePath, pcmPath);
|
||||
input = result.input;
|
||||
sampleRate = result.sampleRate;
|
||||
} else {
|
||||
const result = await FFmpegService.convert(filePath, pcmPath);
|
||||
input = await fsPromise.readFile(pcmPath);
|
||||
sampleRate = result.sampleRate;
|
||||
}
|
||||
const silk = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { input, sampleRate });
|
||||
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
|
||||
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
|
||||
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
|
||||
return {
|
||||
converted: true,
|
||||
path: pttPath,
|
||||
duration: silk.duration / 1000,
|
||||
};
|
||||
} else {
|
||||
let duration = 0;
|
||||
try {
|
||||
duration = getDuration(file) / 1000;
|
||||
} catch (e: unknown) {
|
||||
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack);
|
||||
duration = await guessDuration(filePath, logger);
|
||||
}
|
||||
return {
|
||||
converted: false,
|
||||
path: filePath,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
logger.logError('convert silk failed', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import type { NapCatCore } from '@/napcat-core';
|
||||
import type { NapCatCore } from 'napcat-core';
|
||||
import json5 from 'json5';
|
||||
import Ajv, { AnySchema, ValidateFunction } from 'ajv';
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as os from 'os';
|
||||
import * as compressing from 'compressing'; // 修正导入方式
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { LogWrapper } from './log';
|
||||
|
||||
const downloadOri = 'https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip';
|
||||
const urls = [
|
||||
9
packages/napcat-common/src/env.d.ts
vendored
9
packages/napcat-common/src/env.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare global {
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_NAPCAT_VERSION: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NodeIQQNTWrapperSession } from '@/napcat-core/wrapper';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { ListenerNamingMapping, ServiceNamingMapping } from '@/napcat-core/index';
|
||||
import { ListenerNamingMapping, ServiceNamingMapping } from '@/napcat-core';
|
||||
|
||||
interface InternalMapKey {
|
||||
timeout: number;
|
||||
@@ -54,7 +54,7 @@ export class NTEventWrapper {
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>
|
||||
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
|
||||
> (eventName: `${Service}/${ServiceMethod}`): T | undefined {
|
||||
const eventNameArr = eventName.split('/');
|
||||
type eventType = {
|
||||
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>>; };
|
||||
@@ -112,7 +112,7 @@ export class NTEventWrapper {
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>
|
||||
>(
|
||||
> (
|
||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||
...args: Parameters<EventType>
|
||||
): Promise<Awaited<ReturnType<EventType>>> {
|
||||
@@ -123,7 +123,7 @@ export class NTEventWrapper {
|
||||
Listener extends keyof ListenerNamingMapping,
|
||||
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
|
||||
>(
|
||||
> (
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
checker: (...args: Parameters<ListenerType>) => boolean,
|
||||
waitTimes = 1,
|
||||
@@ -177,7 +177,7 @@ export class NTEventWrapper {
|
||||
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
|
||||
>(
|
||||
> (
|
||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
args: Parameters<EventType>,
|
||||
@@ -3,7 +3,7 @@
|
||||
* 自动检测并选择最佳的 FFmpeg 适配器
|
||||
*/
|
||||
|
||||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { LogWrapper } from './log';
|
||||
import { FFmpegAddonAdapter } from './ffmpeg-addon-adapter';
|
||||
import { FFmpegExecAdapter } from './ffmpeg-exec-adapter';
|
||||
import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';
|
||||
@@ -27,27 +27,21 @@ export interface IFFmpegAdapter {
|
||||
readonly name: string;
|
||||
|
||||
/** 是否可用 */
|
||||
isAvailable (): Promise<boolean>;
|
||||
isAvailable(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 获取视频信息(包含缩略图)
|
||||
* @param videoPath 视频文件路径
|
||||
* @returns 视频信息
|
||||
*/
|
||||
getVideoInfo (videoPath: string): Promise<VideoInfoResult>;
|
||||
getVideoInfo(videoPath: string): Promise<VideoInfoResult>;
|
||||
|
||||
/**
|
||||
* 获取音视频文件时长
|
||||
* @param filePath 文件路径
|
||||
* @returns 时长(秒)
|
||||
*/
|
||||
getDuration (filePath: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 判断是否为 Silk 格式
|
||||
* @param filePath 文件路径
|
||||
*/
|
||||
isSilk (filePath: string): Promise<boolean>;
|
||||
getDuration(filePath: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 转换音频为 PCM 格式
|
||||
@@ -55,7 +49,7 @@ export interface IFFmpegAdapter {
|
||||
* @param pcmPath 输出 PCM 文件路径
|
||||
* @returns PCM 数据 Buffer
|
||||
*/
|
||||
convertToPCM (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number; }>;
|
||||
convertToPCM(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }>;
|
||||
|
||||
/**
|
||||
* 转换音频文件
|
||||
@@ -63,14 +57,12 @@ export interface IFFmpegAdapter {
|
||||
* @param outputFile 输出文件路径
|
||||
* @param format 目标格式 ('amr' | 'silk' 等)
|
||||
*/
|
||||
convertFile (inputFile: string, outputFile: string, format: string): Promise<void>;
|
||||
convertFile(inputFile: string, outputFile: string, format: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 提取视频缩略图
|
||||
* @param videoPath 视频文件路径
|
||||
* @param thumbnailPath 缩略图输出路径
|
||||
*/
|
||||
extractThumbnail (videoPath: string, thumbnailPath: string): Promise<void>;
|
||||
|
||||
convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void>;
|
||||
extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void>;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { platform, arch } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { existsSync, openSync, readSync, closeSync } from 'node:fs';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import type { FFmpeg } from './ffmpeg-addon';
|
||||
import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface';
|
||||
@@ -68,13 +68,13 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
|
||||
const addon = this.ensureAddon();
|
||||
const info = await addon.getVideoInfo(videoPath);
|
||||
|
||||
const format = info.format.includes(',') ? info.format.split(',')[0] ?? info.format : info.format;
|
||||
let format = info.format.includes(',') ? info.format.split(',')[0] ?? info.format : info.format;
|
||||
console.log('[FFmpegAddonAdapter] Detected format:', format);
|
||||
return {
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
duration: info.duration,
|
||||
format,
|
||||
format: format,
|
||||
thumbnail: info.image,
|
||||
};
|
||||
}
|
||||
@@ -87,22 +87,6 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
|
||||
return addon.getDuration(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Silk 格式
|
||||
*/
|
||||
async isSilk (filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const fd = openSync(filePath, 'r');
|
||||
const buffer = Buffer.alloc(10);
|
||||
readSync(fd, buffer, 0, 10, 0);
|
||||
closeSync(fd);
|
||||
const header = buffer.toString();
|
||||
return header.includes('#!SILK') || header.includes('\x02#!SILK');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PCM
|
||||
*/
|
||||
@@ -122,11 +106,6 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
|
||||
await addon.decodeAudioToFmt(inputFile, outputFile, format);
|
||||
}
|
||||
|
||||
async convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void> {
|
||||
const addon = this.ensureAddon();
|
||||
await addon.convertToNTSilkTct(inputFile, outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取缩略图
|
||||
*/
|
||||
@@ -70,6 +70,4 @@ export interface FFmpeg {
|
||||
*/
|
||||
decodeAudioToPCM (filePath: string, pcmPath: string, sampleRate?: number): Promise<{ result: boolean, sampleRate: number; }>;
|
||||
decodeAudioToFmt (filePath: string, pcmPath: string, format: string): Promise<{ channels: number; sampleRate: number; format: string; }>;
|
||||
|
||||
convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void>;
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
* 使用 execFile 调用 FFmpeg 命令行工具的适配器实现
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync, mkdirSync, openSync, readSync, closeSync } from 'fs';
|
||||
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import { imageSizeFallBack } from 'napcat-image-size/src/index';
|
||||
import { downloadFFmpegIfNotExists } from './download-ffmpeg';
|
||||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { LogWrapper } from './log';
|
||||
import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -154,22 +154,6 @@ export class FFmpegExecAdapter implements IFFmpegAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Silk 格式
|
||||
*/
|
||||
async isSilk (filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const fd = openSync(filePath, 'r');
|
||||
const buffer = Buffer.alloc(10);
|
||||
readSync(fd, buffer, 0, 10, 0);
|
||||
closeSync(fd);
|
||||
const header = buffer.toString();
|
||||
return header.includes('#!SILK') || header.includes('\x02#!SILK');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PCM
|
||||
*/
|
||||
@@ -257,8 +241,4 @@ export class FFmpegExecAdapter implements IFFmpegAdapter {
|
||||
throw new Error(`提取缩略图失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async convertToNTSilkTct (_inputFile: string, _outputFile: string): Promise<void> {
|
||||
throw new Error('convertToNTSilkTct is not implemented in FFmpegExecAdapter');
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
import type { VideoInfo } from './video';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import { platform } from 'node:os';
|
||||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { LogWrapper } from './log';
|
||||
import { FFmpegAdapterFactory } from './ffmpeg-adapter-factory';
|
||||
import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';
|
||||
|
||||
@@ -53,6 +53,7 @@ export class FFmpegService {
|
||||
throw new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.');
|
||||
}
|
||||
return this.adapter.name;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,10 +65,7 @@ export class FFmpegService {
|
||||
}
|
||||
return this.adapter;
|
||||
}
|
||||
public static async convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void> {
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.convertToNTSilkTct(inputFile, outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 FFmpeg 路径并更新适配器
|
||||
* @deprecated 建议使用 init() 方法初始化
|
||||
@@ -95,27 +93,11 @@ export class FFmpegService {
|
||||
/**
|
||||
* 转换音频文件
|
||||
*/
|
||||
public static async convertAudioFmt (inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||
public static async convertFile (inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.convertFile(inputFile, outputFile, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频时长
|
||||
*/
|
||||
public static async getDuration (filePath: string): Promise<number> {
|
||||
const adapter = await this.getAdapter();
|
||||
return adapter.getDuration(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Silk 格式
|
||||
*/
|
||||
public static async isSilk (filePath: string): Promise<boolean> {
|
||||
const adapter = await this.getAdapter();
|
||||
return adapter.isSilk(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PCM 格式
|
||||
*/
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Peer } from './types';
|
||||
import { Peer } from '@/napcat-core';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
class TimeBasedCache<K, V> {
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import { stat } from 'fs/promises';
|
||||
import crypto, { randomUUID } from 'crypto';
|
||||
import path from 'node:path';
|
||||
import { solveProblem } from '@/napcat-common/src/helper';
|
||||
import { solveProblem } from '@/napcat-common/helper';
|
||||
|
||||
export interface HttpDownloadOptions {
|
||||
url: string;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import os from 'node:os';
|
||||
import { QQVersionConfigType, QQLevel } from './types';
|
||||
import { compareSemVer } from './version';
|
||||
import { getAllGitHubTags as getAllTagsFromMirror } from './mirror';
|
||||
|
||||
// 导出 compareSemVer 供其他模块使用
|
||||
export { compareSemVer } from './version';
|
||||
import { QQLevel } from '@/napcat-core';
|
||||
import { QQVersionConfigType } from './types';
|
||||
|
||||
export async function solveProblem<T extends (...arg: any[]) => any> (func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
|
||||
return new Promise<ReturnType<T> | undefined>((resolve) => {
|
||||
@@ -216,25 +212,3 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ============== GitHub Tags 获取 ==============
|
||||
// 使用 mirror 模块统一管理镜像
|
||||
|
||||
export async function getAllTags (): Promise<{ tags: string[], mirror: string; }> {
|
||||
return getAllTagsFromMirror('NapNeko', 'NapCatQQ');
|
||||
}
|
||||
|
||||
|
||||
export async function getLatestTag (): Promise<string> {
|
||||
const { tags } = await getAllTags();
|
||||
|
||||
// 使用 SemVer 规范排序
|
||||
tags.sort((a, b) => compareSemVer(a, b));
|
||||
|
||||
const latest = tags.at(-1);
|
||||
if (!latest) {
|
||||
throw new Error('No tags found');
|
||||
}
|
||||
// 去掉开头的 v
|
||||
return latest.replace(/^v/, '');
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export enum LogLevel {
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
export interface ILogWrapper {
|
||||
fileLogEnabled: boolean;
|
||||
consoleLogEnabled: boolean;
|
||||
cleanOldLogs (logDir: string): void;
|
||||
setFileAndConsoleLogLevel (fileLogLevel: LogLevel, consoleLogLevel: LogLevel): void;
|
||||
setLogSelfInfo (selfInfo: { nick: string; uid: string; }): void;
|
||||
setFileLogEnabled (isEnabled: boolean): void;
|
||||
setConsoleLogEnabled (isEnabled: boolean): void;
|
||||
formatMsg (msg: any[]): string;
|
||||
_log (level: LogLevel, ...args: any[]): void;
|
||||
log (...args: any[]): void;
|
||||
logDebug (...args: any[]): void;
|
||||
logError (...args: any[]): void;
|
||||
logWarn (...args: any[]): void;
|
||||
logFatal (...args: any[]): void;
|
||||
logMessage (msg: unknown, selfInfo: unknown): void;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import winston, { format, transports } from 'winston';
|
||||
import { truncateString } from 'napcat-common/src/helper';
|
||||
import { truncateString } from './helper';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
|
||||
import { ILogWrapper } from 'napcat-common/src/log-interface';
|
||||
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from 'napcat-core/index';
|
||||
import EventEmitter from 'node:events';
|
||||
export enum LogLevel {
|
||||
DEBUG = 'debug',
|
||||
@@ -57,7 +56,7 @@ class Subscription {
|
||||
|
||||
export const logSubscription = new Subscription();
|
||||
|
||||
export class LogWrapper implements ILogWrapper {
|
||||
export class LogWrapper {
|
||||
fileLogEnabled = true;
|
||||
consoleLogEnabled = true;
|
||||
logger: winston.Logger;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Peer } from '@/napcat-core';
|
||||
import crypto from 'crypto';
|
||||
import { Peer } from './types';
|
||||
|
||||
export class LimitedHashTable<K, V> {
|
||||
private readonly keyToValue: Map<K, V> = new Map();
|
||||
private readonly valueToKey: Map<V, K> = new Map();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
317
packages/napcat-common/src/performance-monitor.ts
Normal file
317
packages/napcat-common/src/performance-monitor.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 性能监控器 - 用于统计函数调用次数、耗时等信息
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface FunctionStats {
|
||||
name: string;
|
||||
callCount: number;
|
||||
totalTime: number;
|
||||
averageTime: number;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
fileName?: string;
|
||||
lineNumber?: number;
|
||||
}
|
||||
|
||||
export class PerformanceMonitor {
|
||||
private static instance: PerformanceMonitor;
|
||||
private stats = new Map<string, FunctionStats>();
|
||||
private startTimes = new Map<string, number>();
|
||||
private reportInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
static getInstance (): PerformanceMonitor {
|
||||
if (!PerformanceMonitor.instance) {
|
||||
PerformanceMonitor.instance = new PerformanceMonitor();
|
||||
// 启动定时统计报告
|
||||
PerformanceMonitor.instance.startPeriodicReport();
|
||||
}
|
||||
return PerformanceMonitor.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始定时统计报告 (每60秒)
|
||||
*/
|
||||
private startPeriodicReport (): void {
|
||||
if (this.reportInterval) {
|
||||
clearInterval(this.reportInterval);
|
||||
}
|
||||
|
||||
this.reportInterval = setInterval(() => {
|
||||
if (this.stats.size > 0) {
|
||||
this.printPeriodicReport();
|
||||
this.writeDetailedLogToFile();
|
||||
}
|
||||
}, 60000); // 60秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时统计报告
|
||||
*/
|
||||
stopPeriodicReport (): void {
|
||||
if (this.reportInterval) {
|
||||
clearInterval(this.reportInterval);
|
||||
this.reportInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印定时统计报告 (简化版本)
|
||||
*/
|
||||
private printPeriodicReport (): void {
|
||||
const now = new Date().toLocaleString();
|
||||
console.log(`\n=== 性能监控定时报告 [${now}] ===`);
|
||||
|
||||
const totalFunctions = this.stats.size;
|
||||
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
|
||||
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
|
||||
|
||||
console.log(`📊 总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms`);
|
||||
|
||||
// 显示Top 5最活跃的函数
|
||||
console.log('\n🔥 最活跃函数 (Top 5):');
|
||||
this.getTopByCallCount(5).forEach((stat, index) => {
|
||||
console.log(`${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
// 显示Top 5最耗时的函数
|
||||
console.log('\n⏱️ 最耗时函数 (Top 5):');
|
||||
this.getTopByTotalTime(5).forEach((stat, index) => {
|
||||
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
console.log('===============================\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将详细统计数据写入日志文件
|
||||
*/
|
||||
private writeDetailedLogToFile (): void {
|
||||
try {
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString().replace(/[:.]/g, '-').split('T')[0];
|
||||
const timeStr = now.toTimeString().split(' ')[0]?.replace(/:/g, '-') || 'unknown-time';
|
||||
const timestamp = `${dateStr}_${timeStr}`;
|
||||
const fileName = `${timestamp}.log.txt`;
|
||||
const logPath = path.join(process.cwd(), 'logs', fileName);
|
||||
|
||||
// 确保logs目录存在
|
||||
const logsDir = path.dirname(logPath);
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const totalFunctions = this.stats.size;
|
||||
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
|
||||
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
|
||||
|
||||
let logContent = '';
|
||||
logContent += '=== 性能监控详细报告 ===\n';
|
||||
logContent += `生成时间: ${now.toLocaleString()}\n`;
|
||||
logContent += '统计周期: 60秒\n';
|
||||
logContent += `总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms\n\n`;
|
||||
|
||||
// 详细函数统计
|
||||
logContent += '=== 所有函数详细统计 ===\n';
|
||||
const allStats = this.getStats().sort((a, b) => b.totalTime - a.totalTime);
|
||||
|
||||
allStats.forEach((stat, index) => {
|
||||
logContent += `${index + 1}. 函数: ${stat.name}\n`;
|
||||
logContent += ` 文件: ${stat.fileName || 'N/A'}\n`;
|
||||
logContent += ` 行号: ${stat.lineNumber || 'N/A'}\n`;
|
||||
logContent += ` 调用次数: ${stat.callCount}\n`;
|
||||
logContent += ` 总耗时: ${stat.totalTime.toFixed(4)}ms\n`;
|
||||
logContent += ` 平均耗时: ${stat.averageTime.toFixed(4)}ms\n`;
|
||||
logContent += ` 最小耗时: ${stat.minTime === Infinity ? 'N/A' : stat.minTime.toFixed(4)}ms\n`;
|
||||
logContent += ` 最大耗时: ${stat.maxTime.toFixed(4)}ms\n`;
|
||||
logContent += ` 性能占比: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
|
||||
logContent += '\n';
|
||||
});
|
||||
|
||||
// 排行榜统计
|
||||
logContent += '=== 总耗时排行榜 (Top 20) ===\n';
|
||||
this.getTopByTotalTime(20).forEach((stat, index) => {
|
||||
logContent += `${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
|
||||
});
|
||||
|
||||
logContent += '\n=== 调用次数排行榜 (Top 20) ===\n';
|
||||
this.getTopByCallCount(20).forEach((stat, index) => {
|
||||
logContent += `${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
|
||||
});
|
||||
|
||||
logContent += '\n=== 平均耗时排行榜 (Top 20) ===\n';
|
||||
this.getTopByAverageTime(20).forEach((stat, index) => {
|
||||
logContent += `${index + 1}. ${stat.name} - 平均: ${stat.averageTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms\n`;
|
||||
});
|
||||
|
||||
logContent += '\n=== 性能热点分析 ===\n';
|
||||
// 找出最耗时的前10个函数
|
||||
const hotSpots = this.getTopByTotalTime(10);
|
||||
hotSpots.forEach((stat, index) => {
|
||||
const efficiency = stat.callCount / stat.totalTime; // 每毫秒的调用次数
|
||||
logContent += `${index + 1}. ${stat.name}\n`;
|
||||
logContent += ` 性能影响: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
|
||||
logContent += ` 调用效率: ${efficiency.toFixed(4)} 调用/ms\n`;
|
||||
logContent += ` 优化建议: ${stat.averageTime > 10
|
||||
? '考虑优化此函数的执行效率'
|
||||
: stat.callCount > 1000
|
||||
? '考虑减少此函数的调用频率'
|
||||
: '性能表现良好'}\n\n`;
|
||||
});
|
||||
|
||||
logContent += '=== 报告结束 ===\n';
|
||||
|
||||
// 写入文件
|
||||
fs.writeFileSync(logPath, logContent, 'utf8');
|
||||
console.log(`📄 详细性能报告已保存到: ${logPath}`);
|
||||
} catch (error) {
|
||||
console.error('写入性能日志文件时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始记录函数调用
|
||||
*/
|
||||
startFunction (functionName: string, fileName?: string, lineNumber?: number): string {
|
||||
const callId = `${functionName}_${Date.now()}_${Math.random()}`;
|
||||
this.startTimes.set(callId, performance.now());
|
||||
|
||||
// 初始化或更新统计信息
|
||||
if (!this.stats.has(functionName)) {
|
||||
this.stats.set(functionName, {
|
||||
name: functionName,
|
||||
callCount: 0,
|
||||
totalTime: 0,
|
||||
averageTime: 0,
|
||||
minTime: Infinity,
|
||||
maxTime: 0,
|
||||
fileName,
|
||||
lineNumber,
|
||||
});
|
||||
}
|
||||
|
||||
const stat = this.stats.get(functionName)!;
|
||||
stat.callCount++;
|
||||
|
||||
return callId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束记录函数调用
|
||||
*/
|
||||
endFunction (callId: string, functionName: string): void {
|
||||
const startTime = this.startTimes.get(callId);
|
||||
if (!startTime) return;
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
this.startTimes.delete(callId);
|
||||
|
||||
const stat = this.stats.get(functionName);
|
||||
if (!stat) return;
|
||||
|
||||
stat.totalTime += duration;
|
||||
stat.averageTime = stat.totalTime / stat.callCount;
|
||||
stat.minTime = Math.min(stat.minTime, duration);
|
||||
stat.maxTime = Math.max(stat.maxTime, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有统计信息
|
||||
*/
|
||||
getStats (): FunctionStats[] {
|
||||
return Array.from(this.stats.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排行榜 - 按总耗时排序
|
||||
*/
|
||||
getTopByTotalTime (limit = 20): FunctionStats[] {
|
||||
return this.getStats()
|
||||
.sort((a, b) => b.totalTime - a.totalTime)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排行榜 - 按调用次数排序
|
||||
*/
|
||||
getTopByCallCount (limit = 20): FunctionStats[] {
|
||||
return this.getStats()
|
||||
.sort((a, b) => b.callCount - a.callCount)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排行榜 - 按平均耗时排序
|
||||
*/
|
||||
getTopByAverageTime (limit = 20): FunctionStats[] {
|
||||
return this.getStats()
|
||||
.sort((a, b) => b.averageTime - a.averageTime)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空统计数据
|
||||
*/
|
||||
clear (): void {
|
||||
this.stats.clear();
|
||||
this.startTimes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印统计报告
|
||||
*/
|
||||
printReport (): void {
|
||||
console.log('\n=== 函数性能监控报告 ===');
|
||||
|
||||
console.log('\n🔥 总耗时排行榜 (Top 10):');
|
||||
this.getTopByTotalTime(10).forEach((stat, index) => {
|
||||
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
console.log('\n📈 调用次数排行榜 (Top 10):');
|
||||
this.getTopByCallCount(10).forEach((stat, index) => {
|
||||
console.log(`${index + 1}. ${stat.name} - 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
console.log('\n⏱️ 平均耗时排行榜 (Top 10):');
|
||||
this.getTopByAverageTime(10).forEach((stat, index) => {
|
||||
console.log(`${index + 1}. ${stat.name} - 平均耗时: ${stat.averageTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
console.log('\n========================\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取JSON格式的统计数据
|
||||
*/
|
||||
toJSON (): FunctionStats[] {
|
||||
return this.getStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 全局性能监控器实例
|
||||
export const performanceMonitor = PerformanceMonitor.getInstance();
|
||||
|
||||
// 在进程退出时打印报告并停止定时器
|
||||
if (typeof process !== 'undefined') {
|
||||
process.on('exit', () => {
|
||||
performanceMonitor.stopPeriodicReport();
|
||||
performanceMonitor.printReport();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
performanceMonitor.stopPeriodicReport();
|
||||
performanceMonitor.printReport();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
performanceMonitor.stopPeriodicReport();
|
||||
performanceMonitor.printReport();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { LogWrapper } from '@/napcat-common/log';
|
||||
|
||||
export function proxyHandlerOf (logger: LogWrapper) {
|
||||
return {
|
||||
@@ -1,10 +1,10 @@
|
||||
import fs from 'node:fs';
|
||||
import { systemPlatform } from 'napcat-common/src/system';
|
||||
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from 'napcat-common/src/helper';
|
||||
import AppidTable from '@/napcat-core/external/appid.json';
|
||||
import { LogWrapper } from './log';
|
||||
import { getMajorPath } from '@/napcat-core/index';
|
||||
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from 'napcat-common/src/types';
|
||||
import { systemPlatform } from '@/napcat-common/system';
|
||||
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
|
||||
import AppidTable from 'napcat-core/external/appid.json';
|
||||
import { LogWrapper } from '@/napcat-common/log';
|
||||
import { getMajorPath } from 'napcat-core';
|
||||
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from './types';
|
||||
|
||||
export class QQBasicInfoWrapper {
|
||||
QQMainPath: string | undefined;
|
||||
@@ -3,11 +3,11 @@ import http from 'node:http';
|
||||
|
||||
export class RequestUtil {
|
||||
// 适用于获取服务器下发cookies时获取,仅GET
|
||||
static async HttpsGetCookies (url: string): Promise<{ [key: string]: string; }> {
|
||||
static async HttpsGetCookies (url: string): Promise<{ [key: string]: string }> {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = client.get(url, (res) => {
|
||||
const cookies: { [key: string]: string; } = {};
|
||||
const cookies: { [key: string]: string } = {};
|
||||
|
||||
res.on('data', () => { }); // Necessary to consume the stream
|
||||
res.on('end', () => {
|
||||
@@ -27,7 +27,7 @@ export class RequestUtil {
|
||||
});
|
||||
}
|
||||
|
||||
private static async handleRedirect (res: http.IncomingMessage, url: string, cookies: { [key: string]: string; }): Promise<{ [key: string]: string; }> {
|
||||
private static async handleRedirect (res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
if (res.headers.location) {
|
||||
const redirectUrl = new URL(res.headers.location, url);
|
||||
@@ -39,7 +39,7 @@ export class RequestUtil {
|
||||
return cookies;
|
||||
}
|
||||
|
||||
private static extractCookies (setCookieHeaders: string[], cookies: { [key: string]: string; }) {
|
||||
private static extractCookies (setCookieHeaders: string[], cookies: { [key: string]: string }) {
|
||||
setCookieHeaders.forEach((cookie) => {
|
||||
const parts = cookie.split(';')[0]?.split('=');
|
||||
if (parts) {
|
||||
@@ -53,10 +53,9 @@ export class RequestUtil {
|
||||
}
|
||||
|
||||
// 请求和回复都是JSON data传原始内容 自动编码json
|
||||
// 支持 301/302 重定向(最多 5 次)
|
||||
static async HttpGetJson<T> (url: string, method: string = 'GET', data?: any, headers: {
|
||||
[key: string]: string;
|
||||
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true, maxRedirects: number = 5): Promise<T> {
|
||||
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
|
||||
[key: string]: string
|
||||
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
|
||||
const option = new URL(url);
|
||||
const protocol = url.startsWith('https://') ? https : http;
|
||||
const options = {
|
||||
@@ -72,20 +71,6 @@ export class RequestUtil {
|
||||
// },
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = protocol.request(options, (res: http.IncomingMessage) => {
|
||||
// 处理重定向
|
||||
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
|
||||
if (maxRedirects <= 0) {
|
||||
reject(new Error('Too many redirects'));
|
||||
return;
|
||||
}
|
||||
const redirectUrl = new URL(res.headers.location, url).href;
|
||||
// 递归跟随重定向
|
||||
this.HttpGetJson<T>(redirectUrl, method, data, headers, isJsonRet, isArgJson, maxRedirects - 1)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
let responseBody = '';
|
||||
res.on('data', (chunk: string | Buffer) => {
|
||||
responseBody += chunk.toString();
|
||||
@@ -124,7 +109,7 @@ export class RequestUtil {
|
||||
}
|
||||
|
||||
// 请求返回都是原始内容
|
||||
static async HttpGetText (url: string, method: string = 'GET', data?: any, headers: { [key: string]: string; } = {}) {
|
||||
static async HttpGetText (url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
|
||||
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export interface SystemStatus {
|
||||
cpu: {
|
||||
model: string,
|
||||
speed: string;
|
||||
usage: {
|
||||
system: string;
|
||||
qq: string;
|
||||
},
|
||||
core: number;
|
||||
},
|
||||
memory: {
|
||||
total: string;
|
||||
usage: {
|
||||
system: string;
|
||||
qq: string;
|
||||
};
|
||||
},
|
||||
arch: string;
|
||||
}
|
||||
export interface IStatusHelperSubscription {
|
||||
on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
||||
off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
||||
emit (event: 'statusUpdate', status: SystemStatus): boolean;
|
||||
}
|
||||
@@ -19,4 +19,4 @@ class Store {
|
||||
|
||||
const store = new Store();
|
||||
|
||||
export default store;
|
||||
export default store;
|
||||
@@ -1,6 +0,0 @@
|
||||
export type LogListener = (msg: string) => void;
|
||||
export interface ISubscription {
|
||||
subscribe (listener: LogListener): void;
|
||||
unsubscribe (listener: LogListener): void;
|
||||
notify (msg: string): void;
|
||||
}
|
||||
@@ -15,14 +15,3 @@ export type QQVersionConfigType = {
|
||||
export type QQAppidTableType = {
|
||||
[key: string]: { appid: string, qua: string };
|
||||
};
|
||||
export interface Peer {
|
||||
chatType: number; // 聊天类型
|
||||
peerUid: string; // 对等方的唯一标识符
|
||||
guildId?: string; // 可选的频道ID
|
||||
}
|
||||
export interface QQLevel {
|
||||
crownNum: number;
|
||||
sunNum: number;
|
||||
moonNum: number;
|
||||
starNum: number;
|
||||
}
|
||||
|
||||
@@ -1,118 +1 @@
|
||||
// @ts-ignore
|
||||
export const napCatVersion = (typeof import.meta?.env !== 'undefined' && import.meta.env.VITE_NAPCAT_VERSION) || '1.0.0-dev';
|
||||
|
||||
/**
|
||||
* SemVer 2.0 正则表达式
|
||||
* 格式: 主版本号.次版本号.修订号[-先行版本号][+版本编译信息]
|
||||
* 参考: https://semver.org/lang/zh-CN/
|
||||
*/
|
||||
const SEMVER_REGEX = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
||||
|
||||
export interface SemVerInfo {
|
||||
valid: boolean;
|
||||
normalized: string;
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
prerelease: string | null;
|
||||
buildmetadata: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并验证版本号是否符合 SemVer 2.0 规范
|
||||
* @param version - 版本字符串 (支持 v 前缀)
|
||||
* @returns SemVer 解析结果
|
||||
*/
|
||||
export function parseSemVer (version: string | undefined | null): SemVerInfo {
|
||||
if (!version || typeof version !== 'string') {
|
||||
return { valid: false, normalized: '1.0.0-dev', major: 1, minor: 0, patch: 0, prerelease: 'dev', buildmetadata: null };
|
||||
}
|
||||
|
||||
const match = version.trim().match(SEMVER_REGEX);
|
||||
if (match) {
|
||||
const major = parseInt(match[1]!, 10);
|
||||
const minor = parseInt(match[2]!, 10);
|
||||
const patch = parseInt(match[3]!, 10);
|
||||
const prerelease = match[4] || null;
|
||||
const buildmetadata = match[5] || null;
|
||||
|
||||
// 构建标准化版本号(不带 v 前缀)
|
||||
let normalized = `${major}.${minor}.${patch}`;
|
||||
if (prerelease) normalized += `-${prerelease}`;
|
||||
if (buildmetadata) normalized += `+${buildmetadata}`;
|
||||
|
||||
return { valid: true, normalized, major, minor, patch, prerelease, buildmetadata };
|
||||
}
|
||||
return { valid: false, normalized: '1.0.0-dev', major: 1, minor: 0, patch: 0, prerelease: 'dev', buildmetadata: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证版本号是否符合 SemVer 2.0 规范
|
||||
* @param version - 版本字符串
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export function isValidSemVer (version: string | undefined | null): boolean {
|
||||
return parseSemVer(version).valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个 SemVer 版本号
|
||||
* @param v1 - 版本号1
|
||||
* @param v2 - 版本号2
|
||||
* @returns -1 (v1 < v2), 0 (v1 == v2), 1 (v1 > v2)
|
||||
*/
|
||||
export function compareSemVer (v1: string, v2: string): -1 | 0 | 1 {
|
||||
const a = parseSemVer(v1);
|
||||
const b = parseSemVer(v2);
|
||||
|
||||
if (!a.valid || !b.valid) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 比较主版本号
|
||||
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
|
||||
// 比较次版本号
|
||||
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1;
|
||||
// 比较修订号
|
||||
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1;
|
||||
|
||||
// 有先行版本号的版本优先级较低
|
||||
if (a.prerelease && !b.prerelease) return -1;
|
||||
if (!a.prerelease && b.prerelease) return 1;
|
||||
|
||||
// 两者都有先行版本号时,按字典序比较
|
||||
if (a.prerelease && b.prerelease) {
|
||||
const aParts = a.prerelease.split('.');
|
||||
const bParts = b.prerelease.split('.');
|
||||
const len = Math.max(aParts.length, bParts.length);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const aPart = aParts[i];
|
||||
const bPart = bParts[i];
|
||||
|
||||
if (aPart === undefined) return -1;
|
||||
if (bPart === undefined) return 1;
|
||||
|
||||
const aNum = /^\d+$/.test(aPart) ? parseInt(aPart, 10) : NaN;
|
||||
const bNum = /^\d+$/.test(bPart) ? parseInt(bPart, 10) : NaN;
|
||||
|
||||
// 数字 vs 数字
|
||||
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||
if (aNum !== bNum) return aNum > bNum ? 1 : -1;
|
||||
continue;
|
||||
}
|
||||
// 数字优先级低于字符串
|
||||
if (!isNaN(aNum)) return -1;
|
||||
if (!isNaN(bNum)) return 1;
|
||||
// 字符串 vs 字符串
|
||||
if (aPart !== bPart) return aPart > bPart ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解析后的当前版本信息
|
||||
*/
|
||||
export const napCatVersionInfo = parseSemVer(napCatVersion);
|
||||
export const napCatVersion = '4.9.42';
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
],
|
||||
"esModuleInterop": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"rootDir": "src",
|
||||
"noEmit": false,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitAny": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"alwaysStrict": true,
|
||||
@@ -36,8 +36,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"../*"
|
||||
"@/napcat-common/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -5,7 +5,7 @@ export class NodeIDependsAdapter {
|
||||
|
||||
}
|
||||
|
||||
onMSFSsoError (_code: number, _desc: string) {
|
||||
onMSFSsoError (_args: unknown) {
|
||||
|
||||
}
|
||||
|
||||
@@ -24,4 +24,4 @@ export class NodeIDependsAdapter {
|
||||
|
||||
// console.log('[NodeIDependsAdapter] onSendMsfReply', _seq, _cmd, _uk1, _uk2, Buffer.from(_rsp.pbBuffer).toString('hex'));
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
IMAGE_HTTP_HOST_NT,
|
||||
Peer,
|
||||
PicElement,
|
||||
PicSubType,
|
||||
RawMessage,
|
||||
SendFileElement,
|
||||
SendPicElement,
|
||||
SendPttElement,
|
||||
SendVideoElement,
|
||||
} from '@/napcat-core/types';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
@@ -14,9 +19,16 @@ import { InstanceContext, NapCatCore, SearchResultItem } from '@/napcat-core/ind
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import { RkeyManager } from '@/napcat-core/helper/rkey';
|
||||
import { calculateFileMD5 } from 'napcat-common/src/file';
|
||||
import pathLib from 'node:path';
|
||||
import { defaultVideoThumbB64 } from 'napcat-common/src/video';
|
||||
import { encodeSilk } from 'napcat-common/src/audio';
|
||||
import { SendMessageContext } from 'napcat-onebot/api/msg';
|
||||
import { getFileTypeForSendType } from '../helper/msg';
|
||||
import { FFmpegService } from 'napcat-common/src/ffmpeg';
|
||||
import { rkeyDataType } from '../types/file';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { FileId } from '../packet/transformer/proto/misc/fileid';
|
||||
import { imageSizeFallBack } from 'napcat-image-size';
|
||||
|
||||
export class NTQQFileApi {
|
||||
context: InstanceContext;
|
||||
@@ -138,7 +150,7 @@ export class NTQQFileApi {
|
||||
})).urlResult.domainUrl;
|
||||
}
|
||||
|
||||
async uploadFile (filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0, uploadGroupFile = true) {
|
||||
async uploadFile (filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
||||
const fileMd5 = await calculateFileMD5(filePath);
|
||||
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(() => '');
|
||||
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
|
||||
@@ -146,38 +158,188 @@ export class NTQQFileApi {
|
||||
if (fileName.indexOf('.') === -1) {
|
||||
fileName += ext;
|
||||
}
|
||||
|
||||
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
|
||||
md5HexStr: fileMd5,
|
||||
fileName,
|
||||
elementType,
|
||||
elementSubType,
|
||||
thumbSize: 0,
|
||||
needCreate: true,
|
||||
downloadType: 1,
|
||||
file_uuid: '',
|
||||
});
|
||||
|
||||
await this.copyFile(filePath, mediaPath);
|
||||
const fileSize = await this.getFileSize(filePath);
|
||||
if (uploadGroupFile) {
|
||||
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
|
||||
md5HexStr: fileMd5,
|
||||
fileName,
|
||||
elementType,
|
||||
elementSubType,
|
||||
thumbSize: 0,
|
||||
needCreate: true,
|
||||
downloadType: 1,
|
||||
file_uuid: '',
|
||||
});
|
||||
|
||||
await this.copyFile(filePath, mediaPath);
|
||||
|
||||
return {
|
||||
md5: fileMd5,
|
||||
fileName,
|
||||
path: mediaPath,
|
||||
fileSize,
|
||||
ext,
|
||||
};
|
||||
}
|
||||
return {
|
||||
md5: fileMd5,
|
||||
fileName,
|
||||
path: filePath,
|
||||
path: mediaPath,
|
||||
fileSize,
|
||||
ext,
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendFileElement (context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
|
||||
const {
|
||||
fileName: _fileName,
|
||||
path,
|
||||
fileSize,
|
||||
} = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.FILE,
|
||||
elementId: '',
|
||||
fileElement: {
|
||||
fileName: fileName || _fileName,
|
||||
folderId,
|
||||
filePath: path,
|
||||
fileSize: fileSize.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPicElement (context: SendMessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const imageSize = await imageSizeFallBack(picPath);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.PIC,
|
||||
elementId: '',
|
||||
picElement: {
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
picWidth: imageSize.width,
|
||||
picHeight: imageSize.height,
|
||||
fileName,
|
||||
sourcePath: path,
|
||||
original: true,
|
||||
picType: await getFileTypeForSendType(picPath),
|
||||
picSubType: subType,
|
||||
fileUuid: '',
|
||||
fileSubId: '',
|
||||
thumbFileSize: 0,
|
||||
summary,
|
||||
} as PicElement,
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement (context: SendMessageContext, filePath: string, fileName: string = '', _diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
time: 15,
|
||||
format: 'mp4',
|
||||
size: 0,
|
||||
filePath,
|
||||
};
|
||||
let fileExt = 'mp4';
|
||||
try {
|
||||
const tempExt = (await fileTypeFromFile(filePath))?.ext;
|
||||
if (tempExt) fileExt = tempExt;
|
||||
} catch (e) {
|
||||
this.context.logger.logError('获取文件类型失败', e);
|
||||
}
|
||||
const newFilePath = `${filePath}.${fileExt}`;
|
||||
fs.copyFileSync(filePath, newFilePath);
|
||||
context.deleteAfterSentFiles.push(newFilePath);
|
||||
filePath = newFilePath;
|
||||
|
||||
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
|
||||
fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true });
|
||||
const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`);
|
||||
try {
|
||||
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
|
||||
if (!fs.existsSync(thumbPath)) {
|
||||
this.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在'));
|
||||
throw new Error('获取视频缩略图失败');
|
||||
}
|
||||
} catch (e) {
|
||||
this.context.logger.logError('获取视频信息失败', e);
|
||||
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
||||
}
|
||||
if (_diyThumbPath) {
|
||||
try {
|
||||
await this.copyFile(_diyThumbPath, thumbPath);
|
||||
} catch (e) {
|
||||
this.context.logger.logError('复制自定义缩略图失败', e);
|
||||
}
|
||||
}
|
||||
context.deleteAfterSentFiles.push(thumbPath);
|
||||
const thumbSize = (await fsPromises.stat(thumbPath)).size;
|
||||
const thumbMd5 = await calculateFileMD5(thumbPath);
|
||||
context.deleteAfterSentFiles.push(thumbPath);
|
||||
|
||||
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`;
|
||||
return {
|
||||
elementType: ElementType.VIDEO,
|
||||
elementId: '',
|
||||
videoElement: {
|
||||
fileName: uploadName,
|
||||
filePath: path,
|
||||
videoMd5: md5,
|
||||
thumbMd5,
|
||||
fileTime: videoInfo.time,
|
||||
thumbPath: new Map([[0, thumbPath]]),
|
||||
thumbSize,
|
||||
thumbWidth: videoInfo.width,
|
||||
thumbHeight: videoInfo.height,
|
||||
fileSize: fileSize.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPttElement (_context: SendMessageContext, pttPath: string): Promise<SendPttElement> {
|
||||
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
|
||||
if (!silkPath) {
|
||||
throw new Error('语音转换失败, 请检查语音文件是否正常');
|
||||
}
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(silkPath, ElementType.PTT);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (converted) {
|
||||
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e));
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
elementId: '',
|
||||
pttElement: {
|
||||
fileName,
|
||||
filePath: path,
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
duration: duration ?? 1,
|
||||
formatType: 1,
|
||||
voiceType: 1,
|
||||
voiceChangeType: 0,
|
||||
canConvert2Text: true,
|
||||
waveAmplitudes: [
|
||||
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
|
||||
],
|
||||
fileSubId: '',
|
||||
playState: 1,
|
||||
autoConvertText: 0,
|
||||
storeID: 0,
|
||||
otherBusinessInfo: {
|
||||
aiVoiceType: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async downloadFileForModelId (peer: Peer, modelId: string, unknown: string, timeout = 1000 * 60 * 2) {
|
||||
const [, fileTransNotifyInfo] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelRichMediaService/downloadFileForModelId',
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
} from '@/napcat-core/index';
|
||||
import { isNumeric, solveAsyncProblem } from 'napcat-common/src/helper';
|
||||
import { LimitedHashTable } from 'napcat-common/src/message-unique';
|
||||
import { NTEventWrapper } from 'napcat-common/src/event';
|
||||
import { CancelableTask, TaskExecutor } from 'napcat-common/src/cancel-task';
|
||||
import { createGroupDetailInfoV2Param, createGroupExtFilter, createGroupExtInfo } from '../data';
|
||||
import { NTEventWrapper } from '../helper/event';
|
||||
|
||||
export class NTQQGroupApi {
|
||||
context: InstanceContext;
|
||||
@@ -395,7 +395,7 @@ export class NTQQGroupApi {
|
||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||
[groupCode, [uid], forced],
|
||||
(ret) => ret.result === 0,
|
||||
(params: string, _: any, members: Map<string, GroupMember>) => params === GroupCode && members.size > 0 && members.has(uid),
|
||||
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
|
||||
1,
|
||||
forced ? 2500 : 250
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as os from 'os';
|
||||
import offset from '@/napcat-core/external/napi2native.json';
|
||||
import { InstanceContext, NapCatCore } from '@/napcat-core/index';
|
||||
import { LogWrapper } from 'napcat-common/src/log';
|
||||
import { PacketClientSession } from '@/napcat-core/packet/clientSession';
|
||||
import { napCatVersion } from 'napcat-common/src/version';
|
||||
import { LogWrapper } from '../helper/log';
|
||||
|
||||
interface OffsetType {
|
||||
[key: string]: {
|
||||
|
||||
52
packages/napcat-core/external/appid.json
vendored
52
packages/napcat-core/external/appid.json
vendored
@@ -466,57 +466,5 @@
|
||||
"6.9.85-42086": {
|
||||
"appid": 537320237,
|
||||
"qua": "V1_MAC_NQ_6.9.85_42086_GW_B"
|
||||
},
|
||||
"9.9.23-42430": {
|
||||
"appid": 537320212,
|
||||
"qua": "V1_WIN_NQ_9.9.23_42430_GW_B"
|
||||
},
|
||||
"9.9.25-42744": {
|
||||
"appid": 537328470,
|
||||
"qua": "V1_WIN_NQ_9.9.23_42744_GW_B"
|
||||
},
|
||||
"6.9.86-42744": {
|
||||
"appid": 537328495,
|
||||
"qua": "V1_MAC_NQ_6.9.85_42744_GW_B"
|
||||
},
|
||||
"9.9.25-42905": {
|
||||
"appid": 537328521,
|
||||
"qua": "V1_WIN_NQ_9.9.25_42905_GW_B"
|
||||
},
|
||||
"6.9.86-42905": {
|
||||
"appid": 537328546,
|
||||
"qua": "V1_MAC_NQ_6.9.86_42905_GW_B"
|
||||
},
|
||||
"3.2.22-42941": {
|
||||
"appid": 537328659,
|
||||
"qua": "V1_LNX_NQ_3.2.22_42941_GW_B"
|
||||
},
|
||||
"9.9.25-42941": {
|
||||
"appid": 537328623,
|
||||
"qua": "V1_WIN_NQ_9.9.25_42941_GW_B"
|
||||
},
|
||||
"6.9.86-42941": {
|
||||
"appid": 537328648,
|
||||
"qua": "V1_MAC_NQ_6.9.86_42941_GW_B"
|
||||
},
|
||||
"9.9.26-44175": {
|
||||
"appid": 537336450,
|
||||
"qua": "V1_WIN_NQ_9.9.26_44175_GW_B"
|
||||
},
|
||||
"9.9.26-44343": {
|
||||
"appid": 537336603,
|
||||
"qua": "V1_WIN_NQ_9.9.26_44343_GW_B"
|
||||
},
|
||||
"3.2.23-44343": {
|
||||
"appid": 537336639,
|
||||
"qua": "V1_LNX_NQ_3.2.23_44343_GW_B"
|
||||
},
|
||||
"9.9.26-44498": {
|
||||
"appid": 537337416,
|
||||
"qua": "V1_WIN_NQ_9.9.26_44498_GW_B"
|
||||
},
|
||||
"9.9.26-44725": {
|
||||
"appid": 537337569,
|
||||
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B"
|
||||
}
|
||||
}
|
||||
64
packages/napcat-core/external/napi2native.json
vendored
64
packages/napcat-core/external/napi2native.json
vendored
@@ -87,72 +87,8 @@
|
||||
"send": "23B0330",
|
||||
"recv": "0957648"
|
||||
},
|
||||
"3.2.21-42086-arm64": {
|
||||
"send": "3D6D98C",
|
||||
"recv": "14797C8"
|
||||
},
|
||||
"3.2.21-42086-x64": {
|
||||
"send": "5B42CF0",
|
||||
"recv": "2FDA6F0"
|
||||
},
|
||||
"9.9.23-42430-x64": {
|
||||
"send": "0A01A34",
|
||||
"recv": "1D1CFF9"
|
||||
},
|
||||
"9.9.25-42744-x64": {
|
||||
"send": "0A0D104",
|
||||
"recv": "1D3E7F9"
|
||||
},
|
||||
"6.9.85-42744-arm64": {
|
||||
"send": "23DFEF0",
|
||||
"recv": "095FD80"
|
||||
},
|
||||
"9.9.25-42905-x64": {
|
||||
"send": "0A12E74",
|
||||
"recv": "1D450FD"
|
||||
},
|
||||
"6.9.86-42905-arm64": {
|
||||
"send": "2342408",
|
||||
"recv": "09639B8"
|
||||
},
|
||||
"3.2.22-42941-x64": {
|
||||
"send": "5BC1630",
|
||||
"recv": "3011E00"
|
||||
},
|
||||
"3.2.22-42941-arm64": {
|
||||
"send": "3DC90AC",
|
||||
"recv": "1497A70"
|
||||
},
|
||||
"9.9.25-42941-x64": {
|
||||
"send": "0A131D4",
|
||||
"recv": "1D4547D"
|
||||
},
|
||||
"6.9.86-42941-arm64": {
|
||||
"send": "2346108",
|
||||
"recv": "09675F0"
|
||||
},
|
||||
"9.9.26-44175-x64": {
|
||||
"send": "0A0F2EC",
|
||||
"recv": "1D3AD4D"
|
||||
},
|
||||
"9.9.26-44343-x64": {
|
||||
"send": "0A0F7BC",
|
||||
"recv": "1D3C3CD"
|
||||
},
|
||||
"3.2.23-44343-arm64": {
|
||||
"send": "3C867DC",
|
||||
"recv": "1404938"
|
||||
},
|
||||
"3.2.23-44343-x64": {
|
||||
"send": "59A27B0",
|
||||
"recv": "2FFBE90"
|
||||
},
|
||||
"9.9.26-44498-x64": {
|
||||
"send": "0A1051C",
|
||||
"recv": "1D3BC0D"
|
||||
},
|
||||
"9.9.26-44725-x64": {
|
||||
"send": "0A18D0C",
|
||||
"recv": "1D4BF0D"
|
||||
}
|
||||
}
|
||||
60
packages/napcat-core/external/packet.json
vendored
60
packages/napcat-core/external/packet.json
vendored
@@ -602,65 +602,5 @@
|
||||
"3.2.21-42086-arm64": {
|
||||
"send": "6B13038",
|
||||
"recv": "6B169C8"
|
||||
},
|
||||
"9.9.23-42430-x64": {
|
||||
"send": "2C9A4A0",
|
||||
"recv": "2C9DA20"
|
||||
},
|
||||
"9.9.25-42744-x64": {
|
||||
"send": "2CD8E40",
|
||||
"recv": "2CDC3C0"
|
||||
},
|
||||
"6.9.86-42744-arm64": {
|
||||
"send": "3DCC840",
|
||||
"recv": "3DCF150"
|
||||
},
|
||||
"9.9.25-42905-x64": {
|
||||
"send": "2CE46A0",
|
||||
"recv": "2CE7C20"
|
||||
},
|
||||
"6.9.86-42905-arm64": {
|
||||
"send": "3DD6098",
|
||||
"recv": "3DD89A8"
|
||||
},
|
||||
"3.2.22-42941-x64": {
|
||||
"send": "A8AD8A0",
|
||||
"recv": "A8B1320"
|
||||
},
|
||||
"9.9.25-42941-x64": {
|
||||
"send": "2CE4DA0",
|
||||
"recv": "2CE8320"
|
||||
},
|
||||
"3.2.22-42941-arm64": {
|
||||
"send": "6BC95E8",
|
||||
"recv": "6BCCF78"
|
||||
},
|
||||
"6.9.86-42941-arm64": {
|
||||
"send": "3DDDAD0",
|
||||
"recv": "3DE03E0"
|
||||
},
|
||||
"9.9.26-44175-x64": {
|
||||
"send": "2CD84A0",
|
||||
"recv": "2CDBA20"
|
||||
},
|
||||
"3.2.23-44343-x64": {
|
||||
"send": "A46F140",
|
||||
"recv": "A472BE0"
|
||||
},
|
||||
"9.9.26-44343-x64": {
|
||||
"send": "2CD8EE0",
|
||||
"recv": "2CDC460"
|
||||
},
|
||||
"3.2.23-44343-arm64": {
|
||||
"send": "6926F60",
|
||||
"recv": "692A910"
|
||||
},
|
||||
"9.9.26-44498-x64": {
|
||||
"send": "2CDAE40",
|
||||
"recv": "2CDE3C0"
|
||||
},
|
||||
"9.9.26-44725-x64": {
|
||||
"send": "2CEBB20",
|
||||
"recv": "2CEF0A0"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// TODO: further refactor in NapCat.Packet v2
|
||||
import { NapProtoMsg, ProtoField, ScalarType } from 'napcat-protobuf';
|
||||
import { NapProtoMsg, ProtoField, ScalarType } from '@napneko/nap-proto-core';
|
||||
|
||||
const LikeDetail = {
|
||||
txt: ProtoField(1, ScalarType.STRING),
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import fsPromise from 'fs/promises';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
||||
|
||||
async function guessDuration (pttPath: string, logger: LogWrapper) {
|
||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
|
||||
logger.log('通过文件大小估算语音的时长:', duration);
|
||||
return duration;
|
||||
}
|
||||
|
||||
export async function encodeSilk (filePath: string, TEMP_DIR: string, logger: LogWrapper) {
|
||||
try {
|
||||
const pttPath = path.join(TEMP_DIR, randomUUID());
|
||||
if (!(await FFmpegService.isSilk(filePath))) {
|
||||
logger.log(`语音文件${filePath}需要转换成silk`);
|
||||
await FFmpegService.convertToNTSilkTct(filePath, pttPath);
|
||||
const duration = await FFmpegService.getDuration(filePath);
|
||||
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', duration);
|
||||
return {
|
||||
converted: true,
|
||||
path: pttPath,
|
||||
duration: duration,
|
||||
};
|
||||
} else {
|
||||
let duration = 0;
|
||||
try {
|
||||
duration = await FFmpegService.getDuration(filePath);
|
||||
} catch (e: unknown) {
|
||||
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack);
|
||||
duration = await guessDuration(filePath, logger);
|
||||
}
|
||||
return {
|
||||
converted: false,
|
||||
path: filePath,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
logger.logError('convert silk failed', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConfigBase } from '@/napcat-core/helper/config-base';
|
||||
import { ConfigBase } from 'napcat-common/src/config-base';
|
||||
import { NapCatCore } from '@/napcat-core/index';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { AnySchema } from 'ajv';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LogWrapper } from 'napcat-common/src/log';
|
||||
import { RequestUtil } from 'napcat-common/src/request';
|
||||
import { LogWrapper } from './log';
|
||||
|
||||
interface ServerRkeyData {
|
||||
group_rkey: string;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import os from 'node:os';
|
||||
import EventEmitter from 'node:events';
|
||||
import { IStatusHelperSubscription } from 'napcat-common/src/status-interface';
|
||||
|
||||
export interface SystemStatus {
|
||||
cpu: {
|
||||
model: string,
|
||||
speed: string;
|
||||
speed: string
|
||||
usage: {
|
||||
system: string;
|
||||
qq: string;
|
||||
system: string
|
||||
qq: string
|
||||
},
|
||||
core: number;
|
||||
core: number
|
||||
},
|
||||
memory: {
|
||||
total: string;
|
||||
total: string
|
||||
usage: {
|
||||
system: string;
|
||||
qq: string;
|
||||
};
|
||||
system: string
|
||||
qq: string
|
||||
}
|
||||
},
|
||||
arch: string;
|
||||
arch: string
|
||||
}
|
||||
|
||||
export class StatusHelper {
|
||||
@@ -101,7 +101,7 @@ export class StatusHelper {
|
||||
}
|
||||
}
|
||||
|
||||
class StatusHelperSubscription extends EventEmitter implements IStatusHelperSubscription {
|
||||
class StatusHelperSubscription extends EventEmitter {
|
||||
private statusHelper: StatusHelper;
|
||||
private interval: NodeJS.Timeout | null = null;
|
||||
|
||||
|
||||
@@ -16,23 +16,21 @@ import {
|
||||
WrapperNodeApi,
|
||||
WrapperSessionInitConfig,
|
||||
} from '@/napcat-core/wrapper';
|
||||
import { LogLevel, LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
|
||||
import { LogLevel, LogWrapper } from 'napcat-common/src/log';
|
||||
import { NodeIKernelLoginService } from '@/napcat-core/services';
|
||||
import { QQBasicInfoWrapper } from 'napcat-common/src/qq-basic-info';
|
||||
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { hostname, systemName, systemVersion } from 'napcat-common/src/system';
|
||||
import { NTEventWrapper } from '@/napcat-core/helper/event';
|
||||
import { NTEventWrapper } from 'napcat-common/src/event';
|
||||
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
|
||||
import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config';
|
||||
import os from 'node:os';
|
||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners';
|
||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||
import { proxiedListenerOf } from 'napcat-common/src/proxy-handler';
|
||||
import { NTQQPacketApi } from './apis/packet';
|
||||
import { NativePacketHandler } from './packet/handler/client';
|
||||
import { container, ReceiverServiceRegistry } from './packet/handler/serviceRegister';
|
||||
import { appEvent } from './packet/handler/eventList';
|
||||
import { TypedEventEmitter } from './packet/handler/typeEvent';
|
||||
export * from './wrapper';
|
||||
export * from './types/index';
|
||||
export * from './services/index';
|
||||
@@ -94,7 +92,6 @@ export function getMajorPath (QQVersion: string): string {
|
||||
export class NapCatCore {
|
||||
readonly context: InstanceContext;
|
||||
readonly eventWrapper: NTEventWrapper;
|
||||
event = appEvent;
|
||||
NapCatDataPath: string = '';
|
||||
NapCatTempPath: string = '';
|
||||
apis: StableNTApiWrapper;
|
||||
@@ -121,16 +118,6 @@ export class NapCatCore {
|
||||
UserApi: new NTQQUserApi(this.context, this),
|
||||
GroupApi: new NTQQGroupApi(this.context, this),
|
||||
};
|
||||
container.bind(NapCatCore).toConstantValue(this);
|
||||
container.bind(TypedEventEmitter).toConstantValue(this.event);
|
||||
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
|
||||
container.bind(ServiceClass).toSelf();
|
||||
// console.log(`Registering service handler for: ${serviceName}`);
|
||||
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
|
||||
const serviceInstance = container.get(ServiceClass);
|
||||
return serviceInstance.handler(seq, hex_data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async initCore () {
|
||||
@@ -177,10 +164,8 @@ export class NapCatCore {
|
||||
|
||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||
// 下线通知
|
||||
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
|
||||
this.context.logger.logError(tips);
|
||||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||||
this.selfInfo.online = false;
|
||||
this.event.emit('KickedOffLine', tips);
|
||||
};
|
||||
msgListener.onRecvMsg = (msgs) => {
|
||||
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
|
||||
@@ -279,6 +264,7 @@ export interface InstanceContext {
|
||||
readonly wrapper: WrapperNodeApi;
|
||||
readonly session: NodeIQQNTWrapperSession;
|
||||
readonly logger: LogWrapper;
|
||||
readonly loginService: NodeIKernelLoginService;
|
||||
readonly basicInfoWrapper: QQBasicInfoWrapper;
|
||||
readonly pathWrapper: NapCatPathWrapper;
|
||||
readonly packetHandler: NativePacketHandler;
|
||||
|
||||
@@ -53,8 +53,6 @@ export class NodeIKernelLoginListener {
|
||||
|
||||
onLoginState (..._args: any[]): any {
|
||||
}
|
||||
onLoginRecordUpdate (..._args: any[]): any {
|
||||
}
|
||||
}
|
||||
|
||||
export interface QRCodeLoginSucceedResult {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChatType, KickedOffLineInfo, RawMessage } from '@/napcat-core/types';
|
||||
import { CommonFileInfo } from '@/napcat-core/index';
|
||||
import { CommonFileInfo } from '@/napcat-core';
|
||||
|
||||
export interface OnRichMediaDownloadCompleteParams {
|
||||
fileModelId: string,
|
||||
|
||||
@@ -3,7 +3,7 @@ export class NodeIKernelStorageCleanListener {
|
||||
|
||||
}
|
||||
|
||||
onScanCacheProgressChanged (_current_progress: number, _total_progress: number): any {
|
||||
onScanCacheProgressChanged (_args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export class NodeIKernelStorageCleanListener {
|
||||
|
||||
}
|
||||
|
||||
onFinishScan (_sizes: Array<`${number}`>): any {
|
||||
onFinishScan (_args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.ts"
|
||||
@@ -16,18 +13,14 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"winston": "^3.17.0",
|
||||
"json5": "^2.2.3",
|
||||
"inversify": "^7.10.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"@protobuf-ts/runtime": "^2.11.1",
|
||||
"@napneko/nap-proto-core": "^0.0.4",
|
||||
"ajv": "^8.13.0",
|
||||
"@sinclair/typebox": "^0.34.38",
|
||||
"file-type": "^21.0.0",
|
||||
"compressing": "^1.10.1",
|
||||
"napcat-image-size": "workspace:*",
|
||||
"napcat-common": "workspace:*",
|
||||
"napcat-protobuf": "workspace:*"
|
||||
"napcat-onebot": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LogLevel, LogWrapper } from 'napcat-common/src/log';
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { LogWrapper, LogLevel } from '@/napcat-core/helper/log';
|
||||
|
||||
// TODO: check bind?
|
||||
export class PacketLogger {
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
PacketMsgReplyElement,
|
||||
PacketMsgVideoElement,
|
||||
} from '@/napcat-core/packet/message/element';
|
||||
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/napcat-core/index';
|
||||
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/napcat-core';
|
||||
import { MiniAppRawData, MiniAppReqParams } from '@/napcat-core/packet/entities/miniApp';
|
||||
import { AIVoiceChatType } from '@/napcat-core/packet/entities/aiChat';
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { IndexNode, LongMsgResult, MsgInfo, PushMsgBody } from '@/napcat-core/packet/transformer/proto';
|
||||
import { OidbPacket } from '@/napcat-core/packet/transformer/base';
|
||||
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PacketHighwayContext } from '@/napcat-core/packet/highway/highwayContext';
|
||||
import { NapCatCore } from '@/napcat-core/index';
|
||||
import { NapCatCore } from '@/napcat-core';
|
||||
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { PacketClientContext } from '@/napcat-core/packet/context/clientContext';
|
||||
|
||||
@@ -2,8 +2,8 @@ import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import { constants } from 'node:os';
|
||||
import { LogWrapper } from 'napcat-common/src/log';
|
||||
import offset from '@/napcat-core/external/packet.json';
|
||||
import { LogWrapper } from '../../helper/log';
|
||||
interface OffsetType {
|
||||
[key: string]: {
|
||||
recv: string;
|
||||
@@ -50,6 +50,7 @@ export class NativePacketHandler {
|
||||
this.logger.logError('NativePacketClient 加载出错:', error);
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { TypedEventEmitter } from './typeEvent';
|
||||
|
||||
export interface AppEvents {
|
||||
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number; };
|
||||
KickedOffLine: string;
|
||||
}
|
||||
export const appEvent = new TypedEventEmitter<AppEvents>();
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'reflect-metadata';
|
||||
import { Container, injectable } from 'inversify';
|
||||
import { NapCatCore } from '../..';
|
||||
import { TypedEventEmitter } from './typeEvent';
|
||||
|
||||
export const container = new Container();
|
||||
|
||||
export const ReceiverServiceRegistry = new Map<string, new (...args: any[]) => ServiceBase>();
|
||||
|
||||
export abstract class ServiceBase {
|
||||
get core (): NapCatCore {
|
||||
return container.get(NapCatCore);
|
||||
}
|
||||
|
||||
get event () {
|
||||
return container.get(TypedEventEmitter);
|
||||
}
|
||||
|
||||
abstract handler (seq: number, hex_data: string): Promise<void> | void;
|
||||
}
|
||||
|
||||
export function ReceiveService (serviceName: string) {
|
||||
return function <T extends new (...args: any[]) => ServiceBase>(constructor: T) {
|
||||
injectable()(constructor);
|
||||
ReceiverServiceRegistry.set(serviceName, constructor);
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
export class TypedEventEmitter<E extends Record<string, any>> {
|
||||
private emitter = new EventEmitter();
|
||||
|
||||
on<K extends keyof E>(event: K, listener: (payload: E[K]) => void) {
|
||||
this.emitter.on(event as string, listener);
|
||||
return () => this.off(event, listener);
|
||||
}
|
||||
|
||||
once<K extends keyof E>(event: K, listener: (payload: E[K]) => void) {
|
||||
this.emitter.once(event as string, listener);
|
||||
}
|
||||
|
||||
off<K extends keyof E>(event: K, listener: (payload: E[K]) => void) {
|
||||
this.emitter.off(event as string, listener);
|
||||
}
|
||||
|
||||
emit<K extends keyof E>(event: K, payload: E[K]) {
|
||||
this.emitter.emit(event as string, payload);
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,12 @@ import {
|
||||
import { ChatType, Peer } from '@/napcat-core/index';
|
||||
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from '@/napcat-core/packet/utils/crypto/hash';
|
||||
import UploadGroupImage from '@/napcat-core/packet/transformer/highway/UploadGroupImage';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import * as trans from '@/napcat-core/packet/transformer';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { PacketClientContext } from '@/napcat-core/packet/context/clientContext';
|
||||
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
||||
import { defaultVideoThumbB64 } from '@/napcat-core/helper/ffmpeg/video';
|
||||
import { calculateFileMD5 } from 'napcat-common/src/file';
|
||||
|
||||
export const BlockSize = 1024 * 1024;
|
||||
|
||||
@@ -109,89 +105,13 @@ export class PacketHighwayContext {
|
||||
if (+(video.fileSize ?? 0) > 1024 * 1024 * 100) {
|
||||
throw new Error(`[Highway] 视频文件过大: ${(+(video.fileSize ?? 0) / (1024 * 1024)).toFixed(2)} MB > 100 MB,请使用文件上传!`);
|
||||
}
|
||||
|
||||
// 如果缺少视频缩略图,自动生成一个
|
||||
let tempThumbPath: string | null = null;
|
||||
let thumbExists = false;
|
||||
if (video.thumbPath) {
|
||||
try {
|
||||
await fs.promises.access(video.thumbPath, fs.constants.F_OK);
|
||||
thumbExists = true;
|
||||
} catch {
|
||||
thumbExists = false;
|
||||
}
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
await this.uploadGroupVideo(+peer.peerUid, video);
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
await this.uploadC2CVideo(peer.peerUid, video);
|
||||
} else {
|
||||
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
||||
}
|
||||
if (!video.thumbPath || !thumbExists) {
|
||||
tempThumbPath = await this.ensureVideoThumb(video);
|
||||
}
|
||||
|
||||
try {
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
await this.uploadGroupVideo(+peer.peerUid, video);
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
await this.uploadC2CVideo(peer.peerUid, video);
|
||||
} else {
|
||||
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
||||
}
|
||||
} finally {
|
||||
// 清理临时生成的缩略图文件
|
||||
if (tempThumbPath) {
|
||||
const thumbToClean = tempThumbPath;
|
||||
fs.promises.unlink(thumbToClean)
|
||||
.then(() => this.logger.debug(`[Highway] Cleaned up temp thumbnail: ${thumbToClean}`))
|
||||
.catch((e) => {
|
||||
// 文件不存在时忽略错误
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
this.logger.warn(`[Highway] Failed to clean up temp thumbnail: ${thumbToClean}, reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保视频缩略图存在,如果不存在则自动生成
|
||||
* @returns 生成的临时缩略图路径,用于后续清理
|
||||
*/
|
||||
private async ensureVideoThumb (video: PacketMsgVideoElement): Promise<string> {
|
||||
if (!video.filePath) {
|
||||
throw new Error('video.filePath is empty, cannot generate thumbnail');
|
||||
}
|
||||
|
||||
// 生成缩略图路径
|
||||
const videoDir = path.dirname(video.filePath);
|
||||
const videoBasename = path.basename(video.filePath, path.extname(video.filePath));
|
||||
const thumbPath = path.join(videoDir, `${videoBasename}_thumb.png`);
|
||||
|
||||
this.logger.debug(`[Highway] Video thumb missing, generating at: ${thumbPath}`);
|
||||
|
||||
try {
|
||||
// 尝试使用 FFmpeg 提取视频缩略图
|
||||
await FFmpegService.extractThumbnail(video.filePath, thumbPath);
|
||||
try {
|
||||
await fs.promises.access(thumbPath, fs.constants.F_OK);
|
||||
this.logger.debug('[Highway] Video thumbnail generated successfully using FFmpeg');
|
||||
} catch {
|
||||
throw new Error('FFmpeg failed to generate thumbnail');
|
||||
}
|
||||
} catch (e) {
|
||||
// FFmpeg 失败时(包括未初始化的情况)使用默认缩略图
|
||||
this.logger.warn(`[Highway] Failed to extract thumbnail, using default. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
await fs.promises.writeFile(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
||||
}
|
||||
|
||||
// 更新视频元素的缩略图信息
|
||||
video.thumbPath = thumbPath;
|
||||
const thumbStat = await fs.promises.stat(thumbPath);
|
||||
video.thumbSize = thumbStat.size;
|
||||
video.thumbMd5 = await calculateFileMD5(thumbPath);
|
||||
// 默认缩略图尺寸(与 defaultVideoThumbB64 匹配的尺寸)
|
||||
if (!video.thumbWidth) video.thumbWidth = 240;
|
||||
if (!video.thumbHeight) video.thumbHeight = 383;
|
||||
|
||||
this.logger.debug(`[Highway] Video thumb info set: path=${thumbPath}, size=${video.thumbSize}, md5=${video.thumbMd5}`);
|
||||
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
async uploadPtt (peer: Peer, ptt: PacketMsgPttElement): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'node:crypto';
|
||||
import http from 'node:http';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { IHighwayUploader } from '@/napcat-core/packet/highway/uploader/highwayUploader';
|
||||
import { Frame } from '@/napcat-core/packet/highway/frame';
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import net from 'node:net';
|
||||
import stream from 'node:stream';
|
||||
import crypto from 'node:crypto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { BlockSize } from '@/napcat-core/packet/highway/highwayContext';
|
||||
import { Frame } from '@/napcat-core/packet/highway/frame';
|
||||
import { IHighwayUploader } from '@/napcat-core/packet/highway/uploader/highwayUploader';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// import * as tea from '@/napcat-core/packet/utils/crypto/tea';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { PacketHighwayTrans } from '@/napcat-core/packet/highway/client';
|
||||
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NapProtoEncodeStructType } from 'napcat-protobuf';
|
||||
import { NapProtoEncodeStructType } from '@napneko/nap-proto-core';
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
|
||||
export const int32ip2str = (ip: number) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { PushMsgBody } from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoEncodeStructType } from 'napcat-protobuf';
|
||||
import { NapProtoEncodeStructType } from '@napneko/nap-proto-core';
|
||||
import { PacketMsg, PacketSendMsgElement } from '@/napcat-core/packet/message/message';
|
||||
import { IPacketMsgElement, PacketMsgTextElement } from '@/napcat-core/packet/message/element';
|
||||
import { SendTextElement } from '@/napcat-core/index';
|
||||
@@ -14,7 +14,7 @@ export class PacketMsgBuilder {
|
||||
|
||||
buildFakeMsg (selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
|
||||
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
|
||||
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=0&img_type=jpg`;
|
||||
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`;
|
||||
const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => {
|
||||
return acc ?? msg.buildContent();
|
||||
}, undefined);
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
PacketMultiMsgElement,
|
||||
} from '@/napcat-core/packet/message/element';
|
||||
import { PacketMsg, PacketSendMsgElement } from '@/napcat-core/packet/message/message';
|
||||
import { NapProtoDecodeStructType } from 'napcat-protobuf';
|
||||
import { NapProtoDecodeStructType } from '@napneko/nap-proto-core';
|
||||
import { Elem } from '@/napcat-core/packet/transformer/proto';
|
||||
|
||||
const SupportedElementTypes = [
|
||||
@@ -132,6 +132,7 @@ export class PacketMsgConverter {
|
||||
time: msg.time,
|
||||
msg: msg.msg.map((element) => {
|
||||
if (!this.isValidElementType(element.elementType)) return null;
|
||||
// @ts-ignore
|
||||
return this.rawToPacketMsgConverters[element.elementType](element as MessageElement);
|
||||
}).filter((e) => e !== null),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as zlib from 'node:zlib';
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import {
|
||||
CustomFace,
|
||||
Elem,
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
SendTextElement,
|
||||
SendVideoElement,
|
||||
Peer,
|
||||
} from '@/napcat-core/index';
|
||||
import { ForwardMsgBuilder } from '@/napcat-core/helper/forward-msg-builder';
|
||||
} from '@/napcat-core';
|
||||
import { ForwardMsgBuilder } from 'napcat-common/src/forward-msg-builder';
|
||||
import { PacketMsg, PacketSendMsgElement } from '@/napcat-core/packet/message/message';
|
||||
|
||||
export type ParseElementFnR = [MessageElement, NapProtoDecodeStructType<typeof Elem> | null] | undefined;
|
||||
@@ -510,15 +510,15 @@ export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
|
||||
}
|
||||
|
||||
override buildElement (): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
//return [];
|
||||
if (!this.msgInfo) return [];
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 48,
|
||||
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
|
||||
businessType: 22,
|
||||
}
|
||||
}];
|
||||
return [];
|
||||
// if (!this.msgInfo) return [];
|
||||
// return [{
|
||||
// commonElem: {
|
||||
// serviceType: 48,
|
||||
// pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
|
||||
// businessType: 22,
|
||||
// }
|
||||
// }];
|
||||
}
|
||||
|
||||
override toPreview (): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IPacketMsgElement } from '@/napcat-core/packet/message/element';
|
||||
import { SendMessageElement, SendMultiForwardMsgElement } from '@/napcat-core/index';
|
||||
import { SendMessageElement, SendMultiForwardMsgElement } from '@/napcat-core';
|
||||
|
||||
export type PacketSendMsgElement = SendMessageElement | SendMultiForwardMsgElement;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
import { AIVoiceChatType } from '@/napcat-core/packet/entities/aiChat';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
import { AIVoiceChatType } from '@/napcat-core/packet/entities/aiChat';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import { MiniAppReqParams } from '@/napcat-core/packet/entities/miniApp';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NapProtoDecodeStructType } from 'napcat-protobuf';
|
||||
import { NapProtoDecodeStructType } from '@napneko/nap-proto-core';
|
||||
import { PacketMsgBuilder } from '@/napcat-core/packet/message/builder';
|
||||
|
||||
export type PacketBuf = Buffer & { readonly hexNya: unique symbol; };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||
import OidbBase from '@/napcat-core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user