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.
Files changed (100) hide show
  1. package/README.md +459 -407
  2. package/package.json +27 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +629 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +528 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/lanDiscovery.js +369 -0
  23. package/server/agent/llmClient.js +111 -39
  24. package/server/agent/lmStudioWakeup.js +144 -0
  25. package/server/agent/mcpClient.js +2 -2
  26. package/server/agent/memory.js +635 -494
  27. package/server/agent/modelConfig.js +487 -0
  28. package/server/agent/newsIntelligence.js +219 -0
  29. package/server/agent/newsWatch.js +85 -0
  30. package/server/agent/prompts.js +146 -0
  31. package/server/agent/publicTools.js +7 -0
  32. package/server/agent/queryDetails.js +62 -1
  33. package/server/agent/research.js +314 -35
  34. package/server/agent/routingInterceptor.js +77 -0
  35. package/server/agent/rumoredProjects.js +15 -0
  36. package/server/agent/sanitizer.js +117 -17
  37. package/server/agent/subagent.js +52 -9
  38. package/server/agent/suggestionTui.js +33 -13
  39. package/server/agent/suggestions.js +29 -13
  40. package/server/agent/telemetry.js +0 -11
  41. package/server/agent/terminalImage.js +0 -1
  42. package/server/agent/textToolProtocol.js +190 -0
  43. package/server/agent/tokenTracker.js +220 -0
  44. package/server/agent/toolRouter.js +362 -216
  45. package/server/agent/tools.js +1155 -194
  46. package/server/agent/ui.js +144 -41
  47. package/server/agent/vramService.js +572 -0
  48. package/server/agent/webSearch.js +2279 -273
  49. package/server/agent/youtubeAuth.js +82 -73
  50. package/server/agent/youtubeTranscript.js +209 -0
  51. package/server/apiRouter.js +383 -179
  52. package/server/mcp/data/brands.js +5 -5
  53. package/server/mcp/data/discography.js +56 -45
  54. package/server/mcp/data/gallery.js +9 -0
  55. package/server/mcp/data/index.js +1 -0
  56. package/server/mcp/data/industryIndex.js +251 -0
  57. package/server/mcp/data/profile.js +5 -5
  58. package/server/mcp/data/timeline.js +2 -2
  59. package/server/mcp/data/works.js +2 -2
  60. package/server/mcp/handler.js +0 -2
  61. package/server/mcp/prompts.js +42 -2
  62. package/server/mcp/resources.js +5 -1
  63. package/server/mcp/security.js +25 -14
  64. package/server/mcp/server.js +92 -3
  65. package/server/mcp/tools.js +20 -11
  66. package/server/web-chat/core/connection.js +218 -194
  67. package/server/web-chat/core/conversationContext.js +1 -1
  68. package/server/web-chat/core/directCatalogEngine.js +190 -184
  69. package/server/web-chat/core/followUpEngine.js +88 -11
  70. package/server/web-chat/core/security.js +7 -4
  71. package/server/web-chat/router.js +54 -17
  72. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  73. package/server/web-chat/routes/chatRoute.js +488 -397
  74. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  75. package/server/web-chat/routes/exportRoute.js +2 -1
  76. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  77. package/server/web-chat/routes/newsRoute.js +36 -6
  78. package/server/web-chat/routes/providerRoute.js +87 -0
  79. package/server/web-chat/routes/staticRoute.js +61 -13
  80. package/server/web-chat/routes/statusRoute.js +23 -2
  81. package/server/web-chat/routes/vramRoute.js +22 -0
  82. package/server/web-chat/server.js +135 -106
  83. package/server/web-chat/ui/htmlBuilder.js +8 -8
  84. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  85. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  86. package/server/web-chat/ui/scripts/client.js +10526 -8397
  87. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  88. package/server/web-chat/ui/styles/main.css +7471 -4246
  89. package/server/web-chat/ui/templates/body.html +305 -179
  90. package/src/data/brandsData.js +29 -6
  91. package/src/data/discographyData.js +918 -900
  92. package/src/data/galleryData.js +66 -0
  93. package/src/data/industryIndexData.js +292 -0
  94. package/src/data/musicVideoData.js +48 -5
  95. package/src/data/profileData.js +15 -1
  96. package/src/data/timelineData.js +4 -3
  97. package/src/data/worksData.js +536 -535
  98. package/src/lib/vram/estimator.js +464 -0
  99. package/src/lib/vram/modelRegistry.js +387 -0
  100. package/tokens.css +33 -2
@@ -22,3 +22,5 @@ export const EXPORT_DIR = path.join(KIKU_DATA_DIR, 'exports');
22
22
  export const PROVIDER_KEYS_FILE = path.join(KIKU_DATA_DIR, 'provider_keys.json');
23
23
  export const SETTINGS_FILE = path.join(KIKU_DATA_DIR, 'settings.json');
24
24
  export const PROFILES_FILE = path.join(KIKU_DATA_DIR, 'profiles.json');
25
+ export const HIERARCHICAL_MEMORY_DIR = path.join(KIKU_DATA_DIR, 'memory_nodes');
26
+ export const ARTIFACTS_STORE_DIR = path.join(KIKU_DATA_DIR, 'artifacts_store');
@@ -0,0 +1,629 @@
1
+ import { brandsData } from '../../src/data/brandsData.js';
2
+ import { worksData } from '../../src/data/worksData.js';
3
+ import { discographyData } from '../../src/data/discographyData.js';
4
+ import { musicVideoData } from '../../src/data/musicVideoData.js';
5
+ import { awardsData } from '../../src/data/awardsData.js';
6
+ import { timelineData } from '../../src/data/timelineData.js';
7
+ import { profileData } from '../../src/data/profileData.js';
8
+ import { RUMORED_OR_EXTERNAL_PROJECTS } from './rumoredProjects.js';
9
+
10
+ let cachedIndex = null;
11
+
12
+ const PLATFORM_PATTERNS = [
13
+ { key: 'bilibili', regex: /(?:bilibili|b站|b-site|บิลิบิลิ|บิลิ)/i, cnName: 'B站' },
14
+ { key: 'youtube', regex: /(?:youtube|yt|ยูทูป|ยูทูบ)/i, cnName: 'YouTube' },
15
+ { key: 'weibo', regex: /(?:weibo|微博|เวยปั๋ว|เว่ยป๋อ)/i, cnName: '微博' },
16
+ { key: 'douyin', regex: /(?:douyin|tiktok|抖音|โต่วอิน)/i, cnName: '抖音' },
17
+ { key: 'qqmusic', regex: /(?:qqmusic|qq\s*音乐|qq\s*music|คิวคิว)/i, cnName: 'QQ音乐' },
18
+ { key: 'youku', regex: /(?:youku|优酷|โยวคู่)/i, cnName: '优酷' },
19
+ { key: 'iqiyi', regex: /(?:iqiyi|爱奇艺|อ้ายฉีอี้)/i, cnName: '爱奇艺' },
20
+ { key: 'tencent', regex: /(?:tencent|wetv|腾讯视频|วีทีวี)/i, cnName: '腾讯视频' }
21
+ ];
22
+
23
+ const PROMOTION_INTENT_TERMS = [
24
+ 'โปรโมท', 'โปรโมต', 'การโปรโมท', 'การโปรโมต', 'พรีเซนเตอร์', 'พรีเซ็นเตอร์', 'โฆษณา', 'แคมเปญ',
25
+ 'commercial', 'cf', 'promo', 'promotion', 'promoted', 'campaign', 'sponsor', 'partnership',
26
+ 'endorsement', 'endorser', 'ambassador', 'fashion', 'photoshoot',
27
+ '推广', '商务', '代言', '地广', '物料', '广告', '品牌', '大使'
28
+ ];
29
+
30
+ export function detectPlatforms(query = '') {
31
+ const text = String(query || '').toLowerCase();
32
+ const matched = [];
33
+ for (const p of PLATFORM_PATTERNS) {
34
+ if (p.regex.test(text)) {
35
+ matched.push(p.key);
36
+ }
37
+ }
38
+ return matched;
39
+ }
40
+
41
+ export function detectPromotionIntent(query = '') {
42
+ const text = String(query || '').toLowerCase();
43
+ return PROMOTION_INTENT_TERMS.some(term => {
44
+ const t = term.toLowerCase();
45
+ if (!/^[a-z0-9 -]+$/.test(t)) return text.includes(t);
46
+ const escaped = t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
+ return new RegExp(`(?:^|[^a-z0-9])${escaped}(?:s|es)?(?:$|[^a-z0-9])`).test(text);
48
+ });
49
+ }
50
+
51
+ const STOPWORDS = new Set([
52
+ 'the', 'a', 'an', 'in', 'on', 'at', 'to', 'of', 'for', 'by', 'with', 'from',
53
+ 'as', 'into', 'like', 'through', 'after', 'over', 'between', 'out', 'against',
54
+ 'during', 'without', 'before', 'under', 'around', 'among', 'and', 'or', 'but',
55
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'it', 'its', 'this', 'that',
56
+ 'these', 'those', 'who', 'whom', 'whose', 'which', 'what', 'where', 'when', 'why', 'how',
57
+ 'tell', 'me', 'about', 'her', 'she', 'he', 'him', 'his', 'they', 'them', 'their', 'we', 'us',
58
+ 'you', 'your', 'my', 'does', 'do', 'did', 'done', 'doing', 'have', 'has', 'had',
59
+ 'will', 'would', 'shall', 'should', 'can', 'could', 'look', 'looks', 'see', 'song', 'songs', 'concert'
60
+ ]);
61
+
62
+ function cleanAlias(text) {
63
+ if (!text) return '';
64
+ return String(text)
65
+ .trim()
66
+ .replace(/[()[\]{}"'“”‘’]/g, ' ')
67
+ .replace(/\s+/g, ' ')
68
+ .toLowerCase();
69
+ }
70
+
71
+ function extractAliases(...values) {
72
+ const aliases = new Set();
73
+ for (const v of values) {
74
+ if (!v || typeof v !== 'string') continue;
75
+ const clean = cleanAlias(v);
76
+ if (clean.length >= 2 && !STOPWORDS.has(clean)) aliases.add(clean);
77
+ const withSpaces = clean.replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim();
78
+ if (withSpaces.length >= 2 && !STOPWORDS.has(withSpaces)) aliases.add(withSpaces);
79
+ const parts = v.split(/[/\\|–—,]/).map(p => cleanAlias(p)).filter(p => p.length >= 2 && !STOPWORDS.has(p));
80
+ for (const p of parts) aliases.add(p);
81
+ }
82
+ return Array.from(aliases);
83
+ }
84
+
85
+ export const CATEGORY_ALIASES = {
86
+ 'jewelry': ['jewelry', 'jewellery', 'high jewelry', 'เครื่องประดับ', 'อัญมณี', 'french high jewelry', 'fine jewelry', '珠宝', '首饰', 'force 10', 'lvmh'],
87
+ 'eyewear': ['eyewear', 'glasses', 'sunglasses', 'แว่นตา', 'แว่นกันแดด', '眼镜', '太阳镜'],
88
+ 'beauty': ['beauty', 'cosmetics', 'skincare', 'บิวตี้', 'เครื่องสำอาง', 'สกินแคร์', '美妆', '护肤'],
89
+ 'fashion': ['fashion', 'apparel', 'clothing', 'เสื้อผ้า', 'แฟชั่น', '服饰', '时装'],
90
+ 'fragrance': ['fragrance', 'perfume', 'น้ำหอม', '香氛', '香水'],
91
+ 'haircare': ['haircare', 'hair care', 'shampoo', 'ดูแลเส้นผม', 'แชมพู', '洗护', '美发'],
92
+ 'personal_care': ['personal care', 'sanitary', 'ผ้าอนามัย', 'สุขอนามัย', '卫生巾', '个人护理']
93
+ };
94
+
95
+ export function buildCatalogIndex() {
96
+ if (cachedIndex) return cachedIndex;
97
+
98
+ const entities = [];
99
+
100
+ const brandList = Array.isArray(brandsData) ? brandsData : (brandsData.brandList || brandsData.brands || []);
101
+ for (const b of brandList) {
102
+ const enName = b.brandNameEn || b.brand || b.name || '';
103
+ const cnName = b.brandNameCn || '';
104
+ const aliases = extractAliases(
105
+ b.id,
106
+ enName,
107
+ cnName,
108
+ b.title,
109
+ b.tier
110
+ );
111
+
112
+ if (Array.isArray(b.aliases)) aliases.push(...b.aliases);
113
+ if (Array.isArray(b.keywords)) aliases.push(...b.keywords);
114
+
115
+ if (b.id === 'prsr' || enName.toLowerCase().includes('prsr') || enName.toLowerCase().includes('pasha')) {
116
+ aliases.push('prsr', 'pasha', '帕莎', '帕莎眼镜', 'แว่นตา prsr', 'แว่นตา pasha');
117
+ }
118
+ if (b.id === 'versace' || enName.toLowerCase().includes('versace')) {
119
+ aliases.push('versace', '范思哲', '范思哲香氛', 'เวอร์ซาเช่');
120
+ }
121
+ if (b.id === 'fred' || enName.toLowerCase().includes('fred')) {
122
+ aliases.push('fred', 'fred jewelry', 'fred珠宝', 'french high jewelry', 'เฟรด', 'เครื่องประดับฝรั่งเศส', 'french jewelry', 'เครื่องประดับระดับสูงสัญชาติฝรั่งเศส', 'เครื่องประดับชั้นสูงจากฝรั่งเศส', 'เครื่องประดับชั้นสูง', 'french high jewelry brand', 'lvmh', 'force 10');
123
+ }
124
+ if (b.id === 'tutucare' || enName.toLowerCase().includes('tutu') || cnName.includes('凸凸棉')) {
125
+ aliases.push('tutucare', 'ทูทูแคร์', 'tutu care', 'ตูตูแคร์', '凸凸棉', 'กล่องของขวัญ tutucare', 'กล่องของขวัญ', 'สถิติยอดขายกล่องของขวัญ');
126
+ }
127
+ if (b.id === 'lafang' || enName.toLowerCase().includes('lafang') || cnName.includes('拉芳')) {
128
+ aliases.push('lafang', 'ลาฟาง', '拉芳', 'ดูแลเส้นผม');
129
+ }
130
+
131
+ entities.push({
132
+ type: 'BRAND',
133
+ id: b.id || enName.toLowerCase().replace(/\s+/g, '-'),
134
+ canonicalName: `${enName} (${cnName})`.trim(),
135
+ nameEn: enName,
136
+ nameCn: cnName,
137
+ nameTh: '',
138
+ category: b.category || 'Brand Endorsement',
139
+ year: b.year,
140
+ aliases: Array.from(new Set(aliases)),
141
+ recommendedTools: ['get_brand_endorsements', 'global_search'],
142
+ raw: b
143
+ });
144
+ }
145
+
146
+
147
+ const works = Array.isArray(worksData) ? worksData : [];
148
+ for (const w of works) {
149
+ const enName = w.titleEn || '';
150
+ const cnName = w.titleCn || '';
151
+ const thName = w.titleTh || '';
152
+ const aliases = extractAliases(w.id, enName, cnName, thName, w.role, w.roleCn);
153
+
154
+ if (cnName.includes('花间令') || enName.toLowerCase().includes('in blossom')) {
155
+ aliases.push('in blossom', '花间令', '花间', 'ฮวาเจียนลิ่ง', 'ฮวาเจียนหลิง', 'หยางไฉ่เวย', 'ซ่างกวนจื่อ', 'yang caiwei');
156
+ }
157
+ if (cnName.includes('芸汐传') || enName.toLowerCase().includes('yunxi')) {
158
+ aliases.push('legend of yunxi', '芸汐传', 'หยุนซี', 'ตำนานหยุนซี', 'หานหยุนซี', 'han yunxi');
159
+ }
160
+ if (cnName.includes('新白娘子传奇') || enName.toLowerCase().includes('white snake')) {
161
+ aliases.push('the legend of white snake', '新白娘子传奇', 'นางพญางูขาว', 'ตำนานนางพญางูขาว', 'ไป๋ซู่เจิน', 'bai suzhen');
162
+ }
163
+ if (cnName.includes('仙剑四') || enName.toLowerCase().includes('sword and fairy 4')) {
164
+ aliases.push('sword and fairy 4', '仙剑四', 'เซียนกระบี่ 4', 'เซียนกระบี่พิชิตมาร 4', 'หานหลิงซา', 'han lingsha');
165
+ }
166
+ if (cnName.includes('千香') || enName.toLowerCase().includes('eternal fragrance')) {
167
+ aliases.push('the eternal fragrance', '千香', 'พันกลิ่นบุปผา', 'เจียงหลีเฟย', 'เสี่ยวปั้งฉุย', 'jiang lifei');
168
+ }
169
+ if (cnName.includes('月鳞绮纪') || enName.toLowerCase().includes('veil of shadows')) {
170
+ aliases.push('veil of shadows', '月鳞绮纪', 'ลู่หวู่อี้', 'lu wuyi');
171
+ }
172
+
173
+ entities.push({
174
+ type: 'WORK',
175
+ id: w.id || enName.toLowerCase().replace(/\s+/g, '-'),
176
+ canonicalName: `${thName || enName} (${cnName})`.trim(),
177
+ nameEn: enName,
178
+ nameCn: cnName,
179
+ nameTh: thName,
180
+ category: w.category || 'Drama / Series',
181
+ year: w.year,
182
+ aliases: Array.from(new Set(aliases)),
183
+ recommendedTools: ['search_works', 'get_work_detail', 'get_character_roles', 'global_search'],
184
+ raw: w
185
+ });
186
+ }
187
+
188
+
189
+ for (const r of RUMORED_OR_EXTERNAL_PROJECTS) {
190
+ const aliases = new Set((r.keywords || []).map(k => String(k).toLowerCase()));
191
+ if (r.titleCn) aliases.add(r.titleCn.toLowerCase());
192
+ if (r.titleEn) aliases.add(r.titleEn.toLowerCase());
193
+
194
+ entities.push({
195
+ type: 'WORK',
196
+ id: r.id || String(r.titleCn || r.titleEn || 'rumored-project').toLowerCase().replace(/\s+/g, '-'),
197
+ canonicalName: `${r.titleCn} (${r.titleEn})`.trim(),
198
+ nameEn: r.titleEn,
199
+ nameCn: r.titleCn,
200
+ nameTh: '',
201
+ category: 'Rumored Casting Project',
202
+ year: r.year || null,
203
+ aliases: Array.from(aliases),
204
+ recommendedTools: ['search_works', 'global_search'],
205
+ raw: {
206
+ status: r.status,
207
+ producer: r.producer,
208
+ disambiguation: r.disambiguation
209
+ }
210
+ });
211
+ }
212
+
213
+
214
+ const songs = Array.isArray(discographyData) ? discographyData : (discographyData?.songs || []);
215
+ for (const s of songs) {
216
+ const enName = s.titleEn || '';
217
+ const cnName = s.titleCn || '';
218
+ const thName = s.titleTh || '';
219
+ const aliases = extractAliases(s.id, enName, cnName, thName, s.work);
220
+
221
+ if (Array.isArray(s.aliases)) aliases.push(...s.aliases);
222
+ if (Array.isArray(s.keywords)) aliases.push(...s.keywords);
223
+
224
+ if (s.id?.includes('0-2s') || s.id === '0.2s' || s.id?.includes('02s') || enName.toLowerCase().includes('0.2s') || cnName.includes('0.2秒')) {
225
+ aliases.push('0.2s', '0.2秒', '0.2 s', '0.2 seconds', 'point two seconds', '0.2 วินาที');
226
+ }
227
+ if (s.id?.includes('dewdrop') || s.id?.includes('dew-before-dawn') || enName.toLowerCase().includes('dew') || cnName.includes('露珠')) {
228
+ aliases.push('dewdrops', 'dewdrops dissipate before dawn', 'dew before dawn', 'morning dew', '露珠在晨曦前消散', 'หยาดน้ำค้างสลายก่อนรุ่งอรุณ', 'หยาดน้ำค้าง', 'd-vepdasblo');
229
+ }
230
+
231
+ entities.push({
232
+ type: 'SONG',
233
+ id: s.id || (enName || cnName).toLowerCase().replace(/\s+/g, '-'),
234
+ canonicalName: `${thName || enName} (${cnName})`.trim(),
235
+ nameEn: enName,
236
+ nameCn: cnName,
237
+ nameTh: thName,
238
+ category: s.work || 'Song / Single',
239
+ year: s.year,
240
+ aliases: Array.from(new Set(aliases)),
241
+ recommendedTools: ['search_discography', 'get_music_videos', 'get_media_links'],
242
+ raw: s
243
+ });
244
+ }
245
+
246
+
247
+ const awards = Array.isArray(awardsData) ? awardsData : (awardsData?.awards || []);
248
+ for (const a of awards) {
249
+ const title = a.award || a.title || '';
250
+ const ceremony = a.ceremony || '';
251
+ const aliases = extractAliases(title, ceremony, a.category);
252
+
253
+ entities.push({
254
+ type: 'AWARD',
255
+ id: `award-${a.year || ''}-${title.slice(0, 15).toLowerCase().replace(/\s+/g, '-')}`,
256
+ canonicalName: `${title} (${ceremony})`.trim(),
257
+ nameEn: title,
258
+ nameCn: ceremony,
259
+ category: 'Award',
260
+ year: a.year,
261
+ aliases: Array.from(new Set(aliases)),
262
+ recommendedTools: ['get_awards', 'get_career_timeline'],
263
+ raw: a
264
+ });
265
+ }
266
+
267
+
268
+ const eras = Array.isArray(timelineData) ? timelineData : (timelineData?.eras || []);
269
+ for (const e of eras) {
270
+ const title = e.title || '';
271
+ const era = e.era || '';
272
+ const aliases = extractAliases(title, era, e.titleCn);
273
+ if (Array.isArray(e.highlights)) {
274
+ for (const h of e.highlights) {
275
+ if (h.includes("Princess's Cloak")) aliases.push("Princess's Cloak", "公主披风", "เซ็นเตอร์เพลง");
276
+ if (h.includes("Mike") || h.includes("Mr. Swimmer") || h.includes("ไมค์")) aliases.push("Mr. Swimmer", "游泳先生", "Mike", "ไมค์ พิรัชต์", "ไมค์");
277
+ if (h.includes("4 พฤษภาคม") || h.includes("May 4th")) aliases.push("เหรียญเยาวชน 4 พฤษภาคม", "May 4th Medal", "五四青年奖章");
278
+ if (h.includes("Forbes") || h.includes("30 Under 30")) aliases.push("Forbes 30 Under 30", "30 Under 30", "福布斯");
279
+ }
280
+ }
281
+ if (e.summary) {
282
+ if (e.summary.includes("Princess's Cloak")) aliases.push("Princess's Cloak", "公主披风");
283
+ if (e.summary.includes("230,000")) aliases.push("230,000", "230000");
284
+ }
285
+
286
+ entities.push({
287
+ type: 'TIMELINE',
288
+ id: `era-${(e.id || era || String(e.year || '')).toLowerCase().replace(/\s+/g, '-')}`,
289
+ canonicalName: `${title} (${era})`.trim(),
290
+ nameEn: title,
291
+ nameCn: e.titleCn || '',
292
+ category: 'Career Milestone',
293
+ year: e.year,
294
+ aliases: Array.from(new Set(aliases)),
295
+ recommendedTools: ['get_career_timeline', 'get_profile_and_fandom'],
296
+ raw: e
297
+ });
298
+ }
299
+
300
+ const mvs = Array.isArray(musicVideoData) ? musicVideoData : [];
301
+ for (const m of mvs) {
302
+ const enName = m.titleCn || '';
303
+ const thName = m.titleTh || '';
304
+ const aliases = extractAliases(m.id, enName, thName, m.titlePinyin, m.dramaName);
305
+ if (Array.isArray(m.aliases)) aliases.push(...m.aliases);
306
+ if (Array.isArray(m.keywords)) aliases.push(...m.keywords);
307
+
308
+ if (String(m.titleCn || '').toLowerCase().includes('be my poi') || String(m.id || '').includes('be-my-poi')) {
309
+ aliases.push('be my poi', 'บีมายพอย', 'by my poi', 'be-my-poi');
310
+ }
311
+ if (m.id?.includes('0-2s') || m.id?.includes('02s') || String(m.titleCn || '').includes('0.2s') || String(m.titleTh || '').includes('0.2')) {
312
+ aliases.push('0.2s', '0.2秒', '0.2 s', '0.2 seconds', 'point two seconds', '0.2 วินาที', 'n0yv3dge_z4');
313
+ }
314
+ if (m.id?.includes('dewdrop') || String(m.titleCn || '').includes('露珠') || String(m.titleTh || '').includes('หยาดน้ำค้าง') || String(m.dramaName || '').includes('Veil of Shadows')) {
315
+ aliases.push('dewdrops', 'dewdrops dissipate before dawn', 'dew before dawn', 'morning dew', '露珠在晨曦前消散', 'หยาดน้ำค้างสลายก่อนรุ่งอรุณ', 'หยาดน้ำค้าง', 'd-vepdasblo');
316
+ }
317
+
318
+ entities.push({
319
+ type: 'MEDIA',
320
+ id: m.id,
321
+ canonicalName: `${thName || enName}`.trim(),
322
+ nameEn: enName,
323
+ nameCn: m.titleCn,
324
+ nameTh: thName,
325
+ category: m.category || 'Music Video',
326
+ year: m.releaseYear || m.year,
327
+ aliases: Array.from(new Set(aliases)),
328
+ recommendedTools: ['get_music_videos', 'search_discography', 'get_media_links'],
329
+ raw: m
330
+ });
331
+ }
332
+
333
+
334
+ for (const entity of entities) {
335
+ entity._compiledAliases = [];
336
+ for (const alias of entity.aliases) {
337
+ if (!alias) continue;
338
+ const minAliasLength = /[\u0E00-\u0E7F]/.test(alias) ? 4 : 2;
339
+ if (alias.length < minAliasLength) continue;
340
+
341
+ const isLatin = /^[a-z0-9\s_-]+$/i.test(alias);
342
+ if (isLatin) {
343
+ entity._compiledAliases.push({
344
+ isLatin: true,
345
+ alias,
346
+ regex: new RegExp(`(?:^|[^a-z0-9])${alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}(?:$|[^a-z0-9])`, 'i')
347
+ });
348
+ } else {
349
+ entity._compiledAliases.push({
350
+ isLatin: false,
351
+ alias,
352
+ aliasLower: alias.toLowerCase()
353
+ });
354
+ }
355
+ }
356
+ }
357
+
358
+ cachedIndex = entities;
359
+ return entities;
360
+ }
361
+
362
+ export function levenshteinDistance(s1 = '', s2 = '') {
363
+ if (s1 === s2) return 0;
364
+ if (!s1) return s2 ? s2.length : 0;
365
+ if (!s2) return s1 ? s1.length : 0;
366
+
367
+ const a = String(s1).toLowerCase().trim();
368
+ const b = String(s2).toLowerCase().trim();
369
+ if (a === b) return 0;
370
+
371
+ const m = a.length;
372
+ const n = b.length;
373
+ if (m === 0) return n;
374
+ if (n === 0) return m;
375
+
376
+
377
+ let prev = new Array(n + 1);
378
+ let curr = new Array(n + 1);
379
+
380
+ for (let j = 0; j <= n; j++) prev[j] = j;
381
+
382
+ for (let i = 1; i <= m; i++) {
383
+ curr[0] = i;
384
+ const aChar = a[i - 1];
385
+ for (let j = 1; j <= n; j++) {
386
+ const cost = aChar === b[j - 1] ? 0 : 1;
387
+ curr[j] = Math.min(
388
+ prev[j] + 1,
389
+ curr[j - 1] + 1,
390
+ prev[j - 1] + cost
391
+ );
392
+ }
393
+ const temp = prev;
394
+ prev = curr;
395
+ curr = temp;
396
+ }
397
+ return prev[n];
398
+ }
399
+
400
+ export function levenshteinSimilarity(s1 = '', s2 = '', precomputedDist = null) {
401
+ if (!s1 || !s2) return 0;
402
+ const maxLen = Math.max(s1.length, s2.length);
403
+ if (maxLen === 0) return 1.0;
404
+ const dist = precomputedDist !== null ? precomputedDist : levenshteinDistance(s1, s2);
405
+ return Number((1.0 - (dist / maxLen)).toFixed(3));
406
+ }
407
+
408
+ export function levenshteinScore(s1 = '', s2 = '') {
409
+ const dist = levenshteinDistance(s1, s2);
410
+ const maxLen = Math.max(s1 ? s1.length : 0, s2 ? s2.length : 0);
411
+ const sim = maxLen === 0 ? 1.0 : Number((1.0 - (dist / maxLen)).toFixed(3));
412
+ return { dist, sim };
413
+ }
414
+
415
+ const RESOLUTION_CACHE = new Map();
416
+ const MAX_RESOLUTION_CACHE = 250;
417
+
418
+ export function resolveQueryEntities(query = '') {
419
+ const text = String(query || '').trim();
420
+ if (!text) {
421
+ return {
422
+ matchedEntities: [],
423
+ hasBrand: false,
424
+ hasWork: false,
425
+ hasSong: false,
426
+ hasMedia: false,
427
+ hasAward: false,
428
+ hasTimeline: false,
429
+ isTimelineIntent: false,
430
+ isBrandIntent: false,
431
+ recommendedTools: [],
432
+ platforms: [],
433
+ isPromotion: false,
434
+ primaryBrand: null,
435
+ primaryWork: null,
436
+ primarySong: null,
437
+ contextualChineseKeywords: ['鞠婧祎']
438
+ };
439
+ }
440
+
441
+ const cacheKey = text.toLowerCase();
442
+ const cachedResult = RESOLUTION_CACHE.get(cacheKey);
443
+ if (cachedResult) {
444
+ return {
445
+ ...cachedResult,
446
+ matchedEntities: [...cachedResult.matchedEntities],
447
+ contextualChineseKeywords: [...cachedResult.contextualChineseKeywords]
448
+ };
449
+ }
450
+
451
+ const index = buildCatalogIndex();
452
+ const lowerText = text.toLowerCase();
453
+ const matchedEntities = [];
454
+ const seenEntityIds = new Set();
455
+
456
+ for (const entity of index) {
457
+ const matchers = entity._compiledAliases || [];
458
+ for (const matcher of matchers) {
459
+ const isMatch = matcher.isLatin
460
+ ? matcher.regex.test(lowerText)
461
+ : lowerText.includes(matcher.aliasLower);
462
+
463
+ if (isMatch && !seenEntityIds.has(entity.id)) {
464
+ seenEntityIds.add(entity.id);
465
+ matchedEntities.push(entity);
466
+ break;
467
+ }
468
+ }
469
+ }
470
+
471
+
472
+ if (matchedEntities.length === 0 && lowerText.length >= 4) {
473
+ const rawTokens = lowerText
474
+ .replace(/[^\p{L}\p{N}\s]+/gu, ' ')
475
+ .trim()
476
+ .split(/\s+/)
477
+ .filter(w => w.length >= 2);
478
+
479
+ for (const entity of index) {
480
+ if (seenEntityIds.has(entity.id)) continue;
481
+ for (const alias of entity.aliases) {
482
+ if (!alias || alias.length < 5 || STOPWORDS.has(alias.toLowerCase())) continue;
483
+ const aliasLower = alias.toLowerCase();
484
+ const aliasWords = aliasLower.split(/\s+/);
485
+ const aliasWordCount = aliasWords.length;
486
+
487
+
488
+ if (aliasWordCount >= 2) {
489
+ for (let i = 0; i <= rawTokens.length - aliasWordCount; i++) {
490
+ const subPhrase = rawTokens.slice(i, i + aliasWordCount).join(' ');
491
+ if (subPhrase.length >= 5 && Math.abs(subPhrase.length - aliasLower.length) <= Math.max(2, 0.2 * Math.max(subPhrase.length, aliasLower.length))) {
492
+ const { dist, sim } = levenshteinScore(subPhrase, aliasLower);
493
+ if (sim >= 0.80 || (dist <= 2 && subPhrase.length >= 8)) {
494
+ seenEntityIds.add(entity.id);
495
+ matchedEntities.push({ ...entity, fuzzyMatched: true, matchedAlias: alias });
496
+ break;
497
+ }
498
+ }
499
+ }
500
+ } else {
501
+ if (aliasLower.length >= 5) {
502
+ for (const token of rawTokens) {
503
+ if (STOPWORDS.has(token) || token.length < 5) continue;
504
+ if (Math.abs(token.length - aliasLower.length) <= 1) {
505
+ const { dist, sim } = levenshteinScore(token, aliasLower);
506
+ if (dist <= 1 && sim >= 0.82) {
507
+ seenEntityIds.add(entity.id);
508
+ matchedEntities.push({ ...entity, fuzzyMatched: true, matchedAlias: alias });
509
+ break;
510
+ }
511
+ }
512
+ }
513
+ }
514
+ }
515
+ if (seenEntityIds.has(entity.id)) break;
516
+ }
517
+ }
518
+ }
519
+
520
+ const platforms = detectPlatforms(text);
521
+ const isPromotion = detectPromotionIntent(text);
522
+
523
+ const brands = matchedEntities.filter(e => e.type === 'BRAND');
524
+ const works = matchedEntities.filter(e => e.type === 'WORK');
525
+ const songs = matchedEntities.filter(e => e.type === 'SONG' || e.type === 'MEDIA');
526
+ const awards = matchedEntities.filter(e => e.type === 'AWARD');
527
+ const timelines = matchedEntities.filter(e => e.type === 'TIMELINE');
528
+ const medias = matchedEntities.filter(e => e.type === 'MEDIA');
529
+
530
+ const cnTerms = new Set(['鞠婧祎']);
531
+ for (const e of matchedEntities) {
532
+ if (e.nameCn) {
533
+ const cleanCn = e.nameCn.replace(/\s*\([^)]*\)/g, '').trim();
534
+ if (cleanCn) cnTerms.add(cleanCn);
535
+ }
536
+ }
537
+
538
+ const isTimelineIntent = Boolean(
539
+ /ไทม์ไลน์|timeline|chronology|chronological|时间线|时间轴|按时间线|从时间线|จากไทม์ไลน์|ตามไทม์ไลน์|ในปี\s*\d{4}|in\s*(?:the\s*year\s*)?(?:19\d\d|20\d\d)|(ปี|พ\.ศ\.)\s*\d{4}|(milestone|ประวัติการทำงาน|เส้นทางอาชีพ|เส้นทางในวงการ)/i.test(text) ||
540
+ timelines.length > 0
541
+ );
542
+ const isBrandIntent = Boolean(
543
+ isPromotion ||
544
+ /แบรนด์|พรีเซนเตอร์|แอมบาสเดอร์|ambassador|endorsement|tutucare|dafuyan|lafang|fred|prsr|กล่องของขวัญ|สถิติยอดขาย|ยอดขาย/i.test(text) ||
545
+ brands.length > 0
546
+ );
547
+
548
+ const recommendedTools = new Set();
549
+ if (timelines.length > 0 || isTimelineIntent) recommendedTools.add('get_career_timeline');
550
+ if (brands.length > 0 || isBrandIntent) recommendedTools.add('get_brand_endorsements');
551
+ if (works.length > 0) recommendedTools.add('search_works');
552
+ if (songs.length > 0) recommendedTools.add('search_discography');
553
+ if (medias.length > 0) recommendedTools.add('get_music_videos');
554
+ if (awards.length > 0) recommendedTools.add('get_awards');
555
+
556
+ const result = {
557
+ matchedEntities,
558
+ hasBrand: brands.length > 0 || isBrandIntent,
559
+ hasWork: works.length > 0,
560
+ hasSong: songs.length > 0,
561
+ hasMedia: medias.length > 0,
562
+ hasAward: awards.length > 0,
563
+ hasTimeline: timelines.length > 0 || isTimelineIntent,
564
+ isTimelineIntent,
565
+ isBrandIntent,
566
+ recommendedTools: Array.from(recommendedTools),
567
+ platforms,
568
+ isPromotion,
569
+ primaryBrand: brands[0] || null,
570
+ primaryWork: works[0] || null,
571
+ primarySong: songs[0] || null,
572
+ contextualChineseKeywords: Array.from(cnTerms)
573
+ };
574
+
575
+ if (RESOLUTION_CACHE.size >= MAX_RESOLUTION_CACHE) {
576
+ RESOLUTION_CACHE.delete(RESOLUTION_CACHE.keys().next().value);
577
+ }
578
+ RESOLUTION_CACHE.set(cacheKey, result);
579
+
580
+ return result;
581
+ }
582
+
583
+ export function getContextualizedSearchQuery(keyword = '', platform = 'bilibili') {
584
+ const raw = String(keyword || '').trim();
585
+ if (!raw) return '鞠婧祎';
586
+
587
+ const resolution = resolveQueryEntities(raw);
588
+
589
+ if (platform === 'bilibili' || platform === 'chinese_web') {
590
+ const parts = ['鞠婧祎'];
591
+
592
+
593
+ if (resolution.primaryBrand) {
594
+ const cleanCn = resolution.primaryBrand.nameCn.replace(/\s*\([^)]*\)/g, '').trim();
595
+ if (cleanCn && !parts.includes(cleanCn)) parts.push(cleanCn);
596
+ if (resolution.primaryBrand.nameEn && !parts.includes(resolution.primaryBrand.nameEn)) {
597
+
598
+ const acronym = resolution.primaryBrand.nameEn.split('/')[0].trim();
599
+ if (acronym && !parts.includes(acronym)) parts.push(acronym);
600
+ }
601
+ } else if (resolution.primaryWork) {
602
+ const cleanCn = resolution.primaryWork.nameCn.replace(/\s*\([^)]*\)/g, '').trim();
603
+ if (cleanCn && !parts.includes(cleanCn)) parts.push(cleanCn);
604
+ } else if (resolution.primarySong) {
605
+ const cleanCn = resolution.primarySong.nameCn.replace(/\s*\([^)]*\)/g, '').trim();
606
+ if (cleanCn && !parts.includes(cleanCn)) parts.push(cleanCn);
607
+ } else {
608
+
609
+ const cleanTokens = raw
610
+ .replace(/^(?:ค้นหา|หา|ดู|เปิด|คลิป|การโปรโมท|โปรโมท|บน|ใน|ที่|bilibili|b站)\s*/gi, '')
611
+ .trim();
612
+ if (cleanTokens && !cleanTokens.includes('鞠婧祎')) {
613
+ parts.push(cleanTokens);
614
+ }
615
+ }
616
+
617
+ if (resolution.isPromotion && !parts.some(p => p.includes('代言') || p.includes('广告') || p.includes('活动'))) {
618
+ parts.push('代言');
619
+ }
620
+
621
+ return parts.join(' ').trim();
622
+ }
623
+
624
+
625
+ if (!raw.toLowerCase().includes('ju jingyi') && !raw.includes('鞠婧祎')) {
626
+ return `Ju Jingyi 鞠婧祎 ${raw}`;
627
+ }
628
+ return raw;
629
+ }