feat(workspace): 完成图片资产存储与引用
This commit is contained in:
@@ -158,8 +158,8 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
}
|
||||
|
||||
export const apiClient = {
|
||||
postBinary<T>(path: string, body: Blob, headers: Record<string, string> = { 'Content-Type': 'application/zip' }) {
|
||||
return request<T>(path, { method: 'POST', body, headers })
|
||||
postBinary<T>(path: string, body: Blob, headers: Record<string, string> = { 'Content-Type': 'application/zip' }, params?: RequestOptions['params']) {
|
||||
return request<T>(path, { method: 'POST', body, headers, params })
|
||||
},
|
||||
get<T>(path: string, options?: Omit<RequestOptions, 'method'>) {
|
||||
return request<T>(path, { ...options, method: 'GET' })
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveWorkspaceAssetPath, workspaceAssetReference } from './workspaceService'
|
||||
|
||||
describe('workspace asset paths', () => {
|
||||
it('creates portable references relative to the note', () => {
|
||||
expect(workspaceAssetReference('/课程/系统/调度.md', 'attachments/ab/hash.png')).toBe('../../attachments/ab/hash.png')
|
||||
expect(resolveWorkspaceAssetPath('/课程/系统/调度.md', '../../attachments/ab/hash.png')).toBe('attachments/ab/hash.png')
|
||||
})
|
||||
|
||||
it('does not resolve remote URLs or paths escaping the vault', () => {
|
||||
expect(resolveWorkspaceAssetPath('/a.md', 'https://example.com/image.png')).toBeNull()
|
||||
expect(resolveWorkspaceAssetPath('/a.md', '../attachments/ab/hash.png')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,17 @@ 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'
|
||||
|
||||
export interface WorkspaceAsset {
|
||||
asset_id: string
|
||||
path: string
|
||||
content_hash: string
|
||||
media_type: string
|
||||
size: number
|
||||
original_name: string
|
||||
}
|
||||
|
||||
export type WorkspaceAssetSource = 'paste' | 'drop' | 'upload'
|
||||
|
||||
/** Web 联调只连接 AI Core 配置的单一 Vault;多 Vault 选择由 Tauri Host 接管。 */
|
||||
export interface VaultInfo {
|
||||
vault_id: string
|
||||
@@ -147,6 +158,41 @@ export async function readFileContent(filePath: string): Promise<string> {
|
||||
return note.markdown
|
||||
}
|
||||
|
||||
/** 将 Vault 根路径转换为相对当前笔记的可移植 Markdown 引用。 */
|
||||
export function workspaceAssetReference(notePath: string, assetPath: string): string {
|
||||
const noteParts = relativePath(notePath).split('/').filter(Boolean)
|
||||
noteParts.pop()
|
||||
return `${'../'.repeat(noteParts.length)}${relativePath(assetPath)}`
|
||||
}
|
||||
|
||||
/** 将笔记内的相对附件引用还原为 Vault 根路径。 */
|
||||
export function resolveWorkspaceAssetPath(notePath: string, reference: string): string | null {
|
||||
if (/^(?:[a-z]+:|\/\/|#)/i.test(reference)) return null
|
||||
const parts = [...relativePath(notePath).split('/').slice(0, -1)]
|
||||
for (const part of reference.replace(/\\/g, '/').split('/')) {
|
||||
if (!part || part === '.') continue
|
||||
if (part === '..') { if (!parts.length) return null; parts.pop() }
|
||||
else parts.push(part)
|
||||
}
|
||||
const path = parts.join('/')
|
||||
return path.startsWith('attachments/') ? path : null
|
||||
}
|
||||
|
||||
export async function storeWorkspaceImage(
|
||||
file: Blob & { name?: string }, source: WorkspaceAssetSource,
|
||||
notePath: string, noteId: string | null,
|
||||
): Promise<WorkspaceAsset & { reference: string }> {
|
||||
const asset = await apiClient.postBinary<WorkspaceAsset>('/api/workspace/assets', file, { 'Content-Type': 'application/octet-stream' }, {
|
||||
filename: file.name || 'image', note_id: noteId || '', note_path: notePath, source,
|
||||
})
|
||||
return { ...asset, reference: workspaceAssetReference(notePath, asset.path) }
|
||||
}
|
||||
|
||||
export async function loadWorkspaceImage(path: string, notePath = '', noteId: string | null = null): Promise<Blob> {
|
||||
const response = await apiClient.get<Response>('/api/workspace/assets/content', { params: { path, note_path: notePath, note_id: noteId || '' } })
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
/** 解析已与工作空间路径关联的后端笔记标识。 */
|
||||
export async function getNoteId(filePath: string): Promise<string> {
|
||||
return requireNoteId(filePath)
|
||||
|
||||
Reference in New Issue
Block a user