feat(cc-config): switch between Claude Code accounts, keeping the workspace

Adds a "Switch profile" button left of Backups on /settings?tool=claude-code.
A *profile* is one Anthropic account - one quota - saved under a name, so the
local `claude` CLI can be pointed at a different account without disturbing
anything else.

A switch swaps exactly two identity artifacts and nothing more: `claudeAiOauth`
inside `<CLAUDE_HOME>/.credentials.json`, and `oauthAccount` + `userID` inside
the `.claude.json` config file(s). Those files are key-merged in place, and only
where they already exist, so `projects`/`numStartups`/everything else survives.
Skills, subagents, commands, memory, plugins, marketplaces, MCP servers and
settings are never read or written here - that is what makes a switch keep the
whole workspace and change only the account. `mcpOAuth` stays put too: those are
the machine's MCP server tokens, not account identity.

Profiles are one JSON file each under `<CLAUDE_HOME>/agenthub-profiles/` (dir
0700, files 0600, atomic temp+rename) and hold live OAuth tokens, so no route
ever returns token material - `GET /profiles` answers identity metadata only.

The dashboard cannot log an account in; only `claude auth login` mints tokens.
Adding a profile is therefore always "log in from a terminal, then snapshot what
the CLI wrote". `activateProfile` re-snapshots the outgoing account first so
tokens Claude Code refreshed since its last save are not lost, and refuses
(`EUNSAVED`) to switch away from an account that was never saved - that would
discard the only copy of its credentials. A switch applies to new sessions only,
since a running session rewrites the config on exit.

Routes: GET/POST/DELETE /api/cc-config/profiles, POST /profiles/activate.

Verified: server 1420/1420 (14 new, incl. token-free responses and 0600/0700
permissions), client 459/459.
This commit is contained in:
2026-09-07 15:13:38 +07:00
parent 17d56d0e01
commit 4bc77ded17
13 changed files with 1496 additions and 3 deletions
+23
View File
@@ -210,6 +210,29 @@
"scopeLabel": "Phạm vi",
"filterAll": "Tất cả loại"
},
"profiles": {
"title": "Profile Claude Code",
"openButton": "Đổi profile",
"subtitle": "Mỗi profile là một tài khoản Anthropic - quota riêng. Khi đổi, chỉ phần đăng nhập (credentials + danh tính tài khoản) được hoán đổi; skills, subagent, memory, plugin, marketplace, MCP server và settings vẫn giữ nguyên.",
"warning": "Hãy đóng các phiên Claude Code đang chạy trước khi đổi - phiên đang chạy sẽ ghi lại config khi thoát và đưa tài khoản cũ quay về.",
"activeLabel": "Đang đăng nhập",
"noActive": "Chưa có tài khoản Claude nào đăng nhập trên máy này.",
"unsavedActive": "Tài khoản này chưa được lưu thành profile - hãy lưu bên dưới, nếu không sẽ không thể đổi sang profile khác mà không mất credentials của nó.",
"saveLabel": "Lưu tài khoản hiện tại thành profile",
"namePlaceholder": "tên profile (vd: work)",
"saveButton": "Lưu",
"saveHint": "Dashboard không thể đăng nhập thay bạn. Để thêm tài khoản khác: chạy `claude auth login` trong terminal, đăng nhập, rồi quay lại đây lưu với tên mới.",
"saveSuccess": "Đã lưu tài khoản hiện tại thành profile \"{{name}}\".",
"empty": "Chưa có profile nào - hãy lưu tài khoản đang đăng nhập để bắt đầu.",
"activeBadge": "đang dùng",
"switchButton": "Chuyển sang",
"switching": "Đang chuyển…",
"switchSuccess": "Đã chuyển sang profile \"{{name}}\". Mở phiên Claude Code mới để áp dụng.",
"deleteButton": "Xoá profile",
"deleteSuccess": "Đã xoá profile \"{{name}}\".",
"savedAt": "Lưu lúc {{time}}",
"loginHint": "Thêm tài khoản khác bằng cách đăng nhập từ terminal trước:"
},
"explain": {
"hooks": {
"title": "Sửa hooks tức là ghi vào settings.json",