kiku-agent 1.3.2 → 1.3.3

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 (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -11,7 +11,7 @@ let cachedIndex = null;
11
11
 
12
12
  const PLATFORM_PATTERNS = [
13
13
  { key: 'bilibili', regex: /(?:bilibili|b站|b-site|บิลิบิลิ|บิลิ)/i, cnName: 'B站' },
14
- { key: 'youtube', regex: /(?:youtube|yt|ยูทูป|ยูทูบ)/i, cnName: 'YouTube' },
14
+ { key: 'youtube', regex: /(?:youtube|\byt\b|ยูทูป|ยูทูบ)/i, cnName: 'YouTube' },
15
15
  { key: 'weibo', regex: /(?:weibo|微博|เวยปั๋ว|เว่ยป๋อ)/i, cnName: '微博' },
16
16
  { key: 'douyin', regex: /(?:douyin|tiktok|抖音|โต่วอิน)/i, cnName: '抖音' },
17
17
  { key: 'qqmusic', regex: /(?:qqmusic|qq\s*音乐|qq\s*music|คิวคิว)/i, cnName: 'QQ音乐' },
@@ -211,7 +211,14 @@ export function buildCatalogIndex() {
211
211
  }
212
212
 
213
213
 
214
- const songs = Array.isArray(discographyData) ? discographyData : (discographyData?.songs || []);
214
+ const songs = Array.isArray(discographyData)
215
+ ? discographyData
216
+ : ([
217
+ ...(discographyData?.songs || []),
218
+ ...(discographyData?.latestReleases || []),
219
+ ...(discographyData?.singles || []),
220
+ ...(discographyData?.ostList || []),
221
+ ]);
215
222
  for (const s of songs) {
216
223
  const enName = s.titleEn || '';
217
224
  const cnName = s.titleCn || '';
@@ -224,8 +231,8 @@ export function buildCatalogIndex() {
224
231
  if (s.id?.includes('0-2s') || s.id === '0.2s' || s.id?.includes('02s') || enName.toLowerCase().includes('0.2s') || cnName.includes('0.2秒')) {
225
232
  aliases.push('0.2s', '0.2秒', '0.2 s', '0.2 seconds', 'point two seconds', '0.2 วินาที');
226
233
  }
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');
234
+ if (s.id?.includes('dewdrop') || s.id?.includes('dew-before-dawn') || enName.toLowerCase().includes('dew') || cnName.includes('露珠') || cnName.includes('露水')) {
235
+ aliases.push('dewdrops', 'dewdrops dissipate before dawn', 'dew before dawn', 'morning dew', '露珠在晨曦前消散', '露水', 'หยาดน้ำค้างสลายก่อนรุ่งอรุณ', 'หยาดน้ำค้าง', 'd-vepdasblo');
229
236
  }
230
237
 
231
238
  entities.push({
@@ -246,13 +253,13 @@ export function buildCatalogIndex() {
246
253
 
247
254
  const awards = Array.isArray(awardsData) ? awardsData : (awardsData?.awards || []);
248
255
  for (const a of awards) {
249
- const title = a.award || a.title || '';
256
+ const title = a.award || a.title || a.titleEn || a.titleTh || a.titleCn || a.ceremony || '';
250
257
  const ceremony = a.ceremony || '';
251
- const aliases = extractAliases(title, ceremony, a.category);
258
+ const aliases = extractAliases(a.id, title, ceremony, a.category, a.titleTh, a.titleCn, a.work);
252
259
 
253
260
  entities.push({
254
261
  type: 'AWARD',
255
- id: `award-${a.year || ''}-${title.slice(0, 15).toLowerCase().replace(/\s+/g, '-')}`,
262
+ id: a.id || `award-${a.year || ''}-${title.slice(0, 15).toLowerCase().replace(/\s+/g, '-')}`,
256
263
  canonicalName: `${title} (${ceremony})`.trim(),
257
264
  nameEn: title,
258
265
  nameCn: ceremony,
@@ -299,9 +306,10 @@ export function buildCatalogIndex() {
299
306
 
300
307
  const mvs = Array.isArray(musicVideoData) ? musicVideoData : [];
301
308
  for (const m of mvs) {
302
- const enName = m.titleCn || '';
309
+ const enName = m.titleEn || m.titlePinyin || m.titleCn || '';
310
+ const cnName = m.titleCn || '';
303
311
  const thName = m.titleTh || '';
304
- const aliases = extractAliases(m.id, enName, thName, m.titlePinyin, m.dramaName);
312
+ const aliases = extractAliases(m.id, enName, cnName, thName, m.titlePinyin, m.dramaName);
305
313
  if (Array.isArray(m.aliases)) aliases.push(...m.aliases);
306
314
  if (Array.isArray(m.keywords)) aliases.push(...m.keywords);
307
315
 
@@ -311,8 +319,8 @@ export function buildCatalogIndex() {
311
319
  if (m.id?.includes('0-2s') || m.id?.includes('02s') || String(m.titleCn || '').includes('0.2s') || String(m.titleTh || '').includes('0.2')) {
312
320
  aliases.push('0.2s', '0.2秒', '0.2 s', '0.2 seconds', 'point two seconds', '0.2 วินาที', 'n0yv3dge_z4');
313
321
  }
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');
322
+ if (m.id?.includes('dewdrop') || String(m.titleCn || '').includes('露珠') || String(m.titleCn || '').includes('露水') || String(m.titleTh || '').includes('หยาดน้ำค้าง')) {
323
+ aliases.push('dewdrops', 'dewdrops dissipate before dawn', 'dew before dawn', 'morning dew', '露水', '露珠在晨曦前消散', 'หยาดน้ำค้างสลายก่อนรุ่งอรุณ', 'หยาดน้ำค้าง', 'd-vepdasblo');
316
324
  }
317
325
 
318
326
  entities.push({
@@ -320,7 +328,7 @@ export function buildCatalogIndex() {
320
328
  id: m.id,
321
329
  canonicalName: `${thName || enName}`.trim(),
322
330
  nameEn: enName,
323
- nameCn: m.titleCn,
331
+ nameCn: cnName,
324
332
  nameTh: thName,
325
333
  category: m.category || 'Music Video',
326
334
  year: m.releaseYear || m.year,
@@ -545,13 +553,18 @@ export function resolveQueryEntities(query = '') {
545
553
  brands.length > 0
546
554
  );
547
555
 
556
+ const isAwardIntent = Boolean(
557
+ /รางวัล|เกียรติยศ|awards?|honou?rs?|奖项|荣誉|weibo\s+night|微博之夜/i.test(text) ||
558
+ awards.length > 0
559
+ );
560
+
548
561
  const recommendedTools = new Set();
549
562
  if (timelines.length > 0 || isTimelineIntent) recommendedTools.add('get_career_timeline');
550
563
  if (brands.length > 0 || isBrandIntent) recommendedTools.add('get_brand_endorsements');
551
564
  if (works.length > 0) recommendedTools.add('search_works');
552
565
  if (songs.length > 0) recommendedTools.add('search_discography');
553
566
  if (medias.length > 0) recommendedTools.add('get_music_videos');
554
- if (awards.length > 0) recommendedTools.add('get_awards');
567
+ if (awards.length > 0 || isAwardIntent) recommendedTools.add('get_awards');
555
568
 
556
569
  const result = {
557
570
  matchedEntities,
@@ -559,10 +572,11 @@ export function resolveQueryEntities(query = '') {
559
572
  hasWork: works.length > 0,
560
573
  hasSong: songs.length > 0,
561
574
  hasMedia: medias.length > 0,
562
- hasAward: awards.length > 0,
575
+ hasAward: awards.length > 0 || isAwardIntent,
563
576
  hasTimeline: timelines.length > 0 || isTimelineIntent,
564
577
  isTimelineIntent,
565
578
  isBrandIntent,
579
+ isAwardIntent,
566
580
  recommendedTools: Array.from(recommendedTools),
567
581
  platforms,
568
582
  isPromotion,
@@ -580,29 +594,132 @@ export function resolveQueryEntities(query = '') {
580
594
  return result;
581
595
  }
582
596
 
597
+ export const THAI_INTENT_MAP = [
598
+ [/(?:เต้น|dance|dancing|เต้นเพลง)/i, '舞蹈 舞台'],
599
+ [/(?:ร้องสด|live|เปิดไมค์|ไลฟ์|全开麦)/i, '现场 全开麦'],
600
+ [/(?:เวที|stage|สเตจ)/i, '舞台'],
601
+ [/(?:คอนเสิร์ต|concert)/i, '演唱会'],
602
+ [/(?:สัมภาษณ์|interview)/i, '采访'],
603
+ [/(?:เบื้องหลัง|bts|behind)/i, '花絮'],
604
+ [/(?:แฟนแคม|fancam|แคม|直拍)/i, '直拍'],
605
+ [/(?:วาไรตี้|variety|รายการ|综艺)/i, '综艺'],
606
+ [/(?:พรมแดง|แฟชั่น|fashion|red\s*carpet|红毯)/i, '红毯 造型'],
607
+ [/(?:เพลงประกอบ|ost|原声带)/i, 'OST 原声带'],
608
+ [/(?:เอ็มวี|\bmv\b|มิวสิควิดีโอ)/i, 'MV 官方'],
609
+ [/(?:ซีรีส์|ละคร|drama|电视剧)/i, '电视剧'],
610
+ [/(?:ภาพยนตร์|หนัง|movie|电影)/i, '电影'],
611
+ [/(?:โฆษณา|แคมเปญ|commercial|cf|广告)/i, '广告 代言'],
612
+ [/(?:ข่าวล่าสุด|ข่าวใหม่|ข่าวสาร|ข่าว|news|update|ความเคลื่อนไหว|อัปเดต|近况|最新)/i, '最新消息 动态'],
613
+ [/(?:เว่ยป๋อ|เว่ยโป๋|weibo|微博|超话)/i, '微博 超话'],
614
+ [/(?:งาน|อีเวนต์|ออกงาน|งานอีเวนต์|กิจกรรม|event)/i, '活动 现场'],
615
+ [/(?:ถ่ายทำ|กองถ่าย|เปิดกล้อง|ปิดกล้อง|路透|开机)/i, '剧组 路透 开机'],
616
+ [/(?:สไตล์|การแต่งตัว|ชุด|เสื้อผ้า|ลุค)/i, '穿搭 造型 美图'],
617
+ ];
618
+
619
+ export const ARTIST_DIRECT_PATTERN = /(ju\s?jing\s?yi|jujingyi|จวีจิ้งอี|จิ้งอี|เสี่ยวจวี|เสี่ยว|鞠婧祎|小鞠|\bkiku\b|kikuuu?|蜜橘)/i;
620
+
621
+ const EXPLICIT_OPEN_DOMAIN_PATTERNS = [
622
+
623
+ /(?:คณิตศาสตร์|คณิต|แก้โจทย์|โจทย์เลข|คิดเลข|คำนวณ|สมการ|อสมการ|แคลคูลัส|อินทิกรัล|อินทิเกรต|อนุพันธ์|ดิฟ|เมทริกซ์|เวกเตอร์|พีชคณิต|เรขาคณิต|ตรีโกณมิติ|สถิติ|ความน่าจะเป็น|ทฤษฎีบท|พิสูจน์|จำนวนจริง|จำนวนเต็ม|ตรรกศาสตร์|\bmath(?:ematics)?\b|\bcalculus\b|\balgebra\b|\bintegral\b|\bderivative\b|\bmatrix(?:es)?\b|\bvectors?\b|\bgeometry\b|\btrigonometry\b|\bprobability\b|\bstatistics\b|\bequations?\b|\bdifferential\b|\barithmetic\b|\bdiscrete math\b|\blinear algebra\b)/i,
624
+
625
+
626
+ /(?:วิทยาศาสตร์|ฟิสิกส์|เคมี|ชีววิทยา|ดาราศาสตร์|กลศาสตร์|เทอร์โมไดนามิกส์|พันธุศาสตร์|เซลล์|อะตอม|โมเลกุล|ตารางธาตุ|ปฏิกิริยาเคมี|กรดเบส|ไฟฟ้าสถิต|วงจรไฟฟ้า|แม่เหล็กไฟฟ้า|คลื่นแม่เหล็ก|โฟตอน|สัมพัทธภาพ|ควอนตัม|หลุมดำ|ระบบสุริยะ|ดวงอาทิตย์|แรงโน้มถ่วง|ไอน์สไตน์|นิวตัน|\bscience\b|\bphysics\b|\bchemistry\b|\bbiology\b|\bastronomy\b|\bthermodynamics\b|\bgenetics\b|\bperiodic table\b|\bquantum\b|\brelativity\b|\bnewton\b|\beinstein\b|\belectromagnetism\b|\bcellular\b)/i,
627
+
628
+
629
+ /(?:เขียนโค้ด|เขียนโปรแกรม|แก้บั๊ก|แก้โค้ด|สอนเขียนโค้ด|ตัวอย่างโค้ด|อัลกอริทึม|โครงสร้างข้อมูล|สคริปต์|ฟังก์ชัน|ภาษา\s*(?:python|javascript|typescript|c\+\+|java|golang|rust|php|swift|kotlin|c#|sql|ruby)|html|css|react|vue|node|api|fastapi|express|docker|kubernetes|linux|windows|database|sql|nosql|github|git|backend|frontend|fullstack|\bcoding\b|\bprogramming\b|\bwrite code\b|\bdebug(?:ging)?\b|\brefactor(?:ing)?\b|\balgorithms?\b|\bdata structures?\b|\bpython\b|\bjavascript\b|\btypescript\b|\bgolang\b|\brustlang\b|\bc\+\+\b|\bjava\b)/i,
630
+
631
+
632
+ /(?:วิธีทำ|สูตรทำ|สูตรอาหาร|ต้มยำ|ผัดกะเพรา|แกง|ขนม|ทำอาหาร|cooking|recipe|food|ingredients|ส่วนผสม)/i,
633
+
634
+
635
+ /(?:สภาพอากาศ|พยากรณ์อากาศ|อุณหภูมิ|ฝนตกไหม|weather|forecast|temperature|rain)/i,
636
+ /(?:ท่องเที่ยว|สถานที่ท่องเที่ยว|ที่เที่ยวใน|โรงแรม|การเดินทาง|attractions|places\s*to\s*visit|travel\s*to)/i,
637
+
638
+
639
+ /(?:bitcoin|บิตคอยน์|crypto|คริปโต|หุ้น|ตลาดหุ้น|ราคาทอง|ดอลลาร์|อัตราแลกเปลี่ยน|exchange\s*rate|inflation|เงินเฟ้อ)/i,
640
+ /(?:ประธานาธิบดี|นายกรัฐมนตรี|รัฐบาล|การเลือกตั้ง|president|prime\s*minister|parliament|election)/i,
641
+ /(?:ฟุตบอล|พรีเมียร์ลีก|ลิเวอร์พูล|แมนยู|อาร์เซนอล|เชลซี|เรอัลมาดริด|บาร์เซโลนา|football|premier\s*league|liverpool|manchester|world\s*cup)/i,
642
+
643
+
644
+ /(?:taylor\s*swift|เทย์เลอร์|blackpink|แบล็กพิงก์|bts|บีทีเอส|lisa|ลิซ่า|rose|jennie|jisoo|jackson\s*wang|แจ็คสัน|bai\s*lu|ไป๋ลู่|yang\s*zi|หยางจื่อ|zhao\s*lusi|จ้าวลู่ซือ|dilraba|ตี๋ลี่เร่อปา|wang\s*yibo|หวังอี้ป๋อ|xiao\s*zhan|เซียวจ้าน|chen\s*zheyuan|เฉินเจ๋อย่วน)/i,
645
+ /(?:harry\s*potter|แฮร์รี่\s*พอตเตอร์|star\s*wars|สตาร์วอร์ส|marvel|avengers|อเวนเจอร์ส|batman|superman|spiderman|game\s*of\s*thrones|lord\s*of\s*the\s*rings|crash\s*landing|queen\s*of\s*tears)/i,
646
+ ];
647
+
648
+ export const QUERY_DOMAINS = Object.freeze({
649
+ IN_DOMAIN: 'IN_DOMAIN',
650
+ OPEN_DOMAIN: 'OPEN_DOMAIN'
651
+ });
652
+
653
+ export function classifyQuerySpectrum(query = '') {
654
+ const text = String(query || '').trim();
655
+ if (!text) {
656
+ return { bias: 1.0, inScore: 1, openScore: 0, verdict: 'IN_DOMAIN' };
657
+ }
658
+
659
+ let inScore = 0;
660
+ let openScore = 0;
661
+
662
+ if (ARTIST_DIRECT_PATTERN.test(text)) inScore += 6;
663
+ if (/^(?:สวัสดี(?:ครับ|ค่ะ|จ้า|นะ)?|hello|hi|hey|หวัดดี(?:ครับ|ค่ะ)?|ดีครับ|ดีค่ะ|good\s*(?:morning|afternoon|evening))[\s!.]*$/i.test(text)) inScore += 5;
664
+ if (/(?:เล่นเพลง|หยุดเพลง|พักเพลง|เล่นต่อ|ข้ามเพลง|เพลงถัดไป|เพลงก่อนหน้า|ปิดเพลง|ปรับเสียง|เพิ่มเสียง|ลดเสียง|ปิดเสียง|เปิดเสียง|\bmute\b|\bunmute\b|\bvolume\b|\bpause\b|\bresume\b|\bnext\b|\bprev\b|\bseek\b|\bshuffle\b|\brepeat\b|\bloop\b|วนซ้ำ|สุ่มเพลง|กรอเพลง|toggle_video)/i.test(text)) inScore += 5;
665
+ if (/(?:4000[\s-]*(?:ปี|year)|ไอดอล\s*4000|snh48|塞纳河|kikufan|เสี่ยวจวี)/i.test(text)) inScore += 4;
666
+ if (/(ตาม|ใน|จาก)?(แค็ตตาล็อก|catalog|ฐานข้อมูล|database|ข้อมูลทางการ|บันทึกทางการ)/i.test(text)) inScore += 4;
667
+
668
+ const resolution = resolveQueryEntities(text);
669
+ if (resolution.matchedEntities.length > 0) inScore += 4 * resolution.matchedEntities.length;
670
+ if (resolution.hasBrand) inScore += 4;
671
+ if (resolution.hasWork) inScore += 4;
672
+ if (resolution.hasSong) inScore += 4;
673
+ if (resolution.isPromotion) inScore += 3;
674
+
675
+ for (const pat of EXPLICIT_OPEN_DOMAIN_PATTERNS) {
676
+ if (pat.test(text)) openScore += 5;
677
+ }
678
+
679
+ if (/(?:เธอ|เขา|ศิลปิน|she|her)\b/i.test(text) && /(?:ผลงาน|เพลง|ละคร|ซีรีส์|วันเกิด|ประวัติ|ดนตรี|แฟนคลับ|แบรนด์|รางวัล|song|drama|music|album|award|birthday|concert|profile|fandom)/i.test(text)) {
680
+ inScore += 3;
681
+ }
682
+
683
+ if (/(?:ผลงานล่าสุด|ผลงานใหม่|เพลงล่าสุด|เพลงใหม่|ละครล่าสุด|ซีรีส์ล่าสุด|รูปสวยๆ|ภาพถ่ายล่าสุด|แฟชั่นล่าสุด|ข่าวล่าสุด|ตารางงาน|งานแฟนมีต)/i.test(text)) {
684
+ inScore += 3;
685
+ }
686
+
687
+ if (inScore === 0 && openScore === 0) {
688
+ openScore = 1;
689
+ }
690
+
691
+ const bias = inScore / (inScore + openScore);
692
+ const verdict = inScore > openScore ? 'IN_DOMAIN' : 'OPEN_DOMAIN';
693
+
694
+ return { bias, inScore, openScore, verdict };
695
+ }
696
+
697
+ export function classifyQueryDomain(query = '') {
698
+ return classifyQuerySpectrum(query).verdict;
699
+ }
700
+
701
+ export function isQueryInDomain(query = '') {
702
+ return classifyQueryDomain(query) === 'IN_DOMAIN';
703
+ }
704
+
583
705
  export function getContextualizedSearchQuery(keyword = '', platform = 'bilibili') {
584
706
  const raw = String(keyword || '').trim();
585
707
  if (!raw) return '鞠婧祎';
586
708
 
587
- const resolution = resolveQueryEntities(raw);
588
-
589
- const ARTIST_RE = /(ju\s?jing\s?yi|jujingyi|จวีจิ้งอี|จิ้งอี|鞠婧祎|小鞠|kikuuu?|蜜橘)/i;
590
- const mentionsArtist = ARTIST_RE.test(raw);
591
- const hasCatalogMatch = resolution.matchedEntities.length > 0;
592
-
593
- if (!mentionsArtist && !hasCatalogMatch) {
709
+ const domain = classifyQueryDomain(raw);
710
+ if (domain === 'OPEN_DOMAIN') {
594
711
  return raw;
595
712
  }
596
713
 
714
+ const resolution = resolveQueryEntities(raw);
715
+
597
716
  if (platform === 'bilibili' || platform === 'chinese_web') {
598
717
  const parts = ['鞠婧祎'];
599
718
 
600
-
601
719
  if (resolution.primaryBrand) {
602
720
  const cleanCn = resolution.primaryBrand.nameCn.replace(/\s*\([^)]*\)/g, '').trim();
603
721
  if (cleanCn && !parts.includes(cleanCn)) parts.push(cleanCn);
604
722
  if (resolution.primaryBrand.nameEn && !parts.includes(resolution.primaryBrand.nameEn)) {
605
-
606
723
  const acronym = resolution.primaryBrand.nameEn.split('/')[0].trim();
607
724
  if (acronym && !parts.includes(acronym)) parts.push(acronym);
608
725
  }
@@ -612,13 +729,39 @@ export function getContextualizedSearchQuery(keyword = '', platform = 'bilibili'
612
729
  } else if (resolution.primarySong) {
613
730
  const cleanCn = resolution.primarySong.nameCn.replace(/\s*\([^)]*\)/g, '').trim();
614
731
  if (cleanCn && !parts.includes(cleanCn)) parts.push(cleanCn);
732
+ if (resolution.primarySong.titleEn && !parts.includes(resolution.primarySong.titleEn)) {
733
+ parts.push(resolution.primarySong.titleEn);
734
+ }
615
735
  } else {
616
-
617
- const cleanTokens = raw
736
+ let cleanTokens = raw
618
737
  .replace(/^(?:ค้นหา|หา|ดู|เปิด|คลิป|การโปรโมท|โปรโมท|บน|ใน|ที่|bilibili|b站)\s*/gi, '')
619
738
  .trim();
739
+ for (const [re, cnTerm] of THAI_INTENT_MAP) {
740
+ if (re.test(cleanTokens)) {
741
+ if (!parts.includes(cnTerm)) parts.push(cnTerm);
742
+ cleanTokens = cleanTokens.replace(re, ' ');
743
+ }
744
+ }
745
+ cleanTokens = cleanTokens
746
+ .replace(/(?:จวีจิ้งอี|จิ้งอี|鞠婧祎|小鞠|ju\s*jing\s*yi|kiku)/gi, ' ')
747
+ .replace(/(?:มีไหม|อะไร|บ้าง|ของ|หน่อย|ล่าสุด|ไหม|ข่าวล่าสุด|ข่าวใหม่|ข่าวสาร|ข่าว|หรือมี|ช่วงนี้|ตอนนี้|ที่ไหน|อย่างไร|เป็นไง|ใหม่ๆ|มี|ใน)/gi, ' ')
748
+ .replace(/\s+/g, ' ')
749
+ .trim();
620
750
  if (cleanTokens && !cleanTokens.includes('鞠婧祎')) {
621
- parts.push(cleanTokens);
751
+ if (platform === 'chinese_web' || platform === 'bilibili') {
752
+ const nonThaiWords = cleanTokens.split(/\s+/).filter(w => !/^[\u0E00-\u0E7F]+$/.test(w));
753
+ if (nonThaiWords.length > 0) {
754
+ parts.push(nonThaiWords.join(' '));
755
+ }
756
+ } else {
757
+ parts.push(cleanTokens);
758
+ }
759
+ }
760
+ }
761
+
762
+ for (const [re, cnTerm] of THAI_INTENT_MAP) {
763
+ if (re.test(raw) && !parts.some(p => p.includes(cnTerm.split(' ')[0]))) {
764
+ parts.push(cnTerm);
622
765
  }
623
766
  }
624
767
 
@@ -626,10 +769,9 @@ export function getContextualizedSearchQuery(keyword = '', platform = 'bilibili'
626
769
  parts.push('代言');
627
770
  }
628
771
 
629
- return parts.join(' ').trim();
772
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
630
773
  }
631
774
 
632
-
633
775
  if (!raw.toLowerCase().includes('ju jingyi') && !raw.includes('鞠婧祎')) {
634
776
  return `Ju Jingyi 鞠婧祎 ${raw}`;
635
777
  }
@@ -53,7 +53,7 @@ const AMBIGUITY_PATTERNS = [
53
53
  {
54
54
  category: 'scope',
55
55
  weight: 0.6,
56
- detection: /\b(all|every|list|show|tell me about|what about|how about|comprehensive|complete)\b|(ทั้งหมด|ทุกอย่าง|ภาพรวม|全部|一覧|まとめ|모든|전체|목록)/i,
56
+ detection: /\b(all|every|list|show|tell me about|what about|how about|comprehensive|complete|explain)\b|(ทั้งหมด|ทุกอย่าง|ภาพรวม|เล่าเรื่อง|ช่วยเล่า|เรื่องย่อ|เป็นยังไง|สนุกไหม|全部|一覧|まとめ|모든|전체|목록)/i,
57
57
  questions: {
58
58
  en: 'Would you like a comprehensive overview or a focused summary on a specific aspect?',
59
59
  th: 'ต้องการภาพรวมที่ครอบคลุม หรือต้องการเน้นเฉพาะด้านใดด้านหนึ่ง?',
@@ -253,7 +253,7 @@ function localizePattern(pattern, lang) {
253
253
  };
254
254
  }
255
255
 
256
- const KNOWN_WORKS_OR_CHARACTERS_REGEX = /(花间令|花間令|In Blossom|ฮวาเจียนลิ่ง|芸汐传|芸汐伝|Yunxi|หยุนซี|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|The Blooms at Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|杨采薇|上官芷|หยางไฉ่เวย|ซ่างกวนจื่อ|韩芸汐|หานหยุนซี|白素贞|ไป๋ซู่เจิน|傅容|ฟู่หรง|姜保宁|เจียงเป่าหนิง|韩菱纱|หานหลิงซา)/i;
256
+ export const KNOWN_WORKS_OR_CHARACTERS_REGEX = /(花间令|花間令|In Blossom|ฮวาเจียนลิ่ง|ฮวาเจียนหลิง|อินบลอสซัม|芸汐传|芸汐伝|Yunxi|หยุนซี|ตำนานหยุนซี|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|The Blooms at Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|กำเนิดใหม่เพื่อเธอ|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|千香|千香引|The Eternal Fragrance|พันกลิ่นบุปผา|花戎|Beauty of Resilience|ฮวาหรง|ตำนานเลือดฟีนิกซ์|月鳞绮纪|Veil of Shadows|เยว่หลินฉีจี้|ม่านเงาจันทรา|杨采薇|上官芷|หยางไฉ่เวย|ซ่างกวนจื่อ|韩芸汐|หานหยุนซี|白素贞|ไป๋ซู่เจิน|傅容|ฟู่หรง|姜保宁|เจียงเป่าหนิง|韩菱纱|หานหลิงซา|魏枝|เว่ยจือ|雷修远|เหลยซิวหย่วน|陆吾一|ลู่หวู่อี้)/i;
257
257
 
258
258
  const SPECIFICITY_PATTERNS = [
259
259
  /\b(exactly|specifically|precisely|particular)\b/i,
@@ -278,13 +278,17 @@ export function calculateConfidence(query, detectedAmbiguities = []) {
278
278
  else if (wordCount < 5) score -= 0.1;
279
279
  else if (wordCount > 15) score += 0.1;
280
280
 
281
- if (KNOWN_WORKS_OR_CHARACTERS_REGEX.test(trimmed)) {
282
- return {
283
- score: 0.95,
284
- level: 'high',
285
- shouldAsk: false,
286
- reasoning: 'High confidence: specific work or character explicitly identified in query.'
287
- };
281
+ const hasKnownWork = KNOWN_WORKS_OR_CHARACTERS_REGEX.test(trimmed);
282
+ if (hasKnownWork) {
283
+ if (detectedAmbiguities.length === 0) {
284
+ return {
285
+ score: 0.95,
286
+ level: 'high',
287
+ shouldAsk: false,
288
+ reasoning: 'High confidence: specific work or character explicitly identified in query without ambiguities.'
289
+ };
290
+ }
291
+ score += 0.2;
288
292
  }
289
293
 
290
294
  for (const pattern of SPECIFICITY_PATTERNS) {
@@ -342,7 +346,19 @@ export function detectAmbiguities(query, detectedLang = 'en') {
342
346
  const KNOWN_ENTITY_PATTERNS = [
343
347
  {
344
348
  key: 'in_blossom',
345
- labels: { en: 'In Blossom (花间令)', th: 'อินบลอสซัม/ฮวาเจียนหลิง (In Blossom)', zh: '《花间令》', ja: '『花間令』(In Blossom)', ko: 'In Blossom (화간령)' },
349
+ labels: { en: 'In Blossom (花间令)', th: 'ฮวาเจียนลิ่ง/ฮวาเจียนหลิง (In Blossom)', zh: '《花间令》', ja: '『花間令』(In Blossom)', ko: 'In Blossom (화간령)' },
350
+ },
351
+ {
352
+ key: 'the_eternal_fragrance',
353
+ labels: { en: 'The Eternal Fragrance (千香)', th: 'พันกลิ่นบุปผา (The Eternal Fragrance)', zh: '《千香》', ja: '『千香』(The Eternal Fragrance)', ko: 'The Eternal Fragrance (천향)' },
354
+ },
355
+ {
356
+ key: 'beauty_of_resilience',
357
+ labels: { en: 'Beauty of Resilience (花戎)', th: 'ตำนานเลือดฟีนิกซ์/ฮวาหรง (Beauty of Resilience)', zh: '《花戎》', ja: '『花戎』(Beauty of Resilience)', ko: 'Beauty of Resilience (화융)' },
358
+ },
359
+ {
360
+ key: 'veil_of_shadows',
361
+ labels: { en: 'Veil of Shadows (月鳞绮纪)', th: 'เยว่หลินฉีจี้/ม่านเงาจันทรา (Veil of Shadows)', zh: '《月鳞绮纪》', ja: '『月鱗綺紀』(Veil of Shadows)', ko: 'Veil of Shadows (월린기기)' },
346
362
  },
347
363
  {
348
364
  key: 'legend_of_yunxi',
@@ -381,15 +397,18 @@ export function stripSystemDirectives(text) {
381
397
  .trim();
382
398
  }
383
399
 
384
- const KNOWN_ENTITY_REGEX = /(花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง|芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|SNH48)/i;
400
+ const KNOWN_ENTITY_REGEX = /(花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง|ฮวาเจียนลิ่ง|千香|千香引|The Eternal Fragrance|พันกลิ่นบุปผา|花戎|Beauty of Resilience|ฮวาหรง|ตำนานเลือดฟีนิกซ์|月鳞绮纪|Veil of Shadows|เยว่หลินฉีจี้|ม่านเงาจันทรา|ลู่หวู่อี้|芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|กำเนิดใหม่เพื่อเธอ|SNH48)/i;
385
401
 
386
402
  const ENTITY_REGEX_MAP = {
387
- in_blossom: /(?:in_blossom|花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง)/i,
403
+ in_blossom: /(?:in_blossom|花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง|ฮวาเจียนลิ่ง)/i,
404
+ the_eternal_fragrance: /(?:the_eternal_fragrance|千香|千香引|The Eternal Fragrance|พันกลิ่นบุปผา)/i,
405
+ beauty_of_resilience: /(?:beauty_of_resilience|花戎|Beauty of Resilience|ฮวาหรง|ตำนานเลือดฟีนิกซ์)/i,
406
+ veil_of_shadows: /(?:veil_of_shadows|月鳞绮纪|Veil of Shadows|เยว่หลินฉีจี้|ม่านเงาจันทรา|ลู่หวู่อี้)/i,
388
407
  legend_of_yunxi: /(?:legend_of_yunxi|芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี)/i,
389
408
  sword_fairy_4: /(?:sword_fairy_4|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่)/i,
390
409
  white_snake: /(?:white_snake|新白娘子传奇|White Snake|นางพญางูขาว)/i,
391
410
  ruyi_pavilion: /(?:ruyi_pavilion|如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา)/i,
392
- rebirth_for_you: /(?:rebirth_for_you|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ)/i,
411
+ rebirth_for_you: /(?:rebirth_for_you|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|กำเนิดใหม่เพื่อเธอ)/i,
393
412
  snh48: /(?:snh48|SNH48)/i,
394
413
  };
395
414
 
@@ -408,12 +427,15 @@ function extractQueryTopic(query, lang) {
408
427
 
409
428
  function entityRegexFor(key) {
410
429
  const map = {
411
- in_blossom: '花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง',
430
+ in_blossom: '花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง|ฮวาเจียนลิ่ง',
431
+ the_eternal_fragrance: '千香|千香引|The Eternal Fragrance|พันกลิ่นบุปผา',
432
+ beauty_of_resilience: '花戎|Beauty of Resilience|ฮวาหรง|ตำนานเลือดฟีนิกซ์',
433
+ veil_of_shadows: '月鳞绮纪|Veil of Shadows|เยว่หลินฉีจี้|ม่านเงาจันทรา|ลู่หวู่อี้',
412
434
  legend_of_yunxi: '芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี',
413
435
  sword_fairy_4: '仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่',
414
436
  white_snake: '新白娘子传奇|White Snake|นางพญางูขาว',
415
437
  ruyi_pavilion: '如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา',
416
- rebirth_for_you: '嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ',
438
+ rebirth_for_you: '嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|กำเนิดใหม่เพื่อเธอ',
417
439
  snh48: 'SNH48',
418
440
  };
419
441
  return map[key] || '';
@@ -829,10 +851,42 @@ const FALLBACK_OPTIONS = {
829
851
  ko: ['네', '아니요', '전체'],
830
852
  };
831
853
 
854
+ const clarificationCache = new Map();
855
+ const CLARIFY_CACHE_TTL_MS = 10 * 60 * 1000;
856
+ const MAX_CLARIFY_CACHE_ENTRIES = 100;
857
+
858
+ function getCachedClarification(key) {
859
+ const item = clarificationCache.get(key);
860
+ if (!item) return null;
861
+ if (Date.now() > item.expiresAt) {
862
+ clarificationCache.delete(key);
863
+ return null;
864
+ }
865
+ return item.data;
866
+ }
867
+
868
+ function setCachedClarification(key, data) {
869
+ if (clarificationCache.size >= MAX_CLARIFY_CACHE_ENTRIES) {
870
+ const oldestKey = clarificationCache.keys().next().value;
871
+ clarificationCache.delete(oldestKey);
872
+ }
873
+ clarificationCache.set(key, { data, expiresAt: Date.now() + CLARIFY_CACHE_TTL_MS });
874
+ }
875
+
876
+ export function clearClarificationCache() {
877
+ clarificationCache.clear();
878
+ }
879
+
832
880
  export async function generateClarifyingQuestions(query, lang = 'en', options = {}) {
833
881
  const { conversationHistory = [], client = null, maxQuestions = 4, detectedAmbiguities = [] } = options;
834
882
  const cleanQuery = stripSystemDirectives(query) || (typeof query === 'string' ? query.trim() : '');
835
883
  const normalizedLang = normalizeLang(lang);
884
+ const cacheKey = `${normalizedLang}:${cleanQuery.toLowerCase()}`;
885
+
886
+ if (!client) {
887
+ const cached = getCachedClarification(cacheKey);
888
+ if (cached) return cached;
889
+ }
836
890
 
837
891
  if (client && typeof client.chat === 'function') {
838
892
  const recentHistory = conversationHistory
@@ -876,14 +930,18 @@ export async function generateClarifyingQuestions(query, lang = 'en', options =
876
930
  }));
877
931
 
878
932
  if (validated.length >= 2) {
879
- return validated.slice(0, maxQuestions);
933
+ const res = validated.slice(0, maxQuestions);
934
+ setCachedClarification(cacheKey, res);
935
+ return res;
880
936
  }
881
937
  }
882
938
  } catch {
883
939
  }
884
940
  }
885
941
 
886
- return synthesizeDynamicQuestions(cleanQuery, normalizedLang, detectedAmbiguities, maxQuestions);
942
+ const synthesized = synthesizeDynamicQuestions(cleanQuery, normalizedLang, detectedAmbiguities, maxQuestions);
943
+ setCachedClarification(cacheKey, synthesized);
944
+ return synthesized;
887
945
  }
888
946
 
889
947
  export function isQueryTooVague(query) {
@@ -972,30 +1030,61 @@ export function parseClarifyingAnswers(answersText, questions) {
972
1030
  return { answers: {}, summary: 'User chose to skip clarifying questions.' };
973
1031
  }
974
1032
 
1033
+ let extraNote = '';
1034
+ let cleanAnswersText = answersText;
1035
+ const extraMatch = answersText.match(/(?:เพิ่มเติม|additional|extra|note):\s*(.+)$/i);
1036
+ if (extraMatch) {
1037
+ extraNote = extraMatch[1].trim();
1038
+ cleanAnswersText = answersText.replace(/(?:เพิ่มเติม|additional|extra|note):\s*(.+)$/i, '').trim().replace(/;\s*$/, '');
1039
+ }
1040
+
975
1041
  const answers = {};
976
1042
  const summaryParts = [];
977
1043
 
978
- const isStructured = /^[\da-d\s,;]+$/.test(trimmed) && trimmed.length <= 20;
979
-
980
- if (isStructured) {
981
- const structuredPattern = /(\d+)?([a-d])/g;
982
- let match;
983
- while ((match = structuredPattern.exec(trimmed)) !== null) {
984
- const questionIndex = match[1] ? parseInt(match[1], 10) - 1 : 0;
985
- const optionIndex = match[2].charCodeAt(0) - 97;
1044
+ // 1. Try structured colon-delimited key-values (e.g. "drama_aspect: บทบาท; timeframe: 2024-2026")
1045
+ if (cleanAnswersText.includes(':')) {
1046
+ const pairs = cleanAnswersText.split(';').map(p => p.trim()).filter(Boolean);
1047
+ for (const pair of pairs) {
1048
+ const idx = pair.indexOf(':');
1049
+ if (idx !== -1) {
1050
+ const catKey = pair.slice(0, idx).trim();
1051
+ const val = pair.slice(idx + 1).trim();
1052
+ if (catKey && val) {
1053
+ const matchedQ = questions.find(q => q.category === catKey || q.category?.toLowerCase() === catKey.toLowerCase());
1054
+ const actualKey = matchedQ ? matchedQ.category : catKey;
1055
+ answers[actualKey] = val;
1056
+ summaryParts.push(`${actualKey}: ${val}`);
1057
+ }
1058
+ }
1059
+ }
1060
+ }
986
1061
 
987
- if (questionIndex >= 0 && questionIndex < questions.length) {
988
- const q = questions[questionIndex];
989
- if (optionIndex >= 0 && optionIndex < (q.options?.length || 0)) {
990
- answers[q.category] = q.options[optionIndex];
991
- summaryParts.push(`${q.category}: ${q.options[optionIndex]}`);
1062
+ // 2. Try structured shorthand pattern (e.g. "1a 2c" or "a")
1063
+ if (Object.keys(answers).length === 0) {
1064
+ const cleanLower = cleanAnswersText.toLowerCase().trim();
1065
+ const isStructured = /^[\da-d\s,;]+$/.test(cleanLower) && cleanLower.length <= 20;
1066
+
1067
+ if (isStructured) {
1068
+ const structuredPattern = /(\d+)?([a-d])/g;
1069
+ let match;
1070
+ while ((match = structuredPattern.exec(cleanLower)) !== null) {
1071
+ const questionIndex = match[1] ? parseInt(match[1], 10) - 1 : 0;
1072
+ const optionIndex = match[2].charCodeAt(0) - 97;
1073
+
1074
+ if (questionIndex >= 0 && questionIndex < questions.length) {
1075
+ const q = questions[questionIndex];
1076
+ if (optionIndex >= 0 && optionIndex < (q.options?.length || 0)) {
1077
+ answers[q.category] = q.options[optionIndex];
1078
+ summaryParts.push(`${q.category}: ${q.options[optionIndex]}`);
1079
+ }
992
1080
  }
993
1081
  }
994
1082
  }
995
1083
  }
996
1084
 
997
- if (Object.keys(answers).length === 0 && trimmed.length > 0) {
998
- const interpretedAnswers = interpretFreeTextAnswers(answersText, questions);
1085
+ // 3. Fallback to free text interpretation against questions options
1086
+ if (Object.keys(answers).length === 0 && cleanAnswersText.trim().length > 0) {
1087
+ const interpretedAnswers = interpretFreeTextAnswers(cleanAnswersText, questions);
999
1088
 
1000
1089
  if (Object.keys(interpretedAnswers).length > 0) {
1001
1090
  Object.assign(answers, interpretedAnswers);
@@ -1004,12 +1093,18 @@ export function parseClarifyingAnswers(answersText, questions) {
1004
1093
  }
1005
1094
  } else {
1006
1095
  if (questions.length > 0) {
1007
- answers[questions[0].category] = answersText.trim();
1008
- summaryParts.push(`${questions[0].category}: ${answersText.trim()}`);
1096
+ answers[questions[0].category] = cleanAnswersText.trim();
1097
+ summaryParts.push(`${questions[0].category}: ${cleanAnswersText.trim()}`);
1009
1098
  }
1010
1099
  }
1011
1100
  }
1012
1101
 
1102
+ // 4. Attach extra note if present
1103
+ if (extraNote) {
1104
+ answers._additional = extraNote;
1105
+ summaryParts.push(`Additional note: ${extraNote}`);
1106
+ }
1107
+
1013
1108
  return {
1014
1109
  answers,
1015
1110
  summary: summaryParts.length > 0
@@ -1111,7 +1206,7 @@ export function buildClarificationContext(clarifyingResult, originalQuery) {
1111
1206
 
1112
1207
  const { answers, summary } = clarifyingResult;
1113
1208
 
1114
- const isDramaQuery = /(ซีรีส์|ละคร|drama|series|in blossom|ฮวาเจียนลิ่ง|花间令|花間令|หยุนซี|yunxi|นางพญางูขาว|드라마|ドラマ)/i.test(originalQuery);
1209
+ const isDramaQuery = /(ซีรีส์|ละคร|drama|series|in blossom|ฮวาเจียนลิ่ง|ฮวาเจียนหลิง|花间令|花間令|หยุนซี|yunxi|นางพญางูขาว|드라마|ドラマ)/i.test(originalQuery);
1115
1210
  const isMusicQuery = /(เพลง|song|music|ost|album|single|find the way|曲|歌|음악|노래)/i.test(originalQuery);
1116
1211
 
1117
1212
  const parts = [];
@@ -1120,6 +1215,9 @@ export function buildClarificationContext(clarifyingResult, originalQuery) {
1120
1215
  parts.push(`Clarifications provided:`);
1121
1216
 
1122
1217
  for (const [category, value] of Object.entries(answers)) {
1218
+ if (category.startsWith('_')) {
1219
+ continue;
1220
+ }
1123
1221
  if (category === 'media_type' && isDramaQuery && !isMusicQuery && String(value).toLowerCase().includes('music')) {
1124
1222
  continue;
1125
1223
  }
@@ -1129,6 +1227,10 @@ export function buildClarificationContext(clarifyingResult, originalQuery) {
1129
1227
  parts.push(`- ${category}: ${value}`);
1130
1228
  }
1131
1229
 
1230
+ if (answers._additional) {
1231
+ parts.push(`- additional_user_note: ${answers._additional}`);
1232
+ }
1233
+
1132
1234
  if (summary) {
1133
1235
  parts.push(`Summary: ${summary}`);
1134
1236
  }