吸收数字员工产物生命周期治理
This commit is contained in:
@@ -2176,6 +2176,8 @@
|
||||
.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-artifact-source-actions { display: inline-flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
||||
.de-artifact-source-meta { grid-column: 1 / -1; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; }
|
||||
.de-artifact-source-meta span { width: auto; max-width: 160px; padding: 2px 6px; border-radius: 6px; background: rgba(79,172,254,0.08); color: #315a78; text-align: left; }
|
||||
.de-icon-btn { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid rgba(79,172,254,0.18); border-radius: 6px; color: #275a82; background: rgba(255,255,255,0.78); cursor: pointer; }
|
||||
.de-icon-btn:hover:not(:disabled) { border-color: rgba(79,172,254,0.45); background: #fff; }
|
||||
.de-icon-btn:disabled { cursor: not-allowed; opacity: 0.42; }
|
||||
@@ -2183,6 +2185,12 @@
|
||||
.de-artifact-preview { margin-top: 10px; border: 1px solid rgba(79,172,254,0.14); border-radius: 8px; background: rgba(255,255,255,0.78); overflow: hidden; }
|
||||
.de-artifact-preview > div { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 10px; border-bottom: 1px solid rgba(79,172,254,0.1); }
|
||||
.de-artifact-preview pre { max-height: 220px; margin: 0; padding: 10px; overflow: auto; white-space: pre-wrap; word-break: break-word; color: var(--de-text-body); font-size: 12px; line-height: 1.55; }
|
||||
.de-artifact-retention-actions { justify-content: flex-start !important; flex-wrap: wrap; }
|
||||
.de-artifact-version-list { display: grid !important; gap: 6px; align-items: stretch !important; justify-content: stretch !important; border-bottom: 0 !important; }
|
||||
.de-artifact-version-list div { display: grid; grid-template-columns: minmax(72px, 120px) minmax(90px, 1fr); gap: 2px 10px; align-items: center; padding: 8px; border-radius: 6px; background: rgba(244,250,255,0.78); }
|
||||
.de-artifact-version-list strong { font-size: 12px; color: var(--de-text-primary); }
|
||||
.de-artifact-version-list span, .de-artifact-version-list em { min-width: 0; color: var(--de-text-muted); font-size: 12px; font-style: normal; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.de-artifact-version-list em { grid-column: 1 / -1; }
|
||||
.de-decision-card { background: rgba(247,251,255,0.96); }
|
||||
.de-decision-chain-note {
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
|
||||
|
||||
@@ -739,6 +739,27 @@ export function getArtifactAccessEvents(params: Record<string, string | number |
|
||||
return apiFetch(`/api/digital-employee/artifact-access-events${suffix}`);
|
||||
}
|
||||
|
||||
export function getArtifactDetail(artifactId: string): Promise<any> {
|
||||
return apiFetch(`/api/digital-employee/artifacts/${encodeURIComponent(artifactId)}`)
|
||||
.then((data: any) => data.item ?? null);
|
||||
}
|
||||
|
||||
export function getArtifactVersions(artifactId: string): Promise<any[]> {
|
||||
return apiFetch(`/api/digital-employee/artifacts/${encodeURIComponent(artifactId)}/versions`)
|
||||
.then((data: any) => data.items ?? []);
|
||||
}
|
||||
|
||||
export function setArtifactRetention(
|
||||
artifactId: string,
|
||||
action: 'mark_keep' | 'mark_expire' | 'mark_review' | 'clear_retention',
|
||||
options: Record<string, string | null | undefined> = {},
|
||||
): Promise<any> {
|
||||
return apiFetch(`/api/digital-employee/artifacts/${encodeURIComponent(artifactId)}/retention`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action, ...options }),
|
||||
});
|
||||
}
|
||||
|
||||
export function getDebugConversations(): Promise<any[]> {
|
||||
return apiFetch<{ conversations: any[] }>('/api/debug/conversations')
|
||||
.then((data) => data.conversations ?? []);
|
||||
|
||||
@@ -64,6 +64,7 @@ declare global {
|
||||
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
|
||||
recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise<QimingclawDailyReportRecordResult | null>;
|
||||
accessArtifact?: (artifactId: string, action: 'preview' | 'download' | 'open_location', options?: { actor?: string | null; source?: string | null }) => Promise<QimingclawArtifactAccessResponse>;
|
||||
recordArtifactLifecycle?: (input: QimingclawArtifactLifecycleInput) => Promise<QimingclawArtifactLifecycleResult | null>;
|
||||
restartManagedService?: (serviceId: string, options?: { reason?: string | null; actor?: string | null }) => Promise<QimingclawManagedServiceActionResponse>;
|
||||
respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>;
|
||||
};
|
||||
@@ -249,6 +250,27 @@ interface QimingclawArtifactAccessResponse {
|
||||
event_id?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawArtifactLifecycleInput {
|
||||
artifactId?: string | null;
|
||||
action: string;
|
||||
reason?: string | null;
|
||||
retentionUntil?: string | null;
|
||||
versionSummary?: Record<string, unknown> | null;
|
||||
retentionSummary?: Record<string, unknown> | null;
|
||||
deliverySummary?: Record<string, unknown> | null;
|
||||
actor?: string | null;
|
||||
source?: string | null;
|
||||
planId?: string | null;
|
||||
taskId?: string | null;
|
||||
runId?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawArtifactLifecycleResult {
|
||||
eventId: string;
|
||||
kind: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface QimingclawManagedServiceActionResponse {
|
||||
ok: boolean;
|
||||
service_id: string;
|
||||
@@ -680,6 +702,24 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
const artifactId = decodeURIComponent(artifactAccessMatch[1] || '');
|
||||
return await handleArtifactAccess(artifactId, parseOptionalJsonBody(options.body)) as T;
|
||||
}
|
||||
const artifactRetentionMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/retention$/);
|
||||
if (method === 'POST' && artifactRetentionMatch) {
|
||||
const artifactId = decodeURIComponent(artifactRetentionMatch[1] || '');
|
||||
return await handleArtifactRetention(artifactId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
const artifactVersionsMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/versions$/);
|
||||
if (method === 'GET' && artifactVersionsMatch) {
|
||||
const artifactId = decodeURIComponent(artifactVersionsMatch[1] || '');
|
||||
const snapshot = await readQimingclawSnapshot();
|
||||
return { items: artifactVersionRows(snapshot, artifactId), source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
const artifactDetailMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)$/);
|
||||
if (method === 'GET' && artifactDetailMatch) {
|
||||
const artifactId = decodeURIComponent(artifactDetailMatch[1] || '');
|
||||
const snapshot = await readQimingclawSnapshot();
|
||||
const item = artifactDetailRow(snapshot, artifactId);
|
||||
return { item, source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
const businessPlanDetailMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)$/);
|
||||
if (method === 'GET' && businessPlanDetailMatch) {
|
||||
const planId = decodeURIComponent(businessPlanDetailMatch[1] || '');
|
||||
@@ -1410,6 +1450,41 @@ function normalizeArtifactAccessAction(value: string): 'preview' | 'download' |
|
||||
return 'preview';
|
||||
}
|
||||
|
||||
async function handleArtifactRetention(
|
||||
artifactId: string,
|
||||
body: Record<string, unknown>,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
const action = stringValue(body.action) || 'mark_review';
|
||||
const artifact = artifactDetailRow(snapshot, artifactId);
|
||||
if (!bridge?.recordArtifactLifecycle) {
|
||||
return { ok: false, artifact_id: artifactId, action, status: 'rejected', error: 'bridge_unavailable' };
|
||||
}
|
||||
const result = await bridge.recordArtifactLifecycle({
|
||||
artifactId,
|
||||
action,
|
||||
reason: stringValue(body.reason) || null,
|
||||
retentionUntil: stringValue(body.retention_until ?? body.retentionUntil) || null,
|
||||
versionSummary: artifact ? artifactVersionSummary(artifact) : null,
|
||||
retentionSummary: artifact ? artifactRetentionSummaryForAction(artifact, action, body) : null,
|
||||
deliverySummary: artifact ? artifactDeliverySummary(artifact) : null,
|
||||
actor: stringValue(body.actor) || 'digital_employee_ui',
|
||||
source: stringValue(body.source) || 'sgrobot-digital-adapter',
|
||||
planId: artifact ? stringValue(artifact.plan_id) || null : null,
|
||||
taskId: artifact ? stringValue(artifact.task_id) || null : null,
|
||||
runId: artifact ? stringValue(artifact.run_id) || null : null,
|
||||
});
|
||||
return {
|
||||
ok: Boolean(result && result.kind !== 'artifact_retention_rejected'),
|
||||
artifact_id: artifactId,
|
||||
action,
|
||||
status: result?.kind === 'artifact_retention_rejected' ? 'rejected' : 'recorded',
|
||||
event_id: result?.eventId ?? null,
|
||||
item: artifact,
|
||||
};
|
||||
}
|
||||
|
||||
function syncLine(snapshot: QimingclawSnapshot | null): string {
|
||||
const sync = snapshot?.sync;
|
||||
if (!sync) return '管理端同步:未连接';
|
||||
@@ -2343,7 +2418,8 @@ function businessEventRows(snapshot: QimingclawSnapshot | null): BusinessViewRow
|
||||
|
||||
function businessArtifactRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
const runtimeById = new Map(runtimeArtifacts(snapshot).map((artifact) => [artifact.id, artifact]));
|
||||
return buildArtifacts(snapshot).map((artifact) => {
|
||||
const artifacts = buildArtifacts(snapshot);
|
||||
return artifacts.map((artifact) => {
|
||||
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
|
||||
const runtime = runtimeById.get(artifactId);
|
||||
const entity = businessEntityFromRefs({
|
||||
@@ -2353,6 +2429,9 @@ function businessArtifactRows(snapshot: QimingclawSnapshot | null): BusinessView
|
||||
fallback_type: 'artifact',
|
||||
fallback_id: artifact.subject_id ?? artifactId,
|
||||
});
|
||||
const lifecycle = artifactLifecycleSummary(snapshot, artifactId);
|
||||
const version = artifactVersionMeta(artifacts, artifact);
|
||||
const delivery = artifactDeliverySummary(artifact);
|
||||
return {
|
||||
...artifact,
|
||||
artifact_id: artifactId,
|
||||
@@ -2360,10 +2439,53 @@ function businessArtifactRows(snapshot: QimingclawSnapshot | null): BusinessView
|
||||
entity_type: entity.entity_type,
|
||||
entity_id: entity.entity_id,
|
||||
sync_status: (runtime?.syncStatus ?? stringValue(artifact.sync_status)) || null,
|
||||
version: version.version,
|
||||
version_key: version.version_key,
|
||||
version_rank: version.version_rank,
|
||||
retention_status: lifecycle.retention_status,
|
||||
retention_until: lifecycle.retention_until,
|
||||
last_retention_action: lifecycle.last_retention_action,
|
||||
last_access_status: stringValue(asRecord(artifact.access)?.last_access_status) || null,
|
||||
delivery_summary: delivery,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function artifactDetailRow(snapshot: QimingclawSnapshot | null, artifactId: string): BusinessViewRow | null {
|
||||
const row = businessArtifactRows(snapshot)
|
||||
.find((artifact) => stringValue(artifact.artifact_id) === artifactId || stringValue(artifact.remote_id) === artifactId) ?? null;
|
||||
if (!row) return null;
|
||||
return {
|
||||
...row,
|
||||
versions: artifactVersionRows(snapshot, stringValue(row.artifact_id)),
|
||||
lifecycle_events: artifactLifecycleRows(snapshot).filter((event) => stringValue(event.artifact_id) === stringValue(row.artifact_id)).slice(0, 20),
|
||||
};
|
||||
}
|
||||
|
||||
function artifactVersionRows(snapshot: QimingclawSnapshot | null, artifactId: string): BusinessViewRow[] {
|
||||
const artifacts = buildArtifacts(snapshot);
|
||||
const target = artifacts.find((artifact) => stringValue(artifact.artifact_id) === artifactId || stringValue(artifact.subject_id) === artifactId);
|
||||
if (!target) return [];
|
||||
const groupKey = artifactVersionGroupKey(target);
|
||||
return artifacts
|
||||
.filter((artifact) => artifactVersionGroupKey(artifact) === groupKey)
|
||||
.sort((left, right) => stringValue(left.created_at).localeCompare(stringValue(right.created_at)))
|
||||
.map((artifact, index) => {
|
||||
const meta = artifactVersionMeta(artifacts, artifact);
|
||||
return {
|
||||
artifact_id: stringValue(artifact.artifact_id) || stringValue(artifact.subject_id),
|
||||
name: stringValue(artifact.name) || null,
|
||||
version: meta.version || `local-v${index + 1}`,
|
||||
version_key: meta.version_key,
|
||||
version_rank: index + 1,
|
||||
delivery_summary: artifactDeliverySummary(artifact),
|
||||
retention_status: artifactLifecycleSummary(snapshot, stringValue(artifact.artifact_id)).retention_status,
|
||||
created_at: stringValue(artifact.created_at) || null,
|
||||
source_label: stringValue(artifact.source_label) || null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
const runtimeById = new Map(runtimeApprovals(snapshot).map((approval) => [approval.id, approval]));
|
||||
return buildApprovals(snapshot).map((approval) => {
|
||||
@@ -2464,6 +2586,117 @@ function filterArtifactAccessRows(rows: BusinessViewRow[], searchParams: URLSear
|
||||
.filter((row) => !status || stringValue(row.status) === status);
|
||||
}
|
||||
|
||||
function artifactLifecycleRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return runtimeEvents(snapshot)
|
||||
.filter((event) => ['artifact_retention_marked', 'artifact_retention_rejected', 'artifact_version_observed'].includes(event.kind))
|
||||
.map((event) => {
|
||||
const payload = asRecord(event.payload) ?? {};
|
||||
return {
|
||||
lifecycle_id: event.id,
|
||||
event_id: event.id,
|
||||
kind: event.kind,
|
||||
artifact_id: stringValue(payload.artifact_id) || null,
|
||||
action: stringValue(payload.action) || null,
|
||||
status: stringValue(payload.status) || null,
|
||||
retention_status: stringValue(payload.retention_status) || null,
|
||||
retention_until: stringValue(payload.retention_until) || null,
|
||||
reason: stringValue(payload.reason) || null,
|
||||
actor: stringValue(payload.actor) || null,
|
||||
source: stringValue(payload.source) || null,
|
||||
plan_id: (event.planId ?? stringValue(payload.plan_id)) || null,
|
||||
task_id: (event.taskId ?? stringValue(payload.task_id)) || null,
|
||||
run_id: (event.runId ?? stringValue(payload.run_id)) || null,
|
||||
message: event.message,
|
||||
occurred_at: event.occurredAt,
|
||||
sync_status: event.syncStatus ?? null,
|
||||
payload,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
|
||||
}
|
||||
|
||||
function artifactLifecycleSummary(snapshot: QimingclawSnapshot | null, artifactId: string): Record<string, unknown> {
|
||||
const latest = artifactLifecycleRows(snapshot)
|
||||
.find((event) => stringValue(event.artifact_id) === artifactId && stringValue(event.kind) === 'artifact_retention_marked');
|
||||
return {
|
||||
retention_status: stringValue(latest?.retention_status) || 'unmanaged',
|
||||
retention_until: stringValue(latest?.retention_until) || null,
|
||||
last_retention_action: stringValue(latest?.action) || null,
|
||||
last_retention_at: stringValue(latest?.occurred_at) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function artifactRetentionSummaryForAction(
|
||||
artifact: BusinessViewRow,
|
||||
action: string,
|
||||
body: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
previous_status: stringValue(artifact.retention_status) || 'unmanaged',
|
||||
retention_status: retentionStatusForAction(action),
|
||||
retention_until: stringValue(body.retention_until ?? body.retentionUntil) || null,
|
||||
reason: stringValue(body.reason) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function retentionStatusForAction(action: string): string | null {
|
||||
if (action === 'mark_keep') return 'keep';
|
||||
if (action === 'mark_expire') return 'expire_candidate';
|
||||
if (action === 'mark_review') return 'review_required';
|
||||
if (action === 'clear_retention') return 'unmanaged';
|
||||
return null;
|
||||
}
|
||||
|
||||
function artifactVersionSummary(artifact: BusinessViewRow): Record<string, unknown> {
|
||||
return {
|
||||
version: stringValue(artifact.version) || null,
|
||||
version_key: stringValue(artifact.version_key) || null,
|
||||
version_rank: numberValue(artifact.version_rank),
|
||||
file_id: stringValue(artifact.file_id) || null,
|
||||
workspace_id: stringValue(artifact.workspace_id) || null,
|
||||
project_id: stringValue(artifact.project_id) || null,
|
||||
created_at: stringValue(artifact.created_at) || null,
|
||||
name: stringValue(artifact.name) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function artifactDeliverySummary(artifact: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
delivery_status: stringValue(artifact.delivery_status) || null,
|
||||
delivery_stage: stringValue(artifact.delivery_stage) || null,
|
||||
workspace_id: stringValue(artifact.workspace_id) || null,
|
||||
project_id: stringValue(artifact.project_id) || null,
|
||||
file_id: stringValue(artifact.file_id) || null,
|
||||
version: stringValue(artifact.version) || null,
|
||||
file_size: numberValue(artifact.file_size),
|
||||
source_tool: stringValue(artifact.source_tool) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function artifactVersionMeta(artifacts: ArtifactEntry[], artifact: Record<string, unknown>): Record<string, unknown> {
|
||||
const groupKey = artifactVersionGroupKey(artifact);
|
||||
const group = artifacts
|
||||
.filter((item) => artifactVersionGroupKey(item) === groupKey)
|
||||
.sort((left, right) => stringValue(left.created_at).localeCompare(stringValue(right.created_at)));
|
||||
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
|
||||
const index = Math.max(0, group.findIndex((item) => stringValue(item.artifact_id) === artifactId || stringValue(item.subject_id) === artifactId));
|
||||
return {
|
||||
version: stringValue(artifact.version) || `local-v${index + 1}`,
|
||||
version_key: `${groupKey}:v${stringValue(artifact.version) || index + 1}`,
|
||||
version_rank: index + 1,
|
||||
};
|
||||
}
|
||||
|
||||
function artifactVersionGroupKey(artifact: Record<string, unknown>): string {
|
||||
const fileId = stringValue(artifact.file_id);
|
||||
const workspaceId = stringValue(artifact.workspace_id);
|
||||
const name = stringValue(artifact.name);
|
||||
if (fileId) return `file:${workspaceId}:${fileId}:${name}`;
|
||||
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
|
||||
if (artifactId) return `artifact:${artifactId}`;
|
||||
return `fallback:${workspaceId}:${stringValue(artifact.project_id)}:${name || stringValue(artifact.uri)}`;
|
||||
}
|
||||
|
||||
function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return buildArtifacts(snapshot)
|
||||
.filter(isDailyReportArtifact)
|
||||
@@ -3077,18 +3310,28 @@ function buildArtifacts(snapshot: QimingclawSnapshot | null): ArtifactEntry[] {
|
||||
runId: event.runId,
|
||||
})),
|
||||
];
|
||||
return dedupeArtifacts(artifacts).map((artifact) => withArtifactAccessSummary(artifact, snapshot));
|
||||
const deduped = dedupeArtifacts(artifacts);
|
||||
return deduped.map((artifact) => withArtifactAccessSummary(artifact, snapshot, deduped));
|
||||
}
|
||||
|
||||
function withArtifactAccessSummary(artifact: ArtifactEntry, snapshot: QimingclawSnapshot | null): ArtifactEntry {
|
||||
function withArtifactAccessSummary(artifact: ArtifactEntry, snapshot: QimingclawSnapshot | null, artifacts: ArtifactEntry[]): ArtifactEntry {
|
||||
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
|
||||
const lastAccess = artifactAccessRows(snapshot).find((row) => stringValue(row.artifact_id) === artifactId) ?? null;
|
||||
const hasLocalUri = isLocalArtifactUri(stringValue(artifact.uri));
|
||||
const hasDailyText = isDailyReportArtifact(artifact) && Boolean(stringValue(asRecord(artifact.payload)?.text_content));
|
||||
const formalRuntimeArtifact = runtimeArtifacts(snapshot).some((runtime) => runtime.id === artifactId || runtime.remoteId === artifactId);
|
||||
const previewable = formalRuntimeArtifact && (hasDailyText || hasLocalUri);
|
||||
const lifecycle = artifactLifecycleSummary(snapshot, artifactId);
|
||||
const version = artifactVersionMeta(artifacts, artifact);
|
||||
return {
|
||||
...artifact,
|
||||
version: version.version,
|
||||
version_key: version.version_key,
|
||||
version_rank: version.version_rank,
|
||||
retention_status: lifecycle.retention_status,
|
||||
retention_until: lifecycle.retention_until,
|
||||
last_retention_action: lifecycle.last_retention_action,
|
||||
delivery_summary: artifactDeliverySummary(artifact),
|
||||
access: {
|
||||
previewable,
|
||||
downloadable: formalRuntimeArtifact && hasLocalUri,
|
||||
@@ -3733,6 +3976,11 @@ function reportArtifactSources(artifacts: ArtifactEntry[]) {
|
||||
project_id: stringValue(artifact.project_id) || null,
|
||||
file_id: stringValue(artifact.file_id) || null,
|
||||
version: stringValue(artifact.version) || null,
|
||||
version_key: stringValue(artifact.version_key) || null,
|
||||
version_rank: numberValue(artifact.version_rank),
|
||||
retention_status: stringValue(artifact.retention_status) || null,
|
||||
retention_until: stringValue(artifact.retention_until) || null,
|
||||
last_retention_action: stringValue(artifact.last_retention_action) || null,
|
||||
file_size: typeof artifact.file_size === 'number' ? artifact.file_size : null,
|
||||
source_tool: stringValue(artifact.source_tool) || null,
|
||||
source_label: stringValue(artifact.source_label) || null,
|
||||
@@ -3743,6 +3991,7 @@ function reportArtifactSources(artifacts: ArtifactEntry[]) {
|
||||
run_id: stringValue(artifact.run_id) || null,
|
||||
created_at: stringValue(artifact.created_at) || null,
|
||||
access: artifact.access,
|
||||
delivery_summary: artifact.delivery_summary ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import { Download, Eye, FolderOpen } from 'lucide-react';
|
||||
import { Download, Eye, FolderOpen, History } from 'lucide-react';
|
||||
import GlassCard from '@/components/digital/GlassCard';
|
||||
import CardTitleBar from '@/components/digital/CardTitleBar';
|
||||
import {
|
||||
getDigitalEmployeeWorkday,
|
||||
postDigitalEmployeeWorkdayAction,
|
||||
accessArtifact,
|
||||
getArtifactDetail,
|
||||
getArtifactVersions,
|
||||
setArtifactRetention,
|
||||
} from '@/lib/api';
|
||||
import type {
|
||||
DigitalEmployeeDailyReport,
|
||||
@@ -185,6 +188,7 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
|
||||
const [busyKey, setBusyKey] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [preview, setPreview] = useState<{ title: string; text: string } | null>(null);
|
||||
const [detail, setDetail] = useState<{ artifact: DigitalEmployeeReportArtifactSource; item: any; versions: any[] } | null>(null);
|
||||
|
||||
if (artifacts.length === 0) {
|
||||
return <p className="de-empty-text" style={{ padding: '18px 0 6px' }}>运行产物出现后,这里会展示文件、输出和来源记录。</p>;
|
||||
@@ -217,6 +221,47 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
|
||||
}
|
||||
};
|
||||
|
||||
const loadDetail = async (artifact: DigitalEmployeeReportArtifactSource) => {
|
||||
if (!artifact.id || busyKey) return;
|
||||
setBusyKey(`${artifact.id}:detail`);
|
||||
setNotice(null);
|
||||
try {
|
||||
const [item, versions] = await Promise.all([
|
||||
getArtifactDetail(artifact.id),
|
||||
getArtifactVersions(artifact.id),
|
||||
]);
|
||||
setDetail({ artifact, item, versions });
|
||||
} catch (error) {
|
||||
setNotice(error instanceof Error ? error.message : '读取产物详情失败');
|
||||
} finally {
|
||||
setBusyKey(null);
|
||||
}
|
||||
};
|
||||
|
||||
const runRetention = async (action: 'mark_keep' | 'mark_expire' | 'mark_review' | 'clear_retention') => {
|
||||
const artifact = detail?.artifact;
|
||||
if (!artifact?.id || busyKey) return;
|
||||
setBusyKey(`${artifact.id}:${action}`);
|
||||
setNotice(null);
|
||||
try {
|
||||
const result = await setArtifactRetention(artifact.id, action, { reason: 'digital_employee_artifact_governance' });
|
||||
if (!result?.ok) {
|
||||
setNotice(result?.error || '保留策略写入失败');
|
||||
return;
|
||||
}
|
||||
const [item, versions] = await Promise.all([
|
||||
getArtifactDetail(artifact.id),
|
||||
getArtifactVersions(artifact.id),
|
||||
]);
|
||||
setDetail({ artifact: { ...artifact, retention_status: String(item?.retention_status || artifact.retention_status || 'unmanaged') }, item, versions });
|
||||
setNotice('保留策略已记录');
|
||||
} catch (error) {
|
||||
setNotice(error instanceof Error ? error.message : '保留策略写入失败');
|
||||
} finally {
|
||||
setBusyKey(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="de-artifact-source-list" aria-label="日报产物来源">
|
||||
@@ -232,6 +277,13 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
|
||||
<button type="button" className="de-icon-btn" disabled={!previewable || busyKey !== null} onClick={() => runAccess(artifact, 'preview')} title="预览" aria-label="预览"><Eye size={15} /></button>
|
||||
<button type="button" className="de-icon-btn" disabled={!downloadable || busyKey !== null} onClick={() => runAccess(artifact, 'download')} title="下载" aria-label="下载"><Download size={15} /></button>
|
||||
<button type="button" className="de-icon-btn" disabled={!openable || busyKey !== null} onClick={() => runAccess(artifact, 'open_location')} title="位置" aria-label="位置"><FolderOpen size={15} /></button>
|
||||
<button type="button" className="de-icon-btn" disabled={busyKey !== null} onClick={() => loadDetail(artifact)} title="版本" aria-label="版本"><History size={15} /></button>
|
||||
</div>
|
||||
<div className="de-artifact-source-meta">
|
||||
<span>{artifact.version || 'local-v1'}</span>
|
||||
<span>{artifact.retention_status || 'unmanaged'}</span>
|
||||
{artifact.delivery_status ? <span>{artifact.delivery_status}</span> : null}
|
||||
{artifact.access?.last_access_status ? <span>{artifact.access.last_access_status}</span> : null}
|
||||
</div>
|
||||
{artifact.uri ? <code>{artifact.uri}</code> : <em>{artifact.kind || 'artifact'}</em>}
|
||||
</div>
|
||||
@@ -245,6 +297,26 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
|
||||
<pre>{preview.text}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
{detail ? (
|
||||
<div className="de-artifact-preview">
|
||||
<div><strong>{detail.artifact.name}</strong><button type="button" className="de-btn-secondary" onClick={() => setDetail(null)}>关闭</button></div>
|
||||
<div className="de-artifact-retention-actions">
|
||||
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('mark_keep')}>保留</button>
|
||||
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('mark_expire')}>到期</button>
|
||||
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('mark_review')}>复核</button>
|
||||
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('clear_retention')}>清除</button>
|
||||
</div>
|
||||
<div className="de-artifact-version-list">
|
||||
{(detail.versions.length > 0 ? detail.versions : [detail.item]).filter(Boolean).map((item) => (
|
||||
<div key={String(item.artifact_id || item.version_key || item.created_at)}>
|
||||
<strong>{String(item.version || 'local-v1')}</strong>
|
||||
<span>{String(item.delivery_summary?.delivery_status || item.source_label || '未记录')}</span>
|
||||
<em>{String(item.created_at || '')}</em>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -756,6 +756,11 @@ export interface DigitalEmployeeReportArtifactSource {
|
||||
project_id?: string | null;
|
||||
file_id?: string | null;
|
||||
version?: string | null;
|
||||
version_key?: string | null;
|
||||
version_rank?: number | null;
|
||||
retention_status?: string | null;
|
||||
retention_until?: string | null;
|
||||
last_retention_action?: string | null;
|
||||
file_size?: number | null;
|
||||
source_tool?: string | null;
|
||||
source_label?: string | null;
|
||||
@@ -766,6 +771,7 @@ export interface DigitalEmployeeReportArtifactSource {
|
||||
run_id?: string | null;
|
||||
created_at?: string | null;
|
||||
access?: ArtifactEntry['access'];
|
||||
delivery_summary?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeDelivery {
|
||||
|
||||
Reference in New Issue
Block a user