| import { serve } from "https://deno.land/std@0.140.0/http/server.ts"; |
| import { Md5 } from "https://deno.land/std@0.140.0/hash/md5.ts"; |
|
|
| const API_DOMAIN = 'https://ai-api.dangbei.net'; |
| const USER_AGENTS = [ |
| 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', |
| 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', |
| 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', |
| 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' |
| ]; |
| const VALID_API_KEY = Deno.env.get('VALID_API_KEY'); |
| const MAX_CONVERSATIONS_PER_DEVICE = 10; |
|
|
| class ChatManage { |
| private currentDeviceId: string | null = null; |
| private currentConversationId: string | null = null; |
| private conversationCount = 0; |
| private currentUserAgent: string; |
|
|
| constructor() { |
| this.currentUserAgent = this.getRandomUserAgent(); |
| } |
|
|
| private getRandomUserAgent(): string { |
| return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]; |
| } |
|
|
| getOrCreateIds(forceNew = false) { |
| let newDeviceId = this.currentDeviceId; |
| let newConversationId = this.currentConversationId; |
| |
| if (forceNew || !newDeviceId || this.conversationCount >= MAX_CONVERSATIONS_PER_DEVICE) { |
| newDeviceId = this.generateDeviceId(); |
| newConversationId = null; |
| this.conversationCount = 0; |
| |
| this.currentUserAgent = this.getRandomUserAgent(); |
| } |
| |
| this.currentDeviceId = newDeviceId; |
| this.currentConversationId = newConversationId; |
| |
| return { |
| deviceId: newDeviceId, |
| conversationId: newConversationId, |
| userAgent: this.currentUserAgent |
| }; |
| } |
|
|
| updateConversationId(conversationId: string) { |
| this.currentConversationId = conversationId; |
| this.conversationCount++; |
| } |
| |
| generateDeviceId() { |
| const uuid = crypto.randomUUID(); |
| const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; |
| const nanoid = Array.from(crypto.getRandomValues(new Uint8Array(20))) |
| .map(b => urlAlphabet[b % urlAlphabet.length]) |
| .join(''); |
| return `${uuid.replace(/-/g, '')}_${nanoid}`; |
| } |
| } |
|
|
| class Pipe { |
| private dataPrefix = 'data:'; |
| private chatManage = new ChatManage(); |
| private searchModels: Record<string, string> = { |
| 'DeepSeek-R1-Search': 'deepseek', |
| 'DeepSeek-V3-Search': 'deepseek', |
| 'Doubao-Search': 'doubao', |
| 'Qwen-Search': 'qwen' |
| }; |
|
|
| |
| async _create_conversation(deviceId: string) { |
| const { userAgent } = this.chatManage.getOrCreateIds(false); |
| const payload = { botCode: "AI_SEARCH" }; |
| const timestamp = Math.floor(Date.now() / 1000).toString(); |
| const nonce = this.nanoid(21); |
| const sign = await this.generateSign(timestamp, payload, nonce); |
|
|
| const headers = { |
| "Origin": "https://ai.dangbei.com", |
| "Referer": "https://ai.dangbei.com/", |
| "User-Agent": userAgent, |
| "deviceId": deviceId, |
| "nonce": nonce, |
| "sign": sign, |
| "timestamp": timestamp, |
| "Content-Type": "application/json" |
| }; |
|
|
| try { |
| console.log('Creating conversation with:', { |
| url: `${API_DOMAIN}/ai-search/conversationApi/v1/create`, |
| headers, |
| payload |
| }); |
|
|
| const response = await fetch(`${API_DOMAIN}/ai-search/conversationApi/v1/create`, { |
| method: 'POST', |
| headers, |
| body: JSON.stringify(payload), |
| }); |
|
|
| console.log('Response status:', response.status); |
| const responseText = await response.text(); |
| console.log('Response body:', responseText); |
|
|
| if (response.ok) { |
| try { |
| const data = JSON.parse(responseText); |
| if (data.success) { |
| console.log('Successfully created conversation:', data.data.conversationId); |
| return data.data.conversationId; |
| } else { |
| console.error('API returned success: false:', data); |
| } |
| } catch (e) { |
| console.error('Failed to parse response:', e); |
| } |
| } else { |
| console.error('HTTP error:', response.status, responseText); |
| } |
| } catch (e) { |
| console.error('Error creating conversation:', e); |
| } |
| return null; |
| } |
|
|
| |
| _buildFullPrompt(messages: any[]): string { |
| if (!messages || messages.length === 0) { |
| return ''; |
| } |
|
|
| let systemPrompt = ''; |
| const history: string[] = []; |
| let lastUserMessage = ''; |
|
|
| for (const msg of messages) { |
| if (msg.role === 'system' && !systemPrompt) { |
| systemPrompt = msg.content; |
| } else if (msg.role === 'user') { |
| history.push(`user: ${msg.content}`); |
| lastUserMessage = msg.content; |
| } else if (msg.role === 'assistant') { |
| history.push(`assistant: ${msg.content}`); |
| } |
| } |
|
|
| const parts: string[] = []; |
| if (systemPrompt) { |
| parts.push(`[System Prompt]\n${systemPrompt}`); |
| } |
| if (history.length > 1) { |
| parts.push(`[Chat History]\n${history.slice(0, -1).join('\n')}`); |
| } |
| parts.push(`[Question]\n${lastUserMessage}`); |
|
|
| return parts.join('\n\n'); |
| } |
|
|
| async* pipe(body: any) { |
| const thinkingState = { thinking: -1 }; |
| |
| |
| const fullPrompt = this._buildFullPrompt(body.messages); |
| |
| |
| let forceNew = false; |
| const messages = body.messages; |
| if (messages.length === 1) { |
| forceNew = true; |
| } else if (messages.length >= 2) { |
| const lastTwo = messages.slice(-2); |
| if (lastTwo[0].role === 'user' && lastTwo[1].role === 'user') { |
| forceNew = true; |
| } |
| } |
|
|
| |
| const { deviceId, conversationId: storedConversationId, userAgent } = this.chatManage.getOrCreateIds(forceNew); |
| let conversationId = storedConversationId; |
|
|
| |
| if (!conversationId) { |
| conversationId = await this._create_conversation(deviceId); |
| if (!conversationId) { |
| yield { error: 'Failed to create conversation' }; |
| return; |
| } |
| this.chatManage.updateConversationId(conversationId); |
| } |
|
|
| |
| let modelName; |
| const isSearchModel = body.model.endsWith('-Search'); |
| if (isSearchModel) { |
| modelName = this.searchModels[body.model] || body.model.replace('-Search', '').toLowerCase(); |
| } else { |
| const isDeepSeekModel = ['DeepSeek-R1', 'DeepSeek-V3'].includes(body.model); |
| modelName = isDeepSeekModel ? 'deepseek' : body.model.toLowerCase(); |
| } |
|
|
| |
| let userAction = ''; |
| if (body.model.includes('DeepSeek-R1')) { |
| userAction = 'deep'; |
| } |
| if (isSearchModel) { |
| userAction = userAction ? `${userAction},online` : 'online'; |
| } |
|
|
| const payload = { |
| stream: true, |
| botCode: 'AI_SEARCH', |
| userAction, |
| model: modelName, |
| conversationId: conversationId, |
| question: fullPrompt, |
| }; |
|
|
| const timestamp = Math.floor(Date.now() / 1000).toString(); |
| const nonce = this.nanoid(21); |
| const sign = await this.generateSign(timestamp, payload, nonce); |
|
|
| const headers = { |
| 'Origin': 'https://ai.dangbei.com', |
| 'Referer': 'https://ai.dangbei.com/', |
| 'User-Agent': userAgent, |
| 'deviceId': deviceId, |
| 'nonce': nonce, |
| 'sign': sign, |
| 'timestamp': timestamp, |
| 'Content-Type': 'application/json', |
| }; |
|
|
| try { |
| const response = await fetch(`${API_DOMAIN}/ai-search/chatApi/v1/chat`, { |
| method: 'POST', |
| headers, |
| body: JSON.stringify(payload), |
| }); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| console.error('HTTP Error:', response.status, error); |
| yield { error: `HTTP ${response.status}: ${error}` }; |
| return; |
| } |
|
|
| const reader = response.body!.getReader(); |
| const decoder = new TextDecoder(); |
| let buffer = ''; |
| let cardMessages: string[] = []; |
|
|
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
|
|
| buffer += decoder.decode(value, { stream: true }); |
| const lines = buffer.split('\n'); |
| buffer = lines.pop() || ''; |
|
|
| for (const line of lines) { |
| if (!line.startsWith(this.dataPrefix)) continue; |
|
|
| try { |
| const data = JSON.parse(line.slice(this.dataPrefix.length)); |
| if (data.type === 'answer') { |
| const content = data.content; |
| const contentType = data.content_type; |
|
|
| if (thinkingState.thinking === -1 && contentType === 'thinking') { |
| thinkingState.thinking = 0; |
| yield { choices: [{ delta: { content: '<think>\n\n' }, finish_reason: null }] }; |
| } else if (thinkingState.thinking === 0 && contentType === 'text') { |
| thinkingState.thinking = 1; |
| yield { choices: [{ delta: { content: '\n' }, finish_reason: null }] }; |
| yield { choices: [{ delta: { content: '</think>' }, finish_reason: null }] }; |
| yield { choices: [{ delta: { content: '\n\n' }, finish_reason: null }] }; |
| } |
|
|
| if (contentType === 'card') { |
| try { |
| const cardContent = JSON.parse(content); |
| const cardItems = cardContent.cardInfo.cardItems; |
| let markdownOutput = '\n\n---\n\n'; |
|
|
| const searchKeywords = cardItems.find((item: any) => item.type === '2001'); |
| if (searchKeywords) { |
| const keywords = JSON.parse(searchKeywords.content); |
| markdownOutput += `搜索关键字:${keywords.join('; ')}\n\n`; |
| } |
|
|
| const searchResults = cardItems.find((item: any) => item.type === '2002'); |
| if (searchResults) { |
| const results = JSON.parse(searchResults.content); |
| markdownOutput += `共找到 ${results.length} 个搜索结果:\n\n`; |
| |
| results.forEach((result: any) => { |
| markdownOutput += `[${result.idIndex}] [${result.name}](${result.url}) 来源:${result.siteName}\n`; |
| }); |
| } |
|
|
| cardMessages.push(markdownOutput); |
| } catch (e) { |
| console.error('Error processing card:', e); |
| } |
| } |
|
|
| if (content && (contentType === 'text' || contentType === 'thinking')) { |
| yield { choices: [{ delta: { content }, finish_reason: null }] }; |
| } |
| } |
| } catch (e) { |
| console.error('Parse error:', e, 'Line:', line); |
| yield { error: `JSONDecodeError: ${(e as Error).message}` }; |
| return; |
| } |
| } |
| } |
|
|
| if (cardMessages.length > 0) { |
| yield { choices: [{ delta: { content: cardMessages.join('') }, finish_reason: null }] }; |
| } |
|
|
| yield { |
| choices: [{ |
| delta: { |
| meta: { |
| device_id: deviceId, |
| conversation_id: conversationId |
| } |
| }, |
| finish_reason: null |
| }] |
| }; |
|
|
| } catch (e) { |
| console.error('Error in pipe:', e); |
| yield { error: `${(e as Error).name}: ${(e as Error).message}` }; |
| } |
| } |
|
|
| nanoid(size = 21) { |
| const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; |
| const bytes = new Uint8Array(size); |
| crypto.getRandomValues(bytes); |
| return Array.from(bytes).reverse().map(b => urlAlphabet[b & 63]).join(''); |
| } |
|
|
| async generateSign(timestamp: string, payload: any, nonce: string) { |
| const payloadStr = JSON.stringify(payload); |
| const signStr = `${timestamp}${payloadStr}${nonce}`; |
| console.log('Sign string:', signStr); |
| |
| |
| const sign = new Md5() |
| .update(signStr) |
| .toString() |
| .toUpperCase(); |
| |
| console.log('Generated sign:', sign); |
| return sign; |
| } |
| } |
|
|
| const pipe = new Pipe(); |
|
|
| |
| function verifyApiKey(request: Request) { |
| const authorization = request.headers.get('Authorization'); |
| |
| if (!VALID_API_KEY) { |
| return new Response(JSON.stringify({ error: 'API key not configured' }), { |
| status: 500, |
| headers: { 'Content-Type': 'application/json' }, |
| }); |
| } |
| |
| if (!authorization) { |
| return new Response(JSON.stringify({ error: 'Missing API key' }), { |
| status: 401, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }); |
| } |
|
|
| const apiKey = authorization.replace('Bearer ', '').trim(); |
| if (apiKey !== VALID_API_KEY) { |
| return new Response(JSON.stringify({ error: 'Invalid API key' }), { |
| status: 401, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }); |
| } |
|
|
| return null; |
| } |
|
|
| async function handleRequest(request: Request) { |
| const url = new URL(request.url); |
|
|
| |
| if (request.method === 'GET' && url.pathname === '/') { |
| return new Response("it's work!", { |
| headers: { |
| 'Content-Type': 'text/plain', |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }); |
| } |
|
|
| if (request.method === 'OPTIONS') { |
| return new Response(null, { |
| headers: { |
| 'Access-Control-Allow-Origin': '*', |
| 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', |
| 'Access-Control-Allow-Headers': 'Content-Type, Authorization', |
| }, |
| }); |
| } |
|
|
| |
| const authError = verifyApiKey(request); |
| if (authError) return authError; |
|
|
| if (request.method === 'GET' && url.pathname === '/v1/models') { |
| const currentTime = Math.floor(Date.now() / 1000); |
| return new Response(JSON.stringify({ |
| object: 'list', |
| data: [ |
| |
| { |
| id: 'DeepSeek-R1', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library' |
| }, |
| { |
| id: 'DeepSeek-V3', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library' |
| }, |
| { |
| id: 'Doubao', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library' |
| }, |
| { |
| id: 'Qwen', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library' |
| }, |
| { |
| id: 'Glm3', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library' |
| }, |
| { |
| id: 'Moonshot_v1', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library' |
| }, |
| |
| { |
| id: 'DeepSeek-R1-Search', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library', |
| features: ['online_search'] |
| }, |
| { |
| id: 'DeepSeek-V3-Search', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library', |
| features: ['online_search'] |
| }, |
| { |
| id: 'Doubao-Search', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library', |
| features: ['online_search'] |
| }, |
| { |
| id: 'Qwen-Search', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library', |
| features: ['online_search'] |
| }, |
| { |
| id: 'Glm3-Search', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library', |
| features: ['online_search'] |
| }, |
| { |
| id: 'Moonshot_v1-Search', |
| object: 'model', |
| created: currentTime, |
| owned_by: 'library', |
| features: ['online_search'] |
| } |
| ] |
| }), { |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }); |
| } |
|
|
| if (request.method === 'POST' && url.pathname === '/v1/chat/completions') { |
| const body = await request.json(); |
| const isStream = body.stream || false; |
|
|
| if (isStream) { |
| const stream = new ReadableStream({ |
| async start(controller) { |
| try { |
| for await (const chunk of pipe.pipe(body)) { |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); |
| } |
| controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n')); |
| controller.close(); |
| } catch (e) { |
| console.error('Error in stream:', e); |
| controller.error(e); |
| } |
| }, |
| }); |
|
|
| return new Response(stream, { |
| headers: { |
| 'Content-Type': 'text/event-stream', |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive', |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }); |
| } |
|
|
| if (!isStream) { |
| let content = ''; |
| let meta = null; |
| let thinking_content: string[] = []; |
| let is_thinking = false; |
| |
| try { |
| for await (const chunk of pipe.pipe(body)) { |
| if (chunk.choices?.[0]?.delta?.content) { |
| const content_chunk = chunk.choices[0].delta.content; |
| if (content_chunk === '<think>\n\n') { |
| is_thinking = true; |
| } else if (content_chunk === '\n</think>\n\n') { |
| is_thinking = false; |
| } else if (is_thinking) { |
| thinking_content.push(content_chunk); |
| } else { |
| content += content_chunk; |
| } |
| } |
| if (chunk.choices?.[0]?.delta?.meta) { |
| meta = chunk.choices[0].delta.meta; |
| } |
| } |
|
|
| |
| const reasoningContent = thinking_content.join(''); |
| |
| return new Response(JSON.stringify({ |
| id: crypto.randomUUID(), |
| object: 'chat.completion', |
| created: Math.floor(Date.now() / 1000), |
| model: body.model, |
| choices: [{ |
| message: { |
| role: 'assistant', |
| reasoning_content: reasoningContent ? `<think>\n${reasoningContent}\n</think>` : '', |
| content: content.trim(), |
| meta: meta |
| }, |
| finish_reason: 'stop' |
| }] |
| } as NonStreamResponse), { |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }); |
| } catch (e) { |
| console.error('Error processing chat request:', e); |
| return new Response(JSON.stringify({ error: 'Internal Server Error' }), { |
| status: 500, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }); |
| } |
| } |
| } |
|
|
| return new Response('Not Found', { status: 404 }); |
| } |
|
|
| serve(handleRequest, { port: 7860 }); |
|
|
| interface Message { |
| role: string; |
| content: string; |
| } |
|
|
| interface ChatRequest { |
| model: string; |
| messages: Message[]; |
| stream: boolean; |
| temperature?: number; |
| top_p?: number; |
| n?: number; |
| max_tokens?: number; |
| presence_penalty?: number; |
| frequency_penalty?: number; |
| user?: string; |
| } |
|
|
| interface DeltaContent { |
| content?: string; |
| meta?: { |
| device_id: string; |
| conversation_id: string; |
| }; |
| } |
|
|
| interface Choice { |
| delta: DeltaContent; |
| finish_reason: string | null; |
| } |
|
|
| interface StreamResponse { |
| choices?: Choice[]; |
| error?: string; |
| } |
|
|
| interface NonStreamResponse { |
| id: string; |
| object: string; |
| created: number; |
| model: string; |
| choices: Array<{ |
| message: { |
| role: string; |
| reasoning_content: string; |
| content: string; |
| meta: { |
| device_id: string; |
| conversation_id: string; |
| }; |
| }; |
| finish_reason: string; |
| }>; |
| } |