gitlink-cli/doc/reading_notes/03_wiki.md

22 KiB
Raw Blame History

shortcuts/wiki/wiki.go 阅读笔记(面向 Go 小白)


第 1 行:package wiki

字面意思:声明这个文件属于 wiki

运行时作用Go 语言规定每个文件必须属于一个包。包名决定了其他文件如何引用这里的函数/变量。

小白补充

  • 包就像"工具箱"wiki 包就是专门处理 Wiki 功能的工具箱
  • 同一个包下的文件可以直接互相调用函数,不需要导入
  • 包名一般和目录名一致(这里文件在 shortcuts/wiki/ 目录下,所以包名是 wiki

第 3-22 行import 导入依赖

import (
    "encoding/base64"      // Base64 编解码
    "encoding/json"        // JSON 序列化/反序列化
    "errors"               // 错误处理
    "fmt"                  // 格式化输出(类似 Python 的 print
    "net/http"             // HTTP 客户端
    "net/url"              // URL 编码/解析
    "os"                   // 操作系统交互(读文件等)
    "regexp"               // 正则表达式
    "strconv"              // 字符串转数字
    "strings"              // 字符串操作
    "sync"                 // 并发同步(锁、线程安全)
    "time"                 // 时间处理

    "github.com/gitlink-org/gitlink-cli/internal/auth"      // 认证模块
    "github.com/gitlink-org/gitlink-cli/internal/client"    // HTTP 客户端封装
    clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"  // CLI 错误定义
    "github.com/gitlink-org/gitlink-cli/internal/output"    // 输出格式化
    "github.com/gitlink-org/gitlink-cli/shortcuts/common"   // 通用工具
)

字面意思:导入需要用到的外部库/包

运行时作用:告诉 Go 编译器,我需要使用这些包提供的功能。编译时会把这些包的代码链接进来。

小白补充

包名 一句话解释 在本文件中的用途
encoding/base64 把文字转成 Base64 编码 Wiki 内容需要用 Base64 编码后发送
encoding/json 处理 JSON 数据 解析 API 返回的 JSON
errors Go 标准错误处理工具 判断错误类型
fmt 格式化打印 输出错误信息、拼接字符串
net/http HTTP 协议客户端 发送 HTTP 请求
net/url URL 处理 构建查询参数、URL 编码
os 操作系统接口 读取本地文件内容
regexp 正则表达式 匹配 Markdown 链接和图片
strconv 字符串转换 把字符串转成数字
strings 字符串操作 切割、查找、替换字符串
sync 并发同步 提供线程安全的缓存(sync.Map
time 时间处理 设置 HTTP 请求超时
internal/auth 项目内部认证模块 获取带 Token 的 HTTP 客户端
internal/client 项目内部客户端模块 封装 API 调用逻辑
internal/errors 项目内部错误定义 自定义错误类型
internal/output 项目内部输出模块 格式化输出结果JSON/Table
shortcuts/common 通用工具模块 提供 RuntimeContext 等基础结构

第 24 行:var projectIDCache sync.Map

字面意思:声明一个全局变量 projectIDCache,类型是 sync.Map

运行时作用:这是一个线程安全的缓存,用来存储 owner/repo -> projectID 的映射关系,避免重复调用 API 获取项目 ID。

小白补充

  • var 是 Go 声明变量的关键字
  • sync.Map 是 Go 标准库提供的并发安全的 map(普通 map 在多线程下读写会崩溃)
  • projectIDCache 是全局变量(在函数外面声明),整个包内都可以访问
  • 为什么需要缓存?因为每次操作 Wiki 都需要 projectID但获取 projectID 需要调用一次 API缓存可以节省网络请求

第 26-28 行:wikiPath 函数

func wikiPath(endpoint string) string {
    return "/wiki/open/" + endpoint
}

字面意思:定义一个函数 wikiPath,接收一个字符串参数 endpoint返回一个字符串Wiki 功能调用的是 Gateway API (网关 API所有 Wiki 相关的接口都有一个固定的前缀 /wiki/open/

运行时作用:拼接 Wiki API 的路径前缀。比如传入 "wikiPages",返回 "/wiki/open/wikiPages"

小白补充

  • func 是 Go 定义函数的关键字
  • wikiPath(endpoint string):函数名是 wikiPath,参数名是 endpoint,参数类型是 string
  • string(返回类型):表示函数执行完返回一个字符串
  • 这是一个工具函数,用来避免重复写相同的路径前缀

第 30-49 行:getGatewayClient 函数

func getGatewayClient(ctx *common.RuntimeContext) *client.Client {
    baseURL := ctx.GatewayBaseURL
    if baseURL == "" {
        baseURL = "https://gateway.gitlink.org.cn/api"
    }
    httpClient := ctx.GatewayHTTPClient
    if httpClient == nil {
        httpClient = auth.NewHTTPClient()
    }
    return &client.Client{
        HTTP:           httpClient,
        BaseURL:        baseURL,
        SkipJSONSuffix: true,
        Debug:          ctx.Client.Debug,
    }
}

字面意思:定义一个函数 getGatewayClient,接收 *common.RuntimeContext 类型的指针参数 ctx,返回 *client.Client 类型的指针

运行时作用:创建一个专门访问 Wiki Gateway API 的客户端实例。

小白补充

① 为什么需要单独的 Gateway 客户端?

GitLink 的 Wiki API 和主 API 不在同一个域名:

  • 主 APIhttps://www.gitlink.org.cn/api(用于获取项目信息等)
  • Wiki Gateway APIhttps://gateway.gitlink.org.cn/api(专门处理 Wiki 操作)

② 代码逐句解析:

baseURL := ctx.GatewayBaseURL  // 从上下文获取 Gateway 地址
if baseURL == "" {             // 如果没配置,用默认地址
    baseURL = "https://gateway.gitlink.org.cn/api"
}
httpClient := ctx.GatewayHTTPClient  // 获取自定义的 HTTP 客户端
if httpClient == nil {               // 如果没有自定义的,创建一个带认证的默认客户端
    httpClient = auth.NewHTTPClient()
}
return &client.Client{...}  // 创建并返回 Client 结构体实例

③ 结构体初始化语法:

&client.Client{
    HTTP:           httpClient,           // 使用上面创建的 HTTP 客户端
    BaseURL:        baseURL,              // Gateway API 地址
    SkipJSONSuffix: true,                 // 关键Gateway API 不需要 .json 后缀
    Debug:          ctx.Client.Debug,     // 继承调试模式
}
  • & 符号表示取地址返回指针Go 中结构体传参常用指针,避免拷贝)
  • client.Client 是一个结构体类型,里面定义了客户端的各种配置

第 51-58 行:callWikiAPI 函数

func callWikiAPI(ctx *common.RuntimeContext, method, path string, body interface{}) (*output.Envelope, error) {
    gc := getGatewayClient(ctx)
    env, err := gc.Do(method, path, body, nil)
    if err != nil {
        return nil, err
    }
    return unwrapGatewayResponse(env)
}

字面意思:定义函数 callWikiAPI接收上下文、HTTP 方法、路径、请求体,返回 (*output.Envelope, error)

运行时作用:封装对 Wiki Gateway API 的调用流程。

小白补充

① 参数说明:

  • ctx *common.RuntimeContext运行时上下文包含认证信息、owner/repo 等
  • method stringHTTP 方法GET/POST/PUT/DELETE
  • path stringAPI 路径
  • body interface{}请求体可以是任何类型Go 中 interface{} 表示万能类型)

② 返回值说明:

  • (*output.Envelope, error)Go 可以返回多个值!第一个是 API 返回的包装数据,第二个是错误

③ 执行流程:

  1. gc := getGatewayClient(ctx) → 获取 Gateway 客户端
  2. gc.Do(...) → 调用客户端的 Do 方法发送 HTTP 请求
  3. unwrapGatewayResponse(env) → 解析并处理响应(后面会讲)

第 60-67 行:callWikiAPIWithQuery 函数

func callWikiAPIWithQuery(ctx *common.RuntimeContext, method, path string, query url.Values) (*output.Envelope, error) {
    gc := getGatewayClient(ctx)
    env, err := gc.Do(method, path, nil, query)
    if err != nil {
        return nil, err
    }
    return unwrapGatewayResponse(env)
}

字面意思:和 callWikiAPI 类似,但专门用于带查询参数的请求

运行时作用:当需要发送带 ?key=value 查询参数的 GET 请求时使用。

小白补充

  • url.Values 是 Go 标准库类型,本质是 map[string][]string,用来存储 URL 查询参数
  • 比如 ?owner=zzx&repo=test 会被表示为 {"owner": ["zzx"], "repo": ["test"]}

第 69-99 行:unwrapGatewayResponse 函数(核心!)

func unwrapGatewayResponse(env *output.Envelope) (*output.Envelope, error) {
    // 1. 尝试把响应数据转成 map
    resp, ok := env.Data.(map[string]interface{})
    if !ok {
        return env, nil  // 不是 map 格式,直接返回
    }
    
    // 2. 检查响应中的 code 字段
    if code, ok := resp["code"]; ok {
        switch v := code.(type) {
        case float64:
            // HTTP 2xx 都算成功(包括 200/201/204 等)
            if v < 200 || v >= 300 {
                // 错误情况:提取错误信息
                msg, _ := resp["msg"].(string)
                kind := clierrors.KindServer
                if int(v) == 404 {
                    kind = clierrors.KindNotFound
                } else if int(v) == 401 || int(v) == 403 {
                    kind = clierrors.KindForbidden
                }
                // 返回自定义错误
                return nil, clierrors.New(kind, msg,
                    "检查 owner/repo 是否正确,或确认仓库已在 GitLink 网页端开启 Wiki 功能")
            }
        }
    }
    
    // 3. 如果响应有 data 字段,提取出来作为新的响应数据
    if innerData, ok := resp["data"]; ok {
        return output.SuccessEnvelope(innerData, env.Meta), nil
    }
    
    return env, nil
}

字面意思"拆开" Gateway API 的响应,提取真正的数据

运行时作用:处理 Gateway API 返回的特殊格式,统一成标准的 Envelope 结构。

小白补充

① Gateway API 的响应格式:

Gateway API 返回的 JSON 格式是这样的:

{
    "code": 200,
    "msg": "success",
    "data": { "真正的数据在这里" }
}

而我们需要的是直接拿到 data 里面的内容。

② 类型断言Go 的特色语法):

resp, ok := env.Data.(map[string]interface{})
  • 这是类型断言,把 env.Data(类型是 interface{})转换成 map[string]interface{}
  • ok 是一个布尔值,表示转换是否成功
  • 如果转换失败(比如 env.Data 是个字符串而不是 mapok 就是 false

③ switch type 语法:

switch v := code.(type) {
case float64:
    // code 是浮点数类型时执行这里
}
  • 这是 Go 的类型 switch,用来判断一个 interface{} 变量的具体类型
  • JSON 解析数字时,默认会转成 float64 类型

④ 为什么要判断 2xx 状态码?

if v < 200 || v >= 300 {
    // 错误处理
}
  • HTTP 状态码中200-299 表示成功
  • 之前的代码只判断了 200/201导致 DELETE 返回 204No Content时被误判为失败
  • 现在扩展到所有 2xx 都算成功

第 101-135 行:resolveProjectID 函数(核心!)

func resolveProjectID(ctx *common.RuntimeContext) (string, error) {
    // 1. 生成缓存 key
    key := ctx.Owner + "/" + ctx.Repo
    
    // 2. 先查缓存
    if cached, ok := projectIDCache.Load(key); ok {
        return cached.(string), nil  // 缓存命中,直接返回
    }
    
    // 3. 缓存没命中,调用主 API 获取项目详情
    path := fmt.Sprintf("/%s/%s/detail", ctx.Owner, ctx.Repo)
    env, err := ctx.CallAPI("GET", path, nil)
    if err != nil {
        return "", fmt.Errorf("failed to fetch project details (needed for projectId): %w", err)
    }
    
    // 4. 从响应中提取 project_id
    data, ok := env.Data.(map[string]interface{})
    if !ok {
        return "", fmt.Errorf("unexpected response from project detail API")
    }
    
    pid, ok := data["project_id"]
    if !ok {
        return "", fmt.Errorf("project_id not found in project detail response")
    }
    
    // 5. 处理 project_id 的不同类型(可能是 float64 或 int
    var pidStr string
    switch v := pid.(type) {
    case float64:
        pidStr = fmt.Sprintf("%.0f", v)
    case int:
        pidStr = fmt.Sprintf("%d", v)
    default:
        pidStr = fmt.Sprintf("%v", v)
    }
    
    // 6. 存入缓存
    projectIDCache.Store(key, pidStr)
    return pidStr, nil
}

字面意思:根据 owner/repo 解析出项目的数字 ID

运行时作用Wiki API 需要 projectId(数字),但用户只知道 owner/repo(字符串),这个函数就是做转换的。

小白补充

① 为什么需要 projectID

GitLink 的 Wiki Gateway API 设计要求传入数字形式的 projectId,而不是字符串形式的 owner/repo。所以必须先调用主 API 获取项目详情,从中提取 project_id

② 缓存机制:

if cached, ok := projectIDCache.Load(key); ok {
    return cached.(string), nil
}
  • projectIDCache.Load(key) 从缓存中查找
  • 如果找到(ok == true),直接返回缓存的值,不需要再调用 API
  • 这是性能优化,避免重复请求

③ fmt.Sprintf 的用法:

path := fmt.Sprintf("/%s/%s/detail", ctx.Owner, ctx.Repo)
  • 类似 Python 的 "%s/%s/detail" % (owner, repo)
  • %s 是占位符,会被后面的参数替换

④ ctx.CallAPI 是什么?

env, err := ctx.CallAPI("GET", path, nil)
  • ctx*common.RuntimeContext 类型
  • CallAPIRuntimeContext 结构体的方法(后面会详细讲)
  • 它内部调用 ctx.Client.Do() 发送 HTTP 请求

第 137-140 行:parseProjectIDInt 函数

func parseProjectIDInt(pid string) int {
    n, _ := strconv.Atoi(pid)
    return n
}

字面意思:把字符串形式的 projectID 转成整数

运行时作用Wiki API 的某些接口要求 projectId 是整数类型,所以需要转换。

小白补充

  • strconv.Atoi 是 string convert to int 的缩写
  • _ 是 Go 语言的"忽略符",表示忽略返回的错误(这里假设 pid 一定是合法数字)

第 142-154 行:resolveUpdateContent 函数

func resolveUpdateContent(ctx *common.RuntimeContext, text, filePath string) (string, error) {
    if text != "" {
        return text, nil  // 直接使用提供的文本
    }
    if filePath != "" {
        data, err := os.ReadFile(filePath)  // 从文件读取
        if err != nil {
            return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
        }
        return string(data), nil
    }
    return "", fmt.Errorf("no content provided")
}

字面意思:解析更新 Wiki 时的内容来源

运行时作用:支持两种方式提供内容:直接文本(--cover)或文件路径(--file)。


第 156-179 行:fetchPageContent 函数(带自动重试)

func fetchPageContent(ctx *common.RuntimeContext, projectID, pageName string) (content string, actualPageName string, err error) {
    // 第一次尝试
    c, actual, err := fetchPageContentOnce(ctx, projectID, pageName)
    if err == nil {
        return c, actual, nil  // 成功了,直接返回
    }
    
    // 第一次失败,且 pageName 不带 ".-" 后缀,自动重试
    if !strings.HasSuffix(pageName, ".-") {
        c2, actual2, err2 := fetchPageContentOnce(ctx, projectID, pageName+".-")
        if err2 == nil {
            return c2, actual2, nil  // 重试成功
        }
    }
    
    // 两次都失败
    return "", "", fmt.Errorf("获取 Wiki 页面现有内容失败: %w", err)
}

字面意思:获取 Wiki 页面的明文内容,带自动重试机制

运行时作用:解决 GitLink 后端的一个命名问题。

小白补充

GitLink 创建 Wiki 页面时,会自动给内部存储的 sub_url 追加 ".-" 后缀。但 wiki +list 返回的 title 不带后缀。

比如:

  • 用户创建页面 "Home"
  • 后端实际存储的 key 是 "Home.-"
  • 但 list API 返回的 title 是 "Home"

所以用 "Home" 去查询会 404必须用 "Home.-" 才能查到。

② 自动重试逻辑:

  1. 先用原始 pageName 尝试查询
  2. 如果失败,且 pageName 不带 ".-" 后缀
  3. 自动用 pageName+".-" 重试一次

第 181-205 行:fetchPageContentOnce 函数(单次查询)

func fetchPageContentOnce(ctx *common.RuntimeContext, projectID, pageName string) (string, string, error) {
    // 构建查询参数
    q := url.Values{}
    q.Set("owner", ctx.Owner)
    q.Set("repo", ctx.Repo)
    q.Set("projectId", projectID)
    q.Set("pageName", pageName)
    
    // 调用 API
    env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
    if err != nil {
        return "", "", err
    }
    
    // 解析响应
    data, ok := env.Data.(map[string]interface{})
    if !ok {
        return "", "", fmt.Errorf("unexpected response from getWiki")
    }
    
    // 提取 base64 编码的内容
    b64, _ := data["content_base64"].(string)
    if b64 == "" {
        return "", pageName, nil  // 内容为空,返回空字符串
    }
    
    // Base64 解码
    decoded, err := base64.StdEncoding.DecodeString(b64)
    if err != nil {
        return "", "", fmt.Errorf("failed to decode page content: %w", err)
    }
    
    return string(decoded), pageName, nil
}

字面意思:单次尝试获取 Wiki 页面内容

运行时作用:发送 GET 请求到 /wiki/open/getWiki,获取页面数据并解码。

小白补充

① URL 查询参数构建:

q := url.Values{}
q.Set("owner", ctx.Owner)
  • url.Values 是 map 类型,用来存储查询参数
  • 最终会变成 ?owner=zzx&repo=test&projectId=12345&pageName=Home

② Base64 编解码:

b64, _ := data["content_base64"].(string)  // 获取 base64 编码的内容
decoded, err := base64.StdEncoding.DecodeString(b64)  // 解码
return string(decoded), pageName, nil  // 转成字符串返回
  • Wiki API 返回的内容是 Base64 编码的(可能是为了支持二进制文件)
  • 需要解码才能得到人类可读的文本

第 207-216 行:fetchWikiPage 函数

func fetchWikiPage(ctx *common.RuntimeContext, projectID, pageName string) (*output.Envelope, error) {
    q := url.Values{}
    q.Set("owner", ctx.Owner)
    q.Set("repo", ctx.Repo)
    q.Set("projectId", projectID)
    q.Set("pageName", pageName)
    return callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
}

字面意思:获取 Wiki 页面的完整响应(不解码)

运行时作用:和 fetchPageContent 类似,但返回完整的 Envelope 而不是解码后的文本。


第 218-230 行:resolveContent 函数

func resolveContent(ctx *common.RuntimeContext) (string, error) {
    if content := ctx.Arg("content"); content != "" {
        return content, nil
    }
    if filePath := ctx.Arg("file"); filePath != "" {
        data, err := os.ReadFile(filePath)
        if err != nil {
            return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
        }
        return string(data), nil
    }
    return "", fmt.Errorf("--content or --file is required to provide wiki page content")
}

字面意思:解析创建 Wiki 时的内容来源

运行时作用:支持 --content 直接传内容,或 --file 从文件读取。

小白补充

  • ctx.Arg("content") 是从命令行参数中获取 --content 的值
  • 如果两个参数都没提供,返回错误

第 232-249 行:cleanWikiList 函数

func cleanWikiList(env *output.Envelope) {
    // 把 data 转成 slice
    items, ok := env.Data.([]interface{})
    if !ok {
        return
    }
    
    // 遍历每个 wiki 页面
    for _, item := range items {
        m, ok := item.(map[string]interface{})
        if !ok {
            continue
        }
        
        // 删除不需要的字段
        delete(m, "wiki_clone_link")
        
        // URL 解码 sub_url
        if raw, ok := m["sub_url"].(string); ok {
            if decoded, err := url.QueryUnescape(raw); err == nil {
                m["sub_url"] = decoded
            }
        }
    }
}

字面意思:清理 Wiki 列表数据

运行时作用:对 wiki +list 返回的数据进行清洗,去掉无用字段,解码 URL。


第 251-299 行:outputWithDecodedContent 函数

func outputWithDecodedContent(ctx *common.RuntimeContext, env *output.Envelope) error {
    data := env.Data
    
    // 处理 JSON 字符串形式的 data
    if raw, ok := data.(json.RawMessage); ok {
        var m map[string]interface{}
        if err := json.Unmarshal(raw, &m); err == nil {
            data = m
            env.Data = m
        }
    }
    
    // 转成 map
    m, ok := data.(map[string]interface{})
    if !ok {
        return ctx.Output(env)
    }
    
    // content_base64 → content重命名并解码
    if b64, ok := m["content_base64"].(string); ok && b64 != "" {
        if decoded, err := base64.StdEncoding.DecodeString(b64); err == nil {
            m["content"] = string(decoded)
            delete(m, "content_base64")  // 删除原字段
        }
    }
    
    // sidebar / footer 原地解码
    for _, field := range []string{"sidebar", "footer"} {
        if b64, ok := m[field].(string); ok && b64 != "" {
            if decoded, err := base64.StdEncoding.DecodeString(b64); err == nil {
                m[field] = string(decoded)
            }
        }
    }
    
    return ctx.Output(env)
}

字面意思:解码 Wiki 响应中的所有 Base64 字段,用明文替换

运行时作用:让返回的 Wiki 内容更易读,同时节省 tokenBase64 编码会增加约 33% 的体积)。


第 301-526 行Lint 相关