吸收数字员工日报产物闭环
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user