kiku-agent 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +459 -407
- package/package.json +27 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +629 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +528 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +383 -179
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
package/server/mcp/security.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
const SECURITY_CONFIG = {
|
|
4
|
-
apiKeyRequired:
|
|
4
|
+
apiKeyRequired: Boolean(process.env.MCP_API_KEY || process.env.NODE_ENV === 'production'),
|
|
5
5
|
|
|
6
6
|
rateLimitWindowMs: 60_000,
|
|
7
7
|
rateLimitMaxRequests: 30,
|
|
@@ -10,7 +10,7 @@ const SECURITY_CONFIG = {
|
|
|
10
10
|
sessionTtlMs: 30 * 60_000,
|
|
11
11
|
maxMessagesPerSession: 500,
|
|
12
12
|
|
|
13
|
-
corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
13
|
+
corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || 'https://jujingyi-kikuuu.vercel.app').split(',').map(s => s.trim()).filter(Boolean),
|
|
14
14
|
maxRequestBodyBytes: 1_048_576,
|
|
15
15
|
};
|
|
16
16
|
|
|
@@ -19,13 +19,16 @@ export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
|
|
|
19
19
|
export function validateApiKey(req) {
|
|
20
20
|
if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
|
|
21
21
|
|
|
22
|
-
let key = req
|
|
22
|
+
let key = req?.headers?.['x-api-key'];
|
|
23
23
|
if (!key) {
|
|
24
|
-
const authHeader = req
|
|
24
|
+
const authHeader = req?.headers?.['authorization'];
|
|
25
25
|
if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
|
|
26
26
|
key = authHeader.slice(7).trim();
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
if (!process.env.MCP_API_KEY) {
|
|
30
|
+
return { valid: false, error: 'MCP API key is not configured on the server. Please set MCP_API_KEY.' };
|
|
31
|
+
}
|
|
29
32
|
if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or Authorization: Bearer <key>.' };
|
|
30
33
|
const expected = Buffer.from(process.env.MCP_API_KEY || '');
|
|
31
34
|
const received = Buffer.from(String(key));
|
|
@@ -88,16 +91,24 @@ export function validateSessionId(sessionId) {
|
|
|
88
91
|
return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
export function setCorsHeaders(res, origin) {
|
|
92
|
-
const allowed = SECURITY_CONFIG.corsAllowedOrigins;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
export function setCorsHeaders(res, origin) {
|
|
95
|
+
const allowed = SECURITY_CONFIG.corsAllowedOrigins;
|
|
96
|
+
let isLocalhost = false;
|
|
97
|
+
if (process.env.NODE_ENV !== 'production' && origin) {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = new URL(origin);
|
|
100
|
+
isLocalhost = (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') &&
|
|
101
|
+
(parsed.protocol === 'http:' || parsed.protocol === 'https:');
|
|
102
|
+
} catch {
|
|
103
|
+
isLocalhost = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const isAllowed = !origin || allowed.includes(origin) || isLocalhost;
|
|
107
|
+
|
|
108
|
+
if (origin && isAllowed) {
|
|
109
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
110
|
+
res.setHeader('Vary', 'Origin');
|
|
111
|
+
}
|
|
101
112
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
102
113
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
|
|
103
114
|
res.setHeader('Access-Control-Max-Age', '86400');
|
package/server/mcp/server.js
CHANGED
|
@@ -5,6 +5,27 @@ import { toolDefinitions, handleToolCall } from './tools.js';
|
|
|
5
5
|
import { resourceDefinitions, readResource } from './resources.js';
|
|
6
6
|
import { getPrompt } from './prompts.js';
|
|
7
7
|
|
|
8
|
+
const WRITE_TOOLS = new Set([
|
|
9
|
+
'manage_skill',
|
|
10
|
+
'play_music', 'stop_music', 'pause_music', 'resume_music',
|
|
11
|
+
'mute_music', 'unmute_music', 'next_track', 'prev_track',
|
|
12
|
+
'add_to_queue', 'remove_from_queue', 'clear_queue',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const NON_IDEMPOTENT_TOOLS = new Set([
|
|
17
|
+
'manage_skill', 'play_music', 'add_to_queue', 'remove_from_queue',
|
|
18
|
+
'clear_queue', 'next_track', 'prev_track', 'run_quiz',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const OPEN_WORLD_TOOLS = new Set([
|
|
22
|
+
'web_search', 'web_extract', 'fetch_webpage', 'fetch_api', 'fetch_json',
|
|
23
|
+
'screenshot_webpage', 'search_news', 'fetch_rss', 'search_encyclopedia',
|
|
24
|
+
'search_music_platforms', 'search_chinese_web', 'search_bilibili',
|
|
25
|
+
'search_youtube', 'search_web_images', 'run_quiz', 'run_subagent',
|
|
26
|
+
'run_deep_research', 'verify_claim', 'suggest_topics',
|
|
27
|
+
]);
|
|
28
|
+
|
|
8
29
|
export function createMcpServer(options = {}) {
|
|
9
30
|
const toolOptions = {
|
|
10
31
|
transport: options.transport || 'stdio',
|
|
@@ -52,12 +73,19 @@ export function createMcpServer(options = {}) {
|
|
|
52
73
|
});
|
|
53
74
|
|
|
54
75
|
for (const tool of toolDefinitions) {
|
|
76
|
+
const description = tool.name === 'get_api_key'
|
|
77
|
+
? `${tool.description} (SENSITIVE: returns a stored Developer VIP API key — do not expose it publicly.)`
|
|
78
|
+
: tool.description;
|
|
55
79
|
server.registerTool(
|
|
56
80
|
tool.name,
|
|
57
81
|
{
|
|
58
|
-
description
|
|
82
|
+
description,
|
|
59
83
|
inputSchema: tool.inputSchema,
|
|
60
|
-
annotations: {
|
|
84
|
+
annotations: {
|
|
85
|
+
readOnlyHint: !WRITE_TOOLS.has(tool.name),
|
|
86
|
+
idempotentHint: !NON_IDEMPOTENT_TOOLS.has(tool.name),
|
|
87
|
+
openWorldHint: OPEN_WORLD_TOOLS.has(tool.name),
|
|
88
|
+
},
|
|
61
89
|
},
|
|
62
90
|
async (args) => {
|
|
63
91
|
try {
|
|
@@ -85,11 +113,13 @@ export function createMcpServer(options = {}) {
|
|
|
85
113
|
'kiku://discography': 'kiku-discography',
|
|
86
114
|
'kiku://brands': 'kiku-brands',
|
|
87
115
|
'kiku://social': 'kiku-social',
|
|
116
|
+
'kiku://industry-index': 'kiku-industry-index',
|
|
88
117
|
};
|
|
89
118
|
|
|
90
119
|
for (const def of resourceDefinitions) {
|
|
120
|
+
const resourceName = resourceMap[def.uri] || def.uri.replace('kiku://', 'kiku-');
|
|
91
121
|
server.registerResource(
|
|
92
|
-
|
|
122
|
+
resourceName,
|
|
93
123
|
def.uri,
|
|
94
124
|
{ description: def.description, mimeType: def.mimeType },
|
|
95
125
|
async (uri) => readResource(uri.href)
|
|
@@ -131,5 +161,64 @@ export function createMcpServer(options = {}) {
|
|
|
131
161
|
async (args) => getPrompt('career_overview', args)
|
|
132
162
|
);
|
|
133
163
|
|
|
164
|
+
server.registerPrompt(
|
|
165
|
+
'draft_fan_fiction',
|
|
166
|
+
{
|
|
167
|
+
description: 'Draft creative fan fiction scenarios incorporating authentic Ju Jingyi characters and lore.',
|
|
168
|
+
argsSchema: {
|
|
169
|
+
character: z.string().min(1).describe('Character name (e.g. "Yang Caiwei", "Han Yunxi", "Fu Rong")'),
|
|
170
|
+
theme: z.string().optional().describe('Story theme or mood (mystery, romance, fantasy adventure)'),
|
|
171
|
+
setting: z.string().optional().describe('Story backdrop or scenario'),
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
async (args) => getPrompt('draft_fan_fiction', args)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
server.registerPrompt(
|
|
178
|
+
'analyze_drama_rating',
|
|
179
|
+
{
|
|
180
|
+
description: 'Provide an analytical overview of a drama\'s reception, milestones, and iconic performances.',
|
|
181
|
+
argsSchema: {
|
|
182
|
+
title: z.string().min(1).describe('Drama title (e.g. "In Blossom", "Legend of Yunxi")'),
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
async (args) => getPrompt('analyze_drama_rating', args)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
server.registerPrompt(
|
|
189
|
+
'compare_discography_era',
|
|
190
|
+
{
|
|
191
|
+
description: 'Compare musical styles and evolution across Ju Jingyi\'s idol, solo, and studio eras.',
|
|
192
|
+
argsSchema: {
|
|
193
|
+
era_a: z.string().optional().describe('First era (e.g. "snh48", "solo_early", "studio_era")'),
|
|
194
|
+
era_b: z.string().optional().describe('Second era (e.g. "modern", "solo")'),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
async (args) => getPrompt('compare_discography_era', args)
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
server.registerPrompt(
|
|
201
|
+
'investigate_viral_claim',
|
|
202
|
+
{
|
|
203
|
+
description: 'Investigate a viral claim, set leak, casting rumor, or news report about Ju Jingyi using multi-tool evidence and catalog cross-referencing.',
|
|
204
|
+
argsSchema: {
|
|
205
|
+
claim: z.string().min(1).describe('The viral rumor, set leak, or statement to investigate'),
|
|
206
|
+
platform_hint: z.string().optional().describe('Platform where the claim went viral (e.g. "weibo", "douyin", "baidu")'),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
async (args) => getPrompt('investigate_viral_claim', args)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
server.registerPrompt(
|
|
213
|
+
'deep_research_topic',
|
|
214
|
+
{
|
|
215
|
+
description: 'Perform an exhaustive deep research investigation on any Ju Jingyi entertainment topic across catalog, live Chinese media, news, and images.',
|
|
216
|
+
argsSchema: {
|
|
217
|
+
topic: z.string().min(1).describe('The topic, milestone, or controversy to research in depth'),
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
async (args) => getPrompt('deep_research_topic', args)
|
|
221
|
+
);
|
|
222
|
+
|
|
134
223
|
return server;
|
|
135
224
|
}
|
package/server/mcp/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { executeTool, toolsSchema } from '../agent/tools.js';
|
|
3
|
+
import { sanitizeInput } from './security.js';
|
|
3
4
|
|
|
4
|
-
// Rich catalog/database tools — safe to expose over public MCP.
|
|
5
5
|
const PUBLIC_TOOL_NAMES = new Set([
|
|
6
6
|
'get_profile_and_fandom',
|
|
7
7
|
'get_quotes',
|
|
@@ -18,26 +18,34 @@ const PUBLIC_TOOL_NAMES = new Set([
|
|
|
18
18
|
'get_gallery_photos',
|
|
19
19
|
'global_search',
|
|
20
20
|
'get_system_stats',
|
|
21
|
+
'get_current_datetime',
|
|
22
|
+
'get_youtube_transcript',
|
|
23
|
+
'search_news',
|
|
24
|
+
'fetch_rss',
|
|
25
|
+
'search_encyclopedia',
|
|
26
|
+
'search_music_platforms',
|
|
27
|
+
'get_entertainment_metrics',
|
|
28
|
+
'get_industry_index',
|
|
21
29
|
]);
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'web_extract']);
|
|
31
|
+
const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'web_extract', 'search_web_images']);
|
|
25
32
|
const webToolsEnabled = process.env.KIKU_MCP_ENABLE_WEB_TOOLS === 'true';
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
const OPTIONAL_CN_TOOL_NAMES = new Set(['search_chinese_web', 'search_bilibili', 'search_web_images']);
|
|
35
|
+
const cnToolsEnabled = process.env.KIKU_ENABLE_CN_WEB_TOOLS === 'true';
|
|
36
|
+
|
|
28
37
|
const MUSIC_TOOL_NAMES = new Set([
|
|
29
38
|
'search_youtube',
|
|
30
39
|
]);
|
|
31
40
|
|
|
32
|
-
// Stateless engagement tools are safe to expose.
|
|
33
41
|
const RESEARCH_TOOL_NAMES = new Set([
|
|
34
42
|
'suggest_topics',
|
|
35
43
|
'run_quiz',
|
|
44
|
+
'checkpoint_step',
|
|
45
|
+
'record_failure',
|
|
46
|
+
'load_active_run',
|
|
36
47
|
]);
|
|
37
48
|
|
|
38
|
-
// Privileged tools — they touch the filesystem, server-local resources, secrets, or
|
|
39
|
-
// mutated server state. Exposed over MCP but REQUIRE authorization (a valid MCP_API_KEY
|
|
40
|
-
// / X-API-Key / Bearer header). Never callable anonymously on the public HTTP endpoint.
|
|
41
49
|
const PRIVILEGED_TOOL_NAMES = new Set([
|
|
42
50
|
'read_local_file',
|
|
43
51
|
'list_directory',
|
|
@@ -64,12 +72,13 @@ const PRIVILEGED_TOOL_NAMES = new Set([
|
|
|
64
72
|
'fetch_json',
|
|
65
73
|
]);
|
|
66
74
|
|
|
67
|
-
// Any tool that is not privileged is public.
|
|
68
75
|
const isPrivileged = (name) => PRIVILEGED_TOOL_NAMES.has(name);
|
|
69
76
|
|
|
70
77
|
function isAllowedTool(name) {
|
|
71
78
|
if (PUBLIC_TOOL_NAMES.has(name) || MUSIC_TOOL_NAMES.has(name) || RESEARCH_TOOL_NAMES.has(name)) return true;
|
|
72
|
-
if (OPTIONAL_WEB_TOOL_NAMES.has(name)) return
|
|
79
|
+
if (OPTIONAL_WEB_TOOL_NAMES.has(name) && webToolsEnabled) return true;
|
|
80
|
+
if (OPTIONAL_CN_TOOL_NAMES.has(name) && cnToolsEnabled) return true;
|
|
81
|
+
if (OPTIONAL_WEB_TOOL_NAMES.has(name) || OPTIONAL_CN_TOOL_NAMES.has(name)) return false;
|
|
73
82
|
return PRIVILEGED_TOOL_NAMES.has(name);
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -164,7 +173,7 @@ export async function handleToolCall(toolName, args = {}, options = {}) {
|
|
|
164
173
|
}
|
|
165
174
|
}
|
|
166
175
|
|
|
167
|
-
const adaptedArgs = { ...(args || {}) };
|
|
176
|
+
const adaptedArgs = { ...sanitizeInput(args || {}) };
|
|
168
177
|
if (toolName === 'search_gallery') delete adaptedArgs.query;
|
|
169
178
|
if (adaptedArgs.category && definition.targetName === 'search_discography' && !adaptedArgs.type) {
|
|
170
179
|
adaptedArgs.type = adaptedArgs.category;
|