Create main.ts
Browse files
main.ts
ADDED
@@ -0,0 +1,671 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { serve } from "https://deno.land/[email protected]/http/server.ts";
|
2 |
+
import { Md5 } from "https://deno.land/[email protected]/hash/md5.ts";
|
3 |
+
|
4 |
+
const API_DOMAIN = 'https://ai-api.dangbei.net';
|
5 |
+
const USER_AGENTS = [
|
6 |
+
'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',
|
7 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
8 |
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
9 |
+
'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',
|
10 |
+
'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'
|
11 |
+
];
|
12 |
+
const VALID_API_KEY = Deno.env.get('VALID_API_KEY');
|
13 |
+
const MAX_CONVERSATIONS_PER_DEVICE = 10; // 每个设备最多创建的会话数
|
14 |
+
|
15 |
+
class ChatManage {
|
16 |
+
private currentDeviceId: string | null = null;
|
17 |
+
private currentConversationId: string | null = null;
|
18 |
+
private conversationCount = 0;
|
19 |
+
private currentUserAgent: string;
|
20 |
+
|
21 |
+
constructor() {
|
22 |
+
this.currentUserAgent = this.getRandomUserAgent();
|
23 |
+
}
|
24 |
+
|
25 |
+
private getRandomUserAgent(): string {
|
26 |
+
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
|
27 |
+
}
|
28 |
+
|
29 |
+
getOrCreateIds(forceNew = false) {
|
30 |
+
let newDeviceId = this.currentDeviceId;
|
31 |
+
let newConversationId = this.currentConversationId;
|
32 |
+
|
33 |
+
if (forceNew || !newDeviceId || this.conversationCount >= MAX_CONVERSATIONS_PER_DEVICE) {
|
34 |
+
newDeviceId = this.generateDeviceId();
|
35 |
+
newConversationId = null;
|
36 |
+
this.conversationCount = 0;
|
37 |
+
// 在生成新设备ID时更新 User-Agent
|
38 |
+
this.currentUserAgent = this.getRandomUserAgent();
|
39 |
+
}
|
40 |
+
|
41 |
+
this.currentDeviceId = newDeviceId;
|
42 |
+
this.currentConversationId = newConversationId;
|
43 |
+
|
44 |
+
return {
|
45 |
+
deviceId: newDeviceId,
|
46 |
+
conversationId: newConversationId,
|
47 |
+
userAgent: this.currentUserAgent
|
48 |
+
};
|
49 |
+
}
|
50 |
+
|
51 |
+
updateConversationId(conversationId: string) {
|
52 |
+
this.currentConversationId = conversationId;
|
53 |
+
this.conversationCount++;
|
54 |
+
}
|
55 |
+
|
56 |
+
generateDeviceId() {
|
57 |
+
const uuid = crypto.randomUUID();
|
58 |
+
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
|
59 |
+
const nanoid = Array.from(crypto.getRandomValues(new Uint8Array(20)))
|
60 |
+
.map(b => urlAlphabet[b % urlAlphabet.length])
|
61 |
+
.join('');
|
62 |
+
return `${uuid.replace(/-/g, '')}_${nanoid}`;
|
63 |
+
}
|
64 |
+
}
|
65 |
+
|
66 |
+
class Pipe {
|
67 |
+
private dataPrefix = 'data:';
|
68 |
+
private chatManage = new ChatManage();
|
69 |
+
private searchModels: Record<string, string> = {
|
70 |
+
'DeepSeek-R1-Search': 'deepseek',
|
71 |
+
'DeepSeek-V3-Search': 'deepseek',
|
72 |
+
'Doubao-Search': 'doubao',
|
73 |
+
'Qwen-Search': 'qwen'
|
74 |
+
};
|
75 |
+
|
76 |
+
// 创建新的会话
|
77 |
+
async _create_conversation(deviceId: string) {
|
78 |
+
const { userAgent } = this.chatManage.getOrCreateIds(false);
|
79 |
+
const payload = { botCode: "AI_SEARCH" };
|
80 |
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
81 |
+
const nonce = this.nanoid(21);
|
82 |
+
const sign = await this.generateSign(timestamp, payload, nonce);
|
83 |
+
|
84 |
+
const headers = {
|
85 |
+
"Origin": "https://ai.dangbei.com",
|
86 |
+
"Referer": "https://ai.dangbei.com/",
|
87 |
+
"User-Agent": userAgent,
|
88 |
+
"deviceId": deviceId,
|
89 |
+
"nonce": nonce,
|
90 |
+
"sign": sign,
|
91 |
+
"timestamp": timestamp,
|
92 |
+
"Content-Type": "application/json"
|
93 |
+
};
|
94 |
+
|
95 |
+
try {
|
96 |
+
console.log('Creating conversation with:', {
|
97 |
+
url: `${API_DOMAIN}/ai-search/conversationApi/v1/create`,
|
98 |
+
headers,
|
99 |
+
payload
|
100 |
+
});
|
101 |
+
|
102 |
+
const response = await fetch(`${API_DOMAIN}/ai-search/conversationApi/v1/create`, {
|
103 |
+
method: 'POST',
|
104 |
+
headers,
|
105 |
+
body: JSON.stringify(payload),
|
106 |
+
});
|
107 |
+
|
108 |
+
console.log('Response status:', response.status);
|
109 |
+
const responseText = await response.text();
|
110 |
+
console.log('Response body:', responseText);
|
111 |
+
|
112 |
+
if (response.ok) {
|
113 |
+
try {
|
114 |
+
const data = JSON.parse(responseText);
|
115 |
+
if (data.success) {
|
116 |
+
console.log('Successfully created conversation:', data.data.conversationId);
|
117 |
+
return data.data.conversationId;
|
118 |
+
} else {
|
119 |
+
console.error('API returned success: false:', data);
|
120 |
+
}
|
121 |
+
} catch (e) {
|
122 |
+
console.error('Failed to parse response:', e);
|
123 |
+
}
|
124 |
+
} else {
|
125 |
+
console.error('HTTP error:', response.status, responseText);
|
126 |
+
}
|
127 |
+
} catch (e) {
|
128 |
+
console.error('Error creating conversation:', e);
|
129 |
+
}
|
130 |
+
return null;
|
131 |
+
}
|
132 |
+
|
133 |
+
// 新增方法:构建完整提示
|
134 |
+
_buildFullPrompt(messages: any[]): string {
|
135 |
+
if (!messages || messages.length === 0) {
|
136 |
+
return '';
|
137 |
+
}
|
138 |
+
|
139 |
+
let systemPrompt = '';
|
140 |
+
const history: string[] = [];
|
141 |
+
let lastUserMessage = '';
|
142 |
+
|
143 |
+
for (const msg of messages) {
|
144 |
+
if (msg.role === 'system' && !systemPrompt) {
|
145 |
+
systemPrompt = msg.content;
|
146 |
+
} else if (msg.role === 'user') {
|
147 |
+
history.push(`user: ${msg.content}`);
|
148 |
+
lastUserMessage = msg.content;
|
149 |
+
} else if (msg.role === 'assistant') {
|
150 |
+
history.push(`assistant: ${msg.content}`);
|
151 |
+
}
|
152 |
+
}
|
153 |
+
|
154 |
+
const parts: string[] = [];
|
155 |
+
if (systemPrompt) {
|
156 |
+
parts.push(`[System Prompt]\n${systemPrompt}`);
|
157 |
+
}
|
158 |
+
if (history.length > 1) {
|
159 |
+
parts.push(`[Chat History]\n${history.slice(0, -1).join('\n')}`);
|
160 |
+
}
|
161 |
+
parts.push(`[Question]\n${lastUserMessage}`);
|
162 |
+
|
163 |
+
return parts.join('\n\n');
|
164 |
+
}
|
165 |
+
|
166 |
+
async* pipe(body: any) {
|
167 |
+
const thinkingState = { thinking: -1 };
|
168 |
+
|
169 |
+
// Build full prompt
|
170 |
+
const fullPrompt = this._buildFullPrompt(body.messages);
|
171 |
+
|
172 |
+
// Check if we need to force new conversation
|
173 |
+
let forceNew = false;
|
174 |
+
const messages = body.messages;
|
175 |
+
if (messages.length === 1) {
|
176 |
+
forceNew = true;
|
177 |
+
} else if (messages.length >= 2) {
|
178 |
+
const lastTwo = messages.slice(-2);
|
179 |
+
if (lastTwo[0].role === 'user' && lastTwo[1].role === 'user') {
|
180 |
+
forceNew = true;
|
181 |
+
}
|
182 |
+
}
|
183 |
+
|
184 |
+
// Get or create device ID and conversation ID with User-Agent
|
185 |
+
const { deviceId, conversationId: storedConversationId, userAgent } = this.chatManage.getOrCreateIds(forceNew);
|
186 |
+
let conversationId = storedConversationId;
|
187 |
+
|
188 |
+
// Create new conversation if needed
|
189 |
+
if (!conversationId) {
|
190 |
+
conversationId = await this._create_conversation(deviceId);
|
191 |
+
if (!conversationId) {
|
192 |
+
yield { error: 'Failed to create conversation' };
|
193 |
+
return;
|
194 |
+
}
|
195 |
+
this.chatManage.updateConversationId(conversationId);
|
196 |
+
}
|
197 |
+
|
198 |
+
// Model name handling
|
199 |
+
let modelName;
|
200 |
+
const isSearchModel = body.model.endsWith('-Search');
|
201 |
+
if (isSearchModel) {
|
202 |
+
modelName = this.searchModels[body.model] || body.model.replace('-Search', '').toLowerCase();
|
203 |
+
} else {
|
204 |
+
const isDeepSeekModel = ['DeepSeek-R1', 'DeepSeek-V3'].includes(body.model);
|
205 |
+
modelName = isDeepSeekModel ? 'deepseek' : body.model.toLowerCase();
|
206 |
+
}
|
207 |
+
|
208 |
+
// 确定 userAction 参数
|
209 |
+
let userAction = '';
|
210 |
+
if (body.model.includes('DeepSeek-R1')) {
|
211 |
+
userAction = 'deep';
|
212 |
+
}
|
213 |
+
if (isSearchModel) {
|
214 |
+
userAction = userAction ? `${userAction},online` : 'online';
|
215 |
+
}
|
216 |
+
|
217 |
+
const payload = {
|
218 |
+
stream: true,
|
219 |
+
botCode: 'AI_SEARCH',
|
220 |
+
userAction,
|
221 |
+
model: modelName,
|
222 |
+
conversationId: conversationId,
|
223 |
+
question: fullPrompt,
|
224 |
+
};
|
225 |
+
|
226 |
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
227 |
+
const nonce = this.nanoid(21);
|
228 |
+
const sign = await this.generateSign(timestamp, payload, nonce);
|
229 |
+
|
230 |
+
const headers = {
|
231 |
+
'Origin': 'https://ai.dangbei.com',
|
232 |
+
'Referer': 'https://ai.dangbei.com/',
|
233 |
+
'User-Agent': userAgent,
|
234 |
+
'deviceId': deviceId,
|
235 |
+
'nonce': nonce,
|
236 |
+
'sign': sign,
|
237 |
+
'timestamp': timestamp,
|
238 |
+
'Content-Type': 'application/json',
|
239 |
+
};
|
240 |
+
|
241 |
+
try {
|
242 |
+
const response = await fetch(`${API_DOMAIN}/ai-search/chatApi/v1/chat`, {
|
243 |
+
method: 'POST',
|
244 |
+
headers,
|
245 |
+
body: JSON.stringify(payload),
|
246 |
+
});
|
247 |
+
|
248 |
+
if (!response.ok) {
|
249 |
+
const error = await response.text();
|
250 |
+
console.error('HTTP Error:', response.status, error);
|
251 |
+
yield { error: `HTTP ${response.status}: ${error}` };
|
252 |
+
return;
|
253 |
+
}
|
254 |
+
|
255 |
+
const reader = response.body!.getReader();
|
256 |
+
const decoder = new TextDecoder();
|
257 |
+
let buffer = '';
|
258 |
+
let cardMessages: string[] = [];
|
259 |
+
|
260 |
+
while (true) {
|
261 |
+
const { done, value } = await reader.read();
|
262 |
+
if (done) break;
|
263 |
+
|
264 |
+
buffer += decoder.decode(value, { stream: true });
|
265 |
+
const lines = buffer.split('\n');
|
266 |
+
buffer = lines.pop() || '';
|
267 |
+
|
268 |
+
for (const line of lines) {
|
269 |
+
if (!line.startsWith(this.dataPrefix)) continue;
|
270 |
+
|
271 |
+
try {
|
272 |
+
const data = JSON.parse(line.slice(this.dataPrefix.length));
|
273 |
+
if (data.type === 'answer') {
|
274 |
+
const content = data.content;
|
275 |
+
const contentType = data.content_type;
|
276 |
+
|
277 |
+
if (thinkingState.thinking === -1 && contentType === 'thinking') {
|
278 |
+
thinkingState.thinking = 0;
|
279 |
+
yield { choices: [{ delta: { content: '<think>\n\n' }, finish_reason: null }] };
|
280 |
+
} else if (thinkingState.thinking === 0 && contentType === 'text') {
|
281 |
+
thinkingState.thinking = 1;
|
282 |
+
yield { choices: [{ delta: { content: '\n' }, finish_reason: null }] };
|
283 |
+
yield { choices: [{ delta: { content: '</think>' }, finish_reason: null }] };
|
284 |
+
yield { choices: [{ delta: { content: '\n\n' }, finish_reason: null }] };
|
285 |
+
}
|
286 |
+
|
287 |
+
if (contentType === 'card') {
|
288 |
+
try {
|
289 |
+
const cardContent = JSON.parse(content);
|
290 |
+
const cardItems = cardContent.cardInfo.cardItems;
|
291 |
+
let markdownOutput = '\n\n---\n\n';
|
292 |
+
|
293 |
+
const searchKeywords = cardItems.find((item: any) => item.type === '2001');
|
294 |
+
if (searchKeywords) {
|
295 |
+
const keywords = JSON.parse(searchKeywords.content);
|
296 |
+
markdownOutput += `搜索关键字:${keywords.join('; ')}\n\n`;
|
297 |
+
}
|
298 |
+
|
299 |
+
const searchResults = cardItems.find((item: any) => item.type === '2002');
|
300 |
+
if (searchResults) {
|
301 |
+
const results = JSON.parse(searchResults.content);
|
302 |
+
markdownOutput += `共找到 ${results.length} 个搜索结果:\n\n`;
|
303 |
+
|
304 |
+
results.forEach((result: any) => {
|
305 |
+
markdownOutput += `[${result.idIndex}] [${result.name}](${result.url}) 来源:${result.siteName}\n`;
|
306 |
+
});
|
307 |
+
}
|
308 |
+
|
309 |
+
cardMessages.push(markdownOutput);
|
310 |
+
} catch (e) {
|
311 |
+
console.error('Error processing card:', e);
|
312 |
+
}
|
313 |
+
}
|
314 |
+
|
315 |
+
if (content && (contentType === 'text' || contentType === 'thinking')) {
|
316 |
+
yield { choices: [{ delta: { content }, finish_reason: null }] };
|
317 |
+
}
|
318 |
+
}
|
319 |
+
} catch (e) {
|
320 |
+
console.error('Parse error:', e, 'Line:', line);
|
321 |
+
yield { error: `JSONDecodeError: ${(e as Error).message}` };
|
322 |
+
return;
|
323 |
+
}
|
324 |
+
}
|
325 |
+
}
|
326 |
+
|
327 |
+
if (cardMessages.length > 0) {
|
328 |
+
yield { choices: [{ delta: { content: cardMessages.join('') }, finish_reason: null }] };
|
329 |
+
}
|
330 |
+
|
331 |
+
yield {
|
332 |
+
choices: [{
|
333 |
+
delta: {
|
334 |
+
meta: {
|
335 |
+
device_id: deviceId,
|
336 |
+
conversation_id: conversationId
|
337 |
+
}
|
338 |
+
},
|
339 |
+
finish_reason: null
|
340 |
+
}]
|
341 |
+
};
|
342 |
+
|
343 |
+
} catch (e) {
|
344 |
+
console.error('Error in pipe:', e);
|
345 |
+
yield { error: `${(e as Error).name}: ${(e as Error).message}` };
|
346 |
+
}
|
347 |
+
}
|
348 |
+
|
349 |
+
nanoid(size = 21) {
|
350 |
+
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
|
351 |
+
const bytes = new Uint8Array(size);
|
352 |
+
crypto.getRandomValues(bytes);
|
353 |
+
return Array.from(bytes).reverse().map(b => urlAlphabet[b & 63]).join('');
|
354 |
+
}
|
355 |
+
|
356 |
+
async generateSign(timestamp: string, payload: any, nonce: string) {
|
357 |
+
const payloadStr = JSON.stringify(payload);
|
358 |
+
const signStr = `${timestamp}${payloadStr}${nonce}`;
|
359 |
+
console.log('Sign string:', signStr);
|
360 |
+
|
361 |
+
// 使用 Deno 标准库的 MD5
|
362 |
+
const sign = new Md5()
|
363 |
+
.update(signStr)
|
364 |
+
.toString()
|
365 |
+
.toUpperCase();
|
366 |
+
|
367 |
+
console.log('Generated sign:', sign);
|
368 |
+
return sign;
|
369 |
+
}
|
370 |
+
}
|
371 |
+
|
372 |
+
const pipe = new Pipe();
|
373 |
+
|
374 |
+
// 验证 API 密钥
|
375 |
+
function verifyApiKey(request: Request) {
|
376 |
+
const authorization = request.headers.get('Authorization');
|
377 |
+
if (!authorization) {
|
378 |
+
return new Response(JSON.stringify({ error: 'Missing API key' }), {
|
379 |
+
status: 401,
|
380 |
+
headers: {
|
381 |
+
'Content-Type': 'application/json',
|
382 |
+
'Access-Control-Allow-Origin': '*',
|
383 |
+
},
|
384 |
+
});
|
385 |
+
}
|
386 |
+
|
387 |
+
const apiKey = authorization.replace('Bearer ', '').trim();
|
388 |
+
if (apiKey !== VALID_API_KEY) {
|
389 |
+
return new Response(JSON.stringify({ error: 'Invalid API key' }), {
|
390 |
+
status: 401,
|
391 |
+
headers: {
|
392 |
+
'Content-Type': 'application/json',
|
393 |
+
'Access-Control-Allow-Origin': '*',
|
394 |
+
},
|
395 |
+
});
|
396 |
+
}
|
397 |
+
|
398 |
+
return null;
|
399 |
+
}
|
400 |
+
|
401 |
+
async function handleRequest(request: Request) {
|
402 |
+
const url = new URL(request.url);
|
403 |
+
|
404 |
+
// 添加根路径处理
|
405 |
+
if (request.method === 'GET' && url.pathname === '/') {
|
406 |
+
return new Response("it's work!", {
|
407 |
+
headers: {
|
408 |
+
'Content-Type': 'text/plain',
|
409 |
+
'Access-Control-Allow-Origin': '*',
|
410 |
+
},
|
411 |
+
});
|
412 |
+
}
|
413 |
+
|
414 |
+
if (request.method === 'OPTIONS') {
|
415 |
+
return new Response(null, {
|
416 |
+
headers: {
|
417 |
+
'Access-Control-Allow-Origin': '*',
|
418 |
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
419 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
420 |
+
},
|
421 |
+
});
|
422 |
+
}
|
423 |
+
|
424 |
+
// 验证 API 密钥(除了 OPTIONS 请求)
|
425 |
+
const authError = verifyApiKey(request);
|
426 |
+
if (authError) return authError;
|
427 |
+
|
428 |
+
if (request.method === 'GET' && url.pathname === '/v1/models') {
|
429 |
+
const currentTime = Math.floor(Date.now() / 1000);
|
430 |
+
return new Response(JSON.stringify({
|
431 |
+
object: 'list',
|
432 |
+
data: [
|
433 |
+
// Original models
|
434 |
+
{
|
435 |
+
id: 'DeepSeek-R1',
|
436 |
+
object: 'model',
|
437 |
+
created: currentTime,
|
438 |
+
owned_by: 'library'
|
439 |
+
},
|
440 |
+
{
|
441 |
+
id: 'DeepSeek-V3',
|
442 |
+
object: 'model',
|
443 |
+
created: currentTime,
|
444 |
+
owned_by: 'library'
|
445 |
+
},
|
446 |
+
{
|
447 |
+
id: 'Doubao',
|
448 |
+
object: 'model',
|
449 |
+
created: currentTime,
|
450 |
+
owned_by: 'library'
|
451 |
+
},
|
452 |
+
{
|
453 |
+
id: 'Qwen',
|
454 |
+
object: 'model',
|
455 |
+
created: currentTime,
|
456 |
+
owned_by: 'library'
|
457 |
+
},
|
458 |
+
{
|
459 |
+
id: 'Glm3',
|
460 |
+
object: 'model',
|
461 |
+
created: currentTime,
|
462 |
+
owned_by: 'library'
|
463 |
+
},
|
464 |
+
{
|
465 |
+
id: 'Moonshot_v1',
|
466 |
+
object: 'model',
|
467 |
+
created: currentTime,
|
468 |
+
owned_by: 'library'
|
469 |
+
},
|
470 |
+
// Search-enabled models
|
471 |
+
{
|
472 |
+
id: 'DeepSeek-R1-Search',
|
473 |
+
object: 'model',
|
474 |
+
created: currentTime,
|
475 |
+
owned_by: 'library',
|
476 |
+
features: ['online_search']
|
477 |
+
},
|
478 |
+
{
|
479 |
+
id: 'DeepSeek-V3-Search',
|
480 |
+
object: 'model',
|
481 |
+
created: currentTime,
|
482 |
+
owned_by: 'library',
|
483 |
+
features: ['online_search']
|
484 |
+
},
|
485 |
+
{
|
486 |
+
id: 'Doubao-Search',
|
487 |
+
object: 'model',
|
488 |
+
created: currentTime,
|
489 |
+
owned_by: 'library',
|
490 |
+
features: ['online_search']
|
491 |
+
},
|
492 |
+
{
|
493 |
+
id: 'Qwen-Search',
|
494 |
+
object: 'model',
|
495 |
+
created: currentTime,
|
496 |
+
owned_by: 'library',
|
497 |
+
features: ['online_search']
|
498 |
+
},
|
499 |
+
{
|
500 |
+
id: 'Glm3-Search',
|
501 |
+
object: 'model',
|
502 |
+
created: currentTime,
|
503 |
+
owned_by: 'library',
|
504 |
+
features: ['online_search']
|
505 |
+
},
|
506 |
+
{
|
507 |
+
id: 'Moonshot_v1-Search',
|
508 |
+
object: 'model',
|
509 |
+
created: currentTime,
|
510 |
+
owned_by: 'library',
|
511 |
+
features: ['online_search']
|
512 |
+
}
|
513 |
+
]
|
514 |
+
}), {
|
515 |
+
headers: {
|
516 |
+
'Content-Type': 'application/json',
|
517 |
+
'Access-Control-Allow-Origin': '*',
|
518 |
+
},
|
519 |
+
});
|
520 |
+
}
|
521 |
+
|
522 |
+
if (request.method === 'POST' && url.pathname === '/v1/chat/completions') {
|
523 |
+
const body = await request.json();
|
524 |
+
const isStream = body.stream || false;
|
525 |
+
|
526 |
+
if (isStream) {
|
527 |
+
const stream = new ReadableStream({
|
528 |
+
async start(controller) {
|
529 |
+
try {
|
530 |
+
for await (const chunk of pipe.pipe(body)) {
|
531 |
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
532 |
+
}
|
533 |
+
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
534 |
+
controller.close();
|
535 |
+
} catch (e) {
|
536 |
+
console.error('Error in stream:', e);
|
537 |
+
controller.error(e);
|
538 |
+
}
|
539 |
+
},
|
540 |
+
});
|
541 |
+
|
542 |
+
return new Response(stream, {
|
543 |
+
headers: {
|
544 |
+
'Content-Type': 'text/event-stream',
|
545 |
+
'Cache-Control': 'no-cache',
|
546 |
+
'Connection': 'keep-alive',
|
547 |
+
'Access-Control-Allow-Origin': '*',
|
548 |
+
},
|
549 |
+
});
|
550 |
+
}
|
551 |
+
|
552 |
+
if (!isStream) {
|
553 |
+
let content = '';
|
554 |
+
let meta = null;
|
555 |
+
let thinking_content: string[] = [];
|
556 |
+
let is_thinking = false;
|
557 |
+
|
558 |
+
try {
|
559 |
+
for await (const chunk of pipe.pipe(body)) {
|
560 |
+
if (chunk.choices?.[0]?.delta?.content) {
|
561 |
+
const content_chunk = chunk.choices[0].delta.content;
|
562 |
+
if (content_chunk === '<think>\n\n') {
|
563 |
+
is_thinking = true;
|
564 |
+
} else if (content_chunk === '\n</think>\n\n') {
|
565 |
+
is_thinking = false;
|
566 |
+
} else if (is_thinking) {
|
567 |
+
thinking_content.push(content_chunk);
|
568 |
+
} else {
|
569 |
+
content += content_chunk;
|
570 |
+
}
|
571 |
+
}
|
572 |
+
if (chunk.choices?.[0]?.delta?.meta) {
|
573 |
+
meta = chunk.choices[0].delta.meta;
|
574 |
+
}
|
575 |
+
}
|
576 |
+
|
577 |
+
// 处理思考内容
|
578 |
+
const reasoningContent = thinking_content.join('');
|
579 |
+
|
580 |
+
return new Response(JSON.stringify({
|
581 |
+
id: crypto.randomUUID(),
|
582 |
+
object: 'chat.completion',
|
583 |
+
created: Math.floor(Date.now() / 1000),
|
584 |
+
model: body.model,
|
585 |
+
choices: [{
|
586 |
+
message: {
|
587 |
+
role: 'assistant',
|
588 |
+
reasoning_content: reasoningContent ? `<think>\n${reasoningContent}\n</think>` : '',
|
589 |
+
content: content.trim(),
|
590 |
+
meta: meta
|
591 |
+
},
|
592 |
+
finish_reason: 'stop'
|
593 |
+
}]
|
594 |
+
} as NonStreamResponse), {
|
595 |
+
headers: {
|
596 |
+
'Content-Type': 'application/json',
|
597 |
+
'Access-Control-Allow-Origin': '*',
|
598 |
+
},
|
599 |
+
});
|
600 |
+
} catch (e) {
|
601 |
+
console.error('Error processing chat request:', e);
|
602 |
+
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
|
603 |
+
status: 500,
|
604 |
+
headers: {
|
605 |
+
'Content-Type': 'application/json',
|
606 |
+
'Access-Control-Allow-Origin': '*',
|
607 |
+
},
|
608 |
+
});
|
609 |
+
}
|
610 |
+
}
|
611 |
+
}
|
612 |
+
|
613 |
+
return new Response('Not Found', { status: 404 });
|
614 |
+
}
|
615 |
+
|
616 |
+
serve(handleRequest, { port: 8787 });
|
617 |
+
|
618 |
+
interface Message {
|
619 |
+
role: string;
|
620 |
+
content: string;
|
621 |
+
}
|
622 |
+
|
623 |
+
interface ChatRequest {
|
624 |
+
model: string;
|
625 |
+
messages: Message[];
|
626 |
+
stream: boolean;
|
627 |
+
temperature?: number;
|
628 |
+
top_p?: number;
|
629 |
+
n?: number;
|
630 |
+
max_tokens?: number;
|
631 |
+
presence_penalty?: number;
|
632 |
+
frequency_penalty?: number;
|
633 |
+
user?: string;
|
634 |
+
}
|
635 |
+
|
636 |
+
interface DeltaContent {
|
637 |
+
content?: string;
|
638 |
+
meta?: {
|
639 |
+
device_id: string;
|
640 |
+
conversation_id: string;
|
641 |
+
};
|
642 |
+
}
|
643 |
+
|
644 |
+
interface Choice {
|
645 |
+
delta: DeltaContent;
|
646 |
+
finish_reason: string | null;
|
647 |
+
}
|
648 |
+
|
649 |
+
interface StreamResponse {
|
650 |
+
choices?: Choice[];
|
651 |
+
error?: string;
|
652 |
+
}
|
653 |
+
|
654 |
+
interface NonStreamResponse {
|
655 |
+
id: string;
|
656 |
+
object: string;
|
657 |
+
created: number;
|
658 |
+
model: string;
|
659 |
+
choices: Array<{
|
660 |
+
message: {
|
661 |
+
role: string;
|
662 |
+
reasoning_content: string;
|
663 |
+
content: string;
|
664 |
+
meta: {
|
665 |
+
device_id: string;
|
666 |
+
conversation_id: string;
|
667 |
+
};
|
668 |
+
};
|
669 |
+
finish_reason: string;
|
670 |
+
}>;
|
671 |
+
}
|