ai-web-search 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -15
- package/bin/cli.js +20 -1
- package/examples/baidu-search.js +2 -12
- package/package.json +1 -1
- package/src/index.js +10 -0
- package/src/search.js +67 -30
- package/src/server.js +15 -8
- package/test/test.js +16 -8
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ PORT=3000
|
|
|
102
102
|
HOST=localhost
|
|
103
103
|
RATE_LIMIT_MAX=100
|
|
104
104
|
SEARCH_FUNCTION_PATH=./examples/custom-search.js
|
|
105
|
+
CORS_ORIGIN=http://localhost,http://127.0.0.1
|
|
105
106
|
```
|
|
106
107
|
|
|
107
108
|
### 自定义搜索引擎 / Custom Search Engine
|
|
@@ -291,9 +292,7 @@ ai-web-search -p 8080
|
|
|
291
292
|
- 检查防火墙设置 / Check firewall settings
|
|
292
293
|
- 验证端口是否正确 / Verify port is correct
|
|
293
294
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
### 5. npm install失败 / npm install failed
|
|
295
|
+
### 4. npm install失败 / npm install failed
|
|
297
296
|
|
|
298
297
|
```bash
|
|
299
298
|
# 清除npm缓存 / Clear npm cache
|
|
@@ -306,6 +305,15 @@ npm install --registry=https://registry.npmmirror.com
|
|
|
306
305
|
node --version # 需要 >= 14.0.0 / Requires >= 14.0.0
|
|
307
306
|
```
|
|
308
307
|
|
|
308
|
+
### 5. 百度搜索结果是跳转链接 / Baidu results are redirect links
|
|
309
|
+
|
|
310
|
+
当使用百度搜索回退时,返回的URL是百度跳转链接(如 `https://www.baidu.com/link?url=...`)。
|
|
311
|
+
When using Baidu fallback, returned URLs are Baidu redirect links.
|
|
312
|
+
|
|
313
|
+
- 点击链接后会自动跳转到目标网站 / Clicking the link will redirect to the destination
|
|
314
|
+
- 这是百度搜索的正常行为 / This is normal Baidu search behavior
|
|
315
|
+
- 如需直接获取真实URL,建议使用自定义搜索引擎实现 / For direct URLs, consider implementing a custom search engine
|
|
316
|
+
|
|
309
317
|
## 开发 / Development
|
|
310
318
|
|
|
311
319
|
### 本地开发 / Local Development
|
|
@@ -335,23 +343,13 @@ ai-web-search/
|
|
|
335
343
|
│ ├── custom-search.js # 自定义搜索引擎示例 / Custom search engine example
|
|
336
344
|
│ └── baidu-search.js # 百度搜索引擎实现 / Baidu search engine implementation
|
|
337
345
|
├── test/
|
|
338
|
-
│
|
|
339
|
-
│ └── test-custom.js # 自定义搜索测试 / Custom search test
|
|
346
|
+
│ └── test.js # 测试文件 / Test file
|
|
340
347
|
├── package.json
|
|
341
348
|
├── README.md
|
|
342
349
|
├── .gitignore
|
|
343
|
-
└──
|
|
350
|
+
└── LICENSE
|
|
344
351
|
```
|
|
345
352
|
|
|
346
|
-
## 许可证 / License
|
|
347
|
-
|
|
348
|
-
MIT License
|
|
349
|
-
|
|
350
|
-
## 贡献 / Contributing
|
|
351
|
-
|
|
352
|
-
欢迎提交 Issue 和 Pull Request!
|
|
353
|
-
Issues and Pull Requests are welcome!
|
|
354
|
-
|
|
355
353
|
## 相关项目 / Related Projects
|
|
356
354
|
|
|
357
355
|
- [duck-duck-scrape](https://github.com/Snazzah/duck-duck-scrape) - DuckDuckGo 搜索库 / DuckDuckGo search library
|
package/bin/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { createServer } = require('../src/server');
|
|
4
4
|
const dotenv = require('dotenv');
|
|
@@ -38,6 +38,13 @@ for (let i = 0; i < args.length; i++) {
|
|
|
38
38
|
showVersion();
|
|
39
39
|
process.exit(0);
|
|
40
40
|
break;
|
|
41
|
+
default:
|
|
42
|
+
if (args[i].startsWith('-')) {
|
|
43
|
+
console.error(`❌ Unknown option: ${args[i]}`);
|
|
44
|
+
console.error('Run with -h or --help for usage information.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -112,4 +119,16 @@ process.on('SIGTERM', () => {
|
|
|
112
119
|
console.log('✅ Server stopped');
|
|
113
120
|
process.exit(0);
|
|
114
121
|
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Global error handlers to prevent process crashes
|
|
125
|
+
process.on('uncaughtException', (err) => {
|
|
126
|
+
console.error('💥 Uncaught Exception:', err.message);
|
|
127
|
+
if (config.dev) console.error(err.stack);
|
|
128
|
+
// Allow the process to continue running in production
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
132
|
+
console.error('💥 Unhandled Rejection at:', promise, 'reason:', reason);
|
|
133
|
+
// Allow the process to continue running in production
|
|
115
134
|
});
|
package/examples/baidu-search.js
CHANGED
|
@@ -98,18 +98,8 @@ async function baiduSearch(query, options = {}) {
|
|
|
98
98
|
|
|
99
99
|
} catch (error) {
|
|
100
100
|
console.error('Baidu search error:', error.message);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
results: [
|
|
105
|
-
{
|
|
106
|
-
title: '百度搜索暂时不可用',
|
|
107
|
-
url: 'https://www.baidu.com',
|
|
108
|
-
snippet: `搜索出错: ${error.message}。请稍后重试或检查网络连接。`,
|
|
109
|
-
source: 'baidu-error'
|
|
110
|
-
}
|
|
111
|
-
]
|
|
112
|
-
};
|
|
101
|
+
// Throw so callers (e.g. fallback logic in search.js) know the search actually failed
|
|
102
|
+
throw new Error(`Baidu search failed: ${error.message}`);
|
|
113
103
|
}
|
|
114
104
|
}
|
|
115
105
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -43,4 +43,14 @@ process.on('SIGTERM', () => {
|
|
|
43
43
|
console.log('✅ Server stopped');
|
|
44
44
|
process.exit(0);
|
|
45
45
|
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Global error handlers to prevent process crashes
|
|
49
|
+
process.on('uncaughtException', (err) => {
|
|
50
|
+
console.error('💥 Uncaught Exception:', err.message);
|
|
51
|
+
if (config.dev) console.error(err.stack);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
55
|
+
console.error('💥 Unhandled Rejection at:', promise, 'reason:', reason);
|
|
46
56
|
});
|
package/src/search.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const DuckDuckGo = require('duck-duck-scrape');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
|
|
2
4
|
let baiduSearchFn = null;
|
|
3
5
|
try {
|
|
4
6
|
baiduSearchFn = require('../examples/baidu-search');
|
|
@@ -8,11 +10,11 @@ try {
|
|
|
8
10
|
|
|
9
11
|
class SearchEngine {
|
|
10
12
|
constructor(options = {}) {
|
|
13
|
+
// Merge options carefully: explicit values take precedence, then defaults
|
|
11
14
|
this.options = {
|
|
12
|
-
safeSearch: options.safeSearch
|
|
13
|
-
timeout: options.timeout
|
|
14
|
-
searchFn: options.searchFn
|
|
15
|
-
...options
|
|
15
|
+
safeSearch: options.safeSearch !== undefined ? options.safeSearch : DuckDuckGo.SafeSearchType.MODERATE,
|
|
16
|
+
timeout: options.timeout !== undefined ? options.timeout : 10000,
|
|
17
|
+
searchFn: options.searchFn !== undefined ? options.searchFn : null,
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -20,11 +22,11 @@ class SearchEngine {
|
|
|
20
22
|
* Perform web search
|
|
21
23
|
* @param {string} query - Search query
|
|
22
24
|
* @param {Object} options - Search options
|
|
23
|
-
* @returns {Promise<
|
|
25
|
+
* @returns {Promise<Object>} Search results
|
|
24
26
|
*/
|
|
25
27
|
async webSearch(query, options = {}) {
|
|
26
28
|
const { limit = 5, timeRange = 'month' } = options;
|
|
27
|
-
|
|
29
|
+
|
|
28
30
|
try {
|
|
29
31
|
// Use custom search function if provided
|
|
30
32
|
if (this.options.searchFn) {
|
|
@@ -34,7 +36,7 @@ class SearchEngine {
|
|
|
34
36
|
type: 'web',
|
|
35
37
|
...options
|
|
36
38
|
});
|
|
37
|
-
|
|
39
|
+
|
|
38
40
|
// Format results according to our standard
|
|
39
41
|
const results = (searchResults.results || []).slice(0, limit).map(result => ({
|
|
40
42
|
title: result.title || '',
|
|
@@ -50,14 +52,19 @@ class SearchEngine {
|
|
|
50
52
|
timestamp: new Date().toISOString()
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
|
|
54
56
|
// Default: Use DuckDuckGo
|
|
55
57
|
const searchResults = await DuckDuckGo.search(query, {
|
|
56
58
|
safeSearch: this.options.safeSearch,
|
|
57
59
|
timeRange,
|
|
58
|
-
|
|
60
|
+
timeout: this.options.timeout
|
|
59
61
|
});
|
|
60
62
|
|
|
63
|
+
// Guard against unexpected response structure
|
|
64
|
+
if (!searchResults || !Array.isArray(searchResults.results)) {
|
|
65
|
+
throw new Error('Invalid response structure from DuckDuckGo');
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
// Limit and format results
|
|
62
69
|
const results = searchResults.results.slice(0, limit).map(result => ({
|
|
63
70
|
title: result.title,
|
|
@@ -74,7 +81,7 @@ class SearchEngine {
|
|
|
74
81
|
};
|
|
75
82
|
} catch (error) {
|
|
76
83
|
console.log(`⚠️ DuckDuckGo search failed: ${error.message}`);
|
|
77
|
-
|
|
84
|
+
|
|
78
85
|
// Try Baidu as fallback
|
|
79
86
|
if (baiduSearchFn) {
|
|
80
87
|
console.log('🔄 Trying Baidu as fallback...');
|
|
@@ -85,7 +92,7 @@ class SearchEngine {
|
|
|
85
92
|
type: 'web',
|
|
86
93
|
...options
|
|
87
94
|
});
|
|
88
|
-
|
|
95
|
+
|
|
89
96
|
const results = (searchResults.results || []).slice(0, limit).map(result => ({
|
|
90
97
|
title: result.title || '',
|
|
91
98
|
url: result.url || '',
|
|
@@ -104,7 +111,7 @@ class SearchEngine {
|
|
|
104
111
|
console.log(`⚠️ Baidu fallback also failed: ${fallbackError.message}`);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
|
-
|
|
114
|
+
|
|
108
115
|
throw new Error(`Search failed: ${error.message}`);
|
|
109
116
|
}
|
|
110
117
|
}
|
|
@@ -113,11 +120,11 @@ class SearchEngine {
|
|
|
113
120
|
* Perform news search
|
|
114
121
|
* @param {string} query - Search query
|
|
115
122
|
* @param {Object} options - Search options
|
|
116
|
-
* @returns {Promise<
|
|
123
|
+
* @returns {Promise<Object>} News results
|
|
117
124
|
*/
|
|
118
125
|
async newsSearch(query, options = {}) {
|
|
119
126
|
const { limit = 5, timeRange = 'week' } = options;
|
|
120
|
-
|
|
127
|
+
|
|
121
128
|
try {
|
|
122
129
|
// Use custom search function if provided
|
|
123
130
|
if (this.options.searchFn) {
|
|
@@ -127,7 +134,7 @@ class SearchEngine {
|
|
|
127
134
|
type: 'news',
|
|
128
135
|
...options
|
|
129
136
|
});
|
|
130
|
-
|
|
137
|
+
|
|
131
138
|
// Format results according to our standard
|
|
132
139
|
const results = (searchResults.results || []).slice(0, limit).map(result => ({
|
|
133
140
|
title: result.title || '',
|
|
@@ -144,15 +151,20 @@ class SearchEngine {
|
|
|
144
151
|
timestamp: new Date().toISOString()
|
|
145
152
|
};
|
|
146
153
|
}
|
|
147
|
-
|
|
154
|
+
|
|
148
155
|
// Default: Use DuckDuckGo
|
|
149
156
|
const searchResults = await DuckDuckGo.search(query, {
|
|
150
157
|
safeSearch: this.options.safeSearch,
|
|
151
158
|
news: true,
|
|
152
159
|
timeRange,
|
|
153
|
-
|
|
160
|
+
timeout: this.options.timeout
|
|
154
161
|
});
|
|
155
162
|
|
|
163
|
+
// Guard against unexpected response structure
|
|
164
|
+
if (!searchResults || !Array.isArray(searchResults.results)) {
|
|
165
|
+
throw new Error('Invalid response structure from DuckDuckGo');
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
const results = searchResults.results.slice(0, limit).map(result => ({
|
|
157
169
|
title: result.title,
|
|
158
170
|
url: result.url,
|
|
@@ -169,7 +181,7 @@ class SearchEngine {
|
|
|
169
181
|
};
|
|
170
182
|
} catch (error) {
|
|
171
183
|
console.log(`⚠️ DuckDuckGo news search failed: ${error.message}`);
|
|
172
|
-
|
|
184
|
+
|
|
173
185
|
// Try Baidu as fallback for news
|
|
174
186
|
if (baiduSearchFn) {
|
|
175
187
|
console.log('🔄 Trying Baidu news as fallback...');
|
|
@@ -180,7 +192,7 @@ class SearchEngine {
|
|
|
180
192
|
type: 'news',
|
|
181
193
|
...options
|
|
182
194
|
});
|
|
183
|
-
|
|
195
|
+
|
|
184
196
|
const results = (searchResults.results || []).slice(0, limit).map(result => ({
|
|
185
197
|
title: result.title || '',
|
|
186
198
|
url: result.url || '',
|
|
@@ -200,25 +212,50 @@ class SearchEngine {
|
|
|
200
212
|
console.log(`⚠️ Baidu news fallback also failed: ${fallbackError.message}`);
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
|
-
|
|
215
|
+
|
|
204
216
|
throw new Error(`News search failed: ${error.message}`);
|
|
205
217
|
}
|
|
206
218
|
}
|
|
207
219
|
|
|
208
220
|
/**
|
|
209
|
-
* Get search suggestions
|
|
221
|
+
* Get search suggestions
|
|
210
222
|
* @param {string} query - Partial query
|
|
211
|
-
* @returns {Promise<
|
|
223
|
+
* @returns {Promise<Object>} Search suggestions
|
|
212
224
|
*/
|
|
213
225
|
async getSuggestions(query) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
try {
|
|
227
|
+
// Try DuckDuckGo suggestions API
|
|
228
|
+
const response = await axios.get('https://duckduckgo.com/ac/', {
|
|
229
|
+
params: { q: query, type: 'list' },
|
|
230
|
+
timeout: 5000,
|
|
231
|
+
headers: {
|
|
232
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
let suggestions = [];
|
|
237
|
+
if (Array.isArray(response.data)) {
|
|
238
|
+
suggestions = response.data.map(item => {
|
|
239
|
+
if (typeof item === 'string') return item;
|
|
240
|
+
if (item && item.phrase) return item.phrase;
|
|
241
|
+
return null;
|
|
242
|
+
}).filter(Boolean);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
query,
|
|
247
|
+
suggestions,
|
|
248
|
+
timestamp: new Date().toISOString()
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
// If DuckDuckGo suggestions fail, return empty array gracefully
|
|
252
|
+
return {
|
|
253
|
+
query,
|
|
254
|
+
suggestions: [],
|
|
255
|
+
timestamp: new Date().toISOString()
|
|
256
|
+
};
|
|
257
|
+
}
|
|
221
258
|
}
|
|
222
259
|
}
|
|
223
260
|
|
|
224
|
-
module.exports = SearchEngine;
|
|
261
|
+
module.exports = SearchEngine;
|
package/src/server.js
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
|
3
3
|
const rateLimit = require('express-rate-limit');
|
|
4
|
+
const path = require('path');
|
|
4
5
|
const SearchEngine = require('./search');
|
|
5
6
|
|
|
6
7
|
function createServer(config) {
|
|
7
8
|
const app = express();
|
|
8
9
|
// Load custom search function if provided
|
|
10
|
+
// Resolve path relative to the current working directory (user's project root),
|
|
11
|
+
// not relative to this module's location inside node_modules
|
|
9
12
|
let searchFn = null;
|
|
10
13
|
if (config.searchFnPath) {
|
|
14
|
+
const resolvedPath = path.isAbsolute(config.searchFnPath)
|
|
15
|
+
? config.searchFnPath
|
|
16
|
+
: path.resolve(process.cwd(), config.searchFnPath);
|
|
11
17
|
try {
|
|
12
|
-
searchFn = require(
|
|
13
|
-
console.log(`🔌 Loaded custom search function from ${
|
|
18
|
+
searchFn = require(resolvedPath);
|
|
19
|
+
console.log(`🔌 Loaded custom search function from ${resolvedPath}`);
|
|
14
20
|
} catch (error) {
|
|
15
|
-
console.error(`❌ Failed to load custom search function from ${
|
|
21
|
+
console.error(`❌ Failed to load custom search function from ${resolvedPath}:`, error.message);
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
|
|
19
25
|
const searchEngine = new SearchEngine({ searchFn });
|
|
20
26
|
|
|
21
|
-
//
|
|
22
|
-
|
|
27
|
+
// CORS: restrict origins in production unless explicitly configured
|
|
28
|
+
const corsOrigin = process.env.CORS_ORIGIN || (config.dev ? true : ['http://localhost', 'http://127.0.0.1']);
|
|
29
|
+
app.use(cors({ origin: corsOrigin }));
|
|
23
30
|
app.use(express.json());
|
|
24
31
|
app.use(express.urlencoded({ extended: true }));
|
|
25
32
|
|
|
26
|
-
// Rate limiting
|
|
33
|
+
// Rate limiting applied to all API endpoints
|
|
27
34
|
const limiter = rateLimit({
|
|
28
35
|
windowMs: config.rateLimit.windowMs,
|
|
29
36
|
max: config.rateLimit.max,
|
|
@@ -32,7 +39,7 @@ function createServer(config) {
|
|
|
32
39
|
message: 'Please try again later'
|
|
33
40
|
}
|
|
34
41
|
});
|
|
35
|
-
app.use(
|
|
42
|
+
app.use(limiter);
|
|
36
43
|
|
|
37
44
|
// Health check endpoint
|
|
38
45
|
app.get('/health', (req, res) => {
|
package/test/test.js
CHANGED
|
@@ -4,9 +4,10 @@ const SearchEngine = require('../src/search');
|
|
|
4
4
|
|
|
5
5
|
async function runTests() {
|
|
6
6
|
console.log('🧪 Running AI Web Search Tests\n');
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
const searchEngine = new SearchEngine();
|
|
9
|
-
|
|
9
|
+
let failed = 0;
|
|
10
|
+
|
|
10
11
|
// Test 1: Basic web search
|
|
11
12
|
console.log('1. Testing web search...');
|
|
12
13
|
try {
|
|
@@ -19,10 +20,11 @@ async function runTests() {
|
|
|
19
20
|
}
|
|
20
21
|
console.log('');
|
|
21
22
|
} catch (error) {
|
|
23
|
+
failed++;
|
|
22
24
|
console.log('❌ Web search failed:', error.message);
|
|
23
25
|
console.log('');
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
|
|
26
28
|
// Test 2: News search
|
|
27
29
|
console.log('2. Testing news search...');
|
|
28
30
|
try {
|
|
@@ -34,14 +36,15 @@ async function runTests() {
|
|
|
34
36
|
}
|
|
35
37
|
console.log('');
|
|
36
38
|
} catch (error) {
|
|
39
|
+
failed++;
|
|
37
40
|
console.log('❌ News search failed:', error.message);
|
|
38
41
|
console.log('');
|
|
39
42
|
}
|
|
40
|
-
|
|
43
|
+
|
|
41
44
|
// Test 3: Search suggestions
|
|
42
45
|
console.log('3. Testing search suggestions...');
|
|
43
46
|
try {
|
|
44
|
-
const suggestions = await searchEngine.getSuggestions('
|
|
47
|
+
const suggestions = await searchEngine.getSuggestions('machine learn');
|
|
45
48
|
console.log('✅ Suggestions successful');
|
|
46
49
|
console.log(` Got ${suggestions.suggestions.length} suggestions`);
|
|
47
50
|
if (suggestions.suggestions.length > 0) {
|
|
@@ -49,13 +52,18 @@ async function runTests() {
|
|
|
49
52
|
}
|
|
50
53
|
console.log('');
|
|
51
54
|
} catch (error) {
|
|
55
|
+
failed++;
|
|
52
56
|
console.log('❌ Suggestions failed:', error.message);
|
|
53
57
|
console.log('');
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
|
|
56
60
|
console.log('🎉 All tests completed!');
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
if (failed > 0) {
|
|
62
|
+
console.log(`\n⚠️ ${failed} test(s) failed.`);
|
|
63
|
+
console.log('Note: Some tests may fail due to network issues or DuckDuckGo rate limiting.');
|
|
64
|
+
console.log('If tests fail, try again in a few minutes.');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
// Run tests
|