Compare commits

..

1 Commits

Author SHA1 Message Date
co63oc eece6bc53c feat(shortcuts): add user shortcuts 2026-06-16 21:39:24 +08:00
18 changed files with 3188 additions and 1302 deletions

View File

@ -114,7 +114,6 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
| 🔧 CI | View builds, logs, CI/CD operations |
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
| 🔔 Webhook | Manage repo webhooks and test deliveries |
| 📖 Wiki | List, view, create, update, and delete wiki pages |
| 🔍 Search | Search repositories, users |
| 📊 Dataset | Query research datasets by project |
| 👤 User | View user profiles and info |
@ -274,28 +273,6 @@ gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68
```
### Wiki Management
```bash
# List wiki pages (table of contents)
gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345
# View a wiki page by page name
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home
# Create a wiki page
gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \
-n getting-started -t "Getting Started" -c "# Getting Started Guide"
# Update a wiki page title and/or content
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "New Title"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -c "# Updated content"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "New Title" -c "New content"
# Delete a wiki page
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page
```
### Member Management
```bash

View File

@ -113,7 +113,6 @@
| 🏢 组织 | 管理组织、成员、团队 |
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
| 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 |
| 🔍 搜索 | 搜索仓库、用户 |
| 📊 数据集 | 按项目查询科研数据集 |
| 👤 用户 | 查看用户资料和信息 |
@ -285,28 +284,6 @@ gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68
```
### Wiki 管理
```bash
# 列出 Wiki 页面(目录结构)
gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345
# 查看 Wiki 页面
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home
# 创建 Wiki 页面
gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \
-n getting-started -t "快速开始" -c "# 快速开始指南"
# 更新 Wiki 页面标题和/或内容
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -c "# 更新后的内容"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题" -c "新内容"
# 删除 Wiki 页面
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page
```
### 成员管理
```bash

View File

@ -1,23 +0,0 @@
# Wiki Shortcut
新增 `wiki` Shortcut 组,支持 Wiki 页面管理:
- `wiki +list` - 列出 Wiki 页面(目录结构)
- `wiki +view` - 按页面名称查看 Wiki 页面详情
- `wiki +create` - 创建新的 Wiki 页面
- `wiki +update` - 更新 Wiki 页面标题和/或内容
- `wiki +delete` - 删除 Wiki 页面
## 实现要点
- **API 端点**:基于 `/api/wiki/open/{action}` 扁平路径结构,覆盖 5 个 Wiki 管理接口:
- `GET /api/wiki/open/wikiPages` — 目录列表
- `GET /api/wiki/open/getWiki` — 查看页面
- `POST /api/wiki/open/createWiki` — 创建页面
- `PUT /api/wiki/open/updateWiki` — 更新页面
- `DELETE /api/wiki/open/deleteWiki` — 删除页面
- **标识方式**Wiki 页面通过 `pageName`slug标识所有操作需要 `projectId`GitLink 项目数字 ID
- **内容编码**:创建和更新时,内容自动进行 base64 编码后以 `content_base64` 字段发送
- **更新保护**`+update` 要求必须提供 `--title``--page-name``--content` 为可选
- **Shortcut 模式**:使用 `common.Shortcut` + `RuntimeContext` 框架,与其他模块保持一致

View File

@ -115,35 +115,22 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
}
// Check GitLink error-in-body pattern
// Support both {"status":N, "message":"..."} and gateway {"code":N, "msg":"..."}
var bodyCode float64
var bodyMsg string
if status, ok := raw["status"]; ok {
var statusCode float64
switch v := status.(type) {
case float64:
bodyCode = v
statusCode = v
case int:
bodyCode = float64(v)
statusCode = float64(v)
}
bodyMsg, _ = raw["message"].(string)
} else if code, ok := raw["code"]; ok {
switch v := code.(type) {
case float64:
bodyCode = v
case int:
bodyCode = float64(v)
}
bodyMsg, _ = raw["msg"].(string)
if bodyMsg == "" {
bodyMsg, _ = raw["message"].(string)
}
}
if bodyCode != 0 && bodyCode != 200 && bodyCode != 201 && bodyCode != 204 && bodyCode != 1 {
suggestion := suggestFix(int(bodyCode))
return output.ErrorEnvelope(int(bodyCode), bodyMsg, suggestion), &APIError{
StatusCode: int(bodyCode),
Code: int(bodyCode),
Message: bodyMsg,
if statusCode != 0 && statusCode != 200 && statusCode != 1 {
msg, _ := raw["message"].(string)
suggestion := suggestFix(int(statusCode))
return output.ErrorEnvelope(int(statusCode), msg, suggestion), &APIError{
StatusCode: int(statusCode),
Code: int(statusCode),
Message: msg,
}
}
}
@ -183,10 +170,6 @@ func shouldAppendJSONSuffix(path string) bool {
return false
}
}
// Wiki open API endpoints do not use .json suffix
if len(parts) >= 3 && parts[0] == "wiki" && parts[1] == "open" {
return false
}
return true
}

View File

@ -169,45 +169,6 @@ func TestClientDoStatusError(t *testing.T) {
}
}
func TestClientDoGatewayCodeError(t *testing.T) {
// Gateway returns {"code":N, "msg":"..."} instead of {"status":N, "message":"..."}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"code":400,"msg":"Bad Request"}`))
}))
defer server.Close()
c := &Client{HTTP: server.Client(), BaseURL: server.URL}
env, err := c.Do("GET", "/api/test", nil, nil)
if err == nil {
t.Fatal("expected error for code=400")
}
if env == nil {
t.Fatal("expected envelope for code error")
}
if env.OK {
t.Fatal("expected OK=false for code=400")
}
}
func TestClientDoGatewayCode201Success(t *testing.T) {
// Gateway returns code=201 with JSON string data — should be treated as success
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"code":201,"msg":"","data":"{\"title\":\"test\"}"}`))
}))
defer server.Close()
c := &Client{HTTP: server.Client(), BaseURL: server.URL}
env, err := c.Do("POST", "/api/test", map[string]string{"title": "test"}, nil)
if err != nil {
t.Fatalf("unexpected error for code=201: %v", err)
}
if !env.OK {
t.Fatal("expected OK=true for code=201")
}
}
func TestClientDoStatusZero(t *testing.T) {
// status=0, 200, 1 are treated as success
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -564,18 +525,3 @@ func TestShouldAppendJSONSuffixSkipsExistingJSONPath(t *testing.T) {
t.Fatal("existing .json path should not get another suffix")
}
}
func TestShouldAppendJSONSuffixSkipsWikiOpenPaths(t *testing.T) {
paths := []string{
"/wiki/open/createWiki",
"/wiki/open/getWiki",
"/wiki/open/updateWiki",
"/wiki/open/deleteWiki",
"/wiki/open/wikiPages",
}
for _, p := range paths {
if shouldAppendJSONSuffix(p) {
t.Errorf("wiki/open path %q should not get .json suffix", p)
}
}
}

View File

@ -8,25 +8,22 @@ import (
)
const (
DefaultBaseURL = "https://www.gitlink.org.cn/api"
DefaultGatewayURL = "https://gateway.gitlink.org.cn/api"
DefaultFormat = "table"
DefaultBaseURL = "https://www.gitlink.org.cn/api"
DefaultFormat = "table"
)
type Config struct {
BaseURL string `yaml:"base_url"`
GatewayURL string `yaml:"gateway_url"`
Format string `yaml:"default_format"`
Editor string `yaml:"editor,omitempty"`
Pager string `yaml:"pager,omitempty"`
Lang string `yaml:"lang,omitempty"`
BaseURL string `yaml:"base_url"`
Format string `yaml:"default_format"`
Editor string `yaml:"editor,omitempty"`
Pager string `yaml:"pager,omitempty"`
Lang string `yaml:"lang,omitempty"`
}
func DefaultConfig() *Config {
return &Config{
BaseURL: DefaultBaseURL,
GatewayURL: DefaultGatewayURL,
Format: DefaultFormat,
BaseURL: DefaultBaseURL,
Format: DefaultFormat,
}
}
@ -57,9 +54,6 @@ func Load() (*Config, error) {
if cfg.BaseURL == "" {
cfg.BaseURL = DefaultBaseURL
}
if cfg.GatewayURL == "" {
cfg.GatewayURL = DefaultGatewayURL
}
if cfg.Format == "" {
cfg.Format = DefaultFormat
}
@ -86,8 +80,6 @@ func Get(key string) (string, error) {
switch key {
case "base_url":
return cfg.BaseURL, nil
case "gateway_url":
return cfg.GatewayURL, nil
case "default_format":
return cfg.Format, nil
case "editor":
@ -109,8 +101,6 @@ func Set(key, value string) error {
switch key {
case "base_url":
cfg.BaseURL = value
case "gateway_url":
cfg.GatewayURL = value
case "default_format":
cfg.Format = value
case "editor":

View File

@ -267,5 +267,105 @@
"success.config.set": "✓ {key} = {value}",
"warning.auth.not_logged_in": "✗ Not logged in",
"warning.auth.token_unverified": "✓ Token stored (but cannot verify: {message})",
"warning.auth.user_unavailable": "✓ Token stored (user info unavailable)"
}
"warning.auth.user_unavailable": "✓ Token stored (user info unavailable)",
"cmd.user.update.short": "Update user profile",
"cmd.user.update.long": "Update user nickname, gender, province, city, description and department.",
"cmd.user.projects.short": "List user projects",
"cmd.user.projects.long": "List projects for a given user with category, sort and search filters.",
"flag.user.nickname": "User nickname",
"flag.user.gender": "User gender (integer)",
"flag.user.province": "User province",
"flag.user.city": "User city",
"flag.user.description": "User bio / description",
"flag.user.custom_department": "User department / organization",
"flag.user.projects_category": "Project category: all/join/created/manage/only_watched/watched/forked",
"flag.user.projects_is_public": "Filter by public visibility",
"flag.user.projects_project_type": "Project type: common/mirror/sync_mirror",
"flag.user.projects_search": "Search keyword",
"cmd.user.current_info.short": "Show current user info (detailed)",
"cmd.user.current_info.long": "Fetch detailed information for the currently authenticated user via /api/users/get_user_info.json.",
"flag.user.projects_start_at": "Start timestamp (only for watched/only_watched/forked categories)",
"flag.user.projects_end_at": "End timestamp (only for watched/only_watched/forked categories)",
"cmd.user.messages_list.short": "List user messages",
"cmd.user.messages_list.long": "List messages for the current user, with optional type and status filters.",
"cmd.user.messages_read.short": "Mark messages as read",
"cmd.user.messages_read.long": "Mark specified messages as read. Use --ids=-1 to mark all unread messages as read.",
"cmd.user.messages_delete.short": "Delete messages",
"cmd.user.messages_delete.long": "Delete specified messages. Use --ids=-1 to delete all messages of the given type.",
"flag.user.messages_type": "Message type: notification or atme (default: all)",
"flag.user.messages_status": "Read status: 1=unread, 2=read (default: all)",
"flag.user.messages_ids": "Comma-separated message IDs (use -1 for all)",
"cmd.user.messages_create.short": "Create a message (@me notification)",
"cmd.user.messages_create.long": "Create a message notification for specified users. Requires --receivers (comma-separated logins), --atmeable-type (Journal/Issue/PullRequest), and --atmeable-id.",
"cmd.user.platform_settings.short": "Get platform message settings",
"cmd.user.platform_settings.long": "Get the platform-wide message setting templates, showing all configurable notification categories.",
"cmd.user.user_settings.short": "Get user message settings",
"cmd.user.user_settings.long": "Get the current user's notification and email settings for all message categories.",
"flag.user.messages_receivers": "Comma-separated receiver login names",
"flag.user.messages_atmeable_type": "AT target type: Journal, Issue, or PullRequest",
"flag.user.messages_atmeable_id": "AT target object ID",
"cmd.user.update_settings.short": "Update user message settings",
"cmd.user.update_settings.long": "Update user notification and email settings, toggling each category on or off.",
"flag.user.settings_notification": "Comma-separated categories to enable for in-app notifications (e.g. CreateOrAssign::IssueAssigned,ManageProject::Issue)",
"flag.user.settings_email": "Comma-separated categories to enable for email notifications",
"flag.user.settings_notification_off": "Comma-separated categories to disable for in-app notifications",
"flag.user.settings_email_off": "Comma-separated categories to disable for email notifications",
"cmd.user.pinned_list.short": "List pinned projects",
"cmd.user.pinned_list.long": "List the user's pinned/starred projects with basic info and sort position.",
"cmd.user.pinned_update.short": "Update pinned projects",
"cmd.user.pinned_update.long": "Batch-set the user's pinned/starred projects by passing a list of project IDs.",
"flag.user.pinned_project_ids": "Comma-separated project IDs to pin",
"cmd.user.pinned_sort.short": "Reorder a pinned project",
"cmd.user.pinned_sort.long": "Adjust the display order of a pinned project. Higher position = higher rank.",
"flag.user.pinned_id": "Pinned project ID",
"flag.user.pinned_position": "Sort position (higher number = higher rank)",
"cmd.user.feedback.short": "Submit feedback",
"cmd.user.feedback.long": "Submit feedback or suggestions to the GitLink platform.",
"flag.user.feedback_content": "Feedback content",
"cmd.user.transfer_list.short": "List project transfer requests",
"cmd.user.transfer_list.long": "List pending project transfer requests for the specified user, with optional pagination.",
"cmd.user.transfer_accept.short": "Accept project transfer",
"cmd.user.transfer_accept.long": "Accept a project transfer request initiated by the specified user.",
"flag.user.transfer_id": "Transfer request ID",
"cmd.user.transfer_refuse.short": "Refuse project transfer",
"cmd.user.transfer_refuse.long": "Refuse a project transfer request initiated by the specified user.",
"cmd.user.send_email_code.short": "Send email verification code",
"cmd.user.send_email_code.long": "Send a verification code to the specified email for email change.",
"flag.user.email_code_type": "Verification code type (10: update email)",
"flag.user.email": "Target email address",
"flag.user.email_md5": "Email MD5 hash value",
"cmd.user.verify_email_code.short": "Verify email code",
"cmd.user.verify_email_code.long": "Verify the email verification code received.",
"flag.user.verify_code": "Verification code",
"cmd.user.check_password.short": "Verify user password",
"cmd.user.check_password.long": "Verify the current user login password is correct.",
"flag.user.password": "User password",
"cmd.user.applied_list.short": "List project applications",
"cmd.user.applied_list.long": "List pending project join applications (paginated).",
"flag.user.applied_page": "Page number",
"flag.user.applied_per_page": "Items per page",
"cmd.user.applied_accept.short": "Accept project application",
"cmd.user.applied_accept.long": "Accept the specified user project join application.",
"flag.user.applied_id": "Application ID",
"cmd.user.applied_refuse.short": "Refuse project application",
"cmd.user.applied_refuse.long": "Refuse the specified user project join application.",
"cmd.user.check_email.short": "Check email availability",
"cmd.user.check_email.long": "Check if the specified email is already in use.",
"cmd.user.update_email.short": "Change email",
"cmd.user.update_email.long": "Change the user bound email (requires password and email verification code).",
"flag.user.email_code": "Email verification code",
"cmd.user.update_phone.short": "Change phone number",
"cmd.user.update_phone.long": "Change the user bound phone number (requires SMS code).",
"flag.user.phone": "Target phone number",
"flag.user.phone_code": "SMS verification code",
"cmd.user.change_password.short": "Change password",
"cmd.user.change_password.long": "Change the current user login password.",
"flag.user.old_password": "Current password",
"flag.user.new_password": "New password",
"flag.user.new_password_repeat": "Confirm new password",
"cmd.user.check_delete.short": "Check if account can be deleted",
"cmd.user.check_delete.long": "Check if the current user meets deletion criteria (no orgs/projects).",
"cmd.user.delete_account.short": "Delete account",
"cmd.user.delete_account.long": "Permanently delete the user account (irreversible). Confirm before executing.",
"flag.user.delete_memo": "Deletion reason"
}

View File

@ -267,5 +267,105 @@
"success.config.set": "✓ 已设置 {key} = {value}",
"warning.auth.not_logged_in": "✗ 未登录",
"warning.auth.token_unverified": "✓ Token 已保存(但无法验证:{message}",
"warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)"
}
"warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)",
"cmd.user.update.short": "更新用户信息",
"cmd.user.update.long": "更新用户的昵称、性别、省份、城市、简介和单位等扩展信息。",
"cmd.user.projects.short": "列出用户项目",
"cmd.user.projects.long": "列出指定用户的项目列表,支持分类、排序和搜索筛选。",
"flag.user.nickname": "用户昵称",
"flag.user.gender": "用户性别(整数)",
"flag.user.province": "用户所在省份",
"flag.user.city": "用户所在城市",
"flag.user.description": "用户简介",
"flag.user.custom_department": "用户所在单位",
"flag.user.projects_category": "项目分类all/join/created/manage/only_watched/watched/forked",
"flag.user.projects_is_public": "是否为公有项目",
"flag.user.projects_project_type": "项目类型common/mirror/sync_mirror",
"flag.user.projects_search": "搜索关键词",
"cmd.user.current_info.short": "获取当前用户详细信息",
"cmd.user.current_info.long": "通过 /api/users/get_user_info.json 获取当前认证用户的详细资料。",
"flag.user.projects_start_at": "开始时间戳(仅对 watched/only_watched/forked 分类有效)",
"flag.user.projects_end_at": "结束时间戳(仅对 watched/only_watched/forked 分类有效)",
"cmd.user.messages_list.short": "列出用户消息",
"cmd.user.messages_list.long": "列出当前用户的消息,支持按类型和已读状态筛选。",
"cmd.user.messages_read.short": "标记消息为已读",
"cmd.user.messages_read.long": "将指定消息标记为已读。使用 --ids=-1 可将所有未读消息标记为已读。",
"cmd.user.messages_delete.short": "删除消息",
"cmd.user.messages_delete.long": "删除指定消息。使用 --ids=-1 可删除指定类型的所有消息。",
"flag.user.messages_type": "消息类型notification系统消息或 atme@我消息),不传为全部",
"flag.user.messages_status": "已读状态1=未读2=已读(不传为全部)",
"flag.user.messages_ids": "消息ID列表逗号分隔使用 -1 表示全部)",
"cmd.user.messages_create.short": "创建消息(@我通知)",
"cmd.user.messages_create.long": "为指定用户创建@我消息通知。需要 --receivers逗号分隔的用户名、--atmeable-typeJournal/Issue/PullRequest和 --atmeable-id。",
"cmd.user.platform_settings.short": "获取平台消息设置",
"cmd.user.platform_settings.long": "获取平台级别的消息设置模板,展示所有可配置的通知分类。",
"cmd.user.user_settings.short": "获取用户消息设置",
"cmd.user.user_settings.long": "获取当前用户所有消息分类的站内信和邮件设置。",
"flag.user.messages_receivers": "接收消息的用户名列表,逗号分隔",
"flag.user.messages_atmeable_type": "@目标类型Journal评论、Issue疑修、PullRequest合并请求",
"flag.user.messages_atmeable_id": "@目标对象ID",
"cmd.user.update_settings.short": "更新用户消息设置",
"cmd.user.update_settings.long": "更新用户的站内信和邮件通知设置,支持批量设置各分类的开关状态。",
"flag.user.settings_notification": "站内信设置,逗号分隔的分类列表(如 CreateOrAssign::IssueAssigned,ManageProject::Issue设为 true",
"flag.user.settings_email": "邮件设置,逗号分隔的分类列表,设为 true",
"flag.user.settings_notification_off": "站内信设置,逗号分隔的分类列表,设为 false",
"flag.user.settings_email_off": "邮件设置,逗号分隔的分类列表,设为 false",
"cmd.user.pinned_list.short": "列出用户星标项目",
"cmd.user.pinned_list.long": "列出用户精选/星标的项目列表,包含项目基本信息和排序位置。",
"cmd.user.pinned_update.short": "更新用户星标项目",
"cmd.user.pinned_update.long": "批量设置用户的星标/精选项目,传入项目 ID 列表。",
"flag.user.pinned_project_ids": "要设为星标的项目 ID 列表,逗号分隔",
"cmd.user.pinned_sort.short": "调整星标项目排序",
"cmd.user.pinned_sort.long": "调整指定星标项目的显示顺序,数字越大排名越靠前。",
"flag.user.pinned_id": "星标项目 ID",
"flag.user.pinned_position": "排序位置(数字越大越靠前)",
"cmd.user.feedback.short": "提交反馈建议",
"cmd.user.feedback.long": "向 GitLink 平台提交反馈建议或意见。",
"flag.user.feedback_content": "反馈内容",
"cmd.user.transfer_list.short": "列出项目迁移请求",
"cmd.user.transfer_list.long": "列出指定用户的待接受项目迁移请求,支持分页。",
"cmd.user.transfer_accept.short": "接受项目迁移",
"cmd.user.transfer_accept.long": "接受指定用户发起的项目迁移请求。",
"flag.user.transfer_id": "迁移请求 ID",
"cmd.user.transfer_refuse.short": "拒绝项目迁移",
"cmd.user.transfer_refuse.long": "拒绝指定用户发起的项目迁移请求。",
"cmd.user.send_email_code.short": "发送邮箱验证码",
"cmd.user.send_email_code.long": "向指定邮箱发送验证码,用于邮箱变更验证。",
"flag.user.email_code_type": "验证码类型10: 更新邮箱)",
"flag.user.email": "目标邮箱地址",
"flag.user.email_md5": "邮箱 MD5 加密值",
"cmd.user.verify_email_code.short": "验证邮箱验证码",
"cmd.user.verify_email_code.long": "验证用户收到的邮箱验证码是否正确。",
"flag.user.verify_code": "验证码",
"cmd.user.check_password.short": "验证用户密码",
"cmd.user.check_password.long": "验证当前用户的登录密码是否正确。",
"flag.user.password": "用户密码",
"cmd.user.applied_list.short": "列出项目申请",
"cmd.user.applied_list.long": "列出待处理的项目加入申请(分页)。",
"flag.user.applied_page": "页码",
"flag.user.applied_per_page": "每页个数",
"cmd.user.applied_accept.short": "接受项目申请",
"cmd.user.applied_accept.long": "接受指定用户的项目加入申请。",
"flag.user.applied_id": "申请 ID",
"cmd.user.applied_refuse.short": "拒绝项目申请",
"cmd.user.applied_refuse.long": "拒绝指定用户的项目加入申请。",
"cmd.user.check_email.short": "验证邮箱可用性",
"cmd.user.check_email.long": "检查指定邮箱是否已被占用。",
"cmd.user.update_email.short": "更改邮箱",
"cmd.user.update_email.long": "更改用户绑定的邮箱地址(需先验证密码和邮箱验证码)。",
"flag.user.email_code": "邮箱验证码",
"cmd.user.update_phone.short": "更改手机号",
"cmd.user.update_phone.long": "更改用户绑定的手机号(需手机验证码)。",
"flag.user.phone": "目标手机号",
"flag.user.phone_code": "手机验证码",
"cmd.user.change_password.short": "更改密码",
"cmd.user.change_password.long": "修改当前用户的登录密码。",
"flag.user.old_password": "当前密码",
"flag.user.new_password": "新密码",
"flag.user.new_password_repeat": "新密码确认",
"cmd.user.check_delete.short": "检测账号是否可注销",
"cmd.user.check_delete.long": "检测当前用户是否满足注销条件(无组织/项目)。",
"cmd.user.delete_account.short": "注销账号",
"cmd.user.delete_account.long": "永久删除用户账号(不可恢复)。执行前务必二次确认。",
"flag.user.delete_memo": "删除原因"
}

View File

@ -25,7 +25,6 @@ import (
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki"
"github.com/gitlink-org/gitlink-cli/shortcuts/workflow"
)
@ -54,7 +53,6 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"compare": compare.Shortcuts(),
"dataset": dataset.Shortcuts(tr),
"webhook": webhook.Shortcuts(tr),
"wiki": wiki.Shortcuts(),
"health": health.Shortcuts(tr),
"ignore": ignore.Shortcuts(),
"workflow": workflow.Shortcuts(),
@ -79,7 +77,6 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"compare": "Compare branches, tags, or commits",
"dataset": tr.T("cmd.dataset.short"),
"webhook": tr.T("cmd.webhook.short"),
"wiki": "Wiki page management",
"health": "Project health data collection",
"ignore": tr.T("cmd.ignore.short"),
"workflow": "AI agent workflow analysis",

View File

@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
"org", "user", "search", "ci", "workflow",
"compare", "member", "milestone", "pipeline", "webhook",
"dataset", "health", "ignore", "wiki",
"dataset", "health", "ignore",
}
groupSet := map[string]bool{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,200 +0,0 @@
package wiki
import (
"encoding/base64"
"fmt"
"net/url"
"github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// switchToGateway overrides the client base URL with the gateway URL from config.
func switchToGateway(ctx *common.RuntimeContext) error {
cfg, err := config.Load()
if err != nil {
return err
}
if cfg.GatewayURL == "" {
cfg.GatewayURL = config.DefaultGatewayURL
}
ctx.Client.BaseURL = cfg.GatewayURL
return nil
}
// gatewayFlag returns the common --gateway flag definition.
func gatewayFlag() common.Flag {
return common.Flag{Name: "gateway", Short: "g", Usage: "Use gateway API endpoint", Bool: true}
}
// Shortcuts returns all wiki shortcuts.
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
{
Name: "list",
Description: "List wiki pages",
Flags: []common.Flag{
{Name: "project-id", Usage: "GitLink project ID", Required: true},
gatewayFlag(),
},
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("owner", ctx.Owner)
q.Set("repo", ctx.Repo)
q.Set("projectId", ctx.Arg("project-id"))
env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/wikiPages", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "view",
Description: "View a wiki page by page name",
Flags: []common.Flag{
{Name: "project-id", Usage: "GitLink project ID", Required: true},
{Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true},
gatewayFlag(),
},
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("owner", ctx.Owner)
q.Set("repo", ctx.Repo)
q.Set("projectId", ctx.Arg("project-id"))
q.Set("pageName", ctx.Arg("page-name"))
env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/getWiki", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "create",
Description: "Create a new wiki page",
Flags: []common.Flag{
{Name: "project-id", Usage: "GitLink project ID", Required: true},
{Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true},
{Name: "title", Short: "t", Usage: "Wiki page title", Required: true},
{Name: "content", Short: "c", Usage: "Wiki page content (markdown)", Required: true},
{Name: "message", Short: "m", Usage: "Commit message"},
gatewayFlag(),
},
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
content := ctx.Arg("content")
payload := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": ctx.Arg("project-id"),
"pageName": ctx.Arg("page-name"),
"title": ctx.Arg("title"),
"content_base64": base64.StdEncoding.EncodeToString([]byte(content)),
"message": ctx.Arg("message"),
}
env, err := ctx.CallAPI("POST", "/wiki/open/createWiki", payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "update",
Description: "Update an existing wiki page",
Flags: []common.Flag{
{Name: "project-id", Usage: "GitLink project ID", Required: true},
{Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true},
{Name: "title", Short: "t", Usage: "Wiki page title", Required: true},
{Name: "content", Short: "c", Usage: "Wiki page content (markdown)"},
{Name: "message", Short: "m", Usage: "Commit message"},
gatewayFlag(),
},
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
title := ctx.Arg("title")
if title == "" {
return fmt.Errorf("--title is required")
}
content := ctx.Arg("content")
payload := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": ctx.Arg("project-id"),
"pageName": ctx.Arg("page-name"),
"title": title,
"message": ctx.Arg("message"),
}
if content != "" {
payload["content_base64"] = base64.StdEncoding.EncodeToString([]byte(content))
}
env, err := ctx.CallAPI("PUT", "/wiki/open/updateWiki", payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "delete",
Description: "Delete a wiki page",
Flags: []common.Flag{
{Name: "project-id", Usage: "GitLink project ID", Required: true},
{Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true},
gatewayFlag(),
},
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
payload := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": ctx.Arg("project-id"),
"pageName": ctx.Arg("page-name"),
}
env, err := ctx.CallAPI("DELETE", "/wiki/open/deleteWiki", payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
}
}

View File

@ -1,224 +0,0 @@
package wiki
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestWikiList(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/wiki/open/wikiPages")
assertEqual(t, r.URL.Query().Get("owner"), "owner")
assertEqual(t, r.URL.Query().Get("repo"), "repo")
assertEqual(t, r.URL.Query().Get("projectId"), "12345")
writeJSON(t, w, map[string]interface{}{"status": 0, "data": []interface{}{}})
}))
defer server.Close()
err := runWikiShortcut(t, server, "list", map[string]string{
"project-id": "12345",
})
if err != nil {
t.Fatalf("list shortcut failed: %v", err)
}
}
func TestWikiView(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/wiki/open/getWiki")
assertEqual(t, r.URL.Query().Get("owner"), "owner")
assertEqual(t, r.URL.Query().Get("repo"), "repo")
assertEqual(t, r.URL.Query().Get("projectId"), "12345")
assertEqual(t, r.URL.Query().Get("pageName"), "home")
writeJSON(t, w, map[string]interface{}{"status": 0, "data": map[string]interface{}{"title": "home"}})
}))
defer server.Close()
err := runWikiShortcut(t, server, "view", map[string]string{
"project-id": "12345",
"page-name": "home",
})
if err != nil {
t.Fatalf("view shortcut failed: %v", err)
}
}
func TestWikiCreate(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/wiki/open/createWiki")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
}))
defer server.Close()
err := runWikiShortcut(t, server, "create", map[string]string{
"project-id": "12345",
"page-name": "new-page",
"title": "New Page",
"content": "# Hello",
})
if err != nil {
t.Fatalf("create shortcut failed: %v", err)
}
assertEqual(t, payload["owner"], "owner")
assertEqual(t, payload["repo"], "repo")
assertEqual(t, payload["pageName"], "new-page")
assertEqual(t, payload["title"], "New Page")
if _, ok := payload["content_base64"]; !ok {
t.Fatal("body missing content_base64")
}
}
func TestWikiUpdate(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "PUT", "/wiki/open/updateWiki")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
}))
defer server.Close()
err := runWikiShortcut(t, server, "update", map[string]string{
"project-id": "12345",
"page-name": "home",
"title": "Updated Title",
})
if err != nil {
t.Fatalf("update shortcut failed: %v", err)
}
assertEqual(t, payload["owner"], "owner")
assertEqual(t, payload["pageName"], "home")
assertEqual(t, payload["title"], "Updated Title")
}
func TestWikiUpdateRequiresTitle(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("server should not be called when title is missing: %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runWikiShortcut(t, server, "update", map[string]string{
"project-id": "12345",
"page-name": "home",
})
if err == nil {
t.Fatal("expected update without --title to return an error")
}
if err.Error() != "--title is required" {
t.Fatalf("unexpected error message: %s", err.Error())
}
}
func TestWikiUpdateWithContentOnly(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "PUT", "/wiki/open/updateWiki")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
}))
defer server.Close()
err := runWikiShortcut(t, server, "update", map[string]string{
"project-id": "12345",
"page-name": "home",
"title": "Existing Title",
"content": "# Updated content",
})
if err != nil {
t.Fatalf("update with content failed: %v", err)
}
if _, ok := payload["content_base64"]; !ok {
t.Fatal("body missing content_base64 when --content provided")
}
}
func TestWikiDelete(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "DELETE", "/wiki/open/deleteWiki")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
}))
defer server.Close()
err := runWikiShortcut(t, server, "delete", map[string]string{
"project-id": "12345",
"page-name": "old-page",
})
if err != nil {
t.Fatalf("delete shortcut failed: %v", err)
}
assertEqual(t, payload["owner"], "owner")
assertEqual(t, payload["repo"], "repo")
assertEqual(t, payload["pageName"], "old-page")
}
func runWikiShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
shortcut := findWikiShortcut(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 findWikiShortcut(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 assertRequest(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 decodeJSON(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 writeJSON(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 assertEqual(t *testing.T, got interface{}, want interface{}) {
t.Helper()
if got != want {
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
}
}

View File

@ -32,7 +32,7 @@ gitlink-cli auth status
gitlink-cli user +me
```
详见: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md)
详见: [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md)
### 2. 查看可用命令
@ -64,19 +64,25 @@ skills/
├── README.md # 本文件
├── gitlink-shared/ # 共享基础规则
│ ├── SKILL.md # 认证、全局参数、安全规则、分支约定
└── references/
├── api-reference.md # API 详细参考、错误处理
├── raw-api-batch.md # 批量 Raw API 调用参考
│ └── troubleshooting.md # 常见问题排查
├── REFERENCE.md # API 详细参考、错误处理
├── TROUBLESHOOTING.md # 常见问题排查
└── examples/
│ └── auth-workflow.md # 认证工作流示例
├── gitlink-repo/ # 仓库管理
│ ├── SKILL.md # 仓库操作指南
│ └── references/ # 仓库命令参考文档
│ ├── REFERENCE.md # 仓库 API 参考
│ └── examples/
│ └── repo-workflow.md # 仓库管理工作流
├── gitlink-issue/ # Issue 管理
│ ├── SKILL.md # Issue 操作指南
│ └── references/ # Issue 命令参考文档
│ ├── REFERENCE.md # Issue API 参考
│ └── examples/
│ └── issue-workflow.md # Issue 全流程工作流
├── gitlink-pr/ # Pull Request
│ ├── SKILL.md # PR 操作指南
│ └── references/ # PR 命令参考文档
│ ├── REFERENCE.md # PR API 参考
│ └── examples/
│ └── pr-workflow.md # PR 工作流
├── gitlink-member/ # 仓库成员管理
│ └── SKILL.md # 成员与邀请链接操作指南
├── gitlink-branch/ # 分支管理
@ -85,24 +91,25 @@ skills/
│ └── branch-workflow.md # 分支工作流
├── gitlink-release/ # 版本发布
│ ├── SKILL.md # Release 操作指南
└── references/ # Release 命令参考文档
├── gitlink-release-auto/ # 自动化 Release 管理
└── SKILL.md # 自动发版、版本号推荐、Release Notes 生成
├── REFERENCE.md # Release API 参考
│ └── examples/
└── release-workflow.md # Release 工作流
├── gitlink-search/ # 搜索功能
│ ├── SKILL.md # 搜索操作指南
│ └── references/ # 搜索命令参考文档
│ └── examples/
│ └── search-workflow.md # 搜索工作流
├── gitlink-user/ # 用户管理
│ ├── SKILL.md # 用户操作指南
│ └── references/ # 用户命令参考文档
│ └── SKILL.md # 用户操作指南
├── gitlink-org/ # 组织管理
│ ├── SKILL.md # 组织操作指南
│ └── references/ # 组织命令参考文档
│ └── examples/
│ └── org-workflow.md # 组织工作流
├── gitlink-ci/ # CI/CD
│ └── SKILL.md # CI 操作指南
│ ├── SKILL.md # CI 操作指南
│ └── examples/
│ └── ci-workflow.md # CI 工作流
├── gitlink-pipeline/ # 流水线工作流
│ └── SKILL.md # Pipeline 操作指南
├── gitlink-wiki/ # Wiki 页面管理
│ └── SKILL.md # Wiki 操作指南
├── gitlink-pm/ # 项目管理
│ └── SKILL.md # PM 操作指南
├── gitlink-health/ # 项目健康度分析
@ -143,7 +150,6 @@ skills/
| **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` |
| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` |
| **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` |
| **gitlink-wiki** | Wiki 页面管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` |
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
@ -163,7 +169,7 @@ gitlink-cli repo +info
gitlink-cli repo +info --owner wbtiger --repo gitlink-cli
```
详见: [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md)
详见: [gitlink-repo/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md)
### 场景 2创建和管理 Issue
@ -184,7 +190,7 @@ gitlink-cli issue +close -i 123
gitlink-cli issue +batch-close --numbers 123,124 --dry-run
```
详见: [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md)
详见: [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md)
### 场景 3管理分支和发布
@ -202,7 +208,7 @@ gitlink-cli release +create -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..."
gitlink-cli release +view -i <version_id>
```
详见: [gitlink-release/SKILL.md](gitlink-release/SKILL.md)
详见: [gitlink-release/examples/release-workflow.md](gitlink-release/examples/release-workflow.md)
### 场景 4搜索和发现
@ -218,7 +224,7 @@ gitlink-cli org +list
gitlink-cli org +info -i Gitlink
```
详见: [gitlink-search/SKILL.md](gitlink-search/SKILL.md)
详见: [gitlink-search/examples/search-workflow.md](gitlink-search/examples/search-workflow.md)
---
@ -227,8 +233,8 @@ gitlink-cli org +info -i Gitlink
### 快速查找
- **我想了解认证**: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md)
- **我想查看 API 细节**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
- **我遇到了错误**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
- **我想查看 API 细节**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
- **我遇到了错误**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
- **我想看工作流示例**: 查看各 Skill 下的 `examples/` 目录
### 按功能分类
@ -236,12 +242,12 @@ gitlink-cli org +info -i Gitlink
**仓库操作**:
- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 仓库命令
- [gitlink-branch/SKILL.md](gitlink-branch/SKILL.md) - 分支命令
- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 完整工作流
- [gitlink-repo/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md) - 完整工作流
**Issue 和 PR**:
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 命令
- [gitlink-pr/SKILL.md](gitlink-pr/SKILL.md) - PR 命令
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 工作流
- [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md) - Issue 工作流
**发布和搜索**:
- [gitlink-release/SKILL.md](gitlink-release/SKILL.md) - Release 命令
@ -283,11 +289,11 @@ gitlink-cli auth login
### Q: 如何查看完整的 API 参考?
A: 查看 [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
A: 查看 [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
### Q: 遇到错误怎么办?
A: 查看 [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
A: 查看 [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
---
@ -323,7 +329,7 @@ AI 代理可以:
- 所有边界情况处理正确
- 完整的文档和示例
详见: [../doc/design.md](../doc/design.md)
详见: [../doc/SKILLS_TEST_REPORT_2026-04-02.md](../doc/SKILLS_TEST_REPORT_2026-04-02.md)
---
@ -331,7 +337,8 @@ AI 代理可以:
- [主项目 README](../README.md) - gitlink-cli 项目说明
- [设计文档](../doc/design.md) - 架构设计和开发计划
- [API 参考文档](../doc/gitlink_api_reference.md) - GitLink API 参考文档
- [测试报告](../doc/SKILLS_TEST_REPORT_2026-04-02.md) - 功能测试报告
- [代码同步方案](../doc/CODE_SYNC_STRATEGY_FINAL.md) - GitHub ↔ GitLink 同步设计
- [gitlink-bisync](https://www.gitlink.org.cn/wbtiger/gitlink-bisync) - 代码双向同步系统
---
@ -339,8 +346,8 @@ AI 代理可以:
## 📞 获取帮助
- **命令帮助**: `gitlink-cli <command> --help`
- **故障排查**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
- **API 参考**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
- **故障排查**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
- **API 参考**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
- **工作流示例**: 查看各 Skill 下的 `examples/` 目录
---
@ -348,7 +355,7 @@ AI 代理可以:
## 🎓 下一步
1. 阅读 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 了解基础
2. 查看 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 完成认证
2. 查看 [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md) 完成认证
3. 根据需求选择相应的 Skill 文档
4. 参考 `examples/` 目录中的工作流示例
5. 使用 AI 代理自动化你的工作流

View File

@ -1,586 +0,0 @@
---
name: gitlink-competition-manager
version: 1.0.0
description: "编程竞赛管理批量创建队伍仓库、发布题目、追踪提交、生成排行榜、赛后归档。当用户需要管理编程竞赛、ACM 校内赛等竞赛时触发。"
metadata:
requires:
bins: ["gitlink-cli"]
cliHelp: "gitlink-cli issue --help"
---
# gitlink-competition-manager编程竞赛管理
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。**
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`GitHub CLI操作 GitLink 资源。**
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。
---
## 功能概述
本技能覆盖编程竞赛的完整管理流程:
1. **队伍管理** — 批量创建队伍仓库,配置参赛权限
2. **题目发布** — 创建题目 Issue 模板,设置截止时间
3. **提交追踪** — 监控各队伍提交记录,锁定最终版本
4. **排行榜生成** — 按通过率/用时/代码质量评分排序
5. **防作弊检测** — 跨队伍代码相似度比对 + 提交时间异常检测
6. **赛后归档** — 获奖队伍标记 + 优秀代码展示 + 仓库归档
---
## 一、赛前准备
### 1.1 获取组织信息
```bash
# 获取组织 ID用于批量创建仓库如果属于多个组织需要用户手动选择使用哪个组织
gitlink-cli api GET /api/organizations --format json
# AI 匹配竞赛组织名称,获取组织 ID
```
### 1.2 批量创建队伍仓库
```bash
# 参见 gitlink-batch-repo-create Skill本 Skill 复用其创建逻辑
# 输入格式:队伍清单 CSV 或者 Excel
# CSV 格式示例:(名字后面为参赛账号)
# team_id,team_name,leader,members,repo_name
# T001,算法之光,张三(zhangsan),"张三(zhangsan);李四(lisi);王五(wangwu)",algo-light
# T002,代码刺客,赵六(zhaoliu),"赵六(zhaoliu);钱七(qianqi)",code-assassin
# 创建仓库
gitlink-cli api POST /api/projects --body '{
"user_id": <组织ID>,
"name": "<队伍名>",
"repository_name": "<仓库标识>",
"description": "<竞赛名> - <队伍名> 参赛仓库",
"private": true
}' --format json
# 初始化仓库(可选:竞赛模板)
gitlink-cli api POST /v1/:owner/:repo/contents --body '{
"path": "README.md",
"content": "<base64编码的模板内容>",
"message": "Initialize competition repo",
"branch": "master"
}' --format json
```
**仓库目录结构模板**
```
<repo>/
├── README.md # 队伍信息
├── problems/
│ ├── P001/ # 题目1解答
│ │ ├── solution.py
│ │ └── README.md # 解题思路
│ ├── P002/ # 题目2解答
│ └── ...
├── tests/ # 自测用例
└── .gitignore
```
### 1.3 添加队伍成员为协作者
```bash
# 根据队员用户名获取对应的user_id
gitlink-cli user +info --login <member_login_name> --format json
# 为每个队伍仓库添加成员
gitlink-cli api POST /api/:owner/:repo/collaborators --body "{\"user_id\":\"<member_user_id>\"}" --format json
```
---
## 二、题目发布
### 2.1 创建题目标签
```bash
# 确保竞赛标签存在
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
# 创建题目标签体系
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"题目","description":"竞赛题目","color":"#0075ca"}' --format json
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"已通过","description":"题目已通过","color":"#0e8a16"}' --format json
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"未通过","description":"题目未通过","color":"#b60205"}' --format json
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"待评测","description":"等待评测","color":"#fbca04"}' --format json
```
### 2.2 发布全部题目Issue 模板从用户对话信息中提取如果没有提到发布题目则不发布题目issue
```bash
# 在竞赛主仓库创建题目 Issue
gitlink-cli issue +create \
--owner <owner> --repo <main_repo> \
--title "【题目 P001】两数之和" \
--body "## 题目描述
给定一个整数数组 nums 和一个整数目标值 target请你在该数组中找出和为目标值的那两个整数并返回它们的数组下标。
### 输入格式
第一行n target
第二行n 个整数
### 输出格式
两个下标(空格分隔)
### 样例输入
4 9
2 7 11 15
### 样例输出
0 1
### 数据范围
- 2 ≤ n ≤ 10^4
- -10^9 ≤ nums[i] ≤ 10^9
- 只有一个有效答案
### 分值
100 分
### 提交方式
在 problems/P001/ 目录下提交代码,向 master 发起 PR
### 截止时间
2024-07-15 15:00:00" \
--format json
# 打"题目"标签
gitlink-cli api PATCH /v1/:owner/:repo/issues/:id --body '{"issue_tag_ids":[<tag_id>]}' --format json
```
### 2.3 题目发布清单
```bash
# 获取所有题目 Issue
gitlink-cli issue +list --state open --owner <owner> --repo <main_repo> --format json
# AI 过滤标题以"【题目"开头的 Issue
```
**题目清单格式**
```
=== 竞赛题目清单 ===
竞赛2024 校内算法竞赛
题目数5
发布时间2024-07-15 09:00
| 题号 | 标题 | 分值 | 截止时间 | 难度 |
|------|------|------|---------|------|
| P001 | 两数之和 | 100 | 15:00 | 🟢 简单 |
| P002 | 最长回文子串 | 150 | 15:00 | 🟡 中等 |
| P003 | 合并K个有序链表 | 200 | 15:00 | 🟠 较难 |
| P004 | 最短路径 | 200 | 15:00 | 🟠 较难 |
| P005 | 动态规划优化 | 350 | 15:00 | 🔴 困难 |
总分1000 分
```
---
## 三、提交追踪
### 3.1 监控各队伍提交
```bash
# 获取某队伍仓库的所有 PR
gitlink-cli pr +list --state open --owner <org> --repo <team_repo> --format json
gitlink-cli pr +list --state merged --owner <org> --repo <team_repo> --format json
# 批量获取所有队伍仓库的 PR
# AI 遍历所有队伍仓库,汇总提交状态
```
### 3.2 提交记录汇总
```bash
# 对每个队伍的每个 PR 获取详情
gitlink-cli pr +view --id <pr_id> --owner <org> --repo <team_repo> --format json
# 获取 PR 评论(评测反馈)
gitlink-cli api GET /v1/:owner/:repo/issues/:issue_id/journals --format json
```
**提交记录汇总格式**
```
=== 竞赛提交记录汇总 ===
| 队伍 | P001 | P002 | P003 | P004 | P005 | 总提交数 | 最后提交时间 |
|------|------|------|------|------|------|---------|-------------|
| 算法之光 | ✅ 通过 | ✅ 通过 | ❌ 未通过 | ✅ 通过 | — | 8 | 14:52 |
| 代码刺客 | ✅ 通过 | ✅ 通过 | ✅ 通过 | ❌ 未通过 | ❌ 未通过 | 12 | 14:58 |
| AC之王 | ✅ 通过 | ✅ 通过 | ✅ 通过 | ✅ 通过 | ✅ 通过 | 15 | 14:45 |
| 菜鸟队 | ✅ 通过 | ❌ 未通过 | — | — | — | 3 | 13:20 |
✅ = 已通过 ❌ = 未通过 — = 未提交
```
### 3.3 最终版本锁定
```bash
# 竞赛截止后,锁定各队伍最终提交
# 方式:关闭截止后的新 PR + 标记最终版本
# 获取截止时间后的 PR
gitlink-cli pr +list --state open --owner <org> --repo <team_repo> --format json
# AI 筛选 pr_created_unix > 截止时间 的 PR
# 关闭迟到的提交
gitlink-cli issue +comment --id <issue_id> --owner <org> --repo <team_repo> \
--body "⚠️ 此 PR 提交于截止时间之后,不予评测。" --format json
gitlink-cli issue +close --id <issue_id> --owner <org> --repo <team_repo>
```
---
## 四、排行榜生成
### 4.1 评分计算
```
评分规则通过规则自动计算或者通过CSV、Excel导入得分
方式 A通过率排序默认
总分 = Σ 各题通过分值
排序:总分降序 → 最后通过时间升序
方式 B用时排序ACM 赛制)
总罚时 = Σ (通过题目的提交时间 + 未通过提交次数 × 20分钟)
排序:通过题数降序 → 总罚时升序
方式 C代码质量评分
总分 = 通过分 × 80% + 代码质量分 × 20%
代码质量分 = 代码规范(30) + 可读性(30) + 复杂度(20) + 测试覆盖(20)
```
### 4.2 排行榜格式
```markdown
## 🏆 2024 校内算法竞赛 — 排行榜
**竞赛时间:** 2024-07-15 09:00 - 15:00
**参赛队伍:** 24 支
**题目数量:** 5 题
---
### 🥇 最终排名
| 排名 | 队伍 | 队长 | 通过题数 | 总分 | 最后通过 | 罚时 |
|------|------|------|---------|------|---------|------|
| 🥇 1 | AC之王 | 张三 | 5/5 | 1000 | 14:45 | 325min |
| 🥈 2 | 代码刺客 | 赵六 | 4/5 | 650 | 14:58 | 412min |
| 🥉 3 | 算法之光 | 李四 | 3/5 | 450 | 14:52 | 298min |
| 4 | 冲冲冲 | 王五 | 3/5 | 450 | 14:30 | 356min |
| 5 | 菜鸟队 | 钱七 | 1/5 | 100 | 13:20 | 145min |
| ... | ... | ... | ... | ... | ... | ... |
---
### 📊 题目通过统计
| 题号 | 标题 | 分值 | 通过数 | 通过率 | 平均提交次数 |
|------|------|------|--------|--------|------------|
| P001 | 两数之和 | 100 | 22/24 | 91.7% | 1.3 |
| P002 | 最长回文子串 | 150 | 18/24 | 75.0% | 2.1 |
| P003 | 合并K个有序链表 | 200 | 8/24 | 33.3% | 3.5 |
| P004 | 最短路径 | 200 | 5/24 | 20.8% | 4.2 |
| P005 | 动态规划优化 | 350 | 1/24 | 4.2% | 5.0 |
---
### ⏱️ 提交时间线
| 时间段 | 提交数 | 通过数 | 高峰说明 |
|--------|--------|--------|---------|
| 09:00-10:00 | 45 | 28 | 开局快速通过 P001 |
| 10:00-11:00 | 38 | 15 | P002 攻坚阶段 |
| 11:00-12:00 | 22 | 5 | P003/P004 难度提升 |
| 12:00-13:00 | 8 | 2 | 午休低谷 |
| 13:00-14:00 | 35 | 8 | 下午冲刺 |
| 14:00-15:00 | 52 | 12 | 最后冲刺(含多次未通过) |
---
*由 gitlink-competition-manager Skill 自动生成*
```
---
## 五、防作弊检测
### 5.1 跨队伍代码相似度检测(用户可选)
```
检测流程:
1. 提取所有队伍对同一题目的最终提交代码
2. 两两比对代码相似度
a. 预处理:去除注释、空白、变量名重命名归一化
b. Token 序列比对:计算编辑距离相似度
c. AST 结构比对:比较语法树结构相似度
3. 综合相似度 = Token 相似度 × 60% + AST 相似度 × 40%
4. 标记高相似度对(> 80%)为可疑
```
**检测报告格式**
```markdown
### 🔍 防作弊检测报告
**检测方法:** Token 序列比对 + AST 结构比对
**检测范围:** 24 支队伍 × 5 道题目 = 120 份代码
---
#### ⚠️ 可疑相似度对(> 80%
| 队伍A | 队伍B | 题目 | Token相似度 | AST相似度 | 综合相似度 | 判定 |
|-------|-------|------|-----------|----------|-----------|------|
| 算法之光 | 冲冲冲 | P001 | 92% | 88% | 90.4% | 🔴 高度可疑 |
| 菜鸟队 | 摸鱼队 | P001 | 85% | 82% | 83.8% | 🟡 轻度可疑 |
#### ✅ 正常范围
| 统计项 | 数值 |
|--------|------|
| 比对总数 | 7140 对 |
| 可疑对数 | 2 对 |
| 可疑率 | 0.03% |
| 平均相似度 | 23.5% |
---
#### 🔴 高度可疑详情:算法之光 vs 冲冲冲P001
**相似代码片段**
```python
# 算法之光
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
diff = target - num
if diff in seen:
return [seen[diff], i]
seen[num] = i
# 冲冲冲
def twoSum(nums, target):
visited = {}
for i, num in enumerate(nums):
remain = target - num
if remain in visited:
return [visited[remain], i]
visited[num] = i
```
**分析**变量名不同seen→visited, diff→remain但代码结构和逻辑完全一致仅做了变量重命名。
**建议**:约谈两队了解情况,要求解释解题思路。
```
### 5.2 提交时间异常检测
```
异常检测规则:
1. 短时间大量提交:同一队伍 5 分钟内提交 > 5 次
2. 提交时间高度重合:两支队伍提交时间差 < 30 多次
标记为异常的提交需人工复查。
```
---
## 六、赛后归档
### 6.1 获奖队伍标记
```bash
# 为获奖队伍仓库打标签
# 先确保标签存在
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"冠军","description":"竞赛冠军","color":"#ffd700"}' --format json
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"亚军","description":"竞赛亚军","color":"#c0c0c0"}' --format json
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"季军","description":"竞赛季军","color":"#cd7f32"}' --format json
# 在主仓库创建获奖公告 Issue
gitlink-cli issue +create \
--owner <owner> --repo <main_repo> \
--title "【公告】2024 校内算法竞赛获奖名单" \
--body "## 🏆 获奖名单
### 🥇 冠军
- 队伍AC之王
- 队长:张三
- 成员:张三、李四、王五
- 通过题数5/5
- 总分1000
### 🥈 亚军
- 队伍:代码刺客
- 队长:赵六
- 成员:赵六、钱七
- 通过题数4/5
### 🥉 季军
- 队伍:算法之光
- 队长:李四
- 成员:李四、王五
- 通过题数3/5
### 仓库链接
- [AC之王仓库](https://www.gitlink.org.cn/<org>/ac-kings)
- [代码刺客仓库](https://www.gitlink.org.cn/<org>/code-assassin)
- [算法之光仓库](https://www.gitlink.org.cn/<org>/algo-light)
恭喜以上队伍!" \
--format json
```
### 6.2 仓库归档
```bash
# 将所有队伍仓库设为只读(通过移除写权限)
# 保留仓库但将协作者权限降为 报告者
gitlink-cli api PUT /api/:owner/:repo/collaborators/change_role --body "{\"user_id\":\"<member_user_id>\",\"role\":\"Reporter\"}" --format json
# 在仓库 README 中追加归档说明(可选)
gitlink-cli issue +comment \
--id <issue_id> --owner <org> --repo <team_repo> \
--body "📦 本仓库已归档。竞赛已结束,仓库转为只读。如有需要请联系组织者。" --format json
```
---
## 七、执行步骤总览
### 7.1 赛前准备
```bash
# Step 1获取组织 ID
gitlink-cli api GET /api/organizations --format json
# Step 2批量创建队伍仓库复用 gitlink-batch-repo-create 逻辑)
for 每个队伍:
gitlink-cli api POST /api/projects --body '{...}' --format json
gitlink-cli api POST /v1/:owner/:repo/contents --body '{...}' --format json # 初始化模板
for 每个队员:
gitlink-cli api POST /api/:owner/:repo/collaborators --body "{...}" --format json
# Step 3创建题目标签体系
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{...}' --format json
# Step 4发布题目 Issue
for 每道题:
gitlink-cli issue +create --owner <owner> --repo <main_repo> --title "【题目 Pxxx】<标题>" --body "<题目内容>" --format json
gitlink-cli api PATCH /v1/:owner/:repo/issues/:id --body '{"issue_tag_ids":[<tag_id>]}' --format json
# Step 5输出竞赛信息总览队伍列表 + 题目清单 + 仓库链接)
```
### 7.2 赛中监控
```bash
# Step 1获取所有队伍仓库的 PR
for 每个队伍仓库:
gitlink-cli pr +list --state open --owner <org> --repo <team_repo> --format json
gitlink-cli pr +list --state merged --owner <org> --repo <team_repo> --format json
# Step 2汇总提交记录
# Step 3实时排行榜更新
# Step 4输出当前排名 + 提交统计
```
### 7.3 赛后处理
```bash
# Step 1锁定最终提交关闭截止后的 PR
gitlink-cli issue +close --id <issue_id> --owner <org> --repo <team_repo>
# Step 2防作弊检测代码相似度 + 提交时间异常)
# Step 3生成最终排行榜
# Step 4发布获奖公告 Issue
gitlink-cli issue +create --owner <owner> --repo <main_repo> --title "【公告】获奖名单" --body "<获奖信息>" --format json
# Step 5归档队伍仓库降权为报告者
```
---
## 八、可配置参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `scoring_mode` | pass_rate | 评分方式pass_rate / acm / quality |
| `late_penalty` | 0 | 迟交扣分 |
| `plagiarism_threshold` | 80 | 抄袭相似度阈值(% |
| `time_anomaly_threshold` | 5 | 短时间提交异常阈值5分钟内N次 |
| `repo_private` | true | 队伍仓库是否私有 |
| `lock_after_deadline` | true | 截止后是否锁定提交 |
| `archive_after_contest` | true | 赛后是否归档仓库 |
| `team_template` | default | 仓库初始化模板名称 |
---
## 九、常见场景示例
### 场景 A赛前批量准备
```
用户:"帮我准备校内算法竞赛24支队伍5道题7月15日9点开始"
AI 执行:
1. 获取组织 ID
2. 读取队伍清单 CSV → 批量创建 24 个队伍仓库
3. 初始化仓库模板problems/ 目录结构)
4. 添加队员为协作者
5. 创建题目标签体系
6. 逐题创建 Issue含题目描述/分值/截止时间)
7. 输出竞赛信息总览(队伍列表 + 题目清单 + 仓库链接)
```
### 场景 B赛中实时排行
```
用户:"看看现在排行榜什么情况"
AI 执行:
1. 遍历所有队伍仓库获取 PR 数据
2. 统计各队通过/未通过情况
3. 计算排名(按评分模式)
4. 输出当前排行榜 + 提交统计 + 时间线
```
### 场景 C赛后完整处理
```
用户:"竞赛结束了,帮我做赛后处理"
AI 执行:
1. 锁定最终提交(关闭截止后的 PR
2. 防作弊检测(代码相似度 + 提交时间异常)
3. 生成最终排行榜
4. 创建获奖公告 Issue
5. 归档队伍仓库(降权为只读)
6. 输出赛后报告(排行榜 + 防作弊报告 + 归档清单)
```
---
## 十、注意事项
- ✅ **标签预创建**:打标签前必须先查询标签列表,确认目标标签存在,不存在则先创建
- ✅ **issue_tag_ids 完整替换**PATCH 的 issue_tag_ids 是完整替换,需包含已有标签 ID
- ✅ **仓库隐私**:竞赛仓库建议设为 private避免队伍间互相查看代码
- ⚠️ **防作弊局限性**Token+AST 相似度检测不能替代人工审查,仅作为辅助参考
- ⚠️ **API 频率**24 支队伍 × 5 题目 = 120 个仓库的 PR 查询,需分批处理
- ⚠️ **协作者权限**:赛后归档时需逐个修改协作者权限,工作量与队伍数成正比
- ⚠️ **时区**:截止时间以服务器时区为准,建议明确标注 GMT+8

View File

@ -1,7 +1,7 @@
---
name: gitlink-user
version: 1.0.0
description: "用户操作:查看当前用户、用户详情。当用户需要查看 GitLink 用户信息时触发。"
description: "用户操作:查看当前用户、用户详情、关注/取关、更新资料、项目列表、消息设置、星标项目、反馈建议、项目迁移、邮箱验证、密码验证、项目申请、邮箱变更、手机变更、更改密码、注销。当用户需要查看或管理 GitLink 用户信息时触发。"
metadata:
requires:
bins: ["gitlink-cli"]
@ -21,7 +21,36 @@ metadata:
| Shortcut | 说明 | 需要认证 |
|----------|------|----------|
| `user +me` | 当前登录用户 | 是 |
| `user +current-info` | 当前用户详细信息 | 是 |
| `user +info` | 查看用户详情 | 否 |
| `user +update` | 更新用户资料 | 是 |
| `user +projects` | 用户项目列表 | 是 |
| `user +messages-list` | 列出用户消息 | 是 |
| `user +messages-read` | 标记消息为已读 | 是 |
| `user +messages-delete` | 删除消息 | 是 |
| `user +messages-create` | 创建@我消息通知 | 是 |
| `user +platform-settings` | 获取平台消息设置模板 | 是 |
| `user +user-settings` | 获取用户消息设置 | 是 |
| `user +update-settings` | 更新用户消息设置(站内信/邮件) | 是 |
| `user +pinned-list` | 列出用户星标项目 | 是 |
| `user +pinned-update` | 批量更新星标项目 | 是 |
| `user +pinned-sort` | 调整星标项目排序 | 是 |
| `user +feedback` | 提交反馈建议 | 是 |
| `user +transfer-list` | 列出项目迁移请求 | 是 |
| `user +transfer-accept` | 接受项目迁移 | 是 |
| `user +transfer-refuse` | 拒绝项目迁移 | 是 |
| `user +send-email-code` | 发送邮箱验证码 | 是 |
| `user +verify-email-code` | 验证邮箱验证码 | 是 |
| `user +check-password` | 验证用户密码 | 是 |
| `user +applied-list` | 列出项目申请 | 是 |
| `user +applied-accept` | 接受项目申请 | 是 |
| `user +applied-refuse` | 拒绝项目申请 | 是 |
| `user +check-email` | 验证邮箱可用性 | 是 |
| `user +update-email` | 更改邮箱 | 是 |
| `user +update-phone` | 更改手机号 | 是 |
| `user +change-password` | 更改密码 | 是 |
| `user +check-delete` | 检测账号是否可注销 | 是 |
| `user +delete-account` | 注销账号(不可恢复) | 是 |
## 使用示例
@ -29,10 +58,120 @@ metadata:
# 查看当前用户
gitlink-cli user +me
# 获取当前用户详细信息
gitlink-cli user +current-info
# 查看其他用户
gitlink-cli user +info --login zhangsan
# 更新用户资料
gitlink-cli user +update --login yystopf --nickname "新昵称" --gender 0 --province "广东" --city "深圳" --description "个性签名" --custom-department "某单位"
# 列出用户项目
gitlink-cli user +projects --login zhangsan --page 1 --limit 15 --category created
# 搜索用户项目
gitlink-cli user +projects --login zhangsan --search "关键词" --sort-by updated_at --sort-direction desc
# 列出用户消息
gitlink-cli user +messages-list --login alice --type notification --status 1
# 标记消息已读(全部)
gitlink-cli user +messages-read --login alice --type notification --ids -1
# 标记指定消息已读
gitlink-cli user +messages-read --login alice --type atme --ids 101,102,103
# 删除消息
gitlink-cli user +messages-delete --login alice --type atme --ids 201,202
# 创建@我消息通知
gitlink-cli user +messages-create --login alice --receivers bob,carol --atmeable-type Issue --atmeable-id 123
# 获取平台消息设置模板
gitlink-cli user +platform-settings
# 获取用户消息设置
gitlink-cli user +user-settings --login alice
# 更新用户消息设置(开启站内信、关闭邮件)
gitlink-cli user +update-settings --login alice \
--notification "CreateOrAssign::IssueAssigned,ManageProject::Issue" \
--email "Normal::Permission" \
--notification-off "ManageProject::Member" \
--email-off "Normal::Organization"
# 列出用户星标项目
gitlink-cli user +pinned-list --login alice
# 批量更新星标项目
gitlink-cli user +pinned-update --login alice --project-ids 17,23,42
# 调整星标项目排序(数字越大越靠前)
gitlink-cli user +pinned-sort --login alice --pinned-id 5 --position 10
# 提交反馈建议
gitlink-cli user +feedback --login alice --content "希望增加批量操作功能"
# 接受项目迁移
gitlink-cli user +transfer-accept --login alice --transfer-id 42
# 拒绝项目迁移
gitlink-cli user +transfer-refuse --login alice --transfer-id 42
# 发送邮箱验证码(用于更新邮箱)
gitlink-cli user +send-email-code --login alice --code-type 10 --email new@example.com --email-md5 abc123md5hash
# 验证邮箱验证码
gitlink-cli user +verify-email-code --login alice --code-type 10 --email new@example.com --code 123456
# 验证用户密码
gitlink-cli user +check-password --login alice --password mysecretpass
# 列出项目申请(支持分页)
gitlink-cli user +applied-list --login alice --page 1 --per-page 10
# 接受项目申请
gitlink-cli user +applied-accept --login alice --applied-id 7
# 拒绝项目申请
gitlink-cli user +applied-refuse --login alice --applied-id 7
# 检查邮箱是否可用
gitlink-cli user +check-email --login alice --email new@example.com
# 更改邮箱(需先发送验证码)
gitlink-cli user +update-email --login alice --password *** --email new@example.com --code 123456
# 更改手机号(需手机验证码)
gitlink-cli user +update-phone --login alice --phone 13800138000 --code 654321 --password ***
# 更改密码
gitlink-cli user +change-password --login alice --old-password *** --new-password *** --new-password-repeat newpass456
# 检测账号是否可注销
gitlink-cli user +check-delete --login alice
# 注销账号(⚠️ 不可恢复)
gitlink-cli user +delete-account --login alice --password *** --memo "不再使用"
```
## 消息设置分类
更新消息设置(`+update-settings`)时,`--notification` / `--email` / `--notification-off` / `--email-off` 支持以下分类 key
| 分类 Key | 说明 |
|----------|------|
| `CreateOrAssign::IssueAssigned` | 创建或指派给我的疑修 |
| `CreateOrAssign::PullRequestAssigned` | 创建或指派给我的合并请求 |
| `ManageProject::Issue` | 我管理的项目的疑修 |
| `ManageProject::PullRequest` | 我管理的项目的合并请求 |
| `ManageProject::Member` | 我管理的项目的成员变更 |
| `ManageProject::SettingChanged` | 我管理的项目的设置变更 |
| `Normal::Organization` | 组织相关通知 |
| `Normal::Project` | 项目相关通知 |
| `Normal::Permission` | 权限变更通知 |
## Raw API 补充
```bash

View File

@ -1,79 +0,0 @@
---
name: gitlink-wiki
version: 2.0.0
description: "Wiki 页面管理:查看目录、查看、创建、更新和删除 GitLink Wiki 页面。"
metadata:
requires:
bins: ["gitlink-cli"]
cliHelp: "gitlink-cli wiki --help"
---
# gitlink-wiki
**重要**: 开始操作前请先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中涵盖认证、权限、全局参数和 GitLink API 行为说明。
**重要**: 执行写入或破坏性操作(如 `+create`、`+update` 或 `+delete`)前,请先确认用户意图。
**重要**: 操作 GitLink 资源请使用 `gitlink-cli`,不要使用 `gh` 等 GitHub 专用工具。
## 快捷命令
| 快捷命令 | 说明 | 操作类型 |
|----------|------|----------|
| `wiki +list` | 列出 Wiki 页面(目录结构) | 只读 |
| `wiki +view` | 按页面名称查看 Wiki 页面详情 | 只读 |
| `wiki +create` | 创建新的 Wiki 页面 | 写入 |
| `wiki +update` | 更新 Wiki 页面标题和/或内容 | 写入 |
| `wiki +delete` | 删除 Wiki 页面 | 破坏性 |
## 使用示例
```bash
# 列出 Wiki 目录
gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345
# 查看 Wiki 页面
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home
# 创建 Wiki 页面
gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \
-n getting-started -t "快速开始" -c "# 快速开始指南\n\n这是入门文档。"
# 创建时附带提交信息
gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \
-n api-guide -t "API 指南" -c "# API 指南" -m "Add API guide"
# 仅更新页面标题
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题"
# 仅更新页面内容
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -c "# 更新后的内容"
# 同时更新标题和内容
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 \
-n home -t "新标题" -c "新内容"
# 删除 Wiki 页面
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page
```
## 参数说明
| 命令 | 关键参数 |
|------|----------|
| `+list` | `--project-id` |
| `+view` | `--project-id`、`--page-name` (`-n`) |
| `+create` | `--project-id`、`--page-name` (`-n`)、`--title` (`-t`)、`--content` (`-c`)、`--message` (`-m`) |
| `+update` | `--project-id`、`--page-name` (`-n`)、`--title` (`-t`),可选 `--content` (`-c`)、`--message` (`-m`) |
| `+delete` | `--project-id`、`--page-name` (`-n`) |
## API 说明
- 所有 Wiki 端点使用 `/api/wiki/{action}` 扁平路径结构(非 REST 嵌套路径)。
- 目录列表: `GET /api/wiki/wikiPages`查询参数owner, repo, projectId
- 查看详情: `GET /api/wiki/getWiki`查询参数owner, repo, projectId, pageName
- 创建页面: `POST /api/wiki/createWiki`JSON bodycontent 需 base64 编码)
- 更新页面: `PUT /api/wiki/updateWiki`JSON bodycontent 需 base64 编码)
- 删除页面: `DELETE /api/wiki/deleteWiki`JSON bodyowner, repo, projectId, pageName
- Wiki 页面通过 `pageName`slug标识而非数字 ID。
- 所有操作都需要 `--project-id`GitLink 项目数字 ID
- 创建和更新时,内容自动进行 base64 编码后以 `content_base64` 字段发送。
- `+update` 要求必须提供 `--title``--page-name``--content` 为可选。