Compare commits

...

112 Commits

Author SHA1 Message Date
手瓜一十雪
0b8bf739e9 fix: event 2024-11-18 19:51:08 +08:00
手瓜一十雪
0222664db8 fix: type
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 14m48s
Build Action / Build-Shell (push) Failing after 14m43s
2024-11-17 16:02:25 +08:00
手瓜一十雪
a88792e452 docs: 没有参考故移除 2024-11-17 16:01:44 +08:00
手瓜一十雪
ad45400742 docs: 调整更新速度与Packet重构 2024-11-17 15:57:21 +08:00
手瓜一十雪
53e5ba03be fix 2024-11-17 15:56:48 +08:00
手瓜一十雪
b587d6b91d fix: (SetGroupSign) BaseAction-->GetPacketStatusDepends 2024-11-17 15:37:10 +08:00
手瓜一十雪
5e750d4ee9 feat: uploadQunAlbum未测试
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 2m40s
Build Action / Build-Shell (push) Failing after 2m38s
2024-11-17 13:39:57 +08:00
Mlikiowa
50fb32f81c release: v4.1.5 2024-11-17 03:39:17 +00:00
手瓜一十雪
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
手瓜一十雪
bda174bed4 fix: 异常 2024-11-16 20:13:36 +08:00
Mlikiowa
caf98b8655 release: v4.1.1 2024-11-16 11:26:41 +00:00
手瓜一十雪
c9833c5988 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-16 19:25:58 +08:00
手瓜一十雪
55ef7e529e fix: 4.1.1 2024-11-16 19:25:54 +08:00
Mlikiowa
9b04ddcefd release: v4.1.0 2024-11-16 10:41:27 +00:00
手瓜一十雪
6dc4f38581 refactor: AdapterConfig 2024-11-16 18:38:44 +08:00
手瓜一十雪
93ce8bfb85 refactor: emitMsg 2024-11-16 18:31:24 +08:00
手瓜一十雪
e7d138448a refactor: reloadNetwork 2024-11-16 18:10:03 +08:00
手瓜一十雪
02c4a468cb fix 2024-11-16 16:56:34 +08:00
手瓜一十雪
d392e653e1 refactor: network 2024-11-16 16:56:20 +08:00
手瓜一十雪
e8faa09f1d refactor: fetch->RequestUtil
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 17s
Build Action / Build-Shell (push) Failing after 9s
2024-11-16 13:58:46 +08:00
手瓜一十雪
e80ed3b33e fix: 切分依赖 2024-11-16 13:47:33 +08:00
手瓜一十雪
41a346e1cf Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-16 13:44:06 +08:00
手瓜一十雪
5e19fc112a fix: ws 0.0.0. 兼容 2024-11-16 13:44:02 +08:00
手瓜一十雪
2f7aff2b56 feat: 调整配置顺序 2024-11-16 13:35:10 +08:00
手瓜一十雪
ccb0e1fb4f docs: thank tdesign 2024-11-16 13:24:19 +08:00
手瓜一十雪
d4163c913a docs: tdesign 2024-11-16 13:23:05 +08:00
手瓜一十雪
8087ba0e4a fix: webui 2024-11-16 13:20:42 +08:00
手瓜一十雪
6700523b61 feat: 网络重载日志 2024-11-16 13:18:42 +08:00
手瓜一十雪
49f1c3f9ba fix: 翻新网络重载 2024-11-16 13:07:20 +08:00
手瓜一十雪
575ab4f1d1 fix: 打包流程 2024-11-16 12:57:12 +08:00
手瓜一十雪
3658547731 Merge pull request #524 from NapNeko/refactor-config-webui
refactor: new config & vue webui & new network & new parseMsg
2024-11-16 12:53:46 +08:00
手瓜一十雪
eb6590e9e2 feat: 打包脚本 2024-11-16 12:50:45 +08:00
手瓜一十雪
83f28795f2 feat: 移除无用代码 2024-11-16 12:45:27 +08:00
手瓜一十雪
e98bfaac11 fix: error 2024-11-16 12:34:50 +08:00
手瓜一十雪
4f4bd3c6e0 fix: 抑制警告 2024-11-16 11:45:57 +08:00
手瓜一十雪
bd1faccaa8 fix: build 2024-11-16 11:44:23 +08:00
手瓜一十雪
25751b8149 fix 2024-11-16 11:39:12 +08:00
手瓜一十雪
e34b60315c fix: build 2024-11-16 11:37:47 +08:00
手瓜一十雪
046afc0c23 Merge branch 'main' into refactor-config-webui 2024-11-16 11:35:59 +08:00
手瓜一十雪
2f61ba7f25 feat: 优化上报问题 2024-11-16 11:32:27 +08:00
手瓜一十雪
8981f12b1a fix: 修复大部分逻辑 2024-11-16 11:25:16 +08:00
手瓜一十雪
34e96b1089 fix: 逻辑操作 2024-11-16 11:14:21 +08:00
手瓜一十雪
41db435ef5 fix: 样式 2024-11-16 11:08:45 +08:00
手瓜一十雪
b525fa81bb fix: 一处样式问题 2024-11-16 10:57:15 +08:00
手瓜一十雪
6382b29da8 feat: 提升交互体验 2024-11-16 10:55:26 +08:00
手瓜一十雪
8bc0403139 feat: 微调样式 2024-11-16 10:42:01 +08:00
手瓜一十雪
9f261e78c3 fix: name保存问题 2024-11-16 10:38:03 +08:00
手瓜一十雪
15d9390ee4 feat: 基础样式 2024-11-16 10:34:31 +08:00
手瓜一十雪
572b8809a5 feat: dev webui 2024-11-16 10:20:17 +08:00
pk5ls20
623799c049 fix: migrateOneBotConfigsV1 2024-11-16 07:22:29 +08:00
pk5ls20
4271acc6ab feat: add migrateOneBotConfigsV2 2024-11-16 06:52:18 +08:00
pk5ls20
609e83a824 refactor: more comprehensive dev and prod env isolation and build process 2024-11-16 06:10:36 +08:00
pk5ls20
e98910c9ff chore: adjust eslint 2024-11-16 05:50:44 +08:00
pk5ls20
c432799580 refactor: webui network 2024-11-16 05:43:44 +08:00
pk5ls20
fa87f7c8c3 fix: vite config 2024-11-16 00:13:05 +08:00
pk5ls20
4a44062814 chore: revert package.json 2024-11-15 23:41:17 +08:00
pk5ls20
fe0bda11d3 refactor: webui 2024-11-15 23:39:19 +08:00
手瓜一十雪
1ec1040e43 feat: 产物替换 2024-11-15 20:39:57 +08:00
pk5ls20
e44595334a perf: avoid silk encoding blocking the main event loop (#527)
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 13s
Build Action / Build-Shell (push) Failing after 12s
2024-11-15 20:39:18 +08:00
手瓜一十雪
f40de023b0 feat: 加入注释 2024-11-15 20:35:30 +08:00
手瓜一十雪
9799d02ad2 fix: 清除基础信息 2024-11-15 20:34:42 +08:00
手瓜一十雪
bec88fee04 feat: 修复渲染异常bug 2024-11-15 20:33:06 +08:00
手瓜一十雪
1a94e20691 fix: 关闭按钮 2024-11-15 20:26:00 +08:00
手瓜一十雪
3690307d0b fix: 添加配置 2024-11-15 20:22:42 +08:00
手瓜一十雪
2d5b4bc90a feat: 保存数据 2024-11-15 20:00:49 +08:00
手瓜一十雪
cc93ed3567 fix: shallowRef 2024-11-15 19:54:21 +08:00
手瓜一十雪
dce4988767 refactor: network 2024-11-15 19:51:19 +08:00
手瓜一十雪
5c81b60b58 feat: 渲染网络配置 2024-11-15 19:48:27 +08:00
手瓜一十雪
a668bfbc13 refactor: emitMsg 2024-11-15 18:40:35 +08:00
手瓜一十雪
bc0fc96b9b refactor: rkey get 2024-11-15 18:35:34 +08:00
手瓜一十雪
ae14692d5b refactor: parseMsg 2024-11-15 18:29:42 +08:00
手瓜一十雪
d445dc6644 refactor: 初步可用 2024-11-15 17:35:09 +08:00
手瓜一十雪
db3d435402 feat: 彻底 扬了Old WebUi 2024-11-15 16:56:53 +08:00
手瓜一十雪
7ee48f1443 feat: 迁移后端与前端 大部分逻辑 2024-11-15 16:50:12 +08:00
手瓜一十雪
a54f30acc1 feat: 翻新除了配置文件的所有代码了 2024-11-15 16:45:57 +08:00
手瓜一十雪
75e7bc7275 feat: 开始迁移webui 2024-11-15 16:10:19 +08:00
手瓜一十雪
f1b2c8b1cf fix 2024-11-15 15:24:44 +08:00
手瓜一十雪
50079e7a96 fix: 面板关于信息 2024-11-15 14:46:05 +08:00
手瓜一十雪
6d37868ae8 refactor: nnetwork -> network 2024-11-15 13:57:45 +08:00
手瓜一十雪
543961e980 feat: 调整基础样式 2024-11-15 13:52:23 +08:00
手瓜一十雪
1e2c76bb47 feat: 布局面板基础结构 2024-11-15 13:25:02 +08:00
手瓜一十雪
ddc0ed066d feat: nav 2024-11-15 13:03:18 +08:00
手瓜一十雪
6708903c65 feat: 基础逻辑拼接 2024-11-15 11:32:30 +08:00
手瓜一十雪
5ee0afb604 Merge branch 'main' into refactor-config-webui 2024-11-15 10:42:02 +08:00
手瓜一十雪
9b20e9db29 feat: 路由 2024-11-15 10:41:55 +08:00
Mlikiowa
74b4d9bf49 release: v4.0.3
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 7s
Build Action / Build-Shell (push) Failing after 8s
2024-11-14 15:34:43 +00:00
手瓜一十雪
f83bf197d2 feat: QQLogin 2024-11-14 22:02:15 +08:00
手瓜一十雪
5bcc130dd7 feat: 拆分组件 2024-11-14 21:46:54 +08:00
手瓜一十雪
4be6d8ec01 feat: 初始化webui login 2024-11-14 21:41:43 +08:00
手瓜一十雪
ae57ab78f3 fix: adapter api 2024-11-14 20:25:08 +08:00
手瓜一十雪
4487db4e0a feat: msg push 2024-11-14 20:18:19 +08:00
手瓜一十雪
a0a50755d3 feat: mergeOnebotConfigs 2024-11-14 18:00:31 +08:00
手瓜一十雪
621e41cc96 feat: new config helper/nnetwork 2024-11-14 17:00:34 +08:00
113 changed files with 3064 additions and 3696 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,54 +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
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:
npm run build:shell name: NapCat.Shell
cd dist path: 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

@@ -49,6 +49,9 @@ jobs:
- name: Build NuCat Framework - name: Build NuCat Framework
run: | run: |
npm i npm i
cd napcat.webui
npm i
cd ..
npm run build:framework npm run build:framework
cd dist cd dist
npm i --omit=dev npm i --omit=dev
@@ -78,6 +81,9 @@ jobs:
- name: Build NuCat Shell - name: Build NuCat Shell
run: | run: |
npm i npm i
cd napcat.webui
npm i
cd ..
npm run build:shell npm run build:shell
cd dist cd dist
npm i --omit=dev npm i --omit=dev

View File

@@ -8,7 +8,7 @@
## 欢迎回家 ## 欢迎回家
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现 NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 碎碎叨叨 ## 特性介绍
- [x] **安装简单**:就算是笨蛋也能使用 - [x] **安装简单**:就算是笨蛋也能使用
- [x] **性能友好**:就算是低内存也能使用 - [x] **性能友好**:就算是低内存也能使用
- [x] **接口丰富**:就算是没有也能使用 - [x] **接口丰富**:就算是没有也能使用
@@ -26,25 +26,34 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
[Cloudflare.HKServer](https://napcat.napneko.icu/) [Cloudflare.HKServer](https://napcat.napneko.icu/)
[Github.IO](https://napneko.github.io/)
[Cloudflare.Pages](https://napneko.pages.dev/) [Cloudflare.Pages](https://napneko.pages.dev/)
[Server.China](https://napneko.com/) [Server.China](https://napneko.com/)
[Server.Other](https://napcat.cyou/) [Server.Other](https://napcat.cyou/)
[Github.IO](https://napneko.github.io/)
## 回家旅途 ## 回家旅途
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS) [QQ Group](https://qm.qq.com/q/VfjAq5HIMS)
## 感谢他们 ## 感谢他们
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
不过最最重要的 还是需要感谢屏幕前的你哦~ 不过最最重要的 还是需要感谢屏幕前的你哦~
--- ---
## 延缓Native模块与NapCat对新版QQ适配
为未来持续与高效的使用Native模块 模块代码转为完全非Git仓库的本地保存源码 并进行相关重构
同时为了保证稳定 NapCat 本体通常会在3 Week+的周期进行新版本适配
因此此时推荐使用release指定版本
## 开源附加 ## 开源附加
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。** 任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**

View File

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

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

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

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

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

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

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

View File

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

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

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

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

After

Width:  |  Height:  |  Size: 496 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,66 @@
<template>
<div class="about-us">
<div>
<t-divider content="面板关于信息" align="left" />
<t-alert theme="success" message="NapCat.WebUi is running" />
<t-list class="list">
<t-list-item class="list-item">
<span class="item-label">开发人员:</span>
<span class="item-content">
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
</span>
</t-list-item>
<t-list-item class="list-item">
<span class="item-label">版本信息:</span>
<span class="item-content">
<t-tag class="tag-item" theme="success"> WebUi: {{ 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-tag>
</span>
</t-list-item>
</t-list>
</div>
</div>
</template>
<script setup lang="ts">
import pkg from '../../package.json';
import { napCatVersion } from '../../../src/common/version';
</script>
<style scoped>
.about-us {
padding: 20px;
text-align: left;
}
.list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-label {
flex: 1;
font-weight: bold;
}
.item-content {
flex: 2;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
}
.tag-item {
margin-right: 10px;
margin-bottom: 10px;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,134 @@
<template>
<div>
<t-divider content="其余配置" align="left" />
</div>
<div class="other-config-container">
<div class="other-config">
<t-form ref="form" :model="otherConfig" class="form">
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
<t-input v-model="otherConfig.musicSignUrl" />
</t-form-item>
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
<t-switch v-model="otherConfig.enableLocalFile2Url" />
</t-form-item>
</t-form>
<div class="button-container">
<t-button @click="saveConfig">保存</t-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { OneBotConfig } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
const otherConfig = ref<Partial<OneBotConfig>>({
musicSignUrl: '',
enableLocalFile2Url: false,
});
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (userConfig) {
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
}
} catch (error) {
console.error('Error loading config:', error);
}
};
const saveConfig = async () => {
try {
const userConfig = await getOB11Config();
if (userConfig) {
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
}
} catch (error) {
console.error('Error saving config:', error);
MessagePlugin.error('配置保存失败');
}
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.other-config-container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.other-config {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
.form {
display: flex;
flex-direction: column;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.button-container {
display: flex;
justify-content: center;
}
@media (min-width: 768px) {
.form-item {
flex-direction: row;
align-items: center;
}
.form-item t-input,
.form-item t-switch {
flex: 1;
margin-left: 20px;
}
}
</style>

View File

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

View File

@@ -0,0 +1,68 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Client 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { HttpClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: HttpClientConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
}
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Server 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-checkbox v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { HttpServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: HttpServerConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
}
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Client 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: WebsocketClientConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
}
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Server 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="上报自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="强制推送事件">
<t-checkbox v-model="config.enableForcePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: WebsocketServerConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
}
);
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
proxy: {
'/api': 'http://localhost:6099',
},
},
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
}
}
}
}
});

View File

@@ -2,12 +2,15 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "4.0.2", "version": "4.1.5",
"scripts": { "scripts": {
"build:framework": "vite build --mode framework", "build:framework": "npm run build:webui && vite build --mode framework",
"build:shell": "vite build --mode shell", "build:shell": "npm run build:webui && vite build --mode shell",
"build:webui": "cd ./src/webui && vite build", "build:webui": "cd napcat.webui && vite build",
"lint": "eslint --fix src/**/*.{js,ts}", "dev:framework": "vite build --mode framework",
"dev:shell": "vite build --mode shell",
"dev:webui": "cd napcat.webui && npm run webui:dev",
"lint": "eslint --fix src/**/*.{js,ts,vue}",
"depend": "cd dist && npm install --omit=dev" "depend": "cd dist && npm install --omit=dev"
}, },
"devDependencies": { "devDependencies": {
@@ -48,9 +51,10 @@
}, },
"dependencies": { "dependencies": {
"express": "^5.0.0", "express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0", "ws": "^8.18.0",
"qrcode-terminal": "^0.12.0", "piscina": "^4.7.0"
"fluent-ffmpeg": "^2.1.2"
} }
} }

View File

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

View File

@@ -0,0 +1,9 @@
import { encode } from "silk-wasm";
export interface EncodeArgs {
input: ArrayBufferView | ArrayBuffer
sampleRate: number
}
export default async ({ input, sampleRate }: EncodeArgs) => {
return await encode(input, sampleRate);
};

View File

@@ -1,13 +1,23 @@
import Piscina from 'piscina';
import fsPromise from 'fs/promises'; import fsPromise from 'fs/promises';
import path from 'node:path'; import path from 'node:path';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm'; import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { LogWrapper } from './log'; import { LogWrapper } from './log';
import { EncodeArgs } from "@/common/audio-worker";
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const EXIT_CODES = [0, 255]; const EXIT_CODES = [0, 255];
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg'; const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
async function getWorkerPath() {
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
async function guessDuration(pttPath: string, logger: LogWrapper) { async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath); const pttFileInfo = await fsPromise.stat(pttPath);
@@ -41,8 +51,11 @@ async function convert(filePath: string, pcmPath: string, logger: LogWrapper): P
} }
async function handleWavFile( async function handleWavFile(
file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper file: Buffer,
): Promise<{input: Buffer, sampleRate: number}> { filePath: string,
pcmPath: string,
logger: LogWrapper
): Promise<{ input: Buffer; sampleRate: number }> {
const { fmt } = getWavFileInfo(file); const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) { if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 }; return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
@@ -60,8 +73,8 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
const { input, sampleRate } = isWav(file) const { input, sampleRate } = isWav(file)
? (await handleWavFile(file, filePath, pcmPath, logger)) ? (await handleWavFile(file, filePath, pcmPath, logger))
: { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 }; : { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
const silk = await encode(input, sampleRate); const silk = await piscina.run({ input: input, sampleRate: sampleRate });
await fsPromise.writeFile(pttPath, silk.data); await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return { return {
converted: true, converted: true,
@@ -86,4 +99,4 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
logger.logError.bind(logger)('convert silk failed', error.stack); logger.logError.bind(logger)('convert silk failed', error.stack);
return {}; return {};
} }
} }

View File

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

View File

@@ -234,25 +234,33 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback); this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
this.createListenerFunction(ListenerMainName); this.createListenerFunction(ListenerMainName);
this.createEventFunction(serviceAndMethod)!(...(args)) let eventResult = this.createEventFunction(serviceAndMethod)!(...(args));
.then((eventResult: any) => {
retEvent = eventResult; const eventRetHandle = (eventData: any) => {
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) { retEvent = eventData;
clearTimeout(timeoutRef); if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
reject( clearTimeout(timeoutRef);
new Error( reject(
'EventChecker Failed: NTEvent serviceAndMethod:' + new Error(
serviceAndMethod + 'EventChecker Failed: NTEvent serviceAndMethod:' +
' ListenerName:' + serviceAndMethod +
listenerAndMethod + ' ListenerName:' +
' EventRet:\n' + listenerAndMethod +
JSON.stringify(retEvent, null, 4) + ' EventRet:\n' +
'\n', JSON.stringify(retEvent, null, 4) +
), '\n',
); ),
} );
}
}
if (eventResult instanceof Promise) {
eventResult.then((eventResult: any) => {
eventRetHandle(eventResult);
}) })
.catch(reject); .catch(reject);
} else {
eventRetHandle(eventResult);
}
}, },
); );
} }

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.0.2'; export const napCatVersion = '4.1.5';

View File

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

View File

@@ -8,6 +8,9 @@ import {
WebHonorType, WebHonorType,
} from '@/core'; } from '@/core';
import { NapCatCore } from '..'; import { NapCatCore } from '..';
import { createReadStream, readFileSync, statSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { basename } from 'node:path';
export class NTQQWebApi { export class NTQQWebApi {
context: InstanceContext; context: InstanceContext;
@@ -303,4 +306,110 @@ export class NTQQWebApi {
} }
return (hash & 0x7FFFFFFF).toString(); return (hash & 0x7FFFFFFF).toString();
} }
async createQunAlbumSession(gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, uin: string) {
const img = readFileSync(path);
const img_md5 = createHash('md5').update(img).digest('hex');
const img_size = img.length;
const img_name = basename(path);
const time = Math.floor(Date.now() / 1000);
const GTK = this.getBknFromSKey(pskey);
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
const body = {
control_req: [{
uin: uin,
token: {
type: 4,
data: pskey,
appid: 5
},
appid: "qun",
checksum: img_md5,
check_type: 0,
file_len: img_size,
env: {
refer: "qzone",
deviceInfo: "h5"
},
model: 0,
biz_req: {
sPicTitle: img_name,
sPicDesc: "",
sAlbumName: sAlbumName,
sAlbumID: sAlbumID,
iAlbumTypeID: 0,
iBitmap: 0,
iUploadType: 0,
iUpPicType: 0,
iBatchID: time,
sPicPath: "",
iPicWidth: 0,
iPicHight: 0,
iWaterType: 0,
iDistinctUse: 0,
iNeedFeeds: 1,
iUploadTime: time,
mapExt: {
appid: "qun",
userid: gc
}
},
session: "",
asy_upload: 0,
cmd: "FileUpload"
}]
};
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
"Cookie": cookie,
"Content-Type": "application/json"
});
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

@@ -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,5 +1,5 @@
{ {
"fileLog": true, "fileLog": false,
"consoleLog": true, "consoleLog": true,
"fileLogLevel": "debug", "fileLogLevel": "debug",
"consoleLogLevel": "info", "consoleLogLevel": "info",

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

@@ -150,7 +150,7 @@ export interface NodeIQQNTWrapperSession {
nodeIKernelSessionListener: NodeIKernelSessionListener, nodeIKernelSessionListener: NodeIKernelSessionListener,
): void; ): void;
startNT(n: 0): void; startNT(session: number): void;
startNT(): void; startNT(): void;

View File

@@ -37,27 +37,27 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
}; };
} }
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> { public async handle(payload: PayloadType, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload); const result = await this.check(payload);
if (!result.valid) { if (!result.valid) {
return OB11Response.error(result.message, 400); return OB11Response.error(result.message, 400);
} }
try { try {
const resData = await this._handle(payload); const resData = await this._handle(payload, adaptername);
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);
} }
} }
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> { public async websocketHandle(payload: PayloadType, echo: any, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload); const result = await this.check(payload);
if (!result.valid) { if (!result.valid) {
return OB11Response.error(result.message, 1400, echo); return OB11Response.error(result.message, 1400, echo);
} }
try { try {
const resData = await this._handle(payload); const resData = await this._handle(payload, adaptername);
return OB11Response.ok(resData, echo); return OB11Response.ok(resData, echo);
} 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);
@@ -65,7 +65,7 @@ abstract class BaseAction<PayloadType, ReturnDataType> {
} }
} }
abstract _handle(payload: PayloadType): PromiseLike<ReturnDataType>; abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
} }
export default BaseAction; export default BaseAction;

View File

@@ -1,4 +1,4 @@
import BaseAction from '../BaseAction'; import { GetPacketStatusDepends } from '../packet/GetPacketStatus';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
@@ -12,7 +12,7 @@ const SchemaData = {
type Payload = FromSchema<typeof SchemaData>; type Payload = FromSchema<typeof SchemaData>;
export class SetGroupSign extends BaseAction<Payload, any> { export class SetGroupSign extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SetGroupSign; actionName = ActionName.SetGroupSign;
payloadSchema = SchemaData; payloadSchema = SchemaData;

View File

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

View File

@@ -4,6 +4,7 @@ import { ActionName } from '../types';
import { ChatType } from '@/core/entities'; import { ChatType } from '@/core/entities';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config';
interface Response { interface Response {
messages: OB11Message[]; messages: OB11Message[];
@@ -26,7 +27,7 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GetFriendMsgHistory; actionName = ActionName.GetFriendMsgHistory;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<Response> { async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数 //处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const MsgCount = +(payload.count ?? 20); const MsgCount = +(payload.count ?? 20);
@@ -45,9 +46,10 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
await Promise.all(msgList.map(async msg => { await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId); msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
})); }));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
//烘焙消息 //烘焙消息
const ob11MsgList = (await Promise.all( const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg))) msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array')))
).filter(msg => msg !== undefined); ).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList }; return { 'messages': ob11MsgList };
} }

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

@@ -4,6 +4,7 @@ import { ActionName } from '../types';
import { ChatType, Peer } from '@/core/entities'; import { ChatType, Peer } from '@/core/entities';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config';
interface Response { interface Response {
messages: OB11Message[]; messages: OB11Message[];
@@ -26,7 +27,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory; actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<Response> { async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数 //处理参数
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder; const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
const MsgCount = +(payload.count ?? 20); const MsgCount = +(payload.count ?? 20);
@@ -43,9 +44,11 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
await Promise.all(msgList.map(async msg => { await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId); msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
})); }));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
//烘焙消息 //烘焙消息
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
const ob11MsgList = (await Promise.all( const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg))) msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, msgFormat)))
).filter(msg => msg !== undefined); ).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList }; return { 'messages': ob11MsgList };
} }

View File

@@ -28,7 +28,7 @@ export class GetGroupRootFiles extends BaseAction<Payload, {
startIndex: 0, startIndex: 0,
sortOrder: 2, sortOrder: 2,
showOnlinedocFolder: 0, showOnlinedocFolder: 0,
}).catch(() => []); });
return { return {
files: ret.filter(item => item.fileInfo) files: ret.filter(item => item.fileInfo)

View File

@@ -4,6 +4,7 @@ import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import crypto from 'crypto'; import crypto from 'crypto';
import { AdapterConfigWrap } from '@/onebot/config/config';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -30,7 +31,9 @@ export class GetGroupEssence extends BaseAction<Payload, any> {
}; };
} }
async _handle(payload: Payload) { async _handle(payload: Payload, adapter: string) {
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
const msglist = (await this.core.apis.WebApi.getGroupEssenceMsgAll(payload.group_id.toString())).flatMap((e) => e.data.msg_list); const msglist = (await this.core.apis.WebApi.getGroupEssenceMsgAll(payload.group_id.toString())).flatMap((e) => e.data.msg_list);
if (!msglist) { if (!msglist) {
throw new Error('获取失败'); throw new Error('获取失败');
@@ -51,7 +54,7 @@ export class GetGroupEssence extends BaseAction<Payload, any> {
operator_nick: msg.add_digest_nick, operator_nick: msg.add_digest_nick,
message_id: message_id, message_id: message_id,
operator_time: msg.add_digest_time, operator_time: msg.add_digest_time,
content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage))?.message content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage, msgFormat))?.message
}; };
} }
const msgTempData = JSON.stringify({ const msgTempData = JSON.stringify({

View File

@@ -4,6 +4,7 @@ import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { RawMessage } from '@/core'; import { RawMessage } from '@/core';
import { AdapterConfigWrap } from '@/onebot/config/config';
export type ReturnDataType = OB11Message export type ReturnDataType = OB11Message
@@ -22,8 +23,10 @@ class GetMsg extends BaseAction<Payload, OB11Message> {
actionName = ActionName.GetMsg; actionName = ActionName.GetMsg;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload, adapter: string) {
// log("history msg ids", Object.keys(msgHistory)); // log("history msg ids", Object.keys(msgHistory));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
if (!payload.message_id) { if (!payload.message_id) {
throw Error('参数message_id不能为空'); throw Error('参数message_id不能为空');
} }
@@ -40,7 +43,7 @@ class GetMsg extends BaseAction<Payload, OB11Message> {
} else { } else {
msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0]; msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0];
} }
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg); const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, msgFormat);
if (!retMsg) throw Error('消息为空'); if (!retMsg) throw Error('消息为空');
try { try {
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!; retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!;

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

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

View File

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

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

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

View File

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

View File

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

View File

@@ -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

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

View File

@@ -1,25 +1,31 @@
import { IOB11NetworkAdapter, OB11EmitEventContent } from '@/onebot/network/index'; import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { LogWrapper } from '@/common/log'; import { LogWrapper } from '@/common/log';
import { QuickAction, QuickActionEvent } from '../types'; import { QuickAction, QuickActionEvent } from '../types';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
import { NapCatOneBot11Adapter } from '..'; import { NapCatOneBot11Adapter } from '..';
import { RequestUtil } from '@/common/request';
import { HttpClientConfig } from '../config/config';
import { ActionMap } from '../action';
export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter { export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
logger: LogWrapper; logger: LogWrapper;
isOpen: boolean = false; isEnable: boolean = false;
public config: HttpClientConfig;
constructor( constructor(
public url: string, public name: string,
public secret: string | undefined, config: HttpClientConfig,
public core: NapCatCore, public core: NapCatCore,
public obContext: NapCatOneBot11Adapter, public obContext: NapCatOneBot11Adapter,
public actions: ActionMap,
) { ) {
this.logger = core.context.logger; this.logger = core.context.logger;
this.config = structuredClone(config);
} }
onEvent<T extends OB11EmitEventContent>(event: T) { onEvent<T extends OB11EmitEventContent>(event: T) {
if (!this.isOpen) { if (!this.isEnable) {
return; return;
} }
const headers: Record<string, string> = { const headers: Record<string, string> = {
@@ -27,20 +33,16 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
'x-self-id': this.core.selfInfo.uin, 'x-self-id': this.core.selfInfo.uin,
}; };
const msgStr = JSON.stringify(event); const msgStr = JSON.stringify(event);
if (this.secret && this.secret.length > 0) { if (this.config.token && this.config.token.length > 0) {
const hmac = createHmac('sha1', this.secret); const hmac = createHmac('sha1', this.config.token);
hmac.update(msgStr); hmac.update(msgStr);
const sig = hmac.digest('hex'); const sig = hmac.digest('hex');
headers['x-signature'] = 'sha1=' + sig; headers['x-signature'] = 'sha1=' + sig;
} }
fetch(this.url, { RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers).then(async (res) => {
method: 'POST',
headers,
body: msgStr,
}).then(async (res) => {
let resJson: QuickAction; let resJson: QuickAction;
try { try {
resJson = await res.json(); resJson = JSON.parse(res);
//logDebug('新消息事件HTTP上报返回快速操作: ', JSON.stringify(resJson)); //logDebug('新消息事件HTTP上报返回快速操作: ', JSON.stringify(resJson));
} catch (e) { } catch (e) {
this.logger.logDebug('[OneBot] [Http Client] 新消息事件HTTP上报没有返回快速操作不需要处理'); this.logger.logDebug('[OneBot] [Http Client] 新消息事件HTTP上报没有返回快速操作不需要处理');
@@ -59,10 +61,26 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
} }
open() { open() {
this.isOpen = true; this.isEnable = true;
} }
close() { close() {
this.isOpen = false; this.isEnable = false;
}
async reload(newconfig: HttpClientConfig) {
const wasEnabled = this.isEnable;
const oldUrl = this.config.url;
this.config = newconfig;
if (newconfig.enable && !wasEnabled) {
this.open();
return OB11NetworkReloadType.NetWorkOpen;
} else if (!newconfig.enable && wasEnabled) {
this.close();
return OB11NetworkReloadType.NetWorkClose;
}
if (oldUrl !== newconfig.url) {
return OB11NetworkReloadType.NetWorkReload;
}
return OB11NetworkReloadType.Normal;
} }
} }

View File

@@ -1,4 +1,4 @@
import { IOB11NetworkAdapter, OB11EmitEventContent } from '@/onebot/network/index'; import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { WebSocket } from 'ws'; import { WebSocket } from 'ws';
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'; import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
@@ -7,22 +7,23 @@ import { OB11Response } from '@/onebot/action/OB11Response';
import { LogWrapper } from '@/common/log'; import { LogWrapper } from '@/common/log';
import { ActionMap } from '@/onebot/action'; import { ActionMap } from '@/onebot/action';
import { LifeCycleSubType, OB11LifeCycleEvent } from '../event/meta/OB11LifeCycleEvent'; import { LifeCycleSubType, OB11LifeCycleEvent } from '../event/meta/OB11LifeCycleEvent';
import { WebsocketClientConfig } from '../config/config';
export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter { export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
isClosed: boolean = false; isEnable: boolean = false;
logger: LogWrapper; logger: LogWrapper;
private connection: WebSocket | null = null; private connection: WebSocket | null = null;
private heartbeatRef: NodeJS.Timeout | null = null; private heartbeatRef: NodeJS.Timeout | null = null;
public config: WebsocketClientConfig;
constructor( constructor(
public url: string, public name: string,
public reconnectIntervalInMillis: number, confg: WebsocketClientConfig,
public heartbeatIntervalInMillis: number,
private readonly token: string,
public core: NapCatCore, public core: NapCatCore,
public actions: ActionMap, public actions: ActionMap,
) { ) {
this.logger = core.context.logger; this.logger = core.context.logger;
this.config = structuredClone(confg);
} }
onEvent<T extends OB11EmitEventContent>(event: T) { onEvent<T extends OB11EmitEventContent>(event: T) {
@@ -35,20 +36,23 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
if (this.connection) { if (this.connection) {
return; return;
} }
this.heartbeatRef = setInterval(() => { if (this.config.heartInterval > 0) {
if (this.connection && this.connection.readyState === WebSocket.OPEN) { this.heartbeatRef = setInterval(() => {
this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatIntervalInMillis, this.core.selfInfo.online ?? true, true))); if (this.connection && this.connection.readyState === WebSocket.OPEN) {
} this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.config.heartInterval, this.core.selfInfo.online ?? true, true)));
}, this.heartbeatIntervalInMillis); }
}, this.config.heartInterval);
}
this.isEnable = true;
await this.tryConnect(); await this.tryConnect();
} }
close() { close() {
if (this.isClosed) { if (!this.isEnable) {
this.logger.logDebug('Cannot close a closed WebSocket connection'); this.logger.logDebug('Cannot close a closed WebSocket connection');
return; return;
} }
this.isClosed = true; this.isEnable = false;
if (this.connection) { if (this.connection) {
this.connection.close(); this.connection.close();
this.connection = null; this.connection = null;
@@ -66,16 +70,16 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
} }
private async tryConnect() { private async tryConnect() {
if (!this.connection && !this.isClosed) { if (!this.connection && this.isEnable) {
let isClosedByError = false; let isClosedByError = false;
this.connection = new WebSocket(this.url, { this.connection = new WebSocket(this.config.url, {
maxPayload: 1024 * 1024 * 1024, maxPayload: 1024 * 1024 * 1024,
handshakeTimeout: 2000, handshakeTimeout: 2000,
perMessageDeflate: false, perMessageDeflate: false,
headers: { headers: {
'X-Self-ID': this.core.selfInfo.uin, 'X-Self-ID': this.core.selfInfo.uin,
'Authorization': `Bearer ${this.token}`, 'Authorization': `Bearer ${this.config.token}`,
'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段 'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段
'User-Agent': 'OneBot/11', 'User-Agent': 'OneBot/11',
}, },
@@ -100,21 +104,21 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
}); });
this.connection.once('close', () => { this.connection.once('close', () => {
if (!isClosedByError) { if (!isClosedByError) {
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.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.reconnectIntervalInMillis / 1000)} 秒后尝试重新连接`); this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
if (!this.isClosed) { if (this.isEnable) {
this.connection = null; this.connection = null;
setTimeout(() => this.tryConnect(), this.reconnectIntervalInMillis); setTimeout(() => this.tryConnect(), this.config.reconnectInterval);
} }
} }
}); });
this.connection.on('error', (err) => { this.connection.on('error', (err) => {
isClosedByError = true; isClosedByError = true;
this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.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.reconnectIntervalInMillis / 1000)} 秒后尝试重新连接`); this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`);
if (!this.isClosed) { if (this.isEnable) {
this.connection = null; this.connection = null;
setTimeout(() => this.tryConnect(), this.reconnectIntervalInMillis); setTimeout(() => this.tryConnect(), this.config.reconnectInterval);
} }
}); });
} }
@@ -147,7 +151,46 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
this.checkStateAndReply<any>(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo)); this.checkStateAndReply<any>(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo));
return; return;
} }
const retdata = await action.websocketHandle(receiveData.params, echo ?? ''); const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name);
this.checkStateAndReply<any>({ ...retdata }); this.checkStateAndReply<any>({ ...retdata });
} }
async reload(newConfig: WebsocketClientConfig) {
const wasEnabled = this.isEnable;
const oldUrl = this.config.url;
const oldHeartInterval = this.config.heartInterval;
this.config = newConfig;
if (newConfig.enable && !wasEnabled) {
this.open();
return OB11NetworkReloadType.NetWorkOpen;
} else if (!newConfig.enable && wasEnabled) {
this.close();
return OB11NetworkReloadType.NetWorkClose;
}
if (oldUrl !== newConfig.url) {
this.close();
if (newConfig.enable) {
this.open();
}
return OB11NetworkReloadType.NetWorkReload;
}
if (oldHeartInterval !== newConfig.heartInterval) {
if (this.heartbeatRef) {
clearInterval(this.heartbeatRef);
this.heartbeatRef = null;
}
if (newConfig.heartInterval > 0 && this.isEnable) {
this.heartbeatRef = setInterval(() => {
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, newConfig.heartInterval, this.core.selfInfo.online ?? true, true)));
}
}, newConfig.heartInterval);
}
return OB11NetworkReloadType.NetWorkReload;
}
return OB11NetworkReloadType.Normal;
}
} }

View File

@@ -1,33 +1,60 @@
import { OB11BaseEvent } from '@/onebot/event/OB11BaseEvent'; import { OB11BaseEvent } from '@/onebot/event/OB11BaseEvent';
import { OB11Message } from '@/onebot'; import { OB11Message } from '@/onebot';
import { ActionMap } from '@/onebot/action'; import { ActionMap } from '@/onebot/action';
import { NetworkConfigAdapter } from '../config/config';
export type OB11EmitEventContent = OB11BaseEvent | OB11Message; export type OB11EmitEventContent = OB11BaseEvent | OB11Message;
export enum OB11NetworkReloadType {
Normal = 0,
ConfigChange = 1,
NetWorkReload = 2,
NetWorkClose = 3,
NetWorkOpen = 4
}
export interface IOB11NetworkAdapter { export interface IOB11NetworkAdapter {
actions?: ActionMap; actions: ActionMap;
name: string;
isEnable: boolean;
config: NetworkConfigAdapter;
onEvent<T extends OB11EmitEventContent>(event: T): void; onEvent<T extends OB11EmitEventContent>(event: T): void;
open(): void | Promise<void>; open(): void | Promise<void>;
close(): void | Promise<void>; close(): void | Promise<void>;
reload(config: any): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
} }
export class OB11NetworkManager { export class OB11NetworkManager {
adapters: IOB11NetworkAdapter[] = []; adapters: Map<string, IOB11NetworkAdapter> = new Map();
async openAllAdapters() { async openAllAdapters() {
return Promise.all(this.adapters.map(adapter => adapter.open())); return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.open()));
} }
async emitEvent(event: OB11EmitEventContent) { async emitEvent(event: OB11EmitEventContent) {
//console.log('adapters', this.adapters.length); return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.onEvent(event)));
return Promise.all(this.adapters.map(adapter => adapter.onEvent(event)));
} }
async emitEventByName(names: string[], event: OB11EmitEventContent) {
return Promise.all(names.map(name => {
const adapter = this.adapters.get(name);
if (adapter) {
return adapter.onEvent(event);
}
}));
}
async emitEventByNames(map: Map<string, OB11EmitEventContent>) {
return Promise.all(Array.from(map.entries()).map(([name, event]) => {
const adapter = this.adapters.get(name);
if (adapter) {
return adapter.onEvent(event);
}
}));
}
registerAdapter(adapter: IOB11NetworkAdapter) { registerAdapter(adapter: IOB11NetworkAdapter) {
this.adapters.push(adapter); this.adapters.set(adapter.name, adapter);
} }
async registerAdapterAndOpen(adapter: IOB11NetworkAdapter) { async registerAdapterAndOpen(adapter: IOB11NetworkAdapter) {
@@ -36,24 +63,38 @@ export class OB11NetworkManager {
} }
async closeSomeAdapters(adaptersToClose: IOB11NetworkAdapter[]) { async closeSomeAdapters(adaptersToClose: IOB11NetworkAdapter[]) {
this.adapters = this.adapters.filter(adapter => !adaptersToClose.includes(adapter)); for (const adapter of adaptersToClose) {
await Promise.all(adaptersToClose.map(adapter => adapter.close())); this.adapters.delete(adapter.name);
await adapter.close();
}
}
findSomeAdapter(name: string) {
return this.adapters.get(name);
} }
/**
* Close all adapters that satisfy the predicate.
*/
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter) => boolean) { async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter) => boolean) {
await this.closeSomeAdapters(this.adapters.filter(closeFilter)); const adaptersToClose = Array.from(this.adapters.values()).filter(closeFilter);
await this.closeSomeAdapters(adaptersToClose);
} }
async closeAllAdapters() { async closeAllAdapters() {
await Promise.all(this.adapters.map(adapter => adapter.close())); await Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.close()));
this.adapters = []; this.adapters.clear();
}
async readloadAdapter<T>(name: string, config: T) {
const adapter = this.adapters.get(name);
if (adapter) {
await adapter.reload(config);
}
}
async readloadSomeAdapters<T>(configMap: Map<string, T>) {
await Promise.all(Array.from(configMap.entries()).map(([name, config]) => this.readloadAdapter(name, config)));
} }
} }
export * from './active-http'; export * from './active-http';
export * from './active-websocket'; export * from './active-websocket';
export * from './passive-http'; export * from './passive-http';
export * from './passive-websocket'; export * from './passive-websocket';

View File

@@ -1,22 +1,25 @@
import { IOB11NetworkAdapter } from './index'; import { IOB11NetworkAdapter, OB11NetworkReloadType } from './index';
import express, { Express, Request, Response } from 'express'; import express, { Express, Request, Response } from 'express';
import http from 'http'; import http from 'http';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
import { OB11Response } from '../action/OB11Response'; import { OB11Response } from '../action/OB11Response';
import { ActionMap } from '@/onebot/action'; import { ActionMap } from '@/onebot/action';
import cors from 'cors'; import cors from 'cors';
import { HttpServerConfig } from '../config/config';
export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
private app: Express | undefined; private app: Express | undefined;
private server: http.Server | undefined; private server: http.Server | undefined;
private isOpen: boolean = false; isEnable: boolean = false;
public config: HttpServerConfig;
constructor( constructor(
public port: number, public name: string,
public token: string, config: HttpServerConfig,
public core: NapCatCore, public core: NapCatCore,
public actions: ActionMap, public actions: ActionMap,
) { ) {
this.config = structuredClone(config);
} }
onEvent() { onEvent() {
@@ -25,13 +28,13 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
open() { open() {
try { try {
if (this.isOpen) { if (this.isEnable) {
this.core.context.logger.logError('Cannot open a closed HTTP server'); this.core.context.logger.logError('Cannot open a closed HTTP server');
return; return;
} }
if (!this.isOpen) { if (!this.isEnable) {
this.initializeServer(); this.initializeServer();
this.isOpen = true; this.isEnable = true;
} }
} catch (e) { } catch (e) {
this.core.context.logger.logError(`[OneBot] [HTTP Server Adapter] Boot Error: ${e}`); this.core.context.logger.logError(`[OneBot] [HTTP Server Adapter] Boot Error: ${e}`);
@@ -40,7 +43,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
} }
async close() { async close() {
this.isOpen = false; this.isEnable = false;
this.server?.close(); this.server?.close();
this.app = undefined; this.app = undefined;
} }
@@ -63,12 +66,12 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
}); });
}); });
this.app.use((req, res, next) => this.authorize(this.token, req, res, next)); this.app.use((req, res, next) => this.authorize(this.config.token, req, res, next));
this.app.use(async (req, res, _) => { this.app.use(async (req, res, _) => {
await this.handleRequest(req, res); await this.handleRequest(req, res);
}); });
this.server.listen(this.port, () => { this.server.listen(this.config.port, () => {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.port}`); this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.config.port}`);
}); });
} }
@@ -85,7 +88,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
} }
private async handleRequest(req: Request, res: Response) { private async handleRequest(req: Request, res: Response) {
if (!this.isOpen) { if (!this.isEnable) {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Server is closed`); this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Server is closed`);
return res.json(OB11Response.error('Server is closed', 200)); return res.json(OB11Response.error('Server is closed', 200));
} }
@@ -101,7 +104,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
const action = this.actions.get(actionName); const action = this.actions.get(actionName);
if (action) { if (action) {
try { try {
const result = await action.handle(payload); const result = await action.handle(payload, this.name);
return res.json(result); return res.json(result);
} catch (error: any) { } catch (error: any) {
return res.json(OB11Response.error(error?.stack?.toString() || error?.message || 'Error Handle', 200)); return res.json(OB11Response.error(error?.stack?.toString() || error?.message || 'Error Handle', 200));
@@ -110,4 +113,28 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
return res.json(OB11Response.error('不支持的api ' + actionName, 200)); return res.json(OB11Response.error('不支持的api ' + actionName, 200));
} }
} }
async reload(newConfig: HttpServerConfig) {
const wasEnabled = this.isEnable;
const oldPort = this.config.port;
this.config = newConfig;
if (newConfig.enable && !wasEnabled) {
this.open();
return OB11NetworkReloadType.NetWorkOpen;
} else if (!newConfig.enable && wasEnabled) {
this.close();
return OB11NetworkReloadType.NetWorkClose;
}
if (oldPort !== newConfig.port) {
this.close();
if (newConfig.enable) {
this.open();
}
return OB11NetworkReloadType.NetWorkReload;
}
return OB11NetworkReloadType.Normal;
}
} }

View File

@@ -1,4 +1,4 @@
import { IOB11NetworkAdapter, OB11EmitEventContent } from './index'; import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from './index';
import urlParse from 'url'; import urlParse from 'url';
import { WebSocket, WebSocketServer } from 'ws'; import { WebSocket, WebSocketServer } from 'ws';
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
@@ -10,43 +10,43 @@ import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent';
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
import { ActionMap } from '@/onebot/action'; import { ActionMap } from '@/onebot/action';
import { LifeCycleSubType, OB11LifeCycleEvent } from '../event/meta/OB11LifeCycleEvent'; import { LifeCycleSubType, OB11LifeCycleEvent } from '../event/meta/OB11LifeCycleEvent';
import { WebsocketServerConfig } from '../config/config';
export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter { export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
wsServer: WebSocketServer; wsServer: WebSocketServer;
wsClients: WebSocket[] = []; wsClients: WebSocket[] = [];
wsClientsMutex = new Mutex(); wsClientsMutex = new Mutex();
isOpen: boolean = false; isEnable: boolean = false;
hasBeenClosed: boolean = false;
heartbeatInterval: number = 0; heartbeatInterval: number = 0;
core: NapCatCore;
logger: LogWrapper; logger: LogWrapper;
public config: WebsocketServerConfig;
private heartbeatIntervalId: NodeJS.Timeout | null = null; private heartbeatIntervalId: NodeJS.Timeout | null = null;
wsClientWithEvent: WebSocket[] = []; wsClientWithEvent: WebSocket[] = [];
constructor( constructor(
ip: string, public name: string,
port: number, config: WebsocketServerConfig,
heartbeatInterval: number, public core: NapCatCore,
token: string,
core: NapCatCore,
public actions: ActionMap, public actions: ActionMap,
) { ) {
this.core = core; this.config = structuredClone(config);
this.logger = core.context.logger; this.logger = core.context.logger;
if (this.config.host === '0.0.0.0') {
this.heartbeatInterval = heartbeatInterval; //兼容配置同时处理0.0.0.0逻辑
this.config.host = '';
}
this.wsServer = new WebSocketServer({ this.wsServer = new WebSocketServer({
port: port, port: this.config.port,
host: ip, host: this.config.host,
maxPayload: 1024 * 1024 * 1024, maxPayload: 1024 * 1024 * 1024,
}); });
this.wsServer.on('connection', async (wsClient, wsReq) => { this.wsServer.on('connection', async (wsClient, wsReq) => {
if (!this.isOpen) { if (!this.isEnable) {
wsClient.close(); wsClient.close();
return; return;
} }
//鉴权 //鉴权
this.authorize(token, wsClient, wsReq); this.authorize(this.config.token, wsClient, wsReq);
const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url; const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
const isApiConnect = paramUrl === '/api' || paramUrl === '/api/'; const isApiConnect = paramUrl === '/api' || paramUrl === '/api/';
if (!isApiConnect) { if (!isApiConnect) {
@@ -102,23 +102,22 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
} }
open() { open() {
if (this.isOpen) { if (this.isEnable) {
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);
this.isOpen = true; this.isEnable = true;
this.registerHeartBeat(); if (this.heartbeatInterval > 0) {
this.registerHeartBeat();
}
} }
async close() { async close() {
this.isOpen = false; this.isEnable = false;
this.wsServer.close((err) => { this.wsServer.close((err) => {
if (err) { if (err) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Error closing server:', err.message); this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Error closing server:', err.message);
@@ -188,8 +187,51 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
this.checkStateAndReply<any>(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo), wsClient); this.checkStateAndReply<any>(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo), wsClient);
return; return;
} }
const retdata = await action.websocketHandle(receiveData.params, echo ?? ''); const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name);
this.checkStateAndReply<any>({ ...retdata }, wsClient); this.checkStateAndReply<any>({ ...retdata }, wsClient);
} }
async reload(newConfig: WebsocketServerConfig) {
const wasEnabled = this.isEnable;
const oldPort = this.config.port;
const oldHost = this.config.host;
const oldHeartbeatInterval = this.heartbeatInterval;
this.config = newConfig;
if (newConfig.enable && !wasEnabled) {
this.open();
return OB11NetworkReloadType.NetWorkOpen;
} else if (!newConfig.enable && wasEnabled) {
this.close();
return OB11NetworkReloadType.NetWorkClose;
}
if (oldPort !== newConfig.port || oldHost !== newConfig.host) {
this.close();
this.wsServer = new WebSocketServer({
port: newConfig.port,
host: newConfig.host === '0.0.0.0' ? '' : newConfig.host,
maxPayload: 1024 * 1024 * 1024,
});
if (newConfig.enable) {
this.open();
}
return OB11NetworkReloadType.NetWorkReload;
}
if (oldHeartbeatInterval !== newConfig.heartInterval) {
if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null;
}
this.heartbeatInterval = newConfig.heartInterval;
if (newConfig.heartInterval > 0 && this.isEnable) {
this.registerHeartBeat();
}
return OB11NetworkReloadType.NetWorkReload;
}
return OB11NetworkReloadType.Normal;
}
} }

View File

@@ -36,26 +36,34 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// 配置静态文件服务,提供./static目录下的文件服务访问路径为/webui // 配置静态文件服务,提供./static目录下的文件服务访问路径为/webui
app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath)); app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath));
//挂载API接口 //挂载API接口
// 添加CORS支持
// TODO:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
next();
});
app.use(config.prefix + '/api', ALLRouter); app.use(config.prefix + '/api', ALLRouter);
app.listen(config.port, config.host, async () => { app.listen(config.port, config.host, async () => {
log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`); log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`);
log(`[NapCat] [WebUi] Login Token is ${config.token}`); log(`[NapCat] [WebUi] Login Token is ${config.token}`);
log(`[NapCat] [WebUi] WebUi User Panel Url: http://${config.host}:${config.port}${config.prefix}/webui?token=${config.token}`); log(
log(`[NapCat] [WebUi] WebUi Local Panel Url: http://127.0.0.1:${config.port}${config.prefix}/webui?token=${config.token}`); `[NapCat] [WebUi] WebUi User Panel Url: http://${config.host}:${config.port}${config.prefix}/webui?token=${config.token}`
);
log(
`[NapCat] [WebUi] WebUi Local Panel Url: http://127.0.0.1:${config.port}${config.prefix}/webui?token=${config.token}`
);
//获取上网Ip //获取上网Ip
//https://www.ip.cn/api/index?ip&type=0 //https://www.ip.cn/api/index?ip&type=0
RequestUtil.HttpGetJson<{ IP: {IP:string} }>( RequestUtil.HttpGetJson<{ IP: { IP: string } }>('https://ip.011102.xyz/', 'GET', {}, {}, true, true)
'https://ip.011102.xyz/', .then((data) => {
'GET', log(
{}, `[NapCat] [WebUi] WebUi Publish Panel Url: http://${data.IP.IP}:${config.port}${config.prefix}/webui/?token=${config.token}`
{}, );
true, })
true .catch((err) => {
).then((data) => { logger.logError.bind(logger)(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
log(`[NapCat] [WebUi] WebUi Publish Panel Url: http://${data.IP.IP}:${config.port}${config.prefix}/webui/?token=${config.token}`); });
}).catch((err) => {
logger.logError.bind(logger)(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
});
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More