-
Notifications
You must be signed in to change notification settings - Fork 252
重构文档中心、账号分组与 API Key 权限管理 #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
james-6-23
merged 30 commits into
james-6-23:main
from
DeliciousBuding:rebase-onto-v2.1.3
May 13, 2026
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
b5fc435
feat(types): add AccountGroup types, tags/groups fields, and UsageSta…
DeliciousBuding 537e107
feat(apikeys): add inline rename with pencil button
DeliciousBuding 65dcfc5
feat(api): add account group CRUD, updateAPIKey, and cache:no-store
DeliciousBuding 8fcaf27
feat(css): update font stack and add code panel typography
DeliciousBuding 17e98e0
feat(PageHeader): add actionMeta prop for secondary header content
DeliciousBuding e713199
feat(ChipInput): add reusable multi-select chip input component
DeliciousBuding 567592b
feat(proxies): add edit dialog + concurrent test with progress
DeliciousBuding 6daf41c
feat(Dashboard): replace inline stats with UsageStatsSummary component
DeliciousBuding 82b2fa3
feat(api): add updateAPIKey and extend updateProxy with url param
DeliciousBuding 83f9c6e
feat(usage): add configurable request log columns
DeliciousBuding ae4cc09
feat(accounts): add tags groups and column controls
DeliciousBuding 474e4ea
Implement backend account groups and scheduler scope
DeliciousBuding 54b2b98
Merge branch 'track-b-frontend-core' into rebase-onto-v2.1.3
DeliciousBuding 8e0a96a
Merge branch 'track-c-frontend-pages' into rebase-onto-v2.1.3
DeliciousBuding 685a3c9
docs: add tabbed quick start guide
DeliciousBuding afa129f
docs: restore integrated documentation page
DeliciousBuding 8ae8f06
docs: localize docs page and align tabs
DeliciousBuding 5891485
docs: add client import config tabs
DeliciousBuding 63e207f
Improve combined docs experience
DeliciousBuding dd45c10
Polish docs import and highlighting performance
DeliciousBuding 7bc87e5
Keep account filters on one row
DeliciousBuding 0ae576b
Fix column settings popover clipping
DeliciousBuding f9b5bf9
Harden account group routing controls
DeliciousBuding 5b23e67
Complete account group permission management
DeliciousBuding 28d317c
Complete API key and group management polish
DeliciousBuding 852a703
Fix review issues in docs and scheduler management
DeliciousBuding 3217698
Fill dashboard cache rate and first-token stats
DeliciousBuding ca475b7
Fix remaining review issues in docs and group scopes
DeliciousBuding fb405c5
Harden docs highlighter and try-it guards
DeliciousBuding ab123ef
fix: use errors.Is for sql.ErrNoRows checks across codebase
james-6-23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| package admin | ||
|
|
||
| import ( | ||
| "context" | ||
| "database/sql" | ||
| "errors" | ||
| "net/http" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
| "unicode/utf8" | ||
|
|
||
| "github.com/codex2api/database" | ||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| const ( | ||
| maxAccountGroups = 64 | ||
| maxAccountGroupNameRuneSize = 80 | ||
| ) | ||
|
|
||
| type accountGroupResponse struct { | ||
| ID int64 `json:"id"` | ||
| Name string `json:"name"` | ||
| Description string `json:"description"` | ||
| Color string `json:"color"` | ||
| SortOrder int64 `json:"sort_order"` | ||
| MemberCount int64 `json:"member_count"` | ||
| CreatedAt string `json:"created_at"` | ||
| UpdatedAt string `json:"updated_at"` | ||
| } | ||
|
|
||
| func toAccountGroupResponse(g database.AccountGroup) accountGroupResponse { | ||
| return accountGroupResponse{ | ||
| ID: g.ID, | ||
| Name: g.Name, | ||
| Description: g.Description, | ||
| Color: g.Color, | ||
| SortOrder: g.SortOrder, | ||
| MemberCount: g.MemberCount, | ||
| CreatedAt: g.CreatedAt.Format(time.RFC3339), | ||
| UpdatedAt: g.UpdatedAt.Format(time.RFC3339), | ||
| } | ||
| } | ||
|
|
||
| func (h *Handler) ListAccountGroups(c *gin.Context) { | ||
| ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) | ||
| defer cancel() | ||
| groups, err := h.db.ListAccountGroups(ctx) | ||
| if err != nil { | ||
| writeInternalError(c, err) | ||
| return | ||
| } | ||
| out := make([]accountGroupResponse, 0, len(groups)) | ||
| for _, group := range groups { | ||
| out = append(out, toAccountGroupResponse(group)) | ||
| } | ||
| c.JSON(http.StatusOK, gin.H{"groups": out}) | ||
| } | ||
|
|
||
| type createAccountGroupReq struct { | ||
| Name string `json:"name"` | ||
| Description string `json:"description"` | ||
| Color string `json:"color"` | ||
| SortOrder *int64 `json:"sort_order"` | ||
| } | ||
|
|
||
| func (h *Handler) CreateAccountGroup(c *gin.Context) { | ||
| var req createAccountGroupReq | ||
| if err := c.ShouldBindJSON(&req); err != nil { | ||
| writeError(c, http.StatusBadRequest, "请求格式错误") | ||
| return | ||
| } | ||
| name, err := sanitizeAccountGroupName(req.Name) | ||
| if err != nil { | ||
| writeError(c, http.StatusBadRequest, err.Error()) | ||
| return | ||
| } | ||
| description := strings.TrimSpace(req.Description) | ||
| if utf8.RuneCountInString(description) > 240 { | ||
| writeError(c, http.StatusBadRequest, "描述长度不能超过 240 字符") | ||
| return | ||
| } | ||
| color := strings.TrimSpace(req.Color) | ||
| if utf8.RuneCountInString(color) > 20 { | ||
| writeError(c, http.StatusBadRequest, "颜色长度不能超过 20 字符") | ||
| return | ||
| } | ||
| sortOrder := int64(0) | ||
| if req.SortOrder != nil { | ||
| sortOrder = *req.SortOrder | ||
| } | ||
| ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) | ||
| defer cancel() | ||
| groups, err := h.db.ListAccountGroups(ctx) | ||
| if err != nil { | ||
| writeInternalError(c, err) | ||
| return | ||
| } | ||
| if len(groups) >= maxAccountGroups { | ||
| writeError(c, http.StatusBadRequest, "分组数量已达上限") | ||
| return | ||
| } | ||
| id, err := h.db.CreateAccountGroup(ctx, name, description, color, sortOrder) | ||
| if err != nil { | ||
| if errors.Is(err, database.ErrDuplicateAccountGroupName) { | ||
| writeError(c, http.StatusConflict, err.Error()) | ||
| return | ||
| } | ||
| writeInternalError(c, err) | ||
| return | ||
| } | ||
| c.JSON(http.StatusOK, gin.H{"id": id, "message": "分组已创建"}) | ||
| } | ||
|
|
||
| type updateAccountGroupReq struct { | ||
| Name *string `json:"name"` | ||
| Description *string `json:"description"` | ||
| Color *string `json:"color"` | ||
| SortOrder *int64 `json:"sort_order"` | ||
| } | ||
|
|
||
| func (h *Handler) UpdateAccountGroup(c *gin.Context) { | ||
| id, err := strconv.ParseInt(c.Param("id"), 10, 64) | ||
| if err != nil { | ||
| writeError(c, http.StatusBadRequest, "无效的分组 ID") | ||
| return | ||
| } | ||
| var req updateAccountGroupReq | ||
| if err := c.ShouldBindJSON(&req); err != nil { | ||
| writeError(c, http.StatusBadRequest, "请求格式错误") | ||
| return | ||
| } | ||
| if req.Name != nil { | ||
| name, err := sanitizeAccountGroupName(*req.Name) | ||
| if err != nil { | ||
| writeError(c, http.StatusBadRequest, err.Error()) | ||
| return | ||
| } | ||
| req.Name = &name | ||
| } | ||
| if req.Description != nil { | ||
| desc := strings.TrimSpace(*req.Description) | ||
| if utf8.RuneCountInString(desc) > 240 { | ||
| writeError(c, http.StatusBadRequest, "描述长度不能超过 240 字符") | ||
| return | ||
| } | ||
| req.Description = &desc | ||
| } | ||
| if req.Color != nil { | ||
| color := strings.TrimSpace(*req.Color) | ||
| if utf8.RuneCountInString(color) > 20 { | ||
| writeError(c, http.StatusBadRequest, "颜色长度不能超过 20 字符") | ||
| return | ||
| } | ||
| req.Color = &color | ||
| } | ||
| ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) | ||
| defer cancel() | ||
| if err := h.db.UpdateAccountGroup(ctx, id, req.Name, req.Description, req.Color, req.SortOrder); err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| writeError(c, http.StatusNotFound, "分组不存在") | ||
| return | ||
| } | ||
| if errors.Is(err, database.ErrDuplicateAccountGroupName) { | ||
| writeError(c, http.StatusConflict, err.Error()) | ||
| return | ||
| } | ||
| writeInternalError(c, err) | ||
| return | ||
| } | ||
| writeMessage(c, http.StatusOK, "分组已更新") | ||
| } | ||
|
|
||
| func (h *Handler) DeleteAccountGroup(c *gin.Context) { | ||
| id, err := strconv.ParseInt(c.Param("id"), 10, 64) | ||
| if err != nil { | ||
| writeError(c, http.StatusBadRequest, "无效的分组 ID") | ||
| return | ||
| } | ||
| force := strings.EqualFold(c.Query("force"), "true") | ||
| ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) | ||
| defer cancel() | ||
| if err := h.db.DeleteAccountGroup(ctx, id, force); err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| writeError(c, http.StatusNotFound, "分组不存在") | ||
| return | ||
| } | ||
| if errors.Is(err, database.ErrAccountGroupNotEmpty) { | ||
| writeError(c, http.StatusConflict, err.Error()) | ||
| return | ||
| } | ||
| writeInternalError(c, err) | ||
| return | ||
| } | ||
| if h.store != nil { | ||
| for _, acc := range h.store.Accounts() { | ||
| acc.Mu().RLock() | ||
| groups := removeInt64(acc.GroupIDs, id) | ||
| acc.Mu().RUnlock() | ||
| h.store.ApplyAccountGroups(acc.DBID, groups) | ||
| } | ||
| } | ||
| h.refreshAPIKeyAllowedGroupsAfterGroupDelete(ctx, id) | ||
| writeMessage(c, http.StatusOK, "分组已删除") | ||
| } | ||
|
|
||
| func (h *Handler) refreshAPIKeyAllowedGroupsAfterGroupDelete(ctx context.Context, groupID int64) { | ||
| if h == nil || h.db == nil || groupID <= 0 { | ||
| return | ||
| } | ||
| keys, err := h.db.ListAPIKeys(ctx) | ||
| if err != nil { | ||
| return | ||
| } | ||
| for _, key := range keys { | ||
| if key == nil { | ||
| continue | ||
| } | ||
| if h.store != nil { | ||
| h.store.SetAPIKeyAllowedGroups(key.ID, key.AllowedGroupIDs) | ||
| } | ||
| h.invalidateAPIKeyRuntimeCaches(ctx, key.Key) | ||
| } | ||
| } | ||
|
|
||
| func sanitizeAccountGroupName(raw string) (string, error) { | ||
| name := strings.TrimSpace(raw) | ||
| if name == "" { | ||
| return "", errors.New("分组名称不能为空") | ||
| } | ||
| if utf8.RuneCountInString(name) > maxAccountGroupNameRuneSize { | ||
| return "", errors.New("分组名称长度超过 80 字符") | ||
| } | ||
| for _, r := range name { | ||
| if r < 0x20 || r == 0x7f { | ||
| return "", errors.New("分组名称包含非法控制字符") | ||
| } | ||
| } | ||
| return name, nil | ||
| } | ||
|
|
||
| func removeInt64(slice []int64, target int64) []int64 { | ||
| out := make([]int64, 0, len(slice)) | ||
| for _, v := range slice { | ||
| if v != target { | ||
| out = append(out, v) | ||
| } | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func containsInt64(slice []int64, target int64) bool { | ||
| for _, v := range slice { | ||
| if v == target { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func dedupeInt64(ids []int64) []int64 { | ||
| if len(ids) == 0 { | ||
| return nil | ||
| } | ||
| seen := make(map[int64]struct{}, len(ids)) | ||
| out := make([]int64, 0, len(ids)) | ||
| for _, id := range ids { | ||
| if id <= 0 { | ||
| continue | ||
| } | ||
| if _, ok := seen[id]; ok { | ||
| continue | ||
| } | ||
| seen[id] = struct{}{} | ||
| out = append(out, id) | ||
| } | ||
| return out | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.