吸收数字员工审批跨端冲突可视化
This commit is contained in:
@@ -2344,6 +2344,13 @@
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
|
||||
margin-top: 8px; color: #60758a; font-size: 12px; line-height: 1.45;
|
||||
}
|
||||
.de-decision-chain-note--conflict {
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
color: #8c3434;
|
||||
background: rgba(244,96,96,0.09);
|
||||
border: 1px solid rgba(244,96,96,0.18);
|
||||
}
|
||||
@keyframes deFeedIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
|
||||
@@ -920,8 +920,8 @@ export function recordApprovalAction(
|
||||
});
|
||||
}
|
||||
|
||||
export function pullApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }> {
|
||||
return apiFetch<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>('/api/approval/updates/pull', {
|
||||
export function pullApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; conflicted?: number; skipped?: boolean; error?: string | null }> {
|
||||
return apiFetch<{ ok: boolean; applied?: number; ignored?: number; conflicted?: number; skipped?: boolean; error?: string | null }>('/api/approval/updates/pull', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ declare global {
|
||||
setSkillEnabled?: (skillId: string, enabled: boolean) => Promise<{ ok?: boolean; skill_id?: string; enabled?: boolean; error?: string }>;
|
||||
pullSkillPolicies?: () => Promise<{ ok?: boolean; skipped?: boolean; error?: string | null }>;
|
||||
pullRiskApprovalPolicy?: () => Promise<{ ok?: boolean; skipped?: boolean; error?: string | null }>;
|
||||
pullApprovalUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>;
|
||||
pullApprovalUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; conflicted?: number; skipped?: boolean; error?: string | null }>;
|
||||
submitApprovalDecision?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
|
||||
pullNotificationUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>;
|
||||
submitNotificationAction?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
|
||||
@@ -1308,7 +1308,7 @@ async function pullBridgeRouteDecisionPolicy(): Promise<RouteDecisionPolicyPullR
|
||||
}
|
||||
}
|
||||
|
||||
async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }> {
|
||||
async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; conflicted?: number; skipped?: boolean; error?: string | null }> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.pullApprovalUpdates) {
|
||||
return { ok: false, error: 'qimingclaw_bridge_unavailable' };
|
||||
@@ -1322,6 +1322,7 @@ async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: num
|
||||
ok: result?.ok !== false,
|
||||
applied: Number(result?.applied ?? 0),
|
||||
ignored: Number(result?.ignored ?? 0),
|
||||
conflicted: Number(result?.conflicted ?? 0),
|
||||
...(result?.skipped ? { skipped: true } : {}),
|
||||
...(result?.error ? { error: result.error } : {}),
|
||||
};
|
||||
@@ -3560,6 +3561,7 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView
|
||||
const runtime = runtimeById.get(approvalId);
|
||||
const summary = approvalActionSummary(snapshot, approvalId, approval);
|
||||
const workflow = approvalWorkflowSummary(approval);
|
||||
const conflict = approvalConflictSummary(approval);
|
||||
const entity = businessEntityFromRefs({
|
||||
plan_id: approval.plan_id,
|
||||
task_id: approval.task_id,
|
||||
@@ -3588,6 +3590,12 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView
|
||||
approval_step_index: workflow.step_index,
|
||||
approval_step_count: workflow.step_count,
|
||||
next_step_label: workflow.next_step_label,
|
||||
approval_conflict_active: conflict.active,
|
||||
approval_conflict_reason: conflict.reason,
|
||||
approval_conflict_remote_status: conflict.remote_status,
|
||||
approval_conflict_detected_at: conflict.detected_at,
|
||||
approval_conflict_local_status: conflict.local_status,
|
||||
approval_conflict_remote_current_step_id: conflict.remote_current_step_id,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -3728,6 +3736,19 @@ function approvalWorkflowSummary(approval: Record<string, unknown>): ApprovalWor
|
||||
};
|
||||
}
|
||||
|
||||
function approvalConflictSummary(approval: Record<string, unknown>): Record<string, unknown> {
|
||||
const payload = asRecord(approval.payload) ?? {};
|
||||
const conflict = asRecord(payload.conflict) ?? {};
|
||||
return {
|
||||
active: conflict.active === true,
|
||||
reason: stringValue(conflict.reason) || null,
|
||||
remote_status: stringValue(conflict.remote_status ?? conflict.remoteStatus) || null,
|
||||
detected_at: stringValue(conflict.detected_at ?? conflict.detectedAt) || null,
|
||||
local_status: stringValue(conflict.local_status ?? conflict.localStatus) || null,
|
||||
remote_current_step_id: stringValue(conflict.remote_current_step_id ?? conflict.remoteCurrentStepId) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function approvalActionSummary(
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
approvalId: string,
|
||||
@@ -4994,6 +5015,7 @@ function buildWorkdayDecisions(
|
||||
const plan = planId ? plansById.get(planId) : null;
|
||||
const task = taskId ? tasksById.get(taskId) : null;
|
||||
const summary = approvalSummaries.get(decisionId);
|
||||
const hasConflict = summary?.approval_conflict_active === true;
|
||||
return {
|
||||
id: decisionId,
|
||||
title: stringValue(approval.title) || stringValue(payload?.title) || '待确认事项',
|
||||
@@ -5016,7 +5038,17 @@ function buildWorkdayDecisions(
|
||||
approval_step_index: numberValue(summary?.approval_step_index),
|
||||
approval_step_count: numberValue(summary?.approval_step_count),
|
||||
next_step_label: stringValue(summary?.next_step_label) || null,
|
||||
actions: isDecisionOpenStatus(status)
|
||||
conflict: hasConflict
|
||||
? {
|
||||
active: true,
|
||||
reason: stringValue(summary?.approval_conflict_reason) || null,
|
||||
remote_status: stringValue(summary?.approval_conflict_remote_status) || null,
|
||||
detected_at: stringValue(summary?.approval_conflict_detected_at) || null,
|
||||
local_status: stringValue(summary?.approval_conflict_local_status) || null,
|
||||
remote_current_step_id: stringValue(summary?.approval_conflict_remote_current_step_id) || null,
|
||||
}
|
||||
: null,
|
||||
actions: isDecisionOpenStatus(status) && !hasConflict
|
||||
? [
|
||||
{ id: 'approved', label: '同意', tone: 'primary' },
|
||||
{ id: 'rejected', label: '驳回', tone: 'secondary' },
|
||||
|
||||
@@ -302,6 +302,24 @@ function approvalActionLabel(value?: string | null): string {
|
||||
return normalized || '暂无动作';
|
||||
}
|
||||
|
||||
function approvalConflictReasonLabel(value?: string | null): string {
|
||||
const normalized = compactText(value).toLowerCase();
|
||||
if (normalized === 'terminal_status_mismatch') return '终态不一致';
|
||||
if (normalized === 'local_newer_status_mismatch') return '本地状态较新';
|
||||
if (normalized === 'workflow_step_mismatch') return '审批步骤不一致';
|
||||
return normalized || '跨端状态不一致';
|
||||
}
|
||||
|
||||
function approvalStatusShortLabel(value?: string | null): string {
|
||||
const normalized = compactText(value).toLowerCase();
|
||||
if (normalized === 'approved') return '已同意';
|
||||
if (normalized === 'rejected') return '已驳回';
|
||||
if (normalized === 'cancelled') return '已取消';
|
||||
if (normalized === 'pending') return '待确认';
|
||||
if (normalized === 'dismissed') return '已忽略';
|
||||
return normalized || '未知';
|
||||
}
|
||||
|
||||
function dutyCardClass(planItem: DigitalPlanItem, selected: boolean): string {
|
||||
const classes = ['de-template-card-row', 'de-duty-card', 'de-duty-card--compact'];
|
||||
if (!planItem.implemented) classes.push('de-duty-card--disabled');
|
||||
@@ -1464,7 +1482,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
await fetchProjection();
|
||||
if (!mountedRef.current) return;
|
||||
setActionNotice(result.ok
|
||||
? `审批更新已拉取:应用 ${result.applied ?? 0} 条,忽略 ${result.ignored ?? 0} 条。`
|
||||
? `审批更新已拉取:应用 ${result.applied ?? 0} 条,忽略 ${result.ignored ?? 0} 条,冲突 ${result.conflicted ?? 0} 项。`
|
||||
: `审批更新拉取失败:${result.error || '未知错误'}。`);
|
||||
} catch (error) {
|
||||
if (mountedRef.current) {
|
||||
@@ -3363,6 +3381,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
const timeoutBusy = actionBusy === `${actionBusyPrefix}mark_timeout`;
|
||||
const markRiskBusy = actionBusy === `${actionBusyPrefix}mark_risk`;
|
||||
const clearRiskBusy = actionBusy === `${actionBusyPrefix}clear_risk`;
|
||||
const approvalConflict = decision.conflict?.active ? decision.conflict : null;
|
||||
return (
|
||||
<div key={decision.id} className="de-task-manager-decision-card">
|
||||
<div className="de-log-entry-meta">
|
||||
@@ -3371,6 +3390,9 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
</div>
|
||||
<p>{decision.scenario}</p>
|
||||
<div className="de-approval-summary-grid">
|
||||
{approvalConflict && (
|
||||
<span className="de-badge de-badge-danger">跨端冲突</span>
|
||||
)}
|
||||
<span className={`de-badge ${badgeClass(approvalRiskTone(decision.risk_level))}`}>
|
||||
{approvalRiskLabel(decision.risk_level)}
|
||||
</span>
|
||||
@@ -3386,6 +3408,15 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
<span>{decision.current_participant ? `处理人 ${decision.current_participant}` : '未指定处理人'}</span>
|
||||
<span>{decision.next_step_label ? `下一步 ${decision.next_step_label}` : '无后续步骤'}</span>
|
||||
</div>
|
||||
{approvalConflict && (
|
||||
<div className="de-decision-chain-note de-decision-chain-note--conflict">
|
||||
<span className="de-badge de-badge-danger">人工排查</span>
|
||||
<span>
|
||||
{approvalConflictReasonLabel(approvalConflict.reason)}:本地 {approvalStatusShortLabel(approvalConflict.local_status ?? decision.status)},管理端 {approvalStatusShortLabel(approvalConflict.remote_status)}。
|
||||
{approvalConflict.detected_at ? ` 检测 ${formatBeijingDateTime(approvalConflict.detected_at) || approvalConflict.detected_at}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="de-approval-action-fields">
|
||||
<input
|
||||
value={draft.comment}
|
||||
@@ -3451,8 +3482,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
</button>
|
||||
</div>
|
||||
<div className="de-decision-chain-note">
|
||||
<span className="de-badge de-badge-success">本地记录</span>
|
||||
<span>{isDecisionOpen(decision.status) ? '处理结果会写入 qimingclaw runtime event。' : '该事项已在本地状态中处理。'}</span>
|
||||
<span className={`de-badge ${approvalConflict ? 'de-badge-danger' : 'de-badge-success'}`}>{approvalConflict ? '冲突保护' : '本地记录'}</span>
|
||||
<span>{approvalConflict ? '直接同意/驳回已暂停,请先同步或在管理端工作台排查。' : isDecisionOpen(decision.status) ? '处理结果会写入 qimingclaw runtime event。' : '该事项已在本地状态中处理。'}</span>
|
||||
</div>
|
||||
<div className="de-workbench-actions">
|
||||
{decision.actions.map((action) => (
|
||||
@@ -3460,7 +3491,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
key={action.id}
|
||||
type="button"
|
||||
className={action.tone === 'primary' ? 'de-workbench-action-accent' : ''}
|
||||
disabled={actionBusy === `${decision.id}:${action.id}` || !isDecisionOpen(decision.status)}
|
||||
disabled={actionBusy === `${decision.id}:${action.id}` || !isDecisionOpen(decision.status) || Boolean(approvalConflict)}
|
||||
onClick={() => { void resolveDecision(decision, action.id); }}
|
||||
>
|
||||
{actionBusy === `${decision.id}:${action.id}` ? '处理中...' : action.label}
|
||||
|
||||
@@ -976,6 +976,14 @@ export interface DigitalEmployeeDecision {
|
||||
approval_step_index?: number | null;
|
||||
approval_step_count?: number | null;
|
||||
next_step_label?: string | null;
|
||||
conflict?: {
|
||||
active: boolean;
|
||||
reason?: string | null;
|
||||
remote_status?: string | null;
|
||||
detected_at?: string | null;
|
||||
local_status?: string | null;
|
||||
remote_current_step_id?: string | null;
|
||||
} | null;
|
||||
actions: DigitalEmployeeDecisionAction[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user