diff --git a/desktop/src/components/Settings/FeatureGates.tsx b/desktop/src/components/Settings/FeatureGates.tsx new file mode 100644 index 0000000..7b0b7e4 --- /dev/null +++ b/desktop/src/components/Settings/FeatureGates.tsx @@ -0,0 +1,163 @@ +/** + * Feature Gates — 实验性功能开关 + * + * 控制前端实验性功能的可见性。当前为前端运行时开关, + * 持久化到 localStorage。未来可对接 Kernel config。 + * + * 可用开关: + * - multiAgent: 多 Agent 协作模式 (Director + A2A) + * - wasmSkills: WASM 技能沙箱执行 + * - verboseToolOutput: 显示完整工具输出 (调试用) + */ + +import { useState, useCallback } from 'react'; +import { createLogger } from '../../lib/logger'; + +const log = createLogger('FeatureGates'); + +// === Feature Flag Definitions === + +interface FeatureDef { + key: string; + label: string; + description: string; + defaultEnabled: boolean; + /** Whether this feature has Rust backend support compiled in */ + backendNote?: string; +} + +const FEATURES: FeatureDef[] = [ + { + key: 'multiAgent', + label: '多 Agent 协作', + description: '启用 Director 编排 + A2A 协议,支持多 Agent 协作完成复杂任务', + defaultEnabled: false, + backendNote: '需要编译时启用 multi-agent feature', + }, + { + key: 'wasmSkills', + label: 'WASM 技能沙箱', + description: '启用 WebAssembly 技能在沙箱中执行,支持用户自定义技能', + defaultEnabled: false, + backendNote: '需要编译时启用 wasm feature', + }, + { + key: 'verboseToolOutput', + label: '详细工具输出', + description: '在聊天中显示完整的工具调用结果(默认已截断)', + defaultEnabled: false, + }, +]; + +// === Helpers === + +const STORAGE_PREFIX = 'zclaw-feature-'; + +function loadFeatureState(key: string, defaultEnabled: boolean): boolean { + try { + const stored = localStorage.getItem(`${STORAGE_PREFIX}${key}`); + if (stored !== null) return stored === 'true'; + } catch { /* ignore */ } + return defaultEnabled; +} + +function saveFeatureState(key: string, enabled: boolean): void { + try { + localStorage.setItem(`${STORAGE_PREFIX}${key}`, String(enabled)); + } catch (e) { + log.warn('Failed to persist feature flag', { key, error: e }); + } +} + +// === Public API === + +/** Check if a feature gate is enabled (can be called outside React) */ +export function isFeatureEnabled(key: string): boolean { + const def = FEATURES.find(f => f.key === key); + return loadFeatureState(key, def?.defaultEnabled ?? false); +} + +// === Component === + +export function FeatureGates() { + const [states, setStates] = useState>(() => { + const initial: Record = {}; + for (const f of FEATURES) { + initial[f.key] = loadFeatureState(f.key, f.defaultEnabled); + } + return initial; + }); + + const toggle = useCallback((key: string) => { + setStates(prev => { + const next = { ...prev, [key]: !prev[key] }; + saveFeatureState(key, next[key]); + log.info(`Feature gate '${key}' set to ${next[key]}`); + return next; + }); + }, []); + + return ( +
+
+

实验性功能

+

+ 这些功能处于早期开发阶段,可能不稳定。启用后请反馈使用体验。 +

+
+ + {/* Warning banner */} +
+ ⚠️ +
+

实验性功能警告

+

启用实验性功能可能影响系统稳定性。建议在非生产环境中测试。

+
+
+ + {/* Feature toggles */} +
+ {FEATURES.map(feature => ( +
+
+
+

{feature.label}

+ {states[feature.key] && ( + + 已启用 + + )} +
+

{feature.description}

+ {feature.backendNote && ( +

{feature.backendNote}

+ )} +
+ +
+ ))} +
+
+ ); +} diff --git a/desktop/src/components/Settings/SettingsLayout.tsx b/desktop/src/components/Settings/SettingsLayout.tsx index 25dfed1..5cb8441 100644 --- a/desktop/src/components/Settings/SettingsLayout.tsx +++ b/desktop/src/components/Settings/SettingsLayout.tsx @@ -19,6 +19,7 @@ import { Activity, Cloud, CreditCard, + FlaskConical, } from 'lucide-react'; import { silentErrorHandler } from '../../lib/error-utils'; import { General } from './General'; @@ -39,6 +40,7 @@ import { SecureStorage } from './SecureStorage'; import { VikingPanel } from '../VikingPanel'; import { SaaSSettings } from '../SaaS/SaaSSettings'; import { PricingPage } from '../SaaS/PricingPage'; +import { FeatureGates } from './FeatureGates'; import { ErrorBoundary } from '../ui/ErrorBoundary'; interface SettingsLayoutProps { @@ -62,6 +64,7 @@ type SettingsPage = | 'tasks' | 'heartbeat' | 'health' + | 'labs' | 'feedback' | 'about'; @@ -85,6 +88,7 @@ const menuItems: { id: SettingsPage; label: string; icon: React.ReactNode; group { id: 'tasks', label: '定时任务', icon: , group: 'advanced' }, { id: 'heartbeat', label: '心跳配置', icon: , group: 'advanced' }, { id: 'health', label: '系统健康', icon: , group: 'advanced' }, + { id: 'labs', label: '实验性功能', icon: , group: 'advanced' }, // --- Footer --- { id: 'feedback', label: '提交反馈', icon: }, { id: 'about', label: '关于', icon: }, @@ -179,6 +183,7 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) { ); + case 'labs': return ; case 'viking': return ( 语义记忆加载失败}