kiku-agent 1.3.2 → 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.
- package/README.md +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { getWebChatHtml } from '../../webChatHtml.js';
|
|
4
5
|
import { executeDirectCatalogKnowledge, makeInspectableToolPayload } from '../../webChatServer.js';
|
|
5
6
|
|
|
@@ -12,9 +13,9 @@ test('web chat tool payloads redact secrets and bound large values', () => {
|
|
|
12
13
|
}, { maxArrayItems: 2 });
|
|
13
14
|
|
|
14
15
|
assert.equal(payload.query, 'In Blossom');
|
|
15
|
-
assert.equal(payload.apiKey, '
|
|
16
|
-
assert.equal(payload.nested.token, '
|
|
17
|
-
assert.deepEqual(payload.values, [0, 1, '…
|
|
16
|
+
assert.equal(payload.apiKey, '···');
|
|
17
|
+
assert.equal(payload.nested.token, '···');
|
|
18
|
+
assert.deepEqual(payload.values, [0, 1, '… ย่ออีก 3 รายการ']);
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
test('direct catalog fallback retains inspectable tool input and result', async () => {
|
|
@@ -27,6 +28,169 @@ test('direct catalog fallback retains inspectable tool input and result', async
|
|
|
27
28
|
assert.ok(Number.isFinite(tool.durationMs));
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
test('agent bare URLs render as clickable _blank links without double-wrapping', () => {
|
|
32
|
+
const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
|
|
33
|
+
const start = src.indexOf('function linkifyBareUrls(segment)');
|
|
34
|
+
assert.ok(start >= 0);
|
|
35
|
+
let depth = 0;
|
|
36
|
+
let end = -1;
|
|
37
|
+
for (let i = src.indexOf('{', start); i < src.length; i++) {
|
|
38
|
+
if (src[i] === '{') depth++;
|
|
39
|
+
else if (src[i] === '}') {
|
|
40
|
+
depth--;
|
|
41
|
+
if (depth === 0) {
|
|
42
|
+
end = i + 1;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
assert.ok(end > start);
|
|
48
|
+
const escapeHtml = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
49
|
+
const linkifyBareUrls = new Function('escapeHtml', src.slice(start, end) + '; return linkifyBareUrls;')(escapeHtml);
|
|
50
|
+
|
|
51
|
+
const linked = linkifyBareUrls('ชมได้ที่ https://www.youtube.com/watch?v=OMLpNyp8T7Q จบ.');
|
|
52
|
+
assert.match(linked, /<a href="https:\/\/www\.youtube\.com\/watch\?v=OMLpNyp8T7Q" target="_blank" rel="noopener noreferrer" class="md-link">/);
|
|
53
|
+
|
|
54
|
+
const intact = linkifyBareUrls('<a href="https://a.test">https://a.test</a>');
|
|
55
|
+
assert.equal((intact.match(/<a /g) || []).length, 1);
|
|
56
|
+
|
|
57
|
+
assert.equal(linkifyBareUrls('no link here'), 'no link here');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('web chat loads KaTeX assets for math typesetting', () => {
|
|
61
|
+
const html = getWebChatHtml();
|
|
62
|
+
assert.match(html, /katex@0\.16\.11\/dist\/katex\.min\.css/);
|
|
63
|
+
assert.match(html, /katex@0\.16\.11\/dist\/katex\.min\.js/);
|
|
64
|
+
assert.match(html, /contrib\/auto-render\.min\.js/);
|
|
65
|
+
assert.match(html, /function typesetMathInBubble\(/);
|
|
66
|
+
assert.match(html, /renderMathInElement/);
|
|
67
|
+
assert.match(html, /KIKU_CLIENT_BUILD/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('math segments are shielded from markdown and restored as KaTeX targets', () => {
|
|
71
|
+
const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
|
|
72
|
+
const loadFn = (name) => {
|
|
73
|
+
const start = src.indexOf('function ' + name + '(');
|
|
74
|
+
assert.ok(start >= 0, name + ' must exist');
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let end = -1;
|
|
77
|
+
for (let i = src.indexOf('{', start); i < src.length; i++) {
|
|
78
|
+
if (src[i] === '{') depth++;
|
|
79
|
+
else if (src[i] === '}') {
|
|
80
|
+
depth--;
|
|
81
|
+
if (depth === 0) {
|
|
82
|
+
end = i + 1;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
assert.ok(end > start);
|
|
88
|
+
return new Function(src.slice(start, end) + '; return ' + name + ';')();
|
|
89
|
+
};
|
|
90
|
+
const extractMathSegments = loadFn('extractMathSegments');
|
|
91
|
+
const restoreMathSegments = loadFn('restoreMathSegments');
|
|
92
|
+
|
|
93
|
+
const store = [];
|
|
94
|
+
const shielded = extractMathSegments('โจทย์ $\\lim_{x \\to 0} a * b$ และ $$x^2$$ จบ ส่วน `$y$` ในโค้ดไม่แตะ', store);
|
|
95
|
+
assert.equal(store.length, 2);
|
|
96
|
+
assert.ok(shielded.includes('§§§MATH0§§§'));
|
|
97
|
+
assert.ok(shielded.includes('§§§MATH1§§§'));
|
|
98
|
+
assert.ok(shielded.includes('`$y$`'), 'math inside inline code must stay untouched');
|
|
99
|
+
assert.ok(!shielded.includes('\\lim'), 'raw math must not leak into markdown processing');
|
|
100
|
+
|
|
101
|
+
const restored = restoreMathSegments(shielded, store);
|
|
102
|
+
assert.match(restored, /<span class="math-inline">\$\\lim/);
|
|
103
|
+
assert.match(restored, /<div class="math-display">\$\$x\^2\$\$<\/div>/);
|
|
104
|
+
|
|
105
|
+
assert.equal(extractMathSegments('ราคา $100 บาท', []), 'ราคา $100 บาท');
|
|
106
|
+
assert.equal(extractMathSegments('no math here', []).includes('§§§MATH'), false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('latexToUnicode fallback covers calculus symbols without KaTeX', () => {
|
|
110
|
+
const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
|
|
111
|
+
const loadBlock = (prefix) => {
|
|
112
|
+
const start = src.indexOf(prefix);
|
|
113
|
+
assert.ok(start >= 0, prefix + ' must exist');
|
|
114
|
+
let depth = 0;
|
|
115
|
+
let end = -1;
|
|
116
|
+
for (let i = src.indexOf('{', start); i < src.length; i++) {
|
|
117
|
+
if (src[i] === '{') depth++;
|
|
118
|
+
else if (src[i] === '}') {
|
|
119
|
+
depth--;
|
|
120
|
+
if (depth === 0) {
|
|
121
|
+
end = i + 1;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
assert.ok(end > start);
|
|
127
|
+
return src.slice(start, end);
|
|
128
|
+
};
|
|
129
|
+
const sandbox = loadBlock('const LATEX_SYMBOL_MAP =') + '\n' +
|
|
130
|
+
loadBlock('const LATEX_SUP_MAP =') + '\n' +
|
|
131
|
+
loadBlock('const LATEX_SUB_MAP =') + '\n' +
|
|
132
|
+
src.slice(src.indexOf('function latexReadBrace('), src.indexOf('function latexMapChars(')) + '\n' +
|
|
133
|
+
src.slice(src.indexOf('function latexMapChars('), src.indexOf('function latexToUnicode(')) + '\n' +
|
|
134
|
+
src.slice(src.indexOf('function latexToUnicode('), src.indexOf('window.latexToUnicode')) +
|
|
135
|
+
'\nreturn latexToUnicode;';
|
|
136
|
+
const latexToUnicode = new Function(sandbox)();
|
|
137
|
+
assert.equal(latexToUnicode('\\frac{1}{2}'), '(1)/(2)');
|
|
138
|
+
assert.equal(latexToUnicode('L = \\frac{1}{\\sqrt{e}}'), 'L = (1)/(√(e))');
|
|
139
|
+
assert.equal(latexToUnicode('1^\\infty'), '1^∞');
|
|
140
|
+
assert.equal(latexToUnicode('\\lim_{x \\to 0} \\cos x'), 'lim_(x → 0) cos x');
|
|
141
|
+
assert.equal(latexToUnicode('x_0 + x_{10}'), 'x₀ + x₁₀');
|
|
142
|
+
assert.match(readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8'), /fallbackTypesetMath\(bubble\)/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('latexToUnicode fallback covers advanced math: matrices, accents, aliases', () => {
|
|
146
|
+
const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
|
|
147
|
+
const sandbox = src.slice(src.indexOf('const LATEX_SYMBOL_MAP ='), src.indexOf('window.latexToUnicode')) +
|
|
148
|
+
'\nreturn latexToUnicode;';
|
|
149
|
+
const latexToUnicode = new Function(sandbox)();
|
|
150
|
+
assert.equal(latexToUnicode('\\begin{matrix}a&b\\\\c&d\\end{matrix}'), 'a b c d');
|
|
151
|
+
assert.equal(latexToUnicode('\\binom{n}{k}'), '(n choose k)');
|
|
152
|
+
assert.equal(latexToUnicode('\\hat{\\theta}+\\vec{v}+\\bar{z}+\\dot{x}'), 'θ+v+z+x');
|
|
153
|
+
assert.equal(latexToUnicode('x\\le 0, y\\ge 1, a\\ne b'), 'x≤ 0, y≥ 1, a≠ b');
|
|
154
|
+
assert.equal(latexToUnicode('\\tag{1} x=1'), '(1) x=1');
|
|
155
|
+
assert.equal(latexToUnicode('\\sqrt{\\frac{1}{2}}'), '√((1)/(2))');
|
|
156
|
+
assert.equal(latexToUnicode('\\dfrac{a}{b}'), '(a)/(b)');
|
|
157
|
+
assert.equal(latexToUnicode('\\boxed{42} + \\cancel{x}'), '[42] + x');
|
|
158
|
+
assert.equal(latexToUnicode('\\text{อนันต์} + \\mathrm{d}x'), 'อนันต์ + dx');
|
|
159
|
+
assert.equal(latexToUnicode('x \\in \\mathbb{R}^n'), 'x ∈ ℝⁿ');
|
|
160
|
+
const noRaw = (s) => !/[\\][a-zA-Z]+/.test(s) && !/[{}]/.test(s);
|
|
161
|
+
for (const tex of [
|
|
162
|
+
'\\sum_{n=1}^{\\infty} n + \\prod_{i=1}^n i + \\int_0^1 x\\,dx',
|
|
163
|
+
'\\forall x \\exists y: x \\subset y \\cup z \\approx w',
|
|
164
|
+
'\\alpha\\beta\\gamma\\Delta\\Omega + \\clubsuit\\sharp\\flat',
|
|
165
|
+
'\\left( \\frac{a}{b} \\right) + \\big| x \\big| + A \\overset{!}{=} B',
|
|
166
|
+
]) {
|
|
167
|
+
assert.ok(noRaw(latexToUnicode(tex)), 'raw latex left in: ' + latexToUnicode(tex));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('math extractor shields bare display environments and auto-render knows them', () => {
|
|
172
|
+
const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
|
|
173
|
+
const loadFn = (name) => {
|
|
174
|
+
const start = src.indexOf('function ' + name + '(');
|
|
175
|
+
assert.ok(start >= 0, name + ' must exist');
|
|
176
|
+
const endMarker = name === 'extractMathSegments' ? 'function restoreMathSegments(' : 'const LATEX_SYMBOL_MAP =';
|
|
177
|
+
return new Function(src.slice(start, src.indexOf(endMarker)) + '; return ' + name + ';')();
|
|
178
|
+
};
|
|
179
|
+
const extractMathSegments = loadFn('extractMathSegments');
|
|
180
|
+
const restoreMathSegments = loadFn('restoreMathSegments');
|
|
181
|
+
const store = [];
|
|
182
|
+
const shielded = extractMathSegments('ผลคือ\n\\begin{align}x &= 1 \\\\ y &= 2\\end{align}\nจบ', store);
|
|
183
|
+
assert.equal(store.length, 1);
|
|
184
|
+
assert.equal(store[0].display, true);
|
|
185
|
+
assert.ok(shielded.includes('§§§MATH0§§§'));
|
|
186
|
+
const restored = restoreMathSegments(shielded, store);
|
|
187
|
+
assert.match(restored, /<div class="math-display">\\begin\{align\}/);
|
|
188
|
+
const html = getWebChatHtml();
|
|
189
|
+
for (const env of ['equation', 'align', 'gather', 'multline']) {
|
|
190
|
+
assert.ok(html.includes('\\\\begin{' + env + '}'), 'auto-render must know ' + env);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
30
194
|
test('web chat HTML isolates answer text and renders expandable tool details', () => {
|
|
31
195
|
const html = getWebChatHtml();
|
|
32
196
|
|
|
@@ -106,6 +270,8 @@ test('renderFollowUpSuggestions creates intelligent contextual follow-up topic c
|
|
|
106
270
|
const html = getWebChatHtml();
|
|
107
271
|
assert.match(html, /function renderFollowUpSuggestions\(botGroup, answerText, userQuery, serverSuggestions\)/);
|
|
108
272
|
assert.match(html, /follow-up-suggestions/);
|
|
273
|
+
assert.match(html, /typesetMathInBubble\(chip\)/, 'renderFollowUpSuggestions must typeset math in chips');
|
|
274
|
+
assert.match(html, /detectSpecializedOpenDomainFollowUps/, 'client must have specialized open-domain follow-up detector');
|
|
109
275
|
assert.match(html, /💎 บทบาท Versace/);
|
|
110
276
|
assert.match(html, /👗 แบรนด์อื่นๆ ทั้งหมด/);
|
|
111
277
|
assert.match(html, /🌸 ตัวตนกับรูปลักษณ์/);
|
|
@@ -230,11 +396,19 @@ test('user bubble renders dedicated renderUserContent with high contrast white t
|
|
|
230
396
|
assert.match(html, /color:\s*#ffffff\s*!important/);
|
|
231
397
|
});
|
|
232
398
|
|
|
399
|
+
test('user bubble renders math formulas with typesetMathInBubble and dedicated KaTeX styles', () => {
|
|
400
|
+
const html = getWebChatHtml();
|
|
401
|
+
assert.match(html, /renderUserContent\(bubble,\s*text,\s*details\s*=\s*\{\}\)\s*\{[\s\S]*?typesetMathInBubble\(bubble\)/);
|
|
402
|
+
assert.match(html, /\.user-content-body\s*\.katex\s*,\s*\.bubble-user\s*\.katex/);
|
|
403
|
+
assert.match(html, /\.bubble-user\s*\.math-inline/);
|
|
404
|
+
assert.match(html, /\.bubble-user\s*\.math-display/);
|
|
405
|
+
});
|
|
406
|
+
|
|
233
407
|
test('clarifying questions and interactive question cards are stably pinned without stream flickering', () => {
|
|
234
408
|
const html = getWebChatHtml();
|
|
235
409
|
assert.match(html, /\.clarify-slot/);
|
|
236
410
|
assert.match(html, /clarifySlot\.className = 'clarify-slot'/);
|
|
237
|
-
assert.match(html, /msgGroup\.insertBefore\(clarifySlot,\s*botBubble
|
|
411
|
+
assert.match(html, /msgGroup\.insertBefore\(clarifySlot,\s*botBubble(?:\.nextSibling)?\)/);
|
|
238
412
|
assert.match(html, /dismissAgentInteractiveQuestion/);
|
|
239
413
|
assert.match(html, /dismissedAgentQuestions/);
|
|
240
414
|
assert.match(html, /!details\.isStreaming/);
|
|
@@ -253,7 +427,7 @@ test('subagent progress renders a dedicated live card with evidence report', ()
|
|
|
253
427
|
assert.match(html, /subagent-report/);
|
|
254
428
|
assert.match(html, /\.subagent-card\s*\{/);
|
|
255
429
|
assert.match(html, /subagent-pulse/);
|
|
256
|
-
assert.match(html, /run_deep_research: '
|
|
430
|
+
assert.match(html, /run_deep_research: '▸/);
|
|
257
431
|
});
|
|
258
432
|
|
|
259
433
|
test('error handling renders dedicated retry error card and interactive retry badge', () => {
|
|
@@ -298,3 +472,80 @@ test('input box enhancements: slash commands menu, model chip, relocated attachm
|
|
|
298
472
|
assert.match(html, /updateSendButtonState/);
|
|
299
473
|
assert.match(html, /drag-over/);
|
|
300
474
|
});
|
|
475
|
+
|
|
476
|
+
test('file upload system: attachments are validated, sent to /api/chat, and rendered with lightbox preview', () => {
|
|
477
|
+
const html = getWebChatHtml();
|
|
478
|
+
assert.match(html, /function safeRasterDataUrl\(/);
|
|
479
|
+
assert.match(html, /function addAttachmentFile\(/);
|
|
480
|
+
assert.match(html, /function renderAttachmentTray\(/);
|
|
481
|
+
assert.match(html, /function handleImageUpload\(/);
|
|
482
|
+
assert.match(html, /chat-attached-images-wrap/);
|
|
483
|
+
assert.match(html, /chat-attached-image/);
|
|
484
|
+
assert.match(html, /openImageLightbox/);
|
|
485
|
+
assert.match(html, /attachments:\s*attachmentsToSend/);
|
|
486
|
+
assert.match(html, /svg\+xml/);
|
|
487
|
+
assert.match(html, /avif/);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('welcome hero gallery image picker and drag-and-drop system exist and are wired', () => {
|
|
491
|
+
const html = getWebChatHtml();
|
|
492
|
+
assert.match(html, /id="gallery-picker-modal"/);
|
|
493
|
+
assert.match(html, /id="gallery-picker-grid"/);
|
|
494
|
+
assert.match(html, /id="gallery-picker-search"/);
|
|
495
|
+
assert.match(html, /data-hero-slot="primary"/);
|
|
496
|
+
assert.match(html, /data-hero-slot="secondary"/);
|
|
497
|
+
assert.match(html, /data-hero-slot="poster"/);
|
|
498
|
+
assert.match(html, /function openHeroGalleryPicker\(/);
|
|
499
|
+
assert.match(html, /function closeHeroGalleryPicker\(/);
|
|
500
|
+
assert.match(html, /function filterGalleryPickerCategory\(/);
|
|
501
|
+
assert.match(html, /function handleGallerySearchInput\(/);
|
|
502
|
+
assert.match(html, /function applyHeroImage\(/);
|
|
503
|
+
assert.match(html, /function resetHeroImages\(/);
|
|
504
|
+
assert.match(html, /function scoreGalleryItem\(/);
|
|
505
|
+
assert.match(html, /function initWelcomeHeroGallery\(/);
|
|
506
|
+
assert.match(html, /kiku_hero_images/);
|
|
507
|
+
assert.match(html, /application\/x-kiku-hero-img/);
|
|
508
|
+
assert.match(html, /\.gallery-picker-dialog/);
|
|
509
|
+
assert.match(html, /\.gallery-picker-grid/);
|
|
510
|
+
assert.match(html, /\.hero-media-frame \.hero-slot-action/);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test('all gallery images exist on disk and staticRoute serves them without 404', async () => {
|
|
514
|
+
const { handleApiRequest } = await import('../../apiRouter.js');
|
|
515
|
+
const { handleStaticRoute } = await import('../../web-chat/routes/staticRoute.js');
|
|
516
|
+
const stream = await import('node:stream');
|
|
517
|
+
|
|
518
|
+
let body = '';
|
|
519
|
+
const req = {
|
|
520
|
+
method: 'GET',
|
|
521
|
+
url: '/api/v1/gallery?scope=all&limit=200',
|
|
522
|
+
headers: { host: 'localhost:3000' }
|
|
523
|
+
};
|
|
524
|
+
const res = {
|
|
525
|
+
writeHead: () => {},
|
|
526
|
+
end: (chunk) => { if (chunk) body += chunk; },
|
|
527
|
+
setHeader: () => {}
|
|
528
|
+
};
|
|
529
|
+
await handleApiRequest(req, res);
|
|
530
|
+
const json = JSON.parse(body);
|
|
531
|
+
assert.ok(Array.isArray(json.data) && json.data.length >= 70);
|
|
532
|
+
|
|
533
|
+
for (const item of json.data) {
|
|
534
|
+
const encodedUrl = encodeURI(item.image);
|
|
535
|
+
const mockReq = { url: encodedUrl };
|
|
536
|
+
let handledCode = 0;
|
|
537
|
+
const mockRes = new stream.Writable({
|
|
538
|
+
write(chunk, encoding, callback) { callback(); }
|
|
539
|
+
});
|
|
540
|
+
mockRes.writeHead = (code) => { handledCode = code; };
|
|
541
|
+
const u = new URL(mockReq.url, 'http://localhost:3002');
|
|
542
|
+
const handled = handleStaticRoute(mockReq, mockRes, u.pathname, 3002);
|
|
543
|
+
assert.strictEqual(handled, true, `Route must handle image ${item.image}`);
|
|
544
|
+
assert.strictEqual(handledCode, 200, `Image must return 200 OK for ${item.image}`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const html = getWebChatHtml();
|
|
548
|
+
assert.match(html, /onerror="this\.onerror=null;const it=this\.closest/);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { handleWebChatRequest } from '../../web-chat/router.js';
|
|
5
|
+
|
|
6
|
+
test('Web Chat MCP: GET /mcp returns protocol discovery JSON', async () => {
|
|
7
|
+
const server = http.createServer(async (req, res) => {
|
|
8
|
+
await handleWebChatRequest(req, res, { port: 3002 });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
12
|
+
const port = server.address().port;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`http://127.0.0.1:${port}/mcp`);
|
|
16
|
+
assert.equal(res.status, 200);
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
assert.equal(data.protocol, 'mcp');
|
|
19
|
+
assert.equal(data.name, 'kiku');
|
|
20
|
+
assert.ok(data.capabilities?.toolsCount > 0);
|
|
21
|
+
} finally {
|
|
22
|
+
server.close();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('Web Chat MCP: GET /api/mcp/status returns tools ecosystem and configs', async () => {
|
|
27
|
+
const server = http.createServer(async (req, res) => {
|
|
28
|
+
await handleWebChatRequest(req, res, { port: 3002 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
32
|
+
const port = server.address().port;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/mcp/status`);
|
|
36
|
+
assert.equal(res.status, 200);
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
assert.equal(data.status, 'ok');
|
|
39
|
+
assert.equal(data.mcpServer?.protocolVersion, '2025-11-25');
|
|
40
|
+
assert.ok(data.mcpServer?.supportedProtocolVersions?.includes('2024-11-05'));
|
|
41
|
+
assert.ok(data.mcpServer?.toolsCount >= 60);
|
|
42
|
+
assert.ok(data.totalTools >= 68);
|
|
43
|
+
assert.ok(data.categories?.ALL >= 68);
|
|
44
|
+
assert.ok(data.categories?.CATALOG > 0);
|
|
45
|
+
assert.ok(data.categories?.RESEARCH > 0);
|
|
46
|
+
assert.ok(Array.isArray(data.tools));
|
|
47
|
+
assert.ok(data.tools.length >= 68);
|
|
48
|
+
assert.ok(data.mcpServer?.clientConfigs?.claudeDesktop);
|
|
49
|
+
assert.ok(data.mcpServer?.clientConfigs?.cursor);
|
|
50
|
+
assert.ok(data.mcpServer?.clientConfigs?.windsurf);
|
|
51
|
+
assert.ok(data.mcpServer?.clientConfigs?.vscode);
|
|
52
|
+
assert.ok(data.mcpServer?.clientConfigs?.antigravity);
|
|
53
|
+
assert.ok(data.mcpServer?.clientConfigs?.cline);
|
|
54
|
+
assert.ok(data.mcpServer?.clientConfigs?.claudeCode);
|
|
55
|
+
assert.ok(data.mcpServer?.clientConfigs?.geminiCli);
|
|
56
|
+
assert.ok(data.mcpServer?.clientConfigs?.librechat);
|
|
57
|
+
assert.ok(data.mcpServer?.clientConfigs?.opencode);
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
assert.ok(data.mcpServer.clientConfigs.antigravity.http?.mcpServers?.kiku?.url);
|
|
61
|
+
assert.ok(data.mcpServer.clientConfigs.antigravity.stdio?.mcpServers?.kiku?.command);
|
|
62
|
+
assert.ok(data.mcpServer.clientConfigs.vscode.http?.servers?.kiku?.url);
|
|
63
|
+
assert.ok(data.mcpServer.clientConfigs.librechat.httpYaml?.includes('streamable-http'));
|
|
64
|
+
assert.ok(data.mcpServer.clientConfigs.opencode.http?.$schema);
|
|
65
|
+
|
|
66
|
+
const profileTool = data.tools.find(t => t.name === 'get_profile_and_fandom');
|
|
67
|
+
assert.ok(profileTool);
|
|
68
|
+
assert.equal(profileTool.category, 'CATALOG');
|
|
69
|
+
assert.equal(profileTool.isMcpExposed, true);
|
|
70
|
+
} finally {
|
|
71
|
+
server.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('Web Chat MCP: GET /api/status includes mcp server capability', async () => {
|
|
76
|
+
const server = http.createServer(async (req, res) => {
|
|
77
|
+
await handleWebChatRequest(req, res, { port: 3002 });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
81
|
+
const port = server.address().port;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/status?light=1`);
|
|
85
|
+
assert.equal(res.status, 200);
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
assert.equal(data.status, 'ok');
|
|
88
|
+
assert.ok(data.mcp);
|
|
89
|
+
assert.equal(data.mcp.endpoint, '/mcp');
|
|
90
|
+
assert.equal(data.mcp.online, true);
|
|
91
|
+
} finally {
|
|
92
|
+
server.close();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
tokenizeForMatch,
|
|
17
17
|
truncateByWords,
|
|
18
18
|
} from '../websearch/extract.js';
|
|
19
|
+
import { buildPageResult } from '../webSearch.js';
|
|
19
20
|
|
|
20
21
|
const ARTICLE_FIXTURE = `<!DOCTYPE html>
|
|
21
22
|
<html lang="th">
|
|
@@ -160,6 +161,28 @@ test('extractMainContent flags thin single-page-app shells', async () => {
|
|
|
160
161
|
assert.ok(result.quality.htmlChars > 15_000);
|
|
161
162
|
});
|
|
162
163
|
|
|
164
|
+
test('buildPageResult reports login-walled/JS-shell pages as actionable errors, not hollow success', () => {
|
|
165
|
+
const empty = buildPageResult(
|
|
166
|
+
{ markdown: ' ', meta: {}, engine: 'cheerio-readability', quality: { isThin: true, textChars: 0 } },
|
|
167
|
+
{ finalUrl: 'https://weibo.com/u/3669102477', maxLength: 8000, query: '', cached: false }
|
|
168
|
+
);
|
|
169
|
+
assert.equal(empty.status, 'error');
|
|
170
|
+
assert.equal(empty.contentLength, 0);
|
|
171
|
+
assert.equal(empty.extraction.mode, 'no_content');
|
|
172
|
+
assert.match(empty.message, /m\.weibo\.cn/);
|
|
173
|
+
assert.match(empty.message, /search_chinese_web/);
|
|
174
|
+
assert.match(empty.message, /search_news/);
|
|
175
|
+
assert.match(empty.message, /Do NOT present this as the final answer/);
|
|
176
|
+
assert.ok(!('content' in empty) || empty.content === undefined, 'no dead-end sentence for the model to parrot');
|
|
177
|
+
|
|
178
|
+
const ok = buildPageResult(
|
|
179
|
+
{ markdown: 'Hello world', meta: {}, engine: 'cheerio-readability', quality: { isThin: false } },
|
|
180
|
+
{ finalUrl: 'https://example.com/a', maxLength: 8000, query: '', cached: false }
|
|
181
|
+
);
|
|
182
|
+
assert.equal(ok.status, 'success');
|
|
183
|
+
assert.equal(ok.content, 'Hello world');
|
|
184
|
+
});
|
|
185
|
+
|
|
163
186
|
test('splitIntoPassages bounds sections by headings and size', () => {
|
|
164
187
|
const markdown = [
|
|
165
188
|
'## Section one',
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
detectLanguage,
|
|
9
9
|
checkUrlLiveness,
|
|
10
10
|
decodeBaiduUrl,
|
|
11
|
+
buildImageSearchQuery,
|
|
12
|
+
titleMentionsArtist,
|
|
13
|
+
isLowValueImageUrl,
|
|
14
|
+
imageHostKey,
|
|
11
15
|
} from '../webSearch.js';
|
|
12
16
|
import { executeTool, prepareToolArguments, toolHandlers, toolsSchema } from '../tools.js';
|
|
13
17
|
import { dynamicToolRouter as toolRouter } from '../toolRouter.js';
|
|
@@ -151,24 +155,27 @@ test('search_encyclopedia requires a term and rejects unknown languages', async
|
|
|
151
155
|
assert.match(badLang.message || JSON.stringify(badLang), /language/i);
|
|
152
156
|
});
|
|
153
157
|
|
|
154
|
-
test('chinese web tools are
|
|
158
|
+
test('chinese web tools are enabled by default and KIKU_ENABLE_CN_WEB_TOOLS=false gates them', async () => {
|
|
155
159
|
const previous = process.env.KIKU_ENABLE_CN_WEB_TOOLS;
|
|
156
160
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
[
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
process.env.KIKU_ENABLE_CN_WEB_TOOLS
|
|
175
|
+
delete process.env.KIKU_ENABLE_CN_WEB_TOOLS;
|
|
170
176
|
const missingQuery = await toolHandlers.search_chinese_web({});
|
|
171
177
|
assert.equal(missingQuery.status, 'error');
|
|
178
|
+
assert.equal(missingQuery.gated, undefined);
|
|
172
179
|
assert.match(missingQuery.message, /query parameter is required/);
|
|
173
180
|
const missingKeyword = await toolHandlers.search_bilibili({});
|
|
174
181
|
assert.equal(missingKeyword.status, 'error');
|
|
@@ -245,3 +252,30 @@ test('decodeBaiduUrl decodes Baidu encrypted objURL and fromURL into valid http
|
|
|
245
252
|
});
|
|
246
253
|
|
|
247
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
|
+
});
|