From fec8d0cbe013ca69382ee804967fa6ec33e1fc01 Mon Sep 17 00:00:00 2001 From: Yrobot Date: Sun, 7 Dec 2025 00:13:06 +0800 Subject: [PATCH] init --- .gitignore | 2 + envs.js | 23 +++ index.d.ts | 25 +++ utils/dev.js | 18 ++ utils/getHTML.js | 375 ++++++++++++++++++++++++++++++++++++++ utils/index.js | 6 + utils/searchBing.js | 112 ++++++++++++ utils/searchBrave.js | 70 +++++++ utils/searchDuckDuckGo.js | 146 +++++++++++++++ utils/searchGoogle.js | 34 ++++ worker.js | 225 +++++++++++++++++++++++ wrangler.toml | 63 +++++++ 12 files changed, 1099 insertions(+) create mode 100644 .gitignore create mode 100644 envs.js create mode 100644 index.d.ts create mode 100644 utils/dev.js create mode 100644 utils/getHTML.js create mode 100644 utils/index.js create mode 100644 utils/searchBing.js create mode 100644 utils/searchBrave.js create mode 100644 utils/searchDuckDuckGo.js create mode 100644 utils/searchGoogle.js create mode 100644 worker.js create mode 100644 wrangler.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73a0142 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.wrangler +node_modules \ No newline at end of file diff --git a/envs.js b/envs.js new file mode 100644 index 0000000..d5e805a --- /dev/null +++ b/envs.js @@ -0,0 +1,23 @@ +export const env = { + // 默认超时时间(毫秒) - Default timeout in milliseconds + DEFAULT_TIMEOUT: "3000", + + // 支持的搜索引擎列表 - Supported search engines + SUPPORTED_ENGINES: ["google", "brave", "duckduckgo", "bing"], + + // 默认启用的搜索引擎 - Default enabled engines + DEFAULT_ENGINES: [ + "google", + "brave", + "duckduckgo", + // "bing" // Bing 目前结果不稳定,默认禁用 - Bing results are currently unstable, disabled by default + ], + + // Google Custom Search API credentials + GOOGLE_API_KEY: null, + GOOGLE_CX: null, +}; + +export const setEnv = (newEnv) => { + Object.assign(env, newEnv); +}; diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..2c4294a --- /dev/null +++ b/index.d.ts @@ -0,0 +1,25 @@ +type ResultItem = { + title: string; + description: string; + url: string; +}; + +type TimeRange = "day" | "month" | "year"; + +export type subSearch = (params: { + query: string; + language?: string; + time_range?: TimeRange; + pageno?: number; +}) => Promise>; + +export type searchAll = (params: { + query: string; + engines?: string[]; +}) => Promise<{ + query: string; + number_of_results: number; + enabled_engines: string[]; + unresponsive_engines: string[]; + results: Array; +}>; diff --git a/utils/dev.js b/utils/dev.js new file mode 100644 index 0000000..c541482 --- /dev/null +++ b/utils/dev.js @@ -0,0 +1,18 @@ +// HTTPS_PROXY=http://localhost:7777 bun ./output/cloudflare-search/utils/dev.js + +import searchBrave from "./searchBrave.js"; +import searchGoogle from "./searchGoogle.js"; +import searchDuckDuckGo from "./searchDuckDuckGo.js"; +import searchBing from "./searchBing.js"; + +const query = "yrobot 博客"; + +(async () => { + const results = await searchBing({ + query, + }); + console.log({ results }); +})().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/utils/getHTML.js b/utils/getHTML.js new file mode 100644 index 0000000..cc81b8e --- /dev/null +++ b/utils/getHTML.js @@ -0,0 +1,375 @@ +import { env } from "../envs.js"; + +// ============================================ +// HTML 界面 - HTML UI +// ============================================ + +export function getSearchHtml() { + const GOOGLE_ENABLED = env.GOOGLE_API_KEY && env.GOOGLE_CX; + const DEFAULT_ENGINES = env.DEFAULT_ENGINES || []; + const handlerEngineDefaultChecked = (engine) => + DEFAULT_ENGINES.includes(engine) ? "checked" : ""; + return ` + + + + + 搜索聚合服务 - SearXNG Compatible + + + + + + + + + + +
+ +
+
+
+
+
+
+ + +
+
🔍
+

+ 搜索聚合服务 +

+

+ 基于 Cloudflare Workers 的多引擎搜索聚合服务,支持 Google、DuckDuckGo、Bing、Brave Search,兼容 SearXNG API 规范。 +

+
+ + +
+
+
+ + +
+ +
+ +
+ + + + +
+
+ + +
+
+ + + + + +
+

+ API 调用方式 +

+
+
+
GET 请求
+ +
+
+
POST 请求
+ +
+
+
+ + +
+

+ 支持的搜索引擎 +

+
+
+
+ Google + ${ + !GOOGLE_ENABLED + ? '(未配置)' + : "" + } +
+

+ 全球最大的搜索引擎${ + !GOOGLE_ENABLED ? ",需配置 API Key" : "" + } +

+
+
+
DuckDuckGo
+

注重隐私保护的搜索引擎

+
+
+
+ Bing + (不稳定) +
+

微软的搜索引擎,目前结果质量尚不稳定

+
+
+
Brave Search
+

独立的搜索引擎

+
+
+
+ + +
+
+ + + + 多引擎聚合 +
+
+ + + + SearXNG 兼容 +
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+
+

+ Powered by Cloudflare Workers +

+
+
+
+
+
+
+
+
+
+ + + +`; +} diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..a488b6f --- /dev/null +++ b/utils/index.js @@ -0,0 +1,6 @@ +export const normalizeResults = (results) => + results.map((result) => ({ + title: result.title || result.name || "", + url: result.url || result.link || result.href || "", + description: result.description || result.content || result.snippet || "", + })); diff --git a/utils/searchBing.js b/utils/searchBing.js new file mode 100644 index 0000000..b94573b --- /dev/null +++ b/utils/searchBing.js @@ -0,0 +1,112 @@ +import { normalizeResults } from "./index.js"; + +/** + * Extract real URL from Bing redirect URL + * Bing uses URLs like https://www.bing.com/ck/a?...&u=a1... + * The 'u' parameter contains 'a1' + base64 encoded real URL + */ +function extractRealUrl(bingUrl) { + // Return as-is if not a Bing redirect URL + if (!bingUrl.includes("bing.com/ck/a?")) { + return bingUrl; + } + + try { + // HTML entities like & need to be decoded first + const decodedUrl = bingUrl.replace(/&/g, "&"); + const url = new URL(decodedUrl); + const uParam = url.searchParams.get("u"); + + if (!uParam) { + return bingUrl; + } + + // The 'u' parameter starts with 'a1' followed by base64 encoded URL + if (uParam.startsWith("a1")) { + const base64Part = uParam.substring(2); // Remove 'a1' prefix + + // Decode from base64 (use Buffer in Node.js, atob in browser) + const decoded = + typeof Buffer !== "undefined" + ? Buffer.from(base64Part, "base64").toString("utf-8") + : atob(base64Part); + + return decoded; + } + + return bingUrl; + } catch (error) { + // If extraction fails, return original URL + return bingUrl; + } +} + +// type subSearch under index.d.ts +// TODO: support language, time_range, pageno +async function searchBing({ query, language, time_range, pageno, signal }) { + const searchUrl = `https://www.bing.com/search?q=${encodeURIComponent( + query + )}&form=QBLH&sp=-1&lq=0&pq=&sc=0-0&qs=n&sk=&cvid=&ghsh=0&ghacc=0&ghpl=`; + + const response = await fetch(searchUrl, { + signal, + headers: { + accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7", + priority: "u=0, i", + "sec-ch-ua": + '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"macOS"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + }, + }); + + if (!response.ok) { + console.error(`Bing search failed: ${response.status}`); + return []; + } + + const html = await response.text(); + + if (!html) throw new Error("html is empty"); + + const results = []; + + // Parse Bing search results from HTML + // Bing uses
  • for organic search results + // Updated regex to match current Bing HTML structure where

    and are siblings + const resultRegex = + /
  • ]*>[\s\S]*?]*>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a><\/h2>[\s\S]*?
    ]*>[\s\S]*?]*>([\s\S]*?)<\/p>/g; + + let match; + while ((match = resultRegex.exec(html)) !== null) { + let url = match[1]; + // Remove HTML tags from title + const title = match[2].replace(/<[^>]+>/g, ""); + // Remove HTML tags from description + const description = match[3].replace(/<[^>]+>/g, ""); + + // Extract real URL from Bing redirect URL + url = extractRealUrl(url); + + results.push({ + title: title.trim(), + url: url.trim(), + description: description.trim(), + }); + } + + if (results.length === 0) { + throw new Error("can't find results in html"); + } + + return normalizeResults(results); +} + +export default searchBing; diff --git a/utils/searchBrave.js b/utils/searchBrave.js new file mode 100644 index 0000000..8e9c169 --- /dev/null +++ b/utils/searchBrave.js @@ -0,0 +1,70 @@ +import { normalizeResults } from "./index.js"; + +// type subSearch under index.d.ts +// TODO: support language, time_range, pageno +async function searchBrave({ query, language, time_range, pageno, signal }) { + const searchUrl = `https://search.brave.com/search?q=${encodeURIComponent( + query + )}&spellcheck=0&source=web&summary=0`; + + const response = await fetch(searchUrl, { + signal, + headers: { + accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7", + priority: "u=0, i", + "sec-ch-ua": + '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"macOS"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + cookie: + "ui_lang=zh; country=all; safesearch=off; useLocation=0; __Secure-sku#brave-search-captcha=eyJ0eXBlIjoic2luZ2xlLXVzZSIsInZlcnNpb24iOjEsInNrdSI6ImJyYXZlLXNlYXJjaC1jYXB0Y2hhIiwicHJlc2VudGF0aW9uIjoiZXlKcGMzTjFaWElpT2lKaWNtRjJaUzVqYjIwL2MydDFQV0p5WVhabExYTmxZWEpqYUMxallYQjBZMmhoSWl3aWMybG5ibUYwZFhKbElqb2lPRFY2YVU1S1JVMXFTbXhvTTI5TE5UUXhTMFJtTlZkdU4yWjJiall2VmtVMVVHTmFkMlIzV2poMGFreDBTR0V2Tm1SVVMyVjRZWFpSZVVKSFNIcHRVR0ZLUWtSbVFUTkxlV3gzUjNaSVkyZ3ZUVXhvTTFFOVBTSXNJblFpT2lKM1dVZzJTVTFVTlRsTGMzRnpaM1ptTWpVMVRDOHpOVlZrYzFVNU1sRnlSSGxGZVdoWVlXSXdOa2x3TVhscU1HMUlUa0ozUjNGVVpYTlliWGh1TlVGREx5OXdLMUpJUjJzemJrTTJVWHBQWW1aUFpVOHlVVDA5SW4wPSJ9", + }, + referrer: "https://search.brave.com/search?source=web", + }); + + if (!response.ok) { + console.error(`Brave search failed: ${response.status}`); + return []; + } + const html = await response.text(); + + if (!html) throw new Error("html is empty"); + + let data; + + html.split("\n").forEach((line) => { + if (data) return; + if (line.trimStart().startsWith(`data: [{type:"data",`)) { + const pureLine = line.trim(); + const jsonStr = pureLine.slice( + "data: ".length, + pureLine.endsWith(",") ? -1 : undefined + ); + data = eval("(" + jsonStr + ")"); + } + }); + + if (!data) throw new Error("can't find data in html"); + + let result; + + data.forEach((item) => { + if (result) return; + if (item?.data?.body?.response?.web?.results) + result = normalizeResults(item.data.body.response.web.results); + }); + + if (!result) throw new Error("can't find result in data"); + if (!(result instanceof Array)) throw new Error("result is not an array"); + + return result; +} + +export default searchBrave; diff --git a/utils/searchDuckDuckGo.js b/utils/searchDuckDuckGo.js new file mode 100644 index 0000000..88a379b --- /dev/null +++ b/utils/searchDuckDuckGo.js @@ -0,0 +1,146 @@ +import { normalizeResults } from "./index.js"; + +/** + * 从 HTML 响应中提取搜索结果 + * DuckDuckGo 的 HTML 版本更稳定,不容易被反爬虫机制拦截 + */ +function extractResultsFromHTML(html) { + const results = []; + + // 匹配搜索结果容器 - DuckDuckGo 使用 class="result" 的 div + const resultRegex = + /]*class="[^"]*result[^"]*"[^>]*>([\s\S]*?)
    <\/div>/gi; + const matches = html.matchAll(resultRegex); + + for (const match of matches) { + const resultHTML = match[1]; + + // 提取标题 - 在

    中 + const titleMatch = resultHTML.match( + /]*class="result__title"[^>]*>[\s\S]*?]*>(.*?)<\/a>/i + ); + const title = titleMatch + ? titleMatch[1].replace(/<[^>]*>/g, "").trim() + : ""; + + // 提取 URL - DuckDuckGo 使用重定向链接,格式为 //duckduckgo.com/l/?uddg=真实URL + // 需要从 uddg 参数中解码真实 URL + const linkMatch = resultHTML.match( + /]*class="[^"]*result__a[^"]*"[^>]*href="\/\/duckduckgo\.com\/l\/\?uddg=([^"&]+)/i + ); + + let url = ""; + if (linkMatch) { + try { + // 解码 URL + url = decodeURIComponent(linkMatch[1]); + } catch (e) { + console.error("[DuckDuckGo] Failed to decode URL:", e); + } + } + + // 提取描述 - 在 中 + const snippetMatch = resultHTML.match( + /]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/i + ); + const description = snippetMatch + ? snippetMatch[1].replace(/<[^>]*>/g, "").trim() + : ""; + + // 只添加有效的结果 + if (title && url && !url.startsWith("javascript:")) { + results.push({ + title: decodeHTMLEntities(title), + url: url, + content: decodeHTMLEntities(description), + }); + } + } + + return results; +} + +/** + * 解码 HTML 实体 + */ +function decodeHTMLEntities(text) { + if (!text) return ""; + return text + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(///g, "/") + .replace(/'/g, "'") + .replace(/<[^>]*>/g, ""); // 移除任何剩余的 HTML 标签 +} + +/** + * 搜索 DuckDuckGo + * 使用 HTML 版本,更稳定且不容易被反爬虫机制拦截 + * @param {Object} params - 搜索参数 + * @param {string} params.query - 搜索关键词 + * @param {string} [params.language] - 语言设置 + * @param {string} [params.time_range] - 时间范围 + * @param {number} [params.pageno] - 页码 (默认: 0) + * @returns {Promise} 搜索结果数组 + */ +async function searchDuckDuckGo({ query, language, time_range, pageno, signal }) { + try { + if (!query) throw new Error("Query cannot be empty!"); + + // 构建查询参数 - 使用 HTML 版本 + const queryParams = { + q: query, + kl: language === "zh" ? "cn-zh" : "wt-wt", // 语言/区域 + df: time_range || "", // 时间范围 + s: String((pageno || 0) * 30), // 偏移量 + }; + + const queryString = Object.entries(queryParams) + .filter(([_, value]) => value !== "") + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join("&"); + + // 使用 /html 端点 + const searchUrl = `https://html.duckduckgo.com/html/?${queryString}`; + + const response = await fetch( + searchUrl, + { + signal, + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + Accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + Referer: "https://duckduckgo.com/", + }, + } + ); + + if (!response.ok) { + console.error(`[DuckDuckGo] Search failed: ${response.status}`); + return []; + } + + const html = await response.text(); + + // 从 HTML 中提取结果 + const results = extractResultsFromHTML(html); + + if (results.length === 0) { + console.log(`[DuckDuckGo] No results found for query: ${query}`); + return []; + } + + return normalizeResults(results); + } catch (error) { + console.error("[DuckDuckGo] Search error:", error.message); + return []; + } +} + +export default searchDuckDuckGo; diff --git a/utils/searchGoogle.js b/utils/searchGoogle.js new file mode 100644 index 0000000..931ef07 --- /dev/null +++ b/utils/searchGoogle.js @@ -0,0 +1,34 @@ +import { normalizeResults } from "./index.js"; +import { env } from "../envs.js"; + +// type subSearch under index.d.ts +// TODO: support language, time_range, pageno +async function searchGoogle({ query, language, time_range, pageno, signal }) { + const searchUrl = `https://www.googleapis.com/customsearch/v1?key=${env.GOOGLE_API_KEY}&cx=${env.GOOGLE_CX}&q=${encodeURIComponent( + query + )}`; + + const response = await fetch(searchUrl, { signal }); + + if (!response.ok) { + console.error(await response.text()); + return []; + } + + const data = await response.json(); + const results = []; + + if (data.items && Array.isArray(data.items)) { + for (const item of data.items) { + results.push({ + title: item.title, + url: item.link, + content: item.snippet || "", + }); + } + } + + return normalizeResults(results); +} + +export default searchGoogle; diff --git a/worker.js b/worker.js new file mode 100644 index 0000000..1b1e741 --- /dev/null +++ b/worker.js @@ -0,0 +1,225 @@ +import { env, setEnv } from "./envs.js"; +import { getSearchHtml } from "./utils/getHTML.js"; +import searchGoogle from "./utils/searchGoogle.js"; +import searchBrave from "./utils/searchBrave.js"; +import searchDuckDuckGo from "./utils/searchDuckDuckGo.js"; +import searchBing from "./utils/searchBing.js"; + +const SEARCH_ENGINES = { + google: searchGoogle, + brave: searchBrave, + duckduckgo: searchDuckDuckGo, + bing: searchBing, +}; + +/** + * Parse engines parameter + * @param {string|undefined} enginesParam - Comma-separated engine names + * @returns {string[]} Array of valid engine names + */ +function parseEngines(enginesParam) { + if (!enginesParam) return env.DEFAULT_ENGINES || env.SUPPORTED_ENGINES; + + return enginesParam + .split(",") + .map((e) => e.trim().toLowerCase()) + .filter((e) => { + // Filter out google if not enabled + if (e === "google" && !(env.GOOGLE_API_KEY && env.GOOGLE_CX)) return false; + return env.SUPPORTED_ENGINES.includes(e); + }); +} + +/** + * Search with a single engine + * @param {string} engineName - Engine name + * @param {string} query - Search query + * @returns {Promise} Search results + */ +async function searchSingle(engineName, query) { + const searchFn = SEARCH_ENGINES[engineName]; + if (!searchFn) { + console.warn(`Unknown engine: ${engineName}`); + return []; + } + + // 创建 AbortController 用于取消请求 + const controller = new AbortController(); + const timeout = parseInt(env.DEFAULT_TIMEOUT ?? "3000", 10); + + try { + // 设置超时自动取消 + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const result = await searchFn({ query, signal: controller.signal }); + + clearTimeout(timeoutId); + return result; + } catch (error) { + if (error.name === 'AbortError') { + console.error(`[${engineName}] Timeout after ${timeout}ms`); + } else { + console.error(`[${engineName}] Error:`, error.message); + } + return []; + } +} + +/** + * Search with all specified engines in parallel + * @param {Object} params - Search parameters + * @param {string} params.query - Search query + * @param {string[]} [params.engines] - Array of engine names + * @returns {Promise} Search response matching searchAll type + */ +async function searchAll({ query, engines }) { + const enabledEngines = parseEngines(engines?.join(",")); + + console.log(`[searchAll] query="${query}", engines=[${enabledEngines}]`); + + // Execute all searches in parallel + const resultsArr = await Promise.allSettled( + enabledEngines.map((engine) => searchSingle(engine, query)) + ); + + // Collect resultsArr and track unresponsive engines + const results = []; + const unresponsive = []; + + resultsArr.forEach((result, index) => { + const engineName = enabledEngines[index]; + if (result.status === "fulfilled" && result.value.length > 0) { + results.push( + ...result.value.map((item) => ({ + ...item, + engine: engineName, + })) + ); + } else { + unresponsive.push(engineName); + if (result.status === "rejected") { + console.error(`[${engineName}] Rejected:`, result.reason); + } + } + }); + + return { + query, + number_of_results: results.length, + enabled_engines: enabledEngines, + unresponsive_engines: unresponsive, + results, + }; +} + +/** + * CORS headers + */ +const CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*", + "Access-Control-Max-Age": "86400", +}; + +/** + * Main request handler + */ +async function handleRequest(request) { + const url = new URL(request.url); + + // Handle CORS preflight + if (request.method === "OPTIONS") { + return new Response(null, { headers: CORS_HEADERS }); + } + + // Only allow GET and POST + if (request.method !== "GET" && request.method !== "POST") { + return new Response("Method Not Allowed", { + status: 405, + headers: CORS_HEADERS, + }); + } + + // Root path: return HTML UI + if (url.pathname === "/") { + return new Response(getSearchHtml(), { + headers: { + "Content-Type": "text/html; charset=utf-8", + ...CORS_HEADERS, + }, + }); + } + + // /search path: handle API requests + if (url.pathname === "/search") { + // Parse query parameters + let params = {}; + if (request.method === "POST") { + const formData = await request.formData(); + params = Object.fromEntries(formData.entries()); + } else { + params = Object.fromEntries(url.searchParams.entries()); + } + + const query = params.q || params.query; + + if (!query) { + return new Response( + JSON.stringify({ + error: "Missing query parameter", + message: "Please provide 'q' or 'query' parameter", + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + ...CORS_HEADERS, + }, + } + ); + } + + // Parse engines parameter (optional) + const engines = params.engines?.split(",").filter(Boolean) || undefined; + + try { + const response = await searchAll({ query, engines }); + + return new Response(JSON.stringify(response, null, 2), { + headers: { + "Content-Type": "application/json", + ...CORS_HEADERS, + }, + }); + } catch (error) { + console.error("[handleRequest] Error:", error); + return new Response( + JSON.stringify({ + error: "Internal server error", + message: error.message, + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + ...CORS_HEADERS, + }, + } + ); + } + } + + // 404 for other paths + return new Response("Not Found", { + status: 404, + headers: CORS_HEADERS, + }); +} + +export default { + async fetch(request, env_param) { + setEnv(env_param); + return handleRequest(request); + }, +}; diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..4cbe011 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,63 @@ +# Cloudflare Worker 配置文件 +# 更多配置选项: https://developers.cloudflare.com/workers/wrangler/configuration/ + +# Worker 名称(修改为你想要的名称) +name = "cloudflare-search" + +# Worker 主文件 +main = "worker.js" + +# 兼容日期 - 锁定 Workers 运行时行为 +# 使用你开发时的日期,避免未来行为变更影响你的代码 +compatibility_date = "2024-12-04" + +# 自动使用 workers.dev 子域名 +# 设置为 true 会自动分配一个 xxx.workers.dev 域名 +workers_dev = true + +# 账号 ID (可选,部署时会自动填充) +# account_id = "your-account-id" + +# ============================================ +# 路由配置(可选) +# ============================================ +# 如果你想将 Worker 绑定到自定义域名,取消注释并配置: +# routes = [ +# { pattern = "search.yourdomain.com/*", zone_name = "yourdomain.com" } +# ] + +# ============================================ +# 环境变量(可选) +# ============================================ +[vars] +GOOGLE_API_KEY = "" +GOOGLE_CX = "" + +# ============================================ +# 环境配置(可选) +# ============================================ +# 生产环境配置 +# [env.production] +# name = "cloudflare-search-prod" +# workers_dev = false +# routes = [ +# { pattern = "search.yourdomain.com/*", zone_name = "yourdomain.com" } +# ] + +# 开发环境配置 +# [env.staging] +# name = "cloudflare-search-staging" +# workers_dev = true + +# ============================================ +# 资源限制(可选) +# ============================================ +# 限制 CPU 时间(毫秒) +# limits = { cpu_ms = 50 } + +# ============================================ +# 观察性配置(可选) +# ============================================ +# 启用日志推送 +# [observability] +# enabled = true