kiku-agent 1.3.0 → 1.3.2
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 +7 -7
- package/package.json +4 -7
- package/server/agent/agentCore.js +2 -2
- package/server/agent/catalogEntityResolver.js +8 -0
- package/server/agent/cli.js +4 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +298 -0
- package/server/agent/eval/modules/evaluation/scoring.js +351 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4796 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -0
- package/server/agent/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/prompts.js +1 -1
- package/server/agent/telemetry/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +127 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +251 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +594 -0
- package/server/agent/tests/cliAutomation.test.js +121 -0
- package/server/agent/tests/cloudProviders.test.js +234 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +417 -0
- package/server/agent/tests/contextIntelligence.test.js +162 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +215 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +312 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +192 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +319 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/modelManager.test.js +109 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +99 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/providerPersistence.test.js +39 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/securityAuditRemediation.test.js +214 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +232 -0
- package/server/agent/tests/sidebarControls.test.js +101 -0
- package/server/agent/tests/statefulMemory.test.js +270 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +126 -0
- package/server/agent/tests/textToolParsing.test.js +139 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +152 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +520 -0
- package/server/agent/tests/webChatInspection.test.js +300 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +261 -0
- package/server/agent/tests/webSources.test.js +247 -0
- package/server/agent/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/apiRouter.js +7 -11
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/tests/connection.test.js +156 -0
- package/server/routes/agent.js +55 -0
- package/server/supabase_schema.sql +63 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildBilibiliEmbedUrl,
|
|
9
|
+
buildBilibiliIframeHtml,
|
|
10
|
+
normalizeBilibiliIframeSrc,
|
|
11
|
+
normalizeBilibiliPic,
|
|
12
|
+
fetchBilibiliVideoInfo,
|
|
13
|
+
isBrokenBilibiliIframeSrc,
|
|
14
|
+
parseBilibiliViewPayload,
|
|
15
|
+
parseBilibiliSearchPayload,
|
|
16
|
+
} from '../webSearch.js';
|
|
17
|
+
|
|
18
|
+
const BROKEN_SRC = '//://bilibili.com';
|
|
19
|
+
const SAMPLE_BVID = 'BV1xx411c7mD';
|
|
20
|
+
|
|
21
|
+
test('broken src "//://bilibili.com" is detected as invalid', () => {
|
|
22
|
+
assert.equal(isBrokenBilibiliIframeSrc(BROKEN_SRC), true);
|
|
23
|
+
assert.equal(isBrokenBilibiliIframeSrc(''), true);
|
|
24
|
+
assert.equal(isBrokenBilibiliIframeSrc('//player.bilibili.com/player.html'), true);
|
|
25
|
+
assert.equal(isBrokenBilibiliIframeSrc('https://www.bilibili.com/video/' + SAMPLE_BVID), true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('builder produces the canonical player URL (never the broken form)', () => {
|
|
29
|
+
const url = buildBilibiliEmbedUrl(SAMPLE_BVID);
|
|
30
|
+
assert.ok(url.startsWith('https://player.bilibili.com/player.html?bvid='));
|
|
31
|
+
assert.ok(url.includes(`bvid=${SAMPLE_BVID}`));
|
|
32
|
+
assert.ok(!url.includes('//://'));
|
|
33
|
+
|
|
34
|
+
const parsed = new URL(url);
|
|
35
|
+
assert.equal(parsed.hostname, 'player.bilibili.com');
|
|
36
|
+
assert.equal(parsed.searchParams.get('bvid'), SAMPLE_BVID);
|
|
37
|
+
|
|
38
|
+
assert.equal(buildBilibiliEmbedUrl('not-a-bvid'), '');
|
|
39
|
+
assert.equal(buildBilibiliEmbedUrl(''), '');
|
|
40
|
+
assert.ok(buildBilibiliEmbedUrl(SAMPLE_BVID, { autoplay: true }).includes('autoplay=1'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('normalizer repairs the reported broken snippet given a bvid', () => {
|
|
44
|
+
const fixed = normalizeBilibiliIframeSrc(BROKEN_SRC, SAMPLE_BVID);
|
|
45
|
+
assert.equal(fixed, buildBilibiliEmbedUrl(SAMPLE_BVID));
|
|
46
|
+
assert.ok(!fixed.includes('//://'));
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const legacy = '//player.bilibili.com/player.html?bvid=' + SAMPLE_BVID + '&page=1';
|
|
50
|
+
assert.equal(normalizeBilibiliIframeSrc(legacy), buildBilibiliEmbedUrl(SAMPLE_BVID));
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
assert.equal(normalizeBilibiliIframeSrc(BROKEN_SRC), '');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('builder preserves aid/cid/page (official embed params)', () => {
|
|
57
|
+
const url = buildBilibiliEmbedUrl('BV1PK4y1b7dt', { aid: '883409884', cid: '201056987', page: 1 });
|
|
58
|
+
const parsed = new URL(url);
|
|
59
|
+
assert.equal(parsed.hostname, 'player.bilibili.com');
|
|
60
|
+
assert.equal(parsed.searchParams.get('bvid'), 'BV1PK4y1b7dt');
|
|
61
|
+
assert.equal(parsed.searchParams.get('aid'), '883409884');
|
|
62
|
+
assert.equal(parsed.searchParams.get('cid'), '201056987');
|
|
63
|
+
assert.equal(parsed.searchParams.get('page'), '1');
|
|
64
|
+
|
|
65
|
+
const clean = buildBilibiliEmbedUrl('BV1PK4y1b7dt', { aid: 'x";evil', cid: '' });
|
|
66
|
+
assert.ok(!clean.includes('evil'));
|
|
67
|
+
assert.equal(new URL(clean).searchParams.get('aid'), null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('user-reported snippet: protocol-relative src with aid/bvid/cid is upgraded with params kept', () => {
|
|
71
|
+
const userSrc = '//player.bilibili.com/player.html?aid=883409884&bvid=BV1PK4y1b7dt&cid=201056987&page=1';
|
|
72
|
+
const fixed = normalizeBilibiliIframeSrc(userSrc);
|
|
73
|
+
assert.ok(fixed.startsWith('https://player.bilibili.com/player.html?'));
|
|
74
|
+
const parsed = new URL(fixed);
|
|
75
|
+
assert.equal(parsed.searchParams.get('bvid'), 'BV1PK4y1b7dt');
|
|
76
|
+
assert.equal(parsed.searchParams.get('aid'), '883409884');
|
|
77
|
+
assert.equal(parsed.searchParams.get('cid'), '201056987');
|
|
78
|
+
|
|
79
|
+
assert.equal(normalizeBilibiliIframeSrc('https://www.bilibili.com/video/BV1PK4y1b7dt'), '');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('iframe HTML keeps legacy attrs but points at the fixed src', () => {
|
|
83
|
+
const html = buildBilibiliIframeHtml(SAMPLE_BVID, { title: 'Test Clip', autoplay: true });
|
|
84
|
+
assert.match(html, /<iframe /);
|
|
85
|
+
assert.match(html, /src="https:\/\/player\.bilibili\.com\/player\.html\?bvid=/);
|
|
86
|
+
assert.ok(!html.includes('src="//player.bilibili.com'));
|
|
87
|
+
for (const attr of ['scrolling="no"', 'border="0"', 'frameborder="no"', 'framespacing="0"', 'allowfullscreen']) {
|
|
88
|
+
assert.ok(html.includes(attr), `iframe should contain ${attr}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
assert.match(html, /width="100%"/);
|
|
92
|
+
assert.match(html, /height="500"/);
|
|
93
|
+
assert.match(html, /aspect-ratio:16\/9/);
|
|
94
|
+
const userHtml = buildBilibiliIframeHtml('BV1PK4y1b7dt', { aid: '883409884', cid: '201056987', page: 1 });
|
|
95
|
+
assert.ok(userHtml.includes('aid=883409884'));
|
|
96
|
+
assert.ok(userHtml.includes('cid=201056987'));
|
|
97
|
+
assert.equal(buildBilibiliIframeHtml('bad-id'), '');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('CSP frame-src allows the Bilibili player everywhere it is served', () => {
|
|
101
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
102
|
+
const repoRoot = join(root, '..', '..', '..');
|
|
103
|
+
const vercel = readFileSync(join(repoRoot, 'vercel.json'), 'utf8');
|
|
104
|
+
const webchatSecurity = readFileSync(join(repoRoot, 'server', 'web-chat', 'core', 'security.js'), 'utf8');
|
|
105
|
+
const apiRouter = readFileSync(join(repoRoot, 'server', 'apiRouter.js'), 'utf8');
|
|
106
|
+
|
|
107
|
+
for (const [name, source] of [['vercel.json', vercel], ['web-chat/core/security.js', webchatSecurity], ['server/apiRouter.js', apiRouter]]) {
|
|
108
|
+
const frameSrc = source.match(/frame-src[^;]*/)?.[0] || '';
|
|
109
|
+
assert.ok(frameSrc.includes('https://player.bilibili.com'), `${name} frame-src must allow the Bilibili player`);
|
|
110
|
+
assert.ok(frameSrc.includes('https://www.bilibili.com'), `${name} frame-src must allow the Bilibili mobile-player redirect`);
|
|
111
|
+
|
|
112
|
+
assert.ok(frameSrc.includes('https://www.youtube-nocookie.com'), `${name} frame-src must keep YouTube`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
assert.ok(vercel.includes('frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.bilibili.com'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('cover pic URLs are upgraded to https (mixed-content safe)', () => {
|
|
120
|
+
assert.equal(normalizeBilibiliPic('http://i0.hdslb.com/bfs/archive/abc.jpg'), 'https://i0.hdslb.com/bfs/archive/abc.jpg');
|
|
121
|
+
assert.equal(normalizeBilibiliPic('//i0.hdslb.com/bfs/archive/abc.jpg'), 'https://i0.hdslb.com/bfs/archive/abc.jpg');
|
|
122
|
+
assert.equal(normalizeBilibiliPic('https://i0.hdslb.com/bfs/archive/abc.jpg'), 'https://i0.hdslb.com/bfs/archive/abc.jpg');
|
|
123
|
+
assert.equal(normalizeBilibiliPic(''), '');
|
|
124
|
+
assert.equal(normalizeBilibiliPic('not a url'), '');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('fetchBilibiliVideoInfo rejects bad bvids without network', async () => {
|
|
128
|
+
for (const bad of ['', 'BVshort', 'av12345', 'https://www.bilibili.com/video/BV1PK4y1b7dt']) {
|
|
129
|
+
const res = await fetchBilibiliVideoInfo(bad);
|
|
130
|
+
assert.equal(res.status, 'error');
|
|
131
|
+
assert.match(res.message, /bvid/i);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('parseBilibiliViewPayload maps the view API payload with https pic and string cid', () => {
|
|
136
|
+
const payload = parseBilibiliViewPayload({
|
|
137
|
+
code: 0,
|
|
138
|
+
data: {
|
|
139
|
+
bvid: SAMPLE_BVID,
|
|
140
|
+
title: 'Ju Jingyi <em class="keyword">MV</em> & more',
|
|
141
|
+
pic: '//i0.hdslb.com/bfs/archive/cover1.jpg',
|
|
142
|
+
owner: { name: 'UP Main' },
|
|
143
|
+
stat: { view: 12345, danmaku: 67 },
|
|
144
|
+
duration: 210,
|
|
145
|
+
pubdate: 1750000000,
|
|
146
|
+
cid: 987654321,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
assert.equal(payload.title, 'Ju Jingyi MV & more');
|
|
151
|
+
assert.equal(payload.pic, 'https://i0.hdslb.com/bfs/archive/cover1.jpg');
|
|
152
|
+
assert.equal(payload.author, 'UP Main');
|
|
153
|
+
assert.equal(payload.playCount, 12345);
|
|
154
|
+
assert.equal(payload.cid, '987654321');
|
|
155
|
+
assert.equal(payload.duration, 210);
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
assert.equal(parseBilibiliViewPayload({ code: -352, message: 'risk control' }), null);
|
|
159
|
+
assert.equal(parseBilibiliViewPayload({ code: 0, data: null }), null);
|
|
160
|
+
assert.equal(parseBilibiliViewPayload(null), null);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('parseBilibiliSearchPayload accepts ONLY the exact bvid result', () => {
|
|
164
|
+
const json = {
|
|
165
|
+
code: 0,
|
|
166
|
+
data: {
|
|
167
|
+
result: [
|
|
168
|
+
{ bvid: 'BV1zzz411zzzz', title: '<em class="keyword">BV1NG411y7Tc</em> lookalike clip', pic: '//i0.hdslb.com/bfs/archive/wrong.jpg', author: 'Other UP', play: 9 },
|
|
169
|
+
{ bvid: 'BV1NG411y7Tc', title: '【鞠婧祎】real clip & 精剪', pic: '//i0.hdslb.com/bfs/archive/right.jpg', author: 'Ju Jingyi Studio', play: 456 },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const picked = parseBilibiliSearchPayload(json, 'BV1NG411y7Tc');
|
|
175
|
+
assert.ok(picked);
|
|
176
|
+
assert.equal(picked.title, '【鞠婧祎】real clip & 精剪');
|
|
177
|
+
assert.equal(picked.pic, 'https://i0.hdslb.com/bfs/archive/right.jpg');
|
|
178
|
+
assert.equal(picked.author, 'Ju Jingyi Studio');
|
|
179
|
+
assert.equal(picked.playCount, 456);
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
assert.equal(parseBilibiliSearchPayload({ code: 0, data: { result: json.data.result.slice(0, 1) } }, 'BV1NG411y7Tc'), null);
|
|
183
|
+
assert.equal(parseBilibiliSearchPayload({ code: 0, data: { result: [] } }, 'BV1NG411y7Tc'), null);
|
|
184
|
+
assert.equal(parseBilibiliSearchPayload({ code: -412 }, 'BV1NG411y7Tc'), null);
|
|
185
|
+
assert.equal(parseBilibiliSearchPayload(null, 'BV1NG411y7Tc'), null);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('web-chat card keeps the video part (?p=) in the watch link', () => {
|
|
189
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
190
|
+
const clientPath = join(root, '..', '..', 'web-chat', 'ui', 'scripts', 'client.js');
|
|
191
|
+
const source = readFileSync(clientPath, 'utf8');
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
assert.match(source, /bilibili\\\.com\\\/video\\\/\(BV\[0-9a-zA-Z\]\{10\}\)\(\\\?\[/);
|
|
195
|
+
assert.ok(source.includes('pageFromUrl'));
|
|
196
|
+
|
|
197
|
+
assert.ok(!source.includes('data-page='));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('bilibili card extraction enriches from tool payloads and answers linkify bare BV ids', () => {
|
|
201
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
202
|
+
const clientPath = join(root, '..', '..', 'web-chat', 'ui', 'scripts', 'client.js');
|
|
203
|
+
const source = readFileSync(clientPath, 'utf8');
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
assert.match(source, /function upsertBili\(/);
|
|
207
|
+
assert.match(source, /existing\.author === 'UP 主'/);
|
|
208
|
+
|
|
209
|
+
assert.match(source, /\(b\.playCount \|\| 0\) - \(a\.playCount \|\| 0\)/);
|
|
210
|
+
|
|
211
|
+
assert.match(source, /function linkifyBareBilibiliIds\(/);
|
|
212
|
+
assert.match(source, /formatMarkdown\(linkifyBareBilibiliIds\(cleanAnswerText\)\)/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('GET /api/bilibili/cover validates bvid (no network needed)', async () => {
|
|
216
|
+
const { startWebChatServer, stopWebChatServer } = await import('../../webChatServer.js');
|
|
217
|
+
await startWebChatServer({ port: 3311 });
|
|
218
|
+
try {
|
|
219
|
+
const missing = await fetch('http://localhost:3311/api/bilibili/cover');
|
|
220
|
+
assert.equal(missing.status, 400);
|
|
221
|
+
|
|
222
|
+
const bad = await fetch('http://localhost:3311/api/bilibili/cover?bvid=nope');
|
|
223
|
+
assert.equal(bad.status, 400);
|
|
224
|
+
|
|
225
|
+
const wrongMethod = await fetch('http://localhost:3311/api/bilibili/cover?bvid=BV1PK4y1b7dt', { method: 'POST' });
|
|
226
|
+
assert.equal(wrongMethod.status, 405);
|
|
227
|
+
} finally {
|
|
228
|
+
await stopWebChatServer();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('web-chat Bilibili card is iframe-free: cover + open-on-Bilibili link', () => {
|
|
233
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
234
|
+
const clientPath = join(root, '..', '..', 'web-chat', 'ui', 'scripts', 'client.js');
|
|
235
|
+
const source = readFileSync(clientPath, 'utf8');
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
assert.ok(!source.includes('player.bilibili.com/player.html'), 'client must not build player iframes');
|
|
239
|
+
assert.ok(!source.includes('loadBilibiliIframe'));
|
|
240
|
+
assert.ok(!source.includes('data-bilibili-facade'));
|
|
241
|
+
assert.ok(!source.includes('chat-bilibili-iframe'));
|
|
242
|
+
|
|
243
|
+
assert.match(source, /<a class="bilibili-card-media" href="' \+ escapeHtml\(watchUrl\)/);
|
|
244
|
+
|
|
245
|
+
assert.match(source, /function loadBilibiliCover\(/);
|
|
246
|
+
assert.ok(source.includes('/api/bilibili/cover?bvid='));
|
|
247
|
+
assert.ok(source.includes('bilibili-facade-thumb'));
|
|
248
|
+
assert.match(source, /closest\('\[data-bilibili-cover\]'\)/);
|
|
249
|
+
|
|
250
|
+
assert.match(source, /!wrap\.querySelector\('\.bilibili-facade-thumb'\)/);
|
|
251
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { worksData as richWorks } from '../../../src/data/worksData.js';
|
|
8
|
+
import { discographyData as richDiscography } from '../../../src/data/discographyData.js';
|
|
9
|
+
import { brandsData as richBrands } from '../../../src/data/brandsData.js';
|
|
10
|
+
import { awardsData as richAwards } from '../../../src/data/awardsData.js';
|
|
11
|
+
import { timelineData as richTimeline } from '../../../src/data/timelineData.js';
|
|
12
|
+
import { profileData as richProfile } from '../../../src/data/profileData.js';
|
|
13
|
+
|
|
14
|
+
import { worksData as mcpWorks } from '../../mcp/data/works.js';
|
|
15
|
+
import { discographyData as mcpDiscography } from '../../mcp/data/discography.js';
|
|
16
|
+
import { brandsData as mcpBrands } from '../../mcp/data/brands.js';
|
|
17
|
+
import { awardsData as mcpAwards } from '../../mcp/data/awards.js';
|
|
18
|
+
import { timelineData as mcpTimeline } from '../../mcp/data/timeline.js';
|
|
19
|
+
import { profileData as mcpProfile } from '../../mcp/data/profile.js';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
getUnifiedProfile,
|
|
23
|
+
getUnifiedWorks,
|
|
24
|
+
getUnifiedDiscography,
|
|
25
|
+
getUnifiedAwards,
|
|
26
|
+
getUnifiedBrands,
|
|
27
|
+
getUnifiedTimeline
|
|
28
|
+
} from '../../mcp/dataAdapter.js';
|
|
29
|
+
|
|
30
|
+
import { readResource } from '../../mcp/resources.js';
|
|
31
|
+
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
const rootDir = resolve(__dirname, '../../../');
|
|
35
|
+
|
|
36
|
+
test('Catalog SSOT: every work in src/data/worksData.js matches server/mcp/data/works.js', () => {
|
|
37
|
+
assert.ok(richWorks.length > 0);
|
|
38
|
+
assert.ok(mcpWorks.length >= richWorks.length);
|
|
39
|
+
|
|
40
|
+
for (const richItem of richWorks) {
|
|
41
|
+
const matched = mcpWorks.find(m => m.id === richItem.id || m.alias === richItem.id);
|
|
42
|
+
assert.ok(
|
|
43
|
+
matched,
|
|
44
|
+
`Work "${richItem.id}" (${richItem.titleEn || richItem.titleTh}) in src/data/worksData.js must exist in server/mcp/data/works.js`
|
|
45
|
+
);
|
|
46
|
+
assert.equal(matched.year, richItem.year);
|
|
47
|
+
assert.ok(matched.titleCn, `Work "${richItem.id}" should have a Chinese title`);
|
|
48
|
+
assert.ok(matched.titleEn, `Work "${richItem.id}" should have an English title`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('Catalog SSOT: discography covers major 2026 releases and career landmarks', () => {
|
|
53
|
+
const mcpTitles = mcpDiscography.map(s => (s.titleEn || s.titleCn || '').toLowerCase());
|
|
54
|
+
|
|
55
|
+
assert.ok(mcpTitles.some(t => t.includes('find the way')));
|
|
56
|
+
assert.ok(mcpTitles.some(t => t.includes('not suffering') || t.includes('不苦')));
|
|
57
|
+
assert.ok(mcpTitles.some(t => t.includes('eternal fragrance') || t.includes('千香')));
|
|
58
|
+
assert.ok(mcpTitles.some(t => t.includes('everyday') || t.includes('每一天')));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Catalog SSOT: brand endorsements synchronize global ambassador contracts', () => {
|
|
62
|
+
const mcpBrandIds = mcpBrands.map(b => b.id);
|
|
63
|
+
|
|
64
|
+
assert.ok(mcpBrandIds.includes('prsr'));
|
|
65
|
+
assert.ok(mcpBrandIds.includes('versace'));
|
|
66
|
+
assert.ok(mcpBrandIds.includes('lafang'));
|
|
67
|
+
assert.ok(mcpBrandIds.includes('tutucare'));
|
|
68
|
+
assert.ok(mcpBrandIds.includes('dafuyan'));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Unified DataAdapter: correctly extracts and standardizes all domain models', () => {
|
|
72
|
+
const profile = getUnifiedProfile();
|
|
73
|
+
assert.equal(profile.nameEn, 'Ju Jingyi');
|
|
74
|
+
assert.equal(profile.nameCn, '鞠婧祎');
|
|
75
|
+
assert.ok(profile.fandomName.includes('Miju'));
|
|
76
|
+
|
|
77
|
+
const works = getUnifiedWorks();
|
|
78
|
+
assert.equal(works.length, richWorks.length);
|
|
79
|
+
assert.ok(works.every(w => w.id && w.titleEn && w.year));
|
|
80
|
+
|
|
81
|
+
const discography = getUnifiedDiscography();
|
|
82
|
+
assert.ok(discography.length >= 20);
|
|
83
|
+
|
|
84
|
+
const awards = getUnifiedAwards();
|
|
85
|
+
assert.ok(awards.length >= 10);
|
|
86
|
+
|
|
87
|
+
const brands = getUnifiedBrands();
|
|
88
|
+
assert.ok(brands.length >= 10);
|
|
89
|
+
|
|
90
|
+
const timeline = getUnifiedTimeline();
|
|
91
|
+
assert.ok(timeline.length >= 10);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('MCP Resource: kiku://catalog generates complete unified summary without errors', async () => {
|
|
95
|
+
const res = await readResource('kiku://catalog');
|
|
96
|
+
assert.equal(res.contents.length, 1);
|
|
97
|
+
assert.equal(res.contents[0].mimeType, 'application/json');
|
|
98
|
+
|
|
99
|
+
const catalog = JSON.parse(res.contents[0].text);
|
|
100
|
+
assert.equal(catalog.artist, 'Ju Jingyi');
|
|
101
|
+
assert.ok(catalog.totalWorks >= 18);
|
|
102
|
+
assert.ok(catalog.totalSongs >= 25);
|
|
103
|
+
assert.ok(catalog.featuredWorks.length > 0);
|
|
104
|
+
assert.ok(catalog.globalBrands.length > 0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Packaging & Bins: package.json has valid bin wrappers and files manifest', () => {
|
|
108
|
+
const pkgPath = resolve(rootDir, 'package.json');
|
|
109
|
+
assert.ok(existsSync(pkgPath));
|
|
110
|
+
|
|
111
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
112
|
+
assert.equal(pkg.name, 'kiku-agent');
|
|
113
|
+
assert.ok(pkg.bin['kiku-agent'], 'kiku-agent bin must be defined');
|
|
114
|
+
assert.ok(pkg.bin['kiku-mcp'], 'kiku-mcp bin must be defined');
|
|
115
|
+
|
|
116
|
+
const cliPath = resolve(rootDir, pkg.bin['kiku-agent']);
|
|
117
|
+
assert.ok(existsSync(cliPath), `CLI entry "${cliPath}" must exist`);
|
|
118
|
+
const cliContent = readFileSync(cliPath, 'utf8');
|
|
119
|
+
assert.ok(cliContent.startsWith('#!/usr/bin/env node'), 'CLI must start with shebang');
|
|
120
|
+
|
|
121
|
+
const mcpPath = resolve(rootDir, pkg.bin['kiku-mcp']);
|
|
122
|
+
assert.ok(existsSync(mcpPath), `MCP entry "${mcpPath}" must exist`);
|
|
123
|
+
const mcpContent = readFileSync(mcpPath, 'utf8');
|
|
124
|
+
assert.ok(mcpContent.startsWith('#!/usr/bin/env node'), 'MCP must start with shebang');
|
|
125
|
+
|
|
126
|
+
assert.ok(pkg.files.includes('src/data/**/*.js'), 'package.json files must include src/data');
|
|
127
|
+
const covers = (rel) => pkg.files.some((p) => p === 'server/**/*' || p === rel || (rel === 'server/agent/*.js' && p === 'server/**/*.js') || (rel === 'server/mcp/*.js' && (p === 'server/**/*.js' || p === 'server/mcp/**/*.js')));
|
|
128
|
+
assert.ok(covers('server/agent/*.js'), 'package.json files must include server/agent (broad server/**/* also satisfies this)');
|
|
129
|
+
assert.ok(covers('server/mcp/*.js'), 'package.json files must include server/mcp (broad server/**/* also satisfies this)');
|
|
130
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { extractCitations, formatCitationsBlock, resolveCitationReference } from '../citations.js';
|
|
4
|
+
import { routeCommand } from '../commands.js';
|
|
5
|
+
|
|
6
|
+
test('extractCitations: correctly extracts and deduplicates works from search_works', () => {
|
|
7
|
+
const tools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'search_works',
|
|
10
|
+
result: {
|
|
11
|
+
count: 2,
|
|
12
|
+
works: [
|
|
13
|
+
{
|
|
14
|
+
id: 'legend-of-yunxi',
|
|
15
|
+
titleTh: 'ตำนานหยุนซี มเหสียอดอัจฉริยะแห่งพิษ',
|
|
16
|
+
titleCn: '芸汐传',
|
|
17
|
+
category: 'ซีรีส์โบราณ',
|
|
18
|
+
year: 2018,
|
|
19
|
+
role: 'หานอวิ๋นซี (Han Yunxi)'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'in-blossom',
|
|
23
|
+
titleTh: 'บุปผารักอลวน',
|
|
24
|
+
titleCn: '花间令',
|
|
25
|
+
category: 'ซีรีส์สืบสวนโบราณ',
|
|
26
|
+
year: 2024,
|
|
27
|
+
role: 'หยางไฉ่เวย / ซ่างกวนจื่อ'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const answer = 'จวีจิ้งอี มีผลงานซีรีส์ชื่อดังอย่าง ตำนานหยุนซี (芸汐传) รับบทเป็น หานอวิ๋นซี';
|
|
35
|
+
const citations = extractCitations(answer, tools);
|
|
36
|
+
|
|
37
|
+
assert.equal(citations.length, 1);
|
|
38
|
+
assert.equal(citations[0].id, 1);
|
|
39
|
+
assert.equal(citations[0].type, 'work');
|
|
40
|
+
assert.match(citations[0].title, /ตำนานหยุนซี/);
|
|
41
|
+
assert.equal(citations[0].year, 2018);
|
|
42
|
+
assert.equal(citations[0].verified, true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('extractCitations: extracts discography, timeline, and web search results', () => {
|
|
46
|
+
const tools = [
|
|
47
|
+
{
|
|
48
|
+
name: 'search_discography',
|
|
49
|
+
result: {
|
|
50
|
+
songs: [
|
|
51
|
+
{
|
|
52
|
+
title: 'Everyday',
|
|
53
|
+
titleTh: 'ทุกๆ วัน',
|
|
54
|
+
category: 'Single',
|
|
55
|
+
year: 2016
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'get_career_timeline',
|
|
62
|
+
result: {
|
|
63
|
+
timeline: [
|
|
64
|
+
{
|
|
65
|
+
year: 2013,
|
|
66
|
+
event: 'เดบิวต์ในฐานะสมาชิกรุ่นที่ 2 ของ SNH48'
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'web_search',
|
|
73
|
+
result: {
|
|
74
|
+
results: [
|
|
75
|
+
{
|
|
76
|
+
title: 'Ju Jingyi Studio Official Statement',
|
|
77
|
+
url: 'https://weibo.com/jujingyi/status/12345'
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const answer = 'เธอเดบิวต์ในปี 2013 กับ SNH48 และมีเพลง Everyday รวมถึงแถลงการณ์อย่างเป็นทางการ';
|
|
85
|
+
const citations = extractCitations(answer, tools);
|
|
86
|
+
|
|
87
|
+
assert.equal(citations.length, 3);
|
|
88
|
+
assert.equal(citations[0].id, 1);
|
|
89
|
+
assert.equal(citations[0].type, 'song');
|
|
90
|
+
assert.equal(citations[1].id, 2);
|
|
91
|
+
assert.equal(citations[1].type, 'timeline');
|
|
92
|
+
assert.equal(citations[2].id, 3);
|
|
93
|
+
assert.equal(citations[2].type, 'web');
|
|
94
|
+
assert.equal(citations[2].source, 'weibo.com');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('extractCitations: returns empty array for empty inputs or no matches', () => {
|
|
98
|
+
assert.deepEqual(extractCitations('', []), []);
|
|
99
|
+
assert.deepEqual(extractCitations('ข้อความทั่วไป', null), []);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('formatCitationsBlock: formats citations in Thai, English, and Chinese', () => {
|
|
103
|
+
const citations = [
|
|
104
|
+
{
|
|
105
|
+
id: 1,
|
|
106
|
+
title: 'บุปผารักอลวน (花间令)',
|
|
107
|
+
category: 'ซีรีส์',
|
|
108
|
+
year: 2024,
|
|
109
|
+
source: 'catalog:works (search_works)',
|
|
110
|
+
verified: true
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 2,
|
|
114
|
+
title: 'Weibo News',
|
|
115
|
+
url: 'https://weibo.com/test',
|
|
116
|
+
category: 'เว็บข่าว',
|
|
117
|
+
verified: true
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const thBlock = formatCitationsBlock(citations, { lang: 'th' });
|
|
122
|
+
assert.match(thBlock, /แหล่งข้อมูลและหลักฐานอ้างอิง/);
|
|
123
|
+
assert.match(thBlock, /\[1\] บุปผารักอลวน/);
|
|
124
|
+
assert.match(thBlock, /\[2024\]/);
|
|
125
|
+
assert.match(thBlock, /\[2\] Weibo News/);
|
|
126
|
+
assert.match(thBlock, /https:\/\/weibo.com\/test/);
|
|
127
|
+
|
|
128
|
+
const enBlock = formatCitationsBlock(citations, { lang: 'en' });
|
|
129
|
+
assert.match(enBlock, /Sources & Evidence Citations/);
|
|
130
|
+
|
|
131
|
+
const zhBlock = formatCitationsBlock(citations, { lang: 'zh' });
|
|
132
|
+
assert.match(zhBlock, /参考来源与证据引用/);
|
|
133
|
+
|
|
134
|
+
assert.equal(formatCitationsBlock([]), '');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('resolveCitationReference: resolves bracket notation, hash, number, and entity name', () => {
|
|
138
|
+
const citations = [
|
|
139
|
+
{ id: 1, title: 'ตำนานหยุนซี (芸汐传)', year: 2018 },
|
|
140
|
+
{ id: 2, title: 'บุปผารักอลวน (In Blossom)', year: 2024 },
|
|
141
|
+
{ id: 3, title: 'เพลง Everyday', year: 2016 }
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const res1 = resolveCitationReference('เล่าเรื่องย่อของ [1] ให้ฟังหน่อย', citations);
|
|
145
|
+
assert.equal(res1?.id, 1);
|
|
146
|
+
assert.equal(res1?.year, 2018);
|
|
147
|
+
|
|
148
|
+
const res2 = resolveCitationReference('ขอรายละเอียดของ #2 เพิ่มเติม', citations);
|
|
149
|
+
assert.equal(res2?.id, 2);
|
|
150
|
+
|
|
151
|
+
const res3 = resolveCitationReference('3', citations);
|
|
152
|
+
assert.equal(res3?.id, 3);
|
|
153
|
+
|
|
154
|
+
const resTitle = resolveCitationReference('บุปผารักอลวน มีกี่ตอน', citations);
|
|
155
|
+
assert.equal(resTitle?.id, 2);
|
|
156
|
+
|
|
157
|
+
const notFound = resolveCitationReference('ไม่มีในรายการ', citations);
|
|
158
|
+
assert.equal(notFound, null);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('/sources command: displays active grounded citations when available', async () => {
|
|
162
|
+
const mockAgent = {
|
|
163
|
+
lastCitations: [
|
|
164
|
+
{
|
|
165
|
+
id: 1,
|
|
166
|
+
title: 'ตำนานหยุนซี',
|
|
167
|
+
category: 'ซีรีส์',
|
|
168
|
+
year: 2018,
|
|
169
|
+
tool: 'search_works'
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const status = await routeCommand('/sources', {
|
|
175
|
+
agent: mockAgent,
|
|
176
|
+
setIsProcessing: () => {}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
assert.equal(status, 'HANDLED');
|
|
180
|
+
});
|