agent-Specialization/static/src/app/methods/upload.ts

396 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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();
}
};