mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
144 Commits
protobuf-d
...
base-rpc-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8243abaf0c | ||
![]() |
25be976fc9 | ||
![]() |
a8d8a94309 | ||
![]() |
eb9209cffb | ||
![]() |
06bc761dd3 | ||
![]() |
8994d3af14 | ||
![]() |
5eefd3dbe8 | ||
![]() |
2be014a9f2 | ||
![]() |
ee4c9e95ad | ||
![]() |
3e25172450 | ||
![]() |
ac51c50046 | ||
![]() |
a2cae1734b | ||
![]() |
88e9caddfa | ||
![]() |
f576cd9417 | ||
![]() |
9cfd224b74 | ||
![]() |
c12f8de8b4 | ||
![]() |
ed9a7c52e2 | ||
![]() |
38fcaaa28b | ||
![]() |
5317a1c1a9 | ||
![]() |
4bc5933ea2 | ||
![]() |
6a6bd33fe5 | ||
![]() |
8256942a3d | ||
![]() |
697632eee8 | ||
![]() |
6bbf5b254d | ||
![]() |
5831898c4a | ||
![]() |
2cc413bec1 | ||
![]() |
0af36e89d9 | ||
![]() |
b2c0f5d2e5 | ||
![]() |
80b74c7da9 | ||
![]() |
f14f13b158 | ||
![]() |
9dda00b6fa | ||
![]() |
a29debb738 | ||
![]() |
b990fc43df | ||
![]() |
915e9552ee | ||
![]() |
c522e0a386 | ||
![]() |
c9cc08a9ba | ||
![]() |
66e1b1662f | ||
![]() |
9372e83bd8 | ||
![]() |
b38a240dbb | ||
![]() |
76b9506395 | ||
![]() |
f1cf636aa2 | ||
![]() |
312dcd0e13 | ||
![]() |
42c2419613 | ||
![]() |
8f7f748e82 | ||
![]() |
7ad3bad1be | ||
![]() |
5cd682e69f | ||
![]() |
5d57780e84 | ||
![]() |
f399955204 | ||
![]() |
770652fe6b | ||
![]() |
9ed5fa8c67 | ||
![]() |
5a4ad29727 | ||
![]() |
1eda3f2e33 | ||
![]() |
95cb95ef96 | ||
![]() |
4e7c96634c | ||
![]() |
58587b8aea | ||
![]() |
3fbf6239db | ||
![]() |
faec53d497 | ||
![]() |
482dcc534e | ||
![]() |
854f61dda6 | ||
![]() |
fca38713a1 | ||
![]() |
5dd3bade53 | ||
![]() |
665360f48d | ||
![]() |
65719cb56a | ||
![]() |
bdb76d4639 | ||
![]() |
15634412ef | ||
![]() |
bbcf9649fa | ||
![]() |
e845d7314e | ||
![]() |
6927b1c94f | ||
![]() |
a09c6acd0d | ||
![]() |
0963650ccb | ||
![]() |
380688b353 | ||
![]() |
ad5466bff8 | ||
![]() |
a83652bf3f | ||
![]() |
c632de314d | ||
![]() |
259c9610d5 | ||
![]() |
e9936c5524 | ||
![]() |
3f60440e72 | ||
![]() |
71a15f92fb | ||
![]() |
32bc0dd820 | ||
![]() |
20d1ac9d01 | ||
![]() |
18baf89e0e | ||
![]() |
3a1d1f2e59 | ||
![]() |
e9a048721d | ||
![]() |
68f0c7ff1a | ||
![]() |
2875fe94ea | ||
![]() |
1870427c0f | ||
![]() |
636568fd30 | ||
![]() |
bbc2391bf8 | ||
![]() |
401684542a | ||
![]() |
870edb2513 | ||
![]() |
7ad09169ea | ||
![]() |
c1a0f8915b | ||
![]() |
dcdab8e5a1 | ||
![]() |
eb3278fdab | ||
![]() |
34db3af48d | ||
![]() |
198da960dd | ||
![]() |
cb83918fb3 | ||
![]() |
f59a48540b | ||
![]() |
ccf9c1a5fb | ||
![]() |
ba6a85142a | ||
![]() |
440baccd2a | ||
![]() |
690c073328 | ||
![]() |
3f0730ed4f | ||
![]() |
01d5663bc8 | ||
![]() |
49806cd00e | ||
![]() |
935b0848e5 | ||
![]() |
5ca20a89a2 | ||
![]() |
e89a2266ec | ||
![]() |
6607533311 | ||
![]() |
4057054220 | ||
![]() |
055e43845e | ||
![]() |
d67270f2f8 | ||
![]() |
d061b6c190 | ||
![]() |
945f87d77f | ||
![]() |
6c9be52d39 | ||
![]() |
98e347f010 | ||
![]() |
607e367bb1 | ||
![]() |
7a25dc1ef1 | ||
![]() |
e22ec4be09 | ||
![]() |
51a06622f9 | ||
![]() |
22faf5b831 | ||
![]() |
e781c662b2 | ||
![]() |
5744698d24 | ||
![]() |
2c2ab3cd48 | ||
![]() |
cfae4f5acd | ||
![]() |
de541e3249 | ||
![]() |
f5187c5c01 | ||
![]() |
9936279443 | ||
![]() |
2818773fd4 | ||
![]() |
b9293cbcd0 | ||
![]() |
5b9e44ddfc | ||
![]() |
1791accab7 | ||
![]() |
08081360f3 | ||
![]() |
e933a95e97 | ||
![]() |
4ef457fe6f | ||
![]() |
bd9cae8921 | ||
![]() |
303a74f8fd | ||
![]() |
0b7f126ce1 | ||
![]() |
308b5c027f | ||
![]() |
ed3abc4b43 | ||
![]() |
87ecb3b380 | ||
![]() |
7e31763a25 | ||
![]() |
c9df57d16a | ||
![]() |
3d0f8ee657 |
11
README.md
11
README.md
@@ -1,8 +1,9 @@
|
|||||||
|
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# NapCat
|
# NapCat
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
_Modern protocol-side framework implemented based on NTQQ._
|
_Modern protocol-side framework implemented based on NTQQ._
|
||||||
|
|
||||||
@@ -50,6 +51,10 @@ _Modern protocol-side framework implemented based on NTQQ._
|
|||||||
|
|
||||||
+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||||
|
|
||||||
|
+ [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
|
||||||
|
|
||||||
|
+ [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
|
||||||
|
|
||||||
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
|
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -61,7 +66,3 @@ _Modern protocol-side framework implemented based on NTQQ._
|
|||||||
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
|
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
|
||||||
|
|
||||||
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
|
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
|
||||||
|
|
||||||
## Warnings
|
|
||||||
|
|
||||||
[某框架抄袭部分分析](https://napneko.github.io/other/about-copy)
|
|
||||||
|
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
BIN
external/fonts/AaCute.ttf
vendored
BIN
external/fonts/AaCute.ttf
vendored
Binary file not shown.
BIN
external/fonts/JetBrainsMono.ttf
vendored
BIN
external/fonts/JetBrainsMono.ttf
vendored
Binary file not shown.
BIN
external/fonts/post.jpg
vendored
BIN
external/fonts/post.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 695 KiB |
BIN
external/logo.png
vendored
BIN
external/logo.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 250 KiB |
Binary file not shown.
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "qq-chat",
|
"name": "qq-chat",
|
||||||
"version": "9.9.18-32869",
|
"version": "9.9.19-34740",
|
||||||
"verHash": "e735296c",
|
"verHash": "f31348f2",
|
||||||
"linuxVersion": "3.2.16-32869",
|
"linuxVersion": "3.2.17-34740",
|
||||||
"linuxVerHash": "4c192ba9",
|
"linuxVerHash": "5aa2d8d6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "QQ",
|
"description": "QQ",
|
||||||
"productName": "QQ",
|
"productName": "QQ",
|
||||||
@@ -16,27 +16,10 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"qd": "externals/devtools/cli/index.js"
|
"qd": "externals/devtools/cli/index.js"
|
||||||
},
|
},
|
||||||
"appid": {
|
|
||||||
"win32": "537258389",
|
|
||||||
"darwin": "537258412",
|
|
||||||
"linux": "537258424"
|
|
||||||
},
|
|
||||||
"main": "./loadNapCat.js",
|
"main": "./loadNapCat.js",
|
||||||
"peerDependenciesMeta": {
|
"buildVersion": "34740",
|
||||||
"*": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"patchedDependencies": {
|
|
||||||
"@vue/runtime-dom@3.5.12": "patches/@vue__runtime-dom@3.5.12.patch",
|
|
||||||
"@swc/helpers@0.5.3": "patches/@swc__helpers@0.5.3.patch",
|
|
||||||
"vuex@4.1.0": "patches/vuex@4.1.0.patch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"buildVersion": "32869",
|
|
||||||
"isPureShell": true,
|
"isPureShell": true,
|
||||||
"isByteCodeShell": true,
|
"isByteCodeShell": true,
|
||||||
"platform": "win32",
|
"platform": "win32",
|
||||||
"eleArch": "x64"
|
"eleArch": "x64"
|
||||||
}
|
}
|
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.7.45",
|
"version": "4.7.78",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@@ -92,7 +92,9 @@ const MusicInsert = () => {
|
|||||||
className="w-96"
|
className="w-96"
|
||||||
fullWidth
|
fullWidth
|
||||||
selectedKey={mode}
|
selectedKey={mode}
|
||||||
onSelectionChange={setMode}
|
onSelectionChange={(key) => {
|
||||||
|
if (key !== null) setMode(key)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
|
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
|
||||||
<Select
|
<Select
|
||||||
|
@@ -26,7 +26,7 @@ const itemVariants = {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
transition: { type: 'spring', stiffness: 300, damping: 20 }
|
transition: { type: 'spring' as const, stiffness: 300, damping: 20 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,9 +24,7 @@ const oneBotHttpApiGroup = {
|
|||||||
},
|
},
|
||||||
'/get_group_system_msg': {
|
'/get_group_system_msg': {
|
||||||
description: '获取群系统消息',
|
description: '获取群系统消息',
|
||||||
request: z.object({
|
request: z.object({}),
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
|
||||||
}),
|
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
InvitedRequest: z
|
InvitedRequest: z
|
||||||
@@ -37,6 +35,7 @@ const oneBotHttpApiGroup = {
|
|||||||
invitor_uin: z.string().describe('邀请人 QQ 号'),
|
invitor_uin: z.string().describe('邀请人 QQ 号'),
|
||||||
invitor_nick: z.string().describe('邀请人昵称'),
|
invitor_nick: z.string().describe('邀请人昵称'),
|
||||||
group_id: z.string().describe('群号'),
|
group_id: z.string().describe('群号'),
|
||||||
|
message: z.string().describe('入群回答'),
|
||||||
group_name: z.string().describe('群名称'),
|
group_name: z.string().describe('群名称'),
|
||||||
checked: z.boolean().describe('是否已处理'),
|
checked: z.boolean().describe('是否已处理'),
|
||||||
actor: z.string().describe('处理人 QQ 号')
|
actor: z.string().describe('处理人 QQ 号')
|
||||||
@@ -50,6 +49,7 @@ const oneBotHttpApiGroup = {
|
|||||||
requester_uin: z.string().describe('请求人 QQ 号'),
|
requester_uin: z.string().describe('请求人 QQ 号'),
|
||||||
requester_nick: z.string().describe('请求人昵称'),
|
requester_nick: z.string().describe('请求人昵称'),
|
||||||
group_id: z.string().describe('群号'),
|
group_id: z.string().describe('群号'),
|
||||||
|
message: z.string().describe('入群回答'),
|
||||||
group_name: z.string().describe('群名称'),
|
group_name: z.string().describe('群名称'),
|
||||||
checked: z.boolean().describe('是否已处理'),
|
checked: z.boolean().describe('是否已处理'),
|
||||||
actor: z.string().describe('处理人 QQ 号')
|
actor: z.string().describe('处理人 QQ 号')
|
||||||
@@ -604,7 +604,7 @@ const oneBotHttpApiGroup = {
|
|||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z
|
data: z
|
||||||
.object({
|
.object({
|
||||||
group_id: z.string().describe('群号'),
|
group_id: z.number().describe('群号'),
|
||||||
current_talkative: z
|
current_talkative: z
|
||||||
.object({
|
.object({
|
||||||
user_id: z.number().describe('QQ 号'),
|
user_id: z.number().describe('QQ 号'),
|
||||||
|
@@ -56,9 +56,9 @@ export default function TerminalPage() {
|
|||||||
|
|
||||||
setTabs((prev) => [...prev, newTab])
|
setTabs((prev) => [...prev, newTab])
|
||||||
setSelectedTab(id)
|
setSelectedTab(id)
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Failed to create terminal:', error)
|
console.error('Failed to create terminal:', error)
|
||||||
toast.error('创建终端失败')
|
toast.error((error as Error).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
package.json
22
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.7.45",
|
"version": "4.7.78",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||||
@@ -35,41 +35,35 @@
|
|||||||
"@types/qrcode-terminal": "^0.12.2",
|
"@types/qrcode-terminal": "^0.12.2",
|
||||||
"@types/react-color": "^3.0.13",
|
"@types/react-color": "^3.0.13",
|
||||||
"@types/type-is": "^1.6.7",
|
"@types/type-is": "^1.6.7",
|
||||||
"@types/wordcloud": "^1.2.2",
|
|
||||||
"@types/ws": "^8.5.12",
|
"@types/ws": "^8.5.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||||
"@typescript-eslint/parser": "^8.3.0",
|
"@typescript-eslint/parser": "^8.3.0",
|
||||||
"ajv": "^8.13.0",
|
"ajv": "^8.13.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"commander": "^13.0.0",
|
"commander": "^13.0.0",
|
||||||
|
"compressing": "^1.10.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.25.5",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-import-resolver-typescript": "^4.0.0",
|
"eslint-import-resolver-typescript": "^4.0.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"fast-xml-parser": "^4.3.6",
|
"fast-xml-parser": "^4.3.6",
|
||||||
"file-type": "^20.0.0",
|
"file-type": "^21.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"image-size": "^1.1.1",
|
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^2.0.1",
|
||||||
|
"napcat.protobuf": "^1.1.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.13.0",
|
"typescript-eslint": "^8.13.0",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
"vite-plugin-cp": "^6.0.0",
|
"vite-plugin-cp": "^6.0.0",
|
||||||
"vite-tsconfig-paths": "^5.1.0",
|
"vite-tsconfig-paths": "^5.1.0",
|
||||||
"napcat.protobuf": "^1.1.4",
|
"winston": "^3.17.0"
|
||||||
"winston": "^3.17.0",
|
|
||||||
"compressing": "^1.10.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/canvas": "^0.1.67",
|
|
||||||
"@node-rs/jieba": "^2.0.1",
|
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
"napcat.protobuf": "^1.1.2",
|
|
||||||
"silk-wasm": "^3.6.1",
|
"silk-wasm": "^3.6.1",
|
||||||
"wordcloud": "^1.2.3",
|
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -8,11 +8,12 @@ import { pipeline } from 'stream/promises';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
|
|
||||||
const downloadOri = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2025-04-16-12-54/ffmpeg-n7.1.1-6-g48c0f071d4-win64-lgpl-7.1.zip"
|
const downloadOri = "https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip"
|
||||||
const urls = [
|
const urls = [
|
||||||
"https://github.moeyy.xyz/" + downloadOri,
|
"https://github.moeyy.xyz/" + downloadOri,
|
||||||
"https://ghp.ci/" + downloadOri,
|
"https://ghp.ci/" + downloadOri,
|
||||||
"https://gh.api.99988866.xyz/" + downloadOri,
|
"https://gh.api.99988866.xyz/" + downloadOri,
|
||||||
|
"https://gh.api.99988866.xyz/" + downloadOri,
|
||||||
downloadOri
|
downloadOri
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -336,9 +337,16 @@ export async function downloadFFmpegIfNotExists(log: LogWrapper) {
|
|||||||
const ffprobe_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffprobe.exe'));
|
const ffprobe_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffprobe.exe'));
|
||||||
|
|
||||||
if (!ffmpeg_exist || !ffprobe_exist) {
|
if (!ffmpeg_exist || !ffprobe_exist) {
|
||||||
await downloadFFmpeg(path.join(currentPath, 'ffmpeg'), path.join(currentPath, 'cache'), (percentage: number, message: string) => {
|
let url = await downloadFFmpeg(path.join(currentPath, 'ffmpeg'), path.join(currentPath, 'cache'), (percentage: number, message: string) => {
|
||||||
log.log(`[FFmpeg] [Download] ${percentage}% - ${message}`);
|
log.log(`[FFmpeg] [Download] ${percentage}% - ${message}`);
|
||||||
});
|
});
|
||||||
|
if (!url) {
|
||||||
|
log.log('[FFmpeg] [Error] 下载FFmpeg失败');
|
||||||
|
return {
|
||||||
|
path: null,
|
||||||
|
reset: false
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
path: path.join(currentPath, 'ffmpeg'),
|
path: path.join(currentPath, 'ffmpeg'),
|
||||||
reset: true
|
reset: true
|
||||||
|
@@ -10,9 +10,9 @@ interface InternalMapKey {
|
|||||||
checker: ((...args: any[]) => boolean) | undefined;
|
checker: ((...args: any[]) => boolean) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
|
export type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
|
||||||
|
|
||||||
type FuncKeys<T> = Extract<
|
export type FuncKeys<T> = Extract<
|
||||||
{
|
{
|
||||||
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
|
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
|
||||||
}[keyof T],
|
}[keyof T],
|
||||||
|
@@ -4,10 +4,10 @@ import { execFile } from 'child_process';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import type { VideoInfo } from './video';
|
import type { VideoInfo } from './video';
|
||||||
import { fileTypeFromFile } from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import imageSize from 'image-size';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { platform } from 'node:os';
|
import { platform } from 'node:os';
|
||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
|
import { imageSizeFallBack } from '@/image-size';
|
||||||
const currentPath = dirname(fileURLToPath(import.meta.url));
|
const currentPath = dirname(fileURLToPath(import.meta.url));
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const getFFmpegPath = (tool: string): string => {
|
const getFFmpegPath = (tool: string): string => {
|
||||||
@@ -157,7 +157,7 @@ export class FFmpegService {
|
|||||||
try {
|
try {
|
||||||
await this.extractThumbnail(videoPath, thumbnailPath);
|
await this.extractThumbnail(videoPath, thumbnailPath);
|
||||||
// 获取图片尺寸
|
// 获取图片尺寸
|
||||||
const dimensions = imageSize(thumbnailPath);
|
const dimensions = await imageSizeFallBack(thumbnailPath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
format: fileType?.ext ?? 'mp4',
|
format: fileType?.ext ?? 'mp4',
|
||||||
|
@@ -13,11 +13,15 @@ export class NapCatPathWrapper {
|
|||||||
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
|
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
|
||||||
this.binaryPath = mainPath;
|
this.binaryPath = mainPath;
|
||||||
let writePath: string;
|
let writePath: string;
|
||||||
if (os.platform() === 'darwin') {
|
|
||||||
|
if (process.env['NAPCAT_WORKDIR']) {
|
||||||
|
writePath = process.env['NAPCAT_WORKDIR'];
|
||||||
|
} else if (os.platform() === 'darwin') {
|
||||||
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
|
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
|
||||||
} else {
|
} else {
|
||||||
writePath = this.binaryPath;
|
writePath = this.binaryPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logsPath = path.join(writePath, 'logs');
|
this.logsPath = path.join(writePath, 'logs');
|
||||||
this.configPath = path.join(writePath, 'config');
|
this.configPath = path.join(writePath, 'config');
|
||||||
this.cachePath = path.join(writePath, 'cache');
|
this.cachePath = path.join(writePath, 'cache');
|
||||||
|
@@ -20,3 +20,23 @@ export function proxyHandlerOf(logger: LogWrapper) {
|
|||||||
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
|
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
|
||||||
return new Proxy<T>(listener, proxyHandlerOf(logger));
|
return new Proxy<T>(listener, proxyHandlerOf(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function proxyHandlerOfWithoutLogger() {
|
||||||
|
return {
|
||||||
|
get(target: any, prop: any, receiver: any) {
|
||||||
|
if (typeof target[prop] === 'undefined') {
|
||||||
|
// 如果方法不存在,返回一个函数,这个函数调用existentMethod
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
return (..._args: unknown[]) => {
|
||||||
|
console.log(`${target.constructor.name} has no method ${prop}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 如果方法存在,正常返回
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function proxiedListenerOfWithoutLogger<T extends object>(listener: T) {
|
||||||
|
return new Proxy<T>(listener, proxyHandlerOfWithoutLogger());
|
||||||
|
}
|
@@ -109,6 +109,7 @@ export class RequestUtil {
|
|||||||
req.end();
|
req.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求返回都是原始内容
|
// 请求返回都是原始内容
|
||||||
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
|
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
|
||||||
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
||||||
|
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '4.7.45';
|
export const napCatVersion = '4.7.78';
|
||||||
|
@@ -17,8 +17,6 @@ import fs from 'fs';
|
|||||||
import fsPromises from 'fs/promises';
|
import fsPromises from 'fs/promises';
|
||||||
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
||||||
import { fileTypeFromFile } from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import imageSize from 'image-size';
|
|
||||||
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
|
||||||
import { RkeyManager } from '@/core/helper/rkey';
|
import { RkeyManager } from '@/core/helper/rkey';
|
||||||
import { calculateFileMD5 } from '@/common/file';
|
import { calculateFileMD5 } from '@/common/file';
|
||||||
import pathLib from 'node:path';
|
import pathLib from 'node:path';
|
||||||
@@ -28,6 +26,9 @@ import { SendMessageContext } from '@/onebot/api';
|
|||||||
import { getFileTypeForSendType } from '../helper/msg';
|
import { getFileTypeForSendType } from '../helper/msg';
|
||||||
import { FFmpegService } from '@/common/ffmpeg';
|
import { FFmpegService } from '@/common/ffmpeg';
|
||||||
import { rkeyDataType } from '../types/file';
|
import { rkeyDataType } from '../types/file';
|
||||||
|
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
|
import { FileId } from '../packet/transformer/proto/misc/fileid';
|
||||||
|
import { imageSizeFallBack } from '@/image-size';
|
||||||
|
|
||||||
export class NTQQFileApi {
|
export class NTQQFileApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
@@ -63,6 +64,76 @@ export class NTQQFileApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined) {
|
||||||
|
if (this.core.apis.PacketApi.available) {
|
||||||
|
try {
|
||||||
|
if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) {
|
||||||
|
return this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+peer, fileUUID);
|
||||||
|
} else if (file10MMd5 && fileUUID) {
|
||||||
|
return this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(peer, fileUUID, file10MMd5);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.context.logger.logError('获取文件URL失败', (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('fileUUID or file10MMd5 is undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPttUrl(peer: string, fileUUID?: string) {
|
||||||
|
if (this.core.apis.PacketApi.available && fileUUID) {
|
||||||
|
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
|
||||||
|
try {
|
||||||
|
if (appid && appid === 1403) {
|
||||||
|
return this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+peer, {
|
||||||
|
fileUuid: fileUUID,
|
||||||
|
storeId: 1,
|
||||||
|
uploadTime: 0,
|
||||||
|
ttl: 0,
|
||||||
|
subType: 0,
|
||||||
|
});
|
||||||
|
} else if (fileUUID) {
|
||||||
|
return this.core.apis.PacketApi.pkt.operation.GetPttUrl(peer, {
|
||||||
|
fileUuid: fileUUID,
|
||||||
|
storeId: 1,
|
||||||
|
uploadTime: 0,
|
||||||
|
ttl: 0,
|
||||||
|
subType: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.context.logger.logError('获取文件URL失败', (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('packet cant get ptt url');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVideoUrlPacket(peer: string, fileUUID?: string) {
|
||||||
|
if (this.core.apis.PacketApi.available && fileUUID) {
|
||||||
|
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
|
||||||
|
try {
|
||||||
|
if (appid && appid === 1415) {
|
||||||
|
return this.core.apis.PacketApi.pkt.operation.GetGroupVideoUrl(+peer, {
|
||||||
|
fileUuid: fileUUID,
|
||||||
|
storeId: 1,
|
||||||
|
uploadTime: 0,
|
||||||
|
ttl: 0,
|
||||||
|
subType: 0,
|
||||||
|
});
|
||||||
|
} else if (fileUUID) {
|
||||||
|
return this.core.apis.PacketApi.pkt.operation.GetVideoUrl(peer, {
|
||||||
|
fileUuid: fileUUID,
|
||||||
|
storeId: 1,
|
||||||
|
uploadTime: 0,
|
||||||
|
ttl: 0,
|
||||||
|
subType: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.context.logger.logError('获取文件URL失败', (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('packet cant get video url');
|
||||||
|
}
|
||||||
|
|
||||||
async copyFile(filePath: string, destPath: string) {
|
async copyFile(filePath: string, destPath: string) {
|
||||||
await this.core.util.copyFile(filePath, destPath);
|
await this.core.util.copyFile(filePath, destPath);
|
||||||
@@ -137,7 +208,7 @@ export class NTQQFileApi {
|
|||||||
if (fileSize === 0) {
|
if (fileSize === 0) {
|
||||||
throw new Error('文件异常,大小为0');
|
throw new Error('文件异常,大小为0');
|
||||||
}
|
}
|
||||||
const imageSize = await this.core.apis.FileApi.getImageSize(picPath);
|
const imageSize = await imageSizeFallBack(picPath);
|
||||||
context.deleteAfterSentFiles.push(path);
|
context.deleteAfterSentFiles.push(path);
|
||||||
return {
|
return {
|
||||||
elementType: ElementType.PIC,
|
elementType: ElementType.PIC,
|
||||||
@@ -325,6 +396,7 @@ export class NTQQFileApi {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return res.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
|
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
|
||||||
@@ -364,19 +436,6 @@ export class NTQQFileApi {
|
|||||||
return completeRetData.filePath;
|
return completeRetData.filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
imageSize(filePath, (err: Error | null, dimensions) => {
|
|
||||||
if (err) {
|
|
||||||
reject(new Error(err.message));
|
|
||||||
} else if (!dimensions) {
|
|
||||||
reject(new Error('获取图片尺寸失败'));
|
|
||||||
} else {
|
|
||||||
resolve(dimensions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
|
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
|
||||||
const randomResultId = 100000 + Math.floor(Math.random() * 10000);
|
const randomResultId = 100000 + Math.floor(Math.random() * 10000);
|
||||||
|
@@ -10,11 +10,14 @@ import {
|
|||||||
GroupNotify,
|
GroupNotify,
|
||||||
GroupInfoSource,
|
GroupInfoSource,
|
||||||
ShutUpGroupMember,
|
ShutUpGroupMember,
|
||||||
|
Peer,
|
||||||
|
ChatType,
|
||||||
} from '@/core';
|
} from '@/core';
|
||||||
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
||||||
import { LimitedHashTable } from '@/common/message-unique';
|
import { LimitedHashTable } from '@/common/message-unique';
|
||||||
import { NTEventWrapper } from '@/common/event';
|
import { NTEventWrapper } from '@/common/event';
|
||||||
import { CancelableTask, TaskExecutor } from '@/common/cancel-task';
|
import { CancelableTask, TaskExecutor } from '@/common/cancel-task';
|
||||||
|
import { createGroupDetailInfoV2Param, createGroupExtFilter, createGroupExtInfo } from '../data';
|
||||||
|
|
||||||
export class NTQQGroupApi {
|
export class NTQQGroupApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
@@ -47,6 +50,22 @@ export class NTQQGroupApi {
|
|||||||
this.initCache().then().catch(e => this.context.logger.logError(e));
|
this.initCache().then().catch(e => this.context.logger.logError(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createGrayTip(groupCode: string, tip: string) {
|
||||||
|
return this.context.session.getMsgService().addLocalJsonGrayTipMsg(
|
||||||
|
{
|
||||||
|
chatType: ChatType.KCHATTYPEGROUP,
|
||||||
|
peerUid: groupCode,
|
||||||
|
} as Peer,
|
||||||
|
{
|
||||||
|
busiId: 2201,
|
||||||
|
jsonStr: JSON.stringify({ "align": "center", "items": [{ "txt": tip, "type": "nor" }] }),
|
||||||
|
recentAbstract: tip,
|
||||||
|
isServer: false
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
async initCache() {
|
async initCache() {
|
||||||
for (const group of await this.getGroups(true)) {
|
for (const group of await this.getGroups(true)) {
|
||||||
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
|
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
|
||||||
@@ -95,6 +114,58 @@ export class NTQQGroupApi {
|
|||||||
return this.context.session.getGroupService().setHeader(groupCode, filePath);
|
return this.context.session.getGroupService().setHeader(groupCode, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0 0 无需管理员审核
|
||||||
|
// 0 2 需要管理员审核
|
||||||
|
// 1 2 禁止Bot入群( 最好只传一个1 ?)
|
||||||
|
async setGroupRobotAddOption(groupCode: string, robotMemberSwitch?: number, robotMemberExamine?: number) {
|
||||||
|
let extInfo = createGroupExtInfo(groupCode);
|
||||||
|
let groupExtFilter = createGroupExtFilter();
|
||||||
|
if (robotMemberSwitch !== undefined) {
|
||||||
|
extInfo.extInfo.inviteRobotMemberSwitch = robotMemberSwitch;
|
||||||
|
groupExtFilter.inviteRobotMemberSwitch = 1;
|
||||||
|
}
|
||||||
|
if (robotMemberExamine !== undefined) {
|
||||||
|
extInfo.extInfo.inviteRobotMemberExamine = robotMemberExamine;
|
||||||
|
groupExtFilter.inviteRobotMemberExamine = 1;
|
||||||
|
}
|
||||||
|
return this.context.session.getGroupService().modifyGroupExtInfoV2(extInfo, groupExtFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGroupAddOption(groupCode: string, option: {
|
||||||
|
addOption: number;
|
||||||
|
groupQuestion?: string;
|
||||||
|
groupAnswer?: string;
|
||||||
|
}) {
|
||||||
|
let param = createGroupDetailInfoV2Param(groupCode);
|
||||||
|
// 设置要修改的目标
|
||||||
|
param.filter.addOption = 1;
|
||||||
|
if (option.addOption == 4 || option.addOption == 5) {
|
||||||
|
// 4 问题进入答案 5 问题管理员批准
|
||||||
|
param.filter.groupQuestion = 1;
|
||||||
|
param.filter.groupAnswer = option.addOption == 4 ? 1 : 0;
|
||||||
|
param.modifyInfo.groupQuestion = option.groupQuestion || '';
|
||||||
|
param.modifyInfo.groupAnswer = option.addOption == 4 ? option.groupAnswer || '' : '';
|
||||||
|
}
|
||||||
|
param.modifyInfo.addOption = option.addOption;
|
||||||
|
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGroupSearch(groupCode: string, option: {
|
||||||
|
noCodeFingerOpenFlag?: number;
|
||||||
|
noFingerOpenFlag?: number;
|
||||||
|
}) {
|
||||||
|
let param = createGroupDetailInfoV2Param(groupCode);
|
||||||
|
if (option.noCodeFingerOpenFlag) {
|
||||||
|
param.filter.noCodeFingerOpenFlag = 1;
|
||||||
|
param.modifyInfo.noCodeFingerOpenFlag = option.noCodeFingerOpenFlag;
|
||||||
|
}
|
||||||
|
if (option.noFingerOpenFlag) {
|
||||||
|
param.filter.noFingerOpenFlag = 1;
|
||||||
|
param.modifyInfo.noFingerOpenFlag = option.noFingerOpenFlag;
|
||||||
|
}
|
||||||
|
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
|
||||||
|
}
|
||||||
|
|
||||||
async getGroups(forced: boolean = false) {
|
async getGroups(forced: boolean = false) {
|
||||||
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
|
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
|
||||||
'NodeIKernelGroupService/getGroupList',
|
'NodeIKernelGroupService/getGroupList',
|
||||||
|
@@ -17,24 +17,6 @@ export class NTQQMsgApi {
|
|||||||
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
|
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchMsgWithKeywords(keyWords: string[], param: Peer & { searchFields: number, pageLimit: number }) {
|
|
||||||
let outputSearchId = 0;
|
|
||||||
return this.core.eventWrapper.callNormalEventV2(
|
|
||||||
'NodeIKernelSearchService/searchMsgWithKeywords',
|
|
||||||
'NodeIKernelSearchListener/onSearchMsgKeywordsResult',
|
|
||||||
[keyWords, param],
|
|
||||||
(searchId) => {
|
|
||||||
outputSearchId = searchId;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
(event) => {
|
|
||||||
return event.searchId == outputSearchId;
|
|
||||||
},
|
|
||||||
1,
|
|
||||||
5000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
|
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
|
||||||
// https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取
|
// https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取
|
||||||
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
|
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
|
||||||
@@ -89,6 +71,7 @@ export class NTQQMsgApi {
|
|||||||
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
|
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
|
||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: [],
|
filterSendersUid: [],
|
||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
@@ -102,6 +85,7 @@ export class NTQQMsgApi {
|
|||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
|
//searchFields: 3,
|
||||||
filterSendersUid: SendersUid,
|
filterSendersUid: SendersUid,
|
||||||
filterMsgToTime: MsgTime,
|
filterMsgToTime: MsgTime,
|
||||||
filterMsgFromTime: MsgTime,
|
filterMsgFromTime: MsgTime,
|
||||||
@@ -118,6 +102,7 @@ export class NTQQMsgApi {
|
|||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
filterMsgFromTime: '0',
|
filterMsgFromTime: '0',
|
||||||
isReverseOrder: false,
|
isReverseOrder: false,
|
||||||
|
//searchFields: 3,
|
||||||
isIncludeCurrent: true,
|
isIncludeCurrent: true,
|
||||||
pageLimit: 1,
|
pageLimit: 1,
|
||||||
});
|
});
|
||||||
@@ -128,6 +113,7 @@ export class NTQQMsgApi {
|
|||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: [],
|
filterSendersUid: [],
|
||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgFromTime: '0',
|
filterMsgFromTime: '0',
|
||||||
isReverseOrder: true,
|
isReverseOrder: true,
|
||||||
isIncludeCurrent: true,
|
isIncludeCurrent: true,
|
||||||
@@ -146,6 +132,7 @@ export class NTQQMsgApi {
|
|||||||
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
|
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: [],
|
filterSendersUid: [],
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgToTime: filterMsgToTime,
|
filterMsgToTime: filterMsgToTime,
|
||||||
filterMsgFromTime: filterMsgFromTime,
|
filterMsgFromTime: filterMsgFromTime,
|
||||||
isReverseOrder: false,
|
isReverseOrder: false,
|
||||||
@@ -159,6 +146,7 @@ export class NTQQMsgApi {
|
|||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: SendersUid,
|
filterSendersUid: SendersUid,
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
filterMsgFromTime: '0',
|
filterMsgFromTime: '0',
|
||||||
isReverseOrder: true,
|
isReverseOrder: true,
|
||||||
@@ -166,30 +154,7 @@ export class NTQQMsgApi {
|
|||||||
pageLimit: 20000,
|
pageLimit: 20000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async queryFirstMsgBySenderTime(peer: Peer, SendersUid: string[], filterMsgFromTime: string, filterMsgToTime: string) {
|
|
||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
|
|
||||||
chatInfo: peer,
|
|
||||||
filterMsgType: [],
|
|
||||||
filterSendersUid: SendersUid,
|
|
||||||
filterMsgToTime: filterMsgToTime,
|
|
||||||
filterMsgFromTime: filterMsgFromTime,
|
|
||||||
isReverseOrder: true,
|
|
||||||
isIncludeCurrent: true,
|
|
||||||
pageLimit: 20000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async queryFirstMsgByTime(peer: Peer, filterMsgFromTime: string, filterMsgToTime: string) {
|
|
||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
|
|
||||||
chatInfo: peer,
|
|
||||||
filterMsgType: [],
|
|
||||||
filterSendersUid: [],
|
|
||||||
filterMsgToTime: filterMsgToTime,
|
|
||||||
filterMsgFromTime: filterMsgFromTime,
|
|
||||||
isReverseOrder: true,
|
|
||||||
isIncludeCurrent: true,
|
|
||||||
pageLimit: 20000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async setMsgRead(peer: Peer) {
|
async setMsgRead(peer: Peer) {
|
||||||
return this.context.session.getMsgService().setMsgRead(peer);
|
return this.context.session.getMsgService().setMsgRead(peer);
|
||||||
}
|
}
|
||||||
|
@@ -264,7 +264,7 @@ export class NTQQWebApi {
|
|||||||
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
|
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
|
||||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||||
let HonorInfo = {
|
let HonorInfo = {
|
||||||
group_id: groupCode,
|
group_id: Number(groupCode),
|
||||||
current_talkative: {},
|
current_talkative: {},
|
||||||
talkative_list: [],
|
talkative_list: [],
|
||||||
performer_list: [],
|
performer_list: [],
|
||||||
|
245
src/core/data/group.ts
Normal file
245
src/core/data/group.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { GroupDetailInfoV2Param, GroupExtInfo, GroupExtFilter } from "../types";
|
||||||
|
|
||||||
|
export function createGroupDetailInfoV2Param(group_code: string): GroupDetailInfoV2Param {
|
||||||
|
return {
|
||||||
|
groupCode: group_code,
|
||||||
|
filter:
|
||||||
|
{
|
||||||
|
noCodeFingerOpenFlag: 0,
|
||||||
|
noFingerOpenFlag: 0,
|
||||||
|
groupName: 0,
|
||||||
|
classExt: 0,
|
||||||
|
classText: 0,
|
||||||
|
fingerMemo: 0,
|
||||||
|
richFingerMemo: 0,
|
||||||
|
tagRecord: 0,
|
||||||
|
groupGeoInfo:
|
||||||
|
{
|
||||||
|
ownerUid: 0,
|
||||||
|
setTime: 0,
|
||||||
|
cityId: 0,
|
||||||
|
longitude: 0,
|
||||||
|
latitude: 0,
|
||||||
|
geoContent: 0,
|
||||||
|
poiId: 0
|
||||||
|
},
|
||||||
|
groupExtAdminNum: 0,
|
||||||
|
flag: 0,
|
||||||
|
groupMemo: 0,
|
||||||
|
groupAioSkinUrl: 0,
|
||||||
|
groupBoardSkinUrl: 0,
|
||||||
|
groupCoverSkinUrl: 0,
|
||||||
|
groupGrade: 0,
|
||||||
|
activeMemberNum: 0,
|
||||||
|
certificationType: 0,
|
||||||
|
certificationText: 0,
|
||||||
|
groupNewGuideLines:
|
||||||
|
{
|
||||||
|
enabled: 0,
|
||||||
|
content: 0
|
||||||
|
},
|
||||||
|
groupFace: 0,
|
||||||
|
addOption: 0,
|
||||||
|
shutUpTime: 0,
|
||||||
|
groupTypeFlag: 0,
|
||||||
|
appPrivilegeFlag: 0,
|
||||||
|
appPrivilegeMask: 0,
|
||||||
|
groupExtOnly:
|
||||||
|
{
|
||||||
|
tribeId: 0,
|
||||||
|
moneyForAddGroup: 0
|
||||||
|
}, groupSecLevel: 0,
|
||||||
|
groupSecLevelInfo: 0,
|
||||||
|
subscriptionUin: 0,
|
||||||
|
subscriptionUid: "",
|
||||||
|
allowMemberInvite: 0,
|
||||||
|
groupQuestion: 0,
|
||||||
|
groupAnswer: 0,
|
||||||
|
groupFlagExt3: 0,
|
||||||
|
groupFlagExt3Mask: 0,
|
||||||
|
groupOpenAppid: 0,
|
||||||
|
rootId: 0,
|
||||||
|
msgLimitFrequency: 0,
|
||||||
|
hlGuildAppid: 0,
|
||||||
|
hlGuildSubType: 0,
|
||||||
|
hlGuildOrgId: 0,
|
||||||
|
groupFlagExt4: 0,
|
||||||
|
groupFlagExt4Mask: 0,
|
||||||
|
groupSchoolInfo: {
|
||||||
|
location: 0,
|
||||||
|
grade: 0,
|
||||||
|
school: 0
|
||||||
|
},
|
||||||
|
groupCardPrefix:
|
||||||
|
{
|
||||||
|
introduction: 0,
|
||||||
|
rptPrefix: 0
|
||||||
|
}, allianceId: 0,
|
||||||
|
groupFlagPro1: 0,
|
||||||
|
groupFlagPro1Mask: 0
|
||||||
|
},
|
||||||
|
modifyInfo: {
|
||||||
|
noCodeFingerOpenFlag: 0,
|
||||||
|
noFingerOpenFlag: 0,
|
||||||
|
groupName: "",
|
||||||
|
classExt: 0,
|
||||||
|
classText: "",
|
||||||
|
fingerMemo: "",
|
||||||
|
richFingerMemo: "",
|
||||||
|
tagRecord: [],
|
||||||
|
groupGeoInfo: {
|
||||||
|
ownerUid: "",
|
||||||
|
SetTime: 0,
|
||||||
|
CityId: 0,
|
||||||
|
Longitude: "",
|
||||||
|
Latitude: "",
|
||||||
|
GeoContent: "",
|
||||||
|
poiId: ""
|
||||||
|
},
|
||||||
|
groupExtAdminNum: 0,
|
||||||
|
flag: 0,
|
||||||
|
groupMemo: "",
|
||||||
|
groupAioSkinUrl: "",
|
||||||
|
groupBoardSkinUrl: "",
|
||||||
|
groupCoverSkinUrl: "",
|
||||||
|
groupGrade: 0,
|
||||||
|
activeMemberNum: 0,
|
||||||
|
certificationType: 0,
|
||||||
|
certificationText: "",
|
||||||
|
groupNewGuideLines: {
|
||||||
|
enabled: false,
|
||||||
|
content: ""
|
||||||
|
}, groupFace: 0,
|
||||||
|
addOption: 0,
|
||||||
|
shutUpTime: 0,
|
||||||
|
groupTypeFlag: 0,
|
||||||
|
appPrivilegeFlag: 0,
|
||||||
|
appPrivilegeMask: 0,
|
||||||
|
groupExtOnly: {
|
||||||
|
tribeId: 0,
|
||||||
|
moneyForAddGroup: 0
|
||||||
|
},
|
||||||
|
groupSecLevel: 0,
|
||||||
|
groupSecLevelInfo: 0,
|
||||||
|
subscriptionUin: "",
|
||||||
|
subscriptionUid: "",
|
||||||
|
allowMemberInvite: 0,
|
||||||
|
groupQuestion: "",
|
||||||
|
groupAnswer: "",
|
||||||
|
groupFlagExt3: 0,
|
||||||
|
groupFlagExt3Mask: 0,
|
||||||
|
groupOpenAppid: 0,
|
||||||
|
rootId: "",
|
||||||
|
msgLimitFrequency: 0,
|
||||||
|
hlGuildAppid: 0,
|
||||||
|
hlGuildSubType: 0,
|
||||||
|
hlGuildOrgId: 0,
|
||||||
|
groupFlagExt4: 0,
|
||||||
|
groupFlagExt4Mask: 0,
|
||||||
|
groupSchoolInfo: {
|
||||||
|
location: "",
|
||||||
|
grade: 0,
|
||||||
|
school: ""
|
||||||
|
},
|
||||||
|
groupCardPrefix:
|
||||||
|
{
|
||||||
|
introduction: "",
|
||||||
|
rptPrefix: []
|
||||||
|
},
|
||||||
|
allianceId: "",
|
||||||
|
groupFlagPro1: 0,
|
||||||
|
groupFlagPro1Mask: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function createGroupExtInfo(group_code: string): GroupExtInfo {
|
||||||
|
return {
|
||||||
|
groupCode: group_code,
|
||||||
|
resultCode: 0,
|
||||||
|
extInfo: {
|
||||||
|
groupInfoExtSeq: 0,
|
||||||
|
reserve: 0,
|
||||||
|
luckyWordId: '',
|
||||||
|
lightCharNum: 0,
|
||||||
|
luckyWord: '',
|
||||||
|
starId: 0,
|
||||||
|
essentialMsgSwitch: 0,
|
||||||
|
todoSeq: 0,
|
||||||
|
blacklistExpireTime: 0,
|
||||||
|
isLimitGroupRtc: 0,
|
||||||
|
companyId: 0,
|
||||||
|
hasGroupCustomPortrait: 0,
|
||||||
|
bindGuildId: '',
|
||||||
|
groupOwnerId: {
|
||||||
|
memberUin: '',
|
||||||
|
memberUid: '',
|
||||||
|
memberQid: '',
|
||||||
|
},
|
||||||
|
essentialMsgPrivilege: 0,
|
||||||
|
msgEventSeq: '',
|
||||||
|
inviteRobotSwitch: 0,
|
||||||
|
gangUpId: '',
|
||||||
|
qqMusicMedalSwitch: 0,
|
||||||
|
showPlayTogetherSwitch: 0,
|
||||||
|
groupFlagPro1: '',
|
||||||
|
groupBindGuildIds: {
|
||||||
|
guildIds: [],
|
||||||
|
},
|
||||||
|
viewedMsgDisappearTime: '',
|
||||||
|
groupExtFlameData: {
|
||||||
|
switchState: 0,
|
||||||
|
state: 0,
|
||||||
|
dayNums: [],
|
||||||
|
version: 0,
|
||||||
|
updateTime: '',
|
||||||
|
isDisplayDayNum: false,
|
||||||
|
},
|
||||||
|
groupBindGuildSwitch: 0,
|
||||||
|
groupAioBindGuildId: '',
|
||||||
|
groupExcludeGuildIds: {
|
||||||
|
guildIds: [],
|
||||||
|
},
|
||||||
|
fullGroupExpansionSwitch: 0,
|
||||||
|
fullGroupExpansionSeq: '',
|
||||||
|
inviteRobotMemberSwitch: 0,
|
||||||
|
inviteRobotMemberExamine: 0,
|
||||||
|
groupSquareSwitch: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function createGroupExtFilter(): GroupExtFilter {
|
||||||
|
return {
|
||||||
|
groupInfoExtSeq: 0,
|
||||||
|
reserve: 0,
|
||||||
|
luckyWordId: 0,
|
||||||
|
lightCharNum: 0,
|
||||||
|
luckyWord: 0,
|
||||||
|
starId: 0,
|
||||||
|
essentialMsgSwitch: 0,
|
||||||
|
todoSeq: 0,
|
||||||
|
blacklistExpireTime: 0,
|
||||||
|
isLimitGroupRtc: 0,
|
||||||
|
companyId: 0,
|
||||||
|
hasGroupCustomPortrait: 0,
|
||||||
|
bindGuildId: 0,
|
||||||
|
groupOwnerId: 0,
|
||||||
|
essentialMsgPrivilege: 0,
|
||||||
|
msgEventSeq: 0,
|
||||||
|
inviteRobotSwitch: 0,
|
||||||
|
gangUpId: 0,
|
||||||
|
qqMusicMedalSwitch: 0,
|
||||||
|
showPlayTogetherSwitch: 0,
|
||||||
|
groupFlagPro1: 0,
|
||||||
|
groupBindGuildIds: 0,
|
||||||
|
viewedMsgDisappearTime: 0,
|
||||||
|
groupExtFlameData: 0,
|
||||||
|
groupBindGuildSwitch: 0,
|
||||||
|
groupAioBindGuildId: 0,
|
||||||
|
groupExcludeGuildIds: 0,
|
||||||
|
fullGroupExpansionSwitch: 0,
|
||||||
|
fullGroupExpansionSeq: 0,
|
||||||
|
inviteRobotMemberSwitch: 0,
|
||||||
|
inviteRobotMemberExamine: 0,
|
||||||
|
groupSquareSwitch: 0,
|
||||||
|
}
|
||||||
|
};
|
1
src/core/data/index.ts
Normal file
1
src/core/data/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./group";
|
296
src/core/external/appid.json
vendored
296
src/core/external/appid.json
vendored
@@ -1,76 +1,4 @@
|
|||||||
{
|
{
|
||||||
"3.1.2-13107": {
|
|
||||||
"appid": 537146866,
|
|
||||||
"qua": "V1_LNX_NQ_3.1.2-13107_RDM_B"
|
|
||||||
},
|
|
||||||
"3.2.10-25765": {
|
|
||||||
"appid": 537234773,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.10_25765_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-26702": {
|
|
||||||
"appid": 537237950,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_26702_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-26740": {
|
|
||||||
"appid": 537237950,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_26740_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-26909": {
|
|
||||||
"appid": 537237923,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_26909_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-27187": {
|
|
||||||
"appid": 537240645,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_27187_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-27206": {
|
|
||||||
"appid": 537240645,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_27206_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-27254": {
|
|
||||||
"appid": 537240795,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_27254_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.11-24815": {
|
|
||||||
"appid": 537226656,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.11_24815_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.12-25493": {
|
|
||||||
"appid": 537231759,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.12_25493_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.12-25765": {
|
|
||||||
"appid": 537234702,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.12_25765_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.12-26299": {
|
|
||||||
"appid": 537234826,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.12_26299_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.12-26339": {
|
|
||||||
"appid": 537234826,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.12_26339_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.12-26466": {
|
|
||||||
"appid": 537234826,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.12_26466_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.15-26702": {
|
|
||||||
"appid": 537237765,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.15_26702_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.15-26740": {
|
|
||||||
"appid": 537237765,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.15_26740_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-27556": {
|
|
||||||
"appid": 537243600,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_27556_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.12-27597": {
|
|
||||||
"appid": 537243600,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.12_27597_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.15-28060": {
|
"9.9.15-28060": {
|
||||||
"appid": 537246092,
|
"appid": 537246092,
|
||||||
"qua": "V1_WIN_NQ_9.9.15_28060_GW_B"
|
"qua": "V1_WIN_NQ_9.9.15_28060_GW_B"
|
||||||
@@ -355,212 +283,36 @@
|
|||||||
"appid": 537290727,
|
"appid": 537290727,
|
||||||
"qua": "V1_LNX_NQ_3.2.17_34740_GW_B"
|
"qua": "V1_LNX_NQ_3.2.17_34740_GW_B"
|
||||||
},
|
},
|
||||||
"8.9.50-Android": {
|
"9.9.19-34958": {
|
||||||
"appid": 537155551,
|
"appid": 537290742,
|
||||||
"qua": "V1_AND_SQ_8.9.50_3898_YYB_D"
|
"qua": "V1_WIN_NQ_9.9.19_34958_GW_B"
|
||||||
},
|
},
|
||||||
"9.0.1-Watch": {
|
"3.2.17-35184": {
|
||||||
"appid": 537214131,
|
"appid": 537291084,
|
||||||
"qua": "V1_AND_SQ_8.9.68_0_RDM_B"
|
"qua": "V1_LNX_NQ_3.2.17_35184_GW_B"
|
||||||
},
|
},
|
||||||
"6.8.2-21241": {
|
"9.9.19-35184": {
|
||||||
"appid": 537128930,
|
"appid": 537291048,
|
||||||
"qua": "V1_IOS_SQ_6.8.2_21241_YYB_D"
|
"qua": "V1_WIN_NQ_9.9.19_35184_GW_B"
|
||||||
},
|
},
|
||||||
"8.8.88-Android": {
|
"3.2.17-35341": {
|
||||||
"appid": 537118044,
|
"appid": 537291383,
|
||||||
"qua": "V1_IOS_SQ_8.8.88_2770_YYB_D"
|
"qua": "V1_LNX_NQ_3.2.17_35341_GW_B"
|
||||||
},
|
},
|
||||||
"8.9.90-Android": {
|
"9.9.19-35341": {
|
||||||
"appid": 537185007,
|
"appid": 537291347,
|
||||||
"qua": "V1_AND_SQ_8.9.90_4938_YYB_D"
|
"qua": "V1_WIN_NQ_9.9.19_35341_GW_B"
|
||||||
},
|
},
|
||||||
"8.9.33-Android": {
|
"9.9.19-35469": {
|
||||||
"appid": 537151682,
|
"appid": 537291398,
|
||||||
"qua": "V1_AND_SQ_8.9.33_3898_YYB_D"
|
"qua": "V1_WIN_NQ_9.9.19_35469_GW_B"
|
||||||
},
|
},
|
||||||
"3.5.1-Tim": {
|
"3.2.18-35951": {
|
||||||
"appid": 537150355,
|
"appid": 537296013,
|
||||||
"qua": "V1_AND_SQ_8.3.9_351_TIM_D"
|
"qua": "V1_LNX_NQ_3.2.18_35951_GW_B"
|
||||||
},
|
},
|
||||||
"8.9.58-Android": {
|
"9.9.20-35951": {
|
||||||
"appid": 537163194,
|
"appid": 537295977,
|
||||||
"qua": "V1_AND_SQ_8.9.58_4108_YYB_D"
|
"qua": "V1_WIN_NQ_9.9.20_35951_GW_B"
|
||||||
},
|
|
||||||
"8.9.63-Android": {
|
|
||||||
"appid": 537163194,
|
|
||||||
"qua": "V1_AND_SQ_8.9.63_4194_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.68-Android": {
|
|
||||||
"appid": 537168313,
|
|
||||||
"qua": "V1_AND_SQ_8.9.68_4264_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.70-Android": {
|
|
||||||
"appid": 537169928,
|
|
||||||
"qua": "V1_AND_SQ_8.9.70_4330_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.71-Android": {
|
|
||||||
"appid": 537170024,
|
|
||||||
"qua": "V1_AND_SQ_8.9.71_4332_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.73-Android": {
|
|
||||||
"appid": 537171689,
|
|
||||||
"qua": "V1_AND_SQ_8.9.73_4416_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.75-Android": {
|
|
||||||
"appid": 537173381,
|
|
||||||
"qua": "V1_AND_SQ_8.9.75_4482_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.76-Android": {
|
|
||||||
"appid": 537173477,
|
|
||||||
"qua": "V1_AND_SQ_8.9.76_4484_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.78-Android": {
|
|
||||||
"appid": 537175315,
|
|
||||||
"qua": "V1_AND_SQ_8.9.78_4548_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.80-Android": {
|
|
||||||
"appid": 537176863,
|
|
||||||
"qua": "V1_AND_SQ_8.9.80_4614_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.83-Android": {
|
|
||||||
"appid": 537178646,
|
|
||||||
"qua": "V1_AND_SQ_8.9.83_4680_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.85-Android": {
|
|
||||||
"appid": 537180568,
|
|
||||||
"qua": "V1_AND_SQ_8.9.85_4766_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.88-Android": {
|
|
||||||
"appid": 537182769,
|
|
||||||
"qua": "V1_AND_SQ_8.9.88_4852_YYB_D"
|
|
||||||
},
|
|
||||||
"8.9.93-Android": {
|
|
||||||
"appid": 537187398,
|
|
||||||
"qua": "V1_AND_SQ_8.9.93_5028_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.0-Android": {
|
|
||||||
"appid": 537194351,
|
|
||||||
"qua": "V1_AND_SQ_9.0.0_5282_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.8-Android": {
|
|
||||||
"appid": 537200218,
|
|
||||||
"qua": "V1_AND_SQ_9.0.8_5540_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.17-Android": {
|
|
||||||
"appid": 537204056,
|
|
||||||
"qua": "V1_AND_SQ_9.0.17_5712_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.25-Android": {
|
|
||||||
"appid": 537210084,
|
|
||||||
"qua": "V1_AND_SQ_9.0.25_5942_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.35-Android": {
|
|
||||||
"appid": 537215475,
|
|
||||||
"qua": "V1_AND_SQ_9.0.35_6150_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.50-Android": {
|
|
||||||
"appid": 537217916,
|
|
||||||
"qua": "V1_AND_SQ_9.0.50_6258_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.56-Android": {
|
|
||||||
"appid": 537220323,
|
|
||||||
"qua": "V1_AND_SQ_9.0.56_6372_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.60-Android": {
|
|
||||||
"appid": 537222797,
|
|
||||||
"qua": "V1_AND_SQ_9.0.60_6478_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.65-Android": {
|
|
||||||
"appid": 537225139,
|
|
||||||
"qua": "V1_AND_SQ_9.0.65_6588_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.70-Android": {
|
|
||||||
"appid": 537228487,
|
|
||||||
"qua": "V1_AND_SQ_9.0.70_6698_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.81-Android": {
|
|
||||||
"appid": 537233527,
|
|
||||||
"qua": "V1_AND_SQ_9.0.81_6922_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.85-Android": {
|
|
||||||
"appid": 537236316,
|
|
||||||
"qua": "V1_AND_SQ_9.0.85_7068_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.90-Android":{
|
|
||||||
"appid": 537239255,
|
|
||||||
"qua": "V1_AND_SQ_9.0.90_7218_YYB_D"
|
|
||||||
},
|
|
||||||
"9.0.95-Android": {
|
|
||||||
"appid": 537242075,
|
|
||||||
"qua": "V1_AND_SQ_9.0.95_7368_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.0-Android": {
|
|
||||||
"appid": 537244893,
|
|
||||||
"qua": "V1_AND_SQ_9.1.0_7518_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.20-Android": {
|
|
||||||
"appid": 537257414,
|
|
||||||
"qua": "V1_AND_SQ_9.1.20_8198_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.25-Android": {
|
|
||||||
"appid": 537260030,
|
|
||||||
"qua": "V1_AND_SQ_9.1.25_8368_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.67-Android": {
|
|
||||||
"appid": 537284101,
|
|
||||||
"qua": "V1_AND_SQ_9.1.67_9728_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.70-Android": {
|
|
||||||
"appid": 537285947,
|
|
||||||
"qua": "V1_AND_SQ_9.1.70_9898_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.65-Android": {
|
|
||||||
"appid": 537278302,
|
|
||||||
"qua": "V1_AND_SQ_9.1.65_9558_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.60-Android": {
|
|
||||||
"appid": 537275636,
|
|
||||||
"qua": "V1_AND_SQ_9.1.60_9388_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.55-Android": {
|
|
||||||
"appid": 537272835,
|
|
||||||
"qua": "V1_AND_SQ_9.1.55_9218_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.52-Android": {
|
|
||||||
"appid": 537270265,
|
|
||||||
"qua": "V1_AND_SQ_9.1.52_9054_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.50-Android": {
|
|
||||||
"appid": 537270031,
|
|
||||||
"qua": "V1_AND_SQ_9.1.50_9048_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.5-Android": {
|
|
||||||
"appid": 537247779,
|
|
||||||
"qua": "V1_AND_SQ_9.1.5_7688_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.35-Android": {
|
|
||||||
"appid": 537265576,
|
|
||||||
"qua": "V1_AND_SQ_9.1.35_8708_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.31-Android": {
|
|
||||||
"appid": 537262715,
|
|
||||||
"qua": "V1_AND_SQ_9.1.31_8542_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.30-Android": {
|
|
||||||
"appid": 537262559,
|
|
||||||
"qua": "V1_AND_SQ_9.1.30_8538_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.16-Android": {
|
|
||||||
"appid": 537254305,
|
|
||||||
"qua": "V1_AND_SQ_9.1.16_8032_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.15-Android": {
|
|
||||||
"appid": 537254149,
|
|
||||||
"qua": "V1_AND_SQ_9.1.15_8028_YYB_D"
|
|
||||||
},
|
|
||||||
"9.1.10-Android": {
|
|
||||||
"appid": 537251380,
|
|
||||||
"qua": "V1_AND_SQ_9.1.10_7858_YYB_D"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
48
src/core/external/offset.json
vendored
48
src/core/external/offset.json
vendored
@@ -354,5 +354,53 @@
|
|||||||
"9.9.19-34740-x64": {
|
"9.9.19-34740-x64": {
|
||||||
"send": "3BDD8D0",
|
"send": "3BDD8D0",
|
||||||
"recv": "3BE20D0"
|
"recv": "3BE20D0"
|
||||||
|
},
|
||||||
|
"3.2.17-34740-x64": {
|
||||||
|
"send": "ADDF0A0",
|
||||||
|
"recv": "ADE2AC0"
|
||||||
|
},
|
||||||
|
"3.2.17-34740-arm64": {
|
||||||
|
"send": "7753BB8",
|
||||||
|
"recv": "77574E8"
|
||||||
|
},
|
||||||
|
"9.9.19-34958-x64": {
|
||||||
|
"send": "3BDD8D0",
|
||||||
|
"recv": "3BE20D0"
|
||||||
|
},
|
||||||
|
"3.2.17-35184-x64": {
|
||||||
|
"send": "AE0DDE0",
|
||||||
|
"recv": "AE11800"
|
||||||
|
},
|
||||||
|
"3.2.17-35184-arm64": {
|
||||||
|
"send": "7776028",
|
||||||
|
"recv": "7779958"
|
||||||
|
},
|
||||||
|
"9.9.19-35184-x64": {
|
||||||
|
"send": "3BE5A10",
|
||||||
|
"recv": "3BEA210"
|
||||||
|
},
|
||||||
|
"9.9.19-35341-x64": {
|
||||||
|
"send": "3BF1D50",
|
||||||
|
"recv": "3BF6550"
|
||||||
|
},
|
||||||
|
"9.9.19-35469-x64": {
|
||||||
|
"send": "3BF1D50",
|
||||||
|
"recv": "3BF6550"
|
||||||
|
},
|
||||||
|
"3.2.17-35341-x64": {
|
||||||
|
"send": "AE2F700",
|
||||||
|
"recv": "AE33120"
|
||||||
|
},
|
||||||
|
"3.2.17-35341-arm64": {
|
||||||
|
"send": "778D840",
|
||||||
|
"recv": "7791170"
|
||||||
|
},
|
||||||
|
"9.9.20-35951-x64": {
|
||||||
|
"send": "3034BAC",
|
||||||
|
"recv": "3038354"
|
||||||
|
},
|
||||||
|
"3.2.18-35951-x64": {
|
||||||
|
"send": "AFBBB00",
|
||||||
|
"recv": "AFBF520"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -30,6 +30,10 @@ import os from 'node:os';
|
|||||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||||
import { NTQQPacketApi } from './apis/packet';
|
import { NTQQPacketApi } from './apis/packet';
|
||||||
|
import { handleServiceServerOnce, receiverServiceListener, ServiceMethodCommand } from '@/remote/service';
|
||||||
|
import { rpc_decode, rpc_encode } from '@/remote/serialize';
|
||||||
|
import { PipeClient, PipeServer } from '@/remote/pipe';
|
||||||
|
import { RemoteWrapperSession } from '@/remote/remoteSession';
|
||||||
export * from './wrapper';
|
export * from './wrapper';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './services';
|
export * from './services';
|
||||||
@@ -97,9 +101,63 @@ export class NapCatCore {
|
|||||||
constructor(context: InstanceContext, selfInfo: SelfInfo) {
|
constructor(context: InstanceContext, selfInfo: SelfInfo) {
|
||||||
this.selfInfo = selfInfo;
|
this.selfInfo = selfInfo;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
||||||
this.eventWrapper = new NTEventWrapper(context.session);
|
this.eventWrapper = new NTEventWrapper(context.session);
|
||||||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath,NapcatConfigSchema);
|
|
||||||
|
// 管道服务端测试
|
||||||
|
let pipe_server = new PipeServer('//./pipe/napcat');
|
||||||
|
pipe_server.registerHandler(async (packet, helper) => {
|
||||||
|
if (packet.type !== 'event_request') {
|
||||||
|
return helper.error('Invalid packet type');
|
||||||
|
}
|
||||||
|
let event_rpc_data = rpc_decode<{ params: any[] }>(JSON.parse(packet.data));
|
||||||
|
let event_rpc_trace = packet.trace;
|
||||||
|
let event_rpc_command = packet.command as ServiceMethodCommand;
|
||||||
|
let event_rpc_result = await handleServiceServerOnce(event_rpc_command,
|
||||||
|
async (listenerCommand: string, ...args: any[]) => {
|
||||||
|
let listener_data = rpc_encode<{ params: any[] }>({ params: args });
|
||||||
|
helper.sendListenerCallback(listenerCommand, JSON.stringify(rpc_encode(listener_data)));
|
||||||
|
},
|
||||||
|
this.eventWrapper,
|
||||||
|
...event_rpc_data.params
|
||||||
|
);
|
||||||
|
return helper.sendEventResponse(event_rpc_trace, JSON.stringify(rpc_encode(event_rpc_result)));
|
||||||
|
});
|
||||||
|
pipe_server.start().then(() => {
|
||||||
|
this.context.logger.log('Pipe server started successfully');
|
||||||
|
let pipe_client = new PipeClient('//./pipe/napcat');
|
||||||
|
let trace_callback_map = new Map<string, (trace: string, data: any) => void>();
|
||||||
|
pipe_client.registerHandler(async (packet, _helper) => {
|
||||||
|
if (packet.type == 'event_response') {
|
||||||
|
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
|
||||||
|
trace_callback_map.get(packet.trace)?.(packet.trace, event_rpc_data);
|
||||||
|
} else if (packet.type == 'listener_callback') {
|
||||||
|
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
|
||||||
|
await receiverServiceListener(packet.command, ...event_rpc_data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.context.session = new RemoteWrapperSession(async (_serviceClient, serviceCommand, ...args) => {
|
||||||
|
let trace = crypto.randomUUID();
|
||||||
|
return await new Promise((resolve, _reject) => {
|
||||||
|
trace_callback_map.set(trace, (_trace, data) => {
|
||||||
|
//console.log('Received response for trace:', _trace, 'with data:', data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
pipe_client.sendRequest(serviceCommand, JSON.stringify(rpc_encode({ params: args })), trace);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pipe_client.connect().then(() => {
|
||||||
|
this.context.logger.log('Pipe client connected successfully');
|
||||||
|
}).catch((e) => {
|
||||||
|
this.context.logger.logError('Pipe client connection failed: ' + e.message);
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.context.logger.logError('Pipe server start failed: ' + e.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
|
||||||
this.apis = {
|
this.apis = {
|
||||||
FileApi: new NTQQFileApi(this.context, this),
|
FileApi: new NTQQFileApi(this.context, this),
|
||||||
SystemApi: new NTQQSystemApi(this.context, this),
|
SystemApi: new NTQQSystemApi(this.context, this),
|
||||||
@@ -251,13 +309,13 @@ export async function genSessionConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface InstanceContext {
|
export interface InstanceContext {
|
||||||
readonly workingEnv: NapCatCoreWorkingEnv;
|
session: NodeIQQNTWrapperSession;
|
||||||
readonly wrapper: WrapperNodeApi;
|
workingEnv: NapCatCoreWorkingEnv;
|
||||||
readonly session: NodeIQQNTWrapperSession;
|
wrapper: WrapperNodeApi;
|
||||||
readonly logger: LogWrapper;
|
logger: LogWrapper;
|
||||||
readonly loginService: NodeIKernelLoginService;
|
loginService: NodeIKernelLoginService;
|
||||||
readonly basicInfoWrapper: QQBasicInfoWrapper;
|
basicInfoWrapper: QQBasicInfoWrapper;
|
||||||
readonly pathWrapper: NapCatPathWrapper;
|
pathWrapper: NapCatPathWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StableNTApiWrapper {
|
export interface StableNTApiWrapper {
|
||||||
|
@@ -3,44 +3,43 @@ import { BuddyCategoryType, FriendRequestNotify } from '@/core/types';
|
|||||||
export type OnBuddyChangeParams = BuddyCategoryType[];
|
export type OnBuddyChangeParams = BuddyCategoryType[];
|
||||||
|
|
||||||
export class NodeIKernelBuddyListener {
|
export class NodeIKernelBuddyListener {
|
||||||
onBuddyListChangedV2(arg: unknown): any {
|
onBuddyListChangedV2(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddBuddyNeedVerify(arg: unknown): any {
|
onAddBuddyNeedVerify(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddMeSettingChanged(arg: unknown): any {
|
onAddMeSettingChanged(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAvatarUrlUpdated(arg: unknown): any {
|
onAvatarUrlUpdated(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlockChanged(arg: unknown): any {
|
onBlockChanged(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyDetailInfoChange(arg: unknown): any {
|
onBuddyDetailInfoChange(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyInfoChange(arg: unknown): any {
|
onBuddyInfoChange(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyListChange(arg: OnBuddyChangeParams): any {
|
onBuddyListChange(_arg: OnBuddyChangeParams): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyRemarkUpdated(arg: unknown): any {
|
onBuddyRemarkUpdated(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyReqChange(arg: FriendRequestNotify): any {
|
onBuddyReqChange(_arg: FriendRequestNotify): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyReqUnreadCntChange(arg: unknown): any {
|
onBuddyReqUnreadCntChange(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onCheckBuddySettingResult(arg: unknown): any {
|
onCheckBuddySettingResult(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onDelBatchBuddyInfos(arg: unknown): any {
|
onDelBatchBuddyInfos(_arg: unknown): any {
|
||||||
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubtBuddyReqChange(_arg:
|
onDoubtBuddyReqChange(_arg:
|
||||||
@@ -66,12 +65,12 @@ export class NodeIKernelBuddyListener {
|
|||||||
onDoubtBuddyReqUnreadNumChange(_num: number): void | Promise<void> {
|
onDoubtBuddyReqUnreadNumChange(_num: number): void | Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onNickUpdated(arg: unknown): any {
|
onNickUpdated(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSmartInfos(arg: unknown): any {
|
onSmartInfos(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSpacePermissionInfos(arg: unknown): any {
|
onSpacePermissionInfos(_arg: unknown): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,71 +1,71 @@
|
|||||||
import { User, UserDetailInfoListenerArg } from '@/core/types';
|
import { SelfStatusInfo, User, UserDetailInfoListenerArg } from '@/core/types';
|
||||||
|
|
||||||
export class NodeIKernelProfileListener {
|
export class NodeIKernelProfileListener {
|
||||||
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
|
onUserDetailInfoChanged(_arg: UserDetailInfoListenerArg): void {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfileSimpleChanged(...args: unknown[]): any {
|
onProfileSimpleChanged(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfileDetailInfoChanged(profile: User): any {
|
onProfileDetailInfoChanged(_profile: User): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatusUpdate(...args: unknown[]): any {
|
onStatusUpdate(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelfStatusChanged(...args: unknown[]): any {
|
onSelfStatusChanged(_info: SelfStatusInfo): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStrangerRemarkChanged(...args: unknown[]): any {
|
onStrangerRemarkChanged(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberListChange(...args: unknown[]): any {
|
onMemberListChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberInfoChange(...args: unknown[]): any {
|
onMemberInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupListUpdate(...args: unknown[]): any {
|
onGroupListUpdate(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupAllInfoChange(...args: unknown[]): any {
|
onGroupAllInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupDetailInfoChange(...args: unknown[]): any {
|
onGroupDetailInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupConfMemberChange(...args: unknown[]): any {
|
onGroupConfMemberChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupExtListUpdate(...args: unknown[]): any {
|
onGroupExtListUpdate(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUpdated(...args: unknown[]): any {
|
onGroupNotifiesUpdated(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
|
onGroupNotifiesUnreadCountUpdated(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupMemberLevelInfoChange(...args: unknown[]): any {
|
onGroupMemberLevelInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupBulletinChange(...args: unknown[]): any {
|
onGroupBulletinChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -96,7 +96,7 @@ export interface NodeIKernelSearchListener {
|
|||||||
}): any;
|
}): any;
|
||||||
|
|
||||||
onSearchMsgKeywordsResult(params: {
|
onSearchMsgKeywordsResult(params: {
|
||||||
searchId: number,
|
searchId: string,
|
||||||
hasMore: boolean,
|
hasMore: boolean,
|
||||||
resultItems: Array<{
|
resultItems: Array<{
|
||||||
msgId: string,
|
msgId: string,
|
||||||
|
@@ -8,8 +8,7 @@ import { LRUCache } from '@/common/lru-cache';
|
|||||||
import { LogStack } from '@/core/packet/context/clientContext';
|
import { LogStack } from '@/core/packet/context/clientContext';
|
||||||
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
|
||||||
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
import { PacketLogger } from '@/core/packet/context/loggerContext';
|
||||||
import { ProtoBufDecode } from 'napcat.protobuf';
|
|
||||||
export const MsgData = new LRUCache<string, string>(5000);
|
|
||||||
// 0 send 1 recv
|
// 0 send 1 recv
|
||||||
export interface NativePacketExportType {
|
export interface NativePacketExportType {
|
||||||
InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void, o3_hook: boolean) => boolean;
|
InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void, o3_hook: boolean) => boolean;
|
||||||
@@ -57,19 +56,6 @@ export class NativePacketClient extends IPacketClient {
|
|||||||
// console.log('callback:', callback, trace_id);
|
// console.log('callback:', callback, trace_id);
|
||||||
callback?.({ seq, cmd, hex_data });
|
callback?.({ seq, cmd, hex_data });
|
||||||
}
|
}
|
||||||
if (cmd === 'trpc.msg.olpush.OlPushService.MsgPush') {
|
|
||||||
try {
|
|
||||||
let msg_info = ProtoBufDecode(Buffer.from(hex_data, 'hex')) as any;
|
|
||||||
let group_id = (msg_info['1']['1']['8']['1'] as number).toString()
|
|
||||||
let msg_seq = (msg_info['1']['2']['5'] as number).toString()
|
|
||||||
let msg_id = group_id + '_' + msg_seq;
|
|
||||||
MsgData.put(msg_id, hex_data);
|
|
||||||
console.log('add msgid:', msg_id);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}, this.napcore.config.o3HookMode == 1);
|
}, this.napcore.config.o3HookMode == 1);
|
||||||
this.available = true;
|
this.available = true;
|
||||||
}
|
}
|
||||||
|
@@ -30,13 +30,8 @@ export class PacketOperationContext {
|
|||||||
return await this.context.client.sendOidbPacket(pkt, rsp);
|
return await this.context.client.sendOidbPacket(pkt, rsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async GroupPoke(groupUin: number, uin: number) {
|
async SendPoke(is_group: boolean, peer: number, target?: number) {
|
||||||
const req = trans.SendPoke.build(uin, groupUin);
|
const req = trans.SendPoke.build(is_group, peer, target ?? peer);
|
||||||
await this.context.client.sendOidbPacket(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
async FriendPoke(uin: number) {
|
|
||||||
const req = trans.SendPoke.build(uin);
|
|
||||||
await this.context.client.sendOidbPacket(req);
|
await this.context.client.sendOidbPacket(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +119,20 @@ export class PacketOperationContext {
|
|||||||
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
|
const req = trans.DownloadPtt.build(selfUid, node);
|
||||||
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
const res = trans.DownloadPtt.parse(resp);
|
||||||
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
|
const req = trans.DownloadVideo.build(selfUid, node);
|
||||||
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
const res = trans.DownloadVideo.parse(resp);
|
||||||
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
|
}
|
||||||
|
|
||||||
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
const req = trans.DownloadGroupImage.build(groupUin, node);
|
const req = trans.DownloadGroupImage.build(groupUin, node);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
@@ -131,6 +140,21 @@ export class PacketOperationContext {
|
|||||||
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
|
const req = trans.DownloadGroupPtt.build(groupUin, node);
|
||||||
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
const res = trans.DownloadImage.parse(resp);
|
||||||
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async GetGroupVideoUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
|
const req = trans.DownloadGroupVideo.build(groupUin, node);
|
||||||
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
const res = trans.DownloadImage.parse(resp);
|
||||||
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async ImageOCR(imgUrl: string) {
|
async ImageOCR(imgUrl: string) {
|
||||||
const req = trans.ImageOCR.build(imgUrl);
|
const req = trans.ImageOCR.build(imgUrl);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
@@ -154,7 +178,7 @@ export class PacketOperationContext {
|
|||||||
|
|
||||||
private async SendPreprocess(msg: PacketMsg[], groupUin: number = 0) {
|
private async SendPreprocess(msg: PacketMsg[], groupUin: number = 0) {
|
||||||
const ps = msg.map((m) => {
|
const ps = msg.map((m) => {
|
||||||
return m.msg.map(async(e) => {
|
return m.msg.map(async (e) => {
|
||||||
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
|
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
|
||||||
this.context.logger.debug(`Cannot find reply element's targetElems, prepare to fetch it...`);
|
this.context.logger.debug(`Cannot find reply element's targetElems, prepare to fetch it...`);
|
||||||
if (!e.targetPeer?.peerUid) {
|
if (!e.targetPeer?.peerUid) {
|
||||||
@@ -222,6 +246,7 @@ export class PacketOperationContext {
|
|||||||
const res = trans.DownloadGroupFile.parse(resp);
|
const res = trans.DownloadGroupFile.parse(resp);
|
||||||
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
|
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
|
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
|
||||||
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
|
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
@@ -229,13 +254,6 @@ export class PacketOperationContext {
|
|||||||
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
|
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
|
||||||
const req = trans.DownloadGroupPtt.build(groupUin, node);
|
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
||||||
const res = trans.DownloadGroupPtt.parse(resp);
|
|
||||||
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async GetMiniAppAdaptShareInfo(param: MiniAppReqParams) {
|
async GetMiniAppAdaptShareInfo(param: MiniAppReqParams) {
|
||||||
const req = trans.GetMiniAppAdaptShareInfo.build(param);
|
const req = trans.GetMiniAppAdaptShareInfo.build(param);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
@@ -8,13 +8,13 @@ class SendPoke extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
build(peer: number, group?: number): OidbPacket {
|
build(is_group: boolean, peer: number, target: number): OidbPacket {
|
||||||
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XED3_1).encode({
|
const payload = {
|
||||||
uin: peer,
|
uin: target,
|
||||||
groupUin: group,
|
ext: 0,
|
||||||
friendUin: group ?? peer,
|
...(is_group ? { groupUin: peer } : { friendUin: peer })
|
||||||
ext: 0
|
};
|
||||||
});
|
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XED3_1).encode(payload);
|
||||||
return OidbBase.build(0xED3, 1, data);
|
return OidbBase.build(0xED3, 1, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
50
src/core/packet/transformer/highway/DownloadGroupVideo.ts
Normal file
50
src/core/packet/transformer/highway/DownloadGroupVideo.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import * as proto from '@/core/packet/transformer/proto';
|
||||||
|
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
|
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||||
|
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||||
|
import { IndexNode } from '@/core/packet/transformer/proto';
|
||||||
|
|
||||||
|
class DownloadGroupVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
build(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||||
|
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
|
||||||
|
reqHead: {
|
||||||
|
common: {
|
||||||
|
requestId: 1,
|
||||||
|
command: 200
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
requestType: 2,
|
||||||
|
businessType: 2,
|
||||||
|
sceneType: 2,
|
||||||
|
group: {
|
||||||
|
groupUin: groupUin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
agentType: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
node: node,
|
||||||
|
download: {
|
||||||
|
video: {
|
||||||
|
busiType: 0,
|
||||||
|
sceneType: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return OidbBase.build(0x11EA, 200, body, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data: Buffer) {
|
||||||
|
const oidbBody = OidbBase.parse(data).body;
|
||||||
|
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DownloadGroupVideo();
|
51
src/core/packet/transformer/highway/DownloadPtt.ts
Normal file
51
src/core/packet/transformer/highway/DownloadPtt.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import * as proto from '@/core/packet/transformer/proto';
|
||||||
|
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
|
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||||
|
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||||
|
import { IndexNode } from '@/core/packet/transformer/proto';
|
||||||
|
|
||||||
|
class DownloadPtt extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||||
|
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
|
||||||
|
reqHead: {
|
||||||
|
common: {
|
||||||
|
requestId: 1,
|
||||||
|
command: 200
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
requestType: 1,
|
||||||
|
businessType: 3,
|
||||||
|
sceneType: 1,
|
||||||
|
c2C: {
|
||||||
|
accountType: 2,
|
||||||
|
targetUid: selfUid
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
agentType: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
node: node,
|
||||||
|
download: {
|
||||||
|
video: {
|
||||||
|
busiType: 0,
|
||||||
|
sceneType: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return OidbBase.build(0x126D, 200, body, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data: Buffer) {
|
||||||
|
const oidbBody = OidbBase.parse(data).body;
|
||||||
|
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DownloadPtt();
|
51
src/core/packet/transformer/highway/DownloadVideo.ts
Normal file
51
src/core/packet/transformer/highway/DownloadVideo.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import * as proto from '@/core/packet/transformer/proto';
|
||||||
|
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
|
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||||
|
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||||
|
import { IndexNode } from '@/core/packet/transformer/proto';
|
||||||
|
|
||||||
|
class DownloadVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||||
|
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
|
||||||
|
reqHead: {
|
||||||
|
common: {
|
||||||
|
requestId: 1,
|
||||||
|
command: 200
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
requestType: 2,
|
||||||
|
businessType: 2,
|
||||||
|
sceneType: 1,
|
||||||
|
c2C: {
|
||||||
|
accountType: 2,
|
||||||
|
targetUid: selfUid
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
agentType: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
node: node,
|
||||||
|
download: {
|
||||||
|
video: {
|
||||||
|
busiType: 0,
|
||||||
|
sceneType: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return OidbBase.build(0x11E9, 200, body, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data: Buffer) {
|
||||||
|
const oidbBody = OidbBase.parse(data).body;
|
||||||
|
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DownloadVideo();
|
@@ -13,3 +13,6 @@ export { default as UploadPrivatePtt } from './UploadPrivatePtt';
|
|||||||
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
|
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
|
||||||
export { default as DownloadImage } from './DownloadImage';
|
export { default as DownloadImage } from './DownloadImage';
|
||||||
export { default as DownloadGroupImage } from './DownloadGroupImage';
|
export { default as DownloadGroupImage } from './DownloadGroupImage';
|
||||||
|
export { default as DownloadVideo } from './DownloadVideo';
|
||||||
|
export { default as DownloadGroupVideo } from './DownloadGroupVideo';
|
||||||
|
export { default as DownloadPtt } from './DownloadPtt';
|
@@ -7,7 +7,7 @@ class OidbBase extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
|
build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, _isLafter: boolean = false): OidbPacket {
|
||||||
const data = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).encode({
|
const data = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).encode({
|
||||||
command: cmd,
|
command: cmd,
|
||||||
subCommand: subCmd,
|
subCommand: subCmd,
|
||||||
|
6
src/core/packet/transformer/proto/misc/fileid.ts
Normal file
6
src/core/packet/transformer/proto/misc/fileid.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ProtoField, ScalarType } from '@napneko/nap-proto-core';
|
||||||
|
|
||||||
|
export const FileId = {
|
||||||
|
appid: ProtoField(4, ScalarType.UINT32, true),
|
||||||
|
ttl: ProtoField(10, ScalarType.UINT32, true),
|
||||||
|
};
|
@@ -8,10 +8,22 @@ import {
|
|||||||
GroupNotifyMsgType,
|
GroupNotifyMsgType,
|
||||||
NTGroupRequestOperateTypes,
|
NTGroupRequestOperateTypes,
|
||||||
KickMemberV2Req,
|
KickMemberV2Req,
|
||||||
|
GroupDetailInfoV2Param,
|
||||||
|
GroupExtInfo,
|
||||||
|
GroupExtFilter,
|
||||||
} from '@/core/types';
|
} from '@/core/types';
|
||||||
import { GeneralCallResult } from '@/core/services/common';
|
import { GeneralCallResult } from '@/core/services/common';
|
||||||
|
|
||||||
export interface NodeIKernelGroupService {
|
export interface NodeIKernelGroupService {
|
||||||
|
|
||||||
|
modifyGroupExtInfoV2(groupExtInfo: GroupExtInfo, groupExtFilter: GroupExtFilter): Promise<GeneralCallResult &
|
||||||
|
{
|
||||||
|
result: {
|
||||||
|
groupCode: string,
|
||||||
|
result: number
|
||||||
|
}
|
||||||
|
}>;
|
||||||
|
|
||||||
// --->
|
// --->
|
||||||
// 待启用 For Next Version 3.2.0
|
// 待启用 For Next Version 3.2.0
|
||||||
// isTroopMember ? 0 : 111
|
// isTroopMember ? 0 : 111
|
||||||
@@ -169,6 +181,9 @@ export interface NodeIKernelGroupService {
|
|||||||
|
|
||||||
modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
|
modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
|
||||||
|
|
||||||
|
// 第二个参数在大多数情况为0 设置群成员权限 例如上传群文件权限和群成员付费/加入邀请加入时为8
|
||||||
|
modifyGroupDetailInfoV2(param: GroupDetailInfoV2Param, arg: number): Promise<GeneralCallResult>;
|
||||||
|
|
||||||
setGroupMsgMask(groupCode: string, arg: unknown): void;
|
setGroupMsgMask(groupCode: string, arg: unknown): void;
|
||||||
|
|
||||||
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void;
|
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void;
|
||||||
@@ -249,7 +264,7 @@ export interface NodeIKernelGroupService {
|
|||||||
|
|
||||||
reqToJoinGroup(groupCode: string, arg: unknown): void;
|
reqToJoinGroup(groupCode: string, arg: unknown): void;
|
||||||
|
|
||||||
setGroupShutUp(groupCode: string, shutUp: boolean): void;
|
setGroupShutUp(groupCode: string, shutUp: boolean): Promise<GeneralCallResult>;
|
||||||
|
|
||||||
getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>;
|
getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>;
|
||||||
|
|
||||||
|
@@ -148,10 +148,11 @@ export interface NodeIKernelMsgService {
|
|||||||
msgList: RawMessage[]
|
msgList: RawMessage[]
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
//@deprecated
|
// getMsgService/getMsgs { chatType: 2, peerUid: '975206796', privilegeFlag: 336068800 } 0 20 true
|
||||||
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>;
|
getMsgs(peer: Peer & { privilegeFlag: number }, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
|
||||||
|
msgList: RawMessage[]
|
||||||
|
}>;
|
||||||
|
|
||||||
//@deprecated
|
|
||||||
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
|
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
|
||||||
msgList: RawMessage[]
|
msgList: RawMessage[]
|
||||||
}>;
|
}>;
|
||||||
|
@@ -54,7 +54,7 @@ export interface NodeIKernelSearchService {
|
|||||||
|
|
||||||
cancelSearchChatMsgs(...args: unknown[]): unknown;// needs 3 arguments
|
cancelSearchChatMsgs(...args: unknown[]): unknown;// needs 3 arguments
|
||||||
|
|
||||||
searchMsgWithKeywords(keyWords: string[], param: Peer & { searchFields: number, pageLimit: number }): number;
|
searchMsgWithKeywords(keyWords: string[], param: Peer & { searchFields: number, pageLimit: number }): Promise<GeneralCallResult>;
|
||||||
|
|
||||||
searchMoreMsgWithKeywords(...args: unknown[]): unknown;// needs 1 arguments
|
searchMoreMsgWithKeywords(...args: unknown[]): unknown;// needs 1 arguments
|
||||||
|
|
||||||
|
@@ -16,6 +16,16 @@ export * from './NodeIKernelDbToolsService';
|
|||||||
export * from './NodeIKernelTipOffService';
|
export * from './NodeIKernelTipOffService';
|
||||||
export * from './NodeIKernelSearchService';
|
export * from './NodeIKernelSearchService';
|
||||||
export * from './NodeIKernelCollectionService';
|
export * from './NodeIKernelCollectionService';
|
||||||
|
export * from './NodeIKernelAlbumService';
|
||||||
|
export * from './NodeIKernelECDHService';
|
||||||
|
export * from './NodeIKernelNodeMiscService';
|
||||||
|
export * from './NodeIKernelMsgBackupService';
|
||||||
|
export * from './NodeIKernelTianShuService';
|
||||||
|
export * from './NodeIKernelUnitedConfigService';
|
||||||
|
export * from './NodeIkernelTestPerformanceService';
|
||||||
|
export * from './NodeIKernelUixConvertService';
|
||||||
|
export * from './NodeIKernelMSFService';
|
||||||
|
export * from './NodeIKernelRecentContactService';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
NodeIKernelAvatarService,
|
NodeIKernelAvatarService,
|
||||||
@@ -36,8 +46,19 @@ import type {
|
|||||||
NodeIKernelTicketService,
|
NodeIKernelTicketService,
|
||||||
NodeIKernelTipOffService,
|
NodeIKernelTipOffService,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
import { NodeIKernelAlbumService } from './NodeIKernelAlbumService';
|
||||||
|
import { NodeIKernelECDHService } from './NodeIKernelECDHService';
|
||||||
|
import { NodeIKernelNodeMiscService } from './NodeIKernelNodeMiscService';
|
||||||
|
import { NodeIKernelMsgBackupService } from './NodeIKernelMsgBackupService';
|
||||||
|
import { NodeIKernelTianShuService } from './NodeIKernelTianShuService';
|
||||||
|
import { NodeIKernelUnitedConfigService } from './NodeIKernelUnitedConfigService';
|
||||||
|
import { NodeIkernelTestPerformanceService } from './NodeIkernelTestPerformanceService';
|
||||||
|
import { NodeIKernelUixConvertService } from './NodeIKernelUixConvertService';
|
||||||
|
import { NodeIKernelMSFService } from './NodeIKernelMSFService';
|
||||||
|
import { NodeIKernelRecentContactService } from './NodeIKernelRecentContactService';
|
||||||
|
|
||||||
export type ServiceNamingMapping = {
|
export type ServiceNamingMapping = {
|
||||||
|
NodeIKernelAlbumService: NodeIKernelAlbumService;
|
||||||
NodeIKernelAvatarService: NodeIKernelAvatarService;
|
NodeIKernelAvatarService: NodeIKernelAvatarService;
|
||||||
NodeIKernelBuddyService: NodeIKernelBuddyService;
|
NodeIKernelBuddyService: NodeIKernelBuddyService;
|
||||||
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
|
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
|
||||||
@@ -53,6 +74,15 @@ export type ServiceNamingMapping = {
|
|||||||
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
|
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
|
||||||
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
|
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
|
||||||
NodeIKernelTipOffService: NodeIKernelTipOffService;
|
NodeIKernelTipOffService: NodeIKernelTipOffService;
|
||||||
NodeIKernelSearchService: NodeIKernelSearchService,
|
NodeIKernelSearchService: NodeIKernelSearchService;
|
||||||
NodeIKernelCollectionService: NodeIKernelCollectionService;
|
NodeIKernelCollectionService: NodeIKernelCollectionService;
|
||||||
|
NodeIKernelECDHService: NodeIKernelECDHService;
|
||||||
|
NodeIKernelNodeMiscService: NodeIKernelNodeMiscService;
|
||||||
|
NodeIKernelMsgBackupService: NodeIKernelMsgBackupService;
|
||||||
|
NodeIKernelTianShuService: NodeIKernelTianShuService;
|
||||||
|
NodeIKernelUnitedConfigService: NodeIKernelUnitedConfigService;
|
||||||
|
NodeIkernelTestPerformanceService: NodeIkernelTestPerformanceService;
|
||||||
|
NodeIKernelUixConvertService: NodeIKernelUixConvertService;
|
||||||
|
NodeIKernelMSFService: NodeIKernelMSFService;
|
||||||
|
NodeIKernelRecentContactService: NodeIKernelRecentContactService;
|
||||||
};
|
};
|
||||||
|
@@ -58,6 +58,7 @@ export interface GrayTipRovokeElement {
|
|||||||
operatorUid: string;
|
operatorUid: string;
|
||||||
operatorNick: string;
|
operatorNick: string;
|
||||||
operatorRemark: string;
|
operatorRemark: string;
|
||||||
|
isSelfOperate: boolean; // 是否是自己撤回的
|
||||||
operatorMemRemark?: string;
|
operatorMemRemark?: string;
|
||||||
wording: string; // 自定义的撤回提示语
|
wording: string; // 自定义的撤回提示语
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,97 @@
|
|||||||
import { QQLevel, NTSex } from './user';
|
import { QQLevel, NTSex } from './user';
|
||||||
|
export interface GroupExtInfo {
|
||||||
|
groupCode: string;
|
||||||
|
resultCode: number;
|
||||||
|
extInfo: EXTInfo;
|
||||||
|
}
|
||||||
|
export interface GroupExtFilter {
|
||||||
|
groupInfoExtSeq: number;
|
||||||
|
reserve: number;
|
||||||
|
luckyWordId: number;
|
||||||
|
lightCharNum: number;
|
||||||
|
luckyWord: number;
|
||||||
|
starId: number;
|
||||||
|
essentialMsgSwitch: number;
|
||||||
|
todoSeq: number;
|
||||||
|
blacklistExpireTime: number;
|
||||||
|
isLimitGroupRtc: number;
|
||||||
|
companyId: number;
|
||||||
|
hasGroupCustomPortrait: number;
|
||||||
|
bindGuildId: number;
|
||||||
|
groupOwnerId: number;
|
||||||
|
essentialMsgPrivilege: number;
|
||||||
|
msgEventSeq: number;
|
||||||
|
inviteRobotSwitch: number;
|
||||||
|
gangUpId: number;
|
||||||
|
qqMusicMedalSwitch: number;
|
||||||
|
showPlayTogetherSwitch: number;
|
||||||
|
groupFlagPro1: number;
|
||||||
|
groupBindGuildIds: number;
|
||||||
|
viewedMsgDisappearTime: number;
|
||||||
|
groupExtFlameData: number;
|
||||||
|
groupBindGuildSwitch: number;
|
||||||
|
groupAioBindGuildId: number;
|
||||||
|
groupExcludeGuildIds: number;
|
||||||
|
fullGroupExpansionSwitch: number;
|
||||||
|
fullGroupExpansionSeq: number;
|
||||||
|
inviteRobotMemberSwitch: number;
|
||||||
|
inviteRobotMemberExamine: number;
|
||||||
|
groupSquareSwitch: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface EXTInfo {
|
||||||
|
groupInfoExtSeq: number;
|
||||||
|
reserve: number;
|
||||||
|
luckyWordId: string;
|
||||||
|
lightCharNum: number;
|
||||||
|
luckyWord: string;
|
||||||
|
starId: number;
|
||||||
|
essentialMsgSwitch: number;
|
||||||
|
todoSeq: number;
|
||||||
|
blacklistExpireTime: number;
|
||||||
|
isLimitGroupRtc: number;
|
||||||
|
companyId: number;
|
||||||
|
hasGroupCustomPortrait: number;
|
||||||
|
bindGuildId: string;
|
||||||
|
groupOwnerId: GroupOwnerID;
|
||||||
|
essentialMsgPrivilege: number;
|
||||||
|
msgEventSeq: string;
|
||||||
|
inviteRobotSwitch: number;
|
||||||
|
gangUpId: string;
|
||||||
|
qqMusicMedalSwitch: number;
|
||||||
|
showPlayTogetherSwitch: number;
|
||||||
|
groupFlagPro1: string;
|
||||||
|
groupBindGuildIds: GroupGuildIDS;
|
||||||
|
viewedMsgDisappearTime: string;
|
||||||
|
groupExtFlameData: GroupEXTFlameData;
|
||||||
|
groupBindGuildSwitch: number;
|
||||||
|
groupAioBindGuildId: string;
|
||||||
|
groupExcludeGuildIds: GroupGuildIDS;
|
||||||
|
fullGroupExpansionSwitch: number;
|
||||||
|
fullGroupExpansionSeq: string;
|
||||||
|
inviteRobotMemberSwitch: number;
|
||||||
|
inviteRobotMemberExamine: number;
|
||||||
|
groupSquareSwitch: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupGuildIDS {
|
||||||
|
guildIds: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupEXTFlameData {
|
||||||
|
switchState: number;
|
||||||
|
state: number;
|
||||||
|
dayNums: any[];
|
||||||
|
version: number;
|
||||||
|
updateTime: string;
|
||||||
|
isDisplayDayNum: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupOwnerID {
|
||||||
|
memberUin: string;
|
||||||
|
memberUid: string;
|
||||||
|
memberQid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface KickMemberInfo {
|
export interface KickMemberInfo {
|
||||||
optFlag: number;
|
optFlag: number;
|
||||||
@@ -7,6 +100,185 @@ export interface KickMemberInfo {
|
|||||||
optBytesMsg: string;
|
optBytesMsg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface GroupDetailInfoV2Param {
|
||||||
|
groupCode: string;
|
||||||
|
filter: Filter;
|
||||||
|
modifyInfo: ModifyInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
noCodeFingerOpenFlag: number;
|
||||||
|
noFingerOpenFlag: number;
|
||||||
|
groupName: number;
|
||||||
|
classExt: number;
|
||||||
|
classText: number;
|
||||||
|
fingerMemo: number;
|
||||||
|
richFingerMemo: number;
|
||||||
|
tagRecord: number;
|
||||||
|
groupGeoInfo: FilterGroupGeoInfo;
|
||||||
|
groupExtAdminNum: number;
|
||||||
|
flag: number;
|
||||||
|
groupMemo: number;
|
||||||
|
groupAioSkinUrl: number;
|
||||||
|
groupBoardSkinUrl: number;
|
||||||
|
groupCoverSkinUrl: number;
|
||||||
|
groupGrade: number;
|
||||||
|
activeMemberNum: number;
|
||||||
|
certificationType: number;
|
||||||
|
certificationText: number;
|
||||||
|
groupNewGuideLines: FilterGroupNewGuideLines;
|
||||||
|
groupFace: number;
|
||||||
|
addOption: number;
|
||||||
|
shutUpTime: number;
|
||||||
|
groupTypeFlag: number;
|
||||||
|
appPrivilegeFlag: number;
|
||||||
|
appPrivilegeMask: number;
|
||||||
|
groupExtOnly: GroupEXTOnly;
|
||||||
|
groupSecLevel: number;
|
||||||
|
groupSecLevelInfo: number;
|
||||||
|
subscriptionUin: number;
|
||||||
|
subscriptionUid: string;
|
||||||
|
allowMemberInvite: number;
|
||||||
|
groupQuestion: number;
|
||||||
|
groupAnswer: number;
|
||||||
|
groupFlagExt3: number;
|
||||||
|
groupFlagExt3Mask: number;
|
||||||
|
groupOpenAppid: number;
|
||||||
|
rootId: number;
|
||||||
|
msgLimitFrequency: number;
|
||||||
|
hlGuildAppid: number;
|
||||||
|
hlGuildSubType: number;
|
||||||
|
hlGuildOrgId: number;
|
||||||
|
groupFlagExt4: number;
|
||||||
|
groupFlagExt4Mask: number;
|
||||||
|
groupSchoolInfo: FilterGroupSchoolInfo;
|
||||||
|
groupCardPrefix: FilterGroupCardPrefix;
|
||||||
|
allianceId: number;
|
||||||
|
groupFlagPro1: number;
|
||||||
|
groupFlagPro1Mask: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterGroupCardPrefix {
|
||||||
|
introduction: number;
|
||||||
|
rptPrefix: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupEXTOnly {
|
||||||
|
tribeId: number;
|
||||||
|
moneyForAddGroup: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterGroupGeoInfo {
|
||||||
|
ownerUid: number;
|
||||||
|
setTime: number;
|
||||||
|
cityId: number;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
geoContent: number;
|
||||||
|
poiId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterGroupNewGuideLines {
|
||||||
|
enabled: number;
|
||||||
|
content: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterGroupSchoolInfo {
|
||||||
|
location: number;
|
||||||
|
grade: number;
|
||||||
|
school: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModifyInfo {
|
||||||
|
noCodeFingerOpenFlag: number;
|
||||||
|
noFingerOpenFlag: number;
|
||||||
|
groupName: string;
|
||||||
|
classExt: number;
|
||||||
|
classText: string;
|
||||||
|
fingerMemo: string;
|
||||||
|
richFingerMemo: string;
|
||||||
|
tagRecord: any[];
|
||||||
|
groupGeoInfo: ModifyInfoGroupGeoInfo;
|
||||||
|
groupExtAdminNum: number;
|
||||||
|
flag: number;
|
||||||
|
groupMemo: string;
|
||||||
|
groupAioSkinUrl: string;
|
||||||
|
groupBoardSkinUrl: string;
|
||||||
|
groupCoverSkinUrl: string;
|
||||||
|
groupGrade: number;
|
||||||
|
activeMemberNum: number;
|
||||||
|
certificationType: number;
|
||||||
|
certificationText: string;
|
||||||
|
groupNewGuideLines: ModifyInfoGroupNewGuideLines;
|
||||||
|
groupFace: number;
|
||||||
|
addOption: number;// 0 空设置 1 任何人都可以进入 2 需要管理员批准 3 不允许任何人入群 4 问题进入答案 5 问题管理员批准
|
||||||
|
shutUpTime: number;
|
||||||
|
groupTypeFlag: number;
|
||||||
|
appPrivilegeFlag: number;
|
||||||
|
// 需要管理员审核
|
||||||
|
// 0000 0000 0000 0000 0000 0000 0000
|
||||||
|
// 无需审核入群
|
||||||
|
// 0000 0001 0000 0000 0000 0000 0000
|
||||||
|
// 成员数100内无审核
|
||||||
|
// 0100 0000 0000 0000 0000 0000 0000
|
||||||
|
// 禁用 群成员邀请好友
|
||||||
|
// 0100 0000 0000 0000 0000 0000 0000
|
||||||
|
|
||||||
|
appPrivilegeMask: number;
|
||||||
|
// 0110 0001 0000 0000 0000 0000 0000
|
||||||
|
// 101711872
|
||||||
|
groupExtOnly: GroupEXTOnly;
|
||||||
|
groupSecLevel: number;
|
||||||
|
groupSecLevelInfo: number;
|
||||||
|
subscriptionUin: string;
|
||||||
|
subscriptionUid: string;
|
||||||
|
allowMemberInvite: number;
|
||||||
|
groupQuestion: string;
|
||||||
|
groupAnswer: string;
|
||||||
|
groupFlagExt3: number;
|
||||||
|
groupFlagExt3Mask: number;
|
||||||
|
groupOpenAppid: number;
|
||||||
|
rootId: string;
|
||||||
|
msgLimitFrequency: number;
|
||||||
|
hlGuildAppid: number;
|
||||||
|
hlGuildSubType: number;
|
||||||
|
hlGuildOrgId: number;
|
||||||
|
groupFlagExt4: number;
|
||||||
|
groupFlagExt4Mask: number;
|
||||||
|
groupSchoolInfo: ModifyInfoGroupSchoolInfo;
|
||||||
|
groupCardPrefix: ModifyInfoGroupCardPrefix;
|
||||||
|
allianceId: string;
|
||||||
|
groupFlagPro1: number;
|
||||||
|
groupFlagPro1Mask: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModifyInfoGroupCardPrefix {
|
||||||
|
introduction: string;
|
||||||
|
rptPrefix: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModifyInfoGroupGeoInfo {
|
||||||
|
ownerUid: string;
|
||||||
|
SetTime: number;
|
||||||
|
CityId: number;
|
||||||
|
Longitude: string;
|
||||||
|
Latitude: string;
|
||||||
|
GeoContent: string;
|
||||||
|
poiId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModifyInfoGroupNewGuideLines {
|
||||||
|
enabled: boolean;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModifyInfoGroupSchoolInfo {
|
||||||
|
location: string;
|
||||||
|
grade: number;
|
||||||
|
school: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取群详细信息的来源类型
|
// 获取群详细信息的来源类型
|
||||||
export enum GroupInfoSource {
|
export enum GroupInfoSource {
|
||||||
KUNSPECIFIED,
|
KUNSPECIFIED,
|
||||||
|
@@ -508,7 +508,8 @@ export interface RawMessage {
|
|||||||
* 查询消息参数接口
|
* 查询消息参数接口
|
||||||
*/
|
*/
|
||||||
export interface QueryMsgsParams {
|
export interface QueryMsgsParams {
|
||||||
chatInfo: Peer;
|
chatInfo: Peer & { privilegeFlag?: number };
|
||||||
|
//searchFields: number;
|
||||||
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
|
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
|
||||||
filterSendersUid: string[];
|
filterSendersUid: string[];
|
||||||
filterMsgFromTime: string;
|
filterMsgFromTime: string;
|
||||||
|
@@ -132,18 +132,26 @@ export enum BuddyReqType {
|
|||||||
KMEINITIATORWAITPEERCONFIRM = 13
|
KMEINITIATORWAITPEERCONFIRM = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 其中 ? 代表新版本参数
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
isBuddy?: boolean;
|
|
||||||
isInitiator?: boolean;
|
isInitiator?: boolean;
|
||||||
isDecide: boolean;
|
isDecide: boolean;
|
||||||
friendUid: string;
|
friendUid: string;
|
||||||
reqType: BuddyReqType,
|
reqType: BuddyReqType,
|
||||||
reqTime: string; // 时间戳 秒
|
reqTime: string; // 时间戳 秒
|
||||||
|
flag?: number; // 0
|
||||||
|
preGroupingId?: number; // 0
|
||||||
|
commFriendNum?: number; // 共同好友数
|
||||||
extWords: string; // 申请人填写的验证消息
|
extWords: string; // 申请人填写的验证消息
|
||||||
isUnread: boolean;
|
isUnread: boolean;
|
||||||
|
isDoubt?: boolean; // 是否是可疑的好友请求
|
||||||
|
nameMore?: string;
|
||||||
friendNick: string;
|
friendNick: string;
|
||||||
sourceId: number;
|
sourceId: number;
|
||||||
groupCode: string
|
groupCode: string;
|
||||||
|
isBuddy?: boolean;
|
||||||
|
isAgreed?: boolean;
|
||||||
|
relation?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FriendRequestNotify {
|
export interface FriendRequestNotify {
|
||||||
|
@@ -48,6 +48,12 @@ export async function NCoreInitFramework(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
//直到登录成功后,执行下一步
|
//直到登录成功后,执行下一步
|
||||||
|
// const selfInfo = {
|
||||||
|
// uid: 'u_FUSS0_x06S_9Tf4na_WpUg',
|
||||||
|
// uin: '3684714082',
|
||||||
|
// nick: '',
|
||||||
|
// online: true
|
||||||
|
// }
|
||||||
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
|
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
|
||||||
const loginListener = new NodeIKernelLoginListener();
|
const loginListener = new NodeIKernelLoginListener();
|
||||||
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
||||||
|
26
src/framework/nativeLoader.cjs
Normal file
26
src/framework/nativeLoader.cjs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function initializeNapCat(session, loginService, registerCallback) {
|
||||||
|
//const logFile = path.join(currentPath, 'napcat.log');
|
||||||
|
|
||||||
|
console.log('[NapCat] [Info] 开始初始化NapCat');
|
||||||
|
|
||||||
|
//fs.writeFileSync(logFile, '', { flag: 'w' });
|
||||||
|
|
||||||
|
//fs.writeFileSync(logFile, '[NapCat] [Info] NapCat 初始化成功\n', { flag: 'a' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentPath = path.dirname(__filename);
|
||||||
|
const { NCoreInitFramework } = await import('file://' + path.join(currentPath, './napcat.mjs'));
|
||||||
|
await NCoreInitFramework(session, loginService, (callback) => { registerCallback(callback) });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[NapCat] [Error] 初始化NapCat', error);
|
||||||
|
//fs.writeFileSync(logFile, `[NapCat] [Error] 初始化NapCat失败: ${error.message}\n`, { flag: 'a' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initializeNapCat: initializeNapCat
|
||||||
|
};
|
426
src/image-size/index.ts
Normal file
426
src/image-size/index.ts
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
export interface ImageSize {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageType {
|
||||||
|
JPEG = 'jpeg',
|
||||||
|
PNG = 'png',
|
||||||
|
BMP = 'bmp',
|
||||||
|
GIF = 'gif',
|
||||||
|
WEBP = 'webp',
|
||||||
|
UNKNOWN = 'unknown',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageParser {
|
||||||
|
readonly type: ImageType;
|
||||||
|
canParse(buffer: Buffer): boolean;
|
||||||
|
parseSize(stream: ReadStream): Promise<ImageSize | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 魔术匹配
|
||||||
|
function matchMagic(buffer: Buffer, magic: number[], offset = 0): boolean {
|
||||||
|
if (buffer.length < offset + magic.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < magic.length; i++) {
|
||||||
|
if (buffer[offset + i] !== magic[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PNG解析器
|
||||||
|
class PngParser implements ImageParser {
|
||||||
|
readonly type = ImageType.PNG;
|
||||||
|
// PNG 魔术头:89 50 4E 47 0D 0A 1A 0A
|
||||||
|
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.PNG_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(24) as Buffer;
|
||||||
|
if (!buf || buf.length < 24) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt32BE(16);
|
||||||
|
const height = buf.readUInt32BE(20);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JPEG解析器
|
||||||
|
class JpegParser implements ImageParser {
|
||||||
|
readonly type = ImageType.JPEG;
|
||||||
|
// JPEG 魔术头:FF D8
|
||||||
|
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
|
||||||
|
|
||||||
|
// JPEG标记常量
|
||||||
|
private readonly SOF_MARKERS = {
|
||||||
|
SOF0: 0xC0, // 基线DCT
|
||||||
|
SOF1: 0xC1, // 扩展顺序DCT
|
||||||
|
SOF2: 0xC2, // 渐进式DCT
|
||||||
|
SOF3: 0xC3, // 无损
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 非SOF标记
|
||||||
|
private readonly NON_SOF_MARKERS: number[] = [
|
||||||
|
0xC4, // DHT
|
||||||
|
0xC8, // JPEG扩展
|
||||||
|
0xCC, // DAC
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.JPEG_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSOFMarker(marker: number): boolean {
|
||||||
|
return (
|
||||||
|
marker === this.SOF_MARKERS.SOF0 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF1 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF2 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isNonSOFMarker(marker: number): boolean {
|
||||||
|
return this.NON_SOF_MARKERS.includes(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise<ImageSize | undefined>((resolve, reject) => {
|
||||||
|
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
let offset = 0;
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
stream.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理数据块
|
||||||
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
|
// 追加新数据到缓冲区
|
||||||
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
|
||||||
|
const bufferSize = buffer.length;
|
||||||
|
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
|
||||||
|
|
||||||
|
// 从JPEG头部后开始扫描
|
||||||
|
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
|
||||||
|
// 寻找FF标记
|
||||||
|
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
|
||||||
|
const marker = buffer[offset + 1];
|
||||||
|
if (!marker) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 跳过非SOF标记
|
||||||
|
if (this.isNonSOFMarker(marker)) {
|
||||||
|
offset += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理SOF标记 (包含尺寸信息)
|
||||||
|
if (this.isSOFMarker(marker)) {
|
||||||
|
// 确保缓冲区中有足够数据读取尺寸
|
||||||
|
if (offset + 9 < bufferSize) {
|
||||||
|
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
|
||||||
|
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
|
||||||
|
const height = buffer.readUInt16BE(offset + 5);
|
||||||
|
const width = buffer.readUInt16BE(offset + 7);
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
stream.destroy();
|
||||||
|
resolve({ width, height });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 如果缓冲区内数据不够,保留当前位置等待更多数据
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓冲区管理: 如果处理了许多数据但没找到标记,
|
||||||
|
// 保留最后N字节用于跨块匹配,丢弃之前的数据
|
||||||
|
if (offset > BUFFER_SIZE) {
|
||||||
|
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
|
||||||
|
if (offset > KEEP_BYTES) {
|
||||||
|
buffer = buffer.subarray(offset - KEEP_BYTES);
|
||||||
|
offset = KEEP_BYTES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理流结束
|
||||||
|
stream.on('end', () => {
|
||||||
|
if (!found) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BMP解析器
|
||||||
|
class BmpParser implements ImageParser {
|
||||||
|
readonly type = ImageType.BMP;
|
||||||
|
// BMP 魔术头:42 4D (BM)
|
||||||
|
private readonly BMP_SIGNATURE = [0x42, 0x4D];
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.BMP_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(26) as Buffer;
|
||||||
|
if (!buf || buf.length < 26) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt32LE(18);
|
||||||
|
const height = buf.readUInt32LE(22);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GIF解析器
|
||||||
|
class GifParser implements ImageParser {
|
||||||
|
readonly type = ImageType.GIF;
|
||||||
|
// GIF87a 魔术头:47 49 46 38 37 61
|
||||||
|
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
|
||||||
|
// GIF89a 魔术头:47 49 46 38 39 61
|
||||||
|
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return (
|
||||||
|
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
|
||||||
|
matchMagic(buffer, this.GIF89A_SIGNATURE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(10) as Buffer;
|
||||||
|
if (!buf || buf.length < 10) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt16LE(6);
|
||||||
|
const height = buf.readUInt16LE(8);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
|
||||||
|
class WebpParser implements ImageParser {
|
||||||
|
readonly type = ImageType.WEBP;
|
||||||
|
// WEBP RIFF 头:52 49 46 46 (RIFF)
|
||||||
|
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
|
||||||
|
// WEBP 魔术头:57 45 42 50 (WEBP)
|
||||||
|
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
|
||||||
|
|
||||||
|
// WEBP 块头
|
||||||
|
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
|
||||||
|
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
|
||||||
|
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
|
||||||
|
|
||||||
|
canParse(buffer: Buffer): boolean {
|
||||||
|
return (
|
||||||
|
buffer.length >= 12 &&
|
||||||
|
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
|
||||||
|
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isChunkType(buffer: Buffer, offset: number, chunkType: number[]): boolean {
|
||||||
|
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 需要读取足够的字节来检测所有三种格式
|
||||||
|
const MAX_HEADER_SIZE = 32;
|
||||||
|
let totalBytes = 0;
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
|
||||||
|
stream.on('error', reject);
|
||||||
|
|
||||||
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
buffer = Buffer.concat([buffer, chunkBuffer]);
|
||||||
|
totalBytes += chunk.length;
|
||||||
|
|
||||||
|
// 检查是否有足够的字节进行格式检测
|
||||||
|
if (totalBytes >= MAX_HEADER_SIZE) {
|
||||||
|
stream.destroy();
|
||||||
|
|
||||||
|
// 检查基本的WEBP签名
|
||||||
|
if (!this.canParse(buffer)) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查chunk头部,位于字节12-15
|
||||||
|
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
|
||||||
|
// VP8格式 - 标准WebP
|
||||||
|
// 宽度和高度在帧头中
|
||||||
|
const width = buffer.readUInt16LE(26) & 0x3FFF;
|
||||||
|
const height = buffer.readUInt16LE(28) & 0x3FFF;
|
||||||
|
return resolve({ width, height });
|
||||||
|
|
||||||
|
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
|
||||||
|
// VP8L格式 - 无损WebP
|
||||||
|
// 1字节标记后是14位宽度和14位高度
|
||||||
|
const bits = buffer.readUInt32LE(21);
|
||||||
|
const width = 1 + (bits & 0x3FFF);
|
||||||
|
const height = 1 + ((bits >> 14) & 0x3FFF);
|
||||||
|
return resolve({ width, height });
|
||||||
|
|
||||||
|
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
|
||||||
|
// VP8X格式 - 扩展WebP
|
||||||
|
// 24位宽度和高度(减去1)
|
||||||
|
if (!buffer[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
const width = 1 + ((buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) & 0xFFFFFF);
|
||||||
|
const height = 1 + ((buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) & 0xFFFFFF);
|
||||||
|
return resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
// 未知的WebP子格式
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
// 如果没有读到足够的字节
|
||||||
|
if (totalBytes < MAX_HEADER_SIZE) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsers: ReadonlyArray<ImageParser> = [
|
||||||
|
new PngParser(),
|
||||||
|
new JpegParser(),
|
||||||
|
new BmpParser(),
|
||||||
|
new GifParser(),
|
||||||
|
new WebpParser(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function detectImageType(filePath: string): Promise<ImageType> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const stream = fs.createReadStream(filePath, {
|
||||||
|
highWaterMark: 64, // 优化读取buffer大小
|
||||||
|
start: 0,
|
||||||
|
end: 63
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer: Buffer | null = null;
|
||||||
|
|
||||||
|
stream.once('error', (err) => {
|
||||||
|
stream.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.once('readable', () => {
|
||||||
|
buffer = stream.read(64) as Buffer;
|
||||||
|
stream.destroy();
|
||||||
|
|
||||||
|
if (!buffer) {
|
||||||
|
return resolve(ImageType.UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parser of parsers) {
|
||||||
|
if (parser.canParse(buffer)) {
|
||||||
|
return resolve(parser.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(ImageType.UNKNOWN);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.once('end', () => {
|
||||||
|
if (!buffer) {
|
||||||
|
resolve(ImageType.UNKNOWN);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function imageSizeFromFile(filePath: string): Promise<ImageSize | undefined> {
|
||||||
|
try {
|
||||||
|
// 先检测类型
|
||||||
|
const type = await detectImageType(filePath);
|
||||||
|
const parser = parsers.find(p => p.type === type);
|
||||||
|
if (!parser) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用流式方式解析尺寸
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
try {
|
||||||
|
return await parser.parseSize(stream);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`解析图片尺寸出错: ${err}`);
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
if (!stream.destroyed) {
|
||||||
|
stream.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`检测图片类型出错: ${err}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function imageSizeFallBack(
|
||||||
|
filePath: string,
|
||||||
|
fallback: ImageSize = {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
}
|
||||||
|
): Promise<ImageSize> {
|
||||||
|
return await imageSizeFromFile(filePath) ?? fallback;
|
||||||
|
}
|
28
src/onebot/action/extends/SetGroupAddOption.ts
Normal file
28
src/onebot/action/extends/SetGroupAddOption.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
const SchemaData = Type.Object({
|
||||||
|
group_id: Type.String(),
|
||||||
|
add_type: Type.Number(),
|
||||||
|
group_question: Type.Optional(Type.String()),
|
||||||
|
group_answer: Type.Optional(Type.String()),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
|
export default class SetGroupAddOption extends OneBotAction<Payload, null> {
|
||||||
|
override actionName = ActionName.SetGroupAddOption;
|
||||||
|
override payloadSchema = SchemaData;
|
||||||
|
async _handle(payload: Payload): Promise<null> {
|
||||||
|
let ret = await this.core.apis.GroupApi.setGroupAddOption(payload.group_id, {
|
||||||
|
addOption: payload.add_type,
|
||||||
|
groupQuestion: payload.group_question,
|
||||||
|
groupAnswer: payload.group_answer,
|
||||||
|
});
|
||||||
|
if (ret.result != 0) {
|
||||||
|
throw new Error(`设置群添加选项失败, ${ret.result}:${ret.errMsg}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
23
src/onebot/action/extends/SetGroupKickMembers.ts
Normal file
23
src/onebot/action/extends/SetGroupKickMembers.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
const SchemaData = Type.Object({
|
||||||
|
group_id: Type.String(),
|
||||||
|
user_id: Type.Array(Type.String()),
|
||||||
|
reject_add_request: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
|
export default class SetGroupKickMembers extends OneBotAction<Payload, null> {
|
||||||
|
override actionName = ActionName.SetGroupKickMembers;
|
||||||
|
override payloadSchema = SchemaData;
|
||||||
|
|
||||||
|
async _handle(payload: Payload): Promise<null> {
|
||||||
|
const rejectReq = payload.reject_add_request?.toString() == 'true';
|
||||||
|
const uids: string[] = await Promise.all(payload.user_id.map(async uin => await this.core.apis.UserApi.getUidByUinV2(uin)));
|
||||||
|
await this.core.apis.GroupApi.kickMember(payload.group_id.toString(), uids.filter(uid => !!uid), rejectReq);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
27
src/onebot/action/extends/SetGroupRobotAddOption.ts
Normal file
27
src/onebot/action/extends/SetGroupRobotAddOption.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
const SchemaData = Type.Object({
|
||||||
|
group_id: Type.String(),
|
||||||
|
robot_member_switch: Type.Optional(Type.Number()),
|
||||||
|
robot_member_examine: Type.Optional(Type.Number()),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
|
export default class SetGroupRobotAddOption extends OneBotAction<Payload, null> {
|
||||||
|
override actionName = ActionName.SetGroupRobotAddOption;
|
||||||
|
override payloadSchema = SchemaData;
|
||||||
|
async _handle(payload: Payload): Promise<null> {
|
||||||
|
let ret = await this.core.apis.GroupApi.setGroupRobotAddOption(
|
||||||
|
payload.group_id,
|
||||||
|
payload.robot_member_switch,
|
||||||
|
payload.robot_member_examine,
|
||||||
|
);
|
||||||
|
if (ret.result != 0) {
|
||||||
|
throw new Error(`设置群机器人添加选项失败, ${ret.result}:${ret.errMsg}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
26
src/onebot/action/extends/SetGroupSearch.ts
Normal file
26
src/onebot/action/extends/SetGroupSearch.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
const SchemaData = Type.Object({
|
||||||
|
group_id: Type.String(),
|
||||||
|
no_code_finger_open: Type.Optional(Type.Number()),
|
||||||
|
no_finger_open: Type.Optional(Type.Number()),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
|
export default class SetGroupSearch extends OneBotAction<Payload, null> {
|
||||||
|
override actionName = ActionName.SetGroupSearch;
|
||||||
|
override payloadSchema = SchemaData;
|
||||||
|
async _handle(payload: Payload): Promise<null> {
|
||||||
|
let ret = await this.core.apis.GroupApi.setGroupSearch(payload.group_id, {
|
||||||
|
noCodeFingerOpenFlag: payload.no_code_finger_open,
|
||||||
|
noFingerOpenFlag: payload.no_finger_open,
|
||||||
|
});
|
||||||
|
if (ret.result != 0) {
|
||||||
|
throw new Error(`设置群搜索失败, ${ret.result}:${ret.errMsg}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,10 @@ import { Static, Type } from '@sinclair/typebox';
|
|||||||
|
|
||||||
const SchemaData = Type.Object({
|
const SchemaData = Type.Object({
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||||
folder_name: Type.String(),
|
// 兼容gocq 与name二选一
|
||||||
|
folder_name: Type.Optional(Type.String()),
|
||||||
|
// 兼容gocq 与folder_name二选一
|
||||||
|
name: Type.Optional(Type.String()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
@@ -16,6 +19,7 @@ export class CreateGroupFileFolder extends OneBotAction<Payload, ResponseType>
|
|||||||
override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
|
override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = SchemaData;
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
|
const folderName = payload.folder_name || payload.name;
|
||||||
|
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), folderName!)).resultWithGroupItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { normalize, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
import { ContextMode, normalize, ReturnDataType, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
||||||
import { OB11PostSendMsg } from '@/onebot/types';
|
import { OB11PostSendMsg } from '@/onebot/types';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
|
||||||
@@ -19,8 +19,14 @@ export class GoCQHTTPSendForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
|||||||
}
|
}
|
||||||
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
||||||
override actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
|
override actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Private);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsgBase {
|
||||||
override actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
|
override actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
27
src/onebot/action/group/GetGroupDetailInfo.ts
Normal file
27
src/onebot/action/group/GetGroupDetailInfo.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
const SchemaData = Type.Object({
|
||||||
|
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
|
export class GetGroupDetailInfo extends OneBotAction<Payload, unknown> {
|
||||||
|
override actionName = ActionName.GetGroupDetailInfo;
|
||||||
|
override payloadSchema = SchemaData;
|
||||||
|
|
||||||
|
async _handle(payload: Payload) {
|
||||||
|
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
group_all_shut: data.shutUpAllTimestamp > 0 ? -1 : 0,
|
||||||
|
group_remark: '',
|
||||||
|
group_id: +payload.group_id,
|
||||||
|
group_name: data.groupName,
|
||||||
|
member_count: data.memberNum,
|
||||||
|
max_member_count: data.maxMemberNum,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ import { ActionName } from '@/onebot/action/router';
|
|||||||
import { Notify } from '@/onebot/types';
|
import { Notify } from '@/onebot/types';
|
||||||
|
|
||||||
interface RetData {
|
interface RetData {
|
||||||
|
invited_requests: Notify[];
|
||||||
InvitedRequest: Notify[];
|
InvitedRequest: Notify[];
|
||||||
join_requests: Notify[];
|
join_requests: Notify[];
|
||||||
}
|
}
|
||||||
@@ -13,7 +14,7 @@ export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
|
|||||||
|
|
||||||
async _handle(): Promise<RetData> {
|
async _handle(): Promise<RetData> {
|
||||||
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
|
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
|
||||||
const retData: RetData = { InvitedRequest: [], join_requests: [] };
|
const retData: RetData = { invited_requests: [], InvitedRequest: [], join_requests: [] };
|
||||||
|
|
||||||
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
|
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
|
||||||
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
||||||
@@ -38,7 +39,7 @@ export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(notifyPromises);
|
await Promise.all(notifyPromises);
|
||||||
|
retData.invited_requests = retData.InvitedRequest;
|
||||||
return retData;
|
return retData;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -8,10 +8,16 @@ interface GroupNotice {
|
|||||||
notice_id: string;
|
notice_id: string;
|
||||||
message: {
|
message: {
|
||||||
text: string
|
text: string
|
||||||
|
// 保持一段时间兼容性 防止以往版本出现问题 后续版本可考虑移除
|
||||||
image: Array<{
|
image: Array<{
|
||||||
height: string
|
height: string
|
||||||
width: string
|
width: string
|
||||||
id: string
|
id: string
|
||||||
|
}>,
|
||||||
|
images: Array<{
|
||||||
|
height: string
|
||||||
|
width: string
|
||||||
|
id: string
|
||||||
}>
|
}>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -40,15 +46,18 @@ export class GetGroupNotice extends OneBotAction<Payload, GroupNotice[]> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const retApiNotice: WebApiGroupNoticeFeed = ret.feeds[key];
|
const retApiNotice: WebApiGroupNoticeFeed = ret.feeds[key];
|
||||||
|
const image = retApiNotice.msg.pics?.map((pic) => {
|
||||||
|
return { id: pic.id, height: pic.h, width: pic.w };
|
||||||
|
}) || [];
|
||||||
|
|
||||||
const retNotice: GroupNotice = {
|
const retNotice: GroupNotice = {
|
||||||
notice_id: retApiNotice.fid,
|
notice_id: retApiNotice.fid,
|
||||||
sender_id: retApiNotice.u,
|
sender_id: retApiNotice.u,
|
||||||
publish_time: retApiNotice.pubt,
|
publish_time: retApiNotice.pubt,
|
||||||
message: {
|
message: {
|
||||||
text: retApiNotice.msg.text,
|
text: retApiNotice.msg.text,
|
||||||
image: retApiNotice.msg.pics?.map((pic) => {
|
image,
|
||||||
return { id: pic.id, height: pic.h, width: pic.w };
|
images: image,
|
||||||
}) || [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
retNotices.push(retNotice);
|
retNotices.push(retNotice);
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
|
||||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
|
||||||
user_id: Type.Union([Type.Number(), Type.String()]),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
|
||||||
|
|
||||||
export class GroupPoke extends GetPacketStatusDepends<Payload, void> {
|
|
||||||
override actionName = ActionName.GroupPoke;
|
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
|
||||||
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +1,19 @@
|
|||||||
import { ContextMode, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
import { ContextMode, ReturnDataType, SendMsgBase } from '@/onebot/action/msg/SendMsg';
|
||||||
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
||||||
import { OB11PostSendMsg } from '@/onebot/types';
|
import { OB11PostSendMsg } from '@/onebot/types';
|
||||||
|
|
||||||
// 未检测参数
|
// 未检测参数
|
||||||
class SendGroupMsg extends SendMsgBase {
|
class SendGroupMsg extends SendMsgBase {
|
||||||
override actionName = ActionName.SendGroupMsg;
|
override actionName = ActionName.SendGroupMsg;
|
||||||
override contextMode: ContextMode = ContextMode.Group;
|
|
||||||
|
|
||||||
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||||
delete payload.user_id;
|
delete payload.user_id;
|
||||||
payload.message_type = 'group';
|
payload.message_type = 'group';
|
||||||
return super.check(payload);
|
return super.check(payload);
|
||||||
}
|
}
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SendGroupMsg;
|
export default SendGroupMsg;
|
||||||
|
@@ -15,7 +15,10 @@ export default class SetGroupWholeBan extends OneBotAction<Payload, null> {
|
|||||||
|
|
||||||
async _handle(payload: Payload): Promise<null> {
|
async _handle(payload: Payload): Promise<null> {
|
||||||
const enable = payload.enable?.toString() !== 'false';
|
const enable = payload.enable?.toString() !== 'false';
|
||||||
await this.core.apis.GroupApi.banGroup(payload.group_id.toString(), enable);
|
let res = await this.core.apis.GroupApi.banGroup(payload.group_id.toString(), enable);
|
||||||
|
if (res.result !== 0) {
|
||||||
|
throw new Error(`SetGroupWholeBan failed: ${res.errMsg} ${res.result}`);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -78,7 +78,6 @@ import { GetGroupFileSystemInfo } from '@/onebot/action/go-cqhttp/GetGroupFileSy
|
|||||||
import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles';
|
import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles';
|
||||||
import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder';
|
import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder';
|
||||||
import { GetGroupSystemMsg } from './system/GetSystemMsg';
|
import { GetGroupSystemMsg } from './system/GetSystemMsg';
|
||||||
import { GroupPoke } from './group/GroupPoke';
|
|
||||||
import { GetUserStatus } from './extends/GetUserStatus';
|
import { GetUserStatus } from './extends/GetUserStatus';
|
||||||
import { GetRkey } from './extends/GetRkey';
|
import { GetRkey } from './extends/GetRkey';
|
||||||
import { SetSpecialTitle } from './extends/SetSpecialTitle';
|
import { SetSpecialTitle } from './extends/SetSpecialTitle';
|
||||||
@@ -86,7 +85,6 @@ import { GetGroupShutList } from './group/GetGroupShutList';
|
|||||||
import { GetGroupMemberList } from './group/GetGroupMemberList';
|
import { GetGroupMemberList } from './group/GetGroupMemberList';
|
||||||
import { GetGroupFileUrl } from '@/onebot/action/file/GetGroupFileUrl';
|
import { GetGroupFileUrl } from '@/onebot/action/file/GetGroupFileUrl';
|
||||||
import { GetPacketStatus } from '@/onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatus } from '@/onebot/action/packet/GetPacketStatus';
|
||||||
import { FriendPoke } from '@/onebot/action/user/FriendPoke';
|
|
||||||
import { GetCredentials } from './system/GetCredentials';
|
import { GetCredentials } from './system/GetCredentials';
|
||||||
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
|
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
|
||||||
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
|
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
|
||||||
@@ -102,7 +100,7 @@ import { GetGuildList } from './guild/GetGuildList';
|
|||||||
import { GetGuildProfile } from './guild/GetGuildProfile';
|
import { GetGuildProfile } from './guild/GetGuildProfile';
|
||||||
import { GetClientkey } from './extends/GetClientkey';
|
import { GetClientkey } from './extends/GetClientkey';
|
||||||
import { SendPacket } from './extends/SendPacket';
|
import { SendPacket } from './extends/SendPacket';
|
||||||
import { SendPoke } from '@/onebot/action/packet/SendPoke';
|
import { FriendPoke, GroupPoke, SendPoke } from '@/onebot/action/packet/SendPoke';
|
||||||
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
|
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
|
||||||
import { BotExit } from './extends/BotExit';
|
import { BotExit } from './extends/BotExit';
|
||||||
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
|
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
|
||||||
@@ -118,10 +116,22 @@ import { CleanCache } from './system/CleanCache';
|
|||||||
import SetFriendRemark from './user/SetFriendRemark';
|
import SetFriendRemark from './user/SetFriendRemark';
|
||||||
import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
|
import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
|
||||||
import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest';
|
import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest';
|
||||||
|
import SetGroupAddOption from './extends/SetGroupAddOption';
|
||||||
|
import SetGroupSearch from './extends/SetGroupSearch';
|
||||||
|
import SetGroupRobotAddOption from './extends/SetGroupRobotAddOption';
|
||||||
|
import SetGroupKickMembers from './extends/SetGroupKickMembers';
|
||||||
|
import { GetGroupDetailInfo } from './group/GetGroupDetailInfo';
|
||||||
|
import GetGroupAddRequest from './extends/GetGroupAddRequest';
|
||||||
|
import { GetCollectionList } from './extends/GetCollectionList';
|
||||||
|
|
||||||
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||||
|
|
||||||
const actionHandlers = [
|
const actionHandlers = [
|
||||||
|
new GetGroupDetailInfo(obContext, core),
|
||||||
|
new SetGroupKickMembers(obContext, core),
|
||||||
|
new SetGroupAddOption(obContext, core),
|
||||||
|
new SetGroupRobotAddOption(obContext, core),
|
||||||
|
new SetGroupSearch(obContext, core),
|
||||||
new SetDoubtFriendsAddRequest(obContext, core),
|
new SetDoubtFriendsAddRequest(obContext, core),
|
||||||
new GetDoubtFriendsAddRequest(obContext, core),
|
new GetDoubtFriendsAddRequest(obContext, core),
|
||||||
new SetFriendRemark(obContext, core),
|
new SetFriendRemark(obContext, core),
|
||||||
@@ -249,6 +259,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
|||||||
new GetPrivateFileUrl(obContext, core),
|
new GetPrivateFileUrl(obContext, core),
|
||||||
new GetUnidirectionalFriendList(obContext, core),
|
new GetUnidirectionalFriendList(obContext, core),
|
||||||
new CleanCache(obContext, core),
|
new CleanCache(obContext, core),
|
||||||
|
new GetGroupAddRequest(obContext, core),
|
||||||
|
new GetCollectionList(obContext, core),
|
||||||
];
|
];
|
||||||
|
|
||||||
type HandlerUnion = typeof actionHandlers[number];
|
type HandlerUnion = typeof actionHandlers[number];
|
||||||
|
@@ -19,6 +19,7 @@ import { rawMsgWithSendMsg } from '@/core/packet/message/converter';
|
|||||||
export interface ReturnDataType {
|
export interface ReturnDataType {
|
||||||
message_id: number;
|
message_id: number;
|
||||||
res_id?: string;
|
res_id?: string;
|
||||||
|
forward_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContextMode {
|
export enum ContextMode {
|
||||||
@@ -104,8 +105,6 @@ function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
||||||
contextMode = ContextMode.Normal;
|
|
||||||
|
|
||||||
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||||
const messages = normalize(payload.message);
|
const messages = normalize(payload.message);
|
||||||
const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node);
|
const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node);
|
||||||
@@ -117,12 +116,13 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
}
|
}
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
this.contextMode = ContextMode.Normal;
|
return this.base_handle(payload);
|
||||||
if (payload.message_type === 'group') this.contextMode = ContextMode.Group;
|
}
|
||||||
if (payload.message_type === 'private') this.contextMode = ContextMode.Private;
|
async base_handle(payload: OB11PostSendMsg, contextMode: ContextMode = ContextMode.Normal): Promise<ReturnDataType> {
|
||||||
const peer = await createContext(this.core, payload, this.contextMode);
|
if (payload.message_type === 'group') contextMode = ContextMode.Group;
|
||||||
|
if (payload.message_type === 'private') contextMode = ContextMode.Private;
|
||||||
|
const peer = await createContext(this.core, payload, contextMode);
|
||||||
|
|
||||||
const messages = normalize(
|
const messages = normalize(
|
||||||
payload.message,
|
payload.message,
|
||||||
@@ -148,7 +148,10 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
peerUid: peer.peerUid,
|
peerUid: peer.peerUid,
|
||||||
chatType: peer.chatType,
|
chatType: peer.chatType,
|
||||||
}, (returnMsgAndResId.message).msgId);
|
}, (returnMsgAndResId.message).msgId);
|
||||||
return { message_id: msgShortId!, res_id: returnMsgAndResId.res_id! };
|
|
||||||
|
// 对gocq的forward_id进行兼容
|
||||||
|
const resId = returnMsgAndResId.res_id!;
|
||||||
|
return { message_id: msgShortId!, res_id: resId, forward_id: resId };
|
||||||
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
|
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
|
||||||
throw Error(`发送转发消息(res_id:${returnMsgAndResId.res_id} 失败`);
|
throw Error(`发送转发消息(res_id:${returnMsgAndResId.res_id} 失败`);
|
||||||
}
|
}
|
||||||
@@ -174,9 +177,11 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
nickname: string,
|
nickname: string,
|
||||||
}, dp: number = 0): Promise<{
|
}, dp: number = 0): Promise<{
|
||||||
finallySendElements: SendArkElement,
|
finallySendElements: SendArkElement,
|
||||||
res_id?: string
|
res_id?: string,
|
||||||
|
deleteAfterSentFiles: string[],
|
||||||
} | null> {
|
} | null> {
|
||||||
const packetMsg: PacketMsg[] = [];
|
const packetMsg: PacketMsg[] = [];
|
||||||
|
let delFiles: string[] = [];
|
||||||
for (const node of messageNodes) {
|
for (const node of messageNodes) {
|
||||||
if (dp >= 3) {
|
if (dp >= 3) {
|
||||||
this.core.context.logger.logWarn('转发消息深度超过3层,将停止解析!');
|
this.core.context.logger.logWarn('转发消息深度超过3层,将停止解析!');
|
||||||
@@ -192,9 +197,11 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
nickname: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? 'QQ用户',
|
nickname: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? 'QQ用户',
|
||||||
}, dp + 1);
|
}, dp + 1);
|
||||||
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
|
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
|
||||||
|
delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || []));
|
||||||
} else {
|
} else {
|
||||||
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
|
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
|
||||||
sendElements = sendElementsCreateReturn.sendElements;
|
sendElements = sendElementsCreateReturn.sendElements;
|
||||||
|
delFiles.push(...sendElementsCreateReturn.deleteAfterSentFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
const packetMsgElements: rawMsgWithSendMsg = {
|
const packetMsgElements: rawMsgWithSendMsg = {
|
||||||
@@ -218,7 +225,8 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0];
|
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0];
|
||||||
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
|
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
|
||||||
if (msg) {
|
if (msg) {
|
||||||
await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
|
let msgCache = await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
|
||||||
|
delFiles.push(...msgCache);
|
||||||
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgToPacketMsg(msg, msgPeer);
|
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgToPacketMsg(msg, msgPeer);
|
||||||
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
|
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
|
||||||
packetMsg.push(transformedMsg);
|
packetMsg.push(transformedMsg);
|
||||||
@@ -234,6 +242,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
|
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
|
||||||
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt);
|
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt);
|
||||||
return {
|
return {
|
||||||
|
deleteAfterSentFiles: delFiles,
|
||||||
finallySendElements: {
|
finallySendElements: {
|
||||||
elementType: ElementType.ARK,
|
elementType: ElementType.ARK,
|
||||||
elementId: '',
|
elementId: '',
|
||||||
@@ -255,7 +264,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
|
|||||||
const res_id = uploadReturnData?.res_id;
|
const res_id = uploadReturnData?.res_id;
|
||||||
const finallySendElements = uploadReturnData?.finallySendElements;
|
const finallySendElements = uploadReturnData?.finallySendElements;
|
||||||
if (!finallySendElements) throw Error('转发消息失败,生成节点为空');
|
if (!finallySendElements) throw Error('转发消息失败,生成节点为空');
|
||||||
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], []).catch(() => undefined);
|
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], uploadReturnData.deleteAfterSentFiles || []).catch(() => undefined);
|
||||||
return { message: returnMsg ?? null, res_id: res_id! };
|
return { message: returnMsg ?? null, res_id: res_id! };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
import { ContextMode, SendMsgBase } from './SendMsg';
|
import { ContextMode, ReturnDataType, SendMsgBase } from './SendMsg';
|
||||||
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
|
||||||
import { OB11PostSendMsg } from '@/onebot/types';
|
import { OB11PostSendMsg } from '@/onebot/types';
|
||||||
|
|
||||||
// 未检测参数
|
// 未检测参数
|
||||||
class SendPrivateMsg extends SendMsgBase {
|
class SendPrivateMsg extends SendMsgBase {
|
||||||
override actionName = ActionName.SendPrivateMsg;
|
override actionName = ActionName.SendPrivateMsg;
|
||||||
override contextMode: ContextMode = ContextMode.Private;
|
|
||||||
|
|
||||||
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||||
payload.message_type = 'private';
|
payload.message_type = 'private';
|
||||||
return super.check(payload);
|
return super.check(payload);
|
||||||
}
|
}
|
||||||
|
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
|
||||||
|
return this.base_handle(payload, ContextMode.Private);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SendPrivateMsg;
|
export default SendPrivateMsg;
|
||||||
|
@@ -3,21 +3,36 @@ import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
|||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const SchemaData = Type.Object({
|
||||||
group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
|
group_id: Type.Optional(Type.String()),
|
||||||
user_id: Type.Union([Type.Number(), Type.String()]),
|
user_id: Type.Optional(Type.String()),
|
||||||
|
target_id: Type.Optional(Type.String()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
export class SendPokeBase extends GetPacketStatusDepends<Payload, void> {
|
||||||
export class SendPoke extends GetPacketStatusDepends<Payload, void> {
|
|
||||||
override actionName = ActionName.SendPoke;
|
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = SchemaData;
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
if (payload.group_id) {
|
// 这里的 !! 可以传入空字符串 忽略这些数据有利用接口统一接口
|
||||||
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
|
const target_id = !!payload.target_id ? payload.target_id : payload.user_id;
|
||||||
} else {
|
const peer_id = !!payload.group_id ? payload.group_id : payload.user_id;
|
||||||
await this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
|
|
||||||
|
const is_group = !!payload.group_id;
|
||||||
|
if (!target_id || !peer_id) {
|
||||||
|
throw new Error('请检查参数,缺少 user_id 或 group_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.core.apis.PacketApi.pkt.operation.SendPoke(is_group, +peer_id, +target_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class SendPoke extends SendPokeBase {
|
||||||
|
override actionName = ActionName.SendPoke;
|
||||||
|
}
|
||||||
|
export class GroupPoke extends SendPokeBase {
|
||||||
|
override actionName = ActionName.GroupPoke;
|
||||||
|
}
|
||||||
|
export class FriendPoke extends SendPokeBase {
|
||||||
|
override actionName = ActionName.FriendPoke;
|
||||||
|
}
|
||||||
|
@@ -10,6 +10,10 @@ export interface InvalidCheckResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ActionName = {
|
export const ActionName = {
|
||||||
|
SetGroupKickMembers: 'set_group_kick_members',
|
||||||
|
SetGroupRobotAddOption: 'set_group_robot_add_option',
|
||||||
|
SetGroupAddOption: 'set_group_add_option',
|
||||||
|
SetGroupSearch: 'set_group_search',
|
||||||
// new extends 完全差异OneBot类别
|
// new extends 完全差异OneBot类别
|
||||||
GetDoubtFriendsAddRequest: 'get_doubt_friends_add_request',
|
GetDoubtFriendsAddRequest: 'get_doubt_friends_add_request',
|
||||||
SetDoubtFriendsAddRequest: 'set_doubt_friends_add_request',
|
SetDoubtFriendsAddRequest: 'set_doubt_friends_add_request',
|
||||||
@@ -59,7 +63,7 @@ export const ActionName = {
|
|||||||
GetStatus: 'get_status',
|
GetStatus: 'get_status',
|
||||||
GetVersionInfo: 'get_version_info',
|
GetVersionInfo: 'get_version_info',
|
||||||
// Reboot : 'set_restart',
|
// Reboot : 'set_restart',
|
||||||
CleanCache : 'clean_cache',
|
CleanCache: 'clean_cache',
|
||||||
Exit: 'bot_exit',
|
Exit: 'bot_exit',
|
||||||
// go-cqhttp
|
// go-cqhttp
|
||||||
SetQQProfile: 'set_qq_profile',
|
SetQQProfile: 'set_qq_profile',
|
||||||
@@ -128,6 +132,7 @@ export const ActionName = {
|
|||||||
FetchEmojiLike: 'fetch_emoji_like',
|
FetchEmojiLike: 'fetch_emoji_like',
|
||||||
SetInputStatus: 'set_input_status',
|
SetInputStatus: 'set_input_status',
|
||||||
GetGroupInfoEx: 'get_group_info_ex',
|
GetGroupInfoEx: 'get_group_info_ex',
|
||||||
|
GetGroupDetailInfo: 'get_group_detail_info',
|
||||||
GetGroupIgnoreAddRequest: 'get_group_ignore_add_request',
|
GetGroupIgnoreAddRequest: 'get_group_ignore_add_request',
|
||||||
DelGroupNotice: '_del_group_notice',
|
DelGroupNotice: '_del_group_notice',
|
||||||
FriendPoke: 'friend_poke',
|
FriendPoke: 'friend_poke',
|
||||||
|
@@ -4,6 +4,7 @@ import { ActionName } from '@/onebot/action/router';
|
|||||||
import { Notify } from '@/onebot/types';
|
import { Notify } from '@/onebot/types';
|
||||||
|
|
||||||
interface RetData {
|
interface RetData {
|
||||||
|
invited_requests: Notify[];
|
||||||
InvitedRequest: Notify[];
|
InvitedRequest: Notify[];
|
||||||
join_requests: Notify[];
|
join_requests: Notify[];
|
||||||
}
|
}
|
||||||
@@ -13,7 +14,7 @@ export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
|
|||||||
|
|
||||||
async _handle(): Promise<RetData> {
|
async _handle(): Promise<RetData> {
|
||||||
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
|
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
|
||||||
const retData: RetData = { InvitedRequest: [], join_requests: [] };
|
const retData: RetData = { invited_requests: [], InvitedRequest: [], join_requests: [] };
|
||||||
|
|
||||||
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
|
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
|
||||||
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
||||||
@@ -39,6 +40,7 @@ export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
|
|||||||
|
|
||||||
await Promise.all(notifyPromises);
|
await Promise.all(notifyPromises);
|
||||||
|
|
||||||
|
retData.invited_requests = retData.InvitedRequest;
|
||||||
return retData;
|
return retData;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,18 +0,0 @@
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
|
||||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
|
||||||
user_id: Type.Union([Type.Number(), Type.String()])
|
|
||||||
});
|
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
|
||||||
|
|
||||||
export class FriendPoke extends GetPacketStatusDepends<Payload, void> {
|
|
||||||
override actionName = ActionName.FriendPoke;
|
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
|
||||||
await this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -85,9 +85,6 @@ export class OneBotMsgApi {
|
|||||||
textElement: async element => {
|
textElement: async element => {
|
||||||
if (element.atType === NTMsgAtType.ATTYPEUNKNOWN) {
|
if (element.atType === NTMsgAtType.ATTYPEUNKNOWN) {
|
||||||
let text = element.content;
|
let text = element.content;
|
||||||
if (!text.trim()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// 兼容 9.7.x 换行符
|
// 兼容 9.7.x 换行符
|
||||||
if (text.indexOf('\n') === -1 && text.indexOf('\r\n') === -1) {
|
if (text.indexOf('\n') === -1 && text.indexOf('\r\n') === -1) {
|
||||||
text = text.replace(/\r/g, '\n');
|
text = text.replace(/\r/g, '\n');
|
||||||
@@ -100,7 +97,7 @@ export class OneBotMsgApi {
|
|||||||
let qq: string = 'all';
|
let qq: string = 'all';
|
||||||
if (element.atType !== NTMsgAtType.ATTYPEALL) {
|
if (element.atType !== NTMsgAtType.ATTYPEALL) {
|
||||||
const { atNtUid, atUid } = element;
|
const { atNtUid, atUid } = element;
|
||||||
qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid;
|
qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : String(Number(atUid) >>> 0);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: OB11MessageDataType.at,
|
type: OB11MessageDataType.at,
|
||||||
@@ -150,12 +147,31 @@ export class OneBotMsgApi {
|
|||||||
};
|
};
|
||||||
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid);
|
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid);
|
||||||
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
||||||
|
if (this.core.apis.PacketApi.available) {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = await this.core.apis.FileApi.getFileUrl(msg.chatType, msg.peerUid, element.fileUuid, element.file10MMd5)
|
||||||
|
} catch (error) {
|
||||||
|
url = '';
|
||||||
|
}
|
||||||
|
if (url) {
|
||||||
|
return {
|
||||||
|
type: OB11MessageDataType.file,
|
||||||
|
data: {
|
||||||
|
file: element.fileName,
|
||||||
|
file_id: element.fileUuid,
|
||||||
|
file_size: element.fileSize,
|
||||||
|
url: url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: OB11MessageDataType.file,
|
type: OB11MessageDataType.file,
|
||||||
data: {
|
data: {
|
||||||
file: element.fileName,
|
file: element.fileName,
|
||||||
file_id: element.fileUuid,
|
file_id: element.fileUuid,
|
||||||
file_size: element.fileSize,
|
file_size: element.fileSize
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -225,17 +241,13 @@ export class OneBotMsgApi {
|
|||||||
},
|
},
|
||||||
|
|
||||||
replyElement: async (element, msg) => {
|
replyElement: async (element, msg) => {
|
||||||
const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords);
|
|
||||||
const peer = {
|
const peer = {
|
||||||
chatType: msg.chatType,
|
chatType: msg.chatType,
|
||||||
peerUid: msg.peerUid,
|
peerUid: msg.peerUid,
|
||||||
guildId: '',
|
guildId: '',
|
||||||
};
|
};
|
||||||
if (!records || !element.replyMsgTime || !element.senderUidStr) {
|
|
||||||
this.core.context.logger.logError('似乎是旧版客户端,获取不到引用的消息', element.replayMsgSeq);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 创建回复数据的通用方法
|
||||||
const createReplyData = (msgId: string): OB11MessageData => ({
|
const createReplyData = (msgId: string): OB11MessageData => ({
|
||||||
type: OB11MessageDataType.reply,
|
type: OB11MessageDataType.reply,
|
||||||
data: {
|
data: {
|
||||||
@@ -243,48 +255,96 @@ export class OneBotMsgApi {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (records.peerUin === '284840486' || records.peerUin === '1094950020') {
|
// 查找记录
|
||||||
|
const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords);
|
||||||
|
|
||||||
|
// 特定账号的特殊处理
|
||||||
|
if (records && (records.peerUin === '284840486' || records.peerUin === '1094950020')) {
|
||||||
return createReplyData(records.msgId);
|
return createReplyData(records.msgId);
|
||||||
}
|
}
|
||||||
let replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(peer, element.replayMsgSeq, records.msgTime, [element.senderUidStr])).msgList;
|
|
||||||
let replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
|
||||||
|
|
||||||
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
// 获取消息的通用方法组
|
||||||
this.core.context.logger.logError(
|
const tryFetchMethods = async (msgSeq: string, senderUid?: string, msgTime?: string, msgRandom?: string): Promise<RawMessage | undefined> => {
|
||||||
'筛选结果,筛选消息失败,将使用Fallback-1 Seq: ',
|
try {
|
||||||
|
// 方法1:通过序号和时间筛选
|
||||||
|
if (senderUid && msgTime) {
|
||||||
|
const replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(
|
||||||
|
peer, msgSeq, msgTime, [senderUid]
|
||||||
|
)).msgList;
|
||||||
|
|
||||||
|
const replyMsg = msgRandom
|
||||||
|
? replyMsgList.find(msg => msg.msgRandom === msgRandom)
|
||||||
|
: replyMsgList.find(msg => msg.msgSeq === msgSeq);
|
||||||
|
|
||||||
|
if (replyMsg) return replyMsg;
|
||||||
|
|
||||||
|
this.core.context.logger.logWarn(`方法1查询失败,序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法2:直接通过序号获取
|
||||||
|
const replyMsgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount(
|
||||||
|
peer, msgSeq, 1, true, true
|
||||||
|
)).msgList;
|
||||||
|
|
||||||
|
const replyMsg = msgRandom
|
||||||
|
? replyMsgList.find(msg => msg.msgRandom === msgRandom)
|
||||||
|
: replyMsgList.find(msg => msg.msgSeq === msgSeq);
|
||||||
|
|
||||||
|
if (replyMsg) return replyMsg;
|
||||||
|
|
||||||
|
this.core.context.logger.logWarn(`方法2查询失败,序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
|
||||||
|
|
||||||
|
// 方法3:另一种筛选方式
|
||||||
|
if (senderUid) {
|
||||||
|
const replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(
|
||||||
|
peer, msgSeq, [senderUid]
|
||||||
|
)).msgList;
|
||||||
|
|
||||||
|
const replyMsg = msgRandom
|
||||||
|
? replyMsgList.find(msg => msg.msgRandom === msgRandom)
|
||||||
|
: replyMsgList.find(msg => msg.msgSeq === msgSeq);
|
||||||
|
|
||||||
|
if (replyMsg) return replyMsg;
|
||||||
|
|
||||||
|
this.core.context.logger.logWarn(`方法3查询失败,序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
} catch (error) {
|
||||||
|
this.core.context.logger.logError('查询回复消息出错', error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 有记录情况下,使用完整信息查询
|
||||||
|
if (records && element.replyMsgTime && element.senderUidStr) {
|
||||||
|
const replyMsg = await tryFetchMethods(
|
||||||
element.replayMsgSeq,
|
element.replayMsgSeq,
|
||||||
',消息长度:',
|
element.senderUidStr,
|
||||||
replyMsgList.length
|
records.msgTime,
|
||||||
|
records.msgRandom
|
||||||
);
|
);
|
||||||
replyMsgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount(peer, element.replayMsgSeq, 1, true, true)).msgList;
|
|
||||||
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
if (replyMsg) {
|
||||||
|
return createReplyData(replyMsg.msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.core.context.logger.logError('所有查找方法均失败,获取不到带记录的引用消息', element.replayMsgSeq);
|
||||||
|
} else {
|
||||||
|
// 旧版客户端或不完整记录的情况,也尝试使用相同流程
|
||||||
|
this.core.context.logger.logWarn('似乎是旧版客户端,尝试仅通过序号获取引用消息', element.replayMsgSeq);
|
||||||
|
|
||||||
|
const replyMsg = await tryFetchMethods(element.replayMsgSeq);
|
||||||
|
|
||||||
|
if (replyMsg) {
|
||||||
|
return createReplyData(replyMsg.msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.core.context.logger.logError('所有查找方法均失败,获取不到旧客户端的引用消息', element.replayMsgSeq);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
return null;
|
||||||
this.core.context.logger.logWarn(
|
|
||||||
'筛选消息失败,将使用Fallback-2 Seq:',
|
|
||||||
element.replayMsgSeq,
|
|
||||||
',消息长度:',
|
|
||||||
replyMsgList.length
|
|
||||||
);
|
|
||||||
replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(peer, element.replayMsgSeq, [element.senderUidStr])).msgList;
|
|
||||||
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 丢弃该消息段
|
|
||||||
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
|
||||||
this.core.context.logger.logError(
|
|
||||||
'最终筛选结果,筛选消息失败,获取不到引用的消息 Seq: ',
|
|
||||||
element.replayMsgSeq,
|
|
||||||
',消息长度:',
|
|
||||||
replyMsgList.length
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return createReplyData(replyMsg.msgId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
videoElement: async (element, msg, elementWrapper) => {
|
videoElement: async (element, msg, elementWrapper) => {
|
||||||
const peer = {
|
const peer = {
|
||||||
chatType: msg.chatType,
|
chatType: msg.chatType,
|
||||||
@@ -331,7 +391,17 @@ export class OneBotMsgApi {
|
|||||||
|
|
||||||
//开始兜底
|
//开始兜底
|
||||||
if (!videoDownUrl) {
|
if (!videoDownUrl) {
|
||||||
videoDownUrl = element.filePath;
|
if (this.core.apis.PacketApi.available) {
|
||||||
|
try {
|
||||||
|
videoDownUrl = await this.core.apis.FileApi.getVideoUrlPacket(msg.peerUid, element.fileUuid);
|
||||||
|
} catch (e) {
|
||||||
|
this.core.context.logger.logError('获取视频url失败', (e as Error).stack);
|
||||||
|
videoDownUrl = element.filePath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
videoDownUrl = element.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
||||||
return {
|
return {
|
||||||
@@ -351,6 +421,28 @@ export class OneBotMsgApi {
|
|||||||
guildId: '',
|
guildId: '',
|
||||||
};
|
};
|
||||||
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, '', element.fileName);
|
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, '', element.fileName);
|
||||||
|
let pttUrl = '';
|
||||||
|
if (this.core.apis.PacketApi.available) {
|
||||||
|
try {
|
||||||
|
pttUrl = await this.core.apis.FileApi.getPttUrl(msg.peerUid, element.fileUuid);
|
||||||
|
} catch (e) {
|
||||||
|
this.core.context.logger.logError('获取语音url失败', (e as Error).stack);
|
||||||
|
pttUrl = element.filePath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pttUrl = element.filePath;
|
||||||
|
}
|
||||||
|
if (pttUrl) {
|
||||||
|
return {
|
||||||
|
type: OB11MessageDataType.voice,
|
||||||
|
data: {
|
||||||
|
file: fileCode,
|
||||||
|
path: element.filePath,
|
||||||
|
url: pttUrl,
|
||||||
|
file_size: element.fileSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: OB11MessageDataType.voice,
|
type: OB11MessageDataType.voice,
|
||||||
data: {
|
data: {
|
||||||
@@ -1117,7 +1209,6 @@ export class OneBotMsgApi {
|
|||||||
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
|
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
|
||||||
const groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
|
const groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
|
||||||
const isAdminOrOwner = groupRole === 3 || groupRole === 4;
|
const isAdminOrOwner = groupRole === 3 || groupRole === 4;
|
||||||
|
|
||||||
if (isAdminOrOwner && !operatorUid) {
|
if (isAdminOrOwner && !operatorUid) {
|
||||||
let dataNotify: GroupNotify | undefined;
|
let dataNotify: GroupNotify | undefined;
|
||||||
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
|
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
|
||||||
@@ -1147,7 +1238,7 @@ export class OneBotMsgApi {
|
|||||||
const operatorUid = await this.waitGroupNotify(
|
const operatorUid = await this.waitGroupNotify(
|
||||||
groupChange.groupUin.toString(),
|
groupChange.groupUin.toString(),
|
||||||
groupChange.memberUid,
|
groupChange.memberUid,
|
||||||
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : ''
|
groupChange.operatorInfo ? new TextDecoder('utf-8').decode(groupChange.operatorInfo) : undefined
|
||||||
);
|
);
|
||||||
return new OB11GroupIncreaseEvent(
|
return new OB11GroupIncreaseEvent(
|
||||||
this.core,
|
this.core,
|
||||||
@@ -1159,13 +1250,42 @@ export class OneBotMsgApi {
|
|||||||
|
|
||||||
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
|
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
|
||||||
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
|
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
|
||||||
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
|
|
||||||
|
let operator_uid_parse: string | undefined = undefined;
|
||||||
|
if (groupChange.operatorInfo) {
|
||||||
|
// 先判断是否可能是protobuf(自身被踢出或以0a开头)
|
||||||
|
if (groupChange.decreaseType === 3 || Buffer.from(groupChange.operatorInfo).toString('hex').startsWith('0a')) {
|
||||||
|
// 可能是protobuf,尝试解析
|
||||||
|
try {
|
||||||
|
operator_uid_parse = new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid;
|
||||||
|
} catch (error) {
|
||||||
|
// protobuf解析失败,fallback到字符串解析
|
||||||
|
try {
|
||||||
|
const decoded = new TextDecoder('utf-8').decode(groupChange.operatorInfo);
|
||||||
|
// 检查是否包含非ASCII字符,如果包含则丢弃
|
||||||
|
const isAsciiOnly = [...decoded].every(char => char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126);
|
||||||
|
operator_uid_parse = isAsciiOnly ? decoded : '';
|
||||||
|
} catch (e2) {
|
||||||
|
operator_uid_parse = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 直接进行字符串解析
|
||||||
|
try {
|
||||||
|
const decoded = new TextDecoder('utf-8').decode(groupChange.operatorInfo);
|
||||||
|
// 检查是否包含非ASCII字符,如果包含则丢弃
|
||||||
|
const isAsciiOnly = [...decoded].every(char => char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126);
|
||||||
|
operator_uid_parse = isAsciiOnly ? decoded : '';
|
||||||
|
} catch (e) {
|
||||||
|
operator_uid_parse = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const operatorUid = await this.waitGroupNotify(
|
const operatorUid = await this.waitGroupNotify(
|
||||||
groupChange.groupUin.toString(),
|
groupChange.groupUin.toString(),
|
||||||
groupChange.memberUid,
|
groupChange.memberUid,
|
||||||
groupChange.decreaseType === 3 && groupChange.operatorInfo ?
|
operator_uid_parse
|
||||||
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
|
|
||||||
groupChange.operatorInfo?.toString()
|
|
||||||
);
|
);
|
||||||
if (groupChange.memberUid === this.core.selfInfo.uid) {
|
if (groupChange.memberUid === this.core.selfInfo.uid) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@@ -2,4 +2,5 @@ import { EventType, OneBotEvent } from '@/onebot/event/OneBotEvent';
|
|||||||
|
|
||||||
export abstract class OB11BaseNoticeEvent extends OneBotEvent {
|
export abstract class OB11BaseNoticeEvent extends OneBotEvent {
|
||||||
post_type = EventType.NOTICE;
|
post_type = EventType.NOTICE;
|
||||||
}
|
abstract notice_type: string;
|
||||||
|
}
|
||||||
|
6
src/onebot/event/request/OB11BaseRequestEvent.ts
Normal file
6
src/onebot/event/request/OB11BaseRequestEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { EventType, OneBotEvent } from '@/onebot/event/OneBotEvent';
|
||||||
|
|
||||||
|
export abstract class OB11BaseRequestEvent extends OneBotEvent {
|
||||||
|
readonly post_type = EventType.REQUEST;
|
||||||
|
abstract request_type: string;
|
||||||
|
}
|
@@ -1,10 +1,8 @@
|
|||||||
import { OB11BaseNoticeEvent } from '@/onebot/event/notice/OB11BaseNoticeEvent';
|
|
||||||
import { EventType } from '@/onebot/event/OneBotEvent';
|
|
||||||
import { NapCatCore } from '@/core';
|
import { NapCatCore } from '@/core';
|
||||||
|
import { OB11BaseRequestEvent } from './OB11BaseRequestEvent';
|
||||||
|
|
||||||
export class OB11FriendRequestEvent extends OB11BaseNoticeEvent {
|
export class OB11FriendRequestEvent extends OB11BaseRequestEvent {
|
||||||
override post_type = EventType.REQUEST;
|
override request_type = 'friend';
|
||||||
request_type = 'friend';
|
|
||||||
|
|
||||||
user_id: number;
|
user_id: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
import { OB11GroupNoticeEvent } from '@/onebot/event/notice/OB11GroupNoticeEvent';
|
|
||||||
import { EventType } from '@/onebot/event/OneBotEvent';
|
|
||||||
import { NapCatCore } from '@/core';
|
import { NapCatCore } from '@/core';
|
||||||
|
import { OB11BaseRequestEvent } from './OB11BaseRequestEvent';
|
||||||
|
|
||||||
export class OB11GroupRequestEvent extends OB11GroupNoticeEvent {
|
export class OB11GroupRequestEvent extends OB11BaseRequestEvent {
|
||||||
override post_type = EventType.REQUEST;
|
override readonly request_type = 'group' as const;
|
||||||
request_type = 'group';
|
|
||||||
|
|
||||||
override user_id: number;
|
group_id: number;
|
||||||
|
user_id: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
flag: string;
|
flag: string;
|
||||||
sub_type: string;
|
sub_type: string;
|
||||||
|
|
||||||
constructor(core: NapCatCore, groupId: number, userId: number, sub_type: string, comment: string, flag: string) {
|
constructor(core: NapCatCore, groupId: number, userId: number, sub_type: string, comment: string, flag: string) {
|
||||||
super(core, groupId, userId);
|
super(core);
|
||||||
|
this.group_id = groupId;
|
||||||
this.user_id = userId;
|
this.user_id = userId;
|
||||||
this.sub_type = sub_type;
|
this.sub_type = sub_type;
|
||||||
this.comment = comment;
|
this.comment = comment;
|
||||||
|
@@ -50,7 +50,6 @@ import {
|
|||||||
import { OB11Message } from './types';
|
import { OB11Message } from './types';
|
||||||
import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
||||||
import { OB11HttpSSEServerAdapter } from './network/http-server-sse';
|
import { OB11HttpSSEServerAdapter } from './network/http-server-sse';
|
||||||
import { OB11PluginAdapter } from './network/plugin';
|
|
||||||
|
|
||||||
//OneBot实现类
|
//OneBot实现类
|
||||||
export class NapCatOneBot11Adapter {
|
export class NapCatOneBot11Adapter {
|
||||||
@@ -114,9 +113,9 @@ export class NapCatOneBot11Adapter {
|
|||||||
//创建NetWork服务
|
//创建NetWork服务
|
||||||
|
|
||||||
// 注册Plugin 如果需要基于NapCat进行快速开发
|
// 注册Plugin 如果需要基于NapCat进行快速开发
|
||||||
this.networkManager.registerAdapter(
|
// this.networkManager.registerAdapter(
|
||||||
new OB11PluginAdapter('myPlugin', this.core, this, this.actions)
|
// new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
|
||||||
);
|
// );
|
||||||
for (const key of ob11Config.network.httpServers) {
|
for (const key of ob11Config.network.httpServers) {
|
||||||
if (key.enable) {
|
if (key.enable) {
|
||||||
this.networkManager.registerAdapter(
|
this.networkManager.registerAdapter(
|
||||||
@@ -271,7 +270,6 @@ export class NapCatOneBot11Adapter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
msgListener.onAddSendMsg = async (msg) => {
|
msgListener.onAddSendMsg = async (msg) => {
|
||||||
try {
|
try {
|
||||||
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
|
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
|
||||||
@@ -283,7 +281,8 @@ export class NapCatOneBot11Adapter {
|
|||||||
}, 1, 10 * 60 * 1000);
|
}, 1, 10 * 60 * 1000);
|
||||||
// 10分钟 超时
|
// 10分钟 超时
|
||||||
const updatemsg = updatemsgs.find((e) => e.msgId === msg.msgId);
|
const updatemsg = updatemsgs.find((e) => e.msgId === msg.msgId);
|
||||||
if (updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS || updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS_NOSEQ) {
|
// updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS_NOSEQ NOSEQ一般是服务器未下发SEQ 这意味着这条消息不应该推送network
|
||||||
|
if (updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS) {
|
||||||
updatemsg.id = MessageUnique.createUniqueMsgId(
|
updatemsg.id = MessageUnique.createUniqueMsgId(
|
||||||
{
|
{
|
||||||
chatType: updatemsg.chatType,
|
chatType: updatemsg.chatType,
|
||||||
@@ -305,8 +304,18 @@ export class NapCatOneBot11Adapter {
|
|||||||
peerUid: uid,
|
peerUid: uid,
|
||||||
guildId: ''
|
guildId: ''
|
||||||
};
|
};
|
||||||
const msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
|
let msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
|
||||||
const element = msg?.elements.find(e => !!e.grayTipElement?.revokeElement);
|
const element = msg?.elements.find(e => !!e.grayTipElement?.revokeElement);
|
||||||
|
if (element?.grayTipElement?.revokeElement.isSelfOperate && msg) {
|
||||||
|
await this.core.eventWrapper.registerListen('NodeIKernelMsgListener/onMsgRecall',
|
||||||
|
(chatType: ChatType, uid: string, msgSeq: string) => {
|
||||||
|
return chatType === msg?.chatType && uid === msg?.peerUid && msgSeq === msg?.msgSeq;
|
||||||
|
}
|
||||||
|
).catch(() => {
|
||||||
|
msg = undefined;
|
||||||
|
this.context.logger.logDebug('自操作消息撤回事件');
|
||||||
|
});
|
||||||
|
}
|
||||||
if (msg && element) {
|
if (msg && element) {
|
||||||
const recallEvent = await this.emitRecallMsg(msg, element);
|
const recallEvent = await this.emitRecallMsg(msg, element);
|
||||||
try {
|
try {
|
||||||
@@ -317,6 +326,7 @@ export class NapCatOneBot11Adapter {
|
|||||||
this.context.logger.logError('处理消息撤回失败', e);
|
this.context.logger.logError('处理消息撤回失败', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
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);
|
||||||
@@ -335,7 +345,7 @@ export class NapCatOneBot11Adapter {
|
|||||||
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];
|
||||||
if (!req) continue;
|
if (!req) continue;
|
||||||
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
|
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM) || !req.isUnread) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -353,7 +363,6 @@ export class NapCatOneBot11Adapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.context.session
|
this.context.session
|
||||||
.getBuddyService()
|
.getBuddyService()
|
||||||
.addKernelBuddyListener(proxiedListenerOf(buddyListener, this.context.logger));
|
.addKernelBuddyListener(proxiedListenerOf(buddyListener, this.context.logger));
|
||||||
|
@@ -87,8 +87,8 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
|
|||||||
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.config.port, () => {
|
this.server.listen(this.config.port, this.config.host, () => {
|
||||||
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.config.port}`);
|
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On ${this.config.host}:${this.config.port}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,14 +15,14 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
|||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
reportSelfMessage: false,
|
reportSelfMessage: false,
|
||||||
enable: true,
|
enable: true,
|
||||||
debug: true,
|
debug: false,
|
||||||
};
|
};
|
||||||
super(name, config, core, obContext, actions);
|
super(name, config, core, obContext, actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent<T extends OB11EmitEventContent>(event: T) {
|
onEvent<T extends OB11EmitEventContent>(event: T) {
|
||||||
if (event.post_type === 'message') {
|
if (event.post_type === 'message') {
|
||||||
plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11Message, this.actions, this).then().catch(console.log);
|
plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11Message, this.actions, this).then().catch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -39,8 +39,11 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
wsClient.close();
|
wsClient.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//鉴权
|
// 鉴权 close 不会立刻销毁 当前返回可避免挂载message事件 close 并未立刻关闭 而是存在timer操作后关闭
|
||||||
this.authorize(this.config.token, wsClient, wsReq);
|
// 引发高危漏洞
|
||||||
|
if (!this.authorize(this.config.token, wsClient, wsReq)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
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) {
|
||||||
@@ -145,15 +148,16 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
|
private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
|
||||||
if (!token || token.length == 0) return;//客户端未设置密钥
|
if (!token || token.length == 0) return true;//客户端未设置密钥
|
||||||
const QueryClientToken = urlParse.parse(wsReq?.url || '', true).query['access_token'];
|
const QueryClientToken = urlParse.parse(wsReq?.url || '', true).query['access_token'];
|
||||||
const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
|
const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
|
||||||
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
|
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
|
||||||
if (ClientToken === token) {
|
if (ClientToken === token) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
|
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
|
||||||
wsClient.close();
|
wsClient.close();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkStateAndReply<T>(data: T, wsClient: WebSocket) {
|
private checkStateAndReply<T>(data: T, wsClient: WebSocket) {
|
||||||
|
@@ -180,7 +180,7 @@ export interface OB11MessageNode {
|
|||||||
id?: string;
|
id?: string;
|
||||||
user_id?: number | string; // number
|
user_id?: number | string; // number
|
||||||
uin?: number | string; // number, compatible with go-cqhttp
|
uin?: number | string; // number, compatible with go-cqhttp
|
||||||
nickname?: string;
|
nickname: string;
|
||||||
name?: string; // compatible with go-cqhttp
|
name?: string; // compatible with go-cqhttp
|
||||||
content: OB11MessageMixType;
|
content: OB11MessageMixType;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
import { dirname } from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
export let current_path = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1,319 +0,0 @@
|
|||||||
import { createCanvas, loadImage } from "@napi-rs/canvas";
|
|
||||||
import path from "path";
|
|
||||||
import { current_path } from "./data";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制时间模式匹配的可视化图表
|
|
||||||
* @param data 需要绘制的数据和配置
|
|
||||||
* @returns Base64编码的图片
|
|
||||||
*/
|
|
||||||
export async function drawTimePattern(data: {
|
|
||||||
targetUser: string,
|
|
||||||
matchedUsers: Array<{
|
|
||||||
username: string,
|
|
||||||
similarity: number,
|
|
||||||
pattern: Map<string, number>
|
|
||||||
}>,
|
|
||||||
targetPattern: Map<string, number>,
|
|
||||||
timeRange: string
|
|
||||||
}) {
|
|
||||||
// 计算需要的画布高度,根据匹配用户数量可能需要更多空间
|
|
||||||
const legendRowHeight = 30; // 每行图例的高度,增加一点空间
|
|
||||||
const legendRows = Math.ceil(data.matchedUsers.length / 2) + 1; // 目标用户一行,其他匹配用户每两个一行
|
|
||||||
|
|
||||||
// 画布基础配置
|
|
||||||
const padding = 50;
|
|
||||||
const titleHeight = 80;
|
|
||||||
const hourChartHeight = 250;
|
|
||||||
const weekdayChartHeight = 180;
|
|
||||||
const legendTitleHeight = 40;
|
|
||||||
|
|
||||||
// 计算图例总高度,确保足够空间
|
|
||||||
const legendHeight = legendRows * legendRowHeight + legendTitleHeight;
|
|
||||||
|
|
||||||
// 计算所需的总高度
|
|
||||||
const requiredHeight = titleHeight + hourChartHeight + 60 + weekdayChartHeight + 40 + legendHeight + padding;
|
|
||||||
|
|
||||||
// 设置画布尺寸,确保足够显示所有内容
|
|
||||||
const width = 1000;
|
|
||||||
const height = requiredHeight + padding; // 确保底部有足够的padding
|
|
||||||
|
|
||||||
// 创建画布
|
|
||||||
const canvas = createCanvas(width, height);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
// 加载背景图
|
|
||||||
const backgroundImage = await loadImage(path.join(current_path,'./fonts/post.jpg'));
|
|
||||||
const pattern = ctx.createPattern(backgroundImage, 'repeat');
|
|
||||||
ctx.fillStyle = pattern;
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
// 应用模糊效果
|
|
||||||
ctx.filter = 'blur(5px)';
|
|
||||||
ctx.drawImage(canvas, 0, 0);
|
|
||||||
ctx.filter = 'none';
|
|
||||||
|
|
||||||
// 绘制半透明白色背景卡片
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
|
||||||
const radius = 20;
|
|
||||||
const cardWidth = width - padding * 2;
|
|
||||||
const cardHeight = height - padding * 2;
|
|
||||||
const cardX = padding;
|
|
||||||
const cardY = padding;
|
|
||||||
|
|
||||||
// 绘制圆角矩形
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(cardX + radius, cardY);
|
|
||||||
ctx.lineTo(cardX + cardWidth - radius, cardY);
|
|
||||||
ctx.quadraticCurveTo(cardX + cardWidth, cardY, cardX + cardWidth, cardY + radius);
|
|
||||||
ctx.lineTo(cardX + cardWidth, cardY + cardHeight - radius);
|
|
||||||
ctx.quadraticCurveTo(cardX + cardWidth, cardY + cardHeight, cardX + cardWidth - radius, cardY + cardHeight);
|
|
||||||
ctx.lineTo(cardX + radius, cardY + cardHeight);
|
|
||||||
ctx.quadraticCurveTo(cardX, cardY + cardHeight, cardX, cardY + cardHeight - radius);
|
|
||||||
ctx.lineTo(cardX, cardY + radius);
|
|
||||||
ctx.quadraticCurveTo(cardX, cardY, cardX + radius, cardY);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 设置标题
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.font = 'bold 24px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText(`${data.targetUser} ${data.timeRange}聊天时间匹配分析`, width / 2, cardY + 35);
|
|
||||||
|
|
||||||
// 绘制小时分布图表
|
|
||||||
const hourChartTop = cardY + titleHeight;
|
|
||||||
const hourChartBottom = hourChartTop + hourChartHeight;
|
|
||||||
const hourChartLeft = cardX + 40;
|
|
||||||
const hourChartRight = cardX + cardWidth - 40;
|
|
||||||
const hourChartWidth = hourChartRight - hourChartLeft;
|
|
||||||
|
|
||||||
// 绘制小时图表标题
|
|
||||||
ctx.font = 'bold 18px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.fillText('每日小时活跃度分布', width / 2, hourChartTop - 10);
|
|
||||||
|
|
||||||
// 绘制小时图表横坐标
|
|
||||||
ctx.fillStyle = '#666';
|
|
||||||
ctx.font = '14px "JetBrains Mono"';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
for (let hour = 0; hour < 24; hour += 2) {
|
|
||||||
const x = hourChartLeft + (hour / 24) * hourChartWidth;
|
|
||||||
ctx.fillText(`${hour}`, x, hourChartBottom + 20);
|
|
||||||
}
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.font = '14px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.fillText('时间(小时)', hourChartLeft, hourChartBottom + 40);
|
|
||||||
ctx.font = '14px "JetBrains Mono"';
|
|
||||||
|
|
||||||
// 绘制小时图表网格线
|
|
||||||
ctx.strokeStyle = '#ddd';
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
for (let hour = 0; hour < 24; hour += 2) {
|
|
||||||
const x = hourChartLeft + (hour / 24) * hourChartWidth;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, hourChartTop);
|
|
||||||
ctx.lineTo(x, hourChartBottom);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定最大活跃度值,用于缩放
|
|
||||||
let maxHourValue = 0;
|
|
||||||
for (let hour = 0; hour < 24; hour++) {
|
|
||||||
const targetValue = data.targetPattern.get(`hour_${hour}`) || 0;
|
|
||||||
if (targetValue > maxHourValue) maxHourValue = targetValue;
|
|
||||||
|
|
||||||
for (const match of data.matchedUsers) {
|
|
||||||
const matchValue = match.pattern.get(`hour_${hour}`) || 0;
|
|
||||||
if (matchValue > maxHourValue) maxHourValue = matchValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 为了美观,确保最大值不会让图表太扁
|
|
||||||
maxHourValue = Math.max(maxHourValue, 0.15);
|
|
||||||
|
|
||||||
// 绘制目标用户小时分布曲线
|
|
||||||
ctx.strokeStyle = '#e74c3c';
|
|
||||||
ctx.lineWidth = 3;
|
|
||||||
ctx.beginPath();
|
|
||||||
for (let hour = 0; hour < 24; hour++) {
|
|
||||||
const x = hourChartLeft + (hour / 24) * hourChartWidth;
|
|
||||||
const value = data.targetPattern.get(`hour_${hour}`) || 0;
|
|
||||||
const y = hourChartBottom - (value / maxHourValue) * (hourChartHeight - 30);
|
|
||||||
if (hour === 0) {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 绘制匹配用户小时分布曲线
|
|
||||||
const colors = ['#3498db', '#2ecc71', '#9b59b6', '#f1c40f', '#1abc9c'];
|
|
||||||
data.matchedUsers.forEach((match, index) => {
|
|
||||||
const colorIndex = index % colors.length;
|
|
||||||
ctx.strokeStyle = colors[colorIndex]!;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
for (let hour = 0; hour < 24; hour++) {
|
|
||||||
const x = hourChartLeft + (hour / 24) * hourChartWidth;
|
|
||||||
const value = match.pattern.get(`hour_${hour}`) || 0;
|
|
||||||
const y = hourChartBottom - (value / maxHourValue) * (hourChartHeight - 30);
|
|
||||||
if (hour === 0) {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 绘制星期分布图表
|
|
||||||
const weekChartTop = hourChartBottom + 60;
|
|
||||||
const weekChartBottom = weekChartTop + weekdayChartHeight;
|
|
||||||
const weekChartLeft = hourChartLeft;
|
|
||||||
const weekChartRight = hourChartRight;
|
|
||||||
const weekChartWidth = weekChartRight - weekChartLeft;
|
|
||||||
|
|
||||||
// 绘制星期图表标题
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.font = 'bold 18px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('星期活跃度分布', width / 2, weekChartTop - 10);
|
|
||||||
|
|
||||||
// 绘制星期图表横坐标
|
|
||||||
ctx.fillStyle = '#666';
|
|
||||||
ctx.font = '14px "Aa偷吃可爱长大的"';
|
|
||||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
||||||
for (let day = 0; day < 7; day++) {
|
|
||||||
const x = weekChartLeft + (day / 7) * weekChartWidth + (weekChartWidth / 14);
|
|
||||||
ctx.fillText(weekdays[day]!, x, weekChartBottom + 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制星期图表网格线
|
|
||||||
ctx.strokeStyle = '#ddd';
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
for (let day = 0; day <= 7; day++) {
|
|
||||||
const x = weekChartLeft + (day / 7) * weekChartWidth;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, weekChartTop);
|
|
||||||
ctx.lineTo(x, weekChartBottom);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定最大活跃度值,用于缩放
|
|
||||||
let maxDayValue = 0;
|
|
||||||
for (let day = 0; day < 7; day++) {
|
|
||||||
const targetValue = data.targetPattern.get(`day_${day}`) || 0;
|
|
||||||
if (targetValue > maxDayValue) maxDayValue = targetValue;
|
|
||||||
|
|
||||||
for (const match of data.matchedUsers) {
|
|
||||||
const matchValue = match.pattern.get(`day_${day}`) || 0;
|
|
||||||
if (matchValue > maxDayValue) maxDayValue = matchValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 为了美观,确保最大值不会让图表太扁
|
|
||||||
maxDayValue = Math.max(maxDayValue, 0.3);
|
|
||||||
|
|
||||||
// 改进柱状图绘制逻辑,避免重叠
|
|
||||||
const totalUsers = data.matchedUsers.length + 1; // 包括目标用户
|
|
||||||
const dayWidth = weekChartWidth / 7; // 每天的总宽度
|
|
||||||
|
|
||||||
// 计算单个柱状图宽度,确保有足够间距
|
|
||||||
const barWidth = dayWidth * 0.7 / totalUsers; // 每个柱子的宽度
|
|
||||||
const groupPadding = dayWidth * 0.15; // 不同天之间的组间距
|
|
||||||
const barPadding = dayWidth * 0.15 / (totalUsers + 1); // 同一天内柱子之间的间距
|
|
||||||
|
|
||||||
// 绘制所有用户的星期分布(包括目标用户和匹配用户)
|
|
||||||
const allUsers = [
|
|
||||||
{ username: data.targetUser, pattern: data.targetPattern, color: '#e74c3c', isTarget: true }
|
|
||||||
];
|
|
||||||
|
|
||||||
data.matchedUsers.forEach((match, idx) => {
|
|
||||||
allUsers.push({
|
|
||||||
username: match.username,
|
|
||||||
pattern: match.pattern,
|
|
||||||
color: colors[idx % colors.length] || '#3498db',
|
|
||||||
isTarget: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 统一绘制所有用户的柱状图
|
|
||||||
allUsers.forEach((user, userIndex) => {
|
|
||||||
ctx.fillStyle = user.color;
|
|
||||||
|
|
||||||
for (let day = 0; day < 7; day++) {
|
|
||||||
const value = user.pattern.get(`day_${day}`) || 0;
|
|
||||||
const barHeight = (value / maxDayValue) * (weekdayChartHeight - 30);
|
|
||||||
|
|
||||||
// 计算柱子的位置,确保均匀分布
|
|
||||||
const startX = weekChartLeft + day * dayWidth + groupPadding / 2;
|
|
||||||
const x = startX + barPadding * (userIndex + 1) + barWidth * userIndex;
|
|
||||||
const y = weekChartBottom - barHeight;
|
|
||||||
|
|
||||||
// 绘制柱子
|
|
||||||
ctx.fillRect(x, y, barWidth, barHeight);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// 绘制图例
|
|
||||||
let legendTop = weekChartBottom + 50; // 增加与上方图表的间距
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.font = '14px "Aa偷吃可爱长大的"';
|
|
||||||
|
|
||||||
// 绘制图例标题
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.font = 'bold 18px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('图例说明', width / 2, legendTop);
|
|
||||||
ctx.font = '14px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
|
|
||||||
// 计算图例开始位置和每列宽度
|
|
||||||
const legendStartX = hourChartLeft;
|
|
||||||
const legendColumnWidth = Math.min(450, (cardWidth - 80) / 2); // 确保在宽度有限时也能正常显示
|
|
||||||
const legendsPerRow = 2; // 每行最多2个图例
|
|
||||||
|
|
||||||
// 目标用户图例 - 单独一行
|
|
||||||
legendTop += 25; // 图例标题与第一个图例的间距
|
|
||||||
ctx.fillStyle = '#e74c3c';
|
|
||||||
ctx.fillRect(legendStartX, legendTop, 20, 10);
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.fillText(data.targetUser + " (目标用户)", legendStartX + 30, legendTop + 10);
|
|
||||||
|
|
||||||
// 匹配用户图例 - 每行最多2个用户
|
|
||||||
legendTop += legendRowHeight; // 进入下一行
|
|
||||||
|
|
||||||
data.matchedUsers.forEach((match, index) => {
|
|
||||||
const colorIndex = index % colors.length;
|
|
||||||
const row = Math.floor(index / legendsPerRow);
|
|
||||||
const col = index % legendsPerRow;
|
|
||||||
|
|
||||||
const x = legendStartX + col * legendColumnWidth;
|
|
||||||
const y = legendTop + row * legendRowHeight;
|
|
||||||
|
|
||||||
// 确保没有超出画布范围
|
|
||||||
if (y + 20 <= cardY + cardHeight - padding / 2) {
|
|
||||||
ctx.fillStyle = colors[colorIndex]!;
|
|
||||||
ctx.fillRect(x, y, 20, 10);
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
const similarity = (match.similarity * 100).toFixed(1);
|
|
||||||
|
|
||||||
// 测量文本长度,确保不超出列宽
|
|
||||||
const text = `${match.username} (${similarity}% 匹配)`;
|
|
||||||
const metrics = ctx.measureText(text);
|
|
||||||
if (metrics.width > legendColumnWidth - 40) {
|
|
||||||
// 如果文本过长,缩短显示
|
|
||||||
const shortUsername = match.username.length > 10 ?
|
|
||||||
match.username.substring(0, 10) + "..." :
|
|
||||||
match.username;
|
|
||||||
ctx.fillText(`${shortUsername} (${similarity}% 匹配)`, x + 30, y + 10);
|
|
||||||
} else {
|
|
||||||
ctx.fillText(text, x + 30, y + 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存图像
|
|
||||||
const buffer = canvas.toBuffer('image/png');
|
|
||||||
return "base64://" + buffer.toString('base64');
|
|
||||||
}
|
|
1817
src/plugin/index.ts
1817
src/plugin/index.ts
File diff suppressed because it is too large
Load Diff
@@ -1,432 +0,0 @@
|
|||||||
import { createCanvas, loadImage } from "@napi-rs/canvas";
|
|
||||||
import path from "path";
|
|
||||||
import { current_path } from "./data";
|
|
||||||
|
|
||||||
interface NetworkNode {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NetworkEdge {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NetworkData {
|
|
||||||
nodes: NetworkNode[];
|
|
||||||
edges: NetworkEdge[];
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function drawWordNetwork(data: NetworkData): Promise<string> {
|
|
||||||
// 根据节点数量动态调整画布尺寸
|
|
||||||
const nodeCount = data.nodes.length;
|
|
||||||
const baseWidth = 1000;
|
|
||||||
const baseHeight = 800;
|
|
||||||
|
|
||||||
// 根据节点数量计算合适的尺寸
|
|
||||||
const width = Math.max(baseWidth, Math.min(2000, baseWidth + (nodeCount - 10) * 30));
|
|
||||||
const height = Math.max(baseHeight, Math.min(1500, baseHeight + (nodeCount - 10) * 25));
|
|
||||||
|
|
||||||
// 根据画布大小调整边距
|
|
||||||
const padding = Math.max(60, Math.min(100, 60 + nodeCount / 20));
|
|
||||||
const centerX = width / 2;
|
|
||||||
const centerY = height / 2;
|
|
||||||
|
|
||||||
// 创建画布
|
|
||||||
const canvas = createCanvas(width, height);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
// 绘制背景
|
|
||||||
try {
|
|
||||||
const backgroundImage = await loadImage(path.join(current_path, '.\\fonts\\post.jpg'));
|
|
||||||
const pattern = ctx.createPattern(backgroundImage, 'repeat');
|
|
||||||
if (pattern) {
|
|
||||||
ctx.fillStyle = pattern;
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
// 添加模糊效果
|
|
||||||
ctx.filter = 'blur(5px)';
|
|
||||||
ctx.drawImage(canvas, 0, 0);
|
|
||||||
ctx.filter = 'none';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 如果背景图加载失败,使用纯色背景
|
|
||||||
ctx.fillStyle = '#f0f0f0';
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制半透明卡片背景
|
|
||||||
const cardWidth = width - padding * 2;
|
|
||||||
const cardHeight = height - padding * 2;
|
|
||||||
const cardX = padding;
|
|
||||||
const cardY = padding;
|
|
||||||
const radius = 20;
|
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(cardX + radius, cardY);
|
|
||||||
ctx.lineTo(cardX + cardWidth - radius, cardY);
|
|
||||||
ctx.quadraticCurveTo(cardX + cardWidth, cardY, cardX + cardWidth, cardY + radius);
|
|
||||||
ctx.lineTo(cardX + cardWidth, cardY + cardHeight - radius);
|
|
||||||
ctx.quadraticCurveTo(cardX + cardWidth, cardY + cardHeight, cardX + cardWidth - radius, cardY + cardHeight);
|
|
||||||
ctx.lineTo(cardX + radius, cardY + cardHeight);
|
|
||||||
ctx.quadraticCurveTo(cardX, cardY + cardHeight, cardX, cardY + cardHeight - radius);
|
|
||||||
ctx.lineTo(cardX, cardY + radius);
|
|
||||||
ctx.quadraticCurveTo(cardX, cardY, cardX + radius, cardY);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 绘制标题
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.font = '28px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText(data.title, centerX, cardY + 40);
|
|
||||||
|
|
||||||
// 计算节点位置 (使用简化版力导向算法)
|
|
||||||
const nodePositions = new Map<string, { x: number, y: number }>();
|
|
||||||
const radiusScale = 15; // 基础节点半径
|
|
||||||
const maxRadius = 40; // 最大节点半径
|
|
||||||
|
|
||||||
// 找出最大频率值用于缩放
|
|
||||||
const maxValue = Math.max(...data.nodes.map(n => n.value));
|
|
||||||
|
|
||||||
// 计算每个节点的实际半径
|
|
||||||
const nodeRadiusMap = new Map<string, number>();
|
|
||||||
for (const node of data.nodes) {
|
|
||||||
const radius = Math.min(maxRadius, radiusScale + (node.value / maxValue) * 25);
|
|
||||||
nodeRadiusMap.set(node.id, radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 节点重叠标记系统 - 跟踪哪些节点存在重叠
|
|
||||||
const overlapTracker = new Map<string, Set<string>>();
|
|
||||||
for (const node of data.nodes) {
|
|
||||||
overlapTracker.set(node.id, new Set<string>());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据画布尺寸调整初始分布范围 - 增加分布范围
|
|
||||||
const initialRadius = Math.min(cardWidth, cardHeight) * 0.4;
|
|
||||||
|
|
||||||
// 对节点按大小排序,确保大节点先放置
|
|
||||||
const sortedNodes = [...data.nodes].sort((a, b) => b.value - a.value);
|
|
||||||
|
|
||||||
// 初始化随机位置 - 改进的空间分配策略
|
|
||||||
for (let i = 0; i < sortedNodes.length; i++) {
|
|
||||||
const node = sortedNodes[i];
|
|
||||||
if (!node) continue; // 防止空节点
|
|
||||||
const nodeRadius = nodeRadiusMap.get(node.id)!;
|
|
||||||
|
|
||||||
// 使用黄金角度法进行更均匀的分布
|
|
||||||
const goldenAngle = Math.PI * (3 - Math.sqrt(5)); // 黄金角
|
|
||||||
const angle = i * goldenAngle;
|
|
||||||
|
|
||||||
// 根据节点大小调整距离
|
|
||||||
const sizeFactor = 1 + (nodeRadius / maxRadius) * 0.5; // 大节点获得更远的初始距离
|
|
||||||
const distance = initialRadius * (0.4 + 0.6 * Math.random()) * sizeFactor;
|
|
||||||
|
|
||||||
nodePositions.set(node.id, {
|
|
||||||
x: centerX + Math.cos(angle) * distance,
|
|
||||||
y: centerY + Math.sin(angle) * distance
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据节点数量调整迭代次数 - 增加迭代次数确保充分布局
|
|
||||||
const iterations = Math.max(40, Math.min(80, 40 + nodeCount));
|
|
||||||
|
|
||||||
// 模拟物理力学
|
|
||||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
||||||
// 冷却因子 - 调整冷却曲线以减缓冷却速度
|
|
||||||
const temperatureFactor = 1 - Math.pow(iteration / iterations, 1.5) * 0.8;
|
|
||||||
|
|
||||||
// 清除重叠标记
|
|
||||||
for (const nodeId of overlapTracker.keys()) {
|
|
||||||
overlapTracker.get(nodeId)!.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 斥力 (所有节点相互排斥)
|
|
||||||
for (let i = 0; i < data.nodes.length; i++) {
|
|
||||||
const node1 = data.nodes[i];
|
|
||||||
if (!node1) continue; // 防止空节点
|
|
||||||
const pos1 = nodePositions.get(node1.id)!;
|
|
||||||
const radius1 = nodeRadiusMap.get(node1.id)!;
|
|
||||||
|
|
||||||
for (let j = i + 1; j < data.nodes.length; j++) {
|
|
||||||
const node2 = data.nodes[j];
|
|
||||||
if (!node2) continue; // 防止空节点
|
|
||||||
const pos2 = nodePositions.get(node2.id)!;
|
|
||||||
const radius2 = nodeRadiusMap.get(node2.id)!;
|
|
||||||
|
|
||||||
const dx = pos2.x - pos1.x;
|
|
||||||
const dy = pos2.y - pos1.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
|
|
||||||
// 根据节点实际大小计算最小距离
|
|
||||||
const minDistance = radius1 + radius2 + 40; // 增加最小间隙
|
|
||||||
|
|
||||||
// 检测并标记重叠
|
|
||||||
if (distance < minDistance) {
|
|
||||||
overlapTracker.get(node1.id)!.add(node2.id);
|
|
||||||
overlapTracker.get(node2.id)!.add(node1.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对所有节点应用基础斥力
|
|
||||||
const repulsionStrength = 1200 * temperatureFactor; // 增强基础斥力
|
|
||||||
|
|
||||||
if (distance > 0) {
|
|
||||||
// 使用反比平方斥力,但对近距离节点增加额外斥力
|
|
||||||
const proximityFactor = Math.pow(Math.max(0, 1 - distance / (minDistance * 2)), 2) * 3 + 1;
|
|
||||||
const force = Math.min(8, repulsionStrength / (distance * distance)) * proximityFactor;
|
|
||||||
|
|
||||||
// 根据节点大小调整斥力
|
|
||||||
const sizeFactor = (radius1 + radius2) / (radiusScale * 2);
|
|
||||||
const adjustedForce = force * Math.sqrt(sizeFactor);
|
|
||||||
|
|
||||||
const moveX = (dx / distance) * adjustedForce;
|
|
||||||
const moveY = (dy / distance) * adjustedForce;
|
|
||||||
|
|
||||||
pos1.x -= moveX;
|
|
||||||
pos1.y -= moveY;
|
|
||||||
pos2.x += moveX;
|
|
||||||
pos2.y += moveY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果距离小于最小距离,增加强制分离力
|
|
||||||
if (distance < minDistance) {
|
|
||||||
// 计算重叠度
|
|
||||||
const overlapRatio = (minDistance - distance) / minDistance;
|
|
||||||
|
|
||||||
// 计算分离力 - 重叠程度越高,力越大
|
|
||||||
// 在迭代后期增加分离力
|
|
||||||
const lateStageFactor = 1 + Math.max(0, (iteration - iterations * 0.6) / (iterations * 0.4)) * 2;
|
|
||||||
const separationForce = overlapRatio * 0.8 * temperatureFactor * lateStageFactor;
|
|
||||||
|
|
||||||
pos1.x -= dx * separationForce;
|
|
||||||
pos1.y -= dy * separationForce;
|
|
||||||
pos2.x += dx * separationForce;
|
|
||||||
pos2.y += dy * separationForce;
|
|
||||||
|
|
||||||
// 额外的扭矩力,帮助节点绕过彼此
|
|
||||||
if (overlapRatio > 0.5 && iteration > iterations * 0.3) {
|
|
||||||
// 计算垂直于连线的方向
|
|
||||||
const perpX = -dy / distance;
|
|
||||||
const perpY = dx / distance;
|
|
||||||
|
|
||||||
// 随机选择扭矩方向
|
|
||||||
const sign = Math.random() > 0.5 ? 1 : -1;
|
|
||||||
const torqueFactor = 0.2 * overlapRatio * temperatureFactor;
|
|
||||||
|
|
||||||
pos1.x += perpX * torqueFactor * sign;
|
|
||||||
pos1.y += perpY * torqueFactor * sign;
|
|
||||||
pos2.x -= perpX * torqueFactor * sign;
|
|
||||||
pos2.y -= perpY * torqueFactor * sign;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 引力 (有连接的节点相互吸引) - 优化以避免过度聚集
|
|
||||||
for (const edge of data.edges) {
|
|
||||||
const pos1 = nodePositions.get(edge.from)!;
|
|
||||||
const pos2 = nodePositions.get(edge.to)!;
|
|
||||||
const radius1 = nodeRadiusMap.get(edge.from)!;
|
|
||||||
const radius2 = nodeRadiusMap.get(edge.to)!;
|
|
||||||
|
|
||||||
const dx = pos2.x - pos1.x;
|
|
||||||
const dy = pos2.y - pos1.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
|
|
||||||
// 根据边权值和节点大小调整引力
|
|
||||||
const baseStrength = Math.min(0.015, edge.value / 200);
|
|
||||||
const strength = baseStrength * temperatureFactor;
|
|
||||||
|
|
||||||
// 根据节点大小动态调整最佳距离
|
|
||||||
const minNodeDistance = radius1 + radius2 + 40;
|
|
||||||
const optimalDistance = minNodeDistance + 60 + edge.value * 0.5;
|
|
||||||
|
|
||||||
if (distance > optimalDistance) {
|
|
||||||
// 如果节点距离过远,应用引力
|
|
||||||
const attractionForce = strength * Math.min(1, (distance - optimalDistance) / optimalDistance);
|
|
||||||
pos1.x += dx * attractionForce;
|
|
||||||
pos1.y += dy * attractionForce;
|
|
||||||
pos2.x -= dx * attractionForce;
|
|
||||||
pos2.y -= dy * attractionForce;
|
|
||||||
} else if (distance < minNodeDistance) {
|
|
||||||
// 如果节点距离过近,应用斥力
|
|
||||||
const repulsionForce = 0.05 * temperatureFactor * (minNodeDistance - distance) / minNodeDistance;
|
|
||||||
pos1.x -= dx * repulsionForce;
|
|
||||||
pos1.y -= dy * repulsionForce;
|
|
||||||
pos2.x += dx * repulsionForce;
|
|
||||||
pos2.y += dy * repulsionForce;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中心引力 - 防止节点飞得太远
|
|
||||||
const availableArea = Math.min(cardWidth, cardHeight) * 0.45; // 增加有效区域
|
|
||||||
|
|
||||||
for (const node of data.nodes) {
|
|
||||||
const pos = nodePositions.get(node.id)!;
|
|
||||||
const dx = centerX - pos.x;
|
|
||||||
const dy = centerY - pos.y;
|
|
||||||
const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
// 根据与中心距离施加引力
|
|
||||||
if (distanceFromCenter > availableArea) {
|
|
||||||
const centerForce = 0.01 * temperatureFactor *
|
|
||||||
Math.pow((distanceFromCenter - availableArea) / availableArea, 1.2);
|
|
||||||
pos.x += dx * centerForce;
|
|
||||||
pos.y += dy * centerForce;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保节点不会超出边界
|
|
||||||
for (const node of data.nodes) {
|
|
||||||
const pos = nodePositions.get(node.id)!;
|
|
||||||
const radius = nodeRadiusMap.get(node.id)!;
|
|
||||||
const margin = radius + 20; // 考虑节点实际大小的边距
|
|
||||||
|
|
||||||
pos.x = Math.max(cardX + margin, Math.min(cardX + cardWidth - margin, pos.x));
|
|
||||||
pos.y = Math.max(cardY + margin, Math.min(cardY + cardHeight - margin, pos.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重叠度计算 - 统计当前总重叠数量
|
|
||||||
let totalOverlaps = 0;
|
|
||||||
for (const overlaps of overlapTracker.values()) {
|
|
||||||
totalOverlaps += overlaps.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果迭代已进行了3/4以上且没有重叠,可以提前结束
|
|
||||||
if (iteration > iterations * 0.75 && totalOverlaps === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终重叠消除阶段 - 专门解决残余重叠问题
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
let overlapsFixed = 0;
|
|
||||||
|
|
||||||
for (let j = 0; j < data.nodes.length; j++) {
|
|
||||||
const node1 = data.nodes[j];
|
|
||||||
if (!node1) continue; // 防止空节点
|
|
||||||
const pos1 = nodePositions.get(node1.id)!;
|
|
||||||
const radius1 = nodeRadiusMap.get(node1.id)!;
|
|
||||||
|
|
||||||
for (let k = j + 1; k < data.nodes.length; k++) {
|
|
||||||
const node2 = data.nodes[k];
|
|
||||||
if (!node2) continue; // 防止空节点
|
|
||||||
const pos2 = nodePositions.get(node2.id)!;
|
|
||||||
const radius2 = nodeRadiusMap.get(node2.id)!;
|
|
||||||
|
|
||||||
const dx = pos2.x - pos1.x;
|
|
||||||
const dy = pos2.y - pos1.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
const minDistance = radius1 + radius2 + 40;
|
|
||||||
|
|
||||||
if (distance < minDistance) {
|
|
||||||
// 计算需要移动的距离
|
|
||||||
const moveDistance = (minDistance - distance) / 2 + 1;
|
|
||||||
const moveX = (dx / distance) * moveDistance;
|
|
||||||
const moveY = (dy / distance) * moveDistance;
|
|
||||||
|
|
||||||
// 应用移动
|
|
||||||
pos1.x -= moveX;
|
|
||||||
pos1.y -= moveY;
|
|
||||||
pos2.x += moveX;
|
|
||||||
pos2.y += moveY;
|
|
||||||
|
|
||||||
// 施加小的随机扰动以打破对称性
|
|
||||||
const jitter = 1;
|
|
||||||
pos1.x += (Math.random() - 0.5) * jitter;
|
|
||||||
pos1.y += (Math.random() - 0.5) * jitter;
|
|
||||||
pos2.x += (Math.random() - 0.5) * jitter;
|
|
||||||
pos2.y += (Math.random() - 0.5) * jitter;
|
|
||||||
|
|
||||||
overlapsFixed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保节点不会超出边界
|
|
||||||
const radius = nodeRadiusMap.get(node1.id)!;
|
|
||||||
const margin = radius + 20;
|
|
||||||
pos1.x = Math.max(cardX + margin, Math.min(cardX + cardWidth - margin, pos1.x));
|
|
||||||
pos1.y = Math.max(cardY + margin, Math.min(cardY + cardHeight - margin, pos1.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有重叠了,提前退出
|
|
||||||
if (overlapsFixed === 0) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制边 - 改进边的视觉效果
|
|
||||||
for (const edge of data.edges) {
|
|
||||||
const pos1 = nodePositions.get(edge.from)!;
|
|
||||||
const pos2 = nodePositions.get(edge.to)!;
|
|
||||||
const radius1 = nodeRadiusMap.get(edge.from)!;
|
|
||||||
const radius2 = nodeRadiusMap.get(edge.to)!;
|
|
||||||
|
|
||||||
// 计算边的实际起止点,从节点边缘开始而不是中心
|
|
||||||
const dx = pos2.x - pos1.x;
|
|
||||||
const dy = pos2.y - pos1.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
|
|
||||||
// 计算实际的起点和终点,从节点边缘开始
|
|
||||||
const startX = pos1.x + (dx / distance) * radius1;
|
|
||||||
const startY = pos1.y + (dy / distance) * radius1;
|
|
||||||
const endX = pos2.x - (dx / distance) * radius2;
|
|
||||||
const endY = pos2.y - (dy / distance) * radius2;
|
|
||||||
|
|
||||||
// 根据权重确定线宽
|
|
||||||
const lineWidth = Math.max(1, Math.min(5, edge.value / 10));
|
|
||||||
|
|
||||||
// 计算透明度 (权重越高透明度越低)
|
|
||||||
const alpha = Math.min(0.7, 0.2 + edge.value / 20);
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(startX, startY);
|
|
||||||
ctx.lineTo(endX, endY);
|
|
||||||
ctx.strokeStyle = `rgba(100, 100, 255, ${alpha})`;
|
|
||||||
ctx.lineWidth = lineWidth;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制节点
|
|
||||||
for (const node of data.nodes) {
|
|
||||||
const pos = nodePositions.get(node.id)!;
|
|
||||||
|
|
||||||
// 使用预计算的节点半径
|
|
||||||
const radius = nodeRadiusMap.get(node.id)!;
|
|
||||||
|
|
||||||
// 绘制节点圆形
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = `rgba(255, 100, 100, 0.8)`;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 绘制边框
|
|
||||||
ctx.strokeStyle = '#800000';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 根据节点大小调整字体大小
|
|
||||||
const fontSize = Math.max(14, Math.min(18, 14 + (node.value / maxValue) * 6));
|
|
||||||
|
|
||||||
// 绘制文本
|
|
||||||
ctx.fillStyle = '#000';
|
|
||||||
ctx.font = `${fontSize}px "Aa偷吃可爱长大的"`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(node.label, pos.x, pos.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制图例
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.font = '18px "Aa偷吃可爱长大的"';
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.fillText('词频越高,节点越大', cardX + 20, cardY + 20);
|
|
||||||
ctx.fillText('关联越强,连线越粗', cardX + 20, cardY + 50);
|
|
||||||
|
|
||||||
// 保存图像
|
|
||||||
const buffer = canvas.toBuffer('image/png');
|
|
||||||
return "base64://" + buffer.toString('base64');
|
|
||||||
}
|
|
@@ -1,952 +0,0 @@
|
|||||||
import { createCanvas } from '@napi-rs/canvas';
|
|
||||||
interface WordFrequency {
|
|
||||||
word: string;
|
|
||||||
frequency: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Position {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
rotation: number;
|
|
||||||
fontSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据词频生成词云图片
|
|
||||||
* @param wordFrequencies 词频数组,包含单词和对应的频率
|
|
||||||
* @param initialWidth 初始画布宽度(最终会自动调整)
|
|
||||||
* @param initialHeight 初始画布高度(最终会自动调整)
|
|
||||||
* @param options 词云配置选项
|
|
||||||
* @returns 图片的base64编码字符串
|
|
||||||
*/
|
|
||||||
export async function generateWordCloud(
|
|
||||||
wordFrequencies: WordFrequency[],
|
|
||||||
initialWidth = 1000,
|
|
||||||
initialHeight = 800,
|
|
||||||
options = {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
enableRotation: true,
|
|
||||||
maxAttempts: 60, // 每个词的最大尝试次数
|
|
||||||
minFontSize: 20, // 最小字体大小
|
|
||||||
maxFontSize: 100, // 最大字体大小
|
|
||||||
padding: 20, // 降低内边距提高紧凑度
|
|
||||||
horizontalWeight: 0.6, // 提高横排权重增强可读性
|
|
||||||
rotationVariance: 10, // 减少旋转角度变化
|
|
||||||
safetyMargin: 6, // 减小安全距离以提高密度
|
|
||||||
fontSizeRatio: 2.0, // 字体大小差异
|
|
||||||
overlapThreshold: 0.10, // 允许10%的重叠
|
|
||||||
maxExpansionAttempts: 10, // 最大画布扩展次数
|
|
||||||
expansionRatio: 1.15 // 降低每次扩展比例
|
|
||||||
}
|
|
||||||
): Promise<string> {
|
|
||||||
// 空数组检查
|
|
||||||
if (wordFrequencies.length === 0) {
|
|
||||||
const emptyCanvas = createCanvas(initialWidth, initialHeight);
|
|
||||||
const ctx = emptyCanvas.getContext('2d');
|
|
||||||
ctx.fillStyle = options.backgroundColor;
|
|
||||||
ctx.fillRect(0, 0, initialWidth, initialHeight);
|
|
||||||
return "base64://" + emptyCanvas.toBuffer('image/png').toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤不可渲染字符 - 增强过滤能力
|
|
||||||
const filteredWordFrequencies = wordFrequencies.map(item => ({
|
|
||||||
...item,
|
|
||||||
word: filterUnrenderableChars(item.word)
|
|
||||||
})).filter(item => item.word.length > 0);
|
|
||||||
|
|
||||||
// 再次检查过滤后是否为空
|
|
||||||
if (filteredWordFrequencies.length === 0) {
|
|
||||||
const emptyCanvas = createCanvas(initialWidth, initialHeight);
|
|
||||||
const ctx = emptyCanvas.getContext('2d');
|
|
||||||
ctx.fillStyle = options.backgroundColor;
|
|
||||||
ctx.fillRect(0, 0, initialWidth, initialHeight);
|
|
||||||
return "base64://" + emptyCanvas.toBuffer('image/png').toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对词频进行排序,频率高的先绘制
|
|
||||||
const sortedWords = [...filteredWordFrequencies].sort((a, b) => b.frequency - a.frequency);
|
|
||||||
|
|
||||||
// 计算最小和最大频率,用于字体大小缩放
|
|
||||||
const maxFreq = sortedWords[0]?.frequency || 1;
|
|
||||||
const minFreq = sortedWords[sortedWords.length - 1]?.frequency || 1;
|
|
||||||
const freqRange = Math.max(1, maxFreq - minFreq); // 避免除以零
|
|
||||||
|
|
||||||
// 检查字符类型
|
|
||||||
const isChineseChar = (char: string): boolean => /[\u4e00-\u9fa5]/.test(char);
|
|
||||||
const isEnglishChar = (char: string): boolean => /[a-zA-Z0-9]/.test(char);
|
|
||||||
|
|
||||||
// 判断单词类型(中文、英文或混合)
|
|
||||||
const getWordType = (word: string): 'chinese' | 'english' | 'mixed' => {
|
|
||||||
let hasChinese = false;
|
|
||||||
let hasEnglish = false;
|
|
||||||
|
|
||||||
for (const char of word) {
|
|
||||||
if (isChineseChar(char)) hasChinese = true;
|
|
||||||
else if (isEnglishChar(char)) hasEnglish = true;
|
|
||||||
|
|
||||||
if (hasChinese && hasEnglish) return 'mixed';
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasChinese ? 'chinese' : 'english';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取适合单词的字体
|
|
||||||
const getFontFamily = (word: string): string => {
|
|
||||||
const wordType = getWordType(word);
|
|
||||||
|
|
||||||
if (wordType === 'chinese') return '"Aa偷吃可爱长大的", sans-serif';
|
|
||||||
if (wordType === 'english') return '"JetBrains Mono", monospace';
|
|
||||||
return '"Aa偷吃可爱长大的", "JetBrains Mono", sans-serif'; // 混合类型
|
|
||||||
};
|
|
||||||
|
|
||||||
// 增强的字体大小计算函数,保持高频词更大但减小差距
|
|
||||||
const calculateFontSize = (frequency: number, index: number): number => {
|
|
||||||
// 基本的频率比例
|
|
||||||
const frequencyRatio = (frequency - minFreq) / freqRange;
|
|
||||||
|
|
||||||
// 根据词云大小调整差异系数
|
|
||||||
const smallCloudFactor = sortedWords.length < 15 ? 1.5 : 1.0; // 小词云时增大差异
|
|
||||||
|
|
||||||
// 应用非线性映射,使高频词更大但差距不过大
|
|
||||||
let sizeRatio;
|
|
||||||
|
|
||||||
if (index === 0) {
|
|
||||||
// 最高频词
|
|
||||||
sizeRatio = Math.pow(frequencyRatio, 0.3) * 2.2 * smallCloudFactor;
|
|
||||||
} else if (index < sortedWords.length * 0.05) {
|
|
||||||
// 前5%的高频词
|
|
||||||
sizeRatio = Math.pow(frequencyRatio, 0.4) * 1.8 * smallCloudFactor;
|
|
||||||
} else if (index < sortedWords.length * 0.15) {
|
|
||||||
// 前15%的高频词
|
|
||||||
sizeRatio = Math.pow(frequencyRatio, 0.5) * 1.5 * smallCloudFactor;
|
|
||||||
} else if (index < sortedWords.length * 0.3) {
|
|
||||||
// 前30%的高频词
|
|
||||||
sizeRatio = Math.pow(frequencyRatio, 0.6) * 1.3 * smallCloudFactor;
|
|
||||||
} else {
|
|
||||||
// 其余的词
|
|
||||||
sizeRatio = Math.pow(frequencyRatio, 0.7) * 1.0 * smallCloudFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用配置的字体大小比例系数
|
|
||||||
sizeRatio *= options.fontSizeRatio;
|
|
||||||
|
|
||||||
// 计算最终字体大小
|
|
||||||
return Math.max(
|
|
||||||
options.minFontSize,
|
|
||||||
Math.min(
|
|
||||||
options.maxFontSize,
|
|
||||||
Math.floor(options.minFontSize + sizeRatio * (options.maxFontSize - options.minFontSize))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取基于词频的颜色
|
|
||||||
const getColorFromFrequency = (frequency: number, index: number): string => {
|
|
||||||
// 使用词频和索引生成不同的色相值
|
|
||||||
const hue = (index * 137.5) % 360; // 黄金角分布
|
|
||||||
|
|
||||||
// 重要词使用更醒目的颜色
|
|
||||||
let saturation, lightness;
|
|
||||||
|
|
||||||
if (index === 0) {
|
|
||||||
// 最高频词
|
|
||||||
saturation = 95;
|
|
||||||
lightness = 45;
|
|
||||||
} else if (index < sortedWords.length * 0.1) {
|
|
||||||
// 前10%的高频词
|
|
||||||
saturation = 90;
|
|
||||||
lightness = 45;
|
|
||||||
} else {
|
|
||||||
// 降低其他词的饱和度,增加整体和谐性
|
|
||||||
saturation = 75 + (Math.max(0.3, frequency / maxFreq) * 15);
|
|
||||||
lightness = 40 + (Math.max(0.3, frequency / maxFreq) * 15);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 临时画布用于测量文本
|
|
||||||
let tempCanvas = createCanvas(initialWidth, initialHeight);
|
|
||||||
let tempCtx = tempCanvas.getContext('2d');
|
|
||||||
|
|
||||||
// 已确定位置的单词数组
|
|
||||||
const placedWords: Position[] = [];
|
|
||||||
|
|
||||||
// 根据旋转角度计算包围盒(用于碰撞检测)
|
|
||||||
const getRotatedBoundingBox = (x: number, y: number, width: number, height: number, rotation: number) => {
|
|
||||||
// 转换角度为弧度
|
|
||||||
const rad = rotation * Math.PI / 180;
|
|
||||||
const cos = Math.cos(rad);
|
|
||||||
const sin = Math.sin(rad);
|
|
||||||
|
|
||||||
// 计算四个角的坐标
|
|
||||||
const corners = [
|
|
||||||
{ x: -width / 2, y: -height / 2 },
|
|
||||||
{ x: width / 2, y: -height / 2 },
|
|
||||||
{ x: width / 2, y: height / 2 },
|
|
||||||
{ x: -width / 2, y: height / 2 }
|
|
||||||
].map(pt => {
|
|
||||||
return {
|
|
||||||
x: x + width / 2 + (pt.x * cos - pt.y * sin),
|
|
||||||
y: y + height / 2 + (pt.x * sin + pt.y * cos)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算包围盒
|
|
||||||
const boxMinX = Math.min(...corners.map(c => c.x));
|
|
||||||
const boxMaxX = Math.max(...corners.map(c => c.x));
|
|
||||||
const boxMinY = Math.min(...corners.map(c => c.y));
|
|
||||||
const boxMaxY = Math.max(...corners.map(c => c.y));
|
|
||||||
|
|
||||||
return { minX: boxMinX, maxX: boxMaxX, minY: boxMinY, maxY: boxMaxY };
|
|
||||||
};
|
|
||||||
|
|
||||||
// 精确重叠检测 - 允许适度重叠,并考虑词的重要性
|
|
||||||
const isOverlapping = (x: number, y: number, width: number, height: number, rotation: number, index: number): boolean => {
|
|
||||||
// 获取当前词的包围盒
|
|
||||||
const currentBox = getRotatedBoundingBox(x, y, width, height, rotation);
|
|
||||||
|
|
||||||
// 为边缘增加安全距离,根据重要性调整
|
|
||||||
const safetyMargin = index < sortedWords.length * 0.05 ?
|
|
||||||
options.safetyMargin * 1.2 : options.safetyMargin * 0.9;
|
|
||||||
|
|
||||||
const safetyBox = {
|
|
||||||
minX: currentBox.minX - safetyMargin,
|
|
||||||
maxX: currentBox.maxX + safetyMargin,
|
|
||||||
minY: currentBox.minY - safetyMargin,
|
|
||||||
maxY: currentBox.maxY + safetyMargin
|
|
||||||
};
|
|
||||||
|
|
||||||
// 计算当前单词的面积
|
|
||||||
const currentArea = (safetyBox.maxX - safetyBox.minX) * (safetyBox.maxY - safetyBox.minY);
|
|
||||||
|
|
||||||
// 为高频词设置更严格的重叠阈值
|
|
||||||
const overlapThreshold = index < sortedWords.length * 0.1 ?
|
|
||||||
options.overlapThreshold * 0.6 : options.overlapThreshold;
|
|
||||||
|
|
||||||
// 检查是否与已放置的词重叠超过阈值
|
|
||||||
for (const pos of placedWords) {
|
|
||||||
const posBox = getRotatedBoundingBox(
|
|
||||||
pos.x, pos.y, pos.width, pos.height, pos.rotation
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算重叠区域
|
|
||||||
const overlapX = Math.max(0, Math.min(safetyBox.maxX, posBox.maxX) - Math.max(safetyBox.minX, posBox.minX));
|
|
||||||
const overlapY = Math.max(0, Math.min(safetyBox.maxY, posBox.maxY) - Math.max(safetyBox.minY, posBox.minY));
|
|
||||||
const overlapArea = overlapX * overlapY;
|
|
||||||
|
|
||||||
// 计算重叠率
|
|
||||||
const overlapRatio = overlapArea / currentArea;
|
|
||||||
|
|
||||||
// 如果重叠率超过阈值,则认为重叠
|
|
||||||
if (overlapRatio > overlapThreshold) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取当前词云形状信息 - 改进密度计算
|
|
||||||
const getCloudShape = () => {
|
|
||||||
if (placedWords.length === 0) {
|
|
||||||
return {
|
|
||||||
width: initialWidth,
|
|
||||||
height: initialHeight,
|
|
||||||
ratio: initialWidth / initialHeight,
|
|
||||||
density: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算已放置区域的边界
|
|
||||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
||||||
let totalArea = 0;
|
|
||||||
|
|
||||||
placedWords.forEach(pos => {
|
|
||||||
const box = getRotatedBoundingBox(pos.x, pos.y, pos.width, pos.height, pos.rotation);
|
|
||||||
minX = Math.min(minX, box.minX);
|
|
||||||
maxX = Math.max(maxX, box.maxX);
|
|
||||||
minY = Math.min(minY, box.minY);
|
|
||||||
maxY = Math.max(maxY, box.maxY);
|
|
||||||
|
|
||||||
// 累计词的面积
|
|
||||||
totalArea += pos.width * pos.height;
|
|
||||||
});
|
|
||||||
|
|
||||||
const width = Math.max(1, maxX - minX);
|
|
||||||
const height = Math.max(1, maxY - minY);
|
|
||||||
const area = width * height;
|
|
||||||
|
|
||||||
// 计算密度 (已用面积 / 总面积)
|
|
||||||
const density = totalArea / area;
|
|
||||||
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
ratio: width / height,
|
|
||||||
density
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自适应旋转角度决策 - 基于可用空间和单词特性
|
|
||||||
const getOptimalRotation = (word: string, textWidth: number, textHeight: number, index: number) => {
|
|
||||||
if (!options.enableRotation) return 0;
|
|
||||||
|
|
||||||
const wordType = getWordType(word);
|
|
||||||
const isHighFrequencyWord = index < sortedWords.length * 0.15; // 前15%的高频词
|
|
||||||
|
|
||||||
// 单字或短词偏好水平排列
|
|
||||||
if (word.length === 1 || (wordType === 'english' && word.length <= 3)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中文单字不旋转
|
|
||||||
if (wordType === 'chinese' && word.length === 1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 高频词优先水平排列
|
|
||||||
if (isHighFrequencyWord) {
|
|
||||||
// 最高频词不旋转
|
|
||||||
if (index === 0) return 0;
|
|
||||||
// 其他高频词轻微旋转
|
|
||||||
return (Math.random() * 2 - 1) * 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前词云形状与密度
|
|
||||||
const cloudShape = getCloudShape();
|
|
||||||
|
|
||||||
// 特别定制:根据词的宽高比决定旋转
|
|
||||||
// 细长的词在排版上更灵活
|
|
||||||
const isLongWord = textWidth / textHeight > 3;
|
|
||||||
|
|
||||||
// 宽高比例调整旋转策略
|
|
||||||
if (cloudShape.ratio > 1.3) {
|
|
||||||
// 宽大于高,优先考虑竖排
|
|
||||||
if (isLongWord) {
|
|
||||||
// 细长词适合90度旋转
|
|
||||||
return 90;
|
|
||||||
} else {
|
|
||||||
// 其他词随机选择,但偏向竖排
|
|
||||||
return Math.random() < 0.7 ?
|
|
||||||
90 + (Math.random() * 2 - 1) * 5 : // 竖排
|
|
||||||
(Math.random() * 2 - 1) * 5; // 横排
|
|
||||||
}
|
|
||||||
} else if (cloudShape.ratio < 0.7) {
|
|
||||||
// 高大于宽,优先考虑横排
|
|
||||||
return (Math.random() * 2 - 1) * 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据词的类型进一步决定倾向
|
|
||||||
let horizontalBias = options.horizontalWeight;
|
|
||||||
|
|
||||||
if (wordType === 'chinese' && word.length > 1) {
|
|
||||||
// 中文词组更适合横排
|
|
||||||
horizontalBias += 0.2;
|
|
||||||
} else if (wordType === 'english' && word.length > 5) {
|
|
||||||
// 长英文单词可增加竖排几率
|
|
||||||
horizontalBias -= 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据词的长宽比进一步微调
|
|
||||||
const aspectRatio = textWidth / textHeight;
|
|
||||||
if (aspectRatio > 4) {
|
|
||||||
// 极细长的词更适合竖排
|
|
||||||
horizontalBias -= 0.25;
|
|
||||||
} else if (aspectRatio < 1.5) {
|
|
||||||
// 近方形的词更适合横排
|
|
||||||
horizontalBias += 0.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终决定旋转
|
|
||||||
return Math.random() < horizontalBias ?
|
|
||||||
(Math.random() * 2 - 1) * options.rotationVariance / 3 : // 横排,减小角度变化
|
|
||||||
90 + (Math.random() * 2 - 1) * options.rotationVariance / 3; // 竖排,减小角度变化
|
|
||||||
};
|
|
||||||
|
|
||||||
// 增强的过滤不可渲染字符函数
|
|
||||||
function filterUnrenderableChars(text: string): string {
|
|
||||||
// 过滤掉控制字符、特殊Unicode和一些可能导致渲染问题的字符
|
|
||||||
return text
|
|
||||||
.replace(/[\u0000-\u001F\u007F-\u009F\uFFFD\uFFFE\uFFFF]/g, '') // 控制字符和特殊字符
|
|
||||||
.replace(/[\u2000-\u200F\u2028-\u202F]/g, '') // 一些特殊空白和控制字符
|
|
||||||
.replace(/[\u0080-\u00A0]/g, '') // 一些Latin-1补充字符
|
|
||||||
.replace(/[^\p{L}\p{N}\p{P}\p{Z}]/gu, '') // 只保留字母、数字、标点和空格
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 优化: 添加新的位置策略函数 =====
|
|
||||||
|
|
||||||
// 改进的螺旋布局 - 更紧凑的布局策略
|
|
||||||
const getSpiralPosition = (
|
|
||||||
textWidth: number,
|
|
||||||
textHeight: number,
|
|
||||||
attempt: number,
|
|
||||||
canvasShape: { width: number, height: number, ratio: number, density: number }
|
|
||||||
) => {
|
|
||||||
// 根据词数量调整螺旋参数 - 词少时更紧凑
|
|
||||||
const wordCountFactor = Math.min(1, placedWords.length / 20); // 少于20个词时更紧凑
|
|
||||||
|
|
||||||
// 使用已放置词的中心点,而非固定画布中心
|
|
||||||
let centerX = initialWidth / 2;
|
|
||||||
let centerY = initialHeight / 2;
|
|
||||||
|
|
||||||
// 如果已经有足够的词,使用它们的质心作为新的中心点
|
|
||||||
if (placedWords.length >= 3) {
|
|
||||||
let sumX = 0, sumY = 0, weightSum = 0;
|
|
||||||
for (const pos of placedWords) {
|
|
||||||
// 较大的词有更大的权重影响中心点
|
|
||||||
const weight = Math.sqrt(pos.width * pos.height);
|
|
||||||
sumX += (pos.x + pos.width / 2) * weight;
|
|
||||||
sumY += (pos.y + pos.height / 2) * weight;
|
|
||||||
weightSum += weight;
|
|
||||||
}
|
|
||||||
centerX = sumX / weightSum;
|
|
||||||
centerY = sumY / weightSum;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态调整螺旋参数,词数少时更紧凑
|
|
||||||
const baseA = Math.min(initialWidth, initialHeight) / (35 + (1 - wordCountFactor) * 15);
|
|
||||||
const densityFactor = Math.max(0.7, Math.min(1.4, 0.7 + canvasShape.density * 1.2));
|
|
||||||
const a = baseA / densityFactor; // 反比例,密度高时参数更小,螺旋更紧凑
|
|
||||||
|
|
||||||
// 词数量少时使用更小的角度增量,产生更紧凑的螺旋
|
|
||||||
const angleIncrement = 0.1 + wordCountFactor * 0.25;
|
|
||||||
const angle = angleIncrement * attempt;
|
|
||||||
|
|
||||||
// 非线性距离增长,词数少时增长更慢
|
|
||||||
const distanceMultiplier = wordCountFactor * (
|
|
||||||
attempt < 8 ?
|
|
||||||
0.2 + Math.pow(attempt / 8, 1.5) : // 前几次更靠近中心,呈幂次增长
|
|
||||||
0.7 + Math.pow((attempt - 8) / 25, 0.7) // 之后缓慢增长
|
|
||||||
) + (1 - wordCountFactor) * (0.1 + Math.pow(attempt / 20, 1.2)); // 词少时增长更慢
|
|
||||||
|
|
||||||
// 根据画布形状自适应调整螺旋方向
|
|
||||||
let dx, dy;
|
|
||||||
if (canvasShape.ratio > 1.2) { // 宽大于高
|
|
||||||
// 水平方向拉伸,但减少拉伸强度
|
|
||||||
dx = a * angle * Math.cos(angle) * distanceMultiplier * 1.1;
|
|
||||||
dy = a * angle * Math.sin(angle) * distanceMultiplier * 0.9;
|
|
||||||
} else if (canvasShape.ratio < 0.8) { // 高大于宽
|
|
||||||
// 垂直方向拉伸,但减少拉伸强度
|
|
||||||
dx = a * angle * Math.cos(angle) * distanceMultiplier * 0.9;
|
|
||||||
dy = a * angle * Math.sin(angle) * distanceMultiplier * 1.1;
|
|
||||||
} else {
|
|
||||||
// 更均衡的螺旋
|
|
||||||
dx = a * angle * Math.cos(angle) * distanceMultiplier;
|
|
||||||
dy = a * angle * Math.sin(angle) * distanceMultiplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加少量随机抖动以打破规则性
|
|
||||||
dx += (Math.random() - 0.5) * a * 0.5;
|
|
||||||
dy += (Math.random() - 0.5) * a * 0.5;
|
|
||||||
|
|
||||||
const x = centerX + dx - textWidth / 2;
|
|
||||||
const y = centerY + dy - textHeight / 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: Math.max(options.safetyMargin, Math.min(initialWidth - textWidth - options.safetyMargin, x)),
|
|
||||||
y: Math.max(textHeight / 2 + options.safetyMargin, Math.min(initialHeight - textHeight / 2 - options.safetyMargin, y))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 新增: 空白区域填充策略
|
|
||||||
const findGapPosition = (
|
|
||||||
textWidth: number,
|
|
||||||
textHeight: number,
|
|
||||||
canvasShape: { width: number, height: number, ratio: number, density: number }
|
|
||||||
) => {
|
|
||||||
// 如果词太少,直接返回螺旋位置
|
|
||||||
if (placedWords.length < 5) {
|
|
||||||
return getSpiralPosition(textWidth, textHeight, Math.floor(Math.random() * 10), canvasShape);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算当前词云的边界
|
|
||||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
||||||
|
|
||||||
for (const pos of placedWords) {
|
|
||||||
const box = getRotatedBoundingBox(pos.x, pos.y, pos.width, pos.height, pos.rotation);
|
|
||||||
minX = Math.min(minX, box.minX);
|
|
||||||
maxX = Math.max(maxX, box.maxX);
|
|
||||||
minY = Math.min(minY, box.minY);
|
|
||||||
maxY = Math.max(maxY, box.maxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义搜索区域,适当扩大范围
|
|
||||||
const searchMargin = Math.max(textWidth, textHeight) * 0.5;
|
|
||||||
const searchMinX = Math.max(0, minX - searchMargin);
|
|
||||||
const searchMaxX = Math.min(initialWidth, maxX + searchMargin);
|
|
||||||
const searchMinY = Math.max(0, minY - searchMargin);
|
|
||||||
const searchMaxY = Math.min(initialHeight, maxY + searchMargin);
|
|
||||||
|
|
||||||
// 搜索区域宽高
|
|
||||||
const searchWidth = searchMaxX - searchMinX;
|
|
||||||
const searchHeight = searchMaxY - searchMinY;
|
|
||||||
|
|
||||||
// 网格尺寸,较小的网格可以更精确地找到空白区域
|
|
||||||
const gridSize = Math.min(textWidth, textHeight) / 2;
|
|
||||||
const gridRows = Math.max(3, Math.ceil(searchHeight / gridSize));
|
|
||||||
const gridCols = Math.max(3, Math.ceil(searchWidth / gridSize));
|
|
||||||
|
|
||||||
// 初始化网格密度
|
|
||||||
const gridDensity = Array(gridRows).fill(0).map(() => Array(gridCols).fill(0));
|
|
||||||
|
|
||||||
// 计算每个网格的密度
|
|
||||||
for (const pos of placedWords) {
|
|
||||||
const box = getRotatedBoundingBox(pos.x, pos.y, pos.width, pos.height, pos.rotation);
|
|
||||||
|
|
||||||
// 计算此词覆盖的网格范围
|
|
||||||
const startRow = Math.max(0, Math.floor((box.minY - searchMinY) / gridSize));
|
|
||||||
const endRow = Math.min(gridRows - 1, Math.floor((box.maxY - searchMinY) / gridSize));
|
|
||||||
const startCol = Math.max(0, Math.floor((box.minX - searchMinX) / gridSize));
|
|
||||||
const endCol = Math.min(gridCols - 1, Math.floor((box.maxX - searchMinX) / gridSize));
|
|
||||||
|
|
||||||
// 增加网格密度值
|
|
||||||
for (let r = startRow; r <= endRow; r++) {
|
|
||||||
for (let c = startCol; c <= endCol; c++) {
|
|
||||||
if (r >= 0 && r < gridRows && c >= 0 && c < gridCols) {
|
|
||||||
gridDensity[r]![c] += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 找出能容纳当前词的最低密度区域
|
|
||||||
let bestDensity = Infinity;
|
|
||||||
let bestRow = 0, bestCol = 0;
|
|
||||||
|
|
||||||
// 需要的网格数量
|
|
||||||
const needRows = Math.ceil(textHeight / gridSize);
|
|
||||||
const needCols = Math.ceil(textWidth / gridSize);
|
|
||||||
|
|
||||||
// 搜索最优位置
|
|
||||||
for (let r = 0; r <= gridRows - needRows; r++) {
|
|
||||||
for (let c = 0; c <= gridCols - needCols; c++) {
|
|
||||||
let totalDensity = 0;
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
// 计算区域总密度
|
|
||||||
for (let nr = 0; nr < needRows && isValid; nr++) {
|
|
||||||
for (let nc = 0; nc < needCols && isValid; nc++) {
|
|
||||||
if (r + nr < gridRows && c + nc < gridCols) {
|
|
||||||
totalDensity += gridDensity[r + nr]![c + nc];
|
|
||||||
|
|
||||||
// 如果单个网格密度过高,直接判定无效
|
|
||||||
if (gridDensity[r + nr]![c + nc] > 3) {
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最佳位置
|
|
||||||
if (isValid && totalDensity < bestDensity) {
|
|
||||||
bestDensity = totalDensity;
|
|
||||||
bestRow = r;
|
|
||||||
bestCol = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加随机抖动避免太规则
|
|
||||||
const jitterX = (Math.random() - 0.5) * gridSize * 0.6;
|
|
||||||
const jitterY = (Math.random() - 0.5) * gridSize * 0.6;
|
|
||||||
|
|
||||||
// 计算最终位置
|
|
||||||
const x = searchMinX + bestCol * gridSize + jitterX;
|
|
||||||
const y = searchMinY + bestRow * gridSize + jitterY;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: Math.max(options.safetyMargin, Math.min(initialWidth - textWidth - options.safetyMargin, x)),
|
|
||||||
y: Math.max(textHeight + options.safetyMargin, Math.min(initialHeight - options.safetyMargin, y))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新增: 边缘扩展策略
|
|
||||||
const getEdgeExtendPosition = (
|
|
||||||
textWidth: number,
|
|
||||||
textHeight: number,
|
|
||||||
attempt: number,
|
|
||||||
canvasShape: { width: number, height: number, ratio: number, density: number }
|
|
||||||
) => {
|
|
||||||
// 如果无已放置词,回退到螺旋
|
|
||||||
if (placedWords.length === 0) {
|
|
||||||
return getSpiralPosition(textWidth, textHeight, attempt, canvasShape);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机选择一个已放置的词作为参考点
|
|
||||||
const referenceIndex = Math.floor(Math.random() * placedWords.length);
|
|
||||||
const reference = placedWords[referenceIndex];
|
|
||||||
|
|
||||||
// 随机选择方向 (0=右, 1=下, 2=左, 3=上,4-7=对角线)
|
|
||||||
const direction = Math.floor(Math.random() * 8);
|
|
||||||
|
|
||||||
// 基础位置
|
|
||||||
let baseX = reference!.x;
|
|
||||||
let baseY = reference!.y;
|
|
||||||
|
|
||||||
// 获取参考词的旋转后边界框
|
|
||||||
const refBox = getRotatedBoundingBox(
|
|
||||||
reference!.x, reference!.y, reference!.width, reference!.height, reference!.rotation
|
|
||||||
);
|
|
||||||
|
|
||||||
// 根据方向计算新位置
|
|
||||||
const margin = options.safetyMargin * 0.5; // 减小边距,增加紧凑度
|
|
||||||
|
|
||||||
switch (direction) {
|
|
||||||
case 0: // 右
|
|
||||||
baseX = refBox.maxX + margin;
|
|
||||||
baseY = refBox.minY + (refBox.maxY - refBox.minY) / 2 - textHeight / 2;
|
|
||||||
break;
|
|
||||||
case 1: // 下
|
|
||||||
baseX = refBox.minX + (refBox.maxX - refBox.minX) / 2 - textWidth / 2;
|
|
||||||
baseY = refBox.maxY + margin;
|
|
||||||
break;
|
|
||||||
case 2: // 左
|
|
||||||
baseX = refBox.minX - textWidth - margin;
|
|
||||||
baseY = refBox.minY + (refBox.maxY - refBox.minY) / 2 - textHeight / 2;
|
|
||||||
break;
|
|
||||||
case 3: // 上
|
|
||||||
baseX = refBox.minX + (refBox.maxX - refBox.minX) / 2 - textWidth / 2;
|
|
||||||
baseY = refBox.minY - textHeight - margin;
|
|
||||||
break;
|
|
||||||
case 4: // 右上
|
|
||||||
baseX = refBox.maxX + margin;
|
|
||||||
baseY = refBox.minY - textHeight - margin;
|
|
||||||
break;
|
|
||||||
case 5: // 右下
|
|
||||||
baseX = refBox.maxX + margin;
|
|
||||||
baseY = refBox.maxY + margin;
|
|
||||||
break;
|
|
||||||
case 6: // 左下
|
|
||||||
baseX = refBox.minX - textWidth - margin;
|
|
||||||
baseY = refBox.maxY + margin;
|
|
||||||
break;
|
|
||||||
case 7: // 左上
|
|
||||||
baseX = refBox.minX - textWidth - margin;
|
|
||||||
baseY = refBox.minY - textHeight - margin;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加少量随机抖动
|
|
||||||
baseX += (Math.random() - 0.5) * margin * 2;
|
|
||||||
baseY += (Math.random() - 0.5) * margin * 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: Math.max(options.safetyMargin, Math.min(initialWidth - textWidth - options.safetyMargin, baseX)),
|
|
||||||
y: Math.max(textHeight + options.safetyMargin, Math.min(initialHeight - options.safetyMargin, baseY))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新增: 多策略选择函数,根据情况选择最佳策略
|
|
||||||
const getPositionWithStrategy = (
|
|
||||||
textWidth: number,
|
|
||||||
textHeight: number,
|
|
||||||
attempt: number,
|
|
||||||
canvasShape: { width: number, height: number, ratio: number, density: number },
|
|
||||||
index: number
|
|
||||||
) => {
|
|
||||||
// 检测是否为小词云
|
|
||||||
const isSmallWordCloud = sortedWords.length < 15;
|
|
||||||
|
|
||||||
// 第一个词或前几个高频词仍然放在中心,小词云时范围更大
|
|
||||||
if (placedWords.length === 0 || (index < 3 && attempt < 5) || (isSmallWordCloud && index < Math.min(5, sortedWords.length / 2))) {
|
|
||||||
// 添加小偏移以避免完全重叠
|
|
||||||
const offset = isSmallWordCloud ? index * 8 : 0;
|
|
||||||
return {
|
|
||||||
x: initialWidth / 2 - textWidth / 2 + (Math.random() - 0.5) * offset,
|
|
||||||
y: initialHeight / 2 - textHeight / 2 + (Math.random() - 0.5) * offset
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据尝试次数选择不同策略
|
|
||||||
const attemptProgress = attempt / options.maxAttempts; // 0到1的进度值
|
|
||||||
|
|
||||||
// 小词云优先使用紧凑布局策略
|
|
||||||
if (isSmallWordCloud) {
|
|
||||||
if (attemptProgress < 0.6) {
|
|
||||||
return getSpiralPosition(textWidth, textHeight, attempt / 2, canvasShape); // 减少螺旋步长,更紧凑
|
|
||||||
} else {
|
|
||||||
return Math.random() < 0.7 ?
|
|
||||||
findGapPosition(textWidth, textHeight, canvasShape) :
|
|
||||||
getEdgeExtendPosition(textWidth, textHeight, attempt / 2, canvasShape);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 高频词优先使用螺旋或中心布局
|
|
||||||
if (index < sortedWords.length * 0.1) {
|
|
||||||
if (attemptProgress < 0.5) {
|
|
||||||
return getSpiralPosition(textWidth, textHeight, attempt, canvasShape);
|
|
||||||
} else {
|
|
||||||
return Math.random() < 0.7 ?
|
|
||||||
findGapPosition(textWidth, textHeight, canvasShape) :
|
|
||||||
getEdgeExtendPosition(textWidth, textHeight, attempt, canvasShape);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不同阶段使用不同策略
|
|
||||||
if (attemptProgress < 0.3) {
|
|
||||||
// 前30%尝试: 主要使用改进的螺旋
|
|
||||||
return getSpiralPosition(textWidth, textHeight, attempt, canvasShape);
|
|
||||||
} else if (attemptProgress < 0.7) {
|
|
||||||
// 中间40%尝试: 主要寻找空白区域
|
|
||||||
return Math.random() < 0.8 ?
|
|
||||||
findGapPosition(textWidth, textHeight, canvasShape) :
|
|
||||||
getSpiralPosition(textWidth, textHeight, attempt, canvasShape);
|
|
||||||
} else {
|
|
||||||
// 后30%尝试: 主要使用边缘扩展和随机策略
|
|
||||||
const r = Math.random();
|
|
||||||
if (r < 0.6) {
|
|
||||||
return getEdgeExtendPosition(textWidth, textHeight, attempt, canvasShape);
|
|
||||||
} else if (r < 0.8) {
|
|
||||||
return findGapPosition(textWidth, textHeight, canvasShape);
|
|
||||||
} else {
|
|
||||||
return getSpiralPosition(textWidth, textHeight, attempt * 2, canvasShape); // 双倍螺旋步进,迅速扩展
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 记录所有单词的边界以自动调整画布大小
|
|
||||||
let minX = initialWidth;
|
|
||||||
let maxX = 0;
|
|
||||||
let minY = initialHeight;
|
|
||||||
let maxY = 0;
|
|
||||||
|
|
||||||
// 记录原始中心点,用于居中重定位
|
|
||||||
let originalCenterX = initialWidth / 2;
|
|
||||||
let originalCenterY = initialHeight / 2;
|
|
||||||
|
|
||||||
// 动态画布扩展计数
|
|
||||||
let canvasExpansionCount = 0;
|
|
||||||
|
|
||||||
// 第一轮:计算每个单词的位置并追踪边界
|
|
||||||
for (let i = 0; i < sortedWords.length; i++) {
|
|
||||||
const { word, frequency } = sortedWords[i]!;
|
|
||||||
|
|
||||||
// 安全检查 - 过滤不可渲染字符
|
|
||||||
const safeWord = filterUnrenderableChars(word);
|
|
||||||
if (!safeWord) continue;
|
|
||||||
|
|
||||||
// 使用增强的字体大小计算函数
|
|
||||||
const fontSize = calculateFontSize(frequency, i);
|
|
||||||
|
|
||||||
// 获取合适的字体
|
|
||||||
const fontFamily = getFontFamily(safeWord);
|
|
||||||
|
|
||||||
// 设置字体和测量文本
|
|
||||||
tempCtx.font = `bold ${fontSize}px ${fontFamily}`;
|
|
||||||
const metrics = tempCtx.measureText(safeWord);
|
|
||||||
|
|
||||||
// 更精确地计算文本高度
|
|
||||||
const textHeight = fontSize;
|
|
||||||
const textWidth = metrics.width;
|
|
||||||
|
|
||||||
// 获取当前云形状与密度
|
|
||||||
const cloudShape = getCloudShape();
|
|
||||||
|
|
||||||
// 获取最佳旋转角度
|
|
||||||
const rotation = getOptimalRotation(safeWord, textWidth, textHeight, i);
|
|
||||||
|
|
||||||
// 尝试定位
|
|
||||||
let positioned = false;
|
|
||||||
let finalX = 0, finalY = 0;
|
|
||||||
|
|
||||||
// 尝试放置单词,如果失败可能会扩展画布
|
|
||||||
for (let attempt = 0; attempt < options.maxAttempts && !positioned; attempt++) {
|
|
||||||
// 使用多策略获取位置,而不是仅用螺旋布局
|
|
||||||
const { x, y } = getPositionWithStrategy(textWidth, textHeight, attempt, cloudShape, i);
|
|
||||||
|
|
||||||
if (!isOverlapping(x, y, textWidth, textHeight, rotation, i)) {
|
|
||||||
finalX = x;
|
|
||||||
finalY = y;
|
|
||||||
positioned = true;
|
|
||||||
|
|
||||||
// 获取此单词旋转后的包围盒
|
|
||||||
const box = getRotatedBoundingBox(x, y, textWidth, textHeight, rotation);
|
|
||||||
|
|
||||||
// 更新整体边界
|
|
||||||
minX = Math.min(minX, box.minX);
|
|
||||||
maxX = Math.max(maxX, box.maxX);
|
|
||||||
minY = Math.min(minY, box.minY);
|
|
||||||
maxY = Math.max(maxY, box.maxY);
|
|
||||||
|
|
||||||
// 记录位置,保存字体大小
|
|
||||||
placedWords.push({
|
|
||||||
x: finalX,
|
|
||||||
y: finalY,
|
|
||||||
width: textWidth,
|
|
||||||
height: textHeight,
|
|
||||||
rotation,
|
|
||||||
fontSize
|
|
||||||
});
|
|
||||||
} else if (attempt === options.maxAttempts - 1 && canvasExpansionCount < options.maxExpansionAttempts) {
|
|
||||||
// 如果所有尝试都失败,并且还有扩展余量,则扩展画布
|
|
||||||
canvasExpansionCount++;
|
|
||||||
|
|
||||||
// 计算当前中心点
|
|
||||||
const currentCenterX = (maxX + minX) / 2;
|
|
||||||
const currentCenterY = (maxY + minY) / 2;
|
|
||||||
|
|
||||||
// 保存原始画布尺寸
|
|
||||||
const oldWidth = initialWidth;
|
|
||||||
const oldHeight = initialHeight;
|
|
||||||
|
|
||||||
// 扩展画布尺寸 - 使用更小的扩展比例
|
|
||||||
initialWidth = Math.ceil(initialWidth * options.expansionRatio);
|
|
||||||
initialHeight = Math.ceil(initialHeight * options.expansionRatio);
|
|
||||||
|
|
||||||
// 计算扩展量
|
|
||||||
const widthIncrease = initialWidth - oldWidth;
|
|
||||||
const heightIncrease = initialHeight - oldHeight;
|
|
||||||
|
|
||||||
// 调整所有已放置单词的位置,使它们保持居中
|
|
||||||
placedWords.forEach(pos => {
|
|
||||||
// 相对于原中心的偏移
|
|
||||||
const offsetX = pos.x - originalCenterX;
|
|
||||||
const offsetY = pos.y - originalCenterY;
|
|
||||||
|
|
||||||
// 计算新位置,保持相对于中心的偏移不变
|
|
||||||
pos.x = originalCenterX + widthIncrease / 2 + offsetX;
|
|
||||||
pos.y = originalCenterY + heightIncrease / 2 + offsetY;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新坐标系中心点
|
|
||||||
originalCenterX = originalCenterX + widthIncrease / 2;
|
|
||||||
originalCenterY = originalCenterY + heightIncrease / 2;
|
|
||||||
|
|
||||||
// 更新边界信息
|
|
||||||
minX += widthIncrease / 2;
|
|
||||||
maxX += widthIncrease / 2;
|
|
||||||
minY += heightIncrease / 2;
|
|
||||||
maxY += heightIncrease / 2;
|
|
||||||
|
|
||||||
// 重新创建临时画布
|
|
||||||
tempCanvas = createCanvas(initialWidth, initialHeight);
|
|
||||||
tempCtx = tempCanvas.getContext('2d');
|
|
||||||
|
|
||||||
// 重置尝试计数,在新的扩展画布上再次尝试
|
|
||||||
attempt = -1; // 会在循环中+1变成0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果仍然无法放置,尝试增加重叠容忍度
|
|
||||||
if (!positioned) {
|
|
||||||
const maxOverlapThreshold = options.overlapThreshold * 2.0; // 允许更多重叠
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < options.maxAttempts && !positioned; attempt++) {
|
|
||||||
// 再次使用多策略获取位置
|
|
||||||
const { x, y } = getPositionWithStrategy(textWidth, textHeight, attempt, cloudShape, i);
|
|
||||||
|
|
||||||
// 获取当前词的包围盒
|
|
||||||
const currentBox = getRotatedBoundingBox(x, y, textWidth, textHeight, rotation);
|
|
||||||
|
|
||||||
// 计算当前单词的面积
|
|
||||||
const currentArea = (currentBox.maxX - currentBox.minX) * (currentBox.maxY - currentBox.minY);
|
|
||||||
|
|
||||||
// 计算最大重叠面积
|
|
||||||
let maxOverlapArea = 0;
|
|
||||||
|
|
||||||
for (const pos of placedWords) {
|
|
||||||
const posBox = getRotatedBoundingBox(
|
|
||||||
pos.x, pos.y, pos.width, pos.height, pos.rotation
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算重叠区域
|
|
||||||
const overlapX = Math.max(0, Math.min(currentBox.maxX, posBox.maxX) - Math.max(currentBox.minX, posBox.minX));
|
|
||||||
const overlapY = Math.max(0, Math.min(currentBox.maxY, posBox.maxY) - Math.max(currentBox.minY, posBox.minY));
|
|
||||||
const overlapArea = overlapX * overlapY;
|
|
||||||
|
|
||||||
maxOverlapArea = Math.max(maxOverlapArea, overlapArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算重叠率
|
|
||||||
const overlapRatio = maxOverlapArea / currentArea;
|
|
||||||
|
|
||||||
// 如果重叠率在允许范围内,则放置
|
|
||||||
if (overlapRatio <= maxOverlapThreshold) {
|
|
||||||
finalX = x;
|
|
||||||
finalY = y;
|
|
||||||
positioned = true;
|
|
||||||
|
|
||||||
// 获取此单词旋转后的包围盒
|
|
||||||
const box = getRotatedBoundingBox(x, y, textWidth, textHeight, rotation);
|
|
||||||
|
|
||||||
// 更新整体边界(即使是增加重叠度放置的单词也计入边界)
|
|
||||||
minX = Math.min(minX, box.minX);
|
|
||||||
maxX = Math.max(maxX, box.maxX);
|
|
||||||
minY = Math.min(minY, box.minY);
|
|
||||||
maxY = Math.max(maxY, box.maxY);
|
|
||||||
|
|
||||||
// 记录位置
|
|
||||||
placedWords.push({
|
|
||||||
x: finalX,
|
|
||||||
y: finalY,
|
|
||||||
width: textWidth,
|
|
||||||
height: textHeight,
|
|
||||||
rotation,
|
|
||||||
fontSize
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果仍然无法放置,则跳过该词
|
|
||||||
if (!positioned) {
|
|
||||||
console.log(`无法放置单词: ${safeWord}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第二阶段:确定最终画布大小并绘制
|
|
||||||
// 添加内边距
|
|
||||||
minX = Math.max(0, minX - options.padding);
|
|
||||||
minY = Math.max(0, minY - options.padding);
|
|
||||||
maxX = maxX + options.padding;
|
|
||||||
maxY = maxY + options.padding;
|
|
||||||
|
|
||||||
// 计算最终画布尺寸
|
|
||||||
const finalWidth = Math.ceil(maxX - minX);
|
|
||||||
const finalHeight = Math.ceil(maxY - minY);
|
|
||||||
|
|
||||||
// 创建最终画布
|
|
||||||
const canvas = createCanvas(finalWidth, finalHeight);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
// 设置背景
|
|
||||||
ctx.fillStyle = options.backgroundColor;
|
|
||||||
ctx.fillRect(0, 0, finalWidth, finalHeight);
|
|
||||||
|
|
||||||
for (let i = 0; i < sortedWords.length; i++) {
|
|
||||||
if (i >= placedWords.length) continue;
|
|
||||||
|
|
||||||
const { word, frequency } = sortedWords[i]!;
|
|
||||||
const position = placedWords[i];
|
|
||||||
if (!position) continue;
|
|
||||||
|
|
||||||
const safeWord = filterUnrenderableChars(word);
|
|
||||||
if (!safeWord) continue;
|
|
||||||
|
|
||||||
const fontFamily = getFontFamily(safeWord);
|
|
||||||
|
|
||||||
ctx.font = `bold ${position.fontSize}px ${fontFamily}`;
|
|
||||||
ctx.fillStyle = getColorFromFrequency(frequency, i);
|
|
||||||
|
|
||||||
const adjustedX = position.x - minX;
|
|
||||||
const adjustedY = position.y - minY;
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(
|
|
||||||
adjustedX + position.width / 2,
|
|
||||||
adjustedY + position.height / 2
|
|
||||||
);
|
|
||||||
ctx.rotate(position.rotation * Math.PI / 180);
|
|
||||||
ctx.fillText(safeWord, -position.width / 2, position.height / 2);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = canvas.toBuffer('image/png');
|
|
||||||
return "base64://" + buffer.toString('base64');
|
|
||||||
}
|
|
@@ -1,82 +0,0 @@
|
|||||||
# QRCode Terminal Edition [![Build Status][travis-ci-img]][travis-ci-url]
|
|
||||||
|
|
||||||
> Going where no QRCode has gone before.
|
|
||||||
|
|
||||||
![Basic Example][basic-example-img]
|
|
||||||
|
|
||||||
# Node Library
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Can be installed with:
|
|
||||||
|
|
||||||
$ npm install qrcode-terminal
|
|
||||||
|
|
||||||
and used:
|
|
||||||
|
|
||||||
var qrcode = require('qrcode-terminal');
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
To display some data to the terminal just call:
|
|
||||||
|
|
||||||
qrcode.generate('This will be a QRCode, eh!');
|
|
||||||
|
|
||||||
You can even specify the error level (default is 'L'):
|
|
||||||
|
|
||||||
qrcode.setErrorLevel('Q');
|
|
||||||
qrcode.generate('This will be a QRCode with error level Q!');
|
|
||||||
|
|
||||||
If you don't want to display to the terminal but just want to string you can provide a callback:
|
|
||||||
|
|
||||||
qrcode.generate('http://github.com', function (qrcode) {
|
|
||||||
console.log(qrcode);
|
|
||||||
});
|
|
||||||
|
|
||||||
If you want to display small output, provide `opts` with `small`:
|
|
||||||
|
|
||||||
qrcode.generate('This will be a small QRCode, eh!', {small: true});
|
|
||||||
|
|
||||||
qrcode.generate('This will be a small QRCode, eh!', {small: true}, function (qrcode) {
|
|
||||||
console.log(qrcode)
|
|
||||||
});
|
|
||||||
|
|
||||||
# Command-Line
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
$ npm install -g qrcode-terminal
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
$ qrcode-terminal --help
|
|
||||||
$ qrcode-terminal 'http://github.com'
|
|
||||||
$ echo 'http://github.com' | qrcode-terminal
|
|
||||||
|
|
||||||
# Support
|
|
||||||
|
|
||||||
- OS X
|
|
||||||
- Linux
|
|
||||||
- Windows
|
|
||||||
|
|
||||||
# Server-side
|
|
||||||
|
|
||||||
[node-qrcode][node-qrcode-url] is a popular server-side QRCode generator that
|
|
||||||
renders to a `canvas` object.
|
|
||||||
|
|
||||||
# Developing
|
|
||||||
|
|
||||||
To setup the development envrionment run `npm install`
|
|
||||||
|
|
||||||
To run tests run `npm test`
|
|
||||||
|
|
||||||
# Contributers
|
|
||||||
|
|
||||||
Gord Tanner <gtanner@gmail.com>
|
|
||||||
Micheal Brooks <michael@michaelbrooks.ca>
|
|
||||||
|
|
||||||
[travis-ci-img]: https://travis-ci.org/gtanner/qrcode-terminal.png
|
|
||||||
[travis-ci-url]: https://travis-ci.org/gtanner/qrcode-terminal
|
|
||||||
[basic-example-img]: https://raw.github.com/gtanner/qrcode-terminal/master/example/basic.png
|
|
||||||
[node-qrcode-url]: https://github.com/soldair/node-qrcode
|
|
||||||
|
|
536
src/remote/pipe.ts
Normal file
536
src/remote/pipe.ts
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
import * as net from 'net';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export interface Packet<T = any> {
|
||||||
|
command: string;
|
||||||
|
trace: string;
|
||||||
|
data: T;
|
||||||
|
type: 'listener_callback' | 'event_response' | 'event_request' | 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 协议常量
|
||||||
|
const PROTOCOL_MAGIC = 0x4E415043; // 'NAPC'
|
||||||
|
const PROTOCOL_VERSION = 0x01;
|
||||||
|
const HEADER_SIZE = 12;
|
||||||
|
const MAX_PACKET_SIZE = 16 * 1024 * 1024; // 降低到16MB
|
||||||
|
const BUFFER_HIGH_WATER_MARK = 2 * 1024 * 1024; // 2MB背压阈值
|
||||||
|
const BUFFER_LOW_WATER_MARK = 512 * 1024; // 512KB恢复阈值
|
||||||
|
|
||||||
|
// 高效缓冲区管理器
|
||||||
|
class BufferManager {
|
||||||
|
private buffers: Buffer[] = [];
|
||||||
|
private totalSize: number = 0;
|
||||||
|
private readOffset: number = 0;
|
||||||
|
private isHighWaterMark: boolean = false;
|
||||||
|
|
||||||
|
// 添加数据
|
||||||
|
append(data: Buffer): void {
|
||||||
|
this.buffers.push(data);
|
||||||
|
this.totalSize += data.length;
|
||||||
|
|
||||||
|
// 检查背压
|
||||||
|
if (!this.isHighWaterMark && this.totalSize > BUFFER_HIGH_WATER_MARK) {
|
||||||
|
this.isHighWaterMark = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消费数据
|
||||||
|
consume(length: number): Buffer {
|
||||||
|
if (length > this.available) {
|
||||||
|
throw new Error('消费长度超过可用数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Buffer.allocUnsafe(length);
|
||||||
|
let resultOffset = 0;
|
||||||
|
let remaining = length;
|
||||||
|
|
||||||
|
while (remaining > 0 && this.buffers.length > 0) {
|
||||||
|
const currentBuffer = this.buffers[0];
|
||||||
|
if (!currentBuffer?.[0]) continue;
|
||||||
|
const availableInCurrent = currentBuffer.length - this.readOffset;
|
||||||
|
const toCopy = Math.min(remaining, availableInCurrent);
|
||||||
|
|
||||||
|
currentBuffer.copy(result, resultOffset, this.readOffset, this.readOffset + toCopy);
|
||||||
|
resultOffset += toCopy;
|
||||||
|
remaining -= toCopy;
|
||||||
|
this.readOffset += toCopy;
|
||||||
|
|
||||||
|
// 如果当前buffer用完了,移除它
|
||||||
|
if (this.readOffset >= currentBuffer.length) {
|
||||||
|
this.buffers.shift();
|
||||||
|
this.readOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totalSize -= length;
|
||||||
|
|
||||||
|
// 检查是否可以恢复读取
|
||||||
|
if (this.isHighWaterMark && this.totalSize < BUFFER_LOW_WATER_MARK) {
|
||||||
|
this.isHighWaterMark = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览数据(不消费)
|
||||||
|
peek(length: number): Buffer | null {
|
||||||
|
if (length > this.available) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Buffer.allocUnsafe(length);
|
||||||
|
let resultOffset = 0;
|
||||||
|
let remaining = length;
|
||||||
|
let bufferIndex = 0;
|
||||||
|
let currentReadOffset = this.readOffset;
|
||||||
|
|
||||||
|
while (remaining > 0 && bufferIndex < this.buffers.length) {
|
||||||
|
const currentBuffer = this.buffers[bufferIndex];
|
||||||
|
if (!currentBuffer) continue;
|
||||||
|
const availableInCurrent = currentBuffer.length - currentReadOffset;
|
||||||
|
const toCopy = Math.min(remaining, availableInCurrent);
|
||||||
|
|
||||||
|
currentBuffer.copy(result, resultOffset, currentReadOffset, currentReadOffset + toCopy);
|
||||||
|
resultOffset += toCopy;
|
||||||
|
remaining -= toCopy;
|
||||||
|
|
||||||
|
if (currentReadOffset + toCopy >= currentBuffer.length) {
|
||||||
|
bufferIndex++;
|
||||||
|
currentReadOffset = 0;
|
||||||
|
} else {
|
||||||
|
currentReadOffset += toCopy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get available(): number {
|
||||||
|
return this.totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldPause(): boolean {
|
||||||
|
return this.isHighWaterMark;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.buffers = [];
|
||||||
|
this.totalSize = 0;
|
||||||
|
this.readOffset = 0;
|
||||||
|
this.isHighWaterMark = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的数据包管理器
|
||||||
|
class PacketManager {
|
||||||
|
static pack(packet: Packet): Buffer {
|
||||||
|
const jsonStr = JSON.stringify(packet);
|
||||||
|
const jsonBuffer = Buffer.from(jsonStr, 'utf8');
|
||||||
|
|
||||||
|
if (jsonBuffer.length > MAX_PACKET_SIZE - HEADER_SIZE) {
|
||||||
|
throw new Error(`数据包过大: ${jsonBuffer.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.allocUnsafe(HEADER_SIZE + jsonBuffer.length);
|
||||||
|
|
||||||
|
buffer.writeUInt32BE(PROTOCOL_MAGIC, 0);
|
||||||
|
buffer.writeUInt32BE(jsonBuffer.length, 4);
|
||||||
|
buffer.writeUInt32BE(PROTOCOL_VERSION, 8);
|
||||||
|
jsonBuffer.copy(buffer, HEADER_SIZE);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unpack(bufferManager: BufferManager): Packet[] {
|
||||||
|
const packets: Packet[] = [];
|
||||||
|
|
||||||
|
while (bufferManager.available >= HEADER_SIZE) {
|
||||||
|
// 检查魔数
|
||||||
|
const header = bufferManager.peek(HEADER_SIZE);
|
||||||
|
if (!header) break;
|
||||||
|
|
||||||
|
const magic = header.readUInt32BE(0);
|
||||||
|
if (magic !== PROTOCOL_MAGIC) {
|
||||||
|
// 简单的同步恢复:跳过一个字节
|
||||||
|
bufferManager.consume(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataLength = header.readUInt32BE(4);
|
||||||
|
//const version = header.readUInt32BE(8);
|
||||||
|
|
||||||
|
// 基本验证
|
||||||
|
if (dataLength <= 0 || dataLength > MAX_PACKET_SIZE - HEADER_SIZE) {
|
||||||
|
bufferManager.consume(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查完整包
|
||||||
|
const totalSize = HEADER_SIZE + dataLength;
|
||||||
|
if (bufferManager.available < totalSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消费完整包
|
||||||
|
bufferManager.consume(HEADER_SIZE);
|
||||||
|
const jsonBuffer = bufferManager.consume(dataLength);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packet = JSON.parse(jsonBuffer.toString('utf8')) as Packet;
|
||||||
|
if (this.isValidPacket(packet)) {
|
||||||
|
packets.push(packet);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JSON解析失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isValidPacket(packet: any): packet is Packet {
|
||||||
|
return packet &&
|
||||||
|
typeof packet.command === 'string' &&
|
||||||
|
typeof packet.trace === 'string' &&
|
||||||
|
packet.data !== undefined &&
|
||||||
|
['listener_callback', 'event_response', 'event_request', 'default'].includes(packet.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static createRequest<T = any>(command: string, data: T, trace?: string): Packet<T> {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
trace: trace || randomUUID(),
|
||||||
|
data,
|
||||||
|
type: 'event_request'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createResponse<T = any>(trace: string, data: T, command = ''): Packet<T> {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
trace,
|
||||||
|
data,
|
||||||
|
type: 'event_response'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createCallback<T = any>(command: string, data: T, trace?: string): Packet<T> {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
trace: trace || randomUUID(),
|
||||||
|
data,
|
||||||
|
type: 'listener_callback'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应助手类
|
||||||
|
class ResponseHelper {
|
||||||
|
private responseSent = false;
|
||||||
|
|
||||||
|
constructor(private socket: net.Socket, private trace: string, private command: string = '') { }
|
||||||
|
|
||||||
|
success<T = any>(data: T): void {
|
||||||
|
if (this.responseSent) return;
|
||||||
|
|
||||||
|
const response = PacketManager.createResponse(this.trace, data, this.command);
|
||||||
|
this.writePacket(response);
|
||||||
|
this.responseSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, code = 500): void {
|
||||||
|
if (this.responseSent) return;
|
||||||
|
|
||||||
|
const response = PacketManager.createResponse(this.trace, { error: message, code }, this.command);
|
||||||
|
this.writePacket(response);
|
||||||
|
this.responseSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEventResponse<T = any>(trace: string, data: T): void {
|
||||||
|
const response = PacketManager.createResponse(trace, data, this.command);
|
||||||
|
this.writePacket(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendListenerCallback<T = any>(command: string, data: T): void {
|
||||||
|
const callback = PacketManager.createCallback(command, data);
|
||||||
|
this.writePacket(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private writePacket(packet: Packet): void {
|
||||||
|
console.log(`发送数据包: ${packet.command}, trace: ${packet.trace} (${packet.type}) `);
|
||||||
|
if (!this.socket.destroyed) {
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
this.socket.write(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasResponseSent(): boolean {
|
||||||
|
return this.responseSent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带背压控制的Socket包装器
|
||||||
|
class ManagedSocket {
|
||||||
|
private bufferManager = new BufferManager();
|
||||||
|
private isPaused = false;
|
||||||
|
|
||||||
|
constructor(private socket: net.Socket, private onPacket: (packet: Packet) => void) {
|
||||||
|
this.setupSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSocket(): void {
|
||||||
|
this.socket.on('data', (chunk) => {
|
||||||
|
this.bufferManager.append(chunk);
|
||||||
|
|
||||||
|
// 背压控制
|
||||||
|
if (this.bufferManager.shouldPause && !this.isPaused) {
|
||||||
|
this.socket.pause();
|
||||||
|
this.isPaused = true;
|
||||||
|
console.warn('Socket暂停读取 - 缓冲区过大');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processPackets();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('drain', () => {
|
||||||
|
// 当socket的写缓冲区有空间时,检查是否可以恢复读取
|
||||||
|
if (this.isPaused && !this.bufferManager.shouldPause) {
|
||||||
|
this.socket.resume();
|
||||||
|
this.isPaused = false;
|
||||||
|
console.log('Socket恢复读取');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processPackets(): void {
|
||||||
|
try {
|
||||||
|
const packets = PacketManager.unpack(this.bufferManager);
|
||||||
|
packets.forEach(packet => this.onPacket(packet));
|
||||||
|
|
||||||
|
// 处理完包后检查是否可以恢复读取
|
||||||
|
if (this.isPaused && !this.bufferManager.shouldPause) {
|
||||||
|
this.socket.resume();
|
||||||
|
this.isPaused = false;
|
||||||
|
console.log('Socket恢复读取');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理数据包失败:', error);
|
||||||
|
this.bufferManager.reset();
|
||||||
|
if (this.isPaused) {
|
||||||
|
this.socket.resume();
|
||||||
|
this.isPaused = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write(buffer: Buffer): boolean {
|
||||||
|
return this.socket.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
get destroyed(): boolean {
|
||||||
|
return this.socket.destroyed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PacketHandler = (packet: Packet, helper: ResponseHelper) => Promise<any> | any;
|
||||||
|
|
||||||
|
// 简化的管道服务端
|
||||||
|
class PipeServer extends EventEmitter {
|
||||||
|
private server: net.Server;
|
||||||
|
private clients: Map<net.Socket, ManagedSocket> = new Map();
|
||||||
|
private handler: PacketHandler | null = null;
|
||||||
|
|
||||||
|
constructor(private pipeName: string) {
|
||||||
|
super();
|
||||||
|
this.server = net.createServer();
|
||||||
|
this.setupServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupServer(): void {
|
||||||
|
this.server.on('connection', (socket) => {
|
||||||
|
console.log('客户端连接');
|
||||||
|
|
||||||
|
const managedSocket = new ManagedSocket(socket, (packet) => {
|
||||||
|
this.handlePacket(packet, socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.clients.set(socket, managedSocket);
|
||||||
|
|
||||||
|
socket.on('close', () => {
|
||||||
|
console.log('客户端断开');
|
||||||
|
this.clients.delete(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (error) => {
|
||||||
|
console.error('Socket错误:', error);
|
||||||
|
this.clients.delete(socket);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHandler(handler: PacketHandler): void {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePacket(packet: Packet, socket: net.Socket): Promise<void> {
|
||||||
|
if (packet.type === 'event_response' || packet.type === 'listener_callback') {
|
||||||
|
this.emit(packet.type, packet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const helper = new ResponseHelper(socket, packet.trace, packet.command);
|
||||||
|
|
||||||
|
if (!this.handler) {
|
||||||
|
helper.error('未注册处理器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.handler(packet, helper);
|
||||||
|
if (result !== undefined && !helper.hasResponseSent) {
|
||||||
|
helper.success(result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!helper.hasResponseSent) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
helper.error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server.listen(this.pipeName, () => {
|
||||||
|
console.log(`管道服务器启动: ${this.pipeName}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.server.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.clients.forEach((managedSocket) => managedSocket.destroy());
|
||||||
|
this.clients.clear();
|
||||||
|
this.server.close(() => {
|
||||||
|
console.log('管道服务器停止');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast<T = any>(command: string, data: T, type: Packet['type'] = 'default'): void {
|
||||||
|
const packet: Packet<T> = {
|
||||||
|
command,
|
||||||
|
trace: randomUUID(),
|
||||||
|
data,
|
||||||
|
type
|
||||||
|
};
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
|
||||||
|
this.clients.forEach((managedSocket) => {
|
||||||
|
if (!managedSocket.destroyed) {
|
||||||
|
managedSocket.write(buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientCount(): number {
|
||||||
|
return this.clients.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的管道客户端
|
||||||
|
class PipeClient extends EventEmitter {
|
||||||
|
private socket: net.Socket | null = null;
|
||||||
|
private managedSocket: ManagedSocket | null = null;
|
||||||
|
private isConnected = false;
|
||||||
|
private handler: PacketHandler | null = null;
|
||||||
|
|
||||||
|
constructor(private pipeName: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHandler(handler: PacketHandler): void {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket = net.createConnection(this.pipeName);
|
||||||
|
|
||||||
|
this.managedSocket = new ManagedSocket(this.socket, (packet) => {
|
||||||
|
this.handlePacket(packet);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('connect', () => {
|
||||||
|
console.log('连接到管道服务器');
|
||||||
|
this.isConnected = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('close', () => {
|
||||||
|
console.log('与服务器断开连接');
|
||||||
|
this.isConnected = false;
|
||||||
|
this.emit('disconnect');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('error', (error) => {
|
||||||
|
console.error('Socket错误:', error);
|
||||||
|
this.isConnected = false;
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePacket(packet: Packet): Promise<void> {
|
||||||
|
if (this.handler && this.socket) {
|
||||||
|
const helper = new ResponseHelper(this.socket, packet.trace, packet.command);
|
||||||
|
try {
|
||||||
|
await this.handler(packet, helper);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理数据包失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest<T = any>(command: string, data: T, trace?: string): void {
|
||||||
|
if (!this.isConnected || !this.managedSocket) {
|
||||||
|
throw new Error('未连接到服务器');
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = PacketManager.createRequest(command, data, trace);
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
this.managedSocket.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse<T = any>(trace: string, data: T, command = ''): void {
|
||||||
|
if (!this.isConnected || !this.managedSocket) {
|
||||||
|
throw new Error('未连接到服务器');
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = PacketManager.createResponse(trace, data, command);
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
this.managedSocket.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
if (this.managedSocket) {
|
||||||
|
this.managedSocket.destroy();
|
||||||
|
this.managedSocket = null;
|
||||||
|
}
|
||||||
|
this.socket = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get connected(): boolean {
|
||||||
|
return this.isConnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PipeServer, PipeClient, PacketManager, ResponseHelper, BufferManager };
|
109
src/remote/remoteSession.ts
Normal file
109
src/remote/remoteSession.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { createRemoteServiceClient } from "@/remote/service";
|
||||||
|
import {
|
||||||
|
NodeIQQNTWrapperSession,
|
||||||
|
WrapperSessionInitConfig
|
||||||
|
} from "../core/wrapper";
|
||||||
|
import { NodeIKernelSessionListener } from "../core/listeners/NodeIKernelSessionListener";
|
||||||
|
import {
|
||||||
|
NodeIDependsAdapter,
|
||||||
|
NodeIDispatcherAdapter
|
||||||
|
} from "../core/adapters";
|
||||||
|
import { ServiceNamingMapping } from "@/core";
|
||||||
|
|
||||||
|
class RemoteServiceManager {
|
||||||
|
private services: Map<string, any> = new Map();
|
||||||
|
private handler;
|
||||||
|
|
||||||
|
constructor(handler: (client: any, listenerCommand: string, ...args: any[]) => Promise<any>) {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
private createRemoteService<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T
|
||||||
|
): ServiceNamingMapping[T] {
|
||||||
|
if (this.services.has(serviceName)) {
|
||||||
|
return this.services.get(serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
let serviceClient: any;
|
||||||
|
serviceClient = createRemoteServiceClient(serviceName, async (serviceCommand, ...args) => {
|
||||||
|
return await this.handler(serviceClient, serviceCommand, ...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.services.set(serviceName, serviceClient.object);
|
||||||
|
return serviceClient.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
getService<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T
|
||||||
|
): ServiceNamingMapping[T] {
|
||||||
|
return this.createRemoteService(serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export class RemoteWrapperSession implements NodeIQQNTWrapperSession {
|
||||||
|
private serviceManager: RemoteServiceManager;
|
||||||
|
|
||||||
|
constructor(handler: (client: { object: keyof ServiceNamingMapping, receiverListener: (command: string, ...args: any[]) => void }, listenerCommand: string, ...args: any[]) => Promise<void>) {
|
||||||
|
this.serviceManager = new RemoteServiceManager(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): RemoteWrapperSession {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
_wrapperSessionInitConfig: WrapperSessionInitConfig,
|
||||||
|
_nodeIDependsAdapter: NodeIDependsAdapter,
|
||||||
|
_nodeIDispatcherAdapter: NodeIDispatcherAdapter,
|
||||||
|
_nodeIKernelSessionListener: NodeIKernelSessionListener,
|
||||||
|
): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
startNT(_session?: number): void {
|
||||||
|
}
|
||||||
|
getBdhUploadService() { return null; }
|
||||||
|
getECDHService() { return this.serviceManager.getService('NodeIKernelECDHService'); }
|
||||||
|
getMsgService() { return this.serviceManager.getService('NodeIKernelMsgService'); }
|
||||||
|
getProfileService() { return this.serviceManager.getService('NodeIKernelProfileService'); }
|
||||||
|
getProfileLikeService() { return this.serviceManager.getService('NodeIKernelProfileLikeService'); }
|
||||||
|
getGroupService() { return this.serviceManager.getService('NodeIKernelGroupService'); }
|
||||||
|
getStorageCleanService() { return this.serviceManager.getService('NodeIKernelStorageCleanService'); }
|
||||||
|
getBuddyService() { return this.serviceManager.getService('NodeIKernelBuddyService'); }
|
||||||
|
getRobotService() { return this.serviceManager.getService('NodeIKernelRobotService'); }
|
||||||
|
getTicketService() { return this.serviceManager.getService('NodeIKernelTicketService'); }
|
||||||
|
getTipOffService() { return this.serviceManager.getService('NodeIKernelTipOffService'); }
|
||||||
|
getNodeMiscService() { return this.serviceManager.getService('NodeIKernelNodeMiscService'); }
|
||||||
|
getRichMediaService() { return this.serviceManager.getService('NodeIKernelRichMediaService'); }
|
||||||
|
getMsgBackupService() { return this.serviceManager.getService('NodeIKernelMsgBackupService'); }
|
||||||
|
getAlbumService() { return this.serviceManager.getService('NodeIKernelAlbumService'); }
|
||||||
|
getTianShuService() { return this.serviceManager.getService('NodeIKernelTianShuService'); }
|
||||||
|
getUnitedConfigService() { return this.serviceManager.getService('NodeIKernelUnitedConfigService'); }
|
||||||
|
getSearchService() { return this.serviceManager.getService('NodeIKernelSearchService'); }
|
||||||
|
getDirectSessionService() { return null; }
|
||||||
|
getRDeliveryService() { return null; }
|
||||||
|
getAvatarService() { return this.serviceManager.getService('NodeIKernelAvatarService'); }
|
||||||
|
getFeedChannelService() { return null; }
|
||||||
|
getYellowFaceService() { return null; }
|
||||||
|
getCollectionService() { return this.serviceManager.getService('NodeIKernelCollectionService'); }
|
||||||
|
getSettingService() { return null; }
|
||||||
|
getQiDianService() { return null; }
|
||||||
|
getFileAssistantService() { return this.serviceManager.getService('NodeIKernelFileAssistantService'); }
|
||||||
|
getGuildService() { return null; }
|
||||||
|
getSkinService() { return null; }
|
||||||
|
getTestPerformanceService() { return this.serviceManager.getService('NodeIkernelTestPerformanceService'); }
|
||||||
|
getQQPlayService() { return null; }
|
||||||
|
getDbToolsService() { return this.serviceManager.getService('NodeIKernelDbToolsService'); }
|
||||||
|
getUixConvertService() { return this.serviceManager.getService('NodeIKernelUixConvertService'); }
|
||||||
|
getOnlineStatusService() { return this.serviceManager.getService('NodeIKernelOnlineStatusService'); }
|
||||||
|
getRemotingService() { return null; }
|
||||||
|
getGroupTabService() { return null; }
|
||||||
|
getGroupSchoolService() { return null; }
|
||||||
|
getLiteBusinessService() { return null; }
|
||||||
|
getGuildMsgService() { return null; }
|
||||||
|
getLockService() { return null; }
|
||||||
|
getMSFService() { return this.serviceManager.getService('NodeIKernelMSFService'); }
|
||||||
|
getGuildHotUpdateService() { return null; }
|
||||||
|
getAVSDKService() { return null; }
|
||||||
|
getRecentContactService() { return this.serviceManager.getService('NodeIKernelRecentContactService'); }
|
||||||
|
getConfigMgrService() { return null; }
|
||||||
|
}
|
1210
src/remote/serialize.cpp
Normal file
1210
src/remote/serialize.cpp
Normal file
File diff suppressed because it is too large
Load Diff
131
src/remote/serialize.ts
Normal file
131
src/remote/serialize.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
interface EncodedValue {
|
||||||
|
$type: string;
|
||||||
|
$value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedNull {
|
||||||
|
$type: "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedUndefined {
|
||||||
|
$type: "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedPrimitive {
|
||||||
|
$type: "number" | "string" | "boolean";
|
||||||
|
$value: number | string | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedBuffer {
|
||||||
|
$type: "Buffer";
|
||||||
|
$value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedMap {
|
||||||
|
$type: "Map";
|
||||||
|
$value: [EncodedValue, EncodedValue][];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedArray {
|
||||||
|
$type: "Array";
|
||||||
|
$value: EncodedValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedObject {
|
||||||
|
$type: "Object";
|
||||||
|
$value: { [key: string]: EncodedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
type SerializedValue = EncodedNull | EncodedUndefined | EncodedPrimitive | EncodedBuffer | EncodedMap | EncodedArray | EncodedObject;
|
||||||
|
|
||||||
|
function rpc_encode<T>(value: T): SerializedValue {
|
||||||
|
if (value === null) return { $type: "null" };
|
||||||
|
if (value === undefined) return { $type: "undefined" };
|
||||||
|
|
||||||
|
if (typeof value === "number") return { $type: "number", $value: value };
|
||||||
|
if (typeof value === "string") return { $type: "string", $value: value };
|
||||||
|
if (typeof value === "boolean") return { $type: "boolean", $value: value };
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
||||||
|
// Buffer和Uint8Array都转成base64字符串
|
||||||
|
let base64: string = Buffer.from(value).toString("base64");
|
||||||
|
return { $type: "Buffer", $value: base64 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Map) {
|
||||||
|
let arr: [SerializedValue, SerializedValue][] = [];
|
||||||
|
for (let [k, v] of value.entries()) {
|
||||||
|
arr.push([rpc_encode(k), rpc_encode(v)]);
|
||||||
|
}
|
||||||
|
return { $type: "Map", $value: arr };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value) || (typeof value === "object" && value !== null && typeof (value as unknown as ArrayLike<unknown>).length === "number")) {
|
||||||
|
// ArrayLike也认为是Array
|
||||||
|
let arr: SerializedValue[] = [];
|
||||||
|
const arrayLike = value as unknown as ArrayLike<unknown>;
|
||||||
|
for (let i = 0; i < arrayLike.length; i++) {
|
||||||
|
arr.push(rpc_encode(arrayLike[i]));
|
||||||
|
}
|
||||||
|
return { $type: "Array", $value: arr };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
let obj: { [key: string]: SerializedValue } = {};
|
||||||
|
for (let k in value) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||||
|
obj[k] = rpc_encode((value as Record<string, unknown>)[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { $type: "Object", $value: obj };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unsupported type");
|
||||||
|
}
|
||||||
|
|
||||||
|
function rpc_decode<T = unknown>(obj: EncodedValue): T {
|
||||||
|
if (obj == null || typeof obj !== "object" || !("$type" in obj)) {
|
||||||
|
throw new Error("Invalid encoded object");
|
||||||
|
}
|
||||||
|
switch (obj.$type) {
|
||||||
|
case "null": return null as T;
|
||||||
|
case "undefined": return undefined as T;
|
||||||
|
case "number": return (obj as EncodedPrimitive).$value as T;
|
||||||
|
case "string": return (obj as EncodedPrimitive).$value as T;
|
||||||
|
case "boolean": return (obj as EncodedPrimitive).$value as T;
|
||||||
|
case "Buffer":
|
||||||
|
return Buffer.from((obj as EncodedBuffer).$value, "base64") as T;
|
||||||
|
case "Map":
|
||||||
|
{
|
||||||
|
let map = new Map();
|
||||||
|
for (let [k, v] of (obj as EncodedMap).$value) {
|
||||||
|
map.set(rpc_decode(k), rpc_decode(v));
|
||||||
|
}
|
||||||
|
return map as T;
|
||||||
|
}
|
||||||
|
case "Array":
|
||||||
|
{
|
||||||
|
let arr: unknown[] = [];
|
||||||
|
for (let item of (obj as EncodedArray).$value) {
|
||||||
|
arr.push(rpc_decode(item));
|
||||||
|
}
|
||||||
|
return arr as T;
|
||||||
|
}
|
||||||
|
case "Object":
|
||||||
|
{
|
||||||
|
let out: Record<string, unknown> = {};
|
||||||
|
for (let k in (obj as EncodedObject).$value) {
|
||||||
|
const value = (obj as EncodedObject).$value[k];
|
||||||
|
if (value !== undefined) {
|
||||||
|
out[k] = rpc_decode(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out as T;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown $type: " + obj.$type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { rpc_encode, rpc_decode };
|
||||||
|
export type { SerializedValue };
|
114
src/remote/service.ts
Normal file
114
src/remote/service.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { FuncKeys, NTEventWrapper } from "@/common/event";
|
||||||
|
import { ServiceNamingMapping } from "@/core";
|
||||||
|
|
||||||
|
export type ServiceMethodCommand = {
|
||||||
|
[Service in keyof ServiceNamingMapping]: `${Service}/${FuncKeys<ServiceNamingMapping[Service]>}`
|
||||||
|
}[keyof ServiceNamingMapping];
|
||||||
|
|
||||||
|
const LISTENER_COMMAND_PATTERN = /\/addKernel\w*Listener$/;
|
||||||
|
|
||||||
|
function isListenerCommand(command: ServiceMethodCommand): boolean {
|
||||||
|
return LISTENER_COMMAND_PATTERN.test(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRemoteServiceServer<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T,
|
||||||
|
ntevent: NTEventWrapper,
|
||||||
|
callback: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
|
||||||
|
): ServiceNamingMapping[T] {
|
||||||
|
return new Proxy(() => { }, {
|
||||||
|
get: (_target: any, functionName: string) => {
|
||||||
|
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
|
||||||
|
if (isListenerCommand(command)) {
|
||||||
|
return async (..._args: any[]) => {
|
||||||
|
const listener = new Proxy(new class { }(), {
|
||||||
|
apply: (_target, _thisArg, _arguments) => {
|
||||||
|
return callback(command, ..._arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await (ntevent.callNoListenerEvent as any)(command, listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return async (...args: any[]) => {
|
||||||
|
return await (ntevent.callNoListenerEvent as any)(command, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 避免重复远程注册 多份传输会消耗很大
|
||||||
|
export const listenerCmdRegisted = new Map<ServiceMethodCommand, boolean>();
|
||||||
|
// 已经注册的Listener实例托管
|
||||||
|
export const clientCallback = new Map<string, Array<(...args: any[]) => Promise<any>>>();
|
||||||
|
|
||||||
|
export async function handleServiceServerOnce(
|
||||||
|
command: ServiceMethodCommand,// 服务注册命令
|
||||||
|
recvListener: (command: string, ...args: any[]) => Promise<any>,//listener监听器
|
||||||
|
ntevent: NTEventWrapper,// 事件处理器
|
||||||
|
...args: any[]//实际参数
|
||||||
|
) {
|
||||||
|
if (isListenerCommand(command)) {
|
||||||
|
if (!listenerCmdRegisted.has(command)) {
|
||||||
|
listenerCmdRegisted.set(command, true);
|
||||||
|
return (ntevent.callNoListenerEvent as any)(command, new Proxy(new class { }(), {
|
||||||
|
get: (_target: any, prop: string) => {
|
||||||
|
return async (..._args: any[]) => {
|
||||||
|
let listenerCmd = `${command.split('/')[0]}/${prop}`;
|
||||||
|
recvListener(listenerCmd, ..._args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return await (ntevent.callNoListenerEvent as (command: ServiceMethodCommand, ...args: any[]) => Promise<any>)(command, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRemoteServiceClient<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T,
|
||||||
|
receiverEvent: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
|
||||||
|
) {
|
||||||
|
const object = new Proxy(() => { }, {
|
||||||
|
get: (_target: any, functionName: string) => {
|
||||||
|
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
|
||||||
|
if (isListenerCommand(command)) {
|
||||||
|
return async (listener: Record<string, any>) => {
|
||||||
|
for (const key in listener) {
|
||||||
|
if (typeof listener[key] === 'function') {
|
||||||
|
const listenerCmd = `${command.split('/')[0]}/${key}`;
|
||||||
|
if (!clientCallback.has(listenerCmd)) {
|
||||||
|
clientCallback.set(listenerCmd, [listener[key].bind(listener)]);
|
||||||
|
} else {
|
||||||
|
clientCallback.get(listenerCmd)?.push(listener[key].bind(listener));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await receiverEvent(command);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return async (...args: any[]) => {
|
||||||
|
return await receiverEvent(command, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const receiverListener = async function (command: string, ...args: any[]) {
|
||||||
|
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
|
||||||
|
};
|
||||||
|
return { receiverListener: receiverListener, object: object as ServiceNamingMapping[T] };
|
||||||
|
}
|
||||||
|
export async function receiverServiceListener(
|
||||||
|
command: string,
|
||||||
|
...args: any[]
|
||||||
|
) {
|
||||||
|
if (clientCallback.has(command)) {
|
||||||
|
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearServiceState() {
|
||||||
|
listenerCmdRegisted.clear();
|
||||||
|
clientCallback.clear();
|
||||||
|
}
|
23
src/remote/wrapper.ts
Normal file
23
src/remote/wrapper.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NodeIKernelLoginService, NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, NodeQQNTWrapperUtil, WrapperNodeApi } from "@/core";
|
||||||
|
import { NodeIO3MiscService } from "@/core/services/NodeIO3MiscService";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
export const LocalVirtualWrapper: WrapperNodeApi = {
|
||||||
|
NodeIO3MiscService: {
|
||||||
|
get: () => LocalVirtualWrapper.NodeIO3MiscService,
|
||||||
|
addO3MiscListener: () => 0,
|
||||||
|
setAmgomDataPiece: () => { },
|
||||||
|
reportAmgomWeather: () => { },
|
||||||
|
} as NodeIO3MiscService,
|
||||||
|
NodeQQNTWrapperUtil: {
|
||||||
|
get: () => LocalVirtualWrapper.NodeQQNTWrapperUtil,
|
||||||
|
getNTUserDataInfoConfig: function (): string {
|
||||||
|
let current_path = dirname(fileURLToPath(import.meta.url));
|
||||||
|
return current_path;
|
||||||
|
}
|
||||||
|
} as NodeQQNTWrapperUtil,
|
||||||
|
NodeIQQNTWrapperSession: {} as NodeIQQNTWrapperSession,
|
||||||
|
NodeIQQNTWrapperEngine: {} as NodeIQQNTWrapperEngine,
|
||||||
|
NodeIKernelLoginService: {} as NodeIKernelLoginService,
|
||||||
|
};
|
@@ -1,79 +0,0 @@
|
|||||||
import { current_path } from "@/plugin/data";
|
|
||||||
import { createCanvas, loadImage } from "@napi-rs/canvas";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
export async function drawJsonContent(jsonContent: string) {
|
|
||||||
const lines = jsonContent.split('\n');
|
|
||||||
|
|
||||||
const padding = 40;
|
|
||||||
const lineHeight = 30;
|
|
||||||
const canvas = createCanvas(1, 1);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const chineseRegex = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/;
|
|
||||||
let maxLineWidth = 0;
|
|
||||||
for (const line of lines) {
|
|
||||||
let lineWidth = 0;
|
|
||||||
for (const char of line) {
|
|
||||||
const isChinese = chineseRegex.test(char);
|
|
||||||
ctx.font = isChinese ? '20px "Aa偷吃可爱长大的"' : '20px "JetBrains Mono"';
|
|
||||||
lineWidth += ctx.measureText(char).width;
|
|
||||||
}
|
|
||||||
if (lineWidth > maxLineWidth) {
|
|
||||||
maxLineWidth = lineWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = maxLineWidth + padding * 2;
|
|
||||||
const height = lines.length * lineHeight + padding * 2;
|
|
||||||
|
|
||||||
const finalCanvas = createCanvas(width, height);
|
|
||||||
const finalCtx = finalCanvas.getContext('2d');
|
|
||||||
|
|
||||||
const backgroundImage = await loadImage(path.join(current_path,'./fonts/post.jpg'));
|
|
||||||
const pattern = finalCtx.createPattern(backgroundImage, 'repeat');
|
|
||||||
finalCtx.fillStyle = pattern;
|
|
||||||
finalCtx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
finalCtx.filter = 'blur(5px)';
|
|
||||||
finalCtx.drawImage(finalCanvas, 0, 0);
|
|
||||||
|
|
||||||
finalCtx.filter = 'none';
|
|
||||||
const cardWidth = width - padding;
|
|
||||||
const cardHeight = height - padding;
|
|
||||||
const cardX = padding / 2;
|
|
||||||
const cardY = padding / 2;
|
|
||||||
const radius = 20;
|
|
||||||
|
|
||||||
finalCtx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
||||||
finalCtx.beginPath();
|
|
||||||
finalCtx.moveTo(cardX + radius, cardY);
|
|
||||||
finalCtx.lineTo(cardX + cardWidth - radius, cardY);
|
|
||||||
finalCtx.quadraticCurveTo(cardX + cardWidth, cardY, cardX + cardWidth, cardY + radius);
|
|
||||||
finalCtx.lineTo(cardX + cardWidth, cardY + cardHeight - radius);
|
|
||||||
finalCtx.quadraticCurveTo(cardX + cardWidth, cardY + cardHeight, cardX + cardWidth - radius, cardY + cardHeight);
|
|
||||||
finalCtx.lineTo(cardX + radius, cardY + cardHeight);
|
|
||||||
finalCtx.quadraticCurveTo(cardX, cardY + cardHeight, cardX, cardY + cardHeight - radius);
|
|
||||||
finalCtx.lineTo(cardX, cardY + radius);
|
|
||||||
finalCtx.quadraticCurveTo(cardX, cardY, cardX + radius, cardY);
|
|
||||||
finalCtx.closePath();
|
|
||||||
finalCtx.fill();
|
|
||||||
|
|
||||||
// 绘制 JSON 内容
|
|
||||||
finalCtx.fillStyle = 'black';
|
|
||||||
let textY = cardY + 40;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
let x = cardX + 20;
|
|
||||||
for (const char of line) {
|
|
||||||
const isChinese = /[\u4e00-\u9fa5]/.test(char);
|
|
||||||
finalCtx.font = isChinese ? '20px "Aa偷吃可爱长大的"' : '20px "JetBrains Mono"';
|
|
||||||
finalCtx.fillText(char, x, textY);
|
|
||||||
x += finalCtx.measureText(char).width;
|
|
||||||
}
|
|
||||||
textY += 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存图像
|
|
||||||
const buffer = finalCanvas.toBuffer('image/png');
|
|
||||||
return "base64://" + buffer.toString('base64');
|
|
||||||
}
|
|
@@ -1,10 +1,2 @@
|
|||||||
|
|
||||||
import { NCoreInitShell } from './base';
|
import { NCoreInitShell } from './base';
|
||||||
import { GlobalFonts } from '@napi-rs/canvas';
|
|
||||||
import { current_path } from '@/plugin/data';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
GlobalFonts.registerFromPath(path.join(current_path, './fonts/JetBrainsMono.ttf'), 'JetBrains Mono');
|
|
||||||
GlobalFonts.registerFromPath(path.join(current_path, './fonts/AaCute.ttf'), 'Aa偷吃可爱长大的');
|
|
||||||
console.log('字体注册完成');
|
|
||||||
NCoreInitShell();
|
NCoreInitShell();
|
@@ -1,6 +1,7 @@
|
|||||||
import { LogWrapper } from '@/common/log';
|
import { LogWrapper } from '@/common/log';
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import * as process from 'process';
|
import * as process from 'process';
|
||||||
|
import { Writable } from 'stream';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接到命名管道并重定向stdout
|
* 连接到命名管道并重定向stdout
|
||||||
@@ -11,7 +12,6 @@ import * as process from 'process';
|
|||||||
export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000): Promise<{ disconnect: () => void }> {
|
export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000): Promise<{ disconnect: () => void }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
logger.log('只有Windows平台支持命名管道');
|
|
||||||
// 非Windows平台不reject,而是返回一个空的disconnect函数
|
// 非Windows平台不reject,而是返回一个空的disconnect函数
|
||||||
return resolve({ disconnect: () => { } });
|
return resolve({ disconnect: () => { } });
|
||||||
}
|
}
|
||||||
@@ -25,12 +25,50 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||||
const pipeSocket = net.connect(pipePath, () => {
|
const pipeSocket = net.connect(pipePath, () => {
|
||||||
// 清除超时
|
// 清除超时
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// 优化网络性能设置
|
||||||
|
pipeSocket.setNoDelay(true); // 减少延迟
|
||||||
|
|
||||||
|
// 设置更高的高水位线,允许更多数据缓冲
|
||||||
|
|
||||||
logger.log(`[StdOut] 已重定向到命名管道: ${pipePath}`);
|
logger.log(`[StdOut] 已重定向到命名管道: ${pipePath}`);
|
||||||
|
|
||||||
|
// 创建拥有更优雅背压处理的 Writable 流
|
||||||
|
const pipeWritable = new Writable({
|
||||||
|
highWaterMark: 1024 * 64, // 64KB 高水位线
|
||||||
|
write(chunk, encoding, callback) {
|
||||||
|
if (!pipeSocket.writable) {
|
||||||
|
// 如果管道不可写,退回到原始stdout
|
||||||
|
logger.log('[StdOut] 管道不可写,回退到控制台输出');
|
||||||
|
return originalStdoutWrite(chunk, encoding, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试写入数据到管道
|
||||||
|
const canContinue = pipeSocket.write(chunk, encoding, () => {
|
||||||
|
// 数据已被发送或放入内部缓冲区
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canContinue) {
|
||||||
|
// 如果返回true,表示可以继续写入更多数据
|
||||||
|
// 立即通知写入流可以继续
|
||||||
|
process.nextTick(callback);
|
||||||
|
} else {
|
||||||
|
// 如果返回false,表示内部缓冲区已满
|
||||||
|
// 等待drain事件再恢复写入
|
||||||
|
pipeSocket.once('drain', () => {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 明确返回true,表示写入已处理
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重定向stdout
|
||||||
process.stdout.write = (
|
process.stdout.write = (
|
||||||
chunk: any,
|
chunk: any,
|
||||||
encoding?: BufferEncoding | (() => void),
|
encoding?: BufferEncoding | (() => void),
|
||||||
@@ -40,8 +78,11 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
cb = encoding;
|
cb = encoding;
|
||||||
encoding = undefined;
|
encoding = undefined;
|
||||||
}
|
}
|
||||||
return pipeSocket.write(chunk, encoding as BufferEncoding, cb);
|
|
||||||
|
// 使用优化的writable流处理写入
|
||||||
|
return pipeWritable.write(chunk, encoding as BufferEncoding, cb as () => void);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提供断开连接的方法
|
// 提供断开连接的方法
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
process.stdout.write = originalStdoutWrite;
|
process.stdout.write = originalStdoutWrite;
|
||||||
@@ -53,6 +94,7 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
resolve({ disconnect });
|
resolve({ disconnect });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 管道错误处理
|
||||||
pipeSocket.on('error', (err) => {
|
pipeSocket.on('error', (err) => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
process.stdout.write = originalStdoutWrite;
|
process.stdout.write = originalStdoutWrite;
|
||||||
@@ -60,11 +102,18 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 管道关闭处理
|
||||||
pipeSocket.on('end', () => {
|
pipeSocket.on('end', () => {
|
||||||
process.stdout.write = originalStdoutWrite;
|
process.stdout.write = originalStdoutWrite;
|
||||||
logger.log('命名管道连接已关闭');
|
logger.log('命名管道连接已关闭');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 确保在连接意外关闭时恢复stdout
|
||||||
|
pipeSocket.on('close', () => {
|
||||||
|
process.stdout.write = originalStdoutWrite;
|
||||||
|
logger.log('命名管道连接已关闭');
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
logger.log(`尝试连接命名管道 ${pipePath} 时发生异常:`, error);
|
logger.log(`尝试连接命名管道 ${pipePath} 时发生异常:`, error);
|
||||||
|
@@ -122,7 +122,9 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
|
|
||||||
// ------------挂载路由------------
|
// ------------挂载路由------------
|
||||||
// 挂载静态路由(前端),路径为 /webui
|
// 挂载静态路由(前端),路径为 /webui
|
||||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
app.use('/webui', express.static(pathWrapper.staticPath, {
|
||||||
|
maxAge: '1d'
|
||||||
|
}));
|
||||||
// 初始化WebSocket服务器
|
// 初始化WebSocket服务器
|
||||||
const sslCerts = await checkCertificates(logger);
|
const sslCerts = await checkCertificates(logger);
|
||||||
const isHttps = !!sslCerts;
|
const isHttps = !!sslCerts;
|
||||||
|
@@ -47,6 +47,9 @@ export const CreateTerminalHandler: RequestHandler = async (req, res) => {
|
|||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
return sendError(res, 'MacOS不支持终端');
|
return sendError(res, 'MacOS不支持终端');
|
||||||
}
|
}
|
||||||
|
if ((await WebUiConfig.GetWebUIConfig()).token === 'napcat') {
|
||||||
|
return sendError(res, '默认密码禁止创建终端');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const { cols, rows } = req.body;
|
const { cols, rows } = req.body;
|
||||||
const { id } = terminalManager.createTerminal(cols, rows);
|
const { id } = terminalManager.createTerminal(cols, rows);
|
||||||
|
@@ -9,7 +9,7 @@ Object.defineProperty(global, '__dirname', {
|
|||||||
// 注意:堆栈格式可能不同,请根据实际环境调整索引及正则表达式
|
// 注意:堆栈格式可能不同,请根据实际环境调整索引及正则表达式
|
||||||
for (const line of stack) {
|
for (const line of stack) {
|
||||||
const match = line.match(/\((.*):\d+:\d+\)/);
|
const match = line.match(/\((.*):\d+:\d+\)/);
|
||||||
if (match) {
|
if (match?.[1]) {
|
||||||
callerFile = match[1];
|
callerFile = match[1];
|
||||||
if (!callerFile.includes('init-dynamic-dirname.ts')) {
|
if (!callerFile.includes('init-dynamic-dirname.ts')) {
|
||||||
break;
|
break;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user