mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-02 00:30:25 +00:00
Compare commits
10 Commits
v4.8.115
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e431471c4c | ||
|
|
e6c694c2b7 | ||
|
|
65bd37fdb5 | ||
|
|
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
|
## Welcome
|
||||||
+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
|
+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
|
||||||
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||||
@@ -48,6 +57,9 @@ _Modern protocol-side framework implemented based on NTQQ._
|
|||||||
| Telegram | [](https://t.me/napcatqq) |
|
| Telegram | [](https://t.me/napcatqq) |
|
||||||
|:-:|:-:|
|
|:-:|:-:|
|
||||||
|
|
||||||
|
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||||
|
|:-:|:-:|
|
||||||
|
|
||||||
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论,如有建议到达官方交流群讨论或PR。
|
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论,如有建议到达官方交流群讨论或PR。
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.8.114",
|
"version": "4.8.116",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.8.114",
|
"version": "4.8.116",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '4.8.114';
|
export const napCatVersion = '4.8.116';
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ export class MoveGroupFile extends GetPacketStatusDepends<Payload, MoveGroupFile
|
|||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||||
if (contextMsgFile?.fileUUID) {
|
const fileUUID = contextMsgFile?.fileUUID || contextMsgFile?.fileId;
|
||||||
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);
|
if (fileUUID) {
|
||||||
|
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, fileUUID, payload.current_parent_directory, payload.target_parent_directory);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ export class RenameGroupFile extends GetPacketStatusDepends<Payload, RenameGroup
|
|||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||||
if (contextMsgFile?.fileUUID) {
|
const fileUUID = contextMsgFile?.fileUUID || contextMsgFile?.fileId;
|
||||||
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);
|
if (fileUUID) {
|
||||||
|
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, fileUUID, payload.current_parent_directory, payload.new_name);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ export class TransGroupFile extends GetPacketStatusDepends<Payload, TransGroupFi
|
|||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||||
if (contextMsgFile?.fileUUID) {
|
const fileUUID = contextMsgFile?.fileUUID || contextMsgFile?.fileId;
|
||||||
const result = await this.core.apis.GroupApi.transGroupFile(payload.group_id.toString(), contextMsgFile.fileUUID);
|
if (fileUUID) {
|
||||||
|
const result = await this.core.apis.GroupApi.transGroupFile(payload.group_id.toString(), fileUUID);
|
||||||
if (result.transGroupFileResult.result.retCode === 0) {
|
if (result.transGroupFileResult.result.retCode === 0) {
|
||||||
return {
|
return {
|
||||||
ok: true
|
ok: true
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ export class GetGroupFileUrl extends GetPacketStatusDepends<Payload, GetGroupFil
|
|||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||||
if (contextMsgFile?.fileUUID) {
|
const fileUUID = contextMsgFile?.fileUUID || contextMsgFile?.fileId;
|
||||||
|
if (fileUUID) {
|
||||||
return {
|
return {
|
||||||
url: await this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+payload.group_id, contextMsgFile.fileUUID)
|
url: await this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+payload.group_id, fileUUID)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error('real fileUUID not found!');
|
throw new Error('real fileUUID not found!');
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class DeleteGroupFile extends OneBotAction<Payload, Awaited<ReturnType<NT
|
|||||||
override actionName = ActionName.GOCQHTTP_DeleteGroupFile;
|
override actionName = ActionName.GOCQHTTP_DeleteGroupFile;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = SchemaData;
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
const data = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||||
if (!data || !data.fileId) throw new Error('Invalid file_id');
|
if (!data || !data.fileId) throw new Error('Invalid file_id');
|
||||||
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
|
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import fs from 'fs';
|
|||||||
import { join as joinPath } from 'node:path';
|
import { join as joinPath } from 'node:path';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
import { unlink } from 'node:fs';
|
||||||
|
|
||||||
// 简化配置
|
// 简化配置
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
@@ -25,7 +26,8 @@ const SchemaData = Type.Object({
|
|||||||
is_complete: Type.Optional(Type.Boolean()),
|
is_complete: Type.Optional(Type.Boolean()),
|
||||||
filename: Type.Optional(Type.String()),
|
filename: Type.Optional(Type.String()),
|
||||||
reset: Type.Optional(Type.Boolean()),
|
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>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
@@ -47,6 +49,7 @@ interface StreamState {
|
|||||||
memoryChunks?: Map<number, Buffer>;
|
memoryChunks?: Map<number, Buffer>;
|
||||||
tempDir?: string;
|
tempDir?: string;
|
||||||
finalPath?: string;
|
finalPath?: string;
|
||||||
|
fileRetention?: number;
|
||||||
|
|
||||||
// 管理
|
// 管理
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
@@ -131,9 +134,9 @@ export class UploadFileStream extends OneBotAction<Payload, StreamPacket<StreamR
|
|||||||
expectedSha256: expected_sha256,
|
expectedSha256: expected_sha256,
|
||||||
useMemory,
|
useMemory,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
timeoutId: this.setupTimeout(stream_id)
|
timeoutId: this.setupTimeout(stream_id),
|
||||||
|
fileRetention: payload.file_retention
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (useMemory) {
|
if (useMemory) {
|
||||||
stream.memoryChunks = new Map();
|
stream.memoryChunks = new Map();
|
||||||
@@ -244,7 +247,13 @@ export class UploadFileStream extends OneBotAction<Payload, StreamPacket<StreamR
|
|||||||
|
|
||||||
// 清理资源但保留文件
|
// 清理资源但保留文件
|
||||||
this.cleanupStream(stream.id, false);
|
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 {
|
return {
|
||||||
type: StreamStatus.Response,
|
type: StreamStatus.Response,
|
||||||
stream_id: stream.id,
|
stream_id: stream.id,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class OneBotUploadTester:
|
|||||||
headers["Authorization"] = f"Bearer {self.access_token}"
|
headers["Authorization"] = f"Bearer {self.access_token}"
|
||||||
|
|
||||||
print(f"连接到 {self.ws_url}")
|
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 连接成功")
|
print("WebSocket 连接成功")
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
@@ -38,7 +38,7 @@ class OneBotUploadTester:
|
|||||||
await self.websocket.close()
|
await self.websocket.close()
|
||||||
print("WebSocket 连接已断开")
|
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
|
计算文件分片和 SHA256
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ class OneBotUploadTester:
|
|||||||
print(f"收到其他消息: {data}")
|
print(f"收到其他消息: {data}")
|
||||||
continue
|
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,
|
"total_chunks": total_chunks,
|
||||||
"file_size": total_size,
|
"file_size": total_size,
|
||||||
"expected_sha256": sha256_hash,
|
"expected_sha256": sha256_hash,
|
||||||
"filename": file_path.name
|
"filename": file_path.name,
|
||||||
|
"file_retention": 30 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
# 发送分片
|
# 发送分片
|
||||||
@@ -171,7 +172,7 @@ class OneBotUploadTester:
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"文件状态异常: {result}")
|
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:
|
try:
|
||||||
await self.connect()
|
await self.connect()
|
||||||
|
|||||||
Reference in New Issue
Block a user