根因: autonomy-manager.ts:268 将 hand_trigger 硬编码为 null, 导致任何自主权级别都无法自动触发 Hand。 新增 handAutoTrigger 字段,autonomous 级别默认 true。 UI 增加对应开关。
505 lines
16 KiB
TypeScript
505 lines
16 KiB
TypeScript
/**
|
||
* AutonomyConfig - Configuration UI for L4 self-evolution authorization
|
||
*
|
||
* Allows users to configure:
|
||
* - Autonomy level (supervised/assisted/autonomous)
|
||
* - Individual action permissions
|
||
* - Approval thresholds
|
||
* - Audit log viewing
|
||
*
|
||
* Part of ZCLAW L4 Self-Evolution capability.
|
||
*/
|
||
|
||
import { useState, useCallback, useEffect } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import {
|
||
Shield,
|
||
ShieldAlert,
|
||
ShieldCheck,
|
||
ShieldQuestion,
|
||
Settings,
|
||
AlertTriangle,
|
||
CheckCircle,
|
||
Clock,
|
||
RotateCcw,
|
||
Info,
|
||
ChevronDown,
|
||
ChevronRight,
|
||
Trash2,
|
||
} from 'lucide-react';
|
||
import {
|
||
getAutonomyManager,
|
||
DEFAULT_AUTONOMY_CONFIGS,
|
||
type AutonomyConfig,
|
||
type AutonomyLevel,
|
||
type AuditLogEntry,
|
||
type ActionType,
|
||
} from '../lib/autonomy-manager';
|
||
|
||
// === Types ===
|
||
|
||
interface AutonomyConfigProps {
|
||
className?: string;
|
||
onConfigChange?: (config: AutonomyConfig) => void;
|
||
}
|
||
|
||
// === Autonomy Level Config ===
|
||
|
||
const LEVEL_CONFIG: Record<AutonomyLevel, {
|
||
label: string;
|
||
description: string;
|
||
icon: typeof Shield;
|
||
color: string;
|
||
}> = {
|
||
supervised: {
|
||
label: '监督模式',
|
||
description: '所有操作都需要用户确认',
|
||
icon: ShieldQuestion,
|
||
color: 'text-yellow-500',
|
||
},
|
||
assisted: {
|
||
label: '辅助模式',
|
||
description: '低风险操作自动执行,高风险需确认',
|
||
icon: ShieldAlert,
|
||
color: 'text-blue-500',
|
||
},
|
||
autonomous: {
|
||
label: '自主模式',
|
||
description: 'Agent 自主决策,仅高影响操作通知',
|
||
icon: ShieldCheck,
|
||
color: 'text-green-500',
|
||
},
|
||
};
|
||
|
||
const ACTION_LABELS: Record<ActionType, string> = {
|
||
memory_save: '自动保存记忆',
|
||
memory_delete: '删除记忆',
|
||
identity_update: '更新身份文件',
|
||
identity_rollback: '回滚身份',
|
||
skill_install: '安装技能',
|
||
skill_uninstall: '卸载技能',
|
||
config_change: '修改配置',
|
||
workflow_trigger: '触发工作流',
|
||
hand_trigger: '触发 Hand',
|
||
llm_call: '调用 LLM',
|
||
reflection_run: '运行反思',
|
||
compaction_run: '运行压缩',
|
||
};
|
||
|
||
// === Components ===
|
||
|
||
function LevelSelector({
|
||
value,
|
||
onChange,
|
||
}: {
|
||
value: AutonomyLevel;
|
||
onChange: (level: AutonomyLevel) => void;
|
||
}) {
|
||
return (
|
||
<div className="space-y-2">
|
||
{(Object.keys(LEVEL_CONFIG) as AutonomyLevel[]).map((level) => {
|
||
const config = LEVEL_CONFIG[level];
|
||
const Icon = config.icon;
|
||
const isSelected = value === level;
|
||
|
||
return (
|
||
<button
|
||
key={level}
|
||
onClick={() => onChange(level)}
|
||
className={`w-full flex items-start gap-3 p-3 rounded-lg border transition-all text-left ${
|
||
isSelected
|
||
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/30'
|
||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||
}`}
|
||
>
|
||
<Icon className={`w-5 h-5 mt-0.5 flex-shrink-0 ${config.color}`} />
|
||
<div className="flex-1 min-w-0">
|
||
<div className={`text-sm font-medium ${
|
||
isSelected ? 'text-purple-700 dark:text-purple-400' : 'text-gray-700 dark:text-gray-300'
|
||
}`}>
|
||
{config.label}
|
||
</div>
|
||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||
{config.description}
|
||
</div>
|
||
</div>
|
||
{isSelected && (
|
||
<CheckCircle className="w-4 h-4 text-purple-500 flex-shrink-0" />
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ActionToggle({
|
||
label,
|
||
enabled,
|
||
onChange,
|
||
disabled,
|
||
}: {
|
||
label: string;
|
||
enabled: boolean;
|
||
onChange: (enabled: boolean) => void;
|
||
disabled?: boolean;
|
||
}) {
|
||
return (
|
||
<div className={`flex items-center justify-between py-2 ${disabled ? 'opacity-50' : ''}`}>
|
||
<span className="text-sm text-gray-700 dark:text-gray-300">{label}</span>
|
||
<button
|
||
onClick={() => !disabled && onChange(!enabled)}
|
||
disabled={disabled}
|
||
className={`relative w-9 h-5 rounded-full transition-colors ${
|
||
enabled ? 'bg-green-500' : 'bg-gray-300 dark:bg-gray-600'
|
||
} ${disabled ? 'cursor-not-allowed' : ''}`}
|
||
>
|
||
<motion.div
|
||
animate={{ x: enabled ? 18 : 0 }}
|
||
className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow"
|
||
/>
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function AuditLogEntryItem({
|
||
entry,
|
||
onRollback,
|
||
}: {
|
||
entry: AuditLogEntry;
|
||
onRollback?: (id: string) => void;
|
||
}) {
|
||
const [expanded, setExpanded] = useState(false);
|
||
|
||
const outcomeColors = {
|
||
success: 'text-green-500',
|
||
failed: 'text-red-500',
|
||
rolled_back: 'text-yellow-500',
|
||
};
|
||
|
||
const outcomeLabels = {
|
||
success: '成功',
|
||
failed: '失败',
|
||
rolled_back: '已回滚',
|
||
};
|
||
|
||
const time = new Date(entry.timestamp).toLocaleString('zh-CN', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
});
|
||
|
||
return (
|
||
<div className="border-b border-gray-100 dark:border-gray-800 last:border-b-0">
|
||
<button
|
||
onClick={() => setExpanded(!expanded)}
|
||
className="w-full flex items-center gap-2 py-2 px-1 hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors"
|
||
>
|
||
{expanded ? (
|
||
<ChevronDown className="w-3 h-3 text-gray-400" />
|
||
) : (
|
||
<ChevronRight className="w-3 h-3 text-gray-400" />
|
||
)}
|
||
<span className="text-xs text-gray-400">{time}</span>
|
||
<span className="text-sm text-gray-700 dark:text-gray-300 flex-1 text-left truncate">
|
||
{ACTION_LABELS[entry.action] || entry.action}
|
||
</span>
|
||
<span className={`text-xs ${outcomeColors[entry.outcome]}`}>
|
||
{outcomeLabels[entry.outcome]}
|
||
</span>
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{expanded && (
|
||
<motion.div
|
||
initial={{ height: 0, opacity: 0 }}
|
||
animate={{ height: 'auto', opacity: 1 }}
|
||
exit={{ height: 0, opacity: 0 }}
|
||
className="px-6 pb-2 space-y-1"
|
||
>
|
||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||
风险: {entry.decision.riskLevel} · 重要性: {entry.decision.importance}
|
||
</div>
|
||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||
原因: {entry.decision.reason}
|
||
</div>
|
||
{entry.outcome !== 'rolled_back' && entry.decision.riskLevel !== 'low' && (
|
||
<button
|
||
onClick={() => onRollback?.(entry.id)}
|
||
className="flex items-center gap-1 text-xs text-yellow-600 dark:text-yellow-400 hover:underline mt-1"
|
||
>
|
||
<RotateCcw className="w-3 h-3" />
|
||
回滚此操作
|
||
</button>
|
||
)}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// === Main Component ===
|
||
|
||
export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfigProps) {
|
||
const [manager] = useState(() => getAutonomyManager());
|
||
const [config, setConfig] = useState<AutonomyConfig>(manager.getConfig());
|
||
const [auditLog, setAuditLog] = useState<AuditLogEntry[]>([]);
|
||
const [hasChanges, setHasChanges] = useState(false);
|
||
|
||
// Load audit log
|
||
useEffect(() => {
|
||
setAuditLog(manager.getAuditLog(50));
|
||
}, [manager]);
|
||
|
||
const updateConfig = useCallback(
|
||
(updates: Partial<AutonomyConfig>) => {
|
||
setConfig((prev) => {
|
||
const next = { ...prev, ...updates };
|
||
setHasChanges(true);
|
||
onConfigChange?.(next);
|
||
return next;
|
||
});
|
||
},
|
||
[onConfigChange]
|
||
);
|
||
|
||
const handleLevelChange = useCallback((level: AutonomyLevel) => {
|
||
const newConfig = DEFAULT_AUTONOMY_CONFIGS[level];
|
||
setConfig(newConfig);
|
||
setHasChanges(true);
|
||
onConfigChange?.(newConfig);
|
||
}, [onConfigChange]);
|
||
|
||
const handleSave = useCallback(() => {
|
||
manager.updateConfig(config);
|
||
setHasChanges(false);
|
||
}, [manager, config]);
|
||
|
||
const handleRollback = useCallback((auditId: string) => {
|
||
if (manager.rollback(auditId)) {
|
||
setAuditLog(manager.getAuditLog(50));
|
||
}
|
||
}, [manager]);
|
||
|
||
const handleClearLog = useCallback(() => {
|
||
manager.clearAuditLog();
|
||
setAuditLog([]);
|
||
}, [manager]);
|
||
|
||
return (
|
||
<div className={`flex flex-col h-full ${className}`}>
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||
<div className="flex items-center gap-2">
|
||
<Shield className="w-5 h-5 text-purple-500" />
|
||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">自主授权</h2>
|
||
</div>
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={!hasChanges}
|
||
className="px-3 py-1.5 text-sm bg-purple-500 hover:bg-purple-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
|
||
>
|
||
保存
|
||
</button>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||
{/* Autonomy Level */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<ShieldAlert className="w-4 h-4 text-gray-500" />
|
||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
自主级别
|
||
</span>
|
||
</div>
|
||
<LevelSelector value={config.level} onChange={handleLevelChange} />
|
||
</div>
|
||
|
||
{/* Allowed Actions */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<Settings className="w-4 h-4 text-gray-500" />
|
||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
允许的操作
|
||
</span>
|
||
</div>
|
||
<div className="pl-6 space-y-1 border-l-2 border-gray-200 dark:border-gray-700">
|
||
<ActionToggle
|
||
label="自动保存记忆"
|
||
enabled={config.allowedActions.memoryAutoSave}
|
||
onChange={(enabled) =>
|
||
updateConfig({
|
||
allowedActions: { ...config.allowedActions, memoryAutoSave: enabled },
|
||
})
|
||
}
|
||
/>
|
||
<ActionToggle
|
||
label="自动更新身份文件"
|
||
enabled={config.allowedActions.identityAutoUpdate}
|
||
onChange={(enabled) =>
|
||
updateConfig({
|
||
allowedActions: { ...config.allowedActions, identityAutoUpdate: enabled },
|
||
})
|
||
}
|
||
/>
|
||
<ActionToggle
|
||
label="自动安装技能"
|
||
enabled={config.allowedActions.skillAutoInstall}
|
||
onChange={(enabled) =>
|
||
updateConfig({
|
||
allowedActions: { ...config.allowedActions, skillAutoInstall: enabled },
|
||
})
|
||
}
|
||
/>
|
||
<ActionToggle
|
||
label="自我修改行为"
|
||
enabled={config.allowedActions.selfModification}
|
||
onChange={(enabled) =>
|
||
updateConfig({
|
||
allowedActions: { ...config.allowedActions, selfModification: enabled },
|
||
})
|
||
}
|
||
/>
|
||
<ActionToggle
|
||
label="自动上下文压缩"
|
||
enabled={config.allowedActions.autoCompaction}
|
||
onChange={(enabled) =>
|
||
updateConfig({
|
||
allowedActions: { ...config.allowedActions, autoCompaction: enabled },
|
||
})
|
||
}
|
||
/>
|
||
<ActionToggle
|
||
label="自动反思"
|
||
enabled={config.allowedActions.autoReflection}
|
||
onChange={(enabled) =>
|
||
updateConfig({
|
||
allowedActions: { ...config.allowedActions, autoReflection: enabled },
|
||
})
|
||
}
|
||
/>
|
||
<ActionToggle
|
||
label="自动触发 Hand 能力"
|
||
enabled={config.allowedActions.handAutoTrigger}
|
||
onChange={(enabled) =>
|
||
updateConfig({
|
||
allowedActions: { ...config.allowedActions, handAutoTrigger: enabled },
|
||
})
|
||
}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Approval Thresholds */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<AlertTriangle className="w-4 h-4 text-gray-500" />
|
||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
审批阈值
|
||
</span>
|
||
</div>
|
||
<div className="pl-6 space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||
重要性上限
|
||
</span>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="10"
|
||
value={config.approvalThreshold.importanceMax}
|
||
onChange={(e) =>
|
||
updateConfig({
|
||
approvalThreshold: {
|
||
...config.approvalThreshold,
|
||
importanceMax: parseInt(e.target.value),
|
||
},
|
||
})
|
||
}
|
||
className="w-24 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500"
|
||
/>
|
||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 w-6 text-right">
|
||
{config.approvalThreshold.importanceMax}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||
风险等级上限
|
||
</span>
|
||
<select
|
||
value={config.approvalThreshold.riskMax}
|
||
onChange={(e) =>
|
||
updateConfig({
|
||
approvalThreshold: {
|
||
...config.approvalThreshold,
|
||
riskMax: e.target.value as 'low' | 'medium' | 'high',
|
||
},
|
||
})
|
||
}
|
||
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||
>
|
||
<option value="low">低风险</option>
|
||
<option value="medium">中风险</option>
|
||
<option value="high">高风险</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Audit Log */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="w-4 h-4 text-gray-500" />
|
||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
审计日志
|
||
</span>
|
||
<span className="text-xs text-gray-400">({auditLog.length} 条)</span>
|
||
</div>
|
||
<button
|
||
onClick={handleClearLog}
|
||
className="p-1 text-gray-400 hover:text-red-500 transition-colors"
|
||
title="清除日志"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
{auditLog.length > 0 ? (
|
||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden max-h-64 overflow-y-auto">
|
||
{auditLog
|
||
.slice()
|
||
.reverse()
|
||
.map((entry) => (
|
||
<AuditLogEntryItem
|
||
key={entry.id}
|
||
entry={entry}
|
||
onRollback={handleRollback}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-8 text-gray-400 dark:text-gray-500 text-sm">
|
||
暂无审计日志
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Info */}
|
||
<div className="flex items-start gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg text-xs text-yellow-600 dark:text-yellow-400">
|
||
<Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||
<p>
|
||
高风险操作(删除记忆、修改身份文件)始终需要用户确认,无论自主级别如何设置。
|
||
所有自主操作都会记录在审计日志中,支持一键回滚。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default AutonomyConfig;
|