兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
我们来逐行、逐块地详细解释这份 `src/worker.js` Cloudflare Worker 脚本。 这份代码是一个功能丰富的后端服务,它为聊天应用提供了核心能力,包括: 1. **实时聊天室**:通过 Durable Objects 实现。 2. **文件(图片)上传**:通过 Cloudflare R2 Storage 实现。 3. **AI 文本解释**:通过调用外部 AI API (DeepSeek 和 Gemini) 实现。 4. **AI 图片描述**:通过调用外部 AI API (Gemini Vision) 实现。 5. **房间数据统计**:从 Durable Object 中获取统计信息。 6. **前端页面托管**:直接从 Worker 提供 HTML 页面。 --- ### 第一部分:导入与导出 (Imports & Exports) ```javascript // src/worker.js import { ChatRoomDurableObject } from './chatroom_do.js'; import html from '../public/index.html'; // Export Durable Object class for Cloudflare platform instantiation export { ChatRoomDurableObject }; ``` * `import { ChatRoomDurableObject } from './chatroom_do.js';` * **含义**:从 `./chatroom_do.js` 这个文件中,导入名为 `ChatRoomDurableObject` 的类。 * **解释**:这个 `ChatRoomDurableObject` 类定义了一个"持久对象"(Durable Object)的行为。持久对象是 Cloudflare 的一种特殊功能,它是一个有状态的 Worker 实例。在这里,每个聊天室(如 `room-a`)都会对应一个 `ChatRoomDurableObject` 实例,这个实例会记住该房间的所有在线用户和聊天记录。主 Worker 需要导入这个类,以便稍后可以创建或获取它的实例。 * `import html from '../public/index.html';` * **含义**:将 `../public/index.html` 文件的全部内容,作为一个字符串,导入到名为 `html` 的变量中。 * **解释**:这是 Cloudflare Workers/Wrangler 的一个特性。在构建项目时,它会读取 `index.html` 文件的内容并将其内联到 JavaScript 代码中。这样做的好处是,你可以直接从 Worker 返回整个 HTML 页面,而不需要额外的文件服务器。 * `export { ChatRoomDurableObject };` * **含义**:将 `ChatRoomDurableObject` 类导出。 * **解释**:这是**至关重要**的一步。为了让 Cloudflare 平台知道哪个类是持久对象,你必须在主 Worker 文件中导出它。Cloudflare 的运行时系统会查找这个导出,以便在需要时(例如,当第一个用户访问某个聊天室时)能够正确地创建和管理 `ChatRoomDurableObject` 的实例。 --- ### 第二部分:模块化的AI服务调用函数 这部分代码将调用外部 AI 服务的逻辑封装成了独立的、可重用的函数,这是非常好的编程实践。 #### `getDeepSeekExplanation` 函数 ```javascript /** * 调用 DeepSeek API 获取解释 * @param {string} text - 需要解释的文本 * @param {object} env - Cloudflare环境变量 * @returns {Promise<string>} - AI返回的解释文本 */ async function getDeepSeekExplanation(text, env) { // 从环境变量中获取 DeepSeek API 密钥 const DEEPSEEK_API_KEY = env.DEEPSEEK_API_KEY; // 检查密钥是否存在,如果不存在则抛出错误,增强了代码的健壮性 if (!DEEPSEEK_API_KEY) { throw new Error('Server configuration error: DEEPSEEK_API_KEY is not set.'); } // 使用 fetch API 向 DeepSeek 的 API 端点发起网络请求 const response = await fetch("https://api.deepseek.com/chat/completions", { method: "POST", // 使用 POST 方法发送数据 headers: { "Content-Type": "application/json", // 告诉服务器我们发送的是 JSON 格式的数据 "Authorization": `Bearer ${DEEPSEEK_API_KEY}` // API 认证,使用 Bearer Token 方案 }, body: JSON.stringify({ // 将 JavaScript 对象转换为 JSON 字符串作为请求体 model: "deepseek-chat", // 指定使用的 AI 模型 messages: [ // 设置系统消息,定义 AI 的角色和行为 { role: "system", content: "你是一个有用的,善于用简洁的markdown语言来解释下面的文本." }, // 设置用户消息,包含详细的指令(Prompt)和需要解释的文本 { role: "user", content: `你是一位非常耐心的小学老师...(此处为详细的Prompt)...:\n\n${text}` } ] }) }); // 检查 API 响应是否成功 (HTTP 状态码在 200-299 之间) if (!response.ok) { const errorText = await response.text(); // 获取详细的错误信息 console.error(`DeepSeek API error: ${response.status} - ${errorText}`); // 在后台打印错误日志 throw new Error(`DeepSeek API error: ${errorText}`); // 抛出错误,中断执行 } // 解析返回的 JSON 数据 const data = await response.json(); // 从返回的数据结构中安全地提取 AI 生成的文本 // 使用可选链操作符 `?.` 来避免因结构不符导致的错误 const explanation = data?.choices?.[0]?.message?.content; // 再次检查是否成功提取到文本 if (!explanation) { console.error('Unexpected DeepSeek API response structure:', JSON.stringify(data)); throw new Error('Unexpected AI response format from DeepSeek.'); } // 返回最终的解释文本 return explanation; } ``` #### `getGeminiExplanation` 函数 这个函数与 `getDeepSeekExplanation` 目的相同,但调用的是 Google Gemini API。注意它们在 API URL、认证方式、请求体和响应体结构上的不同。 ```javascript async function getGeminiExplanation(text, env) { // 从环境变量中获取 Gemini API 密钥 const GEMINI_API_KEY = env.GEMINI_API_KEY; if (!GEMINI_API_KEY) { throw new Error('Server configuration error: GEMINI_API_KEY is not set.'); } // Gemini API 的端点,注意 API Key 是作为 URL 的查询参数传入的 const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=${GEMINI_API_KEY}`; // 发起 fetch 请求 const response = await fetch(GEMINI_API_URL, { method: "POST", headers: { "Content-Type": "application/json", // 只需指定内容类型 }, body: JSON.stringify({ // Gemini 的请求体结构与 DeepSeek 不同 contents: [{ parts: [{ // Prompt 直接放在 text 字段里 text: `你是一位非常耐心的小学老师...(此处为详细的Prompt)...:\n\n${text}` }] }] }) }); // 同样进行错误处理 if (!response.ok) { const errorText = await response.text(); console.error(`Gemini API error: ${response.status} - ${errorText}`); throw new Error(`Gemini API error: ${errorText}`); } // 解析 JSON 响应 const data = await response.json(); // 从 Gemini 特有的响应结构中提取文本 const explanation = data?.candidates?.[0]?.content?.parts?.[0]?.text; // 同样进行最终检查 if (!explanation) { console.error('Unexpected Gemini API response structure:', JSON.stringify(data)); throw new Error('Unexpected AI response format from Gemini.'); } return explanation; } ``` --- ### 第三部分:AI 图片描述服务 这部分代码用于处理图片,将其发送给 Gemini Vision 模型进行分析和描述。 #### `fetchImageAsBase64` 辅助函数 ```javascript async function fetchImageAsBase64(imageUrl) { // 根据传入的 URL 下载图片 const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); } // 获取图片的 MIME 类型(如 'image/jpeg'),如果服务器没提供则默认为 'image/jpeg' const contentType = response.headers.get('content-type') || 'image/jpeg'; // 将响应体读取为 ArrayBuffer,这是原始的二进制数据 const buffer = await response.arrayBuffer(); // --- 将 ArrayBuffer 转换为 Base64 字符串 --- let binary = ''; // 将 ArrayBuffer 包装成 Uint8Array 以便按字节访问 const bytes = new Uint8Array(buffer); // 遍历每个字节 for (let i = 0; i < bytes.byteLength; i++) { // 将字节的数字值转换为对应的字符 binary += String.fromCharCode(bytes[i]); } // 使用 btoa 函数将二进制字符串编码为 Base64 const base64 = btoa(binary); // 返回一个包含 Base64 数据和内容类型的对象 return { base64, contentType }; } ``` * **为什么需要 Base64?** Gemini Vision API 要求图片数据直接嵌入到 JSON 请求中,而不是通过 URL 引用。Base64 是一种将二进制数据表示为文本字符串的方法,非常适合在 JSON 中传输。 #### `getGeminiImageDescription` 函数 ```javascript async function getGeminiImageDescription(imageUrl, env) { // 同样,先获取和检查 API Key const GEMINI_API_KEY = env.GEMINI_API_KEY; if (!GEMINI_API_KEY) { throw new Error('Server configuration error: GEMINI_API_KEY is not set.'); } // 调用上面的辅助函数,下载图片并转换为 Base64 const { base64, contentType } = await fetchImageAsBase64(imageUrl); // 使用支持视觉功能的 Gemini 模型 (gemini-1.5-flash-latest) const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${GEMINI_API_KEY}`; // 定义一个专门用于图片描述的 Prompt const prompt = "请仔细描述图片的内容..."; // 发起 fetch 请求 const response = await fetch(GEMINI_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contents: [{ // "parts" 数组现在包含多个部分:文本 Prompt 和图片数据 parts: [ { text: prompt }, // 第一个部分是文本指令 { // 第二个部分是内联的图片数据 inline_data: { mime_type: contentType, // 告诉 API 图片的格式 data: base64 // 传入 Base64 编码的图片数据 } } ] }] }) }); // 标准的错误处理和响应解析流程 if (!response.ok) { // ... (错误处理) } const data = await response.json(); const description = data?.candidates?.[0]?.content?.parts?.[0]?.text; if (!description) { // ... (格式检查) } return description; } ``` --- ### 第四部分:主 Worker 逻辑 (路由) 这是 Worker 的核心入口,它接收所有传入的 HTTP 请求,并根据请求的 URL 和方法将其分发到不同的处理逻辑中。 ```javascript export default { async fetch(request, env, ctx) { // 将请求的 URL 字符串解析成一个 URL 对象,方便地获取路径、参数等 const url = new URL(request.url); // --- 预处理路径名 --- let pathname = url.pathname; // 如果路径以'/'结尾且长度大于1(即不是根路径'/'),则去掉结尾的'/' // 这使得 /room-a/ 和 /room-a 被视为相同路径 if (pathname.endsWith('/') && pathname.length > 1) { pathname = pathname.slice(0, -1); } // 将路径按'/'分割成数组,并过滤掉空字符串(例如,'/a/b/'.split('/') 会产生空元素) const pathParts = pathname.split('/').filter(part => part); // --- 路由逻辑开始 --- // 1. 处理文件上传请求 if (pathname === '/upload' && request.method === 'POST') { // ... (上传逻辑与原版相同,此处省略详细解释) ... // 核心是使用 `env.R2_BUCKET.put()` 将请求体(文件内容)存入 R2 存储桶。 } // 2. 处理 AI 文本解释请求 if (pathname === '/ai-explain' && request.method === 'POST') { try { // 解析请求体中的 JSON 数据 const requestBody = await request.json(); const text = requestBody.text; // 从请求中获取要使用的模型,如果前端没传,则默认使用 'gemini' const model = requestBody.model || 'gemini'; if (!text) { // 检查必要参数 return new Response('Missing text in request body.', { status: 400 }); } let explanation = ""; // 根据 'model' 参数的值,调用对应的 AI 函数 console.log(`Routing AI request to model: ${model}`); if (model === 'gemini') { explanation = await getGeminiExplanation(text, env); } else if (model === 'deepseek') { explanation = await getDeepSeekExplanation(text, env); } else { return new Response(`Unknown AI model: ${model}`, { status: 400 }); } // 将 AI 的返回结果包装成 JSON 响应返回给前端 return new Response(JSON.stringify({ explanation }), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { // 捕获整个过程中的任何错误 console.error('AI explanation request error:', error.message); // 返回一个包含具体错误信息的 500 响应,方便前端调试 return new Response(`Error processing AI request: ${error.message}`, { status: 500 }); } } // 3. 处理 AI 图片描述请求 if (pathname === '/ai-describe-image' && request.method === 'POST') { try { const requestBody = await request.json(); const imageUrl = requestBody.imageUrl; if (!imageUrl) { return new Response('Missing imageUrl in request body.', { status: 400 }); } // 调用图片描述函数 const description = await getGeminiImageDescription(imageUrl, env); // 返回结果 return new Response(JSON.stringify({ description }), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { // ... (错误处理) ... } } // 4. 处理获取房间用户统计数据的请求 if (pathname === '/room-user-stats' && request.method === 'GET') { try { // 从 URL 查询参数中获取房间名 (例如 /room-user-stats?roomName=my-room) const roomName = url.searchParams.get('roomName'); if (!roomName) { return new Response('Missing roomName in query parameters.', { status: 400 }); } if (!env.CHAT_ROOM_DO) { // 检查 Durable Object 绑定是否存在 return new Response('Server configuration error: CHAT_ROOM_DO not bound.', { status: 500 }); } // 使用房间名生成一个确定性的、唯一的 ID const doId = env.CHAT_ROOM_DO.idFromName(roomName); // 获取该 ID 对应的 Durable Object 的 "存根" (stub) // stub 是一个代理对象,用于与实际的 Durable Object 实例通信 const stub = env.CHAT_ROOM_DO.get(doId); // **核心通信**:向 Durable Object 实例发起一个内部的 fetch 请求 // 这就像 Worker 自己在访问一个 URL,但这个 URL 会被路由到 DO 内部的 fetch 处理器 const doResponse = await stub.fetch(new Request(`${url.origin}/user-stats`, { method: 'GET' })); // 处理从 DO 返回的响应 if (!doResponse.ok) { // ... (错误处理) ... } const stats = await doResponse.json(); return new Response(JSON.stringify(stats), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { // ... (错误处理) ... } } // --- 剩余的路由逻辑 --- // 5. 处理根路径 '/' if (pathParts.length === 0) { return new Response('Welcome! Please access /<room-name> to join a chat room.', { status: 200 }); } // 提取房间名,例如 /my-room -> 'my-room' const roomName = pathParts[0]; // 6. 处理 WebSocket 升级请求 // 检查请求头,看客户端是否想建立 WebSocket 连接 const upgradeHeader = request.headers.get("Upgrade"); if (upgradeHeader === "websocket") { if (!env.CHAT_ROOM_DO) { // 检查绑定 return new Response('Server configuration error: CHAT_ROOM_DO not bound.', { status: 500 }); } // 同样,根据房间名获取 DO 的 ID 和 stub const doId = env.CHAT_ROOM_DO.idFromName(roomName); const stub = env.CHAT_ROOM_DO.get(doId); // **核心操作**:将整个 WebSocket 升级请求转发给 Durable Object。 // DO 内部的 fetch 处理器会接管这个请求,并与客户端建立 WebSocket 连接。 return stub.fetch(request); } // 7. 默认行为:返回 HTML 页面 // 如果以上所有路由都不匹配(例如,直接访问 /my-room),则返回之前导入的 HTML 内容。 return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8' }, }); }, }; ``` 希望这份详尽的解释能帮助你完全理解这份代码的每一部分!
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章