266 lines
6.8 KiB
JavaScript
266 lines
6.8 KiB
JavaScript
import { env, setEnv } from "./envs.js";
|
|
import { getSearchHtml } from "./utils/getHTML.js";
|
|
import searchDuckDuckGo from "./utils/searchDuckDuckGo.js";
|
|
import searchBing from "./utils/searchBing.js";
|
|
|
|
const SEARCH_ENGINES = {
|
|
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) => env.SUPPORTED_ENGINES.includes(e));
|
|
}
|
|
|
|
/**
|
|
* Search with a single engine
|
|
* @param {string} engineName - Engine name
|
|
* @param {string} query - Search query
|
|
* @returns {Promise<Array>} Search results
|
|
*/
|
|
async function searchSingle(engineName, query) {
|
|
const searchFn = SEARCH_ENGINES[engineName];
|
|
if (!searchFn) {
|
|
console.warn(`Unknown engine: ${engineName}`);
|
|
return { results: [], error: "Unknown engine" };
|
|
}
|
|
|
|
// 创建 AbortController 用于取消请求
|
|
const controller = new AbortController();
|
|
const timeout = parseInt(env.DEFAULT_TIMEOUT ?? "3000", 10);
|
|
let timeoutId;
|
|
|
|
try {
|
|
// 设置超时自动取消
|
|
timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
|
|
const results = await searchFn({ query, signal: controller.signal });
|
|
|
|
clearTimeout(timeoutId);
|
|
return { results, error: null };
|
|
} catch (error) {
|
|
const message =
|
|
error.name === "AbortError"
|
|
? `Timeout after ${timeout}ms`
|
|
: error.message || String(error);
|
|
|
|
if (error.name === "AbortError") {
|
|
console.error(`[${engineName}] ${message}`);
|
|
} else {
|
|
console.error(`[${engineName}] Error:`, message);
|
|
}
|
|
clearTimeout(timeoutId);
|
|
return { results: [], error: message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<Object>} 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 = [];
|
|
const empty = [];
|
|
const errors = {};
|
|
|
|
resultsArr.forEach((result, index) => {
|
|
const engineName = enabledEngines[index];
|
|
if (result.status === "fulfilled" && result.value.results.length > 0) {
|
|
results.push(
|
|
...result.value.results.map((item) => ({
|
|
...item,
|
|
engine: engineName,
|
|
}))
|
|
);
|
|
} else if (result.status === "fulfilled" && result.value.error) {
|
|
unresponsive.push(engineName);
|
|
errors[engineName] = result.value.error;
|
|
} else if (result.status === "fulfilled") {
|
|
empty.push(engineName);
|
|
} else {
|
|
unresponsive.push(engineName);
|
|
errors[engineName] = result.reason?.message || String(result.reason);
|
|
console.error(`[${engineName}] Rejected:`, result.reason);
|
|
}
|
|
});
|
|
|
|
return {
|
|
query,
|
|
number_of_results: results.length,
|
|
enabled_engines: enabledEngines,
|
|
unresponsive_engines: unresponsive,
|
|
empty_engines: empty,
|
|
engine_errors: errors,
|
|
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",
|
|
};
|
|
|
|
/**
|
|
* Verify authentication token
|
|
*/
|
|
function verifyToken(request, paramToken) {
|
|
// If TOKEN is not configured, skip authentication
|
|
if (!env.TOKEN) {
|
|
return true;
|
|
}
|
|
|
|
const token =
|
|
request.headers.get("Authorization")?.replace(/^Bearer\s+/i, "") ||
|
|
paramToken;
|
|
|
|
return token === env.TOKEN;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
});
|
|
}
|
|
|
|
// 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());
|
|
}
|
|
|
|
// Verify authentication token
|
|
if (!verifyToken(request, params.token)) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "Unauthorized",
|
|
message: "Invalid or missing authentication token",
|
|
}),
|
|
{
|
|
status: 401,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...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") {
|
|
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);
|
|
},
|
|
};
|