我们来逐行、逐块地详细解释这份 src/worker.js Cloudflare Worker 脚本。
这份代码是一个功能丰富的后端服务,它为聊天应用提供了核心能力,包括:
-
实时聊天室:通过 Durable Objects 实现。
-
文件(图片)上传:通过 Cloudflare R2 Storage 实现。
-
AI 文本解释:通过调用外部 AI API (DeepSeek 和 Gemini) 实现。
-
AI 图片描述:通过调用外部 AI API (Gemini Vision) 实现。
-
房间数据统计:从 Durable Object 中获取统计信息。
-
前端页面托管:直接从 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' },
});
},
};
希望这份详尽的解释能帮助你完全理解这份代码的每一部分!