feat(desktop): 增加原生 Vault 写入与属性导入

This commit is contained in:
2026-09-07 16:52:30 +08:00
parent 7fffbcd55a
commit afb76dc325
40 changed files with 6870 additions and 26 deletions
+57
View File
@@ -10,6 +10,7 @@ import apiClient from './apiClient'
import { t } from '@/i18n'
import * as noteService from './noteService'
import { splitNoteMetadata } from '@/utils/noteMetadata'
import { contentHash, hostInvoke, isDesktop, nativePath, nativeTree, type HostDocument, type HostEntry, type HostVault } from './platform/desktop'
/** Web 联调只连接 AI Core 配置的单一 Vault;多 Vault 选择由 Tauri Host 接管。 */
export interface VaultInfo {
@@ -80,15 +81,28 @@ async function requireNoteId(filePath: string): Promise<string> {
}
export async function getWorkspaceInfo(): Promise<ApiWorkspaceInfo> {
if (isDesktop()) {
const vault = (await getRecentVaults())[0]
if (!vault) throw new Error('VAULT_NOT_OPEN')
const entries = await hostInvoke<HostEntry[]>('workspace_tree')
return { ...vault, file_count: entries.length, indexed_note_count: 0, requires_refresh: false }
}
return apiClient.get('/api/workspace', { timeoutMs: 15000 })
}
export async function getRecentVaults(): Promise<VaultInfo[]> {
if (isDesktop()) return hostInvoke<HostVault[]>('workspace_recent')
const workspace = await getWorkspaceInfo()
return [{ vault_id: workspace.vault_id, path: workspace.path, name: workspace.name }]
}
export async function openVault(path: string): Promise<VaultInfo> {
if (isDesktop()) {
const vault = await hostInvoke<HostVault | null>('workspace_choose')
if (!vault) throw new Error(t('已取消选择', 'Selection cancelled'))
cachedTree = null; noteIdByPath.clear(); typeByPath.clear(); treeRequestVersion++
return vault
}
treeRequestVersion++
const snapshot = await apiClient.post<ApiWorkspaceSnapshot>('/api/workspace/open', { path }, { timeoutMs: 15000 })
cacheEntries(snapshot.items)
@@ -107,6 +121,14 @@ export async function createVault(path: string, name: string): Promise<VaultInfo
export async function refreshTree(): Promise<FileNode[]> {
const version = ++treeRequestVersion
if (isDesktop()) {
const entries = await hostInvoke<HostEntry[]>('workspace_tree')
if (version !== treeRequestVersion) return cachedTree ?? []
noteIdByPath.clear(); typeByPath.clear()
for (const entry of entries) { if (!entry.is_folder) noteIdByPath.set(`/${entry.path}`, entry.file_id); typeByPath.set(`/${entry.path}`, entry.is_folder ? 'folder' : 'file') }
cachedTree = nativeTree(entries)
return cachedTree
}
const entries = await apiClient.get<ApiWorkspaceEntry[]>('/api/workspace/tree', { timeoutMs: 10000 })
if (version !== treeRequestVersion) return cachedTree ?? []
return cacheEntries(entries)
@@ -117,6 +139,7 @@ export async function getFileTree(): Promise<FileNode[]> {
}
export async function readFileContent(filePath: string): Promise<string> {
if (isDesktop()) return (await hostInvoke<HostDocument>('workspace_read', { path: nativePath(filePath) })).content
const note = await noteService.getNote(await requireNoteId(filePath))
return note.markdown
}
@@ -127,6 +150,11 @@ export async function getNoteId(filePath: string): Promise<string> {
}
export async function saveFileContent(filePath: string, content: string, expectedContent?: string): Promise<void> {
if (isDesktop()) {
if (expectedContent === undefined) throw new Error('EXPECTED_REVISION_REQUIRED')
await hostInvoke('workspace_write', { path: nativePath(filePath), expected: await contentHash(expectedContent), content })
return
}
const metadata = splitNoteMetadata(content)
const expectedHash = expectedContent === undefined ? undefined : Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(expectedContent)))).map(byte => byte.toString(16).padStart(2, '0')).join('')
await noteService.updateNote(await requireNoteId(filePath), {
@@ -142,6 +170,12 @@ export async function createFile(
name: string,
content = '',
): Promise<FileNode> {
if (isDesktop()) {
const path = [nativePath(folderPath), name.endsWith('.md') ? name : `${name}.md`].filter(Boolean).join('/')
const entry = await hostInvoke<HostEntry>('workspace_write', { path, expected: '', content })
noteIdByPath.set(`/${path}`, entry.file_id)
return { id: entry.file_id, note_id: entry.file_id, path: `/${path}`, name: path.split('/').at(-1)!, type: 'file' }
}
const title = name.replace(/\.md$/i, '')
const note = await noteService.createNote({
title,
@@ -152,6 +186,11 @@ export async function createFile(
}
export async function createFolder(parentPath: string, name: string): Promise<FileNode> {
if (isDesktop()) {
const path = [nativePath(parentPath), name].filter(Boolean).join('/')
await hostInvoke('workspace_mkdir', { path })
return { id: `folder:/${path}`, path: `/${path}`, name, type: 'folder', children: [] }
}
const entry = await apiClient.post<ApiWorkspaceEntry>('/api/workspace/folders', {
parent: relativePath(parentPath),
name,
@@ -160,6 +199,12 @@ export async function createFolder(parentPath: string, name: string): Promise<Fi
}
export async function renameFile(oldPath: string, newName: string): Promise<void> {
if (isDesktop()) {
const path = nativePath(oldPath)
const document = await hostInvoke<HostDocument>('workspace_read', { path })
await hostInvoke('workspace_rename', { path, destination: [...path.split('/').slice(0, -1), newName].join('/'), expected: document.hash })
await refreshTree(); return
}
const path = normalizePublicPath(oldPath)
if (typeByPath.get(path) === 'folder') {
await apiClient.post('/api/workspace/folders/rename', {
@@ -173,6 +218,12 @@ export async function renameFile(oldPath: string, newName: string): Promise<void
}
export async function deleteFile(pathValue: string): Promise<void> {
if (isDesktop()) {
const path = nativePath(pathValue)
const document = await hostInvoke<HostDocument>('workspace_read', { path })
await hostInvoke('workspace_delete', { path, expected: document.hash })
await refreshTree(); return
}
const path = normalizePublicPath(pathValue)
if (typeByPath.get(path) === 'folder') {
await apiClient.post<OperationResponse>('/api/workspace/folders/delete', {
@@ -185,6 +236,12 @@ export async function deleteFile(pathValue: string): Promise<void> {
}
export async function moveFile(sourcePath: string, targetPath: string): Promise<void> {
if (isDesktop()) {
const path = nativePath(sourcePath)
const document = await hostInvoke<HostDocument>('workspace_read', { path })
await hostInvoke('workspace_rename', { path, destination: [nativePath(targetPath), path.split('/').at(-1)].filter(Boolean).join('/'), expected: document.hash })
await refreshTree(); return
}
const source = normalizePublicPath(sourcePath)
if (typeByPath.get(source) !== 'file') {
throw new Error(t('当前阶段只支持移动笔记文件。', 'Only note files can be moved at this stage.'))