Skip to content

fix: restore tenant-scoped oauth broadcasts and align websocket tests#442

Closed
awsl233777 wants to merge 7 commits intomainfrom
fix/multi-tenant-isolation
Closed

fix: restore tenant-scoped oauth broadcasts and align websocket tests#442
awsl233777 wants to merge 7 commits intomainfrom
fix/multi-tenant-isolation

Conversation

@awsl233777
Copy link
Collaborator

@awsl233777 awsl233777 commented Mar 20, 2026

Summary

  • add tenant tracking back to Claude/Codex OAuth sessions
  • restore tenant-scoped OAuth result broadcasting for Claude and Codex flows
  • expose CountByTenant on the upstream-attempt repository interface for the admin cost recalculation path
  • align websocket tests with the queued broadcast message type used by the hub

Why

Recent multi-tenant changes left the OAuth session structs and repository interface out of sync with their callers. That broke builds in internal/service and caused OAuth result broadcasts to lose tenant scoping. The websocket tests were also still targeting the old broadcast channel shape.

Testing

  • go test ./...

Summary by CodeRabbit

发行说明

  • 新功能

    • 事件广播新增按租户发送接口,支持租户范围的实时消息推送。
    • WebSocket 记录每个连接的租户/用户/角色并按租户过滤广播;新增认证中间件以拒绝未授权连接。
    • OAuth 流程在会话创建与完成时关联租户并做租户范围广播。
    • 统计与成本计算增加按租户的计数与流式处理接口。
  • 测试

    • 更新单元/集成测试以匹配广播消息与队列结构变更;调整端到端测试超时与阈值。

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 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

在 OAuth 会话中加入 TenantID 并将 CreateSession 签名改为接收 tenantID;CompleteSession 在删除会话前读取 TenantID 并在 TenantID != 0 时通过租户范围广播(BroadcastMessageToTenant),其余为全局广播。为此扩展了广播接口、Wails 实现、WebSocket Hub、仓储接口及相关测试与处理路径。

Changes

Cohort / File(s) Summary
OAuth 服务租户隔离
internal/adapter/provider/claude/service.go, internal/adapter/provider/codex/service.go
OAuthSession 中添加 TenantID uint64 字段;CreateSession(state, tenantID uint64) 改签名并保存 TenantID;CompleteSession 在删除前读取 TenantID,根据其是否为 0 调用 BroadcastMessageToTenantBroadcastMessage
WebSocket 与消息队列
internal/handler/websocket.go, internal/handler/websocket_test.go
将连接布尔标记换为每连接元数据(tenantID/userID/role);广播通道类型改为 queuedWSMessage(包含 tenantID/scoped/defaultOnly);run() 根据这些字段过滤发送目标;新增认证中间件挂载与基于 access_token 的认证流程;测试适配为使用 queuedWSMessage 包装。
广播接口与实现
internal/event/broadcaster.go, internal/event/wails_broadcaster_desktop.go, internal/event/wails_broadcaster_http.go
扩展 Broadcaster 接口,新增 BroadcastMessageToTenant(tenantID uint64, messageType string, data interface{})NopBroadcasterWailsBroadcaster(桌面与 HTTP)添加对应实现与委托逻辑。
仓储接口与实现(租户感知)
internal/repository/interfaces.go, internal/repository/sqlite/proxy_upstream_attempt.go
ProxyUpstreamAttemptRepository 添加 tenant-scoped 方法 CountByTenantStreamForCostCalcByTenantCountAll/StreamForCostCalc 委托到 tenant-all 常量;SQLite 实现使用 tenantScope(...) 进行查询过滤并新增对应方法。
Handler 与命令行入口
internal/handler/claude.go, internal/handler/codex.go, cmd/maxx/main.go
OAuth 启动方法签名改为接收 tenantID 并从请求上下文提取传入;Codex HTTP handler 在 tenantID==0 时返回 401;cmd/maxx 将 WebSocket Hub 注入认证中间件(当存在时)。
测试与工具链调整
internal/handler/websocket_test.go, tests/e2e/playwright/requests-table-alignment.spec.ts
WebSocket 测试改为使用 queuedWSMessage;Playwright 测试延长超时并降低预期请求计数阈值。

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant OAuthManager
    participant SessionStore as Repository
    participant Broadcaster
    participant Wails as WailsBroadcaster

    Client->>OAuthManager: CreateSession(state, tenantID)
    OAuthManager->>SessionStore: persist OAuthSession{State, TenantID, ...}
    OAuthManager-->>Client: return session + PKCE

    Client->>OAuthManager: CompleteSession(state, result)
    OAuthManager->>SessionStore: lookup session by state -> returns TenantID
    OAuthManager->>SessionStore: delete session
    alt TenantID != 0
        OAuthManager->>Broadcaster: BroadcastMessageToTenant(tenantID, type, data)
    else TenantID == 0
        OAuthManager->>Broadcaster: BroadcastMessage(type, data)
    end
    Broadcaster->>Wails: emitWailsEvent(type, data)
    Wails->>Client: deliver websocket/event update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ymkiux
  • whhjdi

诗文

🐰 轻跳庆新枝

会话添上租户印,广播各归各厅厅;

WebSocket 换新袍,仓储计数更分明;

兔儿拍手又蹦跳,消息有家不再停。

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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的两个主要变更:恢复租户隔离的OAuth广播功能和对WebSocket测试的对齐调整。标题简洁明确,符合变更集的核心内容。

✏️ 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 fix/multi-tenant-isolation
📝 Coding Plan
  • Generate coding plan for human review comments

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.

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: 5

🧹 Nitpick comments (1)
internal/adapter/provider/codex/service.go (1)

86-93: 建议:Claude 和 Codex 的 OAuth 管理器存在明显的代码重复。

OAuthSession 结构体、CreateSessionGetSessionCompleteSession 方法在两个服务中几乎完全相同。可以考虑将通用的 OAuth 会话管理逻辑抽取到一个共享的基础包中,仅保留特定于提供商的部分(如事件类型名称 "claude_oauth_result" vs "codex_oauth_result")。

此重构不阻塞当前 PR,可作为后续改进。

Also applies to: 140-158, 181-204

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

In `@internal/adapter/provider/codex/service.go` around lines 86 - 93, The
OAuthSession struct and the methods CreateSession, GetSession, and
CompleteSession are duplicated between the Claude and Codex services; extract
the shared OAuth session management into a common package (e.g., internal/oauth
or internal/adapter/oauth) containing the OAuthSession type and generic
CreateSession/GetSession/CompleteSession implementations, and have the
provider-specific services call into that package, keeping only
provider-specific differences (such as the event type string
"claude_oauth_result" vs "codex_oauth_result") in their own code; update
references in the Codex service (OAuthSession, CreateSession, GetSession,
CompleteSession) to use the new shared package API.
🤖 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/adapter/provider/claude/service.go`:
- Line 117: CreateSession's signature now requires tenantID uint64 but callers
still use the old signature; update each call site (notably the invocations in
internal/handler/claude.go and internal/handler/codex.go) to fetch the tenant ID
from context like in internal/handler/antigravity.go: use
maxxctx.GetTenantID(ctx), default to domain.DefaultTenantID when zero, then pass
that tenantID into OAuthManager.CreateSession(state, tenantID). Ensure you
import or reference maxxctx/domain as needed and propagate any context variable
(ctx) already available at the call site.
- Around line 169-173: 接口 event.Broadcaster 缺少 BroadcastMessageToTenant 方法,且
OAuthManager.CreateSession 签名已变更导致调用不匹配;为修复:在 internal/event/broadcaster.go 的
event.Broadcaster 接口中添加 BroadcastMessageToTenant(tenantID uint64, event string,
payload interface{}) 方法签名(与现有 BroadcastMessage 对称),并在
internal/adapter/provider/claude/service.go 和
internal/adapter/provider/codex/service.go 保留对 BroadcastMessageToTenant
的调用;然后更新调用 CreateSession 的处理器 internal/handler/claude.go 和
internal/handler/codex.go,将缺失的 tenantID 参数一并传入(参考
internal/handler/antigravity.go 的传参方式和获取 tenantID 的逻辑)。

In `@internal/adapter/provider/codex/service.go`:
- Around line 198-202: The build fails because service.go calls
BroadcastMessageToTenant on m.broadcaster but event.Broadcaster lacks that
method; add a BroadcastMessageToTenant(ctx context.Context, tenantID int64,
topic string, payload interface{}) (or matching signature used in other
services) to the event.Broadcaster interface and implement it in the concrete
broadcaster type(s) (the same change applied for Claude in
internal/adapter/provider/claude/service.go should be mirrored here); update any
call sites (m.broadcaster.BroadcastMessageToTenant and tests) to use the new
signature so the call in internal/adapter/provider/codex/service.go compiles.
- Line 141: internal/handler/codex.go still calls
h.oauthManager.CreateSession(state) but the signature changed to
CreateSession(state string, tenantID uint64) returning (*OAuthSession,
*PKCEChallenge, error); update that call to obtain tenantID via
maxxctx.GetTenantID(ctx) and default to domain.DefaultTenantID when zero, then
call h.oauthManager.CreateSession(state, tenantID) and capture the returned
values (e.g. _, pkce, err) following the pattern used in antigravity.go.

In `@internal/repository/interfaces.go`:
- Around line 160-165: The SQLite implementation is missing the
ProxyUpstreamAttemptRepository methods CountByTenant and
StreamForCostCalcByTenant; implement CountByTenant to run a COUNT query against
the attempts table filtered by tenant_id (respect TenantIDAll semantics used
elsewhere) and return (int64, error), and implement StreamForCostCalcByTenant to
mirror the existing StreamForCostCalc logic but add tenant_id filtering: fetch
rows in batches (use LIMIT/OFFSET or keyset pagination consistent with other
repository methods), map each row to domain.AttemptCostData, invoke the provided
callback with the batch, and stop/return immediately if the callback returns an
error; ensure method signatures exactly match CountByTenant and
StreamForCostCalcByTenant on ProxyUpstreamAttemptRepository and reuse existing
DB helpers and error handling patterns used in proxy_upstream_attempt.go.

---

Nitpick comments:
In `@internal/adapter/provider/codex/service.go`:
- Around line 86-93: The OAuthSession struct and the methods CreateSession,
GetSession, and CompleteSession are duplicated between the Claude and Codex
services; extract the shared OAuth session management into a common package
(e.g., internal/oauth or internal/adapter/oauth) containing the OAuthSession
type and generic CreateSession/GetSession/CompleteSession implementations, and
have the provider-specific services call into that package, keeping only
provider-specific differences (such as the event type string
"claude_oauth_result" vs "codex_oauth_result") in their own code; update
references in the Codex service (OAuthSession, CreateSession, GetSession,
CompleteSession) to use the new shared package API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dd56afc0-17de-48d9-8895-c28c6deeda3e

📥 Commits

Reviewing files that changed from the base of the PR and between 1165cc5 and 94afb07.

📒 Files selected for processing (4)
  • internal/adapter/provider/claude/service.go
  • internal/adapter/provider/codex/service.go
  • internal/handler/websocket_test.go
  • internal/repository/interfaces.go
📜 Review details
🧰 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.
🪛 GitHub Actions: E2E Playwright Tests
internal/adapter/provider/claude/service.go

[error] 170-170: go build failed: m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant)

🪛 GitHub Actions: E2E Tests
internal/adapter/provider/claude/service.go

[error] 170-170: go test failed: event.Broadcaster has no field or method BroadcastMessageToTenant (m.broadcaster.BroadcastMessageToTenant undefined).

🪛 GitHub Actions: PR Checks
internal/adapter/provider/claude/service.go

[error] 170-170: go vet failed: m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant).

🪛 GitHub Check: Backend Checks
internal/adapter/provider/claude/service.go

[failure] 170-170:
m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant)

internal/adapter/provider/codex/service.go

[failure] 199-199:
m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant)

🪛 GitHub Check: e2e
internal/adapter/provider/claude/service.go

[failure] 170-170:
m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant)

internal/adapter/provider/codex/service.go

[failure] 199-199:
m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant)

🪛 GitHub Check: playwright
internal/adapter/provider/claude/service.go

[failure] 170-170:
m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant)

internal/adapter/provider/codex/service.go

[failure] 199-199:
m.broadcaster.BroadcastMessageToTenant undefined (type event.Broadcaster has no field or method BroadcastMessageToTenant)

🔇 Additional comments (3)
internal/handler/websocket_test.go (3)

13-50: 测试更新正确对齐了 hub 的内部消息类型。

测试代码正确地将 broadcast 通道类型从 WSMessage 更新为 queuedWSMessage,并通过 queued.message 解包访问底层消息。快照隔离验证逻辑保持完整。


52-108: 其余测试函数的更新保持一致性。

BroadcastProxyUpstreamAttemptBroadcastDrop 测试同样正确地适配了新的消息封装结构。


110-185: 死锁测试和 BroadcastMessage 测试更新完整。

通道填充和消息断言都正确使用了 queuedWSMessage 类型。

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: 2

🤖 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/event/wails_broadcaster_desktop.go`:
- Around line 76-82: The WebSocketHub currently lacks an implementation of
BroadcastMessageToTenant which causes a runtime panic when
WailsBroadcaster.BroadcastMessageToTenant calls
w.inner.BroadcastMessageToTenant; implement BroadcastMessageToTenant on the
WebSocketHub type (matching the Broadcaster signature) and delegate to its
existing broadcast mechanisms so it supports tenant-scoped messages;
additionally, modify WailsBroadcaster.emitWailsEvent call from
BroadcastMessageToTenant (or the data passed into it) to include tenantID in the
payload so desktop clients receive tenant context, or if the desktop app is
truly single-tenant, add a clear comment on
WailsBroadcaster.BroadcastMessageToTenant stating tenantID is ignored and why.

In `@internal/event/wails_broadcaster_http.go`:
- Around line 55-60: The WebSocketHub type lacks the
BroadcastMessageToTenant(tenantID uint64, messageType string, data interface{})
method required by the Broadcaster interface; add this method to WebSocketHub
(same signature as WailsBroadcaster.BroadcastMessageToTenant) and implement it
to send the message only to connections belonging to the given tenantID (e.g.,
mirror the logic used in existing broadcast methods but filter by tenant),
ensuring thread-safety (use the hub's mutex or dispatch channel) and making it a
no-op when no tenant connections exist to avoid runtime panic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec3a4a38-6ccc-48cf-8fd9-85237b1d328f

📥 Commits

Reviewing files that changed from the base of the PR and between 94afb07 and 432465b.

📒 Files selected for processing (3)
  • internal/event/broadcaster.go
  • internal/event/wails_broadcaster_desktop.go
  • internal/event/wails_broadcaster_http.go
✅ Files skipped from review due to trivial changes (1)
  • internal/event/broadcaster.go
📜 Review details
🧰 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.

@ymkiux
Copy link
Contributor

ymkiux commented Mar 20, 2026

@coderabbitai re-review !Stop making breaking changes, do a proper review!

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll do a thorough audit of the breaking changes in this PR right away.


🏁 Script executed:

#!/bin/bash

echo "=== All callers of CreateSession (claude) ==="
rg -n "CreateSession" --type go -C 3

echo ""
echo "=== ProxyUpstreamAttemptRepository implementations ==="
rg -n "ProxyUpstreamAttemptRepository" --type go -C 3

echo ""
echo "=== Broadcaster interface implementations (BroadcastMessage method) ==="
rg -n "BroadcastMessage\b" --type go -C 3

echo ""
echo "=== BroadcastMessageToTenant all references ==="
rg -n "BroadcastMessageToTenant" --type go -C 3

echo ""
echo "=== CountByTenant all references ==="
rg -n "CountByTenant" --type go -C 3

echo ""
echo "=== StreamForCostCalcByTenant all references ==="
rg -n "StreamForCostCalcByTenant" --type go -C 3

echo ""
echo "=== queuedWSMessage type definition ==="
rg -n "queuedWSMessage" --type go -C 5

echo ""
echo "=== WebSocketHub broadcast channel definition ==="
rg -n "broadcast" --type go -C 3 internal/handler/websocket.go 2>/dev/null || rg -n "broadcast" --type go -C 3 internal/handler/

echo ""
echo "=== wails_broadcaster_http.go full content ==="
cat internal/event/wails_broadcaster_http.go 2>/dev/null || find . -name "wails_broadcaster_http.go" -exec cat {} \;

echo ""
echo "=== wails_broadcaster_desktop.go full content ==="
cat internal/event/wails_broadcaster_desktop.go 2>/dev/null || find . -name "wails_broadcaster_desktop.go" -exec cat {} \;

echo ""
echo "=== broadcaster.go full content ==="
cat internal/event/broadcaster.go 2>/dev/null || find . -name "broadcaster.go" -exec cat {} \;

echo ""
echo "=== repository interfaces.go full content ==="
cat internal/repository/interfaces.go 2>/dev/null || find . -name "interfaces.go" -path "*/repository/*" -exec cat {} \;

echo ""
echo "=== All struct types implementing ProxyUpstreamAttemptRepository ==="
rg -n "func.*CountAll\(\)" --type go -C 3
rg -n "func.*StreamForCostCalc\b" --type go -C 3

echo ""
echo "=== websocket_test.go full content ==="
cat internal/handler/websocket_test.go 2>/dev/null || find . -name "websocket_test.go" -exec cat {} \;

Length of output: 80004


@ymkiux: Apologies for any prior comments that missed the mark. Here is the result of a thorough audit covering every surface area this PR touches:


1. CreateSession signature change (Claude & Codex) — ✅ No broken callers

All three call sites already pass tenantID:

Caller Location
h.oauthManager.CreateSession(state, tenantID) internal/handler/claude.go:111
h.oauthManager.CreateSession(state, tenantID) internal/handler/codex.go:157
h.oauthManager.CreateSession(state, tenantID) internal/handler/antigravity.go:186

No unpatched callers exist in the codebase.


2. ProxyUpstreamAttemptRepository interface additions — ✅ Fully implemented

Both new methods (CountByTenant, StreamForCostCalcByTenant) are already implemented in the only concrete struct (internal/repository/sqlite/ProxyUpstreamAttemptRepository). Critically, the existing methods delegate correctly to preserve backward compatibility:

  • CountAll()CountByTenant(domain.TenantIDAll)
  • StreamForCostCalc()StreamForCostCalcByTenant(domain.TenantIDAll, ...)

E2E tests use the real sqlite implementation, so no mocks need updating.


3. Broadcaster interface addition (BroadcastMessageToTenant) — ✅ All implementors updated

Implementation Status
NopBroadcaster Implemented (no-op) ✅
WailsBroadcaster (HTTP build) Delegates to inner only — correct, no Wails events in HTTP mode ✅
WailsBroadcaster (Desktop build) Delegates to inner + calls emitWailsEvent — consistent with BroadcastMessage desktop behavior ✅
WebSocketHub Delegates to broadcastJSONMessage with scoped=true

No other Broadcaster implementations exist in the codebase.


4. CompleteSession sequencing (Claude & Codex) — ✅ Correct

The session's TenantID is captured before the session is deleted, preventing tenant scope loss:

lookup session → capture tenantID → delete session → broadcast with tenantID

5. WebSocket test alignment — ✅ Correct

queuedWSMessage is the actual type of WebSocketHub.broadcast (chan queuedWSMessage). All five test functions are correctly updated to construct with make(chan queuedWSMessage, ...) and extract queued.message before asserting on Type and Data.


Summary: This PR is internally consistent and complete. All interface additions are fully implemented, all call-site signatures match, and there are no unpatched callers, missing implementations, or behavioral regressions. The changes correctly restore the tenant-scoped OAuth broadcasting and repository interface that were inadvertently removed in a prior refactor.


🧠 Learnings used
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.

@ymkiux ymkiux enabled auto-merge (squash) March 20, 2026 10:36
@ymkiux
Copy link
Contributor

ymkiux commented Mar 20, 2026

@coderabbitai 你怎么一直等待中 认真点

@ymkiux ymkiux disabled auto-merge March 20, 2026 13:25
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Tip

For best results, initiate chat on the files or code changes.

@ymkiux:抱歉让您等待!我在上方已完成了详细的重新审查,以下是核心结论:

  1. CreateSession 签名变更(Claude & Codex) — ✅ 无破坏性变更,所有调用方(claude.go, codex.go, antigravity.go)已正确传入 tenantID
  2. ProxyUpstreamAttemptRepository 接口新增方法 — ✅ CountByTenantStreamForCostCalcByTenant 均已在 sqlite 实现中完整实现,旧方法委托新方法,向后兼容。
  3. Broadcaster 接口新增 BroadcastMessageToTenant — ✅ 所有实现(NopBroadcaster、HTTP/Desktop WailsBroadcasterWebSocketHub)均已更新。
  4. CompleteSession 时序 — ✅ 先捕获 TenantID,再删除 session,再广播,逻辑正确。
  5. WebSocket 测试对齐 — ✅ 所有测试已正确使用 queuedWSMessage 类型。

综合结论:此 PR 内部一致、完整,无破坏性变更或回归风险。 如有其他具体疑虑,请告知!


🧠 Learnings used
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.

@awsl233777
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 2

🧹 Nitpick comments (1)
internal/repository/sqlite/proxy_upstream_attempt.go (1)

93-98: 建议复用 tenantScope 辅助函数以保持租户过滤逻辑一致。

CountByTenant() 已经使用 tenantScope() 来处理租户过滤,但 StreamForCostCalcByTenant() 在第 93-98 行手动重新实现了相同的逻辑。这会导致维护问题:当 tenantScope 扩展以支持共享租户、NULL 处理或额外的谓词时,流式扫描不会自动更新,导致计数和实际处理的数据集出现差异。

建议改为:

♻️ 建议改法
-		query := r.db.gorm.Table("proxy_upstream_attempts").
-			Select("id, proxy_request_id, response_model, mapped_model, request_model, input_token_count, output_token_count, cache_read_count, cache_write_count, cache_5m_write_count, cache_1h_write_count, cost").
-			Where("id > ?", lastID)
-		if tenantID != domain.TenantIDAll {
-			query = query.Where("tenant_id = ?", tenantID)
-		}
+		query := tenantScope(
+			r.db.gorm.Table("proxy_upstream_attempts").
+				Select("id, proxy_request_id, response_model, mapped_model, request_model, input_token_count, output_token_count, cache_read_count, cache_write_count, cache_5m_write_count, cache_1h_write_count, cost").
+				Where("id > ?", lastID),
+			tenantID,
+		)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/sqlite/proxy_upstream_attempt.go` around lines 93 - 98,
StreamForCostCalcByTenant currently reimplements tenant filtering (the query
built on r.db.gorm.Table("proxy_upstream_attempts") with Where("tenant_id =
?")), which duplicates CountByTenant's use of tenantScope; change
StreamForCostCalcByTenant to apply the existing tenantScope helper instead of
the manual Where so the same tenant predicates (including shared/NULL handling)
are reused—locate StreamForCostCalcByTenant and replace the conditional tenant
Where with a call to tenantScope on the GORM query (same pattern used in
CountByTenant) so future tenant logic changes automatically apply to both
counting and streaming.
🤖 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/codex.go`:
- Around line 210-211: The /api/codex/oauth/start handler is missing tenant
validation: call to maxxctx.GetTenantID(r.Context()) in codex.go followed by
h.StartOAuth(tenantID) proceeds even when tenantID == 0 which allows session
creation to fall back to global broadcasts (see CompleteSession,
BroadcastMessage vs BroadcastMessageToTenant); add an explicit check in the
handler after GetTenantID to reject tenantID == 0 by returning an HTTP error
(unauthorized/bad request) and avoid calling StartOAuth, ensuring multi-tenant
isolation is preserved.

In `@internal/handler/websocket.go`:
- Around line 286-289: The current BroadcastLog implementation incorrectly
hardcodes domain.DefaultTenantID and forces default-only visibility; revert
BroadcastLog to its original non-tenantized behavior (do not force default
tenant) by calling h.broadcastJSONMessage with the tenant parameter that
represents instance-wide delivery (e.g. empty string or the previous sentinel
used for instance-wide logs) and keep the existing flags, and instead add a new
explicit tenant-scoped API such as BroadcastLogForTenant(tenantID string,
message string) or BroadcastLogDefaultTenant(message string) that calls
h.broadcastJSONMessage(domain.DefaultTenantID, ...), so callers in
internal/event/* can continue working without breaking behavior while consumers
can opt into tenant-scoped logs.

---

Nitpick comments:
In `@internal/repository/sqlite/proxy_upstream_attempt.go`:
- Around line 93-98: StreamForCostCalcByTenant currently reimplements tenant
filtering (the query built on r.db.gorm.Table("proxy_upstream_attempts") with
Where("tenant_id = ?")), which duplicates CountByTenant's use of tenantScope;
change StreamForCostCalcByTenant to apply the existing tenantScope helper
instead of the manual Where so the same tenant predicates (including shared/NULL
handling) are reused—locate StreamForCostCalcByTenant and replace the
conditional tenant Where with a call to tenantScope on the GORM query (same
pattern used in CountByTenant) so future tenant logic changes automatically
apply to both counting and streaming.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0f9e073-7186-48a5-a454-b1b0a7918318

📥 Commits

Reviewing files that changed from the base of the PR and between 432465b and 640d074.

📒 Files selected for processing (5)
  • internal/handler/claude.go
  • internal/handler/codex.go
  • internal/handler/websocket.go
  • internal/repository/sqlite/proxy_upstream_attempt.go
  • tests/e2e/playwright/requests-table-alignment.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/e2e/playwright/requests-table-alignment.spec.ts
📜 Review details
🧰 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 (7)
internal/handler/codex.go (1)

149-157: 租户参数透传到 OAuth 会话创建链路这部分是正确的。

Line 149 与 Line 157 将 tenantID 明确传入 StartOAuth/CreateSession,与本次多租户隔离修复目标一致。

internal/repository/sqlite/proxy_upstream_attempt.go (3)

55-66: 这里的兼容层收敛得很好。

CountAll() 统一走 CountByTenant(domain.TenantIDAll) 后,新旧入口只维护一套计数逻辑,后续再调整租户规则时更不容易漏改旧路径。


70-72: 全量扫描入口的委托也合理。

StreamForCostCalc() 现在只是租户版入口的一个特例,避免了两套批处理实现继续分叉。


100-139: 批处理语义保持住了。

引入租户过滤后,id > lastID 的游标推进、按批回调以及 callback 出错即短路返回都还在,原有成本重算流程的执行模型没有被顺手改坏。

internal/handler/claude.go (1)

105-111: Claude OAuth 的租户上下文已经贯穿到会话创建路径。

这里把 tenantID 从 HTTP 入口一路传到 CreateSession,能和 internal/adapter/provider/claude/service.go 里的 OAuthSession.TenantID / CompleteSession 广播逻辑对齐,修复目标是准确的。

Also applies to: 161-162

internal/handler/websocket.go (2)

39-44: 把作用域元数据和消息一起入队这个方案是对的。

tenantID/scoped/defaultOnly 统一在队列层携带,再由 run() 集中筛选收件人,能避免某个广播分支漏掉租户过滤,也和这次更新后的 websocket 测试断言模型保持一致。

Also applies to: 75-93, 201-252, 254-283


127-153: 先鉴权再 Upgrade 的顺序正确。

未授权请求可以直接返回普通 HTTP 错误,不会先建立连接再清理,也避免了失败连接短暂进入 clients 映射。

Also applies to: 155-170

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/websocket.go`:
- Around line 132-134: The current WebSocket auth branch returns an admin
wsClient when auth == nil which grants admin privileges by default; change the
behavior to deny by returning wsClient{} and false instead of an admin client,
and add a controlled opt-in for "local-only" anonymous admin access (e.g., check
a new localOnly bool or config flag before returning elevated wsClient) so
anonymous admin is only allowed when explicitly enabled; also ensure the
SetAuthMiddleware setter is actually used during server initialization (wire
authMiddleware into the WebSocket connection logic where wsClient is created) so
deployments without an auth middleware fail closed rather than falling back to
admin.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f718b46f-acc1-4b29-a903-5cadc58dded0

📥 Commits

Reviewing files that changed from the base of the PR and between 640d074 and 88a6c9d.

📒 Files selected for processing (3)
  • internal/handler/codex.go
  • internal/handler/websocket.go
  • internal/repository/sqlite/proxy_upstream_attempt.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/handler/codex.go
  • internal/repository/sqlite/proxy_upstream_attempt.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: 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 (2)
internal/handler/websocket.go (2)

75-113: 广播发送循环的并发处理实现是稳妥的

这里先在读锁下复制目标连接,再在锁外写 socket,最后集中清理失效连接,避免了“持锁网络 I/O”和 map 并发删除问题,方向正确。


286-289: BroadcastLog 当前实现保持全局广播语义,兼容性正确

BroadcastLogbroadcastJSONMessage(0, false, false, ...),没有把日志流静默收窄到默认租户,行为与既有接口语义一致。

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 `@cmd/maxx/main.go`:
- Line 378: The call to handler.NewProjectProxyHandler passes four arguments but
the function signature for NewProjectProxyHandler (returning
*ProjectProxyHandler) accepts only three: proxyHandler *ProxyHandler,
modelsHandler *ModelsHandler, projectRepo repository.ProjectRepository; remove
the extra tokenAuthMiddleware argument from the call site (the variable named
tokenAuthMiddleware) or alternatively update the NewProjectProxyHandler function
signature to accept and wire that middleware if intended — adjust either the
call or the function definition (NewProjectProxyHandler / ProjectProxyHandler)
so the parameter lists match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cd06a775-d27d-4b33-a46c-b1686b488c5e

📥 Commits

Reviewing files that changed from the base of the PR and between 88a6c9d and 4d522e1.

📒 Files selected for processing (3)
  • cmd/maxx/main.go
  • internal/handler/websocket.go
  • tests/e2e/playwright/requests-table-alignment.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/e2e/playwright/requests-table-alignment.spec.ts
  • internal/handler/websocket.go
📜 Review details
🧰 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.
🪛 GitHub Actions: E2E Playwright Tests
cmd/maxx/main.go

[error] 378-378: go build failed: too many arguments in call to handler.NewProjectProxyHandler

🪛 GitHub Actions: PR Checks
cmd/maxx/main.go

[error] 378-378: go vet failed: too many arguments in call to handler.NewProjectProxyHandler

🪛 GitHub Check: playwright
cmd/maxx/main.go

[failure] 378-378:
too many arguments in call to handler.NewProjectProxyHandler

🔇 Additional comments (1)
cmd/maxx/main.go (1)

420-422: LGTM!

WebSocket 认证中间件的条件注入逻辑正确:仅当 authMiddleware != nil(即启用了管理员认证)时才调用 wsHub.SetAuthMiddleware,且在注册 /ws 端点(第 425 行)之前完成设置,确保后续连接能正确应用认证逻辑。

// Use already-created cached project repository for project proxy handler
modelsHandler := handler.NewModelsHandler(responseModelRepo, cachedProviderRepo, cachedModelMappingRepo)
projectProxyHandler := handler.NewProjectProxyHandler(proxyHandler, modelsHandler, cachedProjectRepo)
projectProxyHandler := handler.NewProjectProxyHandler(proxyHandler, modelsHandler, cachedProjectRepo, tokenAuthMiddleware)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 查找 NewProjectProxyHandler 的函数定义
ast-grep --pattern 'func NewProjectProxyHandler($$$) $_ {
  $$$
}'

Repository: awsl-project/maxx

Length of output: 752


修复构建失败:handler.NewProjectProxyHandler 参数过多

第 378 行调用 handler.NewProjectProxyHandler 时传递了 4 个参数,但该函数只接受 3 个参数。函数签名位于 internal/handler/project_proxy.go 第 21-25 行,定义为:

func NewProjectProxyHandler(
	proxyHandler *ProxyHandler,
	modelsHandler *ModelsHandler,
	projectRepo repository.ProjectRepository,
) *ProjectProxyHandler

移除第四个参数 tokenAuthMiddleware,或根据需要更新函数签名以接受该参数。

🧰 Tools
🪛 GitHub Actions: E2E Playwright Tests

[error] 378-378: go build failed: too many arguments in call to handler.NewProjectProxyHandler

🪛 GitHub Actions: PR Checks

[error] 378-378: go vet failed: too many arguments in call to handler.NewProjectProxyHandler

🪛 GitHub Check: playwright

[failure] 378-378:
too many arguments in call to handler.NewProjectProxyHandler

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

In `@cmd/maxx/main.go` at line 378, The call to handler.NewProjectProxyHandler
passes four arguments but the function signature for NewProjectProxyHandler
(returning *ProjectProxyHandler) accepts only three: proxyHandler *ProxyHandler,
modelsHandler *ModelsHandler, projectRepo repository.ProjectRepository; remove
the extra tokenAuthMiddleware argument from the call site (the variable named
tokenAuthMiddleware) or alternatively update the NewProjectProxyHandler function
signature to accept and wire that middleware if intended — adjust either the
call or the function definition (NewProjectProxyHandler / ProjectProxyHandler)
so the parameter lists match.

@ymkiux ymkiux closed this Mar 20, 2026
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