吸收数字员工审批冲突与产物业务闭环

This commit is contained in:
baiyanyun
2026-06-12 16:29:01 +08:00
parent fffa0ebe50
commit 81d2797035
26 changed files with 2963 additions and 241 deletions

View File

@@ -14,6 +14,9 @@ import type {
BrowserSession,
ApprovalSubscriptionPreferences,
BusinessViewResponse,
DigitalEmployeeBusinessViewKind,
DigitalEmployeeBusinessViewQuery,
DigitalEmployeeBusinessViewRow,
PlanStepBusinessRow,
PlanStepDispatchPolicy,
PlanStepDispatchPolicyPullResult,
@@ -713,6 +716,26 @@ export function getPlanStepGraph(params: Record<string, string | number | null |
return apiFetch<BusinessViewResponse<PlanStepBusinessRow>>(`/api/digital-employee/plan-step-graph${suffix ? `?${suffix}` : ''}`);
}
export function getDigitalEmployeeBusinessView(
kind: DigitalEmployeeBusinessViewKind,
params: DigitalEmployeeBusinessViewQuery = {},
): Promise<BusinessViewResponse<DigitalEmployeeBusinessViewRow>> {
const endpoints: Record<DigitalEmployeeBusinessViewKind, string> = {
plans: '/api/digital-employee/plans',
tasks: '/api/digital-employee/tasks',
runs: '/api/digital-employee/runs',
events: '/api/digital-employee/events',
artifacts: '/api/digital-employee/artifacts',
approvals: '/api/digital-employee/approvals',
};
const query = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
});
const suffix = query.toString();
return apiFetch<BusinessViewResponse<DigitalEmployeeBusinessViewRow>>(`${endpoints[kind]}${suffix ? `?${suffix}` : ''}`);
}
export function markNotificationRead(id: string): Promise<boolean> {
return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, {
method: 'POST',
@@ -893,6 +916,21 @@ export function accessArtifact(artifactId: string, action: 'preview' | 'download
});
}
export function recordDailyReportDelivery(body: {
reportId: string;
action: 'send' | 'confirm' | 'request_approval';
status: string;
actor?: string | null;
endpoint?: string | null;
remoteId?: string | null;
message?: string | null;
}): Promise<any> {
return apiFetch('/api/digital-employee/daily-reports/delivery', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function getArtifactAccessEvents(params: Record<string, string | number | undefined> = {}): Promise<any> {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
@@ -923,6 +961,16 @@ export function setArtifactRetention(
});
}
export function cleanupArtifact(
artifactId: string,
options: Record<string, string | null | undefined> = {},
): Promise<any> {
return apiFetch(`/api/digital-employee/artifacts/${encodeURIComponent(artifactId)}/cleanup`, {
method: 'POST',
body: JSON.stringify(options),
});
}
export function recordApprovalAction(
approvalId: string,
action: 'comment' | 'delegate' | 'mark_timeout' | 'mark_risk' | 'clear_risk' | 'approve' | 'reject',
@@ -934,6 +982,16 @@ export function recordApprovalAction(
});
}
export function resolveApprovalConflict(
approvalId: string,
options: { resolution: 'keep_local' | 'accept_remote' | 'mark_reviewed'; actor?: string | null; comment?: string | null },
): Promise<any> {
return apiFetch(`/api/approval/${encodeURIComponent(approvalId)}/conflict/resolve`, {
method: 'POST',
body: JSON.stringify(options),
});
}
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',

View File

@@ -65,6 +65,7 @@ declare global {
pullRiskApprovalPolicy?: () => Promise<{ ok?: boolean; 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 }>;
resolveApprovalConflict?: (input: QimingclawApprovalConflictResolutionInput) => Promise<QimingclawApprovalConflictResolutionResponse>;
pullNotificationUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>;
submitNotificationAction?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
showDesktopNotification?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; shown?: boolean; reason?: string | null; error?: string | null }>;
@@ -93,7 +94,9 @@ declare global {
recordRouteInterventionResolution?: (input: QimingclawRouteInterventionResolutionInput) => Promise<RouteInterventionRecord>;
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise<QimingclawDailyReportRecordResult | null>;
recordDailyReportDelivery?: (input: QimingclawDailyReportDeliveryInput) => Promise<QimingclawDailyReportDeliveryResult | null>;
accessArtifact?: (artifactId: string, action: 'preview' | 'download' | 'open_location', options?: { actor?: string | null; source?: string | null }) => Promise<QimingclawArtifactAccessResponse>;
cleanupArtifact?: (artifactId: string, options?: QimingclawArtifactCleanupInput) => Promise<QimingclawArtifactCleanupResponse>;
recordArtifactLifecycle?: (input: QimingclawArtifactLifecycleInput) => Promise<QimingclawArtifactLifecycleResult | null>;
recordApprovalAction?: (input: QimingclawApprovalActionInput) => Promise<QimingclawApprovalActionResult | null>;
restartManagedService?: (serviceId: string, options?: { reason?: string | null; actor?: string | null }) => Promise<QimingclawManagedServiceActionResponse>;
@@ -274,6 +277,10 @@ interface QimingclawDailyReportRecordInput {
artifactSources?: unknown[];
filename: string;
textContent: string;
htmlFilename?: string | null;
htmlContent?: string | null;
pdfFilename?: string | null;
pdfContentBase64?: string | null;
generatedAt?: string | null;
reportScheduleTime?: string | null;
source?: string | null;
@@ -286,9 +293,26 @@ interface QimingclawDailyReportRecordResult {
taskId: string;
runId: string;
artifactId: string;
htmlArtifactId: string;
pdfArtifactId: string;
eventId: string;
}
interface QimingclawDailyReportDeliveryInput {
reportId: string;
action: 'send' | 'confirm' | 'request_approval';
status: string;
actor?: string | null;
endpoint?: string | null;
remoteId?: string | null;
message?: string | null;
}
interface QimingclawDailyReportDeliveryResult {
eventId: string;
approvalId?: string | null;
}
interface QimingclawArtifactAccessResponse {
ok: boolean;
artifact_id: string;
@@ -300,6 +324,24 @@ interface QimingclawArtifactAccessResponse {
event_id?: string | null;
}
interface QimingclawArtifactCleanupInput {
actor?: string | null;
reason?: string | null;
source?: string | null;
retentionStatus?: string | null;
lastRetentionAction?: string | null;
}
interface QimingclawArtifactCleanupResponse {
ok: boolean;
artifact_id: string;
status: 'deleted' | 'rejected' | 'failed';
error?: string;
reason_codes?: string[];
event_id?: string | null;
deleted_at?: string | null;
}
interface QimingclawArtifactLifecycleInput {
artifactId?: string | null;
action: string;
@@ -343,6 +385,23 @@ interface QimingclawApprovalActionResult {
payload: Record<string, unknown>;
}
interface QimingclawApprovalConflictResolutionInput {
approvalId?: string | null;
resolution?: string | null;
actor?: string | null;
comment?: string | null;
}
interface QimingclawApprovalConflictResolutionResponse {
ok: boolean;
approval_id: string;
resolution: string | null;
status: string;
conflict_active: boolean;
reason_codes?: string[];
event_id?: string | null;
}
interface ApprovalWorkflowSummary {
workflow_id: string | null;
current_step_id: string | null;
@@ -858,6 +917,9 @@ export async function handleQimingclawDigitalApi<T>(
const rows = filterDailyReportRows(dailyReportRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
if (method === 'POST' && pathname === '/api/digital-employee/daily-reports/delivery') {
return await handleDailyReportDelivery(parseJsonBody<QimingclawDailyReportDeliveryInput>(options.body)) as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/skill-call-audits') {
const rows = filterSkillCallAuditRows(skillCallAuditRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
@@ -876,6 +938,11 @@ export async function handleQimingclawDigitalApi<T>(
const artifactId = decodeURIComponent(artifactRetentionMatch[1] || '');
return await handleArtifactRetention(artifactId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
const artifactCleanupMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/cleanup$/);
if (method === 'POST' && artifactCleanupMatch) {
const artifactId = decodeURIComponent(artifactCleanupMatch[1] || '');
return await handleArtifactCleanup(artifactId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
const artifactVersionsMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/versions$/);
if (method === 'GET' && artifactVersionsMatch) {
const artifactId = decodeURIComponent(artifactVersionsMatch[1] || '');
@@ -901,6 +968,11 @@ export async function handleQimingclawDigitalApi<T>(
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
const approvalConflictResolveMatch = pathname.match(/^\/api\/approval\/([^/]+)\/conflict\/resolve$/);
if (method === 'POST' && approvalConflictResolveMatch) {
const approvalId = decodeURIComponent(approvalConflictResolveMatch[1] || '');
return await handleApprovalConflictResolution(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/approval/subscriptions') {
return approvalSubscriptionPreferences(notificationAdapterState(await readQimingclawSnapshot())) as T;
}
@@ -2080,6 +2152,52 @@ async function handleArtifactRetention(
};
}
async function handleArtifactCleanup(
artifactId: string,
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<Record<string, unknown>> {
const bridge = window.QimingClawBridge?.digital;
const artifact = artifactDetailRow(snapshot, artifactId);
const riskPolicy = await evaluateBridgeRiskPolicy({
actionKind: 'artifact.cleanup.delete_local_file',
actionLabel: '清理产物本地文件',
riskLevel: 'high',
reasonCodes: ['artifact_cleanup'],
actor: stringValue(body.actor) || 'digital_employee_ui',
source: 'sgrobot-digital-adapter',
planId: artifact ? stringValue(artifact.plan_id) || null : null,
taskId: artifact ? stringValue(artifact.task_id) || null : null,
runId: artifact ? stringValue(artifact.run_id) || null : null,
correlationId: `artifact:${artifactId}:cleanup`,
payload: { artifact_id: artifactId, retention_status: stringValue(artifact?.retention_status) || null },
});
const blocked = riskPolicyBlockedResponse(riskPolicy);
if (blocked) return { ...blocked, artifact_id: artifactId, status: 'rejected', item: artifact };
if (!bridge?.cleanupArtifact) {
return { ok: false, artifact_id: artifactId, status: 'rejected', error: 'bridge_unavailable', reason_codes: ['bridge_unavailable'], item: artifact };
}
try {
const result = await bridge.cleanupArtifact(artifactId, {
actor: stringValue(body.actor) || 'digital_employee_ui',
source: stringValue(body.source) || 'sgrobot-digital-adapter',
reason: stringValue(body.reason) || 'digital_employee_artifact_cleanup',
retentionStatus: stringValue(body.retention_status ?? body.retentionStatus) || stringValue(artifact?.retention_status) || null,
lastRetentionAction: stringValue(body.last_retention_action ?? body.lastRetentionAction) || stringValue(artifact?.last_retention_action) || null,
});
return { ...result, item: artifact };
} catch (error) {
return {
ok: false,
artifact_id: artifactId,
status: 'failed',
error: error instanceof Error ? error.message : String(error),
reason_codes: ['artifact_cleanup_failed'],
item: artifact,
};
}
}
async function handleApprovalAction(
approvalId: string,
body: Record<string, unknown>,
@@ -2131,6 +2249,47 @@ async function handleApprovalAction(
};
}
async function handleApprovalConflictResolution(
approvalId: string,
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<Record<string, unknown>> {
const bridge = window.QimingClawBridge?.digital;
const resolution = stringValue(body.resolution) || 'mark_reviewed';
const approval = approvalDetailRow(snapshot, approvalId);
const riskPolicy = await evaluateBridgeRiskPolicy({
actionKind: `approval.conflict.resolve.${resolution}`,
actionLabel: `审批冲突裁决:${resolution}`,
riskLevel: resolution === 'accept_remote' ? 'medium' : 'normal',
reasonCodes: ['approval_conflict_resolution'],
actor: stringValue(body.actor) || 'digital_employee_ui',
source: 'sgrobot-digital-adapter',
planId: approval ? stringValue(approval.plan_id) || null : null,
taskId: approval ? stringValue(approval.task_id) || null : null,
runId: approval ? stringValue(approval.run_id) || null : null,
correlationId: `approval:${approvalId}:conflict:${resolution}`,
payload: { approval_id: approvalId, resolution },
});
const blocked = riskPolicyBlockedResponse(riskPolicy);
if (blocked) return { ...blocked, approval_id: approvalId, resolution, item: approval };
if (!bridge?.resolveApprovalConflict) {
return { ok: false, approval_id: approvalId, resolution, status: 'rejected', error: 'bridge_unavailable', reason_codes: ['bridge_unavailable'], item: approval };
}
const result = await bridge.resolveApprovalConflict({
approvalId,
resolution,
actor: stringValue(body.actor) || 'digital_employee_ui',
comment: stringValue(body.comment) || null,
});
return {
...result,
ok: result?.ok === true,
approval_id: result?.approval_id || approvalId,
resolution: result?.resolution ?? resolution,
item: approval,
};
}
function syncLine(snapshot: QimingclawSnapshot | null): string {
const sync = snapshot?.sync;
if (!sync) return '管理端同步:未连接';
@@ -3632,6 +3791,7 @@ function businessArtifactRows(snapshot: QimingclawSnapshot | null): BusinessView
fallback_id: artifact.subject_id ?? artifactId,
});
const lifecycle = artifactLifecycleSummary(snapshot, artifactId);
const cleanup = artifactCleanupSummary(snapshot, artifactId, artifact);
const version = artifactVersionMeta(artifacts, artifact);
const delivery = artifactDeliverySummary(artifact);
return {
@@ -3647,6 +3807,10 @@ function businessArtifactRows(snapshot: QimingclawSnapshot | null): BusinessView
retention_status: lifecycle.retention_status,
retention_until: lifecycle.retention_until,
last_retention_action: lifecycle.last_retention_action,
cleanup_status: cleanup.cleanup_status,
deleted_at: cleanup.deleted_at,
cleanup_reason: cleanup.cleanup_reason,
cleanup_event_id: cleanup.cleanup_event_id,
last_access_status: stringValue(asRecord(artifact.access)?.last_access_status) || null,
delivery_summary: delivery,
};
@@ -3661,6 +3825,7 @@ function artifactDetailRow(snapshot: QimingclawSnapshot | null, artifactId: stri
...row,
versions: artifactVersionRows(snapshot, stringValue(row.artifact_id)),
lifecycle_events: artifactLifecycleRows(snapshot).filter((event) => stringValue(event.artifact_id) === stringValue(row.artifact_id)).slice(0, 20),
cleanup_events: artifactCleanupRows(snapshot).filter((event) => stringValue(event.artifact_id) === stringValue(row.artifact_id)).slice(0, 20),
};
}
@@ -3730,6 +3895,10 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView
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,
approval_conflict_resolved_at: conflict.resolved_at,
approval_conflict_resolution: conflict.resolution,
approval_conflict_resolution_actor: conflict.resolution_actor,
approval_conflict_resolution_event_id: conflict.resolution_event_id,
};
});
}
@@ -3873,6 +4042,7 @@ 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) ?? {};
const resolution = asRecord(conflict.resolution) ?? {};
return {
active: conflict.active === true,
reason: stringValue(conflict.reason) || null,
@@ -3880,6 +4050,10 @@ function approvalConflictSummary(approval: Record<string, unknown>): Record<stri
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,
resolved_at: stringValue(resolution.resolved_at ?? resolution.resolvedAt) || null,
resolution: stringValue(resolution.resolution) || null,
resolution_actor: stringValue(resolution.actor) || null,
resolution_event_id: stringValue(resolution.event_id ?? resolution.eventId) || null,
};
}
@@ -4023,6 +4197,47 @@ function artifactLifecycleRows(snapshot: QimingclawSnapshot | null): BusinessVie
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
}
function artifactCleanupRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => ['artifact_cleanup_executed', 'artifact_cleanup_rejected', 'artifact_cleanup_failed'].includes(event.kind))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
return {
cleanup_event_id: event.id,
event_id: event.id,
kind: event.kind,
artifact_id: stringValue(payload.artifact_id) || null,
status: stringValue(payload.status) || null,
cleanup_status: stringValue(payload.status) || null,
cleanup_reason: stringValue(payload.reason) || null,
reason_codes: uniqueStrings(arrayValue(payload.reason_codes ?? payload.reasonCodes).map(stringValue)),
actor: stringValue(payload.actor) || null,
source: stringValue(payload.source) || null,
deleted_at: stringValue(payload.deleted_at) || null,
plan_id: (event.planId ?? stringValue(payload.plan_id)) || null,
task_id: (event.taskId ?? stringValue(payload.task_id)) || null,
run_id: (event.runId ?? stringValue(payload.run_id)) || null,
message: event.message,
occurred_at: event.occurredAt,
sync_status: event.syncStatus ?? null,
payload,
};
})
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
}
function artifactCleanupSummary(snapshot: QimingclawSnapshot | null, artifactId: string, artifact?: ArtifactEntry): Record<string, unknown> {
const payloadCleanup = asRecord(asRecord(artifact?.payload)?.cleanup) ?? {};
const latest = artifactCleanupRows(snapshot)
.find((event) => stringValue(event.artifact_id) === artifactId) ?? null;
return {
cleanup_status: stringValue(latest?.cleanup_status) || stringValue(payloadCleanup.status) || null,
deleted_at: stringValue(latest?.deleted_at) || stringValue(payloadCleanup.deleted_at) || null,
cleanup_reason: stringValue(latest?.cleanup_reason) || stringValue(payloadCleanup.cleanup_reason) || null,
cleanup_event_id: stringValue(latest?.cleanup_event_id) || stringValue(payloadCleanup.event_id) || null,
};
}
function artifactLifecycleSummary(snapshot: QimingclawSnapshot | null, artifactId: string): Record<string, unknown> {
const latest = artifactLifecycleRows(snapshot)
.find((event) => stringValue(event.artifact_id) === artifactId && stringValue(event.kind) === 'artifact_retention_marked');
@@ -4107,7 +4322,7 @@ function artifactVersionGroupKey(artifact: Record<string, unknown>): string {
function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return buildArtifacts(snapshot)
.filter(isDailyReportArtifact)
.filter(isDailyReportSummaryArtifact)
.map((artifact) => {
const payload = asRecord(artifact.payload) ?? {};
const artifactId = stringValue(artifact.artifact_id) || stringValue(payload.artifact_id) || stringValue(artifact.subject_id);
@@ -4126,8 +4341,13 @@ function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[]
detail_lines: arrayValue(payload.detail_lines),
artifact_sources: arrayValue(payload.artifact_sources),
filename: stringValue(payload.filename) || stringValue(artifact.name) || dailyReportFilename(date),
html_artifact_id: stringValue(payload.html_artifact_id) || `${reportId}:artifact:html`,
pdf_artifact_id: stringValue(payload.pdf_artifact_id) || `${reportId}:artifact:pdf`,
html_filename: stringValue(payload.html_filename) || dailyReportHtmlFilename(date),
pdf_filename: stringValue(payload.pdf_filename) || dailyReportPdfFilename(date),
format: stringValue(payload.format) || 'text',
text_content: stringValue(payload.text_content) || stringValue(artifact.value),
html_content: stringValue(payload.html_content) || '',
generated_at: generatedAt,
sync_status: stringValue(artifact.sync_status) || null,
entity_type: 'artifact',
@@ -4143,6 +4363,12 @@ function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[]
.sort((left, right) => stringValue(right.generated_at).localeCompare(stringValue(left.generated_at)));
}
function isDailyReportSummaryArtifact(artifact: ArtifactEntry): boolean {
const payload = asRecord(artifact.payload) ?? {};
const format = stringValue(payload.format) || 'text';
return stringValue(artifact.kind) === 'daily_report' && format !== 'html' && format !== 'pdf';
}
function isDailyReportArtifact(artifact: ArtifactEntry): boolean {
const payload = asRecord(artifact.payload) ?? {};
return stringValue(artifact.kind) === 'daily_report'
@@ -5310,6 +5536,8 @@ function withArtifactAccessSummary(artifact: ArtifactEntry, snapshot: Qimingclaw
const formalRuntimeArtifact = runtimeArtifacts(snapshot).some((runtime) => runtime.id === artifactId || runtime.remoteId === artifactId);
const previewable = formalRuntimeArtifact && (hasDailyText || hasLocalUri);
const lifecycle = artifactLifecycleSummary(snapshot, artifactId);
const cleanup = artifactCleanupSummary(snapshot, artifactId, artifact);
const deleted = stringValue(cleanup.cleanup_status) === 'deleted';
const version = artifactVersionMeta(artifacts, artifact);
return {
...artifact,
@@ -5319,11 +5547,15 @@ function withArtifactAccessSummary(artifact: ArtifactEntry, snapshot: Qimingclaw
retention_status: lifecycle.retention_status,
retention_until: lifecycle.retention_until,
last_retention_action: lifecycle.last_retention_action,
cleanup_status: stringValue(cleanup.cleanup_status) || null,
deleted_at: stringValue(cleanup.deleted_at) || null,
cleanup_reason: stringValue(cleanup.cleanup_reason) || null,
cleanup_event_id: stringValue(cleanup.cleanup_event_id) || null,
delivery_summary: artifactDeliverySummary(artifact),
access: {
previewable,
downloadable: formalRuntimeArtifact && hasLocalUri,
openable: formalRuntimeArtifact && hasLocalUri,
previewable: !deleted && previewable,
downloadable: !deleted && formalRuntimeArtifact && hasLocalUri,
openable: !deleted && formalRuntimeArtifact && hasLocalUri,
access_policy: formalRuntimeArtifact ? 'qimingclaw-local-artifact-policy' : 'runtime-projection-readonly',
last_access_status: stringValue(lastAccess?.status) || null,
},
@@ -5875,9 +6107,13 @@ function buildDailyReportProjection(input: {
const detailLines = input.events.map((event) => `${event.time} ${event.task_title}${event.detail}`);
const reportId = input.generatedAt ? dailyReportId(input.date, input.generatedAt) : null;
const downloadFilename = dailyReportFilename(input.date);
const htmlFilename = dailyReportHtmlFilename(input.date);
const pdfFilename = dailyReportPdfFilename(input.date);
const baseReport: DigitalEmployeeDailyReport = {
report_id: reportId,
artifact_id: reportId ? `${reportId}:artifact:text` : null,
html_artifact_id: reportId ? `${reportId}:artifact:html` : null,
pdf_artifact_id: reportId ? `${reportId}:artifact:pdf` : null,
event_id: reportId ? `${reportId}:event:generated` : null,
date: input.date,
title,
@@ -5893,15 +6129,20 @@ function buildDailyReportProjection(input: {
detail_lines: detailLines,
artifact_sources: input.artifactSources,
download_filename: downloadFilename,
html_filename: htmlFilename,
pdf_filename: pdfFilename,
text_content: null,
html_content: null,
pdf_available: Boolean(input.generatedAt),
pdf_url: null,
};
const textContent = input.generatedAt
? buildDailyReportText(baseReport, input.runDetails, input.artifactSources)
: null;
return {
...baseReport,
text_content: input.generatedAt
? buildDailyReportText(baseReport, input.runDetails, input.artifactSources)
: null,
text_content: textContent,
html_content: textContent ? buildDailyReportHtml(baseReport, textContent) : null,
};
}
@@ -5950,6 +6191,59 @@ function dailyReportFilename(date: string): string {
return `qimingclaw-digital-report-${date}.txt`;
}
function dailyReportHtmlFilename(date: string): string {
return `qimingclaw-digital-report-${date}.html`;
}
function dailyReportPdfFilename(date: string): string {
return `qimingclaw-digital-report-${date}.pdf`;
}
function buildDailyReportHtml(report: DigitalEmployeeDailyReport, textContent: string): string {
const body = textContent.split('\n').map((line) => {
if (line.startsWith('## ')) return `<h2>${escapeHtml(line.slice(3))}</h2>`;
if (line.startsWith('- ')) return `<li>${escapeHtml(line.slice(2))}</li>`;
if (!line.trim()) return '';
return `<p>${escapeHtml(line)}</p>`;
}).join('\n');
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(report.title)}</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.65;padding:32px;color:#172033;}article{max-width:860px;margin:0 auto;}h1{font-size:28px;}h2{font-size:18px;margin-top:24px;}li{margin:4px 0;}</style></head><body><article><h1>${escapeHtml(report.title)}</h1>${body}</article></body></html>`;
}
function buildDailyReportPdfBase64(report: DigitalEmployeeDailyReport, textContent: string): string {
const safeText = `${report.title}\n\n${textContent}`.replace(/[^\x20-\x7E\n]/g, '?').slice(0, 3600);
const lines = safeText.split('\n').slice(0, 48);
const commands = lines.map((line, index) => `BT /F1 11 Tf 48 ${760 - index * 14} Td (${pdfEscape(line)}) Tj ET`).join('\n');
const stream = commands || 'BT /F1 11 Tf 48 760 Td (Daily Report) Tj ET';
const objects = [
'1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj',
'2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj',
'3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj',
'4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj',
`5 0 obj << /Length ${stream.length} >> stream\n${stream}\nendstream endobj`,
];
let offset = '%PDF-1.4\n'.length;
const xref = ['0000000000 65535 f '];
for (const object of objects) {
xref.push(`${String(offset).padStart(10, '0')} 00000 n `);
offset += object.length + 1;
}
const pdf = `%PDF-1.4\n${objects.join('\n')}\nxref\n0 ${objects.length + 1}\n${xref.join('\n')}\ntrailer << /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${offset}\n%%EOF`;
return window.btoa(pdf);
}
function pdfEscape(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function dailyReportId(date: string, generatedAt: string): string {
return `daily-report:${date}:${slugifyIdPart(generatedAt)}`;
}
@@ -5971,6 +6265,10 @@ function reportArtifactSources(artifacts: ArtifactEntry[]) {
retention_status: stringValue(artifact.retention_status) || null,
retention_until: stringValue(artifact.retention_until) || null,
last_retention_action: stringValue(artifact.last_retention_action) || null,
cleanup_status: stringValue(artifact.cleanup_status) || null,
deleted_at: stringValue(artifact.deleted_at) || null,
cleanup_reason: stringValue(artifact.cleanup_reason) || null,
cleanup_event_id: stringValue(artifact.cleanup_event_id) || null,
file_size: typeof artifact.file_size === 'number' ? artifact.file_size : null,
source_tool: stringValue(artifact.source_tool) || null,
source_label: stringValue(artifact.source_label) || null,
@@ -6064,6 +6362,10 @@ async function recordDailyReportArtifact(report: DigitalEmployeeDailyReport): Pr
artifactSources: report.artifact_sources ?? [],
filename: report.download_filename || dailyReportFilename(report.date),
textContent: report.text_content,
htmlFilename: report.html_filename || dailyReportHtmlFilename(report.date),
htmlContent: report.html_content || buildDailyReportHtml(report, report.text_content),
pdfFilename: report.pdf_filename || dailyReportPdfFilename(report.date),
pdfContentBase64: buildDailyReportPdfBase64(report, report.text_content),
generatedAt: report.generated_at,
reportScheduleTime: report.report_schedule_time,
source: report.source ?? 'qimingclaw',
@@ -6074,6 +6376,23 @@ async function recordDailyReportArtifact(report: DigitalEmployeeDailyReport): Pr
}
}
async function handleDailyReportDelivery(input: QimingclawDailyReportDeliveryInput): Promise<QimingclawDailyReportDeliveryResult> {
const reportId = stringValue(input.reportId);
if (!reportId) return { eventId: '' };
const recordDelivery = window.QimingClawBridge?.digital?.recordDailyReportDelivery;
const payload: QimingclawDailyReportDeliveryInput = {
reportId,
action: input.action,
status: input.status,
actor: input.actor ?? 'digital_employee_operator',
endpoint: input.endpoint ?? 'management-console',
remoteId: input.remoteId ?? null,
message: input.message ?? null,
};
if (!recordDelivery) return { eventId: '' };
return await recordDelivery(payload) ?? { eventId: '' };
}
async function respondAcpPermissionForDecision(
body: DigitalEmployeeWorkdayActionRequest,
snapshot: QimingclawSnapshot | null,