fix: wiki commands use correct gateway API (gateway.gitlink.org.cn)

This commit is contained in:
15972095207 2026-06-03 20:27:19 +08:00
parent d3bdbbc28a
commit 5cb6ea1cd1
4 changed files with 323 additions and 91 deletions

View File

@ -172,6 +172,106 @@ func normalizeAPIPath(baseURL, path string) string {
return path
}
// DoRaw makes an API call without appending .json to the path.
func (c *Client) DoRaw(method, path string, body interface{}, query url.Values) (*output.Envelope, error) {
fullURL := c.BaseURL + path
if query != nil && len(query) > 0 {
sep := "?"
if strings.Contains(fullURL, "?") {
sep = "&"
}
fullURL += sep + query.Encode()
}
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequest(method, fullURL, bodyReader)
if err != nil {
return nil, err
}
if c.Debug {
fmt.Printf("→ %s %s\n", method, fullURL)
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if c.Debug {
fmt.Printf("← %d %s\n", resp.StatusCode, string(respData[:min(len(respData), 200)]))
}
if resp.StatusCode >= 400 {
return nil, &APIError{
StatusCode: resp.StatusCode,
Code: resp.StatusCode,
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))),
}
}
var raw map[string]interface{}
if err := json.Unmarshal(respData, &raw); err != nil {
return output.SuccessEnvelope(string(respData), nil), nil
}
if status, ok := raw["status"]; ok {
var statusCode float64
switch v := status.(type) {
case float64:
statusCode = v
case int:
statusCode = 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,
}
}
}
if dataStr, ok := raw["data"].(string); ok {
var parsedData interface{}
if err := json.Unmarshal([]byte(dataStr), &parsedData); err == nil {
raw["data"] = json.RawMessage(dataStr)
}
}
var meta *output.Meta
if tc, ok := raw["total_count"]; ok {
meta = &output.Meta{}
if v, ok := tc.(float64); ok {
meta.TotalCount = int(v)
}
if v, ok := raw["page"].(float64); ok {
meta.Page = int(v)
}
if v, ok := raw["limit"].(float64); ok {
meta.Limit = int(v)
}
}
return output.SuccessEnvelope(raw, meta), nil
}
func (c *Client) Get(path string, query url.Values) (*output.Envelope, error) {
return c.Do("GET", path, nil, query)
}

View File

@ -81,6 +81,16 @@ func (ctx *RuntimeContext) CallAPIWithQuery(method, path string, query url.Value
return ctx.Client.Do(method, path, nil, query)
}
// CallAPIRaw makes an API call without appending .json suffix.
func (ctx *RuntimeContext) CallAPIRaw(method, path string, body interface{}) (*output.Envelope, error) {
return ctx.Client.DoRaw(method, path, body, nil)
}
// CallAPIRawWithQuery makes an API call with query parameters without appending .json suffix.
func (ctx *RuntimeContext) CallAPIRawWithQuery(method, path string, query url.Values) (*output.Envelope, error) {
return ctx.Client.DoRaw(method, path, nil, query)
}
// PaginateAll fetches all pages.
func (ctx *RuntimeContext) PaginateAll(path string, params url.Values) ([]json.RawMessage, error) {
return ctx.Client.PaginateAll(path, params)

View File

@ -3,10 +3,15 @@ package wiki
import (
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
const wikiBaseURL = "https://gateway.gitlink.org.cn/api"
// Shortcuts returns wiki page management shortcuts.
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
@ -17,122 +22,189 @@ func Shortcuts() []*common.Shortcut {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", wikiPagesPath(ctx), nil)
projectID, err := fetchProjectID(ctx)
if err != nil {
return err
}
return ctx.Output(env)
q := url.Values{}
q.Set("owner", ctx.Owner)
q.Set("repo", ctx.Repo)
q.Set("projectId", strconv.Itoa(projectID))
return callWikiAPI(ctx, "GET", "/wiki/open/wikiPages", nil, q)
},
},
{
Name: "view",
Description: "View a wiki page",
Flags: []common.Flag{
{Name: "slug", Short: "s", Usage: "Wiki page slug", Required: true},
{Name: "name", Short: "n", Usage: "Wiki page name", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
slug, err := ctx.RequireArg("slug")
name, err := ctx.RequireArg("name")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", wikiPagePath(ctx, slug), nil)
projectID, err := fetchProjectID(ctx)
if err != nil {
return err
}
return ctx.Output(env)
q := url.Values{}
q.Set("owner", ctx.Owner)
q.Set("repo", ctx.Repo)
q.Set("projectId", strconv.Itoa(projectID))
q.Set("pageName", name)
return callWikiAPI(ctx, "GET", "/wiki/open/getWiki", nil, q)
},
},
{
Name: "create",
Description: "Create a wiki page",
Flags: []common.Flag{
{Name: "title", Short: "t", Usage: "Wiki page title", Required: true},
{Name: "body", Short: "b", Usage: "Wiki page content", Required: true},
{Name: "name", Short: "n", Usage: "Wiki page name", Required: true},
{Name: "content", Short: "c", Usage: "Page content (will be base64 encoded)", Required: true},
{Name: "message", Short: "m", Usage: "Commit message"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
title, err := ctx.RequireArg("title")
name, err := ctx.RequireArg("name")
if err != nil {
return err
}
body, err := ctx.RequireArg("body")
content, err := ctx.RequireArg("content")
if err != nil {
return err
}
payload := map[string]interface{}{
"title": title,
"content_base64": base64.StdEncoding.EncodeToString([]byte(body)),
}
env, err := ctx.CallAPI("POST", wikiPagesPath(ctx), payload)
projectID, err := fetchProjectID(ctx)
if err != nil {
return err
}
return ctx.Output(env)
body := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": projectID,
"pageName": name,
"title": name,
"message": ctx.Arg("message"),
"content_base64": base64.StdEncoding.EncodeToString([]byte(content)),
}
return callWikiAPI(ctx, "POST", "/wiki/open/createWiki", body, nil)
},
},
{
Name: "update",
Description: "Update a wiki page",
Flags: []common.Flag{
{Name: "slug", Short: "s", Usage: "Wiki page slug", Required: true},
{Name: "title", Short: "t", Usage: "New title"},
{Name: "body", Short: "b", Usage: "New content"},
{Name: "name", Short: "n", Usage: "Wiki page name", Required: true},
{Name: "content", Short: "c", Usage: "New page content (will be base64 encoded)", Required: true},
{Name: "message", Short: "m", Usage: "Commit message"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
slug, err := ctx.RequireArg("slug")
name, err := ctx.RequireArg("name")
if err != nil {
return err
}
payload := map[string]interface{}{}
if title := ctx.Arg("title"); title != "" {
payload["title"] = title
}
if body := ctx.Arg("body"); body != "" {
payload["content_base64"] = base64.StdEncoding.EncodeToString([]byte(body))
}
env, err := ctx.CallAPI("PATCH", wikiPagePath(ctx, slug), payload)
content, err := ctx.RequireArg("content")
if err != nil {
return err
}
return ctx.Output(env)
projectID, err := fetchProjectID(ctx)
if err != nil {
return err
}
body := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": projectID,
"pageName": name,
"title": name,
"message": ctx.Arg("message"),
"content_base64": base64.StdEncoding.EncodeToString([]byte(content)),
}
return callWikiAPI(ctx, "PUT", "/wiki/open/updateWiki", body, nil)
},
},
{
Name: "delete",
Description: "Delete a wiki page",
Flags: []common.Flag{
{Name: "slug", Short: "s", Usage: "Wiki page slug", Required: true},
{Name: "name", Short: "n", Usage: "Wiki page name", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
slug, err := ctx.RequireArg("slug")
name, err := ctx.RequireArg("name")
if err != nil {
return err
}
env, err := ctx.CallAPI("DELETE", wikiPagePath(ctx, slug), nil)
projectID, err := fetchProjectID(ctx)
if err != nil {
return err
}
return ctx.Output(env)
body := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": projectID,
"pageName": name,
}
return callWikiAPI(ctx, "DELETE", "/wiki/open/deleteWiki", body, nil)
},
},
}
}
func wikiPagesPath(ctx *common.RuntimeContext) string {
return fmt.Sprintf("/%s/%s/wiki/pages", ctx.Owner, ctx.Repo)
// callWikiAPI temporarily switches the client BaseURL to the wiki gateway.
// In test mode (BaseURL is a local httptest server), the switch is skipped.
func callWikiAPI(ctx *common.RuntimeContext, method, path string, body interface{}, query url.Values) error {
origBase := ctx.Client.BaseURL
if !strings.HasPrefix(origBase, "http://127.0.0.1") {
ctx.Client.BaseURL = wikiBaseURL
}
defer func() { ctx.Client.BaseURL = origBase }()
if query != nil {
env, err := ctx.CallAPIRawWithQuery(method, path, query)
if err != nil {
return err
}
return ctx.Output(env)
}
env, err := ctx.CallAPIRaw(method, path, body)
if err != nil {
return err
}
return ctx.Output(env)
if err != nil {
return err
}
return ctx.Output(env)
}
func wikiPagePath(ctx *common.RuntimeContext, slug string) string {
return fmt.Sprintf("/%s/%s/wiki/pages/%s", ctx.Owner, ctx.Repo, slug)
func fetchProjectID(ctx *common.RuntimeContext) (int, error) {
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
if err != nil {
return 0, fmt.Errorf("获取项目信息失败: %w", err)
}
data, ok := env.Data.(map[string]interface{})
if !ok {
return 0, fmt.Errorf("无法解析项目信息")
}
if idFloat, ok := data["project_id"].(float64); ok {
return int(idFloat), nil
}
if idFloat, ok := data["repo_id"].(float64); ok {
return int(idFloat), nil
}
if idFloat, ok := data["id"].(float64); ok {
return int(idFloat), nil
}
return 0, fmt.Errorf("项目 ID 未找到,请确认仓库是否存在")
}

View File

@ -15,19 +15,28 @@ import (
func TestWikiList(t *testing.T) {
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/wiki/pages.json")
writeJSON(t, w, map[string]interface{}{
"total_count": 2,
"wiki_pages": []interface{}{
map[string]interface{}{"title": "Home", "slug": "home"},
map[string]interface{}{"title": "Getting Started", "slug": "getting-started"},
},
})
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{
"id": float64(1547460),
"project_id": float64(1547460),
"name": "gitlink-cli",
})
case r.Method == "GET" && r.URL.Path == "/wiki/open/wikiPages":
writeJSON(t, w, map[string]interface{}{
"data": []interface{}{
map[string]interface{}{"title": "Home", "sub_url": "Home"},
map[string]interface{}{"title": "Guide", "sub_url": "Guide"},
},
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
if err := runWikiShortcut(t, server, "list", nil); err != nil {
t.Fatalf("list shortcut failed: %v", err)
t.Fatalf("list failed: %v", err)
}
}
@ -35,93 +44,134 @@ func TestWikiList(t *testing.T) {
func TestWikiView(t *testing.T) {
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/wiki/pages/home.json")
writeJSON(t, w, map[string]interface{}{
"title": "Home",
"slug": "home",
"content_base64": base64.StdEncoding.EncodeToString([]byte("Welcome to the wiki")),
})
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{
"id": float64(1547460),
"project_id": float64(1547460),
})
case r.Method == "GET" && r.URL.Path == "/wiki/open/getWiki":
if r.URL.Query().Get("pageName") != "Home" {
t.Fatalf("expected pageName=Home, got %s", r.URL.Query().Get("pageName"))
}
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{
"title": "Home",
"content_base64": base64.StdEncoding.EncodeToString([]byte("Welcome to the wiki")),
},
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
if err := runWikiShortcut(t, server, "view", map[string]string{"slug": "home"}); err != nil {
t.Fatalf("view shortcut failed: %v", err)
if err := runWikiShortcut(t, server, "view", map[string]string{"name": "Home"}); err != nil {
t.Fatalf("view failed: %v", err)
}
}
// --- Create ---
func TestWikiCreate(t *testing.T) {
var createPayload map[string]interface{}
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/owner/repo/wiki/pages.json")
body := decodeJSON(t, r)
assertEqual(t, body["title"], "TestPage")
decoded, err := base64.StdEncoding.DecodeString(body["content_base64"].(string))
if err != nil {
t.Fatalf("failed to decode content_base64: %v", err)
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{
"id": float64(1547460),
"project_id": float64(1547460),
})
case r.Method == "POST" && r.URL.Path == "/wiki/open/createWiki":
createPayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{
"code": 201,
"data": map[string]interface{}{"title": "NewPage"},
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
assertEqual(t, string(decoded), "Hello World")
writeJSON(t, w, map[string]interface{}{
"title": "TestPage",
"slug": "TestPage",
})
})
defer server.Close()
if err := runWikiShortcut(t, server, "create", map[string]string{
"title": "TestPage",
"body": "Hello World",
"name": "NewPage",
"content": "Hello Wiki",
}); err != nil {
t.Fatalf("create shortcut failed: %v", err)
t.Fatalf("create failed: %v", err)
}
assertEqual(t, createPayload["pageName"], "NewPage")
assertEqual(t, createPayload["owner"], "owner")
assertEqual(t, createPayload["repo"], "repo")
expectedContent := base64.StdEncoding.EncodeToString([]byte("Hello Wiki"))
assertEqual(t, createPayload["content_base64"], expectedContent)
}
// --- Update ---
func TestWikiUpdate(t *testing.T) {
var updatePayload map[string]interface{}
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "PATCH", "/owner/repo/wiki/pages/home.json")
body := decodeJSON(t, r)
assertEqual(t, body["title"], "Updated Title")
decoded, err := base64.StdEncoding.DecodeString(body["content_base64"].(string))
if err != nil {
t.Fatalf("failed to decode content_base64: %v", err)
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{
"id": float64(1547460),
"project_id": float64(1547460),
})
case r.Method == "PUT" && r.URL.Path == "/wiki/open/updateWiki":
updatePayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{
"code": 200,
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
assertEqual(t, string(decoded), "Updated content")
writeJSON(t, w, map[string]interface{}{
"title": "Updated Title",
"slug": "home",
})
})
defer server.Close()
if err := runWikiShortcut(t, server, "update", map[string]string{
"slug": "home",
"title": "Updated Title",
"body": "Updated content",
"name": "Home",
"content": "Updated content",
"message": "Update wiki page",
}); err != nil {
t.Fatalf("update shortcut failed: %v", err)
t.Fatalf("update failed: %v", err)
}
assertEqual(t, updatePayload["pageName"], "Home")
assertEqual(t, updatePayload["message"], "Update wiki page")
}
// --- Delete ---
func TestWikiDelete(t *testing.T) {
var deletePayload map[string]interface{}
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "DELETE", "/owner/repo/wiki/pages/old-page.json")
writeJSON(t, w, map[string]interface{}{
"message": "deleted",
})
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{
"id": float64(1547460),
"project_id": float64(1547460),
})
case r.Method == "DELETE" && r.URL.Path == "/wiki/open/deleteWiki":
deletePayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{
"code": 204,
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
if err := runWikiShortcut(t, server, "delete", map[string]string{
"slug": "old-page",
"name": "OldPage",
}); err != nil {
t.Fatalf("delete shortcut failed: %v", err)
t.Fatalf("delete failed: %v", err)
}
assertEqual(t, deletePayload["pageName"], "OldPage")
assertEqual(t, deletePayload["projectId"], float64(1547460))
}
// --- test helpers ---