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/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/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/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/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() 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')