@geminilight/mindos 0.6.52 → 0.6.53
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/_standalone/.mindos-build-version +1 -1
- package/_standalone/.next/BUILD_ID +1 -1
- package/_standalone/.next/app-path-routes-manifest.json +16 -16
- package/_standalone/.next/build-manifest.json +2 -2
- package/_standalone/.next/cache/.previewinfo +1 -1
- package/_standalone/.next/cache/.rscinfo +1 -1
- package/_standalone/.next/cache/config.json +3 -3
- package/_standalone/.next/prerender-manifest.json +3 -3
- package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error.html +2 -2
- package/_standalone/.next/server/app/_global-error.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/_standalone/.next/server/app/_not-found/page.js +1 -1
- package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/page.js +1 -1
- package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask/route.js +44 -16
- package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/restart/route.js +1 -1
- package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update/route.js +1 -1
- package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changes/page.js +1 -1
- package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/page.js +1 -1
- package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/explore/page.js +1 -1
- package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/help/page.js +1 -1
- package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/login/page.js +1 -1
- package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/page.js +1 -1
- package/_standalone/.next/server/app/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/setup/page.js +1 -1
- package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/trash/page.js +3 -3
- package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
- package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app-paths-manifest.json +16 -16
- package/_standalone/.next/server/chunks/3436.js +52 -0
- package/_standalone/.next/server/chunks/6539.js +1 -1
- package/_standalone/.next/server/chunks/8388.js +1 -1
- package/_standalone/.next/server/chunks/8408.js +28 -28
- package/_standalone/.next/server/pages/500.html +2 -2
- package/_standalone/.next/server/server-reference-manifest.js +1 -1
- package/_standalone/.next/server/server-reference-manifest.json +1 -1
- package/_standalone/.next/static/chunks/{1053-c519cb347faa75e2.js → 1053-17786066ecd6f216.js} +2 -2
- package/_standalone/.next/static/chunks/app/{layout-e706f9a963b177ba.js → layout-e5d2fb384207d821.js} +27 -27
- package/_standalone/.next/static/chunks/app/{page-2595175820acfd65.js → page-54edd045d3448df5.js} +2 -2
- package/_standalone/.next/static/chunks/app/trash/page-6372bb3f5463affa.js +1 -0
- package/_standalone/.next/static/chunks/app/view/[...path]/page-f3d1d87190dcfe28.js +12 -0
- package/_standalone/.next/trace +64 -64
- package/_standalone/components/walkthrough/WalkthroughOverlay.tsx +39 -10
- package/_standalone/lib/stores/walkthrough-store.ts +33 -1
- package/_standalone/tsconfig.tsbuildinfo +1 -1
- package/app/app/api/ask/route.ts +193 -3
- package/app/app/api/restart/route.ts +26 -21
- package/app/app/api/update/route.ts +24 -10
- package/app/components/walkthrough/WalkthroughOverlay.tsx +39 -10
- package/app/lib/agent/model.ts +2 -1
- package/app/lib/i18n/modules/ai-chat.ts +8 -0
- package/app/lib/settings.ts +32 -0
- package/app/lib/stores/walkthrough-store.ts +33 -1
- package/bin/commands/update.js +23 -11
- package/bin/lib/clean-env.js +38 -0
- package/bin/lib/gateway.js +41 -4
- package/bin/lib/stop.js +40 -4
- package/package.json +1 -1
- package/_standalone/.next/server/chunks/2673.js +0 -52
- package/_standalone/.next/static/chunks/app/trash/page-61c6210fac1e9710.js +0 -1
- package/_standalone/.next/static/chunks/app/view/[...path]/page-798359c36f10ef8d.js +0 -12
- package/_standalone/lib/i18n/generated/explore-i18n.generated.ts +0 -138
- package/_standalone/lib/i18n/index.ts +0 -38
- package/_standalone/lib/i18n/modules/ai-chat.ts +0 -237
- package/_standalone/lib/i18n/modules/common.ts +0 -71
- package/_standalone/lib/i18n/modules/features.ts +0 -153
- package/_standalone/lib/i18n/modules/knowledge.ts +0 -727
- package/_standalone/lib/i18n/modules/navigation.ts +0 -157
- package/_standalone/lib/i18n/modules/onboarding.ts +0 -445
- package/_standalone/lib/i18n/modules/panels.ts +0 -1255
- package/_standalone/lib/i18n/modules/settings.ts +0 -889
- package/_standalone/lib/i18n.ts +0 -3
- /package/_standalone/.next/static/{IiTZbEzZo8l5MSUxlDwKp → W-wLgvhPtPJ5Psq9V4i3M}/_buildManifest.js +0 -0
- /package/_standalone/.next/static/{IiTZbEzZo8l5MSUxlDwKp → W-wLgvhPtPJ5Psq9V4i3M}/_ssgManifest.js +0 -0
package/app/app/api/ask/route.ts
CHANGED
|
@@ -26,7 +26,8 @@ import { AGENT_SYSTEM_PROMPT, ORGANIZE_SYSTEM_PROMPT, CHAT_SYSTEM_PROMPT } from
|
|
|
26
26
|
import type { AskModeApi } from '@/lib/types';
|
|
27
27
|
import { toAgentMessages } from '@/lib/agent/to-agent-messages';
|
|
28
28
|
import { logAgentOp } from '@/lib/agent/log';
|
|
29
|
-
import { readSettings } from '@/lib/settings';
|
|
29
|
+
import { readSettings, readBaseUrlCompat, writeBaseUrlCompat } from '@/lib/settings';
|
|
30
|
+
import { en as i18nEn, zh as i18nZh } from '@/lib/i18n';
|
|
30
31
|
import { MindOSError, apiError, ErrorCodes } from '@/lib/errors';
|
|
31
32
|
import { metrics } from '@/lib/metrics';
|
|
32
33
|
import { assertNotProtected } from '@/lib/core';
|
|
@@ -400,6 +401,134 @@ function toPiCustomToolDefinitions(tools: AgentTool<any>[]): ToolDefinition<any,
|
|
|
400
401
|
}));
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// Non-streaming fallback for proxies that don't support stream + tools
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Mini agent loop using non-streaming OpenAI-compatible API.
|
|
410
|
+
* Used when a proxy silently breaks stream+tools by returning plain text.
|
|
411
|
+
* Emits SSE events identical to the streaming path so the frontend is unaffected.
|
|
412
|
+
*/
|
|
413
|
+
async function runNonStreamingFallback(opts: {
|
|
414
|
+
baseUrl: string;
|
|
415
|
+
apiKey: string;
|
|
416
|
+
model: string;
|
|
417
|
+
systemPrompt: string;
|
|
418
|
+
historyMessages: { role: string; content: unknown }[];
|
|
419
|
+
userContent: string;
|
|
420
|
+
tools: AgentTool<any>[];
|
|
421
|
+
send: (event: MindOSSSEvent) => void;
|
|
422
|
+
signal: AbortSignal;
|
|
423
|
+
maxSteps: number;
|
|
424
|
+
}): Promise<void> {
|
|
425
|
+
const { baseUrl, apiKey, model, systemPrompt, historyMessages, userContent, tools, send, signal, maxSteps } = opts;
|
|
426
|
+
|
|
427
|
+
const openaiTools = tools.map(t => ({
|
|
428
|
+
type: 'function' as const,
|
|
429
|
+
function: {
|
|
430
|
+
name: t.name,
|
|
431
|
+
description: t.description ?? '',
|
|
432
|
+
parameters: (t as any).parameters ?? { type: 'object', properties: {} },
|
|
433
|
+
},
|
|
434
|
+
}));
|
|
435
|
+
|
|
436
|
+
const messages: { role: string; content: unknown; tool_calls?: unknown; tool_call_id?: string }[] = [
|
|
437
|
+
{ role: 'system', content: systemPrompt },
|
|
438
|
+
...historyMessages,
|
|
439
|
+
{ role: 'user', content: userContent },
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
const toolMap = new Map(tools.map(t => [t.name, t]));
|
|
443
|
+
const endpoint = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
444
|
+
let step = 0;
|
|
445
|
+
|
|
446
|
+
while (step < maxSteps) {
|
|
447
|
+
if (signal.aborted) throw new Error('Request aborted');
|
|
448
|
+
step++;
|
|
449
|
+
|
|
450
|
+
const resp = await fetch(endpoint, {
|
|
451
|
+
method: 'POST',
|
|
452
|
+
headers: {
|
|
453
|
+
'Content-Type': 'application/json',
|
|
454
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
455
|
+
},
|
|
456
|
+
body: JSON.stringify({
|
|
457
|
+
model,
|
|
458
|
+
messages,
|
|
459
|
+
tools: openaiTools.length > 0 ? openaiTools : undefined,
|
|
460
|
+
tool_choice: openaiTools.length > 0 ? 'auto' : undefined,
|
|
461
|
+
stream: false,
|
|
462
|
+
}),
|
|
463
|
+
signal,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (!resp.ok) {
|
|
467
|
+
const errText = await resp.text().catch(() => '');
|
|
468
|
+
throw new Error(`Non-streaming API error ${resp.status}: ${errText.slice(0, 200)}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const data = await resp.json() as any;
|
|
472
|
+
const choice = data?.choices?.[0];
|
|
473
|
+
if (!choice) throw new Error('Empty response from API');
|
|
474
|
+
|
|
475
|
+
const msg = choice.message;
|
|
476
|
+
const finishReason: string = choice.finish_reason ?? 'stop';
|
|
477
|
+
|
|
478
|
+
// Emit text content in chunks to simulate streaming appearance
|
|
479
|
+
if (msg.content) {
|
|
480
|
+
const text: string = typeof msg.content === 'string' ? msg.content : '';
|
|
481
|
+
if (text) {
|
|
482
|
+
const chunkSize = 40;
|
|
483
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
484
|
+
send({ type: 'text_delta', delta: text.slice(i, i + chunkSize) });
|
|
485
|
+
await new Promise(r => setTimeout(r, 8));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// No tool calls or naturally stopped → done
|
|
491
|
+
if (finishReason === 'stop' || !msg.tool_calls?.length) break;
|
|
492
|
+
|
|
493
|
+
// Execute each tool call
|
|
494
|
+
const toolResultMessages: { role: string; tool_call_id: string; content: string }[] = [];
|
|
495
|
+
for (const tc of msg.tool_calls) {
|
|
496
|
+
const toolName = tc.function?.name ?? '';
|
|
497
|
+
const toolCallId = tc.id ?? `call_${Date.now()}`;
|
|
498
|
+
let parsedArgs: Record<string, unknown> = {};
|
|
499
|
+
try { parsedArgs = JSON.parse(tc.function?.arguments ?? '{}'); } catch { /* ignore */ }
|
|
500
|
+
|
|
501
|
+
const tool = toolMap.get(toolName);
|
|
502
|
+
send({ type: 'tool_start', toolCallId, toolName, args: parsedArgs });
|
|
503
|
+
|
|
504
|
+
let resultText = '';
|
|
505
|
+
let isError = false;
|
|
506
|
+
if (tool) {
|
|
507
|
+
try {
|
|
508
|
+
const result = await tool.execute(toolCallId, parsedArgs, signal);
|
|
509
|
+
resultText = result.content
|
|
510
|
+
.filter((c: any) => c.type === 'text')
|
|
511
|
+
.map((c: any) => c.text)
|
|
512
|
+
.join('\n');
|
|
513
|
+
} catch (err) {
|
|
514
|
+
resultText = err instanceof Error ? err.message : String(err);
|
|
515
|
+
isError = true;
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
resultText = `Tool "${toolName}" not found`;
|
|
519
|
+
isError = true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
send({ type: 'tool_end', toolCallId, output: resultText, isError });
|
|
523
|
+
toolResultMessages.push({ role: 'tool', tool_call_id: toolCallId, content: resultText });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Append assistant turn + tool results for next iteration
|
|
527
|
+
messages.push({ role: 'assistant', content: msg.content ?? null, tool_calls: msg.tool_calls });
|
|
528
|
+
messages.push(...toolResultMessages);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
403
532
|
// ---------------------------------------------------------------------------
|
|
404
533
|
// POST /api/ask
|
|
405
534
|
// ---------------------------------------------------------------------------
|
|
@@ -433,6 +562,10 @@ export async function POST(req: NextRequest) {
|
|
|
433
562
|
// Read agent config from settings
|
|
434
563
|
const serverSettings = readSettings();
|
|
435
564
|
const agentConfig = serverSettings.agent ?? {};
|
|
565
|
+
|
|
566
|
+
// Detect locale from Accept-Language header for i18n status messages
|
|
567
|
+
const acceptLang = req.headers.get('accept-language') ?? '';
|
|
568
|
+
const t = acceptLang.startsWith('zh') ? i18nZh.ask : i18nEn.ask;
|
|
436
569
|
const defaultMaxSteps = askMode === 'chat' ? 8 : (agentConfig.maxSteps ?? 20);
|
|
437
570
|
const stepLimit = Number.isFinite(body.maxSteps)
|
|
438
571
|
? Math.min(30, Math.max(1, Number(body.maxSteps)))
|
|
@@ -689,7 +822,7 @@ export async function POST(req: NextRequest) {
|
|
|
689
822
|
const provOverride = body.providerOverride && isProviderId(body.providerOverride)
|
|
690
823
|
? body.providerOverride as ProviderId
|
|
691
824
|
: undefined;
|
|
692
|
-
const { model, modelName, apiKey, provider } = getModelConfig({
|
|
825
|
+
const { model, modelName, apiKey, provider, baseUrl } = getModelConfig({
|
|
693
826
|
provider: provOverride,
|
|
694
827
|
hasImages: hasImages(messages),
|
|
695
828
|
});
|
|
@@ -788,6 +921,7 @@ export async function POST(req: NextRequest) {
|
|
|
788
921
|
|
|
789
922
|
let hasContent = false;
|
|
790
923
|
let lastModelError = '';
|
|
924
|
+
const effectiveBaseUrlKey = baseUrl || 'default';
|
|
791
925
|
|
|
792
926
|
session.subscribe((event: AgentEvent) => {
|
|
793
927
|
if (isTextDeltaEvent(event)) {
|
|
@@ -993,6 +1127,36 @@ export async function POST(req: NextRequest) {
|
|
|
993
1127
|
safeClose();
|
|
994
1128
|
} else {
|
|
995
1129
|
// Route to MindOS agent (existing logic)
|
|
1130
|
+
|
|
1131
|
+
// ── Proxy compatibility check ──
|
|
1132
|
+
// If this baseUrl is known to reject stream+tools, skip session.prompt() entirely
|
|
1133
|
+
// and go straight to the non-streaming fallback path.
|
|
1134
|
+
const compatCache = readBaseUrlCompat();
|
|
1135
|
+
if (compatCache[effectiveBaseUrlKey] === 'non-streaming' && baseUrl && provider === 'openai') {
|
|
1136
|
+
send({ type: 'status', message: t.proxyCompatMode });
|
|
1137
|
+
try {
|
|
1138
|
+
await runNonStreamingFallback({
|
|
1139
|
+
baseUrl,
|
|
1140
|
+
apiKey,
|
|
1141
|
+
model: modelName,
|
|
1142
|
+
systemPrompt,
|
|
1143
|
+
historyMessages: llmHistoryMessages,
|
|
1144
|
+
userContent: typeof lastUserContent === 'string' ? lastUserContent : JSON.stringify(lastUserContent),
|
|
1145
|
+
tools: requestTools,
|
|
1146
|
+
send,
|
|
1147
|
+
signal: req.signal,
|
|
1148
|
+
maxSteps: stepLimit,
|
|
1149
|
+
});
|
|
1150
|
+
metrics.recordRequest(Date.now() - requestStartTime);
|
|
1151
|
+
send({ type: 'done' });
|
|
1152
|
+
} catch (fallbackErr) {
|
|
1153
|
+
metrics.recordRequest(Date.now() - requestStartTime);
|
|
1154
|
+
send({ type: 'error', message: t.proxyCompatFailed(fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)) });
|
|
1155
|
+
}
|
|
1156
|
+
safeClose();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
996
1160
|
// Retry with exponential backoff for transient failures (timeout, rate limit, 5xx).
|
|
997
1161
|
// Only retry if no content has been streamed yet — once the user sees partial
|
|
998
1162
|
// output, retrying would produce duplicate/garbled content.
|
|
@@ -1022,7 +1186,33 @@ export async function POST(req: NextRequest) {
|
|
|
1022
1186
|
|
|
1023
1187
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
1024
1188
|
if (!hasContent && lastModelError) {
|
|
1025
|
-
|
|
1189
|
+
// No content received — check if this looks like a proxy stream+tools incompatibility.
|
|
1190
|
+
// Only attempt fallback for OpenAI-compatible endpoints with a custom baseUrl.
|
|
1191
|
+
if (baseUrl && provider === 'openai') {
|
|
1192
|
+
send({ type: 'status', message: t.proxyCompatDetecting });
|
|
1193
|
+
try {
|
|
1194
|
+
await runNonStreamingFallback({
|
|
1195
|
+
baseUrl,
|
|
1196
|
+
apiKey,
|
|
1197
|
+
model: modelName,
|
|
1198
|
+
systemPrompt,
|
|
1199
|
+
historyMessages: llmHistoryMessages,
|
|
1200
|
+
userContent: typeof lastUserContent === 'string' ? lastUserContent : JSON.stringify(lastUserContent),
|
|
1201
|
+
tools: requestTools,
|
|
1202
|
+
send,
|
|
1203
|
+
signal: req.signal,
|
|
1204
|
+
maxSteps: stepLimit,
|
|
1205
|
+
});
|
|
1206
|
+
// Success → cache this endpoint as non-streaming so future requests skip the probe
|
|
1207
|
+
writeBaseUrlCompat(effectiveBaseUrlKey, 'non-streaming');
|
|
1208
|
+
console.log(`[ask] Proxy compat detected: ${effectiveBaseUrlKey} → non-streaming (cached)`);
|
|
1209
|
+
send({ type: 'done' });
|
|
1210
|
+
} catch (fallbackErr) {
|
|
1211
|
+
send({ type: 'error', message: t.proxyCompatAlsoFailed(fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)) });
|
|
1212
|
+
}
|
|
1213
|
+
} else {
|
|
1214
|
+
send({ type: 'error', message: lastModelError });
|
|
1215
|
+
}
|
|
1026
1216
|
} else {
|
|
1027
1217
|
send({ type: 'done' });
|
|
1028
1218
|
}
|
|
@@ -3,31 +3,36 @@ import { NextResponse } from 'next/server';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Strip ALL MINDOS_ and MIND_ prefixed env vars so the restart child
|
|
8
|
+
* process re-derives paths from its own installation root.
|
|
9
|
+
* Preserves old port values via MINDOS_OLD_ for cleanup.
|
|
10
|
+
*/
|
|
11
|
+
function cleanEnvForRestart(): { env: NodeJS.ProcessEnv; oldWebPort?: string; oldMcpPort?: string } {
|
|
12
|
+
const cleaned = { ...process.env };
|
|
13
|
+
const oldWebPort = cleaned.MINDOS_WEB_PORT;
|
|
14
|
+
const oldMcpPort = cleaned.MINDOS_MCP_PORT;
|
|
15
|
+
for (const key of Object.keys(cleaned)) {
|
|
16
|
+
if (key.startsWith('MINDOS_') || key.startsWith('MIND_')) {
|
|
17
|
+
delete cleaned[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
delete cleaned.AUTH_TOKEN;
|
|
21
|
+
delete cleaned.WEB_PASSWORD;
|
|
22
|
+
delete cleaned.NODE_OPTIONS;
|
|
23
|
+
// Pass old ports so restart command can clean up stale listeners
|
|
24
|
+
if (oldWebPort) cleaned.MINDOS_OLD_WEB_PORT = oldWebPort;
|
|
25
|
+
if (oldMcpPort) cleaned.MINDOS_OLD_MCP_PORT = oldMcpPort;
|
|
26
|
+
return { env: cleaned, oldWebPort, oldMcpPort };
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
export async function POST() {
|
|
7
30
|
try {
|
|
31
|
+
// Resolve CLI path BEFORE cleaning env (we still need current vars)
|
|
8
32
|
const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.env.MINDOS_PROJECT_ROOT || process.cwd(), '..', 'bin', 'cli.js');
|
|
9
33
|
const nodeBin = process.env.MINDOS_NODE_BIN || process.execPath;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// its MCP child are still holding the ports.
|
|
13
|
-
//
|
|
14
|
-
// IMPORTANT: Strip MINDOS_* env vars so the child's loadConfig() reads
|
|
15
|
-
// the *updated* config file instead of inheriting stale values from this
|
|
16
|
-
// process. Without this, changing ports in the GUI has no effect on the
|
|
17
|
-
// restarted server — it would start on the old ports.
|
|
18
|
-
//
|
|
19
|
-
// Pass the current (old) ports via MINDOS_OLD_* so the restart command
|
|
20
|
-
// can clean up processes still listening on the previous ports.
|
|
21
|
-
const childEnv = { ...process.env };
|
|
22
|
-
const oldWebPort = childEnv.MINDOS_WEB_PORT;
|
|
23
|
-
const oldMcpPort = childEnv.MINDOS_MCP_PORT;
|
|
24
|
-
delete childEnv.MINDOS_WEB_PORT;
|
|
25
|
-
delete childEnv.MINDOS_MCP_PORT;
|
|
26
|
-
delete childEnv.MIND_ROOT;
|
|
27
|
-
delete childEnv.AUTH_TOKEN;
|
|
28
|
-
delete childEnv.WEB_PASSWORD;
|
|
29
|
-
if (oldWebPort) childEnv.MINDOS_OLD_WEB_PORT = oldWebPort;
|
|
30
|
-
if (oldMcpPort) childEnv.MINDOS_OLD_MCP_PORT = oldMcpPort;
|
|
34
|
+
|
|
35
|
+
const { env: childEnv } = cleanEnvForRestart();
|
|
31
36
|
const child = spawn(nodeBin, [cliPath, 'restart'], {
|
|
32
37
|
detached: true,
|
|
33
38
|
stdio: 'ignore',
|
|
@@ -3,25 +3,39 @@ import { NextResponse } from 'next/server';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Strip ALL MINDOS_ and MIND_ prefixed env vars so the update child
|
|
8
|
+
* process re-derives paths from its own installation root after npm install.
|
|
9
|
+
* This prevents the "fake update" bug where the new process inherits
|
|
10
|
+
* stale MINDOS_PROJECT_ROOT / MINDOS_CLI_PATH pointing to old code.
|
|
11
|
+
*/
|
|
12
|
+
function cleanEnvForUpdate(): NodeJS.ProcessEnv {
|
|
13
|
+
const cleaned = { ...process.env };
|
|
14
|
+
for (const key of Object.keys(cleaned)) {
|
|
15
|
+
if (key.startsWith('MINDOS_') || key.startsWith('MIND_')) {
|
|
16
|
+
delete cleaned[key];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
delete cleaned.AUTH_TOKEN;
|
|
20
|
+
delete cleaned.WEB_PASSWORD;
|
|
21
|
+
delete cleaned.NODE_OPTIONS;
|
|
22
|
+
return cleaned;
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
/**
|
|
7
26
|
* POST /api/update — trigger `mindos update` as a detached child process.
|
|
8
27
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
28
|
+
* Spawns the CLI command and returns immediately. The update process will
|
|
29
|
+
* npm install, remove build stamp, and restart the server.
|
|
30
|
+
* The current process will be killed during restart.
|
|
12
31
|
*/
|
|
13
32
|
export async function POST() {
|
|
14
33
|
try {
|
|
34
|
+
// Resolve CLI path BEFORE cleaning env (we still need current vars)
|
|
15
35
|
const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.env.MINDOS_PROJECT_ROOT || process.cwd(), '..', 'bin', 'cli.js');
|
|
16
36
|
const nodeBin = process.env.MINDOS_NODE_BIN || process.execPath;
|
|
17
37
|
|
|
18
|
-
|
|
19
|
-
const childEnv = { ...process.env };
|
|
20
|
-
delete childEnv.MINDOS_WEB_PORT;
|
|
21
|
-
delete childEnv.MINDOS_MCP_PORT;
|
|
22
|
-
delete childEnv.MIND_ROOT;
|
|
23
|
-
delete childEnv.AUTH_TOKEN;
|
|
24
|
-
delete childEnv.WEB_PASSWORD;
|
|
38
|
+
const childEnv = cleanEnvForUpdate();
|
|
25
39
|
|
|
26
40
|
const child = spawn(nodeBin, [cliPath, 'update'], {
|
|
27
41
|
detached: true,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useId } from 'react';
|
|
4
4
|
import { useLocale } from '@/lib/stores/locale-store';
|
|
5
|
-
import { useWalkthrough } from '@/lib/stores/walkthrough-store';
|
|
5
|
+
import { useWalkthrough, useWalkthroughStore } from '@/lib/stores/walkthrough-store';
|
|
6
6
|
import { walkthroughSteps } from './steps';
|
|
7
7
|
import WalkthroughTooltip from './WalkthroughTooltip';
|
|
8
8
|
|
|
@@ -68,16 +68,31 @@ export default function WalkthroughOverlay() {
|
|
|
68
68
|
return () => window.removeEventListener('keydown', handler, true);
|
|
69
69
|
}, [skipFn]);
|
|
70
70
|
|
|
71
|
-
// If target element doesn't exist (e.g.
|
|
71
|
+
// If target element doesn't exist (e.g. panel not enabled), auto-skip
|
|
72
|
+
// after a grace period. Use stable store ref to avoid re-triggering on
|
|
73
|
+
// every store change (which caused spurious skips during step transitions).
|
|
74
|
+
const currentStep = wt?.currentStep ?? 0;
|
|
72
75
|
useEffect(() => {
|
|
73
76
|
if (!step) return;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// Wait 500ms for DOM to settle (sidebar animations, lazy rendering)
|
|
78
|
+
const timer = setTimeout(() => {
|
|
79
|
+
const el = document.querySelector(`[data-walkthrough="${step.anchor}"]`);
|
|
80
|
+
if (!el) {
|
|
81
|
+
useWalkthroughStore.getState().next();
|
|
82
|
+
}
|
|
83
|
+
}, 500);
|
|
84
|
+
return () => clearTimeout(timer);
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
}, [currentStep]); // Only re-run when step index changes, not on every store update
|
|
87
|
+
|
|
88
|
+
// Safety timeout: if overlay stays for 60s without user interaction, auto-dismiss.
|
|
89
|
+
// Prevents permanently stuck overlay from any unforeseen edge case.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
useWalkthroughStore.getState().skip();
|
|
93
|
+
}, 60_000);
|
|
94
|
+
return () => clearTimeout(timer);
|
|
95
|
+
}, [currentStep]); // Reset on each step change
|
|
81
96
|
|
|
82
97
|
if (!wt || !step) return null;
|
|
83
98
|
|
|
@@ -138,7 +153,7 @@ export default function WalkthroughOverlay() {
|
|
|
138
153
|
/>
|
|
139
154
|
</svg>
|
|
140
155
|
|
|
141
|
-
{/* Tooltip */}
|
|
156
|
+
{/* Tooltip — shown when target element found */}
|
|
142
157
|
{targetRect && (
|
|
143
158
|
<WalkthroughTooltip
|
|
144
159
|
stepIndex={wt.currentStep}
|
|
@@ -146,6 +161,20 @@ export default function WalkthroughOverlay() {
|
|
|
146
161
|
position={step.position}
|
|
147
162
|
/>
|
|
148
163
|
)}
|
|
164
|
+
|
|
165
|
+
{/* Fallback dismiss button — shown when target element NOT found.
|
|
166
|
+
Without this, user sees dark overlay but no visible UI to dismiss it. */}
|
|
167
|
+
{!targetRect && (
|
|
168
|
+
<div className="fixed inset-0 z-[101] flex items-center justify-center pointer-events-none">
|
|
169
|
+
<button
|
|
170
|
+
onClick={wt.skip}
|
|
171
|
+
className="pointer-events-auto px-4 py-2 text-sm rounded-lg font-medium transition-all hover:opacity-90"
|
|
172
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
173
|
+
>
|
|
174
|
+
{wt.currentStep === wt.totalSteps - 1 ? '✓' : '→'}
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
149
178
|
</>
|
|
150
179
|
);
|
|
151
180
|
}
|
package/app/lib/agent/model.ts
CHANGED
|
@@ -32,6 +32,7 @@ export function getModelConfig(options?: ModelConfigOverrides): {
|
|
|
32
32
|
modelName: string;
|
|
33
33
|
apiKey: string;
|
|
34
34
|
provider: ProviderId;
|
|
35
|
+
baseUrl: string;
|
|
35
36
|
} {
|
|
36
37
|
const saved = effectiveAiConfig(options?.provider);
|
|
37
38
|
|
|
@@ -49,7 +50,7 @@ export function getModelConfig(options?: ModelConfigOverrides): {
|
|
|
49
50
|
model = ensureVisionCapable(model);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
return { model, modelName, apiKey: cfg.apiKey, provider: cfg.provider };
|
|
53
|
+
return { model, modelName, apiKey: cfg.apiKey, provider: cfg.provider, baseUrl: cfg.baseUrl };
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -30,6 +30,10 @@ export const aiChatEn = {
|
|
|
30
30
|
errorNoResponse: 'AI returned no content. Possible causes: model does not support streaming, proxy compatibility issue, or request exceeds context limit.',
|
|
31
31
|
reconnecting: (attempt: number, max: number) => `Connection lost. Reconnecting (${attempt}/${max})...`,
|
|
32
32
|
reconnectFailed: 'Connection failed after multiple attempts.',
|
|
33
|
+
proxyCompatMode: 'Using compatibility mode (non-streaming)...',
|
|
34
|
+
proxyCompatFailed: (err: string) => `Compatibility mode failed: ${err}. Please check your Base URL, API key, and model name.`,
|
|
35
|
+
proxyCompatDetecting: 'Detecting proxy compatibility, switching to non-streaming mode...',
|
|
36
|
+
proxyCompatAlsoFailed: (err: string) => `Compatibility mode also failed: ${err}. Please check your Base URL, API key, and model name.`,
|
|
33
37
|
retry: 'Retry',
|
|
34
38
|
suggestions: [
|
|
35
39
|
'Summarize this document',
|
|
@@ -148,6 +152,10 @@ export const aiChatZh = {
|
|
|
148
152
|
errorNoResponse: 'AI 未返回有效内容。可能原因:模型不支持流式输出、中转站兼容性问题、或请求超出上下文限制。',
|
|
149
153
|
reconnecting: (attempt: number, max: number) => `连接中断,正在重连 (${attempt}/${max})...`,
|
|
150
154
|
reconnectFailed: '多次重连失败,请检查网络后重试。',
|
|
155
|
+
proxyCompatMode: '正在以兼容模式调用...',
|
|
156
|
+
proxyCompatFailed: (err: string) => `兼容模式失败:${err}。请检查 Base URL、API Key 和模型名称。`,
|
|
157
|
+
proxyCompatDetecting: '正在检测接口兼容性,切换到兼容模式重试...',
|
|
158
|
+
proxyCompatAlsoFailed: (err: string) => `兼容模式也失败了:${err}。请检查 Base URL、API Key 和模型名称。`,
|
|
151
159
|
retry: '重试',
|
|
152
160
|
suggestions: [
|
|
153
161
|
'总结这篇文档',
|
package/app/lib/settings.ts
CHANGED
|
@@ -51,6 +51,8 @@ export interface ServerSettings {
|
|
|
51
51
|
guideState?: GuideState;
|
|
52
52
|
/** Per-agent ACP overrides (command, args, env, enabled). Keyed by agent ID. */
|
|
53
53
|
acpAgents?: Record<string, import('./acp/agent-descriptors').AcpAgentOverride>;
|
|
54
|
+
/** Proxy compatibility cache: keyed by baseUrl, value is detected mode. */
|
|
55
|
+
baseUrlCompat?: Record<string, 'streaming' | 'non-streaming'>;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const DEFAULTS: ServerSettings = {
|
|
@@ -179,6 +181,15 @@ export function readSettings(): ServerSettings {
|
|
|
179
181
|
setupPending: parsed.setupPending === true ? true : undefined,
|
|
180
182
|
disabledSkills: Array.isArray(parsed.disabledSkills) ? parsed.disabledSkills as string[] : undefined,
|
|
181
183
|
guideState: parseGuideState(parsed.guideState),
|
|
184
|
+
baseUrlCompat: (() => {
|
|
185
|
+
const raw = parsed.baseUrlCompat;
|
|
186
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
187
|
+
const result: Record<string, 'streaming' | 'non-streaming'> = {};
|
|
188
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
189
|
+
if (v === 'streaming' || v === 'non-streaming') result[k] = v;
|
|
190
|
+
}
|
|
191
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
192
|
+
})(),
|
|
182
193
|
};
|
|
183
194
|
} catch {
|
|
184
195
|
// Config file missing or corrupt → force setup wizard
|
|
@@ -206,6 +217,7 @@ export function writeSettings(settings: ServerSettings): void {
|
|
|
206
217
|
if (settings.disabledSkills !== undefined) merged.disabledSkills = settings.disabledSkills;
|
|
207
218
|
if (settings.guideState !== undefined) merged.guideState = settings.guideState;
|
|
208
219
|
if (settings.acpAgents !== undefined) merged.acpAgents = settings.acpAgents;
|
|
220
|
+
if (settings.baseUrlCompat !== undefined) merged.baseUrlCompat = settings.baseUrlCompat;
|
|
209
221
|
// setupPending: false/undefined → remove the field (cleanup); true → set it
|
|
210
222
|
if ('setupPending' in settings) {
|
|
211
223
|
if (settings.setupPending) merged.setupPending = true;
|
|
@@ -287,3 +299,23 @@ export function effectiveSopRoot(): string {
|
|
|
287
299
|
const s = readSettings();
|
|
288
300
|
return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS', 'mind');
|
|
289
301
|
}
|
|
302
|
+
|
|
303
|
+
/** Read the baseUrl → compat mode cache from config. Never throws. */
|
|
304
|
+
export function readBaseUrlCompat(): Record<string, 'streaming' | 'non-streaming'> {
|
|
305
|
+
try {
|
|
306
|
+
const s = readSettings();
|
|
307
|
+
return s.baseUrlCompat ?? {};
|
|
308
|
+
} catch {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Persist a baseUrl compatibility detection result. Thread-safe via merge-write. */
|
|
314
|
+
export function writeBaseUrlCompat(baseUrl: string, mode: 'streaming' | 'non-streaming'): void {
|
|
315
|
+
const s = readSettings();
|
|
316
|
+
const updated: Record<string, 'streaming' | 'non-streaming'> = {
|
|
317
|
+
...(s.baseUrlCompat ?? {}),
|
|
318
|
+
[baseUrl]: mode,
|
|
319
|
+
};
|
|
320
|
+
writeSettings({ ...s, baseUrlCompat: updated });
|
|
321
|
+
}
|
|
@@ -21,7 +21,20 @@ export interface WalkthroughStoreState {
|
|
|
21
21
|
|
|
22
22
|
/* ── Helpers ── */
|
|
23
23
|
|
|
24
|
+
const LS_KEY = 'mindos_walkthrough_done';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Persist walkthrough state to server AND localStorage.
|
|
28
|
+
* localStorage acts as a safety net: even if the server persist fails
|
|
29
|
+
* (e.g. during update restart, network blip), the completion state
|
|
30
|
+
* survives page reload and prevents the "stuck overlay" bug.
|
|
31
|
+
*/
|
|
24
32
|
function persistStep(step: number, dismissed: boolean) {
|
|
33
|
+
// Local safety net — instant, survives server outage
|
|
34
|
+
if (step >= walkthroughSteps.length || dismissed) {
|
|
35
|
+
try { localStorage.setItem(LS_KEY, '1'); } catch {}
|
|
36
|
+
}
|
|
37
|
+
// Server persist — fire-and-forget (localStorage is the safety net)
|
|
25
38
|
fetch('/api/setup', {
|
|
26
39
|
method: 'PATCH',
|
|
27
40
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -31,6 +44,11 @@ function persistStep(step: number, dismissed: boolean) {
|
|
|
31
44
|
}).catch((err) => { console.warn('[walkthrough-store] persist failed:', err); });
|
|
32
45
|
}
|
|
33
46
|
|
|
47
|
+
/** Check if walkthrough was completed/dismissed (fast, sync, local) */
|
|
48
|
+
function isLocallyDone(): boolean {
|
|
49
|
+
try { return localStorage.getItem(LS_KEY) === '1'; } catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
/* ── Store ── */
|
|
35
53
|
|
|
36
54
|
export const useWalkthroughStore = create<WalkthroughStoreState>((set, get) => {
|
|
@@ -84,12 +102,26 @@ export const useWalkthroughStore = create<WalkthroughStoreState>((set, get) => {
|
|
|
84
102
|
// Only auto-start on desktop
|
|
85
103
|
if (window.innerWidth < 768) return () => {};
|
|
86
104
|
|
|
105
|
+
// Fast local check: if walkthrough was completed/dismissed, never reactivate.
|
|
106
|
+
// This prevents the "stuck overlay" bug where server persist failed during
|
|
107
|
+
// update restart but localStorage recorded the completion.
|
|
108
|
+
if (isLocallyDone()) return () => {};
|
|
109
|
+
|
|
87
110
|
fetch('/api/setup')
|
|
88
111
|
.then(r => r.json())
|
|
89
112
|
.then(data => {
|
|
90
113
|
const gs = data.guideState;
|
|
91
114
|
if (!gs) return;
|
|
92
|
-
if (gs.walkthroughDismissed)
|
|
115
|
+
if (gs.walkthroughDismissed) {
|
|
116
|
+
try { localStorage.setItem(LS_KEY, '1'); } catch {} // sync local
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If server says completed (step >= totalSteps), mark locally done
|
|
121
|
+
if (typeof gs.walkthroughStep === 'number' && gs.walkthroughStep >= totalSteps) {
|
|
122
|
+
try { localStorage.setItem(LS_KEY, '1'); } catch {};
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
93
125
|
|
|
94
126
|
if (gs.active && !gs.dismissed && gs.walkthroughStep === undefined) {
|
|
95
127
|
if (isWelcome) {
|
package/bin/commands/update.js
CHANGED
|
@@ -15,6 +15,7 @@ import { EXIT } from '../lib/command.js';
|
|
|
15
15
|
import { stopMindos } from '../lib/stop.js';
|
|
16
16
|
import { getLocalIP } from '../lib/startup.js';
|
|
17
17
|
import { isPortInUse } from '../lib/port.js';
|
|
18
|
+
import { cleanEnvForRestart } from '../lib/clean-env.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Dynamically resolve the new ROOT after `npm install -g`.
|
|
@@ -180,7 +181,7 @@ export const run = async () => {
|
|
|
180
181
|
const webPort = updateConfig.port ?? 3456;
|
|
181
182
|
const mcpPort = updateConfig.mcpPort ?? 8781;
|
|
182
183
|
console.log(dim(' (Waiting for Web UI to come back up — first run after update includes a rebuild...)'));
|
|
183
|
-
const ready = await gateway.waitForHttp(Number(webPort), { retries: 450, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH });
|
|
184
|
+
const ready = await gateway.waitForHttp(Number(webPort), { retries: 450, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH, expectedVersion: newVersion });
|
|
184
185
|
if (ready) {
|
|
185
186
|
const localIP = getLocalIP();
|
|
186
187
|
console.log(`\n${'─'.repeat(53)}`);
|
|
@@ -214,13 +215,29 @@ export const run = async () => {
|
|
|
214
215
|
if (wasRunning) {
|
|
215
216
|
console.log(cyan('\n MindOS is running — restarting to apply the new version...'));
|
|
216
217
|
stopMindos();
|
|
217
|
-
// Wait for ports to free (up to
|
|
218
|
-
|
|
218
|
+
// Wait for ports to free (up to 20s) with stabilization check.
|
|
219
|
+
// After first "free" reading, wait 1s and check again to avoid
|
|
220
|
+
// false negatives from TCP TIME_WAIT flickering.
|
|
221
|
+
const deadline = Date.now() + 20_000;
|
|
222
|
+
let portsFree = false;
|
|
219
223
|
while (Date.now() < deadline) {
|
|
220
224
|
const busy = await isPortInUse(webPort) || await isPortInUse(mcpPort);
|
|
221
|
-
if (!busy)
|
|
225
|
+
if (!busy) {
|
|
226
|
+
// Stabilization: wait 1s, then double-check
|
|
227
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
228
|
+
const stillFree = !(await isPortInUse(webPort)) && !(await isPortInUse(mcpPort));
|
|
229
|
+
if (stillFree) { portsFree = true; break; }
|
|
230
|
+
}
|
|
222
231
|
await new Promise((r) => setTimeout(r, 500));
|
|
223
232
|
}
|
|
233
|
+
if (!portsFree) {
|
|
234
|
+
console.log(yellow(' ⚠ Ports not fully released, force-killing remaining processes...'));
|
|
235
|
+
// Last resort: import killByPort and SIGKILL anything on these ports
|
|
236
|
+
const stopLib = await import('../lib/stop.js');
|
|
237
|
+
stopLib.killByPort(webPort);
|
|
238
|
+
stopLib.killByPort(mcpPort);
|
|
239
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
240
|
+
}
|
|
224
241
|
|
|
225
242
|
// Stage 3: Rebuild
|
|
226
243
|
writeUpdateStatus('rebuilding', vOpts);
|
|
@@ -237,12 +254,7 @@ export const run = async () => {
|
|
|
237
254
|
// (`mindos start` has its own build-on-startup logic)
|
|
238
255
|
writeUpdateStatus('restarting', vOpts);
|
|
239
256
|
const newCliPath = resolve(updatedRoot, 'bin', 'cli.js');
|
|
240
|
-
const childEnv =
|
|
241
|
-
delete childEnv.MINDOS_WEB_PORT;
|
|
242
|
-
delete childEnv.MINDOS_MCP_PORT;
|
|
243
|
-
delete childEnv.MIND_ROOT;
|
|
244
|
-
delete childEnv.AUTH_TOKEN;
|
|
245
|
-
delete childEnv.WEB_PASSWORD;
|
|
257
|
+
const childEnv = cleanEnvForRestart();
|
|
246
258
|
const child = nodeSpawn(
|
|
247
259
|
process.execPath, [newCliPath, 'start'],
|
|
248
260
|
{ detached: true, stdio: 'ignore', env: childEnv },
|
|
@@ -250,7 +262,7 @@ export const run = async () => {
|
|
|
250
262
|
child.unref();
|
|
251
263
|
|
|
252
264
|
console.log(dim(' (Waiting for Web UI to come back up...)'));
|
|
253
|
-
const ready = await gateway.waitForHttp(webPort, { retries: 180, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH });
|
|
265
|
+
const ready = await gateway.waitForHttp(webPort, { retries: 180, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH, expectedVersion: newVersion });
|
|
254
266
|
if (ready) {
|
|
255
267
|
const localIP = getLocalIP();
|
|
256
268
|
console.log(`\n${'─'.repeat(53)}`);
|