Skip to content

feat: add provider-scoped proxy routes#446

Merged
ymkiux merged 1 commit intomainfrom
feat/provider-proxy-scope
Mar 23, 2026
Merged

feat: add provider-scoped proxy routes#446
ymkiux merged 1 commit intomainfrom
feat/provider-proxy-scope

Conversation

@awsl233777
Copy link
Collaborator

@awsl233777 awsl233777 commented Mar 22, 2026

Summary

  • add provider-scoped proxy routes so requests can target /provider/{id}/... directly
  • thread provider scope through proxy matching while keeping the design easy to extend later

Changes

  • add scoped proxy parsing for /provider/{id}/... alongside /project/{slug}/...
  • pass providerID through proxy ingress and router matching so only routes for that provider are considered
  • update combined/static routing to recognize provider-scoped proxy paths
  • add handler tests plus Go E2E and Playwright E2E coverage for provider-scoped requests

Tests

  • go test ./...
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9980 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

Risks

  • low: this changes scoped proxy path handling and route filtering, so regressions would likely show up in scoped proxy entrypoints

Follow-ups

  • None

Summary by CodeRabbit

发布说明

  • 新功能

    • 新增提供者作用域代理路由,支持 /provider/{id}/... 请求并将其作为提供者级别的代理处理;根路由同时支持项目与提供者作用域并分别路由。
    • 支持通过路径或 X-Maxx-Provider-ID 头关联提供者;有效 ID 会在请求流中可用并在响应/日志中体现。
  • 测试

    • 新增单元与端到端测试,覆盖路径解析、路由匹配、头部验证及在 UI/请求记录中的可见性验证(含截图)。

@coderabbitai
Copy link

coderabbitai bot commented Mar 22, 2026

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
📝 Walkthrough

Walkthrough

引入 provider 范围代理路由:新增 ProviderProxyHandler 并在启动/组件初始化中注入;项目/提供商范围路径解析与路由分发更新;流上下文与 Router.Match 中传递 provider id;静态/根路由注册与多项单元/端到端测试扩展以覆盖 provider 范围行为。

Changes

Cohort / File(s) Summary
入口与服务初始化
cmd/maxx/main.go, internal/core/database.go, internal/core/server.go
构造并注入 ProviderProxyHandler,将其加入 ServerComponents,更新根/前缀路由注册以同时支持 /project//provider/(兼顾 ServeStatic 分支)。
新 Handler:provider 代理
internal/handler/provider_proxy.go, internal/handler/provider_proxy_test.go
新增 ProviderProxyHandler,解析 /provider/{id}/*、验证 provider id、查找 provider、设置 X-Maxx-Provider-ID 并转发到 models/proxy;附带详细单元测试与路径/端点验证。
Project 路由与静态组合
internal/handler/project_proxy.go, internal/handler/project_proxy_test.go, internal/handler/static.go
ProjectProxy 改为通用 scoped 路径解析(支持 project/provider 范围),接受 providerRepo;NewCombinedHandler 增加 provider 参数并按 /project//provider/、其他 顺序路由。
代理入站与流上下文
internal/handler/proxy.go, internal/flow/keys.go, internal/executor/flow_state.go, internal/executor/middleware_ingress.go, internal/executor/middleware_route_match.go
ProxyHandler 读取并验证 X-Maxx-Provider-ID 并写入 flow;新增 flow.KeyProviderID 常量与 execState.providerID 字段;中间件在匹配上下文中传入 providerID。
路由引擎与匹配逻辑
internal/router/router.go, internal/router/router_test.go
MatchContext 添加 ProviderID,重构匹配逻辑为 routeMatchesBaseScope / routeMatchesProviderScope,并更新匹配规则以支持 provider 范围与相应单元测试。
测试与 E2E 基础设施
tests/e2e/provider_proxy_test.go, tests/e2e/proxy_setup_test.go, tests/e2e/playwright/provider-proxy-route.spec.ts, tests/e2e/playwright/package.json
新增 Go E2E 与 Playwright 测试(含 mock 提供商后端),测试环境注册 project/provider proxy handlers,并新增 Playwright 脚本。

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Root as Root\ Handler
    participant ProviderPH as Provider\Proxy\Handler
    participant ProviderRepo as Provider\ Repository
    participant Proxy as Proxy\ Handler
    participant Models as Models\ Handler
    participant Router as Router
    participant Backend as Backend\ Service

    Client->>Root: HTTP /provider/123/v1/messages
    Root->>ProviderPH: delegate (isProviderProxyPath)
    ProviderPH->>ProviderPH: parseScopedPath -> id="123", apiPath="/v1/messages"
    ProviderPH->>ProviderRepo: GetByID(tenantID, 123)
    ProviderRepo-->>ProviderPH: provider
    ProviderPH->>ProviderPH: set header X-Maxx-Provider-ID, rewrite path
    alt apiPath == "/v1/models"
        ProviderPH->>Models: ServeHTTP(rewritten)
    else
        ProviderPH->>Proxy: ServeHTTP(rewritten)
        Proxy->>Proxy: ingress reads X-Maxx-Provider-ID -> flow.KeyProviderID
        Proxy->>Router: Match(ctx with ProviderID)
        Router-->>Proxy: matched provider-scoped route
        Proxy->>Backend: forward proxied request
        Backend-->>Proxy: response
    end
    Proxy-->>Client: proxied response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ymkiux
  • dreamhunter2333
  • whhjdi

Poem

🐰 我是小兔子,穿过代码林,
/provider/ 小径新,ID 在心中印。
路由分派稳,流里携带标识,
测试守护好,合并正当春。

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了 PR 的主要变更:添加供应商(提供商)范围的代理路由功能,允许请求通过 /provider/{id}/... 路径直接访问。

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/provider-proxy-scope

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.

@awsl233777
Copy link
Collaborator Author

Test results for provider-scoped proxy routing:

  • go test ./...
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9980 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

Playwright evidence (requests UI after hitting /provider/{id}/v1/messages):

provider-proxy-route

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/handler/project_proxy.go`:
- Around line 110-129: The isValidAPIPath whitelist is too permissive because it
uses strings.HasPrefix and will accept names like "/v1/messages-debug" or
"/v1/responsesXYZ"; update the logic in isValidAPIPath to accept either exact
matches (e.g., "/v1/models") or a prefix followed by "/" for subpaths (e.g.,
"/v1/messages" OR "/v1/messages/") so only intended endpoints pass; ensure this
aligns with how ServeHTTP dispatches to modelsHandler and proxyHandler so that
exact "/v1/models" routes to modelsHandler while "/v1/models/..." is allowed
only when intended.

In `@internal/handler/proxy.go`:
- Around line 186-193: The code silently falls back to providerID=0 when
X-Maxx-Provider-ID is present but invalid; update the handler around the
r.Header.Get("X-Maxx-Provider-ID")/strconv.ParseUint logic so that if the header
exists but ParseUint returns an error or the parsed pid == 0 you immediately
write a 400 response and return (stop processing) instead of continuing with
providerID=0; reference the providerID variable, the X-Maxx-Provider-ID header
access, and the strconv.ParseUint call to locate and change the behavior (apply
same check at the other occurrence noted).

In `@internal/router/router.go`:
- Line 117: providerID is read into providerID (ctx.ProviderID) but only applied
in the global-route branch; when useProjectRoutes is true and ctx.ProjectID != 0
the project-specific branch currently ignores provider scope and can match other
providers and block the global provider filter. Fix by adding providerID-aware
filtering in the project-route branch (the code that builds/matches project
routes when useProjectRoutes is true): if providerID != 0 ensure the route’s
provider parameter matches providerID (or skip that route) instead of returning
a match that belongs to a different provider; also adjust control flow so a
provider-mismatch in the project branch does not short-circuit the global
fallback (i.e., skip/continue rather than return), and apply the same providerID
check logic you use in the global branch to the project-route matching code
paths referenced by providerID, useProjectRoutes, and ctx.ProjectID.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26afddb3-c7ac-48ca-872f-e70e804e793f

📥 Commits

Reviewing files that changed from the base of the PR and between 1cfd130 and 324bae9.

📒 Files selected for processing (15)
  • cmd/maxx/main.go
  • internal/core/database.go
  • internal/executor/flow_state.go
  • internal/executor/middleware_ingress.go
  • internal/executor/middleware_route_match.go
  • internal/flow/keys.go
  • internal/handler/project_proxy.go
  • internal/handler/project_proxy_test.go
  • internal/handler/proxy.go
  • internal/handler/static.go
  • internal/router/router.go
  • tests/e2e/playwright/package.json
  • tests/e2e/playwright/provider-proxy-route.spec.ts
  • tests/e2e/provider_proxy_test.go
  • tests/e2e/proxy_setup_test.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). (2)
  • GitHub Check: e2e
  • GitHub Check: playwright
🔇 Additional comments (7)
internal/executor/flow_state.go (1)

22-22: 变更合理,作用域信息承载清晰。

为执行态补充 providerID 字段是必要且一致的扩展,便于后续路由匹配传递上下文。

internal/flow/keys.go (1)

13-13: 上下文键新增正确。

KeyProviderID 的引入与现有 flow key 体系保持一致,便于作用域信息在链路中传递。

internal/executor/middleware_route_match.go (1)

27-27: 路由匹配上下文透传完整。

在匹配上下文中补充 ProviderID 是本次功能的关键连接点,实现上简洁且位置正确。

internal/executor/middleware_ingress.go (1)

39-43: Ingress 状态注入实现正确。

flow -> execState 的 provider 透传逻辑清晰,和现有 projectID 注入方式一致。

tests/e2e/proxy_setup_test.go (1)

197-197: E2E 路由装配到位。

测试环境已正确接入作用域代理入口,能有效覆盖 /provider/{id}/.../project/{slug}/... 场景。

Also applies to: 220-221

tests/e2e/playwright/package.json (1)

14-14: 脚本补充准确。

新增命令可直接触发 provider 作用域的 Playwright 用例,便于 CI/本地定向回归。

internal/handler/project_proxy_test.go (1)

5-20: 单测覆盖点选择很好。

已覆盖 project/provider 两类 scoped path 的解析与判定主路径,能有效防止路由入口回归。

Also applies to: 22-37, 39-49

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/handler/project_proxy.go`:
- Around line 96-108: The function parseScopePath currently trims parts[0] only
for the emptiness check but then returns the untrimmed parts[0]; change it to
trim once and use the trimmed value everywhere: compute trimmed :=
strings.TrimSpace(parts[0]), use trimmed for the empty check and assign
scopeValue = trimmed before returning; keep apiPath logic the same and validate
with isValidAPIPath(apiPath) as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b251572f-9c02-43e3-8da9-89599e23cc99

📥 Commits

Reviewing files that changed from the base of the PR and between 324bae9 and 3483fc1.

📒 Files selected for processing (6)
  • internal/handler/project_proxy.go
  • internal/handler/project_proxy_test.go
  • internal/handler/proxy.go
  • internal/router/router.go
  • internal/router/router_test.go
  • tests/e2e/playwright/provider-proxy-route.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • internal/handler/project_proxy_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/playwright/provider-proxy-route.spec.ts
📜 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). (2)
  • GitHub Check: playwright
  • GitHub Check: e2e
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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 (8)
internal/handler/proxy.go (2)

186-197: 修复已实现:现在正确拒绝无效的 X-Maxx-Provider-ID。

代码现在在 header 存在但解析失败或值为 0 时返回 400 错误,避免了静默降级到未作用域路由的问题。这解决了之前 review 中提出的安全隐患。


224-225: LGTM!

Provider ID 正确地在验证后存入 flow context,与 project ID 的处理方式一致。

internal/router/router_test.go (2)

9-21: 测试覆盖完善。

TestRouteMatchesProviderScope 正确覆盖了三种关键场景:providerID=0 的通配匹配、精确 ID 匹配、以及 ID 不匹配的拒绝情况。


23-44: LGTM!

TestRouteMatchesBaseScope 完整测试了 tenant、client type 匹配及 IsEnabled 禁用场景,覆盖了基础作用域过滤的核心逻辑。

internal/router/router.go (2)

144-161: 修复已实现:项目路由分支现在正确应用 provider 作用域过滤。

Line 155 的 routeMatchesProviderScope(route, providerID) 检查确保了 /provider/{id}/... 请求在项目路由分支中也会按 provider 过滤,不再会命中其他 provider 的路由。这解决了之前 review 中提出的跨 provider 匹配问题。


246-261: LGTM! 辅助函数设计清晰。

routeMatchesBaseScoperouteMatchesProviderScope 的抽取提高了代码可读性和可维护性,逻辑正确且边界条件处理得当(如 tenantID > 0 时才做租户匹配,providerID == 0 时通配所有 provider)。

internal/handler/project_proxy.go (2)

110-122: 修复已实现:白名单校验现在更严格。

matchesEndpointPath 使用 path == endpoint || strings.HasPrefix(path, endpoint+"/") 的逻辑,正确防止了 /v1/messages-debug 等相邻路径绕过白名单。这解决了之前 review 中提出的安全隐患。


57-70: LGTM! Provider 作用域路由实现正确。

Provider 分支正确验证了 ID 格式(非法或为 0 返回 400),通过 providerRepo.GetByID 确认 provider 存在性,并设置 X-Maxx-Provider-ID header 传递给下游 proxy handler。

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 3483fc1 to c7bb9a7 Compare March 22, 2026 05:05
@awsl233777
Copy link
Collaborator Author

Refactored the current PR to keep provider scope independent from project scope.

What changed:

  • split /provider/{id}/... into a dedicated ProviderProxyHandler
  • narrowed ProjectProxyHandler back to /project/{slug}/... only
  • updated combined/static routing and server wiring to dispatch project/provider scopes separately
  • changed router matching so provider-scoped requests do not reuse project custom-route selection and instead use provider-filtered global routes only
  • added regression coverage for the separated handler and router flow

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...

Force-pushed updated branch head: c7bb9a7

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/handler/provider_proxy_test.go (1)

5-29: LGTM! 单元测试覆盖了基本场景。

测试验证了 provider-scoped 路径解析和检测的核心功能。建议后续可考虑补充边界情况测试,如空 provider ID(/provider//v1/messages)、无效 API 路径等。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handler/provider_proxy_test.go` around lines 5 - 29, Add unit tests
covering edge cases for provider-scoped path parsing and detection: extend or
add tests that call ProviderProxyHandler.parseScopedPath (same scope as
TestParseProviderScopedPath) with an empty provider ID (e.g.,
"/provider//v1/messages") and with invalid/malformed API paths to assert parse
failure (ok == false) and expected zero-values; likewise add cases to
isProviderProxyPath (same scope as TestIsProviderProxyPath) verifying it returns
false for empty provider IDs and malformed paths. Ensure tests reference
ProviderProxyHandler.parseScopedPath and isProviderProxyPath and assert both
boolean results and returned providerID/apiPath values where applicable.
internal/handler/provider_proxy.go (1)

56-57: 请求热路径日志建议降噪。

Line 56 每次请求都 log.Printf 可能导致高流量下日志量过大;建议改为 debug 级、采样,或仅在异常路径记录。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handler/provider_proxy.go` around lines 56 - 57, 当前在
provider_proxy.go 中每次请求都会调用 log.Printf("[ProviderProxy] Routing request through
provider: %s (ID: %d)", provider.Name, provider.ID)
导致热路径日志噪声过大;请将该调用改为不在每个请求打印,例如:使用现有的结构化 logger 的 Debug/Trace 级别(如
logger.Debugf)或添加采样/速率限制封装,或者只在异常分支(provider 为 nil 或错误发生)记录 info/error
日志;定位并修改包含 log.Printf、provider.Name、provider.ID 的代码行,确保保留
r.Header.Set("X-Maxx-Provider-ID", ...) 行不变。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/handler/provider_proxy.go`:
- Around line 50-53: The current block conflates storage errors and a missing
provider by returning 404 for both; change the logic in ProviderProxy where
GetByID is called so that if err != nil you log the error (include err and
providerValue) and return a 500 (internal server error) via writeError, while
only when provider == nil you log “Provider not found” and return 404; update
the log.Printf and writeError calls accordingly to reference providerValue, err,
provider, and keep behavior for successful retrieval unchanged.

---

Nitpick comments:
In `@internal/handler/provider_proxy_test.go`:
- Around line 5-29: Add unit tests covering edge cases for provider-scoped path
parsing and detection: extend or add tests that call
ProviderProxyHandler.parseScopedPath (same scope as TestParseProviderScopedPath)
with an empty provider ID (e.g., "/provider//v1/messages") and with
invalid/malformed API paths to assert parse failure (ok == false) and expected
zero-values; likewise add cases to isProviderProxyPath (same scope as
TestIsProviderProxyPath) verifying it returns false for empty provider IDs and
malformed paths. Ensure tests reference ProviderProxyHandler.parseScopedPath and
isProviderProxyPath and assert both boolean results and returned
providerID/apiPath values where applicable.

In `@internal/handler/provider_proxy.go`:
- Around line 56-57: 当前在 provider_proxy.go 中每次请求都会调用 log.Printf("[ProviderProxy]
Routing request through provider: %s (ID: %d)", provider.Name, provider.ID)
导致热路径日志噪声过大;请将该调用改为不在每个请求打印,例如:使用现有的结构化 logger 的 Debug/Trace 级别(如
logger.Debugf)或添加采样/速率限制封装,或者只在异常分支(provider 为 nil 或错误发生)记录 info/error
日志;定位并修改包含 log.Printf、provider.Name、provider.ID 的代码行,确保保留
r.Header.Set("X-Maxx-Provider-ID", ...) 行不变。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d2a3640-4e0c-4077-8ab1-ebdd37a7ac72

📥 Commits

Reviewing files that changed from the base of the PR and between 3483fc1 and c7bb9a7.

📒 Files selected for processing (13)
  • cmd/maxx/main.go
  • internal/core/database.go
  • internal/core/server.go
  • internal/handler/project_proxy.go
  • internal/handler/project_proxy_test.go
  • internal/handler/provider_proxy.go
  • internal/handler/provider_proxy_test.go
  • internal/handler/proxy.go
  • internal/handler/static.go
  • internal/router/router.go
  • internal/router/router_test.go
  • tests/e2e/playwright/provider-proxy-route.spec.ts
  • tests/e2e/proxy_setup_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/handler/project_proxy_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/e2e/playwright/provider-proxy-route.spec.ts
  • internal/handler/static.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). (2)
  • GitHub Check: e2e
  • GitHub Check: playwright
🔇 Additional comments (16)
internal/handler/proxy.go (2)

186-197: LGTM! Provider ID 验证逻辑正确实现。

代码已按之前的审查建议修复:当 X-Maxx-Provider-ID header 存在但解析失败或值为 0 时,正确返回 400 错误并中止请求,避免了静默降级到未作用域路由的问题。


224-225: LGTM! Provider ID 正确传递到 flow context。

flow.KeyProviderIDflow.KeyProjectID 之后设置,与下游中间件(middleware_ingress.gomiddleware_route_match.go)的读取逻辑一致。

tests/e2e/proxy_setup_test.go (1)

197-198: LGTM! 测试环境正确配置了 provider-scoped 路由。

测试环境的路由配置与生产环境(cmd/maxx/main.gointernal/core/server.go)保持一致,确保 E2E 测试能够覆盖新增的 provider-scoped 代理功能。

Also applies to: 221-222

internal/core/server.go (1)

107-116: LGTM! 路由配置正确支持 provider-scoped 代理。

两种模式的路由配置逻辑清晰:

  • 静态服务启用时:CombinedHandler 根据路径前缀分发到对应处理器
  • 静态服务禁用时:显式注册 /project//provider/ 前缀路由
cmd/maxx/main.go (2)

379-379: LGTM! Provider proxy handler 正确集成到主程序。

providerProxyHandler 的创建和注册与 internal/core/database.go 保持一致,CombinedHandler 正确接收三个参数以支持 project 和 provider 两种作用域的代理。

Also applies to: 424-427


534-535: LGTM! 启动日志清晰记录了新增的端点。

日志格式与现有端点保持一致,便于用户了解 provider-scoped 代理的访问方式。

internal/core/database.go (1)

92-92: LGTM! ServerComponents 正确扩展以支持 ProviderProxyHandler。

ProviderProxyHandler 字段添加到 ServerComponents 结构体,并在 InitializeServerComponents 中使用 cachedProviderRepo 正确初始化,与 ProjectProxyHandler 使用 cachedProjectRepo 的模式一致。

Also applies to: 413-413, 436-436

internal/handler/project_proxy.go (3)

60-68: LGTM! 路径解析逻辑重构为共享函数。

parseScopedPath 现在委托给 parseScopePath,使得 project 和 provider 处理器可以共享相同的解析逻辑,减少代码重复。


70-86: LGTM! 空白字符处理已修复。

parseScopePath 现在使用 strings.TrimSpace 处理 scopeValue,确保带空格的 scope value(如 " my-project ")被正确清理,与之前的审查建议一致。


88-100: LGTM! API 路径白名单验证更加严格。

matchesEndpointPath 实现了精确匹配或前缀加 / 的子路径匹配,避免了 strings.HasPrefix 可能误匹配 /v1/messages-debug 等非法路径的问题。测试文件 project_proxy_test.go 中的 TestIsValidAPIPath_AllowsExactAndSubpathsOnly 验证了所有预期场景。

internal/router/router.go (4)

27-27: LGTM! MatchContext 正确扩展以支持 ProviderID。

ProviderID 字段添加到 MatchContext,并在 Match 方法中正确读取,与调用方(middleware_route_match.go)的传参一致。

Also applies to: 117-117


122-139: LGTM! Provider-scoped 请求正确绕过项目路由。

providerID != 0 时,useProjectRoutesfalse,确保 provider-scoped 请求直接使用全局路由,不会错误地匹配到其他项目的路由。这解决了之前审查中指出的问题。


145-161: LGTM! 项目路由过滤现在包含 provider scope 检查。

routeMatchesProviderScope 确保在查找项目路由时,如果请求带有 providerID,只有匹配该 provider 的路由才会被选中。


247-262: LGTM! 辅助函数提高了代码可读性。

routeMatchesBaseScoperouteMatchesProviderScope 将重复的过滤逻辑抽取为独立函数,使主逻辑更清晰,也便于单元测试验证。

internal/router/router_test.go (2)

22-57: 基础匹配测试覆盖完整,回归保护到位。

这两组用例把 provider/base scope 的成功与失败路径都覆盖到了,粒度和断言都比较扎实。


59-99: Provider scope 跳过 project route 的行为验证很关键,写得很好。

该用例精准锁定本次变更目标,能有效防止后续路由回归。

@awsl233777
Copy link
Collaborator Author

Adding the missing Playwright evidence screenshot via a free image host.

Playwright UI evidence (requests page after hitting /provider/{id}/v1/messages):

This screenshot comes from the provider-scoped Playwright test artifact for the current PR branch.

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from c7bb9a7 to 0a97489 Compare March 22, 2026 10:42
@awsl233777
Copy link
Collaborator Author

Updated the PR again with a narrower rewrite.

What changed in this revision:

  • kept the existing ProjectProxyHandler structure in place instead of rewriting it into a project-only handler
  • added a separate ProviderProxyHandler for /provider/{id}/...
  • updated server/static dispatch to send provider-scoped requests to the new handler directly
  • preserved the provider-scope safety fixes:
    • stricter scoped API path validation
    • trimmed scoped path values
    • invalid X-Maxx-Provider-ID rejection
    • provider-scoped router matching that bypasses project custom-route selection

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...

Current branch head: 0a97489

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

49-54: ⚠️ Potential issue | 🟠 Major

存储层错误与"未找到"不应同一处理。

当前代码将 GetByID 返回的真实错误(如数据库连接失败)与 provider == nil 的情况都返回 404,这会误导客户端重试策略并增加排障难度。

建议分开处理:err != nil 时返回 500 并记录错误详情,provider == nil 时返回 404。

建议修复
  tenantID := maxxctx.GetTenantID(r.Context())
  provider, err := h.providerRepo.GetByID(tenantID, providerID)
- if err != nil || provider == nil {
- 	log.Printf("[ProviderProxy] Provider not found for id: %s", providerValue)
- 	writeError(w, http.StatusNotFound, "provider not found")
- 	return
- }
+ if err != nil {
+ 	log.Printf("[ProviderProxy] failed to load provider tenant=%d id=%d: %v", tenantID, providerID, err)
+ 	writeError(w, http.StatusInternalServerError, "internal server error")
+ 	return
+ }
+ if provider == nil {
+ 	log.Printf("[ProviderProxy] provider not found tenant=%d id=%d", tenantID, providerID)
+ 	writeError(w, http.StatusNotFound, "provider not found")
+ 	return
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handler/provider_proxy.go` around lines 49 - 54, 当前逻辑把
h.providerRepo.GetByID 返回的真实错误与 provider==nil 混为一谈并统一返回 404;请分开处理:如果 err !=
nil,则记录具体错误(使用 log.Printf 或更合适的记录器并包含 err)并通过 writeError 返回
http.StatusInternalServerError,只有在 err == nil 且 provider == nil 时才返回
http.StatusNotFound 并返回 "provider not found";调整处理顺序并保留
provider、providerID/tenantID 和 h.providerRepo.GetByID 的上下文以便定位修改点。
🧹 Nitpick comments (2)
internal/handler/project_proxy.go (1)

13-20: /provider/ 分支收回到 ProviderProxyHandler 即可。

internal/core/server.go 在 Line 113-114 已经把 /provider/ 直接注册到 ProviderProxyHandlerinternal/handler/static.go 在 Line 318-320 也会先把这类请求分流过去,所以这里的 provider 解析和查库分支不会经过生产流量。继续把它留在 ProjectProxyHandler 里,会让 NewProjectProxyHandler 平白多背一个 providerRepo 依赖,并且让 internal/handler/project_proxy_test.go 继续覆盖一条不可达路径。建议把这个类型重新收窄到 /project/{slug}/...,provider 逻辑和对应测试只保留在 ProviderProxyHandler

Also applies to: 22-34, 37-72, 84-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handler/project_proxy.go` around lines 13 - 20, The
ProjectProxyHandler currently contains provider-specific routing and a
providerRepo dependency; remove the provider branch and any provider lookup
logic from ProjectProxyHandler (and its constructor NewProjectProxyHandler) so
the struct only holds proxyHandler, modelsHandler and projectRepo and only
handles /project/{slug}/... routes; move the provider-specific code and tests
into ProviderProxyHandler (and its tests like project_proxy_test.go should be
split/moved so provider paths are covered only by ProviderProxyHandler tests)
and remove providerRepo usage/references from ProjectProxyHandler and its tests
to avoid the unreachable branch.
internal/handler/provider_proxy.go (1)

57-57: TrimSpace 对整数转换结果是多余的。

itoa(provider.ID) 将整数转为字符串,结果不会包含空白字符,TrimSpace 调用可移除。

可选简化
- r.Header.Set("X-Maxx-Provider-ID", strings.TrimSpace(itoa(provider.ID)))
+ r.Header.Set("X-Maxx-Provider-ID", itoa(provider.ID))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handler/provider_proxy.go` at line 57, The code calls
strings.TrimSpace on the result of itoa(provider.ID), which is unnecessary
because integer-to-string conversion yields no surrounding whitespace; remove
the strings.TrimSpace wrapper and set the header directly using the
integer-to-string result (i.e., replace strings.TrimSpace(itoa(provider.ID))
with the direct itoa(provider.ID) / strconv.Itoa(provider.ID) value) in the
r.Header.Set call in provider_proxy.go.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/router/router_test.go`:
- Around line 59-70: 测试使用了全局单例 cooldown.Default(), 导致与包内其它测试互相污染(provider 7 /
"claude" 可能被标记为 cooldown);请改为为该用例注入一个独立的 cooldown 管理器到 Router(不要使用
cooldown.Default()),或者在测试前后显式清理全局管理器中 provider 7 和 "claude" 的 cooldown 状态
以保证隔离;参考符号:Router.Match、cooldown.Default()、provider id 7、"claude",将构造中的
cooldown: cooldown.Default() 替换为新建的 manager 实例(或在 setup/teardown 调用清理方法)以修复偶发的
ErrNoRoutes。

In `@internal/router/router.go`:
- Around line 122-126: When handling provider-scoped requests, don't let project
routing rules influence selection: update the logic around
useProjectRoutes/getRoutingStrategy so that when providerID (ctx.ProviderID) !=
0 you force the project argument passed into getRoutingStrategy to 0 (global)
instead of the original projectID; e.g., compute a projectIDForStrategy variable
(0 for provider scope, original projectID otherwise) and use that in the calls
to getRoutingStrategy (also adjust the same behavior at the other occurrence
around the second getRoutingStrategy call referenced in the comment).

In `@tests/e2e/playwright/provider-proxy-route.spec.ts`:
- Around line 15-65: The startMockClaudeServer Promise executor only uses
resolve and throws inside the server.listen callback which won't reject the
Promise; modify the executor to accept both (resolve, reject), add
server.on('error', (err) => reject(err)) to propagate listen errors, and replace
the throw in the server.listen callback (inside startMockClaudeServer) with
reject(new Error(...)) so the caller receives a proper rejected Promise when the
mock server fails to bind.

---

Duplicate comments:
In `@internal/handler/provider_proxy.go`:
- Around line 49-54: 当前逻辑把 h.providerRepo.GetByID 返回的真实错误与 provider==nil
混为一谈并统一返回 404;请分开处理:如果 err != nil,则记录具体错误(使用 log.Printf 或更合适的记录器并包含 err)并通过
writeError 返回 http.StatusInternalServerError,只有在 err == nil 且 provider == nil
时才返回 http.StatusNotFound 并返回 "provider not found";调整处理顺序并保留
provider、providerID/tenantID 和 h.providerRepo.GetByID 的上下文以便定位修改点。

---

Nitpick comments:
In `@internal/handler/project_proxy.go`:
- Around line 13-20: The ProjectProxyHandler currently contains
provider-specific routing and a providerRepo dependency; remove the provider
branch and any provider lookup logic from ProjectProxyHandler (and its
constructor NewProjectProxyHandler) so the struct only holds proxyHandler,
modelsHandler and projectRepo and only handles /project/{slug}/... routes; move
the provider-specific code and tests into ProviderProxyHandler (and its tests
like project_proxy_test.go should be split/moved so provider paths are covered
only by ProviderProxyHandler tests) and remove providerRepo usage/references
from ProjectProxyHandler and its tests to avoid the unreachable branch.

In `@internal/handler/provider_proxy.go`:
- Line 57: The code calls strings.TrimSpace on the result of itoa(provider.ID),
which is unnecessary because integer-to-string conversion yields no surrounding
whitespace; remove the strings.TrimSpace wrapper and set the header directly
using the integer-to-string result (i.e., replace
strings.TrimSpace(itoa(provider.ID)) with the direct itoa(provider.ID) /
strconv.Itoa(provider.ID) value) in the r.Header.Set call in provider_proxy.go.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bcbe2f24-43f0-4560-841b-ddffc114d880

📥 Commits

Reviewing files that changed from the base of the PR and between c7bb9a7 and 0a97489.

📒 Files selected for processing (13)
  • cmd/maxx/main.go
  • internal/core/database.go
  • internal/core/server.go
  • internal/handler/project_proxy.go
  • internal/handler/project_proxy_test.go
  • internal/handler/provider_proxy.go
  • internal/handler/provider_proxy_test.go
  • internal/handler/proxy.go
  • internal/handler/static.go
  • internal/router/router.go
  • internal/router/router_test.go
  • tests/e2e/playwright/provider-proxy-route.spec.ts
  • tests/e2e/proxy_setup_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/handler/provider_proxy_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/handler/proxy.go
  • internal/core/database.go
  • cmd/maxx/main.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). (2)
  • GitHub Check: e2e
  • GitHub Check: playwright
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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 (4)
internal/handler/provider_proxy.go (4)

1-11: LGTM!

包声明和导入正确,所有导入在代码中均有使用。


13-32: LGTM!

结构体定义和构造函数遵循标准的 Go 依赖注入模式,无线程安全问题。


34-46: LGTM!

路径解析和 provider ID 验证逻辑正确,对无效路径返回 404、无效 ID 返回 400 的状态码选择合理。


67-75: LGTM!

parseScopedPath 实现正确,复用了 project_proxy.go 中的共享 parseScopePath 辅助函数,保持了与项目范围解析一致的行为。

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 0a97489 to 4d739ff Compare March 22, 2026 11:04
@awsl233777
Copy link
Collaborator Author

Updated the branch again to narrow the rewrite further.

This revision keeps ProjectProxyHandler itself unchanged and moves the provider-specific work into a dedicated ProviderProxyHandler plus separate wiring.

What remains in the diff:

  • new ProviderProxyHandler for /provider/{id}/...
  • server/static wiring to dispatch /provider/... directly to the new handler
  • provider-scoped router behavior that bypasses project custom-route selection
  • invalid X-Maxx-Provider-ID rejection in proxy ingress
  • provider-focused regression coverage (handler/router/E2E/Playwright)

Project-side churn was reduced to the routing detection expectation in project_proxy_test.go so the combined handler now reflects the separate /project/... vs /provider/... dispatch.

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...

Current branch head: 4d739ff

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 4d739ff to 0a02e95 Compare March 22, 2026 11:06
@awsl233777
Copy link
Collaborator Author

Pushed another narrowed update.

This revision keeps the original projectProxyHandler := handler.NewProjectProxyHandler(...) call unchanged and only adds a separate providerProxyHandler := handler.NewProviderProxyHandler(...) path for /provider/{id}/....

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...

Current branch head: 0a02e95

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

48-53: ⚠️ Potential issue | 🟠 Major

把仓储异常和“未找到”分开处理。

Line 49-53 现在把所有 GetByID 错误都映射成 404。当前仓储实现会用 domain.ErrNotFound 表示不存在,但其它数据库/缓存异常也会落到这里并被伪装成 provider not found,这会误导客户端和排障。这里应该只把 not found 返回 404,其它错误返回 500。

🛠️ 建议修复
 import (
+	"errors"
 	"log"
 	"net/http"
 	"strconv"
 	"strings"
 
 	maxxctx "github.com/awsl-project/maxx/internal/context"
+	"github.com/awsl-project/maxx/internal/domain"
 	"github.com/awsl-project/maxx/internal/repository"
 )
@@
 	tenantID := maxxctx.GetTenantID(r.Context())
 	provider, err := h.providerRepo.GetByID(tenantID, providerID)
-	if err != nil || provider == nil {
-		log.Printf("[ProviderProxy] Provider not found for id: %s", providerValue)
-		writeError(w, http.StatusNotFound, "provider not found")
-		return
-	}
+	if err != nil {
+		if errors.Is(err, domain.ErrNotFound) {
+			log.Printf("[ProviderProxy] Provider not found tenant=%d id=%d", tenantID, providerID)
+			writeError(w, http.StatusNotFound, "provider not found")
+			return
+		}
+		log.Printf("[ProviderProxy] Failed to load provider tenant=%d id=%d: %v", tenantID, providerID, err)
+		writeError(w, http.StatusInternalServerError, "internal server error")
+		return
+	}
+	if provider == nil {
+		log.Printf("[ProviderProxy] Provider not found tenant=%d id=%d", tenantID, providerID)
+		writeError(w, http.StatusNotFound, "provider not found")
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handler/provider_proxy.go` around lines 48 - 53, Separate repository
"not found" from other errors when calling providerRepo.GetByID: after calling
h.providerRepo.GetByID(tenantID, providerID) check if err != nil and use
errors.Is(err, domain.ErrNotFound) (or compare domain.ErrNotFound) to return
writeError(w, http.StatusNotFound, "provider not found"); for any other non-nil
err log the error and return writeError(w, http.StatusInternalServerError,
"internal server error"); keep the existing provider == nil check only for the
not-found path if you prefer, but do not map all errors to 404.
internal/router/router_test.go (1)

59-70: ⚠️ Potential issue | 🟡 Minor

不要在这个用例里复用全局 cooldown 单例。

Line 69 把 cooldown.Default() 注入给 Router 后,这个测试会和同包其它测试共享 cooldown 状态;只要别的用例给 provider 7 / claude 留下标记,这里就会偶发返回 ErrNoRoutes。建议改成独立 manager,或者至少在用例前后显式清理对应 cooldown。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/router/router_test.go` around lines 59 - 70, Test is injecting the
global cooldown singleton via cooldown.Default() into Router which causes test
flakiness due to shared state (provider id 7 / provider "claude" cooldown marks)
and sporadic ErrNoRoutes; replace the global singleton with a fresh cooldown
manager instance (or instantiate a new cooldown manager and inject that) for
this test's Router (the Router struct created in
TestMatch_ProviderScopeSkipsProjectRoutes) or explicitly clear the provider
7/“claude” cooldown before/after the test so the test does not share state with
other tests.
tests/e2e/playwright/provider-proxy-route.spec.ts (1)

15-65: ⚠️ Potential issue | 🟡 Minor

把 mock server 的启动失败显式 reject 给调用方。

Line 16 这里只有 resolve,而 Line 61 的 throw 发生在 server.listen() 的回调里,不会把外层 Promise 变成 rejected;同时这里也没监听 error 事件。端口绑定失败时,这个 helper 很容易变成未捕获异常或悬空 Promise。

🛠️ 建议修复
 function startMockClaudeServer(): Promise<{ server: http.Server; port: number }> {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     const server = http.createServer((req, res) => {
       if (req.method === 'POST' && req.url?.startsWith('/v1/messages')) {
         let body = '';
         req.on('data', (chunk) => {
           body += chunk;
@@
       res.writeHead(404, { 'Content-Type': 'application/json' });
       res.end(JSON.stringify({ error: 'not found' }));
     });
 
+    server.once('error', reject);
     server.listen(0, '127.0.0.1', () => {
       const address = server.address();
       if (!address || typeof address === 'string') {
-        throw new Error('Failed to determine mock server port');
+        reject(new Error('Failed to determine mock server port'));
+        return;
       }
       resolve({ server, port: address.port });
     });
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/playwright/provider-proxy-route.spec.ts` around lines 15 - 65, The
startMockClaudeServer Promise currently only calls resolve and throws inside the
server.listen callback, which doesn't reject the outer Promise or handle listen
errors; change the Promise executor to (resolve, reject), attach
server.on('error', reject) before calling server.listen, and replace the throw
in the server.listen callback with reject(new Error(...)) so port binding
failures are propagated to callers; ensure any event listeners are cleaned up on
resolve/reject if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/handler/provider_proxy.go`:
- Around line 48-53: Separate repository "not found" from other errors when
calling providerRepo.GetByID: after calling h.providerRepo.GetByID(tenantID,
providerID) check if err != nil and use errors.Is(err, domain.ErrNotFound) (or
compare domain.ErrNotFound) to return writeError(w, http.StatusNotFound,
"provider not found"); for any other non-nil err log the error and return
writeError(w, http.StatusInternalServerError, "internal server error"); keep the
existing provider == nil check only for the not-found path if you prefer, but do
not map all errors to 404.

In `@internal/router/router_test.go`:
- Around line 59-70: Test is injecting the global cooldown singleton via
cooldown.Default() into Router which causes test flakiness due to shared state
(provider id 7 / provider "claude" cooldown marks) and sporadic ErrNoRoutes;
replace the global singleton with a fresh cooldown manager instance (or
instantiate a new cooldown manager and inject that) for this test's Router (the
Router struct created in TestMatch_ProviderScopeSkipsProjectRoutes) or
explicitly clear the provider 7/“claude” cooldown before/after the test so the
test does not share state with other tests.

In `@tests/e2e/playwright/provider-proxy-route.spec.ts`:
- Around line 15-65: The startMockClaudeServer Promise currently only calls
resolve and throws inside the server.listen callback, which doesn't reject the
outer Promise or handle listen errors; change the Promise executor to (resolve,
reject), attach server.on('error', reject) before calling server.listen, and
replace the throw in the server.listen callback with reject(new Error(...)) so
port binding failures are propagated to callers; ensure any event listeners are
cleaned up on resolve/reject if needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6e7ac052-bf9e-4caf-8a82-3f38023a5ae2

📥 Commits

Reviewing files that changed from the base of the PR and between 0a97489 and 4d739ff.

📒 Files selected for processing (12)
  • cmd/maxx/main.go
  • internal/core/database.go
  • internal/core/server.go
  • internal/handler/project_proxy_test.go
  • internal/handler/provider_proxy.go
  • internal/handler/provider_proxy_test.go
  • internal/handler/proxy.go
  • internal/handler/static.go
  • internal/router/router.go
  • internal/router/router_test.go
  • tests/e2e/playwright/provider-proxy-route.spec.ts
  • tests/e2e/proxy_setup_test.go
✅ Files skipped from review due to trivial changes (4)
  • internal/core/server.go
  • internal/handler/project_proxy_test.go
  • internal/router/router.go
  • tests/e2e/proxy_setup_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/handler/proxy.go
  • internal/core/database.go
  • internal/handler/provider_proxy_test.go
  • internal/handler/static.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). (2)
  • GitHub Check: e2e
  • GitHub Check: playwright
🔇 Additional comments (3)
cmd/maxx/main.go (1)

378-379: Provider 作用域入口的接线是完整的。

ProviderProxyHandler 的构造、CombinedHandler 注入和启动日志三处都同步补齐了,和 /provider/... 的分流设计保持一致。

Also applies to: 426-427, 535-535

internal/router/router_test.go (1)

22-57: 基础匹配规则的回归点补得很到位。

routeMatchesProviderScoperouteMatchesBaseScope 的正反例都固定下来了,后续再改匹配逻辑时回归会很直接。

internal/handler/provider_proxy.go (1)

67-101: 路径解析和白名单收口得比较稳。

这里先裁剪 scope,再只放行明确支持的 API 前缀,能避免 /provider/{id}/... 意外落到任意代理路径。

@awsl233777
Copy link
Collaborator Author

Pushed an update that keeps the original projectProxyHandler := handler.NewProjectProxyHandler(...) block untouched and moves the provider handler construction out of that block.

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...

Current branch head: c811709

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch 2 times, most recently from c811709 to 0117639 Compare March 22, 2026 15:57
@awsl233777
Copy link
Collaborator Author

Updated the provider handler to follow the existing project handler style more closely.

What changed in this revision:

  • kept ProjectProxyHandler on its original constructor/flow
  • rewrote ProviderProxyHandler to mirror the project handler structure more closely (same overall field layout, ServeHTTP flow, path parser shape, and forwarding pattern)
  • kept the provider-specific safety fixes and provider-scoped routing behavior intact

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...

Current branch head: e0415d3

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 0117639 to e0415d3 Compare March 22, 2026 17:06
cmd/maxx/main.go Outdated
combinedHandler := handler.NewCombinedHandler(projectProxyHandler, staticHandler)
combinedHandler := handler.NewCombinedHandler(
projectProxyHandler,
handler.NewProviderProxyHandler(proxyHandler, modelsHandler, cachedProviderRepo),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

why this not same as project handler

log.Printf("[Server] Static file serving enabled")
} else {
mux.Handle("/", components.ProjectProxyHandler)
mux.Handle("/project/", components.ProjectProxyHandler)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

why change / to /projject

@awsl233777
Copy link
Collaborator Author

Addressed the latest review points.

Updates in this revision:

  • made main.go wiring read more like the project handler flow by constructing providerProxyHandler as a named sibling before NewCombinedHandler(...)
  • stopped changing the Wails/no-static branch to /project/ + /provider/ handlers directly and routed it back through / using the combined handler with a not-found fallback
  • made ProviderProxyHandler return 500 for provider repository errors and 404 only for an actual missing provider
  • forced provider-scoped routing strategy lookup to use the global strategy (projectID=0)
  • isolated the router test from the global cooldown singleton
  • fixed Playwright mock server startup error propagation

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...
  • pnpm --dir tests/e2e/playwright exec playwright test provider-proxy-route.spec.ts --list

Current branch head: a5a5451

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from e0415d3 to a5a5451 Compare March 22, 2026 17:40
cmd/maxx/main.go Outdated
// Serve static files (Web UI) with project proxy support - must be last (default route)
staticHandler := handler.NewStaticHandler()
combinedHandler := handler.NewCombinedHandler(projectProxyHandler, staticHandler)
providerProxyHandler := handler.NewProviderProxyHandler(proxyHandler, modelsHandler, cachedProviderRepo)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

移动到和 project 相同的初始化位置

}

var providerID uint64
if providerIDStr := r.Header.Get("X-Maxx-Provider-ID"); providerIDStr != "" {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

不需要支持这个

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch 2 times, most recently from 9487b55 to c0ab7fd Compare March 23, 2026 05:41
@awsl233777
Copy link
Collaborator Author

Updated the PR with only the two required follow-up changes from the latest review.

What changed:

  • moved providerProxyHandler := handler.NewProviderProxyHandler(...) up to the same initialization area as projectProxyHandler
  • removed the extra X-Maxx-Provider-ID support path in ProxyHandler and now pass provider scope internally from ProviderProxyHandler to ProxyHandler via request context instead

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9880 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

Current branch head: c0ab7fd

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from c0ab7fd to 704a401 Compare March 23, 2026 06:01
@awsl233777
Copy link
Collaborator Author

Updated the PR again to keep only the required provider-scope changes.

What changed in this revision:

  • removed provider-specific branching from internal/handler/static.go
  • registered /provider/ explicitly at the route layer in main.go and internal/core/server.go
  • rewrote provider route matching in internal/router/router.go as its own provider-scoped path instead of mixing it into the project-routing control flow
  • kept the internal provider-scope propagation from ProviderProxyHandler into ProxyHandler

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9880 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

Current branch head: 704a401

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 704a401 to 4515f09 Compare March 23, 2026 06:29
@awsl233777
Copy link
Collaborator Author

Updated the branch again to minimize changes and keep the new behavior parallel to the existing project path.

What changed in this revision:

  • kept the existing project/static routing path intact
  • registered provider scope explicitly as a parallel /provider/ route in the server wiring
  • preserved provider scope by passing it from ProviderProxyHandler into ProxyHandler / executor flow
  • handled provider-scoped route selection in a dedicated router path instead of mixing it into the existing project-routing branch
  • reduced unrelated churn in internal/core/database.go and internal/router/router.go

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9880 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

Current branch head: 4515f09

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 4515f09 to 2240e3d Compare March 23, 2026 07:26
@awsl233777
Copy link
Collaborator Author

Pushed another narrowed revision focused on parallel provider logic with minimal churn to existing paths.

What changed in this revision:

  • kept the existing project/static route flow intact
  • kept provider scope as an explicit parallel /provider/ route
  • reduced internal/router/router.go to a small early provider-scoped branch while leaving the existing project-routing path in place
  • reduced noise in internal/core/database.go to only the required provider handler wiring

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9880 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

Current branch head: 2240e3d

@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 2240e3d to 4532ced Compare March 23, 2026 15:24
@awsl233777
Copy link
Collaborator Author

Updated the branch again with a narrower provider-scoped pass.

This revision keeps provider forwarding as a fixed one-to-one path:

  • provider scope stays on request context instead of adding a wider flow-state chain
  • provider-scoped route selection reads that scope where needed
  • provider-scoped dispatch disables retries and cooldown handling

Validation:

  • go test ./internal/handler ./internal/router ./tests/e2e/...
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9880 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

Current branch head: 4532ced

What:
- rewrite provider-scoped proxy requests to use a dedicated direct-forward path instead of the generic router/executor route selection flow
- wire provider proxy construction to the route and proxy-request repositories needed for one-to-one forwarding and request recording
- drop the previous provider-scoped context/router/executor changes so the diff only keeps the required handler and test wiring

Why:
- provider-scoped requests should forward to the specified provider one-to-one without retry or generic route reuse
- keeping the logic parallel to the existing project proxy entrypoint minimizes unrelated surface area in this PR

Tests:
- go test ./internal/handler ./internal/router ./tests/e2e/... (pass)
- go build -o maxx ./cmd/maxx (pass)
- MAXX_ADMIN_PASSWORD=test123 ./maxx -addr :9880 + MAXX_E2E_BASE_URL=http://127.0.0.1:9880 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route (pass)
@awsl233777 awsl233777 force-pushed the feat/provider-proxy-scope branch from 4532ced to 0577dd6 Compare March 23, 2026 16:17
@awsl233777
Copy link
Collaborator Author

已按 review 意见把 provider-scoped proxy 收敛成 provider 一对一直转发,不再复用通用 router/executor 选路重试链路,相关的 context/router/executor 改动也已从这条 PR 里移除。

本轮保留的核心改动:

  • ProviderProxyHandler 改为独立直转发到指定 provider
  • 仅补充必要的 handler 接线与 request record 持久化
  • 保留 provider-scoped 的 handler/e2e/Playwright 覆盖

本地验证:

  • go test ./internal/handler ./internal/router ./tests/e2e/...
  • go build -o maxx ./cmd/maxx
  • MAXX_ADMIN_PASSWORD=test123 ./maxx -addr :9880
  • MAXX_E2E_BASE_URL=http://127.0.0.1:9880 MAXX_E2E_USERNAME=admin MAXX_E2E_PASSWORD=test123 pnpm --dir tests/e2e/playwright test:provider-proxy-route

都已通过。

@ymkiux ymkiux merged commit e63c47c into main Mar 23, 2026
5 checks passed
@ymkiux ymkiux deleted the feat/provider-proxy-scope branch March 23, 2026 17:01
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