From 1c28e2edf5f04162b3290aba2a2717978728a904 Mon Sep 17 00:00:00 2001 From: xiaohai1314-sys <219774556+xiaohai1314-sys@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:47:26 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20keepalive=20beat=E3=80=81notifySched?= =?UTF-8?q?uling=E3=80=81minimizeMemory=20=E7=9A=84=20AfterFunc=20?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E5=8A=A0=20paniclog.Recover=EF=BC=9B?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E9=A1=B5=20markdown=20=E6=98=BE=E5=BC=8F=20h?= =?UTF-8?q?tml:=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/api/keepalive.go | 12 ++++++++++-- internal/app/worker_manager.go | 5 ++++- internal/camoufoxnative/worker.go | 10 ++++++++-- web/src/markdown.ts | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/internal/api/keepalive.go b/internal/api/keepalive.go index 0f1ae54a..da0ad523 100644 --- a/internal/api/keepalive.go +++ b/internal/api/keepalive.go @@ -6,6 +6,8 @@ import ( "net/http" "sync" "time" + + "github.com/Mag1cFall/AIStudio2API/internal/paniclog" ) // nonstreamKeepaliveInterval 是非流式保活第一次写出和后续间隔。测试可以改短,避免真实等待 10 秒。 @@ -42,7 +44,10 @@ func (s *server) nonstreamKeepalive(w http.ResponseWriter, r *http.Request) (htt interval = streamHeartbeatInterval } writer := &keepaliveWriter{ResponseWriter: w, ctx: r.Context(), interval: interval} - writer.timer = time.AfterFunc(interval, writer.beat) + writer.timer = time.AfterFunc(interval, func() { + defer paniclog.Recover("keepalive.go:nonstreamKeepalive.beat") + writer.beat() + }) return writer, writer.stop } @@ -126,7 +131,10 @@ type streamKeepalive struct { func keepStreamAlive(w http.ResponseWriter, frame string) *streamKeepalive { writer := &streamKeepalive{ResponseWriter: w, frame: func() string { return frame }} writer.mu.Lock() - writer.timer = time.AfterFunc(streamHeartbeatInterval, writer.beat) + writer.timer = time.AfterFunc(streamHeartbeatInterval, func() { + defer paniclog.Recover("keepalive.go:streamKeepalive.beat") + writer.beat() + }) writer.mu.Unlock() return writer } diff --git a/internal/app/worker_manager.go b/internal/app/worker_manager.go index 3bc507dc..45615139 100644 --- a/internal/app/worker_manager.go +++ b/internal/app/worker_manager.go @@ -280,7 +280,10 @@ func (manager *accountWorkerManager) markRuntimeBusy(account *accountWorker, cau "WAA Worker 账户由其他进程占用 | 暂停调度=%s | 错误=%s", delay, strings.TrimSpace(cause.Error()), )) } - time.AfterFunc(delay, manager.notifyScheduling) + time.AfterFunc(delay, func() { + defer paniclog.Recover("worker_manager.go:notifyScheduling") + manager.notifyScheduling() + }) return busyErr } diff --git a/internal/camoufoxnative/worker.go b/internal/camoufoxnative/worker.go index b0e63f49..6226b46c 100644 --- a/internal/camoufoxnative/worker.go +++ b/internal/camoufoxnative/worker.go @@ -133,8 +133,8 @@ func Start(ctx context.Context, options Options) (*Worker, error) { if _, err := worker.chromeEvaluate(ctx, idleRenderingExpression); err != nil { return nil, fmt.Errorf("停止 Camoufox 标签页绘制: %w", err) } - worker.idle = time.AfterFunc(idleMinimizeDelay, worker.minimizeMemory) - worker.settle = time.AfterFunc(readyMinimizeDelay, worker.minimizeMemory) + worker.idle = time.AfterFunc(idleMinimizeDelay, worker.minimizeMemoryRecovered) + worker.settle = time.AfterFunc(readyMinimizeDelay, worker.minimizeMemoryRecovered) } failed = false return worker, nil @@ -263,6 +263,12 @@ const idleMinimizeDelay = 5 * time.Minute // readyMinimizeDelay 为无头 Worker 就绪后发出一次 memory-pressure 的延迟,回收浏览器启动与首次载入留下的渲染目标与 GC 垃圾 const readyMinimizeDelay = 2 * time.Second +// minimizeMemoryRecovered 供 time.AfterFunc 调用,回调里的 panic 只记录不终止进程 +func (worker *Worker) minimizeMemoryRecovered() { + defer paniclog.Recover("worker.go:minimizeMemory") + worker.minimizeMemory() +} + // minimizeMemory 在没有进行中的界面操作时发出 memory-pressure heap-minimize,各进程回收 GC 堆与软件 WebRender 的渲染缓存 func (worker *Worker) minimizeMemory() { worker.frameMu.Lock() diff --git a/web/src/markdown.ts b/web/src/markdown.ts index 94b8aea0..287bf376 100644 --- a/web/src/markdown.ts +++ b/web/src/markdown.ts @@ -16,7 +16,7 @@ export function gfmLinkify(md: Markdown) { } // markdown 渲染 Markdown 正文,原始 HTML 按文本转义,链接在新标签页打开 -export const markdown = new MarkdownIt({ linkify: true, breaks: true }) +export const markdown = new MarkdownIt({ linkify: true, breaks: true, html: false }) gfmLinkify(markdown) markdown.renderer.rules.link_open = (tokens, index, options, _env, self) => { tokens[index]!.attrSet('target', '_blank') From 6265a8d069f502f4ac12a79cb74eb5e0ac20c338 Mon Sep 17 00:00:00 2001 From: xiaohai1314-sys <219774556+xiaohai1314-sys@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:51:31 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=85=B6=E4=BD=99=20time.AfterFunc?= =?UTF-8?q?=20=E5=9B=9E=E8=B0=83=EF=BC=88=E4=BB=A3=E7=90=86=E8=AF=84?= =?UTF-8?q?=E5=88=86=E4=BF=9D=E5=AD=98=E3=80=81HTTP/3=20=E8=A7=A3=E7=A0=81?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E3=80=81=E4=B8=8A=E4=BC=A0=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E6=B8=85=E7=90=86=E3=80=81=E8=BD=AE=E6=AC=A1=E6=88=AA=E6=AD=A2?= =?UTF-8?q?=E3=80=81WAA=20=E5=AE=9A=E6=97=B6=E5=99=A8=E4=B8=8E=E7=A9=BA?= =?UTF-8?q?=E9=97=B2=E6=94=B6=E7=BC=A9=EF=BC=89=E5=8A=A0=20paniclog.Recove?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/aistudio/proxy_score_store.go | 7 ++++++- internal/aistudio/transport_http3_conn.go | 1 + internal/api/gemini_files.go | 2 ++ internal/app/runtime.go | 5 ++++- internal/waa/loop.go | 1 + internal/waa/runtime.go | 6 +++++- 6 files changed, 19 insertions(+), 3 deletions(-) diff --git a/internal/aistudio/proxy_score_store.go b/internal/aistudio/proxy_score_store.go index 03541d48..8a517420 100644 --- a/internal/aistudio/proxy_score_store.go +++ b/internal/aistudio/proxy_score_store.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" "time" + + "github.com/Mag1cFall/AIStudio2API/internal/paniclog" ) // proxyScoreSaveDelay 是评分变化后延迟落盘的时间,避免每个请求都写文件 @@ -93,7 +95,10 @@ func (p *ProxyPool) scheduleSaveLocked() { if p.scorePath == "" || p.closed || p.saveTimer != nil { return } - p.saveTimer = time.AfterFunc(proxyScoreSaveDelay, p.flushScores) + p.saveTimer = time.AfterFunc(proxyScoreSaveDelay, func() { + defer paniclog.Recover("proxy_score_store.go:flushScores") + p.flushScores() + }) } // flushScores 执行一次延迟保存;保存失败时等下一次评分变化再重试 diff --git a/internal/aistudio/transport_http3_conn.go b/internal/aistudio/transport_http3_conn.go index a45d55ed..b3bfc43f 100644 --- a/internal/aistudio/transport_http3_conn.go +++ b/internal/aistudio/transport_http3_conn.go @@ -156,6 +156,7 @@ func (conn *http3Conn) flushDecoder() error { func (conn *http3Conn) scheduleDecoderFlush() { if conn.ackPending.CompareAndSwap(false, true) { time.AfterFunc(time.Millisecond, func() { + defer paniclog.Recover("transport_http3_conn.go:scheduleDecoderFlush") conn.ackPending.Store(false) _ = conn.flushDecoder() }) diff --git a/internal/api/gemini_files.go b/internal/api/gemini_files.go index b44b6d7f..42123fb5 100644 --- a/internal/api/gemini_files.go +++ b/internal/api/gemini_files.go @@ -22,6 +22,7 @@ import ( "time" "github.com/Mag1cFall/AIStudio2API/internal/aistudio" + "github.com/Mag1cFall/AIStudio2API/internal/paniclog" ) // geminiFilesURIPrefix 为 Files API 文件 URI 的前缀 @@ -151,6 +152,7 @@ func (uploads *geminiUploads) start(session *geminiUploadSession) (string, error } session.expires = time.Now().Add(geminiUploadTTL) session.timer = time.AfterFunc(geminiUploadTTL, func() { + defer paniclog.Recover("gemini_files.go:uploadTTL") session.mu.Lock() defer session.mu.Unlock() if !session.done && !time.Now().Before(session.expires) { diff --git a/internal/app/runtime.go b/internal/app/runtime.go index a6282253..b3025ee3 100644 --- a/internal/app/runtime.go +++ b/internal/app/runtime.go @@ -2638,7 +2638,10 @@ type roundDeadlineContext struct { func newRoundDeadlineContext(parent context.Context, timeout time.Duration) (*roundDeadlineContext, context.CancelFunc) { ctx := &roundDeadlineContext{parent: parent, timeout: timeout, done: make(chan struct{}), deadline: time.Now().Add(timeout)} ctx.mu.Lock() - ctx.timer = time.AfterFunc(timeout, func() { ctx.finish(context.DeadlineExceeded) }) + ctx.timer = time.AfterFunc(timeout, func() { + defer paniclog.Recover("runtime.go:roundDeadline") + ctx.finish(context.DeadlineExceeded) + }) ctx.stop = context.AfterFunc(parent, func() { ctx.finish(parent.Err()) }) ctx.mu.Unlock() return ctx, func() { ctx.finish(context.Canceled) } diff --git a/internal/waa/loop.go b/internal/waa/loop.go index 49500211..fb57aa1b 100644 --- a/internal/waa/loop.go +++ b/internal/waa/loop.go @@ -92,6 +92,7 @@ func (loop *eventLoop) schedule(delay time.Duration, job func()) { } var timer *time.Timer timer = time.AfterFunc(delay, func() { + defer paniclog.Recover("loop.go:setTimeout") loop.mu.Lock() delete(loop.timers, timer) loop.mu.Unlock() diff --git a/internal/waa/runtime.go b/internal/waa/runtime.go index 8023aeb4..1ec7ec51 100644 --- a/internal/waa/runtime.go +++ b/internal/waa/runtime.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/Mag1cFall/AIStudio2API/internal/paniclog" "github.com/Mag1cFall/AIStudio2API/internal/tzcache" "github.com/Mag1cFall/AIStudio2API/internal/waa/mozjs" ) @@ -147,7 +148,10 @@ func newRuntime(ctx context.Context, options Options, prelude string) (*Runtime, return nil, errors.New("WAA 事件循环已退出") } } - runtime.idle = time.AfterFunc(idleShrinkDelay, func() { loop.post(runtime.agent.cx.ShrinkGC) }) + runtime.idle = time.AfterFunc(idleShrinkDelay, func() { + defer paniclog.Recover("runtime.go:idleShrink") + loop.post(runtime.agent.cx.ShrinkGC) + }) return runtime, nil case <-loop.done: loop.close()