吸收数字员工风险策略本地管理

This commit is contained in:
baiyanyun
2026-06-11 21:14:29 +08:00
parent 245a210b25
commit 84d34236e8
10 changed files with 394 additions and 189 deletions

View File

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