kiku-agent 1.0.11 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -0,0 +1,397 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { resolveAgentConnection } from '../core/connection.js';
3
+ import { executeDirectCatalogKnowledge } from '../core/directCatalogEngine.js';
4
+ import { makeInspectableToolPayload } from '../core/toolInspector.js';
5
+ import { getAgentMemorySnapshot } from '../core/memorySnapshot.js';
6
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
7
+ import { registerClarificationResolver } from './clarifyRoute.js';
8
+ import { extractAgentFollowUpQuestions, generateDynamicFollowUps } from '../core/followUpEngine.js';
9
+ import { resolveConversationContext } from '../core/conversationContext.js';
10
+ import { applyFutureEventGuard } from '../core/futureEventGuard.js';
11
+ import { extractApiKey } from '../core/security.js';
12
+ import { assessDramaAnswerQuality } from '../core/dramaKnowledge.js';
13
+
14
+ function stripInvalidYouTubeLinks(text) {
15
+ if (!text || typeof text !== 'string') return text || '';
16
+ return text.replace(/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{1,20})/gi, (m, id) => (id.length === 11 ? m : ''));
17
+ }
18
+
19
+ async function replaceLowQualityDramaAnswer({ query, answer, persona, conversationContext, toolsUsed }) {
20
+ const assessment = assessDramaAnswerQuality(query, answer);
21
+ if (!assessment.applicable || assessment.valid) return null;
22
+
23
+ const fallback = await executeDirectCatalogKnowledge(query, persona, { conversationContext });
24
+ if (Array.isArray(toolsUsed)) toolsUsed.push(...fallback.tools);
25
+ return {
26
+ answer: fallback.answer,
27
+ suggestions: fallback.suggestions || [],
28
+ issues: assessment.issues
29
+ };
30
+ }
31
+
32
+
33
+ export function sanitizeSubagentProgress(event = {}) {
34
+ const text = (value, max = 200) => String(value ?? '').slice(0, max);
35
+ const num = (value) => (Number.isFinite(Number(value)) ? Number(value) : undefined);
36
+ const out = { type: 'subagent_progress' };
37
+ if (['subagent', 'research', 'verify'].includes(event.source)) out.source = event.source;
38
+ else out.source = 'subagent';
39
+ if (['start', 'iteration', 'tool', 'worker-start', 'worker-complete', 'complete'].includes(event.type)) out.kind = event.type;
40
+ else out.kind = 'tool';
41
+ const subject = event.task ?? event.question ?? event.claim ?? '';
42
+ if (subject) out.subject = text(subject);
43
+ if (event.iteration !== undefined) out.iteration = num(event.iteration);
44
+ if (event.maxIterations !== undefined) out.maxIterations = num(event.maxIterations);
45
+ if (event.name) out.tool = text(event.name, 80);
46
+ if (event.label) out.label = text(event.label, 120);
47
+ if (event.id) out.workerId = text(event.id, 80);
48
+ if (event.workerId) out.workerId = text(event.workerId, 80);
49
+ if (event.sources !== undefined) out.sources = num(event.sources);
50
+ return out;
51
+ }
52
+
53
+ export async function handleChatRoute(req, res, urlObj) {
54
+ let body;
55
+ try {
56
+ body = await parseJsonBody(req);
57
+ } catch (err) {
58
+ return sendError(res, 400, `Invalid JSON body: ${err.message}`);
59
+ }
60
+
61
+ const query = String(body.query || '').trim();
62
+
63
+ const persona = 'default';
64
+ const wantsStream = body.stream === true || (req.headers.accept && req.headers.accept.includes('text/event-stream'));
65
+
66
+ if (!query) {
67
+ return sendError(res, 400, 'Query is required');
68
+ }
69
+
70
+ const apiKey = extractApiKey(req);
71
+ const abortController = new AbortController();
72
+ req.on('close', () => {
73
+ if (!res.writableEnded) {
74
+ abortController.abort();
75
+ }
76
+ });
77
+
78
+ const startTime = Date.now();
79
+ const clarificationIds = new Map();
80
+ const externalClarificationId = (internalId) => {
81
+ if (!clarificationIds.has(internalId)) clarificationIds.set(internalId, randomUUID());
82
+ return clarificationIds.get(internalId);
83
+ };
84
+ const conversationContext = resolveConversationContext(query, body.history);
85
+ const publicContext = {
86
+ contextual: conversationContext.contextual,
87
+ domain: conversationContext.domain
88
+ };
89
+
90
+ try {
91
+ const conn = await resolveAgentConnection({
92
+ provider: body.provider,
93
+ model: body.model,
94
+ thinking: body.thinking,
95
+ apiKey,
96
+ sessionId: body.sessionId,
97
+ history: body.history,
98
+ }, { signal: abortController.signal });
99
+
100
+ let augmentedQuery = conversationContext.resolvedQuery;
101
+
102
+ augmentedQuery += `\n\n[SYSTEM DIRECTIVE (Fact Grounding & Music Playback):
103
+ 1. อ้างอิงเฉพาะเพลง ซีรีส์ และข้อมูลที่ได้จากผลลัพธ์เครื่องมือ (search_works, search_discography, get_song_detail, get_character_roles, get_awards, get_career_timeline) เท่านั้น ห้ามแต่งชื่อเพลง ชื่อซีรีส์ ตัวเลข หรือลิงก์ YouTube ขึ้นเอง หากเครื่องมือไม่พบข้อมูล ให้บอกผู้ใช้ตามตรงว่าไม่พบข้อมูลในคลัง
104
+ 2. เมื่อพูดถึงเพลงใด ให้ระบุ "ชื่อเพลงจริง" จากคลังข้อมูลกำกับในประโยคเสมอ (เช่น "เพลง 千年等一回") และห้ามแนบ URL YouTube ใดๆ ที่ไม่ได้มาจากผลลัพธ์เครื่องมือโดยเด็ดขาด
105
+ 3. เงื่อนไขการเสนอเปิดเพลงใน Mini Player: ให้เสนอเปิดเพลงเฉพาะเมื่อผู้ใช้ถามเรื่องบทเพลง การฟังเพลง หรือ OST โดยตรงเท่านั้น และต้องระบุชื่อเพลงที่เจาะจงในประโยคเดียวกัน ห้ามเสนอเปิดเพลงหรือถามชวนเปิดเพลงเมื่อผู้ใช้ถามเรื่องซีรีส์ ข่าวลือ หรือเนื้อหาทั่วไป
106
+ 4. การแยกแยะชื่อเรื่องและข่าวลือแคสติ้ง (Entity Disambiguation & Anti-Hallucination): ห้ามเดาหรือจับคู่คำค้นหาที่ออกเสียงคล้ายกันเข้ากับผลงานในคลังโดยไม่มีหลักฐานเด็ดขาด (เช่น คำว่า "Qi Xiang Cai" หรือ "七星彩" (ชีซิงไฉ่) คือโปรเจกต์ซีรีส์จีนโบราณของ Youku x Stellar Media ดัดแปลงจากนิยายของหมิงเยว่ตัง ที่มีกระแสข่าวลือแคสติ้ง ไม่ใช่เพลงหรือซีรีส์ 《千香》 (The Eternal Fragrance / พันกลิ่นบุปผา) และไม่มีความเกี่ยวข้องกัน ห้ามอ้างว่าเป็นเพลง OST หรือผลงานในคลังเด็ดขาด)
107
+ 5. หากผู้ใช้ส่งคำว่า "ดำเนินการต่อ" หรือตอบตกลงต่อคำถามก่อนหน้า ให้ลงมือทำสิ่งที่เสนอไว้ทันทีด้วยข้อมูลจริงจากเครื่องมือ (เช่น สรุปเพลงที่เสนอพร้อมชื่อจริงและรายละเอียด) อย่าถามคำถามเดิมซ้ำอีก
108
+ 6. ข้อมูลที่อ่อนไหวต่อเวลา (ผลงานล่าสุด, กำหนดการ, โซเชียล, ข่าว, ข่าวลือแคสติ้ง, รางวัล, แบรนด์) ให้ลำดับการใช้ข้อมูลสดจาก fetch_api/global_search/verify_claim/web_search แทนข้อมูลที่อาจเก่าในคลัง เพื่อหลีกเลี่ยงคำตอบที่ล้าหลังหรือเข้าใจผิด]`;
109
+
110
+ augmentedQuery += `\n\n[SYSTEM DIRECTIVE (Answer Quality for Drama Analysis):
111
+ 1. แยก "ข้อเท็จจริงจากเครื่องมือ" ออกจาก "บทวิเคราะห์" ให้ชัดเจน ห้ามนำเรื่องย่อไปสรุปเป็นเสียงวิจารณ์หรือการยอมรับของวงการ
112
+ 2. ห้ามเหมารวมว่าผลงานใหม่กว่าแปลว่าลึกกว่า สมจริงกว่า หรือแสดงดีกว่า ให้เทียบจากภาระของบท แรงขับของตัวละคร แนวเรื่อง และฉากที่มีหลักฐานเท่านั้น
113
+ 3. ห้ามใช้คำยกย่องแบบไม่มีหลักฐาน เช่น "ระดับแนวหน้า", "ไม่มีใครเทียบได้", "ก้าวข้ามไอดอลอย่างสมบูรณ์" หรือ "ได้รับรางวัลมากมาย"
114
+ 4. คะแนนในแคตตาล็อกที่ไม่มีชื่อแหล่งและเกณฑ์คะแนนเป็นเพียง metadata ภายใน ห้ามใช้ยืนยันคุณภาพและไม่ต้องแสดงในบทวิเคราะห์
115
+ 5. In Blossom: จวีจิ้งอีแสดงตัวตนของหยางไฉ่เวยภายใต้รูปลักษณ์ซ่างกวนจื่อหลัง premise การเปลี่ยนใบหน้า ไม่ใช่สองตัวละครอิสระที่สลับเล่นตลอดเรื่อง
116
+ 6. The Legend of White Snake: ไป๋ซู่เจินคือวิญญาณ/ปีศาจงูขาวที่แปลงเป็นหญิงมนุษย์ ส่วนสวี่เซียนคือมนุษย์ชาย ห้ามสลับเพศหรือเผ่าพันธุ์
117
+ 7. หากเป็นคำสั่ง "ดำเนินการต่อ" ให้ตอบเฉพาะมุมใหม่ที่ผู้ใช้ขอ ห้ามคัดลอกคำตอบก่อนหน้ามาตอบซ้ำ]`;
118
+
119
+ augmentedQuery += `\n\n[SYSTEM DIRECTIVE (Topic & Future-event Safety):
120
+ 1. คำถามสั้นที่มีสรรพนามหรืออ้างถึง "เรื่องนี้/งานนี้/ทำไม/น่าจับตามอง" ต้องยึดหัวข้อหลักล่าสุดในบทสนทนาก่อน ห้ามเปลี่ยนไปตอบเรื่องศิลปินเพียงเพราะ Persona หรือฐานข้อมูลเน้นศิลปิน
121
+ 2. วันนี้คือ ${conversationContext.today}. เหตุการณ์หลังวันนี้เป็นข้อมูลอนาคต: ใช้คำว่า "มีกำหนด", "มีรายงาน", หรือ "ยังไม่ยืนยัน" ตามหลักฐาน ห้ามฟันธงว่าได้รับเชิญหรือจะเข้าร่วมแน่นอนโดยไม่มีแหล่งทางการที่เป็นปัจจุบัน
122
+ 3. ห้ามนำวัน เดือน รายชื่อแขก หรือสถานะการเข้าร่วมจากงานคนละปีมาใช้กับฉบับปีปัจจุบัน หากต้องระบุวันที่แน่นอนหรือผู้เข้าร่วมงานอนาคต ให้ตรวจสอบด้วยเครื่องมือเว็บก่อน]`;
123
+
124
+ augmentedQuery += `\n\n[SYSTEM DIRECTIVE: After completing your answer, proactively formulate and suggest 2-3 engaging, creative, and relevant follow-up questions tailored specifically to what you just discussed for the user to explore deeper. Place them at the very end of your response strictly formatted as:
125
+ [SUGGESTED_FOLLOW_UPS]
126
+ - [คำถามแนะนำที่ 1 ที่เชื่อมโยงกับเรื่องที่เพิ่งเล่า]
127
+ - [คำถามแนะนำที่ 2 ที่เชื่อมโยงกับเรื่องที่เพิ่งเล่า]
128
+ - [คำถามแนะนำที่ 3 ที่เชื่อมโยงกับเรื่องที่เพิ่งเล่า]
129
+ [/SUGGESTED_FOLLOW_UPS]
130
+ Rules for the block: one question per line, plain text only — no markdown bold/italic, no extra headers, no lead-in sentences, no numbering inside the block.]`;
131
+
132
+ if (wantsStream) {
133
+ res.writeHead(200, {
134
+ 'Content-Type': 'text/event-stream',
135
+ 'Cache-Control': 'no-cache, no-transform',
136
+ 'Connection': 'keep-alive',
137
+ 'X-Accel-Buffering': 'no'
138
+ });
139
+
140
+ const sendSse = (eventObj) => {
141
+ res.write(`data: ${JSON.stringify(eventObj)}\n\n`);
142
+ };
143
+
144
+ if (conn.ready && conn.agent) {
145
+ try {
146
+ const toolsUsed = [];
147
+ let reasoningStarted = false;
148
+ const runRes = await conn.agent.run(augmentedQuery, {
149
+ signal: abortController.signal,
150
+ onReasoningDelta: () => {
151
+ if (reasoningStarted) return;
152
+ reasoningStarted = true;
153
+ sendSse({ type: 'reasoning', chunk: 'กำลังวิเคราะห์บริบทและวางแผนคำตอบ' });
154
+ },
155
+ onToolStart: (name, input) => {
156
+ toolsUsed.push({ name, input });
157
+ sendSse({ type: 'tool_start', name, input: makeInspectableToolPayload(input) });
158
+ },
159
+ onToolResult: (name, result, durationMs) => {
160
+ const existing = toolsUsed.find(t => t.name === name);
161
+ if (existing) { existing.result = result; existing.durationMs = durationMs; }
162
+ sendSse({
163
+ type: 'tool_result',
164
+ name,
165
+ durationMs,
166
+ resultCount: result?.count ?? 0,
167
+ result: makeInspectableToolPayload(result)
168
+ });
169
+ },
170
+ onSubagentProgress: (event) => {
171
+ sendSse(sanitizeSubagentProgress(event));
172
+ },
173
+ onTokenDelta: (chunk) => {
174
+ sendSse({ type: 'token', chunk });
175
+ },
176
+ ...(conversationContext.contextual ? {} : {
177
+ onClarifyingQuestions: (questions, queryId) => {
178
+ sendSse({ type: 'clarifying_questions', questions, queryId: externalClarificationId(queryId) });
179
+ },
180
+ onClarifyingAnswer: (queryId) => {
181
+ return new Promise((resolve) => {
182
+ registerClarificationResolver(externalClarificationId(queryId), resolve);
183
+ });
184
+ }
185
+ })
186
+ });
187
+
188
+ let finalAnswer = runRes?.answer || String(runRes || '');
189
+ finalAnswer = finalAnswer.replace(/The user has provided a complex instruction set:[\s\S]*?(?=\n\n|\n[A-Zก-๙]|$)/i, '').trim();
190
+ finalAnswer = finalAnswer.replace(/^\[SYSTEM (?:INSTRUCTION|DIRECTIVE):[\s\S]*?\]\s*/i, '').trim();
191
+
192
+ let suggestions = [];
193
+ const extracted = extractAgentFollowUpQuestions(finalAnswer);
194
+ finalAnswer = stripInvalidYouTubeLinks(extracted.cleanText);
195
+ finalAnswer = applyFutureEventGuard(finalAnswer, { tools: toolsUsed });
196
+ suggestions = extracted.suggestions;
197
+
198
+ if (!finalAnswer.trim()) {
199
+ const fallback = await executeDirectCatalogKnowledge(query, persona, { conversationContext });
200
+ finalAnswer = applyFutureEventGuard(fallback.answer, { tools: fallback.tools });
201
+ if (!suggestions.length) suggestions = fallback.suggestions || [];
202
+ }
203
+
204
+ const qualityFallback = await replaceLowQualityDramaAnswer({
205
+ query,
206
+ answer: finalAnswer,
207
+ persona,
208
+ conversationContext,
209
+ toolsUsed
210
+ });
211
+ if (qualityFallback) {
212
+ finalAnswer = applyFutureEventGuard(qualityFallback.answer, { tools: toolsUsed });
213
+ suggestions = qualityFallback.suggestions;
214
+ }
215
+
216
+ if (!suggestions.length) {
217
+ suggestions = generateDynamicFollowUps({ query, answer: finalAnswer, tools: toolsUsed });
218
+ }
219
+
220
+ sendSse({
221
+ type: 'final',
222
+ answer: finalAnswer,
223
+ suggestions,
224
+ durationMs: Date.now() - startTime,
225
+ tokens: conn.agent?.lastTelemetry?.tokens || conn.agent?.session?.lastQueryTokens || null,
226
+ costUsd: conn.agent?.lastTelemetry?.costUsd ?? conn.agent?.session?.lastQueryCostUsd ?? 0,
227
+ citations: conn.agent?.lastCitations || [],
228
+ context: publicContext,
229
+ memory: getAgentMemorySnapshot(conn.agent, body.sessionId)
230
+ });
231
+ res.write('data: [DONE]\n\n');
232
+ res.end();
233
+ return;
234
+ } catch (streamErr) {
235
+ if (abortController.signal.aborted || streamErr?.name === 'AbortError') {
236
+ if (!res.writableEnded) {
237
+ res.write('data: [DONE]\n\n');
238
+ res.end();
239
+ }
240
+ return;
241
+ }
242
+ const fallback = await executeDirectCatalogKnowledge(query, persona, { conversationContext });
243
+ sendSse({
244
+ type: 'final',
245
+ answer: applyFutureEventGuard(fallback.answer, { tools: fallback.tools }),
246
+ suggestions: fallback.suggestions,
247
+ durationMs: Date.now() - startTime,
248
+ citations: [],
249
+ context: publicContext,
250
+ memory: { sessionId: body.sessionId || null, messageCount: body.history?.length || 0, persistent: false }
251
+ });
252
+ res.write('data: [DONE]\n\n');
253
+ res.end();
254
+ return;
255
+ }
256
+ }
257
+
258
+ sendSse({ type: 'reasoning', chunk: '⚡ ตรวจพบ LM Studio ออฟไลน์ -> เปิดใช้งาน Auto Direct Catalog Knowledge Engine...' });
259
+
260
+ const fallback = await executeDirectCatalogKnowledge(query, persona, { conversationContext });
261
+ const guardedFallbackAnswer = applyFutureEventGuard(fallback.answer, { tools: fallback.tools });
262
+ for (const t of fallback.tools) {
263
+ sendSse({ type: 'tool_start', name: t.name, input: t.input });
264
+ sendSse({ type: 'tool_result', name: t.name, durationMs: t.durationMs, resultCount: t.resultCount, result: t.result });
265
+ }
266
+ sendSse({ type: 'token', chunk: guardedFallbackAnswer });
267
+ sendSse({
268
+ type: 'final',
269
+ answer: guardedFallbackAnswer,
270
+ suggestions: fallback.suggestions,
271
+ durationMs: Date.now() - startTime,
272
+ citations: [],
273
+ context: publicContext,
274
+ memory: { sessionId: body.sessionId || null, messageCount: body.history?.length || 0, persistent: false }
275
+ });
276
+ res.write('data: [DONE]\n\n');
277
+ res.end();
278
+ return;
279
+ }
280
+
281
+ if (conn.ready && conn.agent) {
282
+ let answer = '';
283
+ let thinking = '';
284
+ let thinkingDurationMs = 0;
285
+ const toolsUsed = [];
286
+ const subagentActivity = [];
287
+
288
+ let runRes;
289
+ try {
290
+ runRes = await conn.agent.run(augmentedQuery, {
291
+ signal: abortController.signal,
292
+ onReasoningDelta: () => { thinking = 'วิเคราะห์บริบทและวางแผนคำตอบแล้ว'; },
293
+ onReasoningEnd: () => { thinkingDurationMs = Date.now() - startTime; },
294
+ onSubagentProgress: (event) => {
295
+ subagentActivity.push(sanitizeSubagentProgress(event));
296
+ },
297
+ onToolStart: (name, input) => {
298
+ toolsUsed.push({ name, input: makeInspectableToolPayload(input), status: 'running' });
299
+ },
300
+ onToolResult: (name, result, dur) => {
301
+ let tool = null;
302
+ for (let index = toolsUsed.length - 1; index >= 0; index--) {
303
+ if (toolsUsed[index].name === name && toolsUsed[index].status === 'running') {
304
+ tool = toolsUsed[index];
305
+ break;
306
+ }
307
+ }
308
+ if (!tool) {
309
+ tool = { name };
310
+ toolsUsed.push(tool);
311
+ }
312
+ Object.assign(tool, {
313
+ status: 'done',
314
+ result: makeInspectableToolPayload(result),
315
+ resultCount: result?.count ?? 0,
316
+ durationMs: dur
317
+ });
318
+ }
319
+ });
320
+ } catch (agentErr) {
321
+ if (abortController.signal.aborted || agentErr?.name === 'AbortError') {
322
+ if (!res.writableEnded) {
323
+ return sendError(res, 499, 'Request aborted by client');
324
+ }
325
+ return;
326
+ }
327
+ throw agentErr;
328
+ }
329
+
330
+ answer = runRes?.answer || String(runRes || '');
331
+ let suggestions = [];
332
+ const extracted = extractAgentFollowUpQuestions(answer);
333
+ answer = stripInvalidYouTubeLinks(extracted.cleanText);
334
+ answer = applyFutureEventGuard(answer, { tools: toolsUsed });
335
+ suggestions = extracted.suggestions;
336
+
337
+ if (!answer.trim()) {
338
+ const fallback = await executeDirectCatalogKnowledge(query, persona, { conversationContext });
339
+ answer = applyFutureEventGuard(fallback.answer, { tools: fallback.tools });
340
+ if (!suggestions.length) suggestions = fallback.suggestions || [];
341
+ }
342
+
343
+ const qualityFallback = await replaceLowQualityDramaAnswer({
344
+ query,
345
+ answer,
346
+ persona,
347
+ conversationContext,
348
+ toolsUsed
349
+ });
350
+ if (qualityFallback) {
351
+ answer = applyFutureEventGuard(qualityFallback.answer, { tools: toolsUsed });
352
+ suggestions = qualityFallback.suggestions;
353
+ }
354
+
355
+ if (!suggestions.length) {
356
+ suggestions = generateDynamicFollowUps({ query, answer, tools: toolsUsed });
357
+ }
358
+
359
+ return sendJson(res, 200, {
360
+ status: 'ok',
361
+ query,
362
+ persona,
363
+ answer,
364
+ suggestions,
365
+ thinking,
366
+ thinkingDurationMs,
367
+ tools: toolsUsed,
368
+ subagents: subagentActivity,
369
+ tokens: conn.agent?.lastTelemetry?.tokens || conn.agent?.session?.lastQueryTokens || null,
370
+ costUsd: conn.agent?.lastTelemetry?.costUsd ?? conn.agent?.session?.lastQueryCostUsd ?? 0,
371
+ citations: conn.agent?.lastCitations || [],
372
+ context: publicContext,
373
+ memory: getAgentMemorySnapshot(conn.agent, body.sessionId),
374
+ durationMs: Date.now() - startTime
375
+ });
376
+ }
377
+
378
+ const fallback = await executeDirectCatalogKnowledge(query, persona, { conversationContext });
379
+ return sendJson(res, 200, {
380
+ status: 'ok',
381
+ query,
382
+ persona,
383
+ answer: applyFutureEventGuard(fallback.answer, { tools: fallback.tools }),
384
+ suggestions: fallback.suggestions || [],
385
+ thinking: fallback.thinking,
386
+ thinkingDurationMs: Date.now() - startTime,
387
+ tools: fallback.tools,
388
+ citations: [],
389
+ context: publicContext,
390
+ memory: { sessionId: body.sessionId || null, messageCount: body.history?.length || 0, persistent: false },
391
+ durationMs: Date.now() - startTime
392
+ });
393
+
394
+ } catch (err) {
395
+ return sendError(res, 500, err.message);
396
+ }
397
+ }
@@ -0,0 +1,98 @@
1
+ import { resolveAgentConnection } from '../core/connection.js';
2
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+ import { ClarificationAnalytics, ClarificationConfig, formatAnalyticsForAPI } from '../../agent/clarificationAnalytics.js';
4
+ import { isWebChatAdmin } from '../core/security.js';
5
+
6
+ let sharedAnalytics = null;
7
+ let sharedConfig = null;
8
+
9
+ function getAnalytics() {
10
+ if (!sharedAnalytics) {
11
+ sharedAnalytics = new ClarificationAnalytics();
12
+ }
13
+ return sharedAnalytics;
14
+ }
15
+
16
+ function getConfig() {
17
+ if (!sharedConfig) {
18
+ sharedConfig = new ClarificationConfig();
19
+ }
20
+ return sharedConfig;
21
+ }
22
+
23
+ export async function handleClarifyAnalyticsRoute(req, res, options = {}) {
24
+ if (!isWebChatAdmin(req)) return sendError(res, 401, 'Admin API key required');
25
+ try {
26
+ const urlObj = new URL(req.url, `http://localhost:${options.port || 3001}`);
27
+ const days = parseInt(urlObj.searchParams.get('days') || '7', 10);
28
+
29
+ const analytics = getAnalytics();
30
+ const data = analytics.getAnalytics({ days: Math.min(Math.max(days, 1), 90) });
31
+
32
+ return sendJson(res, 200, {
33
+ status: 'ok',
34
+ ...formatAnalyticsForAPI(data),
35
+ });
36
+ } catch (err) {
37
+ return sendError(res, 500, `Failed to get analytics: ${err.message}`);
38
+ }
39
+ }
40
+
41
+ export async function handleClarifyConfigRoute(req, res, options = {}, action = 'get') {
42
+ try {
43
+ const config = getConfig();
44
+
45
+ if (action === 'get') {
46
+ return sendJson(res, 200, {
47
+ status: 'ok',
48
+ config: config.getConfig(),
49
+ });
50
+ }
51
+
52
+ if (!isWebChatAdmin(req)) return sendError(res, 401, 'Admin API key required');
53
+
54
+ if (action === 'reset') {
55
+ const newConfig = config.reset();
56
+ return sendJson(res, 200, {
57
+ status: 'ok',
58
+ message: 'Configuration reset to defaults',
59
+ config: newConfig,
60
+ });
61
+ }
62
+
63
+ if (action === 'update') {
64
+ let body;
65
+ try {
66
+ body = await parseJsonBody(req);
67
+ } catch (err) {
68
+ return sendError(res, 400, `Invalid JSON body: ${err.message}`);
69
+ }
70
+
71
+ const { enabled, confidenceThreshold, maxRounds, timeoutMs, preferLLMQuestions } = body;
72
+ const updates = {};
73
+
74
+ if (enabled !== undefined) updates.enabled = enabled;
75
+ if (confidenceThreshold !== undefined) updates.confidenceThreshold = confidenceThreshold;
76
+ if (maxRounds !== undefined) updates.maxRounds = maxRounds;
77
+ if (timeoutMs !== undefined) updates.timeoutMs = timeoutMs;
78
+ if (preferLLMQuestions !== undefined) updates.preferLLMQuestions = preferLLMQuestions;
79
+
80
+ if (Object.keys(updates).length === 0) {
81
+ return sendError(res, 400, 'No valid configuration updates provided');
82
+ }
83
+
84
+ const newConfig = config.updateConfig(updates);
85
+ return sendJson(res, 200, {
86
+ status: 'ok',
87
+ message: 'Configuration updated',
88
+ config: newConfig,
89
+ });
90
+ }
91
+
92
+ return sendError(res, 400, 'Invalid action');
93
+ } catch (err) {
94
+ return sendError(res, 500, `Failed to handle config: ${err.message}`);
95
+ }
96
+ }
97
+
98
+ export default { handleClarifyAnalyticsRoute, handleClarifyConfigRoute };
@@ -0,0 +1,83 @@
1
+ import { resolveAgentConnection } from '../core/connection.js';
2
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+ import { eventBus, Events } from '../../agent/eventBus.js';
4
+
5
+ const pendingClarifications = new Map();
6
+ const MAX_PENDING_CLARIFICATIONS = 100;
7
+
8
+ export function registerClarificationResolver(queryId, resolve) {
9
+ if (!/^[a-f0-9-]{36}$/i.test(String(queryId || '')) || typeof resolve !== 'function') return false;
10
+ while (pendingClarifications.size >= MAX_PENDING_CLARIFICATIONS) {
11
+ const oldestId = pendingClarifications.keys().next().value;
12
+ const oldest = pendingClarifications.get(oldestId);
13
+ clearTimeout(oldest?.timer);
14
+ oldest?.resolve?.('skip');
15
+ pendingClarifications.delete(oldestId);
16
+ }
17
+ const timer = setTimeout(() => {
18
+ const entry = pendingClarifications.get(queryId);
19
+ if (entry) {
20
+ pendingClarifications.delete(queryId);
21
+ entry.resolve('skip');
22
+ }
23
+ }, 35_000);
24
+ timer.unref?.();
25
+ pendingClarifications.set(queryId, { resolve, timer });
26
+ return true;
27
+ }
28
+
29
+ export function resolveClarification(queryId, answers) {
30
+ const entry = pendingClarifications.get(queryId);
31
+ if (entry) {
32
+ pendingClarifications.delete(queryId);
33
+ clearTimeout(entry.timer);
34
+ entry.resolve(answers);
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ export function resolveAllPendingClarifications(answers = 'skip') {
41
+ for (const [queryId, entry] of pendingClarifications.entries()) {
42
+ pendingClarifications.delete(queryId);
43
+ clearTimeout(entry.timer);
44
+ entry.resolve(answers);
45
+ }
46
+ }
47
+
48
+ export async function handleClarifyRoute(req, res) {
49
+ let body;
50
+ try {
51
+ body = await parseJsonBody(req);
52
+ } catch (err) {
53
+ return sendError(res, 400, `Invalid JSON body: ${err.message}`);
54
+ }
55
+
56
+ const { queryId, answers } = body;
57
+
58
+ if (!/^[a-f0-9-]{36}$/i.test(String(queryId || '')) || typeof answers !== 'string' || answers.length > 2000) {
59
+ return sendError(res, 400, 'queryId and answers (string) are required');
60
+ }
61
+
62
+ const resolved = resolveClarification(queryId, answers);
63
+
64
+ if (resolved) {
65
+ eventBus.emit(Events.AGENT_CLARIFYING_ANSWER, {
66
+ queryId,
67
+ answers,
68
+ timestamp: Date.now(),
69
+ });
70
+
71
+ return sendJson(res, 200, {
72
+ status: 'ok',
73
+ message: 'Clarifying answers received. Agent will continue processing.',
74
+ });
75
+ }
76
+
77
+ return sendJson(res, 200, {
78
+ status: 'ok',
79
+ message: 'No pending clarification found for this queryId. The agent may have already timed out or completed.',
80
+ });
81
+ }
82
+
83
+ export default { handleClarifyRoute, registerClarificationResolver, resolveClarification };
@@ -0,0 +1,79 @@
1
+ import { executeTool } from '../../agent/tools.js';
2
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+
4
+ export async function handleCompareRoute(req, res) {
5
+ let body;
6
+ try {
7
+ body = await parseJsonBody(req);
8
+ } catch (err) {
9
+ return sendError(res, 400, `Invalid JSON body: ${err.message}`);
10
+ }
11
+
12
+ const { itemA, itemB } = body;
13
+ if (!itemA || !itemB) {
14
+ return sendError(res, 400, 'Both itemA and itemB are required');
15
+ }
16
+
17
+ let resultA = null;
18
+ let resultB = null;
19
+
20
+ try {
21
+ resultA = await executeTool('global_search', { query: itemA });
22
+ resultB = await executeTool('global_search', { query: itemB });
23
+ } catch {}
24
+
25
+ const extractItems = (searchRes) => {
26
+ if (!searchRes) return [];
27
+ const raw = searchRes.results;
28
+ if (Array.isArray(raw)) return raw;
29
+ if (raw && typeof raw === 'object') {
30
+ const collected = [];
31
+ for (const val of Object.values(raw)) {
32
+ if (Array.isArray(val)) collected.push(...val);
33
+ }
34
+ return collected;
35
+ }
36
+ return [];
37
+ };
38
+
39
+ const formatItem = (item) => {
40
+ if (typeof item === 'string') {
41
+ return {
42
+ title: item,
43
+ type: 'general',
44
+ year: null,
45
+ description: item,
46
+ rating: null,
47
+ episodes: null,
48
+ streamingUrl: null
49
+ };
50
+ }
51
+ return {
52
+ title: item?.title || item?.titleCn || item?.name || 'N/A',
53
+ type: item?.type || item?.category || 'unknown',
54
+ year: item?.year || null,
55
+ description: item?.description || item?.synopsis || '',
56
+ rating: item?.rating || null,
57
+ episodes: item?.episodes || null,
58
+ streamingUrl: item?.streamingUrl || item?.youtubeSearchUrl || null
59
+ };
60
+ };
61
+
62
+ const itemsA = extractItems(resultA).slice(0, 3).map(formatItem);
63
+ const itemsB = extractItems(resultB).slice(0, 3).map(formatItem);
64
+
65
+ sendJson(res, 200, {
66
+ status: 'ok',
67
+ comparison: {
68
+ itemA: {
69
+ query: itemA,
70
+ results: itemsA,
71
+ },
72
+ itemB: {
73
+ query: itemB,
74
+ results: itemsB,
75
+ }
76
+ },
77
+ source: 'catalog'
78
+ });
79
+ }