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>
|
||||
|
||||
@@ -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 的执行结果可沉淀、可回看、可汇报。
|
||||
- 日报可以追溯到具体 Run / Event / Artifact 来源。
|
||||
|
||||
### 验收
|
||||
|
||||
- 完成任务后能在日报区域看到执行摘要。
|
||||
- Artifact 可从任务详情进入查看。
|
||||
- 日报区域能看到产物来源和 URI。
|
||||
|
||||
## Phase 9:ManagedService / 常驻能力治理
|
||||
|
||||
|
||||
Reference in New Issue
Block a user