Skip to content

Commit ae635a6

Browse files
authored
Merge pull request #1 from juzibot/feat/videosupport
谷歌视频上传代理
2 parents 62c9269 + e3ac18b commit ae635a6

File tree

3 files changed

+238
-35
lines changed

3 files changed

+238
-35
lines changed

src/google/google-proxy.controller.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import {
22
Body,
33
Controller,
4+
Get,
45
Headers,
56
HttpCode,
67
HttpException,
78
Inject,
9+
Param,
810
Post,
911
Query,
10-
Request,
12+
Req,
13+
Res,
1114
StreamableFile,
1215
} from '@nestjs/common';
13-
import { Request as ExpressRequest } from 'express';
16+
import { Request, Response } from 'express';
1417
import { GoogleProxyService } from './google-proxy.service';
1518

1619
@Controller('google')
@@ -24,9 +27,9 @@ export class GoogleProxyController {
2427
@Body() body: any,
2528
@Headers() headers: any,
2629
@Query() query: any,
27-
@Request() req: ExpressRequest,
30+
@Req() req: Request,
2831
) {
29-
const params = req.params;
32+
const params = req.params as any;
3033
const reqParams = params.reqParams;
3134
const [model, method] = reqParams.split(':');
3235

@@ -50,4 +53,65 @@ export class GoogleProxyController {
5053
throw new HttpException('Method not found', 404);
5154
}
5255
}
56+
57+
@Post('upload/v1beta/files')
58+
async uploadFileInit(
59+
@Query() query: any,
60+
@Body() body: any,
61+
@Headers() headers: any,
62+
@Res({ passthrough: true }) res: Response,
63+
@Req() req: any,
64+
) {
65+
if (query.upload_id) {
66+
const queryString = new URLSearchParams(query).toString();
67+
const uploadUrl = `https://generativelanguage.googleapis.com/upload/v1beta/files?${queryString}`;
68+
const bufferBody = req.body;
69+
return this.service.uploadFileData(uploadUrl, bufferBody, headers);
70+
}
71+
const result = await this.service.uploadFileInit(body, headers);
72+
res.status(result.status);
73+
Object.keys(result.headers).forEach(key => {
74+
res.setHeader(key, result.headers[key]);
75+
});
76+
return result.data;
77+
}
78+
79+
@Post('upload/v1beta/files/:path(*)')
80+
async uploadFileData(
81+
@Param('path') path: string,
82+
@Body() body: Buffer,
83+
@Headers() headers: any,
84+
@Query() query: any,
85+
) {
86+
const uploadUrl = `https://generativelanguage.googleapis.com/upload/v1beta/files/${path}`;
87+
const chunkIndex = query.chunk_index ? parseInt(query.chunk_index) : 0;
88+
const totalChunks = query.total_chunks ? parseInt(query.total_chunks) : 1;
89+
if (totalChunks > 1) {
90+
return this.service.uploadFileChunk(uploadUrl, body, headers, chunkIndex, totalChunks);
91+
} else {
92+
return this.service.uploadFileData(uploadUrl, body, headers);
93+
}
94+
}
95+
96+
@Post('upload/v1beta/files/chunk/:path(*)')
97+
async uploadFileChunk(
98+
@Param('path') path: string,
99+
@Body() body: Buffer,
100+
@Headers() headers: any,
101+
@Query() query: any,
102+
) {
103+
const uploadUrl = `https://generativelanguage.googleapis.com/upload/v1beta/files/${path}`;
104+
const chunkIndex = parseInt(query.chunk_index || '0');
105+
const totalChunks = parseInt(query.total_chunks || '1');
106+
return this.service.uploadFileChunk(uploadUrl, body, headers, chunkIndex, totalChunks);
107+
}
108+
109+
@Get('v1beta/:path(*)')
110+
async getFile(
111+
@Param('path') path: string,
112+
@Headers() headers: any,
113+
) {
114+
const url = `https://generativelanguage.googleapis.com/v1beta/${path}`;
115+
return this.service.getFileInfo(url, headers);
116+
}
53117
}

src/google/google-proxy.service.ts

Lines changed: 159 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export class GoogleProxyService {
1313

1414
async generateContent(body: any, headers: any, query: any, model: string) {
1515
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
16-
1716
return this.makeRequest(url, headers, body, query, false);
1817
}
1918

@@ -24,55 +23,188 @@ export class GoogleProxyService {
2423
model: string,
2524
) {
2625
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent`;
27-
2826
return this.makeRequest(url, headers, body, query, true);
2927
}
3028

29+
async uploadFileInit(body: any, headers: any): Promise<{ status: number; headers: any; data: any }> {
30+
const url = 'https://generativelanguage.googleapis.com/upload/v1beta/files';
31+
const result = await this.makeRequest(url, headers, body, {}, false, {
32+
customHeaders: {
33+
'X-Goog-Upload-Protocol': headers['x-goog-upload-protocol'],
34+
'X-Goog-Upload-Command': headers['x-goog-upload-command'],
35+
'X-Goog-Upload-Header-Content-Length': headers['x-goog-upload-header-content-length'],
36+
'X-Goog-Upload-Header-Content-Type': headers['x-goog-upload-header-content-type'],
37+
},
38+
validateStatus: (status) => status === 200 || status === 308,
39+
timeout: 2 * MINUTE,
40+
returnFullResponse: true,
41+
});
42+
return result;
43+
}
44+
45+
async uploadFileData(uploadUrl: string, body: Buffer, headers: any) {
46+
const contentLength = headers['content-length'] || body.length;
47+
const uploadOffset = headers['x-goog-upload-offset'] || '0';
48+
const uploadCommand = headers['x-goog-upload-command'] || 'upload, finalize';
49+
const result = await this.makeRequest(uploadUrl, headers, body, {}, false, {
50+
customHeaders: {
51+
'Content-Length': contentLength.toString(),
52+
'X-Goog-Upload-Offset': uploadOffset,
53+
'X-Goog-Upload-Command': uploadCommand,
54+
},
55+
timeout: 5 * MINUTE,
56+
maxContentLength: Infinity,
57+
maxBodyLength: Infinity,
58+
validateStatus: (status) => status === 200 || status === 308,
59+
isBinaryData: true,
60+
});
61+
return result.data;
62+
}
63+
64+
async uploadFileChunk(
65+
uploadUrl: string,
66+
chunkData: Buffer,
67+
headers: any,
68+
chunkIndex: number,
69+
totalChunks: number
70+
) {
71+
const contentLength = chunkData.length;
72+
const uploadOffset = headers['x-goog-upload-offset'] || '0';
73+
const isLastChunk = chunkIndex === totalChunks - 1;
74+
const uploadCommand = isLastChunk ? 'upload, finalize' : 'upload';
75+
76+
const result = await this.makeRequest(uploadUrl, headers, chunkData, {}, false, {
77+
customHeaders: {
78+
'Content-Length': contentLength.toString(),
79+
'X-Goog-Upload-Offset': uploadOffset,
80+
'X-Goog-Upload-Command': uploadCommand,
81+
},
82+
timeout: 5 * MINUTE,
83+
maxContentLength: Infinity,
84+
maxBodyLength: Infinity,
85+
validateStatus: (status) => status === 200 || status === 308,
86+
isBinaryData: true,
87+
});
88+
return result.data;
89+
}
90+
91+
async getFileInfo(url: string, headers: any) {
92+
return this.makeRequest(url, headers, null, {}, false, {
93+
method: 'GET',
94+
timeout: 30000,
95+
});
96+
}
97+
3198
private async makeRequest(
3299
url: string,
33100
headers: any,
34101
body: any,
35102
query: any,
36103
stream?: boolean,
104+
options?: {
105+
method?: string;
106+
customHeaders?: Record<string, string>;
107+
validateStatus?: (status: number) => boolean;
108+
timeout?: number;
109+
returnFullResponse?: boolean;
110+
maxContentLength?: number;
111+
maxBodyLength?: number;
112+
isBinaryData?: boolean;
113+
},
37114
) {
38115
const { httpAgent, httpsAgent } = this.getAgents();
116+
const {
117+
method = 'POST',
118+
customHeaders = {},
119+
validateStatus = (status) => status === 200,
120+
timeout = 10 * MINUTE,
121+
returnFullResponse = false,
122+
maxContentLength,
123+
maxBodyLength,
124+
isBinaryData = false,
125+
} = options || {};
126+
127+
const requestHeaders: Record<string, string> = {
128+
...customHeaders,
129+
};
130+
if (!isBinaryData) {
131+
requestHeaders['Content-Type'] = 'application/json';
132+
}
133+
if (headers['anthropic-version']) {
134+
requestHeaders['anthropic-version'] = headers['anthropic-version'];
135+
}
136+
if (headers['x-api-key']) {
137+
requestHeaders['x-api-key'] = headers['x-api-key'];
138+
}
139+
if (headers['x-goog-api-key']) {
140+
requestHeaders['x-goog-api-key'] = headers['x-goog-api-key'];
141+
}
142+
const axiosConfig: any = {
143+
httpAgent,
144+
httpsAgent,
145+
method,
146+
headers: requestHeaders,
147+
params: query,
148+
responseType: stream ? 'stream' : 'json',
149+
data: body,
150+
timeout,
151+
validateStatus,
152+
};
153+
if (maxContentLength !== undefined) {
154+
axiosConfig.maxContentLength = maxContentLength;
155+
}
156+
if (maxBodyLength !== undefined) {
157+
axiosConfig.maxBodyLength = maxBodyLength;
158+
}
39159
let response: any;
40160
try {
41-
response = await axios(url, {
42-
httpAgent,
43-
httpsAgent,
44-
method: 'POST',
45-
headers: {
46-
'Content-Type': 'application/json',
47-
'anthropic-version': headers['anthropic-version'],
48-
'x-api-key': headers['x-api-key'],
49-
},
50-
params: query,
51-
responseType: stream ? 'stream' : 'json',
52-
data: body,
53-
timeout: 10 * MINUTE,
54-
});
161+
response = await axios(url, axiosConfig);
55162
} catch (e) {
56163
if (e.response) {
57164
if (stream) {
58165
return e.response.data;
59166
}
60-
throw new HttpException(e.response.data, e.response.status);
167+
throw new HttpException({
168+
message: e.response.data?.message || e.message,
169+
data: e.response.data,
170+
status: e.response.status,
171+
statusText: e.response.statusText,
172+
headers: e.response.headers,
173+
}, e.response.status);
61174
} else if (e.request) {
62-
console.log(e.message);
63-
throw new Error(
64-
`Failed to send message. error message: ${e.message}, request: ${e.request}`,
65-
);
175+
throw new HttpException({
176+
message: `Network request failed: ${e.message}`,
177+
code: e.code,
178+
errno: e.errno,
179+
syscall: e.syscall,
180+
hostname: e.hostname,
181+
port: e.port,
182+
path: e.path,
183+
}, 500);
66184
} else {
67-
throw e;
185+
throw new HttpException({
186+
message: e.message,
187+
stack: e.stack,
188+
name: e.name,
189+
}, 500);
68190
}
69191
}
70-
71-
if (response.status !== 200) {
72-
const error = new HttpException(response.data, response.status);
73-
throw error;
192+
if (!validateStatus(response.status)) {
193+
throw new HttpException({
194+
message: response.data?.message || `HTTP ${response.status} Error`,
195+
data: response.data,
196+
status: response.status,
197+
statusText: response.statusText,
198+
headers: response.headers,
199+
}, response.status);
74200
}
75-
return response.data;
201+
return returnFullResponse
202+
? {
203+
status: response.status,
204+
headers: response.headers,
205+
data: response.data,
206+
}
207+
: response.data;
76208
}
77209

78210
private getAgents() {

src/main.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@ import { NestFactory } from '@nestjs/core';
22
import { ConfigService } from '@nestjs/config';
33
import { AppModule } from './app.module';
44
import { Logger, ValidationPipe } from '@nestjs/common';
5-
import { urlencoded, json } from 'express';
5+
import { urlencoded, json, raw } from 'express';
66

77
async function bootstrap() {
88
const app = await NestFactory.create(AppModule);
99
app.enableShutdownHooks();
10+
11+
app.use((req, res, next) => {
12+
if (req.path === '/google/upload/v1beta/files' && req.query.upload_id) {
13+
raw({ type: '*/*', limit: '100mb' })(req, res, next);
14+
} else {
15+
next();
16+
}
17+
});
18+
1019
app.use(json({ limit: '50mb' }));
1120
app.useGlobalPipes(new ValidationPipe({ transform: true }));
12-
app.use(urlencoded({ extended: true, limit: '50mb' }));
13-
21+
app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 50000 }));
1422
const configService = app.get(ConfigService);
1523
const port = configService.get<number>('port');
16-
1724
await app.listen(port, () => {
1825
Logger.log(`Server started listening on port: ${port}`);
1926
});

0 commit comments

Comments
 (0)