src/worker.js 逻辑解读-路由分发功能

我们来逐行、逐块地详细解释这份 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)

  
// 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 函数

  
/**
  
 * 调用 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、认证方式、请求体和响应体结构上的不同。

  
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 辅助函数

  
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 函数

  
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 和方法将其分发到不同的处理逻辑中。

  
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' },
  
        });
  
    },
  
};
  

希望这份详尽的解释能帮助你完全理解这份代码的每一部分!