feat(client): persist digital governance events
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user