kiku-agent 1.0.0
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 +122 -0
- package/package.json +85 -0
- package/server/agent/agentCore.js +782 -0
- package/server/agent/agentState.js +140 -0
- package/server/agent/audioPlayer.js +673 -0
- package/server/agent/cli.js +1111 -0
- package/server/agent/commandRegistry.js +58 -0
- package/server/agent/commands.js +788 -0
- package/server/agent/config.js +90 -0
- package/server/agent/errors.js +9 -0
- package/server/agent/eventBus.js +103 -0
- package/server/agent/llmClient.js +200 -0
- package/server/agent/memory.js +566 -0
- package/server/agent/sanitizer.js +314 -0
- package/server/agent/session.js +239 -0
- package/server/agent/theme.js +124 -0
- package/server/agent/tools.js +1528 -0
- package/server/agent/ui.js +469 -0
- package/server/agent/webSearch.js +719 -0
- package/server/agent/youtubeAuth.js +299 -0
- package/server/mcp/claude-desktop-config.example.json +9 -0
- package/server/mcp/data/awards.js +14 -0
- package/server/mcp/data/brands.js +9 -0
- package/server/mcp/data/discography.js +36 -0
- package/server/mcp/data/gallery.js +25 -0
- package/server/mcp/data/index.js +7 -0
- package/server/mcp/data/profile.js +44 -0
- package/server/mcp/data/timeline.js +16 -0
- package/server/mcp/data/works.js +104 -0
- package/server/mcp/handler.js +159 -0
- package/server/mcp/prompts.js +143 -0
- package/server/mcp/resources.js +54 -0
- package/server/mcp/security.js +138 -0
- package/server/mcp/server.js +100 -0
- package/server/mcp/stdio.js +33 -0
- package/server/mcp/tools.js +126 -0
- package/src/data/awardsData.js +146 -0
- package/src/data/brandsData.js +249 -0
- package/src/data/discographyData.js +874 -0
- package/src/data/galleryData.js +666 -0
- package/src/data/musicVideoData.js +406 -0
- package/src/data/profileData.js +156 -0
- package/src/data/socialUpdatesData.js +381 -0
- package/src/data/timelineData.js +172 -0
- package/src/data/worksData.js +516 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
let _cheerioLoad = null;
|
|
8
|
+
async function loadCheerio() {
|
|
9
|
+
if (_cheerioLoad) return _cheerioLoad;
|
|
10
|
+
try {
|
|
11
|
+
const mod = await import('cheerio');
|
|
12
|
+
_cheerioLoad = mod.load;
|
|
13
|
+
return _cheerioLoad;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function cleanHtml(html) {
|
|
20
|
+
if (!html) return '';
|
|
21
|
+
return html
|
|
22
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ' ')
|
|
23
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ' ')
|
|
24
|
+
.replace(/<nav\b[^<]*(?:(?!<\/nav>)<[^<]*)*<\/nav>/gi, ' ')
|
|
25
|
+
.replace(/<footer\b[^<]*(?:(?!<\/footer>)<[^<]*)*<\/footer>/gi, ' ')
|
|
26
|
+
.replace(/<header\b[^<]*(?:(?!<\/header>)<[^<]*)*<\/header>/gi, ' ')
|
|
27
|
+
.replace(/<aside\b[^<]*(?:(?!<\/aside>)<[^<]*)*<\/aside>/gi, ' ')
|
|
28
|
+
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, ' ')
|
|
29
|
+
.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, ' ')
|
|
30
|
+
.replace(/<[^>]+>/g, ' ')
|
|
31
|
+
.replace(/ /g, ' ')
|
|
32
|
+
.replace(/&/g, '&')
|
|
33
|
+
.replace(/</g, '<')
|
|
34
|
+
.replace(/>/g, '>')
|
|
35
|
+
.replace(/"/g, '"')
|
|
36
|
+
.replace(/'/g, "'")
|
|
37
|
+
.replace(/—/g, '—')
|
|
38
|
+
.replace(/–/g, '–')
|
|
39
|
+
.replace(/…/g, '...')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectLanguage(query) {
|
|
45
|
+
if (/[\u0E00-\u0E7F]/.test(query)) return 'th';
|
|
46
|
+
if (/[\u4E00-\u9FA5]/.test(query)) return 'zh';
|
|
47
|
+
return 'en';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function expandQuery(query, lang) {
|
|
51
|
+
const expansions = [];
|
|
52
|
+
|
|
53
|
+
if (lang === 'zh') {
|
|
54
|
+
if (query.includes('鞠婧祎')) {
|
|
55
|
+
expansions.push(query.replace('鞠婧祎', '鞠婧祎 最新'));
|
|
56
|
+
expansions.push(query.replace('鞠婧祎', '鞠婧祎 2026'));
|
|
57
|
+
expansions.push(query.replace('鞠婧祎', '鞠婧祎 官方'));
|
|
58
|
+
}
|
|
59
|
+
} else if (lang === 'th') {
|
|
60
|
+
if (query.includes('จวีจิ้งอี')) {
|
|
61
|
+
expansions.push(query.replace('จวีจิ้งอี', 'จวีจิ้งอี ล่าสุด'));
|
|
62
|
+
expansions.push(query.replace('จวีจิ้งอี', 'จวีจิ้งอี 2026'));
|
|
63
|
+
expansions.push(query.replace('จวีจิ้งอี', 'จวีจิ้งอี อย่างเป็นทางการ'));
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
if (query.includes('Ju Jingyi')) {
|
|
67
|
+
expansions.push(query.replace('Ju Jingyi', 'Ju Jingyi latest'));
|
|
68
|
+
expansions.push(query.replace('Ju Jingyi', 'Ju Jingyi 2026'));
|
|
69
|
+
expansions.push(query.replace('Ju Jingyi', 'Ju Jingyi official'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return expansions;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function searchDuckDuckGo(query, maxResults) {
|
|
77
|
+
const results = [];
|
|
78
|
+
try {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
|
81
|
+
|
|
82
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=${detectLanguage(query) === 'th' ? 'th-th' : detectLanguage(query) === 'zh' ? 'zh-cn' : 'en-us'}`;
|
|
83
|
+
const res = await fetch(searchUrl, {
|
|
84
|
+
headers: {
|
|
85
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
86
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
87
|
+
'Accept-Language': 'th,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6'
|
|
88
|
+
},
|
|
89
|
+
signal: controller.signal
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
clearTimeout(timeoutId);
|
|
93
|
+
|
|
94
|
+
if (res.ok) {
|
|
95
|
+
const html = await res.text();
|
|
96
|
+
|
|
97
|
+
const resultRegex = /<div[^>]*class="[^"]*result[^"]*"[^>]*>[\s\S]*?<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
98
|
+
let match;
|
|
99
|
+
while ((match = resultRegex.exec(html)) !== null && results.length < maxResults) {
|
|
100
|
+
let rawLink = match[1];
|
|
101
|
+
if (rawLink.includes('uddg=')) {
|
|
102
|
+
try {
|
|
103
|
+
const decoded = new URL('https://duckduckgo.com' + rawLink).searchParams.get('uddg');
|
|
104
|
+
if (decoded) rawLink = decoded;
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const title = cleanHtml(match[2]);
|
|
109
|
+
const snippet = cleanHtml(match[3]);
|
|
110
|
+
|
|
111
|
+
if (title && snippet && !results.some(r => r.url === rawLink)) {
|
|
112
|
+
results.push({
|
|
113
|
+
title,
|
|
114
|
+
url: rawLink,
|
|
115
|
+
snippet,
|
|
116
|
+
source: 'DuckDuckGo'
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function searchWikipedia(query, maxResults, lang) {
|
|
128
|
+
const results = [];
|
|
129
|
+
try {
|
|
130
|
+
const wikiUrl = `https://${lang}.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&utf8=1&srlimit=${maxResults}&srenablerewrites=1`;
|
|
131
|
+
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const timeoutId = setTimeout(() => controller.abort(), 6000);
|
|
134
|
+
|
|
135
|
+
const wikiRes = await fetch(wikiUrl, {
|
|
136
|
+
headers: { 'User-Agent': 'KikuAgent/1.0 (JuJingyi Fan Knowledge Bot)' },
|
|
137
|
+
signal: controller.signal
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
|
|
142
|
+
if (wikiRes.ok) {
|
|
143
|
+
const data = await wikiRes.json();
|
|
144
|
+
const wikiItems = data.query?.search || [];
|
|
145
|
+
for (const item of wikiItems) {
|
|
146
|
+
if (results.length >= maxResults) break;
|
|
147
|
+
results.push({
|
|
148
|
+
title: `[Wikipedia] ${item.title}`,
|
|
149
|
+
url: `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(item.title.replace(/ /g, '_'))}`,
|
|
150
|
+
snippet: cleanHtml(item.snippet.replace(/<span class="searchmatch">/g, '').replace(/<\/span>/g, '')),
|
|
151
|
+
source: 'Wikipedia'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function searchBing(query, maxResults) {
|
|
162
|
+
const results = [];
|
|
163
|
+
try {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
|
166
|
+
|
|
167
|
+
const searchUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&setlang=${detectLanguage(query) === 'th' ? 'th' : detectLanguage(query) === 'zh' ? 'zh-cn' : 'en-us'}`;
|
|
168
|
+
const res = await fetch(searchUrl, {
|
|
169
|
+
headers: {
|
|
170
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
171
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
172
|
+
'Accept-Language': 'th,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6'
|
|
173
|
+
},
|
|
174
|
+
signal: controller.signal
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
clearTimeout(timeoutId);
|
|
178
|
+
|
|
179
|
+
if (res.ok) {
|
|
180
|
+
const html = await res.text();
|
|
181
|
+
|
|
182
|
+
const linkRegex = /<h2><a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<p[^>]*class="b_lineclamp2 b_algoSlug"[^>]*>([\s\S]*?)<\/p>/gi;
|
|
183
|
+
let match;
|
|
184
|
+
while ((match = linkRegex.exec(html)) !== null && results.length < maxResults) {
|
|
185
|
+
const rawLink = match[1];
|
|
186
|
+
const title = cleanHtml(match[2]);
|
|
187
|
+
const snippet = cleanHtml(match[3]);
|
|
188
|
+
|
|
189
|
+
if (title && snippet && !results.some(r => r.url === rawLink)) {
|
|
190
|
+
results.push({
|
|
191
|
+
title,
|
|
192
|
+
url: rawLink,
|
|
193
|
+
snippet,
|
|
194
|
+
source: 'Bing'
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function searchWeb(query, maxResults = 8) {
|
|
206
|
+
if (!query || typeof query !== 'string') {
|
|
207
|
+
return { status: 'error', message: 'Query string is required for web search.' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const lang = detectLanguage(query);
|
|
211
|
+
const results = [];
|
|
212
|
+
const sources = [];
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const ddgResults = await searchDuckDuckGo(query, maxResults);
|
|
216
|
+
ddgResults.forEach(r => {
|
|
217
|
+
if (!results.some(existing => existing.url === r.url)) {
|
|
218
|
+
results.push(r);
|
|
219
|
+
sources.push('DuckDuckGo');
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
} catch {}
|
|
223
|
+
|
|
224
|
+
if (results.length < maxResults) {
|
|
225
|
+
try {
|
|
226
|
+
const wikiResults = await searchWikipedia(query, maxResults - results.length, lang);
|
|
227
|
+
wikiResults.forEach(r => {
|
|
228
|
+
if (!results.some(existing => existing.url === r.url)) {
|
|
229
|
+
results.push(r);
|
|
230
|
+
sources.push('Wikipedia');
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (results.length < maxResults / 2) {
|
|
237
|
+
try {
|
|
238
|
+
const expandedQueries = expandQuery(query, lang);
|
|
239
|
+
for (const expandedQuery of expandedQueries) {
|
|
240
|
+
if (results.length >= maxResults) break;
|
|
241
|
+
const ddgResults = await searchDuckDuckGo(expandedQuery, Math.ceil(maxResults / 2));
|
|
242
|
+
ddgResults.forEach(r => {
|
|
243
|
+
if (!results.some(existing => existing.url === r.url)) {
|
|
244
|
+
results.push(r);
|
|
245
|
+
sources.push('DuckDuckGo (expanded)');
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (results.length < maxResults) {
|
|
253
|
+
try {
|
|
254
|
+
const bingResults = await searchBing(query, maxResults - results.length);
|
|
255
|
+
bingResults.forEach(r => {
|
|
256
|
+
if (!results.some(existing => existing.url === r.url)) {
|
|
257
|
+
results.push(r);
|
|
258
|
+
sources.push('Bing');
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
status: results.length > 0 ? 'success' : 'no_results',
|
|
266
|
+
query,
|
|
267
|
+
language: lang,
|
|
268
|
+
count: results.length,
|
|
269
|
+
sources: [...new Set(sources)],
|
|
270
|
+
results: results.slice(0, maxResults)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function fetchWebpage(url, maxLength = 8000) {
|
|
275
|
+
if (!url || typeof url !== 'string' || !url.startsWith('http')) {
|
|
276
|
+
return { status: 'error', message: 'Valid HTTP/HTTPS URL is required.' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
282
|
+
|
|
283
|
+
const res = await fetch(url, {
|
|
284
|
+
headers: {
|
|
285
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
286
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
287
|
+
'Accept-Language': 'en-US,en;q=0.9,th;q=0.8,zh-CN;q=0.7'
|
|
288
|
+
},
|
|
289
|
+
signal: controller.signal,
|
|
290
|
+
redirect: 'follow'
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
clearTimeout(timeoutId);
|
|
294
|
+
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
return { status: 'error', message: `HTTP ${res.status}: Failed to load webpage` };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const contentType = res.headers.get('content-type') || '';
|
|
300
|
+
const finalUrl = res.url || url;
|
|
301
|
+
|
|
302
|
+
if (contentType.includes('application/json')) {
|
|
303
|
+
const json = await res.json();
|
|
304
|
+
const text = JSON.stringify(json, null, 2);
|
|
305
|
+
return {
|
|
306
|
+
status: 'success',
|
|
307
|
+
url: finalUrl,
|
|
308
|
+
contentType: 'json',
|
|
309
|
+
contentLength: text.length,
|
|
310
|
+
content: text.length > maxLength ? text.slice(0, maxLength) + '... [truncated]' : text
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const html = await res.text();
|
|
315
|
+
const cheerioLoad = await loadCheerio();
|
|
316
|
+
|
|
317
|
+
if (cheerioLoad) {
|
|
318
|
+
const $ = cheerioLoad(html);
|
|
319
|
+
|
|
320
|
+
$('script, style, noscript, iframe, svg, nav, footer, aside, form, button').remove();
|
|
321
|
+
|
|
322
|
+
const meta = {};
|
|
323
|
+
meta.title = $('title').first().text().trim() || '';
|
|
324
|
+
meta.description = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || '';
|
|
325
|
+
meta.author = $('meta[name="author"]').attr('content') || '';
|
|
326
|
+
meta.keywords = $('meta[name="keywords"]').attr('content') || '';
|
|
327
|
+
meta.ogTitle = $('meta[property="og:title"]').attr('content') || '';
|
|
328
|
+
meta.ogImage = $('meta[property="og:image"]').attr('content') || '';
|
|
329
|
+
meta.ogType = $('meta[property="og:type"]').attr('content') || '';
|
|
330
|
+
meta.canonical = $('link[rel="canonical"]').attr('href') || '';
|
|
331
|
+
meta.favicon = $('link[rel="icon"], link[rel="shortcut icon"]').attr('href') || '';
|
|
332
|
+
meta.lang = $('html').attr('lang') || '';
|
|
333
|
+
|
|
334
|
+
const headings = [];
|
|
335
|
+
$('h1, h2, h3').each((i, el) => {
|
|
336
|
+
if (i < 10) {
|
|
337
|
+
const txt = $(el).text().trim();
|
|
338
|
+
if (txt) headings.push({ level: el.tagName, text: txt });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const paragraphs = [];
|
|
343
|
+
$('article p, main p, .content p, .post-content p, .entry-content p, .article-body p, .wiki-content p').each((i, el) => {
|
|
344
|
+
if (i < 20) {
|
|
345
|
+
const txt = $(el).text().trim();
|
|
346
|
+
if (txt && txt.length > 20) paragraphs.push(txt);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
if (paragraphs.length === 0) {
|
|
350
|
+
$('p').each((i, el) => {
|
|
351
|
+
if (i < 15) {
|
|
352
|
+
const txt = $(el).text().trim();
|
|
353
|
+
if (txt && txt.length > 30) paragraphs.push(txt);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const links = [];
|
|
359
|
+
$('article a, main a, .content a').each((i, el) => {
|
|
360
|
+
if (i < 15) {
|
|
361
|
+
const href = $(el).attr('href');
|
|
362
|
+
const text = $(el).text().trim();
|
|
363
|
+
if (href && text && text.length > 2) {
|
|
364
|
+
let absoluteUrl = href;
|
|
365
|
+
try { absoluteUrl = new URL(href, finalUrl).href; } catch {}
|
|
366
|
+
links.push({ text: text.slice(0, 80), url: absoluteUrl });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const images = [];
|
|
372
|
+
$('article img, main img, .content img').each((i, el) => {
|
|
373
|
+
if (i < 8) {
|
|
374
|
+
const src = $(el).attr('src');
|
|
375
|
+
const alt = $(el).attr('alt') || '';
|
|
376
|
+
if (src) {
|
|
377
|
+
let absoluteUrl = src;
|
|
378
|
+
try { absoluteUrl = new URL(src, finalUrl).href; } catch {}
|
|
379
|
+
images.push({ src: absoluteUrl, alt: alt.slice(0, 100) });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
let bodyText = $('body').text().replace(/\s+/g, ' ').trim();
|
|
385
|
+
if (bodyText.length > maxLength) {
|
|
386
|
+
bodyText = bodyText.slice(0, maxLength) + '... [Content truncated for brevity]';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const result = {
|
|
390
|
+
status: 'success',
|
|
391
|
+
url: finalUrl,
|
|
392
|
+
contentType: 'html',
|
|
393
|
+
contentLength: bodyText.length,
|
|
394
|
+
content: bodyText || 'Unable to retrieve text content.',
|
|
395
|
+
meta: Object.fromEntries(Object.entries(meta).filter(([, v]) => v))
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
if (headings.length > 0) result.headings = headings;
|
|
399
|
+
if (paragraphs.length > 0) result.paragraphs = paragraphs;
|
|
400
|
+
if (links.length > 0) result.links = links;
|
|
401
|
+
if (images.length > 0) result.images = images;
|
|
402
|
+
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let text = cleanHtml(html);
|
|
407
|
+
if (text.length > maxLength) {
|
|
408
|
+
text = text.slice(0, maxLength) + '... [Content truncated for brevity]';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
status: 'success',
|
|
413
|
+
url: finalUrl,
|
|
414
|
+
contentType: 'html',
|
|
415
|
+
contentLength: text.length,
|
|
416
|
+
content: text || 'Unable to retrieve text content from this webpage.'
|
|
417
|
+
};
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return {
|
|
420
|
+
status: 'error',
|
|
421
|
+
url,
|
|
422
|
+
message: err.name === 'AbortError' ? 'Webpage fetch timed out (15s)' : err.message
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function fetchJson(url, options = {}) {
|
|
428
|
+
if (!url || typeof url !== 'string' || !url.startsWith('http')) {
|
|
429
|
+
return { status: 'error', message: 'Valid HTTP/HTTPS URL is required.' };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const {
|
|
433
|
+
method = 'GET',
|
|
434
|
+
headers = {},
|
|
435
|
+
body = null,
|
|
436
|
+
timeoutMs = 12000,
|
|
437
|
+
maxResponseLength = 32000
|
|
438
|
+
} = options;
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const controller = new AbortController();
|
|
442
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
443
|
+
|
|
444
|
+
const fetchOptions = {
|
|
445
|
+
method: method.toUpperCase(),
|
|
446
|
+
headers: {
|
|
447
|
+
'Accept': 'application/json, text/plain, */*',
|
|
448
|
+
'User-Agent': 'KikuAgent/2.0',
|
|
449
|
+
...headers
|
|
450
|
+
},
|
|
451
|
+
signal: controller.signal
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
if (body && ['POST', 'PUT', 'PATCH'].includes(fetchOptions.method)) {
|
|
455
|
+
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
456
|
+
fetchOptions.headers['Content-Type'] = 'application/json';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const res = await fetch(url, fetchOptions);
|
|
460
|
+
clearTimeout(timeoutId);
|
|
461
|
+
|
|
462
|
+
const responseInfo = {
|
|
463
|
+
status: res.status,
|
|
464
|
+
statusText: res.statusText,
|
|
465
|
+
contentType: res.headers.get('content-type') || '',
|
|
466
|
+
finalUrl: res.url || url
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
if (!res.ok) {
|
|
470
|
+
let errorBody = '';
|
|
471
|
+
try { errorBody = (await res.text()).slice(0, 1000); } catch {}
|
|
472
|
+
return {
|
|
473
|
+
status: 'error',
|
|
474
|
+
...responseInfo,
|
|
475
|
+
message: `HTTP ${res.status} ${res.statusText}`,
|
|
476
|
+
errorBody
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const rawText = await res.text();
|
|
481
|
+
let data;
|
|
482
|
+
try {
|
|
483
|
+
data = JSON.parse(rawText);
|
|
484
|
+
} catch {
|
|
485
|
+
return {
|
|
486
|
+
status: 'success',
|
|
487
|
+
...responseInfo,
|
|
488
|
+
dataType: 'text',
|
|
489
|
+
contentLength: rawText.length,
|
|
490
|
+
data: rawText.length > maxResponseLength
|
|
491
|
+
? rawText.slice(0, maxResponseLength) + '... [truncated]'
|
|
492
|
+
: rawText
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const serialized = JSON.stringify(data, null, 2);
|
|
497
|
+
const truncated = serialized.length > maxResponseLength;
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
status: 'success',
|
|
501
|
+
...responseInfo,
|
|
502
|
+
dataType: 'json',
|
|
503
|
+
contentLength: serialized.length,
|
|
504
|
+
truncated,
|
|
505
|
+
data: truncated ? serialized.slice(0, maxResponseLength) + '... [truncated]' : data
|
|
506
|
+
};
|
|
507
|
+
} catch (err) {
|
|
508
|
+
return {
|
|
509
|
+
status: 'error',
|
|
510
|
+
url,
|
|
511
|
+
message: err.name === 'AbortError' ? `Request timed out (${timeoutMs}ms)` : err.message
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function findChromePath() {
|
|
517
|
+
const p = process.platform;
|
|
518
|
+
if (p === 'win32') {
|
|
519
|
+
const candidates = [
|
|
520
|
+
(process.env.LOCALAPPDATA || '') + '/Google/Chrome/Application/chrome.exe',
|
|
521
|
+
(process.env['PROGRAMFILES'] || '') + '/Google/Chrome/Application/chrome.exe',
|
|
522
|
+
(process.env['PROGRAMFILES(X86)'] || '') + '/Google/Chrome/Application/chrome.exe',
|
|
523
|
+
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
524
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
|
|
525
|
+
];
|
|
526
|
+
for (const c of candidates) {
|
|
527
|
+
if (c && existsSync(c)) return c;
|
|
528
|
+
}
|
|
529
|
+
} else if (p === 'darwin') {
|
|
530
|
+
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
531
|
+
} else {
|
|
532
|
+
const candidates = ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium-browser', '/usr/bin/chromium'];
|
|
533
|
+
for (const c of candidates) {
|
|
534
|
+
if (existsSync(c)) return c;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export async function screenshotWebpage(url, options = {}) {
|
|
541
|
+
if (!url || typeof url !== 'string' || !url.startsWith('http')) {
|
|
542
|
+
return { status: 'error', message: 'Valid HTTP/HTTPS URL is required.' };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const { width = 1280, height = 800, fullPage = false, timeoutMs = 20000 } = options;
|
|
546
|
+
const chromePath = findChromePath();
|
|
547
|
+
|
|
548
|
+
if (!chromePath) {
|
|
549
|
+
return { status: 'error', message: 'Google Chrome not found. Install Chrome to enable screenshot feature.' };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const tmpDir = await mkdtemp(join(tmpdir(), 'kiku-screenshot-'));
|
|
553
|
+
const profileDir = join(tmpDir, 'chrome-profile');
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const screenshotFile = 'kiku_screenshot.png';
|
|
557
|
+
const screenshotPath = join(tmpDir, screenshotFile);
|
|
558
|
+
const args = [
|
|
559
|
+
'--headless=new',
|
|
560
|
+
'--no-sandbox',
|
|
561
|
+
'--disable-gpu',
|
|
562
|
+
'--disable-dev-shm-usage',
|
|
563
|
+
'--disable-software-rasterizer',
|
|
564
|
+
`--user-data-dir=${profileDir}`,
|
|
565
|
+
`--window-size=${width},${height}`,
|
|
566
|
+
`--screenshot=${screenshotPath}`,
|
|
567
|
+
'--hide-scrollbars',
|
|
568
|
+
'--virtual-time-budget=8000'
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
args.push(url);
|
|
572
|
+
|
|
573
|
+
await new Promise((resolve, reject) => {
|
|
574
|
+
const child = execFile(chromePath, args, {
|
|
575
|
+
timeout: timeoutMs
|
|
576
|
+
}, (err, stdout, stderr) => {
|
|
577
|
+
if (err) reject(err);
|
|
578
|
+
else resolve({ stdout, stderr });
|
|
579
|
+
});
|
|
580
|
+
child.on('error', reject);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
if (!existsSync(screenshotPath)) {
|
|
584
|
+
return { status: 'error', message: 'Screenshot was not generated. The page may have failed to load.' };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const imgBuffer = await readFile(screenshotPath);
|
|
588
|
+
const base64 = imgBuffer.toString('base64');
|
|
589
|
+
const sizeKB = Math.round(imgBuffer.length / 1024);
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
status: 'success',
|
|
593
|
+
url,
|
|
594
|
+
width,
|
|
595
|
+
height,
|
|
596
|
+
format: 'png',
|
|
597
|
+
sizeKB,
|
|
598
|
+
base64Length: base64.length,
|
|
599
|
+
data: `data:image/png;base64,${base64}`
|
|
600
|
+
};
|
|
601
|
+
} catch (err) {
|
|
602
|
+
return {
|
|
603
|
+
status: 'error',
|
|
604
|
+
url,
|
|
605
|
+
message: `Screenshot failed: ${err.message}`
|
|
606
|
+
};
|
|
607
|
+
} finally {
|
|
608
|
+
try { await rm(tmpDir, { recursive: true, force: true }); } catch {}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export async function webExtract(query, opts = {}) {
|
|
613
|
+
if (!query || typeof query !== 'string') {
|
|
614
|
+
return { status: 'error', message: 'Query string is required.' };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const {
|
|
618
|
+
maxResults = 3,
|
|
619
|
+
fetchTimeoutMs = 12000,
|
|
620
|
+
maxContentLength = 4000,
|
|
621
|
+
allowedDomains = []
|
|
622
|
+
} = opts;
|
|
623
|
+
|
|
624
|
+
const cappedResults = Math.min(Math.max(maxResults, 1), 5);
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
const searchResult = await searchWeb(query, cappedResults + 2);
|
|
628
|
+
if (searchResult.status !== 'success' || !searchResult.results?.length) {
|
|
629
|
+
return {
|
|
630
|
+
status: 'no_results',
|
|
631
|
+
query,
|
|
632
|
+
message: 'No search results found for this query.',
|
|
633
|
+
searchResults: searchResult.results || []
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
let candidates = searchResult.results;
|
|
639
|
+
if (allowedDomains.length > 0) {
|
|
640
|
+
candidates = candidates.filter(r => {
|
|
641
|
+
try {
|
|
642
|
+
const host = new URL(r.url).hostname;
|
|
643
|
+
return allowedDomains.some(d => host.includes(d));
|
|
644
|
+
} catch { return false; }
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
const toFetch = candidates.slice(0, cappedResults);
|
|
650
|
+
const fetchPromises = toFetch.map(async (result) => {
|
|
651
|
+
try {
|
|
652
|
+
const page = await fetchWebpage(result.url, maxContentLength);
|
|
653
|
+
return {
|
|
654
|
+
title: result.title || page.meta?.title || '',
|
|
655
|
+
url: result.url,
|
|
656
|
+
snippet: result.snippet || '',
|
|
657
|
+
source: result.source || 'web',
|
|
658
|
+
...(page.status === 'success' ? {
|
|
659
|
+
content: page.content || '',
|
|
660
|
+
meta: page.meta || {},
|
|
661
|
+
headings: page.headings || [],
|
|
662
|
+
paragraphs: (page.paragraphs || []).slice(0, 8),
|
|
663
|
+
links: (page.links || []).slice(0, 5),
|
|
664
|
+
images: (page.images || []).slice(0, 3),
|
|
665
|
+
fetchStatus: 'success'
|
|
666
|
+
} : {
|
|
667
|
+
content: result.snippet || '',
|
|
668
|
+
fetchStatus: 'failed',
|
|
669
|
+
fetchError: page.message || 'Failed to fetch page'
|
|
670
|
+
})
|
|
671
|
+
};
|
|
672
|
+
} catch (err) {
|
|
673
|
+
return {
|
|
674
|
+
title: result.title || '',
|
|
675
|
+
url: result.url,
|
|
676
|
+
snippet: result.snippet || '',
|
|
677
|
+
source: result.source || 'web',
|
|
678
|
+
content: result.snippet || '',
|
|
679
|
+
fetchStatus: 'error',
|
|
680
|
+
fetchError: err.message
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const fetched = await Promise.all(fetchPromises);
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
const combinedContent = fetched
|
|
689
|
+
.filter(r => r.fetchStatus === 'success' && r.content)
|
|
690
|
+
.map(r => {
|
|
691
|
+
const metaLine = r.meta?.description ? `\nDescription: ${r.meta.description}` : '';
|
|
692
|
+
const headingsLine = r.headings?.length ? `\nHeadings: ${r.headings.map(h => h.text).join(' | ')}` : '';
|
|
693
|
+
return `### ${r.title}\nSource: ${r.url}${metaLine}${headingsLine}\n\n${r.content.slice(0, maxContentLength)}`;
|
|
694
|
+
})
|
|
695
|
+
.join('\n\n---\n\n');
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
status: 'success',
|
|
699
|
+
query,
|
|
700
|
+
language: searchResult.language,
|
|
701
|
+
pagesFetched: fetched.length,
|
|
702
|
+
pagesSucceeded: fetched.filter(r => r.fetchStatus === 'success').length,
|
|
703
|
+
combinedContentLength: combinedContent.length,
|
|
704
|
+
pages: fetched.map(r => ({
|
|
705
|
+
title: r.title,
|
|
706
|
+
url: r.url,
|
|
707
|
+
snippet: r.snippet,
|
|
708
|
+
source: r.source,
|
|
709
|
+
fetchStatus: r.fetchStatus,
|
|
710
|
+
contentLength: r.content?.length || 0,
|
|
711
|
+
meta: r.meta || {},
|
|
712
|
+
headings: r.headings || [],
|
|
713
|
+
topParagraphs: (r.paragraphs || []).slice(0, 5),
|
|
714
|
+
topLinks: (r.links || []).slice(0, 3),
|
|
715
|
+
topImages: (r.images || []).slice(0, 2)
|
|
716
|
+
})),
|
|
717
|
+
combinedContent: combinedContent || 'No content could be extracted from search results.'
|
|
718
|
+
};
|
|
719
|
+
}
|