Add streaming file upload and download actions

Introduces new OneBot actions for streaming file upload and download, including chunked file transfer with memory/disk management and SHA256 verification. Adds CleanStreamTempFile, DownloadFileStream, UploadFileStream, and TestStreamDownload actions, updates action routing and network adapters to support streaming via HTTP and WebSocket, and provides Python test scripts for concurrent upload testing.
This commit is contained in:
手瓜一十雪
2025-09-16 23:24:00 +08:00
parent 66f30e1ebf
commit 890d032794
14 changed files with 1163 additions and 17 deletions

View File

@@ -9,7 +9,7 @@ export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
if (req.path === '/_events') {
this.createSseSupport(req, res);
} else {
super.httpApiRequest(req, res);
super.httpApiRequest(req, res, true);
}
}

View File

@@ -104,7 +104,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
}
}
async httpApiRequest(req: Request, res: Response) {
async httpApiRequest(req: Request, res: Response, request_sse: boolean = false) {
let payload = req.body;
if (req.method == 'get') {
payload = req.query;
@@ -117,17 +117,31 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
return res.json(hello);
}
const actionName = req.path.split('/')[1];
const payload_echo = payload['echo'];
const real_echo = payload_echo ?? Math.random().toString(36).substring(2, 15);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const action = this.actions.get(actionName as any);
if (action) {
try {
const result = await action.handle(payload, this.name, this.config);
let stream = false;
const result = await action.handle(payload, this.name, this.config, {
send: request_sse ? async (data: object) => {
this.onEvent({ ...OB11Response.ok(data, real_echo), type: 'sse-action' } as unknown as OB11EmitEventContent);
} : async (data: object) => {
stream = true;
res.write(JSON.stringify({ ...OB11Response.ok(data, real_echo), type: 'stream-action' }) + "\r\n\r\n");
}
}, real_echo);
if (stream) {
res.write(JSON.stringify({ ...result, type: 'stream-action' }) + "\r\n\r\n");
return res.end();
};
return res.json(result);
} catch (error: unknown) {
return res.json(OB11Response.error((error as Error)?.stack?.toString() || (error as Error)?.message || 'Error Handle', 200));
return res.json(OB11Response.error((error as Error)?.stack?.toString() || (error as Error)?.message || 'Error Handle', 200, real_echo));
}
} else {
return res.json(OB11Response.error('不支持的Api ' + actionName, 200));
return res.json(OB11Response.error('不支持的Api ' + actionName, 200, real_echo));
}
}

View File

@@ -151,7 +151,11 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
this.checkStateAndReply<unknown>(OB11Response.error('不支持的Api ' + receiveData.action, 1404, echo));
return;
}
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config);
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config, {
send: async (data: object) => {
this.checkStateAndReply<unknown>({ ...OB11Response.ok(data, echo ?? '') });
}
});
this.checkStateAndReply<unknown>({ ...retdata });
}
async reload(newConfig: WebsocketClientConfig) {

View File

@@ -186,7 +186,11 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
this.checkStateAndReply<unknown>(OB11Response.error('不支持的API ' + receiveData.action, 1404, echo), wsClient);
return;
}
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config);
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config, {
send: async (data: object) => {
this.checkStateAndReply<unknown>({ ...OB11Response.ok(data, echo ?? '') }, wsClient);
}
});
this.checkStateAndReply<unknown>({ ...retdata }, wsClient);
}