|
|
|
|
|
|
|
|
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 = false; |
|
|
const PROXY_ROTATION_ENABLED = false; |
|
|
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(); |