吸收数字员工风险策略本地管理
This commit is contained in:
@@ -3,6 +3,7 @@ import GlassCard from '@/components/digital/GlassCard';
|
||||
import CardTitleBar from '@/components/digital/CardTitleBar';
|
||||
import { useRole, ROLE_PROFILES, type ConsoleRole } from '@/contexts/RoleContext';
|
||||
import { readDigitalEmployeeUiState, saveDigitalEmployeeUiStatePatch } from '@/lib/digitalUiStateBridge';
|
||||
import { getRiskApprovalPolicy, patchRiskApprovalPolicy, type RiskApprovalPolicy } from '@/lib/api';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
const ROLES: ConsoleRole[] = ['sales', 'engineering', 'hr', 'finance', 'executive'];
|
||||
@@ -32,6 +33,28 @@ const densityLabel: Record<string, string> = {
|
||||
balanced: '均衡',
|
||||
presentation: '汇报',
|
||||
};
|
||||
const RISK_LEVEL_OPTIONS: Array<{ id: string; label: string }> = [
|
||||
{ id: 'medium', label: '中风险' },
|
||||
{ id: 'high', label: '高风险' },
|
||||
{ id: 'critical', label: '严重风险' },
|
||||
];
|
||||
const RISK_ACTION_OPTIONS: Array<{ id: string; label: string }> = [
|
||||
{ id: 'governance.run_schedule_now', label: '立即运行调度' },
|
||||
{ id: 'plan_step.dispatch_ready_steps', label: '派发 Ready 步骤' },
|
||||
{ id: 'managed_service.restart.fileServer', label: '重启文件服务' },
|
||||
{ id: 'artifact.access.open_location', label: '打开产物位置' },
|
||||
];
|
||||
const DEFAULT_RISK_POLICY: RiskApprovalPolicy = {
|
||||
enabled: true,
|
||||
approval_required_risk_levels: ['high', 'critical'],
|
||||
approval_required_actions: [],
|
||||
auto_reject_actions: [],
|
||||
updated_at: null,
|
||||
source: 'local',
|
||||
remote_updated_at: null,
|
||||
last_pulled_at: null,
|
||||
last_pull_error: null,
|
||||
};
|
||||
|
||||
const fieldGridStyle: CSSProperties = {
|
||||
display: 'grid',
|
||||
@@ -76,6 +99,7 @@ function normalizePreferences(value: unknown): RolePreferences {
|
||||
export default function OpsSettings() {
|
||||
const { role, profile, setRole } = useRole();
|
||||
const [preferences, setPreferences] = useState<RolePreferences>(loadPreferences);
|
||||
const [riskPolicy, setRiskPolicy] = useState<RiskApprovalPolicy>(DEFAULT_RISK_POLICY);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -97,6 +121,14 @@ export default function OpsSettings() {
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void getRiskApprovalPolicy()
|
||||
.then((policy) => { if (!cancelled) setRiskPolicy(policy); })
|
||||
.catch(() => { if (!cancelled) setRiskPolicy(DEFAULT_RISK_POLICY); });
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const markSaved = () => {
|
||||
setSaved(true);
|
||||
if (savedTimerRef.current) clearTimeout(savedTimerRef.current);
|
||||
@@ -125,6 +157,29 @@ export default function OpsSettings() {
|
||||
markSaved();
|
||||
};
|
||||
|
||||
const updateRiskPolicy = (patch: Partial<RiskApprovalPolicy>) => {
|
||||
const next = { ...riskPolicy, ...patch };
|
||||
setRiskPolicy(next);
|
||||
void patchRiskApprovalPolicy(patch)
|
||||
.then((policy) => setRiskPolicy(policy))
|
||||
.catch(() => setRiskPolicy(riskPolicy));
|
||||
markSaved();
|
||||
};
|
||||
|
||||
const toggleRiskLevel = (level: string) => {
|
||||
const levels = new Set(riskPolicy.approval_required_risk_levels);
|
||||
if (levels.has(level)) levels.delete(level);
|
||||
else levels.add(level);
|
||||
updateRiskPolicy({ approval_required_risk_levels: Array.from(levels) });
|
||||
};
|
||||
|
||||
const toggleRiskAction = (field: 'approval_required_actions' | 'auto_reject_actions', action: string) => {
|
||||
const values = new Set(riskPolicy[field]);
|
||||
if (values.has(action)) values.delete(action);
|
||||
else values.add(action);
|
||||
updateRiskPolicy({ [field]: Array.from(values) } as Pick<RiskApprovalPolicy, typeof field>);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '14px 18px' }}>
|
||||
<GlassCard>
|
||||
@@ -187,6 +242,47 @@ export default function OpsSettings() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="de-settings-section">
|
||||
<h3 className="de-settings-heading">风险审批策略</h3>
|
||||
<p className="de-settings-desc">调整客户端高风险动作在执行前是否需要人工审批,或直接拒绝。</p>
|
||||
<div style={fieldGridStyle}>
|
||||
<label style={fieldStyle}>
|
||||
<span className="de-role-label">启用策略</span>
|
||||
<select style={inputStyle} value={riskPolicy.enabled ? 'enabled' : 'disabled'} onChange={(event) => updateRiskPolicy({ enabled: event.target.value === 'enabled' })}>
|
||||
<option value="enabled">启用</option>
|
||||
<option value="disabled">关闭</option>
|
||||
</select>
|
||||
</label>
|
||||
<div style={fieldStyle}>
|
||||
<span className="de-role-label">按风险等级审批</span>
|
||||
{RISK_LEVEL_OPTIONS.map((option) => (
|
||||
<label key={option.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input type="checkbox" checked={riskPolicy.approval_required_risk_levels.includes(option.id)} onChange={() => toggleRiskLevel(option.id)} />
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div style={fieldStyle}>
|
||||
<span className="de-role-label">指定动作需审批</span>
|
||||
{RISK_ACTION_OPTIONS.map((option) => (
|
||||
<label key={option.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input type="checkbox" checked={riskPolicy.approval_required_actions.includes(option.id)} onChange={() => toggleRiskAction('approval_required_actions', option.id)} />
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div style={fieldStyle}>
|
||||
<span className="de-role-label">指定动作直接拒绝</span>
|
||||
{RISK_ACTION_OPTIONS.map((option) => (
|
||||
<label key={option.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input type="checkbox" checked={riskPolicy.auto_reject_actions.includes(option.id)} onChange={() => toggleRiskAction('auto_reject_actions', option.id)} />
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details className="de-settings-section" style={{ marginBottom: 0 }}>
|
||||
<summary className="de-settings-heading" style={{ cursor: 'pointer' }}>高级技术设置</summary>
|
||||
<p className="de-settings-desc">默认隐藏,仅供技术人员排查使用。当前角色刷新阈值:{Math.round(profile.staleDataThresholdMs / 1000)} 秒。</p>
|
||||
|
||||
Reference in New Issue
Block a user