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
@@ -21,3 +21,4 @@ export const SESSION_DIR = path.join(KIKU_DATA_DIR, 'sessions');
21
21
  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
+ export const PROFILES_FILE = path.join(KIKU_DATA_DIR, 'profiles.json');
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { existsSync, unlinkSync, readdirSync, statSync } from 'node:fs';
3
3
  import { join, dirname, basename } from 'node:path';
4
- import { ensureYouTubeCookies, getCookieStatus } from './youtubeAuth.js';
4
+ import { ensureYouTubeCookies, getCookieStatus, isCookieFileValid } from './youtubeAuth.js';
5
5
 
6
6
  let _ffmpeg = null;
7
7
  async function loadFfmpeg() {
@@ -66,7 +66,9 @@ function buildStrategies(cookieResult) {
66
66
  const strategies = [];
67
67
 
68
68
  const status = getCookieStatus();
69
- const cookiesFile = process.env.KIKU_YT_COOKIES || status.envCookies;
69
+ const cookiesFile = (process.env.KIKU_YT_COOKIES && isCookieFileValid(process.env.KIKU_YT_COOKIES))
70
+ ? process.env.KIKU_YT_COOKIES
71
+ : (status.envCookies && isCookieFileValid(status.envCookies) ? status.envCookies : null);
70
72
  if (cookiesFile) {
71
73
  strategies.push({ cookies: cookiesFile });
72
74
  }
@@ -0,0 +1,294 @@
1
+ function normalizeForMatch(str) {
2
+ if (!str || typeof str !== 'string') return '';
3
+ return str.toLowerCase().replace(/[\s\-_'"《》〈〉「」『』()()\[\]【】]/g, '');
4
+ }
5
+
6
+
7
+ export function extractCitations(answerText = '', recordedTools = []) {
8
+ if (!Array.isArray(recordedTools) || recordedTools.length === 0) {
9
+ return [];
10
+ }
11
+
12
+ const normAnswer = normalizeForMatch(answerText || '');
13
+ const candidateCitations = [];
14
+ const seenKeys = new Set();
15
+
16
+ for (const tool of recordedTools) {
17
+ if (!tool || !tool.result) continue;
18
+ const toolName = tool.name;
19
+ const res = tool.result;
20
+
21
+
22
+ if ((toolName === 'search_works' || toolName === 'get_work_detail') && Array.isArray(res.works || res.results)) {
23
+ const list = res.works || res.results;
24
+ for (const item of list) {
25
+ if (!item) continue;
26
+ const key = `work:${item.titleCn || item.titleTh || item.id}`;
27
+ if (seenKeys.has(key)) continue;
28
+
29
+ const titleTh = item.titleTh || '';
30
+ const titleCn = item.titleCn || '';
31
+ const titleEn = item.titleEn || '';
32
+ const year = item.year || item.releaseYear || '';
33
+
34
+
35
+ const isMentioned = !normAnswer ||
36
+ (titleTh && normAnswer.includes(normalizeForMatch(titleTh))) ||
37
+ (titleCn && normAnswer.includes(normalizeForMatch(titleCn))) ||
38
+ (titleEn && normAnswer.includes(normalizeForMatch(titleEn))) ||
39
+ (item.role && normAnswer.includes(normalizeForMatch(item.role)));
40
+
41
+ if (isMentioned) {
42
+ seenKeys.add(key);
43
+ const displayTitle = [titleTh, titleCn ? `《${titleCn}》` : ''].filter(Boolean).join(' / ') || titleEn || 'ผลงานการแสดง';
44
+ candidateCitations.push({
45
+ id: 0,
46
+ type: 'work',
47
+ title: displayTitle,
48
+ category: item.category || 'ซีรีส์/ภาพยนตร์',
49
+ year: year ? Number(year) : null,
50
+ role: item.role || item.character || null,
51
+ source: `catalog:works (${toolName})`,
52
+ tool: toolName,
53
+ verified: true,
54
+ });
55
+ }
56
+ }
57
+ }
58
+
59
+
60
+ if ((toolName === 'search_discography' || toolName === 'get_song_detail') && Array.isArray(res.songs || res.results)) {
61
+ const list = res.songs || res.results;
62
+ for (const item of list) {
63
+ if (!item) continue;
64
+ const key = `song:${item.title || item.titleCn || item.id}`;
65
+ if (seenKeys.has(key)) continue;
66
+
67
+ const title = item.title || item.titleCn || '';
68
+ const titleTh = item.titleTh || '';
69
+ const isMentioned = !normAnswer ||
70
+ (title && normAnswer.includes(normalizeForMatch(title))) ||
71
+ (titleTh && normAnswer.includes(normalizeForMatch(titleTh)));
72
+
73
+ if (isMentioned) {
74
+ seenKeys.add(key);
75
+ const displayTitle = titleTh && titleTh !== title ? `${titleTh} (${title})` : title;
76
+ candidateCitations.push({
77
+ id: 0,
78
+ type: 'song',
79
+ title: displayTitle,
80
+ category: item.category || 'เพลง/OST',
81
+ album: item.album || null,
82
+ year: item.year || (item.releaseDate ? parseInt(item.releaseDate, 10) : null),
83
+ source: `catalog:discography (${toolName})`,
84
+ tool: toolName,
85
+ verified: true,
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+
92
+ if (toolName === 'get_career_timeline' && Array.isArray(res.timeline || res.milestones || res.events)) {
93
+ const list = res.timeline || res.milestones || res.events;
94
+ for (const item of list.slice(0, 5)) {
95
+ if (!item) continue;
96
+ const key = `timeline:${item.year || ''}:${item.event || item.title || ''}`;
97
+ if (seenKeys.has(key)) continue;
98
+
99
+ const eventText = item.event || item.title || item.milestone || '';
100
+ const normEvent = normalizeForMatch(eventText);
101
+ const yearMentioned = item.year && normAnswer.includes(String(item.year));
102
+ const eventMentioned = !normAnswer || (normEvent && normAnswer.includes(normEvent));
103
+ const partialEventMentioned = normEvent && normEvent.length > 5 && normAnswer.includes(normEvent.slice(0, 8));
104
+ const isMentioned = eventMentioned || partialEventMentioned || (yearMentioned && normAnswer.length > 0);
105
+
106
+ if (isMentioned) {
107
+ seenKeys.add(key);
108
+ candidateCitations.push({
109
+ id: 0,
110
+ type: 'timeline',
111
+ title: eventText,
112
+ category: 'หมุดหมายเส้นทางอาชีพ',
113
+ year: item.year ? Number(item.year) : null,
114
+ source: `catalog:timeline (${toolName})`,
115
+ tool: toolName,
116
+ verified: true,
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+
123
+ if (toolName === 'get_character_roles' && Array.isArray(res.roles || res.results)) {
124
+ const list = res.roles || res.results;
125
+ for (const item of list) {
126
+ if (!item) continue;
127
+ const key = `role:${item.characterName || item.name}:${item.workTitle || ''}`;
128
+ if (seenKeys.has(key)) continue;
129
+
130
+ const charName = item.characterName || item.name || '';
131
+ if (charName && (!normAnswer || normAnswer.includes(normalizeForMatch(charName)))) {
132
+ seenKeys.add(key);
133
+ candidateCitations.push({
134
+ id: 0,
135
+ type: 'character',
136
+ title: `${charName} (จากเรื่อง ${item.workTitle || item.work || ''})`,
137
+ category: 'บทบาทตัวละคร',
138
+ year: item.year ? Number(item.year) : null,
139
+ source: `catalog:roles (${toolName})`,
140
+ tool: toolName,
141
+ verified: true,
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+
148
+ if (toolName === 'get_awards' && Array.isArray(res.awards || res.results)) {
149
+ const list = res.awards || res.results;
150
+ for (const item of list) {
151
+ if (!item) continue;
152
+ const key = `award:${item.award || item.title}:${item.year || ''}`;
153
+ if (seenKeys.has(key)) continue;
154
+
155
+ const awardName = item.award || item.title || '';
156
+ if (awardName && (!normAnswer || normAnswer.includes(normalizeForMatch(awardName)))) {
157
+ seenKeys.add(key);
158
+ candidateCitations.push({
159
+ id: 0,
160
+ type: 'award',
161
+ title: awardName,
162
+ category: 'รางวัล & เกียรติยศ',
163
+ year: item.year ? Number(item.year) : null,
164
+ source: `catalog:awards (${toolName})`,
165
+ tool: toolName,
166
+ verified: true,
167
+ });
168
+ }
169
+ }
170
+ }
171
+
172
+
173
+ if ((toolName === 'verify_claim' || toolName === 'debate_claim') && res.claim) {
174
+ const key = `verify:${res.claim}`;
175
+ if (!seenKeys.has(key)) {
176
+ seenKeys.add(key);
177
+ candidateCitations.push({
178
+ id: 0,
179
+ type: 'verification',
180
+ title: `การตรวจสอบข้อเท็จจริง: "${res.claim.slice(0, 60)}"`,
181
+ category: `ผลสรุป: ${res.verdict || res.status || 'Verified'}`,
182
+ source: `verifier:independent (${toolName})`,
183
+ tool: toolName,
184
+ verified: res.verdict === 'TRUE' || res.status === 'verified',
185
+ });
186
+ }
187
+ }
188
+
189
+
190
+ if (toolName === 'web_search' && Array.isArray(res.results)) {
191
+ for (const r of res.results) {
192
+ if (!r || !r.title) continue;
193
+ const key = `web:${r.url || r.title}`;
194
+ if (seenKeys.has(key)) continue;
195
+ seenKeys.add(key);
196
+ let hostname = 'Web Search';
197
+ try { if (r.url) hostname = new URL(r.url).hostname; } catch {}
198
+ candidateCitations.push({
199
+ id: 0,
200
+ type: 'web',
201
+ title: r.title,
202
+ url: r.url || null,
203
+ category: 'เว็บข่าวสาร / แหล่งข้อมูลภายนอก',
204
+ source: hostname,
205
+ tool: toolName,
206
+ verified: true,
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+
213
+ return candidateCitations.slice(0, 8).map((cit, idx) => ({
214
+ ...cit,
215
+ id: idx + 1,
216
+ }));
217
+ }
218
+
219
+ /**
220
+ * Formats citations as a clean markdown/terminal reference list.
221
+ *
222
+ * @param {Array<Object>} citations
223
+ * @param {Object} [options]
224
+ * @returns {string}
225
+ */
226
+ export function formatCitationsBlock(citations = [], { lang = 'th', plain = false } = {}) {
227
+ if (!Array.isArray(citations) || citations.length === 0) return '';
228
+
229
+ const header = lang === 'zh'
230
+ ? '📖 参考来源与证据引用 (Sources & Citations):'
231
+ : lang === 'en'
232
+ ? '📖 Sources & Evidence Citations:'
233
+ : '📖 แหล่งข้อมูลและหลักฐานอ้างอิง (Sources & Citations):';
234
+
235
+ const items = citations.map(c => {
236
+ const yearStr = c.year ? ` [${c.year}]` : '';
237
+ const catStr = c.category ? ` • ${c.category}` : '';
238
+ const srcStr = c.url ? ` • ${c.url}` : ` • ${c.source}`;
239
+ return `[${c.id}] ${c.title}${yearStr}${catStr}${srcStr}`;
240
+ });
241
+
242
+ return `${header}\n${items.join('\n')}`;
243
+ }
244
+
245
+ /**
246
+ * Resolves a citation reference like "[1]", "1", or an entity substring
247
+ * to the cited item from a previous conversation turn.
248
+ *
249
+ * @param {string} query - The user's query or reference string
250
+ * @param {Array<Object>} citations - Array of citation objects
251
+ * @returns {Object|null}
252
+ */
253
+ export function resolveCitationReference(query = '', citations = []) {
254
+ if (!query || !Array.isArray(citations) || citations.length === 0) return null;
255
+
256
+ const trimmed = query.trim();
257
+
258
+
259
+ const bracketMatch = trimmed.match(/\[(\d+)\]/) || trimmed.match(/#(\d+)/) || trimmed.match(/^(\d+)$/);
260
+ if (bracketMatch) {
261
+ const targetId = parseInt(bracketMatch[1], 10);
262
+ return citations.find(c => c.id === targetId) || null;
263
+ }
264
+
265
+
266
+ const normQuery = normalizeForMatch(trimmed);
267
+ if (normQuery.length >= 2) {
268
+ for (const c of citations) {
269
+ const normTitle = normalizeForMatch(c.title || '');
270
+ if (normTitle && (normTitle.includes(normQuery) || normQuery.includes(normTitle))) {
271
+ return c;
272
+ }
273
+ const parts = String(c.title || '').split(/[\(\)《》\/]/).map(p => normalizeForMatch(p)).filter(p => p.length >= 2);
274
+ for (const p of parts) {
275
+ if (normQuery.includes(p) || p.includes(normQuery)) {
276
+ return c;
277
+ }
278
+ }
279
+ }
280
+
281
+ const stopWords = new Set(['เล่า', 'เรื่อง', 'ย่อ', 'ของ', 'ใน', 'ให้', 'ฟัง', 'หน่อย', 'ครับ', 'ค่ะ', 'นะ', 'มี', 'กี่', 'ตอน', 'คือ', 'ใคร', 'ปี', 'ไหน', 'tell', 'about', 'more', 'what', 'when', 'who', 'show', 'info']);
282
+ const words = trimmed.split(/[\s,.;!?]+/).map(w => normalizeForMatch(w)).filter(w => w.length >= 2 && !stopWords.has(w));
283
+ for (const word of words) {
284
+ for (const c of citations) {
285
+ const normTitle = normalizeForMatch(c.title || '');
286
+ if (normTitle.includes(word)) {
287
+ return c;
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ return null;
294
+ }