Add HTTP PaddleOCR-VL settings and shared document previews for chat and knowledge imports. Preserve attachment originals and parsed assets across drafts, messages and queues; support explicit image OCR, selecting document images into a conversation draft, and restoring queued input for editing. Protect unsaved note bodies and titles during navigation and asynchronous updates. Match canvas analysis to source revisions, invalidate affected comments, preserve annotations during pagination, and provide a resizable record index with saved width preferences. Existing OCR settings retain local behavior. Historical attachments without saved originals are not reconstructed. Advanced OCR options depend on the configured service; image sending depends on the effective model and Runtime. Validation: full typecheck and lint passed; 477 focused tests passed across 12 files; staged whitespace checks passed. The full repository suite was not rerun for this commit. Release note: Keep note drafts safe while switching workspaces, resize the record index, and revisit saved document attachments with OCR previews and selected images in chat.
194 lines
6.4 KiB
TypeScript
194 lines
6.4 KiB
TypeScript
import {
|
|
mkdtemp,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
writeFile
|
|
} from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
defaultDocumentParsingSettings,
|
|
DocumentParsingSettingsStore
|
|
} from './document-parsing-settings-store'
|
|
import { defaultHttpOcrSettings } from '../shared/document-parsing-contracts'
|
|
|
|
const temporaryDirectories: string[] = []
|
|
|
|
async function createStore(): Promise<{
|
|
directory: string
|
|
filePath: string
|
|
store: DocumentParsingSettingsStore
|
|
}> {
|
|
const directory = await mkdtemp(
|
|
join(tmpdir(), 'goodbuddy-document-parsing-settings-')
|
|
)
|
|
temporaryDirectories.push(directory)
|
|
const filePath = join(directory, 'document-parsing-settings.json')
|
|
return {
|
|
directory,
|
|
filePath,
|
|
store: new DocumentParsingSettingsStore(filePath)
|
|
}
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryDirectories.splice(0).map((directory) =>
|
|
rm(directory, { recursive: true, force: true })
|
|
)
|
|
)
|
|
})
|
|
|
|
describe('DocumentParsingSettingsStore', () => {
|
|
it('freezes credentials with the task settings and keeps hidden provider configuration', async () => {
|
|
const { filePath } = await createStore()
|
|
const cipher = { isAvailable: () => true, encrypt: (value: string) => Buffer.from(value).reverse(), decrypt: (value: Buffer) => Buffer.from(value).reverse().toString('utf8') }
|
|
const store = new DocumentParsingSettingsStore(filePath, cipher)
|
|
await store.update({ ...defaultDocumentParsingSettings, ocrProvider: 'paddleocr-vl', httpOcr: { ...defaultHttpOcrSettings, baseUrl: 'http://first.test', authentication: 'bearer' }, apiKey: 'fixture-key-a' })
|
|
const operation = await store.forOperation()
|
|
await store.update({ ...(await store.get()), ocrProvider: 'local', httpOcr: { ...defaultHttpOcrSettings, baseUrl: 'http://second.test', authentication: 'bearer' }, apiKey: 'fixture-key-b' })
|
|
expect(operation.settings.httpOcr?.baseUrl).toBe('http://first.test')
|
|
expect(operation.apiKey()).toBe('fixture-key-a')
|
|
expect(await store.getApiKey()).toBe('fixture-key-b')
|
|
expect(JSON.stringify(await store.get())).not.toContain('fixture-key')
|
|
expect(await readFile(filePath, 'utf8')).not.toContain('fixture-key')
|
|
await store.update({ ...(await store.get()), apiKey: '' })
|
|
expect(await store.getApiKey()).toBe('fixture-key-b')
|
|
await store.update({ ...(await store.get()), clearApiKey: true })
|
|
expect(await store.getApiKey()).toBeUndefined()
|
|
})
|
|
it('returns local-first defaults without creating a file', async () => {
|
|
const { directory, store } = await createStore()
|
|
|
|
await expect(store.get()).resolves.toEqual(
|
|
defaultDocumentParsingSettings
|
|
)
|
|
expect(store.getWarnings()).toEqual([])
|
|
await expect(readdir(directory)).resolves.toEqual([])
|
|
})
|
|
|
|
it('persists a complete versioned settings document', async () => {
|
|
const { filePath, store } = await createStore()
|
|
const settings = {
|
|
...defaultDocumentParsingSettings,
|
|
chatWorkflow: 'fast-text' as const,
|
|
maximumPages: 42
|
|
}
|
|
|
|
await expect(store.update(settings)).resolves.toEqual(settings)
|
|
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
|
version: 4,
|
|
...settings
|
|
})
|
|
await expect(
|
|
new DocumentParsingSettingsStore(filePath).get()
|
|
).resolves.toEqual(settings)
|
|
})
|
|
|
|
it('migrates version 2 OCR switches into scenario modes', async () => {
|
|
const { filePath, store } = await createStore()
|
|
await writeFile(
|
|
filePath,
|
|
JSON.stringify({
|
|
version: 2,
|
|
chatWorkflow: 'auto',
|
|
knowledgeWorkflow: 'complete-index',
|
|
pdfOcrMode: 'always',
|
|
ocrProvider: 'local',
|
|
localOcrEnabled: true,
|
|
localOcrModelId: 'pp-ocrv6-small',
|
|
maximumPages: 42,
|
|
ocrConcurrency: 4,
|
|
pageTimeoutSeconds: 90
|
|
}),
|
|
'utf8'
|
|
)
|
|
|
|
await expect(store.get()).resolves.toEqual({
|
|
chatWorkflow: 'high-fidelity',
|
|
knowledgeWorkflow: 'high-fidelity',
|
|
localOcrModelId: 'pp-ocrv6-small',
|
|
maximumPages: 42,
|
|
pageTimeoutSeconds: 90
|
|
})
|
|
})
|
|
|
|
it('migrates version 1 cloud settings into local scenario modes', async () => {
|
|
const { filePath, store } = await createStore()
|
|
await writeFile(
|
|
filePath,
|
|
JSON.stringify({
|
|
version: 1,
|
|
chatWorkflow: 'auto',
|
|
knowledgeWorkflow: 'complete-index',
|
|
pdfOcrMode: 'auto',
|
|
localOcrEnabled: false,
|
|
localOcrModelId: 'pp-ocrv6-tiny',
|
|
maximumPages: 100,
|
|
ocrConcurrency: 1,
|
|
pageTimeoutSeconds: 60,
|
|
chatCloudPermission: 'always',
|
|
knowledgeCloudPermission: 'never'
|
|
}),
|
|
'utf8'
|
|
)
|
|
|
|
await expect(store.get()).resolves.toEqual({
|
|
...defaultDocumentParsingSettings,
|
|
chatWorkflow: 'fast-text',
|
|
knowledgeWorkflow: 'fast-index'
|
|
})
|
|
})
|
|
|
|
it('rejects incomplete or out-of-range settings', async () => {
|
|
const { directory, store } = await createStore()
|
|
|
|
await expect(store.update({})).rejects.toThrow()
|
|
await expect(
|
|
store.update({
|
|
...defaultDocumentParsingSettings,
|
|
maximumPages: 0
|
|
})
|
|
).rejects.toThrow()
|
|
await expect(readdir(directory)).resolves.toEqual([])
|
|
})
|
|
|
|
it('isolates corrupt settings and restores defaults', async () => {
|
|
const { directory, filePath, store } = await createStore()
|
|
await writeFile(filePath, '{not-json', 'utf8')
|
|
|
|
await expect(store.get()).resolves.toEqual(
|
|
defaultDocumentParsingSettings
|
|
)
|
|
expect(store.getWarnings()).toEqual([
|
|
{ code: 'document-parsing-settings-recovered' }
|
|
])
|
|
const entries = await readdir(directory)
|
|
expect(entries).toHaveLength(1)
|
|
expect(entries[0]).toMatch(
|
|
/^document-parsing-settings\.json\.corrupt-\d+-[a-f0-9]{12}$/u
|
|
)
|
|
})
|
|
|
|
it('preserves settings created by a newer unsupported version', async () => {
|
|
const { directory, filePath, store } = await createStore()
|
|
const futureSettings = JSON.stringify({
|
|
version: 99,
|
|
futureField: 'keep-me'
|
|
})
|
|
await writeFile(filePath, futureSettings, 'utf8')
|
|
|
|
await expect(store.get()).rejects.toThrow(
|
|
'不支持文档解析设置版本 99'
|
|
)
|
|
expect(await readFile(filePath, 'utf8')).toBe(futureSettings)
|
|
expect(
|
|
(await readdir(directory)).some((name) =>
|
|
name.startsWith('document-parsing-settings.json.corrupt-')
|
|
)
|
|
).toBe(false)
|
|
})
|
|
})
|