Skip to content

feat(console): 补齐控制台工作区归档入口并对齐 Anthropic 级联吊销语义 - #96

Open
Postroggy wants to merge 10 commits into
superduck-ai:mainfrom
Postroggy:feat/console-workspace-archive
Open

feat(console): 补齐控制台工作区归档入口并对齐 Anthropic 级联吊销语义#96
Postroggy wants to merge 10 commits into
superduck-ai:mainfrom
Postroggy:feat/console-workspace-archive

Conversation

@Postroggy

@Postroggy Postroggy commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

问题背景

Closes #95

当前控制台(Console)工作区只支持创建,没有归档入口;且后端归档只盖 workspaces.archived_at,不级联吊销该工作区下的 console_api_keys,归档后这些 key 仍可查询和使用,与 Anthropic 官方「归档立即吊销所有 API key」的语义不一致。#95 给出了代码证据与官方文档依据。

方案

面向 Console 用户补一个归档端点,在单个事务内软删除工作区并级联吊销其 API key,并加防自锁与默认工作区保护:

  • DB 层 internal/db/console_workspaces.go:新增 ArchiveConsoleWorkspace(orgUUID, workspaceID),单事务内 update workspaces set archived_at = coalesce(...) 并级联 update console_api_keys set archived_at = coalesce(...),组织隔离 + 幂等(coalesce 保留原 archived_at)。
  • platformapi 层 internal/platformapi/console_workspaces.gohandleArchiveConsoleWorkspace 做禁归档校验——default 工作区与当前会话绑定的工作区返回 409(防自锁:归档当前工作区会立即吊销会话绑定的 API key);未知 / 跨组织工作区返回 404
  • 路由:归档路由挂在已有的 RegisterConsoleOrganizationWorkspaceRoutesplatform_backend_routes.go),与 GET /workspaces 同处。
  • 前端 ArchiveWorkspaceDialog:破坏性二次确认;WorkspacesSettingsPage Actions 列改为 DropdownMenu,default 与当前激活工作区的归档项禁用并给出切换提示;归档成功后从工作区列表移除。

改动文件

  • internal/db/console_workspaces.go(新增)
  • internal/platformapi/console_workspaces.go(新增)
  • internal/platformapi/platform_backend_routes.go(+1 行归档路由)
  • tests/console_workspace_archive_api_test.go(新增,覆盖 200 / 404 / 409 default / 409 current / 级联吊销 / 幂等)
  • web/src/shared/workspaces/ArchiveWorkspaceDialog.tsx(新增)
  • web/src/shared/workspaces/{api,context,WorkspaceProvider}.ts(x)
  • web/src/features/settings/WorkspacesSettingsPage.tsx
  • web/src/shared/i18n/messages/{en,zh-CN}.json
  • docs/design/be/console-workspace-archive.md(状态机 / 级联时序 / 防自锁 / API 契约 / 测试)

验证

  • go test ./... -count=1 通过(含归档子测试)
  • just lint / just dead-code / just duplicates / just complexity 通过
  • 前端 bun run build(tsc + vite)、just web-format-checkbun run lint:naminglint:complexity 通过

说明

待维护者 review,谢谢。

Summary by CodeRabbit

  • New Features

    • Added an “Archive workspace” flow for console workspaces with confirmation, archiving-in-progress state, and inline error handling.
    • New endpoint supports safe archiving with clear 409 (default/current workspace) and 404 (not found/other organization) responses.
    • Archiving removes the workspace from the list, archives related console API keys, and revokes access; repeat archiving is idempotent.
    • Updated the workspaces table to use a per-row overflow menu; archive is disabled with explanatory hints. UI text added in English and Simplified Chinese.
  • Tests

    • Added end-to-end coverage for success, key revocation, idempotency, isolation, and 409/404 behavior.
  • Documentation

    • Documented the archive/soft-delete contract and cascade behavior.

Soft-delete a console workspace by setting archived_at and, in the same
idempotent transaction, cascade the archive to every console API key
scoped to it, mirroring Anthropic workspace semantics. Refuse to archive
the default workspace and the caller's current workspace (409) to avoid
self-lockout; unknown or cross-org workspaces return 404. Mount the
route on the existing RegisterConsoleOrganizationWorkspaceRoutes.

The front-end adds an archive action with a confirmation dialog and
disables it for the default and active workspaces.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Postroggy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a56b6440-0a40-44eb-aaa6-a18c5419024d

📥 Commits

Reviewing files that changed from the base of the PR and between 2a5eac1 and ff469c8.

📒 Files selected for processing (2)
  • docs/design/be/console-workspace-archive.md
  • internal/db/console_workspaces.go
📝 Walkthrough

Walkthrough

新增 Console workspace 归档能力:后端提供事务级联归档接口并保护默认/当前工作区,前端加入确认对话框和操作菜单,测试覆盖错误响应、API key 吊销、幂等性与组织隔离。

Changes

工作区归档

Layer / File(s) Summary
归档契约与事务存储
docs/design/be/console-workspace-archive.md, internal/db/console_workspaces.go
定义 archived_at 归档语义、状态约束和 API 契约;数据库事务幂等更新工作区并级联归档关联 API key。
归档 API 与验证
internal/platformapi/console_workspaces.go, internal/platformapi/platform_backend_routes.go, tests/console_workspace_archive_api_test.go
新增归档路由和处理器,返回 200/404/409,并测试默认工作区、未知工作区、级联吊销、幂等性及组织隔离。
前端归档入口与状态更新
web/src/shared/workspaces/*, web/src/features/settings/WorkspacesSettingsPage.tsx, web/src/features/settings/WorkspaceApiKeysPage.tsx, web/src/shared/i18n/messages/*.json
新增归档 API 与上下文操作、CSRF 传递、确认对话框、工作区操作菜单及中英文文案;归档成功后从查询缓存移除工作区。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WorkspacesSettingsPage
  participant ArchiveWorkspaceDialog
  participant archiveConsoleWorkspace
  participant ConsoleWorkspaceAPI
  participant Database
  User->>WorkspacesSettingsPage: Select archive action
  WorkspacesSettingsPage->>ArchiveWorkspaceDialog: Open confirmation
  User->>ArchiveWorkspaceDialog: Confirm archive
  ArchiveWorkspaceDialog->>archiveConsoleWorkspace: Submit workspace ID
  archiveConsoleWorkspace->>ConsoleWorkspaceAPI: POST archive endpoint
  ConsoleWorkspaceAPI->>Database: Archive workspace and related keys
  Database-->>ConsoleWorkspaceAPI: Archived workspace
  ConsoleWorkspaceAPI-->>archiveConsoleWorkspace: Workspace response
  archiveConsoleWorkspace-->>ArchiveWorkspaceDialog: Resolve
  ArchiveWorkspaceDialog-->>WorkspacesSettingsPage: Close dialog
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: console workspace archiving with Anthropic-style cascade revocation.
Linked Issues check ✅ Passed The PR matches #95: it adds console archive API, cascades key revocation, blocks default/current workspaces, returns 404/409, and adds UI/tests/docs.
Out of Scope Changes check ✅ Passed The changes stay within the archive feature scope; the CSRF and API tweaks support the new console archive flow.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/design/be/console-workspace-archive.md (1)

79-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a language for the fenced code block.

To satisfy markdown lint rules (MD040) and improve syntax highlighting, specify a language like http or text for this code block.

♻️ Proposed fix
-```
+```http
 POST /api/console/organizations/{orgUuid}/workspaces/{workspaceId}/archive
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In @docs/design/be/console-workspace-archive.md around lines 79 - 81, Update the
fenced code block containing the archive POST endpoint to specify an appropriate
language such as http, while preserving the endpoint content unchanged.


</details>

<!-- cr-comment:v1:dcfd7ef8f192cac41def58d8 -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>tests/console_workspace_archive_api_test.go (1)</summary><blockquote>

`90-99`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Move the failure scenario to precede the success scenarios.**

As per coding guidelines, "测试组织顺序应先写失败场景,再写成功场景" (test organization order should place failure scenarios first, followed by success scenarios). The "isolated by organization" test is a failure scenario (expects a 404) and should be positioned before the "archive succeeds and cascades to api keys" test.




<details>
<summary>♻️ Proposed fix</summary>

Move this test block to line 44, right before the `archive succeeds and cascades to api keys` test.

```go
	t.Run("isolated by organization", func(t *testing.T) {
		otherOrgID := seedArchiveOrganization(t, app, "org_archive_isolation_"+uniqueAdminSuffix())
		otherWS := seedArchiveTargetWorkspace(t, app, otherOrgID, "Other Org WS")
		resp := app.doPlatformConsole(t, http.MethodPost, base+"/workspaces/"+otherWS+"/archive", nil, cookies)
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("status = %d, want 404 (org isolation): %s", resp.StatusCode, readAll(t, resp.Body))
		}
	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/console_workspace_archive_api_test.go` around lines 90 - 99, Move the
“isolated by organization” subtest within the relevant test function so it runs
before “archive succeeds and cascades to api keys.” Keep the test body and its
404 assertion unchanged, preserving failure scenarios before success scenarios.

Source: Coding guidelines

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

Inline comments:
In `@internal/db/console_workspaces.go`:
- Around line 56-57: Update the workspace cascade query in the surrounding
function to filter console_api_keys.workspace_id using the workspace external
ID, reusing the function’s workspaceID argument or workspace.ExternalID rather
than workspace.UUID; keep the org_uuid filter unchanged.
- Around line 35-46: Update the RETURNING list used by scanConsoleWorkspace so
the second color slot returns w.color instead of duplicating w.display_color;
preserve w.display_color in the first slot and keep the remaining returned
columns unchanged.

In `@web/src/features/settings/WorkspacesSettingsPage.tsx`:
- Around line 178-181: Update
web/src/features/settings/WorkspacesSettingsPage.tsx:178-181 to pass an archive
restriction reason (`default`, `current`, or `undefined`) to WorkspaceRowActions
instead of a boolean, distinguishing the permanently non-archivable default
workspace from the active workspace. Update
web/src/features/settings/WorkspacesSettingsPage.tsx:251-261 to render the
corresponding accessible explanation without relying on title or pointer events.
Add the distinct default-workspace message in
web/src/shared/i18n/messages/en.json:1217 and its Chinese translation in
web/src/shared/i18n/messages/zh-CN.json:1217.

In `@web/src/shared/workspaces/WorkspaceProvider.tsx`:
- Around line 80-85: The archiveWorkspace mutation must forward the bootstrap
CSRF token for cookie-authenticated requests. In
web/src/shared/workspaces/WorkspaceProvider.tsx lines 80-85, obtain the token
from the authentication context and pass it to archiveConsoleWorkspace; in
web/src/shared/workspaces/api.ts lines 75-79, update archiveConsoleWorkspace to
accept the token and include it in the consoleApi options so X-CSRF-Token is
sent.

---

Nitpick comments:
In `@docs/design/be/console-workspace-archive.md`:
- Around line 79-81: Update the fenced code block containing the archive POST
endpoint to specify an appropriate language such as http, while preserving the
endpoint content unchanged.

In `@tests/console_workspace_archive_api_test.go`:
- Around line 90-99: Move the “isolated by organization” subtest within the
relevant test function so it runs before “archive succeeds and cascades to api
keys.” Keep the test body and its 404 assertion unchanged, preserving failure
scenarios before success scenarios.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af678cc1-9f48-4213-864d-45e76dd43b7f

📥 Commits

Reviewing files that changed from the base of the PR and between c653b80 and c04a1a7.

📒 Files selected for processing (12)
  • docs/design/be/console-workspace-archive.md
  • internal/db/console_workspaces.go
  • internal/platformapi/console_workspaces.go
  • internal/platformapi/platform_backend_routes.go
  • tests/console_workspace_archive_api_test.go
  • web/src/features/settings/WorkspacesSettingsPage.tsx
  • web/src/shared/i18n/messages/en.json
  • web/src/shared/i18n/messages/zh-CN.json
  • web/src/shared/workspaces/ArchiveWorkspaceDialog.tsx
  • web/src/shared/workspaces/WorkspaceProvider.tsx
  • web/src/shared/workspaces/api.ts
  • web/src/shared/workspaces/context.ts

Comment thread internal/db/console_workspaces.go
Comment thread internal/db/console_workspaces.go Outdated
Comment thread web/src/features/settings/WorkspacesSettingsPage.tsx
Comment thread web/src/shared/workspaces/WorkspaceProvider.tsx Outdated

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

Caution

当前实现尚未完成与 Anthropic 对齐的“级联吊销”语义:归档后 /v1 服务使用的 api_keys 仍保持 active,实际 API key 仍可调用服务;同时真正的默认工作区(数据库 external_id = workspace_default)可被归档。这两处会造成功能性和数据完整性风险,需要修复后再合并。

Reviewed changes — 审查了 Console 工作区归档的完整实现:后端单事务归档、防自锁与默认工作区保护、前端 DropdownMenu 操作入口与二次确认 Dialog、相关测试与设计文档。

  • 新增 ArchiveConsoleWorkspacehandleArchiveConsoleWorkspace — 在 internal/dbinternal/platformapi 中实现工作区软删除及级联 console_api_keys 归档。
  • 新增归档路由POST /api/console/organizations/{orgUuid}/workspaces/{workspaceId}/archive
  • 前端归档入口WorkspacesSettingsPage 改为 DropdownMenuArchiveWorkspaceDialog 做破坏性二次确认。
  • 新增测试与设计文档 — 覆盖 200/404/409 default/级联/幂等/组织隔离。

ℹ️ Nitpicks

  • web/src/features/settings/WorkspacesSettingsPage.tsx:180 前端用 === 比较工作区 ID,后端用 strings.EqualFold,建议统一策略。
  • web/src/shared/workspaces/WorkspaceProvider.tsx:86-88 归档成功后仅过滤列表缓存,建议失效 ['console', ...] 前缀的相关查询并清理 localStorage 中的 preferredWorkspaceId
  • 对 disabled 的 DropdownMenuItem 使用 title 属性展示原因,可访问性弱于 Tooltiparia-describedby

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using anthropic/glm-5.2𝕏

Comment thread internal/db/console_workspaces.go Outdated
Comment thread internal/db/console_workspaces.go Outdated
Comment thread internal/platformapi/console_workspaces.go Outdated
Comment thread web/src/shared/workspaces/api.ts
Comment thread web/src/shared/workspaces/WorkspaceProvider.tsx
Comment thread tests/console_workspace_archive_api_test.go
Comment thread tests/console_workspace_archive_api_test.go
Comment thread web/src/features/settings/WorkspacesSettingsPage.tsx
@arthur-zhang

arthur-zhang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor
image image image

curl 'https://platform.claude.com/api/console/organizations/xx/workspaces/wrkspc_0/archive'
-X 'POST' \

{
"id": "s",
"type": "workspace",
"name": "bbb",
"created_at": "2026-xxx",
"archived_at": "2026-xxx084088Z",
"display_color": "#ACA1CC",
"data_residency": {
"workspace_geo": "us",
"allowed_inference_geos": "unrestricted",
"default_inference_geo": "global"
},
"external_key_id": null,
"compartment_id": "sssc1",
"inference_data_retention": false,
"tags": {}
}

@arthur-zhang

Copy link
Copy Markdown
Contributor

default不能 archive
然后视觉效果要对齐 shadcn 的样式

- Extract principalWorkspace local in handleArchiveConsoleWorkspace to
  avoid duplicating TrimSpace and keep the self-lockout guard within
  style line limits.
- Filter the API key cascade by the workspaceID argument instead of
  workspace.UUID. Both hold the workspace external_id (scanConsoleWorkspace
  maps w.external_id into ConsoleWorkspace.UUID), but the parameter is
  clearer and avoids the field-name trap.

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

Caution

新提交仅修正了 console_api_keys 级联查询的 workspace_id 参数,但上一轮 review 指出的关键风险仍未处理,PR 尚不可合并。

Reviewed changes — 重新审查了 a13d877 对归档级联查询的修正,以及当前会话工作区比较逻辑的整理。

  • 修正 console_api_keys 级联定位列internal/db/console_workspaces.go:57 将级联更新中的 workspace.UUID 改为 workspaceID,与 console_api_keys.workspace_id 存储外部 ID 的语义一致。
  • 整理当前会话工作区比较internal/platformapi/console_workspaces.go:47 先对 principal.WorkspaceExternalIDTrimSpace,再与请求的工作区 ID 比较。

🚨 关键风险仍未解决

上一轮指出的以下问题在本提交中未得到修复,仍会导致归档后 /v1 调用可继续成功、真正的默认工作区被归档、前端请求缺少 CSRF 防护等后果。

  • api_keys 表未随归档更新。
  • console_api_keys 级联只写 archived_at,未同步 status = 'archived'
  • 默认工作区保护仅拦截 "default",未拦截数据库真实默认 workspace_default
  • 归档请求未携带 X-CSRF-Token
  • 测试未覆盖 workspace_default 和「归档当前工作区」场景。
Technical details
# 关键风险仍未解决

## Affected sites
- 上一轮 review: https://github.com/superduck-ai/open-managed-agents/pull/96#pullrequestreview-4725311244
- `internal/db/console_workspaces.go` — 归档事务未同步吊销 `api_keys`
- `internal/db/console_workspaces.go` — 级联 `console_api_keys` 未设置 `status = 'archived'`
- `internal/platformapi/console_workspaces.go` — 默认工作区保护未拦截 `workspace_default`
- `tests/console_workspace_archive_api_test.go` — 未覆盖 `workspace_default` 与归档当前工作区
- `web/src/shared/workspaces/api.ts``WorkspaceProvider.tsx` — 归档请求未携带 CSRF token

## Required outcome
- 归档事务需要同时使 `/v1` 调用立即失败(通过更新 `api_keys` 或让 `GetAPIKey` 读取 `workspaces.archived_at`)。
- 已归档的 `console_api_keys` 在 API 响应中显示 `status: 'archived'`- 后端默认工作区保护应拦截真实默认 `external_id = 'workspace_default'`,并在测试中覆盖。
- 前端基于 cookie 的归档请求携带 `X-CSRF-Token`

Pullfrog  | Fix it ➔View workflow run | Using anthropic/glm-5.2𝕏

The handler only blocked the literal "default" alias, so a caller who
knew the default workspace's real external_id could bypass the guard and
archive it. Add `lower(coalesce(name, '')) <> 'default'` to the UPDATE
WHERE clause so the "default workspace is never archivable" invariant
holds at the write path regardless of which identifier the caller used;
such a request now surfaces as ErrNotFound (404). The (organization_id,
name) unique constraint guarantees one default workspace per org, so the
guard is exact. Cover it with a direct DB-layer test and reorder tests
so all failure cases precede the success cases.
@Postroggy

Copy link
Copy Markdown
Contributor Author

@arthur-zhang 两点反馈都已处理,见最新两个 commit a13d8770b87aec,逐条说明:

1. default 不能 archive(已加固)

之前 handler 只拦截 "default" 字面别名,传默认工作区的真实 external_id 仍能绕过。已在 DB 层 ArchiveConsoleWorkspace 的 UPDATE WHERElower(coalesce(name, '')) <> 'default',把"默认工作区永不可归档"作为写入路径不变量——无论传 "default" 别名还是默认工作区的真实 external_id,UPDATE 都命中 0 行 → ErrNotFound → 404。

workspaces 表的 (organization_id, name) 唯一约束保证每个组织只有一个 name = 'default' 的工作区,守卫精确不误伤。已补 DB 层直接调用测试(default workspace cannot be archived by external_id)验证:传默认工作区真实 external_id → ErrNotFound,且 archived_at 保持空。

handler 仍保留 "default" 别名的 409 映射(cannot_archive_default_workspace),让最常见的调用拿到明确语义;DB 层不变量是兜底权威防线。

2. 归档响应结构(已对齐你贴的官方 curl)

归档响应复用既有 formatConsoleWorkspace,输出字段与你截图里 Anthropic 官方 POST .../workspaces/{id}/archive 的响应一致:id / type / name / display_color / created_at / archived_at / data_residency(嵌套 workspace_geo / allowed_inference_geos / default_inference_geo)/ external_key_id / compartment_id / inference_data_retention / tags

3. 视觉对齐 shadcn 样式

归档链路本身已是标准 shadcn new-york:

  • 入口 WorkspaceRowActionsDropdownMenu + ghost Button(MoreVertical 触发)+ DropdownMenuItem variant="destructive"(Archive,默认/当前工作区 disabled)。
  • 确认对话框 ArchiveWorkspaceDialogAlertDialog + AlertDialogMediabg-destructive/10)+ Alert + AlertDialogAction variant="destructive",全部走语义 CSS 变量。

你三张截图展示的官方列表页(Search workspaces 搜索框、Status Active 筛选、数量徽章、ID / Created / API Keys 数量 / Tags 列)属于工作区管理页整体信息密度与视觉的升级,超出本归档 PR 的范围。本 PR 聚焦"归档"这一条功能链路(后端语义 + 级联吊销 + 禁归档边界 + 前端入口 + 二次确认)。

如果你说的"视觉对齐"特指归档入口 / 对话框的某个具体细节(间距、图标、色调、文案),麻烦再指一下,我直接在这改;列表页整体视觉我愿意单独跟进,建议另开 issue,避免在这个归档 PR 里混入不相关的列表重构。

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

Reviewed changes — 重新审查了 0b87aec 对默认工作区归档防护的补强。

  • internal/db/console_workspaces.go 的归档 WHERE 子句中增加 lower(coalesce(w.name, '')) <> 'default',使默认工作区无论通过别名还是真实 external_id 都无法被归档。
  • 新增 tests/console_workspace_archive_api_test.go 用例,直接验证 DB 层对真实 external_id 的默认工作区返回 ErrNotFoundarchived_at 保持为空。
  • 更新 docs/design/be/console-workspace-archive.md 状态图与错误码表,将真实 external_id 默认工作区的归档结果表述为 404。

Caution

新提交在 DB 层为默认工作区补上了不可归档的兜底,但此前 review 中标记的关键风险仍未修复,PR 尚不可合并。仍未解决的上游问题包括:归档事务未同步吊销 /v1 服务使用的 api_keys;级联 console_api_keys 更新未写入 status = 'archived';前端归档请求未携带 X-CSRF-Token;测试未覆盖「归档当前会话工作区」的 409 场景;WorkspacesSettingsPagedefaultcurrent 的禁用原因使用了同一提示文案。

Pullfrog  | Fix it ➔View workflow run | Using anthropic/glm-5.2𝕏

@Postroggy

Copy link
Copy Markdown
Contributor Author

default不能 archive 然后视觉效果要对齐 shadcn 的样式

image image

The finally block ran setSubmitting(false) after onClose() on the success
path, flipping the confirm button from "Archiving..." back to "Archive" for
a frame before the dialog unmounted. Drop the finally: leave submitting set
on success (the dialog is about to unmount anyway) and only reset it in the
catch path so the action can be retried after a failure.
@Postroggy

Copy link
Copy Markdown
Contributor Author

补充一个归档确认弹窗的体验修复(620ecaa):点 Archive 后确认按钮会从「Archiving…」闪回「Archive」一帧再消失——根因是 handleArchivefinally 在成功路径里、onClose() 之后又 setSubmitting(false)。已去掉 finally,成功路径保留 submitting 态直到弹窗卸载,失败路径在 catch 里 reset 以便重试。

@Postroggy Postroggy changed the title 补齐控制台工作区归档入口并对齐 Anthropic 级联吊销语义 feat(console): 补齐控制台工作区归档入口并对齐 Anthropic 级联吊销语义 Jul 19, 2026
- ArchiveConsoleWorkspace 事务中同时归档 api_keys 表里该工作区的活跃 key,修复归档后 /v1 API key 仍可鉴权的漏洞
- console_api_keys 级联更新补充设置 status='archived'
- WorkspaceApiKeysPage / WorkspaceProvider 变更类请求传入 csrfToken
- WorkspacesSettingsPage 通过 archiveDisabledReason 区分 default/current 工作区禁用提示
- 新增 cannot_archive_current_workspace 子测试及 live API key 吊销断言
- 新增 workspace.archive.defaultDisabledHint i18n key(中/英)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
web/src/shared/workspaces/api.ts (1)

3-14: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Workspace type omits archived_at.

The archive endpoint returns the full workspace payload including archived_at (per formatConsoleWorkspace and the backend test asserting archived["archived_at"]), and WorkspaceApiKey in this same file already models archived_at?: string | null. Workspace should too, so downstream consumers (e.g., the archive dialog/confirmation UI) can read it without unsafe casts.

🐛 Proposed fix
 export type Workspace = {
   id: string;
   type: 'workspace';
   name: string;
   display_color?: string;
   color?: string;
   data_residency?: {
     workspace_geo?: string;
     allowed_inference_geos?: string;
     default_inference_geo?: string;
   } | null;
+  archived_at?: string | null;
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/shared/workspaces/api.ts` around lines 3 - 14, Update the Workspace
type to include an optional nullable archived_at string property, matching
WorkspaceApiKey and the archive endpoint payload so consumers can access it
without casts.
🧹 Nitpick comments (2)
web/src/shared/workspaces/api.ts (1)

68-81: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

csrfToken reintroduced as optional across all four functions — same pattern that already caused a missed-CSRF bug once.

Every current call site (WorkspaceProvider.tsx, WorkspaceApiKeysPage.tsx) already passes csrfToken, so making it a required parameter costs nothing today but prevents a future call site from silently omitting it, per the guideline requiring CSRF on all cookie-authenticated mutating requests.

Also applies to: 89-103, 105-120

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

In `@web/src/shared/workspaces/api.ts` around lines 68 - 81, Make the csrfToken
parameter required for all four workspace mutation functions, including
createConsoleWorkspace and archiveConsoleWorkspace and the functions in the
referenced ranges. Remove the optional marker while preserving the existing
argument order and ensure every current call site continues passing the token.

Source: Path instructions

tests/console_workspace_archive_api_test.go (1)

14-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

TestConsoleWorkspaceArchive flagged as high complexity/length by static analysis.

The function spans roughly 170 lines across 6 subtests plus shared fixture setup, exceeding the general Go function-length guidance (**/*.go: 函数最多 163 行). Consider extracting the cascade/idempotency logic (129-182) and/or the current-workspace session-binding logic (82-123) into named helper functions to shrink the top-level body while keeping the subtests intact.

♻️ Sketch of extraction
func testArchiveCascadesAndIsIdempotent(t *testing.T, app *testApp, base, orgUUID string, orgID int64, cookies []*http.Cookie) {
	// move lines 125-182 body here, called from within t.Run wrappers
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/console_workspace_archive_api_test.go` around lines 14 - 183, Reduce
the complexity of TestConsoleWorkspaceArchive by extracting the
current-workspace session-binding setup/restore and the archive
cascade/idempotency assertions into focused named helpers. Keep the existing
t.Run cases, fixtures, assertions, and test behavior unchanged, with helpers
receiving the required app, organization, workspace, and cookie context.

Source: Linters/SAST tools

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

Inline comments:
In `@tests/console_workspace_archive_api_test.go`:
- Around line 224-242: Update seedLiveAPIKeyForWorkspace to verify that the
INSERT ... SELECT affected exactly one row, capturing the command result and
checking its affected-row count after execution. Fail the test when the
workspace lookup inserts zero rows (or an unexpected number), so later GetAPIKey
assertions genuinely exercise revocation.

---

Outside diff comments:
In `@web/src/shared/workspaces/api.ts`:
- Around line 3-14: Update the Workspace type to include an optional nullable
archived_at string property, matching WorkspaceApiKey and the archive endpoint
payload so consumers can access it without casts.

---

Nitpick comments:
In `@tests/console_workspace_archive_api_test.go`:
- Around line 14-183: Reduce the complexity of TestConsoleWorkspaceArchive by
extracting the current-workspace session-binding setup/restore and the archive
cascade/idempotency assertions into focused named helpers. Keep the existing
t.Run cases, fixtures, assertions, and test behavior unchanged, with helpers
receiving the required app, organization, workspace, and cookie context.

In `@web/src/shared/workspaces/api.ts`:
- Around line 68-81: Make the csrfToken parameter required for all four
workspace mutation functions, including createConsoleWorkspace and
archiveConsoleWorkspace and the functions in the referenced ranges. Remove the
optional marker while preserving the existing argument order and ensure every
current call site continues passing the token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b4b568b8-dcd9-4d12-8211-c7470f042ce8

📥 Commits

Reviewing files that changed from the base of the PR and between a13d877 and 557492e.

📒 Files selected for processing (10)
  • docs/design/be/console-workspace-archive.md
  • internal/db/console_workspaces.go
  • tests/console_workspace_archive_api_test.go
  • web/src/features/settings/WorkspaceApiKeysPage.tsx
  • web/src/features/settings/WorkspacesSettingsPage.tsx
  • web/src/shared/i18n/messages/en.json
  • web/src/shared/i18n/messages/zh-CN.json
  • web/src/shared/workspaces/ArchiveWorkspaceDialog.tsx
  • web/src/shared/workspaces/WorkspaceProvider.tsx
  • web/src/shared/workspaces/api.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • web/src/shared/i18n/messages/zh-CN.json
  • web/src/shared/i18n/messages/en.json
  • web/src/shared/workspaces/WorkspaceProvider.tsx
  • web/src/shared/workspaces/ArchiveWorkspaceDialog.tsx
  • web/src/features/settings/WorkspacesSettingsPage.tsx
  • docs/design/be/console-workspace-archive.md
  • internal/db/console_workspaces.go

Comment thread tests/console_workspace_archive_api_test.go

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

Important

新提交在合并 sqlx 迁移后修复了归档扫描回归,但 ArchiveConsoleWorkspace 仍使用 raw pgx 句柄,是 internal/db 中唯一未跟随迁移的函数,建议在合并前对齐。

Reviewed changes — 重新审查了 0b87aec 之后的增量改动:babdf1a 修复 sqlx 迁移合并引入的归档扫描回归,以及两个 shell 测试脚本的 cosmetic refactor。

  • 重引入 scanConsoleWorkspace 修复归档扫描internal/db/console_workspaces.go0f9104e 合并 sqlx 迁移后扫描逻辑丢失,babdf1a 重新加入 scanConsoleWorkspace helper 并把 pgx.ErrNoRows 映射为 platform.ErrNotFound,归档流程恢复可用。
  • Shell 测试脚本 cosmetic refactorgenerate-code-session-jwt-key_test.shgenerate-upstream-proxy-ca-key_test.sh 把 PEM header 比较中的 "PRIVATE KEY" 字面量提取为 private_key_label 变量,无行为变化。
  • 此前 review 关注点已全部解决557492e/2a5eac1/620ecaa 已完成 api_keys 级联吊销、CSRF 传递、seed helper 行数校验、当前工作区 409 测试与 default/current 禁用文案拆分;全部 8 条 pullfrog 线程已由作者确认并 resolve。

Note: 2 inline comment(s) dropped because they did not anchor to lines inside the PR diff:

  • internal/db/console_workspaces.go:169 (RIGHT) — line 169 (RIGHT) is not inside a diff hunk
  • docs/design/be/console-workspace-archive.md:125 (RIGHT) — line 125 (RIGHT) is not inside a diff hunk

Pullfrog  | Fix it ➔View workflow run | Using anthropic/glm-5.2𝕏

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

ℹ️ 本轮无关键问题 —— 一处文档同步建议见内联。

Reviewed changes — 重新审查了 babdf1a 之后的增量改动 a2fbcdc:将 ArchiveConsoleWorkspace 从 raw pgx 全量迁移到 sqlx,并对齐 UUID 引用语义。

  • ArchiveConsoleWorkspace 迁移到 sqlxinternal/db/console_workspaces.god.Pool.Begin + tx.QueryRow/tx.Exec 改为 d.sql.BeginTxx + getConsoleWorkspaceSQLX + namedExecContext,使用 :organization_uuid / :workspace_uuid / :workspace_external_id 命名参数与 dbUUID / tryParseDBUUIDIdentifier helper,消除 internal/db 中最后一个 raw-pgx 写入路径。WHERE 与 RETURNING 改为按 organization_uuid / workspace_uuid 等 UUID 列过滤,与 migration 00044 回填的 UUID 引用列及对应 v2 索引一致;上一轮指出的 raw-pgx 句柄问题由此关闭。
  • 归档自锁防护改用 WorkspaceScopeinternal/platformapi/console_workspaces.go 改为通过 consoleWorkspaceScopeFromRequest 解析出 WorkspaceScope,同时按 UUIDDisplayID 匹配当前会话工作区,比上一版的单一 external_id 比较更稳健;ArchiveConsoleWorkspace 现接收 workspaceScope.UUID
  • 测试 seed helper 对齐 UUID 引用规则tests/console_workspace_archive_api_test.goseedArchiveOrganization / seedArchiveTargetWorkspace / seedLiveAPIKeyForWorkspaceorganization_id(bigint)与 workspace_id(text external id)改为 organization_uuid / workspace_uuidseedConsoleAPIKeyForWorkspace 改用真实的 db.CreateConsoleAPIKey,符合 internal/db/AGENTS.md 的标识符引用约定。
  • Shell 测试脚本空白整理generate-code-session-jwt-key_test.shgenerate-upstream-proxy-ca-key_test.sh 删除一处多余空行,无行为变化。

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using anthropic/glm-5.2𝕏

- `workspaces.archived_at timestamptz`:非空即归档。
- `console_api_keys.archived_at timestamptz`:随工作区归档一并置位。

`console_api_keys.workspace_id` 存储工作区的 `external_id`(如 `default`),级联按 `org_uuid` + `workspace_id` 定位。归档写使用 `coalesce(archived_at, now())`,因此对已归档工作区重复归档是幂等的:`archived_at` 不会被改写。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ 这段数据模型描述与本提交的级联实现已经脱节。a2fbcdcArchiveConsoleWorkspace 的级联 WHERE 从 org_uuid + workspace_id(文本 external_id 列)改成了 organization_uuid + workspace_uuid(UUID 列,见 internal/db/console_workspaces.go:64-65),但本段与时序图第 48 行仍按旧的文本列描述。同一段还提到「只写两个时间戳列」,而实现现在还会把 console_api_keys.status 置为 'archived' 并级联吊销 api_keys(见 console_workspaces.go:60-6173-79)。

Technical details
# 文档与实现脱节

## Affected sites
- `docs/design/be/console-workspace-archive.md:20` — 数据模型段称级联按 `org_uuid` + `workspace_id` 定位,且只写 `archived_at`;实现已改为 UUID 列并写入 `status`- `docs/design/be/console-workspace-archive.md:48` — 时序图标注 `WHERE org_uuid AND workspace_id`,同样过时。
- `docs/design/be/console-workspace-archive.md:71-73` — 包职责段称级联只更新 `console_api_keys.archived_at`,且「复用 `console_api_keys.go``scanConsoleWorkspace`」;本提交已删除本地 `scanConsoleWorkspace`、改用 `getConsoleWorkspaceSQLX`,且级联同时写 `status='archived'``api_keys`## Required outcome
- 数据模型段与时序图按 `organization_uuid` / `workspace_uuid`(UUID 列)描述级联定位。
- 级联写操作列出演齐 `console_api_keys``status` + `archived_at`)与 `api_keys``status`)。
- 包职责段的 scan 辅助函数引用更新为 `getConsoleWorkspaceSQLX`## Suggested approach
- 第 20 行改为「级联按 `organization_uuid` + `workspace_uuid` 定位」,并补充 `console_api_keys.status``api_keys.status` 的级联写入。
- 第 48 行改为 `WHERE organization_uuid AND workspace_uuid`- 第 71-73 行更新为 `getConsoleWorkspaceSQLX` 并补全 `console_api_keys.status``api_keys` 级联吊销。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

已在 ff469c8 同步:数据模型段、时序图与包职责段均已按 organization_uuid / workspace_uuidsqlx 事务、api_keys 级联吊销与 WorkspaceScope 更新。

Pullfrog  | View workflow run | via Pullfrog | Using anthropic/glm-5.2𝕏

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.

工作区缺少归档(软删除)入口,且现有归档未对齐 Anthropic 的级联吊销与权限语义

2 participants