// 对话附件(chat_uploads)文件管理 let chatFilesCache = []; /** 后端 GET /api/chat-uploads 返回的目录相对路径(含空文件夹),与 files 合并成树 */ let chatFilesFoldersCache = []; const chatFilesProjectNameById = {}; const chatFilesConversationTitleById = {}; let chatFilesDisplayed = []; let chatFilesEditRelativePath = ''; let chatFilesRenameRelativePath = ''; let chatFilesTotal = 0; let chatFilesPage = 1; let chatFilesPageSize = 20; let chatFilesSearchDebounceTimer = null; const CHAT_FILES_GROUP_STORAGE_KEY = 'csai_chat_files_group_by'; const CHAT_FILES_BROWSE_PATH_KEY = 'csai_chat_files_browse_path'; const CHAT_FILES_PAGE_SIZE_STORAGE_KEY = 'csai_chat_files_page_size'; const CHAT_FILES_TREE_UPLOAD_ROOT = 'uploads'; const CHAT_FILES_TREE_REDUCTION_ROOT = 'tool_outputs'; const CHAT_FILES_TREE_WORKSPACE_ROOT = 'workspace'; const CHAT_FILES_TREE_ARTIFACT_ROOT = 'conversation_artifacts'; /** 按文件夹浏览模式下的当前路径(虚拟根段数组),如 ['uploads','2024-03-21','uuid'] */ let chatFilesBrowsePath = []; /** 非空时,下一次上传文件落到此相对路径(chat_uploads 下目录),如 2026-03-21/uuid/sub */ let chatFilesPendingUploadDir = ''; /** 文件管理页面向服务器上传进行中,避免重复选择并禁用顶栏按钮 */ let chatFilesXHRUploadBusy = false; const CHAT_FILES_FILTER_SELECT_IDS = ['chat-files-filter-source', 'chat-files-group-by']; const chatFilesFilterSelectMap = {}; let chatFilesFilterSelectDocBound = false; const CHAT_FILES_FILTER_SELECT_CARET = ''; function closeAllChatFilesFilterSelects() { Object.keys(chatFilesFilterSelectMap).forEach(function (id) { const reg = chatFilesFilterSelectMap[id]; if (!reg || !reg.wrapper) return; reg.wrapper.classList.remove('open'); if (reg.trigger) reg.trigger.setAttribute('aria-expanded', 'false'); }); } function chatFilesRememberDisplayNames(files) { Object.keys(chatFilesProjectNameById).forEach(function (k) { delete chatFilesProjectNameById[k]; }); Object.keys(chatFilesConversationTitleById).forEach(function (k) { delete chatFilesConversationTitleById[k]; }); (Array.isArray(files) ? files : []).forEach(function (f) { const pid = String((f && f.projectId) || '').trim(); const pname = String((f && f.projectName) || '').trim(); if (pid && pname) chatFilesProjectNameById[pid] = pname; const cid = String((f && f.conversationId) || '').trim(); const ctitle = String((f && f.conversationTitle) || '').trim(); if (cid && ctitle) chatFilesConversationTitleById[cid] = ctitle; }); } function syncChatFilesFilterSelect(selectId) { const reg = chatFilesFilterSelectMap[selectId]; if (!reg) return; const select = reg.select; const dropdown = reg.dropdown; const trigger = reg.trigger; const valueSpan = trigger.querySelector('.chat-files-filter-select-value'); dropdown.innerHTML = ''; Array.prototype.forEach.call(select.options, function (opt) { const item = document.createElement('button'); item.type = 'button'; item.className = 'chat-files-filter-select-option'; item.setAttribute('role', 'option'); item.setAttribute('data-value', opt.value); if (opt.value === select.value) { item.classList.add('is-selected'); item.setAttribute('aria-selected', 'true'); } else { item.setAttribute('aria-selected', 'false'); } const check = document.createElement('span'); check.className = 'chat-files-filter-select-check'; check.setAttribute('aria-hidden', 'true'); check.textContent = '✓'; const label = document.createElement('span'); label.className = 'chat-files-filter-select-label'; label.textContent = opt.textContent; item.appendChild(check); item.appendChild(label); dropdown.appendChild(item); }); const selectedOpt = select.options[select.selectedIndex]; if (valueSpan) { valueSpan.textContent = selectedOpt ? selectedOpt.textContent : ''; } trigger.disabled = !!select.disabled; reg.wrapper.classList.toggle('is-disabled', !!select.disabled); } function syncAllChatFilesFilterSelects() { CHAT_FILES_FILTER_SELECT_IDS.forEach(syncChatFilesFilterSelect); } function enhanceChatFilesFilterSelect(selectId) { const select = document.getElementById(selectId); if (!select) return; const existing = chatFilesFilterSelectMap[selectId]; if (existing && existing.select !== select) { delete chatFilesFilterSelectMap[selectId]; } if (select.dataset.chatFilesCustomSelect === '1') { syncChatFilesFilterSelect(selectId); return; } select.dataset.chatFilesCustomSelect = '1'; select.classList.add('chat-files-filter-native-select'); select.tabIndex = -1; select.setAttribute('aria-hidden', 'true'); const wrapper = document.createElement('div'); wrapper.className = 'chat-files-filter-select-ui'; const trigger = document.createElement('button'); trigger.type = 'button'; trigger.className = 'chat-files-filter-select-trigger'; trigger.setAttribute('aria-haspopup', 'listbox'); trigger.setAttribute('aria-expanded', 'false'); const valueSpan = document.createElement('span'); valueSpan.className = 'chat-files-filter-select-value'; trigger.appendChild(valueSpan); trigger.insertAdjacentHTML('beforeend', CHAT_FILES_FILTER_SELECT_CARET); const dropdown = document.createElement('div'); dropdown.className = 'chat-files-filter-select-dropdown'; dropdown.setAttribute('role', 'listbox'); const parent = select.parentNode; parent.insertBefore(wrapper, select); wrapper.appendChild(trigger); wrapper.appendChild(dropdown); wrapper.appendChild(select); chatFilesFilterSelectMap[selectId] = { wrapper: wrapper, trigger: trigger, dropdown: dropdown, select: select }; trigger.addEventListener('click', function (e) { e.stopPropagation(); if (select.disabled) return; const open = wrapper.classList.contains('open'); closeAllChatFilesFilterSelects(); if (!open) { wrapper.classList.add('open'); trigger.setAttribute('aria-expanded', 'true'); } }); dropdown.addEventListener('click', function (e) { const opt = e.target.closest('.chat-files-filter-select-option'); if (!opt) return; e.stopPropagation(); const val = opt.getAttribute('data-value'); if (val === null) return; if (select.value !== val) { select.value = val; select.dispatchEvent(new Event('change', { bubbles: true })); } wrapper.classList.remove('open'); trigger.setAttribute('aria-expanded', 'false'); syncChatFilesFilterSelect(selectId); }); select.addEventListener('change', function () { syncChatFilesFilterSelect(selectId); }); if (!select.dataset.chatFilesFilterBound) { select.dataset.chatFilesFilterBound = '1'; select.addEventListener('change', chatFilesGroupByChange); } syncChatFilesFilterSelect(selectId); } function initChatFilesFilterSelects() { if (!chatFilesFilterSelectDocBound) { document.addEventListener('click', closeAllChatFilesFilterSelects); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAllChatFilesFilterSelects(); }); chatFilesFilterSelectDocBound = true; } CHAT_FILES_FILTER_SELECT_IDS.forEach(enhanceChatFilesFilterSelect); syncAllChatFilesFilterSelects(); } function chatFilesLoadBrowsePathFromStorage() { try { const raw = localStorage.getItem(CHAT_FILES_BROWSE_PATH_KEY); if (!raw) { chatFilesBrowsePath = []; return; } const p = JSON.parse(raw); if (Array.isArray(p) && p.every(function (x) { return typeof x === 'string'; })) { chatFilesBrowsePath = p; } } catch (e) { chatFilesBrowsePath = []; } } function chatFilesSetBrowsePath(path) { chatFilesBrowsePath = path.slice(); try { localStorage.setItem(CHAT_FILES_BROWSE_PATH_KEY, JSON.stringify(chatFilesBrowsePath)); } catch (e) { /* ignore */ } } function chatFilesLoadPageSizeFromStorage() { try { const n = parseInt(localStorage.getItem(CHAT_FILES_PAGE_SIZE_STORAGE_KEY) || '', 10); if ([10, 20, 50, 100].includes(n)) { chatFilesPageSize = n; } } catch (e) { /* ignore */ } } function chatFilesResolveTreeNode(root, path) { let node = root; let i; for (i = 0; i < path.length; i++) { const seg = path[i]; if (!node.dirs[seg]) return null; node = node.dirs[seg]; } return node; } function chatFilesNormalizeBrowsePathForTree(root) { let path = chatFilesBrowsePath.slice(); while (path.length > 0 && !chatFilesResolveTreeNode(root, path)) { path.pop(); } if (path.length !== chatFilesBrowsePath.length) { chatFilesSetBrowsePath(path); } } function initChatFilesPage() { chatFilesLoadBrowsePathFromStorage(); chatFilesLoadPageSizeFromStorage(); try { localStorage.removeItem('csai_chat_files_synthetic_dirs'); } catch (e) { /* ignore */ } ensureChatFilesDocClickClose(); const sel = document.getElementById('chat-files-group-by'); if (sel) { try { const v = localStorage.getItem(CHAT_FILES_GROUP_STORAGE_KEY); if (v === 'none' || v === 'date' || v === 'conversation' || v === 'project' || v === 'folder') { sel.value = v; } } catch (e) { /* ignore */ } } initChatFilesFilterSelects(); setupChatFilesDragDrop(); loadChatFilesPage(); } function chatFilesCloseAllMenus() { document.querySelectorAll('.chat-files-dropdown').forEach((el) => { el.hidden = true; el.style.position = ''; el.style.left = ''; el.style.top = ''; el.style.right = ''; el.style.minWidth = ''; el.style.zIndex = ''; el.classList.remove('chat-files-dropdown-fixed'); }); } /** * 「更多」菜单使用 fixed 定位,避免表格外层 overflow 把菜单裁成一条细线。 */ function chatFilesToggleMoreMenu(ev, idx) { if (ev) ev.stopPropagation(); const menu = document.getElementById('chat-files-menu-' + idx); const btn = ev && ev.currentTarget; if (!menu) return; const opening = menu.hidden; chatFilesCloseAllMenus(); if (!opening) return; menu.hidden = false; menu.classList.add('chat-files-dropdown-fixed'); if (!btn || typeof btn.getBoundingClientRect !== 'function') return; requestAnimationFrame(() => { const r = btn.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; const margin = 8; const minW = 220; menu.style.boxSizing = 'border-box'; menu.style.position = 'fixed'; menu.style.zIndex = '5000'; menu.style.minWidth = minW + 'px'; menu.style.right = 'auto'; const w = Math.max(minW, menu.offsetWidth || minW); let left = r.right - w; if (left < margin) left = margin; if (left + w > vw - margin) left = Math.max(margin, vw - margin - w); menu.style.left = left + 'px'; const gap = 6; let top = r.bottom + gap; const estH = menu.offsetHeight || 120; if (top + estH > vh - margin && r.top - gap - estH >= margin) { top = r.top - gap - estH; } menu.style.top = top + 'px'; }); } window.chatFilesCloseAllMenus = chatFilesCloseAllMenus; window.chatFilesToggleMoreMenu = chatFilesToggleMoreMenu; function ensureChatFilesDocClickClose() { if (window.__chatFilesDocClose) return; window.__chatFilesDocClose = true; document.addEventListener('click', function (ev) { if (ev.target.closest && ev.target.closest('.chat-files-dropdown-wrap')) return; chatFilesCloseAllMenus(); }); document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') chatFilesCloseAllMenus(); }); window.addEventListener( 'scroll', function () { chatFilesCloseAllMenus(); }, true ); window.addEventListener('resize', function () { chatFilesCloseAllMenus(); }); } async function loadChatFilesPage() { const wrap = document.getElementById('chat-files-list-wrap'); if (!wrap) return; const pager = document.getElementById('chat-files-pagination'); if (pager) pager.hidden = true; wrap.classList.remove('chat-files-table-wrap--grouped'); wrap.classList.remove('chat-files-table-wrap--tree'); wrap.innerHTML = '
加载中…
'; if (typeof window.applyTranslations === 'function') { window.applyTranslations(wrap); } const conv = document.getElementById('chat-files-filter-conv'); const convQ = conv ? conv.value.trim() : ''; const project = document.getElementById('chat-files-filter-project'); const projectQ = project ? project.value.trim() : ''; const source = document.getElementById('chat-files-filter-source'); const sourceQ = source ? source.value : 'all'; const search = document.getElementById('chat-files-filter-name'); const searchQ = search ? search.value.trim() : ''; const groupMode = chatFilesGetGroupByMode(); const params = new URLSearchParams(); if (convQ) { params.set('conversation', convQ); } if (projectQ) { params.set('project', projectQ); } if (sourceQ && sourceQ !== 'all') { params.set('source', sourceQ); } if (searchQ) { params.set('search', searchQ); } params.set('page', String(chatFilesPage)); params.set('pageSize', groupMode === 'folder' ? 'all' : String(chatFilesPageSize)); let url = '/api/chat-uploads'; const query = params.toString(); if (query) url += '?' + query; try { const res = await apiFetch(url); if (!res.ok) { const t = await res.text(); throw new Error(t || res.status); } const data = await res.json(); chatFilesCache = Array.isArray(data.files) ? data.files : []; chatFilesRememberDisplayNames(chatFilesCache); chatFilesFoldersCache = Array.isArray(data.folders) ? data.folders : []; chatFilesTotal = Number.isFinite(Number(data.total)) ? Number(data.total) : chatFilesCache.length; chatFilesPage = Number.isFinite(Number(data.page)) ? Math.max(1, Number(data.page)) : chatFilesPage; if (Number.isFinite(Number(data.pageSize)) && Number(data.pageSize) > 0) { chatFilesPageSize = Number(data.pageSize); } if (groupMode !== 'folder' && chatFilesTotal > 0 && chatFilesCache.length === 0 && chatFilesPage > chatFilesTotalPages()) { chatFilesPage = chatFilesTotalPages(); loadChatFilesPage(); return; } renderChatFilesTable(); } catch (e) { console.error(e); wrap.classList.remove('chat-files-table-wrap--grouped'); wrap.classList.remove('chat-files-table-wrap--tree'); const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.errorLoad') : '加载失败'; wrap.innerHTML = '
' + escapeHtml(msg + ': ' + (e.message || String(e))) + '
'; renderChatFilesPagination(); } } async function exportChatFiles() { const conv = document.getElementById('chat-files-filter-conv'); const project = document.getElementById('chat-files-filter-project'); const convQ = conv ? conv.value.trim() : ''; const projectQ = project ? project.value.trim() : ''; const source = document.getElementById('chat-files-filter-source'); const sourceQ = source ? source.value : 'all'; const search = document.getElementById('chat-files-filter-name'); const searchQ = search ? search.value.trim() : ''; const params = new URLSearchParams(); if (convQ) params.set('conversation', convQ); if (projectQ) params.set('project', projectQ); if (sourceQ && sourceQ !== 'all') params.set('source', sourceQ); if (searchQ) params.set('search', searchQ); const url = '/api/chat-uploads/export' + (params.toString() ? ('?' + params.toString()) : ''); try { const res = await apiFetch(url); if (!res.ok) { const raw = await res.text(); let msg = raw; try { const j = JSON.parse(raw); if (j && j.error) msg = j.error; } catch (e) { /* keep raw */ } if (res.status === 404) { msg = (typeof window.t === 'function') ? window.t('chatFilesPage.exportEmpty') : '没有可导出的文件'; } throw new Error(msg || String(res.status)); } const blob = await res.blob(); const disposition = res.headers.get('Content-Disposition') || ''; let filename = 'chat-files-export.zip'; const m = disposition.match(/filename="?([^"]+)"?/i); if (m && m[1]) filename = m[1]; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); const ok = (typeof window.t === 'function') ? window.t('chatFilesPage.exportStarted') : '已开始导出'; chatFilesShowToast(ok); } catch (e) { alert((e && e.message) ? e.message : String(e)); } } function chatFilesNameFilter(files) { return Array.isArray(files) ? files : []; } function chatFilesResetToFirstPageAndLoad() { chatFilesPage = 1; loadChatFilesPage(); } function chatFilesFilterNameOnInput() { if (chatFilesSearchDebounceTimer) clearTimeout(chatFilesSearchDebounceTimer); chatFilesSearchDebounceTimer = setTimeout(function () { chatFilesSearchDebounceTimer = null; chatFilesResetToFirstPageAndLoad(); }, 250); } function chatFilesTotalPages() { if (chatFilesGetGroupByMode() === 'folder') return 1; return Math.max(1, Math.ceil((chatFilesTotal || 0) / Math.max(1, chatFilesPageSize))); } function renderChatFilesPagination() { const pager = document.getElementById('chat-files-pagination'); if (!pager) return; const groupMode = chatFilesGetGroupByMode(); if (groupMode === 'folder' || chatFilesTotal <= 0) { pager.hidden = true; pager.innerHTML = ''; return; } const totalPages = chatFilesTotalPages(); if (chatFilesPage > totalPages) { chatFilesPage = totalPages; } const start = (chatFilesPage - 1) * chatFilesPageSize + 1; const end = Math.min(chatFilesTotal, chatFilesPage * chatFilesPageSize); const info = (typeof window.t === 'function') ? window.t('chatFilesPage.paginationInfo', { start: start, end: end, total: chatFilesTotal }) : ('显示 ' + start + '-' + end + ' / ' + chatFilesTotal); const pageText = (typeof window.t === 'function') ? window.t('chatFilesPage.paginationPage', { page: chatFilesPage, totalPages: totalPages }) : ('第 ' + chatFilesPage + ' / ' + totalPages + ' 页'); const pageSizeLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.pageSize') : '每页'; const prevLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.prevPage') : '上一页'; const nextLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.nextPage') : '下一页'; const sizes = [10, 20, 50, 100].map(function (n) { return ''; }).join(''); pager.innerHTML = `
${escapeHtml(info)}
${escapeHtml(pageText)}
`; pager.hidden = false; } function changeChatFilesPage(page) { const totalPages = chatFilesTotalPages(); const next = Math.min(totalPages, Math.max(1, parseInt(page, 10) || 1)); if (next === chatFilesPage) return; chatFilesPage = next; loadChatFilesPage(); } function changeChatFilesPageSize(value) { const n = parseInt(value, 10); if (![10, 20, 50, 100].includes(n)) return; chatFilesPageSize = n; chatFilesPage = 1; try { localStorage.setItem(CHAT_FILES_PAGE_SIZE_STORAGE_KEY, String(n)); } catch (e) { /* ignore */ } loadChatFilesPage(); } function formatChatFileBytes(n) { if (n < 1024) return n + ' B'; if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'; return (n / (1024 * 1024)).toFixed(1) + ' MB'; } function chatFilesShowToast(message) { const el = document.createElement('div'); el.className = 'chat-files-toast'; el.setAttribute('role', 'status'); el.textContent = message; document.body.appendChild(el); requestAnimationFrame(() => el.classList.add('chat-files-toast-visible')); setTimeout(() => { el.classList.remove('chat-files-toast-visible'); setTimeout(() => el.remove(), 300); }, 2200); } async function chatFilesCopyText(text) { try { if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { await navigator.clipboard.writeText(text); return true; } } catch (e) { /* fall through */ } try { const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); return ok; } catch (e2) { return false; } } async function copyChatFilePathIdx(idx) { const f = chatFilesDisplayed[idx]; if (!f) return; const text = (f.absolutePath && String(f.absolutePath).trim()) ? String(f.absolutePath).trim() : ('chat_uploads/' + String(f.relativePath || '').replace(/^\/+/, '')); const ok = await chatFilesCopyText(text); if (ok) { const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.pathCopied') : '路径已复制,可粘贴到对话中引用'; chatFilesShowToast(msg); } else { const fail = (typeof window.t === 'function') ? window.t('common.copyFailed') : '复制失败'; alert(fail); } } /** 常见二进制扩展名:此类文件无法在纯文本编辑器中打开 */ const CHAT_FILES_BINARY_EXT = new Set([ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'tif', 'tiff', 'heic', 'heif', 'svgz', 'pdf', 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'zst', 'mp3', 'm4a', 'wav', 'ogg', 'flac', 'aac', 'mp4', 'avi', 'mkv', 'mov', 'wmv', 'webm', 'm4v', 'exe', 'dll', 'so', 'dylib', 'bin', 'app', 'dmg', 'pkg', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'sqlite', 'db', 'sqlite3', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'class', 'jar', 'war', 'apk', 'ipa', 'iso', 'img' ]); function chatFileIsBinaryByName(fileName) { if (!fileName || typeof fileName !== 'string') return false; const i = fileName.lastIndexOf('.'); if (i < 0 || i === fileName.length - 1) return false; const ext = fileName.slice(i + 1).toLowerCase(); return CHAT_FILES_BINARY_EXT.has(ext); } function chatFilesEditBlockedHint() { return (typeof window.t === 'function') ? window.t('chatFilesPage.editBinaryHint') : '图片、压缩包等二进制文件无法在此以文本方式编辑,请使用「下载」。'; } function chatFilesAlertMessage(raw) { const s = (raw == null) ? '' : String(raw).trim(); const lower = s.toLowerCase(); if (lower.includes('binary file not editable') || lower.includes('binary')) { return chatFilesEditBlockedHint(); } if (lower.includes('file too large') || lower.includes('entity too large') || lower.includes('413')) { return (typeof window.t === 'function') ? window.t('chatFilesPage.editTooLarge') : '文件过大,无法在此编辑。'; } return s || ((typeof window.t === 'function') ? window.t('chatFilesPage.errorGeneric') : '操作失败'); } function chatFilesGetGroupByMode() { const sel = document.getElementById('chat-files-group-by'); const v = sel ? sel.value : 'none'; if (v === 'date' || v === 'conversation' || v === 'project' || v === 'folder') return v; return 'none'; } function chatFilesGroupByChange() { const sel = document.getElementById('chat-files-group-by'); if (sel) { try { localStorage.setItem(CHAT_FILES_GROUP_STORAGE_KEY, sel.value); } catch (e) { /* ignore */ } } chatFilesResetToFirstPageAndLoad(); } function chatFilesCompareDateKeysDesc(a, b) { const as = String(a); const bs = String(b); if (as === '—' && bs !== '—') return 1; if (bs === '—' && as !== '—') return -1; return bs.localeCompare(as); } /** 目录树节点:dirs[段名] -> 子节点;files: { idx, name }[] */ function chatFilesTreeMakeNode() { return { dirs: {}, files: [] }; } function chatFilesTreeInsertFile(root, f, idx) { const rp = chatFilesTreePathForFile(f); if (!rp) return; const parts = rp.split('/').filter(function (p) { return p.length > 0; }); if (parts.length < 2) return; let node = root; for (let i = 0; i < parts.length - 1; i++) { const seg = parts[i]; if (!node.dirs[seg]) node.dirs[seg] = chatFilesTreeMakeNode(); node = node.dirs[seg]; } node.files.push({ idx: idx, name: parts[parts.length - 1] }); } function chatFilesBuildTree(files) { const root = chatFilesTreeMakeNode(); files.forEach(function (f, idx) { chatFilesTreeInsertFile(root, f, idx); }); return root; } function chatFilesTreePathForFile(f) { const source = f && f.source; let rp = String((f && f.relativePath) || '').replace(/\\/g, '/').replace(/^\/+/, ''); if (!rp) return ''; if (source === 'reduction') { rp = rp.replace(/^__reduction__\//, ''); return CHAT_FILES_TREE_REDUCTION_ROOT + '/' + rp; } if (source === 'workspace') { rp = rp.replace(/^__workspace__\//, ''); return CHAT_FILES_TREE_WORKSPACE_ROOT + '/' + rp; } if (source === 'conversation_artifact') { rp = rp.replace(/^__conversation_artifact__\//, ''); return CHAT_FILES_TREE_ARTIFACT_ROOT + '/' + rp; } return CHAT_FILES_TREE_UPLOAD_ROOT + '/' + rp; } /** 将后端返回的目录相对路径(如 a/b/c)并入树,便于展示空文件夹 */ function chatFilesTreeInsertFolderPath(root, relSlash) { const rp = String(relSlash || '').replace(/\\/g, '/').replace(/^\/+/, ''); if (!rp) return; const parts = rp.split('/').filter(function (p) { return p.length > 0; }); if (!parts.length) return; let node = root; let i; for (i = 0; i < parts.length; i++) { const seg = parts[i]; if (!node.dirs[seg]) node.dirs[seg] = chatFilesTreeMakeNode(); node = node.dirs[seg]; } } function chatFilesMergeFoldersIntoTree(root, folderPaths) { if (!Array.isArray(folderPaths)) return; if (!chatFilesShouldMergeUploadFolders()) return; let i; for (i = 0; i < folderPaths.length; i++) { chatFilesTreeInsertFolderPath(root, CHAT_FILES_TREE_UPLOAD_ROOT + '/' + folderPaths[i]); } } function chatFilesTreeRootMerged() { const root = chatFilesBuildTree(chatFilesDisplayed); chatFilesMergeFoldersIntoTree(root, chatFilesFoldersCache); chatFilesEnsureSourceRoots(root); return root; } function chatFilesCurrentSourceFilter() { const el = document.getElementById('chat-files-filter-source'); return el ? (el.value || 'all') : 'all'; } function chatFilesCurrentSearchQuery() { const el = document.getElementById('chat-files-filter-name'); return el ? String(el.value || '').trim() : ''; } function chatFilesShouldMergeUploadFolders() { const source = chatFilesCurrentSourceFilter(); return (source === 'all' || source === 'upload') && !chatFilesCurrentSearchQuery(); } function chatFilesEnsureSourceRoots(root) { const source = chatFilesCurrentSourceFilter(); const roots = []; if (source === 'all' || source === 'upload') roots.push(CHAT_FILES_TREE_UPLOAD_ROOT); if (source === 'all' || source === 'reduction') roots.push(CHAT_FILES_TREE_REDUCTION_ROOT); if (source === 'all' || source === 'workspace') roots.push(CHAT_FILES_TREE_WORKSPACE_ROOT); if (source === 'all' || source === 'conversation_artifact') roots.push(CHAT_FILES_TREE_ARTIFACT_ROOT); roots.forEach(function (name) { if (!root.dirs[name]) root.dirs[name] = chatFilesTreeMakeNode(); }); } function chatFilesIsInternalSource(f) { const source = f && f.source; return source === 'reduction' || source === 'workspace' || source === 'conversation_artifact'; } function chatFilesSourceLabel(source) { if (source === 'reduction') { return (typeof window.t === 'function') ? window.t('chatFilesPage.sourceReduction') : '工具输出'; } if (source === 'workspace') { return (typeof window.t === 'function') ? window.t('chatFilesPage.sourceWorkspace') : '工作目录'; } if (source === 'conversation_artifact') { return (typeof window.t === 'function') ? window.t('chatFilesPage.sourceConversationArtifact') : '会话产物'; } return (typeof window.t === 'function') ? window.t('chatFilesPage.sourceUpload') : '对话附件'; } function chatFilesBrowseCanMutateCurrentPath() { return chatFilesBrowsePath[0] === CHAT_FILES_TREE_UPLOAD_ROOT; } function chatFilesBrowseCanUploadToPath(path) { return Array.isArray(path) && path[0] === CHAT_FILES_TREE_UPLOAD_ROOT; } function chatFilesBrowseCanDeleteFolderPath(path) { return Array.isArray(path) && path[0] === CHAT_FILES_TREE_UPLOAD_ROOT && path.length > 1; } function chatFilesTreeDisplayName(name) { if (name === CHAT_FILES_TREE_UPLOAD_ROOT) { return (typeof window.t === 'function') ? window.t('chatFilesPage.treeUploadsRoot') : '对话附件'; } if (name === CHAT_FILES_TREE_REDUCTION_ROOT) { return (typeof window.t === 'function') ? window.t('chatFilesPage.treeReductionRoot') : '工具输出'; } if (name === CHAT_FILES_TREE_WORKSPACE_ROOT) { return (typeof window.t === 'function') ? window.t('chatFilesPage.treeWorkspaceRoot') : '工作目录'; } if (name === CHAT_FILES_TREE_ARTIFACT_ROOT) { return (typeof window.t === 'function') ? window.t('chatFilesPage.treeArtifactsRoot') : '会话产物'; } return name; } function chatFilesIDDisplay(id, labelMap, emptyLabel) { const raw = id == null ? '' : String(id); if (!raw || raw === '—') { return { text: emptyLabel || '—', title: '' }; } const label = String((labelMap && labelMap[raw]) || '').trim(); if (label) { return { text: label, title: label + ' (' + raw + ')' }; } if (raw.length > 36) { return { text: raw.slice(0, 8) + '…' + raw.slice(-6), title: raw }; } return { text: raw, title: raw }; } function chatFilesConversationDisplay(id) { const c = id == null ? '' : String(id); if (typeof window.t === 'function') { if (c === '_manual') { return { text: window.t('chatFilesPage.convManual'), title: '_manual' }; } if (c === '_new') { return { text: window.t('chatFilesPage.convNew'), title: '_new' }; } } return chatFilesIDDisplay(c, chatFilesConversationTitleById); } function chatFilesProjectDisplay(id) { const empty = (typeof window.t === 'function') ? window.t('chatFilesPage.projectUnbound') : '未绑定项目'; return chatFilesIDDisplay(id, chatFilesProjectNameById, empty); } function chatFilesTreePathDisplayName(pathParts) { const parts = Array.isArray(pathParts) ? pathParts : []; const name = parts.length ? parts[parts.length - 1] : ''; const root = parts[0] || ''; if ((root === CHAT_FILES_TREE_WORKSPACE_ROOT || root === CHAT_FILES_TREE_REDUCTION_ROOT) && parts.length === 3) { if (parts[1] === 'projects') return chatFilesProjectDisplay(name); if (parts[1] === 'conversations') return chatFilesConversationDisplay(name); } if (root === CHAT_FILES_TREE_ARTIFACT_ROOT && parts.length === 2) { return chatFilesConversationDisplay(name); } if (root === CHAT_FILES_TREE_UPLOAD_ROOT && parts.length === 3) { return chatFilesConversationDisplay(name); } const text = chatFilesTreeDisplayName(name); return { text: text, title: String(name || '') }; } function chatFilesUploadRelativeDirFromBrowsePath(path) { const parts = Array.isArray(path) ? path.slice() : []; if (parts[0] === CHAT_FILES_TREE_UPLOAD_ROOT) { parts.shift(); } return parts.join('/'); } function chatFilesTreeNodeMaxMod(node) { let m = 0; let i; for (i = 0; i < node.files.length; i++) { const f = chatFilesDisplayed[node.files[i].idx]; m = Math.max(m, (f && f.modifiedUnix) || 0); } const keys = Object.keys(node.dirs); for (i = 0; i < keys.length; i++) { m = Math.max(m, chatFilesTreeNodeMaxMod(node.dirs[keys[i]])); } return m; } function chatFilesTreeSortDirKeys(node, keys) { return keys.slice().sort(function (a, b) { const ma = chatFilesTreeNodeMaxMod(node.dirs[a]); const mb = chatFilesTreeNodeMaxMod(node.dirs[b]); if (mb !== ma) return mb - ma; return String(a).localeCompare(String(b)); }); } function chatFilesBuildGroups(files, mode) { const map = new Map(); files.forEach(function (f, idx) { let key; if (mode === 'date') { key = f.date || '—'; } else if (mode === 'project') { key = f.projectId || '—'; } else { key = f.conversationId || '—'; } if (!map.has(key)) { map.set(key, { key: key, items: [] }); } map.get(key).items.push({ idx: idx, f: f }); }); const groups = Array.from(map.values()); groups.forEach(function (g) { g.items.sort(function (a, b) { return (b.f.modifiedUnix || 0) - (a.f.modifiedUnix || 0); }); }); if (mode === 'date') { groups.sort(function (a, b) { return chatFilesCompareDateKeysDesc(a.key, b.key); }); } else { groups.sort(function (a, b) { const ma = Math.max.apply( null, a.items.map(function (x) { return x.f.modifiedUnix || 0; }) ); const mb = Math.max.apply( null, b.items.map(function (x) { return x.f.modifiedUnix || 0; }) ); return mb - ma; }); } return groups; } /** 分组标题:长 ID 缩短展示,完整值放在 title */ function chatFilesGroupHeadingID(key, emptyLabel) { return chatFilesIDDisplay(key, null, emptyLabel); } function chatFilesGroupHeadingConversation(key) { return chatFilesConversationDisplay(key); } function chatFilesGroupHeadingProject(key) { return chatFilesProjectDisplay(key); } function renderChatFilesTable() { const wrap = document.getElementById('chat-files-list-wrap'); if (!wrap) return; chatFilesDisplayed = chatFilesNameFilter(chatFilesCache); const groupMode = chatFilesGetGroupByMode(); const emptyMsg = (typeof window.t === 'function') ? window.t('chatFilesPage.empty') : '暂无文件'; // 「按文件夹」模式下即使尚无文件,也要显示 chat_uploads 路径栏与「新建文件夹」,否则无法先建目录 if (!chatFilesDisplayed.length && groupMode !== 'folder') { wrap.classList.remove('chat-files-table-wrap--grouped'); wrap.classList.remove('chat-files-table-wrap--tree'); wrap.innerHTML = '
' + escapeHtml(emptyMsg) + '
'; if (typeof window.applyTranslations === 'function') { window.applyTranslations(wrap); } renderChatFilesPagination(); return; } const thDate = (typeof window.t === 'function') ? window.t('chatFilesPage.colDate') : '日期'; const thConv = (typeof window.t === 'function') ? window.t('chatFilesPage.colConversation') : '会话'; const thSubPath = (typeof window.t === 'function') ? window.t('chatFilesPage.colSubPath') : '子路径'; const thName = (typeof window.t === 'function') ? window.t('chatFilesPage.colName') : '文件名'; const thSize = (typeof window.t === 'function') ? window.t('chatFilesPage.colSize') : '大小'; const thModified = (typeof window.t === 'function') ? window.t('chatFilesPage.colModified') : '修改时间'; const thActions = (typeof window.t === 'function') ? window.t('chatFilesPage.colActions') : '操作'; const svgCopy = ''; const svgDownload = ''; const svgMore = ''; const svgFolder = ''; const svgFile = ''; const tCopyTitle = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.copyPathTitle') : '复制服务器上的绝对路径,可粘贴到对话中引用'); const tDlTitle = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.download') : '下载'); const tMoreTitle = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.moreActions') : '更多操作'); function rowHtml(f, idx) { const rp = f.relativePath || ''; const pathForTitle = (f.absolutePath && String(f.absolutePath).trim()) ? String(f.absolutePath).trim() : rp; const nameEsc = escapeHtml(f.name || ''); const isInternal = chatFilesIsInternalSource(f); const sourceBadge = isInternal ? '' + escapeHtml(chatFilesSourceLabel(f.source)) + '' : ''; const conv = f.conversationId || ''; const convDisplay = chatFilesConversationDisplay(conv); const convEsc = escapeHtml(convDisplay.text || conv); const convTitleEsc = escapeHtml(convDisplay.title || conv); const dt = f.modifiedUnix ? new Date(f.modifiedUnix * 1000).toLocaleString() : '—'; const canOpenChat = conv && conv !== '_manual' && conv !== '_new'; const bin = chatFileIsBinaryByName(f.name); const editHint = escapeHtml(chatFilesEditBlockedHint()); const editUnavailable = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.editUnavailable')) : '不可编辑'; const tEdit = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.edit')) : '编辑'; const tOpenChat = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.openChat')) : '打开对话'; const tRename = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.rename')) : '重命名'; const tDelete = (typeof window.t === 'function') ? escapeHtml(window.t('common.delete')) : '删除'; const menuParts = []; if (canOpenChat) { menuParts.push(``); } if (isInternal) { menuParts.push(`
${escapeHtml(chatFilesSourceLabel(f.source))}
`); } else if (!bin) { menuParts.push(``); } else { menuParts.push(`
${editUnavailable}
`); } if (!isInternal) { menuParts.push(``); menuParts.push(``); } const menuHtml = menuParts.join(''); const subRaw = (f.subPath && String(f.subPath).trim()) ? String(f.subPath).trim() : ''; const rootLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.folderRoot') : '(根目录)'; let subCellInner; if (subRaw) { const segs = subRaw.split('/').filter(function (s) { return s.length > 0; }); subCellInner = '' + segs.map(function (seg, i) { return (i > 0 ? '' : '') + '' + escapeHtml(seg) + ''; }).join('') + ''; } else { subCellInner = '' + escapeHtml(rootLabel) + ''; } return ` ${escapeHtml(f.date || '—')} ${convEsc} ${subCellInner} ${nameEsc}${sourceBadge} ${formatChatFileBytes(f.size || 0)} ${escapeHtml(dt)}
`; } const theadHtml = ` ${escapeHtml(thDate)} ${escapeHtml(thConv)} ${escapeHtml(thSubPath)} ${escapeHtml(thName)} ${escapeHtml(thSize)} ${escapeHtml(thModified)} ${escapeHtml(thActions)} `; const theadCompact = ` ${escapeHtml(thName)} ${escapeHtml(thSize)} ${escapeHtml(thModified)} ${escapeHtml(thActions)} `; let innerHtml; if (groupMode === 'folder') { const root = chatFilesTreeRootMerged(); chatFilesNormalizeBrowsePathForTree(root); const node = chatFilesResolveTreeNode(root, chatFilesBrowsePath); const current = node || root; const dirKeys = chatFilesTreeSortDirKeys(current, Object.keys(current.dirs)); current.files.sort(function (a, b) { return (chatFilesDisplayed[b.idx].modifiedUnix || 0) - (chatFilesDisplayed[a.idx].modifiedUnix || 0); }); const tRoot = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseRoot') : '文件'); const tUp = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseUp') : '上级'); const tMkdir = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.newFolderButton') : '新建文件夹'); const tEmpty = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.folderEmpty') : '此文件夹为空'); const tCopyFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.copyFolderPathTitle') : '复制目录路径'); const tEnter = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.enterFolderTitle') : '进入'); let breadcrumbHtml = ''; const canMutateCurrentPath = chatFilesBrowseCanMutateCurrentPath(); const upDisabled = chatFilesBrowsePath.length === 0 ? ' disabled' : ''; const mkdirButton = canMutateCurrentPath ? '' : ''; const toolbarHtml = '
' + breadcrumbHtml + mkdirButton + '
'; const svgTrash = ''; const svgUploadToFolder = ''; const tDeleteFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.deleteFolderTitle') : '删除文件夹'); const tUploadToFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.uploadToFolderTitle') : '上传到此文件夹'); function rowHtmlBrowseFolder(name) { const nameAttr = encodeURIComponent(String(name)); const folderPath = chatFilesBrowsePath.concat([name]); const folderDisplay = chatFilesTreePathDisplayName(folderPath); const folderTitle = folderDisplay.title || tEnter; const relToFolder = folderPath.join('/'); const uploadDirAttr = encodeURIComponent(relToFolder); const canUploadFolder = chatFilesBrowseCanUploadToPath(folderPath); const canDeleteFolder = chatFilesBrowseCanDeleteFolderPath(folderPath); const uploadBtn = canUploadFolder ? `` : ''; const deleteBtn = canDeleteFolder ? `` : ''; return ` ${svgFolder}${escapeHtml(folderDisplay.text)} — —
${uploadBtn} ${deleteBtn}
`; } function rowHtmlTreeFile(f, idx) { const pathForTitle = (f.absolutePath && String(f.absolutePath).trim()) ? String(f.absolutePath).trim() : (f.relativePath || ''); const nameEsc = escapeHtml(f.name || ''); const isInternal = chatFilesIsInternalSource(f); const sourceBadge = isInternal ? '' + escapeHtml(chatFilesSourceLabel(f.source)) + '' : ''; const dt = f.modifiedUnix ? new Date(f.modifiedUnix * 1000).toLocaleString() : '—'; const conv = f.conversationId || ''; const canOpenChat = conv && conv !== '_manual' && conv !== '_new'; const bin = chatFileIsBinaryByName(f.name); const editHint = escapeHtml(chatFilesEditBlockedHint()); const editUnavailable = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.editUnavailable')) : '不可编辑'; const tEdit = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.edit')) : '编辑'; const tOpenChat = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.openChat')) : '打开对话'; const tRename = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.rename')) : '重命名'; const tDelete = (typeof window.t === 'function') ? escapeHtml(window.t('common.delete')) : '删除'; const menuParts = []; if (canOpenChat) { menuParts.push(``); } if (isInternal) { menuParts.push(`
${escapeHtml(chatFilesSourceLabel(f.source))}
`); } else if (!bin) { menuParts.push(``); } else { menuParts.push(`
${editUnavailable}
`); } if (!isInternal) { menuParts.push(``); menuParts.push(``); } const menuHtml = menuParts.join(''); return ` ${svgFile}${nameEsc}${sourceBadge} ${formatChatFileBytes(f.size || 0)} ${escapeHtml(dt)}
`; } const folderRows = dirKeys.map(rowHtmlBrowseFolder).join(''); const fileRows = current.files.map(function (item) { return rowHtmlTreeFile(chatFilesDisplayed[item.idx], item.idx); }).join(''); let bodyRows = folderRows + fileRows; if (!bodyRows) { bodyRows = '' + tEmpty + ''; } innerHtml = '
' + toolbarHtml + '' + theadCompact + '' + bodyRows + '
'; } else if (groupMode === 'none') { const rows = chatFilesDisplayed.map(function (f, idx) { return rowHtml(f, idx); }).join(''); innerHtml = `${theadHtml}${rows}
`; } else { const groups = chatFilesBuildGroups(chatFilesDisplayed, groupMode); const blocks = groups.map(function (g) { const rows = g.items.map(function (item) { return rowHtml(item.f, item.idx); }).join(''); let summaryMain; let summaryTitleAttr = ''; if (groupMode === 'date') { summaryMain = escapeHtml(String(g.key)); } else if (groupMode === 'project') { const h = chatFilesGroupHeadingProject(g.key); summaryMain = escapeHtml(h.text); summaryTitleAttr = h.title ? ' title="' + escapeHtml(h.title) + '"' : ''; } else { const h = chatFilesGroupHeadingConversation(g.key); summaryMain = escapeHtml(h.text); summaryTitleAttr = h.title ? ' title="' + escapeHtml(h.title) + '"' : ''; } const n = g.items.length; const countLabel = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.groupCount', { count: n })) : escapeHtml(String(n)); return `
${summaryMain} ${countLabel}
${theadHtml}${rows}
`; }).join(''); innerHtml = `
${blocks}
`; } ensureChatFilesDocClickClose(); wrap.innerHTML = innerHtml; wrap.classList.toggle('chat-files-table-wrap--grouped', groupMode !== 'none' && groupMode !== 'folder'); wrap.classList.toggle('chat-files-table-wrap--tree', groupMode === 'folder'); renderChatFilesPagination(); } window.chatFilesGroupByChange = chatFilesGroupByChange; window.changeChatFilesPage = changeChatFilesPage; window.changeChatFilesPageSize = changeChatFilesPageSize; window.chatFilesResetToFirstPageAndLoad = chatFilesResetToFirstPageAndLoad; function chatFilesNavigateInto(name) { const root = chatFilesTreeRootMerged(); chatFilesNormalizeBrowsePathForTree(root); const next = chatFilesBrowsePath.concat([name]); if (!chatFilesResolveTreeNode(root, next)) return; chatFilesSetBrowsePath(next); renderChatFilesTable(); } function chatFilesNavigateBreadcrumb(level) { const root = chatFilesTreeRootMerged(); chatFilesNormalizeBrowsePathForTree(root); if (level < 0) { chatFilesSetBrowsePath([]); } else { chatFilesSetBrowsePath(chatFilesBrowsePath.slice(0, level + 1)); } renderChatFilesTable(); } function chatFilesNavigateUp() { if (chatFilesBrowsePath.length === 0) return; chatFilesSetBrowsePath(chatFilesBrowsePath.slice(0, -1)); renderChatFilesTable(); } function chatFilesFolderNameFromRow(el) { if (!el || !el.getAttribute) return ''; try { return decodeURIComponent(String(el.getAttribute('data-chat-folder-name') || '')); } catch (e) { return ''; } } function chatFilesOnFolderRowClick(ev) { if (ev.target.closest && ev.target.closest('[data-chat-files-stop]')) return; const name = chatFilesFolderNameFromRow(ev.currentTarget); if (!name) return; chatFilesNavigateInto(name); } function chatFilesOnFolderRowKeydown(ev) { if (ev.key !== 'Enter' && ev.key !== ' ') return; ev.preventDefault(); const name = chatFilesFolderNameFromRow(ev.currentTarget); if (!name) return; chatFilesNavigateInto(name); } function chatFilesCopyFolderPathFromBtn(ev, btn) { if (ev) ev.stopPropagation(); const name = chatFilesFolderNameFromRow(btn); if (!name) return; copyChatFolderPathFromBrowse(name); } async function deleteChatFolderFromBrowse(folderName) { const segs = chatFilesBrowsePath.concat([folderName]); if (!chatFilesBrowseCanDeleteFolderPath(segs)) return; const rel = chatFilesUploadRelativeDirFromBrowsePath(segs); const q = (typeof window.t === 'function') ? window.t('chatFilesPage.confirmDeleteFolder') : '确定删除该文件夹及其中的全部文件?'; if (!confirm(q)) return; try { const res = await apiFetch('/api/chat-uploads', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: rel }) }); if (!res.ok) { const raw = await res.text(); if (res.status === 404) { let errMsg = raw; try { const j = JSON.parse(raw); if (j && j.error) errMsg = j.error; } catch (eParse) { /* keep raw */ } if (/not\s*found/i.test(String(errMsg))) { loadChatFilesPage(); const cleared = (typeof window.t === 'function') ? window.t('chatFilesPage.folderRemovedStale') : '服务器上不存在该目录,列表已刷新。'; if (typeof chatFilesShowToast === 'function') { chatFilesShowToast(cleared); } else { alert(cleared); } return; } } throw new Error(raw || String(res.status)); } loadChatFilesPage(); } catch (e) { alert((e && e.message) ? e.message : String(e)); } } function chatFilesDeleteFolderFromBtn(ev, btn) { if (ev) ev.stopPropagation(); const name = chatFilesFolderNameFromRow(btn); if (!name) return; deleteChatFolderFromBrowse(name); } async function copyChatFolderPathFromBrowse(folderName) { const segs = chatFilesBrowsePath.concat([folderName]); const relativePath = chatFilesRelativePathFromTreeSegments(segs); let text = ''; if (relativePath) { try { const res = await apiFetch('/api/chat-uploads/path?kind=directory&path=' + encodeURIComponent(relativePath)); if (!res.ok) { const raw = await res.text(); throw new Error(raw || String(res.status)); } const data = await res.json(); text = String((data && data.absolutePath) || '').trim(); } catch (e) { alert((e && e.message) ? e.message : String(e)); return; } } if (!text) { text = chatFilesClipboardFolderPath(segs); } const ok = await chatFilesCopyText(text); if (ok) { const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.folderPathCopied') : '目录路径已复制'; chatFilesShowToast(msg); } else { const fail = (typeof window.t === 'function') ? window.t('common.copyFailed') : '复制失败'; alert(fail); } } function chatFilesRelativePathFromTreeSegments(segs) { const parts = Array.isArray(segs) ? segs.slice() : []; if (!parts.length) return ''; const root = parts.shift(); if (root === CHAT_FILES_TREE_UPLOAD_ROOT) { return parts.length ? parts.join('/') : '.'; } if (root === CHAT_FILES_TREE_REDUCTION_ROOT) { return '__reduction__/' + parts.join('/'); } if (root === CHAT_FILES_TREE_WORKSPACE_ROOT) { return '__workspace__/' + parts.join('/'); } if (root === CHAT_FILES_TREE_ARTIFACT_ROOT) { return '__conversation_artifact__/' + parts.join('/'); } return [root].concat(parts).join('/'); } function chatFilesClipboardFolderPath(segs) { const parts = Array.isArray(segs) ? segs.slice() : []; if (!parts.length) return ''; const root = parts.shift(); if (root === CHAT_FILES_TREE_UPLOAD_ROOT) { return ['chat_uploads'].concat(parts).join('/'); } if (root === CHAT_FILES_TREE_REDUCTION_ROOT) { return ['tool_outputs'].concat(parts).join('/'); } if (root === CHAT_FILES_TREE_ARTIFACT_ROOT) { return ['conversation_artifacts'].concat(parts).join('/'); } return [root].concat(parts).map(chatFilesTreeDisplayName).join('/'); } window.chatFilesNavigateInto = chatFilesNavigateInto; window.chatFilesNavigateBreadcrumb = chatFilesNavigateBreadcrumb; window.chatFilesNavigateUp = chatFilesNavigateUp; window.chatFilesOnFolderRowClick = chatFilesOnFolderRowClick; window.copyChatFolderPathFromBrowse = copyChatFolderPathFromBrowse; window.chatFilesOnFolderRowKeydown = chatFilesOnFolderRowKeydown; window.chatFilesCopyFolderPathFromBtn = chatFilesCopyFolderPathFromBtn; window.chatFilesDeleteFolderFromBtn = chatFilesDeleteFolderFromBtn; window.chatFilesOpenUploadPicker = chatFilesOpenUploadPicker; window.chatFilesUploadToFolderClick = chatFilesUploadToFolderClick; window.exportChatFiles = exportChatFiles; window.openChatFilesMkdirModal = openChatFilesMkdirModal; window.closeChatFilesMkdirModal = closeChatFilesMkdirModal; window.submitChatFilesMkdir = submitChatFilesMkdir; function openChatFilesConversationIdx(idx) { const f = chatFilesDisplayed[idx]; if (!f || !f.conversationId) return; openChatFilesConversation(f.conversationId); } function downloadChatFileIdx(idx) { const f = chatFilesDisplayed[idx]; if (!f) return; downloadChatFile(f.relativePath, f.name); } function openChatFilesEditIdx(idx) { const f = chatFilesDisplayed[idx]; if (!f) return; if (chatFileIsBinaryByName(f.name)) { alert(chatFilesEditBlockedHint()); return; } openChatFilesEdit(f.relativePath); } function openChatFilesRenameIdx(idx) { const f = chatFilesDisplayed[idx]; if (!f) return; openChatFilesRename(f.relativePath, f.name); } function deleteChatFileIdx(idx) { const f = chatFilesDisplayed[idx]; if (!f) return; deleteChatFile(f.relativePath); } function openChatFilesConversation(conversationId) { if (!conversationId) return; window.location.hash = 'chat?conversation=' + encodeURIComponent(conversationId); if (typeof switchPage === 'function') { switchPage('chat'); } setTimeout(() => { if (typeof loadConversation === 'function') { loadConversation(conversationId); } }, 400); } async function downloadChatFile(relativePath, filename) { try { const url = '/api/chat-uploads/download?path=' + encodeURIComponent(relativePath); const res = await apiFetch(url); if (!res.ok) { throw new Error(await res.text()); } const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename || 'download'; a.click(); URL.revokeObjectURL(a.href); } catch (e) { alert((e && e.message) ? e.message : String(e)); } } async function deleteChatFile(relativePath) { const q = (typeof window.t === 'function') ? window.t('chatFilesPage.confirmDelete') : '确定删除该文件?'; if (!confirm(q)) return; try { const res = await apiFetch('/api/chat-uploads', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: relativePath }) }); if (!res.ok) { throw new Error(await res.text()); } loadChatFilesPage(); } catch (e) { alert((e && e.message) ? e.message : String(e)); } } async function openChatFilesEdit(relativePath) { chatFilesEditRelativePath = relativePath; const pathEl = document.getElementById('chat-files-edit-path'); const ta = document.getElementById('chat-files-edit-textarea'); const modal = document.getElementById('chat-files-edit-modal'); if (pathEl) pathEl.textContent = relativePath; if (ta) ta.value = ''; openAppModal('chat-files-edit-modal', { focus: false }); try { const res = await apiFetch('/api/chat-uploads/content?path=' + encodeURIComponent(relativePath)); if (!res.ok) { let errText = ''; try { const err = await res.json(); errText = err.error || JSON.stringify(err); } catch (e2) { errText = await res.text(); } throw new Error(errText || res.status); } const data = await res.json(); const content = data.content != null ? String(data.content) : ''; deferModalContent(() => { if (ta) ta.value = content; ta?.focus(); }); } catch (e) { closeAppModal('chat-files-edit-modal'); alert(chatFilesAlertMessage(e && e.message)); } } function closeChatFilesEditModal() { closeAppModal('chat-files-edit-modal'); chatFilesEditRelativePath = ''; } async function saveChatFilesEdit() { const ta = document.getElementById('chat-files-edit-textarea'); if (!ta || !chatFilesEditRelativePath) return; try { const res = await apiFetch('/api/chat-uploads/content', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: chatFilesEditRelativePath, content: ta.value }) }); if (!res.ok) { throw new Error(await res.text()); } closeChatFilesEditModal(); loadChatFilesPage(); } catch (e) { alert(chatFilesAlertMessage(e && e.message)); } } function openChatFilesRename(relativePath, currentName) { chatFilesRenameRelativePath = relativePath; const input = document.getElementById('chat-files-rename-input'); const hint = document.getElementById('chat-files-rename-path-hint'); const modal = document.getElementById('chat-files-rename-modal'); const pathText = relativePath ? ('chat_uploads/' + String(relativePath).replace(/\\/g, '/')) : 'chat_uploads'; if (hint) hint.textContent = pathText; if (input) { input.value = currentName || ''; input.select(); } if (modal) openAppModal(modal); if (modal && typeof window.applyTranslations === 'function') { window.applyTranslations(modal); } setTimeout(() => { if (input) { input.focus(); input.select(); } }, 100); } function closeChatFilesRenameModal() { closeAppModal('chat-files-rename-modal'); const hint = document.getElementById('chat-files-rename-path-hint'); if (hint) hint.textContent = ''; chatFilesRenameRelativePath = ''; } async function submitChatFilesRename() { const input = document.getElementById('chat-files-rename-input'); const newName = input ? input.value.trim() : ''; if (!newName || !chatFilesRenameRelativePath) { closeChatFilesRenameModal(); return; } try { const res = await apiFetch('/api/chat-uploads/rename', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: chatFilesRenameRelativePath, newName: newName }) }); if (!res.ok) { throw new Error(await res.text()); } closeChatFilesRenameModal(); loadChatFilesPage(); } catch (e) { alert((e && e.message) ? e.message : String(e)); } } function openChatFilesMkdirModal() { if (chatFilesGetGroupByMode() !== 'folder') return; if (!chatFilesBrowseCanMutateCurrentPath()) return; const hint = document.getElementById('chat-files-mkdir-parent-hint'); const input = document.getElementById('chat-files-mkdir-input'); const modal = document.getElementById('chat-files-mkdir-modal'); const p = chatFilesBrowsePath.map(chatFilesTreeDisplayName).join('/'); if (hint) hint.textContent = p || chatFilesTreeDisplayName(CHAT_FILES_TREE_UPLOAD_ROOT); if (input) input.value = ''; if (modal) openAppModal(modal); if (modal && typeof window.applyTranslations === 'function') { window.applyTranslations(modal); } setTimeout(() => { if (input) input.focus(); }, 100); } function closeChatFilesMkdirModal() { closeAppModal('chat-files-mkdir-modal'); const input = document.getElementById('chat-files-mkdir-input'); if (input) input.value = ''; } async function submitChatFilesMkdir() { const input = document.getElementById('chat-files-mkdir-input'); const name = input ? String(input.value).trim() : ''; if (!name) { closeChatFilesMkdirModal(); return; } if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') { const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.mkdirInvalidName') : '名称无效'; alert(msg); return; } const parent = chatFilesUploadRelativeDirFromBrowsePath(chatFilesBrowsePath); try { const res = await apiFetch('/api/chat-uploads/mkdir', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ parent: parent, name: name }) }); if (!res.ok) { let errText = ''; try { const j = await res.json(); errText = j.error || JSON.stringify(j); } catch (e2) { errText = await res.text(); } if (res.status === 409) { const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.mkdirExists') : errText; alert(msg); return; } throw new Error(errText || String(res.status)); } closeChatFilesMkdirModal(); loadChatFilesPage(); const okMsg = (typeof window.t === 'function') ? window.t('chatFilesPage.mkdirOk') : '文件夹已创建'; chatFilesShowToast(okMsg); } catch (e) { alert((e && e.message) ? e.message : String(e)); } } function chatFilesSetUploadProgressUI(visible, percent, fileName) { const wrap = document.getElementById('chat-files-upload-progress'); const fill = document.getElementById('chat-files-upload-progress-fill'); const label = document.getElementById('chat-files-upload-progress-label'); if (!wrap || !fill || !label) return; if (!visible) { wrap.hidden = true; fill.style.width = '0%'; label.textContent = ''; return; } wrap.hidden = false; const p = Math.min(100, Math.max(0, Math.round(percent))); fill.style.width = p + '%'; const name = fileName || ''; label.textContent = (typeof window.t === 'function') ? window.t('chatFilesPage.uploadingFile', { name: name, percent: p }) : ('正在上传 ' + name + ' · ' + p + '%'); } function chatFilesSetUploadBusy(busy) { chatFilesXHRUploadBusy = !!busy; ['chat-files-header-upload-btn', 'chat-files-refresh-btn'].forEach(function (id) { const el = document.getElementById(id); if (el) el.disabled = chatFilesXHRUploadBusy; }); } function chatFilesOpenUploadPicker() { if (chatFilesXHRUploadBusy) return; if (chatFilesGetGroupByMode() === 'folder') { if (!chatFilesBrowseCanMutateCurrentPath()) return; chatFilesPendingUploadDir = chatFilesUploadRelativeDirFromBrowsePath(chatFilesBrowsePath); } else { chatFilesPendingUploadDir = ''; } const inp = document.getElementById('chat-files-upload-input'); if (inp) inp.click(); } function chatFilesUploadToFolderClick(ev, btn) { if (ev) ev.stopPropagation(); if (chatFilesXHRUploadBusy) return; const raw = btn.getAttribute('data-upload-dir'); if (!raw) return; try { chatFilesPendingUploadDir = decodeURIComponent(raw); chatFilesPendingUploadDir = chatFilesUploadRelativeDirFromBrowsePath(chatFilesPendingUploadDir.split('/').filter(Boolean)); } catch (e) { chatFilesPendingUploadDir = ''; return; } const inp = document.getElementById('chat-files-upload-input'); if (inp) inp.click(); } function chatFilesResolveUploadTarget() { const pendingDir = chatFilesPendingUploadDir; chatFilesPendingUploadDir = ''; if (pendingDir) { return { relativeDir: pendingDir }; } if (chatFilesGetGroupByMode() === 'folder') { if (!chatFilesBrowseCanMutateCurrentPath()) return {}; const relDir = chatFilesUploadRelativeDirFromBrowsePath(chatFilesBrowsePath); return relDir ? { relativeDir: relDir } : {}; } const conv = document.getElementById('chat-files-filter-conv'); if (conv && conv.value.trim()) { return { conversationId: conv.value.trim() }; } return {}; } async function chatFilesUploadFile(file, target) { if (!file || chatFilesXHRUploadBusy) return false; const form = new FormData(); form.append('file', file); if (target && target.relativeDir) { form.append('relativeDir', target.relativeDir); } else if (target && target.conversationId) { form.append('conversationId', target.conversationId); } chatFilesSetUploadBusy(true); chatFilesSetUploadProgressUI(true, 0, file.name); try { const doXhr = typeof apiUploadWithProgress === 'function'; const res = doXhr ? await apiUploadWithProgress('/api/chat-uploads', form, { onProgress: function (p) { chatFilesSetUploadProgressUI(true, p.percent, file.name); } }) : await apiFetch('/api/chat-uploads', { method: 'POST', body: form }); if (!res.ok) { throw new Error(await res.text()); } const data = await res.json().catch(() => ({})); chatFilesSetUploadProgressUI(true, 100, file.name); loadChatFilesPage(); if (data && data.ok) { const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.uploadOkHint') : '上传成功。在列表中点击「复制路径」即可粘贴到对话中引用。'; chatFilesShowToast(msg); } return true; } catch (e) { alert((e && e.message) ? e.message : String(e)); return false; } finally { chatFilesSetUploadBusy(false); chatFilesSetUploadProgressUI(false); } } async function chatFilesUploadFiles(fileList) { if (!fileList || !fileList.length || chatFilesXHRUploadBusy) return; const files = Array.from(fileList).filter(function (f) { return f && (f.name || f.size > 0); }); if (!files.length) return; const target = chatFilesResolveUploadTarget(); for (let i = 0; i < files.length; i++) { const ok = await chatFilesUploadFile(files[i], target); if (!ok) break; } } async function onChatFilesUploadPick(ev) { const input = ev.target; const files = input && input.files; if (!files || !files.length) return; try { await chatFilesUploadFiles(files); } finally { input.value = ''; } } let chatFilesDragDropBound = false; function setupChatFilesDragDrop() { if (chatFilesDragDropBound) return; const wrap = document.getElementById('chat-files-list-wrap'); if (!wrap) return; chatFilesDragDropBound = true; wrap.addEventListener('dragover', function (e) { e.preventDefault(); e.stopPropagation(); if (chatFilesXHRUploadBusy) return; this.classList.add('drag-over'); }); wrap.addEventListener('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); if (!this.contains(e.relatedTarget)) { this.classList.remove('drag-over'); } }); wrap.addEventListener('drop', function (e) { e.preventDefault(); e.stopPropagation(); this.classList.remove('drag-over'); if (chatFilesXHRUploadBusy) return; const files = e.dataTransfer && e.dataTransfer.files; if (files && files.length) { chatFilesUploadFiles(files).catch(function (err) { if (err) alert((err && err.message) ? err.message : String(err)); }); } }); } // 语言切换后重新渲染列表:表头与「更多」菜单由 JS 拼接,无 data-i18n,需用当前语言的 t() 再生成一遍 document.addEventListener('languagechange', function () { if (typeof window.currentPage !== 'function') return; if (window.currentPage() !== 'chat-files') return; syncAllChatFilesFilterSelects(); if (typeof renderChatFilesTable === 'function') { renderChatFilesTable(); } }); document.addEventListener('DOMContentLoaded', function () { initChatFilesFilterSelects(); });