feat(user): add account and stats shortcuts

This commit is contained in:
wangyue789 2026-05-27 22:53:02 +08:00
parent 52b7093846
commit a1de64ace7
7 changed files with 544 additions and 81 deletions

View File

@ -107,7 +107,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
| 🔔 Webhook | Manage repo webhooks and test deliveries |
| 🔍 Search | Search repositories, users |
| 👤 User | View user profiles and info |
| 👤 User | View profiles, manage public keys, inspect user statistics |
| 📋 PM | Sprint management, kanban boards, weekly reports |
| 🤖 Workflow | AI-powered issue triage, PR review, release notes |
@ -477,6 +477,24 @@ gitlink-cli search +repos -k "machine learning"
gitlink-cli search +users -k "zhangsan"
```
### User Operations
```bash
# Current authenticated user and detailed profile
gitlink-cli user +me
gitlink-cli user +current --format json
# Public key management
gitlink-cli user +keys --page 1 --limit 20
gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run
gitlink-cli user +key-delete --id 7 --dry-run
# User statistics
gitlink-cli user +activity --login zhangsan
gitlink-cli user +headmap --login zhangsan --year 2026
gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599
```
### Workflow Agent Commands
`workflow` provides rule-based repository analysis for maintainers and AI Agents. It currently supports:
@ -647,7 +665,7 @@ See [skills/README.md](skills/README.md) for details.
| `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) |
| `gitlink-search` | Search (repositories, users, etc.) |
| `gitlink-org` | Organization management (members, teams, etc.) |
| `gitlink-user` | User management (profile info, etc.) |
| `gitlink-user` | User management (profiles, public keys, statistics, etc.) |
| `gitlink-pm` | Project management (sprints, kanban, weekly reports, etc.) |
| `gitlink-workflow` | AI-powered workflows (issue triage, PR review, release notes, etc.) |
| `gitlink-health` | Project health analysis (PR/Issue metrics aggregation, health reports) |

View File

@ -106,7 +106,7 @@
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
| 🔍 搜索 | 搜索仓库、用户 |
| 👤 用户 | 查看用户资料和信息 |
| 👤 用户 | 查看资料、管理 Public Keys、查询用户统计 |
| 📋 项目管理 | Sprint 管理、看板、周报 |
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
@ -455,6 +455,24 @@ gitlink-cli search +repos -k "machine learning"
gitlink-cli search +users -k "zhangsan"
```
### 用户操作
```bash
# 当前登录用户和详细资料
gitlink-cli user +me
gitlink-cli user +current --format json
# Public Key 管理
gitlink-cli user +keys --page 1 --limit 20
gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run
gitlink-cli user +key-delete --id 7 --dry-run
# 用户统计
gitlink-cli user +activity --login zhangsan
gitlink-cli user +headmap --login zhangsan --year 2026
gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599
```
### Raw API
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
@ -525,7 +543,7 @@ git push gitlink
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
| `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) |
| `gitlink-search` | 搜索功能(仓库、用户等) |
| `gitlink-user` | 用户管理(个人信息等) |
| `gitlink-user` | 用户管理(资料、Public Keys、统计等) |
| `gitlink-pm` | 项目管理Sprint、看板、周报等 |
| `gitlink-workflow` | AI 自动化工作流Issue 分类、PR Review、Release Notes 等) |

View File

@ -0,0 +1,36 @@
# User Account And Stats Shortcuts
Submitter: Wang Yue
This change expands the `user` shortcut group with account metadata, Public Key management, and user statistics OpenAPI coverage.
## Commands
- `user +current`
- `user +keys`
- `user +key-create`
- `user +key-delete`
- `user +activity`
- `user +headmap`
- `user +develop`
- `user +role`
- `user +major`
## API Mapping
| Shortcut | Method | API path |
|----------|--------|----------|
| `user +current` | GET | `/api/users/get_user_info.json` |
| `user +keys` | GET | `/api/public_keys.json` |
| `user +key-create` | POST | `/api/public_keys.json` |
| `user +key-delete` | DELETE | `/api/public_keys/{id}.json` |
| `user +activity` | GET | `/api/users/{owner}/statistics/activity.json` |
| `user +headmap` | GET | `/api/users/{owner}/headmaps.json` |
| `user +develop` | GET | `/api/users/{owner}/statistics/develop.json` |
| `user +role` | GET | `/api/users/{owner}/statistics/role.json` |
| `user +major` | GET | `/api/users/{owner}/statistics/major.json` |
## Verification
- Unit tests cover public key list/create/delete requests, dry-run behavior, current profile lookup, user statistics endpoints, query parameters, and argument validation.
- `user +key-create` supports either inline `--key` content or `--key-file`.

View File

@ -2,6 +2,9 @@ package user
import (
"fmt"
"net/url"
"os"
"strings"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
@ -21,6 +24,17 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "current",
Description: "Show current user profile details",
Run: func(ctx *common.RuntimeContext) error {
env, err := ctx.CallAPI("GET", "/users/get_user_info", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "info",
Description: tr.T("cmd.user.info.short"),
@ -39,6 +53,160 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "keys",
Description: "List current user public keys",
Flags: []common.Flag{
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
{Name: "limit", Usage: "Items per page", Default: "20"},
},
Run: func(ctx *common.RuntimeContext) error {
q := url.Values{}
setQueryIfPresent(q, ctx, "page", "page")
setQueryIfPresent(q, ctx, "limit", "limit")
env, err := ctx.CallAPIWithQuery("GET", "/public_keys", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "key-create",
Description: "Create a public key",
Flags: []common.Flag{
{Name: "title", Short: "t", Usage: "Public key title", Required: true},
{Name: "key", Short: "k", Usage: "Public key content"},
{Name: "key-file", Usage: "Path to a public key file"},
{Name: "dry-run", Usage: "Preview the create request without adding a key", Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
title, err := ctx.RequireArg("title")
if err != nil {
return err
}
key, err := publicKeyFromArgs(ctx)
if err != nil {
return err
}
payload := map[string]interface{}{
"title": title,
"key": key,
}
path := "/public_keys"
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "create_public_key",
"method": "POST",
"path": path,
"payload": payload,
})
}
env, err := ctx.CallAPI("POST", path, payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "key-delete",
Description: "Delete a public key",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Public key ID", Required: true},
{Name: "dry-run", Usage: "Preview the delete request without removing a key", Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
id, err := ctx.RequireArg("id")
if err != nil {
return err
}
path := fmt.Sprintf("/public_keys/%s", url.PathEscape(id))
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "delete_public_key",
"method": "DELETE",
"path": path,
})
}
env, err := ctx.CallAPI("DELETE", path, nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
userStatsShortcut("activity", "Show user recent activity statistics", "statistics/activity", false, false),
userStatsShortcut("headmap", "Show user contribution heatmap", "headmaps", true, false),
userStatsShortcut("develop", "Show user development ability statistics", "statistics/develop", false, true),
userStatsShortcut("role", "Show user role statistics", "statistics/role", false, true),
userStatsShortcut("major", "Show user major statistics", "statistics/major", false, true),
}
}
func userStatsShortcut(name, description, endpoint string, withYear, withTimeRange bool) *common.Shortcut {
flags := []common.Flag{
{Name: "login", Short: "l", Usage: "User login name", Required: true},
}
if withYear {
flags = append(flags, common.Flag{Name: "year", Usage: "Contribution year"})
}
if withTimeRange {
flags = append(flags,
common.Flag{Name: "start-time", Usage: "Start timestamp"},
common.Flag{Name: "end-time", Usage: "End timestamp"},
)
}
return &common.Shortcut{
Name: name,
Description: description,
Flags: flags,
Run: func(ctx *common.RuntimeContext) error {
login, err := ctx.RequireArg("login")
if err != nil {
return err
}
q := url.Values{}
setQueryIfPresent(q, ctx, "year", "year")
setQueryIfPresent(q, ctx, "start-time", "start_time")
setQueryIfPresent(q, ctx, "end-time", "end_time")
path := fmt.Sprintf("/users/%s/%s", url.PathEscape(login), endpoint)
env, err := ctx.CallAPIWithQuery("GET", path, q)
if err != nil {
return err
}
return ctx.Output(env)
},
}
}
func publicKeyFromArgs(ctx *common.RuntimeContext) (string, error) {
key := strings.TrimSpace(ctx.Arg("key"))
keyFile := strings.TrimSpace(ctx.Arg("key-file"))
if key != "" && keyFile != "" {
return "", fmt.Errorf("use only one of --key or --key-file")
}
if key == "" && keyFile == "" {
return "", fmt.Errorf("required flag --key or --key-file is missing")
}
if keyFile != "" {
data, err := os.ReadFile(keyFile)
if err != nil {
return "", fmt.Errorf("read --key-file: %w", err)
}
key = strings.TrimSpace(string(data))
}
if key == "" {
return "", fmt.Errorf("public key content is empty")
}
return key, nil
}
func setQueryIfPresent(q url.Values, ctx *common.RuntimeContext, flagName, queryName string) {
if value := ctx.Arg(flagName); value != "" {
q.Set(queryName, value)
}
}

View File

@ -2,120 +2,328 @@ package user
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func runShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
shortcut := findShortcut(t, name)
ctx := &common.RuntimeContext{
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
Owner: "owner",
Repo: "repo",
Format: "json",
Args: args,
}
return shortcut.Run(ctx)
}
func findShortcut(t *testing.T, name string) *common.Shortcut {
t.Helper()
for _, s := range Shortcuts() {
if s.Name == name {
return s
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
// --- me ---
func TestUserMe(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/me.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(w, map[string]interface{}{
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertUserRequest(t, r, "GET", "/users/me.json")
writeUserJSON(t, w, map[string]interface{}{
"login": "currentuser",
"name": "Current User",
"id": float64(1),
})
}))
})
defer server.Close()
err := runShortcut(t, server, "me", nil)
if err != nil {
t.Fatalf("me failed: %v", err)
if err := runUserShortcut(t, server, "me", nil); err != nil {
t.Fatalf("me shortcut failed: %v", err)
}
}
// --- info ---
func TestUserCurrent(t *testing.T) {
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertUserRequest(t, r, "GET", "/users/get_user_info.json")
writeUserJSON(t, w, map[string]interface{}{"login": "alice"})
})
defer server.Close()
if err := runUserShortcut(t, server, "current", nil); err != nil {
t.Fatalf("current shortcut failed: %v", err)
}
}
func TestUserInfo(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/users/alice.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(w, map[string]interface{}{
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertUserRequest(t, r, "GET", "/users/alice.json")
writeUserJSON(t, w, map[string]interface{}{
"login": "alice",
"name": "Alice",
})
}))
})
defer server.Close()
err := runShortcut(t, server, "info", map[string]string{"login": "alice"})
if err := runUserShortcut(t, server, "info", map[string]string{"login": "alice"}); err != nil {
t.Fatalf("info shortcut failed: %v", err)
}
}
func TestUserKeys(t *testing.T) {
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertUserRequest(t, r, "GET", "/public_keys.json")
assertUserQuery(t, r, "page", "2")
assertUserQuery(t, r, "limit", "50")
writeUserJSON(t, w, map[string]interface{}{"total_count": 0, "public_keys": []interface{}{}})
})
defer server.Close()
err := runUserShortcut(t, server, "keys", map[string]string{
"page": "2",
"limit": "50",
})
if err != nil {
t.Fatalf("info failed: %v", err)
t.Fatalf("keys shortcut failed: %v", err)
}
}
func TestUserKeyCreatePayload(t *testing.T) {
var payload map[string]interface{}
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertUserRequest(t, r, "POST", "/public_keys.json")
payload = decodeUserJSON(t, r)
writeUserJSON(t, w, map[string]interface{}{"id": 1})
})
defer server.Close()
err := runUserShortcut(t, server, "key-create", map[string]string{
"title": "work laptop",
"key": "ssh-rsa AAAA test@example.com",
})
if err != nil {
t.Fatalf("key-create shortcut failed: %v", err)
}
assertUserEqual(t, payload["title"], "work laptop")
assertUserEqual(t, payload["key"], "ssh-rsa AAAA test@example.com")
}
func TestUserKeyDelete(t *testing.T) {
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertUserRequest(t, r, "DELETE", "/public_keys/7.json")
writeUserJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
if err := runUserShortcut(t, server, "key-delete", map[string]string{"id": "7"}); err != nil {
t.Fatalf("key-delete shortcut failed: %v", err)
}
}
func TestUserKeyDryRunDoesNotCallAPI(t *testing.T) {
dryRunCases := []struct {
name string
args map[string]string
}{
{name: "key-create", args: map[string]string{"title": "work laptop", "key": "ssh-rsa AAAA", "dry-run": "true"}},
{name: "key-delete", args: map[string]string{"id": "7", "dry-run": "true"}},
}
for _, tc := range dryRunCases {
t.Run(tc.name, func(t *testing.T) {
server := newUserTestServer(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()
if err := runUserShortcut(t, server, tc.name, tc.args); err != nil {
t.Fatalf("%s dry-run failed: %v", tc.name, err)
}
})
}
}
func TestUserStatsShortcuts(t *testing.T) {
tests := []struct {
name string
args map[string]string
path string
queryKey string
queryValue string
queryKey2 string
queryValue2 string
}{
{name: "activity", args: map[string]string{"login": "alice"}, path: "/users/alice/statistics/activity.json"},
{name: "headmap", args: map[string]string{"login": "alice", "year": "2026"}, path: "/users/alice/headmaps.json", queryKey: "year", queryValue: "2026"},
{name: "develop", args: map[string]string{"login": "alice", "start-time": "100", "end-time": "200"}, path: "/users/alice/statistics/develop.json", queryKey: "start_time", queryValue: "100", queryKey2: "end_time", queryValue2: "200"},
{name: "role", args: map[string]string{"login": "alice", "start-time": "100", "end-time": "200"}, path: "/users/alice/statistics/role.json", queryKey: "start_time", queryValue: "100", queryKey2: "end_time", queryValue2: "200"},
{name: "major", args: map[string]string{"login": "alice", "start-time": "100", "end-time": "200"}, path: "/users/alice/statistics/major.json", queryKey: "start_time", queryValue: "100", queryKey2: "end_time", queryValue2: "200"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertUserRequest(t, r, "GET", tc.path)
if tc.queryKey != "" {
assertUserQuery(t, r, tc.queryKey, tc.queryValue)
}
if tc.queryKey2 != "" {
assertUserQuery(t, r, tc.queryKey2, tc.queryValue2)
}
writeUserJSON(t, w, map[string]interface{}{"status": 0})
})
defer server.Close()
if err := runUserShortcut(t, server, tc.name, tc.args); err != nil {
t.Fatalf("%s shortcut failed: %v", tc.name, err)
}
})
}
}
func TestUserInfoMissingLogin(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
}))
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("missing login should not call API")
})
defer server.Close()
err := runShortcut(t, server, "info", map[string]string{})
err := runUserShortcut(t, server, "info", map[string]string{})
if err == nil {
t.Fatal("expected error for missing login")
}
}
// --- HTTP error paths ---
func TestUserMeHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
func TestUserKeyCreateRejectsMissingKey(t *testing.T) {
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("missing key should not call API, got %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runShortcut(t, server, "me", nil)
err := runUserShortcut(t, server, "key-create", map[string]string{"title": "work laptop"})
if err == nil {
t.Fatal("expected missing key to return an error")
}
}
func TestUserMeHTTPError(t *testing.T) {
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
if _, err := w.Write([]byte("server error")); err != nil {
t.Fatalf("write response: %v", err)
}
})
defer server.Close()
err := runUserShortcut(t, server, "me", nil)
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
func TestUserInfoHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
if _, err := w.Write([]byte("server error")); err != nil {
t.Fatalf("write response: %v", err)
}
})
defer server.Close()
err := runShortcut(t, server, "info", map[string]string{"login": "alice"})
err := runUserShortcut(t, server, "info", map[string]string{"login": "alice"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
func TestUserShortcutNames(t *testing.T) {
got := map[string]bool{}
for _, shortcut := range Shortcuts() {
got[shortcut.Name] = true
}
want := []string{"me", "current", "info", "keys", "key-create", "key-delete", "activity", "headmap", "develop", "role", "major"}
for _, name := range want {
if !got[name] {
t.Fatalf("missing shortcut %q in %v", name, got)
}
}
if len(got) != len(want) {
t.Fatalf("shortcut count = %d, want %d: %v", len(got), len(want), got)
}
}
func runUserShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
shortcut := findUserShortcut(t, name)
ctx := &common.RuntimeContext{
Client: &client.Client{
HTTP: server.Client(),
BaseURL: server.URL,
},
Owner: "owner",
Repo: "repo",
Format: "json",
Args: args,
}
if ctx.Args == nil {
ctx.Args = map[string]string{}
}
return shortcut.Run(ctx)
}
func findUserShortcut(t *testing.T, name string) *common.Shortcut {
t.Helper()
for _, shortcut := range Shortcuts() {
if shortcut.Name == name {
return shortcut
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func newUserTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func assertUserRequest(t *testing.T, r *http.Request, method, path string) {
t.Helper()
if r.Method != method || r.URL.Path != path {
t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path)
}
}
func assertUserQuery(t *testing.T, r *http.Request, key, want string) {
t.Helper()
if got := r.URL.Query().Get(key); got != want {
t.Fatalf("query %s = %q, want %q", key, got, want)
}
}
func decodeUserJSON(t *testing.T, r *http.Request) map[string]interface{} {
t.Helper()
var payload map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
return payload
}
func writeUserJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(payload); err != nil {
t.Fatalf("failed to write response: %v", err)
}
}
func assertUserEqual(t *testing.T, got interface{}, want interface{}) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
}
}
func ExampleShortcuts() {
for _, shortcut := range Shortcuts() {
fmt.Println(shortcut.Name)
}
// Output:
// me
// current
// info
// keys
// key-create
// key-delete
// activity
// headmap
// develop
// role
// major
}

View File

@ -146,7 +146,7 @@ skills/
| Skill | 说明 | 常用命令 |
|-------|------|----------|
| **gitlink-search** | 搜索功能 | `search +repos`, `search +users` |
| **gitlink-user** | 用户管理 | `user +me`, `user +info` |
| **gitlink-user** | 用户管理 | `user +me`, `user +current`, `user +keys`, `user +activity` |
| **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` |
| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` |
| **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` |

View File

@ -1,7 +1,7 @@
---
name: gitlink-user
version: 1.0.0
description: "用户操作:查看当前用户、用户详情。当用户需要查看 GitLink 用户信息时触发。"
description: "用户操作:查看当前用户、用户详情、Public Keys 和用户统计。当用户需要查看或管理 GitLink 用户信息时触发。"
metadata:
requires:
bins: ["gitlink-cli"]
@ -21,27 +21,42 @@ metadata:
| Shortcut | 说明 | 需要认证 |
|----------|------|----------|
| `user +me` | 当前登录用户 | 是 |
| `user +current` | 当前用户详细资料 | 是 |
| `user +info` | 查看用户详情 | 否 |
| `user +keys` | 当前用户 Public Keys 列表 | 是 |
| `user +key-create` | 创建 Public Key | 是 |
| `user +key-delete` | 删除 Public Key | 是 |
| `user +activity` | 用户近期活动统计 | 否 |
| `user +headmap` | 用户贡献热力图 | 否 |
| `user +develop` | 用户开发能力统计 | 否 |
| `user +role` | 用户角色统计 | 否 |
| `user +major` | 用户专业定位统计 | 否 |
## 使用示例
```bash
# 查看当前用户
gitlink-cli user +me
gitlink-cli user +current --format json
# 查看其他用户
gitlink-cli user +info --login zhangsan
```
## Raw API 补充
```bash
# 用户贡献热力图
gitlink-cli api GET /users/:user_id/headmaps
# Public Key 管理,写入/删除前先 dry-run
gitlink-cli user +keys --page 1 --limit 20
gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run
gitlink-cli user +key-delete --id 7 --dry-run
# 用户统计
gitlink-cli api GET /users/:user_id/statistics
# 用户项目动态
gitlink-cli api GET /users/:user_id/project_trends
gitlink-cli user +activity --login zhangsan
gitlink-cli user +headmap --login zhangsan --year 2026
gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599
gitlink-cli user +role --login zhangsan --start-time 1704067200 --end-time 1735689599
gitlink-cli user +major --login zhangsan --start-time 1704067200 --end-time 1735689599
```
## API 注意事项
- `user +key-create` 支持 `--key` 直接传入公钥内容,也支持 `--key-file` 从本地公钥文件读取。
- `user +key-create``user +key-delete` 支持 `--dry-run`,写入/删除前建议先预览请求。
- 统计命令使用用户 `login`,不是数字用户 ID。