Compare commits

..

7 Commits

Author SHA1 Message Date
手瓜一十雪
05043f8dfc fix: code 2024-12-03 21:41:53 +08:00
手瓜一十雪
dc2b45fa00 fix: 优化处理 2024-12-03 21:36:55 +08:00
手瓜一十雪
cc70fc766a fix 2024-12-03 21:14:18 +08:00
手瓜一十雪
d2e9db5571 fix: 临时的抽象方案 2024-12-03 20:55:24 +08:00
手瓜一十雪
8e01638a36 fix: error 2024-12-03 19:50:47 +08:00
手瓜一十雪
e8b8eae8a9 refactor: GroupAdminChange 2024-12-03 19:44:38 +08:00
手瓜一十雪
0f0275243b feat: 迁移事件解析原理 2024-12-03 19:28:51 +08:00
123 changed files with 1169 additions and 2986 deletions

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto) ![Logo](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto)
</div> </div>
@@ -30,25 +30,11 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
[Cloudflare.Pages](https://napneko.pages.dev/) [Cloudflare.Pages](https://napneko.pages.dev/)
[Server.Other](https://docs.napcat.cyou/) [Server.Other](https://napcat.cyou/)
[Qbot.News](https://neko.qbot.news)
## 回家旅途 ## 回家旅途
[QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq) [QQ Group](https://qm.qq.com/q/I6LU87a0Yq)
[QQ Group#2](https://qm.qq.com/q/uqh4I87KoM)
[Telegram](https://t.me/MelodicMoonlight)
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
## 性能设计/协议标准
NapCat 已实现90+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
由此设计带来一系列好处在开发中获取群员列表通常小于50Ms单条文本消息发送在320Ms以内在1k+的群聊流畅运行同时带来一些副作用上报数据中大量使用Magic生成字段消息Id无法持久无法上报撤回消息原始内容。
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
## 感谢他们 ## 感谢他们
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权

Binary file not shown.

BIN
external/logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

View File

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

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 335 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { OneBotConfig } from '../../../src/onebot/config/config'; import { OneBotConfig } from '../../../src/onebot/config/config';
import { ResponseCode } from '../../../src/webui/src/const/status';
export class QQLoginManager { export class QQLoginManager {
private retCredential: string; private retCredential: string;
private readonly apiPrefix: string; private readonly apiPrefix: string;
@@ -22,8 +22,8 @@ export class QQLoginManager {
}); });
if (ConfigResponse.status == 200) { if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json(); const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == ResponseCode.Success) { if (ConfigResponseJson.code == 0) {
return ConfigResponseJson.data; return ConfigResponseJson?.data as OneBotConfig;
} }
} }
} catch (error) { } catch (error) {

View File

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

View File

@@ -1,20 +1,34 @@
<template> <template>
<t-card class="layout" :bordered="false"> <t-card class="layout">
<div class="login-container"> <div class="login-container">
<h2 class="sotheby-font">QQ Login</h2> <h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods"> <div class="login-methods">
<t-tooltip content="快速登录"> <t-tooltip content="快速登录">
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }" <t-button
@click="loginMethod = 'quick'">Quick Login</t-button> id="quick-login"
class="login-method"
:class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'"
>Quick Login</t-button
>
</t-tooltip> </t-tooltip>
<t-tooltip content="二维码登录"> <t-tooltip content="二维码登录">
<t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }" <t-button
@click="loginMethod = 'qrcode'">QR Code</t-button> id="qrcode-login"
class="login-method"
:class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'"
>QR Code</t-button
>
</t-tooltip> </t-tooltip>
</div> </div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form"> <div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account" <t-select
@change="selectAccount"> 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-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select> </t-select>
</div> </div>
@@ -27,7 +41,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { ref, onMounted } from 'vue';
import * as QRCode from 'qrcode'; import * as QRCode from 'qrcode';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next'; import { MessagePlugin } from 'tdesign-vue-next';
@@ -41,7 +55,6 @@ const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || ''); const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
let heartBeatTimer: number | null = null; let heartBeatTimer: number | null = null;
let qrcodeUrl: string = ''; let qrcodeUrl: string = '';
const selectAccount = async (accountName: string): Promise<void> => { const selectAccount = async (accountName: string): Promise<void> => {
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName); const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
if (result) { if (result) {
@@ -75,6 +88,10 @@ const HeartBeat = async (): Promise<void> => {
if (heartBeatTimer) { if (heartBeatTimer) {
clearInterval(heartBeatTimer); clearInterval(heartBeatTimer);
} }
// //判断是否已经调转
// if (router.currentRoute.value.path !== '/dashboard/basic-info') {
// return;
// }
await MessagePlugin.success('登录成功即将跳转'); await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' }); await router.push({ path: '/dashboard/basic-info' });
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) { } else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
@@ -86,38 +103,19 @@ const HeartBeat = async (): Promise<void> => {
const InitPages = async (): Promise<void> => { const InitPages = async (): Promise<void> => {
quickLoginList.value = await qqLoginManager.getQQQuickLoginList(); quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
qrcodeUrl = await qqLoginManager.getQQLoginQrcode(); qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
await nextTick();
generateQrCode(qrcodeUrl, qrcodeCanvas.value); generateQrCode(qrcodeUrl, qrcodeCanvas.value);
heartBeatTimer = window.setInterval(HeartBeat, 3000);
}; };
onMounted(() => { onMounted(() => {
InitPages().then().catch((err) => { InitPages();
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> </script>
<style scoped> <style scoped>
.layout { .layout {
height: 100vh; height: 100vh;
} }
.login-container { .login-container {
padding: 20px; padding: 20px;
border-radius: 5px; border-radius: 5px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-switch v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="URL"> <t-form-item label="URL">
<t-input v-model="config.url" /> <t-input v-model="config.url" />
@@ -11,20 +11,20 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item> </t-form-item>
<t-form-item label="报告自身消息"> <t-form-item label="报告自身消息">
<t-switch v-model="config.reportSelfMessage" /> <t-checkbox v-model="config.reportSelfMessage" />
</t-form-item> </t-form-item>
<t-form-item label="Token"> <t-form-item label="Token">
<t-input v-model="config.token" /> <t-input v-model="config.token" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-switch v-model="config.debug" /> <t-checkbox v-model="config.debug" />
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { defineProps, ref, watch } from 'vue';
import { HttpClientConfig } from '../../../../src/onebot/config/config'; import { HttpClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-switch v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="端口"> <t-form-item label="端口">
<t-input v-model.number="config.port" type="number" /> <t-input v-model.number="config.port" type="number" />
@@ -11,10 +11,10 @@
<t-input v-model="config.host" type="text" /> <t-input v-model="config.host" type="text" />
</t-form-item> </t-form-item>
<t-form-item label="启用 CORS"> <t-form-item label="启用 CORS">
<t-switch v-model="config.enableCors" /> <t-checkbox v-model="config.enableCors" />
</t-form-item> </t-form-item>
<t-form-item label="启用 WS"> <t-form-item label="启用 WS">
<t-switch v-model="config.enableWebsocket" /> <t-checkbox v-model="config.enableWebsocket" />
</t-form-item> </t-form-item>
<t-form-item label="消息格式"> <t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
@@ -23,14 +23,14 @@
<t-input v-model="config.token" type="text" /> <t-input v-model="config.token" type="text" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-switch v-model="config.debug" /> <t-checkbox v-model="config.debug" />
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { defineProps, ref, watch } from 'vue';
import { HttpServerConfig } from '../../../../src/onebot/config/config'; import { HttpServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-switch v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="URL"> <t-form-item label="URL">
<t-input v-model="config.url" /> <t-input v-model="config.url" />
@@ -11,13 +11,13 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item> </t-form-item>
<t-form-item label="报告自身消息"> <t-form-item label="报告自身消息">
<t-switch v-model="config.reportSelfMessage" /> <t-checkbox v-model="config.reportSelfMessage" />
</t-form-item> </t-form-item>
<t-form-item label="Token"> <t-form-item label="Token">
<t-input v-model="config.token" /> <t-input v-model="config.token" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-switch v-model="config.debug" /> <t-checkbox v-model="config.debug" />
</t-form-item> </t-form-item>
<t-form-item label="心跳间隔"> <t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" /> <t-input v-model.number="config.heartInterval" type="number" />
@@ -27,7 +27,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { defineProps, ref, watch } from 'vue';
import { WebsocketClientConfig } from '../../../../src/onebot/config/config'; import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-switch v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="主机"> <t-form-item label="主机">
<t-input v-model="config.host" /> <t-input v-model="config.host" />
@@ -14,16 +14,16 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item> </t-form-item>
<t-form-item label="上报自身消息"> <t-form-item label="上报自身消息">
<t-switch v-model="config.reportSelfMessage" /> <t-checkbox v-model="config.reportSelfMessage" />
</t-form-item> </t-form-item>
<t-form-item label="Token"> <t-form-item label="Token">
<t-input v-model="config.token" /> <t-input v-model="config.token" />
</t-form-item> </t-form-item>
<t-form-item label="强制推送事件"> <t-form-item label="强制推送事件">
<t-switch v-model="config.enableForcePushEvent" /> <t-checkbox v-model="config.enableForcePushEvent" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-switch v-model="config.debug" /> <t-checkbox v-model="config.debug" />
</t-form-item> </t-form-item>
<t-form-item label="心跳间隔"> <t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" /> <t-input v-model.number="config.heartInterval" type="number" />
@@ -33,7 +33,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { defineProps, ref, watch } from 'vue';
import { WebsocketServerConfig } from '../../../../src/onebot/config/config'; import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -7,8 +7,6 @@ import NetWork from '../pages/NetWork.vue';
import QQLogin from '../components/QQLogin.vue'; import QQLogin from '../components/QQLogin.vue';
import WebUiLogin from '../components/WebUiLogin.vue'; import WebUiLogin from '../components/WebUiLogin.vue';
import OtherConfig from '../pages/OtherConfig.vue'; import OtherConfig from '../pages/OtherConfig.vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ path: '/', redirect: '/webui' }, { path: '/', redirect: '/webui' },
@@ -28,27 +26,7 @@ const routes: Array<RouteRecordRaw> = [
}, },
]; ];
const router = createRouter({ export const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes, 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;

View File

@@ -3,15 +3,22 @@
"target": "ESNext", "target": "ESNext",
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "vue", "jsxImportSource": "vue",
"lib": ["DOM", "DOM.Iterable", "ES2022"], "lib": [
"DOM",
"DOM.Iterable"
],
"baseUrl": ".", "baseUrl": ".",
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": [
"src/*"
]
}, },
"resolveJsonModule": true, "resolveJsonModule": true,
"types": ["vite/client"], "types": [
"vite/client"
],
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@@ -23,5 +30,5 @@
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules"], "exclude": ["node_modules"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{"path": "./tsconfig.node.json"}]
} }

View File

@@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "4.2.41", "version": "4.2.12",
"scripts": { "scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1", "build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1", "build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -13,21 +13,18 @@
"dev:shell": "vite build --mode shell", "dev:shell": "vite build --mode shell",
"dev:webui": "cd napcat.webui && npm run webui:dev", "dev:webui": "cd napcat.webui && npm run webui:dev",
"lint": "eslint --fix src/**/*.{js,ts,vue}", "lint": "eslint --fix src/**/*.{js,ts,vue}",
"depend": "cd dist && npm install --omit=dev", "depend": "cd dist && npm install --omit=dev"
"dev:depend": "npm i && cd napcat.webui && npm i"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "0.24.0",
"@babel/preset-typescript": "^7.24.7", "@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.2", "@eslint/compat": "^1.2.2",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
"@log4js-node/log4js-api": "^1.0.2", "@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.4", "@napneko/nap-proto-core": "^0.0.4",
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@sinclair/typebox": "^0.34.9",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^22.0.1", "@types/node": "^22.0.1",
@@ -51,7 +48,8 @@
"vite": "^6.0.1", "vite": "^6.0.1",
"vite-plugin-cp": "^4.0.8", "vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0", "vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0" "winston": "^3.17.0",
"@sinclair/typebox": "^0.34.9"
}, },
"dependencies": { "dependencies": {
"express": "^5.0.0", "express": "^5.0.0",

View File

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

View File

@@ -1 +1 @@
export const napCatVersion = '4.2.41'; export const napCatVersion = '4.2.12';

View File

@@ -6,6 +6,7 @@ import {
Peer, Peer,
PicElement, PicElement,
PicSubType, PicSubType,
PicType,
RawMessage, RawMessage,
SendFileElement, SendFileElement,
SendPicElement, SendPicElement,
@@ -16,7 +17,7 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import fsPromises from 'fs/promises'; import fsPromises from 'fs/promises';
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core'; import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
import { fileTypeFromFile } from 'file-type'; import * as fileType from 'file-type';
import imageSize from 'image-size'; import imageSize from 'image-size';
import { ISizeCalculationResult } from 'image-size/dist/types/interface'; import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { RkeyManager } from '@/core/helper/rkey'; import { RkeyManager } from '@/core/helper/rkey';
@@ -61,7 +62,7 @@ export class NTQQFileApi {
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
const fileMd5 = await calculateFileMD5(filePath); const fileMd5 = await calculateFileMD5(filePath);
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => ''); const extOrEmpty = (await fileType.fileTypeFromFile(filePath))?.ext;
const ext = extOrEmpty ? `.${extOrEmpty}` : ''; const ext = extOrEmpty ? `.${extOrEmpty}` : '';
let fileName = `${path.basename(filePath)}`; let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf('.') === -1) { if (fileName.indexOf('.') === -1) {
@@ -157,7 +158,7 @@ export class NTQQFileApi {
let fileExt = 'mp4'; let fileExt = 'mp4';
try { try {
const tempExt = (await fileTypeFromFile(filePath))?.ext; const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
if (tempExt) fileExt = tempExt; if (tempExt) fileExt = tempExt;
} catch (e) { } catch (e) {
this.context.logger.logError('获取文件类型失败', e); this.context.logger.logError('获取文件类型失败', e);

View File

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

View File

@@ -1,5 +1,6 @@
import { import {
GeneralCallResult, GeneralCallResult,
Group,
GroupMember, GroupMember,
NTGroupMemberRole, NTGroupMemberRole,
NTGroupRequestOperateTypes, NTGroupRequestOperateTypes,
@@ -7,8 +8,6 @@ import {
KickMemberV2Req, KickMemberV2Req,
MemberExtSourceType, MemberExtSourceType,
NapCatCore, NapCatCore,
GroupNotify,
GroupInfoSource,
} from '@/core'; } from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper'; import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique'; import { LimitedHashTable } from '@/common/message-unique';
@@ -17,35 +16,35 @@ import { NTEventWrapper } from '@/common/event';
export class NTQQGroupApi { export class NTQQGroupApi {
context: InstanceContext; context: InstanceContext;
core: NapCatCore; core: NapCatCore;
groupCache: Map<string, Group> = new Map<string, Group>();
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>(); groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
groups: Group[] = [];
essenceLRU = new LimitedHashTable<number, string>(1000); essenceLRU = new LimitedHashTable<number, string>(1000);
session: any;
constructor(context: InstanceContext, core: NapCatCore) { constructor(context: InstanceContext, core: NapCatCore) {
this.context = context; this.context = context;
this.core = core; this.core = core;
} }
async fetchGroupDetail(groupCode: string) {
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupDetailInfo',
'NodeIKernelGroupListener/onGroupDetailInfoChange',
[groupCode, GroupInfoSource.KDATACARD],
(ret) => ret.result === 0,
(detailInfo) => detailInfo.groupCode === groupCode,
1,
5000
);
return detailInfo;
}
async initApi() { async initApi() {
this.initCache().then().catch(e => this.context.logger.logError(e)); this.initCache().then().catch(e => this.context.logger.logError(e));
} }
async initCache() { async initCache() {
for (const group of await this.getGroups(true)) { this.groups = await this.getGroups();
this.refreshGroupMemberCache(group.groupCode).then().catch(); for (const group of this.groups) {
this.groupCache.set(group.groupCode, group);
this.refreshGroupMemberCache(group.groupCode).then().catch(e => this.context.logger.logError(e));
} }
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
// process.pid 调试点
}
async getCoreAndBaseInfo(uids: string[]) {
return await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo',
'nodeStore',
uids,
);
} }
async fetchGroupEssenceList(groupCode: string) { async fetchGroupEssenceList(groupCode: string) {
@@ -63,15 +62,15 @@ export class NTQQGroupApi {
return (await data)[1]; return (await data)[1];
} }
async clearGroupNotifiesUnreadCount(doubt: boolean) { async clearGroupNotifiesUnreadCount(uk: boolean) {
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(doubt); return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
} }
async setGroupAvatar(groupCode: string, filePath: string) { async setGroupAvatar(gc: string, filePath: string) {
return this.context.session.getGroupService().setHeader(groupCode, filePath); return this.context.session.getGroupService().setHeader(gc, filePath);
} }
async getGroups(forced: boolean = false) { async getGroups(forced = false) {
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2( const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupList', 'NodeIKernelGroupService/getGroupList',
'NodeIKernelGroupListener/onGroupListUpdate', 'NodeIKernelGroupListener/onGroupListUpdate',
@@ -80,9 +79,9 @@ export class NTQQGroupApi {
return groupList; return groupList;
} }
async getGroupExtFE0Info(groupCodes: Array<string>, forced = true) { async getGroupExtFE0Info(groupCode: string[], forced = true) {
return this.context.session.getGroupService().getGroupExt0xEF0Info( return this.context.session.getGroupService().getGroupExt0xEF0Info(
groupCodes, groupCode,
[], [],
{ {
bindGuildId: 1, bindGuildId: 1,
@@ -122,6 +121,24 @@ export class NTQQGroupApi {
); );
} }
async getGroup(groupCode: string, forced = false) {
let group = this.groupCache.get(groupCode.toString());
if (!group) {
try {
const groupList = await this.getGroups(forced);
if (groupList.length) {
groupList.forEach(g => {
this.groupCache.set(g.groupCode, g);
});
}
} catch (e) {
return undefined;
}
}
group = this.groupCache.get(groupCode.toString());
return group;
}
async getGroupMemberAll(groupCode: string, forced = false) { async getGroupMemberAll(groupCode: string, forced = false) {
return this.context.session.getGroupService().getAllMemberList(groupCode, forced); return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
} }
@@ -129,35 +146,56 @@ export class NTQQGroupApi {
async refreshGroupMemberCache(groupCode: string) { async refreshGroupMemberCache(groupCode: string) {
try { try {
const members = await this.getGroupMemberAll(groupCode, true); const members = await this.getGroupMemberAll(groupCode, true);
this.groupMemberCache.set(groupCode, members.result.infos); // 首先填入基础信息
} catch (e) { const existingMembers = this.groupMemberCache.get(groupCode) ?? new Map<string, GroupMember>();
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`); members.result.infos.forEach((value, key) => {
existingMembers.set(value.uid, { ...value, ...existingMembers.get(value.uid) });
});
// 后台补全复杂信息
let event = (async () => {
let data = (await Promise.allSettled(members.result.ids.map(e => this.core.apis.UserApi.getUserDetailInfo(e.uid)))).filter(e => e.status === 'fulfilled').map(e => e.value);
data.forEach(e => {
const existingMember = members.result.infos.get(e.uid);
if (existingMember) {
members.result.infos.set(e.uid, { ...existingMember, ...e });
}
});
this.groupMemberCache.set(groupCode, members.result.infos);
})().then().catch(e => this.context.logger.logError(e));
// 处理首次空缺
if (!this.groupMemberCache.get(groupCode)) {
await event;
}
} catch (e) {
this.context.logger.logError(`刷新群成员缓存失败, ${e}`);
} }
return this.groupMemberCache;
} }
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString(); const groupCodeStr = groupCode.toString();
const memberUinOrUidStr = memberUinOrUid.toString(); const memberUinOrUidStr = memberUinOrUid.toString();
// 获取群成员缓存 // 检查群缓存
let members = this.groupMemberCache.get(groupCodeStr); let members = this.groupMemberCache.get(groupCodeStr);
if (!members) { if (!members) {
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr); await this.refreshGroupMemberCache(groupCodeStr);
} }
const getMember = () => { function getMember() {
let member: GroupMember | undefined;
if (isNumeric(memberUinOrUidStr)) { if (isNumeric(memberUinOrUidStr)) {
return Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr); member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr);
} else { } else {
return members!.get(memberUinOrUidStr); member = members!.get(memberUinOrUidStr);
} }
}; return member;
}
let member = getMember(); let member = getMember();
// 如果缓存中不存在该成员,尝试刷新缓存 // 不存在群友缓存 尝试刷新
if (!member) { if (!member) {
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr); await this.refreshGroupMemberCache(groupCode.toString());
member = getMember(); member = getMember();
} }
return member; return member;
@@ -167,26 +205,26 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode); return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode);
} }
async creatGroupFileFolder(groupCode: string, folderName: string) { async CreatGroupFileFolder(groupCode: string, folderName: string) {
return this.context.session.getRichMediaService().createGroupFolder(groupCode, folderName); return this.context.session.getRichMediaService().createGroupFolder(groupCode, folderName);
} }
async delGroupFile(groupCode: string, files: Array<string>) { async DelGroupFile(groupCode: string, files: string[]) {
return this.context.session.getRichMediaService().deleteGroupFile(groupCode, [102], files); return this.context.session.getRichMediaService().deleteGroupFile(groupCode, [102], files);
} }
async delGroupFileFolder(groupCode: string, folderId: string) { async DelGroupFileFolder(groupCode: string, folderId: string) {
return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId); return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
} }
async addGroupEssence(groupCode: string, msgId: string) { async addGroupEssence(GroupCode: string, msgId: string) {
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({ const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2, chatType: 2,
guildId: '', guildId: '',
peerUid: groupCode, peerUid: GroupCode,
}, msgId, 1, false); }, msgId, 1, false);
const param = { const param = {
groupCode: groupCode, groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq), msgSeq: parseInt(MsgData.msgList[0].msgSeq),
}; };
@@ -197,9 +235,9 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().kickMemberV2(param); return this.context.session.getGroupService().kickMemberV2(param);
} }
async deleteGroupBulletin(groupCode: string, noticeId: string) { async deleteGroupBulletin(GroupCode: string, noticeId: string) {
const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().deleteGroupBulletin(groupCode, psKey, noticeId); return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, psKey, noticeId);
} }
async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) { async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) {
@@ -210,37 +248,37 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().quitGroupV2(param); return this.context.session.getGroupService().quitGroupV2(param);
} }
async removeGroupEssenceBySeq(groupCode: string, msgRandom: string, msgSeq: string) { async removeGroupEssenceBySeq(GroupCode: string, msgRandom: string, msgSeq: string) {
const param = { const param = {
groupCode: groupCode, groupCode: GroupCode,
msgRandom: parseInt(msgRandom), msgRandom: parseInt(msgRandom),
msgSeq: parseInt(msgSeq), msgSeq: parseInt(msgSeq),
}; };
return this.context.session.getGroupService().removeGroupEssence(param); return this.context.session.getGroupService().removeGroupEssence(param);
} }
async removeGroupEssence(groupCode: string, msgId: string) { async removeGroupEssence(GroupCode: string, msgId: string) {
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({ const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2, chatType: 2,
guildId: '', guildId: '',
peerUid: groupCode, peerUid: GroupCode,
}, msgId, 1, false); }, msgId, 1, false);
const param = { const param = {
groupCode: groupCode, groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq), msgSeq: parseInt(MsgData.msgList[0].msgSeq),
}; };
return this.context.session.getGroupService().removeGroupEssence(param); return this.context.session.getGroupService().removeGroupEssence(param);
} }
async getSingleScreenNotifies(doubt: boolean, count: number) { async getSingleScreenNotifies(doubt: boolean, num: number) {
const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2( const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getSingleScreenNotifies', 'NodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupListener/onGroupSingleScreenNotifies', 'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
[ [
doubt, doubt,
'', '',
count, num,
], ],
); );
return notifies; return notifies;
@@ -264,89 +302,95 @@ export class NTQQGroupApi {
return ret.groupInfos.find(g => g.groupCode === groupCode); return ret.groupInfos.find(g => g.groupCode === groupCode);
} }
async getGroupMemberEx(groupCode: string, uid: string, forced: boolean = false, retry: number = 2) { async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) {
const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => { const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => {
return eventWrapper.callNormalEventV2( return eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getMemberInfo', 'NodeIKernelGroupService/getMemberInfo',
'NodeIKernelGroupListener/onMemberInfoChange', 'NodeIKernelGroupListener/onMemberInfoChange',
[groupCode, [uid], forced], [GroupCode, [uid], forced],
(ret) => ret.result === 0, (ret) => ret.result === 0,
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid), (params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
1, 1,
forced ? 2500 : 250 forced ? 2500 : 250
); );
}, this.core.eventWrapper, groupCode, uid, forced); }, this.core.eventWrapper, GroupCode, uid, forced);
if (data && data[3] instanceof Map && data[3].has(uid)) { if (data && data[3] instanceof Map && data[3].has(uid)) {
return data[3].get(uid); return data[3].get(uid);
} }
if (retry > 0) { if (retry > 0) {
const trydata = await this.getGroupMemberEx(groupCode, uid, true, retry - 1) as GroupMember | undefined; const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined;
if (trydata) return trydata; if (trydata) return trydata;
} }
return undefined; return undefined;
} }
async getGroupFileCount(groupCodes: Array<string>) { async getGroupFileCount(group_ids: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(groupCodes); return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids);
} }
async getArkJsonGroupShare(groupCode: string) { async getArkJsonGroupShare(GroupCode: string) {
const ret = await this.core.eventWrapper.callNoListenerEvent( const ret = await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelGroupService/getGroupRecommendContactArkJson', 'NodeIKernelGroupService/getGroupRecommendContactArkJson',
groupCode, GroupCode,
) as GeneralCallResult & { arkJson: string }; ) as GeneralCallResult & { arkJson: string };
return ret.arkJson; return ret.arkJson;
} }
async uploadGroupBulletinPic(groupCode: string, imageurl: string) { //需要异常处理
async uploadGroupBulletinPic(GroupCode: string, imageurl: string) {
const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().uploadGroupBulletinPic(groupCode, _Pskey, imageurl); return this.context.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl);
} }
async handleGroupRequest(notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) { async handleGroupRequest(flag: string, operateType: NTGroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|');
const groupCode = flagitem[0];
const seq = flagitem[1];
const type = parseInt(flagitem[2]);
return this.context.session.getGroupService().operateSysNotify( return this.context.session.getGroupService().operateSysNotify(
false, false,
{ {
operateType: operateType, operateType: operateType,
targetMsg: { targetMsg: {
seq: notify.seq, // 通知序列号 seq: seq, // 通知序列号
type: notify.type, type: type,
groupCode: notify.group.groupCode, groupCode: groupCode,
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格 postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
}, },
}); });
} }
async quitGroup(groupCode: string) { async quitGroup(groupQQ: string) {
return this.context.session.getGroupService().quitGroup(groupCode); return this.context.session.getGroupService().quitGroup(groupQQ);
} }
async kickMember(groupCode: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
return this.context.session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason); return this.context.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason);
} }
async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) { async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 // timeStamp为秒数, 0为解除禁言
return this.context.session.getGroupService().setMemberShutUp(groupCode, memList); return this.context.session.getGroupService().setMemberShutUp(groupQQ, memList);
} }
async banGroup(groupCode: string, shutUp: boolean) { async banGroup(groupQQ: string, shutUp: boolean) {
return this.context.session.getGroupService().setGroupShutUp(groupCode, shutUp); return this.context.session.getGroupService().setGroupShutUp(groupQQ, shutUp);
} }
async setMemberCard(groupCode: string, memberUid: string, cardName: string) { async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return this.context.session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName); return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName);
} }
async setMemberRole(groupCode: string, memberUid: string, role: NTGroupMemberRole) { async setMemberRole(groupQQ: string, memberUid: string, role: NTGroupMemberRole) {
return this.context.session.getGroupService().modifyMemberRole(groupCode, memberUid, role); return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role);
} }
async setGroupName(groupCode: string, groupName: string) { async setGroupName(groupQQ: string, groupName: string) {
return this.context.session.getGroupService().modifyGroupName(groupCode, groupName, false); return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false);
} }
async publishGroupBulletin(groupCode: string, content: string, picInfo: { async publishGroupBulletin(groupQQ: string, content: string, picInfo: {
id: string, id: string,
width: number, width: number,
height: number height: number
@@ -360,11 +404,11 @@ export class NTQQGroupApi {
pinned: pinned, pinned: pinned,
confirmRequired: confirmRequired, confirmRequired: confirmRequired,
}; };
return this.context.session.getGroupService().publishGroupBulletin(groupCode, psKey!, data); return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data);
} }
async getGroupRemainAtTimes(groupCode: string) { async getGroupRemainAtTimes(GroupCode: string) {
return this.context.session.getGroupService().getGroupRemainAtTimes(groupCode); return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
} }
async getMemberExtInfo(groupCode: string, uin: string) { async getMemberExtInfo(groupCode: string, uin: string) {

25
src/core/apis/sign.ts Normal file
View File

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

View File

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

View File

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

View File

@@ -98,65 +98,5 @@
"6.9.61-29927": { "6.9.61-29927": {
"appid": 537255836, "appid": 537255836,
"qua": "V1_MAC_NQ_6.9.61_29927_GW_B" "qua": "V1_MAC_NQ_6.9.61_29927_GW_B"
},
"9.9.17-30366": {
"appid": 537258389,
"qua": "V1_WIN_NQ_9.9.17_30366_GW_B"
},
"3.2.15-30366": {
"appid": 537258413,
"qua": "V1_LNX_NQ_3.2.15_30366_GW_B"
},
"6.9.62-30366": {
"appid": 537258401,
"qua": "V1_MAC_NQ_6.9.62_30366_GW_B"
},
"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"
} }
} }

View File

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

View File

@@ -0,0 +1,49 @@
// 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);
}

View File

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

View File

@@ -1,138 +0,0 @@
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();

View File

@@ -1,4 +1,4 @@
import { DataSource, Group, GroupDetailInfo, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types'; import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types';
export class NodeIKernelGroupListener { export class NodeIKernelGroupListener {
onGroupListInited(listEmpty: boolean): any { } onGroupListInited(listEmpty: boolean): any { }
@@ -28,7 +28,7 @@ export class NodeIKernelGroupListener {
onGroupConfMemberChange(...args: unknown[]): any { onGroupConfMemberChange(...args: unknown[]): any {
} }
onGroupDetailInfoChange(detailInfo: GroupDetailInfo): any { onGroupDetailInfoChange(...args: unknown[]): any {
} }
onGroupExtListUpdate(...args: unknown[]): any { onGroupExtListUpdate(...args: unknown[]): any {

View File

@@ -23,9 +23,7 @@ export class PacketClientSession {
get operation() { get operation() {
return this.context.operation; return this.context.operation;
} }
get client() {
return this.context.client;
}
// TODO: global message element adapter (? // TODO: global message element adapter (?
get msgConverter() { get msgConverter() {
return this.context.msgConverter; return this.context.msgConverter;

View File

@@ -3,7 +3,6 @@ export interface MiniAppReqCustomParams {
desc: string; desc: string;
picUrl: string; picUrl: string;
jumpUrl: string; jumpUrl: string;
webUrl?: string;
} }
export interface MiniAppReqTemplateParams { export interface MiniAppReqTemplateParams {

View File

@@ -124,7 +124,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
} }
get isGroupReply(): boolean { get isGroupReply(): boolean {
return this.messageClientSeq === 0; return this.messageClientSeq !== 0;
} }
buildElement(): NapProtoEncodeStructType<typeof Elem>[] { buildElement(): NapProtoEncodeStructType<typeof Elem>[] {

View File

@@ -30,7 +30,7 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAda
shareType: req.shareType, shareType: req.shareType,
versionId: req.versionId, versionId: req.versionId,
withShareTicket: req.withShareTicket, withShareTicket: req.withShareTicket,
webURL: req.webUrl ?? "", webURL: "",
appidRich: Buffer.alloc(0), appidRich: Buffer.alloc(0),
template: { template: {
templateId: "", templateId: "",

View File

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

View File

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

View File

@@ -149,7 +149,7 @@ export interface NodeIKernelGroupService {
getGroupExtList(force: boolean): Promise<GeneralCallResult>; getGroupExtList(force: boolean): Promise<GeneralCallResult>;
getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<GeneralCallResult>; getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<unknown>;
getMemberExtInfo(param: GroupExtParam): Promise<unknown>;//req getMemberExtInfo(param: GroupExtParam): Promise<unknown>;//req
@@ -187,13 +187,13 @@ export interface NodeIKernelGroupService {
destroyGroup(groupCode: string): void; destroyGroup(groupCode: string): void;
getSingleScreenNotifies(doubt: boolean, startSeq: string, count: number): Promise<GeneralCallResult>; getSingleScreenNotifies(doubted: boolean, start_seq: string, num: number): Promise<GeneralCallResult>;
clearGroupNotifies(groupCode: string): void; clearGroupNotifies(groupCode: string): void;
getGroupNotifiesUnreadCount(doubt: boolean): Promise<GeneralCallResult>; getGroupNotifiesUnreadCount(unknown: boolean): Promise<GeneralCallResult>;
clearGroupNotifiesUnreadCount(doubt: boolean): void; clearGroupNotifiesUnreadCount(unknown: boolean): void;
operateSysNotify( operateSysNotify(
doubt: boolean, doubt: boolean,

View File

@@ -4,7 +4,6 @@ import { GeneralCallResult } from '@/core/services/common';
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/core/types/msg'; import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/core/types/msg';
export interface NodeIKernelMsgService { export interface NodeIKernelMsgService {
buildMultiForwardMsg(req: { srcMsgIds: Array<string>, srcContact: Peer }): Promise<GeneralCallResult & { rspInfo: { elements: unknown } }>;
generateMsgUniqueId(chatType: number, time: string): string; generateMsgUniqueId(chatType: number, time: string): string;

View File

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

View File

@@ -16,7 +16,7 @@ export interface NodeIKernelSearchService {
penetrate: string penetrate: string
}): Promise<GeneralCallResult>;// needs 1 arguments }): Promise<GeneralCallResult>;// needs 1 arguments
searchLocalInfo(keywords: string, type: number/*4*/): unknown; searchLocalInfo(keywords: string, unknown: number/*4*/): unknown;
cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments

View File

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

View File

@@ -17,160 +17,7 @@ export enum GroupInfoSource {
KRECENTCONTACT, KRECENTCONTACT,
KMOREPANEL KMOREPANEL
} }
export interface GroupDetailInfo {
groupCode: string;
groupUin: string;
ownerUid: string;
ownerUin: string;
groupFlag: number;
groupFlagExt: number;
maxMemberNum: number;
memberNum: number;
groupOption: number;
classExt: number;
groupName: string;
fingerMemo: string;
groupQuestion: string;
certType: number;
richFingerMemo: string;
tagRecord: any[];
shutUpAllTimestamp: number;
shutUpMeTimestamp: number;
groupTypeFlag: number;
privilegeFlag: number;
groupSecLevel: number;
groupFlagExt3: number;
isConfGroup: number;
isModifyConfGroupFace: number;
isModifyConfGroupName: number;
groupFlagExt4: number;
groupMemo: string;
cmdUinMsgSeq: number;
cmdUinJoinTime: number;
cmdUinUinFlag: number;
cmdUinMsgMask: number;
groupSecLevelInfo: number;
cmdUinPrivilege: number;
cmdUinFlagEx2: number;
appealDeadline: number;
remarkName: string;
isTop: boolean;
groupFace: number;
groupGeoInfo: {
ownerUid: string;
SetTime: number;
CityId: number;
Longitude: string;
Latitude: string;
GeoContent: string;
poiId: string;
};
certificationText: string;
cmdUinRingtoneId: number;
longGroupName: string;
autoAgreeJoinGroupUserNumForConfGroup: number;
autoAgreeJoinGroupUserNumForNormalGroup: number;
cmdUinFlagExt3Grocery: number;
groupCardPrefix: {
introduction: string;
rptPrefix: any[];
};
groupExt: {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: string;
lightCharNum: number;
luckyWord: string;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: string;
groupOwnerId: {
memberUin: string;
memberUid: string;
memberQid: string;
};
essentialMsgPrivilege: number;
msgEventSeq: string;
inviteRobotSwitch: number;
gangUpId: string;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: string;
groupBindGuildIds: {
guildIds: any[];
};
viewedMsgDisappearTime: string;
groupExtFlameData: {
switchState: number;
state: number;
dayNums: any[];
version: number;
updateTime: string;
isDisplayDayNum: boolean;
};
groupBindGuildSwitch: number;
groupAioBindGuildId: string;
groupExcludeGuildIds: {
guildIds: any[];
};
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: string;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
};
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
isAllowRecallMsg: number;
confUin: string;
confMaxMsgSeq: number;
confToGroupTime: number;
groupSchoolInfo: {
location: string;
grade: number;
school: string;
};
activeMemberNum: number;
groupGrade: number;
groupCreateTime: number;
subscriptionUin: string;
subscriptionUid: string;
noFingerOpenFlag: number;
noCodeFingerOpenFlag: number;
isGroupFreeze: number;
allianceId: string;
groupExtOnly: {
tribeId: number;
moneyForAddGroup: number;
};
isAllowConfGroupMemberModifyGroupName: number;
isAllowConfGroupMemberNick: number;
isAllowConfGroupMemberAtAll: number;
groupClassText: string;
groupFreezeReason: number;
headPortraitSeq: number;
groupHeadPortrait: {
portraitCnt: number;
portraitInfo: any[];
defaultId: number;
verifyingPortraitCnt: number;
verifyingPortraitInfo: any[];
};
cmdUinJoinMsgSeq: number;
cmdUinJoinRealMsgSeq: number;
groupAnswer: string;
groupAdminMaxNum: number;
inviteNoAuthNumLimit: string;
hlGuildOrgId: number;
isAllowHlGuildBinary: number;
localExitGroupReason: number;
}
export interface GroupExt0xEF0InfoFilter { export interface GroupExt0xEF0InfoFilter {
bindGuildId: number; bindGuildId: number;
blacklistExpireTime: number; blacklistExpireTime: number;

View File

@@ -508,7 +508,7 @@ export interface RawMessage {
*/ */
export interface QueryMsgsParams { export interface QueryMsgsParams {
chatInfo: Peer; chatInfo: Peer;
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>; filterMsgType: [];
filterSendersUid: string[]; filterSendersUid: string[];
filterMsgFromTime: string; filterMsgFromTime: string;
filterMsgToTime: string; filterMsgToTime: string;

View File

@@ -1,5 +1,5 @@
//LiteLoader需要提供部分IPC接口以便于其他插件调用 //LiteLoader需要提供部分IPC接口以便于其他插件调用
const { ipcMain, BrowserWindow } = require('electron'); const { ipcMain } = require('electron');
const napcat = require('./napcat.cjs'); const napcat = require('./napcat.cjs');
const { shell } = require('electron'); const { shell } = require('electron');
ipcMain.handle('napcat_get_webtoken', async (event, arg) => { ipcMain.handle('napcat_get_webtoken', async (event, arg) => {
@@ -14,13 +14,3 @@ ipcMain.handle('napcat_get_reactweb', async (event, arg) => {
let token = url.searchParams.get('token'); let token = url.searchParams.get('token');
return `https://napcat.152710.xyz/web_login?back=http://127.0.0.1:${port}&token=${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)
})
});

View File

@@ -6,9 +6,6 @@ const napcat = {
openExternalUrl: async (url) => { openExternalUrl: async (url) => {
ipcRenderer.send('open_external_url', url); ipcRenderer.send('open_external_url', url);
}, },
openInnerUrl: async (url) => {
ipcRenderer.send('napcat_open_inner_url', url);
},
getWebUiUrlReact: async () => { getWebUiUrlReact: async () => {
return ipcRenderer.invoke('napcat_get_reactweb'); return ipcRenderer.invoke('napcat_get_reactweb');
} }

View File

@@ -24,7 +24,7 @@ export const onSettingWindowCreated = async (view) => {
`; `;
view.querySelector('.nc_openwebui').addEventListener('click', () => { view.querySelector('.nc_openwebui').addEventListener('click', () => {
window.napcat.openInnerUrl(webui); window.open(webui, '_blank');
}); });
view.querySelector('.nc_openwebui_ex').addEventListener('click', () => { view.querySelector('.nc_openwebui_ex').addEventListener('click', () => {
window.napcat.openExternalUrl(webui); window.napcat.openExternalUrl(webui);

View File

@@ -29,7 +29,7 @@ export class OB11Response {
} }
export abstract class OneBotAction<PayloadType, ReturnDataType> { export abstract class OneBotAction<PayloadType, ReturnDataType> {
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown; actionName: ActionName = ActionName.Unknown;
core: NapCatCore; core: NapCatCore;
private validate: ValidateFunction<any> | undefined = undefined; private validate: ValidateFunction<any> | undefined = undefined;
payloadSchema: any = undefined; payloadSchema: any = undefined;

View File

@@ -1,14 +0,0 @@
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 };
}
}

View File

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

View File

@@ -11,8 +11,7 @@ const SchemaData = Type.Union([
desc: Type.String(), desc: Type.String(),
picUrl: Type.String(), picUrl: Type.String(),
jumpUrl: Type.String(), jumpUrl: Type.String(),
webUrl: Type.Optional(Type.String()), rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
rawArkData: Type.Optional(Type.Union([Type.String()]))
}), }),
Type.Object({ Type.Object({
title: Type.String(), title: Type.String(),
@@ -20,7 +19,6 @@ const SchemaData = Type.Union([
picUrl: Type.String(), picUrl: Type.String(),
jumpUrl: Type.String(), jumpUrl: Type.String(),
iconUrl: Type.String(), iconUrl: Type.String(),
webUrl: Type.Optional(Type.String()),
appId: Type.String(), appId: Type.String(),
scene: Type.Union([Type.Number(), Type.String()]), scene: Type.Union([Type.Number(), Type.String()]),
templateType: Type.Union([Type.Number(), Type.String()]), templateType: Type.Union([Type.Number(), Type.String()]),
@@ -30,7 +28,7 @@ const SchemaData = Type.Union([
versionId: Type.String(), versionId: Type.String(),
sdkId: Type.String(), sdkId: Type.String(),
withShareTicket: Type.Union([Type.Number(), Type.String()]), withShareTicket: Type.Union([Type.Number(), Type.String()]),
rawArkData: Type.Optional(Type.Union([Type.String()])) rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
}) })
]); ]);
type Payload = Static<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
@@ -47,8 +45,7 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
title: payload.title, title: payload.title,
desc: payload.desc, desc: payload.desc,
picUrl: payload.picUrl, picUrl: payload.picUrl,
jumpUrl: payload.jumpUrl, jumpUrl: payload.jumpUrl
webUrl: payload.webUrl,
} as MiniAppReqCustomParams; } as MiniAppReqCustomParams;
if ('type' in payload) { if ('type' in payload) {
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template); reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
@@ -66,13 +63,13 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
verType: +verType, verType: +verType,
shareType: +shareType, shareType: +shareType,
versionId: versionId, versionId: versionId,
withShareTicket: +withShareTicket, withShareTicket: +withShareTicket
} }
); );
} }
const arkData = await this.core.apis.PacketApi.pkt.operation.GetMiniAppAdaptShareInfo(reqParam); const arkData = await this.core.apis.PacketApi.pkt.operation.GetMiniAppAdaptShareInfo(reqParam);
return { return {
data: payload.rawArkData === 'true' ? arkData : MiniAppInfoHelper.RawToSend(arkData) data: payload.rawArkData ? arkData : MiniAppInfoHelper.RawToSend(arkData)
}; };
} }
} }

View File

@@ -1,6 +1,6 @@
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { checkFileExist, uriToLocalFile } from '@/common/file'; import { checkFileExist, uri2local } from '@/common/file';
import fs from 'fs'; import fs from 'fs';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
@@ -10,11 +10,12 @@ const SchemaData = Type.Object({
type Payload = Static<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
class OCRImageBase extends OneBotAction<Payload, any> { export class OCRImage extends OneBotAction<Payload, any> {
actionName = ActionName.OCRImage;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image)); const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.image));
if (!success) { if (!success) {
throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`); throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`);
} }
@@ -33,10 +34,6 @@ class OCRImageBase extends OneBotAction<Payload, any> {
} }
} }
export class OCRImage extends OCRImageBase { export class IOCRImage extends OCRImage {
actionName = ActionName.OCRImage;
}
export class IOCRImage extends OCRImageBase {
actionName = ActionName.IOCRImage; actionName = ActionName.IOCRImage;
} }

View File

@@ -1,21 +0,0 @@
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
cmd: Type.String(),
data: Type.String(),
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true }),
});
type Payload = Static<typeof SchemaData>;
export class SendPacket extends GetPacketStatusDepends<Payload, any> {
payloadSchema = SchemaData;
actionName = ActionName.SendPacket;
async _handle(payload: Payload) {
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
const data = await this.core.apis.PacketApi.pkt.client.sendOidbPacket({ cmd: payload.cmd, data: payload.data as any }, rsp);
return typeof data === 'object' ? data.toString('hex') : undefined;
}
}

View File

@@ -8,18 +8,14 @@ const SchemaData = Type.Object({
type Payload = Static<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
class SetGroupSignBase extends GetPacketStatusDepends<Payload, any> { export class SetGroupSign extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SetGroupSign;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id); return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id);
} }
} }
export class SendGroupSign extends SetGroupSign {
export class SetGroupSign extends SetGroupSignBase {
actionName = ActionName.SendGroupSign;
}
export class SendGroupSign extends SetGroupSignBase {
actionName = ActionName.SendGroupSign; actionName = ActionName.SendGroupSign;
} }

View File

@@ -1,7 +1,7 @@
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import { checkFileExist, uriToLocalFile } from '@/common/file'; import { checkFileExist, uri2local } from '@/common/file';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({ const SchemaData = Type.Object({
@@ -14,7 +14,7 @@ export default class SetAvatar extends OneBotAction<Payload, null> {
actionName = ActionName.SetQQAvatar; actionName = ActionName.SetQQAvatar;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file)); const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file));
if (!success) { if (!success) {
throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`); throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`);
} }

View File

@@ -13,6 +13,6 @@ export class CreateGroupFileFolder extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_CreateGroupFileFolder; actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem; return (await this.core.apis.GroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
} }
} }

View File

@@ -17,6 +17,6 @@ export class DeleteGroupFile extends OneBotAction<Payload, any> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id); const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (!data) throw new Error('Invalid file_id'); if (!data) throw new Error('Invalid file_id');
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]); return await this.core.apis.GroupApi.DelGroupFile(payload.group_id.toString(), [data.fileId]);
} }
} }

View File

@@ -14,7 +14,7 @@ export class DeleteGroupFileFolder extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DeleteGroupFileFolder; actionName = ActionName.GoCQHTTP_DeleteGroupFileFolder;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.delGroupFileFolder( return (await this.core.apis.GroupApi.DelGroupFileFolder(
payload.group_id.toString(), payload.folder ?? payload.folder_id ?? '')).groupFileCommonResult; payload.group_id.toString(), payload.folder ?? payload.folder_id ?? '')).groupFileCommonResult;
} }
} }

View File

@@ -1,4 +1,4 @@
import { checkFileExist, uriToLocalFile } from '@/common/file'; import { checkFileExist, uri2local } from '@/common/file';
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { unlink } from 'node:fs/promises'; import { unlink } from 'node:fs/promises';
@@ -28,7 +28,7 @@ export class SendGroupNotice extends OneBotAction<Payload, null> {
const { const {
path, path,
success, success,
} = (await uriToLocalFile(this.core.NapCatTempPath, payload.image)); } = (await uri2local(this.core.NapCatTempPath, payload.image));
if (!success) { if (!success) {
throw new Error(`群公告${payload.image}设置失败,image字段可能格式不正确`); throw new Error(`群公告${payload.image}设置失败,image字段可能格式不正确`);
} }

View File

@@ -1,6 +1,6 @@
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { checkFileExistV2, uriToLocalFile } from '@/common/file'; import { checkFileExistV2, uri2local } from '@/common/file';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
const SchemaData = Type.Object({ const SchemaData = Type.Object({
@@ -15,7 +15,7 @@ export default class SetGroupPortrait extends OneBotAction<Payload, any> {
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<any> { async _handle(payload: Payload): Promise<any> {
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file)); const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file));
if (!success) { if (!success) {
throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`); throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`);
} }

View File

@@ -2,7 +2,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { ChatType, Peer } from '@/core/types'; import { ChatType, Peer } from '@/core/types';
import fs from 'fs'; import fs from 'fs';
import { uriToLocalFile } from '@/common/file'; import { uri2local } from '@/common/file';
import { SendMessageContext } from '@/onebot/api'; import { SendMessageContext } from '@/onebot/api';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
@@ -25,7 +25,7 @@ export default class GoCQHTTPUploadGroupFile extends OneBotAction<Payload, null>
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
file = `file://${file}`; file = `file://${file}`;
} }
const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, file); const downloadResult = await uri2local(this.core.NapCatTempPath, file);
const peer: Peer = { const peer: Peer = {
chatType: ChatType.KCHATTYPEGROUP, chatType: ChatType.KCHATTYPEGROUP,
peerUid: payload.group_id.toString(), peerUid: payload.group_id.toString(),

View File

@@ -2,7 +2,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { ChatType, Peer, SendFileElement } from '@/core/types'; import { ChatType, Peer, SendFileElement } from '@/core/types';
import fs from 'fs'; import fs from 'fs';
import { uriToLocalFile } from '@/common/file'; import { uri2local } from '@/common/file';
import { SendMessageContext } from '@/onebot/api'; import { SendMessageContext } from '@/onebot/api';
import { ContextMode, createContext } from '@/onebot/action/msg/SendMsg'; import { ContextMode, createContext } from '@/onebot/action/msg/SendMsg';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
@@ -36,7 +36,7 @@ export default class GoCQHTTPUploadPrivateFile extends OneBotAction<Payload, nul
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
file = `file://${file}`; file = `file://${file}`;
} }
const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, file); const downloadResult = await uri2local(this.core.NapCatTempPath, file);
if (!downloadResult.success) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg); throw new Error(downloadResult.errMsg);
} }

View File

@@ -1,44 +1,26 @@
import { GroupNotifyMsgStatus } from '@/core'; import { GroupNotifyMsgStatus } from '@/core';
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { Notify } from '@/onebot/types'; export class GetGroupIgnoredNotifies extends OneBotAction<void, any> {
interface RetData {
InvitedRequest: Notify[];
join_requests: Notify[];
}
export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
actionName = ActionName.GetGroupIgnoredNotifies; actionName = ActionName.GetGroupIgnoredNotifies;
async _handle(): Promise<RetData> { async _handle(payload: void) {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50); const ignoredNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 10);
const retData: RetData = { InvitedRequest: [], join_requests: [] }; const retData: any = {
join_requests: await Promise.all(
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => { ignoredNotifies
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0; .filter(notify => notify.type === 7)
const actorUin = SSNotify.user2?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2.uid) : 0; .map(async SSNotify => ({
const commonData = { request_id: SSNotify.seq,
request_id: +SSNotify.seq, requester_uin: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1?.uid),
invitor_uin: invitorUin, requester_nick: SSNotify.user1?.nickName,
invitor_nick: SSNotify.user1?.nickName, group_id: SSNotify.group?.groupCode,
group_id: +SSNotify.group?.groupCode,
message: SSNotify?.postscript,
group_name: SSNotify.group?.groupName, group_name: SSNotify.group?.groupName,
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE, checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
actor: actorUin, actor: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2?.uid) || 0,
requester_nick: SSNotify.user1?.nickName, }))),
}; };
if (SSNotify.type === 1) {
retData.InvitedRequest.push(commonData);
} else if (SSNotify.type === 7) {
retData.join_requests.push(commonData);
}
});
await Promise.all(notifyPromises);
return retData; return retData;
} }
} }

View File

@@ -17,13 +17,14 @@ class GetGroupInfo extends OneBotAction<Payload, OB11Group> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const group = (await this.core.apis.GroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString()); const group = (await this.core.apis.GroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString());
if (!group) { if (!group) {
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString()); const data = await this.core.apis.GroupApi.searchGroup(payload.group_id.toString());
if (!data) throw new Error('Group not found');
return { return {
...data, ...data.searchGroupInfo,
group_id: +payload.group_id, group_id: +payload.group_id,
group_name: data.groupName, group_name: data.searchGroupInfo.groupName,
member_count: data.memberNum, member_count: data.searchGroupInfo.memberNum,
max_member_count: data.maxMemberNum, max_member_count: data.searchGroupInfo.maxMemberNum,
}; };
} }
return OB11Construct.group(group); return OB11Construct.group(group);

View File

@@ -29,14 +29,13 @@ class GetGroupMemberInfo extends OneBotAction<Payload, OB11GroupMember> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const isNocache = this.parseBoolean(payload.no_cache ?? true); const isNocache = this.parseBoolean(payload.no_cache ?? true);
const uid = await this.getUid(payload.user_id); const uid = await this.getUid(payload.user_id);
const groupMember = this.core.apis.GroupApi.groupMemberCache.get(payload.group_id.toString())?.get(uid); const [member, info] = await Promise.all([
let [member, info] = await Promise.all([
this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, isNocache), this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, isNocache),
this.core.apis.UserApi.getUserDetailInfo(uid), this.core.apis.UserApi.getUserDetailInfo(uid),
]); ]);
if (!member || !groupMember) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`); if (!member) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
if (info) { if (info) {
member = { ...groupMember, ...member, ...info }; Object.assign(member, info);
} else { } else {
this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`); this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`);
} }

View File

@@ -21,8 +21,7 @@ export class GetGroupMemberList extends OneBotAction<Payload, OB11GroupMember[]>
const memberCache = this.core.apis.GroupApi.groupMemberCache; const memberCache = this.core.apis.GroupApi.groupMemberCache;
let groupMembers = memberCache.get(groupIdStr); let groupMembers = memberCache.get(groupIdStr);
if (noCache || !groupMembers) { if (noCache || !groupMembers) {
this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr).then().catch(); await this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr);
//下次刷新
groupMembers = memberCache.get(groupIdStr); groupMembers = memberCache.get(groupIdStr);
if (!groupMembers) { if (!groupMembers) {
throw new Error(`Failed to get group member list for group ${groupIdStr}`); throw new Error(`Failed to get group member list for group ${groupIdStr}`);

View File

@@ -1,6 +1,6 @@
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { uriToLocalFile } from "@/common/file"; import { uri2local } from "@/common/file";
import { ChatType, Peer } from "@/core"; import { ChatType, Peer } from "@/core";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
@@ -23,7 +23,7 @@ export class SendGroupAiRecord extends GetPacketStatusDepends<Payload, {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const rawRsp = await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound); 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 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)); const { path, errMsg, success } = (await uri2local(this.core.NapCatTempPath, url));
if (!success) { if (!success) {
throw new Error(errMsg); throw new Error(errMsg);
} }

View File

@@ -1,9 +1,9 @@
import { ContextMode, SendMsgBase } from '@/onebot/action/msg/SendMsg'; import SendMsg, { ContextMode } from '@/onebot/action/msg/SendMsg';
import { ActionName, BaseCheckResult } from '@/onebot/action/router'; import { ActionName, BaseCheckResult } from '@/onebot/action/router';
import { OB11PostSendMsg } from '@/onebot/types'; import { OB11PostSendMsg } from '@/onebot/types';
// 未检测参数 // 未检测参数
class SendGroupMsg extends SendMsgBase { class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg; actionName = ActionName.SendGroupMsg;
contextMode: ContextMode = ContextMode.Group; contextMode: ContextMode = ContextMode.Group;

View File

@@ -4,9 +4,9 @@ import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({ const SchemaData = Type.Object({
flag: Type.Union([Type.String(), Type.Number()]), flag: Type.String(),
approve: Type.Optional(Type.Union([Type.Boolean(), Type.String()])), approve: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
reason: Type.Optional(Type.Union([Type.String({ default: ' ' }), Type.Null()])), reason: Type.Union([Type.String({ default: ' ' }), Type.Null()]),
}); });
type Payload = Static<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
@@ -18,25 +18,10 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
const flag = payload.flag.toString(); const flag = payload.flag.toString();
const approve = payload.approve?.toString() !== 'false'; const approve = payload.approve?.toString() !== 'false';
const reason = payload.reason ?? ' '; await this.core.apis.GroupApi.handleGroupRequest(flag,
let invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag);
const notify = invite_notify ?? await this.findNotify(flag);
if (!notify) {
throw new Error('No such request');
}
await this.core.apis.GroupApi.handleGroupRequest(
notify,
approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE, approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
reason, payload.reason ?? ' ',
); );
return null; return null;
} }
private async findNotify(flag: string) {
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
if (!notify) {
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag);
}
return notify;
}
} }

View File

@@ -18,7 +18,7 @@ export default class SetGroupBan extends OneBotAction<Payload, null> {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('uid error'); if (!uid) throw new Error('uid error');
await this.core.apis.GroupApi.banMember(payload.group_id.toString(), await this.core.apis.GroupApi.banMember(payload.group_id.toString(),
[{ uid: uid, timeStamp: +payload.duration }]); [{ uid: uid, timeStamp: +payload.duration}]);
return null; return null;
} }
} }

View File

@@ -102,10 +102,11 @@ import { SendGroupAiRecord } from "@/onebot/action/group/SendGroupAiRecord";
import { GetAiCharacters } from "@/onebot/action/extends/GetAiCharacters"; import { GetAiCharacters } from "@/onebot/action/extends/GetAiCharacters";
import { GetGuildList } from './guild/GetGuildList'; import { GetGuildList } from './guild/GetGuildList';
import { GetGuildProfile } from './guild/GetGuildProfile'; import { GetGuildProfile } from './guild/GetGuildProfile';
import { GetClientkey } from './extends/GetClientkey';
import { SendPacket } from './extends/SendPacket';
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
export type ActionMap = Map<string, OneBotAction<any, any>>;
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore): ActionMap {
const actionHandlers = [ const actionHandlers = [
new GetGroupInfoEx(obContext, core), new GetGroupInfoEx(obContext, core),
@@ -125,7 +126,6 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetGroupRootFiles(obContext, core), new GetGroupRootFiles(obContext, core),
new SetGroupSign(obContext, core), new SetGroupSign(obContext, core),
new SendGroupSign(obContext, core), new SendGroupSign(obContext, core),
new GetClientkey(obContext, core),
// onebot11 // onebot11
new SendLike(obContext, core), new SendLike(obContext, core),
new GetMsg(obContext, core), new GetMsg(obContext, core),
@@ -219,32 +219,13 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetAiRecord(obContext, core), new GetAiRecord(obContext, core),
new SendGroupAiRecord(obContext, core), new SendGroupAiRecord(obContext, core),
new GetAiCharacters(obContext, core), new GetAiCharacters(obContext, core),
new SendPacket(obContext, core),
]; ];
const actionMap = new Map();
type HandlerUnion = typeof actionHandlers[number]; for (const action of actionHandlers) {
type MapType = { actionMap.set(action.actionName, action);
[H in HandlerUnion as H['actionName']]: H; actionMap.set(action.actionName + '_async', action);
} & { actionMap.set(action.actionName + '_rate_limited', action);
[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 { get }; return actionMap;
} }
export type ActionMap = ReturnType<typeof createActionMap>

View File

@@ -88,7 +88,8 @@ function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType
return 0; return 0;
} }
export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> { export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg;
contextMode = ContextMode.Normal; contextMode = ContextMode.Normal;
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
@@ -378,6 +379,4 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
export default class SendMsg extends SendMsgBase { export default SendMsg;
actionName = ActionName.SendMsg;
}

View File

@@ -1,9 +1,9 @@
import { ContextMode, SendMsgBase } from './SendMsg'; import SendMsg, { ContextMode } from './SendMsg';
import { ActionName, BaseCheckResult } from '@/onebot/action/router'; import { ActionName, BaseCheckResult } from '@/onebot/action/router';
import { OB11PostSendMsg } from '@/onebot/types'; import { OB11PostSendMsg } from '@/onebot/types';
// 未检测参数 // 未检测参数
class SendPrivateMsg extends SendMsgBase { class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg; actionName = ActionName.SendPrivateMsg;
contextMode: ContextMode = ContextMode.Private; contextMode: ContextMode = ContextMode.Private;

View File

@@ -3,6 +3,8 @@ import { ActionName, BaseCheckResult } from '@/onebot/action/router';
export abstract class GetPacketStatusDepends<PT, RT> extends OneBotAction<PT, RT> { export abstract class GetPacketStatusDepends<PT, RT> extends OneBotAction<PT, RT> {
actionName = ActionName.GetPacketStatus;
protected async check(payload: PT): Promise<BaseCheckResult>{ protected async check(payload: PT): Promise<BaseCheckResult>{
if (!this.core.apis.PacketApi.available) { if (!this.core.apis.PacketApi.available) {
return { return {
@@ -16,8 +18,6 @@ export abstract class GetPacketStatusDepends<PT, RT> extends OneBotAction<PT, RT
} }
export class GetPacketStatus extends GetPacketStatusDepends<any, null> { export class GetPacketStatus extends GetPacketStatusDepends<any, null> {
actionName = ActionName.GetPacketStatus;
async _handle(payload: any) { async _handle(payload: any) {
return null; return null;
} }

View File

@@ -13,136 +13,134 @@ export interface InvalidCheckResult {
[k: string | number]: any; [k: string | number]: any;
} }
export const ActionName = { export enum ActionName {
// onebot 11 // onebot 11
SendPrivateMsg: 'send_private_msg', SendPrivateMsg = 'send_private_msg',
SendGroupMsg: 'send_group_msg', SendGroupMsg = 'send_group_msg',
SendMsg: 'send_msg', SendMsg = 'send_msg',
DeleteMsg: 'delete_msg', DeleteMsg = 'delete_msg',
GetMsg: 'get_msg', GetMsg = 'get_msg',
GoCQHTTP_GetForwardMsg: 'get_forward_msg', GoCQHTTP_GetForwardMsg = 'get_forward_msg',
SendLike: 'send_like', SendLike = 'send_like',
SetGroupKick: 'set_group_kick', SetGroupKick = 'set_group_kick',
SetGroupBan: 'set_group_ban', SetGroupBan = 'set_group_ban',
// SetGroupAnoymousBan : 'set_group_anonymous_ban', // SetGroupAnoymousBan = 'set_group_anonymous_ban',
SetGroupWholeBan: 'set_group_whole_ban', SetGroupWholeBan = 'set_group_whole_ban',
SetGroupAdmin: 'set_group_admin', SetGroupAdmin = 'set_group_admin',
// SetGroupAnoymous : 'set_group_anonymous', // SetGroupAnoymous = 'set_group_anonymous',
SetGroupCard: 'set_group_card', SetGroupCard = 'set_group_card',
SetGroupName: 'set_group_name', SetGroupName = 'set_group_name',
SetGroupLeave: 'set_group_leave', SetGroupLeave = 'set_group_leave',
SetSpecialTittle: 'set_group_special_title', SetSpecialTittle = 'set_group_special_title',
SetFriendAddRequest: 'set_friend_add_request', SetFriendAddRequest = 'set_friend_add_request',
SetGroupAddRequest: 'set_group_add_request', SetGroupAddRequest = 'set_group_add_request',
GetLoginInfo: 'get_login_info', GetLoginInfo = 'get_login_info',
GoCQHTTP_GetStrangerInfo: 'get_stranger_info', GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
GetFriendList: 'get_friend_list', GetFriendList = 'get_friend_list',
GetGroupInfo: 'get_group_info', GetGroupInfo = 'get_group_info',
GetGroupList: 'get_group_list', GetGroupList = 'get_group_list',
GetGroupMemberInfo: 'get_group_member_info', GetGroupMemberInfo = 'get_group_member_info',
GetGroupMemberList: 'get_group_member_list', GetGroupMemberList = 'get_group_member_list',
GetGroupHonorInfo: 'get_group_honor_info', GetGroupHonorInfo = 'get_group_honor_info',
GetCookies: 'get_cookies', GetCookies = 'get_cookies',
GetCSRF: 'get_csrf_token', GetCSRF = 'get_csrf_token',
GetCredentials: 'get_credentials', GetCredentials = 'get_credentials',
GetRecord: 'get_record', GetRecord = 'get_record',
GetImage: 'get_image', GetImage = 'get_image',
CanSendImage: 'can_send_image', CanSendImage = 'can_send_image',
CanSendRecord: 'can_send_record', CanSendRecord = 'can_send_record',
GetStatus: 'get_status', GetStatus = 'get_status',
GetVersionInfo: 'get_version_info', GetVersionInfo = 'get_version_info',
// Reboot : 'set_restart', // Reboot = 'set_restart',
// CleanCache : 'clean_cache', // CleanCache = 'clean_cache',
// go-cqhttp // go-cqhttp
SetQQProfile: 'set_qq_profile', SetQQProfile = 'set_qq_profile',
// QidianGetAccountInfo : 'qidian_get_account_info', // QidianGetAccountInfo = 'qidian_get_account_info',
GoCQHTTP_GetModelShow: '_get_model_show', GoCQHTTP_GetModelShow = '_get_model_show',
GoCQHTTP_SetModelShow: '_set_model_show', GoCQHTTP_SetModelShow = '_set_model_show',
GetOnlineClient: 'get_online_clients', GetOnlineClient = 'get_online_clients',
// GetUnidirectionalFriendList : 'get_unidirectional_friend_list', // GetUnidirectionalFriendList = 'get_unidirectional_friend_list',
GoCQHTTP_DeleteFriend: 'delete_friend', GoCQHTTP_DeleteFriend = 'delete_friend',
// DeleteUnidirectionalFriendList : 'delete_unidirectional_friend', // DeleteUnidirectionalFriendList = 'delete_unidirectional_friend',
GoCQHTTP_MarkMsgAsRead: 'mark_msg_as_read', GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read',
GoCQHTTP_SendGroupForwardMsg: 'send_group_forward_msg', GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg: 'send_private_forward_msg', GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
GoCQHTTP_GetGroupMsgHistory: 'get_group_msg_history', GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
OCRImage: 'ocr_image', OCRImage = 'ocr_image',
IOCRImage: '.ocr_image', IOCRImage = '.ocr_image',
GetGroupSystemMsg: 'get_group_system_msg', GetGroupSystemMsg = 'get_group_system_msg',
GoCQHTTP_GetEssenceMsg: 'get_essence_msg_list', GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list',
GoCQHTTP_GetGroupAtAllRemain: 'get_group_at_all_remain', GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
SetGroupPortrait: 'set_group_portrait', SetGroupPortrait = 'set_group_portrait',
SetEssenceMsg: 'set_essence_msg', SetEssenceMsg = 'set_essence_msg',
DelEssenceMsg: 'delete_essence_msg', DelEssenceMsg = 'delete_essence_msg',
GoCQHTTP_SendGroupNotice: '_send_group_notice', GoCQHTTP_SendGroupNotice = '_send_group_notice',
GoCQHTTP_GetGroupNotice: '_get_group_notice', GoCQHTTP_GetGroupNotice = '_get_group_notice',
GoCQHTTP_UploadGroupFile: 'upload_group_file', GoCQHTTP_UploadGroupFile = 'upload_group_file',
GOCQHTTP_DeleteGroupFile: 'delete_group_file', GOCQHTTP_DeleteGroupFile = 'delete_group_file',
GoCQHTTP_CreateGroupFileFolder: 'create_group_file_folder', GoCQHTTP_CreateGroupFileFolder = 'create_group_file_folder',
GoCQHTTP_DeleteGroupFileFolder: 'delete_group_folder', GoCQHTTP_DeleteGroupFileFolder = 'delete_group_folder',
GoCQHTTP_GetGroupFileSystemInfo: 'get_group_file_system_info', GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info',
GoCQHTTP_GetGroupRootFiles: 'get_group_root_files', GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
GoCQHTTP_GetGroupFilesByFolder: 'get_group_files_by_folder', GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder',
GOCQHTTP_GetGroupFileUrl: 'get_group_file_url', GOCQHTTP_GetGroupFileUrl = 'get_group_file_url',
GOCQHTTP_UploadPrivateFile: 'upload_private_file', GOCQHTTP_UploadPrivateFile = 'upload_private_file',
// GOCQHTTP_ReloadEventFilter : 'reload_event_filter', // GOCQHTTP_ReloadEventFilter = 'reload_event_filter',
GoCQHTTP_DownloadFile: 'download_file', GoCQHTTP_DownloadFile = 'download_file',
GoCQHTTP_CheckUrlSafely: 'check_url_safely', GoCQHTTP_CheckUrlSafely = 'check_url_safely',
GoCQHTTP_GetWordSlices: '.get_word_slices', GoCQHTTP_GetWordSlices = '.get_word_slices',
GoCQHTTP_HandleQuickAction: '.handle_quick_operation', GoCQHTTP_HandleQuickAction = '.handle_quick_operation',
// 以下为扩展napcat扩展 // 以下为扩展napcat扩展
Unknown: 'unknown', Unknown = 'unknown',
SharePeer: 'ArkSharePeer', SharePeer = 'ArkSharePeer',
ShareGroupEx: 'ArkShareGroup', ShareGroupEx = 'ArkShareGroup',
// RebootNormal : 'reboot_normal', //无快速登录重新启动 // RebootNormal = 'reboot_normal', //无快速登录重新启动
GetRobotUinRange: 'get_robot_uin_range', GetRobotUinRange = 'get_robot_uin_range',
SetOnlineStatus: 'set_online_status', SetOnlineStatus = 'set_online_status',
GetFriendsWithCategory: 'get_friends_with_category', GetFriendsWithCategory = 'get_friends_with_category',
SetQQAvatar: 'set_qq_avatar', SetQQAvatar = 'set_qq_avatar',
GetFile: 'get_file', GetFile = 'get_file',
ForwardFriendSingleMsg: 'forward_friend_single_msg', ForwardFriendSingleMsg = 'forward_friend_single_msg',
ForwardGroupSingleMsg: 'forward_group_single_msg', ForwardGroupSingleMsg = 'forward_group_single_msg',
TranslateEnWordToZn: 'translate_en2zh', TranslateEnWordToZn = 'translate_en2zh',
SetMsgEmojiLike: 'set_msg_emoji_like', SetMsgEmojiLike = 'set_msg_emoji_like',
GoCQHTTP_SendForwardMsg: 'send_forward_msg', GoCQHTTP_SendForwardMsg = 'send_forward_msg',
MarkPrivateMsgAsRead: 'mark_private_msg_as_read', MarkPrivateMsgAsRead = 'mark_private_msg_as_read',
MarkGroupMsgAsRead: 'mark_group_msg_as_read', MarkGroupMsgAsRead = 'mark_group_msg_as_read',
GetFriendMsgHistory: 'get_friend_msg_history', GetFriendMsgHistory = 'get_friend_msg_history',
CreateCollection: 'create_collection', CreateCollection = 'create_collection',
GetCollectionList: 'get_collection_list', GetCollectionList = 'get_collection_list',
SetLongNick: 'set_self_longnick', SetLongNick = 'set_self_longnick',
GetRecentContact: 'get_recent_contact', GetRecentContact = 'get_recent_contact',
_MarkAllMsgAsRead: '_mark_all_as_read', _MarkAllMsgAsRead = '_mark_all_as_read',
GetProfileLike: 'get_profile_like', GetProfileLike = 'get_profile_like',
FetchCustomFace: 'fetch_custom_face', FetchCustomFace = 'fetch_custom_face',
FetchEmojiLike: 'fetch_emoji_like', FetchEmojiLike = 'fetch_emoji_like',
SetInputStatus: 'set_input_status', SetInputStatus = 'set_input_status',
GetGroupInfoEx: 'get_group_info_ex', GetGroupInfoEx = 'get_group_info_ex',
GetGroupIgnoreAddRequest: 'get_group_ignore_add_request', GetGroupIgnoreAddRequest = 'get_group_ignore_add_request',
DelGroupNotice: '_del_group_notice', DelGroupNotice = '_del_group_notice',
FetchUserProfileLike: 'fetch_user_profile_like', FetchUserProfileLike = 'fetch_user_profile_like',
FriendPoke: 'friend_poke', FriendPoke = 'friend_poke',
GroupPoke: 'group_poke', GroupPoke = 'group_poke',
GetPacketStatus: 'nc_get_packet_status', GetPacketStatus = 'nc_get_packet_status',
GetUserStatus: 'nc_get_user_status', GetUserStatus = 'nc_get_user_status',
GetRkey: 'nc_get_rkey', GetRkey = 'nc_get_rkey',
GetGroupShutList: 'get_group_shut_list', GetGroupShutList = 'get_group_shut_list',
GetGuildList: 'get_guild_list', GetGuildList = 'get_guild_list',
GetGuildProfile: 'get_guild_service_profile', GetGuildProfile = 'get_guild_service_profile',
GetGroupIgnoredNotifies: 'get_group_ignored_notifies', GetGroupIgnoredNotifies = 'get_group_ignored_notifies',
SetGroupSign: "set_group_sign", SetGroupSign = "set_group_sign",
SendGroupSign: "send_group_sign", SendGroupSign = "send_group_sign",
SendPacket: "send_packet",
GetMiniAppArk: "get_mini_app_ark",
// UploadForwardMsg : "upload_forward_msg",
GetAiRecord: "get_ai_record",
GetAiCharacters: "get_ai_characters",
SendGroupAiRecord: "send_group_ai_record",
GetClientkey: "get_clientkey", GetMiniAppArk = "get_mini_app_ark",
} as const; // UploadForwardMsg = "upload_forward_msg",
GetAiRecord = "get_ai_record",
GetAiCharacters = "get_ai_characters",
SendGroupAiRecord = "send_group_ai_record",
}

View File

@@ -1,10 +1,10 @@
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import CanSendRecord, { CanSend } from './CanSendRecord'; import CanSendRecord from './CanSendRecord';
interface ReturnType { interface ReturnType {
yes: boolean; yes: boolean;
} }
export default class CanSendImage extends CanSend { export default class CanSendImage extends CanSendRecord {
actionName = ActionName.CanSendImage; actionName = ActionName.CanSendImage;
} }

View File

@@ -5,15 +5,12 @@ interface ReturnType {
yes: boolean; yes: boolean;
} }
export class CanSend extends OneBotAction<any, ReturnType> { export default class CanSendRecord extends OneBotAction<any, ReturnType> {
actionName = ActionName.CanSendRecord;
async _handle(_payload: void): Promise<ReturnType> { async _handle(_payload: void): Promise<ReturnType> {
return { return {
yes: true, yes: true,
}; };
} }
} }
export default class CanSendRecord extends CanSend{
actionName = ActionName.CanSendRecord;
}

View File

@@ -1,43 +1,38 @@
import { GroupNotifyMsgStatus } from '@/core'; import { GroupNotifyMsgStatus } from '@/core';
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { Notify } from '@/onebot/types'; export class GetGroupSystemMsg extends OneBotAction<void, any> {
interface RetData {
InvitedRequest: Notify[];
join_requests: Notify[];
}
export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
actionName = ActionName.GetGroupSystemMsg; actionName = ActionName.GetGroupSystemMsg;
async _handle(): Promise<RetData> { async _handle() {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50); const NTQQUserApi = this.core.apis.UserApi;
const retData: RetData = { InvitedRequest: [], join_requests: [] }; const NTQQGroupApi = this.core.apis.GroupApi;
// 默认10条 该api未完整实现 包括响应数据规范化 类型规范化
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => { const SingleScreenNotifies = await NTQQGroupApi.getSingleScreenNotifies(false,10);
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0; const retData: any = { InvitedRequest: [], join_requests: [] };
const actorUin = SSNotify.user2?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2.uid) : 0; for (const SSNotify of SingleScreenNotifies) {
const commonData = { if (SSNotify.type == 1) {
request_id: +SSNotify.seq, retData.InvitedRequest.push({
invitor_uin: invitorUin, request_id: SSNotify.seq,
invitor_uin: await NTQQUserApi.getUinByUidV2(SSNotify.user1?.uid),
invitor_nick: SSNotify.user1?.nickName, invitor_nick: SSNotify.user1?.nickName,
group_id: +SSNotify.group?.groupCode, group_id: SSNotify.group?.groupCode,
message: SSNotify?.postscript,
group_name: SSNotify.group?.groupName, group_name: SSNotify.group?.groupName,
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE, checked: SSNotify.status === GroupNotifyMsgStatus.KUNHANDLE ? false : true,
actor: actorUin, actor: await NTQQUserApi.getUinByUidV2(SSNotify.user2?.uid) || 0,
requester_nick: SSNotify.user1?.nickName,
};
if (SSNotify.type === 1) {
retData.InvitedRequest.push(commonData);
} else if (SSNotify.type === 7) {
retData.join_requests.push(commonData);
}
}); });
} else if (SSNotify.type == 7) {
await Promise.all(notifyPromises); retData.join_requests.push({
request_id: SSNotify.seq,
requester_uin: await NTQQUserApi.getUinByUidV2(SSNotify.user1?.uid),
requester_nick: SSNotify.user1?.nickName,
group_id: SSNotify.group?.groupCode,
group_name: SSNotify.group?.groupName,
checked: SSNotify.status === GroupNotifyMsgStatus.KUNHANDLE ? false : true,
actor: await NTQQUserApi.getUinByUidV2(SSNotify.user2?.uid) || 0,
});
}
}
return retData; return retData;
} }

View File

@@ -3,7 +3,7 @@ import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({ const SchemaData = Type.Object({
flag: Type.Union([Type.String(), Type.Number()]), flag: Type.String(),
approve: Type.Optional(Type.Union([Type.String(), Type.Boolean()])), approve: Type.Optional(Type.Union([Type.String(), Type.Boolean()])),
remark: Type.Optional(Type.String()) remark: Type.Optional(Type.String())
}); });
@@ -16,13 +16,14 @@ export default class SetFriendAddRequest extends OneBotAction<Payload, null> {
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
const approve = payload.approve?.toString() !== 'false'; const approve = payload.approve?.toString() !== 'false';
const notify = (await this.core.apis.FriendApi.getBuddyReq()).buddyReqs.find(e => e.reqTime == payload.flag.toString()); await this.core.apis.FriendApi.handleFriendRequest(payload.flag, approve);
if (!notify) {
throw new Error('No such request');
}
await this.core.apis.FriendApi.handleFriendRequest(notify, approve);
if (payload.remark) { if (payload.remark) {
await this.core.apis.FriendApi.setBuddyRemark(notify.friendUid, payload.remark); const data = payload.flag.split('|');
if (data.length < 2) {
throw new Error('Invalid flag');
}
const friendUid = data[0];
await this.core.apis.FriendApi.setBuddyRemark(friendUid, payload.remark);
} }
return null; return null;
} }

View File

@@ -12,17 +12,16 @@ export class OneBotFriendApi {
} }
//使用前预先判断 busiId 1061 //使用前预先判断 busiId 1061
async parsePrivatePokeEvent(grayTipElement: GrayTipElement, uin: number) { async parsePrivatePokeEvent(grayTipElement: GrayTipElement) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr); const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
const pokedetail: Array<{ uid: string }> = json.items; let pokedetail: Array<{ uid: string }> = json.items;
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid); pokedetail = pokedetail.filter(item => item.uid);
if (poke_uid.length == 2) { if (pokedetail.length == 2) {
return new OB11FriendPokeEvent( return new OB11FriendPokeEvent(
this.core, this.core,
uin, parseInt((await this.core.apis.UserApi.getUinByUidV2(pokedetail[0].uid))),
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid))), parseInt((await this.core.apis.UserApi.getUinByUidV2(pokedetail[1].uid))),
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid))),
pokedetail, pokedetail,
); );
} }

View File

@@ -1,5 +1,19 @@
import type { OneBotFriendApi } from '@/onebot/api/friend';
import type { OneBotUserApi } from '@/onebot/api/user';
import type { OneBotGroupApi } from '@/onebot/api/group';
import type { OneBotMsgApi } from '@/onebot/api/msg';
import type { OneBotQuickActionApi } from '@/onebot/api/quick-action';
export * from './friend'; export * from './friend';
export * from './group'; export * from './group';
export * from './user'; export * from './user';
export * from './msg'; export * from './msg';
export * from './quick-action'; export * from './quick-action';
export interface StableOneBotApiWrapper {
FriendApi: OneBotFriendApi;
UserApi: OneBotUserApi;
GroupApi: OneBotGroupApi;
MsgApi: OneBotMsgApi;
QuickActionApi: OneBotQuickActionApi,
}

View File

@@ -17,26 +17,23 @@ import {
SendTextElement, SendTextElement,
FaceType, FaceType,
GrayTipElement, GrayTipElement,
GroupNotify,
} from '@/core'; } from '@/core';
import faceConfig from '@/core/external/face_config.json'; import faceConfig from '@/core/external/face_config.json';
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, } from '@/onebot'; import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, } from '@/onebot';
import { OB11Construct } from '@/onebot/helper/data'; import { OB11Construct } from '@/onebot/helper/data';
import { EventType } from '@/onebot/event/OneBotEvent'; import { EventType } from '@/onebot/event/OneBotEvent';
import { encodeCQCode } from '@/onebot/helper/cqcode'; import { encodeCQCode } from '@/onebot/helper/cqcode';
import { uriToLocalFile } from '@/common/file'; import { uri2local } from '@/common/file';
import { RequestUtil } from '@/common/request'; import { RequestUtil } from '@/common/request';
import fsPromise, { constants } from 'node:fs/promises'; import fsPromise, { constants } from 'node:fs/promises';
import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
import { GroupChange, PushMsgBody } from "@/core/packet/transformer/proto";
import { NapProtoMsg } from '@napneko/nap-proto-core'; import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent'; import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent'; import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent';
import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin'; import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin';
import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent'; import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent';
import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto';
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import { LRUCache } from '@/common/lru-cache';
type RawToOb11Converters = { type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -70,8 +67,7 @@ function keyCanBeParsed(key: string, parser: RawToOb11Converters): key is keyof
export class OneBotMsgApi { export class OneBotMsgApi {
obContext: NapCatOneBot11Adapter; obContext: NapCatOneBot11Adapter;
core: NapCatCore; core: NapCatCore;
notifyGroupInvite: LRUCache<string, GroupNotify> = new LRUCache(50);
// seq -> notify
rawToOb11Converters: RawToOb11Converters = { rawToOb11Converters: RawToOb11Converters = {
textElement: async element => { textElement: async element => {
if (element.atType === NTMsgAtType.ATTYPEUNKNOWN) { if (element.atType === NTMsgAtType.ATTYPEUNKNOWN) {
@@ -157,17 +153,6 @@ export class OneBotMsgApi {
faceElement: async element => { faceElement: async element => {
const faceIndex = element.faceIndex; const faceIndex = element.faceIndex;
if (element.faceType == FaceType.Poke) {
return {
type: OB11MessageDataType.poke,
data: {
type: element?.pokeType?.toString() ?? '0',
id: faceIndex.toString(),
}
};
}
if (faceIndex === FaceIndex.DICE) { if (faceIndex === FaceIndex.DICE) {
return { return {
type: OB11MessageDataType.dice, type: OB11MessageDataType.dice,
@@ -465,7 +450,7 @@ export class OneBotMsgApi {
}, },
[OB11MessageDataType.face]: async ({ data: { id } }) => { [OB11MessageDataType.face]: async ({ data: { id } }) => {
const parsedFaceId = +id; let parsedFaceId = +id;
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface; const sysFaces = faceConfig.sysface;
const face: any = sysFaces.find((systemFace) => systemFace.QSid === parsedFaceId.toString()); const face: any = sysFaces.find((systemFace) => systemFace.QSid === parsedFaceId.toString());
@@ -529,7 +514,7 @@ export class OneBotMsgApi {
let thumb = sendMsg.data.thumb; let thumb = sendMsg.data.thumb;
if (thumb) { if (thumb) {
const uri2LocalRes = await uriToLocalFile(this.core.NapCatTempPath, thumb); const uri2LocalRes = await uri2local(this.core.NapCatTempPath, thumb);
if (uri2LocalRes.success) thumb = uri2LocalRes.path; if (uri2LocalRes.success) thumb = uri2LocalRes.path;
} }
return await this.core.apis.FileApi.createValidSendVideoElement(context, path, fileName, thumb); return await this.core.apis.FileApi.createValidSendVideoElement(context, path, fileName, thumb);
@@ -683,7 +668,7 @@ export class OneBotMsgApi {
async parsePrivateMsgEvent(msg: RawMessage, grayTipElement: GrayTipElement) { async parsePrivateMsgEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) { if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
if (grayTipElement.jsonGrayTipElement.busiId == 1061) { if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid))); const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement);
if (PokeEvent) { return PokeEvent; }; if (PokeEvent) { return PokeEvent; };
} else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') { } else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid))); return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
@@ -895,53 +880,49 @@ export class OneBotMsgApi {
if (!sendElements.length) { if (!sendElements.length) {
throw new Error('消息体无法解析, 请检查是否发送了不支持的消息类型'); throw new Error('消息体无法解析, 请检查是否发送了不支持的消息类型');
} }
let totalSize = 0;
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => { let timeout = 10000;
const sizePromises = elements.map(async element => { try {
switch (element.elementType) { for (const fileElement of sendElements) {
case ElementType.PTT: if (fileElement.elementType === ElementType.PTT) {
return (await fsPromise.stat(element.pttElement.filePath)).size; totalSize += (await fsPromise.stat(fileElement.pttElement.filePath)).size;
case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size;
default:
return 0;
} }
}); if (fileElement.elementType === ElementType.FILE) {
const sizes = await Promise.all(sizePromises); totalSize += (await fsPromise.stat(fileElement.fileElement.filePath)).size;
return sizes.reduce((total, size) => total + size, 0); }
}; if (fileElement.elementType === ElementType.VIDEO) {
totalSize += (await fsPromise.stat(fileElement.videoElement.filePath)).size;
const totalSize = await calculateTotalSize(sendElements).catch(e => { }
if (fileElement.elementType === ElementType.PIC) {
totalSize += (await fsPromise.stat(fileElement.picElement.sourcePath)).size;
}
}
//且 PredictTime ((totalSize / 1024 / 512) * 1000)不等于Nan
const PredictTime = totalSize / 1024 / 256 * 1000;
if (!Number.isNaN(PredictTime)) {
timeout += PredictTime;// 10S Basic Timeout + PredictTime( For File 512kb/s )
}
} catch (e) {
this.core.context.logger.logError('发送消息计算预计时间异常', e); this.core.context.logger.logError('发送消息计算预计时间异常', e);
return 0; }
});
const timeout = 10000 + (totalSize / 1024 / 256 * 1000);
const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, waitComplete, timeout); const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
if (!returnMsg) throw new Error('发送消息失败'); if (!returnMsg) throw new Error('发送消息失败');
returnMsg.id = MessageUnique.createUniqueMsgId({ returnMsg.id = MessageUnique.createUniqueMsgId({
chatType: peer.chatType, chatType: peer.chatType,
guildId: '', guildId: '',
peerUid: peer.peerUid, peerUid: peer.peerUid,
}, returnMsg.msgId); }, returnMsg.msgId);
setTimeout(async () => { setTimeout(() => {
const deletePromises = deleteAfterSentFiles.map(async file => { deleteAfterSentFiles.forEach(async file => {
try { try {
if (await fsPromise.access(file, constants.W_OK).then(() => true).catch(() => false)) { if (await fsPromise.access(file, constants.W_OK).then(() => true).catch(() => false)) {
await fsPromise.unlink(file); fsPromise.unlink(file).then().catch(e => this.core.context.logger.logError('发送消息删除文件失败', e));
} }
} catch (e) { } catch (error) {
this.core.context.logger.logError('发送消息删除文件失败', e); this.core.context.logger.logError('发送消息删除文件失败', (error as Error).message);
} }
}); });
await Promise.all(deletePromises);
}, 60000); }, 60000);
return returnMsg; return returnMsg;
@@ -951,7 +932,7 @@ export class OneBotMsgApi {
{ data: inputdata }: OB11MessageFileBase, { data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: SendMessageContext, { deleteAfterSentFiles }: SendMessageContext,
) { ) {
const realUri = inputdata.url ?? inputdata.file ?? inputdata.path ?? ''; const realUri = inputdata.url || inputdata.file || inputdata.path || '';
if (realUri.length === 0) { if (realUri.length === 0) {
this.core.context.logger.logError('文件消息缺少参数', inputdata); this.core.context.logger.logError('文件消息缺少参数', inputdata);
throw Error('文件消息缺少参数'); throw Error('文件消息缺少参数');
@@ -961,7 +942,7 @@ export class OneBotMsgApi {
fileName, fileName,
errMsg, errMsg,
success, success,
} = (await uriToLocalFile(this.core.NapCatTempPath, realUri)); } = (await uri2local(this.core.NapCatTempPath, realUri));
if (!success) { if (!success) {
this.core.context.logger.logError('文件下载失败', errMsg); this.core.context.logger.logError('文件下载失败', errMsg);
@@ -990,33 +971,21 @@ export class OneBotMsgApi {
if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) { if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent); const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch(); this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
const operatorUid = groupChange.operatorInfo?.toString();
return new OB11GroupIncreaseEvent( return new OB11GroupIncreaseEvent(
this.core, this.core,
groupChange.groupUin, groupChange.groupUin,
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0, groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0, groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
groupChange.decreaseType == 131 ? 'invite' : 'approve', groupChange.decreaseType == 131 ? 'invite' : 'approve',
); );
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) { } else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent); const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
const operatorUid = groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString();
if (groupChange.memberUid === this.core.selfInfo.uid) {
setTimeout(() => {
this.core.apis.GroupApi.groupMemberCache.delete(groupChange.groupUin.toString());
}, 5000);
// 自己被踢了 5S后回收
} else {
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch(); this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
}
return new OB11GroupDecreaseEvent( return new OB11GroupDecreaseEvent(
this.core, this.core,
groupChange.groupUin, groupChange.groupUin,
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0, groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0, groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
this.groupChangDecreseType2String(groupChange.decreaseType), this.groupChangDecreseType2String(groupChange.decreaseType),
); );
} else if (SysMessage.contentHead.type == 44 && SysMessage.body?.msgContent) { } else if (SysMessage.contentHead.type == 44 && SysMessage.body?.msgContent) {
@@ -1037,74 +1006,7 @@ export class OneBotMsgApi {
+await this.core.apis.UserApi.getUinByUidV2(uid), +await this.core.apis.UserApi.getUinByUidV2(uid),
enabled ? 'set' : 'unset' enabled ? 'set' : 'unset'
); );
} else if (SysMessage.contentHead.type == 87 && SysMessage.body?.msgContent) { } else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
let groupInvite = new NapProtoMsg(GroupInvite).decode(SysMessage.body.msgContent);
let request_seq = '';
try {
await this.core.eventWrapper.registerListen('NodeIKernelMsgListener/onRecvMsg', (msgs) => {
for (const msg of msgs) {
if (msg.senderUid === groupInvite.invitorUid && msg.msgType === 11) {
let jumpUrl = JSON.parse(msg.elements.find(e => e.elementType === 10)?.arkElement?.bytesData ?? '').meta?.news?.jumpUrl;
let jumpUrlParams = new URLSearchParams(jumpUrl);
let groupcode = jumpUrlParams.get('groupcode');
let receiveruin = jumpUrlParams.get('receiveruin');
let msgseq = jumpUrlParams.get('msgseq');
request_seq = msgseq ?? '';
if (groupcode === groupInvite.groupUin.toString() && receiveruin === this.core.selfInfo.uin) {
return true;
}
}
}
return false;
}, 1, 1000);
} catch (error) {
request_seq = '';
}
// 未拉取到seq
if (request_seq === '') {
return;
}
// 创建个假的
this.notifyGroupInvite.put(request_seq, {
seq: request_seq,
type: 1,
group: {
groupCode: groupInvite.groupUin.toString(),
groupName: '',
},
user1: {
uid: groupInvite.invitorUid,
nickName: '',
},
user2: {
uid: this.core.selfInfo.uid,
nickName: '',
},
actionUser: {
uid: groupInvite.invitorUid,
nickName: '',
},
actionTime: Date.now().toString(),
postscript: '',
repeatSeqs: [],
warningTips: '',
invitationExt: {
srcType: 1,
groupCode: groupInvite.groupUin.toString(),
waitStatus: 1,
},
status: 1
})
return new OB11GroupRequestEvent(
this.core,
+groupInvite.groupUin,
+await this.core.apis.UserApi.getUinByUidV2(groupInvite.invitorUid),
'invite',
'',
request_seq
);
}
else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent); return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
} }
} }

View File

@@ -18,8 +18,8 @@ import { ContextMode, createContext, normalize } from '@/onebot/action/msg/SendM
import { isNull } from '@/common/helper'; import { isNull } from '@/common/helper';
export class OneBotQuickActionApi { export class OneBotQuickActionApi {
obContext: NapCatOneBot11Adapter; private obContext: NapCatOneBot11Adapter;
core: NapCatCore; private core: NapCatCore;
constructor(obContext: NapCatOneBot11Adapter, core: NapCatCore) { constructor(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
this.obContext = obContext; this.obContext = obContext;
this.core = core; this.core = core;
@@ -82,22 +82,11 @@ export class OneBotQuickActionApi {
this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles, false).then().catch(e => this.core.context.logger.logError(e)); this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles, false).then().catch(e => this.core.context.logger.logError(e));
} }
} }
async findNotify(flag: string) {
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
if (!notify) {
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag);
}
return notify;
}
async handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) { async handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) {
if (!isNull(quickAction.approve)) {
let invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag);
const notify = invite_notify ?? await this.findNotify(request.flag);
if (!isNull(quickAction.approve) && notify) {
this.core.apis.GroupApi.handleGroupRequest( this.core.apis.GroupApi.handleGroupRequest(
notify, request.flag,
quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE, quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
quickAction.reason, quickAction.reason,
).catch(e => this.core.context.logger.logError(e)); ).catch(e => this.core.context.logger.logError(e));
@@ -105,9 +94,8 @@ export class OneBotQuickActionApi {
} }
async handleFriendRequest(request: OB11FriendRequestEvent, quickAction: QuickActionFriendRequest) { async handleFriendRequest(request: OB11FriendRequestEvent, quickAction: QuickActionFriendRequest) {
const notify = (await this.core.apis.FriendApi.getBuddyReq()).buddyReqs.find(e => e.reqTime == request.flag.toString()); if (!isNull(quickAction.approve)) {
if (!isNull(quickAction.approve) && notify) { this.core.apis.FriendApi.handleFriendRequest(request.flag, !!quickAction.approve).then().catch(e => this.core.context.logger.logError(e));
this.core.apis.FriendApi.handleFriendRequest(notify, !!quickAction.approve).then().catch(e => this.core.context.logger.logError(e));
} }
} }
} }

View File

@@ -28,7 +28,6 @@ interface v1Config {
export interface AdapterConfigInner { export interface AdapterConfigInner {
name: string; name: string;
enable: boolean; enable: boolean;
} }
export type AdapterConfigWrap = AdapterConfigInner & Partial<NetworkConfigAdapter>; export type AdapterConfigWrap = AdapterConfigInner & Partial<NetworkConfigAdapter>;
@@ -38,14 +37,6 @@ export interface AdapterConfig extends AdapterConfigInner {
const createDefaultAdapterConfig = <T extends AdapterConfig>(config: T): T => config; const createDefaultAdapterConfig = <T extends AdapterConfig>(config: T): T => config;
export interface PluginConfig extends AdapterConfig {
name: string;
enable: boolean;
messagePostFormat: string;
reportSelfMessage: boolean;
debug: boolean;
}
export const httpServerDefaultConfigs = createDefaultAdapterConfig({ export const httpServerDefaultConfigs = createDefaultAdapterConfig({
name: 'http-server', name: 'http-server',
enable: false as boolean, enable: false as boolean,
@@ -136,7 +127,7 @@ export const mergeNetworkDefaultConfig = {
websocketClients: websocketClientDefaultConfigs, websocketClients: websocketClientDefaultConfigs,
} as const; } as const;
export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | PluginConfig; export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig;
type NetworkConfigKeys = keyof typeof mergeNetworkDefaultConfig; type NetworkConfigKeys = keyof typeof mergeNetworkDefaultConfig;
export function mergeOneBotConfigs( export function mergeOneBotConfigs(

View File

@@ -10,14 +10,12 @@ class OB11PokeEvent extends OB11BaseNoticeEvent {
export class OB11FriendPokeEvent extends OB11PokeEvent { export class OB11FriendPokeEvent extends OB11PokeEvent {
raw_info: any; raw_info: any;
sender_id: number;
//raw_message nb等框架标准为string //raw_message nb等框架标准为string
constructor(core: NapCatCore, user_id: number, sender_id: number, target_id: number, raw_message: any) { constructor(core: NapCatCore, user_id: number, target_id: number, raw_message: any) {
super(core); super(core);
this.target_id = target_id; this.target_id = target_id;
this.user_id = user_id; this.user_id = user_id;
this.sender_id = sender_id;
this.raw_info = raw_message; this.raw_info = raw_message;
} }
} }

View File

@@ -16,6 +16,7 @@ import {
} from '@/core'; } from '@/core';
import { OB11ConfigLoader } from '@/onebot/config'; import { OB11ConfigLoader } from '@/onebot/config';
import { import {
IOB11NetworkAdapter,
OB11ActiveHttpAdapter, OB11ActiveHttpAdapter,
OB11ActiveWebSocketAdapter, OB11ActiveWebSocketAdapter,
OB11NetworkManager, OB11NetworkManager,
@@ -30,6 +31,7 @@ import {
OneBotMsgApi, OneBotMsgApi,
OneBotQuickActionApi, OneBotQuickActionApi,
OneBotUserApi, OneBotUserApi,
StableOneBotApiWrapper,
} from '@/onebot/api'; } from '@/onebot/api';
import { ActionMap, createActionMap } from '@/onebot/action'; import { ActionMap, createActionMap } from '@/onebot/action';
import { WebUiDataRuntime } from '@/webui/src/helper/Data'; import { WebUiDataRuntime } from '@/webui/src/helper/Data';
@@ -43,16 +45,8 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal
import { LRUCache } from '@/common/lru-cache'; import { LRUCache } from '@/common/lru-cache';
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener'; import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
import { BotOfflineEvent } from './event/notice/BotOfflineEvent'; import { BotOfflineEvent } from './event/notice/BotOfflineEvent';
import { import { AdapterConfigWrap, mergeOneBotConfigs, migrateOneBotConfigsV1, NetworkConfigAdapter, OneBotConfig } from './config/config';
AdapterConfigWrap,
mergeOneBotConfigs,
migrateOneBotConfigsV1,
NetworkConfigAdapter,
OneBotConfig,
} from './config/config';
import { OB11Message } from './types'; import { OB11Message } from './types';
import { OB11PluginAdapter } from './network/plugin';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
//OneBot实现类 //OneBot实现类
export class NapCatOneBot11Adapter { export class NapCatOneBot11Adapter {
@@ -60,7 +54,7 @@ export class NapCatOneBot11Adapter {
readonly context: InstanceContext; readonly context: InstanceContext;
configLoader: OB11ConfigLoader; configLoader: OB11ConfigLoader;
public readonly apis; public readonly apis: StableOneBotApiWrapper;
networkManager: OB11NetworkManager; networkManager: OB11NetworkManager;
actions: ActionMap; actions: ActionMap;
private readonly bootTime = Date.now() / 1000; private readonly bootTime = Date.now() / 1000;
@@ -77,7 +71,7 @@ export class NapCatOneBot11Adapter {
UserApi: new OneBotUserApi(this, core), UserApi: new OneBotUserApi(this, core),
FriendApi: new OneBotFriendApi(this, core), FriendApi: new OneBotFriendApi(this, core),
MsgApi: new OneBotMsgApi(this, core), MsgApi: new OneBotMsgApi(this, core),
QuickActionApi: new OneBotQuickActionApi(this, core) QuickActionApi: new OneBotQuickActionApi(this, core),
} as const; } as const;
this.actions = createActionMap(this, core); this.actions = createActionMap(this, core);
this.networkManager = new OB11NetworkManager(); this.networkManager = new OB11NetworkManager();
@@ -112,16 +106,11 @@ export class NapCatOneBot11Adapter {
const serviceInfo = await this.creatOneBotLog(ob11Config); const serviceInfo = await this.creatOneBotLog(ob11Config);
this.context.logger.log(`[Notice] [OneBot11] ${serviceInfo}`); this.context.logger.log(`[Notice] [OneBot11] ${serviceInfo}`);
//创建NetWork服务 // //创建NetWork服务
// 注册Plugin 如果需要基于NapCat进行快速开发
// this.networkManager.registerAdapter(
// new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
// );
for (const key of ob11Config.network.httpServers) { for (const key of ob11Config.network.httpServers) {
if (key.enable) { if (key.enable) {
this.networkManager.registerAdapter( this.networkManager.registerAdapter(
new OB11PassiveHttpAdapter(key.name, key, this.core, this, this.actions) new OB11PassiveHttpAdapter(key.name, key, this.core, this.actions)
); );
} }
} }
@@ -139,7 +128,6 @@ export class NapCatOneBot11Adapter {
key.name, key.name,
key, key,
this.core, this.core,
this,
this.actions this.actions
) )
); );
@@ -152,7 +140,6 @@ export class NapCatOneBot11Adapter {
key.name, key.name,
key, key,
this.core, this.core,
this,
this.actions this.actions
) )
); );
@@ -164,10 +151,9 @@ export class NapCatOneBot11Adapter {
this.initBuddyListener(); this.initBuddyListener();
this.initGroupListener(); this.initGroupListener();
WebUiDataRuntime.setQQVersion(this.core.context.basicInfoWrapper.getFullQQVesion()); await WebUiDataRuntime.setQQLoginUin(selfInfo.uin.toString());
WebUiDataRuntime.setQQLoginInfo(selfInfo); await WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setQQLoginStatus(true); await WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
const prev = this.configLoader.configData; const prev = this.configLoader.configData;
this.configLoader.save(newConfig); this.configLoader.save(newConfig);
this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`); this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
@@ -200,12 +186,10 @@ export class NapCatOneBot11Adapter {
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11ActiveWebSocketAdapter); await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11ActiveWebSocketAdapter);
} }
private async handleConfigChange<CT extends NetworkConfigAdapter>( private async handleConfigChange(
prevConfig: NetworkConfigAdapter[], prevConfig: NetworkConfigAdapter[],
nowConfig: NetworkConfigAdapter[], nowConfig: NetworkConfigAdapter[],
adapterClass: new ( adapterClass: new (...args: any[]) => IOB11NetworkAdapter
...args: ConstructorParameters<typeof IOB11NetworkAdapter<CT>>
) => IOB11NetworkAdapter<CT>
): Promise<void> { ): Promise<void> {
// 比较旧的在新的找不到的回收 // 比较旧的在新的找不到的回收
for (const adapterConfig of prevConfig) { for (const adapterConfig of prevConfig) {
@@ -226,7 +210,7 @@ export class NapCatOneBot11Adapter {
await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]); await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]);
} }
} else if (adapterConfig.enable) { } else if (adapterConfig.enable) {
const newAdapter = new adapterClass(adapterConfig.name, adapterConfig as CT, this.core, this, this.actions); const newAdapter = new adapterClass(adapterConfig.name, adapterConfig, this.core, this.actions);
await this.networkManager.registerAdapterAndOpen(newAdapter); await this.networkManager.registerAdapterAndOpen(newAdapter);
} }
} }
@@ -311,7 +295,7 @@ export class NapCatOneBot11Adapter {
guildId: '' guildId: ''
}; };
const msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS); const msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
const element = msg?.elements.find(e => !!e.grayTipElement?.revokeElement); const element = msg?.elements[0];
if (msg && element) { if (msg && element) {
const recallEvent = await this.emitRecallMsg(msg, element); const recallEvent = await this.emitRecallMsg(msg, element);
try { try {
@@ -349,7 +333,7 @@ export class NapCatOneBot11Adapter {
this.core, this.core,
+requesterUin, +requesterUin,
req.extWords, req.extWords,
req.reqTime req.friendUid + '|' + req.reqTime
) )
); );
} catch (e) { } catch (e) {
@@ -381,7 +365,8 @@ export class NapCatOneBot11Adapter {
if (notifyTime < this.bootTime) { if (notifyTime < this.bootTime) {
continue; continue;
} }
const flag = notify.seq;
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type;
this.context.logger.logDebug('收到群通知', notify); this.context.logger.logDebug('收到群通知', notify);
if ( if (
[GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) && [GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) &&
@@ -420,8 +405,8 @@ export class NapCatOneBot11Adapter {
this.context.logger.logDebug(`收到邀请我加群通知:${notify}`); this.context.logger.logDebug(`收到邀请我加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent( const groupInviteEvent = new OB11GroupRequestEvent(
this.core, this.core,
+notify.group.groupCode, parseInt(notify.group.groupCode),
+await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid), parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid)),
'invite', 'invite',
notify.postscript, notify.postscript,
flag flag
@@ -438,8 +423,8 @@ export class NapCatOneBot11Adapter {
this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`); this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent( const groupInviteEvent = new OB11GroupRequestEvent(
this.core, this.core,
+notify.group.groupCode, parseInt(notify.group.groupCode),
+await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid), parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid)),
'add', 'add',
notify.postscript, notify.postscript,
flag flag
@@ -459,7 +444,7 @@ export class NapCatOneBot11Adapter {
} }
private async emitMsg(message: RawMessage) { private async emitMsg(message: RawMessage) {
const network = await this.networkManager.getAllConfig(); const network = Object.values(this.configLoader.configData.network).flat() as Array<AdapterConfigWrap>;
this.context.logger.logDebug('收到新消息 RawMessage', message); this.context.logger.logDebug('收到新消息 RawMessage', message);
await Promise.allSettled([ await Promise.allSettled([
this.handleMsg(message, network), this.handleMsg(message, network),
@@ -468,10 +453,6 @@ export class NapCatOneBot11Adapter {
} }
private async handleMsg(message: RawMessage, network: Array<AdapterConfigWrap>) { private async handleMsg(message: RawMessage, network: Array<AdapterConfigWrap>) {
// 过滤无效消息
if (message.msgType === NTMsgType.KMSGTYPENULL) {
return;
}
try { try {
const ob11Msg = await this.apis.MsgApi.parseMessageV2(message, this.configLoader.configData.parseMultMsg); const ob11Msg = await this.apis.MsgApi.parseMessageV2(message, this.configLoader.configData.parseMultMsg);
if (ob11Msg) { if (ob11Msg) {
@@ -503,7 +484,7 @@ export class NapCatOneBot11Adapter {
ob11Msg.stringMsg.target_id = parseInt(message.peerUin); ob11Msg.stringMsg.target_id = parseInt(message.peerUin);
ob11Msg.arrayMsg.target_id = parseInt(message.peerUin); ob11Msg.arrayMsg.target_id = parseInt(message.peerUin);
} }
if ('messagePostFormat' in e && e.messagePostFormat == 'string') { if (e.messagePostFormat == 'string') {
msgMap.set(e.name, structuredClone(ob11Msg.stringMsg)); msgMap.set(e.name, structuredClone(ob11Msg.stringMsg));
} else { } else {
msgMap.set(e.name, structuredClone(ob11Msg.arrayMsg)); msgMap.set(e.name, structuredClone(ob11Msg.arrayMsg));
@@ -541,27 +522,21 @@ export class NapCatOneBot11Adapter {
// 群名片修改事件解析 任何都该判断 // 群名片修改事件解析 任何都该判断
if (message.senderUin && message.senderUin !== '0') { if (message.senderUin && message.senderUin !== '0') {
const cardChangedEvent = await this.apis.GroupApi.parseCardChangedEvent(message); const cardChangedEvent = await this.apis.GroupApi.parseCardChangedEvent(message);
if (cardChangedEvent) { cardChangedEvent && await this.networkManager.emitEvent(cardChangedEvent);
await this.networkManager.emitEvent(cardChangedEvent);
}
} }
if (message.msgType === NTMsgType.KMSGTYPEFILE) { if (message.msgType === NTMsgType.KMSGTYPEFILE) {
// 文件为单元素消息 // 文件为单元素消息
const elementWrapper = message.elements.find(e => !!e.fileElement); const elementWrapper = message.elements.find(e => !!e.fileElement);
if (elementWrapper?.fileElement) { if (elementWrapper?.fileElement) {
const uploadGroupFileEvent = await this.apis.GroupApi.parseGroupUploadFileEvene(message, elementWrapper.fileElement, elementWrapper); const uploadGroupFileEvent = await this.apis.GroupApi.parseGroupUploadFileEvene(message, elementWrapper.fileElement, elementWrapper);
if (uploadGroupFileEvent) { uploadGroupFileEvent && await this.networkManager.emitEvent(uploadGroupFileEvent);
await this.networkManager.emitEvent(uploadGroupFileEvent);
}
} }
} else if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) { } else if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) {
// 灰条为单元素消息 // 灰条为单元素消息
const grayTipElement = message.elements[0].grayTipElement; const grayTipElement = message.elements[0].grayTipElement;
if (grayTipElement) { if (grayTipElement) {
const event = await this.apis.GroupApi.parseGrayTipElement(message, grayTipElement); const event = await this.apis.GroupApi.parseGrayTipElement(message, grayTipElement);
if (event) { event && await this.networkManager.emitEvent(event);
await this.networkManager.emitEvent(event);
}
} }
} }
} catch (e) { } catch (e) {
@@ -576,10 +551,7 @@ export class NapCatOneBot11Adapter {
const grayTipElement = message.elements[0].grayTipElement; const grayTipElement = message.elements[0].grayTipElement;
if (grayTipElement) { if (grayTipElement) {
const event = await this.apis.MsgApi.parsePrivateMsgEvent(message, grayTipElement); const event = await this.apis.MsgApi.parsePrivateMsgEvent(message, grayTipElement);
if (event) { event && await this.networkManager.emitEvent(event);
await this.networkManager.emitEvent(event);
}
} }
} }
} catch (e) { } catch (e) {
@@ -599,8 +571,6 @@ export class NapCatOneBot11Adapter {
} }
private async emitFriendRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) { private async emitFriendRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid;
if (!operatorUid) return undefined;
return new OB11FriendRecallNoticeEvent( return new OB11FriendRecallNoticeEvent(
this.core, this.core,
+message.senderUin, +message.senderUin,
@@ -611,7 +581,7 @@ export class NapCatOneBot11Adapter {
private async emitGroupRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) { private async emitGroupRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid; const operatorUid = element.grayTipElement?.revokeElement.operatorUid;
if (!operatorUid) return undefined; if (!operatorUid) return undefined;
const operatorId = await this.core.apis.UserApi.getUinByUidV2(operatorUid); const operatorId = message.senderUin ?? await this.core.apis.UserApi.getUinByUidV2(operatorUid);
return new OB11GroupRecallNoticeEvent( return new OB11GroupRecallNoticeEvent(
this.core, this.core,
+message.peerUin, +message.peerUin,

View File

@@ -1,42 +1,62 @@
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index'; import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { LogWrapper } from '@/common/log';
import { QuickAction, QuickActionEvent } from '@/onebot/types'; import { QuickAction, QuickActionEvent } from '@/onebot/types';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
import { NapCatOneBot11Adapter } from '..'; import { NapCatOneBot11Adapter } from '..';
import { RequestUtil } from '@/common/request'; import { RequestUtil } from '@/common/request';
import { HttpClientConfig } from '@/onebot/config/config'; import { HttpClientConfig } from '@/onebot/config/config';
import { ActionMap } from '@/onebot/action'; import { ActionMap } from '@/onebot/action';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11ActiveHttpAdapter extends IOB11NetworkAdapter<HttpClientConfig> { export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
logger: LogWrapper;
isEnable: boolean = false;
public config: HttpClientConfig;
constructor( constructor(
name: string, config: HttpClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap public name: string,
config: HttpClientConfig,
public core: NapCatCore,
public obContext: NapCatOneBot11Adapter,
public actions: ActionMap,
) { ) {
super(name, config, core, obContext, actions); this.logger = core.context.logger;
this.config = structuredClone(config);
} }
onEvent<T extends OB11EmitEventContent>(event: T) { onEvent<T extends OB11EmitEventContent>(event: T) {
this.emitEventAsync(event).catch(e => this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报返回快速操作失败', e)); if (!this.isEnable) {
return;
} }
async emitEventAsync<T extends OB11EmitEventContent>(event: T) {
if (!this.isEnable) return;
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-self-id': this.core.selfInfo.uin, 'x-self-id': this.core.selfInfo.uin,
}; };
const msgStr = JSON.stringify(event); const msgStr = JSON.stringify(event);
if (this.config.token) { if (this.config.token && this.config.token.length > 0) {
const hmac = createHmac('sha1', this.config.token); const hmac = createHmac('sha1', this.config.token);
hmac.update(msgStr); hmac.update(msgStr);
headers['x-signature'] = 'sha1=' + hmac.digest('hex'); const sig = hmac.digest('hex');
headers['x-signature'] = 'sha1=' + sig;
} }
RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers).then(async (res) => {
const data = await RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers); let resJson: QuickAction;
const resJson: QuickAction = data ? JSON.parse(data) : {}; try {
resJson = JSON.parse(res);
await this.obContext.apis.QuickActionApi.handleQuickOperation(event as QuickActionEvent, resJson); //logDebug('新消息事件HTTP上报返回快速操作: ', JSON.stringify(resJson));
} catch (e) {
this.logger.logDebug('[OneBot] [Http Client] 新消息事件HTTP上报没有返回快速操作不需要处理');
return;
}
try {
this.obContext.apis.QuickActionApi
.handleQuickOperation(event as QuickActionEvent, resJson)
.catch(e => this.logger.logError(e));
} catch (e: any) {
this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报返回快速操作失败', e);
}
}).catch((e) => {
this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报失败', e);
});
} }
open() { open() {
@@ -46,24 +66,20 @@ export class OB11ActiveHttpAdapter extends IOB11NetworkAdapter<HttpClientConfig>
close() { close() {
this.isEnable = false; this.isEnable = false;
} }
async reload(newconfig: HttpClientConfig) {
async reload(newConfig: HttpClientConfig) {
const wasEnabled = this.isEnable; const wasEnabled = this.isEnable;
const oldUrl = this.config.url; const oldUrl = this.config.url;
this.config = newConfig; this.config = newconfig;
if (newconfig.enable && !wasEnabled) {
if (newConfig.enable && !wasEnabled) {
this.open(); this.open();
return OB11NetworkReloadType.NetWorkOpen; return OB11NetworkReloadType.NetWorkOpen;
} else if (!newConfig.enable && wasEnabled) { } else if (!newconfig.enable && wasEnabled) {
this.close(); this.close();
return OB11NetworkReloadType.NetWorkClose; return OB11NetworkReloadType.NetWorkClose;
} }
if (oldUrl !== newconfig.url) {
if (oldUrl !== newConfig.url) {
return OB11NetworkReloadType.NetWorkReload; return OB11NetworkReloadType.NetWorkReload;
} }
return OB11NetworkReloadType.Normal; return OB11NetworkReloadType.Normal;
} }
} }

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