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:
+2
-2
@@ -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 {
|
||||
await fs.promises.writeFile(filePath, content, 'utf-8');
|
||||
await fs.promises.writeFile(filePath, content, (encoding as BufferEncoding) || 'utf-8');
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
|
||||
},
|
||||
fs: {
|
||||
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: {
|
||||
execute: (toolName: string, args: Record<string, unknown>) => ipcRenderer.invoke('tool:execute', toolName, args),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
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 {
|
||||
renderMessages, appendAssistantPlaceholder, appendSystemMessage,
|
||||
@@ -21,8 +21,6 @@ import type { ChatSession, ChatMessage, OllamaStreamChunk, RagSource, FileConten
|
||||
|
||||
let chatInputEl: HTMLTextAreaElement;
|
||||
let btnSendEl: HTMLButtonElement;
|
||||
let fileInputEl: HTMLInputElement;
|
||||
let textFileInputEl: HTMLInputElement;
|
||||
let imagePreviewEl: HTMLElement;
|
||||
let filePreviewEl: HTMLElement;
|
||||
let pendingImages: Array<{ name: string; base64: string }> = [];
|
||||
@@ -50,18 +48,20 @@ function getFileIcon(filename: string): string {
|
||||
return map[ext] || '📄';
|
||||
}
|
||||
|
||||
function isTextFile(file: File): boolean {
|
||||
const name = file.name.toLowerCase();
|
||||
if (name === 'dockerfile' || name === 'makefile') return true;
|
||||
const ext = name.split('.').pop() || '';
|
||||
function isTextFile(name: string): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === 'dockerfile' || lower === 'makefile') return true;
|
||||
const ext = lower.split('.').pop() || '';
|
||||
return TEXT_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
function getBridge(): any {
|
||||
return (window as any).metonaDesktop;
|
||||
}
|
||||
|
||||
export function initInputArea(): void {
|
||||
chatInputEl = document.querySelector('#chatInput')!;
|
||||
btnSendEl = document.querySelector('#btnSend')!;
|
||||
fileInputEl = document.querySelector('#fileInput')!;
|
||||
textFileInputEl = document.querySelector('#textFileInput')!;
|
||||
imagePreviewEl = document.querySelector('#imagePreview')!;
|
||||
filePreviewEl = document.querySelector('#filePreview')!;
|
||||
|
||||
@@ -82,22 +82,30 @@ export function initInputArea(): void {
|
||||
|
||||
chatInputEl.addEventListener('input', autoResizeTextarea);
|
||||
|
||||
document.querySelector('#btnAttachImg')!.addEventListener('click', () => {
|
||||
// ── 图片上传:原生文件对话框 ──
|
||||
document.querySelector('#btnAttachImg')!.addEventListener('click', async () => {
|
||||
if (!isVisionAvailable()) {
|
||||
showToast('当前模型不支持图片分析', 'warning');
|
||||
return;
|
||||
}
|
||||
fileInputEl.click();
|
||||
const bridge = getBridge();
|
||||
if (!bridge) return;
|
||||
const paths = await bridge.dialog.openFile({
|
||||
filters: [{ name: '图片', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'] }]
|
||||
});
|
||||
fileInputEl.addEventListener('change', (e) => {
|
||||
handleImageSelect((e.target as HTMLInputElement).files!);
|
||||
(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) => {
|
||||
handleTextFileSelect((e.target as HTMLInputElement).files!);
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
// ── 文件上传:原生文件对话框 ──
|
||||
document.querySelector('#btnAttachFile')!.addEventListener('click', async () => {
|
||||
const bridge = getBridge();
|
||||
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) => {
|
||||
@@ -122,23 +130,32 @@ function autoResizeTextarea(): void {
|
||||
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;
|
||||
for (const file of Array.from(files)) {
|
||||
if (!file.type.startsWith('image/')) continue;
|
||||
if (file.size > maxSize) {
|
||||
appendSystemMessage(`⚠️ 图片 ${file.name} 超过 20MB 限制`);
|
||||
for (const filePath of paths) {
|
||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||
try {
|
||||
const result = await bridge.fs.readFile(filePath);
|
||||
if (!result.success) {
|
||||
appendSystemMessage(`❌ 读取 ${name} 失败: ${result.error}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const base64 = await fileToBase64(file);
|
||||
pendingImages.push({ name: file.name, base64 });
|
||||
logInfo(`图片已添加: ${file.name}`, formatSize(file.size));
|
||||
renderImagePreviews();
|
||||
// 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) {
|
||||
console.error('[InputArea] 图片读取失败:', err);
|
||||
appendSystemMessage(`❌ 读取 ${name} 失败`);
|
||||
}
|
||||
}
|
||||
renderImagePreviews();
|
||||
}
|
||||
|
||||
function renderImagePreviews(): void {
|
||||
@@ -156,28 +173,36 @@ function renderImagePreviews(): void {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function handleTextFileSelect(files: FileList): Promise<void> {
|
||||
for (const file of Array.from(files)) {
|
||||
if (!isTextFile(file)) {
|
||||
appendSystemMessage(`⚠️ ${file.name} 不是支持的文本/代码文件`);
|
||||
continue;
|
||||
}
|
||||
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} 已添加`);
|
||||
async function handleTextFilePaths(paths: string[]): Promise<void> {
|
||||
const bridge = getBridge();
|
||||
for (const filePath of paths) {
|
||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||
if (!isTextFile(name)) {
|
||||
appendSystemMessage(`⚠️ ${name} 不是支持的文本/代码文件`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const content = await fileToText(file);
|
||||
pendingFiles.push({ name: file.name, content, language: detectLanguage(file.name), size: file.size });
|
||||
logInfo(`文件已添加: ${file.name}`, formatSize(file.size));
|
||||
const result = await bridge.fs.readFile(filePath);
|
||||
if (!result.success) {
|
||||
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();
|
||||
} catch (err) {
|
||||
console.error('[InputArea] 文件读取失败:', err);
|
||||
appendSystemMessage(`❌ 读取 ${file.name} 失败`);
|
||||
appendSystemMessage(`❌ 读取 ${name} 失败`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
removeDocumentFromKB, getDocumentsInCollection, retrieveContext,
|
||||
buildRagSystemPrompt
|
||||
} 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 { showConfirm } from './prompt-modal.js';
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
@@ -31,11 +31,16 @@ export function initKBModal(): void {
|
||||
|
||||
document.querySelector('#btnNewCollection')!.addEventListener('click', createCollection);
|
||||
|
||||
document.querySelector('#btnUploadDoc')!.addEventListener('click', () => {
|
||||
document.querySelector('#btnUploadDoc')!.addEventListener('click', async () => {
|
||||
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) => {
|
||||
ragEnabled = (e.target as HTMLInputElement).checked;
|
||||
@@ -216,47 +221,56 @@ async function createCollection(): Promise<void> {
|
||||
await refreshDocList();
|
||||
}
|
||||
|
||||
async function handleDocUpload(e: Event): Promise<void> {
|
||||
const fileArr = Array.from((e.target as HTMLInputElement).files || []);
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
|
||||
if (fileArr.length === 0) return;
|
||||
async function handleDocUpload(paths: string[]): Promise<void> {
|
||||
if (paths.length === 0) return;
|
||||
|
||||
const embedModel = (document.querySelector('#selectEmbedModel') as HTMLSelectElement).value;
|
||||
if (!embedModel) { showToast('请先选择一个嵌入模型', 'warning'); return; }
|
||||
|
||||
const bridge = (window as any).metonaDesktop;
|
||||
const progressEl = document.querySelector('#kbProgress') as HTMLElement;
|
||||
const progressTextEl = document.querySelector('#kbProgressText')!;
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const file of fileArr) {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
showToast(`${file.name} 超过 5MB 限制`, 'warning');
|
||||
for (const filePath of paths) {
|
||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||
|
||||
try {
|
||||
progressEl.style.display = '';
|
||||
progressTextEl.textContent = `正在处理 ${name}...`;
|
||||
|
||||
const result = await bridge.fs.readFile(filePath);
|
||||
if (!result.success) {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
progressEl.style.display = '';
|
||||
progressTextEl.textContent = `正在处理 ${file.name}...`;
|
||||
|
||||
const content = await fileToText(file);
|
||||
const result = await addDocumentToKB(
|
||||
currentColId!, file.name, content, embedModel,
|
||||
const kbResult = await addDocumentToKB(
|
||||
currentColId!, name, content, embedModel,
|
||||
(done, total, msg) => {
|
||||
progressTextEl.textContent = `${file.name}: ${msg} (${done}/${total})`;
|
||||
progressTextEl.textContent = `${name}: ${msg} (${done}/${total})`;
|
||||
}
|
||||
);
|
||||
|
||||
showToast(`${file.name} 已添加到知识库(${result.chunkCount} 个分块)`, 'success');
|
||||
logRAG(`文档上传: ${file.name}`, `${result.chunkCount} 个分块`);
|
||||
showToast(`${name} 已添加到知识库(${kbResult.chunkCount} 个分块)`, 'success');
|
||||
logRAG(`文档上传: ${name}`, `${kbResult.chunkCount} 个分块`);
|
||||
successCount++;
|
||||
} catch (err) {
|
||||
console.error('[KB] 文档处理失败:', err);
|
||||
showToast(`${file.name} 处理失败: ${(err as Error).message}`, 'error');
|
||||
logError(`RAG 文档处理失败: ${file.name}`, (err as Error).message);
|
||||
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
||||
showToast(`${fileName} 处理失败: ${(err as Error).message}`, 'error');
|
||||
logError(`RAG 文档处理失败: ${fileName}`, (err as Error).message);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { state, KEYS } from '../state/state.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 { setToolEnabled } from '../services/tool-registry.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);
|
||||
|
||||
const importFileInput = document.querySelector('#importFileInput') as HTMLInputElement;
|
||||
document.querySelector('#btnImportSessions')!.addEventListener('click', () => importFileInput.click());
|
||||
importFileInput.addEventListener('change', (e) => {
|
||||
if ((e.target as HTMLInputElement).files!.length > 0) {
|
||||
importSessions((e.target as HTMLInputElement).files![0]);
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
}
|
||||
// ── 导入会话:原生文件对话框 ──
|
||||
document.querySelector('#btnImportSessions')!.addEventListener('click', async () => {
|
||||
const bridge = (window as any).metonaDesktop;
|
||||
if (!bridge) return;
|
||||
const paths = await bridge.dialog.openFile({
|
||||
filters: [{ name: 'Metona 备份', extensions: ['metona'] }]
|
||||
});
|
||||
if (!paths || paths.length === 0) return;
|
||||
await importSessions(paths[0]);
|
||||
});
|
||||
|
||||
// ── Tool Calling 设置 ──
|
||||
@@ -198,10 +200,35 @@ async function exportAllSessions(): Promise<void> {
|
||||
try {
|
||||
const blob = await encryptData(backup);
|
||||
const ts = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// ── 原生保存对话框 ──
|
||||
const bridge = (window as any).metonaDesktop;
|
||||
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');
|
||||
logSuccess(`导出完成: ${sessions.length} 个会话`);
|
||||
} 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);
|
||||
if (!db) return;
|
||||
|
||||
if (!isMetonaFile(file)) {
|
||||
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
||||
if (!fileName.endsWith('.metona')) {
|
||||
showToast('仅支持导入 .metona 格式文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const data = await decryptData(buffer);
|
||||
logInfo('导入会话', `文件: ${file.name}`);
|
||||
const bridge = (window as any).metonaDesktop;
|
||||
const result = await bridge.fs.readFile(filePath);
|
||||
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[];
|
||||
if (Array.isArray(data)) {
|
||||
|
||||
@@ -141,8 +141,6 @@
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
</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>
|
||||
<label class="think-toggle" id="thinkToggleWrap" title="Think 模式未可用">
|
||||
<input type="checkbox" id="toggleThink" disabled>
|
||||
@@ -264,7 +262,6 @@
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<button class="btn btn-outline" id="btnExportAll">📦 导出全部会话</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>
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:6px;font-size:11px;">导出格式为 JSON,可备份后在其他设备导入恢复。</p>
|
||||
@@ -344,7 +341,6 @@
|
||||
<div class="kb-main">
|
||||
<div class="kb-toolbar">
|
||||
<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 class="spinner"></span>
|
||||
<span id="kbProgressText">处理中...</span>
|
||||
|
||||
@@ -158,6 +158,3 @@ export async function decryptData(buffer: ArrayBuffer): Promise<unknown> {
|
||||
return JSON.parse(new TextDecoder().decode(jsonBytes));
|
||||
}
|
||||
|
||||
export function isMetonaFile(file: File): boolean {
|
||||
return file.name.endsWith('.metona');
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -198,7 +198,7 @@ export interface MetonaDesktopAPI {
|
||||
};
|
||||
fs: {
|
||||
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: {
|
||||
execute: (toolName: string, args: Record<string, unknown>) => Promise<ToolResult>;
|
||||
|
||||
@@ -59,30 +59,6 @@ export function downloadFile(filename: string, content: string, type: string): v
|
||||
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 代码块) */
|
||||
export function detectLanguage(filename: string): string {
|
||||
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
||||
|
||||
Reference in New Issue
Block a user