mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
fix(desktop): avoid false init failure after long background
This commit is contained in:
@@ -187,19 +187,17 @@ export class PythonBackend extends EventEmitter {
|
|||||||
|
|
||||||
private waitForReady(): Promise<void> {
|
private waitForReady(): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const maxAttempts = 120
|
// Wall-clock deadline rather than an attempt counter: if the machine
|
||||||
let attempts = 0
|
// sleeps/suspends, the 1s timers stretch out and a counter would give up
|
||||||
|
// far too early. Time-based bounding tracks real elapsed time instead.
|
||||||
|
const timeoutMs = 120_000
|
||||||
|
const startedAt = Date.now()
|
||||||
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
attempts++
|
|
||||||
if (attempts % 10 === 0) {
|
|
||||||
this.emit('log', `Waiting for backend... (${attempts}s)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = http.get(`http://127.0.0.1:${this.port}/config`, (res) => {
|
const req = http.get(`http://127.0.0.1:${this.port}/config`, (res) => {
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
this.status = 'ready'
|
this.status = 'ready'
|
||||||
this.emit('log', `Backend ready on port ${this.port} after ${attempts}s`)
|
this.emit('log', `Backend ready on port ${this.port}`)
|
||||||
this.emit('ready', this.port)
|
this.emit('ready', this.port)
|
||||||
resolve()
|
resolve()
|
||||||
} else {
|
} else {
|
||||||
@@ -215,13 +213,13 @@ export class PythonBackend extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
if (this.status === 'stopped') {
|
if (this.status === 'stopped' || this.status === 'ready') {
|
||||||
resolve()
|
resolve()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (attempts >= maxAttempts) {
|
if (Date.now() - startedAt >= timeoutMs) {
|
||||||
this.status = 'error'
|
this.status = 'error'
|
||||||
this.emit('error', `Backend failed to start within ${maxAttempts} seconds`)
|
this.emit('error', `Backend failed to start within ${Math.round(timeoutMs / 1000)} seconds`)
|
||||||
resolve()
|
resolve()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,29 +24,46 @@ export function useBackend() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// True once the backend has answered at least once. After this we never flip
|
||||||
|
// back to "error" from polling — a hidden/backgrounded window throttles JS
|
||||||
|
// timers, so attempt counters are unreliable and would otherwise produce a
|
||||||
|
// false "failed to start" even though the backend is alive.
|
||||||
|
const readyRef = useRef(false)
|
||||||
|
// Holds the latest resolved port so the visibility handler (registered once)
|
||||||
|
// always probes the correct port without re-running the effect.
|
||||||
|
const portRef = useRef(9899)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
let offStatus: (() => void) | undefined
|
let offStatus: (() => void) | undefined
|
||||||
const api = window.electronAPI
|
const api = window.electronAPI
|
||||||
|
|
||||||
|
// Use a wall-clock deadline instead of an attempt counter so timer
|
||||||
|
// throttling (when the window is in the background) can't fast-forward us
|
||||||
|
// into a false failure. Only give up if we genuinely can't reach the
|
||||||
|
// backend for this long.
|
||||||
const startPolling = async (port: number) => {
|
const startPolling = async (port: number) => {
|
||||||
let attempts = 0
|
portRef.current = port
|
||||||
const maxAttempts = 90
|
const deadline = Date.now() + 90_000
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
attempts++
|
|
||||||
|
|
||||||
const ready = await probeBackend(port)
|
const ready = await probeBackend(port)
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
|
||||||
if (ready) {
|
if (ready) {
|
||||||
|
readyRef.current = true
|
||||||
setState({ status: 'ready', port })
|
setState({ status: 'ready', port })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempts >= maxAttempts) {
|
// Backend already answered before but is briefly unreachable (e.g.
|
||||||
setState({ status: 'error', port, error: 'Backend failed to start. Check if Python and dependencies are installed.' })
|
// window was asleep): keep retrying, never surface an error.
|
||||||
|
if (!readyRef.current && Date.now() >= deadline) {
|
||||||
|
// Leave error undefined so StatusScreen shows the localized,
|
||||||
|
// user-friendly message instead of a raw technical string.
|
||||||
|
setState({ status: 'error', port })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,31 +76,51 @@ export function useBackend() {
|
|||||||
if (api) {
|
if (api) {
|
||||||
api.getBackendPort().then((port) => {
|
api.getBackendPort().then((port) => {
|
||||||
const p = port || 9899
|
const p = port || 9899
|
||||||
|
portRef.current = p
|
||||||
setState((prev) => ({ ...prev, port: p }))
|
setState((prev) => ({ ...prev, port: p }))
|
||||||
startPolling(p)
|
startPolling(p)
|
||||||
})
|
})
|
||||||
|
|
||||||
offStatus = api.onBackendStatus((data) => {
|
offStatus = api.onBackendStatus((data) => {
|
||||||
if (data.status === 'ready' && data.port) {
|
if (data.status === 'ready' && data.port) {
|
||||||
|
readyRef.current = true
|
||||||
|
portRef.current = data.port
|
||||||
setState({ status: 'ready', port: data.port })
|
setState({ status: 'ready', port: data.port })
|
||||||
if (pollingRef.current) {
|
if (pollingRef.current) {
|
||||||
clearTimeout(pollingRef.current)
|
clearTimeout(pollingRef.current)
|
||||||
pollingRef.current = null
|
pollingRef.current = null
|
||||||
}
|
}
|
||||||
} else if (data.status === 'error') {
|
} else if (data.status === 'error' && !readyRef.current) {
|
||||||
setState((prev) => ({ ...prev, status: 'error', error: data.error }))
|
// Ignore late "error" from the main process once we've been ready —
|
||||||
|
// it usually means the window was backgrounded, not a real failure.
|
||||||
|
// Drop the raw technical message; StatusScreen shows a localized one.
|
||||||
|
setState((prev) => ({ ...prev, status: 'error' }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
startPolling(9899)
|
startPolling(9899)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the window comes back to the foreground, re-probe immediately so a
|
||||||
|
// user returning after a while sees the real (ready) state right away
|
||||||
|
// instead of waiting for the throttled timer to catch up.
|
||||||
|
const onVisible = () => {
|
||||||
|
if (cancelled || document.visibilityState !== 'visible') return
|
||||||
|
probeBackend(portRef.current).then((ready) => {
|
||||||
|
if (cancelled || !ready) return
|
||||||
|
readyRef.current = true
|
||||||
|
setState((prev) => ({ ...prev, status: 'ready' }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', onVisible)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
if (pollingRef.current) {
|
if (pollingRef.current) {
|
||||||
clearTimeout(pollingRef.current)
|
clearTimeout(pollingRef.current)
|
||||||
}
|
}
|
||||||
offStatus?.()
|
offStatus?.()
|
||||||
|
document.removeEventListener('visibilitychange', onVisible)
|
||||||
}
|
}
|
||||||
}, [probeBackend])
|
}, [probeBackend])
|
||||||
|
|
||||||
|
|||||||
@@ -290,9 +290,9 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
logs_live: '实时',
|
logs_live: '实时',
|
||||||
logs_connecting: '日志流将在连接后显示...',
|
logs_connecting: '日志流将在连接后显示...',
|
||||||
status_starting: '正在启动 CowAgent...',
|
status_starting: '正在启动 CowAgent...',
|
||||||
status_starting_desc: '正在初始化后端服务,请稍候',
|
status_starting_desc: '正在初始化客户端,请稍候',
|
||||||
status_error: '启动失败',
|
status_error: '初始化失败',
|
||||||
status_error_desc: '无法连接到后端服务',
|
status_error_desc: '客户端初始化失败,请重试',
|
||||||
status_retry: '重试',
|
status_retry: '重试',
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
@@ -586,9 +586,9 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
logs_live: 'Live',
|
logs_live: 'Live',
|
||||||
logs_connecting: 'Log streaming will connect shortly...',
|
logs_connecting: 'Log streaming will connect shortly...',
|
||||||
status_starting: 'Starting CowAgent...',
|
status_starting: 'Starting CowAgent...',
|
||||||
status_starting_desc: 'Initializing the backend service, please wait',
|
status_starting_desc: 'Initializing the client, please wait',
|
||||||
status_error: 'Failed to Start',
|
status_error: 'Initialization Failed',
|
||||||
status_error_desc: 'Unable to connect to the backend service',
|
status_error_desc: 'Failed to initialize the client, please retry',
|
||||||
status_retry: 'Retry',
|
status_retry: 'Retry',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user