ai-web-search 1.0.2 → 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 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
@@ -304,6 +305,15 @@ npm install --registry=https://registry.npmmirror.com
304
305
  node --version # 需要 >= 14.0.0 / Requires >= 14.0.0
305
306
  ```
306
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
+
307
317
  ## 开发 / Development
308
318
 
309
319
  ### 本地开发 / Local Development
@@ -333,23 +343,13 @@ ai-web-search/
333
343
  │ ├── custom-search.js # 自定义搜索引擎示例 / Custom search engine example
334
344
  │ └── baidu-search.js # 百度搜索引擎实现 / Baidu search engine implementation
335
345
  ├── test/
336
- ├── test.js # 测试文件 / Test file
337
- │ └── test-custom.js # 自定义搜索测试 / Custom search test
346
+ └── test.js # 测试文件 / Test file
338
347
  ├── package.json
339
348
  ├── README.md
340
349
  ├── .gitignore
341
- └── .env.example
350
+ └── LICENSE
342
351
  ```
343
352
 
344
- ## 许可证 / License
345
-
346
- MIT License
347
-
348
- ## 贡献 / Contributing
349
-
350
- 欢迎提交问题和建议!
351
- Issues and suggestions are welcome!
352
-
353
353
  ## 相关项目 / Related Projects
354
354
 
355
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
  });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-web-search",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "A lightweight tool that enables AI assistants to search the internet via local HTTP server",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
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 || DuckDuckGo.SafeSearchType.MODERATE,
13
- timeout: options.timeout || 10000,
14
- searchFn: options.searchFn || null,
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<Array>} Search results
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
- ...this.options
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<Array>} News results
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
- ...this.options
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 (placeholder - not implemented in duck-duck-scrape)
221
+ * Get search suggestions
210
222
  * @param {string} query - Partial query
211
- * @returns {Promise<Array>} Search suggestions
223
+ * @returns {Promise<Object>} Search suggestions
212
224
  */
213
225
  async getSuggestions(query) {
214
- // duck-duck-scrape doesn't provide getSuggestions method
215
- // Return empty suggestions for now
216
- return {
217
- query,
218
- suggestions: [],
219
- timestamp: new Date().toISOString()
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(config.searchFnPath);
13
- console.log(`🔌 Loaded custom search function from ${config.searchFnPath}`);
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 ${config.searchFnPath}:`, error.message);
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
- // Middleware
22
- app.use(cors());
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('/search', limiter);
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
- console.log('\nNote: Some tests may fail due to network issues or DuckDuckGo rate limiting.');
58
- console.log('If tests fail, try again in a few minutes.');
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