吸收数字员工风险策略本地管理
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;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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"],
|
||||
];
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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、错误和回复长度,通知投影、本地订阅策略、桌面提醒、跨端未读状态与任务输入链路已开始闭环。
|
||||
|
||||
Reference in New Issue
Block a user