吸收数字员工产物访问治理

This commit is contained in:
baiyanyun
2026-06-10 10:00:34 +08:00
parent cc36d8dcaf
commit 362f397e37
18 changed files with 1016 additions and 166 deletions

View File

@@ -1,10 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { Download } from 'lucide-react';
import { Download, Eye, FolderOpen } from 'lucide-react';
import GlassCard from '@/components/digital/GlassCard';
import CardTitleBar from '@/components/digital/CardTitleBar';
import {
getDigitalEmployeeWorkday,
postDigitalEmployeeWorkdayAction,
accessArtifact,
} from '@/lib/api';
import type {
DigitalEmployeeDailyReport,
@@ -181,20 +182,70 @@ function DetailList({ details }: { details: DigitalEmployeeRunDetail[] }) {
}
function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArtifactSource[] }) {
const [busyKey, setBusyKey] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [preview, setPreview] = useState<{ title: string; text: string } | null>(null);
if (artifacts.length === 0) {
return <p className="de-empty-text" style={{ padding: '18px 0 6px' }}></p>;
}
const runAccess = async (artifact: DigitalEmployeeReportArtifactSource, action: 'preview' | 'download' | 'open_location') => {
if (!artifact.id || busyKey) return;
const key = `${artifact.id}:${action}`;
setBusyKey(key);
setNotice(null);
try {
const result = await accessArtifact(artifact.id, action);
if (!result?.ok) {
setNotice(result?.error || '产物访问被拒绝');
return;
}
if (action === 'preview') {
const data = result.preview || {};
setPreview({
title: String(data.name || artifact.name || '产物预览'),
text: String(data.text || `${data.mime || artifact.kind || 'artifact'} · ${data.size ?? 'unknown'}`),
});
} else {
setNotice(action === 'download' ? '已打开产物' : '已打开位置');
}
} catch (error) {
setNotice(error instanceof Error ? error.message : '产物访问失败');
} finally {
setBusyKey(null);
}
};
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 className="de-artifact-source-list" aria-label="日报产物来源">
{artifacts.map((artifact) => {
const previewable = Boolean(artifact.access?.previewable);
const downloadable = Boolean(artifact.access?.downloadable);
const openable = Boolean(artifact.access?.openable);
return (
<div key={artifact.id} className="de-artifact-source-row">
<strong>{artifact.name}</strong>
<span>{artifact.source_label || artifact.source_type || '未知来源'}</span>
<div className="de-artifact-source-actions">
<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>
</div>
{artifact.uri ? <code>{artifact.uri}</code> : <em>{artifact.kind || 'artifact'}</em>}
</div>
);
})}
</div>
{notice ? <p className="de-inline-notice">{notice}</p> : null}
{preview ? (
<div className="de-artifact-preview">
<div><strong>{preview.title}</strong><button type="button" className="de-btn-secondary" onClick={() => setPreview(null)}></button></div>
<pre>{preview.text}</pre>
</div>
))}
</div>
) : null}
</>
);
}