kiku-agent 1.0.11 → 1.2.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 +300 -495
- package/package.json +12 -11
- package/public/fonts/GoogleSans-Bold.ttf +0 -0
- package/public/fonts/GoogleSans-Bold.woff2 +0 -0
- package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
- package/public/fonts/GoogleSans-Italic.ttf +0 -0
- package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
- package/public/fonts/GoogleSans-Medium.ttf +0 -0
- package/public/fonts/GoogleSans-Medium.woff2 +0 -0
- package/public/fonts/GoogleSans-Regular.ttf +0 -0
- package/public/fonts/GoogleSans-Regular.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
- package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
- package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
- package/public/fonts/google-sans-official.css +900 -0
- package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
- package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/general/jujingyi_portrait.jpg +0 -0
- package/public/images/jujingyi_portrait.jpg +0 -0
- package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
- package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
- package/public/images/series/detective-samoyeds/poster.jpg +0 -0
- package/public/images/series/everyday/poster.jpg +0 -0
- package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
- package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
- package/public/images/series/in-blossom/poster.jpg +0 -0
- package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
- package/public/images/series/lets-fight/poster-2.jpg +0 -0
- package/public/images/series/lets-fight/poster.jpg +0 -0
- package/public/images/series/love-emergency/poster.jpg +0 -0
- package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
- package/public/images/series/money-bastion/poster.jpg +0 -0
- package/public/images/series/mr-swimmer/poster.jpg +0 -0
- package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
- package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
- package/public/images/series/rebirth-for-you/poster.jpg +0 -0
- package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
- package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
- package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
- package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
- package/public/images/series/veil-of-shadows/poster.jpg +0 -0
- package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
- package/public/images/series/wanhua-sin/poster.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
- package/public/manifest.json +25 -0
- package/public/mcp-logo-128.png +0 -0
- package/public/mcp-logo-64.png +0 -0
- package/public/mcp-logo.svg +16 -0
- package/server/agent/agentCore.js +542 -74
- package/server/agent/appPaths.js +1 -0
- package/server/agent/audioPlayer.js +4 -2
- package/server/agent/citations.js +294 -0
- package/server/agent/clarificationAnalytics.js +477 -0
- package/server/agent/clarifyingQuestions.js +1089 -0
- package/server/agent/cli.js +549 -125
- package/server/agent/commandFeatures.js +1 -0
- package/server/agent/commandRegistry.js +946 -879
- package/server/agent/commands.js +546 -272
- package/server/agent/config.js +581 -412
- package/server/agent/contextCompactor.js +2 -2
- package/server/agent/debate.js +235 -0
- package/server/agent/eventBus.js +3 -0
- package/server/agent/feedbackLearning.js +217 -0
- package/server/agent/llmClient.js +512 -394
- package/server/agent/mcpClient.js +182 -32
- package/server/agent/memory.js +283 -224
- package/server/agent/modelManager.js +79 -39
- package/server/agent/profiles.js +178 -0
- package/server/agent/publicTools.js +22 -0
- package/server/agent/queryDetails.js +68 -8
- package/server/agent/quizData.js +398 -384
- package/server/agent/research.js +121 -79
- package/server/agent/sanitizer.js +104 -13
- package/server/agent/session.js +319 -11
- package/server/agent/subagent.js +8 -3
- package/server/agent/suggestionTui.js +294 -0
- package/server/agent/suggestions.js +69 -3
- package/server/agent/telemetry.js +119 -0
- package/server/agent/terminalImage.js +146 -27
- package/server/agent/theme.js +17 -0
- package/server/agent/toolRouter.js +16 -1
- package/server/agent/tools.js +424 -225
- package/server/agent/ui.js +486 -64
- package/server/agent/version.js +2 -4
- package/server/agent/webSearch.js +111 -11
- package/server/agent/youtubeAuth.js +24 -6
- package/server/apiRouter.js +4222 -0
- package/server/mcp/data/discography.js +25 -16
- package/server/mcp/data/gallery.js +308 -20
- package/server/mcp/data/profile.js +74 -44
- package/server/mcp/data/timeline.js +1 -1
- package/server/mcp/data/works.js +51 -17
- package/server/mcp/dataAdapter.js +5 -3
- package/server/mcp/handler.js +5 -11
- package/server/mcp/prompts.js +86 -0
- package/server/mcp/security.js +18 -15
- package/server/mcp/server.js +7 -2
- package/server/mcp/tools.js +66 -2
- package/server/web-chat/core/browserLauncher.js +18 -0
- package/server/web-chat/core/connection.js +194 -0
- package/server/web-chat/core/conversationContext.js +82 -0
- package/server/web-chat/core/directCatalogEngine.js +196 -0
- package/server/web-chat/core/dramaKnowledge.js +142 -0
- package/server/web-chat/core/errorHandler.js +51 -0
- package/server/web-chat/core/followUpEngine.js +182 -0
- package/server/web-chat/core/futureEventGuard.js +80 -0
- package/server/web-chat/core/memorySnapshot.js +12 -0
- package/server/web-chat/core/security.js +146 -0
- package/server/web-chat/core/toolInspector.js +52 -0
- package/server/web-chat/router.js +120 -0
- package/server/web-chat/routes/chatRoute.js +397 -0
- package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
- package/server/web-chat/routes/clarifyRoute.js +83 -0
- package/server/web-chat/routes/compareRoute.js +79 -0
- package/server/web-chat/routes/exportRoute.js +59 -0
- package/server/web-chat/routes/feedbackRoute.js +57 -0
- package/server/web-chat/routes/newsRoute.js +33 -0
- package/server/web-chat/routes/quizRoute.js +11 -0
- package/server/web-chat/routes/staticRoute.js +103 -0
- package/server/web-chat/routes/statusRoute.js +52 -0
- package/server/web-chat/routes/suggestRoute.js +77 -0
- package/server/web-chat/routes/timelineRoute.js +34 -0
- package/server/web-chat/routes/verifyRoute.js +57 -0
- package/server/web-chat/server.js +116 -0
- package/server/web-chat/ui/htmlBuilder.js +91 -0
- package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
- package/server/web-chat/ui/scripts/client.js +8397 -0
- package/server/web-chat/ui/scripts/kikuDb.js +177 -0
- package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
- package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
- package/server/web-chat/ui/styles/main.css +8568 -0
- package/server/web-chat/ui/templates/body.html +796 -0
- package/server/webChatHtml.js +1 -0
- package/server/webChatServer.js +6 -0
- package/src/data/discographyData.js +892 -866
- package/src/data/galleryData.js +1805 -561
- package/src/data/profileData.js +79 -2
- package/src/data/socialUpdatesData.js +106 -47
- package/src/data/timelineData.js +1 -1
- package/src/data/worksData.js +25 -4
- package/tokens.css +97 -0
- package/server/agent/updateManager.js +0 -456
package/server/agent/tools.js
CHANGED
|
@@ -8,10 +8,10 @@ import { galleryData } from '../../src/data/galleryData.js';
|
|
|
8
8
|
import { awardsData } from '../../src/data/awardsData.js';
|
|
9
9
|
import { timelineData } from '../../src/data/timelineData.js';
|
|
10
10
|
import { searchWeb, fetchWebpage, fetchJson, screenshotWebpage, webExtract } from './webSearch.js';
|
|
11
|
-
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
11
|
+
import { readFile, readdir, stat, realpath } from 'node:fs/promises';
|
|
12
12
|
import { join, relative, extname, resolve } from 'node:path';
|
|
13
13
|
import { audioPlayer } from './audioPlayer.js';
|
|
14
|
-
import { sanitizeToolArgs } from './sanitizer.js';
|
|
14
|
+
import { sanitizeToolArgs, sanitizeUrl } from './sanitizer.js';
|
|
15
15
|
import { getGpuMetrics } from './gpuMetrics.js';
|
|
16
16
|
|
|
17
17
|
export function buildYouTubeSearchUrl(query) {
|
|
@@ -30,16 +30,48 @@ export function buildQqMusicSearchUrl(query) {
|
|
|
30
30
|
return `https://y.qq.com/n/ryqq/search?w=${encodeURIComponent(query)}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function buildYoukuSearchUrl(query) {
|
|
34
|
-
return `https://so.youku.com/search_video/q_${encodeURIComponent(query)}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function formatCatalogTitle(primary, secondary) {
|
|
38
|
-
const first = String(primary || secondary || '').trim();
|
|
39
|
-
const second = String(secondary || '').trim();
|
|
40
|
-
if (!second || first.toLocaleLowerCase().includes(second.toLocaleLowerCase())) return first;
|
|
41
|
-
return `${first} (${second})`;
|
|
42
|
-
}
|
|
33
|
+
export function buildYoukuSearchUrl(query) {
|
|
34
|
+
return `https://so.youku.com/search_video/q_${encodeURIComponent(query)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatCatalogTitle(primary, secondary) {
|
|
38
|
+
const first = String(primary || secondary || '').trim();
|
|
39
|
+
const second = String(secondary || '').trim();
|
|
40
|
+
if (!second || first.toLocaleLowerCase().includes(second.toLocaleLowerCase())) return first;
|
|
41
|
+
return `${first} (${second})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createCatalogMatcher(query) {
|
|
45
|
+
if (!query) return () => true;
|
|
46
|
+
const raw = String(query).toLowerCase().trim();
|
|
47
|
+
if (!raw) return () => true;
|
|
48
|
+
|
|
49
|
+
const stopWords = new Set([
|
|
50
|
+
'the', 'from', 'what', 'are', 'is', 'about', 'drama', 'series', 'ost', 'song', 'songs', 'soundtrack', 'music', 'single', 'album', 'stage',
|
|
51
|
+
'brand', 'brands', 'ambassador', 'endorsement', 'commercial', 'role', 'roles', 'character', 'characters', 'award', 'awards', 'category', 'video', 'mv', 'clip',
|
|
52
|
+
'ju', 'jingyi', 'jujingyi', 'kiku', 'kikuuu', 'จวีจิ้งอี', 'จวี', 'จิ้งอี', 'เสี่ยวจวี', 'เสี่ยว', '鞠婧祎', '小鞠', '鞠',
|
|
53
|
+
'เพลง', 'เพลงของ', 'ซีรีส์', 'ซีรีส์ของ', 'ละคร', 'เรื่อง', 'ผลงาน', 'อัลบั้ม', 'คอนเสิร์ต', 'บทบาท', 'ตัวละคร', 'แบรนด์', 'รางวัล', 'ผลงานล่าสุด', 'พรีเซนเตอร์'
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const allTokens = raw.split(/[^\p{L}\p{M}\p{N}]+/u).filter(t => t.length >= 2);
|
|
57
|
+
|
|
58
|
+
const deDated = raw.replace(/ใหม่ล่าสุด|ล่าสุด|最新|latest|newest|recent/gi, ' ');
|
|
59
|
+
const datedTokens = deDated.split(/[^\p{L}\p{M}\p{N}]+/u).filter(t => t.length >= 2);
|
|
60
|
+
const coreTokens = datedTokens.filter(t => !stopWords.has(t));
|
|
61
|
+
|
|
62
|
+
if (coreTokens.length === 0) return () => true;
|
|
63
|
+
const effectiveTokens = coreTokens;
|
|
64
|
+
|
|
65
|
+
return (...fields) => {
|
|
66
|
+
const haystack = fields.filter(Boolean).join(' ').toLowerCase();
|
|
67
|
+
if (haystack.includes(raw)) return true;
|
|
68
|
+
if (effectiveTokens.length > 0) {
|
|
69
|
+
const matchCount = effectiveTokens.filter(term => haystack.includes(term)).length;
|
|
70
|
+
return matchCount > 0 && (matchCount / effectiveTokens.length >= 0.5);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
43
75
|
|
|
44
76
|
export class ToolCache {
|
|
45
77
|
constructor(maxSize = 300, defaultTtlMs = 600_000) {
|
|
@@ -100,6 +132,17 @@ const WEB_CACHE_TOOLS = new Set([
|
|
|
100
132
|
'fetch_json',
|
|
101
133
|
]);
|
|
102
134
|
|
|
135
|
+
|
|
136
|
+
const LIVE_DATA_TOOLS = new Set([
|
|
137
|
+
'get_social_feed',
|
|
138
|
+
'get_gallery_photos',
|
|
139
|
+
'get_awards',
|
|
140
|
+
'get_career_timeline',
|
|
141
|
+
'get_brand_endorsements',
|
|
142
|
+
'get_music_videos',
|
|
143
|
+
'get_media_links',
|
|
144
|
+
]);
|
|
145
|
+
|
|
103
146
|
const CACHED_DISCOGRAPHY = (() => {
|
|
104
147
|
const map = new Map();
|
|
105
148
|
const allTracks = [
|
|
@@ -183,7 +226,7 @@ export const toolsSchema = [
|
|
|
183
226
|
type: "function",
|
|
184
227
|
function: {
|
|
185
228
|
name: "search_works",
|
|
186
|
-
description: "Search Ju Jingyi's filmography (TV dramas, web series, movies, and
|
|
229
|
+
description: "Search Ju Jingyi's filmography (TV dramas, web series, movies, and upcoming projects). Returns grounded role context, synopsis, highlights, catalog platform, and trailer search links; use these fields instead of inferring plot or acting claims.",
|
|
187
230
|
parameters: {
|
|
188
231
|
type: "object",
|
|
189
232
|
properties: {
|
|
@@ -309,11 +352,11 @@ export const toolsSchema = [
|
|
|
309
352
|
description: "Query awards and honors received by Ju Jingyi across music, television, and cultural influence.",
|
|
310
353
|
parameters: {
|
|
311
354
|
type: "object",
|
|
312
|
-
properties: {
|
|
313
|
-
category: { type: "string", description: "Category (Acting, Music, Honor)" },
|
|
314
|
-
year: { type: "number", description: "Year filter" },
|
|
315
|
-
query: { type: "string", description: "Award title, ceremony, or category keyword" },
|
|
316
|
-
limit: { type: "number", description: "Maximum results from 1 to 20" }
|
|
355
|
+
properties: {
|
|
356
|
+
category: { type: "string", description: "Category (Acting, Music, Honor)" },
|
|
357
|
+
year: { type: "number", description: "Year filter" },
|
|
358
|
+
query: { type: "string", description: "Award title, ceremony, or category keyword" },
|
|
359
|
+
limit: { type: "number", description: "Maximum results from 1 to 20" }
|
|
317
360
|
},
|
|
318
361
|
required: []
|
|
319
362
|
}
|
|
@@ -341,11 +384,11 @@ export const toolsSchema = [
|
|
|
341
384
|
description: "Search visual photo gallery of Ju Jingyi (red carpet, photoshoot, drama stills).",
|
|
342
385
|
parameters: {
|
|
343
386
|
type: "object",
|
|
344
|
-
properties: {
|
|
345
|
-
category: { type: "string", description: "Category (fashion-magazine, red-carpet, drama-stills)" },
|
|
346
|
-
query: { type: "string", description: "Photo title or category keyword" },
|
|
347
|
-
year: { type: "number", description: "Year filter" },
|
|
348
|
-
limit: { type: "number", description: "Number of photos from 1 to 20" }
|
|
387
|
+
properties: {
|
|
388
|
+
category: { type: "string", description: "Category (fashion-magazine, red-carpet, drama-stills)" },
|
|
389
|
+
query: { type: "string", description: "Photo title or category keyword" },
|
|
390
|
+
year: { type: "number", description: "Year filter" },
|
|
391
|
+
limit: { type: "number", description: "Number of photos from 1 to 20" }
|
|
349
392
|
},
|
|
350
393
|
required: []
|
|
351
394
|
}
|
|
@@ -707,7 +750,8 @@ export const toolsSchema = [
|
|
|
707
750
|
type: "object",
|
|
708
751
|
properties: {
|
|
709
752
|
topic: { type: "string", description: "Optional topic or category focus (e.g. 'drama', 'music', 'fashion', '2026')." },
|
|
710
|
-
limit: { type: "number", description: "Number of suggestions to generate (default: 5)." }
|
|
753
|
+
limit: { type: "number", description: "Number of suggestions to generate (default: 5)." },
|
|
754
|
+
lang: { type: "string", description: "UI language for the questions: th, zh, en, ja, ko (default: en)." }
|
|
711
755
|
}
|
|
712
756
|
}
|
|
713
757
|
}
|
|
@@ -764,6 +808,50 @@ export const toolsSchema = [
|
|
|
764
808
|
}
|
|
765
809
|
];
|
|
766
810
|
|
|
811
|
+
|
|
812
|
+
const TOOL_DEFAULT_TIMEOUTS = {
|
|
813
|
+
run_subagent: 180_000,
|
|
814
|
+
run_deep_research: 300_000,
|
|
815
|
+
verify_claim: 300_000,
|
|
816
|
+
suggest_topics: 120_000,
|
|
817
|
+
web_extract: 90_000,
|
|
818
|
+
screenshot_webpage: 90_000,
|
|
819
|
+
search_youtube: 60_000,
|
|
820
|
+
play_music: 60_000,
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
async function ensureLlmAvailable(client) {
|
|
824
|
+
if (!client || typeof client.checkConnection !== 'function') return null;
|
|
825
|
+
try {
|
|
826
|
+
const status = await client.checkConnection();
|
|
827
|
+
if (status && status.connected === false) {
|
|
828
|
+
const provider = client.providerName || client.provider || 'LLM';
|
|
829
|
+
return {
|
|
830
|
+
status: 'error',
|
|
831
|
+
message: `AI provider offline (${provider}): ${status.error || 'connection failed'}. Start LM Studio or configure a cloud provider key, or answer from local catalog tools instead.`
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
} catch {
|
|
835
|
+
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export const RUMORED_OR_EXTERNAL_PROJECTS = [
|
|
841
|
+
{
|
|
842
|
+
keywords: ['七星彩', 'qi xiang cai', 'qi xing cai', 'ชีซิงไฉ่', 'seven star color'],
|
|
843
|
+
titleCn: '七星彩',
|
|
844
|
+
titleEn: 'Qi Xing Cai (Seven Star Color)',
|
|
845
|
+
status: 'rumored_casting',
|
|
846
|
+
producer: 'Youku (优酷) x Stellar Media (恒星引力传媒)',
|
|
847
|
+
originalNovel: '《七星彩》 by 明月珰 (Mingyue Dang)',
|
|
848
|
+
leads: '纪澄 (Ji Cheng) & 沈彻 (Shen Che)',
|
|
849
|
+
details: 'โปรเจกต์ซีรีส์จีนโบราณฟอร์มยักษ์ของ Youku ร่วมกับค่ายเหิงซิงอิ่นลี่ (ผู้สร้างของรักของข้า Love Between Fairy and Devil) ดัดแปลงจากนิยายของหมิงเยว่ตัง เล่าเรื่องราวของจี้เฉิง บุตรสาวตระกูลพ่อค้าที่ใช้ปฏิภาณไหวพริบเอาตัวรอดจนได้พบกับแม่ทัพเสิ่นเช่อ',
|
|
850
|
+
buzzReason: 'เหตุผลที่ชาวเน็ตให้ความสนใจอย่างมากคือ กระแสข่าวลือแคสติ้งนักแสดงนำ (Casting Rumors / 选角瓜) โดยมีการลือชื่อนักแสดงหญิงแถวหน้าหลายคนรวมถึง จวีจิ้งอี, หลูอวี้เสี่ยว ฯลฯ แต่ปัจจุบันยังไม่มีการประกาศยืนยันนักแสดงทางการจากทางค่ายหรือสตูดิโอ',
|
|
851
|
+
disambiguation: 'คนละผลงานกับซีรีส์ 《千香》 (The Eternal Fragrance / พันกลิ่นบุปผา) ที่จวีจิ้งอีนำแสดงร่วมกับ ซ่งเวยหลง'
|
|
852
|
+
}
|
|
853
|
+
];
|
|
854
|
+
|
|
767
855
|
export const toolHandlers = {
|
|
768
856
|
get_profile_and_fandom: async () => {
|
|
769
857
|
const d = profileData.details || {};
|
|
@@ -786,6 +874,8 @@ export const toolHandlers = {
|
|
|
786
874
|
hobbies: d.hobbies,
|
|
787
875
|
favoriteFoods: d.favoriteFoods,
|
|
788
876
|
talentsSummary: d.talentsSummary,
|
|
877
|
+
petsSummary: d.petsSummary,
|
|
878
|
+
cats: profileData.cats || [],
|
|
789
879
|
fandomName: d.fandomName || "มี่จวี (Miju / 蜜橘)",
|
|
790
880
|
fandomColor: d.fandomColorName || "ฟ้าคริสตัล & ชมพูซากุระ",
|
|
791
881
|
slogan: profileData.motto || "保持热爱,奔赴山海",
|
|
@@ -822,24 +912,29 @@ export const toolHandlers = {
|
|
|
822
912
|
if (year) results = results.filter(w => w.year === Number(year));
|
|
823
913
|
if (category && category !== 'all') results = results.filter(w => w.category === category);
|
|
824
914
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (query && !isGenericArtistQuery) {
|
|
829
|
-
const q = query.toLowerCase().trim();
|
|
830
|
-
results = results.filter(w =>
|
|
831
|
-
(w.titleTh && w.titleTh.toLowerCase().includes(q)) ||
|
|
832
|
-
(w.titleEn && w.titleEn.toLowerCase().includes(q)) ||
|
|
833
|
-
(w.titleCn && w.titleCn.includes(query)) ||
|
|
834
|
-
(w.role && w.role.toLowerCase().includes(q)) ||
|
|
835
|
-
(w.roleCn && w.roleCn.includes(query))
|
|
836
|
-
);
|
|
915
|
+
if (query) {
|
|
916
|
+
const matcher = createCatalogMatcher(query);
|
|
917
|
+
results = results.filter(w => matcher(w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis));
|
|
837
918
|
}
|
|
838
919
|
|
|
839
920
|
if (featuredOnly && results.some(w => !!w.featured)) {
|
|
840
921
|
results = results.filter(w => !!w.featured);
|
|
841
922
|
}
|
|
842
923
|
|
|
924
|
+
if (results.length === 0 && query) {
|
|
925
|
+
const qLower = String(query).toLowerCase().trim();
|
|
926
|
+
const rumored = RUMORED_OR_EXTERNAL_PROJECTS.find(p => p.keywords.some(k => qLower.includes(k)));
|
|
927
|
+
if (rumored) {
|
|
928
|
+
return {
|
|
929
|
+
status: "rumored_project_info",
|
|
930
|
+
count: 0,
|
|
931
|
+
works: [],
|
|
932
|
+
rumoredProject: rumored,
|
|
933
|
+
message: `"${rumored.titleCn}" (${rumored.titleEn}) เป็นโปรเจกต์ข่าวลือแคสติ้ง/ยังไม่ยืนยัน: ${rumored.details}. จุดที่ชาวเน็ตสนใจ: ${rumored.buzzReason}. ข้อควรระวัง: ${rumored.disambiguation}.`
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
843
938
|
results.sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
844
939
|
|
|
845
940
|
return {
|
|
@@ -855,17 +950,21 @@ export const toolHandlers = {
|
|
|
855
950
|
titleEn: w.titleEn,
|
|
856
951
|
year: w.year,
|
|
857
952
|
category: w.category,
|
|
858
|
-
role: w.role,
|
|
859
|
-
roleCn: w.roleCn,
|
|
953
|
+
role: w.role,
|
|
954
|
+
roleCn: w.roleCn,
|
|
955
|
+
synopsis: w.synopsis || null,
|
|
956
|
+
highlights: (w.highlights || []).slice(0, 3),
|
|
860
957
|
episodes: w.episodes,
|
|
861
958
|
rating: w.rating,
|
|
862
959
|
platform: w.streamPlatform,
|
|
863
960
|
streamUrl: w.streamUrl || null,
|
|
864
961
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`${titleSearch} Trailer Official`),
|
|
962
|
+
youtubeTrailerIsDirectLink: false,
|
|
865
963
|
youkuSearchUrl: buildYoukuSearchUrl(w.titleCn || w.titleEn),
|
|
866
964
|
themeSong: w.themeSong ? {
|
|
867
|
-
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
965
|
+
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
868
966
|
youtubeUrl: themeSongYtId ? buildYouTubeWatchUrl(themeSongYtId) : null,
|
|
967
|
+
isDirectLink: Boolean(themeSongYtId),
|
|
869
968
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.themeSong.titleCn || w.themeSong.titleTh} OST`)
|
|
870
969
|
} : null
|
|
871
970
|
};
|
|
@@ -876,12 +975,20 @@ export const toolHandlers = {
|
|
|
876
975
|
get_work_detail: async ({ workIdOrTitle }) => {
|
|
877
976
|
if (!workIdOrTitle) return { status: "error", message: "workIdOrTitle parameter is required." };
|
|
878
977
|
const q = workIdOrTitle.toLowerCase().trim();
|
|
879
|
-
|
|
880
|
-
|
|
978
|
+
|
|
979
|
+
const normId = (value) => String(value || '').toLowerCase().trim().replace(/[_\s]+/g, '-');
|
|
980
|
+
const qId = normId(workIdOrTitle);
|
|
981
|
+
let work = worksData.find(w =>
|
|
982
|
+
normId(w.id) === qId ||
|
|
881
983
|
w.titleTh.toLowerCase().includes(q) ||
|
|
882
984
|
(w.titleEn && w.titleEn.toLowerCase().includes(q)) ||
|
|
883
985
|
(w.titleCn && w.titleCn.includes(workIdOrTitle))
|
|
884
986
|
);
|
|
987
|
+
if (!work) {
|
|
988
|
+
|
|
989
|
+
const matcher = createCatalogMatcher(workIdOrTitle);
|
|
990
|
+
work = worksData.find(w => matcher(w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn));
|
|
991
|
+
}
|
|
885
992
|
|
|
886
993
|
if (!work) {
|
|
887
994
|
return { status: "not_found", message: `No work found for "${workIdOrTitle}".` };
|
|
@@ -899,19 +1006,22 @@ export const toolHandlers = {
|
|
|
899
1006
|
titleEn: work.titleEn,
|
|
900
1007
|
year: work.year,
|
|
901
1008
|
category: work.category,
|
|
902
|
-
role: work.role,
|
|
1009
|
+
role: work.role,
|
|
1010
|
+
roleCn: work.roleCn,
|
|
903
1011
|
episodes: work.episodes,
|
|
904
1012
|
rating: work.rating,
|
|
905
1013
|
platform: work.streamPlatform,
|
|
906
1014
|
streamUrl: work.streamUrl || null,
|
|
907
1015
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`${titleSearch} Trailer Official`),
|
|
1016
|
+
youtubeTrailerIsDirectLink: false,
|
|
908
1017
|
youkuSearchUrl: buildYoukuSearchUrl(work.titleCn || work.titleEn),
|
|
909
1018
|
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${work.titleCn}`),
|
|
910
|
-
synopsis: work.synopsis
|
|
1019
|
+
synopsis: work.synopsis || null,
|
|
911
1020
|
highlights: (work.highlights || []).slice(0, 3),
|
|
912
1021
|
themeSong: work.themeSong ? {
|
|
913
|
-
title: formatCatalogTitle(work.themeSong.titleTh, work.themeSong.titleCn),
|
|
1022
|
+
title: formatCatalogTitle(work.themeSong.titleTh, work.themeSong.titleCn),
|
|
914
1023
|
youtubeUrl: themeSongYtId ? buildYouTubeWatchUrl(themeSongYtId) : null,
|
|
1024
|
+
isDirectLink: Boolean(themeSongYtId),
|
|
915
1025
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${work.themeSong.titleCn || work.themeSong.titleTh} OST`)
|
|
916
1026
|
} : null
|
|
917
1027
|
}
|
|
@@ -920,7 +1030,7 @@ export const toolHandlers = {
|
|
|
920
1030
|
|
|
921
1031
|
get_character_roles: async ({ query } = {}) => {
|
|
922
1032
|
const roles = worksData.map(w => ({
|
|
923
|
-
workTitle: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
1033
|
+
workTitle: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
924
1034
|
year: w.year,
|
|
925
1035
|
role: w.role,
|
|
926
1036
|
roleCn: w.roleCn,
|
|
@@ -928,16 +1038,9 @@ export const toolHandlers = {
|
|
|
928
1038
|
}));
|
|
929
1039
|
|
|
930
1040
|
let filtered = roles;
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
if (query && !isGeneric) {
|
|
935
|
-
const q = query.toLowerCase().trim();
|
|
936
|
-
filtered = roles.filter(r =>
|
|
937
|
-
(r.role && r.role.toLowerCase().includes(q)) ||
|
|
938
|
-
(r.roleCn && r.roleCn.includes(query)) ||
|
|
939
|
-
(r.workTitle && r.workTitle.toLowerCase().includes(q))
|
|
940
|
-
);
|
|
1041
|
+
if (query) {
|
|
1042
|
+
const matcher = createCatalogMatcher(query);
|
|
1043
|
+
filtered = roles.filter(r => matcher(r.role, r.roleCn, r.workTitle));
|
|
941
1044
|
}
|
|
942
1045
|
|
|
943
1046
|
filtered.sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
@@ -956,17 +1059,19 @@ export const toolHandlers = {
|
|
|
956
1059
|
if (type === 'ost') list = list.filter(s => s.work && s.work !== "Solo Single");
|
|
957
1060
|
if (type === 'single') list = list.filter(s => !s.work || s.work === "Solo Single");
|
|
958
1061
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1062
|
+
if (query) {
|
|
1063
|
+
const qLower = String(query).toLowerCase().trim();
|
|
1064
|
+
const rumored = RUMORED_OR_EXTERNAL_PROJECTS.find(p => p.keywords.some(k => qLower.includes(k)));
|
|
1065
|
+
if (rumored) {
|
|
1066
|
+
return {
|
|
1067
|
+
status: "not_found",
|
|
1068
|
+
count: 0,
|
|
1069
|
+
songs: [],
|
|
1070
|
+
message: `"${rumored.titleCn}" (${rumored.titleEn}) เป็นโปรเจกต์ซีรีส์ที่มีข่าวลือแคสติ้ง ไม่ใช่เพลงหรือ OST ของจวีจิ้งอีในคลังผลงาน และคนละเรื่องกับเพลง 《千香》 (The Eternal Fragrance).`
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const matcher = createCatalogMatcher(query);
|
|
1074
|
+
list = list.filter(s => matcher(s.titleTh, s.titleCn, s.titleEn, s.work, s.role, s.description));
|
|
970
1075
|
}
|
|
971
1076
|
|
|
972
1077
|
return {
|
|
@@ -975,12 +1080,13 @@ export const toolHandlers = {
|
|
|
975
1080
|
count: list.length,
|
|
976
1081
|
songs: list.slice(0, 8).map(s => ({
|
|
977
1082
|
id: s.id,
|
|
978
|
-
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
1083
|
+
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
979
1084
|
titleEn: s.titleEn,
|
|
980
1085
|
year: s.year,
|
|
981
1086
|
type: s.work,
|
|
982
1087
|
duration: s.duration,
|
|
983
1088
|
youtubeUrl: s.youtubeUrl || (s.youtubeId ? buildYouTubeWatchUrl(s.youtubeId) : null),
|
|
1089
|
+
isDirectLink: Boolean(s.youtubeUrl || s.youtubeId),
|
|
984
1090
|
youtubeSearchUrl: s.youtubeSearchUrl || buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${s.titleCn || s.titleTh}`),
|
|
985
1091
|
qqMusicSearchUrl: s.qqMusicSearchUrl || buildQqMusicSearchUrl(`鞠婧祎 ${s.titleCn || s.titleTh}`),
|
|
986
1092
|
bilibiliSearchUrl: s.bilibiliSearchUrl || buildBilibiliSearchUrl(`鞠婧祎 ${s.titleCn || s.titleTh}`)
|
|
@@ -1004,18 +1110,9 @@ export const toolHandlers = {
|
|
|
1004
1110
|
});
|
|
1005
1111
|
}
|
|
1006
1112
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (query && !isGeneric) {
|
|
1011
|
-
const q = query.toLowerCase().trim();
|
|
1012
|
-
results = results.filter(m =>
|
|
1013
|
-
(m.titleTh && m.titleTh.toLowerCase().includes(q)) ||
|
|
1014
|
-
(m.titleCn && m.titleCn.includes(query)) ||
|
|
1015
|
-
(m.titlePinyin && m.titlePinyin.toLowerCase().includes(q)) ||
|
|
1016
|
-
(m.dramaName && m.dramaName.toLowerCase().includes(q)) ||
|
|
1017
|
-
(m.description && m.description.toLowerCase().includes(q))
|
|
1018
|
-
);
|
|
1113
|
+
if (query) {
|
|
1114
|
+
const matcher = createCatalogMatcher(query);
|
|
1115
|
+
results = results.filter(m => matcher(m.titleTh, m.titleCn, m.titlePinyin, m.dramaName, m.description));
|
|
1019
1116
|
}
|
|
1020
1117
|
|
|
1021
1118
|
return {
|
|
@@ -1026,7 +1123,7 @@ export const toolHandlers = {
|
|
|
1026
1123
|
const searchTitle = `Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh} ${m.category === 'Stage-Performance' ? 'Stage Live' : 'Official MV'}`;
|
|
1027
1124
|
return {
|
|
1028
1125
|
id: m.id,
|
|
1029
|
-
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1126
|
+
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1030
1127
|
category: m.category,
|
|
1031
1128
|
year: m.releaseYear || m.year,
|
|
1032
1129
|
drama: m.dramaName || null,
|
|
@@ -1034,6 +1131,7 @@ export const toolHandlers = {
|
|
|
1034
1131
|
description: m.description || null,
|
|
1035
1132
|
isFeatured: !!m.isFeatured,
|
|
1036
1133
|
youtubeUrl: ytId ? buildYouTubeWatchUrl(ytId) : null,
|
|
1134
|
+
isDirectLink: Boolean(ytId),
|
|
1037
1135
|
youtubeSearchUrl: buildYouTubeSearchUrl(searchTitle),
|
|
1038
1136
|
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${m.titleCn || m.titleTh}`)
|
|
1039
1137
|
};
|
|
@@ -1056,7 +1154,7 @@ export const toolHandlers = {
|
|
|
1056
1154
|
(m.dramaName && m.dramaName.toLowerCase().includes(q))
|
|
1057
1155
|
).map(m => ({
|
|
1058
1156
|
type: m.category || 'MV',
|
|
1059
|
-
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1157
|
+
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1060
1158
|
year: m.releaseYear || m.year,
|
|
1061
1159
|
duration: m.duration,
|
|
1062
1160
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
@@ -1071,7 +1169,7 @@ export const toolHandlers = {
|
|
|
1071
1169
|
(s.work && s.work.toLowerCase().includes(q))
|
|
1072
1170
|
).map(s => ({
|
|
1073
1171
|
type: s.work || 'Song',
|
|
1074
|
-
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
1172
|
+
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
1075
1173
|
year: s.year,
|
|
1076
1174
|
duration: s.duration,
|
|
1077
1175
|
youtubeUrl: s.youtubeUrl || (s.youtubeId ? buildYouTubeWatchUrl(s.youtubeId) : null),
|
|
@@ -1086,14 +1184,14 @@ export const toolHandlers = {
|
|
|
1086
1184
|
(w.titleEn && w.titleEn.toLowerCase().includes(q))
|
|
1087
1185
|
).map(w => ({
|
|
1088
1186
|
type: 'Drama / Series',
|
|
1089
|
-
title: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
1187
|
+
title: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
1090
1188
|
year: w.year,
|
|
1091
1189
|
platform: w.streamPlatform,
|
|
1092
1190
|
streamUrl: w.streamUrl || null,
|
|
1093
1191
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.titleCn || w.titleEn} Trailer Official`),
|
|
1094
1192
|
youkuSearchUrl: buildYoukuSearchUrl(w.titleCn || w.titleEn),
|
|
1095
1193
|
themeSong: w.themeSong ? {
|
|
1096
|
-
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
1194
|
+
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
1097
1195
|
youtubeUrl: w.themeSong.youtubeId ? buildYouTubeWatchUrl(w.themeSong.youtubeId) : null,
|
|
1098
1196
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.themeSong.titleCn || w.themeSong.titleTh} OST`)
|
|
1099
1197
|
} : null
|
|
@@ -1122,24 +1220,13 @@ export const toolHandlers = {
|
|
|
1122
1220
|
if (year) list = list.filter(b => b.year === Number(year));
|
|
1123
1221
|
if (category) list = list.filter(b => (b.category && b.category.toLowerCase().includes(category.toLowerCase())) || (b.tier && b.tier.toLowerCase().includes(category.toLowerCase())));
|
|
1124
1222
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
if (query && !isGeneric) {
|
|
1129
|
-
const q = query.toLowerCase().trim();
|
|
1130
|
-
const numYear = Number(q);
|
|
1223
|
+
if (query) {
|
|
1224
|
+
const numYear = Number(String(query).trim());
|
|
1131
1225
|
if (!isNaN(numYear) && numYear >= 2013 && numYear <= 2030) {
|
|
1132
1226
|
list = list.filter(b => b.year === numYear);
|
|
1133
1227
|
} else {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
(b.brandNameCn && b.brandNameCn.includes(query)) ||
|
|
1137
|
-
(b.title && b.title.toLowerCase().includes(q)) ||
|
|
1138
|
-
(b.category && b.category.toLowerCase().includes(q)) ||
|
|
1139
|
-
(b.highlight && b.highlight.toLowerCase().includes(q)) ||
|
|
1140
|
-
(b.tier && b.tier.toLowerCase().includes(q)) ||
|
|
1141
|
-
(b.name && b.name.toLowerCase().includes(q))
|
|
1142
|
-
);
|
|
1228
|
+
const matcher = createCatalogMatcher(query);
|
|
1229
|
+
list = list.filter(b => matcher(b.brandNameEn, b.brandNameCn, b.name, b.title, b.category, b.highlight, b.tier));
|
|
1143
1230
|
}
|
|
1144
1231
|
}
|
|
1145
1232
|
|
|
@@ -1170,39 +1257,61 @@ export const toolHandlers = {
|
|
|
1170
1257
|
count: eras.length,
|
|
1171
1258
|
milestones: eras.slice(0, 6).map(e => ({
|
|
1172
1259
|
period: e.period || e.year,
|
|
1173
|
-
title: formatCatalogTitle(e.title, e.titleCn),
|
|
1260
|
+
title: formatCatalogTitle(e.title, e.titleCn),
|
|
1174
1261
|
summary: e.summary?.slice(0, 150),
|
|
1175
1262
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${e.titleCn || e.title}`)
|
|
1176
1263
|
}))
|
|
1177
1264
|
};
|
|
1178
1265
|
},
|
|
1179
1266
|
|
|
1180
|
-
get_awards: async ({ category, year, query, limit = 8 } = {}) => {
|
|
1181
|
-
let list = Array.isArray(awardsData) ? awardsData : (awardsData.awards || []);
|
|
1182
|
-
if (year) list = list.filter(a => a.year === Number(year));
|
|
1183
|
-
if (category && category !== 'all') list = list.filter(a => a.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1267
|
+
get_awards: async ({ category, year, query, limit = 8 } = {}) => {
|
|
1268
|
+
let list = Array.isArray(awardsData) ? awardsData : (awardsData.awards || []);
|
|
1269
|
+
if (year) list = list.filter(a => a.year === Number(year));
|
|
1270
|
+
if (category && category !== 'all') list = list.filter(a => a.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1271
|
+
|
|
1272
|
+
let usedQueryFilter = false;
|
|
1273
|
+
if (query) {
|
|
1274
|
+
const q = String(query).toLowerCase().trim();
|
|
1275
|
+
|
|
1276
|
+
const generic = /(ju jingyi|jujingyi|จวีจิ้งอี|จิ้งอี|จวี|鞠婧祎|小鞠|kiku|kikuuu|ju jing yi|蜜橘)/i.test(q);
|
|
1277
|
+
if (!generic) {
|
|
1278
|
+
const filtered = list.filter(a => [a.titleTh, a.titleCn, a.titleEn, a.ceremony, a.category, a.work].filter(Boolean).join(' ').toLowerCase().includes(q));
|
|
1279
|
+
if (filtered.length > 0) {
|
|
1280
|
+
list = filtered;
|
|
1281
|
+
usedQueryFilter = true;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
list.sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
1288
|
+
const safeLimit = Math.min(Math.max(Number(limit) || 8, 1), 20);
|
|
1289
|
+
const awards = list.slice(0, safeLimit).map((a, index) => ({
|
|
1290
|
+
id: a.id || `award-${a.year || 'unknown'}-${index + 1}`,
|
|
1291
|
+
year: a.year,
|
|
1292
|
+
title: `${a.titleTh || a.titleEn || a.titleCn || 'Award'}${a.titleCn && a.titleCn !== a.titleTh ? ` (${a.titleCn})` : ''}`,
|
|
1293
|
+
ceremony: a.ceremony,
|
|
1294
|
+
category: a.category,
|
|
1295
|
+
work: a.work || null,
|
|
1296
|
+
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${a.titleCn || a.ceremony}`)
|
|
1297
|
+
}));
|
|
1298
|
+
|
|
1299
|
+
const result = {
|
|
1300
|
+
status: awards.length > 0 ? "success" : "not_found",
|
|
1301
|
+
count: awards.length,
|
|
1302
|
+
awards
|
|
1201
1303
|
};
|
|
1304
|
+
if (usedQueryFilter === false && query) {
|
|
1305
|
+
result.message = `Showing all ${awards.length} awards (broad query " ${query} " applied to full list).`;
|
|
1306
|
+
}
|
|
1307
|
+
return result;
|
|
1202
1308
|
},
|
|
1203
1309
|
|
|
1204
1310
|
get_social_feed: async ({ platform, limit = 4 } = {}) => {
|
|
1205
|
-
|
|
1311
|
+
|
|
1312
|
+
const douyinCards = socialUpdatesData?.douyin?.cards || [];
|
|
1313
|
+
const weiboCards = socialUpdatesData?.weibo?.cards || [];
|
|
1314
|
+
let posts = [...douyinCards, ...weiboCards];
|
|
1206
1315
|
if (platform && platform !== 'all') posts = posts.filter(p => p.platform?.toLowerCase() === platform.toLowerCase());
|
|
1207
1316
|
|
|
1208
1317
|
return {
|
|
@@ -1211,46 +1320,55 @@ export const toolHandlers = {
|
|
|
1211
1320
|
posts: posts.slice(0, Number(limit) || 4).map(p => ({
|
|
1212
1321
|
platform: p.platform,
|
|
1213
1322
|
date: p.date,
|
|
1214
|
-
content: p.content
|
|
1323
|
+
content: String(p.content || p.captionTh || p.captionCn || p.title || p.titleCn || '').slice(0, 120) + '...',
|
|
1215
1324
|
likes: p.likes
|
|
1216
1325
|
}))
|
|
1217
1326
|
};
|
|
1218
1327
|
},
|
|
1219
1328
|
|
|
1220
|
-
get_gallery_photos: async ({ category, query, year, limit = 4 } = {}) => {
|
|
1221
|
-
let photos = Array.isArray(galleryData) ? galleryData : (galleryData.photos || []);
|
|
1222
|
-
if (category && category !== 'all') photos = photos.filter(p => p.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1329
|
+
get_gallery_photos: async ({ category, query, year, limit = 4 } = {}) => {
|
|
1330
|
+
let photos = Array.isArray(galleryData) ? galleryData : (galleryData.photos || []);
|
|
1331
|
+
if (category && category !== 'all') photos = photos.filter(p => p.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1332
|
+
let usedQueryFilter = false;
|
|
1333
|
+
if (query) {
|
|
1334
|
+
const q = String(query).toLowerCase().trim();
|
|
1335
|
+
|
|
1336
|
+
const generic = /(ju jingyi|jujingyi|จวีจิ้งอี|จิ้งอี|จวี|鞠婧祎|小鞠|kiku|kikuuu|ju jing yi|蜜橘)/i.test(q);
|
|
1337
|
+
if (!generic) {
|
|
1338
|
+
const filtered = photos.filter(p => [p.titleTh, p.titleCn, p.titleEn, p.category, p.description].filter(Boolean).join(' ').toLowerCase().includes(q));
|
|
1339
|
+
if (filtered.length > 0) {
|
|
1340
|
+
photos = filtered;
|
|
1341
|
+
usedQueryFilter = true;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (year) photos = photos.filter(p => Number(p.era || p.year) === Number(year));
|
|
1347
|
+
const safeLimit = Math.min(Math.max(Number(limit) || 4, 1), 20);
|
|
1348
|
+
const shown = photos.slice(0, safeLimit);
|
|
1349
|
+
|
|
1350
|
+
const result = {
|
|
1351
|
+
status: "success",
|
|
1352
|
+
count: Math.min(photos.length, safeLimit),
|
|
1353
|
+
photos: shown.map((p, index) => ({
|
|
1354
|
+
id: p.id || `photo-${p.era || p.year || 'unknown'}-${index + 1}`,
|
|
1355
|
+
title: p.titleTh || p.titleCn,
|
|
1236
1356
|
category: p.category,
|
|
1237
1357
|
year: p.era || p.year
|
|
1238
1358
|
}))
|
|
1239
1359
|
};
|
|
1360
|
+
if (usedQueryFilter === false && query) {
|
|
1361
|
+
result.message = `Showing all ${result.count} photos (broad query "${query}" applied to full list).`;
|
|
1362
|
+
}
|
|
1363
|
+
return result;
|
|
1240
1364
|
},
|
|
1241
1365
|
|
|
1242
1366
|
global_search: async ({ query }) => {
|
|
1243
1367
|
if (!query) return { status: "error", message: "Query parameter is required." };
|
|
1244
|
-
const
|
|
1245
|
-
const ignoredTerms = new Set(['the', 'from', 'what', 'are', 'drama', 'ost', 'song', 'songs', 'soundtrack', 'music']);
|
|
1246
|
-
const terms = q.split(/[^\p{L}\p{N}]+/u).filter(term => term.length > 2 && !ignoredTerms.has(term));
|
|
1247
|
-
const matchesQuery = (...values) => {
|
|
1248
|
-
const haystack = values.filter(Boolean).join(' ').toLowerCase();
|
|
1249
|
-
return haystack.includes(q) || (terms.length > 0 && terms.every(term => haystack.includes(term)));
|
|
1250
|
-
};
|
|
1368
|
+
const matcher = createCatalogMatcher(query);
|
|
1251
1369
|
|
|
1252
1370
|
const matchedWorks = worksData.filter(w =>
|
|
1253
|
-
|
|
1371
|
+
matcher(w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn)
|
|
1254
1372
|
).slice(0, 4).map(w => ({
|
|
1255
1373
|
title: `${w.titleTh} (${w.year}) - Role: ${w.role}`,
|
|
1256
1374
|
trailerYouTubeSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.titleCn || w.titleEn} Trailer`)
|
|
@@ -1258,24 +1376,26 @@ export const toolHandlers = {
|
|
|
1258
1376
|
|
|
1259
1377
|
const disc = getCleanDiscography();
|
|
1260
1378
|
const matchedSongs = disc.filter(s =>
|
|
1261
|
-
|
|
1379
|
+
matcher(s.titleTh, s.titleEn, s.titleCn, s.work, s.role, s.description)
|
|
1262
1380
|
).slice(0, 4).map(s => ({
|
|
1263
1381
|
title: `${s.titleTh} (${s.year})`,
|
|
1264
1382
|
youtubeUrl: s.youtubeUrl || null,
|
|
1383
|
+
isDirectLink: Boolean(s.youtubeUrl),
|
|
1265
1384
|
youtubeSearchUrl: s.youtubeSearchUrl
|
|
1266
1385
|
}));
|
|
1267
1386
|
|
|
1268
1387
|
const mvs = getAllMusicVideos().filter(m =>
|
|
1269
|
-
|
|
1388
|
+
matcher(m.titleTh, m.titleCn, m.titlePinyin, m.dramaName, m.description)
|
|
1270
1389
|
).slice(0, 4).map(m => ({
|
|
1271
|
-
title: `${formatCatalogTitle(m.titleTh, m.titleCn)} [${m.category}]`,
|
|
1390
|
+
title: `${formatCatalogTitle(m.titleTh, m.titleCn)} [${m.category}]`,
|
|
1272
1391
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
1392
|
+
isDirectLink: Boolean(m.youtubeId),
|
|
1273
1393
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh}`)
|
|
1274
1394
|
}));
|
|
1275
1395
|
|
|
1276
|
-
const brands = Array.isArray(brandsData) ? (brandsData.brandList ||
|
|
1396
|
+
const brands = Array.isArray(brandsData) ? brandsData : (brandsData.brandList || brandsData.brands || []);
|
|
1277
1397
|
const matchedBrands = brands.filter(b =>
|
|
1278
|
-
|
|
1398
|
+
matcher(b.brandNameEn, b.brandNameCn, b.name, b.title, b.category, b.highlight)
|
|
1279
1399
|
).slice(0, 4).map(b => `${b.brandNameEn || b.name} (${b.year}) - ${b.title}`);
|
|
1280
1400
|
|
|
1281
1401
|
const count = matchedWorks.length + matchedSongs.length + mvs.length + matchedBrands.length;
|
|
@@ -1302,32 +1422,35 @@ export const toolHandlers = {
|
|
|
1302
1422
|
return await fetchWebpage(url);
|
|
1303
1423
|
},
|
|
1304
1424
|
|
|
1305
|
-
fetch_api: async ({ endpoint, query, baseUrl } = {}) => {
|
|
1306
|
-
if (!endpoint) return { status: "error", message: "endpoint parameter is required (e.g. 'profile', 'works', 'search')" };
|
|
1307
|
-
const base = (baseUrl || 'https://jujingyi-kikuuu.vercel.app/api/v1').replace(/\/$/, '');
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
return
|
|
1329
|
-
|
|
1330
|
-
|
|
1425
|
+
fetch_api: async ({ endpoint, query, baseUrl } = {}) => {
|
|
1426
|
+
if (!endpoint) return { status: "error", message: "endpoint parameter is required (e.g. 'profile', 'works', 'search')" };
|
|
1427
|
+
const base = (baseUrl || 'https://jujingyi-kikuuu.vercel.app/api/v1').replace(/\/$/, '');
|
|
1428
|
+
const originCheck = sanitizeUrl(base);
|
|
1429
|
+
if (!originCheck.safe) {
|
|
1430
|
+
return { status: "error", message: `Unsafe API base URL: ${originCheck.reason}` };
|
|
1431
|
+
}
|
|
1432
|
+
const allowedOrigins = new Set([
|
|
1433
|
+
'https://jujingyi-kikuuu.vercel.app',
|
|
1434
|
+
...(process.env.KIKU_API_ALLOWED_ORIGINS || '').split(',').map(value => value.trim()).filter(Boolean)
|
|
1435
|
+
]);
|
|
1436
|
+
if (!originCheck.url || !allowedOrigins.has(originCheck.url.origin)) {
|
|
1437
|
+
return { status: 'error', message: 'API base origin is not allowlisted.' };
|
|
1438
|
+
}
|
|
1439
|
+
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
1440
|
+
const target = new URL(`${base}/${endpoint.replace(/^\/+/, '')}`);
|
|
1441
|
+
if (target.origin !== originCheck.url.origin || !target.pathname.startsWith('/api/v1/')) {
|
|
1442
|
+
return { status: 'error', message: 'API endpoint must remain under /api/v1/.' };
|
|
1443
|
+
}
|
|
1444
|
+
if (query) target.searchParams.set('q', query);
|
|
1445
|
+
|
|
1446
|
+
try {
|
|
1447
|
+
const result = await fetchJson(target.href, { timeoutMs: 10000, maxResponseLength: 100000 });
|
|
1448
|
+
if (result.status !== 'success') return result;
|
|
1449
|
+
const json = result.data;
|
|
1450
|
+
const data = json?.data || json;
|
|
1451
|
+
return {
|
|
1452
|
+
status: "success",
|
|
1453
|
+
endpoint: path,
|
|
1331
1454
|
count: Array.isArray(data) ? data.length : undefined,
|
|
1332
1455
|
data
|
|
1333
1456
|
};
|
|
@@ -1335,7 +1458,7 @@ export const toolHandlers = {
|
|
|
1335
1458
|
return {
|
|
1336
1459
|
status: "error",
|
|
1337
1460
|
endpoint: path,
|
|
1338
|
-
message: err.name === 'AbortError' ? 'API request timed out' : err.message
|
|
1461
|
+
message: err.name === 'AbortError' ? 'API request timed out' : err.message
|
|
1339
1462
|
};
|
|
1340
1463
|
}
|
|
1341
1464
|
},
|
|
@@ -1347,15 +1470,21 @@ export const toolHandlers = {
|
|
|
1347
1470
|
read_local_file: async ({ path: filePath, maxLines = 200 } = {}) => {
|
|
1348
1471
|
if (!filePath) return { status: 'error', message: 'path parameter is required.' };
|
|
1349
1472
|
try {
|
|
1350
|
-
const projectRoot = process.cwd();
|
|
1351
|
-
const resolved = resolve(projectRoot, filePath);
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1473
|
+
const projectRoot = await realpath(process.cwd());
|
|
1474
|
+
const resolved = resolve(projectRoot, filePath);
|
|
1475
|
+
const realResolved = await realpath(resolved);
|
|
1476
|
+
const relativePath = relative(projectRoot, realResolved);
|
|
1477
|
+
|
|
1478
|
+
if (relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
1479
|
+
return { status: 'error', message: 'Path must be within the project directory.' };
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (/(^|[\\/])\.env(?:\.|$)/i.test(relativePath) || /(^|[\\/])cookies\.txt$/i.test(relativePath) ||
|
|
1483
|
+
/\.(pem|key|p12|pfx)$/i.test(relativePath) || /(^|[\\/])\.(git|npmrc|netrc|ssh)([\\/]|$)/i.test(relativePath)) {
|
|
1484
|
+
return { status: 'error', message: 'Reading this file is not permitted (sensitive file).' };
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const fileStat = await stat(realResolved);
|
|
1359
1488
|
if (!fileStat.isFile()) {
|
|
1360
1489
|
return { status: 'error', message: `"${filePath}" is not a file.` };
|
|
1361
1490
|
}
|
|
@@ -1364,7 +1493,7 @@ export const toolHandlers = {
|
|
|
1364
1493
|
return { status: 'error', message: `File too large (${Math.round(fileStat.size / 1024)}KB). Use maxLines to read a portion.` };
|
|
1365
1494
|
}
|
|
1366
1495
|
|
|
1367
|
-
const content = await readFile(
|
|
1496
|
+
const content = await readFile(realResolved, 'utf-8');
|
|
1368
1497
|
const lines = content.split('\n');
|
|
1369
1498
|
const totalLines = lines.length;
|
|
1370
1499
|
const truncated = totalLines > maxLines;
|
|
@@ -1389,13 +1518,14 @@ export const toolHandlers = {
|
|
|
1389
1518
|
|
|
1390
1519
|
list_directory: async ({ path: dirPath = '.', recursive = false } = {}) => {
|
|
1391
1520
|
try {
|
|
1392
|
-
const projectRoot = process.cwd();
|
|
1393
|
-
const resolved = resolve(projectRoot, dirPath);
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1521
|
+
const projectRoot = await realpath(process.cwd());
|
|
1522
|
+
const resolved = resolve(projectRoot, dirPath);
|
|
1523
|
+
const realResolved = await realpath(resolved);
|
|
1524
|
+
const relativePath = relative(projectRoot, realResolved);
|
|
1525
|
+
|
|
1526
|
+
if (relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
1527
|
+
return { status: 'error', message: 'Path must be within the project directory.' };
|
|
1528
|
+
}
|
|
1399
1529
|
|
|
1400
1530
|
async function listDir(dir, depth = 0) {
|
|
1401
1531
|
if (depth > (recursive ? 3 : 0)) return [];
|
|
@@ -1435,7 +1565,7 @@ export const toolHandlers = {
|
|
|
1435
1565
|
return items;
|
|
1436
1566
|
}
|
|
1437
1567
|
|
|
1438
|
-
const items = await listDir(
|
|
1568
|
+
const items = await listDir(realResolved);
|
|
1439
1569
|
return {
|
|
1440
1570
|
status: 'success',
|
|
1441
1571
|
path: relativePath || '.',
|
|
@@ -1466,8 +1596,20 @@ export const toolHandlers = {
|
|
|
1466
1596
|
const mvs = getAllMusicVideos();
|
|
1467
1597
|
const gpu = await getGpuMetrics().catch(() => ({ available: false }));
|
|
1468
1598
|
|
|
1599
|
+
const debutMatch = profileData.details?.debutDate?.match(/\b(19\d\d|20\d\d)\b/);
|
|
1600
|
+
const debutYear = debutMatch ? Number(debutMatch[1]) : 2013;
|
|
1601
|
+
const years = worksData.map(w => w.year).filter(y => typeof y === 'number' && !isNaN(y));
|
|
1602
|
+
const maxYear = years.length > 0 ? Math.max(...years, new Date().getFullYear()) : new Date().getFullYear();
|
|
1603
|
+
const activeYears = `${debutYear} - Present (${maxYear})`;
|
|
1604
|
+
const latestWorks = worksData.filter(w => w.year === maxYear);
|
|
1605
|
+
const featuredWorks = latestWorks.length > 0
|
|
1606
|
+
? latestWorks.map(w => formatCatalogTitle(w.titleTh, w.titleCn))
|
|
1607
|
+
: ["Veil of Shadows (月鳞绮纪)", "The Eternal Fragrance (千香)", "Wanhua Sin (万华世界)"];
|
|
1608
|
+
|
|
1469
1609
|
return {
|
|
1470
1610
|
status: "success",
|
|
1611
|
+
sourceType: "local_database",
|
|
1612
|
+
dataFreshness: "2026-09-02",
|
|
1471
1613
|
stats: {
|
|
1472
1614
|
artistName: profileData.nameTh,
|
|
1473
1615
|
artistNameCn: profileData.nameCn,
|
|
@@ -1477,8 +1619,8 @@ export const toolHandlers = {
|
|
|
1477
1619
|
brandEndorsements: brands.length,
|
|
1478
1620
|
totalAwards: awards.length,
|
|
1479
1621
|
timelineMilestones: timeline.length,
|
|
1480
|
-
activeYears
|
|
1481
|
-
featured2026Works:
|
|
1622
|
+
activeYears,
|
|
1623
|
+
featured2026Works: featuredWorks,
|
|
1482
1624
|
youtubeOfficialHub: buildYouTubeSearchUrl("Ju Jingyi 鞠婧祎"),
|
|
1483
1625
|
gpu: gpu.available ? gpu.primary : { available: false }
|
|
1484
1626
|
}
|
|
@@ -1540,7 +1682,12 @@ export const toolHandlers = {
|
|
|
1540
1682
|
},
|
|
1541
1683
|
|
|
1542
1684
|
remove_from_queue: async (args) => {
|
|
1543
|
-
const
|
|
1685
|
+
const queue = audioPlayer.getQueue();
|
|
1686
|
+
const index = Number(args.index);
|
|
1687
|
+
if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
|
|
1688
|
+
return { status: 'error', message: `Invalid queue index ${args.index} (queue has ${queue.length} item(s)).` };
|
|
1689
|
+
}
|
|
1690
|
+
const result = await audioPlayer.removeFromQueue(index);
|
|
1544
1691
|
return { status: 'success', message: result };
|
|
1545
1692
|
},
|
|
1546
1693
|
|
|
@@ -1574,54 +1721,100 @@ export const toolHandlers = {
|
|
|
1574
1721
|
},
|
|
1575
1722
|
|
|
1576
1723
|
run_subagent: async ({ task }, options = {}) => {
|
|
1724
|
+
const forwardProgress = (event) => options?.onSubagentProgress?.({ source: 'subagent', task, ...event });
|
|
1725
|
+
forwardProgress({ type: 'start' });
|
|
1577
1726
|
if (options?.agent?.runSubagent) {
|
|
1578
|
-
const result = await options.agent.runSubagent(task, { signal: options.signal });
|
|
1727
|
+
const result = await options.agent.runSubagent(task, { signal: options.signal, onProgress: forwardProgress });
|
|
1579
1728
|
return { status: "success", task, answer: result.answer, toolCalls: result.toolCalls, iterations: result.iterations };
|
|
1580
1729
|
}
|
|
1581
1730
|
const { LMStudioClient } = await import('./llmClient.js');
|
|
1582
1731
|
const { ResearchSubagent } = await import('./subagent.js');
|
|
1583
1732
|
const client = options?.client || new LMStudioClient();
|
|
1733
|
+
const offline = await ensureLlmAvailable(client);
|
|
1734
|
+
if (offline) return { ...offline, task };
|
|
1584
1735
|
const subagent = new ResearchSubagent({ client });
|
|
1585
|
-
const result = await subagent.run(task, { signal: options?.signal });
|
|
1736
|
+
const result = await subagent.run(task, { signal: options?.signal, onProgress: forwardProgress });
|
|
1586
1737
|
return { status: "success", task, answer: result.answer, toolCalls: result.toolCalls, iterations: result.iterations };
|
|
1587
1738
|
},
|
|
1588
1739
|
|
|
1589
1740
|
run_deep_research: async ({ question }, options = {}) => {
|
|
1590
|
-
|
|
1591
|
-
|
|
1741
|
+
const forwardProgress = (event) => options?.onSubagentProgress?.({ source: 'research', question, ...event });
|
|
1742
|
+
forwardProgress({ type: 'start' });
|
|
1743
|
+
try {
|
|
1744
|
+
if (options?.agent?.runResearch) {
|
|
1745
|
+
const result = await options.agent.runResearch(question, { signal: options.signal, onProgress: forwardProgress });
|
|
1746
|
+
return { status: "success", question, answer: result.answer, sources: result.sources, reports: result.reports };
|
|
1747
|
+
}
|
|
1748
|
+
const { LMStudioClient } = await import('./llmClient.js');
|
|
1749
|
+
const { ResearchOrchestrator } = await import('./research.js');
|
|
1750
|
+
const client = options?.client || options?.agent?.client || new LMStudioClient();
|
|
1751
|
+
const offlineResearch = await ensureLlmAvailable(client);
|
|
1752
|
+
if (offlineResearch) throw new Error(offlineResearch.message);
|
|
1753
|
+
const orchestrator = options?.agent?.research || new ResearchOrchestrator({ client });
|
|
1754
|
+
const result = await orchestrator.run(question, { signal: options?.signal, onProgress: forwardProgress });
|
|
1592
1755
|
return { status: "success", question, answer: result.answer, sources: result.sources, reports: result.reports };
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
try {
|
|
1758
|
+
const { webExtract } = await import('./webSearch.js');
|
|
1759
|
+
const extractRes = await webExtract(question);
|
|
1760
|
+
return {
|
|
1761
|
+
status: "success",
|
|
1762
|
+
question,
|
|
1763
|
+
answer: extractRes.text || extractRes.summary || "Retrieved live web intelligence.",
|
|
1764
|
+
sources: extractRes.sources || [],
|
|
1765
|
+
fallbackUsed: true
|
|
1766
|
+
};
|
|
1767
|
+
} catch {
|
|
1768
|
+
return { status: "error", question, message: err.message || "Deep research failed." };
|
|
1769
|
+
}
|
|
1593
1770
|
}
|
|
1594
|
-
const { LMStudioClient } = await import('./llmClient.js');
|
|
1595
|
-
const { ResearchOrchestrator } = await import('./research.js');
|
|
1596
|
-
const client = options?.client || new LMStudioClient();
|
|
1597
|
-
const orchestrator = new ResearchOrchestrator({ client });
|
|
1598
|
-
const result = await orchestrator.run(question, { signal: options?.signal });
|
|
1599
|
-
return { status: "success", question, answer: result.answer, sources: result.sources, reports: result.reports };
|
|
1600
1771
|
},
|
|
1601
1772
|
|
|
1602
1773
|
verify_claim: async ({ claim }, options = {}) => {
|
|
1603
|
-
|
|
1604
|
-
|
|
1774
|
+
const forwardProgress = (event) => options?.onSubagentProgress?.({ source: 'verify', claim, ...event });
|
|
1775
|
+
forwardProgress({ type: 'start' });
|
|
1776
|
+
try {
|
|
1777
|
+
if (options?.agent?.runResearch) {
|
|
1778
|
+
const result = await options.agent.runResearch(claim, { mode: 'verify', signal: options.signal, onProgress: forwardProgress });
|
|
1779
|
+
return { status: "success", claim, answer: result.answer, sources: result.sources, reports: result.reports };
|
|
1780
|
+
}
|
|
1781
|
+
const { LMStudioClient } = await import('./llmClient.js');
|
|
1782
|
+
const { ResearchOrchestrator } = await import('./research.js');
|
|
1783
|
+
const client = options?.client || options?.agent?.client || new LMStudioClient();
|
|
1784
|
+
const offlineVerify = await ensureLlmAvailable(client);
|
|
1785
|
+
if (offlineVerify) throw new Error(offlineVerify.message);
|
|
1786
|
+
const orchestrator = options?.agent?.research || new ResearchOrchestrator({ client });
|
|
1787
|
+
const result = await orchestrator.run(claim, { mode: 'verify', signal: options?.signal, onProgress: forwardProgress });
|
|
1605
1788
|
return { status: "success", claim, answer: result.answer, sources: result.sources, reports: result.reports };
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
try {
|
|
1791
|
+
const { webExtract } = await import('./webSearch.js');
|
|
1792
|
+
const extractRes = await webExtract(claim);
|
|
1793
|
+
return {
|
|
1794
|
+
status: "success",
|
|
1795
|
+
claim,
|
|
1796
|
+
answer: extractRes.text || extractRes.summary || "Retrieved live web verification intelligence.",
|
|
1797
|
+
sources: extractRes.sources || [],
|
|
1798
|
+
fallbackUsed: true
|
|
1799
|
+
};
|
|
1800
|
+
} catch {
|
|
1801
|
+
return { status: "error", claim, message: err.message || "Verification failed." };
|
|
1802
|
+
}
|
|
1606
1803
|
}
|
|
1607
|
-
const { LMStudioClient } = await import('./llmClient.js');
|
|
1608
|
-
const { ResearchOrchestrator } = await import('./research.js');
|
|
1609
|
-
const client = options?.client || new LMStudioClient();
|
|
1610
|
-
const orchestrator = new ResearchOrchestrator({ client });
|
|
1611
|
-
const result = await orchestrator.run(claim, { mode: 'verify', signal: options?.signal });
|
|
1612
|
-
return { status: "success", claim, answer: result.answer, sources: result.sources, reports: result.reports };
|
|
1613
1804
|
},
|
|
1614
1805
|
|
|
1615
|
-
suggest_topics: async ({ topic, limit }, options = {}) => {
|
|
1806
|
+
suggest_topics: async ({ topic, limit, lang }, options = {}) => {
|
|
1616
1807
|
if (options?.agent?.generateSuggestions) {
|
|
1617
|
-
const suggestions = await options.agent.generateSuggestions({ topic, limit, signal: options.signal });
|
|
1808
|
+
const suggestions = await options.agent.generateSuggestions({ topic, limit, lang, signal: options.signal });
|
|
1618
1809
|
return { status: "success", topic, suggestions };
|
|
1619
1810
|
}
|
|
1620
1811
|
const { SuggestionEngine } = await import('./suggestions.js');
|
|
1621
1812
|
const { LMStudioClient } = await import('./llmClient.js');
|
|
1622
1813
|
const client = options?.client || new LMStudioClient();
|
|
1814
|
+
const offline = await ensureLlmAvailable(client);
|
|
1815
|
+
if (offline) return { ...offline, topic };
|
|
1623
1816
|
const engine = new SuggestionEngine({ client });
|
|
1624
|
-
const suggestions = await engine.generateSuggestions({ topic, limit, signal: options?.signal });
|
|
1817
|
+
const suggestions = await engine.generateSuggestions({ topic, limit, lang, signal: options?.signal });
|
|
1625
1818
|
return { status: "success", topic, suggestions };
|
|
1626
1819
|
},
|
|
1627
1820
|
|
|
@@ -1839,6 +2032,10 @@ export function normalizeToolResult(toolName, result) {
|
|
|
1839
2032
|
return normalized;
|
|
1840
2033
|
}
|
|
1841
2034
|
|
|
2035
|
+
export function isBuiltinTool(toolName) {
|
|
2036
|
+
return Boolean(toolHandlers && toolHandlers[toolName]);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1842
2039
|
export async function executeTool(toolName, args = {}, options = {}) {
|
|
1843
2040
|
const handler = toolHandlers[toolName];
|
|
1844
2041
|
if (!handler) {
|
|
@@ -1860,14 +2057,16 @@ export async function executeTool(toolName, args = {}, options = {}) {
|
|
|
1860
2057
|
|
|
1861
2058
|
const isStatic = STATIC_CATALOG_TOOLS.has(toolName);
|
|
1862
2059
|
const isWeb = WEB_CACHE_TOOLS.has(toolName);
|
|
1863
|
-
const
|
|
2060
|
+
const isLive = LIVE_DATA_TOOLS.has(toolName);
|
|
2061
|
+
const cacheable = (isStatic || isWeb) && !isLive;
|
|
2062
|
+
const cacheKey = cacheable ? `${toolName}:${JSON.stringify(prepared.args)}` : null;
|
|
1864
2063
|
|
|
1865
2064
|
if (cacheKey && !options.noCache) {
|
|
1866
2065
|
const cached = globalToolCache.get(cacheKey);
|
|
1867
2066
|
if (cached) return cached;
|
|
1868
2067
|
}
|
|
1869
2068
|
|
|
1870
|
-
const configuredTimeout = Number(options.timeoutMs ?? process.env.KIKU_TOOL_TIMEOUT_MS ?? 45_000);
|
|
2069
|
+
const configuredTimeout = Number(options.timeoutMs ?? TOOL_DEFAULT_TIMEOUTS[toolName] ?? process.env.KIKU_TOOL_TIMEOUT_MS ?? 45_000);
|
|
1871
2070
|
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 45_000;
|
|
1872
2071
|
let timeoutId;
|
|
1873
2072
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -1885,8 +2084,8 @@ export async function executeTool(toolName, args = {}, options = {}) {
|
|
|
1885
2084
|
normalized.validationWarnings = prepared.warnings;
|
|
1886
2085
|
}
|
|
1887
2086
|
|
|
1888
|
-
if (cacheKey && normalized.status
|
|
1889
|
-
const ttl = isStatic ?
|
|
2087
|
+
if (cacheKey && normalized.status === 'success' && (normalized.count === undefined || normalized.count > 0)) {
|
|
2088
|
+
const ttl = isStatic ? 600_000 : 300_000;
|
|
1890
2089
|
globalToolCache.set(cacheKey, normalized, ttl);
|
|
1891
2090
|
}
|
|
1892
2091
|
|