feat: 添加MinIO文件系统支持并优化外部连接器功能
- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等) - 更新外部连接器配置结构,包括BASE_URL和认证令牌设置 - 改进connector provider支持更多类型(official, feishu_bot等) - 实现Mistral模型推理模式支持reasoning_effort参数 - 增强外部连接器策略配置和运行时配置管理 - 添加connector bridge事件验证和安全保护机制 - 优化任务路由逻辑,区分simple_chat和new_task场景 - 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
@ -34,6 +34,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { type AppLocale, pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { containedLongTextClass, containedPreservedLongTextClass } from '@/lib/text-wrapping';
|
||||
|
||||
const LOAD_RETRY_DELAYS_MS = [0, 600, 1200];
|
||||
|
||||
@ -126,8 +127,8 @@ export default function FilesPage() {
|
||||
if (selectedFile?.path === item.path) {
|
||||
setSelectedFile(null);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err: any) {
|
||||
setLoadError(err.message || pickAppText(locale, '删除失败', 'Delete failed'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -147,8 +148,8 @@ export default function FilesPage() {
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(a.href);
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err: any) {
|
||||
setPreviewError(err.message || pickAppText(locale, '下载失败', 'Download failed'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -160,13 +161,13 @@ export default function FilesPage() {
|
||||
setUploadProgress(0);
|
||||
try {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await uploadUserFile(files[i], currentPath || 'uploads', (pct) => {
|
||||
await uploadUserFile(files[i], currentPath, (pct) => {
|
||||
setUploadProgress(Math.round((i / files.length) * 100 + pct / files.length));
|
||||
});
|
||||
}
|
||||
await load();
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err: any) {
|
||||
setLoadError(err.message || pickAppText(locale, '上传失败', 'Upload failed'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setUploadProgress(0);
|
||||
@ -183,8 +184,8 @@ export default function FilesPage() {
|
||||
setShowMkdir(false);
|
||||
setNewDirName('');
|
||||
await load();
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err: any) {
|
||||
setLoadError(err.message || pickAppText(locale, '创建文件夹失败', 'Failed to create folder'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -213,16 +214,17 @@ export default function FilesPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl p-6">
|
||||
<div className="mx-auto w-full max-w-7xl overflow-x-hidden px-4 py-6 sm:px-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold">{pickAppText(locale, '文件管理', 'Files')}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-11"
|
||||
onClick={() => setShowMkdir(true)}
|
||||
disabled={loading || !currentPath}
|
||||
disabled={loading}
|
||||
>
|
||||
<FolderPlus className="w-4 h-4 mr-1" />
|
||||
{pickAppText(locale, '新建文件夹', 'New folder')}
|
||||
@ -230,8 +232,9 @@ export default function FilesPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-11"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading || !currentPath}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
@ -252,7 +255,15 @@ export default function FilesPage() {
|
||||
className="hidden"
|
||||
onChange={handleUpload}
|
||||
/>
|
||||
<Button variant="outline" size="sm" onClick={() => load()} disabled={loading}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-11 w-11"
|
||||
onClick={() => load()}
|
||||
disabled={loading}
|
||||
aria-label={pickAppText(locale, '刷新', 'Refresh')}
|
||||
title={pickAppText(locale, '刷新', 'Refresh')}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
@ -266,7 +277,7 @@ export default function FilesPage() {
|
||||
<div className="flex items-center gap-1 mb-4 text-sm text-muted-foreground flex-wrap">
|
||||
<button
|
||||
onClick={() => navigateTo('')}
|
||||
className="flex items-center gap-1 hover:text-foreground transition-colors px-1.5 py-0.5 rounded hover:bg-accent"
|
||||
className="inline-flex h-11 items-center gap-1 rounded px-2 transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<Home className="w-3.5 h-3.5" />
|
||||
{pickAppText(locale, '文件', 'Files')}
|
||||
@ -279,7 +290,7 @@ export default function FilesPage() {
|
||||
<ChevronRight className="w-3 h-3 flex-shrink-0" />
|
||||
<button
|
||||
onClick={() => navigateTo(path)}
|
||||
className={`px-1.5 py-0.5 rounded transition-colors ${
|
||||
className={`inline-flex h-11 items-center rounded px-2 text-left transition-colors ${containedLongTextClass} ${
|
||||
isLast
|
||||
? 'text-foreground font-medium'
|
||||
: 'hover:text-foreground hover:bg-accent'
|
||||
@ -294,9 +305,13 @@ export default function FilesPage() {
|
||||
|
||||
{/* New directory input */}
|
||||
{showMkdir && (
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
||||
<Folder className="w-4 h-4 text-muted-foreground" />
|
||||
<label htmlFor="new-folder-name" className="sr-only">
|
||||
{pickAppText(locale, '文件夹名称', 'Folder name')}
|
||||
</label>
|
||||
<input
|
||||
id="new-folder-name"
|
||||
ref={mkdirInputRef}
|
||||
type="text"
|
||||
value={newDirName}
|
||||
@ -309,15 +324,16 @@ export default function FilesPage() {
|
||||
}
|
||||
}}
|
||||
placeholder={pickAppText(locale, '文件夹名称', 'Folder name')}
|
||||
className="flex-1 px-3 py-1.5 text-sm border border-border rounded-md bg-background focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
className="h-11 min-w-0 flex-1 rounded-md border border-border bg-background px-3 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="sm" onClick={handleCreateDir}>
|
||||
<Button size="sm" className="h-11" onClick={handleCreateDir}>
|
||||
{pickAppText(locale, '创建', 'Create')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-11"
|
||||
onClick={() => {
|
||||
setShowMkdir(false);
|
||||
setNewDirName('');
|
||||
@ -328,9 +344,9 @@ export default function FilesPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(360px,440px)_minmax(0,1fr)]">
|
||||
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-4 lg:grid-cols-[minmax(360px,440px)_minmax(0,1fr)]">
|
||||
{/* File list */}
|
||||
<div className="min-h-[520px] rounded-lg border border-border bg-card">
|
||||
<div className="min-w-0 rounded-lg border border-border bg-card lg:min-h-[520px]">
|
||||
{loading && items.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-20 text-muted-foreground">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
@ -340,7 +356,7 @@ export default function FilesPage() {
|
||||
<FolderOpen className="w-12 h-12 mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium">{pickAppText(locale, '加载失败', 'Failed to load')}</p>
|
||||
<p className="max-w-sm text-center text-sm">{loadError}</p>
|
||||
<Button className="mt-4" variant="outline" size="sm" onClick={() => load()}>
|
||||
<Button className="mt-4 h-11" variant="outline" size="sm" onClick={() => load()}>
|
||||
<RefreshCw className="mr-1 h-4 w-4" />
|
||||
{pickAppText(locale, '重试', 'Retry')}
|
||||
</Button>
|
||||
@ -349,90 +365,80 @@ export default function FilesPage() {
|
||||
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||
<FolderOpen className="w-12 h-12 mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium">{pickAppText(locale, '空文件夹', 'Empty folder')}</p>
|
||||
<p className="text-sm">{pickAppText(locale, '点击上方"上传"或"新建文件夹"按钮开始使用', 'Use "Upload" or "New folder" above to get started')}</p>
|
||||
<p className="px-4 text-center text-sm">{pickAppText(locale, '点击上方"上传"或"新建文件夹"按钮开始使用', 'Use "Upload" or "New folder" above to get started')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-[calc(100vh-15rem)] min-h-[520px]">
|
||||
<ScrollArea className="max-h-[calc(100vh-15rem)] min-h-[360px] lg:min-h-[520px]">
|
||||
<div className="space-y-1 p-2">
|
||||
{items.map((item) => (
|
||||
<button
|
||||
<div
|
||||
key={item.path}
|
||||
type="button"
|
||||
className={`group flex w-full items-center gap-3 rounded-lg border px-3 py-2.5 text-left transition-colors hover:bg-accent/30 ${
|
||||
className={`group flex min-w-0 flex-col gap-2 rounded-lg border p-2 text-left transition-colors hover:bg-accent/30 sm:flex-row sm:items-center ${
|
||||
selectedFile?.path === item.path ? 'border-primary bg-accent/40' : 'border-border bg-card'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (item.type === 'directory') {
|
||||
navigateTo(item.path);
|
||||
} else {
|
||||
void openFile(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{item.type === 'directory' ? (
|
||||
<Folder className="w-5 h-5 text-blue-500" />
|
||||
) : (
|
||||
<FileIcon name={item.name} contentType={item.content_type || undefined} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium">{item.name}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{item.type === 'file' && formatSize(item.size)}
|
||||
{item.modified && (
|
||||
<>
|
||||
{item.type === 'file' && ' · '}
|
||||
{formatDate(item.modified)}
|
||||
</>
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-h-[3.5rem] min-w-0 flex-1 items-center gap-3 rounded-md px-1 py-2 text-left"
|
||||
onClick={() => {
|
||||
if (item.type === 'directory') {
|
||||
navigateTo(item.path);
|
||||
} else {
|
||||
void openFile(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{item.type === 'directory' ? (
|
||||
<Folder className="w-5 h-5 text-blue-500" />
|
||||
) : (
|
||||
<FileIcon name={item.name} contentType={item.content_type || undefined} />
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={`text-sm font-medium ${containedLongTextClass}`}>{item.name}</div>
|
||||
<p className={`text-xs text-muted-foreground ${containedLongTextClass}`}>
|
||||
{item.type === 'file' && formatSize(item.size)}
|
||||
{item.modified && (
|
||||
<>
|
||||
{item.type === 'file' && ' · '}
|
||||
{formatDate(item.modified)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-1 opacity-100 md:opacity-0 md:transition-opacity md:group-hover:opacity-100">
|
||||
{item.type === 'file' && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent"
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-11 w-11 items-center justify-center rounded-md hover:bg-accent"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleDownload(item);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleDownload(item);
|
||||
}
|
||||
}}
|
||||
aria-label={`${pickAppText(locale, '下载', 'Download')} ${item.name}`}
|
||||
title={pickAppText(locale, '下载', 'Download')}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-destructive hover:bg-accent hover:text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-11 w-11 items-center justify-center rounded-md text-destructive hover:bg-accent hover:text-destructive"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleDelete(item);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleDelete(item);
|
||||
}
|
||||
}}
|
||||
aria-label={`${pickAppText(locale, '删除', 'Delete')} ${item.name}`}
|
||||
title={pickAppText(locale, '删除', 'Delete')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
@ -471,7 +477,7 @@ function FilePreviewPanel({
|
||||
locale: AppLocale;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-[520px] rounded-lg border border-border bg-card p-4">
|
||||
<div className="min-w-0 rounded-lg border border-border bg-card p-4 lg:min-h-[520px]">
|
||||
{loading ? (
|
||||
<div className="flex h-[420px] items-center justify-center text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
@ -485,16 +491,16 @@ function FilePreviewPanel({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
||||
<div className="flex min-w-0 flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="break-all text-base font-semibold">{file.name}</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<h2 className={`text-base font-semibold ${containedLongTextClass}`}>{file.name}</h2>
|
||||
<p className={`mt-1 text-xs text-muted-foreground ${containedLongTextClass}`}>
|
||||
{formatSize(file.size)} · {formatDate(file.modified)} · {file.content_type}
|
||||
{file.is_truncated ? ` · ${pickAppText(locale, '仅预览前 1MB', 'Showing first 1MB')}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
{downloadUrl && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Button variant="outline" size="sm" className="h-11" asChild>
|
||||
<a href={downloadUrl}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '下载', 'Download')}
|
||||
@ -514,11 +520,11 @@ function FilePreviewPanel({
|
||||
<p className="text-sm font-medium">{pickAppText(locale, '该文件不能直接预览', 'This file cannot be previewed')}</p>
|
||||
</div>
|
||||
) : isMarkdown(file) ? (
|
||||
<div className="prose prose-sm max-h-[620px] max-w-none overflow-auto text-black prose-a:text-black prose-code:text-black prose-headings:text-black prose-li:text-black prose-p:text-black prose-pre:bg-muted prose-pre:text-black prose-strong:text-black [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||
<div className={`prose prose-sm max-h-[620px] max-w-none overflow-auto text-black prose-a:text-black prose-code:text-black prose-headings:text-black prose-li:text-black prose-p:text-black prose-pre:bg-muted prose-pre:text-black prose-strong:text-black [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_*]:min-w-0 ${containedLongTextClass}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{file.content || ''}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<pre className="max-h-[620px] overflow-auto whitespace-pre-wrap rounded-md border border-border bg-background p-4 text-xs leading-5 text-black">
|
||||
<pre className={`max-h-[620px] overflow-auto rounded-md border border-border bg-background p-4 text-xs leading-5 text-black ${containedPreservedLongTextClass}`}>
|
||||
{file.content || ''}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user