forked from Gitlink/gitlink-cli
Compare commits
6 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
9749a4c832 | |
|
|
11ecb9b79a | |
|
|
71ca2bb683 | |
|
|
593b5e8f0d | |
|
|
dbf0f45ce3 | |
|
|
2c9a8b6192 |
23
README.md
23
README.md
|
|
@ -114,6 +114,7 @@ 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 |
|
||||
|
|
@ -273,6 +274,28 @@ 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
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@
|
|||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
|
||||
| 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 📊 数据集 | 按项目查询科研数据集 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
|
|
@ -284,6 +285,28 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# 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` 框架,与其他模块保持一致
|
||||
|
||||
|
|
@ -115,22 +115,35 @@ 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:
|
||||
statusCode = v
|
||||
bodyCode = v
|
||||
case int:
|
||||
statusCode = float64(v)
|
||||
bodyCode = float64(v)
|
||||
}
|
||||
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,
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +183,10 @@ 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,45 @@ 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) {
|
||||
|
|
@ -525,3 +564,18 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,25 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
DefaultBaseURL = "https://www.gitlink.org.cn/api"
|
||||
DefaultFormat = "table"
|
||||
DefaultBaseURL = "https://www.gitlink.org.cn/api"
|
||||
DefaultGatewayURL = "https://gateway.gitlink.org.cn/api"
|
||||
DefaultFormat = "table"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
BaseURL: DefaultBaseURL,
|
||||
Format: DefaultFormat,
|
||||
BaseURL: DefaultBaseURL,
|
||||
GatewayURL: DefaultGatewayURL,
|
||||
Format: DefaultFormat,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +57,9 @@ func Load() (*Config, error) {
|
|||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = DefaultBaseURL
|
||||
}
|
||||
if cfg.GatewayURL == "" {
|
||||
cfg.GatewayURL = DefaultGatewayURL
|
||||
}
|
||||
if cfg.Format == "" {
|
||||
cfg.Format = DefaultFormat
|
||||
}
|
||||
|
|
@ -80,6 +86,8 @@ 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":
|
||||
|
|
@ -101,6 +109,8 @@ 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":
|
||||
|
|
|
|||
|
|
@ -267,105 +267,5 @@
|
|||
"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)",
|
||||
"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"
|
||||
}
|
||||
"warning.auth.user_unavailable": "✓ Token stored (user info unavailable)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,105 +267,5 @@
|
|||
"success.config.set": "✓ 已设置 {key} = {value}",
|
||||
"warning.auth.not_logged_in": "✗ 未登录",
|
||||
"warning.auth.token_unverified": "✓ Token 已保存(但无法验证:{message})",
|
||||
"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-type(Journal/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": "删除原因"
|
||||
}
|
||||
"warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ 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"
|
||||
)
|
||||
|
||||
|
|
@ -53,6 +54,7 @@ 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(),
|
||||
|
|
@ -77,6 +79,7 @@ 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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"dataset", "health", "ignore", "wiki",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,200 @@
|
|||
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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ gitlink-cli auth status
|
|||
gitlink-cli user +me
|
||||
```
|
||||
|
||||
详见: [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md)
|
||||
详见: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md)
|
||||
|
||||
### 2. 查看可用命令
|
||||
|
||||
|
|
@ -64,25 +64,19 @@ skills/
|
|||
├── README.md # 本文件
|
||||
├── gitlink-shared/ # 共享基础规则
|
||||
│ ├── SKILL.md # 认证、全局参数、安全规则、分支约定
|
||||
│ ├── REFERENCE.md # API 详细参考、错误处理
|
||||
│ ├── TROUBLESHOOTING.md # 常见问题排查
|
||||
│ └── examples/
|
||||
│ └── auth-workflow.md # 认证工作流示例
|
||||
│ └── references/
|
||||
│ ├── api-reference.md # API 详细参考、错误处理
|
||||
│ ├── raw-api-batch.md # 批量 Raw API 调用参考
|
||||
│ └── troubleshooting.md # 常见问题排查
|
||||
├── gitlink-repo/ # 仓库管理
|
||||
│ ├── SKILL.md # 仓库操作指南
|
||||
│ ├── REFERENCE.md # 仓库 API 参考
|
||||
│ └── examples/
|
||||
│ └── repo-workflow.md # 仓库管理工作流
|
||||
│ └── references/ # 仓库命令参考文档
|
||||
├── gitlink-issue/ # Issue 管理
|
||||
│ ├── SKILL.md # Issue 操作指南
|
||||
│ ├── REFERENCE.md # Issue API 参考
|
||||
│ └── examples/
|
||||
│ └── issue-workflow.md # Issue 全流程工作流
|
||||
│ └── references/ # Issue 命令参考文档
|
||||
├── gitlink-pr/ # Pull Request
|
||||
│ ├── SKILL.md # PR 操作指南
|
||||
│ ├── REFERENCE.md # PR API 参考
|
||||
│ └── examples/
|
||||
│ └── pr-workflow.md # PR 工作流
|
||||
│ └── references/ # PR 命令参考文档
|
||||
├── gitlink-member/ # 仓库成员管理
|
||||
│ └── SKILL.md # 成员与邀请链接操作指南
|
||||
├── gitlink-branch/ # 分支管理
|
||||
|
|
@ -91,25 +85,24 @@ skills/
|
|||
│ └── branch-workflow.md # 分支工作流
|
||||
├── gitlink-release/ # 版本发布
|
||||
│ ├── SKILL.md # Release 操作指南
|
||||
│ ├── REFERENCE.md # Release API 参考
|
||||
│ └── examples/
|
||||
│ └── release-workflow.md # Release 工作流
|
||||
│ └── references/ # Release 命令参考文档
|
||||
├── gitlink-release-auto/ # 自动化 Release 管理
|
||||
│ └── SKILL.md # 自动发版、版本号推荐、Release Notes 生成
|
||||
├── gitlink-search/ # 搜索功能
|
||||
│ ├── SKILL.md # 搜索操作指南
|
||||
│ └── examples/
|
||||
│ └── search-workflow.md # 搜索工作流
|
||||
│ └── references/ # 搜索命令参考文档
|
||||
├── gitlink-user/ # 用户管理
|
||||
│ └── SKILL.md # 用户操作指南
|
||||
│ ├── SKILL.md # 用户操作指南
|
||||
│ └── references/ # 用户命令参考文档
|
||||
├── gitlink-org/ # 组织管理
|
||||
│ ├── SKILL.md # 组织操作指南
|
||||
│ └── examples/
|
||||
│ └── org-workflow.md # 组织工作流
|
||||
│ └── references/ # 组织命令参考文档
|
||||
├── gitlink-ci/ # CI/CD
|
||||
│ ├── SKILL.md # CI 操作指南
|
||||
│ └── examples/
|
||||
│ └── ci-workflow.md # CI 工作流
|
||||
│ └── SKILL.md # CI 操作指南
|
||||
├── gitlink-pipeline/ # 流水线工作流
|
||||
│ └── SKILL.md # Pipeline 操作指南
|
||||
├── gitlink-wiki/ # Wiki 页面管理
|
||||
│ └── SKILL.md # Wiki 操作指南
|
||||
├── gitlink-pm/ # 项目管理
|
||||
│ └── SKILL.md # PM 操作指南
|
||||
├── gitlink-health/ # 项目健康度分析
|
||||
|
|
@ -150,6 +143,7 @@ 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 |
|
||||
|
|
@ -169,7 +163,7 @@ gitlink-cli repo +info
|
|||
gitlink-cli repo +info --owner wbtiger --repo gitlink-cli
|
||||
```
|
||||
|
||||
详见: [gitlink-repo/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md)
|
||||
详见: [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md)
|
||||
|
||||
### 场景 2:创建和管理 Issue
|
||||
|
||||
|
|
@ -190,7 +184,7 @@ gitlink-cli issue +close -i 123
|
|||
gitlink-cli issue +batch-close --numbers 123,124 --dry-run
|
||||
```
|
||||
|
||||
详见: [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md)
|
||||
详见: [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md)
|
||||
|
||||
### 场景 3:管理分支和发布
|
||||
|
||||
|
|
@ -208,7 +202,7 @@ gitlink-cli release +create -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..."
|
|||
gitlink-cli release +view -i <version_id>
|
||||
```
|
||||
|
||||
详见: [gitlink-release/examples/release-workflow.md](gitlink-release/examples/release-workflow.md)
|
||||
详见: [gitlink-release/SKILL.md](gitlink-release/SKILL.md)
|
||||
|
||||
### 场景 4:搜索和发现
|
||||
|
||||
|
|
@ -224,7 +218,7 @@ gitlink-cli org +list
|
|||
gitlink-cli org +info -i Gitlink
|
||||
```
|
||||
|
||||
详见: [gitlink-search/examples/search-workflow.md](gitlink-search/examples/search-workflow.md)
|
||||
详见: [gitlink-search/SKILL.md](gitlink-search/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -233,8 +227,8 @@ gitlink-cli org +info -i Gitlink
|
|||
### 快速查找
|
||||
|
||||
- **我想了解认证**: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md)
|
||||
- **我想查看 API 细节**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
|
||||
- **我遇到了错误**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
|
||||
- **我想查看 API 细节**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
|
||||
- **我遇到了错误**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
|
||||
- **我想看工作流示例**: 查看各 Skill 下的 `examples/` 目录
|
||||
|
||||
### 按功能分类
|
||||
|
|
@ -242,12 +236,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/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md) - 完整工作流
|
||||
- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 完整工作流
|
||||
|
||||
**Issue 和 PR**:
|
||||
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 命令
|
||||
- [gitlink-pr/SKILL.md](gitlink-pr/SKILL.md) - PR 命令
|
||||
- [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md) - Issue 工作流
|
||||
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 工作流
|
||||
|
||||
**发布和搜索**:
|
||||
- [gitlink-release/SKILL.md](gitlink-release/SKILL.md) - Release 命令
|
||||
|
|
@ -289,11 +283,11 @@ gitlink-cli auth login
|
|||
|
||||
### Q: 如何查看完整的 API 参考?
|
||||
|
||||
A: 查看 [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
|
||||
A: 查看 [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
|
||||
|
||||
### Q: 遇到错误怎么办?
|
||||
|
||||
A: 查看 [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
|
||||
A: 查看 [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -329,7 +323,7 @@ AI 代理可以:
|
|||
- 所有边界情况处理正确
|
||||
- 完整的文档和示例
|
||||
|
||||
详见: [../doc/SKILLS_TEST_REPORT_2026-04-02.md](../doc/SKILLS_TEST_REPORT_2026-04-02.md)
|
||||
详见: [../doc/design.md](../doc/design.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -337,8 +331,7 @@ AI 代理可以:
|
|||
|
||||
- [主项目 README](../README.md) - gitlink-cli 项目说明
|
||||
- [设计文档](../doc/design.md) - 架构设计和开发计划
|
||||
- [测试报告](../doc/SKILLS_TEST_REPORT_2026-04-02.md) - 功能测试报告
|
||||
- [代码同步方案](../doc/CODE_SYNC_STRATEGY_FINAL.md) - GitHub ↔ GitLink 同步设计
|
||||
- [API 参考文档](../doc/gitlink_api_reference.md) - GitLink API 参考文档
|
||||
- [gitlink-bisync](https://www.gitlink.org.cn/wbtiger/gitlink-bisync) - 代码双向同步系统
|
||||
|
||||
---
|
||||
|
|
@ -346,8 +339,8 @@ AI 代理可以:
|
|||
## 📞 获取帮助
|
||||
|
||||
- **命令帮助**: `gitlink-cli <command> --help`
|
||||
- **故障排查**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
|
||||
- **API 参考**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
|
||||
- **故障排查**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
|
||||
- **API 参考**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
|
||||
- **工作流示例**: 查看各 Skill 下的 `examples/` 目录
|
||||
|
||||
---
|
||||
|
|
@ -355,7 +348,7 @@ AI 代理可以:
|
|||
## 🎓 下一步
|
||||
|
||||
1. 阅读 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 了解基础
|
||||
2. 查看 [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md) 完成认证
|
||||
2. 查看 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 完成认证
|
||||
3. 根据需求选择相应的 Skill 文档
|
||||
4. 参考 `examples/` 目录中的工作流示例
|
||||
5. 使用 AI 代理自动化你的工作流
|
||||
|
|
|
|||
|
|
@ -0,0 +1,586 @@
|
|||
---
|
||||
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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-user
|
||||
version: 1.0.0
|
||||
description: "用户操作:查看当前用户、用户详情、关注/取关、更新资料、项目列表、消息设置、星标项目、反馈建议、项目迁移、邮箱验证、密码验证、项目申请、邮箱变更、手机变更、更改密码、注销。当用户需要查看或管理 GitLink 用户信息时触发。"
|
||||
description: "用户操作:查看当前用户、用户详情。当用户需要查看 GitLink 用户信息时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -21,36 +21,7 @@ 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` | 注销账号(不可恢复) | 是 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
|
|
@ -58,120 +29,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
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 body,content 需 base64 编码)
|
||||
- 更新页面: `PUT /api/wiki/updateWiki`(JSON body,content 需 base64 编码)
|
||||
- 删除页面: `DELETE /api/wiki/deleteWiki`(JSON body:owner, repo, projectId, pageName)
|
||||
- Wiki 页面通过 `pageName`(slug)标识,而非数字 ID。
|
||||
- 所有操作都需要 `--project-id`(GitLink 项目数字 ID)。
|
||||
- 创建和更新时,内容自动进行 base64 编码后以 `content_base64` 字段发送。
|
||||
- `+update` 要求必须提供 `--title` 和 `--page-name`;`--content` 为可选。
|
||||
Loading…
Reference in New Issue