[DevOps] 如何用 Webhook API 發訊息到 Microsoft Teams (使用 Workflows)

看到標題你一定想說,這麼簡單的東西,為何要寫一篇文章,
想當然爾,這篇文章一定是伴隨著怒氣寫出來的
沒事一個簡單的事情,它一定要搞得超複雜它才開心 Orz

需求很稀鬆平常,就是我有各種系統通知要串接 Microsoft Teams
簡單來說就是讓機器發訊息到 Teams 的指定群組

前前後後花了好長時間找文件,最後終於找到了
避免大家走彎路,就分享給大家

舊方式 Incoming webhooks (準備棄用)

有全網搜尋過就知道,以前舊的方式是使用 Incoming webhooks,

在 Teams 的左側欄的左下角有個 Apps 按下去,
然後搜尋「Incoming Webhook

會看到這個 Incoming Webhook (to be retired)

按照步驟,選擇你的頻道名稱

按下 Create

然後你就會得到一個 Webhook URL

可能類似長這樣

https://xxxxxxxx.webhook.office.com/webhookb2/6dxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx@xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx3f4/IncomingWebhook/bf0xxxxxxxxxxxxxxxxxxxxxxxxxxx54/40xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx3de/V2xx-xxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxx_xxxxBF

(這個就是 API 需要打的網址,網址每個人都不一樣)

呼叫它最簡單的方式就是,打一個 POST 到這個 URL,
內容帶入這個 JSON

{ "text": "My Test Message example" }

正常會回 200 OK

習慣看 curl 的朋友,也附上 curl 的版本

curl --location 'https://xxxxxxxx.webhook.office.com/webhookb2/6dxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx@xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx3f4/IncomingWebhook/bf0xxxxxxxxxxxxxxxxxxxxxxxxxxx54/40xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx3de/V2xx-xxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxx_xxxxBF' 
--data '{ "text": "My Test Message example" }'

結案

但以上這段已經準備要 Deprecated (棄用) 了

新方式,使用 Workflows

在 Teams 的左側欄的左下角有個 Apps 按鈕,按下去,
然後搜尋「Workflows

可能會很多個都叫 Workflows,找到確定是 Microsoft Corporation 才對哦!

把它加進去 Teams 中

然後在左側標籤,按下 Workflows 的標籤

在上方找到 Create 標籤

等等會用到 Send webhook alerts to a channel 或者 Send webhook alerts to a chat 等等分開說明

Send webhook alerts to a channel(收到 webhook 要求時發佈在頻道中)

搜尋「Send webhook alerts to a channel
中文名稱「收到 webhook 要求時發佈在頻道中」

根據提示,選擇你的 Teams 與 Channel

最後會出現一個網址

Send webhook alerts to a chat(收到 webhook 要求時發佈在聊天中)

搜尋「Send webhook alerts to a chat
中文名稱「收到 webhook 要求時發佈在聊天中」

設定你想要丟訊息的群組名稱

不管這二種方式,最後會出現一個網址,像這樣

https://defaultxxxxxxxxxxxxxxxxxxxxxxxxxxxab0.xx.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/2aaxxxxxxxxxxxxxxxxxxxxxxxxxx77d/triggers/manual/paths/invoke/?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=kmcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFBU

(這個就是 API 需要打的網址,網址每個人都不一樣)

重點來了!!!

他的介面沒有交代你怎麼使用這個 API
在微軟的網站翻找超級久,終於找到他的 API 用法

API 文件在這裡(淚~~)

https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1%2Cdotnet&cf_lbyyhhwhyjj5l3rs65cb3w=j09h5ro1lgi6jiaolddgf#microsoft-teams-webhook

等等直接上範例說明

Workflows 發訊息到 Teams 簡單範例

打 POST 到你剛剛產生的網址

https://defaultxxxxxxxxxxxxxxxxxxxxxxxxxxxab0.xx.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/2aaxxxxxxxxxxxxxxxxxxxxxxxxxx77d/triggers/manual/paths/invoke/?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=kmcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFBU

Body 的部分選 Raw (JSON)
然後打上以下內容

{
  "type": "message",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.adaptive",
      "contentUrl": null,
      "content": {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.5",
        "body": [
          {
            "type": "TextBlock",
            "text": "Hello, World",
            "wrap": true
          }
        ]
      }
    }
  ]
}

這就是最簡單的範例了
(別問我傳個文字訊息為何要搞這麼複雜…)

回應會是 202 Accepted 內容為空白
再檢查一下你的 Channel,應該就會看到訊息了

怕只看 curl 的朋友看不懂,這邊附上 curl 的版本

curl --location 'https://defaultxxxxxxxxxxxxxxxxxxxxxxxxxxxab0.xx.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/2aaxxxxxxxxxxxxxxxxxxxxxxxxxx77d/triggers/manual/paths/invoke/?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=kmcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFBU' 
--header 'Content-Type: application/json' 
--data '{
    "type": "message",
    "attachments": [
        {
            "contentType": "application/vnd.microsoft.card.adaptive",
            "contentUrl": null,
            "content": {
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                "type": "AdaptiveCard",
                "version": "1.5",
                "body": [
                    {
                        "type": "TextBlock",
                        "text": "Hello, World",
                        "wrap": true
                    }
                ]
            }
        }
    ]
}'

訊息長這樣

這個 JSON 格式在這裡是有講究的

  • "type": "message"
  • "contentType": "application/vnd.microsoft.card.adaptive"
  • "contentUrl": null

這些值都是固定的
你能改的地方只有 body 這裡。
它是為了可以做特殊客製卡片訊息,放了很多保留參數在上面。

Workflows 發訊息到 Teams 複雜範例

放一個稍微複雜的範例

{
  "type": "message",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.adaptive",
      "contentUrl": null,
      "content": {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.5",
        "body": [
          {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "This is my title"
          },
          {
            "type": "ColumnSet",
            "columns": [
              {
                "type": "Column",
                "items": [
                  {
                    "type": "TextBlock",
                    "weight": "Bolder",
                    "text": "Matt Hidinger",
                    "wrap": true
                  },
                  {
                    "type": "TextBlock",
                    "spacing": "None",
                    "text": "Created 2025-08-25 21:03",
                    "isSubtle": true,
                    "wrap": true
                  }
                ],
                "width": "stretch"
              }
            ]
          },
          {
            "type": "TextBlock",
            "text": "This is desc",
            "wrap": true
          }
        ]
      }
    }
  ]
}{
  "type": "message",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.adaptive",
      "contentUrl": null,
      "content": {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.5",
        "body": [
          {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "This is Alert Title"
          },
          {
            "type": "ColumnSet",
            "columns": [
              {
                "type": "Column",
                "items": [
                  {
                    "type": "Image",
                    "style": "person",
                    "url": "https://pbs.twimg.com/profile_images/3647943215/d7f12830b3c17a5a9e4afcc370e3a37e_400x400.jpeg",
                    "size": "small"
                  }
                ],
                "width": "auto"
              },
              {
                "type": "Column",
                "items": [
                  {
                    "type": "TextBlock",
                    "weight": "Bolder",
                    "text": "Matt Hidinger",
                    "wrap": true
                  },
                  {
                    "type": "TextBlock",
                    "spacing": "None",
                    "text": "Created 2025-08-25 21:03",
                    "isSubtle": true,
                    "wrap": true
                  }
                ],
                "width": "stretch"
              }
            ]
          },
          {
            "type": "TextBlock",
            "text": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
            "wrap": true
          }
        ]
      }
    }
  ]
}

這個可以做一個類似文章的小卡片

附上 curl 的版本

curl --location 'https://defaultxxxxxxxxxxxxxxxxxxxxxxxxxxxab0.xx.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/2aaxxxxxxxxxxxxxxxxxxxxxxxxxx77d/triggers/manual/paths/invoke/?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=kmcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFBU' 
--data '{
    "type": "message",
    "attachments": [
        {
            "contentType": "application/vnd.microsoft.card.adaptive",
            "contentUrl": null,
            "content": {
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                "type": "AdaptiveCard",
                "version": "1.5",
                "body": [
                    {
                        "type": "TextBlock",
                        "size": "Medium",
                        "weight": "Bolder",
                        "text": "This is Alert Title"
                    },
                    {
                        "type": "ColumnSet",
                        "columns": [
                            {
                                "type": "Column",
                                "items": [
                                    {
                                        "type": "Image",
                                        "style": "person",
                                        "url": "https://pbs.twimg.com/profile_images/3647943215/d7f12830b3c17a5a9e4afcc370e3a37e_400x400.jpeg",
                                        "size": "small"
                                    }
                                ],
                                "width": "auto"
                            },
                            {
                                "type": "Column",
                                "items": [
                                    {
                                        "type": "TextBlock",
                                        "weight": "Bolder",
                                        "text": "Matt Hidinger",
                                        "wrap": true
                                    },
                                    {
                                        "type": "TextBlock",
                                        "spacing": "None",
                                        "text": "Created 2025-08-25 21:03",
                                        "isSubtle": true,
                                        "wrap": true
                                    }
                                ],
                                "width": "stretch"
                            }
                        ]
                    },
                    {
                        "type": "TextBlock",
                        "text": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry'''s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
                        "wrap": true
                    }
                ]
            }
        }
    ]
}'

卡片大概就是這樣使用

它有做一個卡片編輯器可以使用
https://adaptivecards.io/designer

你可以編輯成你喜歡的樣子之後,再拿來使用

取代 attachments[0].content 的部分就好

這樣就可以發送你自訂的訊息了

根據實測,這個 JSON 架構不要亂改
他的欄位是根據他文件定義的 JSON 架構去做的
如果你改了 JSON 架構,一樣會回應 202 Accepted
只是你在設定 Workflows 的時候會選不到指定欄位,這部分我暫時無解

另外它文件有提到小限制,
封包最大大約 28 KB,超過大小會回應 Request Entity too large.

整個測試最謎的地方就在這裡,
除非 JSON 格式錯誤,或者權限不足等問題,
不然他都會回應 202 Accepted

至少,看完整篇你會避掉一些雷,
有些方向不對的文件,就不要參考了
應該…是新版最簡單發訊息的方法,分享給大家。

相關資料

卡片編輯器
https://adaptivecards.io/designer

各種卡片範例
https://adaptivecards.io/samples/

參考資料

[DevOps/RPA] 使用 pyautogui 做自動化 automation

tags: pyautogui

用了一下 pyautogui 這個自動化套件,
覺得這個套件其實還蠻容易上手的,
你可以把它想成某種程式化的「按鍵精靈」
會照著你的想法去操作鍵盤、滑鼠。

在製作 RPA (Robotic Process Automation) 節省時間,增進效率,
會是一個非常關鍵要件

這邊帶你快速上手 pyautogui,算是我的某種 Cheat Sheet 吧

PyAutoGUI 介紹

PyAutoGUI 是一個用於 桌面自動化 的 Python 套件,能模擬滑鼠移動、點擊、拖曳,以及鍵盤輸入等操作。它跨平台支援 Windows、macOS、Linux,非常適合用來撰寫腳本自動完成重複性工作,例如批次截圖、測試 UI 或自動填表。PyAutoGUI 也提供螢幕截圖與簡易畫面影像辨識功能,能根據畫面上的元素定位與操作,讓自動化更靈活易用。

快速上手 pyautogui

鍵盤滑鼠類

列出我幾個常用的 method

按組合鍵

pyautoui.hotkey('win', 'r')

鍵盤上按指定按鍵

pyautogui.press('tab')

鍵盤打字

pyautogui.write('Hello, World.')

把滑鼠移過去點擊

pyautogui.click(100, 50, duration=0.5)

螢幕相關

找圖片上的位置(做定位點)

myAncher = pyautogui.locateOnScreen('button.png', grayscale=True)

螢幕截圖

pyautogui.screenshot('screenshot/1.png')

這邊你可以先螢幕截圖到時候讓程式來便是它

如果找不到圖片會噴 ImageNotFoundException

光這樣就可以玩很多花樣了

警告視窗系列

其他的部分它有簡單的提供一些 Alert, Confirm 的視窗

打開一個 Alert 警告窗

pyautogui.alert('This displays some text with an OK button.')

有時候提示使用者需要用到

取得視窗相關

取得所有視窗物件

pyautogui.getAllWindows()

取得所有視窗標題

pyautogui.getAllTitles()

它官網範例是操作 Windows 計算機
準備各按鈕截圖好的圖片,利用 locateOnScreen() 來定位
click() 來點擊操作計算機

基礎是這樣

參考資料

https://ithelp.ithome.com.tw/articles/10277668
https://pyautogui.readthedocs.io/en/latest/