Skip to content

Commit a05c3f4

Browse files
authored
fix(chat): restore image generation blocks (#1402)
* fix(chat): restore image generation blocks * test(chat): scope image accumulator test
1 parent de42096 commit a05c3f4

File tree

5 files changed

+73
-2
lines changed

5 files changed

+73
-2
lines changed

src/main/presenter/deepchatAgentPresenter/accumulator.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ export function accumulate(state: StreamState, event: LLMCoreStreamEvent): void
119119
}
120120
break
121121
}
122+
case 'image_data': {
123+
if (state.firstTokenTime === null) state.firstTokenTime = Date.now()
124+
const block: AssistantMessageBlock = {
125+
type: 'image',
126+
status: 'pending',
127+
timestamp: Date.now(),
128+
image_data: {
129+
data: event.image_data.data,
130+
mimeType: event.image_data.mimeType
131+
}
132+
}
133+
state.blocks.push(block)
134+
state.dirty = true
135+
break
136+
}
122137
case 'usage': {
123138
state.metadata.inputTokens = event.usage.prompt_tokens
124139
state.metadata.outputTokens = event.usage.completion_tokens

src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ const SUPPORTED_IMAGE_SIZES = {
6666
// Add list of models with configurable sizes
6767
const SIZE_CONFIGURABLE_MODELS = ['gpt-image-1', 'gpt-4o-image', 'gpt-4o-all']
6868

69+
export function normalizeExtractedImageText(content: string): string {
70+
const normalized = content
71+
.replace(/\r\n/g, '\n')
72+
.replace(/\n\s*\n/g, '\n')
73+
.trim()
74+
if (!normalized) {
75+
return ''
76+
}
77+
78+
const semanticText = normalized.replace(/[\`*_~!\[\]\(\)]/g, '').trim()
79+
return semanticText.length > 0 ? normalized : ''
80+
}
81+
6982
function getOpenAIChatCachedTokens(usage: unknown): number | undefined {
7083
if (!usage || typeof usage !== 'object') {
7184
return undefined
@@ -1294,7 +1307,7 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
12941307
// 如果处理了图片,清理多余的空行并记录日志
12951308
if (hasImages) {
12961309
// 清理移除图片后可能留下的多余空行
1297-
processedCurrentContent = processedCurrentContent.replace(/\n\s*\n/g, '\n').trim()
1310+
processedCurrentContent = normalizeExtractedImageText(processedCurrentContent)
12981311
console.log(
12991312
`[handleChatCompletion] Processed ${currentContent.length} chars -> ${processedCurrentContent.length} chars (images removed)`
13001313
)

src/shared/types/agent-interface.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export type AssistantBlockType =
210210
| 'error'
211211
| 'tool_call'
212212
| 'action'
213+
| 'image'
213214

214215
export interface ToolCallBlockData {
215216
id?: string
@@ -263,6 +264,10 @@ export interface AssistantMessageBlock {
263264
start: number
264265
end: number
265266
}
267+
image_data?: {
268+
data: string
269+
mimeType: string
270+
}
266271
tool_call?: ToolCallBlockData
267272
extra?: AssistantMessageExtra
268273
action_type?: 'tool_call_permission' | 'question_request'

test/main/presenter/deepchatAgentPresenter/accumulator.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,26 @@ describe('accumulate', () => {
235235
accumulate(state, { type: 'permission', permission: {} } as any)
236236
expect(state.blocks.length).toBe(blocksBefore)
237237
})
238+
239+
it('creates image blocks for image_data events without empty text blocks', () => {
240+
accumulate(state, {
241+
type: 'image_data',
242+
image_data: {
243+
data: 'imgcache://generated/test.png',
244+
mimeType: 'deepchat/image-url'
245+
}
246+
})
247+
248+
expect(state.blocks).toHaveLength(1)
249+
expect(state.blocks[0]).toMatchObject({
250+
type: 'image',
251+
status: 'pending',
252+
image_data: {
253+
data: 'imgcache://generated/test.png',
254+
mimeType: 'deepchat/image-url'
255+
}
256+
})
257+
expect(state.blocks.some((block) => block.type === 'content')).toBe(false)
258+
expect(state.dirty).toBe(true)
259+
})
238260
})

test/main/presenter/llmProviderPresenter/openAICompatibleProvider.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import type {
77
MCPToolDefinition,
88
ModelConfig
99
} from '../../../../src/shared/presenter'
10-
import { OpenAICompatibleProvider } from '../../../../src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider'
10+
import {
11+
OpenAICompatibleProvider,
12+
normalizeExtractedImageText
13+
} from '../../../../src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider'
1114
import { OpenRouterProvider } from '../../../../src/main/presenter/llmProviderPresenter/providers/openRouterProvider'
1215
import { LLMProviderPresenter } from '../../../../src/main/presenter/llmProviderPresenter'
1316

@@ -335,3 +338,16 @@ describe('OpenAICompatibleProvider MCP runtime injection', () => {
335338
expect(requestParams.tools).toEqual(convertedTools)
336339
})
337340
})
341+
342+
describe('normalizeExtractedImageText', () => {
343+
it('keeps meaningful text after image markdown cleanup', () => {
344+
expect(normalizeExtractedImageText(' Here is the updated image.\n\n')).toBe(
345+
'Here is the updated image.'
346+
)
347+
})
348+
349+
it('drops markdown residue after image markdown cleanup', () => {
350+
expect(normalizeExtractedImageText('`\n')).toBe('')
351+
expect(normalizeExtractedImageText('[]()')).toBe('')
352+
})
353+
})

0 commit comments

Comments
 (0)