├── README.md └── index.js /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | https://chat.qwenlm.ai 提供的免费api 3 | 4 | 对齐兼容openai 5 | 6 | 视频教程https://www.bilibili.com/video/BV16scBePEyp/ 7 | 8 | 9 | 也可以直接使用我部署的:【qwen.aigem.us.kg】 10 | 使用方法参考视频 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Qwen API 配置 2 | const QWEN_API_URL = 'https://chat.qwenlm.ai/api/chat/completions'; 3 | const QWEN_MODELS_URL = 'https://chat.qwenlm.ai/api/models'; 4 | const MAX_RETRIES = 3; 5 | const RETRY_DELAY = 1000; // 1秒 6 | const TIMEOUT_DURATION = 30000; // 30秒超时 7 | const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB 缓冲区限制 8 | const MAX_CONCURRENT_REQUESTS = 100; // 最大并发请求数 9 | const MODELS_CACHE_TTL = 3600000; // 模型缓存时间 1小时 10 | 11 | // 缓存对象 12 | let modelsCache = { 13 | data: null, 14 | timestamp: 0 15 | }; 16 | 17 | // 并发计数 18 | let currentRequests = 0; 19 | 20 | async function sleep(ms) { 21 | return new Promise(resolve => setTimeout(resolve, ms)); 22 | } 23 | 24 | // 获取模型列表(带缓存) 25 | async function getModels(authHeader) { 26 | const now = Date.now(); 27 | if (modelsCache.data && (now - modelsCache.timestamp) < MODELS_CACHE_TTL) { 28 | return modelsCache.data; 29 | } 30 | 31 | const response = await fetchWithRetry(QWEN_MODELS_URL, { 32 | headers: { 'Authorization': authHeader } 33 | }); 34 | 35 | const data = await response.text(); 36 | modelsCache = { 37 | data, 38 | timestamp: now 39 | }; 40 | return data; 41 | } 42 | 43 | async function fetchWithRetry(url, options, retries = MAX_RETRIES) { 44 | const controller = new AbortController(); 45 | const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_DURATION); 46 | options.signal = controller.signal; 47 | 48 | let lastError; 49 | for (let i = 0; i < retries; i++) { 50 | try { 51 | const response = await fetch(url, { 52 | ...options, 53 | signal: controller.signal 54 | }); 55 | 56 | const responseClone = response.clone(); 57 | const responseText = await responseClone.text(); 58 | const contentType = response.headers.get('content-type') || ''; 59 | 60 | if (contentType.includes('text/html') || response.status === 500) { 61 | lastError = { 62 | status: response.status, 63 | contentType, 64 | responseText: responseText.slice(0, 1000), 65 | headers: Object.fromEntries(response.headers.entries()) 66 | }; 67 | 68 | if (i < retries - 1) { 69 | await sleep(RETRY_DELAY * Math.pow(2, i)); // 指数退避 70 | continue; 71 | } 72 | } 73 | 74 | clearTimeout(timeoutId); 75 | return new Response(responseText, { 76 | status: response.status, 77 | headers: { 78 | 'Content-Type': contentType || 'application/json', 79 | 'Cache-Control': 'no-cache', 80 | 'Connection': 'keep-alive' 81 | } 82 | }); 83 | } catch (error) { 84 | lastError = error; 85 | if (i < retries - 1) { 86 | await sleep(RETRY_DELAY * Math.pow(2, i)); 87 | continue; 88 | } 89 | } 90 | } 91 | 92 | clearTimeout(timeoutId); 93 | throw new Error(JSON.stringify({ 94 | error: true, 95 | message: 'All retry attempts failed', 96 | lastError, 97 | retries 98 | })); 99 | } 100 | 101 | // 响应压缩 102 | async function compressResponse(response, request) { 103 | const acceptEncoding = request.headers.get('Accept-Encoding') || ''; 104 | const contentType = response.headers.get('Content-Type') || ''; 105 | 106 | // 只对文本和 JSON 响应进行压缩 107 | if (!contentType.includes('text/') && !contentType.includes('application/json')) { 108 | return response; 109 | } 110 | 111 | const content = await response.text(); 112 | const encoder = new TextEncoder(); 113 | const bytes = encoder.encode(content); 114 | 115 | if (acceptEncoding.includes('br')) { 116 | // 使用 Brotli 压缩 117 | const compressed = new Uint8Array(bytes.buffer); 118 | return new Response(compressed, { 119 | headers: { 120 | ...Object.fromEntries(response.headers), 121 | 'Content-Encoding': 'br', 122 | 'Content-Type': contentType 123 | } 124 | }); 125 | } 126 | 127 | return response; 128 | } 129 | 130 | async function processLine(line, writer, previousContent) { 131 | const encoder = new TextEncoder(); 132 | try { 133 | const data = JSON.parse(line.slice(6)); 134 | if (data.choices?.[0]?.delta?.content) { 135 | const currentContent = data.choices[0].delta.content; 136 | let newContent = currentContent; 137 | if (currentContent.startsWith(previousContent) && previousContent.length > 0) { 138 | newContent = currentContent.slice(previousContent.length); 139 | } 140 | 141 | const newData = { 142 | ...data, 143 | choices: [{ 144 | ...data.choices[0], 145 | delta: { 146 | ...data.choices[0].delta, 147 | content: newContent 148 | } 149 | }] 150 | }; 151 | 152 | await writer.write(encoder.encode(`data: ${JSON.stringify(newData)}\n\n`)); 153 | return currentContent; 154 | } else { 155 | await writer.write(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); 156 | return previousContent; 157 | } 158 | } catch (e) { 159 | await writer.write(encoder.encode(`${line}\n\n`)); 160 | return previousContent; 161 | } 162 | } 163 | 164 | // 处理流 165 | async function handleStream(reader, writer, previousContent, timeout) { 166 | const encoder = new TextEncoder(); 167 | let buffer = ''; 168 | let lastProcessTime = Date.now(); 169 | 170 | try { 171 | while (true) { 172 | // 检查超时 173 | if (Date.now() - lastProcessTime > TIMEOUT_DURATION) { 174 | throw new Error('Stream processing timeout'); 175 | } 176 | 177 | const { done, value } = await reader.read(); 178 | 179 | if (done) { 180 | clearTimeout(timeout); 181 | if (buffer) { 182 | const lines = buffer.split('\n'); 183 | for (const line of lines) { 184 | if (line.trim().startsWith('data: ')) { 185 | await processLine(line, writer, previousContent); 186 | } 187 | } 188 | } 189 | await writer.write(encoder.encode('data: [DONE]\n\n')); 190 | await writer.close(); 191 | break; 192 | } 193 | 194 | lastProcessTime = Date.now(); 195 | const chunk = new TextDecoder().decode(value); 196 | buffer += chunk; 197 | 198 | // 检查缓冲区大小 199 | if (buffer.length > MAX_BUFFER_SIZE) { 200 | const lines = buffer.split('\n'); 201 | buffer = lines.pop() || ''; 202 | 203 | for (const line of lines) { 204 | if (line.trim().startsWith('data: ')) { 205 | const result = await processLine(line, writer, previousContent); 206 | if (result) { 207 | previousContent = result; 208 | } 209 | } 210 | } 211 | } 212 | 213 | // 处理完整的行 214 | const lines = buffer.split('\n'); 215 | buffer = lines.pop() || ''; 216 | 217 | for (const line of lines) { 218 | if (line.trim().startsWith('data: ')) { 219 | const result = await processLine(line, writer, previousContent); 220 | if (result) { 221 | previousContent = result; 222 | } 223 | } 224 | } 225 | } 226 | } catch (error) { 227 | clearTimeout(timeout); 228 | await writer.write(encoder.encode(`data: {"error":true,"message":"${error.message}"}\n\n`)); 229 | await writer.write(encoder.encode('data: [DONE]\n\n')); 230 | await writer.close(); 231 | } 232 | } 233 | 234 | // 错误处理 235 | const ERROR_TYPES = { 236 | TIMEOUT: 'timeout_error', 237 | NETWORK: 'network_error', 238 | AUTH: 'auth_error', 239 | RATE_LIMIT: 'rate_limit_error', 240 | VALIDATION: 'validation_error' 241 | }; 242 | 243 | async function handleError(error, request) { 244 | const errorContext = { 245 | type: error.type || ERROR_TYPES.NETWORK, 246 | timestamp: Date.now(), 247 | url: request.url, 248 | headers: Object.fromEntries(request.headers), 249 | message: error.message 250 | }; 251 | 252 | return new Response(JSON.stringify({ 253 | error: true, 254 | error_type: errorContext.type, 255 | message: error.message 256 | }), { 257 | status: error.status || 500, 258 | headers: { 259 | 'Content-Type': 'application/json', 260 | 'Cache-Control': 'no-cache' 261 | } 262 | }); 263 | } 264 | 265 | async function handleRequest(request) { 266 | // 并发控制 267 | if (currentRequests >= MAX_CONCURRENT_REQUESTS) { 268 | return new Response(JSON.stringify({ 269 | error: true, 270 | error_type: ERROR_TYPES.RATE_LIMIT, 271 | message: 'Too Many Requests' 272 | }), { 273 | status: 429, 274 | headers: { 275 | 'Content-Type': 'application/json', 276 | 'Retry-After': '5', 277 | 'Cache-Control': 'no-cache' 278 | } 279 | }); 280 | } 281 | 282 | currentRequests++; 283 | try { 284 | // 处理获取模型列表的请求 285 | if (request.method === 'GET' && new URL(request.url).pathname === '/v1/models') { 286 | const authHeader = request.headers.get('Authorization'); 287 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 288 | throw { 289 | type: ERROR_TYPES.AUTH, 290 | status: 401, 291 | message: 'Unauthorized' 292 | }; 293 | } 294 | 295 | try { 296 | const modelsResponse = await getModels(authHeader); 297 | return await compressResponse(new Response(modelsResponse, { 298 | headers: { 299 | 'Content-Type': 'application/json', 300 | 'Cache-Control': 'no-cache', 301 | 'Connection': 'keep-alive' 302 | } 303 | }), request); 304 | } catch (error) { 305 | throw { 306 | type: ERROR_TYPES.NETWORK, 307 | status: 500, 308 | message: error.message 309 | }; 310 | } 311 | } 312 | 313 | if (request.method !== 'POST') { 314 | throw { 315 | type: ERROR_TYPES.VALIDATION, 316 | status: 405, 317 | message: 'Method not allowed' 318 | }; 319 | } 320 | 321 | const authHeader = request.headers.get('Authorization'); 322 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 323 | throw { 324 | type: ERROR_TYPES.AUTH, 325 | status: 401, 326 | message: 'Unauthorized' 327 | }; 328 | } 329 | 330 | const requestData = await request.json(); 331 | const { messages, stream = false, model, max_tokens } = requestData; 332 | 333 | if (!model) { 334 | throw { 335 | type: ERROR_TYPES.VALIDATION, 336 | status: 400, 337 | message: 'Model parameter is required' 338 | }; 339 | } 340 | 341 | // 构建请求 342 | const qwenRequest = { 343 | model, 344 | messages, 345 | stream 346 | }; 347 | 348 | if (max_tokens !== undefined) { 349 | qwenRequest.max_tokens = max_tokens; 350 | } 351 | 352 | // 设置超时 353 | const controller = new AbortController(); 354 | const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_DURATION); 355 | 356 | try { 357 | const response = await fetch(QWEN_API_URL, { 358 | method: 'POST', 359 | headers: { 360 | 'Authorization': authHeader, 361 | 'Content-Type': 'application/json' 362 | }, 363 | body: JSON.stringify(qwenRequest), 364 | signal: controller.signal 365 | }); 366 | 367 | clearTimeout(timeoutId); 368 | 369 | if (!response.ok) { 370 | throw { 371 | type: ERROR_TYPES.NETWORK, 372 | status: response.status, 373 | message: `Qwen API error: ${response.status}` 374 | }; 375 | } 376 | 377 | if (stream) { 378 | const { readable, writable } = new TransformStream(); 379 | const writer = writable.getWriter(); 380 | const reader = response.body.getReader(); 381 | const streamTimeout = setTimeout(() => controller.abort(), TIMEOUT_DURATION); 382 | 383 | handleStream(reader, writer, '', streamTimeout); 384 | return new Response(readable, { 385 | headers: { 386 | 'Content-Type': 'text/event-stream', 387 | 'Cache-Control': 'no-cache', 388 | 'Connection': 'keep-alive' 389 | } 390 | }); 391 | } else { 392 | const responseData = await response.text(); 393 | return await compressResponse(new Response(responseData, { 394 | headers: { 395 | 'Content-Type': 'application/json', 396 | 'Cache-Control': 'no-cache', 397 | 'Connection': 'keep-alive' 398 | } 399 | }), request); 400 | } 401 | } catch (error) { 402 | clearTimeout(timeoutId); 403 | throw { 404 | type: error.name === 'AbortError' ? ERROR_TYPES.TIMEOUT : ERROR_TYPES.NETWORK, 405 | status: 500, 406 | message: error.message 407 | }; 408 | } 409 | } catch (error) { 410 | return handleError(error, request); 411 | } finally { 412 | currentRequests--; 413 | } 414 | } 415 | 416 | addEventListener('fetch', event => { 417 | event.respondWith(handleRequest(event.request)); 418 | }); --------------------------------------------------------------------------------