refactor(startup): simplify stack to Tauri-managed OpenFang + optional ChromeDriver
- Remove OpenFang CLI dependency from startup scripts - OpenFang now bundled with Tauri and managed via gateway_start/gateway_status commands - Add bootstrap screen in App.tsx to auto-start local gateway before UI loads - Update Makefile: replace start-no-gateway with start-desktop-only - Fix gateway config endpoints: use /api/config instead of /api/config/quick - Add Playwright dependencies for future E2E testing
This commit is contained in:
@@ -14,16 +14,31 @@ import { useTeamStore } from './store/teamStore';
|
||||
import { getStoredGatewayToken } from './lib/gateway-client';
|
||||
import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations';
|
||||
import { silentErrorHandler } from './lib/error-utils';
|
||||
import { Bot, Users } from 'lucide-react';
|
||||
import { Bot, Users, Loader2 } from 'lucide-react';
|
||||
import { EmptyState } from './components/ui';
|
||||
import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway';
|
||||
|
||||
type View = 'main' | 'settings';
|
||||
|
||||
// Bootstrap component that ensures OpenFang is running before rendering main UI
|
||||
function BootstrapScreen({ status }: { status: string }) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||
<p className="text-gray-600 text-sm">{status}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [view, setView] = useState<View>('main');
|
||||
const [mainContentView, setMainContentView] = useState<MainViewType>('chat');
|
||||
const [selectedHandId, setSelectedHandId] = useState<string | undefined>(undefined);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(undefined);
|
||||
const [bootstrapping, setBootstrapping] = useState(true);
|
||||
const [bootstrapStatus, setBootstrapStatus] = useState('Initializing...');
|
||||
const { connect, connectionState } = useGatewayStore();
|
||||
const { activeTeam, setActiveTeam, teams } = useTeamStore();
|
||||
|
||||
@@ -31,12 +46,61 @@ function App() {
|
||||
document.title = 'ZCLAW';
|
||||
}, []);
|
||||
|
||||
// Bootstrap: Start OpenFang Gateway before rendering main UI
|
||||
useEffect(() => {
|
||||
if (connectionState === 'disconnected') {
|
||||
const gatewayToken = getStoredGatewayToken();
|
||||
connect(undefined, gatewayToken).catch(silentErrorHandler('App'));
|
||||
}
|
||||
}, [connect, connectionState]);
|
||||
let mounted = true;
|
||||
|
||||
const bootstrap = async () => {
|
||||
try {
|
||||
// Step 1: Check and start local gateway in Tauri environment
|
||||
if (isTauriRuntime()) {
|
||||
setBootstrapStatus('Checking gateway status...');
|
||||
|
||||
try {
|
||||
const status = await getLocalGatewayStatus();
|
||||
const isRunning = status.portStatus === 'busy' || status.listenerPids.length > 0;
|
||||
|
||||
if (!isRunning && status.cliAvailable) {
|
||||
setBootstrapStatus('Starting OpenFang Gateway...');
|
||||
console.log('[App] Local gateway not running, auto-starting...');
|
||||
|
||||
await startLocalGateway();
|
||||
|
||||
// Wait for gateway to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
console.log('[App] Local gateway started');
|
||||
} else if (isRunning) {
|
||||
console.log('[App] Local gateway already running');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[App] Failed to check/start local gateway:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Step 2: Connect to gateway
|
||||
setBootstrapStatus('Connecting to gateway...');
|
||||
const gatewayToken = getStoredGatewayToken();
|
||||
await connect(undefined, gatewayToken);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Step 3: Bootstrap complete
|
||||
setBootstrapping(false);
|
||||
} catch (err) {
|
||||
console.error('[App] Bootstrap failed:', err);
|
||||
// Still allow app to load, connection status will show error
|
||||
setBootstrapping(false);
|
||||
}
|
||||
};
|
||||
|
||||
bootstrap();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
// 当切换到非 hands 视图时清除选中的 Hand
|
||||
const handleMainViewChange = (view: MainViewType) => {
|
||||
@@ -59,6 +123,11 @@ function App() {
|
||||
return <SettingsLayout onBack={() => setView('main')} />;
|
||||
}
|
||||
|
||||
// Show bootstrap screen while starting gateway
|
||||
if (bootstrapping) {
|
||||
return <BootstrapScreen status={bootstrapStatus} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden text-gray-800 text-sm">
|
||||
{/* 左侧边栏 */}
|
||||
|
||||
@@ -1086,7 +1086,28 @@ export class GatewayClient {
|
||||
}
|
||||
async getQuickConfig(): Promise<any> {
|
||||
try {
|
||||
return await this.restGet('/api/config/quick');
|
||||
// Use /api/config endpoint (OpenFang's actual config endpoint)
|
||||
const config = await this.restGet('/api/config');
|
||||
// Map OpenFang config to frontend expected format
|
||||
return {
|
||||
quickConfig: {
|
||||
agentName: 'ZCLAW',
|
||||
agentRole: 'AI 助手',
|
||||
userName: '用户',
|
||||
userRole: '用户',
|
||||
agentNickname: 'ZCLAW',
|
||||
scenarios: ['通用对话', '代码助手', '文档编写'],
|
||||
workspaceDir: config.data_dir || config.home_dir,
|
||||
gatewayUrl: this.baseUrl,
|
||||
defaultModel: config.default_model?.model,
|
||||
defaultProvider: config.default_model?.provider,
|
||||
theme: 'dark',
|
||||
showToolCalls: true,
|
||||
autoSaveContext: true,
|
||||
fileWatching: true,
|
||||
privacyOptIn: false,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Return structured fallback if API not available (404)
|
||||
if (isNotFoundError(error)) {
|
||||
@@ -1096,7 +1117,16 @@ export class GatewayClient {
|
||||
}
|
||||
}
|
||||
async saveQuickConfig(config: Record<string, any>): Promise<any> {
|
||||
return this.restPut('/api/config/quick', config);
|
||||
// Use /api/config endpoint for saving config
|
||||
// Map frontend config back to OpenFang format
|
||||
const openfangConfig = {
|
||||
data_dir: config.workspaceDir,
|
||||
default_model: config.defaultModel ? {
|
||||
model: config.defaultModel,
|
||||
provider: config.defaultProvider || 'bailian',
|
||||
} : undefined,
|
||||
};
|
||||
return this.restPut('/api/config', openfangConfig);
|
||||
}
|
||||
async listSkills(): Promise<any> {
|
||||
return this.restGet('/api/skills');
|
||||
|
||||
@@ -219,6 +219,26 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
} catch {
|
||||
/* ignore local gateway preparation failures during connection bootstrap */
|
||||
}
|
||||
|
||||
// Auto-start local gateway if not running
|
||||
try {
|
||||
const localStatus = await fetchLocalGatewayStatus();
|
||||
const isRunning = localStatus.portStatus === 'busy' || localStatus.listenerPids.length > 0;
|
||||
|
||||
if (!isRunning && localStatus.cliAvailable) {
|
||||
console.log('[ConnectionStore] Local gateway not running, auto-starting...');
|
||||
set({ localGatewayBusy: true });
|
||||
await startLocalGatewayCommand();
|
||||
set({ localGatewayBusy: false });
|
||||
|
||||
// Wait for gateway to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
console.log('[ConnectionStore] Local gateway started');
|
||||
}
|
||||
} catch (startError) {
|
||||
console.warn('[ConnectionStore] Failed to auto-start local gateway:', startError);
|
||||
set({ localGatewayBusy: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve effective token: param > quickConfig > localStorage > local auth
|
||||
|
||||
@@ -659,12 +659,34 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
|
||||
try {
|
||||
set({ error: null });
|
||||
|
||||
// Prepare local gateway for Tauri
|
||||
if (isTauriRuntime()) {
|
||||
try {
|
||||
await prepareLocalGatewayForTauri();
|
||||
} catch {
|
||||
/* ignore local gateway preparation failures during connection bootstrap */
|
||||
}
|
||||
|
||||
// Auto-start local gateway if not running
|
||||
try {
|
||||
const localStatus = await getLocalGatewayStatus();
|
||||
const isRunning = localStatus.portStatus === 'busy' || localStatus.listenerPids.length > 0;
|
||||
|
||||
if (!isRunning && localStatus.cliAvailable) {
|
||||
console.log('[GatewayStore] Local gateway not running, auto-starting...');
|
||||
set({ localGatewayBusy: true });
|
||||
await startLocalGatewayCommand();
|
||||
set({ localGatewayBusy: false });
|
||||
|
||||
// Wait for gateway to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
console.log('[GatewayStore] Local gateway started');
|
||||
}
|
||||
} catch (startError) {
|
||||
console.warn('[GatewayStore] Failed to auto-start local gateway:', startError);
|
||||
set({ localGatewayBusy: false });
|
||||
}
|
||||
}
|
||||
// Use the first non-empty token from: param > quickConfig > localStorage
|
||||
let effectiveToken = token || get().quickConfig.gatewayToken || getStoredGatewayToken();
|
||||
|
||||
Reference in New Issue
Block a user