/** * 记忆文件管理页面 */ import { api } from '../lib/tauri-api.js' import { toast } from '../components/toast.js' import { showModal } from '../components/modal.js' import { t } from '../lib/i18n.js' function CATEGORIES() { return [ { key: 'memory', label: t('memory.catMemory'), desc: t('memory.catMemoryDesc') }, { key: 'archive', label: t('memory.catArchive'), desc: t('memory.catArchiveDesc') }, { key: 'core', label: t('memory.catCore'), desc: t('memory.catCoreDesc') }, ] } export async function render() { const page = document.createElement('div') page.className = 'page' page.innerHTML = `
${CATEGORIES().map((c, i) => `
${c.label}
`).join('')}
${CATEGORIES()[0].desc}
${t('memory.selectFile')}
` const state = { category: 'memory', currentPath: null, agentId: 'main' } // 先用默认选项填充下拉框,立即显示页面 const agentSelect = page.querySelector('#agent-select') agentSelect.innerHTML = '' // 异步加载 agent 列表并更新下拉框 api.listAgents().then(agents => { if (!agentSelect) return const options = agents.map(a => { const label = a.identityName ? a.identityName.split(',')[0].trim() : a.id return `` }).join('') agentSelect.innerHTML = options }).catch(() => {}) // Agent 切换 page.querySelector('#agent-select').onchange = (e) => { state.agentId = e.target.value state.currentPath = null resetEditor(page) // 显示加载动画 const tree = page.querySelector('#file-tree') tree.innerHTML = '
' loadFiles(page, state) } // Tab 切换 page.querySelectorAll('.tab').forEach(tab => { tab.onclick = () => { page.querySelectorAll('.tab').forEach(t => t.classList.remove('active')) tab.classList.add('active') state.category = tab.dataset.tab state.currentPath = null const cat = CATEGORIES().find(c => c.key === state.category) page.querySelector('#category-desc').textContent = cat?.desc || '' resetEditor(page) // 显示加载动画 const tree = page.querySelector('#file-tree') tree.innerHTML = '
' loadFiles(page, state) } }) // 保存 page.querySelector('#btn-save-file').onclick = () => saveFile(page, state) // 预览(简易 Markdown 渲染) page.querySelector('#btn-preview').onclick = () => togglePreview(page, state) // 新建文件 page.querySelector('#btn-new-file').onclick = () => { showModal({ title: t('memory.newFileTitle'), fields: [{ name: 'filename', label: t('memory.newFileLabel'), placeholder: t('memory.newFilePlaceholder'), hint: t('memory.newFileHint') }], onConfirm: async ({ filename }) => { if (!filename) return try { await api.writeMemoryFile(filename, `# ${filename}\n\n`, state.category, state.agentId) toast(t('memory.created', { name: filename }), 'success') loadFiles(page, state) } catch (e) { toast(t('memory.createFailed') + ': ' + e, 'error') } }, }) } // 删除文件 page.querySelector('#btn-del-file').onclick = async () => { if (!state.currentPath) return const name = state.currentPath.split('/').pop() const { showConfirm } = await import('../components/modal.js') const yes = await showConfirm(t('memory.confirmDelete', { name })) if (!yes) return try { await api.deleteMemoryFile(state.currentPath, state.agentId) toast(t('memory.deleted', { name }), 'success') state.currentPath = null resetEditor(page) loadFiles(page, state) } catch (e) { toast(t('memory.deleteFailed') + ': ' + e, 'error') } } // 单个下载 page.querySelector('#btn-download').onclick = () => downloadCurrentFile(page, state) // 打包下载 page.querySelector('#btn-export-zip').onclick = () => exportZip(state) loadFiles(page, state) return page } async function loadFiles(page, state) { const tree = page.querySelector('#file-tree') try { const files = await api.listMemoryFiles(state.category, state.agentId) if (!files || !files.length) { tree.innerHTML = `
${t('memory.noFiles')}
` return } renderFileTree(page, state, files) } catch (e) { tree.innerHTML = `
${t('memory.loadFailed')}: ${e}
` toast(t('memory.loadListFailed') + ': ' + e, 'error') } } function renderFileTree(page, state, files) { const tree = page.querySelector('#file-tree') tree.innerHTML = files.map(f => { const name = f.split('/').pop() const active = state.currentPath === f ? ' active' : '' return `
${name}
` }).join('') tree.querySelectorAll('.file-item').forEach(item => { item.onclick = () => { state.currentPath = item.dataset.path tree.querySelectorAll('.file-item').forEach(i => i.classList.remove('active')) item.classList.add('active') loadFileContent(page, state) } }) } async function loadFileContent(page, state) { const editor = page.querySelector('#file-editor') const label = page.querySelector('#current-file') const btnSave = page.querySelector('#btn-save-file') const btnPreview = page.querySelector('#btn-preview') const btnDel = page.querySelector('#btn-del-file') const btnDl = page.querySelector('#btn-download') editor.disabled = true editor.value = t('memory.loading') label.textContent = state.currentPath // 退出预览模式 editor.style.display = '' const previewEl = page.querySelector('#md-preview') if (previewEl) previewEl.remove() btnPreview.textContent = t('memory.preview') try { const content = await api.readMemoryFile(state.currentPath, state.agentId) editor.value = content || '' editor.disabled = false btnSave.disabled = false btnPreview.disabled = false btnDel.disabled = false btnDl.disabled = false } catch (e) { editor.value = t('memory.readFailed') + ': ' + e toast(t('memory.readFileFailed') + ': ' + e, 'error') } } function resetEditor(page) { const editor = page.querySelector('#file-editor') editor.value = '' editor.disabled = true editor.style.display = '' const previewEl = page.querySelector('#md-preview') if (previewEl) previewEl.remove() page.querySelector('#current-file').textContent = t('memory.selectFile') page.querySelector('#btn-save-file').disabled = true page.querySelector('#btn-preview').disabled = true page.querySelector('#btn-preview').textContent = t('memory.preview') page.querySelector('#btn-del-file').disabled = true page.querySelector('#btn-download').disabled = true } async function saveFile(page, state) { if (!state.currentPath) return const content = page.querySelector('#file-editor').value try { await api.writeMemoryFile(state.currentPath, content, state.category, state.agentId) toast(t('memory.fileSaved'), 'success') } catch (e) { toast(t('memory.saveFailed') + ': ' + e, 'error') } } function togglePreview(page) { const editor = page.querySelector('#file-editor') const btn = page.querySelector('#btn-preview') let previewEl = page.querySelector('#md-preview') if (previewEl) { // 退出预览 previewEl.remove() editor.style.display = '' btn.textContent = t('memory.preview') } else { // 进入预览 const md = editor.value previewEl = document.createElement('div') previewEl.id = 'md-preview' previewEl.style.cssText = 'flex:1;padding:var(--space-lg);overflow-y:auto;line-height:1.8;color:var(--text-primary)' previewEl.innerHTML = renderMarkdown(md) editor.style.display = 'none' editor.parentElement.appendChild(previewEl) btn.textContent = t('memory.edit') } } // 简易 Markdown 渲染 function renderMarkdown(md) { return md .replace(/&/g, '&').replace(//g, '>') .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/`(.+?)`/g, '$1') .replace(/^- (.+)$/gm, '
  • $1
  • ') .replace(/\n\n/g, '

    ') .replace(/\n/g, '
    ') } // ===== 下载功能 ===== function triggerDownload(filename, content) { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } async function downloadCurrentFile(page, state) { if (!state.currentPath) return try { const content = page.querySelector('#file-editor').value const filename = state.currentPath.split('/').pop() triggerDownload(filename, content) toast(t('memory.downloaded', { name: filename }), 'success') } catch (e) { toast(t('memory.downloadFailed') + ': ' + e, 'error') } } async function exportZip(state) { try { const zipPath = await api.exportMemoryZip(state.category, state.agentId) const label = CATEGORIES().find(c => c.key === state.category)?.label || state.category // 尝试用 Tauri shell open 打开文件所在目录 try { const { open } = await import('@tauri-apps/plugin-shell') const dir = zipPath.substring(0, zipPath.lastIndexOf('/')) || zipPath await open(dir) toast(t('memory.exported', { label, path: zipPath }), 'success') } catch { // fallback:仅显示路径 toast(t('memory.exported', { label, path: zipPath }), 'success') } } catch (e) { toast(t('memory.exportFailed') + ': ' + e, 'error') } }