@oevortex/ddg_search 1.2.0 → 1.2.1
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/CHANGELOG.md +36 -26
- package/README.md +14 -3
- package/babel.config.js +11 -11
- package/bin/cli.js +14 -8
- package/package.json +1 -1
- package/src/index.js +6 -1
- package/src/index.ts +6 -1
- package/src/tools/braveTool.js +63 -0
- package/src/tools/searchTool.js +40 -40
- package/src/utils/search.js +322 -322
- package/src/utils/search_brave_ai.js +167 -0
- package/src/utils/search_iask.js +228 -228
- package/src/utils/search_monica.js +238 -238
- package/test.setup.js +119 -119
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import { getRandomUserAgent } from './user_agents.js';
|
|
4
|
+
|
|
5
|
+
const BASE_URL = 'https://search.brave.com/api/tap/v1';
|
|
6
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
7
|
+
|
|
8
|
+
function generateKeyB64() {
|
|
9
|
+
const key = randomBytes(32);
|
|
10
|
+
const k = key.toString('base64url');
|
|
11
|
+
const jwk = {
|
|
12
|
+
alg: 'A256GCM',
|
|
13
|
+
ext: true,
|
|
14
|
+
k,
|
|
15
|
+
key_ops: ['encrypt', 'decrypt'],
|
|
16
|
+
kty: 'oct'
|
|
17
|
+
};
|
|
18
|
+
return Buffer.from(JSON.stringify(jwk)).toString('base64');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildHeaders() {
|
|
22
|
+
return {
|
|
23
|
+
accept: 'application/json',
|
|
24
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
25
|
+
'user-agent': getRandomUserAgent(),
|
|
26
|
+
'sec-ch-ua': '"Chromium";v="127", "Not)A;Brand";v="99"',
|
|
27
|
+
'sec-ch-ua-mobile': '?0',
|
|
28
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
29
|
+
'sec-fetch-dest': 'empty',
|
|
30
|
+
'sec-fetch-mode': 'cors',
|
|
31
|
+
'sec-fetch-site': 'same-origin',
|
|
32
|
+
referer: 'https://search.brave.com/ask'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseStream(stream) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let buffer = '';
|
|
39
|
+
let text = '';
|
|
40
|
+
|
|
41
|
+
stream.on('data', (chunk) => {
|
|
42
|
+
buffer += chunk.toString();
|
|
43
|
+
const lines = buffer.split('\n');
|
|
44
|
+
buffer = lines.pop() ?? '';
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (!trimmed) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const payload = JSON.parse(trimmed);
|
|
54
|
+
if (payload?.type === 'text_delta') {
|
|
55
|
+
text += payload.delta ?? '';
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Ignore malformed lines
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
stream.on('end', () => resolve(text));
|
|
64
|
+
stream.on('error', (error) => reject(error));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Search using Brave AI Search.
|
|
70
|
+
* @param {string} prompt - The search query.
|
|
71
|
+
* @param {object} [options] - Search options.
|
|
72
|
+
* @param {boolean} [options.enableResearch=false] - Enable deep research mode.
|
|
73
|
+
* @param {number} [options.timeout=30000] - Request timeout in ms.
|
|
74
|
+
* @param {string} [options.language='en'] - Language code.
|
|
75
|
+
* @param {string} [options.country='US'] - Country code.
|
|
76
|
+
* @param {string} [options.uiLang='en-us'] - UI language.
|
|
77
|
+
* @param {string|null} [options.geoloc=null] - Geolocation coordinates.
|
|
78
|
+
* @returns {Promise<string>} AI-generated response text.
|
|
79
|
+
*/
|
|
80
|
+
export async function searchBraveAI(
|
|
81
|
+
prompt,
|
|
82
|
+
{
|
|
83
|
+
enableResearch = false,
|
|
84
|
+
timeout = DEFAULT_TIMEOUT,
|
|
85
|
+
language = 'en',
|
|
86
|
+
country = 'US',
|
|
87
|
+
uiLang = 'en-us',
|
|
88
|
+
geoloc = null
|
|
89
|
+
} = {}
|
|
90
|
+
) {
|
|
91
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
92
|
+
throw new Error('Invalid prompt: must be a non-empty string');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (prompt.length > 5000) {
|
|
96
|
+
throw new Error('Invalid prompt: too long (maximum 5000 characters)');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const symmetricKey = generateKeyB64();
|
|
100
|
+
const client = axios.create({
|
|
101
|
+
timeout,
|
|
102
|
+
headers: buildHeaders(),
|
|
103
|
+
validateStatus: (status) => status >= 200 && status < 500
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const newParams = {
|
|
107
|
+
language,
|
|
108
|
+
country,
|
|
109
|
+
ui_lang: uiLang,
|
|
110
|
+
symmetric_key: symmetricKey,
|
|
111
|
+
source: enableResearch ? 'home' : 'llmSuggest',
|
|
112
|
+
query: prompt,
|
|
113
|
+
enable_research: enableResearch ? 'true' : 'false'
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (geoloc) {
|
|
117
|
+
newParams.geoloc = geoloc;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const newResponse = await client.get(`${BASE_URL}/new`, { params: newParams });
|
|
122
|
+
if (newResponse.status !== 200) {
|
|
123
|
+
throw new Error(`Brave AI failed to initialize chat: HTTP ${newResponse.status}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const chatId = newResponse.data?.id;
|
|
127
|
+
if (!chatId) {
|
|
128
|
+
throw new Error('Brave AI failed to initialize chat: missing conversation id');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const streamParams = {
|
|
132
|
+
id: chatId,
|
|
133
|
+
query: prompt,
|
|
134
|
+
symmetric_key: symmetricKey,
|
|
135
|
+
language,
|
|
136
|
+
country,
|
|
137
|
+
ui_lang: uiLang,
|
|
138
|
+
enable_research: enableResearch ? 'true' : 'false',
|
|
139
|
+
enable_followups: enableResearch ? 'true' : 'false'
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const referer = `https://search.brave.com/ask?q=${encodeURIComponent(prompt)}&conversation=${chatId}`;
|
|
143
|
+
const streamResponse = await client.get(`${BASE_URL}/stream`, {
|
|
144
|
+
params: streamParams,
|
|
145
|
+
responseType: 'stream',
|
|
146
|
+
headers: {
|
|
147
|
+
referer
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (streamResponse.status !== 200) {
|
|
152
|
+
throw new Error(`Brave AI stream failed: HTTP ${streamResponse.status}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return await parseStream(streamResponse.data);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.response?.status === 429) {
|
|
158
|
+
throw new Error('Brave AI rate limit: too many requests');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (error.code === 'ECONNABORTED') {
|
|
162
|
+
throw new Error('Brave AI request timeout: took too long');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error(`Brave AI search failed for "${prompt}": ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/utils/search_iask.js
CHANGED
|
@@ -107,233 +107,233 @@ function formatHtml(htmlContent) {
|
|
|
107
107
|
return outputLines.join('');
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/**
|
|
111
|
-
* Search using IAsk AI via WebSocket (Phoenix LiveView)
|
|
112
|
-
* @param {string} prompt - The search query or prompt
|
|
113
|
-
* @param {string} mode - Search mode: 'question', 'academic', 'forums', 'wiki', 'thinking'
|
|
114
|
-
* @param {string|null} detailLevel - Detail level: 'concise', 'detailed', 'comprehensive'
|
|
115
|
-
* @returns {Promise<string>} The search results
|
|
116
|
-
*/
|
|
117
|
-
async function searchIAsk(prompt, mode = 'thinking', detailLevel = null) {
|
|
118
|
-
// Input validation
|
|
119
|
-
if (!prompt || typeof prompt !== 'string') {
|
|
120
|
-
throw new Error('Invalid prompt: prompt must be a non-empty string');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Validate mode
|
|
124
|
-
if (!VALID_MODES.includes(mode)) {
|
|
125
|
-
throw new Error(`Invalid mode: ${mode}. Valid modes are: ${VALID_MODES.join(', ')}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Validate detail level
|
|
129
|
-
if (detailLevel && !VALID_DETAIL_LEVELS.includes(detailLevel)) {
|
|
130
|
-
throw new Error(`Invalid detail level: ${detailLevel}. Valid levels are: ${VALID_DETAIL_LEVELS.join(', ')}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
console.log(`IAsk search starting: "${prompt}" (mode: ${mode}, detailLevel: ${detailLevel || 'default'})`);
|
|
134
|
-
|
|
135
|
-
// Clear old cache entries
|
|
136
|
-
clearOldCache();
|
|
137
|
-
|
|
138
|
-
const cacheKey = getCacheKey(prompt, mode, detailLevel);
|
|
139
|
-
const cachedResults = resultsCache.get(cacheKey);
|
|
140
|
-
|
|
141
|
-
if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
|
|
142
|
-
console.log(`Cache hit for IAsk query: "${prompt}"`);
|
|
143
|
-
return cachedResults.results;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Build URL parameters
|
|
147
|
-
const params = new URLSearchParams({ mode, q: prompt });
|
|
148
|
-
if (detailLevel) {
|
|
149
|
-
params.append('options[detail_level]', detailLevel);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Create a cookie jar for session management
|
|
153
|
-
const jar = new CookieJar();
|
|
154
|
-
const client = wrapper(axios.create({ jar }));
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
// Get initial page and extract tokens
|
|
158
|
-
console.log('Fetching IAsk AI initial page...');
|
|
159
|
-
const response = await client.get(API_ENDPOINT, {
|
|
160
|
-
params: Object.fromEntries(params),
|
|
161
|
-
timeout: DEFAULT_TIMEOUT,
|
|
162
|
-
headers: {
|
|
163
|
-
'User-Agent': getRandomUserAgent()
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const $ = cheerio.load(response.data);
|
|
168
|
-
|
|
169
|
-
const phxNode = $('[id^="phx-"]').first();
|
|
170
|
-
const csrfToken = $('[name="csrf-token"]').attr('content');
|
|
171
|
-
const phxId = phxNode.attr('id');
|
|
172
|
-
const phxSession = phxNode.attr('data-phx-session');
|
|
173
|
-
|
|
174
|
-
if (!phxId || !csrfToken) {
|
|
175
|
-
throw new Error('Failed to extract required tokens from IAsk AI page');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Get the actual response URL (after any redirects)
|
|
179
|
-
const responseUrl = response.request.res?.responseUrl || response.config.url;
|
|
180
|
-
|
|
181
|
-
// Get cookies from the jar for WebSocket connection
|
|
182
|
-
const cookies = await jar.getCookies(API_ENDPOINT);
|
|
183
|
-
const cookieString = cookies.map(c => `${c.key}=${c.value}`).join('; ');
|
|
184
|
-
|
|
185
|
-
// Build WebSocket URL
|
|
186
|
-
const wsParams = new URLSearchParams({
|
|
187
|
-
'_csrf_token': csrfToken,
|
|
188
|
-
'vsn': '2.0.0'
|
|
189
|
-
});
|
|
190
|
-
const wsUrl = `wss://iask.ai/live/websocket?${wsParams.toString()}`;
|
|
191
|
-
|
|
192
|
-
return new Promise((resolve, reject) => {
|
|
193
|
-
const ws = new WebSocket(wsUrl, {
|
|
194
|
-
headers: {
|
|
195
|
-
'Cookie': cookieString,
|
|
196
|
-
'User-Agent': getRandomUserAgent(),
|
|
197
|
-
'Origin': 'https://iask.ai'
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
let buffer = '';
|
|
202
|
-
let timeoutId;
|
|
203
|
-
let connectionTimeoutId;
|
|
204
|
-
|
|
205
|
-
// Set connection timeout
|
|
206
|
-
connectionTimeoutId = setTimeout(() => {
|
|
207
|
-
ws.close();
|
|
208
|
-
reject(new Error('IAsk connection timeout: unable to establish WebSocket connection'));
|
|
209
|
-
}, 15000);
|
|
210
|
-
|
|
211
|
-
ws.on('open', () => {
|
|
212
|
-
clearTimeout(connectionTimeoutId);
|
|
213
|
-
console.log('IAsk WebSocket connection established');
|
|
214
|
-
|
|
215
|
-
// Send phx_join message
|
|
216
|
-
ws.send(JSON.stringify([
|
|
217
|
-
null,
|
|
218
|
-
null,
|
|
219
|
-
`lv:${phxId}`,
|
|
220
|
-
'phx_join',
|
|
221
|
-
{
|
|
222
|
-
params: { _csrf_token: csrfToken },
|
|
223
|
-
url: responseUrl,
|
|
224
|
-
session: phxSession
|
|
225
|
-
}
|
|
226
|
-
]));
|
|
227
|
-
|
|
228
|
-
// Set message timeout
|
|
229
|
-
timeoutId = setTimeout(() => {
|
|
230
|
-
ws.close();
|
|
231
|
-
if (buffer) {
|
|
232
|
-
resolve(buffer || 'No results found.');
|
|
233
|
-
} else {
|
|
234
|
-
reject(new Error('IAsk response timeout: no response received'));
|
|
235
|
-
}
|
|
236
|
-
}, DEFAULT_TIMEOUT);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
ws.on('message', (data) => {
|
|
240
|
-
try {
|
|
241
|
-
const msg = JSON.parse(data.toString());
|
|
242
|
-
if (!msg) return;
|
|
243
|
-
|
|
244
|
-
const diff = msg[4];
|
|
245
|
-
if (!diff) return;
|
|
246
|
-
|
|
247
|
-
let chunk = null;
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
// Try to get chunk from diff.e[0][1].data
|
|
251
|
-
if (diff.e) {
|
|
252
|
-
chunk = diff.e[0][1].data;
|
|
253
|
-
|
|
254
|
-
if (chunk) {
|
|
255
|
-
let formatted;
|
|
256
|
-
if (/<[^>]+>/.test(chunk)) {
|
|
257
|
-
formatted = formatHtml(chunk);
|
|
258
|
-
} else {
|
|
259
|
-
formatted = chunk.replace(/<br\/>/g, '\n');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
buffer += formatted;
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
throw new Error('No diff.e');
|
|
266
|
-
}
|
|
267
|
-
} catch {
|
|
268
|
-
// Fallback to cacheFind
|
|
269
|
-
const cache = cacheFind(diff);
|
|
270
|
-
if (cache) {
|
|
271
|
-
let formatted;
|
|
272
|
-
if (/<[^>]+>/.test(cache)) {
|
|
273
|
-
formatted = formatHtml(cache);
|
|
274
|
-
} else {
|
|
275
|
-
formatted = cache;
|
|
276
|
-
}
|
|
277
|
-
buffer += formatted;
|
|
278
|
-
// Close after cache find
|
|
279
|
-
ws.close();
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.error('Error parsing IAsk message:', err.message);
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
ws.on('close', () => {
|
|
289
|
-
clearTimeout(timeoutId);
|
|
290
|
-
clearTimeout(connectionTimeoutId);
|
|
291
|
-
|
|
292
|
-
console.log(`IAsk search completed: ${buffer.length} characters received`);
|
|
293
|
-
|
|
294
|
-
// Cache the result
|
|
295
|
-
if (buffer) {
|
|
296
|
-
resultsCache.set(cacheKey, {
|
|
297
|
-
results: buffer,
|
|
298
|
-
timestamp: Date.now()
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
resolve(buffer || 'No results found.');
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
ws.on('error', (err) => {
|
|
306
|
-
clearTimeout(timeoutId);
|
|
307
|
-
clearTimeout(connectionTimeoutId);
|
|
308
|
-
console.error('IAsk WebSocket error:', err.message);
|
|
309
|
-
|
|
310
|
-
if (err.message.includes('timeout')) {
|
|
311
|
-
reject(new Error('IAsk WebSocket timeout: connection took too long'));
|
|
312
|
-
} else if (err.message.includes('connection refused')) {
|
|
313
|
-
reject(new Error('IAsk connection refused: service may be unavailable'));
|
|
314
|
-
} else {
|
|
315
|
-
reject(new Error(`IAsk WebSocket error: ${err.message}`));
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
} catch (error) {
|
|
320
|
-
console.error('Error in IAsk search:', error.message);
|
|
321
|
-
|
|
322
|
-
// Enhanced error handling
|
|
323
|
-
if (error.code === 'ENOTFOUND') {
|
|
324
|
-
throw new Error('IAsk network error: unable to resolve host');
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (error.code === 'ECONNREFUSED') {
|
|
328
|
-
throw new Error('IAsk network error: connection refused');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (error.message.includes('timeout')) {
|
|
332
|
-
throw new Error(`IAsk timeout: ${error.message}`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
throw new Error(`IAsk search failed for "${prompt}": ${error.message}`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
110
|
+
/**
|
|
111
|
+
* Search using IAsk AI via WebSocket (Phoenix LiveView)
|
|
112
|
+
* @param {string} prompt - The search query or prompt
|
|
113
|
+
* @param {string} mode - Search mode: 'question', 'academic', 'forums', 'wiki', 'thinking'
|
|
114
|
+
* @param {string|null} detailLevel - Detail level: 'concise', 'detailed', 'comprehensive'
|
|
115
|
+
* @returns {Promise<string>} The search results
|
|
116
|
+
*/
|
|
117
|
+
async function searchIAsk(prompt, mode = 'thinking', detailLevel = null) {
|
|
118
|
+
// Input validation
|
|
119
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
120
|
+
throw new Error('Invalid prompt: prompt must be a non-empty string');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate mode
|
|
124
|
+
if (!VALID_MODES.includes(mode)) {
|
|
125
|
+
throw new Error(`Invalid mode: ${mode}. Valid modes are: ${VALID_MODES.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Validate detail level
|
|
129
|
+
if (detailLevel && !VALID_DETAIL_LEVELS.includes(detailLevel)) {
|
|
130
|
+
throw new Error(`Invalid detail level: ${detailLevel}. Valid levels are: ${VALID_DETAIL_LEVELS.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`IAsk search starting: "${prompt}" (mode: ${mode}, detailLevel: ${detailLevel || 'default'})`);
|
|
134
|
+
|
|
135
|
+
// Clear old cache entries
|
|
136
|
+
clearOldCache();
|
|
137
|
+
|
|
138
|
+
const cacheKey = getCacheKey(prompt, mode, detailLevel);
|
|
139
|
+
const cachedResults = resultsCache.get(cacheKey);
|
|
140
|
+
|
|
141
|
+
if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
|
|
142
|
+
console.log(`Cache hit for IAsk query: "${prompt}"`);
|
|
143
|
+
return cachedResults.results;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Build URL parameters
|
|
147
|
+
const params = new URLSearchParams({ mode, q: prompt });
|
|
148
|
+
if (detailLevel) {
|
|
149
|
+
params.append('options[detail_level]', detailLevel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Create a cookie jar for session management
|
|
153
|
+
const jar = new CookieJar();
|
|
154
|
+
const client = wrapper(axios.create({ jar }));
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Get initial page and extract tokens
|
|
158
|
+
console.log('Fetching IAsk AI initial page...');
|
|
159
|
+
const response = await client.get(API_ENDPOINT, {
|
|
160
|
+
params: Object.fromEntries(params),
|
|
161
|
+
timeout: DEFAULT_TIMEOUT,
|
|
162
|
+
headers: {
|
|
163
|
+
'User-Agent': getRandomUserAgent()
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const $ = cheerio.load(response.data);
|
|
168
|
+
|
|
169
|
+
const phxNode = $('[id^="phx-"]').first();
|
|
170
|
+
const csrfToken = $('[name="csrf-token"]').attr('content');
|
|
171
|
+
const phxId = phxNode.attr('id');
|
|
172
|
+
const phxSession = phxNode.attr('data-phx-session');
|
|
173
|
+
|
|
174
|
+
if (!phxId || !csrfToken) {
|
|
175
|
+
throw new Error('Failed to extract required tokens from IAsk AI page');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get the actual response URL (after any redirects)
|
|
179
|
+
const responseUrl = response.request.res?.responseUrl || response.config.url;
|
|
180
|
+
|
|
181
|
+
// Get cookies from the jar for WebSocket connection
|
|
182
|
+
const cookies = await jar.getCookies(API_ENDPOINT);
|
|
183
|
+
const cookieString = cookies.map(c => `${c.key}=${c.value}`).join('; ');
|
|
184
|
+
|
|
185
|
+
// Build WebSocket URL
|
|
186
|
+
const wsParams = new URLSearchParams({
|
|
187
|
+
'_csrf_token': csrfToken,
|
|
188
|
+
'vsn': '2.0.0'
|
|
189
|
+
});
|
|
190
|
+
const wsUrl = `wss://iask.ai/live/websocket?${wsParams.toString()}`;
|
|
191
|
+
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const ws = new WebSocket(wsUrl, {
|
|
194
|
+
headers: {
|
|
195
|
+
'Cookie': cookieString,
|
|
196
|
+
'User-Agent': getRandomUserAgent(),
|
|
197
|
+
'Origin': 'https://iask.ai'
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let buffer = '';
|
|
202
|
+
let timeoutId;
|
|
203
|
+
let connectionTimeoutId;
|
|
204
|
+
|
|
205
|
+
// Set connection timeout
|
|
206
|
+
connectionTimeoutId = setTimeout(() => {
|
|
207
|
+
ws.close();
|
|
208
|
+
reject(new Error('IAsk connection timeout: unable to establish WebSocket connection'));
|
|
209
|
+
}, 15000);
|
|
210
|
+
|
|
211
|
+
ws.on('open', () => {
|
|
212
|
+
clearTimeout(connectionTimeoutId);
|
|
213
|
+
console.log('IAsk WebSocket connection established');
|
|
214
|
+
|
|
215
|
+
// Send phx_join message
|
|
216
|
+
ws.send(JSON.stringify([
|
|
217
|
+
null,
|
|
218
|
+
null,
|
|
219
|
+
`lv:${phxId}`,
|
|
220
|
+
'phx_join',
|
|
221
|
+
{
|
|
222
|
+
params: { _csrf_token: csrfToken },
|
|
223
|
+
url: responseUrl,
|
|
224
|
+
session: phxSession
|
|
225
|
+
}
|
|
226
|
+
]));
|
|
227
|
+
|
|
228
|
+
// Set message timeout
|
|
229
|
+
timeoutId = setTimeout(() => {
|
|
230
|
+
ws.close();
|
|
231
|
+
if (buffer) {
|
|
232
|
+
resolve(buffer || 'No results found.');
|
|
233
|
+
} else {
|
|
234
|
+
reject(new Error('IAsk response timeout: no response received'));
|
|
235
|
+
}
|
|
236
|
+
}, DEFAULT_TIMEOUT);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
ws.on('message', (data) => {
|
|
240
|
+
try {
|
|
241
|
+
const msg = JSON.parse(data.toString());
|
|
242
|
+
if (!msg) return;
|
|
243
|
+
|
|
244
|
+
const diff = msg[4];
|
|
245
|
+
if (!diff) return;
|
|
246
|
+
|
|
247
|
+
let chunk = null;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// Try to get chunk from diff.e[0][1].data
|
|
251
|
+
if (diff.e) {
|
|
252
|
+
chunk = diff.e[0][1].data;
|
|
253
|
+
|
|
254
|
+
if (chunk) {
|
|
255
|
+
let formatted;
|
|
256
|
+
if (/<[^>]+>/.test(chunk)) {
|
|
257
|
+
formatted = formatHtml(chunk);
|
|
258
|
+
} else {
|
|
259
|
+
formatted = chunk.replace(/<br\/>/g, '\n');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
buffer += formatted;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
throw new Error('No diff.e');
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// Fallback to cacheFind
|
|
269
|
+
const cache = cacheFind(diff);
|
|
270
|
+
if (cache) {
|
|
271
|
+
let formatted;
|
|
272
|
+
if (/<[^>]+>/.test(cache)) {
|
|
273
|
+
formatted = formatHtml(cache);
|
|
274
|
+
} else {
|
|
275
|
+
formatted = cache;
|
|
276
|
+
}
|
|
277
|
+
buffer += formatted;
|
|
278
|
+
// Close after cache find
|
|
279
|
+
ws.close();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error('Error parsing IAsk message:', err.message);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
ws.on('close', () => {
|
|
289
|
+
clearTimeout(timeoutId);
|
|
290
|
+
clearTimeout(connectionTimeoutId);
|
|
291
|
+
|
|
292
|
+
console.log(`IAsk search completed: ${buffer.length} characters received`);
|
|
293
|
+
|
|
294
|
+
// Cache the result
|
|
295
|
+
if (buffer) {
|
|
296
|
+
resultsCache.set(cacheKey, {
|
|
297
|
+
results: buffer,
|
|
298
|
+
timestamp: Date.now()
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
resolve(buffer || 'No results found.');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
ws.on('error', (err) => {
|
|
306
|
+
clearTimeout(timeoutId);
|
|
307
|
+
clearTimeout(connectionTimeoutId);
|
|
308
|
+
console.error('IAsk WebSocket error:', err.message);
|
|
309
|
+
|
|
310
|
+
if (err.message.includes('timeout')) {
|
|
311
|
+
reject(new Error('IAsk WebSocket timeout: connection took too long'));
|
|
312
|
+
} else if (err.message.includes('connection refused')) {
|
|
313
|
+
reject(new Error('IAsk connection refused: service may be unavailable'));
|
|
314
|
+
} else {
|
|
315
|
+
reject(new Error(`IAsk WebSocket error: ${err.message}`));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('Error in IAsk search:', error.message);
|
|
321
|
+
|
|
322
|
+
// Enhanced error handling
|
|
323
|
+
if (error.code === 'ENOTFOUND') {
|
|
324
|
+
throw new Error('IAsk network error: unable to resolve host');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (error.code === 'ECONNREFUSED') {
|
|
328
|
+
throw new Error('IAsk network error: connection refused');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (error.message.includes('timeout')) {
|
|
332
|
+
throw new Error(`IAsk timeout: ${error.message}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
throw new Error(`IAsk search failed for "${prompt}": ${error.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
338
|
|
|
339
339
|
export { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS };
|