mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
168 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7b82444338 | ||
![]() |
8108b9f565 | ||
![]() |
c6ddd00cd9 | ||
![]() |
20c0c00fa0 | ||
![]() |
1f90364ba6 | ||
![]() |
49ea4d31a5 | ||
![]() |
dc35f1456a | ||
![]() |
0ebeb90804 | ||
![]() |
3ef5436c98 | ||
![]() |
de7996d789 | ||
![]() |
ac52d9bae2 | ||
![]() |
cb02df3b76 | ||
![]() |
5fc5a6f1a6 | ||
![]() |
726a0d0394 | ||
![]() |
6edf5345a3 | ||
![]() |
242bbfdb14 | ||
![]() |
89e7712676 | ||
![]() |
9525786929 | ||
![]() |
72088e41a8 | ||
![]() |
a3ed9ff2ef | ||
![]() |
ff16dc73ec | ||
![]() |
2da4ef5f0f | ||
![]() |
eaf481799d | ||
![]() |
1f72863aba | ||
![]() |
6b353fd8d8 | ||
![]() |
56cde4ad79 | ||
![]() |
3b86d3c632 | ||
![]() |
4ac7a25afb | ||
![]() |
8248011a12 | ||
![]() |
5f454456d2 | ||
![]() |
e99a619c23 | ||
![]() |
1fc791bb68 | ||
![]() |
f1d83f7c16 | ||
![]() |
527bb72bcf | ||
![]() |
d78409fd07 | ||
![]() |
d5e7e8944f | ||
![]() |
fb405a5c1c | ||
![]() |
a9e471deca | ||
![]() |
9cd15ae337 | ||
![]() |
8ed4cc4b0a | ||
![]() |
a62de441cf | ||
![]() |
02a8999410 | ||
![]() |
59c7979d69 | ||
![]() |
bb7b28cd8f | ||
![]() |
056497b98a | ||
![]() |
ac2fb032c4 | ||
![]() |
c933bdd5d9 | ||
![]() |
89c71a58fa | ||
![]() |
27ba85b4ff | ||
![]() |
79a75fed8e | ||
![]() |
ee7a76b29f | ||
![]() |
c53bdc3ce0 | ||
![]() |
f36e328751 | ||
![]() |
871b5688c2 | ||
![]() |
b96076b297 | ||
![]() |
d4488e40cf | ||
![]() |
7e61497243 | ||
![]() |
e71ccdd12a | ||
![]() |
202129d491 | ||
![]() |
a1700dd503 | ||
![]() |
2954776539 | ||
![]() |
fb1f122ef7 | ||
![]() |
96c63e4689 | ||
![]() |
c94936d3dc | ||
![]() |
8c22f11087 | ||
![]() |
8a089c84a9 | ||
![]() |
b631e6f8a2 | ||
![]() |
b3b48b032c | ||
![]() |
f3e8230eca | ||
![]() |
cc9adf9d40 | ||
![]() |
15a640d1dc | ||
![]() |
c25b9f86db | ||
![]() |
ecfd033afb | ||
![]() |
f3ed8c7dff | ||
![]() |
6089046721 | ||
![]() |
44ff92ad4b | ||
![]() |
892262eb85 | ||
![]() |
2d9cc4d198 | ||
![]() |
a0c479485d | ||
![]() |
5650f18e50 | ||
![]() |
553885d025 | ||
![]() |
35de00c4af | ||
![]() |
09583e5de5 | ||
![]() |
38b0b7cd00 | ||
![]() |
8b9c7b0c27 | ||
![]() |
1005619bf3 | ||
![]() |
3e09cff9cb | ||
![]() |
c24384e454 | ||
![]() |
f87a543406 | ||
![]() |
f752136283 | ||
![]() |
7e71622a44 | ||
![]() |
da92afb379 | ||
![]() |
d3062de5f9 | ||
![]() |
f1440b03a8 | ||
![]() |
9a8b266cef | ||
![]() |
2a9bc57120 | ||
![]() |
2ed83a0e30 | ||
![]() |
116e8fd30a | ||
![]() |
891f11173b | ||
![]() |
dfc7996c17 | ||
![]() |
dc0561d34f | ||
![]() |
4fb0845d79 | ||
![]() |
0e0d4837b8 | ||
![]() |
a6adde7966 | ||
![]() |
7b693132f9 | ||
![]() |
3c3114b6ab | ||
![]() |
5cdbf58f59 | ||
![]() |
6f0a4131a2 | ||
![]() |
aa520e2f5d | ||
![]() |
2c3b7e9ee8 | ||
![]() |
b86a28092a | ||
![]() |
d59e5f2133 | ||
![]() |
3fdd187102 | ||
![]() |
3f085fd8ae | ||
![]() |
a4fc131aec | ||
![]() |
d7d446c3fc | ||
![]() |
212666e603 | ||
![]() |
b545c28340 | ||
![]() |
72bc345515 | ||
![]() |
cc5082a9e3 | ||
![]() |
45782a6c6c | ||
![]() |
e86d646cce | ||
![]() |
92cfc6b8c8 | ||
![]() |
82289d9f1f | ||
![]() |
4cdbdaaf4e | ||
![]() |
ecde2427da | ||
![]() |
fed1ec5d83 | ||
![]() |
4fbd764ced | ||
![]() |
5361079010 | ||
![]() |
002d135ef5 | ||
![]() |
a39b0a4a78 | ||
![]() |
eb5d68422f | ||
![]() |
3dc13e5c2e | ||
![]() |
16881f057a | ||
![]() |
1cd7d0577f | ||
![]() |
3c872df97a | ||
![]() |
218b7bd2a0 | ||
![]() |
4552d6970d | ||
![]() |
4b319d15a7 | ||
![]() |
0ae3a4172c | ||
![]() |
bf0c12f1c4 | ||
![]() |
cb5eeecb86 | ||
![]() |
8d857cf2be | ||
![]() |
6f232c465f | ||
![]() |
032d444246 | ||
![]() |
49488dd3fb | ||
![]() |
9aec3865ff | ||
![]() |
b6b7f2051b | ||
![]() |
46254a699a | ||
![]() |
7b3c287137 | ||
![]() |
1a533742a5 | ||
![]() |
2027266852 | ||
![]() |
946d8b1a7b | ||
![]() |
6d2fb5de6f | ||
![]() |
91c4a002dd | ||
![]() |
4d8112aae5 | ||
![]() |
bb53f245cf | ||
![]() |
9f31cdbf5b | ||
![]() |
9a33039d73 | ||
![]() |
7cf3be8333 | ||
![]() |
82afb88e53 | ||
![]() |
4aa24b5d67 | ||
![]() |
57112c21a2 | ||
![]() |
0e8ceeb6c9 | ||
![]() |
f52b8d1f04 | ||
![]() |
f374cc77ae | ||
![]() |
7c694e7fae | ||
![]() |
932ffc2673 |
20
README.md
20
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -30,11 +30,25 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||
|
||||
[Server.Other](https://napcat.cyou/)
|
||||
[Server.Other](https://docs.napcat.cyou/)
|
||||
|
||||
[Qbot.News](https://neko.qbot.news)
|
||||
|
||||
## 回家旅途
|
||||
[QQ Group](https://qm.qq.com/q/I6LU87a0Yq)
|
||||
[QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq)
|
||||
|
||||
[QQ Group#2](https://qm.qq.com/q/uqh4I87KoM)
|
||||
|
||||
[Telegram](https://t.me/MelodicMoonlight)
|
||||
|
||||
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
|
||||
|
||||
## 性能设计/协议标准
|
||||
NapCat 已实现90%+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
|
||||
|
||||
由此设计带来一系列好处,在开发中,获取群员列表通常小于50Ms,单条文本消息发送在320Ms以内,在1k+的群聊流畅运行,同时带来一些副作用,上报数据中大量使用Magic生成字段,消息Id无法持久,无法上报撤回消息原始内容。
|
||||
|
||||
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
|
||||
|
||||
## 感谢他们
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||
|
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
BIN
external/logo.png
vendored
Normal file
BIN
external/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 204 KiB |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "qq-chat",
|
||||
"version": "9.9.16-29927",
|
||||
"verHash": "3e273e30",
|
||||
"linuxVersion": "3.2.13-29927",
|
||||
"linuxVerHash": "833d113c",
|
||||
"version": "9.9.17-30899",
|
||||
"verHash": "ececf273",
|
||||
"linuxVersion": "3.2.15-30899",
|
||||
"linuxVerHash": "63c751e8",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "QQ",
|
||||
@@ -18,7 +18,7 @@
|
||||
"qd": "externals/devtools/cli/index.js"
|
||||
},
|
||||
"main": "./loadNapCat.js",
|
||||
"buildVersion": "29927",
|
||||
"buildVersion": "30899",
|
||||
"isPureShell": true,
|
||||
"isByteCodeShell": true,
|
||||
"platform": "win32",
|
||||
|
BIN
logo.png
BIN
logo.png
Binary file not shown.
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 684 KiB |
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.2.22",
|
||||
"version": "4.2.51",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -5,12 +5,14 @@
|
||||
"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",
|
||||
"mitt": "^3.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"tdesign-icons-vue-next": "^0.3.3",
|
||||
"tdesign-vue-next": "^1.10.3",
|
||||
@@ -20,6 +22,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",
|
||||
|
@@ -109,4 +109,4 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', haddingFbars);
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
||||
<style></style>
|
||||
|
BIN
napcat.webui/src/assets/0xProtoNerdFont-Italic.ttf
Normal file
BIN
napcat.webui/src/assets/0xProtoNerdFont-Italic.ttf
Normal file
Binary file not shown.
66
napcat.webui/src/backend/githubApi.ts
Normal file
66
napcat.webui/src/backend/githubApi.ts
Normal 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;
|
||||
}
|
||||
}
|
72
napcat.webui/src/backend/log.ts
Normal file
72
napcat.webui/src/backend/log.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||
|
||||
import { ResponseCode } from '../../../src/webui/src/const/status';
|
||||
export class QQLoginManager {
|
||||
private retCredential: string;
|
||||
private readonly apiPrefix: string;
|
||||
@@ -22,8 +22,8 @@ export class QQLoginManager {
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return ConfigResponseJson?.data as OneBotConfig;
|
||||
if (ConfigResponseJson.code == ResponseCode.Success) {
|
||||
return ConfigResponseJson.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -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>
|
||||
|
@@ -1,34 +1,20 @@
|
||||
<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">
|
||||
<t-tooltip content="快速登录">
|
||||
<t-button
|
||||
id="quick-login"
|
||||
class="login-method"
|
||||
:class="{ active: loginMethod === 'quick' }"
|
||||
@click="loginMethod = 'quick'"
|
||||
>Quick Login</t-button
|
||||
>
|
||||
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }"
|
||||
@click="loginMethod = 'quick'">Quick Login</t-button>
|
||||
</t-tooltip>
|
||||
<t-tooltip content="二维码登录">
|
||||
<t-button
|
||||
id="qrcode-login"
|
||||
class="login-method"
|
||||
:class="{ active: loginMethod === 'qrcode' }"
|
||||
@click="loginMethod = 'qrcode'"
|
||||
>QR Code</t-button
|
||||
>
|
||||
<t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }"
|
||||
@click="loginMethod = 'qrcode'">QR Code</t-button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
||||
<t-select
|
||||
id="quick-login-select"
|
||||
v-model="selectedAccount"
|
||||
placeholder="Select Account"
|
||||
@change="selectAccount"
|
||||
>
|
||||
<t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account"
|
||||
@change="selectAccount">
|
||||
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
|
||||
</t-select>
|
||||
</div>
|
||||
@@ -41,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import * as QRCode from 'qrcode';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
@@ -55,6 +41,7 @@ const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
|
||||
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
|
||||
let heartBeatTimer: number | null = null;
|
||||
let qrcodeUrl: string = '';
|
||||
|
||||
const selectAccount = async (accountName: string): Promise<void> => {
|
||||
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
||||
if (result) {
|
||||
@@ -88,10 +75,6 @@ const HeartBeat = async (): Promise<void> => {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
// //判断是否已经调转
|
||||
// if (router.currentRoute.value.path !== '/dashboard/basic-info') {
|
||||
// return;
|
||||
// }
|
||||
await MessagePlugin.success('登录成功即将跳转');
|
||||
await router.push({ path: '/dashboard/basic-info' });
|
||||
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
|
||||
@@ -103,19 +86,38 @@ const HeartBeat = async (): Promise<void> => {
|
||||
const InitPages = async (): Promise<void> => {
|
||||
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
||||
qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
|
||||
await nextTick();
|
||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
InitPages();
|
||||
InitPages().then().catch((err) => {
|
||||
console.error('InitPages Error:', err);
|
||||
});
|
||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
watch(loginMethod, async (newMethod) => {
|
||||
if (newMethod === 'qrcode') {
|
||||
await nextTick();
|
||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
@@ -182,4 +184,4 @@ onMounted(() => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
@@ -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">
|
||||
|
@@ -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" />
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, onMounted, watch } from 'vue';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
|
||||
type MenuItem = {
|
||||
@@ -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);
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
});
|
||||
onMounted(() => {
|
||||
const mediaQuery = window.matchMedia('(max-width: 800px)');
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
disBtn.value = e.matches;
|
||||
if (e.matches) {
|
||||
|
35
napcat.webui/src/components/webui/NavBottom.vue
Normal file
35
napcat.webui/src/components/webui/NavBottom.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<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">
|
||||
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>
|
@@ -3,4 +3,11 @@
|
||||
src: url('../assets/Sotheby.ttf') format('truetype');
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -40,8 +40,13 @@ 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 router from './router';
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
@@ -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');
|
||||
|
@@ -1,23 +1,101 @@
|
||||
<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:
|
||||
{{ napCatVersion }}
|
||||
</t-tag>
|
||||
<t-tag v-if="githubReleasesData&&githubReleasesData[0] ?.tag_name" class="tag-item nc-color">
|
||||
New NapCat:
|
||||
{{ githubReleasesData[0].tag_name }}
|
||||
</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 +106,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 +158,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 +190,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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
||||
<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>
|
||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="local-box" v-if="item.url">
|
||||
<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>
|
||||
@@ -105,20 +169,34 @@
|
||||
</div>
|
||||
<div style="height: 20vh"></div>
|
||||
</div>
|
||||
<t-card v-else>
|
||||
<t-card v-else>
|
||||
<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);
|
||||
MessagePlugin.success('复制成功');
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -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>();
|
||||
|
@@ -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,20 +11,20 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@@ -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,14 +23,14 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@@ -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" />
|
||||
@@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@@ -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" />
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@@ -7,6 +7,8 @@ import NetWork from '../pages/NetWork.vue';
|
||||
import QQLogin from '../components/QQLogin.vue';
|
||||
import WebUiLogin from '../components/WebUiLogin.vue';
|
||||
import OtherConfig from '../pages/OtherConfig.vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{ path: '/', redirect: '/webui' },
|
||||
@@ -26,7 +28,27 @@ const routes: Array<RouteRecordRaw> = [
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const isPublicRoute = ['/webui', '/qqlogin'].includes(to.path);
|
||||
const token = localStorage.getItem('auth');
|
||||
|
||||
if (!isPublicRoute) {
|
||||
if (!token) {
|
||||
MessagePlugin.error('请先登录');
|
||||
return next('/webui');
|
||||
}
|
||||
const login = await new QQLoginManager(token).checkWebUiLogined();
|
||||
if (!login) {
|
||||
MessagePlugin.error('请先登录');
|
||||
return next('/webui');
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@@ -3,22 +3,15 @@
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"baseUrl": ".",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"types": ["vite/client"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
@@ -30,5 +23,5 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [{"path": "./tsconfig.node.json"}]
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
14
package.json
14
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.2.22",
|
||||
"version": "4.2.51",
|
||||
"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",
|
||||
@@ -13,18 +13,21 @@
|
||||
"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": {
|
||||
"esbuild": "0.24.0",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@eslint/compat": "^1.2.2",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@napneko/nap-proto-core": "^0.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@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",
|
||||
@@ -48,8 +51,7 @@
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"@sinclair/typebox": "^0.34.9"
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
|
@@ -175,32 +175,34 @@ export async function checkUriType(Uri: string) {
|
||||
return { Uri: Uri, Type: FileUriType.Unknown };
|
||||
}
|
||||
|
||||
export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2LocalRes> {
|
||||
export async function uriToLocalFile(dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
|
||||
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||
|
||||
const filename = randomUUID();
|
||||
const filePath = path.join(dir, filename);
|
||||
|
||||
switch (UriType) {
|
||||
case FileUriType.Local:
|
||||
const fileExt = path.extname(HandledUri);
|
||||
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 };
|
||||
|
||||
case FileUriType.Remote:
|
||||
const buffer = await httpDownload(HandledUri);
|
||||
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
|
||||
case FileUriType.Base64:
|
||||
const base64 = HandledUri.replace(/^base64:\/\//, '');
|
||||
const base64Buffer = Buffer.from(base64, 'base64');
|
||||
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
|
||||
default:
|
||||
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
|
||||
case FileUriType.Local: {
|
||||
const fileExt = path.extname(HandledUri);
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
case FileUriType.Remote: {
|
||||
const buffer = await httpDownload({ url: HandledUri, headers: headers });
|
||||
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
|
||||
case FileUriType.Base64: {
|
||||
const base64 = HandledUri.replace(/^base64:\/\//, '');
|
||||
const base64Buffer = Buffer.from(base64, 'base64');
|
||||
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
|
||||
}
|
||||
}
|
||||
|
@@ -315,5 +315,5 @@ function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel:
|
||||
return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
|
||||
? rawMessageToText(recordMsgOrNull, recursiveLevel + 1)
|
||||
: `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
|
||||
}]`;
|
||||
}]`;
|
||||
}
|
||||
|
126
src/common/umami.ts
Normal file
126
src/common/umami.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import https from 'node:https';
|
||||
import { napCatVersion } from './version';
|
||||
import os from 'os';
|
||||
export class UmamiTraceCore {
|
||||
napcatVersion = napCatVersion;
|
||||
qqversion = '1.0.0';
|
||||
guid = 'default-user';
|
||||
heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
website: string = '1fabb2b1-c3a3-4416-b1be-31e2cbdce978';
|
||||
referrer: string = 'https://trace.napneko.icu/';
|
||||
hostname: string = 'trace.napneko.icu';
|
||||
ua: string = '';
|
||||
|
||||
init(qqversion: string, guid: string) {
|
||||
this.qqversion = qqversion;
|
||||
let UaList = {
|
||||
'linux': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/124.0.0.0 Safari/537.36 PTST/240508.140043',
|
||||
'win32': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36',
|
||||
'darwin': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36',
|
||||
};
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
const ntVersion = os.release();
|
||||
UaList.win32 = `Mozilla/5.0 (Windows NT ${ntVersion}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36`;
|
||||
} else if (process.platform === 'darwin') {
|
||||
const macVersion = os.release();
|
||||
UaList.darwin = `Mozilla/5.0 (Macintosh; Intel Mac OS X ${macVersion}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36`;
|
||||
}
|
||||
} catch (error) {
|
||||
this.ua = UaList.win32;
|
||||
}
|
||||
|
||||
this.ua = UaList[process.platform as keyof typeof UaList] || UaList.win32;
|
||||
|
||||
this.identifyUser(guid);
|
||||
this.startHeartbeat();
|
||||
}
|
||||
|
||||
identifyUser(guid: string) {
|
||||
this.guid = guid;
|
||||
const data = {
|
||||
napcat_version: this.napcatVersion,
|
||||
qq_version: this.qqversion,
|
||||
guid: guid
|
||||
};
|
||||
this.sendRequest({ website: this.website, ...data }, 'identify');
|
||||
}
|
||||
|
||||
sendEvent(event: string, data?: object) {
|
||||
const env = process.env;
|
||||
const language = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES;
|
||||
const payload = {
|
||||
name: event,
|
||||
hostname: this.hostname,
|
||||
referrer: this.referrer,
|
||||
website: this.website,
|
||||
language: language || 'es-US',
|
||||
napcat_version: this.napcatVersion,
|
||||
qq_version: this.qqversion,
|
||||
...data
|
||||
};
|
||||
this.sendRequest(payload);
|
||||
}
|
||||
|
||||
sendTrace(eventName: string) {
|
||||
const payload = {
|
||||
website: this.website,
|
||||
hostname: this.hostname,
|
||||
title: 'NapCat ' + this.napcatVersion,
|
||||
url: `/${this.qqversion}/${this.napcatVersion}/${eventName}`,
|
||||
referrer: this.referrer,
|
||||
};
|
||||
this.sendRequest(payload);
|
||||
}
|
||||
|
||||
sendRequest(payload: object, type = 'event') {
|
||||
const options = {
|
||||
hostname: '104.19.42.72', // 固定 IP 或者从 hostUrl 获取
|
||||
port: 443,
|
||||
path: '/api/send',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Host": "umami.napneko.icu",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": this.ua
|
||||
}
|
||||
};
|
||||
|
||||
const request = https.request(options, (res) => {
|
||||
res.on('error', (error) => {
|
||||
|
||||
});
|
||||
res.on('data', (data) => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
request.write(JSON.stringify({ type, payload }));
|
||||
request.end();
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
}
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
this.sendEvent('heartbeat', {
|
||||
title: 'NapCat ' + this.napcatVersion,
|
||||
language: process.env.LANG || 'en-US',
|
||||
url: `/${this.qqversion}/${this.napcatVersion}/heartbeat`,
|
||||
version: this.napcatVersion,
|
||||
qq_version: this.qqversion,
|
||||
user_id: this.guid
|
||||
});
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
export const UmamiTrace = new UmamiTraceCore();
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.2.22';
|
||||
export const napCatVersion = '4.2.51';
|
||||
|
@@ -40,7 +40,7 @@ export class NTQQFileApi {
|
||||
this.rkeyManager = new RkeyManager([
|
||||
'https://rkey.napneko.icu/rkeys'
|
||||
],
|
||||
this.context.logger
|
||||
this.context.logger
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class NTQQFileApi {
|
||||
|
||||
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
||||
const fileMd5 = await calculateFileMD5(filePath);
|
||||
let extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
|
||||
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
|
||||
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
|
||||
let fileName = `${path.basename(filePath)}`;
|
||||
if (fileName.indexOf('.') === -1) {
|
||||
@@ -305,18 +305,18 @@ export class NTQQFileApi {
|
||||
element.elementType === ElementType.FILE
|
||||
) {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PIC:
|
||||
case ElementType.PIC:
|
||||
element.picElement!.sourcePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
element.videoElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
element.pttElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
element.fileElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
break;
|
||||
}
|
||||
elementIndex++;
|
||||
}
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ import {
|
||||
KickMemberV2Req,
|
||||
MemberExtSourceType,
|
||||
NapCatCore,
|
||||
GroupNotify,
|
||||
GroupInfoSource,
|
||||
} from '@/core';
|
||||
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
||||
import { LimitedHashTable } from '@/common/message-unique';
|
||||
@@ -23,6 +25,19 @@ export class NTQQGroupApi {
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async fetchGroupDetail(groupCode: string) {
|
||||
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getGroupDetailInfo',
|
||||
'NodeIKernelGroupListener/onGroupDetailInfoChange',
|
||||
[groupCode, GroupInfoSource.KDATACARD],
|
||||
(ret) => ret.result === 0,
|
||||
(detailInfo) => detailInfo.groupCode === groupCode,
|
||||
1,
|
||||
5000
|
||||
);
|
||||
return detailInfo;
|
||||
}
|
||||
|
||||
async initApi() {
|
||||
this.initCache().then().catch(e => this.context.logger.logError(e));
|
||||
}
|
||||
@@ -120,7 +135,7 @@ export class NTQQGroupApi {
|
||||
}
|
||||
return this.groupMemberCache;
|
||||
}
|
||||
|
||||
|
||||
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
||||
const groupCodeStr = groupCode.toString();
|
||||
const memberUinOrUidStr = memberUinOrUid.toString();
|
||||
@@ -288,20 +303,15 @@ export class NTQQGroupApi {
|
||||
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 ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
|
||||
},
|
||||
});
|
||||
|
@@ -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域名
|
||||
}
|
||||
|
@@ -176,7 +176,7 @@ export class NTQQUserApi {
|
||||
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.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),
|
||||
];
|
||||
@@ -198,7 +198,7 @@ export class NTQQUserApi {
|
||||
() => 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.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),
|
||||
];
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
56
src/core/external/appid.json
vendored
56
src/core/external/appid.json
vendored
@@ -102,5 +102,61 @@
|
||||
"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"
|
||||
},
|
||||
"9.9.17-30483": {
|
||||
"appid": 537258439,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30483_GW_B"
|
||||
},
|
||||
"6.9.62-30483": {
|
||||
"appid": 537258463,
|
||||
"qua": "V1_MAC_NQ_6.9.62_30483_GW_B"
|
||||
},
|
||||
"3.2.15-30483": {
|
||||
"appid": 537258474,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30483_GW_B"
|
||||
},
|
||||
"9.9.17-30594": {
|
||||
"appid": 537258439,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30594_GW_B"
|
||||
},
|
||||
"6.9.62-30594": {
|
||||
"appid": 537258463,
|
||||
"qua": "V1_MAC_NQ_6.9.62_30594_GW_B"
|
||||
},
|
||||
"3.2.15-30594": {
|
||||
"appid": 537258474,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30594_GW_B"
|
||||
},
|
||||
"9.9.17-30851": {
|
||||
"appid": 537263796,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30851_GW_B"
|
||||
},
|
||||
"3.2.15-30851": {
|
||||
"appid": 537263831,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30851_GW_B"
|
||||
},
|
||||
"6.9.63-30851": {
|
||||
"appid": 537263820,
|
||||
"qua": "V1_MAC_NQ_6.9.63_30851_GW_B"
|
||||
},
|
||||
"9.9.17-30899": {
|
||||
"appid": 537263796,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30899_GW_B"
|
||||
},
|
||||
"3.2.15-30899": {
|
||||
"appid": 537263831,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30899_GW_B"
|
||||
},
|
||||
"6.9.63-30899": {
|
||||
"appid": 537263820,
|
||||
"qua": "V1_MAC_NQ_6.9.63_30899_GW_B"
|
||||
}
|
||||
}
|
96
src/core/external/offset.json
vendored
96
src/core/external/offset.json
vendored
@@ -106,5 +106,101 @@
|
||||
"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"
|
||||
},
|
||||
"9.9.17-30483-x64": {
|
||||
"send": "39AC1B0",
|
||||
"recv": "39B05E4"
|
||||
},
|
||||
"6.9.62-30483-arm64": {
|
||||
"send": "41896B0",
|
||||
"recv": "418bec8"
|
||||
},
|
||||
"6.9.62-30483-x64": {
|
||||
"send": "4669460",
|
||||
"recv": "466BCCC"
|
||||
},
|
||||
"3.2.15-30483-x64": {
|
||||
"send": "A402540",
|
||||
"recv": "A405E40"
|
||||
},
|
||||
"3.2.15-30483-arm64": {
|
||||
"send": "70C40E8",
|
||||
"recv": "70C7920"
|
||||
},
|
||||
"9.9.17-30594-x64": {
|
||||
"send": "39AC1B0",
|
||||
"recv": "39B05E4"
|
||||
},
|
||||
"6.9.62-30594-arm64": {
|
||||
"send": "41896B0",
|
||||
"recv": "418bec8"
|
||||
},
|
||||
"6.9.62-30594-x64": {
|
||||
"send": "4669460",
|
||||
"recv": "466BCCC"
|
||||
},
|
||||
"3.2.15-30594-x64": {
|
||||
"send": "A402540",
|
||||
"recv": "A405E40"
|
||||
},
|
||||
"3.2.15-30594-arm64": {
|
||||
"send": "70C40E8",
|
||||
"recv": "70C7920"
|
||||
},
|
||||
"9.9.17-30851-x64": {
|
||||
"send": "395C150",
|
||||
"recv": "3960584"
|
||||
},
|
||||
"3.2.15-30851-x64": {
|
||||
"send": "A4A03E0",
|
||||
"recv": "A4A3CE0"
|
||||
},
|
||||
"3.2.15-30851-arm64": {
|
||||
"send": "713A318",
|
||||
"recv": "713DB50"
|
||||
},
|
||||
"6.9.63.30851-x64": {
|
||||
"send": "46C8040",
|
||||
"recv": "46CA8AC"
|
||||
},
|
||||
"6.9.63-30851-arm64": {
|
||||
"send": "41DCBD8",
|
||||
"recv": "41DF3F0"
|
||||
},
|
||||
"9.9.17-30899-x64": {
|
||||
"send": "395C150",
|
||||
"recv": "3960584"
|
||||
},
|
||||
"3.2.15-30899-x64": {
|
||||
"send": "A4A03E0",
|
||||
"recv": "A4A3CE0"
|
||||
},
|
||||
"3.2.15-30899-arm64": {
|
||||
"send": "713A318",
|
||||
"recv": "713DB50"
|
||||
},
|
||||
"6.9.63.30899-x64": {
|
||||
"send": "46C8040",
|
||||
"recv": "46CA8AC"
|
||||
},
|
||||
"6.9.63-30899-arm64": {
|
||||
"send": "41DCBD8",
|
||||
"recv": "41DF3F0"
|
||||
}
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
// TODO: further refactor in NapCat.Packet v2
|
||||
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
|
||||
|
||||
const BodyInner = {
|
||||
msgType: ProtoField(1, ScalarType.UINT32, true),
|
||||
subType: ProtoField(2, ScalarType.UINT32, true)
|
||||
};
|
||||
|
||||
const NoifyData = {
|
||||
skip: ProtoField(1, ScalarType.BYTES, true),
|
||||
innerData: ProtoField(2, ScalarType.BYTES, true)
|
||||
};
|
||||
|
||||
const MsgHead = {
|
||||
bodyInner: ProtoField(2, () => BodyInner, true),
|
||||
noifyData: ProtoField(3, () => NoifyData, true)
|
||||
};
|
||||
|
||||
const Message = {
|
||||
msgHead: ProtoField(1, () => MsgHead)
|
||||
};
|
||||
|
||||
const SubDetail = {
|
||||
msgSeq: ProtoField(1, ScalarType.UINT32),
|
||||
msgTime: ProtoField(2, ScalarType.UINT32),
|
||||
senderUid: ProtoField(6, ScalarType.STRING)
|
||||
};
|
||||
|
||||
const RecallDetails = {
|
||||
operatorUid: ProtoField(1, ScalarType.STRING),
|
||||
subDetail: ProtoField(3, () => SubDetail)
|
||||
};
|
||||
|
||||
const RecallGroup = {
|
||||
type: ProtoField(1, ScalarType.INT32),
|
||||
peerUid: ProtoField(4, ScalarType.UINT32),
|
||||
recallDetails: ProtoField(11, () => RecallDetails),
|
||||
grayTipsSeq: ProtoField(37, ScalarType.UINT32)
|
||||
};
|
||||
|
||||
export function decodeMessage(buffer: Uint8Array) {
|
||||
const msg = new NapProtoMsg(Message);
|
||||
return msg.decode(buffer);
|
||||
}
|
||||
|
||||
export function decodeRecallGroup(buffer: Uint8Array){
|
||||
const msg = new NapProtoMsg(RecallGroup);
|
||||
return msg.decode(buffer);
|
||||
}
|
138
src/core/helper/status.ts
Normal file
138
src/core/helper/status.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import os from "node:os";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
export interface SystemStatus {
|
||||
cpu: {
|
||||
model: string,
|
||||
speed: string
|
||||
usage: {
|
||||
system: string
|
||||
qq: string
|
||||
},
|
||||
core: number
|
||||
},
|
||||
memory: {
|
||||
total: string
|
||||
usage: {
|
||||
system: string
|
||||
qq: string
|
||||
}
|
||||
},
|
||||
arch: string
|
||||
}
|
||||
|
||||
export class StatusHelper {
|
||||
private psCpuUsage = process.cpuUsage();
|
||||
private psCurrentTime = process.hrtime();
|
||||
private cpuTimes = os.cpus().map(cpu => cpu.times);
|
||||
|
||||
private replaceNaN(value: number) {
|
||||
return isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
private sysCpuInfo() {
|
||||
const currentTimes = os.cpus().map(cpu => cpu.times);
|
||||
const { total, active } = currentTimes.map((times, index) => {
|
||||
const prevTimes = this.cpuTimes[index];
|
||||
const totalCurrent = times.user + times.nice + times.sys + times.idle + times.irq;
|
||||
const totalPrev = prevTimes.user + prevTimes.nice + prevTimes.sys + prevTimes.idle + prevTimes.irq;
|
||||
const activeCurrent = totalCurrent - times.idle;
|
||||
const activePrev = totalPrev - prevTimes.idle;
|
||||
return {
|
||||
total: totalCurrent - totalPrev,
|
||||
active: activeCurrent - activePrev
|
||||
};
|
||||
}).reduce((acc, cur) => ({
|
||||
total: acc.total + cur.total,
|
||||
active: acc.active + cur.active
|
||||
}), { total: 0, active: 0 });
|
||||
this.cpuTimes = currentTimes;
|
||||
return {
|
||||
usage: this.replaceNaN(((active / total) * 100)).toFixed(2),
|
||||
model: os.cpus()[0].model,
|
||||
speed: os.cpus()[0].speed,
|
||||
core: os.cpus().length
|
||||
};
|
||||
}
|
||||
|
||||
private sysMemoryUsage() {
|
||||
const { total, free } = { total: os.totalmem(), free: os.freemem() };
|
||||
return ((total - free) / 1024 / 1024).toFixed(2);
|
||||
}
|
||||
|
||||
private qqUsage() {
|
||||
const mem = process.memoryUsage();
|
||||
const numCpus = os.cpus().length;
|
||||
const usageDiff = process.cpuUsage(this.psCpuUsage);
|
||||
const endTime = process.hrtime(this.psCurrentTime);
|
||||
this.psCpuUsage = process.cpuUsage();
|
||||
this.psCurrentTime = process.hrtime();
|
||||
const usageMS = (usageDiff.user + usageDiff.system) / 1e3;
|
||||
const totalMS = endTime[0] * 1e3 + endTime[1] / 1e6;
|
||||
const normPercent = (usageMS / totalMS / numCpus) * 100;
|
||||
return {
|
||||
cpu: this.replaceNaN(normPercent).toFixed(2),
|
||||
memory: ((mem.heapTotal + mem.external + mem.arrayBuffers) / 1024 / 1024).toFixed(2)
|
||||
};
|
||||
}
|
||||
|
||||
systemStatus(): SystemStatus {
|
||||
const qqUsage = this.qqUsage();
|
||||
const sysCpuInfo = this.sysCpuInfo();
|
||||
return {
|
||||
cpu: {
|
||||
core: sysCpuInfo.core,
|
||||
model: sysCpuInfo.model,
|
||||
speed: (sysCpuInfo.speed / 1000).toFixed(2),
|
||||
usage: {
|
||||
system: sysCpuInfo.usage,
|
||||
qq: qqUsage.cpu
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
total: (os.totalmem() / 1024 / 1024).toFixed(2),
|
||||
usage: {
|
||||
system: this.sysMemoryUsage(),
|
||||
qq: qqUsage.memory
|
||||
}
|
||||
},
|
||||
arch: `${os.platform()} ${os.arch()} ${os.release()}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StatusHelperSubscription extends EventEmitter {
|
||||
private statusHelper: StatusHelper;
|
||||
private interval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(time: number = 3000) {
|
||||
super();
|
||||
this.statusHelper = new StatusHelper();
|
||||
this.on('newListener', (event: string) => {
|
||||
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
|
||||
this.startInterval(time);
|
||||
}
|
||||
});
|
||||
this.on('removeListener', (event: string) => {
|
||||
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
|
||||
this.stopInterval();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startInterval(time: number) {
|
||||
this.interval ??= setInterval(() => {
|
||||
const status = this.statusHelper.systemStatus();
|
||||
this.emit('statusUpdate', status);
|
||||
}, time);
|
||||
}
|
||||
|
||||
private stopInterval() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const statusHelperSubscription = new StatusHelperSubscription();
|
@@ -30,6 +30,7 @@ import os from 'node:os';
|
||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||
import { NTQQPacketApi } from './apis/packet';
|
||||
import { UmamiTrace } from '@/common/umami';
|
||||
export * from './wrapper';
|
||||
export * from './types';
|
||||
export * from './services';
|
||||
@@ -152,7 +153,13 @@ export class NapCatCore {
|
||||
// Renamed from 'InitDataListener'
|
||||
async initNapCatCoreListeners() {
|
||||
const msgListener = new NodeIKernelMsgListener();
|
||||
|
||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||
if (this.context.workingEnv === NapCatCoreWorkingEnv.Framework) {
|
||||
UmamiTrace.sendEvent('framework/kickoff');
|
||||
} else {
|
||||
UmamiTrace.sendEvent('shell/kickoff');
|
||||
}
|
||||
// 下线通知
|
||||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||||
this.selfInfo.online = false;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types';
|
||||
import { DataSource, Group, GroupDetailInfo, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types';
|
||||
|
||||
export class NodeIKernelGroupListener {
|
||||
onGroupListInited(listEmpty: boolean): any { }
|
||||
@@ -28,7 +28,7 @@ export class NodeIKernelGroupListener {
|
||||
onGroupConfMemberChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupDetailInfoChange(...args: unknown[]): any {
|
||||
onGroupDetailInfoChange(detailInfo: GroupDetailInfo): any {
|
||||
}
|
||||
|
||||
onGroupExtListUpdate(...args: unknown[]): any {
|
||||
|
@@ -23,7 +23,9 @@ export class PacketClientSession {
|
||||
get operation() {
|
||||
return this.context.operation;
|
||||
}
|
||||
|
||||
get client() {
|
||||
return this.context.client;
|
||||
}
|
||||
// TODO: global message element adapter (?
|
||||
get msgConverter() {
|
||||
return this.context.msgConverter;
|
||||
|
@@ -3,6 +3,7 @@ export interface MiniAppReqCustomParams {
|
||||
desc: string;
|
||||
picUrl: string;
|
||||
jumpUrl: string;
|
||||
webUrl?: string;
|
||||
}
|
||||
|
||||
export interface MiniAppReqTemplateParams {
|
||||
|
@@ -124,7 +124,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
}
|
||||
|
||||
get isGroupReply(): boolean {
|
||||
return this.messageClientSeq !== 0;
|
||||
return this.messageClientSeq === 0;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
|
@@ -30,7 +30,7 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAda
|
||||
shareType: req.shareType,
|
||||
versionId: req.versionId,
|
||||
withShareTicket: req.withShareTicket,
|
||||
webURL: "",
|
||||
webURL: req.webUrl ?? "",
|
||||
appidRich: Buffer.alloc(0),
|
||||
template: {
|
||||
templateId: "",
|
||||
|
@@ -3,16 +3,16 @@ 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),
|
||||
}
|
||||
};
|
@@ -54,16 +54,32 @@ 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 GroupInvite = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
field2: ProtoField(2, ScalarType.UINT32),
|
||||
field3: ProtoField(2, ScalarType.UINT32),
|
||||
field4: ProtoField(2, ScalarType.UINT32),
|
||||
invitorUid: ProtoField(5, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const PushMsgBody = {
|
||||
responseHead: ProtoField(1, () => ResponseHead),
|
||||
contentHead: ProtoField(2, () => ContentHead),
|
||||
|
@@ -149,7 +149,7 @@ export interface NodeIKernelGroupService {
|
||||
|
||||
getGroupExtList(force: boolean): Promise<GeneralCallResult>;
|
||||
|
||||
getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<unknown>;
|
||||
getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<GeneralCallResult>;
|
||||
|
||||
getMemberExtInfo(param: GroupExtParam): Promise<unknown>;//req
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { GeneralCallResult } from '@/core/services/common';
|
||||
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/core/types/msg';
|
||||
|
||||
export interface NodeIKernelMsgService {
|
||||
buildMultiForwardMsg(req: { srcMsgIds: Array<string>, srcContact: Peer }): Promise<GeneralCallResult & { rspInfo: { elements: unknown } }>;
|
||||
|
||||
generateMsgUniqueId(chatType: number, time: string): string;
|
||||
|
||||
|
@@ -16,7 +16,7 @@ export interface NodeIKernelSearchService {
|
||||
penetrate: string
|
||||
}): Promise<GeneralCallResult>;// needs 1 arguments
|
||||
|
||||
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown;
|
||||
searchLocalInfo(keywords: string, type: number/*4*/): unknown;
|
||||
|
||||
cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments
|
||||
|
||||
|
@@ -56,6 +56,7 @@ export interface GrayTipElement {
|
||||
aioOpGrayTipElement: TipAioOpGrayTipElement;
|
||||
groupElement: TipGroupElement;
|
||||
xmlElement: {
|
||||
busiId: string;
|
||||
content: string;
|
||||
templId: string;
|
||||
};
|
||||
|
@@ -17,7 +17,160 @@ export enum GroupInfoSource {
|
||||
KRECENTCONTACT,
|
||||
KMOREPANEL
|
||||
}
|
||||
|
||||
export interface GroupDetailInfo {
|
||||
groupCode: string;
|
||||
groupUin: string;
|
||||
ownerUid: string;
|
||||
ownerUin: string;
|
||||
groupFlag: number;
|
||||
groupFlagExt: number;
|
||||
maxMemberNum: number;
|
||||
memberNum: number;
|
||||
groupOption: number;
|
||||
classExt: number;
|
||||
groupName: string;
|
||||
fingerMemo: string;
|
||||
groupQuestion: string;
|
||||
certType: number;
|
||||
richFingerMemo: string;
|
||||
tagRecord: any[];
|
||||
shutUpAllTimestamp: number;
|
||||
shutUpMeTimestamp: number;
|
||||
groupTypeFlag: number;
|
||||
privilegeFlag: number;
|
||||
groupSecLevel: number;
|
||||
groupFlagExt3: number;
|
||||
isConfGroup: number;
|
||||
isModifyConfGroupFace: number;
|
||||
isModifyConfGroupName: number;
|
||||
groupFlagExt4: number;
|
||||
groupMemo: string;
|
||||
cmdUinMsgSeq: number;
|
||||
cmdUinJoinTime: number;
|
||||
cmdUinUinFlag: number;
|
||||
cmdUinMsgMask: number;
|
||||
groupSecLevelInfo: number;
|
||||
cmdUinPrivilege: number;
|
||||
cmdUinFlagEx2: number;
|
||||
appealDeadline: number;
|
||||
remarkName: string;
|
||||
isTop: boolean;
|
||||
groupFace: number;
|
||||
groupGeoInfo: {
|
||||
ownerUid: string;
|
||||
SetTime: number;
|
||||
CityId: number;
|
||||
Longitude: string;
|
||||
Latitude: string;
|
||||
GeoContent: string;
|
||||
poiId: string;
|
||||
};
|
||||
certificationText: string;
|
||||
cmdUinRingtoneId: number;
|
||||
longGroupName: string;
|
||||
autoAgreeJoinGroupUserNumForConfGroup: number;
|
||||
autoAgreeJoinGroupUserNumForNormalGroup: number;
|
||||
cmdUinFlagExt3Grocery: number;
|
||||
groupCardPrefix: {
|
||||
introduction: string;
|
||||
rptPrefix: any[];
|
||||
};
|
||||
groupExt: {
|
||||
groupInfoExtSeq: number;
|
||||
reserve: number;
|
||||
luckyWordId: string;
|
||||
lightCharNum: number;
|
||||
luckyWord: string;
|
||||
starId: number;
|
||||
essentialMsgSwitch: number;
|
||||
todoSeq: number;
|
||||
blacklistExpireTime: number;
|
||||
isLimitGroupRtc: number;
|
||||
companyId: number;
|
||||
hasGroupCustomPortrait: number;
|
||||
bindGuildId: string;
|
||||
groupOwnerId: {
|
||||
memberUin: string;
|
||||
memberUid: string;
|
||||
memberQid: string;
|
||||
};
|
||||
essentialMsgPrivilege: number;
|
||||
msgEventSeq: string;
|
||||
inviteRobotSwitch: number;
|
||||
gangUpId: string;
|
||||
qqMusicMedalSwitch: number;
|
||||
showPlayTogetherSwitch: number;
|
||||
groupFlagPro1: string;
|
||||
groupBindGuildIds: {
|
||||
guildIds: any[];
|
||||
};
|
||||
viewedMsgDisappearTime: string;
|
||||
groupExtFlameData: {
|
||||
switchState: number;
|
||||
state: number;
|
||||
dayNums: any[];
|
||||
version: number;
|
||||
updateTime: string;
|
||||
isDisplayDayNum: boolean;
|
||||
};
|
||||
groupBindGuildSwitch: number;
|
||||
groupAioBindGuildId: string;
|
||||
groupExcludeGuildIds: {
|
||||
guildIds: any[];
|
||||
};
|
||||
fullGroupExpansionSwitch: number;
|
||||
fullGroupExpansionSeq: string;
|
||||
inviteRobotMemberSwitch: number;
|
||||
inviteRobotMemberExamine: number;
|
||||
groupSquareSwitch: number;
|
||||
};
|
||||
msgLimitFrequency: number;
|
||||
hlGuildAppid: number;
|
||||
hlGuildSubType: number;
|
||||
isAllowRecallMsg: number;
|
||||
confUin: string;
|
||||
confMaxMsgSeq: number;
|
||||
confToGroupTime: number;
|
||||
groupSchoolInfo: {
|
||||
location: string;
|
||||
grade: number;
|
||||
school: string;
|
||||
};
|
||||
activeMemberNum: number;
|
||||
groupGrade: number;
|
||||
groupCreateTime: number;
|
||||
subscriptionUin: string;
|
||||
subscriptionUid: string;
|
||||
noFingerOpenFlag: number;
|
||||
noCodeFingerOpenFlag: number;
|
||||
isGroupFreeze: number;
|
||||
allianceId: string;
|
||||
groupExtOnly: {
|
||||
tribeId: number;
|
||||
moneyForAddGroup: number;
|
||||
};
|
||||
isAllowConfGroupMemberModifyGroupName: number;
|
||||
isAllowConfGroupMemberNick: number;
|
||||
isAllowConfGroupMemberAtAll: number;
|
||||
groupClassText: string;
|
||||
groupFreezeReason: number;
|
||||
headPortraitSeq: number;
|
||||
groupHeadPortrait: {
|
||||
portraitCnt: number;
|
||||
portraitInfo: any[];
|
||||
defaultId: number;
|
||||
verifyingPortraitCnt: number;
|
||||
verifyingPortraitInfo: any[];
|
||||
};
|
||||
cmdUinJoinMsgSeq: number;
|
||||
cmdUinJoinRealMsgSeq: number;
|
||||
groupAnswer: string;
|
||||
groupAdminMaxNum: number;
|
||||
inviteNoAuthNumLimit: string;
|
||||
hlGuildOrgId: number;
|
||||
isAllowHlGuildBinary: number;
|
||||
localExitGroupReason: number;
|
||||
}
|
||||
export interface GroupExt0xEF0InfoFilter {
|
||||
bindGuildId: number;
|
||||
blacklistExpireTime: number;
|
||||
|
@@ -508,7 +508,7 @@ export interface RawMessage {
|
||||
*/
|
||||
export interface QueryMsgsParams {
|
||||
chatInfo: Peer;
|
||||
filterMsgType: [];
|
||||
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
|
||||
filterSendersUid: string[];
|
||||
filterMsgFromTime: string;
|
||||
filterMsgToTime: string;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
//LiteLoader需要提供部分IPC接口,以便于其他插件调用
|
||||
const { ipcMain } = require('electron');
|
||||
const { ipcMain, BrowserWindow } = require('electron');
|
||||
const napcat = require('./napcat.cjs');
|
||||
const { shell } = require('electron');
|
||||
ipcMain.handle('napcat_get_webtoken', async (event, arg) => {
|
||||
@@ -13,4 +13,14 @@ ipcMain.handle('napcat_get_reactweb', async (event, arg) => {
|
||||
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}`;
|
||||
});
|
||||
|
||||
ipcMain.on('napcat_open_inner_url', (event, url) => {
|
||||
const win = new BrowserWindow({
|
||||
autoHideMenuBar: true,
|
||||
});
|
||||
win.loadURL(url);
|
||||
win.webContents.setWindowOpenHandler(details => {
|
||||
win.loadURL(details.url)
|
||||
})
|
||||
});
|
@@ -9,6 +9,7 @@ import { NodeIKernelLoginService } from '@/core/services';
|
||||
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
|
||||
import { InitWebUi, WebUiConfig } from '@/webui';
|
||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||
import { UmamiTrace } from '@/common/umami';
|
||||
|
||||
//Framework ES入口文件
|
||||
export async function getWebUiUrl() {
|
||||
@@ -25,8 +26,10 @@ export async function NCoreInitFramework(
|
||||
console.log('NapCat Framework App Loading...');
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
UmamiTrace.sendEvent('framework/error', { name: err.name });
|
||||
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.log('[NapCat] [Error] unhandledRejection:', reason);
|
||||
});
|
||||
@@ -35,6 +38,10 @@ export async function NCoreInitFramework(
|
||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
|
||||
let guid = loginService.getMachineGuid();
|
||||
UmamiTrace.init(basicInfoWrapper.getFullQQVesion(), guid);
|
||||
UmamiTrace.sendTrace('framework/boot');
|
||||
UmamiTrace.sendEvent('framework/login');
|
||||
//直到登录成功后,执行下一步
|
||||
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
|
||||
const loginListener = new NodeIKernelLoginListener();
|
||||
|
@@ -6,6 +6,9 @@ const napcat = {
|
||||
openExternalUrl: async (url) => {
|
||||
ipcRenderer.send('open_external_url', url);
|
||||
},
|
||||
openInnerUrl: async (url) => {
|
||||
ipcRenderer.send('napcat_open_inner_url', url);
|
||||
},
|
||||
getWebUiUrlReact: async () => {
|
||||
return ipcRenderer.invoke('napcat_get_reactweb');
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export const onSettingWindowCreated = async (view) => {
|
||||
`;
|
||||
|
||||
view.querySelector('.nc_openwebui').addEventListener('click', () => {
|
||||
window.open(webui, '_blank');
|
||||
window.napcat.openInnerUrl(webui);
|
||||
});
|
||||
view.querySelector('.nc_openwebui_ex').addEventListener('click', () => {
|
||||
window.napcat.openExternalUrl(webui);
|
||||
|
@@ -29,7 +29,7 @@ export class OB11Response {
|
||||
}
|
||||
|
||||
export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||
actionName: ActionName = ActionName.Unknown;
|
||||
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
|
||||
core: NapCatCore;
|
||||
private validate: ValidateFunction<any> | undefined = undefined;
|
||||
payloadSchema: any = undefined;
|
||||
@@ -84,4 +84,4 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||
}
|
||||
|
||||
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
|
||||
}
|
||||
}
|
||||
|
14
src/onebot/action/extends/GetClientkey.ts
Normal file
14
src/onebot/action/extends/GetClientkey.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { OneBotAction } from '../OneBotAction';
|
||||
|
||||
interface GetClientkeyResponse {
|
||||
clientkey?: string;
|
||||
}
|
||||
|
||||
export class GetClientkey extends OneBotAction<void, GetClientkeyResponse> {
|
||||
actionName = ActionName.GetClientkey;
|
||||
|
||||
async _handle() {
|
||||
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };
|
||||
}
|
||||
}
|
@@ -1,17 +1,7 @@
|
||||
import { GroupNotifyMsgStatus } from '@/core';
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
|
||||
interface Notify {
|
||||
request_id: string;
|
||||
invitor_uin: number;
|
||||
invitor_nick?: string;
|
||||
group_id?: number;
|
||||
group_name?: string;
|
||||
checked: boolean;
|
||||
requester_nick?: string;
|
||||
actor: number;
|
||||
}
|
||||
import { Notify } from '@/onebot/types';
|
||||
|
||||
export default class GetGroupAddRequest extends OneBotAction<null, Notify[] | null> {
|
||||
actionName = ActionName.GetGroupIgnoreAddRequest;
|
||||
@@ -28,13 +18,15 @@ export default class GetGroupAddRequest extends OneBotAction<null, Notify[] | nu
|
||||
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.group.groupCode}|${SSNotify.seq}|${SSNotify.type}`,
|
||||
request_id: +SSNotify.seq,
|
||||
invitor_uin: invitorUin,
|
||||
requester_nick: SSNotify.user1?.nickName,
|
||||
invitor_nick: SSNotify.user1?.nickName,
|
||||
group_id: +SSNotify.group?.groupCode,
|
||||
message: SSNotify?.postscript,
|
||||
group_name: SSNotify.group?.groupName,
|
||||
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
|
||||
actor: actorUin,
|
||||
requester_nick: SSNotify.user1?.nickName,
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -11,7 +11,8 @@ const SchemaData = Type.Union([
|
||||
desc: Type.String(),
|
||||
picUrl: Type.String(),
|
||||
jumpUrl: Type.String(),
|
||||
rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
|
||||
webUrl: Type.Optional(Type.String()),
|
||||
rawArkData: Type.Optional(Type.Union([Type.String()]))
|
||||
}),
|
||||
Type.Object({
|
||||
title: Type.String(),
|
||||
@@ -19,6 +20,7 @@ const SchemaData = Type.Union([
|
||||
picUrl: Type.String(),
|
||||
jumpUrl: Type.String(),
|
||||
iconUrl: Type.String(),
|
||||
webUrl: Type.Optional(Type.String()),
|
||||
appId: Type.String(),
|
||||
scene: Type.Union([Type.Number(), Type.String()]),
|
||||
templateType: Type.Union([Type.Number(), Type.String()]),
|
||||
@@ -28,7 +30,7 @@ const SchemaData = Type.Union([
|
||||
versionId: Type.String(),
|
||||
sdkId: Type.String(),
|
||||
withShareTicket: Type.Union([Type.Number(), Type.String()]),
|
||||
rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
|
||||
rawArkData: Type.Optional(Type.Union([Type.String()]))
|
||||
})
|
||||
]);
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
@@ -45,7 +47,8 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
|
||||
title: payload.title,
|
||||
desc: payload.desc,
|
||||
picUrl: payload.picUrl,
|
||||
jumpUrl: payload.jumpUrl
|
||||
jumpUrl: payload.jumpUrl,
|
||||
webUrl: payload.webUrl,
|
||||
} as MiniAppReqCustomParams;
|
||||
if ('type' in payload) {
|
||||
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
|
||||
@@ -63,13 +66,13 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
|
||||
verType: +verType,
|
||||
shareType: +shareType,
|
||||
versionId: versionId,
|
||||
withShareTicket: +withShareTicket
|
||||
withShareTicket: +withShareTicket,
|
||||
}
|
||||
);
|
||||
}
|
||||
const arkData = await this.core.apis.PacketApi.pkt.operation.GetMiniAppAdaptShareInfo(reqParam);
|
||||
return {
|
||||
data: payload.rawArkData ? arkData : MiniAppInfoHelper.RawToSend(arkData)
|
||||
data: payload.rawArkData === 'true' ? arkData : MiniAppInfoHelper.RawToSend(arkData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,7 @@ const SchemaData = Type.Object({
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export class OCRImage extends OneBotAction<Payload, any> {
|
||||
actionName = ActionName.OCRImage;
|
||||
class OCRImageBase extends OneBotAction<Payload, any> {
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
@@ -34,6 +33,10 @@ export class OCRImage extends OneBotAction<Payload, any> {
|
||||
}
|
||||
}
|
||||
|
||||
export class IOCRImage extends OCRImage {
|
||||
export class OCRImage extends OCRImageBase {
|
||||
actionName = ActionName.OCRImage;
|
||||
}
|
||||
|
||||
export class IOCRImage extends OCRImageBase {
|
||||
actionName = ActionName.IOCRImage;
|
||||
}
|
||||
|
21
src/onebot/action/extends/SendPacket.ts
Normal file
21
src/onebot/action/extends/SendPacket.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
cmd: Type.String(),
|
||||
data: Type.String(),
|
||||
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true }),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export class SendPacket extends GetPacketStatusDepends<Payload, any> {
|
||||
payloadSchema = SchemaData;
|
||||
actionName = ActionName.SendPacket;
|
||||
async _handle(payload: Payload) {
|
||||
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
|
||||
const data = await this.core.apis.PacketApi.pkt.client.sendOidbPacket({ cmd: payload.cmd, data: payload.data as any }, rsp);
|
||||
return typeof data === 'object' ? data.toString('hex') : undefined;
|
||||
}
|
||||
}
|
@@ -8,14 +8,18 @@ const SchemaData = Type.Object({
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export class SetGroupSign extends GetPacketStatusDepends<Payload, any> {
|
||||
actionName = ActionName.SetGroupSign;
|
||||
class SetGroupSignBase extends GetPacketStatusDepends<Payload, any> {
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id);
|
||||
}
|
||||
}
|
||||
export class SendGroupSign extends SetGroupSign {
|
||||
|
||||
export class SetGroupSign extends SetGroupSignBase {
|
||||
actionName = ActionName.SendGroupSign;
|
||||
}
|
||||
|
||||
export class SendGroupSign extends SetGroupSignBase {
|
||||
actionName = ActionName.SendGroupSign;
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import fs from 'fs';
|
||||
import { join as joinPath } from 'node:path';
|
||||
import { calculateFileMD5, httpDownload } from '@/common/file';
|
||||
import { calculateFileMD5, uriToLocalFile } from '@/common/file';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
@@ -26,17 +26,20 @@ export default class GoCQHTTPDownloadFile extends OneBotAction<Payload, FileResp
|
||||
async _handle(payload: Payload): Promise<FileResponse> {
|
||||
const isRandomName = !payload.name;
|
||||
const name = payload.name || randomUUID();
|
||||
const filePath = joinPath(this.core.NapCatTempPath, name);
|
||||
let result: Awaited<ReturnType<typeof uriToLocalFile>>;
|
||||
|
||||
if (payload.base64) {
|
||||
fs.writeFileSync(filePath, payload.base64, 'base64');
|
||||
result = await uriToLocalFile(this.core.NapCatTempPath, `base64://${payload.base64}`, name);
|
||||
} else if (payload.url) {
|
||||
const headers = this.getHeaders(payload.headers);
|
||||
const buffer = await httpDownload({ url: payload.url, headers: headers });
|
||||
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary');
|
||||
result = await uriToLocalFile(this.core.NapCatTempPath, payload.url, name, headers);
|
||||
} else {
|
||||
throw new Error('不存在任何文件, 无法下载');
|
||||
}
|
||||
if (!result.success) {
|
||||
throw new Error(result.errMsg);
|
||||
}
|
||||
const filePath = result.path;
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
||||
if (isRandomName) {
|
||||
|
@@ -1,40 +1,29 @@
|
||||
import { GroupNotifyMsgStatus } from '@/core';
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { Notify } from '@/onebot/types';
|
||||
|
||||
interface Notify {
|
||||
request_id: string;
|
||||
invitor_uin: number;
|
||||
invitor_nick?: string;
|
||||
group_id?: number;
|
||||
group_name?: string;
|
||||
checked: boolean;
|
||||
requester_nick?: string;
|
||||
actor: number;
|
||||
}
|
||||
interface RetData {
|
||||
InvitedRequest: Notify[];
|
||||
join_requests: Notify[];
|
||||
}
|
||||
|
||||
export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
|
||||
actionName = ActionName.GetGroupSystemMsg;
|
||||
actionName = ActionName.GetGroupIgnoredNotifies;
|
||||
|
||||
async _handle(): Promise<RetData> {
|
||||
const NTQQUserApi = this.core.apis.UserApi;
|
||||
const NTQQGroupApi = this.core.apis.GroupApi;
|
||||
const SingleScreenNotifies = await NTQQGroupApi.getSingleScreenNotifies(false, 50);
|
||||
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
|
||||
const retData: RetData = { InvitedRequest: [], join_requests: [] };
|
||||
|
||||
const notifyPromises = SingleScreenNotifies.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;
|
||||
|
||||
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
||||
const actorUin = SSNotify.user2?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2.uid) : 0;
|
||||
const commonData = {
|
||||
request_id: `${SSNotify.group.groupCode}|${SSNotify.seq}|${SSNotify.type}`,
|
||||
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: actorUin,
|
||||
|
@@ -17,14 +17,13 @@ class GetGroupInfo extends OneBotAction<Payload, OB11Group> {
|
||||
async _handle(payload: Payload) {
|
||||
const group = (await this.core.apis.GroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString());
|
||||
if (!group) {
|
||||
const data = await this.core.apis.GroupApi.searchGroup(payload.group_id.toString());
|
||||
if (!data) throw new Error('Group not found');
|
||||
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
|
||||
return {
|
||||
...data.searchGroupInfo,
|
||||
...data,
|
||||
group_id: +payload.group_id,
|
||||
group_name: data.searchGroupInfo.groupName,
|
||||
member_count: data.searchGroupInfo.memberNum,
|
||||
max_member_count: data.searchGroupInfo.maxMemberNum,
|
||||
group_name: data.groupName,
|
||||
member_count: data.memberNum,
|
||||
max_member_count: data.maxMemberNum,
|
||||
};
|
||||
}
|
||||
return OB11Construct.group(group);
|
||||
|
@@ -29,13 +29,14 @@ class GetGroupMemberInfo extends OneBotAction<Payload, OB11GroupMember> {
|
||||
async _handle(payload: Payload) {
|
||||
const isNocache = this.parseBoolean(payload.no_cache ?? true);
|
||||
const uid = await this.getUid(payload.user_id);
|
||||
const [member, info] = await Promise.all([
|
||||
const groupMember = this.core.apis.GroupApi.groupMemberCache.get(payload.group_id.toString())?.get(uid);
|
||||
let [member, info] = await Promise.all([
|
||||
this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, isNocache),
|
||||
this.core.apis.UserApi.getUserDetailInfo(uid),
|
||||
]);
|
||||
if (!member) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
|
||||
if (!member || !groupMember) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
|
||||
if (info) {
|
||||
Object.assign(member, info);
|
||||
member = { ...groupMember, ...member, ...info };
|
||||
} else {
|
||||
this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`);
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import SendMsg, { ContextMode } from '@/onebot/action/msg/SendMsg';
|
||||
import { ContextMode, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
||||
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
||||
import { OB11PostSendMsg } from '@/onebot/types';
|
||||
|
||||
// 未检测参数
|
||||
class SendGroupMsg extends SendMsg {
|
||||
class SendGroupMsg extends SendMsgBase {
|
||||
actionName = ActionName.SendGroupMsg;
|
||||
contextMode: ContextMode = ContextMode.Group;
|
||||
|
||||
|
@@ -4,9 +4,9 @@ import { ActionName } from '@/onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
flag: Type.String(),
|
||||
flag: Type.Union([Type.String(), Type.Number()]),
|
||||
approve: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
|
||||
reason: Type.Union([Type.String({ default: ' ' }), Type.Null()]),
|
||||
reason: Type.Optional(Type.Union([Type.String({ default: ' ' }), Type.Null()])),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
@@ -18,10 +18,25 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
|
||||
async _handle(payload: Payload): Promise<null> {
|
||||
const flag = payload.flag.toString();
|
||||
const approve = payload.approve?.toString() !== 'false';
|
||||
await this.core.apis.GroupApi.handleGroupRequest(flag,
|
||||
const reason = payload.reason ?? ' ';
|
||||
let invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag);
|
||||
const notify = invite_notify ?? await this.findNotify(flag);
|
||||
if (!notify) {
|
||||
throw new Error('No such request');
|
||||
}
|
||||
await this.core.apis.GroupApi.handleGroupRequest(
|
||||
notify,
|
||||
approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
|
||||
payload.reason ?? ' ',
|
||||
reason,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async findNotify(flag: string) {
|
||||
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
|
||||
if (!notify) {
|
||||
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag);
|
||||
}
|
||||
return notify;
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ export default class SetGroupBan extends OneBotAction<Payload, null> {
|
||||
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||
if (!uid) throw new Error('uid error');
|
||||
await this.core.apis.GroupApi.banMember(payload.group_id.toString(),
|
||||
[{ uid: uid, timeStamp: +payload.duration}]);
|
||||
[{ uid: uid, timeStamp: +payload.duration }]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -102,11 +102,11 @@ import { SendGroupAiRecord } from "@/onebot/action/group/SendGroupAiRecord";
|
||||
import { GetAiCharacters } from "@/onebot/action/extends/GetAiCharacters";
|
||||
import { GetGuildList } from './guild/GetGuildList';
|
||||
import { GetGuildProfile } from './guild/GetGuildProfile';
|
||||
import { GetClientkey } from './extends/GetClientkey';
|
||||
import { SendPacket } from './extends/SendPacket';
|
||||
import { SendPoke } from "@/onebot/action/packet/SendPoke";
|
||||
|
||||
|
||||
export type ActionMap = Map<string, OneBotAction<any, any>>;
|
||||
|
||||
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore): ActionMap {
|
||||
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||
|
||||
const actionHandlers = [
|
||||
new GetGroupInfoEx(obContext, core),
|
||||
@@ -126,6 +126,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
new GetGroupRootFiles(obContext, core),
|
||||
new SetGroupSign(obContext, core),
|
||||
new SendGroupSign(obContext, core),
|
||||
new GetClientkey(obContext, core),
|
||||
// onebot11
|
||||
new SendLike(obContext, core),
|
||||
new GetMsg(obContext, core),
|
||||
@@ -219,13 +220,33 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
new GetAiRecord(obContext, core),
|
||||
new SendGroupAiRecord(obContext, core),
|
||||
new GetAiCharacters(obContext, core),
|
||||
new SendPacket(obContext, core),
|
||||
new SendPoke(obContext, core),
|
||||
];
|
||||
const actionMap = new Map();
|
||||
for (const action of actionHandlers) {
|
||||
actionMap.set(action.actionName, action);
|
||||
actionMap.set(action.actionName + '_async', action);
|
||||
actionMap.set(action.actionName + '_rate_limited', action);
|
||||
|
||||
type HandlerUnion = typeof actionHandlers[number];
|
||||
type MapType = {
|
||||
[H in HandlerUnion as H['actionName']]: H;
|
||||
} & {
|
||||
[H in HandlerUnion as `${H['actionName']}_async`]: H;
|
||||
} & {
|
||||
[H in HandlerUnion as `${H['actionName']}_rate_limited`]: H;
|
||||
};
|
||||
|
||||
const _map = new Map<keyof MapType, HandlerUnion>();
|
||||
|
||||
actionHandlers.forEach(h => {
|
||||
_map.set(h.actionName as keyof MapType, h);
|
||||
_map.set(`${h.actionName}_async` as keyof MapType, h);
|
||||
_map.set(`${h.actionName}_rate_limited` as keyof MapType, h);
|
||||
});
|
||||
|
||||
function get<K extends keyof MapType>(key: K): MapType[K];
|
||||
function get<K extends keyof MapType>(key: K): null;
|
||||
function get<K extends keyof MapType>(key: K): HandlerUnion | null | MapType[K] {
|
||||
return _map.get(key as keyof MapType) ?? null;
|
||||
}
|
||||
|
||||
return actionMap;
|
||||
return { get };
|
||||
}
|
||||
export type ActionMap = ReturnType<typeof createActionMap>
|
||||
|
@@ -88,8 +88,7 @@ function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType
|
||||
return 0;
|
||||
}
|
||||
|
||||
export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
||||
actionName = ActionName.SendMsg;
|
||||
export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
||||
contextMode = ContextMode.Normal;
|
||||
|
||||
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||
@@ -379,4 +378,6 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
||||
}
|
||||
}
|
||||
|
||||
export default SendMsg;
|
||||
export default class SendMsg extends SendMsgBase {
|
||||
actionName = ActionName.SendMsg;
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import SendMsg, { ContextMode } from './SendMsg';
|
||||
import { ContextMode, SendMsgBase } from './SendMsg';
|
||||
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
||||
import { OB11PostSendMsg } from '@/onebot/types';
|
||||
|
||||
// 未检测参数
|
||||
class SendPrivateMsg extends SendMsg {
|
||||
class SendPrivateMsg extends SendMsgBase {
|
||||
actionName = ActionName.SendPrivateMsg;
|
||||
contextMode: ContextMode = ContextMode.Private;
|
||||
|
||||
|
@@ -3,8 +3,6 @@ import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
||||
|
||||
|
||||
export abstract class GetPacketStatusDepends<PT, RT> extends OneBotAction<PT, RT> {
|
||||
actionName = ActionName.GetPacketStatus;
|
||||
|
||||
protected async check(payload: PT): Promise<BaseCheckResult>{
|
||||
if (!this.core.apis.PacketApi.available) {
|
||||
return {
|
||||
@@ -18,6 +16,8 @@ export abstract class GetPacketStatusDepends<PT, RT> extends OneBotAction<PT, RT
|
||||
}
|
||||
|
||||
export class GetPacketStatus extends GetPacketStatusDepends<any, null> {
|
||||
actionName = ActionName.GetPacketStatus;
|
||||
|
||||
async _handle(payload: any) {
|
||||
return null;
|
||||
}
|
||||
|
23
src/onebot/action/packet/SendPoke.ts
Normal file
23
src/onebot/action/packet/SendPoke.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
|
||||
user_id: Type.Union([Type.Number(), Type.String()]),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export class SendPoke extends GetPacketStatusDepends<Payload, any> {
|
||||
actionName = ActionName.SendPoke;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
if (payload.group_id) {
|
||||
this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
|
||||
} else {
|
||||
this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,134 +13,138 @@ export interface InvalidCheckResult {
|
||||
[k: string | number]: any;
|
||||
}
|
||||
|
||||
export enum ActionName {
|
||||
export const ActionName = {
|
||||
// onebot 11
|
||||
SendPrivateMsg = 'send_private_msg',
|
||||
SendGroupMsg = 'send_group_msg',
|
||||
SendMsg = 'send_msg',
|
||||
DeleteMsg = 'delete_msg',
|
||||
GetMsg = 'get_msg',
|
||||
GoCQHTTP_GetForwardMsg = 'get_forward_msg',
|
||||
SendLike = 'send_like',
|
||||
SetGroupKick = 'set_group_kick',
|
||||
SetGroupBan = 'set_group_ban',
|
||||
// SetGroupAnoymousBan = 'set_group_anonymous_ban',
|
||||
SetGroupWholeBan = 'set_group_whole_ban',
|
||||
SetGroupAdmin = 'set_group_admin',
|
||||
// SetGroupAnoymous = 'set_group_anonymous',
|
||||
SetGroupCard = 'set_group_card',
|
||||
SetGroupName = 'set_group_name',
|
||||
SetGroupLeave = 'set_group_leave',
|
||||
SetSpecialTittle = 'set_group_special_title',
|
||||
SetFriendAddRequest = 'set_friend_add_request',
|
||||
SetGroupAddRequest = 'set_group_add_request',
|
||||
GetLoginInfo = 'get_login_info',
|
||||
GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
|
||||
GetFriendList = 'get_friend_list',
|
||||
GetGroupInfo = 'get_group_info',
|
||||
GetGroupList = 'get_group_list',
|
||||
GetGroupMemberInfo = 'get_group_member_info',
|
||||
GetGroupMemberList = 'get_group_member_list',
|
||||
GetGroupHonorInfo = 'get_group_honor_info',
|
||||
GetCookies = 'get_cookies',
|
||||
GetCSRF = 'get_csrf_token',
|
||||
GetCredentials = 'get_credentials',
|
||||
GetRecord = 'get_record',
|
||||
GetImage = 'get_image',
|
||||
CanSendImage = 'can_send_image',
|
||||
CanSendRecord = 'can_send_record',
|
||||
GetStatus = 'get_status',
|
||||
GetVersionInfo = 'get_version_info',
|
||||
// Reboot = 'set_restart',
|
||||
// CleanCache = 'clean_cache',
|
||||
SendPrivateMsg: 'send_private_msg',
|
||||
SendGroupMsg: 'send_group_msg',
|
||||
SendMsg: 'send_msg',
|
||||
DeleteMsg: 'delete_msg',
|
||||
GetMsg: 'get_msg',
|
||||
GoCQHTTP_GetForwardMsg: 'get_forward_msg',
|
||||
SendLike: 'send_like',
|
||||
SetGroupKick: 'set_group_kick',
|
||||
SetGroupBan: 'set_group_ban',
|
||||
// SetGroupAnoymousBan : 'set_group_anonymous_ban',
|
||||
SetGroupWholeBan: 'set_group_whole_ban',
|
||||
SetGroupAdmin: 'set_group_admin',
|
||||
// SetGroupAnoymous : 'set_group_anonymous',
|
||||
SetGroupCard: 'set_group_card',
|
||||
SetGroupName: 'set_group_name',
|
||||
SetGroupLeave: 'set_group_leave',
|
||||
SetSpecialTittle: 'set_group_special_title',
|
||||
SetFriendAddRequest: 'set_friend_add_request',
|
||||
SetGroupAddRequest: 'set_group_add_request',
|
||||
GetLoginInfo: 'get_login_info',
|
||||
GoCQHTTP_GetStrangerInfo: 'get_stranger_info',
|
||||
GetFriendList: 'get_friend_list',
|
||||
GetGroupInfo: 'get_group_info',
|
||||
GetGroupList: 'get_group_list',
|
||||
GetGroupMemberInfo: 'get_group_member_info',
|
||||
GetGroupMemberList: 'get_group_member_list',
|
||||
GetGroupHonorInfo: 'get_group_honor_info',
|
||||
GetCookies: 'get_cookies',
|
||||
GetCSRF: 'get_csrf_token',
|
||||
GetCredentials: 'get_credentials',
|
||||
GetRecord: 'get_record',
|
||||
GetImage: 'get_image',
|
||||
CanSendImage: 'can_send_image',
|
||||
CanSendRecord: 'can_send_record',
|
||||
GetStatus: 'get_status',
|
||||
GetVersionInfo: 'get_version_info',
|
||||
// Reboot : 'set_restart',
|
||||
// CleanCache : 'clean_cache',
|
||||
|
||||
// go-cqhttp
|
||||
SetQQProfile = 'set_qq_profile',
|
||||
// QidianGetAccountInfo = 'qidian_get_account_info',
|
||||
GoCQHTTP_GetModelShow = '_get_model_show',
|
||||
GoCQHTTP_SetModelShow = '_set_model_show',
|
||||
GetOnlineClient = 'get_online_clients',
|
||||
// GetUnidirectionalFriendList = 'get_unidirectional_friend_list',
|
||||
GoCQHTTP_DeleteFriend = 'delete_friend',
|
||||
// DeleteUnidirectionalFriendList = 'delete_unidirectional_friend',
|
||||
GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read',
|
||||
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
|
||||
GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
|
||||
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
|
||||
OCRImage = 'ocr_image',
|
||||
IOCRImage = '.ocr_image',
|
||||
GetGroupSystemMsg = 'get_group_system_msg',
|
||||
GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list',
|
||||
GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
|
||||
SetGroupPortrait = 'set_group_portrait',
|
||||
SetEssenceMsg = 'set_essence_msg',
|
||||
DelEssenceMsg = 'delete_essence_msg',
|
||||
GoCQHTTP_SendGroupNotice = '_send_group_notice',
|
||||
GoCQHTTP_GetGroupNotice = '_get_group_notice',
|
||||
GoCQHTTP_UploadGroupFile = 'upload_group_file',
|
||||
GOCQHTTP_DeleteGroupFile = 'delete_group_file',
|
||||
GoCQHTTP_CreateGroupFileFolder = 'create_group_file_folder',
|
||||
GoCQHTTP_DeleteGroupFileFolder = 'delete_group_folder',
|
||||
GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info',
|
||||
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
|
||||
GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder',
|
||||
GOCQHTTP_GetGroupFileUrl = 'get_group_file_url',
|
||||
GOCQHTTP_UploadPrivateFile = 'upload_private_file',
|
||||
// GOCQHTTP_ReloadEventFilter = 'reload_event_filter',
|
||||
GoCQHTTP_DownloadFile = 'download_file',
|
||||
GoCQHTTP_CheckUrlSafely = 'check_url_safely',
|
||||
GoCQHTTP_GetWordSlices = '.get_word_slices',
|
||||
GoCQHTTP_HandleQuickAction = '.handle_quick_operation',
|
||||
SetQQProfile: 'set_qq_profile',
|
||||
// QidianGetAccountInfo : 'qidian_get_account_info',
|
||||
GoCQHTTP_GetModelShow: '_get_model_show',
|
||||
GoCQHTTP_SetModelShow: '_set_model_show',
|
||||
GetOnlineClient: 'get_online_clients',
|
||||
// GetUnidirectionalFriendList : 'get_unidirectional_friend_list',
|
||||
GoCQHTTP_DeleteFriend: 'delete_friend',
|
||||
// DeleteUnidirectionalFriendList : 'delete_unidirectional_friend',
|
||||
GoCQHTTP_MarkMsgAsRead: 'mark_msg_as_read',
|
||||
GoCQHTTP_SendGroupForwardMsg: 'send_group_forward_msg',
|
||||
GoCQHTTP_SendPrivateForwardMsg: 'send_private_forward_msg',
|
||||
GoCQHTTP_GetGroupMsgHistory: 'get_group_msg_history',
|
||||
OCRImage: 'ocr_image',
|
||||
IOCRImage: '.ocr_image',
|
||||
GetGroupSystemMsg: 'get_group_system_msg',
|
||||
GoCQHTTP_GetEssenceMsg: 'get_essence_msg_list',
|
||||
GoCQHTTP_GetGroupAtAllRemain: 'get_group_at_all_remain',
|
||||
SetGroupPortrait: 'set_group_portrait',
|
||||
SetEssenceMsg: 'set_essence_msg',
|
||||
DelEssenceMsg: 'delete_essence_msg',
|
||||
GoCQHTTP_SendGroupNotice: '_send_group_notice',
|
||||
GoCQHTTP_GetGroupNotice: '_get_group_notice',
|
||||
GoCQHTTP_UploadGroupFile: 'upload_group_file',
|
||||
GOCQHTTP_DeleteGroupFile: 'delete_group_file',
|
||||
GoCQHTTP_CreateGroupFileFolder: 'create_group_file_folder',
|
||||
GoCQHTTP_DeleteGroupFileFolder: 'delete_group_folder',
|
||||
GoCQHTTP_GetGroupFileSystemInfo: 'get_group_file_system_info',
|
||||
GoCQHTTP_GetGroupRootFiles: 'get_group_root_files',
|
||||
GoCQHTTP_GetGroupFilesByFolder: 'get_group_files_by_folder',
|
||||
GOCQHTTP_GetGroupFileUrl: 'get_group_file_url',
|
||||
GOCQHTTP_UploadPrivateFile: 'upload_private_file',
|
||||
// GOCQHTTP_ReloadEventFilter : 'reload_event_filter',
|
||||
GoCQHTTP_DownloadFile: 'download_file',
|
||||
GoCQHTTP_CheckUrlSafely: 'check_url_safely',
|
||||
GoCQHTTP_GetWordSlices: '.get_word_slices',
|
||||
GoCQHTTP_HandleQuickAction: '.handle_quick_operation',
|
||||
|
||||
// 以下为扩展napcat扩展
|
||||
Unknown = 'unknown',
|
||||
SharePeer = 'ArkSharePeer',
|
||||
ShareGroupEx = 'ArkShareGroup',
|
||||
// RebootNormal = 'reboot_normal', //无快速登录重新启动
|
||||
GetRobotUinRange = 'get_robot_uin_range',
|
||||
SetOnlineStatus = 'set_online_status',
|
||||
GetFriendsWithCategory = 'get_friends_with_category',
|
||||
SetQQAvatar = 'set_qq_avatar',
|
||||
GetFile = 'get_file',
|
||||
ForwardFriendSingleMsg = 'forward_friend_single_msg',
|
||||
ForwardGroupSingleMsg = 'forward_group_single_msg',
|
||||
TranslateEnWordToZn = 'translate_en2zh',
|
||||
SetMsgEmojiLike = 'set_msg_emoji_like',
|
||||
GoCQHTTP_SendForwardMsg = 'send_forward_msg',
|
||||
MarkPrivateMsgAsRead = 'mark_private_msg_as_read',
|
||||
MarkGroupMsgAsRead = 'mark_group_msg_as_read',
|
||||
GetFriendMsgHistory = 'get_friend_msg_history',
|
||||
CreateCollection = 'create_collection',
|
||||
GetCollectionList = 'get_collection_list',
|
||||
SetLongNick = 'set_self_longnick',
|
||||
GetRecentContact = 'get_recent_contact',
|
||||
_MarkAllMsgAsRead = '_mark_all_as_read',
|
||||
GetProfileLike = 'get_profile_like',
|
||||
FetchCustomFace = 'fetch_custom_face',
|
||||
FetchEmojiLike = 'fetch_emoji_like',
|
||||
SetInputStatus = 'set_input_status',
|
||||
GetGroupInfoEx = 'get_group_info_ex',
|
||||
GetGroupIgnoreAddRequest = 'get_group_ignore_add_request',
|
||||
DelGroupNotice = '_del_group_notice',
|
||||
FetchUserProfileLike = 'fetch_user_profile_like',
|
||||
FriendPoke = 'friend_poke',
|
||||
GroupPoke = 'group_poke',
|
||||
GetPacketStatus = 'nc_get_packet_status',
|
||||
GetUserStatus = 'nc_get_user_status',
|
||||
GetRkey = 'nc_get_rkey',
|
||||
GetGroupShutList = 'get_group_shut_list',
|
||||
Unknown: 'unknown',
|
||||
SharePeer: 'ArkSharePeer',
|
||||
ShareGroupEx: 'ArkShareGroup',
|
||||
// RebootNormal : 'reboot_normal', //无快速登录重新启动
|
||||
GetRobotUinRange: 'get_robot_uin_range',
|
||||
SetOnlineStatus: 'set_online_status',
|
||||
GetFriendsWithCategory: 'get_friends_with_category',
|
||||
SetQQAvatar: 'set_qq_avatar',
|
||||
GetFile: 'get_file',
|
||||
ForwardFriendSingleMsg: 'forward_friend_single_msg',
|
||||
ForwardGroupSingleMsg: 'forward_group_single_msg',
|
||||
TranslateEnWordToZn: 'translate_en2zh',
|
||||
SetMsgEmojiLike: 'set_msg_emoji_like',
|
||||
GoCQHTTP_SendForwardMsg: 'send_forward_msg',
|
||||
MarkPrivateMsgAsRead: 'mark_private_msg_as_read',
|
||||
MarkGroupMsgAsRead: 'mark_group_msg_as_read',
|
||||
GetFriendMsgHistory: 'get_friend_msg_history',
|
||||
CreateCollection: 'create_collection',
|
||||
GetCollectionList: 'get_collection_list',
|
||||
SetLongNick: 'set_self_longnick',
|
||||
GetRecentContact: 'get_recent_contact',
|
||||
_MarkAllMsgAsRead: '_mark_all_as_read',
|
||||
GetProfileLike: 'get_profile_like',
|
||||
FetchCustomFace: 'fetch_custom_face',
|
||||
FetchEmojiLike: 'fetch_emoji_like',
|
||||
SetInputStatus: 'set_input_status',
|
||||
GetGroupInfoEx: 'get_group_info_ex',
|
||||
GetGroupIgnoreAddRequest: 'get_group_ignore_add_request',
|
||||
DelGroupNotice: '_del_group_notice',
|
||||
FetchUserProfileLike: 'fetch_user_profile_like',
|
||||
FriendPoke: 'friend_poke',
|
||||
GroupPoke: 'group_poke',
|
||||
GetPacketStatus: 'nc_get_packet_status',
|
||||
GetUserStatus: 'nc_get_user_status',
|
||||
GetRkey: 'nc_get_rkey',
|
||||
GetGroupShutList: 'get_group_shut_list',
|
||||
|
||||
GetGuildList = 'get_guild_list',
|
||||
GetGuildProfile = 'get_guild_service_profile',
|
||||
GetGuildList: 'get_guild_list',
|
||||
GetGuildProfile: 'get_guild_service_profile',
|
||||
|
||||
GetGroupIgnoredNotifies = 'get_group_ignored_notifies',
|
||||
GetGroupIgnoredNotifies: 'get_group_ignored_notifies',
|
||||
|
||||
SetGroupSign = "set_group_sign",
|
||||
SendGroupSign = "send_group_sign",
|
||||
SetGroupSign: "set_group_sign",
|
||||
SendGroupSign: "send_group_sign",
|
||||
SendPacket: "send_packet",
|
||||
GetMiniAppArk: "get_mini_app_ark",
|
||||
// UploadForwardMsg : "upload_forward_msg",
|
||||
GetAiRecord: "get_ai_record",
|
||||
GetAiCharacters: "get_ai_characters",
|
||||
SendGroupAiRecord: "send_group_ai_record",
|
||||
|
||||
GetClientkey: "get_clientkey",
|
||||
|
||||
GetMiniAppArk = "get_mini_app_ark",
|
||||
// UploadForwardMsg = "upload_forward_msg",
|
||||
GetAiRecord = "get_ai_record",
|
||||
GetAiCharacters = "get_ai_characters",
|
||||
SendGroupAiRecord = "send_group_ai_record",
|
||||
}
|
||||
SendPoke: 'send_poke',
|
||||
} as const;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import CanSendRecord from './CanSendRecord';
|
||||
import CanSendRecord, { CanSend } from './CanSendRecord';
|
||||
|
||||
interface ReturnType {
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
export default class CanSendImage extends CanSendRecord {
|
||||
export default class CanSendImage extends CanSend {
|
||||
actionName = ActionName.CanSendImage;
|
||||
}
|
||||
|
@@ -5,12 +5,15 @@ interface ReturnType {
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
export default class CanSendRecord extends OneBotAction<any, ReturnType> {
|
||||
actionName = ActionName.CanSendRecord;
|
||||
|
||||
export class CanSend extends OneBotAction<any, ReturnType> {
|
||||
async _handle(_payload: void): Promise<ReturnType> {
|
||||
return {
|
||||
yes: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class CanSendRecord extends CanSend{
|
||||
actionName = ActionName.CanSendRecord;
|
||||
}
|
||||
|
@@ -1,59 +1,39 @@
|
||||
import { GroupNotifyMsgStatus } from '@/core';
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
|
||||
interface Notify {
|
||||
request_id: string;
|
||||
invitor_uin: number;
|
||||
invitor_nick?: string;
|
||||
group_id?: number;
|
||||
group_name?: string;
|
||||
checked: boolean;
|
||||
actor: number;
|
||||
}
|
||||
|
||||
interface JoinRequest extends Notify {
|
||||
requester_nick?: string;
|
||||
}
|
||||
import { Notify } from '@/onebot/types';
|
||||
|
||||
interface RetData {
|
||||
InvitedRequest: Notify[];
|
||||
join_requests: JoinRequest[];
|
||||
join_requests: Notify[];
|
||||
}
|
||||
|
||||
export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
|
||||
actionName = ActionName.GetGroupSystemMsg;
|
||||
|
||||
async _handle(): Promise<RetData> {
|
||||
const NTQQUserApi = this.core.apis.UserApi;
|
||||
const NTQQGroupApi = this.core.apis.GroupApi;
|
||||
const SingleScreenNotifies = await NTQQGroupApi.getSingleScreenNotifies(false, 50);
|
||||
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
|
||||
const retData: RetData = { InvitedRequest: [], join_requests: [] };
|
||||
|
||||
const notifyPromises = SingleScreenNotifies.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;
|
||||
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
||||
const actorUin = SSNotify.user2?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2.uid) : 0;
|
||||
const commonData = {
|
||||
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: actorUin,
|
||||
requester_nick: SSNotify.user1?.nickName,
|
||||
};
|
||||
|
||||
if (SSNotify.type === 1) {
|
||||
retData.InvitedRequest.push({
|
||||
request_id: `${SSNotify.group.groupCode}|${SSNotify.seq}|${SSNotify.type}`,
|
||||
invitor_uin: invitorUin,
|
||||
invitor_nick: SSNotify.user1?.nickName,
|
||||
group_id: +SSNotify.group?.groupCode,
|
||||
group_name: SSNotify.group?.groupName,
|
||||
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
|
||||
actor: actorUin,
|
||||
});
|
||||
retData.InvitedRequest.push(commonData);
|
||||
} else if (SSNotify.type === 7) {
|
||||
retData.join_requests.push({
|
||||
request_id: `${SSNotify.group.groupCode}|${SSNotify.seq}|${SSNotify.type}`,
|
||||
invitor_uin: invitorUin,
|
||||
requester_nick: SSNotify.user1?.nickName,
|
||||
group_id: +SSNotify.group?.groupCode,
|
||||
group_name: SSNotify.group?.groupName,
|
||||
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
|
||||
actor: actorUin,
|
||||
});
|
||||
retData.join_requests.push(commonData);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import { ActionName } from '@/onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
flag: Type.String(),
|
||||
flag: Type.Union([Type.String(), Type.Number()]),
|
||||
approve: Type.Optional(Type.Union([Type.String(), Type.Boolean()])),
|
||||
remark: Type.Optional(Type.String())
|
||||
});
|
||||
@@ -16,14 +16,13 @@ export default class SetFriendAddRequest extends OneBotAction<Payload, null> {
|
||||
|
||||
async _handle(payload: Payload): Promise<null> {
|
||||
const approve = payload.approve?.toString() !== 'false';
|
||||
await this.core.apis.FriendApi.handleFriendRequest(payload.flag, approve);
|
||||
const notify = (await this.core.apis.FriendApi.getBuddyReq()).buddyReqs.find(e => e.reqTime == payload.flag.toString());
|
||||
if (!notify) {
|
||||
throw new Error('No such request');
|
||||
}
|
||||
await this.core.apis.FriendApi.handleFriendRequest(notify, approve);
|
||||
if (payload.remark) {
|
||||
const data = payload.flag.split('|');
|
||||
if (data.length < 2) {
|
||||
throw new Error('Invalid flag');
|
||||
}
|
||||
const friendUid = data[0];
|
||||
await this.core.apis.FriendApi.setBuddyRemark(friendUid, payload.remark);
|
||||
await this.core.apis.FriendApi.setBuddyRemark(notify.friendUid, payload.remark);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@@ -12,16 +12,17 @@ export class OneBotFriendApi {
|
||||
}
|
||||
|
||||
//使用前预先判断 busiId 1061
|
||||
async parsePrivatePokeEvent(grayTipElement: GrayTipElement) {
|
||||
async parsePrivatePokeEvent(grayTipElement: GrayTipElement, uin: number) {
|
||||
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
let pokedetail: Array<{ uid: string }> = json.items;
|
||||
const pokedetail: Array<{ uid: string }> = json.items;
|
||||
//筛选item带有uid的元素
|
||||
pokedetail = pokedetail.filter(item => item.uid);
|
||||
if (pokedetail.length == 2) {
|
||||
const poke_uid = pokedetail.filter(item => item.uid);
|
||||
if (poke_uid.length == 2) {
|
||||
return new OB11FriendPokeEvent(
|
||||
this.core,
|
||||
parseInt((await this.core.apis.UserApi.getUinByUidV2(pokedetail[0].uid))),
|
||||
parseInt((await this.core.apis.UserApi.getUinByUidV2(pokedetail[1].uid))),
|
||||
uin,
|
||||
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid))),
|
||||
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid))),
|
||||
pokedetail,
|
||||
);
|
||||
}
|
||||
|
@@ -8,6 +8,8 @@ import {
|
||||
NapCatCore,
|
||||
NTGrayTipElementSubTypeV2,
|
||||
RawMessage,
|
||||
TipGroupElement,
|
||||
TipGroupElementType,
|
||||
} from '@/core';
|
||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||
import { OB11GroupBanEvent } from '@/onebot/event/notice/OB11GroupBanEvent';
|
||||
@@ -19,8 +21,10 @@ import { OB11GroupPokeEvent } from '@/onebot/event/notice/OB11PokeEvent';
|
||||
import { OB11GroupEssenceEvent } from '@/onebot/event/notice/OB11GroupEssenceEvent';
|
||||
import { OB11GroupTitleEvent } from '@/onebot/event/notice/OB11GroupTitleEvent';
|
||||
import { OB11GroupUploadNoticeEvent } from '../event/notice/OB11GroupUploadNoticeEvent';
|
||||
import { OB11GroupNameEvent } from '../event/notice/OB11GroupNameEvent';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { FileNapCatOneBotUUID } from '@/common/helper';
|
||||
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
|
||||
|
||||
export class OneBotGroupApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
@@ -204,13 +208,63 @@ export class OneBotGroupApi {
|
||||
);
|
||||
}
|
||||
|
||||
async parseGroupElement(msg: RawMessage, element: TipGroupElement, elementWrapper: GrayTipElement) {
|
||||
if (element.type === TipGroupElementType.KGROUPNAMEMODIFIED) {
|
||||
this.core.context.logger.logDebug('收到群名称变更事件', element);
|
||||
return new OB11GroupNameEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
parseInt(await this.core.apis.UserApi.getUinByUidV2(element.memberUid)),
|
||||
element.groupName,
|
||||
);
|
||||
} else if (element.type === TipGroupElementType.KSHUTUP) {
|
||||
let event = await this.parseGroupBanEvent(msg.peerUid, elementWrapper);
|
||||
return event;
|
||||
} else if (element.type === TipGroupElementType.KMEMBERADD) {
|
||||
// 自己的通知 协议推送为type->85 在这里实现为了避免邀请出现问题
|
||||
if (element.memberUid == this.core.selfInfo.uid) {
|
||||
this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
|
||||
return new OB11GroupIncreaseEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
+this.core.selfInfo.uin,
|
||||
element.adminUid ? +await this.core.apis.UserApi.getUinByUidV2(element.adminUid) : 0,
|
||||
'approve'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async parseSelfInviteEvent(msg: RawMessage, inviterUin: string, inviteeUin: string) {
|
||||
return new OB11GroupIncreaseEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
+inviteeUin,
|
||||
+inviterUin,
|
||||
'invite'
|
||||
);
|
||||
}
|
||||
|
||||
async parseGrayTipElement(msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
|
||||
// 解析群组事件 由sysmsg解析
|
||||
// return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
|
||||
|
||||
return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
|
||||
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) {
|
||||
// 筛选出表情回应 事件
|
||||
// 筛选自身入群情况
|
||||
// if (grayTipElement.xmlElement.busiId === '10145') {
|
||||
// const inviteData = new fastXmlParser.XMLParser({
|
||||
// ignoreAttributes: false,
|
||||
// attributeNamePrefix: '',
|
||||
// }).parse(grayTipElement.xmlElement.content);
|
||||
|
||||
// const inviterUin: string = inviteData.gtip.qq[0].jp;
|
||||
// const inviteeUin: string = inviteData.gtip.qq[1].jp;
|
||||
// //刷新群缓存
|
||||
// if (inviteeUin === this.core.selfInfo.uin) {
|
||||
// this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
|
||||
// return this.parseSelfInviteEvent(msg, inviterUin, inviteeUin);
|
||||
// }
|
||||
// } else
|
||||
if (grayTipElement.xmlElement?.templId === '10382') {
|
||||
return await this.obContext.apis.GroupApi.parseGroupEmojiLikeEventByGrayTip(msg.peerUid, grayTipElement);
|
||||
} else {
|
||||
|
@@ -1,19 +1,5 @@
|
||||
import type { OneBotFriendApi } from '@/onebot/api/friend';
|
||||
import type { OneBotUserApi } from '@/onebot/api/user';
|
||||
import type { OneBotGroupApi } from '@/onebot/api/group';
|
||||
import type { OneBotMsgApi } from '@/onebot/api/msg';
|
||||
import type { OneBotQuickActionApi } from '@/onebot/api/quick-action';
|
||||
|
||||
export * from './friend';
|
||||
export * from './group';
|
||||
export * from './user';
|
||||
export * from './msg';
|
||||
export * from './quick-action';
|
||||
|
||||
export interface StableOneBotApiWrapper {
|
||||
FriendApi: OneBotFriendApi;
|
||||
UserApi: OneBotUserApi;
|
||||
GroupApi: OneBotGroupApi;
|
||||
MsgApi: OneBotMsgApi;
|
||||
QuickActionApi: OneBotQuickActionApi,
|
||||
}
|
||||
export * from './quick-action';
|
@@ -17,6 +17,7 @@ import {
|
||||
SendTextElement,
|
||||
FaceType,
|
||||
GrayTipElement,
|
||||
GroupNotify,
|
||||
} from '@/core';
|
||||
import faceConfig from '@/core/external/face_config.json';
|
||||
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, } from '@/onebot';
|
||||
@@ -28,12 +29,14 @@ import { RequestUtil } from '@/common/request';
|
||||
import fsPromise, { constants } from 'node:fs/promises';
|
||||
import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
|
||||
import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
|
||||
import { GroupChange, PushMsgBody } from "@/core/packet/transformer/proto";
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
|
||||
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent';
|
||||
import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin';
|
||||
import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent';
|
||||
import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto';
|
||||
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
|
||||
import { LRUCache } from '@/common/lru-cache';
|
||||
|
||||
type RawToOb11Converters = {
|
||||
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
|
||||
@@ -67,7 +70,8 @@ function keyCanBeParsed(key: string, parser: RawToOb11Converters): key is keyof
|
||||
export class OneBotMsgApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: NapCatCore;
|
||||
|
||||
notifyGroupInvite: LRUCache<string, GroupNotify> = new LRUCache(50);
|
||||
// seq -> notify
|
||||
rawToOb11Converters: RawToOb11Converters = {
|
||||
textElement: async element => {
|
||||
if (element.atType === NTMsgAtType.ATTYPEUNKNOWN) {
|
||||
@@ -160,7 +164,7 @@ export class OneBotMsgApi {
|
||||
type: element?.pokeType?.toString() ?? '0',
|
||||
id: faceIndex.toString(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -461,7 +465,7 @@ export class OneBotMsgApi {
|
||||
},
|
||||
|
||||
[OB11MessageDataType.face]: async ({ data: { id } }) => {
|
||||
let parsedFaceId = +id;
|
||||
const parsedFaceId = +id;
|
||||
// 从face_config.json中获取表情名称
|
||||
const sysFaces = faceConfig.sysface;
|
||||
const face: any = sysFaces.find((systemFace) => systemFace.QSid === parsedFaceId.toString());
|
||||
@@ -679,7 +683,7 @@ export class OneBotMsgApi {
|
||||
async parsePrivateMsgEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
|
||||
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
|
||||
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement);
|
||||
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
||||
if (PokeEvent) { return PokeEvent; };
|
||||
} else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
|
||||
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
||||
@@ -891,49 +895,53 @@ export class OneBotMsgApi {
|
||||
if (!sendElements.length) {
|
||||
throw new Error('消息体无法解析, 请检查是否发送了不支持的消息类型');
|
||||
}
|
||||
let totalSize = 0;
|
||||
let timeout = 10000;
|
||||
try {
|
||||
for (const fileElement of sendElements) {
|
||||
if (fileElement.elementType === ElementType.PTT) {
|
||||
totalSize += (await fsPromise.stat(fileElement.pttElement.filePath)).size;
|
||||
|
||||
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => {
|
||||
const sizePromises = elements.map(async element => {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PTT:
|
||||
return (await fsPromise.stat(element.pttElement.filePath)).size;
|
||||
case ElementType.FILE:
|
||||
return (await fsPromise.stat(element.fileElement.filePath)).size;
|
||||
case ElementType.VIDEO:
|
||||
return (await fsPromise.stat(element.videoElement.filePath)).size;
|
||||
case ElementType.PIC:
|
||||
return (await fsPromise.stat(element.picElement.sourcePath)).size;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
if (fileElement.elementType === ElementType.FILE) {
|
||||
totalSize += (await fsPromise.stat(fileElement.fileElement.filePath)).size;
|
||||
}
|
||||
if (fileElement.elementType === ElementType.VIDEO) {
|
||||
totalSize += (await fsPromise.stat(fileElement.videoElement.filePath)).size;
|
||||
}
|
||||
if (fileElement.elementType === ElementType.PIC) {
|
||||
totalSize += (await fsPromise.stat(fileElement.picElement.sourcePath)).size;
|
||||
}
|
||||
}
|
||||
//且 PredictTime ((totalSize / 1024 / 512) * 1000)不等于Nan
|
||||
const PredictTime = totalSize / 1024 / 256 * 1000;
|
||||
if (!Number.isNaN(PredictTime)) {
|
||||
timeout += PredictTime;// 10S Basic Timeout + PredictTime( For File 512kb/s )
|
||||
}
|
||||
} catch (e) {
|
||||
});
|
||||
const sizes = await Promise.all(sizePromises);
|
||||
return sizes.reduce((total, size) => total + size, 0);
|
||||
};
|
||||
|
||||
const totalSize = await calculateTotalSize(sendElements).catch(e => {
|
||||
this.core.context.logger.logError('发送消息计算预计时间异常', e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const timeout = 10000 + (totalSize / 1024 / 256 * 1000);
|
||||
|
||||
const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
|
||||
if (!returnMsg) throw new Error('发送消息失败');
|
||||
|
||||
returnMsg.id = MessageUnique.createUniqueMsgId({
|
||||
chatType: peer.chatType,
|
||||
guildId: '',
|
||||
peerUid: peer.peerUid,
|
||||
}, returnMsg.msgId);
|
||||
|
||||
setTimeout(() => {
|
||||
deleteAfterSentFiles.forEach(async file => {
|
||||
setTimeout(async () => {
|
||||
const deletePromises = deleteAfterSentFiles.map(async file => {
|
||||
try {
|
||||
if (await fsPromise.access(file, constants.W_OK).then(() => true).catch(() => false)) {
|
||||
fsPromise.unlink(file).then().catch(e => this.core.context.logger.logError('发送消息删除文件失败', e));
|
||||
await fsPromise.unlink(file);
|
||||
}
|
||||
} catch (error) {
|
||||
this.core.context.logger.logError('发送消息删除文件失败', (error as Error).message);
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('发送消息删除文件失败', e);
|
||||
}
|
||||
});
|
||||
await Promise.all(deletePromises);
|
||||
}, 60000);
|
||||
|
||||
return returnMsg;
|
||||
@@ -964,6 +972,7 @@ export class OneBotMsgApi {
|
||||
|
||||
return { path, fileName: inputdata.name ?? fileName };
|
||||
}
|
||||
|
||||
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
|
||||
switch (type) {
|
||||
case 130:
|
||||
@@ -977,20 +986,59 @@ export class OneBotMsgApi {
|
||||
}
|
||||
}
|
||||
|
||||
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
|
||||
let groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
|
||||
let isAdminOrOwner = groupRole === 3 || groupRole === 4;
|
||||
|
||||
if (isAdminOrOwner && !operatorUid) {
|
||||
let dataNotify: GroupNotify | undefined;
|
||||
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
|
||||
(doubt, notifies) => {
|
||||
for (const notify of notifies) {
|
||||
if (notify.group.groupCode === groupUin && notify.user1.uid === memberUid) {
|
||||
dataNotify = notify;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, 1, 1000).catch(undefined);
|
||||
if (dataNotify) {
|
||||
return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid;
|
||||
}
|
||||
}
|
||||
|
||||
return operatorUid;
|
||||
}
|
||||
|
||||
async parseSysMessage(msg: number[]) {
|
||||
const SysMessage = new NapProtoMsg(PushMsgBody).decode(Uint8Array.from(msg));
|
||||
// 邀请需要解grayTipElement
|
||||
if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) {
|
||||
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
|
||||
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
|
||||
let operatorUid = await this.waitGroupNotify(
|
||||
groupChange.groupUin.toString(),
|
||||
groupChange.memberUid,
|
||||
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : ''
|
||||
);
|
||||
return new OB11GroupIncreaseEvent(
|
||||
this.core,
|
||||
groupChange.groupUin,
|
||||
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
|
||||
groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
|
||||
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0,
|
||||
groupChange.decreaseType == 131 ? 'invite' : 'approve',
|
||||
);
|
||||
|
||||
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
|
||||
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
|
||||
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
|
||||
let operatorUid = await this.waitGroupNotify(
|
||||
groupChange.groupUin.toString(),
|
||||
groupChange.memberUid,
|
||||
groupChange.decreaseType === 3 && groupChange.operatorInfo ?
|
||||
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
|
||||
groupChange.operatorInfo?.toString()
|
||||
);
|
||||
if (groupChange.memberUid === this.core.selfInfo.uid) {
|
||||
setTimeout(() => {
|
||||
this.core.apis.GroupApi.groupMemberCache.delete(groupChange.groupUin.toString());
|
||||
@@ -1003,7 +1051,7 @@ export class OneBotMsgApi {
|
||||
this.core,
|
||||
groupChange.groupUin,
|
||||
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
|
||||
groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
|
||||
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0,
|
||||
this.groupChangDecreseType2String(groupChange.decreaseType),
|
||||
);
|
||||
} else if (SysMessage.contentHead.type == 44 && SysMessage.body?.msgContent) {
|
||||
@@ -1024,6 +1072,72 @@ export class OneBotMsgApi {
|
||||
+await this.core.apis.UserApi.getUinByUidV2(uid),
|
||||
enabled ? 'set' : 'unset'
|
||||
);
|
||||
} else if (SysMessage.contentHead.type == 87 && SysMessage.body?.msgContent) {
|
||||
let groupInvite = new NapProtoMsg(GroupInvite).decode(SysMessage.body.msgContent);
|
||||
let request_seq = '';
|
||||
try {
|
||||
await this.core.eventWrapper.registerListen('NodeIKernelMsgListener/onRecvMsg', (msgs) => {
|
||||
for (const msg of msgs) {
|
||||
if (msg.senderUid === groupInvite.invitorUid && msg.msgType === 11) {
|
||||
let jumpUrl = JSON.parse(msg.elements.find(e => e.elementType === 10)?.arkElement?.bytesData ?? '').meta?.news?.jumpUrl;
|
||||
let jumpUrlParams = new URLSearchParams(jumpUrl);
|
||||
let groupcode = jumpUrlParams.get('groupcode');
|
||||
let receiveruin = jumpUrlParams.get('receiveruin');
|
||||
let msgseq = jumpUrlParams.get('msgseq');
|
||||
request_seq = msgseq ?? '';
|
||||
if (groupcode === groupInvite.groupUin.toString() && receiveruin === this.core.selfInfo.uin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, 1, 1000);
|
||||
} catch (error) {
|
||||
request_seq = '';
|
||||
}
|
||||
// 未拉取到seq
|
||||
if (request_seq === '') {
|
||||
return;
|
||||
}
|
||||
// 创建个假的
|
||||
this.notifyGroupInvite.put(request_seq, {
|
||||
seq: request_seq,
|
||||
type: 1,
|
||||
group: {
|
||||
groupCode: groupInvite.groupUin.toString(),
|
||||
groupName: '',
|
||||
},
|
||||
user1: {
|
||||
uid: groupInvite.invitorUid,
|
||||
nickName: '',
|
||||
},
|
||||
user2: {
|
||||
uid: this.core.selfInfo.uid,
|
||||
nickName: '',
|
||||
},
|
||||
actionUser: {
|
||||
uid: groupInvite.invitorUid,
|
||||
nickName: '',
|
||||
},
|
||||
actionTime: Date.now().toString(),
|
||||
postscript: '',
|
||||
repeatSeqs: [],
|
||||
warningTips: '',
|
||||
invitationExt: {
|
||||
srcType: 1,
|
||||
groupCode: groupInvite.groupUin.toString(),
|
||||
waitStatus: 1,
|
||||
},
|
||||
status: 1
|
||||
})
|
||||
return new OB11GroupRequestEvent(
|
||||
this.core,
|
||||
+groupInvite.groupUin,
|
||||
+await this.core.apis.UserApi.getUinByUidV2(groupInvite.invitorUid),
|
||||
'invite',
|
||||
'',
|
||||
request_seq
|
||||
);
|
||||
} else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
|
||||
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
|
||||
}
|
||||
|
@@ -18,8 +18,8 @@ import { ContextMode, createContext, normalize } from '@/onebot/action/msg/SendM
|
||||
import { isNull } from '@/common/helper';
|
||||
|
||||
export class OneBotQuickActionApi {
|
||||
private obContext: NapCatOneBot11Adapter;
|
||||
private core: NapCatCore;
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: NapCatCore;
|
||||
constructor(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
@@ -82,11 +82,22 @@ export class OneBotQuickActionApi {
|
||||
this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles, false).then().catch(e => this.core.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
async findNotify(flag: string) {
|
||||
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
|
||||
if (!notify) {
|
||||
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag);
|
||||
}
|
||||
return notify;
|
||||
}
|
||||
|
||||
async handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) {
|
||||
if (!isNull(quickAction.approve)) {
|
||||
|
||||
let invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag);
|
||||
const notify = invite_notify ?? await this.findNotify(request.flag);
|
||||
|
||||
if (!isNull(quickAction.approve) && notify) {
|
||||
this.core.apis.GroupApi.handleGroupRequest(
|
||||
request.flag,
|
||||
notify,
|
||||
quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
|
||||
quickAction.reason,
|
||||
).catch(e => this.core.context.logger.logError(e));
|
||||
@@ -94,8 +105,9 @@ export class OneBotQuickActionApi {
|
||||
}
|
||||
|
||||
async handleFriendRequest(request: OB11FriendRequestEvent, quickAction: QuickActionFriendRequest) {
|
||||
if (!isNull(quickAction.approve)) {
|
||||
this.core.apis.FriendApi.handleFriendRequest(request.flag, !!quickAction.approve).then().catch(e => this.core.context.logger.logError(e));
|
||||
const notify = (await this.core.apis.FriendApi.getBuddyReq()).buddyReqs.find(e => e.reqTime == request.flag.toString());
|
||||
if (!isNull(quickAction.approve) && notify) {
|
||||
this.core.apis.FriendApi.handleFriendRequest(notify, !!quickAction.approve).then().catch(e => this.core.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ interface v1Config {
|
||||
export interface AdapterConfigInner {
|
||||
name: string;
|
||||
enable: boolean;
|
||||
|
||||
}
|
||||
export type AdapterConfigWrap = AdapterConfigInner & Partial<NetworkConfigAdapter>;
|
||||
|
||||
@@ -37,6 +38,14 @@ export interface AdapterConfig extends AdapterConfigInner {
|
||||
|
||||
const createDefaultAdapterConfig = <T extends AdapterConfig>(config: T): T => config;
|
||||
|
||||
export interface PluginConfig extends AdapterConfig {
|
||||
name: string;
|
||||
enable: boolean;
|
||||
messagePostFormat: string;
|
||||
reportSelfMessage: boolean;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
export const httpServerDefaultConfigs = createDefaultAdapterConfig({
|
||||
name: 'http-server',
|
||||
enable: false as boolean,
|
||||
@@ -127,7 +136,7 @@ export const mergeNetworkDefaultConfig = {
|
||||
websocketClients: websocketClientDefaultConfigs,
|
||||
} as const;
|
||||
|
||||
export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig;
|
||||
export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | PluginConfig;
|
||||
type NetworkConfigKeys = keyof typeof mergeNetworkDefaultConfig;
|
||||
|
||||
export function mergeOneBotConfigs(
|
||||
@@ -233,4 +242,4 @@ export function getConfigBoolKey(
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
13
src/onebot/event/notice/OB11GroupNameEvent.ts
Normal file
13
src/onebot/event/notice/OB11GroupNameEvent.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
import { NapCatCore } from '@/core';
|
||||
|
||||
export class OB11GroupNameEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'notify';
|
||||
sub_type = 'group_name';
|
||||
name_new: string;
|
||||
|
||||
constructor(core: NapCatCore, groupId: number, userId: number, nameNew: string) {
|
||||
super(core, groupId, userId);
|
||||
this.name_new = nameNew;
|
||||
}
|
||||
}
|
@@ -10,12 +10,14 @@ class OB11PokeEvent extends OB11BaseNoticeEvent {
|
||||
|
||||
export class OB11FriendPokeEvent extends OB11PokeEvent {
|
||||
raw_info: any;
|
||||
sender_id: number;
|
||||
|
||||
//raw_message nb等框架标准为string
|
||||
constructor(core: NapCatCore, user_id: number, target_id: number, raw_message: any) {
|
||||
constructor(core: NapCatCore, user_id: number, sender_id: number, target_id: number, raw_message: any) {
|
||||
super(core);
|
||||
this.target_id = target_id;
|
||||
this.user_id = user_id;
|
||||
this.sender_id = sender_id;
|
||||
this.raw_info = raw_message;
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,6 @@ import {
|
||||
} from '@/core';
|
||||
import { OB11ConfigLoader } from '@/onebot/config';
|
||||
import {
|
||||
IOB11NetworkAdapter,
|
||||
OB11ActiveHttpAdapter,
|
||||
OB11ActiveWebSocketAdapter,
|
||||
OB11NetworkManager,
|
||||
@@ -31,7 +30,6 @@ import {
|
||||
OneBotMsgApi,
|
||||
OneBotQuickActionApi,
|
||||
OneBotUserApi,
|
||||
StableOneBotApiWrapper,
|
||||
} from '@/onebot/api';
|
||||
import { ActionMap, createActionMap } from '@/onebot/action';
|
||||
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
|
||||
@@ -45,8 +43,16 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal
|
||||
import { LRUCache } from '@/common/lru-cache';
|
||||
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
|
||||
import { BotOfflineEvent } from './event/notice/BotOfflineEvent';
|
||||
import { AdapterConfigWrap, mergeOneBotConfigs, migrateOneBotConfigsV1, NetworkConfigAdapter, OneBotConfig } from './config/config';
|
||||
import {
|
||||
AdapterConfigWrap,
|
||||
mergeOneBotConfigs,
|
||||
migrateOneBotConfigsV1,
|
||||
NetworkConfigAdapter,
|
||||
OneBotConfig,
|
||||
} from './config/config';
|
||||
import { OB11Message } from './types';
|
||||
import { OB11PluginAdapter } from './network/plugin';
|
||||
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
|
||||
|
||||
//OneBot实现类
|
||||
export class NapCatOneBot11Adapter {
|
||||
@@ -54,7 +60,7 @@ export class NapCatOneBot11Adapter {
|
||||
readonly context: InstanceContext;
|
||||
|
||||
configLoader: OB11ConfigLoader;
|
||||
public readonly apis: StableOneBotApiWrapper;
|
||||
public readonly apis;
|
||||
networkManager: OB11NetworkManager;
|
||||
actions: ActionMap;
|
||||
private readonly bootTime = Date.now() / 1000;
|
||||
@@ -71,7 +77,7 @@ export class NapCatOneBot11Adapter {
|
||||
UserApi: new OneBotUserApi(this, core),
|
||||
FriendApi: new OneBotFriendApi(this, core),
|
||||
MsgApi: new OneBotMsgApi(this, core),
|
||||
QuickActionApi: new OneBotQuickActionApi(this, core),
|
||||
QuickActionApi: new OneBotQuickActionApi(this, core)
|
||||
} as const;
|
||||
this.actions = createActionMap(this, core);
|
||||
this.networkManager = new OB11NetworkManager();
|
||||
@@ -106,11 +112,16 @@ export class NapCatOneBot11Adapter {
|
||||
const serviceInfo = await this.creatOneBotLog(ob11Config);
|
||||
this.context.logger.log(`[Notice] [OneBot11] ${serviceInfo}`);
|
||||
|
||||
// //创建NetWork服务
|
||||
//创建NetWork服务
|
||||
|
||||
// 注册Plugin 如果需要基于NapCat进行快速开发
|
||||
// this.networkManager.registerAdapter(
|
||||
// new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
|
||||
// );
|
||||
for (const key of ob11Config.network.httpServers) {
|
||||
if (key.enable) {
|
||||
this.networkManager.registerAdapter(
|
||||
new OB11PassiveHttpAdapter(key.name, key, this.core, this.actions)
|
||||
new OB11PassiveHttpAdapter(key.name, key, this.core, this, this.actions)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +139,7 @@ export class NapCatOneBot11Adapter {
|
||||
key.name,
|
||||
key,
|
||||
this.core,
|
||||
this,
|
||||
this.actions
|
||||
)
|
||||
);
|
||||
@@ -140,6 +152,7 @@ export class NapCatOneBot11Adapter {
|
||||
key.name,
|
||||
key,
|
||||
this.core,
|
||||
this,
|
||||
this.actions
|
||||
)
|
||||
);
|
||||
@@ -151,9 +164,10 @@ export class NapCatOneBot11Adapter {
|
||||
this.initBuddyListener();
|
||||
this.initGroupListener();
|
||||
|
||||
await WebUiDataRuntime.setQQLoginUin(selfInfo.uin.toString());
|
||||
await WebUiDataRuntime.setQQLoginStatus(true);
|
||||
await WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
|
||||
WebUiDataRuntime.setQQVersion(this.core.context.basicInfoWrapper.getFullQQVesion());
|
||||
WebUiDataRuntime.setQQLoginInfo(selfInfo);
|
||||
WebUiDataRuntime.setQQLoginStatus(true);
|
||||
WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
|
||||
const prev = this.configLoader.configData;
|
||||
this.configLoader.save(newConfig);
|
||||
this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
|
||||
@@ -186,10 +200,12 @@ export class NapCatOneBot11Adapter {
|
||||
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11ActiveWebSocketAdapter);
|
||||
}
|
||||
|
||||
private async handleConfigChange(
|
||||
private async handleConfigChange<CT extends NetworkConfigAdapter>(
|
||||
prevConfig: NetworkConfigAdapter[],
|
||||
nowConfig: NetworkConfigAdapter[],
|
||||
adapterClass: new (...args: any[]) => IOB11NetworkAdapter
|
||||
adapterClass: new (
|
||||
...args: ConstructorParameters<typeof IOB11NetworkAdapter<CT>>
|
||||
) => IOB11NetworkAdapter<CT>
|
||||
): Promise<void> {
|
||||
// 比较旧的在新的找不到的回收
|
||||
for (const adapterConfig of prevConfig) {
|
||||
@@ -201,7 +217,7 @@ export class NapCatOneBot11Adapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 通知新配置重载 删除关闭的 加入新开的
|
||||
// 通知新配置重载 删除关闭的 加入新开的
|
||||
for (const adapterConfig of nowConfig) {
|
||||
const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name);
|
||||
if (existingAdapter) {
|
||||
@@ -210,7 +226,7 @@ export class NapCatOneBot11Adapter {
|
||||
await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]);
|
||||
}
|
||||
} else if (adapterConfig.enable) {
|
||||
const newAdapter = new adapterClass(adapterConfig.name, adapterConfig, this.core, this.actions);
|
||||
const newAdapter = new adapterClass(adapterConfig.name, adapterConfig as CT, this.core, this, this.actions);
|
||||
await this.networkManager.registerAdapterAndOpen(newAdapter);
|
||||
}
|
||||
}
|
||||
@@ -295,7 +311,7 @@ export class NapCatOneBot11Adapter {
|
||||
guildId: ''
|
||||
};
|
||||
const msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
|
||||
const element = msg?.elements[0];
|
||||
const element = msg?.elements.find(e => !!e.grayTipElement?.revokeElement);
|
||||
if (msg && element) {
|
||||
const recallEvent = await this.emitRecallMsg(msg, element);
|
||||
try {
|
||||
@@ -333,7 +349,7 @@ export class NapCatOneBot11Adapter {
|
||||
this.core,
|
||||
+requesterUin,
|
||||
req.extWords,
|
||||
req.friendUid + '|' + req.reqTime
|
||||
req.reqTime
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -365,8 +381,7 @@ export class NapCatOneBot11Adapter {
|
||||
if (notifyTime < this.bootTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type;
|
||||
const flag = notify.seq;
|
||||
this.context.logger.logDebug('收到群通知', notify);
|
||||
if (
|
||||
[GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) &&
|
||||
@@ -444,7 +459,7 @@ export class NapCatOneBot11Adapter {
|
||||
}
|
||||
|
||||
private async emitMsg(message: RawMessage) {
|
||||
const network = Object.values(this.configLoader.configData.network).flat() as Array<AdapterConfigWrap>;
|
||||
const network = await this.networkManager.getAllConfig();
|
||||
this.context.logger.logDebug('收到新消息 RawMessage', message);
|
||||
await Promise.allSettled([
|
||||
this.handleMsg(message, network),
|
||||
@@ -453,6 +468,10 @@ export class NapCatOneBot11Adapter {
|
||||
}
|
||||
|
||||
private async handleMsg(message: RawMessage, network: Array<AdapterConfigWrap>) {
|
||||
// 过滤无效消息
|
||||
if (message.msgType === NTMsgType.KMSGTYPENULL) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const ob11Msg = await this.apis.MsgApi.parseMessageV2(message, this.configLoader.configData.parseMultMsg);
|
||||
if (ob11Msg) {
|
||||
@@ -484,7 +503,7 @@ export class NapCatOneBot11Adapter {
|
||||
ob11Msg.stringMsg.target_id = parseInt(message.peerUin);
|
||||
ob11Msg.arrayMsg.target_id = parseInt(message.peerUin);
|
||||
}
|
||||
if (e.messagePostFormat == 'string') {
|
||||
if ('messagePostFormat' in e && e.messagePostFormat == 'string') {
|
||||
msgMap.set(e.name, structuredClone(ob11Msg.stringMsg));
|
||||
} else {
|
||||
msgMap.set(e.name, structuredClone(ob11Msg.arrayMsg));
|
||||
@@ -522,21 +541,27 @@ export class NapCatOneBot11Adapter {
|
||||
// 群名片修改事件解析 任何都该判断
|
||||
if (message.senderUin && message.senderUin !== '0') {
|
||||
const cardChangedEvent = await this.apis.GroupApi.parseCardChangedEvent(message);
|
||||
cardChangedEvent && await this.networkManager.emitEvent(cardChangedEvent);
|
||||
if (cardChangedEvent) {
|
||||
await this.networkManager.emitEvent(cardChangedEvent);
|
||||
}
|
||||
}
|
||||
if (message.msgType === NTMsgType.KMSGTYPEFILE) {
|
||||
// 文件为单元素消息
|
||||
const elementWrapper = message.elements.find(e => !!e.fileElement);
|
||||
if (elementWrapper?.fileElement) {
|
||||
const uploadGroupFileEvent = await this.apis.GroupApi.parseGroupUploadFileEvene(message, elementWrapper.fileElement, elementWrapper);
|
||||
uploadGroupFileEvent && await this.networkManager.emitEvent(uploadGroupFileEvent);
|
||||
if (uploadGroupFileEvent) {
|
||||
await this.networkManager.emitEvent(uploadGroupFileEvent);
|
||||
}
|
||||
}
|
||||
} else if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) {
|
||||
// 灰条为单元素消息
|
||||
const grayTipElement = message.elements[0].grayTipElement;
|
||||
if (grayTipElement) {
|
||||
const event = await this.apis.GroupApi.parseGrayTipElement(message, grayTipElement);
|
||||
event && await this.networkManager.emitEvent(event);
|
||||
if (event) {
|
||||
await this.networkManager.emitEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -551,7 +576,10 @@ export class NapCatOneBot11Adapter {
|
||||
const grayTipElement = message.elements[0].grayTipElement;
|
||||
if (grayTipElement) {
|
||||
const event = await this.apis.MsgApi.parsePrivateMsgEvent(message, grayTipElement);
|
||||
event && await this.networkManager.emitEvent(event);
|
||||
if (event) {
|
||||
await this.networkManager.emitEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@@ -1,62 +1,42 @@
|
||||
import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
|
||||
import { createHmac } from 'crypto';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { QuickAction, QuickActionEvent } from '@/onebot/types';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { NapCatOneBot11Adapter } from '..';
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import { HttpClientConfig } from '@/onebot/config/config';
|
||||
import { ActionMap } from '@/onebot/action';
|
||||
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
|
||||
|
||||
export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
|
||||
logger: LogWrapper;
|
||||
isEnable: boolean = false;
|
||||
public config: HttpClientConfig;
|
||||
export class OB11ActiveHttpAdapter extends IOB11NetworkAdapter<HttpClientConfig> {
|
||||
constructor(
|
||||
public name: string,
|
||||
config: HttpClientConfig,
|
||||
public core: NapCatCore,
|
||||
public obContext: NapCatOneBot11Adapter,
|
||||
public actions: ActionMap,
|
||||
name: string, config: HttpClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
|
||||
) {
|
||||
this.logger = core.context.logger;
|
||||
this.config = structuredClone(config);
|
||||
super(name, config, core, obContext, actions);
|
||||
}
|
||||
|
||||
onEvent<T extends OB11EmitEventContent>(event: T) {
|
||||
if (!this.isEnable) {
|
||||
return;
|
||||
}
|
||||
this.emitEventAsync(event).catch(e => this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报返回快速操作失败', e));
|
||||
}
|
||||
|
||||
async emitEventAsync<T extends OB11EmitEventContent>(event: T) {
|
||||
if (!this.isEnable) return;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-self-id': this.core.selfInfo.uin,
|
||||
};
|
||||
|
||||
const msgStr = JSON.stringify(event);
|
||||
if (this.config.token && this.config.token.length > 0) {
|
||||
if (this.config.token) {
|
||||
const hmac = createHmac('sha1', this.config.token);
|
||||
hmac.update(msgStr);
|
||||
const sig = hmac.digest('hex');
|
||||
headers['x-signature'] = 'sha1=' + sig;
|
||||
headers['x-signature'] = 'sha1=' + hmac.digest('hex');
|
||||
}
|
||||
RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers).then(async (res) => {
|
||||
let resJson: QuickAction;
|
||||
try {
|
||||
resJson = JSON.parse(res);
|
||||
//logDebug('新消息事件HTTP上报返回快速操作: ', JSON.stringify(resJson));
|
||||
} catch (e) {
|
||||
this.logger.logDebug('[OneBot] [Http Client] 新消息事件HTTP上报没有返回快速操作,不需要处理');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.obContext.apis.QuickActionApi
|
||||
.handleQuickOperation(event as QuickActionEvent, resJson)
|
||||
.catch(e => this.logger.logError(e));
|
||||
} catch (e: any) {
|
||||
this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报返回快速操作失败', e);
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报失败', e);
|
||||
});
|
||||
|
||||
const data = await RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers);
|
||||
const resJson: QuickAction = data ? JSON.parse(data) : {};
|
||||
|
||||
await this.obContext.apis.QuickActionApi.handleQuickOperation(event as QuickActionEvent, resJson);
|
||||
}
|
||||
|
||||
open() {
|
||||
@@ -66,20 +46,24 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
|
||||
close() {
|
||||
this.isEnable = false;
|
||||
}
|
||||
async reload(newconfig: HttpClientConfig) {
|
||||
|
||||
async reload(newConfig: HttpClientConfig) {
|
||||
const wasEnabled = this.isEnable;
|
||||
const oldUrl = this.config.url;
|
||||
this.config = newconfig;
|
||||
if (newconfig.enable && !wasEnabled) {
|
||||
this.config = newConfig;
|
||||
|
||||
if (newConfig.enable && !wasEnabled) {
|
||||
this.open();
|
||||
return OB11NetworkReloadType.NetWorkOpen;
|
||||
} else if (!newconfig.enable && wasEnabled) {
|
||||
} else if (!newConfig.enable && wasEnabled) {
|
||||
this.close();
|
||||
return OB11NetworkReloadType.NetWorkClose;
|
||||
}
|
||||
if (oldUrl !== newconfig.url) {
|
||||
|
||||
if (oldUrl !== newConfig.url) {
|
||||
return OB11NetworkReloadType.NetWorkReload;
|
||||
}
|
||||
|
||||
return OB11NetworkReloadType.Normal;
|
||||
}
|
||||
}
|
||||
|
@@ -1,29 +1,21 @@
|
||||
import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
|
||||
import { WebSocket } from 'ws';
|
||||
import { OB11HeartbeatEvent } from '@/onebot/event/meta/OB11HeartbeatEvent';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { OB11Response } from '@/onebot/action/OneBotAction';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { ActionMap } from '@/onebot/action';
|
||||
import { LifeCycleSubType, OB11LifeCycleEvent } from '@/onebot/event/meta/OB11LifeCycleEvent';
|
||||
import { WebsocketClientConfig } from '@/onebot/config/config';
|
||||
import { NapCatOneBot11Adapter } from "@/onebot";
|
||||
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
|
||||
|
||||
export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
|
||||
isEnable: boolean = false;
|
||||
logger: LogWrapper;
|
||||
export class OB11ActiveWebSocketAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> {
|
||||
private connection: WebSocket | null = null;
|
||||
private heartbeatRef: NodeJS.Timeout | null = null;
|
||||
public config: WebsocketClientConfig;
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
confg: WebsocketClientConfig,
|
||||
public core: NapCatCore,
|
||||
public actions: ActionMap,
|
||||
) {
|
||||
this.logger = core.context.logger;
|
||||
this.config = structuredClone(confg);
|
||||
constructor(name: string, config: WebsocketClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
|
||||
super(name, config, core, obContext, actions);
|
||||
}
|
||||
|
||||
onEvent<T extends OB11EmitEventContent>(event: T) {
|
||||
@@ -133,7 +125,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
|
||||
}
|
||||
|
||||
private async handleMessage(message: any) {
|
||||
let receiveData: { action: ActionName, params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
|
||||
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
|
||||
let echo = undefined;
|
||||
|
||||
try {
|
||||
@@ -145,7 +137,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
|
||||
return;
|
||||
}
|
||||
receiveData.params = (receiveData?.params) ? receiveData.params : {};// 兼容类型验证
|
||||
const action = this.actions.get(receiveData.action);
|
||||
const action = this.actions.get(receiveData.action as any);
|
||||
if (!action) {
|
||||
this.logger.logError('[OneBot] [WebSocket Client] 发生错误', '不支持的Api ' + receiveData.action);
|
||||
this.checkStateAndReply<any>(OB11Response.error('不支持的Api ' + receiveData.action, 1404, echo));
|
||||
|
33
src/onebot/network/adapter.ts
Normal file
33
src/onebot/network/adapter.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NetworkConfigAdapter } from "@/onebot/config/config";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { NapCatOneBot11Adapter } from "@/onebot";
|
||||
import { ActionMap } from "@/onebot/action";
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from "@/onebot/network/index";
|
||||
|
||||
export abstract class IOB11NetworkAdapter<CT extends NetworkConfigAdapter> {
|
||||
name: string;
|
||||
isEnable: boolean = false;
|
||||
config: CT;
|
||||
readonly logger: LogWrapper;
|
||||
readonly core: NapCatCore;
|
||||
readonly obContext: NapCatOneBot11Adapter;
|
||||
readonly actions: ActionMap;
|
||||
|
||||
constructor(name: string, config: CT, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
|
||||
this.name = name;
|
||||
this.config = structuredClone(config);
|
||||
this.core = core;
|
||||
this.obContext = obContext;
|
||||
this.actions = actions;
|
||||
this.logger = core.context.logger;
|
||||
}
|
||||
|
||||
abstract onEvent<T extends OB11EmitEventContent>(event: T): void;
|
||||
|
||||
abstract open(): void | Promise<void>;
|
||||
|
||||
abstract close(): void | Promise<void>;
|
||||
|
||||
abstract reload(config: any): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import { OneBotEvent } from '@/onebot/event/OneBotEvent';
|
||||
import { OB11Message } from '@/onebot';
|
||||
import { ActionMap } from '@/onebot/action';
|
||||
import { NetworkConfigAdapter } from '@/onebot/config/config';
|
||||
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
|
||||
|
||||
export type OB11EmitEventContent = OneBotEvent | OB11Message;
|
||||
export enum OB11NetworkReloadType {
|
||||
@@ -11,30 +11,20 @@ export enum OB11NetworkReloadType {
|
||||
NetWorkClose = 3,
|
||||
NetWorkOpen = 4
|
||||
}
|
||||
export interface IOB11NetworkAdapter {
|
||||
actions: ActionMap;
|
||||
name: string;
|
||||
isEnable: boolean;
|
||||
config: NetworkConfigAdapter;
|
||||
|
||||
onEvent<T extends OB11EmitEventContent>(event: T): void;
|
||||
|
||||
open(): void | Promise<void>;
|
||||
|
||||
close(): void | Promise<void>;
|
||||
|
||||
reload(config: any): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
|
||||
}
|
||||
|
||||
export class OB11NetworkManager {
|
||||
adapters: Map<string, IOB11NetworkAdapter> = new Map();
|
||||
adapters: Map<string, IOB11NetworkAdapter<NetworkConfigAdapter>> = new Map();
|
||||
|
||||
async openAllAdapters() {
|
||||
return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.open()));
|
||||
}
|
||||
|
||||
async emitEvent(event: OB11EmitEventContent) {
|
||||
return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.onEvent(event)));
|
||||
return Promise.all(Array.from(this.adapters.values()).map(adapter => {
|
||||
if (adapter.isEnable) {
|
||||
return adapter.onEvent(event);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async emitEvents(events: OB11EmitEventContent[]) {
|
||||
@@ -44,35 +34,37 @@ export class OB11NetworkManager {
|
||||
async emitEventByName(names: string[], event: OB11EmitEventContent) {
|
||||
return Promise.all(names.map(name => {
|
||||
const adapter = this.adapters.get(name);
|
||||
if (adapter) {
|
||||
if (adapter && adapter.isEnable) {
|
||||
return adapter.onEvent(event);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async emitEventByNames(map: Map<string, OB11EmitEventContent>) {
|
||||
return Promise.all(Array.from(map.entries()).map(([name, event]) => {
|
||||
const adapter = this.adapters.get(name);
|
||||
if (adapter) {
|
||||
if (adapter && adapter.isEnable) {
|
||||
return adapter.onEvent(event);
|
||||
}
|
||||
}));
|
||||
}
|
||||
registerAdapter(adapter: IOB11NetworkAdapter) {
|
||||
|
||||
registerAdapter<CT extends NetworkConfigAdapter>(adapter: IOB11NetworkAdapter<CT>) {
|
||||
this.adapters.set(adapter.name, adapter);
|
||||
}
|
||||
|
||||
async registerAdapterAndOpen(adapter: IOB11NetworkAdapter) {
|
||||
async registerAdapterAndOpen<CT extends NetworkConfigAdapter>(adapter: IOB11NetworkAdapter<CT>) {
|
||||
this.registerAdapter(adapter);
|
||||
await adapter.open();
|
||||
}
|
||||
|
||||
async closeSomeAdapters(adaptersToClose: IOB11NetworkAdapter[]) {
|
||||
async closeSomeAdapters<CT extends NetworkConfigAdapter>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
|
||||
for (const adapter of adaptersToClose) {
|
||||
this.adapters.delete(adapter.name);
|
||||
await adapter.close();
|
||||
}
|
||||
}
|
||||
async closeSomeAdaterWhenOpen(adaptersToClose: IOB11NetworkAdapter[]) {
|
||||
async closeSomeAdaterWhenOpen<CT extends NetworkConfigAdapter>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
|
||||
for (const adapter of adaptersToClose) {
|
||||
this.adapters.delete(adapter.name);
|
||||
if (adapter.isEnable) {
|
||||
@@ -85,7 +77,7 @@ export class OB11NetworkManager {
|
||||
return this.adapters.get(name);
|
||||
}
|
||||
|
||||
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter) => boolean) {
|
||||
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter<NetworkConfigAdapter>) => boolean) {
|
||||
const adaptersToClose = Array.from(this.adapters.values()).filter(closeFilter);
|
||||
await this.closeSomeAdapters(adaptersToClose);
|
||||
}
|
||||
@@ -104,9 +96,12 @@ export class OB11NetworkManager {
|
||||
async readloadSomeAdapters<T>(configMap: Map<string, T>) {
|
||||
await Promise.all(Array.from(configMap.entries()).map(([name, config]) => this.readloadAdapter(name, config)));
|
||||
}
|
||||
async getAllConfig() {
|
||||
return Array.from(this.adapters.values()).map(adapter => adapter.config);
|
||||
}
|
||||
}
|
||||
|
||||
export * from './active-http';
|
||||
export * from './active-websocket';
|
||||
export * from './passive-http';
|
||||
export * from './passive-websocket';
|
||||
export * from './passive-websocket';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { IOB11NetworkAdapter, OB11NetworkReloadType } from './index';
|
||||
import { OB11NetworkReloadType } from './index';
|
||||
import express, { Express, Request, Response } from 'express';
|
||||
import http from 'http';
|
||||
import { NapCatCore } from '@/core';
|
||||
@@ -6,20 +6,15 @@ import { OB11Response } from '@/onebot/action/OneBotAction';
|
||||
import { ActionMap } from '@/onebot/action';
|
||||
import cors from 'cors';
|
||||
import { HttpServerConfig } from '@/onebot/config/config';
|
||||
import { NapCatOneBot11Adapter } from "@/onebot";
|
||||
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
|
||||
|
||||
export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
|
||||
export class OB11PassiveHttpAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
|
||||
private app: Express | undefined;
|
||||
private server: http.Server | undefined;
|
||||
isEnable: boolean = false;
|
||||
public config: HttpServerConfig;
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
config: HttpServerConfig,
|
||||
public core: NapCatCore,
|
||||
public actions: ActionMap,
|
||||
) {
|
||||
this.config = structuredClone(config);
|
||||
constructor(name: string, config: HttpServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
|
||||
super(name, config, core, obContext, actions);
|
||||
}
|
||||
|
||||
onEvent() {
|
||||
@@ -105,7 +100,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
|
||||
return res.json(hello);
|
||||
}
|
||||
const actionName = req.path.split('/')[1];
|
||||
const action = this.actions.get(actionName);
|
||||
const action = this.actions.get(actionName as any);
|
||||
if (action) {
|
||||
try {
|
||||
const result = await action.handle(payload, this.name);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user