Merge pull request 'feat(shortcut): add shortcuts/wiki' (#193) from co63oc/gitlink-cli:fix4 into master

This commit is contained in:
wbtiger 2026-06-22 00:03:52 +08:00
commit 593b5e8f0d
12 changed files with 680 additions and 21 deletions

View File

@ -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

View File

@ -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

View File

@ -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` 框架,与其他模块保持一致

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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":

View File

@ -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",

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",
"dataset", "health", "ignore", "wiki",
}
groupSet := map[string]bool{}

200
shortcuts/wiki/wiki.go Normal file
View File

@ -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)
},
},
}
}

224
shortcuts/wiki/wiki_test.go Normal file
View File

@ -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)
}
}

View File

@ -110,6 +110,8 @@ skills/
│ └── ci-workflow.md # CI 工作流
├── gitlink-pipeline/ # 流水线工作流
│ └── SKILL.md # Pipeline 操作指南
├── gitlink-wiki/ # Wiki 页面管理
│ └── SKILL.md # Wiki 操作指南
├── gitlink-pm/ # 项目管理
│ └── SKILL.md # PM 操作指南
├── gitlink-health/ # 项目健康度分析
@ -150,6 +152,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 |

View File

@ -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 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` 为可选。