吸收数字员工产物生命周期治理
This commit is contained in:
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user