Compare commits

...

37 Commits

Author SHA1 Message Date
手瓜一十雪
bf0c12f1c4 fix: #605 2024-12-06 10:39:49 +08:00
Mlikiowa
cb5eeecb86 release: v4.2.25 2024-12-05 13:13:34 +00:00
手瓜一十雪
8d857cf2be Revert "refactor: CardChangedEvent"
This reverts commit 0e8ceeb6c9.
2024-12-05 21:13:03 +08:00
手瓜一十雪
6f232c465f Merge pull request #604 from Ander-pixe/webui-new
feat:实时日志、关于and部分样式优化
2024-12-05 21:10:08 +08:00
纸凤孤凰
032d444246 Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:27:08 +08:00
纸凤孤凰
49488dd3fb Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:25:14 +08:00
手瓜一十雪
9aec3865ff fix: historyLog 2024-12-05 20:22:55 +08:00
手瓜一十雪
b6b7f2051b fix 2024-12-05 20:02:24 +08:00
手瓜一十雪
46254a699a fix 2024-12-05 19:56:36 +08:00
手瓜一十雪
7b3c287137 fix: cors 2024-12-05 19:37:56 +08:00
手瓜一十雪
1a533742a5 fix: Log 2024-12-05 19:17:07 +08:00
手瓜一十雪
2027266852 fix 2024-12-05 18:56:55 +08:00
手瓜一十雪
946d8b1a7b fix: dependencies error 2024-12-05 18:49:45 +08:00
手瓜一十雪
6d2fb5de6f Merge branch 'main' into pr/604 2024-12-05 18:40:46 +08:00
pk5ls20
91c4a002dd fix: dependencies 2024-12-05 18:15:00 +08:00
纸凤孤凰
4d8112aae5 feat:实时日志、关于and部分样式优化 2024-12-05 15:31:42 +08:00
手瓜一十雪
bb53f245cf style: lint check 2024-12-05 14:50:27 +08:00
手瓜一十雪
9f31cdbf5b fix: 移除不使用api 2024-12-05 14:46:05 +08:00
手瓜一十雪
9a33039d73 fix: 暂时移除 QunAlbum 2024-12-05 14:45:12 +08:00
手瓜一十雪
7cf3be8333 refactor: predict time 2024-12-05 14:42:45 +08:00
Nanako
82afb88e53 Update README.md 2024-12-05 14:37:10 +08:00
Mlikiowa
4aa24b5d67 release: v4.2.24 2024-12-05 06:17:46 +00:00
手瓜一十雪
57112c21a2 refactor: flag handle&onebot标准化 2024-12-05 14:17:09 +08:00
手瓜一十雪
0e8ceeb6c9 refactor: CardChangedEvent 2024-12-05 11:36:06 +08:00
手瓜一十雪
f52b8d1f04 feat: 30366 全平台通用性适配 2024-12-05 11:15:10 +08:00
手瓜一十雪
f374cc77ae chore: readme 2024-12-04 23:04:37 +08:00
手瓜一十雪
7c694e7fae chore: 跑路的规范/困难的实现 2024-12-04 23:03:46 +08:00
Mlikiowa
932ffc2673 release: v4.2.23 2024-12-04 14:18:02 +00:00
手瓜一十雪
3de5438139 fix: poke report 2024-12-04 22:16:59 +08:00
手瓜一十雪
c4b5f34271 fix: 兜底 防止进入影响速度 2024-12-04 22:02:11 +08:00
手瓜一十雪
22d3ac33a2 Refactor: 更新群组通知处理逻辑,优化数据结构和异步处理 2024-12-04 21:59:53 +08:00
Mlikiowa
2e5dd6535a release: v4.2.22 2024-12-04 13:18:58 +00:00
手瓜一十雪
eac58a2a50 fix: 9.9.17-30366 2024-12-04 21:04:24 +08:00
手瓜一十雪
e939ec0e52 fix: #597 2024-12-04 20:37:08 +08:00
Mlikiowa
5b17a14a2a release: v4.2.21 2024-12-04 11:49:26 +00:00
手瓜一十雪
8fb8c888f5 refactor: 移除未使用的uidCache和uinCache逻辑 2024-12-04 19:48:59 +08:00
Mlikiowa
4a2884509e release: v4.2.20 2024-12-04 11:46:12 +00:00
50 changed files with 1613 additions and 445 deletions

View File

@@ -36,6 +36,13 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 回家旅途
[QQ Group](https://qm.qq.com/q/I6LU87a0Yq)
## 性能设计/协议标准
NapCat 已实现90+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
由此设计带来一系列好处在开发中获取群员列表通常小于50Ms单条文本消息发送在320Ms以内在1k+的群聊流畅运行同时带来一些副作用上报数据中大量使用Magic生成字段消息Id无法持久无法上报撤回消息原始内容。
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
## 感谢他们
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<t-card class="layout">
<t-card class="layout" :bordered="false">
<div class="login-container">
<h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
@@ -11,13 +11,13 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
<t-switch 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-switch v-model="config.debug" />
</t-form-item>
</t-form>
</div>

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
@@ -11,10 +11,10 @@
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
<t-switch v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-checkbox v-model="config.enableWebsocket" />
<t-switch v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
@@ -23,7 +23,7 @@
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
<t-switch v-model="config.debug" />
</t-form-item>
</t-form>
</div>

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
@@ -11,13 +11,13 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
<t-switch 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-switch v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />

View File

@@ -2,7 +2,7 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
<t-switch v-model="config.enable" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" />
@@ -14,16 +14,16 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="上报自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
<t-switch v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="强制推送事件">
<t-checkbox v-model="config.enableForcePushEvent" />
<t-switch v-model="config.enableForcePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
<t-switch v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.2.19",
"version": "4.2.25",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -13,7 +13,8 @@
"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"
"depend": "cd dist && npm install --omit=dev",
"dev:depend": "npm i && cd napcat.webui && npm i"
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.7",
@@ -25,6 +26,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17",
"@sinclair/typebox": "^0.34.9",
"@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^22.0.1",
@@ -48,8 +50,7 @@
"vite": "^6.0.1",
"vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0",
"@sinclair/typebox": "^0.34.9"
"winston": "^3.17.0"
},
"dependencies": {
"express": "^5.0.0",
@@ -59,4 +60,4 @@
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}
}
}

View File

@@ -182,25 +182,28 @@ export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2Loca
const filePath = path.join(dir, filename);
switch (UriType) {
case FileUriType.Local:
const fileExt = path.extname(HandledUri);
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
const tempFilePath = path.join(dir, filename + fileExt);
fs.copyFileSync(HandledUri, tempFilePath);
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
case FileUriType.Local: {
const fileExt = path.extname(HandledUri);
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
const tempFilePath = path.join(dir, filename + fileExt);
fs.copyFileSync(HandledUri, tempFilePath);
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
}
case FileUriType.Remote:
const buffer = await httpDownload(HandledUri);
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
case FileUriType.Remote: {
const buffer = await httpDownload(HandledUri);
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
case FileUriType.Base64:
const base64 = HandledUri.replace(/^base64:\/\//, '');
const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
case FileUriType.Base64: {
const base64 = HandledUri.replace(/^base64:\/\//, '');
const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
}
}

View File

@@ -315,5 +315,5 @@ function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel:
return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
? rawMessageToText(recordMsgOrNull, recursiveLevel + 1)
: `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
}]`;
}]`;
}

View File

@@ -1 +1 @@
export const napCatVersion = '4.2.19';
export const napCatVersion = '4.2.25';

View File

@@ -40,7 +40,7 @@ export class NTQQFileApi {
this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys'
],
this.context.logger
this.context.logger
);
}
@@ -61,7 +61,7 @@ export class NTQQFileApi {
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
const fileMd5 = await calculateFileMD5(filePath);
let extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf('.') === -1) {
@@ -305,18 +305,18 @@ export class NTQQFileApi {
element.elementType === ElementType.FILE
) {
switch (element.elementType) {
case ElementType.PIC:
case ElementType.PIC:
element.picElement!.sourcePath = elementResults[elementIndex];
break;
case ElementType.VIDEO:
break;
case ElementType.VIDEO:
element.videoElement!.filePath = elementResults[elementIndex];
break;
case ElementType.PTT:
break;
case ElementType.PTT:
element.pttElement!.filePath = elementResults[elementIndex];
break;
case ElementType.FILE:
break;
case ElementType.FILE:
element.fileElement!.filePath = elementResults[elementIndex];
break;
break;
}
elementIndex++;
}

View File

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

View File

@@ -7,6 +7,7 @@ import {
KickMemberV2Req,
MemberExtSourceType,
NapCatCore,
GroupNotify,
} from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique';
@@ -120,7 +121,7 @@ export class NTQQGroupApi {
}
return this.groupMemberCache;
}
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString();
const memberUinOrUidStr = memberUinOrUid.toString();
@@ -288,20 +289,15 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().uploadGroupBulletinPic(groupCode, _Pskey, imageurl);
}
async handleGroupRequest(flag: string, operateType: NTGroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|');
const groupCode = flagitem[0];
const seq = flagitem[1];
const type = parseInt(flagitem[2]);
async handleGroupRequest(notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) {
return this.context.session.getGroupService().operateSysNotify(
false,
{
operateType: operateType,
targetMsg: {
seq: seq, // 通知序列号
type: type,
groupCode: groupCode,
seq: notify.seq, // 通知序列号
type: notify.type,
groupCode: notify.group.groupCode,
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
},
});

View File

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

View File

@@ -8,14 +8,10 @@ import { LRUCache } from '@/common/lru-cache';
export class NTQQUserApi {
context: InstanceContext;
core: NapCatCore;
private uidCache: LRUCache<string, string>;
private uinCache: LRUCache<string, string>;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.uidCache = new LRUCache(1000);
this.uinCache = new LRUCache(1000);
}
async getCoreAndBaseInfo(uids: string[]) {
@@ -174,13 +170,13 @@ export class NTQQUserApi {
}
async getUidByUinV2(Uin: string) {
if (this.uidCache.get(Uin)) {
return this.uidCache.get(Uin);
if (!Uin) {
return '';
}
const services = [
() => this.context.session.getUixConvertService().getUid([Uin]).then((data) => data.uidInfo.get(Uin)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined),
(this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined),
() => this.context.session.getGroupService().getUidByUins([Uin]).then((data) => data.uids.get(Uin)).catch(() => undefined),
() => this.getUserDetailInfoByUin(Uin).then((data) => data.detail.uid).catch(() => undefined),
];
@@ -188,22 +184,21 @@ export class NTQQUserApi {
for (const service of services) {
uid = await service();
if (uid && uid.indexOf('*') == -1 && uid !== '') {
this.uidCache.put(Uin, uid);
break;
}
}
return uid;
return uid ?? '';
}
async getUinByUidV2(Uid: string) {
if (this.uinCache.get(Uid)) {
return this.uinCache.get(Uid);
if (!Uid) {
return '0';
}
const services = [
() => this.context.session.getUixConvertService().getUin([Uid]).then((data) => data.uinInfo.get(Uid)).catch(() => undefined),
() => this.context.session.getGroupService().getUinByUids([Uid]).then((data) => data.uins.get(Uid)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined),
(this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined),
() => this.core.apis.FriendApi.getBuddyIdMap(true).then((data) => data.getKey(Uid)).catch(() => undefined),
() => this.getUserDetailInfo(Uid).then((data) => data.uin).catch(() => undefined),
];
@@ -211,11 +206,10 @@ export class NTQQUserApi {
for (const service of services) {
uin = await service();
if (uin && uin !== '0' && uin !== '') {
this.uinCache.put(Uid, uin);
break;
}
}
return uin;
return uin ?? '0';
}
async getRecentContactListSnapShot(count: number) {

View File

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

View File

@@ -98,5 +98,17 @@
"6.9.61-29927": {
"appid": 537255836,
"qua": "V1_MAC_NQ_6.9.61_29927_GW_B"
},
"9.9.17-30366": {
"appid": 537258389,
"qua": "V1_WIN_NQ_9.9.17_30366_GW_B"
},
"3.2.15-30366": {
"appid": 537258413,
"qua": "V1_LNX_NQ_3.2.15_30366_GW_B"
},
"6.9.62-30366": {
"appid": 537258401,
"qua": "V1_MAC_NQ_6.9.62_30366_GW_B"
}
}
}

View File

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

View File

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

View File

@@ -54,12 +54,20 @@ export const PushMsg = {
generalFlag: ProtoField(9, ScalarType.INT32, true),
};
export const GroupChangeInfo = {
operator: ProtoField(1, () => GroupChangeOperator, true),
};
export const GroupChangeOperator = {
operatorUid: ProtoField(1, ScalarType.STRING, true),
};
export const GroupChange = {
groupUin: ProtoField(1, ScalarType.UINT32),
flag: ProtoField(2, ScalarType.UINT32),
memberUid: ProtoField(3, ScalarType.STRING, true),
decreaseType: ProtoField(4, ScalarType.UINT32),
operatorUid: ProtoField(5, ScalarType.STRING, true),
operatorInfo: ProtoField(5, ScalarType.BYTES, true),
increaseType: ProtoField(6, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.BYTES, true),
};

View File

@@ -187,11 +187,11 @@ export interface NodeIKernelGroupService {
destroyGroup(groupCode: string): void;
getSingleScreenNotifies(doubted: boolean, start_seq: string, num: number): Promise<GeneralCallResult>;
getSingleScreenNotifies(doubt: boolean, startSeq: string, count: number): Promise<GeneralCallResult>;
clearGroupNotifies(groupCode: string): void;
getGroupNotifiesUnreadCount(unknown: boolean): Promise<GeneralCallResult>;
getGroupNotifiesUnreadCount(doubt: boolean): Promise<GeneralCallResult>;
clearGroupNotifiesUnreadCount(doubt: boolean): void;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,12 +28,12 @@ import { RequestUtil } from '@/common/request';
import fsPromise, { constants } from 'node:fs/promises';
import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
import { GroupChange, PushMsgBody } from "@/core/packet/transformer/proto";
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent';
import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin';
import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent';
import { GroupChange, GroupChangeInfo, PushMsgBody } from '@/core/packet/transformer/proto';
type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -153,6 +153,17 @@ export class OneBotMsgApi {
faceElement: async element => {
const faceIndex = element.faceIndex;
if (element.faceType == FaceType.Poke) {
return {
type: OB11MessageDataType.poke,
data: {
type: element?.pokeType?.toString() ?? '0',
id: faceIndex.toString(),
}
};
}
if (faceIndex === FaceIndex.DICE) {
return {
type: OB11MessageDataType.dice,
@@ -450,7 +461,7 @@ export class OneBotMsgApi {
},
[OB11MessageDataType.face]: async ({ data: { id } }) => {
let parsedFaceId = +id;
const parsedFaceId = +id;
// 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface;
const face: any = sysFaces.find((systemFace) => systemFace.QSid === parsedFaceId.toString());
@@ -880,49 +891,53 @@ export class OneBotMsgApi {
if (!sendElements.length) {
throw new Error('消息体无法解析, 请检查是否发送了不支持的消息类型');
}
let totalSize = 0;
let timeout = 10000;
try {
for (const fileElement of sendElements) {
if (fileElement.elementType === ElementType.PTT) {
totalSize += (await fsPromise.stat(fileElement.pttElement.filePath)).size;
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => {
const sizePromises = elements.map(async element => {
switch (element.elementType) {
case ElementType.PTT:
return (await fsPromise.stat(element.pttElement.filePath)).size;
case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size;
default:
return 0;
}
if (fileElement.elementType === ElementType.FILE) {
totalSize += (await fsPromise.stat(fileElement.fileElement.filePath)).size;
}
if (fileElement.elementType === ElementType.VIDEO) {
totalSize += (await fsPromise.stat(fileElement.videoElement.filePath)).size;
}
if (fileElement.elementType === ElementType.PIC) {
totalSize += (await fsPromise.stat(fileElement.picElement.sourcePath)).size;
}
}
//且 PredictTime ((totalSize / 1024 / 512) * 1000)不等于Nan
const PredictTime = totalSize / 1024 / 256 * 1000;
if (!Number.isNaN(PredictTime)) {
timeout += PredictTime;// 10S Basic Timeout + PredictTime( For File 512kb/s )
}
} catch (e) {
});
const sizes = await Promise.all(sizePromises);
return sizes.reduce((total, size) => total + size, 0);
};
const totalSize = await calculateTotalSize(sendElements).catch(e => {
this.core.context.logger.logError('发送消息计算预计时间异常', e);
}
return 0;
});
const timeout = 10000 + (totalSize / 1024 / 256 * 1000);
const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
if (!returnMsg) throw new Error('发送消息失败');
returnMsg.id = MessageUnique.createUniqueMsgId({
chatType: peer.chatType,
guildId: '',
peerUid: peer.peerUid,
}, returnMsg.msgId);
setTimeout(() => {
deleteAfterSentFiles.forEach(async file => {
setTimeout(async () => {
const deletePromises = deleteAfterSentFiles.map(async file => {
try {
if (await fsPromise.access(file, constants.W_OK).then(() => true).catch(() => false)) {
fsPromise.unlink(file).then().catch(e => this.core.context.logger.logError('发送消息删除文件失败', e));
await fsPromise.unlink(file);
}
} catch (error) {
this.core.context.logger.logError('发送消息删除文件失败', (error as Error).message);
} catch (e) {
this.core.context.logger.logError('发送消息删除文件失败', e);
}
});
await Promise.all(deletePromises);
}, 60000);
return returnMsg;
@@ -971,15 +986,20 @@ export class OneBotMsgApi {
if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
const operatorUid = groupChange.operatorInfo?.toString();
return new OB11GroupIncreaseEvent(
this.core,
groupChange.groupUin,
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0,
groupChange.decreaseType == 131 ? 'invite' : 'approve',
);
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
const operatorUid = groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString();
if (groupChange.memberUid === this.core.selfInfo.uid) {
setTimeout(() => {
this.core.apis.GroupApi.groupMemberCache.delete(groupChange.groupUin.toString());
@@ -992,7 +1012,7 @@ export class OneBotMsgApi {
this.core,
groupChange.groupUin,
groupChange.memberUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.memberUid) : 0,
groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0,
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0,
this.groupChangDecreseType2String(groupChange.decreaseType),
);
} else if (SysMessage.contentHead.type == 44 && SysMessage.body?.msgContent) {

View File

@@ -333,7 +333,7 @@ export class NapCatOneBot11Adapter {
this.core,
+requesterUin,
req.extWords,
req.friendUid + '|' + req.reqTime
req.reqTime
)
);
} catch (e) {
@@ -365,8 +365,7 @@ export class NapCatOneBot11Adapter {
if (notifyTime < this.bootTime) {
continue;
}
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type;
const flag = notify.seq;
this.context.logger.logDebug('收到群通知', notify);
if (
[GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) &&
@@ -405,8 +404,8 @@ export class NapCatOneBot11Adapter {
this.context.logger.logDebug(`收到邀请我加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid)),
+notify.group.groupCode,
+await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid),
'invite',
notify.postscript,
flag
@@ -423,8 +422,8 @@ export class NapCatOneBot11Adapter {
this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid)),
+notify.group.groupCode,
+await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid),
'add',
notify.postscript,
flag
@@ -522,21 +521,27 @@ export class NapCatOneBot11Adapter {
// 群名片修改事件解析 任何都该判断
if (message.senderUin && message.senderUin !== '0') {
const cardChangedEvent = await this.apis.GroupApi.parseCardChangedEvent(message);
cardChangedEvent && await this.networkManager.emitEvent(cardChangedEvent);
if (cardChangedEvent) {
await this.networkManager.emitEvent(cardChangedEvent);
}
}
if (message.msgType === NTMsgType.KMSGTYPEFILE) {
// 文件为单元素消息
const elementWrapper = message.elements.find(e => !!e.fileElement);
if (elementWrapper?.fileElement) {
const uploadGroupFileEvent = await this.apis.GroupApi.parseGroupUploadFileEvene(message, elementWrapper.fileElement, elementWrapper);
uploadGroupFileEvent && await this.networkManager.emitEvent(uploadGroupFileEvent);
if (uploadGroupFileEvent) {
await this.networkManager.emitEvent(uploadGroupFileEvent);
}
}
} else if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) {
// 灰条为单元素消息
const grayTipElement = message.elements[0].grayTipElement;
if (grayTipElement) {
const event = await this.apis.GroupApi.parseGrayTipElement(message, grayTipElement);
event && await this.networkManager.emitEvent(event);
if (event) {
await this.networkManager.emitEvent(event);
}
}
}
} catch (e) {
@@ -551,7 +556,10 @@ export class NapCatOneBot11Adapter {
const grayTipElement = message.elements[0].grayTipElement;
if (grayTipElement) {
const event = await this.apis.MsgApi.parsePrivateMsgEvent(message, grayTipElement);
event && await this.networkManager.emitEvent(event);
if (event) {
await this.networkManager.emitEvent(event);
}
}
}
} catch (e) {
@@ -571,6 +579,8 @@ export class NapCatOneBot11Adapter {
}
private async emitFriendRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid;
if (!operatorUid) return undefined;
return new OB11FriendRecallNoticeEvent(
this.core,
+message.senderUin,
@@ -581,7 +591,7 @@ export class NapCatOneBot11Adapter {
private async emitGroupRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid;
if (!operatorUid) return undefined;
const operatorId = message.senderUin ?? await this.core.apis.UserApi.getUinByUidV2(operatorUid);
const operatorId = await this.core.apis.UserApi.getUinByUidV2(operatorUid);
return new OB11GroupRecallNoticeEvent(
this.core,
+message.peerUin,

View File

@@ -11,6 +11,17 @@ export interface OB11User {
categoryName?: string; // 分组名称
categoryId?: number; // 分组ID 999为特别关心
}
export interface Notify {
request_id: number;
invitor_uin: number;
invitor_nick?: string;
group_id?: number;
group_name?: string;
message?: string;
checked: boolean;
actor: number;
requester_nick?: string;
}
export enum OB11UserSex {
male = 'male', // 男性

View File

@@ -71,6 +71,14 @@ export enum OB11MessageDataType {
location = 'location'
}
export interface OB11MessagePoke {
type: OB11MessageDataType.poke;
data: {
type: string;
id: string;
};
}
// 商城表情消息接口定义
export interface OB11MessageMFace {
type: OB11MessageDataType.mface;
@@ -247,7 +255,7 @@ export type OB11MessageData =
OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
OB11MessageNode | OB11MessageIdMusic | OB11MessageCustomMusic | OB11MessageJson |
OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContext;
OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContext | OB11MessagePoke;
// 发送消息接口定义
export interface OB11PostSendMsg {

View File

@@ -18,13 +18,16 @@ export const LogListHandler: RequestHandler = async (_, res) => {
const logList = await WebUiConfigWrapper.GetLogsList();
return sendSuccess(res, logList);
};
// 实时日志SSE
export const LogRealTimeHandler: RequestHandler = async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Connection', 'keep-alive');
const listener = (log: string) => {
try {
res.write(log + '\n');
res.write(`data: ${log}\n\n`);
} catch (error) {
// ignore
console.error('向客户端写入日志数据时出错:', error);
}
};
logSubscription.subscribe(listener);

View File

@@ -1,9 +1,16 @@
import type { RequestHandler } from 'express';
// CORS 中间件,跨域用
export const cors: RequestHandler = (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
export const cors: RequestHandler = (req, res, next) => {
const origin = req.headers.origin || '*';
res.header('Access-Control-Allow-Origin', 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');
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
res.sendStatus(204);
return;
}
next();
};
};