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
|
@@ -39,6 +39,7 @@ export async function handleClarifyAnalyticsRoute(req, res, options = {}) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export async function handleClarifyConfigRoute(req, res, options = {}, action = 'get') {
|
|
42
|
+
if (!isWebChatAdmin(req)) return sendError(res, 401, 'Admin API key required');
|
|
42
43
|
try {
|
|
43
44
|
const config = getConfig();
|
|
44
45
|
|
|
@@ -48,8 +49,6 @@ export async function handleClarifyConfigRoute(req, res, options = {}, action =
|
|
|
48
49
|
config: config.getConfig(),
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
if (!isWebChatAdmin(req)) return sendError(res, 401, 'Admin API key required');
|
|
53
52
|
|
|
54
53
|
if (action === 'reset') {
|
|
55
54
|
const newConfig = config.reset();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeWebSessionId } from '../core/connection.js';
|
|
2
2
|
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
3
|
+
import { PACKAGE_VERSION } from '../../agent/version.js';
|
|
3
4
|
|
|
4
5
|
export async function handleExportRoute(req, res) {
|
|
5
6
|
let body;
|
|
@@ -19,7 +20,7 @@ export async function handleExportRoute(req, res) {
|
|
|
19
20
|
exportedAt: new Date().toISOString(),
|
|
20
21
|
sessionId: normSessionId,
|
|
21
22
|
messages: history,
|
|
22
|
-
metadata: { app: '鞠婧祎', version:
|
|
23
|
+
metadata: { app: '鞠婧祎', version: PACKAGE_VERSION }
|
|
23
24
|
};
|
|
24
25
|
res.writeHead(200, {
|
|
25
26
|
'Content-Type': 'application/json; charset=utf-8',
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
2
|
+
import { scanLan, mapServerTypeToProvider } from '../../agent/lanDiscovery.js';
|
|
3
|
+
import { isPrivateIPv4 } from '../../agent/sanitizer.js';
|
|
4
|
+
import { resolveClientIp, isWebChatAdmin } from '../core/security.js';
|
|
5
|
+
import {
|
|
6
|
+
getProvider,
|
|
7
|
+
getUnifiedProviderCatalog,
|
|
8
|
+
savePersistedProviderBaseUrl,
|
|
9
|
+
setActiveProvider,
|
|
10
|
+
} from '../../agent/config.js';
|
|
11
|
+
import { currentAgent, setCurrentAgent } from '../core/connection.js';
|
|
12
|
+
|
|
13
|
+
function lanDiscoveryAllowed(req) {
|
|
14
|
+
if (String(process.env.KIKU_ALLOW_REMOTE_LAN_DISCOVERY || '').toLowerCase() === 'true') return true;
|
|
15
|
+
if (isWebChatAdmin(req)) return true;
|
|
16
|
+
const ip = resolveClientIp(req);
|
|
17
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleLanDiscoveryRoute(req, res) {
|
|
21
|
+
if (!lanDiscoveryAllowed(req)) {
|
|
22
|
+
return sendError(res, 403, 'LAN discovery is restricted to the local machine. Set KIKU_ALLOW_REMOTE_LAN_DISCOVERY=true to allow other devices.');
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const urlObj = new URL(req.url, 'http://localhost');
|
|
26
|
+
const subnetsParam = urlObj.searchParams.get('subnets');
|
|
27
|
+
const portsParam = urlObj.searchParams.get('ports');
|
|
28
|
+
const options = {};
|
|
29
|
+
if (subnetsParam && subnetsParam.trim()) {
|
|
30
|
+
options.subnets = subnetsParam.split(',').map(s => s.trim()).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
if (portsParam && portsParam.trim()) {
|
|
33
|
+
options.ports = portsParam.split(',').map(Number).filter(p => Number.isInteger(p) && p >= 1 && p <= 65535);
|
|
34
|
+
}
|
|
35
|
+
const result = await scanLan(options);
|
|
36
|
+
sendJson(res, 200, { status: 'ok', ...result });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
sendError(res, 400, err.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function handleLanApplyRoute(req, res) {
|
|
43
|
+
if (!lanDiscoveryAllowed(req)) {
|
|
44
|
+
return sendError(res, 403, 'LAN discovery is restricted to the local machine. Set KIKU_ALLOW_REMOTE_LAN_DISCOVERY=true to allow other devices.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let body;
|
|
48
|
+
try {
|
|
49
|
+
body = await parseJsonBody(req, 16 * 1024);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return sendError(res, 400, `Invalid JSON body: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const host = String(body.host || '').trim();
|
|
55
|
+
const port = Number(body.port);
|
|
56
|
+
const model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : null;
|
|
57
|
+
const serverType = String(body.serverType || 'openai-compatible').toLowerCase();
|
|
58
|
+
|
|
59
|
+
if (!/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(host) || !isPrivateIPv4(host)) {
|
|
60
|
+
return sendError(res, 400, 'host must be a private IPv4 address discovered by the LAN scan');
|
|
61
|
+
}
|
|
62
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
63
|
+
return sendError(res, 400, 'port must be an integer between 1 and 65535');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const provider = mapServerTypeToProvider(serverType);
|
|
67
|
+
if (!getProvider(provider)) {
|
|
68
|
+
return sendError(res, 400, `Unknown provider "${provider}"`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const baseUrl = `http://${host}:${port}/v1`;
|
|
72
|
+
savePersistedProviderBaseUrl(provider, baseUrl);
|
|
73
|
+
setActiveProvider(provider, { model, baseUrl });
|
|
74
|
+
|
|
75
|
+
if (currentAgent?.client) {
|
|
76
|
+
try {
|
|
77
|
+
currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl });
|
|
78
|
+
setCurrentAgent(currentAgent);
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const catalog = getUnifiedProviderCatalog();
|
|
83
|
+
sendJson(res, 200, {
|
|
84
|
+
status: 'ok',
|
|
85
|
+
applied: { provider, baseUrl, model },
|
|
86
|
+
activeProvider: catalog.activeProvider,
|
|
87
|
+
activeBaseUrl: catalog.activeBaseUrl,
|
|
88
|
+
activeModel: catalog.activeModel,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { socialUpdatesData } from '../../../src/data/socialUpdatesData.js';
|
|
2
2
|
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
3
|
+
import { searchNews } from '../../agent/webSearch.js';
|
|
4
|
+
import { rankNewsItems } from '../../agent/newsIntelligence.js';
|
|
5
|
+
|
|
6
|
+
const LIVE_TIMEOUT_MS = 6000;
|
|
7
|
+
|
|
8
|
+
async function fetchLiveItems(query, days) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => controller.abort(), LIVE_TIMEOUT_MS);
|
|
11
|
+
try {
|
|
12
|
+
const result = await searchNews(query, 12);
|
|
13
|
+
if (result?.status !== 'success') return [];
|
|
14
|
+
const cutoff = days > 0 ? new Date(Date.now() - days * 86400000) : null;
|
|
15
|
+
return rankNewsItems(result.results || [], { cutoff })
|
|
16
|
+
.filter(item => !cutoff || !item.date || item.date.getTime() >= cutoff.getTime())
|
|
17
|
+
.map(item => ({
|
|
18
|
+
title: item.title,
|
|
19
|
+
summary: item.snippet || '',
|
|
20
|
+
date: item.date ? item.date.toISOString().slice(0, 10) : null,
|
|
21
|
+
platform: item.tierLabel || 'news',
|
|
22
|
+
url: item.url,
|
|
23
|
+
}));
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
} finally {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
3
30
|
|
|
4
31
|
export async function handleNewsRoute(req, res) {
|
|
5
32
|
let body = {};
|
|
@@ -8,12 +35,13 @@ export async function handleNewsRoute(req, res) {
|
|
|
8
35
|
} catch {}
|
|
9
36
|
|
|
10
37
|
const { query = 'จวีจิ้งอี Ju Jingyi', days = 30 } = body;
|
|
11
|
-
let newsItems = [];
|
|
12
|
-
const source = 'social';
|
|
13
38
|
|
|
39
|
+
const liveItems = await fetchLiveItems(String(query || 'จวีจิ้งอี Ju Jingyi'), Number(days) || 30);
|
|
40
|
+
|
|
41
|
+
let socialItems = [];
|
|
14
42
|
try {
|
|
15
43
|
const cards = socialUpdatesData?.douyin?.cards || socialUpdatesData?.cards || [];
|
|
16
|
-
|
|
44
|
+
socialItems = cards.map(c => ({
|
|
17
45
|
title: c.title || c.titleCn,
|
|
18
46
|
summary: c.captionTh || c.captionCn || c.content,
|
|
19
47
|
date: c.date,
|
|
@@ -22,12 +50,14 @@ export async function handleNewsRoute(req, res) {
|
|
|
22
50
|
}));
|
|
23
51
|
} catch {}
|
|
24
52
|
|
|
53
|
+
const items = [...liveItems, ...socialItems.slice(0, 6)];
|
|
25
54
|
sendJson(res, 200, {
|
|
26
55
|
status: 'ok',
|
|
27
56
|
query,
|
|
28
57
|
days,
|
|
29
|
-
items:
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
items: items.slice(0, 12),
|
|
59
|
+
liveCount: liveItems.length,
|
|
60
|
+
source: liveItems.length > 0 ? 'live+social' : 'social',
|
|
61
|
+
count: items.length
|
|
32
62
|
});
|
|
33
63
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
2
|
+
import {
|
|
3
|
+
getUnifiedProviderCatalog,
|
|
4
|
+
getProviderBaseUrl,
|
|
5
|
+
setActiveProvider,
|
|
6
|
+
savePersistedProviderBaseUrl,
|
|
7
|
+
getProvider,
|
|
8
|
+
} from '../../agent/config.js';
|
|
9
|
+
import { currentAgent, setCurrentAgent } from '../core/connection.js';
|
|
10
|
+
import { LLMClient } from '../../agent/llmClient.js';
|
|
11
|
+
|
|
12
|
+
export async function handleProviderGetRoute(req, res) {
|
|
13
|
+
try {
|
|
14
|
+
const catalog = getUnifiedProviderCatalog();
|
|
15
|
+
sendJson(res, 200, {
|
|
16
|
+
status: 'ok',
|
|
17
|
+
activeProvider: catalog.activeProvider,
|
|
18
|
+
activeBaseUrl: catalog.activeBaseUrl,
|
|
19
|
+
activeModel: catalog.activeModel,
|
|
20
|
+
localEndpoints: catalog.localEndpoints,
|
|
21
|
+
providers: catalog.providers,
|
|
22
|
+
});
|
|
23
|
+
} catch (err) {
|
|
24
|
+
sendError(res, 500, err.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleProviderUpdateRoute(req, res) {
|
|
29
|
+
let body;
|
|
30
|
+
try {
|
|
31
|
+
body = await parseJsonBody(req);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return sendError(res, 400, `Invalid JSON body: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const provider = String(body.provider || body.activeProvider || '').toLowerCase().trim() || null;
|
|
37
|
+
const model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : null;
|
|
38
|
+
const baseUrl = typeof body.baseUrl === 'string' && body.baseUrl.trim() ? body.baseUrl.trim().replace(/\/+$/, '') : null;
|
|
39
|
+
|
|
40
|
+
if (baseUrl && !provider) {
|
|
41
|
+
return sendError(res, 400, 'Provider is required when setting a baseUrl');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (baseUrl && provider) {
|
|
45
|
+
const pDef = getProvider(provider);
|
|
46
|
+
if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
|
|
47
|
+
try {
|
|
48
|
+
new URL(baseUrl);
|
|
49
|
+
} catch {
|
|
50
|
+
return sendError(res, 400, `Invalid baseUrl "${baseUrl}"`);
|
|
51
|
+
}
|
|
52
|
+
savePersistedProviderBaseUrl(provider, baseUrl);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let catalog;
|
|
56
|
+
if (provider) {
|
|
57
|
+
const pDef = getProvider(provider);
|
|
58
|
+
if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
|
|
59
|
+
setActiveProvider(provider, { model, baseUrl });
|
|
60
|
+
if (currentAgent?.client) {
|
|
61
|
+
try {
|
|
62
|
+
currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl: baseUrl || undefined });
|
|
63
|
+
setCurrentAgent(currentAgent);
|
|
64
|
+
} catch {}
|
|
65
|
+
} else if (baseUrl || model) {
|
|
66
|
+
try {
|
|
67
|
+
const client = new LLMClient({ provider, model: model || undefined, baseUrl: baseUrl || getProviderBaseUrl(provider) });
|
|
68
|
+
void client;
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
catalog = getUnifiedProviderCatalog();
|
|
72
|
+
} else {
|
|
73
|
+
catalog = getUnifiedProviderCatalog();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sendJson(res, 200, {
|
|
77
|
+
status: 'ok',
|
|
78
|
+
activeProvider: catalog.activeProvider,
|
|
79
|
+
activeBaseUrl: catalog.activeBaseUrl,
|
|
80
|
+
activeModel: catalog.activeModel,
|
|
81
|
+
localEndpoints: catalog.localEndpoints,
|
|
82
|
+
providers: catalog.providers,
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
sendError(res, 500, err.message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -26,16 +26,48 @@ const MIME_TYPES = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export function handleStaticRoute(req, res, pathname, port) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
29
|
+
const rawUrl = req?.url || '';
|
|
30
|
+
let currentPath = pathname;
|
|
31
|
+
if (!currentPath && rawUrl) {
|
|
32
|
+
try {
|
|
33
|
+
currentPath = new URL(rawUrl, 'http://localhost').pathname;
|
|
34
|
+
} catch {
|
|
35
|
+
currentPath = rawUrl;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
currentPath = currentPath || '/';
|
|
39
|
+
|
|
40
|
+
const rawContainsTraversal = rawUrl.includes('..') || /%2e/i.test(rawUrl);
|
|
41
|
+
let decodedPath;
|
|
42
|
+
try {
|
|
43
|
+
decodedPath = decodeURIComponent(currentPath);
|
|
44
|
+
} catch {
|
|
45
|
+
decodedPath = currentPath;
|
|
46
|
+
}
|
|
47
|
+
const decodedContainsTraversal = decodedPath.includes('..') || decodedPath.includes('\0');
|
|
48
|
+
|
|
49
|
+
if (rawContainsTraversal || decodedContainsTraversal) {
|
|
50
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
51
|
+
res.end('403 Forbidden: Access denied');
|
|
36
52
|
return true;
|
|
37
53
|
}
|
|
38
54
|
|
|
55
|
+
if (currentPath === '/favicon.ico') {
|
|
56
|
+
let icoPath = path.join(PUBLIC_DIR, 'favicon.ico');
|
|
57
|
+
if (!fs.existsSync(icoPath)) {
|
|
58
|
+
const cwdPath = path.join(process.cwd(), 'public', 'favicon.ico');
|
|
59
|
+
if (fs.existsSync(cwdPath)) icoPath = cwdPath;
|
|
60
|
+
}
|
|
61
|
+
if (fs.existsSync(icoPath)) {
|
|
62
|
+
res.writeHead(200, {
|
|
63
|
+
'Content-Type': 'image/x-icon',
|
|
64
|
+
'Cache-Control': 'public, max-age=86400, immutable'
|
|
65
|
+
});
|
|
66
|
+
res.end(fs.readFileSync(icoPath));
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
if (pathname === '/sw.js' || pathname === '/service-worker.js') {
|
|
40
72
|
let swPath = path.join(PUBLIC_DIR, 'sw.js');
|
|
41
73
|
if (!fs.existsSync(swPath)) {
|
|
@@ -72,13 +104,29 @@ self.addEventListener('fetch', (event) => {
|
|
|
72
104
|
return true;
|
|
73
105
|
}
|
|
74
106
|
|
|
75
|
-
if (
|
|
76
|
-
const
|
|
77
|
-
|
|
107
|
+
if (currentPath.startsWith('/fonts/') || currentPath.startsWith('/images/') || currentPath === '/manifest.json' || currentPath === '/robots.txt') {
|
|
108
|
+
const targetDir = fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : path.join(process.cwd(), 'public');
|
|
109
|
+
const targetDirCanonical = path.resolve(targetDir);
|
|
110
|
+
const targetDirPrefix = targetDirCanonical + path.sep;
|
|
111
|
+
|
|
112
|
+
const relativePath = currentPath.replace(/^\/+/, '');
|
|
113
|
+
const resolvedPath = path.resolve(targetDirCanonical, relativePath);
|
|
114
|
+
|
|
115
|
+
if (!resolvedPath.startsWith(targetDirPrefix) && resolvedPath !== targetDirCanonical) {
|
|
116
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
117
|
+
res.end('403 Forbidden: Access denied');
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let filePath = resolvedPath;
|
|
78
122
|
if (!fs.existsSync(filePath)) {
|
|
79
|
-
const
|
|
80
|
-
|
|
123
|
+
const cwdPublic = path.resolve(process.cwd(), 'public');
|
|
124
|
+
const cwdPath = path.resolve(cwdPublic, relativePath);
|
|
125
|
+
if (cwdPath.startsWith(cwdPublic + path.sep) && fs.existsSync(cwdPath)) {
|
|
126
|
+
filePath = cwdPath;
|
|
127
|
+
}
|
|
81
128
|
}
|
|
129
|
+
|
|
82
130
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
83
131
|
const ext = path.extname(filePath).toLowerCase();
|
|
84
132
|
res.writeHead(200, {
|
|
@@ -90,7 +138,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
90
138
|
}
|
|
91
139
|
}
|
|
92
140
|
|
|
93
|
-
if (
|
|
141
|
+
if (currentPath === '/' || currentPath === '/chat' || currentPath.startsWith('/chat/') || currentPath === '/index.html') {
|
|
94
142
|
res.writeHead(200, {
|
|
95
143
|
'Content-Type': 'text/html; charset=utf-8',
|
|
96
144
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toolsSchema } from '../../agent/tools.js';
|
|
2
|
-
import { resolveAgentConnection } from '../core/connection.js';
|
|
2
|
+
import { resolveAgentConnection, currentAgent } from '../core/connection.js';
|
|
3
|
+
import { config, CLOUD_PROVIDERS, getProviderBaseUrl, getUnifiedProviderCatalog } from '../../agent/config.js';
|
|
3
4
|
import { getAgentMemorySnapshot } from '../core/memorySnapshot.js';
|
|
4
5
|
import { sendJson, sendError } from '../core/errorHandler.js';
|
|
5
6
|
import { extractApiKey } from '../core/security.js';
|
|
@@ -8,17 +9,30 @@ export async function handleStatusRoute(req, res, urlObj, port) {
|
|
|
8
9
|
try {
|
|
9
10
|
const queryProvider = urlObj.searchParams.get('provider');
|
|
10
11
|
const queryModel = urlObj.searchParams.get('model');
|
|
11
|
-
const queryApiKey = extractApiKey(req);
|
|
12
|
+
const queryApiKey = extractApiKey(req);
|
|
12
13
|
const querySessionId = urlObj.searchParams.get('sessionId');
|
|
14
|
+
const queryBaseUrl = urlObj.searchParams.get('baseUrl');
|
|
13
15
|
|
|
14
16
|
const conn = await resolveAgentConnection({
|
|
15
17
|
provider: queryProvider,
|
|
16
18
|
model: queryModel,
|
|
17
19
|
apiKey: queryApiKey,
|
|
18
20
|
sessionId: querySessionId,
|
|
21
|
+
baseUrl: queryBaseUrl || undefined,
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
const totalTools = conn.agent?.getAllToolsSchema?.().length || toolsSchema.length;
|
|
25
|
+
const serverDefaultProvider = currentAgent?.client?.provider || config.activeProvider || 'lmstudio';
|
|
26
|
+
const serverDefaultModel = currentAgent?.client?.model || config.providers?.[serverDefaultProvider]?.model || 'google/gemma-4-e4b';
|
|
27
|
+
const serverDefaultProviderName = currentAgent?.client?.providerName || (CLOUD_PROVIDERS?.[serverDefaultProvider]?.name) || serverDefaultProvider;
|
|
28
|
+
const serverDefaultBaseUrl = currentAgent?.client?.baseUrl || getProviderBaseUrl(serverDefaultProvider);
|
|
29
|
+
let localEndpoints = null;
|
|
30
|
+
let providerCatalog = null;
|
|
31
|
+
try {
|
|
32
|
+
const catalog = getUnifiedProviderCatalog();
|
|
33
|
+
localEndpoints = catalog.localEndpoints;
|
|
34
|
+
providerCatalog = catalog.providers;
|
|
35
|
+
} catch {}
|
|
22
36
|
|
|
23
37
|
sendJson(res, 200, {
|
|
24
38
|
status: 'ok',
|
|
@@ -32,6 +46,13 @@ export async function handleStatusRoute(req, res, urlObj, port) {
|
|
|
32
46
|
activeLoadedModel: conn.resolution?.activeLoadedModel || conn.model,
|
|
33
47
|
loadedModels: conn.models || [],
|
|
34
48
|
usedFallback: conn.usedFallback || false,
|
|
49
|
+
serverDefaultProvider,
|
|
50
|
+
serverDefaultModel,
|
|
51
|
+
serverDefaultProviderName,
|
|
52
|
+
serverDefaultBaseUrl,
|
|
53
|
+
localEndpoints,
|
|
54
|
+
providers: providerCatalog,
|
|
55
|
+
hasServerAgent: Boolean(currentAgent),
|
|
35
56
|
error: conn.error || null,
|
|
36
57
|
capabilities: {
|
|
37
58
|
reactAgent: Boolean(conn.agent),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
2
|
+
import { buildHardwareReport, computeVramEstimate, parseEstimateRequest } from '../../agent/vramService.js';
|
|
3
|
+
|
|
4
|
+
export async function handleSystemHardwareRoute(req, res) {
|
|
5
|
+
try {
|
|
6
|
+
const report = await buildHardwareReport();
|
|
7
|
+
return sendJson(res, 200, { status: 'success', data: report });
|
|
8
|
+
} catch (err) {
|
|
9
|
+
return sendError(res, 500, err.message || 'Hardware probe failed');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleEstimateVramRoute(req, res, urlObj) {
|
|
14
|
+
try {
|
|
15
|
+
const body = req.method === 'POST' ? await parseJsonBody(req) : {};
|
|
16
|
+
const request = parseEstimateRequest(body, urlObj?.searchParams || null);
|
|
17
|
+
const estimate = await computeVramEstimate(request);
|
|
18
|
+
return sendJson(res, 200, { status: 'success', data: estimate });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
return sendError(res, 500, err.message || 'VRAM estimation failed');
|
|
21
|
+
}
|
|
22
|
+
}
|