修正文件处理
parent
d20bdc959f
commit
a71b092ffb
|
|
@ -1395,6 +1395,20 @@
|
|||
width: min(1080px, 95vw);
|
||||
}
|
||||
|
||||
.modal-preview-fullscreen {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.modal-preview-fullscreen .workspace-preview-body {
|
||||
min-height: calc(100vh - 170px);
|
||||
max-height: calc(100vh - 170px);
|
||||
}
|
||||
|
||||
.workspace-preview-body {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Activity, Boxes, Check, Clock3, EllipsisVertical, Eye, EyeOff, FileText, FolderOpen, Hammer, MessageSquareText, Paperclip, Plus, Power, PowerOff, RefreshCw, Repeat2, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react';
|
||||
import { Activity, Boxes, Check, Clock3, EllipsisVertical, Eye, EyeOff, FileText, FolderOpen, Hammer, Maximize2, MessageSquareText, Minimize2, Paperclip, Plus, Power, PowerOff, RefreshCw, Repeat2, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
|
@ -208,7 +208,7 @@ function normalizeRuntimeState(s?: string) {
|
|||
function isPreviewableWorkspaceFile(node: WorkspaceNode) {
|
||||
if (node.type !== 'file') return false;
|
||||
const ext = (node.ext || '').toLowerCase();
|
||||
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp'].includes(ext);
|
||||
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].includes(ext);
|
||||
}
|
||||
|
||||
function isPdfPath(path: string) {
|
||||
|
|
@ -220,9 +220,16 @@ function isImagePath(path: string) {
|
|||
return normalized.endsWith('.png') || normalized.endsWith('.jpg') || normalized.endsWith('.jpeg') || normalized.endsWith('.webp');
|
||||
}
|
||||
|
||||
function isOfficePath(path: string) {
|
||||
const normalized = String(path || '').trim().toLowerCase();
|
||||
return ['.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].some((ext) =>
|
||||
normalized.endsWith(ext),
|
||||
);
|
||||
}
|
||||
|
||||
function isPreviewableWorkspacePath(path: string) {
|
||||
const normalized = String(path || '').trim().toLowerCase();
|
||||
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp'].some((ext) =>
|
||||
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].some((ext) =>
|
||||
normalized.endsWith(ext),
|
||||
);
|
||||
}
|
||||
|
|
@ -252,7 +259,7 @@ function decorateWorkspacePathsForMarkdown(text: string) {
|
|||
'[$1]($2)',
|
||||
);
|
||||
const workspacePathPattern =
|
||||
/\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b/gi;
|
||||
/\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi;
|
||||
return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => {
|
||||
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
||||
if (!normalized) return fullPath;
|
||||
|
|
@ -366,6 +373,7 @@ export function BotDashboardModule({
|
|||
const [workspaceParentPath, setWorkspaceParentPath] = useState<string | null>(null);
|
||||
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
||||
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
||||
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(true);
|
||||
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
||||
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
||||
|
|
@ -396,9 +404,32 @@ export function BotDashboardModule({
|
|||
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
||||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) =>
|
||||
`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(filePath)}${forceDownload ? '&download=1' : ''}`;
|
||||
const closeWorkspacePreview = () => {
|
||||
setWorkspacePreview(null);
|
||||
setWorkspacePreviewFullscreen(false);
|
||||
};
|
||||
const triggerWorkspaceFileDownload = (filePath: string) => {
|
||||
if (!selectedBotId) return;
|
||||
const normalized = String(filePath || '').trim();
|
||||
if (!normalized) return;
|
||||
const filename = normalized.split('/').pop() || 'workspace-file';
|
||||
const link = document.createElement('a');
|
||||
link.href = buildWorkspaceDownloadHref(normalized, true);
|
||||
link.download = filename;
|
||||
link.rel = 'noopener noreferrer';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
const openWorkspacePathFromChat = (path: string) => {
|
||||
const normalized = String(path || '').trim();
|
||||
if (!normalized) return;
|
||||
if (isPdfPath(normalized) || isOfficePath(normalized)) {
|
||||
triggerWorkspaceFileDownload(normalized);
|
||||
return;
|
||||
}
|
||||
if (!isPreviewableWorkspacePath(normalized)) {
|
||||
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
||||
return;
|
||||
|
|
@ -409,7 +440,7 @@ export function BotDashboardModule({
|
|||
const source = String(text || '');
|
||||
if (!source) return [source];
|
||||
const pattern =
|
||||
/\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi;
|
||||
/\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi;
|
||||
const nodes: ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let matchIndex = 0;
|
||||
|
|
@ -485,11 +516,7 @@ export function BotDashboardModule({
|
|||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
if (!isPreviewableWorkspacePath(workspacePath)) {
|
||||
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
||||
return;
|
||||
}
|
||||
void openWorkspaceFilePreview(workspacePath);
|
||||
openWorkspacePathFromChat(workspacePath);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -696,17 +723,20 @@ export function BotDashboardModule({
|
|||
<div className="ops-chat-attachments">
|
||||
{(item.attachments || []).map((rawPath) => {
|
||||
const filePath = normalizeDashboardAttachmentPath(rawPath);
|
||||
const isPdf = isPdfPath(filePath);
|
||||
const href = `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(filePath)}${isPdf ? '&download=1' : ''}`;
|
||||
const filename = filePath.split('/').pop() || filePath;
|
||||
return (
|
||||
<a
|
||||
key={`${item.ts}-${filePath}`}
|
||||
className="ops-attach-link mono"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
download={isPdf ? filename : undefined}
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
if (isPdfPath(filePath) || isOfficePath(filePath)) {
|
||||
triggerWorkspaceFileDownload(filePath);
|
||||
return;
|
||||
}
|
||||
openWorkspacePathFromChat(filePath);
|
||||
}}
|
||||
>
|
||||
{filename}
|
||||
</a>
|
||||
|
|
@ -822,9 +852,9 @@ export function BotDashboardModule({
|
|||
const openWorkspaceFilePreview = async (path: string) => {
|
||||
if (!selectedBotId || !path) return;
|
||||
const normalizedPath = String(path || '').trim();
|
||||
if (isPdfPath(normalizedPath)) {
|
||||
const href = `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(normalizedPath)}&download=1`;
|
||||
window.open(href, '_blank', 'noopener,noreferrer');
|
||||
setWorkspacePreviewFullscreen(false);
|
||||
if (isPdfPath(normalizedPath) || isOfficePath(normalizedPath)) {
|
||||
triggerWorkspaceFileDownload(normalizedPath);
|
||||
return;
|
||||
}
|
||||
if (isImagePath(normalizedPath)) {
|
||||
|
|
@ -1565,14 +1595,14 @@ export function BotDashboardModule({
|
|||
}
|
||||
|
||||
const previewable = isPreviewableWorkspaceFile(node);
|
||||
const pdfFile = String(node.ext || '').toLowerCase() === '.pdf';
|
||||
const downloadOnlyFile = isPdfPath(node.path) || isOfficePath(node.path);
|
||||
rendered.push(
|
||||
<button
|
||||
key={key}
|
||||
className={`workspace-entry file ${previewable ? '' : 'disabled'}`}
|
||||
disabled={!previewable || workspaceFileLoading}
|
||||
onClick={() => void openWorkspaceFilePreview(node.path)}
|
||||
title={previewable ? (pdfFile ? t.download : t.previewTitle) : t.fileNotPreviewable}
|
||||
title={previewable ? (downloadOnlyFile ? t.download : t.previewTitle) : t.fileNotPreviewable}
|
||||
>
|
||||
<FileText size={14} />
|
||||
<span className="workspace-entry-name">{node.name}</span>
|
||||
|
|
@ -2499,8 +2529,8 @@ export function BotDashboardModule({
|
|||
)}
|
||||
|
||||
{workspacePreview && (
|
||||
<div className="modal-mask" onClick={() => setWorkspacePreview(null)}>
|
||||
<div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-mask" onClick={closeWorkspacePreview}>
|
||||
<div className={`modal-card modal-preview ${workspacePreviewFullscreen ? 'modal-preview-fullscreen' : ''}`} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-title-row">
|
||||
<h3>{t.filePreview}</h3>
|
||||
<span className="modal-sub mono">{workspacePreview.path}</span>
|
||||
|
|
@ -2532,6 +2562,14 @@ export function BotDashboardModule({
|
|||
<div className="row-between">
|
||||
<span className="workspace-preview-meta mono">{workspacePreview.ext || '-'}</span>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setWorkspacePreviewFullscreen((v) => !v)}
|
||||
title={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
|
||||
aria-label={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
|
||||
>
|
||||
{workspacePreviewFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
||||
</button>
|
||||
<a
|
||||
className="btn btn-secondary"
|
||||
href={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}&download=1`}
|
||||
|
|
@ -2541,7 +2579,7 @@ export function BotDashboardModule({
|
|||
>
|
||||
{t.download}
|
||||
</a>
|
||||
<button className="btn btn-primary" onClick={() => setWorkspacePreview(null)}>{t.close}</button>
|
||||
<button className="btn btn-primary" onClick={closeWorkspacePreview}>{t.close}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue