kiku-agent 1.3.2 → 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 +2 -1
- 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/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- 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/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -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/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 +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -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 +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- 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/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -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 +14 -8
- 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 +18 -18
- 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
|
|