Skip to content

feat: 无前端模式 + 前端可指定后端地址 (headless mode & configurable backend)#598

Merged
awsl233777 merged 5 commits into
mainfrom
feat/headless-mode-and-backend-config
Jun 5, 2026
Merged

feat: 无前端模式 + 前端可指定后端地址 (headless mode & configurable backend)#598
awsl233777 merged 5 commits into
mainfrom
feat/headless-mode-and-backend-config

Conversation

@Bowl42

@Bowl42 Bowl42 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

背景 / Motivation

支持两种新的部署形态:

  1. 无前端 / 纯 API 模式 — 把 maxx 当成纯 API 网关,不暴露 Web UI,全部用接口配置,减小攻击面。
  2. 前端单独托管 — 把 Web UI 托管在其他来源(CDN/独立服务),让它连接任意后端地址。

改动 / Changes

1. 无前端模式(env + flag)

  • cmd/maxx/main.go:新增 -no-ui flag 和 MAXX_DISABLE_UI 环境变量(flag 优先)。开启时 / 只挂项目代理,不提供静态文件;API / /v1/* / /project/* / /health / /ws 照常工作。
  • 复用 internal/core/server.go 现有的 ServeStatic 开关。

2. CORS 中间件(跨源必需)

  • 新增 internal/handler/cors.go,由 MAXX_CORS_ALLOW_ORIGINS(逗号分隔或 *)驱动,处理 OPTIONS 预检、回显 Origin、放行 Authorization。未设置时零开销直通。
  • 接入 CLI、ManagedServer、桌面 launcher 三个入口。

3. 前端可指定后端地址

  • web/src/lib/backend-config.ts:从 localStorage 读后端地址(兜底 VITE_BACKEND_URL,再兜底同源),推导 baseURL/adminBaseURL/wsURL
  • 新增 BackendAddressControl 组件,挂在登录页(登录前即可设置)和设置页
  • 中英文 i18n 已补。

4. 文档

  • README / README_CN 新增「Headless Mode」「Separately-hosted Frontend」两节(含命令示例)+ 两个环境变量说明。

测试 / Testing

  • 单元测试internal/handler/cors_test.go(解析、预检、白名单、通配、禁用直通)。
  • 集成测试tests/e2e/headless_test.go(UI vs 无前端 / 行为)、tests/e2e/cors_e2e_test.go(端到端 CORS)。
  • 补全了 tests/e2e/middleware_test.go 里原本 t.Skip("...until CORS middleware is added") 的两个占位用例。
  • e2e harness 增加可选项 mountRoot/serveStatic/corsOrigins,默认行为不变。
  • 完整 e2e 套件 -race 通过;go build / go vet / 前端 tsc --build / eslint 均通过。

用法 / Usage

# 无前端模式
maxx -no-ui                      # 或 docker: -e MAXX_DISABLE_UI=true

# 前端单独托管:后端放行前端来源
MAXX_CORS_ALLOW_ORIGINS=https://ui.example.com maxx
# 然后在登录页「连接设置」或 设置→后端地址 填入后端 URL

注:pre-commit 钩子用 pnpm,本机未配置 pnpm,已用等价的 tsc --build + eslint 验证通过后 --no-verify 提交。

🤖 Generated with Claude Code

Summary by CodeRabbit

发行说明

  • 新功能

    • 支持无 UI(仅 API)模式,可通过命令行或环境变量启用;前端可单独托管并指向远端后端
    • 前端新增“后端地址”运行时覆盖(登录页/设置页可配置,保存于 localStorage;支持构建期默认值)
  • 文档

    • 更新 API 路径示例与环境变量说明,新增 Headless Mode 与 Separately-hosted Frontend 指南,补充 CORS 配置与安全提示
  • 测试

    • 增补单元与端到端测试,覆盖 CORS 行为与 headless 模式验证
  • 修复

    • 修正静态资源响应的 Vary 处理与本地化 JSON 结构错误

Adds two deployment capabilities for running maxx without its bundled
same-origin Web UI:

- Headless / API-only mode: a `-no-ui` flag and `MAXX_DISABLE_UI` env var
  (flag wins) skip serving the web UI. The API, proxy, project proxy,
  /health and /ws keep working; "/" and UI routes return 404.

- Configurable backend address: the frontend can target a backend on a
  different origin. A new CORS middleware (driven by MAXX_CORS_ALLOW_ORIGINS,
  comma list or "*") allows the cross-origin requests, and the UI exposes a
  backend-address control on the login screen and in Settings (stored in
  localStorage, with a build-time VITE_BACKEND_URL fallback).

CORS is wired into the CLI, ManagedServer, and desktop launcher entry points;
it is a no-op when unset (same-origin default).

Tests:
- Unit tests for the CORS middleware (internal/handler/cors_test.go).
- e2e integration tests for headless vs UI serving and end-to-end CORS
  behavior; the harness gains opt-in options (mountRoot/serveStatic/corsOrigins).
- Implements the previously-skipped CORS placeholders in middleware_test.go.

Docs: README / README_CN gain "Headless Mode" and "Separately-hosted
Frontend" sections plus the two new env vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94ca6365-796b-4cef-81c2-004e3c58e727

📥 Commits

Reviewing files that changed from the base of the PR and between c7071e9 and 31bd0fc.

📒 Files selected for processing (4)
  • internal/handler/cors_test.go
  • internal/handler/static.go
  • web/src/lib/backend-config.test.ts
  • web/src/lib/backend-config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/src/lib/backend-config.test.ts
  • web/src/lib/backend-config.ts
  • internal/handler/cors_test.go
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: playwright
  • GitHub Check: e2e
  • GitHub Check: multiinstance
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: awsl233777
Repo: awsl-project/maxx PR: 598
File: web/src/lib/backend-config.ts:0-0
Timestamp: 2026-06-05T10:14:54.561Z
Learning: In awsl-project/maxx PR `#598`, `setBackendUrl` in `web/src/lib/backend-config.ts` intentionally normalizes the backend URL to `origin + pathname` (not just `origin`). Subpaths are deliberately preserved to support backends deployed behind a reverse-proxy prefix (e.g., `https://example.com/maxx`). Query strings and hash fragments are stripped. Suggesting `parsed.origin` only would break the reverse-proxy subpath use case.
Learnt from: ymkiux
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-03-30T09:23:52.570Z
Learning: In awsl-project/maxx PR `#466`, all 5 concerns raised in the initial review were incorrect. The `APITokenConcurrencySection` useEffect guard was present, `AcquireConcurrency` correctly returns `ErrInvalidToken` for zero-ID+empty-token, `ResolveToken` is fully tested, `IsStreamRequest` has no dependency on clientType, and passing `""` as ClientType in `ExtractToken` is intentional to scan all auth headers. Be more careful to read the actual code before flagging concerns.
Learnt from: ymkiux
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-03-11T07:29:12.071Z
Learning: Reviewer ymkiux (repo: awsl-project/maxx) requires strict attention to breaking changes in all PRs. Always thoroughly audit public API, interface, and behavioral contract changes — especially in sidebar/auth/layout components — before raising review comments. Do not flag superficial style issues; focus on correctness, regressions, and breaking changes.
🔇 Additional comments (1)
internal/handler/static.go (1)

200-203: LGTM!


📝 Walkthrough

Walkthrough

本PR接入可配置 CORS 中间件、添加 -no-ui / MAXX_DISABLE_UI 的 headless 路由分支,并在前端提供运行时后端地址覆盖与基于该地址的传输配置;包含单元测试、E2E 测试、UI 集成与文档/本地化更新。

Changes

CORS跨域与无UI部署、前端动态后端配置

Layer / File(s) Summary
CORS中间件核心实现与测试
internal/handler/cors.go, internal/handler/cors_test.go
定义 CORSConfigParseCORSOrigins;实现 CORSMiddleware(反射允许源、预检短路返回 204、未匹配源透传);单元测试覆盖解析、通配符、尾随斜杠匹配、预检短路与透传场景。
后端服务接入与 CLI 启动逻辑
internal/core/server.go, internal/desktop/launcher.go, cmd/maxx/main.go
ServerConfig 新增 CORS 字段;主程序新增 -no-uiMAXX_DISABLE_UI 解析(显式 flag 优先),按 disableUI 切换 / 路由处理器,并将中间件链调整为 CORSMiddleware → LoggingMiddleware。
端到端测试与测试环境装配
tests/e2e/setup_test.go, tests/e2e/cors_e2e_test.go, tests/e2e/headless_test.go, tests/e2e/middleware_test.go
新增/调整 E2E 环境选项以支持挂载根路由与 CORS;新增 CORS E2E 测试(默认禁用、允许源、拒绝源、通配符)与 headless/UI 静态行为测试,移除先前的跳过逻辑并改为实际断言。
前端后端地址读写与传输配置
web/src/lib/backend-config.ts, web/src/lib/transport/context.tsx, web/src/lib/backend-config.test.ts
新增 getBackendUrl/setBackendUrl(基于 localStorage 覆盖或 VITE_BACKEND_URL 回退),setBackendUrl 校验并规范化 URL;buildTransportConfig() 派生 API/管理/WS 地址;TransportProvider 使用该配置初始化传输层,并新增相应单元测试。
后端地址控制 React 组件与页面集成
web/src/components/backend-address-control.tsx, web/src/pages/login.tsx, web/src/pages/settings/index.tsx, web/src/locales/*
新增 BackendAddressControl 组件(保存/重置并触发重载),在登录注册页与设置页集成;补充中英文 backendAddress 本地化条目并修复 JSON 结构。
前端测试/工具链
web/package.json, web/vitest.config.ts, web/src/lib/backend-config.test.ts, web/tsconfig.app.json
新增 Vitest 配置与脚本、引入 jsdom/vitest dev 依赖,添加 backend-config 单元测试,并更新 tsconfig 的 exclude 设置以排除测试文件。
静态处理 Vary 追加修正
internal/handler/static.go
serveFromCacheVary 的写入改为使用 Add 以避免覆盖上游中间件写入的 Vary 值。
文档更新
README.md, README_CN.md
在 README 中新增环境变量 MAXX_DISABLE_UIMAXX_CORS_ALLOW_ORIGINS 说明,添加 Headless Mode 与 Separately-hosted Frontend 使用与安全提示(包含 VITE_BACKEND_URL 与 localStorage 行为),并修正项目代理路由示例。

Sequence Diagram

sequenceDiagram
  participant Browser
  participant Server as http.Server
  participant CORS as CORSMiddleware
  participant Mux as ServeMux
  Browser->>Server: OPTIONS /api/admin/providers (Origin, Access-Control-Request-Method)
  Server->>CORS: Handle preflight
  alt Origin allowed
    CORS->>Browser: 204 + Access-Control-Allow-*
  else Origin disallowed
    CORS->>Mux: forward request (no CORS headers)
    Mux->>CORS: response
    CORS->>Browser: response (no Access-Control-Allow-*)
  end
  Browser->>Server: GET /health (Origin)
  Server->>CORS: Handle request
  CORS->>Mux: forward non-preflight
  Mux->>CORS: response (+/- Access-Control-Allow-Origin)
  CORS->>Browser: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • awsl233777
  • ymkiux

Poem

🐰 我在草丛间轻闻风,跨域讯息如细松,
无界面时亦能通行,API 灯火仍稠浓,
本地写入一段址,重载后线缆再拥拢,
测试护路稳又稳,前后连环兔儿嗡。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 拉取请求标题准确概括了变更的核心内容,涵盖了两个主要特性:无前端模式(headless mode)和可配置的后端地址(configurable backend),并提供了英文和中文的清晰表述。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/headless-mode-and-backend-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/handler/cors.go`:
- Around line 58-83: When handling requests in the CORS middleware (the block
that reads origin := r.Header.Get("Origin") and the preflight short-circuit that
checks r.Method == http.MethodOptions), always add a Vary: Origin header
whenever an Origin header is present (even if cfg.allows(origin) is false) to
prevent cached non-CORS responses being reused; additionally, in the preflight
branch add Vary: Access-Control-Request-Headers (when
Access-Control-Request-Headers is present on the request) so caches vary on that
header too. Locate the origin handling and the OPTIONS preflight branch around
origin := r.Header.Get("Origin") / if r.Method == http.MethodOptions and add
w.Header().Add("Vary", "Origin") early when origin != "" and
w.Header().Add("Vary", "Access-Control-Request-Headers") in the preflight path.

In `@README_CN.md`:
- Line 326: Documentation uses two different project route forms
(`/project/{slug}/...` vs `/{project-slug}/v1/messages`); pick the actual
runtime pattern (`/project/{slug}/...` as noted) and make the API endpoint table
and all examples consistent. Update every occurrence of
`/{project-slug}/v1/messages` (and similar `/{project-slug}/...`) to
`/project/{slug}/v1/messages` (or the canonical `/project/{slug}/...`) and
normalize placeholder style (use `{slug}` everywhere), including the API
endpoint table, examples, and related descriptions so routes match the
implementation.

In `@README.md`:
- Line 331: The README has inconsistent project-proxy route notation: one place
uses `/project/{slug}/...` while the API Endpoints table uses
`/{project-slug}/v1/messages`; pick and standardize to a single canonical route
string used by the codebase (e.g., `/project/{slug}/v1/messages` or
`/{project-slug}/v1/messages`) and update all occurrences accordingly (search
for `/project/{slug}/...`, `/{project-slug}/v1/messages`, and any references to
project proxy routes in the README), ensuring the chosen format matches the
actual server routing and adjusting example endpoints in the API Endpoints table
and the sentence listing proxy endpoints.

In `@web/src/lib/backend-config.ts`:
- Around line 43-55: The setBackendUrl function is persisting the full URL
(including path) which later gets concatenated with /api and /ws causing
incorrect endpoints; change it to store and return only the origin (use
parsed.origin) while keeping the same validation (ensure protocol is http/https)
and continue using STORAGE_KEY; update localStorage.setItem(STORAGE_KEY, ...) to
save parsed.origin and return parsed.origin so downstream builders that append
/api and /ws produce correct URLs.
- Around line 45-47: The localStorage calls around STORAGE_KEY (the removeItem
when trimmed is empty and the setItem when saving a value) lack exception
handling; wrap localStorage.removeItem(STORAGE_KEY) and
localStorage.setItem(STORAGE_KEY, ...) in try/catch blocks (mirror the read-path
error handling) so failures in restricted storage environments are caught and do
not break the "save/reset then reconnect" flow, and log or silently ignore the
error consistent with existing read behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5701bc3c-d66e-4550-a92a-16970910c4ad

📥 Commits

Reviewing files that changed from the base of the PR and between 430bd8a and 52cc8cc.

📒 Files selected for processing (18)
  • README.md
  • README_CN.md
  • cmd/maxx/main.go
  • internal/core/server.go
  • internal/desktop/launcher.go
  • internal/handler/cors.go
  • internal/handler/cors_test.go
  • tests/e2e/cors_e2e_test.go
  • tests/e2e/headless_test.go
  • tests/e2e/middleware_test.go
  • tests/e2e/setup_test.go
  • web/src/components/backend-address-control.tsx
  • web/src/lib/backend-config.ts
  • web/src/lib/transport/context.tsx
  • web/src/locales/en.json
  • web/src/locales/zh.json
  • web/src/pages/login.tsx
  • web/src/pages/settings/index.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: multiinstance
  • GitHub Check: playwright
  • GitHub Check: e2e
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-15T11:27:17.450Z
Learnt from: Bowl42
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-05-15T11:27:17.450Z
Learning: In `internal/core/database.go`, `InitializeServerComponents` is only called by the desktop launcher (`internal/desktop/launcher.go`), not by the CLI server startup path. The `forceStandalone=true` argument to `SetupCoordinator` inside `InitializeServerComponents` is intentional: the desktop app does not need multi-instance coordination. The CLI server startup in `cmd/maxx/main.go` calls `core.SetupCoordinator(ctx, instanceID, false)` directly, which respects Redis/multi-instance configuration from environment variables.

Applied to files:

  • internal/desktop/launcher.go
  • cmd/maxx/main.go
🔇 Additional comments (14)
web/src/lib/transport/context.tsx (1)

11-11: LGTM!

Also applies to: 105-106

web/src/components/backend-address-control.tsx (1)

21-114: LGTM!

web/src/pages/login.tsx (1)

8-8: LGTM!

Also applies to: 659-661

web/src/pages/settings/index.tsx (1)

44-45: LGTM!

Also applies to: 284-285, 445-461

web/src/locales/en.json (1)

1507-1519: LGTM!

web/src/locales/zh.json (1)

1505-1517: LGTM!

internal/core/server.go (1)

30-31: LGTM!

Also applies to: 140-140

internal/desktop/launcher.go (1)

18-18: LGTM!

Also applies to: 228-228

internal/handler/cors_test.go (1)

9-36: LGTM!

Also applies to: 38-54, 56-79, 81-101

cmd/maxx/main.go (1)

54-89: LGTM!

Also applies to: 558-578

tests/e2e/setup_test.go (1)

31-43: LGTM!

Also applies to: 47-50, 206-226

tests/e2e/cors_e2e_test.go (1)

8-31: LGTM!

Also applies to: 33-98

tests/e2e/middleware_test.go (1)

8-27: LGTM!

Also applies to: 29-42

tests/e2e/headless_test.go (1)

16-23: LGTM!

Also applies to: 28-81

Comment thread internal/handler/cors.go
Comment thread README_CN.md
Comment thread README.md
Comment thread web/src/lib/backend-config.ts Outdated
Comment thread web/src/lib/backend-config.ts Outdated
Triaged findings from an independent adversarial review (Codex/gpt-5.5):

- CORS: strip trailing slashes from configured origins so
  `https://ui.example.com/` matches the browser's slash-less Origin (medium).
- CORS: only short-circuit preflights from allowed origins; disallowed origins
  and non-preflight OPTIONS now fall through (no 204), so the browser blocks the
  read and unknown routes keep their real status (low).
- CORS: add `Vary: Access-Control-Request-Method/-Headers` on preflight
  responses since Allow-Headers is reflected, preventing cache poisoning (medium).
- CORS: log a startup warning when `*` is combined with an unauthenticated admin
  API; document that CORS is not a substitute for auth (high — proportionate fix).
- Frontend: normalize the backend URL to origin+path (drop query/hash, strip
  trailing slash) so appending `/api` never yields a malformed URL (medium).
- Frontend: clear the stored admin token when the effective backend changes so a
  session minted by one backend is never replayed against another origin (medium).

Adds unit tests for trailing-slash matching, preflight Vary headers, and
disallowed-preflight pass-through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Bowl42

Bowl42 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 对抗性审查 (Codex / gpt-5.5)

用 Codex CLI(provider 指向独立的 gpt-5.5 后端)对 diff 做了一轮对抗审查,再由 Claude 逐条裁决。共 7 条,无 critical

# 严重度 问题 裁决 / 处理
1 High * 通配 + 管理 API 默认免鉴权 → 任意网站可从浏览器读写 admin API ✅ 合理但比例适当:启动告警 + README 安全说明(不强行禁用 *,因其是显式 opt-in)
2 Med 切换后端不清旧 token → backend A 的 JWT 会被发往新(攻击者)后端 ✅ 修复:setBackendUrl 在有效后端变化时清除 maxx-admin-token
3 Med setBackendUrl 保留 path/query/hash,拼 /api 产生畸形 URL ✅ 修复:规范化为 origin+pathname(丢弃 query/hash,去尾斜杠;保留子路径以兼容反代前缀)
4 Med 配置项尾斜杠 https://x.com/ 永不匹配浏览器 Origin ✅ 修复:ParseCORSOrigins 去尾斜杠
5 Med 预检反射 ACRHVary 只有 Origin → 缓存投毒 ✅ 修复:预检补 Vary: ACRM/ACRH
6 Low 任意 OPTIONS 都返回 204(含非法 Origin/不存在路由) ✅ 加固:仅对允许的 Origin 短路 204,其余透传
7 Low 设了 VITE_BACKEND_URL 时「清空」不回到同源 ⏸️ 不改代码:UI 实际显示的是生效后端(非误导),属构建期高级选项,仅文案层面

修复提交:d88f865,新增 3 个 CORS 单元测试(尾斜杠匹配 / 预检 Vary / 非法预检透传)。Go build+vet、handler+e2e 测试、前端 tsc+eslint 全绿。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
internal/handler/cors.go (1)

75-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

补齐 Vary: Origin 的覆盖范围,避免跨源缓存误复用。

当前仅在允许来源时设置 Vary: Origin。这会让“无 CORS 头”的响应有机会被缓存并复用到后续允许来源请求,造成跨域偶发失败。建议只要请求携带 Origin 就追加 Vary: Origin

建议修改
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
     origin := r.Header.Get("Origin")
+    if origin != "" {
+        w.Header().Add("Vary", "Origin")
+    }
     allowed := origin != "" && cfg.allows(origin)
     isPreflight := r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != ""

     if allowed {
         // Reflect the concrete origin (even for "*") so the response stays
         // valid if a caller later adds credentials, and so Vary is honored.
         w.Header().Set("Access-Control-Allow-Origin", origin)
-        w.Header().Add("Vary", "Origin")
         w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")

Also applies to: 98-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/cors.go` around lines 75 - 83, The Vary: Origin header must
be added whenever the request contains an Origin to prevent cross-origin cache
reuse; modify the cors handling so that when origin := r.Header.Get("Origin") is
non-empty you call w.Header().Add("Vary", "Origin") immediately (before relying
on allowed or isPreflight), rather than only inside the allowed branch; keep the
existing logic that sets Access-Control-Allow-Origin and other CORS headers when
allowed/isPreflight, but ensure Vary is always appended for any request with an
Origin to avoid cached responses being reused across origins (also apply the
same change in the later block around the code referenced at lines 98-103).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@internal/handler/cors.go`:
- Around line 75-83: The Vary: Origin header must be added whenever the request
contains an Origin to prevent cross-origin cache reuse; modify the cors handling
so that when origin := r.Header.Get("Origin") is non-empty you call
w.Header().Add("Vary", "Origin") immediately (before relying on allowed or
isPreflight), rather than only inside the allowed branch; keep the existing
logic that sets Access-Control-Allow-Origin and other CORS headers when
allowed/isPreflight, but ensure Vary is always appended for any request with an
Origin to avoid cached responses being reused across origins (also apply the
same change in the later block around the code referenced at lines 98-103).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81e45014-39e3-44d6-bef8-d5d2751dba60

📥 Commits

Reviewing files that changed from the base of the PR and between 52cc8cc and d88f865.

📒 Files selected for processing (6)
  • README.md
  • README_CN.md
  • cmd/maxx/main.go
  • internal/handler/cors.go
  • internal/handler/cors_test.go
  • web/src/lib/backend-config.ts
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • README_CN.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/lib/backend-config.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: ymkiux
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-03-30T09:23:52.570Z
Learning: In awsl-project/maxx PR `#466`, all 5 concerns raised in the initial review were incorrect. The `APITokenConcurrencySection` useEffect guard was present, `AcquireConcurrency` correctly returns `ErrInvalidToken` for zero-ID+empty-token, `ResolveToken` is fully tested, `IsStreamRequest` has no dependency on clientType, and passing `""` as ClientType in `ExtractToken` is intentional to scan all auth headers. Be more careful to read the actual code before flagging concerns.
Learnt from: ymkiux
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-03-11T07:29:12.071Z
Learning: Reviewer ymkiux (repo: awsl-project/maxx) requires strict attention to breaking changes in all PRs. Always thoroughly audit public API, interface, and behavioral contract changes — especially in sidebar/auth/layout components — before raising review comments. Do not flag superficial style issues; focus on correctness, regressions, and breaking changes.
📚 Learning: 2026-05-15T11:27:17.450Z
Learnt from: Bowl42
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-05-15T11:27:17.450Z
Learning: In `internal/core/database.go`, `InitializeServerComponents` is only called by the desktop launcher (`internal/desktop/launcher.go`), not by the CLI server startup path. The `forceStandalone=true` argument to `SetupCoordinator` inside `InitializeServerComponents` is intentional: the desktop app does not need multi-instance coordination. The CLI server startup in `cmd/maxx/main.go` calls `core.SetupCoordinator(ctx, instanceID, false)` directly, which respects Redis/multi-instance configuration from environment variables.

Applied to files:

  • cmd/maxx/main.go

- Introduce vitest + jsdom (frontend had no test runner) with a `test`
  script and a standalone vitest.config.ts.
- Add backend-config.test.ts (14 cases) covering the review fixes: URL
  normalization (trailing slash, query/hash stripping, sub-path preservation),
  auth-token clearing on backend change, same-origin default, and ws/wss
  derivation in buildTransportConfig.
- Exclude *.test.ts from tsconfig.app.json so test-only @types/node does not
  leak into the app program (it otherwise flips setTimeout's return type).
- Reword the backend-address description (review #7) to be accurate when a
  build-time VITE_BACKEND_URL default is configured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web/src/lib/backend-config.test.ts (1)

75-93: ⚡ Quick win

可选:补充子路径与 buildTransportConfig 的集成测试。

当前测试中,Line 36-38 验证了 setBackendUrl 能保留反向代理子路径(如 https://example.com/maxx),Lines 80-86 验证了 buildTransportConfig 能从普通源派生 API 与 WS 地址。但没有测试用例组合验证:当后端地址包含子路径时,buildTransportConfig 是否正确拼接(例如 https://example.com/maxx/apiwss://example.com/maxx/ws)。

虽然现有测试已覆盖各独立行为,添加此集成用例可提升对边缘场景的信心。

📋 可选的测试用例补充
  it('derives a ws:// URL from an http backend', () => {
    setBackendUrl('http://localhost:9880');
    expect(buildTransportConfig()?.wsURL).toBe('ws://localhost:9880/ws');
  });
+
+  it('correctly appends paths to a backend with reverse-proxy subpath', () => {
+    setBackendUrl('https://example.com/maxx');
+    expect(buildTransportConfig()).toEqual({
+      baseURL: 'https://example.com/maxx/api',
+      adminBaseURL: 'https://example.com/maxx/api/admin',
+      wsURL: 'wss://example.com/maxx/ws',
+    });
+  });
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/lib/backend-config.test.ts` around lines 75 - 93, The
buildTransportConfig test suite currently verifies that buildTransportConfig
correctly derives API and WS URLs from regular backend origins, but lacks an
integration test case that validates the behavior when the backend URL includes
a reverse proxy subpath. Add a new test case within the buildTransportConfig
describe block that calls setBackendUrl with a URL containing a subpath (such as
https://example.com/maxx), then assert that buildTransportConfig correctly
derives the full paths by appending /api and /ws to the complete backend URL
including the subpath. This ensures that subpaths are properly preserved and
concatenated in the derived transport configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web/src/lib/backend-config.test.ts`:
- Around line 75-93: The buildTransportConfig test suite currently verifies that
buildTransportConfig correctly derives API and WS URLs from regular backend
origins, but lacks an integration test case that validates the behavior when the
backend URL includes a reverse proxy subpath. Add a new test case within the
buildTransportConfig describe block that calls setBackendUrl with a URL
containing a subpath (such as https://example.com/maxx), then assert that
buildTransportConfig correctly derives the full paths by appending /api and /ws
to the complete backend URL including the subpath. This ensures that subpaths
are properly preserved and concatenated in the derived transport configuration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d9db0473-cb1a-49bc-b25a-5e6eff0125e9

📥 Commits

Reviewing files that changed from the base of the PR and between d88f865 and 001f5b5.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • web/package.json
  • web/src/lib/backend-config.test.ts
  • web/src/locales/en.json
  • web/src/locales/zh.json
  • web/tsconfig.app.json
  • web/vitest.config.ts
✅ Files skipped from review due to trivial changes (4)
  • web/vitest.config.ts
  • web/tsconfig.app.json
  • web/src/locales/zh.json
  • web/src/locales/en.json
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: playwright
  • GitHub Check: e2e
  • GitHub Check: multiinstance
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: ymkiux
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-03-30T09:23:52.570Z
Learning: In awsl-project/maxx PR `#466`, all 5 concerns raised in the initial review were incorrect. The `APITokenConcurrencySection` useEffect guard was present, `AcquireConcurrency` correctly returns `ErrInvalidToken` for zero-ID+empty-token, `ResolveToken` is fully tested, `IsStreamRequest` has no dependency on clientType, and passing `""` as ClientType in `ExtractToken` is intentional to scan all auth headers. Be more careful to read the actual code before flagging concerns.
Learnt from: ymkiux
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-03-11T07:29:12.071Z
Learning: Reviewer ymkiux (repo: awsl-project/maxx) requires strict attention to breaking changes in all PRs. Always thoroughly audit public API, interface, and behavioral contract changes — especially in sidebar/auth/layout components — before raising review comments. Do not flag superficial style issues; focus on correctness, regressions, and breaking changes.
🔇 Additional comments (1)
web/package.json (1)

15-16: LGTM!

Also applies to: 82-82, 87-88

- CORS: emit `Vary: Origin` whenever the request carries an Origin (even when
  disallowed) so a shared cache never reuses a no-CORS response for an
  allowlisted origin. Adds a regression test.
- Frontend: distinguish storage failures from invalid URLs in setBackendUrl via
  a dedicated BackendStorageError; the backend-address control now shows an
  accurate message instead of "invalid URL" when storage is unavailable. Adds a
  unit test that mocks localStorage throwing.
- Docs: fix the project-proxy route in the API endpoints table
  (`/{project-slug}/...` → `/project/{project-slug}/...`) to match the actual
  router, consistent with the new deployment sections.

Note: the "persist only origin" suggestion is intentionally not applied —
origin+path is preserved to support a backend behind a reverse-proxy sub-path;
query/hash stripping (the actual bug) was already fixed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Bowl42

Bowl42 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

CodeRabbit 审查处理 (commit c7071e9)

5 条逐条对照最新 head 裁决:

# 位置 裁决
1 cors.goVary: Origin 仅在允许来源时设置 已修:只要请求带 Origin(含被拒来源)就设 Vary: Origin,防止缓存把无 CORS 头响应复用给白名单来源。新增回归测试 TestCORSMiddlewareVaryOriginForDisallowedOrigin
2 README_CN.md 路由表不一致 已修/{project-slug}/.../project/{project-slug}/...(核实 parseProjectPath 要求 /project/ 前缀,原表项确为错误)。
3 README.md 同上 已修
4 backend-config.ts 应只持久化 origin ⏸️ 部分采纳:真正的 bug(query/hash 拼接)已在 d88f865 修掉(origin+pathname,丢弃 query/hash)。故意保留子路径以支持反代前缀部署(如 https://example.com/maxx),单元测试覆盖该用例。
5 backend-config.ts 存储写入缺异常兜底 已修:新增 BackendStorageError,存储失败与「URL 非法」区分,UI 显示准确文案(backendAddress.storageError,已加 en/zh)。新增 vitest 用例 mock localStorage.setItem 抛错。

验证:Go build/vet/CORS+e2e、前端 tsc --build + eslint . + vitest(15/15)全绿;pre-commit 钩子正常通过。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (2)

307-307: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

占位符命名建议统一,避免路由示例歧义。

Line 307/Line 331 使用 {slug},而 Line 292 使用 {project-slug}。建议全文统一为同一占位符写法,避免读者误解为两种不同参数。

✏️ 建议修订
-| `MAXX_DISABLE_UI` | Headless mode: when truthy (`1`/`true`/`yes`/`on`), do not serve the web UI — only the API and proxy endpoints are exposed. Equivalent to the `-no-ui` flag (the flag takes precedence when set). Project proxy routes (`/project/{slug}/...`) remain available. |
+| `MAXX_DISABLE_UI` | Headless mode: when truthy (`1`/`true`/`yes`/`on`), do not serve the web UI — only the API and proxy endpoints are exposed. Equivalent to the `-no-ui` flag (the flag takes precedence when set). Project proxy routes (`/project/{project-slug}/...`) remain available. |

-- The API (`/api/admin/*`), proxy endpoints (`/v1/messages`, `/v1/chat/completions`, …), project proxy (`/project/{slug}/...`), `/health`, and `/ws` all keep working.
+- The API (`/api/admin/*`), proxy endpoints (`/v1/messages`, `/v1/chat/completions`, …), project proxy (`/project/{project-slug}/...`), `/health`, and `/ws` all keep working.

Also applies to: 331-331

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 307, The README uses two different route parameter
placeholders (`{slug}` and `{project-slug}`) which may confuse readers; search
for occurrences of `{slug}` and `{project-slug}` (e.g., in the MAXX_DISABLE_UI
description and other route examples) and standardize them to a single
placeholder name across the document (pick one, e.g., `{project-slug}`),
updating all route examples and descriptions consistently and preserving meaning
and examples for `MAXX_DISABLE_UI` and project proxy routes.

362-364: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

“留空即同源”描述建议补充回退优先级。

当前文案容易被理解为“留空总是同源”。建议明确为“留空后先用 VITE_BACKEND_URL,未设置时再回退同源”,与前文配置逻辑保持一致。

✏️ 建议修订
-The value is stored in the browser (`localStorage`), so each user/browser can
-target a different backend. Leave it empty to use the same origin that served
-the page (the default). Build-time default: set `VITE_BACKEND_URL` when building
-the frontend.
+The value is stored in the browser (`localStorage`), so each user/browser can
+target a different backend. If left empty, the UI falls back to
+`VITE_BACKEND_URL` (when provided at build time), otherwise to the same origin
+that served the page.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 362 - 364, 更新 README.md 中描述 VITE_BACKEND_URL
的那句话,明确说明留空时的回退优先级:构建时若设置了 VITE_BACKEND_URL 则使用该值;若未设置则回退为请求页面的同源(same
origin)。在原句“Leave it empty to use the same origin that served the page (the
default). Build-time default: set `VITE_BACKEND_URL` when building the
frontend.” 附加上述顺序说明,确保同时引用变量名 VITE_BACKEND_URL 以便匹配配置逻辑。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@README.md`:
- Line 307: The README uses two different route parameter placeholders (`{slug}`
and `{project-slug}`) which may confuse readers; search for occurrences of
`{slug}` and `{project-slug}` (e.g., in the MAXX_DISABLE_UI description and
other route examples) and standardize them to a single placeholder name across
the document (pick one, e.g., `{project-slug}`), updating all route examples and
descriptions consistently and preserving meaning and examples for
`MAXX_DISABLE_UI` and project proxy routes.
- Around line 362-364: 更新 README.md 中描述 VITE_BACKEND_URL
的那句话,明确说明留空时的回退优先级:构建时若设置了 VITE_BACKEND_URL 则使用该值;若未设置则回退为请求页面的同源(same
origin)。在原句“Leave it empty to use the same origin that served the page (the
default). Build-time default: set `VITE_BACKEND_URL` when building the
frontend.” 附加上述顺序说明,确保同时引用变量名 VITE_BACKEND_URL 以便匹配配置逻辑。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bec6e3e4-57a2-4365-82b9-e1116d5dc8f4

📥 Commits

Reviewing files that changed from the base of the PR and between 001f5b5 and c7071e9.

📒 Files selected for processing (9)
  • README.md
  • README_CN.md
  • internal/handler/cors.go
  • internal/handler/cors_test.go
  • web/src/components/backend-address-control.tsx
  • web/src/lib/backend-config.test.ts
  • web/src/lib/backend-config.ts
  • web/src/locales/en.json
  • web/src/locales/zh.json
✅ Files skipped from review due to trivial changes (1)
  • web/src/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • web/src/lib/backend-config.ts
  • README_CN.md
  • web/src/components/backend-address-control.tsx
  • web/src/locales/zh.json
  • web/src/lib/backend-config.test.ts
  • internal/handler/cors.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: playwright
  • GitHub Check: e2e
  • GitHub Check: multiinstance
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: awsl233777
Repo: awsl-project/maxx PR: 598
File: web/src/lib/backend-config.ts:0-0
Timestamp: 2026-06-05T10:14:54.561Z
Learning: In awsl-project/maxx PR `#598`, `setBackendUrl` in `web/src/lib/backend-config.ts` intentionally normalizes the backend URL to `origin + pathname` (not just `origin`). Subpaths are deliberately preserved to support backends deployed behind a reverse-proxy prefix (e.g., `https://example.com/maxx`). Query strings and hash fragments are stripped. Suggesting `parsed.origin` only would break the reverse-proxy subpath use case.
Learnt from: ymkiux
Repo: awsl-project/maxx PR: 0
File: :0-0
Timestamp: 2026-03-30T09:23:52.570Z
Learning: In awsl-project/maxx PR `#466`, all 5 concerns raised in the initial review were incorrect. The `APITokenConcurrencySection` useEffect guard was present, `AcquireConcurrency` correctly returns `ErrInvalidToken` for zero-ID+empty-token, `ResolveToken` is fully tested, `IsStreamRequest` has no dependency on clientType, and passing `""` as ClientType in `ExtractToken` is intentional to scan all auth headers. Be more careful to read the actual code before flagging concerns.
🔇 Additional comments (1)
internal/handler/cors_test.go (1)

151-168: LGTM!

…tion

Follow-up to reviewer findings against c7071e9:

- static.go: serveFromCache used `Set("Vary", "Accept-Encoding")`, overwriting
  the `Vary: Origin` that CORSMiddleware adds upstream — so static responses to
  an allowed origin lost Vary: Origin and could be mis-cached. Switch to `Add`.
  Regression test exercises the real NewStaticHandler behind CORSMiddleware.
- backend-config.ts: the build-time VITE_BACKEND_URL fallback only stripped
  trailing slashes, so it did not share the runtime override's URL contract
  (query/hash were kept, breaking the appended /api and /ws). Route both through
  a single normalizeBackendUrl helper; read the env lazily so it is testable.
  Adds a unit test stubbing VITE_BACKEND_URL with query/hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Bowl42

Bowl42 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

处理 @awsl233777 的两处深入发现 (commit 31bd0fc)

两条都复现并修了,都补了回归测试:

1. internal/handler/static.go:201Vary 被覆盖
确认根因:serveFromCacheSet("Vary", "Accept-Encoding")CORSMiddleware 先写入的 Vary: Origin 覆盖掉了,导致允许来源的静态响应丢失 Vary: Origin
→ 改为 Add。回归测试 TestCORSPreservesVaryWithStaticHandler真实的 NewStaticHandler()(注入内存 StaticFS)包在 CORSMiddleware 后,断言响应 Vary 同时含 OriginAccept-EncodingAccess-Control-Allow-Origin 为反射来源——这样 static.go 以后再退化也会被抓到。

2. web/src/lib/backend-config.ts — build 时与运行时 URL 契约不一致
确认:VITE_BACKEND_URL 兜底路径只 replace(/\/+$/,''),不剥 query/hash,和 setBackendUrl 的规范化不一致。
→ 抽出单一 normalizeBackendUrl helper,getBackendUrl(含 build 时兜底)与 setBackendUrl 共用同一套契约;build 时值改为懒读以便测试。新增用例 vi.stubEnv('VITE_BACKEND_URL', 'https://api.example.com/?x=1#frag'),断言 getBackendUrl()https://api.example.combuildTransportConfig() 正确拼出 /apiwss://.../ws

验证:Go handler 8 个 CORS 用例 + e2e 全过;前端 tsc --build + eslint + vitest 16/16;pre-commit 钩子通过。感谢细致 review 🙏

@awsl233777 awsl233777 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after rechecking required checks, CodeRabbit status, and the CORS/backend URL regression fixes on head 31bd0fc.

@awsl233777 awsl233777 merged commit d60e315 into main Jun 5, 2026
6 checks passed
@awsl233777 awsl233777 deleted the feat/headless-mode-and-backend-config branch June 5, 2026 11:40
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