Compare commits

..

20 Commits

Author SHA1 Message Date
手瓜一十雪
6c46cdd947 fix: error 2024-11-17 11:33:01 +08:00
手瓜一十雪
372452fbee fix: 消息上报 2024-11-17 11:29:27 +08:00
手瓜一十雪
417ef5d335 Revert "fix"
This reverts commit 9c534f8afd.
2024-11-17 11:21:48 +08:00
手瓜一十雪
9c534f8afd fix 2024-11-17 11:12:14 +08:00
pk5ls20
ecd426bb80 refactor: webui network 2024-11-17 08:17:09 +08:00
pk5ls20
f74ef273de fix: workflow
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 3m25s
Build Action / Build-Shell (push) Failing after 2m52s
2024-11-17 06:24:58 +08:00
pk5ls20
f913e0b027 chore: workflow 2024-11-17 06:23:33 +08:00
pk5ls20
f7268c30ca chore: revert todo 2024-11-17 05:28:46 +08:00
pk5ls20
0f5ef03d63 chore: try todo x2 2024-11-17 05:21:35 +08:00
pk5ls20
745276d0f0 chore: try todo 2024-11-17 05:16:18 +08:00
pk5ls20
2e108a4bd6 feat: error stack 2024-11-17 04:43:29 +08:00
pk5ls20
666da80ef5 feat: version display 2024-11-17 03:43:09 +08:00
pk5ls20
cc73104d62 chore: eslint 2024-11-17 03:35:20 +08:00
手瓜一十雪
3c10b82bab Merge branch 'main' of https://github.com/NapNeko/NapCatQQ
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 11s
Build Action / Build-Shell (push) Failing after 14s
2024-11-16 20:35:31 +08:00
手瓜一十雪
9a65dae6a2 fix: #531 2024-11-16 20:32:52 +08:00
Mlikiowa
f26cd8cdc9 release: v4.1.3 2024-11-16 12:22:06 +00:00
手瓜一十雪
eeec905df0 fix: 反向ws 2024-11-16 20:21:38 +08:00
手瓜一十雪
0c6aac7f66 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-16 20:20:07 +08:00
手瓜一十雪
86d22db141 feat: remove hasBeenClosed 2024-11-16 20:15:02 +08:00
Mlikiowa
48a5d0eef3 release: v4.1.2 2024-11-16 12:14:28 +00:00
38 changed files with 323 additions and 598 deletions

View File

@@ -1,8 +1,7 @@
name: "Build Action" name: "Build Action"
on: on:
push: push:
branches: pull_request:
- main
workflow_dispatch: workflow_dispatch:
permissions: write-all permissions: write-all
@@ -11,60 +10,38 @@ jobs:
Build-LiteLoader: Build-LiteLoader:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone Main Repository - name: Clone Main Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: - name: Use Node.js 20.X
repository: 'NapNeko/NapCatQQ' uses: actions/setup-node@v4
submodules: true with:
ref: main node-version: 20.x
token: ${{ secrets.NAPCAT_BUILD }} - name: Build NapCat.Framework
- name: Use Node.js 20.X run: |
uses: actions/setup-node@v4 npm i && cd napcat.webui && npm i && cd ..
with: npm run build:framework && npm run depend
node-version: 20.x
- name: Build NuCat Framework
run: |
npm i
cd napcat.webui
npm i
cd ..
npm run build:framework
cd dist
npm i --omit=dev
rm package-lock.json rm package-lock.json
cd .. - name: Upload Artifact
- name: Upload Artifact uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v4 with:
with: name: NapCat.Framework
name: NapCat.Framework path: dist
path: dist
Build-Shell: Build-Shell:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone Main Repository - name: Clone Main Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: - name: Use Node.js 20.X
repository: 'NapNeko/NapCatQQ' uses: actions/setup-node@v4
submodules: true with:
ref: main node-version: 20.x
token: ${{ secrets.NAPCAT_BUILD }} - name: Build NapCat.Shell
- name: Use Node.js 20.X run: |
uses: actions/setup-node@v4 npm i && cd napcat.webui && npm i && cd ..
with: npm run build:shell && npm run depend
node-version: 20.x rm package-lock.json
- name: Build NuCat LiteLoader - name: Upload Artifact
run: | uses: actions/upload-artifact@v4
npm i with:
cd napcat.webui name: NapCat.Shell
npm i path: dist
cd ..
npm run build:shell
cd dist
npm i --omit=dev
rm package-lock.json
cd ..
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell
path: dist

View File

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

View File

@@ -1,10 +1,10 @@
{ {
"name": "napcat.webui", "name": "napcat.webui",
"private": true, "private": true,
"version": "0.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"webui:lint": "eslint . --fix", "webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
"webui:dev": "vite", "webui:dev": "vite",
"webui:build": "vue-tsc -b && vite build", "webui:build": "vue-tsc -b && vite build",
"webui:preview": "vite preview" "webui:preview": "vite preview"

View File

@@ -13,9 +13,11 @@
<t-list-item class="list-item"> <t-list-item class="list-item">
<span class="item-label">版本信息:</span> <span class="item-label">版本信息:</span>
<span class="item-content"> <span class="item-content">
<t-tag class="tag-item" theme="success"> WebUi: 1.0.0 </t-tag> <t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag>
<t-tag class="tag-item" theme="success"> NapCat: 4.?.? </t-tag> <t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
<t-tag class="tag-item" theme="success"> Tdesign: 1.10.3 </t-tag> <t-tag class="tag-item" theme="success">
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
</t-tag>
</span> </span>
</t-list-item> </t-list-item>
</t-list> </t-list>
@@ -24,7 +26,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import pkg from '../../package.json';
import { napCatVersion } from '../../../src/common/version';
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,52 +1,59 @@
<template> <template>
<t-space class="full-space"> <t-space class="full-space">
<template v-if="clientPanelData.length > 0"> <template v-if="clientPanelData.length > 0">
<t-tabs v-model="activeTab" :addable="true" theme="card" @add="showAddTabDialog" @remove="removeTab" class="full-tabs"> <t-tabs
<t-tab-panel v-model="activeTab"
v-for="(config, idx) in clientPanelData" :addable="true"
:key="idx" theme="card"
:label="config.name" @add="showAddTabDialog"
:removable="true" @remove="removeTab"
:value="idx" class="full-tabs"
class="full-tab-panel" >
> <t-tab-panel
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" /> v-for="(config, idx) in clientPanelData"
<div class="button-container"> :key="idx"
<t-button @click="saveConfig" style="width: 100px; height: 40px;">保存</t-button> :label="config.name"
</div> :removable="true"
</t-tab-panel> :value="idx"
</t-tabs> class="full-tab-panel"
</template> >
<template v-else> <component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
<EmptyStateComponent :showAddTabDialog="showAddTabDialog" /> <div class="button-container">
</template> <t-button @click="saveConfig" style="width: 100px; height: 40px">保存</t-button>
<t-dialog </div>
v-model:visible="isDialogVisible" </t-tab-panel>
header="添加网络配置" </t-tabs>
@close="isDialogVisible = false" </template>
@confirm="addTab" <template v-else>
> <EmptyStateComponent :showAddTabDialog="showAddTabDialog" />
<t-form ref="form" :model="newTab"> </template>
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name"> <t-dialog
<t-input v-model="newTab.name" /> v-model:visible="isDialogVisible"
</t-form-item> header="添加网络配置"
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type"> @close="isDialogVisible = false"
<t-select v-model="newTab.type"> @confirm="addTab"
<t-option value="httpServers">HTTP 服务器</t-option> >
<t-option value="httpClients">HTTP 客户端</t-option> <t-form ref="form" :model="newTab">
<t-option value="websocketServers">WebSocket 服务器</t-option> <t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
<t-option value="websocketClients">WebSocket 客户端</t-option> <t-input v-model="newTab.name" />
</t-select> </t-form-item>
</t-form-item> <t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
</t-form> <t-select v-model="newTab.type">
</t-dialog> <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> </t-space>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue'; import { ref, resolveDynamicComponent, nextTick, Ref, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next'; import { MessagePlugin } from 'tdesign-vue-next';
import { import {
httpServerDefaultConfigs, httpServerDefaultConfigs,
httpClientDefaultConfigs, httpClientDefaultConfigs,
websocketServerDefaultConfigs, websocketServerDefaultConfigs,
@@ -58,187 +65,185 @@
NetworkConfig, NetworkConfig,
OneBotConfig, OneBotConfig,
mergeOneBotConfigs, mergeOneBotConfigs,
} from '../../../src/onebot/config/config'; } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell'; import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue'; import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue'; import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue'; import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue'; import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
import EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue'; import EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients'; type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig; type ComponentUnion =
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
const componentMap: Record<
ConfigKey,
| typeof HttpServerComponent | typeof HttpServerComponent
| typeof HttpClientComponent | typeof HttpClientComponent
| typeof WebsocketServerComponent | typeof WebsocketServerComponent
| typeof WebsocketClientComponent | typeof WebsocketClientComponent;
> = {
const componentMap: Record<ConfigKey, ComponentUnion> = {
httpServers: HttpServerComponent, httpServers: HttpServerComponent,
httpClients: HttpClientComponent, httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent, websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent, websocketClients: WebsocketClientComponent,
}; };
interface ClientPanel { const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
interface ConfigMap {
httpServers: HttpServerConfig;
httpClients: HttpClientConfig;
websocketServers: WebsocketServerConfig;
websocketClients: WebsocketClientConfig;
}
interface ClientPanel<K extends ConfigKey = ConfigKey> {
name: string; name: string;
key: ConfigKey; key: K;
data: Ref<ConfigUnion>; data: ConfigMap[K];
} }
type ComponentKey = keyof typeof componentMap; const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
// TODO: store these state in global store (aka pinia) const newTab = ref<{ name: string; type: ConfigKey }>({ name: '', type: 'httpServers' });
const activeTab = ref<number>(0); const clientPanelData: Ref<ClientPanel[]> = ref([]);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' }); const getComponent = (type: ConfigKey) => {
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
const getComponent = (type: ComponentKey) => {
return componentMap[type]; return componentMap[type];
}; };
const getOB11Config = async (): Promise<OneBotConfig | undefined> => { const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth'); const storedCredential = localStorage.getItem('auth');
if (!storedCredential) { if (!storedCredential) {
console.error('No stored credential found'); console.error('No stored credential found');
return; return;
} }
const loginManager = new QQLoginManager(storedCredential); const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config(); return await loginManager.GetOB11Config();
}; };
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => { const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth'); const storedCredential = localStorage.getItem('auth');
if (!storedCredential) { if (!storedCredential) {
console.error('No stored credential found'); console.error('No stored credential found');
return false; return false;
} }
const loginManager = new QQLoginManager(storedCredential); const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config); return await loginManager.SetOB11Config(config);
}; };
const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => { const addToPanel = <K extends ConfigKey>(configs: ConfigMap[K][], key: K) => {
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key })); configs.forEach((config) => clientPanelData.value.push({ name: config.name, data: config, key }));
}; };
const addConfigDataToPanel = (data: NetworkConfig) => { const addConfigDataToPanel = (data: NetworkConfig) => {
Object.entries(data).forEach(([key, configs]) => { (Object.keys(data) as ConfigKey[]).forEach((key) => {
if (key in defaultConfigs) { addToPanel(data[key], key);
addToPanel(configs as ConfigUnion[], key as ConfigKey);
}
}); });
}; };
const parsePanelData = (): NetworkConfig => { const parsePanelData = (): NetworkConfig => {
return { const result: NetworkConfig = {
websocketClients: clientPanelData httpServers: [],
.filter((panel) => panel.key === 'websocketClients') httpClients: [],
.map((panel) => panel.data as WebsocketClientConfig), websocketServers: [],
websocketServers: clientPanelData websocketClients: [],
.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),
}; };
}; clientPanelData.value.forEach((panel) => {
(result[panel.key] as Array<typeof panel.data>).push(panel.data);
const loadConfig = async () => { });
return result;
};
const loadConfig = async () => {
try { try {
const userConfig = await getOB11Config(); const userConfig = await getOB11Config();
if (!userConfig) return; if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig); const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network); addConfigDataToPanel(mergedConfig.network);
} catch (error) { } catch (error) {
console.error('Error loading config:', error); console.error('Error loading config:', error);
} }
}; };
// It's better to "saveConfig" instead of using deep watch const saveConfig = async () => {
const saveConfig = async () => {
const config = parsePanelData(); const config = parsePanelData();
const userConfig = await getOB11Config(); const userConfig = await getOB11Config();
if (!userConfig) return; if (!userConfig) {
await MessagePlugin.error('无法获取配置!');
return;
}
userConfig.network = config; userConfig.network = config;
const success = await setOB11Config(userConfig); const success = await setOB11Config(userConfig);
if (success) { if (success) {
MessagePlugin.success('配置保存成功'); await MessagePlugin.success('配置保存成功');
} else { } else {
MessagePlugin.error('配置保存失败'); await MessagePlugin.error('配置保存失败');
} }
}; };
const showAddTabDialog = () => { const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' }; newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true; isDialogVisible.value = true;
}; };
const addTab = async () => { const addTab = async () => {
const { name, type } = newTab.value; const { name, type } = newTab.value;
if (clientPanelData.some(panel => panel.name === name)) { if (clientPanelData.value.some((panel) => panel.name === name)) {
MessagePlugin.error('选项卡名称已存在'); await MessagePlugin.error('选项卡名称已存在');
return; return;
} }
const defaultConfig = structuredClone(defaultConfigs[type]); const defaultConfig = structuredClone(defaultConfigMap[type]);
defaultConfig.name = name; defaultConfig.name = name;
clientPanelData.push({ name, data: defaultConfig, key: type }); clientPanelData.value.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false; isDialogVisible.value = false;
await nextTick(); await nextTick();
activeTab.value = clientPanelData.length - 1; activeTab.value = clientPanelData.value.length - 1;
MessagePlugin.success('选项卡添加成功'); await MessagePlugin.success('选项卡添加成功');
}; };
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => { const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.splice(payload.index, 1); clientPanelData.value.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1); activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig(); await saveConfig();
}; };
onMounted(() => { onMounted(() => {
loadConfig(); loadConfig();
}); });
</script> </script>
<style scoped> <style scoped>
.full-space { .full-space {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
} }
.full-tabs { .full-tabs {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.full-tab-panel { .full-tab-panel {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.button-container { .button-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 20px; margin-top: 20px;
} }
</style> </style>

View File

@@ -131,4 +131,4 @@ onMounted(() => {
margin-left: 20px; margin-left: 20px;
} }
} }
</style> </style>

View File

@@ -19,4 +19,4 @@ defineProps<{ showAddTabDialog: () => void }>();
height: 100%; height: 100%;
text-align: center; text-align: center;
} }
</style> </style>

View File

@@ -36,14 +36,17 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' } { label: 'String', value: 'string' },
]); ]);
watch(() => props.config.messagePostFormat, (newValue) => { watch(
if (newValue !== 'array' && newValue !== 'string') { () => props.config.messagePostFormat,
props.config.messagePostFormat = 'array'; (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
} }
}); );
</script> </script>
<style scoped> <style scoped>
@@ -62,4 +65,4 @@ watch(() => props.config.messagePostFormat, (newValue) => {
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
} }
</style> </style>

View File

@@ -42,14 +42,17 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' } { label: 'String', value: 'string' },
]); ]);
watch(() => props.config.messagePostFormat, (newValue) => { watch(
if (newValue !== 'array' && newValue !== 'string') { () => props.config.messagePostFormat,
props.config.messagePostFormat = 'array'; (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
} }
}); );
</script> </script>
<style scoped> <style scoped>
@@ -68,4 +71,4 @@ watch(() => props.config.messagePostFormat, (newValue) => {
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
} }
</style> </style>

View File

@@ -39,14 +39,17 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' } { label: 'String', value: 'string' },
]); ]);
watch(() => props.config.messagePostFormat, (newValue) => { watch(
if (newValue !== 'array' && newValue !== 'string') { () => props.config.messagePostFormat,
props.config.messagePostFormat = 'array'; (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
} }
}); );
</script> </script>
<style scoped> <style scoped>
@@ -65,4 +68,4 @@ watch(() => props.config.messagePostFormat, (newValue) => {
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
} }
</style> </style>

View File

@@ -45,14 +45,17 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' } { label: 'String', value: 'string' },
]); ]);
watch(() => props.config.messagePostFormat, (newValue) => { watch(
if (newValue !== 'array' && newValue !== 'string') { () => props.config.messagePostFormat,
props.config.messagePostFormat = 'array'; (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
} }
}); );
</script> </script>
<style scoped> <style scoped>
@@ -71,4 +74,4 @@ watch(() => props.config.messagePostFormat, (newValue) => {
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
} }
</style> </style>

View File

@@ -1,267 +0,0 @@
<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

@@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "4.1.1", "version": "4.1.3",
"scripts": { "scripts": {
"build:framework": "npm run build:webui && vite build --mode framework", "build:framework": "npm run build:webui && vite build --mode framework",
"build:shell": "npm run build:webui && vite build --mode shell", "build:shell": "npm run build:webui && vite build --mode shell",

View File

@@ -242,7 +242,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und
//解析Http和Https协议 //解析Http和Https协议
if (UriType == FileUriType.Unknown) { if (UriType == FileUriType.Unknown) {
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' }; return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
} }
//解析File协议和本地文件 //解析File协议和本地文件
if (UriType == FileUriType.Local) { if (UriType == FileUriType.Local) {
@@ -289,5 +289,5 @@ export async function uri2local(dir: string, uri: string, filename: string | und
} }
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath }; return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
} }
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' }; return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
} }

View File

@@ -36,7 +36,7 @@ export class LogWrapper {
this.logger = winston.createLogger({ this.logger = winston.createLogger({
level: 'debug', level: 'debug',
format: format.combine( format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.timestamp({ format: 'MM-DD HH:mm:ss' }),
format.printf(({ timestamp, level, message, ...meta }) => { format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : ''; const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`; return `${timestamp} [${level}] ${userInfo}${message}`;
@@ -61,7 +61,7 @@ export class LogWrapper {
] ]
}); });
this.setLogSelfInfo({ nick: '', uin: '', uid: '' }); this.setLogSelfInfo({ nick: '', uid: '' });
this.cleanOldLogs(logDir); this.cleanOldLogs(logDir);
} }
@@ -111,8 +111,8 @@ export class LogWrapper {
}); });
} }
setLogSelfInfo(selfInfo: { nick: string, uin: string, uid: string }) { setLogSelfInfo(selfInfo: { nick: string, uid: string }) {
const userInfo = `${selfInfo.nick}(${selfInfo.uin})`; const userInfo = `${selfInfo.nick}`;
this.logger.defaultMeta = { userInfo }; this.logger.defaultMeta = { userInfo };
} }

View File

@@ -1 +1 @@
export const napCatVersion = '4.1.1'; export const napCatVersion = '4.1.3';

View File

@@ -23,7 +23,7 @@ export interface ChatCacheList {
export interface ChatCacheListItem { export interface ChatCacheListItem {
chatType: ChatType; chatType: ChatType;
basicChatCacheInfo: ChatCacheListItemBasic; basicChatCacheInfo: ChatCacheListItemBasic;
guildChatCacheInfo: unknown[]; // work: 没用过频道所以不知道这里边的详细内容 guildChatCacheInfo: unknown[]; // TODO: 没用过频道所以不知道这里边的详细内容
} }
export interface ChatCacheListItemBasic { export interface ChatCacheListItemBasic {

View File

@@ -1,4 +1,4 @@
// work:further refactor in NapCat.Packet v2 // TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core"; import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const LikeDetail = { const LikeDetail = {

View File

@@ -1,4 +1,4 @@
// work:further refactor in NapCat.Packet v2 // TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core"; import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const BodyInner = { const BodyInner = {

View File

@@ -120,7 +120,7 @@ export class NapCatCore {
if (!fs.existsSync(this.NapCatTempPath)) { if (!fs.existsSync(this.NapCatTempPath)) {
fs.mkdirSync(this.NapCatTempPath, { recursive: true }); fs.mkdirSync(this.NapCatTempPath, { recursive: true });
} }
//遍历this.apis[i].initApi 如果存在该函数进行async 调用 //遍历this.apis[i].initApi 如果存在该函数进行async 调用
for (const apiKey in this.apis) { for (const apiKey in this.apis) {
const api = this.apis[apiKey as keyof StableNTApiWrapper]; const api = this.apis[apiKey as keyof StableNTApiWrapper];
if ('initApi' in api && typeof api.initApi === 'function') { if ('initApi' in api && typeof api.initApi === 'function') {
@@ -210,7 +210,7 @@ export class NapCatCore {
}); });
}; };
groupListener.onMemberListChange = (arg) => { groupListener.onMemberListChange = (arg) => {
// work:应该加一个内部自己维护的成员变动callback用于判断成员变化通知 // TODO: 应该加一个内部自己维护的成员变动callback用于判断成员变化通知
const groupCode = arg.sceneId.split('_')[0]; const groupCode = arg.sceneId.split('_')[0];
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) { if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!; const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;

View File

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

View File

@@ -1,7 +1,7 @@
import { LogLevel, LogWrapper } from "@/common/log"; import { LogLevel, LogWrapper } from "@/common/log";
import { PacketContext } from "@/core/packet/context/packetContext"; import { PacketContext } from "@/core/packet/context/packetContext";
// work: check bind? // TODO: check bind?
export class PacketLogger { export class PacketLogger {
private readonly napLogger: LogWrapper; private readonly napLogger: LogWrapper;

View File

@@ -76,7 +76,7 @@ export type rawMsgWithSendMsg = {
msg: PacketSendMsgElement[] msg: PacketSendMsgElement[]
} }
// work:make it become adapter? // TODO: make it become adapter?
export class PacketMsgConverter { export class PacketMsgConverter {
private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters { private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters {
return SupportedElementTypes.includes(type); return SupportedElementTypes.includes(type);
@@ -116,7 +116,7 @@ export class PacketMsgConverter {
[ElementType.MARKDOWN]: (element) => { [ElementType.MARKDOWN]: (element) => {
return new PacketMsgMarkDownElement(element as SendMarkdownElement); return new PacketMsgMarkDownElement(element as SendMarkdownElement);
}, },
// work:check this logic, move it in arkElement? // TODO: check this logic, move it in arkElement?
[ElementType.STRUCTLONGMSG]: (element) => { [ElementType.STRUCTLONGMSG]: (element) => {
return new PacketMultiMsgElement(element as SendStructLongMsgElement); return new PacketMultiMsgElement(element as SendStructLongMsgElement);
} }

View File

@@ -32,7 +32,7 @@ import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
// raw <-> packet // raw <-> packet
// work:SendStructLongMsgElement // TODO: SendStructLongMsgElement
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> { export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
protected constructor(rawElement: T) { protected constructor(rawElement: T) {
} }
@@ -118,7 +118,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
this.targetUin = +(element.replyElement.senderUin ?? 0); this.targetUin = +(element.replyElement.senderUin ?? 0);
this.targetUid = element.replyElement.senderUidStr ?? ''; this.targetUid = element.replyElement.senderUidStr ?? '';
this.time = +(element.replyElement.replyMsgTime ?? 0); this.time = +(element.replyElement.replyMsgTime ?? 0);
this.elems = []; // work:in replyElement.sourceMsgTextElems this.elems = []; // TODO: in replyElement.sourceMsgTextElems
} }
get isGroupReply(): boolean { get isGroupReply(): boolean {
@@ -131,7 +131,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq], origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq],
senderUin: BigInt(this.targetUin), senderUin: BigInt(this.targetUin),
time: this.time, time: this.time,
elems: [], // work:in replyElement.sourceMsgTextElems elems: [], // TODO: in replyElement.sourceMsgTextElems
pbReserve: { pbReserve: {
messageId: this.messageId, messageId: this.messageId,
}, },
@@ -346,9 +346,9 @@ export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
constructor(element: SendPttElement) { constructor(element: SendPttElement) {
super(element); super(element);
this.filePath = element.pttElement.filePath; this.filePath = element.pttElement.filePath;
this.fileSize = +element.pttElement.fileSize; // work:cc this.fileSize = +element.pttElement.fileSize; // TODO: cc
this.fileMd5 = element.pttElement.md5HexStr; this.fileMd5 = element.pttElement.md5HexStr;
this.fileDuration = Math.round(element.pttElement.duration); // work:cc this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc
} }
get valid(): boolean { get valid(): boolean {

View File

@@ -25,7 +25,7 @@ class DownloadOfflineFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0
return OidbBase.build(0xE37, 800, body, false, false); return OidbBase.build(0xE37, 800, body, false, false);
} }
// work:check // TODO:check
parse(data: Buffer) { parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body; const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37Response).decode(oidbBody); return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37Response).decode(oidbBody);

View File

@@ -16,7 +16,7 @@ class FetchSessionKey extends PacketTransformer<typeof proto.HttpConn0x6ff_501Re
field4: 1, field4: 1,
field6: 3, field6: 3,
serviceTypes: [1, 5, 10, 21], serviceTypes: [1, 5, 10, 21],
// tgt: "", // work:do we really need tgt? seems not // tgt: "", // TODO: do we really need tgt? seems not
field9: 2, field9: 2,
field10: 9, field10: 9,
field11: 8, field11: 8,

View File

@@ -16,7 +16,7 @@ class UploadGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6
appId: 4, appId: 4,
busId: 102, busId: 102,
entrance: 6, entrance: 6,
targetDirectory: '/', // work: targetDirectory: '/', // TODO:
fileName: file.fileName, fileName: file.fileName,
localDirectory: `/${file.fileName}`, localDirectory: `/${file.fileName}`,
fileSize: BigInt(file.fileSize), fileSize: BigInt(file.fileSize),

View File

@@ -40,7 +40,7 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
fileName: img.name, fileName: img.name,
type: { type: {
type: 1, type: 1,
picFormat: img.picType, //work:extend NapCat imgType /cc @MliKiowa picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0, videoFormat: 0,
voiceFormat: 0, voiceFormat: 0,
}, },
@@ -59,7 +59,7 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
extBizInfo: { extBizInfo: {
pic: { pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // work: textSummary: "Nya~", // TODO:
}, },
video: { video: {
bytesPbReserve: Buffer.alloc(0), bytesPbReserve: Buffer.alloc(0),

View File

@@ -40,7 +40,7 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
fileName: img.name, fileName: img.name,
type: { type: {
type: 1, type: 1,
picFormat: img.picType, //work:extend NapCat imgType /cc @MliKiowa picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0, videoFormat: 0,
voiceFormat: 0, voiceFormat: 0,
}, },
@@ -59,7 +59,7 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
extBizInfo: { extBizInfo: {
pic: { pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // work: textSummary: "Nya~", // TODO:
}, },
video: { video: {
bytesPbReserve: Buffer.alloc(0), bytesPbReserve: Buffer.alloc(0),

View File

@@ -47,7 +47,7 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
return OB11Response.ok(resData); return OB11Response.ok(resData);
} catch (e: any) { } catch (e: any) {
this.core.context.logger.logError.bind(this.core.context.logger)('发生错误', e); this.core.context.logger.logError.bind(this.core.context.logger)('发生错误', e);
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200); return OB11Response.error(e?.stack?.toString() || e?.toString() || '未知错误,可能操作超时', 200);
} }
} }

View File

@@ -15,7 +15,7 @@ type Payload = FromSchema<typeof SchemaData>;
export class GetGroupFileSystemInfo extends BaseAction<Payload, { export class GetGroupFileSystemInfo extends BaseAction<Payload, {
file_count: number, file_count: number,
limit_count: number, // unimplemented limit_count: number, // unimplemented
used_space: number, // work:unimplemented, but can be implemented later used_space: number, // TODO:unimplemented, but can be implemented later
total_space: number, // unimplemented, 10 GB by default total_space: number, // unimplemented, 10 GB by default
}> { }> {
actionName = ActionName.GoCQHTTP_GetGroupFileSystemInfo; actionName = ActionName.GoCQHTTP_GetGroupFileSystemInfo;

View File

@@ -122,8 +122,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
returnMsgAndResId = packetMode returnMsgAndResId = packetMode
? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[], payload.source, payload.news, payload.summary, payload.prompt) ? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[], payload.source, payload.news, payload.summary, payload.prompt)
: await this.handleForwardedNodes(peer, messages as OB11MessageNode[]); : await this.handleForwardedNodes(peer, messages as OB11MessageNode[]);
} catch (e) { } catch (e: any) {
throw Error(packetMode ? `发送伪造合并转发消息失败: ${e}` : `发送合并转发消息失败: ${e}`); throw Error(packetMode ? `发送伪造合并转发消息失败: ${e?.stack}` : `发送合并转发消息失败: ${e?.stack}`);
} }
if (!returnMsgAndResId) { if (!returnMsgAndResId) {
throw Error('发送合并转发消息失败returnMsgAndResId 为空!'); throw Error('发送合并转发消息失败returnMsgAndResId 为空!');
@@ -308,8 +308,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
MessageUnique.createUniqueMsgId(selfPeer, result.value.msgId); MessageUnique.createUniqueMsgId(selfPeer, result.value.msgId);
} }
}); });
} catch (e) { } catch (e: any) {
logger.logDebug('生成转发消息节点失败', e); logger.logDebug('生成转发消息节点失败', e?.stack);
} }
} }
} }
@@ -350,8 +350,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
return { return {
message: await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds) message: await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
}; };
} catch (e) { } catch (e: any) {
logger.logError.bind(this.core.context.logger)('forward failed', e); logger.logError.bind(this.core.context.logger)('forward failed', e?.stack);
return { return {
message: null message: null
}; };
@@ -376,8 +376,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
try { try {
return await this.core.apis.MsgApi.sendMsg(selfPeer, sendElements, true); return await this.core.apis.MsgApi.sendMsg(selfPeer, sendElements, true);
} catch (e) { } catch (e: any) {
logger.logError.bind(this.core.context.logger)(e, '克隆转发消息失败,将忽略本条消息', msg); logger.logError.bind(this.core.context.logger)(e?.stack, '克隆转发消息失败,将忽略本条消息', msg);
} }
} }
} }

View File

@@ -7,7 +7,6 @@ export abstract class GetPacketStatusDepends<PT, RT> extends BaseAction<PT, RT>
protected async check(payload: PT): Promise<BaseCheckResult>{ protected async check(payload: PT): Promise<BaseCheckResult>{
if (!this.core.apis.PacketApi.available) { if (!this.core.apis.PacketApi.available) {
// work:add error stack?
return { return {
valid: false, valid: false,
message: "packetBackend不可用请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置" + message: "packetBackend不可用请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置" +

View File

@@ -5,7 +5,7 @@ export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me';
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent { export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_decrease'; notice_type = 'group_decrease';
sub_type: GroupDecreaseSubType = 'leave'; // work:实现其他几种子类型的识别 ("leave" | "kick" | "kick_me") sub_type: GroupDecreaseSubType = 'leave'; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operator_id: number; operator_id: number;
constructor(core: NapCatCore, groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') { constructor(core: NapCatCore, groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {

View File

@@ -1,7 +1,7 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
//work: 输入状态事件 初步完成 Mlikiowa 需要做一些过滤 //TODO: 输入状态事件 初步完成 Mlikiowa 需要做一些过滤
export class OB11InputStatusEvent extends OB11BaseNoticeEvent { export class OB11InputStatusEvent extends OB11BaseNoticeEvent {
notice_type = 'notify'; notice_type = 'notify';
sub_type = 'input_status'; sub_type = 'input_status';

View File

@@ -207,7 +207,7 @@ export class NapCatOneBot11Adapter {
for (const adapterConfig of adapters) { for (const adapterConfig of adapters) {
const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name); const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name);
if (existingAdapter) { if (existingAdapter) {
let networkChange = await existingAdapter.reload(adapterConfig); const networkChange = await existingAdapter.reload(adapterConfig);
if (networkChange === OB11NetworkReloadType.NetWorkClose) { if (networkChange === OB11NetworkReloadType.NetWorkClose) {
this.networkManager.closeSomeAdapters([existingAdapter]); this.networkManager.closeSomeAdapters([existingAdapter]);
@@ -543,28 +543,29 @@ export class NapCatOneBot11Adapter {
} }
private async emitMsg(message: RawMessage) { private async emitMsg(message: RawMessage) {
const network = Object.values(this.configLoader.configData.network) as Array<AdapterConfigWrap>; const network = Object.values(this.configLoader.configData.network).flat() as Array<AdapterConfigWrap>;
this.context.logger.logDebug('收到新消息 RawMessage', message); this.context.logger.logDebug('收到新消息 RawMessage', message);
await this.handleMsg(message, network);
await this.handleGroupEvent(message);
await this.handlePrivateMsgEvent(message);
}
private async handleMsg(message: RawMessage, network: Array<AdapterConfigWrap>) {
try { try {
const ob11Msg = await this.apis.MsgApi.parseMessageV2(message); const ob11Msg = await this.apis.MsgApi.parseMessageV2(message);
if (!ob11Msg) return; if (ob11Msg) {
const isSelfMsg = this.isSelfMessage(ob11Msg);
this.context.logger.logDebug('转化为 OB11Message', ob11Msg);
const msgMap = this.createMsgMap(network, ob11Msg, isSelfMsg, message);
this.handleDebugNetwork(network, msgMap, message);
this.handleNotReportSelfNetwork(network, msgMap, isSelfMsg);
this.networkManager.emitEventByNames(msgMap);
}
const isSelfMsg = this.isSelfMessage(ob11Msg);
this.context.logger.logDebug('转化为 OB11Message', ob11Msg);
const msgMap = this.createMsgMap(network, ob11Msg, isSelfMsg, message);
this.handleDebugNetwork(network, msgMap, message);
this.handleNotReportSelfNetwork(network, msgMap, isSelfMsg);
this.networkManager.emitEventByNames(msgMap);
} catch (e) { } catch (e) {
this.context.logger.logError('constructMessage error: ', e); this.context.logger.logError('constructMessage error: ', e);
} }
this.handleGroupEvent(message);
this.handlePrivateMsgEvent(message);
} }
private isSelfMessage(ob11Msg: { private isSelfMessage(ob11Msg: {
stringMsg: OB11Message; stringMsg: OB11Message;
arrayMsg: OB11Message; arrayMsg: OB11Message;
@@ -575,7 +576,7 @@ export class NapCatOneBot11Adapter {
private createMsgMap(network: Array<AdapterConfigWrap>, ob11Msg: any, isSelfMsg: boolean, message: RawMessage): Map<string, OB11Message> { private createMsgMap(network: Array<AdapterConfigWrap>, ob11Msg: any, isSelfMsg: boolean, message: RawMessage): Map<string, OB11Message> {
const msgMap: Map<string, OB11Message> = new Map(); const msgMap: Map<string, OB11Message> = new Map();
network.flat().filter(e => e.enable).forEach(e => { network.filter(e => e.enable).forEach(e => {
if (e.messagePostFormat == 'string') { if (e.messagePostFormat == 'string') {
msgMap.set(e.name, structuredClone(ob11Msg.stringMsg)); msgMap.set(e.name, structuredClone(ob11Msg.stringMsg));
} else { } else {
@@ -590,7 +591,7 @@ export class NapCatOneBot11Adapter {
} }
private handleDebugNetwork(network: Array<AdapterConfigWrap>, msgMap: Map<string, OB11Message>, message: RawMessage) { private handleDebugNetwork(network: Array<AdapterConfigWrap>, msgMap: Map<string, OB11Message>, message: RawMessage) {
const debugNetwork = network.flat().filter(e => e.enable && e.debug); const debugNetwork = network.filter(e => e.enable && e.debug);
if (debugNetwork.length > 0) { if (debugNetwork.length > 0) {
debugNetwork.forEach(adapter => { debugNetwork.forEach(adapter => {
const msg = msgMap.get(adapter.name); const msg = msgMap.get(adapter.name);
@@ -605,7 +606,7 @@ export class NapCatOneBot11Adapter {
private handleNotReportSelfNetwork(network: Array<AdapterConfigWrap>, msgMap: Map<string, OB11Message>, isSelfMsg: boolean) { private handleNotReportSelfNetwork(network: Array<AdapterConfigWrap>, msgMap: Map<string, OB11Message>, isSelfMsg: boolean) {
if (isSelfMsg) { if (isSelfMsg) {
const notReportSelfNetwork = network.flat().filter(e => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e))); const notReportSelfNetwork = network.filter(e => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e)));
notReportSelfNetwork.forEach(adapter => { notReportSelfNetwork.forEach(adapter => {
msgMap.delete(adapter.name); msgMap.delete(adapter.name);
}); });
@@ -638,7 +639,7 @@ export class NapCatOneBot11Adapter {
// log("message update", message.sendStatus, message.msgId, message.msgSeq) // log("message update", message.sendStatus, message.msgId, message.msgSeq)
const peer: Peer = { chatType: message.chatType, peerUid: message.peerUid, guildId: '' }; const peer: Peer = { chatType: message.chatType, peerUid: message.peerUid, guildId: '' };
if (message.recallTime != '0' && !cache.get(message.msgId)) { if (message.recallTime != '0' && !cache.get(message.msgId)) {
//work:这个判断方法不太好,应该使用灰色消息元素来判断? //TODO: 这个判断方法不太好,应该使用灰色消息元素来判断?
cache.put(message.msgId, true); cache.put(message.msgId, true);
// 撤回消息上报 // 撤回消息上报
let oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId); let oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId);

View File

@@ -43,7 +43,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
} }
}, this.config.heartInterval); }, this.config.heartInterval);
} }
this.isEnable = true;
await this.tryConnect(); await this.tryConnect();
} }
@@ -70,7 +70,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
} }
private async tryConnect() { private async tryConnect() {
if (!this.connection && !this.isEnable) { if (!this.connection && this.isEnable) {
let isClosedByError = false; let isClosedByError = false;
this.connection = new WebSocket(this.config.url, { this.connection = new WebSocket(this.config.url, {
@@ -106,7 +106,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
if (!isClosedByError) { if (!isClosedByError) {
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`); this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`);
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`); this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
if (!this.isEnable) { if (this.isEnable) {
this.connection = null; this.connection = null;
setTimeout(() => this.tryConnect(), this.config.reconnectInterval); setTimeout(() => this.tryConnect(), this.config.reconnectInterval);
} }
@@ -116,7 +116,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
isClosedByError = true; isClosedByError = true;
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err); this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err);
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`); this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
if (!this.isEnable) { if (this.isEnable) {
this.connection = null; this.connection = null;
setTimeout(() => this.tryConnect(), this.config.reconnectInterval); setTimeout(() => this.tryConnect(), this.config.reconnectInterval);
} }

View File

@@ -17,7 +17,6 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
wsClients: WebSocket[] = []; wsClients: WebSocket[] = [];
wsClientsMutex = new Mutex(); wsClientsMutex = new Mutex();
isEnable: boolean = false; isEnable: boolean = false;
hasBeenClosed: boolean = false;
heartbeatInterval: number = 0; heartbeatInterval: number = 0;
logger: LogWrapper; logger: LogWrapper;
public config: WebsocketServerConfig; public config: WebsocketServerConfig;
@@ -107,10 +106,6 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server'); this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server');
return; return;
} }
if (this.hasBeenClosed) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Cannot open a WebSocket server that has been closed');
return;
}
const addressInfo = this.wsServer.address(); const addressInfo = this.wsServer.address();
this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port); this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);