✨ feat(vscode): 问 AI 输入框能粘贴、拖进图片随问题发给 Claude Code,缩略图能点开看大图,图片存进 .spx/code-qa 跟着问答删

This commit is contained in:
cruldra committed 2026-09-26 23:15:02 +08:00
1 parent d9a5c3673e
commit 36d842e18e
28 files changed
+825 -121

No files matched your search

+55 -1
View File
@@ -1,9 +1,11 @@
import type { QaEntry, QaMap } from './qaMap'
import { Buffer } from 'node:buffer'
import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { dropEntries, moveEntries, readPathMap, writePathMap } from '../pathMap'
import { detachedFiles, dropEntries, moveEntries, readPathMap, removeFiles, writePathMap } from '../pathMap'
import { writeQaImages } from './qaImages'
import { latestSession, QA_FILE, withEntry, withoutEntry } from './qaMap'
function entry(id: string, createdAt: string, question = `问 ${id}`): QaEntry {
@@ -143,3 +145,55 @@ describe('读写 .spx/code-qa.json', () => {
expect(await fsp.readFile(qaFile(), 'utf8')).toBe('{}\n')
})
})
describe('问题带的图片', () => {
const imageFile = (name: string): string => path.join(root, '.spx', 'code-qa', name)
const exists = (file: string): Promise<boolean> => fsp.access(file).then(() => true, () => false)
const png = (id: string): { mediaType: string, base64: string } => ({ mediaType: 'image/png', base64: Buffer.from(id).toString('base64') })
/** 同 FolderStore.update:读盘上的一份改好写回,再删掉不再有条目带着的文件 */
async function update(change: (map: QaMap) => QaMap): Promise<void> {
const before = await readPathMap(root, QA_FILE)
const after = change(before)
await writePathMap(root, QA_FILE, after)
await removeFiles(root, detachedFiles(QA_FILE, before, after))
}
it('记在这条上(.spx/code-qa/ 里的文件名)、读得回来;认不出的名字(跑到别处的路径、不是字符串)跳过,一张都不剩就没有这一项', async () => {
const withImages: QaEntry = { ...entry('p', '2026-09-26T10:00:00.000Z'), images: ['p-1.png', 'p-2.webp'] }
await writePathMap(root, QA_FILE, withEntry({}, 'a.py', IF, withImages))
expect(await fsp.readFile(qaFile(), 'utf8')).toContain('"images": [')
expect(await readPathMap(root, QA_FILE)).toEqual({ 'a.py': [{ anchor: IF, entries: [withImages] }] })
await writeRaw({ 'a.py': [{ anchor: IF, entries: [
{ ...entry('x', '2026-09-25T10:00:00.000Z'), images: ['../../secret-1.png', 'x-1.png', 7, 'x-2.exe'] },
{ ...entry('y', '2026-09-26T10:00:00.000Z'), images: ['../y-1.png'] },
{ ...entry('z', '2026-09-26T11:00:00.000Z'), images: 'z-1.png' },
] }] })
expect(await readPathMap(root, QA_FILE)).toEqual({ 'a.py': [{ anchor: IF, entries: [
{ ...entry('x', '2026-09-25T10:00:00.000Z'), images: ['x-1.png'] },
entry('y', '2026-09-26T10:00:00.000Z'),
entry('z', '2026-09-26T11:00:00.000Z'),
] }] })
})
it('删一次问答,它的图片跟着删;别的问答的图片留着', async () => {
const a = { ...entry('a', '2026-09-25T10:00:00.000Z'), images: await writeQaImages(root, 'a', [png('a1'), png('a2')]) }
const b = { ...entry('b', '2026-09-26T10:00:00.000Z'), images: await writeQaImages(root, 'b', [png('b1')]) }
await update(map => withEntry(withEntry(map, 'a.py', IF, a), 'a.py', DISPLAY, b))
expect(await exists(imageFile('a-1.png'))).toBe(true)
await update(map => withoutEntry(map, 'a.py', IF, 'a'))
expect(await exists(imageFile('a-1.png'))).toBe(false)
expect(await exists(imageFile('a-2.png'))).toBe(false)
expect(await exists(imageFile('b-1.png'))).toBe(true)
})
it('源文件删了、问答跟着删时图片也删;改名、挪位置按问答 id 起的名字不用动', async () => {
const a = { ...entry('a', '2026-09-25T10:00:00.000Z'), images: await writeQaImages(root, 'a', [png('a1')]) }
await update(map => withEntry(map, 'src/a.py', IF, a))
await update(map => moveEntries(map, 'src', 'lib'))
expect(await readPathMap(root, QA_FILE)).toEqual({ 'lib/a.py': [{ anchor: IF, entries: [a] }] })
expect(await exists(imageFile('a-1.png'))).toBe(true)
await update(map => dropEntries(map, 'lib/a.py'))
expect(await exists(imageFile('a-1.png'))).toBe(false)
})
})