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,59 @@
1
+ import { normalizeWebSessionId } from '../core/connection.js';
2
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+
4
+ export async function handleExportRoute(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 { sessionId, format = 'markdown', history = [] } = body;
13
+ const normSessionId = normalizeWebSessionId(sessionId);
14
+ const now = new Date().toISOString().replace(/[:.]/g, '-');
15
+ const filename = `kiku-chat-${normSessionId || 'session'}-${now}`;
16
+
17
+ if (format === 'json') {
18
+ const exportData = {
19
+ exportedAt: new Date().toISOString(),
20
+ sessionId: normSessionId,
21
+ messages: history,
22
+ metadata: { app: '鞠婧祎', version: '1.0.0' }
23
+ };
24
+ res.writeHead(200, {
25
+ 'Content-Type': 'application/json; charset=utf-8',
26
+ 'Content-Disposition': `attachment; filename="${filename}.json"`
27
+ });
28
+ res.end(JSON.stringify(exportData, null, 2));
29
+ return;
30
+ }
31
+
32
+ if (format === 'html') {
33
+ const msgHtml = history.map(m => {
34
+ const role = m.role === 'user' ? '🧑 You' : '🌸 Kiku';
35
+ const escaped = String(m.text || m.content || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
36
+ return `<div style="margin:12px 0;padding:12px;border-radius:8px;background:${m.role === 'user' ? '#e3f2fd' : '#fce4ec'}"><strong>${role}:</strong><br><pre style="white-space:pre-wrap;margin:4px 0 0;font-family:inherit">${escaped}</pre></div>`;
37
+ }).join('\n');
38
+ const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Kiku Chat Export</title><style>body{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px}h1{color:#e91e63}</style></head><body><h1>🌸 Kiku AI Chat Export</h1><p>Session: ${normSessionId || 'N/A'} | Exported: ${new Date().toLocaleString()}</p><hr>${msgHtml}</body></html>`;
39
+ res.writeHead(200, {
40
+ 'Content-Type': 'text/html; charset=utf-8',
41
+ 'Content-Disposition': `attachment; filename="${filename}.html"`
42
+ });
43
+ res.end(html);
44
+ return;
45
+ }
46
+
47
+ let md = `# 🌸 Kiku AI Chat Export\n\n`;
48
+ md += `- **Session:** ${normSessionId || 'N/A'}\n`;
49
+ md += `- **Exported:** ${new Date().toLocaleString()}\n\n---\n\n`;
50
+ history.forEach(m => {
51
+ const role = m.role === 'user' ? '🧑 **You**' : '🌸 **Kiku**';
52
+ md += `### ${role}\n\n${m.text || m.content || ''}\n\n---\n\n`;
53
+ });
54
+ res.writeHead(200, {
55
+ 'Content-Type': 'text/markdown; charset=utf-8',
56
+ 'Content-Disposition': `attachment; filename="${filename}.md"`
57
+ });
58
+ res.end(md);
59
+ }
@@ -0,0 +1,57 @@
1
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
2
+ import { FeedbackLearning } from '../../agent/feedbackLearning.js';
3
+ import { isWebChatAdmin } from '../core/security.js';
4
+
5
+ let store = null;
6
+
7
+ export function setFeedbackStore(customStore) {
8
+ store = customStore;
9
+ }
10
+
11
+ function getStore() {
12
+ if (!store) store = new FeedbackLearning({ persistence: false });
13
+ return store;
14
+ }
15
+
16
+ export async function handleFeedbackRoute(req, res) {
17
+ let body;
18
+ try {
19
+ body = await parseJsonBody(req, 64 * 1024);
20
+ } catch (err) {
21
+ return sendError(res, 400, `Invalid JSON body: ${err.message}`);
22
+ }
23
+
24
+ const rating = String(body.rating || '').toLowerCase();
25
+ if (rating !== 'like' && rating !== 'dislike') {
26
+ return sendError(res, 400, 'rating must be "like" or "dislike".');
27
+ }
28
+
29
+ let result;
30
+ try {
31
+ result = getStore().record({
32
+ rating,
33
+ query: body.query,
34
+ answer: body.answer,
35
+ toolsUsed: body.tools,
36
+ model: body.model,
37
+ sessionId: body.sessionId,
38
+ messageId: body.messageId,
39
+ });
40
+ } catch (err) {
41
+ return sendError(res, 400, err.message);
42
+ }
43
+
44
+ sendJson(res, 200, {
45
+ status: 'ok',
46
+ rating,
47
+ totals: result.totals,
48
+ directives: result.directives,
49
+ learnedCount: 0,
50
+ });
51
+ }
52
+
53
+ export async function handleFeedbackStatsRoute(req, res) {
54
+ if (!isWebChatAdmin(req)) return sendError(res, 401, 'Admin API key required');
55
+ const stats = getStore().getStats();
56
+ sendJson(res, 200, { status: 'ok', ...stats });
57
+ }
@@ -0,0 +1,33 @@
1
+ import { socialUpdatesData } from '../../../src/data/socialUpdatesData.js';
2
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+
4
+ export async function handleNewsRoute(req, res) {
5
+ let body = {};
6
+ try {
7
+ body = await parseJsonBody(req);
8
+ } catch {}
9
+
10
+ const { query = 'จวีจิ้งอี Ju Jingyi', days = 30 } = body;
11
+ let newsItems = [];
12
+ const source = 'social';
13
+
14
+ try {
15
+ const cards = socialUpdatesData?.douyin?.cards || socialUpdatesData?.cards || [];
16
+ newsItems = cards.map(c => ({
17
+ title: c.title || c.titleCn,
18
+ summary: c.captionTh || c.captionCn || c.content,
19
+ date: c.date,
20
+ platform: c.platform,
21
+ videoUrl: c.videoUrl
22
+ }));
23
+ } catch {}
24
+
25
+ sendJson(res, 200, {
26
+ status: 'ok',
27
+ query,
28
+ days,
29
+ items: newsItems.slice(0, 10),
30
+ source,
31
+ count: newsItems.length
32
+ });
33
+ }
@@ -0,0 +1,11 @@
1
+ import { QUIZ_QUESTIONS, shuffleQuizQuestions } from '../../agent/quizData.js';
2
+ import { sendJson } from '../core/errorHandler.js';
3
+
4
+ export async function handleQuizRoute(req, res) {
5
+ const shuffledQuestions = shuffleQuizQuestions(QUIZ_QUESTIONS);
6
+ sendJson(res, 200, {
7
+ status: 'ok',
8
+ questions: shuffledQuestions
9
+ });
10
+ }
11
+
@@ -0,0 +1,103 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { getWebChatHtml } from '../ui/htmlBuilder.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const PUBLIC_DIR = path.resolve(__dirname, '../../../public');
9
+
10
+ const MIME_TYPES = {
11
+ '.woff2': 'font/woff2',
12
+ '.woff': 'font/woff',
13
+ '.ttf': 'font/ttf',
14
+ '.css': 'text/css; charset=utf-8',
15
+ '.js': 'application/javascript; charset=utf-8',
16
+ '.mjs': 'application/javascript; charset=utf-8',
17
+ '.png': 'image/png',
18
+ '.jpg': 'image/jpeg',
19
+ '.jpeg': 'image/jpeg',
20
+ '.webp': 'image/webp',
21
+ '.gif': 'image/gif',
22
+ '.svg': 'image/svg+xml',
23
+ '.mp4': 'video/mp4',
24
+ '.json': 'application/json; charset=utf-8',
25
+ '.txt': 'text/plain; charset=utf-8'
26
+ };
27
+
28
+ export function handleStaticRoute(req, res, pathname, port) {
29
+ if (pathname === '/favicon.ico' || pathname === '/favicon.svg') {
30
+ const svgIcon = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌸</text></svg>`;
31
+ res.writeHead(200, {
32
+ 'Content-Type': 'image/svg+xml',
33
+ 'Cache-Control': 'public, max-age=86400, immutable'
34
+ });
35
+ res.end(svgIcon);
36
+ return true;
37
+ }
38
+
39
+ if (pathname === '/sw.js' || pathname === '/service-worker.js') {
40
+ let swPath = path.join(PUBLIC_DIR, 'sw.js');
41
+ if (!fs.existsSync(swPath)) {
42
+ const cwdPath = path.join(process.cwd(), 'public', 'sw.js');
43
+ if (fs.existsSync(cwdPath)) swPath = cwdPath;
44
+ }
45
+ res.writeHead(200, {
46
+ 'Content-Type': 'application/javascript; charset=utf-8',
47
+ 'Service-Worker-Allowed': '/',
48
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
49
+ });
50
+ if (fs.existsSync(swPath)) {
51
+ fs.createReadStream(swPath).pipe(res);
52
+ } else {
53
+ res.end(`const CACHE_NAME = 'kiku-v1';
54
+ const ASSETS = ['/', '/manifest.json'];
55
+ self.addEventListener('install', (event) => {
56
+ event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)).then(() => self.skipWaiting()));
57
+ });
58
+ self.addEventListener('activate', (event) => {
59
+ event.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
60
+ });
61
+ self.addEventListener('fetch', (event) => {
62
+ if (event.request.url.includes('/api/')) return;
63
+ event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request).then((response) => {
64
+ if (response.ok && event.request.method === 'GET') {
65
+ const clone = response.clone();
66
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
67
+ }
68
+ return response;
69
+ }).catch(() => cached)));
70
+ });`);
71
+ }
72
+ return true;
73
+ }
74
+
75
+ if (pathname.startsWith('/fonts/') || pathname.startsWith('/images/') || pathname === '/manifest.json' || pathname === '/robots.txt') {
76
+ const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, '');
77
+ let filePath = path.join(PUBLIC_DIR, safePath);
78
+ if (!fs.existsSync(filePath)) {
79
+ const cwdPath = path.join(process.cwd(), 'public', safePath);
80
+ if (fs.existsSync(cwdPath)) filePath = cwdPath;
81
+ }
82
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
83
+ const ext = path.extname(filePath).toLowerCase();
84
+ res.writeHead(200, {
85
+ 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
86
+ 'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable'
87
+ });
88
+ fs.createReadStream(filePath).pipe(res);
89
+ return true;
90
+ }
91
+ }
92
+
93
+ if (pathname === '/' || pathname === '/chat' || pathname.startsWith('/chat/') || pathname === '/index.html') {
94
+ res.writeHead(200, {
95
+ 'Content-Type': 'text/html; charset=utf-8',
96
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
97
+ });
98
+ res.end(getWebChatHtml(port));
99
+ return true;
100
+ }
101
+
102
+ return false;
103
+ }
@@ -0,0 +1,52 @@
1
+ import { toolsSchema } from '../../agent/tools.js';
2
+ import { resolveAgentConnection } from '../core/connection.js';
3
+ import { getAgentMemorySnapshot } from '../core/memorySnapshot.js';
4
+ import { sendJson, sendError } from '../core/errorHandler.js';
5
+ import { extractApiKey } from '../core/security.js';
6
+
7
+ export async function handleStatusRoute(req, res, urlObj, port) {
8
+ try {
9
+ const queryProvider = urlObj.searchParams.get('provider');
10
+ const queryModel = urlObj.searchParams.get('model');
11
+ const queryApiKey = extractApiKey(req);
12
+ const querySessionId = urlObj.searchParams.get('sessionId');
13
+
14
+ const conn = await resolveAgentConnection({
15
+ provider: queryProvider,
16
+ model: queryModel,
17
+ apiKey: queryApiKey,
18
+ sessionId: querySessionId,
19
+ });
20
+
21
+ const totalTools = conn.agent?.getAllToolsSchema?.().length || toolsSchema.length;
22
+
23
+ sendJson(res, 200, {
24
+ status: 'ok',
25
+ server: 'Kiku Web Chatbot',
26
+ port,
27
+ connected: conn.ready,
28
+ state: conn.ready ? 'ready' : (conn.state || 'server_offline'),
29
+ provider: conn.provider,
30
+ model: conn.model,
31
+ activeModel: conn.model,
32
+ activeLoadedModel: conn.resolution?.activeLoadedModel || conn.model,
33
+ loadedModels: conn.models || [],
34
+ usedFallback: conn.usedFallback || false,
35
+ error: conn.error || null,
36
+ capabilities: {
37
+ reactAgent: Boolean(conn.agent),
38
+ streaming: true,
39
+ toolCalling: Boolean(conn.agent),
40
+ localTools: toolsSchema.length,
41
+ mcpTools: Math.max(0, totalTools - toolsSchema.length),
42
+ totalTools,
43
+ sessionMemory: Boolean(conn.agent),
44
+ longTermMemory: Boolean(conn.agent?.memory),
45
+ cachedAgent: conn.cached === true,
46
+ },
47
+ memory: getAgentMemorySnapshot(conn.agent, querySessionId)
48
+ });
49
+ } catch (err) {
50
+ sendError(res, 500, err.message);
51
+ }
52
+ }
@@ -0,0 +1,77 @@
1
+ import { executeTool } from '../../agent/tools.js';
2
+ import { CATEGORIZED_SUGGESTIONS, DEFAULT_SUGGESTIONS } from '../ui/scripts/suggestionsData.js';
3
+ import { resolveAgentConnection } from '../core/connection.js';
4
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
5
+ import { extractApiKey } from '../core/security.js';
6
+
7
+ export async function handleSuggestionsRoute(req, res) {
8
+ sendJson(res, 200, {
9
+ suggestions: DEFAULT_SUGGESTIONS,
10
+ categories: CATEGORIZED_SUGGESTIONS
11
+ });
12
+ }
13
+
14
+ const AI_SUGGEST_LANGS = new Set(['th', 'zh', 'en', 'ja', 'ko']);
15
+
16
+ function normalizeAiSuggestions(raw) {
17
+ const list = Array.isArray(raw) ? raw : [];
18
+ const out = [];
19
+ for (const item of list) {
20
+ const q = typeof item === 'string' ? item : (item?.question || item?.query || item?.text || '');
21
+ const clean = String(q || '').replace(/\*\*(.+?)\*\*/g, '$1').trim();
22
+ if (clean.length > 3 && out.length < 8 && !out.includes(clean)) out.push(clean);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ export async function handleAiSuggestRoute(req, res) {
28
+ let body = {};
29
+ try {
30
+ body = await parseJsonBody(req);
31
+ } catch {}
32
+
33
+ const { topic = '' } = body;
34
+ const limit = Math.min(Math.max(Number(body.limit) || 5, 2), 8);
35
+ const lang = AI_SUGGEST_LANGS.has(String(body.lang || '').toLowerCase())
36
+ ? String(body.lang).toLowerCase()
37
+ : 'en';
38
+ let suggestions = [];
39
+ let source = 'curated';
40
+
41
+ try {
42
+ const apiKey = extractApiKey(req);
43
+ const conn = await resolveAgentConnection({
44
+ provider: body.provider,
45
+ model: body.model,
46
+ apiKey,
47
+ sessionId: body.sessionId,
48
+ });
49
+ const controller = new AbortController();
50
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
51
+ try {
52
+ if (conn.ready && conn.agent?.generateSuggestions) {
53
+ const toolResult = await executeTool(
54
+ 'suggest_topics',
55
+ { topic: topic || undefined, limit, lang },
56
+ { agent: conn.agent, signal: controller.signal }
57
+ );
58
+ suggestions = normalizeAiSuggestions(toolResult?.suggestions || toolResult?.topics);
59
+ if (suggestions.length > 0) source = 'ai';
60
+ }
61
+ } finally {
62
+ clearTimeout(timeoutId);
63
+ }
64
+ } catch {}
65
+
66
+ if (suggestions.length === 0) {
67
+ suggestions = DEFAULT_SUGGESTIONS.slice(0, 8);
68
+ }
69
+
70
+ sendJson(res, 200, {
71
+ status: 'ok',
72
+ suggestions,
73
+ topic: topic || null,
74
+ lang,
75
+ source
76
+ });
77
+ }
@@ -0,0 +1,34 @@
1
+ import { timelineData } from '../../../src/data/timelineData.js';
2
+ import { sendJson } from '../core/errorHandler.js';
3
+
4
+ export async function handleTimelineRoute(req, res, urlObj) {
5
+ if (req.method !== 'GET') {
6
+ sendJson(res, 405, { status: 'error', error: 'Method not allowed' });
7
+ return;
8
+ }
9
+
10
+ const year = urlObj.searchParams.get('year') || '';
11
+ const era = (urlObj.searchParams.get('era') || '').toLowerCase();
12
+ const category = (urlObj.searchParams.get('category') || '').toLowerCase();
13
+ const search = (urlObj.searchParams.get('search') || urlObj.searchParams.get('q') || '').toLowerCase();
14
+
15
+ let results = [...timelineData];
16
+ if (year) results = results.filter(item => String(item.year).includes(year));
17
+ if (era) results = results.filter(item => String(item.era || '').toLowerCase().includes(era));
18
+ if (category) results = results.filter(item => String(item.category || '').toLowerCase().includes(category));
19
+ if (search) {
20
+ results = results.filter(item => [item.title, item.titleCn, item.summary, item.badge]
21
+ .some(value => String(value || '').toLowerCase().includes(search)));
22
+ }
23
+
24
+ sendJson(res, 200, {
25
+ status: 'success',
26
+ code: 200,
27
+ timestamp: new Date().toISOString(),
28
+ data: results,
29
+ meta: {
30
+ totalMilestones: timelineData.length,
31
+ filteredCount: results.length
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,57 @@
1
+ import { executeTool } from '../../agent/tools.js';
2
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+
4
+ export async function handleVerifyRoute(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 { claim } = body;
13
+ if (!claim) {
14
+ return sendError(res, 400, 'Claim is required');
15
+ }
16
+
17
+ let result = null;
18
+
19
+ try {
20
+ const searchResult = await executeTool('global_search', { query: claim });
21
+ const rawResults = searchResult?.results;
22
+ const flatItems = Array.isArray(rawResults)
23
+ ? rawResults
24
+ : Object.values(rawResults || {}).flat().filter(item => typeof item === 'object');
25
+
26
+ const hasDirectMatch = flatItems.some(r =>
27
+ String(r.title || r.name || '').toLowerCase().includes(claim.toLowerCase()) ||
28
+ String(r.description || r.synopsis || '').toLowerCase().includes(claim.toLowerCase())
29
+ );
30
+
31
+ result = {
32
+ verdict: hasDirectMatch ? 'PARTIALLY_VERIFIED' : 'UNVERIFIED',
33
+ claim,
34
+ evidence: flatItems.slice(0, 3),
35
+ note: hasDirectMatch
36
+ ? 'A related entry was found in the Ju Jingyi knowledge graph; review the evidence before relying on the claim.'
37
+ : 'No supporting entry was found in the Ju Jingyi knowledge graph.'
38
+ };
39
+ } catch {
40
+ result = {
41
+ verdict: 'UNKNOWN',
42
+ claim,
43
+ evidence: [],
44
+ note: 'Verification could not be completed. Treat the claim as unverified.'
45
+ };
46
+ }
47
+
48
+ sendJson(res, 200, {
49
+ status: 'ok',
50
+ claim,
51
+ verdict: result.verdict || result.verdicts?.[0] || 'UNKNOWN',
52
+ confidence: result.confidence || (result.verdict === 'PARTIALLY_VERIFIED' ? 'medium' : 'low'),
53
+ evidence: result.evidence || result.supporting || result.counter || [],
54
+ detail: result.detail || result.analysis || result.note || null,
55
+ durationMs: result.durationMs || 0
56
+ });
57
+ }
@@ -0,0 +1,116 @@
1
+ import http from 'node:http';
2
+ import { handleWebChatRequest } from './router.js';
3
+ import { currentAgent, setCurrentAgent, webAgentCache } from './core/connection.js';
4
+ import { config } from '../agent/config.js';
5
+
6
+ let activeServer = null;
7
+ let activePort = Number(process.env.PORT_WEB_CHAT || 3300);
8
+ let activeHost = process.env.KIKU_WEB_CHAT_HOST || '127.0.0.1';
9
+
10
+ export function getWebChatServerStatus() {
11
+ if (!activeServer) {
12
+ return {
13
+ running: false,
14
+ port: activePort,
15
+ url: `http://${activeHost}:${activePort}`,
16
+ };
17
+ }
18
+ return {
19
+ running: true,
20
+ port: activePort,
21
+ url: `http://${activeHost}:${activePort}`,
22
+ provider: currentAgent?.client?.provider || config.activeProvider || 'lmstudio',
23
+ model: currentAgent?.client?.model || 'google/gemma-4-e4b',
24
+ };
25
+ }
26
+
27
+ export function stopWebChatServer() {
28
+ return new Promise((resolve) => {
29
+ if (!activeServer) return resolve(false);
30
+ if (typeof activeServer.closeAllConnections === 'function') {
31
+ try { activeServer.closeAllConnections(); } catch {}
32
+ }
33
+ activeServer.close(async () => {
34
+ await Promise.allSettled(
35
+ Array.from(webAgentCache.values(), connection => connection.agent?.saveSession?.())
36
+ );
37
+ webAgentCache.clear();
38
+ activeServer = null;
39
+ resolve(true);
40
+ });
41
+ });
42
+ }
43
+
44
+ function getNextSafePort(port) {
45
+ // If in Windows Hyper-V / WSL TCP excluded port range (2943..3142 or 5388..5587), jump past it
46
+ if (port >= 2940 && port <= 3150) {
47
+ return 3300;
48
+ }
49
+ if (port >= 5380 && port <= 5590) {
50
+ return 5600;
51
+ }
52
+ return port + 1;
53
+ }
54
+
55
+ export async function startWebChatServer(options = {}) {
56
+ const requestedPort = Number(options.port || process.env.PORT_WEB_CHAT || 3300);
57
+ const requestedHost = String(options.host || process.env.KIKU_WEB_CHAT_HOST || '127.0.0.1');
58
+ const isLoopback = requestedHost === '127.0.0.1' || requestedHost === '::1' || requestedHost === 'localhost';
59
+ if (!isLoopback && process.env.KIKU_ALLOW_REMOTE_WEB_CHAT !== 'true') {
60
+ throw new Error('Remote web-chat binding is disabled. Set KIKU_ALLOW_REMOTE_WEB_CHAT=true only behind an authenticated reverse proxy.');
61
+ }
62
+ if (options.agent) {
63
+ setCurrentAgent(options.agent);
64
+ }
65
+
66
+ if (activeServer) {
67
+ if (options.port && activePort !== Number(options.port)) {
68
+ await stopWebChatServer();
69
+ } else {
70
+ return Promise.resolve({
71
+ server: activeServer,
72
+ port: activePort,
73
+ url: `http://${activeHost}:${activePort}`,
74
+ alreadyRunning: true
75
+ });
76
+ }
77
+ }
78
+
79
+ return new Promise((resolve, reject) => {
80
+ let currentPort = requestedPort;
81
+ let attempts = 0;
82
+ const maxAttempts = 15;
83
+ let server = null;
84
+
85
+ function tryBind() {
86
+ server = http.createServer(async (req, res) => {
87
+ await handleWebChatRequest(req, res, { port: activePort });
88
+ });
89
+
90
+ server.on('error', (err) => {
91
+ if ((err.code === 'EACCES' || err.code === 'EADDRINUSE') && attempts < maxAttempts) {
92
+ attempts++;
93
+ currentPort = getNextSafePort(currentPort);
94
+ try { server.close(); } catch {}
95
+ setTimeout(tryBind, 20);
96
+ return;
97
+ }
98
+ reject(err);
99
+ });
100
+
101
+ server.listen(currentPort, requestedHost, () => {
102
+ activeServer = server;
103
+ activePort = currentPort;
104
+ activeHost = requestedHost;
105
+ resolve({
106
+ server,
107
+ port: activePort,
108
+ url: `http://${activeHost}:${activePort}`,
109
+ alreadyRunning: false
110
+ });
111
+ });
112
+ }
113
+
114
+ tryBind();
115
+ });
116
+ }