吸收数字员工风险策略本地管理
This commit is contained in:
@@ -18,6 +18,7 @@ import type {
|
||||
PlanStepDispatchPolicy,
|
||||
PlanStepDispatchPolicyPullResult,
|
||||
ReadyStepDispatchResult,
|
||||
RiskApprovalPolicy,
|
||||
NotificationPreferences,
|
||||
NotificationUpdatesPullResult,
|
||||
UserNotification,
|
||||
@@ -568,7 +569,7 @@ export function getEventLog(params: {
|
||||
// Notifications
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type NotificationUpdatesPullResult, type PlanStepDispatchPolicy, type UserNotification };
|
||||
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type NotificationUpdatesPullResult, type PlanStepDispatchPolicy, type RiskApprovalPolicy, type UserNotification };
|
||||
|
||||
export function getNotifications(
|
||||
status: string = 'unread',
|
||||
@@ -663,6 +664,17 @@ export function pullRiskApprovalPolicy(): Promise<{ ok: boolean; skipped?: boole
|
||||
});
|
||||
}
|
||||
|
||||
export function getRiskApprovalPolicy(): Promise<RiskApprovalPolicy> {
|
||||
return apiFetch<RiskApprovalPolicy>('/api/risk-approval/policy');
|
||||
}
|
||||
|
||||
export function patchRiskApprovalPolicy(patch: Partial<RiskApprovalPolicy>): Promise<RiskApprovalPolicy> {
|
||||
return apiFetch<RiskApprovalPolicy>('/api/risk-approval/policy', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
|
||||
export function pullRouteDecisionPolicy(): Promise<RouteDecisionPolicyPullResult> {
|
||||
return apiFetch<RouteDecisionPolicyPullResult>('/api/route-decision/policy/pull', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
PlanStepDispatchPolicy,
|
||||
PlanStepDispatchPolicyPullResult,
|
||||
PlanStepSummary,
|
||||
RiskApprovalPolicy,
|
||||
RouteDecisionPolicy,
|
||||
RouteDecisionPolicyPullResult,
|
||||
RouteDecisionRecord,
|
||||
@@ -113,6 +114,7 @@ interface AdapterState {
|
||||
notificationPreferences?: NotificationPreferences;
|
||||
approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences;
|
||||
planStepDispatchPolicy?: PlanStepDispatchPolicy;
|
||||
riskApprovalPolicy?: RiskApprovalPolicy;
|
||||
routeDecisionPolicy?: RouteDecisionPolicy;
|
||||
consoleRole?: string;
|
||||
rolePreferences?: Record<string, unknown>;
|
||||
@@ -909,6 +911,15 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
if (method === 'GET' && pathname === '/api/plan-step/dispatch-policy') {
|
||||
return planStepDispatchPolicy(notificationAdapterState(await readQimingclawSnapshot())) as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/risk-approval/policy') {
|
||||
return riskApprovalPolicy(notificationAdapterState(await readQimingclawSnapshot())) as T;
|
||||
}
|
||||
if (method === 'PATCH' && pathname === '/api/risk-approval/policy') {
|
||||
return await handleRiskApprovalPolicyPatch(
|
||||
parseJsonBody<Partial<RiskApprovalPolicy>>(options.body),
|
||||
await readQimingclawSnapshot(),
|
||||
) as T;
|
||||
}
|
||||
if (method === 'POST' && pathname === '/api/plan-step/dispatch-policy/pull') {
|
||||
return await handlePlanStepDispatchPolicyPull(await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
@@ -1140,6 +1151,7 @@ function defaultState(): AdapterState {
|
||||
notificationPreferences: defaultNotificationPreferences(),
|
||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||
planStepDispatchPolicy: defaultPlanStepDispatchPolicy(),
|
||||
riskApprovalPolicy: defaultRiskApprovalPolicy(),
|
||||
routeDecisionPolicy: defaultRouteDecisionPolicy(),
|
||||
consoleRole: 'sales',
|
||||
rolePreferences: {},
|
||||
@@ -1168,6 +1180,7 @@ function normalizeAdapterState(value: Partial<AdapterState>): AdapterState {
|
||||
notificationPreferences: notificationPreferences(value),
|
||||
approvalSubscriptionPreferences: approvalSubscriptionPreferences(value),
|
||||
planStepDispatchPolicy: planStepDispatchPolicy(value),
|
||||
riskApprovalPolicy: riskApprovalPolicy(value),
|
||||
routeDecisionPolicy: routeDecisionPolicy(value),
|
||||
};
|
||||
}
|
||||
@@ -4384,6 +4397,20 @@ function defaultPlanStepDispatchPolicy(): PlanStepDispatchPolicy {
|
||||
};
|
||||
}
|
||||
|
||||
function defaultRiskApprovalPolicy(): RiskApprovalPolicy {
|
||||
return {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function defaultRouteDecisionPolicy(): RouteDecisionPolicy {
|
||||
return {
|
||||
enabled: true,
|
||||
@@ -4435,6 +4462,25 @@ function planStepDispatchPolicy(state: Partial<AdapterState> | null | undefined)
|
||||
};
|
||||
}
|
||||
|
||||
function riskApprovalPolicy(state: Partial<AdapterState> | null | undefined): RiskApprovalPolicy {
|
||||
const stored = state?.riskApprovalPolicy;
|
||||
const fallback = defaultRiskApprovalPolicy();
|
||||
if (!stored || typeof stored !== 'object') return fallback;
|
||||
const riskLevels = uniqueStrings(arrayValue(stored.approval_required_risk_levels).map((value) => stringValue(value).toLowerCase()))
|
||||
.filter((value) => ['normal', 'medium', 'high', 'critical'].includes(value));
|
||||
return {
|
||||
enabled: stored.enabled !== false,
|
||||
approval_required_risk_levels: riskLevels.length > 0 ? riskLevels : fallback.approval_required_risk_levels,
|
||||
approval_required_actions: uniqueStrings(arrayValue(stored.approval_required_actions)),
|
||||
auto_reject_actions: uniqueStrings(arrayValue(stored.auto_reject_actions)),
|
||||
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
|
||||
source: stored.source === 'remote' ? 'remote' : 'local',
|
||||
remote_updated_at: typeof stored.remote_updated_at === 'string' ? stored.remote_updated_at : null,
|
||||
last_pulled_at: typeof stored.last_pulled_at === 'string' ? stored.last_pulled_at : null,
|
||||
last_pull_error: typeof stored.last_pull_error === 'string' ? stored.last_pull_error : null,
|
||||
};
|
||||
}
|
||||
|
||||
function routeDecisionPolicy(state: Partial<AdapterState> | null | undefined): RouteDecisionPolicy {
|
||||
const stored = state?.routeDecisionPolicy;
|
||||
const fallback = defaultRouteDecisionPolicy();
|
||||
@@ -4561,6 +4607,33 @@ async function handlePlanStepDispatchPolicyPatch(
|
||||
return next;
|
||||
}
|
||||
|
||||
async function handleRiskApprovalPolicyPatch(
|
||||
patch: Partial<RiskApprovalPolicy>,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<RiskApprovalPolicy> {
|
||||
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
|
||||
const current = riskApprovalPolicy(state);
|
||||
const next = riskApprovalPolicy({
|
||||
riskApprovalPolicy: {
|
||||
...current,
|
||||
...patch,
|
||||
source: 'local',
|
||||
updated_at: new Date().toISOString(),
|
||||
last_pull_error: null,
|
||||
},
|
||||
});
|
||||
await saveState(
|
||||
{
|
||||
...normalizeAdapterState(state),
|
||||
riskApprovalPolicy: next,
|
||||
},
|
||||
'update_risk_approval_policy',
|
||||
undefined,
|
||||
{ risk_approval_policy: next },
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
async function handlePlanStepDispatchPolicyPull(
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<PlanStepDispatchPolicyPullResult> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -226,6 +226,18 @@ export interface PlanStepDispatchPolicyPullResult {
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface RiskApprovalPolicy {
|
||||
enabled: boolean;
|
||||
approval_required_risk_levels: string[];
|
||||
approval_required_actions: string[];
|
||||
auto_reject_actions: string[];
|
||||
updated_at: string | null;
|
||||
source?: 'local' | 'remote';
|
||||
remote_updated_at?: string | null;
|
||||
last_pulled_at?: string | null;
|
||||
last_pull_error?: string | null;
|
||||
}
|
||||
|
||||
export interface PlanStepDependencyDetail {
|
||||
step_id: string;
|
||||
task_id: string | null;
|
||||
|
||||
Reference in New Issue
Block a user