| | 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; |
| | }>; |
| | } |