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 { overflow: auto; min-height: 0; }
.de-workday-result-summary { margin: 0; color: #173550; font-size: 13px; line-height: 1.55; } .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-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-card { background: rgba(247,251,255,0.96); }
.de-decision-chain-note { .de-decision-chain-note {
display: flex; align-items: center; flex-wrap: wrap; gap: 8px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;

View File

@@ -1324,7 +1324,14 @@ function buildArtifacts(snapshot: QimingclawSnapshot | null): ArtifactEntry[] {
const artifacts = [ const artifacts = [
...runtimeArtifacts(snapshot).map(artifactFromRuntimeRecord), ...runtimeArtifacts(snapshot).map(artifactFromRuntimeRecord),
...runtimeRuns(snapshot).flatMap((run) => artifactsForRun(run, runtimeEvents(snapshot))), ...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); return dedupeArtifacts(artifacts);
} }
@@ -1343,6 +1350,8 @@ function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): Artifact
run_id: artifact.runId ?? null, run_id: artifact.runId ?? null,
created_at: artifact.createdAt, created_at: artifact.createdAt,
source: 'qimingclaw-runtime', source: 'qimingclaw-runtime',
source_type: 'artifact',
source_label: artifactSourceLabel('artifact', artifact.id),
sync_status: artifact.syncStatus, sync_status: artifact.syncStatus,
sync_error: artifact.syncError, sync_error: artifact.syncError,
payload: artifact.payload, payload: artifact.payload,
@@ -1351,16 +1360,43 @@ function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): Artifact
function artifactsForRun(run: QimingclawRunRecord, events: QimingclawEventRecord[]): ArtifactEntry[] { function artifactsForRun(run: QimingclawRunRecord, events: QimingclawEventRecord[]): ArtifactEntry[] {
return dedupeArtifacts([ 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 ...events
.filter((event) => event.runId === run.id) .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); 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 = [ const explicitArtifacts = [
...arrayValue(record.artifacts), ...arrayValue(record.artifacts),
@@ -1368,16 +1404,22 @@ function artifactsFromPayload(subjectId: string, payload: unknown, createdAt?: s
...arrayValue(record.files), ...arrayValue(record.files),
...arrayValue(record.attachments), ...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']) { 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); 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); const record = asRecord(value);
if (record) { if (record) {
const uri = stringValue(record.uri ?? record.url ?? record.path ?? record.filePath ?? record.file_path ?? record.outputPath ?? record.output_path); 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, value: stringValue(record.value ?? record.summary ?? record.text) || uri,
created_at: (stringValue(record.created_at ?? record.createdAt) || createdAt) ?? undefined, created_at: (stringValue(record.created_at ?? record.createdAt) || createdAt) ?? undefined,
source: 'qimingclaw-runtime', 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, value: text,
created_at: createdAt ?? undefined, created_at: createdAt ?? undefined,
source: 'qimingclaw-runtime', 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[] { function dedupeArtifacts(artifacts: ArtifactEntry[]): ArtifactEntry[] {
const seen = new Set<string>(); const seen = new Set<string>();
return artifacts.filter((artifact) => { 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 liveRecordCount = runtimePlans(snapshot).length + runtimeTasks(snapshot).length;
const hasLiveRuntime = hasRuntimeRecords(snapshot); const hasLiveRuntime = hasRuntimeRecords(snapshot);
const sync = snapshot?.sync; const sync = snapshot?.sync;
const artifactSources = reportArtifactSources(buildArtifacts(snapshot));
return { return {
source: 'live', source: 'live',
@@ -1709,6 +1776,13 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
], ],
artifacts: [ artifacts: [
{ name: '数字员工运行摘要', status: generatedAt ? 'ready' : 'pending' }, { 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: { daily_report: {
@@ -1732,12 +1806,29 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
}, },
task_sections: executionSummaries, task_sections: executionSummaries,
detail_lines: events.map((event) => `${event.time} ${event.task_title}${event.detail}`), detail_lines: events.map((event) => `${event.time} ${event.task_title}${event.detail}`),
artifact_sources: artifactSources,
pdf_available: Boolean(generatedAt), pdf_available: Boolean(generatedAt),
pdf_url: null, 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> { async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): Promise<DigitalEmployeeWorkdayProjection> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const date = body.date || todayInputDate(); const date = body.date || todayInputDate();

View File

@@ -8,6 +8,7 @@ import {
} from '@/lib/api'; } from '@/lib/api';
import type { import type {
DigitalEmployeeDailyReport, DigitalEmployeeDailyReport,
DigitalEmployeeReportArtifactSource,
DigitalEmployeeRunDetail, DigitalEmployeeRunDetail,
DigitalEmployeeTaskExecutionSummary, DigitalEmployeeTaskExecutionSummary,
DigitalEmployeeWorkdayProjection, DigitalEmployeeWorkdayProjection,
@@ -94,6 +95,7 @@ function buildReportDownloadText(
report: DigitalEmployeeDailyReport, report: DigitalEmployeeDailyReport,
sections: DigitalEmployeeTaskExecutionSummary[], sections: DigitalEmployeeTaskExecutionSummary[],
details: DigitalEmployeeRunDetail[], details: DigitalEmployeeRunDetail[],
artifacts: DigitalEmployeeReportArtifactSource[],
): string { ): string {
return [ return [
report.title || `${date} 数字员工日报`, 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}`) ? 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 : ['暂无详情。']), ...((report.detail_lines ?? []).length > 0 ? report.detail_lines : ['暂无详情。']),
].join('\n'); ].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 }) { export default function DailyReport({ focusSection }: { focusSection?: string | null }) {
const today = useMemo(() => todayInputDate(), []); const today = useMemo(() => todayInputDate(), []);
const [projection, setProjection] = useState<DigitalEmployeeWorkdayProjection | null>(null); 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 report = isLiveProjection ? (projection?.daily_report ?? null) : null;
const taskSections = isLiveProjection ? (report?.task_sections ?? projection?.execution_summaries ?? []) : []; const taskSections = isLiveProjection ? (report?.task_sections ?? projection?.execution_summaries ?? []) : [];
const runDetails = isLiveProjection ? (projection?.run_details ?? []) : []; const runDetails = isLiveProjection ? (projection?.run_details ?? []) : [];
const artifactSources = isLiveProjection ? (report?.artifact_sources ?? []) : [];
const sourceNotice = isLiveProjection ? null : (projection?.demo_reason ?? '当前没有真实运行投影,日报区域暂不展示兜底数据。'); const sourceNotice = isLiveProjection ? null : (projection?.demo_reason ?? '当前没有真实运行投影,日报区域暂不展示兜底数据。');
const totals = report?.totals ?? { const totals = report?.totals ?? {
task_count: taskSections.length, task_count: taskSections.length,
@@ -272,8 +298,8 @@ export default function DailyReport({ focusSection }: { focusSection?: string |
const downloadDailyReport = useCallback(() => { const downloadDailyReport = useCallback(() => {
if (!report || report.status !== 'ready') return; if (!report || report.status !== 'ready') return;
downloadTextFile(reportFileName(today), buildReportDownloadText(today, report, taskSections, runDetails)); downloadTextFile(reportFileName(today), buildReportDownloadText(today, report, taskSections, runDetails, artifactSources));
}, [report, runDetails, taskSections, today]); }, [artifactSources, report, runDetails, taskSections, today]);
return ( return (
<div style={{ padding: '14px 18px' }}> <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> <h3 className="de-settings-heading" style={{ marginTop: 0 }}></h3>
<DetailList details={runDetails} /> <DetailList details={runDetails} />
</section> </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}> <section id="report-section-summary" style={panelStyle}>
<h3 className="de-settings-heading" style={{ marginTop: 0 }}></h3> <h3 className="de-settings-heading" style={{ marginTop: 0 }}></h3>
{(report?.detail_lines ?? []).length === 0 ? ( {(report?.detail_lines ?? []).length === 0 ? (

View File

@@ -654,6 +654,23 @@ export interface DigitalEmployeeDecision {
export interface DigitalEmployeeDeliveryArtifact { export interface DigitalEmployeeDeliveryArtifact {
name: string; name: string;
status: 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 { export interface DigitalEmployeeDelivery {
@@ -682,6 +699,7 @@ export interface DigitalEmployeeDailyReport {
}; };
task_sections: DigitalEmployeeTaskExecutionSummary[]; task_sections: DigitalEmployeeTaskExecutionSummary[];
detail_lines: string[]; detail_lines: string[];
artifact_sources?: DigitalEmployeeReportArtifactSource[];
pdf_available: boolean; pdf_available: boolean;
pdf_url?: string | null; 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

View File

@@ -15,8 +15,8 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error); console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
} }
</script> </script>
<script type="module" crossorigin src="./assets/index-qn2ia0Ac.js"></script> <script type="module" crossorigin src="./assets/index-BZWWD4XK.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DeYFPN7i.css"> <link rel="stylesheet" crossorigin href="./assets/index-BgRGGB2I.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -996,14 +996,23 @@ Plan status
- 风险提示 - 风险提示
- 可下载报告 - 可下载报告
当前客户端实现:
- adapter 从 `digital_artifacts`、Run payload、Event payload 中抽取 artifact。
- artifact 会携带 `source_type``source_label``source_event_id``plan_id``task_id``run_id`
- 日报数据新增 `artifact_sources`,页面展示产物名称、来源和 URI。
- 下载的本地日报文本包含“产物来源”段落。
### 产出 ### 产出
- qimingclaw 的执行结果可沉淀、可回看、可汇报。 - qimingclaw 的执行结果可沉淀、可回看、可汇报。
- 日报可以追溯到具体 Run / Event / Artifact 来源。
### 验收 ### 验收
- 完成任务后能在日报区域看到执行摘要。 - 完成任务后能在日报区域看到执行摘要。
- Artifact 可从任务详情进入查看。 - Artifact 可从任务详情进入查看。
- 日报区域能看到产物来源和 URI。
## Phase 9ManagedService / 常驻能力治理 ## Phase 9ManagedService / 常驻能力治理