吸收数字员工日报产物闭环

This commit is contained in:
baiyanyun
2026-06-09 22:16:03 +08:00
parent ba806f779e
commit 0d2008b35c
11 changed files with 588 additions and 180 deletions

View File

@@ -12,6 +12,7 @@ import type {
DebugStoreResponse,
DebugTask,
DiagResult,
DigitalEmployeeDailyReport,
DigitalEmployeeDecision,
DigitalEmployeeDuty,
DigitalEmployeeManagedService,
@@ -61,6 +62,7 @@ declare global {
listRouteDecisions?: (options?: { limit?: number }) => Promise<RouteDecisionRecord[]>;
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise<RouteDecisionRecord>;
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise<QimingclawDailyReportRecordResult | null>;
respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>;
};
};
@@ -209,6 +211,31 @@ interface QimingclawGovernanceCommandRecord {
result?: Record<string, unknown> | null;
}
interface QimingclawDailyReportRecordInput {
reportId?: string | null;
date: string;
title: string;
summary: string;
totals: Record<string, unknown>;
detailLines: string[];
artifactSources?: unknown[];
filename: string;
textContent: string;
generatedAt?: string | null;
reportScheduleTime?: string | null;
source?: string | null;
model?: string | null;
}
interface QimingclawDailyReportRecordResult {
reportId: string;
planId: string;
taskId: string;
runId: string;
artifactId: string;
eventId: string;
}
interface QimingclawRouteDecisionInput {
id?: string | null;
idempotencyKey?: string | null;
@@ -598,6 +625,17 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
return await handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) as T;
}
const dailyReportDetailMatch = pathname.match(/^\/api\/digital-employee\/daily-reports\/([^/]+)$/);
if (method === 'GET' && dailyReportDetailMatch) {
const reportId = decodeURIComponent(dailyReportDetailMatch[1] || '');
const item = dailyReportRows(await readQimingclawSnapshot())
.find((row) => stringValue(row.report_id) === reportId || stringValue(row.artifact_id) === reportId) ?? null;
return { item, source: BUSINESS_VIEW_SOURCE } as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/daily-reports') {
const rows = filterDailyReportRows(dailyReportRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
const businessPlanDetailMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)$/);
if (method === 'GET' && businessPlanDetailMatch) {
const planId = decodeURIComponent(businessPlanDetailMatch[1] || '');
@@ -2228,6 +2266,57 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView
});
}
function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return buildArtifacts(snapshot)
.filter(isDailyReportArtifact)
.map((artifact) => {
const payload = asRecord(artifact.payload) ?? {};
const artifactId = stringValue(artifact.artifact_id) || stringValue(payload.artifact_id) || stringValue(artifact.subject_id);
const reportId = stringValue(payload.report_id) || stringValue(artifact.report_id) || artifactId.replace(/:artifact:text$/, '');
const generatedAt = stringValue(payload.generated_at) || stringValue(artifact.created_at) || new Date().toISOString();
const date = stringValue(payload.date) || generatedAt.slice(0, 10);
return {
report_id: reportId,
artifact_id: artifactId,
event_id: stringValue(payload.event_id) || `${reportId}:event:generated`,
date,
title: stringValue(payload.title) || stringValue(artifact.name) || `${date} 数字员工日报`,
status: stringValue(payload.status) || 'ready',
summary: stringValue(payload.summary) || stringValue(artifact.value) || '',
totals: asRecord(payload.totals) ?? {},
detail_lines: arrayValue(payload.detail_lines),
artifact_sources: arrayValue(payload.artifact_sources),
filename: stringValue(payload.filename) || stringValue(artifact.name) || dailyReportFilename(date),
format: stringValue(payload.format) || 'text',
text_content: stringValue(payload.text_content) || stringValue(artifact.value),
generated_at: generatedAt,
sync_status: stringValue(artifact.sync_status) || null,
entity_type: 'artifact',
entity_id: artifactId,
plan_id: stringValue(artifact.plan_id) || null,
task_id: stringValue(artifact.task_id) || null,
run_id: stringValue(artifact.run_id) || null,
created_at: stringValue(artifact.created_at) || generatedAt,
updated_at: generatedAt,
payload,
};
})
.sort((left, right) => stringValue(right.generated_at).localeCompare(stringValue(left.generated_at)));
}
function isDailyReportArtifact(artifact: ArtifactEntry): boolean {
const payload = asRecord(artifact.payload) ?? {};
return stringValue(artifact.kind) === 'daily_report'
|| stringValue(payload.kind) === 'daily_report'
|| Boolean(stringValue(payload.report_id) && stringValue(payload.text_content));
}
function filterDailyReportRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const date = stringValue(searchParams.get('date'));
return filterBusinessRows(rows, searchParams)
.filter((row) => !date || stringValue(row.date) === date);
}
function filterBusinessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const entityId = stringValue(searchParams.get('entity_id'));
const remoteId = stringValue(searchParams.get('remote_id'));
@@ -2282,6 +2371,7 @@ function businessRowEntityIds(row: BusinessViewRow): string[] {
row.event_id,
row.artifact_id,
row.approval_id,
row.report_id,
]);
}
@@ -3191,6 +3281,7 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
const hasLiveRuntime = hasRuntimeRecords(snapshot);
const sync = snapshot?.sync;
const artifactSources = reportArtifactSources(buildArtifacts(snapshot));
const runDetails = buildRunDetails(events);
const managedServices = buildManagedServices(snapshot);
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
const runningManagedServiceCount = managedServices.filter((service) => service.running).length;
@@ -3260,7 +3351,7 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
duties,
events,
execution_summaries: executionSummaries,
run_details: buildRunDetails(events),
run_details: runDetails,
decisions: buildWorkdayDecisions(state, date, snapshot),
delivery: {
headline: hasLiveRuntime ? 'qimingclaw 本地记录已接入' : confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置',
@@ -3295,34 +3386,117 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
})),
],
},
daily_report: {
date,
title: `${displayDate(date)} 飞天数字员工日报`,
report_schedule_time: state.reportScheduleTime,
status: generatedAt ? 'ready' : 'not_generated',
generated_at: generatedAt,
generated_by: generatedAt ? 'ai' : 'fallback',
model: 'qimingclaw-adapter',
source: 'qimingclaw',
summary: generatedAt
? `今日已完成数字员工页面到 qimingclaw adapter 的接入,并读取了 qimingclaw 服务状态。${syncLine(snapshot)}`
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。',
totals: {
task_count: selectedCount,
execution_count: events.length,
success_count: successCount,
failure_count: failureCount,
running_count: runningCount,
},
task_sections: executionSummaries,
detail_lines: events.map((event) => `${event.time} ${event.task_title}${event.detail}`),
artifact_sources: artifactSources,
pdf_available: Boolean(generatedAt),
pdf_url: null,
},
daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, snapshot }),
};
}
function buildDailyReportProjection(input: {
date: string;
state: AdapterState;
generatedAt: string | null;
selectedCount: number;
events: DigitalEmployeeWorkEvent[];
executionSummaries: DigitalEmployeeTaskExecutionSummary[];
runDetails: DigitalEmployeeRunDetail[];
artifactSources: ReturnType<typeof reportArtifactSources>;
successCount: number;
failureCount: number;
runningCount: number;
snapshot: QimingclawSnapshot | null;
}): DigitalEmployeeDailyReport {
const title = `${displayDate(input.date)} 飞天数字员工日报`;
const summary = input.generatedAt
? `今日已完成数字员工页面到 qimingclaw adapter 的接入,并读取了 qimingclaw 服务状态。${syncLine(input.snapshot)}`
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。';
const totals = {
task_count: input.selectedCount,
execution_count: input.events.length,
success_count: input.successCount,
failure_count: input.failureCount,
running_count: input.runningCount,
};
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 baseReport: DigitalEmployeeDailyReport = {
report_id: reportId,
artifact_id: reportId ? `${reportId}:artifact:text` : null,
event_id: reportId ? `${reportId}:event:generated` : null,
date: input.date,
title,
report_schedule_time: input.state.reportScheduleTime,
status: input.generatedAt ? 'ready' : 'not_generated',
generated_at: input.generatedAt,
generated_by: input.generatedAt ? 'ai' : 'fallback',
model: 'qimingclaw-adapter',
source: 'qimingclaw',
summary,
totals,
task_sections: input.executionSummaries,
detail_lines: detailLines,
artifact_sources: input.artifactSources,
download_filename: downloadFilename,
text_content: null,
pdf_available: Boolean(input.generatedAt),
pdf_url: null,
};
return {
...baseReport,
text_content: input.generatedAt
? buildDailyReportText(baseReport, input.runDetails, input.artifactSources)
: null,
};
}
function buildDailyReportText(
report: DigitalEmployeeDailyReport,
details: DigitalEmployeeRunDetail[],
artifacts: ReturnType<typeof reportArtifactSources>,
): string {
return [
report.title || `${report.date} 数字员工日报`,
`日期:${report.date}`,
`生成时间:${report.generated_at || '暂未生成'}`,
`生成来源:${report.generated_by === 'ai' ? 'AI' : 'qimingclaw adapter'}`,
'',
'## 摘要',
report.summary || '暂无摘要。',
'',
'## 指标',
`任务总数:${report.totals.task_count}`,
`执行次数:${report.totals.execution_count}`,
`成功次数:${report.totals.success_count}`,
`失败次数:${report.totals.failure_count}`,
`运行中:${report.totals.running_count ?? 0}`,
'',
'## 任务汇总',
...(report.task_sections.length > 0
? report.task_sections.map((section) => `- ${section.title}${section.latest_status_label},执行 ${section.total_count} 次,成功 ${section.success_count},失败 ${section.failure_count}${section.ai_summary || section.business_summary || ''}`)
: ['暂无任务汇总。']),
'',
'## 运行明细',
...(details.length > 0
? details.map((detail) => `- ${detail.time || '--:--'} ${detail.task_name} / ${detail.step_name || detail.operation_name || '任务执行'}${detail.status_label}${detail.business_detail}`)
: ['暂无运行明细。']),
'',
'## 产物来源',
...(artifacts.length > 0
? artifacts.map((artifact) => `- ${artifact.name}${artifact.source_label || artifact.source_type || '未知来源'}${artifact.uri ? `${artifact.uri}` : ''}`)
: ['暂无产物来源。']),
'',
'## 详情',
...((report.detail_lines ?? []).length > 0 ? report.detail_lines : ['暂无详情。']),
].join('\n');
}
function dailyReportFilename(date: string): string {
return `qimingclaw-digital-report-${date}.txt`;
}
function dailyReportId(date: string, generatedAt: string): string {
return `daily-report:${date}:${slugifyIdPart(generatedAt)}`;
}
function reportArtifactSources(artifacts: ArtifactEntry[]) {
return artifacts.slice(0, 20).map((artifact) => ({
id: stringValue(artifact.artifact_id) || `${stringValue(artifact.subject_id)}:${stringValue(artifact.uri ?? artifact.name)}`,
@@ -3395,7 +3569,35 @@ async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, sn
}
: undefined,
);
return buildWorkday(date, { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState });
const nextSnapshot = { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState };
const projection = buildWorkday(date, nextSnapshot);
if (body.action === 'generate_daily_report' && projection.daily_report) {
await recordDailyReportArtifact(projection.daily_report);
}
return projection;
}
async function recordDailyReportArtifact(report: DigitalEmployeeDailyReport): Promise<void> {
if (!report.report_id || !report.generated_at || !report.text_content) return;
try {
await window.QimingClawBridge?.digital?.recordDailyReport?.({
reportId: report.report_id,
date: report.date,
title: report.title,
summary: report.summary,
totals: report.totals,
detailLines: report.detail_lines,
artifactSources: report.artifact_sources ?? [],
filename: report.download_filename || dailyReportFilename(report.date),
textContent: report.text_content,
generatedAt: report.generated_at,
reportScheduleTime: report.report_schedule_time,
source: report.source ?? 'qimingclaw',
model: report.model ?? 'qimingclaw-adapter',
});
} catch (error) {
console.warn('[qimingclawAdapter] record daily report failed:', error);
}
}
async function respondAcpPermissionForDecision(

View File

@@ -298,7 +298,10 @@ export default function DailyReport({ focusSection }: { focusSection?: string |
const downloadDailyReport = useCallback(() => {
if (!report || report.status !== 'ready') return;
downloadTextFile(reportFileName(today), buildReportDownloadText(today, report, taskSections, runDetails, artifactSources));
downloadTextFile(
report.download_filename || reportFileName(today),
report.text_content || buildReportDownloadText(today, report, taskSections, runDetails, artifactSources),
);
}, [artifactSources, report, runDetails, taskSections, today]);
return (

View File

@@ -768,6 +768,9 @@ export interface DigitalEmployeeDelivery {
}
export interface DigitalEmployeeDailyReport {
report_id?: string | null;
artifact_id?: string | null;
event_id?: string | null;
date: string;
title: string;
report_schedule_time: string;
@@ -787,6 +790,8 @@ export interface DigitalEmployeeDailyReport {
task_sections: DigitalEmployeeTaskExecutionSummary[];
detail_lines: string[];
artifact_sources?: DigitalEmployeeReportArtifactSource[];
download_filename?: string | null;
text_content?: string | null;
pdf_available: boolean;
pdf_url?: string | null;
}