This commit is contained in:
2026-05-13 20:40:14 +08:00
parent bd13c842a8
commit c8a75ef2d5
17 changed files with 920 additions and 93 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strings"
@@ -45,8 +46,8 @@ type apiUser struct {
func NewClient(baseURL string, auth Auth) *Client {
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
auth: auth,
baseURL: strings.TrimRight(baseURL, "/"),
auth: auth,
httpClient: &http.Client{Timeout: 20 * time.Second},
}
}
@@ -80,7 +81,7 @@ func (c *Client) ListOrgSkills(ctx context.Context, org string) ([]domain.Remote
Name: repo.Name,
FullName: repo.FullName,
Description: repo.Description,
CloneURL: repo.CloneURL,
CloneURL: normalizeCloneURL(repo.CloneURL, c.baseURL),
SSHURL: repo.SSHURL,
DefaultBranch: repo.DefaultBranch,
UpdatedAt: repo.UpdatedAt,
@@ -175,3 +176,34 @@ func (c *Client) newRequest(ctx context.Context, method, endpoint string) (*http
req.Header.Set("Accept", "application/json")
return req, nil
}
func normalizeCloneURL(cloneURL, baseURL string) string {
clone, err := url.Parse(cloneURL)
if err != nil || clone.Scheme == "" || clone.Host == "" {
return cloneURL
}
if clone.Scheme != "http" && clone.Scheme != "https" {
return cloneURL
}
if !isLoopbackHost(clone.Hostname()) {
return cloneURL
}
base, err := url.Parse(baseURL)
if err != nil || base.Scheme == "" || base.Host == "" {
return cloneURL
}
if clone.Scheme == base.Scheme && strings.EqualFold(clone.Host, base.Host) {
return cloneURL
}
clone.Scheme = base.Scheme
clone.Host = base.Host
return clone.String()
}
func isLoopbackHost(host string) bool {
if strings.EqualFold(host, "localhost") {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}

View File

@@ -44,6 +44,31 @@ func TestListOrgSkillsFiltersReposWithoutSkillMD(t *testing.T) {
}
}
func TestListOrgSkillsNormalizesLoopbackCloneURLToConfiguredBaseURL(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/orgs/skills/repos", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode([]apiRepo{
{Name: "good", FullName: "skills/good", CloneURL: "http://localhost:3000/skills/good.git", DefaultBranch: "main"},
})
})
mux.HandleFunc("/api/v1/repos/skills/good/contents/SKILL.md", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
server := httptest.NewServer(mux)
defer server.Close()
client := NewClient(server.URL, Auth{Type: AuthPassword, Username: "alice", Secret: "secret"})
repos, err := client.ListOrgSkills(context.Background(), "skills")
if err != nil {
t.Fatalf("ListOrgSkills returned error: %v", err)
}
want := server.URL + "/skills/good.git"
if repos[0].CloneURL != want {
t.Fatalf("CloneURL = %q, want %q", repos[0].CloneURL, want)
}
}
func TestListOrgSkillsUsesTokenAuth(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/orgs/skills/repos", func(w http.ResponseWriter, r *http.Request) {