Merge pull request #524 from NapNeko/refactor-config-webui

refactor: new config & vue webui & new network & new parseMsg
This commit is contained in:
手瓜一十雪 2024-11-16 12:53:46 +08:00 committed by GitHub
commit 3658547731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 2814 additions and 3506 deletions

24
napcat.webui/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
napcat.webui/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
napcat.webui/README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

View File

@ -0,0 +1,52 @@
import globals from 'globals';
import ts from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import prettier from 'eslint-plugin-prettier/recommended';
export default [
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
...ts.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
},
},
...vue.configs['flat/base'],
{
files: ['*.vue', '**/*.vue'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
},
{
rules: {
indent: ['error', 4],
semi: ['error', 'always'],
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'object-curly-spacing': ['error', 'always'],
'vue/v-for-delimiter-style': ['error', 'in'],
'vue/require-name-property': 'warn',
'vue/prefer-true-attribute-shorthand': 'warn',
'prefer-arrow-callback': 'warn',
},
},
prettier,
{
rules: {
'prettier/prettier': 'warn',
},
},
];

13
napcat.webui/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<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>
</html>

31
napcat.webui/package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "napcat.webui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"webui:lint": "eslint . --fix",
"webui:dev": "vite",
"webui:build": "vue-tsc -b && vite build",
"webui:preview": "vite preview"
},
"dependencies": {
"eslint-plugin-prettier": "^5.2.1",
"qrcode": "^1.5.4",
"tdesign-vue-next": "^1.10.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-vue": "^5.1.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.31.0",
"globals": "^15.12.0",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vue-tsc": "^2.1.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

7
napcat.webui/src/App.vue Normal file
View File

@ -0,0 +1,7 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup lang="ts"></script>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,185 @@
import { OneBotConfig } from '../../../src/onebot/config/config';
export class QQLoginManager {
private retCredential: string;
private readonly apiPrefix: string;
//调试时http://127.0.0.1:6099/api 打包时 ../api
constructor(retCredential: string, apiPrefix: string = '../api') {
this.retCredential = retCredential;
this.apiPrefix = apiPrefix;
}
// TODO:
public async GetOB11Config(): Promise<OneBotConfig> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/GetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data as OneBotConfig;
}
}
} catch (error) {
console.error('Error getting OB11 config:', error);
}
return {} as OneBotConfig;
}
public async SetOB11Config(config: OneBotConfig): Promise<boolean> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ config: JSON.stringify(config) }),
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return true;
}
}
} catch (error) {
console.error('Error setting OB11 config:', error);
}
return false;
}
public async checkQQLoginStatus(): Promise<boolean> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.isLogin;
}
}
} catch (error) {
console.error('Error checking QQ login status:', error);
}
return false;
}
public async checkWebUiLogined(): Promise<boolean> {
try {
const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (LoginResponse.status == 200) {
const LoginResponseJson = await LoginResponse.json();
if (LoginResponseJson.code == 0) {
return true;
}
}
} catch (error) {
console.error('Error checking web UI login status:', error);
}
return false;
}
public async loginWithToken(token: string): Promise<string | null> {
try {
const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token }),
});
const loginResponseJson = await loginResponse.json();
const retCode = loginResponseJson.code;
if (retCode === 0) {
this.retCredential = loginResponseJson.data.Credential;
return this.retCredential;
}
} catch (error) {
console.error('Error logging in with token:', error);
}
return null;
}
public async getQQLoginQrcode(): Promise<string> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.qrcode || '';
}
}
} catch (error) {
console.error('Error getting QQ login QR code:', error);
}
return '';
}
public async getQQQuickLoginList(): Promise<string[]> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data || [];
}
}
} catch (error) {
console.error('Error getting QQ quick login list:', error);
}
return [];
}
public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ uin: uin }),
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return { result: true, errMsg: '' };
} else {
return { result: false, errMsg: QQLoginResponseJson.message };
}
}
} catch (error) {
console.error('Error setting quick login:', error);
}
return { result: false, errMsg: '接口异常' };
}
}

View File

@ -0,0 +1,55 @@
<template>
<div class="dashboard-container">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
<div class="content">
<router-view />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import SidebarMenu from './webui/Nav.vue';
interface MenuItem {
value: string;
icon: string;
label: string;
route: string;
}
const menuItems = ref<MenuItem[]>([
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
{ value: 'item4', icon: 'setting', label: '其余配置', route: '/dashboard/other-config' },
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
]);
</script>
<style scoped>
.dashboard-container {
display: flex;
flex-direction: row;
height: 100vh;
}
.sidebar-menu {
position: relative;
z-index: 2;
}
.content {
flex: 1;
/* padding: 20px; */
overflow: auto;
position: relative;
z-index: 1;
}
@media (max-width: 768px) {
.content {
padding: 10px;
}
}
</style>

View File

@ -0,0 +1,167 @@
<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
>
</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>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as QRCode from 'qrcode';
import { useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
const loginMethod = ref<'quick' | 'qrcode'>('quick');
const quickLoginList = ref<string[]>([]);
const selectedAccount = ref<string>('');
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
let heartBeatTimer: number | null = null;
const selectAccount = async (accountName: string): Promise<void> => {
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
if (result) {
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' });
} else {
await MessagePlugin.error('登录失败,' + errMsg);
}
};
const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void => {
if (!canvas) {
console.error('Canvas element not found');
return;
}
QRCode.toCanvas(canvas, data, function (error: Error | null | undefined) {
if (error) {
console.error('Error generating QR Code:', error);
} else {
console.log('QR Code generated!');
}
});
};
const HeartBeat = async (): Promise<void> => {
const isLogined = await qqLoginManager.checkQQLoginStatus();
if (isLogined) {
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
await router.push({ path: '/dashboard/basic-info' });
}
};
const InitPages = async (): Promise<void> => {
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
const qrcodeData = await qqLoginManager.getQQLoginQrcode();
generateQrCode(qrcodeData, qrcodeCanvas.value);
heartBeatTimer = window.setInterval(HeartBeat, 3000);
};
onMounted(() => {
InitPages();
});
</script>
<style scoped>
.login-container {
padding: 20px;
border-radius: 5px;
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 0 auto;
}
@media (max-width: 600px) {
.login-container {
width: 90%;
min-width: unset;
}
}
.login-methods {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.login-method {
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.login-method.active {
background-color: #e6f0ff;
color: #007bff;
}
.login-form,
.qrcode {
display: flex;
flex-direction: column;
gap: 15px;
}
.qrcode {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
text-align: center;
}
.sotheby-font {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
margin: 0;
font-size: 0.875rem;
color: #888;
position: fixed;
bottom: 20px;
left: 0;
right: 0;
width: 100%;
background-color: white;
}
</style>

View File

@ -0,0 +1,151 @@
<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>
</template>
<script setup lang="ts">
import '../css/style.css';
import '../css/font.css';
import { reactive, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { LockOnIcon } from 'tdesign-icons-vue-next';
import { useRouter } from 'vue-router';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
interface FormData {
token: string;
}
const formData: FormData = reactive({
token: '',
});
const handleLoginSuccess = async (credential: string) => {
localStorage.setItem('auth', credential);
await checkLoginStatus();
};
const handleLoginFailure = (message: string) => {
MessagePlugin.error(message);
};
const checkLoginStatus = async () => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
return;
}
const loginManager = new QQLoginManager(storedCredential);
const isWenUiLoggedIn = await loginManager.checkWebUiLogined();
console.log('isWenUiLoggedIn', isWenUiLoggedIn);
if (!isWenUiLoggedIn) {
return;
}
const isQQLoggedIn = await loginManager.checkQQLoginStatus();
if (isQQLoggedIn) {
await router.push({ path: '/dashboard/basic-info' });
} else {
await router.push({ path: '/qqlogin' });
}
};
const loginWithToken = async (token: string) => {
const loginManager = new QQLoginManager('');
const credential = await loginManager.loginWithToken(token);
if (credential) {
await handleLoginSuccess(credential);
} else {
handleLoginFailure('登录失败请检查Token');
}
};
onMounted(() => {
const url = new URL(window.location.href);
const token = url.searchParams.get('token');
if (token) {
loginWithToken(token);
}
checkLoginStatus();
});
const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
if (validateResult) {
await loginWithToken(formData.token);
} else {
handleLoginFailure('请填写Token');
}
};
</script>
<style scoped>
.login-container {
padding: 20px;
border-radius: 5px;
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 0 auto;
}
@media (max-width: 600px) {
.login-container {
width: 90%;
min-width: unset;
}
}
.tdesign-demo-block-column {
display: flex;
flex-direction: column;
row-gap: 16px;
}
.tdesign-demo-block-column-large {
display: flex;
flex-direction: column;
row-gap: 32px;
}
.tdesign-demo-block-row {
display: flex;
column-gap: 16px;
align-items: center;
}
.sotheby-font {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
margin: 0;
font-size: 0.875rem;
color: #888;
position: fixed;
bottom: 20px;
left: 0;
right: 0;
width: 100%;
background-color: white;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
<template #logo> </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>
</router-link>
<template #operations>
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
<template #icon><t-icon :name="iconName" /></template>
</t-button>
</template>
</t-menu>
</template>
<script setup lang="ts">
import { ref, defineProps } from 'vue';
type MenuItem = {
value: string;
label: string;
route: string;
icon?: string;
disabled?: boolean;
};
defineProps<{
menuItems: MenuItem[];
}>();
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
const changeCollapsed = (): void => {
collapsed.value = !collapsed.value;
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
};
</script>
<style scoped>
.sidebar-menu {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 200px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.sidebar-menu {
width: 100px; /* 移动端侧边栏宽度 */
}
}
.logo-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.menu-item {
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,6 @@
@font-face {
font-family: 'Sotheby';
src: url('../assets/Sotheby.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,84 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
height: 100%;
width: 100%;
margin: 0;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

62
napcat.webui/src/main.ts Normal file
View File

@ -0,0 +1,62 @@
import { createApp } from 'vue';
import App from './App.vue';
import {
Button as TButton,
Input as TInput,
Form as TForm,
FormItem as TFormItem,
Select as TSelect,
Option as TOption,
Menu as TMenu,
MenuItem as TMenuItem,
Icon as TIcon,
Submenu as TSubmenu,
Col as TCol,
Row as TRow,
Card as TCard,
Divider as TDivider,
Link as TLink,
List as TList,
Alert as TAlert,
Tag as TTag,
ListItem as TListItem,
Tabs as TTabs,
TabPanel as TTabPanel,
Space as TSpace,
Checkbox as TCheckbox,
Popup as TPopup,
Dialog as TDialog,
Switch as TSwitch,
} 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);
app.use(TInput);
app.use(TForm);
app.use(TFormItem);
app.use(TSelect);
app.use(TOption);
app.use(TMenu);
app.use(TMenuItem);
app.use(TIcon);
app.use(TSubmenu);
app.use(TCol);
app.use(TRow);
app.use(TCard);
app.use(TDivider);
app.use(TLink);
app.use(TList);
app.use(TAlert);
app.use(TTag);
app.use(TListItem);
app.use(TTabs);
app.use(TTabPanel);
app.use(TSpace);
app.use(TCheckbox);
app.use(TPopup);
app.use(TDialog);
app.use(TSwitch);
app.mount('#app');

View File

@ -0,0 +1,63 @@
<template>
<div class="about-us">
<div>
<t-divider content="面板关于信息" align="left" />
<t-alert theme="success" message="NapCat.WebUi is running" />
<t-list class="list">
<t-list-item class="list-item">
<span class="item-label">开发人员:</span>
<span class="item-content">
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
</span>
</t-list-item>
<t-list-item class="list-item">
<span class="item-label">版本信息:</span>
<span class="item-content">
<t-tag class="tag-item" theme="success"> WebUi: 1.0.0 </t-tag>
<t-tag class="tag-item" theme="success"> NapCat: 4.?.? </t-tag>
<t-tag class="tag-item" theme="success"> Tdesign: 1.10.3 </t-tag>
</span>
</t-list-item>
</t-list>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.about-us {
padding: 20px;
text-align: left;
}
.list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-label {
flex: 1;
font-weight: bold;
}
.item-content {
flex: 2;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
}
.tag-item {
margin-right: 10px;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,6 @@
<template>
<div class="basic-info">
<h1>面板基础信息</h1>
<p>这里显示面板的基础信息</p>
</div>
</template>

View File

@ -0,0 +1,6 @@
<template>
<div class="log-view">
<h1>面板日志信息</h1>
<p>这里显示面板的日志信息</p>
</div>
</template>

View File

@ -0,0 +1,244 @@
<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>
</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-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-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>
</t-form>
</t-dialog>
</t-space>
</template>
<script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import {
httpServerDefaultConfigs,
httpClientDefaultConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig,
HttpServerConfig,
WebsocketClientConfig,
WebsocketServerConfig,
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';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
const componentMap: Record<
ConfigKey,
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent
> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
interface ClientPanel {
name: string;
key: ConfigKey;
data: Ref<ConfigUnion>;
}
type ComponentKey = keyof typeof componentMap;
// TODO: store these state in global store (aka pinia)
const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
const getComponent = (type: ComponentKey) => {
return componentMap[type];
};
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
};
const addConfigDataToPanel = (data: NetworkConfig) => {
Object.entries(data).forEach(([key, configs]) => {
if (key in defaultConfigs) {
addToPanel(configs as ConfigUnion[], key as ConfigKey);
}
});
};
const parsePanelData = (): NetworkConfig => {
return {
websocketClients: clientPanelData
.filter((panel) => panel.key === 'websocketClients')
.map((panel) => panel.data as WebsocketClientConfig),
websocketServers: clientPanelData
.filter((panel) => panel.key === 'websocketServers')
.map((panel) => panel.data as WebsocketServerConfig),
httpClients: clientPanelData
.filter((panel) => panel.key === 'httpClients')
.map((panel) => panel.data as HttpClientConfig),
httpServers: clientPanelData
.filter((panel) => panel.key === 'httpServers')
.map((panel) => panel.data as HttpServerConfig),
};
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network);
} catch (error) {
console.error('Error loading config:', error);
}
};
// It's better to "saveConfig" instead of using deep watch
const saveConfig = async () => {
const config = parsePanelData();
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = config;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
};
const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true;
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.some(panel => panel.name === name)) {
MessagePlugin.error('选项卡名称已存在');
return;
}
const defaultConfig = structuredClone(defaultConfigs[type]);
defaultConfig.name = name;
clientPanelData.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.length - 1;
MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<div>
<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>
<div class="button-container">
<t-button @click="saveConfig">保存</t-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { OneBotConfig } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
const otherConfig = ref<Partial<OneBotConfig>>({
musicSignUrl: '',
enableLocalFile2Url: false,
});
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (userConfig) {
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
}
} catch (error) {
console.error('Error loading config:', error);
}
};
const saveConfig = async () => {
try {
const userConfig = await getOB11Config();
if (userConfig) {
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
}
} catch (error) {
console.error('Error saving config:', error);
MessagePlugin.error('配置保存失败');
}
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.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;
border-radius: 8px;
}
.form {
display: flex;
flex-direction: column;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.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;
}
}
</style>

View File

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

View File

@ -0,0 +1,65 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Client 配置</h3>
<t-form>
<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-checkbox v-model="config.enable" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { HttpClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: HttpClientConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</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>

View File

@ -0,0 +1,71 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Server 配置</h3>
<t-form>
<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-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { HttpServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: HttpServerConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</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>

View File

@ -0,0 +1,68 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Client 配置</h3>
<t-form>
<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-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: WebsocketClientConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</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>

View File

@ -0,0 +1,74 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Server 配置</h3>
<t-form>
<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.enablePushEvent" />
</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-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: WebsocketServerConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</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>

View File

@ -0,0 +1,267 @@
<template>
<t-space class="full-space">
<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>
</t-tab-panel>
</t-tabs>
<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-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-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>
</t-form>
</t-dialog>
</t-space>
</template>
<script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import {
httpServerDefaultConfigs,
httpClientDefaultConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig,
HttpServerConfig,
WebsocketClientConfig,
WebsocketServerConfig,
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';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
const componentMap: Record<
ConfigKey,
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent
> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
interface ClientPanel {
name: string;
key: ConfigKey;
data: Ref<ConfigUnion>;
}
type ComponentKey = keyof typeof componentMap;
// TODO: store these state in global store (aka pinia)
const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
const getComponent = (type: ComponentKey) => {
return componentMap[type];
};
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
};
const addConfigDataToPanel = (data: NetworkConfig) => {
Object.entries(data).forEach(([key, configs]) => {
if (key in defaultConfigs) {
addToPanel(configs as ConfigUnion[], key as ConfigKey);
}
});
};
const parsePanelData = (): NetworkConfig => {
return {
websocketClients: clientPanelData
.filter((panel) => panel.key === 'websocketClients')
.map((panel) => panel.data as WebsocketClientConfig),
websocketServers: clientPanelData
.filter((panel) => panel.key === 'websocketServers')
.map((panel) => panel.data as WebsocketServerConfig),
httpClients: clientPanelData
.filter((panel) => panel.key === 'httpClients')
.map((panel) => panel.data as HttpClientConfig),
httpServers: clientPanelData
.filter((panel) => panel.key === 'httpServers')
.map((panel) => panel.data as HttpServerConfig),
};
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network);
} catch (error) {
console.error('Error loading config:', error);
}
};
// It's better to "saveConfig" instead of using deep watch
const saveConfig = async () => {
const config = parsePanelData();
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = config;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
};
const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true;
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.some(panel => panel.name === name)) {
MessagePlugin.error('选项卡名称已存在');
return;
}
const defaultConfig = structuredClone(defaultConfigs[type]);
defaultConfig.name = name;
clientPanelData.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.length - 1;
MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,32 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Dashboard from '../components/Dashboard.vue';
import BasicInfo from '../pages/BasicInfo.vue';
import AboutUs from '../pages/AboutUs.vue';
import LogView from '../pages/Log.vue';
import NetWork from '../pages/NetWork.vue';
import QQLogin from '../components/QQLogin.vue';
import WebUiLogin from '../components/WebUiLogin.vue';
import OtherConfig from '../pages/OtherConfig.vue';
const routes: Array<RouteRecordRaw> = [
{ path: '/', redirect: '/webui' },
{ path: '/webui', component: WebUiLogin, name: 'WebUiLogin' },
{ path: '/qqlogin', component: QQLogin, name: 'QQLogin' },
{
path: '/dashboard',
component: Dashboard,
children: [
{ path: '', redirect: 'basic-info' },
{ path: 'basic-info', component: BasicInfo, name: 'BasicInfo' },
{ path: 'network-config', component: NetWork, name: 'NetWork' },
{ path: 'log-view', component: LogView, name: 'LogView' },
{ path: 'other-config', component: OtherConfig, name: 'OtherConfig' },
{ path: 'about-us', component: AboutUs, name: 'AboutUs' },
],
},
];
export const router = createRouter({
history: createWebHashHistory(),
routes,
});

1
napcat.webui/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"jsxImportSource": "vue",
"lib": [
"DOM",
"DOM.Iterable"
],
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": [
"src/*"
]
},
"resolveJsonModule": true,
"types": [
"vite/client"
],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"experimentalDecorators": true,
"useDefineForClassFields": true
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [{"path": "./tsconfig.node.json"}]
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strictNullChecks": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
proxy: {
'/api': 'http://localhost:6099',
},
},
});

View File

@ -4,10 +4,13 @@
"type": "module",
"version": "4.0.3",
"scripts": {
"build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell",
"build:webui": "cd ./src/webui && vite build",
"lint": "eslint --fix src/**/*.{js,ts}",
"build:framework": "npm run build:webui && vite build --mode framework",
"build:shell": "npm run build:webui && vite build --mode shell",
"build:webui": "cd napcat.webui && vite build",
"dev:framework": "vite build --mode framework",
"dev:shell": "vite build --mode shell",
"dev:webui": "cd napcat.webui && npm run webui:dev",
"lint": "eslint --fix src/**/*.{js,ts,vue}",
"depend": "cd dist && npm install --omit=dev"
},
"devDependencies": {

View File

@ -45,7 +45,6 @@ try {
sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json
sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json
sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts
sed -i "s/SettingButton(\\"V.*\\", \\"napcat-update-button\\", \\"secondary\\")/SettingButton(\\"V${targetVersion}\\", \\"napcat-update-button\\", \\"secondary\\")/g" ./static/assets/renderer.js
git add .
git commit -m "release: v${targetVersion}"
git push -u origin main`;

View File

@ -11,8 +11,12 @@ const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const EXIT_CODES = [0, 255];
const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
async function getWorkerPath() {
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: new URL('./audio-worker.mjs', import.meta.url).href,
filename: await getWorkerPath(),
});
async function guessDuration(pttPath: string, logger: LogWrapper) {

View File

@ -8,12 +8,12 @@ export abstract class ConfigBase<T> {
configPath: string;
configData: T = {} as T;
protected constructor(name: string, core: NapCatCore, configPath: string) {
protected constructor(name: string, core: NapCatCore, configPath: string, copy_default: boolean = true) {
this.name = name;
this.core = core;
this.configPath = configPath;
fs.mkdirSync(this.configPath, { recursive: true });
this.read();
this.read(copy_default);
}
protected getKeys(): string[] | null {
@ -32,16 +32,18 @@ export abstract class ConfigBase<T> {
}
}
read(): T {
read(copy_default: boolean = true): T {
const logger = this.core.context.logger;
const configPath = this.getConfigPath(this.core.selfInfo.uin);
if (!fs.existsSync(configPath)) {
if (!fs.existsSync(configPath) && copy_default) {
try {
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
} catch (e: any) {
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
}
} else if (!fs.existsSync(configPath) && !copy_default) {
fs.writeFileSync(configPath, '{}');
}
try {
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));

View File

@ -410,27 +410,27 @@ export class NTQQFileApi {
if (!element) {
return '';
}
const url: string = element.originImageUrl ?? '';
const md5HexStr = element.md5HexStr;
const fileMd5 = element.md5HexStr;
if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
const rkeyData = await this.getRkeyData();
return this.getImageUrlFromParsedUrl(parsedUrl, rkeyData);
}
return this.getImageUrlFromMd5(fileMd5, md5HexStr);
}
private async getRkeyData() {
const rkeyData = {
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
online_rkey: false
};
try {
if (this.core.apis.PacketApi.available) {
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
@ -447,7 +447,7 @@ export class NTQQFileApi {
} catch (error: any) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
}
if (!rkeyData.online_rkey) {
try {
const tempRkeyData = await this.rkeyManager.getRkey();
@ -458,34 +458,30 @@ export class NTQQFileApi {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
}
}
return rkeyData;
}
private getImageUrlFromParsedUrl(parsedUrl: URL, rkeyData: any): string {
const urlRkey = parsedUrl.searchParams.get('rkey');
const imageAppid = parsedUrl.searchParams.get('appid');
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
const imageFileId = parsedUrl.searchParams.get('fileid');
if (isNTV2 && urlRkey) {
return IMAGE_HTTP_HOST_NT + urlRkey;
} else if (isNTV2 && rkeyData.online_rkey) {
if (isNTV2 && rkeyData.online_rkey) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
} else if (isNTV2 && imageFileId) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
}
return '';
}
private getImageUrlFromMd5(fileMd5: string | undefined, md5HexStr: string | undefined): string {
if (fileMd5 || md5HexStr) {
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr ?? '').toUpperCase()}/0`;
}
this.context.logger.logDebug('图片url获取失败', { fileMd5, md5HexStr });
return '';
}

View File

@ -1,5 +1,5 @@
{
"fileLog": true,
"fileLog": false,
"consoleLog": true,
"fileLogLevel": "debug",
"consoleLogLevel": "info",

View File

@ -37,13 +37,13 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
};
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
public async handle(payload: PayloadType, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 400);
}
try {
const resData = await this._handle(payload);
const resData = await this._handle(payload, adaptername);
return OB11Response.ok(resData);
} catch (e: any) {
this.core.context.logger.logError.bind(this.core.context.logger)('发生错误', e);
@ -51,13 +51,13 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
}
}
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
public async websocketHandle(payload: PayloadType, echo: any, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 1400, echo);
}
try {
const resData = await this._handle(payload);
const resData = await this._handle(payload, adaptername);
return OB11Response.ok(resData, echo);
} catch (e: any) {
this.core.context.logger.logError.bind(this.core.context.logger)('发生错误', e);
@ -65,7 +65,7 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
}
}
abstract _handle(payload: PayloadType): PromiseLike<ReturnDataType>;
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
}
export default BaseAction;

View File

@ -72,7 +72,7 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
}
const singleMsg = data.msgList[0];
const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
throw new Error('找不到相关的聊天记录');
}

View File

@ -26,7 +26,7 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GetFriendMsgHistory;
payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<Response> {
async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const MsgCount = +(payload.count ?? 20);
@ -45,9 +45,10 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<typeof this.obContext.configLoader.configData.network[keyof typeof this.obContext.configLoader.configData.network]>;
//烘焙消息
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg)))
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array')))
).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList };
}

View File

@ -26,7 +26,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<Response> {
async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
const MsgCount = +(payload.count ?? 20);
@ -43,9 +43,11 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<typeof this.obContext.configLoader.configData.network[keyof typeof this.obContext.configLoader.configData.network]>;
//烘焙消息
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg)))
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, msgFormat)))
).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList };
}

View File

@ -30,7 +30,9 @@ export class GetGroupEssence extends BaseAction<Payload, any> {
};
}
async _handle(payload: Payload) {
async _handle(payload: Payload, adapter: string) {
const network = Object.values(this.obContext.configLoader.configData.network) as Array<typeof this.obContext.configLoader.configData.network[keyof typeof this.obContext.configLoader.configData.network]>;
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
const msglist = (await this.core.apis.WebApi.getGroupEssenceMsgAll(payload.group_id.toString())).flatMap((e) => e.data.msg_list);
if (!msglist) {
throw new Error('获取失败');
@ -51,7 +53,7 @@ export class GetGroupEssence extends BaseAction<Payload, any> {
operator_nick: msg.add_digest_nick,
message_id: message_id,
operator_time: msg.add_digest_time,
content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage))?.message
content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage, msgFormat))?.message
};
}
const msgTempData = JSON.stringify({

View File

@ -22,8 +22,10 @@ class GetMsg extends BaseAction<Payload, OB11Message> {
actionName = ActionName.GetMsg;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
async _handle(payload: Payload, adapter: string) {
// log("history msg ids", Object.keys(msgHistory));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<typeof this.obContext.configLoader.configData.network[keyof typeof this.obContext.configLoader.configData.network]>;
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
if (!payload.message_id) {
throw Error('参数message_id不能为空');
}
@ -40,7 +42,7 @@ class GetMsg extends BaseAction<Payload, OB11Message> {
} else {
msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0];
}
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg);
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, msgFormat);
if (!retMsg) throw Error('消息为空');
try {
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!;

View File

@ -15,13 +15,16 @@ export default class GetRecentContact extends BaseAction<Payload, any> {
actionName = ActionName.GetRecentContact;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
async _handle(payload: Payload, adapter: string) {
const ret = await this.core.apis.UserApi.getRecentContactListSnapShot(+(payload.count || 10));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<typeof this.obContext.configLoader.configData.network[keyof typeof this.obContext.configLoader.configData.network]>;
//烘焙消息
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
return await Promise.all(ret.info.changedList.map(async (t) => {
const FastMsg = await this.core.apis.MsgApi.getMsgsByMsgId({ chatType: t.chatType, peerUid: t.peerUid }, [t.msgId]);
if (FastMsg.msgList.length > 0) {
//扩展ret.info.changedList
const lastestMsg = await this.obContext.apis.MsgApi.parseMessage(FastMsg.msgList[0]);
const lastestMsg = await this.obContext.apis.MsgApi.parseMessage(FastMsg.msgList[0], msgFormat);
return {
lastestMsg: lastestMsg,
peerUin: t.peerUin,

View File

@ -368,7 +368,7 @@ export class OneBotMsgApi {
multiMsgItem.parentMsgPeer = parentMsgPeer;
multiMsgItem.parentMsgIdList = msg.parentMsgIdList;
multiMsgItem.id = MessageUnique.createUniqueMsgId(parentMsgPeer, multiMsgItem.msgId); //该ID仅用查看 无法调用
return await this.parseMessage(multiMsgItem);
return await this.parseMessage(multiMsgItem, 'array');
},
))).filter(item => item !== undefined),
},
@ -693,7 +693,16 @@ export class OneBotMsgApi {
async parseMessage(
msg: RawMessage,
messagePostFormat: string = this.obContext.configLoader.configData.messagePostFormat,
messagePostFormat: string,
) {
if (messagePostFormat === 'string') {
return (await this.parseMessageV2(msg))?.stringMsg;
}
return (await this.parseMessageV2(msg))?.arrayMsg;
}
async parseMessageV2(
msg: RawMessage,
) {
if (msg.senderUin == '0' || msg.senderUin == '') return;
if (msg.peerUin == '0' || msg.peerUin == '') return;
@ -714,8 +723,8 @@ export class OneBotMsgApi {
raw_message: '',
font: 14,
sub_type: 'friend',
message: messagePostFormat === 'string' ? '' : [],
message_format: messagePostFormat === 'string' ? 'string' : 'array',
message: [],
message_format: 'array',
post_type: this.core.selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
};
if (this.core.selfInfo.uin == msg.senderUin) {
@ -785,17 +794,17 @@ export class OneBotMsgApi {
}).map((entry) => (<PromiseFulfilledResult<OB11MessageData>>entry).value).filter(value => value != null);
const msgAsCQCode = validSegments.map(msg => encodeCQCode(msg)).join('').trim();
if (messagePostFormat === 'string') {
resMsg.message = msgAsCQCode;
resMsg.raw_message = msgAsCQCode;
} else {
resMsg.message = validSegments;
resMsg.raw_message = msgAsCQCode;
}
return resMsg;
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) {
msg.message_format = 'string';
msg.message = msg.raw_message;
return msg;
}
async createSendElements(
messageData: OB11MessageData[],
peer: Peer,

206
src/onebot/config/config.ts Normal file
View File

@ -0,0 +1,206 @@
interface v1Config {
http: {
enable: boolean;
host: string;
port: number;
secret: string;
enableHeart: boolean;
enablePost: boolean;
postUrls: string[];
};
ws: {
enable: boolean;
host: string;
port: number;
};
reverseWs: {
enable: boolean;
urls: string[];
};
debug: boolean;
heartInterval: number;
messagePostFormat: string;
enableLocalFile2Url: boolean;
musicSignUrl: string;
reportSelfMessage: boolean;
token: string;
}
export interface AdapterConfig {
name: string;
enable: boolean;
[key: string]: any;
}
const createDefaultAdapterConfig = <T extends AdapterConfig>(config: T): T => config;
export const httpServerDefaultConfigs = createDefaultAdapterConfig({
name: 'http-server',
enable: false as boolean,
port: 3000,
host: '0.0.0.0',
enableCors: true,
enableWebsocket: true,
messagePostFormat: 'array',
token: '',
debug: false,
});
export type HttpServerConfig = typeof httpServerDefaultConfigs;
export const httpClientDefaultConfigs = createDefaultAdapterConfig({
name: 'http-client',
enable: false as boolean,
url: 'http://localhost:8080',
messagePostFormat: 'array',
reportSelfMessage: false,
token: '',
debug: false,
});
export type HttpClientConfig = typeof httpClientDefaultConfigs;
export const websocketServerDefaultConfigs = createDefaultAdapterConfig({
name: 'websocket-server',
enable: false as boolean,
host: '0.0.0.0',
port: 3001,
messagePostFormat: 'array',
reportSelfMessage: false,
token: '',
enablePushEvent: true,
debug: false,
heartInterval: 30000,
});
export type WebsocketServerConfig = typeof websocketServerDefaultConfigs;
export const websocketClientDefaultConfigs = createDefaultAdapterConfig({
name: 'websocket-client',
enable: false as boolean,
url: 'ws://localhost:8082',
messagePostFormat: 'array',
reportSelfMessage: false,
token: '',
debug: false,
heartInterval: 30000,
});
export type WebsocketClientConfig = typeof websocketClientDefaultConfigs;
export interface NetworkConfig {
httpServers: Array<HttpServerConfig>;
httpClients: Array<HttpClientConfig>;
websocketServers: Array<WebsocketServerConfig>;
websocketClients: Array<WebsocketClientConfig>;
}
export function mergeConfigs<T extends AdapterConfig>(defaultConfig: T, userConfig: Partial<T>): T {
return { ...defaultConfig, ...userConfig };
}
export interface OneBotConfig {
network: NetworkConfig; // 网络配置
musicSignUrl: string; // 音乐签名地址
enableLocalFile2Url: boolean;
}
const createDefaultConfig = <T>(config: T): T => config;
export const defaultOneBotConfigs = createDefaultConfig<OneBotConfig>({
network: {
httpServers: [],
httpClients: [],
websocketServers: [],
websocketClients: [],
},
musicSignUrl: '',
enableLocalFile2Url: false,
});
export const mergeNetworkDefaultConfig = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
} as const;
type NetworkConfigKeys = keyof typeof mergeNetworkDefaultConfig;
export function mergeOneBotConfigs(
userConfig: Partial<OneBotConfig>,
defaultConfig: OneBotConfig = defaultOneBotConfigs
): OneBotConfig {
const mergedConfig = { ...defaultConfig };
if (userConfig.network) {
mergedConfig.network = { ...defaultConfig.network };
for (const key in userConfig.network) {
const userNetworkConfig = userConfig.network[key as keyof NetworkConfig];
const defaultNetworkConfig = mergeNetworkDefaultConfig[key as NetworkConfigKeys];
if (Array.isArray(userNetworkConfig)) {
mergedConfig.network[key as keyof NetworkConfig] = userNetworkConfig.map<any>((e) =>
mergeConfigs(defaultNetworkConfig, e)
);
}
}
}
if (userConfig.musicSignUrl !== undefined) {
mergedConfig.musicSignUrl = userConfig.musicSignUrl;
}
return mergedConfig;
}
function checkIsOneBotConfigV1(v1Config: Partial<v1Config>): boolean {
return v1Config.http !== undefined || v1Config.ws !== undefined || v1Config.reverseWs !== undefined;
}
export function migrateOneBotConfigsV1(config: Partial<v1Config>): OneBotConfig {
if (!checkIsOneBotConfigV1(config)) {
return config as OneBotConfig;
}
const mergedConfig = { ...defaultOneBotConfigs };
if (config.http) {
mergedConfig.network.httpServers = [
mergeConfigs(httpServerDefaultConfigs, {
enable: config.http.enable,
port: config.http.port,
host: config.http.host,
token: config.http.secret,
debug: config.debug,
messagePostFormat: config.messagePostFormat,
}),
];
}
if (config.ws) {
mergedConfig.network.websocketServers = [
mergeConfigs(websocketServerDefaultConfigs, {
enable: config.ws.enable,
port: config.ws.port,
host: config.ws.host,
token: config.token,
debug: config.debug,
messagePostFormat: config.messagePostFormat,
reportSelfMessage: config.reportSelfMessage,
}),
];
}
if (config.reverseWs) {
mergedConfig.network.websocketClients = config.reverseWs.urls.map((url) =>
mergeConfigs(websocketClientDefaultConfigs, {
enable: config.reverseWs?.enable,
url: url,
token: config.token,
debug: config.debug,
messagePostFormat: config.messagePostFormat,
reportSelfMessage: config.reportSelfMessage,
})
);
}
if (config.heartInterval) {
mergedConfig.network.websocketServers[0].heartInterval = config.heartInterval;
}
if (config.musicSignUrl) {
mergedConfig.musicSignUrl = config.musicSignUrl;
}
if (config.enableLocalFile2Url) {
mergedConfig.enableLocalFile2Url = config.enableLocalFile2Url;
}
return mergedConfig;
}

View File

@ -1,11 +1,9 @@
import { ConfigBase } from '@/common/config-base';
import ob11DefaultConfig from './onebot11.json';
import { NapCatCore } from '@/core';
import { OneBotConfig } from './config';
export type OB11Config = typeof ob11DefaultConfig;
export class OB11ConfigLoader extends ConfigBase<OB11Config> {
export class OB11ConfigLoader extends ConfigBase<OneBotConfig> {
constructor(core: NapCatCore, configPath: string) {
super('onebot11', core, configPath);
super('onebot11', core, configPath, false);
}
}

View File

@ -1,31 +0,0 @@
{
"http": {
"enable": false,
"host": "",
"port": 3000,
"secret": "",
"enableHeart": false,
"enablePost": false,
"postUrls": []
},
"ws": {
"enable": false,
"host": "",
"port": 3001
},
"reverseWs": {
"enable": false,
"urls": []
},
"GroupLocalTime": {
"Record": false,
"RecordList": []
},
"debug": false,
"heartInterval": 30000,
"messagePostFormat": "array",
"enableLocalFile2Url": true,
"musicSignUrl": "",
"reportSelfMessage": false,
"token": ""
}

View File

@ -14,7 +14,7 @@ import {
RawMessage,
SendStatusType,
} from '@/core';
import { OB11Config, OB11ConfigLoader } from '@/onebot/config';
import { OB11ConfigLoader } from '@/onebot/config';
import {
OB11ActiveHttpAdapter,
OB11ActiveWebSocketAdapter,
@ -45,6 +45,8 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal
import { LRUCache } from '@/common/lru-cache';
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
import { BotOfflineEvent } from './event/notice/BotOfflineEvent';
import { mergeOneBotConfigs, migrateOneBotConfigsV1, OneBotConfig } from './config/config';
import { OB11Message } from './types';
//OneBot实现类
export class NapCatOneBot11Adapter {
@ -62,6 +64,8 @@ export class NapCatOneBot11Adapter {
this.core = core;
this.context = context;
this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath);
this.configLoader.save(migrateOneBotConfigsV1(this.configLoader.configData));
this.configLoader.save(mergeOneBotConfigs(this.configLoader.configData));
this.apis = {
GroupApi: new OneBotGroupApi(this, core),
UserApi: new OneBotUserApi(this, core),
@ -72,70 +76,102 @@ export class NapCatOneBot11Adapter {
this.actions = createActionMap(this, core);
this.networkManager = new OB11NetworkManager();
}
async creatOneBotLog(ob11Config: OneBotConfig) {
let log = `[network] 配置加载\n`;
for (const key of ob11Config.network.httpServers) {
log += `HTTP服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
for (const key of ob11Config.network.httpClients) {
log += `HTTP上报服务: ${key.url}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
for (const key of ob11Config.network.websocketServers) {
log += `WebSocket服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
for (const key of ob11Config.network.websocketClients) {
log += `WebSocket反向服务: ${key.url}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
return log;
}
async InitOneBot() {
const selfInfo = this.core.selfInfo;
const ob11Config = this.configLoader.configData;
const serviceInfo = `
HTTP服务 ${ob11Config.http.enable ? '已启动' : '未启动'}, ${ob11Config.http.host}:${ob11Config.http.port}
HTTP上报服务 ${ob11Config.http.enablePost ? '已启动' : '未启动'}, 上报地址: ${ob11Config.http.postUrls}
WebSocket服务 ${ob11Config.ws.enable ? '已启动' : '未启动'}, ${ob11Config.ws.host}:${ob11Config.ws.port}
WebSocket反向服务 ${ob11Config.reverseWs.enable ? '已启动' : '未启动'}, 反向地址: ${ob11Config.reverseWs.urls}`;
this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid)
.then((user) => {
selfInfo.nick = user.nick;
this.context.logger.setLogSelfInfo(selfInfo);
})
.catch(this.context.logger.logError.bind(this.context.logger));
this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid).then(user => {
selfInfo.nick = user.nick;
this.context.logger.setLogSelfInfo(selfInfo);
}).catch(this.context.logger.logError.bind(this.context.logger));
const serviceInfo = await this.creatOneBotLog(ob11Config);
this.context.logger.log(`[Notice] [OneBot11] ${serviceInfo}`);
//创建NetWork服务
if (ob11Config.http.enable) {
this.networkManager.registerAdapter(new OB11PassiveHttpAdapter(
ob11Config.http.port, ob11Config.token, this.core, this.actions,
));
// //创建NetWork服务
for (const key of ob11Config.network.httpServers) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11PassiveHttpAdapter(key.name, key.port, key.token, this.core, this.actions)
);
}
}
if (ob11Config.http.enablePost) {
ob11Config.http.postUrls.forEach(url => {
this.networkManager.registerAdapter(new OB11ActiveHttpAdapter(
url, ob11Config.http.secret, this.core, this,
));
});
for (const key of ob11Config.network.httpClients) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11ActiveHttpAdapter(key.name, key.url, key.token, this.core, this)
);
}
}
if (ob11Config.ws.enable) {
const OBPassiveWebSocketAdapter = new OB11PassiveWebSocketAdapter(
ob11Config.ws.host, ob11Config.ws.port, ob11Config.heartInterval, ob11Config.token, this.core, this.actions,
);
this.networkManager.registerAdapter(OBPassiveWebSocketAdapter);
for (const key of ob11Config.network.websocketServers) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11PassiveWebSocketAdapter(
key.name,
key.host,
key.port,
key.heartInterval,
key.token,
this.core,
this.actions
)
);
}
}
if (ob11Config.reverseWs.enable) {
ob11Config.reverseWs.urls.forEach(url => {
this.networkManager.registerAdapter(new OB11ActiveWebSocketAdapter(
url, 5000, ob11Config.heartInterval, ob11Config.token, this.core, this.actions,
));
});
for (const key of ob11Config.network.websocketClients) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11ActiveWebSocketAdapter(
key.name,
key.url,
5000,
key.heartInterval,
key.token,
this.core,
this.actions
)
);
}
}
await this.networkManager.openAllAdapters();
this.initMsgListener();
this.initBuddyListener();
this.initGroupListener();
//this.initRecentContactListener();
await WebUiDataRuntime.setQQLoginUin(selfInfo.uin.toString());
await WebUiDataRuntime.setQQLoginStatus(true);
await WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig: OB11Config) => {
await WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
const prev = this.configLoader.configData;
this.configLoader.save(newConfig);
this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
await this.reloadNetwork(prev, newConfig);
//await this.reloadNetwork(prev, newConfig);
});
}
initRecentContactListener() {
const recentContactListener = new NodeIKernelRecentContactListener();
recentContactListener.onRecentContactNotification = function (msgList: any[] /* arg0: { msgListUnreadCnt: string }, arg1: number */) {
recentContactListener.onRecentContactNotification = function (
msgList: any[] /* arg0: { msgListUnreadCnt: string }, arg1: number */
) {
msgList.forEach((msg) => {
if (msg.chatType == ChatType.KCHATTYPEGROUP) {
// log("recent contact", msgList, arg0, arg1);
@ -144,115 +180,120 @@ export class NapCatOneBot11Adapter {
};
}
private async reloadNetwork(prev: OB11Config, now: OB11Config) {
const serviceInfo = `
HTTP服务 ${now.http.enable ? '已启动' : '未启动'}, ${now.http.host}:${now.http.port}
HTTP上报服务 ${now.http.enablePost ? '已启动' : '未启动'}, 上报地址: ${now.http.postUrls}
WebSocket服务 ${now.ws.enable ? '已启动' : '未启动'}, ${now.ws.host}:${now.ws.port}
WebSocket反向服务 ${now.reverseWs.enable ? '已启动' : '未启动'}, 反向地址: ${now.reverseWs.urls}`;
this.context.logger.log(`[Notice] [OneBot11] 热重载 ${serviceInfo}`);
// private async reloadNetwork(prev: OB11Config, now: OB11Config) {
// const serviceInfo = `
// HTTP服务 ${now.http.enable ? '已启动' : '未启动'}, ${now.http.host}:${now.http.port}
// HTTP上报服务 ${now.http.enablePost ? '已启动' : '未启动'}, 上报地址: ${now.http.postUrls}
// WebSocket服务 ${now.ws.enable ? '已启动' : '未启动'}, ${now.ws.host}:${now.ws.port}
// WebSocket反向服务 ${now.reverseWs.enable ? '已启动' : '未启动'}, 反向地址: ${now.reverseWs.urls}`;
// this.context.logger.log(`[Notice] [OneBot11] 热重载 ${serviceInfo}`);
// check difference in passive http (Http)
if (prev.http.enable !== now.http.enable) {
if (now.http.enable) {
await this.networkManager.registerAdapterAndOpen(new OB11PassiveHttpAdapter(
now.http.port, now.token, this.core, this.actions,
));
} else {
await this.networkManager.closeAdapterByPredicate(adapter => adapter instanceof OB11PassiveHttpAdapter);
}
}
// // check difference in passive http (Http)
// if (prev.http.enable !== now.http.enable) {
// if (now.http.enable) {
// await this.networkManager.registerAdapterAndOpen(new OB11PassiveHttpAdapter(
// now.http.port, now.token, this.core, this.actions,
// ));
// } else {
// await this.networkManager.closeAdapterByPredicate(adapter => adapter instanceof OB11PassiveHttpAdapter);
// }
// }
// check difference in active http (HttpPost)
if (prev.http.enablePost !== now.http.enablePost) {
if (now.http.enablePost) {
now.http.postUrls.forEach(url => {
this.networkManager.registerAdapterAndOpen(new OB11ActiveHttpAdapter(
url, now.http.secret, this.core, this,
));
});
} else {
await this.networkManager.closeAdapterByPredicate(adapter => adapter instanceof OB11ActiveHttpAdapter);
}
} else if (now.http.enablePost) {
const { added, removed } = this.findDifference<string>(prev.http.postUrls, now.http.postUrls);
await this.networkManager.closeAdapterByPredicate(
adapter => adapter instanceof OB11ActiveHttpAdapter && removed.includes(adapter.url),
);
for (const url of added) {
await this.networkManager.registerAdapterAndOpen(new OB11ActiveHttpAdapter(
url, now.http.secret, this.core, this,
));
}
}
// // check difference in active http (HttpPost)
// if (prev.http.enablePost !== now.http.enablePost) {
// if (now.http.enablePost) {
// now.http.postUrls.forEach(url => {
// this.networkManager.registerAdapterAndOpen(new OB11ActiveHttpAdapter(
// url, now.http.secret, this.core, this,
// ));
// });
// } else {
// await this.networkManager.closeAdapterByPredicate(adapter => adapter instanceof OB11ActiveHttpAdapter);
// }
// } else if (now.http.enablePost) {
// const { added, removed } = this.findDifference<string>(prev.http.postUrls, now.http.postUrls);
// await this.networkManager.closeAdapterByPredicate(
// adapter => adapter instanceof OB11ActiveHttpAdapter && removed.includes(adapter.url),
// );
// for (const url of added) {
// await this.networkManager.registerAdapterAndOpen(new OB11ActiveHttpAdapter(
// url, now.http.secret, this.core, this,
// ));
// }
// }
// // check difference in passive websocket (Ws)
// if (prev.ws.enable !== now.ws.enable) {
// if (now.ws.enable) {
// await this.networkManager.registerAdapterAndOpen(new OB11PassiveWebSocketAdapter(
// now.ws.host, now.ws.port, now.heartInterval, now.token, this.core, this.actions,
// ));
// } else {
// await this.networkManager.closeAdapterByPredicate(
// adapter => adapter instanceof OB11PassiveWebSocketAdapter,
// );
// }
// }
// check difference in passive websocket (Ws)
if (prev.ws.enable !== now.ws.enable) {
if (now.ws.enable) {
await this.networkManager.registerAdapterAndOpen(new OB11PassiveWebSocketAdapter(
now.ws.host, now.ws.port, now.heartInterval, now.token, this.core, this.actions,
));
} else {
await this.networkManager.closeAdapterByPredicate(
adapter => adapter instanceof OB11PassiveWebSocketAdapter,
);
}
}
// // check difference in active websocket (ReverseWs)
// if (prev.reverseWs.enable !== now.reverseWs.enable) {
// if (now.reverseWs.enable) {
// now.reverseWs.urls.forEach(url => {
// this.networkManager.registerAdapterAndOpen(new OB11ActiveWebSocketAdapter(
// url, 5000, now.heartInterval, now.token, this.core, this.actions,
// ));
// });
// } else {
// await this.networkManager.closeAdapterByPredicate(
// adapter => adapter instanceof OB11ActiveWebSocketAdapter,
// );
// }
// } else if (now.reverseWs.enable) {
// const { added, removed } = this.findDifference<string>(prev.reverseWs.urls, now.reverseWs.urls);
// await this.networkManager.closeAdapterByPredicate(
// adapter => adapter instanceof OB11ActiveWebSocketAdapter && removed.includes(adapter.url),
// );
// for (const url of added) {
// await this.networkManager.registerAdapterAndOpen(new OB11ActiveWebSocketAdapter(
// url, 5000, now.heartInterval, now.token, this.core, this.actions,
// ));
// }
// }
// check difference in active websocket (ReverseWs)
if (prev.reverseWs.enable !== now.reverseWs.enable) {
if (now.reverseWs.enable) {
now.reverseWs.urls.forEach(url => {
this.networkManager.registerAdapterAndOpen(new OB11ActiveWebSocketAdapter(
url, 5000, now.heartInterval, now.token, this.core, this.actions,
));
});
} else {
await this.networkManager.closeAdapterByPredicate(
adapter => adapter instanceof OB11ActiveWebSocketAdapter,
);
}
} else if (now.reverseWs.enable) {
const { added, removed } = this.findDifference<string>(prev.reverseWs.urls, now.reverseWs.urls);
await this.networkManager.closeAdapterByPredicate(
adapter => adapter instanceof OB11ActiveWebSocketAdapter && removed.includes(adapter.url),
);
for (const url of added) {
await this.networkManager.registerAdapterAndOpen(new OB11ActiveWebSocketAdapter(
url, 5000, now.heartInterval, now.token, this.core, this.actions,
));
}
}
// }
}
private findDifference<T>(prev: T[], now: T[]): { added: T[], removed: T[] } {
const added = now.filter(item => !prev.includes(item));
const removed = prev.filter(item => !now.includes(item));
private findDifference<T>(prev: T[], now: T[]): { added: T[]; removed: T[] } {
const added = now.filter((item) => !prev.includes(item));
const removed = prev.filter((item) => !now.includes(item));
return { added, removed };
}
private initMsgListener() {
const msgListener = new NodeIKernelMsgListener();
msgListener.onRecvSysMsg = (msg) => {
this.apis.MsgApi.parseSysMessage(msg).then((event) => {
if (event) this.networkManager.emitEvent(event);
}).catch(e => this.context.logger.logError.bind(this.context.logger)('constructSysMessage error: ', e, '\n Parse Hex:', Buffer.from(msg).toString('hex')));
this.apis.MsgApi.parseSysMessage(msg)
.then((event) => {
if (event) this.networkManager.emitEvent(event);
})
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)(
'constructSysMessage error: ',
e,
'\n Parse Hex:',
Buffer.from(msg).toString('hex')
)
);
};
msgListener.onInputStatusPush = async data => {
msgListener.onInputStatusPush = async (data) => {
const uin = await this.core.apis.UserApi.getUinByUidV2(data.fromUin);
this.context.logger.log(`[Notice] [输入状态] ${uin} ${data.statusText}`);
await this.networkManager.emitEvent(new OB11InputStatusEvent(
this.core,
parseInt(uin),
data.eventType,
data.statusText,
));
await this.networkManager.emitEvent(
new OB11InputStatusEvent(this.core, parseInt(uin), data.eventType, data.statusText)
);
};
msgListener.onRecvMsg = async msg => {
msgListener.onRecvMsg = async (msg) => {
for (const m of msg) {
if (this.bootTime > parseInt(m.msgTime)) {
this.context.logger.logDebug(`消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`);
@ -264,59 +305,54 @@ export class NapCatOneBot11Adapter {
peerUid: m.peerUid,
guildId: '',
},
m.msgId,
m.msgId
);
await this.emitMsg(m).catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理消息失败', e)
);
await this.emitMsg(m)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理消息失败', e));
}
};
const msgIdSend = new LRUCache<string, number>(100);
const recallMsgs = new LRUCache<string, boolean>(100);
msgListener.onAddSendMsg = async msg => {
msgListener.onAddSendMsg = async (msg) => {
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
msgIdSend.put(msg.msgId, 0);
}
};
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)) {
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
this.apis.MsgApi.parseMessage(msg)
.then((ob11Msg) => {
if (!ob11Msg) return;
ob11Msg.target_id = parseInt(msg.peerUin);
if (this.configLoader.configData.reportSelfMessage) {
msg.id = MessageUnique.createUniqueMsgId({
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
}, msg.msgId);
this.emitMsg(msg);
} else {
// logOB11Message(this.core, ob11Msg);
}
});
msg.id = MessageUnique.createUniqueMsgId(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
},
msg.msgId
);
this.emitMsg(msg);
}
}
};
msgListener.onKickedOffLine = async (kick) => {
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);
this.networkManager.emitEvent(event)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理Bot掉线失败', e));
this.networkManager
.emitEvent(event)
.catch((e) => this.context.logger.logError.bind(this.context.logger)('处理Bot掉线失败', e));
};
this.context.session.getMsgService().addKernelMsgListener(
proxiedListenerOf(msgListener, this.context.logger),
);
this.context.session.getMsgService().addKernelMsgListener(proxiedListenerOf(msgListener, this.context.logger));
}
private initBuddyListener() {
const buddyListener = new NodeIKernelBuddyListener();
buddyListener.onBuddyReqChange = async reqs => {
buddyListener.onBuddyReqChange = async (reqs) => {
this.core.apis.FriendApi.clearBuddyReqUnreadCnt();
for (let i = 0; i < reqs.unreadNums; i++) {
const req = reqs.buddyReqs[i];
@ -325,21 +361,23 @@ export class NapCatOneBot11Adapter {
}
try {
const requesterUin = await this.core.apis.UserApi.getUinByUidV2(req.friendUid);
await this.networkManager.emitEvent(new OB11FriendRequestEvent(
this.core,
+requesterUin,
req.extWords,
req.friendUid + '|' + req.reqTime,
));
await this.networkManager.emitEvent(
new OB11FriendRequestEvent(
this.core,
+requesterUin,
req.extWords,
req.friendUid + '|' + req.reqTime
)
);
} catch (e) {
this.context.logger.logDebug('获取加好友者QQ号失败', e);
}
}
};
this.context.session.getBuddyService().addKernelBuddyListener(
proxiedListenerOf(buddyListener, this.context.logger),
);
this.context.session
.getBuddyService()
.addKernelBuddyListener(proxiedListenerOf(buddyListener, this.context.logger));
}
private initGroupListener() {
@ -348,11 +386,13 @@ export class NapCatOneBot11Adapter {
groupListener.onGroupNotifiesUpdated = async (_, notifies) => {
//console.log('ob11 onGroupNotifiesUpdated', notifies[0]);
await this.core.apis.GroupApi.clearGroupNotifiesUnreadCount(false);
if (![
GroupNotifyMsgType.SET_ADMIN,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN,
].includes(notifies[0]?.type)) {
if (
![
GroupNotifyMsgType.SET_ADMIN,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN,
].includes(notifies[0]?.type)
) {
for (const notify of notifies) {
const notifyTime = parseInt(notify.seq) / 1000 / 1000;
// log(`群通知时间${notifyTime}`, `启动时间${this.bootTime}`);
@ -363,15 +403,19 @@ export class NapCatOneBot11Adapter {
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type;
this.context.logger.logDebug('收到群通知', notify);
if ([
GroupNotifyMsgType.SET_ADMIN,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN,
].includes(notify.type)) {
const member1 = await this.core.apis.GroupApi.getGroupMember(notify.group.groupCode, notify.user1.uid);
if (
[
GroupNotifyMsgType.SET_ADMIN,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN,
].includes(notify.type)
) {
const member1 = await this.core.apis.GroupApi.getGroupMember(
notify.group.groupCode,
notify.user1.uid
);
this.context.logger.logDebug('有管理员变动通知');
// refreshGroupMembers(notify.group.groupCode).then();
this.context.logger.logDebug('开始获取变动的管理员');
if (member1) {
this.context.logger.logDebug('变动管理员获取成功');
@ -382,16 +426,28 @@ export class NapCatOneBot11Adapter {
[
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED,
GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN,
].includes(notify.type) ? 'unset' : 'set',
].includes(notify.type)
? 'unset'
: 'set'
);
this.networkManager.emitEvent(groupAdminNoticeEvent)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e));
this.networkManager
.emitEvent(groupAdminNoticeEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e)
);
} else {
this.context.logger.logDebug('获取群通知的成员信息失败', notify, this.core.apis.GroupApi.getGroup(notify.group.groupCode));
this.context.logger.logDebug(
'获取群通知的成员信息失败',
notify,
this.core.apis.GroupApi.getGroup(notify.group.groupCode)
);
}
} else if (notify.type == GroupNotifyMsgType.MEMBER_LEAVE_NOTIFY_ADMIN || notify.type == GroupNotifyMsgType.KICK_MEMBER_NOTIFY_ADMIN) {
} 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));
const member1Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
let operatorId = member1Uin;
let subType: GroupDecreaseSubType = 'leave';
if (notify.user2.uid) {
@ -407,17 +463,21 @@ export class NapCatOneBot11Adapter {
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
subType
);
this.networkManager.emitEvent(groupDecreaseEvent)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理群成员退出失败', e));
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) {
} 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));
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;
}
@ -427,14 +487,24 @@ export class NapCatOneBot11Adapter {
parseInt(requestUin),
'add',
notify.postscript,
flag,
flag
);
this.networkManager.emitEvent(groupRequestEvent)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理加群请求失败', e));
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);
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) {
} else if (
notify.type == GroupNotifyMsgType.INVITED_BY_MEMBER &&
notify.status == GroupNotifyMsgStatus.KUNHANDLE
) {
this.context.logger.logDebug(`收到邀请我加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent(
this.core,
@ -442,11 +512,17 @@ export class NapCatOneBot11Adapter {
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid)),
'invite',
notify.postscript,
flag,
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.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,
@ -454,10 +530,13 @@ export class NapCatOneBot11Adapter {
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid)),
'add',
notify.postscript,
flag,
flag
);
this.networkManager.emitEvent(groupInviteEvent)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e));
this.networkManager
.emitEvent(groupInviteEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e)
);
}
}
}
@ -476,92 +555,102 @@ export class NapCatOneBot11Adapter {
this.core,
parseInt(groupCode),
parseInt(member.uin),
member.role === GroupMemberRole.admin ? 'set' : 'unset',
member.role === GroupMemberRole.admin ? 'set' : 'unset'
);
this.networkManager.emitEvent(groupAdminNoticeEvent)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e));
this.networkManager
.emitEvent(groupAdminNoticeEvent)
.catch((e) =>
this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e)
);
existMember.isChangeRole = false;
this.context.logger.logDebug.bind(this.context.logger)('群管理员变动处理完毕');
});
}
};
this.context.session.getGroupService().addKernelGroupListener(
proxiedListenerOf(groupListener, this.context.logger),
);
this.context.session
.getGroupService()
.addKernelGroupListener(proxiedListenerOf(groupListener, this.context.logger));
}
private async emitMsg(message: RawMessage, parseEvent: boolean = true) {
const { debug, reportSelfMessage, messagePostFormat } = this.configLoader.configData;
private async emitMsg(message: RawMessage) {
const network = Object.values(this.configLoader.configData.network) as Array<
(typeof this.configLoader.configData.network)[keyof typeof this.configLoader.configData.network]
>;
this.context.logger.logDebug('收到新消息 RawMessage', message);
this.apis.MsgApi.parseMessage(message, messagePostFormat).then((ob11Msg) => {
if (!ob11Msg) return;
this.context.logger.logDebug('转化为 OB11Message', ob11Msg);
if (debug) {
ob11Msg.raw = message;
} else if (ob11Msg.message.length === 0) {
return;
this.apis.MsgApi.parseMessageV2(message)
.then((ob11Msg) => {
if (!ob11Msg) return;
const isSelfMsg =
ob11Msg.stringMsg.user_id.toString() == this.core.selfInfo.uin ||
ob11Msg.arrayMsg.user_id.toString() == this.core.selfInfo.uin;
this.context.logger.logDebug('转化为 OB11Message', ob11Msg);
const msgMap: Map<string, OB11Message> = new Map();
const enable_client: string[] = [];
network
.flat()
.filter((e) => e.enable)
.map((e) => {
enable_client.push(e.name);
if (e.messagePostFormat == 'string') {
msgMap.set(e.name, structuredClone(ob11Msg.stringMsg));
} else {
msgMap.set(e.name, structuredClone(ob11Msg.arrayMsg));
}
if (isSelfMsg) {
ob11Msg.stringMsg.target_id = parseInt(message.peerUin);
ob11Msg.arrayMsg.target_id = parseInt(message.peerUin);
}
});
}
const isSelfMsg = ob11Msg.user_id.toString() == this.core.selfInfo.uin;
if (isSelfMsg && !reportSelfMessage) {
return;
}
if (isSelfMsg) {
ob11Msg.target_id = parseInt(message.peerUin);
}
// if (ob11Msg.raw_message.startsWith('!set')) {
// this.core.apis.UserApi.getUidByUinV2(ob11Msg.user_id.toString()).then(uid => {
// if(uid){
// this.core.apis.PacketApi.sendSetSpecialTittlePacket(message.peerUin, uid, '测试');
// console.log('set', message.peerUin, uid);
// }
const debug_network = network.flat().filter((e) => e.enable && e.debug);
if (debug_network.length > 0) {
for (const adapter of debug_network) {
if (adapter.name) {
const msg = msgMap.get(adapter.name);
if (msg) {
msg.raw = message;
}
}
}
} else if (ob11Msg.stringMsg.message.length === 0 || ob11Msg.arrayMsg.message.length == 0) {
return;
}
const notreportSelf_network = network.flat().filter((e) => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e)));
if (isSelfMsg) {
for (const adapter of notreportSelf_network) {
msgMap.delete(adapter.name);
}
}
// });
this.networkManager.emitEventByNames(msgMap);
})
.catch((e) => this.context.logger.logError.bind(this.context.logger)('constructMessage error: ', e));
// }
// if (ob11Msg.raw_message.startsWith('!status')) {
// console.log('status', message.peerUin, message.senderUin);
// let delMsg: string[] = [];
// let peer = {
// peerUid: message.peerUin,
// chatType: 2,
// };
// this.core.apis.PacketApi.sendStatusPacket(+message.senderUin).then(async e => {
// if (e) {
// const { sendElements } = await this.apis.MsgApi.createSendElements([{
// type: OB11MessageDataType.text,
// data: {
// text: 'status ' + JSON.stringify(e, null, 2),
// }
// }], peer)
this.apis.GroupApi.parseGroupEvent(message)
.then((groupEvent) => {
if (groupEvent) {
// log("post group event", groupEvent);
this.networkManager.emitEvent(groupEvent);
}
})
.catch((e) => this.context.logger.logError.bind(this.context.logger)('constructGroupEvent error: ', e));
// this.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, delMsg)
// }
// })
// }
this.networkManager.emitEvent(ob11Msg);
}).catch(e => this.context.logger.logError.bind(this.context.logger)('constructMessage error: ', e));
this.apis.GroupApi.parseGroupEvent(message).then(groupEvent => {
if (groupEvent) {
// log("post group event", groupEvent);
this.networkManager.emitEvent(groupEvent);
}
}).catch(e => this.context.logger.logError.bind(this.context.logger)('constructGroupEvent error: ', e));
this.apis.MsgApi.parsePrivateMsgEvent(message).then(privateEvent => {
if (privateEvent) {
// log("post private event", privateEvent);
this.networkManager.emitEvent(privateEvent);
}
}).catch(e => this.context.logger.logError.bind(this.context.logger)('constructPrivateEvent error: ', e));
this.apis.MsgApi.parsePrivateMsgEvent(message)
.then((privateEvent) => {
if (privateEvent) {
// log("post private event", privateEvent);
this.networkManager.emitEvent(privateEvent);
}
})
.catch((e) => this.context.logger.logError.bind(this.context.logger)('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)) { //work:这个判断方法不太好,应该使用灰色消息元素来判断?
if (message.recallTime != '0' && !cache.get(message.msgId)) {
//work:这个判断方法不太好,应该使用灰色消息元素来判断?
cache.put(message.msgId, true);
// 撤回消息上报
let oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId);
@ -572,10 +661,13 @@ export class NapCatOneBot11Adapter {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(
this.core,
+message.senderUin,
oriMessageId,
oriMessageId
);
this.networkManager.emitEvent(friendRecallEvent)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理好友消息撤回失败', e));
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) {
@ -591,8 +683,9 @@ export class NapCatOneBot11Adapter {
+operatorId,
oriMessageId
);
this.networkManager.emitEvent(groupRecallEvent)
.catch(e => this.context.logger.logError.bind(this.context.logger)('处理群消息撤回失败', e));
this.networkManager
.emitEvent(groupRecallEvent)
.catch((e) => this.context.logger.logError.bind(this.context.logger)('处理群消息撤回失败', e));
}
}
}

View File

@ -10,6 +10,7 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
isOpen: boolean = false;
constructor(
public name: string,
public url: string,
public secret: string | undefined,
public core: NapCatCore,

View File

@ -15,6 +15,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
private heartbeatRef: NodeJS.Timeout | null = null;
constructor(
public name: string,
public url: string,
public reconnectIntervalInMillis: number,
public heartbeatIntervalInMillis: number,
@ -35,11 +36,14 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
if (this.connection) {
return;
}
this.heartbeatRef = setInterval(() => {
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatIntervalInMillis, this.core.selfInfo.online ?? true, true)));
}
}, this.heartbeatIntervalInMillis);
if (this.heartbeatIntervalInMillis > 0) {
this.heartbeatRef = setInterval(() => {
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatIntervalInMillis, this.core.selfInfo.online ?? true, true)));
}
}, this.heartbeatIntervalInMillis);
}
await this.tryConnect();
}
@ -147,7 +151,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
this.checkStateAndReply<any>(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo));
return;
}
const retdata = await action.websocketHandle(receiveData.params, echo ?? '');
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name);
this.checkStateAndReply<any>({ ...retdata });
}
}

View File

@ -6,6 +6,7 @@ export type OB11EmitEventContent = OB11BaseEvent | OB11Message;
export interface IOB11NetworkAdapter {
actions?: ActionMap;
name: string;
onEvent<T extends OB11EmitEventContent>(event: T): void;
@ -15,19 +16,34 @@ export interface IOB11NetworkAdapter {
}
export class OB11NetworkManager {
adapters: IOB11NetworkAdapter[] = [];
adapters: Map<string, IOB11NetworkAdapter> = new Map();
async openAllAdapters() {
return Promise.all(this.adapters.map(adapter => adapter.open()));
return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.open()));
}
async emitEvent(event: OB11EmitEventContent) {
//console.log('adapters', this.adapters.length);
return Promise.all(this.adapters.map(adapter => adapter.onEvent(event)));
return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.onEvent(event)));
}
async emitEventByName(names: string[], event: OB11EmitEventContent) {
return Promise.all(names.map(name => {
const adapter = this.adapters.get(name);
if (adapter) {
return adapter.onEvent(event);
}
}));
}
async emitEventByNames(map:Map<string,OB11EmitEventContent>){
return Promise.all(Array.from(map.entries()).map(([name, event]) => {
const adapter = this.adapters.get(name);
if (adapter) {
return adapter.onEvent(event);
}
}));
}
registerAdapter(adapter: IOB11NetworkAdapter) {
this.adapters.push(adapter);
this.adapters.set(adapter.name, adapter);
}
async registerAdapterAndOpen(adapter: IOB11NetworkAdapter) {
@ -36,24 +52,28 @@ export class OB11NetworkManager {
}
async closeSomeAdapters(adaptersToClose: IOB11NetworkAdapter[]) {
this.adapters = this.adapters.filter(adapter => !adaptersToClose.includes(adapter));
await Promise.all(adaptersToClose.map(adapter => adapter.close()));
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
await adapter.close();
}
}
findSomeAdapter(name: string) {
return this.adapters.get(name);
}
/**
* Close all adapters that satisfy the predicate.
*/
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter) => boolean) {
await this.closeSomeAdapters(this.adapters.filter(closeFilter));
const adaptersToClose = Array.from(this.adapters.values()).filter(closeFilter);
await this.closeSomeAdapters(adaptersToClose);
}
async closeAllAdapters() {
await Promise.all(this.adapters.map(adapter => adapter.close()));
this.adapters = [];
await Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.close()));
this.adapters.clear();
}
}
export * from './active-http';
export * from './active-websocket';
export * from './passive-http';
export * from './passive-websocket';
export * from './passive-websocket';

View File

@ -12,6 +12,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
private isOpen: boolean = false;
constructor(
public name: string,
public port: number,
public token: string,
public core: NapCatCore,
@ -101,7 +102,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
const action = this.actions.get(actionName);
if (action) {
try {
const result = await action.handle(payload);
const result = await action.handle(payload,this.name);
return res.json(result);
} catch (error: any) {
return res.json(OB11Response.error(error?.stack?.toString() || error?.message || 'Error Handle', 200));

View File

@ -24,6 +24,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
wsClientWithEvent: WebSocket[] = [];
constructor(
public name: string,
ip: string,
port: number,
heartbeatInterval: number,
@ -114,7 +115,10 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
this.isOpen = true;
this.registerHeartBeat();
if (this.heartbeatInterval > 0) {
this.registerHeartBeat();
}
}
async close() {
@ -188,7 +192,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
this.checkStateAndReply<any>(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo), wsClient);
return;
}
const retdata = await action.websocketHandle(receiveData.params, echo ?? '');
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name);
this.checkStateAndReply<any>({ ...retdata }, wsClient);
}
}

View File

@ -36,26 +36,34 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// 配置静态文件服务,提供./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 () => {
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: http://${config.host}:${config.port}${config.prefix}/webui?token=${config.token}`);
log(`[NapCat] [WebUi] WebUi Local Panel Url: http://127.0.0.1:${config.port}${config.prefix}/webui?token=${config.token}`);
log(
`[NapCat] [WebUi] WebUi User Panel Url: http://${config.host}:${config.port}${config.prefix}/webui?token=${config.token}`
);
log(
`[NapCat] [WebUi] WebUi Local Panel Url: http://127.0.0.1:${config.port}${config.prefix}/webui?token=${config.token}`
);
//获取上网Ip
//https://www.ip.cn/api/index?ip&type=0
RequestUtil.HttpGetJson<{ IP: {IP:string} }>(
'https://ip.011102.xyz/',
'GET',
{},
{},
true,
true
).then((data) => {
log(`[NapCat] [WebUi] WebUi Publish Panel Url: http://${data.IP.IP}:${config.port}${config.prefix}/webui/?token=${config.token}`);
}).catch((err) => {
logger.logError.bind(logger)(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
});
RequestUtil.HttpGetJson<{ IP: { IP: string } }>('https://ip.011102.xyz/', 'GET', {}, {}, true, true)
.then((data) => {
log(
`[NapCat] [WebUi] WebUi Publish Panel Url: http://${data.IP.IP}:${config.port}${config.prefix}/webui/?token=${config.token}`
);
})
.catch((err) => {
logger.logError.bind(logger)(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
});
});
}

View File

@ -0,0 +1,15 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
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()
}
});
};

View File

@ -1,12 +1,11 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '../helper/Data';
import { existsSync, readFileSync } from 'node:fs';
import { OB11Config } from '@/webui/ui/components/WebUiApiOB11Config';
import { OneBotConfig } from '@/onebot/config/config';
import { resolve } from 'node:path';
import { webUiPathWrapper } from '@/webui';
const isEmpty = (data: any) =>
data === undefined || data === null || data === '';
const isEmpty = (data: any) => data === undefined || data === null || data === '';
export const OB11GetConfigHandler: RequestHandler = async (req, res) => {
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
if (!isLogin) {
@ -19,15 +18,15 @@ export const OB11GetConfigHandler: RequestHandler = async (req, res) => {
const uin = await WebUiDataRuntime.getQQLoginUin();
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
//console.log(configFilePath);
let data: OB11Config;
let data: OneBotConfig;
try {
data = JSON.parse(
existsSync(configFilePath)
? readFileSync(configFilePath).toString()
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString(),
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString()
);
} catch (e) {
data = {} as OB11Config;
data = {} as OneBotConfig;
res.send({
code: -1,
message: 'Config Get Error',

View File

@ -1,4 +1,4 @@
import { OB11Config } from '@/onebot/config';
import { OneBotConfig } from '@/onebot/config/config';
interface LoginRuntimeType {
LoginCurrentTime: number;
@ -7,9 +7,9 @@ interface LoginRuntimeType {
QQQRCodeURL: string;
QQLoginUin: string;
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean, message: string }>;
onOB11ConfigChanged: (ob11: OB11Config) => Promise<void>;
QQLoginList: string[]
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
QQLoginList: string[];
};
}
@ -31,62 +31,62 @@ const LoginRuntime: LoginRuntimeType = {
};
export const WebUiDataRuntime = {
checkLoginRate: async function(RateLimit: number): Promise<boolean> {
checkLoginRate: async function (RateLimit: number): Promise<boolean> {
LoginRuntime.LoginCurrentRate++;
//console.log(RateLimit, LoginRuntime.LoginCurrentRate, Date.now() - LoginRuntime.LoginCurrentTime);
if (Date.now() - LoginRuntime.LoginCurrentTime > 1000 * 60) {
LoginRuntime.LoginCurrentRate = 0;//超出时间重置限速
LoginRuntime.LoginCurrentRate = 0; //超出时间重置限速
LoginRuntime.LoginCurrentTime = Date.now();
return true;
}
return LoginRuntime.LoginCurrentRate <= RateLimit;
},
getQQLoginStatus: async function(): Promise<boolean> {
getQQLoginStatus: async function (): Promise<boolean> {
return LoginRuntime.QQLoginStatus;
},
setQQLoginStatus: async function(status: boolean): Promise<void> {
setQQLoginStatus: async function (status: boolean): Promise<void> {
LoginRuntime.QQLoginStatus = status;
},
setQQLoginQrcodeURL: async function(url: string): Promise<void> {
setQQLoginQrcodeURL: async function (url: string): Promise<void> {
LoginRuntime.QQQRCodeURL = url;
},
getQQLoginQrcodeURL: async function(): Promise<string> {
getQQLoginQrcodeURL: async function (): Promise<string> {
return LoginRuntime.QQQRCodeURL;
},
setQQLoginUin: async function(uin: string): Promise<void> {
setQQLoginUin: async function (uin: string): Promise<void> {
LoginRuntime.QQLoginUin = uin;
},
getQQLoginUin: async function(): Promise<string> {
getQQLoginUin: async function (): Promise<string> {
return LoginRuntime.QQLoginUin;
},
getQQQuickLoginList: async function(): Promise<any[]> {
getQQQuickLoginList: async function (): Promise<any[]> {
return LoginRuntime.NapCatHelper.QQLoginList;
},
setQQQuickLoginList: async function(list: string[]): Promise<void> {
setQQQuickLoginList: async function (list: string[]): Promise<void> {
LoginRuntime.NapCatHelper.QQLoginList = list;
},
setQuickLoginCall(func: (uin: string) => Promise<{ result: boolean, message: string }>): void {
setQuickLoginCall(func: (uin: string) => Promise<{ result: boolean; message: string }>): void {
LoginRuntime.NapCatHelper.onQuickLoginRequested = func;
},
requestQuickLogin: async function(uin: string): Promise<{ result: boolean, message: string }> {
requestQuickLogin: async function (uin: string): Promise<{ result: boolean; message: string }> {
return await LoginRuntime.NapCatHelper.onQuickLoginRequested(uin);
},
setOnOB11ConfigChanged: async function(func: (ob11: OB11Config) => Promise<void>): Promise<void> {
setOnOB11ConfigChanged: async function (func: (ob11: OneBotConfig) => Promise<void>): Promise<void> {
LoginRuntime.NapCatHelper.onOB11ConfigChanged = func;
},
setOB11Config: async function(ob11: OB11Config): Promise<void> {
setOB11Config: async function (ob11: OneBotConfig): Promise<void> {
await LoginRuntime.NapCatHelper.onOB11ConfigChanged(ob11);
},
};

View File

@ -1,393 +0,0 @@
import { SettingList } from './components/SettingList';
import { SettingItem } from './components/SettingItem';
import { SettingButton } from './components/SettingButton';
import { SettingSwitch } from './components/SettingSwitch';
import { SettingSelect } from './components/SettingSelect';
import { OB11Config, OB11ConfigWrapper } from './components/WebUiApiOB11Config';
async function onSettingWindowCreated(view: Element) {
const isEmpty = (value: any) => value === undefined || false || value === '';
await OB11ConfigWrapper.Init(localStorage.getItem('auth') as string);
const ob11Config: OB11Config = await OB11ConfigWrapper.GetOB11Config();
const setOB11Config = (key: string, value: any) => {
const configKey = key.split('.');
if (configKey.length === 2) {
ob11Config[configKey[1]] = value;
} else if (configKey.length === 3) {
ob11Config[configKey[1]][configKey[2]] = value;
}
// OB11ConfigWrapper.SetOB11Config(ob11Config); // 只有当点保存时才下发配置,而不是在修改值后立即下发
};
const parser = new DOMParser();
const doc = parser.parseFromString(
[
'<div>',
`<setting-section id="napcat-error">
<setting-panel><pre><code></code></pre></setting-panel>
</setting-section>`,
SettingList([
SettingItem(
'<span id="napcat-update-title">Napcat</span>',
undefined,
SettingButton('V3.3.12', 'napcat-update-button', 'secondary'),
),
]),
SettingList([
SettingItem(
'启用 HTTP 服务',
undefined,
SettingSwitch('ob11.http.enable', ob11Config.http.enable, {
'control-display-id': 'config-ob11-http-port',
}),
),
SettingItem(
'HTTP 服务监听端口',
undefined,
`<div class="q-input"><input class="q-input__inner" data-config-key="ob11.http.port" type="number" min="1" max="65534" value="${ob11Config.http.port}" placeholder="${ob11Config.http.port}" /></div>`,
'config-ob11-http-port',
ob11Config.http.enable,
),
SettingItem(
'启用 HTTP 心跳',
undefined,
SettingSwitch('ob11.http.enableHeart', ob11Config.http.enableHeart, {
'control-display-id': 'config-ob11-HTTP.enableHeart',
}),
),
SettingItem(
'启用 HTTP 事件上报',
undefined,
SettingSwitch('ob11.http.enablePost', ob11Config.http.enablePost, {
'control-display-id': 'config-ob11-http-postUrls',
}),
),
`<div class="config-host-list" id="config-ob11-http-postUrls" ${ob11Config.http.enablePost ? '' : 'is-hidden'
}>
<setting-item data-direction="row">
<div>
<setting-text>HTTP </setting-text>
</div>
<div class="q-input">
<input id="config-ob11-http-secret" class="q-input__inner" data-config-key="ob11.http.secret" type="text" value="${ob11Config.http.secret}" placeholder="未设置" />
</div>
</setting-item>
<setting-item data-direction="row">
<div>
<setting-text>HTTP </setting-text>
</div>
<setting-button id="config-ob11-http-postUrls-add" data-type="primary"></setting-button>
</setting-item>
<div id="config-ob11-http-postUrls-list"></div>
</div>`,
SettingItem(
'启用正向 WebSocket 服务',
undefined,
SettingSwitch('ob11.ws.enable', ob11Config.ws.enable, {
'control-display-id': 'config-ob11-ws-port',
}),
),
SettingItem(
'正向 WebSocket 服务监听端口',
undefined,
`<div class="q-input"><input class="q-input__inner" data-config-key="ob11.ws.port" type="number" min="1" max="65534" value="${ob11Config.ws.port}" placeholder="${ob11Config.ws.port}" /></div>`,
'config-ob11-ws-port',
ob11Config.ws.enable,
),
SettingItem(
'启用反向 WebSocket 服务',
undefined,
SettingSwitch('ob11.reverseWs.enable', ob11Config.reverseWs.enable, {
'control-display-id': 'config-ob11-reverseWs-urls',
}),
),
`<div class="config-host-list" id="config-ob11-reverseWs-urls" ${ob11Config.reverseWs.enable ? '' : 'is-hidden'}>
<setting-item data-direction="row">
<div>
<setting-text> WebSocket </setting-text>
</div>
<setting-button id="config-ob11-reverseWs-urls-add" data-type="primary"></setting-button>
</setting-item>
<div id="config-ob11-reverseWs-urls-list"></div>
</div>`,
SettingItem(
' WebSocket 服务心跳间隔',
'控制每隔多久发送一个心跳包,单位为毫秒',
`<div class="q-input"><input class="q-input__inner" data-config-key="ob11.heartInterval" type="number" min="1000" value="${ob11Config.heartInterval}" placeholder="${ob11Config.heartInterval}" /></div>`,
),
SettingItem(
'Access token',
undefined,
`<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="ob11.token" type="text" value="${ob11Config.token}" placeholder="未设置" /></div>`,
),
SettingItem(
'新消息上报格式',
'如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 <a href="javascript:LiteLoader.api.openExternal(\'https://github.com/botuniverse/onebot-11/tree/master/message#readme\');">OneBot v11 文档</a>',
SettingSelect(
[
{ text: '消息段', value: 'array' },
{ text: 'CQ码', value: 'string' },
],
'ob11.messagePostFormat',
ob11Config.messagePostFormat,
),
),
SettingItem(
'音乐卡片签名地址',
undefined,
`<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="ob11.musicSignUrl" type="text" value="${ob11Config.musicSignUrl}" placeholder="未设置" /></div>`,
'ob11.musicSignUrl',
),
SettingItem(
'启用本地进群时间与发言时间记录',
undefined,
SettingSwitch('ob11.GroupLocalTime.Record', ob11Config.GroupLocalTime.Record, {
'control-display-id': 'config-ob11-GroupLocalTime-RecordList',
}),
),
`<div class="config-host-list" id="config-ob11-GroupLocalTime-RecordList" ${ob11Config.GroupLocalTime.Record ? '' : 'is-hidden'}>
<setting-item data-direction="row">
<div>
<setting-text></setting-text>
</div>
<setting-button id="config-ob11-GroupLocalTime-RecordList-add" data-type="primary"></setting-button>
</setting-item>
<div id="config-ob11-GroupLocalTime-RecordList-list"></div>
</div>`,
SettingItem(
'',
undefined,
SettingButton('保存', 'config-ob11-save', 'primary'),
),
]),
SettingList([
SettingItem(
'上报 Bot 自身发送的消息',
'上报 event 为 message_sent',
SettingSwitch('ob11.reportSelfMessage', ob11Config.reportSelfMessage),
),
]),
SettingList([
SettingItem(
'GitHub 仓库',
'https://github.com/NapNeko/NapCatQQ',
SettingButton('点个星星', 'open-github'),
),
SettingItem('NapCat 文档', '', SettingButton('看看文档', 'open-docs')),
]),
SettingItem(
'Telegram 群',
'https://t.me/+nLZEnpne-pQ1OWFl',
SettingButton('进去逛逛', 'open-telegram'),
),
SettingItem(
'QQ 群',
'518662028',
SettingButton('我要进去', 'open-qq-group'),
),
'</div>',
].join(''),
'text/html',
);
// 外链按钮
doc.querySelector('#open-github')?.addEventListener('click', () => {
window.open('https://github.com/NapNeko/NapCatQQ', '_blank');
});
doc.querySelector('#open-docs')?.addEventListener('click', () => {
window.open('https://napneko.github.io/', '_blank');
});
doc.querySelector('#open-telegram')?.addEventListener('click', () => {
window.open('https://t.me/+nLZEnpne-pQ1OWFl', '_blank');
});
doc.querySelector('#open-qq-group')?.addEventListener('click', () => {
window.open('https://qm.qq.com/q/VfjAq5HIMS', '_blank');
});
// 生成反向地址列表
const buildHostListItem = (
type: string,
host: string,
index: number,
inputAttrs: any = {},
) => {
const dom = {
container: document.createElement('setting-item'),
input: document.createElement('input'),
inputContainer: document.createElement('div'),
deleteBtn: document.createElement('setting-button'),
};
dom.container.classList.add('setting-host-list-item');
dom.container.dataset.direction = 'row';
Object.assign(dom.input, inputAttrs);
dom.input.classList.add('q-input__inner');
dom.input.type = 'url';
dom.input.value = host;
dom.input.addEventListener('input', () => {
ob11Config[type.split('-')[0]][type.split('-')[1]][index] =
dom.input.value;
});
dom.inputContainer.classList.add('q-input');
dom.inputContainer.appendChild(dom.input);
dom.deleteBtn.innerHTML = '删除';
dom.deleteBtn.dataset.type = 'secondary';
dom.deleteBtn.addEventListener('click', () => {
ob11Config[type.split('-')[0]][type.split('-')[1]].splice(index, 1);
initReverseHost(type);
});
dom.container.appendChild(dom.inputContainer);
dom.container.appendChild(dom.deleteBtn);
return dom.container;
};
const buildHostList = (
hosts: string[],
type: string,
inputAttr: any = {},
) => {
const result: HTMLElement[] = [];
hosts?.forEach((host, index) => {
result.push(buildHostListItem(type, host, index, inputAttr));
});
return result;
};
const addReverseHost = (
type: string,
doc: Document = document,
inputAttr: any = {},
) => {
type = type.replace(/\./g, '-');//替换操作
const hostContainerDom = doc.body.querySelector(
`#config-ob11-${type}-list`,
);
hostContainerDom?.appendChild(
buildHostListItem(
type,
'',
ob11Config[type.split('-')[0]][type.split('-')[1]].length,
inputAttr,
),
);
ob11Config[type.split('-')[0]][type.split('-')[1]].push('');
};
const initReverseHost = (type: string, doc: Document = document) => {
type = type.replace(/\./g, '-');//替换操作
const hostContainerDom = doc.body?.querySelector(
`#config-ob11-${type}-list`,
);
if (hostContainerDom) {
[...hostContainerDom.childNodes].forEach((dom) => dom.remove());
buildHostList(
ob11Config[type.split('-')[0]][type.split('-')[1]],
type,
).forEach((dom) => {
hostContainerDom?.appendChild(dom);
});
}
};
initReverseHost('http.postUrls', doc);
initReverseHost('reverseWs.urls', doc);
initReverseHost('GroupLocalTime.RecordList', doc);
doc
.querySelector('#config-ob11-http-postUrls-add')
?.addEventListener('click', () =>
addReverseHost('http.postUrls', document, {
placeholder: '如http://127.0.0.1:5140/onebot',
}),
);
doc
.querySelector('#config-ob11-reverseWs-urls-add')
?.addEventListener('click', () =>
addReverseHost('reverseWs.urls', document, {
placeholder: '如ws://127.0.0.1:5140/onebot',
}),
);
doc
.querySelector('#config-ob11-GroupLocalTime-RecordList-add')
?.addEventListener('click', () =>
addReverseHost('GroupLocalTime.RecordList', document, {
placeholder: '此处填写群号 -1为全部',
}),
);
doc.querySelector('#config-ffmpeg-select')?.addEventListener('click', () => {
//选择ffmpeg
});
doc.querySelector('#config-open-log-path')?.addEventListener('click', () => {
//打开日志
});
// 开关
doc
.querySelectorAll('setting-switch[data-config-key]')
.forEach((dom: Element) => {
dom.addEventListener('click', () => {
const active = dom.getAttribute('is-active') == undefined;
//@ts-expect-error 等待修复
setOB11Config(dom.dataset.configKey, active);
if (active) dom.setAttribute('is-active', '');
else dom.removeAttribute('is-active');
//@ts-expect-error 等待修复
if (!isEmpty(dom.dataset.controlDisplayId)) {
const displayDom = document.querySelector(
//@ts-expect-error 等待修复
`#${dom.dataset.controlDisplayId}`,
);
if (active) displayDom?.removeAttribute('is-hidden');
else displayDom?.setAttribute('is-hidden', '');
}
});
});
// 输入框
doc
.querySelectorAll(
'setting-item .q-input input.q-input__inner[data-config-key]',
)
.forEach((dom: Element) => {
dom.addEventListener('input', () => {
const Type = dom.getAttribute('type');
//@ts-expect-error等待修复
const configKey = dom.dataset.configKey;
const configValue =
Type === 'number'
? parseInt((dom as HTMLInputElement).value) >= 1
? parseInt((dom as HTMLInputElement).value)
: 1
: (dom as HTMLInputElement).value;
setOB11Config(configKey, configValue);
});
});
// 下拉框
doc
.querySelectorAll('ob-setting-select[data-config-key]')
.forEach((dom: Element) => {
//@ts-expect-error等待修复
dom?.addEventListener('selected', (e: CustomEvent) => {
//@ts-expect-error等待修复
const configKey = dom.dataset.configKey;
const configValue = e.detail.value;
setOB11Config(configKey, configValue);
});
});
// 保存按钮
doc.querySelector('#config-ob11-save')?.addEventListener('click', () => {
OB11ConfigWrapper.SetOB11Config(ob11Config);
alert('保存成功');
});
doc.body.childNodes.forEach((node) => {
view.appendChild(node);
});
}
export { onSettingWindowCreated };

View File

@ -1,3 +0,0 @@
export const SettingButton = (text: string, id?: string, type: string = 'secondary') => {
return `<setting-button ${type ? `data-type="${type}"` : ''} ${id ? `id="${id}"` : ''}>${text}</setting-button>`;
};

View File

@ -1,15 +0,0 @@
export const SettingItem = (
title: string,
subtitle?: string,
action?: string,
id?: string,
visible: boolean = true,
) => {
return `<setting-item ${id ? `id="${id}"` : ''} ${!visible ? 'is-hidden' : ''}>
<div>
<setting-text>${title}</setting-text>
${subtitle ? `<setting-text data-type="secondary">${subtitle}</setting-text>` : ''}
</div>
${action ? `<div>${action}</div>` : ''}
</setting-item>`;
};

View File

@ -1,14 +0,0 @@
export const SettingList = (
items: string[],
title?: string,
isCollapsible: boolean = false,
direction: string = 'column',
) => {
return `<setting-section ${title && !isCollapsible ? `data-title="${title}"` : ''}>
<setting-panel>
<setting-list ${direction ? `data-direction="${direction}"` : ''} ${isCollapsible ? 'is-collapsible' : ''} ${title && isCollapsible ? `data-title="${title}"` : ''}>
${items.join('')}
</setting-list>
</setting-panel>
</setting-section>`;
};

View File

@ -1,3 +0,0 @@
export const SettingOption = (text: string, value?: string, isSelected: boolean = false) => {
return `<setting-option ${value ? `data-value="${value}"` : ''} ${isSelected ? 'is-selected' : ''}>${text}</setting-option>`;
};

View File

@ -1,86 +0,0 @@
import { SettingOption } from './SettingOption';
interface MouseEventExtend extends MouseEvent {
target: HTMLElement;
}
// <ob-setting-select>
const SelectTemplate = document.createElement('template');
SelectTemplate.innerHTML = `<style>
.hidden { display: none !important; }
</style>
<div part="parent">
<div part="button">
<input type="text" placeholder="请选择" part="current-text" />
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" part="button-arrow">
<path d="M12 6.0001L8.00004 10L4 6" stroke="currentColor" stroke-linejoin="round"></path>
</svg>
</div>
<ul class="hidden" part="option-list"><slot></slot></ul>
</div>`;
window.customElements.define(
'ob-setting-select',
class extends HTMLElement {
readonly _button: HTMLDivElement;
readonly _text: HTMLInputElement;
readonly _context: HTMLUListElement;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot?.append(SelectTemplate.content.cloneNode(true));
this._button = this.shadowRoot!.querySelector('div[part="button"]')!;
this._text = this.shadowRoot!.querySelector('input[part="current-text"]')!;
this._context = this.shadowRoot!.querySelector('ul[part="option-list"]')!;
const buttonClick = () => {
const isHidden = this._context.classList.toggle('hidden');
window[`${isHidden ? 'remove' : 'add'}EventListener`]('pointerdown', windowPointerDown);
};
const windowPointerDown = ({ target }: any) => {
if (!this.contains(target)) buttonClick();
};
this._button.addEventListener('click', buttonClick);
this._context.addEventListener('click', (event) => {
const { target } = event as MouseEventExtend;
if (target.tagName !== 'SETTING-OPTION') return;
buttonClick();
if (target.hasAttribute('is-selected')) return;
this.querySelectorAll('setting-option[is-selected]').forEach((dom) => dom.toggleAttribute('is-selected'));
target.toggleAttribute('is-selected');
this._text.value = target.textContent as string;
this.dispatchEvent(
new CustomEvent('selected', {
bubbles: true,
composed: true,
detail: {
name: target.textContent,
value: target.dataset.value,
},
}),
);
});
this._text.value = this.querySelector('setting-option[is-selected]')?.textContent as string;
}
},
);
export const SettingSelect = (items: Array<{ text: string; value: string }>, configKey?: string, configValue?: any) => {
return `<ob-setting-select ${configKey ? `data-config-key="${configKey}"` : ''}>
${items
.map((e, i) => {
return SettingOption(e.text, e.value, configKey && configValue ? configValue === e.value : i === 0);
})
.join('')}
</ob-setting-select>`;
};

View File

@ -1,8 +0,0 @@
export const SettingSwitch = (configKey?: string, isActive: boolean = false, extraData?: Record<string, string>) => {
return `<setting-switch
${configKey ? `data-config-key="${configKey}"` : ''}
${isActive ? 'is-active' : ''}
${extraData ? Object.keys(extraData).map((key) => `data-${key}="${extraData[key]}"`) : ''}
>
</setting-switch>`;
};

View File

@ -1,79 +0,0 @@
export interface OB11Config {
[key: string]: any;
http: {
enable: boolean;
host: '';
port: number;
secret: '';
enableHeart: boolean;
enablePost: boolean;
postUrls: string[];
};
ws: {
enable: boolean;
host: '';
port: number;
};
reverseWs: {
enable: boolean;
urls: string[];
};
GroupLocalTime: {
Record: boolean,
RecordList: Array<string>
};
debug: boolean;
heartInterval: number;
messagePostFormat: 'array' | 'string';
enableLocalFile2Url: boolean;
musicSignUrl: '';
reportSelfMessage: boolean;
token: '';
}
class WebUiApiOB11ConfigWrapper {
private retCredential: string = '';
async Init(Credential: string) {
this.retCredential = Credential;
}
async GetOB11Config(): Promise<OB11Config> {
const ConfigResponse = await fetch('../api/OB11Config/GetConfig', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data;
}
}
return {} as OB11Config;
}
async SetOB11Config(config: OB11Config): Promise<boolean> {
const ConfigResponse = await fetch('../api/OB11Config/SetConfig', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ config: JSON.stringify(config) }),
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return true;
}
}
return false;
}
}
export const OB11ConfigWrapper = new WebUiApiOB11ConfigWrapper();

View File

@ -1,13 +0,0 @@
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: 'esnext',
minify: false,
lib: {
entry: 'ui/NapCat.ts',
formats: ['es'],
fileName: () => 'renderer.js',
},
},
});

View File

@ -1,294 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NapCat - WebUi</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
}
.login-container {
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
}
.login-methods {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.login-method {
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.login-method.active {
background-color: #e6f0ff;
color: #007BFF;
}
.login-form,
.qrcode {
display: flex;
flex-direction: column;
gap: 15px;
}
.qrcode {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
text-align: center;
}
button {
width: 100%;
padding: 10px;
background-color: #007BFF;
color: white;
border: none;
cursor: pointer;
transition: all 0.3s;
}
button:hover {
background-color: #0056b3;
}
.hidden {
display: none;
}
#qrcode-canvas {
width: 200px;
height: 200px;
}
#quick-login-dropdown {
width: 100%;
padding: 10px;
font-size: 16px;
background-color: transparent;
border: none;
cursor: pointer;
outline: none;
transition: all 0.3s;
}
#quick-login-dropdown:hover {
background-color: #e6f0ff;
}
#quick-login-options {
position: absolute;
top: calc(100% + 5px);
left: 0;
right: 0;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 1;
display: none;
}
#quick-login-options.show {
display: block;
}
.quick-login-option {
padding: 10px 15px;
cursor: pointer;
transition: all 0.3s;
}
.quick-login-option:hover {
background-color: #e6f0ff;
}
#quick-login-select {
width: 100%;
padding: 10px;
font-size: 16px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
outline: none;
transition: all 0.3s;
}
#quick-login-select option {
background-color: #fff;
color: #333;
}
</style>
<script src="./assets/qrcode.min.js"></script>
</head>
<body>
<div class="login-container">
<h2>Login</h2>
<div class="login-methods">
<button id="quick-login" class="login-method active">Quick Login</button>
<button id="qrcode-login" class="login-method">QR Code</button>
</div>
<div id="quick-login-dropdown" class="login-form">
<select id="quick-login-select" onchange="selectAccount(this.value)">
<option value="">Select Account</option>
</select>
</div>
<div id="qrcode" class="qrcode" style="display: none;">
<canvas id="qrcode-canvas"></canvas>
</div>
<p id="message"></p>
</div>
<script>
async function GetQQLoginQrcode(retCredential) {
let QQLoginResponse = await fetch('../api/QQLogin/GetQQLoginQrcode', {
method: 'POST',
headers: {
'Authorization': "Bearer " + retCredential,
'Content-Type': 'application/json'
}
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
if (QQLoginResponseJson.data.qrcode) {
return QQLoginResponseJson.data.qrcode;
} else {
return "";
}
}
}
return "";
}
async function CheckQQLoginStatus(retCredential) {
let QQLoginResponse = await fetch('../api/QQLogin/CheckLoginStatus', {
method: 'POST',
headers: {
'Authorization': "Bearer " + retCredential,
'Content-Type': 'application/json'
}
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
if (QQLoginResponseJson.data.isLogin) {
return true;
} else {
return false;
}
}
}
return false;
}
async function GetQQQucickLoginList(retCredential) {
let QQLoginResponse = await fetch('../api/QQLogin/GetQuickLoginList', {
method: 'POST',
headers: {
'Authorization': "Bearer " + retCredential,
'Content-Type': 'application/json'
}
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson?.data;
}
}
return [];
}
async function SetQuickLogin(uin, retCredential) {
let QQLoginResponse = await fetch('../api/QQLogin/SetQuickLogin', {
method: 'POST',
headers: {
'Authorization': "Bearer " + retCredential,
'Content-Type': 'application/json'
},
body: JSON.stringify({
uin: uin
})
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return { result: true, errMsg: "" };
} else {
return { result: false, errMsg: QQLoginResponseJson.message };
}
}
return { result: false, errMsg: "接口异常" };;
}
//心跳轮询CheckQQLoginStatus 是否已经进入登录状态如果为true进入./config.html
async function HeartBeat() {
let isLogined = await CheckQQLoginStatus(localStorage.getItem('auth'));
if (isLogined) {
window.location.href = './config.html';
}
}
async function InitPages() {
let QuickLists = await GetQQQucickLoginList(localStorage.getItem('auth'));
let QuickListSelect = document.querySelector("#quick-login-select");
QuickLists.forEach(QuickUin => {
let optionUinEle = document.createElement('option');
optionUinEle.innerHTML = QuickUin;
optionUinEle.value = QuickUin;
QuickListSelect.appendChild(optionUinEle);
});
generateQrCode(await GetQQLoginQrcode(localStorage.getItem('auth')), document.querySelector('#qrcode-canvas'));
setInterval(HeartBeat, 3000);
}
document.getElementById('quick-login').addEventListener('click', function () {
let quickLoginOptions = document.querySelector('#quick-login-dropdown');
let qrcode = document.querySelector('#qrcode');
quickLoginOptions.style.display = 'flex';
qrcode.style.display = 'none';
});
async function selectAccount(accountName) {
//alert(`Logging in with ${accountName}...`);
const { result, errMsg } = await SetQuickLogin(accountName, localStorage.getItem('auth'));
if (result) {
alert("登录成功即将跳转");
window.location.href = './config.html';
} else {
alert("登录失败," + errMsg);
}
//await (localStorage.getItem('auth'))
//document.getElementById('quick-login-options').classList.remove('show');
}
document.getElementById('qrcode-login').addEventListener('click', function () {
let loginForm = document.querySelector('#quick-login-dropdown');
let qrcode = document.querySelector('#qrcode');
loginForm.style.display = 'none';
qrcode.style.display = 'flex';
});
function generateQrCode(data, canvas) {
QRCode.toCanvas(canvas, data, function (error) {
if (error) console.log(error);
console.log('QR Code generated!');
});
}
InitPages();
</script>
</body>
</html>

View File

@ -1,180 +0,0 @@
setting-item[is-hidden],
setting-item[is-hidden] + setting-divider {
display: none !important;
}
.config-host-list {
width: 100%;
padding-left: 16px;
box-sizing: border-box;
}
.config-host-list[is-hidden],
.config-host-list[is-hidden] + setting-divider {
display: none !important;
}
setting-item .q-input {
height: 24px;
width: 100px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
box-sizing: border-box;
position: relative;
background: var(--bg_bottom_light);
border: 1px solid var(--border_dark);
}
setting-item .q-input .q-input__inner {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
box-sizing: border-box;
color: var(--text_primary);
font-family: inherit;
font-size: 12px;
height: 24px;
line-height: 24px;
width: 100%;
border: 1px solid transparent;
padding: 0px 8px;
}
setting-item .q-input input[type='number'].q-input__inner::-webkit-outer-spin-button,
setting-item .q-input input[type='number'].q-input__inner::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.config-host-list setting-item.setting-host-list-item .q-input {
width: 260px;
}
setting-item a {
color: var(--text-link);
}
setting-item a:hover {
color: var(--hover-link);
}
setting-item a:active,
setting-item a:visited {
color: var(--text-link);
}
ob-setting-select {
width: 100px;
}
ob-setting-select,
ob-setting-select::part(parent),
ob-setting-select::part(button) {
display: block;
position: relative;
height: 24px;
font-size: 12px;
line-height: 24px;
box-sizing: border-box;
}
ob-setting-select::part(button) {
display: flex;
padding: 0px 8px;
background-color: transparent;
border-radius: 4px;
border: 1px solid var(--border_dark);
z-index: 5;
cursor: default;
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
}
ob-setting-select::part(current-text) {
display: block;
margin-right: 8px;
padding: 0px;
background: none;
background-color: transparent;
font-size: 12px;
color: var(--text_primary);
text-overflow: ellipsis;
border-radius: 0px;
border: none;
outline: none;
overflow: hidden;
appearance: none;
box-sizing: border-box;
cursor: default;
flex: 1;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-pointer-events: none;
-moz-pointer-events: none;
-ms-pointer-events: none;
-o-pointer-events: none;
pointer-events: none;
}
ob-setting-select::part(button-arrow) {
position: relative;
display: block;
width: 16px;
height: 16px;
color: var(--icon_primary);
}
ob-setting-select::part(option-list) {
display: flex;
position: absolute;
top: 100%;
padding: 4px;
margin: 5px 0px;
width: 100%;
max-height: var(--q-contextmenu-max-height);
background-color: var(--blur_middle_standard);
background-clip: padding-box;
backdrop-filter: blur(8px);
font-size: 12px;
box-shadow: var(--shadow_bg_middle_secondary);
border: 1px solid var(--border_secondary);
border-radius: 4px;
box-sizing: border-box;
app-region: no-drag;
overflow-x: hidden;
overflow-y: auto;
list-style: none;
z-index: 999;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
justify-content: flex-start;
gap: 4px;
}
#napcat-error {
display: none;
}
#napcat-error setting-panel {
background: rgba(255, 0, 0, 0.5);
color: white;
}
#napcat-error setting-panel pre {
margin: 0;
padding: 16px;
box-sizing: border-box;
}
#napcat-error setting-panel pre code {
font-family: 'FiraCode Nerd Font', 'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace;
}
#napcat-error.show {
display: block;
}

View File

@ -1,679 +0,0 @@
:root {
--font-bold: 400;
--font_size_1: 10px;
--font_size_2: 12px;
--font_size_3: 14px;
--font_size_4: 16px;
--font_size_5: 18px;
--avatar_size_1: 20px;
--avatar_size_2: 32px;
--avatar_size_3: 40px;
--font_size_main_1: 12px;
--font_size_main_2: 14px;
--line_height_1: 14px;
--line_height_2: 16px;
--line_height_3: 20px;
--line_height_4: 22px;
--line_height_5: 24px;
--line_height_main_1: 18px;
--line_height_main_2: 22px;
--shadow_card_rest: 0px 2px 4px rgba(0, 0, 0, 0.12);
--shadow_tooltip: 0px 4px 8px rgba(0, 0, 0, 0.26);
--shadow_flyout: 0px 8px 16px rgba(0, 0, 0, 0.14);
--shadow_dialog: 0px 30px 60px rgba(0, 0, 0, 0.36) 0 2px 20px rgba(0, 0, 0, 0.37);
--blend_brightness_white_004: 1.04;
--blend_brightness_white_008: 1.08;
--blend_brightness_white_010: 1.10;
--blend_brightness_white_016: 1.16;
--blend_brightness_white_020: 1.20;
--blend_brightness_black_004: 0.96;
--blend_brightness_black_008: 0.92;
--blend_brightness_black_010: 0.90;
--blend_brightness_black_016: 0.84;
--blend_brightness_black_020: 0.80;
--blend_white_004: rgba(255, 255, 255, 0.04);
--blend_white_008: rgba(255, 255, 255, 0.08);
--blend_white_010: rgba(255, 255, 255, 0.10);
--blend_white_016: rgba(255, 255, 255, 0.16);
--blend_white_020: rgba(255, 255, 255, 0.20);
--blend_black_004: rgba(0, 0, 0, 0.04);
--blend_black_008: rgba(0, 0, 0, 0.08);
--blend_black_010: rgba(0, 0, 0, 0.10);
--blend_black_016: rgba(0, 0, 0, 0.16);
--blend_black_020: rgba(0, 0, 0, 0.20);
--blend_transparent: rgba(0, 0, 0, 0);
--el-color-white: #fff;
--el-color-black: #000;
--el-color-primary: #409eff;
--el-color-primary-rgb: 64,158,255;
--el-color-success-rgb: 103,194,58;
--el-color-warning-rgb: 230,162,60;
--el-color-danger-rgb: 245,108,108;
--el-color-error-rgb: 245,108,108;
--el-color-info-rgb: 144,147,153;
--el-color-primary-light-1: #53a8ff;
--el-color-primary-light-2: #66b1ff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-4: #8cc5ff;
--el-color-primary-light-5: #a0cfff;
--el-color-primary-light-6: #b3d8ff;
--el-color-primary-light-7: #c6e2ff;
--el-color-primary-light-8: #d9ecff;
--el-color-primary-light-9: #ecf5ff;
--el-color-primary-dark-2: #337ecc;
--el-color-success: #67c23a;
--el-color-success-light-3: #95d475;
--el-color-success-light-5: #b3e19d;
--el-color-success-light-7: #d1edc4;
--el-color-success-light-8: #e1f3d8;
--el-color-success-light-9: #f0f9eb;
--el-color-success-dark-2: #529b2e;
--el-color-warning: #e6a23c;
--el-color-warning-light-3: #eebe77;
--el-color-warning-light-5: #f3d19e;
--el-color-warning-light-7: #f8e3c5;
--el-color-warning-light-8: #faecd8;
--el-color-warning-light-9: #fdf6ec;
--el-color-warning-dark-2: #b88230;
--el-color-danger: #f56c6c;
--el-color-danger-light-3: #f89898;
--el-color-danger-light-5: #fab6b6;
--el-color-danger-light-7: #fcd3d3;
--el-color-danger-light-8: #fde2e2;
--el-color-danger-light-9: #fef0f0;
--el-color-danger-dark-2: #c45656;
--el-color-error: #f56c6c;
--el-color-error-light-3: #f89898;
--el-color-error-light-5: #fab6b6;
--el-color-error-light-7: #fcd3d3;
--el-color-error-light-8: #fde2e2;
--el-color-error-light-9: #fef0f0;
--el-color-error-dark-2: #c45656;
--el-color-info: #909399;
--el-color-info-light-3: #b1b3b8;
--el-color-info-light-5: #c8c9cc;
--el-color-info-light-7: #dedfe0;
--el-color-info-light-8: #e9e9eb;
--el-color-info-light-9: #f4f4f5;
--el-color-info-dark-2: #73767a;
--el-bg-color: #fff;
--el-bg-color-page: #fff;
--el-bg-color-overlay: #fff;
--el-text-color-primary: #303133;
--el-text-color-regular: #606266;
--el-text-color-secondary: #909399;
--el-text-color-placeholder: #a8abb2;
--el-text-color-disabled: #c0c4cc;
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
--el-border-color-extra-light: #f2f6fc;
--el-border-color-dark: #d4d7de;
--el-border-color-darker: #cdd0d6;
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f5f7fa;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #fafcff;
--el-fill-color-dark: #ebedf0;
--el-fill-color-darker: #e6e8eb;
--el-fill-color-blank: #fff;
--el-border-radius-base: 4px;
--el-border-radius-small: 2px;
--el-border-radius-round: 20px;
--el-border-radius-circle: 100%;
--el-font-size-extra-large: 20px;
--el-font-size-large: 18px;
--el-font-size-medium: 16px;
--el-font-size-base: 14px;
--el-font-size-small: 13px;
--el-font-size-extra-small: 12px;
--el-font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
--el-box-shadow: 0px 12px 32px 4px rgba(0,0,0,.04),0px 8px 20px rgba(0,0,0,.08);
--el-box-shadow-light: 0px 0px 12px rgba(0,0,0,.12);
--el-box-shadow-lighter: 0px 0px 6px rgba(0,0,0,.12);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0,0,0,.08),0px 12px 32px rgba(0,0,0,.12),0px 8px 16px -8px rgba(0,0,0,.16);
--el-disabled-bg-color: var(--el-fill-color-light);
--el-disabled-text-color: var(--el-text-color-placeholder);
--el-disabled-border-color: var(--el-border-color-light);
--el-index-normal: 1;
--el-index-top: 1000;
--el-index-popper: 2000;
--el-overlay-color: rgba(0,0,0,.8);
--el-overlay-color-light: rgba(0,0,0,.7);
--el-overlay-color-lighter: rgba(0,0,0,.5);
--el-mask-color: hsla(0,0%,100%,.9);
--el-mask-color-extra-light: hsla(0,0%,100%,.3);
--el-border-width: 1px;
--el-border-style: solid;
--el-border-color-hover: var(--el-text-color-disabled);
--el-border: var(--el-border-width) var(--el-border-style) var(--el-border-color);
--el-svg-monochrome-grey: var(--el-border-color);
--el-font-weight-primary: 500;
--el-font-line-height-primary: 24px;
--el-transition-duration: 0.3s;
--el-transition-duration-fast: 0.2s;
--el-transition-function-ease-in-out-bezier: cubic-bezier(0.645,0.045,0.355,1);
--el-transition-function-fast-bezier: cubic-bezier(0.23,1,0.32,1);
--el-transition-all: all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);
--el-transition-fade: opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);
--el-transition-md-fade: transform var(--el-transition-duration) var(--el-transition-function-fast-bezier),opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);
--el-transition-fade-linear: opacity var(--el-transition-duration-fast) linear;
--el-transition-border: border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);
--el-transition-box-shadow: box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);
--el-transition-color: color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);
--nt_brand_standard_2_overlay_hover_brand_2_mix: #008debff;
--nt_brand_standard_2_overlay_pressed_brand_2_mix: #0080d6ff;
--nt_feedback_error_2_overlay_hover_brand_2_mix: #e4462cff;
--nt_feedback_error_2_overlay_pressed_brand_2_mix: #cf4028ff;
--nt_icon_white_2_overlay_hover_2_mix: #f5f5f5ff;
--nt_icon_white_2_overlay_pressed_2_mix: #e0e0e0ff;
--nt_bg_top_light_2_overlay_hover_2_mix: #f5f5f5ff;
--nt_bg_top_light_2_overlay_pressed_2_mix: #e0e0e0ff;
--nt_icon_secondary_02_2_0_2_alpha: rgba(204, 204, 204, 0);
--nt_icon_secondary_02_2_70_2_alpha: rgba(204, 204, 204, 0.7);
--nt_text_link_2_50_2_alpha: rgba(45, 119, 229, 0.5);
--nt_bubble_host_2_overlay_pressed_brand_2_mix: #0080d6ff;
--nt_bg_white_2_overlay_pressed_brand_2_mix: #d6d6d6ff;
--nt_bg_white_2_overlay_hover_2_mix: #f5f5f5ff;
--nt_bg_white_2_overlay_pressed_2_mix: #e0e0e0ff;
--nt_fg_white_2_overlay_hover_2_mix: #f5f5f5ff;
--nt_fg_white_2_overlay_pressed_2_mix: #e0e0e0ff;
--nt_icon_red_2_overlay_hover_2_mix: #f5314fff;
--nt_icon_red_2_overlay_pressed_2_mix: #e02d48ff;
--nt_fg_grey_standard_2_overlay_hover_2_mix: #0000003b;
--nt_fg_grey_standard_2_overlay_pressed_2_mix: #0000004c;
--nt_bubble_guest_2_overlay_pressed_2_mix: #e0e0e0ff;
--nt_icon_primary_2_20_2_alpha: rgba(0, 0, 0, 0.2);
--nt_bg_grey_standard_2_95_2_alpha: rgba(242, 242, 242, 0.95);
--nt_tag_red_2_20_2_alpha: rgba(255, 134, 46, 0.2);
--nt_tag_red_2_25_2_alpha: rgba(255, 134, 46, 0.25);
--nt_tag_blue_2_20_2_alpha: rgba(0, 153, 255, 0.2);
--nt_tag_blue_2_25_2_alpha: rgba(0, 153, 255, 0.25);
--nt_tag_blue_2_10_2_alpha: rgba(0, 153, 255, 0.1);
--nt_brand_standard_2_20_2_alpha: rgba(0, 153, 255, 0.2);
--nt_feedback_error_2_20_2_alpha: rgba(247, 76, 48, 0.2);
--nt_text_white_2_60_2_alpha: rgba(255, 255, 255, 0.6);
--nt_bg_white_2_70_2_alpha: rgba(255, 255, 255, 0.7);
--nt_bg_white_2_90_2_alpha: rgba(255, 255, 255, 0.9);
--nt_bg_white_2_97_2_alpha: rgba(255, 255, 255, 0.97);
--nt_bg_white_2_40_2_alpha: rgba(255, 255, 255, 0.4);
--nt_bg_white_2_30_2_alpha: rgba(255, 255, 255, 0.3);
--nt_text_white_2_80_2_alpha: rgba(255, 255, 255, 0.8);
--nt_brand_standard_2_50_2_alpha: rgba(0, 153, 255, 0.5);
--nt_bg_nav_secondary_2_60_2_alpha: rgba(255, 255, 255, 0.6);
--nt_bg_nav_2_60_2_alpha: rgba(242, 242, 242, 0.6);
--nt_feedback_error_2_10_2_alpha: rgba(247, 76, 48, 0.1);
--nt_brand_standard_2_10_2_alpha: rgba(0, 153, 255, 0.1);
--nt_on_brand_primary_2_40_2_alpha: rgba(255, 255, 255, 0.4);
--nt_text_primary_2_72_2_alpha: rgba(0, 0, 0, 0.72);
--nt_text_white_2_72_2_alpha: rgba(255, 255, 255, 0.72);
--border_secondary: 1px solid rgba(0, 0, 0, 0.0578);
--border_primary: 1px solid rgba(117, 117, 117, 0.4);
--shadow_bg_top: 0px 4px 8px rgba(0, 0, 0, 0.14);
--shadow_bg_middle_secondary: 0px 8px 16px rgba(0, 0, 0, 0.14);
--shadow_bg_middle_primary: 0px 32px 64px rgba(0, 0, 0, 0.1876), 0px 2px 21px rgba(0, 0, 0, 0.1474);
--shadow_bg_bottom_inactive: 0px 16px 32px rgba(0, 0, 0, 0.1876), 0px 2px 10.67px rgba(0, 0, 0, 0.1474);
--shadow_bg_bottom_active: 0px 32px 64px rgba(0, 0, 0, 0.28), 0px 2px 21px rgba(0, 0, 0, 0.22);
--brand_standard: #0099ffff;
--on_brand_primary: #ffffffff;
--on_brand_secondary: #ffffffff;
--text_primary: #000000ff;
--text_primary_light: #00000099;
--text_secondary_02: #ccccccff;
--text_white: #ffffffff;
--text_secondary_01: #999999ff;
--text_black: #000000ff;
--on_bg_text: #999999ff;
--text_link: #2d77e5ff;
--text_secondary: #00000080;
--text_tertiary: #0000004d;
--icon_primary: #000000ff;
--icon_secondary_01: #999999ff;
--icon_secondary_02: #ccccccff;
--icon_white: #ffffffff;
--icon_red: #ff3352ff;
--icon_black: #000000ff;
--icon_secondary: #00000066;
--icon_tertiary: #0000004d;
--feedback_success: #15d173ff;
--feedback_warning: #ffb300ff;
--feedback_error: #f74c30ff;
--bg_grey_standard: #f2f2f2ff;
--bg_white: #ffffffff;
--bg_list: #ffffffff;
--bg_aio_1: #f2f2f2ff;
--bg_aio_2: #f2f2f2ff;
--bg_aio_3: #f2f2f2ff;
--bg_aio_4: #f2f2f2ff;
--bg_nav: #f2f2f2ff;
--mac_bg_nav: #ffffff1a;
--bg_bottom_standard: #f2f2f2ff;
--bg_bottom_light: #ffffffff;
--bg_middle_standard: #f2f2f2ff;
--bg_middle_light: #ffffffff;
--bg_top_standard: #f2f2f2ff;
--bg_top_light: #ffffffff;
--bg_nav_secondary: #ffffffff;
--bubble_host: #0099ffff;
--bubble_guest: #ffffffff;
--bubble_host_text: #ffffff;
--bubble_guest_text: #000000ff;
--bubble_host_1: #0099ffff;
--bubble_host_2: #0099ffff;
--fg_grey_standard: #00000033;
--fg_white: #ffffffff;
--fg_grey_light: #0000000a;
--fill_standard_secondary: #0000000a;
--fill_standard_primary: #00000033;
--fill_light_primary: #ffffffff;
--fill_light_secondary: #ffffffff;
--divider_standard: #0000000a;
--divider_dark: #00000014;
--border_standard: #0000000a;
--border_dark: #00000014;
--overlay_hover: #0000000a;
--overlay_hover_brand: #00000014;
--overlay_pressed_brand: #00000029;
--overlay_active_brand: #0099ffff;
--overlay_top: #0000000f;
--overlay_mask_standard: #00000080;
--overlay_mask_dark: #00000099;
--overlay_pressed: #0000001f;
--overlay_active: #00000014;
--overlay_mask_aio: #00000000;
--blur_standard: #ffffffcc;
--blur_superlight: #ffffff1a;
--blur_middle_standard: #ffffffcc;
--blur_bottom_superlight: #ffffff1a;
--extend_blue: #eaf1ffff;
--svip_red: #ff4222ff;
--tag_sage_green_bg: #a3c4c633;
--tag_sage_green_text: #769698ff;
--tag_red_bg: #ff3f3233;
--tag_red_text: #f74c30ff;
--tag_orange_text: #ff8d40ff;
--tag_orange_bg: #ff862e33;
--tag_purple_text: #aa76f6ff;
--tag_purple_bg: #b27eff33;
--tag_blue_text: #0099ffff;
--tag_blue_bg: #0099ff33;
--tag_blue: #0099ff33;
--tag_red: #ff862e33;
--border_white: #ffffffff;
--border_secondary: #0000000f;
--border_primary: #75757566;
--mac_border_primary: #00000014;
--mac_border_secondary: #0000000a;
--host_bubble_bg_css_value: #0099ff;
--on_bubble_host_text: #ffffffff;
--brand_text: #0099ffff;
--text-primary: #000;
--text-primary-light: #666;
--text-secondary-01: #999;
--text-secondary-02: #ccc;
--text-white: #fff;
--text-brand: #0099ff;
--text-link: #2d77e5;
--text-success: #12d173;
--text-warning: #ffb300;
--text-error: #ff5967;
--icon-primary: #000;
--icon-secondary-01: #999999;
--icon-secondary-02: #cccccc;
--icon-white: #fff;
--icon-brand: #0099ff;
--icon-success: #15d173;
--icon-warning: #ffb300;
--icon-error: #ff5967;
--button-primary-default: #0099ff;
--button-primary-hover: #4DB7FF;
--button-primary-pressed: #0089E5;
--button-primary-disable: #CCEBFF;
--button-secondary-default: #ccc;
--button-secondary-hover: #E5E5E5;
--button-secondary-pressed: #B2B2B2;
--button-secondary-disable: #F0F0F0;
--button-white-default: #FFFFFF;
--button-white-hover: #E5E5E5;
--button-white-pressed: #B2B2B2;
--button-white-disable: #ffffff4d;
--button-error-default: #FF5967;
--button-error-hover: #FF8B94;
--button-error-pressed: #E6505C;
--button-error-disable: #FFEEEF;
--bubble-host: #0099FF;
--bubble-guest: #EBEBEB;
--divider-standard: #E5E5E5;
--divider-light: #F5F5F5;
--divider-brand: #0099FF;
--background-01: #000;
--background-02: #E5E5E5;
--background-03: #F5F5F5;
--background-04: #FAFAFA;
--background-05: #FFFFFF;
--background-dialogue: #FFFFFF;
--hover-list: #F0F0F0;
--hover-icon: #EBEBEB;
--hover-link: #81ADEF;
--press-list: #E5E5E5;
--press-icon: #E5E5E5;
--press-link: #286BCE;
--badge-brand: #0099FF;
--badge-red: #FF5967;
--badge-grey: #CCCCCC;
--audio-hangup: #FF3350;
--gray-black: #000;
--gray-20: #333333;
--gray-40: #666666;
--gray-60: #999999;
--gray-80: #cccccc;
--gray-90: #e5e5e5;
--gray-96: #f5f5f5;
--gray-white: #ffffff;
--blue-dark: #0089E5;
--blue-standard: #0099FF;
--blue-light: #4DB7FF;
--blue-superlight: #E6F5FF;
--green-dark: #12BC67;
--green-standard: #15D173;
--green-light: #5BDE9D;
--green-superlight: #E8FAF1;
--yellow-dark: #E5A000;
--yellow-standard: #FFB300;
--yellow-light: #FFC94C;
--yellow-superlight: #FFF7E5;
--orange-dark: #E57E39;
--orange-standard: #FF8D40;
--orange-light: #FFAE78;
--orange-superlight: #FFF3EB;
--red-dark: #E6505C;
--red-standard: #FF5967;
--red-light: #FF8B94;
--red-superlight: #FFEEEF;
--pink-dark: #E55BA0;
--pink-standard: #FF66B3;
--pink-light: #FF93C9;
--pink-superlight: #FFEFF7;
--indigo-dark: #775CE6;
--indigo-standard: #8566FF;
--indigo-light: #A994FF;
--indigo-superlight: #F3F0FF;
--list-hover: rgba(243, 243, 243);
--list-pressed: rgba(226, 226, 226);
--background_01: #000000;
--background_02: #E6E6E6;
--background_03: #F5F5F5;
--background_04: #FAFAFA;
--background_05: #FFFFFF;
--background_dialogue: #FFFFFF;
--sidebar_win: #ebebeb;
--sidebar_mac: rgba(255,255,255,0.1);
--nt_mix_tokens: nt_brand_standard_2_overlay_hover_brand_2_mix,nt_brand_standard_2_overlay_pressed_brand_2_mix,nt_feedback_error_2_overlay_hover_brand_2_mix,nt_feedback_error_2_overlay_pressed_brand_2_mix,nt_icon_white_2_overlay_hover_2_mix,nt_icon_white_2_overlay_pressed_2_mix,nt_bubble_host_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_hover_2_mix,nt_bg_white_2_overlay_pressed_2_mix,nt_fg_white_2_overlay_hover_2_mix,nt_fg_white_2_overlay_pressed_2_mix,nt_icon_red_2_overlay_hover_2_mix,nt_icon_red_2_overlay_pressed_2_mix,nt_fg_grey_standard_2_overlay_hover_2_mix,nt_fg_grey_standard_2_overlay_pressed_2_mix,nt_bubble_guest_2_overlay_pressed_2_mix,nt_icon_primary_2_20_2_alpha,nt_bg_grey_standard_2_95_2_alpha,nt_tag_red_2_20_2_alpha,nt_tag_red_2_25_2_alpha,nt_tag_blue_2_20_2_alpha,nt_tag_blue_2_25_2_alpha,nt_tag_blue_2_10_2_alpha,nt_tag_purple_2_20_2_alpha,nt_brand_standard_2_20_2_alpha,nt_tag_sage_green_2_20_2_alpha,nt_feedback_error_2_20_2_alpha,nt_text_white_2_60_2_alpha,nt_bg_white_2_70_2_alpha,nt_bg_white_2_90_2_alpha,nt_bg_white_2_97_2_alpha,nt_bg_white_2_40_2_alpha,nt_bg_white_2_30_2_alpha,nt_text_white_2_80_2_alpha,nt_brand_standard_2_50_2_alpha,nt_bg_nav_secondary_2_60_2_alpha,nt_bg_nav_2_60_2_alpha,nt_feedback_error_2_10_2_alpha,nt_brand_standard_2_10_2_alpha,nt_on_brand_primary_2_40_2_alpha,nt_text_primary_2_72_2_alpha,nt_text_white_2_72_2_alpha;
color-scheme: light;
}
.q-theme-tokens-dark {
--nt_brand_standard_2_overlay_hover_brand_2_mix: #1472d0ff;
--nt_brand_standard_2_overlay_pressed_brand_2_mix: #0056abff;
--nt_feedback_error_2_overlay_hover_brand_2_mix: #f85a40ff;
--nt_feedback_error_2_overlay_pressed_brand_2_mix: #cf4028ff;
--nt_icon_white_2_overlay_hover_2_mix: #ffffffff;
--nt_icon_white_2_overlay_pressed_2_mix: #d6d6d6ff;
--nt_bg_top_light_2_overlay_hover_2_mix: #404040ff;
--nt_bg_top_light_2_overlay_pressed_2_mix: #282828ff;
--nt_icon_secondary_02_2_0_2_alpha: rgba(77, 77, 77, 0);
--nt_icon_secondary_02_2_70_2_alpha: rgba(77, 77, 77, 0.7);
--nt_text_link_2_50_2_alpha: rgba(45, 119, 229, 0.5);
--nt_bubble_host_2_overlay_pressed_brand_2_mix: #202020ff;
--nt_bg_white_2_overlay_pressed_brand_2_mix: #202020ff;
--nt_bg_white_2_overlay_hover_2_mix: #373737ff;
--nt_bg_white_2_overlay_pressed_2_mix: #202020ff;
--nt_fg_white_2_overlay_hover_2_mix: #373737ff;
--nt_fg_white_2_overlay_pressed_2_mix: #202020ff;
--nt_icon_red_2_overlay_hover_2_mix: #ff4360ff;
--nt_icon_red_2_overlay_pressed_2_mix: #d62b45ff;
--nt_fg_grey_standard_2_overlay_hover_2_mix: #ffffff43;
--nt_fg_grey_standard_2_overlay_pressed_2_mix: #82828254;
--nt_bubble_guest_2_overlay_pressed_2_mix: #202020ff;
--nt_icon_primary_2_20_2_alpha: rgba(255, 255, 255, 0.2);
--nt_bg_grey_standard_2_95_2_alpha: rgba(26, 26, 26, 0.95);
--nt_tag_red_2_20_2_alpha: rgba(255, 134, 46, 0.2);
--nt_tag_red_2_25_2_alpha: rgba(255, 134, 46, 0.25);
--nt_tag_blue_2_20_2_alpha: rgba(0, 153, 255, 0.2);
--nt_tag_blue_2_25_2_alpha: rgba(0, 153, 255, 0.25);
--nt_tag_blue_2_10_2_alpha: rgba(0, 153, 255, 0.1);
--nt_brand_standard_2_20_2_alpha: rgba(0, 102, 204, 0.2);
--nt_feedback_error_2_20_2_alpha: rgba(247, 76, 48, 0.2);
--nt_text_white_2_60_2_alpha: rgba(255, 255, 255, 0.6);
--nt_bg_white_2_70_2_alpha: rgba(38, 38, 38, 0.7);
--nt_bg_white_2_90_2_alpha: rgba(38, 38, 38, 0.9);
--nt_bg_white_2_97_2_alpha: rgba(38, 38, 38, 0.97);
--nt_bg_white_2_40_2_alpha: rgba(38, 38, 38, 0.4);
--nt_bg_white_2_30_2_alpha: rgba(38, 38, 38, 0.3);
--nt_text_white_2_80_2_alpha: rgba(255, 255, 255, 0.8);
--nt_brand_standard_2_50_2_alpha: rgba(0, 102, 204, 0.5);
--nt_bg_nav_secondary_2_60_2_alpha: rgba(27, 27, 27, 0.6);
--nt_bg_nav_2_60_2_alpha: rgba(17, 17, 17, 0.6);
--nt_feedback_error_2_10_2_alpha: rgba(247, 76, 48, 0.1);
--nt_brand_standard_2_10_2_alpha: rgba(0, 102, 204, 0.1);
--nt_on_brand_primary_2_40_2_alpha: rgba(255, 255, 255, 0.4);
--nt_text_primary_2_72_2_alpha: rgba(255, 255, 255, 0.72);
--nt_text_white_2_72_2_alpha: rgba(255, 255, 255, 0.72);
--border_secondary: 1px solid rgba(0, 0, 0, 0.2);
--border_primary: 1px solid rgba(117, 117, 117, 0.4);
--shadow_bg_top: 0px 4px 8px rgba(0, 0, 0, 0.26);
--shadow_bg_middle_secondary: 0px 8px 16px rgba(0, 0, 0, 0.14);
--shadow_bg_middle_primary: 0px 32px 64px rgba(0, 0, 0, 0.37), 0px 2px 21px rgba(0, 0, 0, 0.37);
--shadow_bg_bottom_inactive: 0px 32px 64px rgba(0, 0, 0, 0.56), 0px 2px 21px rgba(0, 0, 0, 0.55);
--shadow_bg_bottom_active: 0px 32px 64px rgba(0, 0, 0, 0.56), 0px 2px 21px rgba(0, 0, 0, 0.55);
--brand_standard: #0066ccff;
--on_brand_primary: #ffffffff;
--on_brand_secondary: #ffffffff;
--text_primary: #ffffffe6;
--text_primary_light: #ffffff99;
--text_secondary_02: #666666ff;
--text_white: #ffffffe6;
--text_secondary_01: #808080ff;
--text_black: #000000ff;
--on_bg_text: #808080ff;
--text_link: #2d77e5ff;
--text_secondary: #ffffff99;
--text_tertiary: #ffffff66;
--icon_primary: #ffffffb3;
--icon_secondary_01: #666666ff;
--icon_secondary_02: #4d4d4dff;
--icon_white: #ffffffff;
--icon_red: #ff3352ff;
--icon_black: #000000ff;
--icon_secondary: #ffffff80;
--icon_tertiary: #ffffff66;
--feedback_success: #15d173ff;
--feedback_warning: #ffb300ff;
--feedback_error: #f74c30ff;
--bg_grey_standard: #1a1a1aff;
--bg_white: #262626ff;
--bg_list: #1b1b1bff;
--bg_aio_1: #1a1a1aff;
--bg_aio_2: #1a1a1aff;
--bg_aio_3: #1a1a1aff;
--bg_aio_4: #1a1a1aff;
--bg_nav: #111111ff;
--mac_bg_nav: #0000001a;
--bg_bottom_standard: #111111ff;
--bg_bottom_light: #1b1b1bff;
--bg_middle_standard: #1b1b1bff;
--bg_middle_light: #262626ff;
--bg_top_standard: #262626ff;
--bg_top_light: #303030ff;
--bg_nav_secondary: #1b1b1bff;
--bubble_host: #262626ff;
--bubble_guest: #262626ff;
--bubble_host_text: #f2f2f2;
--bubble_guest_text: #f2f2f2ff;
--bubble_host_1: #262626ff;
--bubble_host_2: #262626ff;
--fg_grey_standard: #ffffff33;
--fg_white: #262626ff;
--fg_grey_light: #00000033;
--fill_standard_secondary: #ffffff0f;
--fill_standard_primary: #ffffff33;
--fill_light_primary: #262626ff;
--fill_light_secondary: #ffffff0f;
--divider_standard: #ffffff0a;
--divider_dark: #ffffff14;
--border_standard: #ffffff0f;
--border_dark: #ffffff14;
--overlay_hover: #ffffff14;
--overlay_hover_brand: #ffffff14;
--overlay_pressed_brand: #00000029;
--overlay_active_brand: #0066ccff;
--overlay_top: #ffffff0f;
--overlay_mask_standard: #00000080;
--overlay_mask_dark: #00000099;
--overlay_pressed: #00000029;
--overlay_active: #ffffff1f;
--overlay_mask_aio: #00000000;
--blur_standard: #000000cc;
--blur_superlight: #0000001a;
--blur_middle_standard: #262626cc;
--blur_bottom_superlight: #0000001a;
--extend_blue: #002f65ff;
--svip_red: #ff4222ff;
--tag_sage_green_bg: #a3c4c633;
--tag_sage_green_text: #769698ff;
--tag_red_bg: #ff3f3233;
--tag_red_text: #f74c30ff;
--tag_orange_text: #ff8d40ff;
--tag_orange_bg: #ff862e33;
--tag_purple_text: #aa76f6ff;
--tag_purple_bg: #b27eff33;
--tag_blue_text: #0066ccff;
--tag_blue_bg: #0099ff33;
--tag_blue: #0099ff40;
--tag_red: #ff862e33;
--border_white: #262626ff;
--border_secondary: #00000033;
--border_primary: #75757566;
--mac_border_primary: #ffffff14;
--mac_border_secondary: #ffffff0a;
--host_bubble_bg_css_value: #262626;
--on_bubble_host_text: #f2f2f2ff;
--brand_text: #0066ccff;
--text-primary: #FFFFFF;
--text-primary-light: #CCCCCC;
--text-secondary-01: #999;
--text-secondary-02: #666666;
--text-white: #fff;
--text-brand: #0099ff;
--text-link: #2d77e5;
--text-success: #12d173;
--text-warning: #ffb300;
--text-error: #ff5967;
--icon-primary: #999999;
--icon-secondary-01: #999999;
--icon-secondary-02: #999999;
--icon-white: #fff;
--icon-brand: #0099ff;
--icon-success: #15d173;
--icon-warning: #ffb300;
--icon-error: #ff5967;
--button-primary-default: #0066CC;
--button-primary-hover: #19467F;
--button-primary-pressed: #0A1F33;
--button-primary-disable: #232323;
--button-secondary-default: #232323;
--button-secondary-hover: #1F1F1F;
--button-secondary-pressed: #1A1A1A;
--button-secondary-disable: #1A1A1A;
--button-white-default: #FFFFFF;
--button-white-hover: #FAFAFA;
--button-white-pressed: #F5F5F5;
--button-white-disable: #FFFFFF;
--button-error-default: #FF5967;
--button-error-hover: #FF8B94;
--button-error-pressed: #E6505C;
--button-error-disable: #FFEEEF;
--bubble-host: #262626;
--bubble-guest: #262626;
--divider-standard: #242424;
--divider-light: #4D4D4D;
--divider-brand: #0099FF;
--background-01: #FFFFFF;
--background-02: #242424;
--background-03: #181818;
--background-04: #1F1F1F;
--background-05: #000000;
--background-dialogue: #262626;
--hover-list: #292929;
--hover-icon: #333333;
--hover-link: #81ADEF;
--press-list: #383838;
--press-icon: #262626;
--press-link: #286BCE;
--badge-brand: #0099FF;
--badge-red: #FF5967;
--badge-grey: #4D4D4D;
--audio-hangup: #FF3350;
--gray-black: #000;
--gray-20: #333333;
--gray-40: #666666;
--gray-60: #999999;
--gray-80: #cccccc;
--gray-90: #e5e5e5;
--gray-96: #f5f5f5;
--gray-white: #ffffff;
--blue-dark: #0057BD;
--blue-standard: #0066CC;
--blue-light: #0072E4;
--blue-superlight: #E6F5FF;
--green-dark: #12BC67;
--green-standard: #15D173;
--green-light: #5BDE9D;
--green-superlight: #E8FAF1;
--yellow-dark: #E5A000;
--yellow-standard: #FFB300;
--yellow-light: #FFC94C;
--yellow-superlight: #FFF7E5;
--orange-dark: #E57E39;
--orange-standard: #FF8D40;
--orange-light: #FFAE78;
--orange-superlight: #FFF3EB;
--red-dark: #E6505C;
--red-standard: #FF5967;
--red-light: #FF8B94;
--red-superlight: #FFEEEF;
--pink-dark: #E55BA0;
--pink-standard: #FF66B3;
--pink-light: #FF93C9;
--pink-superlight: #FFEFF7;
--indigo-dark: #775CE6;
--indigo-standard: #8566FF;
--indigo-light: #A994FF;
--indigo-superlight: #F3F0FF;
--list-hover: rgba(71, 71, 71);
--list-pressed: rgba(28, 28, 28);
--background_01: #ffffff;
--background_02: #292929;
--background_03: #1A1A1A;
--background_04: #212121;
--background_05: #212121;
--background_dialogue: #292929;
--sidebar_win: rgba(0,0,0,0.8);
--sidebar_mac: rgba(0,0,0,0.1);
color-scheme: dark;
}

File diff suppressed because one or more lines are too long

View File

@ -1,469 +0,0 @@
const SettingList = (items, title, isCollapsible = false, direction = "column") => {
return `<setting-section ${""}>
<setting-panel>
<setting-list ${direction ? `data-direction="${direction}"` : ""} ${isCollapsible ? "is-collapsible" : ""} ${""}>
${items.join("")}
</setting-list>
</setting-panel>
</setting-section>`;
};
const SettingItem = (title, subtitle, action, id, visible = true) => {
return `<setting-item ${id ? `id="${id}"` : ""} ${!visible ? "is-hidden" : ""}>
<div>
<setting-text>${title}</setting-text>
${subtitle ? `<setting-text data-type="secondary">${subtitle}</setting-text>` : ""}
</div>
${action ? `<div>${action}</div>` : ""}
</setting-item>`;
};
const SettingButton = (text, id, type = "secondary") => {
return `<setting-button ${type ? `data-type="${type}"` : ""} ${id ? `id="${id}"` : ""}>${text}</setting-button>`;
};
const SettingSwitch = (configKey, isActive = false, extraData) => {
return `<setting-switch
${configKey ? `data-config-key="${configKey}"` : ""}
${isActive ? "is-active" : ""}
${extraData ? Object.keys(extraData).map((key) => `data-${key}="${extraData[key]}"`) : ""}
>
</setting-switch>`;
};
const SettingOption = (text, value, isSelected = false) => {
return `<setting-option ${value ? `data-value="${value}"` : ""} ${isSelected ? "is-selected" : ""}>${text}</setting-option>`;
};
const SelectTemplate = document.createElement("template");
SelectTemplate.innerHTML = `<style>
.hidden { display: none !important; }
</style>
<div part="parent">
<div part="button">
<input type="text" placeholder="请选择" part="current-text" />
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" part="button-arrow">
<path d="M12 6.0001L8.00004 10L4 6" stroke="currentColor" stroke-linejoin="round"></path>
</svg>
</div>
<ul class="hidden" part="option-list"><slot></slot></ul>
</div>`;
window.customElements.define(
"ob-setting-select",
class extends HTMLElement {
_button;
_text;
_context;
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot?.append(SelectTemplate.content.cloneNode(true));
this._button = this.shadowRoot.querySelector('div[part="button"]');
this._text = this.shadowRoot.querySelector('input[part="current-text"]');
this._context = this.shadowRoot.querySelector('ul[part="option-list"]');
const buttonClick = () => {
const isHidden = this._context.classList.toggle("hidden");
window[`${isHidden ? "remove" : "add"}EventListener`]("pointerdown", windowPointerDown);
};
const windowPointerDown = ({ target }) => {
if (!this.contains(target)) buttonClick();
};
this._button.addEventListener("click", buttonClick);
this._context.addEventListener("click", (event) => {
const { target } = event;
if (target.tagName !== "SETTING-OPTION") return;
buttonClick();
if (target.hasAttribute("is-selected")) return;
this.querySelectorAll("setting-option[is-selected]").forEach((dom) => dom.toggleAttribute("is-selected"));
target.toggleAttribute("is-selected");
this._text.value = target.textContent;
this.dispatchEvent(
new CustomEvent("selected", {
bubbles: true,
composed: true,
detail: {
name: target.textContent,
value: target.dataset.value
}
})
);
});
this._text.value = this.querySelector("setting-option[is-selected]")?.textContent;
}
}
);
const SettingSelect = (items, configKey, configValue) => {
return `<ob-setting-select ${`data-config-key="${configKey}"` }>
${items.map((e, i) => {
return SettingOption(e.text, e.value, configValue ? configValue === e.value : i === 0);
}).join("")}
</ob-setting-select>`;
};
class WebUiApiOB11ConfigWrapper {
retCredential = "";
async Init(Credential) {
this.retCredential = Credential;
}
async GetOB11Config() {
const ConfigResponse = await fetch("../api/OB11Config/GetConfig", {
method: "POST",
headers: {
Authorization: "Bearer " + this.retCredential,
"Content-Type": "application/json"
}
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data;
}
}
return {};
}
async SetOB11Config(config) {
const ConfigResponse = await fetch("../api/OB11Config/SetConfig", {
method: "POST",
headers: {
Authorization: "Bearer " + this.retCredential,
"Content-Type": "application/json"
},
body: JSON.stringify({ config: JSON.stringify(config) })
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return true;
}
}
return false;
}
}
const OB11ConfigWrapper = new WebUiApiOB11ConfigWrapper();
async function onSettingWindowCreated(view) {
const isEmpty = (value) => value === void 0 || value === void 0 || value === "";
await OB11ConfigWrapper.Init(localStorage.getItem("auth"));
const ob11Config = await OB11ConfigWrapper.GetOB11Config();
const setOB11Config = (key, value) => {
const configKey = key.split(".");
if (configKey.length === 2) {
ob11Config[configKey[1]] = value;
} else if (configKey.length === 3) {
ob11Config[configKey[1]][configKey[2]] = value;
}
};
const parser = new DOMParser();
const doc = parser.parseFromString(
[
"<div>",
`<setting-section id="napcat-error">
<setting-panel><pre><code></code></pre></setting-panel>
</setting-section>`,
SettingList([
SettingItem(
'<span id="napcat-update-title">Napcat</span>',
void 0,
SettingButton("V4.0.3", "napcat-update-button", "secondary")
)
]),
SettingList([
SettingItem(
"启用 HTTP 服务",
void 0,
SettingSwitch("ob11.http.enable", ob11Config.http.enable, {
"control-display-id": "config-ob11-http-port"
})
),
SettingItem(
"HTTP 服务监听端口",
void 0,
`<div class="q-input"><input class="q-input__inner" data-config-key="ob11.http.port" type="number" min="1" max="65534" value="${ob11Config.http.port}" placeholder="${ob11Config.http.port}" /></div>`,
"config-ob11-http-port",
ob11Config.http.enable
),
SettingItem(
"启用 HTTP 心跳",
void 0,
SettingSwitch("ob11.http.enableHeart", ob11Config.http.enableHeart, {
"control-display-id": "config-ob11-HTTP.enableHeart"
})
),
SettingItem(
"启用 HTTP 事件上报",
void 0,
SettingSwitch("ob11.http.enablePost", ob11Config.http.enablePost, {
"control-display-id": "config-ob11-http-postUrls"
})
),
`<div class="config-host-list" id="config-ob11-http-postUrls" ${ob11Config.http.enablePost ? "" : "is-hidden"}>
<setting-item data-direction="row">
<div>
<setting-text>HTTP 事件上报密钥</setting-text>
</div>
<div class="q-input">
<input id="config-ob11-http-secret" class="q-input__inner" data-config-key="ob11.http.secret" type="text" value="${ob11Config.http.secret}" placeholder="未设置" />
</div>
</setting-item>
<setting-item data-direction="row">
<div>
<setting-text>HTTP 事件上报地址</setting-text>
</div>
<setting-button id="config-ob11-http-postUrls-add" data-type="primary">添加</setting-button>
</setting-item>
<div id="config-ob11-http-postUrls-list"></div>
</div>`,
SettingItem(
"启用正向 WebSocket 服务",
void 0,
SettingSwitch("ob11.ws.enable", ob11Config.ws.enable, {
"control-display-id": "config-ob11-ws-port"
})
),
SettingItem(
"正向 WebSocket 服务监听端口",
void 0,
`<div class="q-input"><input class="q-input__inner" data-config-key="ob11.ws.port" type="number" min="1" max="65534" value="${ob11Config.ws.port}" placeholder="${ob11Config.ws.port}" /></div>`,
"config-ob11-ws-port",
ob11Config.ws.enable
),
SettingItem(
"启用反向 WebSocket 服务",
void 0,
SettingSwitch("ob11.reverseWs.enable", ob11Config.reverseWs.enable, {
"control-display-id": "config-ob11-reverseWs-urls"
})
),
`<div class="config-host-list" id="config-ob11-reverseWs-urls" ${ob11Config.reverseWs.enable ? "" : "is-hidden"}>
<setting-item data-direction="row">
<div>
<setting-text>反向 WebSocket 监听地址</setting-text>
</div>
<setting-button id="config-ob11-reverseWs-urls-add" data-type="primary">添加</setting-button>
</setting-item>
<div id="config-ob11-reverseWs-urls-list"></div>
</div>`,
SettingItem(
" WebSocket 服务心跳间隔",
"控制每隔多久发送一个心跳包,单位为毫秒",
`<div class="q-input"><input class="q-input__inner" data-config-key="ob11.heartInterval" type="number" min="1000" value="${ob11Config.heartInterval}" placeholder="${ob11Config.heartInterval}" /></div>`
),
SettingItem(
"Access token",
void 0,
`<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="ob11.token" type="text" value="${ob11Config.token}" placeholder="未设置" /></div>`
),
SettingItem(
"新消息上报格式",
`如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 <a href="javascript:LiteLoader.api.openExternal('https://github.com/botuniverse/onebot-11/tree/master/message#readme');">OneBot v11 文档</a>`,
SettingSelect(
[
{ text: "消息段", value: "array" },
{ text: "CQ码", value: "string" }
],
"ob11.messagePostFormat",
ob11Config.messagePostFormat
)
),
SettingItem(
"音乐卡片签名地址",
void 0,
`<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="ob11.musicSignUrl" type="text" value="${ob11Config.musicSignUrl}" placeholder="未设置" /></div>`,
"ob11.musicSignUrl"
),
SettingItem(
"启用本地进群时间与发言时间记录",
void 0,
SettingSwitch("ob11.GroupLocalTime.Record", ob11Config.GroupLocalTime.Record, {
"control-display-id": "config-ob11-GroupLocalTime-RecordList"
})
),
`<div class="config-host-list" id="config-ob11-GroupLocalTime-RecordList" ${ob11Config.GroupLocalTime.Record ? "" : "is-hidden"}>
<setting-item data-direction="row">
<div>
<setting-text>群列表</setting-text>
</div>
<setting-button id="config-ob11-GroupLocalTime-RecordList-add" data-type="primary">添加</setting-button>
</setting-item>
<div id="config-ob11-GroupLocalTime-RecordList-list"></div>
</div>`,
SettingItem(
"",
void 0,
SettingButton("保存", "config-ob11-save", "primary")
)
]),
SettingList([
SettingItem(
"上报 Bot 自身发送的消息",
"上报 event 为 message_sent",
SettingSwitch("ob11.reportSelfMessage", ob11Config.reportSelfMessage)
)
]),
SettingList([
SettingItem(
"GitHub 仓库",
"https://github.com/NapNeko/NapCatQQ",
SettingButton("点个星星", "open-github")
),
SettingItem("NapCat 文档", "", SettingButton("看看文档", "open-docs"))
]),
SettingItem(
"Telegram 群",
"https://t.me/+nLZEnpne-pQ1OWFl",
SettingButton("进去逛逛", "open-telegram")
),
SettingItem(
"QQ 群",
"518662028",
SettingButton("我要进去", "open-qq-group")
),
"</div>"
].join(""),
"text/html"
);
doc.querySelector("#open-github")?.addEventListener("click", () => {
window.open("https://github.com/NapNeko/NapCatQQ", "_blank");
});
doc.querySelector("#open-docs")?.addEventListener("click", () => {
window.open("https://napneko.github.io/", "_blank");
});
doc.querySelector("#open-telegram")?.addEventListener("click", () => {
window.open("https://t.me/+nLZEnpne-pQ1OWFl", "_blank");
});
doc.querySelector("#open-qq-group")?.addEventListener("click", () => {
window.open("https://qm.qq.com/q/VfjAq5HIMS", "_blank");
});
const buildHostListItem = (type, host, index, inputAttrs = {}) => {
const dom = {
container: document.createElement("setting-item"),
input: document.createElement("input"),
inputContainer: document.createElement("div"),
deleteBtn: document.createElement("setting-button")
};
dom.container.classList.add("setting-host-list-item");
dom.container.dataset.direction = "row";
Object.assign(dom.input, inputAttrs);
dom.input.classList.add("q-input__inner");
dom.input.type = "url";
dom.input.value = host;
dom.input.addEventListener("input", () => {
ob11Config[type.split("-")[0]][type.split("-")[1]][index] = dom.input.value;
});
dom.inputContainer.classList.add("q-input");
dom.inputContainer.appendChild(dom.input);
dom.deleteBtn.innerHTML = "删除";
dom.deleteBtn.dataset.type = "secondary";
dom.deleteBtn.addEventListener("click", () => {
ob11Config[type.split("-")[0]][type.split("-")[1]].splice(index, 1);
initReverseHost(type);
});
dom.container.appendChild(dom.inputContainer);
dom.container.appendChild(dom.deleteBtn);
return dom.container;
};
const buildHostList = (hosts, type, inputAttr = {}) => {
const result = [];
hosts?.forEach((host, index) => {
result.push(buildHostListItem(type, host, index, inputAttr));
});
return result;
};
const addReverseHost = (type, doc2 = document, inputAttr = {}) => {
type = type.replace(/\./g, "-");
const hostContainerDom = doc2.body.querySelector(
`#config-ob11-${type}-list`
);
hostContainerDom?.appendChild(
buildHostListItem(
type,
"",
ob11Config[type.split("-")[0]][type.split("-")[1]].length,
inputAttr
)
);
ob11Config[type.split("-")[0]][type.split("-")[1]].push("");
};
const initReverseHost = (type, doc2 = document) => {
type = type.replace(/\./g, "-");
const hostContainerDom = doc2.body?.querySelector(
`#config-ob11-${type}-list`
);
if (hostContainerDom) {
[...hostContainerDom.childNodes].forEach((dom) => dom.remove());
buildHostList(
ob11Config[type.split("-")[0]][type.split("-")[1]],
type
).forEach((dom) => {
hostContainerDom?.appendChild(dom);
});
}
};
initReverseHost("http.postUrls", doc);
initReverseHost("reverseWs.urls", doc);
initReverseHost("GroupLocalTime.RecordList", doc);
doc.querySelector("#config-ob11-http-postUrls-add")?.addEventListener(
"click",
() => addReverseHost("http.postUrls", document, {
placeholder: "如http://127.0.0.1:5140/onebot"
})
);
doc.querySelector("#config-ob11-reverseWs-urls-add")?.addEventListener(
"click",
() => addReverseHost("reverseWs.urls", document, {
placeholder: "如ws://127.0.0.1:5140/onebot"
})
);
doc.querySelector("#config-ob11-GroupLocalTime-RecordList-add")?.addEventListener(
"click",
() => addReverseHost("GroupLocalTime.RecordList", document, {
placeholder: "此处填写群号 -1为全部"
})
);
doc.querySelector("#config-ffmpeg-select")?.addEventListener("click", () => {
});
doc.querySelector("#config-open-log-path")?.addEventListener("click", () => {
});
doc.querySelectorAll("setting-switch[data-config-key]").forEach((dom) => {
dom.addEventListener("click", () => {
const active = dom.getAttribute("is-active") == void 0;
setOB11Config(dom.dataset.configKey, active);
if (active) dom.setAttribute("is-active", "");
else dom.removeAttribute("is-active");
if (!isEmpty(dom.dataset.controlDisplayId)) {
const displayDom = document.querySelector(
//@ts-expect-error 等待修复
`#${dom.dataset.controlDisplayId}`
);
if (active) displayDom?.removeAttribute("is-hidden");
else displayDom?.setAttribute("is-hidden", "");
}
});
});
doc.querySelectorAll(
"setting-item .q-input input.q-input__inner[data-config-key]"
).forEach((dom) => {
dom.addEventListener("input", () => {
const Type = dom.getAttribute("type");
const configKey = dom.dataset.configKey;
const configValue = Type === "number" ? parseInt(dom.value) >= 1 ? parseInt(dom.value) : 1 : dom.value;
setOB11Config(configKey, configValue);
});
});
doc.querySelectorAll("ob-setting-select[data-config-key]").forEach((dom) => {
dom?.addEventListener("selected", (e) => {
const configKey = dom.dataset.configKey;
const configValue = e.detail.value;
setOB11Config(configKey, configValue);
});
});
doc.querySelector("#config-ob11-save")?.addEventListener("click", () => {
OB11ConfigWrapper.SetOB11Config(ob11Config);
alert("保存成功");
});
doc.body.childNodes.forEach((node) => {
view.appendChild(node);
});
}
export { onSettingWindowCreated };

View File

@ -1,201 +0,0 @@
body, html {
background: var(--bg_bottom_standard);
color: var(--text_primary);
font-family: "Color Emoji", system-ui, "PingFang SC", PingFangSC-Regular, "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", Arial, Helvetica, sans-serif, "Apple Braille", "Color Emoji Fix";
min-height: 100vh;
scroll-behavior: smooth;
width: 100%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
--z_index_popover: 999;
--nt_mix_tokens: nt_brand_standard_2_overlay_hover_brand_2_mix,nt_brand_standard_2_overlay_pressed_brand_2_mix,nt_feedback_error_2_overlay_hover_brand_2_mix,nt_feedback_error_2_overlay_pressed_brand_2_mix,nt_icon_white_2_overlay_hover_2_mix,nt_icon_white_2_overlay_pressed_2_mix,nt_bubble_host_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_hover_2_mix,nt_bg_white_2_overlay_pressed_2_mix,nt_fg_white_2_overlay_hover_2_mix,nt_fg_white_2_overlay_pressed_2_mix,nt_icon_red_2_overlay_hover_2_mix,nt_icon_red_2_overlay_pressed_2_mix,nt_fg_grey_standard_2_overlay_hover_2_mix,nt_fg_grey_standard_2_overlay_pressed_2_mix,nt_bubble_guest_2_overlay_pressed_2_mix,nt_icon_primary_2_20_2_alpha,nt_bg_grey_standard_2_95_2_alpha,nt_tag_red_2_20_2_alpha,nt_tag_red_2_25_2_alpha,nt_tag_blue_2_20_2_alpha,nt_tag_blue_2_25_2_alpha,nt_tag_blue_2_10_2_alpha,nt_tag_purple_2_20_2_alpha,nt_brand_standard_2_20_2_alpha,nt_tag_sage_green_2_20_2_alpha,nt_feedback_error_2_20_2_alpha,nt_text_white_2_60_2_alpha,nt_bg_white_2_70_2_alpha,nt_bg_white_2_90_2_alpha,nt_bg_white_2_97_2_alpha,nt_bg_white_2_40_2_alpha,nt_bg_white_2_30_2_alpha,nt_text_white_2_80_2_alpha,nt_brand_standard_2_50_2_alpha,nt_bg_nav_secondary_2_60_2_alpha,nt_bg_nav_2_60_2_alpha,nt_feedback_error_2_10_2_alpha,nt_brand_standard_2_10_2_alpha,nt_on_brand_primary_2_40_2_alpha,nt_text_primary_2_72_2_alpha,nt_text_white_2_72_2_alpha;
}
a, address, article, aside, b, blockquote, body, div, em, fieldset, footer, form, h1, h2, h3, h4, h5, h6, header, html, i, iframe, img, label, legend, li, main, nav, ol, p, s, section, span, table, tbody, td, tfoot, th, thead, tr, ul {
box-sizing: border-box;
font-size: 100%;
font-style: inherit;
font-weight: inherit;
border: 0px;
margin: 0px;
padding: 0px;
}
#app {
position: relative;
display: block;
padding: 20px;
min-height: 100vh;
background: transparent;
}
.fake-bar {
position: fixed;
display: none;
top: 0;
left: 0;
}
/* ======== Input ======== */
.q-input {
align-items: center;
border-radius: 4px;
box-sizing: border-box;
color: var(--text_secondary);
display: inline-flex;
position: relative;
width: 100%;
border: 1px solid transparent;
}
.q-input input, .q-input textarea {
appearance: none;
background-color: transparent;
box-sizing: border-box;
color: var(--text_primary);
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
font-size: 14px;
height: fit-content;
outline-color: initial;
outline-style: none;
outline-width: initial;
resize: none;
width: 100%;
border: none;
}
/* ======== Switch ======== */
.q-switch {
background-color: var(--fill_standard_primary);
border-radius: 14px;
box-sizing: border-box;
display: inline-flex;
position: relative;
transition-behavior: normal;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(0.38, 0, 0.24, 1);
transition-delay: 0s;
transition-property: all;
width: 28px;
padding: 3px;
}
.q-switch__handle {
border-radius: 5px;
box-shadow: rgba(0, 0, 0, 0.09) 0px 2px 4px;
box-sizing: border-box;
display: inline-block;
height: 10px;
position: relative;
transition-behavior: normal;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(0.38, 0, 0.24, 1);
transition-delay: 0s;
transition-property: all;
width: 10px;
z-index: 2;
background: var(--icon_white);
}
.q-switch:not(.is-disabled):hover {
background: var(--fill_standard_secondary);
}
.q-switch:not(.is-disabled):active {
background: var(--nt_bg_white_2_overlay_pressed_brand_2_mix);
}
.q-switch:not(.is-disabled):active .q-switch__handle {
width: 12px;
}
.q-switch.is-active {
background-color: var(--brand_standard);
}
.q-switch.is-active .q-switch__handle {
transform: translateX(12px);
}
.q-switch.is-active:not(.is-disabled):hover {
background: var(--nt_brand_standard_2_overlay_hover_brand_2_mix);
}
.q-switch.is-active:not(.is-disabled):active {
background: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix);
}
.q-switch.is-active:not(.is-disabled):active .q-switch__handle {
transform: translateX(10px);
}
/* ======== Button ======== */
.q-button {
align-items: center;
background-color: var(--brand_standard);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
display: inline-flex;
font-size: 14px;
justify-content: center;
line-height: 18px;
outline-color: initial;
outline-style: none;
outline-width: initial;
position: relative;
vertical-align: text-bottom;
border: 1px solid var(--fg_grey_standard);
padding: 5px 11px;
}
.q-button--small {
font-size: 12px;
line-height: 14px;
min-width: 62px;
padding: 4px 7px;
}
.q-button--primary {
background-color: var(--brand_standard);
border-color: var(--brand_standard);
color: var(--on_brand_primary);
}
.q-button--secondary {
background-color: transparent;
border-color: var(--fg_grey_standard);
color: var(--text_primary);
}
.q-button:not([disabled]):hover {
background-color: var(--overlay_hover);
}
.q-button:not([disabled]):active {
background-color: var(--overlay_pressed);
}
.q-button--primary:hover {
background-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix);
border-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix);
}
.q-button--primary:active {
background-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix);
border-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix);
}
.q-button[disabled] {
opacity: 0.3;
cursor: not-allowed;
}
.q-button--secondary[disabled] {
background-color: transparent;
}

View File

@ -1,350 +0,0 @@
*[is-disabled] {
opacity: 0.3;
-webkit-pointer-events: none;
-moz-pointer-events: none;
-ms-pointer-events: none;
-o-pointer-events: none;
pointer-events: none;
cursor: not-allowed;
}
setting-section::before {
content: attr(data-title);
display: block;
margin: 0px 0px 8px 16px;
color: var(--text_primary);
font-weight: var(--font-bold);
font-size: min(var(--font_size_3), 18px);
line-height: min(var(--line_height_3), 24px);
}
setting-panel {
display: block;
margin-bottom: 20px;
}
setting-section:last-child setting-panel {
margin-bottom: 0;
}
setting-list,
setting-list[data-direction="column"] {
display: flex;
background-color: var(--fill_light_primary);
border-radius: 8px;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
}
setting-list[data-direction="row"] {
padding: 16px 0;
flex-direction: row;
justify-content: space-around;
}
setting-list[data-direction="row"],
setting-list[data-direction="row"] * {
text-align: center;
}
setting-item {
display: flex;
padding: 12px 16px;
background-color: var(--fill_light_primary);
font-size: min(var(--font_size_3),18px);
line-height: min(var(--line_height_3),24px);
border-radius: 8px;
align-items: center;
justify-content: space-between;
}
setting-item > *:first-child {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
flex-wrap: nowrap;
}
setting-list[data-direction="row"] setting-item {
padding: 0;
margin: 0 10px;
flex: 1;
}
setting-list[data-direction="row"] setting-item > *:first-child {
align-items: center;
flex: 1;
}
setting-list[data-direction="row"] setting-item > *:first-child > * {
flex: 1;
}
setting-list setting-divider,
setting-list[data-direction="column"] setting-divider {
display: block;
position: relative;
width: unset;
height: 1px;
margin: 0 16px;
background-color: var(--border_standard);
}
setting-list[data-direction="row"] setting-divider {
width: 1px;
height: unset;
margin: 0;
}
setting-text[data-type="secondary"] {
margin-top: 4px;
color: var(--text_secondary);
font-size: min(var(--font_size_2),16px);
line-height: min(var(--line_height_2),22px);
}
setting-switch {
--transition-timing: cubic-bezier(0.38, 0, 0.24, 1);
display: block;
position: relative;
width: 28px;
height: 16px;
background: var(--fill_standard_primary);
border-radius: 14px;
transition: background var(--transition-timing) .2s;
}
setting-switch::after {
content: '';
display: block;
position: absolute;
top: 0px;
left: 0px;
margin: 3px;
width: 10px;
height: 10px;
background: var(--icon_white);
box-shadow: rgba(0, 0, 0, 0.09) 0px 2px 4px;
border-radius: 5px;
transition: width var(--transition-timing) .2s,
left var(--transition-timing) .2s;
}
setting-switch[is-active] {
background: var(--brand_standard);
}
setting-switch[is-active]::after {
left: calc(100% - 16px);
}
setting-switch:hover {
background: var(--fill_standard_secondary);
}
setting-switch[is-active]:hover {
background: var(--nt_brand_standard_2_overlay_hover_brand_2_mix);
}
setting-switch:active {
background: var(--nt_bg_white_2_overlay_pressed_brand_2_mix);
}
setting-switch[is-active]:active {
background: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix);
}
setting-switch:active::after {
width: 12px;
}
setting-switch[is-active]:active::after {
left: calc(100% - 18px);
}
setting-button,
setting-button[data-type="secondary"] {
position: relative;
display: inline-flex;
padding: 5px 11px;
min-width: 62px;
background-color: transparent;
color: var(--text_primary);
border-radius: 4px;
font-size: 12px;
line-height: 12px;
justify-content: center;
outline-color: initial;
outline-style: none;
outline-width: initial;
vertical-align: text-bottom;
border: 1px solid var(--fg_grey_standard);
align-items: center;
box-sizing: border-box;
}
setting-button[data-type="primary"] {
background-color: var(--brand_standard);
color: var(--on_brand_primary);
border-color: var(--brand_standard);
}
setting-button:hover,
setting-button[data-type="secondary"]:hover {
background-color: var(--overlay_hover);
}
setting-button:active,
setting-button[data-type="secondary"]:active {
background-color: var(--overlay_pressed);
}
setting-button[data-type="primary"]:hover {
background-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix);
border-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix);
}
setting-button[data-type="primary"]:active {
background-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix);
border-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix);
}
setting-select,
setting-select::part(parent),
setting-select::part(button) {
display: block;
position: relative;
height: 24px;
font-size: 12px;
line-height: 24px;
box-sizing: border-box;
}
setting-select::part(button) {
display: flex;
padding: 0px 8px;
background-color: transparent;
border-radius: 4px;
border: 1px solid var(--border_dark);
z-index: 5;
cursor: default;
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
}
setting-select::part(current-text) {
display: block;
margin-right: 8px;
padding: 0px;
background: none;
background-color: transparent;
font-size: 12px;
color: var(--text_primary);
text-overflow: ellipsis;
border-radius: 0px;
border: none;
outline: none;
overflow: hidden;
appearance: none;
box-sizing: border-box;
cursor: default;
flex: 1;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-pointer-events: none;
-moz-pointer-events: none;
-ms-pointer-events: none;
-o-pointer-events: none;
pointer-events: none;
}
setting-select::part(button-arrow) {
position: relative;
display: block;
width: 16px;
height: 16px;
color: var(--icon_primary);
}
setting-select::part(option-list) {
display: flex;
position: absolute;
top: 100%;
padding: 4px;
margin: 5px 0px;
width: 100%;
max-height: var(--q-contextmenu-max-height);
background-color: var(--blur_middle_standard);
background-clip: padding-box;
backdrop-filter: blur(8px);
font-size: 12px;
box-shadow: var(--shadow_bg_middle_secondary);
border: 1px solid var(--border_secondary);
border-radius: 4px;
box-sizing: border-box;
app-region: no-drag;
overflow-x: hidden;
overflow-y: auto;
list-style: none;
z-index: 999;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
justify-content: flex-start;
gap: 4px;
}
setting-option,
setting-option::part(parent) {
display: block;
position: relative;
box-sizing: border-box;
}
setting-option::part(parent) {
display: flex;
padding: 0px 8px;
color: var(--text_primary);
font-size: 12px;
line-height: 24px;
border-radius: 4px;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-start;
}
setting-option:hover::part(parent) {
background-color: var(--overlay_hover);
}
setting-option:active::part(parent) {
background-color: var(--overlay_pressed);
}
setting-option[is-selected]::part(parent) {
background-color: var(--overlay_active);
}
setting-option::part(text) {
margin-right: 8px;
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
flex: 1;
}
setting-option::part(check-icon) {
display: none;
position: relative;
right: -4px;
width: 1em;
height: 1em;
color: var(--icon_primary);
flex-shrink: 0;
}
setting-option[is-selected]::part(check-icon) {
display: block;
}

View File

@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 源样式 -->
<link rel="stylesheet" href="./assets/NapCat.css" />
<!-- 修补样式 -->
<link rel="stylesheet" href="./assets/webcomponents.css" />
<link rel="stylesheet" href="./assets/style.css" />
<link rel="stylesheet" href="./assets/color.css" />
<style>
body > div {
padding: 12px;
@media screen and (min-width: 900px) {
width: 900px;
margin: 0 auto;
}
}
ob-setting-select {
width: 210px;
}
</style>
<!-- 脚手架 -->
<!-- 渲染脚本 -->
<script>
async function InitWebUi() {
const { onSettingWindowCreated } = await import("./assets/renderer.js");
onSettingWindowCreated(document.querySelector("body"));
}
InitWebUi();
</script>
<title>NapCat-WebUi</title>
</head>
<body>
<script>
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('q-theme-tokens-dark');
}
</script>
</body>
</html>

View File

@ -1,86 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebUi - Index</title>
</head>
<body>
<script>
async function CheckQQLoginStatus(retCredential) {
let QQLoginResponse = await fetch('../api/QQLogin/CheckLoginStatus', {
method: 'POST',
headers: {
'Authorization': "Bearer " + retCredential,
'Content-Type': 'application/json'
}
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
if (QQLoginResponseJson.data.isLogin) {
return true;
} else {
return false;
}
}
}
return false;
}
async function CheckWebUiLogined(retCredential) {
let LoginResponse = await fetch('../api/auth/check', {
method: 'POST',
headers: {
'Authorization': "Bearer " + retCredential,
'Content-Type': 'application/json'
}
});
if (LoginResponse.status == 200) {
let LoginResponseJson = await LoginResponse.json();
if (LoginResponseJson.code == 0) {
return true;
}
}
return false;
}
async function InitPage() {
//查找URL参数是否有token
let url = new URL(window.location.href);
let token = url.searchParams.get("token");
if (token) {
let loginResponse = await fetch('../api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: token })
});
const loginResponseJson = await loginResponse.json();
let retCode = loginResponseJson.code;
if (retCode === 0) {
//登录成功
let retCredential = loginResponseJson.data.Credential;
localStorage.setItem('auth', retCredential);
}
}
let authData = localStorage.getItem('auth');
let isLogined = await CheckWebUiLogined(authData);
if (authData && isLogined) {
let isQQLoined = await CheckQQLoginStatus(authData);
if (!isQQLoined) {
window.location.href = './QQLogin.html';
return;
}
window.location.href = './config.html';
return;
}
window.location.href = './login.html';
}
InitPage();
</script>
</body>
</html>

View File

@ -1,135 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebUi - Login</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
}
.login-container {
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
}
.input-group {
margin-bottom: 20px;
}
input[type="password"] {
width: 90%;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
width: 100%;
padding: 10px;
background-color: #007BFF;
color: white;
border: none;
cursor: pointer;
transition: all 0.3s;
}
button:hover {
background-color: #0056b3;
}
.error-message {
color: red;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="login-container">
<h2>WebUi Login</h2>
<form id="token-form" onsubmit="event.preventDefault(); submitToken();">
<div class="input-group">
<label for="token-input">Enter Token:</label>
<input type="password" id="token-input" required>
</div>
<p class="error-message hidden" id="error-message"></p>
<button type="submit">Login</button>
</form>
</div>
<script>
//待封装整理
async function submitToken() {
const tokenInput = document.getElementById('token-input');
const errorMessage = document.getElementById('error-message');
if (tokenInput.value.trim() === '') {
errorMessage.textContent = 'Please enter a token.';
errorMessage.classList.remove('hidden');
} else {
errorMessage.textContent = '';
errorMessage.classList.add('hidden');
//请求 /api/login post token
try {
let loginResponse = await fetch('../api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: tokenInput.value })
});
const loginResponseJson = await loginResponse.json();
let retCode = loginResponseJson.code;
if (retCode === 0) {
//登录成功
let retCredential = loginResponseJson.data.Credential;
localStorage.setItem('auth', retCredential);
let QQLoginResponse = await fetch('../api/QQLogin/CheckLoginStatus', {
method: 'POST',
headers: {
'Authorization': "Bearer " + retCredential,
'Content-Type': 'application/json'
}
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
//alert(QQLoginResponseJson.data.isLogin.toString());
if (QQLoginResponseJson.data.isLogin) {
window.location.href = './config.html';
} else {
window.location.href = './QQLogin.html';
}
}
}
alert("登录成功即将跳转");
} else {
console.log(loginResponseJson.message);
alert(loginResponseJson.message);
}
} catch (e) {
alert("登录失败");
console.log("请求异常", e);
}
}
}
</script>
</body>
</html>

View File

@ -3,14 +3,12 @@ import { defineConfig, PluginOption, UserConfig } from 'vite';
import { resolve } from 'path';
import nodeResolve from '@rollup/plugin-node-resolve';
import { builtinModules } from 'module';
//依赖排除
const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'fluent-ffmpeg', 'piscina'];
const nodeModules = [...builtinModules, builtinModules.map(m => `node:${m}`)].flat();
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false };
}
let startScripts: string[] | undefined = undefined;
if (process.env.NAPCAT_BUILDSYS == 'linux') {
startScripts = [];
@ -19,15 +17,13 @@ if (process.env.NAPCAT_BUILDSYS == 'linux') {
} else {
startScripts = ['./script/KillQQ.bat'];
}
const FrameworkBaseConfigPlugin: PluginOption[] = [
cp({
targets: [
{ src: './manifest.json', dest: 'dist' },
{ src: './src/core/external/napcat.json', dest: 'dist/config/' },
{ src: './src/native/packet', dest: 'dist/moehoo', flatten: false },
{ src: './static/', dest: 'dist/static/', flatten: false },
{ src: './src/onebot/config/onebot11.json', dest: 'dist/config/' },
{ src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false },
{ src: './src/framework/liteloader.cjs', dest: 'dist' },
{ src: './src/framework/napcat.cjs', dest: 'dist' },
{ src: './src/framework/preload.cjs', dest: 'dist' },
@ -42,14 +38,13 @@ const ShellBaseConfigPlugin: PluginOption[] = [
cp({
targets: [
{ src: './src/native/packet', dest: 'dist/moehoo', flatten: false },
{ src: './static/', dest: 'dist/static/', flatten: false },
{ src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false },
{ src: './src/core/external/napcat.json', dest: 'dist/config/' },
{ src: './src/onebot/config/onebot11.json', dest: 'dist/config/' },
{ src: './package.json', dest: 'dist' },
{ src: './launcher/', dest: 'dist', flatten: true },
...(startScripts.map((startScript) => {
...startScripts.map((startScript) => {
return { src: startScript, dest: 'dist' };
})),
}),
],
}),
nodeResolve(),