kiku-agent 1.3.2 → 1.3.4
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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
2
|
+
import { isTrustedRequest } from '../core/urlGuard.js';
|
|
3
|
+
import {
|
|
4
|
+
getPersistedModelLoadSettings,
|
|
5
|
+
savePersistedModelLoadSettings,
|
|
6
|
+
getActiveLocalEngine,
|
|
7
|
+
setActiveLocalEngine,
|
|
8
|
+
withSettingsWriteQueue,
|
|
9
|
+
} from '../../agent/config.js';
|
|
10
|
+
import {
|
|
11
|
+
checkEngineCompatibility,
|
|
12
|
+
getEnginesHealth,
|
|
13
|
+
loadModelOnEngine,
|
|
14
|
+
unloadModelFromEngine,
|
|
15
|
+
stopEngine,
|
|
16
|
+
} from '../../agent/engineService.js';
|
|
17
|
+
import { clearLmStudioProbeCache, probeLmStudioRuntime } from '../../agent/lmStudioWakeup.js';
|
|
18
|
+
import { ensureModelContextFollow, parseAutoContextFlag } from '../../agent/autoContext.js';
|
|
19
|
+
|
|
20
|
+
export async function handleEngineStatusRoute(req, res) {
|
|
21
|
+
try {
|
|
22
|
+
const health = await getEnginesHealth();
|
|
23
|
+
sendJson(res, 200, {
|
|
24
|
+
status: 'ok',
|
|
25
|
+
activeEngine: getActiveLocalEngine(),
|
|
26
|
+
...health,
|
|
27
|
+
modelLoad: getPersistedModelLoadSettings(),
|
|
28
|
+
});
|
|
29
|
+
} catch (err) {
|
|
30
|
+
sendError(res, 500, `Failed to retrieve engine status: ${err.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleEngineLoadRoute(req, res) {
|
|
35
|
+
if (!isTrustedRequest(req)) {
|
|
36
|
+
return sendError(res, 403, 'Engine model loading requires local access');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let body;
|
|
40
|
+
try {
|
|
41
|
+
body = await parseJsonBody(req);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return sendError(res, 400, `Invalid JSON body: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const engine = String(body.engine || getActiveLocalEngine() || 'lms').toLowerCase().trim();
|
|
47
|
+
const compat = checkEngineCompatibility(engine);
|
|
48
|
+
if (!compat.supported) {
|
|
49
|
+
return sendJson(res, 200, {
|
|
50
|
+
status: 'error',
|
|
51
|
+
engine,
|
|
52
|
+
message: compat.reason,
|
|
53
|
+
compatibility: compat,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const model = typeof body.model === 'string' && body.model.trim()
|
|
58
|
+
? body.model.trim()
|
|
59
|
+
: (engine === 'vllm' ? 'google/gemma-4-e4b' : 'google/gemma-4-e4b');
|
|
60
|
+
|
|
61
|
+
if (!/^[A-Za-z0-9._\-/: ]{1,200}$/.test(model)) {
|
|
62
|
+
return sendError(res, 400, `Invalid model id "${model.slice(0, 50)}"`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
const persistUpdate = { activeEngine: engine };
|
|
67
|
+
if (engine === 'lms') {
|
|
68
|
+
const gpu = body.gpuOffload !== undefined ? body.gpuOffload : (body.nGpuLayers !== undefined ? body.nGpuLayers : undefined);
|
|
69
|
+
const threads = body.cpuThreads !== undefined ? body.cpuThreads : (body.threads !== undefined ? body.threads : undefined);
|
|
70
|
+
const ctx = body.contextLength !== undefined ? Number(body.contextLength) : undefined;
|
|
71
|
+
if (gpu !== undefined) persistUpdate.gpuOffload = gpu;
|
|
72
|
+
if (threads !== undefined) persistUpdate.cpuThreads = threads;
|
|
73
|
+
if (ctx !== undefined) persistUpdate.contextLength = ctx;
|
|
74
|
+
if (body.splitMode !== undefined) persistUpdate.splitMode = body.splitMode;
|
|
75
|
+
if (body.tensorSplit !== undefined) persistUpdate.tensorSplit = body.tensorSplit;
|
|
76
|
+
if (body.mainGpu !== undefined) persistUpdate.mainGpu = body.mainGpu;
|
|
77
|
+
if (body.flashAttn !== undefined) persistUpdate.flashAttn = body.flashAttn;
|
|
78
|
+
if (body.batchSize !== undefined) persistUpdate.batchSize = body.batchSize;
|
|
79
|
+
if (body.ubatchSize !== undefined) persistUpdate.ubatchSize = body.ubatchSize;
|
|
80
|
+
if (body.kvCacheDtype !== undefined) persistUpdate.kvCacheDtype = body.kvCacheDtype;
|
|
81
|
+
if (body.htMode !== undefined) persistUpdate.htMode = body.htMode;
|
|
82
|
+
if (body.hyperThreadingMode !== undefined) persistUpdate.htMode = body.hyperThreadingMode;
|
|
83
|
+
} else if (engine === 'vllm') {
|
|
84
|
+
persistUpdate.vllm = {};
|
|
85
|
+
if (body.gpuMemoryUtilization !== undefined) persistUpdate.vllm.gpuMemoryUtilization = Number(body.gpuMemoryUtilization);
|
|
86
|
+
if (body.maxModelLen !== undefined) persistUpdate.vllm.maxModelLen = body.maxModelLen;
|
|
87
|
+
if (body.tensorParallelSize !== undefined) persistUpdate.vllm.tensorParallelSize = Number(body.tensorParallelSize);
|
|
88
|
+
if (body.dtype !== undefined) persistUpdate.vllm.dtype = body.dtype;
|
|
89
|
+
if (body.enforceEager !== undefined) persistUpdate.vllm.enforceEager = Boolean(body.enforceEager);
|
|
90
|
+
if (body.kvCacheDtype !== undefined) persistUpdate.vllm.kvCacheDtype = body.kvCacheDtype;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await withSettingsWriteQueue(() => savePersistedModelLoadSettings(persistUpdate));
|
|
94
|
+
const currentSettings = getPersistedModelLoadSettings();
|
|
95
|
+
|
|
96
|
+
const autoFollow = engine === 'lms'
|
|
97
|
+
&& parseAutoContextFlag(body.autoContext, currentSettings.autoContext ?? true);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
clearLmStudioProbeCache();
|
|
101
|
+
|
|
102
|
+
if (autoFollow) {
|
|
103
|
+
const resolvedGpu = body.gpuOffload !== undefined ? body.gpuOffload : currentSettings.gpuOffload;
|
|
104
|
+
const resolvedThreads = body.cpuThreads !== undefined ? body.cpuThreads : currentSettings.cpuThreads;
|
|
105
|
+
const resolvedCtx = body.contextLength !== undefined ? body.contextLength : currentSettings.contextLength;
|
|
106
|
+
const follow = await ensureModelContextFollow({
|
|
107
|
+
model,
|
|
108
|
+
requestedContext: resolvedCtx,
|
|
109
|
+
probeFn: () => probeLmStudioRuntime('http://127.0.0.1:1234/v1', { timeoutMs: 2000, bypassCache: true }),
|
|
110
|
+
loadFn: ({ contextLength }) => loadModelOnEngine(engine, model, {
|
|
111
|
+
...body,
|
|
112
|
+
quiet: true,
|
|
113
|
+
gpuOffload: resolvedGpu,
|
|
114
|
+
cpuThreads: resolvedThreads,
|
|
115
|
+
htMode: body.htMode !== undefined ? body.htMode : currentSettings.htMode,
|
|
116
|
+
contextLength: contextLength ?? resolvedCtx,
|
|
117
|
+
}),
|
|
118
|
+
unloadFn: () => unloadModelFromEngine('lms', model),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!follow.ok) {
|
|
122
|
+
return sendJson(res, 200, {
|
|
123
|
+
status: 'error',
|
|
124
|
+
engine,
|
|
125
|
+
message: follow.reason || `Failed to load model on ${compat.name}`,
|
|
126
|
+
compatibility: compat,
|
|
127
|
+
followApplied: false,
|
|
128
|
+
followReason: follow.followReason || null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const finalCtx = follow.loadedContextLength || follow.followContext || null;
|
|
133
|
+
if (finalCtx) {
|
|
134
|
+
await withSettingsWriteQueue(() => savePersistedModelLoadSettings({ contextLength: finalCtx }));
|
|
135
|
+
}
|
|
136
|
+
const refreshedSettings = getPersistedModelLoadSettings();
|
|
137
|
+
return sendJson(res, 200, {
|
|
138
|
+
status: 'ok',
|
|
139
|
+
engine,
|
|
140
|
+
message: follow.reloadedForFollow
|
|
141
|
+
? `Model "${model}" reloaded with context ${Number(finalCtx).toLocaleString()} (model max ${follow.modelMaxContextLength ? Number(follow.modelMaxContextLength).toLocaleString() : 'unknown'})`
|
|
142
|
+
: `${compat.name} is online and model is loaded`,
|
|
143
|
+
awakened: Boolean(follow.performedLoad),
|
|
144
|
+
modelLoaded: true,
|
|
145
|
+
loadedModel: model,
|
|
146
|
+
loadedContextLength: follow.loadedContextLength || null,
|
|
147
|
+
modelMaxContextLength: follow.modelMaxContextLength || null,
|
|
148
|
+
followContext: follow.followContext || null,
|
|
149
|
+
followZone: follow.followZone || null,
|
|
150
|
+
followApplied: true,
|
|
151
|
+
followReason: follow.followReason || null,
|
|
152
|
+
compatibility: compat,
|
|
153
|
+
modelLoad: refreshedSettings,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = await loadModelOnEngine(engine, model, {
|
|
158
|
+
...body,
|
|
159
|
+
quiet: true,
|
|
160
|
+
gpuOffload: body.gpuOffload !== undefined ? body.gpuOffload : currentSettings.gpuOffload,
|
|
161
|
+
cpuThreads: body.cpuThreads !== undefined ? body.cpuThreads : currentSettings.cpuThreads,
|
|
162
|
+
htMode: body.htMode !== undefined ? body.htMode : currentSettings.htMode,
|
|
163
|
+
contextLength: body.contextLength !== undefined ? body.contextLength : currentSettings.contextLength,
|
|
164
|
+
gpuMemoryUtilization: body.gpuMemoryUtilization !== undefined ? body.gpuMemoryUtilization : currentSettings.vllm.gpuMemoryUtilization,
|
|
165
|
+
maxModelLen: body.maxModelLen !== undefined ? body.maxModelLen : currentSettings.vllm.maxModelLen,
|
|
166
|
+
tensorParallelSize: body.tensorParallelSize !== undefined ? body.tensorParallelSize : currentSettings.vllm.tensorParallelSize,
|
|
167
|
+
dtype: body.dtype !== undefined ? body.dtype : currentSettings.vllm.dtype,
|
|
168
|
+
enforceEager: body.enforceEager !== undefined ? body.enforceEager : currentSettings.vllm.enforceEager,
|
|
169
|
+
kvCacheDtype: body.kvCacheDtype !== undefined ? body.kvCacheDtype : currentSettings.vllm.kvCacheDtype,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
return sendJson(res, 200, {
|
|
174
|
+
status: 'error',
|
|
175
|
+
engine,
|
|
176
|
+
message: result.reason || `Failed to load model on ${compat.name}`,
|
|
177
|
+
compatibility: compat,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const stateProbe = engine === 'lms'
|
|
182
|
+
? await probeLmStudioRuntime('http://127.0.0.1:1234/v1', { timeoutMs: 2000, bypassCache: true }).catch(() => null)
|
|
183
|
+
: null;
|
|
184
|
+
sendJson(res, 200, {
|
|
185
|
+
status: 'ok',
|
|
186
|
+
engine,
|
|
187
|
+
message: result.awakened
|
|
188
|
+
? `Model "${model}" successfully loaded on ${compat.name}`
|
|
189
|
+
: `${compat.name} is online and model is loaded`,
|
|
190
|
+
awakened: Boolean(result.awakened),
|
|
191
|
+
modelLoaded: Boolean(result.modelLoaded),
|
|
192
|
+
loadedModel: result.loadedModel || model,
|
|
193
|
+
loadedContextLength: stateProbe?.loadedContextLength || null,
|
|
194
|
+
modelMaxContextLength: stateProbe?.modelMaxContextLength || null,
|
|
195
|
+
followContext: null,
|
|
196
|
+
followApplied: false,
|
|
197
|
+
followReason: 'auto-follow-disabled',
|
|
198
|
+
compatibility: compat,
|
|
199
|
+
modelLoad: currentSettings,
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
sendError(res, 500, `Engine load failed: ${err.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function handleEngineSettingsRoute(req, res) {
|
|
207
|
+
if (req.method === 'GET') {
|
|
208
|
+
return sendJson(res, 200, {
|
|
209
|
+
status: 'ok',
|
|
210
|
+
activeEngine: getActiveLocalEngine(),
|
|
211
|
+
modelLoad: getPersistedModelLoadSettings(),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!isTrustedRequest(req)) {
|
|
216
|
+
return sendError(res, 403, 'Changing engine settings requires local access');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let body;
|
|
220
|
+
try {
|
|
221
|
+
body = await parseJsonBody(req);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
return sendError(res, 400, `Invalid JSON body: ${err.message}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const saved = await withSettingsWriteQueue(() => savePersistedModelLoadSettings(body));
|
|
227
|
+
sendJson(res, 200, {
|
|
228
|
+
status: 'ok',
|
|
229
|
+
activeEngine: getActiveLocalEngine(),
|
|
230
|
+
modelLoad: saved,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function handleEngineStopRoute(req, res) {
|
|
235
|
+
if (!isTrustedRequest(req)) {
|
|
236
|
+
return sendError(res, 403, 'Stopping engine requires local access');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let body = {};
|
|
240
|
+
try {
|
|
241
|
+
body = await parseJsonBody(req);
|
|
242
|
+
} catch {}
|
|
243
|
+
|
|
244
|
+
const engine = String(body.engine || getActiveLocalEngine() || 'vllm').toLowerCase().trim();
|
|
245
|
+
const stopRes = await stopEngine(engine, body);
|
|
246
|
+
sendJson(res, 200, {
|
|
247
|
+
status: 'ok',
|
|
248
|
+
...stopRes,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function handleEngineUnloadRoute(req, res) {
|
|
253
|
+
if (!isTrustedRequest(req)) {
|
|
254
|
+
return sendError(res, 403, 'Unloading engine model requires local access');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let body = {};
|
|
258
|
+
try {
|
|
259
|
+
body = await parseJsonBody(req);
|
|
260
|
+
} catch {}
|
|
261
|
+
|
|
262
|
+
const engine = String(body.engine || getActiveLocalEngine() || 'lms').toLowerCase().trim();
|
|
263
|
+
const model = body.model || null;
|
|
264
|
+
const unloadRes = await unloadModelFromEngine(engine, model, body);
|
|
265
|
+
sendJson(res, 200, {
|
|
266
|
+
status: unloadRes.ok ? 'ok' : 'error',
|
|
267
|
+
...unloadRes,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
@@ -36,7 +36,7 @@ export async function handleExportRoute(req, res) {
|
|
|
36
36
|
const escaped = String(m.text || m.content || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
37
37
|
return `<div style="margin:12px 0;padding:12px;border-radius:8px;background:${m.role === 'user' ? '#e3f2fd' : '#fce4ec'}"><strong>${role}:</strong><br><pre style="white-space:pre-wrap;margin:4px 0 0;font-family:inherit">${escaped}</pre></div>`;
|
|
38
38
|
}).join('\n');
|
|
39
|
-
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Kiku Chat Export</title><style>body{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px}h1{color:#e91e63}</style></head><body><h1>🌸 Kiku AI Chat Export</h1><p>Session: ${normSessionId || 'N/A'} | Exported: ${new Date().toLocaleString()}</p><hr>${msgHtml}</body></html>`;
|
|
39
|
+
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Kiku Chat Export</title><link rel="stylesheet" href="/fonts/google-sans-official.css"><style>body{font-family:'Google Sans', 'Google Sans Text', 'Product Sans', 'Noto Sans Thai', 'Noto Sans SC', sans-serif;max-width:800px;margin:40px auto;padding:0 20px}h1{color:#e91e63}</style></head><body><h1>🌸 Kiku AI Chat Export</h1><p>Session: ${normSessionId || 'N/A'} | Exported: ${new Date().toLocaleString()}</p><hr>${msgHtml}</body></html>`;
|
|
40
40
|
res.writeHead(200, {
|
|
41
41
|
'Content-Type': 'text/html; charset=utf-8',
|
|
42
42
|
'Content-Disposition': `attachment; filename="${filename}.html"`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
2
|
-
import { scanLan, mapServerTypeToProvider } from '../../agent/lanDiscovery.js';
|
|
2
|
+
import { scanLan, mapServerTypeToProvider, listLanInterfaces } from '../../agent/lanDiscovery.js';
|
|
3
3
|
import { isPrivateIPv4 } from '../../agent/sanitizer.js';
|
|
4
4
|
import { resolveClientIp, isWebChatAdmin } from '../core/security.js';
|
|
5
5
|
import {
|
|
@@ -25,9 +25,12 @@ export async function handleLanDiscoveryRoute(req, res) {
|
|
|
25
25
|
const urlObj = new URL(req.url, 'http://localhost');
|
|
26
26
|
const subnetsParam = urlObj.searchParams.get('subnets');
|
|
27
27
|
const portsParam = urlObj.searchParams.get('ports');
|
|
28
|
+
const ifaceParam = urlObj.searchParams.get('iface');
|
|
28
29
|
const options = {};
|
|
29
30
|
if (subnetsParam && subnetsParam.trim()) {
|
|
30
31
|
options.subnets = subnetsParam.split(',').map(s => s.trim()).filter(Boolean);
|
|
32
|
+
} else if (ifaceParam && ifaceParam.trim()) {
|
|
33
|
+
options.iface = ifaceParam.trim();
|
|
31
34
|
}
|
|
32
35
|
if (portsParam && portsParam.trim()) {
|
|
33
36
|
options.ports = portsParam.split(',').map(Number).filter(p => Number.isInteger(p) && p >= 1 && p <= 65535);
|
|
@@ -39,6 +42,18 @@ export async function handleLanDiscoveryRoute(req, res) {
|
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
export async function handleLanInterfacesRoute(req, res) {
|
|
46
|
+
if (!lanDiscoveryAllowed(req)) {
|
|
47
|
+
return sendError(res, 403, 'LAN discovery is restricted to the local machine. Set KIKU_ALLOW_REMOTE_LAN_DISCOVERY=true to allow other devices.');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const interfaces = await listLanInterfaces({});
|
|
51
|
+
sendJson(res, 200, { status: 'ok', count: interfaces.length, interfaces });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
sendError(res, 500, `Failed to enumerate network interfaces: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
export async function handleLanApplyRoute(req, res) {
|
|
43
58
|
if (!lanDiscoveryAllowed(req)) {
|
|
44
59
|
return sendError(res, 403, 'LAN discovery is restricted to the local machine. Set KIKU_ALLOW_REMOTE_LAN_DISCOVERY=true to allow other devices.');
|
|
@@ -74,7 +89,7 @@ export async function handleLanApplyRoute(req, res) {
|
|
|
74
89
|
|
|
75
90
|
if (currentAgent?.client) {
|
|
76
91
|
try {
|
|
77
|
-
currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl });
|
|
92
|
+
await currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl });
|
|
78
93
|
setCurrentAgent(currentAgent);
|
|
79
94
|
} catch {}
|
|
80
95
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
|
|
2
|
+
import { isTrustedRequest } from '../core/urlGuard.js';
|
|
3
|
+
import {
|
|
4
|
+
getPersistedModelLoadSettings,
|
|
5
|
+
savePersistedModelLoadSettings,
|
|
6
|
+
withSettingsWriteQueue,
|
|
7
|
+
} from '../../agent/config.js';
|
|
8
|
+
import {
|
|
9
|
+
wakeUpLmStudio,
|
|
10
|
+
probeLmStudioRuntime,
|
|
11
|
+
clearLmStudioProbeCache,
|
|
12
|
+
} from '../../agent/lmStudioWakeup.js';
|
|
13
|
+
import { unloadModelFromEngine } from '../../agent/engineService.js';
|
|
14
|
+
import { ensureModelContextFollow, parseAutoContextFlag } from '../../agent/autoContext.js';
|
|
15
|
+
|
|
16
|
+
export async function handleLmStudioLoadRoute(req, res) {
|
|
17
|
+
if (!isTrustedRequest(req)) {
|
|
18
|
+
return sendError(res, 403, 'LM Studio model loading requires local access');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let body;
|
|
22
|
+
try {
|
|
23
|
+
body = await parseJsonBody(req);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return sendError(res, 400, `Invalid JSON body: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const model = typeof body.model === 'string' && body.model.trim()
|
|
29
|
+
? body.model.trim() : 'google/gemma-4-e4b';
|
|
30
|
+
if (!/^[A-Za-z0-9._\-/: ]{1,200}$/.test(model)) {
|
|
31
|
+
return sendError(res, 400, `Invalid model id "${model.slice(0, 50)}"`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const gpuOffload = body.gpuOffload !== undefined ? body.gpuOffload : (body.nGpuLayers !== undefined ? body.nGpuLayers : undefined);
|
|
35
|
+
const cpuThreads = body.cpuThreads !== undefined ? body.cpuThreads : (body.threads !== undefined ? body.threads : undefined);
|
|
36
|
+
const contextLength = body.contextLength !== undefined ? Number(body.contextLength) : undefined;
|
|
37
|
+
|
|
38
|
+
const persist = {};
|
|
39
|
+
if (gpuOffload !== undefined) persist.gpuOffload = gpuOffload;
|
|
40
|
+
if (cpuThreads !== undefined) persist.cpuThreads = cpuThreads;
|
|
41
|
+
if (contextLength !== undefined) persist.contextLength = contextLength;
|
|
42
|
+
if (body.splitMode !== undefined) persist.splitMode = body.splitMode;
|
|
43
|
+
if (body.tensorSplit !== undefined) persist.tensorSplit = body.tensorSplit;
|
|
44
|
+
if (body.mainGpu !== undefined) persist.mainGpu = body.mainGpu;
|
|
45
|
+
if (body.flashAttn !== undefined) persist.flashAttn = body.flashAttn;
|
|
46
|
+
if (body.batchSize !== undefined) persist.batchSize = body.batchSize;
|
|
47
|
+
if (body.ubatchSize !== undefined) persist.ubatchSize = body.ubatchSize;
|
|
48
|
+
if (body.kvCacheDtype !== undefined) persist.kvCacheDtype = body.kvCacheDtype;
|
|
49
|
+
if (Object.keys(persist).length > 0) {
|
|
50
|
+
await withSettingsWriteQueue(() => savePersistedModelLoadSettings(persist));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const loadSettings = getPersistedModelLoadSettings();
|
|
54
|
+
const autoFollow = parseAutoContextFlag(body.autoContext, loadSettings.autoContext ?? true);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
clearLmStudioProbeCache();
|
|
58
|
+
|
|
59
|
+
if (autoFollow) {
|
|
60
|
+
const resolvedGpu = gpuOffload !== undefined ? gpuOffload : loadSettings.gpuOffload;
|
|
61
|
+
const resolvedThreads = cpuThreads !== undefined ? cpuThreads : loadSettings.cpuThreads;
|
|
62
|
+
const resolvedCtx = contextLength !== undefined ? contextLength : loadSettings.contextLength;
|
|
63
|
+
const follow = await ensureModelContextFollow({
|
|
64
|
+
model,
|
|
65
|
+
requestedContext: resolvedCtx,
|
|
66
|
+
probeFn: () => probeLmStudioRuntime('http://127.0.0.1:1234/v1', { timeoutMs: 2000, bypassCache: true }),
|
|
67
|
+
loadFn: ({ contextLength: followCtx }) => wakeUpLmStudio({
|
|
68
|
+
baseUrl: 'http://127.0.0.1:1234/v1',
|
|
69
|
+
model,
|
|
70
|
+
maxWaitMs: 60000,
|
|
71
|
+
quiet: true,
|
|
72
|
+
gpuOffload: resolvedGpu,
|
|
73
|
+
cpuThreads: resolvedThreads,
|
|
74
|
+
contextLength: followCtx ?? resolvedCtx,
|
|
75
|
+
}),
|
|
76
|
+
unloadFn: () => unloadModelFromEngine('lms', model),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!follow.ok) {
|
|
80
|
+
return sendJson(res, 200, {
|
|
81
|
+
status: 'error',
|
|
82
|
+
message: follow.reason || 'Failed to wake up LM Studio',
|
|
83
|
+
modelLoad: loadSettings,
|
|
84
|
+
followApplied: false,
|
|
85
|
+
followReason: follow.followReason || null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const finalCtx = follow.loadedContextLength || follow.followContext || null;
|
|
90
|
+
let refreshedSettings = loadSettings;
|
|
91
|
+
if (finalCtx) {
|
|
92
|
+
await withSettingsWriteQueue(() => savePersistedModelLoadSettings({ contextLength: finalCtx }));
|
|
93
|
+
refreshedSettings = getPersistedModelLoadSettings();
|
|
94
|
+
}
|
|
95
|
+
clearLmStudioProbeCache();
|
|
96
|
+
return sendJson(res, 200, {
|
|
97
|
+
status: 'ok',
|
|
98
|
+
message: follow.reloadedForFollow
|
|
99
|
+
? `Model "${model}" reloaded with context ${Number(finalCtx).toLocaleString()} (model max ${follow.modelMaxContextLength ? Number(follow.modelMaxContextLength).toLocaleString() : 'unknown'})`
|
|
100
|
+
: `LM Studio already running with model loaded`,
|
|
101
|
+
awakened: Boolean(follow.performedLoad),
|
|
102
|
+
modelLoaded: true,
|
|
103
|
+
loadedModel: model,
|
|
104
|
+
loadedContextLength: follow.loadedContextLength || null,
|
|
105
|
+
modelMaxContextLength: follow.modelMaxContextLength || null,
|
|
106
|
+
followContext: follow.followContext || null,
|
|
107
|
+
followZone: follow.followZone || null,
|
|
108
|
+
followApplied: true,
|
|
109
|
+
followReason: follow.followReason || null,
|
|
110
|
+
quantization: null,
|
|
111
|
+
modelLoad: refreshedSettings,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await wakeUpLmStudio({
|
|
116
|
+
baseUrl: 'http://127.0.0.1:1234/v1',
|
|
117
|
+
model,
|
|
118
|
+
maxWaitMs: 60000,
|
|
119
|
+
quiet: true,
|
|
120
|
+
gpuOffload: gpuOffload !== undefined ? gpuOffload : loadSettings.gpuOffload,
|
|
121
|
+
cpuThreads: cpuThreads !== undefined ? cpuThreads : loadSettings.cpuThreads,
|
|
122
|
+
contextLength: contextLength !== undefined ? contextLength : loadSettings.contextLength,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
return sendJson(res, 200, {
|
|
127
|
+
status: 'error',
|
|
128
|
+
message: result.reason || 'Failed to wake up LM Studio',
|
|
129
|
+
modelLoad: loadSettings,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
clearLmStudioProbeCache();
|
|
134
|
+
const probe = await probeLmStudioRuntime('http://127.0.0.1:1234/v1', { timeoutMs: 3000, bypassCache: true }).catch(() => null);
|
|
135
|
+
|
|
136
|
+
sendJson(res, 200, {
|
|
137
|
+
status: 'ok',
|
|
138
|
+
message: result.awakened
|
|
139
|
+
? `Model "${model}" loaded successfully`
|
|
140
|
+
: `LM Studio already running with model loaded`,
|
|
141
|
+
awakened: Boolean(result.awakened),
|
|
142
|
+
modelLoaded: Boolean(result.modelLoaded),
|
|
143
|
+
loadedModel: probe?.loadedModel || model,
|
|
144
|
+
loadedContextLength: probe?.loadedContextLength || null,
|
|
145
|
+
modelMaxContextLength: probe?.modelMaxContextLength || null,
|
|
146
|
+
followContext: null,
|
|
147
|
+
followApplied: false,
|
|
148
|
+
followReason: 'auto-follow-disabled',
|
|
149
|
+
quantization: probe?.quantization || null,
|
|
150
|
+
modelLoad: loadSettings,
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
sendError(res, 500, `LM Studio load failed: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function handleLmStudioLoadSettingsRoute(req, res) {
|
|
158
|
+
if (req.method === 'GET') {
|
|
159
|
+
return sendJson(res, 200, { status: 'ok', modelLoad: getPersistedModelLoadSettings() });
|
|
160
|
+
}
|
|
161
|
+
if (!isTrustedRequest(req)) {
|
|
162
|
+
return sendError(res, 403, 'Changing model load settings requires local access');
|
|
163
|
+
}
|
|
164
|
+
let body;
|
|
165
|
+
try {
|
|
166
|
+
body = await parseJsonBody(req);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return sendError(res, 400, `Invalid JSON body: ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
const saved = await withSettingsWriteQueue(() => savePersistedModelLoadSettings(body));
|
|
171
|
+
sendJson(res, 200, { status: 'ok', modelLoad: saved });
|
|
172
|
+
}
|