修正文件处理
parent
d20bdc959f
commit
a71b092ffb
|
|
@ -1395,6 +1395,20 @@
|
||||||
width: min(1080px, 95vw);
|
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 {
|
.workspace-preview-body {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react';
|
import { useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react';
|
||||||
import axios from 'axios';
|
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 ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
@ -208,7 +208,7 @@ function normalizeRuntimeState(s?: string) {
|
||||||
function isPreviewableWorkspaceFile(node: WorkspaceNode) {
|
function isPreviewableWorkspaceFile(node: WorkspaceNode) {
|
||||||
if (node.type !== 'file') return false;
|
if (node.type !== 'file') return false;
|
||||||
const ext = (node.ext || '').toLowerCase();
|
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) {
|
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');
|
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) {
|
function isPreviewableWorkspacePath(path: string) {
|
||||||
const normalized = String(path || '').trim().toLowerCase();
|
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),
|
normalized.endsWith(ext),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +259,7 @@ function decorateWorkspacePathsForMarkdown(text: string) {
|
||||||
'[$1]($2)',
|
'[$1]($2)',
|
||||||
);
|
);
|
||||||
const workspacePathPattern =
|
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) => {
|
return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => {
|
||||||
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
||||||
if (!normalized) return fullPath;
|
if (!normalized) return fullPath;
|
||||||
|
|
@ -366,6 +373,7 @@ export function BotDashboardModule({
|
||||||
const [workspaceParentPath, setWorkspaceParentPath] = useState<string | null>(null);
|
const [workspaceParentPath, setWorkspaceParentPath] = useState<string | null>(null);
|
||||||
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
||||||
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
||||||
|
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
||||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(true);
|
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(true);
|
||||||
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
||||||
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
||||||
|
|
@ -396,9 +404,32 @@ export function BotDashboardModule({
|
||||||
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
||||||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
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 openWorkspacePathFromChat = (path: string) => {
|
||||||
const normalized = String(path || '').trim();
|
const normalized = String(path || '').trim();
|
||||||
if (!normalized) return;
|
if (!normalized) return;
|
||||||
|
if (isPdfPath(normalized) || isOfficePath(normalized)) {
|
||||||
|
triggerWorkspaceFileDownload(normalized);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isPreviewableWorkspacePath(normalized)) {
|
if (!isPreviewableWorkspacePath(normalized)) {
|
||||||
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
||||||
return;
|
return;
|
||||||
|
|
@ -409,7 +440,7 @@ export function BotDashboardModule({
|
||||||
const source = String(text || '');
|
const source = String(text || '');
|
||||||
if (!source) return [source];
|
if (!source) return [source];
|
||||||
const pattern =
|
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[] = [];
|
const nodes: ReactNode[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
let matchIndex = 0;
|
let matchIndex = 0;
|
||||||
|
|
@ -485,11 +516,7 @@ export function BotDashboardModule({
|
||||||
href="#"
|
href="#"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!isPreviewableWorkspacePath(workspacePath)) {
|
openWorkspacePathFromChat(workspacePath);
|
||||||
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void openWorkspaceFilePreview(workspacePath);
|
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -696,17 +723,20 @@ export function BotDashboardModule({
|
||||||
<div className="ops-chat-attachments">
|
<div className="ops-chat-attachments">
|
||||||
{(item.attachments || []).map((rawPath) => {
|
{(item.attachments || []).map((rawPath) => {
|
||||||
const filePath = normalizeDashboardAttachmentPath(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;
|
const filename = filePath.split('/').pop() || filePath;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={`${item.ts}-${filePath}`}
|
key={`${item.ts}-${filePath}`}
|
||||||
className="ops-attach-link mono"
|
className="ops-attach-link mono"
|
||||||
href={href}
|
href="#"
|
||||||
target="_blank"
|
onClick={(event) => {
|
||||||
rel="noreferrer"
|
event.preventDefault();
|
||||||
download={isPdf ? filename : undefined}
|
if (isPdfPath(filePath) || isOfficePath(filePath)) {
|
||||||
|
triggerWorkspaceFileDownload(filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openWorkspacePathFromChat(filePath);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{filename}
|
{filename}
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -822,9 +852,9 @@ export function BotDashboardModule({
|
||||||
const openWorkspaceFilePreview = async (path: string) => {
|
const openWorkspaceFilePreview = async (path: string) => {
|
||||||
if (!selectedBotId || !path) return;
|
if (!selectedBotId || !path) return;
|
||||||
const normalizedPath = String(path || '').trim();
|
const normalizedPath = String(path || '').trim();
|
||||||
if (isPdfPath(normalizedPath)) {
|
setWorkspacePreviewFullscreen(false);
|
||||||
const href = `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(normalizedPath)}&download=1`;
|
if (isPdfPath(normalizedPath) || isOfficePath(normalizedPath)) {
|
||||||
window.open(href, '_blank', 'noopener,noreferrer');
|
triggerWorkspaceFileDownload(normalizedPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isImagePath(normalizedPath)) {
|
if (isImagePath(normalizedPath)) {
|
||||||
|
|
@ -1565,14 +1595,14 @@ export function BotDashboardModule({
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewable = isPreviewableWorkspaceFile(node);
|
const previewable = isPreviewableWorkspaceFile(node);
|
||||||
const pdfFile = String(node.ext || '').toLowerCase() === '.pdf';
|
const downloadOnlyFile = isPdfPath(node.path) || isOfficePath(node.path);
|
||||||
rendered.push(
|
rendered.push(
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
className={`workspace-entry file ${previewable ? '' : 'disabled'}`}
|
className={`workspace-entry file ${previewable ? '' : 'disabled'}`}
|
||||||
disabled={!previewable || workspaceFileLoading}
|
disabled={!previewable || workspaceFileLoading}
|
||||||
onClick={() => void openWorkspaceFilePreview(node.path)}
|
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} />
|
<FileText size={14} />
|
||||||
<span className="workspace-entry-name">{node.name}</span>
|
<span className="workspace-entry-name">{node.name}</span>
|
||||||
|
|
@ -2499,8 +2529,8 @@ export function BotDashboardModule({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{workspacePreview && (
|
{workspacePreview && (
|
||||||
<div className="modal-mask" onClick={() => setWorkspacePreview(null)}>
|
<div className="modal-mask" onClick={closeWorkspacePreview}>
|
||||||
<div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
|
<div className={`modal-card modal-preview ${workspacePreviewFullscreen ? 'modal-preview-fullscreen' : ''}`} onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-title-row">
|
<div className="modal-title-row">
|
||||||
<h3>{t.filePreview}</h3>
|
<h3>{t.filePreview}</h3>
|
||||||
<span className="modal-sub mono">{workspacePreview.path}</span>
|
<span className="modal-sub mono">{workspacePreview.path}</span>
|
||||||
|
|
@ -2532,6 +2562,14 @@ export function BotDashboardModule({
|
||||||
<div className="row-between">
|
<div className="row-between">
|
||||||
<span className="workspace-preview-meta mono">{workspacePreview.ext || '-'}</span>
|
<span className="workspace-preview-meta mono">{workspacePreview.ext || '-'}</span>
|
||||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
<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
|
<a
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
href={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}&download=1`}
|
href={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}&download=1`}
|
||||||
|
|
@ -2541,7 +2579,7 @@ export function BotDashboardModule({
|
||||||
>
|
>
|
||||||
{t.download}
|
{t.download}
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue