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

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

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

View File

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

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>

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
<script type="module" crossorigin src="./assets/index-D9KIP1Kc.js"></script>
<script type="module" crossorigin src="./assets/index-Bp_ixnWX.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-4TVkOCsf.css">
</head>
<body>

View File

@@ -562,6 +562,7 @@ function checkDigitalRiskApprovalPolicy() {
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
const apiPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "api.ts");
const commandDeckPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "CommandDeck.tsx");
const opsSettingsPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "OpsSettings.tsx");
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const ipcHandlersPath = path.join(packageRoot, "src", "main", "ipc", "digitalEmployeeHandlers.ts");
@@ -569,6 +570,7 @@ function checkDigitalRiskApprovalPolicy() {
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
const commandDeck = fs.readFileSync(commandDeckPath, "utf8");
const opsSettings = fs.readFileSync(opsSettingsPath, "utf8");
const stateService = fs.readFileSync(stateServicePath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const ipcHandlers = fs.readFileSync(ipcHandlersPath, "utf8");
@@ -579,6 +581,7 @@ function checkDigitalRiskApprovalPolicy() {
[stateService, "evaluateDigitalEmployeeRiskPolicy", "risk approval policy evaluator"],
[stateService, "risk_approval_required", "risk approval-required event kind"],
[stateService, "risk_policy_rejected", "risk policy rejected event kind"],
[stateService, "update_risk_approval_policy", "risk approval policy update UI event"],
[stateService, "pull_risk_approval_policy", "risk approval policy pull UI event"],
[syncService, "pullDigitalEmployeeRiskApprovalPolicy", "remote risk approval policy pull service"],
[syncService, "riskApprovalPolicyEndpoint", "remote risk approval policy endpoint config"],
@@ -592,13 +595,20 @@ function checkDigitalRiskApprovalPolicy() {
[preload, "evaluateRiskPolicy", "preload risk policy evaluation bridge"],
[adapter, "pullRiskApprovalPolicy", "adapter risk approval policy pull bridge"],
[adapter, "evaluateRiskPolicy", "adapter risk policy evaluation bridge"],
[adapter, "/api/risk-approval/policy", "adapter risk approval policy local endpoint"],
[adapter, "handleRiskApprovalPolicyPatch", "adapter risk approval policy patch handler"],
[adapter, "/api/risk-approval/policy/pull", "adapter risk approval policy pull endpoint"],
[adapter, "riskPolicyBlockedResponse", "adapter risk policy blocked response"],
[adapter, "governanceCommandRiskLevel", "adapter governance risk level mapper"],
[adapter, "managed_service.restart", "managed service restart risk action"],
[adapter, "artifact.access", "artifact access risk action"],
[adapter, "plan_step.dispatch_ready_steps", "ready PlanStep dispatch risk action"],
[api, "getRiskApprovalPolicy", "embedded risk approval policy getter"],
[api, "patchRiskApprovalPolicy", "embedded risk approval policy patcher"],
[api, "pullRiskApprovalPolicy", "embedded risk approval policy pull API"],
[opsSettings, "riskPolicy", "settings risk policy state"],
[opsSettings, "updateRiskPolicy", "settings risk policy update handler"],
[opsSettings, "风险审批策略", "settings risk approval policy panel"],
[commandDeck, "pull_risk_approval_policy", "home risk approval policy pull action"],
[commandDeck, "风险策略", "home risk approval policy pull button"],
];

View File

@@ -1202,6 +1202,8 @@ function digitalEmployeeUiActionMessage(action: string, date?: string): string {
return "数字员工PlanStep派发策略已拉取";
case "pull_risk_approval_policy":
return "数字员工风险审批策略已拉取";
case "update_risk_approval_policy":
return "数字员工风险审批策略已更新";
case "pull_route_decision_policy":
return "数字员工入口路由策略已拉取";
case "approval_updates_pulled":

View File

@@ -588,9 +588,9 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕管理端批量治理和跨设备版本聚合,把可操作交付视图推进到完整生命周期管理。
8. **审批 / 人工介入增强**
- 已吸收approval 记录、workday decision、ACP permission 响应桥已有雏形;用户处理审批时会写入 `approval_decision` runtime event评论、转交、超时、风险标记会写入 `approval_action_recorded` / `approval_action_rejected`,并联动 embedded `/api/approval/:approvalId/timeline`、审批列表摘要、管理端 `/api/digital-employee/approval-history` 历史视图、`/api/approval/subscriptions` 订阅策略和 notification 投影;风险审批策略已支持管理端远端下发、手动拉取、本地策略评估高风险动作前置拦截;多步审批 workflow 已复用 `digital_approvals.payload`,支持步骤推进、转交/超时/风险动作更新当前步骤、管理端审批更新拉取和审批处理回写,审批动作历史、风险策略、多步流程与通知联动已开始闭环。
- 缺口:缺少系统级桌面通知、审批策略管理 UI、跨端审批冲突可视化和正式管理端审批工作台。
- 建议:下一轮先补审批策略管理 UI 或管理端审批工作台,再评估系统级桌面通知的跨端策略
- 已吸收approval 记录、workday decision、ACP permission 响应桥已有雏形;用户处理审批时会写入 `approval_decision` runtime event评论、转交、超时、风险标记会写入 `approval_action_recorded` / `approval_action_rejected`,并联动 embedded `/api/approval/:approvalId/timeline`、审批列表摘要、管理端 `/api/digital-employee/approval-history` 历史视图、`/api/approval/subscriptions` 订阅策略和 notification 投影;风险审批策略已支持管理端远端下发、手动拉取、本地策略评估高风险动作前置拦截和本地设置页管理,`/api/risk-approval/policy` 可读写本地策略并记录 `update_risk_approval_policy`;多步审批 workflow 已复用 `digital_approvals.payload`,支持步骤推进、转交/超时/风险动作更新当前步骤、管理端审批更新拉取和审批处理回写,审批动作历史、风险策略、多步流程与通知联动已开始闭环。
- 缺口:缺少跨端审批冲突可视化和正式管理端审批工作台。
- 建议:下一轮先补管理端审批工作台或跨端冲突可视化,把本地策略管理推进到多端协同治理
9. **通知 / Inbox / 用户消息**
- 已吸收运行事件和同步失败能在数字员工页面展示embedded adapter 已接管 sgRobot 风格 `/api/notifications`、未读数、read/dismiss/reply本地 UI state 会保存通知已读、忽略和回复 overlay顶部 Bell 已开始展示 approval、同步失败、服务异常和任务完成通知本地通知订阅偏好已接入 `/api/notifications/preferences`,可按总开关、桌面通知开关、等级和常用类型过滤 Inbox`/api/digital-employee/notifications` 已提供管理端可消费的本地通知业务视图,通知动作和订阅偏好事件的 sync `business_view` 会归类为 notification 并提炼状态字段;`need_input` 通知首次回复会同步转化为 `provide_task_input` 治理命令,保留通知回复 overlay 的同时写入 Task/Run/Event/outbox客户端已支持管理端通知状态拉取与本地通知动作回写未读 action/warn/error 通知会按偏好触发一次系统级桌面提醒,`digital_workday_notification_*` 同步事件只暴露通知 ID、状态、endpoint、错误和回复长度通知投影、本地订阅策略、桌面提醒、跨端未读状态与任务输入链路已开始闭环。