mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
06eba28b4c | ||
![]() |
bbfeac46dd | ||
![]() |
2fe4da094a | ||
![]() |
b454d8c0f9 | ||
![]() |
1f9b5453cc | ||
![]() |
3261791e99 | ||
![]() |
3bb12e3f45 | ||
![]() |
1dc2f7e5a2 | ||
![]() |
2531b08538 | ||
![]() |
9fcfb5493c | ||
![]() |
4576354c51 | ||
![]() |
1dcf2ef0c6 | ||
![]() |
3642c65e8c | ||
![]() |
40e105994a | ||
![]() |
f2ee973882 | ||
![]() |
3aa30792bf | ||
![]() |
6e336fa78e | ||
![]() |
900027a6b7 | ||
![]() |
38bdca2409 | ||
![]() |
7196e476bf | ||
![]() |
e0fd3785d9 | ||
![]() |
b53ebb6c2a | ||
![]() |
1ea80f4447 | ||
![]() |
627d3c0a7a | ||
![]() |
182cccfc71 | ||
![]() |
6a3713e86c | ||
![]() |
788da4e4f1 | ||
![]() |
fd26d34e19 | ||
![]() |
e9fcdc7d2e | ||
![]() |
0fe4911d01 | ||
![]() |
d4fb09fa80 | ||
![]() |
e6d5a37236 | ||
![]() |
79fd10ac10 | ||
![]() |
a2e6095e44 | ||
![]() |
64530471a0 | ||
![]() |
e31e831309 | ||
![]() |
cf6871df9b | ||
![]() |
482e7f1c75 | ||
![]() |
aab501e31e | ||
![]() |
ceec9e5e1b | ||
![]() |
aadebb3cc5 | ||
![]() |
657ddd3341 | ||
![]() |
62127b6d48 | ||
![]() |
f5f405796f | ||
![]() |
39873947a3 | ||
![]() |
a1079dd948 | ||
![]() |
4eeabcc9e0 | ||
![]() |
c3568d07e8 | ||
![]() |
1adb4a4ba8 | ||
![]() |
6d0020533c | ||
![]() |
4e6af0a655 | ||
![]() |
00f726b515 | ||
![]() |
035aa32305 | ||
![]() |
62ea4b98e1 | ||
![]() |
4be821137d | ||
![]() |
7fba9960bf | ||
![]() |
876bfbd3cb | ||
![]() |
edde2c210b | ||
![]() |
f956d96d94 | ||
![]() |
c2296fd900 | ||
![]() |
0feed5b640 | ||
![]() |
93904dcb1b | ||
![]() |
86cbdf793a | ||
![]() |
56b1b9b598 | ||
![]() |
f7ec3ae131 | ||
![]() |
3fbed815a5 |
@@ -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) 对本项目的大力支持 参考部分代码 已获授权
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.1.19",
|
||||
"version": "4.2.5",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -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>
|
||||
|
@@ -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": {
|
||||
|
BIN
napcat.webui/public/logo_webui.png
Normal file
BIN
napcat.webui/public/logo_webui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 201 KiB |
@@ -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>
|
||||
|
BIN
napcat.webui/src/assets/logo_webui.png
Normal file
BIN
napcat.webui/src/assets/logo_webui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 201 KiB |
@@ -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',
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 {
|
||||
|
@@ -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');
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
3
napcat.webui/src/ts/event-bus.ts
Normal file
3
napcat.webui/src/ts/event-bus.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import mitt from 'mitt';
|
||||
const emitter = mitt();
|
||||
export default emitter;
|
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.1.19",
|
||||
"version": "4.2.5",
|
||||
"scripts": {
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
||||
@@ -44,7 +44,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 +52,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"
|
||||
}
|
||||
}
|
||||
|
@@ -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,22 +190,26 @@ 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:')) {
|
||||
// 兼容file:///
|
||||
// file:///C:/1.jpg
|
||||
if (uri.startsWith('file:///') && process.platform === 'win32') {
|
||||
const filePath: string = uri.slice(8);
|
||||
return { Uri: filePath, Type: FileUriType.Local };
|
||||
}
|
||||
// 处理默认规范
|
||||
// file://C:\1.jpg
|
||||
// file:///test/1.jpg
|
||||
const filePath: string = uri.slice(7);
|
||||
|
||||
return { Uri: filePath, Type: FileUriType.Local };
|
||||
}
|
||||
@@ -222,14 +225,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 +246,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 +265,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:\/\//, '');
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.1.19';
|
||||
export const napCatVersion = '4.2.5';
|
||||
|
@@ -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,7 +141,7 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
const logger = this.core.context.logger;
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
@@ -307,18 +307,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++;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -255,7 +255,7 @@ export class NodeIKernelMsgListener {
|
||||
|
||||
}
|
||||
|
||||
onMsgRecall(i2: unknown, str: unknown, j2: unknown): any {
|
||||
onMsgRecall(chatType: ChatType, uid: string, msgSeq: string): any {
|
||||
|
||||
}
|
||||
|
||||
|
@@ -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),
|
||||
|
@@ -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: {
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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.bind(this.core.context.logger)('发送消息删除文件失败', e));
|
||||
}
|
||||
} catch (error) {
|
||||
this.core.context.logger.logError.bind(this.core.context.logger)('发送消息删除文件失败', (error as Error).message);
|
||||
}
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
return returnMsg;
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
@@ -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;
|
||||
|
@@ -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';
|
||||
@@ -181,22 +183,37 @@ export class NapCatOneBot11Adapter {
|
||||
const newLog = await this.creatOneBotLog(now);
|
||||
this.context.logger.log(`[Notice] [OneBot11] 配置变更前:\n${prevLog}`);
|
||||
this.context.logger.log(`[Notice] [OneBot11] 配置变更后:\n${newLog}`);
|
||||
|
||||
await this.handleConfigChange(now.network.httpServers, OB11PassiveHttpAdapter);
|
||||
await this.handleConfigChange(now.network.httpClients, OB11ActiveHttpAdapter);
|
||||
await this.handleConfigChange(now.network.websocketServers, OB11PassiveWebSocketAdapter);
|
||||
await this.handleConfigChange(now.network.websocketClients, OB11ActiveWebSocketAdapter);
|
||||
|
||||
await this.handleConfigChange(prev.network.httpServers, now.network.httpServers, OB11PassiveHttpAdapter);
|
||||
await this.handleConfigChange(prev.network.httpClients, now.network.httpClients, OB11ActiveHttpAdapter);
|
||||
await this.handleConfigChange(prev.network.websocketServers, now.network.websocketServers, OB11PassiveWebSocketAdapter);
|
||||
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11ActiveWebSocketAdapter);
|
||||
}
|
||||
|
||||
private async handleConfigChange(adapters: NetworkConfigAdapter[], adapterClass: new (...args: any[]) => IOB11NetworkAdapter): Promise<void> {
|
||||
for (const adapterConfig of adapters) {
|
||||
|
||||
private async handleConfigChange(
|
||||
prevConfig: NetworkConfigAdapter[],
|
||||
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);
|
||||
if (existingAdapter) {
|
||||
const networkChange = await existingAdapter.reload(adapterConfig);
|
||||
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);
|
||||
}
|
||||
@@ -248,30 +265,49 @@ export class NapCatOneBot11Adapter {
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -377,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.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(
|
||||
this.core,
|
||||
parseInt(notify.group.groupCode),
|
||||
parseInt(requestUin),
|
||||
'add',
|
||||
notify.postscript,
|
||||
flag
|
||||
);
|
||||
this.networkManager
|
||||
.emitEvent(groupRequestEvent)
|
||||
.catch((e) =>
|
||||
this.context.logger.logError.bind(this.context.logger)('处理加群请求失败', e)
|
||||
);
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(this.context.logger)(
|
||||
'获取加群人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.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(groupRequestEvent)
|
||||
.emitEvent(groupInviteEvent)
|
||||
.catch((e) =>
|
||||
this.context.logger.logError.bind(this.context.logger)('处理加群请求失败', e)
|
||||
this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -511,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);
|
||||
@@ -582,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);
|
||||
@@ -593,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';
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -198,13 +198,11 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
|
||||
this.open();
|
||||
return OB11NetworkReloadType.NetWorkOpen;
|
||||
} else if (!newConfig.enable && wasEnabled) {
|
||||
console.log(newConfig.enable, wasEnabled);
|
||||
this.close();
|
||||
return OB11NetworkReloadType.NetWorkClose;
|
||||
}
|
||||
|
||||
if (oldPort !== newConfig.port || oldHost !== newConfig.host) {
|
||||
console.log(oldPort, newConfig.port, oldHost, newConfig.host);
|
||||
this.close();
|
||||
this.wsServer = new WebSocketServer({
|
||||
port: newConfig.port,
|
||||
|
@@ -1,11 +1,20 @@
|
||||
/**
|
||||
* @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();
|
||||
|
||||
/**
|
||||
@@ -26,49 +35,51 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
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();
|
||||
};
|
||||
// 启动后打印出相关地址
|
||||
|
||||
const port = config.port.toString(),
|
||||
searchParams = { token: config.token },
|
||||
path = `${config.prefix}/webui`;
|
||||
|
||||
// 打印日志(地址、token)
|
||||
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)}`);
|
||||
log(`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, port, path, searchParams)}`);
|
||||
log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port, path, searchParams)}`);
|
||||
|
||||
// 获取公网地址
|
||||
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)}`);
|
||||
log(`[NapCat] [WebUi] WebUi Publish Panel Url: ${createUrl(data.IP.IP, port, path, searchParams)}`);
|
||||
} catch (err) {
|
||||
logger.logError(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
|
||||
}
|
||||
});
|
||||
// ------------Over!------------
|
||||
}
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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);
|
||||
};
|
||||
|
13
src/webui/src/const/status.ts
Normal file
13
src/webui/src/const/status.ts
Normal 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,
|
||||
}
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -3,7 +3,6 @@ import { existsSync, readFileSync, writeFileSync } 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
|
||||
@@ -137,4 +132,3 @@ export class WebUiConfigWrapper {
|
||||
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
|
||||
}
|
||||
}
|
||||
|
||||
|
46
src/webui/src/middleware/auth.ts
Normal file
46
src/webui/src/middleware/auth.ts
Normal 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');
|
||||
}
|
9
src/webui/src/middleware/cors.ts
Normal file
9
src/webui/src/middleware/cors.ts
Normal 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();
|
||||
};
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -1,67 +1,30 @@
|
||||
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';
|
||||
|
||||
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);
|
||||
|
||||
export { router as ALLRouter };
|
||||
|
7
src/webui/src/types/config.d.ts
vendored
Normal file
7
src/webui/src/types/config.d.ts
vendored
Normal 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
12
src/webui/src/types/data.d.ts
vendored
Normal 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
7
src/webui/src/types/server.d.ts
vendored
Normal 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
9
src/webui/src/types/sign_token.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
interface WebUiCredentialInnerJson {
|
||||
CreatedTime: number;
|
||||
TokenEncoded: string;
|
||||
}
|
||||
|
||||
interface WebUiCredentialJson {
|
||||
Data: WebUiCredentialInnerJson;
|
||||
Hmac: string;
|
||||
}
|
1
src/webui/src/utils/check.ts
Normal file
1
src/webui/src/utils/check.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isEmpty = <T>(data: T) => data === undefined || data === null || data === '';
|
26
src/webui/src/utils/response.ts
Normal file
26
src/webui/src/utils/response.ts
Normal 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,
|
||||
});
|
||||
};
|
47
src/webui/src/utils/url.ts
Normal file
47
src/webui/src/utils/url.ts
Normal 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();
|
||||
};
|
@@ -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"
|
||||
]
|
||||
}
|
@@ -50,59 +50,63 @@ 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 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'),
|
||||
},
|
||||
formats: ['es'],
|
||||
fileName: (_, entryName) => `${entryName}.mjs`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...nodeModules, ...external],
|
||||
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',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
target: 'esnext',
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: {
|
||||
'napcat': 'src/framework/napcat.ts',
|
||||
'audio-worker': 'src/common/audio-worker.ts',
|
||||
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'),
|
||||
},
|
||||
formats: ['es'],
|
||||
fileName: (_, entryName) => `${entryName}.mjs`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...nodeModules, ...external],
|
||||
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') {
|
||||
|
Reference in New Issue
Block a user