bunnybun07 commited on
Commit
f3dbae9
·
verified ·
1 Parent(s): 8bbb105

Create Cloudflare

Browse files
Files changed (1) hide show
  1. Cloudflare +894 -0
Cloudflare ADDED
@@ -0,0 +1,894 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Cloudflare Workers 环境不需要像 Deno 那样通过 URL 导入模块
2
+ // Web 标准 API (fetch, Request, Response, WebSocket, URL) 是全局可用的。
3
+ // axios 和 qs 不需要了,我们将使用 fetch API。
4
+ // express 不使用了,我们将手动进行路由或使用轻量级路由库。
5
+
6
+ function uuidv4() {
7
+ // 生成16个随机字节(128位)
8
+ const bytes = new Uint8Array(16);
9
+ crypto.getRandomValues(bytes); // 在 Workers 中使用 Web Crypto API 获取更好的随机性
10
+
11
+ // 按照 UUID v4 标准格式设置特定位
12
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // 0100xxxx: version 4
13
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // 10xxxxxx: variant
14
+
15
+ // 转换成 8-4-4-4-12 的 hex 格式
16
+ const hex = [...bytes].map(b => b.toString(16).padStart(2, '0'));
17
+
18
+ return (
19
+ hex.slice(0, 4).join('') + '-' +
20
+ hex.slice(4, 6).join('') + '-' +
21
+ hex.slice(6, 8).join('') + '-' +
22
+ hex.slice(8, 10).join('') + '-' +
23
+ hex.slice(10, 16).join('')
24
+ );
25
+ }
26
+
27
+
28
+ async function fetchApi(url, options = {}) {
29
+ const response = await fetch(url, options);
30
+
31
+ if (!response.ok) {
32
+ // 尝试读取错误响应体
33
+ let errorBody = '未知错误';
34
+ try {
35
+ errorBody = await response.text(); // 使用 text() 因为响应体可能不是 JSON
36
+ } catch (e) {
37
+ console.error('读取错误响应体失败:', e);
38
+ }
39
+ throw new Error(`HTTP error! status: ${response.status}, url: ${url}, body: ${errorBody}`);
40
+ }
41
+
42
+ try {
43
+ // PromptLayer API 通常返回带有 'success' 标志的 JSON
44
+ const data = await response.json();
45
+ if (data.success === false) {
46
+ throw new Error(`PromptLayer API 错误: ${data.message || JSON.stringify(data)}`);
47
+ }
48
+ return data;
49
+ } catch (e) {
50
+ // 处理响应不是 JSON 或 JSON 解析失败的情况
51
+ if (e instanceof SyntaxError) {
52
+ console.error('解析 JSON 响应失败:', await response.text());
53
+ throw new Error(`从 ${url} 接收到无效的 JSON 响应`);
54
+ }
55
+ // 重新抛出 PromptLayer API 特定错误或其他错误
56
+ throw e;
57
+ }
58
+ }
59
+
60
+
61
+ async function fetchTokenDetails(authToken) {
62
+ const url = 'https://api.promptlayer.com/ws-token-request';
63
+ const headers = { Authorization: "Bearer " + authToken };
64
+
65
+ const data = await fetchApi(url, {
66
+ method: 'POST',
67
+ headers: headers,
68
+ // Promptlayer 可能期望有一个请求体,即使是 null。
69
+ // 根据原始 axios 代码发送 null 请求体。
70
+ body: null
71
+ });
72
+
73
+ const access_token = data.token_details.token;
74
+ const clientId = data.token_details.clientId;
75
+ return { access_token, clientId };
76
+ }
77
+
78
+ /**
79
+ * 获取 workspace id
80
+ */
81
+ async function fetchWorkspaceId(authToken) {
82
+ const url = 'https://api.promptlayer.com/workspaces';
83
+ const headers = { Authorization: "Bearer " + authToken };
84
+
85
+ const data = await fetchApi(url, { headers: headers });
86
+
87
+ if (data.workspaces && data.workspaces.length > 0) {
88
+ const workspaceId = data.workspaces[0].id;
89
+ return { workspaceId };
90
+ } else {
91
+ throw new Error('获取 workspace id 失败或未找到 workspaces');
92
+ }
93
+ }
94
+
95
+
96
+ function transformMessagesArray(messages) {
97
+ if (!Array.isArray(messages)) {
98
+ // 返回默认结构或抛出错误,取决于期望的行为
99
+ // 抛出错误更能指示输入无效
100
+ throw new Error("输入的消息必须是数组");
101
+ }
102
+
103
+ return messages.map(msg => {
104
+ // 确保内容被视为文本进行转换
105
+ const textContent = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
106
+
107
+ return {
108
+ role: msg.role,
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: textContent // 确保是 text 类型
113
+ }
114
+ ],
115
+ tool_calls: [], // 根据原始代码,假设 tool_calls 应为空数组
116
+ template_format: "f-string" // 假设这是静态值
117
+ };
118
+ });
119
+ }
120
+
121
+ async function login(username, password) {
122
+ const url = 'https://api.promptlayer.com/login';
123
+ const headers = {
124
+ "Content-Type": "application/json", // fetch 需要为 JSON 请求体明确设置 Content-Type
125
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
126
+ };
127
+ const body = JSON.stringify({ "email": username, "password": password });
128
+
129
+ try {
130
+ // 登录接口通常不返回 response.data 中包含 'success' 标志,
131
+ // 成功时(状态码 200)通常直接返回 token。
132
+ // 需要区别于 fetchApi 辅助函数进行处理。
133
+ const response = await fetch(url, {
134
+ method: 'POST',
135
+ headers: headers,
136
+ body: body
137
+ });
138
+
139
+ if (!response.ok) {
140
+ let errorBody = '未知登录错误';
141
+ try {
142
+ errorBody = await response.text();
143
+ } catch (e) {}
144
+ console.error(`登录失败: ${response.status}, body: ${errorBody}`);
145
+ return { access_token: null, error: `登录失败: ${response.status}` };
146
+ }
147
+
148
+ const data = await response.json(); // 期望返回 { access_token: "..." }
149
+ if (data.access_token) {
150
+ return { access_token: data.access_token };
151
+ } else {
152
+ // 登录状态码成功但缺少 token?不寻常,按失败处理。
153
+ console.error('登录响应缺少 access_token:', data);
154
+ return { access_token: null, error: '登录响应缺少 token' };
155
+ }
156
+
157
+ } catch (e) {
158
+ console.error('登录 fetch 错误:', e);
159
+ return { access_token: null, error: '登录 fetch 错误' };
160
+ }
161
+ }
162
+
163
+ // 刷新playseesion
164
+ async function pysession(authToken, workspaceId, databody) {
165
+ const url = `https://api.promptlayer.com/api/dashboard/v2/workspaces/${workspaceId}/playground_sessions`;
166
+ const headers = {
167
+ Authorization: "Bearer " + authToken,
168
+ "Content-Type": "application/json"
169
+ };
170
+
171
+ let provider = "openai"
172
+ let model = "gpt-4o"
173
+ let parameters = {
174
+ "temperature": 1,
175
+ "seed": 0,
176
+ "response_format": null,
177
+ "top_p": 1,
178
+ "frequency_penalty": 0,
179
+ "presence_penalty": 0
180
+ }
181
+
182
+ // 模型映射逻辑保持不变
183
+ if (databody.model && typeof databody.model === 'string') {
184
+ const lowerModel = databody.model.toLowerCase();
185
+ if (lowerModel.includes("claude") && (lowerModel.includes("3.7") || lowerModel.includes("3-7"))) {
186
+ provider = "anthropic";
187
+ model = "claude-3-7-sonnet-latest";
188
+ parameters = {
189
+ "max_tokens": 64000,
190
+ "temperature": 1
191
+ };
192
+ } else if (lowerModel.includes("claude") && (lowerModel.includes("3.5") || lowerModel.includes("3-5"))) {
193
+ provider = "anthropic";
194
+ model = "claude-3-5-sonnet-latest";
195
+ parameters = {
196
+ "max_tokens": 256, // 注意:这个值很低,确认是否故意为之
197
+ "temperature": 1,
198
+ "top_k": 0,
199
+ "top_p": 0
200
+ };
201
+ } else if (lowerModel.includes("4.1")) {
202
+ model = "gpt-4.1"; // 假设这映射到特定的 PromptLayer 模型名
203
+ } else if (lowerModel.includes("4.5") || lowerModel.includes("4-5")) {
204
+ model = "gpt-4.5-preview"; // 假设这映射
205
+ } else if (lowerModel === "gpt-4o-search-preview") {
206
+ model = "gpt-4o-search-preview";
207
+ parameters = {
208
+ "response_format": null,
209
+ "web_search_options": {
210
+ "search_context_size": "medium",
211
+ "user_location": {
212
+ "approximate": {
213
+ "city": "New York", // 硬编码城市/国家 - 可能需要配置
214
+ "country": "US",
215
+ "region": "New York",
216
+ "timezone": "America/New_York"
217
+ },
218
+ "type": "approximate"
219
+ }
220
+ }
221
+ };
222
+ } else {
223
+ // 如果指定了模型但未明确映射,则使用请求的模型名
224
+ model = databody.model;
225
+ }
226
+ }
227
+ // 否则:保留默认的 openai/gpt-4o
228
+
229
+ const body = JSON.stringify({
230
+ "id": uuidv4(),
231
+ "name": "Not implemented", // 更改名称
232
+ "prompt_blueprint": {
233
+ "inference_client_name": null, // 保持 null
234
+ "metadata": {
235
+ "model": {
236
+ "name": model,
237
+ "provider": provider,
238
+ "parameters": parameters
239
+ }
240
+ },
241
+ "prompt_template": {
242
+ "type": "chat",
243
+ "messages": transformMessagesArray(databody.messages),
244
+ "tools": null,
245
+ "input_variables": [],
246
+ "functions": []
247
+ },
248
+ "provider_base_url_name": null
249
+ },
250
+ "input_variables": []
251
+ });
252
+
253
+ const data = await fetchApi(url, {
254
+ method: 'PUT', // 原始代码使用 PUT
255
+ headers: headers,
256
+ body: body
257
+ });
258
+
259
+ return data.playground_session.id; // 该接口似乎返回 success 和 playground_session.id
260
+ }
261
+
262
+
263
+ async function postmessage(authToken, workspaceId, playground_sessions, databody) {
264
+ const url = `https://api.promptlayer.com/api/dashboard/v2/workspaces/${workspaceId}/run_groups`;
265
+ const headers = {
266
+ Authorization: "Bearer " + authToken,
267
+ "Content-Type": "application/json"
268
+ };
269
+
270
+ let provider = "openai"
271
+ let model = "gpt-4o"
272
+ let parameters = {
273
+ "temperature": 1,
274
+ "seed": 0,
275
+ "response_format": null,
276
+ "top_p": 1,
277
+ "frequency_penalty": 0,
278
+ "presence_penalty": 0
279
+ }
280
+
281
+ // 模型映射逻辑保持不变(与 pysession 重复,考虑重构)
282
+ if (databody.model && typeof databody.model === 'string') {
283
+ const lowerModel = databody.model.toLowerCase();
284
+ if (lowerModel.includes("claude") && (lowerModel.includes("3.7") || lowerModel.includes("3-7"))) {
285
+ provider = "anthropic";
286
+ model = "claude-3-7-sonnet-latest";
287
+ parameters = {
288
+ "max_tokens": 64000,
289
+ "temperature": 1
290
+ };
291
+ } else if (lowerModel.includes("claude") && (lowerModel.includes("3.5") || lowerModel.includes("3-5"))) {
292
+ provider = "anthropic";
293
+ model = "claude-3-5-sonnet-latest";
294
+ parameters = {
295
+ "max_tokens": 256, // 注意:这个值很低,确认是否故意为之
296
+ "temperature": 1,
297
+ "top_k": 0,
298
+ "top_p": 0
299
+ };
300
+ } else if (lowerModel.includes("4.1")) {
301
+ model = "gpt-4.1"; // 假设这映射到特定的 PromptLayer 模型名
302
+ } else if (lowerModel.includes("4.5") || lowerModel.includes("4-5")) {
303
+ model = "gpt-4.5-preview"; // 假设这映射
304
+ } else if (lowerModel === "gpt-4o-search-preview") {
305
+ model = "gpt-4o-search-preview";
306
+ parameters = {
307
+ "response_format": null,
308
+ "web_search_options": {
309
+ "search_context_size": "medium",
310
+ "user_location": {
311
+ "approximate": {
312
+ "city": "New York", // 硬编码城市/国家 - 可能需要配置
313
+ "country": "US",
314
+ "region": "New York",
315
+ "timezone": "America/New_York"
316
+ },
317
+ "type": "approximate"
318
+ }
319
+ }
320
+ };
321
+ } else {
322
+ model = databody.model; // 使用请求的模型名
323
+ }
324
+ }
325
+ // 否则:保留默认的 openai/gpt-4o
326
+
327
+ const body = JSON.stringify({
328
+ "id": uuidv4(),
329
+ "playground_session_id": playground_sessions,
330
+ "shared_prompt_blueprint": {
331
+ "inference_client_name": null,
332
+ "metadata": {
333
+ "model": {
334
+ "name": model,
335
+ "provider": provider,
336
+ "parameters": parameters
337
+ }
338
+ },
339
+ "prompt_template": {
340
+ "type": "chat",
341
+ "messages": transformMessagesArray(databody.messages),
342
+ "tools": null,
343
+ "input_variables": [],
344
+ "functions": []
345
+ },
346
+ "provider_base_url_name": null
347
+ },
348
+ "individual_run_requests": [
349
+ {
350
+ "input_variables": {},
351
+ "run_group_position": 1
352
+ }
353
+ ]
354
+ });
355
+
356
+ const data = await fetchApi(url, {
357
+ method: 'POST',
358
+ headers: headers,
359
+ body: body
360
+ });
361
+
362
+ return data.run_group.individual_run_requests[0].id; // 接口返回 run_group 包含 requests
363
+ }
364
+
365
+ function isJsonString(str) {
366
+ try {
367
+ const parsed = JSON.parse(str);
368
+ return typeof parsed === 'object' && parsed !== null;
369
+ } catch (e) {
370
+ return false;
371
+ }
372
+ }
373
+
374
+ function findDifference(str1, str2) {
375
+ if (str2.startsWith(str1)) {
376
+ return str2.slice(str1.length);
377
+ } else {
378
+ // 这种情况可能表明块处理有问题,或者消息乱序。
379
+ // 如果 str2 不以 str1 开头,返回空字符串意味着跳过不匹配的块。
380
+ // 考虑在这里记录警告或错误。
381
+ console.warn("findDifference: str2 不以 str1 开头。潜在的块不匹配问题。");
382
+ return "";
383
+ }
384
+ }
385
+
386
+ function normalizeMessages(messages) {
387
+ if (!Array.isArray(messages)) {
388
+ // 优雅地处理非数组输入,可能返回空数组
389
+ console.warn("normalizeMessages 接收到非数组输入:", messages);
390
+ return [];
391
+ }
392
+
393
+ return messages.map((message) => {
394
+ // 确保消息结构符合预期或进行规范化
395
+ if (message && typeof message === 'object') {
396
+ if (Array.isArray(message.content)) {
397
+ // 如果 content 是数组,提取 type="text" 的文本部分
398
+ const textContent = message.content
399
+ .filter((item) => item && typeof item === 'object' && item.type === "text")
400
+ .map((item) => item.text)
401
+ .join(" "); // 连接多个文本块(如果存在)
402
+
403
+ // 返回包含原始属性 + 扁平化 content 的对象
404
+ return {
405
+ ...message,
406
+ content: textContent || "" // 确保 content 是字符串,如果没有找到文本部分则默认为空字符串
407
+ };
408
+ }
409
+ // 如果 content 已经是字符串,或不是数组,则按原样返回(确保是字符串)
410
+ return {
411
+ ...message,
412
+ content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content) // 确保 content 是字符串
413
+ };
414
+ }
415
+ // 处理输入数组中的非对象元素
416
+ console.warn("normalizeMessages 在数组中发现非对象项:", message);
417
+ return { // 为无效项返回默认结构
418
+ role: "user", // 默认角色,根据需要调整
419
+ content: typeof message === 'string' ? message : JSON.stringify(message) // 将项表示为字符串内容
420
+ };
421
+ });
422
+ }
423
+
424
+
425
+ // 新增的 /v1/models 接口处理函数
426
+ async function handleModels(request, corsHeaders) {
427
+
428
+ // const models = [
429
+ // "claude-3-5-sonnet-20240620",
430
+ // "claude-3-7-sonnet-20250219",
431
+ // "claude-3-5-sonnet-20241022",
432
+ // "gpt-4o",
433
+ // "gpt-4.1", // Assuming this exists based on your original code
434
+ // "gpt-4.1-2025-04-14", // Assuming this exists
435
+ // "gpt-4.5-preview-2025-02-27", // Assuming this exists
436
+ // "gpt-4o-search-preview"
437
+ // ];
438
+
439
+ // const result = models.map((id) => {
440
+ // return {
441
+ // id,
442
+ // object: "model",
443
+ // created: 1626777600, // Example timestamp, replace if needed
444
+ // owned_by: "custom",
445
+ // root: id.startsWith("claude") ? "anthropic" : "openai" // Determine provider based on ID
446
+ // };
447
+ // });
448
+
449
+ // // Use Response.json() is convenient
450
+ // return Response.json({ data: result, success: true }, { headers: corsHeaders });
451
+
452
+ let access = await getAuthToken(request, corsHeaders);
453
+ if(typeof access != "string"){
454
+ return access;
455
+ }
456
+
457
+ if (access === "") {
458
+ // 使用 Workers 的 Response.json 构建错误响应
459
+ return Response.json(
460
+ { error: "Unauthorized: Missing or invalid Bearer token" },
461
+ { status: 401, headers: corsHeaders } // 包含 CORS 头
462
+ );
463
+ }
464
+
465
+ try {
466
+ // 使用已有的 fetchWorkspaceId 辅助函数
467
+ const { workspaceId } = await fetchWorkspaceId(access);
468
+
469
+ const url = `https://api.promptlayer.com/api/dashboard/v2/workspaces/${workspaceId}/model-configs`;
470
+ const headers = { Authorization: `Bearer ${access}` };
471
+
472
+ // 使用 fetchApi 辅助函数替代 axios.get
473
+ // fetchApi 已经处理了 response.ok 和 data.success 的检查
474
+ const promptLayerData = await fetchApi(url, { headers: headers });
475
+
476
+ // promptLayerData 已经是解析后的 JSON 数据,并且 success: true
477
+ const openAIModels = [];
478
+ // 检查 promptLayerData.results 是否存在且为数组
479
+ if (Array.isArray(promptLayerData.results)) {
480
+ promptLayerData.results.forEach(provider => {
481
+ // 检查 provider.model_configs 是否存在且为数组
482
+ if (provider && Array.isArray(provider.model_configs)) {
483
+ provider.model_configs.forEach(modelConfig => {
484
+ // 检查 modelConfig 是否有必需的字段
485
+ if (modelConfig && typeof modelConfig.llm_model_name === 'string') {
486
+ openAIModels.push({
487
+ id: modelConfig.llm_model_name,
488
+ object: "model",
489
+ // 确保 created_at 存在且是有效日期字符串
490
+ created: modelConfig.created_at ? Math.floor(new Date(modelConfig.created_at).getTime() / 1000) : 0, // 将毫秒转换为秒,无效日期返回 0
491
+ owned_by: provider.provider_name || "unknown", // 提供商名称,如果没有则用 unknown
492
+ // 可以根据需要添加其他字段
493
+ });
494
+ } else {
495
+ console.warn("Skipping invalid modelConfig:", modelConfig);
496
+ }
497
+ });
498
+ } else {
499
+ console.warn("Skipping provider with invalid model_configs:", provider);
500
+ }
501
+ });
502
+ } else {
503
+ console.warn("PromptLayer model-configs response missing or invalid 'results' array:", promptLayerData);
504
+ // 如果 results 格式不正确,仍视为成功但模型列表为空
505
+ }
506
+
507
+
508
+ // 使用 Workers 的 Response.json 构建成功响应
509
+ return Response.json(
510
+ {
511
+ object: "list",
512
+ data: openAIModels
513
+ },
514
+ { headers: corsHeaders } // 包含 CORS 头
515
+ );
516
+
517
+ } catch (error) {
518
+ console.error("获取模型列表错误:", error.message);
519
+
520
+ // 根据错误类型返回不同的状态码和信息
521
+ let status = 500;
522
+ let errorMessage = `Internal Server Error: ${error.message}`;
523
+
524
+ if (error.message.includes('Failed to get workspace id') || error.message.includes('Failed to get workspace id or no workspaces found')) {
525
+ status = 403; // 通常表示认证成功但无权访问工作区
526
+ errorMessage = `Forbidden: ${error.message}`;
527
+ } else if (error.message.includes('HTTP error! status: 401')) {
528
+ status = 401; // 如果 fetchApi 报告了 401
529
+ errorMessage = `Unauthorized: PromptLayer API returned 401`;
530
+ } else if (error.message.includes('HTTP error!')) {
531
+ status = parseInt(error.message.match(/status: (\d+)/)?.[1] || 500); // 尝试从错误消息提取状态码
532
+ errorMessage = `PromptLayer API Error: ${error.message}`;
533
+ } else if (error.message.includes('PromptLayer API error:')) {
534
+ status = 500; // PromptLayer API 报告的逻辑错误
535
+ errorMessage = error.message;
536
+ }
537
+
538
+ // 使用 Workers 的 Response.json 构建错误响应
539
+ return Response.json(
540
+ { error: errorMessage },
541
+ { status: status, headers: corsHeaders } // 包含 CORS 头
542
+ );
543
+ }
544
+ }
545
+
546
+ async function getAuthToken(request, corsHeaders){
547
+ // const username = env.PROMPTLAYER_USERNAME; // 确保在 Worker 设置中绑定 Secrets
548
+ // const password = env.PROMPTLAYER_PASSWORD;
549
+ let access = "";
550
+ let authHeader = request.headers.get('Authorization');
551
+ if (authHeader && authHeader.startsWith("Bearer")) {
552
+ access = authHeader.split("Bearer ")[1];
553
+ }
554
+ if(access == "") {
555
+ return
556
+ }
557
+ let username = access.split("-")[0];
558
+ let password = access.split("-")[1];
559
+ const loginResult = await login(username, password);
560
+ if (loginResult.access_token) {
561
+ access = loginResult.access_token;
562
+ } else {
563
+ console.error("登录失败:", loginResult.error);
564
+ return Response.json({ code: 1, msg: '登录失败', error: loginResult.error }, { status: 401, headers: corsHeaders });
565
+ }
566
+
567
+ if (!access) {
568
+ // 如果登录失败,应该已经在上面返回了,但这作为一个备用检查
569
+ return Response.json({ code: 1, msg: '认证失败', error: '认证失败' }, { status: 401, headers: corsHeaders });
570
+ }
571
+ return access;
572
+ }
573
+
574
+
575
+ async function handleChatCompletions(request, ctx, corsHeaders) {
576
+ let databody;
577
+ try {
578
+ // 限制请求体大小,Workers 默认有上限(如 128MB)
579
+ // 如果需要更大的上限,可能需要考虑不同的处理方式或升级计划
580
+ // 原始代码中的 limit: "50mb" 在 Workers 中通常由平台控制
581
+ databody = await request.json();
582
+ // 验证 databody 和 messages 的基本结构
583
+ if (!databody || !Array.isArray(databody.messages)) {
584
+ throw new Error("无效的请求体格式");
585
+ }
586
+ databody.messages = normalizeMessages(databody.messages);
587
+ } catch (e) {
588
+ console.error("解析请求体错误:", e);
589
+ return Response.json({ code: 1, msg: '无效的 JSON 或请求体格式', error: e.message }, { status: 400, headers: corsHeaders });
590
+ }
591
+
592
+ let access = await getAuthToken(request, corsHeaders);
593
+ if(typeof access != "string"){
594
+ return access;
595
+ }
596
+
597
+ let tokenResult, workspaceResult;
598
+ try {
599
+ [tokenResult, workspaceResult] = await Promise.all([
600
+ fetchTokenDetails(access),
601
+ fetchWorkspaceId(access)
602
+ ]);
603
+ } catch (e) {
604
+ console.error("获取 token/workspace 错误:", e);
605
+ return Response.json({ code: 2, msg: '初始化会话失败', error: e.message }, { status: 500, headers: corsHeaders });
606
+ }
607
+
608
+ const { access_token, clientId } = tokenResult;
609
+ const { workspaceId } = workspaceResult;
610
+
611
+ let playground_sessions;
612
+ try {
613
+ playground_sessions = await pysession(access, workspaceId, databody);
614
+ if (playground_sessions === -1) { // 检查 -1 返回值表示失败
615
+ throw new Error("调用 pysession 失败");
616
+ }
617
+ } catch (e) {
618
+ console.error("创建/刷新 playground session 错误:", e);
619
+ return Response.json({ code: 2, msg: '创建 playground session 失败', error: e.message }, { status: 500, headers: corsHeaders });
620
+ }
621
+
622
+
623
+ // --- WebSocket 处理和流式响应 ---
624
+
625
+ // 对于流式响应,我们需要创建一个流并将数据推入其中
626
+ const stream = new TransformStream();
627
+ const writer = stream.writable.getWriter();
628
+ const encoder = new TextEncoder();
629
+
630
+ // 构建 wss url
631
+ const wsUrl = `wss://realtime.ably.io/?access_token=${encodeURIComponent(access_token)}&clientId=${clientId}&format=json&heartbeats=true&v=3&agent=ably-js%2F2.0.2%20browser`;
632
+ // 创建 WebSocket 连接
633
+ const ws = new WebSocket(wsUrl);
634
+
635
+ let last = ""; // 跟踪上一个块用于计算差异
636
+ let nonstr = ""; // 为非流式响应累积内容
637
+ let requestId = null; // 存储请求 ID 用于过滤消息
638
+
639
+ // Promise,当 WebSocket 过程完成(关闭或出错)时 resolve
640
+ // 这是 event.waitUntil 所需要的
641
+ const wsComplete = new Promise((resolve, reject) => {
642
+
643
+ ws.onopen = async () => {
644
+ try {
645
+ // 在 WS 打开时发送初始 action
646
+ const sendAction = `{"action":10,"channel":"user:${clientId}","params":{"agent":"react-hooks/2.0.2"}}`;
647
+ ws.send(sendAction);
648
+
649
+ // 在 WS 打开后通过 HTTP 发送消息
650
+ requestId = await postmessage(access, workspaceId, playground_sessions, databody);
651
+ if (requestId === -1) { // 检查失败情况
652
+ throw new Error("调用 postmessage 失败");
653
+ }
654
+ // console.log("individual_run_request_id:" + requestId);
655
+
656
+ } catch (err) {
657
+ console.error("WS 打开或 postmessage 过程中发生错误:", err);
658
+ // reject promise 以指示失败,并让 waitUntil 完成
659
+ writer.abort(err).catch(() => {}); // 中止流写入器
660
+ ws.close(); // 尝试关闭 WS
661
+ reject(err); // reject promise
662
+ }
663
+ };
664
+
665
+ ws.onmessage = async (event) => {
666
+ try {
667
+ const data = event.data;
668
+ let msg;
669
+ // Ably 发送心跳消息是纯文本 'ping',忽略它们
670
+ if (typeof data !== 'string' || data === 'ping') {
671
+ return;
672
+ }
673
+
674
+ try {
675
+ msg = JSON.parse(data);
676
+ } catch (e) {
677
+ console.warn("接收到非 JSON 的 WS 消息:", data);
678
+ return; // 忽略不是 JSON 的消息
679
+ }
680
+
681
+
682
+ let firstMsg = msg?.messages?.[0];
683
+
684
+ // 根据从 postmessage 获取的特定 requestId 过滤消息
685
+ const messageRequestId = firstMsg?.data && typeof firstMsg.data === 'string' && isJsonString(firstMsg.data)
686
+ ? JSON.parse(firstMsg.data).individual_run_request_id
687
+ : null;
688
+
689
+ // 只处理与当前请求相关的消息
690
+ if (requestId && messageRequestId !== requestId) {
691
+ // console.log(`忽略针对请求 ID ${messageRequestId} 的 WS 消息,期望 ${requestId}`);
692
+ return;
693
+ }
694
+ // 否则:如果 requestId 匹配或消息是 Ably 协议消息,继续处理
695
+
696
+
697
+ if (
698
+ firstMsg?.name === "UPDATE_LAST_MESSAGE" &&
699
+ typeof firstMsg.data === "string" &&
700
+ isJsonString(firstMsg.data)
701
+ ) {
702
+ const messageData = JSON.parse(firstMsg.data);
703
+ // 确保 payload 和消息结构存在
704
+ if (messageData.payload?.message?.content?.[0]?.text !== undefined) {
705
+ const currentContent = messageData.payload.message.content[0].text;
706
+ const newChunk = findDifference(last, currentContent);
707
+ last = currentContent;
708
+ nonstr += newChunk; // 为非流式累积内容
709
+
710
+ if (databody.stream === true && newChunk.length > 0) {
711
+ // 将块以 Server-Sent Events 格式写入流
712
+ const sseChunk = `data: {"id":"chatcmpl-${uuidv4()}","object":"chat.completion.chunk","created":${Math.floor(Date.now() / 1000)},"model":"${databody.model || 'unknown-model'}","choices":[{"index":0,"delta":{"content":${JSON.stringify(newChunk)}},"logprobs":null,"finish_reason":null}]} \n\n`;
713
+ await writer.write(encoder.encode(sseChunk));
714
+ }
715
+ }
716
+
717
+ } else if (
718
+ firstMsg?.name === "INDIVIDUAL_RUN_COMPLETE" &&
719
+ typeof firstMsg.data === "string" &&
720
+ isJsonString(firstMsg.data)
721
+ ){
722
+ // 确保这是针对我们特定请求的完成消息
723
+ const messageData = JSON.parse(firstMsg.data);
724
+ if (messageData.individual_run_request_id === requestId) {
725
+
726
+ if (databody.stream === true) { // 只在流式模式下发送结束信号
727
+ // 流式:发送最终块并结束流
728
+ const sseEnd = `data: {"id":"chatcmpl-${uuidv4()}","object":"chat.completion.chunk","model":"${databody.model || 'unknown-model'}","created":${Math.floor(Date.now() / 1000)},"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n`;
729
+ await writer.write(encoder.encode(sseEnd));
730
+ await writer.write(encoder.encode('data: [DONE]\n')); // SSE 结束信号
731
+ }
732
+ // 非流式模式下,内容已累积在 nonstr 中,将在 wsComplete Promise resolve 后返回最终 JSON 响应。
733
+
734
+ // 关闭流和 WebSocket
735
+ await writer.close(); // 关闭流写入器
736
+ ws.close(); // 显式关闭 WebSocket
737
+ }
738
+
739
+ } else if (msg.action === 0) {
740
+ // Ably 协议消息指示连接关闭
741
+ console.log("Ably WS 被服务器关闭 (action 0)");
742
+ // 清理流和 WS
743
+ await writer.close(); // 关闭流写入器
744
+ ws.close(); // 确保我们也关闭 WS
745
+ }
746
+
747
+ } catch (err) {
748
+ console.error("处理 WS 消息错误:", err);
749
+ // 在任何消息处理错误时,尝试中止流写入器并关闭 WebSocket
750
+ writer.abort(err).catch(() => {});
751
+ ws.close();
752
+ reject(err); // 在错误时 reject 主 promise
753
+ }
754
+ };
755
+
756
+ ws.onerror = (event) => {
757
+ const errorMsg = event.message || '未知 WebSocket 错误';
758
+ console.error("WebSocket 错误:", errorMsg, event);
759
+ // 在 WebSocket 错误时,中止流写入器并 reject 主 promise
760
+ writer.abort(new Error(`WebSocket 错误: ${errorMsg}`)).catch(() => {});
761
+ // ws.close() 可能在 onerror 后自动发生,但调用它没有坏处
762
+ reject(new Error(`WebSocket 错误: ${errorMsg}`));
763
+ };
764
+
765
+ ws.onclose = (event) => {
766
+ console.log(`WebSocket 已关闭: Code=${event.code}, Reason=${event.reason}, WasClean=${event.wasClean}`);
767
+ // 确保写入器已关闭/中止(如果尚未完成)
768
+ // 这对于 waitUntil 了解任务何时完成非常重要
769
+ if (!event.wasClean) {
770
+ // 如果不是干净关闭,中止流
771
+ writer.abort(new Error(`WebSocket 非干净关闭: Code=${event.code}, Reason=${event.reason}`)).catch(() => {});
772
+ reject(new Error(`WebSocket 非干净关闭: Code=${event.code}`));
773
+ } else {
774
+ // 如果是干净关闭,确保写入器已关闭(如果在 INDIVIDUAL_RUN_COMPLETE 时关闭了)
775
+ // 如果在完成之前以某种方式干净关闭,可能会有问题。
776
+ // 我们在这里 resolve 是假设干净关闭意味着过程已成功完成(或优雅地取消了)。
777
+ writer.close().catch(() => {});
778
+ resolve(); // 在干净关闭时 resolve 主 promise
779
+ }
780
+ };
781
+ });
782
+
783
+
784
+ // 将 WebSocket 过程添加到 waitUntil 中,以保持 Worker 活跃
785
+ ctx.waitUntil(wsComplete);
786
+
787
+ // 立即返回响应。
788
+ // 对于流式响应,此响应包含可读流。
789
+ // 对于非流式响应,流将接收所有块,然后关闭,最终的 JSON 响应将在流完成后
790
+ // 根据累积的 'nonstr' 变量构建。
791
+ // 这是一种使用单个 WS 交互处理流式和非流式响应的模式。
792
+
793
+ if (databody.stream === true) {
794
+ // 流式响应
795
+ return new Response(stream.readable, {
796
+ headers: {
797
+ 'Content-Type': 'text/event-stream', // Server-Sent Events MIME 类型
798
+ 'Cache-Control': 'no-cache',
799
+ 'Connection': 'keep-alive',
800
+ ...corsHeaders // 包含 CORS 头
801
+ },
802
+ });
803
+ } else {
804
+ // 非流式响应:累积数据,并在 WS 完成后发送最终 JSON
805
+ // 响应对象现在被创建并返回,但其响应体是异步填充的,
806
+ // 基于 'nonstr' 变量在 wsComplete promise resolve *之后*。这需要微调。
807
+
808
+ // 更好的非流式处理方法:在返回 Response 之前等待 wsComplete 完成,
809
+ // 然后构建并返回最终的 JSON 响应。
810
+ // 这简化了响应处理逻辑。
811
+
812
+ // 等待 WebSocket 过程完成
813
+ try {
814
+ await wsComplete;
815
+
816
+ // 构建最终的非流式 JSON 响应
817
+ return Response.json({
818
+ id: `chatcmpl-${uuidv4()}`, // 为最终完成生成新的 ID
819
+ object: "chat.completion",
820
+ created: Math.floor(Date.now() / 1000),
821
+ model: databody.model || 'unknown-model',
822
+ choices: [
823
+ {
824
+ index: 0,
825
+ message: {
826
+ role: "assistant",
827
+ content: nonstr, // 使用累积的内容
828
+ },
829
+ finish_reason: "stop", // 假设完成时是 'stop'
830
+ },
831
+ ],
832
+ usage: { // 在这种代理设置中通常未知 Usage
833
+ prompt_tokens: 0,
834
+ completion_tokens: 0,
835
+ total_tokens: 0,
836
+ },
837
+ system_fingerprint: null, // 或尽可能派生
838
+ }, { headers: corsHeaders });
839
+
840
+ } catch (err) {
841
+ console.error("WebSocket 或非流式完成过程中发生错误:", err);
842
+ // 处理 WS 过程中的错误(对于非流式)
843
+ return Response.json({ code: 2, msg: '处理错误', error: err.message }, { status: 500, headers: corsHeaders });
844
+ }
845
+ }
846
+ }
847
+
848
+
849
+ export async function handleRequest(request, env, ctx) {
850
+ const url = new URL(request.url);
851
+ const { pathname } = url;
852
+ const method = request.method;
853
+
854
+ // 为所有响应设置 CORS 头
855
+ const corsHeaders = {
856
+ 'Access-Control-Allow-Origin': '*', // 在生产环境中应更严格
857
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
858
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
859
+ };
860
+
861
+ // 处理 CORS 预检请求
862
+ if (method === 'OPTIONS') {
863
+ return new Response(null, {
864
+ headers: {
865
+ ...corsHeaders,
866
+ 'Access-Control-Max-Age': '86400', // 缓存预检请求 24 小时
867
+ },
868
+ });
869
+ }
870
+
871
+
872
+ if (method === 'GET' && pathname === '/') {
873
+ // 简单的欢迎消息,等同于 app.get("/")
874
+ return new Response("https://linux.do/t/topic/663505", { headers: corsHeaders });
875
+ }
876
+
877
+ if (method === 'GET' && pathname === '/v1/models') {
878
+ return handleModels(request, corsHeaders);
879
+ }
880
+
881
+ if (method === 'POST' && pathname === '/v1/chat/completions') {
882
+ return handleChatCompletions(request, ctx, corsHeaders); // 传递 event 以使用 waitUntil
883
+ }
884
+
885
+ // 处理其他路径或方法
886
+ return new Response('未找到', { status: 404, headers: corsHeaders });
887
+ }
888
+
889
+
890
+ export default {
891
+ async fetch(request, env, ctx) {
892
+ return handleRequest(request, env, ctx);
893
+ },
894
+ };