Compare commits
10 Commits
0b8e66185d
...
9e24d85cd0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e24d85cd0 | ||
|
|
df308e3ae6 | ||
|
|
5a19ea13b4 | ||
|
|
f20626e3e2 | ||
|
|
176b506782 | ||
|
|
74f866d056 | ||
|
|
91619b674b | ||
|
|
7113a13a5b | ||
|
|
7975ee1a67 | ||
|
|
2140ff8692 |
32
.npmignore
Normal file
32
.npmignore
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Cloudflare Workers files (not needed for MCP server)
|
||||||
|
.wrangler/
|
||||||
|
wrangler.toml
|
||||||
|
worker.js
|
||||||
|
envs.js
|
||||||
|
utils/
|
||||||
|
index.d.ts
|
||||||
|
|
||||||
|
# Documentation assets
|
||||||
|
screenshot.png
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
463
README.md
463
README.md
@@ -1,381 +1,428 @@
|
|||||||
# Cloudflare Search
|
# Cloudflare Search
|
||||||
|
|
||||||
> 基于 Cloudflare Workers 的聚合搜索 API 服务
|
English | [中文](./README.zh.md)
|
||||||
|
|
||||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/Yrobot/cloudflare-search)
|
> An aggregated search API service based on Cloudflare Workers
|
||||||
|
|
||||||
## 特性
|
> Supports **MCP (Model Context Protocol)**, giving AI assistants (OpenClaw, Claude Code, Codex, OpenCode) real-time web search capabilities
|
||||||
|
|
||||||
- 🔍 **多引擎聚合** - 同时使用多个搜索引擎(Google、Brave、DuckDuckGo、Bing)
|
[](https://sink.proddig.com/cloudflare-search-github)
|
||||||
- ⚡ **并行搜索** - 所有搜索引擎同时请求,快速返回结果
|
|
||||||
- 🛡️ **容错机制** - 单个引擎失败不影响其他引擎,自动标记无响应引擎
|
|
||||||
- ⏱️ **超时控制** - 可配置请求超时时间,避免长时间等待
|
|
||||||
- 🔒 **Token 鉴权** - 支持 Token 认证,保护服务不被滥用
|
|
||||||
- 🌍 **CORS 支持** - 完整的跨域资源共享支持
|
|
||||||
- 🎨 **Web 界面** - 提供简洁的搜索界面,方便测试
|
|
||||||
- ⚡ **零成本运行** - Cloudflare Workers 免费版每天 10 万次请求
|
|
||||||
|
|
||||||
## 页面展示
|
## Features
|
||||||
|
|
||||||
|
- 🔍 **Multi-engine Aggregation** - Use DuckDuckGo and Bing at the same time
|
||||||
|
- 🤖 **AI Enhanced (MCP)** - Native support for Model Context Protocol, one-click search tool integration for **OpenClaw** / **Claude Code** / **Codex**
|
||||||
|
- ⚡ **Parallel Search** - All search engines are requested concurrently for faster results
|
||||||
|
- 🛡️ **Fault Tolerance** - Failure of a single engine does not affect others; unresponsive engines are automatically marked
|
||||||
|
- ⏱️ **Timeout Control** - Configurable request timeout to avoid long waits
|
||||||
|
- 🔒 **Token Authentication** - Supports token auth to protect the service from abuse
|
||||||
|
- 🌍 **CORS Support** - Full cross-origin resource sharing support
|
||||||
|
- 🎨 **Web Interface** - Provides a clean search UI for easy testing
|
||||||
|
- ⚡ **Zero-cost Operation** - Cloudflare Workers free tier supports 100,000 requests per day
|
||||||
|
|
||||||
|
## Page Preview
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 安装方式
|
## MCP Integration: Use in OpenClaw / Claude Code / AI Agents
|
||||||
|
|
||||||
### 方式一:一键部署(推荐)
|
With MCP (Model Context Protocol), AI assistants can directly call your search service and get real-time search results.
|
||||||
|
|
||||||
点击上方 "Deploy to Cloudflare Workers" 按钮,按照提示完成部署。
|
### Installation and Configuration
|
||||||
|
|
||||||
### 方式二:使用 Wrangler CLI
|
#### 1. Deploy the Service
|
||||||
|
|
||||||
|
First, follow the guide to [Deploy Cloudflare Search](#installation-methods)
|
||||||
|
|
||||||
|
#### 2. Add MCP Server Configuration
|
||||||
|
|
||||||
|
Edit your config file ([configuration guide](https://modelcontextprotocol.io/quickstart/user)):
|
||||||
|
|
||||||
|
- **OpenClaw**: `~/.openclaw/openclaw.json`
|
||||||
|
- **Claude Code**: `~/.claude/config.json` / `~/.claude.json`
|
||||||
|
- **Claude Desktop macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- **Claude Desktop Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"cloudflare-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@yrobot/cf-search-mcp"],
|
||||||
|
"env": {
|
||||||
|
"CF_SEARCH_URL": "https://your-worker.workers.dev",
|
||||||
|
"CF_SEARCH_TOKEN": "your-token-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment Variables**:
|
||||||
|
|
||||||
|
- `CF_SEARCH_URL`: Worker deployment URL (required)
|
||||||
|
- `CF_SEARCH_TOKEN`: Auth token (required if your Worker has `TOKEN` configured)
|
||||||
|
|
||||||
|
#### 3. Verify Installation
|
||||||
|
|
||||||
|
- **OpenClaw**: Run `openclaw gateway restart` + `openclaw mcp list` and check that `cloudflare-search` appears
|
||||||
|
- **Claude Code**:
|
||||||
|
- Run `/mcp` in Claude Code, and you should see the `cloudflare-search` tool.
|
||||||
|
- Or run `claude mcp list`; seeing `cloudflare-search: npx -y @yrobot/cf-search-mcp@latest - ✓ Connected` means setup is successful
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
### Method 1: One-click Deployment (Recommended)
|
||||||
|
|
||||||
|
Click the "Deploy to Cloudflare Workers" button above and follow the prompts.
|
||||||
|
|
||||||
|
### Method 2: Use Wrangler CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 安装 Wrangler
|
# 1. Install Wrangler
|
||||||
npm install -g wrangler
|
npm install -g wrangler
|
||||||
|
|
||||||
# 2. 登录 Cloudflare
|
# 2. Login to Cloudflare
|
||||||
wrangler login
|
wrangler login
|
||||||
|
|
||||||
# 3. 克隆仓库
|
# 3. Clone the repository
|
||||||
git clone https://github.com/Yrobot/cloudflare-search.git
|
git clone https://github.com/Yrobot/cloudflare-search.git
|
||||||
cd cloudflare-search
|
cd cloudflare-search
|
||||||
|
|
||||||
# 4. 部署
|
# 4. Deploy
|
||||||
wrangler deploy
|
wrangler deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方式三:使用 Cloudflare Dashboard
|
### Method 3: Use Cloudflare Dashboard
|
||||||
|
|
||||||
1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
1. Sign in to [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
||||||
2. 进入 **Workers & Pages**
|
2. Go to **Workers & Pages**
|
||||||
3. 点击 **Create Application** > **Create Worker**
|
3. Click **Create Application** > **Create Worker**
|
||||||
4. 点击 **Upload** 上传本地代码文件夹
|
4. Click **Upload** to upload your local code folder
|
||||||
- 选择克隆的 `cloudflare-search` 文件夹
|
- Select the cloned `cloudflare-search` folder
|
||||||
- 或者手动复制 `worker.js`、`envs.js`、`utils/` 等文件
|
- Or manually copy `worker.js`, `envs.js`, `utils/`, and other files
|
||||||
5. 点击 **Save and Deploy**
|
5. Click **Save and Deploy**
|
||||||
|
|
||||||
### 获取访问地址
|
### Get Access URL
|
||||||
|
|
||||||
部署成功后,你会获得一个 Worker URL:
|
After deployment, you will get a Worker URL:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://your-worker-name.your-subdomain.workers.dev
|
https://your-worker-name.your-subdomain.workers.dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意**:这个自带的域名在某些区域可能无法直接访问,建议绑定自己的域名使用。
|
**Note**: The default domain may not be directly accessible in some regions. It is recommended to bind your own custom domain.
|
||||||
|
|
||||||
## 使用方式
|
## Usage
|
||||||
|
|
||||||
### 方式 1: Web 界面
|
### Method 1: Web Interface
|
||||||
|
|
||||||
直接访问你的 Worker URL,在网页界面输入搜索关键词:
|
Open your Worker URL directly and enter search keywords in the web UI:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://$YOUR-DOMAIN/
|
https://$YOUR-DOMAIN/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方式 2: API 请求(GET)
|
### Method 2: API Request (GET)
|
||||||
|
|
||||||
使用查询参数进行搜索:
|
Search using query parameters:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 基本搜索
|
# Basic search
|
||||||
curl "https://$YOUR-DOMAIN/search?q=cloudflare"
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare"
|
||||||
|
|
||||||
# 指定搜索引擎
|
# Specify search engines
|
||||||
curl "https://$YOUR-DOMAIN/search?q=cloudflare&engines=google,brave"
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&engines=duckduckgo,bing"
|
||||||
|
|
||||||
# 使用 token 鉴权(如果配置了 TOKEN 环境变量)
|
# Use token authentication (if TOKEN env var is configured)
|
||||||
curl "https://$YOUR-DOMAIN/search?q=cloudflare&token=$YOUR-TOKEN"
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&token=$YOUR-TOKEN"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方式 3: API 请求(POST)
|
### Method 3: API Request (POST)
|
||||||
|
|
||||||
通过 POST 表单提交搜索:
|
Submit search by POST form:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST "https://$YOUR-DOMAIN/search" \
|
curl -X POST "https://$YOUR-DOMAIN/search" \
|
||||||
-d "q=cloudflare" \
|
-d "q=cloudflare" \
|
||||||
-d "engines=google,brave"
|
-d "engines=duckduckgo,bing"
|
||||||
-d "token=$YOUR-TOKEN" # 如果配置了 TOKEN 环境变量
|
-d "token=$YOUR-TOKEN" # if TOKEN env var is configured
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 接口说明
|
## API Reference
|
||||||
|
|
||||||
### `/search` 接口
|
### `/search` Endpoint
|
||||||
|
|
||||||
用于执行搜索查询并返回聚合结果。
|
Used to execute search queries and return aggregated results.
|
||||||
|
|
||||||
#### 请求参数
|
#### Request Parameters
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 | 示例 |
|
| Parameter | Type | Required | Description | Example |
|
||||||
| ------------- | -------- | ------ | ----------------------------------------- | -------------- |
|
| ------------- | -------- | -------- | ---------------------------------------------------------- | ---------------- |
|
||||||
| `q` / `query` | `string` | yes | 搜索关键词 | `cloudflare` |
|
| `q` / `query` | `string` | yes | Search keyword | `cloudflare` |
|
||||||
| `engines` | `string` | no | 指定搜索引擎,多个用逗号分隔 | `google,brave` |
|
| `engines` | `string` | no | Specify search engines, separated by commas | `duckduckgo,bing` |
|
||||||
| `token` | `string` | no/yse | 访问令牌(当配置了 TOKEN 环境变量时必填) | `$YOUR-TOKEN` |
|
| `token` | `string` | no/yes | Access token (required when `TOKEN` env var is configured) | `$YOUR-TOKEN` |
|
||||||
|
|
||||||
**支持的搜索引擎**:
|
**Supported Search Engines**:
|
||||||
|
|
||||||
- `google` - Google 搜索(需要配置 API Key)
|
- `duckduckgo` - DuckDuckGo Search
|
||||||
- `brave` - Brave 搜索
|
- `bing` - Bing Search
|
||||||
- `duckduckgo` - DuckDuckGo 搜索
|
|
||||||
- `bing` - Bing 搜索
|
|
||||||
|
|
||||||
#### 返回值
|
#### Response Value
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
query: string; // 搜索关键词
|
query: string; // Search keyword
|
||||||
number_of_results: number; // 结果总数
|
number_of_results: number; // Total number of results
|
||||||
enabled_engines: string[]; // 启用的搜索引擎列表
|
enabled_engines: string[]; // Enabled search engine list
|
||||||
unresponsive_engines: string[]; // 无响应的搜索引擎列表
|
unresponsive_engines: string[]; // Unresponsive search engine list
|
||||||
results: Array<{
|
empty_engines: string[]; // Engines that responded with no results
|
||||||
title: string; // 结果标题
|
engine_errors: Record<string, string>; // Error message by engine
|
||||||
description: string; // 结果描述
|
results: Array<{
|
||||||
url: string; // 结果链接
|
title: string; // Result title
|
||||||
engine: string; // 来源引擎
|
description: string; // Result description
|
||||||
}>;
|
url: string; // Result link
|
||||||
|
engine: string; // Source engine
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 请求示例
|
#### Request Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# GET 请求
|
# GET request
|
||||||
curl "https://$YOUR-DOMAIN/search?q=cloudflare&engines=google,brave"
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&engines=duckduckgo,bing"
|
||||||
|
|
||||||
# POST 请求
|
# POST request
|
||||||
curl -X POST "https://$YOUR-DOMAIN/search" \
|
curl -X POST "https://$YOUR-DOMAIN/search" \
|
||||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
-d "q=cloudflare&engines=google,brave"
|
-d "q=cloudflare&engines=duckduckgo,bing"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 响应示例
|
#### Response Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"query": "cloudflare",
|
"query": "cloudflare",
|
||||||
"number_of_results": 15,
|
"number_of_results": 15,
|
||||||
"enabled_engines": ["google", "brave", "duckduckgo"],
|
"enabled_engines": ["duckduckgo", "bing"],
|
||||||
"unresponsive_engines": [],
|
"unresponsive_engines": [],
|
||||||
"results": [
|
"empty_engines": [],
|
||||||
{
|
"engine_errors": {},
|
||||||
"title": "Cloudflare - The Web Performance & Security Company",
|
"results": [
|
||||||
"description": "Cloudflare is on a mission to help build a better Internet...",
|
{
|
||||||
"url": "https://www.cloudflare.com/",
|
"title": "Cloudflare - The Web Performance & Security Company",
|
||||||
"engine": "google"
|
"description": "Cloudflare is on a mission to help build a better Internet...",
|
||||||
},
|
"url": "https://www.cloudflare.com/",
|
||||||
{
|
"engine": "duckduckgo"
|
||||||
"title": "Cloudflare Workers",
|
},
|
||||||
"description": "Deploy serverless code instantly across the globe...",
|
{
|
||||||
"url": "https://workers.cloudflare.com/",
|
"title": "Cloudflare Workers",
|
||||||
"engine": "brave"
|
"description": "Deploy serverless code instantly across the globe...",
|
||||||
}
|
"url": "https://workers.cloudflare.com/",
|
||||||
]
|
"engine": "bing"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 搜索引擎说明
|
## Search Engine Notes
|
||||||
|
|
||||||
### 支持的搜索引擎
|
### Supported Search Engines
|
||||||
|
|
||||||
| 引擎 | 说明 | 是否需要配置 | 默认启用 |
|
| Engine | Description | Configuration Required | Enabled by Default |
|
||||||
| -------------- | ------------------------ | ----------------------------- | --------------- |
|
| -------------- | ---------------------------- | ------------------------------- | ------------------ |
|
||||||
| **Google** | Google Custom Search API | 需要 GOOGLE_API_KEY GOOGLE_CX | yes |
|
| **DuckDuckGo** | DuckDuckGo HTML search | - | yes |
|
||||||
| **Brave** | Brave Search API | - | yes |
|
| **Bing** | Bing Search | - | no (unstable results) |
|
||||||
| **DuckDuckGo** | DuckDuckGo 即时答案 API | - | yes |
|
|
||||||
| **Bing** | Bing 搜索 | - | no (结果不稳定) |
|
|
||||||
|
|
||||||
### 基本工作方案
|
### Basic Working Approach
|
||||||
|
|
||||||
1. **并行请求**:所有启用的搜索引擎同时发起请求,提高响应速度
|
1. **Parallel Requests**: All enabled search engines are requested concurrently to improve response speed
|
||||||
2. **超时控制**:单个引擎超时不影响其他引擎,默认 3 秒超时
|
2. **Timeout Control**: Timeout of a single engine does not affect others; default timeout is 3 seconds
|
||||||
3. **结果聚合**:将所有成功返回的结果合并,标记来源引擎
|
3. **Result Aggregation**: Merge all successfully returned results and mark their source engine
|
||||||
4. **容错处理**:记录无响应的引擎,返回部分结果而不是完全失败
|
4. **Fault Tolerance**: Record unresponsive engines and return partial results instead of failing completely
|
||||||
|
|
||||||
## 环境变量配置
|
## Environment Variable Configuration
|
||||||
|
|
||||||
### 环境变量说明
|
### Environment Variables
|
||||||
|
|
||||||
| 变量名 | 类型 | 默认值 | 说明 |
|
| Variable Name | Type | Default | Description |
|
||||||
| ----------------- | -------- | -------- | ------------------------------------------------- |
|
| ------------------ | -------- | -------- | --------------------------------------------------- |
|
||||||
| `DEFAULT_TIMEOUT` | `string` | `"3000"` | 单个搜索引擎的超时时间(毫秒) |
|
| `DEFAULT_TIMEOUT` | `string` | `"3000"` | Timeout per search engine request (milliseconds) |
|
||||||
| `GOOGLE_API_KEY` | `string` | `null` | https://console.cloud.google.com/apis/credentials |
|
| `TOKEN` | `string` | `null` | Access token. Enables auth when configured to prevent abuse |
|
||||||
| `GOOGLE_CX` | `string` | `null` | https://programmablesearchengine.google.com/ |
|
|
||||||
| `TOKEN` | `string` | `null` | 访问令牌,配置后启用鉴权,保护服务不被滥用 |
|
|
||||||
|
|
||||||
**注意**:
|
**Notes**:
|
||||||
|
|
||||||
- Google Custom Search API 免费版每天限制 100 次请求
|
- After `TOKEN` is configured, all requests must provide a valid token
|
||||||
- `TOKEN` 配置后,所有请求都需要提供有效的 token
|
|
||||||
|
|
||||||
### 配置方式
|
### Configuration Methods
|
||||||
|
|
||||||
#### 方式 1: wrangler.toml 文件
|
#### Method 1: `wrangler.toml` File
|
||||||
|
|
||||||
编辑 `wrangler.toml` 文件中的 `[vars]` 部分:
|
Edit the `[vars]` section in `wrangler.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[vars]
|
[vars]
|
||||||
GOOGLE_API_KEY = "your-google-api-key"
|
|
||||||
GOOGLE_CX = "your-google-custom-search-cx"
|
|
||||||
DEFAULT_TIMEOUT = "3000"
|
DEFAULT_TIMEOUT = "3000"
|
||||||
TOKEN = "your-secret-token-here"
|
TOKEN = "your-secret-token-here"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 方式 2: Cloudflare Dashboard
|
#### Method 2: Cloudflare Dashboard
|
||||||
|
|
||||||
1. 进入 Worker 设置页面
|
1. Go to the Worker settings page
|
||||||
2. 找到 **Environment Variables** 部分
|
2. Find the **Environment Variables** section
|
||||||
3. 添加变量并保存
|
3. Add variables and save
|
||||||
|
|
||||||
## 使用场景
|
## Use Cases
|
||||||
|
|
||||||
### 1. 聚合搜索服务
|
### 1. Aggregated Search Service
|
||||||
|
|
||||||
构建自己的搜索聚合 API,整合多个搜索引擎结果:
|
Build your own aggregated search API and combine results from multiple search engines:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://$YOUR-DOMAIN/search?q=javascript&engines=google,brave"
|
"https://$YOUR-DOMAIN/search?q=javascript&engines=duckduckgo,bing",
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(`找到 ${data.number_of_results} 个结果`);
|
console.log(`Found ${data.number_of_results} results`);
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 前端搜索功能
|
### 2. Frontend Search Feature
|
||||||
|
|
||||||
为网站或应用添加搜索功能:
|
Add search functionality to your website or app:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
async function search(query) {
|
async function search(query) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://$YOUR-DOMAIN/search?q=${encodeURIComponent(query)}`
|
`https://$YOUR-DOMAIN/search?q=${encodeURIComponent(query)}`,
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.results;
|
return data.results;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 数据采集与分析
|
### 3. Data Collection and Analysis
|
||||||
|
|
||||||
收集多个搜索引擎的结果进行对比分析:
|
Collect results from multiple search engines for comparative analysis:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const engines = ["google", "brave", "duckduckgo"];
|
const engines = ["duckduckgo", "bing"];
|
||||||
const results = await fetch(
|
const results = await fetch(
|
||||||
`https://$YOUR-DOMAIN/search?q=AI&engines=${engines.join(",")}`
|
`https://$YOUR-DOMAIN/search?q=AI&engines=${engines.join(",")}`,
|
||||||
);
|
);
|
||||||
const data = await results.json();
|
const data = await results.json();
|
||||||
|
|
||||||
// 按引擎分组
|
// Group by engine
|
||||||
const byEngine = data.results.reduce((acc, result) => {
|
const byEngine = data.results.reduce((acc, result) => {
|
||||||
acc[result.engine] = acc[result.engine] || [];
|
acc[result.engine] = acc[result.engine] || [];
|
||||||
acc[result.engine].push(result);
|
acc[result.engine].push(result);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 注意与提醒
|
## MCP Integration
|
||||||
|
|
||||||
### 🚨 重要提示
|
With MCP (Model Context Protocol), AI assistants can directly call your search service and get real-time search results.
|
||||||
|
|
||||||
1. **使用自定义域名**
|
## Notes and Reminders
|
||||||
|
|
||||||
- Cloudflare 默认的 `*.workers.dev` 域名在某些地区可能无法访问
|
### 🚨 Important Notes
|
||||||
- **强烈建议**绑定自己的域名以获得更好的访问体验
|
|
||||||
- 在 Worker 设置中点击 **Triggers** > **Add Custom Domain** 添加自定义域名
|
|
||||||
|
|
||||||
2. **搜索引擎限制**
|
1. **Use a Custom Domain**
|
||||||
|
- The default Cloudflare `*.workers.dev` domain may be inaccessible in some regions
|
||||||
|
- It is **strongly recommended** to bind your own domain for a better access experience
|
||||||
|
- In Worker settings, click **Triggers** > **Add Custom Domain** to add a custom domain
|
||||||
|
|
||||||
- Google API 免费版每天限制 100 次请求
|
2. **Search Engine Limits**
|
||||||
- 其他搜索引擎一般没有严格限制,但请合理使用
|
- Other search engines generally do not have strict limits, but please use responsibly
|
||||||
- 频繁请求可能导致被临时限制访问
|
- Frequent requests may cause temporary rate limiting
|
||||||
|
|
||||||
3. **超时设置**
|
3. **Timeout Settings**
|
||||||
- 默认单个引擎超时 3 秒
|
- Default timeout per engine is 3 seconds
|
||||||
- 可通过环境变量 `DEFAULT_TIMEOUT` 调整
|
- Can be adjusted with `DEFAULT_TIMEOUT`
|
||||||
- 建议不要设置过长,避免整体响应时间过长
|
- Do not set it too high to avoid long overall response times
|
||||||
|
|
||||||
### 🔒 安全配置
|
### 🔒 Security Configuration
|
||||||
|
|
||||||
#### 启用鉴权
|
#### Enable Authentication
|
||||||
|
|
||||||
1. 配置 `TOKEN` 环境变量 来保护你的服务不被滥用:
|
1. Configure the `TOKEN` environment variable to protect your service from abuse:
|
||||||
|
|
||||||
- 利用 wrangler.toml 配置 TOKEN 环境变量
|
- Configure `TOKEN` in `wrangler.toml`
|
||||||
- 通过 Cloudflare Worker Dashboard 配置 TOKEN 环境变量
|
- Configure `TOKEN` in Cloudflare Worker Dashboard
|
||||||
|
|
||||||
2. 在请求时传入 token:
|
2. Pass token in requests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 访问首页
|
# Access homepage
|
||||||
https://$YOUR-DOMAIN?token=$YOUR-TOKEN
|
https://$YOUR-DOMAIN?token=$YOUR-TOKEN
|
||||||
|
|
||||||
# 使用 query/body 的 token 参数 请求 API
|
# Request API with token parameter in query/body
|
||||||
curl "https://$YOUR-DOMAIN/search?q=cloudflare&token=$YOUR-TOKEN"
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&token=$YOUR-TOKEN"
|
||||||
|
|
||||||
curl -X POST "https://$YOUR-DOMAIN/search" \
|
curl -X POST "https://$YOUR-DOMAIN/search" \
|
||||||
-d "q=cloudflare" \
|
-d "q=cloudflare" \
|
||||||
-d "token=$YOUR-TOKEN"
|
-d "token=$YOUR-TOKEN"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 常见问题
|
## FAQ
|
||||||
|
|
||||||
### Q: 为什么有些搜索引擎返回结果为空?
|
### Q: Why do some search engines return empty results?
|
||||||
|
|
||||||
A: 可能原因:
|
A: Possible reasons:
|
||||||
|
|
||||||
- 搜索引擎 API 临时不可用或响应超时
|
- Search engine API is temporarily unavailable or timed out
|
||||||
- 搜索关键词没有相关结果
|
- No relevant results for the search keyword
|
||||||
- 搜索引擎限制了访问频率
|
- Search engine has rate-limited access
|
||||||
- Google 需要配置 API Key 才能使用
|
|
||||||
|
|
||||||
可以查看返回的 `unresponsive_engines` 字段了解哪些引擎没有响应。
|
You can check the `unresponsive_engines` field in the response to see which engines did not respond.
|
||||||
|
|
||||||
### Q: 如何提高搜索速度?
|
### Q: How can I improve search speed?
|
||||||
|
|
||||||
A: 建议:
|
A: Recommendations:
|
||||||
|
|
||||||
- 减少启用的搜索引擎数量,只使用需要的引擎
|
- Reduce the number of enabled search engines and only use the engines you need
|
||||||
- 适当调整超时时间(`DEFAULT_TIMEOUT`)
|
- Adjust timeout (`DEFAULT_TIMEOUT`) appropriately
|
||||||
|
|
||||||
### Q: Bing 搜索为什么默认禁用?
|
### Q: Why is Bing search disabled by default?
|
||||||
|
|
||||||
A: Bing 搜索结果目前不够稳定,出现内容与搜索关联度低的情况。如需使用,可以在请求时手动指定:`engines=bing` 或修改 `envs.js` 中的 `DEFAULT_ENGINES`。
|
A: Bing search results are currently not stable enough, and may return content with low relevance to the query. If needed, you can manually specify `engines=bing` in requests or modify `DEFAULT_ENGINES` in `envs.js`.
|
||||||
|
|
||||||
### Q: 如何保护服务不被滥用?
|
### Q: How can I protect the service from abuse?
|
||||||
|
|
||||||
A: 建议配置 `TOKEN` 环境变量启用鉴权:
|
A: It is recommended to configure the `TOKEN` environment variable to enable authentication:
|
||||||
|
|
||||||
1. 在 `wrangler.toml` 中设置 `TOKEN = "your-random-token"`
|
1. Set `TOKEN = "your-random-token"` in `wrangler.toml`
|
||||||
2. 或在 Cloudflare Dashboard 的 Environment Variables 中添加
|
2. Or add it in Environment Variables in Cloudflare Dashboard
|
||||||
3. 配置后所有请求都需要提供有效的 token
|
3. After configuration, all requests must provide a valid token
|
||||||
|
|
||||||
鉴权失败会返回 401 错误
|
Authentication failure returns a 401 error.
|
||||||
|
|
||||||
## 免责声明
|
## Disclaimer
|
||||||
|
|
||||||
本项目仅供学习和研究使用,使用者需遵守以下规定:
|
This project is for learning and research purposes only. Users must comply with the following:
|
||||||
|
|
||||||
1. **合法使用** - 仅用于合法搜索需求,不得用于违法或侵权用途
|
1. **Lawful Use** - Only use for legal search purposes. Do not use for illegal or infringing activities
|
||||||
2. **服务条款** - 使用时需遵守 Cloudflare Workers 和各搜索引擎的服务条款
|
2. **Terms of Service** - Comply with the terms of Cloudflare Workers and each search engine
|
||||||
3. **API 限制** - 遵守各搜索引擎 API 的使用限制和配额
|
3. **API Limits** - Follow usage limits and quotas of each search engine API
|
||||||
4. **责任自负** - 使用本服务产生的任何后果由使用者自行承担
|
4. **Use at Your Own Risk** - Any consequences from using this service are the user's responsibility
|
||||||
5. **商业用途** - 如需商业使用,请确保符合相关法律法规和服务条款
|
5. **Commercial Use** - For commercial use, ensure compliance with relevant laws, regulations, and service terms
|
||||||
|
|
||||||
## 贡献
|
## Contributing
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request!
|
Issues and Pull Requests are welcome!
|
||||||
|
|
||||||
## 许可证
|
## License
|
||||||
|
|
||||||
[GPL-3 License](LICENSE)
|
[GPL-3 License](LICENSE)
|
||||||
|
|
||||||
## 相关链接
|
## Related Links
|
||||||
|
|
||||||
- [项目 GitHub](https://github.com/Yrobot/cloudflare-search)
|
- [Project GitHub](https://github.com/Yrobot/cloudflare-search)
|
||||||
- [Cloudflare Workers 文档](https://developers.cloudflare.com/workers/)
|
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
|
||||||
- [Google Custom Search API](https://developers.google.com/custom-search/v1/overview)
|
|
||||||
|
|
||||||
---
|
## Support the Project
|
||||||
|
|
||||||
**如果这个项目对你有帮助,请在 [GitHub](https://github.com/Yrobot/cloudflare-search) 上给我们一个 ⭐️**
|
If this project helps you, you can buy the author a coffee ☕
|
||||||
|
|
||||||
|
<image src="https://yrobot.top/donate_wx.jpeg" width="300"/>
|
||||||
|
|||||||
430
README.zh.md
Normal file
430
README.zh.md
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
# Cloudflare Search
|
||||||
|
|
||||||
|
[English](./README.md) | 中文
|
||||||
|
|
||||||
|
> 基于 Cloudflare Workers 的聚合搜索 API 服务
|
||||||
|
|
||||||
|
> 支持 **MCP (Model Context Protocol)**,让 AI 助手(OpenClaw、Claude Code、Codex、OpenCode)拥有实时联网搜索能力
|
||||||
|
|
||||||
|
[](https://sink.proddig.com/cloudflare-search-github)
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 🔍 **多引擎聚合** - 同时使用 DuckDuckGo 和 Bing 搜索引擎
|
||||||
|
- 🤖 **AI 增强 (MCP)** - 原生支持 Model Context Protocol,一键为 **OpenClaw** / **Claude Code** / **Codex** 添加搜索工具
|
||||||
|
- ⚡ **并行搜索** - 所有搜索引擎同时请求,快速返回结果
|
||||||
|
- 🛡️ **容错机制** - 单个引擎失败不影响其他引擎,自动标记无响应引擎
|
||||||
|
- ⏱️ **超时控制** - 可配置请求超时时间,避免长时间等待
|
||||||
|
- 🔒 **Token 鉴权** - 支持 Token 认证,保护服务不被滥用
|
||||||
|
- 🌍 **CORS 支持** - 完整的跨域资源共享支持
|
||||||
|
- 🎨 **Web 界面** - 提供简洁的搜索界面,方便测试
|
||||||
|
- ⚡ **零成本运行** - Cloudflare Workers 免费版每天 10 万次请求
|
||||||
|
|
||||||
|
## 页面展示
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## MCP 集成, 在 OpenClaw / Claude Code / AI Agent 中使用
|
||||||
|
|
||||||
|
通过 MCP (Model Context Protocol) 让 AI 助手直接调用你的搜索服务,获取实时搜索结果。
|
||||||
|
|
||||||
|
### 安装配置
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 1. 部署服务
|
||||||
|
|
||||||
|
先按照文档 [部署 Cloudflare Search](#安装方式)
|
||||||
|
|
||||||
|
#### 2. 添加 MCP 服务器配置
|
||||||
|
|
||||||
|
编辑配置文件([配置指南](https://modelcontextprotocol.io/quickstart/user)):
|
||||||
|
|
||||||
|
- **OpenClaw**: `~/.openclaw/openclaw.json`
|
||||||
|
- **Claude Code**: `~/.claude/config.json` / `~/.claude.json`
|
||||||
|
- **Claude Desktop macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- **Claude Desktop Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"cloudflare-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@yrobot/cf-search-mcp"],
|
||||||
|
"env": {
|
||||||
|
"CF_SEARCH_URL": "https://your-worker.workers.dev",
|
||||||
|
"CF_SEARCH_TOKEN": "your-token-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**环境变量说明**:
|
||||||
|
|
||||||
|
- `CF_SEARCH_URL`: Worker 部署地址(必填)
|
||||||
|
- `CF_SEARCH_TOKEN`: 鉴权 Token(如果 Worker 配置了 TOKEN 则必填)
|
||||||
|
|
||||||
|
#### 3. 验证安装
|
||||||
|
|
||||||
|
- **OpenClaw**: `openclaw gateway restart` + `openclaw mcp list` 能看到 `cloudflare-search`
|
||||||
|
- **Claude Code**:
|
||||||
|
- 在 Claude Code 中运行 `/mcp` 命令,应该能看到 `cloudflare-search` 工具。
|
||||||
|
- 或 使用 `claude mcp list`, 看到 `cloudflare-search: npx -y @yrobot/cf-search-mcp@latest - ✓ Connected` 说明配置成功
|
||||||
|
|
||||||
|
## 安装方式
|
||||||
|
|
||||||
|
### 方式一:一键部署(推荐)
|
||||||
|
|
||||||
|
点击上方 "Deploy to Cloudflare Workers" 按钮,按照提示完成部署。
|
||||||
|
|
||||||
|
### 方式二:使用 Wrangler CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 安装 Wrangler
|
||||||
|
npm install -g wrangler
|
||||||
|
|
||||||
|
# 2. 登录 Cloudflare
|
||||||
|
wrangler login
|
||||||
|
|
||||||
|
# 3. 克隆仓库
|
||||||
|
git clone https://github.com/Yrobot/cloudflare-search.git
|
||||||
|
cd cloudflare-search
|
||||||
|
|
||||||
|
# 4. 部署
|
||||||
|
wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式三:使用 Cloudflare Dashboard
|
||||||
|
|
||||||
|
1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
||||||
|
2. 进入 **Workers & Pages**
|
||||||
|
3. 点击 **Create Application** > **Create Worker**
|
||||||
|
4. 点击 **Upload** 上传本地代码文件夹
|
||||||
|
- 选择克隆的 `cloudflare-search` 文件夹
|
||||||
|
- 或者手动复制 `worker.js`、`envs.js`、`utils/` 等文件
|
||||||
|
5. 点击 **Save and Deploy**
|
||||||
|
|
||||||
|
### 获取访问地址
|
||||||
|
|
||||||
|
部署成功后,你会获得一个 Worker URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://your-worker-name.your-subdomain.workers.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:这个自带的域名在某些区域可能无法直接访问,建议绑定自己的域名使用。
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 方式 1: Web 界面
|
||||||
|
|
||||||
|
直接访问你的 Worker URL,在网页界面输入搜索关键词:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://$YOUR-DOMAIN/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式 2: API 请求(GET)
|
||||||
|
|
||||||
|
使用查询参数进行搜索:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本搜索
|
||||||
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare"
|
||||||
|
|
||||||
|
# 指定搜索引擎
|
||||||
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&engines=duckduckgo,bing"
|
||||||
|
|
||||||
|
# 使用 token 鉴权(如果配置了 TOKEN 环境变量)
|
||||||
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&token=$YOUR-TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式 3: API 请求(POST)
|
||||||
|
|
||||||
|
通过 POST 表单提交搜索:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://$YOUR-DOMAIN/search" \
|
||||||
|
-d "q=cloudflare" \
|
||||||
|
-d "engines=duckduckgo,bing"
|
||||||
|
-d "token=$YOUR-TOKEN" # 如果配置了 TOKEN 环境变量
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口说明
|
||||||
|
|
||||||
|
### `/search` 接口
|
||||||
|
|
||||||
|
用于执行搜索查询并返回聚合结果。
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 | 示例 |
|
||||||
|
| ------------- | -------- | ------ | ----------------------------------------- | -------------- |
|
||||||
|
| `q` / `query` | `string` | yes | 搜索关键词 | `cloudflare` |
|
||||||
|
| `engines` | `string` | no | 指定搜索引擎,多个用逗号分隔 | `duckduckgo,bing` |
|
||||||
|
| `token` | `string` | no/yes | 访问令牌(当配置了 TOKEN 环境变量时必填) | `$YOUR-TOKEN` |
|
||||||
|
|
||||||
|
**支持的搜索引擎**:
|
||||||
|
|
||||||
|
- `duckduckgo` - DuckDuckGo 搜索
|
||||||
|
- `bing` - Bing 搜索
|
||||||
|
|
||||||
|
#### 返回值
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
query: string; // 搜索关键词
|
||||||
|
number_of_results: number; // 结果总数
|
||||||
|
enabled_engines: string[]; // 启用的搜索引擎列表
|
||||||
|
unresponsive_engines: string[]; // 无响应的搜索引擎列表
|
||||||
|
empty_engines: string[]; // 响应成功但无结果的搜索引擎列表
|
||||||
|
engine_errors: Record<string, string>; // 各搜索引擎错误信息
|
||||||
|
results: Array<{
|
||||||
|
title: string; // 结果标题
|
||||||
|
description: string; // 结果描述
|
||||||
|
url: string; // 结果链接
|
||||||
|
engine: string; // 来源引擎
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GET 请求
|
||||||
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&engines=duckduckgo,bing"
|
||||||
|
|
||||||
|
# POST 请求
|
||||||
|
curl -X POST "https://$YOUR-DOMAIN/search" \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-d "q=cloudflare&engines=duckduckgo,bing"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "cloudflare",
|
||||||
|
"number_of_results": 15,
|
||||||
|
"enabled_engines": ["duckduckgo", "bing"],
|
||||||
|
"unresponsive_engines": [],
|
||||||
|
"empty_engines": [],
|
||||||
|
"engine_errors": {},
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"title": "Cloudflare - The Web Performance & Security Company",
|
||||||
|
"description": "Cloudflare is on a mission to help build a better Internet...",
|
||||||
|
"url": "https://www.cloudflare.com/",
|
||||||
|
"engine": "duckduckgo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Cloudflare Workers",
|
||||||
|
"description": "Deploy serverless code instantly across the globe...",
|
||||||
|
"url": "https://workers.cloudflare.com/",
|
||||||
|
"engine": "bing"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 搜索引擎说明
|
||||||
|
|
||||||
|
### 支持的搜索引擎
|
||||||
|
|
||||||
|
| 引擎 | 说明 | 是否需要配置 | 默认启用 |
|
||||||
|
| -------------- | ------------------------ | ----------------------------- | --------------- |
|
||||||
|
| **DuckDuckGo** | DuckDuckGo HTML 搜索 | - | yes |
|
||||||
|
| **Bing** | Bing 搜索 | - | no (结果不稳定) |
|
||||||
|
|
||||||
|
### 基本工作方案
|
||||||
|
|
||||||
|
1. **并行请求**:所有启用的搜索引擎同时发起请求,提高响应速度
|
||||||
|
2. **超时控制**:单个引擎超时不影响其他引擎,默认 3 秒超时
|
||||||
|
3. **结果聚合**:将所有成功返回的结果合并,标记来源引擎
|
||||||
|
4. **容错处理**:记录无响应的引擎,返回部分结果而不是完全失败
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
### 环境变量说明
|
||||||
|
|
||||||
|
| 变量名 | 类型 | 默认值 | 说明 |
|
||||||
|
| ----------------- | -------- | -------- | ------------------------------------------------- |
|
||||||
|
| `DEFAULT_TIMEOUT` | `string` | `"3000"` | 单个搜索引擎的超时时间(毫秒) |
|
||||||
|
| `TOKEN` | `string` | `null` | 访问令牌,配置后启用鉴权,保护服务不被滥用 |
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
|
||||||
|
- `TOKEN` 配置后,所有请求都需要提供有效的 token
|
||||||
|
|
||||||
|
### 配置方式
|
||||||
|
|
||||||
|
#### 方式 1: wrangler.toml 文件
|
||||||
|
|
||||||
|
编辑 `wrangler.toml` 文件中的 `[vars]` 部分:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[vars]
|
||||||
|
DEFAULT_TIMEOUT = "3000"
|
||||||
|
TOKEN = "your-secret-token-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式 2: Cloudflare Dashboard
|
||||||
|
|
||||||
|
1. 进入 Worker 设置页面
|
||||||
|
2. 找到 **Environment Variables** 部分
|
||||||
|
3. 添加变量并保存
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
### 1. 聚合搜索服务
|
||||||
|
|
||||||
|
构建自己的搜索聚合 API,整合多个搜索引擎结果:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
"https://$YOUR-DOMAIN/search?q=javascript&engines=duckduckgo,bing",
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`找到 ${data.number_of_results} 个结果`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 前端搜索功能
|
||||||
|
|
||||||
|
为网站或应用添加搜索功能:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function search(query) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://$YOUR-DOMAIN/search?q=${encodeURIComponent(query)}`,
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
return data.results;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 数据采集与分析
|
||||||
|
|
||||||
|
收集多个搜索引擎的结果进行对比分析:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const engines = ["duckduckgo", "bing"];
|
||||||
|
const results = await fetch(
|
||||||
|
`https://$YOUR-DOMAIN/search?q=AI&engines=${engines.join(",")}`,
|
||||||
|
);
|
||||||
|
const data = await results.json();
|
||||||
|
|
||||||
|
// 按引擎分组
|
||||||
|
const byEngine = data.results.reduce((acc, result) => {
|
||||||
|
acc[result.engine] = acc[result.engine] || [];
|
||||||
|
acc[result.engine].push(result);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP 集成
|
||||||
|
|
||||||
|
通过 MCP (Model Context Protocol) 让 AI 助手直接调用你的搜索服务,获取实时搜索结果。
|
||||||
|
|
||||||
|
## 注意与提醒
|
||||||
|
|
||||||
|
### 🚨 重要提示
|
||||||
|
|
||||||
|
1. **使用自定义域名**
|
||||||
|
- Cloudflare 默认的 `*.workers.dev` 域名在某些地区可能无法访问
|
||||||
|
- **强烈建议**绑定自己的域名以获得更好的访问体验
|
||||||
|
- 在 Worker 设置中点击 **Triggers** > **Add Custom Domain** 添加自定义域名
|
||||||
|
|
||||||
|
2. **搜索引擎限制**
|
||||||
|
- 其他搜索引擎一般没有严格限制,但请合理使用
|
||||||
|
- 频繁请求可能导致被临时限制访问
|
||||||
|
|
||||||
|
3. **超时设置**
|
||||||
|
- 默认单个引擎超时 3 秒
|
||||||
|
- 可通过环境变量 `DEFAULT_TIMEOUT` 调整
|
||||||
|
- 建议不要设置过长,避免整体响应时间过长
|
||||||
|
|
||||||
|
### 🔒 安全配置
|
||||||
|
|
||||||
|
#### 启用鉴权
|
||||||
|
|
||||||
|
1. 配置 `TOKEN` 环境变量 来保护你的服务不被滥用:
|
||||||
|
|
||||||
|
- 利用 wrangler.toml 配置 TOKEN 环境变量
|
||||||
|
- 通过 Cloudflare Worker Dashboard 配置 TOKEN 环境变量
|
||||||
|
|
||||||
|
2. 在请求时传入 token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 访问首页
|
||||||
|
https://$YOUR-DOMAIN?token=$YOUR-TOKEN
|
||||||
|
|
||||||
|
# 使用 query/body 的 token 参数 请求 API
|
||||||
|
curl "https://$YOUR-DOMAIN/search?q=cloudflare&token=$YOUR-TOKEN"
|
||||||
|
|
||||||
|
curl -X POST "https://$YOUR-DOMAIN/search" \
|
||||||
|
-d "q=cloudflare" \
|
||||||
|
-d "token=$YOUR-TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么有些搜索引擎返回结果为空?
|
||||||
|
|
||||||
|
A: 可能原因:
|
||||||
|
|
||||||
|
- 搜索引擎 API 临时不可用或响应超时
|
||||||
|
- 搜索关键词没有相关结果
|
||||||
|
- 搜索引擎限制了访问频率
|
||||||
|
|
||||||
|
可以查看返回的 `unresponsive_engines` 字段了解哪些引擎没有响应。
|
||||||
|
|
||||||
|
### Q: 如何提高搜索速度?
|
||||||
|
|
||||||
|
A: 建议:
|
||||||
|
|
||||||
|
- 减少启用的搜索引擎数量,只使用需要的引擎
|
||||||
|
- 适当调整超时时间(`DEFAULT_TIMEOUT`)
|
||||||
|
|
||||||
|
### Q: Bing 搜索为什么默认禁用?
|
||||||
|
|
||||||
|
A: Bing 搜索结果目前不够稳定,出现内容与搜索关联度低的情况。如需使用,可以在请求时手动指定:`engines=bing` 或修改 `envs.js` 中的 `DEFAULT_ENGINES`。
|
||||||
|
|
||||||
|
### Q: 如何保护服务不被滥用?
|
||||||
|
|
||||||
|
A: 建议配置 `TOKEN` 环境变量启用鉴权:
|
||||||
|
|
||||||
|
1. 在 `wrangler.toml` 中设置 `TOKEN = "your-random-token"`
|
||||||
|
2. 或在 Cloudflare Dashboard 的 Environment Variables 中添加
|
||||||
|
3. 配置后所有请求都需要提供有效的 token
|
||||||
|
|
||||||
|
鉴权失败会返回 401 错误
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
|
||||||
|
本项目仅供学习和研究使用,使用者需遵守以下规定:
|
||||||
|
|
||||||
|
1. **合法使用** - 仅用于合法搜索需求,不得用于违法或侵权用途
|
||||||
|
2. **服务条款** - 使用时需遵守 Cloudflare Workers 和各搜索引擎的服务条款
|
||||||
|
3. **API 限制** - 遵守各搜索引擎 API 的使用限制和配额
|
||||||
|
4. **责任自负** - 使用本服务产生的任何后果由使用者自行承担
|
||||||
|
5. **商业用途** - 如需商业使用,请确保符合相关法律法规和服务条款
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request!
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
[GPL-3 License](LICENSE)
|
||||||
|
|
||||||
|
## 相关链接
|
||||||
|
|
||||||
|
- [项目 GitHub](https://github.com/Yrobot/cloudflare-search)
|
||||||
|
- [Cloudflare Workers 文档](https://developers.cloudflare.com/workers/)
|
||||||
|
|
||||||
|
## 支持一下
|
||||||
|
|
||||||
|
如果这个项目对你有帮助,可以请作者喝杯咖啡 ☕
|
||||||
|
|
||||||
|
<image src="https://yrobot.top/donate_wx.jpeg" width="300"/>
|
||||||
8
envs.js
8
envs.js
@@ -3,20 +3,14 @@ export const env = {
|
|||||||
DEFAULT_TIMEOUT: "3000",
|
DEFAULT_TIMEOUT: "3000",
|
||||||
|
|
||||||
// 支持的搜索引擎列表 - Supported search engines
|
// 支持的搜索引擎列表 - Supported search engines
|
||||||
SUPPORTED_ENGINES: ["google", "brave", "duckduckgo", "bing"],
|
SUPPORTED_ENGINES: ["duckduckgo", "bing"],
|
||||||
|
|
||||||
// 默认启用的搜索引擎 - Default enabled engines
|
// 默认启用的搜索引擎 - Default enabled engines
|
||||||
DEFAULT_ENGINES: [
|
DEFAULT_ENGINES: [
|
||||||
"google",
|
|
||||||
"brave",
|
|
||||||
"duckduckgo",
|
"duckduckgo",
|
||||||
// "bing" // Bing 目前结果不稳定,默认禁用 - Bing results are currently unstable, disabled by default
|
// "bing" // Bing 目前结果不稳定,默认禁用 - Bing results are currently unstable, disabled by default
|
||||||
],
|
],
|
||||||
|
|
||||||
// Google Custom Search API credentials
|
|
||||||
GOOGLE_API_KEY: null,
|
|
||||||
GOOGLE_CX: null,
|
|
||||||
|
|
||||||
// API 访问令牌 - API access token for authentication
|
// API 访问令牌 - API access token for authentication
|
||||||
// 如果设置了 TOKEN,则所有 /search 请求都需要在 header 或 query 中提供此 token
|
// 如果设置了 TOKEN,则所有 /search 请求都需要在 header 或 query 中提供此 token
|
||||||
// If TOKEN is set, all /search requests must provide this token in header or query
|
// If TOKEN is set, all /search requests must provide this token in header or query
|
||||||
|
|||||||
4
index.d.ts
vendored
4
index.d.ts
vendored
@@ -22,6 +22,8 @@ export type searchAll = (params: {
|
|||||||
number_of_results: number;
|
number_of_results: number;
|
||||||
enabled_engines: string[];
|
enabled_engines: string[];
|
||||||
unresponsive_engines: string[];
|
unresponsive_engines: string[];
|
||||||
|
empty_engines: string[];
|
||||||
|
engine_errors: Record<string, string>;
|
||||||
results: Array<ResultItem & { engine: string }>;
|
results: Array<ResultItem & { engine: string }>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -29,7 +31,5 @@ export interface Env {
|
|||||||
DEFAULT_TIMEOUT?: string;
|
DEFAULT_TIMEOUT?: string;
|
||||||
SUPPORTED_ENGINES?: string[];
|
SUPPORTED_ENGINES?: string[];
|
||||||
DEFAULT_ENGINES?: string[];
|
DEFAULT_ENGINES?: string[];
|
||||||
GOOGLE_API_KEY?: string;
|
|
||||||
GOOGLE_CX?: string;
|
|
||||||
TOKEN?: string;
|
TOKEN?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
205
mcp/cf-search-mcp.js
Normal file
205
mcp/cf-search-mcp.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cloudflare Search MCP Server
|
||||||
|
*
|
||||||
|
* This MCP server provides access to the Cloudflare Search API,
|
||||||
|
* allowing AI assistants to search across multiple search engines.
|
||||||
|
*
|
||||||
|
* Environment Variables:
|
||||||
|
* - CF_SEARCH_URL: The URL of your Cloudflare Search Worker (required)
|
||||||
|
* - CF_SEARCH_TOKEN: Authentication token for the search API (optional)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
CallToolRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
// Get configuration from environment variables
|
||||||
|
const CF_SEARCH_URL = process.env.CF_SEARCH_URL;
|
||||||
|
const CF_SEARCH_TOKEN = process.env.CF_SEARCH_TOKEN;
|
||||||
|
|
||||||
|
if (!CF_SEARCH_URL) {
|
||||||
|
console.error("Error: CF_SEARCH_URL environment variable is required");
|
||||||
|
console.error(
|
||||||
|
"Example: export CF_SEARCH_URL=https://your-worker.workers.dev",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the Cloudflare Search API
|
||||||
|
*/
|
||||||
|
async function searchAPI(query, engines = null) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ q: query });
|
||||||
|
|
||||||
|
if (engines && engines.length > 0) {
|
||||||
|
params.append("engines", engines.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CF_SEARCH_TOKEN) {
|
||||||
|
params.append("token", CF_SEARCH_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${CF_SEARCH_URL}/search?${params.toString()}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to search: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and configure the MCP server
|
||||||
|
*/
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "cloudflare-search",
|
||||||
|
version: "1.1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const SEARCH_INPUT_SCHEMA = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "The search query string",
|
||||||
|
},
|
||||||
|
engines: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["duckduckgo", "bing"],
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Optional: Array of search engines to use. If not specified, uses default engines. " +
|
||||||
|
"Available engines: duckduckgo, bing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["query"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for listing available tools
|
||||||
|
*/
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "web_search",
|
||||||
|
description:
|
||||||
|
"Search the web for current information, news, or any topic. " +
|
||||||
|
"Uses DuckDuckGo and Bing simultaneously " +
|
||||||
|
"and returns aggregated results with source URLs. " +
|
||||||
|
"Use this when you need real-time information not in your training data. ",
|
||||||
|
inputSchema: SEARCH_INPUT_SCHEMA,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search",
|
||||||
|
description:
|
||||||
|
"Search across DuckDuckGo and Bing and return aggregated results. " +
|
||||||
|
"This tool provides comprehensive search results from multiple sources, with source attribution for each result.",
|
||||||
|
inputSchema: SEARCH_INPUT_SCHEMA,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for tool execution
|
||||||
|
*/
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
if (
|
||||||
|
request.params.name !== "search" &&
|
||||||
|
request.params.name !== "web_search"
|
||||||
|
) {
|
||||||
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.params.arguments) {
|
||||||
|
throw new Error("Missing arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { query, engines } = request.params.arguments;
|
||||||
|
|
||||||
|
if (!query || typeof query !== "string") {
|
||||||
|
throw new Error("Query must be a non-empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await searchAPI(query, engines);
|
||||||
|
|
||||||
|
// Format the results for better readability
|
||||||
|
const formattedResults = result.results
|
||||||
|
.map((item, index) => {
|
||||||
|
return `${index + 1}. [${item.engine.toUpperCase()}] ${item.title}\n ${item.description}\n ${item.url}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const summary = [
|
||||||
|
`Search Query: "${result.query}"`,
|
||||||
|
`Total Results: ${result.number_of_results}`,
|
||||||
|
`Engines Used: ${result.enabled_engines.join(", ")}`,
|
||||||
|
result.unresponsive_engines.length > 0
|
||||||
|
? `Unresponsive Engines: ${result.unresponsive_engines.join(", ")}`
|
||||||
|
: null,
|
||||||
|
"",
|
||||||
|
"Results:",
|
||||||
|
formattedResults,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: summary,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Search failed: ${error.message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
console.error("Cloudflare Search MCP Server running on stdio");
|
||||||
|
console.error(`Connected to: ${CF_SEARCH_URL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
43
package.json
Normal file
43
package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@yrobot/cf-search-mcp",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "MCP server for Cloudflare Search API - Aggregated search across multiple engines",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "mcp/cf-search-mcp.js",
|
||||||
|
"bin": {
|
||||||
|
"cf-search-mcp": "./mcp/cf-search-mcp.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node mcp/cf-search-mcp.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"search",
|
||||||
|
"web-search",
|
||||||
|
"cloudflare",
|
||||||
|
"cloudflare-workers",
|
||||||
|
"self-hosted",
|
||||||
|
"ai-agent",
|
||||||
|
"openclaw",
|
||||||
|
"free-search",
|
||||||
|
"privacy",
|
||||||
|
"aggregated-search",
|
||||||
|
"duckduckgo",
|
||||||
|
"bing"
|
||||||
|
],
|
||||||
|
"author": "Yrobot",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Yrobot/cloudflare-search.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
@@ -1,7 +1,5 @@
|
|||||||
// HTTPS_PROXY=http://localhost:7777 bun ./output/cloudflare-search/utils/dev.js
|
// 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 searchDuckDuckGo from "./searchDuckDuckGo.js";
|
||||||
import searchBing from "./searchBing.js";
|
import searchBing from "./searchBing.js";
|
||||||
|
|
||||||
|
|||||||
219
utils/getHTML.js
219
utils/getHTML.js
@@ -5,7 +5,6 @@ import { env } from "../envs.js";
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export function getSearchHtml() {
|
export function getSearchHtml() {
|
||||||
const GOOGLE_ENABLED = env.GOOGLE_API_KEY && env.GOOGLE_CX;
|
|
||||||
const TOKEN_ENABLED = !!env.TOKEN;
|
const TOKEN_ENABLED = !!env.TOKEN;
|
||||||
const DEFAULT_ENGINES = env.DEFAULT_ENGINES || [];
|
const DEFAULT_ENGINES = env.DEFAULT_ENGINES || [];
|
||||||
const handlerEngineDefaultChecked = (engine) =>
|
const handlerEngineDefaultChecked = (engine) =>
|
||||||
@@ -94,40 +93,32 @@ export function getSearchHtml() {
|
|||||||
</h1>
|
</h1>
|
||||||
<div class="mt-6 text-base text-zinc-600 dark:text-zinc-400">
|
<div class="mt-6 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
<p class="">
|
<p class="">
|
||||||
基于 Cloudflare Workers 的多引擎聚合搜索服务。并行搜索 Google、DuckDuckGo、Bing、Brave 四大引擎,快速获取更全面的搜索结果。
|
基于 Cloudflare Workers 的多引擎聚合搜索服务。并行搜索 DuckDuckGo、Bing 等搜索引擎,快速获取更全面的搜索结果。
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
如果这个项目对你有帮助,请在 <a href="https://github.com/Yrobot/cloudflare-search" target="_blank" class="hover:underline font-medium">GitHub 上给我们一个 ⭐️</a>
|
如果这个项目对你有帮助,可以
|
||||||
|
<a
|
||||||
|
href="https://yrobot.top/donate_wx.jpeg"
|
||||||
|
target="_blank"
|
||||||
|
title="如果这个项目对你有帮助,可以请我喝杯咖啡 ☕"
|
||||||
|
class="hover:underline"
|
||||||
|
>
|
||||||
|
请作者喝杯咖啡 ☕️
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 服务状态 -->
|
<!-- 服务状态 -->
|
||||||
<div class="mt-8 rounded-2xl border ${
|
<div class="mt-8 rounded-2xl border ${
|
||||||
GOOGLE_ENABLED && !TOKEN_ENABLED
|
TOKEN_ENABLED
|
||||||
? "border-green-200 bg-green-50 dark:border-green-800/40 dark:bg-green-900/10"
|
|
||||||
: !GOOGLE_ENABLED || TOKEN_ENABLED
|
|
||||||
? "border-amber-200 bg-amber-50 dark:border-amber-800/40 dark:bg-amber-900/10"
|
? "border-amber-200 bg-amber-50 dark:border-amber-800/40 dark:bg-amber-900/10"
|
||||||
: "border-zinc-100 dark:border-zinc-700/40"
|
: "border-green-200 bg-green-50 dark:border-green-800/40 dark:bg-green-900/10"
|
||||||
} p-6">
|
} p-6">
|
||||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 mb-3">
|
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 mb-3">
|
||||||
⚙️ 服务配置状态
|
⚙️ 服务配置状态
|
||||||
</h2>
|
</h2>
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-zinc-700 dark:text-zinc-300">Google 搜索引擎</span>
|
|
||||||
<span class="${
|
|
||||||
GOOGLE_ENABLED
|
|
||||||
? "text-green-600 dark:text-green-400"
|
|
||||||
: "text-amber-600 dark:text-amber-400"
|
|
||||||
}">
|
|
||||||
${
|
|
||||||
GOOGLE_ENABLED
|
|
||||||
? "✓ 已配置"
|
|
||||||
: "○ 未配置 (需要 API Key)"
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-zinc-700 dark:text-zinc-300">访问鉴权</span>
|
<span class="text-zinc-700 dark:text-zinc-300">访问鉴权</span>
|
||||||
<span class="${
|
<span class="${
|
||||||
@@ -139,38 +130,17 @@ export function getSearchHtml() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-zinc-700 dark:text-zinc-300">其他引擎 (Brave/DuckDuckGo/Bing)</span>
|
<span class="text-zinc-700 dark:text-zinc-300">搜索引擎 (DuckDuckGo/Bing)</span>
|
||||||
<span class="text-green-600 dark:text-green-400">✓ 可用</span>
|
<span class="text-green-600 dark:text-green-400">✓ 可用</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${
|
|
||||||
!GOOGLE_ENABLED
|
|
||||||
? `
|
|
||||||
<div class="mt-4 pt-4 border-t border-amber-200 dark:border-amber-800/40">
|
|
||||||
<p class="text-xs text-amber-700 dark:text-amber-400">
|
|
||||||
💡 提示:配置 Google API 后可使用 Google 搜索引擎。在 Cloudflare Dashboard 的 Worker 设置中添加环境变量 <code class="px-1 py-0.5 bg-amber-100 dark:bg-amber-900/30 rounded">GOOGLE_API_KEY</code> 和 <code class="px-1 py-0.5 bg-amber-100 dark:bg-amber-900/30 rounded">GOOGLE_CX</code>。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
${
|
||||||
!TOKEN_ENABLED
|
!TOKEN_ENABLED
|
||||||
? `
|
? `
|
||||||
<div class="mt-4 pt-4 border-t ${
|
<div class="mt-4 pt-4 border-t border-green-200 dark:border-green-800/40">
|
||||||
!GOOGLE_ENABLED
|
<p class="text-xs text-green-700 dark:text-green-400">
|
||||||
? "border-amber-200 dark:border-amber-800/40"
|
|
||||||
: "border-green-200 dark:border-green-800/40"
|
|
||||||
}">
|
|
||||||
<p class="text-xs ${
|
|
||||||
!GOOGLE_ENABLED
|
|
||||||
? "text-amber-700 dark:text-amber-400"
|
|
||||||
: "text-green-700 dark:text-green-400"
|
|
||||||
}">
|
|
||||||
💡 建议:为防止服务被滥用,建议在 Cloudflare Dashboard 的 Worker 设置中添加环境变量 <code class="px-1 py-0.5 ${
|
💡 建议:为防止服务被滥用,建议在 Cloudflare Dashboard 的 Worker 设置中添加环境变量 <code class="px-1 py-0.5 ${
|
||||||
!GOOGLE_ENABLED
|
"bg-green-100 dark:bg-green-900/30"
|
||||||
? "bg-amber-100 dark:bg-amber-900/30"
|
|
||||||
: "bg-green-100 dark:bg-green-900/30"
|
|
||||||
} rounded">TOKEN</code> 启用访问鉴权。
|
} rounded">TOKEN</code> 启用访问鉴权。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,29 +173,6 @@ export function getSearchHtml() {
|
|||||||
选择搜索引擎 (可多选)
|
选择搜索引擎 (可多选)
|
||||||
</label>
|
</label>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<label class="flex items-center space-x-2 ${
|
|
||||||
GOOGLE_ENABLED
|
|
||||||
? "cursor-pointer"
|
|
||||||
: "cursor-not-allowed opacity-50"
|
|
||||||
}" ${
|
|
||||||
!GOOGLE_ENABLED ? 'title="Google 引擎需要配置 API Key"' : ""
|
|
||||||
}>
|
|
||||||
<input type="checkbox" name="engine" value="google" ${
|
|
||||||
GOOGLE_ENABLED
|
|
||||||
? handlerEngineDefaultChecked("google")
|
|
||||||
: "disabled"
|
|
||||||
} class="rounded text-blue-500 focus:ring-blue-500 ${
|
|
||||||
!GOOGLE_ENABLED ? "cursor-not-allowed" : ""
|
|
||||||
}">
|
|
||||||
<span class="text-sm text-zinc-700 dark:text-zinc-300">
|
|
||||||
Google
|
|
||||||
${
|
|
||||||
!GOOGLE_ENABLED
|
|
||||||
? '<span class="text-xs text-zinc-400 dark:text-zinc-500 ml-1">(未配置)</span>'
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center space-x-2 cursor-pointer">
|
<label class="flex items-center space-x-2 cursor-pointer">
|
||||||
<input type="checkbox" name="engine" value="duckduckgo" ${handlerEngineDefaultChecked(
|
<input type="checkbox" name="engine" value="duckduckgo" ${handlerEngineDefaultChecked(
|
||||||
"duckduckgo"
|
"duckduckgo"
|
||||||
@@ -241,12 +188,6 @@ export function getSearchHtml() {
|
|||||||
<span class="text-xs text-zinc-400 dark:text-zinc-500 ml-1">(不稳定)</span>
|
<span class="text-xs text-zinc-400 dark:text-zinc-500 ml-1">(不稳定)</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center space-x-2 cursor-pointer">
|
|
||||||
<input type="checkbox" name="engine" value="brave" ${handlerEngineDefaultChecked(
|
|
||||||
"brave"
|
|
||||||
)} class="rounded text-blue-500 focus:ring-blue-500">
|
|
||||||
<span class="text-sm text-zinc-700 dark:text-zinc-300">Brave</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -333,14 +274,16 @@ export function getSearchHtml() {
|
|||||||
<pre class="text-xs overflow-x-auto"><code>{
|
<pre class="text-xs overflow-x-auto"><code>{
|
||||||
"query": "cloudflare", // 搜索关键词
|
"query": "cloudflare", // 搜索关键词
|
||||||
"number_of_results": 15, // 结果总数
|
"number_of_results": 15, // 结果总数
|
||||||
"enabled_engines": ["google", ...], // 启用的搜索引擎列表
|
"enabled_engines": ["duckduckgo", ...], // 启用的搜索引擎列表
|
||||||
"unresponsive_engines": [], // 无响应的搜索引擎列表
|
"unresponsive_engines": [], // 无响应的搜索引擎列表
|
||||||
|
"empty_engines": [], // 响应成功但无结果的搜索引擎列表
|
||||||
|
"engine_errors": {}, // 各搜索引擎错误信息
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"title": "...", // 结果标题
|
"title": "...", // 结果标题
|
||||||
"description": "...", // 结果描述
|
"description": "...", // 结果描述
|
||||||
"url": "...", // 结果链接
|
"url": "...", // 结果链接
|
||||||
"engine": "google" // 来源引擎
|
"engine": "duckduckgo" // 来源引擎
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}</code></pre>
|
}</code></pre>
|
||||||
@@ -355,21 +298,6 @@ export function getSearchHtml() {
|
|||||||
🚀 支持的搜索引擎
|
🚀 支持的搜索引擎
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50 ${
|
|
||||||
!GOOGLE_ENABLED ? "opacity-50" : ""
|
|
||||||
}">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">Google</div>
|
|
||||||
${
|
|
||||||
GOOGLE_ENABLED
|
|
||||||
? '<span class="text-xs text-green-600 dark:text-green-400">✓ 可用</span>'
|
|
||||||
: '<span class="text-xs text-amber-600 dark:text-amber-400">○ 需配置</span>'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-zinc-600 dark:text-zinc-400">
|
|
||||||
全球最大的搜索引擎,需要配置 Google Custom Search API
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
|
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">DuckDuckGo</div>
|
<div class="font-medium text-zinc-900 dark:text-zinc-100">DuckDuckGo</div>
|
||||||
@@ -377,13 +305,6 @@ export function getSearchHtml() {
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-zinc-600 dark:text-zinc-400">注重隐私保护的搜索引擎,无需配置</p>
|
<p class="text-xs text-zinc-600 dark:text-zinc-400">注重隐私保护的搜索引擎,无需配置</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">Brave Search</div>
|
|
||||||
<span class="text-xs text-green-600 dark:text-green-400">✓ 可用</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-zinc-600 dark:text-zinc-400">独立的搜索引擎,无需配置</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50 opacity-60">
|
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50 opacity-60">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">Bing</div>
|
<div class="font-medium text-zinc-900 dark:text-zinc-100">Bing</div>
|
||||||
@@ -415,11 +336,6 @@ export function getSearchHtml() {
|
|||||||
在 Cloudflare Dashboard → Workers & Pages → 您的 Worker → 设置 → 变量 中添加:
|
在 Cloudflare Dashboard → Workers & Pages → 您的 Worker → 设置 → 变量 中添加:
|
||||||
</p>
|
</p>
|
||||||
<ul class="text-xs text-blue-700 dark:text-blue-300 mt-1 ml-4 list-disc">
|
<ul class="text-xs text-blue-700 dark:text-blue-300 mt-1 ml-4 list-disc">
|
||||||
${
|
|
||||||
!GOOGLE_ENABLED
|
|
||||||
? '<li><code class="px-1 py-0.5 bg-blue-100 dark:bg-blue-900/30 rounded">GOOGLE_API_KEY</code> 和 <code class="px-1 py-0.5 bg-blue-100 dark:bg-blue-900/30 rounded">GOOGLE_CX</code> - 启用 Google 搜索</li>'
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
${
|
||||||
!TOKEN_ENABLED
|
!TOKEN_ENABLED
|
||||||
? '<li><code class="px-1 py-0.5 bg-blue-100 dark:bg-blue-900/30 rounded">TOKEN</code> - 启用访问鉴权 (建议)</li>'
|
? '<li><code class="px-1 py-0.5 bg-blue-100 dark:bg-blue-900/30 rounded">TOKEN</code> - 启用访问鉴权 (建议)</li>'
|
||||||
@@ -443,6 +359,87 @@ export function getSearchHtml() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- MCP 集成 -->
|
||||||
|
<div class="mt-8 rounded-2xl border border-purple-200 bg-purple-50 dark:border-purple-800/40 dark:bg-purple-900/10 p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-purple-900 dark:text-purple-100 mb-4">
|
||||||
|
🤖 MCP 集成
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-purple-800 dark:text-purple-200 mb-4">
|
||||||
|
通过 MCP (Model Context Protocol) 让 AI 助手 (如 Claude) 直接调用你的搜索服务,获取实时搜索结果。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- 步骤 1 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-purple-200 dark:bg-purple-800 text-purple-900 dark:text-purple-100 rounded-full flex items-center justify-center text-xs font-semibold mr-3">1</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">添加 MCP 服务器配置</p>
|
||||||
|
<p class="text-xs text-purple-700 dark:text-purple-300 mb-3">
|
||||||
|
编辑配置文件 (<a href="https://modelcontextprotocol.io/quickstart/user" target="_blank" class="underline hover:text-purple-900 dark:hover:text-purple-100">配置指南</a>):
|
||||||
|
</p>
|
||||||
|
<div class="space-y-1 text-xs text-purple-700 dark:text-purple-300 mb-3">
|
||||||
|
<p><strong>Claude Code:</strong> <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">~/.claude/config.json</code> 或 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">~/.claude.json</code></p>
|
||||||
|
<p><strong>Claude Desktop (macOS):</strong> <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">~/Library/Application Support/Claude/claude_desktop_config.json</code></p>
|
||||||
|
<p><strong>Claude Desktop (Windows):</strong> <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">%APPDATA%\\Claude\\claude_desktop_config.json</code></p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-purple-100 dark:bg-purple-900/30 p-3">
|
||||||
|
<pre class="text-xs overflow-x-auto text-purple-900 dark:text-purple-100"><code id='mcp-config-json'></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤 2 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-purple-200 dark:bg-purple-800 text-purple-900 dark:text-purple-100 rounded-full flex items-center justify-center text-xs font-semibold mr-3">2</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">重启应用</p>
|
||||||
|
<p class="text-xs text-purple-700 dark:text-purple-300">
|
||||||
|
保存配置后重启 Claude Code 或 Claude Desktop。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤 3 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-purple-200 dark:bg-purple-800 text-purple-900 dark:text-purple-100 rounded-full flex items-center justify-center text-xs font-semibold mr-3">3</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">验证安装</p>
|
||||||
|
<div class="text-xs text-purple-700 dark:text-purple-300 space-y-1">
|
||||||
|
<p>• 在 Claude Code 中运行 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">/mcp</code> 命令,应该能看到 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">cloudflare-search</code> 工具</p>
|
||||||
|
<p>• 或 使用 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">claude mcp list</code>, 看到 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">cloudflare-search: ... - ✓ Connected</code> 说明配置成功</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用示例 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">💬 使用示例</p>
|
||||||
|
<div class="space-y-2 text-xs text-purple-700 dark:text-purple-300">
|
||||||
|
<div class="rounded bg-purple-100 dark:bg-purple-900/30 p-2">
|
||||||
|
<code>用 cloudflare-search 搜索 "Cloudflare Workers 最佳实践"</code>
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-purple-100 dark:bg-purple-900/30 p-2">
|
||||||
|
<code>用 cloudflare-search 搜索 "Next.js 14 新特性"</code>
|
||||||
|
</div>
|
||||||
|
<p class="pt-2">AI 会返回来自多个搜索引擎的聚合结果,包括标题、描述和链接。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-4 border-t border-purple-200 dark:border-purple-800/40">
|
||||||
|
<p class="text-xs text-purple-700 dark:text-purple-300">
|
||||||
|
📦 NPM 包: <a href="https://www.npmjs.com/package/@yrobot/cf-search-mcp" target="_blank" class="underline hover:text-purple-900 dark:hover:text-purple-100">@yrobot/cf-search-mcp</a> |
|
||||||
|
📚 MCP 文档: <a href="https://modelcontextprotocol.io" target="_blank" class="underline hover:text-purple-900 dark:hover:text-purple-100">modelcontextprotocol.io</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 功能特性 -->
|
<!-- 功能特性 -->
|
||||||
<div class="mt-16 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
<div class="mt-16 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||||
<div class="flex items-center text-sm text-zinc-600 dark:text-zinc-400">
|
<div class="flex items-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
@@ -522,7 +519,19 @@ export function getSearchHtml() {
|
|||||||
const tokenBodyParam = TOKEN_ENABLED && currentToken ? \`&token=\${currentToken}\` : '';
|
const tokenBodyParam = TOKEN_ENABLED && currentToken ? \`&token=\${currentToken}\` : '';
|
||||||
|
|
||||||
document.getElementById('apiExample1').textContent = currentOrigin + '/search?q=cloudflare' + tokenParam;
|
document.getElementById('apiExample1').textContent = currentOrigin + '/search?q=cloudflare' + tokenParam;
|
||||||
document.getElementById('apiExample2').textContent = 'curl -X POST "' + currentOrigin + '/search" -d "q=cloudflare&engines=google,brave' + tokenBodyParam + '"';
|
document.getElementById('apiExample2').textContent = 'curl -X POST "' + currentOrigin + '/search" -d "q=cloudflare&engines=duckduckgo,bing' + tokenBodyParam + '"';
|
||||||
|
document.getElementById('mcp-config-json').innerHTML = \`{
|
||||||
|
"mcpServers": {
|
||||||
|
"cloudflare-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@yrobot/cf-search-mcp"],
|
||||||
|
"env": {
|
||||||
|
"CF_SEARCH_URL": "\${currentOrigin}",
|
||||||
|
"CF_SEARCH_TOKEN": "\${TOKEN_ENABLED ? TOKEN_ENABLED : ""}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}\`
|
||||||
|
|
||||||
// 搜索表单提交
|
// 搜索表单提交
|
||||||
document.getElementById('searchForm').addEventListener('submit', async function(event) {
|
document.getElementById('searchForm').addEventListener('submit', async function(event) {
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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;
|
|
||||||
50
worker.js
50
worker.js
@@ -1,13 +1,9 @@
|
|||||||
import { env, setEnv } from "./envs.js";
|
import { env, setEnv } from "./envs.js";
|
||||||
import { getSearchHtml } from "./utils/getHTML.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 searchDuckDuckGo from "./utils/searchDuckDuckGo.js";
|
||||||
import searchBing from "./utils/searchBing.js";
|
import searchBing from "./utils/searchBing.js";
|
||||||
|
|
||||||
const SEARCH_ENGINES = {
|
const SEARCH_ENGINES = {
|
||||||
google: searchGoogle,
|
|
||||||
brave: searchBrave,
|
|
||||||
duckduckgo: searchDuckDuckGo,
|
duckduckgo: searchDuckDuckGo,
|
||||||
bing: searchBing,
|
bing: searchBing,
|
||||||
};
|
};
|
||||||
@@ -23,12 +19,7 @@ function parseEngines(enginesParam) {
|
|||||||
return enginesParam
|
return enginesParam
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((e) => e.trim().toLowerCase())
|
.map((e) => e.trim().toLowerCase())
|
||||||
.filter((e) => {
|
.filter((e) => env.SUPPORTED_ENGINES.includes(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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,28 +32,35 @@ async function searchSingle(engineName, query) {
|
|||||||
const searchFn = SEARCH_ENGINES[engineName];
|
const searchFn = SEARCH_ENGINES[engineName];
|
||||||
if (!searchFn) {
|
if (!searchFn) {
|
||||||
console.warn(`Unknown engine: ${engineName}`);
|
console.warn(`Unknown engine: ${engineName}`);
|
||||||
return [];
|
return { results: [], error: "Unknown engine" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 AbortController 用于取消请求
|
// 创建 AbortController 用于取消请求
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = parseInt(env.DEFAULT_TIMEOUT ?? "3000", 10);
|
const timeout = parseInt(env.DEFAULT_TIMEOUT ?? "3000", 10);
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 设置超时自动取消
|
// 设置超时自动取消
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
const result = await searchFn({ query, signal: controller.signal });
|
const results = await searchFn({ query, signal: controller.signal });
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
return result;
|
return { results, error: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error.name === "AbortError"
|
||||||
|
? `Timeout after ${timeout}ms`
|
||||||
|
: error.message || String(error);
|
||||||
|
|
||||||
if (error.name === "AbortError") {
|
if (error.name === "AbortError") {
|
||||||
console.error(`[${engineName}] Timeout after ${timeout}ms`);
|
console.error(`[${engineName}] ${message}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`[${engineName}] Error:`, error.message);
|
console.error(`[${engineName}] Error:`, message);
|
||||||
}
|
}
|
||||||
return [];
|
clearTimeout(timeoutId);
|
||||||
|
return { results: [], error: message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,21 +84,27 @@ async function searchAll({ query, engines }) {
|
|||||||
// Collect resultsArr and track unresponsive engines
|
// Collect resultsArr and track unresponsive engines
|
||||||
const results = [];
|
const results = [];
|
||||||
const unresponsive = [];
|
const unresponsive = [];
|
||||||
|
const empty = [];
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
resultsArr.forEach((result, index) => {
|
resultsArr.forEach((result, index) => {
|
||||||
const engineName = enabledEngines[index];
|
const engineName = enabledEngines[index];
|
||||||
if (result.status === "fulfilled" && result.value.length > 0) {
|
if (result.status === "fulfilled" && result.value.results.length > 0) {
|
||||||
results.push(
|
results.push(
|
||||||
...result.value.map((item) => ({
|
...result.value.results.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
engine: engineName,
|
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 {
|
} else {
|
||||||
unresponsive.push(engineName);
|
unresponsive.push(engineName);
|
||||||
if (result.status === "rejected") {
|
errors[engineName] = result.reason?.message || String(result.reason);
|
||||||
console.error(`[${engineName}] Rejected:`, result.reason);
|
console.error(`[${engineName}] Rejected:`, result.reason);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,6 +113,8 @@ async function searchAll({ query, engines }) {
|
|||||||
number_of_results: results.length,
|
number_of_results: results.length,
|
||||||
enabled_engines: enabledEngines,
|
enabled_engines: enabledEngines,
|
||||||
unresponsive_engines: unresponsive,
|
unresponsive_engines: unresponsive,
|
||||||
|
empty_engines: empty,
|
||||||
|
engine_errors: errors,
|
||||||
results,
|
results,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# Cloudflare Worker 配置文件
|
|
||||||
# 更多配置选项: https://developers.cloudflare.com/workers/wrangler/configuration/
|
|
||||||
|
|
||||||
# Worker 名称(修改为你想要的名称)
|
# Worker 名称(修改为你想要的名称)
|
||||||
name = "cloudflare-search"
|
name = "cloudflare-search"
|
||||||
|
|
||||||
@@ -15,45 +12,16 @@ compatibility_date = "2024-12-04"
|
|||||||
# 设置为 true 会自动分配一个 xxx.workers.dev 域名
|
# 设置为 true 会自动分配一个 xxx.workers.dev 域名
|
||||||
workers_dev = true
|
workers_dev = true
|
||||||
|
|
||||||
# 账号 ID (可选,部署时会自动填充)
|
|
||||||
# account_id = "your-account-id"
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# 路由配置(可选)
|
|
||||||
# ============================================
|
|
||||||
# 如果你想将 Worker 绑定到自定义域名,取消注释并配置:
|
|
||||||
# routes = [
|
|
||||||
# { pattern = "search.yourdomain.com/*", zone_name = "yourdomain.com" }
|
|
||||||
# ]
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 环境变量(可选)
|
# 环境变量(可选)
|
||||||
# ============================================
|
# ============================================
|
||||||
[vars]
|
[vars]
|
||||||
# Google Custom Search API 配置
|
|
||||||
GOOGLE_API_KEY = ""
|
|
||||||
GOOGLE_CX = ""
|
|
||||||
|
|
||||||
# API 访问令牌(可选)
|
# API 访问令牌(可选)
|
||||||
# 配置后,所有 /search 请求都需要提供此 token 进行鉴权
|
# 配置后,所有 /search 请求都需要提供此 token 进行鉴权
|
||||||
# 可以通过 Authorization header (Bearer token) 或 query 参数 (?token=xxx) 传递
|
# 可以通过 Authorization header (Bearer token) 或 query 参数 (?token=xxx) 传递
|
||||||
TOKEN = ""
|
TOKEN = "shumengya520"
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# 环境配置(可选)
|
|
||||||
# ============================================
|
|
||||||
# 生产环境配置
|
|
||||||
# [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
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 资源限制(可选)
|
# 资源限制(可选)
|
||||||
|
|||||||
Reference in New Issue
Block a user