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

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