Compare commits

..

115 Commits

Author SHA1 Message Date
手瓜一十雪
bf0c12f1c4 fix: #605 2024-12-06 10:39:49 +08:00
Mlikiowa
cb5eeecb86 release: v4.2.25 2024-12-05 13:13:34 +00:00
手瓜一十雪
8d857cf2be Revert "refactor: CardChangedEvent"
This reverts commit 0e8ceeb6c9.
2024-12-05 21:13:03 +08:00
手瓜一十雪
6f232c465f Merge pull request #604 from Ander-pixe/webui-new
feat:实时日志、关于and部分样式优化
2024-12-05 21:10:08 +08:00
纸凤孤凰
032d444246 Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:27:08 +08:00
纸凤孤凰
49488dd3fb Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:25:14 +08:00
手瓜一十雪
9aec3865ff fix: historyLog 2024-12-05 20:22:55 +08:00
手瓜一十雪
b6b7f2051b fix 2024-12-05 20:02:24 +08:00
手瓜一十雪
46254a699a fix 2024-12-05 19:56:36 +08:00
手瓜一十雪
7b3c287137 fix: cors 2024-12-05 19:37:56 +08:00
手瓜一十雪
1a533742a5 fix: Log 2024-12-05 19:17:07 +08:00
手瓜一十雪
2027266852 fix 2024-12-05 18:56:55 +08:00
手瓜一十雪
946d8b1a7b fix: dependencies error 2024-12-05 18:49:45 +08:00
手瓜一十雪
6d2fb5de6f Merge branch 'main' into pr/604 2024-12-05 18:40:46 +08:00
pk5ls20
91c4a002dd fix: dependencies 2024-12-05 18:15:00 +08:00
纸凤孤凰
4d8112aae5 feat:实时日志、关于and部分样式优化 2024-12-05 15:31:42 +08:00
手瓜一十雪
bb53f245cf style: lint check 2024-12-05 14:50:27 +08:00
手瓜一十雪
9f31cdbf5b fix: 移除不使用api 2024-12-05 14:46:05 +08:00
手瓜一十雪
9a33039d73 fix: 暂时移除 QunAlbum 2024-12-05 14:45:12 +08:00
手瓜一十雪
7cf3be8333 refactor: predict time 2024-12-05 14:42:45 +08:00
Nanako
82afb88e53 Update README.md 2024-12-05 14:37:10 +08:00
Mlikiowa
4aa24b5d67 release: v4.2.24 2024-12-05 06:17:46 +00:00
手瓜一十雪
57112c21a2 refactor: flag handle&onebot标准化 2024-12-05 14:17:09 +08:00
手瓜一十雪
0e8ceeb6c9 refactor: CardChangedEvent 2024-12-05 11:36:06 +08:00
手瓜一十雪
f52b8d1f04 feat: 30366 全平台通用性适配 2024-12-05 11:15:10 +08:00
手瓜一十雪
f374cc77ae chore: readme 2024-12-04 23:04:37 +08:00
手瓜一十雪
7c694e7fae chore: 跑路的规范/困难的实现 2024-12-04 23:03:46 +08:00
Mlikiowa
932ffc2673 release: v4.2.23 2024-12-04 14:18:02 +00:00
手瓜一十雪
3de5438139 fix: poke report 2024-12-04 22:16:59 +08:00
手瓜一十雪
c4b5f34271 fix: 兜底 防止进入影响速度 2024-12-04 22:02:11 +08:00
手瓜一十雪
22d3ac33a2 Refactor: 更新群组通知处理逻辑,优化数据结构和异步处理 2024-12-04 21:59:53 +08:00
Mlikiowa
2e5dd6535a release: v4.2.22 2024-12-04 13:18:58 +00:00
手瓜一十雪
eac58a2a50 fix: 9.9.17-30366 2024-12-04 21:04:24 +08:00
手瓜一十雪
e939ec0e52 fix: #597 2024-12-04 20:37:08 +08:00
Mlikiowa
5b17a14a2a release: v4.2.21 2024-12-04 11:49:26 +00:00
手瓜一十雪
8fb8c888f5 refactor: 移除未使用的uidCache和uinCache逻辑 2024-12-04 19:48:59 +08:00
Mlikiowa
4a2884509e release: v4.2.20 2024-12-04 11:46:12 +00:00
手瓜一十雪
e295235a89 fix: #596 2024-12-04 19:45:46 +08:00
手瓜一十雪
ef515a38d0 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-04 18:50:55 +08:00
手瓜一十雪
02cff040e3 refactor: 移除未使用的createUidFromTinyId和getStatusByUid方法 2024-12-04 18:50:51 +08:00
Mlikiowa
bb0f65a52d release: v4.2.19 2024-12-04 10:42:32 +00:00
手瓜一十雪
d51d6a5cc1 refactor: getUidByUinV2/getUinByUidV2 2024-12-04 18:41:28 +08:00
手瓜一十雪
eb99379a79 fix: 性能优化 2024-12-04 18:29:33 +08:00
Mlikiowa
388eb57d0d release: v4.2.18 2024-12-04 03:40:59 +00:00
手瓜一十雪
0b8131392a chore: 移出调试 2024-12-04 11:40:36 +08:00
手瓜一十雪
229efbd006 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-04 11:39:09 +08:00
手瓜一十雪
a482fa3a8d refactor: 优化调整文件处理 2024-12-04 11:38:59 +08:00
Mlikiowa
6cf047af39 release: v4.2.17 2024-12-04 02:57:21 +00:00
手瓜一十雪
41748c0b3f refactor: 数据清理与刷新 2024-12-04 10:55:56 +08:00
手瓜一十雪
1ce8be3c7e fix: 精简GroupApi实现 2024-12-04 10:47:21 +08:00
手瓜一十雪
32778acf57 refactor: NTQQGroupApi 2024-12-04 10:43:48 +08:00
Mlikiowa
a3c71473ae release: v4.2.16 2024-12-03 14:15:16 +00:00
手瓜一十雪
aceece7e90 fix: code 2024-12-03 22:12:57 +08:00
Mlikiowa
52efb4f9ef release: v4.2.15 2024-12-03 14:07:35 +00:00
手瓜一十雪
6b0d96fe8d fix: 移除复杂信息 2024-12-03 22:07:11 +08:00
Mlikiowa
ad052821b0 release: v4.2.14 2024-12-03 13:50:37 +00:00
手瓜一十雪
da7636e60c fix: #592 2024-12-03 21:50:12 +08:00
Mlikiowa
ef01dd0d77 release: v4.2.13 2024-12-03 13:43:37 +00:00
手瓜一十雪
03f7d4673f fix: code 2024-12-03 21:42:08 +08:00
手瓜一十雪
94e9c87978 fix: 优化处理 2024-12-03 21:42:08 +08:00
手瓜一十雪
501bbbe4df fix 2024-12-03 21:42:08 +08:00
手瓜一十雪
c9122a3fee fix: 临时的抽象方案 2024-12-03 21:42:08 +08:00
手瓜一十雪
8a289d014e fix: error 2024-12-03 21:42:08 +08:00
手瓜一十雪
ddadd38151 refactor: GroupAdminChange 2024-12-03 21:42:08 +08:00
手瓜一十雪
0b8d0e3cac feat: 迁移事件解析原理 2024-12-03 21:42:08 +08:00
手瓜一十雪
eeb27d38bc fix: 清理旧的脚本 2024-12-03 17:33:13 +08:00
Mlikiowa
491a79ec96 release: v4.2.12 2024-12-03 09:22:43 +00:00
手瓜一十雪
f429db61af fix: #594 2024-12-03 17:16:58 +08:00
Mlikiowa
2881099602 release: v4.2.11 2024-12-02 14:08:36 +00:00
手瓜一十雪
672ae8decf fix: Once处理空格目录 中文目录 2024-12-02 22:08:08 +08:00
手瓜一十雪
2abc7e541d fix: 空格目录启动问题 2024-12-02 21:23:13 +08:00
手瓜一十雪
45b1f369ac style: 异步实现 2024-12-02 11:44:37 +08:00
手瓜一十雪
3b5d2c8f6f style: 简化写法 2024-12-02 11:16:12 +08:00
Mlikiowa
5376e16c9f release: v4.2.10 2024-12-01 11:12:10 +00:00
bietiaop
af052242fa feat:带等级的实时日志 2024-12-01 15:19:23 +08:00
手瓜一十雪
85e0b71545 chore: daily -> weekly 2024-12-01 13:26:56 +08:00
手瓜一十雪
1206d1fcf6 chore: bug report 2024-12-01 13:25:36 +08:00
手瓜一十雪
f7534dc438 fix: 自动迁移 2024-12-01 13:20:20 +08:00
手瓜一十雪
97f317254e fix: MiniApp type check 2024-12-01 13:11:56 +08:00
手瓜一十雪
9eaf51e15f fix: nullable 2024-12-01 13:04:00 +08:00
手瓜一十雪
7221f4ac02 fix: type-check 2024-12-01 12:50:13 +08:00
手瓜一十雪
1bb6dce239 refactor: type-check (#586)
* refactor: type-check

* fix: default

* refactor: type-check
2024-12-01 12:41:51 +08:00
bietiaop
d13db5e8eb feat: 实时日志 (#584)
* feat: 历史日志

* feat: 实时日志

* fix: EventEmitter实现事件监听
2024-12-01 09:31:47 +08:00
Mlikiowa
040b5535f3 release: v4.2.9 2024-11-30 06:12:44 +00:00
手瓜一十雪
b44e1618fb fix: quick error 2024-11-30 14:12:23 +08:00
手瓜一十雪
1e13483bc3 fix: type 2024-11-30 13:32:21 +08:00
手瓜一十雪
f9519d3923 style: lint 2024-11-30 13:29:10 +08:00
手瓜一十雪
86cdfbb79b feat: 取消上报 pic_type 2024-11-30 12:11:33 +08:00
手瓜一十雪
a70585e854 feat: 处理失败的情况 2024-11-30 12:08:58 +08:00
Mlikiowa
040d0a8635 release: v4.2.8 2024-11-30 01:39:59 +00:00
手瓜一十雪
efa512ab21 fix: #580 2024-11-30 09:34:03 +08:00
bietiaop
9b04aed8b3 feat: 历史日志 2024-11-30 09:30:13 +08:00
手瓜一十雪
7087eafe37 feat: Universal Package (#578)
* feat: 统一包支持

* feat: Universal
2024-11-29 15:11:35 +08:00
Mlikiowa
c81c4af653 release: v4.2.7 2024-11-29 04:48:36 +00:00
手瓜一十雪
c05cc9dd02 feat: 迁移29927 2024-11-29 12:48:03 +08:00
手瓜一十雪
1a0da00f2d refactor: webui log 移除公网输出 2024-11-29 12:41:52 +08:00
手瓜一十雪
31b0c1d3d7 refactor: react webui 2024-11-29 12:22:25 +08:00
手瓜一十雪
53c1d40bcf refactor: logger bind (#577) 2024-11-28 20:55:28 +08:00
手瓜一十雪
97cacb4383 refactor: framework的操作性 2024-11-28 20:00:24 +08:00
Mlikiowa
e03905abaf release: v4.2.6 2024-11-28 07:28:33 +00:00
手瓜一十雪
06eba28b4c fux: #574 2024-11-28 15:28:09 +08:00
手瓜一十雪
bbfeac46dd Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-28 15:27:26 +08:00
手瓜一十雪
2fe4da094a fix 2024-11-28 15:14:01 +08:00
Mlikiowa
b454d8c0f9 release: v4.2.5 2024-11-28 07:07:10 +00:00
手瓜一十雪
1f9b5453cc fix: #573 2024-11-28 15:06:47 +08:00
Mlikiowa
3261791e99 release: v4.2.4 2024-11-28 03:00:35 +00:00
手瓜一十雪
3bb12e3f45 fix: #572 2024-11-28 10:56:57 +08:00
手瓜一十雪
1dc2f7e5a2 style: lint 2024-11-28 10:46:14 +08:00
手瓜一十雪
2531b08538 refactor: 提高解析兼容 2024-11-28 10:41:51 +08:00
手瓜一十雪
9fcfb5493c fix: #571 2024-11-28 10:27:04 +08:00
Mlikiowa
4576354c51 release: v4.2.3 2024-11-28 01:54:43 +00:00
手瓜一十雪
1dcf2ef0c6 fix: error handle 2024-11-28 09:53:50 +08:00
Mlikiowa
3642c65e8c release: v4.2.2 2024-11-27 12:39:03 +00:00
手瓜一十雪
40e105994a fix: pic size 2024-11-27 20:38:39 +08:00
Mlikiowa
f2ee973882 release: v4.2.1 2024-11-27 11:07:43 +00:00
164 changed files with 3242 additions and 2734 deletions

2
.env.universal Normal file
View File

@@ -0,0 +1,2 @@
VITE_BUILD_TYPE = Production
VITE_BUILD_PLATFORM = Universal

View File

@@ -10,13 +10,12 @@ body:
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
- type: input
id: system-version
attributes:
label: 系统版本
description: 运行 QQNT 的系统版本
placeholder: Windows 10 Pro Workstation 22H2
placeholder: Windows 11 24H2
validations:
required: true
- type: input
@@ -24,7 +23,7 @@ body:
attributes:
label: QQNT 版本
description: 可在 QQNT 的「关于」的设置页中找到
placeholder: 9.9.7-21804
placeholder: 9.9.16-29927
validations:
required: true
- type: input
@@ -40,21 +39,21 @@ body:
attributes:
label: OneBot 客户端
description: 连接至 NapCat 的客户端版本信息
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
placeholder: Karin 1.0.0
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: 填写你认为的 NapCat 的不正常行为
description: 填写你认为的 NapCat 的常行为
validations:
required: true
- type: textarea
id: how-reproduce
attributes:
label: 如何复现
description: 填写应当如何操作才能触发这个不正常行为
description: 填写应当如何操作才能触发这个常行为
placeholder: |
1. xxx
2. xxx

View File

@@ -1,11 +1,6 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"

View File

@@ -34,7 +34,14 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 回家旅途
[QQ Group](https://qm.qq.com/q/NWP25OeV0c)
[QQ Group](https://qm.qq.com/q/I6LU87a0Yq)
## 性能设计/协议标准
NapCat 已实现90+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
由此设计带来一系列好处在开发中获取群员列表通常小于50Ms单条文本消息发送在320Ms以内在1k+的群聊流畅运行同时带来一些副作用上报数据中大量使用Magic生成字段消息Id无法持久无法上报撤回消息原始内容。
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
## 感谢他们
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.16-29456",
"verHash": "dd395162",
"linuxVersion": "3.2.13-29456",
"linuxVerHash": "e379390a",
"version": "9.9.16-29927",
"verHash": "3e273e30",
"linuxVersion": "3.2.13-29927",
"linuxVerHash": "833d113c",
"type": "module",
"private": true,
"description": "QQ",
@@ -18,7 +18,7 @@
"qd": "externals/devtools/cli/index.js"
},
"main": "./loadNapCat.js",
"buildVersion": "29456",
"buildVersion": "29927",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",

View File

@@ -4,7 +4,7 @@
"name": "NapCatQQ",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
"version": "4.2.0",
"version": "4.2.25",
"icon": "./logo.png",
"authors": [
{

View File

@@ -5,12 +5,13 @@
"type": "module",
"scripts": {
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
"webui:dev": "vite",
"webui:dev": "vite --host",
"webui:build": "vite build",
"webui:preview": "vite preview"
},
"dependencies": {
"eslint-plugin-prettier": "^5.2.1",
"event-source-polyfill": "^1.0.31",
"qrcode": "^1.5.4",
"tdesign-icons-vue-next": "^0.3.3",
"tdesign-vue-next": "^1.10.3",
@@ -20,6 +21,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@types/event-source-polyfill": "^1.0.5",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-legacy": "^5.4.3",
"@vitejs/plugin-vue": "^5.1.4",

View File

@@ -109,4 +109,4 @@ onUnmounted(() => {
window.removeEventListener('resize', haddingFbars);
});
</script>
<style scoped></style>
<style></style>

Binary file not shown.

View File

@@ -0,0 +1,66 @@
export class githubApiManager {
public async GetBaseData(): Promise<Response | null> {
try {
const ConfigResponse= await fetch('https://api.github.com/repos/NapNeko/NapCatQQ', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting github data :', error);
}
return null;
}
public async GetReleasesData(): Promise<Response | null> {
try {
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/releases', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting releases data:', error);
}
return null;
}
public async GetPullsData(): Promise<Response | null> {
try {
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/pulls', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting Pulls data:', error);
}
return null;
}
public async GetContributors(): Promise<Response | null> {
try {
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/contributors', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting Pulls data:', error);
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
import { EventSourcePolyfill } from 'event-source-polyfill';
type LogListItem = string;
type LogListData = LogListItem[];
let eventSourcePoly: EventSourcePolyfill | null = null;
export class LogManager {
private readonly retCredential: string;
private readonly apiPrefix: string;
//调试时http://127.0.0.1:6099/api 打包时 ../api
constructor(retCredential: string, apiPrefix: string = '../api') {
this.retCredential = retCredential;
this.apiPrefix = apiPrefix;
}
public async GetLogList(): Promise<LogListData> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLogList`, {
method: 'GET',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data as LogListData;
}
}
} catch (error) {
console.error('Error getting LogList:', error);
}
return [] as LogListData;
}
public async GetLog(FileName: string): Promise<string> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLog?id=${FileName}`, {
method: 'GET',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data;
}
}
} catch (error) {
console.error('Error getting LogData:', error);
}
return 'null';
}
public async getRealTimeLogs(): Promise<EventSourcePolyfill | null> {
this.creatEventSource();
return eventSourcePoly;
}
private creatEventSource() {
try {
eventSourcePoly = new EventSourcePolyfill(`${this.apiPrefix}/Log/GetLogRealTime`, {
heartbeatTimeout: 3 * 60 * 1000,
headers: {
Authorization: 'Bearer ' + this.retCredential,
Accept: 'text/event-stream',
},
withCredentials: true,
});
} catch (error) {
console.error('创建SSE连接出错:', error);
}
}
}

View File

@@ -1,18 +1,28 @@
<template>
<t-layout class="dashboard-container">
<div ref="menuRef">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
<div v-if="!mediaQuery.matches">
<SidebarMenu
:menu-items="menuItems"
class="sidebar-menu"
:menu-width="sidebarWidth"
/>
</div>
<t-layout>
<router-view />
</t-layout>
<div v-if="mediaQuery.matches" class="bottom-menu">
<BottomMenu :menu-items="menuItems" />
</div>
</t-layout>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { onMounted, onUnmounted, ref } from 'vue';
import SidebarMenu from './webui/Nav.vue';
import BottomMenu from './webui/NavBottom.vue';
import emitter from '@/ts/event-bus';
const mediaQuery = window.matchMedia('(max-width: 768px)');
const sidebarWidth = ['232px', '64px'];
interface MenuItem {
value: string;
icon: string;
@@ -27,13 +37,18 @@ const menuItems = ref<MenuItem[]>([
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
]);
const menuRef = ref<HTMLDivElement | null>(null);
emitter.on('sendMenu', (event) => {
emitter.emit('sendWidth', menuRef.value?.offsetWidth);
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
const menuWidth = event ? sidebarWidth[1] : sidebarWidth[0];
emitter.emit('sendWidth', menuWidth);
localStorage.setItem('menuWidth', menuWidth.toString() || '0');
});
onMounted(() => {
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
if (mediaQuery.matches){
localStorage.setItem('menuWidth', '0');
}
});
onUnmounted(() => {
});
</script>
@@ -49,6 +64,12 @@ onMounted(() => {
position: relative;
z-index: 2;
}
.bottom-menu {
position: fixed;
bottom: 0;
width: 100%;
z-index: 2;
}
@media (max-width: 768px) {
.content {
@@ -56,3 +77,19 @@ onMounted(() => {
}
}
</style>
<style>
@media (max-width: 768px) {
.t-head-menu__inner .t-menu:first-child {
margin-left: 0;
}
.t-head-menu__inner{
width: 100%;
}
.t-head-menu .t-menu{
justify-content: space-evenly;
}
.t-menu__content{
display: none;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<t-card class="layout">
<t-card class="layout" :bordered="false">
<div class="login-container">
<h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods">

View File

@@ -1,5 +1,5 @@
<template>
<t-card class="layout">
<t-card class="layout" :bordered="false">
<div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">

View File

@@ -1,5 +1,5 @@
<template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
<t-menu theme="light" :width="menuWidth" :collapsed="collapsed" class="sidebar-menu">
<template #logo>
<div class="logo">
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
@@ -43,10 +43,11 @@ type MenuItem = {
icon?: string;
disabled?: boolean;
};
defineProps<{
menuItems: MenuItem[];
menuWidth: string | number | Array<string | number>;
}>();
const mediaQuery = window.matchMedia('(max-width: 800px)');
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
const disBtn = ref<boolean>(false);
@@ -57,12 +58,10 @@ const changeCollapsed = (): void => {
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
};
watch(collapsed, (newValue, oldValue) => {
setTimeout(() => {
emitter.emit('sendMenu', collapsed.value);
}, 300);
});
onMounted(() => {
const mediaQuery = window.matchMedia('(max-width: 800px)');
emitter.emit('sendMenu', collapsed.value);
const handleMediaChange = (e: MediaQueryListEvent) => {
disBtn.value = e.matches;
if (e.matches) {

View File

@@ -0,0 +1,37 @@
<template>
<t-head-menu theme="light" class="bottom-menu">
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
<t-tooltip :content="item.label" placement="top">
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
<template #icon>
<t-icon :name="item.icon" />
</template>
<!-- {{item.label}}-->
</t-menu-item>
</t-tooltip>
</router-link>
</t-head-menu>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
type MenuItem = {
value: string;
label: string;
route: string;
icon?: string;
disabled?: boolean;
};
defineProps<{
menuItems: MenuItem[];
}>();
</script>
<style scoped>
.bottom-menu {
display: flex;
justify-content: center;
border-top: 0.8px solid #ddd;
}
</style>

View File

@@ -4,3 +4,10 @@
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'ProtoNerdFontItalic';
src: url('../assets/0xProtoNerdFont-Italic.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@@ -40,6 +40,11 @@ import {
Aside as TAside,
Popconfirm as Tpopconfirm,
Empty as TEmpty,
Dropdown as TDropdown,
Typography as TTypographyText,
TreeSelect as TTreeSelect,
Loading as TLoading,
HeadMenu as THeadMenu
} from 'tdesign-vue-next';
import { router } from './router';
import 'tdesign-vue-next/es/style/index.css';
@@ -84,4 +89,9 @@ app.use(TFooter);
app.use(TAside);
app.use(Tpopconfirm);
app.use(TEmpty);
app.use(TDropdown);
app.use(TTypographyText);
app.use(TTreeSelect);
app.use(TLoading);
app.use(THeadMenu);
app.mount('#app');

View File

@@ -1,23 +1,97 @@
<template>
<div class="about-us">
<div>
<t-divider content="面板关于信息" align="left" />
<t-alert theme="success" message="NapCat.WebUi is running" />
<t-list class="list">
<t-list-item class="list-item">
<span class="item-label">开发人员:</span>
<t-divider content="面板关于信息" align="left">
<template #content>
<div style="display: flex; justify-content: center; align-items: center">
<info-circle-icon></info-circle-icon>
<div style="margin-left: 5px">面板关于信息</div>
</div>
</template>
</t-divider>
<t-alert theme="success" class="header" message="NapCat.WebUi is running" />
<t-list>
<t-list-item>
<div class="label-box">
<star-filled-icon class="item-icon" size="large" />
<span class="item-label">Star:</span>
</div>
<span class="item-content">
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/stargazers">{{
githubBastData?.stargazers_count
}}</t-link>
</span>
</t-list-item>
<t-list-item class="list-item">
<span class="item-label">版本信息:</span>
<t-list-item>
<tips-filled-icon class="item-icon" size="large" />
<span class="item-label">issues:</span>
<span class="item-content">
<t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag>
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
<t-tag class="tag-item" theme="success">
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/issues">{{
githubBastData?.open_issues_count
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<git-pull-request-filled-icon class="item-icon" size="large" />
<span class="item-label">Pull Requests:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/pulls">{{githubPullData?.length
}}</t-link>
</span>
</t-list-item>
<t-list-item >
<bookmark-add-filled-icon class="item-icon" size="large" />
<span class="item-label">Releases:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/releases">{{
githubReleasesData&&githubReleasesData[0]?timeDifference(githubReleasesData[0].published_at) + '前更新':''
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<usergroup-filled-icon class="item-icon" size="large" />
<span class="item-label">Contributors:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/graphs/contributors">{{githubContributorsData?.length}}</t-link>
</span>
</t-list-item>
<t-list-item>
<browse-filled-icon class="item-icon" size="large" />
<span class="item-label">Watchers:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/watchers">{{
githubBastData?.watchers
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<fork-filled-icon class="item-icon" size="large" />
<span class="item-label">Fork:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/fork">{{
githubBastData?.forks_count
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<statue-of-jesus-filled-icon class="item-icon" size="large" />
<span class="item-label">License:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ#License-1-ov-file">{{
githubBastData?.license.key
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<component-layout-filled-icon class="item-icon" size="large" />
<span class="item-label">Version:</span>
<span class="item-content">
<t-tag class="tag-item pgk-color"> WebUi: {{ pkg.version }} </t-tag>
<t-tag class="tag-item nc-color">
NapCat:
{{ githubReleasesData&&githubReleasesData[0] ?.tag_name ? githubReleasesData[0].tag_name : napCatVersion }}
</t-tag>
<t-tag class="tag-item td-color"> TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </t-tag>
</span>
</t-list-item>
</t-list>
@@ -28,6 +102,51 @@
<script setup lang="ts">
import pkg from '../../package.json';
import { napCatVersion } from '../../../src/common/version';
import {
InfoCircleIcon,
TipsFilledIcon,
StarFilledIcon,
GitPullRequestFilledIcon,
ForkFilledIcon,
StatueOfJesusFilledIcon,
BookmarkAddFilledIcon,
UsergroupFilledIcon,
BrowseFilledIcon,
ComponentLayoutFilledIcon,
} from 'tdesign-icons-vue-next';
import { githubApiManager } from '@/backend/githubApi';
import { onMounted, ref } from 'vue';
const githubApi = new githubApiManager();
const githubBastData = ref<any>(null);
const githubReleasesData = ref<any>(null);
const githubContributorsData = ref<any>(null);
const githubPullData = ref<any>(null);
const getBaseData = async () => {
githubBastData.value = await githubApi.GetBaseData();
githubReleasesData.value = await githubApi.GetReleasesData();
githubContributorsData.value = await githubApi.GetContributors();
githubPullData.value = await githubApi.GetPullsData();
};
const timeDifference = (timestamp: string): string => {
const givenTime = new Date(timestamp);
const currentTime = new Date();
const diffInMilliseconds = currentTime.getTime() - givenTime.getTime();
const seconds = Math.floor(diffInMilliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}小时`;
} else if (minutes > 0) {
return `${minutes}分钟`;
} else {
return `${seconds}`;
}
};
onMounted(() => {
getBaseData();
});
</script>
<style scoped>
@@ -35,23 +154,26 @@ import { napCatVersion } from '../../../src/common/version';
padding: 20px;
text-align: left;
}
.list {
.label-box {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
}
.item-icon {
padding: 5px;
color: #ffffff;
border-radius: 3px;
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
}
.item-label {
flex: 1;
font-weight: bold;
margin-left: 8px;
box-sizing: border-box;
height: auto;
padding: 0;
border: none;
font-size: 16px;
}
.item-content {
flex: 2;
display: flex;
@@ -64,3 +186,37 @@ import { napCatVersion } from '../../../src/common/version';
margin-bottom: 10px;
}
</style>
<style>
.t-list-item {
padding: 5px var(--td-comp-paddingLR-l);
}
.item-label {
flex: 2;
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.pgk-color {
color: white;
background-image: linear-gradient(-225deg, #9be15d 0%, #00e3ae 100%);
}
.nc-color {
color: white;
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
}
.td-color {
color: white;
background-image: linear-gradient(225deg, #0acffe 0%, #495aff 100%);
}
.header {
background-image: linear-gradient(225deg, #dfffcd 0%, #90f9c4 48%, #39f3bb 100%) !important;
}
.link-text{
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(-225deg, #B6CEE8 0%, #F578DC 100%);
font-weight: bold;
}
</style>

View File

@@ -1,6 +1,600 @@
<template>
<div class="log-view">
<h1>面板日志信息</h1>
<p>这里显示面板的日志信息</p>
<div class="title">
<t-divider content="日志查看" align="left">
<template #content>
<div style="display: flex; justify-content: center; align-items: center">
<system-log-icon></system-log-icon>
<div style="margin-left: 5px">日志查看</div>
</div>
</template>
</t-divider>
</div>
<div class="tab-box">
<t-tabs default-value="realtime" @change="selectType">
<t-tab-panel value="realtime" label="实时日志"></t-tab-panel>
<t-tab-panel value="history" label="历史日志"></t-tab-panel>
</t-tabs>
</div>
<div class="card-box">
<t-card class="card" :bordered="true">
<template #actions>
<t-row :align="'middle'" justify="center" :style="{ gap: smallScreen.matches ? '5px' : '24px' }">
<t-col flex="auto" style="display: inline-flex; justify-content: center">
<t-tooltip content="清理日志">
<t-button variant="text" shape="square" @click="clearLogs">
<clear-icon></clear-icon>
</t-button>
</t-tooltip>
</t-col>
<t-col flex="auto" style="display: inline-flex; justify-content: center">
<t-tooltip content="下载日志">
<t-button variant="text" shape="square" @click="downloadText">
<download-icon></download-icon>
</t-button>
</t-tooltip>
</t-col>
<t-col
v-if="LogDataType === 'history'"
flex="auto"
style="display: inline-flex; justify-content: center">
<t-tooltip content="历史日志">
<t-button variant="text" shape="square" @click="historyLog">
<history-icon></history-icon>
</t-button>
</t-tooltip>
</t-col>
<t-col flex="auto" style="display: inline-flex; justify-content: center">
<div class="tag-box">
<t-tag class="t-tag" :style="{ backgroundImage: typeKey[optValue.description] }">{{
optValue.content }}</t-tag>
</div>
<t-dropdown :options="options" :min-column-width="112" @click="openTypeList">
<t-button variant="text" shape="square">
<more-icon />
</t-button>
</t-dropdown>
</t-col>
</t-row>
</template>
<template #content>
<div class="content" ref="contentBox">
<div v-for="item in LogDataType === 'realtime'
? realtimeLogHtmlList.get(optValue.description)
: historyLogHtmlList.get(optValue.description)">
<span>{{ item.time }}</span><span :id="item.type">{{ item.content }}</span>
</div>
</div>
</template>
</t-card>
</div>
<t-dialog v-model:visible="visibleBody" header="历史日志" :destroy-on-close="true" :show-in-attached-element="true"
:on-confirm="GetLogList" class=".t-dialog__ctx .t-dialog__position">
<t-select v-model="value" :options="logFileData" placeholder="请选择日志" :multiple="true"
style="text-align: left" />
</t-dialog>
</template>
<script setup lang="ts">
import { MoreIcon, ClearIcon, DownloadIcon, HistoryIcon, SystemLogIcon } from 'tdesign-icons-vue-next';
import { nextTick, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { LogManager } from '@/backend/log';
import { MessagePlugin } from 'tdesign-vue-next';
import { EventSourcePolyfill } from 'event-source-polyfill';
const smallScreen = window.matchMedia('(max-width: 768px)');
const LogDataType = ref<string>('realtime');
const visibleBody = ref<boolean>(false);
const contentBox = ref<HTMLElement | null>(null);
let isMouseEntered = false;
const logManager = new LogManager(localStorage.getItem('auth') || '');
const eventSource = ref<EventSourcePolyfill | null>(null);
const intervalId = ref<number | null>(null);
const isPaused = ref(false);
interface OptionItem {
content: string;
value: number;
description: string;
}
const options = ref<OptionItem[]>([
{
content: '全部',
value: 1,
description: 'all',
},
{
content: '调试',
value: 2,
description: 'debug',
},
{
content: '提示',
value: 3,
description: 'info',
},
{
content: '警告',
value: 4,
description: 'warn',
},
{
content: '错误',
value: 5,
description: 'error',
},
{
content: '致命',
value: 5,
description: 'fatal',
},
]);
const typeKey = ref<Record<string, string>>({
all: 'linear-gradient(60deg,#16a085 0%, #f4d03f 100%)',
debug: 'linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%)',
info: 'linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%)',
warn: 'linear-gradient(to right, #e14fad 0%, #f9d423 48%, #e37318 100%)',
error: 'linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%)',
fatal: 'linear-gradient(-225deg, #fd0700, #ec567f)',
});
interface logHtml {
type?: string;
content: string;
color?: string;
time?: string;
}
type LogHtmlMap = Map<string, logHtml[]>;
const realtimeLogHtmlList = ref<LogHtmlMap>(
new Map([
['all', []],
['debug', []],
['info', []],
['warn', []],
['error', []],
['fatal', []],
])
);
const historyLogHtmlList = ref<LogHtmlMap>(
new Map([
['all', []],
['debug', []],
['info', []],
['warn', []],
['error', []],
['fatal', []],
])
);
const logFileData = ref<{ label: string; value: string }[]>([]);
const value = ref([]);
const optValue = ref<OptionItem>({
content: '全部',
value: 1,
description: 'all',
});
const openTypeList = (data: OptionItem) => {
optValue.value = data;
};
const logType = ['debug', 'info', 'warn', 'error', 'fatal'];
//清理log
const clearLogs = () => {
if (LogDataType.value === 'realtime') {
clearAllLogs(realtimeLogHtmlList);
} else {
clearAllLogs(historyLogHtmlList);
}
};
const clearAllLogs = (logList: Ref<Map<string, Array<logHtml>>>) => {
if ((optValue.value && optValue.value.description === 'all') || !optValue.value) {
logList.value = new Map([
['all', []],
['debug', []],
['info', []],
['warn', []],
['error', []],
['fatal', []],
]);
} else {
logList.value.set(optValue.value.description, []);
}
};
//定时清理log
const TimerClear = () => {
clearAllLogs(realtimeLogHtmlList);
};
const startTimer = () => {
if (!isPaused.value) {
intervalId.value = window.setInterval(TimerClear, 0.5 * 60 * 1000);
}
};
const pauseTimer = () => {
if (intervalId.value) {
window.clearInterval(intervalId.value);
isPaused.value = true;
}
};
const resumeTimer = () => {
if (isPaused.value) {
startTimer();
isPaused.value = false;
}
};
const stopTimer = () => {
if (intervalId.value) {
window.clearInterval(intervalId.value);
intervalId.value = null;
}
};
const extractContent = (text: string): string | null => {
const regex = /\[([^\]]+)]/;
const match = regex.exec(text);
if (match && match[1]) {
const extracted = match[1].toLowerCase();
if (logType.includes(extracted)) {
return match[1];
}
}
return null;
};
const loadData = (text: string, loadType: string) => {
const lines = text.split(/\r\n/);
lines.forEach((line) => {
if (loadType === 'realtime') {
let remoteJson = JSON.parse(line) as { message: string, level: string };
const type = remoteJson.level;
const actualType = type || 'other';
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
const data: logHtml = {
type: actualType,
content: remoteJson.message,
color: color,
time: '',
};
updateLogList(realtimeLogHtmlList, actualType, data);
} else if (loadType === 'history') {
const type = extractContent(line);
const actualType = type || 'other';
const timeRegex = /(\d{2}-\d{2} \d{2}:\d{2}:\d{2})|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
const match = timeRegex.exec(line);
let time = match ? match[0] : null;
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
const data: logHtml = {
type: actualType,
content: line.slice(match ? match[0].length : 0) || '',
color: color,
time: time ? time + ' ' : '',
};
updateLogList(historyLogHtmlList, actualType, data);
}
});
};
const updateLogList = (logList: Ref<Map<string, Array<logHtml>>>, actualType: string, data: logHtml) => {
const allLogs = logList.value.get('all');
if (Array.isArray(allLogs)) {
allLogs.push(data);
}
if (actualType !== 'other') {
const typeLogs = logList.value.get(actualType);
if (Array.isArray(typeLogs)) {
typeLogs.push(data);
}
}
};
const selectType = (key: string) => {
LogDataType.value = key;
};
interface CustomURL extends URL {
recycleObjectURL: (url: string) => void;
}
const isCompatibleWithCustomURL = (obj: any): obj is CustomURL => {
return typeof obj === 'object' && obj !== null && typeof (obj as any).recycleObjectURL === 'function';
};
const recycleURL = (url: string) => {
if (isCompatibleWithCustomURL(window.URL)) {
const customURL = window.URL as CustomURL;
customURL.recycleObjectURL(url);
}
};
const generateTXT = (textContent: string, fileName: string) => {
try {
const blob = new Blob([textContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
recycleURL(url);
} catch (error) {
console.error('下载文本时出现错误:', error);
}
};
const downloadText = () => {
if (LogDataType.value === 'realtime') {
const logs = realtimeLogHtmlList.value.get(optValue.value.description);
if (logs && logs.length > 0) {
const result = logs.map((obj) => obj.content).join('\r\n');
generateTXT(result, '实时日志');
} else {
MessagePlugin.error('暂无可下载日志');
}
} else {
const logs = historyLogHtmlList.value.get(optValue.value.description);
if (logs && logs.length > 0) {
const result = logs.map((obj) => obj.content).join('\r\n');
generateTXT(result, '历史日志');
} else {
MessagePlugin.error('暂无可下载日志');
}
}
};
const historyLog = async () => {
value.value = [];
visibleBody.value = true;
const res = await logManager.GetLogList();
clearAllLogs(historyLogHtmlList);
if (res.length > 0) {
logFileData.value = res.map((ele: string) => {
return { label: ele, value: ele };
});
} else {
logFileData.value = [];
}
};
const GetLogList = async () => {
if (value.value.length > 0) {
for (const ele of value.value) {
try {
const data = await logManager.GetLog(ele);
if (data && data !== 'null') {
loadData(data, 'history');
}
} catch (error) {
console.error(`获取日志 ${ele} 时出现错误:`, error);
}
}
visibleBody.value = false;
} else {
MessagePlugin.error('请选择日志');
}
};
const fetchRealTimeLogs = async () => {
eventSource.value = await logManager.getRealTimeLogs();
if (eventSource.value) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
eventSource.value.onmessage = (event: MessageEvent) => {
console.log(event.data)
loadData(event.data, 'realtime');
};
}
};
const closeRealTimeLogs = async () => {
if (eventSource.value) {
eventSource.value.close();
}
};
const scrollToBottom = () => {
if (!isMouseEntered) {
nextTick(() => {
if (contentBox.value) {
contentBox.value.scrollTop = contentBox.value.scrollHeight;
}
});
}
};
const observeDOMChanges = () => {
if (contentBox.value) {
const observer = new MutationObserver(() => {
scrollToBottom();
});
observer.observe(contentBox.value, {
childList: true,
subtree: true,
});
}
};
const showScrollbar = () => {
if (contentBox.value) {
contentBox.value.style.overflow = 'auto';
}
};
const hideScrollbar = () => {
if (contentBox.value) {
contentBox.value.style.overflow = 'hidden';
if (!isMouseEntered) {
scrollToBottom();
}
}
};
watch(
realtimeLogHtmlList,
() => {
if (!isMouseEntered) {
scrollToBottom();
}
},
{ immediate: true }
);
watch(
historyLogHtmlList,
() => {
if (!isMouseEntered) {
scrollToBottom();
}
},
{ immediate: true }
);
onMounted(() => {
fetchRealTimeLogs();
startTimer();
contentBox.value = document.querySelector('.content');
if (contentBox.value) {
contentBox.value.style.overflow = 'hidden';
contentBox.value.addEventListener('mouseenter', () => {
isMouseEntered = true;
showScrollbar();
pauseTimer();
});
contentBox.value.addEventListener('mouseleave', () => {
isMouseEntered = false;
hideScrollbar();
resumeTimer();
setTimeout(() => {
scrollToBottom();
}, 1000);
});
observeDOMChanges();
}
});
onUnmounted(() => {
closeRealTimeLogs();
stopTimer();
});
</script>
<style scoped>
.title {
padding: 20px 20px 0 20px;
}
.tab-box {
margin: 0 20px;
}
.card-box {
margin: 10px 20px;
}
.content {
height: 56vh;
background-image: url('@/assets/logo.png');
border: 1px solid #ddd6d6 !important;
padding: 5px 10px;
text-align: left;
overflow-y: auto;
margin-top: -10px;
font-family: monospace;
font-size: 15px;
line-height: 16px;
}
.content span {
white-space: pre-wrap;
word-break: break-all;
overflow-wrap: break-word;
}
@keyframes fadeInOnce {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOutOnce {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.content div {
animation: fadeInOnce 0.5s forwards;
}
::-webkit-scrollbar {
width: 5px;
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #888888;
border-radius: 4px;
}
.tag-box {
display: flex;
justify-content: center;
align-items: center;
margin-right: 5px;
}
.t-tag {
min-width: 60px;
text-align: center;
display: flex;
justify-content: center;
color: white;
font-weight: 500;
}
#debug {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%);
}
#info {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%);
}
#warn {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(225deg, #e14fad 0%, #f9d423 48%, #e37318 100%);
}
#error {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%);
}
#fatal {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to right, #fd0700, #ec567f);
}
#other {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to top, #3f51b1 0%, #5a55ae 13%, #7b5fac 25%, #8f6aae 38%, #a86aa4 50%, #cc6b8e 62%, #f18271 75%, #f3a469 87%, #f7c978 100%);
}
@media (max-width: 786px) {
.content {
height: 50vh;
font-family: ProtoNerdFontItalic, monospace;
font-size: 12px;
line-height: 14.3px;
}
}
</style>
<style>
.card {
padding: 5px 10px 20px 10px !important;
}
@media (max-width: 786px) {
.card {
padding: 0 !important;
}
}
</style>

View File

@@ -1,10 +1,18 @@
<template>
<div ref="headerBox" class="title">
<t-divider content="网络配置" align="left" />
<t-divider content="网络配置" align="left">
<template #content>
<div style="display: flex; justify-content: center; align-items: center">
<wifi1-icon />
<div style="margin-left: 5px">网络配置</div>
</div>
</template>
</t-divider>
<t-divider align="right">
<t-button @click="addConfig()">
<template #icon><add-icon /></template>
添加配置</t-button>
添加配置</t-button
>
</t-divider>
</div>
<div v-if="loadPage" ref="setting" class="setting">
@@ -16,86 +24,142 @@
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
</t-tabs>
</div>
<t-loading attach="#alice" :loading="!loadPage" :showOverlay="false">
<div id="alice" v-if="!loadPage" style="height: 80vh;position: relative" ></div>
</t-loading>
<div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
<div v-for="(item, index) in cardConfig" :key="index">
<t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }"
:header-bordered="true" class="setting-card">
<t-card
:title="item.name"
:description="item.type"
:style="{ width: cardWidth + 'px' }"
:header-bordered="true"
class="setting-card"
>
<template #actions>
<t-space>
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
<t-popconfirm theme="danger" content="确认删除" @confirm="delConfig(item)">
<t-popconfirm content="确认删除" @confirm="delConfig(item)">
<delete-icon size="20px"></delete-icon>
</t-popconfirm>
</t-space>
</template>
<div class="setting-content">
<t-card class="card-address" :style="{
borderLeft: '7px solid ' + (item.enable ?
'var(--td-success-color)' :
'var(--td-error-color)')
}">
<div class="local-box" v-if="item.host&&item.port">
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
<t-card
class="card-address"
:style="{
borderLeft:
'7px solid ' + (item.enable ? 'var(--td-success-color)' : 'var(--td-error-color)'),
}"
>
<div class="local-box" v-if="item.host && item.port">
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
<copy-icon class="copy-icon" size="20px" @click="copyText(item.host + ':' + item.port)"></copy-icon>
<copy-icon
class="copy-icon"
size="20px"
@click="copyText(item.host + ':' + item.port)"
></copy-icon>
</div>
<div class="local-box" v-if="item.url">
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
<strong class="local" >{{ item.url }}</strong>
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
<strong class="local">{{ item.url }}</strong>
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
</div>
</t-card>
<t-collapse :default-value="[0]" expand-mutex style="margin-top:10px;" class="info-coll">
<t-collapse :default-value="[0]" expand-mutex style="margin-top: 10px" class="info-coll">
<t-collapse-panel header="基础信息">
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info">
<t-descriptions
size="small"
:layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info"
>
<t-descriptions-item v-if="item.token" label="连接密钥">
<div v-if="mediumScreen.matches||largeScreen.matches" class="token-view">
<div v-if="mediumScreen.matches || largeScreen.matches" class="token-view">
<span>{{ showToken ? item.token : '******' }}</span>
<browse-icon class="browse-icon" v-if="showToken" size="18px"
@click="showToken = false"></browse-icon>
<browse-off-icon class="browse-icon" v-else size="18px"
@click="showToken = true"></browse-off-icon>
<browse-icon
class="browse-icon"
v-if="showToken"
size="18px"
@click="showToken = false"
></browse-icon>
<browse-off-icon
class="browse-icon"
v-else
size="18px"
@click="showToken = true"
></browse-off-icon>
</div>
<div v-else>
<t-popup :showArrow="true" trigger="click">
<t-tag theme="primary">点击查看</t-tag>
<template #content>
<div @click="copyText(item.token)">{{item.token}}</div>
<div @click="copyText(item.token)">{{ item.token }}</div>
</template>
</t-popup>
</div>
</t-descriptions-item>
<t-descriptions-item label="消息格式">{{ item.messagePostFormat }}</t-descriptions-item>
<t-descriptions-item label="消息格式">{{
item.messagePostFormat
}}</t-descriptions-item>
</t-descriptions>
</t-collapse-panel>
<t-collapse-panel header="状态信息">
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info">
<t-descriptions
size="small"
:layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info"
>
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
<t-tag class="tag-item" :theme="item.debug ? 'success' : 'danger'">
{{ item.debug ? '开启' : '关闭' }}</t-tag>
<t-tag
:class="item.debug ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'debug')"
>
{{ item.debug ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
label="Websocket 功能">
<t-tag class="tag-item" :theme="item.enableWebsocket ? 'success' : 'danger'">
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
<t-descriptions-item
v-if="item.hasOwnProperty('enableWebsocket')"
label="Websocket 功能"
>
<t-tag
:class="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableWebsocket')"
>
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag
>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
<t-tag class="tag-item" :theme="item.enableCors ? 'success' : 'danger'">
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
<t-descriptions-item
v-if="item.hasOwnProperty('enableCors')"
label="跨域放行"
>
<t-tag :class="item.enableCors ? 'tag-item-on' : 'tag-item-off'" @click="toggleProperty(item, 'enableCors')">
{{ item.enableCors ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="上报自身消息">
<t-tag class="tag-item" :theme="item.reportSelfMessage ? 'success' : 'danger'">
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
<t-descriptions-item
v-if="item.hasOwnProperty('enableForcePushEvent')"
label="上报自身消息"
>
<t-tag
:class="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'reportSelfMessage')"
>
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="强制推送事件">
<t-tag class="tag-item"
:theme="item.enableForcePushEvent ? 'success' : 'danger'">
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
<t-descriptions-item
v-if="item.hasOwnProperty('enableForcePushEvent')"
label="强制推送事件"
>
<t-tag
class="tag-item"
:class="item.enableForcePushEvent ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableForcePushEvent')"
>
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item>
</t-descriptions>
</t-collapse-panel>
@@ -109,16 +173,30 @@
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
</t-card>
</div>
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
:show-in-attached-element="true" placement="center" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog--defaul">
<div slot="body" class="dialog-body" >
<t-dialog
v-model:visible="visibleBody"
:header="dialogTitle"
:destroy-on-close="true"
:show-in-attached-element="true"
:on-confirm="saveConfig"
class=".t-dialog__ctx .t-dialog__position"
>
<div slot="body" class="dialog-body">
<t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
label="名称" name="name">
<t-form-item
style="text-align: left"
:rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
label="名称"
name="name"
>
<t-input v-model="newTab.name" />
</t-form-item>
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
label="类型" name="type">
<t-form-item
style="text-align: left"
:rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
label="类型"
name="type"
>
<t-select v-model="newTab.type" @change="onloadDefault">
<t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option>
@@ -127,8 +205,10 @@
</t-select>
</t-form-item>
<div>
<component :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
:config="newTab.data" />
<component
:is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
:config="newTab.data"
/>
</div>
</t-form>
</div>
@@ -136,8 +216,17 @@
</template>
<script setup lang="ts">
import { AddIcon, DeleteIcon, Edit2Icon, ServerFilledIcon, CopyIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-vue-next';
import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue';
import {
AddIcon,
DeleteIcon,
Edit2Icon,
ServerFilledIcon,
CopyIcon,
BrowseOffIcon,
BrowseIcon,
Wifi1Icon,
} from 'tdesign-icons-vue-next';
import { onMounted, onUnmounted, ref, resolveDynamicComponent, watch } from 'vue';
import emitter from '@/ts/event-bus';
import {
mergeNetworkDefaultConfig,
@@ -187,7 +276,7 @@ const operateType = ref<string>('');
//配置项索引
const configIndex = ref<number>(0);
//保存时所用数据
const networkConfig: NetworkConfig & { [key: string]: any; } = {
const networkConfig: NetworkConfig & { [key: string]: any } = {
websocketClients: [],
websocketServers: [],
httpClients: [],
@@ -235,6 +324,18 @@ const editConfig = (item: any) => {
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
visibleBody.value = true;
};
const toggleProperty = async (item: any, tagData: string) => {
const type = getKeyByValue(typeCh, item.type);
const newData = { ...item };
newData[tagData] = !item[tagData];
if (type) {
newTab.value = { name: item.name, data: newData, type: type };
}
operateType.value = 'edit';
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
await saveConfig();
};
const delConfig = (item: any) => {
const type = getKeyByValue(typeCh, item.type);
if (type) {
@@ -252,7 +353,6 @@ const selectType = (key: ComponentKey) => {
};
const onloadDefault = (key: ComponentKey) => {
console.log(key);
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
};
//检测重名
@@ -350,22 +450,21 @@ const loadConfig = async () => {
};
const copyText = async (text: string) => {
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
await navigator.clipboard.writeText(text);
document.body.removeChild(input);
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
MessagePlugin.success('复制成功');
} catch (err) {
console.error('复制失败', err);
} finally {
document.body.removeChild(textarea);
}
};
const handleResize = () => {
// 得根据卡片宽度改,懒得改了;先不管了
// if(window.innerWidth < 540) {
// infoOneCol.value= true
// } else {
// infoOneCol.value= false
// }
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
if (mediumScreen.matches) {
cardWidth.value = (tabsWidth.value - 20) / 2;
@@ -375,29 +474,43 @@ const handleResize = () => {
cardWidth.value = tabsWidth.value;
}
loadPage.value = true;
setTimeout(() => {
setTimeout(()=>{
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
}, 300);
},300)
};
emitter.on('sendWidth', (width) => {
if (typeof width === 'number' && !isNaN(width)) {
menuWidth.value = width;
handleResize();
if (typeof width === 'string') {
const strWidth = width as string;
menuWidth.value = parseInt(strWidth);
}
});
watch(menuWidth, (newValue, oldValue) => {
loadPage.value = false;
setTimeout(()=>{
handleResize();
},300)
});
onMounted(() => {
loadConfig();
const cachedWidth = localStorage.getItem('menuWidth');
if (cachedWidth) {
menuWidth.value = parseInt(cachedWidth);
setTimeout(() => {
setTimeout(()=>{
handleResize();
}, 300);
},300)
}
window.addEventListener('resize', handleResize);
window.addEventListener('resize', ()=>{
setTimeout(()=>{
handleResize();
},300)
});
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('resize', ()=>{
setTimeout(()=>{
handleResize();
},300)
});
});
</script>
@@ -437,7 +550,7 @@ onUnmounted(() => {
display: flex;
margin-top: 2px;
}
.local-icon{
.local-icon {
flex: 1;
}
.local {
@@ -448,14 +561,12 @@ onUnmounted(() => {
text-overflow: ellipsis;
}
.copy-icon {
flex: 1;
cursor: pointer;
flex-direction: row;
}
.token-view {
display: flex;
align-items: center;
@@ -467,11 +578,22 @@ onUnmounted(() => {
overflow: hidden;
text-overflow: ellipsis;
}
.browse-icon{
.tag-item-on{
color: white;
cursor: pointer;
background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%) !important;
}
.tag-item-off{
color: white;
cursor: pointer;
background-image: linear-gradient(to top, rgba(255, 8, 68, 0.93) 0%, #D54941 100%) !important;
}
.browse-icon {
flex: 2;
}
:global(.t-dialog__ctx .t-dialog--defaul) {
margin: 0 20px;
:global(.t-dialog__ctx .t-dialog__position) {
padding: 48px 10px;
}
@media (max-width: 1024px) {
.setting-box {
@@ -483,7 +605,6 @@ onUnmounted(() => {
.setting-box {
grid-template-columns: 1fr;
}
}
.card-box {
@@ -494,9 +615,8 @@ onUnmounted(() => {
line-height: 400px !important;
}
.dialog-body {
max-height: 60vh;
max-height: 50vh;
overflow-y: auto;
}
@@ -515,12 +635,6 @@ onUnmounted(() => {
font-size: 12px;
}
.card-address .t-card__body {
display: flex;
flex-direction: row;
align-items: center;
}
.setting-base-info .t-descriptions__header {
font-size: 15px;
margin-bottom: 0;
@@ -530,7 +644,7 @@ onUnmounted(() => {
padding: 0 var(--td-comp-paddingLR-l) !important;
}
.setting-base-info tr>td:last-child {
.setting-base-info tr > td:last-child {
text-align: right;
}

View File

@@ -1,6 +1,13 @@
<template>
<div class="title">
<t-divider content="其余配置" align="left" />
<t-divider content="其余配置" align="left">
<template #content>
<div style="display: flex; justify-content: center; align-items: center">
<setting-icon />
<div style="margin-left: 5px">其余配置</div>
</div>
</template>
</t-divider>
</div>
<t-card class="card">
<div class="other-config-container">
@@ -29,11 +36,12 @@ import { ref, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { OneBotConfig } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
import { SettingIcon } from 'tdesign-icons-vue-next';
const otherConfig = ref<Partial<OneBotConfig>>({
musicSignUrl: '',
enableLocalFile2Url: false,
parseMultMsg: true
parseMultMsg: true,
});
const labelAlign = ref<string>();

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
@@ -11,13 +11,13 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
<t-switch v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
<t-switch v-model="config.debug" />
</t-form-item>
</t-form>
</div>

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
@@ -11,10 +11,10 @@
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
<t-switch v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-checkbox v-model="config.enableWebsocket" />
<t-switch v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
@@ -23,7 +23,7 @@
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
<t-switch v-model="config.debug" />
</t-form-item>
</t-form>
</div>

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
@@ -11,13 +11,13 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
<t-switch v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
<t-switch v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" />
@@ -14,16 +14,16 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="上报自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
<t-switch v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="强制推送事件">
<t-checkbox v-model="config.enableForcePushEvent" />
<t-switch v-model="config.enableForcePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
<t-switch v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />

View File

@@ -2,16 +2,19 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.2.0",
"version": "4.2.25",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
"build:webui": "cd napcat.webui && vite build",
"dev:universal": "vite build --mode universal",
"dev:framework": "vite build --mode framework",
"dev:shell": "vite build --mode shell",
"dev:webui": "cd napcat.webui && npm run webui:dev",
"lint": "eslint --fix src/**/*.{js,ts,vue}",
"depend": "cd dist && npm install --omit=dev"
"depend": "cd dist && npm install --omit=dev",
"dev:depend": "npm i && cd napcat.webui && npm i"
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.7",
@@ -23,6 +26,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17",
"@sinclair/typebox": "^0.34.9",
"@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^22.0.1",
@@ -41,7 +45,6 @@
"file-type": "^19.0.0",
"globals": "^15.12.0",
"image-size": "^1.1.1",
"json-schema-to-ts": "^3.1.1",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^6.0.1",

View File

@@ -96,7 +96,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
};
}
} catch (error: any) {
logger.logError.bind(logger)('convert silk failed', error.stack);
logger.logError('convert silk failed', error.stack);
return {};
}
}

View File

@@ -33,27 +33,27 @@ export abstract class ConfigBase<T> {
}
read(copy_default: boolean = true): T {
const logger = this.core.context.logger;
const configPath = this.getConfigPath(this.core.selfInfo.uin);
if (!fs.existsSync(configPath) && copy_default) {
try {
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
this.core.context.logger.log(`[Core] [Config] 配置文件创建成功!\n`);
} catch (e: any) {
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
this.core.context.logger.logError(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
}
} else if (!fs.existsSync(configPath) && !copy_default) {
fs.writeFileSync(configPath, '{}');
}
try {
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
this.core.context.logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
return this.configData;
} catch (e: any) {
if (e instanceof SyntaxError) {
logger.logError.bind(logger)(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
this.core.context.logger.logError(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
} else {
logger.logError.bind(logger)(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
this.core.context.logger.logError(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
}
return {} as T;
}
@@ -61,14 +61,13 @@ export abstract class ConfigBase<T> {
save(newConfigData: T = this.configData) {
const logger = this.core.context.logger;
const selfInfo = this.core.selfInfo;
this.configData = newConfigData;
const configPath = this.getConfigPath(selfInfo.uin);
try {
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
} catch (e: any) {
logger.logError.bind(logger)(`保存配置文件 ${configPath} 时发生错误:`, e.message);
this.core.context.logger.logError(`保存配置文件 ${configPath} 时发生错误:`, e.message);
}
}
}

View File

@@ -1,9 +1,7 @@
import fs from 'fs';
import { stat } from 'fs/promises';
import crypto, { randomUUID } from 'crypto';
import util from 'util';
import path from 'node:path';
import * as fileType from 'file-type';
import { solveProblem } from '@/common/helper';
export interface HttpDownloadOptions {
@@ -15,7 +13,6 @@ type Uri2LocalRes = {
success: boolean,
errMsg: string,
fileName: string,
ext: string,
path: string
}
@@ -73,27 +70,6 @@ async function checkFile(path: string): Promise<void> {
// 如果文件存在则无需做任何事情Promise 解决resolve自身
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
const result = {
err: '',
data: '',
};
try {
try {
await checkFileExist(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
result.data = data.toString('base64');
} catch (err: any) {
result.err = err.toString();
}
return result;
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
@@ -160,20 +136,6 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
return Buffer.from(buffer);
}
export async function checkFileV2(filePath: string) {
try {
const ext: string | undefined = (await fileType.fileTypeFromFile(filePath))?.ext;
if (ext) {
fs.renameSync(filePath, filePath + `.${ext}`);
filePath += `.${ext}`;
return { success: true, ext: ext, path: filePath };
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
}
return { success: false, ext: '', path: filePath };
}
export enum FileUriType {
Unknown = 0,
Local = 1,
@@ -190,16 +152,17 @@ export async function checkUriType(Uri: string) {
}, Uri);
if (LocalFileRet) return LocalFileRet;
const OtherFileRet = await solveProblem((uri: string) => {
//再判断是否是Http
if (uri.startsWith('http://') || uri.startsWith('https://')) {
// 再判断是否是Http
if (uri.startsWith('http:') || uri.startsWith('https:')) {
return { Uri: uri, Type: FileUriType.Remote };
}
//再判断是否是Base64
if (uri.startsWith('base64://')) {
// 再判断是否是Base64
if (uri.startsWith('base64:')) {
return { Uri: uri, Type: FileUriType.Base64 };
}
if (uri.startsWith('file://')) {
let filePath: string = uri.slice(7);
// 默认file://
if (uri.startsWith('file:')) {
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
return { Uri: filePath, Type: FileUriType.Local };
}
if (uri.startsWith('data:')) {
@@ -212,63 +175,35 @@ export async function checkUriType(Uri: string) {
return { Uri: Uri, Type: FileUriType.Unknown };
}
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> {
export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
//解析失败
const tempName = randomUUID();
if (!filename) filename = randomUUID();
const filename = randomUUID();
const filePath = path.join(dir, filename);
//解析Http和Https协议
if (UriType == FileUriType.Unknown) {
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
}
//解析File协议和本地文件
if (UriType == FileUriType.Local) {
switch (UriType) {
case FileUriType.Local: {
const fileExt = path.extname(HandledUri);
let filename = path.basename(HandledUri, fileExt);
filename += fileExt;
//复制文件到临时文件并保持后缀
const filenameTemp = tempName + fileExt;
const filePath = path.join(dir, filenameTemp);
fs.copyFileSync(HandledUri, filePath);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
const tempFilePath = path.join(dir, filename + fileExt);
fs.copyFileSync(HandledUri, tempFilePath);
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
}
//接下来都要有文件名
if (UriType == FileUriType.Remote) {
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
if (pathInfo.name) {
const pathlen = 200 - dir.length - pathInfo.name.length;
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
if (pathInfo.ext) {
filename += pathInfo.ext;
}
}
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
const filePath = path.join(dir, tempName + fileExt);
case FileUriType.Remote: {
const buffer = await httpDownload(HandledUri);
//没有文件就创建
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
//解析Base64
if (UriType == FileUriType.Base64) {
case FileUriType.Base64: {
const base64 = HandledUri.replace(/^base64:\/\//, '');
const buffer = Buffer.from(base64, 'base64');
let filePath = path.join(dir, filename);
let fileExt = '';
fs.writeFileSync(filePath, buffer);
const { success, ext, path: fileTypePath } = await checkFileV2(filePath);
if (success) {
filePath = fileTypePath;
fileExt = ext;
filename = filename + '.' + ext;
const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
}
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
}

View File

@@ -1,9 +1,9 @@
import winston, { format, transports } from 'winston';
import { truncateString } from '@/common/helper';
import path from 'node:path';
import fs from 'node:fs';
import fs from 'node:fs/promises';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
import EventEmitter from 'node:events';
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
@@ -24,6 +24,36 @@ function getFormattedTimestamp() {
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${milliseconds}`;
}
const logEmitter = new EventEmitter();
export type LogListener = (msg: string) => void;
class Subscription {
public static MAX_HISTORY = 100;
public static history: string[] = [];
subscribe(listener: LogListener) {
for (const history of Subscription.history) {
try {
listener(history);
} catch (_) {
// ignore
}
}
logEmitter.on('log', listener);
}
unsubscribe(listener: LogListener) {
logEmitter.off('log', listener);
}
notify(msg: string) {
logEmitter.emit('log', msg);
if (Subscription.history.length >= Subscription.MAX_HISTORY) {
Subscription.history.shift();
}
Subscription.history.push(msg);
}
}
export const logSubscription = new Subscription();
export class LogWrapper {
fileLogEnabled = true;
consoleLogEnabled = true;
@@ -47,7 +77,7 @@ export class LogWrapper {
filename: logPath,
level: 'debug',
maxsize: 5 * 1024 * 1024, // 5MB
maxFiles: 5
maxFiles: 5,
}),
new transports.Console({
format: format.combine(
@@ -56,9 +86,9 @@ export class LogWrapper {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
)
})
]
),
}),
],
});
this.setLogSelfInfo({ nick: '', uid: '' });
@@ -67,26 +97,20 @@ export class LogWrapper {
cleanOldLogs(logDir: string) {
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
fs.readdir(logDir, (err, files) => {
if (err) {
this.logger.error('Failed to read log directory', err);
return;
}
files.forEach(file => {
fs.readdir(logDir).then((files) => {
files.forEach((file) => {
const filePath = path.join(logDir, file);
this.deleteOldLogFile(filePath, oneWeekAgo);
});
}).catch((err) => {
this.logger.error('Failed to read log directory', err);
});
}
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
fs.stat(filePath, (err, stats) => {
if (err) {
this.logger.error('Failed to get file stats', err);
return;
}
fs.stat(filePath).then((stats) => {
if (stats.mtime.getTime() < oneWeekAgo) {
fs.unlink(filePath, err => {
fs.unlink(filePath).catch((err) => {
if (err) {
if (err.code === 'ENOENT') {
this.logger.warn(`File already deleted: ${filePath}`);
@@ -98,6 +122,8 @@ export class LogWrapper {
}
});
}
}).catch((err) => {
this.logger.error('Failed to get file stats', err);
});
}
@@ -111,7 +137,7 @@ export class LogWrapper {
});
}
setLogSelfInfo(selfInfo: { nick: string, uid: string }) {
setLogSelfInfo(selfInfo: { nick: string; uid: string }) {
const userInfo = `${selfInfo.nick}`;
this.logger.defaultMeta = { userInfo };
}
@@ -135,14 +161,16 @@ export class LogWrapper {
}
formatMsg(msg: any[]) {
return msg.map(msgItem => {
return msg
.map((msgItem) => {
if (msgItem instanceof Error) {
return msgItem.stack;
} else if (typeof msgItem === 'object') {
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
}
return msgItem;
}).join(' ');
})
.join(' ');
}
_log(level: LogLevel, ...args: any[]) {
@@ -155,6 +183,7 @@ export class LogWrapper {
// eslint-disable-next-line no-control-regex
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
}
logSubscription.notify(JSON.stringify({ level, message }));
}
log(...args: any[]) {
@@ -282,13 +311,9 @@ function textElementToText(textElement: any): string {
}
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
const recordMsgOrNull = msg.records.find(
record => replyElement.sourceMsgIdInRecords === record.msgId,
);
return `[回复消息 ${recordMsgOrNull &&
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
?
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
`未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
const recordMsgOrNull = msg.records.find((record) => replyElement.sourceMsgIdInRecords === record.msgId);
return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
? rawMessageToText(recordMsgOrNull, recursiveLevel + 1)
: `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
}]`;
}

View File

@@ -1,6 +1,5 @@
import https from 'node:https';
import http from 'node:http';
import { readFileSync } from 'node:fs';
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
@@ -69,7 +68,7 @@ export class RequestUtil {
// 'Content-Length': Buffer.byteLength(postData),
// },
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: any) => {
const req = protocol.request(options, (res: http.IncomingMessage) => {
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
@@ -112,24 +111,4 @@ export class RequestUtil {
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
}
static async createFormData(boundary: string, filePath: string): Promise<Buffer> {
let type = 'image/png';
if (filePath.endsWith('.jpg')) {
type = 'image/jpeg';
}
const formDataParts = [
`------${boundary}\r\n`,
`Content-Disposition: form-data; name="share_image"; filename="${filePath}"\r\n`,
'Content-Type: ' + type + '\r\n\r\n',
];
const fileContent = readFileSync(filePath);
const footer = `\r\n------${boundary}--`;
return Buffer.concat([
Buffer.from(formDataParts.join(''), 'utf8'),
fileContent,
Buffer.from(footer, 'utf8'),
]);
}
}

View File

@@ -1 +1 @@
export const napCatVersion = '4.2.0';
export const napCatVersion = '4.2.25';

View File

@@ -6,7 +6,6 @@ import {
Peer,
PicElement,
PicSubType,
PicType,
RawMessage,
SendFileElement,
SendPicElement,
@@ -17,7 +16,7 @@ import path from 'path';
import fs from 'fs';
import fsPromises from 'fs/promises';
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
import * as fileType from 'file-type';
import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size';
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { RkeyManager } from '@/core/helper/rkey';
@@ -62,7 +61,7 @@ export class NTQQFileApi {
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
const fileMd5 = await calculateFileMD5(filePath);
const extOrEmpty = (await fileType.fileTypeFromFile(filePath))?.ext;
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf('.') === -1) {
@@ -142,7 +141,6 @@ export class NTQQFileApi {
}
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
const logger = this.core.context.logger;
let videoInfo = {
width: 1920,
height: 1080,
@@ -152,17 +150,17 @@ export class NTQQFileApi {
filePath,
};
try {
videoInfo = await getVideoInfo(filePath, logger);
videoInfo = await getVideoInfo(filePath, this.context.logger);
} catch (e) {
logger.logError.bind(logger)('获取视频信息失败,将使用默认值', e);
this.context.logger.logError('获取视频信息失败,将使用默认值', e);
}
let fileExt = 'mp4';
try {
const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
const tempExt = (await fileTypeFromFile(filePath))?.ext;
if (tempExt) fileExt = tempExt;
} catch (e) {
this.context.logger.logError.bind(logger)('获取文件类型失败', e);
this.context.logger.logError('获取文件类型失败', e);
}
const newFilePath = filePath + '.' + fileExt;
fs.copyFileSync(filePath, newFilePath);
@@ -183,7 +181,7 @@ export class NTQQFileApi {
ffmpeg(filePath)
.on('error', (err) => {
try {
logger.logDebug('获取视频封面失败,使用默认封面', err);
this.context.logger.logDebug('获取视频封面失败,使用默认封面', err);
if (diyThumbPath) {
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
@@ -193,7 +191,7 @@ export class NTQQFileApi {
resolve(thumbPath);
}
} catch (error) {
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
this.context.logger.logError('获取视频封面失败,使用默认封面失败', error);
}
})
.screenshots({
@@ -230,6 +228,7 @@ export class NTQQFileApi {
}
async createValidSendPttElement(pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
if (!silkPath) {
throw new Error('语音转换失败, 请检查语音文件是否正常');
@@ -239,8 +238,7 @@ export class NTQQFileApi {
throw new Error('文件异常大小为0');
}
if (converted) {
fsPromises.unlink(silkPath).then().catch(
(e) => this.context.logger.logError.bind(this.context.logger)('删除临时文件失败', e)
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e)
);
}
return {
@@ -454,7 +452,7 @@ export class NTQQFileApi {
}
}
} catch (error: any) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
this.context.logger.logError('获取rkey失败', error.message);
}
if (!rkeyData.online_rkey) {
@@ -464,7 +462,7 @@ export class NTQQFileApi {
rkeyData.private_rkey = tempRkeyData.private_rkey;
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
} catch (e) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
this.context.logger.logError('获取rkey失败 Fallback Old Mode', e);
}
}

View File

@@ -1,4 +1,4 @@
import { FriendV2 } from '@/core/types';
import { FriendRequest, FriendV2 } from '@/core/types';
import { BuddyListReqType, InstanceContext, NapCatCore } from '@/core';
import { LimitedHashTable } from '@/common/message-unique';
@@ -79,16 +79,10 @@ export class NTQQFriendApi {
return ret;
}
async handleFriendRequest(flag: string, accept: boolean) {
const data = flag.split('|');
if (data.length < 2) {
return;
}
const friendUid = data[0];
const reqTime = data[1];
async handleFriendRequest(notify: FriendRequest, accept: boolean) {
this.context.session.getBuddyService()?.approvalFriendRequest({
friendUid: friendUid,
reqTime: reqTime,
friendUid: notify.friendUid,
reqTime: notify.reqTime,
accept,
});
}

View File

@@ -1,6 +1,5 @@
import {
GeneralCallResult,
Group,
GroupMember,
NTGroupMemberRole,
NTGroupRequestOperateTypes,
@@ -8,6 +7,7 @@ import {
KickMemberV2Req,
MemberExtSourceType,
NapCatCore,
GroupNotify,
} from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique';
@@ -16,34 +16,22 @@ import { NTEventWrapper } from '@/common/event';
export class NTQQGroupApi {
context: InstanceContext;
core: NapCatCore;
groupCache: Map<string, Group> = new Map<string, Group>();
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
groups: Group[] = [];
essenceLRU = new LimitedHashTable<number, string>(1000);
session: any;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
async initApi() {
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
}
async initCache() {
this.groups = await this.getGroups();
for (const group of this.groups) {
this.groupCache.set(group.groupCode, group);
}
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
// process.pid 调试点
this.initCache().then().catch(e => this.context.logger.logError(e));
}
async getCoreAndBaseInfo(uids: string[]) {
return await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo',
'nodeStore',
uids,
);
async initCache() {
for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode).then().catch();
}
}
async fetchGroupEssenceList(groupCode: string) {
@@ -54,20 +42,22 @@ export class NTQQGroupApi {
pageLimit: 300,
}, pskey);
}
async getGroupShutUpMemberList(groupCode: string) {
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
return (await data)[1];
}
async clearGroupNotifiesUnreadCount(uk: boolean) {
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
async clearGroupNotifiesUnreadCount(doubt: boolean) {
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(doubt);
}
async setGroupAvatar(gc: string, filePath: string) {
return this.context.session.getGroupService().setHeader(gc, filePath);
async setGroupAvatar(groupCode: string, filePath: string) {
return this.context.session.getGroupService().setHeader(groupCode, filePath);
}
async getGroups(forced = false) {
async getGroups(forced: boolean = false) {
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupList',
'NodeIKernelGroupListener/onGroupListUpdate',
@@ -76,9 +66,9 @@ export class NTQQGroupApi {
return groupList;
}
async getGroupExtFE0Info(groupCode: string[], forced = true) {
async getGroupExtFE0Info(groupCodes: Array<string>, forced = true) {
return this.context.session.getGroupService().getGroupExt0xEF0Info(
groupCode,
groupCodes,
[],
{
bindGuildId: 1,
@@ -118,53 +108,42 @@ export class NTQQGroupApi {
);
}
async getGroup(groupCode: string, forced = false) {
let group = this.groupCache.get(groupCode.toString());
if (!group) {
try {
const groupList = await this.getGroups(forced);
if (groupList.length) {
groupList.forEach(g => {
this.groupCache.set(g.groupCode, g);
});
}
} catch (e) {
return undefined;
}
}
group = this.groupCache.get(groupCode.toString());
return group;
}
async getGroupMemberAll(groupCode: string, forced = false) {
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
}
async refreshGroupMemberCache(groupCode: string) {
try {
const members = await this.getGroupMemberAll(groupCode, true);
this.groupMemberCache.set(groupCode, members.result.infos);
} catch (e) {
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
}
return this.groupMemberCache;
}
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString();
const memberUinOrUidStr = memberUinOrUid.toString();
// 获取群成员缓存
let members = this.groupMemberCache.get(groupCodeStr);
if (!members) {
try {
members = await this.getGroupMembers(groupCodeStr);
this.groupMemberCache.set(groupCodeStr, members);
} catch (e) {
return null;
}
}
function getMember() {
let member: GroupMember | undefined;
if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr);
} else {
member = members!.get(memberUinOrUidStr);
}
return member;
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
}
const getMember = () => {
if (isNumeric(memberUinOrUidStr)) {
return Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr);
} else {
return members!.get(memberUinOrUidStr);
}
};
let member = getMember();
// 如果缓存中不存在该成员,尝试刷新缓存
if (!member) {
members = await this.getGroupMembers(groupCodeStr);
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
member = getMember();
}
return member;
@@ -174,26 +153,26 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode);
}
async CreatGroupFileFolder(groupCode: string, folderName: string) {
async creatGroupFileFolder(groupCode: string, folderName: string) {
return this.context.session.getRichMediaService().createGroupFolder(groupCode, folderName);
}
async DelGroupFile(groupCode: string, files: string[]) {
async delGroupFile(groupCode: string, files: Array<string>) {
return this.context.session.getRichMediaService().deleteGroupFile(groupCode, [102], files);
}
async DelGroupFileFolder(groupCode: string, folderId: string) {
async delGroupFileFolder(groupCode: string, folderId: string) {
return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
}
async addGroupEssence(GroupCode: string, msgId: string) {
async addGroupEssence(groupCode: string, msgId: string) {
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2,
guildId: '',
peerUid: GroupCode,
peerUid: groupCode,
}, msgId, 1, false);
const param = {
groupCode: GroupCode,
groupCode: groupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
};
@@ -204,9 +183,9 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().kickMemberV2(param);
}
async deleteGroupBulletin(GroupCode: string, noticeId: string) {
async deleteGroupBulletin(groupCode: string, noticeId: string) {
const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, psKey, noticeId);
return this.context.session.getGroupService().deleteGroupBulletin(groupCode, psKey, noticeId);
}
async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) {
@@ -217,65 +196,42 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().quitGroupV2(param);
}
async removeGroupEssenceBySeq(GroupCode: string, msgRandom: string, msgSeq: string) {
async removeGroupEssenceBySeq(groupCode: string, msgRandom: string, msgSeq: string) {
const param = {
groupCode: GroupCode,
groupCode: groupCode,
msgRandom: parseInt(msgRandom),
msgSeq: parseInt(msgSeq),
};
return this.context.session.getGroupService().removeGroupEssence(param);
}
async removeGroupEssence(GroupCode: string, msgId: string) {
async removeGroupEssence(groupCode: string, msgId: string) {
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2,
guildId: '',
peerUid: GroupCode,
peerUid: groupCode,
}, msgId, 1, false);
const param = {
groupCode: GroupCode,
groupCode: groupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
};
return this.context.session.getGroupService().removeGroupEssence(param);
}
async getSingleScreenNotifies(doubt: boolean, num: number) {
async getSingleScreenNotifies(doubt: boolean, count: number) {
const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
[
doubt,
'',
num,
count,
],
);
return notifies;
}
async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
const Listener = this.core.eventWrapper.registerListen(
'NodeIKernelGroupListener/onMemberInfoChange',
(params, _, members) => params === GroupCode && members.size > 0,
1,
forced ? 5000 : 250,
);
const retData = await (
this.core.eventWrapper
.createEventFunction('NodeIKernelGroupService/getMemberInfo')
)!(GroupCode, [uid], forced);
if (retData.result !== 0) {
throw new Error(`${retData.errMsg}`);
}
const result = await Listener as unknown;
let member: GroupMember | undefined;
if (Array.isArray(result) && result?.[2] instanceof Map) {
const members = result[2] as Map<string, GroupMember>;
member = members.get(uid);
}
return member;
}
async searchGroup(groupCode: string) {
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelSearchService/searchGroup',
@@ -294,178 +250,89 @@ export class NTQQGroupApi {
return ret.groupInfos.find(g => g.groupCode === groupCode);
}
async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) {
async getGroupMemberEx(groupCode: string, uid: string, forced: boolean = false, retry: number = 2) {
const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => {
return eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getMemberInfo',
'NodeIKernelGroupListener/onMemberInfoChange',
[GroupCode, [uid], forced],
[groupCode, [uid], forced],
(ret) => ret.result === 0,
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
1,
forced ? 2500 : 250
);
}, this.core.eventWrapper, GroupCode, uid, forced);
}, this.core.eventWrapper, groupCode, uid, forced);
if (data && data[3] instanceof Map && data[3].has(uid)) {
return data[3].get(uid);
}
if (retry > 0) {
const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined;
const trydata = await this.getGroupMemberEx(groupCode, uid, true, retry - 1) as GroupMember | undefined;
if (trydata) return trydata;
}
return undefined;
}
async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{
infos: Map<string, GroupMember>;
finish: boolean;
hasNext: boolean | undefined;
}> {
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
.catch(() => { });
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
let resMode2;
if (modeListener) {
const ret = (await once)?.[0];
if (ret) {
resMode2 = ret;
}
}
this.context.session.getGroupService().destroyMemberListScene(sceneId);
return {
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
finish: result.result.finish,
hasNext: resMode2?.hasNext,
};
async getGroupFileCount(groupCodes: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(groupCodes);
}
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
infos: Map<string, GroupMember>;
finish: boolean;
hasNext: boolean | undefined;
listenerMode: boolean;
}> {
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
.catch(() => { });
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
let resMode2;
if (result.result.finish && result.result.infos.size === 0) {
const ret = (await once)?.[0];
if (ret) {
resMode2 = ret;
}
}
this.context.session.getGroupService().destroyMemberListScene(sceneId);
return {
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
finish: result.result.finish,
hasNext: resMode2?.hasNext,
listenerMode: resMode2?.hasNext !== undefined
};
}
async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise<Map<string, GroupMember>> {
if (no_cache) {
return (await this.getGroupMemberAll(groupQQ, true)).result.infos;
}
let res = await this.GetGroupMembersV3(groupQQ, num);
let ret = res.infos;
if (res.infos.size === 0 && !res.listenerMode) {
res = await this.GetGroupMembersV3(groupQQ, num);
ret = res.infos;
}
if (res.infos.size === 0) {
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
}
return ret;
}
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const groupService = this.context.session.getGroupService();
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
const result = await groupService.getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`);
return result.result.infos;
}
async getGroupFileCount(group_ids: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids);
}
async getArkJsonGroupShare(GroupCode: string) {
async getArkJsonGroupShare(groupCode: string) {
const ret = await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelGroupService/getGroupRecommendContactArkJson',
GroupCode,
groupCode,
) as GeneralCallResult & { arkJson: string };
return ret.arkJson;
}
//需要异常处理
async uploadGroupBulletinPic(GroupCode: string, imageurl: string) {
async uploadGroupBulletinPic(groupCode: string, imageurl: string) {
const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl);
return this.context.session.getGroupService().uploadGroupBulletinPic(groupCode, _Pskey, imageurl);
}
async handleGroupRequest(flag: string, operateType: NTGroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|');
const groupCode = flagitem[0];
const seq = flagitem[1];
const type = parseInt(flagitem[2]);
async handleGroupRequest(notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) {
return this.context.session.getGroupService().operateSysNotify(
false,
{
operateType: operateType,
targetMsg: {
seq: seq, // 通知序列号
type: type,
groupCode: groupCode,
seq: notify.seq, // 通知序列号
type: notify.type,
groupCode: notify.group.groupCode,
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
},
});
}
async quitGroup(groupQQ: string) {
return this.context.session.getGroupService().quitGroup(groupQQ);
async quitGroup(groupCode: string) {
return this.context.session.getGroupService().quitGroup(groupCode);
}
async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
return this.context.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason);
async kickMember(groupCode: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
return this.context.session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason);
}
async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return this.context.session.getGroupService().setMemberShutUp(groupQQ, memList);
return this.context.session.getGroupService().setMemberShutUp(groupCode, memList);
}
async banGroup(groupQQ: string, shutUp: boolean) {
return this.context.session.getGroupService().setGroupShutUp(groupQQ, shutUp);
async banGroup(groupCode: string, shutUp: boolean) {
return this.context.session.getGroupService().setGroupShutUp(groupCode, shutUp);
}
async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName);
async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
return this.context.session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName);
}
async setMemberRole(groupQQ: string, memberUid: string, role: NTGroupMemberRole) {
return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role);
async setMemberRole(groupCode: string, memberUid: string, role: NTGroupMemberRole) {
return this.context.session.getGroupService().modifyMemberRole(groupCode, memberUid, role);
}
async setGroupName(groupQQ: string, groupName: string) {
return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false);
async setGroupName(groupCode: string, groupName: string) {
return this.context.session.getGroupService().modifyGroupName(groupCode, groupName, false);
}
async publishGroupBulletin(groupQQ: string, content: string, picInfo: {
async publishGroupBulletin(groupCode: string, content: string, picInfo: {
id: string,
width: number,
height: number
@@ -479,11 +346,11 @@ export class NTQQGroupApi {
pinned: pinned,
confirmRequired: confirmRequired,
};
return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data);
return this.context.session.getGroupService().publishGroupBulletin(groupCode, psKey!, data);
}
async getGroupRemainAtTimes(GroupCode: string) {
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
async getGroupRemainAtTimes(groupCode: string) {
return this.context.session.getGroupService().getGroupRemainAtTimes(groupCode);
}
async getMemberExtInfo(groupCode: string, uin: string) {

View File

@@ -31,7 +31,7 @@ export class NTQQPacketApi {
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
.then()
.catch((err) => {
this.logger.logError.bind(this.core.context.logger);
this.logger.logError(err);
this.errStack.push(err);
});
}

View File

@@ -1,25 +0,0 @@
import { InstanceContext, NapCatCore } from '..';
export class NTQQMusicSignApi {
context: InstanceContext;
core: NapCatCore;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
//外域名不行得走qgroup中转
//https://proxy.gtimg.cn/tx_tls_gate=y.qq.com/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg
//可外域名
//https://pic.ugcimg.cn/500955bdd6657ecc8e82e02d2df06800/jpg1
//QQ音乐gtimg接口
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
//还有一处公告上传可以上传高质量图片 持久为qq域名
}

View File

@@ -2,6 +2,8 @@ import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
import { RequestUtil } from '@/common/request';
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
import { solveAsyncProblem } from '@/common/helper';
import { promisify } from 'node:util';
import { LRUCache } from '@/common/lru-cache';
export class NTQQUserApi {
context: InstanceContext;
@@ -11,13 +13,15 @@ export class NTQQUserApi {
this.context = context;
this.core = core;
}
//self_tind格式
async createUidFromTinyId(tinyId: string) {
return this.context.session.getMsgService().createUidFromTinyId(this.core.selfInfo.uin, tinyId);
}
async getStatusByUid(uid: string) {
return this.context.session.getProfileService().getStatus(uid);
async getCoreAndBaseInfo(uids: string[]) {
return await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo',
'nodeStore',
uids,
);
}
// 默认获取自己的 type = 2 获取别人 type = 1
async getProfileLike(uid: string, start: number, count: number, type: number = 2) {
return this.context.session.getProfileLikeService().getBuddyProfileLike({
@@ -161,35 +165,51 @@ export class NTQQUserApi {
if (!skey) {
throw new Error('SKey is Empty');
}
return skey;
}
//后期改成流水线处理
async getUidByUinV2(Uin: string) {
let uid = (await this.context.session.getGroupService().getUidByUins([Uin])).uids.get(Uin);
if (uid) return uid;
uid = (await this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin);
if (uid) return uid;
uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
if (uid) return uid;
const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换
if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid;
//if (uid) return uid;
return uid;
if (!Uin) {
return '';
}
const services = [
() => this.context.session.getUixConvertService().getUid([Uin]).then((data) => data.uidInfo.get(Uin)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined),
() => this.context.session.getGroupService().getUidByUins([Uin]).then((data) => data.uids.get(Uin)).catch(() => undefined),
() => this.getUserDetailInfoByUin(Uin).then((data) => data.detail.uid).catch(() => undefined),
];
let uid: string | undefined = undefined;
for (const service of services) {
uid = await service();
if (uid && uid.indexOf('*') == -1 && uid !== '') {
break;
}
}
return uid ?? '';
}
//后期改成流水线处理
async getUinByUidV2(Uid: string) {
let uin = (await this.context.session.getGroupService().getUinByUids([Uid])).uins.get(Uid);
if (uin) return uin;
uin = (await this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid);
if (uin) return uin;
uin = (await this.context.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid);
if (uin) return uin;
uin = (await this.core.apis.FriendApi.getBuddyIdMap(true)).getKey(Uid);
if (uin) return uin;
uin = (await this.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
return uin;
if (!Uid) {
return '0';
}
const services = [
() => this.context.session.getUixConvertService().getUin([Uid]).then((data) => data.uinInfo.get(Uid)).catch(() => undefined),
() => this.context.session.getGroupService().getUinByUids([Uid]).then((data) => data.uins.get(Uid)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined),
() => this.core.apis.FriendApi.getBuddyIdMap(true).then((data) => data.getKey(Uid)).catch(() => undefined),
() => this.getUserDetailInfo(Uid).then((data) => data.uin).catch(() => undefined),
];
let uin: string | undefined = undefined;
for (const service of services) {
uin = await service();
if (uin && uin !== '0' && uin !== '') {
break;
}
}
return uin ?? '0';
}
async getRecentContactListSnapShot(count: number) {

View File

@@ -366,50 +366,4 @@ export class NTQQWebApi {
return post;
}
async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) {
const img_size = statSync(path).size;
const img_name = basename(path);
let seq = 0;
let offset = 0;
const GTK = this.getBknFromSKey(pskey);
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
const stream = createReadStream(path, { highWaterMark: slice_size });
for await (const chunk of stream) {
const end = Math.min(offset + chunk.length, img_size);
const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`;
const formData = await RequestUtil.createFormData(boundary, path);
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`;
const body = {
uin: uin,
appid: "qun",
session: session,
offset: offset,
data: formData,
checksum: "",
check_type: 0,
retry: 0,
seq: seq,
end: end,
cmd: "FileUpload",
slice_size: slice_size,
"biz_req.iUploadType": 0
};
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
"Cookie": cookie,
"Content-Type": `multipart/form-data; boundary=${boundary}`
});
offset += chunk.length;
seq++;
}
}
async uploadQunAlbum(path: string, albumId: string, group: string, skey: string, pskey: string, uin: string) {
const session = (await this.createQunAlbumSession(group, albumId, group, path, skey, pskey, uin) as { data: { session: string } }).data.session;
return await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 1024 * 1024);
}
}

View File

@@ -98,5 +98,17 @@
"6.9.61-29927": {
"appid": 537255836,
"qua": "V1_MAC_NQ_6.9.61_29927_GW_B"
},
"9.9.17-30366": {
"appid": 537258389,
"qua": "V1_WIN_NQ_9.9.17_30366_GW_B"
},
"3.2.15-30366": {
"appid": 537258413,
"qua": "V1_LNX_NQ_3.2.15_30366_GW_B"
},
"6.9.62-30366": {
"appid": 537258401,
"qua": "V1_MAC_NQ_6.9.62_30366_GW_B"
}
}

View File

@@ -102,5 +102,25 @@
"6.9.61-29927-arm64": {
"send": "4038740",
"recv": "403AF58"
},
"9.9.17-30366-x64": {
"send": "39AB0B0",
"recv": "39AF4E4"
},
"3.2.15-30366-x64": {
"send": "A402380",
"recv": "A405C80"
},
"3.2.15-30366-arm64": {
"send": "70C3FA8",
"recv": "70C77E0"
},
"6.9.62-30366-x64": {
"send": "4669760",
"recv": "466BFCC"
},
"6.9.62-30366-arm64": {
"send": "4189770",
"recv": "418BF88"
}
}

View File

@@ -1,14 +1,14 @@
import * as fileType from 'file-type';
import { fileTypeFromFile } from 'file-type';
import { PicType } from '../types';
export async function getFileTypeForSendType(picPath: string): Promise<PicType> {
const fileTypeResult = (await fileType.fileTypeFromFile(picPath))?.ext ?? 'jpg';
const fileTypeResult = (await fileTypeFromFile(picPath))?.ext ?? 'jpg';
const picTypeMap: { [key: string]: PicType } = {
'webp': PicType.NEWPIC_WEBP,
//'webp': PicType.NEWPIC_WEBP,
'gif': PicType.NEWPIC_GIF,
'png': PicType.NEWPIC_APNG,
'jpg': PicType.NEWPIC_JPEG,
'jpeg': PicType.NEWPIC_JPEG,
'bmp': PicType.NEWPIC_BMP,
// 'png': PicType.NEWPIC_APNG,
// 'jpg': PicType.NEWPIC_JPEG,
// 'jpeg': PicType.NEWPIC_JPEG,
// 'bmp': PicType.NEWPIC_BMP,
};
return picTypeMap[fileTypeResult] ?? PicType.NEWPIC_JPEG;
}

View File

@@ -27,7 +27,6 @@ export class RkeyManager {
await this.refreshRkey();
} catch (e) {
throw new Error(`获取rkey失败: ${e}`);//外抛
//this.logger.logError.bind(this.logger)('获取rkey失败', e);
}
}
return this.rkeyData;
@@ -50,7 +49,7 @@ export class RkeyManager {
expired_time: temp.expired_time
};
} catch (e) {
this.logger.logError.bind(this.logger)(`[Rkey] Get Rkey ${url} Error `, e);
this.logger.logError(`[Rkey] Get Rkey ${url} Error `, e);
//是否为最后一个url
if (url === this.serverUrl[this.serverUrl.length - 1]) {
throw new Error(`获取rkey失败: ${e}`);//外抛

View File

@@ -24,10 +24,10 @@ import path from 'node:path';
import fs from 'node:fs';
import { hostname, systemName, systemVersion } from '@/common/system';
import { NTEventWrapper } from '@/common/event';
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
import { GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
import { NapCatConfigLoader } from '@/core/helper/config';
import os from 'node:os';
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
export * from './wrapper';
@@ -127,7 +127,7 @@ export class NapCatCore {
await api.initApi();
}
}
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
this.initNapCatCoreListeners().then().catch((e) => this.context.logger.logError(e));
this.context.logger.setFileLogEnabled(
this.configLoader.configData.fileLog,
@@ -154,7 +154,7 @@ export class NapCatCore {
const msgListener = new NodeIKernelMsgListener();
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
this.context.logger.logError.bind(this.context.logger)('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.selfInfo.online = false;
};
msgListener.onRecvMsg = (msgs) => {
@@ -163,7 +163,6 @@ export class NapCatCore {
msgListener.onAddSendMsg = (msg) => {
this.context.logger.logMessage(msg, this.selfInfo);
};
//await sleep(2500);
this.context.session.getMsgService().addKernelMsgListener(
proxiedListenerOf(msgListener, this.context.logger),
);
@@ -185,92 +184,6 @@ export class NapCatCore {
this.context.session.getProfileService().addKernelProfileListener(
proxiedListenerOf(profileListener, this.context.logger),
);
// 群相关
const groupListener = new NodeIKernelGroupListener();
groupListener.onGroupListUpdate = (updateType, groupList) => {
// console.log("onGroupListUpdate", updateType, groupList)
groupList.map(g => {
const existGroup = this.apis.GroupApi.groupCache.get(g.groupCode);
//群成员数量变化 应该刷新缓存
if (existGroup && g.memberCount === existGroup.memberCount) {
Object.assign(existGroup, g);
} else {
this.apis.GroupApi.groupCache.set(g.groupCode, g);
// 获取群成员
}
const sceneId = this.context.session.getGroupService().createMemberListScene(g.groupCode, 'groupMemberList_MainWindow');
this.context.session.getGroupService().getNextMemberList(sceneId, undefined, 3000).then( /* r => {
// console.log(`get group ${g.groupCode} members`, r);
// r.result.infos.forEach(member => {
// });
// groupMembers.set(g.groupCode, r.result.infos);
} */);
this.context.session.getGroupService().destroyMemberListScene(sceneId);
});
};
groupListener.onMemberListChange = (arg) => {
// TODO: 应该加一个内部自己维护的成员变动callback用于判断成员变化通知
const groupCode = arg.sceneId.split('_')[0];
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
arg.infos.forEach((member, uid) => {
//console.log('onMemberListChange', member);
const existMember = existMembers.get(uid);
if (existMember) {
Object.assign(existMember, member);
} else {
existMembers.set(uid, member);
}
//移除成员
if (member.isDelete) {
existMembers.delete(uid);
}
});
} else {
this.apis.GroupApi.groupMemberCache.set(groupCode, arg.infos);
}
};
groupListener.onMemberInfoChange = (groupCode, dataSource, members) => {
if (dataSource === DataSource.LOCAL && members.get(this.selfInfo.uid)?.isDelete) {
// 自身退群或者被踢退群 5s用于Api操作 之后不再出现
setTimeout(() => {
this.apis.GroupApi.groupCache.delete(groupCode);
}, 5000);
}
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode);
if (existMembers) {
members.forEach((member, uid) => {
const existMember = existMembers.get(uid);
if (existMember) {
// 检查管理变动
member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember);
// 更新成员信息
Object.assign(existMember, member);
} else {
existMembers.set(uid, member);
}
//移除成员
if (member.isDelete) {
existMembers.delete(uid);
}
});
} else {
this.apis.GroupApi.groupMemberCache.set(groupCode, members);
}
};
this.context.session.getGroupService().addKernelGroupListener(
proxiedListenerOf(groupListener, this.context.logger),
);
}
checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean {
if (memberNew.role !== memberOld?.role) {
this.context.logger.logDebug(`${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,18 @@
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const GroupAdminExtra = {
adminUid: ProtoField(1, ScalarType.STRING),
isPromote: ProtoField(2, ScalarType.BOOL),
};
export const GroupAdminBody = {
extraDisable: ProtoField(1, () => GroupAdminExtra),
extraEnable: ProtoField(2, () => GroupAdminExtra),
};
export const GroupAdmin = {
groupUin: ProtoField(1, ScalarType.UINT32),
flag: ProtoField(2, ScalarType.UINT32),
isPromote: ProtoField(3, ScalarType.BOOL),
body: ProtoField(4, () => GroupAdminBody),
};

View File

@@ -54,15 +54,23 @@ export const PushMsg = {
generalFlag: ProtoField(9, ScalarType.INT32, true),
};
export const GroupChangeInfo = {
operator: ProtoField(1, () => GroupChangeOperator, true),
};
export const GroupChangeOperator = {
operatorUid: ProtoField(1, ScalarType.STRING, true),
};
export const GroupChange = {
groupUin: ProtoField(1, ScalarType.UINT32),
flag: ProtoField(2, ScalarType.UINT32),
memberUid: ProtoField(3, ScalarType.STRING, true),
decreaseType: ProtoField(4, ScalarType.UINT32),
operatorUid: ProtoField(5, ScalarType.STRING, true),
operatorInfo: ProtoField(5, ScalarType.BYTES, true),
increaseType: ProtoField(6, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.BYTES, true),
}
};
export const PushMsgBody = {
responseHead: ProtoField(1, () => ResponseHead),

View File

@@ -163,7 +163,7 @@ export interface NodeIKernelGroupService {
getGroupPortrait(): void;
modifyGroupName(groupCode: string, groupName: string, arg: false): void;
modifyGroupName(groupCode: string, groupName: string, isNormalMember: boolean): Promise<GeneralCallResult>;
modifyGroupRemark(groupCode: string, remark: string): void;
@@ -187,13 +187,13 @@ export interface NodeIKernelGroupService {
destroyGroup(groupCode: string): void;
getSingleScreenNotifies(doubted: boolean, start_seq: string, num: number): Promise<GeneralCallResult>;
getSingleScreenNotifies(doubt: boolean, startSeq: string, count: number): Promise<GeneralCallResult>;
clearGroupNotifies(groupCode: string): void;
getGroupNotifiesUnreadCount(unknown: boolean): Promise<GeneralCallResult>;
getGroupNotifiesUnreadCount(doubt: boolean): Promise<GeneralCallResult>;
clearGroupNotifiesUnreadCount(unknown: boolean): void;
clearGroupNotifiesUnreadCount(doubt: boolean): void;
operateSysNotify(
doubt: boolean,

View File

@@ -9,9 +9,9 @@ export interface NodeIKernelProfileService {
getRelationFlag(callfrom: string, uids: string[]): Promise<Map<string, any>>;
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string, string>>;
getUidByUin(callfrom: string, uin: Array<string>): Map<string, string>;
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string, string>>;
getUinByUid(callfrom: string, uid: Array<string>): Map<string, string>;
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>;

View File

@@ -29,6 +29,7 @@ export interface TextElement {
}
export interface FaceElement {
pokeType?: number;
faceIndex: number;
faceType: FaceType;
faceText?: string;

View File

@@ -1,6 +1,16 @@
//LiteLoader需要提供部分IPC接口以便于其他插件调用
const { ipcMain } = require('electron');
const napcat = require('./napcat.cjs');
const { shell } = require('electron');
ipcMain.handle('napcat_get_webtoken', async (event, arg) => {
return napcat.NCgetWebUiUrl();
});
ipcMain.on('open_external_url', (event, url) => {
shell.openExternal(url);
});
ipcMain.handle('napcat_get_reactweb', async (event, arg) => {
let url = new URL(await napcat.NCgetWebUiUrl());
let port = url.port;
let token = url.searchParams.get('token');
return `https://napcat.152710.xyz/web_login?back=http://127.0.0.1:${port}&token=${token}`;
});

View File

@@ -58,7 +58,7 @@ export async function NCoreInitFramework(
await loaderObject.core.initCore();
//启动WebUi
InitWebUi(logger, pathWrapper).then().catch(logger.logError.bind(logger));
InitWebUi(logger, pathWrapper).then().catch(e => logger.logError(e));
//初始化LLNC的Onebot实现
await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot();
}

View File

@@ -1,10 +1,14 @@
const { contextBridge } = require('electron');
const { ipcRenderer } = require('electron');
const { contextBridge, ipcRenderer } = require('electron');
const napcat = {
getWebUiUrl: async () => {
return ipcRenderer.invoke('napcat_get_webtoken');
},
openExternalUrl: async (url) => {
ipcRenderer.send('open_external_url', url);
},
getWebUiUrlReact: async () => {
return ipcRenderer.invoke('napcat_get_reactweb');
}
};
// 在window对象下导出只读对象
contextBridge.exposeInMainWorld('napcat', napcat);

View File

@@ -1,27 +1,20 @@
export const onSettingWindowCreated = async (view) => {
// view.style.width = "100%";
// view.style.height = "100%";
// //添加iframe
// const iframe = document.createElement("iframe");
// iframe.src = await window.napcat.getWebUiUrl();
// iframe.width = "100%";
// iframe.height = "100%";
// iframe.style.border = "none";
// //去掉iframe滚动条
// //iframe.scrolling = "no";
// //有滚动条何尝不是一种美
// view.appendChild(iframe);
let webui = await window.napcat.getWebUiUrl();
let webuiReact = await window.napcat.getWebUiUrlReact();
view.innerHTML = `
<setting-section data-title="">
<setting-panel>
<setting-list data-direction="column">
<setting-item>
<setting-button data-type="primary" class="nc_openwebui">打开配置页面</setting-button>
<setting-button data-type="primary" class="nc_openwebui">在QQ内打开配置页面(VUE)</setting-button>
<setting-button data-type="primary" class="nc_openwebui_ex">在默认浏览器打开配置页面(VUE)</setting-button>
</setting-item>
<setting-item>
<setting-button data-type="primary" class="nc_openwebui_ex_react">在默认浏览器打开配置页面(React)</setting-button>
</setting-item>
<setting-item>
<div>
<setting-text>WebUi远程地址可以点击下方复制哦~</setting-text>
<setting-text class="nc_webui">WebUi</setting-text>
</div>
</setting-item>
@@ -29,8 +22,27 @@ export const onSettingWindowCreated = async (view) => {
</setting-panel>
</setting-section>
`;
view.querySelector('.nc_openwebui').addEventListener('click', () => {
window.open(webui, '_blank');
});
view.querySelector('.nc_openwebui_ex').addEventListener('click', () => {
window.napcat.openExternalUrl(webui);
});
view.querySelector('.nc_openwebui_ex_react').addEventListener('click', () => {
window.napcat.openExternalUrl(webuiReact);
});
view.querySelector('.nc_webui').innerText = webui;
// 添加点击复制功能
view.querySelector('.nc_webui').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(webui);
alert('WebUi URL 已复制到剪贴板');
} catch (err) {
console.error('复制到剪贴板失败: ', err);
}
});
};

View File

@@ -42,7 +42,7 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
if (this.payloadSchema) {
this.validate = new Ajv({ allowUnionTypes: true }).compile(this.payloadSchema);
this.validate = new Ajv({ allowUnionTypes: true, useDefaults: true }).compile(this.payloadSchema);
}
if (this.validate && !this.validate(payload)) {
const errors = this.validate.errors as ErrorObject[];

View File

@@ -1,17 +1,13 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
rawData: { type: 'string' },
brief: { type: 'string' },
},
required: ['brief', 'rawData'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
rawData: Type.String(),
brief: Type.String(),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class CreateCollection extends OneBotAction<Payload, any> {
actionName = ActionName.CreateCollection;

View File

@@ -1,23 +1,19 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Type, Static } from '@sinclair/typebox';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
const SchemaData = {
type: 'object',
properties: {
count: { type: ['number', 'string'] },
},
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
count: Type.Union([Type.Number(), Type.String()], { default: 48 }),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class FetchCustomFace extends OneBotAction<Payload, string[]> {
actionName = ActionName.FetchCustomFace;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
//48 可能正好是QQ需要的一个页面的数量 Tagged Mlikiowa
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+(payload.count ?? 48));
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
return ret.emojiInfoList.map(e => e.url);
}
}

View File

@@ -1,32 +1,27 @@
//getMsgEmojiLikesList
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Type, Static } from '@sinclair/typebox';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { MessageUnique } from '@/common/message-unique';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: 'string' },
group_id: { type: 'string' },
emojiId: { type: 'string' },
emojiType: { type: 'string' },
message_id: { type: ['string', 'number'] },
count: { type: ['string', 'number'] },
},
required: ['emojiId', 'emojiType', 'message_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()]),
emojiId: Type.Union([Type.Number(), Type.String()]),
emojiType: Type.Union([Type.Number(), Type.String()]),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class FetchEmojiLike extends OneBotAction<Payload, any> {
actionName = ActionName.FetchEmojiLike;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
if (!msgIdPeer) throw new Error('消息不存在');
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
return await this.core.apis.MsgApi.getMsgEmojiLikesList(msgIdPeer.Peer, msg.msgSeq, payload.emojiId, payload.emojiType, +(payload.count ?? 20));
return await this.core.apis.MsgApi.getMsgEmojiLikesList(
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), +payload.count
);
}
}

View File

@@ -1,11 +1,17 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
export class FetchUserProfileLike extends OneBotAction<{ qq: number }, any> {
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class FetchUserProfileLike extends OneBotAction<Payload, any> {
actionName = ActionName.FetchUserProfileLike;
async _handle(payload: { qq: number }) {
if (!payload.qq) throw new Error('qq is required');
return await this.core.apis.UserApi.getUidByUinV2(payload.qq.toString());
async _handle(payload: Payload) {
return await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
}
}

View File

@@ -1,18 +1,14 @@
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
import { Type, Static } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
chat_type: { type: ['number', 'string'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
chat_type: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
interface GetAiCharactersResponse {
type: string;
@@ -28,7 +24,7 @@ export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharac
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +(payload.chat_type ?? 1) as AIVoiceChatType);
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +payload.chat_type as AIVoiceChatType);
return rawList?.map((item) => ({
type: item.category,
characters: item.voices.map((voice) => ({

View File

@@ -1,23 +1,19 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
category: { type: ['number', 'string'] },
count: { type: ['number', 'string'] },
},
required: ['category', 'count'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
category: Type.Union([Type.Number(), Type.String()]),
count: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GetCollectionList extends OneBotAction<Payload, any> {
actionName = ActionName.GetCollectionList;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.CollectionApi.getAllCollection(parseInt(payload.category.toString()), +(payload.count ?? 1));
return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count);
}
}

View File

@@ -1,32 +1,36 @@
import { GroupNotifyMsgStatus } from '@/core';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Notify } from '@/onebot/types';
interface OB11GroupRequestNotify {
group_id: number,
user_id: number,
flag: string
}
export default class GetGroupAddRequest extends OneBotAction<null, OB11GroupRequestNotify[] | null> {
export default class GetGroupAddRequest extends OneBotAction<null, Notify[] | null> {
actionName = ActionName.GetGroupIgnoreAddRequest;
async _handle(payload: null): Promise<OB11GroupRequestNotify[] | null> {
const ignoredNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 10);
const retData: any = {
join_requests: await Promise.all(
ignoredNotifies
async _handle(payload: null): Promise<Notify[] | null> {
const NTQQUserApi = this.core.apis.UserApi;
const NTQQGroupApi = this.core.apis.GroupApi;
const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10);
const retData: Notify[] = [];
const notifyPromises = ignoredNotifies
.filter(notify => notify.type === 7)
.map(async SSNotify => ({
request_id: SSNotify.seq,
requester_uin: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1?.uid),
requester_nick: SSNotify.user1?.nickName,
group_id: SSNotify.group?.groupCode,
.map(async SSNotify => {
const invitorUin = SSNotify.user1?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
const actorUin = SSNotify.user2?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user2.uid) : 0;
retData.push({
request_id: +SSNotify.seq,
invitor_uin: invitorUin,
invitor_nick: SSNotify.user1?.nickName,
group_id: +SSNotify.group?.groupCode,
message: SSNotify?.postscript,
group_name: SSNotify.group?.groupName,
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
actor: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2?.uid) || 0,
}))),
};
actor: actorUin,
requester_nick: SSNotify.user1?.nickName,
});
});
await Promise.all(notifyPromises);
return retData;
}

View File

@@ -1,16 +1,11 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GetGroupInfoEx extends OneBotAction<Payload, any> {
actionName = ActionName.GetGroupInfoEx;

View File

@@ -1,47 +1,37 @@
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { MiniAppInfo, MiniAppInfoHelper } from "@/core/packet/utils/helper/miniAppHelper";
import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from "@/core/packet/entities/miniApp";
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['bili', 'weibo']
},
title: { type: 'string' },
desc: { type: 'string' },
picUrl: { type: 'string' },
jumpUrl: { type: 'string' },
iconUrl: { type: 'string' },
sdkId: { type: 'string' },
appId: { type: 'string' },
scene: { type: ['number', 'string'] },
templateType: { type: ['number', 'string'] },
businessType: { type: ['number', 'string'] },
verType: { type: ['number', 'string'] },
shareType: { type: ['number', 'string'] },
versionId: { type: 'string' },
withShareTicket: { type: ['number', 'string'] },
rawArkData: { type: ['boolean', 'string'] }
},
oneOf: [
{
required: ['type', 'title', 'desc', 'picUrl', 'jumpUrl']
},
{
required: [
'title', 'desc', 'picUrl', 'jumpUrl',
'iconUrl', 'appId', 'scene', 'templateType', 'businessType',
'verType', 'shareType', 'versionId', 'withShareTicket'
]
}
]
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Union([
Type.Object({
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')]),
title: Type.String(),
desc: Type.String(),
picUrl: Type.String(),
jumpUrl: Type.String(),
rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
}),
Type.Object({
title: Type.String(),
desc: Type.String(),
picUrl: Type.String(),
jumpUrl: Type.String(),
iconUrl: Type.String(),
appId: Type.String(),
scene: Type.Union([Type.Number(), Type.String()]),
templateType: Type.Union([Type.Number(), Type.String()]),
businessType: Type.Union([Type.Number(), Type.String()]),
verType: Type.Union([Type.Number(), Type.String()]),
shareType: Type.Union([Type.Number(), Type.String()]),
versionId: Type.String(),
sdkId: Type.String(),
withShareTicket: Type.Union([Type.Number(), Type.String()]),
rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
})
]);
type Payload = Static<typeof SchemaData>;
export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
data: MiniAppData | MiniAppRawData
@@ -57,7 +47,7 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
picUrl: payload.picUrl,
jumpUrl: payload.jumpUrl
} as MiniAppReqCustomParams;
if (payload.type) {
if ('type' in payload) {
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
} else {
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload;

View File

@@ -1,34 +1,28 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Type, Static } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
start: { type: ['number', 'string'] },
count: { type: ['number', 'string'] },
type: { type: ['number', 'string'] },
},
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
start: Type.Union([Type.Number(), Type.String()], { default: 0 }),
count: Type.Union([Type.Number(), Type.String()], { default: 10 }),
type: Type.Union([Type.Number(), Type.String()], { default: 2 }),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GetProfileLike extends OneBotAction<Payload, any> {
actionName = ActionName.GetProfileLike;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const start = payload.start ? Number(payload.start) : 0;
const count = payload.count ? Number(payload.count) : 10;
const type = payload.count ? Number(payload.count) : 2;
const user_uid =
this.core.selfInfo.uin === payload.user_id || !payload.user_id ?
this.core.selfInfo.uid :
await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const ret = await this.core.apis.UserApi.getProfileLike(user_uid ?? this.core.selfInfo.uid, start, count, type);
const ret = await this.core.apis.UserApi.getProfileLike(user_uid ?? this.core.selfInfo.uid, +payload.start, +payload.count, +payload.type);
const listdata = ret.info.userLikeInfos[0].voteInfo.userInfos;
for (const item of listdata) {
item.uin = parseInt((await this.core.apis.UserApi.getUinByUidV2(item.uid)) || '');
item.uin = +((await this.core.apis.UserApi.getUinByUidV2(item.uid)) ?? '');
}
return ret.info.userLikeInfos[0].voteInfo;
}

View File

@@ -1,8 +1,7 @@
import { ActionName } from '@/onebot/action/router';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
export class GetRkey extends GetPacketStatusDepends<null, Array<any>> {
export class GetRkey extends GetPacketStatusDepends<void, Array<any>> {
actionName = ActionName.GetRkey;
async _handle() {

View File

@@ -4,7 +4,7 @@ import { ActionName } from '@/onebot/action/router';
export class GetRobotUinRange extends OneBotAction<void, Array<any>> {
actionName = ActionName.GetRobotUinRange;
async _handle(payload: void) {
async _handle() {
return await this.core.apis.UserApi.getRobotUinRange();
}
}

View File

@@ -1,16 +1,12 @@
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
// no_cache get时传字符串
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
},
required: ['user_id'],
} as const satisfies JSONSchema;
import { Static, Type } from '@sinclair/typebox';
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class GetUserStatus extends GetPacketStatusDepends<Payload, { status: number; ext_status: number; } | undefined> {
actionName = ActionName.GetUserStatus;

View File

@@ -1,25 +1,21 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { checkFileExist, uri2local } from '@/common/file';
import { checkFileExist, uriToLocalFile } from '@/common/file';
import fs from 'fs';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
image: { type: 'string' },
},
required: ['image'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
image: Type.String(),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class OCRImage extends OneBotAction<Payload, any> {
actionName = ActionName.OCRImage;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.image));
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image));
if (!success) {
throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`);
}
@@ -29,12 +25,12 @@ export class OCRImage extends OneBotAction<Payload, any> {
fs.unlink(path, () => { });
if (!ret) {
throw new Error(`OCR ${payload.file}失败`);
throw new Error(`OCR ${payload.image}失败`);
}
return ret.result;
}
fs.unlink(path, () => { });
throw new Error(`OCR ${payload.file}失败,文件可能不存在`);
throw new Error(`OCR ${payload.image}失败,文件可能不存在`);
}
}

View File

@@ -1,16 +1,12 @@
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class SetGroupSign extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SetGroupSign;

View File

@@ -1,18 +1,14 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { ChatType } from '@/core';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
event_type: { type: 'number' },
user_id: { type: ['number', 'string'] },
},
required: ['event_type', 'user_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
event_type: Type.Number(),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class SetInputStatus extends OneBotAction<Payload, any> {
actionName = ActionName.SetInputStatus;

View File

@@ -1,16 +1,12 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
longNick: { type: 'string' },
},
required: ['longNick'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
longNick: Type.String(),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class SetLongNick extends OneBotAction<Payload, any> {
actionName = ActionName.SetLongNick;

View File

@@ -1,19 +1,14 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
// 设置在线状态
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
status: { type: ['number', 'string'] },
ext_status: { type: ['number', 'string'] },
battery_status: { type: ['number', 'string'] },
},
required: ['status', 'ext_status', 'battery_status'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
status: Type.Union([Type.Number(), Type.String()]),
ext_status: Type.Union([Type.Number(), Type.String()]),
battery_status: Type.Union([Type.Number(), Type.String()]),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class SetOnlineStatus extends OneBotAction<Payload, null> {
actionName = ActionName.SetOnlineStatus;
@@ -21,9 +16,9 @@ export class SetOnlineStatus extends OneBotAction<Payload, null> {
async _handle(payload: Payload) {
const ret = await this.core.apis.UserApi.setSelfOnlineStatus(
parseInt(payload.status.toString()),
parseInt(payload.ext_status.toString()),
parseInt(payload.battery_status.toString()),
+payload.status,
+payload.ext_status,
+payload.battery_status,
);
if (ret.result !== 0) {
throw new Error('设置在线状态失败');

View File

@@ -1,39 +1,27 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
import * as fs from 'node:fs';
import { checkFileExist, uri2local } from '@/common/file';
import { ActionName } from '@/onebot/action/router';
import fs from 'node:fs/promises';
import { checkFileExist, uriToLocalFile } from '@/common/file';
import { Static, Type } from '@sinclair/typebox';
interface Payload {
file: string;
}
const SchemaData = Type.Object({
file: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export default class SetAvatar extends OneBotAction<Payload, null> {
actionName = ActionName.SetQQAvatar;
// 用不着复杂检测
protected async check(payload: Payload): Promise<BaseCheckResult> {
if (!payload.file || typeof payload.file != 'string') {
return {
valid: false,
message: 'file字段不能为空或者类型错误',
};
}
return {
valid: true,
};
}
payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file));
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file));
if (!success) {
throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`);
}
if (path) {
await checkFileExist(path, 5000);// 避免崩溃
const ret = await this.core.apis.UserApi.setQQAvatar(path);
fs.unlink(path, () => {
});
fs.unlink(path).catch(() => { });
if (!ret) {
throw new Error(`头像${payload.file}设置失败,api无返回`);
}
@@ -44,8 +32,7 @@ export default class SetAvatar extends OneBotAction<Payload, null> {
throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`);
}
} else {
fs.unlink(path, () => { });
fs.unlink(path).catch(() => { });
throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`);
}
return null;

View File

@@ -1,17 +1,14 @@
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] },
special_title: { type: 'string' },
},
required: ['group_id', 'user_id', 'special_title'],
} as const satisfies JSONSchema;
import { Static, Type } from '@sinclair/typebox';
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
user_id: Type.Union([Type.Number(), Type.String()]),
special_title: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class SetSpecialTittle extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SetSpecialTittle;

View File

@@ -1,18 +1,14 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: 'string' },
group_id: { type: 'string' },
phoneNumber: { type: 'string' },
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Object({
user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
phoneNumber: Type.String({ default: '' }),
});
type Payload = Static<typeof SchemaData>;
export class SharePeer extends OneBotAction<Payload, any> {
actionName = ActionName.SharePeer;
@@ -20,28 +16,24 @@ export class SharePeer extends OneBotAction<Payload, any> {
async _handle(payload: Payload) {
if (payload.group_id) {
return await this.core.apis.GroupApi.getGroupRecommendContactArkJson(payload.group_id);
return await this.core.apis.GroupApi.getGroupRecommendContactArkJson(payload.group_id.toString());
} else if (payload.user_id) {
return await this.core.apis.UserApi.getBuddyRecommendContactArkJson(payload.user_id, payload.phoneNumber || '');
return await this.core.apis.UserApi.getBuddyRecommendContactArkJson(payload.user_id.toString(), payload.phoneNumber);
}
}
}
const SchemaDataGroupEx = {
type: 'object',
properties: {
group_id: { type: 'string' },
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaDataGroupEx = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
type PayloadGroupEx = FromSchema<typeof SchemaDataGroupEx>;
type PayloadGroupEx = Static<typeof SchemaDataGroupEx>;
export class ShareGroupEx extends OneBotAction<PayloadGroupEx, any> {
actionName = ActionName.ShareGroupEx;
payloadSchema = SchemaDataGroupEx;
async _handle(payload: PayloadGroupEx) {
return await this.core.apis.GroupApi.getArkJsonGroupShare(payload.group_id);
return await this.core.apis.GroupApi.getArkJsonGroupShare(payload.group_id.toString());
}
}

View File

@@ -1,19 +1,12 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
words: {
type: 'array',
items: { type: 'string' },
},
},
required: ['words'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
words: Type.Array(Type.String()),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class TranslateEnWordToZn extends OneBotAction<Payload, Array<any> | null> {
actionName = ActionName.TranslateEnWordToZn;

View File

@@ -2,12 +2,8 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import fs from 'fs/promises';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types';
// interface GetFilePayload {
// file: string; // 文件名或者fileUuid
// }
import { Static, Type } from '@sinclair/typebox';
export interface GetFileResponse {
file?: string; // path
@@ -16,19 +12,14 @@ export interface GetFileResponse {
file_name?: string;
base64?: string;
}
const GetFileBase_PayloadSchema = {
type: 'object',
properties: {
file: { type: 'string' },
file_id: { type: 'string' }
},
oneOf: [
{ required: ['file'] },
{ required: ['file_id'] }
]
} as const satisfies JSONSchema;
export type GetFilePayload = FromSchema<typeof GetFileBase_PayloadSchema>;
const GetFileBase_PayloadSchema = Type.Object({
file: Type.Optional(Type.String()),
file_id: Type.Optional(Type.String())
});
export type GetFilePayload = Static<typeof GetFileBase_PayloadSchema>;
export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
payloadSchema = GetFileBase_PayloadSchema;
@@ -50,12 +41,12 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
let url = '';
if (mixElement?.picElement && rawMessage) {
const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement) as OB11MessageImage | undefined;
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined;
url = tempData?.data.url ?? '';
}
if (mixElement?.videoElement && rawMessage) {
const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement) as OB11MessageVideo | undefined;
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined;
url = tempData?.data.url ?? '';
}
const res: GetFileResponse = {

View File

@@ -1,18 +1,14 @@
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { FileNapCatOneBotUUID } from "@/common/helper";
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
file_id: { type: ['string'] },
},
required: ['group_id', 'file_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
interface GetGroupFileUrlResponse {
url?: string;

View File

@@ -1,22 +1,18 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
folder_name: { type: 'string' },
},
required: ['group_id', 'folder_name'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
folder_name: Type.String(),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class CreateGroupFileFolder extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
}
}

View File

@@ -1,18 +1,15 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
file_id: { type: 'string' },
},
required: ['group_id', 'file_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class DeleteGroupFile extends OneBotAction<Payload, any> {
actionName = ActionName.GOCQHTTP_DeleteGroupFile;
@@ -20,6 +17,6 @@ export class DeleteGroupFile extends OneBotAction<Payload, any> {
async _handle(payload: Payload) {
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (!data) throw new Error('Invalid file_id');
return await this.core.apis.GroupApi.DelGroupFile(payload.group_id.toString(), [data.fileId]);
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
}
}

View File

@@ -1,24 +1,20 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
folder_id: { type: 'string' },
folder: { type: 'string' }
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
folder_id: Type.Optional(Type.String()),
folder: Type.Optional(Type.String()),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class DeleteGroupFileFolder extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DeleteGroupFileFolder;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.DelGroupFileFolder(
return (await this.core.apis.GroupApi.delGroupFileFolder(
payload.group_id.toString(), payload.folder ?? payload.folder_id ?? '')).groupFileCommonResult;
}
}

View File

@@ -4,29 +4,20 @@ import fs from 'fs';
import { join as joinPath } from 'node:path';
import { calculateFileMD5, httpDownload } from '@/common/file';
import { randomUUID } from 'crypto';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
interface FileResponse {
file: string;
}
const SchemaData = {
type: 'object',
properties: {
thread_count: { type: ['number', 'string'] },
url: { type: 'string' },
base64: { type: 'string' },
name: { type: 'string' },
headers: {
type: ['string', 'array'],
items: {
type: 'string',
},
},
},
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
url: Type.Optional(Type.String()),
base64: Type.Optional(Type.String()),
name: Type.Optional(Type.String()),
headers: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export default class GoCQHTTPDownloadFile extends OneBotAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile;

View File

@@ -1,20 +1,15 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNodePlain as OB11MessageNode } from '@/onebot';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
});
const SchemaData = {
type: 'object',
properties: {
message_id: { type: 'string' },
id: { type: 'string' },
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetForwardMsg;
@@ -60,7 +55,7 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, any> {
throw new Error('message_id is required');
}
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId);
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString());
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
if (!rootMsg) {
throw new Error('msg not found');

View File

@@ -2,26 +2,22 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11Message } from '@/onebot';
import { ActionName } from '@/onebot/action/router';
import { ChatType } from '@/core/types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
interface Response {
messages: OB11Message[];
}
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
});
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
message_seq: { type: ['number', 'string'] },
count: { type: ['number', 'string'] },
reverseOrder: { type: ['boolean', 'string'] },
},
required: ['user_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export default class GetFriendMsgHistory extends OneBotAction<Payload, Response> {
actionName = ActionName.GetFriendMsgHistory;
@@ -30,7 +26,7 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const MsgCount = +(payload.count ?? 20);
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
if (!uid) throw new Error(`记录${payload.user_id}不存在`);
const friend = await this.core.apis.FriendApi.isBuddy(uid);
@@ -38,7 +34,7 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ?
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, MsgCount)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, MsgCount)).msgList;
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
//翻转消息
if (isReverseOrder) msgList.reverse();

View File

@@ -1,15 +1,12 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] }
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()])
});
type Payload = Static<typeof SchemaData>;
export class GoCQHTTPGetGroupAtAllRemain extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain;

View File

@@ -1,16 +1,12 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()])
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GetGroupFileSystemInfo extends OneBotAction<Payload, {
file_count: number,

View File

@@ -1,20 +1,17 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { OB11Construct } from '@/onebot/helper/data';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
folder_id: { type: 'string' },
folder: { type: 'string' },
file_count: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
folder_id: Type.Optional(Type.String()),
folder: Type.Optional(Type.String()),
file_count: Type.Union([Type.Number(), Type.String()], { default: 50 }),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GetGroupFilesByFolder extends OneBotAction<any, any> {
actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder;
@@ -23,7 +20,7 @@ export class GetGroupFilesByFolder extends OneBotAction<any, any> {
const ret = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +(payload.file_count ?? 50),
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,

View File

@@ -1,18 +1,14 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { WebHonorType } from '@/core/types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
type: { enum: [WebHonorType.ALL, WebHonorType.EMOTION, WebHonorType.LEGEND, WebHonorType.PERFORMER, WebHonorType.STRONG_NEWBIE, WebHonorType.TALKATIVE] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
// enum是不是有点抽象
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
type: Type.Optional(Type.Enum(WebHonorType))
});
type Payload = Static<typeof SchemaData>;
export class GetGroupHonorInfo extends OneBotAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo;

View File

@@ -2,26 +2,24 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11Message } from '@/onebot';
import { ActionName } from '@/onebot/action/router';
import { ChatType, Peer } from '@/core/types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
interface Response {
messages: OB11Message[];
}
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
message_seq: { type: ['number', 'string'] },
count: { type: ['number', 'string'] },
reverseOrder: { type: ['boolean', 'string'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
});
type Payload = Static<typeof SchemaData>;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
@@ -30,13 +28,12 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
const MsgCount = +(payload.count ?? 20);
const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
//拉取消息
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ?
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, MsgCount)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, MsgCount)).msgList;
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
//翻转消息
if (isReverseOrder) msgList.reverse();

View File

@@ -1,19 +1,16 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot';
import { OB11Construct } from '@/onebot/helper/data';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['string', 'number'] },
file_count: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_count: Type.Union([Type.Number(), Type.String()], { default: 50 }),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GetGroupRootFiles extends OneBotAction<Payload, {
files: OB11GroupFile[],
@@ -24,7 +21,7 @@ export class GetGroupRootFiles extends OneBotAction<Payload, {
async _handle(payload: Payload) {
const ret = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +(payload.file_count ?? 50),
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,

View File

@@ -1,15 +1,7 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { JSONSchema } from 'json-schema-to-ts';
import { sleep } from '@/common/helper';
const SchemaData = {
type: 'object',
properties: {
no_cache: { type: 'boolean' },
},
} as const satisfies JSONSchema;
export class GetOnlineClient extends OneBotAction<void, Array<any>> {
actionName = ActionName.GetOnlineClient;

View File

@@ -2,18 +2,14 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11User, OB11UserSex } from '@/onebot';
import { OB11Construct } from '@/onebot/helper/data';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { calcQQLevel } from '@/common/helper';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
},
required: ['user_id'],
} as const satisfies JSONSchema;
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo;

View File

@@ -1,15 +1,12 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
url: { type: 'string' },
},
required: ['url'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Object({
url: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class GoCQHTTPCheckUrlSafely extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_CheckUrlSafely;

View File

@@ -1,22 +1,15 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
friend_id: { type: ['string', 'number'] },
user_id: { type: ['string', 'number'] },
temp_block: { type: 'boolean' },
temp_both_del: { type: 'boolean' },
},
oneOf: [
{ required: ['friend_id'] },
{ required: ['user_id'] },
],
const SchemaData = Type.Object({
friend_id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
user_id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
temp_block: Type.Optional(Type.Boolean()),
temp_both_del: Type.Optional(Type.Boolean()),
});
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class GoCQHTTPDeleteFriend extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DeleteFriend;

View File

@@ -1,14 +1,12 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
model: { type: 'string' },
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
const SchemaData = Type.Object({
model: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class GoCQHTTPGetModelShow extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetModelShow;

View File

@@ -1,19 +1,10 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
//兼容性代码
export class GoCQHTTPSetModelShow extends OneBotAction<Payload, any> {
export class GoCQHTTPSetModelShow extends OneBotAction<void, any> {
actionName = ActionName.GoCQHTTP_SetModelShow;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
async _handle(payload: void) {
return null;
}
}

View File

@@ -13,7 +13,7 @@ export class GoCQHTTPHandleQuickAction extends OneBotAction<Payload, null> {
async _handle(payload: Payload): Promise<null> {
this.obContext.apis.QuickActionApi
.handleQuickOperation(payload.context, payload.operation)
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
.catch(e => this.core.context.logger.logError(e));
return null;
}
}

Some files were not shown because too many files have changed in this diff Show More