kiku-agent 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -12
- package/package.json +5 -12
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +288 -0
- package/server/agent/eval/modules/evaluation/scoring.js +363 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4799 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +253 -0
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +296 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +627 -0
- package/server/agent/tests/cliAutomation.test.js +158 -0
- package/server/agent/tests/cloudProviders.test.js +322 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +506 -0
- package/server/agent/tests/contextIntelligence.test.js +209 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +284 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +320 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +194 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +324 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +141 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +101 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +44 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +221 -0
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +267 -0
- package/server/agent/tests/sidebarControls.test.js +135 -0
- package/server/agent/tests/statefulMemory.test.js +277 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +197 -0
- package/server/agent/tests/textToolParsing.test.js +187 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +159 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +595 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +551 -0
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +284 -0
- package/server/agent/tests/webSources.test.js +281 -0
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +162 -0
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +63 -0
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -5,8 +5,12 @@ import {
|
|
|
5
5
|
setActiveProvider,
|
|
6
6
|
savePersistedProviderBaseUrl,
|
|
7
7
|
getProvider,
|
|
8
|
+
getPersistedInferenceSettings,
|
|
9
|
+
savePersistedInferenceSettings,
|
|
10
|
+
withSettingsWriteQueue,
|
|
8
11
|
} from '../../agent/config.js';
|
|
9
12
|
import { currentAgent, setCurrentAgent } from '../core/connection.js';
|
|
13
|
+
import { validateInferenceBaseUrl, isTrustedRequest, sanitizeInferenceError } from '../core/urlGuard.js';
|
|
10
14
|
import { LLMClient } from '../../agent/llmClient.js';
|
|
11
15
|
|
|
12
16
|
export async function handleProviderGetRoute(req, res) {
|
|
@@ -14,6 +18,7 @@ export async function handleProviderGetRoute(req, res) {
|
|
|
14
18
|
const catalog = getUnifiedProviderCatalog();
|
|
15
19
|
sendJson(res, 200, {
|
|
16
20
|
status: 'ok',
|
|
21
|
+
inference: getPersistedInferenceSettings(),
|
|
17
22
|
activeProvider: catalog.activeProvider,
|
|
18
23
|
activeBaseUrl: catalog.activeBaseUrl,
|
|
19
24
|
activeModel: catalog.activeModel,
|
|
@@ -21,11 +26,12 @@ export async function handleProviderGetRoute(req, res) {
|
|
|
21
26
|
providers: catalog.providers,
|
|
22
27
|
});
|
|
23
28
|
} catch (err) {
|
|
24
|
-
sendError(res, 500, err
|
|
29
|
+
sendError(res, 500, sanitizeInferenceError(err, isTrustedRequest(req)));
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export async function handleProviderUpdateRoute(req, res) {
|
|
34
|
+
if (!isTrustedRequest(req)) return sendError(res, 403, 'Changing server provider settings requires local or admin access');
|
|
29
35
|
let body;
|
|
30
36
|
try {
|
|
31
37
|
body = await parseJsonBody(req);
|
|
@@ -33,40 +39,65 @@ export async function handleProviderUpdateRoute(req, res) {
|
|
|
33
39
|
return sendError(res, 400, `Invalid JSON body: ${err.message}`);
|
|
34
40
|
}
|
|
35
41
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
if (body.inference !== undefined && (!body.inference || typeof body.inference !== 'object' || Array.isArray(body.inference))) return sendError(res, 400, 'Invalid inference settings');
|
|
43
|
+
let provider = String(body.provider || body.activeProvider || '').toLowerCase().trim() || null;
|
|
44
|
+
// 'auto' means CLI/Server sync mode: no explicit provider switch, just return catalog.
|
|
45
|
+
if (provider === 'auto') provider = null;
|
|
46
|
+
let model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : null;
|
|
47
|
+
if (model && model.toLowerCase() === 'auto') model = null;
|
|
48
|
+
if (model && !/^[A-Za-z0-9._\-/: ]{1,200}$/.test(model)) {
|
|
49
|
+
return sendError(res, 400, `Invalid model id "${model.slice(0, 50)}"`);
|
|
50
|
+
}
|
|
38
51
|
const baseUrl = typeof body.baseUrl === 'string' && body.baseUrl.trim() ? body.baseUrl.trim().replace(/\/+$/, '') : null;
|
|
39
52
|
|
|
40
|
-
if (
|
|
41
|
-
|
|
53
|
+
if (!provider) {
|
|
54
|
+
// Auto (CLI/Server sync) mode: text-field leftovers (baseUrl/model) are
|
|
55
|
+
// display-only — ignore them and just report the active catalog.
|
|
56
|
+
if (body.inference) await withSettingsWriteQueue(() => savePersistedInferenceSettings(body.inference));
|
|
57
|
+
const catalog = getUnifiedProviderCatalog();
|
|
58
|
+
return sendJson(res, 200, {
|
|
59
|
+
status: 'ok',
|
|
60
|
+
inference: getPersistedInferenceSettings(),
|
|
61
|
+
activeProvider: catalog.activeProvider,
|
|
62
|
+
activeBaseUrl: catalog.activeBaseUrl,
|
|
63
|
+
activeModel: catalog.activeModel,
|
|
64
|
+
localEndpoints: catalog.localEndpoints,
|
|
65
|
+
providers: catalog.providers,
|
|
66
|
+
});
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
if (baseUrl && provider) {
|
|
45
70
|
const pDef = getProvider(provider);
|
|
46
71
|
if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return sendError(res, 400, `Invalid baseUrl "${baseUrl}"`);
|
|
51
|
-
}
|
|
52
|
-
savePersistedProviderBaseUrl(provider, baseUrl);
|
|
72
|
+
const guard = await validateInferenceBaseUrl(baseUrl, { callerIsTrusted: isTrustedRequest(req) });
|
|
73
|
+
if (!guard.ok) return sendError(res, 400, guard.reason);
|
|
74
|
+
await withSettingsWriteQueue(() => savePersistedProviderBaseUrl(provider, guard.normalized));
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
let catalog;
|
|
78
|
+
let switchError = null;
|
|
56
79
|
if (provider) {
|
|
57
80
|
const pDef = getProvider(provider);
|
|
58
81
|
if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
|
|
59
|
-
|
|
82
|
+
await withSettingsWriteQueue(() => {
|
|
83
|
+
setActiveProvider(provider, { model, baseUrl });
|
|
84
|
+
if (body.inference) savePersistedInferenceSettings(body.inference);
|
|
85
|
+
});
|
|
60
86
|
if (currentAgent?.client) {
|
|
61
87
|
try {
|
|
62
|
-
currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl: baseUrl || undefined });
|
|
88
|
+
await currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl: baseUrl || undefined });
|
|
63
89
|
setCurrentAgent(currentAgent);
|
|
64
|
-
} catch {
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// Persisted, but the live agent could not hot-swap — surface instead of swallowing.
|
|
92
|
+
switchError = err?.message || 'Failed to hot-swap the active agent client';
|
|
93
|
+
}
|
|
65
94
|
} else if (baseUrl || model) {
|
|
66
95
|
try {
|
|
67
96
|
const client = new LLMClient({ provider, model: model || undefined, baseUrl: baseUrl || getProviderBaseUrl(provider) });
|
|
68
97
|
void client;
|
|
69
|
-
} catch {
|
|
98
|
+
} catch (err) {
|
|
99
|
+
switchError = err?.message || 'Failed to construct a client for the requested provider';
|
|
100
|
+
}
|
|
70
101
|
}
|
|
71
102
|
catalog = getUnifiedProviderCatalog();
|
|
72
103
|
} else {
|
|
@@ -75,6 +106,8 @@ export async function handleProviderUpdateRoute(req, res) {
|
|
|
75
106
|
|
|
76
107
|
sendJson(res, 200, {
|
|
77
108
|
status: 'ok',
|
|
109
|
+
inference: getPersistedInferenceSettings(),
|
|
110
|
+
switchError: switchError ? sanitizeInferenceError(switchError, isTrustedRequest(req)) : null,
|
|
78
111
|
activeProvider: catalog.activeProvider,
|
|
79
112
|
activeBaseUrl: catalog.activeBaseUrl,
|
|
80
113
|
activeModel: catalog.activeModel,
|
|
@@ -82,6 +115,6 @@ export async function handleProviderUpdateRoute(req, res) {
|
|
|
82
115
|
providers: catalog.providers,
|
|
83
116
|
});
|
|
84
117
|
} catch (err) {
|
|
85
|
-
sendError(res, 500, err
|
|
118
|
+
sendError(res, 500, sanitizeInferenceError(err, isTrustedRequest(req)));
|
|
86
119
|
}
|
|
87
120
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import zlib from 'node:zlib';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { getWebChatHtml } from '../ui/htmlBuilder.js';
|
|
7
|
+
import { getSettingsHtml } from '../ui/settingsBuilder.js';
|
|
8
|
+
import { getDocsHtml } from '../ui/docsBuilder.js';
|
|
5
9
|
|
|
6
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
11
|
const __dirname = path.dirname(__filename);
|
|
@@ -25,6 +29,161 @@ const MIME_TYPES = {
|
|
|
25
29
|
'.txt': 'text/plain; charset=utf-8'
|
|
26
30
|
};
|
|
27
31
|
|
|
32
|
+
const COMPRESSIBLE_EXTS = new Set(['.css', '.js', '.mjs', '.json', '.txt', '.svg']);
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
let webChatCache = null;
|
|
36
|
+
let webChatCacheTimestamp = 0;
|
|
37
|
+
let settingsCache = null;
|
|
38
|
+
let settingsCacheTimestamp = 0;
|
|
39
|
+
let docsCache = null;
|
|
40
|
+
let docsCacheTimestamp = 0;
|
|
41
|
+
const staticCompressCache = new Map();
|
|
42
|
+
|
|
43
|
+
function getCachedWebChatBundle(port) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const ttl = process.env.NODE_ENV === 'production' ? Infinity : 2000;
|
|
46
|
+
if (webChatCache && webChatCache.port === port && (now - webChatCacheTimestamp) < ttl) {
|
|
47
|
+
return webChatCache;
|
|
48
|
+
}
|
|
49
|
+
const html = getWebChatHtml(port);
|
|
50
|
+
const raw = Buffer.from(html, 'utf8');
|
|
51
|
+
const hash = crypto.createHash('md5').update(raw).digest('hex');
|
|
52
|
+
const etag = `W/"${hash}"`;
|
|
53
|
+
webChatCache = {
|
|
54
|
+
port,
|
|
55
|
+
raw,
|
|
56
|
+
etag,
|
|
57
|
+
br: null,
|
|
58
|
+
gzip: null,
|
|
59
|
+
deflate: null
|
|
60
|
+
};
|
|
61
|
+
webChatCacheTimestamp = now;
|
|
62
|
+
return webChatCache;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getCachedSettingsBundle() {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const ttl = process.env.NODE_ENV === 'production' ? Infinity : 2000;
|
|
68
|
+
if (settingsCache && (now - settingsCacheTimestamp) < ttl) {
|
|
69
|
+
return settingsCache;
|
|
70
|
+
}
|
|
71
|
+
const html = getSettingsHtml();
|
|
72
|
+
const raw = Buffer.from(html, 'utf8');
|
|
73
|
+
const hash = crypto.createHash('md5').update(raw).digest('hex');
|
|
74
|
+
const etag = `W/"${hash}"`;
|
|
75
|
+
settingsCache = {
|
|
76
|
+
raw,
|
|
77
|
+
etag,
|
|
78
|
+
br: null,
|
|
79
|
+
gzip: null,
|
|
80
|
+
deflate: null
|
|
81
|
+
};
|
|
82
|
+
settingsCacheTimestamp = now;
|
|
83
|
+
return settingsCache;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getCachedDocsBundle() {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const ttl = process.env.NODE_ENV === 'production' ? Infinity : 2000;
|
|
89
|
+
if (docsCache && (now - docsCacheTimestamp) < ttl) {
|
|
90
|
+
return docsCache;
|
|
91
|
+
}
|
|
92
|
+
const html = getDocsHtml();
|
|
93
|
+
const raw = Buffer.from(html, 'utf8');
|
|
94
|
+
const hash = crypto.createHash('md5').update(raw).digest('hex');
|
|
95
|
+
const etag = `W/"${hash}"`;
|
|
96
|
+
docsCache = {
|
|
97
|
+
raw,
|
|
98
|
+
etag,
|
|
99
|
+
br: null,
|
|
100
|
+
gzip: null,
|
|
101
|
+
deflate: null
|
|
102
|
+
};
|
|
103
|
+
docsCacheTimestamp = now;
|
|
104
|
+
return docsCache;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sendCompressedResponse(req, res, statusCode, baseHeaders, rawBuffer, etag, cacheEntry = null) {
|
|
108
|
+
const reqHeaders = req?.headers || {};
|
|
109
|
+
const acceptEncoding = typeof reqHeaders['accept-encoding'] === 'string'
|
|
110
|
+
? reqHeaders['accept-encoding'].toLowerCase()
|
|
111
|
+
: '';
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
const ifNoneMatch = reqHeaders['if-none-match'];
|
|
115
|
+
if (etag && ifNoneMatch) {
|
|
116
|
+
const cleanEtag = etag.replace(/^W\//, '');
|
|
117
|
+
const cleanClient = ifNoneMatch.replace(/^W\//, '');
|
|
118
|
+
if (ifNoneMatch === etag || cleanClient === cleanEtag || ifNoneMatch.includes(cleanEtag)) {
|
|
119
|
+
const respHeaders = {
|
|
120
|
+
'ETag': etag,
|
|
121
|
+
'Cache-Control': baseHeaders['Cache-Control'] || 'no-cache',
|
|
122
|
+
'Vary': 'Accept-Encoding'
|
|
123
|
+
};
|
|
124
|
+
res.writeHead(304, respHeaders);
|
|
125
|
+
res.end();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const respHeaders = { ...baseHeaders, 'Vary': 'Accept-Encoding' };
|
|
131
|
+
if (etag) {
|
|
132
|
+
respHeaders['ETag'] = etag;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if (rawBuffer.length >= 1024) {
|
|
137
|
+
if (acceptEncoding.includes('br')) {
|
|
138
|
+
let brBuf = cacheEntry?.br;
|
|
139
|
+
if (!brBuf) {
|
|
140
|
+
brBuf = zlib.brotliCompressSync(rawBuffer, {
|
|
141
|
+
params: {
|
|
142
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 5
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
if (cacheEntry) cacheEntry.br = brBuf;
|
|
146
|
+
}
|
|
147
|
+
respHeaders['Content-Encoding'] = 'br';
|
|
148
|
+
respHeaders['Content-Length'] = brBuf.length;
|
|
149
|
+
res.writeHead(statusCode, respHeaders);
|
|
150
|
+
res.end(brBuf);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (acceptEncoding.includes('gzip')) {
|
|
155
|
+
let gzBuf = cacheEntry?.gzip;
|
|
156
|
+
if (!gzBuf) {
|
|
157
|
+
gzBuf = zlib.gzipSync(rawBuffer, { level: 6 });
|
|
158
|
+
if (cacheEntry) cacheEntry.gzip = gzBuf;
|
|
159
|
+
}
|
|
160
|
+
respHeaders['Content-Encoding'] = 'gzip';
|
|
161
|
+
respHeaders['Content-Length'] = gzBuf.length;
|
|
162
|
+
res.writeHead(statusCode, respHeaders);
|
|
163
|
+
res.end(gzBuf);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (acceptEncoding.includes('deflate')) {
|
|
168
|
+
let defBuf = cacheEntry?.deflate;
|
|
169
|
+
if (!defBuf) {
|
|
170
|
+
defBuf = zlib.deflateSync(rawBuffer);
|
|
171
|
+
if (cacheEntry) cacheEntry.deflate = defBuf;
|
|
172
|
+
}
|
|
173
|
+
respHeaders['Content-Encoding'] = 'deflate';
|
|
174
|
+
respHeaders['Content-Length'] = defBuf.length;
|
|
175
|
+
res.writeHead(statusCode, respHeaders);
|
|
176
|
+
res.end(defBuf);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
respHeaders['Content-Length'] = rawBuffer.length;
|
|
183
|
+
res.writeHead(statusCode, respHeaders);
|
|
184
|
+
res.end(rawBuffer);
|
|
185
|
+
}
|
|
186
|
+
|
|
28
187
|
export function handleStaticRoute(req, res, pathname, port) {
|
|
29
188
|
const rawUrl = req?.url || '';
|
|
30
189
|
let currentPath = pathname;
|
|
@@ -52,6 +211,8 @@ export function handleStaticRoute(req, res, pathname, port) {
|
|
|
52
211
|
return true;
|
|
53
212
|
}
|
|
54
213
|
|
|
214
|
+
const reqHeaders = req?.headers || {};
|
|
215
|
+
|
|
55
216
|
if (currentPath === '/favicon.ico') {
|
|
56
217
|
let icoPath = path.join(PUBLIC_DIR, 'favicon.ico');
|
|
57
218
|
if (!fs.existsSync(icoPath)) {
|
|
@@ -59,8 +220,20 @@ export function handleStaticRoute(req, res, pathname, port) {
|
|
|
59
220
|
if (fs.existsSync(cwdPath)) icoPath = cwdPath;
|
|
60
221
|
}
|
|
61
222
|
if (fs.existsSync(icoPath)) {
|
|
223
|
+
const stat = fs.statSync(icoPath);
|
|
224
|
+
const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
|
|
225
|
+
if (reqHeaders['if-none-match'] === etag) {
|
|
226
|
+
res.writeHead(304, {
|
|
227
|
+
'ETag': etag,
|
|
228
|
+
'Cache-Control': 'public, max-age=86400, immutable'
|
|
229
|
+
});
|
|
230
|
+
res.end();
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
62
233
|
res.writeHead(200, {
|
|
63
234
|
'Content-Type': 'image/x-icon',
|
|
235
|
+
'Content-Length': stat.size,
|
|
236
|
+
'ETag': etag,
|
|
64
237
|
'Cache-Control': 'public, max-age=86400, immutable'
|
|
65
238
|
});
|
|
66
239
|
res.end(fs.readFileSync(icoPath));
|
|
@@ -74,15 +247,28 @@ export function handleStaticRoute(req, res, pathname, port) {
|
|
|
74
247
|
const cwdPath = path.join(process.cwd(), 'public', 'sw.js');
|
|
75
248
|
if (fs.existsSync(cwdPath)) swPath = cwdPath;
|
|
76
249
|
}
|
|
77
|
-
res.writeHead(200, {
|
|
78
|
-
'Content-Type': 'application/javascript; charset=utf-8',
|
|
79
|
-
'Service-Worker-Allowed': '/',
|
|
80
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
81
|
-
});
|
|
82
250
|
if (fs.existsSync(swPath)) {
|
|
83
|
-
fs.
|
|
251
|
+
const stat = fs.statSync(swPath);
|
|
252
|
+
const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
|
|
253
|
+
if (reqHeaders['if-none-match'] === etag) {
|
|
254
|
+
res.writeHead(304, {
|
|
255
|
+
'ETag': etag,
|
|
256
|
+
'Service-Worker-Allowed': '/',
|
|
257
|
+
'Cache-Control': 'no-cache, must-revalidate',
|
|
258
|
+
'Vary': 'Accept-Encoding'
|
|
259
|
+
});
|
|
260
|
+
res.end();
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
const raw = fs.readFileSync(swPath);
|
|
264
|
+
sendCompressedResponse(req, res, 200, {
|
|
265
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
266
|
+
'Service-Worker-Allowed': '/',
|
|
267
|
+
'Cache-Control': 'no-cache, must-revalidate'
|
|
268
|
+
}, raw, etag);
|
|
269
|
+
return true;
|
|
84
270
|
} else {
|
|
85
|
-
|
|
271
|
+
const defaultSw = `const CACHE_NAME = 'kiku-v1';
|
|
86
272
|
const ASSETS = ['/', '/manifest.json'];
|
|
87
273
|
self.addEventListener('install', (event) => {
|
|
88
274
|
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)).then(() => self.skipWaiting()));
|
|
@@ -91,6 +277,13 @@ self.addEventListener('activate', (event) => {
|
|
|
91
277
|
event.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
|
|
92
278
|
});
|
|
93
279
|
self.addEventListener('fetch', (event) => {
|
|
280
|
+
let url;
|
|
281
|
+
try {
|
|
282
|
+
url = new URL(event.request.url);
|
|
283
|
+
} catch {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (url.origin !== self.location.origin) return;
|
|
94
287
|
if (event.request.url.includes('/api/')) return;
|
|
95
288
|
event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request).then((response) => {
|
|
96
289
|
if (response.ok && event.request.method === 'GET') {
|
|
@@ -98,18 +291,26 @@ self.addEventListener('fetch', (event) => {
|
|
|
98
291
|
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
99
292
|
}
|
|
100
293
|
return response;
|
|
101
|
-
}).catch(() => cached)));
|
|
102
|
-
})
|
|
294
|
+
}).catch(() => cached || new Response('', { status: 504, statusText: 'Gateway Timeout' }))));
|
|
295
|
+
});`;
|
|
296
|
+
const raw = Buffer.from(defaultSw, 'utf8');
|
|
297
|
+
const hash = crypto.createHash('md5').update(raw).digest('hex');
|
|
298
|
+
sendCompressedResponse(req, res, 200, {
|
|
299
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
300
|
+
'Service-Worker-Allowed': '/',
|
|
301
|
+
'Cache-Control': 'no-cache, must-revalidate'
|
|
302
|
+
}, raw, `W/"${hash}"`);
|
|
303
|
+
return true;
|
|
103
304
|
}
|
|
104
|
-
return true;
|
|
105
305
|
}
|
|
106
306
|
|
|
107
|
-
|
|
307
|
+
const lookupPath = decodedPath || currentPath;
|
|
308
|
+
if (lookupPath.startsWith('/fonts/') || lookupPath.startsWith('/images/') || lookupPath === '/manifest.json' || lookupPath === '/robots.txt') {
|
|
108
309
|
const targetDir = fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : path.join(process.cwd(), 'public');
|
|
109
310
|
const targetDirCanonical = path.resolve(targetDir);
|
|
110
311
|
const targetDirPrefix = targetDirCanonical + path.sep;
|
|
111
312
|
|
|
112
|
-
const relativePath =
|
|
313
|
+
const relativePath = lookupPath.replace(/^\/+/, '');
|
|
113
314
|
const resolvedPath = path.resolve(targetDirCanonical, relativePath);
|
|
114
315
|
|
|
115
316
|
if (!resolvedPath.startsWith(targetDirPrefix) && resolvedPath !== targetDirCanonical) {
|
|
@@ -127,10 +328,80 @@ self.addEventListener('fetch', (event) => {
|
|
|
127
328
|
}
|
|
128
329
|
}
|
|
129
330
|
|
|
331
|
+
if (!fs.existsSync(filePath)) {
|
|
332
|
+
const altRel = relativePath.startsWith('images/general/')
|
|
333
|
+
? 'images/' + relativePath.slice('images/general/'.length)
|
|
334
|
+
: relativePath.startsWith('images/')
|
|
335
|
+
? 'images/general/' + relativePath.slice('images/'.length)
|
|
336
|
+
: null;
|
|
337
|
+
if (altRel) {
|
|
338
|
+
const altResolved = path.resolve(targetDirCanonical, altRel);
|
|
339
|
+
if (altResolved.startsWith(targetDirPrefix) && fs.existsSync(altResolved)) {
|
|
340
|
+
filePath = altResolved;
|
|
341
|
+
} else {
|
|
342
|
+
const cwdPublic = path.resolve(process.cwd(), 'public');
|
|
343
|
+
const altCwd = path.resolve(cwdPublic, altRel);
|
|
344
|
+
if (altCwd.startsWith(cwdPublic + path.sep) && fs.existsSync(altCwd)) {
|
|
345
|
+
filePath = altCwd;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
130
351
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
352
|
+
const stat = fs.statSync(filePath);
|
|
131
353
|
const ext = path.extname(filePath).toLowerCase();
|
|
354
|
+
const mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
355
|
+
const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
if (reqHeaders['if-none-match'] === etag) {
|
|
359
|
+
res.writeHead(304, {
|
|
360
|
+
'ETag': etag,
|
|
361
|
+
'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
362
|
+
'Vary': 'Accept-Encoding'
|
|
363
|
+
});
|
|
364
|
+
res.end();
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (reqHeaders['if-modified-since']) {
|
|
368
|
+
const clientTime = new Date(reqHeaders['if-modified-since']).getTime();
|
|
369
|
+
if (!isNaN(clientTime) && Math.floor(stat.mtimeMs / 1000) <= Math.floor(clientTime / 1000)) {
|
|
370
|
+
res.writeHead(304, {
|
|
371
|
+
'ETag': etag,
|
|
372
|
+
'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
373
|
+
'Vary': 'Accept-Encoding'
|
|
374
|
+
});
|
|
375
|
+
res.end();
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if (COMPRESSIBLE_EXTS.has(ext) && stat.size >= 1024) {
|
|
382
|
+
let entry = staticCompressCache.get(filePath);
|
|
383
|
+
if (!entry || entry.mtimeMs !== stat.mtimeMs) {
|
|
384
|
+
const raw = fs.readFileSync(filePath);
|
|
385
|
+
entry = { mtimeMs: stat.mtimeMs, raw, etag, br: null, gzip: null, deflate: null };
|
|
386
|
+
if (staticCompressCache.size > 80) {
|
|
387
|
+
const oldest = staticCompressCache.keys().next().value;
|
|
388
|
+
staticCompressCache.delete(oldest);
|
|
389
|
+
}
|
|
390
|
+
staticCompressCache.set(filePath, entry);
|
|
391
|
+
}
|
|
392
|
+
sendCompressedResponse(req, res, 200, {
|
|
393
|
+
'Content-Type': mime,
|
|
394
|
+
'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable'
|
|
395
|
+
}, entry.raw, etag, entry);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
132
400
|
res.writeHead(200, {
|
|
133
|
-
'Content-Type':
|
|
401
|
+
'Content-Type': mime,
|
|
402
|
+
'Content-Length': stat.size,
|
|
403
|
+
'ETag': etag,
|
|
404
|
+
'Last-Modified': stat.mtime.toUTCString(),
|
|
134
405
|
'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable'
|
|
135
406
|
});
|
|
136
407
|
fs.createReadStream(filePath).pipe(res);
|
|
@@ -138,12 +409,30 @@ self.addEventListener('fetch', (event) => {
|
|
|
138
409
|
}
|
|
139
410
|
}
|
|
140
411
|
|
|
412
|
+
if (currentPath === '/docs' || currentPath.startsWith('/docs/') || currentPath.startsWith('/docs?') || currentPath === '/settings/docs' || currentPath.startsWith('/settings/docs')) {
|
|
413
|
+
const bundle = getCachedDocsBundle();
|
|
414
|
+
sendCompressedResponse(req, res, 200, {
|
|
415
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
416
|
+
'Cache-Control': 'no-cache, must-revalidate'
|
|
417
|
+
}, bundle.raw, bundle.etag, bundle);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (currentPath === '/settings' || currentPath.startsWith('/settings/') || currentPath.startsWith('/settings?')) {
|
|
422
|
+
const bundle = getCachedSettingsBundle();
|
|
423
|
+
sendCompressedResponse(req, res, 200, {
|
|
424
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
425
|
+
'Cache-Control': 'no-cache, must-revalidate'
|
|
426
|
+
}, bundle.raw, bundle.etag, bundle);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
141
430
|
if (currentPath === '/' || currentPath === '/chat' || currentPath.startsWith('/chat/') || currentPath === '/index.html') {
|
|
142
|
-
|
|
431
|
+
const bundle = getCachedWebChatBundle(port);
|
|
432
|
+
sendCompressedResponse(req, res, 200, {
|
|
143
433
|
'Content-Type': 'text/html; charset=utf-8',
|
|
144
|
-
'Cache-Control': 'no-cache,
|
|
145
|
-
});
|
|
146
|
-
res.end(getWebChatHtml(port));
|
|
434
|
+
'Cache-Control': 'no-cache, must-revalidate'
|
|
435
|
+
}, bundle.raw, bundle.etag, bundle);
|
|
147
436
|
return true;
|
|
148
437
|
}
|
|
149
438
|
|