使用 pharos 操作 Azure DevOps
pharos 是一個 CLI 工具,讓您可以從 shell 操作 Azure DevOps,透過環境變數而非瀏覽器登入進行驗證。
下面列出了它能做的所有事情。請勿執行 pharos --help 來查詢 —— 那會耗費多次 API 呼叫,而本節已包含相同資訊。
whoami 查詢 ADO_PAT 屬於誰。@Me 會解析成這個使用者
types 查詢 --state 和 --type 會接受哪些值,依類型區分
query 查詢哪些工作項目 —— 指派給您、在衝刺 (sprint) 中、
特定類型、仍為開啟狀態。回傳的是已充實的項目,而非 ID。
task <id> 一個完整的工作項目:欄位、註解、附件、
包含標題的關聯,以及每個連結維基頁面的內容與討論
--children 一個 EPIC 及其底下的所有項目,僅需
一次呼叫。--depth <n> 用於更深層(最多 5 層)
--compact 扁平化身份資訊,移除面板按鍵
create <type> --title "…" 建立一個工作項目。--parent 會在同一個 PATCH 中處理
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 links <path> 查詢哪些工作項目連結到某個頁面(ADO 沒有 API)
wiki link <path> --item <id> 這是維基端;--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] 一個 Azure DevOps 缺乏的動詞。"<path> - 副本 N"
wiki image <file> 用於頁面或頁面註解的圖片。與 `image` 的端點不同,且名稱是唯一化的
wiki import <file...> .md .txt .docx .pdf .rtf .html 轉換為頁面。
.pptx 會被拒絕,這是故意的——請見下方說明
--under <path> --as <name>
comment list | add | edit | delete 目標可以是工作項目 ID 或維基路徑
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,所以那個做法只會給您十六個裸露的數字,然後需要為每個項目分別呼叫一次才有意義。
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 …" # 逃生口
多個旗標會進行 AND 運算。輸出是已充實的項目 —— id、類型、標題、狀態、代理人、迭代、標籤、優先權、變更日期——所以 --mine 是一個指令,而不是先查詢再逐筆擷取結果。只需對您實際要處理的少數項目執行 pharos task <id>。
在報告數量之前,請先讀取 openness 欄位。 "12 個開啟" 在您知道哪些狀態被視為已完成之前是沒有意義的,因為 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 只涵蓋工作項目類型的類別,而未知的群組會解析為空集合且不會報錯。已於 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 放在與欄位相同的 PATCH 中,因此子項目永遠不會短暫地成為孤兒,而且它接受與 update 相同的旗標名稱。
unlink 會拒絕而不是猜測,這點在遇到之前值得知道。 Azure DevOps 透過關聯在陣列中的位置來移除關聯,因此來自過時讀取的索引會切斷不同的連結,而且請求仍然會成功。unlink 透過身份尋找關聯,並在其讀取的修訂版本上透過 test 操作將其移除。如果該關聯不存在,您會得到退出碼 3 —— 這表示沒有發生任何事,而不是呼叫失敗。
delete 會移至資源回收筒,且需要 --yes;拒絕時會先顯示標題。restore <id> 會將其還原,且不需要任何旗標。
這裡故意不提供永久刪除。 這是 Azure DevOps 中唯一不可逆的動詞,而 --yes 是您已經學會傳遞的旗標。Web UI 擁有它。如果有人真的需要清除,請將他們送到那裡,而不是尋找某個旗標。
文字中的圖片,以及旁邊的檔案
這是兩件不同的事情,選錯是值得避免的錯誤。
pharos attach 225 ./bestsellers.xlsx # 一個檔案,在附件列表中
pharos image ./screenshot.png # 一張圖片,用來放在文字內部
附件會建立 AttachedFile 關聯,並出現在工作項目的附件列表中。內嵌圖片則不會建立任何關聯 —— 在 #333 上實測,貼入描述中的兩張螢幕截圖產生了 relations: 0 和 attachments: []。它只是一個  在欄位文字中,別無其他,這就是為什麼附件列表正確地顯示沒有項目。
因此,image 會輸出一行 markdown,並將寫入動作留給您:
pharos image ./chart.png --pretty
# 
pharos update 225 --field System.Description="$(cat <<'EOF'
Revenue is up. See the chart:

EOF
)"
維基圖片使用的是不同的端點,需要 wiki image。 維基是一個 git 儲存庫,因此那裡的附件是一個檔案,其名稱就是它的身份:上傳第二個 image.png 會覆蓋第一個,而指向 /.attachments/image.png 的每個頁面都會默默地更換圖片。wiki image 會在傳送前使名稱唯一化,並輸出一個儲存庫相對路徑的連結:
pharos wiki image ./diagram.png --pretty
# 
請使用該路徑,不要使用絕對 URL —— 連結到 wit/attachments URL 的頁面,對有 session 的人來說可以正常顯示,但對其他人則會中斷。相同的 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 # 當您已經持有某個附件時
--out 才是真正寫入的參數。 沒有它,您只會得到檔案大小而不會寫入檔案 —— stdout 上的原始位元組會破壞其他每個動詞輸出的 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 應用程式共享,以便兩者從相同檔案產生相同的頁面 —— 其輸出是為了成為維基頁面而設計的,在這裡使用它會將您讀取的內容與應用程式的匯入路徑耦合。它也完全無法讀取 .pptx。
有兩種方式會靜默失敗,都是在真實的工作項目上實測的:
- 您在錯誤的地方檢查工具是否可用。 在系統 PATH 上執行
which markitdown以及對系統的python3執行import docx,在一個那些工具已安裝的機器上都會回傳空值 —— 因為它們安裝在每個技能專用的 venv 中。執行這兩項檢查的代理會得出「這裡沒有東西」的結論,並繞過從頭到尾都存在的工具。在得出結論之前,請先檢查您的代理將其工具放在哪裡。 - 非空的文字並不能證明您已讀取文件。 一個真實的
.pptx提取出七個片段,約 90 個字元;整個規格文件都在兩個內嵌的 PNG 中。每個純文字閱讀器都會為那個簡報回傳一些內容,而且看起來像是運作正常。對於投影片,或掃描的 PDF,有效載荷通常是圖片 —— 將它們提取出來並實際查看。
首先問問您是否能夠原生閱讀 PDF。如果可以,這只是一個指令。
許多代理程式 —— 包括 Claude Code —— 可以視覺化地閱讀 PDF,逐頁閱讀,就像人眼觀看一樣。這涵蓋了完全沒有文字的掃描文件,無需 OCR 步驟。如果這是您的情況,整個問題就簡化為一次轉換:
soffice --headless --convert-to pdf f.docx --outdir ./out # .pptx、.xlsx 也行
# …

