Skip to content

fix: upgrade golang.org/x/crypto to 0.52.0 (CVE-2026-39829) - #1

Open
anupamme wants to merge 83 commits into
Darkstarrd-dev:mainfrom
anupamme:fix-repo-tinyrouter-cve-2026-39829-golang.org-x-crypto
Open

fix: upgrade golang.org/x/crypto to 0.52.0 (CVE-2026-39829)#1
anupamme wants to merge 83 commits into
Darkstarrd-dev:mainfrom
anupamme:fix-repo-tinyrouter-cve-2026-39829-golang.org-x-crypto

Conversation

@anupamme

Copy link
Copy Markdown

Summary

Upgrade golang.org/x/crypto from v0.51.0 to 0.52.0 to fix CVE-2026-39829.

Vulnerability

Field Value
ID CVE-2026-39829
Severity HIGH
Scanner trivy
Rule CVE-2026-39829
File go.mod
Assessment Likely exploitable

Description: golang.org/x/crypto/ssh: golang.org/x/crypto/ssh: Denial of Service via crafted public key with excessive parameters

Evidence

Scanner confirmation: trivy rule CVE-2026-39829 flagged this pattern.

Production code: This file is in the production codebase, not test-only code.

Changes

  • go.mod
  • go.sum

Behavior Preservation

The change is scoped to 2 files on the vulnerable path, and the project builds successfully with this change applied.

Verification

  • Build passes
  • Scanner re-scan confirms fix
  • LLM code review passed

This change addresses a pattern flagged by static analysis. The code path handles user-influenced input and the fix reduces the attack surface against both manual and automated exploitation.


Automated security fix by OrbisAI Security

Added a community section to acknowledge support.
Added OpenCode example configuration for TinyRouter.
Added instructions for AI First configuration and OpenCode example.
Style Dimension (4 presets x 18 colors = 72 appearances):

- Add data-theme-style attribute (default/sharp/soft/compact)

- New tokens: --font-weight-*, --letter-spacing-heading, --card-padding, --btn-padding, --glass-blur, --glass-blur-sm, --glass-blur-overlay

- ThemeSystem JS: styleRegistry, getStyle/setStyle/applyStyle/renderStylePicker

- Backend: ThemeConfig.Style field + default

- UI: style picker in theme modal, i18n keys

- HTML: first-paint restore for data-theme-style

Design Review Enhancements:

- Tokenize ~40 hardcoded values (border-radius, font-weight, backdrop-filter, transition)

- Extend focus-visible to all interactive elements (a11y)

- Add @container query for card-actions responsiveness

- Add DESIGN.md design system contract (token taxonomy, Do/Don't rules)

- Establish 5 visual regression baselines (tmp/visual-baseline-*.png)
Ollama serves both a native /api/* API (non-OpenAI shape: NDJSON
streaming, message.content instead of choices[], object tool arguments)
and an OpenAI-compatible /v1/* API. The project does no format conversion
and passes SSE through verbatim, so requests must land on /v1/*.

Add isOllamaBaseURL (pure string match, no network probe) detecting
host ollama.com or localhost/127.0.0.1 on port 11434, and
normalizeOllamaBaseURL dropping the entire path so the host-root branch
injects /v1 uniformly. Whatever path the user entered (/api, /api/tags,
/api/chat, /v1, /v1/chat/completions) is reduced to /v1/*. Covers all 11
BuildUpstreamURL call sites (proxy/admin/probe/combo-speedtest) with a
single change.
- Editor page toggled behind the Gallery nav button/F6
  (Gallery -> Editor -> Gallery)
- Dual-pane editing with raw/parsed view, find/replace,
  goto-line, line numbers, diff before/after & left-vs-right
- Reuse playground vendored libs (marked, highlight.js, katex,
  DOMPurify) and vendored diff.min.js for line/char diffs
- Backend: /api/editor/open + /api/editor/save handlers
- Fix IFileOpenDialog::GetResult vtable index (26->20) in
  internal/fsutil/open_windows.go; this was causing the whole
  process to crash when the user confirmed a file/dir selection
- Fix editor.js edBuildFindBar NPE on detached findBar nodes
- Sync PROJECT_MAP.md and docs/playground-architecture.md
…split internal/api into 21 sub-packages, decompose download, break rotation→registry edge

Extract cross-cutting helpers from the proxy/download/registry packages into
reusable internal/sse (SSE framing), internal/urlutil (URL canonicalization),
internal/procutil (shared kill funcs for download+monitor), and internal/keystate
(KeyRuntimeState types pulled out of registry).

Split the monolithic internal/api package into 21 single-concern sub-packages
(apibase, auth, anysearch, combos, compress, console_logs, download, editor,
gallery, image, keys, models, monitor, probe, providers, quickslots,
review_presets, settings, sse, terminal, usage), each with its own Register
handler and a shared apibase.Deps seam. Consolidate internal/proxy/upstream.go
buildUpstreamRequest and internal/api/probe probe funcs. Route registrations for
probe/terminal/monitor/gallery are corrected as part of the split.

Decompose internal/download manager/executor/args into 12 single-concern files
(binary, events, formats, lifecycle, network, parse, playlist, progress, worker).

Break the rotation→registry import edge by introducing a KeyStateProvider
interface, so rotation no longer depends on the registry package directly.
… probe route test

Terminal: add a Windows registry PATH fallback (internal/terminal/path.go,
path_windows.go, path_other.go, path_test.go) consumed by buildShellEnv in
session.go, so spawned shells inherit a complete PATH even when the parent
process environment is incomplete.

Gallery bulk import: raise session capacity 32->128, add bounded-concurrency
rehydrate-on-404 in gallery-io.js, release the session on clear/remove in
gallery-tree.js, and guard escapeHtml in gallery-state.js.

Probe: correct the proto test path to /providers/{id}/models/test-proto and
wire it to the new probe.Handler via apibase.Deps (test was using the old
/api-prefixed route and the deleted Router method).
internal/combo/resolver.go: replace log.Printf with fmt.Fprintf(os.Stderr)
to satisfy the project rule that logging goes through internal/console, not
the log stdlib.

web/playground: drop the dead pgToggleSearchRaw from pg-render.js, and
consolidate the duplicated T() translation helper into pg-i18n.js, removing
the local copy from editor-state.js.
…pling

Refresh PROJECT_MAP.md sections 3-16 and the config-registry-state,
rotation, proxy, download, terminal-monitor, combo, and playground
architecture docs to reflect the extracted sse/urlutil/procutil/keystate
packages, the internal/api sub-package split, the download decomposition,
the rotation KeyStateProvider seam, and the terminal PATH / gallery bulk
import hardening.
P1-5 — file split (behavior unchanged, package proxy, no visibility changes):
- forward.go (591 LOC) -> forward.go (180, shared leaf utils) +
  forward_request.go (121, handleProxy) + forward_combo.go (45, handleCombo) +
  forward_retry.go (268, forwardWithRetry + keep-alive + broadcast*).
  H-8 keep-alive flush region moved verbatim; semantics untouched.
- stream.go (531 LOC) -> stream.go (312, streamResponse/passThroughResponse
  I/O loops) + stream_usage.go (103, OpenAI token parsing) +
  stream_anthropic.go (43, parseAnthropicSSEUsage) + stream_debug.go (34,
  parseAndBroadcastChunk). extractThoughtSignature migrated to
  signature_cache.go as companion to SignatureCacheProvider.

P1-6 — interface segregation (proxy.New signature unchanged, no test changes):
- KeyProvider (11 methods) -> KeySelector + NIMProvider + CooldownManager +
  QuotaLocker + RotationSettings, composed into KeyProvider.
- ModelResolver (9 methods) -> QuickSlotResolver + ProviderResolver +
  KeyStateAccessor + AliasResolver + ComboLister, composed into ModelResolver.
- Handler holds narrow fields; New assigns them from composite params via
  interface embedding. reg ModelResolver retained solely for existing test
  access to h.reg.GetKeyState (documented); non-test call sites route through
  narrow fields. 42 call-site rewrites across 7 non-test files.

Docs synced: PROJECT_MAP.md §7 (file table + interfaces.go row) and
docs/proxy-architecture.md (last-verified 2026-07-26, P1-5/P1-6 dated notes,
re-anchored 变更维护清单 source anchors).

Verified: go build/vet/test ./... + -tags playground green; gofmt clean.
…xhausted keys (H-8)

The non-streaming keep-alive goroutine in forwardWithRetry wrote a "\n" byte
+ Flush after a 20s grace period, which implicitly committed WriteHeader(200).
When the upstream then failed and all retry keys exhausted, the caller's
writeError(w, 502) was silently dropped (Go ignores WriteHeader after the
first write) — the client received HTTP 200 with the error JSON in the body
instead of 502.

Fix: remove the keep-alive byte-flush path entirely. No bytes are written to
the ResponseWriter until the final response/error, so writeError(502) on
all-keys-exhausted always takes effect.

Removed:
- forward_retry.go: keepAliveDelay/keepAliveInterval constants, keepAliveDone/
  keepAliveStopped channels, the `if !isStream { go func(){...}() }` goroutine,
  and the `close(keepAliveDone); <-keepAliveStopped` shutdown wait. Replaced
  with a NOTE comment documenting the removal.
- retry.go: retryState.headersFlushed field (no longer set/read).
- stream.go: passThroughResponse `headersFlushed bool` param + the
  `if !headersFlushed` conditional; now unconditionally sets headers +
  WriteHeader(resp.StatusCode).

Behavior change: the keep-alive only protected clients with short read
timeouts on long non-streaming requests (it did NOT protect against the
server WriteTimeout — Go sets that deadline once after reading headers and
writing bytes does not reset it). The 300s server WriteTimeout cap
(config.ServerConfig.WriteTimeoutSec) is unchanged and continues to bound
non-streaming responses. Clients with short read timeouts must set an
adequate timeout.

H-8's specific manifestation required a >20s upstream latency, impractical
to reproduce in CI; the fix is structural (the keep-alive path no longer
exists). The existing 502-contract tests (TestAfterMaxRetries_WithMock,
TestForwardWithRetry_NetworkError) guard the all-keys-exhausted status
contract and pass.

Docs synced: PROJECT_MAP.md §7 (forward_retry.go row) + §24 变更维护清单;
docs/proxy-architecture.md §8.7 (keep-alive section marked removed/fixed) +
dated note + §17/§18 anchors.

Verified: go build/vet/test ./... + -tags playground green; gofmt clean;
no leftover headersFlushed/keepAlive references.
- Add TooltipSystem module (app.js): delegated hover+focus listeners, single shared .tip DOM node, 600ms hover / 0ms focus delay, flip/clamp positioning, scroll tracking

- Add .tip CSS class consuming theme tokens for automatic variant/style sync

- Migrate ~100 native title= attributes to data-tooltip= across 17 files

- Migrate dynamic .title setters to setAttribute('data-tooltip', ...)

- Add aria-label to key icon-only buttons for screen reader support

- Update PROJECT_MAP.md with module documentation
…page load

The Recent Requests list pre-filled lastUsageEntries from localStorage on
every renderUsage (line 447) when the server ring was empty. Since the
server's usage ring is in-memory (clears on restart) but localStorage
persists across restarts, a fresh page load after restart showed stale
entries from the previous session until the first poll replaced them —
the "content disappears, then reappears as last-shutdown's data" symptom.

Removed: the USAGE_CACHE_KEY constant, saveUsageCache()/loadUsageCache()
functions, the bootstrap at renderUsage, and the saveUsageCache() call in
refreshQuotaData. On fresh load the list now shows the noUsage empty state
until the first server poll (sub-second) fills it. Other localStorage usage
(theme, gallery, auth) is untouched.

Verified: node --check passes; no remaining references to the removed
names; frontend-only (no Go changes).
… + console context

Root cause (user-reported, corrected from an earlier wrong "backoff" framing):
two different providers showed the IDENTICAL "400 → ~30s no-available-keys
burst → success" pattern. The common cause is the proxy's 30s transient
cooldown firing on a 400. A 400 is a CLIENT/request-shape error (the KEY is
healthy — Ollama succeeds with a different request), but ClassifyError(400)
fell through to ActionTransient → MarkRateLimited(30s) locked a healthy key,
bursting "no available keys" for all concurrent requests for 30s, then
"recovering" (the key was never broken). Retrying the same request on
another key 400s again anyway.

4xx pass-through (Part A):
- New ErrorAction `ActionPassThrough` in rotation/error_rules.go.
- 400/422 → ActionPassThrough by default (request-validation client errors;
  the key is fine). 401/402/403/404 stay ActionCooldown (key/auth), 429
  stays ActionBackoff.
- New text rule "upstream request failed" → ActionBackoff, placed BEFORE the
  400/422 status rules so an aggregator reporting its OWN upstream failed
  (transient at the aggregator) retries on multi-key, while genuine
  request-validation 400s pass through.
- handleUpstreamError now returns bool: true = pass-through response already
  written to the client (raw upstream body + status preserved, e.g. Ollama's
  exact "invalid reasoning value" message), retry loop stops and returns
  (true, reqID) so the caller does NOT writeError(502). No key lock, no
  exclude, no cooldown. The client gets the real 400 immediately.

Cooldown-wait (Part B): for LEGITIMATE cooldowns (429/5xx), the
"no available keys" path in forwardWithRetry now waits for the soonest
cooldown to expire (capped 30s, via new Selector.SonestCooldown) instead of
instant-502 — so concurrent requests during a real cooldown wait instead of
bursting. 502 still returns if all keys remain unavailable after the wait.

Console context (Part C): new requestCallerTag() helper (forward.go) adds a
log-safe caller identity (masked Authorization — first4…last4, never full
key; X-TinyRouter-Source; User-Agent; RemoteAddr), threaded as [reqID] +
callerTag into REQUEST/SEND/PROXY/no-available-keys lines. New plain-text
WARN lines state WHY and WHAT-NEXT in Chinese (pass-through reason,
cooldown-wait reason+expiry, exhaustion, balance-lock, backoff consequence,
network error) so a human reading the console doesn't guess.

Tests: TestPassThrough_400RequestShapeError_NotLocked (400 not 502, key not
locked, next request succeeds immediately, no retry), TestPassThrough_422,
TestPassThrough_AggregatorTransient400_Retries (text-rule override retries),
TestCooldownWait_WaitsThenSucceeds / _Still502, TestRequestCallerTag_*
(masking, empty fields, short key, bounded, nil). All pass.

Docs synced: PROJECT_MAP.md §5/§7/§24; docs/proxy-architecture.md;
docs/rotation-architecture.md.

Verified: go build/vet/test ./... + -tags playground green; gofmt clean.
The pass-through WARN line truncated the upstream error body to 120 chars,
cutting off the actual error message (e.g. Ollama's full "invalid reasoning
value: 'minimal' (must be \"high\", \"medium\", \"low\", \"max\", or \"none\")"
is ~150 chars and got cut at "..."). Match the existing upstream-error line's
500-char limit so the console shows the complete reason.
…cent Requests

Bug 1 — records not viewable across debug-mode toggles:
Root cause: payloads (ReqPayload/RespPayload/headers) were captured at record
time ONLY when debug was on (recorder.go captureDetails := h.debugMode(),
stream.go accumulation gates). Records made while debug=off got empty
payloads forever (no re-stamp possible), and usage.js:159 rendered the info
button clickable only when usageDebugMode && payload-present — so debug=off
made ALL records bare non-clickable dots ("not viewable"), and turning debug
on made only newly-recorded records clickable while old ones stayed bare
dots ("之前的记录全部消失").

Fix: decouple payload capture from debug mode — always capture (recorder.go:46,
stream.go:21 and :300 → captureDetails := true). The SSE debug-console
broadcast gates (stream.go:108/152/233, parseAndBroadcastChunk) are PRESERVED
(debug mode still controls live reasoning-panel streaming). Frontend
usage.js:159 drops the usageDebugMode requirement so the info button shows
whenever payload data exists (now always for new records), regardless of
debug state. usageDebugMode is retained for the processing-status streaming
panels. Memory cost bounded by ring size (config.UsageRingSize, default 500)
× per-entry body size (same as the existing debug=on behavior). Old records
recorded before this fix remain bare dots (bodies were never stored; no
retroactive recovery possible).

Bug 2 — pagination: Recent Requests was capped at slice(0, 50) so only the
first 50 of ~500 were visible. Added a page-size dropdown (20/30/40/50,
default 30) + prev/next buttons + page indicator (page / maxPage) in the card
header. Paging state (recentPageSize, recentPage) clamps on data refresh;
filtered.slice((page-1)*size, ...) selects the page. All ~500 records are
now reachable. Reuses existing CSS classes (.btn-sm, .btn-filter,
.console-controls); node --check passes.

Docs synced: PROJECT_MAP.md §7; docs/proxy-architecture.md (always-capture
note + last-verified).

Verified: go build/vet/test ./... + -tags playground green (20 pkgs);
node --check passes; gofmt clean; 3 always-capture sites confirmed;
SSE broadcast debug gates preserved.
Replace Chart.js stacked bar chart with hand-drawn SVG stacked bars.
Switching to the Usage page was slow and the trend region especially
slow: Chart.js added construct/destroy lifecycle, ResizeObserver
re-renders, full-canvas redraws, and DPR-scaled backing store on every
page switch and every SSE-triggered refresh. The prior SVG
implementation was stateless string concatenation + innerHTML.

- usage.js: new buildTrendChartSVG (stacked rects, stepped y-axis,
  bottom legend) + renderTrendChart (inline SVG) + initTrendChart
  (attach hover only) + updateTrendChart (cheap sig-skip rebuild) +
  attachTrendHover (index-mode tooltip: bucket range + per-group
  counts + total). Remove buildTrendChartConfig / Chart instance /
  resize listener / _lastTrendConfigSig; add _lastTrendSig +
  _trendHoverState.
- style.css: .trend-canvas-wrap/#trend-canvas -> .trend-chart-wrap/
  .trend-chart/.trend-legend*/.trend-tooltip*.
- index.html: drop <script src="/chart.umd.js">.
- chart.umd.js: deleted (200KB dead weight, no longer referenced).
- app.js/theme.js: toggleFontSize/notifyModeChange chart-destroy
  blocks -> initTrendChart guard on #trend-chart-card presence.
- PROJECT_MAP.md: drop "三方 JS chart.umd.js" row; usage.js entry now
  "趋势图 SVG 直绘 + initTrendChart 挂载 hover".
- docs/playground-architecture.md: drop Chart.js mention (Lite entry
  drift note); "重绘 Chart.js" -> "重绘趋势图(SVG 堆叠柱)".

Kept unchanged (reused by quota bars / providers.js): getModelColor,
TREND_PALETTE, modelColorMap, buildTrendData, CHART_JS_COLORS,
desaturateRgb, readCssVar, cssVarToInt, getProviderPrefix.

Visual + interaction parity with the Chart.js version: 16 buckets,
stacked per provider/model group, CHART_JS_COLORS palette with
desaturated fill + base stroke, stepped y-axis, bottom legend with
alias-aware labels, index-mode hover tooltip, theme-color aware,
no animation, empty-state safe.
Port novelhelper m1-import flow to TinyRouter Playground as a backend-orchestrated, SSE-streamed text cleanup feature.

Backend:
- internal/textreview: in-process session engine. Scheduler dispatches chapters across a node pool (node = one provider model + user-set concurrency); streams per-chapter SSE via a streaming ResponseWriter through the shared proxy. 502 "all keys exhausted" ramps the node's concurrency down (permanent config.yaml write) and disables at 0; 4xx pass-through and mid-stream failures do not ramp. Pause/resume/stop + single-chapter reprocess.
- internal/api/textreview: /api/text-review/* (review-nodes / split-patterns / prompt-default CRUD + sessions + sessions/{id}/events SSE + pause/resume/stop + chapters/{idx}/reprocess), 32 MiB body, auth-gated.
- internal/config + internal/registry: TextReviewConfig/TextReviewNode/SplitPattern types, builtin split-pattern injection (ported from split.ts), thread-safe CRUD.

Frontend (Playground, -tags playground):
- tr-split.js / tr-diff.js: chapter-split + line-decision algorithms ported from novelhelper (window.TR.*).
- text-review.js + step1..4 + tr-state.js: 4-step wizard (导入/切分/AI 清理/审校) with 3-way nav (Gallery→Editor→TextReview). Step3 subscribes SSE with snapshot-first reconnect (切页不丢: leaving closes EventSource only, backend task continues). Step4 renders per-chapter diff (editorAlignedDiff) with per-row accept/reject/edit, batch ops, TR.applyLineDecisions final text, Blob export.
- app.js / index.html / i18n / pg-i18n / playground.css: nav wiring, script tags, keys, styles (design tokens only).

Docs: PROJECT_MAP.md + docs/playground-architecture.md synced per AGENTS.md doc-sync rule; docs/research/text-review-port-plan.md planning baseline.

Tests: internal/textreview (9) + internal/api/textreview (8) pass; go build/vet/test -tags playground ./... green; 11 new JS files node --check clean.
… by session

OpenAI's protocol defines no session ID and the proxy is a transparent
passthrough, so session continuity is INFERRED from the conversation
history: LLM IDE clients resend the full messages array each turn, so the
"conversation root" (system prompt + first user message) is stable across
a session while msgCount grows monotonically.

Backend:
- sessionKeyFromMessages (internal/proxy/forward.go): hashes the first
  system message + first user message content (content handles string OR
  array-of-parts; truncated to 4096 runes; FNV-1a 64-bit → 8 hex chars).
  Returns "" for single-shot/non-chat requests (treated as ungrouped).
  Inference, not ground truth — edge cases (history-truncating clients →
  root drift; concurrent same-root sessions → merge) documented; v1 uses
  simple root hash, no sliding window.
- Entry.SessionKey field (internal/usage/ring.go) exposed in /usage JSON
  (endpoint encodes entries directly, no stripping).
- sessionKey computed in handleProxy (parsed messages in scope), threaded
  through forwardWithRetry/handleCombo → retry handlers (handle429/
  handleUpstreamError/handleNetworkError) + stream.go → recordUsage →
  Entry. 18 production recordUsage call sites + test call sites updated.
- Console log lines tagged [reqID|sess:xxxxxxxx] via reqLogTag (bare reqID
  when sessionKey empty) so concurrent sessions are distinguishable.

Frontend (web/static/usage.js):
- "按会话分组" toggle in the Recent Requests header. When on, rows on the
  current page cluster under collapsible session headers (label · N
  requests · first→last time · provider/model); empty-sessionKey entries
  collect into an "ungrouped" pseudo-group shown first. Paging operates on
  the flat list; grouping is a display transform within the page (v1).
  Clicking a header collapses/expands its rows via DOM toggle.

Tests: TestSessionKeyFromMessages (root stability across msgCount, content-
as-array, no-messages→"", truncation determinism), TestSessionKey_EndToEnd_
FlowedToEntryAndConsoleLog (sessionKey flows body→Entry→/usage JSON→log tag).
Both pass.

Docs synced: PROJECT_MAP.md §7/§8/§24; docs/proxy-architecture.md (dated
note + 最后核对).

Verified: go build/vet/test ./... + -tags playground green (20 pkgs);
node --check passes; gofmt clean.
…rsistence overhaul

Fold the AI Text Review wizard (was a separate top-level page) into the Editor as a third toolbar mode (Edit/Diff/Clean), and rework navigation, state persistence, and the Step1/2/3 layouts per the 8-point remediation plan.

Nav + persistence (points 1-3):
- Top-level nav simplified to 2-way Gallery<->Editor (gotoGalleryToggle; sessionStorage.trGalView persists the toggle across page nav).
- Editor gains a third toolbar mode 'Clean' hosting the 4-step wizard via renderTextReview/cleanupTextReview; leaving Clean mode closes the SSE + saves state.
- editorState (mode + panes) persisted to sessionStorage (edSaveState/edLoadState in editor-state.js); returning to Editor restores the last mode (incl. Clean). trState (wizard) already used localStorage.

Clean mode layout (point 4):
- Wizard occupies the LEFT pane only; right pane empty (D1=a). New .ed-review-wrap flex container holds .ed-review-area (max-width:50%, right border) + .ed-review-spacer.

Step1 performance + layout (points 5-6):
- Large-text import no longer freezes: rawText stays full in memory; preview renders only the first chunk (2000 lines / 64KB) with a 'Load more' button appending subsequent chunks. Paste is intercepted (preventDefault) so a 10MB paste never fills a live textarea.
- Layout reworked: centered title row with Next (and Abandon after import) on the right; openfile+desc hidden after import; file info moved under the title; text preview pinned to the bottom filling remaining window height. Abandon resets to the initial state.

Step2 layout + auto-detect (point 7):
- Header row (Back | centered Split | Next); controls1 single row (Pattern + select + Title Template + input + KeepPrologue checkbox); controls2 four evenly-distributed buttons (Edit/auto/re-split/ai-split); preview flex-fills remaining height (was too tall / truncated).
- On fresh entry from Step1 (no existing chapters), auto-runs trStep2AutoDetect once; returns to Step2 from Step3 keep existing chapters.

Step3 Settings modal + prompt collapse (point 8):
- Node Pool container gains a Settings button (top-right) opening a pg-modal for node CRUD: add-node form (provider/model dropdowns from /api/models + concurrency + enabled) and per-node delete; re-fetches inline table on close. Resolves the fresh-install block (no nodes -> could not start).
- System Prompt section is now default-collapsed (clickable header + chevron); trState.promptCollapsed persisted.

Docs: PROJECT_MAP.md §24 + docs/playground-architecture.md synced (nav 2-way, Editor 3 modes, Step1-3 layout, Settings modal, prompt collapse).

Verified: go build/vet/test -tags playground ./... green (22 packages); node --check clean on all 8 changed JS; 3-way wiring (router.go pgJSFiles <-> index.html) consistent. No Go changes in this round.
…n tab switch

Bug 1 — grouping showed expanded sessions, not one-row-per-session:
The "按会话分组" toggle rendered a header + ALL of a session's request rows
inline, so a session with N requests showed N+1 rows instead of 1. Fix:
session groups now start COLLAPSED — renderSessionGroupHeader emits
class="session-group-header collapsed" with arrow ▸, and the session's rows
(renderUsageRow with hidden=true) start with display:none. Clicking the
header expands (arrow ▾, rows visible); clicking again collapses. So 5
sessions = 5 visible rows until expanded. The ungrouped pseudo-group (empty
sessionKey, single-shot requests) renders a static non-clickable label with
its rows inline (catch-all, no collapse value). Paging still operates on the
flat list; grouping is a display transform within the page.

Bug 2 — entries disappeared on tab switch / page return:
renderUsage reset lastUsageEntries = [] before repopulating from the API
fetch, so returning to the usage page flashed empty until the fetch
resolved (and inflight entries landed at the tail unsorted). Fix: mirror the
refreshQuotaData merge pattern — map API entries (preserving __streaming*
fields from existing matching entries), append inflight entries (prune
stale >10min), sortEntriesByTimeDesc, then assign atomically. No [] reset;
the sync render at the top of renderUsage shows the previous entries
immediately and updates them in place when the fetch resolves. The
currentPage !== 'usage' abort stays (don't render if navigated away
mid-fetch).

Verified: node --check passes; go build/test ./... green (22 packages);
reset removed; sort now in renderUsage; collapsed class + display:none
on grouped rows confirmed.
…ruption)

Regression from the collapsed-grouping fix: renderRecentRows' flat path
called `rows.map(renderUsageRow).join('')`, but Array.prototype.map passes
(element, index, array) to the callback. renderUsageRow's signature is
(e, sessionKey, hidden), so the numeric index became sessionKey (not
undefined → inSession=true) and the array object became hidden (truthy →
style="display:none" applied). Every flat-mode row was hidden — empty
Recent Requests, no error.

Fix: pass explicit args `renderUsageRow(e, undefined, false)` so
inSession=false and no display:none. Grouped path (which passed explicit
args already) is unchanged.

Verified: node --check passes; go build ./... green.
…t Requests

Root cause: the /usage endpoint (register.go:105-106) returns ring
(completed) entries FOLLOWED BY inflight (processing) entries in one
time-sorted list. The frontend merge in renderUsage and refreshQuotaData
did NOT dedup by ID, so when a request completed, BOTH entries landed in
lastUsageEntries — the completed ring entry (status=success, real
latencyMs/outputTokens/respPayload/respHeaders/sessionKey) AND the stale
inflight entry (status=processing, latencyMs=0, outputTokens=0, no
payloads, no sessionKey). The processing entry's status kept the live
latency counter growing, showed out=0, had empty resp headers/body in the
info modal, and lacked sessionKey so grouping found nothing to bucket.

Fix: add status-aware dedup by ID in BOTH merge paths (renderUsage:544-580
and refreshQuotaData:864-899). Since the /usage response puts ring
(completed) entries first (register.go:105) and the frontend offset is
always 0, the first occurrence of an ID is the completed ring entry; the
seenIds map skips subsequent 'processing' duplicates. Inflight entries
from the frontend inflightEntries map are only merged when their ID is
absent from the API response (genuinely still in-flight). Stale
inflightEntries map entries are cleaned up when a terminal-status entry
now exists. All four symptoms share this single root cause: latency
counter stops (ring entry's fixed latencyMs used), output tokens show real
values, info modal renders resp headers/body (ring entry carries them),
and session grouping buckets by the ring entry's sessionKey.

Verified: node --check passes; go build/vet/test ./... + -tags playground
green (22 pkgs); dedup confirmed at usage.js ~547 and ~867; /usage order
confirmed ring-first at register.go:105-106.
Darkstarrd-dev and others added 29 commits July 28, 2026 00:26
writeRequestLog guarded only on requestLogDir=="", so trace files kept being written even after the user toggled tracing off. TraceMgmtCall already guarded with `|| !h.logRequests()`; writeRequestLog was missed. Add the same check — h.logRequests() is wired to the live Trace.Enabled flag (api router logRequests atomic, initialized from cfg.Trace.Enabled, toggled via settings). Also correct the stale TraceConfig doc comment which claimed Enabled defaults to true; the code default (defaults.go + finalizeConfig) is false.
…odies

The detail pane showed all dashes and an empty request section because getReq returns {reqID, lines} but logsRenderDetail read flattened data.session/reqHeaders/reqBody (all undefined). Header summary fields (session/status/latency/attempts) live in the index line, now looked up from the loaded index rows; reqHeaders/reqBody live in the type:"request" line inside data.lines, now read from there. Multiple attempt cards were clipped because the log reader had no CSS: add a layout grid with a scrollable detail pane. SSE response bodies collapsed to one line because JSON.stringify on a raw SSE string escapes newlines; add logsFormatBody which splits data: lines and pretty-prints each JSON payload, plus white-space:pre-wrap/word-break:break-all on .code so long lines wrap.
The architecture note claimed cfg.Trace.Enabled defaults to true; the code default (defaults.go + finalizeConfig) is false. writeRequestLog now respects the h.logRequests() gate (matching TraceMgmtCall), so the doc's claim that tracing is controlled by the live toggle is now accurate.
captureDetails was hardcoded true, so request/response payloads and headers were always stored in the usage ring even with debug mode off and tracing off — nothing consumed them, wasting memory. Restore the intended gating: captureDetails := isPlayground || h.debugMode(). Playground always captures (it owns its own pgUsage ring, decoupled from Recent Requests); non-playground requests only capture payload/headers when debug mode is on. When debug is off, Recent Requests is a lightweight table (time/provider/model/key/latency/tokens) with no stored bodies. SSE still streams to the client unchanged; only the accumulation-for-storage is skipped. This reverts the non-playground part of the 2026-07-26 "payload capture decoupled from debugMode" change; the tradeoff (records made while debug is off have no detail later) is intentional. Docs synced.
When tracing is on, every request's full body is already written to on-disk JSONL (viewable via the Log Reader), so the in-memory usage ring should not also store payload — that was duplicate memory. Ring storage gate becomes isPlayground || (h.debugMode() && !h.logRequests()): non-playground stores payload only when debug is on AND tracing is off; when tracing is on the ring is a lightweight table; playground always captures (its pgUsage ring is decoupled). Response-body accumulation gate becomes h.logRequests() || isPlayground || h.debugMode() so the trace captures full response bodies independently of debug mode (previously, with debug off + tracing on, the trace's respBody was empty because accumulation was debug-gated). The Recent Requests detail modal gracefully degrades to a basic-info summary when the ring has no payload; full bodies are viewed via the Log Reader. Docs synced.
When tracing is on, the usage ring stores no payload (recent backend change: captureDetails = isPlayground || (debugMode && !logRequests)), so Recent Requests entries had no request/response bodies. The Recent Requests detail modal now fetches the full detail from the on-disk trace files via GET /api/traces/req/{reqID} when tracing is enabled and the ring entry has no payload (new loadTraceDetails in usage.js) — keeping the click-to-view-detail UX but switching the data source from the in-memory ring to the trace files. The basic-info summary still renders synchronously from the ring entry; payload sections load asynchronously from the trace file (with a '(trace not available)' fallback for in-flight/expired entries). The info button is shown when tracing is on even without ring payload. The redundant /usage ring fallback is skipped when tracing is on (the ring has no payload then). This also gives non-playground builds a way to view trace detail, since the Log Reader UI is playground-build-only while the trace API is available in all builds. Docs synced.
…step3, diff refresh in editor

step3.js:
- trS3OnStatus: clear cleaned+error on chapter reset to pending (mirror backend ReprocessChapter), fixing duplicated text after reprocess
- trS3OpenEventSource: 3s delayed reconnect->snapshot on SSE error (replaces dead trS3NeedsReconcile flag); recover lost events via the forward-only SSE's snapshot fallback
- trS3OnChunk: only accept chunks while chapter is pending/processing, preventing terminal-status overwrite by residual/out-of-order chunks
- trS3MaybeSessionDone: don't synthesize 'completed' over a backend running/paused state
- trS3OnSessionGone: reset stale chapter statuses to pending on session 404
- trCleanupStep3: clear reconnect timer on page leave

editor.js:
- edSetMode: call edRenderDiff() when entering diff mode so edited text produces a fresh diff instead of a stale one
…ests modal

loadTraceDetails called apiGet('/api/traces/req/...') but apiGet already prepends /api, so the fetch went to /api/api/traces/req/... → 404 → every completed request showed "(trace not available)". Pass the bare path '/traces/req/...'. Also fix the renderInfoSection contract: formatBody returns a string, but renderInfoSection expects an object (for...in over a string iterates char indices, breaking rendering). Pass objects directly; wrap pre-formatted strings as { Body: formatBody(...) } so renderInfoSection renders one field while preserving SSE pretty-printing. Headers were already objects and stay correct.
…timeout

getUsage calls SweepStale(10min) on every GET /api/usage, removing in-flight entries older than 10 minutes and writing a timeout-error record to the ring. For a streaming request lasting >10 min, this swept the processing entry and wrote an error record before the stream completed; when recordUsage later wrote the success record (same ID), the ring (which does not dedup) ended up with both, and the frontend (which keeps the first occurrence) showed the successful request as a timeout. Add EntryTracker.Refresh(id) which bumps the entry's Timestamp to now, and call it at most once per second in the streamResponse loop so active streams reset the stale window and are not swept. The 10-min window and SweepStale logic are unchanged.
…ssion

Three fixes to the Recent Requests modal: (P2) handleRequestDone only called updateStreamingModalResponse when completeEntry.respPayload was present; when tracing is on the ring stores no payload, so the modal stayed stuck in the streaming Thinking state and never showed the final response. Now, when respPayload is empty but tracing is on, remove the streaming sections and call loadTraceDetails to fetch the final response from the trace file. (P1) showUsageEntryInfoById's fallback referenced an undefined local usage (a var in renderUsage) so the ReferenceError was silently caught and the ring-entry recovery fetch never ran; replace with a real await apiGet('/usage?limit=500'). (P5) handleRequestDone replaced the processing entry (which carries reqPayload/reqHeaders/upstreamUrl) with the completed entry (which lacks them when tracing on), so a user who saw the request payload during processing lost it on completion; merge those fields from the inflight entry when the completed entry lacks them.
Quota Monitor card now renders as a usage-table (same style as Recent Requests): provider/model/quota/input/output/latency/avg-speed columns, active rows sorted to top, multi-key rows expand to per-key sub-rows (availability/name/quota/latency/speed/status). Top-level latency/speed backfilled from active key's model-keys metrics via keyDetailCache. Progress bars removed. Frontend-only; no backend changes.
Quota Monitor card now renders as a usage-table (same style as Recent Requests): provider/model/quota/input/output/latency/avg-speed columns, active rows sorted to top, multi-key rows expand to per-key sub-rows (availability/name/quota/latency/speed/status). Top-level latency/speed backfilled from active key's model-keys metrics via keyDetailCache. Progress bars removed. Frontend-only; no backend changes.
…ty, trim constraints

Convert-all / output naming (rounds #8-#9):
- _getSiblingImages: match siblings by item kind (backend→rootDirPath,
  fs→rootDirHandle, zip→zipAbsPath/sessionId) instead of only 3 fields
  that FSAA/drag-drop items lack; count was always 0, batch never started
- _resolveBatchInput: per-item temp disk path via /edit/extract-zip-entry
  or /edit/upload-temp for items without absPath
- StartRequest.OutputName: optional stem, server appends buildArgs ext;
  avoids leaking temp input name (gallery-edit-XXXX) into saved outputs
- galleryEditZipOutputs: optional zipName (filepath.Base + .zip forced);
  client derives <original-folder/archive>_converted.zip
- i18n: fix geBatchProgress/geBatchDone placeholders %s→{0}/{1}

Batch UX + replace-original (round #9):
- rename toggle + custom name input (compress mode → zip name)
- sequential rename toggle + prefix + digit count; _padNum auto-widens
- replace-original guard: reject overwrite for fs/plain/FSAA-dropped-zip
  (no writable original → would silently write temp file)
- zip in-place writeback: ReplaceZipEntries (zip_replace.go) preserves
  untouched entries byte-for-byte, replaces matched entries; new endpoint
  POST /edit/zip-writeback with atomic writeback + temp cleanup
- POST /api/gallery/open-folder via fsutil.OpenInFileManager (replaces
  meaningless download button with open-folder)
- show-in-gallery fix: path gets dir prefix → enters own bucket;
  updateCurrentFolderItems + renderTreePanel re-render thumbnails

Video parity (round #10):
- _startJob carries outputName for single-file video (was temp name)
- video scale: number input → range slider + live WxH dims preview

UX v2 (round #11):
- Replace Original File → Same Path label (en/zh)
- Same Path + Convert all can now enable sequential rename
  (manager.go: OutputName honored in same-path non-OutputDir branch)
- video rename parity: shared ge-dest-rename input in dest block
- trim segment drag: cross-segment clamp (start ≥ prevEnd, end ≤ nextStart)
- remove Show in Gallery button from all completion result areas

Docs: playground-architecture.md supplements #8-#11, §4.2 + §16;
      PROJECT_MAP.md §10.9/§10.22/§24 synced
…tle truncation, and merge video sub-tabs into a unified panel
…rite

f6997d6 dropped the ge-dest radio read in _getDestination, making
overwrite always false — "Replace Original File" silently became
save-to-download-dir ("saved a new one next to it"). Single-file
and batch zip-writeback branches were entirely unreachable.

Server (internal/mediaedit/manager.go):
- Start() overwrite + cross-format → outputPath = <dir>/<stem><newExt>
  (ffmpeg picks encoder by output ext; writing webp into .png path
  silently kept PNG). runJob gains removeOnSuccess string; on success
  the original file is deleted → true in-place replace. Same-format
  keeps original path temp+rename (unchanged).
- Drop dead OutputName != "" && !Overwrite branch (Same Path = overwrite,
  unreachable).
- TestManager_TranscodeImage_Overwrite updated: png→webp now expects
  source.webp + source.png removed.

Client (gallery-edit.js):
- _getDestination restores radio read; samePath → overwrite:true,
  outputDir:null.
- _startBatch gains _startJob-style canReplace guard (reject fs/plain/
  FSAA-drop-zip overwrite).
- _refreshBatchUXVisibility renorm row re-gated by !samePath (Same Path
  and sequential rename are mutually exclusive); dest radio onchange
  now calls _refreshBatchUXVisibility.
- Batch non-compress Open Folder dead button: _batchJobs=[] ran before
  click → capture outputPaths[0] in closure instead.
- _getSiblingImages kind:'plain' returns [] (was undefined → TypeError
  on .length in batch checkbox / Start).
- _onCompleted: remove dead logHtml declaration in zip-writeback branch.

i18n (pg-i18n.js):
- geReplaceOriginal en: "Same Path" → "Replace Original File"
  (zh "原地替换原文件" unchanged).

Docs: PROJECT_MAP.md §10.9a + §24; playground-architecture.md
增补#12 + §16 constraints + 变更维护清单.
…Name model for image & video

Image dialog:
- Remove Replace Original File radio; overwrite now always false
- Toggle-driven layout: Set Path / Set Name / Uniform / Compress to Zip +
  Format / Quality+Scale / Strip Metadata all always visible, toggles only
  gate input enabled state
- Single & batch unified via _startBatch (single = batch-of-1), so Set Name/
  Uniform/Compress work on single images too
- Two-row source info: row1 = container path (archive/folder) or drag-no-path
  hint for FSAA items; row2 = name + resolution + size + format
- Archive toggle (image|folder icon) switches single ↔ convert-all-in-folder
- Uniform restricted to archive mode only; Set Name always available
- Browse button guards against concurrent native folder dialogs

Video dialog:
- Same model: Remove Replace Original, add Set Path + Set Name
- Header: settings gear + centered 'Video Convert' title
- Two-row source info: _updateVideoSourceInfo + _editVideoPath
- _getDestination replaced by _getDestFromSetPath (overwrite always false)
- _startJob simplified: reads Set Name toggle, no overwrite logic

Dead code removed:
- _zipReplacePending declaration + _onCompleted zip-writeback branch
- ge-dest radio bindings in _bindModalEvents

Server:
- manager.go Start(): honour OutputName when !Overwrite && OutputDir==""
  (Set Name in same-directory mode)

Download:
- download.js playVideo: add absPath to video items so edit/delete work

Documentation: playground-architecture.md supplements #13-#16 + maintenance
checklist rows updated.
依据 .qoder/specs/Gallery_Module_Refactor_task-d33.md 执行 Gallery 模块
重构,覆盖后端包拆分、状态注入、代理接口解耦、前端拆分、编辑器修复与
可追溯性增强。

Fix 1 后端拆分:internal/api/gallery/register.go(原 ~1800 行)拆为
7 文件——register.go(Handler/NewHandler/Register + proxyCaller 接口 +
image decoder blank imports)、session_store.go(zip 会话 LRU)、
fs_handlers.go(文件系统 handlers)、zip_handlers.go(zip 会话
handlers)、review_engine.go(AI 审核引擎核心)、review_handlers.go
(AI 审核 HTTP handlers)、edit_handlers.go(ffmpeg 媒体编辑 handlers)。

Fix 2 CleanZipPath 导出:internal/gallery/zip.go 的 cleanZipPath 重命名
为导出 CleanZipPath(带 doc comment),所有调用方(zip.go/zip_delete.go/
zip_replace.go 及测试)更新;edit_handlers.go 中重复的
cleanZipPathNormalize 移除,改调 gallerylib.CleanZipPath。

Fix 3 状态注入:Handler 新增 sessions/reviews/media/proxy 字段,移除三个
包全局变量(gallerySessions/reviewTasks/mediaJobs),全部调用方迁移到
h.sessions/h.reviews/h.media。media 字段为 *mediaedit.Manager(非 sync.Map)。

Fix 4 proxyCaller 接口:定义 proxyCaller 接口(ChatCompletions),
Handler.proxy 默认 d.ProxyHandler;sendVisionRequest/galleryGeneratePrompt
改用 http.NewRequestWithContext(prompt-gen 路径补 45s 超时)+
h.proxy.ChatCompletions,移除生产代码的 httptest.NewRequest。

Fix 5 前端拆分:gallery-edit.js(2108 行)拆为 gallery-edit.js(shell
1362 行)+ gallery-edit-operations.js(ops 249 行)+
gallery-edit-batch.js(batch 502 行),共享全局脚本作用域,按加载顺序
注册到 router.go pgJSFiles 与 index.html。

Fix 6 编辑器修复:edScrollIntoView 改用镜像 div 测量选区像素偏移滚动到
视口上 1/3(spec 假设的 edLineHeight() 不存在);editor.js t()→T();
editor-state.js 补 _findMatches/_findIdx 默认值。

Fix 7 可追溯性:7 后端 + 5 前端文件加前后端对应头部注释;pg-i18n.js
注释扩展;gallery-review.js 硬编码字符串迁移到 i18n(新增 galleryReview*
键到 i18n.js + pg-i18n.js 英/中);_geT 统一移除——_geT('x')→T('x')、
_geT('x',args)→pgT('x',args)(保留 {0} 插值,spec 的全量替换会破坏参数化
调用)。

文档同步:PROJECT_MAP.md §10.9/§10.22/§18.2/§24 与
docs/playground-architecture.md 新增 2026-07-29 增补#19 条目。

验证:go build ./... / go build -tags playground ./... / go test
(gallery + gallery lib + 全 api/proxy/config)/ node --check(全部相关
JS)均通过。
阶段一 P1:
- i18n 新增 loadFailed(修复 t('loadFailed') 返回 key 原文)
- fetchModelKeyDetail 错误行改 t('failed',[msg]) 一致形式(原 escapeHtml 已转义,无 XSS)

阶段二 性能:
- 抽取 mergeUsageEntries(O(n) Map 索引替代 .find()),renderUsage + refreshQuotaData 共用
- updateQuotaTable 停止每周期 sub-row 重建,改条件性 DocumentFragment 整组重排(稳态 0 次 DOM 移动,sub-row 不孤立)
- refreshAllKeyDetails 跳过折叠行 + cache 新鲜行;展开仍由 toggleQuotaRowExpand 直接 fetch

阶段三 可维护性:
- 删除死代码 lastUsageSig/lastQuotaSig + 4 个废弃 modal 函数 + showUsageEntryInfo(ts)
- Info Modal 硬编码英文走 t()(i18n 新增 17 个 info* 键 + infoTraceNA)

阶段四 结构:
- usage.js (1634 行) 拆为 6 文件(usage_state/io/quota/recent/modal + entry),101 项声明(32 var+69 func)零丢失/零重复,全部全局(onclick 不受影响)
- index.html/index-nopg.html 加载顺序 state→io→quota→recent→modal→entry
- §4.2 IIFE 封装暂缓(会把 onclick 调用的函数移出全局而破坏 UI)

文档同步:PROJECT_MAP §18.2/§24、proxy-architecture 2026-07-29 更新条目

验证:node --check 全过 + go build + 浏览器冒烟(零 JS 错误、配额表渲染、CN i18n 键解析正确、10 个关键函数含 4 个 onclick 项全局可见)
root cause: 子行  循环内的  赋值错误。
 将该节点从  移入 DOM 后,
 已指向下一个待处理节点,而非刚移走的节点。
 被赋值为仍留在  中的节点,导致下一次  变成
 内的空操作 —— 节点永远无法离开 , 永不
为 null,形成同步死循环。

仅当展开的 provider 有 ≥2 个 Key 时触发(2 个以上子行),单 Key 正常。
旧版进度条布局使用  一次性渲染,无此问题。
表格化重构(2e21abd)引入逐行  时写错了  赋值。

fix: 在移动节点前先捕获  到局部变量 ,
然后 (刚移走的节点),而非 。

两处修复:
- toggleQuotaRowExpand 缓存分支
- renderQuotaKeyRowsInto

验证: +  通过;隔离测试 3 Key 正确渲染
(KeyAlpha/KeyBeta/KeyGamma,data-parent 正确);端到端真实展开点击
页面在 4s+ 轮询中保持响应,hang: false。
一、ImageConvert/VideoConvert 弹窗四项整改:
- i18n: 重写 pg-i18n.js T(key,ar) 优先查 PG_I18N 再回退全局 t(), 修复 ge* 键显示英文键名/ge 前缀、cn 切换无效; 新增 geConsole; 清理死 || 回退, footer 统一 T('geCancel')/T('geStart').
- tooltip: 弹窗内按钮原生 title= 改 data-tooltip= (齿轮 title+data-tooltip 双弹修复), 动态 archTog.title 改 setAttribute('data-tooltip',...), 统一走 app.js TooltipSystem 玻璃浮层.
- VideoConvert 源信息: _editVideoPath 对 plain/backend 返回目录(剥离文件名), zip 返回 zipAbsPath, fs 返回 ''; download.js playVideo 的 name 改用 normalizedPath 末段文件名而非下载 URL → row1=目录, row2=文件名+元数据(不再出现源网址).
- 右侧控制台面板(转换时显示 ffmpeg 指令与实时输出): 后端 Job 新增 Command/logBuf, Snapshot 运行中优先 logBuf.Read() 实时输出, tailBuffer 加 sync.Mutex, 提取包级 ffmpegCommonFlags, 新增导出 FfmpegCommandString, runJob 前置 command/logBuf 结束清空, galleryEditStatus 响应加 logTail/command(经 Get→Snapshot 取实时值); 前端 _geEnsureConsole 追加并排隐藏面板, _geConsoleBlock 单/批量共用, _updateProgress/_pollBatchJob 写指令与实时日志, _onCompleted/_onError 移除内嵌 details 日志块, playground.css .ge-console-panel/.ge-console-block* 样式.

二、控制台面板高度对齐 + 任务后台持续 + 重开恢复锁:
- 高度对齐: ResizeObserver(_geWatchConsoleHeight) 观察左侧 .pg-modal, _geSyncConsoleHeight 把 #ge-console-panel.height 同步为左面板实测高度; .ge-console-log 改 flex 列, .ge-console-block-single 单 job 块 flex:1 填满(日志 max-height:none), 批量多块保持 max-height:220px + 容器滚动.
- 后台持续 + 锁: 新增 _geActiveJob(single/batch), _startJob/_startBatch 设置, _onCompleted/_onError/_onCancelled/_onBatchComplete 清空(任一终态释放锁); openMediaEditor 改 async, 有在途任务则校验(single fetch /edit/status 判 running; batch _batchDone<_batchTotal) → _geResumeActive 重显未销毁弹窗 DOM+控制台块+恢复轮询并忽略新点击项(锁), 终态则清空回退正常加载新项; cleanupMediaEditor 改为只 _stopPolling+_geBatchPollingEnabled=false+断开 RO, 保留 _geActiveJob(切页不丢任务); 批量 _pollBatchJob 加 j.polling 防重入守卫 + _geBatchPollingEnabled 闸(页离开停链/重开重启), _cancelJob 批量分支取消所有未完成 job 并清空 _geActiveJob/_batchJobs 释放锁.

文档同步: playground-architecture.md 增补#20/#21, PROJECT_MAP §10.9a, download-architecture.md playVideo 说明.
新增 Settings 侧栏 Path 行,复用 download.js 的共享弹窗 openPathSettingsModal(opts),
按 sections 条件渲染行(Settings 页全 5 项;Download 页 3 项 + UseProxy 开关;Gallery 2 项)。

核心变更:
- config/types.go: TraceConfig +LogDir, DownloadConfig Proxy→UseProxy
- config/paths.go (新): ResolveDownloadProxy/ResolveTraceDir
- config/persistence.go: decodeConfig 自动迁移 deprecatedFieldPaths (download.proxy)
- app/app.go: 装配阶段调用 config 包解析函数
- proxy/handler.go: requestLogDir 改 atomic.Value 支持运行时重指
- request_log.go: 走 TracesDir() 而非直读字段
- api/settings/register.go: getSettings +configDir/trace.logDir/imageSaveDir;
  updateSettings download 指针字段按需合并; pushDownloadSettings 重算 RuntimeSettings
- api/download/register.go: browseSystemPath +initialPath +resolveBrowseInitialDir MkdirAll
- api/trace/register.go: getDates 空目录返回空列表而非 500
- fsutil/open_windows.go: OpenFilePickerAt/OpenDirectoryPickerAt +SHCreateItemFromParsingName
- fsutil/open_other.go: macOS osascript default location stubs
- download.js: openPathSettingsModal 共享弹窗 + fasBrowsePicker 初始目录 + browsePickerOpen 锁 + trapHandler 键盘陷阱 + UseProxy 布局修正
- endpoint.js: Settings 侧栏 Path 行 + openPathModal
- i18n.js: pathSettings/imageDir/logDir/useProxyHint 键
- index-nopg.html: 补加载 download.js (Settings 页依赖)
- gallery-edit.js: 齿轮按钮改调 openPathSettingsModal
- 文档: 4 个架构 doc 加 2026-07-30 最后核对; PROJECT_MAP §24 + AGENTS.md 高频表新增路径设置行
Automated dependency upgrade by OrbisAI Security
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants