396 lines
14 KiB
TypeScript
396 lines
14 KiB
TypeScript
// @ts-nocheck
|
||
import { usePolicyStore } from '../../stores/policy';
|
||
|
||
export const uploadMethods = {
|
||
triggerFileUpload() {
|
||
if (this.uploading) {
|
||
return;
|
||
}
|
||
const input = this.getComposerElement('fileUploadInput');
|
||
if (input) {
|
||
input.click();
|
||
}
|
||
},
|
||
|
||
handleFileSelected(files) {
|
||
const policyStore = usePolicyStore();
|
||
if (policyStore.uiBlocks?.block_upload) {
|
||
this.uiPushToast({
|
||
title: '上传被禁用',
|
||
message: '已被管理员禁用上传功能',
|
||
type: 'warning'
|
||
});
|
||
return;
|
||
}
|
||
this.uploadHandleSelected(files);
|
||
},
|
||
|
||
normalizeLocalFiles(files) {
|
||
if (!files) return [];
|
||
const list = Array.isArray(files) ? files : Array.from(files);
|
||
return list.filter(Boolean);
|
||
},
|
||
|
||
isImageFile(file) {
|
||
const name = file?.name || '';
|
||
const type = file?.type || '';
|
||
return type.startsWith('image/') || /\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(name);
|
||
},
|
||
|
||
isVideoFile(file) {
|
||
const name = file?.name || '';
|
||
const type = file?.type || '';
|
||
return type.startsWith('video/') || /\.(mp4|mov|m4v|webm|avi|mkv|flv|mpg|mpeg)$/i.test(name);
|
||
},
|
||
|
||
upsertImageEntry(path, filename) {
|
||
if (!path) return;
|
||
const name = filename || path.split('/').pop() || path;
|
||
const list = Array.isArray(this.imageEntries) ? this.imageEntries : [];
|
||
if (list.some((item) => item.path === path)) {
|
||
return;
|
||
}
|
||
this.imageEntries = [{ name, path }, ...list];
|
||
},
|
||
|
||
upsertVideoEntry(path, filename) {
|
||
if (!path) return;
|
||
const name = filename || path.split('/').pop() || path;
|
||
const list = Array.isArray(this.videoEntries) ? this.videoEntries : [];
|
||
if (list.some((item) => item.path === path)) {
|
||
return;
|
||
}
|
||
this.videoEntries = [{ name, path }, ...list];
|
||
},
|
||
|
||
async handleLocalImageFiles(files) {
|
||
if (!this.isConnected) {
|
||
return;
|
||
}
|
||
if (this.mediaUploading) {
|
||
this.uiPushToast({
|
||
title: '上传中',
|
||
message: '请等待当前图片上传完成',
|
||
type: 'info'
|
||
});
|
||
return;
|
||
}
|
||
const list = this.normalizeLocalFiles(files);
|
||
if (!list.length) {
|
||
return;
|
||
}
|
||
const existingCount = Array.isArray(this.selectedImages) ? this.selectedImages.length : 0;
|
||
const remaining = Math.max(0, 9 - existingCount);
|
||
if (!remaining) {
|
||
this.uiPushToast({
|
||
title: '已达上限',
|
||
message: '最多只能选择 9 张图片',
|
||
type: 'warning'
|
||
});
|
||
return;
|
||
}
|
||
const valid = list.filter((file) => this.isImageFile(file));
|
||
// 兼容 Android WebView 部分机型:返回的 File 可能缺失 type/扩展名,导致识别失败
|
||
// 若来自图片选择器但未识别出有效类型,回退为“按选择结果直接上传”。
|
||
const candidates = valid.length ? valid : list;
|
||
if (valid.length < list.length && valid.length > 0) {
|
||
this.uiPushToast({
|
||
title: '已忽略',
|
||
message: '已跳过非图片文件',
|
||
type: 'info'
|
||
});
|
||
}
|
||
const limited = candidates.slice(0, remaining);
|
||
if (candidates.length > remaining) {
|
||
this.uiPushToast({
|
||
title: '已超出数量',
|
||
message: `最多还能添加 ${remaining} 张图片,已自动截断`,
|
||
type: 'warning'
|
||
});
|
||
}
|
||
const uploaded = await this.uploadBatchFiles(limited, {
|
||
markUploading: true,
|
||
markMediaUploading: true
|
||
});
|
||
if (!uploaded.length) {
|
||
return;
|
||
}
|
||
uploaded.forEach((item) => {
|
||
if (!item?.path) return;
|
||
this.inputAddSelectedImage(item.path);
|
||
this.upsertImageEntry(item.path, item.filename);
|
||
});
|
||
// 上传完成后自动关闭选择窗口
|
||
this.closeImagePicker();
|
||
},
|
||
|
||
async handleLocalVideoFiles(files) {
|
||
if (!this.isConnected) {
|
||
return;
|
||
}
|
||
if (this.mediaUploading) {
|
||
this.uiPushToast({
|
||
title: '上传中',
|
||
message: '请等待当前视频上传完成',
|
||
type: 'info'
|
||
});
|
||
return;
|
||
}
|
||
const list = this.normalizeLocalFiles(files);
|
||
if (!list.length) {
|
||
return;
|
||
}
|
||
const valid = list.filter((file) => this.isVideoFile(file));
|
||
// 兼容 Android WebView:文件元数据缺失时回退按选择结果直接上传
|
||
const candidates = valid.length ? valid : list;
|
||
if (valid.length < list.length && valid.length > 0) {
|
||
this.uiPushToast({
|
||
title: '已忽略',
|
||
message: '已跳过非视频文件',
|
||
type: 'info'
|
||
});
|
||
}
|
||
if (candidates.length > 1) {
|
||
this.uiPushToast({
|
||
title: '视频数量过多',
|
||
message: '一次只能选择 1 个视频,已使用第一个',
|
||
type: 'warning'
|
||
});
|
||
}
|
||
const [file] = candidates;
|
||
if (!file) {
|
||
return;
|
||
}
|
||
const uploaded = await this.uploadBatchFiles([file], {
|
||
markUploading: true,
|
||
markMediaUploading: true
|
||
});
|
||
const [item] = uploaded;
|
||
if (!item?.path) {
|
||
return;
|
||
}
|
||
this.inputSetSelectedVideos([item.path]);
|
||
this.inputClearSelectedImages();
|
||
this.upsertVideoEntry(item.path, item.filename);
|
||
},
|
||
|
||
async openImagePicker() {
|
||
if (!['qwen3-vl-plus', 'kimi-k2.5'].includes(this.currentModelKey)) {
|
||
this.uiPushToast({
|
||
title: '当前模型不支持图片',
|
||
message: '请选择 Qwen3.5 或 Kimi-k2.5 后再发送图片',
|
||
type: 'error'
|
||
});
|
||
return;
|
||
}
|
||
this.closeQuickMenu();
|
||
this.inputSetImagePickerOpen(true);
|
||
await this.loadWorkspaceImages();
|
||
},
|
||
|
||
closeImagePicker() {
|
||
this.inputSetImagePickerOpen(false);
|
||
},
|
||
|
||
async openVideoPicker() {
|
||
if (!['qwen3-vl-plus', 'kimi-k2.5'].includes(this.currentModelKey)) {
|
||
this.uiPushToast({
|
||
title: '当前模型不支持视频',
|
||
message: '请切换到 Qwen3.5 或 Kimi-k2.5 后再发送视频',
|
||
type: 'error'
|
||
});
|
||
return;
|
||
}
|
||
this.closeQuickMenu();
|
||
this.inputSetVideoPickerOpen(true);
|
||
await this.loadWorkspaceVideos();
|
||
},
|
||
|
||
closeVideoPicker() {
|
||
this.inputSetVideoPickerOpen(false);
|
||
},
|
||
|
||
async loadWorkspaceImages() {
|
||
this.imageLoading = true;
|
||
try {
|
||
const entries = await this.fetchAllImageEntries('');
|
||
this.imageEntries = entries;
|
||
if (!entries.length) {
|
||
this.uiPushToast({
|
||
title: '未找到图片',
|
||
message: '工作区内没有可用的图片文件',
|
||
type: 'info'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载图片列表失败', error);
|
||
this.uiPushToast({
|
||
title: '加载图片失败',
|
||
message: error?.message || '请稍后重试',
|
||
type: 'error'
|
||
});
|
||
} finally {
|
||
this.imageLoading = false;
|
||
}
|
||
},
|
||
|
||
async fetchAllImageEntries(startPath = '') {
|
||
const queue: string[] = [startPath || ''];
|
||
const visited = new Set<string>();
|
||
const results: Array<{ name: string; path: string }> = [];
|
||
const exts = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.svg']);
|
||
const maxFolders = 120;
|
||
|
||
while (queue.length && visited.size < maxFolders) {
|
||
const path = queue.shift() || '';
|
||
if (visited.has(path)) {
|
||
continue;
|
||
}
|
||
visited.add(path);
|
||
try {
|
||
const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, {
|
||
method: 'GET',
|
||
credentials: 'include',
|
||
headers: { Accept: 'application/json' }
|
||
});
|
||
const data = await resp.json().catch(() => null);
|
||
if (!data?.success) {
|
||
continue;
|
||
}
|
||
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
|
||
for (const item of items) {
|
||
const rawPath =
|
||
item?.path ||
|
||
[path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
||
const type = String(item?.type || '').toLowerCase();
|
||
if (type === 'directory' || type === 'folder') {
|
||
queue.push(rawPath);
|
||
continue;
|
||
}
|
||
const ext =
|
||
String(item?.extension || '').toLowerCase() ||
|
||
(rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : '');
|
||
if (exts.has(ext)) {
|
||
results.push({
|
||
name: item?.name || rawPath.split('/').pop() || rawPath,
|
||
path: rawPath
|
||
});
|
||
if (results.length >= 400) {
|
||
return results;
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('遍历文件夹失败', path, error);
|
||
}
|
||
}
|
||
return results;
|
||
},
|
||
|
||
async fetchAllVideoEntries(startPath = '') {
|
||
const queue: string[] = [startPath || ''];
|
||
const visited = new Set<string>();
|
||
const results: Array<{ name: string; path: string }> = [];
|
||
const exts = new Set(['.mp4', '.mov', '.mkv', '.avi', '.webm']);
|
||
const maxFolders = 120;
|
||
|
||
while (queue.length && visited.size < maxFolders) {
|
||
const path = queue.shift() || '';
|
||
if (visited.has(path)) {
|
||
continue;
|
||
}
|
||
visited.add(path);
|
||
try {
|
||
const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, {
|
||
method: 'GET',
|
||
credentials: 'include',
|
||
headers: { Accept: 'application/json' }
|
||
});
|
||
const data = await resp.json().catch(() => null);
|
||
if (!data?.success) {
|
||
continue;
|
||
}
|
||
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
|
||
for (const item of items) {
|
||
const rawPath =
|
||
item?.path ||
|
||
[path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
||
const type = String(item?.type || '').toLowerCase();
|
||
if (type === 'directory' || type === 'folder') {
|
||
queue.push(rawPath);
|
||
continue;
|
||
}
|
||
const ext =
|
||
String(item?.extension || '').toLowerCase() ||
|
||
(rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : '');
|
||
if (exts.has(ext)) {
|
||
results.push({
|
||
name: item?.name || rawPath.split('/').pop() || rawPath,
|
||
path: rawPath
|
||
});
|
||
if (results.length >= 200) {
|
||
return results;
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('遍历文件夹失败', path, error);
|
||
}
|
||
}
|
||
return results;
|
||
},
|
||
|
||
async loadWorkspaceVideos() {
|
||
this.videoLoading = true;
|
||
try {
|
||
const entries = await this.fetchAllVideoEntries('');
|
||
this.videoEntries = entries;
|
||
if (!entries.length) {
|
||
this.uiPushToast({
|
||
title: '未找到视频',
|
||
message: '工作区内没有可用的视频文件',
|
||
type: 'info'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载视频列表失败', error);
|
||
this.uiPushToast({
|
||
title: '加载视频失败',
|
||
message: error?.message || '请稍后重试',
|
||
type: 'error'
|
||
});
|
||
} finally {
|
||
this.videoLoading = false;
|
||
}
|
||
},
|
||
|
||
handleImagesConfirmed(list) {
|
||
this.inputSetSelectedImages(Array.isArray(list) ? list : []);
|
||
this.inputSetImagePickerOpen(false);
|
||
},
|
||
handleRemoveImage(path) {
|
||
this.inputRemoveSelectedImage(path);
|
||
},
|
||
handleVideosConfirmed(list) {
|
||
const arr = Array.isArray(list) ? list.slice(0, 1) : [];
|
||
this.inputSetSelectedVideos(arr);
|
||
this.inputSetVideoPickerOpen(false);
|
||
if (arr.length) {
|
||
this.inputClearSelectedImages();
|
||
}
|
||
},
|
||
handleRemoveVideo(path) {
|
||
this.inputRemoveSelectedVideo(path);
|
||
},
|
||
|
||
handleQuickUpload() {
|
||
if (this.uploading || !this.isConnected) {
|
||
return;
|
||
}
|
||
if (this.isPolicyBlocked('block_upload', '上传功能已被管理员禁用')) {
|
||
return;
|
||
}
|
||
this.triggerFileUpload();
|
||
}
|
||
};
|