feat(client): persist digital governance events

This commit is contained in:
baiyanyun
2026-06-07 18:59:59 +08:00
parent 5e76828afe
commit e37024bdbe
14 changed files with 925 additions and 158 deletions

View File

@@ -34,6 +34,7 @@ declare global {
saveUiState?: (update: { state: AdapterState; action?: string; date?: string; metadata?: Record<string, unknown> }) => Promise<AdapterState>;
getSyncStatus?: () => Promise<QimingclawSyncStatus>;
flushSync?: () => Promise<QimingclawSyncStatus>;
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
};
};
}
@@ -124,6 +125,19 @@ interface QimingclawSnapshot {
uiState?: AdapterState | null;
}
interface QimingclawGovernanceCommandRecord {
commandKind: string;
commandId: string;
correlationId?: string;
planId?: string | null;
scheduleId?: string | null;
accepted?: boolean;
message?: string;
error?: string | null;
payload?: Record<string, unknown> | null;
result?: Record<string, unknown> | null;
}
interface QimingclawRuntimeRecords {
generatedAt: string;
plans: QimingclawPlanRecord[];
@@ -519,6 +533,34 @@ async function saveState(
}
}
async function recordGovernanceCommand(
body: GovernanceCommandRequest,
response: GovernanceCommandResponse,
): Promise<void> {
try {
await window.QimingClawBridge?.digital?.recordGovernanceCommand?.({
commandKind: body.command_kind,
commandId: response.command_id,
correlationId: response.correlation_id,
planId: stringFromUnknown(response.result?.plan_id)
|| stringFromUnknown(body.context_refs?.plan_id)
|| stringFromUnknown(body.payload?.plan_id)
|| null,
scheduleId: stringFromUnknown(response.result?.schedule_id)
|| stringFromUnknown(body.context_refs?.schedule_id)
|| stringFromUnknown(body.payload?.schedule_id)
|| null,
accepted: response.accepted,
message: response.message,
error: response.error ?? null,
payload: body.payload ?? null,
result: response.result ?? null,
});
} catch {
// Bridge persistence is best-effort; the adapter response should not block UI flow.
}
}
function parseJsonBody<T>(body: BodyInit | null | undefined): T {
if (typeof body !== 'string' || !body.trim()) return {} as T;
return JSON.parse(body) as T;
@@ -1689,37 +1731,43 @@ async function handleGovernanceCommand(
if (body.command_kind === 'create_plan') {
const createdPlanId = `qimingclaw-local-${slugifyIdPart(stringFromUnknown(body.payload?.title) || planId || 'plan')}-${Date.now()}`;
return governanceAccepted(commandId, correlationId, '已在 qimingclaw 兼容层创建本地计划草稿。', {
const response = governanceAccepted(commandId, correlationId, '已在 qimingclaw 兼容层创建本地计划草稿。', {
plan_id: createdPlanId,
status: 'draft',
source: 'qimingclaw-adapter',
requested_action: body.payload?.requested_action,
original_plan_id: body.payload?.original_plan_id ?? planId,
});
await recordGovernanceCommand(body, response);
return response;
}
if (body.command_kind === 'activate_plan' || body.command_kind === 'run_schedule_now') {
const targetPlanId = planId || runtimeDebugPlans(snapshot)[0]?.plan_id || PLANS[0]?.plan_id || 'qimingclaw-client-readiness';
const dispatch = await dispatchPlan(targetPlanId, snapshot);
return governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', {
const response = governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', {
plan_id: targetPlanId,
status: 'active',
dispatched_tasks: dispatch.dispatched_tasks,
source: 'qimingclaw-adapter',
});
await recordGovernanceCommand(body, response);
return response;
}
if (body.command_kind === 'create_schedule') {
return governanceAccepted(commandId, correlationId, '已记录 qimingclaw 兼容定时任务请求。', {
const response = governanceAccepted(commandId, correlationId, '已记录 qimingclaw 兼容定时任务请求。', {
schedule_id: scheduleId || `qimingclaw-schedule-${Date.now()}`,
plan_id: planId,
status: 'enabled',
source: 'qimingclaw-adapter',
});
await recordGovernanceCommand(body, response);
return response;
}
if (['submit_plan', 'approve_plan', 'amend_plan', 'resume_plan', 'pause_plan', 'cancel_plan'].includes(body.command_kind)) {
return governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},当前由 qimingclaw 本地 adapter 记录兼容状态。`, {
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},当前由 qimingclaw 本地 adapter 记录兼容状态。`, {
plan_id: planId,
status: body.command_kind === 'approve_plan'
? 'approved'
@@ -1728,15 +1776,19 @@ async function handleGovernanceCommand(
: 'accepted',
source: 'qimingclaw-adapter',
});
await recordGovernanceCommand(body, response);
return response;
}
return {
const response = {
command_id: commandId,
accepted: false,
correlation_id: correlationId,
error: 'missing_capability',
message: `qimingclaw 数字员工兼容层尚未支持 ${body.command_kind}`,
};
await recordGovernanceCommand(body, response);
return response;
}
function governanceAccepted(

View File

@@ -86,6 +86,7 @@ interface ManagementSyncFailure {
id: string;
entityType: string;
entityId: string;
entitySummary?: ManagementSyncEntitySummary | null;
operation: string;
attempts: number;
lastAttemptAt: string | null;
@@ -98,6 +99,12 @@ interface ManagementSyncFailure {
updatedAt: string;
}
interface ManagementSyncEntitySummary {
title?: string | null;
status?: string | null;
summary?: string | null;
}
// 技术侧 PlanTemplate 是任务模板来源;用户侧只展示为“任务”,运行时 Task/PlanStep 展示为“步骤”。
interface EmployeeVisualStateOption {
@@ -523,7 +530,9 @@ function failureGroupSummary(summary: ManagementSyncFailureSummary): string {
function syncFailureSummary(failure: ManagementSyncFailure): string {
const error = summarizeSyncError(failure.error);
const title = compactText(failure.entitySummary?.title);
return [
title,
`${entityTypeLabel(failure.entityType)} / ${failure.operation}`,
`重试 ${failure.attempts}`,
failure.dueForRetry ? '等待补偿' : `下次 ${syncFailureRetryTime(failure)}`,
@@ -2009,6 +2018,12 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<div className="de-event-detail-grid">
<div><span>Outbox</span><strong>{selectedSyncFailure.id}</strong></div>
<div><span></span><strong>{selectedSyncFailure.entityId}</strong></div>
{selectedSyncFailure.entitySummary?.title && (
<div><span></span><strong>{selectedSyncFailure.entitySummary.title}</strong></div>
)}
{selectedSyncFailure.entitySummary?.status && (
<div><span></span><strong>{selectedSyncFailure.entitySummary.status}</strong></div>
)}
<div><span></span><strong>{entityTypeLabel(selectedSyncFailure.entityType)}</strong></div>
<div><span></span><strong>{selectedSyncFailure.operation}</strong></div>
<div><span></span><strong>{selectedSyncFailure.attempts} </strong></div>
@@ -2020,6 +2035,12 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<span></span>
<p>{selectedSyncFailure.error || '暂无错误详情。'}</p>
</div>
{selectedSyncFailure.entitySummary?.summary && (
<div className="de-event-detail-result">
<span></span>
<p>{selectedSyncFailure.entitySummary.summary}</p>
</div>
)}
{(selectedSyncFailure.attemptHistory?.length ?? 0) > 0 && (
<div className="de-sync-attempt-history">
<span></span>