修改了基础镜像DockerFile
parent
6e9eba0e55
commit
296114f5b6
|
|
@ -4,15 +4,15 @@ ENV LANG=C.UTF-8
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
ENV PYTHONIOENCODING=utf-8
|
ENV PYTHONIOENCODING=utf-8
|
||||||
|
|
||||||
# 1. 替换 apt 国内源
|
# 1. 替换 Debian 源为国内镜像
|
||||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources || \
|
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
|
||||||
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
|
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources
|
||||||
|
|
||||||
# 2. 安装基础依赖
|
# 2. 安装基础依赖
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
|
||||||
git \
|
|
||||||
curl \
|
curl \
|
||||||
|
gcc \
|
||||||
|
libpq-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 3. 安装 aiohttp 和基础 python 工具
|
# 3. 安装 aiohttp 和基础 python 工具
|
||||||
|
|
|
||||||
|
|
@ -708,6 +708,46 @@
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-upload-progress {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-upload-progress-track {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--line) 72%, transparent);
|
||||||
|
background: color-mix(in oklab, var(--panel) 78%, var(--panel-soft) 22%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-upload-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
width: 0;
|
||||||
|
background: linear-gradient(90deg, color-mix(in oklab, var(--brand) 75%, #7fb8ff 25%), color-mix(in oklab, var(--brand) 55%, #b6ddff 45%));
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-upload-progress-track.is-indeterminate .ops-upload-progress-fill {
|
||||||
|
width: 28%;
|
||||||
|
animation: ops-upload-indeterminate 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-upload-progress-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--subtitle);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ops-upload-indeterminate {
|
||||||
|
0% { transform: translateX(-110%); }
|
||||||
|
100% { transform: translateX(430%); }
|
||||||
|
}
|
||||||
|
|
||||||
.ops-pending-chip {
|
.ops-pending-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,23 @@ 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MEDIA_UPLOAD_EXTENSIONS = new Set([
|
||||||
|
'.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.svg', '.avif', '.heic', '.heif', '.tif', '.tiff',
|
||||||
|
'.mp3', '.wav', '.m4a', '.flac', '.ogg', '.opus', '.aac', '.amr', '.wma',
|
||||||
|
'.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.3gp', '.mpeg', '.mpg', '.ts',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isMediaUploadFile(file: File): boolean {
|
||||||
|
const mime = String(file.type || '').toLowerCase();
|
||||||
|
if (mime.startsWith('image/') || mime.startsWith('audio/') || mime.startsWith('video/')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const name = String(file.name || '').trim().toLowerCase();
|
||||||
|
const dot = name.lastIndexOf('.');
|
||||||
|
if (dot < 0) return false;
|
||||||
|
return MEDIA_UPLOAD_EXTENSIONS.has(name.slice(dot));
|
||||||
|
}
|
||||||
|
|
||||||
function isHtmlPath(path: string) {
|
function isHtmlPath(path: string) {
|
||||||
const normalized = String(path || '').trim().toLowerCase();
|
const normalized = String(path || '').trim().toLowerCase();
|
||||||
return normalized.endsWith('.html') || normalized.endsWith('.htm');
|
return normalized.endsWith('.html') || normalized.endsWith('.htm');
|
||||||
|
|
@ -544,6 +561,7 @@ export function BotDashboardModule({
|
||||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
||||||
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
||||||
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
||||||
|
const [attachmentUploadPercent, setAttachmentUploadPercent] = useState<number | null>(null);
|
||||||
const filePickerRef = useRef<HTMLInputElement | null>(null);
|
const filePickerRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [cronJobs, setCronJobs] = useState<CronJob[]>([]);
|
const [cronJobs, setCronJobs] = useState<CronJob[]>([]);
|
||||||
const [cronLoading, setCronLoading] = useState(false);
|
const [cronLoading, setCronLoading] = useState(false);
|
||||||
|
|
@ -1974,19 +1992,62 @@ export function BotDashboardModule({
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const formData = new FormData();
|
const mediaFiles: File[] = [];
|
||||||
files.forEach((file) => formData.append('files', file));
|
const normalFiles: File[] = [];
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (isMediaUploadFile(file)) {
|
||||||
|
mediaFiles.push(file);
|
||||||
|
} else {
|
||||||
|
normalFiles.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setIsUploadingAttachments(true);
|
const totalBytes = files.reduce((sum, file) => sum + Math.max(0, Number(file.size) || 0), 0);
|
||||||
try {
|
let uploadedBytes = 0;
|
||||||
|
const uploadedPaths: string[] = [];
|
||||||
|
|
||||||
|
const uploadBatch = async (batchFiles: File[], path: 'media' | 'uploads') => {
|
||||||
|
if (batchFiles.length === 0) return;
|
||||||
|
const batchBytes = batchFiles.reduce((sum, file) => sum + Math.max(0, Number(file.size) || 0), 0);
|
||||||
|
const formData = new FormData();
|
||||||
|
batchFiles.forEach((file) => formData.append('files', file));
|
||||||
const res = await axios.post<WorkspaceUploadResponse>(
|
const res = await axios.post<WorkspaceUploadResponse>(
|
||||||
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/workspace/upload`,
|
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/workspace/upload`,
|
||||||
formData,
|
formData,
|
||||||
{ params: { path: 'uploads' } },
|
{
|
||||||
|
params: { path },
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const loaded = Number(progressEvent.loaded || 0);
|
||||||
|
if (!Number.isFinite(loaded) || loaded < 0) {
|
||||||
|
setAttachmentUploadPercent(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (totalBytes <= 0) {
|
||||||
|
setAttachmentUploadPercent(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cappedLoaded = Math.max(0, Math.min(batchBytes, loaded));
|
||||||
|
const pct = Math.max(0, Math.min(100, Math.round(((uploadedBytes + cappedLoaded) / totalBytes) * 100)));
|
||||||
|
setAttachmentUploadPercent(pct);
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const uploaded = normalizeAttachmentPaths((res.data?.files || []).map((v) => v.path));
|
const uploaded = normalizeAttachmentPaths((res.data?.files || []).map((v) => v.path));
|
||||||
if (uploaded.length > 0) {
|
uploadedPaths.push(...uploaded);
|
||||||
setPendingAttachments((prev) => Array.from(new Set([...prev, ...uploaded])));
|
uploadedBytes += batchBytes;
|
||||||
|
if (totalBytes > 0) {
|
||||||
|
const pct = Math.max(0, Math.min(100, Math.round((uploadedBytes / totalBytes) * 100)));
|
||||||
|
setAttachmentUploadPercent(pct);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsUploadingAttachments(true);
|
||||||
|
setAttachmentUploadPercent(0);
|
||||||
|
try {
|
||||||
|
await uploadBatch(mediaFiles, 'media');
|
||||||
|
await uploadBatch(normalFiles, 'uploads');
|
||||||
|
if (uploadedPaths.length > 0) {
|
||||||
|
setPendingAttachments((prev) => Array.from(new Set([...prev, ...uploadedPaths])));
|
||||||
await loadWorkspaceTree(selectedBot.id, workspaceCurrentPath);
|
await loadWorkspaceTree(selectedBot.id, workspaceCurrentPath);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -1994,6 +2055,7 @@ export function BotDashboardModule({
|
||||||
notify(msg, { tone: 'error' });
|
notify(msg, { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingAttachments(false);
|
setIsUploadingAttachments(false);
|
||||||
|
setAttachmentUploadPercent(null);
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -2521,6 +2583,21 @@ export function BotDashboardModule({
|
||||||
{isSending ? t.sending : t.send}
|
{isSending ? t.sending : t.send}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{isUploadingAttachments ? (
|
||||||
|
<div className="ops-upload-progress" aria-live="polite">
|
||||||
|
<div className={`ops-upload-progress-track ${attachmentUploadPercent === null ? 'is-indeterminate' : ''}`}>
|
||||||
|
<div
|
||||||
|
className="ops-upload-progress-fill"
|
||||||
|
style={{ width: `${Math.max(3, Number(attachmentUploadPercent ?? 24))}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ops-upload-progress-text mono">
|
||||||
|
{attachmentUploadPercent === null
|
||||||
|
? t.uploadingFile
|
||||||
|
: `${t.uploadingFile} ${attachmentUploadPercent}%`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{pendingAttachments.length > 0 ? (
|
{pendingAttachments.length > 0 ? (
|
||||||
<div className="ops-pending-files">
|
<div className="ops-pending-files">
|
||||||
{pendingAttachments.map((p) => (
|
{pendingAttachments.map((p) => (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue