From e5786776673cd6957c4c98cc97f9bb633526001f Mon Sep 17 00:00:00 2001 From: Mengz <2567587994@qq.com> Date: Tue, 9 Jun 2026 19:42:58 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(member):=20=E6=96=B0=E5=A2=9E=E6=88=90?= =?UTF-8?q?=E5=91=98=E7=94=B3=E8=AF=B7=E5=A4=84=E7=90=86=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++ README.zh-CN.md | 10 ++ doc/changes/member-application-shortcuts.md | 33 ++++ internal/client/client.go | 4 +- shortcuts/member/member.go | 174 ++++++++++++++++++++ shortcuts/member/member_test.go | 162 +++++++++++++++++- skills/gitlink-member/SKILL.md | 18 +- 7 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 doc/changes/member-application-shortcuts.md diff --git a/README.md b/README.md index b43dcd1..f44c21d 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,16 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D # Create an invite link gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true + +# List pending project membership applications +gitlink-cli member +applications --user Mengz --page 1 --per-page 20 + +# Accept or refuse a membership application by applied_projects[].id +gitlink-cli member +accept-application --user Mengz --id 42 --dry-run +gitlink-cli member +refuse-application --user Mengz --id 43 --dry-run + +# Apply to join a project by application code +gitlink-cli member +apply --code --role developer --dry-run ``` ### Issue Management diff --git a/README.zh-CN.md b/README.zh-CN.md index 265ac98..6fc87df 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -294,6 +294,16 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D # 生成邀请链接 gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true + +# 查看待处理的项目成员申请 +gitlink-cli member +applications --user Mengz --page 1 --per-page 20 + +# 按 applied_projects[].id 接受或拒绝成员申请 +gitlink-cli member +accept-application --user Mengz --id 42 --dry-run +gitlink-cli member +refuse-application --user Mengz --id 43 --dry-run + +# 通过申请码申请加入项目 +gitlink-cli member +apply --code --role developer --dry-run ``` ### Issue 管理 diff --git a/doc/changes/member-application-shortcuts.md b/doc/changes/member-application-shortcuts.md new file mode 100644 index 0000000..2780b73 --- /dev/null +++ b/doc/changes/member-application-shortcuts.md @@ -0,0 +1,33 @@ +# Member application shortcuts + +This change extends `gitlink-cli member` from direct collaborator and invite-link operations to the project membership application workflow. + +New shortcuts: + +- `member +applications` lists project membership applications for a user inbox with `--user`, `--page`, and `--per-page`. +- `member +accept-application` accepts an application by `applied_projects[].id` and supports `--dry-run`. +- `member +refuse-application` refuses an application by `applied_projects[].id` and supports `--dry-run`. +- `member +apply` applies to join a project with an application code and requested role, also supporting `--dry-run`. + +The implementation follows the documented GitLink OpenAPI endpoints: + +- `GET /api/users/{owner}/applied_projects.json` +- `POST /api/users/{owner}/applied_projects/{id}/accept.json` +- `POST /api/users/{owner}/applied_projects/{id}/refuse.json` +- `POST /api/applied_projects.json` + +Safety details: + +- Application decisions validate positive integer IDs before calling the API. +- Application role values are normalized to `manager`, `developer`, or `reporter`. +- Dry-run output includes the method, path, and request body where applicable. +- When `--user` is omitted, the shortcut uses `--owner` first and falls back to `GET /users/me`. + +Verification: + +- `go test ./shortcuts/member` +- `go test ./shortcuts` +- `go test ./...` +- `go build ./...` +- `go run ./internal/i18n/cmd/check --scan-code` +- `git diff --check` diff --git a/internal/client/client.go b/internal/client/client.go index 1d20aed..ac5c3ff 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -80,7 +80,7 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o } if c.Debug { - fmt.Printf("→ %s %s\n", method, fullURL) + fmt.Printf("-> %s %s\n", method, fullURL) } resp, err := c.HTTP.Do(req) @@ -95,7 +95,7 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o } if c.Debug { - fmt.Printf("← %d %s\n", resp.StatusCode, string(respData[:min(len(respData), 200)])) + fmt.Printf("<- %d %s\n", resp.StatusCode, string(respData[:min(len(respData), 200)])) } // Check HTTP-level errors diff --git a/shortcuts/member/member.go b/shortcuts/member/member.go index 3734da3..bfe5db8 100644 --- a/shortcuts/member/member.go +++ b/shortcuts/member/member.go @@ -193,9 +193,125 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "applications", + Description: "List project membership applications for a user", + Flags: []common.Flag{ + {Name: "user", Short: "u", Usage: "User login that owns the application inbox. Defaults to --owner or current user"}, + {Name: "page", Usage: "Page number", Default: "1"}, + {Name: "per-page", Usage: "Items per page", Default: "20"}, + }, + Run: runApplications, + }, + { + Name: "accept-application", + Description: "Accept a project membership application", + Flags: []common.Flag{ + {Name: "user", Short: "u", Usage: "User login that owns the application inbox. Defaults to --owner or current user"}, + {Name: "id", Usage: "Membership application ID from applied_projects[].id", Required: true}, + {Name: "dry-run", Usage: "Preview the accept request without changing data", Bool: true, Default: "false"}, + }, + Run: func(ctx *common.RuntimeContext) error { + return runApplicationDecision(ctx, "accept") + }, + }, + { + Name: "refuse-application", + Description: "Refuse a project membership application", + Flags: []common.Flag{ + {Name: "user", Short: "u", Usage: "User login that owns the application inbox. Defaults to --owner or current user"}, + {Name: "id", Usage: "Membership application ID from applied_projects[].id", Required: true}, + {Name: "dry-run", Usage: "Preview the refuse request without changing data", Bool: true, Default: "false"}, + }, + Run: func(ctx *common.RuntimeContext) error { + return runApplicationDecision(ctx, "refuse") + }, + }, + { + Name: "apply", + Description: "Apply to join a project by application code", + Flags: []common.Flag{ + {Name: "code", Short: "c", Usage: "Project application code", Required: true}, + {Name: "role", Short: "r", Usage: "Requested role: manager, developer, or reporter", Default: "developer"}, + {Name: "dry-run", Usage: "Preview the application request without changing data", Bool: true, Default: "false"}, + }, + Run: runApply, + }, } } +func runApplications(ctx *common.RuntimeContext) error { + user, err := resolveApplicationUser(ctx) + if err != nil { + return err + } + query := url.Values{} + setIfPresent(query, "page", ctx.Arg("page")) + setIfPresent(query, "per_page", ctx.Arg("per-page")) + env, err := ctx.CallAPIWithQuery("GET", appliedProjectsPath(user), query) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runApplicationDecision(ctx *common.RuntimeContext, action string) error { + user, err := resolveApplicationUser(ctx) + if err != nil { + return err + } + id, err := parsePositiveID("id", ctx.Arg("id")) + if err != nil { + return err + } + path := fmt.Sprintf("%s/%d/%s", appliedProjectsPath(user), id, action) + if parseDryRun(ctx.Arg("dry-run")) { + return ctx.OutputData(map[string]interface{}{ + "dry_run": true, + "method": "POST", + "path": path, + "user": user, + "id": id, + "action": action, + }) + } + env, err := ctx.CallAPI("POST", path, nil) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runApply(ctx *common.RuntimeContext) error { + code, err := requiredTrimmed("code", ctx.Arg("code")) + if err != nil { + return err + } + role, err := normalizeApplicationRole(ctx.Arg("role")) + if err != nil { + return err + } + payload := map[string]interface{}{ + "applied_project": map[string]interface{}{ + "code": code, + "role": role, + }, + } + if parseDryRun(ctx.Arg("dry-run")) { + return ctx.OutputData(map[string]interface{}{ + "dry_run": true, + "method": "POST", + "path": "/applied_projects", + "body": payload, + }) + } + env, err := ctx.CallAPI("POST", "/applied_projects", payload) + if err != nil { + return err + } + return ctx.Output(env) +} + func runBatchAdd(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err @@ -266,6 +382,33 @@ func inviteLinkPath(ctx *common.RuntimeContext, action string) string { return fmt.Sprintf("/%s/%s/project_invite_links/%s", ctx.Owner, ctx.Repo, action) } +func appliedProjectsPath(user string) string { + return fmt.Sprintf("/users/%s/applied_projects", user) +} + +func resolveApplicationUser(ctx *common.RuntimeContext) (string, error) { + if user := strings.TrimSpace(ctx.Arg("user")); user != "" { + return user, nil + } + if user := strings.TrimSpace(ctx.Owner); user != "" { + return user, nil + } + env, err := ctx.CallAPI("GET", "/users/me", nil) + if err != nil { + return "", fmt.Errorf("failed to get current user: %w", err) + } + data, ok := env.Data.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("cannot determine current user login") + } + login, _ := data["login"].(string) + login = strings.TrimSpace(login) + if login == "" { + return "", fmt.Errorf("cannot determine current user login") + } + return login, nil +} + func parseUserID(value string) (int, error) { value = strings.TrimSpace(value) userID, err := strconv.Atoi(value) @@ -291,6 +434,37 @@ func normalizeInviteRole(value string) (string, error) { return strings.ToLower(role), nil } +func normalizeApplicationRole(value string) (string, error) { + role, err := normalizeRole(value) + if err != nil { + return "", fmt.Errorf("invalid --role value %q: use manager, developer, or reporter", value) + } + return strings.ToLower(role), nil +} + +func parsePositiveID(name, value string) (int, error) { + value = strings.TrimSpace(value) + id, err := strconv.Atoi(value) + if err != nil || id <= 0 { + return 0, fmt.Errorf("invalid --%s value %q: use a positive integer", name, value) + } + return id, nil +} + +func requiredTrimmed(name, value string) (string, error) { + value = strings.TrimSpace(value) + if value == "" { + return "", fmt.Errorf("missing required flag: %s", name) + } + return value, nil +} + +func setIfPresent(values url.Values, key, value string) { + if value := strings.TrimSpace(value); value != "" { + values.Set(key, value) + } +} + func parseBoolArg(name, value string) (bool, error) { switch strings.ToLower(strings.TrimSpace(value)) { case "", "true": diff --git a/shortcuts/member/member_test.go b/shortcuts/member/member_test.go index d379e82..9ecb908 100644 --- a/shortcuts/member/member_test.go +++ b/shortcuts/member/member_test.go @@ -188,6 +188,161 @@ func TestMemberAcceptInvite(t *testing.T) { } } +func TestMemberApplicationsUsesExplicitUserAndPagination(t *testing.T) { + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/users/Mengz/applied_projects.json") + if r.URL.Query().Get("page") != "2" { + t.Fatalf("page query = %q, want 2", r.URL.Query().Get("page")) + } + if r.URL.Query().Get("per_page") != "50" { + t.Fatalf("per_page query = %q, want 50", r.URL.Query().Get("per_page")) + } + writeJSON(t, w, map[string]interface{}{"total_count": 1, "applied_projects": []interface{}{}}) + }) + defer server.Close() + + err := runMemberShortcut(t, server, "applications", map[string]string{ + "user": "Mengz", + "page": "2", + "per-page": "50", + }) + if err != nil { + t.Fatalf("applications shortcut failed: %v", err) + } +} + +func TestMemberApplicationsDefaultsToCurrentUserWhenOwnerMissing(t *testing.T) { + var paths []string + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + paths = append(paths, r.URL.Path) + switch r.URL.Path { + case "/users/me.json": + assertRequest(t, r, "GET", "/users/me.json") + writeJSON(t, w, map[string]interface{}{"login": "Mengz"}) + case "/users/Mengz/applied_projects.json": + assertRequest(t, r, "GET", "/users/Mengz/applied_projects.json") + writeJSON(t, w, map[string]interface{}{"total_count": 0, "applied_projects": []interface{}{}}) + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + }) + defer server.Close() + + if err := runMemberShortcutWithOwner(t, server, "", "applications", nil); err != nil { + t.Fatalf("applications shortcut failed: %v", err) + } + want := []string{"/users/me.json", "/users/Mengz/applied_projects.json"} + if !reflect.DeepEqual(paths, want) { + t.Fatalf("request paths = %v, want %v", paths, want) + } +} + +func TestMemberAcceptApplication(t *testing.T) { + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "POST", "/users/Mengz/applied_projects/42/accept.json") + writeJSON(t, w, map[string]interface{}{"id": 42, "status": "accepted"}) + }) + defer server.Close() + + err := runMemberShortcut(t, server, "accept-application", map[string]string{ + "user": "Mengz", + "id": "42", + }) + if err != nil { + t.Fatalf("accept-application shortcut failed: %v", err) + } +} + +func TestMemberRefuseApplicationDryRunDoesNotCallAPI(t *testing.T) { + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runMemberShortcut(t, server, "refuse-application", map[string]string{ + "user": "Mengz", + "id": "43", + "dry-run": "true", + }) + if err != nil { + t.Fatalf("refuse-application dry-run failed: %v", err) + } +} + +func TestMemberApplicationRejectsInvalidIDBeforeAPI(t *testing.T) { + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("invalid id should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runMemberShortcut(t, server, "accept-application", map[string]string{ + "user": "Mengz", + "id": "0", + }) + if err == nil { + t.Fatal("expected invalid id to return an error") + } +} + +func TestMemberApply(t *testing.T) { + var payload map[string]interface{} + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "POST", "/applied_projects.json") + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + err := runMemberShortcut(t, server, "apply", map[string]string{ + "code": "invite-code", + "role": "reporter", + }) + if err != nil { + t.Fatalf("apply shortcut failed: %v", err) + } + appliedProject, ok := payload["applied_project"].(map[string]interface{}) + if !ok { + t.Fatalf("applied_project payload = %v, want object", payload["applied_project"]) + } + if appliedProject["code"] != "invite-code" { + t.Fatalf("code = %v, want invite-code", appliedProject["code"]) + } + if appliedProject["role"] != "reporter" { + t.Fatalf("role = %v, want reporter", appliedProject["role"]) + } +} + +func TestMemberApplyDryRunDoesNotCallAPI(t *testing.T) { + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runMemberShortcut(t, server, "apply", map[string]string{ + "code": "invite-code", + "role": "developer", + "dry-run": "true", + }) + if err != nil { + t.Fatalf("apply dry-run failed: %v", err) + } +} + +func TestMemberApplyRejectsInvalidRoleBeforeAPI(t *testing.T) { + server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("invalid role should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runMemberShortcut(t, server, "apply", map[string]string{ + "code": "invite-code", + "role": "owner", + }) + if err == nil { + t.Fatal("expected invalid role to return an error") + } +} + func TestCollectUserIDs(t *testing.T) { csvPath := writeTempCSV(t, "name,id\nfirst,102\nsecond,103\n") got, err := collectUserIDs("101,102", csvPath) @@ -207,6 +362,11 @@ func TestNormalizeRoleRejectsInvalidRole(t *testing.T) { } func runMemberShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + return runMemberShortcutWithOwner(t, server, "owner", name, args) +} + +func runMemberShortcutWithOwner(t *testing.T, server *httptest.Server, owner, name string, args map[string]string) error { t.Helper() shortcut := findMemberShortcut(t, name) ctx := &common.RuntimeContext{ @@ -214,7 +374,7 @@ func runMemberShortcut(t *testing.T, server *httptest.Server, name string, args HTTP: server.Client(), BaseURL: server.URL, }, - Owner: "owner", + Owner: owner, Repo: "repo", Format: "json", Args: args, diff --git a/skills/gitlink-member/SKILL.md b/skills/gitlink-member/SKILL.md index 61f8c50..273db40 100644 --- a/skills/gitlink-member/SKILL.md +++ b/skills/gitlink-member/SKILL.md @@ -1,6 +1,6 @@ --- name: gitlink-member -description: "仓库成员管理:列出、添加、批量添加、移除成员,调整成员角色,生成、查看和接受项目邀请链接。" +description: "仓库成员管理:列出、添加、批量添加、移除成员,调整成员角色,处理成员申请,生成、查看和接受项目邀请链接。" metadata: cliHelp: "gitlink-cli member --help" --- @@ -21,6 +21,10 @@ metadata: | `member +invite-link` | 获取或生成当前邀请链接 | | `member +invite-info` | 查看邀请链接信息 | | `member +accept-invite` | 接受邀请链接 | +| `member +applications` | 查看用户待办中的项目成员申请 | +| `member +accept-application` | 接受成员申请 | +| `member +refuse-application` | 拒绝成员申请 | +| `member +apply` | 通过申请码申请加入项目 | ## 示例 @@ -48,10 +52,22 @@ gitlink-cli member +invite-info --owner Gitlink --repo forgeplus --sign + +# 查看待处理的项目成员申请。未传 --user 时优先使用 --owner,否则读取当前登录用户。 +gitlink-cli member +applications --user Mengz --page 1 --per-page 20 + +# 接受或拒绝成员申请。--id 是 applied_projects[].id,不是项目 ID。 +gitlink-cli member +accept-application --user Mengz --id 42 --dry-run +gitlink-cli member +refuse-application --user Mengz --id 43 --dry-run + +# 通过申请码申请加入项目。角色支持 manager、developer、reporter。 +gitlink-cli member +apply --code --role developer --dry-run ``` ## 安全规则 - 执行 `member +remove`、`member +role`、`member +add`、`member +batch-add` 前,确认目标仓库和用户 ID。 - 批量添加前优先使用 `--dry-run` 预览。 +- 接受或拒绝成员申请前先运行 `member +applications`,确认 `applied_projects[].id` 和申请人、目标项目一致。 +- 申请加入项目或处理申请时优先使用 `--dry-run` 预览请求路径与请求体。 - 避免在公开日志中暴露邀请链接的完整 `sign`。 From 973756a00f7a7fe188640673058fcd6fffb77e13 Mon Sep 17 00:00:00 2001 From: Mengz <2567587994@qq.com> Date: Fri, 12 Jun 2026 11:03:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore(i18n):=20=E8=A7=84=E8=8C=83=E5=8C=96?= =?UTF-8?q?=E6=88=90=E5=91=98=E7=94=B3=E8=AF=B7=E5=91=BD=E4=BB=A4=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/i18n/locales/en-US.json | 16 ++++++++-------- internal/i18n/locales/zh-CN.json | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index c8df7cf..a0a6cbf 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -90,13 +90,13 @@ "error.config.save_failed": "failed to save config: {message}", "error.missing_required_flag": "required flag --{name} is missing", "error.unsupported_language": "unsupported language: {lang}", - "flag.api.body": "Request body (JSON string)", - "flag.api.body_file": "Read request body JSON from a file", - "flag.api.body_stdin": "Read request body JSON from stdin", "flag.api.batch_continue_on_error": "Continue running remaining batch requests after a failure", "flag.api.batch_dry_run": "Preview batch requests without sending remote requests", "flag.api.batch_file": "Read an API batch plan from a JSON file", "flag.api.batch_var": "Override a batch template variable (key=value, repeatable)", + "flag.api.body": "Request body (JSON string)", + "flag.api.body_file": "Read request body JSON from a file", + "flag.api.body_stdin": "Read request body JSON from stdin", "flag.api.header": "Additional headers (key:value)", "flag.api.query": "Query parameters (key=val&key2=val2)", "flag.auth.token": "Login by pasting an existing token", @@ -113,6 +113,8 @@ "flag.format": "Output format: json, table, yaml (default: table)", "flag.issue.add_label": "Label to add to each matching issue", "flag.issue.assignee": "Assignee login", + "flag.issue.assignee_id": "Assignee user ID", + "flag.issue.author_id": "Author user ID", "flag.issue.batch.reason": "Optional reason shown in the batch result", "flag.issue.batch.yes": "Execute remote operations. Without this flag the command is dry-run only.", "flag.issue.batch_close.older_than_days": "Required safety filter; must be at least 7", @@ -120,8 +122,6 @@ "flag.issue.batch_label.state": "Filter by issue state", "flag.issue.batch_list.limit": "Maximum issues to return, capped at 100", "flag.issue.batch_process.limit": "Maximum issues to process, capped at 100", - "flag.issue.assignee_id": "Assignee user ID", - "flag.issue.author_id": "Author user ID", "flag.issue.body": "Issue description", "flag.issue.label": "Label ID", "flag.issue.label_filter": "Filter by existing label", @@ -149,14 +149,14 @@ "flag.pr.file": "Filter diff by file path", "flag.pr.head": "Source branch", "flag.pr.id": "PR number", - "flag.pr.milestone_id": "Milestone ID", "flag.pr.merge_method": "Merge method: merge, rebase, squash", + "flag.pr.milestone_id": "Milestone ID", "flag.pr.priority_id": "Priority ID", "flag.pr.review_commit": "Commit SHA to attach the review to", "flag.pr.review_content": "Review content", - "flag.pr.reviewer_id": "Reviewer user ID", "flag.pr.review_status": "Review status: common, approved, rejected", "flag.pr.review_status_filter": "Filter review status: common, approved, rejected", + "flag.pr.reviewer_id": "Reviewer user ID", "flag.pr.state": "Filter: open, merged, closed", "flag.pr.tag_id": "Issue tag ID", "flag.pr.title": "PR title", @@ -194,7 +194,6 @@ "output.auth.login_hint": " Run: gitlink-cli auth login", "output.config.file": "Config file: {path}", "output.config.not_set": "(not set)", - "output.version": "gitlink-cli {version}", "output.doctor.api_auth.config_skipped": "API authentication check skipped because the configuration file is invalid.", "output.doctor.api_auth.failed": "Authenticated API request failed: {message}", "output.doctor.api_auth.no_login": "Authenticated API response did not include a login field.", @@ -217,6 +216,7 @@ "output.doctor.suggestion.check_token": "Check whether the stored token is valid, or run gitlink-cli auth login again.", "output.doctor.suggestion.fix_config_yaml": "Fix the YAML syntax in the gitlink-cli config file.", "output.doctor.suggestion.pass_owner_repo": "Run the command with --owner and --repo when not inside a GitLink repository.", + "output.version": "gitlink-cli {version}", "prompt.auth.password": "Password: ", "prompt.auth.token": "Paste your access token: ", "prompt.auth.username": "Username/Email/Phone: ", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 7fdc2b1..c11c5bf 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -90,13 +90,13 @@ "error.config.save_failed": "保存配置失败:{message}", "error.missing_required_flag": "缺少必需参数 --{name}", "error.unsupported_language": "不支持的语言:{lang}", - "flag.api.body": "请求体(JSON 字符串)", - "flag.api.body_file": "从文件读取 JSON 请求体", - "flag.api.body_stdin": "从标准输入读取 JSON 请求体", "flag.api.batch_continue_on_error": "批处理请求失败后继续执行后续请求", "flag.api.batch_dry_run": "预览批处理请求,不发送远端请求", "flag.api.batch_file": "从 JSON 文件读取 API 批处理计划", "flag.api.batch_var": "覆盖批处理模板变量(key=value,可重复)", + "flag.api.body": "请求体(JSON 字符串)", + "flag.api.body_file": "从文件读取 JSON 请求体", + "flag.api.body_stdin": "从标准输入读取 JSON 请求体", "flag.api.header": "附加请求头(key:value)", "flag.api.query": "查询参数(key=val&key2=val2)", "flag.auth.token": "通过粘贴已有 Token 登录", @@ -113,6 +113,8 @@ "flag.format": "输出格式:json、table、yaml(默认:table)", "flag.issue.add_label": "要添加到每个匹配议题的标签", "flag.issue.assignee": "负责人登录名", + "flag.issue.assignee_id": "负责人用户 ID", + "flag.issue.author_id": "作者用户 ID", "flag.issue.batch.reason": "批量结果中显示的可选原因", "flag.issue.batch.yes": "执行远端操作。未传入该参数时仅 dry-run。", "flag.issue.batch_close.older_than_days": "必需的安全筛选条件;至少为 7", @@ -120,8 +122,6 @@ "flag.issue.batch_label.state": "按议题状态筛选", "flag.issue.batch_list.limit": "最多返回的议题数,上限 100", "flag.issue.batch_process.limit": "最多处理的议题数,上限 100", - "flag.issue.assignee_id": "负责人用户 ID", - "flag.issue.author_id": "作者用户 ID", "flag.issue.body": "议题描述", "flag.issue.label": "标签 ID", "flag.issue.label_filter": "按已有标签筛选", @@ -149,14 +149,14 @@ "flag.pr.file": "按文件路径筛选 diff", "flag.pr.head": "源分支", "flag.pr.id": "PR 编号", - "flag.pr.milestone_id": "里程碑 ID", "flag.pr.merge_method": "合并方式:merge、rebase、squash", + "flag.pr.milestone_id": "里程碑 ID", "flag.pr.priority_id": "优先级 ID", "flag.pr.review_commit": "关联评审的 Commit SHA", "flag.pr.review_content": "评审内容", - "flag.pr.reviewer_id": "评审人用户 ID", "flag.pr.review_status": "评审状态:common、approved、rejected", "flag.pr.review_status_filter": "按评审状态筛选:common、approved、rejected", + "flag.pr.reviewer_id": "评审人用户 ID", "flag.pr.state": "筛选:open、merged、closed", "flag.pr.tag_id": "议题标签 ID", "flag.pr.title": "PR 标题", @@ -194,7 +194,6 @@ "output.auth.login_hint": " 运行:gitlink-cli auth login", "output.config.file": "配置文件:{path}", "output.config.not_set": "(未设置)", - "output.version": "gitlink-cli {version}", "output.doctor.api_auth.config_skipped": "配置文件无效,已跳过 API 认证检查。", "output.doctor.api_auth.failed": "认证 API 请求失败:{message}", "output.doctor.api_auth.no_login": "认证 API 响应中缺少 login 字段。", @@ -217,6 +216,7 @@ "output.doctor.suggestion.check_token": "检查已保存的 Token 是否有效,或重新运行 gitlink-cli auth login。", "output.doctor.suggestion.fix_config_yaml": "修复 gitlink-cli 配置文件中的 YAML 语法。", "output.doctor.suggestion.pass_owner_repo": "不在 GitLink 仓库目录内时,请通过 --owner 和 --repo 指定仓库。", + "output.version": "gitlink-cli {version}", "prompt.auth.password": "密码:", "prompt.auth.token": "粘贴你的访问 Token:", "prompt.auth.username": "用户名/邮箱/手机号:",