吸收数字员工产物访问治理
This commit is contained in:
@@ -723,6 +723,22 @@ export function getArtifacts(): Promise<any[]> {
|
||||
.then((data) => data.artifacts ?? []);
|
||||
}
|
||||
|
||||
export function accessArtifact(artifactId: string, action: 'preview' | 'download' | 'open_location'): Promise<any> {
|
||||
return apiFetch(`/api/digital-employee/artifacts/${encodeURIComponent(artifactId)}/access`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action }),
|
||||
});
|
||||
}
|
||||
|
||||
export function getArtifactAccessEvents(params: Record<string, string | number | undefined> = {}): Promise<any> {
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== '') search.set(key, String(value));
|
||||
}
|
||||
const suffix = search.toString() ? `?${search.toString()}` : '';
|
||||
return apiFetch(`/api/digital-employee/artifact-access-events${suffix}`);
|
||||
}
|
||||
|
||||
export function getDebugConversations(): Promise<any[]> {
|
||||
return apiFetch<{ conversations: any[] }>('/api/debug/conversations')
|
||||
.then((data) => data.conversations ?? []);
|
||||
|
||||
@@ -63,6 +63,7 @@ declare global {
|
||||
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise<RouteDecisionRecord>;
|
||||
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>;
|
||||
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 }>;
|
||||
};
|
||||
@@ -237,6 +238,17 @@ interface QimingclawDailyReportRecordResult {
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
interface QimingclawArtifactAccessResponse {
|
||||
ok: boolean;
|
||||
artifact_id: string;
|
||||
action: 'preview' | 'download' | 'open_location';
|
||||
status: 'allowed' | 'rejected';
|
||||
error?: string;
|
||||
reason_codes?: string[];
|
||||
preview?: Record<string, unknown>;
|
||||
event_id?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawManagedServiceActionResponse {
|
||||
ok: boolean;
|
||||
service_id: string;
|
||||
@@ -659,6 +671,15 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
const rows = filterSkillCallAuditRows(skillCallAuditRows(await readQimingclawSnapshot()), url.searchParams);
|
||||
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/digital-employee/artifact-access-events') {
|
||||
const rows = filterArtifactAccessRows(artifactAccessRows(await readQimingclawSnapshot()), url.searchParams);
|
||||
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
const artifactAccessMatch = pathname.match(/^\/api\/(?:digital-employee\/artifacts|artifact)\/([^/]+)\/access$/);
|
||||
if (method === 'POST' && artifactAccessMatch) {
|
||||
const artifactId = decodeURIComponent(artifactAccessMatch[1] || '');
|
||||
return await handleArtifactAccess(artifactId, parseOptionalJsonBody(options.body)) as T;
|
||||
}
|
||||
const businessPlanDetailMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)$/);
|
||||
if (method === 'GET' && businessPlanDetailMatch) {
|
||||
const planId = decodeURIComponent(businessPlanDetailMatch[1] || '');
|
||||
@@ -1351,6 +1372,44 @@ async function handleManagedServiceRestart(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleArtifactAccess(
|
||||
artifactId: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<QimingclawArtifactAccessResponse> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
const action = normalizeArtifactAccessAction(stringValue(body.action));
|
||||
if (!bridge?.accessArtifact) {
|
||||
return {
|
||||
ok: false,
|
||||
artifact_id: artifactId,
|
||||
action,
|
||||
status: 'rejected',
|
||||
error: 'bridge_unavailable',
|
||||
reason_codes: ['bridge_unavailable'],
|
||||
};
|
||||
}
|
||||
try {
|
||||
return await bridge.accessArtifact(artifactId, action, {
|
||||
actor: stringValue(body.actor) || 'digital_employee_ui',
|
||||
source: stringValue(body.source) || 'sgrobot-digital-adapter',
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
artifact_id: artifactId,
|
||||
action,
|
||||
status: 'rejected',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
reason_codes: ['artifact_access_failed'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeArtifactAccessAction(value: string): 'preview' | 'download' | 'open_location' {
|
||||
if (value === 'download' || value === 'open_location') return value;
|
||||
return 'preview';
|
||||
}
|
||||
|
||||
function syncLine(snapshot: QimingclawSnapshot | null): string {
|
||||
const sync = snapshot?.sync;
|
||||
if (!sync) return '管理端同步:未连接';
|
||||
@@ -2368,6 +2427,43 @@ function filterSkillCallAuditRows(rows: BusinessViewRow[], searchParams: URLSear
|
||||
.filter((row) => !decision || stringValue(row.decision) === decision);
|
||||
}
|
||||
|
||||
function artifactAccessRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return runtimeEvents(snapshot)
|
||||
.filter((event) => ['artifact_access_allowed', 'artifact_access_rejected'].includes(event.kind))
|
||||
.map((event) => {
|
||||
const payload = asRecord(event.payload) ?? {};
|
||||
return {
|
||||
access_id: event.id,
|
||||
event_id: event.id,
|
||||
remote_id: event.remoteId ?? null,
|
||||
artifact_id: stringValue(payload.artifact_id) || null,
|
||||
action: stringValue(payload.action) || null,
|
||||
status: stringValue(payload.status) || event.kind.replace('artifact_access_', ''),
|
||||
reason_codes: Array.isArray(payload.reason_codes) ? payload.reason_codes : [],
|
||||
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 filterArtifactAccessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
|
||||
const artifactId = stringValue(searchParams.get('artifact_id'));
|
||||
const action = stringValue(searchParams.get('action'));
|
||||
const status = stringValue(searchParams.get('status'));
|
||||
return rows
|
||||
.filter((row) => !artifactId || stringValue(row.artifact_id) === artifactId)
|
||||
.filter((row) => !action || stringValue(row.action) === action)
|
||||
.filter((row) => !status || stringValue(row.status) === status);
|
||||
}
|
||||
|
||||
function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return buildArtifacts(snapshot)
|
||||
.filter(isDailyReportArtifact)
|
||||
@@ -2981,7 +3077,26 @@ function buildArtifacts(snapshot: QimingclawSnapshot | null): ArtifactEntry[] {
|
||||
runId: event.runId,
|
||||
})),
|
||||
];
|
||||
return dedupeArtifacts(artifacts);
|
||||
return dedupeArtifacts(artifacts).map((artifact) => withArtifactAccessSummary(artifact, snapshot));
|
||||
}
|
||||
|
||||
function withArtifactAccessSummary(artifact: ArtifactEntry, snapshot: QimingclawSnapshot | null): 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);
|
||||
return {
|
||||
...artifact,
|
||||
access: {
|
||||
previewable,
|
||||
downloadable: formalRuntimeArtifact && hasLocalUri,
|
||||
openable: formalRuntimeArtifact && hasLocalUri,
|
||||
access_policy: formalRuntimeArtifact ? 'qimingclaw-local-artifact-policy' : 'runtime-projection-readonly',
|
||||
last_access_status: stringValue(lastAccess?.status) || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): ArtifactEntry {
|
||||
@@ -3181,6 +3296,13 @@ function looksLikeUri(value: string): boolean {
|
||||
return /^(file|https?):\/\//i.test(value) || value.includes('/') || value.includes('\\');
|
||||
}
|
||||
|
||||
function isLocalArtifactUri(value: string): boolean {
|
||||
if (!value) return false;
|
||||
if (/^file:\/\//i.test(value)) return true;
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return false;
|
||||
return value.startsWith('/') || /^[a-z]:[\\/]/i.test(value);
|
||||
}
|
||||
|
||||
function artifactName(value: string): string {
|
||||
if (!value) return '';
|
||||
const normalized = value.split(/[?#]/)[0] || value;
|
||||
@@ -3620,6 +3742,7 @@ function reportArtifactSources(artifacts: ArtifactEntry[]) {
|
||||
task_id: stringValue(artifact.task_id) || null,
|
||||
run_id: stringValue(artifact.run_id) || null,
|
||||
created_at: stringValue(artifact.created_at) || null,
|
||||
access: artifact.access,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user