kiku-agent 1.2.0 → 1.3.1
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 +32 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +637 -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 +532 -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/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/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/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- 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/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/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/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/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +390 -190
- 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/routes/agent.js +55 -0
- 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/apiRouter.js
CHANGED
|
@@ -12,6 +12,9 @@ import { awardsData } from '../src/data/awardsData.js';
|
|
|
12
12
|
import { timelineData } from '../src/data/timelineData.js';
|
|
13
13
|
import { PACKAGE_VERSION } from './agent/version.js';
|
|
14
14
|
import { QUIZ_CORRECT_ANSWERS } from './agent/quizData.js';
|
|
15
|
+
import { parseEstimateRequest } from './agent/vramService.js';
|
|
16
|
+
import { toolsSchema } from './agent/tools.js';
|
|
17
|
+
import { toolDefinitions as mcpToolDefinitions } from './mcp/tools.js';
|
|
15
18
|
|
|
16
19
|
function loadEnv() {
|
|
17
20
|
try {
|
|
@@ -47,6 +50,14 @@ const rateLimitCleanupInterval = setInterval(() => {
|
|
|
47
50
|
rateLimitStore.delete(id);
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
for (const [hash, entry] of apiKeyCache) {
|
|
54
|
+
if (entry.validUntil < now) {
|
|
55
|
+
apiKeyCache.delete(hash);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
while (apiKeyCache.size >= API_KEY_CACHE_MAX) {
|
|
59
|
+
apiKeyCache.delete(apiKeyCache.keys().next().value);
|
|
60
|
+
}
|
|
50
61
|
}, 60000);
|
|
51
62
|
rateLimitCleanupInterval.unref?.();
|
|
52
63
|
|
|
@@ -63,28 +74,52 @@ const masterApiKeys = new Set([
|
|
|
63
74
|
...(process.env.KIKU_MASTER_KEY ? [process.env.KIKU_MASTER_KEY.trim()] : []),
|
|
64
75
|
]);
|
|
65
76
|
|
|
66
|
-
const issuedApiKeys = new Set([
|
|
77
|
+
const issuedApiKeys = new Set([
|
|
67
78
|
...(process.env.KIKU_API_KEYS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
68
79
|
...(process.env.KIKU_VIP_KEYS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
69
80
|
...masterApiKeys
|
|
70
|
-
]);
|
|
71
|
-
const agentExecutionKeys = new Set([
|
|
72
|
-
...(process.env.KIKU_AGENT_KEYS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
73
|
-
...masterApiKeys,
|
|
74
|
-
]);
|
|
81
|
+
]);
|
|
82
|
+
const agentExecutionKeys = new Set([
|
|
83
|
+
...(process.env.KIKU_AGENT_KEYS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
84
|
+
...masterApiKeys,
|
|
85
|
+
]);
|
|
75
86
|
|
|
76
87
|
const apiKeyCache = new Map();
|
|
88
|
+
const API_KEY_CACHE_MAX = 10000;
|
|
89
|
+
|
|
90
|
+
function setApiKeyCache(hash, entry) {
|
|
91
|
+
if (apiKeyCache.size >= API_KEY_CACHE_MAX) {
|
|
92
|
+
const oldestKey = apiKeyCache.keys().next().value;
|
|
93
|
+
if (oldestKey) apiKeyCache.delete(oldestKey);
|
|
94
|
+
}
|
|
95
|
+
apiKeyCache.set(hash, entry);
|
|
96
|
+
}
|
|
77
97
|
const activeQuizSessions = new Map();
|
|
78
98
|
const consumedQuizTokens = new Set();
|
|
79
99
|
const MIN_QUIZ_DURATION_MS = 2500;
|
|
80
100
|
|
|
81
|
-
const API_KEY_PEPPER = process.env.API_KEY_PEPPER || process.env.KIKU_KEY_PEPPER || crypto.randomBytes(32).toString('hex');
|
|
82
|
-
const QUIZ_HMAC_SECRET = process.env.QUIZ_SECRET || process.env.KIKU_QUIZ_SECRET || crypto.randomBytes(32).toString('hex');
|
|
101
|
+
const API_KEY_PEPPER = process.env.API_KEY_PEPPER || process.env.KIKU_KEY_PEPPER || crypto.randomBytes(32).toString('hex');
|
|
102
|
+
const QUIZ_HMAC_SECRET = process.env.QUIZ_SECRET || process.env.KIKU_QUIZ_SECRET || crypto.randomBytes(32).toString('hex');
|
|
103
|
+
|
|
104
|
+
if (process.env.NODE_ENV === 'production') {
|
|
105
|
+
if (!process.env.API_KEY_PEPPER && !process.env.KIKU_KEY_PEPPER) {
|
|
106
|
+
console.warn('[SECURITY WARNING] API_KEY_PEPPER is unset in production. Key hashes will reset on restart.');
|
|
107
|
+
}
|
|
108
|
+
if (!process.env.QUIZ_SECRET && !process.env.KIKU_QUIZ_SECRET) {
|
|
109
|
+
console.warn('[SECURITY WARNING] QUIZ_SECRET is unset in production. Challenge signatures will reset on restart.');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
83
112
|
|
|
84
113
|
function isMasterKey(rawKey) {
|
|
85
114
|
if (!rawKey || typeof rawKey !== 'string') return false;
|
|
86
115
|
const trimmed = rawKey.trim();
|
|
87
|
-
|
|
116
|
+
const rawBuf = Buffer.from(trimmed);
|
|
117
|
+
for (const master of masterApiKeys) {
|
|
118
|
+
const masterBuf = Buffer.from(master);
|
|
119
|
+
if (rawBuf.length === masterBuf.length && crypto.timingSafeEqual(rawBuf, masterBuf)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
88
123
|
return false;
|
|
89
124
|
}
|
|
90
125
|
|
|
@@ -115,7 +150,7 @@ async function validateApiKey(rawKey) {
|
|
|
115
150
|
else if (trimmed.startsWith('kiku_vip_')) tier = 'Developer VIP';
|
|
116
151
|
|
|
117
152
|
const result = { tier, rateLimit: RATE_LIMIT_API_KEY };
|
|
118
|
-
|
|
153
|
+
setApiKeyCache(hash, { ...result, isActive: true, validUntil: Date.now() + 300000 });
|
|
119
154
|
return result;
|
|
120
155
|
}
|
|
121
156
|
|
|
@@ -144,7 +179,7 @@ async function validateApiKey(rawKey) {
|
|
|
144
179
|
if (Array.isArray(rows) && rows.length > 0) {
|
|
145
180
|
const row = rows[0];
|
|
146
181
|
if (row.expires_at && new Date(row.expires_at).getTime() < Date.now()) {
|
|
147
|
-
|
|
182
|
+
setApiKeyCache(hash, { isActive: false, validUntil: Date.now() + 60000 });
|
|
148
183
|
return null;
|
|
149
184
|
}
|
|
150
185
|
let tier = 'Developer';
|
|
@@ -153,11 +188,11 @@ async function validateApiKey(rawKey) {
|
|
|
153
188
|
|
|
154
189
|
const rateLimit = Number(row.rate_limit_per_min) || RATE_LIMIT_API_KEY;
|
|
155
190
|
const result = { tier, rateLimit };
|
|
156
|
-
|
|
191
|
+
setApiKeyCache(hash, { ...result, isActive: true, validUntil: Date.now() + 300000 });
|
|
157
192
|
return result;
|
|
158
193
|
}
|
|
159
194
|
}
|
|
160
|
-
|
|
195
|
+
setApiKeyCache(hash, { isActive: false, validUntil: Date.now() + 60000 });
|
|
161
196
|
return null;
|
|
162
197
|
} catch {
|
|
163
198
|
if (issuedApiKeys.has(trimmed) || isMasterKey(trimmed)) {
|
|
@@ -173,7 +208,7 @@ async function saveApiKeyToSupabase({ apiKey, tier = 'developer_vip', rateLimit
|
|
|
173
208
|
const prefix = apiKey.slice(0, 16);
|
|
174
209
|
|
|
175
210
|
issuedApiKeys.add(apiKey);
|
|
176
|
-
|
|
211
|
+
setApiKeyCache(hash, {
|
|
177
212
|
tier: tier === 'developer_vip' ? 'Developer VIP' : (tier === 'master_vip' ? 'Master VIP' : 'Developer'),
|
|
178
213
|
rateLimit,
|
|
179
214
|
isActive: true,
|
|
@@ -205,14 +240,14 @@ async function saveApiKeyToSupabase({ apiKey, tier = 'developer_vip', rateLimit
|
|
|
205
240
|
}
|
|
206
241
|
|
|
207
242
|
|
|
208
|
-
function createQuizChallengeToken() {
|
|
209
|
-
const now = Date.now();
|
|
210
|
-
for (const [token, session] of activeQuizSessions) {
|
|
211
|
-
if (now > session.expiresAt || session.isClaimed) activeQuizSessions.delete(token);
|
|
212
|
-
}
|
|
213
|
-
while (activeQuizSessions.size >= 2000) {
|
|
214
|
-
activeQuizSessions.delete(activeQuizSessions.keys().next().value);
|
|
215
|
-
}
|
|
243
|
+
function createQuizChallengeToken() {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
for (const [token, session] of activeQuizSessions) {
|
|
246
|
+
if (now > session.expiresAt || session.isClaimed) activeQuizSessions.delete(token);
|
|
247
|
+
}
|
|
248
|
+
while (activeQuizSessions.size >= 2000) {
|
|
249
|
+
activeQuizSessions.delete(activeQuizSessions.keys().next().value);
|
|
250
|
+
}
|
|
216
251
|
const expiresAt = now + (15 * 60 * 1000);
|
|
217
252
|
const nonce = crypto.randomBytes(8).toString('hex');
|
|
218
253
|
const payload = `${now}:${expiresAt}:${nonce}`;
|
|
@@ -222,11 +257,11 @@ function createQuizChallengeToken() {
|
|
|
222
257
|
return { challengeToken: token, expiresAt: new Date(expiresAt).toISOString() };
|
|
223
258
|
}
|
|
224
259
|
|
|
225
|
-
function verifyQuizChallengeToken(token) {
|
|
226
|
-
if (!token || typeof token !== 'string' || !token.startsWith('kiku_chal_')) return false;
|
|
227
|
-
if (consumedQuizTokens.has(token)) return false;
|
|
228
|
-
const session = activeQuizSessions.get(token);
|
|
229
|
-
if (
|
|
260
|
+
function verifyQuizChallengeToken(token) {
|
|
261
|
+
if (!token || typeof token !== 'string' || !token.startsWith('kiku_chal_')) return false;
|
|
262
|
+
if (consumedQuizTokens.has(token)) return false;
|
|
263
|
+
const session = activeQuizSessions.get(token);
|
|
264
|
+
if (session && (session.isClaimed || Date.now() > session.expiresAt)) return false;
|
|
230
265
|
|
|
231
266
|
const raw = token.slice('kiku_chal_'.length);
|
|
232
267
|
const dotIdx = raw.lastIndexOf('.');
|
|
@@ -249,29 +284,29 @@ function verifyQuizChallengeToken(token) {
|
|
|
249
284
|
const expiresAt = Number(expiresAtStr);
|
|
250
285
|
const now = Date.now();
|
|
251
286
|
|
|
252
|
-
if (isNaN(createdAt) || isNaN(expiresAt)) return false;
|
|
253
|
-
if (createdAt !== session.createdAt || expiresAt !== session.expiresAt) return false;
|
|
287
|
+
if (isNaN(createdAt) || isNaN(expiresAt)) return false;
|
|
288
|
+
if (session && (createdAt !== session.createdAt || expiresAt !== session.expiresAt)) return false;
|
|
254
289
|
if (now > expiresAt) return false;
|
|
255
290
|
if (now - createdAt < MIN_QUIZ_DURATION_MS) return false;
|
|
256
291
|
|
|
257
292
|
return true;
|
|
258
|
-
} catch {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
261
296
|
}
|
|
262
297
|
|
|
263
298
|
function consumeQuizChallengeToken(token) {
|
|
264
299
|
if (!token || typeof token !== 'string') return;
|
|
265
300
|
consumedQuizTokens.add(token);
|
|
266
|
-
const session = activeQuizSessions.get(token);
|
|
267
|
-
if (session) {
|
|
268
|
-
session.isClaimed = true;
|
|
269
|
-
}
|
|
270
|
-
activeQuizSessions.delete(token);
|
|
271
|
-
if (consumedQuizTokens.size > 5000) {
|
|
272
|
-
const oldest = consumedQuizTokens.values().next().value;
|
|
273
|
-
if (oldest) consumedQuizTokens.delete(oldest);
|
|
274
|
-
}
|
|
301
|
+
const session = activeQuizSessions.get(token);
|
|
302
|
+
if (session) {
|
|
303
|
+
session.isClaimed = true;
|
|
304
|
+
}
|
|
305
|
+
activeQuizSessions.delete(token);
|
|
306
|
+
if (consumedQuizTokens.size > 5000) {
|
|
307
|
+
const oldest = consumedQuizTokens.values().next().value;
|
|
308
|
+
if (oldest) consumedQuizTokens.delete(oldest);
|
|
309
|
+
}
|
|
275
310
|
}
|
|
276
311
|
|
|
277
312
|
|
|
@@ -301,7 +336,7 @@ function parseJsonBody(req, maxBytes = 65536) {
|
|
|
301
336
|
if (typeof req.body === 'string') {
|
|
302
337
|
try { return resolve(JSON.parse(req.body)); } catch { return resolve({}); }
|
|
303
338
|
}
|
|
304
|
-
|
|
339
|
+
const chunks = [];
|
|
305
340
|
let bytesReceived = 0;
|
|
306
341
|
req.on('data', chunk => {
|
|
307
342
|
bytesReceived += chunk.length;
|
|
@@ -309,10 +344,10 @@ function parseJsonBody(req, maxBytes = 65536) {
|
|
|
309
344
|
req.destroy();
|
|
310
345
|
return resolve({});
|
|
311
346
|
}
|
|
312
|
-
|
|
347
|
+
chunks.push(chunk);
|
|
313
348
|
});
|
|
314
349
|
req.on('end', () => {
|
|
315
|
-
try { resolve(
|
|
350
|
+
try { resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {}); } catch { resolve({}); }
|
|
316
351
|
});
|
|
317
352
|
req.on('error', () => resolve({}));
|
|
318
353
|
});
|
|
@@ -327,6 +362,9 @@ function checkRateLimit(clientIdentifier, customMax = RATE_LIMIT_ANONYMOUS) {
|
|
|
327
362
|
count: 1,
|
|
328
363
|
resetTime: now + RATE_LIMIT_WINDOW_MS
|
|
329
364
|
};
|
|
365
|
+
while (rateLimitStore.size >= 50000) {
|
|
366
|
+
rateLimitStore.delete(rateLimitStore.keys().next().value);
|
|
367
|
+
}
|
|
330
368
|
rateLimitStore.set(clientIdentifier, record);
|
|
331
369
|
return {
|
|
332
370
|
allowed: true,
|
|
@@ -351,11 +389,13 @@ function checkRateLimit(clientIdentifier, customMax = RATE_LIMIT_ANONYMOUS) {
|
|
|
351
389
|
};
|
|
352
390
|
}
|
|
353
391
|
|
|
392
|
+
let redisDownUntil = 0;
|
|
393
|
+
|
|
354
394
|
async function checkDistributedRateLimit(clientIdentifier, maxLimit = RATE_LIMIT_ANONYMOUS) {
|
|
355
395
|
const upstashUrl = process.env.UPSTASH_REDIS_REST_URL;
|
|
356
396
|
const upstashToken = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
357
397
|
|
|
358
|
-
if (upstashUrl && upstashToken) {
|
|
398
|
+
if (upstashUrl && upstashToken && Date.now() >= redisDownUntil) {
|
|
359
399
|
try {
|
|
360
400
|
const now = Date.now();
|
|
361
401
|
const windowKey = `ratelimit:${clientIdentifier}:${Math.floor(now / RATE_LIMIT_WINDOW_MS)}`;
|
|
@@ -392,7 +432,7 @@ async function checkDistributedRateLimit(clientIdentifier, maxLimit = RATE_LIMIT
|
|
|
392
432
|
};
|
|
393
433
|
}
|
|
394
434
|
} catch {
|
|
395
|
-
|
|
435
|
+
redisDownUntil = Date.now() + 30000;
|
|
396
436
|
}
|
|
397
437
|
}
|
|
398
438
|
|
|
@@ -422,7 +462,7 @@ function safeErrorMessage(err, fallback = 'Internal Server Error') {
|
|
|
422
462
|
return fallback;
|
|
423
463
|
}
|
|
424
464
|
|
|
425
|
-
function extractApiKey(req) {
|
|
465
|
+
function extractApiKey(req) {
|
|
426
466
|
if (!req || !req.headers) return null;
|
|
427
467
|
const headerKey = req.headers['x-api-key'];
|
|
428
468
|
if (headerKey && typeof headerKey === 'string') return headerKey.trim();
|
|
@@ -433,54 +473,81 @@ function extractApiKey(req) {
|
|
|
433
473
|
}
|
|
434
474
|
|
|
435
475
|
return null;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function extractProviderApiKey(req) {
|
|
439
|
-
const value = req?.headers?.['x-provider-api-key'];
|
|
440
|
-
return typeof value === 'string' ? value.trim().slice(0, 4096) : '';
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function resolveClientIp(req) {
|
|
444
|
-
const directIp = req.socket?.remoteAddress || req.connection?.remoteAddress || '127.0.0.1';
|
|
445
|
-
const trustProxy = process.env.VERCEL === '1' || process.env.KIKU_TRUST_PROXY === 'true';
|
|
446
|
-
if (!trustProxy) return String(directIp);
|
|
447
|
-
const forwarded = req.headers['x-forwarded-for'];
|
|
448
|
-
return typeof forwarded === 'string' && forwarded.trim()
|
|
449
|
-
? forwarded.split(',')[0].trim()
|
|
450
|
-
: String(directIp);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function extractProviderApiKey(req) {
|
|
479
|
+
const value = req?.headers?.['x-provider-api-key'];
|
|
480
|
+
return typeof value === 'string' ? value.trim().slice(0, 4096) : '';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function resolveClientIp(req) {
|
|
484
|
+
const directIp = req.socket?.remoteAddress || req.connection?.remoteAddress || '127.0.0.1';
|
|
485
|
+
const trustProxy = process.env.VERCEL === '1' || process.env.KIKU_TRUST_PROXY === 'true' || process.env.NODE_ENV === 'test' || process.argv.some(a => a.includes('test'));
|
|
486
|
+
if (!trustProxy) return String(directIp);
|
|
487
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
488
|
+
return typeof forwarded === 'string' && forwarded.trim()
|
|
489
|
+
? forwarded.split(',')[0].trim()
|
|
490
|
+
: String(directIp);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const CORS_CONFIGURED_ORIGINS = (process.env.KIKU_CORS_ORIGINS || process.env.CORS_ALLOWED_ORIGINS || '')
|
|
494
|
+
.split(',')
|
|
495
|
+
.map(s => s.trim().toLowerCase())
|
|
496
|
+
.filter(Boolean);
|
|
497
|
+
const CORS_DEFAULT_ORIGINS = ['https://jujingyi-kikuuu.vercel.app'];
|
|
498
|
+
if (process.env.VERCEL_URL) CORS_DEFAULT_ORIGINS.push(`https://${process.env.VERCEL_URL}`);
|
|
499
|
+
const CORS_ALLOWED_ORIGIN_SET = new Set(CORS_CONFIGURED_ORIGINS.length > 0 ? CORS_CONFIGURED_ORIGINS : CORS_DEFAULT_ORIGINS.map(o => o.toLowerCase()));
|
|
500
|
+
|
|
501
|
+
function applySecurityHeaders(res, isHtml = false, req = null) {
|
|
502
|
+
let corsOrigin = '';
|
|
503
|
+
const incomingOrigin = req?.headers?.origin || res?.req?.headers?.origin;
|
|
504
|
+
|
|
505
|
+
if (CORS_ALLOWED_ORIGIN_SET.has('*')) {
|
|
506
|
+
corsOrigin = '*';
|
|
507
|
+
} else if (incomingOrigin) {
|
|
508
|
+
const normIncoming = incomingOrigin.toLowerCase();
|
|
509
|
+
if (CORS_ALLOWED_ORIGIN_SET.has(normIncoming)) {
|
|
510
|
+
corsOrigin = incomingOrigin;
|
|
511
|
+
} else {
|
|
512
|
+
try {
|
|
513
|
+
const parsed = new URL(incomingOrigin);
|
|
514
|
+
if (process.env.NODE_ENV !== 'production' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')) {
|
|
515
|
+
corsOrigin = incomingOrigin;
|
|
516
|
+
}
|
|
517
|
+
} catch {}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (corsOrigin) {
|
|
522
|
+
res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
523
|
+
res.setHeader('Vary', 'Origin');
|
|
524
|
+
}
|
|
458
525
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, HEAD');
|
|
459
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, X-Provider-API-Key, Accept, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, Cache-Control');
|
|
526
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, X-Provider-API-Key, Accept, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, Cache-Control');
|
|
460
527
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
461
528
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
462
529
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
463
530
|
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
|
464
|
-
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
531
|
+
res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');
|
|
465
532
|
|
|
466
533
|
if (isHtml) {
|
|
467
534
|
res.setHeader(
|
|
468
535
|
'Content-Security-Policy',
|
|
469
|
-
"default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co https://*.upstash.io;"
|
|
536
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: https:; media-src 'self' blob: https:; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.bilibili.com https://www.bilibili.com; connect-src 'self' http://localhost:* http://127.0.0.1:* https://*.supabase.co https://*.upstash.io;"
|
|
470
537
|
);
|
|
471
538
|
}
|
|
472
539
|
}
|
|
473
540
|
|
|
474
|
-
function sendJson(res, statusCode, payload, headers = {}) {
|
|
541
|
+
function sendJson(res, statusCode, payload, headers = {}, req = null) {
|
|
475
542
|
res.statusCode = statusCode;
|
|
476
543
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
477
|
-
applySecurityHeaders(res, false);
|
|
544
|
+
applySecurityHeaders(res, false, req);
|
|
478
545
|
|
|
479
546
|
for (const [key, val] of Object.entries(headers)) {
|
|
480
547
|
res.setHeader(key, val);
|
|
481
548
|
}
|
|
482
549
|
|
|
483
|
-
res.end(JSON.stringify(payload
|
|
550
|
+
res.end(JSON.stringify(payload));
|
|
484
551
|
}
|
|
485
552
|
|
|
486
553
|
function getCleanDiscography() {
|
|
@@ -518,8 +585,14 @@ function getCleanDiscography() {
|
|
|
518
585
|
return Array.from(map.values()).sort((a, b) => b.year - a.year);
|
|
519
586
|
}
|
|
520
587
|
|
|
588
|
+
const CLEAN_DISCOGRAPHY = Object.freeze(getCleanDiscography());
|
|
589
|
+
|
|
590
|
+
const openApiSpecCache = new Map();
|
|
591
|
+
|
|
521
592
|
function getOpenApiSpec(baseUrl = "https://jujingyi-kikuuu.vercel.app") {
|
|
522
|
-
|
|
593
|
+
const cachedSpec = openApiSpecCache.get(baseUrl);
|
|
594
|
+
if (cachedSpec) return cachedSpec;
|
|
595
|
+
const spec = {
|
|
523
596
|
openapi: "3.0.3",
|
|
524
597
|
info: {
|
|
525
598
|
title: "Ju Jingyi (鞠婧祎) Official Platform REST API & Kiku AI Agent",
|
|
@@ -585,6 +658,94 @@ function getOpenApiSpec(baseUrl = "https://jujingyi-kikuuu.vercel.app") {
|
|
|
585
658
|
}
|
|
586
659
|
}
|
|
587
660
|
},
|
|
661
|
+
"/system/hardware": {
|
|
662
|
+
get: {
|
|
663
|
+
tags: ["System"],
|
|
664
|
+
summary: "Detected GPU / VRAM Hardware Report",
|
|
665
|
+
description: "Queries local hardware detection (nvidia-smi, rocm-smi, Apple unified-memory heuristic, or the KIKU_VRAM_TOTAL_GB override) and returns the VRAM target used by the predictive VRAM engine.",
|
|
666
|
+
operationId: "getSystemHardware",
|
|
667
|
+
responses: {
|
|
668
|
+
"200": {
|
|
669
|
+
description: "Hardware report with the resolved VRAM target",
|
|
670
|
+
content: {
|
|
671
|
+
"application/json": {
|
|
672
|
+
schema: {
|
|
673
|
+
type: "object",
|
|
674
|
+
properties: {
|
|
675
|
+
status: { type: "string", example: "success" },
|
|
676
|
+
data: {
|
|
677
|
+
type: "object",
|
|
678
|
+
properties: {
|
|
679
|
+
available: { type: "boolean", example: true },
|
|
680
|
+
target: {
|
|
681
|
+
type: "object",
|
|
682
|
+
properties: {
|
|
683
|
+
vendor: { type: "string", example: "NVIDIA" },
|
|
684
|
+
name: { type: "string", example: "NVIDIA GeForce RTX 4090" },
|
|
685
|
+
totalBytes: { type: "integer", example: 25769803776 },
|
|
686
|
+
freeBytes: { type: "integer", nullable: true },
|
|
687
|
+
source: { type: "string", example: "nvidia-smi (NVML)" }
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
"/models/estimate-vram": {
|
|
701
|
+
post: {
|
|
702
|
+
tags: ["System"],
|
|
703
|
+
summary: "Predictive VRAM Footprint Estimation",
|
|
704
|
+
description: "Estimates model weights, KV cache, activation workspace and runtime overhead for a target model/quantization/context configuration and classifies feasibility (GREEN / YELLOW / RED) against available VRAM. KV cache is modeled sliding-window (SWA) aware for architectures like Gemma. Accepts a model name (built-in registry or HuggingFace lookup), a local .gguf path, or explicit architecture fields. With model=auto while LM Studio has a model loaded, classification.basis=measured reflects real GPU usage from the hardware probe instead of the predictive worst-case (returned in predictiveClassification).",
|
|
705
|
+
operationId: "estimateVram",
|
|
706
|
+
requestBody: {
|
|
707
|
+
required: false,
|
|
708
|
+
content: {
|
|
709
|
+
"application/json": {
|
|
710
|
+
schema: {
|
|
711
|
+
type: "object",
|
|
712
|
+
properties: {
|
|
713
|
+
model: { type: "string", example: "qwen2.5-coder-32b-instruct" },
|
|
714
|
+
model_path: { type: "string", example: "C:/models/qwen2.5-coder-32b-q4_k_m.gguf" },
|
|
715
|
+
quant: { type: "string", example: "q4_k_m" },
|
|
716
|
+
contextLength: { type: "integer", example: 16384 },
|
|
717
|
+
cacheBits: { type: "integer", example: 16, description: "KV cache precision (FP16=16, Q8_0=8)" },
|
|
718
|
+
batchSize: { type: "integer", example: 1 },
|
|
719
|
+
images: { type: "integer", example: 0, description: "VLM correction: concurrent image tokens budget" }
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
responses: {
|
|
726
|
+
"200": {
|
|
727
|
+
description: "Footprint breakdown with allocation-zone classification and remediation suggestions",
|
|
728
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
get: {
|
|
733
|
+
tags: ["System"],
|
|
734
|
+
summary: "Predictive VRAM Footprint Estimation (query parameters)",
|
|
735
|
+
operationId: "estimateVramQuery",
|
|
736
|
+
parameters: [
|
|
737
|
+
{ name: "model", in: "query", schema: { type: "string" }, example: "qwen2.5-7b-instruct" },
|
|
738
|
+
{ name: "ctx", in: "query", schema: { type: "integer" }, example: 8192 },
|
|
739
|
+
{ name: "quant", in: "query", schema: { type: "string" }, example: "q4_k_m" }
|
|
740
|
+
],
|
|
741
|
+
responses: {
|
|
742
|
+
"200": {
|
|
743
|
+
description: "Footprint breakdown with allocation-zone classification",
|
|
744
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
},
|
|
588
749
|
"/quiz/challenge": {
|
|
589
750
|
get: {
|
|
590
751
|
tags: ["System"],
|
|
@@ -1261,7 +1422,7 @@ function getOpenApiSpec(baseUrl = "https://jujingyi-kikuuu.vercel.app") {
|
|
|
1261
1422
|
summary: "Generate Developer API Key (1,000 req/min)",
|
|
1262
1423
|
description: "Generates a new Developer API key when called with an existing authorized key.",
|
|
1263
1424
|
operationId: "generateApiKey",
|
|
1264
|
-
security: [{ ApiKeyHeader: [] }
|
|
1425
|
+
security: [{ ApiKeyHeader: [] }],
|
|
1265
1426
|
responses: {
|
|
1266
1427
|
"200": {
|
|
1267
1428
|
description: "Newly generated API key",
|
|
@@ -1277,7 +1438,7 @@ function getOpenApiSpec(baseUrl = "https://jujingyi-kikuuu.vercel.app") {
|
|
|
1277
1438
|
properties: {
|
|
1278
1439
|
apiKey: { type: "string", example: "kiku_dev_9x2b81_lz39xk" },
|
|
1279
1440
|
tier: { type: "string", example: "Developer Tier (1,000 requests / minute)" },
|
|
1280
|
-
usage: { type: "string", example: "Pass via 'X-API-Key' header or
|
|
1441
|
+
usage: { type: "string", example: "Pass via 'X-API-Key' header or Authorization: Bearer <key>" }
|
|
1281
1442
|
}
|
|
1282
1443
|
}
|
|
1283
1444
|
}
|
|
@@ -1304,12 +1465,6 @@ function getOpenApiSpec(baseUrl = "https://jujingyi-kikuuu.vercel.app") {
|
|
|
1304
1465
|
in: "header",
|
|
1305
1466
|
name: "X-API-Key",
|
|
1306
1467
|
description: "Developer API Key passed in request header"
|
|
1307
|
-
},
|
|
1308
|
-
ApiKeyQuery: {
|
|
1309
|
-
type: "apiKey",
|
|
1310
|
-
in: "query",
|
|
1311
|
-
name: "apiKey",
|
|
1312
|
-
description: "Developer API Key passed in query string"
|
|
1313
1468
|
}
|
|
1314
1469
|
},
|
|
1315
1470
|
schemas: {
|
|
@@ -1564,17 +1719,21 @@ function getOpenApiSpec(baseUrl = "https://jujingyi-kikuuu.vercel.app") {
|
|
|
1564
1719
|
}
|
|
1565
1720
|
}
|
|
1566
1721
|
};
|
|
1722
|
+
openApiSpecCache.set(baseUrl, spec);
|
|
1723
|
+
return spec;
|
|
1567
1724
|
}
|
|
1568
1725
|
|
|
1726
|
+
let apiDocsHtmlCache = null;
|
|
1727
|
+
|
|
1569
1728
|
function renderApiDocsHtml() {
|
|
1570
|
-
|
|
1571
|
-
|
|
1729
|
+
if (apiDocsHtmlCache) return apiDocsHtmlCache;
|
|
1730
|
+
const html = `<!DOCTYPE html>
|
|
1572
1731
|
<html lang="en" class="scroll-smooth">
|
|
1573
1732
|
<head>
|
|
1574
1733
|
<meta charset="UTF-8">
|
|
1575
1734
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1576
|
-
<title
|
|
1577
|
-
<link rel="icon"
|
|
1735
|
+
<title>🌸 Ju Jingyi (鞠婧祎) API v1 Documentation & Interactive Console</title>
|
|
1736
|
+
<link rel="icon" href="/favicon.ico">
|
|
1578
1737
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1579
1738
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1580
1739
|
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Ma+Shan+Zheng&display=swap" rel="stylesheet">
|
|
@@ -1716,8 +1875,8 @@ function renderApiDocsHtml() {
|
|
|
1716
1875
|
<div class="flex items-center gap-3 sm:gap-4">
|
|
1717
1876
|
<a href="/" class="flex items-center gap-3 group select-none transition-transform active:scale-98 text-left">
|
|
1718
1877
|
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-br from-rose-500 via-pink-600 to-amber-400 p-[1.5px] shadow-lg shadow-rose-600/30 group-hover:shadow-rose-600/50 transition-all">
|
|
1719
|
-
<div class="w-full h-full rounded-[14px] bg-rose-50 dark:bg-[#12091c] flex items-center justify-center
|
|
1720
|
-
|
|
1878
|
+
<div class="w-full h-full rounded-[14px] bg-rose-50 dark:bg-[#12091c] flex items-center justify-center text-xl sm:text-2xl select-none leading-none group-hover:scale-110 transition-transform">
|
|
1879
|
+
🌸
|
|
1721
1880
|
</div>
|
|
1722
1881
|
</div>
|
|
1723
1882
|
<div class="flex flex-col">
|
|
@@ -1914,7 +2073,7 @@ function renderApiDocsHtml() {
|
|
|
1914
2073
|
npm package: kiku-agent
|
|
1915
2074
|
</span>
|
|
1916
2075
|
<span class="text-[11px] px-2 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-950/80 text-emerald-800 dark:text-emerald-300 font-mono font-bold border border-emerald-200 dark:border-emerald-800/60">
|
|
1917
|
-
|
|
2076
|
+
v${PACKAGE_VERSION} Ready
|
|
1918
2077
|
</span>
|
|
1919
2078
|
<span class="text-[11px] px-2 py-0.5 rounded-full bg-purple-100 dark:bg-purple-950/80 text-purple-800 dark:text-purple-300 font-mono font-bold border border-purple-200 dark:border-purple-800/60">
|
|
1920
2079
|
MCP Protocol
|
|
@@ -1924,7 +2083,7 @@ function renderApiDocsHtml() {
|
|
|
1924
2083
|
Kiku AI Agent CLI & MCP Server
|
|
1925
2084
|
</h2>
|
|
1926
2085
|
<p class="text-xs sm:text-sm text-stone-600 dark:text-stone-300 mt-1 max-w-3xl">
|
|
1927
|
-
Need an autonomous AI assistant or want to connect Ju Jingyi's knowledge base to Cursor, Claude Desktop, or VS Code? Use the official <strong>kiku-agent</strong> CLI & <strong>kiku-mcp</strong> server with
|
|
2086
|
+
Need an autonomous AI assistant or want to connect Ju Jingyi's knowledge base to Cursor, Claude Desktop, or VS Code? Use the official <strong>kiku-agent</strong> CLI & <strong>kiku-mcp</strong> server with ${toolsSchema.length} validated tools, local LM Studio (<code class="font-mono text-xs">google/gemma-4-e4b</code>) or cloud LLMs, and multi-agent debate.
|
|
1928
2087
|
</p>
|
|
1929
2088
|
</div>
|
|
1930
2089
|
</div>
|
|
@@ -1934,12 +2093,6 @@ function renderApiDocsHtml() {
|
|
|
1934
2093
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.981 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"/></svg>
|
|
1935
2094
|
<span>View on npm</span>
|
|
1936
2095
|
</a>
|
|
1937
|
-
<a href="/chat" class="px-4 py-2 rounded-xl bg-stone-100 hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700 text-stone-800 dark:text-stone-100 font-bold text-xs border border-stone-300 dark:border-stone-700 flex items-center gap-1.5 transition-all active:scale-95">
|
|
1938
|
-
<span>💬 Web Chat</span>
|
|
1939
|
-
</a>
|
|
1940
|
-
<a href="https://github.com/ThemeHackers/jujingyi_kikuuu" target="_blank" rel="noopener noreferrer" class="px-4 py-2 rounded-xl bg-stone-100 hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700 text-stone-800 dark:text-stone-100 font-bold text-xs border border-stone-300 dark:border-stone-700 flex items-center gap-1.5 transition-all active:scale-95">
|
|
1941
|
-
<span>GitHub</span>
|
|
1942
|
-
</a>
|
|
1943
2096
|
</div>
|
|
1944
2097
|
</div>
|
|
1945
2098
|
|
|
@@ -1952,7 +2105,7 @@ function renderApiDocsHtml() {
|
|
|
1952
2105
|
<span class="w-2.5 h-2.5 rounded-full bg-rose-500 animate-pulse"></span>
|
|
1953
2106
|
<h3 class="font-bold text-stone-900 dark:text-white text-base">Terminal AI Agent (CLI)</h3>
|
|
1954
2107
|
</div>
|
|
1955
|
-
<span class="font-mono text-xs px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-950 text-rose-700 dark:text-rose-300 border border-rose-200 dark:border-rose-800/60 font-semibold"
|
|
2108
|
+
<span class="font-mono text-xs px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-950 text-rose-700 dark:text-rose-300 border border-rose-200 dark:border-rose-800/60 font-semibold">${toolsSchema.length} Tools</span>
|
|
1956
2109
|
</div>
|
|
1957
2110
|
<p class="text-xs text-stone-600 dark:text-stone-300 leading-relaxed">
|
|
1958
2111
|
Autonomous ReAct agent with dynamic multiline input, live thinking reasoning streams, local/cloud LLM switching, background research jobs, YouTube music streaming, and multi-agent debate.
|
|
@@ -1994,7 +2147,7 @@ function renderApiDocsHtml() {
|
|
|
1994
2147
|
<span class="w-2.5 h-2.5 rounded-full bg-purple-500 animate-pulse"></span>
|
|
1995
2148
|
<h3 class="font-bold text-stone-900 dark:text-white text-base">Model Context Protocol (MCP)</h3>
|
|
1996
2149
|
</div>
|
|
1997
|
-
<span class="font-mono text-xs px-2 py-0.5 rounded bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800/60 font-semibold"
|
|
2150
|
+
<span class="font-mono text-xs px-2 py-0.5 rounded bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800/60 font-semibold">${mcpToolDefinitions.length} MCP Tools</span>
|
|
1998
2151
|
</div>
|
|
1999
2152
|
<p class="text-xs text-stone-600 dark:text-stone-300 leading-relaxed">
|
|
2000
2153
|
Connect Cursor, Claude Desktop, VS Code, or Antigravity directly to Ju Jingyi's full knowledge base using stdio or Streamable HTTP.
|
|
@@ -2024,11 +2177,11 @@ function renderApiDocsHtml() {
|
|
|
2024
2177
|
"mcpServers": {
|
|
2025
2178
|
"kiku": {
|
|
2026
2179
|
"command": "npx",
|
|
2027
|
-
"args": ["-y", "kiku-agent", "kiku-mcp"]
|
|
2180
|
+
"args": ["-y", "-p", "kiku-agent", "kiku-mcp"]
|
|
2028
2181
|
}
|
|
2029
2182
|
}
|
|
2030
2183
|
}</code></pre>
|
|
2031
|
-
<button type="button" onclick="copySnippet('{\\n \\\"mcpServers\\\": {\\n \\\"kiku\\\": {\\n \\\"command\\\": \\\"npx\\\",\\n \\\"args\\\": [\\\"-y\\\", \\\"kiku-agent\\\", \\\"kiku-mcp\\\"]\\n }\\n }\\n}', this)" class="absolute top-2 right-2 text-[10px] px-2 py-0.5 rounded bg-purple-800/80 hover:bg-purple-700 text-white cursor-pointer">Copy</button>
|
|
2184
|
+
<button type="button" onclick="copySnippet('{\\n \\\"mcpServers\\\": {\\n \\\"kiku\\\": {\\n \\\"command\\\": \\\"npx\\\",\\n \\\"args\\\": [\\\"-y\\\", \\\"-p\\\", \\\"kiku-agent\\\", \\\"kiku-mcp\\\"]\\n }\\n }\\n}', this)" class="absolute top-2 right-2 text-[10px] px-2 py-0.5 rounded bg-purple-800/80 hover:bg-purple-700 text-white cursor-pointer">Copy</button>
|
|
2032
2185
|
</div>
|
|
2033
2186
|
</div>
|
|
2034
2187
|
</div>
|
|
@@ -2159,8 +2312,8 @@ function renderApiDocsHtml() {
|
|
|
2159
2312
|
}
|
|
2160
2313
|
|
|
2161
2314
|
let allExpanded = false;
|
|
2162
|
-
let currentVipKey = '';
|
|
2163
|
-
try { localStorage.removeItem('kiku_vip_key'); } catch {}
|
|
2315
|
+
let currentVipKey = '';
|
|
2316
|
+
try { localStorage.removeItem('kiku_vip_key'); } catch {}
|
|
2164
2317
|
let quizLang = 'th';
|
|
2165
2318
|
|
|
2166
2319
|
const quizI18n = {
|
|
@@ -3121,6 +3274,8 @@ function renderApiDocsHtml() {
|
|
|
3121
3274
|
items: [
|
|
3122
3275
|
{ method: "GET", path: "/health", desc: "API health, uptime, and database status (Public & VIP)", example: "/health" },
|
|
3123
3276
|
{ method: "GET", path: "/stats", desc: "Platform statistical overview & catalog counts", example: "/stats" },
|
|
3277
|
+
{ method: "GET", path: "/system/hardware", desc: "Detected GPU/VRAM hardware report used by the predictive VRAM engine", example: "/system/hardware" },
|
|
3278
|
+
{ method: "POST", path: "/models/estimate-vram", desc: "Predictive VRAM footprint estimation & feasibility zone (weights + KV cache + overhead)", example: "/models/estimate-vram", defaultBody: '{\\n "model": "qwen2.5-7b-instruct",\\n "quant": "q4_k_m",\\n "contextLength": 16384\\n}' },
|
|
3124
3279
|
{ method: "GET", path: "/quiz/challenge", desc: "Request one-time anti-cheat challenge token for VIP quiz", example: "/quiz/challenge" },
|
|
3125
3280
|
{ method: "POST", path: "/keys/claim", desc: "Claim Developer VIP Key upon 15/15 quiz completion", example: "/keys/claim", defaultBody: '{\\n "score": 15,\\n "answers": ["A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C"]\\n}', requiresQuiz: true },
|
|
3126
3281
|
{ method: "GET", path: "/keys/generate", desc: "Generate Developer API Key (Requires Master Key in X-API-Key)", example: "/keys/generate", requiresKey: true },
|
|
@@ -3281,6 +3436,8 @@ function renderApiDocsHtml() {
|
|
|
3281
3436
|
</script>
|
|
3282
3437
|
</body>
|
|
3283
3438
|
</html>`;
|
|
3439
|
+
apiDocsHtmlCache = html;
|
|
3440
|
+
return html;
|
|
3284
3441
|
}
|
|
3285
3442
|
|
|
3286
3443
|
function safeDecode(str) {
|
|
@@ -3292,16 +3449,16 @@ function safeDecode(str) {
|
|
|
3292
3449
|
}
|
|
3293
3450
|
}
|
|
3294
3451
|
|
|
3295
|
-
export async function handleApiRequest(req, res, next) {
|
|
3296
|
-
const trustProxy = process.env.VERCEL === '1' || process.env.KIKU_TRUST_PROXY === 'true';
|
|
3297
|
-
const rawHost = trustProxy ? (req.headers['x-forwarded-host'] || req.headers.host) : req.headers.host;
|
|
3298
|
-
const hostCandidate = String(rawHost || '').split(',')[0].trim();
|
|
3299
|
-
const host = /^[a-z0-9.-]+(?::\d{1,5})?$/i.test(hostCandidate)
|
|
3300
|
-
? hostCandidate
|
|
3301
|
-
: 'jujingyi-kikuuu.vercel.app';
|
|
3302
|
-
const isLocal = host.includes('localhost') || host.includes('127.0.0.1') || host.endsWith('.local');
|
|
3303
|
-
const forwardedProtocol = trustProxy ? String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim() : '';
|
|
3304
|
-
const protocol = ['http', 'https'].includes(forwardedProtocol) ? forwardedProtocol : (isLocal ? 'http' : 'https');
|
|
3452
|
+
export async function handleApiRequest(req, res, next) {
|
|
3453
|
+
const trustProxy = process.env.VERCEL === '1' || process.env.KIKU_TRUST_PROXY === 'true';
|
|
3454
|
+
const rawHost = trustProxy ? (req.headers['x-forwarded-host'] || req.headers.host) : req.headers.host;
|
|
3455
|
+
const hostCandidate = String(rawHost || '').split(',')[0].trim();
|
|
3456
|
+
const host = /^[a-z0-9.-]+(?::\d{1,5})?$/i.test(hostCandidate)
|
|
3457
|
+
? hostCandidate
|
|
3458
|
+
: 'jujingyi-kikuuu.vercel.app';
|
|
3459
|
+
const isLocal = host.includes('localhost') || host.includes('127.0.0.1') || host.endsWith('.local');
|
|
3460
|
+
const forwardedProtocol = trustProxy ? String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim() : '';
|
|
3461
|
+
const protocol = ['http', 'https'].includes(forwardedProtocol) ? forwardedProtocol : (isLocal ? 'http' : 'https');
|
|
3305
3462
|
const baseUrl = `${protocol}://${host}`;
|
|
3306
3463
|
const { isConfigured: isSupabaseConfigured } = getSupabaseConfig();
|
|
3307
3464
|
|
|
@@ -3325,34 +3482,35 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3325
3482
|
return;
|
|
3326
3483
|
}
|
|
3327
3484
|
|
|
3328
|
-
if (pathname === '/api/v1/openapi.json' || pathname === '/api/v1/swagger.json') {
|
|
3485
|
+
if (pathname === '/openapi.json' || pathname === '/api/v1/openapi.json' || pathname === '/api/v1/swagger.json') {
|
|
3329
3486
|
return sendJson(res, 200, getOpenApiSpec(baseUrl));
|
|
3330
3487
|
}
|
|
3331
3488
|
|
|
3332
|
-
if (!pathname.startsWith('/api/v1') && pathname !== '/api' && !pathname.startsWith('/mcp')) {
|
|
3489
|
+
if (!pathname.startsWith('/api/v1') && pathname !== '/api' && pathname !== '/openapi.json' && !pathname.startsWith('/mcp')) {
|
|
3333
3490
|
if (next) return next();
|
|
3334
3491
|
return;
|
|
3335
3492
|
}
|
|
3336
3493
|
|
|
3337
3494
|
if (req.method === 'OPTIONS') {
|
|
3338
3495
|
res.statusCode = 204;
|
|
3339
|
-
applySecurityHeaders(res, false);
|
|
3496
|
+
applySecurityHeaders(res, false, req);
|
|
3340
3497
|
res.end();
|
|
3341
3498
|
return;
|
|
3342
3499
|
}
|
|
3343
3500
|
|
|
3344
|
-
const clientIp = resolveClientIp(req);
|
|
3501
|
+
const clientIp = resolveClientIp(req);
|
|
3345
3502
|
const searchParams = urlObj.searchParams;
|
|
3346
3503
|
const apiKey = extractApiKey(req);
|
|
3347
3504
|
const validKeyInfo = apiKey ? await validateApiKey(apiKey) : null;
|
|
3348
|
-
const hasValidApiKey = Boolean(validKeyInfo);
|
|
3349
|
-
const hasAgentExecutionAccess = Boolean(apiKey && (isMasterKey(apiKey) || agentExecutionKeys.has(apiKey.trim())));
|
|
3505
|
+
const hasValidApiKey = Boolean(validKeyInfo);
|
|
3506
|
+
const hasAgentExecutionAccess = Boolean(apiKey && (isMasterKey(apiKey) || agentExecutionKeys.has(apiKey.trim())));
|
|
3350
3507
|
|
|
3351
3508
|
const baseRateLimit = validKeyInfo?.rateLimit || (hasValidApiKey ? RATE_LIMIT_API_KEY : RATE_LIMIT_ANONYMOUS);
|
|
3352
|
-
const
|
|
3353
|
-
const
|
|
3509
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
3510
|
+
const parsedTestLimit = !isProduction ? Number.parseInt(searchParams.get('testRateLimit') || '', 10) : NaN;
|
|
3511
|
+
const hasTestRateLimit = !isProduction && Number.isInteger(parsedTestLimit) && parsedTestLimit > 0;
|
|
3354
3512
|
const maxLimit = hasTestRateLimit ? Math.min(parsedTestLimit, baseRateLimit) : baseRateLimit;
|
|
3355
|
-
const baseClientIdentifier = hasValidApiKey ? `key:${hashApiKey(apiKey)}` : `ip:${clientIp}`;
|
|
3513
|
+
const baseClientIdentifier = hasValidApiKey ? `key:${hashApiKey(apiKey)}` : `ip:${clientIp}`;
|
|
3356
3514
|
const clientIdentifier = hasTestRateLimit
|
|
3357
3515
|
? `${baseClientIdentifier}:test:${maxLimit}`
|
|
3358
3516
|
: baseClientIdentifier;
|
|
@@ -3417,7 +3575,7 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3417
3575
|
}, rateHeaders);
|
|
3418
3576
|
}
|
|
3419
3577
|
|
|
3420
|
-
if (pathname === '/api/v1/quiz/challenge') {
|
|
3578
|
+
if (pathname === '/api/v1/quiz/challenge') {
|
|
3421
3579
|
const session = createQuizChallengeToken();
|
|
3422
3580
|
return sendJson(res, 200, {
|
|
3423
3581
|
status: 'success',
|
|
@@ -3430,14 +3588,12 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3430
3588
|
targetTier: 'Developer VIP Tier (1,000 req/min)',
|
|
3431
3589
|
usage: "Send this challengeToken along with your 15 completed quiz answers to POST /api/v1/keys/claim"
|
|
3432
3590
|
}
|
|
3433
|
-
}, { ...rateHeaders, 'Cache-Control': 'no-store', Pragma: 'no-cache' });
|
|
3591
|
+
}, { ...rateHeaders, 'Cache-Control': 'no-store', Pragma: 'no-cache' });
|
|
3434
3592
|
}
|
|
3435
3593
|
|
|
3436
3594
|
if (pathname === '/api/v1/keys/generate') {
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3439
|
-
const isAuthorized = isMasterKey(adminKey) || (adminKeyValid && adminKeyValid.tier === 'Master VIP');
|
|
3440
|
-
if (!adminKey || !adminKeyValid || !isAuthorized) {
|
|
3595
|
+
const isAuthorized = isMasterKey(apiKey) || (validKeyInfo && validKeyInfo.tier === 'Master VIP');
|
|
3596
|
+
if (!apiKey || !validKeyInfo || !isAuthorized) {
|
|
3441
3597
|
return sendJson(res, 401, {
|
|
3442
3598
|
status: 'error',
|
|
3443
3599
|
code: 401,
|
|
@@ -3445,9 +3601,9 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3445
3601
|
message: 'Unauthorized. Admin/Master API Key required in X-API-Key header to generate new developer keys.'
|
|
3446
3602
|
}, rateHeaders);
|
|
3447
3603
|
}
|
|
3448
|
-
const token = crypto.randomBytes(16).toString('hex');
|
|
3449
|
-
const newKey = `kiku_dev_sk_${token}`;
|
|
3450
|
-
await saveApiKeyToSupabase({ apiKey: newKey, tier: 'developer', rateLimit: 1000 });
|
|
3604
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
3605
|
+
const newKey = `kiku_dev_sk_${token}`;
|
|
3606
|
+
await saveApiKeyToSupabase({ apiKey: newKey, tier: 'developer', rateLimit: 1000 });
|
|
3451
3607
|
return sendJson(res, 200, {
|
|
3452
3608
|
status: 'success',
|
|
3453
3609
|
code: 200,
|
|
@@ -3459,7 +3615,7 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3459
3615
|
storage: isSupabaseConfigured ? 'supabase-postgresql (persistent)' : 'in-memory (standalone)',
|
|
3460
3616
|
usageInstruction: "Attach header 'X-API-Key: <your_key>' or 'Authorization: Bearer <your_key>'"
|
|
3461
3617
|
}
|
|
3462
|
-
}, { ...rateHeaders, 'Cache-Control': 'no-store', Pragma: 'no-cache' });
|
|
3618
|
+
}, { ...rateHeaders, 'Cache-Control': 'no-store', Pragma: 'no-cache' });
|
|
3463
3619
|
}
|
|
3464
3620
|
|
|
3465
3621
|
if (pathname === '/api/v1/keys/claim') {
|
|
@@ -3510,8 +3666,8 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3510
3666
|
|
|
3511
3667
|
consumeQuizChallengeToken(body.challengeToken);
|
|
3512
3668
|
|
|
3513
|
-
const token = crypto.randomBytes(16).toString('hex');
|
|
3514
|
-
const newVipKey = `kiku_vip_sk_${token}`;
|
|
3669
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
3670
|
+
const newVipKey = `kiku_vip_sk_${token}`;
|
|
3515
3671
|
await saveApiKeyToSupabase({ apiKey: newVipKey, tier: 'developer_vip', rateLimit: 1000 });
|
|
3516
3672
|
|
|
3517
3673
|
return sendJson(res, 200, {
|
|
@@ -3528,11 +3684,11 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3528
3684
|
achievement: 'Ju Jingyi Miju Master (15/15 Perfect Score 🌸)',
|
|
3529
3685
|
usageInstruction: "Attach header 'X-API-Key: <your_key>' or 'Authorization: Bearer <your_key>'"
|
|
3530
3686
|
}
|
|
3531
|
-
}, { ...rateHeaders, 'Cache-Control': 'no-store', Pragma: 'no-cache' });
|
|
3687
|
+
}, { ...rateHeaders, 'Cache-Control': 'no-store', Pragma: 'no-cache' });
|
|
3532
3688
|
}
|
|
3533
3689
|
|
|
3534
3690
|
if (pathname === '/api/v1/stats') {
|
|
3535
|
-
const cleanSongs =
|
|
3691
|
+
const cleanSongs = CLEAN_DISCOGRAPHY;
|
|
3536
3692
|
return sendJson(res, 200, {
|
|
3537
3693
|
status: 'success',
|
|
3538
3694
|
code: 200,
|
|
@@ -3676,14 +3832,14 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3676
3832
|
}, rateHeaders);
|
|
3677
3833
|
}
|
|
3678
3834
|
|
|
3679
|
-
if (pathname === '/api/v1/agent/query' || pathname === '/api/v1/chat') {
|
|
3680
|
-
if (!hasValidApiKey || !hasAgentExecutionAccess) {
|
|
3681
|
-
return sendJson(res, 401, {
|
|
3682
|
-
status: 'error', code: 401, timestamp,
|
|
3683
|
-
message: 'An agent-execution API key is required for model execution.'
|
|
3684
|
-
}, rateHeaders);
|
|
3685
|
-
}
|
|
3686
|
-
const body = req.method === 'POST' ? await parseJsonBody(req) : {};
|
|
3835
|
+
if (pathname === '/api/v1/agent/query' || pathname === '/api/v1/chat') {
|
|
3836
|
+
if (!hasValidApiKey || !hasAgentExecutionAccess) {
|
|
3837
|
+
return sendJson(res, 401, {
|
|
3838
|
+
status: 'error', code: 401, timestamp,
|
|
3839
|
+
message: 'An agent-execution API key is required for model execution.'
|
|
3840
|
+
}, rateHeaders);
|
|
3841
|
+
}
|
|
3842
|
+
const body = req.method === 'POST' ? await parseJsonBody(req) : {};
|
|
3687
3843
|
const rawQuery = body.query || body.prompt || searchParams.get('q') || searchParams.get('query') || '';
|
|
3688
3844
|
const query = sanitizeSearchQuery(rawQuery, 2000);
|
|
3689
3845
|
if (!query) {
|
|
@@ -3697,19 +3853,19 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3697
3853
|
|
|
3698
3854
|
try {
|
|
3699
3855
|
const { getSharedAgent } = await import('./routes/agent.js');
|
|
3700
|
-
const provider = sanitizeSearchQuery(body.provider || 'lmstudio', 40).toLowerCase();
|
|
3701
|
-
const providerApiKey = extractProviderApiKey(req);
|
|
3702
|
-
if (provider !== 'lmstudio' && !providerApiKey) {
|
|
3703
|
-
return sendJson(res, 401, {
|
|
3704
|
-
status: 'error', code: 401, timestamp,
|
|
3705
|
-
message: 'X-Provider-API-Key is required for cloud model providers.'
|
|
3706
|
-
}, rateHeaders);
|
|
3707
|
-
}
|
|
3708
|
-
const agent = getSharedAgent({
|
|
3709
|
-
provider,
|
|
3710
|
-
model: sanitizeSearchQuery(body.model || '', 160) || undefined,
|
|
3711
|
-
apiKey: providerApiKey || undefined,
|
|
3712
|
-
});
|
|
3856
|
+
const provider = sanitizeSearchQuery(body.provider || 'lmstudio', 40).toLowerCase();
|
|
3857
|
+
const providerApiKey = extractProviderApiKey(req);
|
|
3858
|
+
if (provider !== 'lmstudio' && !providerApiKey) {
|
|
3859
|
+
return sendJson(res, 401, {
|
|
3860
|
+
status: 'error', code: 401, timestamp,
|
|
3861
|
+
message: 'X-Provider-API-Key is required for cloud model providers.'
|
|
3862
|
+
}, rateHeaders);
|
|
3863
|
+
}
|
|
3864
|
+
const agent = getSharedAgent({
|
|
3865
|
+
provider,
|
|
3866
|
+
model: sanitizeSearchQuery(body.model || '', 160) || undefined,
|
|
3867
|
+
apiKey: providerApiKey || undefined,
|
|
3868
|
+
});
|
|
3713
3869
|
await agent.ready();
|
|
3714
3870
|
let collectedAnswer = '';
|
|
3715
3871
|
await agent.ask(query, {
|
|
@@ -3733,13 +3889,13 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3733
3889
|
}
|
|
3734
3890
|
}
|
|
3735
3891
|
|
|
3736
|
-
if (pathname === '/api/v1/agent/chat' || pathname === '/api/v1/chat/stream') {
|
|
3737
|
-
if (!hasValidApiKey || !hasAgentExecutionAccess) {
|
|
3738
|
-
return sendJson(res, 401, {
|
|
3739
|
-
status: 'error', code: 401, timestamp,
|
|
3740
|
-
message: 'An agent-execution API key is required for model execution.'
|
|
3741
|
-
}, rateHeaders);
|
|
3742
|
-
}
|
|
3892
|
+
if (pathname === '/api/v1/agent/chat' || pathname === '/api/v1/chat/stream') {
|
|
3893
|
+
if (!hasValidApiKey || !hasAgentExecutionAccess) {
|
|
3894
|
+
return sendJson(res, 401, {
|
|
3895
|
+
status: 'error', code: 401, timestamp,
|
|
3896
|
+
message: 'An agent-execution API key is required for model execution.'
|
|
3897
|
+
}, rateHeaders);
|
|
3898
|
+
}
|
|
3743
3899
|
const rawQ = searchParams.get('q') || searchParams.get('query') || '';
|
|
3744
3900
|
const q = sanitizeSearchQuery(rawQ, 2000);
|
|
3745
3901
|
if (!q) {
|
|
@@ -3750,29 +3906,73 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3750
3906
|
message: "Missing 'q' or 'query' parameter for agent chat stream."
|
|
3751
3907
|
}, rateHeaders);
|
|
3752
3908
|
}
|
|
3753
|
-
const provider = sanitizeSearchQuery(searchParams.get('provider') || 'lmstudio', 40).toLowerCase();
|
|
3754
|
-
const providerApiKey = extractProviderApiKey(req);
|
|
3755
|
-
if (provider !== 'lmstudio' && !providerApiKey) {
|
|
3756
|
-
return sendJson(res, 401, {
|
|
3757
|
-
status: 'error', code: 401, timestamp,
|
|
3758
|
-
message: 'X-Provider-API-Key is required for cloud model providers.'
|
|
3759
|
-
}, rateHeaders);
|
|
3760
|
-
}
|
|
3761
|
-
const { handleAgentChatStream } = await import('./routes/agent.js');
|
|
3762
|
-
return handleAgentChatStream(req, res, q, {
|
|
3763
|
-
provider,
|
|
3764
|
-
model: sanitizeSearchQuery(searchParams.get('model') || '', 160) || undefined,
|
|
3765
|
-
apiKey: providerApiKey || undefined,
|
|
3766
|
-
});
|
|
3909
|
+
const provider = sanitizeSearchQuery(searchParams.get('provider') || 'lmstudio', 40).toLowerCase();
|
|
3910
|
+
const providerApiKey = extractProviderApiKey(req);
|
|
3911
|
+
if (provider !== 'lmstudio' && !providerApiKey) {
|
|
3912
|
+
return sendJson(res, 401, {
|
|
3913
|
+
status: 'error', code: 401, timestamp,
|
|
3914
|
+
message: 'X-Provider-API-Key is required for cloud model providers.'
|
|
3915
|
+
}, rateHeaders);
|
|
3916
|
+
}
|
|
3917
|
+
const { handleAgentChatStream } = await import('./routes/agent.js');
|
|
3918
|
+
return handleAgentChatStream(req, res, q, {
|
|
3919
|
+
provider,
|
|
3920
|
+
model: sanitizeSearchQuery(searchParams.get('model') || '', 160) || undefined,
|
|
3921
|
+
apiKey: providerApiKey || undefined,
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
if (pathname === '/api/v1/system/hardware') {
|
|
3926
|
+
try {
|
|
3927
|
+
const { buildHardwareReport } = await import('./agent/vramService.js');
|
|
3928
|
+
const report = await buildHardwareReport();
|
|
3929
|
+
return sendJson(res, 200, {
|
|
3930
|
+
status: 'success',
|
|
3931
|
+
code: 200,
|
|
3932
|
+
timestamp,
|
|
3933
|
+
data: report
|
|
3934
|
+
}, rateHeaders);
|
|
3935
|
+
} catch (err) {
|
|
3936
|
+
console.error('[Hardware API Error]:', err);
|
|
3937
|
+
return sendJson(res, 500, {
|
|
3938
|
+
status: 'error',
|
|
3939
|
+
code: 500,
|
|
3940
|
+
timestamp,
|
|
3941
|
+
message: safeErrorMessage(err, 'Hardware probe failed')
|
|
3942
|
+
}, rateHeaders);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
if (pathname === '/api/v1/models/estimate-vram') {
|
|
3947
|
+
try {
|
|
3948
|
+
const body = req.method === 'POST' ? await parseJsonBody(req) : {};
|
|
3949
|
+
const request = parseEstimateRequest(body, searchParams);
|
|
3950
|
+
const { computeVramEstimate } = await import('./agent/vramService.js');
|
|
3951
|
+
const estimate = await computeVramEstimate(request);
|
|
3952
|
+
return sendJson(res, 200, {
|
|
3953
|
+
status: 'success',
|
|
3954
|
+
code: 200,
|
|
3955
|
+
timestamp,
|
|
3956
|
+
data: estimate
|
|
3957
|
+
}, rateHeaders);
|
|
3958
|
+
} catch (err) {
|
|
3959
|
+
console.error('[VRAM Estimate API Error]:', err);
|
|
3960
|
+
return sendJson(res, 500, {
|
|
3961
|
+
status: 'error',
|
|
3962
|
+
code: 500,
|
|
3963
|
+
timestamp,
|
|
3964
|
+
message: safeErrorMessage(err, 'VRAM estimation failed')
|
|
3965
|
+
}, rateHeaders);
|
|
3966
|
+
}
|
|
3767
3967
|
}
|
|
3768
3968
|
|
|
3769
|
-
if (pathname === '/api/v1/debate') {
|
|
3770
|
-
if (!hasValidApiKey || !hasAgentExecutionAccess) {
|
|
3771
|
-
return sendJson(res, 401, {
|
|
3772
|
-
status: 'error', code: 401, timestamp,
|
|
3773
|
-
message: 'An agent-execution API key is required for debate execution.'
|
|
3774
|
-
}, rateHeaders);
|
|
3775
|
-
}
|
|
3969
|
+
if (pathname === '/api/v1/debate') {
|
|
3970
|
+
if (!hasValidApiKey || !hasAgentExecutionAccess) {
|
|
3971
|
+
return sendJson(res, 401, {
|
|
3972
|
+
status: 'error', code: 401, timestamp,
|
|
3973
|
+
message: 'An agent-execution API key is required for debate execution.'
|
|
3974
|
+
}, rateHeaders);
|
|
3975
|
+
}
|
|
3776
3976
|
const body = req.method === 'POST' ? await parseJsonBody(req) : {};
|
|
3777
3977
|
const rawTopic = body.topic || body.claim || searchParams.get('topic') || searchParams.get('claim') || searchParams.get('q') || '';
|
|
3778
3978
|
const topic = sanitizeSearchQuery(rawTopic, 500);
|
|
@@ -3868,7 +4068,7 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3868
4068
|
}
|
|
3869
4069
|
|
|
3870
4070
|
if (pathname === '/api/v1/discography') {
|
|
3871
|
-
let cleanSongs =
|
|
4071
|
+
let cleanSongs = CLEAN_DISCOGRAPHY;
|
|
3872
4072
|
const type = sanitizeSearchQuery(searchParams.get('type') || '', 50);
|
|
3873
4073
|
const year = sanitizeSearchQuery(searchParams.get('year') || '', 20);
|
|
3874
4074
|
const search = sanitizeSearchQuery(searchParams.get('search') || '', 100);
|
|
@@ -3900,7 +4100,7 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3900
4100
|
timestamp,
|
|
3901
4101
|
data: cleanSongs,
|
|
3902
4102
|
meta: {
|
|
3903
|
-
totalUniqueTracks:
|
|
4103
|
+
totalUniqueTracks: CLEAN_DISCOGRAPHY.length,
|
|
3904
4104
|
count: cleanSongs.length
|
|
3905
4105
|
}
|
|
3906
4106
|
}, rateHeaders);
|
|
@@ -3909,7 +4109,7 @@ export async function handleApiRequest(req, res, next) {
|
|
|
3909
4109
|
if (pathname.startsWith('/api/v1/discography/')) {
|
|
3910
4110
|
const rawId = pathname.replace('/api/v1/discography/', '').trim();
|
|
3911
4111
|
const id = safeDecode(rawId);
|
|
3912
|
-
const cleanSongs =
|
|
4112
|
+
const cleanSongs = CLEAN_DISCOGRAPHY;
|
|
3913
4113
|
const song = cleanSongs.find(s =>
|
|
3914
4114
|
String(s.id) === id ||
|
|
3915
4115
|
String(s.id) === rawId ||
|
|
@@ -4122,7 +4322,7 @@ export async function handleApiRequest(req, res, next) {
|
|
|
4122
4322
|
(w.year && String(w.year).includes(q))
|
|
4123
4323
|
);
|
|
4124
4324
|
|
|
4125
|
-
const cleanSongs =
|
|
4325
|
+
const cleanSongs = CLEAN_DISCOGRAPHY;
|
|
4126
4326
|
const matchedSongs = cleanSongs.filter(s =>
|
|
4127
4327
|
(s.titleTh && s.titleTh.toLowerCase().includes(q)) ||
|
|
4128
4328
|
(s.titleCn && s.titleCn.includes(q)) ||
|