Communitygithub.com

ASNNetworks/floh-skills

由 FLOH 提供的 AI Agent 技能。为 Claude Code、Codex、Gemini CLI、Cursor 及其他 Agent 客户端提供可移植的 SKILL.md 能力。

floh-skills 是什么?

该技能是一个为 AI 编程助手(如 Claude Code、Cursor、Codex 等)设计的、高度优化的 Azure DevOps CLI 工具 `pharos` 的完整说明书。它详细描述了如何通过环境变量(`ADO_PAT`)进行认证,并提供了从查询工作项、创建/更新/删除、关联链接、管理附件和 Wiki,到处理评论、服务钩子等全方位操作。与直接使用 `curl` 和 WIQL API 不同,`pharos` 封装了复杂的身份认证和 API 调用流程,将 `pharos task <id>`、`pharos query` 等命令作为原子化的“技能单元”,直接返回经过数据水合(hydrated)的完整工作项信息,而非原始 ID。该技能文档通过大量示例(如 `pharos query --mine`、`pharos image`)和针对 Agent 特性的警告(如不同工具对 PDF 的读取方式的差异、图片附件与内联图片的区别),指导 Agent 如何高效、安全地执行 DevOps 任务,避免常见的陷阱(例如错误地猜测工具路径、误将内联图片当作附件处理)。

兼容平台✓Claude Code✓Codex CLI✓Cursor✓Gemini CLI
npx skills add ASNNetworks/floh-skills

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

预览

来自技能 README

floh-skills preview
chart
diagram

文档

使用 pharos 操作 Azure DevOps

pharos 是一个 CLI 工具,让你可以从 shell 中操作 Azure DevOps,通过环境变量而非浏览器登录进行认证。

它能做的所有事情都在下面列出。不要运行 pharos --help 来查找——那会浪费多次调用,而本节包含了相同的信息。

whoami                         查出 `ADO_PAT` 属于谁。`@Me` 解析为此身份
types                          查出 `--state` 和 `--type` 对于每种类型所接受的值
query                          查询哪些工作项——分配给您的、在某个迭代中的、
                               某个类型的、仍未关闭的。返回的是数据已水合的项,而不是 ID。
task <id>                      获取一个工作项的完整信息:字段、评论、
                               附件、带标题的关系、以及每个关联 Wiki 页面的内容和讨论
                               --children  在一次调用中获取一个 EPIC 及其所有子项。
                               --depth <n> 获取更深层级(最多 5 级)
                               --compact   展平身份信息,去掉看板键
create <type> --title "…"      创建一个工作项。`--parent` 在同一个请求中完成
update <id>                    更改字段:`--state`、`--priority`、`--assignee`、
                               `--title`,或使用 `--field Name=value` 更改其他任何字段
link <id> --parent <id>        关联两个工作项。也支持 `--child`、`--related`、
unlink <id> --parent <id>      `--predecessor`、`--successor`、`--duplicate`
link <id> --wiki-page <path>   将 Wiki 页面链接到工作项——是工作项侧的操作
wiki links <path>              查出哪些工作项链接了一个页面(ADO 没有此 API)
wiki link <path> --item <id>   是 Wiki 侧的操作;`--item` 可重复
wiki unlink <path> --item <id>
history <id>                   查出每个字段的更改内容、更改者和更改时间
iterations | areas             查迭代(带日期)和区域
links                          查出该组织拥有的每种关系类型,以及哪个 `--flag` 可达到该类型(约十八种中的六种)
fields [--type T]              字段可接受的值——允许的值列表
attach <id> <file>             将文件上传到工作项(附件列表)
detach <id> <url-or-guid>     移除一个附件。需要 `--yes`。或者使用 `--name <file>`
download <item-id|guid|url>   读取附件。如果是工作项 ID,使用 `--name <file>`,
                               或者使用 `--all --out <dir>`。需要 `--out`,否则不写入文件
image <file>                   上传一张图片以在文本中使用。打印要粘贴的
                               markdown 代码。这不等同于 `attach`。
people [query]                 查找可以被 @提及的用户,格式为 `@<guid>`
delete <id> --yes              移动到回收站(出于安全考虑,未提供永久删除功能)
restore <id>                   从回收站恢复一个工作项
deleted                        查看回收站中的内容及名称。
                               `--top <n>`——默认返回 50 个,并在 count 字段报告实际总数
wiki list | tree | read <path> | write <path> | delete <path>
wiki move <path> <new path>    移动页面;子页面会一起移动
wiki rename <path> <new name>  相同调用,但仅影响叶子页面。两者都需要 `--yes` 确认
wiki duplicate <path> [to]     API 缺乏的复制功能。"<path> - Copy N"
wiki image <file>              用于页面或页面评论的图片。与 `image` 的端点不同,名称会去重
wiki import <file...>          将 `.md`、`.txt`、`.docx`、`.pdf`、`.rtf`、`.html` 转换为页面。
                               会拒绝 `.pptx`,原因见下文。
                               `--under <path>`、`--as <name>`
comment list | add | edit | delete  目标 `<target>` 可以是工作项 ID 或 Wiki 路径
comment react | unreact | reactors  支持 like、dislike、heart、hooray、smile、confused
hooks list | check | create | repoint | delete   用于实时通知的服务钩子。
                               `check` 需要 `--hub <url>`;`list` 会显示已使用的 URL
plan <file>                    实施计划 → 创建工作项树
setup                           组织、项目、令牌 → 保存到钥匙串和 shell 配置文件

文本输入:--text / --file / --stdin。全局参数:--pretty 用于人类可读输出,--yes 用于破坏性操作,--dry-run 用于预览。

ADO_ORG、ADO_PROJECT 和 ADO_PAT 已在环境变量中(安装后)。工作前无需检查它们;缺少时会在退出代码为 2 时显示 "kind": "config",这是唯一需要注意的情况。

查找工作项:pharos query

不要使用 curl 和 WIQL 端点。 这个技能曾经提供过相关的操作指南,因为当时没有 query 命令。但现在有了,而且它弥补了原有方案的不足:WIQL 只返回 ID,所以之前的方法会返回 16 个数字,然后需要一个一个地去请求才能让它们有意义。

pharos query --mine                       # 分配给您的、仍未关闭的工作项
pharos query --sprint "Sprint 1"          # `--sprint current` 用于 @currentIteration
pharos query --type Epic --state Doing    # 两者都可重复
pharos query --assignee "[email protected]"
pharos query --tag api --all              # `--all` 包含已完成的工作
pharos query --wiql "SELECT [System.Id] FROM WorkItems WHERE …"   # 逃生通道

多个 Flag 之间是 AND 关系。输出是数据已水合的项——包含 id、type、title、state、assignee、iteration、tags、priority、changed——所以 --mine 一个命令就搞定,无需先查后取。只有在你真正打算处理的那几个工作项上,才需要用 pharos task <id> 跟进。

在报告数量之前,先检查 openness 字段。 如果不清楚哪些状态被视为“已完成”,"12 open" 就毫无意义。Azure DevOps 允许流程模板重命名所有状态。query 会读取项目自身的状态分类,并告知哪些状态被当作“终止状态”;如果无法读取,它会说明并回退到猜测模式,这时你需要显式传递 --state 参数。assignedTo 字段告诉你 @Me 实际解析到了谁——共享或服务令牌可能会让“分配给我”悄悄地变成另一个人。

--all 可以与其他任何筛选条件组合,所以 --mine --all 可以区分“没有任务分配给你”和“所有任务都已关闭”。单独的 pharos query --all 是合法命令,会返回整个项目。

--sprint 既可以接收名称字符串,也可以接收 fieldPath,并且会自动在两者间进行标准化——fieldPath 规则指的是 System.IterationPath 作为字段值。但是,使用名称匹配时是 UNDER(包含子迭代),而 --sprint current 匹配时是 =(精确匹配),所以它们不是相同的查询。

--wiql 也可以接收 FROM WorkItemLinks … MODE (MustContain) 查询,并以相同的方式对 ID 进行数据水合,这让你一次调用就能找到所有携带特定关系类型的工作项。

如果使用 --wiql,不要写 [System.State] NOT IN GROUP 'Completed'。它会解析成功、返回 200,但实际上会匹配所有项——IN GROUP 只覆盖工作项类型分类,未知的 GROUP 会静默地解析为空集。此行为于 2026-08-05 实测确认。

更改工作项

pharos update 225 --state Doing              # 工作推进时移动状态
pharos update 225 --priority 1 --assignee "[email protected]"
pharos update 225 --field Microsoft.VSTS.Scheduling.RemainingWork=3
pharos attach 225 ./bestsellers.xlsx --comment "这些数字"

update 会先读取工作项,然后在 /rev 上以 test 操作的方式应用更改,这样如果有人在你读取和写入之间修改了该工作项,你会收到 conflict 错误,而不是丢失他们的编辑。如果设置的值与当前值相同,则不会写入任何内容,并会给出提示——无意义的 PATCH 操作仍然会增加 System.Rev,从而使该工作项上的所有其他缓存修订版失效。--dry-run 会显示修改前后的对比,但不会写入任何数据。没有 --yes 参数:字段编辑是常规编辑,Azure DevOps 会保留所有修订版本。

attach 会上传字节,然后将它们作为 AttachedFile 关系链接起来——两个调用,一个命令。附件是不可变的:两次上传同一个文件会产生两个副本,没有替换或版本控制机制。detach 用于移除附件;它需要 --yes,通过附件标识(而非位置)查找附件,并且在 Azure DevOps 中保留字节,因此重新链接 URL 可以恢复附件。使用 --name <file> 指定文件名或传递 GUID——与 download 类似,如果两个文件同名,则会拒绝操作,而不是无差别地移除其中一个。

create 会在同一个请求中将 --parent 与字段一同提交,因此子项永远不会短暂地成为“孤儿”,并且它接受与 update 相同的参数名。

unlink 会拒绝操作,而不是去猜测,这一点在遇到之前值得了解。 Azure DevOps 通过数组中的位置来移除关系,因此基于过期读取的索引可能会切错链接,但请求仍然会成功。unlink 通过标识找到关系,并在其读取的修订版本上以 test 操作的方式移除它。如果关系不存在,你会收到退出代码 3——这意味着操作没有发生,而不是调用失败。

delete 会将工作项移动到回收站,并且需要 --yes 确认;拒绝操作时会先显示工作项的标题。restore <id> 可以将工作项恢复,并且不需要任何标志。

此处故意不提供永久删除功能。 这是 Azure DevOps 中唯一不可逆的操作,而 --yes 是你已经学会传递的标志。Web 界面拥有此权限。如果确实需要永久清除数据,请让他们通过 Web 界面操作,而不是寻找某个命令。

图片和文件:两者不同

这是两个不同的概念,弄混了是常见的错误,值得注意。

pharos attach 225 ./bestsellers.xlsx    # 这是文件,会出现在附件列表中
pharos image ./screenshot.png           # 这是图片,用于放入文本内容中

附件 会创建一个 AttachedFile 关系,并显示在工作项的附件列表中。内联图片 不会创建任何关系——在 #333 工作项上实测:两张粘贴到描述中的截图,结果显示 relations: 0 且 attachments: []。它只是字段文本中的一个 ![](…) 标记,没有其他关联,因此附件列表不显示它完全是正确的。

所以,image 命令会打印一行 markdown 代码,然后由你将其写入字段:

pharos image ./chart.png --pretty
# ![chart](https://dev.azure.com/…/wit/attachments/<guid>?fileName=chart.png)

pharos update 225 --field System.Description="$(cat <<'EOF'
收入增长了。参见下图:

![chart](https://dev.azure.com/…/wit/attachments/<guid>?fileName=chart.png)
EOF
)"

Wiki 图片是一个不同的端点,需要使用 wiki image。 Wiki 是一个 git 仓库,因此那里的附件是文件,其文件名就是身份标识:再次上传 image.png 会覆盖第一个文件,并且所有指向 /.attachments/image.png 的页面都会静默地更改图片。wiki image 在发送前会将文件名唯一化,并打印一个仓库相对路径:

pharos wiki image ./diagram.png --pretty
# ![diagram](/.attachments/diagram-1786046773042.png)

请使用这个路径,不要使用绝对 URL——指向 wit/attachments 的页面链接对于有会话的用户可以渲染,但对于其他人则会失效。同样的 markdown 代码在页面和页面评论中都有效。

从工作项下载附件

pharos task <id> 会列出所有附件,包括每个文件的 GUID——在 JSON 输出和 --pretty 中都有。然后可以通过名称一次调用获取:

pharos download 41 --name Skills.pptx --out ./Skills.pptx
pharos download 41 --all --out ./attachments      # 获取工作项上的所有附件
pharos download <guid-or-url> --out ./file.bin    # 当你已经持有某个 GUID 时

--out 参数才是真正写入文件的操作。 没有它你只能得到文件大小,没有实际文件——单纯的原始字节输出到标准输出会破坏其他命令的 JSON 输出。

单独的 pharos download 41 是一个用法错误,但它会同时列出所有附件——名称、GUID 和大小——因此选择正确的文件永远不会多花一次调用。如果存在两个同名附件,命令会拒绝执行而不是猜测:附件是不可变的,所以上传同一个文件两次会产生两个副本,名称本身不是唯一标识。

detach 接受相同的 --name <file> 参数,所以你在看到 task --pretty 的输出后想要执行的命令,可以直接使用它显示的参数:

pharos detach 41 --name Skills.pptx --yes      # `--dry-run` 可以预览

读取附件内容是你的职责,使用你自带的工具

download 只是把文件保存到磁盘,然后就结束了。将 .docx、.pptx、.xlsx 或 .pdf 转换为可读内容是你所在环境的工作。

不要为此使用 pharos-convert。 它是 wiki import 的转换器,与 macOS 应用共享,以便两者能从同一文件生成相同的页面——它的输出被设计为成为 wiki 页面,在这里使用它会将你的读取行为与应用导入路径耦合起来。此外,它根本无法读取 .pptx。

有两种方式会导致静默失败,均在真实工作项上实测验证过:

  • 你在错误的位置检查了工具是否存在。 在系统 PATH 中查找 which markitdown,以及在系统 python3 中执行 import docx,如果这些工具安装在特定技能的虚拟环境中,那么在机器上检查时都会返回空。如果 AI Agent 运行了这两项检查,就会得出“这里没有任何工具”的结论,从而绕开那些唾手可得的工具。在得出结论之前,请检查你的 Agent 把工具放在了哪里。
  • 非空的文本输出不能证明你已成功读取文档。 一个真实的 .pptx 文件提取出了七个片段,大约 90 个字符;而整个说明文档嵌在两个 PNG 图片中。任何纯文本读取器都会为该演示文稿返回一些内容,看起来像是正常工作。但对于幻灯片或扫描的 PDF 文件,有效载荷通常都在图片里——请提取图片并实际查看它们。

首先,确认你是否能原生读取 PDF。如果可以,那么下一步就是一条命令的事。

许多 AI Agent——包括 Claude Code——可以像人看 PDF 一样视觉地、逐页地读取 PDF。这完全涵盖了没有文字的扫描件,无需 OCR 步骤。如果你有这样能力,那么整个问题就简化为一个转换步骤:

soffice --headless --convert-to pdf f.docx --outdir ./out    # 也支持 .pptx 和 .xlsx
# 将输出目录中的文件发送给 AI Agent 处理:
# marcophy-<date>-<rand>.pdf
# 然后直接读取该 pdf(或它的截图)

无论是在本地还是通过远程解释器运行,soffice 都是标准工具,不会改变你的技能环境。--headless 模式不需要显示器。--convert-to pdf 可以处理 .docx、.pptx 和 .xlsx——对于这三种格式,这是实现“从二进制文件到 AI 可读内容”转换的最简单、最通用的单一命令,适用于所有 AI Agent。


pharos plan:读取文件,创建工作项树

pharos plan ./spec.md           # spec.md 中的每一行标题都成为一个工作项
pharos plan --sprint "Sprint 2" # 将这些工作项放入指定的迭代

plan 会读取一个 markdown 文件,解析其标题结构(# ## ###),并为每个标题创建一个工作项,包括子级和父级关系。它读取的文档与你用 read 读取的文档不同——它忠实地遵循 markdown 标题级别。

  • 一个 # 类型的标题创建的类型是 Epic
  • 一个 ## 类型的标题创建的类型是 Feature
  • 一个 ### 类型的标题创建的类型是 User Story(或 Product Backlog Item,取决于流程模板——它会读取模板并选择正确的那个)
  • 一个 #### 类型的标题创建的类型是 Task(子级)

如果一个文件以 ## Feature 级别开始,那么它不会在顶层创建一个 Epic——plan 会遵守文件已有的结构。如果你想要一个根工作项,可以在文件开头加一个 # The Epic Title。

spec.md 中的文本会成为工作项描述。如果是有序列表或无序列表中的文本,它会在描述字段中转换为 markdown。

plan 会读取文件,然后将计划文本作为描述、将每行视为一个工作项的仪式,以一次调用完成。在调用之后,执行 pharos query --parent <id>(使用从 plan 返回的根工作项 ID)来查看生成的树。pharos task <id> --children 也会做同样的事情。

--dry-run 会打印将要创建的内容,且不进行任何调用。


pharos setup:使认证持久化

开发环境中的setup 命令:

pharos setup    # 提示输入组织、项目和令牌
# 保存在钥匙串中 + shell 配置文件中

它不会让你登录,只是存储密钥,绑定到当前项目的名称。

CI 环境中的 pharos setup

设置环境变量:

export ADO_ORG=your-org
export ADO_PROJECT=your-project
export ADO_PAT=your-pat

然后,不需要运行 pharos setup。setup 只是保存密钥,而环境变量是即时生效的。如果环境变量中已经存在这三个值,pharos 会直接使用它们;如果不存在,它会查找钥匙串中的条目。同时设置两者时,环境变量优先。

认证失败(缺失或无效的令牌)总是表现为 exit 2,并输出 "kind": "config"。如果看到这个错误,请仔细检查组织名称——ADO_ORG 通常是 https://dev.azure.com/{org} 中的 {org} 部分,而不是 dev.azure.com/{org} 本身。

认证失败可能是错误的组织名称——而不是错误的令牌。如果你认为令牌是正确的,请检查 ADO_ORG 这个环境变量。


使用 pharos wiki 处理 Wiki 页面

Wiki 命令接受一个路径参数。路径可以是以下任一形式:

  • 相对路径:my-page 或 folder/my-page(相对于项目 wiki 的根目录)
  • 绝对路径:/my-page 或 /folder/my-page(也是相对于项目 wiki 的根目录——/ 前缀是可选的)
  • 带有 .md 扩展名的路径:my-page.md(也是可选的)

路径区分大小写——Azure DevOps Wiki 支持大小写不同的页面。

wiki read 会输出页面的 markdown 内容。wiki write 会用给定的文本替换页面内容。wiki list 可以显示项目 wiki 的目录树。

pharos wiki list                      # 所有页面,以树状结构显示
pharos wiki list --page "Folder/My%20Page"  # 列出该页面及其子页面
pharos wiki read "我的页面"            # 将 markdown 内容输出到标准输出
pharos wiki read "我的页面" --pretty   # 以格式化方式显示页面内容
pharos wiki write "我的页面" --text "# 新标题"
pharos wiki write "我的页面" --file ./new-content.md
pharos wiki write "我的页面" --stdin <<'EOF'
# 来自标准输入的内容
在这里写入文本...
EOF

wiki delete 会移除一个页面。如果该页面有子页面,它们不会被删除——命令会拒绝执行。

pharos wiki delete "旧页面" --yes          # 删除单页
pharos wiki delete "带子页面的文件夹" --yes  # 如果有子页面,会拒绝——你应该先移动或删除子页面

wiki move 会重命名一个页面,它的子页面也会一起移动。

pharos wiki move "旧名称" "新名称" --yes     # 重命名页面,子页面会跟随
pharos wiki move "旧文件夹" "新文件夹" --yes  # 重命名文件夹(页面名称中的路径部分)

wiki rename 是只影响叶子页面的 wiki move。两者都需要 --yes。

wiki duplicate 会创建一个副本。如果未指定新路径,则会添加 "- Copy N" 后缀。

pharos wiki duplicate "我的页面"                          # 创建为 "我的页面 - Copy 1"
pharos wiki duplicate "我的页面" "我的页面 - 版本 2"       # 使用特定名称

使用 pharos comment 处理评论

评论可以属于工作项或 Wiki 页面。<target> 参数可以是工作项 ID 或 Wiki 路径。

pharos comment list 225                    # 工作项 225 的所有评论
pharos comment list "我的页面"             # Wiki 页面的所有评论
pharos comment add 225 --text "已审阅。"  # 添加评论
pharos comment add "我的页面" --text "需要更多详情。"
pharos comment edit 225 --id <id> --text "已审阅。实际上还有问题。"
pharos comment delete "我的页面" --id <id> --yes

pharos comment react 允许你为评论添加反应(表情符号)。有效的反应类型:like、dislike、heart、hooray、smile、confused。

pharos comment react 225 --id <id> like              # 点赞
pharos comment unreact 225 --id <id> like            # 取消点赞
pharos comment reactors 225 --id <id>                # 列出所有反应和反应者

使用 pharos hooks 处理服务钩子

服务钩子是实时通知。pharos hooks list 会显示当前有效的钩子。

pharos hooks list                        # 列出友好 URL 中的钩子
pharos hooks check --hub "https://hooks.slack.com/…"   # 检查 URL 是否已被使用
pharos hooks create --hub "https://hooks.slack.com/…" --event workitem.updated
pharos hooks repoint <id> --hub "https://hooks.slack.com/…"  # 更改目标 URL
pharos hooks delete <id> --yes           # 移除钩子

hooks list 和 hooks check 对于避免重复非常有用。hooks repoint 允许你保持相同的事件,但更改目标。


使用 pharos plan 进行实现计划

创建一个类似 markdown 的文件,包含标题级别,以规划工作。然后使用 pharos plan 来创建工作项树。

pharos plan ./my-plan.md --sprint "Sprint 1"

plan 会读取文件,为每个标记为 Epic、Feature、User Story 或 Task 的标题创建对应类型的工作项,并为它们建立父子关系。--sprint 是可选的,用于指定所有工作项的迭代路径。

使用 pharos people 查找人员

pharos people                      # 列出项目中的所有用户
pharos people "ada"                # 搜索包含 "ada" 的用户

输出格式为 @<guid>,可以与 pharos update 一起使用。

故障排除与最佳实践

  1. 使用 --dry-run 预览更改:在运行可能修改大量数据或破坏性的命令之前,可以使用 --dry-run 来查看会发生什么,而无需实际执行。
  2. 附件与图片:image 用于文本中的内联图片;attach 用于文件。不要混淆它们。
  3. PDF 读取:首先检查你的 Agent 是否支持原生视觉 PDF 阅读。如果是,使用 soffice --headless --convert-to pdf 将文档转换为 PDF,然后直接读取。
  4. 在安静的环境中检查工具路径:在技能特定的虚拟环境或路径中查找 markitdown 或 python-docx,而不仅仅是在全局系统环境中查找。
  5. 认证:总是来自环境变量或钥匙串。通过 whoami 验证认证状态。exit 2 + "kind": "config" 总是认证/配置问题。

相关技能