kiku-agent 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +69 -12
  2. package/package.json +5 -12
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/AGENTS.md +228 -0
  18. package/server/agent/eval/analyze.py +684 -0
  19. package/server/agent/eval/gates.json +15 -0
  20. package/server/agent/eval/modules/analysis/compare.js +198 -0
  21. package/server/agent/eval/modules/analysis/gates.js +72 -0
  22. package/server/agent/eval/modules/analysis/judge.js +70 -0
  23. package/server/agent/eval/modules/core/cli.js +453 -0
  24. package/server/agent/eval/modules/core/config.js +60 -0
  25. package/server/agent/eval/modules/core/store.js +236 -0
  26. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  27. package/server/agent/eval/modules/evaluation/runner.js +288 -0
  28. package/server/agent/eval/modules/evaluation/scoring.js +363 -0
  29. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  30. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  31. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  32. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  33. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  34. package/server/agent/eval/modules/ui/evalUi.js +4799 -0
  35. package/server/agent/eval/modules/utils/smokes.js +119 -0
  36. package/server/agent/eventBus.js +1 -0
  37. package/server/agent/ink/main.jsx +196 -191
  38. package/server/agent/jobManager.js +11 -1
  39. package/server/agent/lanDiscovery.js +721 -23
  40. package/server/agent/llmClient.js +317 -71
  41. package/server/agent/lmStudioWakeup.js +517 -65
  42. package/server/agent/locationService.js +797 -0
  43. package/server/agent/mcpQuizData.js +509 -0
  44. package/server/agent/memory/contextAllocator.js +19 -7
  45. package/server/agent/memory.js +3 -1
  46. package/server/agent/modelCompatibility.js +352 -0
  47. package/server/agent/modelDefaults.js +1 -0
  48. package/server/agent/modelManager.js +46 -2
  49. package/server/agent/newsWatch.js +9 -3
  50. package/server/agent/processManager.js +229 -0
  51. package/server/agent/prompts.js +174 -8
  52. package/server/agent/routingInterceptor.js +9 -1
  53. package/server/agent/sanitizer.js +41 -6
  54. package/server/agent/searchQueryPlanner.js +202 -0
  55. package/server/agent/session.js +59 -4
  56. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  57. package/server/agent/tests/agentFollowUps.test.js +253 -0
  58. package/server/agent/tests/agentMusicControl.test.js +245 -0
  59. package/server/agent/tests/apiRouter.test.js +108 -0
  60. package/server/agent/tests/architectureFixes.test.js +81 -0
  61. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  62. package/server/agent/tests/autoContext.test.js +176 -0
  63. package/server/agent/tests/autoResearch.test.js +64 -0
  64. package/server/agent/tests/bilibiliEmbed.test.js +296 -0
  65. package/server/agent/tests/catalogSync.test.js +130 -0
  66. package/server/agent/tests/chatHardening.test.js +121 -0
  67. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  68. package/server/agent/tests/citations.test.js +180 -0
  69. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  70. package/server/agent/tests/clarifyingQuestions.test.js +627 -0
  71. package/server/agent/tests/cliAutomation.test.js +158 -0
  72. package/server/agent/tests/cloudProviders.test.js +322 -0
  73. package/server/agent/tests/commandInput.test.js +24 -0
  74. package/server/agent/tests/commands.test.js +506 -0
  75. package/server/agent/tests/contextIntelligence.test.js +209 -0
  76. package/server/agent/tests/conversationSafety.test.js +53 -0
  77. package/server/agent/tests/cpuMetrics.test.js +101 -0
  78. package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
  79. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  80. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  81. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  82. package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
  83. package/server/agent/tests/engineService.test.js +436 -0
  84. package/server/agent/tests/engineUnload.test.js +94 -0
  85. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  86. package/server/agent/tests/envBootstrap.test.js +87 -0
  87. package/server/agent/tests/envCommand.test.js +284 -0
  88. package/server/agent/tests/errorHandling.test.js +80 -0
  89. package/server/agent/tests/errors.test.js +13 -0
  90. package/server/agent/tests/evalAnalyze.test.js +54 -0
  91. package/server/agent/tests/evalCommand.test.js +54 -0
  92. package/server/agent/tests/evalCorpus.test.js +126 -0
  93. package/server/agent/tests/evalHarness.test.js +62 -0
  94. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  95. package/server/agent/tests/evalMetrics.test.js +320 -0
  96. package/server/agent/tests/evalStore.test.js +190 -0
  97. package/server/agent/tests/evalStudio.test.js +179 -0
  98. package/server/agent/tests/feedbackLearning.test.js +186 -0
  99. package/server/agent/tests/fetchApi.test.js +99 -0
  100. package/server/agent/tests/firstRun.test.js +44 -0
  101. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  102. package/server/agent/tests/hardwareTiers.test.js +206 -0
  103. package/server/agent/tests/hybridWebSearch.test.js +194 -0
  104. package/server/agent/tests/judgeService.test.js +251 -0
  105. package/server/agent/tests/lanDiscovery.test.js +324 -0
  106. package/server/agent/tests/lanInterfaces.test.js +510 -0
  107. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  108. package/server/agent/tests/locationTool.test.js +278 -0
  109. package/server/agent/tests/mcpClient.test.js +201 -0
  110. package/server/agent/tests/memoryRoute.test.js +173 -0
  111. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  112. package/server/agent/tests/modelCompatibility.test.js +148 -0
  113. package/server/agent/tests/modelManager.test.js +141 -0
  114. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  115. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  116. package/server/agent/tests/newCommands.test.js +347 -0
  117. package/server/agent/tests/newCommandsTui.test.js +141 -0
  118. package/server/agent/tests/newTools.test.js +113 -0
  119. package/server/agent/tests/newsIntelligence.test.js +178 -0
  120. package/server/agent/tests/nextGenArchitecture.test.js +101 -0
  121. package/server/agent/tests/offlineInput.test.js +217 -0
  122. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  123. package/server/agent/tests/openDomainSearch.test.js +121 -0
  124. package/server/agent/tests/packagingGuard.test.js +57 -0
  125. package/server/agent/tests/presentation.test.js +61 -0
  126. package/server/agent/tests/processManager.test.js +144 -0
  127. package/server/agent/tests/providerPersistence.test.js +44 -0
  128. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  129. package/server/agent/tests/queryDetails.test.js +126 -0
  130. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  131. package/server/agent/tests/realWorldValidation.test.js +391 -0
  132. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  133. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  134. package/server/agent/tests/sanitizer.test.js +37 -0
  135. package/server/agent/tests/scenariosData.js +378 -0
  136. package/server/agent/tests/schemaRouting.test.js +54 -0
  137. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  138. package/server/agent/tests/securityAuditRemediation.test.js +221 -0
  139. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  140. package/server/agent/tests/securityHardening.test.js +146 -0
  141. package/server/agent/tests/securitySanitizer.test.js +38 -0
  142. package/server/agent/tests/sessionAndCopy.test.js +267 -0
  143. package/server/agent/tests/sidebarControls.test.js +135 -0
  144. package/server/agent/tests/statefulMemory.test.js +277 -0
  145. package/server/agent/tests/subagent.test.js +216 -0
  146. package/server/agent/tests/subagentProgress.test.js +109 -0
  147. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  148. package/server/agent/tests/suggestions.test.js +199 -0
  149. package/server/agent/tests/suggestionsTui.test.js +251 -0
  150. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  151. package/server/agent/tests/terminalImage.test.js +80 -0
  152. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  153. package/server/agent/tests/terminalText.test.js +133 -0
  154. package/server/agent/tests/textProtocolLoop.test.js +197 -0
  155. package/server/agent/tests/textToolParsing.test.js +187 -0
  156. package/server/agent/tests/theme.test.js +26 -0
  157. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  158. package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
  159. package/server/agent/tests/tools.test.js +259 -0
  160. package/server/agent/tests/unifiedEndpoints.test.js +159 -0
  161. package/server/agent/tests/v110Features.test.js +262 -0
  162. package/server/agent/tests/versionBump.test.js +69 -0
  163. package/server/agent/tests/vramEstimator.test.js +595 -0
  164. package/server/agent/tests/webChatDocs.test.js +199 -0
  165. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  166. package/server/agent/tests/webChatInspection.test.js +551 -0
  167. package/server/agent/tests/webChatMcp.test.js +94 -0
  168. package/server/agent/tests/webChatMemory.test.js +112 -0
  169. package/server/agent/tests/webChatSecurity.test.js +169 -0
  170. package/server/agent/tests/webExtract.test.js +284 -0
  171. package/server/agent/tests/webSources.test.js +281 -0
  172. package/server/agent/textToolProtocol.js +173 -46
  173. package/server/agent/toolRouter.js +318 -44
  174. package/server/agent/tools.js +877 -96
  175. package/server/agent/ui.js +39 -34
  176. package/server/agent/vramService.js +35 -54
  177. package/server/agent/webSearch.js +441 -162
  178. package/server/agent/websearch/orchestrator.js +15 -5
  179. package/server/agent/websearch/resilience/cache.js +9 -2
  180. package/server/agent/websearch/security/ssrf.js +41 -10
  181. package/server/agent/youtubeAuth.js +5 -20
  182. package/server/apiRouter.js +5558 -4422
  183. package/server/eval/judgeRubrics.js +110 -0
  184. package/server/eval/judgeService.js +290 -0
  185. package/server/eval/runBenchmark.js +182 -0
  186. package/server/mcp/data/discography.js +2 -2
  187. package/server/mcp/examples/antigravity.example.json +17 -0
  188. package/server/mcp/examples/librechat.example.yaml +16 -0
  189. package/server/mcp/handler.js +8 -4
  190. package/server/mcp/security.js +220 -21
  191. package/server/mcp/server.js +2 -1
  192. package/server/mcp/tests/connection.test.js +162 -0
  193. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  194. package/server/mcp/tools.js +7 -1
  195. package/server/supabase_migration_prefix.sql +102 -0
  196. package/server/supabase_schema.sql +63 -0
  197. package/server/web-chat/core/connection.js +179 -218
  198. package/server/web-chat/core/conversationContext.js +9 -2
  199. package/server/web-chat/core/directCatalogEngine.js +52 -28
  200. package/server/web-chat/core/dramaKnowledge.js +33 -22
  201. package/server/web-chat/core/followUpEngine.js +735 -81
  202. package/server/web-chat/core/futureEventGuard.js +5 -1
  203. package/server/web-chat/core/musicControlAction.js +70 -0
  204. package/server/web-chat/core/security.js +32 -21
  205. package/server/web-chat/core/toolInspector.js +6 -6
  206. package/server/web-chat/core/urlGuard.js +45 -0
  207. package/server/web-chat/router.js +204 -157
  208. package/server/web-chat/routes/chatRoute.js +885 -488
  209. package/server/web-chat/routes/engineRoute.js +270 -0
  210. package/server/web-chat/routes/exportRoute.js +1 -1
  211. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  212. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  213. package/server/web-chat/routes/mcpRoute.js +386 -0
  214. package/server/web-chat/routes/memoryRoute.js +253 -0
  215. package/server/web-chat/routes/providerRoute.js +49 -16
  216. package/server/web-chat/routes/staticRoute.js +306 -17
  217. package/server/web-chat/routes/statusRoute.js +194 -73
  218. package/server/web-chat/routes/suggestRoute.js +11 -3
  219. package/server/web-chat/routes/verifyRoute.js +26 -4
  220. package/server/web-chat/server.js +7 -6
  221. package/server/web-chat/ui/docsBuilder.js +3558 -0
  222. package/server/web-chat/ui/htmlBuilder.js +12 -0
  223. package/server/web-chat/ui/scripts/client.js +3401 -1302
  224. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  225. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  226. package/server/web-chat/ui/styles/lan-card.css +165 -0
  227. package/server/web-chat/ui/styles/main.css +4289 -447
  228. package/server/web-chat/ui/sw.js +64 -0
  229. package/server/web-chat/ui/templates/body.html +407 -281
  230. package/src/data/awardsData.js +146 -146
  231. package/src/data/musicVideoData.js +6 -4
  232. package/src/lib/vram/estimator.js +195 -19
@@ -40,7 +40,11 @@ export function findFutureDates(text, now = new Date()) {
40
40
  }
41
41
 
42
42
  function hasLiveVerification(tools = []) {
43
- return tools.some(tool => /^(?:web_search|web_extract|fetch_webpage|verify_claim)$/.test(tool?.name || ''));
43
+ return tools.some(tool => {
44
+ const name = tool?.name || tool?.function?.name || '';
45
+ if (tool?.capabilities?.includes('live_verification') || tool?.capabilities?.includes('catalog_verification')) return true;
46
+ return /^(?:web_search|web_extract|fetch_webpage|verify_claim|search_news|search_chinese_web|search_bilibili|search_encyclopedia|search_works|get_work_detail|get_career_timeline|global_search)$/.test(name);
47
+ });
44
48
  }
45
49
 
46
50
  function redactUnverifiedFutureDates(text, now) {
@@ -0,0 +1,70 @@
1
+ const MUSIC_CONTROL_ACTIONS = new Map([
2
+ ['play_music', 'play'],
3
+ ['pause_music', 'pause'],
4
+ ['resume_music', 'resume'],
5
+ ['stop_music', 'stop'],
6
+ ['next_track', 'next'],
7
+ ['prev_track', 'prev'],
8
+ ['set_volume', 'set_volume'],
9
+ ['mute_music', 'mute'],
10
+ ['unmute_music', 'unmute'],
11
+ ['seek_music', 'seek'],
12
+ ['set_repeat_mode', 'set_repeat'],
13
+ ['set_shuffle_mode', 'set_shuffle'],
14
+ ['toggle_video_mode', 'toggle_video'],
15
+ ]);
16
+
17
+ function text(value, maxLength = 500) {
18
+ return typeof value === 'string' ? value.slice(0, maxLength) : undefined;
19
+ }
20
+
21
+ function normalizeSong(song) {
22
+ if (!song || typeof song !== 'object') return undefined;
23
+ const youtubeId = text(song.youtubeId, 32);
24
+ return {
25
+ id: text(song.id, 200),
26
+ title: text(song.title),
27
+ titleTh: text(song.titleTh),
28
+ titleCn: text(song.titleCn),
29
+ titleEn: text(song.titleEn),
30
+ youtubeId: youtubeId && /^[a-zA-Z0-9_-]{11}$/.test(youtubeId) ? youtubeId : undefined,
31
+ duration: text(song.duration, 32),
32
+ category: text(song.category, 100),
33
+ dramaName: text(song.dramaName),
34
+ };
35
+ }
36
+
37
+ export function createMusicControlAction(toolName, result) {
38
+ const fallbackAction = MUSIC_CONTROL_ACTIONS.get(toolName);
39
+ if (!fallbackAction || !result || typeof result !== 'object') return null;
40
+
41
+ const action = fallbackAction;
42
+ const event = {
43
+ type: 'ui_action',
44
+ target: 'music_player',
45
+ tool: toolName,
46
+ action,
47
+ };
48
+
49
+ if (result.status === 'error') return event;
50
+
51
+ const playing = text(result.playing);
52
+ const currentTrack = text(result.currentTrack);
53
+ const song = normalizeSong(result.song);
54
+ if (playing !== undefined) event.playing = playing;
55
+ if (currentTrack !== undefined) event.currentTrack = currentTrack;
56
+ if (song !== undefined) event.song = song;
57
+ if (action === 'set_volume') event.volume = Math.max(0, Math.min(100, Number(result.volume) || 0));
58
+ if (action === 'seek') event.seconds = Math.max(0, Number(result.seconds) || 0);
59
+ if (action === 'set_repeat') event.mode = ['off', 'one', 'all'].includes(result.mode) ? result.mode : 'off';
60
+ if (action === 'set_shuffle') event.enabled = Boolean(result.enabled);
61
+ if (action === 'toggle_video') event.isVideoOpen = Boolean(result.isVideoOpen);
62
+
63
+ return event;
64
+ }
65
+
66
+ export function collectMusicControlActions(tools = []) {
67
+ return tools
68
+ .map((tool) => createMusicControlAction(tool?.name, tool?.result))
69
+ .filter(Boolean);
70
+ }
@@ -43,10 +43,10 @@ export function applySecurityHeaders(req, res) {
43
43
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
44
44
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
45
45
  res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
46
- res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(), payment=(), usb=()');
46
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(self), payment=(), usb=()');
47
47
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
48
48
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
49
- res.setHeader('Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' https://www.youtube.com https://www.youtube-nocookie.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https:; media-src 'self' blob: https:; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.bilibili.com https://www.bilibili.com; connect-src 'self' http://localhost:* http://127.0.0.1:* https:;");
49
+ res.setHeader('Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' https://www.youtube.com https://www.youtube-nocookie.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net data:; img-src 'self' data: blob: https:; media-src 'self' blob: https:; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.bilibili.com https://www.bilibili.com https://*.openstreetmap.org https://openstreetmap.org; connect-src 'self' http://localhost:* http://127.0.0.1:* https:;");
50
50
  }
51
51
  return !origin || Boolean(allowedOrigin);
52
52
  }
@@ -64,13 +64,23 @@ export function extractApiKey(req, urlObj = null) {
64
64
  }
65
65
 
66
66
  export function resolveClientIp(req) {
67
- const directIp = req?.socket?.remoteAddress || '127.0.0.1';
67
+ const directIp = String(req?.socket?.remoteAddress || 'unknown');
68
68
  const trustProxy = process.env.VERCEL === '1' || process.env.KIKU_TRUST_PROXY === 'true';
69
- if (!trustProxy) return String(directIp);
69
+ if (!trustProxy) return directIp;
70
70
  const forwarded = req?.headers?.['x-forwarded-for'];
71
- return typeof forwarded === 'string' && forwarded.trim()
72
- ? forwarded.split(',')[0].trim()
73
- : String(directIp);
71
+ if (typeof forwarded === 'string' && forwarded.trim()) {
72
+ const parts = forwarded.split(',').map(s => s.trim()).filter(Boolean);
73
+ const candidate = parts[0] || directIp;
74
+ const isCandidateLoopback = candidate === '127.0.0.1' || candidate === '::1' || candidate === 'localhost' || candidate.startsWith('::ffff:127.');
75
+ const isDirectLoopback = directIp === '127.0.0.1' || directIp === '::1' || directIp === 'localhost' || directIp.startsWith('::ffff:127.');
76
+
77
+ if (isCandidateLoopback && !isDirectLoopback) {
78
+ const nonLoopback = parts.find(p => p !== '127.0.0.1' && p !== '::1' && p !== 'localhost' && !p.startsWith('::ffff:127.'));
79
+ return nonLoopback || directIp;
80
+ }
81
+ return candidate;
82
+ }
83
+ return directIp;
74
84
  }
75
85
 
76
86
  function constantTimeEqual(left, right) {
@@ -80,41 +90,42 @@ function constantTimeEqual(left, right) {
80
90
  }
81
91
 
82
92
  export function isWebChatAdmin(req) {
83
- // Explicit opt-in only: never infer test mode from NODE_ENV/--test flags,
84
- // otherwise a stray NODE_ENV=test in production would disable admin auth.
93
+
85
94
  if (process.env.WEBCHAT_ALLOW_TEST_ADMIN === 'true' && process.env.NODE_ENV !== 'production') return true;
86
95
  const expected = process.env.WEBCHAT_ADMIN_KEY || '';
87
96
  if (!expected) return false;
88
97
  return constantTimeEqual(extractApiKey(req), expected);
89
98
  }
90
99
 
91
- export function checkChatRateLimit(clientIp) {
92
- const ip = clientIp || 'unknown';
100
+ export function checkRateLimit(clientIp, bucket = 'default', limit = MAX_CHAT_REQUESTS_PER_MINUTE) {
101
+ const ip = bucket + ':' + (clientIp || 'unknown');
93
102
  const now = Date.now();
94
103
  const entry = rateLimitStore.get(ip);
95
104
 
96
105
  if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
97
106
  rateLimitStore.set(ip, { count: 1, windowStart: now });
98
- return { allowed: true, remaining: MAX_CHAT_REQUESTS_PER_MINUTE - 1 };
107
+ return { allowed: true, remaining: limit - 1 };
99
108
  }
100
109
 
101
110
  entry.count++;
102
- if (entry.count > MAX_CHAT_REQUESTS_PER_MINUTE) {
111
+ if (entry.count > limit) {
103
112
  const retryAfter = Math.ceil((RATE_LIMIT_WINDOW_MS - (now - entry.windowStart)) / 1000);
104
113
  return { allowed: false, retryAfter };
105
114
  }
106
115
 
107
- return { allowed: true, remaining: MAX_CHAT_REQUESTS_PER_MINUTE - entry.count };
116
+ return { allowed: true, remaining: limit - entry.count };
108
117
  }
109
118
 
110
- export async function checkChatRateLimitDistributed(clientIp) {
119
+ export function checkChatRateLimit(ip) { return checkRateLimit(ip, 'chat', 60); }
120
+
121
+ export async function checkChatRateLimitDistributed(clientIp, bucket = 'chat', limit = 60) {
111
122
  const redisUrl = process.env.UPSTASH_REDIS_REST_URL;
112
123
  const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN;
113
- if (!redisUrl || !redisToken) return checkChatRateLimit(clientIp);
124
+ if (!redisUrl || !redisToken) return checkRateLimit(clientIp, bucket, limit);
114
125
 
115
126
  const now = Date.now();
116
127
  const identity = createHash('sha256').update(String(clientIp || 'unknown')).digest('hex');
117
- const key = `webchat-rate:${identity}:${Math.floor(now / RATE_LIMIT_WINDOW_MS)}`;
128
+ const key = `webchat-rate:${bucket}:${identity}:${Math.floor(now / RATE_LIMIT_WINDOW_MS)}`;
118
129
  try {
119
130
  const response = await fetch(`${redisUrl.replace(/\/$/, '')}/pipeline`, {
120
131
  method: 'POST',
@@ -126,13 +137,13 @@ export async function checkChatRateLimitDistributed(clientIp) {
126
137
  const result = await response.json();
127
138
  const count = Number(result?.[0]?.result) || 1;
128
139
  return {
129
- allowed: count <= MAX_CHAT_REQUESTS_PER_MINUTE,
130
- remaining: Math.max(0, MAX_CHAT_REQUESTS_PER_MINUTE - count),
131
- retryAfter: count <= MAX_CHAT_REQUESTS_PER_MINUTE ? 0 : 60,
140
+ allowed: count <= limit,
141
+ remaining: Math.max(0, limit - count),
142
+ retryAfter: count <= limit ? 0 : 60,
132
143
  };
133
144
  }
134
145
  } catch {}
135
- return checkChatRateLimit(clientIp);
146
+ return checkRateLimit(clientIp, bucket, limit);
136
147
  }
137
148
 
138
149
 
@@ -13,19 +13,19 @@ export function makeInspectableToolPayload(value, options = {}) {
13
13
  if (typeof item === 'bigint') return String(item);
14
14
  if (typeof item === 'string') {
15
15
  return item.length > maxStringLength
16
- ? item.slice(0, maxStringLength) + '\n… [ตัดข้อความที่ยาวเกินไป]'
16
+ ? item.slice(0, maxStringLength) + '\n… ตัดข้อความที่ยาวเกินไป · ย่อแล้ว'
17
17
  : item;
18
18
  }
19
- if (typeof item === 'undefined') return '[undefined]';
19
+ if (typeof item === 'undefined') return '—';
20
20
  if (typeof item !== 'object') return String(item);
21
- if (depth >= maxDepth) return '[ตัดข้อมูลที่ซ้อนลึกเกินไป]';
22
- if (seen.has(item)) return '[อ้างอิงซ้ำ]';
21
+ if (depth >= maxDepth) return '… ข้อมูลซ้อนลึก · ย่อแล้ว';
22
+ if (seen.has(item)) return '○ อ้างอิงซ้ำ';
23
23
  seen.add(item);
24
24
 
25
25
  if (Array.isArray(item)) {
26
26
  const output = item.slice(0, maxArrayItems).map(entry => visit(entry, depth + 1));
27
27
  if (item.length > maxArrayItems) {
28
- output.push(`… [ตัดอีก ${item.length - maxArrayItems} รายการ]`);
28
+ output.push(`… ย่ออีก ${item.length - maxArrayItems} รายการ`);
29
29
  }
30
30
  return output;
31
31
  }
@@ -33,7 +33,7 @@ export function makeInspectableToolPayload(value, options = {}) {
33
33
  const output = {};
34
34
  const entries = Object.entries(item);
35
35
  entries.slice(0, maxObjectKeys).forEach(([key, entry]) => {
36
- output[key] = SENSITIVE_TOOL_FIELD.test(key) ? '[redacted]' : visit(entry, depth + 1);
36
+ output[key] = SENSITIVE_TOOL_FIELD.test(key) ? '···' : visit(entry, depth + 1);
37
37
  });
38
38
  if (entries.length > maxObjectKeys) {
39
39
  output.__truncated__ = `ตัดอีก ${entries.length - maxObjectKeys} ฟิลด์`;
@@ -0,0 +1,45 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import { isIP } from 'node:net';
3
+ import { parseIPv4, isPrivateIPv4, isPrivateIPv6 } from '../../agent/websearch/security/ssrf.js';
4
+ import { isWebChatAdmin, resolveClientIp } from './security.js';
5
+
6
+ function ipv4Address(host) {
7
+ const mapped = host.match(/^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/i);
8
+ if (mapped) return parseIPv4(String(parseInt(mapped[1], 16) * 65536 + parseInt(mapped[2], 16)));
9
+ return parseIPv4(host.replace(/^::ffff:/i, ''));
10
+ }
11
+
12
+ export function isTrustedRequest(req) {
13
+ const ip = resolveClientIp(req).toLowerCase();
14
+ const v4 = ipv4Address(ip);
15
+ return ip === '::1' || Boolean(v4?.startsWith('127.')) || isWebChatAdmin(req);
16
+ }
17
+
18
+ export async function validateInferenceBaseUrl(rawUrl, { callerIsTrusted = false, trusted = callerIsTrusted, lookupHost = lookup } = {}) {
19
+ if (typeof rawUrl !== 'string' || !rawUrl.trim() || rawUrl.length > 2048) return { ok: false, reason: 'Invalid baseUrl length' };
20
+ let url;
21
+ try { url = new URL(rawUrl.trim()); } catch { return { ok: false, reason: 'Invalid baseUrl' }; }
22
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) return { ok: false, reason: 'baseUrl must use HTTP(S), without credentials or fragment' };
23
+ const host = url.hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
24
+ if (host === 'metadata.google.internal' || host.endsWith('.metadata.google.internal')) return { ok: false, reason: 'Metadata endpoint is forbidden' };
25
+ let addresses;
26
+ try {
27
+ addresses = isIP(host) ? [{ address: host }] : await lookupHost(host, { all: true, verbatim: true });
28
+ } catch { return { ok: false, reason: 'Cannot resolve baseUrl hostname' }; }
29
+ if (!addresses.length) return { ok: false, reason: 'Cannot resolve baseUrl hostname' };
30
+ for (const { address } of addresses) {
31
+ const v4 = ipv4Address(address);
32
+ if (v4?.startsWith('169.254.') || /^fe[89ab]/i.test(address) || address.toLowerCase() === 'fd00:ec2::254') return { ok: false, reason: 'Metadata/link-local endpoint is forbidden' };
33
+ if (!trusted && (v4 ? isPrivateIPv4(v4) : isPrivateIPv6(address))) return { ok: false, reason: 'Private/loopback baseUrl requires a local or admin caller' };
34
+ }
35
+ return { ok: true, normalized: url.href.replace(/\/+$/, '') };
36
+ }
37
+
38
+ export function sanitizeInferenceError(error, trusted = false) {
39
+ if (!trusted) return 'Inference provider unavailable. Check provider settings or contact the administrator.';
40
+ return String(error?.message || error || 'Inference provider unavailable')
41
+ .replace(/Bearer\s+[^\s]+/gi, 'Bearer [redacted]')
42
+ .replace(/(?:sk-|AIza)[A-Za-z0-9_-]+/g, '[redacted]')
43
+ .replace(/https?:\/\/[^\s]+/g, '[endpoint]')
44
+ .replace(/[\r\n\x00-\x1f]+/g, ' ').slice(0, 200);
45
+ }