feat: teai role

This commit is contained in:
ydc3148
2026-09-14 16:13:02 +08:00
parent 8735e35db6
commit 540718dda7
12 changed files with 416 additions and 120 deletions
+28 -29
View File
@@ -1,7 +1,7 @@
// auth.go — 認證來源的解析與選序。
//
// 選序(README「全域介面」):--token 旗標 → TEAI_TOKEN 環境變數 →
// tea 登入組態(TEA_CONFIG 指定的路徑,或 ~/.config/tea/config.yml)。
// teai 登入組態(TEAI_CONFIG 指定的路徑,或 ~/.config/teai/config.yml)。
// token 一律只在記憶體傳遞,不寫入輸出、日誌或錯誤訊息。
package gitea
@@ -23,8 +23,8 @@ const (
SourceFlag
// SourceEnv 是 TEAI_TOKEN 環境變數。
SourceEnv
// SourceTeaConfig 是 tea 登入組態。
SourceTeaConfig
// SourceConfig 是 teai 登入組態。
SourceConfig
)
// String 回傳來源名稱(診斷用)。
@@ -34,8 +34,8 @@ func (s TokenSource) String() string {
return "flag"
case SourceEnv:
return "env"
case SourceTeaConfig:
return "tea-config"
case SourceConfig:
return "config"
default:
return "none"
}
@@ -62,12 +62,12 @@ func osReadFile(path string) ([]byte, error) {
type AuthOptions struct {
// FlagToken 是 --token 旗標值;空字串表示未提供。
FlagToken string
// SiteURL 是目標站點(用來比對 tea 組態中的 login url)。
// SiteURL 是目標站點(用來比對組態中的 login url)。
SiteURL string
// Env 是環境變數查詢;nil 時用 os.LookupEnv。
Env EnvLookup
// ConfigPath 是 tea 組態的明確路徑(TEA_CONFIG 或 --config)。
// 空字串表示用預設 ~/.config/tea/config.yml。
// ConfigPath 是 teai 組態的明確路徑(TEAI_CONFIG 或 --config)。
// 空字串表示用預設 ~/.config/teai/config.yml。
ConfigPath string
// Home 是使用者家目錄(展開 ~ 用);空字串表示不展開。
Home string
@@ -75,7 +75,7 @@ type AuthOptions struct {
ReadFile ReadFileFunc
}
// ResolveToken 依選序解析 token:flag → env(TEAI_TOKEN) → tea 組態。
// ResolveToken 依選序解析 token:flag → env(TEAI_TOKEN) → teai 組態。
//
// 找不到任何 token 時回傳 ("", SourceNone, nil)——查詢公開端點不需要
// token,是否為錯由呼叫端依情境決定。組態檔存在但解析失敗(格式錯誤)
@@ -93,8 +93,8 @@ func ResolveToken(opts AuthOptions) (string, TokenSource, error) {
}
path := opts.ConfigPath
if path == "" {
// --config 未給時,先看 TEA_CONFIG 環境變數。
if p, ok := env("TEA_CONFIG"); ok && strings.TrimSpace(p) != "" {
// --config 未給時,先看 TEAI_CONFIG 環境變數。
if p, ok := env("TEAI_CONFIG"); ok && strings.TrimSpace(p) != "" {
path = strings.TrimSpace(p)
}
}
@@ -110,7 +110,7 @@ func ResolveToken(opts AuthOptions) (string, TokenSource, error) {
if home == "" {
return "", SourceNone, nil
}
path = filepath.Join(home, ".config", "tea", "config.yml")
path = filepath.Join(home, ".config", "teai", "config.yml")
}
if path == "" {
return "", SourceNone, nil
@@ -124,33 +124,32 @@ func ResolveToken(opts AuthOptions) (string, TokenSource, error) {
// 組態不存在或讀不到:視為沒有這個來源,不是錯誤。
return "", SourceNone, nil
}
login, err := parseTeaConfig(data, opts.SiteURL)
login, err := parseConfig(data, opts.SiteURL)
if err != nil {
return "", SourceNone, fmt.Errorf("gitea: parse tea config %s: %w", path, err)
return "", SourceNone, fmt.Errorf("gitea: parse config %s: %w", path, err)
}
if login.Token == "" {
return "", SourceNone, nil
}
return login.Token, SourceTeaConfig, nil
return login.Token, SourceConfig, nil
}
// teaLogin 是 tea 組態中一筆登入資料(只取 teai 需要的欄位)。
type teaLogin struct {
// configLogin 是組態中一筆登入資料(只取解析 token 需要的欄位)。
type configLogin struct {
Name string
URL string
Token string
Default bool
}
// parseTeaConfig 從 tea 的 config.yml 內容挑出最匹配 siteURL 的登入:
// parseConfig 從 teai 的 config.yml 內容挑出最匹配 siteURL 的登入:
// url 主機相同者優先,其中 default: true 最優,否則取第一筆;
// 都不相同時退而取 default: true,再退第一筆。
//
// 僅解析 tea 實際寫出的 YAML 子集:頂層 `logins:` 清單,項目為
// `key: value` 的平面映射。不處理錨點、多文件、巢狀清單——tea 不會寫出
// 這些,遇到也不該默默誤讀。
func parseTeaConfig(data []byte, siteURL string) (teaLogin, error) {
var logins []teaLogin
// 僅解析 teai 寫出的 YAML 子集:頂層 `logins:` 清單,項目為
// `key: value` 的平面映射。不處理錨點、多文件、巢狀清單。
func parseConfig(data []byte, siteURL string) (configLogin, error) {
var logins []configLogin
inLogins := false
cur := -1 // 目前項目在 logins 的索引;-1 表示尚未開始任何項目
for lineNo, raw := range strings.Split(string(data), "\n") {
@@ -171,26 +170,26 @@ func parseTeaConfig(data []byte, siteURL string) (teaLogin, error) {
}
if strings.HasPrefix(trimmed, "- ") || trimmed == "-" {
// 新的清單項目;`- name: x` 形式同行帶鍵值。
logins = append(logins, teaLogin{})
logins = append(logins, configLogin{})
cur = len(logins) - 1
rest := strings.TrimSpace(strings.TrimPrefix(trimmed, "-"))
if rest != "" {
if err := applyLoginField(&logins[cur], rest, lineNo+1); err != nil {
return teaLogin{}, err
return configLogin{}, err
}
}
continue
}
if cur < 0 {
// logins: 底下卻不是清單項目——不是預期的結構。
return teaLogin{}, fmt.Errorf("line %d: expected list item under logins:", lineNo+1)
return configLogin{}, fmt.Errorf("line %d: expected list item under logins:", lineNo+1)
}
if err := applyLoginField(&logins[cur], trimmed, lineNo+1); err != nil {
return teaLogin{}, err
return configLogin{}, err
}
}
if len(logins) == 0 {
return teaLogin{}, nil
return configLogin{}, nil
}
wantHost := hostOf(siteURL)
// 第一輪:主機相符者,default 優先,否則第一筆。
@@ -219,7 +218,7 @@ func parseTeaConfig(data []byte, siteURL string) (teaLogin, error) {
}
// applyLoginField 把 `key: value` 套用到 login;value 的引號會剝除。
func applyLoginField(l *teaLogin, kv string, lineNo int) error {
func applyLoginField(l *configLogin, kv string, lineNo int) error {
key, value, ok := strings.Cut(kv, ":")
if !ok {
return fmt.Errorf("line %d: expected key: value, got %q", lineNo, kv)