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;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -15,8 +15,8 @@
|
||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/index-qn2ia0Ac.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DeYFPN7i.css">
|
||||
<script type="module" crossorigin src="./assets/index-BZWWD4XK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BgRGGB2I.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user