fix: 修复 label API 路径和字段名,batch-label 现在实际生效

- Label 列表 API 路径从 /v1/:owner/:repo/labels 改为 /:owner/:repo/labels(v0 前缀)
- Label 数据解析从 env.Data 改为从 env.Data["issue_tags"] 提取
- Issue PATCH 中 label 字段从 label_ids 改为 issue_tag_ids(GitLink 正确字段名)
- 新增 getTagIDs() 函数处理 GET issue 返回的 tags 对象数组
- 端到端验证:add/remove/set/dry-run/幂等/多标签/数字ID 全部通过
This commit is contained in:
wauxing 2026-06-01 12:01:43 +08:00
parent a4507ca9e9
commit d183bc2c5d
5 changed files with 40 additions and 10 deletions

View File

@ -395,7 +395,7 @@ func ResolveUserID(ctx *common.RuntimeContext, name string) (int, error) {
// ResolveLabelID 把 label 名称解析为数字 label ID。
// 若 name 本身是数字则直接返回;否则首次按当前仓库拉取全部 label
// GET /v1/{owner}/{repo}/labels并按仓库维度缓存后续直接走缓存。
// GET /{owner}/{repo}/labelsv0 前缀)并按仓库维度缓存,后续直接走缓存。
func ResolveLabelID(ctx *common.RuntimeContext, name string) (int, error) {
if id, err := strconv.Atoi(name); err == nil {
return id, nil
@ -416,14 +416,24 @@ func ResolveLabelID(ctx *common.RuntimeContext, name string) (int, error) {
resolverCacheMu.Unlock()
// 缓存未命中,从 API 拉取该仓库的全部 label
env, err := ctx.CallAPI("GET", v1RepoPath(ctx)+"/labels", nil)
env, err := ctx.CallAPI("GET", fmt.Sprintf("/%s/%s/labels", ctx.Owner, ctx.Repo), nil)
if err != nil {
return 0, fmt.Errorf("resolve label: %w", err)
}
items, err := parseDataArray(env.Data)
// API 返回 {"status":0, "issue_tags":[...], ...}client.Do 把整个响应包在 envelope 里,
// 所以 env.Data 是包含 issue_tags 键的 map需要先提取 issue_tags 再解析数组。
rawMap, ok := env.Data.(map[string]interface{})
if !ok {
return 0, fmt.Errorf("resolve label: unexpected response type %T", env.Data)
}
itemsRaw, ok := rawMap["issue_tags"]
if !ok {
return 0, fmt.Errorf("resolve label: response missing issue_tags field")
}
items, err := parseDataArray(itemsRaw)
if err != nil {
return 0, fmt.Errorf("resolve label: parse data: %w", err)
return 0, fmt.Errorf("resolve label: parse issue_tags: %w", err)
}
// 在锁内把当前仓库的 label 全量写入按 repoKey 隔离的缓存

View File

@ -138,7 +138,7 @@ func createIssueFromRow(ctx *common.RuntimeContext, row []string, titleCol, body
if err != nil {
return fmt.Errorf("label %q: %w", labels, err)
}
body["label_ids"] = labelIDs
body["issue_tag_ids"] = labelIDs
}
if pri := getCell(row, priorityCol); pri != "" {
pid, err := strconv.Atoi(pri)

View File

@ -94,7 +94,7 @@ func manageIssueLabels(ctx *common.RuntimeContext, number string, action string,
body := map[string]interface{}{
"subject": current.Subject,
"description": current.Description,
"label_ids": finalIDs,
"issue_tag_ids": finalIDs,
}
_, err = ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
if err != nil {

View File

@ -15,7 +15,7 @@ var updateFieldMapping = map[string]string{
"state": "status_id",
"assignee": "assigner_ids",
"milestone": "fixed_version_id",
"label": "label_ids",
"label": "issue_tag_ids",
"priority": "priority_id",
}
@ -127,12 +127,12 @@ func applyIssueUpdates(ctx *common.RuntimeContext, number string, row []string,
}
body["fixed_version_id"] = id
}
case "label_ids":
case "issue_tag_ids":
labelIDs, err := resolveLabelArgs(ctx, val, "")
if err != nil {
return fmt.Errorf("issue #%s label %q: %w", number, val, err)
}
body["label_ids"] = labelIDs
body["issue_tag_ids"] = labelIDs
case "priority_id":
pid, err := strconv.Atoi(val)
if err != nil {

View File

@ -265,7 +265,7 @@ func fetchIssueData(ctx *common.RuntimeContext, number string) (*IssueData, erro
AssignedToID: getMapInt(issueMap, "assigned_to_id"),
FixedVersionID: getMapInt(issueMap, "fixed_version_id"),
PriorityID: getMapInt(issueMap, "priority_id"),
LabelIDs: getMapIntSlice(issueMap, "label_ids"),
LabelIDs: getTagIDs(issueMap, "tags"),
}
return data, nil
}
@ -307,6 +307,26 @@ func getMapIntSlice(m map[string]interface{}, key string) []int {
return ids
}
// getTagIDs 从 map 中提取 tag 对象数组中每个对象的 id 字段。
// API 返回 tags: [{id: 1, name: "bug"}, ...],需要遍历对象提取 id。
// 类型不匹配或缺失时返回 nil。
func getTagIDs(m map[string]interface{}, key string) []int {
raw, ok := m[key].([]interface{})
if !ok {
return nil
}
ids := make([]int, 0, len(raw))
for _, item := range raw {
if tag, ok := item.(map[string]interface{}); ok {
id := getMapInt(tag, "id")
if id > 0 {
ids = append(ids, id)
}
}
}
return ids
}
func normalizeIssueStatus(state string) (interface{}, error) {
switch strings.ToLower(strings.TrimSpace(state)) {
case "open":