From 5cb6ea1cd18668292e9814b0a392d6e85a0ea765 Mon Sep 17 00:00:00 2001 From: 15972095207 <2484216370@qq.com> Date: Wed, 3 Jun 2026 20:27:19 +0800 Subject: [PATCH] fix: wiki commands use correct gateway API (gateway.gitlink.org.cn) --- internal/client/client.go | 100 +++++++++++++++++++++++ shortcuts/common/types.go | 10 +++ shortcuts/wiki/wiki.go | 146 ++++++++++++++++++++++++--------- shortcuts/wiki/wiki_test.go | 158 ++++++++++++++++++++++++------------ 4 files changed, 323 insertions(+), 91 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 831caa7..ea7b337 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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) } diff --git a/shortcuts/common/types.go b/shortcuts/common/types.go index 15441c9..11762d1 100644 --- a/shortcuts/common/types.go +++ b/shortcuts/common/types.go @@ -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) diff --git a/shortcuts/wiki/wiki.go b/shortcuts/wiki/wiki.go index d60abff..8d9cd84 100644 --- a/shortcuts/wiki/wiki.go +++ b/shortcuts/wiki/wiki.go @@ -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 未找到,请确认仓库是否存在") } diff --git a/shortcuts/wiki/wiki_test.go b/shortcuts/wiki/wiki_test.go index fcf4e8c..cd7743a 100644 --- a/shortcuts/wiki/wiki_test.go +++ b/shortcuts/wiki/wiki_test.go @@ -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 ---