kiku-agent 1.3.2 → 1.3.4
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 +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
package/server/agent/tools.js
CHANGED
|
@@ -16,11 +16,29 @@ import {
|
|
|
16
16
|
} from './webSearch.js';
|
|
17
17
|
import { readFile, readdir, stat, realpath } from 'node:fs/promises';
|
|
18
18
|
import { join, relative, extname, resolve } from 'node:path';
|
|
19
|
-
import { audioPlayer } from './audioPlayer.js';
|
|
19
|
+
import { audioPlayer, resolveCatalogSong } from './audioPlayer.js';
|
|
20
20
|
import { fetchYoutubeTranscript } from './youtubeTranscript.js';
|
|
21
21
|
import { sanitizeToolArgs, sanitizeUrl } from './sanitizer.js';
|
|
22
22
|
import { getGpuMetrics } from './gpuMetrics.js';
|
|
23
23
|
import { levenshteinDistance, levenshteinSimilarity, levenshteinScore, CATEGORY_ALIASES } from './catalogEntityResolver.js';
|
|
24
|
+
import { USER_ENV_PATH } from './envManager.js';
|
|
25
|
+
import { planSearchRequest } from './searchQueryPlanner.js';
|
|
26
|
+
import {
|
|
27
|
+
calculateHaversineDistance,
|
|
28
|
+
estimateWalkingTime,
|
|
29
|
+
estimateDrivingTime,
|
|
30
|
+
formatDistance,
|
|
31
|
+
findNearestLandmark,
|
|
32
|
+
buildOsmMapTag,
|
|
33
|
+
buildOsmUrl,
|
|
34
|
+
reverseGeocodeOsm,
|
|
35
|
+
searchLocationOsm,
|
|
36
|
+
cleanLocationQuery,
|
|
37
|
+
LANDMARK_REGISTRY,
|
|
38
|
+
detectIpLocation,
|
|
39
|
+
fetchRealtimeWeather,
|
|
40
|
+
scoreLocationConfidence
|
|
41
|
+
} from './locationService.js';
|
|
24
42
|
|
|
25
43
|
export function buildYouTubeSearchUrl(query) {
|
|
26
44
|
return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`;
|
|
@@ -50,11 +68,11 @@ export function formatCatalogTitle(primary, secondary) {
|
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
export const CATALOG_STOP_WORDS = new Set([
|
|
53
|
-
'the', 'from', 'what', 'are', 'is', 'about', 'drama', 'series', 'ost', 'song', 'songs', 'soundtrack', 'music', 'single', 'album', 'stage',
|
|
71
|
+
'the', 'of', 'from', 'what', 'are', 'is', 'about', 'drama', 'series', 'ost', 'song', 'songs', 'soundtrack', 'music', 'single', 'album', 'stage',
|
|
54
72
|
'brand', 'brands', 'ambassador', 'endorsement', 'commercial', 'role', 'roles', 'character', 'characters', 'award', 'awards', 'category', 'video', 'mv', 'clip',
|
|
55
73
|
'ju', 'jingyi', 'jujingyi', 'kiku', 'kikuuu', 'จวีจิ้งอี', 'จวี', 'จิ้งอี', 'เสี่ยวจวี', 'เสี่ยว', '鞠婧祎', '小鞠', '鞠',
|
|
56
74
|
'เพลง', 'เพลงของ', 'ซีรีส์', 'ซีรีส์ของ', 'ละคร', 'เรื่อง', 'ผลงาน', 'อัลบั้ม', 'คอนเสิร์ต', 'บทบาท', 'ตัวละคร', 'แบรนด์', 'รางวัล', 'ผลงานล่าสุด', 'พรีเซนเตอร์',
|
|
57
|
-
'ค้นหา', 'หา', 'ดู', 'เปิด', 'ฟัง', 'โปรโมท', 'โปรโมต', 'การโปรโมท', 'การโปรโมต', 'โฆษณา', 'แคมเปญ', 'ข้อมูล', 'รายละเอียด', 'บน', 'ใน', 'ที่', 'กับ', 'ช่วย', 'อยาก', 'ให้', 'หน่อย', 'คือ', 'อะไร', 'บ้าง', 'ไหม', 'มั้ย', 'มี',
|
|
75
|
+
'ค้นหา', 'หา', 'ดู', 'เปิด', 'ฟัง', 'โปรโมท', 'โปรโมต', 'การโปรโมท', 'การโปรโมต', 'โฆษณา', 'แคมเปญ', 'ข้อมูล', 'รายละเอียด', 'บน', 'ใน', 'ที่', 'กับ', 'ช่วย', 'อยาก', 'ให้', 'หน่อย', 'คือ', 'อะไร', 'บ้าง', 'ไหม', 'มั้ย', 'มี', 'แนว', 'สไตล์', 'ประเภท', 'เกี่ยวกับ', 'เรื่องราว', 'แสดง', 'แสดงเป็น', 'รับบท', 'บท',
|
|
58
76
|
'search', 'find', 'show', 'look', 'promo', 'promote', 'promoted', 'campaign', 'for', 'with', 'tell', 'me', 'please', 'can', 'you', 'info', 'details',
|
|
59
77
|
'搜索', '查找', '看看', '播放', '在', '上的', '关于', '有什么', '吗', '呢',
|
|
60
78
|
'bilibili', 'b站', 'youtube', 'weibo', 'douyin', 'spotify', 'qqmusic', 'iqiyi', 'youku', 'tencent'
|
|
@@ -79,10 +97,21 @@ export function createCatalogMatcher(query) {
|
|
|
79
97
|
const raw = String(query).toLowerCase().trim();
|
|
80
98
|
if (!raw) return () => true;
|
|
81
99
|
|
|
82
|
-
const allTokens = raw.split(/[^\p{L}\p{M}\p{N}]+/u).filter(t => t.length >= 2);
|
|
83
100
|
const deDated = raw.replace(/ใหม่ล่าสุด|ล่าสุด|最新|latest|newest|recent/gi, ' ');
|
|
101
|
+
let cleaned = deDated;
|
|
102
|
+
for (const sw of CATALOG_STOP_WORDS) {
|
|
103
|
+
if (sw.length >= 2) {
|
|
104
|
+
if (/^[a-z0-9]+$/i.test(sw)) {
|
|
105
|
+
cleaned = cleaned.replace(new RegExp('\\b' + sw + '\\b', 'gi'), ' ');
|
|
106
|
+
} else if (cleaned.includes(sw)) {
|
|
107
|
+
cleaned = cleaned.split(sw).join(' ');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
84
112
|
const datedTokens = deDated.split(/[^\p{L}\p{M}\p{N}]+/u).filter(t => t.length >= 2);
|
|
85
|
-
const
|
|
113
|
+
const strippedTokens = cleaned.split(/[^\p{L}\p{M}\p{N}]+/u).filter(t => t.length >= 2);
|
|
114
|
+
const coreTokens = (strippedTokens.length > 0 ? strippedTokens : datedTokens).filter(t => !CATALOG_STOP_WORDS.has(t));
|
|
86
115
|
|
|
87
116
|
if (coreTokens.length === 0) {
|
|
88
117
|
const broadMatcher = () => true;
|
|
@@ -188,6 +217,7 @@ const WEB_CACHE_TOOLS = new Set([
|
|
|
188
217
|
'search_chinese_web',
|
|
189
218
|
'search_bilibili',
|
|
190
219
|
'search_web_images',
|
|
220
|
+
'search_location_map',
|
|
191
221
|
]);
|
|
192
222
|
|
|
193
223
|
|
|
@@ -293,6 +323,81 @@ export const toolsSchema = [
|
|
|
293
323
|
}
|
|
294
324
|
}
|
|
295
325
|
},
|
|
326
|
+
{
|
|
327
|
+
type: "function",
|
|
328
|
+
function: {
|
|
329
|
+
name: "get_user_location",
|
|
330
|
+
description: "Resolve current user's local coordinates and nearby landmark using IP or client GPS. Use this ONLY when the user asks about their own current location (e.g. 'แถวนี้', 'ใกล้ฉัน', 'here'). Do NOT use this if the user specified a destination name (use search_location_map or get_weather instead).",
|
|
331
|
+
parameters: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
reason: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "Optional purpose or reason for requesting location (e.g. 'weather_forecast', 'current_weather', 'nearby_services', 'local_time')."
|
|
337
|
+
},
|
|
338
|
+
query: {
|
|
339
|
+
type: "string",
|
|
340
|
+
description: "Optional place name, district, or landmark to search or resolve coordinates for."
|
|
341
|
+
},
|
|
342
|
+
latitude: {
|
|
343
|
+
type: "number",
|
|
344
|
+
description: "Optional latitude coordinate to reverse geocode and match nearest landmarks."
|
|
345
|
+
},
|
|
346
|
+
longitude: {
|
|
347
|
+
type: "number",
|
|
348
|
+
description: "Optional longitude coordinate to reverse geocode and match nearest landmarks."
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
required: []
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
type: "function",
|
|
357
|
+
function: {
|
|
358
|
+
name: "search_location_map",
|
|
359
|
+
description: "Search for geographic coordinates, formatted address, real-time weather, and interactive map tags [MAP_OSM:lat,lon,Name] of any named location, landmark, venue, concert hall, filming studio, or address. Use this when the user asks for maps, navigation, directions, or location information of places.",
|
|
360
|
+
parameters: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
query: {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "The name of the location, venue, landmark, or address to search for (e.g. 'CentralWorld', 'Siam Paragon', '横店影视城', 'วัดพระแก้ว', 'ประตูท่าแพ')."
|
|
366
|
+
},
|
|
367
|
+
limit: {
|
|
368
|
+
type: "number",
|
|
369
|
+
description: "Maximum number of search results to return (default: 5, max: 10)."
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
required: ["query"]
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
type: "function",
|
|
378
|
+
function: {
|
|
379
|
+
name: "get_weather",
|
|
380
|
+
description: "Get real-time weather forecast, temperature, humidity, precipitation, wind speed, and conditions for any named city, landmark, or current location. Call this whenever answering questions about weather, rain, temperature, or climate. If the user specifies a place (e.g. 'CentralWorld', 'เซ็นทรัลเวิลด์', 'Chiang Mai'), pass it in 'location'.",
|
|
381
|
+
parameters: {
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: {
|
|
384
|
+
location: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "City, province, district, or landmark name (e.g. 'เซ็นทรัลเวิลด์', 'CentralWorld', 'กรุงเทพ', 'เชียงใหม่', 'Bangkok', 'Tokyo', 'Shanghai'). If omitted, uses current user location."
|
|
387
|
+
},
|
|
388
|
+
latitude: {
|
|
389
|
+
type: "number",
|
|
390
|
+
description: "Optional latitude coordinate."
|
|
391
|
+
},
|
|
392
|
+
longitude: {
|
|
393
|
+
type: "number",
|
|
394
|
+
description: "Optional longitude coordinate."
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
required: []
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
},
|
|
296
401
|
{
|
|
297
402
|
type: "function",
|
|
298
403
|
function: {
|
|
@@ -559,9 +664,12 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
559
664
|
parameters: {
|
|
560
665
|
type: "object",
|
|
561
666
|
properties: {
|
|
562
|
-
|
|
667
|
+
keywords: { type: "string", description: "Canonical search keywords with conversational filler removed" },
|
|
668
|
+
target_language: { type: "string", enum: ["zh", "th", "en", "ja", "ko"], description: "Preferred language for search results" },
|
|
669
|
+
domain_filter: { type: "array", items: { type: "string" }, description: "Optional hostnames to prioritize or restrict, e.g. ['weibo.com', 'bilibili.com']" },
|
|
670
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" }
|
|
563
671
|
},
|
|
564
|
-
required: [
|
|
672
|
+
required: []
|
|
565
673
|
}
|
|
566
674
|
}
|
|
567
675
|
},
|
|
@@ -569,7 +677,7 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
569
677
|
type: "function",
|
|
570
678
|
function: {
|
|
571
679
|
name: "fetch_webpage",
|
|
572
|
-
description: "Fetch and read the text content of a specific public web page URL. Returns extracted text — read the content field carefully and extract specific facts (names, dates, titles, descriptions) for your answer. NOTE: This cannot read SPA/React sites — use fetch_api for jujingyi-kikuuu.vercel.app.",
|
|
680
|
+
description: "Fetch and read the text content of a specific public web page URL. Returns extracted text — read the content field carefully and extract specific facts (names, dates, titles, descriptions) for your answer. NOTE: This cannot read SPA/React sites — use fetch_api for jujingyi-kikuuu.vercel.app. Login-walled or JavaScript-heavy social pages (Weibo, Douyin, Xiaohongshu) may return no readable text; when that happens, retry the mobile-mirror URL and then search_chinese_web / search_news on the topic instead of giving up.",
|
|
573
681
|
parameters: {
|
|
574
682
|
type: "object",
|
|
575
683
|
properties: {
|
|
@@ -583,13 +691,13 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
583
691
|
type: "function",
|
|
584
692
|
function: {
|
|
585
693
|
name: "fetch_api",
|
|
586
|
-
description: "Query the Ju Jingyi Official REST API (jujingyi-kikuuu.vercel.app/api/v1) for structured JSON data. Use this instead of fetch_webpage when the user shares a link to the platform site, or when you need live/verified data from the official API.",
|
|
694
|
+
description: "Query the Ju Jingyi Official REST API (jujingyi-kikuuu.vercel.app/api/v1) for structured JSON data. Use this instead of fetch_webpage when the user shares a link to the platform site, or when you need live/verified data from the official API. For third-party or general external APIs, use fetch_json instead.",
|
|
587
695
|
parameters: {
|
|
588
696
|
type: "object",
|
|
589
697
|
properties: {
|
|
590
|
-
endpoint: { type: "string", description: "API endpoint path (e.g. 'profile', 'works', '
|
|
591
|
-
query: { type: "string", description: "Optional search/filter query parameter" },
|
|
592
|
-
baseUrl: { type: "string", description: "Optional override base URL (defaults to https://jujingyi-kikuuu.vercel.app/api/v1)" }
|
|
698
|
+
endpoint: { type: "string", description: "API endpoint path under /api/v1/ (e.g. 'profile', 'works', 'discography', 'mv', 'brands', 'timeline', 'awards', 'gallery', 'social', 'character-roles', 'fandom', 'quotes', 'stats', 'search'). Automatic aliases ('music' -> 'discography', 'commercial' -> 'brands', 'milestones' -> 'timeline', 'honors' -> 'awards') are supported." },
|
|
699
|
+
query: { type: "string", description: "Optional search/filter query parameter ('q')" },
|
|
700
|
+
baseUrl: { type: "string", description: "Optional override base URL (defaults to https://jujingyi-kikuuu.vercel.app/api/v1, or http://localhost:3000/api/v1 in local development)" }
|
|
593
701
|
},
|
|
594
702
|
required: ["endpoint"]
|
|
595
703
|
}
|
|
@@ -682,10 +790,13 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
682
790
|
parameters: {
|
|
683
791
|
type: "object",
|
|
684
792
|
properties: {
|
|
685
|
-
|
|
793
|
+
keywords: { type: "string", description: "Canonical news keywords, e.g. '鞠婧祎 最新'" },
|
|
794
|
+
target_language: { type: "string", enum: ["zh", "th", "en", "ja", "ko"] },
|
|
795
|
+
domain_filter: { type: "array", items: { type: "string" }, description: "Optional news or official-site hostnames" },
|
|
796
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
686
797
|
maxResults: { type: "number", description: "Max results (default 8, max 15)" }
|
|
687
798
|
},
|
|
688
|
-
required: [
|
|
799
|
+
required: []
|
|
689
800
|
}
|
|
690
801
|
}
|
|
691
802
|
},
|
|
@@ -712,11 +823,14 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
712
823
|
parameters: {
|
|
713
824
|
type: "object",
|
|
714
825
|
properties: {
|
|
715
|
-
term: { type: "string", description: "
|
|
826
|
+
term: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
827
|
+
keywords: { type: "string", description: "Canonical lookup term, e.g. '宋昕冉 SNH48'" },
|
|
828
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
716
829
|
language: { type: "string", enum: ["zh", "th", "en"], description: "Optional: force language. Defaults to auto-detect from the term." },
|
|
830
|
+
target_language: { type: "string", enum: ["zh", "th", "en", "ja", "ko"], description: "Preferred language for results when language is unset" },
|
|
717
831
|
maxEntries: { type: "number", description: "Max encyclopedia entries to return (default 2, max 3)" }
|
|
718
832
|
},
|
|
719
|
-
required: [
|
|
833
|
+
required: []
|
|
720
834
|
}
|
|
721
835
|
}
|
|
722
836
|
},
|
|
@@ -728,10 +842,12 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
728
842
|
parameters: {
|
|
729
843
|
type: "object",
|
|
730
844
|
properties: {
|
|
731
|
-
term: { type: "string", description: "
|
|
845
|
+
term: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
846
|
+
keywords: { type: "string", description: "Canonical song/album/artist search term, e.g. 'Ju Jingyi 叹云兮'" },
|
|
847
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
732
848
|
maxResults: { type: "number", description: "Max results (default 8, max 20)" }
|
|
733
849
|
},
|
|
734
|
-
required: [
|
|
850
|
+
required: []
|
|
735
851
|
}
|
|
736
852
|
}
|
|
737
853
|
},
|
|
@@ -739,15 +855,18 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
739
855
|
type: "function",
|
|
740
856
|
function: {
|
|
741
857
|
name: "search_chinese_web",
|
|
742
|
-
description: "Search Chinese websites (Baidu first, then site-filtered DuckDuckGo across sina/sohu/weibo/bilibili/iqiyi/mgtv etc). Use for Chinese entertainment news, Weibo topics, and anything on Chinese portals.
|
|
858
|
+
description: "Search Chinese websites (Baidu first, then site-filtered DuckDuckGo across sina/sohu/weibo/bilibili/iqiyi/mgtv etc). Use for Chinese entertainment news, Weibo topics, and anything on Chinese portals. Enabled by default; set KIKU_ENABLE_CN_WEB_TOOLS=false to disable.",
|
|
743
859
|
parameters: {
|
|
744
860
|
type: "object",
|
|
745
861
|
properties: {
|
|
746
|
-
|
|
862
|
+
keywords: { type: "string", description: "Canonical Chinese search keywords, e.g. '鞠婧祎 微博'" },
|
|
863
|
+
target_language: { type: "string", enum: ["zh", "th", "en", "ja", "ko"] },
|
|
864
|
+
domain_filter: { type: "array", items: { type: "string" }, description: "Optional Chinese portal hostnames" },
|
|
865
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
747
866
|
maxResults: { type: "number", description: "Max results (default 8, max 15)" },
|
|
748
867
|
site: { type: "string", description: "Optional domain hint to restrict fallback search, e.g. 'sina.com.cn'" }
|
|
749
868
|
},
|
|
750
|
-
required: [
|
|
869
|
+
required: []
|
|
751
870
|
}
|
|
752
871
|
}
|
|
753
872
|
},
|
|
@@ -755,14 +874,17 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
755
874
|
type: "function",
|
|
756
875
|
function: {
|
|
757
876
|
name: "search_bilibili",
|
|
758
|
-
description: "Search Bilibili videos via its public API (stage performances, MVs, fancams, interviews). Returns title, author, duration, play count, publish date and video URL.
|
|
877
|
+
description: "Search Bilibili videos via its public API (stage performances, MVs, fancams, interviews). Returns title, author, duration, play count, publish date and video URL. Enabled by default; set KIKU_ENABLE_CN_WEB_TOOLS=false to disable.",
|
|
759
878
|
parameters: {
|
|
760
879
|
type: "object",
|
|
761
880
|
properties: {
|
|
762
|
-
keyword: { type: "string", description: "
|
|
881
|
+
keyword: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
882
|
+
keywords: { type: "string", description: "Canonical Bilibili search keywords, e.g. '宋昕冉 SNH48 舞台'" },
|
|
883
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
884
|
+
target_language: { type: "string", enum: ["zh", "th", "en", "ja", "ko"] },
|
|
763
885
|
maxResults: { type: "number", description: "Max results (default 8, max 20)" }
|
|
764
886
|
},
|
|
765
|
-
required: [
|
|
887
|
+
required: []
|
|
766
888
|
}
|
|
767
889
|
}
|
|
768
890
|
},
|
|
@@ -770,14 +892,17 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
770
892
|
type: "function",
|
|
771
893
|
function: {
|
|
772
894
|
name: "search_web_images",
|
|
773
|
-
description: "Search the web (Baidu Images & Chinese entertainment media portals) for photos, on-set stills (路透), red carpet appearances, and press images of Ju Jingyi. Returns direct image URLs, thumbnails, dimensions, and source article links.
|
|
895
|
+
description: "Search the web (Baidu Images & Chinese entertainment media portals) for photos, on-set stills (路透), red carpet appearances, and press images of Ju Jingyi. The query always includes 鞠婧祎 and results are filtered to artist-related images only. Returns direct image URLs, thumbnails, dimensions, and source article links. Enabled by default; set KIKU_ENABLE_CN_WEB_TOOLS=false to disable.",
|
|
774
896
|
parameters: {
|
|
775
897
|
type: "object",
|
|
776
898
|
properties: {
|
|
777
|
-
|
|
899
|
+
keywords: { type: "string", description: "Canonical image keywords or topic, e.g. '鞠婧祎 万花世界 路透'" },
|
|
900
|
+
target_language: { type: "string", enum: ["zh", "th", "en", "ja", "ko"] },
|
|
901
|
+
domain_filter: { type: "array", items: { type: "string" }, description: "Optional image-source hostnames" },
|
|
902
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
778
903
|
maxResults: { type: "number", description: "Max images to return (default 8, max 20)" }
|
|
779
904
|
},
|
|
780
|
-
required: [
|
|
905
|
+
required: []
|
|
781
906
|
}
|
|
782
907
|
}
|
|
783
908
|
},
|
|
@@ -866,6 +991,94 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
866
991
|
parameters: { type: "object", properties: {} }
|
|
867
992
|
}
|
|
868
993
|
},
|
|
994
|
+
{
|
|
995
|
+
type: "function",
|
|
996
|
+
function: {
|
|
997
|
+
name: "set_volume",
|
|
998
|
+
description: "Set the music playback volume level from 0 to 100 percent.",
|
|
999
|
+
parameters: {
|
|
1000
|
+
type: "object",
|
|
1001
|
+
properties: {
|
|
1002
|
+
volume: {
|
|
1003
|
+
type: "number",
|
|
1004
|
+
description: "Volume percentage between 0 and 100."
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
required: ["volume"]
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
type: "function",
|
|
1013
|
+
function: {
|
|
1014
|
+
name: "seek_music",
|
|
1015
|
+
description: "Seek or jump playback to a specific timestamp (in seconds or mm:ss format).",
|
|
1016
|
+
parameters: {
|
|
1017
|
+
type: "object",
|
|
1018
|
+
properties: {
|
|
1019
|
+
seconds: {
|
|
1020
|
+
type: "number",
|
|
1021
|
+
description: "Target playback position in seconds (e.g. 90)."
|
|
1022
|
+
},
|
|
1023
|
+
position: {
|
|
1024
|
+
type: "string",
|
|
1025
|
+
description: "Target position in mm:ss format (e.g. '01:30')."
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
type: "function",
|
|
1033
|
+
function: {
|
|
1034
|
+
name: "set_repeat_mode",
|
|
1035
|
+
description: "Set playback repeat or loop mode ('one' to repeat single track, 'all' to repeat playlist, 'off' to disable repeat).",
|
|
1036
|
+
parameters: {
|
|
1037
|
+
type: "object",
|
|
1038
|
+
properties: {
|
|
1039
|
+
mode: {
|
|
1040
|
+
type: "string",
|
|
1041
|
+
enum: ["one", "all", "off"],
|
|
1042
|
+
description: "Repeat mode: 'one', 'all', or 'off'."
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
required: ["mode"]
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
type: "function",
|
|
1051
|
+
function: {
|
|
1052
|
+
name: "set_shuffle_mode",
|
|
1053
|
+
description: "Enable or disable random shuffle playback.",
|
|
1054
|
+
parameters: {
|
|
1055
|
+
type: "object",
|
|
1056
|
+
properties: {
|
|
1057
|
+
enabled: {
|
|
1058
|
+
type: "boolean",
|
|
1059
|
+
description: "True to enable shuffle, false to disable."
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
required: ["enabled"]
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
type: "function",
|
|
1068
|
+
function: {
|
|
1069
|
+
name: "toggle_video_mode",
|
|
1070
|
+
description: "Toggle music video (MV) display view in the music player.",
|
|
1071
|
+
parameters: {
|
|
1072
|
+
type: "object",
|
|
1073
|
+
properties: {
|
|
1074
|
+
showVideo: {
|
|
1075
|
+
type: "boolean",
|
|
1076
|
+
description: "True to show video, false to hide video."
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
869
1082
|
{
|
|
870
1083
|
type: "function",
|
|
871
1084
|
function: {
|
|
@@ -924,12 +1137,11 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
924
1137
|
parameters: {
|
|
925
1138
|
type: "object",
|
|
926
1139
|
properties: {
|
|
927
|
-
query: {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
1140
|
+
query: { type: "string", description: "Deprecated compatibility alias for keywords" },
|
|
1141
|
+
keywords: { type: "string", description: "Canonical YouTube search keywords, e.g. 'Ju Jingyi FIND THE WAY'" },
|
|
1142
|
+
target_language: { type: "string", enum: ["zh", "th", "en", "ja", "ko"] }
|
|
931
1143
|
},
|
|
932
|
-
required: [
|
|
1144
|
+
required: []
|
|
933
1145
|
}
|
|
934
1146
|
}
|
|
935
1147
|
},
|
|
@@ -1191,6 +1403,85 @@ Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
|
1191
1403
|
}
|
|
1192
1404
|
];
|
|
1193
1405
|
|
|
1406
|
+
export const TOOL_CAPABILITIES = {
|
|
1407
|
+
get_current_datetime: ['temporal'],
|
|
1408
|
+
get_user_location: ['system_location', 'temporal', 'external_retrieval'],
|
|
1409
|
+
search_location_map: ['system_location', 'map_navigation', 'external_retrieval'],
|
|
1410
|
+
get_weather: ['system_location', 'weather_forecast', 'external_retrieval'],
|
|
1411
|
+
get_youtube_transcript: ['media_transcript', 'live_verification'],
|
|
1412
|
+
get_profile_and_fandom: ['catalog_profile', 'biography'],
|
|
1413
|
+
get_quotes: ['catalog_profile'],
|
|
1414
|
+
search_works: ['catalog_works', 'filmography'],
|
|
1415
|
+
get_work_detail: ['catalog_works', 'filmography'],
|
|
1416
|
+
get_character_roles: ['catalog_works', 'filmography'],
|
|
1417
|
+
search_discography: ['catalog_music', 'discography'],
|
|
1418
|
+
get_music_videos: ['catalog_music', 'media'],
|
|
1419
|
+
get_media_links: ['catalog_media'],
|
|
1420
|
+
get_brand_endorsements: ['catalog_brands', 'commercial'],
|
|
1421
|
+
get_career_timeline: ['catalog_timeline', 'biography'],
|
|
1422
|
+
get_awards: ['catalog_awards', 'honors'],
|
|
1423
|
+
get_social_feed: ['catalog_social'],
|
|
1424
|
+
get_gallery_photos: ['catalog_gallery', 'images'],
|
|
1425
|
+
global_search: ['catalog_omni', 'search'],
|
|
1426
|
+
web_search: ['web_search', 'live_verification', 'external_retrieval'],
|
|
1427
|
+
fetch_webpage: ['web_content', 'live_verification', 'external_retrieval'],
|
|
1428
|
+
fetch_api: ['external_api'],
|
|
1429
|
+
fetch_json: ['external_api'],
|
|
1430
|
+
read_local_file: ['file_system'],
|
|
1431
|
+
list_directory: ['file_system'],
|
|
1432
|
+
screenshot_webpage: ['web_render', 'visual'],
|
|
1433
|
+
web_extract: ['web_content', 'live_verification', 'external_retrieval'],
|
|
1434
|
+
search_news: ['news', 'live_verification', 'temporal'],
|
|
1435
|
+
fetch_rss: ['news', 'live_verification', 'temporal'],
|
|
1436
|
+
search_encyclopedia: ['encyclopedia', 'live_verification'],
|
|
1437
|
+
search_music_platforms: ['music_search', 'live_verification'],
|
|
1438
|
+
search_chinese_web: ['chinese_web', 'live_verification', 'external_retrieval'],
|
|
1439
|
+
search_bilibili: ['chinese_video', 'live_verification', 'media'],
|
|
1440
|
+
search_web_images: ['images', 'live_verification'],
|
|
1441
|
+
get_system_stats: ['system_telemetry'],
|
|
1442
|
+
play_music: ['music_control', 'audio_playback'],
|
|
1443
|
+
stop_music: ['music_control', 'audio_playback'],
|
|
1444
|
+
mute_music: ['music_control', 'audio_playback'],
|
|
1445
|
+
unmute_music: ['music_control', 'audio_playback'],
|
|
1446
|
+
pause_music: ['music_control', 'audio_playback'],
|
|
1447
|
+
resume_music: ['music_control', 'audio_playback'],
|
|
1448
|
+
next_track: ['music_control', 'audio_playback'],
|
|
1449
|
+
prev_track: ['music_control', 'audio_playback'],
|
|
1450
|
+
set_volume: ['music_control', 'audio_playback'],
|
|
1451
|
+
seek_music: ['music_control', 'audio_playback'],
|
|
1452
|
+
set_repeat_mode: ['music_control', 'audio_playback'],
|
|
1453
|
+
set_shuffle_mode: ['music_control', 'audio_playback'],
|
|
1454
|
+
toggle_video_mode: ['music_control', 'video_playback'],
|
|
1455
|
+
add_to_queue: ['music_control', 'queue_management'],
|
|
1456
|
+
remove_from_queue: ['music_control', 'queue_management'],
|
|
1457
|
+
clear_queue: ['music_control', 'queue_management'],
|
|
1458
|
+
get_queue: ['music_control', 'queue_management'],
|
|
1459
|
+
search_youtube: ['music_search', 'video_search'],
|
|
1460
|
+
get_current_track: ['music_control', 'audio_status'],
|
|
1461
|
+
get_music_status: ['music_control', 'audio_status'],
|
|
1462
|
+
run_subagent: ['autonomous_agent'],
|
|
1463
|
+
run_deep_research: ['autonomous_agent', 'research'],
|
|
1464
|
+
verify_claim: ['fact_check', 'live_verification'],
|
|
1465
|
+
suggest_topics: ['recommendation'],
|
|
1466
|
+
manage_skill: ['skill_management'],
|
|
1467
|
+
run_quiz: ['entertainment', 'quiz'],
|
|
1468
|
+
get_api_key: ['system_auth'],
|
|
1469
|
+
get_entertainment_metrics: ['industry_metrics'],
|
|
1470
|
+
get_industry_index: ['industry_metrics'],
|
|
1471
|
+
checkpoint_step: ['memory_checkpoint'],
|
|
1472
|
+
record_failure: ['memory_failure'],
|
|
1473
|
+
load_active_run: ['memory_context'],
|
|
1474
|
+
record_dynamic_event: ['catalog_write']
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
for (const item of toolsSchema) {
|
|
1478
|
+
const name = item.function?.name;
|
|
1479
|
+
if (name && TOOL_CAPABILITIES[name]) {
|
|
1480
|
+
item.capabilities = TOOL_CAPABILITIES[name];
|
|
1481
|
+
if (item.function) item.function.capabilities = TOOL_CAPABILITIES[name];
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1194
1485
|
|
|
1195
1486
|
const TOOL_DEFAULT_TIMEOUTS = {
|
|
1196
1487
|
run_subagent: 180_000,
|
|
@@ -1211,9 +1502,10 @@ const TOOL_DEFAULT_TIMEOUTS = {
|
|
|
1211
1502
|
search_web_images: 60_000,
|
|
1212
1503
|
};
|
|
1213
1504
|
|
|
1214
|
-
const
|
|
1505
|
+
const CN_WEB_DISABLED_VALUES = new Set(['false', '0', 'off', 'no']);
|
|
1506
|
+
const isCnWebEnabled = () => !CN_WEB_DISABLED_VALUES.has((process.env.KIKU_ENABLE_CN_WEB_TOOLS ?? '').trim().toLowerCase());
|
|
1215
1507
|
const cnWebDisabledMessage = (toolName) =>
|
|
1216
|
-
`"${toolName}" is disabled.
|
|
1508
|
+
`"${toolName}" is disabled because KIKU_ENABLE_CN_WEB_TOOLS="${(process.env.KIKU_ENABLE_CN_WEB_TOOLS ?? '').trim()}" is set in the environment. Remove that line (or set KIKU_ENABLE_CN_WEB_TOOLS=true) in your .env file — the package-root .env for repo checkouts, or ${USER_ENV_PATH} for global installs — to enable Chinese web tools (Baidu/Baike/Bilibili scraping — these sites use anti-bot protection and may be unreliable). Meanwhile use web_search, search_news, or search_encyclopedia.`;
|
|
1217
1509
|
|
|
1218
1510
|
async function ensureLlmAvailable(client) {
|
|
1219
1511
|
if (!client || typeof client.checkConnection !== 'function') return null;
|
|
@@ -1261,6 +1553,301 @@ export const toolHandlers = {
|
|
|
1261
1553
|
};
|
|
1262
1554
|
},
|
|
1263
1555
|
|
|
1556
|
+
get_user_location: async (args = {}, options = {}) => {
|
|
1557
|
+
const passedLoc = options.clientLocation || options.location || options.agent?.clientLocation;
|
|
1558
|
+
|
|
1559
|
+
if (passedLoc && passedLoc.denied) {
|
|
1560
|
+
return {
|
|
1561
|
+
status: "permission_denied_or_unavailable",
|
|
1562
|
+
location_granted: false,
|
|
1563
|
+
source: "client_denied",
|
|
1564
|
+
message: "User declined to share geolocation coordinates. Continuing with fallback or asking user to specify city name."
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const rawQuery = String(args.query || args.place || args.location || '').trim();
|
|
1569
|
+
if (rawQuery && !passedLoc && typeof args.latitude !== 'number') {
|
|
1570
|
+
const searchResults = await searchLocationOsm(rawQuery, options);
|
|
1571
|
+
if (searchResults && searchResults.length > 0) {
|
|
1572
|
+
const top = searchResults[0];
|
|
1573
|
+
const nearest = findNearestLandmark(top.latitude, top.longitude);
|
|
1574
|
+
const weather = await fetchRealtimeWeather(top.latitude, top.longitude, options);
|
|
1575
|
+
const confidence = scoreLocationConfidence({
|
|
1576
|
+
source: "osm_query_search",
|
|
1577
|
+
accuracy: null
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
let summaryParts = [`Location resolved for "${rawQuery}": ${top.displayName} (${top.latitude.toFixed(4)}, ${top.longitude.toFixed(4)})`];
|
|
1581
|
+
if (nearest) {
|
|
1582
|
+
summaryParts.push(nearest.summaryTh);
|
|
1583
|
+
}
|
|
1584
|
+
if (weather && weather.summaryTh) {
|
|
1585
|
+
summaryParts.push(weather.summaryTh);
|
|
1586
|
+
}
|
|
1587
|
+
summaryParts.push(top.mapTag);
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
status: "success",
|
|
1591
|
+
location_granted: true,
|
|
1592
|
+
source: "osm_query_search",
|
|
1593
|
+
query: rawQuery,
|
|
1594
|
+
coordinates: { latitude: top.latitude, longitude: top.longitude },
|
|
1595
|
+
latitude: top.latitude,
|
|
1596
|
+
longitude: top.longitude,
|
|
1597
|
+
formatted_address: top.displayName,
|
|
1598
|
+
subdistrict: top.subdistrict,
|
|
1599
|
+
district: top.district,
|
|
1600
|
+
city: top.city || top.province || rawQuery,
|
|
1601
|
+
province: top.province,
|
|
1602
|
+
country: top.country,
|
|
1603
|
+
postcode: top.postcode,
|
|
1604
|
+
weather,
|
|
1605
|
+
nearest_landmark: nearest,
|
|
1606
|
+
confidence,
|
|
1607
|
+
map_url: top.mapUrl,
|
|
1608
|
+
map_tag: top.mapTag,
|
|
1609
|
+
results: searchResults,
|
|
1610
|
+
message: summaryParts.join('. ')
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const hasExplicitCoords = typeof args.latitude === 'number' && typeof args.longitude === 'number';
|
|
1616
|
+
const hasPassedCoords = passedLoc && (typeof passedLoc.latitude === 'number' || typeof passedLoc.lat === 'number');
|
|
1617
|
+
|
|
1618
|
+
if (hasExplicitCoords || hasPassedCoords) {
|
|
1619
|
+
const lat = hasExplicitCoords ? Number(args.latitude) : Number(passedLoc.latitude ?? passedLoc.lat);
|
|
1620
|
+
const lon = hasExplicitCoords ? Number(args.longitude) : Number(passedLoc.longitude ?? passedLoc.lon ?? passedLoc.lng);
|
|
1621
|
+
const accuracy = passedLoc?.accuracy || null;
|
|
1622
|
+
const tz = String(passedLoc?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Bangkok');
|
|
1623
|
+
|
|
1624
|
+
const geo = await reverseGeocodeOsm(lat, lon, options);
|
|
1625
|
+
const nearest = findNearestLandmark(lat, lon);
|
|
1626
|
+
const weather = await fetchRealtimeWeather(lat, lon, options);
|
|
1627
|
+
|
|
1628
|
+
const city = String(geo?.city || passedLoc?.city || passedLoc?.locality || (nearest ? nearest.landmark.city : 'Bangkok'));
|
|
1629
|
+
const country = String(geo?.country || passedLoc?.country || (nearest ? nearest.landmark.country : 'Thailand'));
|
|
1630
|
+
const region = String(geo?.province || passedLoc?.region || passedLoc?.state || city);
|
|
1631
|
+
const subdistrict = String(geo?.subdistrict || '');
|
|
1632
|
+
const district = String(geo?.district || '');
|
|
1633
|
+
const postcode = String(geo?.postcode || '');
|
|
1634
|
+
const formattedAddress = String(geo?.displayName || `${district ? district + ', ' : ''}${city}, ${country}`);
|
|
1635
|
+
const mapUrl = geo?.mapUrl || buildOsmUrl(lat, lon);
|
|
1636
|
+
const mapTag = geo?.mapTag || buildOsmMapTag(lat, lon, district || city || 'Location');
|
|
1637
|
+
|
|
1638
|
+
let summaryParts = [`User location retrieved: ${formattedAddress} (${lat.toFixed(4)}, ${lon.toFixed(4)})`];
|
|
1639
|
+
if (nearest) {
|
|
1640
|
+
summaryParts.push(nearest.summaryTh);
|
|
1641
|
+
}
|
|
1642
|
+
if (weather && weather.summaryTh) {
|
|
1643
|
+
summaryParts.push(weather.summaryTh);
|
|
1644
|
+
}
|
|
1645
|
+
summaryParts.push(mapTag);
|
|
1646
|
+
|
|
1647
|
+
return {
|
|
1648
|
+
status: "success",
|
|
1649
|
+
location_granted: true,
|
|
1650
|
+
source: hasExplicitCoords ? "explicit_coordinates" : "client_location",
|
|
1651
|
+
coordinates: { latitude: lat, longitude: lon },
|
|
1652
|
+
latitude: lat,
|
|
1653
|
+
longitude: lon,
|
|
1654
|
+
accuracy,
|
|
1655
|
+
formatted_address: formattedAddress,
|
|
1656
|
+
subdistrict,
|
|
1657
|
+
district,
|
|
1658
|
+
city,
|
|
1659
|
+
region,
|
|
1660
|
+
province: geo?.province || region,
|
|
1661
|
+
country,
|
|
1662
|
+
country_code: geo?.countryCode || '',
|
|
1663
|
+
postcode,
|
|
1664
|
+
timezone: tz,
|
|
1665
|
+
weather,
|
|
1666
|
+
nearest_landmark: nearest,
|
|
1667
|
+
confidence: scoreLocationConfidence({
|
|
1668
|
+
source: hasExplicitCoords ? "explicit_coordinates" : "client_location",
|
|
1669
|
+
accuracy
|
|
1670
|
+
}),
|
|
1671
|
+
map_url: mapUrl,
|
|
1672
|
+
map_tag: mapTag,
|
|
1673
|
+
message: summaryParts.join('. ')
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const ipLoc = await detectIpLocation(options);
|
|
1678
|
+
const lat = ipLoc.latitude;
|
|
1679
|
+
const lon = ipLoc.longitude;
|
|
1680
|
+
const fallbackNearest = findNearestLandmark(lat, lon);
|
|
1681
|
+
const weather = await fetchRealtimeWeather(lat, lon, options);
|
|
1682
|
+
|
|
1683
|
+
let summaryParts = [`Location detected: ${ipLoc.city}, ${ipLoc.country} (${lat.toFixed(4)}, ${lon.toFixed(4)})`];
|
|
1684
|
+
if (fallbackNearest) {
|
|
1685
|
+
summaryParts.push(fallbackNearest.summaryTh);
|
|
1686
|
+
}
|
|
1687
|
+
if (weather && weather.summaryTh) {
|
|
1688
|
+
summaryParts.push(weather.summaryTh);
|
|
1689
|
+
}
|
|
1690
|
+
const mapTag = buildOsmMapTag(lat, lon, ipLoc.city);
|
|
1691
|
+
summaryParts.push(mapTag);
|
|
1692
|
+
|
|
1693
|
+
return {
|
|
1694
|
+
status: "success",
|
|
1695
|
+
location_granted: true,
|
|
1696
|
+
is_estimated_location: true,
|
|
1697
|
+
source: ipLoc.source,
|
|
1698
|
+
reason: args.reason || "weather_forecast",
|
|
1699
|
+
timezone: ipLoc.timezone,
|
|
1700
|
+
estimated_city: ipLoc.city,
|
|
1701
|
+
estimatedLocation: {
|
|
1702
|
+
city: ipLoc.city,
|
|
1703
|
+
region: ipLoc.region,
|
|
1704
|
+
country: ipLoc.country,
|
|
1705
|
+
latitude: lat,
|
|
1706
|
+
longitude: lon,
|
|
1707
|
+
timezone: ipLoc.timezone
|
|
1708
|
+
},
|
|
1709
|
+
latitude: lat,
|
|
1710
|
+
longitude: lon,
|
|
1711
|
+
city: ipLoc.city,
|
|
1712
|
+
country: ipLoc.country,
|
|
1713
|
+
weather,
|
|
1714
|
+
nearest_landmark: fallbackNearest,
|
|
1715
|
+
confidence: scoreLocationConfidence({
|
|
1716
|
+
source: ipLoc.source,
|
|
1717
|
+
accuracy: null
|
|
1718
|
+
}),
|
|
1719
|
+
map_url: buildOsmUrl(lat, lon),
|
|
1720
|
+
map_tag: mapTag,
|
|
1721
|
+
quickCityChoices: [
|
|
1722
|
+
{ nameTh: 'กรุงเทพมหานคร', nameEn: 'Bangkok', latitude: 13.7563, longitude: 100.5018 },
|
|
1723
|
+
{ nameTh: 'เชียงใหม่', nameEn: 'Chiang Mai', latitude: 18.7883, longitude: 98.9853 },
|
|
1724
|
+
{ nameTh: 'ภูเก็ต', nameEn: 'Phuket', latitude: 7.8804, longitude: 98.3923 },
|
|
1725
|
+
{ nameTh: 'ขอนแก่น', nameEn: 'Khon Kaen', latitude: 16.4419, longitude: 102.8360 },
|
|
1726
|
+
{ nameTh: 'พัทยา', nameEn: 'Pattaya', latitude: 12.9276, longitude: 100.8771 }
|
|
1727
|
+
],
|
|
1728
|
+
message: summaryParts.join('. ')
|
|
1729
|
+
};
|
|
1730
|
+
},
|
|
1731
|
+
|
|
1732
|
+
get_weather: async (args = {}, options = {}) => {
|
|
1733
|
+
let lat = null;
|
|
1734
|
+
let lon = null;
|
|
1735
|
+
let locationName = '';
|
|
1736
|
+
let mapTag = '';
|
|
1737
|
+
let isEstimated = false;
|
|
1738
|
+
|
|
1739
|
+
const query = String(args.location || args.place || args.city || '').trim();
|
|
1740
|
+
if (query) {
|
|
1741
|
+
const searchResults = await searchLocationOsm(query, options);
|
|
1742
|
+
if (searchResults && searchResults.length > 0) {
|
|
1743
|
+
const top = searchResults[0];
|
|
1744
|
+
lat = top.latitude;
|
|
1745
|
+
lon = top.longitude;
|
|
1746
|
+
locationName = top.displayName;
|
|
1747
|
+
mapTag = top.mapTag;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (lat === null && typeof args.latitude === 'number' && typeof args.longitude === 'number') {
|
|
1752
|
+
lat = Number(args.latitude);
|
|
1753
|
+
lon = Number(args.longitude);
|
|
1754
|
+
locationName = `พิกัด (${lat.toFixed(4)}, ${lon.toFixed(4)})`;
|
|
1755
|
+
mapTag = buildOsmMapTag(lat, lon, locationName);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
const clientLoc = options.clientLocation || options.location || options.agent?.clientLocation;
|
|
1759
|
+
if (lat === null && clientLoc && typeof clientLoc.latitude === 'number') {
|
|
1760
|
+
lat = Number(clientLoc.latitude);
|
|
1761
|
+
lon = Number(clientLoc.longitude ?? clientLoc.lon);
|
|
1762
|
+
locationName = String(clientLoc.city || clientLoc.formatted_address || 'Current Location');
|
|
1763
|
+
mapTag = buildOsmMapTag(lat, lon, locationName);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
if (lat === null) {
|
|
1767
|
+
const ipLoc = await detectIpLocation(options);
|
|
1768
|
+
lat = ipLoc.latitude;
|
|
1769
|
+
lon = ipLoc.longitude;
|
|
1770
|
+
locationName = `${ipLoc.city}, ${ipLoc.country}`;
|
|
1771
|
+
mapTag = buildOsmMapTag(lat, lon, ipLoc.city);
|
|
1772
|
+
isEstimated = true;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const weather = await fetchRealtimeWeather(lat, lon, options);
|
|
1776
|
+
const nearest = findNearestLandmark(lat, lon);
|
|
1777
|
+
|
|
1778
|
+
return {
|
|
1779
|
+
status: "success",
|
|
1780
|
+
location: locationName,
|
|
1781
|
+
latitude: lat,
|
|
1782
|
+
longitude: lon,
|
|
1783
|
+
is_estimated_location: isEstimated,
|
|
1784
|
+
current_weather: weather,
|
|
1785
|
+
nearest_landmark: nearest,
|
|
1786
|
+
confidence: scoreLocationConfidence({
|
|
1787
|
+
source: isEstimated ? 'ip_lookup' : (query ? 'osm_query_search' : 'explicit_coordinates'),
|
|
1788
|
+
accuracy: null
|
|
1789
|
+
}),
|
|
1790
|
+
map_tag: mapTag,
|
|
1791
|
+
message: weather ?
|
|
1792
|
+
`สภาพอากาศ ณ ${locationName}: ${weather.summaryTh} ${nearest ? '(' + nearest.summaryTh + ')' : ''}. ${mapTag}` :
|
|
1793
|
+
`ตำแหน่งที่ตั้ง: ${locationName}. ไม่สามารถดึงข้อมูลสภาพอากาศได้ในขณะนี้ ${mapTag}`
|
|
1794
|
+
};
|
|
1795
|
+
},
|
|
1796
|
+
|
|
1797
|
+
search_location_map: async (args = {}, options = {}) => {
|
|
1798
|
+
const rawQuery = String(args.query || args.location || args.place || '').trim();
|
|
1799
|
+
if (!rawQuery) {
|
|
1800
|
+
return {
|
|
1801
|
+
status: "error",
|
|
1802
|
+
message: "Parameter 'query' is required to search for a location map."
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const limit = Number(args.limit) > 0 ? Number(args.limit) : 5;
|
|
1807
|
+
const results = await searchLocationOsm(rawQuery, { ...options, limit });
|
|
1808
|
+
|
|
1809
|
+
if (!results || results.length === 0) {
|
|
1810
|
+
return {
|
|
1811
|
+
status: "success",
|
|
1812
|
+
count: 0,
|
|
1813
|
+
query: rawQuery,
|
|
1814
|
+
results: [],
|
|
1815
|
+
message: `ไม่พบข้อมูลพิกัดสถานที่สำหรับ "${rawQuery}" บน OpenStreetMap กรุณาลองใช้ชื่อสถานที่หรือคำค้นหาอื่น`
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
const top = results[0];
|
|
1820
|
+
const nearest = findNearestLandmark(top.latitude, top.longitude);
|
|
1821
|
+
const weather = await fetchRealtimeWeather(top.latitude, top.longitude, options);
|
|
1822
|
+
|
|
1823
|
+
let summaryLines = [`พบพิกัดบน OpenStreetMap สำหรับ "${rawQuery}" ดังนี้:`];
|
|
1824
|
+
results.forEach((r, idx) => {
|
|
1825
|
+
summaryLines.push(`${idx + 1}. ${r.name} (${r.province || r.city || r.country}) [Lat: ${r.latitude.toFixed(4)}, Lon: ${r.longitude.toFixed(4)}]`);
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
summaryLines.push(`\nพิกัดหลัก: ${top.displayName}`);
|
|
1829
|
+
if (nearest) {
|
|
1830
|
+
summaryLines.push(`สถานที่ใกล้เคียง: ${nearest.summaryTh}`);
|
|
1831
|
+
}
|
|
1832
|
+
if (weather && weather.summaryTh) {
|
|
1833
|
+
summaryLines.push(`สภาพอากาศ: ${weather.summaryTh}`);
|
|
1834
|
+
}
|
|
1835
|
+
summaryLines.push(`\nแผนที่ปักหมุด: ${top.mapTag}`);
|
|
1836
|
+
|
|
1837
|
+
return {
|
|
1838
|
+
status: "success",
|
|
1839
|
+
count: results.length,
|
|
1840
|
+
query: rawQuery,
|
|
1841
|
+
primaryLocation: top,
|
|
1842
|
+
nearest_landmark: nearest,
|
|
1843
|
+
weather,
|
|
1844
|
+
results,
|
|
1845
|
+
map_url: top.mapUrl,
|
|
1846
|
+
map_tag: top.mapTag,
|
|
1847
|
+
message: summaryLines.join('\n')
|
|
1848
|
+
};
|
|
1849
|
+
},
|
|
1850
|
+
|
|
1264
1851
|
get_youtube_transcript: async (args = {}) => fetchYoutubeTranscript(args),
|
|
1265
1852
|
|
|
1266
1853
|
get_profile_and_fandom: async ({ section, query } = {}) => {
|
|
@@ -1366,8 +1953,8 @@ export const toolHandlers = {
|
|
|
1366
1953
|
broadMatch = true;
|
|
1367
1954
|
} else {
|
|
1368
1955
|
results = results.filter(w => matcher(
|
|
1369
|
-
w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis,
|
|
1370
|
-
...(w.aliases || []), ...(w.keywords || [])
|
|
1956
|
+
w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis, w.category,
|
|
1957
|
+
...(w.highlights || []), ...(w.aliases || []), ...(w.keywords || [])
|
|
1371
1958
|
));
|
|
1372
1959
|
}
|
|
1373
1960
|
}
|
|
@@ -1380,8 +1967,8 @@ export const toolHandlers = {
|
|
|
1380
1967
|
let relaxed = [...baseWorks];
|
|
1381
1968
|
const fallbackMatcher = matcher || createCatalogMatcher(query);
|
|
1382
1969
|
relaxed = relaxed.filter(w => fallbackMatcher(
|
|
1383
|
-
w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis,
|
|
1384
|
-
...(w.aliases || []), ...(w.keywords || [])
|
|
1970
|
+
w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis, w.category,
|
|
1971
|
+
...(w.highlights || []), ...(w.aliases || []), ...(w.keywords || [])
|
|
1385
1972
|
));
|
|
1386
1973
|
if (relaxed.length > 0) {
|
|
1387
1974
|
results = relaxed;
|
|
@@ -2112,7 +2699,10 @@ export const toolHandlers = {
|
|
|
2112
2699
|
const matcher = createCatalogMatcher(query);
|
|
2113
2700
|
|
|
2114
2701
|
const matchedWorks = worksData.filter(w =>
|
|
2115
|
-
matcher(
|
|
2702
|
+
matcher(
|
|
2703
|
+
w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis, w.category,
|
|
2704
|
+
...(w.highlights || []), ...(w.aliases || []), ...(w.keywords || [])
|
|
2705
|
+
)
|
|
2116
2706
|
).slice(0, 4).map(w => ({
|
|
2117
2707
|
title: `${w.titleTh} (${w.year}) - Role: ${w.role}`,
|
|
2118
2708
|
trailerYouTubeSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.titleCn || w.titleEn} Trailer`)
|
|
@@ -2167,8 +2757,15 @@ export const toolHandlers = {
|
|
|
2167
2757
|
};
|
|
2168
2758
|
},
|
|
2169
2759
|
|
|
2170
|
-
web_search: async (
|
|
2171
|
-
|
|
2760
|
+
web_search: async (args = {}) => {
|
|
2761
|
+
const plan = planSearchRequest(args, { kind: 'web' });
|
|
2762
|
+
if (!plan.originalQuery) return { status: 'error', message: 'query parameter is required (use keywords for new callers).' };
|
|
2763
|
+
return await searchWeb(plan.keywords, undefined, {
|
|
2764
|
+
originalQuery: plan.originalQuery,
|
|
2765
|
+
targetLanguage: plan.targetLanguage,
|
|
2766
|
+
allowedDomains: plan.domains,
|
|
2767
|
+
queryPlan: plan,
|
|
2768
|
+
});
|
|
2172
2769
|
},
|
|
2173
2770
|
|
|
2174
2771
|
fetch_webpage: async ({ url } = {}) => {
|
|
@@ -2176,41 +2773,96 @@ export const toolHandlers = {
|
|
|
2176
2773
|
},
|
|
2177
2774
|
|
|
2178
2775
|
fetch_api: async ({ endpoint, query, baseUrl } = {}) => {
|
|
2179
|
-
if (!endpoint) return { status: "error", message: "endpoint parameter is required (e.g. 'profile', 'works', '
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2776
|
+
if (!endpoint) return { status: "error", message: "endpoint parameter is required (e.g. 'profile', 'works', 'discography', 'brands', 'timeline', 'awards')" };
|
|
2777
|
+
const defaultBase = 'https://jujingyi-kikuuu.vercel.app/api/v1';
|
|
2778
|
+
let rawBase = String(baseUrl || defaultBase).trim().replace(/\/+$/, '');
|
|
2779
|
+
if (!rawBase.includes('/api/v1')) {
|
|
2780
|
+
rawBase = `${rawBase}/api/v1`;
|
|
2781
|
+
}
|
|
2782
|
+
const originCheck = sanitizeUrl(rawBase, { allowLoopback: true });
|
|
2182
2783
|
if (!originCheck.safe) {
|
|
2183
2784
|
return { status: "error", message: `Unsafe API base URL: ${originCheck.reason}` };
|
|
2184
2785
|
}
|
|
2786
|
+
|
|
2787
|
+
const isLocalOrigin = Boolean(originCheck.url && (
|
|
2788
|
+
originCheck.url.hostname === 'localhost' ||
|
|
2789
|
+
originCheck.url.hostname === '127.0.0.1' ||
|
|
2790
|
+
originCheck.url.hostname.endsWith('.local')
|
|
2791
|
+
));
|
|
2792
|
+
|
|
2185
2793
|
const allowedOrigins = new Set([
|
|
2186
2794
|
'https://jujingyi-kikuuu.vercel.app',
|
|
2187
2795
|
...(process.env.KIKU_API_ALLOWED_ORIGINS || '').split(',').map(value => value.trim()).filter(Boolean)
|
|
2188
2796
|
]);
|
|
2189
|
-
if (!originCheck.url || !allowedOrigins.has(originCheck.url.origin)) {
|
|
2797
|
+
if (!originCheck.url || (!isLocalOrigin && !allowedOrigins.has(originCheck.url.origin))) {
|
|
2190
2798
|
return { status: 'error', message: 'API base origin is not allowlisted.' };
|
|
2191
2799
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
if (
|
|
2195
|
-
|
|
2800
|
+
|
|
2801
|
+
let rawEndpoint = String(endpoint || '').trim();
|
|
2802
|
+
if (/^https?:\/\//i.test(rawEndpoint)) {
|
|
2803
|
+
try {
|
|
2804
|
+
const parsedEndpoint = new URL(rawEndpoint);
|
|
2805
|
+
if (parsedEndpoint.origin !== originCheck.url.origin && !isLocalOrigin) {
|
|
2806
|
+
return { status: 'error', message: `API endpoint must point to ${originCheck.url.origin}. For general external APIs, use fetch_json(url=...) instead.` };
|
|
2807
|
+
}
|
|
2808
|
+
rawEndpoint = `${parsedEndpoint.pathname}${parsedEndpoint.search}`;
|
|
2809
|
+
} catch {}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
let cleanPath = rawEndpoint.replace(/^\/?api\/v1\/?/i, '').replace(/^\/+/, '');
|
|
2813
|
+
const qIndex = cleanPath.indexOf('?');
|
|
2814
|
+
let pathPart = qIndex >= 0 ? cleanPath.slice(0, qIndex) : cleanPath;
|
|
2815
|
+
const queryPart = qIndex >= 0 ? cleanPath.slice(qIndex + 1) : '';
|
|
2816
|
+
|
|
2817
|
+
const ENDPOINT_ALIASES = {
|
|
2818
|
+
'music': 'discography',
|
|
2819
|
+
'songs': 'discography',
|
|
2820
|
+
'tracks': 'discography',
|
|
2821
|
+
'commercial': 'brands',
|
|
2822
|
+
'endorsements': 'brands',
|
|
2823
|
+
'milestones': 'timeline',
|
|
2824
|
+
'history': 'timeline',
|
|
2825
|
+
'honors': 'awards',
|
|
2826
|
+
'drama': 'works',
|
|
2827
|
+
'dramas': 'works',
|
|
2828
|
+
'videos': 'mv',
|
|
2829
|
+
'photos': 'gallery'
|
|
2830
|
+
};
|
|
2831
|
+
|
|
2832
|
+
const firstSegment = pathPart.split('/')[0].toLowerCase();
|
|
2833
|
+
if (ENDPOINT_ALIASES[firstSegment]) {
|
|
2834
|
+
const rest = pathPart.slice(firstSegment.length);
|
|
2835
|
+
pathPart = `${ENDPOINT_ALIASES[firstSegment]}${rest}`;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
const finalPath = `/${pathPart}`;
|
|
2839
|
+
const target = new URL(`${originCheck.url.origin}/api/v1${finalPath}`);
|
|
2840
|
+
|
|
2841
|
+
if (queryPart) {
|
|
2842
|
+
const existingParams = new URLSearchParams(queryPart);
|
|
2843
|
+
for (const [k, v] of existingParams.entries()) {
|
|
2844
|
+
target.searchParams.set(k, v);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
if (query) {
|
|
2848
|
+
target.searchParams.set('q', query);
|
|
2196
2849
|
}
|
|
2197
|
-
if (query) target.searchParams.set('q', query);
|
|
2198
2850
|
|
|
2199
2851
|
try {
|
|
2200
|
-
const result = await fetchJson(target.href, { timeoutMs: 10000, maxResponseLength: 100000 });
|
|
2852
|
+
const result = await fetchJson(target.href, { timeoutMs: 10000, maxResponseLength: 100000, allowLoopback: isLocalOrigin });
|
|
2201
2853
|
if (result.status !== 'success') return result;
|
|
2202
2854
|
const json = result.data;
|
|
2203
2855
|
const data = json?.data || json;
|
|
2204
2856
|
return {
|
|
2205
2857
|
status: "success",
|
|
2206
|
-
endpoint:
|
|
2858
|
+
endpoint: finalPath,
|
|
2207
2859
|
count: Array.isArray(data) ? data.length : undefined,
|
|
2208
2860
|
data
|
|
2209
2861
|
};
|
|
2210
2862
|
} catch (err) {
|
|
2211
2863
|
return {
|
|
2212
2864
|
status: "error",
|
|
2213
|
-
endpoint:
|
|
2865
|
+
endpoint: finalPath,
|
|
2214
2866
|
message: err.name === 'AbortError' ? 'API request timed out' : err.message
|
|
2215
2867
|
};
|
|
2216
2868
|
}
|
|
@@ -2220,42 +2872,59 @@ export const toolHandlers = {
|
|
|
2220
2872
|
return await fetchJson(url, { method, headers, body });
|
|
2221
2873
|
},
|
|
2222
2874
|
|
|
2223
|
-
search_news: async (
|
|
2224
|
-
|
|
2225
|
-
return
|
|
2875
|
+
search_news: async (args = {}) => {
|
|
2876
|
+
const plan = planSearchRequest(args, { kind: 'news' });
|
|
2877
|
+
if (!plan.originalQuery) return { status: 'error', message: 'query parameter is required (use keywords for new callers).' };
|
|
2878
|
+
return await searchNews(plan.keywords, args.maxResults, { queryPlan: plan });
|
|
2226
2879
|
},
|
|
2227
2880
|
|
|
2228
2881
|
fetch_rss: async ({ url, maxItems } = {}) => {
|
|
2229
2882
|
return await fetchRssFeed(url, maxItems);
|
|
2230
2883
|
},
|
|
2231
2884
|
|
|
2232
|
-
search_encyclopedia: async (
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2885
|
+
search_encyclopedia: async (args = {}) => {
|
|
2886
|
+
const plan = planSearchRequest({
|
|
2887
|
+
keywords: args.keywords || args.term || args.query || '',
|
|
2888
|
+
target_language: args.language || args.target_language,
|
|
2889
|
+
});
|
|
2890
|
+
if (!plan.originalQuery) return { status: 'error', message: 'term parameter is required (use keywords for new callers).' };
|
|
2891
|
+
const term = plan.entities[0]?.native || plan.keywords || plan.originalQuery;
|
|
2892
|
+
const lang = ['zh', 'th', 'en'].includes(args.language) ? args.language : detectLanguage(term);
|
|
2893
|
+
return await searchEncyclopedia(term, lang, args.maxEntries);
|
|
2236
2894
|
},
|
|
2237
2895
|
|
|
2238
|
-
search_music_platforms: async (
|
|
2239
|
-
|
|
2240
|
-
|
|
2896
|
+
search_music_platforms: async (args = {}) => {
|
|
2897
|
+
const plan = planSearchRequest({
|
|
2898
|
+
keywords: args.keywords || args.term || args.query || '',
|
|
2899
|
+
target_language: args.target_language,
|
|
2900
|
+
});
|
|
2901
|
+
if (!plan.originalQuery) return { status: 'error', message: 'term parameter is required (use keywords for new callers).' };
|
|
2902
|
+
return await searchMusicPlatforms(plan.keywords || plan.originalQuery, args.maxResults);
|
|
2241
2903
|
},
|
|
2242
2904
|
|
|
2243
|
-
search_chinese_web: async (
|
|
2905
|
+
search_chinese_web: async (args = {}) => {
|
|
2244
2906
|
if (!isCnWebEnabled()) return { status: 'error', gated: true, message: cnWebDisabledMessage('search_chinese_web') };
|
|
2245
|
-
|
|
2246
|
-
return
|
|
2907
|
+
const plan = planSearchRequest(args, { kind: 'chinese' });
|
|
2908
|
+
if (!plan.originalQuery) return { status: 'error', message: 'query parameter is required (use keywords for new callers).' };
|
|
2909
|
+
return await searchChineseWeb(plan.keywords, args.maxResults, {
|
|
2910
|
+
site: args.site,
|
|
2911
|
+
allowedDomains: plan.domains,
|
|
2912
|
+
queryPlan: plan,
|
|
2913
|
+
});
|
|
2247
2914
|
},
|
|
2248
2915
|
|
|
2249
|
-
search_bilibili: async (
|
|
2916
|
+
search_bilibili: async (args = {}) => {
|
|
2250
2917
|
if (!isCnWebEnabled()) return { status: 'error', gated: true, message: cnWebDisabledMessage('search_bilibili') };
|
|
2251
|
-
|
|
2252
|
-
return
|
|
2918
|
+
const plan = planSearchRequest(args, { kind: 'chinese' });
|
|
2919
|
+
if (!plan.originalQuery) return { status: 'error', message: 'keyword parameter is required (use keywords for new callers).' };
|
|
2920
|
+
return await searchBilibili(plan.keywords || plan.originalQuery, args.maxResults);
|
|
2253
2921
|
},
|
|
2254
2922
|
|
|
2255
|
-
search_web_images: async (
|
|
2923
|
+
search_web_images: async (args = {}) => {
|
|
2256
2924
|
if (!isCnWebEnabled()) return { status: 'error', gated: true, message: cnWebDisabledMessage('search_web_images') };
|
|
2257
|
-
|
|
2258
|
-
return
|
|
2925
|
+
const plan = planSearchRequest(args, { kind: 'image' });
|
|
2926
|
+
if (!plan.originalQuery) return { status: 'error', message: 'query parameter is required (use keywords for new callers).' };
|
|
2927
|
+
return await searchWebImages(plan.keywords, args.maxResults, { queryPlan: plan });
|
|
2259
2928
|
},
|
|
2260
2929
|
|
|
2261
2930
|
read_local_file: async ({ path: filePath, maxLines = 200 } = {}) => {
|
|
@@ -2420,56 +3089,147 @@ export const toolHandlers = {
|
|
|
2420
3089
|
|
|
2421
3090
|
play_music: async (args) => {
|
|
2422
3091
|
try {
|
|
2423
|
-
const query = args.query || '';
|
|
3092
|
+
const query = (args.query || '').trim();
|
|
3093
|
+
if (!query) {
|
|
3094
|
+
return { status: 'error', action: 'play', message: 'No song query provided. Tell me a Ju Jingyi song or OST name.' };
|
|
3095
|
+
}
|
|
3096
|
+
const catalogMatch = resolveCatalogSong(query);
|
|
3097
|
+
const songInfo = catalogMatch ? catalogMatch : null;
|
|
3098
|
+
const effectiveTitle = songInfo?.title || query;
|
|
2424
3099
|
|
|
2425
3100
|
const hasArtist = /jujingyi|จวีจิ้งอี|鞠婧祎/i.test(query);
|
|
2426
3101
|
const isUrl = /^https?:\/\//.test(query);
|
|
2427
|
-
const searchQuery = (!hasArtist && !isUrl) ? `Ju Jingyi ${
|
|
2428
|
-
const
|
|
2429
|
-
|
|
3102
|
+
const searchQuery = (!hasArtist && !isUrl) ? `Ju Jingyi ${effectiveTitle}` : effectiveTitle;
|
|
3103
|
+
const youtubeUrl = songInfo?.youtubeId
|
|
3104
|
+
? buildYouTubeWatchUrl(songInfo.youtubeId)
|
|
3105
|
+
: buildYouTubeSearchUrl(searchQuery);
|
|
3106
|
+
let playMsg = '';
|
|
3107
|
+
let localPlayback = false;
|
|
3108
|
+
let playbackError = null;
|
|
3109
|
+
try {
|
|
3110
|
+
playMsg = await audioPlayer.play(searchQuery);
|
|
3111
|
+
localPlayback = true;
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
|
|
3114
|
+
playbackError = err?.message || 'Local audio unavailable';
|
|
3115
|
+
playMsg = `Local audio unavailable (${playbackError}). Listen here: ${youtubeUrl}`;
|
|
3116
|
+
}
|
|
3117
|
+
return {
|
|
3118
|
+
status: 'success',
|
|
3119
|
+
action: 'play',
|
|
3120
|
+
message: playMsg,
|
|
3121
|
+
playing: effectiveTitle,
|
|
3122
|
+
song: songInfo,
|
|
3123
|
+
youtubeUrl,
|
|
3124
|
+
localPlayback,
|
|
3125
|
+
...(playbackError ? { playbackError } : {})
|
|
3126
|
+
};
|
|
2430
3127
|
} catch (err) {
|
|
2431
|
-
return { status: 'error', message: err.message };
|
|
3128
|
+
return { status: 'error', action: 'play', message: err.message };
|
|
2432
3129
|
}
|
|
2433
3130
|
},
|
|
2434
3131
|
|
|
2435
3132
|
stop_music: async () => {
|
|
2436
|
-
const result =
|
|
2437
|
-
return { status: 'success', message: result };
|
|
3133
|
+
const result = audioPlayer.stop();
|
|
3134
|
+
return { status: 'success', action: 'stop', message: `[OK] ${result || 'Playback stopped'}` };
|
|
2438
3135
|
},
|
|
2439
3136
|
|
|
2440
3137
|
mute_music: async () => {
|
|
2441
|
-
const result =
|
|
2442
|
-
|
|
3138
|
+
const result = audioPlayer.mute();
|
|
3139
|
+
|
|
3140
|
+
if (result && result.includes('No song')) {
|
|
3141
|
+
return { status: 'error', action: 'mute', message: result };
|
|
3142
|
+
}
|
|
3143
|
+
return { status: 'success', action: 'mute', message: `[OK] ${result || 'Muted'}` };
|
|
2443
3144
|
},
|
|
2444
3145
|
|
|
2445
3146
|
unmute_music: async () => {
|
|
2446
|
-
const result =
|
|
2447
|
-
|
|
3147
|
+
const result = audioPlayer.unmute();
|
|
3148
|
+
if (result && result.includes('No song')) {
|
|
3149
|
+
return { status: 'error', action: 'unmute', message: result };
|
|
3150
|
+
}
|
|
3151
|
+
return { status: 'success', action: 'unmute', message: `[OK] ${result || 'Unmuted'}` };
|
|
2448
3152
|
},
|
|
2449
3153
|
|
|
2450
3154
|
pause_music: async () => {
|
|
2451
|
-
const result =
|
|
2452
|
-
|
|
3155
|
+
const result = audioPlayer.pause();
|
|
3156
|
+
if (result && (result.includes('No song') || result.includes('already paused'))) {
|
|
3157
|
+
const isIdle = result.includes('No song');
|
|
3158
|
+
return { status: isIdle ? 'error' : 'success', action: 'pause', message: result };
|
|
3159
|
+
}
|
|
3160
|
+
return { status: 'success', action: 'pause', message: `[OK] ${result || 'Playback paused'}` };
|
|
2453
3161
|
},
|
|
2454
3162
|
|
|
2455
3163
|
resume_music: async () => {
|
|
2456
|
-
const result =
|
|
2457
|
-
|
|
3164
|
+
const result = audioPlayer.resume();
|
|
3165
|
+
if (result && result.includes('No track')) {
|
|
3166
|
+
return { status: 'error', action: 'resume', message: result };
|
|
3167
|
+
}
|
|
3168
|
+
return { status: 'success', action: 'resume', message: `[OK] ${result || 'Playback resumed'}` };
|
|
2458
3169
|
},
|
|
2459
3170
|
|
|
2460
3171
|
next_track: async () => {
|
|
2461
|
-
const result =
|
|
2462
|
-
|
|
3172
|
+
const result = audioPlayer.next();
|
|
3173
|
+
if (result && (result.includes('No next track') || result.includes('No track') || result.includes('No song'))) {
|
|
3174
|
+
return { status: 'error', action: 'next', message: result };
|
|
3175
|
+
}
|
|
3176
|
+
return { status: 'success', action: 'next', message: `[OK] ${result || 'Skipped to next track'}`, currentTrack: audioPlayer.currentTrack };
|
|
2463
3177
|
},
|
|
2464
3178
|
|
|
2465
3179
|
prev_track: async () => {
|
|
2466
|
-
const result =
|
|
2467
|
-
|
|
3180
|
+
const result = audioPlayer.prev();
|
|
3181
|
+
if (result && (result.includes('No previous track') || result.includes('No track') || result.includes('No song'))) {
|
|
3182
|
+
return { status: 'error', action: 'prev', message: result };
|
|
3183
|
+
}
|
|
3184
|
+
return { status: 'success', action: 'prev', message: `[OK] ${result || 'Skipped to previous track'}`, currentTrack: audioPlayer.currentTrack };
|
|
3185
|
+
},
|
|
3186
|
+
|
|
3187
|
+
set_volume: async (args) => {
|
|
3188
|
+
const vol = Math.max(0, Math.min(100, Number(args?.volume) || 0));
|
|
3189
|
+
const result = audioPlayer.setVolume(vol);
|
|
3190
|
+
return { status: 'success', action: 'set_volume', volume: vol, message: result };
|
|
3191
|
+
},
|
|
3192
|
+
|
|
3193
|
+
seek_music: async (args) => {
|
|
3194
|
+
let targetSec = 0;
|
|
3195
|
+
if (typeof args?.seconds === 'number') {
|
|
3196
|
+
targetSec = Math.max(0, args.seconds);
|
|
3197
|
+
} else if (typeof args?.position === 'string') {
|
|
3198
|
+
const parts = args.position.split(':').map(Number);
|
|
3199
|
+
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
|
|
3200
|
+
targetSec = parts[0] * 60 + parts[1];
|
|
3201
|
+
} else if (!isNaN(Number(args.position))) {
|
|
3202
|
+
targetSec = Number(args.position);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
const result = audioPlayer.seek(targetSec);
|
|
3206
|
+
return { status: 'success', action: 'seek', seconds: targetSec, message: result };
|
|
3207
|
+
},
|
|
3208
|
+
|
|
3209
|
+
set_repeat_mode: async (args) => {
|
|
3210
|
+
const mode = String(args?.mode || 'off').toLowerCase().trim();
|
|
3211
|
+
const result = audioPlayer.setRepeatMode(mode);
|
|
3212
|
+
return { status: 'success', action: 'set_repeat', mode: audioPlayer.repeatMode, message: result };
|
|
3213
|
+
},
|
|
3214
|
+
|
|
3215
|
+
set_shuffle_mode: async (args) => {
|
|
3216
|
+
const enabled = args?.enabled !== undefined ? Boolean(args.enabled) : !audioPlayer.isShuffle;
|
|
3217
|
+
const result = audioPlayer.setShuffleMode(enabled);
|
|
3218
|
+
return { status: 'success', action: 'set_shuffle', enabled, message: result };
|
|
3219
|
+
},
|
|
3220
|
+
|
|
3221
|
+
toggle_video_mode: async (args) => {
|
|
3222
|
+
const show = args?.showVideo !== undefined ? Boolean(args.showVideo) : true;
|
|
3223
|
+
return { status: 'success', action: 'toggle_video', isVideoOpen: show, message: `Video display mode: ${show ? 'shown' : 'hidden'}` };
|
|
2468
3224
|
},
|
|
2469
3225
|
|
|
2470
3226
|
add_to_queue: async (args) => {
|
|
2471
|
-
|
|
2472
|
-
|
|
3227
|
+
try {
|
|
3228
|
+
const result = await audioPlayer.addToQueue(args.query);
|
|
3229
|
+
return { status: 'success', message: result };
|
|
3230
|
+
} catch (err) {
|
|
3231
|
+
return { status: 'error', message: err?.message || 'Could not add track to queue' };
|
|
3232
|
+
}
|
|
2473
3233
|
},
|
|
2474
3234
|
|
|
2475
3235
|
remove_from_queue: async (args) => {
|
|
@@ -2492,10 +3252,13 @@ export const toolHandlers = {
|
|
|
2492
3252
|
return { status: 'success', queue };
|
|
2493
3253
|
},
|
|
2494
3254
|
|
|
2495
|
-
search_youtube: async (args) => {
|
|
3255
|
+
search_youtube: async (args = {}) => {
|
|
2496
3256
|
try {
|
|
2497
|
-
const
|
|
2498
|
-
|
|
3257
|
+
const plan = planSearchRequest(args);
|
|
3258
|
+
const query = plan.keywords || plan.originalQuery || args.query || '';
|
|
3259
|
+
if (!query) return { status: 'error', message: 'query parameter is required (use keywords for new callers).' };
|
|
3260
|
+
const results = await audioPlayer.searchYouTube(query);
|
|
3261
|
+
return { status: 'success', query: plan.originalQuery || query, normalizedQuery: plan.keywords || query, results };
|
|
2499
3262
|
} catch (err) {
|
|
2500
3263
|
return { status: 'error', message: err.message };
|
|
2501
3264
|
}
|
|
@@ -3056,5 +3819,23 @@ export const toolsRegistry = {
|
|
|
3056
3819
|
schema: toolsSchema.find(t => t.function?.name === 'search_works')?.function,
|
|
3057
3820
|
handler: toolHandlers.search_works,
|
|
3058
3821
|
},
|
|
3822
|
+
get_user_location: {
|
|
3823
|
+
domain: 'location',
|
|
3824
|
+
isCore: false,
|
|
3825
|
+
schema: toolsSchema.find(t => t.function?.name === 'get_user_location')?.function,
|
|
3826
|
+
handler: toolHandlers.get_user_location,
|
|
3827
|
+
},
|
|
3828
|
+
search_location_map: {
|
|
3829
|
+
domain: 'location',
|
|
3830
|
+
isCore: false,
|
|
3831
|
+
schema: toolsSchema.find(t => t.function?.name === 'search_location_map')?.function,
|
|
3832
|
+
handler: toolHandlers.search_location_map,
|
|
3833
|
+
},
|
|
3834
|
+
get_weather: {
|
|
3835
|
+
domain: 'location',
|
|
3836
|
+
isCore: false,
|
|
3837
|
+
schema: toolsSchema.find(t => t.function?.name === 'get_weather')?.function,
|
|
3838
|
+
handler: toolHandlers.get_weather,
|
|
3839
|
+
},
|
|
3059
3840
|
};
|
|
3060
3841
|
|