Skip to content

Commit 891deff

Browse files
fix: network timeout error (chatgpt-web-dev#671)
1 parent 3c34e41 commit 891deff

File tree

6 files changed

+123
-50
lines changed

6 files changed

+123
-50
lines changed

service/src/chatgpt/index.ts

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -300,60 +300,81 @@ search result: <search_result>${searchResultContent}</search_result>`,
300300
const toolCalls: Array<{ type: string, result?: any }> = []
301301
let editImageId: string | undefined
302302

303-
for await (const event of stream) {
304-
if (event.type === 'response.reasoning_summary_text.delta') {
305-
const delta: string = event.delta || ''
306-
responseReasoning += delta
307-
process?.({
308-
delta: { reasoning: delta },
309-
})
310-
}
311-
else if (event.type === 'response.reasoning_summary_text.done') {
312-
responseReasoning += '\n'
313-
process?.({
314-
delta: { reasoning: '\n' },
315-
})
316-
}
317-
else if (event.type === 'response.output_text.delta') {
318-
const delta: string = event.delta || ''
319-
responseText += delta
320-
process?.({
321-
text: responseText,
322-
delta: { text: delta },
323-
})
324-
}
325-
else if (event.type === 'response.completed') {
326-
const resp = event.response
327-
responseId = resp.id
328-
usage.prompt_tokens = resp.usage.input_tokens
329-
usage.completion_tokens = resp.usage.output_tokens
330-
usage.total_tokens = resp.usage.total_tokens
331-
332-
// Extract tool calls from response
333-
if (resp.output && Array.isArray(resp.output)) {
334-
editImageId = responseId
335-
for (const output of resp.output) {
336-
if (output.type === 'image_generation_call' && output.result) {
337-
const base64Data = output.result
338-
const fileIdentifier = await saveBase64ToFile(base64Data)
339-
340-
if (fileIdentifier) {
341-
toolCalls.push({
342-
type: 'image_generation',
343-
result: fileIdentifier, // 文件名或S3 URL,前端会自动处理
344-
})
345-
}
346-
else {
347-
toolCalls.push({
348-
type: 'image_generation',
349-
result: base64Data,
350-
})
303+
// 心跳机制:防止生图等长时间操作时连接超时
304+
let heartbeatInterval: NodeJS.Timeout | null = null
305+
const HEARTBEAT_INTERVAL = 30000 // 30秒发送一次心跳
306+
307+
// 启动心跳定时器
308+
heartbeatInterval = setInterval(() => {
309+
// 发送心跳数据,保持连接活跃
310+
process?.({
311+
delta: { heartbeat: true },
312+
})
313+
}, HEARTBEAT_INTERVAL)
314+
315+
try {
316+
for await (const event of stream) {
317+
if (event.type === 'response.reasoning_summary_text.delta') {
318+
const delta: string = event.delta || ''
319+
responseReasoning += delta
320+
process?.({
321+
delta: { reasoning: delta },
322+
})
323+
}
324+
else if (event.type === 'response.reasoning_summary_text.done') {
325+
responseReasoning += '\n'
326+
process?.({
327+
delta: { reasoning: '\n' },
328+
})
329+
}
330+
else if (event.type === 'response.output_text.delta') {
331+
const delta: string = event.delta || ''
332+
responseText += delta
333+
process?.({
334+
text: responseText,
335+
delta: { text: delta },
336+
})
337+
}
338+
else if (event.type === 'response.completed') {
339+
const resp = event.response
340+
responseId = resp.id
341+
usage.prompt_tokens = resp.usage.input_tokens
342+
usage.completion_tokens = resp.usage.output_tokens
343+
usage.total_tokens = resp.usage.total_tokens
344+
345+
// Extract tool calls from response
346+
if (resp.output && Array.isArray(resp.output)) {
347+
editImageId = responseId
348+
for (const output of resp.output) {
349+
if (output.type === 'image_generation_call' && output.result) {
350+
const base64Data = output.result
351+
const fileIdentifier = await saveBase64ToFile(base64Data)
352+
353+
if (fileIdentifier) {
354+
toolCalls.push({
355+
type: 'image_generation',
356+
result: fileIdentifier, // 文件名或S3 URL,前端会自动处理
357+
})
358+
}
359+
else {
360+
toolCalls.push({
361+
type: 'image_generation',
362+
result: base64Data,
363+
})
364+
}
351365
}
352366
}
353367
}
354368
}
355369
}
356370
}
371+
finally {
372+
// 清除心跳定时器
373+
if (heartbeatInterval) {
374+
clearInterval(heartbeatInterval)
375+
heartbeatInterval = null
376+
}
377+
}
357378

358379
const response = {
359380
id: responseId || messageId,

service/src/chatgpt/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface ResponseChunk {
2626
delta?: {
2727
reasoning?: string
2828
text?: string
29+
heartbeat?: boolean
2930
}
3031
// 工具调用结果
3132
tool_calls?: Array<{

service/src/routes/chat.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ router.get('/chat-history', auth, async (req, res) => {
6767
result.push({
6868
uuid: c.uuid,
6969
model: c.model,
70-
dateTime: new Date(c.dateTime),
70+
// 用户消息使用 promptDateTime(如果存在),否则使用 dateTime(兼容旧数据)
71+
dateTime: new Date(c.promptDateTime || c.dateTime),
7172
text: c.prompt,
7273
images: c.images,
7374
inversion: true,
@@ -92,6 +93,7 @@ router.get('/chat-history', auth, async (req, res) => {
9293
// Build response object with tool-related fields
9394
const responseObj: any = {
9495
uuid: c.uuid,
96+
// AI回复使用 dateTime(AI回复完成时间)
9597
dateTime: new Date(c.dateTime),
9698
searchQuery: c.searchQuery,
9799
searchResults: c.searchResults,
@@ -411,6 +413,9 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
411413
.map((tool: any) => tool.result)
412414
}
413415

416+
// 判断是否是生图请求(有 tool_images 或 tool_calls 中包含 image_generation)
417+
const isImageGeneration = tool_images && tool_images.length > 0
418+
414419
if (regenerate && message.options.messageId) {
415420
const previousResponse = message.previousResponse || []
416421
previousResponse.push({ response: message.response, options: message.options })
@@ -425,6 +430,7 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
425430
tool_images,
426431
tool_calls,
427432
editImageId,
433+
isImageGeneration, // 生图时更新完成时间
428434
)
429435
}
430436
else {
@@ -439,6 +445,7 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
439445
tool_images,
440446
tool_calls,
441447
editImageId,
448+
isImageGeneration, // 生图时更新完成时间
442449
)
443450
}
444451

service/src/storage/model.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export class ChatInfo {
133133
model: string
134134
uuid: number
135135
dateTime: number
136+
promptDateTime?: number // 用户消息的创建时间
136137
prompt: string
137138
images?: string[]
138139
searchQuery?: string
@@ -153,7 +154,9 @@ export class ChatInfo {
153154
this.prompt = prompt
154155
this.images = images
155156
this.options = options
156-
this.dateTime = new Date().getTime()
157+
const now = new Date().getTime()
158+
this.dateTime = now
159+
this.promptDateTime = now // 保存用户消息的创建时间
157160
}
158161
}
159162

service/src/storage/mongo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export async function updateChat(
267267
tool_images?: string[],
268268
tool_calls?: Array<{ type: string, result?: any }>,
269269
editImageId?: string,
270+
updateDateTime?: boolean,
270271
) {
271272
const query = { _id: new ObjectId(chatId) }
272273
const update: any = {
@@ -294,6 +295,9 @@ export async function updateChat(
294295
if (editImageId)
295296
update.$set.editImageId = editImageId
296297

298+
if (updateDateTime || (response && response.trim().length > 0))
299+
update.$set.dateTime = new Date().getTime()
300+
297301
await chatCol.updateOne(query, update)
298302
}
299303

src/views/chat/index.vue

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,30 @@ const currentNavIndexRef = ref<number>(-1)
5454
// 存储上一次工具调用的响应ID,用于传递 previousResponseId
5555
const lastToolResponseId = ref<string>('')
5656
57+
// 从历史记录中初始化 lastToolResponseId
58+
function initLastToolResponseId() {
59+
// 遍历历史记录,找到最后一个有 editImageId 的消息
60+
for (let i = dataSources.value.length - 1; i >= 0; i--) {
61+
const chat = dataSources.value[i]
62+
if (!chat.inversion && chat.editImageId) {
63+
lastToolResponseId.value = chat.editImageId
64+
return
65+
}
66+
}
67+
// 如果没有找到 editImageId,尝试使用最后一个 assistant 消息的 conversationOptions.parentMessageId
68+
// 只有当 parentMessageId 以 resp_ 开头时才使用
69+
for (let i = dataSources.value.length - 1; i >= 0; i--) {
70+
const chat = dataSources.value[i]
71+
const parentMessageId = chat.conversationOptions?.parentMessageId
72+
if (!chat.inversion && parentMessageId && parentMessageId.startsWith('resp_')) {
73+
lastToolResponseId.value = parentMessageId
74+
return
75+
}
76+
}
77+
// 如果都没有,清空
78+
lastToolResponseId.value = ''
79+
}
80+
5781
let loadingms: MessageReactive
5882
let allmsg: MessageReactive
5983
let prevScrollTop: number
@@ -842,6 +866,8 @@ async function loadMoreMessage(event: any) {
842866
const lastId = chatStore.chat[chatIndex].data[0].uuid
843867
await chatStore.syncChat({ roomId: currentChatRoom.value!.roomId } as Chat.ChatRoom, lastId, () => {
844868
loadingms && loadingms.destroy()
869+
// 加载更多消息后,重新初始化 lastToolResponseId
870+
initLastToolResponseId()
845871
nextTick(() => scrollTo(event.target.scrollHeight - scrollPosition))
846872
}, () => {
847873
loadingms = ms.loading(
@@ -864,6 +890,8 @@ const handleSyncChat
864890
// 直接刷 极小概率不请求
865891
chatStore.syncChat({ roomId: Number(uuid) } as Chat.ChatRoom, undefined, () => {
866892
firstLoading.value = false
893+
// 初始化 lastToolResponseId 从历史记录中
894+
initLastToolResponseId()
867895
const scrollRef = document.querySelector('#scrollRef')
868896
if (scrollRef)
869897
nextTick(() => scrollRef.scrollTop = scrollRef.scrollHeight)
@@ -1120,6 +1148,15 @@ watch(() => chatStore.active, () => {
11201148
handleSyncChat()
11211149
})
11221150
1151+
// 监听 dataSources 变化,自动更新 lastToolResponseId
1152+
// 使用 immediate: false 避免初始化时重复调用(handleSyncChat 回调中已调用)
1153+
watch(() => dataSources.value.length, () => {
1154+
// 只在数据源长度变化时更新(历史记录加载完成)
1155+
nextTick(() => {
1156+
initLastToolResponseId()
1157+
})
1158+
})
1159+
11231160
onUnmounted(() => {
11241161
if (loading.value)
11251162
controller.abort()

0 commit comments

Comments
 (0)