feat(ui): Feature Gates 设置页 — 实验性功能开关 (Phase 3B)
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
新增 Settings > 实验性功能 页面: - 3 个开关: 多 Agent 协作 / WASM 技能沙箱 / 详细工具输出 - localStorage 持久化 + isFeatureEnabled() 公共 API - 实验性功能警告横幅 - 当前为前端运行时开关,未来可对接 Kernel config
This commit is contained in:
163
desktop/src/components/Settings/FeatureGates.tsx
Normal file
163
desktop/src/components/Settings/FeatureGates.tsx
Normal file
@@ -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<Record<string, boolean>>(() => {
|
||||
const initial: Record<string, boolean> = {};
|
||||
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 (
|
||||
<div className="max-w-3xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-xl font-bold text-gray-900">实验性功能</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
这些功能处于早期开发阶段,可能不稳定。启用后请反馈使用体验。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Warning banner */}
|
||||
<div className="mb-6 flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<span className="text-amber-600 text-lg">⚠️</span>
|
||||
<div className="text-sm text-amber-800">
|
||||
<p className="font-medium">实验性功能警告</p>
|
||||
<p className="mt-1">启用实验性功能可能影响系统稳定性。建议在非生产环境中测试。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature toggles */}
|
||||
<div className="space-y-4">
|
||||
{FEATURES.map(feature => (
|
||||
<div
|
||||
key={feature.key}
|
||||
className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-gray-900">{feature.label}</h3>
|
||||
{states[feature.key] && (
|
||||
<span className="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
|
||||
已启用
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">{feature.description}</p>
|
||||
{feature.backendNote && (
|
||||
<p className="mt-1 text-xs text-gray-400">{feature.backendNote}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => toggle(feature.key)}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||
${states[feature.key] ? 'bg-blue-600' : 'bg-gray-200'}
|
||||
`}
|
||||
role="switch"
|
||||
aria-checked={states[feature.key]}
|
||||
aria-label={`切换 ${feature.label}`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0
|
||||
transition duration-200 ease-in-out
|
||||
${states[feature.key] ? 'translate-x-5' : 'translate-x-0'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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: <Clock className="w-4 h-4" />, group: 'advanced' },
|
||||
{ id: 'heartbeat', label: '心跳配置', icon: <Heart className="w-4 h-4" />, group: 'advanced' },
|
||||
{ id: 'health', label: '系统健康', icon: <Activity className="w-4 h-4" />, group: 'advanced' },
|
||||
{ id: 'labs', label: '实验性功能', icon: <FlaskConical className="w-4 h-4" />, group: 'advanced' },
|
||||
// --- Footer ---
|
||||
{ id: 'feedback', label: '提交反馈', icon: <HelpCircle className="w-4 h-4" /> },
|
||||
{ id: 'about', label: '关于', icon: <Info className="w-4 h-4" /> },
|
||||
@@ -179,6 +183,7 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) {
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
case 'labs': return <FeatureGates />;
|
||||
case 'viking': return (
|
||||
<ErrorBoundary
|
||||
fallback={<div className="p-6 text-center text-gray-500">语义记忆加载失败</div>}
|
||||
|
||||
Reference in New Issue
Block a user