kiku-agent 1.2.0 → 1.3.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 +459 -407
- package/package.json +27 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +629 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +528 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +383 -179
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
package/server/agent/tools.js
CHANGED
|
@@ -7,12 +7,20 @@ import { socialUpdatesData } from '../../src/data/socialUpdatesData.js';
|
|
|
7
7
|
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
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { getDynamicEntries, addDynamicEntry } from './dynamicCatalog.js';
|
|
11
|
+
import { industryIndexData, platformsCatalog, artistIndustryMetrics, categoryDefinitions } from '../../src/data/industryIndexData.js';
|
|
12
|
+
import {
|
|
13
|
+
searchWeb, fetchWebpage, fetchJson, screenshotWebpage, webExtract,
|
|
14
|
+
searchNews, fetchRssFeed, searchEncyclopedia, searchMusicPlatforms,
|
|
15
|
+
searchChineseWeb, searchBilibili, searchWebImages, detectLanguage
|
|
16
|
+
} from './webSearch.js';
|
|
17
|
+
import { readFile, readdir, stat, realpath } from 'node:fs/promises';
|
|
12
18
|
import { join, relative, extname, resolve } from 'node:path';
|
|
13
19
|
import { audioPlayer } from './audioPlayer.js';
|
|
20
|
+
import { fetchYoutubeTranscript } from './youtubeTranscript.js';
|
|
14
21
|
import { sanitizeToolArgs, sanitizeUrl } from './sanitizer.js';
|
|
15
22
|
import { getGpuMetrics } from './gpuMetrics.js';
|
|
23
|
+
import { levenshteinDistance, levenshteinSimilarity, levenshteinScore, CATEGORY_ALIASES } from './catalogEntityResolver.js';
|
|
16
24
|
|
|
17
25
|
export function buildYouTubeSearchUrl(query) {
|
|
18
26
|
return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`;
|
|
@@ -41,25 +49,46 @@ export function formatCatalogTitle(primary, secondary) {
|
|
|
41
49
|
return `${first} (${second})`;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
export const CATALOG_STOP_WORDS = new Set([
|
|
53
|
+
'the', 'from', 'what', 'are', 'is', 'about', 'drama', 'series', 'ost', 'song', 'songs', 'soundtrack', 'music', 'single', 'album', 'stage',
|
|
54
|
+
'brand', 'brands', 'ambassador', 'endorsement', 'commercial', 'role', 'roles', 'character', 'characters', 'award', 'awards', 'category', 'video', 'mv', 'clip',
|
|
55
|
+
'ju', 'jingyi', 'jujingyi', 'kiku', 'kikuuu', 'จวีจิ้งอี', 'จวี', 'จิ้งอี', 'เสี่ยวจวี', 'เสี่ยว', '鞠婧祎', '小鞠', '鞠',
|
|
56
|
+
'เพลง', 'เพลงของ', 'ซีรีส์', 'ซีรีส์ของ', 'ละคร', 'เรื่อง', 'ผลงาน', 'อัลบั้ม', 'คอนเสิร์ต', 'บทบาท', 'ตัวละคร', 'แบรนด์', 'รางวัล', 'ผลงานล่าสุด', 'พรีเซนเตอร์',
|
|
57
|
+
'ค้นหา', 'หา', 'ดู', 'เปิด', 'ฟัง', 'โปรโมท', 'โปรโมต', 'การโปรโมท', 'การโปรโมต', 'โฆษณา', 'แคมเปญ', 'ข้อมูล', 'รายละเอียด', 'บน', 'ใน', 'ที่', 'กับ', 'ช่วย', 'อยาก', 'ให้', 'หน่อย', 'คือ', 'อะไร', 'บ้าง', 'ไหม', 'มั้ย', 'มี',
|
|
58
|
+
'search', 'find', 'show', 'look', 'promo', 'promote', 'promoted', 'campaign', 'for', 'with', 'tell', 'me', 'please', 'can', 'you', 'info', 'details',
|
|
59
|
+
'搜索', '查找', '看看', '播放', '在', '上的', '关于', '有什么', '吗', '呢',
|
|
60
|
+
'bilibili', 'b站', 'youtube', 'weibo', 'douyin', 'spotify', 'qqmusic', 'iqiyi', 'youku', 'tencent'
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const PERSONAL_FACT_KEYWORDS = Object.freeze([
|
|
64
|
+
'birthplace', 'born', 'birthday', 'education', 'instrument', 'violin', 'blood', 'height', 'hometown', 'cat', 'cats', 'pet', 'pets', 'fandom', 'slogan', 'motto', 'กำเนิด', 'เกิด', 'บ้านเกิด', 'ไวโอลิน', 'เครื่องดนตรี', 'การศึกษา', 'กรุ๊ปเลือด', 'ส่วนสูง', 'แมว', '出生', '籍贯', '血型', '身高', '毕业'
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function normalizeWorkId(value) {
|
|
68
|
+
return String(value || '').toLowerCase().trim().replace(/[_\s]+/g, '-');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const WORKS_BY_NORM_ID = new Map();
|
|
72
|
+
for (const w of worksData) {
|
|
73
|
+
const id = normalizeWorkId(w.id);
|
|
74
|
+
if (id && !WORKS_BY_NORM_ID.has(id)) WORKS_BY_NORM_ID.set(id, w);
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
export function createCatalogMatcher(query) {
|
|
45
78
|
if (!query) return () => true;
|
|
46
79
|
const raw = String(query).toLowerCase().trim();
|
|
47
80
|
if (!raw) return () => true;
|
|
48
81
|
|
|
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
82
|
const allTokens = raw.split(/[^\p{L}\p{M}\p{N}]+/u).filter(t => t.length >= 2);
|
|
57
|
-
|
|
58
83
|
const deDated = raw.replace(/ใหม่ล่าสุด|ล่าสุด|最新|latest|newest|recent/gi, ' ');
|
|
59
84
|
const datedTokens = deDated.split(/[^\p{L}\p{M}\p{N}]+/u).filter(t => t.length >= 2);
|
|
60
|
-
const coreTokens = datedTokens.filter(t => !
|
|
61
|
-
|
|
62
|
-
if (coreTokens.length === 0)
|
|
85
|
+
const coreTokens = datedTokens.filter(t => !CATALOG_STOP_WORDS.has(t));
|
|
86
|
+
|
|
87
|
+
if (coreTokens.length === 0) {
|
|
88
|
+
const broadMatcher = () => true;
|
|
89
|
+
broadMatcher.broad = true;
|
|
90
|
+
return broadMatcher;
|
|
91
|
+
}
|
|
63
92
|
const effectiveTokens = coreTokens;
|
|
64
93
|
|
|
65
94
|
return (...fields) => {
|
|
@@ -67,7 +96,27 @@ export function createCatalogMatcher(query) {
|
|
|
67
96
|
if (haystack.includes(raw)) return true;
|
|
68
97
|
if (effectiveTokens.length > 0) {
|
|
69
98
|
const matchCount = effectiveTokens.filter(term => haystack.includes(term)).length;
|
|
70
|
-
|
|
99
|
+
if (matchCount > 0 && (matchCount / effectiveTokens.length >= 0.5)) return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (raw.length >= 4) {
|
|
103
|
+
for (const field of fields) {
|
|
104
|
+
if (!field) continue;
|
|
105
|
+
const fStr = String(field).toLowerCase();
|
|
106
|
+
if (fStr.includes(raw) || raw.includes(fStr)) return true;
|
|
107
|
+
const words = fStr.split(/\s+/);
|
|
108
|
+
for (let i = 0; i < words.length; i++) {
|
|
109
|
+
for (let len = 1; len <= Math.min(5, words.length - i); len++) {
|
|
110
|
+
const sub = words.slice(i, i + len).join(' ');
|
|
111
|
+
if (sub.length >= 4 && Math.abs(sub.length - raw.length) <= 3) {
|
|
112
|
+
const { dist, sim } = levenshteinScore(raw, sub);
|
|
113
|
+
if (dist <= 2 || sim >= 0.75) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
71
120
|
}
|
|
72
121
|
return false;
|
|
73
122
|
};
|
|
@@ -114,15 +163,17 @@ const STATIC_CATALOG_TOOLS = new Set([
|
|
|
114
163
|
'get_profile_and_fandom',
|
|
115
164
|
'get_quotes',
|
|
116
165
|
'get_awards',
|
|
117
|
-
'
|
|
166
|
+
'get_career_timeline',
|
|
118
167
|
'get_brand_endorsements',
|
|
119
|
-
'
|
|
168
|
+
'get_gallery_photos',
|
|
120
169
|
'get_music_videos',
|
|
121
170
|
'search_works',
|
|
122
171
|
'search_discography',
|
|
123
172
|
'get_work_detail',
|
|
124
|
-
'get_social_updates',
|
|
125
173
|
'get_media_links',
|
|
174
|
+
'get_entertainment_metrics',
|
|
175
|
+
'get_industry_index',
|
|
176
|
+
'global_search',
|
|
126
177
|
]);
|
|
127
178
|
|
|
128
179
|
const WEB_CACHE_TOOLS = new Set([
|
|
@@ -130,17 +181,19 @@ const WEB_CACHE_TOOLS = new Set([
|
|
|
130
181
|
'web_extract',
|
|
131
182
|
'fetch_webpage',
|
|
132
183
|
'fetch_json',
|
|
184
|
+
'search_news',
|
|
185
|
+
'fetch_rss',
|
|
186
|
+
'search_encyclopedia',
|
|
187
|
+
'search_music_platforms',
|
|
188
|
+
'search_chinese_web',
|
|
189
|
+
'search_bilibili',
|
|
190
|
+
'search_web_images',
|
|
133
191
|
]);
|
|
134
192
|
|
|
135
193
|
|
|
194
|
+
|
|
136
195
|
const LIVE_DATA_TOOLS = new Set([
|
|
137
196
|
'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
197
|
]);
|
|
145
198
|
|
|
146
199
|
const CACHED_DISCOGRAPHY = (() => {
|
|
@@ -163,6 +216,11 @@ const CACHED_DISCOGRAPHY = (() => {
|
|
|
163
216
|
titleEn: track.titleEn || '',
|
|
164
217
|
year: track.year,
|
|
165
218
|
work: track.work || "Solo Single",
|
|
219
|
+
category: track.category || 'single',
|
|
220
|
+
categoryName: track.categoryName || '',
|
|
221
|
+
role: track.role || '',
|
|
222
|
+
description: track.description || '',
|
|
223
|
+
badge: track.badge || '',
|
|
166
224
|
duration: track.duration || track.trackDuration || '',
|
|
167
225
|
theme: track.theme || "Pop / Ballad",
|
|
168
226
|
youtubeId: ytId,
|
|
@@ -171,6 +229,12 @@ const CACHED_DISCOGRAPHY = (() => {
|
|
|
171
229
|
qqMusicSearchUrl: buildQqMusicSearchUrl(`鞠婧祎 ${track.titleCn || track.titleTh}`),
|
|
172
230
|
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${track.titleCn || track.titleTh}`)
|
|
173
231
|
});
|
|
232
|
+
} else {
|
|
233
|
+
const existing = map.get(key);
|
|
234
|
+
if (!existing.role && track.role) existing.role = track.role;
|
|
235
|
+
if (!existing.description && track.description) existing.description = track.description;
|
|
236
|
+
if (!existing.categoryName && track.categoryName) existing.categoryName = track.categoryName;
|
|
237
|
+
if (!existing.work && track.work) existing.work = track.work;
|
|
174
238
|
}
|
|
175
239
|
}
|
|
176
240
|
|
|
@@ -195,12 +259,33 @@ function getAllMusicVideos() {
|
|
|
195
259
|
return CACHED_MUSIC_VIDEOS;
|
|
196
260
|
}
|
|
197
261
|
|
|
262
|
+
|
|
263
|
+
const WORK_CATEGORY_VALUES = ['historical-drama', 'modern-drama', 'movie'];
|
|
264
|
+
const WORK_CATEGORY_SYNONYMS = {
|
|
265
|
+
series: ['historical-drama', 'modern-drama'],
|
|
266
|
+
drama: ['historical-drama', 'modern-drama'],
|
|
267
|
+
tv: ['historical-drama', 'modern-drama'],
|
|
268
|
+
television: ['historical-drama', 'modern-drama'],
|
|
269
|
+
film: ['movie'],
|
|
270
|
+
cinema: ['movie'],
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export function normalizeWorkCategoryFilter(category) {
|
|
274
|
+
const raw = String(category || '').toLowerCase().trim();
|
|
275
|
+
if (!raw || raw === 'all') return null;
|
|
276
|
+
const compact = raw.replace(/[\s_]+/g, '-');
|
|
277
|
+
if (WORK_CATEGORY_VALUES.includes(compact)) return [compact];
|
|
278
|
+
if (WORK_CATEGORY_SYNONYMS[compact]) return WORK_CATEGORY_SYNONYMS[compact];
|
|
279
|
+
const partial = WORK_CATEGORY_VALUES.filter(k => k.includes(compact) || compact.includes(k));
|
|
280
|
+
return partial.length ? partial : [compact];
|
|
281
|
+
}
|
|
282
|
+
|
|
198
283
|
export const toolsSchema = [
|
|
199
284
|
{
|
|
200
285
|
type: "function",
|
|
201
286
|
function: {
|
|
202
|
-
name: "
|
|
203
|
-
description: "Get
|
|
287
|
+
name: "get_current_datetime",
|
|
288
|
+
description: "Get the current local date and time (ISO 8601, timezone, weekday, and current year). Call this whenever 'today', 'now', or the current year matters — NEVER guess or rely on training data for dates.",
|
|
204
289
|
parameters: {
|
|
205
290
|
type: "object",
|
|
206
291
|
properties: {},
|
|
@@ -208,15 +293,60 @@ export const toolsSchema = [
|
|
|
208
293
|
}
|
|
209
294
|
}
|
|
210
295
|
},
|
|
296
|
+
{
|
|
297
|
+
type: "function",
|
|
298
|
+
function: {
|
|
299
|
+
name: "get_youtube_transcript",
|
|
300
|
+
description: "Fetch the caption/transcript text of a YouTube video (manual or auto captions). Use it to verify lyrics, quotes, or statements from interviews instead of guessing their content.",
|
|
301
|
+
parameters: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
url: { type: "string", description: "Full YouTube video URL (watch, youtu.be, shorts, embed, or live)" },
|
|
305
|
+
videoId: { type: "string", description: "11-character YouTube video ID — alternative to url" },
|
|
306
|
+
lang: { type: "string", enum: ["th", "en", "zh"], description: "Preferred caption language" },
|
|
307
|
+
maxChars: { type: "number", description: "Maximum transcript characters to return (default 6000, cap 20000)" }
|
|
308
|
+
},
|
|
309
|
+
required: []
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
type: "function",
|
|
315
|
+
function: {
|
|
316
|
+
name: "get_profile_and_fandom",
|
|
317
|
+
description: "Get Ju Jingyi's biographical profile — the authoritative source for ALL personal facts: Chinese/English/Thai names, birthday, birthplace, height, blood type, education, musical instruments (classical violin), debut date, SNH48 graduation date, current status, Miju (蜜橘) fan community, official slogan, pets, and socials. For exact personal dates and biographical facts use this instead of get_career_timeline. Optional sections expose talents, career stats, Forbes-level honors, and official social links.",
|
|
318
|
+
parameters: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
query: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "Optional fact keyword to inspect (e.g. 'birthplace', 'education', 'instrument', 'violin', 'blood type', 'height', 'birthday', 'hometown', 'cats')"
|
|
324
|
+
},
|
|
325
|
+
section: {
|
|
326
|
+
type: "string",
|
|
327
|
+
enum: ["all", "talents", "stats", "honors", "social"],
|
|
328
|
+
description: "Optional extra profile section to include (default: all = core profile)"
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
required: []
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
211
335
|
{
|
|
212
336
|
type: "function",
|
|
213
337
|
function: {
|
|
214
338
|
name: "get_quotes",
|
|
215
|
-
description:
|
|
339
|
+
description: `Retrieve official quotes, mottoes, interview philosophies, mindsets, and life credos (e.g., "保持热爱,奔赴山海", "往前看").
|
|
340
|
+
[CRITICAL]: Use this tool when the user asks for her motto (座右铭), personal quotes, or philosophical sayings. DO NOT use get_profile_and_fandom for mindset/quotes.`,
|
|
216
341
|
parameters: {
|
|
217
342
|
type: "object",
|
|
218
343
|
properties: {
|
|
219
|
-
tag: {
|
|
344
|
+
tag: {
|
|
345
|
+
type: "string",
|
|
346
|
+
enum: ["mindset", "career", "fans", "all", "growth", "acting"],
|
|
347
|
+
description: "Theme tag of the quote"
|
|
348
|
+
},
|
|
349
|
+
query: { type: "string", description: "Specific keyword in Thai, Chinese, or English" }
|
|
220
350
|
},
|
|
221
351
|
required: []
|
|
222
352
|
}
|
|
@@ -226,14 +356,14 @@ export const toolsSchema = [
|
|
|
226
356
|
type: "function",
|
|
227
357
|
function: {
|
|
228
358
|
name: "search_works",
|
|
229
|
-
description:
|
|
359
|
+
description: `Search filmography, drama series, movies, and character costume details (e.g., Sword and Fairy 4 / 仙剑四, In Blossom, Han Lingsha red costume).
|
|
360
|
+
[CRITICAL]: Use this tool for drama episode counts, platforms, character visual traits, and plot synopses. DO NOT use global_search.`,
|
|
230
361
|
parameters: {
|
|
231
362
|
type: "object",
|
|
232
363
|
properties: {
|
|
233
|
-
query: { type: "string", description: "
|
|
234
|
-
year: { type: "number", description: "
|
|
235
|
-
category: { type: "string", description: "
|
|
236
|
-
featuredOnly: { type: "boolean", description: "Filter only featured major hit dramas." }
|
|
364
|
+
query: { type: "string", description: "Drama/Movie title or character name" },
|
|
365
|
+
year: { type: "number", description: "Release year" },
|
|
366
|
+
category: { type: "string", description: "Drama or movie category filter ('historical-drama', 'modern-drama', 'movie', 'series', 'drama', 'film', or 'all')" }
|
|
237
367
|
},
|
|
238
368
|
required: []
|
|
239
369
|
}
|
|
@@ -257,11 +387,11 @@ export const toolsSchema = [
|
|
|
257
387
|
type: "function",
|
|
258
388
|
function: {
|
|
259
389
|
name: "get_character_roles",
|
|
260
|
-
description: "
|
|
390
|
+
description: "Authoritative role catalog for FICTIONAL drama and movie characters Ju Jingyi portrays (e.g. Yang Caiwei, Lu Wuyi, Bai Suzhen, Han Yunxi). NEVER call this for Ju Jingyi's real-life personal facts, birthplace, birthday, education, or blood type — use get_profile_and_fandom instead.",
|
|
261
391
|
parameters: {
|
|
262
392
|
type: "object",
|
|
263
393
|
properties: {
|
|
264
|
-
query: { type: "string", description: "Optional role name or drama title keyword" }
|
|
394
|
+
query: { type: "string", description: "Optional fictional role name or drama title keyword" }
|
|
265
395
|
},
|
|
266
396
|
required: []
|
|
267
397
|
}
|
|
@@ -271,7 +401,8 @@ export const toolsSchema = [
|
|
|
271
401
|
type: "function",
|
|
272
402
|
function: {
|
|
273
403
|
name: "search_discography",
|
|
274
|
-
description:
|
|
404
|
+
description: `Search songs, solo singles, drama OSTs, and album tracks by Chinese title, English title, or Thai transliteration (e.g., View From The Wall / 壁上观, FIND THE WAY, 如纱, 不苦).
|
|
405
|
+
[CRITICAL]: Use this tool to find translations of song titles (Chinese <-> English). DO NOT use global_search for song queries.`,
|
|
275
406
|
parameters: {
|
|
276
407
|
type: "object",
|
|
277
408
|
properties: {
|
|
@@ -287,7 +418,7 @@ export const toolsSchema = [
|
|
|
287
418
|
type: "function",
|
|
288
419
|
function: {
|
|
289
420
|
name: "get_music_videos",
|
|
290
|
-
description: "
|
|
421
|
+
description: "Use for official music videos, dance practices, video release years, and video durations. Titles may include alphanumeric codes or short tokens like '0.2s' or 'Everyday' or catalog video IDs. If a query asks for the stored releaseYear of a video entry (e.g. Dewdrops video ID), retrieve it here.",
|
|
291
422
|
parameters: {
|
|
292
423
|
type: "object",
|
|
293
424
|
properties: {
|
|
@@ -302,7 +433,7 @@ export const toolsSchema = [
|
|
|
302
433
|
type: "function",
|
|
303
434
|
function: {
|
|
304
435
|
name: "get_media_links",
|
|
305
|
-
description: "
|
|
436
|
+
description: "LINKS ONLY: get direct YouTube video watch links (watch?v=...), official YouTube search queries (results?search_query=...), Bilibili links, and streaming URLs for a given Ju Jingyi song, MV, live stage, or drama trailer. It returns media URLs and basic metadata but NO character roles, song credits, or duet artists — for role facts use search_works/get_character_roles, for song facts use search_discography. Use this tool only when the user asks for URLs/links themselves.",
|
|
306
437
|
parameters: {
|
|
307
438
|
type: "object",
|
|
308
439
|
properties: {
|
|
@@ -317,7 +448,8 @@ export const toolsSchema = [
|
|
|
317
448
|
type: "function",
|
|
318
449
|
function: {
|
|
319
450
|
name: "get_brand_endorsements",
|
|
320
|
-
description:
|
|
451
|
+
description: `Authoritative source for all commercial brand contracts, spokesperson titles, global ambassadors, and commercial sales records/statistics (e.g., FRED Paris high jewelry, PRSR Milan, Lafang 30-year ambassador, TuTuCare 20,000 gift boxes 1-second sellout, DAFUYAN).
|
|
452
|
+
[CRITICAL]: If query mentions a commercial brand, ambassador title, or sales record/statistics, ALWAYS call this tool. Do NOT answer from memory or claim lack of records without querying.`,
|
|
321
453
|
parameters: {
|
|
322
454
|
type: "object",
|
|
323
455
|
properties: {
|
|
@@ -334,12 +466,14 @@ export const toolsSchema = [
|
|
|
334
466
|
type: "function",
|
|
335
467
|
function: {
|
|
336
468
|
name: "get_career_timeline",
|
|
337
|
-
description:
|
|
469
|
+
description: `Use for historical chronology, career milestones, debut dates, annual awards, election rankings, and chronological milestones across 2013-2026.
|
|
470
|
+
[CRITICAL]: Whenever the user query asks 'from timeline', 'according to timeline' ('ตามไทม์ไลน์', '按时间线', 'in the timeline') or asks for chronological events in a specific year (e.g. 2015 Seine River/Running Man China, 2016 election champion & center song Princess's Cloak, 2017 May 4th Medal & Studio graduation, 2018 co-star with Mike in Mr. Swimmer, 2019 Forbes 30 Under 30 Asia, 2024 Beauty Ambassador), you MUST invoke get_career_timeline. Do NOT substitute with search_works or get_media_links.`,
|
|
338
471
|
parameters: {
|
|
339
472
|
type: "object",
|
|
340
473
|
properties: {
|
|
341
474
|
year: { type: "number", description: "Year filter (2013-2026)" },
|
|
342
|
-
era: { type: "string", description: "Era keyword" }
|
|
475
|
+
era: { type: "string", description: "Era keyword (e.g. SNH48 Era, Solo Studio, Breakthrough Dramas, Present)" },
|
|
476
|
+
limit: { type: "number", description: "Maximum milestones to return (1-20)" }
|
|
343
477
|
},
|
|
344
478
|
required: []
|
|
345
479
|
}
|
|
@@ -349,13 +483,18 @@ export const toolsSchema = [
|
|
|
349
483
|
type: "function",
|
|
350
484
|
function: {
|
|
351
485
|
name: "get_awards",
|
|
352
|
-
description:
|
|
486
|
+
description: `Retrieve official acting and music awards, nominations, rankings, and festival honors (e.g., ERC Chinese Top Ten Awards / 东方风云榜, Weibo TV & Video Summit, Scream Night, Weibo Night).
|
|
487
|
+
[CRITICAL]: Use this tool for ANY question asking which song, role, or album WON an award or was honored at a music chart ceremony. DO NOT use get_media_links or search_discography for award verification.`,
|
|
353
488
|
parameters: {
|
|
354
489
|
type: "object",
|
|
355
490
|
properties: {
|
|
356
|
-
category: {
|
|
357
|
-
|
|
358
|
-
|
|
491
|
+
category: {
|
|
492
|
+
type: "string",
|
|
493
|
+
enum: ["Music", "Acting", "Breakthrough", "Fashion", "All"],
|
|
494
|
+
description: "Category of the award"
|
|
495
|
+
},
|
|
496
|
+
year: { type: "number", description: "Year of the award ceremony" },
|
|
497
|
+
query: { type: "string", description: "Ceremony name, song title, or drama title (e.g., 东方风云榜, 恋爱告急, In Blossom)" },
|
|
359
498
|
limit: { type: "number", description: "Maximum results from 1 to 20" }
|
|
360
499
|
},
|
|
361
500
|
required: []
|
|
@@ -381,12 +520,13 @@ export const toolsSchema = [
|
|
|
381
520
|
type: "function",
|
|
382
521
|
function: {
|
|
383
522
|
name: "get_gallery_photos",
|
|
384
|
-
description:
|
|
523
|
+
description: `Retrieve magazine editorial photoshoots, fashion pictorials, desert/studio spreads, and runway styling (e.g., PORT Magazine / 《PORT》九月刊, Harper's BAZAAR, Milan Fashion Week outfits).
|
|
524
|
+
[CRITICAL]: Use this tool when queries ask about clothing, colors, jewelry (e.g., FRED Force 10 in PORT photoshoot), or visual styling featured IN A MAGAZINE OR PHOTO SET. DO NOT call get_brand_endorsements unless the query specifically asks for commercial brand ambassadorship contracts.`,
|
|
385
525
|
parameters: {
|
|
386
526
|
type: "object",
|
|
387
527
|
properties: {
|
|
388
528
|
category: { type: "string", description: "Category (fashion-magazine, red-carpet, drama-stills)" },
|
|
389
|
-
query: { type: "string", description: "
|
|
529
|
+
query: { type: "string", description: "Magazine name, theme, jewelry collection, or outfit color (e.g., PORT, FRED, Milan, pink)" },
|
|
390
530
|
year: { type: "number", description: "Year filter" },
|
|
391
531
|
limit: { type: "number", description: "Number of photos from 1 to 20" }
|
|
392
532
|
},
|
|
@@ -398,11 +538,14 @@ export const toolsSchema = [
|
|
|
398
538
|
type: "function",
|
|
399
539
|
function: {
|
|
400
540
|
name: "global_search",
|
|
401
|
-
description:
|
|
541
|
+
description: `STRICT FALLBACK TOOL ONLY.
|
|
542
|
+
[NEGATIVE CONSTRAINT]: FORBIDDEN to use on the first turn or for specific entity queries (songs, works, awards, photos, endorsements).
|
|
543
|
+
[PERMITTED ONLY WHEN]: Domain-specific tools (search_works, search_discography, get_awards, etc.) return 0 results or count == 0.
|
|
544
|
+
Searches raw text catalog across all unindexed cross-domain documents.`,
|
|
402
545
|
parameters: {
|
|
403
546
|
type: "object",
|
|
404
547
|
properties: {
|
|
405
|
-
query: { type: "string", description: "
|
|
548
|
+
query: { type: "string", description: "The exact fallback search string" }
|
|
406
549
|
},
|
|
407
550
|
required: ["query"]
|
|
408
551
|
}
|
|
@@ -531,6 +674,113 @@ export const toolsSchema = [
|
|
|
531
674
|
}
|
|
532
675
|
}
|
|
533
676
|
},
|
|
677
|
+
{
|
|
678
|
+
type: "function",
|
|
679
|
+
function: {
|
|
680
|
+
name: "search_news",
|
|
681
|
+
description: "Search live news via Google News RSS + Bing News RSS (supports Thai, Chinese, English automatically). PREFERRED over web_search whenever the user asks about news, latest updates, recent events, or what is new - results include publish dates. Append 'when:7d' to the query to narrow to the last N days.",
|
|
682
|
+
parameters: {
|
|
683
|
+
type: "object",
|
|
684
|
+
properties: {
|
|
685
|
+
query: { type: "string", description: "News search query, e.g. '鞠婧祎 最新' or 'Ju Jingyi news'" },
|
|
686
|
+
maxResults: { type: "number", description: "Max results (default 8, max 15)" }
|
|
687
|
+
},
|
|
688
|
+
required: ["query"]
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
type: "function",
|
|
694
|
+
function: {
|
|
695
|
+
name: "fetch_rss",
|
|
696
|
+
description: "Fetch and parse any public RSS/Atom feed URL (news sites, blogs, Sina/Weibo feeds, etc). Returns feed items with title, link, publish date and snippet.",
|
|
697
|
+
parameters: {
|
|
698
|
+
type: "object",
|
|
699
|
+
properties: {
|
|
700
|
+
url: { type: "string", description: "Full HTTP/HTTPS URL of the RSS/Atom feed" },
|
|
701
|
+
maxItems: { type: "number", description: "Max items to return (default 10, max 30)" }
|
|
702
|
+
},
|
|
703
|
+
required: ["url"]
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
type: "function",
|
|
709
|
+
function: {
|
|
710
|
+
name: "search_encyclopedia",
|
|
711
|
+
description: "Look up encyclopedia entries: Baidu Baike (百度百科) for Chinese terms, Wikipedia for Thai/English. Returns a structured summary with title, text, thumbnail and page URL. Use for background/biography/profile depth beyond the local catalog.",
|
|
712
|
+
parameters: {
|
|
713
|
+
type: "object",
|
|
714
|
+
properties: {
|
|
715
|
+
term: { type: "string", description: "Encyclopedia term to look up, e.g. '鞠婧祎', 'Ju Jingyi', 'SNH48'" },
|
|
716
|
+
language: { type: "string", enum: ["zh", "th", "en"], description: "Optional: force language. Defaults to auto-detect from the term." },
|
|
717
|
+
maxEntries: { type: "number", description: "Max encyclopedia entries to return (default 2, max 3)" }
|
|
718
|
+
},
|
|
719
|
+
required: ["term"]
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
type: "function",
|
|
725
|
+
function: {
|
|
726
|
+
name: "search_music_platforms",
|
|
727
|
+
description: "Search international music platforms (iTunes Search API + MusicBrainz, no API key needed) for songs/albums. Complements the local discography catalog with global platform data, previews and release metadata.",
|
|
728
|
+
parameters: {
|
|
729
|
+
type: "object",
|
|
730
|
+
properties: {
|
|
731
|
+
term: { type: "string", description: "Song/album/artist search term, e.g. 'Ju Jingyi' or '叹云兮'" },
|
|
732
|
+
maxResults: { type: "number", description: "Max results (default 8, max 20)" }
|
|
733
|
+
},
|
|
734
|
+
required: ["term"]
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
type: "function",
|
|
740
|
+
function: {
|
|
741
|
+
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. Requires KIKU_ENABLE_CN_WEB_TOOLS=true.",
|
|
743
|
+
parameters: {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties: {
|
|
746
|
+
query: { type: "string", description: "Chinese search query, e.g. '鞠婧祎 微博'" },
|
|
747
|
+
maxResults: { type: "number", description: "Max results (default 8, max 15)" },
|
|
748
|
+
site: { type: "string", description: "Optional domain hint to restrict fallback search, e.g. 'sina.com.cn'" }
|
|
749
|
+
},
|
|
750
|
+
required: ["query"]
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
type: "function",
|
|
756
|
+
function: {
|
|
757
|
+
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. Requires KIKU_ENABLE_CN_WEB_TOOLS=true.",
|
|
759
|
+
parameters: {
|
|
760
|
+
type: "object",
|
|
761
|
+
properties: {
|
|
762
|
+
keyword: { type: "string", description: "Bilibili search keyword, e.g. '鞠婧祎 舞台'" },
|
|
763
|
+
maxResults: { type: "number", description: "Max results (default 8, max 20)" }
|
|
764
|
+
},
|
|
765
|
+
required: ["keyword"]
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
type: "function",
|
|
771
|
+
function: {
|
|
772
|
+
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. Requires KIKU_ENABLE_CN_WEB_TOOLS=true.",
|
|
774
|
+
parameters: {
|
|
775
|
+
type: "object",
|
|
776
|
+
properties: {
|
|
777
|
+
query: { type: "string", description: "Image search keyword or topic, e.g. '鞠婧祎 路透', '万花世界 剧照', 'Ju Jingyi red carpet'" },
|
|
778
|
+
maxResults: { type: "number", description: "Max images to return (default 8, max 20)" }
|
|
779
|
+
},
|
|
780
|
+
required: ["query"]
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
},
|
|
534
784
|
{
|
|
535
785
|
type: "function",
|
|
536
786
|
function: {
|
|
@@ -687,7 +937,7 @@ export const toolsSchema = [
|
|
|
687
937
|
type: "function",
|
|
688
938
|
function: {
|
|
689
939
|
name: "get_current_track",
|
|
690
|
-
description: "Get information about the currently playing or paused track.",
|
|
940
|
+
description: "Get information about the currently playing or paused track only. For the full player state (queue, volume, mute) use get_music_status instead.",
|
|
691
941
|
parameters: { type: "object", properties: {} }
|
|
692
942
|
}
|
|
693
943
|
},
|
|
@@ -695,7 +945,7 @@ export const toolsSchema = [
|
|
|
695
945
|
type: "function",
|
|
696
946
|
function: {
|
|
697
947
|
name: "get_music_status",
|
|
698
|
-
description: "Get
|
|
948
|
+
description: "Get FULL music player status in one call: playing/paused state, current track, queue contents, volume, and mute state. Prefer this over get_current_track.",
|
|
699
949
|
parameters: { type: "object", properties: {} }
|
|
700
950
|
}
|
|
701
951
|
},
|
|
@@ -805,6 +1055,139 @@ export const toolsSchema = [
|
|
|
805
1055
|
properties: {}
|
|
806
1056
|
}
|
|
807
1057
|
}
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
type: "function",
|
|
1061
|
+
function: {
|
|
1062
|
+
name: "get_entertainment_metrics",
|
|
1063
|
+
description: "Query professional Chinese entertainment industry big data platforms (Vlinkage, DataWin, Guduo, Yunhe, Maoyan Pro, Dengta, Weibo, Douban, Xiaohongshu), popularity index, effective streaming views, and Ju Jingyi commercial benchmarks.",
|
|
1064
|
+
parameters: {
|
|
1065
|
+
type: "object",
|
|
1066
|
+
properties: {
|
|
1067
|
+
platform: {
|
|
1068
|
+
type: "string",
|
|
1069
|
+
enum: ["all", "vlinkage", "datawin", "guduo", "yunhe", "maoyan", "dengta", "weibo", "douban", "xiaohongshu"],
|
|
1070
|
+
description: "Specific platform ID or 'all'."
|
|
1071
|
+
},
|
|
1072
|
+
category: {
|
|
1073
|
+
type: "string",
|
|
1074
|
+
enum: ["all", "actor_drama_index", "streaming_boxoffice", "social_commercial"],
|
|
1075
|
+
description: "Category pillar filter."
|
|
1076
|
+
},
|
|
1077
|
+
query: {
|
|
1078
|
+
type: "string",
|
|
1079
|
+
description: "Optional search query to match platform name, metrics, or drama title (e.g. 'In Blossom', 'Yunhe', 'Vlinkage', 'Commercial')."
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
required: []
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
type: "function",
|
|
1088
|
+
function: {
|
|
1089
|
+
name: "get_industry_index",
|
|
1090
|
+
description: "DEPRECATED ALIAS — identical to get_entertainment_metrics. Call get_entertainment_metrics instead; kept only for backward compatibility.",
|
|
1091
|
+
parameters: {
|
|
1092
|
+
type: "object",
|
|
1093
|
+
properties: {
|
|
1094
|
+
platform: {
|
|
1095
|
+
type: "string",
|
|
1096
|
+
enum: ["all", "vlinkage", "datawin", "guduo", "yunhe", "maoyan", "dengta", "weibo", "douban", "xiaohongshu"],
|
|
1097
|
+
description: "Specific platform ID or 'all'."
|
|
1098
|
+
},
|
|
1099
|
+
category: {
|
|
1100
|
+
type: "string",
|
|
1101
|
+
enum: ["all", "actor_drama_index", "streaming_boxoffice", "social_commercial"],
|
|
1102
|
+
description: "Category pillar filter."
|
|
1103
|
+
},
|
|
1104
|
+
query: {
|
|
1105
|
+
type: "string",
|
|
1106
|
+
description: "Optional search query to match platform name, metrics, or drama title."
|
|
1107
|
+
}
|
|
1108
|
+
},
|
|
1109
|
+
required: []
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
type: "function",
|
|
1115
|
+
function: {
|
|
1116
|
+
name: "checkpoint_step",
|
|
1117
|
+
description: "Record immediate progress and state checkpoint for an active run. Updates kiku/runs/{run_id}/state and plan checklist.",
|
|
1118
|
+
parameters: {
|
|
1119
|
+
type: "object",
|
|
1120
|
+
properties: {
|
|
1121
|
+
run_id: { type: "string", description: "The active run identifier (e.g. run_123)." },
|
|
1122
|
+
step_name: { type: "string", description: "Name of the subtask or step being executed." },
|
|
1123
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "paused", "waiting_user"], description: "Current execution status of the step." },
|
|
1124
|
+
summary: { type: "string", description: "Concise summary of step execution or outcome." }
|
|
1125
|
+
},
|
|
1126
|
+
required: ["run_id", "step_name", "status", "summary"]
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
type: "function",
|
|
1132
|
+
function: {
|
|
1133
|
+
name: "record_failure",
|
|
1134
|
+
description: "Record an error trace and tactical recovery action during task execution for episodic reflection.",
|
|
1135
|
+
parameters: {
|
|
1136
|
+
type: "object",
|
|
1137
|
+
properties: {
|
|
1138
|
+
run_id: { type: "string", description: "The active run identifier." },
|
|
1139
|
+
error_trace: { type: "string", description: "Error message or stack trace describing what failed." },
|
|
1140
|
+
recovery_action: { type: "string", description: "Tactical recovery action or workaround taken." },
|
|
1141
|
+
tool_name: { type: "string", description: "Optional name of the tool that encountered the failure." }
|
|
1142
|
+
},
|
|
1143
|
+
required: ["run_id", "error_trace", "recovery_action"]
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
type: "function",
|
|
1149
|
+
function: {
|
|
1150
|
+
name: "load_active_run",
|
|
1151
|
+
description: "Load active run state, execution plan, and checkpoint history for task hydration and resuming.",
|
|
1152
|
+
parameters: {
|
|
1153
|
+
type: "object",
|
|
1154
|
+
properties: {
|
|
1155
|
+
run_id: { type: "string", description: "The run identifier to load." }
|
|
1156
|
+
},
|
|
1157
|
+
required: ["run_id"]
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
type: "function",
|
|
1163
|
+
function: {
|
|
1164
|
+
name: "record_dynamic_event",
|
|
1165
|
+
description: "Record a newly verified live event, brand activity, appearance, or work into the local dynamic catalog overlay. Persists immediately across restarts without modifying static source files.",
|
|
1166
|
+
parameters: {
|
|
1167
|
+
type: "object",
|
|
1168
|
+
properties: {
|
|
1169
|
+
category: {
|
|
1170
|
+
type: "string",
|
|
1171
|
+
enum: ["brands", "timeline", "gallery", "works", "events"],
|
|
1172
|
+
description: "Catalog category to record this event into."
|
|
1173
|
+
},
|
|
1174
|
+
entry: {
|
|
1175
|
+
type: "object",
|
|
1176
|
+
description: "The structured catalog entry to record (must match category schema, e.g. year, title/brand, etc.)."
|
|
1177
|
+
},
|
|
1178
|
+
source: {
|
|
1179
|
+
type: "string",
|
|
1180
|
+
description: "Origin source URL or platform (e.g. Douyin, Weibo, Official Press)."
|
|
1181
|
+
},
|
|
1182
|
+
trustTier: {
|
|
1183
|
+
type: "number",
|
|
1184
|
+
enum: [1, 2, 3],
|
|
1185
|
+
description: "Trust tier of the information source (1=Official, 2=Press, 3=Community/Other)."
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
required: ["category", "entry"]
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
808
1191
|
}
|
|
809
1192
|
];
|
|
810
1193
|
|
|
@@ -816,10 +1199,22 @@ const TOOL_DEFAULT_TIMEOUTS = {
|
|
|
816
1199
|
suggest_topics: 120_000,
|
|
817
1200
|
web_extract: 90_000,
|
|
818
1201
|
screenshot_webpage: 90_000,
|
|
1202
|
+
get_youtube_transcript: 90_000,
|
|
819
1203
|
search_youtube: 60_000,
|
|
820
1204
|
play_music: 60_000,
|
|
1205
|
+
search_news: 45_000,
|
|
1206
|
+
fetch_rss: 30_000,
|
|
1207
|
+
search_encyclopedia: 45_000,
|
|
1208
|
+
search_music_platforms: 45_000,
|
|
1209
|
+
search_chinese_web: 60_000,
|
|
1210
|
+
search_bilibili: 45_000,
|
|
1211
|
+
search_web_images: 60_000,
|
|
821
1212
|
};
|
|
822
1213
|
|
|
1214
|
+
const isCnWebEnabled = () => process.env.KIKU_ENABLE_CN_WEB_TOOLS === 'true';
|
|
1215
|
+
const cnWebDisabledMessage = (toolName) =>
|
|
1216
|
+
`"${toolName}" is disabled. Set KIKU_ENABLE_CN_WEB_TOOLS=true 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
|
+
|
|
823
1218
|
async function ensureLlmAvailable(client) {
|
|
824
1219
|
if (!client || typeof client.checkConnection !== 'function') return null;
|
|
825
1220
|
try {
|
|
@@ -837,26 +1232,56 @@ async function ensureLlmAvailable(client) {
|
|
|
837
1232
|
return null;
|
|
838
1233
|
}
|
|
839
1234
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
];
|
|
1235
|
+
import { RUMORED_OR_EXTERNAL_PROJECTS } from './rumoredProjects.js';
|
|
1236
|
+
|
|
1237
|
+
export { RUMORED_OR_EXTERNAL_PROJECTS };
|
|
854
1238
|
|
|
855
1239
|
export const toolHandlers = {
|
|
856
|
-
|
|
857
|
-
const
|
|
1240
|
+
get_current_datetime: async () => {
|
|
1241
|
+
const now = new Date();
|
|
1242
|
+
const offsetMinutes = -now.getTimezoneOffset();
|
|
1243
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
1244
|
+
const absOffset = Math.abs(offsetMinutes);
|
|
1245
|
+
const timezoneOffset = `${sign}${String(Math.floor(absOffset / 60)).padStart(2, '0')}:${String(absOffset % 60).padStart(2, '0')}`;
|
|
1246
|
+
let timezoneName = null;
|
|
1247
|
+
try {
|
|
1248
|
+
timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
1249
|
+
} catch { }
|
|
858
1250
|
return {
|
|
859
1251
|
status: "success",
|
|
1252
|
+
iso: now.toISOString(),
|
|
1253
|
+
local: now.toLocaleString('en-GB', { hour12: false }),
|
|
1254
|
+
timezoneOffset,
|
|
1255
|
+
timezoneName,
|
|
1256
|
+
weekday: now.toLocaleDateString('en-US', { weekday: 'long' }),
|
|
1257
|
+
year: now.getFullYear(),
|
|
1258
|
+
month: now.getMonth() + 1,
|
|
1259
|
+
day: now.getDate(),
|
|
1260
|
+
timestampMs: now.getTime()
|
|
1261
|
+
};
|
|
1262
|
+
},
|
|
1263
|
+
|
|
1264
|
+
get_youtube_transcript: async (args = {}) => fetchYoutubeTranscript(args),
|
|
1265
|
+
|
|
1266
|
+
get_profile_and_fandom: async ({ section, query } = {}) => {
|
|
1267
|
+
const d = profileData.details || {};
|
|
1268
|
+
const qStr = String(query || '').toLowerCase().trim();
|
|
1269
|
+
const isCatQuery = /(cat|cats|แมว|สัตว์เลี้ยง|pet|pets|miaomiao)/i.test(qStr);
|
|
1270
|
+
|
|
1271
|
+
if (isCatQuery) {
|
|
1272
|
+
return {
|
|
1273
|
+
status: "success",
|
|
1274
|
+
query: query || "cats",
|
|
1275
|
+
nameTh: profileData.nameTh,
|
|
1276
|
+
nameCn: profileData.nameCn,
|
|
1277
|
+
petsSummary: d.petsSummary || "แมว 3 ตัว: ฮวาฮวา (Huahua), เมี่ยวเมี่ยว (Miaomiao), เลิฟเวอร์ (Lover)",
|
|
1278
|
+
cats: profileData.cats || []
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const result = {
|
|
1283
|
+
status: "success",
|
|
1284
|
+
query: query || undefined,
|
|
860
1285
|
profile: {
|
|
861
1286
|
nameTh: profileData.nameTh,
|
|
862
1287
|
nameCn: profileData.nameCn,
|
|
@@ -878,10 +1303,28 @@ export const toolHandlers = {
|
|
|
878
1303
|
cats: profileData.cats || [],
|
|
879
1304
|
fandomName: d.fandomName || "มี่จวี (Miju / 蜜橘)",
|
|
880
1305
|
fandomColor: d.fandomColorName || "ฟ้าคริสตัล & ชมพูซากุระ",
|
|
881
|
-
slogan: profileData.motto || "保持热爱,奔赴山海"
|
|
882
|
-
youtubeSearchUrl: buildYouTubeSearchUrl("Ju Jingyi 鞠婧祎")
|
|
1306
|
+
slogan: profileData.motto || "保持热爱,奔赴山海"
|
|
883
1307
|
}
|
|
884
1308
|
};
|
|
1309
|
+
|
|
1310
|
+
const requested = String(section || 'all').toLowerCase();
|
|
1311
|
+
if (requested === 'talents' || requested === 'all') {
|
|
1312
|
+
result.talents = (profileData.talents || []).map(t => ({
|
|
1313
|
+
id: t.id, title: t.title, titleCn: t.titleCn, level: t.level, description: t.description
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
if (requested === 'stats' || requested === 'all') {
|
|
1317
|
+
result.stats = profileData.stats || [];
|
|
1318
|
+
}
|
|
1319
|
+
if (requested === 'honors' || requested === 'all') {
|
|
1320
|
+
result.honors = profileData.forbesHonors || [];
|
|
1321
|
+
}
|
|
1322
|
+
if (requested === 'social' || requested === 'all') {
|
|
1323
|
+
result.socialLinks = (profileData.socialLinks || []).map(s => ({
|
|
1324
|
+
name: s.name, url: s.url, username: s.username, type: s.type, badge: s.badge
|
|
1325
|
+
}));
|
|
1326
|
+
}
|
|
1327
|
+
return result;
|
|
885
1328
|
},
|
|
886
1329
|
|
|
887
1330
|
get_quotes: async ({ tag } = {}) => {
|
|
@@ -907,20 +1350,44 @@ export const toolHandlers = {
|
|
|
907
1350
|
},
|
|
908
1351
|
|
|
909
1352
|
search_works: async ({ query, year, category, featuredOnly } = {}) => {
|
|
910
|
-
|
|
1353
|
+
const dynamicWorks = getDynamicEntries('works');
|
|
1354
|
+
const baseWorks = dynamicWorks && dynamicWorks.length > 0 ? [...dynamicWorks, ...worksData] : [...worksData];
|
|
1355
|
+
let results = [...baseWorks];
|
|
911
1356
|
|
|
912
1357
|
if (year) results = results.filter(w => w.year === Number(year));
|
|
913
|
-
|
|
1358
|
+
const categoryFilter = normalizeWorkCategoryFilter(category);
|
|
1359
|
+
if (categoryFilter) results = results.filter(w => categoryFilter.includes(w.category));
|
|
914
1360
|
|
|
1361
|
+
let broadMatch = false;
|
|
1362
|
+
let matcher = null;
|
|
915
1363
|
if (query) {
|
|
916
|
-
|
|
917
|
-
|
|
1364
|
+
matcher = createCatalogMatcher(query);
|
|
1365
|
+
if (matcher.broad) {
|
|
1366
|
+
broadMatch = true;
|
|
1367
|
+
} else {
|
|
1368
|
+
results = results.filter(w => matcher(
|
|
1369
|
+
w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis,
|
|
1370
|
+
...(w.aliases || []), ...(w.keywords || [])
|
|
1371
|
+
));
|
|
1372
|
+
}
|
|
918
1373
|
}
|
|
919
1374
|
|
|
920
1375
|
if (featuredOnly && results.some(w => !!w.featured)) {
|
|
921
1376
|
results = results.filter(w => !!w.featured);
|
|
922
1377
|
}
|
|
923
1378
|
|
|
1379
|
+
if (results.length === 0 && query && (categoryFilter || year)) {
|
|
1380
|
+
let relaxed = [...baseWorks];
|
|
1381
|
+
const fallbackMatcher = matcher || createCatalogMatcher(query);
|
|
1382
|
+
relaxed = relaxed.filter(w => fallbackMatcher(
|
|
1383
|
+
w.titleTh, w.titleEn, w.titleCn, w.role, w.roleCn, w.synopsis,
|
|
1384
|
+
...(w.aliases || []), ...(w.keywords || [])
|
|
1385
|
+
));
|
|
1386
|
+
if (relaxed.length > 0) {
|
|
1387
|
+
results = relaxed;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
924
1391
|
if (results.length === 0 && query) {
|
|
925
1392
|
const qLower = String(query).toLowerCase().trim();
|
|
926
1393
|
const rumored = RUMORED_OR_EXTERNAL_PROJECTS.find(p => p.keywords.some(k => qLower.includes(k)));
|
|
@@ -937,7 +1404,7 @@ export const toolHandlers = {
|
|
|
937
1404
|
|
|
938
1405
|
results.sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
939
1406
|
|
|
940
|
-
|
|
1407
|
+
const result = {
|
|
941
1408
|
status: "success",
|
|
942
1409
|
count: results.length,
|
|
943
1410
|
works: results.slice(0, 6).map(w => {
|
|
@@ -950,14 +1417,15 @@ export const toolHandlers = {
|
|
|
950
1417
|
titleEn: w.titleEn,
|
|
951
1418
|
year: w.year,
|
|
952
1419
|
category: w.category,
|
|
953
|
-
role: w.role,
|
|
954
|
-
roleCn: w.roleCn,
|
|
955
|
-
synopsis: w.synopsis || null,
|
|
956
|
-
highlights: (w.highlights || []).slice(0, 3),
|
|
1420
|
+
role: w.role,
|
|
1421
|
+
roleCn: w.roleCn,
|
|
1422
|
+
synopsis: w.synopsis || null,
|
|
1423
|
+
highlights: (w.highlights || []).slice(0, 3),
|
|
957
1424
|
episodes: w.episodes,
|
|
958
1425
|
rating: w.rating,
|
|
959
1426
|
platform: w.streamPlatform,
|
|
960
1427
|
streamUrl: w.streamUrl || null,
|
|
1428
|
+
poster: w.poster || null,
|
|
961
1429
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`${titleSearch} Trailer Official`),
|
|
962
1430
|
youtubeTrailerIsDirectLink: false,
|
|
963
1431
|
youkuSearchUrl: buildYoukuSearchUrl(w.titleCn || w.titleEn),
|
|
@@ -968,22 +1436,33 @@ export const toolHandlers = {
|
|
|
968
1436
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.themeSong.titleCn || w.themeSong.titleTh} OST`)
|
|
969
1437
|
} : null
|
|
970
1438
|
};
|
|
971
|
-
})
|
|
1439
|
+
}),
|
|
1440
|
+
images: results.slice(0, 6).map(w => ({
|
|
1441
|
+
title: `${w.titleTh || w.titleCn} (${w.year})`,
|
|
1442
|
+
imageUrl: w.poster || null,
|
|
1443
|
+
thumbUrl: w.poster || null,
|
|
1444
|
+
source: 'Series Poster'
|
|
1445
|
+
})).filter(img => img.imageUrl)
|
|
972
1446
|
};
|
|
1447
|
+
if (broadMatch) {
|
|
1448
|
+
result.message = `Broad query "${query}": every search term was generic, so no title/character filter was applied. The works below are the newest catalog entries, not an exact match for "${query}" — say so explicitly if you use them.`;
|
|
1449
|
+
}
|
|
1450
|
+
return result;
|
|
973
1451
|
},
|
|
974
1452
|
|
|
975
1453
|
get_work_detail: async ({ workIdOrTitle }) => {
|
|
976
1454
|
if (!workIdOrTitle) return { status: "error", message: "workIdOrTitle parameter is required." };
|
|
977
1455
|
const q = workIdOrTitle.toLowerCase().trim();
|
|
978
1456
|
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1457
|
+
const qId = normalizeWorkId(workIdOrTitle);
|
|
1458
|
+
let work = WORKS_BY_NORM_ID.get(qId) || null;
|
|
1459
|
+
if (!work) {
|
|
1460
|
+
work = worksData.find(w =>
|
|
1461
|
+
w.titleTh.toLowerCase().includes(q) ||
|
|
1462
|
+
(w.titleEn && w.titleEn.toLowerCase().includes(q)) ||
|
|
1463
|
+
(w.titleCn && w.titleCn.includes(workIdOrTitle))
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
987
1466
|
if (!work) {
|
|
988
1467
|
|
|
989
1468
|
const matcher = createCatalogMatcher(workIdOrTitle);
|
|
@@ -1006,8 +1485,8 @@ export const toolHandlers = {
|
|
|
1006
1485
|
titleEn: work.titleEn,
|
|
1007
1486
|
year: work.year,
|
|
1008
1487
|
category: work.category,
|
|
1009
|
-
role: work.role,
|
|
1010
|
-
roleCn: work.roleCn,
|
|
1488
|
+
role: work.role,
|
|
1489
|
+
roleCn: work.roleCn,
|
|
1011
1490
|
episodes: work.episodes,
|
|
1012
1491
|
rating: work.rating,
|
|
1013
1492
|
platform: work.streamPlatform,
|
|
@@ -1016,7 +1495,7 @@ export const toolHandlers = {
|
|
|
1016
1495
|
youtubeTrailerIsDirectLink: false,
|
|
1017
1496
|
youkuSearchUrl: buildYoukuSearchUrl(work.titleCn || work.titleEn),
|
|
1018
1497
|
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${work.titleCn}`),
|
|
1019
|
-
synopsis: work.synopsis || null,
|
|
1498
|
+
synopsis: work.synopsis || null,
|
|
1020
1499
|
highlights: (work.highlights || []).slice(0, 3),
|
|
1021
1500
|
themeSong: work.themeSong ? {
|
|
1022
1501
|
title: formatCatalogTitle(work.themeSong.titleTh, work.themeSong.titleCn),
|
|
@@ -1029,7 +1508,26 @@ export const toolHandlers = {
|
|
|
1029
1508
|
},
|
|
1030
1509
|
|
|
1031
1510
|
get_character_roles: async ({ query } = {}) => {
|
|
1032
|
-
|
|
1511
|
+
if (query) {
|
|
1512
|
+
const qLower = String(query).toLowerCase();
|
|
1513
|
+
const isPersonal = PERSONAL_FACT_KEYWORDS.some(k => qLower.includes(k));
|
|
1514
|
+
if (isPersonal) {
|
|
1515
|
+
return {
|
|
1516
|
+
status: "misdirected",
|
|
1517
|
+
count: 0,
|
|
1518
|
+
roles: [],
|
|
1519
|
+
message: "get_character_roles contains fictional drama and movie characters only. For Ju Jingyi's personal facts, birthplace, education, musical instruments, blood type, or biography, call get_profile_and_fandom."
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
let matchedWorks = worksData;
|
|
1525
|
+
if (query) {
|
|
1526
|
+
const matcher = createCatalogMatcher(query);
|
|
1527
|
+
matchedWorks = worksData.filter(w => matcher(w.role, w.roleCn, formatCatalogTitle(w.titleTh, w.titleCn), w.titleTh, w.titleCn));
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const filtered = matchedWorks.map(w => ({
|
|
1033
1531
|
workTitle: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
1034
1532
|
year: w.year,
|
|
1035
1533
|
role: w.role,
|
|
@@ -1037,12 +1535,6 @@ export const toolHandlers = {
|
|
|
1037
1535
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.roleCn || w.role} ${w.titleCn}`)
|
|
1038
1536
|
}));
|
|
1039
1537
|
|
|
1040
|
-
let filtered = roles;
|
|
1041
|
-
if (query) {
|
|
1042
|
-
const matcher = createCatalogMatcher(query);
|
|
1043
|
-
filtered = roles.filter(r => matcher(r.role, r.roleCn, r.workTitle));
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
1538
|
filtered.sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
1047
1539
|
|
|
1048
1540
|
return {
|
|
@@ -1053,7 +1545,9 @@ export const toolHandlers = {
|
|
|
1053
1545
|
},
|
|
1054
1546
|
|
|
1055
1547
|
search_discography: async ({ query, year, type } = {}) => {
|
|
1056
|
-
let
|
|
1548
|
+
let rawList = getCleanDiscography();
|
|
1549
|
+
let list = [...rawList];
|
|
1550
|
+
let broadMatch = false;
|
|
1057
1551
|
|
|
1058
1552
|
if (year) list = list.filter(s => s.year === Number(year));
|
|
1059
1553
|
if (type === 'ost') list = list.filter(s => s.work && s.work !== "Solo Single");
|
|
@@ -1071,20 +1565,99 @@ export const toolHandlers = {
|
|
|
1071
1565
|
};
|
|
1072
1566
|
}
|
|
1073
1567
|
const matcher = createCatalogMatcher(query);
|
|
1074
|
-
|
|
1568
|
+
if (matcher.broad) {
|
|
1569
|
+
broadMatch = true;
|
|
1570
|
+
} else {
|
|
1571
|
+
list = list.filter(s => matcher(
|
|
1572
|
+
s.titleTh, s.titleCn, s.titleEn, s.work, s.role, s.description,
|
|
1573
|
+
...(s.aliases || []), ...(s.keywords || [])
|
|
1574
|
+
));
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
if (list.length === 0 && (type || year)) {
|
|
1578
|
+
let relaxed = [...rawList];
|
|
1579
|
+
relaxed = relaxed.filter(s => matcher(
|
|
1580
|
+
s.titleTh, s.titleCn, s.titleEn, s.work, s.role, s.description,
|
|
1581
|
+
...(s.aliases || []), ...(s.keywords || [])
|
|
1582
|
+
));
|
|
1583
|
+
if (relaxed.length > 0) {
|
|
1584
|
+
list = relaxed;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const qClean = qLower;
|
|
1589
|
+
const scoredList = list.map(item => {
|
|
1590
|
+
const titles = [item.titleEn, item.titleCn, item.titleTh, ...(item.aliases || [])];
|
|
1591
|
+
let score = 0;
|
|
1592
|
+
for (const t of titles) {
|
|
1593
|
+
if (!t) continue;
|
|
1594
|
+
const lower = String(t).toLowerCase().trim();
|
|
1595
|
+
if (lower === qClean) {
|
|
1596
|
+
score = 2;
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1599
|
+
if (score < 1 && lower.includes(qClean)) {
|
|
1600
|
+
score = 1;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return { item, score };
|
|
1604
|
+
});
|
|
1605
|
+
scoredList.sort((a, b) => b.score - a.score);
|
|
1606
|
+
list = scoredList.map(s => s.item);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (list.length === 0 && query) {
|
|
1610
|
+
const allMVs = getAllMusicVideos();
|
|
1611
|
+
const mvMatcher = createCatalogMatcher(query);
|
|
1612
|
+
const mvMatches = allMVs.filter(m => mvMatcher(
|
|
1613
|
+
m.titleTh, m.titleCn, m.titlePinyin, m.dramaName, m.description,
|
|
1614
|
+
...(m.aliases || []), ...(m.keywords || [])
|
|
1615
|
+
));
|
|
1616
|
+
if (mvMatches.length > 0) {
|
|
1617
|
+
return {
|
|
1618
|
+
status: "success",
|
|
1619
|
+
source: "musicVideoData",
|
|
1620
|
+
cascadeMatched: true,
|
|
1621
|
+
count: mvMatches.length,
|
|
1622
|
+
message: `Cascade retrieval: Matched in music videos catalog (musicVideoData) for "${query}".`,
|
|
1623
|
+
songs: mvMatches.slice(0, 5).map(m => ({
|
|
1624
|
+
id: m.id,
|
|
1625
|
+
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1626
|
+
titleEn: m.titleCn,
|
|
1627
|
+
titleCn: m.titleCn,
|
|
1628
|
+
year: m.releaseYear || m.year,
|
|
1629
|
+
type: m.category || "Single",
|
|
1630
|
+
work: m.dramaName || "Music Video",
|
|
1631
|
+
category: m.category,
|
|
1632
|
+
duration: m.duration,
|
|
1633
|
+
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
1634
|
+
isDirectLink: Boolean(m.youtubeId),
|
|
1635
|
+
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh}`),
|
|
1636
|
+
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${m.titleCn || m.titleTh}`)
|
|
1637
|
+
}))
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1075
1640
|
}
|
|
1076
1641
|
|
|
1077
1642
|
return {
|
|
1078
1643
|
status: list.length > 0 ? "success" : "not_found",
|
|
1079
|
-
message: list.length > 0
|
|
1644
|
+
message: list.length > 0
|
|
1645
|
+
? (broadMatch ? `Broad query "${query}": every search term was generic, so no title filter was applied. The songs below are the newest catalog entries, not an exact match for "${query}" — say so explicitly if you use them.` : undefined)
|
|
1646
|
+
: `No matching discography entries for "${query || 'this filter'}".`,
|
|
1080
1647
|
count: list.length,
|
|
1081
1648
|
songs: list.slice(0, 8).map(s => ({
|
|
1082
1649
|
id: s.id,
|
|
1083
1650
|
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
1084
1651
|
titleEn: s.titleEn,
|
|
1652
|
+
titleCn: s.titleCn,
|
|
1085
1653
|
year: s.year,
|
|
1086
1654
|
type: s.work,
|
|
1087
|
-
|
|
1655
|
+
work: s.work,
|
|
1656
|
+
category: s.category,
|
|
1657
|
+
categoryName: s.categoryName,
|
|
1658
|
+
role: s.role,
|
|
1659
|
+
description: s.description,
|
|
1660
|
+
duration: s.duration || s.trackDuration,
|
|
1088
1661
|
youtubeUrl: s.youtubeUrl || (s.youtubeId ? buildYouTubeWatchUrl(s.youtubeId) : null),
|
|
1089
1662
|
isDirectLink: Boolean(s.youtubeUrl || s.youtubeId),
|
|
1090
1663
|
youtubeSearchUrl: s.youtubeSearchUrl || buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${s.titleCn || s.titleTh}`),
|
|
@@ -1098,7 +1671,7 @@ export const toolHandlers = {
|
|
|
1098
1671
|
const allMVs = getAllMusicVideos();
|
|
1099
1672
|
|
|
1100
1673
|
let results = allMVs;
|
|
1101
|
-
if (category) {
|
|
1674
|
+
if (category && category.toLowerCase() !== 'all') {
|
|
1102
1675
|
const catLower = category.toLowerCase();
|
|
1103
1676
|
results = results.filter(m => {
|
|
1104
1677
|
const mCat = (m.category || '').toLowerCase();
|
|
@@ -1112,7 +1685,10 @@ export const toolHandlers = {
|
|
|
1112
1685
|
|
|
1113
1686
|
if (query) {
|
|
1114
1687
|
const matcher = createCatalogMatcher(query);
|
|
1115
|
-
results = results.filter(m => matcher(
|
|
1688
|
+
results = results.filter(m => matcher(
|
|
1689
|
+
m.titleTh, m.titleCn, m.titlePinyin, m.dramaName, m.description,
|
|
1690
|
+
...(m.aliases || []), ...(m.keywords || [])
|
|
1691
|
+
));
|
|
1116
1692
|
}
|
|
1117
1693
|
|
|
1118
1694
|
return {
|
|
@@ -1124,12 +1700,14 @@ export const toolHandlers = {
|
|
|
1124
1700
|
return {
|
|
1125
1701
|
id: m.id,
|
|
1126
1702
|
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1703
|
+
titleCn: m.titleCn || null,
|
|
1127
1704
|
category: m.category,
|
|
1128
1705
|
year: m.releaseYear || m.year,
|
|
1129
1706
|
drama: m.dramaName || null,
|
|
1130
1707
|
duration: m.duration || null,
|
|
1131
1708
|
description: m.description || null,
|
|
1132
1709
|
isFeatured: !!m.isFeatured,
|
|
1710
|
+
youtubeId: ytId,
|
|
1133
1711
|
youtubeUrl: ytId ? buildYouTubeWatchUrl(ytId) : null,
|
|
1134
1712
|
isDirectLink: Boolean(ytId),
|
|
1135
1713
|
youtubeSearchUrl: buildYouTubeSearchUrl(searchTitle),
|
|
@@ -1157,6 +1735,8 @@ export const toolHandlers = {
|
|
|
1157
1735
|
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1158
1736
|
year: m.releaseYear || m.year,
|
|
1159
1737
|
duration: m.duration,
|
|
1738
|
+
drama: m.dramaName || null,
|
|
1739
|
+
description: m.description || null,
|
|
1160
1740
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
1161
1741
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh} ${m.category === 'Stage-Performance' ? 'Stage' : 'Official MV'}`),
|
|
1162
1742
|
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${m.titleCn || m.titleTh}`)
|
|
@@ -1170,8 +1750,12 @@ export const toolHandlers = {
|
|
|
1170
1750
|
).map(s => ({
|
|
1171
1751
|
type: s.work || 'Song',
|
|
1172
1752
|
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
1753
|
+
titleEn: s.titleEn || null,
|
|
1173
1754
|
year: s.year,
|
|
1174
1755
|
duration: s.duration,
|
|
1756
|
+
work: s.work || null,
|
|
1757
|
+
role: s.role || null,
|
|
1758
|
+
description: s.description || null,
|
|
1175
1759
|
youtubeUrl: s.youtubeUrl || (s.youtubeId ? buildYouTubeWatchUrl(s.youtubeId) : null),
|
|
1176
1760
|
youtubeSearchUrl: s.youtubeSearchUrl || buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${s.titleCn || s.titleTh}`),
|
|
1177
1761
|
qqMusicSearchUrl: s.qqMusicSearchUrl || buildQqMusicSearchUrl(`鞠婧祎 ${s.titleCn || s.titleTh}`),
|
|
@@ -1186,6 +1770,9 @@ export const toolHandlers = {
|
|
|
1186
1770
|
type: 'Drama / Series',
|
|
1187
1771
|
title: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
1188
1772
|
year: w.year,
|
|
1773
|
+
role: w.role,
|
|
1774
|
+
roleCn: w.roleCn,
|
|
1775
|
+
synopsis: w.synopsis || null,
|
|
1189
1776
|
platform: w.streamPlatform,
|
|
1190
1777
|
streamUrl: w.streamUrl || null,
|
|
1191
1778
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.titleCn || w.titleEn} Trailer Official`),
|
|
@@ -1200,6 +1787,21 @@ export const toolHandlers = {
|
|
|
1200
1787
|
const generalYouTubeSearch = buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${query}`);
|
|
1201
1788
|
const generalBilibiliSearch = buildBilibiliSearchUrl(`鞠婧祎 ${query}`);
|
|
1202
1789
|
|
|
1790
|
+
const brandItems = Array.isArray(brandsData) ? brandsData : (brandsData.brandList || brandsData.brands || []);
|
|
1791
|
+
const matchedBrands = brandItems.filter(b => {
|
|
1792
|
+
const bEn = String(b.brandNameEn || b.brand || b.name || '').toLowerCase();
|
|
1793
|
+
const bCn = String(b.brandNameCn || '');
|
|
1794
|
+
return bEn.includes(q) || (bCn && query.includes(bCn)) || (bCn && bCn.includes(query)) || (b.id && b.id.toLowerCase().includes(q));
|
|
1795
|
+
}).map(b => ({
|
|
1796
|
+
type: 'Brand / Commercial',
|
|
1797
|
+
brand: `${b.brandNameEn || b.brand || b.name} (${b.brandNameCn || ''})`,
|
|
1798
|
+
year: b.year,
|
|
1799
|
+
title: b.title,
|
|
1800
|
+
category: b.category,
|
|
1801
|
+
youtubeCommercialSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${b.brandNameEn || b.brandNameCn} Commercial`),
|
|
1802
|
+
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${b.brandNameCn || b.brandNameEn}`)
|
|
1803
|
+
}));
|
|
1804
|
+
|
|
1203
1805
|
return {
|
|
1204
1806
|
status: "success",
|
|
1205
1807
|
query,
|
|
@@ -1208,57 +1810,164 @@ export const toolHandlers = {
|
|
|
1208
1810
|
results: {
|
|
1209
1811
|
musicVideos: matchedVideos.slice(0, 5),
|
|
1210
1812
|
songs: matchedSongs.slice(0, 5),
|
|
1211
|
-
works: matchedWorks.slice(0, 3)
|
|
1813
|
+
works: matchedWorks.slice(0, 3),
|
|
1814
|
+
brands: matchedBrands.slice(0, 3)
|
|
1212
1815
|
}
|
|
1213
1816
|
};
|
|
1214
1817
|
},
|
|
1215
1818
|
|
|
1216
1819
|
get_brand_endorsements: async ({ isGlobal, category, year, query } = {}) => {
|
|
1217
|
-
|
|
1820
|
+
const dynamicBrands = getDynamicEntries('brands');
|
|
1821
|
+
let rawList = Array.isArray(brandsData) ? [...brandsData] : [...(brandsData.brandList || brandsData.brands || [])];
|
|
1822
|
+
if (dynamicBrands && dynamicBrands.length > 0) {
|
|
1823
|
+
rawList = [...dynamicBrands, ...rawList];
|
|
1824
|
+
}
|
|
1825
|
+
let list = [...rawList];
|
|
1218
1826
|
|
|
1219
1827
|
if (typeof isGlobal === 'boolean') list = list.filter(b => !!b.isGlobal === isGlobal);
|
|
1220
1828
|
if (year) list = list.filter(b => b.year === Number(year));
|
|
1221
|
-
if (category) list = list.filter(b => (b.category && b.category.toLowerCase().includes(category.toLowerCase())) || (b.tier && b.tier.toLowerCase().includes(category.toLowerCase())));
|
|
1222
1829
|
|
|
1830
|
+
const matchesCategory = (b, cat) => {
|
|
1831
|
+
const cLower = String(cat).toLowerCase().trim();
|
|
1832
|
+
if ((b.category && b.category.toLowerCase().includes(cLower)) || (b.tier && b.tier.toLowerCase().includes(cLower))) return true;
|
|
1833
|
+
if (CATEGORY_ALIASES) {
|
|
1834
|
+
for (const [canonical, aliases] of Object.entries(CATEGORY_ALIASES)) {
|
|
1835
|
+
if (cLower.includes(canonical) || aliases.some(a => cLower.includes(a.toLowerCase()))) {
|
|
1836
|
+
const bText = `${b.category || ''} ${b.tier || ''} ${(b.keywords || []).join(' ')} ${(b.aliases || []).join(' ')}`.toLowerCase();
|
|
1837
|
+
if (aliases.some(a => bText.includes(a.toLowerCase()))) return true;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return false;
|
|
1842
|
+
};
|
|
1843
|
+
|
|
1844
|
+
if (category) list = list.filter(b => matchesCategory(b, category));
|
|
1845
|
+
|
|
1846
|
+
let activeMatcher = null;
|
|
1223
1847
|
if (query) {
|
|
1224
1848
|
const numYear = Number(String(query).trim());
|
|
1225
1849
|
if (!isNaN(numYear) && numYear >= 2013 && numYear <= 2030) {
|
|
1226
1850
|
list = list.filter(b => b.year === numYear);
|
|
1227
1851
|
} else {
|
|
1228
|
-
|
|
1229
|
-
list = list.filter(b =>
|
|
1852
|
+
activeMatcher = createCatalogMatcher(query);
|
|
1853
|
+
list = list.filter(b => activeMatcher(
|
|
1854
|
+
b.brandNameEn, b.brandNameCn, b.brand, b.name, b.title, b.category, b.highlight, b.tier,
|
|
1855
|
+
...(b.aliases || []), ...(b.keywords || [])
|
|
1856
|
+
));
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (list.length === 0 && (category || query)) {
|
|
1861
|
+
let relaxed = [...rawList];
|
|
1862
|
+
if (typeof isGlobal === 'boolean') relaxed = relaxed.filter(b => !!b.isGlobal === isGlobal);
|
|
1863
|
+
if (year) relaxed = relaxed.filter(b => b.year === Number(year));
|
|
1864
|
+
const targetQuery = query || category;
|
|
1865
|
+
if (targetQuery) {
|
|
1866
|
+
const matcher = (targetQuery === query && activeMatcher) ? activeMatcher : createCatalogMatcher(targetQuery);
|
|
1867
|
+
relaxed = relaxed.filter(b => matcher(
|
|
1868
|
+
b.brandNameEn, b.brandNameCn, b.brand, b.name, b.title, b.category, b.highlight, b.tier,
|
|
1869
|
+
...(b.aliases || []), ...(b.keywords || [])
|
|
1870
|
+
));
|
|
1871
|
+
if (relaxed.length > 0) {
|
|
1872
|
+
list = relaxed;
|
|
1873
|
+
}
|
|
1230
1874
|
}
|
|
1231
1875
|
}
|
|
1232
1876
|
|
|
1233
1877
|
list.sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
1234
1878
|
|
|
1879
|
+
if (list.length === 0 && query) {
|
|
1880
|
+
const qLower = String(query).toLowerCase().trim();
|
|
1881
|
+
const isMedia = /poi|song|single|video|mv|ost|เพลง|ละคร|ซีรีส์|บทบาท/i.test(qLower);
|
|
1882
|
+
return {
|
|
1883
|
+
status: "not_found",
|
|
1884
|
+
count: 0,
|
|
1885
|
+
brands: [],
|
|
1886
|
+
message: isMedia
|
|
1887
|
+
? `No brand endorsement matching "${query}". "${query}" appears to be a song, music video, or drama work. Please call search_discography, get_music_videos, or search_works instead.`
|
|
1888
|
+
: `No brand endorsements found for query "${query}".`
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const dynamicSales = dynamicBrands ? dynamicBrands.filter(b => b.salesHighlights || b.stat).flatMap(b => b.salesHighlights || [{ brand: b.brandNameEn || b.brand, detail: b.highlight || '', stat: b.stat || '' }]) : [];
|
|
1893
|
+
const allSales = [...dynamicSales, ...(Array.isArray(brandsData?.salesHighlights) ? brandsData.salesHighlights : [])];
|
|
1894
|
+
let relevantSales = allSales;
|
|
1895
|
+
if (query) {
|
|
1896
|
+
const qLower = String(query).toLowerCase().trim();
|
|
1897
|
+
const filtered = allSales.filter(s =>
|
|
1898
|
+
(s.brand && s.brand.toLowerCase().includes(qLower)) ||
|
|
1899
|
+
(s.brandId && s.brandId.toLowerCase().includes(qLower)) ||
|
|
1900
|
+
(s.detail && s.detail.toLowerCase().includes(qLower)) ||
|
|
1901
|
+
(s.stat && s.stat.toLowerCase().includes(qLower))
|
|
1902
|
+
);
|
|
1903
|
+
if (filtered.length > 0) relevantSales = filtered;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1235
1906
|
return {
|
|
1236
1907
|
status: "success",
|
|
1237
1908
|
count: list.length,
|
|
1238
|
-
brands: list.slice(0, 8).map(b =>
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1909
|
+
brands: list.slice(0, 8).map(b => {
|
|
1910
|
+
const brandName = b.brandNameEn || b.brand || b.name || '';
|
|
1911
|
+
const brandCn = b.brandNameCn || '';
|
|
1912
|
+
return {
|
|
1913
|
+
brand: `${brandName} (${brandCn})`.trim(),
|
|
1914
|
+
brandNameEn: brandName,
|
|
1915
|
+
brandNameCn: brandCn,
|
|
1916
|
+
year: b.year,
|
|
1917
|
+
title: b.title || (b.isGlobal ? 'Global Brand Ambassador' : 'Brand Ambassador'),
|
|
1918
|
+
category: b.category,
|
|
1919
|
+
isGlobal: !!b.isGlobal,
|
|
1920
|
+
highlight: b.highlight || '',
|
|
1921
|
+
youtubeCommercialSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${brandName || brandCn} CF Commercial`),
|
|
1922
|
+
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${brandCn || brandName}`)
|
|
1923
|
+
};
|
|
1924
|
+
}),
|
|
1925
|
+
salesHighlights: relevantSales
|
|
1247
1926
|
};
|
|
1248
1927
|
},
|
|
1249
1928
|
|
|
1250
|
-
get_career_timeline: async ({ year, era } = {}) => {
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
if (
|
|
1929
|
+
get_career_timeline: async ({ year, era, limit = 10 } = {}) => {
|
|
1930
|
+
const dynamicTimeline = [...getDynamicEntries('timeline'), ...getDynamicEntries('events')];
|
|
1931
|
+
let eras = Array.isArray(timelineData) ? [...timelineData] : [...(timelineData.eras || [])];
|
|
1932
|
+
if (dynamicTimeline && dynamicTimeline.length > 0) {
|
|
1933
|
+
eras = [...dynamicTimeline, ...eras];
|
|
1934
|
+
}
|
|
1935
|
+
if (year) eras = eras.filter(e => e.year == year || (e.period && String(e.period).includes(String(year))) || (e.date && String(e.date).includes(String(year))));
|
|
1936
|
+
if (era) {
|
|
1937
|
+
const q = String(era).toLowerCase().trim();
|
|
1938
|
+
const qNorm = q.replace(/\s*era\s*/gi, '').trim();
|
|
1939
|
+
eras = eras.filter(e => {
|
|
1940
|
+
const eEra = String(e.era || '').toLowerCase().trim();
|
|
1941
|
+
const eEraNorm = eEra.replace(/\s*era\s*/gi, '').trim();
|
|
1942
|
+
const eTitle = String(e.title || '').toLowerCase();
|
|
1943
|
+
const eTitleCn = String(e.titleCn || '');
|
|
1944
|
+
const eCategory = String(e.category || '').toLowerCase();
|
|
1945
|
+
const eSummary = String(e.summary || '').toLowerCase();
|
|
1946
|
+
return (
|
|
1947
|
+
(eEra && (eEra.includes(q) || (qNorm && eEraNorm.includes(qNorm)) || (qNorm && qNorm.includes(eEraNorm)))) ||
|
|
1948
|
+
eTitle.includes(q) || (qNorm && eTitle.includes(qNorm)) ||
|
|
1949
|
+
eTitleCn.includes(era) ||
|
|
1950
|
+
eCategory.includes(q) || (qNorm && eCategory.includes(qNorm)) ||
|
|
1951
|
+
eSummary.includes(q) || (qNorm && eSummary.includes(qNorm))
|
|
1952
|
+
);
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1254
1955
|
|
|
1956
|
+
const safeLimit = Math.min(Math.max(Number(limit) || 10, 1), 30);
|
|
1255
1957
|
return {
|
|
1256
|
-
status: "success",
|
|
1958
|
+
status: eras.length > 0 ? "success" : "not_found",
|
|
1257
1959
|
count: eras.length,
|
|
1258
|
-
milestones: eras.slice(0,
|
|
1960
|
+
milestones: eras.slice(0, safeLimit).map(e => ({
|
|
1961
|
+
id: e.id,
|
|
1962
|
+
year: e.year,
|
|
1963
|
+
date: e.date || e.period || String(e.year || ''),
|
|
1259
1964
|
period: e.period || e.year,
|
|
1260
1965
|
title: formatCatalogTitle(e.title, e.titleCn),
|
|
1261
|
-
|
|
1966
|
+
titleCn: e.titleCn,
|
|
1967
|
+
era: e.era,
|
|
1968
|
+
category: e.category,
|
|
1969
|
+
summary: e.summary,
|
|
1970
|
+
highlights: e.highlights || [],
|
|
1262
1971
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${e.titleCn || e.title}`)
|
|
1263
1972
|
}))
|
|
1264
1973
|
};
|
|
@@ -1272,15 +1981,27 @@ export const toolHandlers = {
|
|
|
1272
1981
|
let usedQueryFilter = false;
|
|
1273
1982
|
if (query) {
|
|
1274
1983
|
const q = String(query).toLowerCase().trim();
|
|
1275
|
-
|
|
1984
|
+
|
|
1276
1985
|
const generic = /(ju jingyi|jujingyi|จวีจิ้งอี|จิ้งอี|จวี|鞠婧祎|小鞠|kiku|kikuuu|ju jing yi|蜜橘)/i.test(q);
|
|
1277
1986
|
if (!generic) {
|
|
1278
|
-
const
|
|
1279
|
-
if (
|
|
1280
|
-
|
|
1281
|
-
|
|
1987
|
+
const matcher = createCatalogMatcher(q);
|
|
1988
|
+
if (matcher.broad) {
|
|
1989
|
+
|
|
1990
|
+
} else {
|
|
1991
|
+
const filtered = list.filter(a => matcher(a.titleTh, a.titleCn, a.titleEn, a.ceremony, a.category, a.work));
|
|
1992
|
+
if (filtered.length > 0) {
|
|
1993
|
+
list = filtered;
|
|
1994
|
+
usedQueryFilter = true;
|
|
1995
|
+
} else {
|
|
1996
|
+
return {
|
|
1997
|
+
status: "not_found",
|
|
1998
|
+
count: 0,
|
|
1999
|
+
awards: [],
|
|
2000
|
+
query,
|
|
2001
|
+
message: `No awards match the query "${query}" in the local catalog. Do NOT present other awards as an answer to this query. Use web_search or web_extract for external information, or call get_awards without a query to list all awards.`
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
1282
2004
|
}
|
|
1283
|
-
|
|
1284
2005
|
}
|
|
1285
2006
|
}
|
|
1286
2007
|
|
|
@@ -1327,20 +2048,36 @@ export const toolHandlers = {
|
|
|
1327
2048
|
},
|
|
1328
2049
|
|
|
1329
2050
|
get_gallery_photos: async ({ category, query, year, limit = 4 } = {}) => {
|
|
1330
|
-
|
|
2051
|
+
const dynamicPhotos = getDynamicEntries('gallery');
|
|
2052
|
+
let photos = Array.isArray(galleryData) ? [...galleryData] : [...(galleryData.photos || [])];
|
|
2053
|
+
if (dynamicPhotos && dynamicPhotos.length > 0) {
|
|
2054
|
+
photos = [...dynamicPhotos, ...photos];
|
|
2055
|
+
}
|
|
1331
2056
|
if (category && category !== 'all') photos = photos.filter(p => p.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1332
2057
|
let usedQueryFilter = false;
|
|
1333
2058
|
if (query) {
|
|
1334
2059
|
const q = String(query).toLowerCase().trim();
|
|
1335
|
-
|
|
2060
|
+
|
|
1336
2061
|
const generic = /(ju jingyi|jujingyi|จวีจิ้งอี|จิ้งอี|จวี|鞠婧祎|小鞠|kiku|kikuuu|ju jing yi|蜜橘)/i.test(q);
|
|
1337
2062
|
if (!generic) {
|
|
1338
|
-
const
|
|
1339
|
-
if (
|
|
1340
|
-
|
|
1341
|
-
|
|
2063
|
+
const matcher = createCatalogMatcher(q);
|
|
2064
|
+
if (matcher.broad) {
|
|
2065
|
+
|
|
2066
|
+
} else {
|
|
2067
|
+
const filtered = photos.filter(p => matcher(p.titleTh, p.titleCn, p.titleEn, p.category, p.description));
|
|
2068
|
+
if (filtered.length > 0) {
|
|
2069
|
+
photos = filtered;
|
|
2070
|
+
usedQueryFilter = true;
|
|
2071
|
+
} else {
|
|
2072
|
+
return {
|
|
2073
|
+
status: "not_found",
|
|
2074
|
+
count: 0,
|
|
2075
|
+
photos: [],
|
|
2076
|
+
query,
|
|
2077
|
+
message: `No photos match the query "${query}" in the local catalog. Do NOT present other photos as an answer to this query. Use web_search or web_extract for external information, or call get_gallery_photos without a query to browse all photos.`
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
1342
2080
|
}
|
|
1343
|
-
|
|
1344
2081
|
}
|
|
1345
2082
|
}
|
|
1346
2083
|
if (year) photos = photos.filter(p => Number(p.era || p.year) === Number(year));
|
|
@@ -1354,8 +2091,15 @@ export const toolHandlers = {
|
|
|
1354
2091
|
id: p.id || `photo-${p.era || p.year || 'unknown'}-${index + 1}`,
|
|
1355
2092
|
title: p.titleTh || p.titleCn,
|
|
1356
2093
|
category: p.category,
|
|
1357
|
-
year: p.era || p.year
|
|
1358
|
-
|
|
2094
|
+
year: p.era || p.year,
|
|
2095
|
+
imageUrl: p.image || p.imageUrl || p.url || null
|
|
2096
|
+
})),
|
|
2097
|
+
images: shown.map(p => ({
|
|
2098
|
+
title: p.titleTh || p.titleCn || 'Gallery Photo',
|
|
2099
|
+
imageUrl: p.image || p.imageUrl || p.url,
|
|
2100
|
+
thumbUrl: p.image || p.imageUrl || p.url,
|
|
2101
|
+
source: 'Official Gallery'
|
|
2102
|
+
})).filter(img => img.imageUrl)
|
|
1359
2103
|
};
|
|
1360
2104
|
if (usedQueryFilter === false && query) {
|
|
1361
2105
|
result.message = `Showing all ${result.count} photos (broad query "${query}" applied to full list).`;
|
|
@@ -1379,6 +2123,12 @@ export const toolHandlers = {
|
|
|
1379
2123
|
matcher(s.titleTh, s.titleEn, s.titleCn, s.work, s.role, s.description)
|
|
1380
2124
|
).slice(0, 4).map(s => ({
|
|
1381
2125
|
title: `${s.titleTh} (${s.year})`,
|
|
2126
|
+
titleEn: s.titleEn || null,
|
|
2127
|
+
titleCn: s.titleCn || null,
|
|
2128
|
+
year: s.year,
|
|
2129
|
+
work: s.work || null,
|
|
2130
|
+
role: s.role || null,
|
|
2131
|
+
description: s.description || null,
|
|
1382
2132
|
youtubeUrl: s.youtubeUrl || null,
|
|
1383
2133
|
isDirectLink: Boolean(s.youtubeUrl),
|
|
1384
2134
|
youtubeSearchUrl: s.youtubeSearchUrl
|
|
@@ -1388,6 +2138,8 @@ export const toolHandlers = {
|
|
|
1388
2138
|
matcher(m.titleTh, m.titleCn, m.titlePinyin, m.dramaName, m.description)
|
|
1389
2139
|
).slice(0, 4).map(m => ({
|
|
1390
2140
|
title: `${formatCatalogTitle(m.titleTh, m.titleCn)} [${m.category}]`,
|
|
2141
|
+
drama: m.dramaName || null,
|
|
2142
|
+
description: m.description || null,
|
|
1391
2143
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
1392
2144
|
isDirectLink: Boolean(m.youtubeId),
|
|
1393
2145
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh}`)
|
|
@@ -1409,7 +2161,8 @@ export const toolHandlers = {
|
|
|
1409
2161
|
works: matchedWorks,
|
|
1410
2162
|
songs: matchedSongs,
|
|
1411
2163
|
brands: matchedBrands,
|
|
1412
|
-
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${query}`)
|
|
2164
|
+
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${query}`),
|
|
2165
|
+
bilibiliSearchUrl: buildBilibiliSearchUrl(`鞠婧祎 ${query}`)
|
|
1413
2166
|
}
|
|
1414
2167
|
};
|
|
1415
2168
|
},
|
|
@@ -1422,35 +2175,35 @@ export const toolHandlers = {
|
|
|
1422
2175
|
return await fetchWebpage(url);
|
|
1423
2176
|
},
|
|
1424
2177
|
|
|
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);
|
|
2178
|
+
fetch_api: async ({ endpoint, query, baseUrl } = {}) => {
|
|
2179
|
+
if (!endpoint) return { status: "error", message: "endpoint parameter is required (e.g. 'profile', 'works', 'search')" };
|
|
2180
|
+
const base = (baseUrl || 'https://jujingyi-kikuuu.vercel.app/api/v1').replace(/\/$/, '');
|
|
2181
|
+
const originCheck = sanitizeUrl(base);
|
|
1429
2182
|
if (!originCheck.safe) {
|
|
1430
2183
|
return { status: "error", message: `Unsafe API base URL: ${originCheck.reason}` };
|
|
1431
2184
|
}
|
|
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,
|
|
2185
|
+
const allowedOrigins = new Set([
|
|
2186
|
+
'https://jujingyi-kikuuu.vercel.app',
|
|
2187
|
+
...(process.env.KIKU_API_ALLOWED_ORIGINS || '').split(',').map(value => value.trim()).filter(Boolean)
|
|
2188
|
+
]);
|
|
2189
|
+
if (!originCheck.url || !allowedOrigins.has(originCheck.url.origin)) {
|
|
2190
|
+
return { status: 'error', message: 'API base origin is not allowlisted.' };
|
|
2191
|
+
}
|
|
2192
|
+
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
2193
|
+
const target = new URL(`${base}/${endpoint.replace(/^\/+/, '')}`);
|
|
2194
|
+
if (target.origin !== originCheck.url.origin || !target.pathname.startsWith('/api/v1/')) {
|
|
2195
|
+
return { status: 'error', message: 'API endpoint must remain under /api/v1/.' };
|
|
2196
|
+
}
|
|
2197
|
+
if (query) target.searchParams.set('q', query);
|
|
2198
|
+
|
|
2199
|
+
try {
|
|
2200
|
+
const result = await fetchJson(target.href, { timeoutMs: 10000, maxResponseLength: 100000 });
|
|
2201
|
+
if (result.status !== 'success') return result;
|
|
2202
|
+
const json = result.data;
|
|
2203
|
+
const data = json?.data || json;
|
|
2204
|
+
return {
|
|
2205
|
+
status: "success",
|
|
2206
|
+
endpoint: path,
|
|
1454
2207
|
count: Array.isArray(data) ? data.length : undefined,
|
|
1455
2208
|
data
|
|
1456
2209
|
};
|
|
@@ -1458,7 +2211,7 @@ export const toolHandlers = {
|
|
|
1458
2211
|
return {
|
|
1459
2212
|
status: "error",
|
|
1460
2213
|
endpoint: path,
|
|
1461
|
-
message: err.name === 'AbortError' ? 'API request timed out' : err.message
|
|
2214
|
+
message: err.name === 'AbortError' ? 'API request timed out' : err.message
|
|
1462
2215
|
};
|
|
1463
2216
|
}
|
|
1464
2217
|
},
|
|
@@ -1467,24 +2220,62 @@ export const toolHandlers = {
|
|
|
1467
2220
|
return await fetchJson(url, { method, headers, body });
|
|
1468
2221
|
},
|
|
1469
2222
|
|
|
2223
|
+
search_news: async ({ query, maxResults } = {}) => {
|
|
2224
|
+
if (!query) return { status: 'error', message: 'query parameter is required.' };
|
|
2225
|
+
return await searchNews(query, maxResults);
|
|
2226
|
+
},
|
|
2227
|
+
|
|
2228
|
+
fetch_rss: async ({ url, maxItems } = {}) => {
|
|
2229
|
+
return await fetchRssFeed(url, maxItems);
|
|
2230
|
+
},
|
|
2231
|
+
|
|
2232
|
+
search_encyclopedia: async ({ term, language, maxEntries } = {}) => {
|
|
2233
|
+
if (!term) return { status: 'error', message: 'term parameter is required.' };
|
|
2234
|
+
const lang = ['zh', 'th', 'en'].includes(language) ? language : detectLanguage(term);
|
|
2235
|
+
return await searchEncyclopedia(term, lang, maxEntries);
|
|
2236
|
+
},
|
|
2237
|
+
|
|
2238
|
+
search_music_platforms: async ({ term, maxResults } = {}) => {
|
|
2239
|
+
if (!term) return { status: 'error', message: 'term parameter is required.' };
|
|
2240
|
+
return await searchMusicPlatforms(term, maxResults);
|
|
2241
|
+
},
|
|
2242
|
+
|
|
2243
|
+
search_chinese_web: async ({ query, maxResults, site } = {}) => {
|
|
2244
|
+
if (!isCnWebEnabled()) return { status: 'error', gated: true, message: cnWebDisabledMessage('search_chinese_web') };
|
|
2245
|
+
if (!query) return { status: 'error', message: 'query parameter is required.' };
|
|
2246
|
+
return await searchChineseWeb(query, maxResults, { site });
|
|
2247
|
+
},
|
|
2248
|
+
|
|
2249
|
+
search_bilibili: async ({ keyword, maxResults } = {}) => {
|
|
2250
|
+
if (!isCnWebEnabled()) return { status: 'error', gated: true, message: cnWebDisabledMessage('search_bilibili') };
|
|
2251
|
+
if (!keyword) return { status: 'error', message: 'keyword parameter is required.' };
|
|
2252
|
+
return await searchBilibili(keyword, maxResults);
|
|
2253
|
+
},
|
|
2254
|
+
|
|
2255
|
+
search_web_images: async ({ query, maxResults } = {}) => {
|
|
2256
|
+
if (!isCnWebEnabled()) return { status: 'error', gated: true, message: cnWebDisabledMessage('search_web_images') };
|
|
2257
|
+
if (!query) return { status: 'error', message: 'query parameter is required.' };
|
|
2258
|
+
return await searchWebImages(query, maxResults);
|
|
2259
|
+
},
|
|
2260
|
+
|
|
1470
2261
|
read_local_file: async ({ path: filePath, maxLines = 200 } = {}) => {
|
|
1471
2262
|
if (!filePath) return { status: 'error', message: 'path parameter is required.' };
|
|
1472
2263
|
try {
|
|
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);
|
|
2264
|
+
const projectRoot = await realpath(process.cwd());
|
|
2265
|
+
const resolved = resolve(projectRoot, filePath);
|
|
2266
|
+
const realResolved = await realpath(resolved);
|
|
2267
|
+
const relativePath = relative(projectRoot, realResolved);
|
|
2268
|
+
|
|
2269
|
+
if (relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
2270
|
+
return { status: 'error', message: 'Path must be within the project directory.' };
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
if (/(^|[\\/])\.env(?:\.|$)/i.test(relativePath) || /(^|[\\/])cookies\.txt$/i.test(relativePath) ||
|
|
2274
|
+
/\.(pem|key|p12|pfx)$/i.test(relativePath) || /(^|[\\/])\.(git|npmrc|netrc|ssh)([\\/]|$)/i.test(relativePath)) {
|
|
2275
|
+
return { status: 'error', message: 'Reading this file is not permitted (sensitive file).' };
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
const fileStat = await stat(realResolved);
|
|
1488
2279
|
if (!fileStat.isFile()) {
|
|
1489
2280
|
return { status: 'error', message: `"${filePath}" is not a file.` };
|
|
1490
2281
|
}
|
|
@@ -1493,7 +2284,7 @@ export const toolHandlers = {
|
|
|
1493
2284
|
return { status: 'error', message: `File too large (${Math.round(fileStat.size / 1024)}KB). Use maxLines to read a portion.` };
|
|
1494
2285
|
}
|
|
1495
2286
|
|
|
1496
|
-
const content = await readFile(realResolved, 'utf-8');
|
|
2287
|
+
const content = await readFile(realResolved, 'utf-8');
|
|
1497
2288
|
const lines = content.split('\n');
|
|
1498
2289
|
const totalLines = lines.length;
|
|
1499
2290
|
const truncated = totalLines > maxLines;
|
|
@@ -1518,14 +2309,14 @@ export const toolHandlers = {
|
|
|
1518
2309
|
|
|
1519
2310
|
list_directory: async ({ path: dirPath = '.', recursive = false } = {}) => {
|
|
1520
2311
|
try {
|
|
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
|
-
}
|
|
2312
|
+
const projectRoot = await realpath(process.cwd());
|
|
2313
|
+
const resolved = resolve(projectRoot, dirPath);
|
|
2314
|
+
const realResolved = await realpath(resolved);
|
|
2315
|
+
const relativePath = relative(projectRoot, realResolved);
|
|
2316
|
+
|
|
2317
|
+
if (relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
2318
|
+
return { status: 'error', message: 'Path must be within the project directory.' };
|
|
2319
|
+
}
|
|
1529
2320
|
|
|
1530
2321
|
async function listDir(dir, depth = 0) {
|
|
1531
2322
|
if (depth > (recursive ? 3 : 0)) return [];
|
|
@@ -1565,7 +2356,7 @@ export const toolHandlers = {
|
|
|
1565
2356
|
return items;
|
|
1566
2357
|
}
|
|
1567
2358
|
|
|
1568
|
-
const items = await listDir(realResolved);
|
|
2359
|
+
const items = await listDir(realResolved);
|
|
1569
2360
|
return {
|
|
1570
2361
|
status: 'success',
|
|
1571
2362
|
path: relativePath || '.',
|
|
@@ -1594,7 +2385,7 @@ export const toolHandlers = {
|
|
|
1594
2385
|
const awards = Array.isArray(awardsData) ? awardsData : (awardsData?.awards || []);
|
|
1595
2386
|
const timeline = Array.isArray(timelineData) ? timelineData : (timelineData?.eras || []);
|
|
1596
2387
|
const mvs = getAllMusicVideos();
|
|
1597
|
-
const gpu = await getGpuMetrics().catch(() => ({ available: false }));
|
|
2388
|
+
const gpu = await getGpuMetrics({ includeShared: false }).catch(() => ({ available: false }));
|
|
1598
2389
|
|
|
1599
2390
|
const debutMatch = profileData.details?.debutDate?.match(/\b(19\d\d|20\d\d)\b/);
|
|
1600
2391
|
const debutYear = debutMatch ? Number(debutMatch[1]) : 2013;
|
|
@@ -1920,6 +2711,127 @@ export const toolHandlers = {
|
|
|
1920
2711
|
apiKey: null
|
|
1921
2712
|
};
|
|
1922
2713
|
},
|
|
2714
|
+
|
|
2715
|
+
get_entertainment_metrics: async ({ platform, category, query } = {}) => {
|
|
2716
|
+
let platforms = [...platformsCatalog];
|
|
2717
|
+
let benchmarks = [...(artistIndustryMetrics.dramaBenchmarks || [])];
|
|
2718
|
+
|
|
2719
|
+
if (platform && platform !== 'all') {
|
|
2720
|
+
const pId = String(platform).toLowerCase().trim();
|
|
2721
|
+
platforms = platforms.filter(p => p.id.toLowerCase() === pId);
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
if (category && category !== 'all') {
|
|
2725
|
+
const cat = String(category).toLowerCase().trim();
|
|
2726
|
+
platforms = platforms.filter(p => p.category.toLowerCase() === cat);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (query) {
|
|
2730
|
+
const qLower = String(query).toLowerCase().trim();
|
|
2731
|
+
platforms = platforms.filter(p =>
|
|
2732
|
+
p.nameEn.toLowerCase().includes(qLower) ||
|
|
2733
|
+
p.nameCn.includes(qLower) ||
|
|
2734
|
+
(p.nameTh && p.nameTh.toLowerCase().includes(qLower)) ||
|
|
2735
|
+
(p.industryRoleTh && p.industryRoleTh.toLowerCase().includes(qLower)) ||
|
|
2736
|
+
(p.coreMetricsTh && p.coreMetricsTh.some(m => m.toLowerCase().includes(qLower))) ||
|
|
2737
|
+
(p.kikuHighlightsTh && p.kikuHighlightsTh.some(h => h.toLowerCase().includes(qLower)))
|
|
2738
|
+
);
|
|
2739
|
+
benchmarks = benchmarks.filter(b =>
|
|
2740
|
+
b.titleCn.includes(qLower) ||
|
|
2741
|
+
b.titleEn.toLowerCase().includes(qLower) ||
|
|
2742
|
+
(b.titleTh && b.titleTh.toLowerCase().includes(qLower)) ||
|
|
2743
|
+
(b.platform && b.platform.toLowerCase().includes(qLower)) ||
|
|
2744
|
+
(b.significanceTh && b.significanceTh.toLowerCase().includes(qLower))
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
return {
|
|
2749
|
+
status: "success",
|
|
2750
|
+
totalPlatforms: platforms.length,
|
|
2751
|
+
categoryFilter: category || 'all',
|
|
2752
|
+
platformFilter: platform || 'all',
|
|
2753
|
+
platforms,
|
|
2754
|
+
dramaBenchmarks: benchmarks,
|
|
2755
|
+
artistOverview: artistIndustryMetrics.artist,
|
|
2756
|
+
socialBuzzOverview: artistIndustryMetrics.socialBuzzOverview,
|
|
2757
|
+
message: `Found ${platforms.length} big data platforms across Chinese entertainment analytics.`
|
|
2758
|
+
};
|
|
2759
|
+
},
|
|
2760
|
+
|
|
2761
|
+
get_industry_index: async (args = {}, options = {}) => {
|
|
2762
|
+
return toolHandlers.get_entertainment_metrics(args, options);
|
|
2763
|
+
},
|
|
2764
|
+
|
|
2765
|
+
checkpoint_step: async ({ run_id, step_name, status, summary } = {}, options = {}) => {
|
|
2766
|
+
let memory = options?.agent?.memory || options?.memory;
|
|
2767
|
+
if (!memory) {
|
|
2768
|
+
const { AgentMemory } = await import('./memory.js');
|
|
2769
|
+
memory = new AgentMemory();
|
|
2770
|
+
await memory.load();
|
|
2771
|
+
}
|
|
2772
|
+
const result = await memory.checkpointStep({ runId: run_id, stepName: step_name, status, summary });
|
|
2773
|
+
return {
|
|
2774
|
+
status: "success",
|
|
2775
|
+
message: `Checkpoint recorded for run ${run_id}: step "${step_name}" is ${status}.`,
|
|
2776
|
+
checkpoint: result
|
|
2777
|
+
};
|
|
2778
|
+
},
|
|
2779
|
+
|
|
2780
|
+
record_failure: async ({ run_id, error_trace, recovery_action, tool_name } = {}, options = {}) => {
|
|
2781
|
+
let memory = options?.agent?.memory || options?.memory;
|
|
2782
|
+
if (!memory) {
|
|
2783
|
+
const { AgentMemory } = await import('./memory.js');
|
|
2784
|
+
memory = new AgentMemory();
|
|
2785
|
+
await memory.load();
|
|
2786
|
+
}
|
|
2787
|
+
const result = await memory.recordFailure({ runId: run_id, errorTrace: error_trace, recoveryAction: recovery_action, toolName: tool_name });
|
|
2788
|
+
return {
|
|
2789
|
+
status: "success",
|
|
2790
|
+
message: `Failure recorded for run ${run_id}.`,
|
|
2791
|
+
failure: result
|
|
2792
|
+
};
|
|
2793
|
+
},
|
|
2794
|
+
|
|
2795
|
+
load_active_run: async ({ run_id } = {}, options = {}) => {
|
|
2796
|
+
let memory = options?.agent?.memory || options?.memory;
|
|
2797
|
+
if (!memory) {
|
|
2798
|
+
const { AgentMemory } = await import('./memory.js');
|
|
2799
|
+
memory = new AgentMemory();
|
|
2800
|
+
await memory.load();
|
|
2801
|
+
}
|
|
2802
|
+
const result = await memory.loadActiveRun(run_id);
|
|
2803
|
+
if (!result || !result.exists) {
|
|
2804
|
+
return {
|
|
2805
|
+
status: "not_found",
|
|
2806
|
+
message: `Run "${run_id}" not found in active memory.`
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
return {
|
|
2810
|
+
status: "success",
|
|
2811
|
+
run: result
|
|
2812
|
+
};
|
|
2813
|
+
},
|
|
2814
|
+
|
|
2815
|
+
record_dynamic_event: async ({ category, entry, source, trustTier } = {}) => {
|
|
2816
|
+
try {
|
|
2817
|
+
const added = addDynamicEntry(category, {
|
|
2818
|
+
...(entry || {}),
|
|
2819
|
+
source: source || entry?.source || 'agent_inference',
|
|
2820
|
+
trustTier: trustTier || entry?.trustTier || 2,
|
|
2821
|
+
});
|
|
2822
|
+
globalToolCache.clear();
|
|
2823
|
+
return {
|
|
2824
|
+
status: "success",
|
|
2825
|
+
message: `Successfully recorded dynamic event in category "${category}".`,
|
|
2826
|
+
entry: added
|
|
2827
|
+
};
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
return {
|
|
2830
|
+
status: "error",
|
|
2831
|
+
message: `Failed to record dynamic event: ${err.message}`
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
1923
2835
|
};
|
|
1924
2836
|
|
|
1925
2837
|
const toolSchemaByName = new Map(
|
|
@@ -2002,8 +2914,14 @@ export function prepareToolArguments(toolName, rawArgs = {}) {
|
|
|
2002
2914
|
if (!checked.ok) {
|
|
2003
2915
|
return { safe: false, args: {}, warnings, error: `Argument "${name}" must be ${propertySchema.type || 'valid'}.` };
|
|
2004
2916
|
}
|
|
2005
|
-
if (propertySchema.enum
|
|
2006
|
-
|
|
2917
|
+
if (propertySchema.enum) {
|
|
2918
|
+
const target = String(checked.value).toLowerCase().trim();
|
|
2919
|
+
const matched = propertySchema.enum.find(e => String(e).toLowerCase() === target);
|
|
2920
|
+
if (matched !== undefined) {
|
|
2921
|
+
checked.value = matched;
|
|
2922
|
+
} else {
|
|
2923
|
+
return { safe: false, args: {}, warnings, error: `Argument "${name}" must be one of: ${propertySchema.enum.join(', ')}.` };
|
|
2924
|
+
}
|
|
2007
2925
|
}
|
|
2008
2926
|
if (checked.coerced) warnings.push(`Argument "${name}" was converted to ${propertySchema.type}`);
|
|
2009
2927
|
validated[name] = checked.value;
|
|
@@ -2097,3 +3015,46 @@ export async function executeTool(toolName, args = {}, options = {}) {
|
|
|
2097
3015
|
};
|
|
2098
3016
|
}
|
|
2099
3017
|
}
|
|
3018
|
+
|
|
3019
|
+
/**
|
|
3020
|
+
* Authoritative registry of primary tools with domain categorizations and schemas.
|
|
3021
|
+
*/
|
|
3022
|
+
export const toolsRegistry = {
|
|
3023
|
+
global_search: {
|
|
3024
|
+
domain: 'core',
|
|
3025
|
+
isCore: true,
|
|
3026
|
+
schema: toolsSchema.find(t => t.function?.name === 'global_search')?.function,
|
|
3027
|
+
handler: toolHandlers.global_search,
|
|
3028
|
+
},
|
|
3029
|
+
get_awards: {
|
|
3030
|
+
domain: 'awards',
|
|
3031
|
+
isCore: false,
|
|
3032
|
+
schema: toolsSchema.find(t => t.function?.name === 'get_awards')?.function,
|
|
3033
|
+
handler: toolHandlers.get_awards,
|
|
3034
|
+
},
|
|
3035
|
+
get_gallery_photos: {
|
|
3036
|
+
domain: 'gallery',
|
|
3037
|
+
isCore: false,
|
|
3038
|
+
schema: toolsSchema.find(t => t.function?.name === 'get_gallery_photos')?.function,
|
|
3039
|
+
handler: toolHandlers.get_gallery_photos,
|
|
3040
|
+
},
|
|
3041
|
+
get_quotes: {
|
|
3042
|
+
domain: 'quotes',
|
|
3043
|
+
isCore: false,
|
|
3044
|
+
schema: toolsSchema.find(t => t.function?.name === 'get_quotes')?.function,
|
|
3045
|
+
handler: toolHandlers.get_quotes,
|
|
3046
|
+
},
|
|
3047
|
+
search_discography: {
|
|
3048
|
+
domain: 'discography',
|
|
3049
|
+
isCore: false,
|
|
3050
|
+
schema: toolsSchema.find(t => t.function?.name === 'search_discography')?.function,
|
|
3051
|
+
handler: toolHandlers.search_discography,
|
|
3052
|
+
},
|
|
3053
|
+
search_works: {
|
|
3054
|
+
domain: 'works',
|
|
3055
|
+
isCore: false,
|
|
3056
|
+
schema: toolsSchema.find(t => t.function?.name === 'search_works')?.function,
|
|
3057
|
+
handler: toolHandlers.search_works,
|
|
3058
|
+
},
|
|
3059
|
+
};
|
|
3060
|
+
|