fxlinux commited on
Commit
bc0be9c
·
verified ·
1 Parent(s): f373e34

Upload 12 files

Browse files
Files changed (12) hide show
  1. auth.ts +15 -0
  2. ddg-service.ts +146 -0
  3. deno.lock +14 -0
  4. handlers.ts +57 -0
  5. main.ts +8 -0
  6. package-lock.json +502 -0
  7. package.json +5 -0
  8. response.ts +231 -0
  9. router.ts +28 -0
  10. types.ts +24 -0
  11. utils.ts +31 -0
  12. vm.ts +171 -0
auth.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // auth.ts
2
+ import { getToken } from "./utils.ts";
3
+ import { unauthorizedResponse } from "./response.ts";
4
+
5
+ export function validateAuth(req: Request): Response | null {
6
+ const token = getToken();
7
+ if (!token) return null; // 无需认证
8
+
9
+ const auth = req.headers.get("authorization") ?? "";
10
+ if (auth !== token) {
11
+ return unauthorizedResponse("无效的客户端 API 密钥", 403);
12
+ }
13
+
14
+ return null; // 认证通过
15
+ }
ddg-service.ts ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateVqdHash } from "./vm.ts";
2
+ import { userAgent } from "./utils.ts";
3
+ import { CONFIG, getHash, setHash } from "./utils.ts";
4
+ import { ChatMessage } from "./types.ts";
5
+ import { errorResponse } from "./response.ts";
6
+
7
+ export class DDGService {
8
+ private hash: string | null = null;
9
+
10
+ async getVqdHash(): Promise<string | Response> {
11
+ // 优先使用环境变量中的hash
12
+ const envHash = getHash();
13
+ if (envHash) {
14
+ return envHash;
15
+ }
16
+
17
+ try {
18
+ const response = await fetch(CONFIG.DDG_STATUS_URL, {
19
+ method: "GET",
20
+ headers: {
21
+ "User-Agent": userAgent,
22
+ "x-vqd-accept": "1",
23
+ },
24
+ });
25
+
26
+ if (!response.ok) {
27
+ return errorResponse(`hash初始化请求失败: ${response.status}`, 502);
28
+ }
29
+
30
+ const hash = response.headers.get("x-vqd-hash-1");
31
+
32
+ if (!hash) {
33
+ return errorResponse(`未找到hash头部,状态码: ${response.status}`, 502);
34
+ }
35
+
36
+ let decryptedHash: string;
37
+ try {
38
+ decryptedHash = generateVqdHash(hash);
39
+ } catch (decryptError) {
40
+ return errorResponse(`hash解密失败: ${decryptError.message}`, 502);
41
+ }
42
+
43
+ if (!decryptedHash || decryptedHash.trim() === "") {
44
+ return errorResponse(`hash解密结果为空`, 502);
45
+ }
46
+ setHash(decryptedHash);
47
+ return decryptedHash;
48
+ } catch (error) {
49
+ return errorResponse(`获取hash失败: ${error.message}`, 502);
50
+ }
51
+ }
52
+
53
+ // 内部发送消息方法,返回具体的错误信息
54
+ private async sendMessage(
55
+ model: string,
56
+ messages: ChatMessage[]
57
+ ): Promise<{
58
+ success: boolean;
59
+ response?: Response;
60
+ error?: string;
61
+ status?: number;
62
+ }> {
63
+ const hash = await this.getVqdHash();
64
+ if (hash instanceof Response) {
65
+ return { success: false, error: "获取hash失败", status: hash.status };
66
+ }
67
+
68
+ const payload = {
69
+ model,
70
+ messages,
71
+ canUseTools: false,
72
+ canUseApproxLocation: false,
73
+ };
74
+
75
+ try {
76
+ const response = await fetch(CONFIG.DDG_CHAT_URL, {
77
+ method: "POST",
78
+ headers: {
79
+ "User-Agent": userAgent,
80
+ "x-vqd-hash-1": hash,
81
+ "Content-Type": "application/json",
82
+ },
83
+ body: JSON.stringify(payload),
84
+ });
85
+
86
+ if (!response.ok) {
87
+ const errorText = await response.text();
88
+ return {
89
+ success: false,
90
+ error: `上游错误: ${response.status} - ${errorText}`,
91
+ status: response.status,
92
+ };
93
+ }
94
+
95
+ return { success: true, response };
96
+ } catch (error) {
97
+ return {
98
+ success: false,
99
+ error: `请求失败: ${error.message}`,
100
+ status: 502,
101
+ };
102
+ }
103
+ }
104
+
105
+ // 带重试机制的发送消息方法 - 只对418错误重试
106
+ async sendMessageWithRetry(
107
+ model: string,
108
+ messages: ChatMessage[],
109
+ maxRetries = 1
110
+ ): Promise<Response> {
111
+ let lastResult: {
112
+ success: boolean;
113
+ response?: Response;
114
+ error?: string;
115
+ status?: number;
116
+ } | null = null;
117
+
118
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
119
+ if (attempt > 0) {
120
+ setHash(""); // 重置hash,强制重新获取
121
+ }
122
+
123
+ const result = await this.sendMessage(model, messages);
124
+
125
+ if (result.success && result.response) {
126
+ return result.response;
127
+ }
128
+
129
+ lastResult = result;
130
+
131
+ // 只有418错误才重试
132
+ if (result.status === 418 || 429) {
133
+ continue;
134
+ } else {
135
+ // 其他错误直接返回,不重试
136
+ break;
137
+ }
138
+ }
139
+
140
+ // 返回最后一次的错误
141
+ return errorResponse(
142
+ lastResult?.error || "发送消息失败",
143
+ lastResult?.status || 502
144
+ );
145
+ }
146
+ }
deno.lock ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "5",
3
+ "remote": {
4
+ "https://deno.land/[email protected]/async/delay.ts": "f90dd685b97c2f142b8069082993e437b1602b8e2561134827eeb7c12b95c499",
5
+ "https://deno.land/[email protected]/http/server.ts": "f9313804bf6467a1704f45f76cb6cd0a3396a3b31c316035e6a4c2035d1ea514"
6
+ },
7
+ "workspace": {
8
+ "packageJson": {
9
+ "dependencies": [
10
+ "npm:jsdom@^26.1.0"
11
+ ]
12
+ }
13
+ }
14
+ }
handlers.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // handlers.ts
2
+ import { CONFIG } from "./utils.ts";
3
+ import { ChatCompletionRequest, ModelInfo } from "./types.ts";
4
+ import { errorResponse, ResponseBuilder } from "./response.ts";
5
+ import { validateAuth } from "./auth.ts";
6
+ import { DDGService } from "./ddg-service.ts";
7
+
8
+ const ddgService = new DDGService();
9
+
10
+ export async function handleModels(req: Request): Promise<Response> {
11
+ const data: ModelInfo[] = CONFIG.MODELS.map((id) => ({
12
+ id,
13
+ object: "model",
14
+ created: 0,
15
+ owned_by: "ddg",
16
+ }));
17
+
18
+ return ResponseBuilder.jsonResponse({ object: "list", data });
19
+ }
20
+
21
+ export async function handleChatCompletions(req: Request): Promise<Response> {
22
+ // 认证检查
23
+ const authError = validateAuth(req);
24
+ if (authError) return authError;
25
+
26
+ // 解析请求体
27
+ let body: ChatCompletionRequest;
28
+ try {
29
+ const bodyText = await req.text();
30
+ body = JSON.parse(bodyText);
31
+ } catch {
32
+ return errorResponse("请求 JSON 解析失败", 400);
33
+ }
34
+
35
+ // 验证模型
36
+ const { model, messages, stream = false } = body;
37
+ if (!CONFIG.MODELS.includes(model)) {
38
+ return errorResponse(`模型 ${model} 未找到`, 404);
39
+ }
40
+
41
+ // 发送到DDG
42
+ const ddgResponse = await ddgService.sendMessageWithRetry(model, messages);
43
+ if (!ddgResponse.ok) {
44
+ return ddgResponse;
45
+ }
46
+
47
+ return ResponseBuilder.buildResponse(ddgResponse.body!,stream,model);
48
+ }
49
+
50
+ export const handleMeow = async (request: Request): Promise<Response> => {
51
+ return new Response("Meow~", {
52
+ status: 200,
53
+ headers: {
54
+ "Content-Type": "text/plain",
55
+ },
56
+ });
57
+ };
main.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ // main.ts
2
+ import { serve } from "https://deno.land/[email protected]/http/server.ts";
3
+ import { router } from "./router.ts";
4
+ import { CONFIG } from "./utils.ts";
5
+
6
+ console.log(`🚀 服务器启动在端口 ${CONFIG.PORT}`);
7
+
8
+ serve((req: Request) => router(req), { port: CONFIG.PORT });
package-lock.json ADDED
@@ -0,0 +1,502 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "duckduckgo",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "dependencies": {
8
+ "jsdom": "^26.1.0"
9
+ }
10
+ },
11
+ "node_modules/@asamuzakjp/css-color": {
12
+ "version": "3.2.0",
13
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
14
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@csstools/css-calc": "^2.1.3",
18
+ "@csstools/css-color-parser": "^3.0.9",
19
+ "@csstools/css-parser-algorithms": "^3.0.4",
20
+ "@csstools/css-tokenizer": "^3.0.3",
21
+ "lru-cache": "^10.4.3"
22
+ }
23
+ },
24
+ "node_modules/@csstools/color-helpers": {
25
+ "version": "5.0.2",
26
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
27
+ "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
28
+ "funding": [
29
+ {
30
+ "type": "github",
31
+ "url": "https://github.com/sponsors/csstools"
32
+ },
33
+ {
34
+ "type": "opencollective",
35
+ "url": "https://opencollective.com/csstools"
36
+ }
37
+ ],
38
+ "license": "MIT-0",
39
+ "engines": {
40
+ "node": ">=18"
41
+ }
42
+ },
43
+ "node_modules/@csstools/css-calc": {
44
+ "version": "2.1.4",
45
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
46
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
47
+ "funding": [
48
+ {
49
+ "type": "github",
50
+ "url": "https://github.com/sponsors/csstools"
51
+ },
52
+ {
53
+ "type": "opencollective",
54
+ "url": "https://opencollective.com/csstools"
55
+ }
56
+ ],
57
+ "license": "MIT",
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "peerDependencies": {
62
+ "@csstools/css-parser-algorithms": "^3.0.5",
63
+ "@csstools/css-tokenizer": "^3.0.4"
64
+ }
65
+ },
66
+ "node_modules/@csstools/css-color-parser": {
67
+ "version": "3.0.10",
68
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
69
+ "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
70
+ "funding": [
71
+ {
72
+ "type": "github",
73
+ "url": "https://github.com/sponsors/csstools"
74
+ },
75
+ {
76
+ "type": "opencollective",
77
+ "url": "https://opencollective.com/csstools"
78
+ }
79
+ ],
80
+ "license": "MIT",
81
+ "dependencies": {
82
+ "@csstools/color-helpers": "^5.0.2",
83
+ "@csstools/css-calc": "^2.1.4"
84
+ },
85
+ "engines": {
86
+ "node": ">=18"
87
+ },
88
+ "peerDependencies": {
89
+ "@csstools/css-parser-algorithms": "^3.0.5",
90
+ "@csstools/css-tokenizer": "^3.0.4"
91
+ }
92
+ },
93
+ "node_modules/@csstools/css-parser-algorithms": {
94
+ "version": "3.0.5",
95
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
96
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
97
+ "funding": [
98
+ {
99
+ "type": "github",
100
+ "url": "https://github.com/sponsors/csstools"
101
+ },
102
+ {
103
+ "type": "opencollective",
104
+ "url": "https://opencollective.com/csstools"
105
+ }
106
+ ],
107
+ "license": "MIT",
108
+ "engines": {
109
+ "node": ">=18"
110
+ },
111
+ "peerDependencies": {
112
+ "@csstools/css-tokenizer": "^3.0.4"
113
+ }
114
+ },
115
+ "node_modules/@csstools/css-tokenizer": {
116
+ "version": "3.0.4",
117
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
118
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
119
+ "funding": [
120
+ {
121
+ "type": "github",
122
+ "url": "https://github.com/sponsors/csstools"
123
+ },
124
+ {
125
+ "type": "opencollective",
126
+ "url": "https://opencollective.com/csstools"
127
+ }
128
+ ],
129
+ "license": "MIT",
130
+ "engines": {
131
+ "node": ">=18"
132
+ }
133
+ },
134
+ "node_modules/agent-base": {
135
+ "version": "7.1.3",
136
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
137
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
138
+ "license": "MIT",
139
+ "engines": {
140
+ "node": ">= 14"
141
+ }
142
+ },
143
+ "node_modules/cssstyle": {
144
+ "version": "4.6.0",
145
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
146
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
147
+ "license": "MIT",
148
+ "dependencies": {
149
+ "@asamuzakjp/css-color": "^3.2.0",
150
+ "rrweb-cssom": "^0.8.0"
151
+ },
152
+ "engines": {
153
+ "node": ">=18"
154
+ }
155
+ },
156
+ "node_modules/data-urls": {
157
+ "version": "5.0.0",
158
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
159
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
160
+ "license": "MIT",
161
+ "dependencies": {
162
+ "whatwg-mimetype": "^4.0.0",
163
+ "whatwg-url": "^14.0.0"
164
+ },
165
+ "engines": {
166
+ "node": ">=18"
167
+ }
168
+ },
169
+ "node_modules/debug": {
170
+ "version": "4.4.1",
171
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
172
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
173
+ "license": "MIT",
174
+ "dependencies": {
175
+ "ms": "^2.1.3"
176
+ },
177
+ "engines": {
178
+ "node": ">=6.0"
179
+ },
180
+ "peerDependenciesMeta": {
181
+ "supports-color": {
182
+ "optional": true
183
+ }
184
+ }
185
+ },
186
+ "node_modules/decimal.js": {
187
+ "version": "10.6.0",
188
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
189
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
190
+ "license": "MIT"
191
+ },
192
+ "node_modules/entities": {
193
+ "version": "6.0.1",
194
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
195
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
196
+ "license": "BSD-2-Clause",
197
+ "engines": {
198
+ "node": ">=0.12"
199
+ },
200
+ "funding": {
201
+ "url": "https://github.com/fb55/entities?sponsor=1"
202
+ }
203
+ },
204
+ "node_modules/html-encoding-sniffer": {
205
+ "version": "4.0.0",
206
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
207
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
208
+ "license": "MIT",
209
+ "dependencies": {
210
+ "whatwg-encoding": "^3.1.1"
211
+ },
212
+ "engines": {
213
+ "node": ">=18"
214
+ }
215
+ },
216
+ "node_modules/http-proxy-agent": {
217
+ "version": "7.0.2",
218
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
219
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
220
+ "license": "MIT",
221
+ "dependencies": {
222
+ "agent-base": "^7.1.0",
223
+ "debug": "^4.3.4"
224
+ },
225
+ "engines": {
226
+ "node": ">= 14"
227
+ }
228
+ },
229
+ "node_modules/https-proxy-agent": {
230
+ "version": "7.0.6",
231
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
232
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
233
+ "license": "MIT",
234
+ "dependencies": {
235
+ "agent-base": "^7.1.2",
236
+ "debug": "4"
237
+ },
238
+ "engines": {
239
+ "node": ">= 14"
240
+ }
241
+ },
242
+ "node_modules/iconv-lite": {
243
+ "version": "0.6.3",
244
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
245
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
246
+ "license": "MIT",
247
+ "dependencies": {
248
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
249
+ },
250
+ "engines": {
251
+ "node": ">=0.10.0"
252
+ }
253
+ },
254
+ "node_modules/is-potential-custom-element-name": {
255
+ "version": "1.0.1",
256
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
257
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
258
+ "license": "MIT"
259
+ },
260
+ "node_modules/jsdom": {
261
+ "version": "26.1.0",
262
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
263
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
264
+ "license": "MIT",
265
+ "dependencies": {
266
+ "cssstyle": "^4.2.1",
267
+ "data-urls": "^5.0.0",
268
+ "decimal.js": "^10.5.0",
269
+ "html-encoding-sniffer": "^4.0.0",
270
+ "http-proxy-agent": "^7.0.2",
271
+ "https-proxy-agent": "^7.0.6",
272
+ "is-potential-custom-element-name": "^1.0.1",
273
+ "nwsapi": "^2.2.16",
274
+ "parse5": "^7.2.1",
275
+ "rrweb-cssom": "^0.8.0",
276
+ "saxes": "^6.0.0",
277
+ "symbol-tree": "^3.2.4",
278
+ "tough-cookie": "^5.1.1",
279
+ "w3c-xmlserializer": "^5.0.0",
280
+ "webidl-conversions": "^7.0.0",
281
+ "whatwg-encoding": "^3.1.1",
282
+ "whatwg-mimetype": "^4.0.0",
283
+ "whatwg-url": "^14.1.1",
284
+ "ws": "^8.18.0",
285
+ "xml-name-validator": "^5.0.0"
286
+ },
287
+ "engines": {
288
+ "node": ">=18"
289
+ },
290
+ "peerDependencies": {
291
+ "canvas": "^3.0.0"
292
+ },
293
+ "peerDependenciesMeta": {
294
+ "canvas": {
295
+ "optional": true
296
+ }
297
+ }
298
+ },
299
+ "node_modules/lru-cache": {
300
+ "version": "10.4.3",
301
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
302
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
303
+ "license": "ISC"
304
+ },
305
+ "node_modules/ms": {
306
+ "version": "2.1.3",
307
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
308
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
309
+ "license": "MIT"
310
+ },
311
+ "node_modules/nwsapi": {
312
+ "version": "2.2.20",
313
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
314
+ "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
315
+ "license": "MIT"
316
+ },
317
+ "node_modules/parse5": {
318
+ "version": "7.3.0",
319
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
320
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
321
+ "license": "MIT",
322
+ "dependencies": {
323
+ "entities": "^6.0.0"
324
+ },
325
+ "funding": {
326
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
327
+ }
328
+ },
329
+ "node_modules/punycode": {
330
+ "version": "2.3.1",
331
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
332
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
333
+ "license": "MIT",
334
+ "engines": {
335
+ "node": ">=6"
336
+ }
337
+ },
338
+ "node_modules/rrweb-cssom": {
339
+ "version": "0.8.0",
340
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
341
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
342
+ "license": "MIT"
343
+ },
344
+ "node_modules/safer-buffer": {
345
+ "version": "2.1.2",
346
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
347
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
348
+ "license": "MIT"
349
+ },
350
+ "node_modules/saxes": {
351
+ "version": "6.0.0",
352
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
353
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
354
+ "license": "ISC",
355
+ "dependencies": {
356
+ "xmlchars": "^2.2.0"
357
+ },
358
+ "engines": {
359
+ "node": ">=v12.22.7"
360
+ }
361
+ },
362
+ "node_modules/symbol-tree": {
363
+ "version": "3.2.4",
364
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
365
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
366
+ "license": "MIT"
367
+ },
368
+ "node_modules/tldts": {
369
+ "version": "6.1.86",
370
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
371
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
372
+ "license": "MIT",
373
+ "dependencies": {
374
+ "tldts-core": "^6.1.86"
375
+ },
376
+ "bin": {
377
+ "tldts": "bin/cli.js"
378
+ }
379
+ },
380
+ "node_modules/tldts-core": {
381
+ "version": "6.1.86",
382
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
383
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
384
+ "license": "MIT"
385
+ },
386
+ "node_modules/tough-cookie": {
387
+ "version": "5.1.2",
388
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
389
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
390
+ "license": "BSD-3-Clause",
391
+ "dependencies": {
392
+ "tldts": "^6.1.32"
393
+ },
394
+ "engines": {
395
+ "node": ">=16"
396
+ }
397
+ },
398
+ "node_modules/tr46": {
399
+ "version": "5.1.1",
400
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
401
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
402
+ "license": "MIT",
403
+ "dependencies": {
404
+ "punycode": "^2.3.1"
405
+ },
406
+ "engines": {
407
+ "node": ">=18"
408
+ }
409
+ },
410
+ "node_modules/w3c-xmlserializer": {
411
+ "version": "5.0.0",
412
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
413
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
414
+ "license": "MIT",
415
+ "dependencies": {
416
+ "xml-name-validator": "^5.0.0"
417
+ },
418
+ "engines": {
419
+ "node": ">=18"
420
+ }
421
+ },
422
+ "node_modules/webidl-conversions": {
423
+ "version": "7.0.0",
424
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
425
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
426
+ "license": "BSD-2-Clause",
427
+ "engines": {
428
+ "node": ">=12"
429
+ }
430
+ },
431
+ "node_modules/whatwg-encoding": {
432
+ "version": "3.1.1",
433
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
434
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
435
+ "license": "MIT",
436
+ "dependencies": {
437
+ "iconv-lite": "0.6.3"
438
+ },
439
+ "engines": {
440
+ "node": ">=18"
441
+ }
442
+ },
443
+ "node_modules/whatwg-mimetype": {
444
+ "version": "4.0.0",
445
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
446
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
447
+ "license": "MIT",
448
+ "engines": {
449
+ "node": ">=18"
450
+ }
451
+ },
452
+ "node_modules/whatwg-url": {
453
+ "version": "14.2.0",
454
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
455
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
456
+ "license": "MIT",
457
+ "dependencies": {
458
+ "tr46": "^5.1.0",
459
+ "webidl-conversions": "^7.0.0"
460
+ },
461
+ "engines": {
462
+ "node": ">=18"
463
+ }
464
+ },
465
+ "node_modules/ws": {
466
+ "version": "8.18.3",
467
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
468
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
469
+ "license": "MIT",
470
+ "engines": {
471
+ "node": ">=10.0.0"
472
+ },
473
+ "peerDependencies": {
474
+ "bufferutil": "^4.0.1",
475
+ "utf-8-validate": ">=5.0.2"
476
+ },
477
+ "peerDependenciesMeta": {
478
+ "bufferutil": {
479
+ "optional": true
480
+ },
481
+ "utf-8-validate": {
482
+ "optional": true
483
+ }
484
+ }
485
+ },
486
+ "node_modules/xml-name-validator": {
487
+ "version": "5.0.0",
488
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
489
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
490
+ "license": "Apache-2.0",
491
+ "engines": {
492
+ "node": ">=18"
493
+ }
494
+ },
495
+ "node_modules/xmlchars": {
496
+ "version": "2.2.0",
497
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
498
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
499
+ "license": "MIT"
500
+ }
501
+ }
502
+ }
package.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "dependencies": {
3
+ "jsdom": "^26.1.0"
4
+ }
5
+ }
response.ts ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ServerRequest } from "https://deno.land/[email protected]/http/server.ts";
2
+
3
+ interface OpenAIChoice {
4
+ index: number;
5
+ message?: {
6
+ role: string;
7
+ content: string;
8
+ };
9
+ delta?: {
10
+ content: string;
11
+ };
12
+ finish_reason: string | null;
13
+ }
14
+ interface OpenAIUsage {
15
+ prompt_tokens: number;
16
+ completion_tokens: number;
17
+ total_tokens: number;
18
+ }
19
+ interface OpenAIResponse {
20
+ id: string;
21
+ object: string;
22
+ created: number;
23
+ model: string;
24
+ choices: OpenAIChoice[];
25
+ usage?: OpenAIUsage;
26
+ }
27
+ export class ResponseBuilder {
28
+ private static log(
29
+ level: "info" | "warn" | "error",
30
+ message: string,
31
+ data?: any
32
+ ) {
33
+ const timestamp = new Date().toISOString();
34
+ const logData = data ? ` | Data: ${JSON.stringify(data)}` : "";
35
+ console[level](`[${timestamp}] [ResponseBuilder] ${message}${logData}`);
36
+ }
37
+
38
+ // 构建非流式响应
39
+ static buildNonStreamResponse(
40
+ modelName: string,
41
+ fullContent: string,
42
+ finishReason: string = "stop"
43
+ ): Response {
44
+ this.log("info", "Building non-stream response", {
45
+ modelName,
46
+ contentLength: fullContent.length,
47
+ });
48
+
49
+ const response = {
50
+ id: "Chat-Nekohy",
51
+ object: "chat.completion",
52
+ created: Math.floor(Date.now() / 1000),
53
+ model: modelName,
54
+ choices: [
55
+ {
56
+ index: 0,
57
+ message: { role: "assistant", content: fullContent },
58
+ finish_reason: finishReason,
59
+ },
60
+ ],
61
+ };
62
+
63
+ return new Response(JSON.stringify(response), {
64
+ status: 200,
65
+ headers: { "Content-Type": "application/json" },
66
+ });
67
+ }
68
+
69
+ // 构建SSE数据块
70
+ static buildSSEChunk(
71
+ model: string,
72
+ content: string,
73
+ isFinish: boolean = false
74
+ ): string {
75
+ const response = {
76
+ id: "chatcmpl-Nekohy",
77
+ object: "chat.completion.chunk",
78
+ created: Math.floor(Date.now() / 1000),
79
+ model,
80
+ choices: [
81
+ {
82
+ index: 0,
83
+ delta: isFinish ? {} : { content },
84
+ finish_reason: isFinish ? "stop" : null,
85
+ },
86
+ ],
87
+ };
88
+
89
+ const chunk = `data: ${JSON.stringify(response)}\n\n`;
90
+ return isFinish ? chunk + "data: [DONE]\n\n" : chunk;
91
+ }
92
+
93
+ // 主要响应构建方法
94
+ static buildResponse(
95
+ readableStream: ReadableStream,
96
+ stream: boolean = false,
97
+ modelName: string = "default"
98
+ ): Response {
99
+ this.log("info", "Building response", { stream, modelName });
100
+
101
+ const transformedStream = new ReadableStream({
102
+ start: async (controller) => {
103
+ const reader = readableStream.getReader();
104
+ const decoder = new TextDecoder();
105
+ let fullContent = "";
106
+ let chunkCount = 0;
107
+
108
+ try {
109
+ while (true) {
110
+ const { done, value } = await reader.read();
111
+ if (done) break;
112
+
113
+ const chunk = decoder.decode(value, { stream: true });
114
+ const content = this.extractContent(chunk);
115
+
116
+ if (content) {
117
+ chunkCount++;
118
+ fullContent += content;
119
+
120
+ if (stream) {
121
+ const sseChunk = this.buildSSEChunk(modelName, content);
122
+ controller.enqueue(new TextEncoder().encode(sseChunk));
123
+ }
124
+ }
125
+ }
126
+
127
+ this.log("info", "Stream processing completed", {
128
+ chunkCount,
129
+ totalLength: fullContent.length,
130
+ stream,
131
+ });
132
+
133
+ if (stream) {
134
+ // 发送结束标记
135
+ const finishChunk = this.buildSSEChunk(modelName, "", true);
136
+ controller.enqueue(new TextEncoder().encode(finishChunk));
137
+ } else {
138
+ // 发送完整响应
139
+ const response = this.buildNonStreamResponse(
140
+ modelName,
141
+ fullContent
142
+ );
143
+ const responseText = await response.text();
144
+ controller.enqueue(new TextEncoder().encode(responseText));
145
+ }
146
+
147
+ controller.close();
148
+ } catch (error) {
149
+ this.log("error", "Stream processing failed", {
150
+ error: error.message,
151
+ });
152
+ controller.error(error);
153
+ } finally {
154
+ reader.releaseLock();
155
+ }
156
+ },
157
+ });
158
+
159
+ const headers = stream
160
+ ? {
161
+ "Content-Type": "text/event-stream",
162
+ Connection: "keep-alive",
163
+ "Cache-Control": "no-cache",
164
+ }
165
+ : { "Content-Type": "application/json" };
166
+
167
+ return new Response(transformedStream, { status: 200, headers });
168
+ }
169
+
170
+ // 提取SSE数据中的内容
171
+ private static extractContent(chunk: string): string {
172
+ let content = "";
173
+ const lines = chunk.split("\n");
174
+
175
+ for (const line of lines) {
176
+ const trimmedLine = line.trim();
177
+ if (trimmedLine.startsWith("data: ") && !trimmedLine.includes("[DONE]")) {
178
+ try {
179
+ const jsonStr = trimmedLine.slice(6).trim();
180
+ if (jsonStr) {
181
+ const data = JSON.parse(jsonStr);
182
+ if (data.message && typeof data.message === "string") {
183
+ content += data.message;
184
+ }
185
+ }
186
+ } catch (e) {
187
+ this.log("warn", "Failed to parse SSE data", {
188
+ line: trimmedLine,
189
+ error: e.message,
190
+ });
191
+ }
192
+ }
193
+ }
194
+
195
+ return content;
196
+ }
197
+
198
+ // 通用JSON响应
199
+ static jsonResponse(data: unknown, status = 200): Response {
200
+ this.log("info", "Creating JSON response", { status });
201
+ return new Response(JSON.stringify(data), {
202
+ status,
203
+ headers: { "Content-Type": "application/json" },
204
+ });
205
+ }
206
+ }
207
+
208
+ export function streamResponse(body: ReadableStream): Response {
209
+ return new Response(body, {
210
+ status: 200,
211
+ headers: {
212
+ "Content-Type": "text/event-stream",
213
+ Connection: "keep-alive",
214
+ "Cache-Control": "no-cache",
215
+ },
216
+ });
217
+ }
218
+
219
+ export function unauthorizedResponse(message: string, status = 401): Response {
220
+ return new Response(JSON.stringify({ error: message }), {
221
+ status,
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ "WWW-Authenticate": "Bearer",
225
+ },
226
+ });
227
+ }
228
+
229
+ export function errorResponse(message: string, status = 500): Response {
230
+ return ResponseBuilder.jsonResponse({ error: message }, status);
231
+ }
router.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // router.ts
2
+ import { handleMeow, handleModels, handleChatCompletions } from "./handlers.ts";
3
+ import { errorResponse } from "./response.ts";
4
+
5
+
6
+ export async function router(req: Request): Promise<Response> {
7
+ const { method } = req;
8
+ const url = new URL(req.url);
9
+ const pathname = url.pathname;
10
+
11
+ const routes = [
12
+ { method: "GET", path: "/v1/models", handler: handleModels },
13
+ { method: "POST", path: "/v1/chat/completions", handler: handleChatCompletions },
14
+ { method: "GET", path: "/", handler: handleMeow },
15
+ ];
16
+
17
+ const route = routes.find(r => r.method === method && r.path === pathname);
18
+
19
+ if (route) {
20
+ try {
21
+ return await route.handler(req);
22
+ } catch (error) {
23
+ return errorResponse(`处理请求时发生错误: ${error.message}`, 500);
24
+ }
25
+ }
26
+
27
+ return errorResponse("未找到路由", 404);
28
+ }
types.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ChatMessage {
2
+ role: "user" | "assistant" | "system" | string;
3
+ content: string;
4
+ }
5
+
6
+ export interface ChatCompletionRequest {
7
+ model: string;
8
+ messages: ChatMessage[];
9
+ stream?: boolean;
10
+ }
11
+
12
+ export interface ModelInfo {
13
+ id: string;
14
+ object: string;
15
+ created: number;
16
+ owned_by: string;
17
+ }
18
+
19
+ export interface ApiResponse<T = unknown> {
20
+ data?: T;
21
+ error?: string;
22
+ object?: string;
23
+ }
24
+
utils.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const getBaseUrl = (): string => {
2
+ return Deno.env.get("BASE_URL") || "https://duckduckgo.com";
3
+ };
4
+
5
+ export const CONFIG = {
6
+ PORT: 8000,
7
+ DDG_STATUS_URL: `${getBaseUrl()}/duckchat/v1/status`,
8
+ DDG_CHAT_URL: `${getBaseUrl()}/duckchat/v1/chat`,
9
+ MODELS: [
10
+ "gpt-4o-mini",
11
+ "meta-llama/Llama-4-Scout-17B-16E-Instruct",
12
+ "claude-3-5-haiku-latest",
13
+ "o4-mini",
14
+ "mistralai/Mistral-Small-24B-Instruct-2501",
15
+ ],
16
+ };
17
+
18
+ export const userAgent =
19
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
20
+
21
+ export const getToken = (): string | null => {
22
+ return Deno.env.has("TOKEN") ? "Bearer " + Deno.env.get("TOKEN") : null;
23
+ };
24
+
25
+ export const getHash = (): string | undefined => {
26
+ return Deno.env.get("HASH");
27
+ };
28
+
29
+ export const setHash = (hash: string): void => {
30
+ return Deno.env.set("HASH", hash);
31
+ };
vm.ts ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { JSDOM } from "npm:jsdom";
2
+ import { userAgent } from "./utils.ts";
3
+ import crypto from "node:crypto";
4
+
5
+ // 更详细的类型定义
6
+ interface MockMeta {
7
+ readonly origin: string;
8
+ readonly stack: string;
9
+ readonly duration: number;
10
+ }
11
+
12
+ interface HashResults {
13
+ server_hashes: string[];
14
+ client_hashes: string[];
15
+ signals: Record<string, unknown>;
16
+ meta: Record<string, unknown>;
17
+ }
18
+
19
+ interface JSExecutionResult {
20
+ server_hashes?: string[];
21
+ client_hashes?: string[];
22
+ signals?: Record<string, unknown>;
23
+ meta?: Record<string, unknown>;
24
+ }
25
+
26
+ const mockMeta: MockMeta = {
27
+ origin: "https://duckduckgo.com",
28
+ stack: "",
29
+ duration: Math.floor(Math.random() * 100) + 1,
30
+ } as const;
31
+
32
+ class VqdHashGenerator {
33
+ private static extractIIFEJSDOM(jsCode: string): HashResults {
34
+ const results: HashResults = {
35
+ server_hashes: [],
36
+ client_hashes: [],
37
+ signals: {},
38
+ meta: {},
39
+ };
40
+
41
+ let dom: JSDOM | null = null;
42
+
43
+ try {
44
+ dom = new JSDOM(
45
+ `<!DOCTYPE html><html><head></head><body></body></html>`,
46
+ {
47
+ url: "https://duckduckgo.com",
48
+ referrer: "https://duckduckgo.com",
49
+ pretendToBeVisual: true,
50
+ resources: "usable",
51
+ }
52
+ );
53
+
54
+ const { window } = dom;
55
+
56
+ // 设置全局变量
57
+ globalThis.window = window;
58
+ globalThis.document = window.document;
59
+ // 创建自定义 navigator 对象
60
+ const customNavigator = {
61
+ userAgent: userAgent,
62
+ platform: "Win32",
63
+ language: "en-US",
64
+ languages: ["en-US", "en"],
65
+ cookieEnabled: true,
66
+ onLine: true,
67
+ hardwareConcurrency: 4,
68
+ maxTouchPoints: 0,
69
+ vendor: "Google Inc.",
70
+ vendorSub: "",
71
+ productSub: "20030107",
72
+ appName: "Netscape",
73
+ appVersion: userAgent,
74
+ product: "Gecko",
75
+ };
76
+ // 重新定义 window.navigator
77
+ Object.defineProperty(window, "navigator", {
78
+ value: customNavigator,
79
+ writable: true,
80
+ configurable: true,
81
+ enumerable: true,
82
+ });
83
+ // 设置 globalThis.navigator
84
+ Object.defineProperty(globalThis, "navigator", {
85
+ value: customNavigator,
86
+ writable: true,
87
+ configurable: true,
88
+ enumerable: true,
89
+ });
90
+ // 删除 webdriver 属性
91
+ delete (window.navigator as any).webdriver;
92
+
93
+ // 执行 JavaScript 代码
94
+ const result = window.eval(jsCode) as JSExecutionResult;
95
+
96
+ if (result && typeof result === "object") {
97
+ results.server_hashes = result.server_hashes || [];
98
+ results.client_hashes = result.client_hashes || [];
99
+ results.signals = result.signals || {};
100
+ results.meta = result.meta || {};
101
+ }
102
+ } catch (error) {
103
+ console.error("JSDOM execution failed:", error);
104
+ throw new Error(
105
+ `JSDOM执行失败: ${
106
+ error instanceof Error ? error.message : String(error)
107
+ }`
108
+ );
109
+ } finally {
110
+ // 清理资源
111
+ if (dom) {
112
+ dom.window.close();
113
+ }
114
+ delete globalThis.window;
115
+ delete globalThis.document;
116
+ delete globalThis.navigator;
117
+ }
118
+
119
+ return results;
120
+ }
121
+
122
+ private static hashClientValues(mockClientHashes: string[]): string[] {
123
+ return mockClientHashes.map((value: string): string => {
124
+ const hash = crypto.createHash("sha256");
125
+ hash.update(value, "utf8");
126
+ return hash.digest("base64");
127
+ });
128
+ }
129
+
130
+ static generate(vqdHashRequest: string): string {
131
+ if (!vqdHashRequest || typeof vqdHashRequest !== "string") {
132
+ throw new Error("VQD Hash 请求参数无效");
133
+ }
134
+
135
+ try {
136
+ // 解码 base64
137
+ const jsCode = atob(vqdHashRequest);
138
+
139
+ // 提取和处理 hash
140
+ const hash = this.extractIIFEJSDOM(jsCode);
141
+
142
+ // 处理客户端 hash
143
+ hash.client_hashes = this.hashClientValues(hash.client_hashes);
144
+
145
+ // 合并 meta 数据
146
+ hash.meta = {
147
+ ...hash.meta,
148
+ ...mockMeta,
149
+ };
150
+
151
+ // 返回编码后的结果
152
+ return btoa(JSON.stringify(hash));
153
+ } catch (error) {
154
+ console.error("generateVqdHash 执行失败:", error);
155
+ throw new Error(
156
+ `VQD Hash 生成失败: ${
157
+ error instanceof Error ? error.message : String(error)
158
+ }`
159
+ );
160
+ }
161
+ }
162
+ }
163
+
164
+ // 导出函数版本以保持向后兼容
165
+ export function generateVqdHash(vqdHashRequest: string): string {
166
+ return VqdHashGenerator.generate(vqdHashRequest);
167
+ }
168
+
169
+ // 导出类版本
170
+ export { VqdHashGenerator };
171
+ export type { HashResults, MockMeta, JSExecutionResult };