feat: 文件对话框全部改用 Electron 原生对话框

- input-area.ts: 图片/文件上传改用 dialog.showOpenDialog(通过 IPC)
- settings-modal.ts: 导入改用原生打开对话框,导出改用原生保存对话框
- kb-modal.ts: 知识库文档上传改用原生打开对话框
- ipc.ts/preload.ts/types.d.ts: writeFile 增加 encoding 参数支持二进制写入
- index.html: 移除 4 个无用的 <input type="file"> 元素
- 清理死代码: fileToBase64, fileToText, isMetonaFile
This commit is contained in:
thzxx
2026-04-07 01:33:51 +08:00
parent 58ef272061
commit 02d5ff19c7
9 changed files with 167 additions and 122 deletions
+2 -2
View File
@@ -46,9 +46,9 @@ export function setupIPC(): void {
} }
}); });
ipcMain.handle('fs:writeFile', async (_, filePath: string, content: string) => { ipcMain.handle('fs:writeFile', async (_, filePath: string, content: string, encoding?: string) => {
try { try {
await fs.promises.writeFile(filePath, content, 'utf-8'); await fs.promises.writeFile(filePath, content, (encoding as BufferEncoding) || 'utf-8');
return { success: true }; return { success: true };
} catch (err) { } catch (err) {
return { success: false, error: (err as Error).message }; return { success: false, error: (err as Error).message };
+1 -1
View File
@@ -13,7 +13,7 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
}, },
fs: { fs: {
readFile: (filePath: string) => ipcRenderer.invoke('fs:readFile', filePath), readFile: (filePath: string) => ipcRenderer.invoke('fs:readFile', filePath),
writeFile: (filePath: string, content: string) => ipcRenderer.invoke('fs:writeFile', filePath, content) writeFile: (filePath: string, content: string, encoding?: string) => ipcRenderer.invoke('fs:writeFile', filePath, content, encoding)
}, },
tool: { tool: {
execute: (toolName: string, args: Record<string, unknown>) => ipcRenderer.invoke('tool:execute', toolName, args), execute: (toolName: string, args: Record<string, unknown>) => ipcRenderer.invoke('tool:execute', toolName, args),
+71 -46
View File
@@ -3,7 +3,7 @@
*/ */
import { state, KEYS } from '../state/state.js'; import { state, KEYS } from '../state/state.js';
import { fileToBase64, fileToText, detectLanguage, truncate, escapeHtml, formatSize } from '../utils/utils.js'; import { detectLanguage, truncate, escapeHtml, formatSize } from '../utils/utils.js';
import { getSelectedModel, isThinkEnabled, isVisionAvailable, isToolCallingSupported } from './model-bar.js'; import { getSelectedModel, isThinkEnabled, isVisionAvailable, isToolCallingSupported } from './model-bar.js';
import { import {
renderMessages, appendAssistantPlaceholder, appendSystemMessage, renderMessages, appendAssistantPlaceholder, appendSystemMessage,
@@ -21,8 +21,6 @@ import type { ChatSession, ChatMessage, OllamaStreamChunk, RagSource, FileConten
let chatInputEl: HTMLTextAreaElement; let chatInputEl: HTMLTextAreaElement;
let btnSendEl: HTMLButtonElement; let btnSendEl: HTMLButtonElement;
let fileInputEl: HTMLInputElement;
let textFileInputEl: HTMLInputElement;
let imagePreviewEl: HTMLElement; let imagePreviewEl: HTMLElement;
let filePreviewEl: HTMLElement; let filePreviewEl: HTMLElement;
let pendingImages: Array<{ name: string; base64: string }> = []; let pendingImages: Array<{ name: string; base64: string }> = [];
@@ -50,18 +48,20 @@ function getFileIcon(filename: string): string {
return map[ext] || '📄'; return map[ext] || '📄';
} }
function isTextFile(file: File): boolean { function isTextFile(name: string): boolean {
const name = file.name.toLowerCase(); const lower = name.toLowerCase();
if (name === 'dockerfile' || name === 'makefile') return true; if (lower === 'dockerfile' || lower === 'makefile') return true;
const ext = name.split('.').pop() || ''; const ext = lower.split('.').pop() || '';
return TEXT_EXTENSIONS.has(ext); return TEXT_EXTENSIONS.has(ext);
} }
function getBridge(): any {
return (window as any).metonaDesktop;
}
export function initInputArea(): void { export function initInputArea(): void {
chatInputEl = document.querySelector('#chatInput')!; chatInputEl = document.querySelector('#chatInput')!;
btnSendEl = document.querySelector('#btnSend')!; btnSendEl = document.querySelector('#btnSend')!;
fileInputEl = document.querySelector('#fileInput')!;
textFileInputEl = document.querySelector('#textFileInput')!;
imagePreviewEl = document.querySelector('#imagePreview')!; imagePreviewEl = document.querySelector('#imagePreview')!;
filePreviewEl = document.querySelector('#filePreview')!; filePreviewEl = document.querySelector('#filePreview')!;
@@ -82,22 +82,30 @@ export function initInputArea(): void {
chatInputEl.addEventListener('input', autoResizeTextarea); chatInputEl.addEventListener('input', autoResizeTextarea);
document.querySelector('#btnAttachImg')!.addEventListener('click', () => { // ── 图片上传:原生文件对话框 ──
document.querySelector('#btnAttachImg')!.addEventListener('click', async () => {
if (!isVisionAvailable()) { if (!isVisionAvailable()) {
showToast('当前模型不支持图片分析', 'warning'); showToast('当前模型不支持图片分析', 'warning');
return; return;
} }
fileInputEl.click(); const bridge = getBridge();
}); if (!bridge) return;
fileInputEl.addEventListener('change', (e) => { const paths = await bridge.dialog.openFile({
handleImageSelect((e.target as HTMLInputElement).files!); filters: [{ name: '图片', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'] }]
(e.target as HTMLInputElement).value = ''; });
if (!paths || paths.length === 0) return;
await handleImagePaths(paths);
}); });
document.querySelector('#btnAttachFile')!.addEventListener('click', () => textFileInputEl.click()); // ── 文件上传:原生文件对话框 ──
textFileInputEl.addEventListener('change', (e) => { document.querySelector('#btnAttachFile')!.addEventListener('click', async () => {
handleTextFileSelect((e.target as HTMLInputElement).files!); const bridge = getBridge();
(e.target as HTMLInputElement).value = ''; if (!bridge) return;
const paths = await bridge.dialog.openFile({
filters: [{ name: '文本/代码', extensions: Array.from(TEXT_EXTENSIONS) }]
});
if (!paths || paths.length === 0) return;
await handleTextFilePaths(paths);
}); });
imagePreviewEl.addEventListener('click', (e) => { imagePreviewEl.addEventListener('click', (e) => {
@@ -122,23 +130,32 @@ function autoResizeTextarea(): void {
chatInputEl.style.height = Math.min(chatInputEl.scrollHeight, 120) + 'px'; chatInputEl.style.height = Math.min(chatInputEl.scrollHeight, 120) + 'px';
} }
async function handleImageSelect(files: FileList): Promise<void> { async function handleImagePaths(paths: string[]): Promise<void> {
const bridge = getBridge();
const maxSize = 20 * 1024 * 1024; const maxSize = 20 * 1024 * 1024;
for (const file of Array.from(files)) { for (const filePath of paths) {
if (!file.type.startsWith('image/')) continue; const name = filePath.split(/[/\\]/).pop() || filePath;
if (file.size > maxSize) {
appendSystemMessage(`⚠️ 图片 ${file.name} 超过 20MB 限制`);
continue;
}
try { try {
const base64 = await fileToBase64(file); const result = await bridge.fs.readFile(filePath);
pendingImages.push({ name: file.name, base64 }); if (!result.success) {
logInfo(`图片已添加: ${file.name}`, formatSize(file.size)); appendSystemMessage(`❌ 读取 ${name} 失败: ${result.error}`);
renderImagePreviews(); continue;
}
// IPC 返回的 content 是 UTF-8 字符串,转为 base64
const binary = Uint8Array.from(atob(result.content as string), c => c.charCodeAt(0));
const base64 = btoa(String.fromCharCode(...binary));
if (binary.length > maxSize) {
appendSystemMessage(`⚠️ 图片 ${name} 超过 20MB 限制`);
continue;
}
pendingImages.push({ name, base64 });
logInfo(`图片已添加: ${name}`, formatSize(binary.length));
} catch (err) { } catch (err) {
console.error('[InputArea] 图片读取失败:', err); console.error('[InputArea] 图片读取失败:', err);
appendSystemMessage(`❌ 读取 ${name} 失败`);
} }
} }
renderImagePreviews();
} }
function renderImagePreviews(): void { function renderImagePreviews(): void {
@@ -156,28 +173,36 @@ function renderImagePreviews(): void {
`).join(''); `).join('');
} }
async function handleTextFileSelect(files: FileList): Promise<void> { async function handleTextFilePaths(paths: string[]): Promise<void> {
for (const file of Array.from(files)) { const bridge = getBridge();
if (!isTextFile(file)) { for (const filePath of paths) {
appendSystemMessage(`⚠️ ${file.name} 不是支持的文本/代码文件`); const name = filePath.split(/[/\\]/).pop() || filePath;
continue; if (!isTextFile(name)) {
} appendSystemMessage(`⚠️ ${name} 不是支持的文本/代码文件`);
if (file.size > MAX_FILE_SIZE) {
appendSystemMessage(`⚠️ ${file.name} 超过 500KB 限制(${formatSize(file.size)}`);
continue;
}
if (pendingFiles.some(f => f.name === file.name && f.size === file.size)) {
appendSystemMessage(`${file.name} 已添加`);
continue; continue;
} }
try { try {
const content = await fileToText(file); const result = await bridge.fs.readFile(filePath);
pendingFiles.push({ name: file.name, content, language: detectLanguage(file.name), size: file.size }); if (!result.success) {
logInfo(`文件已添加: ${file.name}`, formatSize(file.size)); appendSystemMessage(`❌ 读取 ${name} 失败: ${result.error}`);
continue;
}
const content = result.content as string;
const size = new TextEncoder().encode(content).length;
if (size > MAX_FILE_SIZE) {
appendSystemMessage(`⚠️ ${name} 超过 500KB 限制(${formatSize(size)}`);
continue;
}
if (pendingFiles.some(f => f.name === name)) {
appendSystemMessage(`${name} 已添加`);
continue;
}
pendingFiles.push({ name, content, language: detectLanguage(name), size });
logInfo(`文件已添加: ${name}`, formatSize(size));
renderFilePreviews(); renderFilePreviews();
} catch (err) { } catch (err) {
console.error('[InputArea] 文件读取失败:', err); console.error('[InputArea] 文件读取失败:', err);
appendSystemMessage(`❌ 读取 ${file.name} 失败`); appendSystemMessage(`❌ 读取 ${name} 失败`);
} }
} }
} }
+38 -24
View File
@@ -8,7 +8,7 @@ import {
removeDocumentFromKB, getDocumentsInCollection, retrieveContext, removeDocumentFromKB, getDocumentsInCollection, retrieveContext,
buildRagSystemPrompt buildRagSystemPrompt
} from '../services/rag.js'; } from '../services/rag.js';
import { fileToText, formatSize, escapeHtml, truncate } from '../utils/utils.js'; import { formatSize, escapeHtml, truncate } from '../utils/utils.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { showConfirm } from './prompt-modal.js'; import { showConfirm } from './prompt-modal.js';
import { OllamaAPI } from '../api/ollama.js'; import { OllamaAPI } from '../api/ollama.js';
@@ -31,11 +31,16 @@ export function initKBModal(): void {
document.querySelector('#btnNewCollection')!.addEventListener('click', createCollection); document.querySelector('#btnNewCollection')!.addEventListener('click', createCollection);
document.querySelector('#btnUploadDoc')!.addEventListener('click', () => { document.querySelector('#btnUploadDoc')!.addEventListener('click', async () => {
if (!currentColId) { showToast('请先选择一个知识库集合', 'warning'); return; } if (!currentColId) { showToast('请先选择一个知识库集合', 'warning'); return; }
(document.querySelector('#kbFileInput') as HTMLInputElement).click(); const bridge = (window as any).metonaDesktop;
if (!bridge) return;
const paths = await bridge.dialog.openFile({
filters: [{ name: '文档', extensions: ['txt', 'md', 'py', 'js', 'ts', 'java', 'go', 'rs', 'cpp', 'c', 'h', 'hpp', 'rb', 'php', 'sh', 'html', 'css', 'json', 'yaml', 'yml', 'toml', 'xml', 'sql', 'csv'] }]
});
if (!paths || paths.length === 0) return;
await handleDocUpload(paths);
}); });
document.querySelector('#kbFileInput')!.addEventListener('change', handleDocUpload);
document.querySelector('#toggleRAG')!.addEventListener('change', (e) => { document.querySelector('#toggleRAG')!.addEventListener('change', (e) => {
ragEnabled = (e.target as HTMLInputElement).checked; ragEnabled = (e.target as HTMLInputElement).checked;
@@ -216,47 +221,56 @@ async function createCollection(): Promise<void> {
await refreshDocList(); await refreshDocList();
} }
async function handleDocUpload(e: Event): Promise<void> { async function handleDocUpload(paths: string[]): Promise<void> {
const fileArr = Array.from((e.target as HTMLInputElement).files || []); if (paths.length === 0) return;
(e.target as HTMLInputElement).value = '';
if (fileArr.length === 0) return;
const embedModel = (document.querySelector('#selectEmbedModel') as HTMLSelectElement).value; const embedModel = (document.querySelector('#selectEmbedModel') as HTMLSelectElement).value;
if (!embedModel) { showToast('请先选择一个嵌入模型', 'warning'); return; } if (!embedModel) { showToast('请先选择一个嵌入模型', 'warning'); return; }
const bridge = (window as any).metonaDesktop;
const progressEl = document.querySelector('#kbProgress') as HTMLElement; const progressEl = document.querySelector('#kbProgress') as HTMLElement;
const progressTextEl = document.querySelector('#kbProgressText')!; const progressTextEl = document.querySelector('#kbProgressText')!;
let successCount = 0; let successCount = 0;
let failCount = 0; let failCount = 0;
for (const file of fileArr) { for (const filePath of paths) {
if (file.size > 5 * 1024 * 1024) { const name = filePath.split(/[/\\]/).pop() || filePath;
showToast(`${file.name} 超过 5MB 限制`, 'warning');
failCount++;
continue;
}
try { try {
progressEl.style.display = ''; progressEl.style.display = '';
progressTextEl.textContent = `正在处理 ${file.name}...`; progressTextEl.textContent = `正在处理 ${name}...`;
const content = await fileToText(file); const result = await bridge.fs.readFile(filePath);
const result = await addDocumentToKB( if (!result.success) {
currentColId!, file.name, content, embedModel, showToast(`${name} 读取失败: ${result.error}`, 'error');
failCount++;
continue;
}
const content = result.content as string;
const size = new TextEncoder().encode(content).length;
if (size > 5 * 1024 * 1024) {
showToast(`${name} 超过 5MB 限制`, 'warning');
failCount++;
continue;
}
const kbResult = await addDocumentToKB(
currentColId!, name, content, embedModel,
(done, total, msg) => { (done, total, msg) => {
progressTextEl.textContent = `${file.name}: ${msg} (${done}/${total})`; progressTextEl.textContent = `${name}: ${msg} (${done}/${total})`;
} }
); );
showToast(`${file.name} 已添加到知识库(${result.chunkCount} 个分块)`, 'success'); showToast(`${name} 已添加到知识库(${kbResult.chunkCount} 个分块)`, 'success');
logRAG(`文档上传: ${file.name}`, `${result.chunkCount} 个分块`); logRAG(`文档上传: ${name}`, `${kbResult.chunkCount} 个分块`);
successCount++; successCount++;
} catch (err) { } catch (err) {
console.error('[KB] 文档处理失败:', err); console.error('[KB] 文档处理失败:', err);
showToast(`${file.name} 处理失败: ${(err as Error).message}`, 'error'); const fileName = filePath.split(/[/\\]/).pop() || filePath;
logError(`RAG 文档处理失败: ${file.name}`, (err as Error).message); showToast(`${fileName} 处理失败: ${(err as Error).message}`, 'error');
logError(`RAG 文档处理失败: ${fileName}`, (err as Error).message);
failCount++; failCount++;
} }
} }
+54 -17
View File
@@ -4,7 +4,7 @@
import { state, KEYS } from '../state/state.js'; import { state, KEYS } from '../state/state.js';
import { debounce } from '../utils/utils.js'; import { debounce } from '../utils/utils.js';
import { encryptData, decryptData, isMetonaFile } from '../services/crypto.js'; import { encryptData, decryptData } from '../services/crypto.js';
import { setMemoryEnabled } from '../services/memory-manager.js'; import { setMemoryEnabled } from '../services/memory-manager.js';
import { setToolEnabled } from '../services/tool-registry.js'; import { setToolEnabled } from '../services/tool-registry.js';
import { logSetting, logInfo, logWarn, logError, logSuccess } from '../services/log-service.js'; import { logSetting, logInfo, logWarn, logError, logSuccess } from '../services/log-service.js';
@@ -108,13 +108,15 @@ export function initSettingsModal(): void {
document.querySelector('#btnExportAll')!.addEventListener('click', exportAllSessions); document.querySelector('#btnExportAll')!.addEventListener('click', exportAllSessions);
const importFileInput = document.querySelector('#importFileInput') as HTMLInputElement; // ── 导入会话:原生文件对话框 ──
document.querySelector('#btnImportSessions')!.addEventListener('click', () => importFileInput.click()); document.querySelector('#btnImportSessions')!.addEventListener('click', async () => {
importFileInput.addEventListener('change', (e) => { const bridge = (window as any).metonaDesktop;
if ((e.target as HTMLInputElement).files!.length > 0) { if (!bridge) return;
importSessions((e.target as HTMLInputElement).files![0]); const paths = await bridge.dialog.openFile({
(e.target as HTMLInputElement).value = ''; filters: [{ name: 'Metona 备份', extensions: ['metona'] }]
} });
if (!paths || paths.length === 0) return;
await importSessions(paths[0]);
}); });
// ── Tool Calling 设置 ── // ── Tool Calling 设置 ──
@@ -198,10 +200,35 @@ async function exportAllSessions(): Promise<void> {
try { try {
const blob = await encryptData(backup); const blob = await encryptData(backup);
const ts = new Date().toISOString().slice(0, 10); const ts = new Date().toISOString().slice(0, 10);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob); // ── 原生保存对话框 ──
a.download = `metona-backup-${ts}.metona`; const bridge = (window as any).metonaDesktop;
a.click(); if (bridge) {
const filePath = await bridge.dialog.saveFile({
defaultPath: `metona-backup-${ts}.metona`,
filters: [{ name: 'Metona 备份', extensions: ['metona'] }]
});
if (!filePath) return;
// blob → ArrayBuffer → base64 字符串,通过 base64 编码写入二进制文件
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
const b64 = btoa(binary);
const result = await bridge.fs.writeFile(filePath, b64, 'base64');
if (!result.success) {
showToast(`导出失败: ${result.error}`, 'error');
logError('导出失败', result.error);
return;
}
} else {
// 回退:浏览器下载
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `metona-backup-${ts}.metona`;
a.click();
}
showToast(`已导出 ${sessions.length} 个会话(已加密)`, 'success'); showToast(`已导出 ${sessions.length} 个会话(已加密)`, 'success');
logSuccess(`导出完成: ${sessions.length} 个会话`); logSuccess(`导出完成: ${sessions.length} 个会话`);
} catch (err) { } catch (err) {
@@ -211,19 +238,29 @@ async function exportAllSessions(): Promise<void> {
} }
} }
async function importSessions(file: File): Promise<void> { async function importSessions(filePath: string): Promise<void> {
const db = state.get<ChatDB | null>(KEYS.DB); const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) return; if (!db) return;
if (!isMetonaFile(file)) { const fileName = filePath.split(/[/\\]/).pop() || filePath;
if (!fileName.endsWith('.metona')) {
showToast('仅支持导入 .metona 格式文件', 'error'); showToast('仅支持导入 .metona 格式文件', 'error');
return; return;
} }
try { try {
const buffer = await file.arrayBuffer(); const bridge = (window as any).metonaDesktop;
const data = await decryptData(buffer); const result = await bridge.fs.readFile(filePath);
logInfo('导入会话', `文件: ${file.name}`); if (!result.success) {
showToast(`读取文件失败: ${result.error}`, 'error');
return;
}
// IPC 返回 base64 编码的二进制,转 ArrayBuffer
const binary = atob(result.content as string);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const data = await decryptData(bytes.buffer);
logInfo('导入会话', `文件: ${fileName}`);
let sessions: ChatSession[]; let sessions: ChatSession[];
if (Array.isArray(data)) { if (Array.isArray(data)) {
-4
View File
@@ -141,8 +141,6 @@
<polyline points="10 9 9 9 8 9"/> <polyline points="10 9 9 9 8 9"/>
</svg> </svg>
</button> </button>
<input type="file" id="fileInput" accept="image/*" multiple style="display:none;">
<input type="file" id="textFileInput" accept=".txt,.md,.log,.csv,.py,.pyw,.js,.mjs,.cjs,.ts,.tsx,.jsx,.java,.c,.h,.cpp,.cc,.hpp,.go,.rs,.rb,.php,.sh,.bash,.zsh,.sql,.html,.htm,.css,.json,.jsonl,.xml,.svg,.yaml,.yml,.toml,.ini,.cfg,.conf,.swift,.kt,.kts,.scala,.lua,.r,.m,.cs,.fs,.ex,.exs,.erl,.hs,.clj,.lisp,.diff,.patch,Dockerfile,Makefile,.rst,.tsv,.vb,.pl,.mm,.pyi,.fsharp,.makefile,.dockerfile" multiple style="display:none;">
<textarea class="chat-input" id="chatInput" placeholder="输入消息..." rows="1"></textarea> <textarea class="chat-input" id="chatInput" placeholder="输入消息..." rows="1"></textarea>
<label class="think-toggle" id="thinkToggleWrap" title="Think 模式未可用"> <label class="think-toggle" id="thinkToggleWrap" title="Think 模式未可用">
<input type="checkbox" id="toggleThink" disabled> <input type="checkbox" id="toggleThink" disabled>
@@ -264,7 +262,6 @@
<div style="display:flex;gap:8px;flex-wrap:wrap;"> <div style="display:flex;gap:8px;flex-wrap:wrap;">
<button class="btn btn-outline" id="btnExportAll">📦 导出全部会话</button> <button class="btn btn-outline" id="btnExportAll">📦 导出全部会话</button>
<button class="btn btn-outline" id="btnImportSessions">📥 导入会话</button> <button class="btn btn-outline" id="btnImportSessions">📥 导入会话</button>
<input type="file" id="importFileInput" accept=".metona" style="display:none;">
<button class="btn btn-danger" id="btnClearAllHistory">清空所有历史</button> <button class="btn btn-danger" id="btnClearAllHistory">清空所有历史</button>
</div> </div>
<p class="text-muted" style="margin-top:6px;font-size:11px;">导出格式为 JSON,可备份后在其他设备导入恢复。</p> <p class="text-muted" style="margin-top:6px;font-size:11px;">导出格式为 JSON,可备份后在其他设备导入恢复。</p>
@@ -344,7 +341,6 @@
<div class="kb-main"> <div class="kb-main">
<div class="kb-toolbar"> <div class="kb-toolbar">
<button class="btn btn-sm btn-outline" id="btnUploadDoc">📄 上传文档</button> <button class="btn btn-sm btn-outline" id="btnUploadDoc">📄 上传文档</button>
<input type="file" id="kbFileInput" accept=".txt,.md,.py,.js,.ts,.java,.go,.rs,.cpp,.c,.h,.hpp,.rb,.php,.sh,.html,.css,.json,.yaml,.yml,.toml,.xml,.sql,.csv" multiple style="display:none;">
<span id="kbProgress" style="display:none;" class="kb-progress"> <span id="kbProgress" style="display:none;" class="kb-progress">
<span class="spinner"></span> <span class="spinner"></span>
<span id="kbProgressText">处理中...</span> <span id="kbProgressText">处理中...</span>
-3
View File
@@ -158,6 +158,3 @@ export async function decryptData(buffer: ArrayBuffer): Promise<unknown> {
return JSON.parse(new TextDecoder().decode(jsonBytes)); return JSON.parse(new TextDecoder().decode(jsonBytes));
} }
export function isMetonaFile(file: File): boolean {
return file.name.endsWith('.metona');
}
+1 -1
View File
@@ -198,7 +198,7 @@ export interface MetonaDesktopAPI {
}; };
fs: { fs: {
readFile: (filePath: string) => Promise<{ success: boolean; content?: string; name?: string; error?: string }>; readFile: (filePath: string) => Promise<{ success: boolean; content?: string; name?: string; error?: string }>;
writeFile: (filePath: string, content: string) => Promise<{ success: boolean; error?: string }>; writeFile: (filePath: string, content: string, encoding?: string) => Promise<{ success: boolean; error?: string }>;
}; };
tool: { tool: {
execute: (toolName: string, args: Record<string, unknown>) => Promise<ToolResult>; execute: (toolName: string, args: Record<string, unknown>) => Promise<ToolResult>;
-24
View File
@@ -59,30 +59,6 @@ export function downloadFile(filename: string, content: string, type: string): v
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
/** File → Base64(去前缀) */
export function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
const base64 = result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
/** File → 文本内容 */
export function fileToText(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsText(file);
});
}
/** 根据文件名检测语言标识(用于 Markdown 代码块) */ /** 根据文件名检测语言标识(用于 Markdown 代码块) */
export function detectLanguage(filename: string): string { export function detectLanguage(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase() || ''; const ext = filename.split('.').pop()?.toLowerCase() || '';