✨ feat(vscode): 问 AI 输入框能粘贴、拖进图片随问题发给 Claude Code,缩略图能点开看大图,图片存进 .spx/code-qa 跟着问答删
This commit is contained in:
1 parent
d9a5c3673e
commit
36d842e18e
28 files changed
+825
-121
No files matched your search
@@ -30,12 +30,26 @@ function fakeClaude(runs: { lines: string[], error?: Error }[]): { deps: AskDeps
|
||||
|
||||
function request(overrides: Partial<AskRun> = {}): AskRun & { events: unknown[] } {
|
||||
const events: unknown[] = []
|
||||
return { prompt: '完整的问题', cwd: '/work', signal: new AbortController().signal, onEvent: event => events.push(event), events, ...overrides }
|
||||
return { prompt: '完整的问题', images: [], cwd: '/work', signal: new AbortController().signal, onEvent: event => events.push(event), events, ...overrides }
|
||||
}
|
||||
|
||||
const PNG = { mediaType: 'image/png', base64: 'iVBORw0KGgo=' }
|
||||
const JPEG = { mediaType: 'image/jpeg', base64: '/9j/4AAQ' }
|
||||
|
||||
/** 从 stdin 发出去的那条消息 */
|
||||
function sent(call: RunLinesOptions): { text: string, images: unknown[] } {
|
||||
expect(call.input?.endsWith('\n')).toBe(true)
|
||||
const message = JSON.parse(call.input ?? '') as { type: string, message: { role: string, content: { type: string, text?: string, source?: unknown }[] } }
|
||||
expect(message.type).toBe('user')
|
||||
expect(message.message.role).toBe('user')
|
||||
const [first, ...rest] = message.message.content
|
||||
expect(first.type).toBe('text')
|
||||
return { text: first.text ?? '', images: rest }
|
||||
}
|
||||
|
||||
describe('命令行参数', () => {
|
||||
it('只读:只给读、搜的工具,别的一律拒、不等人批,不加载 MCP;不带跳过权限;输出逐字的 stream-json', () => {
|
||||
const args = askArgs('问题', { profilePath: '/p/offical.json' })
|
||||
it('只读:只给读、搜的工具,别的一律拒、不等人批,不加载 MCP;不带跳过权限;问题从 stdin 按 stream-json 进,输出逐字的 stream-json', () => {
|
||||
const args = askArgs({ profilePath: '/p/offical.json' })
|
||||
expect(args).toEqual([
|
||||
'--settings',
|
||||
'/p/offical.json',
|
||||
@@ -47,7 +61,8 @@ describe('命令行参数', () => {
|
||||
'dontAsk',
|
||||
'--strict-mcp-config',
|
||||
'-p',
|
||||
'问题',
|
||||
'--input-format',
|
||||
'stream-json',
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'--verbose',
|
||||
@@ -56,10 +71,41 @@ describe('命令行参数', () => {
|
||||
expect(args.join(' ')).not.toMatch(/dangerously|bypassPermissions|Edit|Write|Bash/)
|
||||
})
|
||||
|
||||
it('追问带 --resume,放在问题前面;没有 profile 就不带 --settings', () => {
|
||||
const args = askArgs('追问', { resume: 'session-1' })
|
||||
it('追问带 --resume,放在 -p 前面;没有 profile 就不带 --settings', () => {
|
||||
const args = askArgs({ resume: 'session-1' })
|
||||
expect(args).not.toContain('--settings')
|
||||
expect(args.slice(args.indexOf('--resume'), args.indexOf('--resume') + 4)).toEqual(['--resume', 'session-1', '-p', '追问'])
|
||||
expect(args.slice(args.indexOf('--resume'), args.indexOf('--resume') + 3)).toEqual(['--resume', 'session-1', '-p'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('问题连同图片从 stdin 发', () => {
|
||||
it('一条用户消息:先是问题的文字,后面按贴的先后跟着图片(base64 和格式原样)', async () => {
|
||||
const { deps, calls } = fakeClaude([{ lines: [result('答案', 's1')] }])
|
||||
await askClaude(request({ images: [PNG, JPEG] }), deps)
|
||||
expect(sent(calls[0])).toEqual({
|
||||
text: '完整的问题',
|
||||
images: [
|
||||
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'iVBORw0KGgo=' } },
|
||||
{ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: '/9j/4AAQ' } },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('没贴图:只有问题的文字,照样走 stdin', async () => {
|
||||
const { deps, calls } = fakeClaude([{ lines: [result('答案', 's1')] }])
|
||||
await askClaude(request(), deps)
|
||||
expect(sent(calls[0])).toEqual({ text: '完整的问题', images: [] })
|
||||
expect(calls[0].args).not.toContain('完整的问题')
|
||||
})
|
||||
|
||||
it('追问、会话找不到重新问:两次都带上图片', async () => {
|
||||
const { deps, calls } = fakeClaude([
|
||||
{ lines: [], error: new ClaudeError('Claude 退出码非零 (1): No conversation found with session ID: s0', 'No conversation found with session ID: s0') },
|
||||
{ lines: [result('重新答', 's2')] },
|
||||
])
|
||||
await askClaude(request({ images: [PNG], followUp: { sessionId: 's0', prompt: '短问题' } }), deps)
|
||||
expect(sent(calls[0])).toMatchObject({ text: '短问题', images: [{ source: { data: PNG.base64 } }] })
|
||||
expect(sent(calls[1])).toMatchObject({ text: '完整的问题', images: [{ source: { data: PNG.base64 } }] })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,7 +118,7 @@ describe('问一次', () => {
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]).toMatchObject({ cwd: '/work', signal: run.signal, timeoutMs: 300_000 })
|
||||
expect(calls[0].args.slice(0, 2)).toEqual(['--settings', '/profiles/sonnet.json'])
|
||||
expect(calls[0].args[calls[0].args.indexOf('-p') + 1]).toBe('完整的问题')
|
||||
expect(sent(calls[0]).text).toBe('完整的问题')
|
||||
})
|
||||
|
||||
it('选的是「默认」(没给 profile):不带 --settings', async () => {
|
||||
@@ -85,7 +131,8 @@ describe('问一次', () => {
|
||||
const { deps, calls } = fakeClaude([{ lines: [result('接着答', 's1')] }])
|
||||
expect(await askClaude(request({ profilePath: '/profiles/opus.json', followUp: { sessionId: 's1', prompt: '短问题' } }), deps)).toEqual({ text: '接着答', sessionId: 's1' })
|
||||
expect(calls[0].args.slice(0, 2)).toEqual(['--settings', '/profiles/opus.json'])
|
||||
expect(calls[0].args.slice(calls[0].args.indexOf('--resume'), calls[0].args.indexOf('--resume') + 4)).toEqual(['--resume', 's1', '-p', '短问题'])
|
||||
expect(calls[0].args.slice(calls[0].args.indexOf('--resume'), calls[0].args.indexOf('--resume') + 2)).toEqual(['--resume', 's1'])
|
||||
expect(sent(calls[0]).text).toBe('短问题')
|
||||
})
|
||||
|
||||
it('上次的会话找不到了:按完整问题重新开一次', async () => {
|
||||
@@ -97,7 +144,7 @@ describe('问一次', () => {
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls[1].args).not.toContain('--resume')
|
||||
expect(calls[1].args.slice(0, 2)).toEqual(['--settings', '/profiles/opus.json'])
|
||||
expect(calls[1].args[calls[1].args.indexOf('-p') + 1]).toBe('完整的问题')
|
||||
expect(sent(calls[1]).text).toBe('完整的问题')
|
||||
})
|
||||
|
||||
it('别的失败不重试,照样抛出来;Claude 报的失败、没给回答也算失败', async () => {
|
||||
|
||||
Reference in new issue
Block a user