feat(client): show digital artifact sources

This commit is contained in:
baiyanyun
2026-06-08 10:31:36 +08:00
parent f4af77cb2b
commit 700f6a40b8
9 changed files with 294 additions and 141 deletions

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 ? (

View File

@@ -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;
}