Files
EasyCLIProxyAPI/src/pages/Kernel.tsx
T
lzt404 2649df000c feat: support DeepSeek Harness Desktop and refine management UI
- Detect and launch DeepSeek Harness Desktop on Windows
- Improve agent client selection and responsive layouts
- Unify dashboard runtime and connection card styling
- Streamline plugin navigation, cards, and installation dialogs
- Update translations and regression tests
2026-10-04 17:31:52 +08:00

302 lines
11 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { getVersion } from '@tauri-apps/api/app';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { LoaderCircle, Play, RefreshCw, RotateCw, Square } from 'lucide-react';
import { type CoreStatus, useCoreRuntime } from '../coreRuntime';
import { clientApiProfiles } from '../services/clientAccess';
import { useI18n } from '../i18n';
import { useAppUpdate } from '../appUpdate';
import { FloatingNotice, useAppNotice } from '../appNotice';
import { VersionManagementPage, displayAppVersion } from './VersionManagementPage';
import { HomeAccessPanel } from './HomeAccessPanel';
import { HomeOverviewCards } from './HomeOverviewCards';
import { CoreHealthPanel } from './CoreHealthPanel';
import { useHomeOverview } from './useHomeOverview';
import './HomeDashboard.css';
type CoreProcessCommand = 'start_core_process' | 'stop_core_process' | 'restart_core_process';
type GuiSettings = {
host: string;
port: number;
runOnStartup: boolean;
};
type CoreConfigSummary = {
apiKeys: Array<{ apiKey: string }>;
};
type CoreTlsSettings = {
enabled: boolean;
cert: string;
key: string;
};
export type KernelView = 'home' | 'versions';
export function KernelPage({ view = 'home' }: { view?: KernelView }) {
if (view === 'versions') {
return <VersionManagementPage />;
}
const { t } = useI18n();
const { info: appUpdate } = useAppUpdate();
const {
status: coreStatus,
statusError,
refreshStatus,
publishStatus,
} = useCoreRuntime();
const [installedAppVersion, setInstalledAppVersion] = useState('');
const [listenHost, setListenHost] = useState('127.0.0.1');
const [customPort, setCustomPort] = useState('8317');
const [processBusy, setProcessBusy] = useState(false);
const processFeedback = useAppNotice();
const { showNotice: showProcessNotice, clearNotice: clearProcessNotice } = processFeedback;
const copyFeedback = useAppNotice();
const [copiedApiField, setCopiedApiField] = useState('');
const [homeApiKey, setHomeApiKey] = useState<string | null | undefined>(undefined);
const [homeApiKeyError, setHomeApiKeyError] = useState(false);
const [configRevision, setConfigRevision] = useState(0);
const [tlsEnabled, setTlsEnabled] = useState(false);
const savedPortRef = useRef(8317);
const copiedApiTimerRef = useRef<number | null>(null);
useEffect(() => {
let disposed = false;
let unlistenConfig: (() => void) | null = null;
void listen('config-files-changed', () => {
if (disposed) return;
setConfigRevision((value) => value + 1);
void loadGuiSettings();
void loadTlsSettings();
void refreshStatus();
void loadHomeApiKey();
}).then((stop) => {
if (disposed) stop();
else unlistenConfig = stop;
});
loadGuiSettings();
void loadTlsSettings();
void getVersion()
.then((version) => {
if (!disposed) setInstalledAppVersion(version);
})
.catch(() => undefined);
void loadHomeApiKey();
return () => {
disposed = true;
unlistenConfig?.();
if (copiedApiTimerRef.current !== null) {
window.clearTimeout(copiedApiTimerRef.current);
}
};
}, []);
const runCoreProcessCommand = async (command: CoreProcessCommand) => {
const actionLabel =
command === 'start_core_process'
? t('kernel.action.start')
: command === 'stop_core_process'
? t('kernel.action.stop')
: t('kernel.action.restart');
setProcessBusy(true);
clearProcessNotice();
try {
const result = await invoke<CoreStatus>(command);
publishStatus(result);
if (command === 'restart_core_process') {
showProcessNotice({ key: 'kernel.notice.restarted' }, 'success');
}
return true;
} catch (error) {
const errorMessage = String(error);
await refreshStatus();
showProcessNotice(
{ key: 'kernel.notice.actionFailed', variables: { action: actionLabel, error: errorMessage } },
'error',
);
return false;
} finally {
setProcessBusy(false);
}
};
const loadGuiSettings = async () => {
try {
const settings = await invoke<GuiSettings>('get_gui_settings');
setListenHost(settings.host);
setCustomPort(String(settings.port));
savedPortRef.current = settings.port;
} catch {}
};
const loadHomeApiKey = async () => {
try {
const settings = await invoke<CoreConfigSummary>('get_core_config_settings');
setHomeApiKey(settings.apiKeys[0]?.apiKey ?? null);
setHomeApiKeyError(false);
} catch {
setHomeApiKey(undefined);
setHomeApiKeyError(true);
}
};
const loadTlsSettings = async () => {
try {
const settings = await invoke<CoreTlsSettings>('get_core_tls_settings');
setTlsEnabled(settings.enabled);
} catch {
setTlsEnabled(false);
}
};
const copyApiValue = async (value: string, field: string) => {
copyFeedback.clearNotice();
try {
await navigator.clipboard.writeText(value);
setCopiedApiField(field);
if (copiedApiTimerRef.current !== null) {
window.clearTimeout(copiedApiTimerRef.current);
}
copiedApiTimerRef.current = window.setTimeout(() => {
setCopiedApiField('');
copiedApiTimerRef.current = null;
}, 1800);
} catch {
copyFeedback.showNotice({ key: 'kernel.notice.copyFailed' }, 'error');
}
};
const currentVersion = coreStatus?.currentVersion ?? '';
const coreInstalled = Boolean(coreStatus?.installed);
const coreRunning = Boolean(coreStatus?.running);
const coreReady = Boolean(coreStatus?.ready);
const coreProcessBusy = processBusy || Boolean(coreStatus?.starting);
const statusTone = statusError ? 'error' : coreProcessBusy ? 'pending' : coreRunning ? 'success' : 'neutral';
const statusLabel = statusError
? t('common.detectionFailed')
: !coreStatus
? t('common.detecting')
: coreProcessBusy
? t('common.processing')
: coreRunning
? t('kernel.status.running')
: t(coreInstalled ? 'kernel.status.stopped' : 'kernel.status.notInstalled');
const resolvedAppVersion = appUpdate?.currentVersion || installedAppVersion;
const currentAppVersion = resolvedAppVersion
? displayAppVersion(resolvedAppVersion)
: t('common.detecting');
const apiPort = Number(customPort);
const apiProfiles = clientApiProfiles(
Number.isInteger(apiPort) && apiPort >= 1 && apiPort <= 65535
? apiPort
: savedPortRef.current,
tlsEnabled,
listenHost,
);
const healthContext = [listenHost, customPort, tlsEnabled, coreStatus?.processId, configRevision].join(':');
const overview = useHomeOverview(coreReady, healthContext);
return (
<section className="page kernel-page home-page home-dashboard">
<h1 className="sr-only">{t('app.nav.home')}</h1>
<div className="home-top-grid">
<div className="panel control-panel">
<div className="panel-heading home-panel-heading">
<div>
<h2>{t('kernel.control.title')}</h2>
<p>{t(coreReady ? 'home.runtime.ready' : coreInstalled ? 'home.runtime.stopped' : 'home.runtime.notInstalled')}</p>
</div>
<button type="button" className="icon-button quiet home-runtime-refresh" disabled={coreProcessBusy} onClick={() => void refreshStatus()} title={t('kernel.control.refresh')} aria-label={t('kernel.control.refresh')}><RefreshCw size={15} aria-hidden="true" /></button>
</div>
<dl className="panel-detail-grid home-runtime-details">
<div className="panel-detail-row" data-runtime="installation">
<dt>{t('kernel.control.installStatus')}</dt>
<dd>{coreStatus ? t(coreInstalled ? 'kernel.control.installed' : 'kernel.status.notInstalled') : t(statusError ? 'common.detectionFailed' : 'common.detecting')}</dd>
</div>
<div className="panel-detail-row" data-runtime="status">
<dt>{t('kernel.control.runStatus')}</dt>
<dd><span className={`home-runtime-state ${statusTone}`} role="status" title={statusError || undefined}>
{coreProcessBusy ? <LoaderCircle size={12} className="spin" aria-hidden="true" /> : <span className="home-runtime-dot" aria-hidden="true" />}
{statusLabel}
</span></dd>
</div>
<div className="panel-detail-row">
<dt>{t('kernel.overview.coreVersion')}</dt>
<dd>
<span className="mono-tag">
{currentVersion
|| (coreInstalled ? t('common.unavailable') : t('kernel.status.notInstalled'))}
</span>
</dd>
</div>
<div className="panel-detail-row">
<dt>{t('kernel.overview.appVersion')}</dt>
<dd><span className="mono-tag">{currentAppVersion}</span></dd>
</div>
<div className="panel-detail-row">
<dt>{t('kernel.control.pid')}</dt>
<dd><span className="mono-tag">{coreStatus?.processId || t('kernel.control.noPid')}</span></dd>
</div>
<div className="panel-detail-row">
<dt>{t('home.runtime.port')}</dt>
<dd><span className="mono-tag">{customPort}</span></dd>
</div>
</dl>
{statusError && <p className="home-runtime-error" role="alert">{statusError}</p>}
<div className="button-row panel-action-row control-action-row">
<button
type="button"
className={coreRunning ? 'secondary-button home-runtime-stop' : 'primary-button'}
disabled={!coreInstalled || coreProcessBusy}
onClick={() =>
void runCoreProcessCommand(
coreRunning ? 'stop_core_process' : 'start_core_process',
)
}
>
{coreRunning ? <Square size={13} aria-hidden="true" /> : <Play size={14} aria-hidden="true" />}
{coreProcessBusy ? t('common.processing') : coreRunning ? t('kernel.action.stop') : t('kernel.action.start')}
</button>
<button
type="button"
className="secondary-button"
disabled={!coreInstalled || !coreRunning || coreProcessBusy}
onClick={() =>
void runCoreProcessCommand('restart_core_process')
}
>
<RotateCw size={14} aria-hidden="true" />
{t('kernel.action.restart')}
</button>
</div>
<FloatingNotice key={processFeedback.revision} notice={processFeedback.notice} onDismiss={processFeedback.clearNotice} />
</div>
<HomeAccessPanel
profiles={apiProfiles} apiKey={homeApiKey} keyError={homeApiKeyError}
ready={coreReady} copiedField={copiedApiField} onCopy={copyApiValue}
/>
</div>
<FloatingNotice key={copyFeedback.revision} notice={copyFeedback.notice} onDismiss={copyFeedback.clearNotice} />
<HomeOverviewCards snapshot={overview.snapshot} loading={overview.loading} coreReady={coreReady} onRefresh={overview.refresh} actions={<CoreHealthPanel compact
coreReady={coreReady} models={overview.snapshot?.models ?? []}
modelsLoading={overview.loading}
modelsError={overview.snapshot?.errors.models ?? ''}
onRefreshModels={overview.refresh} contextKey={healthContext}
/>} />
</section>
);
}