Refactor docx file paths and update test fixtures for workspace integration; add caching for KB fixture vectors

This commit is contained in:
hy committed 2026-04-29 16:37:52 +08:00
1 parent 47f93e03e3
commit 6f51d22c95
10 files changed
+205 -9

No files matched your search

+1
View File
@@ -6,4 +6,5 @@ out
.eslintcache
*.log*
tests/logs
tests/.cache
artifacts/ai-test-results
+1 -2
View File
@@ -287,8 +287,7 @@ export function registerHandlers(services: {
process.cwd(),
'tests',
'fixtures',
'ai',
'docx',
'workspace',
'sample-edit.docx'
)
@@ -45,8 +45,8 @@ type DocxToolSpec<
activityActionType?: string
}
const DOCX_SOURCE_PATH_EXAMPLE = 'ai/docx/图形学.docx'
const DOCX_CREATE_TARGET_EXAMPLE = 'ai/docx/深度学习与图形学复习总结.docx'
const DOCX_SOURCE_PATH_EXAMPLE = '学习笔记/图形学.docx'
const DOCX_CREATE_TARGET_EXAMPLE = '深度学习与图形学复习总结.docx'
const DOCX_SEARCH_LITERAL_EXAMPLE: JsonObject = {
sourcePath: DOCX_SOURCE_PATH_EXAMPLE,
query: '反走样'
@@ -8,7 +8,7 @@ import {
type DocxSearchResult
} from './testUtils'
const SAMPLE_DOCX_FIXTURE = path.resolve('tests/fixtures/ai/docx/sample-edit.docx')
const SAMPLE_DOCX_FIXTURE = path.resolve('tests/fixtures/workspace/sample-edit.docx')
describe('ai tool docx backend', () => {
it('docx-minimal-edit-flow', async () => {
@@ -20,7 +20,7 @@ type PositionedOutlineResult = {
}>
}
const POSITIONED_FIXTURE_PATH = 'tests/fixtures/ai/docx/图形学.docx'
const POSITIONED_FIXTURE_PATH = 'tests/fixtures/workspace/学习笔记/图形学.docx'
const POSITIONED_READ_ONLY_MESSAGE = '当前命中复杂定位排版区域,暂不支持自动编辑'
describe('ai tool docx backend', () => {
Binary file not shown.
Binary file not shown.
Binary file not shown.
+181
View File
@@ -0,0 +1,181 @@
import { createHash } from 'node:crypto'
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import type { EmbeddingService } from '../../../src/main/services/vector/EmbeddingService'
import type { SeedKbDocumentInput } from './test-types'
const KB_FIXTURE_CACHE_SCHEMA_VERSION = 1
const KB_FIXTURE_CACHE_ROOT = path.resolve(process.cwd(), 'tests', '.cache', 'kb-fixtures')
type CacheableKbChunk = {
id: string
text: string
page: number
}
type CacheableKbDocument = {
id: string
name: string
sourceType: 'file' | 'web'
chunks: CacheableKbChunk[]
}
type CachedKbChunkVector = CacheableKbChunk & {
vector: number[]
}
type CachedKbDocumentVectors = {
id: string
chunks: CachedKbChunkVector[]
}
type KbFixtureCacheFile = {
schemaVersion: 1
modelName: string
documents: CachedKbDocumentVectors[]
}
type KbFixtureVectorSnapshot = {
documents: CachedKbDocumentVectors[]
}
const normalizeSourceType = (value?: 'file' | 'web'): 'file' | 'web' => value || 'file'
const toCacheableDocuments = (documents: readonly SeedKbDocumentInput[]): CacheableKbDocument[] =>
[...documents]
.map((document) => ({
id: document.id,
name: document.name,
sourceType: normalizeSourceType(document.sourceType),
chunks: [...document.chunks]
.map((chunk) => ({
id: chunk.id,
text: chunk.text,
page: chunk.page || 1
}))
.sort((left, right) => left.id.localeCompare(right.id))
}))
.sort((left, right) => left.id.localeCompare(right.id))
const buildCacheKey = (
modelName: string,
documents: readonly CacheableKbDocument[]
): string =>
createHash('sha256')
.update(
JSON.stringify({
schemaVersion: KB_FIXTURE_CACHE_SCHEMA_VERSION,
modelName,
documents
})
)
.digest('hex')
const buildCacheFilePath = (cacheKey: string): string =>
path.join(KB_FIXTURE_CACHE_ROOT, `${cacheKey}.json`)
const loadCachedSnapshot = async (
cacheFilePath: string,
modelName: string
): Promise<KbFixtureVectorSnapshot | null> => {
try {
await access(cacheFilePath)
} catch {
return null
}
const file = JSON.parse(await readFile(cacheFilePath, 'utf8')) as KbFixtureCacheFile
if (
file.schemaVersion !== KB_FIXTURE_CACHE_SCHEMA_VERSION ||
file.modelName !== modelName
) {
return null
}
return {
documents: file.documents
}
}
const persistCachedSnapshot = async (
cacheFilePath: string,
modelName: string,
snapshot: KbFixtureVectorSnapshot
): Promise<void> => {
const payload: KbFixtureCacheFile = {
schemaVersion: KB_FIXTURE_CACHE_SCHEMA_VERSION,
modelName,
documents: snapshot.documents
}
await mkdir(path.dirname(cacheFilePath), { recursive: true })
await writeFile(cacheFilePath, JSON.stringify(payload), 'utf8')
}
const buildVectorSnapshot = async (
documents: readonly CacheableKbDocument[],
embeddingService: EmbeddingService
): Promise<KbFixtureVectorSnapshot> => {
if (documents.length === 0) {
return {
documents: []
}
}
const flattenedChunks = documents.flatMap((document) =>
document.chunks.map((chunk) => ({
documentId: document.id,
chunk
}))
)
if (flattenedChunks.length === 0) {
return {
documents: documents.map((document) => ({
id: document.id,
chunks: []
}))
}
}
const embeddingTexts = flattenedChunks.map((item) => item.chunk.text)
const embeddings = await embeddingService.embed(embeddingTexts)
const vectorSize = embeddings.dims[1]
const vectors = flattenedChunks.map((_, index) =>
Array.from(
embeddings.data.slice(index * vectorSize, (index + 1) * vectorSize)
)
)
return {
documents: documents.map((document) => ({
id: document.id,
chunks: document.chunks.map((chunk) => {
const flattenedIndex = flattenedChunks.findIndex(
(item) => item.documentId === document.id && item.chunk.id === chunk.id
)
return {
...chunk,
vector: vectors[flattenedIndex] || []
}
})
}))
}
}
export const loadOrBuildKbFixtureVectors = async (input: {
embeddingService: EmbeddingService
documents: readonly SeedKbDocumentInput[]
}): Promise<KbFixtureVectorSnapshot> => {
const cacheableDocuments = toCacheableDocuments(input.documents)
const modelName = input.embeddingService.getModelName()
const cacheKey = buildCacheKey(modelName, cacheableDocuments)
const cacheFilePath = buildCacheFilePath(cacheKey)
const cachedSnapshot = await loadCachedSnapshot(cacheFilePath, modelName)
if (cachedSnapshot) {
return cachedSnapshot
}
const builtSnapshot = await buildVectorSnapshot(cacheableDocuments, input.embeddingService)
await persistCachedSnapshot(cacheFilePath, modelName, builtSnapshot)
return builtSnapshot
}
+18 -3
View File
@@ -1,6 +1,7 @@
import type { DocumentsStore } from '../../../src/main/services/storage/domains/DocumentsStore'
import type { EmbeddingService } from '../../../src/main/services/vector/EmbeddingService'
import type { SeedKbDocumentInput } from './test-types'
import { loadOrBuildKbFixtureVectors } from './kbFixtureCache'
export const seedKbFixture = async (input: {
documentsStore: DocumentsStore
@@ -17,6 +18,11 @@ export const seedKbFixture = async (input: {
)
}
const vectorSnapshot = await loadOrBuildKbFixtureVectors({
embeddingService: input.embeddingService,
documents: input.documents
})
for (const document of input.documents) {
const workspaceId = String(document.workspaceId || 'test-workspace').trim()
await input.documentsStore.addDocument({
@@ -40,10 +46,19 @@ export const seedKbFixture = async (input: {
page: chunk.page || 1
}
}))
const embeddings = await input.embeddingService.embed(chunks.map((chunk) => chunk.text))
const vectorList = chunks.map((_, index) =>
embeddings.data.slice(index * embeddings.dims[1], (index + 1) * embeddings.dims[1])
const cachedDocument = vectorSnapshot.documents.find(
(item) => item.id === document.id
)
if (!cachedDocument) {
throw new Error(`cached KB vectors missing document: ${document.id}`)
}
const vectorList = document.chunks.map((chunk) => {
const cachedChunk = cachedDocument.chunks.find((item) => item.id === chunk.id)
if (!cachedChunk) {
throw new Error(`cached KB vectors missing chunk: ${document.id}/${chunk.id}`)
}
return Float32Array.from(cachedChunk.vector)
})
await input.documentsStore.addChunks({
vectors: vectorList,