mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
229 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bff3b85337 | ||
![]() |
811d9a7237 | ||
![]() |
a764cb8dc2 | ||
![]() |
9204b9b286 | ||
![]() |
da94faa9bb | ||
![]() |
4b53e9a895 | ||
![]() |
f5db96187b | ||
![]() |
857b191b03 | ||
![]() |
09014d1ab5 | ||
![]() |
7557b71869 | ||
![]() |
d07187bd5d | ||
![]() |
2c6a6ba440 | ||
![]() |
4592bf7817 | ||
![]() |
afd6d450a0 | ||
![]() |
b134849dcf | ||
![]() |
e7d0f6d6da | ||
![]() |
16a29b0127 | ||
![]() |
1f5596ef16 | ||
![]() |
bef05432d0 | ||
![]() |
67533d7743 | ||
![]() |
0cc86c6348 | ||
![]() |
607dd68620 | ||
![]() |
7c8cbc0799 | ||
![]() |
ec0c2e8c33 | ||
![]() |
7f3dbe0552 | ||
![]() |
0e9044e0c8 | ||
![]() |
3171640193 | ||
![]() |
a56cee3485 | ||
![]() |
c8ee371982 | ||
![]() |
5778daeb60 | ||
![]() |
f51f3b9861 | ||
![]() |
44dd1a0b02 | ||
![]() |
61a00ffcbf | ||
![]() |
4b0a0f0a32 | ||
![]() |
a3088fb8bc | ||
![]() |
88fd1f9eb1 | ||
![]() |
15156bac1e | ||
![]() |
a898d2e7be | ||
![]() |
95b003802c | ||
![]() |
95c9eae4ed | ||
![]() |
e3814403e4 | ||
![]() |
3d16d52dd8 | ||
![]() |
1ae47fffb4 | ||
![]() |
4e7096b9e2 | ||
![]() |
8cc9b7f6a7 | ||
![]() |
fb45c1020e | ||
![]() |
e9db4ae8f4 | ||
![]() |
c46ec32bd6 | ||
![]() |
c58a26ed99 | ||
![]() |
a66f5e4971 | ||
![]() |
574c8c6089 | ||
![]() |
67afd95910 | ||
![]() |
f7d0cb0be7 | ||
![]() |
be9b68a0b1 | ||
![]() |
4637414af2 | ||
![]() |
4bd92a72bd | ||
![]() |
a3be26f3e4 | ||
![]() |
675c906cbf | ||
![]() |
6be6023236 | ||
![]() |
42cee0d018 | ||
![]() |
041f725748 | ||
![]() |
0594d61631 | ||
![]() |
15cae6b765 | ||
![]() |
b984116c35 | ||
![]() |
13bda6e3f4 | ||
![]() |
c0d18549d1 | ||
![]() |
3caff72fce | ||
![]() |
1313e9c3f4 | ||
![]() |
0848d5a39e | ||
![]() |
7660646059 | ||
![]() |
bcd90fc744 | ||
![]() |
638fc22d62 | ||
![]() |
c87d365b88 | ||
![]() |
aee9602f25 | ||
![]() |
976fbd0220 | ||
![]() |
afd955d06f | ||
![]() |
4d548da66b | ||
![]() |
41b70f53d1 | ||
![]() |
a47a618bcd | ||
![]() |
62170a30af | ||
![]() |
780c5ac23c | ||
![]() |
9fba519a5a | ||
![]() |
3cd0e7d26b | ||
![]() |
a8fd6af994 | ||
![]() |
4000b89644 | ||
![]() |
9c00bbc0b7 | ||
![]() |
a2989d3b38 | ||
![]() |
fc731b60d5 | ||
![]() |
193980dd4a | ||
![]() |
35427b0768 | ||
![]() |
73ea130e40 | ||
![]() |
5667e6aaee | ||
![]() |
fbd626131d | ||
![]() |
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 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,14 +1,12 @@
|
||||
# Develop
|
||||
node_modules/
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
out/
|
||||
dist/
|
||||
/src/core.lib/common/
|
||||
/localdebug/
|
||||
|
||||
# Editor
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/*
|
||||
|
||||
|
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
".env.universal": ".env.*",
|
||||
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts",
|
||||
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
|
||||
}
|
||||
}
|
15
README.md
15
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -30,16 +30,23 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||
|
||||
[Server.Other](https://napcat.cyou/)
|
||||
[Server.Other](https://docs.napcat.cyou/)
|
||||
|
||||
[NapCat.Wiki](https://www.napcat.wiki)
|
||||
|
||||
## 回家旅途
|
||||
[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/HaRcfrHpUk)
|
||||
|
||||
[Telegram](https://t.me/MelodicMoonlight)
|
||||
|
||||
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
|
||||
|
||||
## 性能设计/协议标准
|
||||
NapCat 已实现90%+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
|
||||
|
||||
由此设计带来一系列好处,在开发中,获取群员列表通常小于50Ms,单条文本消息发送在320Ms以内,在1k+的群聊流畅运行,同时带来一些副作用,上报数据中大量使用Magic生成字段,消息Id无法持久,无法上报撤回消息原始内容。
|
||||
由此设计带来一系列好处,在开发中,获取群员列表通常小于50Ms,单条文本消息发送在320Ms以内,在1k+的群聊流畅运行,同时带来一些副作用,消息Id无法持久,无法上报撤回消息原始内容。
|
||||
|
||||
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
|
||||
|
||||
|
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,16 +4,12 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.2.27",
|
||||
"version": "4.4.0",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
"name": "MliKiowa",
|
||||
"link": "https://github.com/MliKiowa"
|
||||
},
|
||||
{
|
||||
"name": "Young",
|
||||
"link": "https://github.com/Wesley-Young"
|
||||
"name": "NapNeko",
|
||||
"link": "https://github.com/NapNeko"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
|
@@ -12,6 +12,7 @@
|
||||
"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",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
export class githubApiManager {
|
||||
public async GetBaseData(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse= await fetch('https://api.github.com/repos/NapNeko/NapCatQQ', {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@@ -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) {
|
||||
|
@@ -4,31 +4,17 @@
|
||||
<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>
|
@@ -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 = {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<t-head-menu theme="light" class="bottom-menu">
|
||||
<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">
|
||||
@@ -7,15 +7,13 @@
|
||||
<t-icon :name="item.icon" />
|
||||
</template>
|
||||
|
||||
<!-- {{item.label}}-->
|
||||
<!-- {{item.label}}-->
|
||||
</t-menu-item>
|
||||
</t-tooltip>
|
||||
</router-link>
|
||||
</t-head-menu>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
type MenuItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
|
@@ -46,7 +46,7 @@ import {
|
||||
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);
|
||||
|
@@ -89,7 +89,11 @@
|
||||
<t-tag class="tag-item pgk-color"> WebUi: {{ pkg.version }} </t-tag>
|
||||
<t-tag class="tag-item nc-color">
|
||||
NapCat:
|
||||
{{ githubReleasesData&&githubReleasesData[0] ?.tag_name ? githubReleasesData[0].tag_name : napCatVersion }}
|
||||
{{ 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>
|
||||
|
@@ -11,85 +11,66 @@
|
||||
<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">
|
||||
<t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
|
||||
<t-tab-panel value="all" label="全部"></t-tab-panel>
|
||||
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
|
||||
<t-tab-panel value="httpSseServers" label="HTTP SSE 服务器"></t-tab-panel>
|
||||
<t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
|
||||
<t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
|
||||
<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>
|
||||
<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 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)'),
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
<server-filled-icon class="local-icon" size="20px"
|
||||
@click="toggleProperty(item, 'enable')"></server-filled-icon>
|
||||
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
||||
<copy-icon
|
||||
class="copy-icon"
|
||||
size="20px"
|
||||
@click="copyText(item.host + ':' + item.port)"
|
||||
></copy-icon>
|
||||
<copy-icon class="copy-icon" size="20px"
|
||||
@click="copyText(item.host + ':' + item.port)"></copy-icon>
|
||||
</div>
|
||||
<div class="local-box" v-if="item.url">
|
||||
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
|
||||
<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-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">
|
||||
<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">
|
||||
@@ -106,60 +87,36 @@
|
||||
</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="item.debug ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'debug')"
|
||||
>
|
||||
{{ 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="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'enableWebsocket')"
|
||||
>
|
||||
{{ 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="item.enableCors ? 'tag-item-on' : 'tag-item-off'" @click="toggleProperty(item, 'enableCors')">
|
||||
{{ 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="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'reportSelfMessage')"
|
||||
>
|
||||
{{ 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"
|
||||
<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
|
||||
>
|
||||
@click="toggleProperty(item, 'enableForcePushEvent')">
|
||||
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</t-collapse-panel>
|
||||
@@ -173,42 +130,27 @@
|
||||
<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"
|
||||
:on-confirm="saveConfig"
|
||||
class=".t-dialog__ctx .t-dialog__position"
|
||||
>
|
||||
<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="httpSseServers">HTTP SSE 服务器</t-option>
|
||||
<t-option value="httpClients">HTTP 客户端</t-option>
|
||||
<t-option value="websocketServers">WebSocket 服务器</t-option>
|
||||
<t-option value="websocketClients">WebSocket 客户端</t-option>
|
||||
</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>
|
||||
@@ -226,12 +168,10 @@ import {
|
||||
BrowseIcon,
|
||||
Wifi1Icon,
|
||||
} from 'tdesign-icons-vue-next';
|
||||
import { onMounted, onUnmounted, ref, resolveDynamicComponent, watch } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
import {
|
||||
mergeNetworkDefaultConfig,
|
||||
mergeOneBotConfigs,
|
||||
NetworkConfig,
|
||||
loadConfig as loadConfigOnebot,
|
||||
NetworkAdapterConfig,
|
||||
NetworkConfigKey,
|
||||
OneBotConfig,
|
||||
} from '../../../src/onebot/config/config';
|
||||
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
|
||||
@@ -240,6 +180,9 @@ import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.v
|
||||
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
import { onMounted, onUnmounted, ref, watch, resolveDynamicComponent } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
import HttpSseServerComponent from './network/HttpSseServerComponent.vue';
|
||||
|
||||
const showToken = ref<boolean>(false);
|
||||
const infoOneCol = ref<boolean>(true);
|
||||
@@ -256,16 +199,17 @@ const visibleBody = ref<boolean>(false);
|
||||
const newTab = ref<{ name: string; data: any; type: string }>({ name: '', data: {}, type: '' });
|
||||
const dialogTitle = ref<string>('');
|
||||
|
||||
type ComponentKey = keyof typeof mergeNetworkDefaultConfig;
|
||||
|
||||
type ComponentKey = Exclude<NetworkConfigKey, 'plugins'>
|
||||
const componentMap: Record<
|
||||
ComponentKey,
|
||||
| typeof HttpServerComponent
|
||||
| typeof HttpClientComponent
|
||||
| typeof WebsocketServerComponent
|
||||
| typeof WebsocketClientComponent
|
||||
| typeof HttpSseServerComponent
|
||||
> = {
|
||||
httpServers: HttpServerComponent,
|
||||
httpSseServers: HttpSseServerComponent,
|
||||
httpClients: HttpClientComponent,
|
||||
websocketServers: WebsocketServerComponent,
|
||||
websocketClients: WebsocketClientComponent,
|
||||
@@ -276,9 +220,10 @@ const operateType = ref<string>('');
|
||||
//配置项索引
|
||||
const configIndex = ref<number>(0);
|
||||
//保存时所用数据
|
||||
const networkConfig: NetworkConfig & { [key: string]: any } = {
|
||||
const networkConfig: { [key: string]: any } = {
|
||||
websocketClients: [],
|
||||
websocketServers: [],
|
||||
httpSseServers: [],
|
||||
httpClients: [],
|
||||
httpServers: [],
|
||||
};
|
||||
@@ -289,6 +234,7 @@ const WebConfg = ref(
|
||||
['all', []],
|
||||
['httpServers', []],
|
||||
['httpClients', []],
|
||||
['httpSseServers', []],
|
||||
['websocketServers', []],
|
||||
['websocketClients', []],
|
||||
])
|
||||
@@ -296,6 +242,7 @@ const WebConfg = ref(
|
||||
const typeCh: Record<ComponentKey, string> = {
|
||||
httpServers: 'HTTP 服务器',
|
||||
httpClients: 'HTTP 客户端',
|
||||
httpSseServers: 'HTTP SSE 服务器',
|
||||
websocketServers: 'WebSocket 服务器',
|
||||
websocketClients: 'WebSocket 客户端',
|
||||
};
|
||||
@@ -315,15 +262,12 @@ const addConfig = () => {
|
||||
};
|
||||
|
||||
const editConfig = (item: any) => {
|
||||
dialogTitle.value = '修改配置';
|
||||
const type = getKeyByValue(typeCh, item.type);
|
||||
if (type) {
|
||||
newTab.value = { name: item.name, data: item, type: type };
|
||||
}
|
||||
dialogTitle.value = '编辑配置';
|
||||
newTab.value = { name: item.name, data: { ...item }, type: getKeyByValue(typeCh, item.type) || '' };
|
||||
operateType.value = 'edit';
|
||||
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 };
|
||||
@@ -349,11 +293,11 @@ const delConfig = (item: any) => {
|
||||
};
|
||||
|
||||
const selectType = (key: ComponentKey) => {
|
||||
cardConfig.value = WebConfg.value.get(key);
|
||||
cardConfig.value = WebConfg.value.get(key) || [];
|
||||
};
|
||||
|
||||
const onloadDefault = (key: ComponentKey) => {
|
||||
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
|
||||
newTab.value.data = {};
|
||||
};
|
||||
//检测重名
|
||||
const checkName = (name: string) => {
|
||||
@@ -383,7 +327,7 @@ const saveConfig = async () => {
|
||||
}
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
userConfig.network = networkConfig;
|
||||
userConfig.network = networkConfig as any;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
operateType.value = '';
|
||||
@@ -416,12 +360,12 @@ const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
||||
};
|
||||
|
||||
//获取卡片数据
|
||||
const getAllData = (data: NetworkConfig) => {
|
||||
const getAllData = (data: { [key: string]: Array<NetworkAdapterConfig> }) => {
|
||||
cardConfig.value = [];
|
||||
WebConfg.value.set('all', []);
|
||||
for (const key in data) {
|
||||
const configs = data[key as keyof NetworkConfig];
|
||||
if (key in mergeNetworkDefaultConfig) {
|
||||
const configs = data[key as keyof NetworkAdapterConfig];
|
||||
if (key in networkConfig) {
|
||||
networkConfig[key] = [...configs];
|
||||
const newConfigsArray = configs.map((config: any) => ({
|
||||
...config,
|
||||
@@ -442,13 +386,12 @@ const loadConfig = async () => {
|
||||
try {
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
const mergedConfig = mergeOneBotConfigs(userConfig);
|
||||
const mergedConfig = loadConfigOnebot(userConfig);
|
||||
getAllData(mergedConfig.network);
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const copyText = async (text: string) => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
@@ -474,9 +417,9 @@ 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 === 'string') {
|
||||
@@ -486,30 +429,30 @@ emitter.on('sendWidth', (width) => {
|
||||
});
|
||||
watch(menuWidth, (newValue, oldValue) => {
|
||||
loadPage.value = false;
|
||||
setTimeout(()=>{
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
},300)
|
||||
}, 300)
|
||||
});
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
const cachedWidth = localStorage.getItem('menuWidth');
|
||||
if (cachedWidth) {
|
||||
menuWidth.value = parseInt(cachedWidth);
|
||||
setTimeout(()=>{
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
},300)
|
||||
}, 300)
|
||||
}
|
||||
window.addEventListener('resize', ()=>{
|
||||
setTimeout(()=>{
|
||||
window.addEventListener('resize', () => {
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
},300)
|
||||
}, 300)
|
||||
});
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', ()=>{
|
||||
setTimeout(()=>{
|
||||
window.removeEventListener('resize', () => {
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
},300)
|
||||
}, 300)
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -550,9 +493,11 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.local-icon {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.local {
|
||||
flex: 6;
|
||||
margin: 0 10px 0 10px;
|
||||
@@ -579,22 +524,26 @@ onUnmounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-item-on{
|
||||
.tag-item-on {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%) !important;
|
||||
}
|
||||
.tag-item-off{
|
||||
|
||||
.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__position) {
|
||||
padding: 48px 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.setting-box {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -644,7 +593,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;
|
||||
}
|
||||
|
||||
|
@@ -24,26 +24,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: HttpClientConfig = {
|
||||
name: 'http-client',
|
||||
enable: false,
|
||||
url: 'http://localhost:8080',
|
||||
messagePostFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
token: '',
|
||||
debug: false
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpClientConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped></style>
|
@@ -30,26 +30,40 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: HttpServerConfig = {
|
||||
name: 'http-server',
|
||||
enable: false,
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
enableCors: true,
|
||||
enableWebsocket: true,
|
||||
messagePostFormat: 'array',
|
||||
token: '',
|
||||
debug: false
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpServerConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped></style>
|
73
napcat.webui/src/pages/network/HttpSseServerComponent.vue
Normal file
73
napcat.webui/src/pages/network/HttpSseServerComponent.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="端口">
|
||||
<t-input v-model.number="config.port" type="number" />
|
||||
</t-form-item>
|
||||
<t-form-item label="主机">
|
||||
<t-input v-model="config.host" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 CORS">
|
||||
<t-switch v-model="config.enableCors" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 WS">
|
||||
<t-switch v-model="config.enableWebsocket" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpSseServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: HttpSseServerConfig = {
|
||||
name: 'http-sse-server',
|
||||
enable: false,
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
enableCors: true,
|
||||
enableWebsocket: true,
|
||||
messagePostFormat: 'array',
|
||||
token: '',
|
||||
debug: false,
|
||||
reportSelfMessage: false
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpSseServerConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
config.value.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -27,26 +27,40 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: WebsocketClientConfig = {
|
||||
name: 'websocket-client',
|
||||
enable: false,
|
||||
url: 'ws://localhost:8082',
|
||||
messagePostFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
reconnectInterval: 5000,
|
||||
token: '',
|
||||
debug: false,
|
||||
heartInterval: 30000
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: WebsocketClientConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped></style>
|
@@ -33,26 +33,41 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: WebsocketServerConfig = {
|
||||
name: 'websocket-server',
|
||||
enable: false,
|
||||
host: '0.0.0.0',
|
||||
port: 3001,
|
||||
messagePostFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
token: '',
|
||||
enableForcePushEvent: true,
|
||||
debug: false,
|
||||
heartInterval: 30000
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: WebsocketServerConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped></style>
|
@@ -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" }]
|
||||
}
|
||||
|
11
package.json
11
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.2.27",
|
||||
"version": "4.4.0",
|
||||
"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",
|
||||
@@ -17,14 +17,15 @@
|
||||
"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",
|
||||
@@ -36,7 +37,7 @@
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^12.1.0",
|
||||
"commander": "^13.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
@@ -60,4 +61,4 @@
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
107
src/common/cancel-task.ts
Normal file
107
src/common/cancel-task.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void;
|
||||
|
||||
export class CancelableTask<T> {
|
||||
private promise: Promise<T>;
|
||||
private cancelCallback: (() => void) | null = null;
|
||||
private isCanceled = false;
|
||||
private cancelListeners: Array<() => void> = [];
|
||||
|
||||
constructor(executor: TaskExecutor<T>) {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
const onCancel = (callback: () => void) => {
|
||||
this.cancelCallback = callback;
|
||||
};
|
||||
|
||||
executor(
|
||||
(value) => {
|
||||
if (!this.isCanceled) {
|
||||
resolve(value);
|
||||
}
|
||||
},
|
||||
(reason) => {
|
||||
if (!this.isCanceled) {
|
||||
reject(reason);
|
||||
}
|
||||
},
|
||||
onCancel
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
if (this.cancelCallback) {
|
||||
this.cancelCallback();
|
||||
}
|
||||
this.isCanceled = true;
|
||||
this.cancelListeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
public isTaskCanceled(): boolean {
|
||||
return this.isCanceled;
|
||||
}
|
||||
|
||||
public onCancel(listener: () => void) {
|
||||
this.cancelListeners.push(listener);
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.promise.then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
|
||||
): Promise<T | TResult> {
|
||||
return this.promise.catch(onrejected);
|
||||
}
|
||||
|
||||
public finally(onfinally?: (() => void) | undefined | null): Promise<T> {
|
||||
return this.promise.finally(onfinally);
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return {
|
||||
next: () => this.promise.then(value => ({ value, done: true })),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function demoAwait() {
|
||||
const executor: TaskExecutor<number> = (resolve, reject, onCancel) => {
|
||||
let count = 0;
|
||||
const intervalId = setInterval(() => {
|
||||
count++;
|
||||
console.log(`Task is running... Count: ${count}`);
|
||||
if (count === 5) {
|
||||
clearInterval(intervalId);
|
||||
resolve(count);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onCancel(() => {
|
||||
clearInterval(intervalId);
|
||||
console.log('Task has been canceled.');
|
||||
reject(new Error('Task was canceled'));
|
||||
});
|
||||
};
|
||||
|
||||
const task = new CancelableTask(executor);
|
||||
|
||||
task.onCancel(() => {
|
||||
console.log('Cancel listener triggered.');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
task.cancel(); // 取消任务
|
||||
}, 6000);
|
||||
|
||||
try {
|
||||
const result = await task;
|
||||
console.log(`Task completed with result: ${result}`);
|
||||
} catch (error) {
|
||||
console.error('Task failed:', error);
|
||||
}
|
||||
}
|
22
src/common/decorator.ts
Normal file
22
src/common/decorator.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// decoratorAsyncMethod(this,function,wrapper)
|
||||
async function decoratorMethod<T, R>(
|
||||
target: T,
|
||||
method: () => Promise<R>,
|
||||
wrapper: (result: R) => Promise<any>,
|
||||
executeImmediately: boolean = true
|
||||
): Promise<any> {
|
||||
const execute = async () => {
|
||||
try {
|
||||
const result = await method.call(target);
|
||||
return wrapper(result);
|
||||
} catch (error) {
|
||||
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
};
|
||||
|
||||
if (executeImmediately) {
|
||||
return execute();
|
||||
} else {
|
||||
return execute;
|
||||
}
|
||||
}
|
43
src/common/fall-back.ts
Normal file
43
src/common/fall-back.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
type Handler<T> = () => T | Promise<T>;
|
||||
type Checker<T> = (result: T) => T | Promise<T>;
|
||||
|
||||
export class Fallback<T> {
|
||||
private handlers: Handler<T>[] = [];
|
||||
private checker: Checker<T>;
|
||||
|
||||
constructor(checker?: Checker<T>) {
|
||||
this.checker = checker || (async (result: T) => result);
|
||||
}
|
||||
|
||||
add(handler: Handler<T>): this {
|
||||
this.handlers.push(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 执行处理程序链
|
||||
async run(): Promise<T> {
|
||||
const errors: Error[] = [];
|
||||
for (const handler of this.handlers) {
|
||||
try {
|
||||
const result = await handler();
|
||||
const data = await this.checker(result);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
errors.push(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
throw new AggregateError(errors, 'All handlers failed');
|
||||
}
|
||||
}
|
||||
export class FallbackUtil {
|
||||
static boolchecker<T>(value: T, condition: boolean): T {
|
||||
if (condition) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error('Condition is false, throwing error');
|
||||
}
|
||||
}
|
||||
}
|
121
src/common/file-uuid.ts
Normal file
121
src/common/file-uuid.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Peer } from '@/core';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
class TimeBasedCache<K, V> {
|
||||
private cache = new Map<K, { value: V, timestamp: number, frequency: number }>();
|
||||
private keyList = new Set<K>();
|
||||
private operationCount = 0;
|
||||
|
||||
constructor(private maxCapacity: number, private ttl: number = 30 * 1000 * 60, private cleanupCount: number = 10) {}
|
||||
|
||||
public put(key: K, value: V): void {
|
||||
const timestamp = Date.now();
|
||||
const cacheEntry = { value, timestamp, frequency: 1 };
|
||||
this.cache.set(key, cacheEntry);
|
||||
this.keyList.add(key);
|
||||
this.operationCount++;
|
||||
if (this.keyList.size > this.maxCapacity) this.evict();
|
||||
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
|
||||
}
|
||||
|
||||
public get(key: K): V | undefined {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && Date.now() - entry.timestamp < this.ttl) {
|
||||
entry.timestamp = Date.now();
|
||||
entry.frequency++;
|
||||
this.operationCount++;
|
||||
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
|
||||
return entry.value;
|
||||
} else {
|
||||
this.deleteKey(key);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private cleanup(count: number): void {
|
||||
const currentTime = Date.now();
|
||||
let cleaned = 0;
|
||||
for (const key of this.keyList) {
|
||||
if (cleaned >= count) break;
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && currentTime - entry.timestamp >= this.ttl) {
|
||||
this.deleteKey(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
this.operationCount = 0; // 重置操作计数器
|
||||
}
|
||||
|
||||
private deleteKey(key: K): void {
|
||||
this.cache.delete(key);
|
||||
this.keyList.delete(key);
|
||||
}
|
||||
|
||||
private evict(): void {
|
||||
while (this.keyList.size > this.maxCapacity) {
|
||||
let oldestKey: K | undefined;
|
||||
let minFrequency = Infinity;
|
||||
for (const key of this.keyList) {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && entry.frequency < minFrequency) {
|
||||
minFrequency = entry.frequency;
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
if (oldestKey !== undefined) this.deleteKey(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface FileUUIDData {
|
||||
peer: Peer;
|
||||
modelId?: string;
|
||||
fileId?: string;
|
||||
msgId?: string;
|
||||
elementId?: string;
|
||||
fileUUID?: string;
|
||||
}
|
||||
|
||||
class FileUUIDManager {
|
||||
private cache: TimeBasedCache<string, FileUUIDData>;
|
||||
|
||||
constructor(ttl: number) {
|
||||
this.cache = new TimeBasedCache<string, FileUUIDData>(5000, ttl);
|
||||
}
|
||||
|
||||
public encode(data: FileUUIDData, endString: string = "", customUUID?: string): string {
|
||||
const uuid = customUUID ? customUUID : randomUUID().replace(/-/g, '') + endString;
|
||||
this.cache.put(uuid, data);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public decode(uuid: string): FileUUIDData | undefined {
|
||||
return this.cache.get(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export class FileNapCatOneBotUUIDWrap {
|
||||
private manager: FileUUIDManager;
|
||||
|
||||
constructor(ttl: number = 86400000) {
|
||||
this.manager = new FileUUIDManager(ttl);
|
||||
}
|
||||
|
||||
public encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = "", customUUID?: string): string {
|
||||
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
|
||||
}
|
||||
|
||||
public decodeModelId(uuid: string): FileUUIDData | undefined {
|
||||
return this.manager.decode(uuid);
|
||||
}
|
||||
|
||||
public encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", customUUID?: string): string {
|
||||
return this.manager.encode({ peer, msgId, elementId, fileUUID }, "", customUUID);
|
||||
}
|
||||
|
||||
public decode(uuid: string): FileUUIDData | undefined {
|
||||
return this.manager.decode(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export const FileNapCatOneBotUUID = new FileNapCatOneBotUUIDWrap();
|
@@ -175,10 +175,9 @@ 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) {
|
||||
@@ -191,7 +190,7 @@ export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2Loca
|
||||
}
|
||||
|
||||
case FileUriType.Remote: {
|
||||
const buffer = await httpDownload(HandledUri);
|
||||
const buffer = await httpDownload({ url: HandledUri, headers: headers });
|
||||
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
@@ -206,4 +205,4 @@ export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2Loca
|
||||
default:
|
||||
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import os from 'node:os';
|
||||
import { Peer, QQLevel } from '@/core';
|
||||
import { QQLevel } from '@/core';
|
||||
|
||||
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
|
||||
return new Promise<ReturnType<T> | undefined>((resolve) => {
|
||||
@@ -24,81 +24,6 @@ export async function solveAsyncProblem<T extends (...args: any[]) => Promise<an
|
||||
});
|
||||
}
|
||||
|
||||
export class FileNapCatOneBotUUID {
|
||||
static encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);//储存data的hex长度
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decodeModelId(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
modelId: string,
|
||||
fileId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
modelId,
|
||||
fileId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
|
||||
static encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
//一个字节8位 一个ascii字符1字节 一个hex字符4位 表示一个ascii字符需要两个hex字符
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decode(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
msgId: string,
|
||||
elementId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|MsgFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
msgId,
|
||||
elementId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
@@ -78,7 +78,7 @@ class MessageUniqueWrapper {
|
||||
private readonly msgDataMap: LimitedHashTable<string, number>;
|
||||
private readonly msgIdMap: LimitedHashTable<string, number>;
|
||||
|
||||
constructor(maxMap: number = 1000) {
|
||||
constructor(maxMap: number = 5000) {
|
||||
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
|
||||
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.2.27';
|
||||
export const napCatVersion = '4.4.0';
|
||||
|
@@ -462,7 +462,7 @@ export class NTQQFileApi {
|
||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||
} catch (e) {
|
||||
this.context.logger.logError('获取rkey失败 Fallback Old Mode', e);
|
||||
this.context.logger.logDebug('获取rkey失败 Fallback Old Mode', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,7 +26,7 @@ export class NTQQGroupApi {
|
||||
}
|
||||
|
||||
async fetchGroupDetail(groupCode: string) {
|
||||
let [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
|
||||
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getGroupDetailInfo',
|
||||
'NodeIKernelGroupListener/onGroupDetailInfoChange',
|
||||
[groupCode, GroupInfoSource.KDATACARD],
|
||||
@@ -44,7 +44,7 @@ export class NTQQGroupApi {
|
||||
|
||||
async initCache() {
|
||||
for (const group of await this.getGroups(true)) {
|
||||
this.refreshGroupMemberCache(group.groupCode).then().catch();
|
||||
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,14 +126,23 @@ export class NTQQGroupApi {
|
||||
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
|
||||
}
|
||||
|
||||
async refreshGroupMemberCache(groupCode: string) {
|
||||
try {
|
||||
const members = await this.getGroupMemberAll(groupCode, true);
|
||||
this.groupMemberCache.set(groupCode, members.result.infos);
|
||||
} catch (e) {
|
||||
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
|
||||
async refreshGroupMemberCache(groupCode: string, isWait = true) {
|
||||
const updateCache = async () => {
|
||||
try {
|
||||
const members = await this.getGroupMemberAll(groupCode, true);
|
||||
this.groupMemberCache.set(groupCode, members.result.infos);
|
||||
} catch (e) {
|
||||
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isWait) {
|
||||
await updateCache();
|
||||
} else {
|
||||
updateCache();
|
||||
}
|
||||
return this.groupMemberCache;
|
||||
|
||||
return this.groupMemberCache.get(groupCode);
|
||||
}
|
||||
|
||||
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
||||
@@ -143,7 +152,7 @@ export class NTQQGroupApi {
|
||||
// 获取群成员缓存
|
||||
let members = this.groupMemberCache.get(groupCodeStr);
|
||||
if (!members) {
|
||||
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
|
||||
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
|
||||
}
|
||||
|
||||
const getMember = () => {
|
||||
@@ -157,7 +166,7 @@ export class NTQQGroupApi {
|
||||
let member = getMember();
|
||||
// 如果缓存中不存在该成员,尝试刷新缓存
|
||||
if (!member) {
|
||||
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
|
||||
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
|
||||
member = getMember();
|
||||
}
|
||||
return member;
|
||||
|
@@ -2,8 +2,7 @@ import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
|
||||
import { solveAsyncProblem } from '@/common/helper';
|
||||
import { promisify } from 'node:util';
|
||||
import { LRUCache } from '@/common/lru-cache';
|
||||
import { Fallback, FallbackUtil } from '@/common/fall-back';
|
||||
|
||||
export class NTQQUserApi {
|
||||
context: InstanceContext;
|
||||
@@ -108,6 +107,19 @@ export class NTQQUserApi {
|
||||
return retUser;
|
||||
}
|
||||
|
||||
async getUserDetailInfoV2(uid: string): Promise<User> {
|
||||
const fallback = new Fallback<User>((user) => FallbackUtil.boolchecker(user, user !== undefined && user.uin !== '0'))
|
||||
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KDB))
|
||||
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER));
|
||||
const retUser = await fallback.run().then(async (user) => {
|
||||
if (user && user.uin === '0') {
|
||||
user.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
|
||||
}
|
||||
return user;
|
||||
});
|
||||
return retUser;
|
||||
}
|
||||
|
||||
async modifySelfProfile(param: ModifyProfileParams) {
|
||||
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
|
||||
}
|
||||
@@ -169,46 +181,34 @@ export class NTQQUserApi {
|
||||
return skey;
|
||||
}
|
||||
|
||||
async getUidByUinV2(Uin: string) {
|
||||
if (!Uin) {
|
||||
async getUidByUinV2(uin: string) {
|
||||
if (!uin) {
|
||||
return '';
|
||||
}
|
||||
const services = [
|
||||
() => this.context.session.getUixConvertService().getUid([Uin]).then((data) => data.uidInfo.get(Uin)).catch(() => undefined),
|
||||
() => promisify<string, string[], Map<string, string>>
|
||||
(this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined),
|
||||
() => this.context.session.getGroupService().getUidByUins([Uin]).then((data) => data.uids.get(Uin)).catch(() => undefined),
|
||||
() => this.getUserDetailInfoByUin(Uin).then((data) => data.detail.uid).catch(() => undefined),
|
||||
];
|
||||
let uid: string | undefined = undefined;
|
||||
for (const service of services) {
|
||||
uid = await service();
|
||||
if (uid && uid.indexOf('*') == -1 && uid !== '') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const fallback =
|
||||
new Fallback<string | undefined>((uid) => FallbackUtil.boolchecker(uid, uid !== undefined && uid.indexOf('*') === -1 && uid !== ''))
|
||||
.add(() => this.context.session.getUixConvertService().getUid([uin]).then((data) => data.uidInfo.get(uin)))
|
||||
.add(() => this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [uin]).get(uin))
|
||||
.add(() => this.context.session.getGroupService().getUidByUins([uin]).then((data) => data.uids.get(uin)))
|
||||
.add(() => this.getUserDetailInfoByUin(uin).then((data) => data.detail.uid));
|
||||
|
||||
const uid = await fallback.run().catch(() => '');
|
||||
return uid ?? '';
|
||||
}
|
||||
|
||||
async getUinByUidV2(Uid: string) {
|
||||
if (!Uid) {
|
||||
async getUinByUidV2(uid: string) {
|
||||
if (!uid) {
|
||||
return '0';
|
||||
}
|
||||
const services = [
|
||||
() => this.context.session.getUixConvertService().getUin([Uid]).then((data) => data.uinInfo.get(Uid)).catch(() => undefined),
|
||||
() => this.context.session.getGroupService().getUinByUids([Uid]).then((data) => data.uins.get(Uid)).catch(() => undefined),
|
||||
() => promisify<string, string[], Map<string, string>>
|
||||
(this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined),
|
||||
() => this.core.apis.FriendApi.getBuddyIdMap(true).then((data) => data.getKey(Uid)).catch(() => undefined),
|
||||
() => this.getUserDetailInfo(Uid).then((data) => data.uin).catch(() => undefined),
|
||||
];
|
||||
let uin: string | undefined = undefined;
|
||||
for (const service of services) {
|
||||
uin = await service();
|
||||
if (uin && uin !== '0' && uin !== '') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = new Fallback<string | undefined>((uin) => FallbackUtil.boolchecker(uin, uin !== undefined && uin !== '0' && uin !== ''))
|
||||
.add(() => this.context.session.getUixConvertService().getUin([uid]).then((data) => data.uinInfo.get(uid)))
|
||||
.add(() => this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [uid]).get(uid))
|
||||
.add(() => this.context.session.getGroupService().getUinByUids([uid]).then((data) => data.uins.get(uid)))
|
||||
.add(() => this.getUserDetailInfo(uid).then((data) => data.uin));
|
||||
|
||||
const uin = await fallback.run().catch(() => '0');
|
||||
return uin ?? '0';
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
WebHonorType,
|
||||
} from '@/core';
|
||||
import { NapCatCore } from '..';
|
||||
import { createReadStream, readFileSync, statSync } from 'node:fs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
|
68
src/core/external/appid.json
vendored
68
src/core/external/appid.json
vendored
@@ -110,5 +110,73 @@
|
||||
"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"
|
||||
},
|
||||
"9.9.17-31219": {
|
||||
"appid": 537266450,
|
||||
"qua": "V1_WIN_NQ_9.9.17_31219_GW_B"
|
||||
},
|
||||
"9.9.17-31245": {
|
||||
"appid": 537266450,
|
||||
"qua": "V1_WIN_NQ_9.9.17_31245_GW_B"
|
||||
},
|
||||
"3.2.15-31245": {
|
||||
"appid": 537266485,
|
||||
"qua": "V1_LNX_NQ_3.2.15_31245_GW_B"
|
||||
},
|
||||
"6.9.63-31245": {
|
||||
"appid": 537266474,
|
||||
"qua": "V1_MAC_NQ_6.9.63_31245_GW_B"
|
||||
},
|
||||
"9.9.17-31363": {
|
||||
"appid": 537266500,
|
||||
"qua": "V1_WIN_NQ_9.9.17_31363_GW_B"
|
||||
}
|
||||
}
|
108
src/core/external/offset.json
vendored
108
src/core/external/offset.json
vendored
@@ -122,5 +122,113 @@
|
||||
"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"
|
||||
},
|
||||
"9.9.17-31219-x64": {
|
||||
"send": "39C1350",
|
||||
"recv": "39C5784"
|
||||
},
|
||||
"9.9.17-31245-x64": {
|
||||
"send": "39C1350",
|
||||
"recv": "39C5784"
|
||||
},
|
||||
"6.9.63.31245-x64": {
|
||||
"send": "4720A40",
|
||||
"recv": "47232AC"
|
||||
},
|
||||
"6.9.63-31245-arm64": {
|
||||
"send": "41DCBD8",
|
||||
"recv": "422D4E8"
|
||||
},
|
||||
"3.2.15-31245-x64": {
|
||||
"send": "A550F80",
|
||||
"recv": "A554880"
|
||||
},
|
||||
"3.2.15-31245-arm64": {
|
||||
"send": "71BEBB8",
|
||||
"recv": "71C23F0"
|
||||
},
|
||||
"9.9.17-31363-x64": {
|
||||
"send": "39C1910",
|
||||
"recv": "39C5d44"
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -15,6 +15,10 @@ export class RkeyManager {
|
||||
private_rkey: '',
|
||||
expired_time: 0,
|
||||
};
|
||||
private failureCount: number = 0;
|
||||
private lastFailureTimestamp: number = 0;
|
||||
private readonly FAILURE_LIMIT: number = 8;
|
||||
private readonly ONE_DAY: number = 24 * 60 * 60 * 1000;
|
||||
|
||||
constructor(serverUrl: string[], logger: LogWrapper) {
|
||||
this.logger = logger;
|
||||
@@ -22,11 +26,21 @@ export class RkeyManager {
|
||||
}
|
||||
|
||||
async getRkey() {
|
||||
const now = new Date().getTime();
|
||||
if (now - this.lastFailureTimestamp > this.ONE_DAY) {
|
||||
this.failureCount = 0; // 重置失败计数器
|
||||
}
|
||||
|
||||
if (this.failureCount >= this.FAILURE_LIMIT) {
|
||||
this.logger.logError(`[Rkey] 服务存在异常, 图片使用FallBack机制`);
|
||||
throw new Error('获取rkey失败次数过多,请稍后再试');
|
||||
}
|
||||
|
||||
if (this.isExpired()) {
|
||||
try {
|
||||
await this.refreshRkey();
|
||||
} catch (e) {
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
throw new Error(`${e}`);//外抛
|
||||
}
|
||||
}
|
||||
return this.rkeyData;
|
||||
@@ -34,7 +48,6 @@ export class RkeyManager {
|
||||
|
||||
isExpired(): boolean {
|
||||
const now = new Date().getTime() / 1000;
|
||||
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`);
|
||||
return now > this.rkeyData.expired_time;
|
||||
}
|
||||
|
||||
@@ -48,14 +61,17 @@ export class RkeyManager {
|
||||
private_rkey: temp.private_rkey.slice(6),
|
||||
expired_time: temp.expired_time
|
||||
};
|
||||
this.failureCount = 0;
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logger.logError(`[Rkey] Get Rkey ${url} Error `, e);
|
||||
this.logger.logError(`[Rkey] 异常服务 ${url} 异常 / `, e);
|
||||
this.failureCount++;
|
||||
this.lastFailureTimestamp = new Date().getTime();
|
||||
//是否为最后一个url
|
||||
if (url === this.serverUrl[this.serverUrl.length - 1]) {
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
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();
|
@@ -152,6 +152,7 @@ export class NapCatCore {
|
||||
// Renamed from 'InitDataListener'
|
||||
async initNapCatCoreListeners() {
|
||||
const msgListener = new NodeIKernelMsgListener();
|
||||
|
||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||
// 下线通知
|
||||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
@@ -27,13 +28,15 @@ function randText(len: number): string {
|
||||
|
||||
|
||||
export abstract class IPacketClient {
|
||||
protected readonly context: PacketContext;
|
||||
protected readonly napcore: NapCoreContext;
|
||||
protected readonly logger: PacketLogger;
|
||||
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
||||
logStack: LogStack;
|
||||
available: boolean = false;
|
||||
|
||||
protected constructor(context: PacketContext, logStack: LogStack) {
|
||||
this.context = context;
|
||||
protected constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
|
||||
this.napcore = napCore;
|
||||
this.logger = logger;
|
||||
this.logStack = logStack;
|
||||
}
|
||||
|
||||
@@ -81,7 +84,7 @@ export abstract class IPacketClient {
|
||||
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
||||
const trace_id = (randText(4) + md5 + data).slice(0, data.length / 2);
|
||||
return this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
|
||||
await this.context.napcore.sendSsoCmdReqByContend(cmd, trace_id);
|
||||
await this.napcore.sendSsoCmdReqByContend(cmd, trace_id);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,9 @@ import fs from "fs";
|
||||
import { IPacketClient } from "@/core/packet/client/baseClient";
|
||||
import { constants } from "node:os";
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
|
||||
// 0 send 1 recv
|
||||
export interface NativePacketExportType {
|
||||
@@ -19,8 +20,8 @@ export class NativePacketClient extends IPacketClient {
|
||||
private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
|
||||
private readonly sendEvent = new LRUCache<number, string>(500); // seq->trace_id
|
||||
|
||||
constructor(context: PacketContext, logStack: LogStack) {
|
||||
super(context, logStack);
|
||||
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
|
||||
super(napCore, logger, logStack);
|
||||
}
|
||||
|
||||
check(): boolean {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Data, WebSocket, ErrorEvent } from "ws";
|
||||
import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
|
||||
export class WsPacketClient extends IPacketClient {
|
||||
private websocket: WebSocket | null = null;
|
||||
@@ -13,15 +14,15 @@ export class WsPacketClient extends IPacketClient {
|
||||
private isInitialized: boolean = false;
|
||||
private initPayload: { pid: number, recv: string, send: string } | null = null;
|
||||
|
||||
constructor(context: PacketContext, logStack: LogStack) {
|
||||
super(context, logStack);
|
||||
this.clientUrl = this.context.napcore.config.packetServer
|
||||
? this.clientUrlWrap(this.context.napcore.config.packetServer)
|
||||
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
|
||||
super(napCore, logger, logStack);
|
||||
this.clientUrl = this.napcore.config.packetServer
|
||||
? this.clientUrlWrap(this.napcore.config.packetServer)
|
||||
: this.clientUrlWrap('127.0.0.1:8083');
|
||||
}
|
||||
|
||||
check(): boolean {
|
||||
if (!this.context.napcore.config.packetServer) {
|
||||
if (!this.napcore.config.packetServer) {
|
||||
this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`);
|
||||
return false;
|
||||
}
|
||||
@@ -67,7 +68,7 @@ export class WsPacketClient extends IPacketClient {
|
||||
this.websocket.onopen = () => {
|
||||
this.available = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.context.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
|
||||
this.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
|
||||
if (!this.isInitialized && this.initPayload) {
|
||||
this.websocket!.send(JSON.stringify({
|
||||
action: 'init',
|
||||
@@ -79,15 +80,15 @@ export class WsPacketClient extends IPacketClient {
|
||||
};
|
||||
this.websocket.onclose = () => {
|
||||
this.available = false;
|
||||
this.context.logger.warn(`WebSocket 连接关闭,尝试重连...`);
|
||||
this.logger.warn(`WebSocket 连接关闭,尝试重连...`);
|
||||
reject(new Error('WebSocket 连接关闭'));
|
||||
};
|
||||
this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => {
|
||||
this.context.logger.error(`处理消息时出错: ${err}`);
|
||||
this.logger.error(`处理消息时出错: ${err}`);
|
||||
});
|
||||
this.websocket.onerror = (event: ErrorEvent) => {
|
||||
this.available = false;
|
||||
this.context.logger.error(`WebSocket 出错: ${event.message}`);
|
||||
this.logger.error(`WebSocket 出错: ${event.message}`);
|
||||
this.websocket?.close();
|
||||
reject(new Error(`WebSocket 出错: ${event.message}`));
|
||||
};
|
||||
@@ -106,7 +107,7 @@ export class WsPacketClient extends IPacketClient {
|
||||
const event = this.cb.get(`${trace_id_md5}${action}`);
|
||||
if (event) await event(json.data);
|
||||
} catch (error) {
|
||||
this.context.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
|
||||
this.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { IPacketClient } from "@/core/packet/client/baseClient";
|
||||
import { NativePacketClient } from "@/core/packet/client/nativeClient";
|
||||
import { WsPacketClient } from "@/core/packet/client/wsClient";
|
||||
import { OidbPacket } from "@/core/packet/transformer/base";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
|
||||
type clientPriority = {
|
||||
[key: number]: (context: PacketContext, logStack: LogStack) => IPacketClient;
|
||||
[key: number]: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => IPacketClient;
|
||||
}
|
||||
|
||||
const clientPriority: clientPriority = {
|
||||
10: (context: PacketContext, logStack: LogStack) => new NativePacketClient(context, logStack),
|
||||
1: (context: PacketContext, logStack: LogStack) => new WsPacketClient(context, logStack),
|
||||
10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack),
|
||||
1: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new WsPacketClient(napCore, logger, logStack),
|
||||
};
|
||||
|
||||
export class LogStack {
|
||||
@@ -51,13 +51,15 @@ export class LogStack {
|
||||
}
|
||||
|
||||
export class PacketClientContext {
|
||||
private readonly context: PacketContext;
|
||||
private readonly napCore: NapCoreContext;
|
||||
private readonly logger: PacketLogger;
|
||||
private readonly logStack: LogStack;
|
||||
private readonly _client: IPacketClient;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.context = context;
|
||||
this.logStack = new LogStack(context.logger);
|
||||
constructor(napCore: NapCoreContext, logger: PacketLogger) {
|
||||
this.napCore = napCore;
|
||||
this.logger = logger;
|
||||
this.logStack = new LogStack(logger);
|
||||
this._client = this.newClient();
|
||||
}
|
||||
|
||||
@@ -79,23 +81,23 @@ export class PacketClientContext {
|
||||
}
|
||||
|
||||
private newClient(): IPacketClient {
|
||||
const prefer = this.context.napcore.config.packetBackend;
|
||||
const prefer = this.napCore.config.packetBackend;
|
||||
let client: IPacketClient | null;
|
||||
switch (prefer) {
|
||||
case "native":
|
||||
this.context.logger.info("使用指定的 NativePacketClient 作为后端");
|
||||
client = new NativePacketClient(this.context, this.logStack);
|
||||
this.logger.info("使用指定的 NativePacketClient 作为后端");
|
||||
client = new NativePacketClient(this.napCore, this.logger, this.logStack);
|
||||
break;
|
||||
case "frida":
|
||||
this.context.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
|
||||
client = new WsPacketClient(this.context, this.logStack);
|
||||
this.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
|
||||
client = new WsPacketClient(this.napCore, this.logger, this.logStack);
|
||||
break;
|
||||
case "auto":
|
||||
case undefined:
|
||||
client = this.judgeClient();
|
||||
break;
|
||||
default:
|
||||
this.context.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
|
||||
this.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
|
||||
client = null;
|
||||
}
|
||||
if (!client?.check()) {
|
||||
@@ -110,7 +112,7 @@ export class PacketClientContext {
|
||||
private judgeClient(): IPacketClient {
|
||||
const sortedClients = Object.entries(clientPriority)
|
||||
.map(([priority, clientFactory]) => {
|
||||
const client = clientFactory(this.context, this.logStack);
|
||||
const client = clientFactory(this.napCore, this.logger, this.logStack);
|
||||
const score = +priority * +client.check();
|
||||
return { client, score };
|
||||
})
|
||||
@@ -120,7 +122,7 @@ export class PacketClientContext {
|
||||
if (!selectedClient) {
|
||||
throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!");
|
||||
}
|
||||
this.context.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
|
||||
this.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
|
||||
return selectedClient;
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { LogLevel, LogWrapper } from "@/common/log";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
|
||||
// TODO: check bind?
|
||||
export class PacketLogger {
|
||||
private readonly napLogger: LogWrapper;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.napLogger = context.napcore.logger;
|
||||
constructor(napcore: NapCoreContext) {
|
||||
this.napLogger = napcore.logger;
|
||||
}
|
||||
|
||||
private _log(level: LogLevel, ...msg: any[]): void {
|
||||
|
@@ -13,15 +13,22 @@ import { MiniAppRawData, MiniAppReqParams } from "@/core/packet/entities/miniApp
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from "@napneko/nap-proto-core";
|
||||
import { IndexNode, MsgInfo } from "@/core/packet/transformer/proto";
|
||||
import { OidbPacket } from "@/core/packet/transformer/base";
|
||||
import { ImageOcrResult } from "@/core/packet/entities/ocrResult";
|
||||
|
||||
export class PacketOperationContext {
|
||||
private readonly context: PacketContext;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
async sendPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
|
||||
return await this.context.client.sendOidbPacket(pkt, rsp);
|
||||
}
|
||||
|
||||
async GroupPoke(groupUin: number, uin: number) {
|
||||
const req = trans.SendPoke.build(groupUin, uin);
|
||||
const req = trans.SendPoke.build(uin, groupUin);
|
||||
await this.context.client.sendOidbPacket(req);
|
||||
}
|
||||
|
||||
@@ -90,6 +97,46 @@ export class PacketOperationContext {
|
||||
});
|
||||
}
|
||||
|
||||
async UploadImage(img: PacketMsgPicElement) {
|
||||
await this.context.highway.uploadImage({
|
||||
chatType: ChatType.KCHATTYPEC2C,
|
||||
peerUid: this.context.napcore.basicInfo.uid
|
||||
}, img);
|
||||
const index = img.msgInfo?.msgInfoBody?.at(0)?.index;
|
||||
if (!index) {
|
||||
throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined');
|
||||
}
|
||||
return await this.GetImageUrl(this.context.napcore.basicInfo.uid, index);
|
||||
}
|
||||
|
||||
async GetImageUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||
const req = trans.DownloadImage.build(selfUid, node);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = trans.DownloadImage.parse(resp);
|
||||
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||
}
|
||||
|
||||
async ImageOCR(imgUrl: string) {
|
||||
const req = trans.ImageOCR.build(imgUrl);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = trans.ImageOCR.parse(resp);
|
||||
return {
|
||||
texts: res.ocrRspBody.textDetections.map((item) => {
|
||||
return {
|
||||
text: item.detectedText,
|
||||
confidence: item.confidence,
|
||||
coordinates: item.polygon.coordinates.map((c) => {
|
||||
return {
|
||||
x: c.x,
|
||||
y: c.y
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
language: res.ocrRspBody.language
|
||||
} as ImageOcrResult;
|
||||
}
|
||||
|
||||
async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
|
||||
await this.UploadResources(msg, groupUin);
|
||||
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);
|
||||
|
@@ -7,19 +7,19 @@ import { PacketOperationContext } from "@/core/packet/context/operationContext";
|
||||
import { PacketMsgConverter } from "@/core/packet/message/converter";
|
||||
|
||||
export class PacketContext {
|
||||
readonly msgConverter: PacketMsgConverter;
|
||||
readonly napcore: NapCoreContext;
|
||||
readonly logger: PacketLogger;
|
||||
readonly client: PacketClientContext;
|
||||
readonly highway: PacketHighwayContext;
|
||||
readonly msgConverter: PacketMsgConverter;
|
||||
readonly operation: PacketOperationContext;
|
||||
|
||||
constructor(core: NapCatCore) {
|
||||
this.napcore = new NapCoreContext(core);
|
||||
this.logger = new PacketLogger(this);
|
||||
this.client = new PacketClientContext(this);
|
||||
this.highway = new PacketHighwayContext(this);
|
||||
this.msgConverter = new PacketMsgConverter();
|
||||
this.napcore = new NapCoreContext(core);
|
||||
this.logger = new PacketLogger(this.napcore);
|
||||
this.client = new PacketClientContext(this.napcore, this.logger);
|
||||
this.highway = new PacketHighwayContext(this.napcore, this.logger, this.client);
|
||||
this.operation = new PacketOperationContext(this);
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ export interface MiniAppReqCustomParams {
|
||||
desc: string;
|
||||
picUrl: string;
|
||||
jumpUrl: string;
|
||||
webUrl?: string;
|
||||
}
|
||||
|
||||
export interface MiniAppReqTemplateParams {
|
||||
|
15
src/core/packet/entities/ocrResult.ts
Normal file
15
src/core/packet/entities/ocrResult.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface ImageOcrResult {
|
||||
texts: TextDetection[];
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface TextDetection {
|
||||
text: string;
|
||||
confidence: number;
|
||||
coordinates: Coordinate[];
|
||||
}
|
||||
|
||||
export interface Coordinate {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
import { PacketHighwayClient } from "@/core/packet/highway/client";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import FetchSessionKey from "@/core/packet/transformer/highway/FetchSessionKey";
|
||||
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
|
||||
@@ -16,6 +15,8 @@ import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import * as trans from "@/core/packet/transformer";
|
||||
import fs from "fs";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketClientContext } from "@/core/packet/context/clientContext";
|
||||
|
||||
export const BlockSize = 1024 * 1024;
|
||||
|
||||
@@ -33,23 +34,25 @@ export interface PacketHighwaySig {
|
||||
}
|
||||
|
||||
export class PacketHighwayContext {
|
||||
private readonly context: PacketContext;
|
||||
private readonly napcore: NapCoreContext;
|
||||
private readonly client: PacketClientContext;
|
||||
protected sig: PacketHighwaySig;
|
||||
protected logger: PacketLogger;
|
||||
protected hwClient: PacketHighwayClient;
|
||||
private cachedPrepareReq: Promise<void> | null = null;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.context = context;
|
||||
constructor(napcore: NapCoreContext, logger: PacketLogger, client: PacketClientContext) {
|
||||
this.napcore = napcore;
|
||||
this.client = client;
|
||||
this.sig = {
|
||||
uin: String(context.napcore.basicInfo.uin),
|
||||
uid: context.napcore.basicInfo.uid,
|
||||
uin: String(this.napcore.basicInfo.uin),
|
||||
uid: this.napcore.basicInfo.uid,
|
||||
sigSession: null,
|
||||
sessionKey: null,
|
||||
serverAddr: [],
|
||||
};
|
||||
this.logger = context.logger;
|
||||
this.hwClient = new PacketHighwayClient(this.sig, context.logger);
|
||||
this.logger = logger;
|
||||
this.hwClient = new PacketHighwayClient(this.sig, this.logger);
|
||||
}
|
||||
|
||||
private async checkAvailable() {
|
||||
@@ -66,7 +69,7 @@ export class PacketHighwayContext {
|
||||
private async prepareUpload(): Promise<void> {
|
||||
this.logger.debug('[Highway] on prepareUpload!');
|
||||
const packet = FetchSessionKey.build();
|
||||
const req = await this.context.client.sendOidbPacket(packet, true);
|
||||
const req = await this.client.sendOidbPacket(packet, true);
|
||||
const rsp = FetchSessionKey.parse(req);
|
||||
this.sig.sigSession = rsp.httpConn.sigSession;
|
||||
this.sig.sessionKey = rsp.httpConn.sessionKey;
|
||||
@@ -136,7 +139,7 @@ export class PacketHighwayContext {
|
||||
private async uploadGroupImage(groupUin: number, img: PacketMsgPicElement): Promise<void> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const req = UploadGroupImage.build(groupUin, img);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = UploadGroupImage.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -173,7 +176,7 @@ export class PacketHighwayContext {
|
||||
private async uploadC2CImage(peerUid: string, img: PacketMsgPicElement): Promise<void> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const req = trans.UploadPrivateImage.build(peerUid, img);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateImage.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -211,7 +214,7 @@ export class PacketHighwayContext {
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const req = trans.UploadGroupVideo.build(groupUin, video);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupVideo.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -276,7 +279,7 @@ export class PacketHighwayContext {
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const req = trans.UploadPrivateVideo.build(peerUid, video);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateVideo.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -339,7 +342,7 @@ export class PacketHighwayContext {
|
||||
private async uploadGroupPtt(groupUin: number, ptt: PacketMsgPttElement): Promise<void> {
|
||||
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
||||
const req = trans.UploadGroupPtt.build(groupUin, ptt);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupPtt.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -375,7 +378,7 @@ export class PacketHighwayContext {
|
||||
private async uploadC2CPtt(peerUid: string, ptt: PacketMsgPttElement): Promise<void> {
|
||||
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
||||
const req = trans.UploadPrivatePtt.build(peerUid, ptt);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivatePtt.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -413,7 +416,7 @@ export class PacketHighwayContext {
|
||||
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
||||
file.fileSha1 = await calculateSha1(file.filePath);
|
||||
const req = trans.UploadGroupFile.build(groupUin, file);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupFile.parse(resp);
|
||||
if (!preRespData?.upload?.boolFileExist) {
|
||||
this.logger.debug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
|
||||
@@ -476,7 +479,7 @@ export class PacketHighwayContext {
|
||||
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
||||
file.fileSha1 = await calculateSha1(file.filePath);
|
||||
const req = await trans.UploadPrivateFile.build(this.sig.uid, peerUid, file);
|
||||
const res = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateFile.parse(res);
|
||||
if (!preRespData.upload?.boolFileExist) {
|
||||
this.logger.debug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
|
||||
@@ -531,7 +534,7 @@ export class PacketHighwayContext {
|
||||
file.fileUuid = preRespData.upload?.uuid;
|
||||
file.fileHash = preRespData.upload?.fileAddon;
|
||||
const fileExistReq = trans.DownloadOfflineFile.build(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
|
||||
const fileExistRes = await this.context.client.sendOidbPacket(fileExistReq, true);
|
||||
const fileExistRes = await this.client.sendOidbPacket(fileExistReq, true);
|
||||
file._e37_800_rsp = trans.DownloadOfflineFile.parse(fileExistRes);
|
||||
file._private_send_uid = this.sig.uid;
|
||||
file._private_recv_uid = peerUid;
|
||||
|
@@ -256,6 +256,8 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
width: number;
|
||||
height: number;
|
||||
picType: PicType;
|
||||
picSubType: number;
|
||||
summary: string;
|
||||
sha1: string | null = null;
|
||||
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
|
||||
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
|
||||
@@ -270,6 +272,10 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
this.width = element.picElement.picWidth;
|
||||
this.height = element.picElement.picHeight;
|
||||
this.picType = element.picElement.picType;
|
||||
this.picSubType = element.picElement.picSubType ?? 0;
|
||||
this.summary = element.picElement.summary === '' ? (
|
||||
element.picElement.picSubType === 0 ? '[图片]' : '[动画表情]'
|
||||
) : element.picElement.summary;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
@@ -288,7 +294,7 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[图片]";
|
||||
return this.summary;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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: "",
|
||||
|
37
src/core/packet/transformer/action/ImageOCR.ts
Normal file
37
src/core/packet/transformer/action/ImageOCR.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
|
||||
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
|
||||
|
||||
class ImageOCR extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0xE07_0_Response> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
build(url: string): OidbPacket {
|
||||
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0).encode(
|
||||
{
|
||||
version: 1,
|
||||
client: 0,
|
||||
entrance: 1,
|
||||
ocrReqBody: {
|
||||
imageUrl: url,
|
||||
originMd5: "",
|
||||
afterCompressMd5: "",
|
||||
afterCompressFileSize: "",
|
||||
afterCompressWeight: "",
|
||||
afterCompressHeight: "",
|
||||
isCut: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
return OidbBase.build(0XEB7, 1, body, false, false);
|
||||
}
|
||||
|
||||
parse(data: Buffer) {
|
||||
const base = OidbBase.parse(data);
|
||||
return new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0_Response).decode(base.body);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ImageOCR();
|
@@ -5,3 +5,4 @@ export { default as GroupSign } from './GroupSign';
|
||||
export { default as GetStrangerInfo } from './GetStrangerInfo';
|
||||
export { default as SendPoke } from './SendPoke';
|
||||
export { default as SetSpecialTitle } from './SetSpecialTitle';
|
||||
export { default as ImageOCR } from './ImageOCR';
|
||||
|
51
src/core/packet/transformer/highway/DownloadImage.ts
Normal file
51
src/core/packet/transformer/highway/DownloadImage.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
|
||||
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
|
||||
import { IndexNode } from "@/core/packet/transformer/proto";
|
||||
|
||||
class DownloadImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 1,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 1,
|
||||
sceneType: 1,
|
||||
c2C: {
|
||||
accountType: 2,
|
||||
targetUid: selfUid
|
||||
},
|
||||
},
|
||||
client: {
|
||||
agentType: 2,
|
||||
}
|
||||
},
|
||||
download: {
|
||||
node: node,
|
||||
download: {
|
||||
video: {
|
||||
busiType: 0,
|
||||
sceneType: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return OidbBase.build(0x11C5, 200, body, true, false);
|
||||
}
|
||||
|
||||
parse(data: Buffer) {
|
||||
const oidbBody = OidbBase.parse(data).body;
|
||||
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
|
||||
}
|
||||
}
|
||||
|
||||
export default new DownloadImage();
|
@@ -58,8 +58,11 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
|
||||
compatQMsgSceneType: 2,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
|
||||
textSummary: "Nya~", // TODO:
|
||||
bizType: img.picSubType,
|
||||
bytesPbReserveTroop: {
|
||||
subType: img.picSubType,
|
||||
},
|
||||
textSummary: img.summary,
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
|
@@ -58,8 +58,11 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
|
||||
compatQMsgSceneType: 1,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
|
||||
textSummary: "Nya~", // TODO:
|
||||
bizType: img.picSubType,
|
||||
bytesPbReserveC2C: {
|
||||
subType: img.picSubType,
|
||||
},
|
||||
textSummary: img.summary,
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
|
@@ -11,3 +11,4 @@ export { default as UploadPrivateFile } from './UploadPrivateFile';
|
||||
export { default as UploadPrivateImage } from './UploadPrivateImage';
|
||||
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
|
||||
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
|
||||
export { default as DownloadImage } from './DownloadImage';
|
||||
|
@@ -29,3 +29,4 @@ export * from "./oidb/Oidb.0xEB7";
|
||||
export * from "./oidb/Oidb.0xED3_1";
|
||||
export * from "./oidb/Oidb.0XFE1_2";
|
||||
export * from "./oidb/OidbBase";
|
||||
export * from "./oidb/Oidb.0xE07";
|
||||
|
@@ -72,6 +72,14 @@ export const GroupChange = {
|
||||
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),
|
||||
|
59
src/core/packet/transformer/proto/oidb/Oidb.0xE07.ts
Normal file
59
src/core/packet/transformer/proto/oidb/Oidb.0xE07.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
|
||||
|
||||
export const OidbSvcTrpcTcp0xE07_0 = {
|
||||
version: ProtoField(1, ScalarType.UINT32),
|
||||
client: ProtoField(2, ScalarType.UINT32),
|
||||
entrance: ProtoField(3, ScalarType.UINT32),
|
||||
ocrReqBody: ProtoField(10, () => OcrReqBody, true),
|
||||
};
|
||||
|
||||
export const OcrReqBody = {
|
||||
imageUrl: ProtoField(1, ScalarType.STRING),
|
||||
languageType: ProtoField(2, ScalarType.UINT32),
|
||||
scene: ProtoField(3, ScalarType.UINT32),
|
||||
originMd5: ProtoField(10, ScalarType.STRING),
|
||||
afterCompressMd5: ProtoField(11, ScalarType.STRING),
|
||||
afterCompressFileSize: ProtoField(12, ScalarType.STRING),
|
||||
afterCompressWeight: ProtoField(13, ScalarType.STRING),
|
||||
afterCompressHeight: ProtoField(14, ScalarType.STRING),
|
||||
isCut: ProtoField(15, ScalarType.BOOL),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0xE07_0_Response = {
|
||||
retCode: ProtoField(1, ScalarType.INT32),
|
||||
errMsg: ProtoField(2, ScalarType.STRING),
|
||||
wording: ProtoField(3, ScalarType.STRING),
|
||||
ocrRspBody: ProtoField(10, () => OcrRspBody),
|
||||
};
|
||||
|
||||
export const OcrRspBody = {
|
||||
textDetections: ProtoField(1, () => TextDetection, false, true),
|
||||
language: ProtoField(2, ScalarType.STRING),
|
||||
requestId: ProtoField(3, ScalarType.STRING),
|
||||
ocrLanguageList: ProtoField(101, ScalarType.STRING, false, true),
|
||||
dstTranslateLanguageList: ProtoField(102, ScalarType.STRING, false, true),
|
||||
languageList: ProtoField(103, () => Language, false, true),
|
||||
afterCompressWeight: ProtoField(111, ScalarType.UINT32),
|
||||
afterCompressHeight: ProtoField(112, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const TextDetection = {
|
||||
detectedText: ProtoField(1, ScalarType.STRING),
|
||||
confidence: ProtoField(2, ScalarType.UINT32),
|
||||
polygon: ProtoField(3, () => Polygon),
|
||||
advancedInfo: ProtoField(4, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const Polygon = {
|
||||
coordinates: ProtoField(1, () => Coordinate, false, true),
|
||||
};
|
||||
|
||||
export const Coordinate = {
|
||||
x: ProtoField(1, ScalarType.INT32),
|
||||
y: ProtoField(2, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const Language = {
|
||||
languageCode: ProtoField(1, ScalarType.STRING),
|
||||
languageDesc: ProtoField(2, ScalarType.STRING),
|
||||
};
|
@@ -189,8 +189,8 @@ export const VideoExtBizInfo = {
|
||||
export const PicExtBizInfo = {
|
||||
BizType: ProtoField(1, ScalarType.UINT32),
|
||||
TextSummary: ProtoField(2, ScalarType.STRING),
|
||||
BytesPbReserveC2c: ProtoField(11, ScalarType.BYTES),
|
||||
BytesPbReserveTroop: ProtoField(12, ScalarType.BYTES),
|
||||
BytesPbReserveC2c: ProtoField(11, () => BytesPbReserveC2c),
|
||||
BytesPbReserveTroop: ProtoField(12, () => BytesPbReserveTroop),
|
||||
FromScene: ProtoField(1001, ScalarType.UINT32),
|
||||
ToScene: ProtoField(1002, ScalarType.UINT32),
|
||||
OldFileId: ProtoField(1003, ScalarType.UINT32),
|
||||
@@ -211,3 +211,27 @@ export const UploadInfo = {
|
||||
FileInfo: ProtoField(1, () => FileInfo),
|
||||
SubFileType: ProtoField(2, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const BytesPbReserveC2c = {
|
||||
subType: ProtoField(1, ScalarType.UINT32),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
field4: ProtoField(4, ScalarType.UINT32),
|
||||
field8: ProtoField(8, ScalarType.STRING),
|
||||
field10: ProtoField(10, ScalarType.UINT32),
|
||||
field12: ProtoField(12, ScalarType.STRING),
|
||||
field18: ProtoField(18, ScalarType.STRING),
|
||||
field19: ProtoField(19, ScalarType.STRING),
|
||||
field20: ProtoField(20, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const BytesPbReserveTroop = {
|
||||
subType: ProtoField(1, ScalarType.UINT32),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
field4: ProtoField(4, ScalarType.UINT32),
|
||||
field9: ProtoField(9, ScalarType.STRING),
|
||||
field10: ProtoField(10, ScalarType.UINT32),
|
||||
field12: ProtoField(12, ScalarType.STRING),
|
||||
field18: ProtoField(18, ScalarType.STRING),
|
||||
field19: ProtoField(19, ScalarType.STRING),
|
||||
field21: ProtoField(21, ScalarType.BYTES),
|
||||
};
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -56,6 +56,7 @@ export interface GrayTipElement {
|
||||
aioOpGrayTipElement: TipAioOpGrayTipElement;
|
||||
groupElement: TipGroupElement;
|
||||
xmlElement: {
|
||||
busiId: string;
|
||||
content: string;
|
||||
templId: string;
|
||||
};
|
||||
|
@@ -9,4 +9,5 @@ export * from './sign';
|
||||
export * from './element';
|
||||
export * from './constant';
|
||||
export * from './graytip';
|
||||
export * from './emoji';
|
||||
export * from './emoji';
|
||||
export * from './service';
|
@@ -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;
|
||||
|
35
src/core/types/service.ts
Normal file
35
src/core/types/service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export enum LoginErrorCode {
|
||||
KLOGINERRORACCOUNTNOTUIN = 140022018,
|
||||
KLOGINERRORACCOUNTORPASSWORDERROR = 140022013,
|
||||
KLOGINERRORBLACKACCOUNT = 150022021,
|
||||
KLOGINERRORDEFAULT = 140022000,
|
||||
KLOGINERROREXPIRETICKET = 140022014,
|
||||
KLOGINERRORFROZEN = 140022005,
|
||||
KLOGINERRORILLAGETICKET = 140022016,
|
||||
KLOGINERRORINVAILDCOOKIE = 140022012,
|
||||
KLOGINERRORINVALIDPARAMETER = 140022001,
|
||||
KLOGINERRORKICKEDTICKET = 140022015,
|
||||
KLOGINERRORMUTIPLEPASSWORDINCORRECT = 150022029,
|
||||
KLOGINERRORNEEDUPDATE = 140022004,
|
||||
KLOGINERRORNEEDVERIFYREALNAME = 140022019,
|
||||
KLOGINERRORNEWDEVICE = 140022010,
|
||||
KLOGINERRORNICEACCOUNTEXPIRED = 150022020,
|
||||
KLOGINERRORNICEACCOUNTPARENTCHILDEXPIRED = 150022025,
|
||||
KLOGINERRORPASSWORD = 2,
|
||||
KLOGINERRORPROOFWATER = 140022008,
|
||||
KLOGINERRORPROTECT = 140022006,
|
||||
KLOGINERRORREFUSEPASSOWRDLOGIN = 140022009,
|
||||
KLOGINERRORREMINDCANAELLATEDSTATUS = 150022028,
|
||||
KLOGINERRORSCAN = 1,
|
||||
KLOGINERRORSCCESS = 0,
|
||||
KLOGINERRORSECBEAT = 140022017,
|
||||
KLOGINERRORSMSINVALID = 150022026,
|
||||
KLOGINERRORSTRICK = 140022007,
|
||||
KLOGINERRORSYSTEMFAILED = 140022002,
|
||||
KLOGINERRORTGTGTEXCHAGEA1FORBID = 150022027,
|
||||
KLOGINERRORTIMEOUTRETRY = 140022003,
|
||||
KLOGINERRORTOOMANYTIMESTODAY = 150022023,
|
||||
KLOGINERRORTOOOFTEN = 150022022,
|
||||
KLOGINERRORUNREGISTERED = 150022024,
|
||||
KLOGINERRORUNUSUALDEVICE = 140022011,
|
||||
}
|
@@ -18,7 +18,7 @@ export interface BuddyCategoryType {
|
||||
export interface CoreInfo {
|
||||
uid: string;
|
||||
uin: string;
|
||||
nick: string;
|
||||
nick?: string;
|
||||
remark: 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)
|
||||
})
|
||||
});
|
@@ -27,6 +27,7 @@ export async function NCoreInitFramework(
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.log('[NapCat] [Error] unhandledRejection:', reason);
|
||||
});
|
||||
|
@@ -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);
|
||||
|
@@ -0,0 +1 @@
|
||||
import '@/universal/napcat';
|
@@ -2,6 +2,7 @@ import { ActionName, BaseCheckResult } from './router';
|
||||
import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { NapCatOneBot11Adapter, OB11Return } from '@/onebot';
|
||||
import { NetworkAdapterConfig } from '../config/config';
|
||||
|
||||
export class OB11Response {
|
||||
private static createResponse<T>(data: T, status: string, retcode: number, message: string = '', echo: any = null): OB11Return<T> {
|
||||
@@ -29,7 +30,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;
|
||||
@@ -55,13 +56,13 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
public async handle(payload: PayloadType, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
|
||||
public async handle(payload: PayloadType, adaptername: string, config: NetworkAdapterConfig): Promise<OB11Return<ReturnDataType | null>> {
|
||||
const result = await this.check(payload);
|
||||
if (!result.valid) {
|
||||
return OB11Response.error(result.message, 400);
|
||||
}
|
||||
try {
|
||||
const resData = await this._handle(payload, adaptername);
|
||||
const resData = await this._handle(payload, adaptername, config);
|
||||
return OB11Response.ok(resData);
|
||||
} catch (e: any) {
|
||||
this.core.context.logger.logError('发生错误', e);
|
||||
@@ -69,13 +70,13 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||
}
|
||||
}
|
||||
|
||||
public async websocketHandle(payload: PayloadType, echo: any, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
|
||||
public async websocketHandle(payload: PayloadType, echo: any, adaptername: string, config: NetworkAdapterConfig): Promise<OB11Return<ReturnDataType | null>> {
|
||||
const result = await this.check(payload);
|
||||
if (!result.valid) {
|
||||
return OB11Response.error(result.message, 1400, echo);
|
||||
}
|
||||
try {
|
||||
const resData = await this._handle(payload, adaptername);
|
||||
const resData = await this._handle(payload, adaptername, config);
|
||||
return OB11Response.ok(resData, echo);
|
||||
} catch (e: any) {
|
||||
this.core.context.logger.logError('发生错误', e);
|
||||
@@ -83,5 +84,5 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||
}
|
||||
}
|
||||
|
||||
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
|
||||
}
|
||||
abstract _handle(payload: PayloadType, adaptername: string, config: NetworkAdapterConfig): Promise<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 };
|
||||
}
|
||||
}
|
@@ -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,30 +10,34 @@ 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) {
|
||||
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image));
|
||||
const { path, success } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
|
||||
if (!success) {
|
||||
throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`);
|
||||
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
|
||||
}
|
||||
if (path) {
|
||||
await checkFileExist(path, 5000); // 避免崩溃
|
||||
const ret = await this.core.apis.SystemApi.ocrImage(path);
|
||||
fs.unlink(path, () => { });
|
||||
|
||||
if (!ret) {
|
||||
throw new Error(`OCR ${payload.image}失败`);
|
||||
try {
|
||||
await checkFileExist(path, 5000); // 避免崩溃
|
||||
const ret = await this.core.apis.SystemApi.ocrImage(path);
|
||||
if (!ret) {
|
||||
throw new Error(`OCR ${payload.image}失败`);
|
||||
}
|
||||
return ret.result;
|
||||
} finally {
|
||||
fs.unlink(path, () => { });
|
||||
}
|
||||
return ret.result;
|
||||
}
|
||||
fs.unlink(path, () => { });
|
||||
throw new Error(`OCR ${payload.image}失败,文件可能不存在`);
|
||||
throw new Error(`OCR ${payload.image}失败, 文件可能不存在`);
|
||||
}
|
||||
}
|
||||
|
||||
export class IOCRImage extends OCRImage {
|
||||
actionName = ActionName.IOCRImage;
|
||||
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.operation.sendPacket({ 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.SetGroupSign;
|
||||
}
|
||||
|
||||
export class SendGroupSign extends SetGroupSignBase {
|
||||
actionName = ActionName.SendGroupSign;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import fs from 'fs/promises';
|
||||
import { FileNapCatOneBotUUID } from '@/common/helper';
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
@@ -28,7 +28,7 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
|
||||
payload.file ||= payload.file_id || '';
|
||||
//接收消息标记模式
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file);
|
||||
if (contextMsgFile) {
|
||||
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
|
||||
const { peer, msgId, elementId } = contextMsgFile;
|
||||
const downloadPath = await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
|
||||
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList
|
||||
@@ -68,7 +68,7 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
|
||||
|
||||
//群文件模式
|
||||
const contextModelIdFile = FileNapCatOneBotUUID.decodeModelId(payload.file);
|
||||
if (contextModelIdFile) {
|
||||
if (contextModelIdFile && contextModelIdFile.modelId) {
|
||||
const { peer, modelId } = contextModelIdFile;
|
||||
const downloadPath = await this.core.apis.FileApi.downloadFileForModelId(peer, modelId, '');
|
||||
const res: GetFileResponse = {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from "@/common/helper";
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from '@/common/helper';
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
@@ -16,7 +16,7 @@ export class DeleteGroupFile extends OneBotAction<Payload, any> {
|
||||
payloadSchema = SchemaData;
|
||||
async _handle(payload: Payload) {
|
||||
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||
if (!data) throw new Error('Invalid file_id');
|
||||
if (!data || !data.fileId) throw new Error('Invalid file_id');
|
||||
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -3,8 +3,9 @@ import { OB11Message } from '@/onebot';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { ChatType } from '@/core/types';
|
||||
import { MessageUnique } from '@/common/message-unique';
|
||||
import { AdapterConfigWrap } from '@/onebot/config/config';
|
||||
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
||||
|
||||
interface Response {
|
||||
messages: OB11Message[];
|
||||
@@ -23,7 +24,7 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
|
||||
actionName = ActionName.GetFriendMsgHistory;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload, adapter: string): Promise<Response> {
|
||||
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig): Promise<Response> {
|
||||
//处理参数
|
||||
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||
|
||||
@@ -42,10 +43,9 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
|
||||
await Promise.all(msgList.map(async msg => {
|
||||
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
|
||||
}));
|
||||
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
|
||||
//烘焙消息
|
||||
const ob11MsgList = (await Promise.all(
|
||||
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array')))
|
||||
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat)))
|
||||
).filter(msg => msg !== undefined);
|
||||
return { 'messages': ob11MsgList };
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ import { OB11Message } from '@/onebot';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { ChatType, Peer } from '@/core/types';
|
||||
import { MessageUnique } from '@/common/message-unique';
|
||||
import { AdapterConfigWrap } from '@/onebot/config/config';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
||||
|
||||
interface Response {
|
||||
messages: OB11Message[];
|
||||
@@ -25,7 +25,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
|
||||
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload, adapter: string): Promise<Response> {
|
||||
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig): Promise<Response> {
|
||||
//处理参数
|
||||
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
|
||||
const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
|
||||
@@ -41,11 +41,9 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
|
||||
await Promise.all(msgList.map(async msg => {
|
||||
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
|
||||
}));
|
||||
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
|
||||
//烘焙消息
|
||||
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
|
||||
const ob11MsgList = (await Promise.all(
|
||||
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, msgFormat)))
|
||||
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat)))
|
||||
).filter(msg => msg !== undefined);
|
||||
return { 'messages': ob11MsgList };
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11User> {
|
||||
actionName = ActionName.GoCQHTTP_GetStrangerInfo;
|
||||
|
||||
payloadSchema = SchemaData;
|
||||
async _handle(payload: Payload): Promise<OB11User> {
|
||||
const user_id = payload.user_id.toString();
|
||||
const extendData = await this.core.apis.UserApi.getUserDetailInfoByUin(user_id);
|
||||
|
@@ -3,8 +3,8 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { MessageUnique } from '@/common/message-unique';
|
||||
import crypto from 'crypto';
|
||||
import { AdapterConfigWrap } from '@/onebot/config/config';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||
@@ -27,9 +27,7 @@ export class GetGroupEssence extends OneBotAction<Payload, any> {
|
||||
};
|
||||
}
|
||||
|
||||
async _handle(payload: Payload, adapter: string) {
|
||||
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
|
||||
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
|
||||
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig) {
|
||||
const msglist = (await this.core.apis.WebApi.getGroupEssenceMsgAll(payload.group_id.toString())).flatMap((e) => e.data.msg_list);
|
||||
if (!msglist) {
|
||||
throw new Error('获取失败');
|
||||
@@ -50,7 +48,7 @@ export class GetGroupEssence extends OneBotAction<Payload, any> {
|
||||
operator_nick: msg.add_digest_nick,
|
||||
message_id: message_id,
|
||||
operator_time: msg.add_digest_time,
|
||||
content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage, msgFormat))?.message
|
||||
content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage, config.messagePostFormat))?.message
|
||||
};
|
||||
}
|
||||
const msgTempData = JSON.stringify({
|
||||
|
@@ -24,7 +24,7 @@ class GetGroupInfo extends OneBotAction<Payload, OB11Group> {
|
||||
group_name: data.groupName,
|
||||
member_count: data.memberNum,
|
||||
max_member_count: data.maxMemberNum,
|
||||
}
|
||||
};
|
||||
}
|
||||
return OB11Construct.group(group);
|
||||
}
|
||||
|
@@ -26,19 +26,29 @@ class GetGroupMemberInfo extends OneBotAction<Payload, OB11GroupMember> {
|
||||
return uid;
|
||||
}
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const isNocache = this.parseBoolean(payload.no_cache ?? true);
|
||||
const uid = await this.getUid(payload.user_id);
|
||||
private async getGroupMemberInfo(payload: Payload, uid: string, isNocache: boolean) {
|
||||
const groupMemberCache = this.core.apis.GroupApi.groupMemberCache.get(payload.group_id.toString());
|
||||
const groupMember = groupMemberCache?.get(uid);
|
||||
|
||||
const [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 (info) {
|
||||
Object.assign(member, info);
|
||||
} else {
|
||||
|
||||
if (!member || !groupMember) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
|
||||
|
||||
return info ? { ...groupMember, ...member, ...info } : member;
|
||||
}
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const isNocache = this.parseBoolean(payload.no_cache ?? true);
|
||||
const uid = await this.getUid(payload.user_id);
|
||||
const member = await this.getGroupMemberInfo(payload, uid, isNocache);
|
||||
|
||||
if (!member) {
|
||||
this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`);
|
||||
}
|
||||
|
||||
return OB11Construct.groupMember(payload.group_id.toString(), member);
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { OB11Construct } from '@/onebot/helper/data';
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { GroupMember } from '@/core';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||
@@ -17,25 +18,32 @@ export class GetGroupMemberList extends OneBotAction<Payload, OB11GroupMember[]>
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const groupIdStr = payload.group_id.toString();
|
||||
const noCache = payload.no_cache ? this.stringToBoolean(payload.no_cache) : false;
|
||||
const noCache = this.parseBoolean(payload.no_cache ?? false);
|
||||
const groupMembers = await this.getGroupMembers(groupIdStr, noCache);
|
||||
const _groupMembers = await Promise.all(
|
||||
Array.from(groupMembers.values()).map(item =>
|
||||
OB11Construct.groupMember(groupIdStr, item)
|
||||
)
|
||||
);
|
||||
return Array.from(new Map(_groupMembers.map(member => [member.user_id, member])).values());
|
||||
}
|
||||
|
||||
private parseBoolean(value: boolean | string): boolean {
|
||||
return typeof value === 'string' ? value === 'true' : value;
|
||||
}
|
||||
|
||||
private async getGroupMembers(groupIdStr: string, noCache: boolean): Promise<Map<string, GroupMember>> {
|
||||
const memberCache = this.core.apis.GroupApi.groupMemberCache;
|
||||
let groupMembers = memberCache.get(groupIdStr);
|
||||
|
||||
if (noCache || !groupMembers) {
|
||||
this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr).then().catch();
|
||||
//下次刷新
|
||||
groupMembers = memberCache.get(groupIdStr);
|
||||
const data = this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr, true).then().catch();
|
||||
groupMembers = memberCache.get(groupIdStr) || (await data);
|
||||
if (!groupMembers) {
|
||||
throw new Error(`Failed to get group member list for group ${groupIdStr}`);
|
||||
}
|
||||
}
|
||||
const memberPromises = Array.from(groupMembers.values()).map(item =>
|
||||
OB11Construct.groupMember(groupIdStr, item)
|
||||
);
|
||||
const _groupMembers = await Promise.all(memberPromises);
|
||||
const MemberMap = new Map(_groupMembers.map(member => [member.user_id, member]));
|
||||
return Array.from(MemberMap.values());
|
||||
|
||||
return groupMembers;
|
||||
}
|
||||
stringToBoolean(str: string | boolean): boolean {
|
||||
return typeof str === 'boolean' ? str : str.toLowerCase() === "true";
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,6 +14,6 @@ export class GroupPoke extends GetPacketStatusDepends<Payload, any> {
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.user_id, +payload.group_id);
|
||||
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
|
||||
}
|
||||
}
|
||||
|
@@ -21,15 +21,9 @@ export class SendGroupAiRecord extends GetPacketStatusDepends<Payload, {
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const rawRsp = await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
|
||||
const url = await this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+payload.group_id, rawRsp.msgInfoBody[0].index);
|
||||
const { path, errMsg, success } = (await uriToLocalFile(this.core.NapCatTempPath, url));
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
const peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() } as Peer;
|
||||
const element = await this.core.apis.FileApi.createValidSendPttElement(path);
|
||||
const sendRes = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [element], [path]);
|
||||
return { message_id: sendRes.id ?? -1 };
|
||||
await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
|
||||
return {
|
||||
message_id: 0 // can't get message_id from GetAiVoice
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -19,12 +19,11 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
|
||||
const flag = payload.flag.toString();
|
||||
const approve = payload.approve?.toString() !== 'false';
|
||||
const reason = payload.reason ?? ' ';
|
||||
|
||||
const notify = await this.findNotify(flag);
|
||||
const 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,
|
||||
|
@@ -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>
|
||||
|
@@ -3,8 +3,8 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { MessageUnique } from '@/common/message-unique';
|
||||
import { RawMessage } from '@/core';
|
||||
import { AdapterConfigWrap } from '@/onebot/config/config';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
||||
|
||||
export type ReturnDataType = OB11Message
|
||||
|
||||
@@ -18,10 +18,8 @@ class GetMsg extends OneBotAction<Payload, OB11Message> {
|
||||
actionName = ActionName.GetMsg;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload, adapter: string) {
|
||||
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig) {
|
||||
// log("history msg ids", Object.keys(msgHistory));
|
||||
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
|
||||
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
|
||||
if (!payload.message_id) {
|
||||
throw Error('参数message_id不能为空');
|
||||
}
|
||||
@@ -38,7 +36,7 @@ class GetMsg extends OneBotAction<Payload, OB11Message> {
|
||||
} else {
|
||||
msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0];
|
||||
}
|
||||
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, msgFormat);
|
||||
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat);
|
||||
if (!retMsg) throw Error('消息为空');
|
||||
try {
|
||||
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user