Compare commits

...

72 Commits

Author SHA1 Message Date
手瓜一十雪
efa512ab21 fix: #580 2024-11-30 09:34:03 +08:00
bietiaop
9b04aed8b3 feat: 历史日志 2024-11-30 09:30:13 +08:00
手瓜一十雪
7087eafe37 feat: Universal Package (#578)
* feat: 统一包支持

* feat: Universal
2024-11-29 15:11:35 +08:00
Mlikiowa
c81c4af653 release: v4.2.7 2024-11-29 04:48:36 +00:00
手瓜一十雪
c05cc9dd02 feat: 迁移29927 2024-11-29 12:48:03 +08:00
手瓜一十雪
1a0da00f2d refactor: webui log 移除公网输出 2024-11-29 12:41:52 +08:00
手瓜一十雪
31b0c1d3d7 refactor: react webui 2024-11-29 12:22:25 +08:00
手瓜一十雪
53c1d40bcf refactor: logger bind (#577) 2024-11-28 20:55:28 +08:00
手瓜一十雪
97cacb4383 refactor: framework的操作性 2024-11-28 20:00:24 +08:00
Mlikiowa
e03905abaf release: v4.2.6 2024-11-28 07:28:33 +00:00
手瓜一十雪
06eba28b4c fux: #574 2024-11-28 15:28:09 +08:00
手瓜一十雪
bbfeac46dd Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-28 15:27:26 +08:00
手瓜一十雪
2fe4da094a fix 2024-11-28 15:14:01 +08:00
Mlikiowa
b454d8c0f9 release: v4.2.5 2024-11-28 07:07:10 +00:00
手瓜一十雪
1f9b5453cc fix: #573 2024-11-28 15:06:47 +08:00
Mlikiowa
3261791e99 release: v4.2.4 2024-11-28 03:00:35 +00:00
手瓜一十雪
3bb12e3f45 fix: #572 2024-11-28 10:56:57 +08:00
手瓜一十雪
1dc2f7e5a2 style: lint 2024-11-28 10:46:14 +08:00
手瓜一十雪
2531b08538 refactor: 提高解析兼容 2024-11-28 10:41:51 +08:00
手瓜一十雪
9fcfb5493c fix: #571 2024-11-28 10:27:04 +08:00
Mlikiowa
4576354c51 release: v4.2.3 2024-11-28 01:54:43 +00:00
手瓜一十雪
1dcf2ef0c6 fix: error handle 2024-11-28 09:53:50 +08:00
Mlikiowa
3642c65e8c release: v4.2.2 2024-11-27 12:39:03 +00:00
手瓜一十雪
40e105994a fix: pic size 2024-11-27 20:38:39 +08:00
Mlikiowa
f2ee973882 release: v4.2.1 2024-11-27 11:07:43 +00:00
手瓜一十雪
3aa30792bf Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-27 19:07:11 +08:00
手瓜一十雪
6e336fa78e fix: 合并丢失 2024-11-27 19:07:01 +08:00
Mlikiowa
900027a6b7 release: v4.2.0 2024-11-27 10:55:36 +00:00
手瓜一十雪
38bdca2409 fix: qrcode login 2024-11-27 18:51:54 +08:00
手瓜一十雪
7196e476bf Merge pull request #567 from bietiaop/webapi-bietiaop
refactor:优化WebUI后端代码格式(无新功能添加)
2024-11-27 18:39:19 +08:00
手瓜一十雪
e0fd3785d9 Merge pull request #565 from Ander-pixe/webui-new
修改webui
2024-11-27 18:35:13 +08:00
手瓜一十雪
b53ebb6c2a refactor: parse local path 2024-11-27 18:34:33 +08:00
纸凤孤凰
1ea80f4447 fix:修复webui已知bug 2024-11-27 18:16:16 +08:00
手瓜一十雪
627d3c0a7a Merge pull request #570 from NapNeko/dependabot/npm_and_yarn/vite-6.0.1
chore(deps-dev): bump vite from 5.4.11 to 6.0.1
2024-11-27 16:51:20 +08:00
dependabot[bot]
182cccfc71 chore(deps-dev): bump vite from 5.4.11 to 6.0.1
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.11 to 6.0.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.0.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 08:32:10 +00:00
手瓜一十雪
6a3713e86c fix: webui reload 2024-11-27 12:35:10 +08:00
手瓜一十雪
788da4e4f1 style 2024-11-27 12:25:37 +08:00
手瓜一十雪
fd26d34e19 fix 2024-11-27 12:20:30 +08:00
手瓜一十雪
e9fcdc7d2e feat: 简化代码 2024-11-27 11:35:51 +08:00
手瓜一十雪
0fe4911d01 fix: 优化类型 2024-11-27 11:29:03 +08:00
手瓜一十雪
d4fb09fa80 fix: 简化类型 2024-11-27 10:57:40 +08:00
手瓜一十雪
e6d5a37236 fix: menuRef 2024-11-27 10:53:50 +08:00
手瓜一十雪
79fd10ac10 Merge branch 'main' into pr/567 2024-11-26 20:06:18 +08:00
手瓜一十雪
a2e6095e44 chore: 注释不必要的代码 2024-11-26 19:44:37 +08:00
手瓜一十雪
64530471a0 fix: GroupChange 2024-11-26 19:42:35 +08:00
stapxs
e31e831309 feat: 优化网络配置卡片样式 2024-11-26 17:12:55 +08:00
手瓜一十雪
cf6871df9b Merge branch 'main' into pr/565 2024-11-26 12:56:57 +08:00
手瓜一十雪
482e7f1c75 Merge pull request #566 from NapNeko/refactor-group-event
refactor: parseGroupEvent
2024-11-26 12:51:48 +08:00
手瓜一十雪
aab501e31e Merge branch 'refactor-group-event' into pr/565 2024-11-26 12:40:19 +08:00
手瓜一十雪
ceec9e5e1b refactor: self report 2024-11-26 11:26:34 +08:00
手瓜一十雪
aadebb3cc5 refactor: event emit 2024-11-26 11:10:59 +08:00
手瓜一十雪
657ddd3341 refactor: emitRecallMsg 2024-11-26 11:08:12 +08:00
手瓜一十雪
62127b6d48 refactor: onMsgRecall 2024-11-25 22:36:07 +08:00
bietiaop
f5f405796f fix:vite配置别名framwork缺少@webapi 2024-11-25 22:29:18 +08:00
bietiaop
39873947a3 chore:移除多余依赖 2024-11-25 22:11:57 +08:00
手瓜一十雪
a1079dd948 fix 2024-11-25 21:58:35 +08:00
bietiaop
4eeabcc9e0 refactor:优化WebUI后端代码格式(无新功能添加) 2024-11-25 21:56:57 +08:00
手瓜一十雪
c3568d07e8 fix: #563 2024-11-25 21:56:34 +08:00
手瓜一十雪
1adb4a4ba8 refactor: parsePrivateMsgEvent 2024-11-25 21:53:35 +08:00
手瓜一十雪
6d0020533c chore: 可读性提高 优化代码 2024-11-25 21:44:22 +08:00
手瓜一十雪
4e6af0a655 feat: 性能优化 2024-11-25 21:28:04 +08:00
手瓜一十雪
00f726b515 fix: event parse 2024-11-25 21:20:32 +08:00
手瓜一十雪
035aa32305 fix: parseGroupUploadFileEvene 2024-11-25 21:04:33 +08:00
手瓜一十雪
62ea4b98e1 refactor: parseGroupEvent 2024-11-25 20:39:44 +08:00
pk5ls20
4be821137d feat: eslint 2024-11-25 20:02:50 +08:00
pk5ls20
7fba9960bf Merge branch 'main' into webui-new 2024-11-25 19:57:41 +08:00
手瓜一十雪
876bfbd3cb feat: tipgroup type 2024-11-25 19:32:30 +08:00
手瓜一十雪
edde2c210b feat: type 2024-11-25 19:24:51 +08:00
手瓜一十雪
f956d96d94 feat: 上报文件picType 2024-11-25 18:27:36 +08:00
手瓜一十雪
c2296fd900 fix 2024-11-25 18:24:13 +08:00
Mlikiowa
0feed5b640 release: v4.1.21 2024-11-25 07:48:50 +00:00
纸凤孤凰
3fbed815a5 修改webui 2024-11-25 02:17:48 +08:00
84 changed files with 2661 additions and 1858 deletions

2
.env.universal Normal file
View File

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

View File

@@ -34,7 +34,7 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 回家旅途
[QQ Group](https://qm.qq.com/q/NWP25OeV0c)
[QQ Group](https://qm.qq.com/q/I6LU87a0Yq)
## 感谢他们
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权

Binary file not shown.

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<link rel="icon" type="image/svg+xml" href="./logo_webui.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NapCat WebUI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

View File

@@ -14,7 +14,7 @@
"qrcode": "^1.5.4",
"tdesign-icons-vue-next": "^0.3.3",
"tdesign-vue-next": "^1.10.3",
"vue": "^3.5.12",
"vue": "^3.5.13",
"vue-router": "^4.4.5"
},
"devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -1,7 +1,112 @@
<template>
<div id="app">
<div id="app" theme-mode="dark">
<router-view />
</div>
<div v-if="show">
<t-sticky-tool shape="round" placement="right-bottom" :offset="[-50, 10]" @click="changeTheme">
<t-sticky-item label="浅色" popup="切换浅色模式">
<template #icon><sunny-icon /></template>
</t-sticky-item>
<t-sticky-item label="深色" popup="切换深色模式">
<template #icon><mode-dark-icon /></template>
</t-sticky-item>
<t-sticky-item label="自动" popup="跟随系统">
<template #icon><control-platform-icon /></template>
</t-sticky-item>
</t-sticky-tool>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';
import { ControlPlatformIcon, ModeDarkIcon, SunnyIcon } from 'tdesign-icons-vue-next';
const smallScreen = window.matchMedia('(max-width: 768px)');
interface Item {
label: string;
popup: string;
}
interface Context {
item: Item;
}
enum ThemeMode {
Dark = 'dark',
Light = 'light',
Auto = 'auto',
}
const themeLabelMap: Record<string, ThemeMode> = {
"浅色": ThemeMode.Light,
"深色": ThemeMode.Dark,
"自动": ThemeMode.Auto,
};
const show = ref<boolean>(true);
const createSetThemeAttributeFunction = () => {
let mediaQueryForAutoTheme: MediaQueryList | null = null;
return (mode: ThemeMode | null) => {
const element = document.documentElement;
if (mode === ThemeMode.Dark) {
element.setAttribute('theme-mode', ThemeMode.Dark);
} else if (mode === ThemeMode.Light) {
element.removeAttribute('theme-mode');
} else if (mode === ThemeMode.Auto) {
mediaQueryForAutoTheme = window.matchMedia('(prefers-color-scheme: dark)');
const handleMediaChange = (e: MediaQueryListEvent) => {
if (e.matches) {
element.setAttribute('theme-mode', ThemeMode.Dark);
} else {
element.removeAttribute('theme-mode');
}
};
mediaQueryForAutoTheme.addEventListener('change', handleMediaChange);
const event = new Event('change');
Object.defineProperty(event, 'matches', {
value: mediaQueryForAutoTheme.matches,
writable: false,
});
mediaQueryForAutoTheme.dispatchEvent(event);
onBeforeUnmount(() => {
if (mediaQueryForAutoTheme) {
mediaQueryForAutoTheme.removeEventListener('change', handleMediaChange);
}
});
}
};
};
const setThemeAttribute = createSetThemeAttributeFunction();
const getStoredTheme = (): ThemeMode | null => {
return localStorage.getItem('theme') as ThemeMode | null;
};
const initTheme = () => {
const storedTheme = getStoredTheme();
if (storedTheme === null) {
setThemeAttribute(ThemeMode.Auto);
} else {
setThemeAttribute(storedTheme);
}
};
const changeTheme = (context: Context) => {
const themeLabel = themeLabelMap[context.item.label] as ThemeMode;
console.log(themeLabel);
setThemeAttribute(themeLabel);
localStorage.setItem('theme', themeLabel);
};
const haddingFbars = () => {
show.value = !smallScreen.matches;
if (smallScreen.matches) {
localStorage.setItem('theme', 'auto');
}
};
onMounted(() => {
initTheme();
haddingFbars();
window.addEventListener('resize', haddingFbars);
});
onUnmounted(() => {
window.removeEventListener('resize', haddingFbars);
});
</script>
<style scoped></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -74,7 +74,7 @@ export class QQLoginManager {
}
return false;
}
public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string, isLogin: string } | undefined> {
public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string; isLogin: string } | undefined> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
method: 'POST',

View File

@@ -1,16 +1,18 @@
<template>
<div class="dashboard-container">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
<div class="content">
<router-view />
<t-layout class="dashboard-container">
<div ref="menuRef">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
</div>
</div>
<t-layout>
<router-view />
</t-layout>
</t-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import SidebarMenu from './webui/Nav.vue';
import emitter from '@/ts/event-bus';
interface MenuItem {
value: string;
icon: string;
@@ -25,6 +27,14 @@ const menuItems = ref<MenuItem[]>([
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
]);
const menuRef = ref<HTMLDivElement | null>(null);
emitter.on('sendMenu', (event) => {
emitter.emit('sendWidth', menuRef.value?.offsetWidth);
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
});
onMounted(() => {
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
});
</script>
<style scoped>
@@ -32,6 +42,7 @@ const menuItems = ref<MenuItem[]>([
display: flex;
flex-direction: row;
height: 100vh;
width: 100%;
}
.sidebar-menu {
@@ -39,14 +50,6 @@ const menuItems = ref<MenuItem[]>([
z-index: 2;
}
.content {
flex: 1;
/* padding: 20px; */
overflow: auto;
position: relative;
z-index: 1;
}
@media (max-width: 768px) {
.content {
padding: 10px;

View File

@@ -1,22 +1,43 @@
<template>
<div class="login-container">
<h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods">
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'">Quick Login</t-button>
<t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'">QR Code</t-button>
<t-card class="layout">
<div class="login-container">
<h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods">
<t-tooltip content="快速登录">
<t-button
id="quick-login"
class="login-method"
:class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'"
>Quick Login</t-button
>
</t-tooltip>
<t-tooltip content="二维码登录">
<t-button
id="qrcode-login"
class="login-method"
:class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'"
>QR Code</t-button
>
</t-tooltip>
</div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select
id="quick-login-select"
v-model="selectedAccount"
placeholder="Select Account"
@change="selectAccount"
>
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select>
</div>
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
<canvas ref="qrcodeCanvas"></canvas>
</div>
</div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account"
@change="selectAccount">
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select>
</div>
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
<canvas ref="qrcodeCanvas"></canvas>
</div>
</div>
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
</t-card>
</template>
<script setup lang="ts">
@@ -37,6 +58,9 @@ let qrcodeUrl: string = '';
const selectAccount = async (accountName: string): Promise<void> => {
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
if (result) {
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' });
} else {
@@ -64,10 +88,11 @@ const HeartBeat = async (): Promise<void> => {
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
//判断是否已经调转
if (router.currentRoute.value.path !== '/dashboard/basic-info') {
return;
}
// //判断是否已经调转
// if (router.currentRoute.value.path !== '/dashboard/basic-info') {
// return;
// }
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' });
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
qrcodeUrl = isLogined.qrcodeurl;
@@ -88,14 +113,16 @@ onMounted(() => {
</script>
<style scoped>
.layout {
height: 100vh;
}
.login-container {
padding: 20px;
border-radius: 5px;
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 0 auto;
margin: 50px auto;
}
@media (max-width: 600px) {
@@ -154,7 +181,5 @@ onMounted(() => {
bottom: 20px;
left: 0;
right: 0;
width: 100%;
background-color: white;
}
</style>

View File

@@ -1,20 +1,22 @@
<template>
<div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
<t-form-item name="password">
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
<template #prefix-icon>
<lock-on-icon />
</template>
</t-input>
</t-form-item>
<t-form-item>
<t-button theme="primary" type="submit" block>登录</t-button>
</t-form-item>
</t-form>
</div>
<div class="footer">Power By NapCat.WebUi</div>
<t-card class="layout">
<div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
<t-form-item name="password">
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
<template #prefix-icon>
<lock-on-icon />
</template>
</t-input>
</t-form-item>
<t-form-item>
<t-button theme="primary" type="submit" block>登录</t-button>
</t-form-item>
</t-form>
</div>
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
</t-card>
</template>
<script setup lang="ts">
@@ -94,14 +96,16 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
</script>
<style scoped>
.layout {
height: 100vh;
}
.login-container {
padding: 20px;
border-radius: 5px;
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 0 auto;
margin: 50px auto;
}
@media (max-width: 600px) {
@@ -145,7 +149,5 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
bottom: 20px;
left: 0;
right: 0;
width: 100%;
background-color: white;
}
</style>

View File

@@ -1,16 +1,31 @@
<template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
<template #logo> </template>
<template #logo>
<div class="logo">
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
<div class="logo-textBox">
<div class="logo-text">{{ collapsed ? '' : 'NapCat' }}</div>
</div>
</div>
</template>
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
<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 :disabled="!collapsed" :content="item.label" placement="right">
<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>
<template #operations>
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
<t-button
:disabled="disBtn"
class="t-demo-collapse-btn"
variant="text"
shape="square"
@click="changeCollapsed"
>
<template #icon><t-icon :name="iconName" /></template>
</t-button>
</template>
@@ -18,7 +33,8 @@
</template>
<script setup lang="ts">
import { ref, defineProps } from 'vue';
import { ref, defineProps, onMounted, watch } from 'vue';
import emitter from '@/ts/event-bus';
type MenuItem = {
value: string;
@@ -31,15 +47,39 @@ type MenuItem = {
defineProps<{
menuItems: MenuItem[];
}>();
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
const disBtn = ref<boolean>(false);
const changeCollapsed = (): void => {
collapsed.value = !collapsed.value;
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
};
watch(collapsed, (newValue, oldValue) => {
setTimeout(() => {
emitter.emit('sendMenu', collapsed.value);
}, 300);
});
onMounted(() => {
const mediaQuery = window.matchMedia('(max-width: 800px)');
const handleMediaChange = (e: MediaQueryListEvent) => {
disBtn.value = e.matches;
if (e.matches) {
collapsed.value = e.matches;
}
};
mediaQuery.addEventListener('change', handleMediaChange);
const event = new Event('change');
Object.defineProperty(event, 'matches', {
value: mediaQuery.matches,
writable: false,
});
mediaQuery.dispatchEvent(event);
return () => {
mediaQuery.removeEventListener('change', handleMediaChange);
};
});
</script>
<style scoped>
@@ -57,12 +97,28 @@ const changeCollapsed = (): void => {
width: 100px; /* 移动端侧边栏宽度 */
}
}
.logo {
display: flex;
width: auto;
height: 100%;
}
.logo-img {
object-fit: contain;
margin-top: 8px;
margin-bottom: 8px;
}
.logo-textBox {
display: flex;
align-items: center;
margin-left: 10px;
}
.logo-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 22px;
font-family: Sotheby, Helvetica, monospace;
}
.menu-item {

View File

@@ -19,6 +19,10 @@ import {
List as TList,
Alert as TAlert,
Tag as TTag,
Descriptions as TDescriptionsProps,
DescriptionsItem as TDescriptionsItem,
Collapse as TCollapse,
CollapsePanel as TCollapsePanel,
ListItem as TListItem,
Tabs as TTabs,
TabPanel as TTabPanel,
@@ -27,10 +31,18 @@ import {
Popup as TPopup,
Dialog as TDialog,
Switch as TSwitch,
Tooltip as Tooltip,
StickyTool as TStickyTool,
StickyItem as TStickyItem,
Layout as TLayout,
Content as TContent,
Footer as TFooter,
Aside as TAside,
Popconfirm as Tpopconfirm,
Empty as TEmpty,
} from 'tdesign-vue-next';
import { router } from './router';
import 'tdesign-vue-next/es/style/index.css';
const app = createApp(App);
app.use(router);
app.use(TButton);
@@ -51,6 +63,10 @@ app.use(TLink);
app.use(TList);
app.use(TAlert);
app.use(TTag);
app.use(TDescriptionsProps);
app.use(TDescriptionsItem);
app.use(TCollapse);
app.use(TCollapsePanel);
app.use(TListItem);
app.use(TTabs);
app.use(TTabPanel);
@@ -59,4 +75,13 @@ app.use(TCheckbox);
app.use(TPopup);
app.use(TDialog);
app.use(TSwitch);
app.use(Tooltip);
app.use(TStickyTool);
app.use(TStickyItem);
app.use(TLayout);
app.use(TContent);
app.use(TFooter);
app.use(TAside);
app.use(Tpopconfirm);
app.use(TEmpty);
app.mount('#app');

View File

@@ -1,122 +1,300 @@
<template>
<t-space class="full-space">
<template v-if="clientPanelData.length > 0">
<t-tabs
v-model="activeTab"
:addable="true"
theme="card"
@add="showAddTabDialog"
@remove="removeTab"
class="full-tabs"
>
<t-tab-panel
v-for="(config, idx) in clientPanelData"
:key="idx"
:label="config.name"
:removable="true"
:value="idx"
class="full-tab-panel"
>
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
<div class="button-container">
<t-button @click="saveConfig" style="width: 100px; height: 40px">保存</t-button>
<div ref="headerBox" class="title">
<t-divider content="网络配置" align="left" />
<t-divider align="right">
<t-button @click="addConfig()">
<template #icon><add-icon /></template>
添加配置</t-button>
</t-divider>
</div>
<div v-if="loadPage" ref="setting" class="setting">
<t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
<t-tab-panel value="all" label="全部"></t-tab-panel>
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
<t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
<t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
</t-tabs>
</div>
<div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
<div v-for="(item, index) in cardConfig" :key="index">
<t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }"
:header-bordered="true" class="setting-card">
<template #actions>
<t-space>
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
<t-popconfirm theme="danger" content="确认删除" @confirm="delConfig(item)">
<delete-icon size="20px"></delete-icon>
</t-popconfirm>
</t-space>
</template>
<div class="setting-content">
<t-card class="card-address" :style="{
borderLeft: '7px solid ' + (item.enable ?
'var(--td-success-color)' :
'var(--td-error-color)')
}">
<div class="local-box" v-if="item.host&&item.port">
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
<copy-icon class="copy-icon" size="20px" @click="copyText(item.host + ':' + item.port)"></copy-icon>
</div>
<div class="local-box" v-if="item.url">
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
<strong class="local" >{{ item.url }}</strong>
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
</div>
</t-card>
<t-collapse :default-value="[0]" expand-mutex style="margin-top:10px;" class="info-coll">
<t-collapse-panel header="基础信息">
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info">
<t-descriptions-item v-if="item.token" label="连接密钥">
<div v-if="mediumScreen.matches||largeScreen.matches" class="token-view">
<span>{{ showToken ? item.token : '******' }}</span>
<browse-icon class="browse-icon" v-if="showToken" size="18px"
@click="showToken = false"></browse-icon>
<browse-off-icon class="browse-icon" v-else size="18px"
@click="showToken = true"></browse-off-icon>
</div>
<div v-else>
<t-popup :showArrow="true" trigger="click">
<t-tag theme="primary">点击查看</t-tag>
<template #content>
<div @click="copyText(item.token)">{{item.token}}</div>
</template>
</t-popup>
</div>
</t-descriptions-item>
<t-descriptions-item label="消息格式">{{ item.messagePostFormat }}</t-descriptions-item>
</t-descriptions>
</t-collapse-panel>
<t-collapse-panel header="状态信息">
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info">
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
<t-tag class="tag-item" :theme="item.debug ? 'success' : 'danger'">
{{ item.debug ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
label="Websocket 功能">
<t-tag class="tag-item" :theme="item.enableWebsocket ? 'success' : 'danger'">
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
<t-tag class="tag-item" :theme="item.enableCors ? 'success' : 'danger'">
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="上报自身消息">
<t-tag class="tag-item" :theme="item.reportSelfMessage ? 'success' : 'danger'">
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="强制推送事件">
<t-tag class="tag-item"
:theme="item.enableForcePushEvent ? 'success' : 'danger'">
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
</t-descriptions>
</t-collapse-panel>
</t-collapse>
</div>
</t-tab-panel>
</t-tabs>
</template>
<template v-else>
<EmptyStateComponent :showAddTabDialog="showAddTabDialog" />
</template>
<t-dialog
v-model:visible="isDialogVisible"
header="添加网络配置"
@close="isDialogVisible = false"
@confirm="addTab"
>
<t-form ref="form" :model="newTab">
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
</t-card>
</div>
<div style="height: 20vh"></div>
</div>
<t-card v-else>
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
</t-card>
</div>
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
:show-in-attached-element="true" placement="center" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog--defaul">
<div slot="body" class="dialog-body" >
<t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
label="名称" name="name">
<t-input v-model="newTab.name" />
</t-form-item>
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
<t-select v-model="newTab.type">
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
label="类型" name="type">
<t-select v-model="newTab.type" @change="onloadDefault">
<t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option>
<t-option value="websocketServers">WebSocket 服务器</t-option>
<t-option value="websocketClients">WebSocket 客户端</t-option>
</t-select>
</t-form-item>
<div>
<component :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
:config="newTab.data" />
</div>
</t-form>
</t-dialog>
</t-space>
</div>
</t-dialog>
</template>
<script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { AddIcon, DeleteIcon, Edit2Icon, ServerFilledIcon, CopyIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-vue-next';
import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue';
import emitter from '@/ts/event-bus';
import {
httpServerDefaultConfigs,
httpClientDefaultConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig,
HttpServerConfig,
WebsocketClientConfig,
WebsocketServerConfig,
mergeNetworkDefaultConfig,
mergeOneBotConfigs,
NetworkConfig,
OneBotConfig,
mergeOneBotConfigs,
} from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
import EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
type ComponentUnion =
const showToken = ref<boolean>(false);
const infoOneCol = ref<boolean>(true);
const tabsWidth = ref<number>(0);
const menuWidth = ref<number>(0);
const cardWidth = ref<number>(0);
const cardHeight = ref<number>(0);
const mediumScreen = window.matchMedia('(min-width: 768px) and (max-width: 1024px)');
const largeScreen = window.matchMedia('(min-width: 1025px)');
const headerBox = ref<HTMLDivElement | null>(null);
const setting = ref<HTMLDivElement | null>(null);
const loadPage = ref<boolean>(false);
const visibleBody = ref<boolean>(false);
const newTab = ref<{ name: string; data: any; type: string }>({ name: '', data: {}, type: '' });
const dialogTitle = ref<string>('');
type ComponentKey = keyof typeof mergeNetworkDefaultConfig;
const componentMap: Record<
ComponentKey,
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent;
const componentMap: Record<ConfigKey, ComponentUnion> = {
| typeof WebsocketClientComponent
> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
//操作类型
const operateType = ref<string>('');
//配置项索引
const configIndex = ref<number>(0);
//保存时所用数据
const networkConfig: NetworkConfig & { [key: string]: any; } = {
websocketClients: [],
websocketServers: [],
httpClients: [],
httpServers: [],
};
interface ConfigMap {
httpServers: HttpServerConfig;
httpClients: HttpClientConfig;
websocketServers: WebsocketServerConfig;
websocketClients: WebsocketClientConfig;
}
interface ClientPanel<K extends ConfigKey = ConfigKey> {
name: string;
key: K;
data: ConfigMap[K];
}
const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ConfigKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Ref<ClientPanel[]> = ref([]);
const getComponent = (type: ConfigKey) => {
//挂载的数据
const WebConfg = ref(
new Map<string, Array<null>>([
['all', []],
['httpServers', []],
['httpClients', []],
['websocketServers', []],
['websocketClients', []],
])
);
const typeCh: Record<ComponentKey, string> = {
httpServers: 'HTTP 服务器',
httpClients: 'HTTP 客户端',
websocketServers: 'WebSocket 服务器',
websocketClients: 'WebSocket 客户端',
};
const cardConfig = ref<any>([]);
const getComponent = (type: ComponentKey) => {
return componentMap[type];
};
const getKeyByValue = (obj: typeof typeCh, value: string): string | undefined => {
return Object.entries(obj).find(([_, v]) => v === value)?.[0];
};
const addConfig = () => {
dialogTitle.value = '添加配置';
newTab.value = { name: '', data: {}, type: '' };
operateType.value = 'add';
visibleBody.value = true;
};
const editConfig = (item: any) => {
dialogTitle.value = '修改配置';
const type = getKeyByValue(typeCh, item.type);
if (type) {
newTab.value = { name: item.name, data: item, type: type };
}
operateType.value = 'edit';
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
visibleBody.value = true;
};
const delConfig = (item: any) => {
const type = getKeyByValue(typeCh, item.type);
if (type) {
newTab.value = { name: item.name, data: item, type: type };
}
configIndex.value = configIndex.value = networkConfig[newTab.value.type].findIndex(
(obj: any) => obj.name === item.name
);
operateType.value = 'delete';
saveConfig();
};
const selectType = (key: ComponentKey) => {
cardConfig.value = WebConfg.value.get(key);
};
const onloadDefault = (key: ComponentKey) => {
console.log(key);
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
};
//检测重名
const checkName = (name: string) => {
const allConfigs = WebConfg.value.get('all')?.findIndex((obj: any) => obj.name === name);
if (newTab.value.name === '' || newTab.value.type === '') {
MessagePlugin.error('请填写完整信息');
return false;
} else if (allConfigs === -1 || newTab.value.data.name === name) {
return true;
} else {
MessagePlugin.error('名称已存在');
return false;
}
};
//保存
const saveConfig = async () => {
if (operateType.value == 'add') {
if (!checkName(newTab.value.name)) return;
newTab.value.data.name = newTab.value.name;
networkConfig[newTab.value.type].push(newTab.value.data);
} else if (operateType.value == 'edit') {
if (!checkName(newTab.value.name)) return;
newTab.value.data.name = newTab.value.name;
networkConfig[newTab.value.type][configIndex.value] = newTab.value.data;
} else if (operateType.value == 'delete') {
networkConfig[newTab.value.type].splice(configIndex.value, 1);
}
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = networkConfig;
const success = await setOB11Config(userConfig);
if (success) {
operateType.value = '';
configIndex.value = 0;
MessagePlugin.success('配置保存成功');
await loadConfig();
visibleBody.value = false;
} else {
MessagePlugin.error('配置保存失败');
}
};
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
@@ -137,27 +315,27 @@ const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
return await loginManager.SetOB11Config(config);
};
const addToPanel = <K extends ConfigKey>(configs: ConfigMap[K][], key: K) => {
configs.forEach((config) => clientPanelData.value.push({ name: config.name, data: config, key }));
};
const addConfigDataToPanel = (data: NetworkConfig) => {
(Object.keys(data) as ConfigKey[]).forEach((key) => {
addToPanel(data[key], key);
});
};
const parsePanelData = (): NetworkConfig => {
const result: NetworkConfig = {
httpServers: [],
httpClients: [],
websocketServers: [],
websocketClients: [],
};
clientPanelData.value.forEach((panel) => {
(result[panel.key] as Array<typeof panel.data>).push(panel.data);
});
return result;
//获取卡片数据
const getAllData = (data: NetworkConfig) => {
cardConfig.value = [];
WebConfg.value.set('all', []);
for (const key in data) {
const configs = data[key as keyof NetworkConfig];
if (key in mergeNetworkDefaultConfig) {
networkConfig[key] = [...configs];
const newConfigsArray = configs.map((config: any) => ({
...config,
type: typeCh[key as ComponentKey],
}));
WebConfg.value.set(key, newConfigsArray);
const allConfigs = WebConfg.value.get('all');
if (allConfigs) {
const newAllConfigs = [...allConfigs, ...newConfigsArray];
WebConfg.value.set('all', newAllConfigs);
}
cardConfig.value = WebConfg.value.get('all');
}
}
};
const loadConfig = async () => {
@@ -165,85 +343,198 @@ const loadConfig = async () => {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network);
getAllData(mergedConfig.network);
} catch (error) {
console.error('Error loading config:', error);
}
};
const saveConfig = async () => {
const config = parsePanelData();
const userConfig = await getOB11Config();
if (!userConfig) {
await MessagePlugin.error('无法获取配置!');
return;
}
userConfig.network = config;
const success = await setOB11Config(userConfig);
if (success) {
await MessagePlugin.success('配置保存成功');
const copyText = async (text: string) => {
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
await navigator.clipboard.writeText(text);
document.body.removeChild(input);
MessagePlugin.success('复制成功');
};
const handleResize = () => {
// 得根据卡片宽度改,懒得改了;先不管了
// if(window.innerWidth < 540) {
// infoOneCol.value= true
// } else {
// infoOneCol.value= false
// }
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
if (mediumScreen.matches) {
cardWidth.value = (tabsWidth.value - 20) / 2;
} else if (largeScreen.matches) {
cardWidth.value = (tabsWidth.value - 40) / 3;
} else {
await MessagePlugin.error('配置保存失败');
cardWidth.value = tabsWidth.value;
}
loadPage.value = true;
setTimeout(() => {
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
}, 300);
};
const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true;
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.value.some((panel) => panel.name === name)) {
await MessagePlugin.error('选项卡名称已存在');
return;
emitter.on('sendWidth', (width) => {
if (typeof width === 'number' && !isNaN(width)) {
menuWidth.value = width;
handleResize();
}
const defaultConfig = structuredClone(defaultConfigMap[type]);
defaultConfig.name = name;
clientPanelData.value.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.value.length - 1;
await MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.value.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
});
onMounted(() => {
loadConfig();
const cachedWidth = localStorage.getItem('menuWidth');
if (cachedWidth) {
menuWidth.value = parseInt(cachedWidth);
setTimeout(() => {
handleResize();
}, 300);
}
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.full-space {
width: 100%;
height: 100%;
.title {
padding: 20px 20px 0 20px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
justify-content: space-between;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.setting {
margin: 0 20px;
}
.full-tab-panel {
.setting-box {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
overflow-y: auto;
}
.setting-card {
width: 100%;
text-align: left;
}
.setting-content {
width: 100%;
}
.card-address svg {
fill: var(--td-brand-color);
cursor: pointer;
}
.local-box {
display: flex;
margin-top: 2px;
}
.local-icon{
flex: 1;
display: flex;
flex-direction: column;
}
.local {
flex: 6;
margin: 0 10px 0 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.button-container {
.copy-icon {
flex: 1;
cursor: pointer;
flex-direction: row;
}
.token-view {
display: flex;
justify-content: center;
margin-top: 20px;
align-items: center;
}
.token-view span {
flex: 5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.browse-icon{
flex: 2;
}
:global(.t-dialog__ctx .t-dialog--defaul) {
margin: 0 20px;
}
@media (max-width: 1024px) {
.setting-box {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 786px) {
.setting-box {
grid-template-columns: 1fr;
}
}
.card-box {
margin: 10px 20px 0 20px;
}
.card-none {
line-height: 400px !important;
}
.dialog-body {
max-height: 60vh;
overflow-y: auto;
}
::-webkit-scrollbar {
width: 0;
background: transparent;
}
</style>
<style>
.setting-card .t-card__title {
text-align: left !important;
}
.setting-card .t-card__description {
margin-bottom: 0;
font-size: 12px;
}
.card-address .t-card__body {
display: flex;
flex-direction: row;
align-items: center;
}
.setting-base-info .t-descriptions__header {
font-size: 15px;
margin-bottom: 0;
}
.setting-base-info .t-descriptions__label {
padding: 0 var(--td-comp-paddingLR-l) !important;
}
.setting-base-info tr>td:last-child {
text-align: right;
}
.info-coll .t-collapse-panel__wrapper .t-collapse-panel__content {
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
}
</style>

View File

@@ -1,25 +1,27 @@
<template>
<div>
<div class="title">
<t-divider content="其余配置" align="left" />
</div>
<div class="other-config-container">
<div class="other-config">
<t-form ref="form" :model="otherConfig" class="form">
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
<t-input v-model="otherConfig.musicSignUrl" />
</t-form-item>
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
<t-switch v-model="otherConfig.enableLocalFile2Url" />
</t-form-item>
<t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
<t-switch v-model="otherConfig.parseMultMsg" />
</t-form-item>
</t-form>
<div class="button-container">
<t-button @click="saveConfig">保存</t-button>
<t-card class="card">
<div class="other-config-container">
<div class="other-config">
<t-form ref="form" :model="otherConfig" :label-align="labelAlign" label-width="auto" colon>
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
<t-input v-model="otherConfig.musicSignUrl" />
</t-form-item>
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
<t-switch v-model="otherConfig.enableLocalFile2Url" />
</t-form-item>
<t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
<t-switch v-model="otherConfig.parseMultMsg" />
</t-form-item>
</t-form>
<div class="button-container">
<t-button @click="saveConfig">保存</t-button>
</div>
</div>
</div>
</div>
</t-card>
</template>
<script setup lang="ts">
@@ -34,6 +36,7 @@ const otherConfig = ref<Partial<OneBotConfig>>({
parseMultMsg: true
});
const labelAlign = ref<string>();
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
@@ -86,55 +89,60 @@ const saveConfig = async () => {
MessagePlugin.error('配置保存失败');
}
};
onMounted(() => {
loadConfig();
const mediaQuery = window.matchMedia('(max-width: 768px)');
const handleMediaChange = (e: MediaQueryListEvent) => {
if (e.matches) {
labelAlign.value = 'top';
} else {
labelAlign.value = 'left';
}
};
mediaQuery.addEventListener('change', handleMediaChange);
const event = new Event('change');
Object.defineProperty(event, 'matches', {
value: mediaQuery.matches,
writable: false,
});
mediaQuery.dispatchEvent(event);
return () => {
mediaQuery.removeEventListener('change', handleMediaChange);
};
});
</script>
<style scoped>
.title {
padding: 20px 20px 0 20px;
}
.card {
margin: 0 20px;
padding-top: 20px;
padding-bottom: 20px;
}
.other-config-container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.other-config {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
max-width: 500px;
border-radius: 8px;
}
.form {
display: flex;
flex-direction: column;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 20px;
text-align: left;
}
.button-container {
display: flex;
justify-content: center;
}
@media (min-width: 768px) {
.form-item {
flex-direction: row;
align-items: center;
}
.form-item t-input,
.form-item t-switch {
flex: 1;
margin-left: 20px;
}
margin-top: 20px;
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<div class="empty-state">
<p>当前没有网络配置</p>
<t-button @click="showAddTabDialog">添加网络配置</t-button>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
defineProps<{ showAddTabDialog: () => void }>();
</script>
<style scoped>
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
</style>

View File

@@ -1,28 +1,25 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Client 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
</template>
@@ -49,20 +46,4 @@ watch(
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>
<style scoped></style>

View File

@@ -1,34 +1,31 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Server 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-checkbox v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-checkbox v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
</template>
@@ -55,20 +52,4 @@ watch(
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>
<style scoped></style>

View File

@@ -1,31 +1,28 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Client 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</template>
@@ -52,20 +49,4 @@ watch(
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>
<style scoped></style>

View File

@@ -1,37 +1,34 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Server 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="上报自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="强制推送事件">
<t-checkbox v-model="config.enableForcePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="上报自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="强制推送事件">
<t-checkbox v-model="config.enableForcePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</template>
@@ -58,20 +55,4 @@ watch(
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>
<style scoped></style>

View File

@@ -0,0 +1,3 @@
import mitt from 'mitt';
const emitter = mitt();
export default emitter;

View File

@@ -2,11 +2,13 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.1.20",
"version": "4.2.7",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
"build:webui": "cd napcat.webui && vite build",
"dev:universal": "vite build --mode universal",
"dev:framework": "vite build --mode framework",
"dev:shell": "vite build --mode shell",
"dev:webui": "cd napcat.webui && npm run webui:dev",
@@ -44,7 +46,7 @@
"json-schema-to-ts": "^3.1.1",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^5.2.6",
"vite": "^6.0.1",
"vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0"
@@ -52,9 +54,9 @@
"dependencies": {
"express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2",
"piscina": "^4.7.0",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0",
"piscina": "^4.7.0"
"ws": "^8.18.0"
}
}

View File

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

View File

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

View File

@@ -182,7 +182,6 @@ export enum FileUriType {
}
export async function checkUriType(Uri: string) {
const LocalFileRet = await solveProblem((uri: string) => {
if (fs.existsSync(uri)) {
return { Uri: uri, Type: FileUriType.Local };
@@ -191,23 +190,17 @@ export async function checkUriType(Uri: string) {
}, Uri);
if (LocalFileRet) return LocalFileRet;
const OtherFileRet = await solveProblem((uri: string) => {
//再判断是否是Http
if (uri.startsWith('http://') || uri.startsWith('https://')) {
// 再判断是否是Http
if (uri.startsWith('http:') || uri.startsWith('https:')) {
return { Uri: uri, Type: FileUriType.Remote };
}
//再判断是否是Base64
if (uri.startsWith('base64://')) {
// 再判断是否是Base64
if (uri.startsWith('base64:')) {
return { Uri: uri, Type: FileUriType.Base64 };
}
if (uri.startsWith('file://')) {
let filePath: string;
const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash);
if (process.platform === 'win32') {
filePath = pathname.slice(1);
} else {
filePath = pathname;
}
// 默认file://
if (uri.startsWith('file:')) {
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
return { Uri: filePath, Type: FileUriType.Local };
}
if (uri.startsWith('data:')) {
@@ -222,14 +215,16 @@ export async function checkUriType(Uri: string) {
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
//解析失败
const tempName = randomUUID();
if (!filename) filename = randomUUID();
//解析Http和Https协议
//解析Http和Https协议
if (UriType == FileUriType.Unknown) {
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
}
//解析File协议和本地文件
if (UriType == FileUriType.Local) {
const fileExt = path.extname(HandledUri);
@@ -241,8 +236,8 @@ export async function uri2local(dir: string, uri: string, filename: string | und
fs.copyFileSync(HandledUri, filePath);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
//接下来都要有文件名
//接下来都要有文件名
if (UriType == FileUriType.Remote) {
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
if (pathInfo.name) {
@@ -260,6 +255,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
//解析Base64
if (UriType == FileUriType.Base64) {
const base64 = HandledUri.replace(/^base64:\/\//, '');

View File

@@ -1 +1 @@
export const napCatVersion = '4.1.20';
export const napCatVersion = '4.2.7';

View File

@@ -26,7 +26,7 @@ import pathLib from 'node:path';
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
import ffmpeg from 'fluent-ffmpeg';
import { encodeSilk } from '@/common/audio';
import { MessageContext } from '@/onebot/api';
import { SendMessageContext } from '@/onebot/api';
import { getFileTypeForSendType } from '../helper/msg';
export class NTQQFileApi {
@@ -41,7 +41,7 @@ export class NTQQFileApi {
this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys'
],
this.context.logger
this.context.logger
);
}
@@ -91,7 +91,7 @@ export class NTQQFileApi {
};
}
async createValidSendFileElement(context: MessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
async createValidSendFileElement(context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
const {
fileName: _fileName,
path,
@@ -113,7 +113,7 @@ export class NTQQFileApi {
};
}
async createValidSendPicElement(context: MessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
async createValidSendPicElement(context: SendMessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
@@ -141,8 +141,7 @@ export class NTQQFileApi {
};
}
async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
const logger = this.core.context.logger;
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
let videoInfo = {
width: 1920,
height: 1080,
@@ -152,9 +151,9 @@ export class NTQQFileApi {
filePath,
};
try {
videoInfo = await getVideoInfo(filePath, logger);
videoInfo = await getVideoInfo(filePath, this.context.logger);
} catch (e) {
logger.logError.bind(logger)('获取视频信息失败,将使用默认值', e);
this.context.logger.logError('获取视频信息失败,将使用默认值', e);
}
let fileExt = 'mp4';
@@ -162,7 +161,7 @@ export class NTQQFileApi {
const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
if (tempExt) fileExt = tempExt;
} catch (e) {
this.context.logger.logError.bind(logger)('获取文件类型失败', e);
this.context.logger.logError('获取文件类型失败', e);
}
const newFilePath = filePath + '.' + fileExt;
fs.copyFileSync(filePath, newFilePath);
@@ -183,7 +182,7 @@ export class NTQQFileApi {
ffmpeg(filePath)
.on('error', (err) => {
try {
logger.logDebug('获取视频封面失败,使用默认封面', err);
this.context.logger.logDebug('获取视频封面失败,使用默认封面', err);
if (diyThumbPath) {
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
@@ -193,7 +192,7 @@ export class NTQQFileApi {
resolve(thumbPath);
}
} catch (error) {
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
this.context.logger.logError('获取视频封面失败,使用默认封面失败', error);
}
})
.screenshots({
@@ -230,6 +229,7 @@ export class NTQQFileApi {
}
async createValidSendPttElement(pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
if (!silkPath) {
throw new Error('语音转换失败, 请检查语音文件是否正常');
@@ -239,8 +239,7 @@ export class NTQQFileApi {
throw new Error('文件异常大小为0');
}
if (converted) {
fsPromises.unlink(silkPath).then().catch(
(e) => this.context.logger.logError.bind(this.context.logger)('删除临时文件失败', e)
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e)
);
}
return {
@@ -307,18 +306,18 @@ export class NTQQFileApi {
element.elementType === ElementType.FILE
) {
switch (element.elementType) {
case ElementType.PIC:
case ElementType.PIC:
element.picElement!.sourcePath = elementResults[elementIndex];
break;
case ElementType.VIDEO:
break;
case ElementType.VIDEO:
element.videoElement!.filePath = elementResults[elementIndex];
break;
case ElementType.PTT:
break;
case ElementType.PTT:
element.pttElement!.filePath = elementResults[elementIndex];
break;
case ElementType.FILE:
break;
case ElementType.FILE:
element.fileElement!.filePath = elementResults[elementIndex];
break;
break;
}
elementIndex++;
}
@@ -454,7 +453,7 @@ export class NTQQFileApi {
}
}
} catch (error: any) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
this.context.logger.logError('获取rkey失败', error.message);
}
if (!rkeyData.online_rkey) {
@@ -464,7 +463,7 @@ export class NTQQFileApi {
rkeyData.private_rkey = tempRkeyData.private_rkey;
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
} catch (e) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
this.context.logger.logError('获取rkey失败 Fallback Old Mode', e);
}
}

View File

@@ -27,7 +27,7 @@ export class NTQQGroupApi {
this.core = core;
}
async initApi() {
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
this.initCache().then().catch(e => this.context.logger.logError(e));
}
async initCache() {
this.groups = await this.getGroups();

View File

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

View File

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

View File

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

View File

@@ -127,7 +127,7 @@ export class NapCatCore {
await api.initApi();
}
}
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
this.initNapCatCoreListeners().then().catch((e) => this.context.logger.logError(e));
this.context.logger.setFileLogEnabled(
this.configLoader.configData.fileLog,
@@ -154,7 +154,7 @@ export class NapCatCore {
const msgListener = new NodeIKernelMsgListener();
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
this.context.logger.logError.bind(this.context.logger)('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.selfInfo.online = false;
};
msgListener.onRecvMsg = (msgs) => {

View File

@@ -255,7 +255,7 @@ export class NodeIKernelMsgListener {
}
onMsgRecall(i2: unknown, str: unknown, j2: unknown): any {
onMsgRecall(chatType: ChatType, uid: string, msgSeq: string): any {
}

View File

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

View File

@@ -40,17 +40,18 @@ export interface FaceElement {
surpriseId?: string;
randomType?: number;
}
export interface GrayTipRovokeElement {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}
export interface GrayTipElement {
subElementType: NTGrayTipElementSubTypeV2;
revokeElement: {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
};
revokeElement: GrayTipRovokeElement;
aioOpGrayTipElement: TipAioOpGrayTipElement;
groupElement: TipGroupElement;
xmlElement: {

View File

@@ -383,12 +383,39 @@ export enum MemberAddShowType {
K_YOU_INVITE_OTHER = 7,
}
/**
* 群提示元素成员角色枚举
*/
export enum NTGroupGrayElementRole {
KOTHER = 0,
KMEMBER = 1,
KADMIN = 2
}
/**
* 群灰色提示成员接口
* */
export interface NTGroupGrayMember {
serialVersionUID: string;
uid: string;
name: string;
}
/**
* 群灰色提示邀请者和被邀请者接口
*
* */
export interface NTGroupGrayInviterAndInvite {
invited: NTGroupGrayMember;
inviter: NTGroupGrayMember;
serialVersionUID: string;
}
/**
* 群提示元素接口
*/
export interface TipGroupElement {
type: TipGroupElementType;
role: 0;
role: NTGroupGrayElementRole;
groupName: string;
memberUid: string;
memberNick: string;
@@ -399,13 +426,13 @@ export interface TipGroupElement {
createGroup: null;
memberAdd?: {
showType: MemberAddShowType;
otherAdd: null;
otherAddByOtherQRCode: null;
otherAddByYourQRCode: null;
youAddByOtherQRCode: null;
otherInviteOther: null;
otherInviteYou: null;
youInviteOther: null
otherAdd: NTGroupGrayMember;
otherAddByOtherQRCode: NTGroupGrayInviterAndInvite;
otherAddByYourQRCode: NTGroupGrayMember;
youAddByOtherQRCode: NTGroupGrayMember;
otherInviteOther: NTGroupGrayInviterAndInvite;
otherInviteYou: NTGroupGrayMember;
youInviteOther: NTGroupGrayMember;
};
shutUp?: {
curTime: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -162,11 +162,10 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
finallySendElements: SendArkElement,
res_id?: string
} | null> {
const logger = this.core.context.logger;
const packetMsg: PacketMsg[] = [];
for (const node of messageNodes) {
if (dp >= 3) {
logger.logWarn('转发消息深度超过3层将停止解析');
this.core.context.logger.logWarn('转发消息深度超过3层将停止解析');
break;
}
if (!node.data.id) {
@@ -191,29 +190,29 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
time: Number(node.data.time) || Date.now(),
msg: sendElements,
};
logger.logDebug(`handleForwardedNodesPacket[SendRaw] 开始转换 ${stringifyWithBigInt(packetMsgElements)}`);
this.core.context.logger.logDebug(`handleForwardedNodesPacket[SendRaw] 开始转换 ${stringifyWithBigInt(packetMsgElements)}`);
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements);
logger.logDebug(`handleForwardedNodesPacket[SendRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
this.core.context.logger.logDebug(`handleForwardedNodesPacket[SendRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg);
} else if (node.data.id) {
const id = node.data.id;
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(+id) || MessageUnique.getPeerByMsgId(id);
if (!nodeMsg) {
logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', id);
this.core.context.logger.logError('转发消息失败,未找到消息', id);
continue;
}
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0];
logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgToPacketMsg(msg, msgPeer);
logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg);
} else {
logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${stringifyWithBigInt(node)}`);
this.core.context.logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${stringifyWithBigInt(node)}`);
}
}
if (packetMsg.length === 0) {
logger.logWarn('handleForwardedNodesPacket 元素为空!');
this.core.context.logger.logWarn('handleForwardedNodesPacket 元素为空!');
return null;
}
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
@@ -253,14 +252,13 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
peerUid: this.core.selfInfo.uid,
};
let nodeMsgIds: string[] = [];
const logger = this.core.context.logger;
for (const messageNode of messageNodes) {
const nodeId = messageNode.data.id;
if (nodeId) {
// 对Msgid和OB11ID混用情况兜底
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId);
if (!nodeMsg) {
logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', nodeId);
this.core.context.logger.logError('转发消息失败,未找到消息', nodeId);
continue;
}
nodeMsgIds.push(nodeMsg.MsgId);
@@ -272,7 +270,7 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
const isNodeMsg = OB11Data.filter(e => e.type === OB11MessageDataType.node).length;//找到子转发消息
if (isNodeMsg !== 0) {
if (isNodeMsg !== OB11Data.length) {
logger.logError.bind(this.core.context.logger)('子消息中包含非node消息 跳过不合法部分');
this.core.context.logger.logError('子消息中包含非node消息 跳过不合法部分');
continue;
}
const nodeMsg = await this.handleForwardedNodes(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node));
@@ -309,7 +307,7 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
}
});
} catch (e: any) {
logger.logDebug('生成转发消息节点失败', e?.stack);
this.core.context.logger.logDebug('生成转发消息节点失败', e?.stack);
}
}
}
@@ -320,7 +318,7 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
for (const msgId of nodeMsgIds) {
const nodeMsgPeer = MessageUnique.getPeerByMsgId(msgId);
if (!nodeMsgPeer) {
logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', msgId);
this.core.context.logger.logError('转发消息失败,未找到消息', msgId);
continue;
}
const nodeMsg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0];
@@ -346,12 +344,12 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
}
if (retMsgIds.length === 0) throw Error('转发消息失败,生成节点为空');
try {
logger.logDebug('开发转发', srcPeer, destPeer, retMsgIds);
this.core.context.logger.logDebug('开发转发', srcPeer, destPeer, retMsgIds);
return {
message: await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
};
} catch (e: any) {
logger.logError.bind(this.core.context.logger)('forward failed', e?.stack);
this.core.context.logger.logError('forward failed', e?.stack);
return {
message: null
};
@@ -363,7 +361,6 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.core.selfInfo.uid,
};
const logger = this.core.context.logger;
//msg 为待克隆消息
const sendElements: SendMessageElement[] = [];
@@ -372,12 +369,12 @@ export class SendMsg extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
}
if (sendElements.length === 0) {
logger.logDebug('需要clone的消息无法解析将会忽略掉', msg);
this.core.context.logger.logDebug('需要clone的消息无法解析将会忽略掉', msg);
}
try {
return await this.core.apis.MsgApi.sendMsg(selfPeer, sendElements, true);
} catch (e: any) {
logger.logError.bind(this.core.context.logger)(e?.stack, '克隆转发消息失败,将忽略本条消息', msg);
this.core.context.logger.logError(e?.stack, '克隆转发消息失败,将忽略本条消息', msg);
}
}
}

View File

@@ -1,10 +1,15 @@
import {
ChatType,
FileElement,
GrayTipElement,
InstanceContext,
JsonGrayBusiId,
MessageElement,
NapCatCore,
NTGrayTipElementSubTypeV2,
NTMsgType,
RawMessage,
TipGroupElement,
TipGroupElementType,
} from '@/core';
import { NapCatOneBot11Adapter } from '@/onebot';
@@ -15,13 +20,12 @@ import fastXmlParser from 'fast-xml-parser';
import { OB11GroupMsgEmojiLikeEvent } from '@/onebot/event/notice/OB11MsgEmojiLikeEvent';
import { MessageUnique } from '@/common/message-unique';
import { OB11GroupCardEvent } from '@/onebot/event/notice/OB11GroupCardEvent';
import { OB11GroupUploadNoticeEvent } from '@/onebot/event/notice/OB11GroupUploadNoticeEvent';
import { OB11GroupPokeEvent } from '@/onebot/event/notice/OB11PokeEvent';
import { OB11GroupEssenceEvent } from '@/onebot/event/notice/OB11GroupEssenceEvent';
import { OB11GroupTitleEvent } from '@/onebot/event/notice/OB11GroupTitleEvent';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { OB11GroupUploadNoticeEvent } from '../event/notice/OB11GroupUploadNoticeEvent';
import { pathToFileURL } from 'node:url';
import { FileNapCatOneBotUUID } from '@/common/helper';
export class OneBotGroupApi {
obContext: NapCatOneBot11Adapter;
@@ -32,137 +36,6 @@ export class OneBotGroupApi {
this.core = core;
}
async parseGroupEvent(msg: RawMessage) {
const logger = this.core.context.logger;
if (msg.chatType !== ChatType.KCHATTYPEGROUP) {
return;
}
//log("group msg", msg);
if (msg.senderUin && msg.senderUin !== '0') {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
if (member && member.cardName !== msg.sendMemberName) {
const newCardName = msg.sendMemberName ?? '';
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
member.cardName = newCardName;
return event;
}
}
for (const element of msg.elements) {
if (element.grayTipElement?.groupElement) {
const groupElement = element.grayTipElement.groupElement;
if (groupElement.type == TipGroupElementType.KMEMBERADD) {
const MemberIncreaseEvent = await this.obContext.apis.GroupApi.parseGroupMemberIncreaseEvent(msg.peerUid, element.grayTipElement);
if (MemberIncreaseEvent) return MemberIncreaseEvent;
} else if (groupElement.type === TipGroupElementType.KSHUTUP) {
const BanEvent = await this.obContext.apis.GroupApi.parseGroupBanEvent(msg.peerUid, element.grayTipElement);
if (BanEvent) return BanEvent;
} else if (groupElement.type == TipGroupElementType.KQUITTE) {
this.core.apis.GroupApi.quitGroup(msg.peerUid).then();
try {
const KickEvent = await this.obContext.apis.GroupApi.parseGroupKickEvent(msg.peerUid, element.grayTipElement);
if (KickEvent) return KickEvent;
} catch (e) {
return new OB11GroupDecreaseEvent(
this.core,
parseInt(msg.peerUid),
parseInt(this.core.selfInfo.uin),
0,
'leave',
);
}
}
} else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(
this.core,
parseInt(msg.peerUid), parseInt(msg.senderUin || ''),
{
id: FileNapCatOneBotUUID.encode({
chatType: ChatType.KCHATTYPEGROUP,
peerUid: msg.peerUid,
}, msg.msgId, element.elementId, element.fileElement.fileUuid, "." + element.fileElement.fileName),
url: pathToFileURL(element.fileElement.filePath).href,
name: element.fileElement.fileName,
size: parseInt(element.fileElement.fileSize),
busid: element.fileElement.fileBizId ?? 0,
},
);
}
if (element.grayTipElement) {
if (element.grayTipElement.xmlElement?.templId === '10382') {
const emojiLikeEvent = await this.obContext.apis.GroupApi.parseGroupEmojiLikeEventByGrayTip(msg.peerUid, element.grayTipElement);
if (emojiLikeEvent) return emojiLikeEvent;
}
if (element.grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) {
const GroupIncreaseEvent = await this.obContext.apis.GroupApi.parseGroupIncreaseEvent(msg.peerUid, element.grayTipElement);
if (GroupIncreaseEvent) return GroupIncreaseEvent;
}
else if (element.grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr);
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items;
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid);
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(
this.core,
parseInt(msg.peerUid),
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid),
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid),
pokedetail,
);
}
}
if (element.grayTipElement.jsonGrayTipElement.busiId == JsonGrayBusiId.AIO_GROUP_ESSENCE_MSG_TIP) {
const searchParams = new URL(json.items[0].jp).searchParams;
const msgSeq = searchParams.get('msgSeq')!;
const Group = searchParams.get('groupCode');
if (!Group) return;
// const businessId = searchParams.get('businessid');
const Peer = {
guildId: '',
chatType: ChatType.KCHATTYPEGROUP,
peerUid: Group,
};
const msgData = await this.core.apis.MsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true);
const msgList = (await this.core.apis.WebApi.getGroupEssenceMsgAll(Group)).flatMap((e) => e.data.msg_list);
const realMsg = msgList.find((e) => e.msg_seq.toString() == msgSeq);
return new OB11GroupEssenceEvent(
this.core,
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgData.msgList[0].msgId)!,
parseInt(msgData.msgList[0].senderUin),
parseInt(realMsg?.add_digest_uin ?? '0'),
);
// 获取MsgSeq+Peer可获取具体消息
}
if (element.grayTipElement.jsonGrayTipElement.busiId == JsonGrayBusiId.GROUP_AIO_CONFIGURABLE_GRAY_TIPS) {
const type = json.items[json.items.length - 1]?.txt;
if (type === "头衔") {
const memberUin = json.items[1].param[0];
const title = json.items[3].txt;
logger.logDebug('收到群成员新头衔消息', json);
return new OB11GroupTitleEvent(
this.core,
parseInt(msg.peerUid),
parseInt(memberUin),
title,
);
} else if (type === "移出") {
logger.logDebug('收到机器人被踢消息', json);
return;
} else {
logger.logWarn('收到未知的灰条消息', json);
}
}
}
}
}
}
async parseGroupBanEvent(GroupCode: string, grayTipElement: GrayTipElement) {
const groupElement = grayTipElement?.groupElement;
if (!groupElement?.shutUp) return undefined;
@@ -193,66 +66,66 @@ export class OneBotGroupApi {
return undefined;
}
async parseGroupIncreaseEvent(GroupCode: string, grayTipElement: GrayTipElement) {
this.core.context.logger.logDebug('收到新人被邀请进群消息', grayTipElement);
const xmlElement = grayTipElement.xmlElement;
if (xmlElement?.content) {
const regex = /jp="(\d+)"/g;
// async parseGroupIncreaseEvent(GroupCode: string, grayTipElement: GrayTipElement) {
// this.core.context.logger.logDebug('收到新人被邀请进群消息', grayTipElement);
// const xmlElement = grayTipElement.xmlElement;
// if (xmlElement?.content) {
// const regex = /jp="(\d+)"/g;
const matches = [];
let match = null;
// const matches = [];
// let match = null;
while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1]);
}
if (matches.length === 2) {
const [inviter, invitee] = matches;
return new OB11GroupIncreaseEvent(
this.core,
parseInt(GroupCode),
parseInt(invitee),
parseInt(inviter),
'invite',
);
}
}
return undefined;
}
// while ((match = regex.exec(xmlElement.content)) !== null) {
// matches.push(match[1]);
// }
// if (matches.length === 2) {
// const [inviter, invitee] = matches;
// return new OB11GroupIncreaseEvent(
// this.core,
// parseInt(GroupCode),
// parseInt(invitee),
// parseInt(inviter),
// 'invite',
// );
// }
// }
// return undefined;
// }
async parseGroupMemberIncreaseEvent(GroupCode: string, grayTipElement: GrayTipElement) {
const groupElement = grayTipElement?.groupElement;
if (!groupElement) return undefined;
const member = await this.core.apis.UserApi.getUserDetailInfo(groupElement.memberUid);
const memberUin = member?.uin;
const adminMember = await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid);
if (memberUin) {
const operatorUin = adminMember?.uin ?? memberUin;
return new OB11GroupIncreaseEvent(
this.core,
parseInt(GroupCode),
parseInt(memberUin),
parseInt(operatorUin),
);
} else {
return undefined;
}
}
// async parseGroupMemberIncreaseEvent(GroupCode: string, grayTipElement: GrayTipElement) {
// const groupElement = grayTipElement?.groupElement;
// if (!groupElement) return undefined;
// const member = await this.core.apis.UserApi.getUserDetailInfo(groupElement.memberUid);
// const memberUin = member?.uin;
// const adminMember = await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid);
// if (memberUin) {
// const operatorUin = adminMember?.uin ?? memberUin;
// return new OB11GroupIncreaseEvent(
// this.core,
// parseInt(GroupCode),
// parseInt(memberUin),
// parseInt(operatorUin),
// );
// } else {
// return undefined;
// }
// }
async parseGroupKickEvent(GroupCode: string, grayTipElement: GrayTipElement) {
const groupElement = grayTipElement?.groupElement;
if (!groupElement) return undefined;
const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid))?.uin ?? (await this.core.apis.UserApi.getUidByUinV2(groupElement.adminUid));
if (adminUin) {
return new OB11GroupDecreaseEvent(
this.core,
parseInt(GroupCode),
parseInt(this.core.selfInfo.uin),
parseInt(adminUin),
'kick_me',
);
}
return undefined;
}
// async parseGroupKickEvent(GroupCode: string, grayTipElement: GrayTipElement) {
// const groupElement = grayTipElement?.groupElement;
// if (!groupElement) return undefined;
// const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid))?.uin ?? (await this.core.apis.UserApi.getUidByUinV2(groupElement.adminUid));
// if (adminUin) {
// return new OB11GroupDecreaseEvent(
// this.core,
// parseInt(GroupCode),
// parseInt(this.core.selfInfo.uin),
// parseInt(adminUin),
// 'kick_me',
// );
// }
// return undefined;
// }
async parseGroupEmojiLikeEventByGrayTip(
groupCode: string,
@@ -286,7 +159,7 @@ export class OneBotGroupApi {
}
const replyMsg = replyMsgList[0];
if (!replyMsg) {
this.core.context.logger.logError.bind(this.core.context.logger)('解析表情回应消息失败: 未找到回应消息');
this.core.context.logger.logError('解析表情回应消息失败: 未找到回应消息');
return undefined;
}
return new OB11GroupMsgEmojiLikeEvent(
@@ -300,4 +173,150 @@ export class OneBotGroupApi {
}],
);
}
async parseCardChangedEvent(msg: RawMessage) {
if (msg.senderUin && msg.senderUin !== '0') {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
if (member && member.cardName !== msg.sendMemberName) {
const newCardName = msg.sendMemberName ?? '';
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
member.cardName = newCardName;
return event;
}
}
return undefined;
}
// async parseGroupElement(msg: RawMessage, groupElement: TipGroupElement, elementWrapper: GrayTipElement) {
// if (groupElement.type == TipGroupElementType.KMEMBERADD) {
// const MemberIncreaseEvent = await this.obContext.apis.GroupApi.parseGroupMemberIncreaseEvent(msg.peerUid, elementWrapper);
// if (MemberIncreaseEvent) return MemberIncreaseEvent;
// } else if (groupElement.type === TipGroupElementType.KSHUTUP) {
// const BanEvent = await this.obContext.apis.GroupApi.parseGroupBanEvent(msg.peerUid, elementWrapper);
// if (BanEvent) return BanEvent;
// } else if (groupElement.type == TipGroupElementType.KQUITTE) {
// this.core.apis.GroupApi.quitGroup(msg.peerUid).then();
// try {
// const KickEvent = await this.obContext.apis.GroupApi.parseGroupKickEvent(msg.peerUid, elementWrapper);
// if (KickEvent) return KickEvent;
// } catch (e) {
// return new OB11GroupDecreaseEvent(
// this.core,
// parseInt(msg.peerUid),
// parseInt(this.core.selfInfo.uin),
// 0,
// 'leave',
// );
// }
// }
// return undefined;
// }
async parsePaiYiPai(msg: RawMessage, jsonStr: string) {
const json = JSON.parse(jsonStr);
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items;
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid);
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(
this.core,
parseInt(msg.peerUid),
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid),
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid),
pokedetail,
);
}
return undefined;
}
async parseOtherJsonEvent(msg: RawMessage, jsonStr: string, context: InstanceContext) {
const json = JSON.parse(jsonStr);
const type = json.items[json.items.length - 1]?.txt;
if (type === "头衔") {
const memberUin = json.items[1].param[0];
const title = json.items[3].txt;
context.logger.logDebug('收到群成员新头衔消息', json);
return new OB11GroupTitleEvent(
this.core,
parseInt(msg.peerUid),
parseInt(memberUin),
title,
);
} else if (type === "移出") {
context.logger.logDebug('收到机器人被踢消息', json);
return;
} else {
context.logger.logWarn('收到未知的灰条消息', json);
}
}
async parseEssenceMsg(msg: RawMessage, jsonStr: string) {
const json = JSON.parse(jsonStr);
const searchParams = new URL(json.items[0].jp).searchParams;
const msgSeq = searchParams.get('msgSeq')!;
const Group = searchParams.get('groupCode');
if (!Group) return;
// const businessId = searchParams.get('businessid');
const Peer = {
guildId: '',
chatType: ChatType.KCHATTYPEGROUP,
peerUid: Group,
};
const msgData = await this.core.apis.MsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true);
const msgList = (await this.core.apis.WebApi.getGroupEssenceMsgAll(Group)).flatMap((e) => e.data.msg_list);
const realMsg = msgList.find((e) => e.msg_seq.toString() == msgSeq);
return new OB11GroupEssenceEvent(
this.core,
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgData.msgList[0].msgId)!,
parseInt(msgData.msgList[0].senderUin),
parseInt(realMsg?.add_digest_uin ?? '0'),
);
// 获取MsgSeq+Peer可获取具体消息
}
async parseGroupUploadFileEvene(msg: RawMessage, element: FileElement, elementWrapper: MessageElement) {
return new OB11GroupUploadNoticeEvent(
this.core,
parseInt(msg.peerUid), parseInt(msg.senderUin || ''),
{
id: FileNapCatOneBotUUID.encode({
chatType: ChatType.KCHATTYPEGROUP,
peerUid: msg.peerUid,
}, msg.msgId, elementWrapper.elementId, elementWrapper?.fileElement?.fileUuid, "." + element.fileName),
url: pathToFileURL(element.filePath).href,
name: element.fileName,
size: parseInt(element.fileSize),
busid: element.fileBizId ?? 0,
},
);
}
async parseGrayTipElement(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
// 解析群组事件 由sysmsg解析
// return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) {
// 筛选出表情回应 事件
if (grayTipElement.xmlElement?.templId === '10382') {
return await this.obContext.apis.GroupApi.parseGroupEmojiLikeEventByGrayTip(msg.peerUid, grayTipElement);
} else {
//return await this.obContext.apis.GroupApi.parseGroupIncreaseEvent(msg.peerUid, grayTipElement);
}
} else if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
// 解析json事件
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
return await this.parsePaiYiPai(msg, grayTipElement.jsonGrayTipElement.jsonStr);
} else if (grayTipElement.jsonGrayTipElement.busiId == JsonGrayBusiId.AIO_GROUP_ESSENCE_MSG_TIP) {
return await this.parseEssenceMsg(msg, grayTipElement.jsonGrayTipElement.jsonStr);
} else {
return await this.parseOtherJsonEvent(msg, grayTipElement.jsonGrayTipElement.jsonStr, this.core.context);
}
}
return undefined;
}
}

View File

@@ -17,6 +17,7 @@ import {
SendTextElement,
BaseEmojiType,
FaceType,
GrayTipElement,
} from '@/core';
import faceConfig from '@/core/external/face_config.json';
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, } from '@/onebot';
@@ -31,6 +32,10 @@ import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNot
// import { decodeSysMessage } from '@/core/packet/proto/old/ProfileLike';
import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
import { decodeSysMessage } from "@/core/helper/adaptDecoder";
import { GroupChange, PushMsgBody } from "@/core/packet/transformer/proto";
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent';
type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -113,6 +118,7 @@ export class OneBotMsgApi {
return {
type: OB11MessageDataType.image,
data: {
pic_type: element.picType,
summary: element.summary,
file: encodedFileId,
sub_type: element.picSubType,
@@ -124,7 +130,7 @@ export class OneBotMsgApi {
},
};
} catch (e: any) {
this.core.context.logger.logError.bind(this.core.context.logger)('获取图片url失败', e.stack);
this.core.context.logger.logError('获取图片url失败', e.stack);
return null;
}
},
@@ -207,7 +213,7 @@ export class OneBotMsgApi {
guildId: '',
};
if (!records || !element.replyMsgTime || !element.senderUidStr) {
this.core.context.logger.logError.bind(this.core.context.logger)('似乎是旧版客户端,获取不到引用的消息', element.replayMsgSeq);
this.core.context.logger.logError('似乎是旧版客户端,获取不到引用的消息', element.replayMsgSeq);
return null;
}
@@ -225,7 +231,7 @@ export class OneBotMsgApi {
let replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logError.bind(this.core.context.logger)(
this.core.context.logger.logError(
'筛选结果,筛选消息失败,将使用Fallback-1 Seq: ',
element.replayMsgSeq,
',消息长度:',
@@ -236,7 +242,7 @@ export class OneBotMsgApi {
}
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logWarn.bind(this.core.context.logger)(
this.core.context.logger.logWarn(
'筛选消息失败,将使用Fallback-2 Seq:',
element.replayMsgSeq,
',消息长度:',
@@ -250,7 +256,7 @@ export class OneBotMsgApi {
// 丢弃该消息段
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logError.bind(this.core.context.logger)(
this.core.context.logger.logError(
'最终筛选结果,筛选消息失败,获取不到引用的消息 Seq: ',
element.replayMsgSeq,
',消息长度:',
@@ -452,7 +458,7 @@ export class OneBotMsgApi {
const sysFaces = faceConfig.sysface;
const face: any = sysFaces.find((systemFace) => systemFace.QSid === parsedFaceId.toString());
if (!face) {
this.core.context.logger.logError.bind(this.core.context.logger)('不支持的ID', id);
this.core.context.logger.logError('不支持的ID', id);
return undefined;
}
parsedFaceId = parseInt(parsedFaceId.toString());
@@ -575,20 +581,20 @@ export class OneBotMsgApi {
// 保留, 直到...找到更好的解决方案
if (data.id !== undefined) {
if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) {
this.core.context.logger.logError.bind(this.core.context.logger)('音乐卡片type错误, 只支持qq、163、kugou、kuwo、migu当前type:', data.type);
this.core.context.logger.logError('音乐卡片type错误, 只支持qq、163、kugou、kuwo、migu当前type:', data.type);
return undefined;
}
} else {
if (!['qq', '163', 'kugou', 'kuwo', 'migu', 'custom'].includes(data.type)) {
this.core.context.logger.logError.bind(this.core.context.logger)('音乐卡片type错误, 只支持qq、163、kugou、kuwo、migu、custom当前type:', data.type);
this.core.context.logger.logError('音乐卡片type错误, 只支持qq、163、kugou、kuwo、migu、custom当前type:', data.type);
return undefined;
}
if (!data.url) {
this.core.context.logger.logError.bind(this.core.context.logger)('自定义音卡缺少参数url');
this.core.context.logger.logError('自定义音卡缺少参数url');
return undefined;
}
if (!data.image) {
this.core.context.logger.logError.bind(this.core.context.logger)('自定义音卡缺少参数image');
this.core.context.logger.logError('自定义音卡缺少参数image');
return undefined;
}
}
@@ -612,7 +618,7 @@ export class OneBotMsgApi {
type: OB11MessageDataType.json
}, context);
} catch (e) {
this.core.context.logger.logError.bind(this.core.context.logger)('生成音乐消息失败', e);
this.core.context.logger.logError('生成音乐消息失败', e);
}
},
@@ -663,20 +669,13 @@ export class OneBotMsgApi {
this.core = core;
}
async parsePrivateMsgEvent(msg: RawMessage) {
if (msg.chatType !== ChatType.KCHATTYPEC2C) {
return;
}
for (const element of msg.elements) {
if (element.grayTipElement && element.grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(element.grayTipElement);
if (PokeEvent) return PokeEvent;
}
//好友添加成功事件
if (element.grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
}
async parsePrivateMsgEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement);
if (PokeEvent) { return PokeEvent; };
} else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
}
}
}
@@ -726,8 +725,33 @@ export class OneBotMsgApi {
) {
if (msg.senderUin == '0' || msg.senderUin == '') return;
if (msg.peerUin == '0' || msg.peerUin == '') return;
//跳过空消息
const resMsg: OB11Message = {
const resMsg = this.initializeMessage(msg);
if (this.core.selfInfo.uin == msg.senderUin) {
resMsg.message_sent_type = 'self';
}
if (msg.chatType == ChatType.KCHATTYPEGROUP) {
await this.handleGroupMessage(resMsg, msg);
} else if (msg.chatType == ChatType.KCHATTYPEC2C) {
await this.handlePrivateMessage(resMsg, msg);
} else if (msg.chatType == ChatType.KCHATTYPETEMPC2CFROMGROUP) {
await this.handleTempGroupMessage(resMsg, msg);
} else {
return undefined;
}
const validSegments = await this.parseMessageSegments(msg, parseMultMsg);
resMsg.message = validSegments;
resMsg.raw_message = validSegments.map(msg => encodeCQCode(msg)).join('').trim();
const stringMsg = await this.convertArrayToStringMessage(resMsg);
return { stringMsg, arrayMsg: resMsg };
}
private initializeMessage(msg: RawMessage): OB11Message {
return {
self_id: parseInt(this.core.selfInfo.uin),
user_id: parseInt(msg.senderUin),
time: parseInt(msg.msgTime) || Date.now(),
@@ -747,37 +771,40 @@ export class OneBotMsgApi {
message_format: 'array',
post_type: this.core.selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
};
if (this.core.selfInfo.uin == msg.senderUin) {
resMsg.message_sent_type = 'self';
}
if (msg.chatType == ChatType.KCHATTYPEGROUP) {
resMsg.sub_type = 'normal'; // 这里go-cqhttp是group而onebot11标准是normal, 蛋疼
resMsg.group_id = parseInt(msg.peerUin);
let member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
if (!member) member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
if (member) {
resMsg.sender.role = OB11Construct.groupMemberRole(member.role);
resMsg.sender.nickname = member.nick;
}
} else if (msg.chatType == ChatType.KCHATTYPEC2C) {
resMsg.sub_type = 'friend';
resMsg.sender.nickname = (await this.core.apis.UserApi.getUserDetailInfo(msg.senderUid)).nick;
} else if (msg.chatType == ChatType.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group';
const ret = await this.core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid);
if (ret.result === 0) {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode);
resMsg.sender.nickname = member?.nick ?? member?.cardName ?? '临时会话';
resMsg.temp_source = resMsg.group_id;
} else {
resMsg.group_id = 284840486; //兜底数据
resMsg.temp_source = resMsg.group_id;
resMsg.sender.nickname = '临时会话';
}
}
}
// 处理消息段
private async handleGroupMessage(resMsg: OB11Message, msg: RawMessage) {
resMsg.sub_type = 'normal';
resMsg.group_id = parseInt(msg.peerUin);
let member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
if (!member) member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
if (member) {
resMsg.sender.role = OB11Construct.groupMemberRole(member.role);
resMsg.sender.nickname = member.nick;
}
}
private async handlePrivateMessage(resMsg: OB11Message, msg: RawMessage) {
resMsg.sub_type = 'friend';
resMsg.sender.nickname = (await this.core.apis.UserApi.getUserDetailInfo(msg.senderUid)).nick;
}
private async handleTempGroupMessage(resMsg: OB11Message, msg: RawMessage) {
resMsg.sub_type = 'group';
const ret = await this.core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid);
if (ret.result === 0) {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode);
resMsg.sender.nickname = member?.nick ?? member?.cardName ?? '临时会话';
resMsg.temp_source = resMsg.group_id;
} else {
resMsg.group_id = 284840486;
resMsg.temp_source = resMsg.group_id;
resMsg.sender.nickname = '临时会话';
}
}
private async parseMessageSegments(msg: RawMessage, parseMultMsg: boolean): Promise<OB11MessageData[]> {
const msgSegments = await Promise.allSettled(msg.elements.map(
async (element) => {
for (const key in element) {
@@ -792,43 +819,41 @@ export class OneBotMsgApi {
element[key],
msg,
element,
{
parseMultMsg: parseMultMsg
}
{ parseMultMsg }
);
// 对于 face 类型的消息,检查是否存在
if (key === 'faceElement' && !parsedElement) {
return null; // 如果没有找到对应的表情,返回 null
return null;
}
return parsedElement;
}
}
},
));
// 过滤掉无效的消息段
const validSegments = msgSegments.filter(entry => {
return msgSegments.filter(entry => {
if (entry.status === 'fulfilled') {
return !!entry.value;
} else {
this.core.context.logger.logError.bind(this.core.context.logger)('消息段解析失败', entry.reason);
this.core.context.logger.logError('消息段解析失败', entry.reason);
return false;
}
}).map((entry) => (<PromiseFulfilledResult<OB11MessageData>>entry).value).filter(value => value != null);
const msgAsCQCode = validSegments.map(msg => encodeCQCode(msg)).join('').trim();
resMsg.message = validSegments;
resMsg.raw_message = msgAsCQCode;
let stringMsg = structuredClone(resMsg);
stringMsg = await this.importArrayTostringMsg(stringMsg);
return { stringMsg: stringMsg, arrayMsg: resMsg };
}
async importArrayTostringMsg(msg: OB11Message) {
private async convertArrayToStringMessage(originMsg: OB11Message): Promise<OB11Message> {
const msg = structuredClone(originMsg);
msg.message_format = 'string';
msg.message = msg.raw_message;
return msg;
}
async importArrayTostringMsg(originMsg: OB11Message) {
const msg = structuredClone(originMsg);
msg.message_format = 'string';
msg.message = msg.raw_message;
return msg;
}
async createSendElements(
messageData: OB11MessageData[],
peer: Peer,
@@ -882,7 +907,7 @@ export class OneBotMsgApi {
timeout += PredictTime;// 10S Basic Timeout + PredictTime( For File 512kb/s )
}
} catch (e) {
this.core.context.logger.logError.bind(this.core.context.logger)('发送消息计算预计时间异常', e);
this.core.context.logger.logError('发送消息计算预计时间异常', e);
}
const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
if (!returnMsg) throw new Error('发送消息失败');
@@ -891,11 +916,19 @@ export class OneBotMsgApi {
guildId: '',
peerUid: peer.peerUid,
}, returnMsg.msgId);
setTimeout(() => {
deleteAfterSentFiles.forEach(file => {
fsPromise.unlink(file).then().catch(e => this.core.context.logger.logError.bind(this.core.context.logger)('发送消息删除文件失败', e));
try {
if (fs.existsSync(file)) {
fsPromise.unlink(file).then().catch(e => this.core.context.logger.logError('发送消息删除文件失败', e));
}
} catch (error) {
this.core.context.logger.logError('发送消息删除文件失败', (error as Error).message);
}
});
}, 60000);
return returnMsg;
}
@@ -905,7 +938,7 @@ export class OneBotMsgApi {
) {
const realUri = inputdata.url || inputdata.file || inputdata.path || '';
if (realUri.length === 0) {
this.core.context.logger.logError.bind(this.core.context.logger)('文件消息缺少参数', inputdata);
this.core.context.logger.logError('文件消息缺少参数', inputdata);
throw Error('文件消息缺少参数');
}
const {
@@ -916,7 +949,7 @@ export class OneBotMsgApi {
} = (await uri2local(this.core.NapCatTempPath, realUri));
if (!success) {
this.core.context.logger.logError.bind(this.core.context.logger)('文件下载失败', errMsg);
this.core.context.logger.logError('文件下载失败', errMsg);
throw Error('文件下载失败' + errMsg);
}
@@ -924,17 +957,45 @@ export class OneBotMsgApi {
return { path, fileName: inputdata.name ?? fileName };
}
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
switch (type) {
case 130:
return 'leave';
case 131:
return 'kick';
case 3:
return 'kick_me';
default:
return 'kick';
}
}
async parseSysMessage(msg: number[]) {
const sysMsg = decodeSysMessage(Uint8Array.from(msg));
if (sysMsg.msgSpec.length === 0) {
return;
}
const { msgType, subType, subSubType } = sysMsg.msgSpec[0];
if (msgType === 528 && subType === 39 && subSubType === 39) {
if (!sysMsg.bodyWrapper) return;
return await this.obContext.apis.UserApi.parseLikeEvent(sysMsg.bodyWrapper.wrappedBody);
// Todo Refactor
const SysMessage = new NapProtoMsg(PushMsgBody).decode(Uint8Array.from(msg));
if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
console.log(JSON.stringify(groupChange));
return new OB11GroupIncreaseEvent(
this.core,
groupChange.groupUin,
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
groupChange.decreaseType == 131 ? 'invite' : 'approve',
);
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
return new OB11GroupDecreaseEvent(
this.core,
groupChange.groupUin,
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
this.groupChangDecreseType2String(groupChange.decreaseType),
);
} else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
}
/*
if (msgType === 732 && subType === 16 && subSubType === 16) {
const greyTip = GreyTipWrapper.fromBinary(Uint8Array.from(sysMsg.bodyWrapper!.wrappedBody.slice(7)));

View File

@@ -24,20 +24,19 @@ export class OneBotQuickActionApi {
}
async handleQuickOperation(eventContext: QuickActionEvent, quickAction: QuickAction) {
const logger = this.core.context.logger;
if (eventContext.post_type === 'message') {
await this.handleMsg(eventContext as OB11Message, quickAction)
.catch(logger.logError.bind(logger));
.catch(e => this.core.context.logger.logError(e));
}
if (eventContext.post_type === 'request') {
const friendRequest = eventContext as OB11FriendRequestEvent;
const groupRequest = eventContext as OB11GroupRequestEvent;
if ((friendRequest).request_type === 'friend') {
await this.handleFriendRequest(friendRequest, quickAction)
.catch(logger.logError.bind(logger));
.catch(e => this.core.context.logger.logError(e));
} else if (groupRequest.request_type === 'group') {
await this.handleGroupRequest(groupRequest, quickAction)
.catch(logger.logError.bind(logger));
.catch(e => this.core.context.logger.logError(e));
}
}
}
@@ -51,7 +50,7 @@ export class OneBotQuickActionApi {
group_id: msg.group_id?.toString(),
user_id: msg.user_id?.toString(),
}, peerContextMode);
if (reply) {
// let group: Group | undefined;
let replyMessage: OB11MessageData[] = [];
@@ -78,7 +77,7 @@ export class OneBotQuickActionApi {
sendElements,
deleteAfterSentFiles,
} = await this.obContext.apis.MsgApi.createSendElements(replyMessage, peer);
this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles, false).then().catch(this.core.context.logger.logError.bind(this.core.context.logger));
this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles, false).then().catch(e => this.core.context.logger.logError(e));
}
}
@@ -88,13 +87,13 @@ export class OneBotQuickActionApi {
request.flag,
quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
quickAction.reason,
).catch(this.core.context.logger.logError.bind(this.core.context.logger));
).catch(e => this.core.context.logger.logError(e));
}
}
async handleFriendRequest(request: OB11FriendRequestEvent, quickAction: QuickActionFriendRequest) {
if (!isNull(quickAction.approve)) {
this.core.apis.FriendApi.handleFriendRequest(request.flag, !!quickAction.approve).then().catch(this.core.context.logger.logError.bind(this.core.context.logger));
this.core.apis.FriendApi.handleFriendRequest(request.flag, !!quickAction.approve).then().catch(e => this.core.context.logger.logError(e));
}
}
}

View File

@@ -1,4 +1,4 @@
import { NapCatCore } from '@/core';
import { GrayTipRovokeElement, NapCatCore, RawMessage } from '@/core';
import { NapCatOneBot11Adapter } from '@/onebot';
import { OB11ProfileLikeEvent } from '@/onebot/event/notice/OB11ProfileLikeEvent';
import { decodeProfileLikeTip } from "@/core/helper/adaptDecoder";
@@ -11,9 +11,9 @@ export class OneBotUserApi {
this.obContext = obContext;
this.core = core;
}
async parseLikeEvent(wrappedBody: Uint8Array): Promise<OB11ProfileLikeEvent | undefined> {
const likeTip = decodeProfileLikeTip(Uint8Array.from(wrappedBody));
const likeTip = decodeProfileLikeTip(wrappedBody);
if (likeTip?.msgType !== 0 || likeTip?.subType !== 203) return;
this.core.context.logger.logDebug("收到点赞通知消息");
const likeMsg = likeTip.content.msg;

View File

@@ -13,6 +13,8 @@ import {
Peer,
RawMessage,
SendStatusType,
NTMsgType,
MessageElement,
} from '@/core';
import { OB11ConfigLoader } from '@/onebot/config';
import {
@@ -40,7 +42,7 @@ import { MessageUnique } from '@/common/message-unique';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { OB11FriendRequestEvent } from '@/onebot/event/request/OB11FriendRequest';
import { OB11GroupAdminNoticeEvent } from '@/onebot/event/notice/OB11GroupAdminNoticeEvent';
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '@/onebot/event/notice/OB11GroupDecreaseEvent';
// import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '@/onebot/event/notice/OB11GroupDecreaseEvent';
import { OB11GroupRequestEvent } from '@/onebot/event/request/OB11GroupRequest';
import { OB11FriendRecallNoticeEvent } from '@/onebot/event/notice/OB11FriendRecallNoticeEvent';
import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecallNoticeEvent';
@@ -103,7 +105,7 @@ export class NapCatOneBot11Adapter {
selfInfo.nick = user.nick;
this.context.logger.setLogSelfInfo(selfInfo);
})
.catch(this.context.logger.logError.bind(this.context.logger));
.catch(e => this.context.logger.logError(e));
const serviceInfo = await this.creatOneBotLog(ob11Config);
this.context.logger.log(`[Notice] [OneBot11] ${serviceInfo}`);
@@ -193,6 +195,16 @@ export class NapCatOneBot11Adapter {
nowConfig: NetworkConfigAdapter[],
adapterClass: new (...args: any[]) => IOB11NetworkAdapter
): Promise<void> {
// 比较旧的在新的找不到的回收
for (const adapterConfig of prevConfig) {
const existingAdapter = nowConfig.find((e) => e.name === adapterConfig.name);
if (!existingAdapter) {
const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name);
if (existingAdapter) {
await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]);
}
}
}
// 通知新配置重载 删除关闭的 加入新开的
for (const adapterConfig of nowConfig) {
const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name);
@@ -201,21 +213,11 @@ export class NapCatOneBot11Adapter {
if (networkChange === OB11NetworkReloadType.NetWorkClose) {
await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]);
}
} else {
} else if (adapterConfig.enable) {
const newAdapter = new adapterClass(adapterConfig.name, adapterConfig, this.core, this.actions);
await this.networkManager.registerAdapterAndOpen(newAdapter);
}
}
// 比较旧的找不到的回收
for (const adapterConfig of prevConfig) {
const existingAdapter = nowConfig.find((e) => e.name === adapterConfig.name);
if (!existingAdapter) {
const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name);
if (existingAdapter) {
await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]);
}
}
}
}
private initMsgListener() {
@@ -226,7 +228,7 @@ export class NapCatOneBot11Adapter {
if (event) this.networkManager.emitEvent(event);
})
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)(
this.context.logger.logError(
'constructSysMessage error: ',
e,
'\n Parse Hex:',
@@ -258,35 +260,54 @@ export class NapCatOneBot11Adapter {
m.msgId
);
await this.emitMsg(m).catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理消息失败', e)
this.context.logger.logError('处理消息失败', e)
);
}
};
const msgIdSend = new LRUCache<string, number>(100);
const recallMsgs = new LRUCache<string, boolean>(100);
msgListener.onAddSendMsg = async (msg) => {
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
msgIdSend.put(msg.msgId, 0);
try {
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
const [updatemsgs] = await this.core.eventWrapper.registerListen('NodeIKernelMsgListener/onMsgInfoListUpdate', (msgList: RawMessage[]) => {
const report = msgList.find((e) =>
e.senderUin == this.core.selfInfo.uin && e.sendStatus !== SendStatusType.KSEND_STATUS_SENDING && e.msgId === msg.msgId
);
return !!report;
}, 1, 10 * 60 * 1000);
// 10分钟 超时
const updatemsg = updatemsgs.find((e) => e.msgId === msg.msgId);
if (updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS || updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS_NOSEQ) {
updatemsg.id = MessageUnique.createUniqueMsgId(
{
chatType: updatemsg.chatType,
peerUid: updatemsg.peerUid,
guildId: '',
},
updatemsg.msgId
);
this.emitMsg(updatemsg);
}
}
} catch (error) {
this.context.logger.logError('处理发送消息失败', error);
}
};
msgListener.onMsgInfoListUpdate = async (msgList) => {
this.emitRecallMsg(msgList, recallMsgs).catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理消息失败', e)
);
for (const msg of msgList.filter((e) => e.senderUin == this.core.selfInfo.uin)) {
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS && msgIdSend.get(msg.msgId) == 0) {
msgIdSend.put(msg.msgId, 1);
// 完成后再post
msg.id = MessageUnique.createUniqueMsgId(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
},
msg.msgId
);
this.emitMsg(msg);
msgListener.onMsgRecall = async (chatType: ChatType, uid: string, msgSeq: string) => {
const peer: Peer = {
chatType: chatType,
peerUid: uid,
guildId: ''
};
const msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
const element = msg?.elements[0];
if (msg && element) {
const recallEvent = await this.emitRecallMsg(msg, element);
try {
if (recallEvent) {
await this.networkManager.emitEvent(recallEvent);
}
} catch (e) {
this.context.logger.logError('处理消息撤回失败', e);
}
}
};
@@ -294,7 +315,7 @@ export class NapCatOneBot11Adapter {
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);
this.networkManager
.emitEvent(event)
.catch((e) => this.context.logger.logError.bind(this.context.logger)('处理Bot掉线失败', e));
.catch((e) => this.context.logger.logError('处理Bot掉线失败', e));
};
this.context.session.getMsgService().addKernelMsgListener(proxiedListenerOf(msgListener, this.context.logger));
}
@@ -383,7 +404,7 @@ export class NapCatOneBot11Adapter {
this.networkManager
.emitEvent(groupAdminNoticeEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e)
this.context.logger.logError('处理群管理员变动失败', e)
);
} else {
this.context.logger.logDebug(
@@ -392,102 +413,104 @@ export class NapCatOneBot11Adapter {
this.core.apis.GroupApi.getGroup(notify.group.groupCode)
);
}
} else if (
notify.type == GroupNotifyMsgType.MEMBER_LEAVE_NOTIFY_ADMIN ||
notify.type == GroupNotifyMsgType.KICK_MEMBER_NOTIFY_ADMIN
) {
this.context.logger.logDebug('有成员退出通知', notify);
const member1Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
let operatorId = member1Uin;
let subType: GroupDecreaseSubType = 'leave';
if (notify.user2.uid) {
// 是被踢的
const member2Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid);
if (member2Uin) {
operatorId = member2Uin;
} else
// if (
// notify.type == GroupNotifyMsgType.MEMBER_LEAVE_NOTIFY_ADMIN ||
// notify.type == GroupNotifyMsgType.KICK_MEMBER_NOTIFY_ADMIN
// ) {
// this.context.logger.logDebug('有成员退出通知', notify);
// const member1Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
// let operatorId = member1Uin;
// let subType: GroupDecreaseSubType = 'leave';
// if (notify.user2.uid) {
// // 是被踢的
// const member2Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid);
// if (member2Uin) {
// operatorId = member2Uin;
// }
// subType = 'kick';
// }
// const groupDecreaseEvent = new OB11GroupDecreaseEvent(
// this.core,
// parseInt(notify.group.groupCode),
// parseInt(member1Uin),
// parseInt(operatorId),
// subType
// );
// this.networkManager
// .emitEvent(groupDecreaseEvent)
// .catch((e) =>
// this.context.logger.logError('处理群成员退出失败', e)
// );
// // notify.status == 1 表示未处理 2表示处理完成
// } else
if (
[GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) &&
notify.status == GroupNotifyMsgStatus.KUNHANDLE
) {
this.context.logger.logDebug('有加群请求');
try {
let requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
if (isNaN(parseInt(requestUin))) {
requestUin = (await this.core.apis.UserApi.getUserDetailInfo(notify.user1.uid)).uin;
}
const groupRequestEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(requestUin),
'add',
notify.postscript,
flag
);
this.networkManager
.emitEvent(groupRequestEvent)
.catch((e) =>
this.context.logger.logError('处理加群请求失败', e)
);
} catch (e) {
this.context.logger.logError(
'获取加群人QQ号失败 Uid:',
notify.user1.uid,
e
);
}
subType = 'kick';
}
const groupDecreaseEvent = new OB11GroupDecreaseEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType
);
this.networkManager
.emitEvent(groupDecreaseEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理群成员退出失败', e)
);
// notify.status == 1 表示未处理 2表示处理完成
} else if (
[GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) &&
notify.status == GroupNotifyMsgStatus.KUNHANDLE
) {
this.context.logger.logDebug('有加群请求');
try {
let requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
if (isNaN(parseInt(requestUin))) {
requestUin = (await this.core.apis.UserApi.getUserDetailInfo(notify.user1.uid)).uin;
}
const groupRequestEvent = new OB11GroupRequestEvent(
} else if (
notify.type == GroupNotifyMsgType.INVITED_BY_MEMBER &&
notify.status == GroupNotifyMsgStatus.KUNHANDLE
) {
this.context.logger.logDebug(`收到邀请我加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(requestUin),
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid)),
'invite',
notify.postscript,
flag
);
this.networkManager
.emitEvent(groupInviteEvent)
.catch((e) =>
this.context.logger.logError('处理邀请本人加群失败', e)
);
} else if (
notify.type == GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS &&
notify.status == GroupNotifyMsgStatus.KUNHANDLE
) {
this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid)),
'add',
notify.postscript,
flag
);
this.networkManager
.emitEvent(groupRequestEvent)
.emitEvent(groupInviteEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理加群请求失败', e)
this.context.logger.logError('处理邀请本人加群失败', e)
);
} catch (e) {
this.context.logger.logError.bind(this.context.logger)(
'获取加群人QQ号失败 Uid:',
notify.user1.uid,
e
);
}
} else if (
notify.type == GroupNotifyMsgType.INVITED_BY_MEMBER &&
notify.status == GroupNotifyMsgStatus.KUNHANDLE
) {
this.context.logger.logDebug(`收到邀请我加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid)),
'invite',
notify.postscript,
flag
);
this.networkManager
.emitEvent(groupInviteEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e)
);
} else if (
notify.type == GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS &&
notify.status == GroupNotifyMsgStatus.KUNHANDLE
) {
this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid)),
'add',
notify.postscript,
flag
);
this.networkManager
.emitEvent(groupInviteEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e)
);
}
}
}
};
@@ -510,10 +533,10 @@ export class NapCatOneBot11Adapter {
this.networkManager
.emitEvent(groupAdminNoticeEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e)
this.context.logger.logError('处理群管理员变动失败', e)
);
existMember.isChangeRole = false;
this.context.logger.logDebug.bind(this.context.logger)('群管理员变动处理完毕');
this.context.logger.logDebug('群管理员变动处理完毕');
});
}
};
@@ -526,10 +549,12 @@ export class NapCatOneBot11Adapter {
private async emitMsg(message: RawMessage) {
const network = Object.values(this.configLoader.configData.network).flat() as Array<AdapterConfigWrap>;
this.context.logger.logDebug('收到新消息 RawMessage', message);
await this.handleMsg(message, network);
await this.handleGroupEvent(message);
await this.handlePrivateMsgEvent(message);
await Promise.allSettled([
this.handleMsg(message, network),
message.chatType == ChatType.KCHATTYPEGROUP ? this.handleGroupEvent(message) : this.handlePrivateMsgEvent(message)
]);
}
private async handleMsg(message: RawMessage, network: Array<AdapterConfigWrap>) {
try {
const ob11Msg = await this.apis.MsgApi.parseMessageV2(message, this.configLoader.configData.parseMultMsg);
@@ -597,9 +622,25 @@ export class NapCatOneBot11Adapter {
private async handleGroupEvent(message: RawMessage) {
try {
const groupEvent = await this.apis.GroupApi.parseGroupEvent(message);
if (groupEvent) {
this.networkManager.emitEvent(groupEvent);
// 群名片修改事件解析 任何都该判断
if (message.senderUin && message.senderUin !== '0') {
const cardChangedEvent = await this.apis.GroupApi.parseCardChangedEvent(message);
cardChangedEvent && await this.networkManager.emitEvent(cardChangedEvent);
}
if (message.msgType === NTMsgType.KMSGTYPEFILE) {
// 文件为单元素消息
const elementWrapper = message.elements.find(e => !!e.fileElement);
if (elementWrapper?.fileElement) {
const uploadGroupFileEvent = await this.apis.GroupApi.parseGroupUploadFileEvene(message, elementWrapper.fileElement, elementWrapper);
uploadGroupFileEvent && await this.networkManager.emitEvent(uploadGroupFileEvent);
}
} else if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) {
// 灰条为单元素消息
const grayTipElement = message.elements[0].grayTipElement;
if (grayTipElement) {
const event = await this.apis.GroupApi.parseGrayTipElement(message, grayTipElement);
event && await this.networkManager.emitEvent(event);
}
}
} catch (e) {
this.context.logger.logError('constructGroupEvent error: ', e);
@@ -608,59 +649,51 @@ export class NapCatOneBot11Adapter {
private async handlePrivateMsgEvent(message: RawMessage) {
try {
const privateEvent = await this.apis.MsgApi.parsePrivateMsgEvent(message);
if (privateEvent) {
this.networkManager.emitEvent(privateEvent);
if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) {
// 灰条为单元素消息
const grayTipElement = message.elements[0].grayTipElement;
if (grayTipElement) {
const event = await this.apis.MsgApi.parsePrivateMsgEvent(message, grayTipElement);
event && await this.networkManager.emitEvent(event);
}
}
} catch (e) {
this.context.logger.logError('constructPrivateEvent error: ', e);
}
}
private async emitRecallMsg(msgList: RawMessage[], cache: LRUCache<string, boolean>) {
for (const message of msgList) {
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
const peer: Peer = { chatType: message.chatType, peerUid: message.peerUid, guildId: '' };
if (message.recallTime != '0' && !cache.get(message.msgId)) {
//TODO: 这个判断方法不太好,应该使用灰色消息元素来判断?
cache.put(message.msgId, true);
// 撤回消息上报
let oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId);
if (!oriMessageId) {
oriMessageId = MessageUnique.createUniqueMsgId(peer, message.msgId);
}
if (message.chatType == ChatType.KCHATTYPEC2C) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(
this.core,
+message.senderUin,
oriMessageId
);
this.networkManager
.emitEvent(friendRecallEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理好友消息撤回失败', e)
);
} else if (message.chatType == ChatType.KCHATTYPEGROUP) {
let operatorId = message.senderUin;
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid;
if (!operatorUid) return;
const operator = await this.core.apis.GroupApi.getGroupMember(message.peerUin, operatorUid);
operatorId = operator?.uin ?? message.senderUin;
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
this.core,
+message.peerUin,
+message.senderUin,
+operatorId,
oriMessageId
);
this.networkManager
.emitEvent(groupRecallEvent)
.catch((e) => this.context.logger.logError.bind(this.context.logger)('处理群消息撤回失败', e));
}
}
private async emitRecallMsg(message: RawMessage, element: MessageElement) {
const peer: Peer = { chatType: message.chatType, peerUid: message.peerUid, guildId: '' };
const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId) ?? MessageUnique.createUniqueMsgId(peer, message.msgId);
if (message.chatType == ChatType.KCHATTYPEC2C) {
return await this.emitFriendRecallMsg(message, oriMessageId, element);
} else if (message.chatType == ChatType.KCHATTYPEGROUP) {
return await this.emitGroupRecallMsg(message, oriMessageId, element);
}
}
private async emitFriendRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) {
return new OB11FriendRecallNoticeEvent(
this.core,
+message.senderUin,
oriMessageId
);
}
private async emitGroupRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid;
if (!operatorUid) return undefined;
const operatorId = message.senderUin ?? await this.core.apis.UserApi.getUinByUidV2(operatorUid);
return new OB11GroupRecallNoticeEvent(
this.core,
+message.peerUin,
+message.senderUin,
+operatorId,
oriMessageId
);
}
}
export * from './types';

View File

@@ -50,12 +50,12 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
try {
this.obContext.apis.QuickActionApi
.handleQuickOperation(event as QuickActionEvent, resJson)
.catch(this.logger.logError.bind(this.logger));
.catch(e=>this.logger.logError(e));
} catch (e: any) {
this.logger.logError.bind(this.logger)('[OneBot] [Http Client] 新消息事件HTTP上报返回快速操作失败', e);
this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报返回快速操作失败', e);
}
}).catch((e) => {
this.logger.logError.bind(this.logger)('[OneBot] [Http Client] 新消息事件HTTP上报失败', e);
this.logger.logError('[OneBot] [Http Client] 新消息事件HTTP上报失败', e);
});
}

View File

@@ -95,7 +95,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
try {
this.connectEvent(this.core);
} catch (e) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Client] 发送连接生命周期失败', e);
this.logger.logError('[OneBot] [WebSocket Client] 发送连接生命周期失败', e);
}
});
@@ -104,8 +104,8 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
});
this.connection.once('close', () => {
if (!isClosedByError) {
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`);
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`);
this.logger.logError(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
if (this.isEnable) {
this.connection = null;
setTimeout(() => this.tryConnect(), this.config.reconnectInterval);
@@ -114,8 +114,8 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
});
this.connection.on('error', (err) => {
isClosedByError = true;
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err);
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
this.logger.logError(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err);
this.logger.logError(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
if (this.isEnable) {
this.connection = null;
setTimeout(() => this.tryConnect(), this.config.reconnectInterval);
@@ -128,7 +128,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
try {
this.checkStateAndReply<any>(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT));
} catch (e) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Client] 发送生命周期失败', e);
this.logger.logError('[OneBot] [WebSocket Client] 发送生命周期失败', e);
}
}
@@ -147,7 +147,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
receiveData.params = (receiveData?.params) ? receiveData.params : {};// 兼容类型验证
const action = this.actions.get(receiveData.action);
if (!action) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Client] 发生错误', '不支持的Api ' + receiveData.action);
this.logger.logError('[OneBot] [WebSocket Client] 发生错误', '不支持的Api ' + receiveData.action);
this.checkStateAndReply<any>(OB11Response.error('不支持的Api ' + receiveData.action, 1404, echo));
return;
}

View File

@@ -37,6 +37,10 @@ export class OB11NetworkManager {
return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.onEvent(event)));
}
async emitEvents(events: OB11EmitEventContent[]) {
return Promise.all(events.map(event => this.emitEvent(event)));
}
async emitEventByName(names: string[], event: OB11EmitEventContent) {
return Promise.all(names.map(name => {
const adapter = this.adapters.get(name);
@@ -71,7 +75,7 @@ export class OB11NetworkManager {
async closeSomeAdaterWhenOpen(adaptersToClose: IOB11NetworkAdapter[]) {
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
if(adapter.isEnable){
if (adapter.isEnable) {
await adapter.close();
}
}

View File

@@ -102,7 +102,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
if (req.path === '' || req.path === '/') {
const hello = OB11Response.ok({});
hello.message = 'NapCat4 Ss Running';
return res.json(hello)
return res.json(hello);
}
const actionName = req.path.split('/')[1];
const action = this.actions.get(actionName);

View File

@@ -51,7 +51,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
wsClient.on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Client Error:', err.message));
wsClient.on('message', (message) => {
this.handleMessage(wsClient, message).then().catch(this.logger.logError.bind(this.logger));
this.handleMessage(wsClient, message).then().catch(e => this.logger.logError(e));
});
wsClient.on('ping', () => {
wsClient.pong();
@@ -85,7 +85,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
try {
this.checkStateAndReply<any>(new OB11LifeCycleEvent(core, LifeCycleSubType.CONNECT), wsClient);
} catch (e) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] 发送生命周期失败', e);
this.logger.logError('[OneBot] [WebSocket Server] 发送生命周期失败', e);
}
}
@@ -99,7 +99,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
open() {
if (this.isEnable) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server');
this.logger.logError('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server');
return;
}
const addressInfo = this.wsServer.address();
@@ -116,9 +116,9 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
this.isEnable = false;
this.wsServer.close((err) => {
if (err) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Error closing server:', err.message);
this.logger.logError('[OneBot] [WebSocket Server] Error closing server:', err.message);
} else {
this.logger.log.bind(this.logger)('[OneBot] [WebSocket Server] Server Closed');
this.logger.log('[OneBot] [WebSocket Server] Server Closed');
}
});
@@ -179,7 +179,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
receiveData.params = (receiveData?.params) ? receiveData.params : {};//兼容类型验证 不然类型校验爆炸
const action = this.actions.get(receiveData.action);
if (!action) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Client] 发生错误', '不支持的API ' + receiveData.action);
this.logger.logError('[OneBot] [WebSocket Client] 发生错误', '不支持的API ' + receiveData.action);
this.checkStateAndReply<any>(OB11Response.error('不支持的API ' + receiveData.action, 1404, echo), wsClient);
return;
}

366
src/shell/base.ts Normal file
View File

@@ -0,0 +1,366 @@
import type { SelfInfo } from '@/core/types';
import { LogWrapper } from '@/common/log';
import { NodeIKernelLoginListener, NodeIKernelSessionListener } from '@/core/listeners';
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from '@/core/adapters';
import { NapCatPathWrapper } from '@/common/path';
import {
genSessionConfig,
InstanceContext,
loadQQWrapper,
NapCatCore,
NapCatCoreWorkingEnv,
NodeIQQNTWrapperSession,
PlatformType,
WrapperNodeApi,
} from '@/core';
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
import { hostname, systemVersion } from '@/common/system';
import { proxiedListenerOf } from '@/common/proxy-handler';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { NodeIKernelLoginService } from '@/core/services';
import { program } from 'commander';
import qrcode from 'qrcode-terminal';
import { NapCatOneBot11Adapter } from '@/onebot';
import { InitWebUi } from '@/webui';
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
import { napCatVersion } from '@/common/version';
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions(logger: LogWrapper) {
process.on('uncaughtException', (err) => {
logger.logError('[NapCat] [Error] Unhandled Exception:', err.message);
});
process.on('unhandledRejection', (reason, promise) => {
logger.logError('[NapCat] [Error] unhandledRejection:', reason);
});
}
function getDataPaths(wrapper: WrapperNodeApi): [string, string] {
if (os.platform() === 'darwin') {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
return [appDataPath, path.join(appDataPath, 'global')];
}
let dataPath = wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
if (!dataPath) {
dataPath = path.resolve(os.homedir(), './.config/QQ');
fs.mkdirSync(dataPath, { recursive: true });
}
const dataPathGlobal = path.resolve(dataPath, './nt_qq/global');
return [dataPath, dataPathGlobal];
}
function getPlatformType(): PlatformType {
const platformMapping: Partial<Record<NodeJS.Platform, PlatformType>> = {
win32: PlatformType.KWINDOWS,
darwin: PlatformType.KMAC,
linux: PlatformType.KLINUX,
};
return platformMapping[os.platform()] ?? PlatformType.KWINDOWS;
}
async function initializeEngine(
engine: any,
basicInfoWrapper: QQBasicInfoWrapper,
dataPathGlobal: string,
systemPlatform: PlatformType,
systemVersion: string
) {
engine.initWithDeskTopConfig(
{
base_path_prefix: '',
platform_type: systemPlatform,
app_type: 4,
app_version: basicInfoWrapper.getFullQQVesion(),
os_version: systemVersion,
use_xlog: false,
qua: basicInfoWrapper.QQVersionQua,
global_path_config: {
desktopGlobalPath: dataPathGlobal,
},
thumb_config: { maxSide: 324, minSide: 48, longLimit: 6, density: 2 },
},
new NodeIGlobalAdapter(),
);
}
async function initializeLoginService(
loginService: NodeIKernelLoginService,
basicInfoWrapper: QQBasicInfoWrapper,
dataPathGlobal: string,
systemVersion: string,
hostname: string
) {
loginService.initConfig({
machineId: '',
appid: basicInfoWrapper.QQVersionAppid ?? '',
platVer: systemVersion,
commonPath: dataPathGlobal,
clientVer: basicInfoWrapper.getFullQQVesion(),
hostName: hostname,
});
}
async function handleLogin(
loginService: NodeIKernelLoginService,
logger: LogWrapper,
pathWrapper: NapCatPathWrapper,
quickLoginUin: string | undefined,
historyLoginList: any[]
): Promise<SelfInfo> {
return new Promise<SelfInfo>((resolve) => {
const loginListener = new NodeIKernelLoginListener();
let isLogined = false;
loginListener.onUserLoggedIn = (userid: string) => {
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
};
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
isLogined = true;
resolve({
uid: loginResult.uid,
uin: loginResult.uin,
nick: '',
online: true,
});
};
loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl);
const realBase64 = pngBase64QrcodeData.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(realBase64, 'base64');
logger.logWarn('请扫描下面的二维码然后在手Q上授权登录');
const qrcodePath = path.join(pathWrapper.cachePath, 'qrcode.png');
qrcode.generate(qrcodeUrl, { small: true }, (res) => {
logger.logWarn([
'\n',
res,
'二维码解码URL: ' + qrcodeUrl,
'如果控制台二维码无法扫码可以复制解码url到二维码生成网站生成二维码再扫码也可以打开下方的二维码路径图片进行扫码。',
].join('\n'));
fs.writeFile(qrcodePath, buffer, {}, () => {
logger.logWarn('二维码已保存到', qrcodePath);
});
});
};
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => {
if (!isLogined) {
logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg);
if (errType == 1 && errCode == 3) {
// 二维码过期刷新
}
loginService.getQRCodePicture();
}
};
loginListener.onLoginFailed = (args) => {
logger.logError('[Core] [Login] Login Error , ErrInfo: ', args);
};
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
const isConnect = loginService.connect();
if (!isConnect) {
logger.logError('核心登录服务连接失败!');
return;
}
logger.log('核心登录服务连接成功!');
loginService.getLoginList().then((res) => {
// 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList
WebUiDataRuntime.setQQQuickLoginList(res.LocalLoginInfoList.filter((item) => item.isQuickLogin).map((item) => item.uin.toString()));
});
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
return await new Promise((resolve) => {
if (uin) {
logger.log('正在快速登录 ', uin);
loginService.quickLoginWithUin(uin).then(res => {
if (res.loginErrorInfo.errMsg) {
resolve({ result: false, message: res.loginErrorInfo.errMsg });
}
resolve({ result: true, message: '' });
}).catch((e) => {
logger.logError(e);
resolve({ result: false, message: '快速登录发生错误' });
});
} else {
resolve({ result: false, message: '快速登录失败' });
}
});
});
if (quickLoginUin) {
if (historyLoginList.some(u => u.uin === quickLoginUin)) {
logger.log('正在快速登录 ', quickLoginUin);
setTimeout(() => {
loginService.quickLoginWithUin(quickLoginUin)
.then(result => {
if (result.loginErrorInfo.errMsg) {
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
if (!isLogined) loginService.getQRCodePicture();
}
})
.catch();
}, 1000);
} else {
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式');
if (!isLogined) loginService.getQRCodePicture();
}
} else {
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
if (historyLoginList.length > 0) {
logger.log(`可用于快速登录的 QQ\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n')
}`);
}
loginService.getQRCodePicture();
}
});
}
async function initializeSession(
session: NodeIQQNTWrapperSession,
sessionConfig: any,
logger: LogWrapper
) {
return new Promise<void>((resolve, reject) => {
const sessionListener = new NodeIKernelSessionListener();
sessionListener.onSessionInitComplete = (r: unknown) => {
if (r === 0) {
resolve();
} else {
reject(new Error('登录异常' + r?.toString()));
}
};
session.init(
sessionConfig,
new NodeIDependsAdapter(),
new NodeIDispatcherAdapter(),
sessionListener,
);
try {
session.startNT(0);
} catch (_) {
try {
session.startNT();
} catch (e: unknown) {
reject(new Error('init failed ' + (e as Error).message));
}
}
});
}
export async function NCoreInitShell() {
console.log('NapCat Shell App Loading...');
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
handleUncaughtExceptions(logger);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
const o3Service = wrapper.NodeIO3MiscService.get();
o3Service.addO3MiscListener(new NodeIO3MiscListener());
logger.log(`[NapCat] [Core] NapCat.Core Version: ` + napCatVersion);
InitWebUi(logger, pathWrapper).then().catch(e => logger.logError(e));
const engine = wrapper.NodeIQQNTWrapperEngine.get();
const loginService = wrapper.NodeIKernelLoginService.get();
const session = wrapper.NodeIQQNTWrapperSession.create();
const [dataPath, dataPathGlobal] = getDataPaths(wrapper);
const systemPlatform = getPlatformType();
if (!basicInfoWrapper.QQVersionAppid || !basicInfoWrapper.QQVersionQua) throw new Error('QQVersionAppid or QQVersionQua is not defined');
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
const cmdOptions = program.opts();
const quickLoginUin = cmdOptions.qq;
const historyLoginList = (await loginService.getLoginList()).LocalLoginInfoList;
const dataTimestape = new Date().getTime().toString();
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));
let guid = loginService.getMachineGuid();
guid = guid.slice(0, 8) + '-' + guid.slice(8, 12) + '-' + guid.slice(12, 16) + '-' + guid.slice(16, 20) + '-' + guid.slice(20);
o3Service.reportAmgomWeather('login', 'a6', [dataTimestape, '184', '329']);
const sessionConfig = await genSessionConfig(
guid,
basicInfoWrapper.QQVersionAppid,
basicInfoWrapper.getFullQQVesion(),
selfInfo.uin,
selfInfo.uid,
dataPath,
);
await initializeSession(session, sessionConfig, logger);
const accountDataPath = path.resolve(dataPath, './NapCat/data');
fs.mkdirSync(dataPath, { recursive: true });
logger.logDebug('本账号数据/缓存目录:', accountDataPath);
await new NapCatShell(
wrapper,
session,
logger,
loginService,
selfInfo,
basicInfoWrapper,
pathWrapper,
).InitNapCat();
}
export class NapCatShell {
readonly core: NapCatCore;
readonly context: InstanceContext;
constructor(
wrapper: WrapperNodeApi,
session: NodeIQQNTWrapperSession,
logger: LogWrapper,
loginService: NodeIKernelLoginService,
selfInfo: SelfInfo,
basicInfoWrapper: QQBasicInfoWrapper,
pathWrapper: NapCatPathWrapper,
) {
this.context = {
workingEnv: NapCatCoreWorkingEnv.Shell,
wrapper,
session,
logger,
loginService,
basicInfoWrapper,
pathWrapper,
};
this.core = new NapCatCore(this.context, selfInfo);
}
async InitNapCat() {
await this.core.initCore();
new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper).InitOneBot()
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
}
}

View File

@@ -1,369 +1,3 @@
import type { SelfInfo } from '@/core/types';
import { LogWrapper } from '@/common/log';
import { NodeIKernelLoginListener, NodeIKernelSessionListener } from '@/core/listeners';
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from '@/core/adapters';
import { NapCatPathWrapper } from '@/common/path';
import {
genSessionConfig,
InstanceContext,
loadQQWrapper,
NapCatCore,
NapCatCoreWorkingEnv,
NodeIQQNTWrapperSession,
PlatformType,
WrapperNodeApi,
} from '@/core';
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
import { hostname, systemVersion } from '@/common/system';
import { proxiedListenerOf } from '@/common/proxy-handler';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { NodeIKernelLoginService } from '@/core/services';
import { program } from 'commander';
import qrcode from 'qrcode-terminal';
import { NapCatOneBot11Adapter } from '@/onebot';
import { InitWebUi } from '@/webui';
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
import { napCatVersion } from '@/common/version';
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
const cmdOptions = program.opts();
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions(logger: LogWrapper) {
process.on('uncaughtException', (err) => {
logger.logError('[NapCat] [Error] Unhandled Exception:', err.message);
});
process.on('unhandledRejection', (reason, promise) => {
logger.logError('[NapCat] [Error] unhandledRejection:', reason);
});
}
function getDataPaths(wrapper: WrapperNodeApi): [string, string] {
if (os.platform() === 'darwin') {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
return [appDataPath, path.join(appDataPath, 'global')];
}
let dataPath = wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
if (!dataPath) {
dataPath = path.resolve(os.homedir(), './.config/QQ');
fs.mkdirSync(dataPath, { recursive: true });
}
const dataPathGlobal = path.resolve(dataPath, './nt_qq/global');
return [dataPath, dataPathGlobal];
}
function getPlatformType(): PlatformType {
const platformMapping: Partial<Record<NodeJS.Platform, PlatformType>> = {
win32: PlatformType.KWINDOWS,
darwin: PlatformType.KMAC,
linux: PlatformType.KLINUX,
};
return platformMapping[os.platform()] ?? PlatformType.KWINDOWS;
}
async function initializeEngine(
engine: any,
basicInfoWrapper: QQBasicInfoWrapper,
dataPathGlobal: string,
systemPlatform: PlatformType,
systemVersion: string
) {
engine.initWithDeskTopConfig(
{
base_path_prefix: '',
platform_type: systemPlatform,
app_type: 4,
app_version: basicInfoWrapper.getFullQQVesion(),
os_version: systemVersion,
use_xlog: false,
qua: basicInfoWrapper.QQVersionQua,
global_path_config: {
desktopGlobalPath: dataPathGlobal,
},
thumb_config: { maxSide: 324, minSide: 48, longLimit: 6, density: 2 },
},
new NodeIGlobalAdapter(),
);
}
async function initializeLoginService(
loginService: NodeIKernelLoginService,
basicInfoWrapper: QQBasicInfoWrapper,
dataPathGlobal: string,
systemVersion: string,
hostname: string
) {
loginService.initConfig({
machineId: '',
appid: basicInfoWrapper.QQVersionAppid ?? '',
platVer: systemVersion,
commonPath: dataPathGlobal,
clientVer: basicInfoWrapper.getFullQQVesion(),
hostName: hostname,
});
}
async function handleLogin(
loginService: NodeIKernelLoginService,
logger: LogWrapper,
pathWrapper: NapCatPathWrapper,
quickLoginUin: string | undefined,
historyLoginList: any[]
): Promise<SelfInfo> {
return new Promise<SelfInfo>((resolve) => {
const loginListener = new NodeIKernelLoginListener();
let isLogined = false;
loginListener.onUserLoggedIn = (userid: string) => {
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
};
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
isLogined = true;
resolve({
uid: loginResult.uid,
uin: loginResult.uin,
nick: '',
online: true,
});
};
loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl);
const realBase64 = pngBase64QrcodeData.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(realBase64, 'base64');
logger.logWarn('请扫描下面的二维码然后在手Q上授权登录');
const qrcodePath = path.join(pathWrapper.cachePath, 'qrcode.png');
qrcode.generate(qrcodeUrl, { small: true }, (res) => {
logger.logWarn([
'\n',
res,
'二维码解码URL: ' + qrcodeUrl,
'如果控制台二维码无法扫码可以复制解码url到二维码生成网站生成二维码再扫码也可以打开下方的二维码路径图片进行扫码。',
].join('\n'));
fs.writeFile(qrcodePath, buffer, {}, () => {
logger.logWarn('二维码已保存到', qrcodePath);
});
});
};
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => {
if (!isLogined) {
logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg);
if (errType == 1 && errCode == 3) {
// 二维码过期刷新
}
loginService.getQRCodePicture();
}
};
loginListener.onLoginFailed = (args) => {
logger.logError('[Core] [Login] Login Error , ErrInfo: ', args);
};
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
const isConnect = loginService.connect();
if (!isConnect) {
logger.logError('核心登录服务连接失败!');
return;
}
logger.log('核心登录服务连接成功!');
loginService.getLoginList().then((res) => {
// 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList
WebUiDataRuntime.setQQQuickLoginList(res.LocalLoginInfoList.filter((item) => item.isQuickLogin).map((item) => item.uin.toString()));
});
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
return await new Promise((resolve) => {
if (uin) {
logger.log('正在快速登录 ', uin);
loginService.quickLoginWithUin(uin).then(res => {
if (res.loginErrorInfo.errMsg) {
resolve({ result: false, message: res.loginErrorInfo.errMsg });
}
resolve({ result: true, message: '' });
}).catch((e) => {
logger.logError(e);
resolve({ result: false, message: '快速登录发生错误' });
});
} else {
resolve({ result: false, message: '快速登录失败' });
}
});
});
if (quickLoginUin) {
if (historyLoginList.some(u => u.uin === quickLoginUin)) {
logger.log('正在快速登录 ', quickLoginUin);
setTimeout(() => {
loginService.quickLoginWithUin(quickLoginUin)
.then(result => {
if (result.loginErrorInfo.errMsg) {
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
if (!isLogined) loginService.getQRCodePicture();
}
})
.catch();
}, 1000);
} else {
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式');
if (!isLogined) loginService.getQRCodePicture();
}
} else {
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
if (historyLoginList.length > 0) {
logger.log(`可用于快速登录的 QQ\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n')
}`);
}
loginService.getQRCodePicture();
}
});
}
async function initializeSession(
session: NodeIQQNTWrapperSession,
sessionConfig: any,
logger: LogWrapper
) {
return new Promise<void>((resolve, reject) => {
const sessionListener = new NodeIKernelSessionListener();
sessionListener.onSessionInitComplete = (r: unknown) => {
if (r === 0) {
resolve();
} else {
reject(new Error('登录异常' + r?.toString()));
}
};
session.init(
sessionConfig,
new NodeIDependsAdapter(),
new NodeIDispatcherAdapter(),
sessionListener,
);
try {
session.startNT(0);
} catch (_) {
try {
session.startNT();
} catch (e: unknown) {
reject(new Error('init failed ' + (e as Error).message));
}
}
});
}
export async function NCoreInitShell() {
console.log('NapCat Shell App Loading...');
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
handleUncaughtExceptions(logger);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
const o3Service = wrapper.NodeIO3MiscService.get();
o3Service.addO3MiscListener(new NodeIO3MiscListener());
logger.log(`[NapCat] [Core] NapCat.Core Version: ` + napCatVersion);
InitWebUi(logger, pathWrapper).then().catch(logger.logError.bind(logger));
const engine = wrapper.NodeIQQNTWrapperEngine.get();
const loginService = wrapper.NodeIKernelLoginService.get();
const session = wrapper.NodeIQQNTWrapperSession.create();
const [dataPath, dataPathGlobal] = getDataPaths(wrapper);
const systemPlatform = getPlatformType();
if (!basicInfoWrapper.QQVersionAppid || !basicInfoWrapper.QQVersionQua) throw new Error('QQVersionAppid or QQVersionQua is not defined');
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
const quickLoginUin = cmdOptions.qq;
const historyLoginList = (await loginService.getLoginList()).LocalLoginInfoList;
const dataTimestape = new Date().getTime().toString();
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));
let guid = loginService.getMachineGuid();
guid = guid.slice(0, 8) + '-' + guid.slice(8, 12) + '-' + guid.slice(12, 16) + '-' + guid.slice(16, 20) + '-' + guid.slice(20);
o3Service.reportAmgomWeather('login', 'a6', [dataTimestape, '184', '329']);
const sessionConfig = await genSessionConfig(
guid,
basicInfoWrapper.QQVersionAppid,
basicInfoWrapper.getFullQQVesion(),
selfInfo.uin,
selfInfo.uid,
dataPath,
);
await initializeSession(session, sessionConfig, logger);
const accountDataPath = path.resolve(dataPath, './NapCat/data');
fs.mkdirSync(dataPath, { recursive: true });
logger.logDebug('本账号数据/缓存目录:', accountDataPath);
await new NapCatShell(
wrapper,
session,
logger,
loginService,
selfInfo,
basicInfoWrapper,
pathWrapper,
).InitNapCat();
}
export class NapCatShell {
readonly core: NapCatCore;
readonly context: InstanceContext;
constructor(
wrapper: WrapperNodeApi,
session: NodeIQQNTWrapperSession,
logger: LogWrapper,
loginService: NodeIKernelLoginService,
selfInfo: SelfInfo,
basicInfoWrapper: QQBasicInfoWrapper,
pathWrapper: NapCatPathWrapper,
) {
this.context = {
workingEnv: NapCatCoreWorkingEnv.Shell,
wrapper,
session,
logger,
loginService,
basicInfoWrapper,
pathWrapper,
};
this.core = new NapCatCore(this.context, selfInfo);
}
async InitNapCat() {
await this.core.initCore();
new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper).InitOneBot()
.catch(e => this.context.logger.logError.bind(this.context.logger)('初始化OneBot失败', e));
}
}
import { NCoreInitShell } from "./base";
NCoreInitShell();

7
src/universal/napcat.ts Normal file
View File

@@ -0,0 +1,7 @@
import { NCoreInitShell } from "@/shell/base";
export * from "@/framework/napcat";
export * from "@/shell/base";
if ((global as any).LiteLoader == undefined) {
NCoreInitShell();
}

View File

@@ -1,11 +1,17 @@
/**
* @file WebUI服务入口文件
*/
import express from 'express';
import { ALLRouter } from './src/router';
import { LogWrapper } from '@/common/log';
import { NapCatPathWrapper } from '@/common/path';
import { WebUiConfigWrapper } from './src/helper/config';
import { RequestUtil } from '@/common/request';
import { isIP } from "node:net";
import { WebUiConfigWrapper } from '@webapi/helper/config';
import { ALLRouter } from '@webapi/router';
import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url';
import { sendSuccess } from '@webapi/utils/response';
// 实例化Express
const app = express();
/**
@@ -20,55 +26,46 @@ export let webUiPathWrapper: NapCatPathWrapper;
export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) {
webUiPathWrapper = pathWrapper;
WebUiConfig = new WebUiConfigWrapper();
const log = logger.log.bind(logger);
const config = await WebUiConfig.GetWebUIConfig();
if (config.port == 0) {
log('[NapCat] [WebUi] Current WebUi is not run.');
logger.log('[NapCat] [WebUi] Current WebUi is not run.');
return;
}
// ------------注册中间件------------
// 使用express的json中间件
app.use(express.json());
// 初始服务
// CORS中间件
// TODO:
app.use(cors);
// ------------中间件结束------------
// ------------挂载路由------------
// 挂载静态路由(前端),路径为 [/前缀]/webui
app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath));
// 挂载API接口
app.use(config.prefix + '/api', ALLRouter);
// 初始服务(先放个首页)
// WebUI只在config.prefix所示路径上提供服务可配合Nginx挂载到子目录中
app.all(config.prefix + '/', (_req, res) => {
res.json({
msg: 'NapCat WebAPI is now running!',
});
sendSuccess(res, null, 'NapCat WebAPI is now running!');
});
// 配置静态文件服务,提供./static目录下的文件服务访问路径为/webui
app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath));
//挂载API接口
// 添加CORS支持
// TODO:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
next();
});
app.use(config.prefix + '/api', ALLRouter);
// ------------路由挂载结束------------
// ------------启动服务------------
app.listen(config.port, config.host, async () => {
const normalizeHost = (host: string) => {
if (host === '0.0.0.0') return '127.0.0.1';
if (isIP(host) === 6) return `[${host}]`;
return host;
};
const createUrl = (host: string, path: string, token: string) => {
const url = new URL(`http://${normalizeHost(host)}`);
url.port = config.port.toString();
url.pathname = `${config.prefix}${path}`;
url.searchParams.set('token', token);
return url.toString();
};
log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`);
log(`[NapCat] [WebUi] Login Token is ${config.token}`);
log(`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, '/webui', config.token)}`);
log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', '/webui', config.token)}`);
try {
const publishUrl = 'https://ip.011102.xyz/';
const data = await RequestUtil.HttpGetJson<{ IP: { IP: string } }>(publishUrl, 'GET', {}, {}, true, true);
log(`[NapCat] [WebUi] WebUi Publish Panel Url: ${createUrl(data.IP.IP, '/webui', config.token)}`);
} catch (err) {
logger.logError(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
// 启动后打印出相关地址
const port = config.port.toString(),
searchParams = { token: config.token },
path = `${config.prefix}/webui`;
if (config.host !== '' && config.host !== '0.0.0.0') {
logger.log(`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, port, path, searchParams)}`);
logger.log(`[NapCat] [WebUi] WebUi User Panel Url: https://napcat.152710.xyz/web_login?back=http://${config.host}:${config.port}${config.prefix}&token=${config.token}`);
}
logger.log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port, path, searchParams)}`);
logger.log(`[NapCat] [WebUi] WebUi Local Panel Url: https://napcat.152710.xyz/web_login?back=http://127.0.0.1:${config.port}${config.prefix}&token=${config.token}`);
});
// ------------Over------------
}

View File

@@ -1,69 +1,64 @@
import { RequestHandler } from 'express';
import { AuthHelper } from '../helper/SignToken';
import { WebUiDataRuntime } from '../helper/Data';
import { WebUiConfig } from '@/webui';
const isEmpty = (data: any) => data === undefined || data === null || data === '';
import { AuthHelper } from '@webapi/helper/SignToken';
import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendSuccess, sendError } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check';
// 登录
export const LoginHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
// 获取请求体中的token
const { token } = req.body;
// 如果token为空返回错误信息
if (isEmpty(token)) {
res.json({
code: -1,
message: 'token is empty',
});
return;
return sendError(res, 'token is empty');
}
if (!await WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate)) {
res.json({
code: -1,
message: 'login rate limit',
});
return;
// 检查登录频率
if (!(await WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate))) {
return sendError(res, 'login rate limit');
}
//验证config.token是否等于token
if (WebUiConfigData.token !== token) {
res.json({
code: -1,
message: 'token is invalid',
});
return;
return sendError(res, 'token is invalid');
}
const signCredential = Buffer.from(JSON.stringify(await AuthHelper.signCredential(WebUiConfigData.token))).toString('base64');
res.json({
code: 0,
message: 'success',
data: {
'Credential': signCredential,
},
// 签发凭证
const signCredential = Buffer.from(JSON.stringify(await AuthHelper.signCredential(WebUiConfigData.token))).toString(
'base64'
);
// 返回成功信息
return sendSuccess(res, {
Credential: signCredential,
});
return;
};
export const LogoutHandler: RequestHandler = (req, res) => {
// 这玩意无状态销毁个灯 得想想办法
res.json({
code: 0,
message: 'success',
});
return;
// 退出登录
export const LogoutHandler: RequestHandler = (_, res) => {
// TODO: 这玩意无状态销毁个灯 得想想办法
return sendSuccess(res, null);
};
// 检查登录状态
export const checkHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
// 获取请求头中的Authorization
const authorization = req.headers.authorization;
// 检查凭证
try {
// 从Authorization中获取凭证
const CredentialBase64: string = authorization?.split(' ')[1] as string;
// 解析凭证
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
// 验证凭证是否在一小时内有效
await AuthHelper.validateCredentialWithinOneHour(WebUiConfigData.token, Credential);
res.json({
code: 0,
message: 'success',
});
return;
// 返回成功信息
return sendSuccess(res, null);
} catch (e) {
res.json({
code: -1,
message: 'failed',
});
// 返回错误信息
return sendError(res, 'Authorization Faild');
}
return;
};

View File

@@ -1,14 +1,15 @@
import { RequestHandler } from 'express';
export const LogFileListHandler: RequestHandler = async (req, res) => {
res.send({
code: 0,
data: {
uin: 0,
nick: 'NapCat',
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=0&s=640',
status: 'online',
boottime: Date.now()
}
});
import { sendSuccess } from '@webapi/utils/response';
// TODO: Implement LogFileListHandler
export const LogFileListHandler: RequestHandler = async (_, res) => {
const fakeData = {
uin: 0,
nick: 'NapCat',
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=0&s=640',
status: 'online',
boottime: Date.now(),
};
sendSuccess(res, fakeData);
};

19
src/webui/src/api/Log.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response';
import { WebUiConfigWrapper } from '../helper/config';
// 日志记录
export const LogHandler: RequestHandler = async (req, res) => {
const filename = req.query.id as string;
if (filename.includes('..')) {
return sendError(res, 'ID不合法');
}
const logContent = WebUiConfigWrapper.GetLogContent(filename);
return sendSuccess(res, logContent);
};
// 日志列表
export const LogListHandler: RequestHandler = async (_, res) => {
const logList = WebUiConfigWrapper.GetLogsList();
return sendSuccess(res, logList);
};

View File

@@ -1,79 +1,58 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
import { existsSync, readFileSync } from 'node:fs';
import { OneBotConfig } from '@/onebot/config/config';
import { resolve } from 'node:path';
import { webUiPathWrapper } from '@/webui';
const isEmpty = (data: any) => data === undefined || data === null || data === '';
export const OB11GetConfigHandler: RequestHandler = async (req, res) => {
import { OneBotConfig } from '@/onebot/config/config';
import { webUiPathWrapper } from '@/webui';
import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendError, sendSuccess } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check';
// 获取OneBot11配置
export const OB11GetConfigHandler: RequestHandler = async (_, res) => {
// 获取QQ登录状态
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
// 如果未登录,返回错误
if (!isLogin) {
res.send({
code: -1,
message: 'Not Login',
});
return;
return sendError(res, 'Not Login');
}
// 获取登录的QQ号
const uin = await WebUiDataRuntime.getQQLoginUin();
// 读取配置文件
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
//console.log(configFilePath);
let data: OneBotConfig;
// 尝试解析配置文件
try {
data = JSON.parse(
// 读取配置文件
const data = JSON.parse(
existsSync(configFilePath)
? readFileSync(configFilePath).toString()
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString()
);
) as OneBotConfig;
// 返回配置文件
return sendSuccess(res, data);
} catch (e) {
data = {} as OneBotConfig;
res.send({
code: -1,
message: 'Config Get Error',
});
return;
return sendError(res, 'Config Get Error');
}
res.send({
code: 0,
message: 'success',
data: data,
});
return;
};
// 写入OneBot11配置
export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
// 获取QQ登录状态
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
// 如果未登录,返回错误
if (!isLogin) {
res.send({
code: -1,
message: 'Not Login',
});
return;
return sendError(res, 'Not Login');
}
// 如果配置为空,返回错误
if (isEmpty(req.body.config)) {
res.send({
code: -1,
message: 'config is empty',
});
return;
return sendError(res, 'config is empty');
}
let SetResult;
// 写入配置
try {
await WebUiDataRuntime.setOB11Config(JSON.parse(req.body.config));
SetResult = true;
return sendSuccess(res, null);
} catch (e) {
SetResult = false;
return sendError(res, 'Config Set Error');
}
if (SetResult) {
res.send({
code: 0,
message: 'success',
});
} else {
res.send({
code: -1,
message: 'Config Set Error',
});
}
return;
};

View File

@@ -1,78 +1,64 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
const isEmpty = (data: any) => data === undefined || data === null || data === '';
import { WebUiDataRuntime } from '@webapi/helper/Data';
import { isEmpty } from '@webapi/utils/check';
import { sendError, sendSuccess } from '@webapi/utils/response';
// 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (req, res) => {
// 判断是否已经登录
if (await WebUiDataRuntime.getQQLoginStatus()) {
res.send({
code: -1,
message: 'QQ Is Logined',
});
return;
// 已经登录
return sendError(res, 'QQ Is Logined');
}
// 获取二维码
const qrcodeUrl = await WebUiDataRuntime.getQQLoginQrcodeURL();
// 判断二维码是否为空
if (isEmpty(qrcodeUrl)) {
res.send({
code: -1,
message: 'QRCode Get Error',
});
return;
return sendError(res, 'QRCode Get Error');
}
res.send({
code: 0,
message: 'success',
data: {
qrcode: qrcodeUrl,
},
});
return;
// 返回二维码URL
const data = {
qrcode: qrcodeUrl,
};
return sendSuccess(res, data);
};
// 获取QQ登录状态
export const QQCheckLoginStatusHandler: RequestHandler = async (req, res) => {
res.send({
code: 0,
message: 'success',
data: {
isLogin: await WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: await WebUiDataRuntime.getQQLoginQrcodeURL()
},
});
const data = {
isLogin: await WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: await WebUiDataRuntime.getQQLoginQrcodeURL(),
};
return sendSuccess(res, data);
};
// 快速登录
export const QQSetQuickLoginHandler: RequestHandler = async (req, res) => {
// 获取QQ号
const { uin } = req.body;
// 判断是否已经登录
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
if (isLogin) {
res.send({
code: -1,
message: 'QQ Is Logined',
});
return;
return sendError(res, 'QQ Is Logined');
}
// 判断QQ号是否为空
if (isEmpty(uin)) {
res.send({
code: -1,
message: 'uin is empty',
});
return;
return sendError(res, 'uin is empty');
}
// 获取快速登录状态
const { result, message } = await WebUiDataRuntime.requestQuickLogin(uin);
if (!result) {
res.send({
code: -1,
message: message,
});
return;
return sendError(res, message);
}
//本来应该验证 但是http不宜这么搞 建议前端验证
//isLogin = await WebUiDataRuntime.getQQLoginStatus();
res.send({
code: 0,
message: 'success',
});
return sendSuccess(res, null);
};
export const QQGetQuickLoginListHandler: RequestHandler = async (req, res) => {
// 获取快速登录列表
export const QQGetQuickLoginListHandler: RequestHandler = async (_, res) => {
const quickLoginList = await WebUiDataRuntime.getQQQuickLoginList();
res.send({
code: 0,
data: quickLoginList,
});
return sendSuccess(res, quickLoginList);
};

View File

@@ -0,0 +1,13 @@
export enum HttpStatusCode {
OK = 200,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500,
}
export enum ResponseCode {
Success = 0,
Error = -1,
}

View File

@@ -1,18 +1,5 @@
import { OneBotConfig } from '@/onebot/config/config';
interface LoginRuntimeType {
LoginCurrentTime: number;
LoginCurrentRate: number;
QQLoginStatus: boolean;
QQQRCodeURL: string;
QQLoginUin: string;
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
QQLoginList: string[];
};
}
const LoginRuntime: LoginRuntimeType = {
LoginCurrentTime: Date.now(),
LoginCurrentRate: 0,

View File

@@ -1,15 +1,5 @@
import crypto from 'crypto';
interface WebUiCredentialInnerJson {
CreatedTime: number;
TokenEncoded: string;
}
interface WebUiCredentialJson {
Data: WebUiCredentialInnerJson;
Hmac: string;
}
export class AuthHelper {
private static readonly secretKey = Math.random().toString(36).slice(2);
@@ -24,9 +14,7 @@ export class AuthHelper {
TokenEncoded: token,
};
const jsonString = JSON.stringify(innerJson);
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey)
.update(jsonString, 'utf8')
.digest('hex');
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
return { Data: innerJson, Hmac: hmac };
}
@@ -38,7 +26,8 @@ export class AuthHelper {
public static async checkCredential(credentialJson: WebUiCredentialJson): Promise<boolean> {
try {
const jsonString = JSON.stringify(credentialJson.Data);
const calculatedHmac = crypto.createHmac('sha256', AuthHelper.secretKey)
const calculatedHmac = crypto
.createHmac('sha256', AuthHelper.secretKey)
.update(jsonString, 'utf8')
.digest('hex');
return calculatedHmac === credentialJson.Hmac;
@@ -53,7 +42,10 @@ export class AuthHelper {
* @param credentialJson 已签名的凭证JSON对象。
* @returns 布尔值表示凭证是否有效且token匹配。
*/
public static async validateCredentialWithinOneHour(token: string, credentialJson: WebUiCredentialJson): Promise<boolean> {
public static async validateCredentialWithinOneHour(
token: string,
credentialJson: WebUiCredentialJson
): Promise<boolean> {
const isValid = await AuthHelper.checkCredential(credentialJson);
if (!isValid) {
return false;

View File

@@ -1,9 +1,8 @@
import { webUiPathWrapper } from '@/webui';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
import * as net from 'node:net';
import { resolve } from 'node:path';
// 限制尝试端口的次数,避免死循环
const MAX_PORT_TRY = 100;
@@ -64,14 +63,6 @@ async function tryUsePort(port: number, host: string, tryCount: number = 0): Pro
});
}
export interface WebUiConfigType {
host: string;
port: number;
prefix: string;
token: string;
loginRate: number;
}
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
export class WebUiConfigWrapper {
WebUiConfigData: WebUiConfigType | undefined = undefined;
@@ -114,14 +105,18 @@ export class WebUiConfigWrapper {
// 不希望回写的配置放后面
// 查询主机地址是否可用
const [host_err, host] = await tryUseHost(parsedConfig.host).then(data => [null, data]).catch(err => [err, null]);
const [host_err, host] = await tryUseHost(parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (host_err) {
console.log('host不可用', host_err);
parsedConfig.port = 0; // 设置为0禁用WebUI
} else {
parsedConfig.host = host;
// 修正端口占用情况
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host).then(data => [null, data]).catch(err => [err, null]);
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (port_err) {
console.log('port不可用', port_err);
parsedConfig.port = 0; // 设置为0禁用WebUI
@@ -136,5 +131,26 @@ export class WebUiConfigWrapper {
}
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
}
}
// 获取日志文件夹路径
public static async GetLogsPath(): Promise<string> {
return resolve(webUiPathWrapper.logsPath);
}
// 获取日志列表
public static GetLogsList(): string[] {
if (existsSync(webUiPathWrapper.logsPath)) {
return readdirSync(webUiPathWrapper.logsPath)
.filter((file) => file.endsWith('.log'))
.map((file) => file.replace('.log', ''));
}
return [];
}
// 获取指定日志文件内容
public static GetLogContent(filename: string): string {
const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`);
if (existsSync(logPath)) {
return readFileSync(logPath, 'utf-8');
}
return '';
}
}

View File

@@ -0,0 +1,46 @@
import { NextFunction, Request, Response } from 'express';
import { WebUiConfig } from '@/webui';
import { AuthHelper } from '@webapi/helper/SignToken';
import { sendError } from '@webapi/utils/response';
// 鉴权中间件
export async function auth(req: Request, res: Response, next: NextFunction) {
// 判断当前url是否为/login 如果是跳过鉴权
if (req.url == '/auth/login') {
return next();
}
// 判断是否有Authorization头
if (req.headers?.authorization) {
// 切割参数以获取token
const authorization = req.headers.authorization.split(' ');
// 当Bearer后面没有参数时
if (authorization.length < 2) {
return sendError(res, 'Unauthorized');
}
// 获取token
const token = authorization[1];
// 解析token
let Credential: WebUiCredentialJson;
try {
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
} catch (e) {
return sendError(res, 'Unauthorized');
}
// 获取配置
const config = await WebUiConfig.GetWebUIConfig();
// 验证凭证在1小时内有效且token与原始token相同
const credentialJson = await AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (credentialJson) {
// 通过验证
return next();
}
// 验证失败
return sendError(res, 'Unauthorized');
}
// 没有Authorization头
return sendError(res, 'Unauthorized');
}

View File

@@ -0,0 +1,9 @@
import type { RequestHandler } from 'express';
// CORS 中间件,跨域用
export const cors: RequestHandler = (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
next();
};

View File

@@ -0,0 +1,9 @@
import { Router } from 'express';
import { LogHandler, LogListHandler } from '../api/Log';
const router = Router();
// router:读取日志内容
router.get('/GetLog', LogHandler);
// router:读取日志列表
router.get('/GetLogList', LogListHandler);
export { router as LogRouter };

View File

@@ -1,7 +1,11 @@
import { Router } from 'express';
import { OB11GetConfigHandler, OB11SetConfigHandler } from '../api/OB11Config';
import { OB11GetConfigHandler, OB11SetConfigHandler } from '@webapi/api/OB11Config';
const router = Router();
// router:读取配置
router.post('/GetConfig', OB11GetConfigHandler);
// router:写入配置
router.post('/SetConfig', OB11SetConfigHandler);
export { router as OB11ConfigRouter };

View File

@@ -1,14 +1,20 @@
import { Router } from 'express';
import {
QQCheckLoginStatusHandler,
QQGetQRcodeHandler,
QQGetQuickLoginListHandler,
QQSetQuickLoginHandler,
} from '../api/QQLogin';
} from '@webapi/api/QQLogin';
const router = Router();
// router:获取快速登录列表
router.all('/GetQuickLoginList', QQGetQuickLoginListHandler);
// router:检查QQ登录状态
router.post('/CheckLoginStatus', QQCheckLoginStatusHandler);
// router:获取QQ登录二维码
router.post('/GetQQLoginQrcode', QQGetQRcodeHandler);
// router:设置QQ快速登录
router.post('/SetQuickLogin', QQSetQuickLoginHandler);
export { router as QQLoginRouter };

View File

@@ -1,9 +1,13 @@
import { Router } from 'express';
import { checkHandler, LoginHandler, LogoutHandler } from '../api/Auth';
import { checkHandler, LoginHandler, LogoutHandler } from '@webapi/api/Auth';
const router = Router();
// router:登录
router.post('/login', LoginHandler);
// router:检查登录状态
router.post('/check', checkHandler);
// router:注销
router.post('/logout', LogoutHandler);
export { router as AuthRouter };

View File

@@ -1,67 +1,33 @@
import { NextFunction, Request, Response, Router } from 'express';
import { AuthHelper } from '../../src/helper/SignToken';
import { QQLoginRouter } from './QQLogin';
import { AuthRouter } from './auth';
import { OB11ConfigRouter } from './OB11Config';
import { WebUiConfig } from '@/webui';
/**
* @file 所有路由的入口文件
*/
import { Router } from 'express';
import { OB11ConfigRouter } from '@webapi/router/OB11Config';
import { auth } from '@webapi/middleware/auth';
import { sendSuccess } from '@webapi/utils/response';
import { QQLoginRouter } from '@webapi/router/QQLogin';
import { AuthRouter } from '@webapi/router/auth';
import { LogRouter } from '@webapi/router/Log';
const router = Router();
export async function AuthApi(req: Request, res: Response, next: NextFunction) {
//判断当前url是否为/login 如果是跳过鉴权
if (req.url == '/auth/login') {
next();
return;
}
if (req.headers?.authorization) {
const authorization = req.headers.authorization.split(' ');
if (authorization.length < 2) {
res.json({
code: -1,
msg: 'Unauthorized',
});
return;
}
const token = authorization[1];
let Credential: any;
try {
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
} catch (e) {
res.json({
code: -1,
msg: 'Unauthorized',
});
return;
}
const config = await WebUiConfig.GetWebUIConfig();
const credentialJson = await AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (credentialJson) {
//通过验证
next();
return;
}
res.json({
code: -1,
msg: 'Unauthorized',
});
return;
}
// 鉴权中间件
router.use(auth);
res.json({
code: -1,
msg: 'Server Error',
});
return;
}
router.use(AuthApi);
router.all('/test', (req, res) => {
res.json({
code: 0,
msg: 'ok',
});
// router:测试用
router.all('/test', (_, res) => {
return sendSuccess(res);
});
// router:WebUI登录相关路由
router.use('/auth', AuthRouter);
// router:QQ登录相关路由
router.use('/QQLogin', QQLoginRouter);
// router:OB11配置相关路由
router.use('/OB11Config', OB11ConfigRouter);
// router:日志相关路由
router.use('/Log', LogRouter);
export { router as ALLRouter };

7
src/webui/src/types/config.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
interface WebUiConfigType {
host: string;
port: number;
prefix: string;
token: string;
loginRate: number;
}

12
src/webui/src/types/data.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
interface LoginRuntimeType {
LoginCurrentTime: number;
LoginCurrentRate: number;
QQLoginStatus: boolean;
QQQRCodeURL: string;
QQLoginUin: string;
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
QQLoginList: string[];
};
}

7
src/webui/src/types/server.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
interface APIResponse<T> {
code: number;
message: string;
data: T;
}
type Protocol = 'http' | 'https' | 'ws' | 'wss';

9
src/webui/src/types/sign_token.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
interface WebUiCredentialInnerJson {
CreatedTime: number;
TokenEncoded: string;
}
interface WebUiCredentialJson {
Data: WebUiCredentialInnerJson;
Hmac: string;
}

View File

@@ -0,0 +1 @@
export const isEmpty = <T>(data: T) => data === undefined || data === null || data === '';

View File

@@ -0,0 +1,26 @@
import type { Response } from 'express';
import { ResponseCode, HttpStatusCode } from '@webapi/const/status';
export const sendResponse = <T>(res: Response, data?: T, code: ResponseCode = 0, message = 'success') => {
res.status(HttpStatusCode.OK).json({
code,
message,
data,
});
};
export const sendError = (res: Response, message = 'error') => {
res.status(HttpStatusCode.OK).json({
code: ResponseCode.Error,
message,
});
};
export const sendSuccess = <T>(res: Response, data?: T, message = 'success') => {
res.status(HttpStatusCode.OK).json({
code: ResponseCode.Success,
data,
message,
});
};

View File

@@ -0,0 +1,47 @@
/**
* @file URL工具
*/
import { isIP } from 'node:net';
/**
* 将 host主机地址 转换为标准格式
* @param host 主机地址
* @returns 标准格式的IP地址
* @example normalizeHost('10.0.3.2') => '10.0.3.2'
* @example normalizeHost('0.0.0.0') => '127.0.0.1'
* @example normalizeHost('2001:4860:4801:51::27') => '[2001:4860:4801:51::27]'
*/
export const normalizeHost = (host: string) => {
if (host === '0.0.0.0') return '127.0.0.1';
if (isIP(host) === 6) return `[${host}]`;
return host;
};
/**
* 创建URL
* @param host 主机地址
* @param port 端口
* @param path URL路径
* @param search URL参数
* @returns 完整URL
* @example createUrl('127.0.0.1', '8080', '/api', { token: '123456' }) => 'http://127.0.0.1:8080/api?token=123456'
* @example createUrl('baidu.com', '80', void 0, void 0, 'https') => 'https://baidu.com:80/'
*/
export const createUrl = (
host: string,
port: string,
path = '/',
search?: Record<string, any>,
protocol: Protocol = 'http'
) => {
const url = new URL(`${protocol}://${normalizeHost(host)}`);
url.port = port;
url.pathname = path;
if (search) {
for (const key in search) {
url.searchParams.set(key, search[key]);
}
}
return url.toString();
};

View File

@@ -1,34 +1,37 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "Node",
"experimentalDecorators": true,
"allowImportingTsExtensions": false,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"paths": {
"@*": [
"./src*"
]
}
},
"include": [
"src/**/*.ts"
]
}
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "Node",
"experimentalDecorators": true,
"allowImportingTsExtensions": false,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"paths": {
"@/*": [
"./src/*"
],
"@webapi/*": [
"./src/webui/src/*"
],
}
},
"include": [
"src/**/*.ts"
]
}

View File

@@ -6,9 +6,7 @@ import { builtinModules } from 'module';
//依赖排除
const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'fluent-ffmpeg', 'piscina'];
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false };
}
let startScripts: string[] | undefined = undefined;
if (process.env.NAPCAT_BUILDSYS == 'linux') {
startScripts = [];
@@ -17,6 +15,29 @@ if (process.env.NAPCAT_BUILDSYS == 'linux') {
} else {
startScripts = ['./script/KillQQ.bat'];
}
const UniversalBaseConfigPlugin: PluginOption[] = [
cp({
targets: [
{ src: './manifest.json', dest: 'dist' },
{ src: './src/core/external/napcat.json', dest: 'dist/config/' },
{ src: './src/native/packet', dest: 'dist/moehoo', flatten: false },
{ src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false },
{ src: './src/framework/liteloader.cjs', dest: 'dist' },
{ src: './src/framework/napcat.cjs', dest: 'dist' },
{ src: './src/framework/preload.cjs', dest: 'dist' },
{ src: './src/framework/renderer.js', dest: 'dist' },
{ src: './package.json', dest: 'dist' },
{ src: './logo.png', dest: 'dist' },
{ src: './launcher/', dest: 'dist', flatten: true },
...startScripts.map((startScript) => {
return { src: startScript, dest: 'dist' };
}),
],
}),
nodeResolve(),
];
const FrameworkBaseConfigPlugin: PluginOption[] = [
cp({
targets: [
@@ -34,6 +55,8 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [
}),
nodeResolve(),
];
const ShellBaseConfigPlugin: PluginOption[] = [
cp({
targets: [
@@ -49,60 +72,93 @@ const ShellBaseConfigPlugin: PluginOption[] = [
}),
nodeResolve(),
];
const ShellBaseConfig = () => defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/core': resolve(__dirname, './src/core'),
'@': resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
},
},
build: {
sourcemap: false,
target: 'esnext',
minify: false,
lib: {
entry: {
'napcat': 'src/shell/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
const UniversalBaseConfig = () =>
defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/core': resolve(__dirname, './src/core'),
'@': resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
'@webapi': resolve(__dirname, './src/webui/src'),
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
},
rollupOptions: {
external: [...nodeModules, ...external],
},
},
});
const FrameworkBaseConfig = () => defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/core': resolve(__dirname, './src/core'),
'@': resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
},
},
build: {
sourcemap: false,
target: 'esnext',
minify: false,
lib: {
entry: {
'napcat': 'src/framework/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
build: {
sourcemap: false,
target: 'esnext',
minify: false,
lib: {
entry: {
napcat: 'src/universal/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
},
rollupOptions: {
external: [...nodeModules, ...external],
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
},
rollupOptions: {
external: [...nodeModules, ...external],
});
const ShellBaseConfig = () =>
defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/core': resolve(__dirname, './src/core'),
'@': resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
'@webapi': resolve(__dirname, './src/webui/src'),
},
},
},
});
build: {
sourcemap: false,
target: 'esnext',
minify: false,
lib: {
entry: {
napcat: 'src/shell/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
},
rollupOptions: {
external: [...nodeModules, ...external],
},
},
});
const FrameworkBaseConfig = () =>
defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/core': resolve(__dirname, './src/core'),
'@': resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
'@webapi': resolve(__dirname, './src/webui/src'),
},
},
build: {
sourcemap: false,
target: 'esnext',
minify: false,
lib: {
entry: {
napcat: 'src/framework/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
},
rollupOptions: {
external: [...nodeModules, ...external],
},
},
});
export default defineConfig(({ mode }): UserConfig => {
if (mode === 'shell') {
@@ -110,6 +166,11 @@ export default defineConfig(({ mode }): UserConfig => {
...ShellBaseConfig(),
plugins: [...ShellBaseConfigPlugin],
};
} else if (mode == 'universal') {
return {
...UniversalBaseConfig(),
plugins: [...UniversalBaseConfigPlugin],
};
} else {
return {
...FrameworkBaseConfig(),