feat(client): show digital artifact sources
This commit is contained in:
@@ -1974,6 +1974,11 @@
|
||||
.de-workday-result { overflow: auto; min-height: 0; }
|
||||
.de-workday-result-summary { margin: 0; color: #173550; font-size: 13px; line-height: 1.55; }
|
||||
.de-artifact-pills { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.de-artifact-source-list { display: grid; gap: 8px; max-height: 320px; overflow-y: auto; padding-right: 4px; }
|
||||
.de-artifact-source-row { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(88px, 140px); gap: 4px 10px; align-items: center; padding: 9px 10px; border-radius: 8px; border: 1px solid rgba(79,172,254,0.12); background: rgba(244,250,255,0.62); }
|
||||
.de-artifact-source-row strong { min-width: 0; color: var(--de-text-primary); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.de-artifact-source-row span { color: var(--de-text-muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
|
||||
.de-artifact-source-row code, .de-artifact-source-row em { grid-column: 1 / -1; min-width: 0; color: var(--de-text-body); font-size: 12px; font-style: normal; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.de-decision-card { background: rgba(247,251,255,0.96); }
|
||||
.de-decision-chain-note {
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
|
||||
|
||||
@@ -1324,7 +1324,14 @@ function buildArtifacts(snapshot: QimingclawSnapshot | null): ArtifactEntry[] {
|
||||
const artifacts = [
|
||||
...runtimeArtifacts(snapshot).map(artifactFromRuntimeRecord),
|
||||
...runtimeRuns(snapshot).flatMap((run) => artifactsForRun(run, runtimeEvents(snapshot))),
|
||||
...runtimeEvents(snapshot).flatMap((event) => artifactsFromPayload(event.id, event.payload, event.occurredAt)),
|
||||
...runtimeEvents(snapshot).flatMap((event) => artifactsFromPayload(event.id, event.payload, event.occurredAt, {
|
||||
sourceType: 'event',
|
||||
sourceEventId: event.id,
|
||||
sourceLabel: `Event ${event.kind}`,
|
||||
planId: event.planId,
|
||||
taskId: event.taskId,
|
||||
runId: event.runId,
|
||||
})),
|
||||
];
|
||||
return dedupeArtifacts(artifacts);
|
||||
}
|
||||
@@ -1343,6 +1350,8 @@ function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): Artifact
|
||||
run_id: artifact.runId ?? null,
|
||||
created_at: artifact.createdAt,
|
||||
source: 'qimingclaw-runtime',
|
||||
source_type: 'artifact',
|
||||
source_label: artifactSourceLabel('artifact', artifact.id),
|
||||
sync_status: artifact.syncStatus,
|
||||
sync_error: artifact.syncError,
|
||||
payload: artifact.payload,
|
||||
@@ -1351,16 +1360,43 @@ function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): Artifact
|
||||
|
||||
function artifactsForRun(run: QimingclawRunRecord, events: QimingclawEventRecord[]): ArtifactEntry[] {
|
||||
return dedupeArtifacts([
|
||||
...artifactsFromPayload(run.id, run.payload, run.finishedAt ?? run.startedAt ?? run.createdAt),
|
||||
...artifactsFromPayload(run.id, run.payload, run.finishedAt ?? run.startedAt ?? run.createdAt, {
|
||||
sourceType: 'run',
|
||||
sourceLabel: artifactSourceLabel('run', run.id),
|
||||
planId: run.planId,
|
||||
taskId: run.taskId,
|
||||
runId: run.id,
|
||||
}),
|
||||
...events
|
||||
.filter((event) => event.runId === run.id)
|
||||
.flatMap((event) => artifactsFromPayload(run.id, event.payload, event.occurredAt)),
|
||||
.flatMap((event) => artifactsFromPayload(run.id, event.payload, event.occurredAt, {
|
||||
sourceType: 'event',
|
||||
sourceEventId: event.id,
|
||||
sourceLabel: `Event ${event.kind}`,
|
||||
planId: event.planId,
|
||||
taskId: event.taskId,
|
||||
runId: event.runId,
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
||||
function artifactsFromPayload(subjectId: string, payload: unknown, createdAt?: string | null): ArtifactEntry[] {
|
||||
interface ArtifactSourceContext {
|
||||
sourceType: string;
|
||||
sourceLabel: string;
|
||||
sourceEventId?: string | null;
|
||||
planId?: string | null;
|
||||
taskId?: string | null;
|
||||
runId?: string | null;
|
||||
}
|
||||
|
||||
function artifactsFromPayload(
|
||||
subjectId: string,
|
||||
payload: unknown,
|
||||
createdAt?: string | null,
|
||||
sourceContext?: ArtifactSourceContext,
|
||||
): ArtifactEntry[] {
|
||||
const record = asRecord(payload);
|
||||
if (!record) return typeof payload === 'string' ? [artifactFromValue(subjectId, payload, 0, createdAt)] : [];
|
||||
if (!record) return typeof payload === 'string' ? [artifactFromValue(subjectId, payload, 0, createdAt, sourceContext)] : [];
|
||||
|
||||
const explicitArtifacts = [
|
||||
...arrayValue(record.artifacts),
|
||||
@@ -1368,16 +1404,22 @@ function artifactsFromPayload(subjectId: string, payload: unknown, createdAt?: s
|
||||
...arrayValue(record.files),
|
||||
...arrayValue(record.attachments),
|
||||
];
|
||||
const artifacts = explicitArtifacts.map((item, index) => artifactFromValue(subjectId, item, index, createdAt));
|
||||
const artifacts = explicitArtifacts.map((item, index) => artifactFromValue(subjectId, item, index, createdAt, sourceContext));
|
||||
|
||||
for (const key of ['artifact', 'outputPath', 'output_path', 'filePath', 'file_path', 'path', 'uri', 'url']) {
|
||||
if (record[key]) artifacts.push(artifactFromValue(subjectId, record[key], artifacts.length, createdAt));
|
||||
if (record[key]) artifacts.push(artifactFromValue(subjectId, record[key], artifacts.length, createdAt, sourceContext));
|
||||
}
|
||||
|
||||
return artifacts.filter((artifact) => artifact.name || artifact.uri || artifact.value);
|
||||
}
|
||||
|
||||
function artifactFromValue(subjectId: string, value: unknown, index: number, createdAt?: string | null): ArtifactEntry {
|
||||
function artifactFromValue(
|
||||
subjectId: string,
|
||||
value: unknown,
|
||||
index: number,
|
||||
createdAt?: string | null,
|
||||
sourceContext?: ArtifactSourceContext,
|
||||
): ArtifactEntry {
|
||||
const record = asRecord(value);
|
||||
if (record) {
|
||||
const uri = stringValue(record.uri ?? record.url ?? record.path ?? record.filePath ?? record.file_path ?? record.outputPath ?? record.output_path);
|
||||
@@ -1391,6 +1433,12 @@ function artifactFromValue(subjectId: string, value: unknown, index: number, cre
|
||||
value: stringValue(record.value ?? record.summary ?? record.text) || uri,
|
||||
created_at: (stringValue(record.created_at ?? record.createdAt) || createdAt) ?? undefined,
|
||||
source: 'qimingclaw-runtime',
|
||||
source_type: stringValue(record.source_type) || sourceContext?.sourceType || 'payload',
|
||||
source_event_id: stringValue(record.source_event_id) || sourceContext?.sourceEventId || null,
|
||||
source_label: stringValue(record.source_label) || sourceContext?.sourceLabel || artifactSourceLabel('payload', subjectId),
|
||||
plan_id: stringValue(record.plan_id) || sourceContext?.planId || null,
|
||||
task_id: stringValue(record.task_id) || sourceContext?.taskId || null,
|
||||
run_id: stringValue(record.run_id) || sourceContext?.runId || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1404,9 +1452,27 @@ function artifactFromValue(subjectId: string, value: unknown, index: number, cre
|
||||
value: text,
|
||||
created_at: createdAt ?? undefined,
|
||||
source: 'qimingclaw-runtime',
|
||||
source_type: sourceContext?.sourceType || 'payload',
|
||||
source_event_id: sourceContext?.sourceEventId || null,
|
||||
source_label: sourceContext?.sourceLabel || artifactSourceLabel('payload', subjectId),
|
||||
plan_id: sourceContext?.planId || null,
|
||||
task_id: sourceContext?.taskId || null,
|
||||
run_id: sourceContext?.runId || null,
|
||||
};
|
||||
}
|
||||
|
||||
function artifactSourceLabel(sourceType: string, sourceId?: string | null): string {
|
||||
const suffix = sourceId ? ` ${sourceId}` : '';
|
||||
switch (sourceType) {
|
||||
case 'artifact': return `Artifact${suffix}`;
|
||||
case 'event': return `Event${suffix}`;
|
||||
case 'run': return `Run${suffix}`;
|
||||
case 'task': return `Task${suffix}`;
|
||||
case 'plan': return `Plan${suffix}`;
|
||||
default: return `Payload${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
function dedupeArtifacts(artifacts: ArtifactEntry[]): ArtifactEntry[] {
|
||||
const seen = new Set<string>();
|
||||
return artifacts.filter((artifact) => {
|
||||
@@ -1636,6 +1702,7 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
||||
const liveRecordCount = runtimePlans(snapshot).length + runtimeTasks(snapshot).length;
|
||||
const hasLiveRuntime = hasRuntimeRecords(snapshot);
|
||||
const sync = snapshot?.sync;
|
||||
const artifactSources = reportArtifactSources(buildArtifacts(snapshot));
|
||||
|
||||
return {
|
||||
source: 'live',
|
||||
@@ -1709,6 +1776,13 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
||||
],
|
||||
artifacts: [
|
||||
{ name: '数字员工运行摘要', status: generatedAt ? 'ready' : 'pending' },
|
||||
...artifactSources.slice(0, 4).map((artifact) => ({
|
||||
name: artifact.name,
|
||||
status: generatedAt ? 'ready' : 'available',
|
||||
uri: artifact.uri ?? null,
|
||||
source_label: artifact.source_label ?? null,
|
||||
source_type: artifact.source_type ?? null,
|
||||
})),
|
||||
],
|
||||
},
|
||||
daily_report: {
|
||||
@@ -1732,12 +1806,29 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
||||
},
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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)}`,
|
||||
name: stringValue(artifact.name) || stringValue(artifact.uri) || stringValue(artifact.value) || '未命名产物',
|
||||
kind: stringValue(artifact.kind) || null,
|
||||
uri: stringValue(artifact.uri) || null,
|
||||
source_label: stringValue(artifact.source_label) || null,
|
||||
source_type: stringValue(artifact.source_type) || null,
|
||||
source_event_id: stringValue(artifact.source_event_id) || null,
|
||||
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) || null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): Promise<DigitalEmployeeWorkdayProjection> {
|
||||
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
|
||||
const date = body.date || todayInputDate();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/lib/api';
|
||||
import type {
|
||||
DigitalEmployeeDailyReport,
|
||||
DigitalEmployeeReportArtifactSource,
|
||||
DigitalEmployeeRunDetail,
|
||||
DigitalEmployeeTaskExecutionSummary,
|
||||
DigitalEmployeeWorkdayProjection,
|
||||
@@ -94,6 +95,7 @@ function buildReportDownloadText(
|
||||
report: DigitalEmployeeDailyReport,
|
||||
sections: DigitalEmployeeTaskExecutionSummary[],
|
||||
details: DigitalEmployeeRunDetail[],
|
||||
artifacts: DigitalEmployeeReportArtifactSource[],
|
||||
): string {
|
||||
return [
|
||||
report.title || `${date} 数字员工日报`,
|
||||
@@ -121,6 +123,11 @@ function buildReportDownloadText(
|
||||
? 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');
|
||||
@@ -173,6 +180,24 @@ function DetailList({ details }: { details: DigitalEmployeeRunDetail[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArtifactSource[] }) {
|
||||
if (artifacts.length === 0) {
|
||||
return <p className="de-empty-text" style={{ padding: '18px 0 6px' }}>运行产物出现后,这里会展示文件、输出和来源记录。</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="de-artifact-source-list" aria-label="日报产物来源">
|
||||
{artifacts.map((artifact) => (
|
||||
<div key={artifact.id} className="de-artifact-source-row">
|
||||
<strong>{artifact.name}</strong>
|
||||
<span>{artifact.source_label || artifact.source_type || '未知来源'}</span>
|
||||
{artifact.uri ? <code>{artifact.uri}</code> : <em>{artifact.kind || 'artifact'}</em>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DailyReport({ focusSection }: { focusSection?: string | null }) {
|
||||
const today = useMemo(() => todayInputDate(), []);
|
||||
const [projection, setProjection] = useState<DigitalEmployeeWorkdayProjection | null>(null);
|
||||
@@ -186,6 +211,7 @@ export default function DailyReport({ focusSection }: { focusSection?: string |
|
||||
const report = isLiveProjection ? (projection?.daily_report ?? null) : null;
|
||||
const taskSections = isLiveProjection ? (report?.task_sections ?? projection?.execution_summaries ?? []) : [];
|
||||
const runDetails = isLiveProjection ? (projection?.run_details ?? []) : [];
|
||||
const artifactSources = isLiveProjection ? (report?.artifact_sources ?? []) : [];
|
||||
const sourceNotice = isLiveProjection ? null : (projection?.demo_reason ?? '当前没有真实运行投影,日报区域暂不展示兜底数据。');
|
||||
const totals = report?.totals ?? {
|
||||
task_count: taskSections.length,
|
||||
@@ -272,8 +298,8 @@ export default function DailyReport({ focusSection }: { focusSection?: string |
|
||||
|
||||
const downloadDailyReport = useCallback(() => {
|
||||
if (!report || report.status !== 'ready') return;
|
||||
downloadTextFile(reportFileName(today), buildReportDownloadText(today, report, taskSections, runDetails));
|
||||
}, [report, runDetails, taskSections, today]);
|
||||
downloadTextFile(reportFileName(today), buildReportDownloadText(today, report, taskSections, runDetails, artifactSources));
|
||||
}, [artifactSources, report, runDetails, taskSections, today]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '14px 18px' }}>
|
||||
@@ -355,6 +381,10 @@ export default function DailyReport({ focusSection }: { focusSection?: string |
|
||||
<h3 className="de-settings-heading" style={{ marginTop: 0 }}>运行明细</h3>
|
||||
<DetailList details={runDetails} />
|
||||
</section>
|
||||
<section id="report-section-artifacts" style={panelStyle}>
|
||||
<h3 className="de-settings-heading" style={{ marginTop: 0 }}>产物来源</h3>
|
||||
<ArtifactSourceList artifacts={artifactSources} />
|
||||
</section>
|
||||
<section id="report-section-summary" style={panelStyle}>
|
||||
<h3 className="de-settings-heading" style={{ marginTop: 0 }}>日报内容</h3>
|
||||
{(report?.detail_lines ?? []).length === 0 ? (
|
||||
|
||||
@@ -654,6 +654,23 @@ export interface DigitalEmployeeDecision {
|
||||
export interface DigitalEmployeeDeliveryArtifact {
|
||||
name: string;
|
||||
status: string;
|
||||
uri?: string | null;
|
||||
source_label?: string | null;
|
||||
source_type?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeReportArtifactSource {
|
||||
id: string;
|
||||
name: string;
|
||||
kind?: string | null;
|
||||
uri?: string | null;
|
||||
source_label?: string | null;
|
||||
source_type?: string | null;
|
||||
source_event_id?: string | null;
|
||||
plan_id?: string | null;
|
||||
task_id?: string | null;
|
||||
run_id?: string | null;
|
||||
created_at?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeDelivery {
|
||||
@@ -682,6 +699,7 @@ export interface DigitalEmployeeDailyReport {
|
||||
};
|
||||
task_sections: DigitalEmployeeTaskExecutionSummary[];
|
||||
detail_lines: string[];
|
||||
artifact_sources?: DigitalEmployeeReportArtifactSource[];
|
||||
pdf_available: boolean;
|
||||
pdf_url?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user