mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-28 15:50:27 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f00c0594 | ||
|
|
77c8f874b6 | ||
|
|
fb0a20919b | ||
|
|
0300ba4648 | ||
|
|
d472eee777 | ||
|
|
41bd06e50a | ||
|
|
97334dfbf5 | ||
|
|
e3d8c8e940 | ||
|
|
f2c62db76e | ||
|
|
b1b051c4ce | ||
|
|
a754b2ecc7 | ||
|
|
e0eb625b75 | ||
|
|
937be7678e | ||
|
|
9b88946209 | ||
|
|
74de3d9100 |
12
README.md
12
README.md
@@ -13,6 +13,15 @@ _Modern protocol-side framework implemented based on NTQQ._
|
||||
|
||||
---
|
||||
|
||||
## New Feature
|
||||
在 v4.8.115+ 版本开始
|
||||
|
||||
1. NapCatQQ 支持 [Stream Api](https://napneko.github.io/develop/file)
|
||||
2. NapCatQQ 推荐 message_id/user_id/group_id 均使用字符串类型
|
||||
|
||||
- [1] 解决 Docker/跨设备/大文件 的多媒体上下传问题
|
||||
- [2] 采用字符串可以解决扩展到int64的问题,同时也可以解决部分语言(如JavaScript)对大整数支持不佳的问题,增加极少成本。
|
||||
|
||||
## Welcome
|
||||
+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
|
||||
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
@@ -48,6 +57,9 @@ _Modern protocol-side framework implemented based on NTQQ._
|
||||
| Telegram | [](https://t.me/napcatqq) |
|
||||
|:-:|:-:|
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
|
||||
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论,如有建议到达官方交流群讨论或PR。
|
||||
|
||||
## Thanks
|
||||
|
||||
@@ -7,7 +7,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
|
||||
@@ -7,7 +7,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
|
||||
@@ -16,7 +16,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
|
||||
if not exist "%QQPath%" (
|
||||
echo provided QQ path is invalid
|
||||
|
||||
@@ -16,7 +16,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
|
||||
if not exist "%QQPath%" (
|
||||
echo provided QQ path is invalid
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.8.114",
|
||||
"version": "4.8.117",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
@@ -82,6 +82,7 @@ export default function FileTable({
|
||||
setPreviewImages([])
|
||||
setPreviewIndex(0)
|
||||
setShowImage(false)
|
||||
setPage(1)
|
||||
}, [currentPath])
|
||||
|
||||
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
||||
|
||||
@@ -171,7 +171,8 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
||||
|
||||
export default GenericForm
|
||||
export function random_token(length: number) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?'
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill'
|
||||
|
||||
import { LogLevel } from '@/const/enum'
|
||||
|
||||
import { serverRequest } from '@/utils/request'
|
||||
import CryptoJS from "crypto-js";
|
||||
|
||||
export interface Log {
|
||||
level: LogLevel
|
||||
message: string
|
||||
@@ -17,7 +18,7 @@ export default class WebUIManager {
|
||||
}
|
||||
|
||||
public static async loginWithToken(token: string) {
|
||||
const sha256 = CryptoJS.SHA256(token + '.napcat').toString();
|
||||
const sha256 = CryptoJS.SHA256(token + '.napcat').toString()
|
||||
const { data } = await serverRequest.post<ServerResponse<AuthResponse>>(
|
||||
'/auth/login',
|
||||
{ hash: sha256 }
|
||||
|
||||
@@ -182,4 +182,4 @@ const ServerConfigCard = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerConfigCard
|
||||
export default ServerConfigCard
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.8.114",
|
||||
"version": "4.8.117",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.8.114';
|
||||
export const napCatVersion = '4.8.117';
|
||||
|
||||
4
src/core/external/appid.json
vendored
4
src/core/external/appid.json
vendored
@@ -386,5 +386,9 @@
|
||||
"9.9.21-39038": {
|
||||
"appid": 537313906,
|
||||
"qua": "V1_WIN_NQ_9.9.21_39038_GW_B"
|
||||
},
|
||||
"9.9.22-40362": {
|
||||
"appid": 537314212,
|
||||
"qua": "V1_WIN_NQ_9.9.22_40362_GW_B"
|
||||
}
|
||||
}
|
||||
6
src/core/external/offset.json
vendored
6
src/core/external/offset.json
vendored
@@ -507,8 +507,12 @@
|
||||
"send": "7B025C8",
|
||||
"recv": "7B05F58"
|
||||
},
|
||||
"9.9.21-39038-x64": {
|
||||
"9.9.21-39038-x64": {
|
||||
"send": "313FB58",
|
||||
"recv": "31432FC"
|
||||
},
|
||||
"9.9.22-40362": {
|
||||
"send": "31C0EB8",
|
||||
"recv": "31C465C"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import fs from 'fs';
|
||||
import { join as joinPath } from 'node:path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createHash } from 'crypto';
|
||||
import { unlink } from 'node:fs';
|
||||
|
||||
// 简化配置
|
||||
const CONFIG = {
|
||||
@@ -25,7 +26,8 @@ const SchemaData = Type.Object({
|
||||
is_complete: Type.Optional(Type.Boolean()),
|
||||
filename: Type.Optional(Type.String()),
|
||||
reset: Type.Optional(Type.Boolean()),
|
||||
verify_only: Type.Optional(Type.Boolean())
|
||||
verify_only: Type.Optional(Type.Boolean()),
|
||||
file_retention: Type.Number({ default: 5 * 60 * 1000 }) // 默认5分钟 回收 不设置或0为不回收
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
@@ -47,6 +49,7 @@ interface StreamState {
|
||||
memoryChunks?: Map<number, Buffer>;
|
||||
tempDir?: string;
|
||||
finalPath?: string;
|
||||
fileRetention?: number;
|
||||
|
||||
// 管理
|
||||
createdAt: number;
|
||||
@@ -131,9 +134,9 @@ export class UploadFileStream extends OneBotAction<Payload, StreamPacket<StreamR
|
||||
expectedSha256: expected_sha256,
|
||||
useMemory,
|
||||
createdAt: Date.now(),
|
||||
timeoutId: this.setupTimeout(stream_id)
|
||||
timeoutId: this.setupTimeout(stream_id),
|
||||
fileRetention: payload.file_retention
|
||||
};
|
||||
|
||||
try {
|
||||
if (useMemory) {
|
||||
stream.memoryChunks = new Map();
|
||||
@@ -244,7 +247,13 @@ export class UploadFileStream extends OneBotAction<Payload, StreamPacket<StreamR
|
||||
|
||||
// 清理资源但保留文件
|
||||
this.cleanupStream(stream.id, false);
|
||||
|
||||
if (stream.fileRetention && stream.fileRetention > 0) {
|
||||
setTimeout(() => {
|
||||
unlink(finalPath, err => {
|
||||
if (err) this.core.context.logger.logError(`Failed to delete retained file ${finalPath}:`, err);
|
||||
});
|
||||
}, stream.fileRetention);
|
||||
}
|
||||
return {
|
||||
type: StreamStatus.Response,
|
||||
stream_id: stream.id,
|
||||
|
||||
@@ -29,7 +29,7 @@ class OneBotUploadTester:
|
||||
headers["Authorization"] = f"Bearer {self.access_token}"
|
||||
|
||||
print(f"连接到 {self.ws_url}")
|
||||
self.websocket = await websockets.connect(self.ws_url, extra_headers=headers)
|
||||
self.websocket = await websockets.connect(self.ws_url, additional_headers=headers)
|
||||
print("WebSocket 连接成功")
|
||||
|
||||
async def disconnect(self):
|
||||
@@ -38,7 +38,7 @@ class OneBotUploadTester:
|
||||
await self.websocket.close()
|
||||
print("WebSocket 连接已断开")
|
||||
|
||||
def calculate_file_chunks(self, file_path: str, chunk_size: int = 64 * 1024) -> tuple[List[bytes], str, int]:
|
||||
def calculate_file_chunks(self, file_path: str, chunk_size: int = 64) -> tuple[List[bytes], str, int]:
|
||||
"""
|
||||
计算文件分片和 SHA256
|
||||
|
||||
@@ -97,7 +97,7 @@ class OneBotUploadTester:
|
||||
print(f"收到其他消息: {data}")
|
||||
continue
|
||||
|
||||
async def upload_file_stream_batch(self, file_path: str, chunk_size: int = 64 * 1024) -> str:
|
||||
async def upload_file_stream_batch(self, file_path: str, chunk_size: int = 64 ) -> str:
|
||||
"""
|
||||
一次性批量上传文件流
|
||||
|
||||
@@ -134,7 +134,8 @@ class OneBotUploadTester:
|
||||
"total_chunks": total_chunks,
|
||||
"file_size": total_size,
|
||||
"expected_sha256": sha256_hash,
|
||||
"filename": file_path.name
|
||||
"filename": file_path.name,
|
||||
"file_retention": 30 * 1000
|
||||
}
|
||||
|
||||
# 发送分片
|
||||
@@ -171,7 +172,7 @@ class OneBotUploadTester:
|
||||
else:
|
||||
raise Exception(f"文件状态异常: {result}")
|
||||
|
||||
async def test_upload(self, file_path: str, chunk_size: int = 64 * 1024):
|
||||
async def test_upload(self, file_path: str, chunk_size: int = 64 ):
|
||||
"""测试文件上传"""
|
||||
try:
|
||||
await self.connect()
|
||||
|
||||
@@ -578,7 +578,7 @@ export class OneBotMsgApi {
|
||||
};
|
||||
}
|
||||
|
||||
if (!context.peer || context.peer.chatType == ChatType.KCHATTYPEC2C) return undefined;
|
||||
if (!context.peer || !atQQ || context.peer.chatType == ChatType.KCHATTYPEC2C) return undefined; // 过滤掉空atQQ
|
||||
if (atQQ === 'all') return at(atQQ, atQQ, NTMsgAtType.ATTYPEALL, '全体成员');
|
||||
const atMember = await this.core.apis.GroupApi.getGroupMember(context.peer.peerUid, atQQ);
|
||||
if (atMember) {
|
||||
|
||||
Reference in New Issue
Block a user