吸收数字员工产物生命周期治理

This commit is contained in:
baiyanyun
2026-06-10 10:23:47 +08:00
parent 362f397e37
commit ed2f19d257
15 changed files with 739 additions and 164 deletions

View File

@@ -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 ?? []);

View File

@@ -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,
}));
}