internal/workflow + cli:gitea.py 工作流命令 Go 化(#6)

- internal/workflow:Client 介面+判定語義(最後留言、作者/審核者跟進、
  assignee 交集、最久未更新優先),含假 client 單元測試
- internal/workflow/giteaapi.go:以 internal/gitea(#5)實作 Client
- internal/cli:註冊 whoami/orgs/members/mine/pulls/next;全域選項允許
  出現在命令之後;結束碼 0/2/3
- 與 gitea.py 交叉驗證:next/mine/pulls/members(--has-work) 拾取一致
- 依 #5 分支 feature/issue5-gitea-client 為基底
This commit is contained in:
2026-09-10 08:39:35 +08:00
parent 6951817313
commit 5583a00bb5
10 changed files with 1604 additions and 6 deletions
+242
View File
@@ -0,0 +1,242 @@
// workflow_commands.go — gitea.py 工作流命令的 CLI 接線(#6):
// whoami、orgs、members [--has-work]、mine、pulls [--mine|--reviewer] [--repo]、next。
//
// 輸出走 internal/gitea 的 Printer(--output json|table);錯誤經 cli.exitCodeFor
// 映射結束碼(用法 2/API 3)。token 解析選序見 internal/gitea/auth.go。
package cli
import (
"errors"
"flag"
"fmt"
"strings"
"gitea.alterminal.com/alterminal/teai/internal/gitea"
"gitea.alterminal.com/alterminal/teai/internal/workflow"
)
func init() {
register := func(c *command) {
commands[c.name] = c
}
register(&command{name: "whoami", usage: "目前帳號", run: runWhoami})
register(&command{name: "orgs", usage: "我所屬的組織", run: runOrgs})
register(&command{name: "members", usage: "Agents 團隊成員;--has-work 只列有未完成工作者", run: runMembers})
register(&command{name: "mine", usage: "分派給我、待跟進的 issues", run: runMine})
register(&command{name: "pulls", usage: "跨倉庫 PR 跟進清單(--mine/--reviewer/--repo)", run: runPulls})
register(&command{name: "next", usage: "下一件該做的事", run: runNext})
}
// errNoRepoFlag 是 --repo 值格式錯誤。
var errNoRepoFlag = errors.New("--repo 需為 owner/repo 形式")
// newWorkflowClient 依全域選項建立 workflow 所需的 API client。
func newWorkflowClient(env *Env) (*workflow.Scanner, error) {
token, _, err := gitea.ResolveToken(gitea.AuthOptions{
FlagToken: env.Globals.Token,
SiteURL: env.Globals.URL,
ConfigPath: env.Globals.ConfigPath,
})
if err != nil {
return nil, err
}
client, err := gitea.New(env.Globals.URL, token, env.Globals.Timeout)
if err != nil {
return nil, err
}
return &workflow.Scanner{Client: &workflow.APIClient{Client: client}}, nil
}
// printer 依全域選項建立輸出器。
func printer(env *Env) gitea.Printer {
return gitea.Printer{Format: env.Globals.Output, Out: env.Out}
}
// runWhoami:teai whoami。
func runWhoami(env *Env, args []string) error {
if len(args) != 0 {
return &ErrUsage{Msg: "whoami 不接受參數"}
}
s, err := newWorkflowClient(env)
if err != nil {
return err
}
name, err := s.Whoami()
if err != nil {
return err
}
return printer(env).PrintOne(
[]gitea.Field{{Name: "username"}},
gitea.Values{name},
)
}
// runOrgs:teai orgs。
func runOrgs(env *Env, args []string) error {
if len(args) != 0 {
return &ErrUsage{Msg: "orgs 不接受參數"}
}
s, err := newWorkflowClient(env)
if err != nil {
return err
}
orgs, err := s.Orgs()
if err != nil {
return err
}
rows := make([]gitea.Values, 0, len(orgs))
for _, o := range orgs {
rows = append(rows, gitea.Values{o})
}
return printer(env).PrintList(
[]gitea.Field{{Name: "username"}},
rows,
)
}
// runMembers:teai members [--has-work]。輸出每行一個帳號(同 gitea.py)。
func runMembers(env *Env, args []string) error {
fs := flag.NewFlagSet("teai members", flag.ContinueOnError)
fs.SetOutput(env.Err)
hasWork := fs.Bool("has-work", false, "只列出有未完成工作的成員")
if err := fs.Parse(args); err != nil {
return &ErrUsage{Msg: err.Error()}
}
if fs.NArg() != 0 {
return &ErrUsage{Msg: "members 不接受位置參數"}
}
s, err := newWorkflowClient(env)
if err != nil {
return err
}
names, err := s.Members(*hasWork)
if err != nil {
return err
}
// gitea.py members 每行輸出一個帳號(非 JSON);保持一致以便交叉驗證。
for _, name := range names {
fmt.Fprintln(env.Out, name)
}
return nil
}
// runMine:teai mine。
func runMine(env *Env, args []string) error {
if len(args) != 0 {
return &ErrUsage{Msg: "mine 不接受參數"}
}
s, err := newWorkflowClient(env)
if err != nil {
return err
}
issues, err := s.Mine()
if err != nil {
return err
}
return printer(env).PrintList(
[]gitea.Field{
{Name: "repo"}, {Name: "number"}, {Name: "title"}, {Name: "url"},
},
issueRows(issues),
)
}
// runPulls:teai pulls [--mine|--reviewer] [--repo owner/repo]。
func runPulls(env *Env, args []string) error {
fs := flag.NewFlagSet("teai pulls", flag.ContinueOnError)
fs.SetOutput(env.Err)
mine := fs.Bool("mine", false, "限定作者是目前帳號")
reviewer := fs.Bool("reviewer", false, "限定自己是 requested reviewer")
repo := fs.String("repo", "", "限定單一倉庫(owner/repo)")
if err := fs.Parse(args); err != nil {
return &ErrUsage{Msg: err.Error()}
}
if fs.NArg() != 0 {
return &ErrUsage{Msg: "pulls 不接受位置參數"}
}
if *mine && *reviewer {
return &ErrUsage{Msg: "--mine 與 --reviewer 不能併用"}
}
var owner, name string
if *repo != "" {
parts := strings.SplitN(*repo, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return &ErrUsage{Msg: errNoRepoFlag.Error()}
}
owner, name = parts[0], parts[1]
}
s, err := newWorkflowClient(env)
if err != nil {
return err
}
items, err := s.Pulls(*mine, *reviewer)
if err != nil {
return err
}
rows := make([]gitea.Values, 0, len(items))
for _, item := range items {
if *repo != "" && (item.Owner != owner || item.Repo != name) {
continue
}
rows = append(rows, gitea.Values{
item.Owner + "/" + item.Repo,
item.Pull.Number,
item.Pull.Title,
item.Pull.WebURL(),
})
}
return printer(env).PrintList(
[]gitea.Field{
{Name: "repo"}, {Name: "number"}, {Name: "title"}, {Name: "url"},
},
rows,
)
}
// runNext:teai next。無工作輸出 null(exit 0)。
func runNext(env *Env, args []string) error {
if len(args) != 0 {
return &ErrUsage{Msg: "next 不接受參數"}
}
s, err := newWorkflowClient(env)
if err != nil {
return err
}
work, err := s.Next()
if err != nil {
return err
}
p := printer(env)
if work == nil {
return p.PrintNull()
}
// gitea.py 的 _work_dict:role 僅 pull 帶(issue 不輸出 role 欄位)。
fields := []gitea.Field{
{Name: "type"}, {Name: "role"}, {Name: "repo"},
{Name: "number"}, {Name: "title"}, {Name: "url"},
}
row := gitea.Values{work.Type, work.Role, work.Repo, work.Number, work.Title, work.URL}
if work.Role == "" {
fields = []gitea.Field{
{Name: "type"}, {Name: "repo"},
{Name: "number"}, {Name: "title"}, {Name: "url"},
}
row = gitea.Values{work.Type, work.Repo, work.Number, work.Title, work.URL}
}
return p.PrintOne(fields, row)
}
// issueRows 把 issues 轉成輸出列(repo 欄位由掃描脈絡補齊)。
func issueRows(issues []workflow.Issue) []gitea.Values {
rows := make([]gitea.Values, 0, len(issues))
for i := range issues {
issue := &issues[i]
rows = append(rows, gitea.Values{
issue.FullName(),
issue.Num(),
issue.Title,
issue.WebURL(),
})
}
return rows
}