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
@@ -0,0 +1,281 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ parseRssFeed,
6
+ parseBaikePage,
7
+ parseBaiduResults,
8
+ detectLanguage,
9
+ checkUrlLiveness,
10
+ decodeBaiduUrl,
11
+ buildImageSearchQuery,
12
+ titleMentionsArtist,
13
+ isLowValueImageUrl,
14
+ imageHostKey,
15
+ } from '../webSearch.js';
16
+ import { executeTool, prepareToolArguments, toolHandlers, toolsSchema } from '../tools.js';
17
+ import { dynamicToolRouter as toolRouter } from '../toolRouter.js';
18
+ import { extractCitations } from '../citations.js';
19
+ import { PUBLIC_REMOTE_TOOL_NAMES } from '../publicTools.js';
20
+ import { getMcpToolNames } from '../../mcp/tools.js';
21
+
22
+ const NEW_WEB_TOOLS = [
23
+ 'search_news',
24
+ 'fetch_rss',
25
+ 'search_encyclopedia',
26
+ 'search_music_platforms',
27
+ 'search_chinese_web',
28
+ 'search_bilibili',
29
+ 'search_web_images',
30
+ ];
31
+
32
+ test('new web source tools are registered in the schema, router cluster, and public surfaces', () => {
33
+ const schemaNames = new Set(toolsSchema.map(t => t.function?.name));
34
+ for (const name of NEW_WEB_TOOLS) {
35
+ assert.ok(schemaNames.has(name), `toolsSchema should contain ${name}`);
36
+ assert.ok(toolRouter.toolsByName.has(name), `router should know ${name}`);
37
+ assert.ok(PUBLIC_REMOTE_TOOL_NAMES.has(name), `web-chat public tools should include ${name}`);
38
+ assert.ok(typeof toolHandlers[name] === 'function', `toolHandlers should contain ${name}`);
39
+ }
40
+ const mcpNames = new Set(getMcpToolNames());
41
+ for (const name of ['search_news', 'fetch_rss', 'search_encyclopedia', 'search_music_platforms']) {
42
+ assert.ok(mcpNames.has(name), `MCP should expose ${name} publicly`);
43
+ }
44
+ });
45
+
46
+ test('parseRssFeed handles RSS 2.0 items with CDATA and source tags', () => {
47
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
48
+ <rss version="2.0"><channel><title>Feed Title</title>
49
+ <item>
50
+ <title><![CDATA[鞠婧祎新剧开播]]></title>
51
+ <link>https://news.example.com/a</link>
52
+ <pubDate>Mon, 01 Sep 2026 08:00:00 GMT</pubDate>
53
+ <description><![CDATA[<p>โพยละครเรื่องใหม่</p>]]></description>
54
+ <source url="https://ent.example.com">Example Ent</source>
55
+ </item>
56
+ <item>
57
+ <title>Second story</title>
58
+ <link>https://news.example.com/b</link>
59
+ <pubDate>Tue, 02 Sep 2026 09:00:00 GMT</pubDate>
60
+ <description>Plain snippet</description>
61
+ </item>
62
+ </channel></rss>`;
63
+
64
+ const items = parseRssFeed(xml);
65
+ assert.equal(items.length, 2);
66
+ assert.equal(items[0].title, '鞠婧祎新剧开播');
67
+ assert.equal(items[0].url, 'https://news.example.com/a');
68
+ assert.equal(items[0].source, 'Example Ent');
69
+ assert.match(items[0].snippet, /โพยละครเรื่องใหม่/);
70
+ assert.match(items[0].publishedAt, /Sep 2026/);
71
+ assert.equal(items[1].source, '');
72
+ });
73
+
74
+ test('parseRssFeed handles Atom entries with self-closing link tags', () => {
75
+ const xml = `<?xml version="1.0"?>
76
+ <feed xmlns="http://www.w3.org/2005/Atom">
77
+ <title>Atom Feed</title>
78
+ <entry>
79
+ <title> Kiku releases new single </title>
80
+ <link rel="alternate" href="https://blog.example.com/entry-1"/>
81
+ <published>2026-08-30T10:00:00Z</published>
82
+ <summary>Short summary text</summary>
83
+ </entry>
84
+ </feed>`;
85
+
86
+ const items = parseRssFeed(xml);
87
+ assert.equal(items.length, 1);
88
+ assert.equal(items[0].title, 'Kiku releases new single');
89
+ assert.equal(items[0].url, 'https://blog.example.com/entry-1');
90
+ assert.equal(items[0].publishedAt, '2026-08-30T10:00:00Z');
91
+ assert.equal(items[0].snippet, 'Short summary text');
92
+ });
93
+
94
+ test('parseBaikePage extracts title, summary and paragraphs from a Baike-style page', () => {
95
+ const html = `<!DOCTYPE html><html><head>
96
+ <meta name="description" content="鞠婧祎,1994年6月18日出生于四川省遂宁市,中国内地影视女演员、流行乐歌手。">
97
+ </head><body>
98
+ <h1>鞠婧祎</h1>
99
+ <div class="lemma-summary"><div class="para">鞠婧祎1994年6月18日出生于四川省遂宁市。</div>
100
+ <div class="para">2013年以SNH48成员身份正式出道。</div></div>
101
+ <p>short</p>
102
+ </body></html>`;
103
+
104
+ const parsed = parseBaikePage(html);
105
+ assert.equal(parsed.title, '鞠婧祎');
106
+ assert.match(parsed.summary, /1994年6月18日出生于四川省遂宁市/);
107
+ assert.ok(parsed.paragraphs.length >= 2);
108
+ assert.ok(parsed.paragraphs.every(p => p.length > 10));
109
+ });
110
+
111
+ test('parseBaiduResults extracts title, redirect link and snippet', () => {
112
+ const html = `<!DOCTYPE html><html><body>
113
+ <div class="result c-container" srcid="1599">
114
+ <h3 class="c-title t t tts-title"><a href="http://www.baidu.com/link?url=abc123">鞠婧祎 最新消息 - 新浪娱乐</a></h3>
115
+ <span class="content-right_8Zs40">新浪娱乐讯 9月1日,鞠婧祎工作室发布最新声明。</span>
116
+ </div>
117
+ <div class="result c-container">
118
+ <h3 class="c-title"><a href="http://www.baidu.com/link?url=def456">第二條結果</a></h3>
119
+ <span class="content-right_8Zs40">另一段摘要文字,內容超過三十個字符以方便驗證截取邏輯是否正確。</span>
120
+ </div>
121
+ </body></html>`;
122
+
123
+ const results = parseBaiduResults(html, 8);
124
+ assert.equal(results.length, 2);
125
+ assert.equal(results[0].title, '鞠婧祎 最新消息 - 新浪娱乐');
126
+ assert.equal(results[0].url, 'http://www.baidu.com/link?url=abc123');
127
+ assert.match(results[0].snippet, /最新声明/);
128
+ assert.equal(results[0].source, 'Baidu');
129
+ });
130
+
131
+ test('detectLanguage routes Thai, Chinese, and English terms', () => {
132
+ assert.equal(detectLanguage('จวีจิ้งอี ล่าสุด'), 'th');
133
+ assert.equal(detectLanguage('鞠婧祎 最新'), 'zh');
134
+ assert.equal(detectLanguage('Ju Jingyi latest news'), 'en');
135
+ });
136
+
137
+ test('search_news requires a query', async () => {
138
+ const result = await toolHandlers.search_news({});
139
+ assert.equal(result.status, 'error');
140
+ assert.match(result.message, /query parameter is required/);
141
+ });
142
+
143
+ test('fetch_rss requires a valid URL', async () => {
144
+ const result = await toolHandlers.fetch_rss({ url: 'not-a-url' });
145
+ assert.equal(result.status, 'error');
146
+ assert.match(result.message, /HTTP|URL is required|Invalid URL/);
147
+ });
148
+
149
+ test('search_encyclopedia requires a term and rejects unknown languages', async () => {
150
+ const missing = await toolHandlers.search_encyclopedia({});
151
+ assert.equal(missing.status, 'error');
152
+
153
+ const badLang = await executeTool('search_encyclopedia', { term: '鞠婧祎', language: 'fr' });
154
+ assert.equal(badLang.status, 'error');
155
+ assert.match(badLang.message || JSON.stringify(badLang), /language/i);
156
+ });
157
+
158
+ test('chinese web tools are enabled by default and KIKU_ENABLE_CN_WEB_TOOLS=false gates them', async () => {
159
+ const previous = process.env.KIKU_ENABLE_CN_WEB_TOOLS;
160
+ try {
161
+ for (const disableValue of ['false', '0']) {
162
+ process.env.KIKU_ENABLE_CN_WEB_TOOLS = disableValue;
163
+ for (const [handler, args] of [
164
+ [toolHandlers.search_chinese_web, { query: '鞠婧祎' }],
165
+ [toolHandlers.search_bilibili, { keyword: '鞠婧祎' }],
166
+ [toolHandlers.search_web_images, { query: '鞠婧祎' }],
167
+ ]) {
168
+ const gated = await handler(args);
169
+ assert.equal(gated.status, 'error');
170
+ assert.equal(gated.gated, true);
171
+ assert.match(gated.message, /KIKU_ENABLE_CN_WEB_TOOLS/);
172
+ }
173
+ }
174
+
175
+ delete process.env.KIKU_ENABLE_CN_WEB_TOOLS;
176
+ const missingQuery = await toolHandlers.search_chinese_web({});
177
+ assert.equal(missingQuery.status, 'error');
178
+ assert.equal(missingQuery.gated, undefined);
179
+ assert.match(missingQuery.message, /query parameter is required/);
180
+ const missingKeyword = await toolHandlers.search_bilibili({});
181
+ assert.equal(missingKeyword.status, 'error');
182
+ assert.match(missingKeyword.message, /keyword parameter is required/);
183
+ const missingImageQuery = await toolHandlers.search_web_images({});
184
+ assert.equal(missingImageQuery.status, 'error');
185
+ assert.match(missingImageQuery.message, /query parameter is required/);
186
+ } finally {
187
+ if (previous === undefined) delete process.env.KIKU_ENABLE_CN_WEB_TOOLS;
188
+ else process.env.KIKU_ENABLE_CN_WEB_TOOLS = previous;
189
+ }
190
+ });
191
+
192
+ test('executeTool validates the new tools through the standard pipeline', async () => {
193
+ const noArgs = await executeTool('search_music_platforms', {});
194
+ assert.equal(noArgs.status, 'error');
195
+
196
+ const prepared = prepareToolArguments('search_news', { query: 'Ju Jingyi', bogus: 1 });
197
+ assert.equal(prepared.safe, true);
198
+ assert.deepEqual(prepared.args, { query: 'Ju Jingyi' });
199
+ assert.match(prepared.warnings.join(' '), /Unknown argument "bogus"/);
200
+ });
201
+
202
+ test('router exposes news and encyclopedia tools on research-flavored queries', () => {
203
+ const routedTh = toolRouter.routeToolsForQuery('จวีจิ้งอีมีข่าวอะไรล่าสุดบ้าง');
204
+ const routedZh = toolRouter.routeToolsForQuery('鞠婧祎 最新新闻动态');
205
+ for (const routed of [routedTh, routedZh]) {
206
+ const names = new Set(routed.map(t => t.function?.name));
207
+ assert.ok(names.has('search_news'), 'search_news should be routed for news queries');
208
+ assert.ok(names.has('search_encyclopedia'), 'search_encyclopedia should be routed for research queries');
209
+ }
210
+ });
211
+
212
+ test('router exposes bilibili and music platform tools on media-flavored queries', () => {
213
+ const routed = toolRouter.routeToolsForQuery('ฟังเพลงล่าสุดของจวีจิ้งอี และดูคลิป bilibili');
214
+ const names = new Set(routed.map(t => t.function?.name));
215
+ assert.ok(names.has('search_bilibili'));
216
+ assert.ok(names.has('search_music_platforms'));
217
+ });
218
+
219
+ test('extractCitations works with recorded tool results (recordedTools regression)', () => {
220
+ const citations = extractCitations(
221
+ 'Ju Jingyi news says a new drama is coming: https://example.com/a',
222
+ [{ name: 'web_search', result: { results: [{ title: 'Ju Jingyi new drama', url: 'https://example.com/a' }] } }]
223
+ );
224
+ assert.equal(citations.length, 1);
225
+ assert.equal(citations[0].id, 1);
226
+ assert.equal(citations[0].type, 'web');
227
+ assert.equal(citations[0].url, 'https://example.com/a');
228
+ });
229
+
230
+ test('checkUrlLiveness validates safe URLs and catches dead or invalid links', async () => {
231
+ const invalid = await checkUrlLiveness('not-a-url');
232
+ assert.equal(invalid.alive, false);
233
+
234
+ const empty = await checkUrlLiveness('');
235
+ assert.equal(empty.alive, false);
236
+
237
+ const doc = await checkUrlLiveness('https://example.com/some/article');
238
+ assert.equal(doc.alive, true);
239
+
240
+ const dead = await checkUrlLiveness('https://this-domain-definitely-does-not-exist-at-all-999.xyz/page', { timeoutMs: 500 });
241
+ assert.equal(dead.alive, false);
242
+ });
243
+
244
+ test('decodeBaiduUrl decodes Baidu encrypted objURL and fromURL into valid http urls', () => {
245
+ const encrypted = 'ippr_z2C$qAzdH3FAzdH3Fooo_z&e3Bktstktst_z&e3Bv54AzdH3Fet1j5AzdH3FBV8vv0zm5Ep6';
246
+ const decoded = decodeBaiduUrl(encrypted);
247
+ assert.equal(decoded, 'http://www.bilibili.com/video/BV1cc7z6oEtr');
248
+
249
+ assert.equal(decodeBaiduUrl('https://example.com/pic.jpg'), 'https://example.com/pic.jpg');
250
+ assert.equal(decodeBaiduUrl(''), '');
251
+ assert.equal(decodeBaiduUrl(null), '');
252
+ });
253
+
254
+
255
+
256
+ test('buildImageSearchQuery always targets the artist for image searches', () => {
257
+ assert.match(buildImageSearchQuery('งานแฟนมีตล่าสุด'), /鞠婧祎/);
258
+ assert.match(buildImageSearchQuery('Ju Jingyi red carpet'), /鞠婧祎/);
259
+ const direct = buildImageSearchQuery('鞠婧祎 代言');
260
+ assert.match(direct, /鞠婧祎 代言/);
261
+ });
262
+
263
+ test('titleMentionsArtist separates artist results from keyword collisions', () => {
264
+ assert.equal(titleMentionsArtist('觅橘品牌代言人鞠婧祎'), true);
265
+ assert.equal(titleMentionsArtist('จวีจิ้งอี งานแฟนมีต'), true);
266
+ assert.equal(titleMentionsArtist('Ju Jingyi vogue gala red carpet'), true);
267
+ assert.equal(titleMentionsArtist('romand# red carpet 这支是什么神仙草莓色?'), false);
268
+ assert.equal(titleMentionsArtist('สุดหวานงานแฟนมีต “คิมม่อน”'), false);
269
+ assert.equal(titleMentionsArtist(''), false);
270
+ });
271
+
272
+ test('isLowValueImageUrl and imageHostKey guard junk sources', () => {
273
+ assert.equal(isLowValueImageUrl('data:image/png;base64,AAA'), true);
274
+ assert.equal(isLowValueImageUrl('https://cdn.example.com/icon.svg'), true);
275
+ assert.equal(isLowValueImageUrl('https://cdn.example.com/static/logo.png'), true);
276
+ assert.equal(isLowValueImageUrl('https://img0.baidu.com/it/u=1,2;3?fm=253'), false);
277
+ assert.equal(isLowValueImageUrl(''), true);
278
+
279
+ assert.equal(imageHostKey('https://img0.baidu.com/it/u=1?x=1'), 'img0.baidu.com/it/u=1');
280
+ assert.equal(imageHostKey('not a url'), 'not a url');
281
+ });
@@ -80,8 +80,69 @@ export function sanitizeToolArguments(toolName, rawArgs = {}, toolsSchema = [])
80
80
  return cleanArgs;
81
81
  }
82
82
 
83
+ export const TOOL_NAME_ALIASES = Object.freeze({
84
+ websearch: 'web_search',
85
+ searchweb: 'web_search',
86
+ search_web: 'web_search',
87
+ google_search: 'web_search',
88
+ bing_search: 'web_search',
89
+ webimages: 'search_web_images',
90
+ web_images: 'search_web_images',
91
+ search_images: 'search_web_images',
92
+ search_image: 'search_web_images',
93
+ image_search: 'search_web_images',
94
+ works: 'search_works',
95
+ search_works: 'search_works',
96
+ dramas: 'search_works',
97
+ get_works: 'search_works',
98
+ songs: 'search_discography',
99
+ discography: 'search_discography',
100
+ search_discography: 'search_discography',
101
+ brands: 'get_brand_endorsements',
102
+ get_brand_endorsements: 'get_brand_endorsements',
103
+ news: 'search_news',
104
+ search_news: 'search_news',
105
+ timeline: 'get_career_timeline',
106
+ get_timeline: 'get_career_timeline',
107
+ awards: 'get_awards',
108
+ get_awards: 'get_awards',
109
+ profile: 'get_artist_profile',
110
+ get_profile: 'get_artist_profile',
111
+ gallery: 'get_gallery_photos',
112
+ get_photos: 'get_gallery_photos',
113
+ photos: 'get_gallery_photos',
114
+ });
115
+
116
+ export function resolveCanonicalToolName(rawName) {
117
+ if (!rawName) return null;
118
+ const cleaned = String(rawName).trim().replace(/^call:/i, '').toLowerCase();
119
+ return TOOL_NAME_ALIASES[cleaned] || String(rawName).trim().replace(/^call:/i, '');
120
+ }
121
+
122
+ export function parseCustomToolArgs(rawArgs) {
123
+ if (!rawArgs) return {};
124
+ let clean = String(rawArgs).replace(/<\|["']\|>/g, '"').trim();
125
+ if (clean.startsWith('{') && clean.endsWith('}')) {
126
+ try {
127
+ const parsed = JSON.parse(clean);
128
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
129
+ } catch {}
130
+ }
131
+
132
+ const args = {};
133
+ const kvRegex = /([a-zA-Z0-9_]+)\s*:\s*(?:"([^"]*)"|'([^']*)'|([^,}\s]+))/g;
134
+ for (const m of clean.matchAll(kvRegex)) {
135
+ let key = m[1].trim();
136
+ if (key === 'targetlanguage') key = 'target_language';
137
+ if (key === 'domainfilter') key = 'domain_filter';
138
+ const val = m[2] !== undefined ? m[2] : (m[3] !== undefined ? m[3] : m[4]);
139
+ args[key] = val;
140
+ }
141
+ return args;
142
+ }
143
+
83
144
  /**
84
- * Parse text-emitted tool calls (fenced JSON or ReAct "Action:" blocks).
145
+ * Parse text-emitted tool calls (fenced JSON, ReAct "Action:" blocks, or [TOOLCALL] tags).
85
146
  * Returns { calls, malformed } — malformed counts blocks that LOOK like tool
86
147
  * calls but could not be parsed, so the caller can feed that back to the model
87
148
  * instead of silently treating the message as the final answer.
@@ -92,63 +153,124 @@ export function parseTextToolCallsFromContent(content, toolsSchema) {
92
153
  const allTools = toolsSchema || [];
93
154
  const isValidTool = (name) => allTools.some(t => t.function?.name === name);
94
155
 
95
- const pushCall = (prefix, toolName, args) => {
96
- const cleanArgs = sanitizeToolArguments(toolName, args, allTools);
156
+ const pushCall = (prefix, rawToolName, args) => {
157
+ const canonicalName = resolveCanonicalToolName(rawToolName);
158
+ if (!canonicalName || !isValidTool(canonicalName)) return false;
159
+ const cleanArgs = sanitizeToolArguments(canonicalName, args, allTools);
97
160
  result.calls.push({
98
161
  id: `${prefix}_${Date.now()}_${result.calls.length}`,
99
- function: { name: toolName, arguments: JSON.stringify(cleanArgs) }
162
+ function: { name: canonicalName, arguments: JSON.stringify(cleanArgs) }
100
163
  });
164
+ return true;
101
165
  };
102
166
 
103
- const fenceRegex = /```(?:json)?\s*([\s\S]*?)```/g;
104
- for (const match of content.matchAll(fenceRegex)) {
105
- const body = match[1];
106
- if (!body.includes('{')) continue;
107
- const { objects, unbalanced } = extractTopLevelJsonObjects(body);
108
- if (unbalanced) result.malformed++;
109
- for (const obj of objects) {
110
- let parsed = null;
167
+ // 1. [TOOLCALL] or <tool_call> with toolName{...}
168
+ const inlineToolCallRegex = /(?:\[TOOLCALL\]|<tool_call>|<toolcall>|<\|tool_call\|>)\s*(?:call:)?([a-zA-Z0-9_]+)\s*\{([\s\S]*?)\}\s*(?:<toolcall\|?>|<\/tool_call>|<\/toolcall>|\[\/TOOLCALL\]|$)/gi;
169
+ for (const match of content.matchAll(inlineToolCallRegex)) {
170
+ const toolName = match[1];
171
+ const args = parseCustomToolArgs(match[2]);
172
+ pushCall('call_tc', toolName, args);
173
+ }
174
+
175
+ // 2. <tool_call> or [TOOLCALL] containing JSON object
176
+ if (result.calls.length === 0) {
177
+ const tagJsonRegex = /(?:<tool_call>|\[TOOLCALL\]|<\|tool_call\|>)\s*(\{[\s\S]*?\})\s*(?:<\/tool_call>|<toolcall\|?>|\[\/TOOLCALL\]|$)/gi;
178
+ for (const match of content.matchAll(tagJsonRegex)) {
111
179
  try {
112
- parsed = JSON.parse(obj.raw);
113
- } catch {
114
- parsed = null;
115
- }
116
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
117
- result.malformed++;
118
- continue;
119
- }
120
- const toolName = parsed.action || parsed.tool || parsed.name || parsed.function;
121
- if (!toolName) continue;
122
- let toolArgs = parsed.action_input ?? parsed.parameters ?? parsed.arguments ?? parsed.args ?? {};
123
- if (toolArgs && typeof toolArgs === 'string') {
124
- try { toolArgs = JSON.parse(toolArgs); } catch { toolArgs = { query: toolArgs }; }
125
- }
126
- if (!toolArgs || typeof toolArgs !== 'object' || Array.isArray(toolArgs)) toolArgs = {};
127
- if (isValidTool(String(toolName))) {
180
+ const obj = JSON.parse(match[1].replace(/<\|["']\|>/g, '"'));
181
+ const toolName = obj.name || obj.action || obj.tool || obj.function;
182
+ let args = obj.arguments || obj.parameters || obj.action_input || obj.args || {};
183
+ if (typeof args === 'string') {
184
+ try { args = JSON.parse(args); } catch {}
185
+ }
186
+ if (toolName) pushCall('call_tag_json', toolName, args);
187
+ } catch {}
188
+ }
189
+ }
190
+
191
+ // 3. [TOOL_CALLS] array format
192
+ if (result.calls.length === 0) {
193
+ const arrayRegex = /\[TOOL_CALLS\]\s*(\[[\s\S]*?\])/gi;
194
+ for (const match of content.matchAll(arrayRegex)) {
195
+ try {
196
+ const arr = JSON.parse(match[1].replace(/<\|["']\|>/g, '"'));
197
+ if (Array.isArray(arr)) {
198
+ for (const item of arr) {
199
+ const toolName = item.name || item.function?.name || item.action;
200
+ let args = item.arguments || item.parameters || item.function?.arguments || {};
201
+ if (typeof args === 'string') {
202
+ try { args = JSON.parse(args); } catch {}
203
+ }
204
+ if (toolName) pushCall('call_arr', toolName, args);
205
+ }
206
+ }
207
+ } catch {}
208
+ }
209
+ }
210
+
211
+ // 4. Standalone tool_name{...}<toolcall|>
212
+ if (result.calls.length === 0) {
213
+ const standaloneRegex = /(?:^|\n)(?:call:)?([a-zA-Z0-9_]+)\s*\{([\s\S]*?)\}\s*<toolcall\|?>/gi;
214
+ for (const match of content.matchAll(standaloneRegex)) {
215
+ const toolName = match[1];
216
+ const args = parseCustomToolArgs(match[2]);
217
+ pushCall('call_standalone', toolName, args);
218
+ }
219
+ }
220
+
221
+ // 5. Fenced JSON: ```json ... ```
222
+ if (result.calls.length === 0) {
223
+ const fenceRegex = /```(?:json)?\s*([\s\S]*?)```/g;
224
+ for (const match of content.matchAll(fenceRegex)) {
225
+ const body = match[1];
226
+ if (!body.includes('{')) continue;
227
+ const { objects, unbalanced } = extractTopLevelJsonObjects(body);
228
+ if (unbalanced) result.malformed++;
229
+ for (const obj of objects) {
230
+ let parsed = null;
231
+ try {
232
+ parsed = JSON.parse(obj.raw);
233
+ } catch {
234
+ parsed = null;
235
+ }
236
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
237
+ result.malformed++;
238
+ continue;
239
+ }
240
+ const toolName = parsed.action || parsed.tool || parsed.name || parsed.function;
241
+ if (!toolName) continue;
242
+ let toolArgs = parsed.action_input ?? parsed.parameters ?? parsed.arguments ?? parsed.args ?? {};
243
+ if (toolArgs && typeof toolArgs === 'string') {
244
+ try { toolArgs = JSON.parse(toolArgs); } catch { toolArgs = { query: toolArgs }; }
245
+ }
246
+ if (!toolArgs || typeof toolArgs !== 'object' || Array.isArray(toolArgs)) toolArgs = {};
128
247
  pushCall('call_txt', String(toolName), toolArgs);
129
248
  }
130
249
  }
131
250
  }
132
251
 
133
- const actionRegex = /Action:\s*([a-zA-Z0-9_]+)\s*\nAction Input:\s*([\s\S]*?)(?=\nAction:|\nObservation:|$)/g;
134
- for (const match of content.matchAll(actionRegex)) {
135
- const toolName = match[1].trim();
136
- const rawInput = match[2].trim();
137
- if (!isValidTool(toolName)) continue;
138
- if (rawInput.startsWith('{')) {
139
- const { objects, unbalanced } = extractTopLevelJsonObjects(rawInput);
140
- const candidate = objects.length > 0 ? objects[0].raw : unbalanced;
141
- let parsed = null;
142
- if (candidate) {
143
- try { parsed = JSON.parse(candidate); } catch { parsed = null; }
144
- }
145
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
146
- result.malformed++;
147
- continue;
252
+ // 6. Action: ... \n Action Input: ...
253
+ if (result.calls.length === 0) {
254
+ const actionRegex = /Action:\s*([a-zA-Z0-9_]+)\s*\nAction Input:\s*([\s\S]*?)(?=\nAction:|\nObservation:|$)/g;
255
+ for (const match of content.matchAll(actionRegex)) {
256
+ const toolName = match[1].trim();
257
+ const rawInput = match[2].trim();
258
+ if (!isValidTool(resolveCanonicalToolName(toolName))) continue;
259
+ if (rawInput.startsWith('{')) {
260
+ const { objects, unbalanced } = extractTopLevelJsonObjects(rawInput);
261
+ const candidate = objects.length > 0 ? objects[0].raw : unbalanced;
262
+ let parsed = null;
263
+ if (candidate) {
264
+ try { parsed = JSON.parse(candidate); } catch { parsed = null; }
265
+ }
266
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
267
+ result.malformed++;
268
+ continue;
269
+ }
270
+ pushCall('call_act', toolName, parsed);
271
+ } else {
272
+ pushCall('call_act', toolName, { query: rawInput });
148
273
  }
149
- pushCall('call_act', toolName, parsed);
150
- } else {
151
- pushCall('call_act', toolName, { query: rawInput });
152
274
  }
153
275
  }
154
276
 
@@ -186,5 +308,10 @@ export function stripToolCallBlocks(content) {
186
308
  let cleaned = String(content || '');
187
309
  cleaned = cleaned.replace(/```(?:json)?\s*[\s\S]*?```/g, '');
188
310
  cleaned = cleaned.replace(/Action:\s*[a-zA-Z0-9_]+\s*\nAction Input:[\s\S]*?(?=\nAction:|\nObservation:|$)/g, '');
311
+ cleaned = cleaned.replace(/(?:\[TOOLCALL\]|<tool_call>|<toolcall>|<\|tool_call\|>)\s*(?:call:)?[a-zA-Z0-9_]+\s*\{[\s\S]*?\}\s*(?:<toolcall\|?>|<\/tool_call>|<\/toolcall>|\[\/TOOLCALL\]|$)/gi, '');
312
+ cleaned = cleaned.replace(/(?:<tool_call>|\[TOOLCALL\]|<\|tool_call\|>)\s*\{[\s\S]*?\}\s*(?:<\/tool_call>|<toolcall\|?>|\[\/TOOLCALL\]|$)/gi, '');
313
+ cleaned = cleaned.replace(/\[TOOL_CALLS\]\s*\[[\s\S]*?\]/gi, '');
314
+ cleaned = cleaned.replace(/(?:^|\n)(?:call:)?[a-zA-Z0-9_]+\s*\{[\s\S]*?\}\s*<toolcall\|?>/gi, '');
315
+ cleaned = cleaned.replace(/<\|?toolcall\|?>|<\/?tool_call>|<\/?toolcall>|\[\/?TOOLCALL\]|\[TOOL_CALLS\]|<\|["']\|>/gi, '');
189
316
  return cleaned.trim();
190
317
  }