首页
/ Penpot 的 Taiga API Skill:免认证获取 Issue / User Story / Task 的自包含 CLI 实践

Penpot 的 Taiga API Skill:免认证获取 Issue / User Story / Task 的自包含 CLI 实践

2026-09-05 16:16:40作者:明树来

本文围绕 Penpot 仓库中的 Taiga API skill(.opencode/skills/taiga/SKILL.md)及其配套脚本 scripts/taiga.py 展开,讲解如何在不进行任何认证的情况下,从 Penpot 官方 Taiga 项目管理平台(project id 345963,slug penpot)按 URL 或 <type> <ref> 两种方式拉取单个 Issue、User Story 或 Task,并理解脚本内部的 URL 解析、API 端点映射、按类型差异化的摘要输出与错误处理实现,读完即可在自己的工作流或 Agent 环境中直接复用这套「文档 + 自包含 Python 脚本」的工具化模式。

背景定位:为什么 Penpot 仓库里会有一个 Taiga skill

Penpot 的 CONTRIBUTING.md 明确说明了项目的双 issue 跟踪策略:公开 bug 走 GitHub Issues,而内部项目管理使用 Taiga,且 Changelog 条目会同时引用两者。这也解释了仓库根目录的 CHANGES.md 中为何密集出现形如 Taiga #11964 的引用。

在这样的背景下,仓库把「查询 Taiga 公共数据」这件事封装成了一个 Agent 可用的 skill:

  • skill 文档:.opencode/skills/taiga/SKILL.md,声明了 skill 名称、用途描述与前置依赖(metadata 中声明 requires.bins: ["python3"]);
  • 配套脚本:scripts/taiga.py,仅依赖 Python 标准库的自包含 CLI。

skill 文档原文给出的一句话定位是:Fetch information from Taiga public API for the Penpot project (project id: 345963, slug: penpot). No authentication required — only public project data is accessed. 这一点是整个方案成立的前提:只访问公开项目数据,无需 Token。

前置条件与快速上手

前置条件

skill 文档的 Prerequisites 部分只列出了一条要求:

  • python3 —— 因为 scripts/taiga.py 是自包含脚本(self-contained, stdlib only),只使用 argparsejsonresysurllib.errorurllib.request 等标准库模块(见脚本第 18–23 行的 import 列表),不需要安装任何第三方包。

Quick Start 命令

skill 文档给出的四类基础用法(可直接复制执行):

# Pass a Taiga URL directly
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714

# Or use "<type> <ref>" syntax
python3 scripts/taiga.py us 14128
python3 scripts/taiga.py task 13648

# Add --json for raw output
python3 scripts/taiga.py --json issue 13714

# See full usage
python3 scripts/taiga.py --help

其中 --help 的输出已由 argparsebuild_parser() 生成,包含完整 usage 说明与 Examples 段落(脚本第 186–208 行),无需记忆任何参数即可自助查阅。

双输入方式:Taiga Web URL 与 <type> <ref> 语法

skill 文档提供了一张 URL 模式对照表,用于从 Web 地址提取 typeref

Type Web URL Pattern
Issue https://tree.taiga.io/project/penpot/issue/<REF>
User Story https://tree.taiga.io/project/penpot/us/<REF>
Task https://tree.taiga.io/project/penpot/task/<REF>

提取规则示例:

  • issue/13714 → type=issue, ref=13714
  • us/14128 → type=us, ref=14128
  • task/13648 → type=task, ref=13648

源码实现:URL 解析

这一逻辑在 scripts/taiga.pyparse_taiga_url 中实现,核心是一条正则:

def parse_taiga_url(url: str) -> tuple[str, int] | None:
    """Extract (type, ref) from a tree.taiga.io URL."""
    m = re.search(r"/project/penpot/(issue|us|task)/(\d+)", url)
    if not m:
        return None
    return m.group(1), int(m.group(2))

两个值得注意的细节:

  • 正则硬编码了 project/penpot 路径段,意味着该脚本只服务于 Penpot 这一个项目——这是刻意设计,与 skill 文档「for the Penpot project (project id: 345963)」的定位一致,换取了零配置的易用性;
  • 使用 re.search 而非 re.match,因此 URL 前后可以带其他内容(如查询参数),解析依然成立;不匹配时返回 None,由调用方 main() 打印明确的错误提示并退出。

参数分发逻辑

main()(第 211–246 行)根据位置参数个数走两条分支:

  • 单个参数:按 Taiga URL 解析,解析失败时报错 could not parse Taiga URL 并以退出码 1 结束;
  • 两个参数:按 <type> <ref> 处理,type 必须是 ENDPOINT_MAP 的键(issue / us / task),ref 必须能转为整数,否则分别报 unknown typeref must be a number 错误;
  • 其他参数个数:直接打印 --help 帮助信息并退出。

API 端点映射与 by_ref 查询

类型到端点的映射

脚本用一个字典把三种业务类型映射到 Taiga API v1 的资源端点(第 25–32 行):

API_BASE = "https://api.taiga.io/api/v1"
PROJECT_ID = 345963

ENDPOINT_MAP = {
    "issue": "issues",
    "us": "userstories",
    "task": "tasks",
}

注意 us(User Story)映射到复数端点 userstories——这对应 Taiga REST API 的资源命名约定,也是 skill 文档 URL 表中 us 这一缩写与真实端点之间的桥梁。

by_ref 端点:按 ref 取单条记录

fetch_item(第 59–78 行)构造的请求 URL 形如:

https://api.taiga.io/api/v1/{endpoint}/by_ref?ref={ref}&project={PROJECT_ID}

即调用 issues/by_refuserstories/by_reftasks/by_ref 三个端点,同时用 ref(项目内自增编号,即 Web URL 里那个数字)与 project(项目 id 345963)双重定位记录。这里体现了 Taiga 数据模型的一个特点:ref 是项目级编号而非全局唯一 id,所以查询必须携带 project 参数来限定作用域。

请求通过 urllib.request.urlopen(url, timeout=15) 发起,超时设为 15 秒。异常处理覆盖了三种失败模式,全部输出到 stderr 并返回 None(最终导致进程以退出码 1 结束):

  • urllib.error.HTTPError:打印 HTTP <code> — <reason>;若是 404,额外提示 Item (ref=<ref>) not found in project 345963.,方便区分「网络/服务错误」与「记录不存在」;
  • urllib.error.URLError:打印底层网络原因(如 DNS 失败、连接被拒);
  • json.JSONDecodeError:打印 invalid JSON response 提示。

输出格式:摘要模式与原始 JSON 模式

--json 标志(action="store_true",dest 为 raw_json)决定输出分支:

# 结构化摘要(默认)
python3 scripts/taiga.py issue 13714

# 原始 JSON(indent=2,保留 Unicode 字符)
python3 scripts/taiga.py --json issue 13714

JSON 模式直接 json.dumps(item, indent=2, ensure_ascii=False),输出 API 返回的完整字段,适合二次加工。

摘要格式的结构

默认摘要由 format_summary(第 125–181 行)拼装,与 skill 文档给出的示例一致:

User Story #11964 — 🔴 [DESIGN TOKENS] Typography Composite Input
================================
Status:     Defining
Milestone:  design-systems-sprint-26
Points:     3 role(s)
Assignee:   Natacha Menjibar
Author:     Natacha Menjibar
Created:    2025-09-01
Tags:       iop-design-tokens
URL:        https://tree.taiga.io/project/penpot/us/11964
================================
<full description text, unmodified>

结构上分为三段:标题行({Type} #{ref} — {subject})、字段区块(首行 ==== 分隔符之下)、以及当 description 非空时的第二个分隔符 + 完整描述文本(原文不做任何修改)。

字段区块的按类型差异化

skill 文档明确写道:The fields section includes type-specific information。源码中这一差异化逻辑位于 format_summary 的类型分支(第 143–160 行):

类型 特有字段 取值来源
User Story (us) MilestonePoints milestone_slugpoints 字段是一个字典,摘要展示 len(points) 并标注 role(s)
Task (task) MilestoneParent US milestone_sluguser_story 字段
Issue (issue) Type IDSeverity IDPriority ID type / severity / priority 字段

所有类型共享的字段为:StatusAssigneeAuthorCreatedTagsURL

从源码结构看,这些共享字段的取值依赖 Taiga API 返回的「富化」结构:

  • 状态名_status_name(第 102–106 行)从 status_extra_info 字典中取 name。Taiga 的列表型 API 会附带 *_extra_info 关联对象,这里只取状态名而不是裸 id;
  • 指派人 / 作者_extra_name(第 95–99 行)分别从 assigned_to_extra_infoowner_extra_infofull_name_display,取不到再回退 username
  • 标签_tag_list(第 87–92 行)说明 tags 是 [name, color] 二元组数组,摘要只提取名称并逗号连接;
  • 创建日期created_date[:10] 截取到日(YYYY-MM-DD);
  • URL 回显:用 https://tree.taiga.io/project/penpot/{item_type}/{ref} 重新拼出 Web 地址(第 167 行),与解析阶段使用的 URL 模式互为逆操作;
  • 缺省占位_val 帮助函数(第 83–84 行)为 None 值提供 占位符,保证摘要列对齐、不出现 None

Reference 信息汇总

skill 文档 Reference 一节给出的关键常量(与脚本源码逐一对应):

项目 源码位置
Taiga 实例 tree.taiga.io URL 解析/回显正则与字符串
API base https://api.taiga.io/api/v1 scripts/taiga.py API_BASE
Penpot project id 345963 scripts/taiga.py PROJECT_ID
Penpot project slug penpot URL 模式中的路径段

适用边界与延伸

综合 skill 文档与 scripts/taiga.py 的实现,这套工具有几个明确的能力边界:

  1. 只读、单条查询:仅封装了 by_ref 单条查询路径,没有搜索、分页或列表浏览端点——它是「拿一个已知编号/链接,取详情」的工具,不是完整的 Taiga 客户端;
  2. 项目绑定:正则与 PROJECT_ID 均硬编码为 Penpot 项目,脚本无法直接查询其他 Taiga 项目(这是 skill 文档标题「for the Penpot project」的准确含义);
  3. 免认证前提:依赖 Taiga 公共实例上 Penpot 项目数据对外开放这一事实;脚本不做任何 Token 处理,代码中也不存在认证相关逻辑;
  4. 运行时要求:仅需 python3(脚本使用了 tuple[str, int] | None 这类 3.10+ 类型标注语法,因此实际要求 Python 3.10 及以上;仓库开发环境中的 python3 为 3.12.10,满足要求)。

这套「SKILL.md 声明文档 + 仓库内自包含脚本」的组织方式本身也值得关注:skill 文档承担「给 Agent/人的说明书」角色(何时用、怎么用、URL 模式对照表、输出样例),脚本承担「可执行实现」角色,二者放在 scripts/.opencode/skills/ 下互相引用。与同目录下的其他 skill(如 .opencode/skills/create-issue/SKILL.md 面向 GitHub Issue 创建流程)相比,taiga skill 补齐了「查 Taiga 原始需求/缺陷详情」这一环——而 CHANGES.md 中大量的 Taiga 引用(例如 [Taiga #11964] 一类的 changelog 条目)正是这类查询的典型消费场景:当你在阅读 changelog 或代码中的 Taiga 链接时,无需浏览器认证即可在终端拿到该条目的状态、里程碑、负责人与完整描述。

登录后查看全文
热门项目推荐
相关项目推荐