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:
iven
2026-03-17 14:08:03 +08:00
parent 6c6d21400c
commit 74dbf42644
81 changed files with 5729 additions and 128 deletions

View File

@@ -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">
{/* 左侧边栏 */}

View File

@@ -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');

View File

@@ -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

View File

@@ -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();