| |
|
|
| import { serve } from "https://deno.land/std@0.203.0/http/server.ts"; |
|
|
| |
| |
| |
|
|
| |
| const API_KEY = "your-api-key-here"; |
| const SERVER_HOST = "0.0.0.0"; |
| const SERVER_PORT = 7860; |
|
|
| |
| const CUSTOM_IMAGE_BASE_URL = ""; |
| const INCLUDE_PORT_IN_URL = false; |
| const CUSTOM_PORT = 7860; |
|
|
| |
| const IMAGE_DIR = "/tmp/public/images"; |
| const IMAGE_EXPIRE_HOURS = 1; |
| const ENABLE_IMAGE_STORAGE = true; |
| const RETURN_BASE64_IMAGES = true; |
|
|
| |
| const VENICE_CHAT_URL = "https://outerface.venice.ai/api/inference/chat"; |
| const VENICE_IMAGE_URL = "https://outerface.venice.ai/api/inference/image"; |
| const VENICE_VERSION = "interface@20251007.055834+464da4e"; |
|
|
| |
| const CF_IP_API_URL = "https://ipdb.api.030101.xyz/?type=cfv4;proxy"; |
| const CF_HTTPS_PORTS = [443, 2053, 2083, 2087, 2096, 8443]; |
| const CF_HTTP_PORTS = [80, 8080, 8880, 2052, 2082, 2086, 2095]; |
| const USE_CF_AS_PROXY = true; |
| const PROXY_ROTATION_ENABLED = true; |
| const MAX_REQUESTS_PER_PROXY = 1; |
|
|
| |
| const IMAGE_MODELS = ["stable-diffusion-3.5-rev2", "qwen-image", "hidream"]; |
|
|
| |
| |
| |
|
|
| |
| let cfProxies: Array<{ip: string, port: number, lastUsed: number}> = []; |
| let currentProxyIndex = 0; |
|
|
| |
| const imageStore = new Map<string, Uint8Array>(); |
|
|
| |
| const USER_AGENTS = [ |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0", |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/121.0", |
| "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0", |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", |
| ]; |
|
|
| |
|
|
| function getRandomUserAgent(): string { |
| return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]; |
| } |
|
|
| function validateApiKey(req: Request): boolean { |
| const authHeader = req.headers.get("Authorization"); |
| if (!authHeader) return false; |
| const match = authHeader.match(/^Bearer\s+(.+)$/); |
| if (!match) return false; |
| return match[1] === API_KEY; |
| } |
|
|
| |
| function addCorsHeaders(response: Response): Response { |
| response.headers.set("Access-Control-Allow-Origin", "*"); |
| response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); |
| response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); |
| return response; |
| } |
|
|
| |
| function simpleHash(str: string): string { |
| let hash = 0; |
| for (let i = 0; i < str.length; i++) { |
| const char = str.charCodeAt(i); |
| hash = ((hash << 5) - hash) + char; |
| hash = hash & hash; |
| } |
| return Math.abs(hash).toString(16); |
| } |
|
|
| |
| function generateDynamicUserId(req?: Request): string { |
| const timestamp = Date.now(); |
| const random = Math.floor(Math.random() * 1000000); |
| const randomId = crypto.randomUUID().slice(0, 8); |
| |
| if (req) { |
| const ip = req.headers.get("x-forwarded-for") || |
| req.headers.get("x-real-ip") || |
| "unknown"; |
| const userAgent = req.headers.get("user-agent") || "unknown"; |
| |
| const hashInput = `${ip}-${userAgent}-${timestamp}-${random}`; |
| const hashHex = simpleHash(hashInput); |
| |
| return `user_${hashHex.slice(0, 6)}_${randomId}_${random}`; |
| } |
| |
| return `user_anon_${randomId}_${random}`; |
| } |
|
|
| |
| function generateImageUrl(filename: string, req?: Request): string { |
| if (CUSTOM_IMAGE_BASE_URL.trim()) { |
| let baseUrl = CUSTOM_IMAGE_BASE_URL.trim(); |
| if (baseUrl.endsWith('/')) { |
| baseUrl = baseUrl.slice(0, -1); |
| } |
| |
| if (INCLUDE_PORT_IN_URL && CUSTOM_PORT) { |
| const hasPort = baseUrl.match(/:(\d+)$/); |
| if (!hasPort) { |
| baseUrl += `:${CUSTOM_PORT}`; |
| } |
| } |
| |
| return `${baseUrl}/images/${filename}`; |
| } |
| |
| const protocol = "https"; |
| let host = "your-domain.deno.dev"; |
| |
| if (req) { |
| const reqHost = req.headers.get("host"); |
| if (reqHost) { |
| host = reqHost; |
| } |
| } |
| |
| let url = `${protocol}://${host}`; |
| |
| if (INCLUDE_PORT_IN_URL && CUSTOM_PORT) { |
| if ((protocol === 'http' && CUSTOM_PORT !== 80) || |
| (protocol === 'https' && CUSTOM_PORT !== 443)) { |
| url += `:${CUSTOM_PORT}`; |
| } |
| } |
| |
| return `${url}/images/${filename}`; |
| } |
|
|
| |
| function arrayBufferToBase64(buffer: ArrayBuffer): string { |
| let binary = ''; |
| const bytes = new Uint8Array(buffer); |
| const len = bytes.byteLength; |
| for (let i = 0; i < len; i++) { |
| binary += String.fromCharCode(bytes[i]); |
| } |
| return btoa(binary); |
| } |
|
|
| |
|
|
| async function fetchAndUpdateCfProxies() { |
| try { |
| console.log("正在从API获取最新的Cloudflare代理列表..."); |
| |
| const response = await fetch(CF_IP_API_URL, { |
| headers: { |
| "User-Agent": getRandomUserAgent(), |
| "Accept": "text/plain", |
| } |
| }); |
| |
| if (!response.ok) throw new Error(`API请求失败: ${response.status} ${response.statusText}`); |
| const text = await response.text(); |
| const ips = text.split('\n').filter(ip => ip.trim() !== ''); |
| |
| |
| cfProxies = []; |
| const httpsPorts = CF_HTTPS_PORTS; |
| |
| for (const ip of ips) { |
| for (const port of httpsPorts) { |
| cfProxies.push({ |
| ip: ip.trim(), |
| port: port, |
| lastUsed: 0 |
| }); |
| } |
| } |
| |
| console.log(`成功创建 ${cfProxies.length} 个Cloudflare代理端点。`); |
| if (cfProxies.length === 0) { |
| console.error("警告:没有可用的代理端点。"); |
| } |
| } catch (error) { |
| console.error("获取Cloudflare代理列表时出错:", error); |
| } |
| } |
|
|
| |
| function getNextProxy(): {ip: string, port: number} | null { |
| if (cfProxies.length === 0) { |
| console.error("代理列表为空,无法获取代理。"); |
| return null; |
| } |
| |
| |
| let bestProxy = cfProxies[0]; |
| let bestIndex = 0; |
| |
| for (let i = 0; i < cfProxies.length; i++) { |
| const proxy = cfProxies[i]; |
| if (proxy.lastUsed < bestProxy.lastUsed) { |
| bestProxy = proxy; |
| bestIndex = i; |
| } |
| } |
| |
| |
| bestProxy.lastUsed = Date.now(); |
| cfProxies[bestIndex] = bestProxy; |
| |
| console.log(`[代理轮换] 使用代理: ${bestProxy.ip}:${bestProxy.port}`); |
| return { ip: bestProxy.ip, port: bestProxy.port }; |
| } |
|
|
| |
| async function fetchThroughCloudflareProxy(url: string, options: RequestInit = {}): Promise<Response> { |
| const proxy = getNextProxy(); |
| if (!proxy) { |
| throw new Error("没有可用的代理"); |
| } |
| |
| console.log(`[代理请求] 通过 ${proxy.ip}:${proxy.port} 请求: ${url}`); |
| |
| |
| const client = Deno.createHttpClient({ |
| connect: { |
| hostname: proxy.ip, |
| port: proxy.port, |
| } |
| }); |
| |
| try { |
| |
| const defaultHeaders = { |
| "User-Agent": getRandomUserAgent(), |
| "Accept": "application/json, text/plain, */*", |
| "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", |
| "Accept-Encoding": "gzip, deflate, br", |
| "Connection": "keep-alive", |
| "Sec-Fetch-Dest": "empty", |
| "Sec-Fetch-Mode": "cors", |
| "Sec-Fetch-Site": "cross-site", |
| "Sec-Ch-Ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', |
| "Sec-Ch-Ua-Mobile": "?0", |
| "Sec-Ch-Ua-Platform": '"Windows"', |
| "Cache-Control": "no-cache", |
| "Pragma": "no-cache", |
| |
| "X-Forwarded-For": `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`, |
| "X-Real-IP": `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`, |
| ...options.headers, |
| }; |
| |
| const response = await fetch(url, { |
| ...options, |
| client, |
| headers: defaultHeaders, |
| }); |
| |
| console.log(`[代理响应] 状态: ${response.status} ${response.statusText}`); |
| |
| |
| const remainingRequests = response.headers.get('x-ratelimit-remaining-requests'); |
| const resetRequests = response.headers.get('x-ratelimit-reset-requests'); |
| if (remainingRequests || resetRequests) { |
| console.log(`[速率限制] 剩余: ${remainingRequests}, 重置: ${resetRequests}`); |
| } |
| |
| return response; |
| } finally { |
| client.close(); |
| } |
| } |
|
|
| |
| async function fetchWithRetry(url: string, options: RequestInit = {}, maxRetries: number = 3): Promise<Response> { |
| let attempt = 0; |
| |
| while (attempt < maxRetries) { |
| try { |
| const response = await fetchThroughCloudflareProxy(url, options); |
| |
| if (response.status !== 429) { |
| return response; |
| } |
|
|
| console.warn(`[429错误] 第${attempt + 1}次尝试收到429错误`); |
| |
| const resetTimeHeader = response.headers.get('x-ratelimit-reset-requests'); |
| let waitTime = 5000; |
| |
| if (resetTimeHeader) { |
| const resetTime = parseInt(resetTimeHeader, 10) * 1000; |
| waitTime = Math.max(resetTime - Date.now(), 2000); |
| } |
| |
| console.log(`[重试] 等待 ${waitTime}ms 后重试...`); |
| await new Promise(resolve => setTimeout(resolve, waitTime)); |
| |
| |
| const nextProxy = getNextProxy(); |
| if (nextProxy) { |
| console.log(`[强制换代理] 切换到: ${nextProxy.ip}:${nextProxy.port}`); |
| } |
|
|
| } catch (error) { |
| console.error(`请求失败 (尝试 ${attempt + 1}/${maxRetries}):`, error); |
| if (attempt === maxRetries - 1) throw error; |
| |
| const backoffTime = Math.pow(2, attempt) * 1000; |
| await new Promise(resolve => setTimeout(resolve, backoffTime)); |
| } |
| attempt++; |
| } |
| |
| throw new Error('达到最大重试次数'); |
| } |
|
|
| |
|
|
| async function ensureImageDir() { |
| if (!ENABLE_IMAGE_STORAGE) { |
| console.log("图片存储已禁用,使用内存存储"); |
| return; |
| } |
| |
| try { |
| await Deno.mkdir(IMAGE_DIR, { recursive: true }); |
| console.log(`图片目录已准备就绪: ${IMAGE_DIR}`); |
| } catch (error) { |
| console.error(`创建图片目录失败: ${error}`); |
| console.log("将使用内存存储替代文件存储"); |
| } |
| } |
|
|
| async function cleanOldImages() { |
| if (!ENABLE_IMAGE_STORAGE) { |
| return; |
| } |
| |
| try { |
| console.log("[清理任务] 开始清理旧图片..."); |
| const expireTime = Date.now() - IMAGE_EXPIRE_HOURS * 60 * 60 * 1000; |
| let deletedCount = 0; |
| for await (const entry of Deno.readDir(IMAGE_DIR)) { |
| if (entry.isFile) { |
| const filePath = `${IMAGE_DIR}/${entry.name}`; |
| const fileInfo = await Deno.stat(filePath); |
| if (fileInfo.mtime?.getTime() && fileInfo.mtime.getTime() < expireTime) { |
| await Deno.remove(filePath); |
| deletedCount++; |
| } |
| } |
| } |
| console.log(`[清理任务] 完成,删除了 ${deletedCount} 个旧图片文件。`); |
| } catch (error) { |
| console.error("[清理任务] 清理失败:", error); |
| } |
| } |
|
|
| |
|
|
| function openaiModels() { |
| return { |
| object: "list", |
| data: [ |
| { id: "dolphin-3.0-mistral-24b-1dot1", object: "model", created: 1690000000, owned_by: "venice.ai" }, |
| { id: "mistral-31-24b", object: "model", created: 1690000001, owned_by: "venice.ai" }, |
| { id: "stable-diffusion-3.5-rev2", object: "model", created: 1690000002, owned_by: "venice.ai" }, |
| { id: "qwen-image", object: "model", created: 1690000003, owned_by: "venice.ai" }, |
| { id: "hidream", object: "model", created: 1690000004, owned_by: "venice.ai" }, |
| ], |
| }; |
| } |
|
|
| |
|
|
| async function handleImageGeneration(model: string, prompt: string, size: string, negativePrompt: string, req: Request): Promise<Response> { |
| try { |
| const userId = generateDynamicUserId(req); |
| console.log(`[图片生成] 使用动态用户ID: ${userId}`); |
| |
| const [width, height] = size.split('x').map(Number); |
| const venicePayload = { |
| aspectRatio: `${width}:${height}`, embedExifMetadata: true, format: "webp", height, hideWatermark: false, |
| imageToImageCfgScale: 15, imageToImageStrength: 33, loraStrength: 75, matureFilter: true, |
| messageId: crypto.randomUUID().slice(0, 8), modelId: model, negativePrompt, parentMessageId: null, prompt, |
| requestId: crypto.randomUUID().slice(0, 8), seed: Math.floor(Math.random() * 2**31), |
| steps: model === "hidream" || model === "qwen-image" ? 20 : 25, stylePreset: "None", type: "image", |
| userId: userId, |
| variants: 1, width, |
| }; |
| const headers = { |
| "Content-Type": "application/json", |
| "Origin": "https://venice.ai", |
| "Referer": "https://venice.ai/", |
| "User-Agent": getRandomUserAgent(), |
| "Accept": "application/json, image/*", |
| "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", |
| "Accept-Encoding": "gzip, deflate, br", |
| "Cache-Control": "no-cache", |
| "Pragma": "no-cache", |
| "Sec-Fetch-Dest": "empty", |
| "Sec-Fetch-Mode": "cors", |
| "Sec-Fetch-Site": "cross-site", |
| "x-venice-timestamp": new Date().toISOString(), |
| "x-venice-version": VENICE_VERSION |
| }; |
| |
| console.log(`[图片生成] 请求Venice API: ${VENICE_IMAGE_URL}`); |
| |
| const veniceResp = await fetchWithRetry(VENICE_IMAGE_URL, { |
| method: "POST", |
| headers, |
| body: JSON.stringify(venicePayload) |
| }); |
| |
| console.log(`[图片生成] Venice API响应状态: ${veniceResp.status} ${veniceResp.statusText}`); |
| |
| if (!veniceResp.ok) { |
| const errorText = await veniceResp.text(); |
| console.error(`[错误] Venice API返回错误 (${veniceResp.status}): ${errorText}`); |
| const errorMarkdown = `# 图片生成失败 |
| |
| **错误信息:** |
| - 状态码:${veniceResp.status} |
| - 详情:${errorText} |
| |
| 请检查请求参数后重试。`; |
| return addCorsHeaders(new Response(errorMarkdown, { |
| status: veniceResp.status, |
| headers: { "Content-Type": "text/markdown; charset=utf-8" } |
| })); |
| } |
| |
| const imageBuffer = await veniceResp.arrayBuffer(); |
| if (imageBuffer.byteLength === 0) { |
| const errorMarkdown = `# 图片生成失败 |
| |
| **错误信息:** |
| - 原因:接收到空的图片数据 |
| |
| 请稍后重试。`; |
| return addCorsHeaders(new Response(errorMarkdown, { |
| status: 500, |
| headers: { "Content-Type": "text/markdown; charset=utf-8" } |
| })); |
| } |
|
|
| const filename = `${crypto.randomUUID()}.webp`; |
| const imageData = new Uint8Array(imageBuffer); |
| |
| if (ENABLE_IMAGE_STORAGE) { |
| try { |
| const filePath = `${IMAGE_DIR}/${filename}`; |
| await Deno.writeFile(filePath, imageData); |
| console.log(`[成功] 图片已保存: ${filename}`); |
| } catch (error) { |
| console.error(`保存图片失败: ${error},使用内存存储`); |
| imageStore.set(filename, imageData); |
| } |
| } else { |
| imageStore.set(filename, imageData); |
| console.log(`[成功] 图片已保存到内存: ${filename}`); |
| } |
|
|
| if (RETURN_BASE64_IMAGES) { |
| const base64Image = arrayBufferToBase64(imageBuffer); |
| const dataUrl = `data:image/webp;base64,${base64Image}`; |
| |
| const readmeResponse = ` |
| |
| ## 图片信息 |
| |
| - **模型**:${model} |
| - **提示词**:${prompt} |
| - **尺寸**:${size} |
| - **负面提示词**:${negativePrompt || '无'} |
| |
| ## 图片数据 |
| |
| Base64编码的图片数据已包含在上方图片中。`; |
|
|
| return addCorsHeaders(new Response(readmeResponse, { |
| headers: { "Content-Type": "text/markdown; charset=utf-8" } |
| })); |
| } else { |
| const imageUrl = generateImageUrl(filename, req); |
| |
| const readmeResponse = ` |
| |
| ## 图片信息 |
| |
| - **模型**:${model} |
| - **提示词**:${prompt} |
| - **尺寸**:${size} |
| - **负面提示词**:${negativePrompt || '无'} |
| |
| ## 图片链接 |
| |
| ${imageUrl}`; |
|
|
| return addCorsHeaders(new Response(readmeResponse, { |
| headers: { "Content-Type": "text/markdown; charset=utf-8" } |
| })); |
| } |
| } catch (error) { |
| console.error("Image generation request failed:", error); |
| const errorMarkdown = `# 图片生成失败 |
| |
| **错误信息:** |
| - 原因:${error.message || '未知错误'} |
| |
| 请稍后重试。`; |
| return addCorsHeaders(new Response(errorMarkdown, { |
| status: 500, |
| headers: { "Content-Type": "text/markdown; charset=utf-8" } |
| })); |
| } |
| } |
|
|
| async function handleChatCompletion(model: string, messages: any[], temperature: number, topP: number, stream: boolean, req: Request): Promise<Response> { |
| try { |
| const userId = generateDynamicUserId(req); |
| console.log(`[聊天] 使用动态用户ID: ${userId}`); |
| |
| const venicePayload = { |
| characterId: "", |
| clientProcessingTime: 2, |
| conversationType: "text", |
| includeVeniceSystemPrompt: true, |
| isCharacter: false, |
| modelId: model, |
| prompt: messages, |
| reasoning: true, |
| requestId: crypto.randomUUID().slice(0, 8), |
| systemPrompt: "", |
| temperature, |
| topP, |
| userId: userId, |
| webEnabled: true |
| }; |
| const headers = { |
| "Content-Type": "application/json", |
| "Origin": "https://venice.ai", |
| "Referer": "https://venice.ai/", |
| "User-Agent": getRandomUserAgent(), |
| "Accept": "text/event-stream, application/json, text/plain", |
| "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", |
| "Accept-Encoding": "gzip, deflate, br", |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive", |
| "Sec-Fetch-Dest": "empty", |
| "Sec-Fetch-Mode": "cors", |
| "Sec-Fetch-Site": "cross-site", |
| }; |
| |
| console.log(`[聊天] 请求Venice API: ${VENICE_CHAT_URL}`); |
| |
| const veniceResp = await fetchWithRetry(VENICE_CHAT_URL, { |
| method: "POST", |
| headers, |
| body: JSON.stringify(venicePayload) |
| }); |
| |
| console.log(`[聊天] Venice API响应状态: ${veniceResp.status} ${veniceResp.statusText}`); |
| |
| if (!veniceResp.ok) { |
| const errorText = await veniceResp.text(); |
| console.error(`[错误] Venice API返回错误:`, errorText); |
| return addCorsHeaders(new Response(JSON.stringify({ |
| error: { |
| message: `Venice API error: ${veniceResp.status}`, |
| details: errorText, |
| type: "venice_api_error" |
| } |
| }), { |
| status: veniceResp.status, |
| headers: { "Content-Type": "application/json" } |
| })); |
| } |
|
|
| if (stream) { |
| const reader = veniceResp.body?.getReader(); |
| const encoder = new TextEncoder(); |
| const decoder = new TextDecoder(); |
| let buffer = ""; |
| let isFinished = false; |
| |
| const streamResp = new ReadableStream({ |
| async start(controller) { |
| const timeoutId = setTimeout(() => { |
| if (!isFinished) { |
| console.error("Stream timeout, closing connection"); |
| controller.close(); |
| } |
| }, 60000); |
| |
| try { |
| while (!isFinished) { |
| if (!reader) { |
| controller.enqueue(encoder.encode("data: [DONE]\n\n")); |
| controller.close(); |
| isFinished = true; |
| break; |
| } |
| const { done, value } = await reader.read(); |
| if (done) { |
| controller.enqueue(encoder.encode("data: [DONE]\n\n")); |
| controller.close(); |
| isFinished = true; |
| break; |
| } |
| const chunk = decoder.decode(value, { stream: true }); |
| buffer += chunk; |
| let idx; |
| while ((idx = buffer.indexOf("\n")) >= 0) { |
| const line = buffer.slice(0, idx).trim(); |
| buffer = buffer.slice(idx + 1); |
| if (!line) continue; |
| try { |
| const data = JSON.parse(line); |
| const content = data.content; |
| if (content) { |
| const chunk = { |
| id: `chatcmpl-${crypto.randomUUID().slice(0, 8)}`, |
| object: "chat.completion.chunk", |
| created: Math.floor(Date.now() / 1000), |
| model, |
| choices: [{ |
| delta: { content }, |
| index: 0, |
| finish_reason: null, |
| }], |
| }; |
| controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); |
| } |
| } catch (parseError) { |
| console.error("JSON parse error:", parseError, "Line:", line); |
| } |
| } |
| } |
| } catch (error) { |
| console.error("Stream processing error:", error); |
| const errorChunk = { |
| id: `chatcmpl-${crypto.randomUUID().slice(0, 8)}`, |
| object: "chat.completion.chunk", |
| created: Math.floor(Date.now() / 1000), |
| model, |
| choices: [ |
| { |
| delta: { content: "\n\n[Stream interrupted due to error]" }, |
| index: 0, |
| finish_reason: "error", |
| }, |
| ], |
| }; |
| controller.enqueue( |
| encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`), |
| ); |
| controller.enqueue(encoder.encode("data: [DONE]\n\n")); |
| controller.close(); |
| } finally { |
| clearTimeout(timeoutId); |
| isFinished = true; |
| } |
| } |
| }); |
| |
| const response = new Response(streamResp, { |
| headers: { |
| "Content-Type": "text/event-stream", |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive", |
| "Access-Control-Allow-Origin": "*", |
| "Access-Control-Allow-Headers": "Content-Type, Authorization", |
| }, |
| }); |
| return response; |
| } else { |
| const text = await veniceResp.text(); |
| console.log(`[聊天] Venice API响应内容:`, text); |
| const contents = text.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l).content).join(""); |
| const resp = { |
| id: `chatcmpl-${crypto.randomUUID().slice(0, 8)}`, |
| object: "chat.completion", |
| created: Math.floor(Date.now() / 1000), |
| model, |
| choices: [{ |
| index: 0, |
| message: { role: "assistant", content: contents }, |
| finish_reason: "stop", |
| }], |
| }; |
| return addCorsHeaders(new Response(JSON.stringify(resp), { headers: { "Content-Type": "application/json" }, })); |
| } |
| } catch (error) { |
| console.error("Chat completion request failed:", error); |
| return addCorsHeaders(new Response(JSON.stringify({ |
| error: { |
| message: "Failed to process chat completion", |
| type: "request_error", |
| details: error.message |
| } |
| }), { |
| status: 500, |
| headers: { "Content-Type": "application/json" } |
| })); |
| } |
| } |
|
|
| |
|
|
| async function initializeServer() { |
| await ensureImageDir(); |
| await fetchAndUpdateCfProxies(); |
| if (cfProxies.length === 0) console.error("无法获取任何Cloudflare代理,服务将启动但可能无法正常工作。"); |
| |
| if (ENABLE_IMAGE_STORAGE) { |
| setInterval(fetchAndUpdateCfProxies, 5 * 60 * 1000); |
| setInterval(cleanOldImages, IMAGE_EXPIRE_HOURS * 60 * 60 * 1000); |
| } |
|
|
| console.log(`服务器已启动,监听端口 ${SERVER_PORT}...`); |
| |
| if (CUSTOM_IMAGE_BASE_URL.trim()) { |
| console.log(`使用自定义图片基础URL: ${CUSTOM_IMAGE_BASE_URL}`); |
| if (INCLUDE_PORT_IN_URL && CUSTOM_PORT) { |
| console.log(`端口配置: 包含端口 ${CUSTOM_PORT}`); |
| } else { |
| console.log(`端口配置: 不包含端口`); |
| } |
| } else { |
| console.log(`图片将通过自动检测的URL访问`); |
| console.log(`当前配置: ${SERVER_HOST}:${SERVER_PORT}`); |
| } |
| |
| console.log(`Cloudflare代理: ${USE_CF_AS_PROXY ? '启用' : '禁用'}`); |
| if (USE_CF_AS_PROXY) { |
| console.log(`可用代理数量: ${cfProxies.length}`); |
| } |
| console.log(`图片存储: ${ENABLE_IMAGE_STORAGE ? '文件存储' : '内存存储'}`); |
| console.log(`图片返回格式: ${RETURN_BASE64_IMAGES ? 'Base64' : 'URL'}`); |
| |
| serve(async (req: Request) => { |
| const url = new URL(req.url); |
| |
| if (req.method === "OPTIONS") { |
| return addCorsHeaders(new Response(null, { status: 200 })); |
| } |
| |
| |
| if (url.pathname === "/test-ip" && req.method === "GET") { |
| try { |
| const testUrl = "https://httpbin.org/ip"; |
| const response = await fetchThroughCloudflareProxy(testUrl); |
| const result = await response.json(); |
| return addCorsHeaders(new Response(JSON.stringify({ |
| proxyIP: result.origin, |
| proxyEnabled: USE_CF_AS_PROXY, |
| totalProxies: cfProxies.length, |
| timestamp: Date.now() |
| }), { headers: { "Content-Type": "application/json" } })); |
| } catch (error) { |
| return addCorsHeaders(new Response(JSON.stringify({ |
| error: error.message |
| }), { status: 500 })); |
| } |
| } |
| |
| |
| if (url.pathname.startsWith("/images/")) { |
| const filename = url.pathname.substring("/images/".length); |
| |
| if (imageStore.has(filename)) { |
| const imageData = imageStore.get(filename); |
| |
| if (url.searchParams.get('format') === 'base64') { |
| const base64Image = arrayBufferToBase64(imageData.buffer); |
| const dataUrl = `data:image/webp;base64,${base64Image}`; |
| return addCorsHeaders(new Response(JSON.stringify({ |
| dataUrl, |
| filename |
| }), { |
| headers: { |
| "Content-Type": "application/json", |
| "Cache-Control": `public, max-age=${IMAGE_EXPIRE_HOURS * 3600}` |
| } |
| })); |
| } |
| |
| return addCorsHeaders(new Response(imageData, { |
| headers: { |
| "Content-Type": "image/webp", |
| "Cache-Control": `public, max-age=${IMAGE_EXPIRE_HOURS * 3600}` |
| } |
| })); |
| } |
| |
| if (ENABLE_IMAGE_STORAGE) { |
| try { |
| const filePath = `${IMAGE_DIR}/${filename}`; |
| const imageFile = await Deno.readFile(filePath); |
| |
| if (url.searchParams.get('format') === 'base64') { |
| const base64Image = arrayBufferToBase64(imageFile.buffer); |
| const dataUrl = `data:image/webp;base64,${base64Image}`; |
| return addCorsHeaders(new Response(JSON.stringify({ |
| dataUrl, |
| filename |
| }), { |
| headers: { |
| "Content-Type": "application/json", |
| "Cache-Control": `public, max-age=${IMAGE_EXPIRE_HOURS * 3600}` |
| } |
| })); |
| } |
| |
| return addCorsHeaders(new Response(imageFile, { |
| headers: { |
| "Content-Type": "image/webp", |
| "Cache-Control": `public, max-age=${IMAGE_EXPIRE_HOURS * 3600}` |
| } |
| })); |
| } catch (error) { |
| console.error(`读取图片文件失败: ${error}`); |
| } |
| } |
| |
| return addCorsHeaders(new Response("Image Not Found", { status: 404 })); |
| } |
|
|
| |
| if (url.pathname === "/v1/models" || url.pathname === "/v1/chat/completions") { |
| if (!validateApiKey(req)) { |
| return addCorsHeaders(new Response(JSON.stringify({ |
| error: { |
| message: "Invalid API key", |
| type: "invalid_request_error", |
| code: "invalid_api_key" |
| } |
| }), { |
| status: 401, |
| headers: { "Content-Type": "application/json" } |
| })); |
| } |
| } |
|
|
| |
| if (url.pathname === "/v1/models") { |
| return addCorsHeaders(new Response(JSON.stringify(openaiModels()), { headers: { "Content-Type": "application/json" } })); |
| } |
|
|
| if (url.pathname === "/v1/chat/completions" && req.method === "POST") { |
| try { |
| const body = await req.json(); |
| const model = body.model ?? "dolphin-3.0-mistral-24b-1dot1"; |
| const messages = body.messages ?? []; |
| const temperature = body.temperature ?? 0.7; |
| const topP = body.top_p ?? 0.9; |
| const stream = body.stream ?? false; |
| |
| if (IMAGE_MODELS.includes(model)) { |
| console.log(`[请求类型] 画图 - 模型: ${model}`); |
| const lastUserMessage = messages.filter(m => m.role === 'user').pop(); |
| const prompt = lastUserMessage?.content; |
| if (!prompt || typeof prompt !== 'string') { |
| const errorMarkdown = `# 请求错误 |
| |
| **错误信息:** |
| - 原因:图片生成需要文本提示词 |
| - 要求:请在最后一条用户消息中提供提示词 |
| |
| 示例: |
| \`\`\`json |
| { |
| "model": "stable-diffusion-3.5-rev2", |
| "messages": [ |
| {"role": "user", "content": "a beautiful sunset"} |
| ] |
| } |
| \`\`\``; |
| return addCorsHeaders(new Response(errorMarkdown, { |
| status: 400, |
| headers: { "Content-Type": "text/markdown; charset=utf-8" } |
| })); |
| } |
| const size = body.size ?? "1024x1024"; |
| const negativePrompt = body.negative_prompt ?? ""; |
| return await handleImageGeneration(model, prompt, size, negativePrompt, req); |
| } else { |
| console.log(`[请求类型] 聊天 - 模型: ${model}`); |
| return await handleChatCompletion(model, messages, temperature, topP, stream, req); |
| } |
| } catch (error) { |
| console.error("Request processing error:", error); |
| const errorMarkdown = `# 请求处理失败 |
| |
| **错误信息:** |
| - 原因:${error.message || '未知错误'} |
| |
| 请检查请求格式后重试。`; |
| return addCorsHeaders(new Response(errorMarkdown, { |
| status: 500, |
| headers: { "Content-Type": "text/markdown; charset=utf-8" } |
| })); |
| } |
| } |
|
|
| return addCorsHeaders(new Response("Not Found", { status: 404 })); |
| }, { port: SERVER_PORT }); |
| } |
|
|
| |
| initializeServer(); |