OpenWebUI 部署记录 (HuggingFace + Cloudflare + Deno) | 05.27 更新 worker.js 增加缓存

之前的的帖子好像不能修改了, 开一个新的帖子

看了佬的建议

cf反代的worker.js 更新了一下,加上了缓存
现在感觉对话加载过一次之后, 再切换会确实快很多, 刷新也快了

worker.js
// 常量定义
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 500;
const EXPIRATION_BUFFER_SECONDS = 60; // JWT过期前的缓冲时间

// 缓存策略定义
const CACHE_STRATEGIES = {
  IMMUTABLE: 'public, max-age=31536000, immutable',
  LONG: 'public, max-age=604800',
  MEDIUM: 'public, max-age=86400',
  SHORT: 'public, max-age=3600',
  NONE: 'no-store, max-age=0'
};

// 内容替换规则
const CONTENT_REPLACEMENTS = [
  { pattern: '<title>bocchi Chat</title>', replacement: '<title>OWU</title>' },
  { pattern: '"name":"Open WebUI"', replacement: '"name":"OWU"' },
  { pattern: '"short_name":"Open WebUI"', replacement: '"short_name":"OWU"' },
  { pattern: '<meta name="apple-mobile-web-app-title" content="Open WebUI">', replacement: '<meta name="apple-mobile-web-app-title" content="OWU">'}
];

// 静态资源映射
const STATIC_RESOURCES = {
  '/static/logo.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg',
  '/static/splash.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg',
  '/static/splash-dark.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg',
  '/static/favicon-dark.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg',
  '/static/favicon.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg',
  '/static/favicon-96x96.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg',
  '/static/favicon.svg': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg',
  '/static/favicon.ico': 'https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark/aionlabs-color.png', // 注意:/favicon.ico 通常由 handleStaticResource 中的重定向处理
  '/static/apple-touch-icon.png': 'https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark/aionlabs-color.png',
  '/favicon.ico': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.45.0/files/icons/aionlabs-color.svg' // 主 /favicon.ico 由 handleStaticResource 重定向处理
};

// 根据路径确定缓存策略
function getCacheStrategy(path) {
  if (path.includes('/_app/immutable/') || path.match(/\.[a-f0-9]{8}\.(js|css)$/)) {
    return CACHE_STRATEGIES.IMMUTABLE;
  }
  
  if (path.startsWith('/static/') || path.startsWith('/assets/')) {
    return CACHE_STRATEGIES.MEDIUM;
  }
  
  if (path.includes('favicon') || path.endsWith('manifest.json')) {
    return CACHE_STRATEGIES.MEDIUM;
  }
  
  if (path.endsWith('.html') || path === '/' || !path.includes('.')) {
    return CACHE_STRATEGIES.SHORT;
  }
  
  return CACHE_STRATEGIES.MEDIUM;
}

// 使用缓存处理请求
async function handleWithCache(request, handler) {
  const url = new URL(request.url);
  const path = url.pathname;
  const cacheStrategy = getCacheStrategy(path);
  
  if (cacheStrategy === CACHE_STRATEGIES.NONE || request.method !== 'GET') {
    return handler();
  }
  
  const cache = caches.default;
  let response = await cache.match(request);
  
  if (!response || request.headers.get('cache-control') === 'no-cache') {
    response = await handler();
    
    if (response.status === 200) {
      const clonedResponse = response.clone();
      const headers = new Headers(clonedResponse.headers);
      headers.set('Cache-Control', cacheStrategy);
      
      const cachedResponse = new Response(clonedResponse.body, {
        status: clonedResponse.status,
        statusText: clonedResponse.statusText,
        headers: headers
      });
      
      await cache.put(request, cachedResponse.clone());
      return cachedResponse;
    }
  }
  
  return response;
}

async function getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1, forceRefresh = false) {
  const now = Date.now() / 1000;
  let isRefreshing;
  let retries = 0;
  let jwtToken = null;
  let jwtExpiration = 0;

  while (true) {
    let result;
    try {
      result = await D1.prepare("SELECT token, expiration, is_refreshing FROM tokens WHERE id = 1").first();
    } catch (e) {
      throw new Error("Database access error");
    }

    if (result) {
      jwtToken = result.token;
      jwtExpiration = result.expiration;
      isRefreshing = result.is_refreshing;
    } else {
      jwtToken = null;
      jwtExpiration = 0;
      isRefreshing = 0;
    }

    if (jwtToken && jwtExpiration > now + EXPIRATION_BUFFER_SECONDS && !forceRefresh) {
      return jwtToken;
    }

    if (isRefreshing) {
      await new Promise((resolve) => setTimeout(resolve, 100));
      continue;
    }

    try {
      await D1.prepare("INSERT OR REPLACE INTO tokens (id, is_refreshing) VALUES (1, 1)").run();
    } catch (e) {
      throw new Error("Database update error");
    }

    try {
      if (!HF_TOKEN || !HF_SPACE_NAME || !HF_SPACE_USER) {
        throw new Error('One or more required environment variables are missing.');
      }

      const HF_API_URL = `https://huggingface.co/api/spaces/${HF_SPACE_USER}/${HF_SPACE_NAME}/jwt`;

      let response;
      while (retries < MAX_RETRIES) {
        try {
          response = await fetch(HF_API_URL, {
            headers: { "Authorization": `Bearer ${HF_TOKEN}` },
          });

          if (!response.ok) {
            if (response.status >= 500 && response.status < 600) {
              retries++;
              await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
              continue;
            } else {
              const errorText = await response.text();
              throw new Error(`Failed to fetch JWT token: ${response.status} ${response.statusText} - ${errorText}`);
            }
          }

          break;

        } catch (networkError) {
          retries++;
          await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
        }
      }

      if (retries === MAX_RETRIES) {
        throw new Error("Max retries reached while fetching JWT token");
      }

      const apiResult = await response.json();
      jwtToken = apiResult.token;

      try {
        const jwtPayload = JSON.parse(atob(jwtToken.split('.')[1]));
        jwtExpiration = jwtPayload.exp;
      } catch (_) {
        jwtExpiration = now + 3600;
      }

      try {
        await D1.prepare("INSERT OR REPLACE INTO tokens (id, token, expiration, is_refreshing) VALUES (1, ?, ?, 0)")
          .bind(jwtToken, jwtExpiration)
          .run();
      } catch (e) {
        throw new Error("Database update error");
      }

      return jwtToken;

    } catch (e) {
      try {
        await D1.prepare("UPDATE tokens SET is_refreshing = 0 WHERE id = 1").run();
      } catch (dbError) {}
      throw e;
    }
  }
}

async function initDatabase(D1) {
  try {
    await D1.batch([
      D1.prepare(`CREATE TABLE IF NOT EXISTS tokens (
        id INTEGER PRIMARY KEY,
        token TEXT,
        expiration REAL,
        is_refreshing INTEGER DEFAULT 0
      );`).bind()
    ]);
  } catch (e) {
    throw new Error("Failed to initialize database");
  }
}

// Function to handle static resources
async function handleStaticResource(request) {
  const path = new URL(request.url).pathname;
  let resourceUrl = STATIC_RESOURCES[path];
  
  // Handle special case for /favicon.ico to redirect to /static/favicon.png
  if (path === '/favicon.ico') {
    return new Response(null, {
      status: 301,
      headers: {
        'Location': '/static/favicon.png',
        'Cache-Control': 'public, max-age=2592000'
      }
    });
  }
  
  if (resourceUrl) {
    return handleWithCache(request, async () => {
      // Fetch the static resource
      const response = await fetch(resourceUrl, {
        headers: {
          'Host': '',
          'Referer': ''
        }
      });
      
      // Create a new response with caching headers
      const headers = new Headers(response.headers);
      headers.set('Cache-Control', getCacheStrategy(path));
      
      // Set appropriate content type for favicon.ico
      if (path.endsWith('.ico')) {
        headers.set('Content-Type', 'image/x-icon');
      }
      
      return new Response(response.body, {
        status: response.status,
        headers: headers
      });
    });
  }
  
  return null; // Not a static resource
}

// Function to replace content in responses
async function replaceContent(response, contentType) {
  if (!contentType || !(contentType.includes('text/html') || contentType.includes('application/json'))) {
    return response;
  }
  
  let text = await response.text();
  
  for (const replacement of CONTENT_REPLACEMENTS) {
    text = text.replace(new RegExp(replacement.pattern, 'g'), replacement.replacement);
  }
  
  const headers = new Headers(response.headers);
  headers.set('Content-Type', contentType);
  return new Response(text, {
    status: response.status,
    statusText: response.statusText,
    headers: headers
  });
}

// 处理WebSocket请求的函数
async function handleWebSocketRequest(request, env) {
  const HF_TOKEN = env.HF_TOKEN;
  const HF_SPACE_NAME = env.HF_SPACE_NAME;
  const HF_SPACE_USER = env.HF_SPACE_USER;
  const D1 = env.D1;
  
  try {
    // 获取JWT令牌
    await initDatabase(D1);
    let token = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);
    
    // 准备转发到HF空间的WebSocket连接
    const url = new URL(request.url);
    url.host = `${HF_SPACE_USER}-${HF_SPACE_NAME}.hf.space`;
    
    // 构建新的头部,包含令牌
    const headers = new Headers();
    for (const [key, value] of request.headers.entries()) {
      headers.set(key, value);
    }
    
    // 添加JWT令牌到Cookie
    const originalCookies = headers.get('Cookie') || headers.get('cookie') || '';
    const cookieString = originalCookies ? `${originalCookies}; spaces-jwt=${token}` : `spaces-jwt=${token}`;
    headers.set('Cookie', cookieString);
    
    // 使用Cloudflare的WebSocketPair API
    const webSocketPair = new WebSocketPair();
    const [client, server] = Object.values(webSocketPair);
    
    // 处理WebSocket连接
    await handleWebSocketSession(server, url.toString(), headers, token, HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);
    
    return new Response(null, {
      status: 101,
      webSocket: client,
    });
  } catch (error) {
    return new Response(`WebSocket错误: ${error.message}`, { status: 500 });
  }
}

// 处理WebSocket会话的函数
async function handleWebSocketSession(server, targetUrl, headers, token, HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1) {
  // 接受WebSocket连接
  server.accept();
  
  let backendWs = null;
  
  try {
    // 连接到后端WebSocket
    const resp = await fetch(targetUrl, {
      headers: headers,
      method: 'GET',
      cf: {
        websocket: true,
      }
    });
    
    // 检查连接是否成功
    if (!resp.webSocket) {
      server.send(JSON.stringify({ error: "无法连接到后端WebSocket" }));
      server.close(1011, "无法连接到后端WebSocket");
      return;
    }
    
    backendWs = resp.webSocket;
    backendWs.accept();
    
    // 处理令牌刷新
    let tokenRefreshInterval = setInterval(async () => {
      try {
        // 获取新令牌
        const newToken = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);
        // 发送令牌刷新消息到后端
        backendWs.send(JSON.stringify({ type: "token_refresh", token: newToken }));
      } catch (error) {
        console.error("令牌刷新失败:", error);
      }
    }, 30 * 60 * 1000); // 30分钟刷新一次
    
    // 从客户端转发消息到后端
    server.addEventListener("message", (event) => {
      if (backendWs && backendWs.readyState === 1) {
        backendWs.send(event.data);
      }
    });
    
    // 从后端转发消息到客户端
    backendWs.addEventListener("message", (event) => {
      if (server.readyState === 1) {
        server.send(event.data);
      }
    });
    
    // 处理客户端关闭
    server.addEventListener("close", (event) => {
      clearInterval(tokenRefreshInterval);
      if (backendWs) {
        backendWs.close(event.code, event.reason);
      }
    });
    
    // 处理后端关闭
    backendWs.addEventListener("close", (event) => {
      clearInterval(tokenRefreshInterval);
      if (server.readyState === 1) {
        server.close(event.code, event.reason);
      }
    });
    
    // 处理错误
    server.addEventListener("error", (event) => {
      console.error("客户端WebSocket错误:", event);
      clearInterval(tokenRefreshInterval);
    });
    
    backendWs.addEventListener("error", (event) => {
      console.error("后端WebSocket错误:", event);
      clearInterval(tokenRefreshInterval);
      if (server.readyState === 1) {
        server.send(JSON.stringify({ error: "后端连接错误" }));
      }
    });
    
  } catch (error) {
    console.error("WebSocket会话处理错误:", error);
    if (server.readyState === 1) {
      server.send(JSON.stringify({ error: error.message }));
      server.close(1011, "内部错误");
    }
  }
}

// 处理HTTP请求的函数
async function handleHttpRequest(request, env) {
  return handleWithCache(request, async () => {
    const HF_TOKEN = env.HF_TOKEN;
    const HF_SPACE_NAME = env.HF_SPACE_NAME;
    const HF_SPACE_USER = env.HF_SPACE_USER;
    const D1 = env.D1;
    
    try {
      // Check if this is a request for a static resource
      const staticResponse = await handleStaticResource(request);
      if (staticResponse) {
        return staticResponse;
      }
      
      await initDatabase(D1);
      let token = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);

      const url = new URL(request.url);
      url.host = `${HF_SPACE_USER}-${HF_SPACE_NAME}.hf.space`;

      const headers = new Headers();

      for (const [key, value] of request.headers.entries()) {
        headers.set(key, value);
      }

      // Remove Accept-Encoding to ensure content is not compressed
      headers.delete('Accept-Encoding');
      
      const originalCookies = headers.get('Cookie') || headers.get('cookie') || '';
      const cookieString = originalCookies ? `${originalCookies}; spaces-jwt=${token}` : `spaces-jwt=${token}`;
      headers.set('Cookie', cookieString);

      let newRequest = new Request(url.toString(), {
        method: request.method,
        headers: headers,
        body: request.body,
        redirect: request.redirect,
      });

      let response = await fetch(newRequest);

      if (response.status === 401 || response.status === 403) {
        token = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1, true);
        const updatedCookieString = originalCookies ? `${originalCookies}; spaces-jwt=${token}` : `spaces-jwt=${token}`;
        headers.set('Cookie', updatedCookieString);

        newRequest = new Request(url.toString(), {
          method: request.method,
          headers: headers,
          body: request.body,
          redirect: request.redirect,
        });

        response = await fetch(newRequest);
      }

      const modifiedHeaders = new Headers(response.headers);
      modifiedHeaders.delete('Link');
      modifiedHeaders.set('charset', 'utf-8');
      
      // Create a new response with the modified headers
      let modifiedResponse = new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: modifiedHeaders,
      });
      
      // Apply content replacement
      const contentType = response.headers.get('content-type');
      return await replaceContent(modifiedResponse, contentType);

    } catch (error) {
      return new Response(`Error: ${error.message}`, { status: 500 });
    }
  });
}

export default {
  async fetch(request, env) {
    // Check if this is a request for a static resource
    const path = new URL(request.url).pathname;
    if (path in STATIC_RESOURCES || path === '/favicon.ico') {
      return handleStaticResource(request);
    }
    
    // Check if it's a WebSocket request
    const upgradeHeader = request.headers.get('Upgrade');
    if (upgradeHeader && upgradeHeader.toLowerCase() === 'websocket') {
      return handleWebSocketRequest(request, env);
    } else {
      return handleHttpRequest(request, env);
    }
  },
};
21 个赞

感谢,跟着更新一下试试

5 个赞

感谢佬,有空了部署玩一下

3 个赞

@yangtb2024 你喜欢的

2 个赞

太强了,谢谢大帅哥!

2 个赞

感谢,跟着教程走了一波,目前到cf可以访问

1 个赞

收藏马克,感谢分享

1 个赞

tql 更新部署 真的变快了

1 个赞

有用就太好了 :innocent:

1 个赞

佬,cf代理后对话列表更新很慢是怎么回事?从hf进入是正常的,但是从cf代理后的地址进去看不到对话列表


借楼再问问,有佬知道怎么解决v0.6.11 不显示联网搜索等按钮的问题吗?

不显示是反向代理的特性……

这算是已知bug吗?不能及时展示对话标题不好用哇

应该得算到反向代理的头上,我直连原版没见过这样,看到很多人(包括自己)在使用了nginx的反向代理时,那些功能没有及时出来(需要等待手动刷新出现)

1 个赞

我没遇到这个问题

甚至发现我 hf 直接访问都没有联网搜索 :tieba_087:

我的也是没有联网按钮了。重新部署了一个,没反代之前都是有的,反代之后再登录就没有了。。。现在有找到解决方法吗

1 个赞
woker.js
/**
 * @file Cloudflare Worker Proxy
 * @description This worker proxies both HTTP and WebSocket requests to a specified target host.
 * It removes TypeScript-specific syntax to run in a standard JS environment and includes
 * proper CORS and WebSocket handling.
 */

// The target host to which we will proxy requests.
// You can change this to your desired target.
const TARGET_HOST = "xxx-xxx.hf.space";

/**
 * Logs a message with a timestamp.
 * In Cloudflare, you can view these logs in your Worker's dashboard.
 * @param {string} message The message to log.
 */
function log(message) {
  console.log(`[${new Date().toISOString()}] ${message}`);
}

/**
 * Returns a default User-Agent string based on the client's device type.
 * @param {boolean} isMobile - Whether the user is on a mobile device.
 * @returns {string} The User-Agent string.
 */
function getDefaultUserAgent(isMobile = false) {
  if (isMobile) {
    return "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36";
  } else {
    return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
  }
}

/**
 * Transforms the incoming request headers to be suitable for the target host.
 * @param {Headers} headers - The original request headers.
 * @returns {Headers} The transformed headers.
 */
function transformHeaders(headers) {
  const isMobile = headers.get("sec-ch-ua-mobile") === "?1";
  const newHeaders = new Headers(headers);

  newHeaders.set("User-Agent", getDefaultUserAgent(isMobile));
  newHeaders.set("Host", TARGET_HOST);
  newHeaders.set("Origin", `https://${TARGET_HOST}`);
  // You can add or remove other headers here as needed.

  return newHeaders;
}

/**
 * Handles WebSocket proxying by creating a WebSocket pair and connecting
 * one end to the client and the other to the target server.
 * @param {Request} req - The incoming WebSocket upgrade request.
 * @returns {Promise<Response>}
 */
async function handleWebSocket(req) {
  const url = new URL(req.url);
  const targetUrl = `wss://${TARGET_HOST}${url.pathname}${url.search}`;
  log(`Establishing WebSocket connection to: ${targetUrl}`);

  // In Cloudflare Workers, we instantiate a WebSocket pair.
  // One socket is returned to the client, the other is connected to the origin.
  const { 0: clientSocket, 1: serverSocket } = new WebSocketPair();

  try {
    // Establish the connection to the target WebSocket server
    const serverWs = new WebSocket(targetUrl);

    // Forward messages and events between the client and the server
    serverSocket.accept();
    serverSocket.addEventListener("message", (event) => {
      try {
        serverWs.send(event.data);
      } catch (e) {
        log(`Error sending message to server: ${e.message}`);
      }
    });

    serverWs.addEventListener("message", (event) => {
      try {
        serverSocket.send(event.data);
      } catch (e) {
        log(`Error sending message to client: ${e.message}`);
      }
    });

    // Propagate close events
    serverSocket.addEventListener("close", (event) => {
      log(`Client WebSocket closed: ${event.code} ${event.reason}`);
      if (serverWs.readyState === WebSocket.OPEN) {
        serverWs.close(event.code, event.reason);
      }
    });

    serverWs.addEventListener("close", (event) => {
      log(`Server WebSocket closed: ${event.code} ${event.reason}`);
      if (serverSocket.readyState === WebSocket.OPEN) {
        serverSocket.close(event.code, event.reason);
      }
    });

    // Propagate error events
    serverSocket.addEventListener("error", (error) => {
      log(`Client WebSocket error: ${error.message || 'Unknown error'}`);
    });

    serverWs.addEventListener("error", (error) => {
      log(`Server WebSocket error: ${error.message || 'Unknown error'}`);
    });

    // Return the response to upgrade the client's connection.
    return new Response(null, {
      status: 101, // Switching Protocols
      webSocket: clientSocket,
    });

  } catch (error) {
    log(`WebSocket connection setup error: ${error.message}`);
    return new Response(`WebSocket Error: ${error.message}`, { status: 500 });
  }
}

/**
 * The main fetch handler for the Cloudflare Worker.
 * It determines whether a request is for a WebSocket or a standard HTTP resource
 * and proxies it accordingly.
 */
export default {
  // The 'fetch' method is the entry point for all requests.
  // TypeScript type hints (`req: Request`, `: Promise<Response>`) are removed for standard JS compatibility.
  async fetch(req) {
    try {
      // Handle WebSocket upgrade requests
      if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
        return await handleWebSocket(req);
      }

      // Handle standard HTTP requests
      const url = new URL(req.url);
      const targetUrl = `https://${TARGET_HOST}${url.pathname}${url.search}`;
      log(`Proxying HTTP request: ${req.method} ${targetUrl}`);

      const proxyReq = new Request(targetUrl, {
        method: req.method,
        headers: transformHeaders(req.headers),
        body: req.body,
        redirect: "follow",
      });
      
      const response = await fetch(proxyReq);
      const responseHeaders = new Headers(response.headers);

      // Set broad CORS headers to allow cross-origin requests.
      // For production, you might want to restrict the origin.
      responseHeaders.set("Access-Control-Allow-Origin", "*");
      responseHeaders.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
      responseHeaders.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, *");

      // Handle CORS pre-flight requests (OPTIONS method)
      if (req.method === 'OPTIONS') {
        return new Response(null, {
          status: 204, // No Content
          headers: responseHeaders,
        });
      }

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: responseHeaders,
      });

    } catch (error) {
      log(`Error: ${error.message}`);
      return new Response(`Proxy Error: ${error.message}`, { status: 500 });
    }
  },
};

适用于公开空间的反代代码,拿deno改的,目测还不错(佬们可以继续优化 :tieba_087:

我的号被ban了,改到本地部署了:sob:

用哈基米改了下,目前看下来挺好的

// 常量定义
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 500;
const EXPIRATION_BUFFER_SECONDS = 60; // JWT过期前的缓冲时间

// 缓存策略定义 (简化)
const CACHE_STRATEGIES = {
  // 只为聊天记录定义了短时缓存
  SHORT: 'public, max-age=60', // 缓存60秒
  NONE: 'no-store, max-age=0'
};

// 内容替换规则 (🔥🔥🔥 新增UI隐藏规则 🔥🔥🔥)
const CONTENT_REPLACEMENTS = [
  // 品牌名称替换
  { pattern: '<title>Le OI</title>', replacement: '<title>Le OI</title>' }, 
  { pattern: '"name":"Open WebUI"', replacement: '"name":"Le OI"' },
  { pattern: '"short_name":"Open WebUI"', replacement: '"short_name":"Le OI"' },
  { pattern: '<meta name="apple-mobile-web-app-title" content="Open WebUI">', replacement: '<meta name="apple-mobile-web-app-title" content="Le OI">'},

  // 1. 移除 "代码解释器" (Code Interpreter) 开关
  // 这个规则会查找包含 "Enable Code Interpreter" 文本的父级按钮元素,并将其替换为空字符串
  { pattern: `<button type="button"[^>]*>.*<svg[^>]*>.*</svg>.*Enable Code Interpreter.*</button>`, replacement: '' },
  
  // 2. 移除 "联网搜索" (Web Search) 开关
  // 这个规则会查找包含 "Enable Web Search" 文本的父级按钮元素,并将其替换为空字符串
  { pattern: `<button type="button"[^>]*>.*<svg[^>]*>.*</svg>.*Enable Web Search.*</button>`, replacement: '' }
];

// 静态资源映射
const STATIC_RESOURCES = {
  '/static/logo.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg',
  '/static/splash.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg',
  '/static/splash-dark.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg',
  '/static/favicon-dark.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg',
  '/static/favicon.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg',
  '/static/favicon-96x96.png': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg',
  '/static/favicon.svg': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg',
  '/static/favicon.ico': 'https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark/aionlabs-color.png',
  '/static/apple-touch-icon.png': 'https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark/aionlabs-color.png',
  '/favicon.ico': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.68.0/files/icons/agui.svg'
};

// 🔥🔥🔥 [已重写] 根据路径确定缓存策略 🔥🔥🔥
// 现在的逻辑是:默认不缓存,只对聊天记录API进行短时缓存。
function getCacheStrategy(path) {
  // Open WebUI 加载聊天记录的 API 路径
  if (path === '/api/v1/chats') {
    return CACHE_STRATEGIES.SHORT;
  }
  
  // 对于所有其他请求,都不使用缓存
  return CACHE_STRATEGIES.NONE;
}

// 使用缓存处理请求 (逻辑简化)
async function handleWithCache(request, handler) {
  const url = new URL(request.url);
  const path = url.pathname;

  // 只对 GET 请求应用缓存策略
  if (request.method !== 'GET') {
    return handler();
  }

  const cacheStrategy = getCacheStrategy(path);
  // 如果策略是 NONE,直接从源站获取
  if (cacheStrategy === CACHE_STRATEGIES.NONE) {
    return handler();
  }

  const cache = caches.default;
  let response = await cache.match(request);
  
  // 如果缓存未命中或客户端要求不缓存
  if (!response || request.headers.get('cache-control') === 'no-cache') {
    response = await handler();
    
    // 只缓存成功的响应
    if (response.status === 200) {
      const clonedResponse = response.clone();
      const headers = new Headers(clonedResponse.headers);
      headers.set('Cache-Control', cacheStrategy);
      
      const cachedResponse = new Response(clonedResponse.body, {
        status: clonedResponse.status,
        statusText: clonedResponse.statusText,
        headers: headers
      });
      
      // 放入缓存
      await cache.put(request, cachedResponse.clone());
      return cachedResponse;
    }
  }
  
  return response;
}

// [无变化] 获取JWT令牌的函数
async function getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1, forceRefresh = false) {
  const now = Date.now() / 1000;
  let isRefreshing;
  let retries = 0;
  let jwtToken = null;
  let jwtExpiration = 0;
  while (true) {
    let result;
    try {
      result = await D1.prepare("SELECT token, expiration, is_refreshing FROM tokens WHERE id = 1").first();
    } catch (e) {
      throw new Error("Database access error");
    }
    if (result) {
      jwtToken = result.token;
      jwtExpiration = result.expiration;
      isRefreshing = result.is_refreshing;
    } else {
      jwtToken = null;
      jwtExpiration = 0;
      isRefreshing = 0;
    }
    if (jwtToken && jwtExpiration > now + EXPIRATION_BUFFER_SECONDS && !forceRefresh) {
      return jwtToken;
    }
    if (isRefreshing) {
      await new Promise((resolve) => setTimeout(resolve, 100));
      continue;
    }
    try {
      await D1.prepare("INSERT OR REPLACE INTO tokens (id, is_refreshing) VALUES (1, 1)").run();
    } catch (e) {
      throw new Error("Database update error");
    }
    try {
      if (!HF_TOKEN || !HF_SPACE_NAME || !HF_SPACE_USER) {
        throw new Error('One or more required environment variables are missing.');
      }
      const HF_API_URL = `https://huggingface.co/api/spaces/${HF_SPACE_USER}/${HF_SPACE_NAME}/jwt`;
      let response;
      while (retries < MAX_RETRIES) {
        try {
          response = await fetch(HF_API_URL, {
            headers: { "Authorization": `Bearer ${HF_TOKEN}` },
          });
          if (!response.ok) {
            if (response.status >= 500 && response.status < 600) {
              retries++;
              await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
              continue;
            } else {
              const errorText = await response.text();
              throw new Error(`Failed to fetch JWT token: ${response.status} ${response.statusText} - ${errorText}`);
            }
          }
          break;
        } catch (networkError) {
          retries++;
          await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
        }
      }
      if (retries === MAX_RETRIES) {
        throw new Error("Max retries reached while fetching JWT token");
      }
      const apiResult = await response.json();
      jwtToken = apiResult.token;
      try {
        const jwtPayload = JSON.parse(atob(jwtToken.split('.')[1]));
        jwtExpiration = jwtPayload.exp;
      } catch (_) {
        jwtExpiration = now + 3600;
      }
      try {
        await D1.prepare("INSERT OR REPLACE INTO tokens (id, token, expiration, is_refreshing) VALUES (1, ?, ?, 0)")
          .bind(jwtToken, jwtExpiration)
          .run();
      } catch (e) {
        throw new Error("Database update error");
      }
      return jwtToken;
    } catch (e) {
      try {
        await D1.prepare("UPDATE tokens SET is_refreshing = 0 WHERE id = 1").run();
      } catch (dbError) {}
      throw e;
    }
  }
}

// [无变化] 初始化数据库
async function initDatabase(D1) {
  try {
    await D1.batch([
      D1.prepare(`CREATE TABLE IF NOT EXISTS tokens (
        id INTEGER PRIMARY KEY,
        token TEXT,
        expiration REAL,
        is_refreshing INTEGER DEFAULT 0
      );`).bind()
    ]);
  } catch (e) {
    throw new Error("Failed to initialize database");
  }
}

// [无变化] 处理静态资源
async function handleStaticResource(request) {
  const path = new URL(request.url).pathname;
  let resourceUrl = STATIC_RESOURCES[path];
  
  if (path === '/favicon.ico') {
    return new Response(null, {
      status: 301,
      headers: {
        'Location': '/static/favicon.png',
        'Cache-Control': 'public, max-age=2592000'
      }
    });
  }
  
  if (resourceUrl) {
    // 静态资源也不再走 handleWithCache,直接 fetch
    const response = await fetch(resourceUrl, {
        headers: {
          'Host': '',
          'Referer': ''
        }
      });
    const headers = new Headers(response.headers);
    // 给静态资源一个固定的长期缓存
    headers.set('Cache-Control', 'public, max-age=604800'); 
    if (path.endsWith('.ico')) {
        headers.set('Content-Type', 'image/x-icon');
    }
    return new Response(response.body, {
        status: response.status,
        headers: headers
    });
  }
  
  return null;
}

// [无变化] 内容替换函数
async function replaceContent(response, contentType) {
  if (!contentType || (!contentType.includes('text/html') && !contentType.includes('application/json'))) {
    return response;
  }
  if (contentType.includes('application/javascript')) {
    return response; 
  }
  let text = await response.text();
  for (const replacement of CONTENT_REPLACEMENTS) {
    // 使用 'g' 标志确保替换所有匹配项
    text = text.replace(new RegExp(replacement.pattern, 'g'), replacement.replacement);
  }
  const headers = new Headers(response.headers);
  headers.delete('Content-Length');
  headers.set('Content-Type', contentType);
  
  return new Response(text, {
    status: response.status,
    statusText: response.statusText,
    headers: headers
  });
}

// [无变化] WebSocket 请求处理
async function handleWebSocketRequest(request, env) {
  const { HF_TOKEN, HF_SPACE_NAME, HF_SPACE_USER, D1 } = env;
  try {
    await initDatabase(D1);
    let token = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);
    const url = new URL(request.url);
    url.host = `${HF_SPACE_USER}-${HF_SPACE_NAME}.hf.space`;
    const headers = new Headers(request.headers);
    const originalCookies = headers.get('Cookie') || '';
    const cookieString = originalCookies ? `${originalCookies}; spaces-jwt=${token}` : `spaces-jwt=${token}`;
    headers.set('Cookie', cookieString);
    const webSocketPair = new WebSocketPair();
    const [client, server] = Object.values(webSocketPair);
    await handleWebSocketSession(server, url.toString(), headers, token, HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);
    return new Response(null, {
      status: 101,
      webSocket: client,
    });
  } catch (error) {
    return new Response(`WebSocket错误: ${error.message}`, { status: 500 });
  }
}

// [无变化] WebSocket 会话处理
async function handleWebSocketSession(server, targetUrl, headers, initialToken, HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1) {
  server.accept();
  let backendWs = null;
  let tokenRefreshInterval = null;
  try {
    const resp = await fetch(targetUrl, { headers: headers, cf: { websocket: true } });
    if (!resp.webSocket) {
      server.close(1011, "无法连接到后端WebSocket");
      return;
    }
    backendWs = resp.webSocket;
    backendWs.accept();
    tokenRefreshInterval = setInterval(async () => {
      try {
        const newToken = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);
        if (backendWs.readyState === WebSocket.OPEN) {
          backendWs.send(JSON.stringify({ type: "token_refresh", token: newToken }));
        }
      } catch (error) {
        console.error("令牌刷新失败:", error);
      }
    }, 30 * 60 * 1000);
    const cleanup = () => {
      if (tokenRefreshInterval) {
        clearInterval(tokenRefreshInterval);
        tokenRefreshInterval = null;
      }
    };
    server.addEventListener("message", (event) => {
      if (backendWs && backendWs.readyState === WebSocket.OPEN) {
        backendWs.send(event.data);
      }
    });
    backendWs.addEventListener("message", (event) => {
      if (server.readyState === WebSocket.OPEN) {
        server.send(event.data);
      }
    });
    server.addEventListener("close", (event) => {
      cleanup();
      if (backendWs && backendWs.readyState < WebSocket.CLOSING) {
        backendWs.close(event.code, event.reason);
      }
    });
    backendWs.addEventListener("close", (event) => {
      cleanup();
      if (server.readyState < WebSocket.CLOSING) {
        server.close(event.code, event.reason);
      }
    });
    server.addEventListener("error", (event) => {
      console.error("客户端WebSocket错误:", event);
      cleanup();
    });
    backendWs.addEventListener("error", (event) => {
      console.error("后端WebSocket错误:", event);
      cleanup();
      if (server.readyState === WebSocket.OPEN) {
        server.send(JSON.stringify({ error: "后端连接错误" }));
      }
    });
  } catch (error) {
    console.error("WebSocket会话处理错误:", error);
    cleanup();
    if (server.readyState === WebSocket.OPEN) {
      server.close(1011, "内部错误");
    }
  }
}

// [无变化] HTTP 请求处理
async function handleHttpRequest(request, env) {
  return handleWithCache(request, async () => {
    const { HF_TOKEN, HF_SPACE_NAME, HF_SPACE_USER, D1 } = env;
    try {
      // 静态资源已在主 fetch 函数中处理,这里无需重复
      
      await initDatabase(D1);
      let token = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1);
      const url = new URL(request.url);
      url.host = `${HF_SPACE_USER}-${HF_SPACE_NAME}.hf.space`;
      
      const newRequestHeaders = new Headers(request.headers);
      newRequestHeaders.delete('Accept-Encoding');
      
      const originalCookies = newRequestHeaders.get('Cookie') || '';
      const cookieString = originalCookies ? `${originalCookies}; spaces-jwt=${token}` : `spaces-jwt=${token}`;
      newRequestHeaders.set('Cookie', cookieString);
      
      let newRequest = new Request(url.toString(), {
        method: request.method,
        headers: newRequestHeaders,
        body: request.body,
        redirect: request.redirect,
      });
      
      let response = await fetch(newRequest);
      
      if (response.status === 401 || response.status === 403) {
        token = await getJwtToken(HF_SPACE_NAME, HF_TOKEN, HF_SPACE_USER, D1, true);
        const updatedCookieString = originalCookies ? `${originalCookies}; spaces-jwt=${token}` : `spaces-jwt=${token}`;
        newRequestHeaders.set('Cookie', updatedCookieString);
        newRequest = new Request(url.toString(), {
          method: request.method,
          headers: newRequestHeaders,
          body: request.body,
          redirect: request.redirect,
        });
        response = await fetch(newRequest);
      }
      
      const modifiedHeaders = new Headers(response.headers);
      modifiedHeaders.delete('Link');
      modifiedHeaders.delete('Content-Security-Policy');
      modifiedHeaders.set('charset', 'utf-8');
      
      let modifiedResponse = new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: modifiedHeaders,
      });
      
      const contentType = response.headers.get('content-type');
      return await replaceContent(modifiedResponse, contentType);
      
    } catch (error) {
      return new Response(`Error: ${error.message}`, { status: 500 });
    }
  });
}


// [无变化] 主入口
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    
    // 优先处理静态资源
    if (path in STATIC_RESOURCES || path === '/favicon.ico') {
      return handleStaticResource(request);
    }
    
    // 处理 WebSocket
    const upgradeHeader = request.headers.get('Upgrade');
    if (upgradeHeader && upgradeHeader.toLowerCase() === 'websocket') {
      return handleWebSocketRequest(request, env);
    } 
    
    // 其他所有请求都走 HTTP 处理
    else {
      return handleHttpRequest(request, env);
    }
  },
};
1 个赞