forked from Gitlink/gitlink-cli
706 lines
22 KiB
Markdown
706 lines
22 KiB
Markdown
# shortcuts/wiki/wiki.go 阅读笔记(面向 Go 小白)
|
||
|
||
---
|
||
|
||
## 第 1 行:`package wiki`
|
||
|
||
**字面意思**:声明这个文件属于 `wiki` 包
|
||
|
||
**运行时作用**:Go 语言规定每个文件必须属于一个包。包名决定了其他文件如何引用这里的函数/变量。
|
||
|
||
**小白补充**:
|
||
- 包就像"工具箱",`wiki` 包就是专门处理 Wiki 功能的工具箱
|
||
- 同一个包下的文件可以直接互相调用函数,不需要导入
|
||
- 包名一般和目录名一致(这里文件在 `shortcuts/wiki/` 目录下,所以包名是 `wiki`)
|
||
|
||
---
|
||
|
||
## 第 3-22 行:import 导入依赖
|
||
|
||
```go
|
||
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` 函数
|
||
|
||
```go
|
||
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` 函数
|
||
|
||
```go
|
||
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 不在同一个域名:
|
||
- 主 API:`https://www.gitlink.org.cn/api`(用于获取项目信息等)
|
||
- Wiki Gateway API:`https://gateway.gitlink.org.cn/api`(专门处理 Wiki 操作)
|
||
|
||
### ② 代码逐句解析:
|
||
|
||
```go
|
||
baseURL := ctx.GatewayBaseURL // 从上下文获取 Gateway 地址
|
||
if baseURL == "" { // 如果没配置,用默认地址
|
||
baseURL = "https://gateway.gitlink.org.cn/api"
|
||
}
|
||
```
|
||
|
||
```go
|
||
httpClient := ctx.GatewayHTTPClient // 获取自定义的 HTTP 客户端
|
||
if httpClient == nil { // 如果没有自定义的,创建一个带认证的默认客户端
|
||
httpClient = auth.NewHTTPClient()
|
||
}
|
||
```
|
||
|
||
```go
|
||
return &client.Client{...} // 创建并返回 Client 结构体实例
|
||
```
|
||
|
||
### ③ 结构体初始化语法:
|
||
|
||
```go
|
||
&client.Client{
|
||
HTTP: httpClient, // 使用上面创建的 HTTP 客户端
|
||
BaseURL: baseURL, // Gateway API 地址
|
||
SkipJSONSuffix: true, // 关键:Gateway API 不需要 .json 后缀
|
||
Debug: ctx.Client.Debug, // 继承调试模式
|
||
}
|
||
```
|
||
|
||
- `&` 符号表示取地址,返回指针(Go 中结构体传参常用指针,避免拷贝)
|
||
- `client.Client` 是一个**结构体类型**,里面定义了客户端的各种配置
|
||
|
||
---
|
||
|
||
## 第 51-58 行:`callWikiAPI` 函数
|
||
|
||
```go
|
||
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 string`:HTTP 方法(GET/POST/PUT/DELETE)
|
||
- `path string`:API 路径
|
||
- `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` 函数
|
||
|
||
```go
|
||
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` 函数(核心!)
|
||
|
||
```go
|
||
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 格式是这样的:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"msg": "success",
|
||
"data": { "真正的数据在这里" }
|
||
}
|
||
```
|
||
|
||
而我们需要的是直接拿到 `data` 里面的内容。
|
||
|
||
### ② 类型断言(Go 的特色语法):
|
||
|
||
```go
|
||
resp, ok := env.Data.(map[string]interface{})
|
||
```
|
||
|
||
- 这是**类型断言**,把 `env.Data`(类型是 `interface{}`)转换成 `map[string]interface{}`
|
||
- `ok` 是一个布尔值,表示转换是否成功
|
||
- 如果转换失败(比如 `env.Data` 是个字符串而不是 map),`ok` 就是 `false`
|
||
|
||
### ③ switch type 语法:
|
||
|
||
```go
|
||
switch v := code.(type) {
|
||
case float64:
|
||
// code 是浮点数类型时执行这里
|
||
}
|
||
```
|
||
|
||
- 这是 Go 的**类型 switch**,用来判断一个 `interface{}` 变量的具体类型
|
||
- JSON 解析数字时,默认会转成 `float64` 类型
|
||
|
||
### ④ 为什么要判断 2xx 状态码?
|
||
|
||
```go
|
||
if v < 200 || v >= 300 {
|
||
// 错误处理
|
||
}
|
||
```
|
||
|
||
- HTTP 状态码中,200-299 表示成功
|
||
- 之前的代码只判断了 200/201,导致 DELETE 返回 204(No Content)时被误判为失败
|
||
- 现在扩展到所有 2xx 都算成功
|
||
|
||
---
|
||
|
||
## 第 101-135 行:`resolveProjectID` 函数(核心!)
|
||
|
||
```go
|
||
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`。
|
||
|
||
### ② 缓存机制:
|
||
|
||
```go
|
||
if cached, ok := projectIDCache.Load(key); ok {
|
||
return cached.(string), nil
|
||
}
|
||
```
|
||
|
||
- `projectIDCache.Load(key)` 从缓存中查找
|
||
- 如果找到(`ok == true`),直接返回缓存的值,不需要再调用 API
|
||
- 这是**性能优化**,避免重复请求
|
||
|
||
### ③ fmt.Sprintf 的用法:
|
||
|
||
```go
|
||
path := fmt.Sprintf("/%s/%s/detail", ctx.Owner, ctx.Repo)
|
||
```
|
||
|
||
- 类似 Python 的 `"%s/%s/detail" % (owner, repo)`
|
||
- `%s` 是占位符,会被后面的参数替换
|
||
|
||
### ④ ctx.CallAPI 是什么?
|
||
|
||
```go
|
||
env, err := ctx.CallAPI("GET", path, nil)
|
||
```
|
||
|
||
- `ctx` 是 `*common.RuntimeContext` 类型
|
||
- `CallAPI` 是 `RuntimeContext` 结构体的**方法**(后面会详细讲)
|
||
- 它内部调用 `ctx.Client.Do()` 发送 HTTP 请求
|
||
|
||
---
|
||
|
||
## 第 137-140 行:`parseProjectIDInt` 函数
|
||
|
||
```go
|
||
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` 函数
|
||
|
||
```go
|
||
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` 函数(带自动重试)
|
||
|
||
```go
|
||
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 后端的命名 bug:
|
||
|
||
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` 函数(单次查询)
|
||
|
||
```go
|
||
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 查询参数构建:
|
||
|
||
```go
|
||
q := url.Values{}
|
||
q.Set("owner", ctx.Owner)
|
||
```
|
||
|
||
- `url.Values` 是 map 类型,用来存储查询参数
|
||
- 最终会变成 `?owner=zzx&repo=test&projectId=12345&pageName=Home`
|
||
|
||
### ② Base64 编解码:
|
||
|
||
```go
|
||
b64, _ := data["content_base64"].(string) // 获取 base64 编码的内容
|
||
decoded, err := base64.StdEncoding.DecodeString(b64) // 解码
|
||
return string(decoded), pageName, nil // 转成字符串返回
|
||
```
|
||
|
||
- Wiki API 返回的内容是 Base64 编码的(可能是为了支持二进制文件)
|
||
- 需要解码才能得到人类可读的文本
|
||
|
||
---
|
||
|
||
## 第 207-216 行:`fetchWikiPage` 函数
|
||
|
||
```go
|
||
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` 函数
|
||
|
||
```go
|
||
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` 函数
|
||
|
||
```go
|
||
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` 函数
|
||
|
||
```go
|
||
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 内容更易读,同时节省 token(Base64 编码会增加约 33% 的体积)。
|
||
|
||
---
|
||
|
||
## 第 301-526 行:Lint 相关 |