[{"content":"「資料驅動測試（data-driven testing）」，你可以用迴圈或是參數化的方式，把多筆資料注入到同一個 test case 裡執行\n方法一：使用 test.describe 搭配迴圈 import { test, expect } from \u0026#39;@playwright/test\u0026#39;; const testData = [ { input: \u0026#39;user1\u0026#39;, expected: \u0026#39;Hello user1!\u0026#39; }, { input: \u0026#39;user2\u0026#39;, expected: \u0026#39;Hello user2!\u0026#39; }, // 其餘48筆... ]; for (const data of testData) { test(`Should greet ${data.input}`, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;https://example.com\u0026#39;); await page.fill(\u0026#39;#name-input\u0026#39;, data.input); await page.click(\u0026#39;#submit-button\u0026#39;); const message = await page.textContent(\u0026#39;#greeting\u0026#39;); expect(message).toBe(data.expected); }); } 方法二：使用 test.each（Playwright v1.31 以上支援） import { test, expect } from \u0026#39;@playwright/test\u0026#39;; const testData = [ [\u0026#39;user1\u0026#39;, \u0026#39;Hello user1!\u0026#39;], [\u0026#39;user2\u0026#39;, \u0026#39;Hello user2!\u0026#39;], // ... ]; test.describe(\u0026#39;Greeting tests\u0026#39;, () =\u0026gt; { test.each(testData)( \u0026#39;should greet %s correctly\u0026#39;, async ({ page }, name, expectedGreeting) =\u0026gt; { await page.goto(\u0026#39;https://example.com\u0026#39;); await page.fill(\u0026#39;#name-input\u0026#39;, name); await page.click(\u0026#39;#submit-button\u0026#39;); const message = await page.textContent(\u0026#39;#greeting\u0026#39;); expect(message).toBe(expectedGreeting); } ); }); 方法三：讀取外部資料（如 CSV/JSON） import { test, expect } from \u0026#39;@playwright/test\u0026#39;; import fs from \u0026#39;fs\u0026#39;; const testData = JSON.parse(fs.readFileSync(\u0026#39;./test-data.json\u0026#39;, \u0026#39;utf8\u0026#39;)); for (const data of testData) { test(`Should greet ${data.name}`, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;https://example.com\u0026#39;); await page.fill(\u0026#39;#name-input\u0026#39;, data.name); await page.click(\u0026#39;#submit-button\u0026#39;); const message = await page.textContent(\u0026#39;#greeting\u0026#39;); expect(message).toBe(data.expected); }); } ","permalink":"https://kyleap.github.io/playwright-04-data-driven-testing/","summary":"「資料驅動測試（data-driven testing）」，你可以用迴圈或是參數化的方式，把多筆資料注入到同一個 test case 裡執行\n方法一：使用 test.describe 搭配迴圈 import { test, expect } from \u0026#39;@playwright/test\u0026#39;; const testData = [ { input: \u0026#39;user1\u0026#39;, expected: \u0026#39;Hello user1!\u0026#39; }, { input: \u0026#39;user2\u0026#39;, expected: \u0026#39;Hello user2!\u0026#39; }, // 其餘48筆... ]; for (const data of testData) { test(`Should greet ${data.input}`, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;https://example.com\u0026#39;); await page.fill(\u0026#39;#name-input\u0026#39;, data.input); await page.click(\u0026#39;#submit-button\u0026#39;); const message = await page.textContent(\u0026#39;#greeting\u0026#39;); expect(message).toBe(data.expected); }); } 方法二：使用 test.each（Playwright v1.31 以上支援） import { test, expect } from \u0026#39;@playwright/test\u0026#39;; const testData = [ [\u0026#39;user1\u0026#39;, \u0026#39;Hello user1!","title":"玩玩 Playwright — 測試案例來自資料集"},{"content":"為什麼用 Playwright 測 API？ 雖然我們常用 Postman、curl 或 Jest 來測 API，但若你的專案已經使用 Playwright 做 E2E 測試，那麼善用同一套框架處理 API 測試，不僅可減少工具切換，還能將 UI 流程與 API 驗證整合在一起，提升測試維運的一致性。\nPlaywright 提供的 request 模組，讓我們可以像在用 fetch 一樣操作 HTTP 請求，非常方便。\n開始寫第一個 API 測試 安裝 Playwright 如果你尚未安裝 Playwright，可以透過下列指令快速安裝：\nnpm init playwright@latest 選擇 TypeScript 或 JavaScript 都可以，本篇範例以 TypeScript 為例。\n撰寫 API 測試 假設我們有一個簡單的 API：https://jsonplaceholder.typicode.com/posts/1\n我們來撰寫一個測試，驗證它回傳的內容正確：\nimport { test, expect, request } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;GET /posts/1 should return a valid post\u0026#39;, async () =\u0026gt; { const apiContext = await request.newContext(); const response = await apiContext.get(\u0026#39;https://jsonplaceholder.typicode.com/posts/1\u0026#39;); expect(response.ok()).toBeTruthy(); expect(response.status()).toBe(200); const body = await response.json(); expect(body).toHaveProperty(\u0026#39;id\u0026#39;, 1); expect(body).toHaveProperty(\u0026#39;userId\u0026#39;); expect(body).toHaveProperty(\u0026#39;title\u0026#39;); }); 這段測試會：\n建立一個新的 API context（不會受到 UI session 影響） 發送 GET 請求 驗證 HTTP 狀態碼與回傳資料欄位 常見應用場景 你可以用 Playwright 來做以下這些 API 測試場景：\n使用者登入流程（POST login，取 token，存到測試 session） CRUD 測試（創建、讀取、更新、刪除資源） 驗證後端錯誤處理（如 400, 401, 500 的回傳） 測試前後端串接是否正確（UI 操作後檢查對應 API 結果） 這也意味著，我們可以混合使用 Playwright 的 UI 操作 + API 驗證，打造更全面的 E2E 測試！\n","permalink":"https://kyleap.github.io/playwright-03-api-testing/","summary":"為什麼用 Playwright 測 API？ 雖然我們常用 Postman、curl 或 Jest 來測 API，但若你的專案已經使用 Playwright 做 E2E 測試，那麼善用同一套框架處理 API 測試，不僅可減少工具切換，還能將 UI 流程與 API 驗證整合在一起，提升測試維運的一致性。\nPlaywright 提供的 request 模組，讓我們可以像在用 fetch 一樣操作 HTTP 請求，非常方便。\n開始寫第一個 API 測試 安裝 Playwright 如果你尚未安裝 Playwright，可以透過下列指令快速安裝：\nnpm init playwright@latest 選擇 TypeScript 或 JavaScript 都可以，本篇範例以 TypeScript 為例。\n撰寫 API 測試 假設我們有一個簡單的 API：https://jsonplaceholder.typicode.com/posts/1\n我們來撰寫一個測試，驗證它回傳的內容正確：\nimport { test, expect, request } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;GET /posts/1 should return a valid post\u0026#39;, async () =\u0026gt; { const apiContext = await request.","title":"玩玩 Playwright — API Testing 初體驗"},{"content":"在現代 Web 開發流程中，自動化測試已經成為持續整合（CI）不可或缺的一環。而 Playwright 作為一個功能強大的 E2E 測試框架，搭配 Azure DevOps Pipeline，可以有效地讓我們在每次 commit 或 PR 時，自動進行跨瀏覽器測試，確保系統品質。\n本文將介紹如何從零開始，將 Playwright 測試整合進 Azure DevOps CI 流程。\n環境準備 專案初始化 npm init -y npm install -D @playwright/test npx playwright install 新增基本測試檔案（如 tests/example.spec.ts） import { test, expect } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;首頁標題應正確\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;https://study4.tw\u0026#39;); await expect(page).toHaveTitle(/study4/); }); 在 package.json 中加入測試腳本 \u0026#34;scripts\u0026#34;: { \u0026#34;test\u0026#34;: \u0026#34;npx playwright test\u0026#34; } 設定 Azure DevOps Pipeline 在專案根目錄新增 .azure-pipelines.yml trigger: - main pool: vmImage: \u0026#39;ubuntu-latest\u0026#39; steps: - task: NodeTool@0 inputs: versionSpec: \u0026#39;18.x\u0026#39; displayName: \u0026#39;Install Node.js\u0026#39; - script: | npm install npx playwright install --with-deps displayName: \u0026#39;Install dependencies\u0026#39; - script: npm run test displayName: \u0026#39;Run Playwright tests\u0026#39; Commit 並 Push 上 repo，即可自動觸發 CI，跑完測試。 加分技巧：測試報告整合 Playwright 內建 HTML 測試報告功能，可透過以下指令產出：\nnpx playwright show-report 若要在 Azure DevOps 呈現報告，可以加上下列任務：\n- task: PublishPipelineArtifact@1 inputs: targetPath: \u0026#39;playwright-report\u0026#39; artifact: \u0026#39;e2e-report\u0026#39; publishLocation: \u0026#39;pipeline\u0026#39; ","permalink":"https://kyleap.github.io/playwright-02-azure-devops-ci/","summary":"在現代 Web 開發流程中，自動化測試已經成為持續整合（CI）不可或缺的一環。而 Playwright 作為一個功能強大的 E2E 測試框架，搭配 Azure DevOps Pipeline，可以有效地讓我們在每次 commit 或 PR 時，自動進行跨瀏覽器測試，確保系統品質。\n本文將介紹如何從零開始，將 Playwright 測試整合進 Azure DevOps CI 流程。\n環境準備 專案初始化 npm init -y npm install -D @playwright/test npx playwright install 新增基本測試檔案（如 tests/example.spec.ts） import { test, expect } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;首頁標題應正確\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;https://study4.tw\u0026#39;); await expect(page).toHaveTitle(/study4/); }); 在 package.json 中加入測試腳本 \u0026#34;scripts\u0026#34;: { \u0026#34;test\u0026#34;: \u0026#34;npx playwright test\u0026#34; } 設定 Azure DevOps Pipeline 在專案根目錄新增 .azure-pipelines.yml trigger: - main pool: vmImage: \u0026#39;ubuntu-latest\u0026#39; steps: - task: NodeTool@0 inputs: versionSpec: \u0026#39;18.","title":"玩玩 Playwright - 整合 Azure DevOps CI"},{"content":"在自動化測試的領域中，Playwright 是一款由 Microsoft 開發的現代化 E2E 測試框架，支援多瀏覽器（Chromium、Firefox、WebKit）、多語言（JavaScript、TypeScript、Python、Java、.NET），非常適合用來測試 Web 應用。\n今天我們就來簡單玩玩 Playwright，透過 TypeScript + VS Code 快速搭建一個測試環境，並實作一個基本案例：打開 Google，搜尋「study4」，點擊第一個搜尋結果，並驗證網址是否為 https://study4.tw。\n開始之前：準備好你的環境 請先確保你已安裝以下工具：\nNode.js（建議版本 18 以上） Visual Studio Code 步驟一：建立專案 mkdir playwright-ts-demo cd playwright-ts-demo npm init -y 步驟二：安裝 Playwright 與 TypeScript 支援 npm install -D playwright typescript ts-node @types/node Playwright 安裝後可以支援 Chromium、Firefox、WebKit 等瀏覽器的自動操作。\n步驟三：建立 TypeScript 設定檔 tsconfig.json { \u0026#34;compilerOptions\u0026#34;: { \u0026#34;target\u0026#34;: \u0026#34;ESNext\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;CommonJS\u0026#34;, \u0026#34;strict\u0026#34;: true, \u0026#34;esModuleInterop\u0026#34;: true, \u0026#34;skipLibCheck\u0026#34;: true, \u0026#34;forceConsistentCasingInFileNames\u0026#34;: true, \u0026#34;moduleResolution\u0026#34;: \u0026#34;node\u0026#34;, \u0026#34;outDir\u0026#34;: \u0026#34;dist\u0026#34; }, \u0026#34;include\u0026#34;: [\u0026#34;src\u0026#34;] } 步驟四：撰寫測試腳本 建立 src 資料夾，新增一個檔案 searchStudy4.ts：\nimport { chromium } from \u0026#39;playwright\u0026#39;; (async () =\u0026gt; { const browser = await chromium.launch({ headless: false }); // 顯示瀏覽器操作 const page = await browser.newPage(); await page.goto(\u0026#39;https://www.google.com\u0026#39;); // 接受 Google cookie（若有出現） const acceptButton = await page.$(\u0026#39;text=接受所有\u0026#39;); if (acceptButton) await acceptButton.click(); // 輸入搜尋關鍵字並送出 await page.fill(\u0026#39;input[name=\u0026#34;q\u0026#34;]\u0026#39;, \u0026#39;study4\u0026#39;); await page.keyboard.press(\u0026#39;Enter\u0026#39;); // 等待搜尋結果載入 await page.waitForSelector(\u0026#39;h3\u0026#39;); // 點擊第一個搜尋結果 const firstResult = await page.locator(\u0026#39;h3\u0026#39;).first(); await firstResult.click(); // 等待跳轉完成 await page.waitForLoadState(\u0026#39;load\u0026#39;); // 驗證網址 const url = page.url(); if (url.includes(\u0026#39;study4.tw\u0026#39;)) { console.log(\u0026#39;成功導向 study4.tw\u0026#39;); } else { console.error(`導向錯誤，實際網址為：${url}`); } await browser.close(); })(); 步驟五：執行測試 npx ts-node src/searchStudy4.ts 額外推薦 VS Code 套件 以下是開發 Playwright 時推薦安裝的 VS Code 套件：\nPlaywright Test for VSCode：提供測試錄製、除錯、自動補全等功能。 ESLint：強化 TypeScript 程式碼品質。 Prettier：保持程式碼一致性。 ","permalink":"https://kyleap.github.io/playwright-01-env/","summary":"在自動化測試的領域中，Playwright 是一款由 Microsoft 開發的現代化 E2E 測試框架，支援多瀏覽器（Chromium、Firefox、WebKit）、多語言（JavaScript、TypeScript、Python、Java、.NET），非常適合用來測試 Web 應用。\n今天我們就來簡單玩玩 Playwright，透過 TypeScript + VS Code 快速搭建一個測試環境，並實作一個基本案例：打開 Google，搜尋「study4」，點擊第一個搜尋結果，並驗證網址是否為 https://study4.tw。\n開始之前：準備好你的環境 請先確保你已安裝以下工具：\nNode.js（建議版本 18 以上） Visual Studio Code 步驟一：建立專案 mkdir playwright-ts-demo cd playwright-ts-demo npm init -y 步驟二：安裝 Playwright 與 TypeScript 支援 npm install -D playwright typescript ts-node @types/node Playwright 安裝後可以支援 Chromium、Firefox、WebKit 等瀏覽器的自動操作。\n步驟三：建立 TypeScript 設定檔 tsconfig.json { \u0026#34;compilerOptions\u0026#34;: { \u0026#34;target\u0026#34;: \u0026#34;ESNext\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;CommonJS\u0026#34;, \u0026#34;strict\u0026#34;: true, \u0026#34;esModuleInterop\u0026#34;: true, \u0026#34;skipLibCheck\u0026#34;: true, \u0026#34;forceConsistentCasingInFileNames\u0026#34;: true, \u0026#34;moduleResolution\u0026#34;: \u0026#34;node\u0026#34;, \u0026#34;outDir\u0026#34;: \u0026#34;dist\u0026#34; }, \u0026#34;include\u0026#34;: [\u0026#34;src\u0026#34;] } 步驟四：撰寫測試腳本 建立 src 資料夾，新增一個檔案 searchStudy4.","title":"玩玩 Playwright - 基本環境設定（TypeScript + VS Code）"},{"content":"在撰寫和編輯 Azure Wiki 上的內容時，圖片是一種非常有用的方式，能可以更好地傳達訊息並增強文章的可讀性。但有時候圖片的大小會導致可讀性下降，這時候就需要調整圖片的大小，以符合頁面的版面設計或減少圖片的檔案大小。\n第一種方式可以用 HTML Tag\n\u0026lt;IMG src=\u0026#34;\u0026lt;圖片網址\u0026gt;\u0026#34; alt=\u0026#34;圖片描述\u0026#34; style=\u0026#34;width: 500px;\u0026#34;/\u0026gt; 但如果你的圖片是複製貼上來的，會存在於 Azure Wiki 這個 Repo，這時可以這樣設定去等比例縮放大小\n![xxx.jpg](/.attachments/xxx.jpg =500x) 或明確的給寬跟高\n![xxx.jpg](/.attachments/xxx.jpg =500x1000) ","permalink":"https://kyleap.github.io/azure-wiki-resize-image/","summary":"在撰寫和編輯 Azure Wiki 上的內容時，圖片是一種非常有用的方式，能可以更好地傳達訊息並增強文章的可讀性。但有時候圖片的大小會導致可讀性下降，這時候就需要調整圖片的大小，以符合頁面的版面設計或減少圖片的檔案大小。\n第一種方式可以用 HTML Tag\n\u0026lt;IMG src=\u0026#34;\u0026lt;圖片網址\u0026gt;\u0026#34; alt=\u0026#34;圖片描述\u0026#34; style=\u0026#34;width: 500px;\u0026#34;/\u0026gt; 但如果你的圖片是複製貼上來的，會存在於 Azure Wiki 這個 Repo，這時可以這樣設定去等比例縮放大小\n![xxx.jpg](/.attachments/xxx.jpg =500x) 或明確的給寬跟高\n![xxx.jpg](/.attachments/xxx.jpg =500x1000) ","title":"如何在 Azure Wiki 調整圖片大小"},{"content":"我會建議在使用 Azure Boards 選定 Scrum/Agile 工作流程後，一定要做“繼承”這件事，如此可以彈性的去設定整個專案的模式\nOrganzation Setting =\u0026gt; Process\n好處1: 可自訂欄位 可自己設計 Layout 調整不必要顯示的欄位\n並且可以增加欄位，譬如增加 Version 用來指定該單子的版號，增加 Selection 來日常管理時挑選單子的輔助欄位\n好處2: 自訂工作流程(State) 如果想要讓狀態更明確，可以自己去擴充或增減，譬如可以考慮多一個 State \u0026ldquo;Pull Request\u0026rdquo;\n好處3: 隱藏不必要的 Work Items 敏捷最重要的精神就是極簡，但如果造內建定義好的 Work Items，也許會讓管理上更綁手綁腳的，通常我會建議可以有兩種玩法\n玩法A: 只用 Product Backlogs 或者是 User Story 任何事情都應該對應到商業價值，要解決的問題也都是對應到“人”，用一種方式，依照 User Story 的撰寫方式把它寫好，會讓事情更單純\n玩法B: Product Backlogs + User Story + Bug 將 PBI 定義成從團隊內發起的單子，譬如重構類型，User Story 定義成需求面，Bug 單子內容設計用來專注還原操作步驟 用這種玩法可以進一步撈出每次 Sprint 所著重的比例\n","permalink":"https://kyleap.github.io/azure-boards-inheritance-process/","summary":"我會建議在使用 Azure Boards 選定 Scrum/Agile 工作流程後，一定要做“繼承”這件事，如此可以彈性的去設定整個專案的模式\nOrganzation Setting =\u0026gt; Process\n好處1: 可自訂欄位 可自己設計 Layout 調整不必要顯示的欄位\n並且可以增加欄位，譬如增加 Version 用來指定該單子的版號，增加 Selection 來日常管理時挑選單子的輔助欄位\n好處2: 自訂工作流程(State) 如果想要讓狀態更明確，可以自己去擴充或增減，譬如可以考慮多一個 State \u0026ldquo;Pull Request\u0026rdquo;\n好處3: 隱藏不必要的 Work Items 敏捷最重要的精神就是極簡，但如果造內建定義好的 Work Items，也許會讓管理上更綁手綁腳的，通常我會建議可以有兩種玩法\n玩法A: 只用 Product Backlogs 或者是 User Story 任何事情都應該對應到商業價值，要解決的問題也都是對應到“人”，用一種方式，依照 User Story 的撰寫方式把它寫好，會讓事情更單純\n玩法B: Product Backlogs + User Story + Bug 將 PBI 定義成從團隊內發起的單子，譬如重構類型，User Story 定義成需求面，Bug 單子內容設計用來專注還原操作步驟 用這種玩法可以進一步撈出每次 Sprint 所著重的比例","title":"Azure Boards 繼承 Process的 3 大好處"},{"content":"有時候需求來源可能來自於一份 Excel，但這樣一個一個建到平台上實在太累了，這篇就來介紹一下如何用 Azure Boards 的 Import 功能將 csv 匯入進平台\n要注意匯入功能狀態只能是起始的 state，無法指定該單子為 Doing 的狀態\n第一個簡單的匯入 從 Boards =\u0026gt; Workitem 進入\n我們用 Work Item Type 跟 Title 來做舉例，最後一筆我故意輸入一個錯誤的類型\n如果欄位驗證錯誤，也會明確指出哪一筆資料，哪一欄出錯\n如果成功匯入，也不會馬上儲存，可以在這介面微調內容\n批次修改 如果想要透過 csv 批次修改，首先要先將單子匯出，存在ID欄位，我們可以到 Query，並透過 Column Options 設定所需要的欄位匯入\n這時就會看到包含 ID 欄位，我們將 Work Item Type 修改為 Bug\n再重新執行一次匯入就能看到全部變 Bug 了\n也可以用這種方式來進行不同 Project 之間的單子移動\n可以有階層關係嗎? 譬如我們想要將某些單子指向上一層的 Feature 單，ID為 669\n只要加一個 Parent 欄位即可\n支援匯入的欄位 在官方文件有依據字母排列出所支援的欄位\nhttps://learn.microsoft.com/en-us/azure/devops/boards/work-items/guidance/work-item-field?view=azure-devops\n","permalink":"https://kyleap.github.io/azure-boards-import-workitems-from-csv/","summary":"有時候需求來源可能來自於一份 Excel，但這樣一個一個建到平台上實在太累了，這篇就來介紹一下如何用 Azure Boards 的 Import 功能將 csv 匯入進平台\n要注意匯入功能狀態只能是起始的 state，無法指定該單子為 Doing 的狀態\n第一個簡單的匯入 從 Boards =\u0026gt; Workitem 進入\n我們用 Work Item Type 跟 Title 來做舉例，最後一筆我故意輸入一個錯誤的類型\n如果欄位驗證錯誤，也會明確指出哪一筆資料，哪一欄出錯\n如果成功匯入，也不會馬上儲存，可以在這介面微調內容\n批次修改 如果想要透過 csv 批次修改，首先要先將單子匯出，存在ID欄位，我們可以到 Query，並透過 Column Options 設定所需要的欄位匯入\n這時就會看到包含 ID 欄位，我們將 Work Item Type 修改為 Bug\n再重新執行一次匯入就能看到全部變 Bug 了\n也可以用這種方式來進行不同 Project 之間的單子移動\n可以有階層關係嗎? 譬如我們想要將某些單子指向上一層的 Feature 單，ID為 669\n只要加一個 Parent 欄位即可\n支援匯入的欄位 在官方文件有依據字母排列出所支援的欄位\nhttps://learn.microsoft.com/en-us/azure/devops/boards/work-items/guidance/work-item-field?view=azure-devops","title":"如何在 Azure Boards 透過 csv 匯入 Workitems"},{"content":" 啃書代表是在閱讀時，針對自己有幫助的片段，做反思與紀錄，如想閱讀完整知識建議還是購買完整書籍 :P\n產品負責人作為迷你 CEO 的角色定位 產品負責人（Product Owner，PO）在組織中被視為迷你 CEO，但他們的角色與 CEO 又有所不同，畢竟組織上通常不會明確的是一個直屬長官。儘管負有重大責任，但產品負責人更像是一位具有低調姿態、傾聽他人意見並以事實為基礎說服他人的獨行者。他們必須在不確定的情況下具備洞察真相的能力，這意味著他們需要從大量數據中挖掘潛在的問題，並為可能的變化提前做好準備。\n做為一個產品負責人，溝通能力與分析能力都相當重要，這些都基於每個領域的知識累積，必須培養全局觀決策的能力\n客戶反饋的重要性及其處理方式 客戶是雇用產品而不是購買產品 : 初衷往往是為了解決\u0026quot;某個問題\u0026quot;，產品負責人需要密切關注客戶反饋，並對其給予充分重視。一個著名的例子是 \u0026ldquo;奶昔\u0026rdquo; 的故事，顧客購買奶昔的目的是為了在通勤途中解決無聊得打發時間的問題，而非單純品嚐口味。通過深入了解顧客需求，產品負責人可以確保產品符合市場需求。了解顧客需求，產品負責人在提案時可採用 Amazon 的六頁式報告，簡潔並完整地描述產品的目的、過去的相關試驗、失敗案例、未來的開發方向，以及用於確認產品成功可能性的數據。\n客戶分類: 同樣的產品會擁有各式各樣不同的顧客，要從這些不一樣當中找出相同的意圖，對顧客進行分類，瞭解他們各自雇用這項產品的原因，再根據原因優化產品。PO 如果可以做好顧客分類就等同於成功了一半，因為我們必須瞭解所有的顧客，才可以完整地做好分類。分類完之後就只要專注在如何優化產品就行了。\nPO 最容易犯下的錯誤之一，就是以產品製作人的角度看待顧客\n數據驅動的決策 「假設」是佐證PO思維必備的工具: 不管怎麼從理論上說服自己，只要無法透過測試與數據證明假說，就不應該著手開發。我們必須要有一個標準來判斷PO的提案是否有誤，決定要不要立刻刪除這項功能，這也是假說與實證測試之所以必要的原因。PO應該相信數據，而不是單憑個人直覺。有時候，一個看似有效的改進實際上可能並未帶來實質性的提升。\n假設也會失敗，例如，將選單上的炸雞排序移到最上面可能看似是一個好主意，但實際上可能並未帶來顯著的業績提升。又或者當初 Netflix 導入五星評價機制後發現，儘管評價時間變長，但顧客停留時間並未增加。因此，他們決定改用喜歡/不喜歡的方式，進而推薦給使用者喜歡的影片，從而提高用戶參與度。這些細節影響的小決策變化都會大大影響產品黏濁度因此。\n該有自己的數據儀表板 : 產品負責人應該用全局觀去調查問題，並依據數據作出決策。如何做? 建立儀錶板以定期確認產品狀況是很有幫助的，甚至數位儀表板也要當成一個產品。內容可以是產品的用量，以日/週/月，累積數量角度來看，兩週的使用率變化 ，平均產品使用時間/惡意使用、非正常情況等等…另外可以製作WBR(Weekly Business Review)，通常WBR可以有以下內容\n主旨 (Key Call-Outs): 明確記錄上次WBR會議決策後發生的主要問題 產品目標 (Product Goals): 產品目標（OKR）和主要指標來評估產品的發展狀況 主要指標 (Key Metrics): 以三週左右的數據或近三個月的數據，讓人一目了然哪個部分表現好，哪個部分表現未達預期 身為一個產品負責人，應該要比任何人都還在乎產品發生什麼事，建立定期觀察數據的環境，是一個好產品不可或缺的要素\nPO 在團隊的職責劃分 PO的任務並不是直接參與開發，而是要專注在如何讓工程師/設計師/商業分析師了解各自的任務，並明確提出需求與開發方向，如果過多的外務，就會無法扮演好自己的角色，這時擬定 R\u0026amp;R (Role and Responsibility)的劃分，就是一個很有幫助的方式\n產品負責人應該努力將組織塑造成一個不斷學習和自我改進的團隊。這意味著他們需要鼓勵團隊成員犯錯並從錯誤中學習，以達到最佳的創新水平。團隊成員應該被鼓勵提出各種想法，即使這些想法可能在實施過程中遇到困難。產品負責人需要在失敗中尋找改進的機會，並將這些改進運用到未來的產品開發中。\n也因為有責任又沒管理權力，產品負責人不應該採用獨裁式領導，而是應該用明確的事實和數據說服團隊。此外，在溝通時候盡量以疑問句代替命令，以創建更加民主和包容的工作環境。\n在有限的資源裡發揮最大的價值，就是PO的職責所在，如果無法與團隊維持良好的協作關係，開誠步公地討論，都會影響到PO的決策跟產品交付日期\n本書以 PO 的視角貫穿整個產品開發流程，除了以實例帶出 PO 在面對老闆/外部與團隊內部的溝通例子外，也將敏捷開發流程過程以PO視角闡述過一遍，並且說明數據決策的一些方式，接受產品的不完美，持續改進的方式，建議擔任過 PO 職務的人再來閱讀會更有感\n","permalink":"https://kyleap.github.io/book-review-product-owner-guide/","summary":"啃書代表是在閱讀時，針對自己有幫助的片段，做反思與紀錄，如想閱讀完整知識建議還是購買完整書籍 :P\n產品負責人作為迷你 CEO 的角色定位 產品負責人（Product Owner，PO）在組織中被視為迷你 CEO，但他們的角色與 CEO 又有所不同，畢竟組織上通常不會明確的是一個直屬長官。儘管負有重大責任，但產品負責人更像是一位具有低調姿態、傾聽他人意見並以事實為基礎說服他人的獨行者。他們必須在不確定的情況下具備洞察真相的能力，這意味著他們需要從大量數據中挖掘潛在的問題，並為可能的變化提前做好準備。\n做為一個產品負責人，溝通能力與分析能力都相當重要，這些都基於每個領域的知識累積，必須培養全局觀決策的能力\n客戶反饋的重要性及其處理方式 客戶是雇用產品而不是購買產品 : 初衷往往是為了解決\u0026quot;某個問題\u0026quot;，產品負責人需要密切關注客戶反饋，並對其給予充分重視。一個著名的例子是 \u0026ldquo;奶昔\u0026rdquo; 的故事，顧客購買奶昔的目的是為了在通勤途中解決無聊得打發時間的問題，而非單純品嚐口味。通過深入了解顧客需求，產品負責人可以確保產品符合市場需求。了解顧客需求，產品負責人在提案時可採用 Amazon 的六頁式報告，簡潔並完整地描述產品的目的、過去的相關試驗、失敗案例、未來的開發方向，以及用於確認產品成功可能性的數據。\n客戶分類: 同樣的產品會擁有各式各樣不同的顧客，要從這些不一樣當中找出相同的意圖，對顧客進行分類，瞭解他們各自雇用這項產品的原因，再根據原因優化產品。PO 如果可以做好顧客分類就等同於成功了一半，因為我們必須瞭解所有的顧客，才可以完整地做好分類。分類完之後就只要專注在如何優化產品就行了。\nPO 最容易犯下的錯誤之一，就是以產品製作人的角度看待顧客\n數據驅動的決策 「假設」是佐證PO思維必備的工具: 不管怎麼從理論上說服自己，只要無法透過測試與數據證明假說，就不應該著手開發。我們必須要有一個標準來判斷PO的提案是否有誤，決定要不要立刻刪除這項功能，這也是假說與實證測試之所以必要的原因。PO應該相信數據，而不是單憑個人直覺。有時候，一個看似有效的改進實際上可能並未帶來實質性的提升。\n假設也會失敗，例如，將選單上的炸雞排序移到最上面可能看似是一個好主意，但實際上可能並未帶來顯著的業績提升。又或者當初 Netflix 導入五星評價機制後發現，儘管評價時間變長，但顧客停留時間並未增加。因此，他們決定改用喜歡/不喜歡的方式，進而推薦給使用者喜歡的影片，從而提高用戶參與度。這些細節影響的小決策變化都會大大影響產品黏濁度因此。\n該有自己的數據儀表板 : 產品負責人應該用全局觀去調查問題，並依據數據作出決策。如何做? 建立儀錶板以定期確認產品狀況是很有幫助的，甚至數位儀表板也要當成一個產品。內容可以是產品的用量，以日/週/月，累積數量角度來看，兩週的使用率變化 ，平均產品使用時間/惡意使用、非正常情況等等…另外可以製作WBR(Weekly Business Review)，通常WBR可以有以下內容\n主旨 (Key Call-Outs): 明確記錄上次WBR會議決策後發生的主要問題 產品目標 (Product Goals): 產品目標（OKR）和主要指標來評估產品的發展狀況 主要指標 (Key Metrics): 以三週左右的數據或近三個月的數據，讓人一目了然哪個部分表現好，哪個部分表現未達預期 身為一個產品負責人，應該要比任何人都還在乎產品發生什麼事，建立定期觀察數據的環境，是一個好產品不可或缺的要素\nPO 在團隊的職責劃分 PO的任務並不是直接參與開發，而是要專注在如何讓工程師/設計師/商業分析師了解各自的任務，並明確提出需求與開發方向，如果過多的外務，就會無法扮演好自己的角色，這時擬定 R\u0026amp;R (Role and Responsibility)的劃分，就是一個很有幫助的方式\n產品負責人應該努力將組織塑造成一個不斷學習和自我改進的團隊。這意味著他們需要鼓勵團隊成員犯錯並從錯誤中學習，以達到最佳的創新水平。團隊成員應該被鼓勵提出各種想法，即使這些想法可能在實施過程中遇到困難。產品負責人需要在失敗中尋找改進的機會，並將這些改進運用到未來的產品開發中。\n也因為有責任又沒管理權力，產品負責人不應該採用獨裁式領導，而是應該用明確的事實和數據說服團隊。此外，在溝通時候盡量以疑問句代替命令，以創建更加民主和包容的工作環境。\n在有限的資源裡發揮最大的價值，就是PO的職責所在，如果無法與團隊維持良好的協作關係，開誠步公地討論，都會影響到PO的決策跟產品交付日期\n本書以 PO 的視角貫穿整個產品開發流程，除了以實例帶出 PO 在面對老闆/外部與團隊內部的溝通例子外，也將敏捷開發流程過程以PO視角闡述過一遍，並且說明數據決策的一些方式，接受產品的不完美，持續改進的方式，建議擔任過 PO 職務的人再來閱讀會更有感","title":"(啃書) 產品負責人實戰守則：從洞悉顧客需求，到引領敏捷開發"},{"content":"最近開發流程從 Gitflow 改為 Trunk-Based，讓每個 Feature 能很快速地回到主幹分支，但頻繁交付有時也不會想把新功能推出在使用者面前，所以這時候就能用 Feature Flag 來管理這些還沒有要發佈的功能，本篇文章記錄一下如何在 .NET Core 加入此功能，並講解一下目前實務上常規化的 Flag\nNuget 安裝 dotnet add package Microsoft.FeatureManagement.AspNetCore 加入 Feature Toggle public void ConfigureServices(IServiceCollection services) { // feature toggle services.AddFeatureManagement(); services.AddControllers(); services.AddSwaggerGen(c =\u0026gt; { c.SwaggerDoc(\u0026#34;v1\u0026#34;, new OpenApiInfo {Title = \u0026#34;FeatureFlagPoc.API\u0026#34;, Version = \u0026#34;v1\u0026#34;}); }); } 要使用時直接注入 private readonly IFeatureManager _featureManager; public WeatherForecastController(ILogger\u0026lt;WeatherForecastController\u0026gt; logger, IFeatureManager featureManager) { _logger = logger; _featureManager = featureManager; } 基本的 Feature Toggle 使用 var isFeatureAEnabled = await _featureManager.IsEnabledAsync(\u0026#34;FeatureA\u0026#34;); if(isFeatureAEnabled) { // 有打開，走新邏輯 } else { // 未打開，走舊邏輯 } appsettings.json \u0026#34;FeatureManagement\u0026#34;: { \u0026#34;FeatureA\u0026#34;: true } API Route 多個使用方式，驗證不過會回傳404 [FeatureGate(\u0026#34;FeatureA\u0026#34;)] public async Task\u0026lt;ActionResult\u0026gt; Foo() { return Ok(result); } 也可指定任一或多個 Feature 是否有打開 [FeatureGate(requirementType: RequirementType.Any, \u0026#34;FeatureA\u0026#34;, \u0026#34;FeatureB\u0026#34;)] public async Task\u0026lt;ActionResult\u0026gt; Foo() { return Ok(result); } 也可以用 Enum 避免弱型別的typo [FeatureGate(MyFeatureFlags.FeatureA)] public enum MyFeatureFlags { FeatureA } Toggle 定義 此篇文章值得一看，裡面提到實務上可以定義四種 Toggle 來區別用途\nRelease Toggles: 還未完成之功能, 通常 Release toggles 的變動頻率應該要是相當小的, 等版本推出後功能穩定後就會被拿掉，避免造成未來維護的成本 Experiment Toggles: 用在做A/B Test實驗, 如我自己會用在 LineBot 做不同文案的轉化追蹤 Ops Toggles: 用在跟系統運維相關, 可能需要臨時上維護公告，或者是臨時要開啟此程式片段的系統監控 Permission Toggles: 用在限定特定使用者可以使用, 針對有權限控制的功能做顯示/隱藏 Reference https://github.com/microsoft/FeatureManagement-Dotnet\n","permalink":"https://kyleap.github.io/feature-flag-dotnet-core-basic/","summary":"最近開發流程從 Gitflow 改為 Trunk-Based，讓每個 Feature 能很快速地回到主幹分支，但頻繁交付有時也不會想把新功能推出在使用者面前，所以這時候就能用 Feature Flag 來管理這些還沒有要發佈的功能，本篇文章記錄一下如何在 .NET Core 加入此功能，並講解一下目前實務上常規化的 Flag\nNuget 安裝 dotnet add package Microsoft.FeatureManagement.AspNetCore 加入 Feature Toggle public void ConfigureServices(IServiceCollection services) { // feature toggle services.AddFeatureManagement(); services.AddControllers(); services.AddSwaggerGen(c =\u0026gt; { c.SwaggerDoc(\u0026#34;v1\u0026#34;, new OpenApiInfo {Title = \u0026#34;FeatureFlagPoc.API\u0026#34;, Version = \u0026#34;v1\u0026#34;}); }); } 要使用時直接注入 private readonly IFeatureManager _featureManager; public WeatherForecastController(ILogger\u0026lt;WeatherForecastController\u0026gt; logger, IFeatureManager featureManager) { _logger = logger; _featureManager = featureManager; } 基本的 Feature Toggle 使用 var isFeatureAEnabled = await _featureManager.","title":"在 .NET Core 使用 Feature Flag (Feature Toggle)"},{"content":"Feature Flag 是否開啟，也可以透過自訂的類別來撰寫，可以透過IFeatureFilter來實現，以下範例以取得使用者 User-Agent 為例\n實作IFeatureFilter，新增一個 BrowserFeatureFilter.cs 檔案 [FilterAlias(\u0026#34;Browser\u0026#34;)] public class BrowserFeatureFilter : IFeatureFilter { private readonly IHttpContextAccessor _httpContextAccessor; public BrowserFeatureFilter(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Task\u0026lt;bool\u0026gt; EvaluateAsync(FeatureFilterEvaluationContext context) { // 取得Request的瀏覽器類型 var agent = _httpContextAccessor.HttpContext.Request.Headers[\u0026#34;User-Agent\u0026#34;].ToString(); // 取得 appSettings.json的設定 var settings = context.Parameters.Get\u0026lt;BrowserFilterSettings\u0026gt;(); // TODO: 這邊就可以撰寫邏輯驗證是否針對該瀏覽器開此功能 return Task.FromResult\u0026lt;bool\u0026gt;(false); } } public class BrowserFilterSettings { public string[] Allowed { get; set; } } appsettings.json (允許Edge瀏覽器開啟FeatureA功能) \u0026#34;FeatureManagement\u0026#34;: { \u0026#34;FeatureA\u0026#34;: { \u0026#34;EnabledFor\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Browser\u0026#34;, \u0026#34;Parameters\u0026#34;: { \u0026#34;Allowed\u0026#34;: [ \u0026#34;Edg\u0026#34; ] } } ] } } startup 注入 services.AddSingleton\u0026lt;ITargetingContextAccessor, TestTargetingContextAccessor\u0026gt;(); services.AddFeatureManagement() .AddFeatureFilter\u0026lt;BrowserFeatureFilter\u0026gt;(); ","permalink":"https://kyleap.github.io/feature-flag-dotnet-core-basic-targeting/","summary":"Feature Flag 是否開啟，也可以透過自訂的類別來撰寫，可以透過IFeatureFilter來實現，以下範例以取得使用者 User-Agent 為例\n實作IFeatureFilter，新增一個 BrowserFeatureFilter.cs 檔案 [FilterAlias(\u0026#34;Browser\u0026#34;)] public class BrowserFeatureFilter : IFeatureFilter { private readonly IHttpContextAccessor _httpContextAccessor; public BrowserFeatureFilter(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Task\u0026lt;bool\u0026gt; EvaluateAsync(FeatureFilterEvaluationContext context) { // 取得Request的瀏覽器類型 var agent = _httpContextAccessor.HttpContext.Request.Headers[\u0026#34;User-Agent\u0026#34;].ToString(); // 取得 appSettings.json的設定 var settings = context.Parameters.Get\u0026lt;BrowserFilterSettings\u0026gt;(); // TODO: 這邊就可以撰寫邏輯驗證是否針對該瀏覽器開此功能 return Task.FromResult\u0026lt;bool\u0026gt;(false); } } public class BrowserFilterSettings { public string[] Allowed { get; set; } } appsettings.json (允許Edge瀏覽器開啟FeatureA功能) \u0026#34;FeatureManagement\u0026#34;: { \u0026#34;FeatureA\u0026#34;: { \u0026#34;EnabledFor\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Browser\u0026#34;, \u0026#34;Parameters\u0026#34;: { \u0026#34;Allowed\u0026#34;: [ \u0026#34;Edg\u0026#34; ] } } ] } } startup 注入 services.","title":"在 .NET Core 使用 Feature Flag (Feature Toggle) - 自訂邏輯"},{"content":"如果要針對某用戶或群組來開啟特定功能，在 .NET Feature Management 中可以用 TargetFilter 來指定\nappsettings.json (指定當前使用者的Guid或群組) RolloutPercentage 為選擇性參數，可以指定多少百分比的使用者會導向該功能\n\u0026#34;FeatureManagement\u0026#34;: { \u0026#34;FeatureA\u0026#34;: { \u0026#34;EnabledFor\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Microsoft.Targeting\u0026#34;, \u0026#34;Parameters\u0026#34;: { \u0026#34;Audience\u0026#34;: { \u0026#34;Users\u0026#34;: [ \u0026#34;1948fcc1-dc03-44a6-824f-fb48166ffa9d\u0026#34; ], \u0026#34;Group\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Group1\u0026#34;, \u0026#34;RolloutPercentage\u0026#34;: 80 }, { \u0026#34;Name\u0026#34;: \u0026#34;Group2\u0026#34; } ] } } } ] } } 實作 ITargetingContextAccessor public class TestTargetingContextAccessor : ITargetingContextAccessor { private const string TargetingContextLookup = \u0026#34;TestTargetingContextAccessor.TargetingContext\u0026#34;; private readonly IHttpContextAccessor _httpContextAccessor; public TestTargetingContextAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } public ValueTask\u0026lt;TargetingContext\u0026gt; GetContextAsync() { HttpContext httpContext = _httpContextAccessor.HttpContext; if (httpContext.Items.TryGetValue(TargetingContextLookup, out object value)) { return new ValueTask\u0026lt;TargetingContext\u0026gt;((TargetingContext)value); } List\u0026lt;string\u0026gt; groups = new List\u0026lt;string\u0026gt;(); if (httpContext.User.Identity.Name != null) { groups.Add(httpContext.User.Identity.Name.Split(\u0026#34;@\u0026#34;, StringSplitOptions.None)[1]); } TargetingContext targetingContext = new TargetingContext { UserId = httpContext.User.Identity.Name, Groups = groups }; httpContext.Items[TargetingContextLookup] = targetingContext; return new ValueTask\u0026lt;TargetingContext\u0026gt;(targetingContext); } } startup 注入 services.AddSingleton\u0026lt;ITargetingContextAccessor, TestTargetingContextAccessor\u0026gt;(); services.AddFeatureManagement() .AddFeatureFilter\u0026lt;TargetingFilter\u0026gt;(); ","permalink":"https://kyleap.github.io/feature-flag-dotnet-core-basic-targeting/","summary":"如果要針對某用戶或群組來開啟特定功能，在 .NET Feature Management 中可以用 TargetFilter 來指定\nappsettings.json (指定當前使用者的Guid或群組) RolloutPercentage 為選擇性參數，可以指定多少百分比的使用者會導向該功能\n\u0026#34;FeatureManagement\u0026#34;: { \u0026#34;FeatureA\u0026#34;: { \u0026#34;EnabledFor\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Microsoft.Targeting\u0026#34;, \u0026#34;Parameters\u0026#34;: { \u0026#34;Audience\u0026#34;: { \u0026#34;Users\u0026#34;: [ \u0026#34;1948fcc1-dc03-44a6-824f-fb48166ffa9d\u0026#34; ], \u0026#34;Group\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Group1\u0026#34;, \u0026#34;RolloutPercentage\u0026#34;: 80 }, { \u0026#34;Name\u0026#34;: \u0026#34;Group2\u0026#34; } ] } } } ] } } 實作 ITargetingContextAccessor public class TestTargetingContextAccessor : ITargetingContextAccessor { private const string TargetingContextLookup = \u0026#34;TestTargetingContextAccessor.TargetingContext\u0026#34;; private readonly IHttpContextAccessor _httpContextAccessor; public TestTargetingContextAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor ?","title":"在 .NET Core 使用 Feature Flag (Feature Toggle) - 針對受眾(targeting)"},{"content":"承接上一篇，如果有一些情境是限時開放的功能，時間過了以後就要關閉，那就可以用到 TimeWindow\nappsettings.json \u0026#34;FeatureManagement\u0026#34;: { \u0026#34;FeatureB\u0026#34;: { \u0026#34;EnabledFor\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Microsoft.TimeWindow\u0026#34;, \u0026#34;Parameters\u0026#34;: { \u0026#34;Start\u0026#34;: \u0026#34;2020-11-09 04:58:00Z\u0026#34;, \u0026#34;End\u0026#34;: \u0026#34;2099-11-09 05:00:00Z\u0026#34; } } ] } } 基本的 Feature Toggle 使用 var isFeatureAEnabled = await _featureManager.IsEnabledAsync(\u0026#34;FeatureA\u0026#34;); if(isFeatureAEnabled) { // 有打開，走新邏輯 } else { // 未打開，走舊邏輯 } API Route 多個使用方式，驗證不過會回傳404 [FeatureGate(\u0026#34;FeatureA\u0026#34;)] public async Task\u0026lt;ActionResult\u0026gt; Foo() { return Ok(result); } 也可指定任一或多個 Feature 是否有打開 [FeatureGate(requirementType: RequirementType.Any, \u0026#34;FeatureA\u0026#34;, \u0026#34;FeatureB\u0026#34;)] public async Task\u0026lt;ActionResult\u0026gt; Foo() { return Ok(result); } 也可以用 Enum 避免弱型別的typo [FeatureGate(MyFeatureFlags.FeatureA)] public enum MyFeatureFlags { FeatureA } startup 注入 services.AddFeatureManagement() .AddFeatureFilter\u0026lt;TargetingFilter\u0026gt;(); ","permalink":"https://kyleap.github.io/feature-flag-dotnet-core-basic-timewindow/","summary":"承接上一篇，如果有一些情境是限時開放的功能，時間過了以後就要關閉，那就可以用到 TimeWindow\nappsettings.json \u0026#34;FeatureManagement\u0026#34;: { \u0026#34;FeatureB\u0026#34;: { \u0026#34;EnabledFor\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;Microsoft.TimeWindow\u0026#34;, \u0026#34;Parameters\u0026#34;: { \u0026#34;Start\u0026#34;: \u0026#34;2020-11-09 04:58:00Z\u0026#34;, \u0026#34;End\u0026#34;: \u0026#34;2099-11-09 05:00:00Z\u0026#34; } } ] } } 基本的 Feature Toggle 使用 var isFeatureAEnabled = await _featureManager.IsEnabledAsync(\u0026#34;FeatureA\u0026#34;); if(isFeatureAEnabled) { // 有打開，走新邏輯 } else { // 未打開，走舊邏輯 } API Route 多個使用方式，驗證不過會回傳404 [FeatureGate(\u0026#34;FeatureA\u0026#34;)] public async Task\u0026lt;ActionResult\u0026gt; Foo() { return Ok(result); } 也可指定任一或多個 Feature 是否有打開 [FeatureGate(requirementType: RequirementType.Any, \u0026#34;FeatureA\u0026#34;, \u0026#34;FeatureB\u0026#34;)] public async Task\u0026lt;ActionResult\u0026gt; Foo() { return Ok(result); } 也可以用 Enum 避免弱型別的typo [FeatureGate(MyFeatureFlags.","title":"在 .NET Core 使用 Feature Flag (Feature Toggle) - 時間控制"},{"content":"在一條龍的 Azure DevOps 平台，當然也存在 KM 功能， 透過 Azure Wiki 與 Workitem 結合，撰寫全貌的文件，能協助團隊更有效地進行開發與協作，其中Wiki 支援 Mermaid 語法來繪製圖表，本篇以圓餅圖作為示範\n透過 ::: mermaid 與 ::: 裡面放置 mermaid 語法，即可呈現圖表\n::: mermaid pie title 年度銷售量 \u0026#34;Q1\u0026#34; : 2000 \u0026#34;Q2\u0026#34; : 1500 \u0026#34;Q3\u0026#34; : 3500 \u0026#34;Q4\u0026#34; : 4000 ::: 會產生以下結果\n也可以加入 pie showData 讓 Label 呈現數值\n也支援小數點數值\n只可惜目前在 Azure wiki 無法針對樣式去做設定，但會跟著使用者的 theme 進行適當的配色\nReference\nhttps://learn.microsoft.com/zh-tw/azure/devops/project/wiki/wiki-markdown-guidance?view=azure-devops https://mermaid.js.org/syntax/pie.html ","permalink":"https://kyleap.github.io/azure-wiki-pie-chart/","summary":"在一條龍的 Azure DevOps 平台，當然也存在 KM 功能， 透過 Azure Wiki 與 Workitem 結合，撰寫全貌的文件，能協助團隊更有效地進行開發與協作，其中Wiki 支援 Mermaid 語法來繪製圖表，本篇以圓餅圖作為示範\n透過 ::: mermaid 與 ::: 裡面放置 mermaid 語法，即可呈現圖表\n::: mermaid pie title 年度銷售量 \u0026#34;Q1\u0026#34; : 2000 \u0026#34;Q2\u0026#34; : 1500 \u0026#34;Q3\u0026#34; : 3500 \u0026#34;Q4\u0026#34; : 4000 ::: 會產生以下結果\n也可以加入 pie showData 讓 Label 呈現數值\n也支援小數點數值\n只可惜目前在 Azure wiki 無法針對樣式去做設定，但會跟著使用者的 theme 進行適當的配色\nReference\nhttps://learn.microsoft.com/zh-tw/azure/devops/project/wiki/wiki-markdown-guidance?view=azure-devops https://mermaid.js.org/syntax/pie.html ","title":"用 Mermaid 在 Azure wiki 繪製圖表 - 圓餅圖"},{"content":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 Scrum 團隊可以在 Retro 會議中有效地檢視自己的工作狀況，找出需要改進的地方，並制定具體的行動計劃。這有助於團隊持續改善工作流程，提高工作效率，並確保專案能夠成功地達成預定的目標。這可以說是 Scurm 當中最重要的 Events，必須要有個共識，每次 Sprint 都會越來越好\n實體可以用便利貼，面對面溝通，但如果是遠距團隊，在 Azure Boards 上可以安裝Retrospectives 這個套件，透過這引導可以線上開一個即時的回顧會議，它會分以下五個步驟:\nCollect (收集) Scrum 團隊在 Retro 會議中的第一個步驟是收集。在此階段，團隊成員共同分享在過去的一個迭代或是開發週期中所遇到的問題、挑戰、成功經驗和改進點。收集階段的目的是讓團隊有個機會回顧自己在專案中的表現，並確保大家都有足夠的機會表達他們的想法。在這個過程中，團隊成員可以自行建單，將他們的想法和建議記錄下來。這有助於確保每個人的想法都能夠被考慮到，並創造出一個共同的基礎以進行後續討論。\nGroup (分組) 在分組階段，除了理解大家回饋的項目外，也一起將相似或相關的議題整合在一起。這有助於團隊更有效地針對特定議題進行討論，並確保在限定的時間內充分探討各個問題。分組階段的目的是讓團隊有機會深入了解各個議題，並確保討論的範疇不會過於繁雜。\nVote (投票) 團隊成員將對在隊分組過後的議題進行投票，以確定哪些議題是最為重要且需要優先處理的。一般而言，每個人都會有限定的投票次數，以避免單一意見主導討論。投票階段的目的是讓團隊達成共識，確定哪些改進項目最為關鍵，以便在後續的步驟中針對這些議題進行更深入的討論。\nAction (行動) 行動階段是團隊討論如何解決所確定的問題，並制定具體的行動計劃。在這個過程中，團隊成員將提出解決方案、改進方法，並討論如何實施這些措施。最終，團隊將就每個行動項目達成共識，建立 Workitem，分配負責人和設定期限。行動階段的目的是確保團隊在會議結束後能夠將所討論的改進點付諸實踐，並持續改進團隊的工作流程和效率。在這個階段，團隊成員需要承擔責任，並對所提出的解決方案保持承諾，以確保改進措施能夠成功地實施。\nHistory (紀錄) 歷史階段是 Scrum 團隊在 Retro 會議中回顧以往改進經驗的時間。在這個階段，團隊將檢視先前的 Retro 會議紀錄，評估之前提出的行動項目是否已經完成，以及這些行動項目對團隊的影響。歷史階段的目的是讓團隊在不斷進步的過程中保持學習和成長，並確保之前的改進努力沒有白費。\n如果你想要看完整的演講影片，現在也已經傳到 Youtube\n影片: https://www.youtube.com/watch?v=G0ggjY5kzF8 簡報: https://speakerdeck.com/kyleshen/dot-net-conf-taiwan-na-xie-nian-yong-azure-boards-jiao-fu-guo-de-chan-pin 5篇 Scurm 痛點文章 Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序? 一個好的 User Story, 應具備哪些要素? 估複雜度好花時間? 怎麼抓 Sprint 的能量? 讓 Daily Meeting 不要淪為機械式的報告 在 Retro Meeting 停下腳步，檢討改善 ","permalink":"https://kyleap.github.io/azure-boards-retro-meeting/","summary":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 Scrum 團隊可以在 Retro 會議中有效地檢視自己的工作狀況，找出需要改進的地方，並制定具體的行動計劃。這有助於團隊持續改善工作流程，提高工作效率，並確保專案能夠成功地達成預定的目標。這可以說是 Scurm 當中最重要的 Events，必須要有個共識，每次 Sprint 都會越來越好\n實體可以用便利貼，面對面溝通，但如果是遠距團隊，在 Azure Boards 上可以安裝Retrospectives 這個套件，透過這引導可以線上開一個即時的回顧會議，它會分以下五個步驟:\nCollect (收集) Scrum 團隊在 Retro 會議中的第一個步驟是收集。在此階段，團隊成員共同分享在過去的一個迭代或是開發週期中所遇到的問題、挑戰、成功經驗和改進點。收集階段的目的是讓團隊有個機會回顧自己在專案中的表現，並確保大家都有足夠的機會表達他們的想法。在這個過程中，團隊成員可以自行建單，將他們的想法和建議記錄下來。這有助於確保每個人的想法都能夠被考慮到，並創造出一個共同的基礎以進行後續討論。\nGroup (分組) 在分組階段，除了理解大家回饋的項目外，也一起將相似或相關的議題整合在一起。這有助於團隊更有效地針對特定議題進行討論，並確保在限定的時間內充分探討各個問題。分組階段的目的是讓團隊有機會深入了解各個議題，並確保討論的範疇不會過於繁雜。\nVote (投票) 團隊成員將對在隊分組過後的議題進行投票，以確定哪些議題是最為重要且需要優先處理的。一般而言，每個人都會有限定的投票次數，以避免單一意見主導討論。投票階段的目的是讓團隊達成共識，確定哪些改進項目最為關鍵，以便在後續的步驟中針對這些議題進行更深入的討論。\nAction (行動) 行動階段是團隊討論如何解決所確定的問題，並制定具體的行動計劃。在這個過程中，團隊成員將提出解決方案、改進方法，並討論如何實施這些措施。最終，團隊將就每個行動項目達成共識，建立 Workitem，分配負責人和設定期限。行動階段的目的是確保團隊在會議結束後能夠將所討論的改進點付諸實踐，並持續改進團隊的工作流程和效率。在這個階段，團隊成員需要承擔責任，並對所提出的解決方案保持承諾，以確保改進措施能夠成功地實施。\nHistory (紀錄) 歷史階段是 Scrum 團隊在 Retro 會議中回顧以往改進經驗的時間。在這個階段，團隊將檢視先前的 Retro 會議紀錄，評估之前提出的行動項目是否已經完成，以及這些行動項目對團隊的影響。歷史階段的目的是讓團隊在不斷進步的過程中保持學習和成長，並確保之前的改進努力沒有白費。\n如果你想要看完整的演講影片，現在也已經傳到 Youtube\n影片: https://www.youtube.com/watch?v=G0ggjY5kzF8 簡報: https://speakerdeck.com/kyleshen/dot-net-conf-taiwan-na-xie-nian-yong-azure-boards-jiao-fu-guo-de-chan-pin 5篇 Scurm 痛點文章 Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序? 一個好的 User Story, 應具備哪些要素?","title":"在 Retro Meeting 停下腳步，檢討改善"},{"content":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 Daily Meeting 講什麼? 我昨天做了什麼 我今天打算做什麼 任何阻礙您完成工作的障礙 在透明度看板下，我覺得第三點特別重要，目的在於: 重新確定優先順序並分配重新計劃的工作\n這篇文章寫給 Scurm Master，除了聽取報告外，在 Azure Boards 我們能在 Daily 看哪些事\nWork Detail 在前一篇文章我們設完 Capacity 後，在 Azure Boards 的 Work Detail 就能針對團隊每天的消耗量去看進度是否有異常，如果消耗不如預期，甚至有 Daily風險，會呈現紅色，這時 Scurm Master 就能關心該名成員，是不是有需要協助的地方，並與團隊討論重新調配\nTask Board 在看板模式，我們可以看出成員是否有多工的情況，理論上 In Progress 每個人都只需要專注一件事就好，因為分工是隨著每天的狀態去調整的，最重要的事團隊一起努力達到 Sprint 的目標\n以週的檢視角度 在實務上我更喜歡以週為單位，讓團隊預先安排 Task 發生的先後順序，有計畫地展開一週工作，會讓整個節奏更穩定，在 Azure Boards 可以透過 Drop Plan 這個套件去在 Daily 時檢視，也可以看出實做的先後順序是否有相依性，Task是否拆得不夠細等等\n關注 Sprint 健康程度 Azure Boards 提供很多 Chart 檢視 Sprint 的健康成度，在實務上我特別喜歡看以 Task 為基礎的 Burndown Chart，因為 Story 通常都會交付在 Sprint 後判斷，測試完成 state 才會算結束，這會導致 Burndown chart 總在後面才一口氣下降，但如果 Task 是以 1-2 天為大小的話，應該都會發生 Pull Request \u0026amp; Code Review merge的情況，此時 Task 就會變成是 Done，這樣就會穩定的下降\n小結: 為了達成如期如質交付，Daily Meeting 可是一個關鍵，絕對不要淪為機械式的報告風氣\n如果你想要看完整的演講影片，現在也已經傳到 Youtube\n影片: https://www.youtube.com/watch?v=G0ggjY5kzF8 簡報: https://speakerdeck.com/kyleshen/dot-net-conf-taiwan-na-xie-nian-yong-azure-boards-jiao-fu-guo-de-chan-pin 5篇 Scurm 痛點文章 Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序? 一個好的 User Story, 應具備哪些要素? 估複雜度好花時間? 怎麼抓 Sprint 的能量? 讓 Daily Meeting 不要淪為機械式的報告 在 Retro Meeting 停下腳步，檢討改善 ","permalink":"https://kyleap.github.io/azure-boards-daily-meeting/","summary":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 Daily Meeting 講什麼? 我昨天做了什麼 我今天打算做什麼 任何阻礙您完成工作的障礙 在透明度看板下，我覺得第三點特別重要，目的在於: 重新確定優先順序並分配重新計劃的工作\n這篇文章寫給 Scurm Master，除了聽取報告外，在 Azure Boards 我們能在 Daily 看哪些事\nWork Detail 在前一篇文章我們設完 Capacity 後，在 Azure Boards 的 Work Detail 就能針對團隊每天的消耗量去看進度是否有異常，如果消耗不如預期，甚至有 Daily風險，會呈現紅色，這時 Scurm Master 就能關心該名成員，是不是有需要協助的地方，並與團隊討論重新調配\nTask Board 在看板模式，我們可以看出成員是否有多工的情況，理論上 In Progress 每個人都只需要專注一件事就好，因為分工是隨著每天的狀態去調整的，最重要的事團隊一起努力達到 Sprint 的目標\n以週的檢視角度 在實務上我更喜歡以週為單位，讓團隊預先安排 Task 發生的先後順序，有計畫地展開一週工作，會讓整個節奏更穩定，在 Azure Boards 可以透過 Drop Plan 這個套件去在 Daily 時檢視，也可以看出實做的先後順序是否有相依性，Task是否拆得不夠細等等\n關注 Sprint 健康程度 Azure Boards 提供很多 Chart 檢視 Sprint 的健康成度，在實務上我特別喜歡看以 Task 為基礎的 Burndown Chart，因為 Story 通常都會交付在 Sprint 後判斷，測試完成 state 才會算結束，這會導致 Burndown chart 總在後面才一口氣下降，但如果 Task 是以 1-2 天為大小的話，應該都會發生 Pull Request \u0026amp; Code Review merge的情況，此時 Task 就會變成是 Done，這樣就會穩定的下降","title":"讓 Daily Meeting 不要淪為機械式的報告"},{"content":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 複雜度評估的兩個方式 在 Azure Boards 裡面可以安裝 Estimate 這個套件，在 Planning Meeting 的時候可以針對 User Story 進行估點計算，且這是一個線上即時的功能，即使遠端協作的團隊也可以達到很好的估點流程\n這工具在出點的時候可以一起呈現，如果彼此點數落差大，可以進行多輪的討論，我們通常會用相對點數來讓團隊取得共識，此套件也能選擇很多範本\n但如果你今天是有海量的需求，不想花太多時間一張一張評估，也許可以用 Affinity Estimation 這種方式，他的執行步驟是由PO解釋完單子後，將所有單子都建在看板上，透過團隊共同拖拉的方式，越左邊代表複雜度越小，越右邊越複雜，之後取中間當個分隔線，在給予不同大小的渠道。 最後一個步驟是針對鄰近的單子去比對，兩個大小是否是差不多大小，如果落差過大再微調\n延伸閱讀: https://www.techagilist.com/agile/scrum/affinity-estimation-agile-estimation-method/\n如何知道團隊成員工作量? Planning 另外一個重點就是在 User Story 底下去拆技術細節去分工(Task)，雖然估時估不準，但我會建議還是能在 Original Estimate 這個欄位讓工程師評估一下這個單子預計要花的時間，通常一天我們會以 6hr 為單位，如果 Task 評估大於 12hr，代表顆粒度太大，需要再細拆，為什麼要這樣做呢? 因為搭配 Capacity 針對每個工程師每日可貢獻的時數，可以讓我們知道每個工程師拿的量會不會過多或過少\n但記得一點，不要用這個來挑戰工程師產值，敏捷強調自我管理，信任制，為了共用目標努力，太在乎每日產出只會造成工程師給錯誤的資訊\n在 Planning 檢視每個人的工作量是否平均\n如果你想要看完整的演講影片，現在也已經傳到 Youtube\n影片: https://www.youtube.com/watch?v=G0ggjY5kzF8 簡報: https://speakerdeck.com/kyleshen/dot-net-conf-taiwan-na-xie-nian-yong-azure-boards-jiao-fu-guo-de-chan-pin 5篇 Scurm 痛點文章 Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序? 一個好的 User Story, 應具備哪些要素? 估複雜度好花時間? 怎麼抓 Sprint 的能量? 讓 Daily Meeting 不要淪為機械式的報告 在 Retro Meeting 停下腳步，檢討改善 ","permalink":"https://kyleap.github.io/azure-boards-user-story-point/","summary":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 複雜度評估的兩個方式 在 Azure Boards 裡面可以安裝 Estimate 這個套件，在 Planning Meeting 的時候可以針對 User Story 進行估點計算，且這是一個線上即時的功能，即使遠端協作的團隊也可以達到很好的估點流程\n這工具在出點的時候可以一起呈現，如果彼此點數落差大，可以進行多輪的討論，我們通常會用相對點數來讓團隊取得共識，此套件也能選擇很多範本\n但如果你今天是有海量的需求，不想花太多時間一張一張評估，也許可以用 Affinity Estimation 這種方式，他的執行步驟是由PO解釋完單子後，將所有單子都建在看板上，透過團隊共同拖拉的方式，越左邊代表複雜度越小，越右邊越複雜，之後取中間當個分隔線，在給予不同大小的渠道。 最後一個步驟是針對鄰近的單子去比對，兩個大小是否是差不多大小，如果落差過大再微調\n延伸閱讀: https://www.techagilist.com/agile/scrum/affinity-estimation-agile-estimation-method/\n如何知道團隊成員工作量? Planning 另外一個重點就是在 User Story 底下去拆技術細節去分工(Task)，雖然估時估不準，但我會建議還是能在 Original Estimate 這個欄位讓工程師評估一下這個單子預計要花的時間，通常一天我們會以 6hr 為單位，如果 Task 評估大於 12hr，代表顆粒度太大，需要再細拆，為什麼要這樣做呢? 因為搭配 Capacity 針對每個工程師每日可貢獻的時數，可以讓我們知道每個工程師拿的量會不會過多或過少\n但記得一點，不要用這個來挑戰工程師產值，敏捷強調自我管理，信任制，為了共用目標努力，太在乎每日產出只會造成工程師給錯誤的資訊\n在 Planning 檢視每個人的工作量是否平均\n如果你想要看完整的演講影片，現在也已經傳到 Youtube\n影片: https://www.youtube.com/watch?v=G0ggjY5kzF8 簡報: https://speakerdeck.com/kyleshen/dot-net-conf-taiwan-na-xie-nian-yong-azure-boards-jiao-fu-guo-de-chan-pin 5篇 Scurm 痛點文章 Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序?","title":"估複雜度好花時間? 怎麼抓 Sprint 的能量?"},{"content":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 在 Mike Cohn 的 使用者故事一書中，有提到一個好的 User Story 應具備以下內容\n獨立的 (Independent) 為了提高產品開發效率，User Story 必須盡可能獨立，與其他單子的依賴性越低越好。這樣可以讓團隊更好地分配工作，並使開發流程更加靈活和高效。當 User Story 之間有很強的相互依賴性時，只要一 Delay 可能就會影響其他單子的進度，進而影響整個產品的交付。因此，將 User Story 設計為盡可能獨立的單元，可以降低風險，減少延遲和錯誤，同時提高產品的可靠性和可維護性。\n可協調的 (Negotiable) 為了達到產品開發的效率和靈活性，User Story 必須是可協調的，即可以保留一些開放式描述，讓團隊進行討論和協調。User Story 的開放式描述可以激發團隊成員的討論，進而確定 User Story 的實際需求和功能。同時，開放式描述也可以在開發過程中進行調整和修改，以適應產品開發的實際情況。在 User Story 中保留開放式描述可以幫助團隊在開發期間進行更好的協調和溝通，並確保產品能夠滿足用戶需求。最終，這樣的協調和討論可以幫助團隊建立更強的團隊合作精神，以達成產品開發的最終目標。\n有價值的 (Valuable) User Story 對於使用者來說非常重要，因為它們確定了產品的功能和特性，並且能夠滿足用戶的需求。如果 User Story 無法滿足用戶需求，那麼產品將無法被成功接受。為了確保 User Story 的價值，開發團隊必須理解並關注用戶需求，將其轉化為可實現的 User Story。只有這樣，才能創造出一個有價值的產品，並滿足用戶需求。\n可預估的 (Estimable) 必須能夠對它們的大小、難易度和所需時間進行預估。這樣可以幫助開發團隊更好地分配工作，同時確保產品交付的準時性和品質。通過對 User Story 進行預估，開發團隊可以更好地掌握開發進度和進度風險，並提前進行調整。但需要注意的是，User Story 的預估並不是一個精確的科學，因為它們可能受到多種因素的影響。因此，開發團隊需要持續進行評估和調整，以確保 User Story 的準確性和可行性，並最終創造出高質量的產品。\n夠小的 (Small-sized) User Story 通常會以 Story Point 來做複雜度評估，但點數過大的時候需要思考是否要拆解，代表這個單子要做的事情收斂的不夠，複雜度越高，代表 Delay 風險越高，估時一定是不準的，我們能做的就是將 Scope 鎖小，讓不確定性降低，也更好的統計完成度\n可測試的 (Testable) 為了保證產品開發的品質和有效性，User Story 必須要提供必要的資料，以讓故事可以被測試。這些資料可能包括用戶需求、功能要求、測試條件、期望結果等等。這些資料可以幫助開發團隊確定 User Story 的可行性和功能性，並確保 User Story 滿足用戶需求和期望。通過提供必要的資料，開發團隊可以更好地進行故事測試和驗證，以確保產品能夠符合用戶期望和需求。因此，提供必要的資料是確保 User Story 的可測試性和產品品質的重要一環。\n以下是一個在 Azure Boards 撰寫單子的範例及所要注意的重點\n如果你想要看完整的演講影片，現在也已經傳到 Youtube\n影片: https://www.youtube.com/watch?v=G0ggjY5kzF8 簡報: https://speakerdeck.com/kyleshen/dot-net-conf-taiwan-na-xie-nian-yong-azure-boards-jiao-fu-guo-de-chan-pin 5篇 Scurm 痛點文章 Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序? 一個好的 User Story, 應具備哪些要素? 估複雜度好花時間? 怎麼抓 Sprint 的能量? 讓 Daily Meeting 不要淪為機械式的報告 在 Retro Meeting 停下腳步，檢討改善 ","permalink":"https://kyleap.github.io/azure-boards-user-story/","summary":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 在 Mike Cohn 的 使用者故事一書中，有提到一個好的 User Story 應具備以下內容\n獨立的 (Independent) 為了提高產品開發效率，User Story 必須盡可能獨立，與其他單子的依賴性越低越好。這樣可以讓團隊更好地分配工作，並使開發流程更加靈活和高效。當 User Story 之間有很強的相互依賴性時，只要一 Delay 可能就會影響其他單子的進度，進而影響整個產品的交付。因此，將 User Story 設計為盡可能獨立的單元，可以降低風險，減少延遲和錯誤，同時提高產品的可靠性和可維護性。\n可協調的 (Negotiable) 為了達到產品開發的效率和靈活性，User Story 必須是可協調的，即可以保留一些開放式描述，讓團隊進行討論和協調。User Story 的開放式描述可以激發團隊成員的討論，進而確定 User Story 的實際需求和功能。同時，開放式描述也可以在開發過程中進行調整和修改，以適應產品開發的實際情況。在 User Story 中保留開放式描述可以幫助團隊在開發期間進行更好的協調和溝通，並確保產品能夠滿足用戶需求。最終，這樣的協調和討論可以幫助團隊建立更強的團隊合作精神，以達成產品開發的最終目標。\n有價值的 (Valuable) User Story 對於使用者來說非常重要，因為它們確定了產品的功能和特性，並且能夠滿足用戶的需求。如果 User Story 無法滿足用戶需求，那麼產品將無法被成功接受。為了確保 User Story 的價值，開發團隊必須理解並關注用戶需求，將其轉化為可實現的 User Story。只有這樣，才能創造出一個有價值的產品，並滿足用戶需求。\n可預估的 (Estimable) 必須能夠對它們的大小、難易度和所需時間進行預估。這樣可以幫助開發團隊更好地分配工作，同時確保產品交付的準時性和品質。通過對 User Story 進行預估，開發團隊可以更好地掌握開發進度和進度風險，並提前進行調整。但需要注意的是，User Story 的預估並不是一個精確的科學，因為它們可能受到多種因素的影響。因此，開發團隊需要持續進行評估和調整，以確保 User Story 的準確性和可行性，並最終創造出高質量的產品。","title":"一個好的 User Story, 應具備哪些要素?"},{"content":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 Scrum Guide 說 Product Owner 需要依照價值排好 Backlogs，讓整個團隊可有穩定的節奏開發產品，但怎麼排序往往是個難題，有沒有一個框架可以參考?\nhttps://uxdesign.cc/how-to-choose-your-product-prioritization-framework-ff0320d63ebf 的作者製作了一個框架提供參考\n可以用兩個面向各字給 1-5 分來決定用哪種價值排序方式\n需給客戶驗證的程度: 需要在End User的參與下進行一定程度的數據收集或假設驗證練習來驗證程度，越密切分數越高 定性定量的程度: 需要更多的數據驗證，因素越多分數越高 透過這樣的兩個面向，可以初步選擇所使用的框架，本文以最 MoSCoW 及 WSJF 做舉例\nMoSCoW MoSCoW是一種常用的優先順序法，用於將需求和功能分為四個類別：\n\u0026ldquo;M\u0026rdquo; 代表必需品（Must Have），這些功能或需求是關鍵的，必須在產品中實現，否則產品無法正常工作或無法交付。這些功能或需求通常是必要的核心功能或重要的業務需求。以我自己定義通常為 \u0026ldquo;沒有交付就不能達成商業目標\u0026rdquo; 的項目 \u0026ldquo;S\u0026rdquo; 代表應該品（Should Have），這些功能或需求是非常重要的，但不是必須的。如果有足夠的時間和資源，這些功能或需求應該被優先實現。 \u0026ldquo;C\u0026rdquo; 代表願望品（Could Have），這些功能或需求是可選的，通常是增強產品體驗或功能的選項。如果時間和資源允許，可以考慮實現這些功能或需求。 \u0026ldquo;W\u0026rdquo; 代表不需要品（Won’t Have），這些功能或需求是不必要的，不需要在產品中實現。這些功能或需求可能已被視為過時或不再需要。 當然，以上可以自己定義，目的為直覺得加速判斷，以我自己定義可以用以下方式來做收斂:\nMust Have: 沒有交付就不能達成商業目標 Should Have: 重要但短期有替代方案 Could Have: 不做沒關係，但做了能為產品帶來加分效果 Won’t Have: 就算做了有無助於提升產品價值 而 Azure Boards 可以怎麼做? 我看過太多的團隊一開始建立複雜的 Work Item 階層關係，導致單子都維護不好，其實我認為用一種單子也可以簡化很多流程，目的是讓需求開始有排序，透過 MoSCow 搭配 Priority 1-4 欄位，就可以很快的開始有優先順序\nWeighted Shortest Job First (WSJF) 隨著需求越來越多，單子越來越複雜，MoSCoW 可能無法開始滿足排序的需求，這時可以考慮加權最短作業優先法（Weighted Shortest Job First，簡稱WSJF）是一種產品優先順序框架，它將各個項目的價值、時間和風險因素納入考慮，幫助團隊優先選擇高價值、低風險、短期完成的項目或功能。WSJF通常被應用在敏捷開發、DevOps和Lean等方法中。在WSJF中，每個項目都被賦予一個權重，該權重代表該項目對整體產品目標的重要性。該權重由四個因素計算而得：\n商業價值（Business Value）：該項目能為產品帶來的商業利益、營收增長、市場佔有率等。 時間關鍵性（Time Criticality）：該項目需要在多快的時間內完成，例如對市場機會的回應。 風險評估（RROE Value）：該項目對於減少未來風險的貢獻度，例如減少成本、提高品質等。 機會成本（Job Size）：該項目能為產品帶來的機會成本，例如提升用戶體驗、擴大市場等。 WSJF = 1+2+3/4\n其中最重要的概念是考量的 Job Size，越重要的需求，但開發的成本高，交付也就久，敏捷在談的就是盡早交付快速驗證，對吧?\n而 Azure Boards 可以怎麼做? 可以安裝這個 套件，他能在填寫 1-4 欄位 (需自訂欄位) 時自動算出 WSJF 的數字，以利排序\nProduct Roadmap 不是敏捷就沒有 Roadmap, 敏捷在於適應變化，但仍要有長期規劃，通常我會習慣以季為單位，針對市場變化調整，有一個很好的方式可以透過下列來做分類\nlong term (年) mid term (下一季) near term ( sprint) 在 Azure Boards 你可以用 Delivery Plans 來用橫向展開，或者是用單純的看版來左右拖拉呈現\n如果你想要看完整的演講影片，現在也已經傳到 Youtube\n影片: https://www.youtube.com/watch?v=G0ggjY5kzF8 簡報: https://speakerdeck.com/kyleshen/dot-net-conf-taiwan-na-xie-nian-yong-azure-boards-jiao-fu-guo-de-chan-pin 5篇 Scurm 痛點文章 Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序? 一個好的 User Story, 應具備哪些要素? 估複雜度好花時間? 怎麼抓 Sprint 的能量? 讓 Daily Meeting 不要淪為機械式的報告 在 Retro Meeting 停下腳步，檢討改善 ","permalink":"https://kyleap.github.io/azure-boards-backlogs/","summary":"這是我在 .NET Conf Taiwan 2022 的演講 - 那些年用 Azure Boards 交付過的產品 以 5 個痛點來分享之前 Scurm 所遇到的問題，當時怎麼解決， 並將其整理成為文章版更難完整記錄本場的內容 Scrum Guide 說 Product Owner 需要依照價值排好 Backlogs，讓整個團隊可有穩定的節奏開發產品，但怎麼排序往往是個難題，有沒有一個框架可以參考?\nhttps://uxdesign.cc/how-to-choose-your-product-prioritization-framework-ff0320d63ebf 的作者製作了一個框架提供參考\n可以用兩個面向各字給 1-5 分來決定用哪種價值排序方式\n需給客戶驗證的程度: 需要在End User的參與下進行一定程度的數據收集或假設驗證練習來驗證程度，越密切分數越高 定性定量的程度: 需要更多的數據驗證，因素越多分數越高 透過這樣的兩個面向，可以初步選擇所使用的框架，本文以最 MoSCoW 及 WSJF 做舉例\nMoSCoW MoSCoW是一種常用的優先順序法，用於將需求和功能分為四個類別：\n\u0026ldquo;M\u0026rdquo; 代表必需品（Must Have），這些功能或需求是關鍵的，必須在產品中實現，否則產品無法正常工作或無法交付。這些功能或需求通常是必要的核心功能或重要的業務需求。以我自己定義通常為 \u0026ldquo;沒有交付就不能達成商業目標\u0026rdquo; 的項目 \u0026ldquo;S\u0026rdquo; 代表應該品（Should Have），這些功能或需求是非常重要的，但不是必須的。如果有足夠的時間和資源，這些功能或需求應該被優先實現。 \u0026ldquo;C\u0026rdquo; 代表願望品（Could Have），這些功能或需求是可選的，通常是增強產品體驗或功能的選項。如果時間和資源允許，可以考慮實現這些功能或需求。 \u0026ldquo;W\u0026rdquo; 代表不需要品（Won’t Have），這些功能或需求是不必要的，不需要在產品中實現。這些功能或需求可能已被視為過時或不再需要。 當然，以上可以自己定義，目的為直覺得加速判斷，以我自己定義可以用以下方式來做收斂:\nMust Have: 沒有交付就不能達成商業目標 Should Have: 重要但短期有替代方案 Could Have: 不做沒關係，但做了能為產品帶來加分效果 Won’t Have: 就算做了有無助於提升產品價值 而 Azure Boards 可以怎麼做?","title":"Product Owner 需維護好 Backlogs, 但怎麼判斷價值順序?"},{"content":"Scrum 353 Scrum Inc.創辦人 Jeff Sutherland 在他的書《Scrum: The Art of Doing Twice the Work in Half the Time》中提出的概念，它是 Scrum 框架的一種擴展，旨在幫助團隊更好地應對複雜性和不確定性。\n裡面提到有個口訣 Scrum 353 的，它的核心概念是 \u0026ldquo;三個角色、五個事件、三個交付物\u0026rdquo;，其中：\n三個角色：產品負責人、開發團隊、Scrum Master 五個事件：Sprint、Sprint Planning、Daily Scrum、Sprint Review、Sprint Retrospective 三個工件：產品待辦清單、Sprint待辦清單、增量 在跑 Scrum 一直以來我都是用 Azure DevOps 來完成整個流程，但就像 JIRA 一樣，裝了一些外掛後會讓整個運作更順，此篇文章整理我在實務上推薦必裝的外掛\n此文章會不定期更新\n價值排序 WSJF (Weighted Shortest Job First\nbacklogs開始跟大海一樣，需要的是更多因素的評估，WSJF是一個考量 PMF 及開發成本的評估方式，此外掛可以自動計算出分數\nTags Manager\nTag 維護的好，可以用 Query 產生出更多的報表，此外掛可以用來維護已建立的 Tag\nMultivalue control\n如果想區別單子的Compoment，除了 Tag 外，也可以獨立用這種方式進行分類\nSpecMap aka User Story Mapping\n如果想跟團隊跑 User Story Mapping 腦力激盪跑出產品方向，這就是在 Azure DevOps 實踐的方式\nRefine \u0026amp; Planning Personas\nPO 對於專案的 TA 進行 Persona 描述讓團隊理解為哪一類型使用者\nEstimate\nPlanning 針對單一 Story 進行複雜度估算，這是一個即時的 Story poker 工具\nSplit!\n當 Sprint 有遺留單子時，可以透過這個工具快速將遺留的拆解到新的 Sprint\nSprint Goal\n在 Azure Board 顯示當前 Sprint 目標\nDaily Board Group\n在 Backlogs 檢視下，可以用這種方式移動 Board View 的渠道\nSprint Drop Plan\n以橫軸方式瀏覽當前 Sprint 單子狀況\nReview \u0026amp; Retro Retrospectives\n透過 Collect -\u0026gt; Group -\u0026gt; Vote -\u0026gt; Actions 方向進行回顧會議\n","permalink":"https://kyleap.github.io/azure-board-extensions/","summary":"Scrum 353 Scrum Inc.創辦人 Jeff Sutherland 在他的書《Scrum: The Art of Doing Twice the Work in Half the Time》中提出的概念，它是 Scrum 框架的一種擴展，旨在幫助團隊更好地應對複雜性和不確定性。\n裡面提到有個口訣 Scrum 353 的，它的核心概念是 \u0026ldquo;三個角色、五個事件、三個交付物\u0026rdquo;，其中：\n三個角色：產品負責人、開發團隊、Scrum Master 五個事件：Sprint、Sprint Planning、Daily Scrum、Sprint Review、Sprint Retrospective 三個工件：產品待辦清單、Sprint待辦清單、增量 在跑 Scrum 一直以來我都是用 Azure DevOps 來完成整個流程，但就像 JIRA 一樣，裝了一些外掛後會讓整個運作更順，此篇文章整理我在實務上推薦必裝的外掛\n此文章會不定期更新\n價值排序 WSJF (Weighted Shortest Job First\nbacklogs開始跟大海一樣，需要的是更多因素的評估，WSJF是一個考量 PMF 及開發成本的評估方式，此外掛可以自動計算出分數\nTags Manager\nTag 維護的好，可以用 Query 產生出更多的報表，此外掛可以用來維護已建立的 Tag\nMultivalue control\n如果想區別單子的Compoment，除了 Tag 外，也可以獨立用這種方式進行分類\nSpecMap aka User Story Mapping\n如果想跟團隊跑 User Story Mapping 腦力激盪跑出產品方向，這就是在 Azure DevOps 實踐的方式","title":"以 Scrum 流程來看 Azure Boards 有哪些必裝的 Extensions 外掛"},{"content":"前言 在 Scrum 的開發過程中，維護好 Work Items 是非常重要的一個步驟。這些 Work Items 會在Planning Meeting 或者是 Refine Meeting 被拿出來討論，如果內容定義不清，會導致討論會議的浪費，為了確保團隊在建立工作項目時有一致的認識和理解，以及在回顧過程中能夠更清楚地了解工作項目的緣由，此篇文章將介紹我在 Epic/Feature/User Story/Task 會寫的項目，以實際案例來分享，並簡單介紹如何在 Azure Board 裡面變成範本直接讓團隊套用\n先來聊一下 Workitem 層級 在 Azure Devops 預設就有四種 Process 可以選，以目前 Agile 為大宗的開發模式，多半會選擇 Scurm/Agile，但這兩者基本上大同小異，如果要我一句話解讀這些 Item 代表的定義，我會這樣解釋:\nEpic: 高層老闆看的, 通常是公司的高大尚目標, 時間長度不超過1年 Feature: PM或主管看的, 把高大尚收斂後的具體策略, 時間長度以季為單位 User Story: PM/工程師看的, 達成策略的手段, 時間以1個sprint可交付為單位 Task: 工程師看的, 達成手段需要拆寫的實作, 時間以1-2天為單位 至於這些 Workitem 該寫什麼? 以下章節來細部說明\n我認為 Azure Devops 設計緣由可適用於大型組織，但導入需要逐步，也許小團隊可用 User Story+Task就可足夠了，太早一步到位反而會導致交付混亂\nEpic Epic 對我來講它代表了一個大型的、跨部門的產品目標，這個目標還沒有詳細的策略，也可以把它定義成是老闆的期望，也可以是 OKR 上層的指標，以 Product Owner 角度而言，我會思考如何傳達老闆的想法，並把它收斂成可量化的目標範圍，這會是一個半年不超過一年的時間長度，並需要有達成的指標，在標題上明確表達(e.g. 提升產品使用者滿意度 \u0026gt; 5%)，內容以描述這個需求的背景是什麼，經老闆與客戶的原話，有條理地闡述清楚，以 Azure DevOps Roadmap 來看，Updated Boards experience 這個層級的寫法就很適合當 Epic，屬於比較大方向的描述\nFeature Feature 會是要達成 Epic 所需要的策略，如果有 UX Team 配合的話，通常會是 Design Thinking 後的結果，或者也可以以呈現 Roadmap 的角度下去思考，讓團隊更好地了解當前專注的目標與的開發進度，特別注意 Feature 時間長度不應該超過一季，如果超過代表這個策略還需要再收斂。Feature 的內容會有點類似產品經理所撰寫的 PRD 文件，我通常會依據以下內容去做撰寫\n問題敘述: 緣由與背景，清楚描述 Why? 產品假說: 需求的假設，提供什麼價值，能解決此問題 How? 假說可以用這種範本撰寫 We believe that {function} For {somebody} , Will achieve {outcome}\n產品範疇: 這邊通常會定義最核心的 scope, 也可以描述哪些本次phase先不考慮 成功指標: 好的需求通常要能測量假說，擬定領先指標(lead metrics)可以在開發完成後做前後比較 未知風險: 可以多留一點初步想到的問題，在 Planning meeting 激發討論 User Story 在 Feature 層級清楚策略後，應該能再細拆要達成這個策略所需要做的事情，並且以使用者角度讓工程師理解使用者想要達成的功能。好的 Story 我覺得標題寫清楚就夠了，內容補充這張的重點並連結到 Feautre 或 PRD (看全貌的規格)\nAs a {somebody}, I want to {do something}, so that {some business value}\n一個好的 User Story 應該要注意不能有相依性，可以獨立交付，必且撰寫出來要很明確甚至是能讓人快速有雛型畫面的，因為這些 Story 都是為了在 Planning Meeting 開啟對話，最重要的一點是要能估算，所以 Acceptance criteria (AC) 就變成是很重要的一件事，通常 AC 建議會用Given/When/Then撰寫，如果有 QA Team 的話也能從這些 AC 去長出更多的測試案例，以 Swimlane rules 這個改版功能為例，也是帶了 User Story 的寫法\nTask 工程師具體要怎麼分工，前後端怎麼實作，DBA要準備什麼才能達成 Story 目標，這個層級我會放在 Task，撰寫內容不會太在意，標題也只要能區別目的即可，但這邊通常會有幾個規範，第一個是時間長度請拆解到 1-2 天可交付的單位大小，讓 Sprint 過程能反映到 Story 的完成度，另外 Code Review, 發 Pull Request 時候可以綁 Workitem 到 Task，讓 commit 的 code 能對應到原始需求。\nTask 拆的細，越能掌握目前 Sprint 的健康指數，因為都可以回推到 Story/Feautre 的完成度\n如何建立 workitem template 當單子都定義好，且團隊都有共識後，在 Azure Board 中可以將這些要寫的項目變成範本\n且可以將它變成是一個 URL，每次開單時候可點擊這個網址進行套入\n如果要維護 template 可以到 Project Settings -\u0026gt; Boards -\u0026gt; Team configuration 去管理\n以上是針對 Workitem 自己的一些經驗與想法，當然要寫什麼，要拆什麼單子，還是回到各團隊自己定義出來\nReference\nhttps://learn.microsoft.com/en-us/azure/devops/boards/backlogs/work-item-template?view=azure-devops\u0026amp;tabs=browser ","permalink":"https://kyleap.github.io/azure-board-workitem-template/","summary":"前言 在 Scrum 的開發過程中，維護好 Work Items 是非常重要的一個步驟。這些 Work Items 會在Planning Meeting 或者是 Refine Meeting 被拿出來討論，如果內容定義不清，會導致討論會議的浪費，為了確保團隊在建立工作項目時有一致的認識和理解，以及在回顧過程中能夠更清楚地了解工作項目的緣由，此篇文章將介紹我在 Epic/Feature/User Story/Task 會寫的項目，以實際案例來分享，並簡單介紹如何在 Azure Board 裡面變成範本直接讓團隊套用\n先來聊一下 Workitem 層級 在 Azure Devops 預設就有四種 Process 可以選，以目前 Agile 為大宗的開發模式，多半會選擇 Scurm/Agile，但這兩者基本上大同小異，如果要我一句話解讀這些 Item 代表的定義，我會這樣解釋:\nEpic: 高層老闆看的, 通常是公司的高大尚目標, 時間長度不超過1年 Feature: PM或主管看的, 把高大尚收斂後的具體策略, 時間長度以季為單位 User Story: PM/工程師看的, 達成策略的手段, 時間以1個sprint可交付為單位 Task: 工程師看的, 達成手段需要拆寫的實作, 時間以1-2天為單位 至於這些 Workitem 該寫什麼? 以下章節來細部說明\n我認為 Azure Devops 設計緣由可適用於大型組織，但導入需要逐步，也許小團隊可用 User Story+Task就可足夠了，太早一步到位反而會導致交付混亂\nEpic Epic 對我來講它代表了一個大型的、跨部門的產品目標，這個目標還沒有詳細的策略，也可以把它定義成是老闆的期望，也可以是 OKR 上層的指標，以 Product Owner 角度而言，我會思考如何傳達老闆的想法，並把它收斂成可量化的目標範圍，這會是一個半年不超過一年的時間長度，並需要有達成的指標，在標題上明確表達(e.g. 提升產品使用者滿意度 \u0026gt; 5%)，內容以描述這個需求的背景是什麼，經老闆與客戶的原話，有條理地闡述清楚，以 Azure DevOps Roadmap 來看，Updated Boards experience 這個層級的寫法就很適合當 Epic，屬於比較大方向的描述","title":"在 Azure Board 建立 workitem template 加強團隊共識"},{"content":"建立良好規範與開發者的互動 當團隊開始 Pull Request 順暢後，慢慢的也會討論出一些規範與 Pull Request 的重點, 如果在建立的時候，能有一些 Checklist 讓新工程師自我檢查，或者是讓每個工程師都能遵循相同的寫法，讓同事快速理解這次審核的目的與重點，這時就能利用 Template 來標準化\n建立第一個 Pull Request Template Template 檔案可以是 .md 或 .txt 的檔案類型，預設的範本放置以下的路徑即可生效 (則一即可)\n\u0026lt;repository root\u0026gt;/.azuredevops/pull_request_template/ \u0026lt;repository root\u0026gt;/.vsts/pull_request_template/ \u0026lt;repository root\u0026gt;/docs/pull_request_template/ \u0026lt;repository root\u0026gt;/pull_request_template/ 以下直接用 Azure Repo 的 UI 來做示範：\n建立一個 Folder\n建立一個 docs/pull_request_template 資料夾, 並直接新增 Default-PR-Template.md 檔案\n撰寫一個範本並 Commit\n**將這支 PR 所做的事情，取代這段文字，描述越詳細越好。** 提交 PR 前，請先檢查以下是否已完成： - [ ] 是否已將此 PR 綁定相關 Ticket - [ ] 請先自行 rebase/merge - [ ] 是否撰寫單元測試 - [ ] 這支 PR 是否有明確的標題、內容描述 ----------------- 補充說明: 建立一個 Branch 並針對任一檔案做異動\n建立 Pull Request\n此時就能看到多了一個 Add a template 選項，可直接帶入\n如何針對 Branch 做範本設定 但讓工程師用選的還是會忘記，另外就是如果想針對不同分支有不同的範本，這時就能再建立 **branches **資料夾，依據分支名建立範本，譬如我想要在 merge 至 main 自動帶入範本，就能新增一個 **main.md **檔案\n這時就會發現開 Pull Request 後，範本自動帶進來了\n而如果要讓 feautre/* 或 hotfix/* 這種分支名稱都帶入同一個範本，只要新增 feature.md 即可\n建立 Additional Template 有時除了範本外，也可以視情況插入一些額外的內容，這時候只要新增類似的範本到上面所提到的資料夾底下即可，譬如我們在資料夾底下加入一個 md-table.md 的範本\n| Header1 | Header2| | ---- | ---- | | ItemA | A | | ItemB | B | 此時就可以選剛剛建立的範本，直接插入\n參考連結 Improve pull request descriptions with pull request templates — Azure Repos | Microsoft Learn\n","permalink":"https://kyleap.github.io/azure-board-pull-request-template/","summary":"建立良好規範與開發者的互動 當團隊開始 Pull Request 順暢後，慢慢的也會討論出一些規範與 Pull Request 的重點, 如果在建立的時候，能有一些 Checklist 讓新工程師自我檢查，或者是讓每個工程師都能遵循相同的寫法，讓同事快速理解這次審核的目的與重點，這時就能利用 Template 來標準化\n建立第一個 Pull Request Template Template 檔案可以是 .md 或 .txt 的檔案類型，預設的範本放置以下的路徑即可生效 (則一即可)\n\u0026lt;repository root\u0026gt;/.azuredevops/pull_request_template/ \u0026lt;repository root\u0026gt;/.vsts/pull_request_template/ \u0026lt;repository root\u0026gt;/docs/pull_request_template/ \u0026lt;repository root\u0026gt;/pull_request_template/ 以下直接用 Azure Repo 的 UI 來做示範：\n建立一個 Folder\n建立一個 docs/pull_request_template 資料夾, 並直接新增 Default-PR-Template.md 檔案\n撰寫一個範本並 Commit\n**將這支 PR 所做的事情，取代這段文字，描述越詳細越好。** 提交 PR 前，請先檢查以下是否已完成： - [ ] 是否已將此 PR 綁定相關 Ticket - [ ] 請先自行 rebase/merge - [ ] 是否撰寫單元測試 - [ ] 這支 PR 是否有明確的標題、內容描述 ----------------- 補充說明: 建立一個 Branch 並針對任一檔案做異動","title":"在 Azure Repo 設定 Pull Request Template"},{"content":"前言 除了 Azure 外，還有很多公有雲的服務可以做選擇，譬如AWS/GCP/heroku…等等，甚至可能不採用公有雲服務，自己架設伺服器也是有可能，基於以上這些不同環境，我會建議將服務都包成 docker，讓所開發的應用程式比較不會受到伺服器環境的影響，本篇就來介紹從一個基本的 FastAPI 包成 docker，並發佈的 Azure 服務上\ndocker 環境安裝 Docker 是一種軟體平台，可讓您快速地建立、測試和部署應用程式。Docker 將軟體封裝到名為容器的標準化單位，其中包含程式庫、系統工具、程式碼和執行時間等執行軟體所需的所有項目。使用 Docker，您可以將應用程式快速地部署到各種環境並加以擴展，而且知道程式碼可以執行 — from AWS 的介紹 https://aws.amazon.com/tw/docker/\n關於 docker 環境的安裝，網路上教學應該很多了，此篇文章就不再介紹，如果安裝成功，應該在命令列透過 docker — version 指令可以看到版本\n建立一個基本的 FastAPI 本篇再以一個最基本的 API 來做包成 docker 的介紹\nrequirements.txt fastAPI uvicorn main.py from typing import Optional from fastapi import FastAPI app = FastAPI() @app.get(\u0026quot;/\u0026quot;) def read_root(): return {\u0026quot;Hello\u0026quot;: \u0026quot;World\u0026quot;} @app.get(\u0026quot;/items/{item_id}\u0026quot;) def read_item(item_id: int, q: Optional[str] = None): return {\u0026quot;item_id\u0026quot;: item_id, \u0026quot;q\u0026quot;: q} 執行以下指令 pip install -r requirement.txt uvicorn main:app --reload 確認 localhost:8000 能在本機成功跑起站台\nDockerfile Dockerfile是一個設定檔，用來定義與管理當前這個資料夾所要封裝的應用程式，執行 docker 指令時，就會依造這個檔案所定義的步驟，將應用程式封裝到容器裡面\nDockerfile # 設定基底的 image，我們指定用python 3.9，執行時會從 dockerhub 這個平台將 python 的 image 拉下來 FROM python:3.9 # 設定容器的工作資料夾 WORKDIR /code # 複製本地資料夾 requirements.txt這個檔案至容器的 /code/requirements.txt COPY ./requirements.txt /code/requirements.txt # 於容器執行 pip 腳本 RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt # 複製本地資料夾所有檔案(.代表當前目錄)至容器裡的資料夾 /code/app COPY . /code/app # 透過uvicorn執行FastAPI伺服器 CMD [\u0026quot;uvicorn\u0026quot;, \u0026quot;app.main:app\u0026quot;, \u0026quot;--host\u0026quot;, \u0026quot;0.0.0.0\u0026quot;, \u0026quot;--port\u0026quot;, \u0026quot;80\u0026quot;] 包裝 docker image 將下來我們可以將我們的 FastAPI 程式，封裝成 image，名稱為 myfastapi\ndocker build -t myfastapi . 執行 docker images 應該會看到目前在本機已經有的 images\n執行 docker image 有了 image 後，下一步就是透過這個 image，把服務執行起來，這時我們可以用\ndocker run -d --name myapi -p 80:80 myfastapi -d: 代表是在背景執行\n— name: 這個服務的名稱\n-p: 指令內外port \u0026lt;docker內部\u0026gt;:\u0026lt;你的本機對應的port\u0026gt;\nmyfastapi: image 名稱\n執行後，應該會得到一個這個服務的唯一ID，我們可以用 **docker ps **來檢視目前執行中的服務\n瀏覽器到 localhost 確認能回傳 Hello World\n建立 docker image 的家 — Container Registry 進入 Azure Portal 搜尋 ACR 服務\n建立 ACR 服務，我們選 basic 方案即可\n發佈 docker image ACR 服務建立完了，下一步我們要將我們的 myfastapi 發佈到 ACR 上，在這步驟我會建議用 VS Code 是最方便的，如果要用指令的話，可以研究 Azure CLI 這個工具，來跟 ACR 做溝通\nVS Code 安裝 — Azure Account 安裝後執行 ctrl (cmd) + p 輸入 sign in 登入 azure 帳號\nVS Code 安裝 docker 確認 docker 的擴充功能，可以看到剛剛的包好的 image\n點選右鍵 push\n選擇剛剛建立的ACR服務後，就會看到命令列正在推送\n回到 azure portal 確認有成功推上\n將 image 發佈成 Azure Web App Service 建立 Azure Web App ，這次發行方式我們選擇 docker container\n選擇 ACR 服務與 Image 名稱\n進到 Azure Web App 服務\n確認瀏覽器能得到 Hello World\n持續交付 程式一定會一直改，也會包成新版本的 image，有沒有可能在有新的 image 時自動發行網站，在Azure可以這樣設定\n點選 Deployment Center 並勾選 Continuous deployment\n包成 image 我們再依序練習一次 main.py 改成 hello world 2\n執行 docker build -t myfastapi . — no-cache ，這次我們多加 — no-cache 指令避免它緩存住\n透過 VSCode 再 push 一次\n等待幾分鐘後，就能看到是新版的內容了\n小結 docker 可說是軟體工程師必備的知識了，一篇文章也無法完整教完整個docker的概念，建議可以用本文提到的關鍵字延伸去學習更多的知識，而本文章是以 Azure 文章為例，當應用程式容器化後，發佈或自建到其他平台也會方便不少，譬如 heroku 可以參考下面的文章，步驟其實大同小異，只是發到不同的 Container Registry / Cloud Platform\n站在 Docker 的肩膀上，部署任何語言的 Web 應用到 Heroku | by Larry Lu | Starbugs Weekly 星巴哥技術專欄 | Medium\n","permalink":"https://kyleap.github.io/docker-deploy-fastapi-to-azure/","summary":"前言 除了 Azure 外，還有很多公有雲的服務可以做選擇，譬如AWS/GCP/heroku…等等，甚至可能不採用公有雲服務，自己架設伺服器也是有可能，基於以上這些不同環境，我會建議將服務都包成 docker，讓所開發的應用程式比較不會受到伺服器環境的影響，本篇就來介紹從一個基本的 FastAPI 包成 docker，並發佈的 Azure 服務上\ndocker 環境安裝 Docker 是一種軟體平台，可讓您快速地建立、測試和部署應用程式。Docker 將軟體封裝到名為容器的標準化單位，其中包含程式庫、系統工具、程式碼和執行時間等執行軟體所需的所有項目。使用 Docker，您可以將應用程式快速地部署到各種環境並加以擴展，而且知道程式碼可以執行 — from AWS 的介紹 https://aws.amazon.com/tw/docker/\n關於 docker 環境的安裝，網路上教學應該很多了，此篇文章就不再介紹，如果安裝成功，應該在命令列透過 docker — version 指令可以看到版本\n建立一個基本的 FastAPI 本篇再以一個最基本的 API 來做包成 docker 的介紹\nrequirements.txt fastAPI uvicorn main.py from typing import Optional from fastapi import FastAPI app = FastAPI() @app.get(\u0026quot;/\u0026quot;) def read_root(): return {\u0026quot;Hello\u0026quot;: \u0026quot;World\u0026quot;} @app.get(\u0026quot;/items/{item_id}\u0026quot;) def read_item(item_id: int, q: Optional[str] = None): return {\u0026quot;item_id\u0026quot;: item_id, \u0026quot;q\u0026quot;: q} 執行以下指令 pip install -r requirement.","title":"透過 docker 發行 FastAPI 到 Azure"},{"content":" 一個跑進設計學院的工程師\n目前於製造業擔任 SRE Lead，現任微軟最有價值專家 (Microsoft MVP)，期望能利用 IT力量改變世界。任職過軟體、教育、物流產業…等，依據不同產業的經驗，以使用者角度思維出發設計成可行性的資訊架構。與友人經營 Study4.TW 技術社群，定期舉辦各種領域的技術研討會，在團隊裡樂於分享技術，相信與人分享的同時，會自己能帶來更多。\nLinkedin : https://www.linkedin.com/in/kyleshen-net\n線上課程\n用 Python 快速打造 8 款 LINE Bot 生活助理 (https://hiskio.com/fundraising/602/about?s=tc) 活用 Line Bot + 商業思維打造 MVP 產品！ (https://hiskio.com/courses/588/about) 演講經歷\n2024 Hello World Dev Conference - 講師 (不只是主管 - 那些年技術管理教會我的事 👀) 2024 TSMC IT Community Meetup - 講師 (為什麼需求總是被誤解？實例化需求可能是缺少的拼圖 🧩) 2024 MVP 聚技站 - 講師 (低程式碼 AI 時代 TW（五）：Azure Boards with Power Automate) 2023 Agile Submmit - 講師 (大型組織下的 PO 探索之路) 2022 .NET Conf Taiwan - 講師 (那些年用 Azure Boards 交付過的產品) 2022 LINE TAIWAN TECHPULSE 2022 Startup Demo 2021 .NET Conf Taiwan - 活動策劃小組 2021 MOPCON UnConf - 講師 (行動化解決方案的技術選型，交給 Line Bot 來搞定) 2021 六角學院「大神來六角」 - 講師 (用 Line Bot 來打造 MVP 產品) 2020 .NET Conf Taiwan - 講師 (用.NET Core 來開發.NET Conf 2020 Line活動機器人) 2020 微軟亞太技術年會 - DevDays Asia Online 講師 2019 Microsoft AI Day - 講師 2019 微軟亞太技術年會 - DevDays Asia講師 2019 ESH環安衛年度高峰會講師 2018 ESH環安衛年度高峰會講師 2018 Insider Dev Tour @ 台北 - 講師 (無伺服器架構與 Azure Functions) 2018 Azure Bootcamp @ 北京 - 講師 (快速建立Web服务 - 使用Azure Function) 2017 Study4.TW-Dec-2017 @ 台北 - 講師 (.NET Core 與 Angular 之你儂我儂) 2016 當ASP.NET MVC遇見Angular.js MVC @ 台中 - 講師 2016 Study4Love @ 台中 - 講師 (Cordova with Ionic) 2015 Global Azure Bootcamp @台中 - 講師 (Auzre Website) 2015 Apache Cordova ( PhoneGap ) 開發 @台中 - 講師 Study4\nStudy4 成立於2011/9/25，我們希望藉由社群推廣的力量，來讓台下的朋友聽到來自不同縣市的大師講課，也讓台上年輕一輩的技術傳教士能不斷的琢磨並且追上大師，這是一個社群，我們希望透過分享，給偏遠地區、年輕一輩、每一個人，多那麼一點機會\nhttps://study4.tw/\n","permalink":"https://kyleap.github.io/kyleshen/","summary":"一個跑進設計學院的工程師\n目前於製造業擔任 SRE Lead，現任微軟最有價值專家 (Microsoft MVP)，期望能利用 IT力量改變世界。任職過軟體、教育、物流產業…等，依據不同產業的經驗，以使用者角度思維出發設計成可行性的資訊架構。與友人經營 Study4.TW 技術社群，定期舉辦各種領域的技術研討會，在團隊裡樂於分享技術，相信與人分享的同時，會自己能帶來更多。\nLinkedin : https://www.linkedin.com/in/kyleshen-net\n線上課程\n用 Python 快速打造 8 款 LINE Bot 生活助理 (https://hiskio.com/fundraising/602/about?s=tc) 活用 Line Bot + 商業思維打造 MVP 產品！ (https://hiskio.com/courses/588/about) 演講經歷\n2024 Hello World Dev Conference - 講師 (不只是主管 - 那些年技術管理教會我的事 👀) 2024 TSMC IT Community Meetup - 講師 (為什麼需求總是被誤解？實例化需求可能是缺少的拼圖 🧩) 2024 MVP 聚技站 - 講師 (低程式碼 AI 時代 TW（五）：Azure Boards with Power Automate) 2023 Agile Submmit - 講師 (大型組織下的 PO 探索之路) 2022 .","title":"Kyle Shen"},{"content":"Azure Static Web App 是什麼? 在今年 5 月 Azure 推出了 Azure Static Web App 的服務，如果要一句話來說，建立此服務時，可以一併設定與 Github or Azure DevOps 連結，在每次 Commit 的時候，就能直接將前端應用程式佈署到Azure，大大的增加開發體驗。除了靜態的 HTML、CSS、JavaScript，支援主流框架 React/Angular/Vue.js，也支援用 C# 撰寫 SPA 的框架 Blazor，支援的前端框架可以到此連結，後端可搭配 Azure 的 Serverless 服務 (Azure Function) 來實現 Api 的串接，透過 Reverse-Proxy 也省去的 CORS 的設定\nAzure Static Web App 的價格 在免費的方案，即提供 SSL 的 Hosting，及 2 個自訂網域，可於 DNS Server 設定 cname，也可佈署你的後端程式於 Azure Function 搭配前後端串接，享有 100 萬個 Request 的免費額度\n發佈 LINE Liff App 到 Azure Static Web App LIFF 全名是 LINE Front-end Framework，一個可以在 LINE App 內運作的網頁程式，可以將一些不適合對話式的情境，結合於 Line App 裡面，達成一致的體驗，也因為是 Liff App 是全前端實現，所以很適合發佈到 Azure Static Web App，以下用官方的 Liff Starter 來做一個部屬示範 https://github.com/line/line-liff-v2-starter\nFork Liff Starter Repo 於 Azure 建立 Azure Static Web App 登入 Github 完成驗證 選擇 Repo 與 Branch 選擇 Frameworks 選擇不同的Framework 會影響 Github Action 設定的 build command，此例為單純的 JavaScript，故選擇Custom，Root 設定為 /public\n建立後 Github Action 會同步完成 CI 設定並開始發佈 瀏覽完成後的部署 出現此畫面代表正常，因為我們還未指定 LIFF ID\n建立 Liff App 到 https://developers.line.biz/ 後台，於 Provider 底下建立一個 Line Login 服務 (記得 Bot 跟 Line Login 要在同一個 Provider 底下) 將 LIFF ID 複製起來，LIFF URL 就是待會我們要觸發的網址 修改程式並 Commit\nLIFF Starter 這個 Repo 我們要改兩個地方，一個是將 userNodeJS 改為 false (因為我們是跑 static 站台)，defaultLiffId 改為剛剛複製的 LIFF ID\nCommit 後可以看到 CI 也同時觸發\n瀏覽結果\n小結 用 Azure Static Web App 我覺得有三大好處 📌 免費的 Hosting, SSL 📌 從Repo出發自動建立好 Github Action CI/CD，專注在寫code就好 📌 方便整合 Azure Serverless (Azure Function) 服務也能開發後端 Api (每月有 100 萬 Request 免費)\n參考連結 Azure Static Web https://azure.microsoft.com/zh-tw/services/app-service/static/ Line LIFF Starter https://github.com/line/line-liff-v2-starter ","permalink":"https://kyleap.github.io/liff-app-deploy-azure-static-web/","summary":"Azure Static Web App 是什麼? 在今年 5 月 Azure 推出了 Azure Static Web App 的服務，如果要一句話來說，建立此服務時，可以一併設定與 Github or Azure DevOps 連結，在每次 Commit 的時候，就能直接將前端應用程式佈署到Azure，大大的增加開發體驗。除了靜態的 HTML、CSS、JavaScript，支援主流框架 React/Angular/Vue.js，也支援用 C# 撰寫 SPA 的框架 Blazor，支援的前端框架可以到此連結，後端可搭配 Azure 的 Serverless 服務 (Azure Function) 來實現 Api 的串接，透過 Reverse-Proxy 也省去的 CORS 的設定\nAzure Static Web App 的價格 在免費的方案，即提供 SSL 的 Hosting，及 2 個自訂網域，可於 DNS Server 設定 cname，也可佈署你的後端程式於 Azure Function 搭配前後端串接，享有 100 萬個 Request 的免費額度\n發佈 LINE Liff App 到 Azure Static Web App LIFF 全名是 LINE Front-end Framework，一個可以在 LINE App 內運作的網頁程式，可以將一些不適合對話式的情境，結合於 Line App 裡面，達成一致的體驗，也因為是 Liff App 是全前端實現，所以很適合發佈到 Azure Static Web App，以下用官方的 Liff Starter 來做一個部屬示範 https://github.","title":"將 Line Liff App 發佈到免費的 Azure Static Web App"},{"content":"這是我自己整理的 Line Message Api Postman Collections\n任何人只要 Import 後修改即可直接調用測試\n詳細內容請見我的 Github Repo: https://github.com/kyleap/line-message-api-postman\n","permalink":"https://kyleap.github.io/line-message-api-postman-collections/","summary":"這是我自己整理的 Line Message Api Postman Collections\n任何人只要 Import 後修改即可直接調用測試\n詳細內容請見我的 Github Repo: https://github.com/kyleap/line-message-api-postman","title":"Line Message Api Postman Collections"},{"content":"Flex Message 是 Line 透過 JSON 格式所實現的動態訊息，可以組出各種訊息格式，且比起 Template Message，Flex Message 也支援 Line 電腦版，故現在在寫 Line Bot 的時候，第一考量就會用 Flex 來自訂一些較特別的訊息，官網提供兩個 GUI 工具來快速拉 Flex 的訊息：\nBot Designer Flex Simulator 我自己比較常用是第二個，官網已經很多的範本提供套入\n且目前支援很方便的功能，可以直接發到手機裝置瀏覽結果\n在 Flex 的 image 類型是可支援放入 .APNG 格式圖片的動畫，透過以下網站可以將 Git 轉成 .APNG 格式\nhttps://ezgif.com/gif-to-apng\nDemo Flex 範例 { \u0026#34;type\u0026#34;: \u0026#34;bubble\u0026#34;, \u0026#34;body\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;vertical\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;image\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://apng.onevcat.com/assets/elephant.png\u0026#34;, \u0026#34;size\u0026#34;: \u0026#34;full\u0026#34;, \u0026#34;aspectMode\u0026#34;: \u0026#34;cover\u0026#34;, \u0026#34;aspectRatio\u0026#34;: \u0026#34;1:1\u0026#34;, \u0026#34;gravity\u0026#34;: \u0026#34;center\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;vertical\u0026#34;, \u0026#34;contents\u0026#34;: [], \u0026#34;position\u0026#34;: \u0026#34;absolute\u0026#34;, \u0026#34;background\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;linearGradient\u0026#34;, \u0026#34;angle\u0026#34;: \u0026#34;0deg\u0026#34;, \u0026#34;endColor\u0026#34;: \u0026#34;#00000000\u0026#34;, \u0026#34;startColor\u0026#34;: \u0026#34;#00000099\u0026#34; }, \u0026#34;width\u0026#34;: \u0026#34;100%\u0026#34;, \u0026#34;height\u0026#34;: \u0026#34;40%\u0026#34;, \u0026#34;offsetBottom\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetStart\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetEnd\u0026#34;: \u0026#34;0px\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;horizontal\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;vertical\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;horizontal\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;APNG Demo\u0026#34;, \u0026#34;size\u0026#34;: \u0026#34;xl\u0026#34;, \u0026#34;color\u0026#34;: \u0026#34;#ffffff\u0026#34; } ] } ], \u0026#34;spacing\u0026#34;: \u0026#34;xs\u0026#34; } ], \u0026#34;position\u0026#34;: \u0026#34;absolute\u0026#34;, \u0026#34;offsetBottom\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetStart\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetEnd\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;paddingAll\u0026#34;: \u0026#34;20px\u0026#34; } ], \u0026#34;paddingAll\u0026#34;: \u0026#34;0px\u0026#34; } } \u0026ndash;\nFlex Message Image https://developers.line.biz/en/reference/messaging-api/#f-image\n","permalink":"https://kyleap.github.io/2021-07-11-line-flex-message-animation/","summary":"Flex Message 是 Line 透過 JSON 格式所實現的動態訊息，可以組出各種訊息格式，且比起 Template Message，Flex Message 也支援 Line 電腦版，故現在在寫 Line Bot 的時候，第一考量就會用 Flex 來自訂一些較特別的訊息，官網提供兩個 GUI 工具來快速拉 Flex 的訊息：\nBot Designer Flex Simulator 我自己比較常用是第二個，官網已經很多的範本提供套入\n且目前支援很方便的功能，可以直接發到手機裝置瀏覽結果\n在 Flex 的 image 類型是可支援放入 .APNG 格式圖片的動畫，透過以下網站可以將 Git 轉成 .APNG 格式\nhttps://ezgif.com/gif-to-apng\nDemo Flex 範例 { \u0026#34;type\u0026#34;: \u0026#34;bubble\u0026#34;, \u0026#34;body\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;vertical\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;image\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://apng.onevcat.com/assets/elephant.png\u0026#34;, \u0026#34;size\u0026#34;: \u0026#34;full\u0026#34;, \u0026#34;aspectMode\u0026#34;: \u0026#34;cover\u0026#34;, \u0026#34;aspectRatio\u0026#34;: \u0026#34;1:1\u0026#34;, \u0026#34;gravity\u0026#34;: \u0026#34;center\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;vertical\u0026#34;, \u0026#34;contents\u0026#34;: [], \u0026#34;position\u0026#34;: \u0026#34;absolute\u0026#34;, \u0026#34;background\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;linearGradient\u0026#34;, \u0026#34;angle\u0026#34;: \u0026#34;0deg\u0026#34;, \u0026#34;endColor\u0026#34;: \u0026#34;#00000000\u0026#34;, \u0026#34;startColor\u0026#34;: \u0026#34;#00000099\u0026#34; }, \u0026#34;width\u0026#34;: \u0026#34;100%\u0026#34;, \u0026#34;height\u0026#34;: \u0026#34;40%\u0026#34;, \u0026#34;offsetBottom\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetStart\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetEnd\u0026#34;: \u0026#34;0px\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;horizontal\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;vertical\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;box\u0026#34;, \u0026#34;layout\u0026#34;: \u0026#34;horizontal\u0026#34;, \u0026#34;contents\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;APNG Demo\u0026#34;, \u0026#34;size\u0026#34;: \u0026#34;xl\u0026#34;, \u0026#34;color\u0026#34;: \u0026#34;#ffffff\u0026#34; } ] } ], \u0026#34;spacing\u0026#34;: \u0026#34;xs\u0026#34; } ], \u0026#34;position\u0026#34;: \u0026#34;absolute\u0026#34;, \u0026#34;offsetBottom\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetStart\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;offsetEnd\u0026#34;: \u0026#34;0px\u0026#34;, \u0026#34;paddingAll\u0026#34;: \u0026#34;20px\u0026#34; } ], \u0026#34;paddingAll\u0026#34;: \u0026#34;0px\u0026#34; } } \u0026ndash;","title":"Line Flex Message 傳送動畫訊息，讓訊息更生動"},{"content":"Rich Menu 圖文選單是什麼? 寫 Chatbot 引導很重要，在 Line 裡面有 RichMenu 可以用，讓使用者可以快速的點擊常用功能，要如何建立 RichMenu 呢? 快速不用寫程式的方式是透過 Official Account 設定\n你可選擇已經預設好的版型，並針對每個區塊設定要執行的動作\n但如果是透過 Message Api 的話，可以設定的動作就更彈性了，一張圖總共可以設定 20 個 Actions，可以做得事就更多了。要透過 Message Api 新增一個 Rich Menu，有以下步驟\n新增一個選單 上傳該選單的圖檔 我整理了Rich Menu相關的 postman collections 只要將環境變數 {{botAcceccToken}} 設為你 Channel 的 Access Token 可直接匯入使用： https://www.getpostman.com/collections/f090d101b7e08e411f12\n方法1: 透過 Postman 來上傳 Rich Menu 在上面的 postman collections，你可以使用 Create Rich Menu 來新增，成功的話會回傳 RichMenuID\n那這個 Post 的 JSON 怎麼產生呢？在 NET 5 你可以直接安裝我的 NetCoreLineBotSDK 透過物件的方式直接產生 Api 所需要的 JSON Request，丟到 postman 裡面\nvar sampleMenu = new RichmenuDetail(); sampleMenu.size = new CreateSize(1200, 810); sampleMenu.selected = true; // 是否為預設選單 sampleMenu.name = \u0026#34;sampleMenu\u0026#34;; // 選單名稱 sampleMenu.chatBarText = \u0026#34;More\u0026#34;; // 選單文字 sampleMenu.areas = new List\u0026lt;CreateArea\u0026gt;() // 點擊區域要執行的Action { new CreateArea(boundx:0,boundy:0,width:1200,height:810, _action: new MessageAction(\u0026#34;Hello Rich Menu!\u0026#34;)) }; var postJson = JsonConvert.SerializeObject(sampleMenu, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } }); 或者是這個步驟可以用 Bot Designer 工具，可以直接用匡列的方式設定好，並直接產生JSON\n接著透過 postman collections Upload Rich Menu Images 你可以於 URI 指定剛剛的 Rich Menu ID，及上傳圖片來完成綁定\n方法2: 透過 NetCoreLineBotSDK 上傳 在 SDK 裡面，你可以直接用下列語法完成RichMenu的上傳與圖片綁定\nvar sampleMenu = new RichmenuDetail(); sampleMenu.size = new CreateSize(1200, 810); sampleMenu.selected = true; // 是否為預設選單 sampleMenu.name = \u0026#34;sampleMenu\u0026#34;; // 選單名稱 sampleMenu.chatBarText = \u0026#34;More\u0026#34;; // 選單文字 sampleMenu.areas = new List\u0026lt;CreateArea\u0026gt;() // 點擊區域要執行的Action { new CreateArea(boundx:0,boundy:0,width:1200,height:810, _action: new MessageAction(\u0026#34;Hello Rich Menu!\u0026#34;)) }; await CreateRichMenuWithImageAsync(sampleMenu,\u0026#34;https://via.placeholder.com/1200x810/E71989.png/fff\u0026#34;); 或者是直接丟Bot Designer產出的JSON\nawait CreateRichMenuByJsonWithImageAsync(json, \u0026#34;https://via.placeholder.com/1200x810/E71989.png/fff\u0026#34;, alias: \u0026#34;richmenu-a\u0026#34;); Rich Menu 的切換 Line 在 6/21 的時候推出了 Swtich Rich Mnues 的功能，以前要達到這種需求，都要用 Link Rich Menu To User 這類的 Api 才能達成，在這次改版，新增一個Action 類型叫做 Rich menu switch action，但目前這個 Action 只能支援在Rich Menu觸發，所以透過 Message Types/Flex 裡面去指定這個 Action 是會出錯的，簡單來講就是，切換選單的動作只能在選單來觸發，那怎麼切換呢? 首先要先設定別名:\n方法1: 透過 Postman 設定 RichMenu Alias 透過 Create rich menu alias 這個 Api 指定RichMenuId的別名\n用 Get list of rich menu alias 驗證當前所有已設定的 alias\n當然，別名設定錯了，還是可以用 Update rich menu alias 來更新\n接著只要在建立 RichMenu 的時候，Action 指定為別名就會切換了，以下面為例，我建立了三個RichMenu點擊區域，別名分別是 a/b/c，點擊後就可以切換至相對應的選單，而點擊後也會觸發postback事件，可在這個欄位塞一些資料，做事件的處理，範例的的 JSON 如下\n{\u0026#34;name\u0026#34;:\u0026#34;sampleMenu-c\u0026#34;,\u0026#34;size\u0026#34;:{\u0026#34;width\u0026#34;:1200,\u0026#34;height\u0026#34;:810},\u0026#34;chatBarText\u0026#34;:\u0026#34;More\u0026#34;,\u0026#34;selected\u0026#34;:true,\u0026#34;areas\u0026#34;:[{\u0026#34;bounds\u0026#34;:{\u0026#34;x\u0026#34;:0,\u0026#34;y\u0026#34;:0,\u0026#34;width\u0026#34;:400,\u0026#34;height\u0026#34;:810},\u0026#34;action\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;richmenuswitch\u0026#34;,\u0026#34;richMenuAliasId\u0026#34;:\u0026#34;richmenu-a\u0026#34;,\u0026#34;data\u0026#34;:\u0026#34;richmenu-a-postback\u0026#34;}},{\u0026#34;bounds\u0026#34;:{\u0026#34;x\u0026#34;:400,\u0026#34;y\u0026#34;:0,\u0026#34;width\u0026#34;:400,\u0026#34;height\u0026#34;:810},\u0026#34;action\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;richmenuswitch\u0026#34;,\u0026#34;richMenuAliasId\u0026#34;:\u0026#34;richmenu-b\u0026#34;,\u0026#34;data\u0026#34;:\u0026#34;richmenu-b-postback\u0026#34;}},{\u0026#34;bounds\u0026#34;:{\u0026#34;x\u0026#34;:800,\u0026#34;y\u0026#34;:0,\u0026#34;width\u0026#34;:400,\u0026#34;height\u0026#34;:810},\u0026#34;action\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;richmenuswitch\u0026#34;,\u0026#34;richMenuAliasId\u0026#34;:\u0026#34;richmenu-c\u0026#34;,\u0026#34;data\u0026#34;:\u0026#34;richmenu-c-postback\u0026#34;}}]} 差別在於點擊的區域可以指定 RichMenuSwitch Action\n{ \u0026#34;type\u0026#34;:\u0026#34;richmenuswitch\u0026#34;, \u0026#34;richMenuAliasId\u0026#34;:\u0026#34;richmenu-a\u0026#34;, \u0026#34;data\u0026#34;:\u0026#34;richmenu-a-postback\u0026#34; } 2021.07.08 測試 Bot Designer 工具還不能選到 RichMenuSwitch Action，也許之後會支援\n方法2: 透過 NetCoreLineBotSDK 設定 RichMenu Alias 因應本次改版，在我開源的 SDK 也擴充以下選擇性參數了，你可以在建立 RichMenu 的時候直接給他別名，即可完成 RichMenu 新增.圖片.別名的設定\nvar sampleMenu = new RichmenuDetail(); sampleMenu.size = new CreateSize(1200, 810); sampleMenu.selected = true; sampleMenu.name = \u0026#34;sampleMenu-c\u0026#34;; sampleMenu.chatBarText = \u0026#34;More\u0026#34;; sampleMenu.areas = new List\u0026lt;CreateArea\u0026gt;() { new CreateArea(boundx:0,boundy:0,width:400,height:810, _action: new RichMenuSwitchAction(\u0026#34;richmenu-a\u0026#34;, \u0026#34;richmenu-a-postback\u0026#34;)), new CreateArea(boundx:400,boundy:0,width:400,height:810, _action: new RichMenuSwitchAction(\u0026#34;richmenu-b\u0026#34;, \u0026#34;richmenu-b-postback\u0026#34;)), new CreateArea(boundx:800,boundy:0,width:400,height:810, _action: new RichMenuSwitchAction(\u0026#34;richmenu-c\u0026#34;, \u0026#34;richmenu-c-postback\u0026#34;)), }; await CreateRichMenuWithImageAsync(sampleMenu,\u0026#34;https://via.placeholder.com/1200x810/E71989.png/fff\u0026#34;, alias: \u0026#34;richmenu-a\u0026#34;); 或者是直接丟RichMenu的Json\nawait CreateRichMenuByJsonWithImageAsync(json, \u0026#34;https://via.placeholder.com/1200x810/E71989.png/fff\u0026#34;, alias: \u0026#34;richmenu-a\u0026#34;); Demo 整體的切換速度還蠻順暢的!\nNetCoreLineBotSDK 是我開源的函式庫，目前正在努力茁壯中，如果有任何疑問都可以在 Github 討論\ngithub: https://github.com/kyleap/NetCoreLineBotSDK nuget: https://www.nuget.org/packages/NetCoreLineBotSDK/ 參考連結 https://developers.line.biz/en/reference/messaging-api/#rich-menu https://developers.line.biz/en/reference/messaging-api/#richmenu-switch-action ","permalink":"https://kyleap.github.io/2021-07-08-line-rich-menu-switch/","summary":"Rich Menu 圖文選單是什麼? 寫 Chatbot 引導很重要，在 Line 裡面有 RichMenu 可以用，讓使用者可以快速的點擊常用功能，要如何建立 RichMenu 呢? 快速不用寫程式的方式是透過 Official Account 設定\n你可選擇已經預設好的版型，並針對每個區塊設定要執行的動作\n但如果是透過 Message Api 的話，可以設定的動作就更彈性了，一張圖總共可以設定 20 個 Actions，可以做得事就更多了。要透過 Message Api 新增一個 Rich Menu，有以下步驟\n新增一個選單 上傳該選單的圖檔 我整理了Rich Menu相關的 postman collections 只要將環境變數 {{botAcceccToken}} 設為你 Channel 的 Access Token 可直接匯入使用： https://www.getpostman.com/collections/f090d101b7e08e411f12\n方法1: 透過 Postman 來上傳 Rich Menu 在上面的 postman collections，你可以使用 Create Rich Menu 來新增，成功的話會回傳 RichMenuID\n那這個 Post 的 JSON 怎麼產生呢？在 NET 5 你可以直接安裝我的 NetCoreLineBotSDK 透過物件的方式直接產生 Api 所需要的 JSON Request，丟到 postman 裡面","title":"Line RichMenu 設定與切換，以 NET 5 為例"},{"content":"前言 在非洲一分鐘就有60秒過去，所以在工作上只要節省5秒鐘，一年下來就是省下1825秒阿，比爾蓋茲說過懶惰的人歡迎到微軟，主要是能找到那種工作上會不斷優化自己工作的員工，所以要時時刻刻思考，如果你工作上有太多的重複的事項/太多固定的流程，下一步就要思考自動化。\n今天要介紹的是免費工具 beefText ，工作上可能會有一些長打的程式片段，譬如撰寫SQL查詢一些問題時，但這些片段也許可以用編輯器存起來，但WFH可能也會有一些常見要回的話，或者是一些email範本(面試感謝信…等等)，每次都要重新輸入人生就浪費了幾秒鐘了阿!!\n功能介紹 你可建立多個Group來做分類，Group底下可以建 Combo，譬如 e_ii (email_interview_invite) 就能觸發 Email 的邀請範本，寫 Email 時候再去微調就好了\n預設只要輸入偵測到關鍵字就會觸發取代你建立好的 Template，但也可調整空白鍵才去偵測\n指令越來越多時候，也可以用 picker 功能，我習慣適用 windows+alt+p\n如果換電腦的話，可以將設定檔從以下路徑備份出來，List會是一個JSON檔\nBeefText下載\nhttps://beeftext.org/\n","permalink":"https://kyleap.github.io/tools-beeftext/","summary":"前言 在非洲一分鐘就有60秒過去，所以在工作上只要節省5秒鐘，一年下來就是省下1825秒阿，比爾蓋茲說過懶惰的人歡迎到微軟，主要是能找到那種工作上會不斷優化自己工作的員工，所以要時時刻刻思考，如果你工作上有太多的重複的事項/太多固定的流程，下一步就要思考自動化。\n今天要介紹的是免費工具 beefText ，工作上可能會有一些長打的程式片段，譬如撰寫SQL查詢一些問題時，但這些片段也許可以用編輯器存起來，但WFH可能也會有一些常見要回的話，或者是一些email範本(面試感謝信…等等)，每次都要重新輸入人生就浪費了幾秒鐘了阿!!\n功能介紹 你可建立多個Group來做分類，Group底下可以建 Combo，譬如 e_ii (email_interview_invite) 就能觸發 Email 的邀請範本，寫 Email 時候再去微調就好了\n預設只要輸入偵測到關鍵字就會觸發取代你建立好的 Template，但也可調整空白鍵才去偵測\n指令越來越多時候，也可以用 picker 功能，我習慣適用 windows+alt+p\n如果換電腦的話，可以將設定檔從以下路徑備份出來，List會是一個JSON檔\nBeefText下載\nhttps://beeftext.org/","title":"用 BeefText 來減少常輸入的文字，增加工作效率"},{"content":"今年職涯有些改變，又回到教育科技新創打造 SaaS 平台，公司是全遠端作業，員工遍集全台各地，這也是為了未來海外員工協做能順暢，到職也滿三個月了，故寫篇文章記錄一下這幾個月對於遠端工作的三個面向\n信任感 - 上下層對齊目標 疫情大爆發很慶幸自己目前任職的公司是完全遠端工作，公司是特休無上限，自己安排好任務就好，故遠端團隊我認為一定要找 自我管理 的員工，能管好自己，才能管好團隊目標、公司目標。要擬造自我管理高團隊需要 良好的企業文化，但企業文化如何養成，我覺得最重要的是對於 目標認同感，要塑造員工能挑戰目標、認同目標的文化，故我們每季會有針對全體員工的腦力激盪會議，由公司擬定出上層的關鍵數據目標 (可能是營業額，客戶數等)，對於一個CEO而言，我覺得最重要的事要闡述這些數據背後的意義是什麼，可能是募資策略，合作夥伴，財務狀況…等等，往下後開始一起討論解決方案，產出一個可量化且合理的目標，通常我們會訂指標每日每週就報告這些項目，有指標後，如果有落差再來滾動式調整，這就是常見的OKR：數據/日期/具體作為 不可少。也許全體員工討論會很花時間，但比起無法對齊上層目標，做出可能浪費的作為，這些會議時間都是很值得投資的。\n儀式感 - 切換好工作與生活模式 遠端工作起床就上班，也沒有上班時間通勤的時間能醒腦，故我蠻建議可以提早起床做一些碎片化的閱讀，看看時事或一些文章或書後再來進入工作狀態，另外就是工作環境跟生活環境要區隔我覺得也蠻重要的，到了下班時間要切換下班模式還是有點生活，畢竟長期加班本來就是一個不健康的狀態；遠端工作一定不會比辦公室文化來得互動性高，能當面幹話還是最舒服的，team building也不能少，可能定期的中午線上聚餐.線上桌遊.讀書會都蠻重要的，或者是會議前是否能提前進會議室，跟大家聊聊講講幹話，關心近況，都能讓 WFH 不再那麼邊緣。\n溝通文化 - 應該要謹言慎行，更有條理 遠端工作不單只在家工作，團隊如有北中南，海外團隊，也是另外一種遠端溝通，比起辦公室文化，在溝通上比較不能看到對方的表情，彼此也怕插話就會句點，這都會造成會議的沒效率，之前我們內部在這塊也不太順利，後來決定還是要有個主持人，會議前先條列式闡述會議目標，預期會議結束時間，過程中有偏題就拉回來，少用開放性問答，直接點名問想法，要求大家一起共筆以免對於會議分心或沒貢獻，會議結束再Summay一次會議總結，共筆結果下一步直接列 Actions 貼到對話頻道\n遠端也很難猜到目前對方的情緒，應該要避免一些情緒性的發言，針對事情來討論，如果有任何建議應該時常主動回饋，而不是悶在心裡，畢竟無法面對面觀察情緒，溝通上還是有所折扣，文字表達內容時也盡量描述得清楚一點、去除不必要的語助詞，避免誤會。溝通文化也跟信任感習習相關，老闆是否有在適當的時機跳下來關心員工是否需要更多資源，員工在離目標有落後時，是否能提供自己接下來停損點跟Plan B，危機處理時是否有階段性的回報，總之，多多換位思考，期待老闆給予信任的同時，得思考自己是否也有給老闆足夠的信任感。\n","permalink":"https://kyleap.github.io/wfh-mindset/","summary":"今年職涯有些改變，又回到教育科技新創打造 SaaS 平台，公司是全遠端作業，員工遍集全台各地，這也是為了未來海外員工協做能順暢，到職也滿三個月了，故寫篇文章記錄一下這幾個月對於遠端工作的三個面向\n信任感 - 上下層對齊目標 疫情大爆發很慶幸自己目前任職的公司是完全遠端工作，公司是特休無上限，自己安排好任務就好，故遠端團隊我認為一定要找 自我管理 的員工，能管好自己，才能管好團隊目標、公司目標。要擬造自我管理高團隊需要 良好的企業文化，但企業文化如何養成，我覺得最重要的是對於 目標認同感，要塑造員工能挑戰目標、認同目標的文化，故我們每季會有針對全體員工的腦力激盪會議，由公司擬定出上層的關鍵數據目標 (可能是營業額，客戶數等)，對於一個CEO而言，我覺得最重要的事要闡述這些數據背後的意義是什麼，可能是募資策略，合作夥伴，財務狀況…等等，往下後開始一起討論解決方案，產出一個可量化且合理的目標，通常我們會訂指標每日每週就報告這些項目，有指標後，如果有落差再來滾動式調整，這就是常見的OKR：數據/日期/具體作為 不可少。也許全體員工討論會很花時間，但比起無法對齊上層目標，做出可能浪費的作為，這些會議時間都是很值得投資的。\n儀式感 - 切換好工作與生活模式 遠端工作起床就上班，也沒有上班時間通勤的時間能醒腦，故我蠻建議可以提早起床做一些碎片化的閱讀，看看時事或一些文章或書後再來進入工作狀態，另外就是工作環境跟生活環境要區隔我覺得也蠻重要的，到了下班時間要切換下班模式還是有點生活，畢竟長期加班本來就是一個不健康的狀態；遠端工作一定不會比辦公室文化來得互動性高，能當面幹話還是最舒服的，team building也不能少，可能定期的中午線上聚餐.線上桌遊.讀書會都蠻重要的，或者是會議前是否能提前進會議室，跟大家聊聊講講幹話，關心近況，都能讓 WFH 不再那麼邊緣。\n溝通文化 - 應該要謹言慎行，更有條理 遠端工作不單只在家工作，團隊如有北中南，海外團隊，也是另外一種遠端溝通，比起辦公室文化，在溝通上比較不能看到對方的表情，彼此也怕插話就會句點，這都會造成會議的沒效率，之前我們內部在這塊也不太順利，後來決定還是要有個主持人，會議前先條列式闡述會議目標，預期會議結束時間，過程中有偏題就拉回來，少用開放性問答，直接點名問想法，要求大家一起共筆以免對於會議分心或沒貢獻，會議結束再Summay一次會議總結，共筆結果下一步直接列 Actions 貼到對話頻道\n遠端也很難猜到目前對方的情緒，應該要避免一些情緒性的發言，針對事情來討論，如果有任何建議應該時常主動回饋，而不是悶在心裡，畢竟無法面對面觀察情緒，溝通上還是有所折扣，文字表達內容時也盡量描述得清楚一點、去除不必要的語助詞，避免誤會。溝通文化也跟信任感習習相關，老闆是否有在適當的時機跳下來關心員工是否需要更多資源，員工在離目標有落後時，是否能提供自己接下來停損點跟Plan B，危機處理時是否有階段性的回報，總之，多多換位思考，期待老闆給予信任的同時，得思考自己是否也有給老闆足夠的信任感。","title":"聊遠端工作三個面向：信任感、儀式感、溝通文化"},{"content":"之前也一個需求是需要將一個Blob移轉至另一個另一個訂閱，但不太可能一個一個搬或重新上傳，透過 AzCopy 就可以一鍵完成這件事，且搬移的效能也蠻快的\nAzureCopy 可以在這邊下載 https://docs.microsoft.com/zh-tw/azure/storage/common/storage-use-azcopy-v10\nazcopy copy \u0026lt;source\u0026gt; \u0026lt;destination\u0026gt; --recursive --overwrite false source: https://.blob.core.windows.net/ destination: 可以在 Azure Portal 取得，需要產生一個可以存取的 Access Signature recursive: 是否要遞迴去抓更多資料夾底下的檔案，有時候檔案階層會是 files/aaa/file.txt overwrite: false 表示檔案已存在就忽略 在Azure Portal 取得 Access Signature\n\u0026ndash; References https://docs.microsoft.com/en-us/azure/storage/common/storage-ref-azcopy-copy?toc=/azure/storage/blobs/toc.json\n","permalink":"https://kyleap.github.io/azure-copy-to-blob-container/","summary":"之前也一個需求是需要將一個Blob移轉至另一個另一個訂閱，但不太可能一個一個搬或重新上傳，透過 AzCopy 就可以一鍵完成這件事，且搬移的效能也蠻快的\nAzureCopy 可以在這邊下載 https://docs.microsoft.com/zh-tw/azure/storage/common/storage-use-azcopy-v10\nazcopy copy \u0026lt;source\u0026gt; \u0026lt;destination\u0026gt; --recursive --overwrite false source: https://.blob.core.windows.net/ destination: 可以在 Azure Portal 取得，需要產生一個可以存取的 Access Signature recursive: 是否要遞迴去抓更多資料夾底下的檔案，有時候檔案階層會是 files/aaa/file.txt overwrite: false 表示檔案已存在就忽略 在Azure Portal 取得 Access Signature\n\u0026ndash; References https://docs.microsoft.com/en-us/azure/storage/common/storage-ref-azcopy-copy?toc=/azure/storage/blobs/toc.json","title":"利用 AzCopy 來將 Azure Blob Container 所有檔案移轉至另一個 Blob Container"},{"content":"寫 Line Bot 也應該理解 Line URL Scheme 怎麼用，它是透過一串特殊的 URL 來啟動 Line 的功能，可以設計於 BOt 的 URL Action，或者是直接傳至對話框，讓使用者點擊此篇文章就能引導到開啟一些功能，此篇文章邊玩並做個紀錄，也為自己當個小抄\n開啟相機或相片 https://line.me/R/nv/camera (打開相機) https://line.me/R/nv/cameraRoll/single (打開相簿，傳送一張) https://line.me/R/nv/cameraRoll/multi (打開相簿，傳送多張\n開啟定位 https://line.me/R/nv/location/\t(打開地圖)\n分享官方帳號 https://line.me/R/ti/p/ (直接跳至此Bot的對話視窗) https://line.me/R/nv/recommendOA/ (傳送此Bot給好友) https://line.me/R/home/public/main?id= (Bot主頁) https://line.me/R/home/public/profile?id= (Bot個人檔案頁) https://line.me/R/home/public/post?id=svgvs\u0026amp;postId={postId} (移至貼文)\nChatbox https://line.me/R/share?text=Hello (分享給好友訊息) https://line.me/R/oaMessage/@102svgvs/?Hello學院 (跳至Bot對話視窗，並帶入文字到Chatbox)\n個人資訊相關 https://line.me/R/nv/profile (個人檔案頁) https://line.me/R/nv/profileSetId (移置個人ID設定頁) https://line.me/R/nv/chat (移置對話列表) https://line.me/R/nv/timeline (移至動態列表) https://line.me/R/nv/wallet\t(移至錢包) https://line.me/R/nv/addFriends (加入好友頁) https://line.me/R/nv/officialAccounts (探索官方帳號) https://line.me/R/nv/settings (設定頁) https://line.me/R/nv/settings/account (個人帳號設定頁) https://line.me/R/nv/connectedApps (管理已連結的ChatBot) https://line.me/R/nv/connectedDevices (管理已連結的設備) https://line.me/R/nv/settings/privacy (隱私設定) https://line.me/R/nv/things/deviceLink (管理Line Thing已連結裝置) https://line.me/R/nv/settings/sticker (貼圖設定) https://line.me/R/nv/stickerShop/mySticker (我的貼圖) https://line.me/R/nv/settings/themeSettingsMenu 無效連結 https://line.me/R/nv/settings/theme (主題設定) https://line.me/R/nv/themeSettings\t(主題一覽) https://line.me/R/nv/notificationServiceDetail (連動中的應用程式) https://line.me/R/nv/settings/chatSettings\t(聊天設定) https://line.me/R/nv/suggestSettings (顯示建議貼圖) https://line.me/R/nv/settings/callSettings (通話設定) https://line.me/R/nv/settings/addressBookSync (好友設定) https://line.me/R/nv/settings/timelineSettings (貼文串設定)\n打開貼圖小舖 https://line.me/R/shop/sticker/detail/{package_id} (打開貼圖小舖連結) https://line.me/R/shop/category/{category_id} (官方貼圖連結) https://line.me/R/shop/sticker/author/{author_id} 上面三個的ID可以從 https://store.line.me/ 的 URL 取得 ID\nhttps://line.me/R/nv/stickerShop (貼圖小舖) https://line.me/R/shop/sticker/hot (熱門貼圖) https://line.me/R/shop/sticker/new (最新貼圖) https://line.me/R/shop/sticker/event (貼圖活動) https://line.me/R/shop/sticker/category (貼圖分類)\n打開主題 https://line.me/R/shop/theme/detail?id={product_id}\nLine Out https://line.me/R/calls (通話紀錄) https://line.me/R/call/\u0026lt;國碼\u0026gt;/\u0026lt;號碼\u0026gt;(Line Out, 台灣未支援) https://line.me/R/call/dialpad\t(Line Out, 台灣未支援) https://line.me/R/call/settings\t(Line Out, 台灣未支援) https://line.me/R/call/contacts (Line Out, 台灣未支援) https://line.me/R/call/redeem (Line Out, 台灣未支援)\n","permalink":"https://kyleap.github.io/play-with-line-url-scheme/","summary":"寫 Line Bot 也應該理解 Line URL Scheme 怎麼用，它是透過一串特殊的 URL 來啟動 Line 的功能，可以設計於 BOt 的 URL Action，或者是直接傳至對話框，讓使用者點擊此篇文章就能引導到開啟一些功能，此篇文章邊玩並做個紀錄，也為自己當個小抄\n開啟相機或相片 https://line.me/R/nv/camera (打開相機) https://line.me/R/nv/cameraRoll/single (打開相簿，傳送一張) https://line.me/R/nv/cameraRoll/multi (打開相簿，傳送多張\n開啟定位 https://line.me/R/nv/location/\t(打開地圖)\n分享官方帳號 https://line.me/R/ti/p/ (直接跳至此Bot的對話視窗) https://line.me/R/nv/recommendOA/ (傳送此Bot給好友) https://line.me/R/home/public/main?id= (Bot主頁) https://line.me/R/home/public/profile?id= (Bot個人檔案頁) https://line.me/R/home/public/post?id=svgvs\u0026amp;postId={postId} (移至貼文)\nChatbox https://line.me/R/share?text=Hello (分享給好友訊息) https://line.me/R/oaMessage/@102svgvs/?Hello學院 (跳至Bot對話視窗，並帶入文字到Chatbox)\n個人資訊相關 https://line.me/R/nv/profile (個人檔案頁) https://line.me/R/nv/profileSetId (移置個人ID設定頁) https://line.me/R/nv/chat (移置對話列表) https://line.me/R/nv/timeline (移至動態列表) https://line.me/R/nv/wallet\t(移至錢包) https://line.me/R/nv/addFriends (加入好友頁) https://line.me/R/nv/officialAccounts (探索官方帳號) https://line.me/R/nv/settings (設定頁) https://line.me/R/nv/settings/account (個人帳號設定頁) https://line.me/R/nv/connectedApps (管理已連結的ChatBot) https://line.me/R/nv/connectedDevices (管理已連結的設備) https://line.me/R/nv/settings/privacy (隱私設定) https://line.me/R/nv/things/deviceLink (管理Line Thing已連結裝置) https://line.","title":"不用寫程式，Line URL Scheme 應用整理"},{"content":" 啃書代表是在閱讀時，針對自己有幫助的片段，做反思與紀錄，如想閱讀完整知識建議還是購買完整書籍 :P\nalways center.\nChatbot 的機會與未來 近年來 Chatbot 技術興起，使用者下載App意願降低，加上各大平台也開始推出聊天機器人Api串接，開始越來越多應用出現，交互介面已經從傳統的App點擊互動，轉變為對話互動，也因為 Chatbot 依附在各大平台上 e.g Line/Facebook…等，推廣上也較容易，再加上自然語言分析技術日漸成熟，越來越多的AI服務可以應用，可以想像成百上千的 App 會逐漸轉為 Chatbot。\n在設計 Chatbot 時，要時刻記得對話交互是手段而非目的，要通過最簡單的互動方式讓使用者獲取服務，而最簡單的交付方式不一定要用對話，這時就能透過圖形化介面與人工接入流程來替使用者完成目標，且應該時時刻刻去觀察使用者的心理變化，觀察上下文，是否造成迷路，觀察對話節奏是否合理，逐步的調教與優化流程，讓重複性的任務交給Bot引導來完成。本書提出軟體開發的生命週期，提供給產品設計師在規劃 Chatbot 產品時一個指南。\n思考一下產品定位，定義產品邊界 Chatobt 的應用情境大概可以分為開放領域 (open-domain) 或封閉領域 (closed-domain) 兩大類，開放式大概沒有限定主題，像Google助理或者 Siri 這種問什麼都自由對話，這實踐起來也較困難，而封閉式通常會限縮在一定的場景下，也就是 Chatbot 的輸入輸出是有限的，譬如訂位為在線客服/教育/個人助理，跟這幾個任務無關的事項，就引導客戶能目前能完成什麼事情，僅針對該領域，結合商業.產品.運營，針對領域場景優化交互內容。\n以應用市場角度來看，可以將 Chatbot 定為四類:\n陪伴型: 自由回答，譬如兒童陪伴類的Bot 商務型: 協助企業完成商業任務Bot，譬如 Github 推出 Hubot 來完成跟 DevOps 有關的相關任務 資訊型: 讓使用者獲取新聞類的訊息，譬如疾管家 指令型: 透過指令來讓Bot完成一些任務，譬如串接智能家電打開電燈/電視 在設計 Chatbot 前，先思考一下產品定位，把商務型的bot對話設計成像陪伴型，只會讓人感覺到這個Bot很不智能，什麼都不會，造成極差的使用者體驗\n流程設計應專注在完成商業目標及幫助使用者完成任務 影響網站成功的應用程式都有一個共同點，卓越的使用者體驗，Chatbot 可透過以下因素來評估\n如何用最少的步驟解決使使用者問題? 能比其他方式更好解決使用者問題? 能否在使用者習慣的平台上運行 用戶獲取 Chatbot 的途徑是否方便 用戶能無門檻的使用 Chatbot 在進開發前，可透過以下的步驟來收斂需求：先有大量的對話腳本，再來繪製流程圖，Chatbot 常見的技術名詞有意圖 (intent) 及 實體 (entity)，繪製流程圖避免將意圖跟對話混在一起思考，因對話存在多種可能，故不能只考慮核心場景，故可從對話腳本裡面確定場景與優先及拆出意圖跟實體，e.g 意圖可能從優先級排序會是\n訂火車票 (p1) 常見問題 (p2) 天氣查詢 (p3) 簡單問候語答謝 (p4) 以訂火車票為例，訂票可拆出關鍵訊息：出發時間.到達時間.火車類型.車位.座位\n進而拆出實體 (Entity) e.g 火車類型可拆出的 entity為:高鐵/台鐵…，出發時間可拆出上午/下午/晚上 (需另外設計資料結構定義何謂上午時段)\n這些收斂可以用心智圖很好的拉出與歸納:\n整理完意圖與訊息後，可繪製單意圖的流程圖，譬如以下 Chatbot 的意圖流程:\n蒐羅國內外案例 !48個 FB x LINE Chatbot 玩法大公開\nChatbot 有時會牽扯第三方服務串接，這時用泳道圖就能很好的表達:\n抽取对话流程\n上線後觀測指標 Chatbot 上線後的指標監控，由於閒聊型Bot過於複雜 (可以思考成是由無數個任務型的Bot的集成)，故不再討論範圍，對於語意了可以分析意圖/實體的能力，也可將錯字容錯率/同義詞轉化加入監控的範圍，對話管理的測評指標，可透過任務完成率、用戶滿意度、對話輪次完成度(任務型應該要在最低的步驟引導完成任務)…來定義一些指標。\n閒聊型的 Chatbot 可透過準確率 (正確回答/問題總數) ；召回率(能回答的問題數/問題總數)；未轉為人工客服的問題解決率(成功解決的問題數)，來做指標衡量\n","permalink":"https://kyleap.github.io/chatbot-zero-to-one/","summary":"啃書代表是在閱讀時，針對自己有幫助的片段，做反思與紀錄，如想閱讀完整知識建議還是購買完整書籍 :P\nalways center.\nChatbot 的機會與未來 近年來 Chatbot 技術興起，使用者下載App意願降低，加上各大平台也開始推出聊天機器人Api串接，開始越來越多應用出現，交互介面已經從傳統的App點擊互動，轉變為對話互動，也因為 Chatbot 依附在各大平台上 e.g Line/Facebook…等，推廣上也較容易，再加上自然語言分析技術日漸成熟，越來越多的AI服務可以應用，可以想像成百上千的 App 會逐漸轉為 Chatbot。\n在設計 Chatbot 時，要時刻記得對話交互是手段而非目的，要通過最簡單的互動方式讓使用者獲取服務，而最簡單的交付方式不一定要用對話，這時就能透過圖形化介面與人工接入流程來替使用者完成目標，且應該時時刻刻去觀察使用者的心理變化，觀察上下文，是否造成迷路，觀察對話節奏是否合理，逐步的調教與優化流程，讓重複性的任務交給Bot引導來完成。本書提出軟體開發的生命週期，提供給產品設計師在規劃 Chatbot 產品時一個指南。\n思考一下產品定位，定義產品邊界 Chatobt 的應用情境大概可以分為開放領域 (open-domain) 或封閉領域 (closed-domain) 兩大類，開放式大概沒有限定主題，像Google助理或者 Siri 這種問什麼都自由對話，這實踐起來也較困難，而封閉式通常會限縮在一定的場景下，也就是 Chatbot 的輸入輸出是有限的，譬如訂位為在線客服/教育/個人助理，跟這幾個任務無關的事項，就引導客戶能目前能完成什麼事情，僅針對該領域，結合商業.產品.運營，針對領域場景優化交互內容。\n以應用市場角度來看，可以將 Chatbot 定為四類:\n陪伴型: 自由回答，譬如兒童陪伴類的Bot 商務型: 協助企業完成商業任務Bot，譬如 Github 推出 Hubot 來完成跟 DevOps 有關的相關任務 資訊型: 讓使用者獲取新聞類的訊息，譬如疾管家 指令型: 透過指令來讓Bot完成一些任務，譬如串接智能家電打開電燈/電視 在設計 Chatbot 前，先思考一下產品定位，把商務型的bot對話設計成像陪伴型，只會讓人感覺到這個Bot很不智能，什麼都不會，造成極差的使用者體驗\n流程設計應專注在完成商業目標及幫助使用者完成任務 影響網站成功的應用程式都有一個共同點，卓越的使用者體驗，Chatbot 可透過以下因素來評估\n如何用最少的步驟解決使使用者問題? 能比其他方式更好解決使用者問題? 能否在使用者習慣的平台上運行 用戶獲取 Chatbot 的途徑是否方便 用戶能無門檻的使用 Chatbot 在進開發前，可透過以下的步驟來收斂需求：先有大量的對話腳本，再來繪製流程圖，Chatbot 常見的技術名詞有意圖 (intent) 及 實體 (entity)，繪製流程圖避免將意圖跟對話混在一起思考，因對話存在多種可能，故不能只考慮核心場景，故可從對話腳本裡面確定場景與優先及拆出意圖跟實體，e.g 意圖可能從優先級排序會是","title":"(啃書) Chatbot從0到1"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day30)\n終於走到這一天了，每次都覺得鐵人賽過程都生不如死，但寫完30天候總有一種成就感，技術跟文章不難，難的是30天養成一個固定的習慣阿!\n回到第一天文章所講的：所用到的技術預計採用 .NET Core 3.1、Html/CSS/JavaScript、Line API 、Azure的一些服務…等等 (但寫這篇文章時完全還沒實作XD…只能說待訂)\n這次寫完30天也收穫良多，主要也是挑一些之前比較少碰的東西順便玩玩看\n把 Line 的 Api 文件又完整K了一遍，並一個一個寫成 .NET Core 的版本， 之前比較沒碰過的是 Line Pay/Line Beacon 寫得過程中看到 vue 海賊王版本推出 (vue 3.0)，起了專案實踐了一些功能，來看看它的架構 用了 Razor Page，讓我回到 ASP.NET WebForm 的復古感 玩了一些 Azure 服務，Azure Storage、QnA Maker，其中 Azure SignalR Service 是之前比較沒玩過的 把之前自己婚禮用的 chatbot 改一版，順便要給接下來結婚的朋友用 XD 真實的把所做的Side Project給用上 邊看文件邊寫SDK出來，並開源出來 本篇就來講講最後一哩路，怎麼發佈到 NuGet 上吧！\n我將這次系列邊走邊看得的程式稍作整理，建立一個 .NET Core 的函示庫專案\n在 VS 按右鍵套件\n或者可以使用CLI : dotnet pack\n編輯套件的資訊\nrelease 編譯後，可以看到 bin\\release 有 nupkg 檔案\n接著到 nuget.org 註冊一個帳號，並新增 Api Key\n點選 Copy ，\n在 bin\\release 執行\ndotnet nuget push NetCoreLineBotSDK.0.1.3.nupkg --api-key \u0026lt;Nuget API Key\u0026gt; --source [https://api.nuget.org/v3/index.json](https://api.nuget.org/v3/index.json) 等一段時間，就能看到 NuGet 的連結啦~ https://www.nuget.org/packages/NetCoreLineBotSDK/\n開始第一個 Line Chatbot 這篇再來快速做一次一個Line Chatbot Hello World\n安裝套件\nInstall-Package NetCoreLineBotSDK Startup.cs 新增 AddLineBotSDK\npublic void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddLineBotSDK(Configuration); } appsettings.json 增加token，這些怎麼拿? 看這篇\n\u0026#34;LineSetting\u0026#34;: { \u0026#34;ChannelSecret\u0026#34;: \u0026#34;\u0026lt;Your Line Channel Secret\u0026gt;\u0026#34;, \u0026#34;ChannelAccessToken\u0026#34;: \u0026#34;\u0026lt;Your Line Channel Access Token\u0026gt;\u0026#34; } 新增一個 Web Api\n繼承 LineBotApp，針對不同事件 override 處理\n照這篇文章用 ngrok Run 起來，URL設定於 Line Develop 後台，這樣就完成啦\n本次 Nuget 連結：https://www.nuget.org/packages/NetCoreLineBotSDK/ Github 連結：https://github.com/kyleap/NetCoreLineBotSDK\n版本先定為 v0.1.0 QQ 因為還不夠成熟，一方面還沒有實作完所有功能，基本的例外處理與參數的驗證都還沒加上，但鐵人賽這只是的開始，主要也是希望用自己的SDK來應對之後的開發，再來也會繼續撥空往 v1.0.0 推進~\n本系列文章索引在第一篇：用.NET Core與Line Bot製作婚禮機器人\n懶人包，本次學到了什麼? 如何做自己的元件，發佈到 NuGet https://docs.microsoft.com/zh-tw/nuget/nuget-org/publish-a-package ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-30/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day30)\n終於走到這一天了，每次都覺得鐵人賽過程都生不如死，但寫完30天候總有一種成就感，技術跟文章不難，難的是30天養成一個固定的習慣阿!\n回到第一天文章所講的：所用到的技術預計採用 .NET Core 3.1、Html/CSS/JavaScript、Line API 、Azure的一些服務…等等 (但寫這篇文章時完全還沒實作XD…只能說待訂)\n這次寫完30天也收穫良多，主要也是挑一些之前比較少碰的東西順便玩玩看\n把 Line 的 Api 文件又完整K了一遍，並一個一個寫成 .NET Core 的版本， 之前比較沒碰過的是 Line Pay/Line Beacon 寫得過程中看到 vue 海賊王版本推出 (vue 3.0)，起了專案實踐了一些功能，來看看它的架構 用了 Razor Page，讓我回到 ASP.NET WebForm 的復古感 玩了一些 Azure 服務，Azure Storage、QnA Maker，其中 Azure SignalR Service 是之前比較沒玩過的 把之前自己婚禮用的 chatbot 改一版，順便要給接下來結婚的朋友用 XD 真實的把所做的Side Project給用上 邊看文件邊寫SDK出來，並開源出來 本篇就來講講最後一哩路，怎麼發佈到 NuGet 上吧！\n我將這次系列邊走邊看得的程式稍作整理，建立一個 .NET Core 的函示庫專案\n在 VS 按右鍵套件\n或者可以使用CLI : dotnet pack\n編輯套件的資訊\nrelease 編譯後，可以看到 bin\\release 有 nupkg 檔案","title":"鐵人賽完賽整理\u0026開源 (30)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day29)\n在上一篇我們用 Console App 發送訊息到 Api 的 Server Hub，並呈現在HTML上，今天這篇我們要來結合 chatbot，其實就是在chatbot當成SignalR的client而已，實際效果如下(本篇不會介紹CSS部分)：\n新增一個 MessageBoard class 來處理留言板發送，這部分可以看這篇\n而透過 Line 的 User Id ，我們能取得使用者的大頭照.姓名…等等資訊\npublic async Task ReplyAsync(string replyToken) { var profile = await lineProfileUtility.GetUserProfile(userId); var hubClient = new ClientSignalR(); await hubClient.Initialize(\u0026#34;http://localhost:12733/message\u0026#34;); await hubClient.SendHubMessage(\u0026#34;SendMessage\u0026#34;, profile.pictureUrl, profile.displayName, message); await lineMessageUtility.ReplyMessageAsync(replyToken, \u0026#34;謝謝您，我們已收到留言\u0026#34;); } GetUserProfile\npublic async Task\u0026lt;UserProfile\u0026gt; GetUserProfile(string userId) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}/{userId}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var response = await httpClient.SendAsync(request); if(response.StatusCode != HttpStatusCode.OK) { // 這邊未來應該要 log 起來 throw new Exception(\u0026#34;get_profile_error\u0026#34;); } var result = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject\u0026lt;UserProfile\u0026gt;(result); } } } HTML 部分\n這邊是用JavaScript去塞DOM，如果用主流框架Angular/Vue/React寫起來應該更簡潔~\ndocument.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function () { // 建立連線 var connection = new signalR.HubConnectionBuilder() .withUrl(\u0026#39;/message\u0026#39;) //因為是同個網域，要視情況換成完成Server Hub的URL .build(); // 開始連線 connection.start() .then(function () { console.log(\u0026#39;連線成功...\u0026#39;); }) .catch(function (error) { console.error(error.message); }); bindConnectionMessage(connection); // 呈現資料到Html function createMessageEntry(imgUrl, encodedName, encodedMsg) { var entry = document.createElement(\u0026#39;li\u0026#39;); var html = \u0026#39;\u0026lt;a href=\u0026#34;#\u0026#34;\u0026gt;\u0026lt;div\u0026gt;\u0026lt;div id=\u0026#34;divcss5\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;\u0026#39; + imgUrl +\u0026#39;\u0026#34; /\u0026gt;\u0026#39;; html += \u0026#39;\u0026lt;span class=\u0026#34;displayName\u0026#34;\u0026gt;\u0026#39; + encodedName+\u0026#39;\u0026lt;/span\u0026gt;\u0026#39;; html += \u0026#39;\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;p\u0026gt;\u0026#39; + encodedMsg+\u0026#39;\u0026lt;/p\u0026gt;\u0026lt;/a\u0026gt;\u0026#39;; entry.innerHTML = html; console.log(html); return entry; } // 監聽接收訊息事件 function bindConnectionMessage(connection) { connection.on(\u0026#39;CallBackMessage\u0026#39;, function (imgUrl, displayName, message) { if (!message) return; var encodedName = displayName; var encodedMsg = message.replace(/\u0026amp;/g, \u0026#34;\u0026amp;\u0026#34;).replace(/\u0026lt;/g, \u0026#34;\u0026lt;\u0026#34;).replace(/\u0026gt;/g, \u0026#34;\u0026gt;\u0026#34;); var messageEntry = createMessageEntry(imgUrl, encodedName, encodedMsg); var messageBox = document.getElementById(\u0026#39;messages\u0026#39;); messageBox.appendChild(messageEntry); messageBox.scrollTop = messageBox.scrollHeight; }); } }); 這樣就能完成即時用Line傳送的留言板了，但這個只是Demo，真正的話還是要審核一下訊息，不然把怪怪的訊息發送到大螢幕就不好啦。Azure SignalR Service 已經大大降低做即時通訊的開發門檻，透過這種服務就能去發想更多的應用！\n懶人包，本次學到了什麼? 在 C# Clinet 端發送訊息至 SignalR Hub https://daniel-krzyczkowski.github.io/Real-time-data-with-Microsoft-Azure-SignalR-Service/ ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-29/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day29)\n在上一篇我們用 Console App 發送訊息到 Api 的 Server Hub，並呈現在HTML上，今天這篇我們要來結合 chatbot，其實就是在chatbot當成SignalR的client而已，實際效果如下(本篇不會介紹CSS部分)：\n新增一個 MessageBoard class 來處理留言板發送，這部分可以看這篇\n而透過 Line 的 User Id ，我們能取得使用者的大頭照.姓名…等等資訊\npublic async Task ReplyAsync(string replyToken) { var profile = await lineProfileUtility.GetUserProfile(userId); var hubClient = new ClientSignalR(); await hubClient.Initialize(\u0026#34;http://localhost:12733/message\u0026#34;); await hubClient.SendHubMessage(\u0026#34;SendMessage\u0026#34;, profile.pictureUrl, profile.displayName, message); await lineMessageUtility.ReplyMessageAsync(replyToken, \u0026#34;謝謝您，我們已收到留言\u0026#34;); } GetUserProfile\npublic async Task\u0026lt;UserProfile\u0026gt; GetUserProfile(string userId) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}/{userId}\u0026#34;)) { request.","title":"製作婚禮現場即時留言版- Azure SignalR Service II (29)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day28)\nSignalR是實現即時通訊的框架，如下圖，在Server Hub宣告要給client端呼叫的方法，能傳送即時訊息給已連線的使用者\nhttps://www.pubnub.com/learn/glossary/what-is-signalr/\n而透過Azure SignalR 服務，大大簡化使用門檻，很簡單就能建立一個服務，且享有雲端帶來的高拓展性，這篇文章會step by step 實作以下內容\n建立 Azure SignalR 服務 建立 SignalR Server Hub 建立 HTML Client 即時呈現訊息 建立 .NET Console App 來發送訊息 建立 Azure SignalR 服務 登入Azure，進入https://portal.azure.com/#create/Microsoft.SignalRGalleryPackage 新增服務\n建完後，到key的選單，我們等等需要這個 Connection String\n建立 SignalR Server Hub 打開 Web Api 專案的 .csproj，加入以下程式碼\n\u0026lt;ItemGroup\u0026gt; \u0026lt;DotNetCliToolReference Include=\u0026#34;Microsoft.VisualStudio.Web.CodeGeneration.Tools\u0026#34; Version=\u0026#34;2.0.0\u0026#34; /\u0026gt; \u0026lt;DotNetCliToolReference Include=\u0026#34;Microsoft.Extensions.SecretManager.Tools\u0026#34; Version=\u0026#34;2.0.0\u0026#34; /\u0026gt; \u0026lt;/ItemGroup\u0026gt; 用 Nuget 安裝 Microsoft.Azure.SignalR\n在 Web Api 專案案右鍵，建立管理使用者密碼，\n建立 Azure:SignalR:ConnectionString ，把剛剛的連線字串貼上\nWeb Api 專案 Startup.cs 的 ConfigureServices 增加 AddSignalR\nservices.AddSignalR().AddAzureSignalR(); Configure 增加 Hub 的實作 app.UseEndpoints(endpoints =\u0026gt; { endpoints.MapControllers(); endpoints.MapRazorPages(); endpoints.MapHub\u0026lt;WeddingMessage\u0026gt;(\u0026#34;/message\u0026#34;); }); WeddingMessage.cs\n之後可以依照自己的需求在此實作方法\npublic class WeddingMessage : Hub { public void SendMessage(string name, string message) { Clients.All.SendAsync(\u0026#34;SendMessage\u0026#34;, name, message); } } 簡單幾個步驟就完成 Server Side 的 Hub\n建立 HTML Client 即時呈現訊息 本篇繼續使用 Razor Page，但純HTML也是可以的，因為我們會載入SignalR的JavaScript SDK\n\u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;https://cdn.jsdelivr.net/npm/@@aspnet/signalr@1.1.0/dist/browser/signalr.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 在JavaScript也蠻容易實作發送訊息及接收訊息\n\u0026lt;script\u0026gt; document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function () { // 建立連線 var connection = new signalR.HubConnectionBuilder() .withUrl(\u0026#39;/message\u0026#39;) //因為是同個網域，要視情況換成完成Server Hub的URL .build(); // 開始連線 connection.start() .then(function () { connection.send(\u0026#39;SendMessage\u0026#39;, \u0026#39;SYSTEM\u0026#39;, \u0026#39;連線成功...\u0026#39;); }) .catch(function (error) { console.error(error.message); }); bindConnectionMessage(connection); // 呈現資料到Html function createMessageEntry(encodedName, encodedMsg) { var entry = document.createElement(\u0026#39;li\u0026#39;); entry.innerHTML = encodedName + \u0026#34;:\u0026#34; + encodedMsg; return entry; } // 監聽接收訊息事件 function bindConnectionMessage(connection) { connection.on(\u0026#39;CallBackMessage\u0026#39;, function (name, message) { if (!message) return; var encodedName = name; var encodedMsg = message.replace(/\u0026amp;/g, \u0026#34;\u0026amp;\u0026#34;).replace(/\u0026lt;/g, \u0026#34;\u0026lt;\u0026#34;).replace(/\u0026gt;/g, \u0026#34;\u0026gt;\u0026#34;); var messageEntry = createMessageEntry(encodedName, encodedMsg); var messageBox = document.getElementById(\u0026#39;messages\u0026#39;); messageBox.appendChild(messageEntry); messageBox.scrollTop = messageBox.scrollHeight; }); } }); \u0026lt;/script\u0026gt; Html 部分\n\u0026lt;h2\u0026gt;Kyle\u0026#39;s Wedding 留言板\u0026lt;/h2\u0026gt; \u0026lt;ul id=\u0026#34;messages\u0026#34;\u0026gt; \u0026lt;/ul\u0026gt; 建立 .NET Console App 來發送訊息 再來我們建立個 Console App 來發送看看\n安裝 Microsoft.AspNetCore.SignalR.Client\n建立一個 ClientSignalR 來封裝發送\npublic class ClientSignalR { private HubConnection _hub; public HubConnection Hub { get; } private string _connectionUrl; public string ConnectionUrl { get; } public ClientSignalR() { } public async Task Initialize(string connectionUrl) { _connectionUrl = connectionUrl; _hub = new HubConnectionBuilder() .WithUrl(_connectionUrl) .Build(); await _hub.StartAsync(); } public async Task SendHubMessage(string methodName, string name, string message) { await _hub?.InvokeAsync(methodName, name, message); } public async Task CloseConnection() { await _hub.DisposeAsync(); } } Program.cs\nstatic async System.Threading.Tasks.Task Main(string[] args) { var hubClient = new ClientSignalR(); await hubClient.Initialize(\u0026#34;\u0026lt;Your Server Hub URL\u0026gt;/message\u0026#34;); while (true) { Console.WriteLine(\u0026#34;Please enter you message:\u0026#34;); var input = Console.ReadLine(); await hubClient.SendHubMessage(\u0026#34;SendMessage\u0026#34;, \u0026#34;Kyle\u0026#34;, input); } } 實作效果 今天這一篇先這樣避免太長，下一篇我們要來美化HTML\u0026amp;跟Chatbot結合\n懶人包，本次學到了什麼? 建立 Azure SignalR 服務 https://azure.microsoft.com/zh-tw/services/signalr-service/ 建立 SignalR Server Hub https://docs.microsoft.com/zh-tw/azure/azure-signalr/signalr-quickstart-dotnet-core 建立 HTML Client 即時呈現訊息 https://docs.microsoft.com/zh-tw/azure/azure-signalr/signalr-quickstart-dotnet-core 建立 .NET Console App 來發送訊息 https://daniel-krzyczkowski.github.io/Real-time-data-with-Microsoft-Azure-SignalR-Service/ ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-28/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day28)\nSignalR是實現即時通訊的框架，如下圖，在Server Hub宣告要給client端呼叫的方法，能傳送即時訊息給已連線的使用者\nhttps://www.pubnub.com/learn/glossary/what-is-signalr/\n而透過Azure SignalR 服務，大大簡化使用門檻，很簡單就能建立一個服務，且享有雲端帶來的高拓展性，這篇文章會step by step 實作以下內容\n建立 Azure SignalR 服務 建立 SignalR Server Hub 建立 HTML Client 即時呈現訊息 建立 .NET Console App 來發送訊息 建立 Azure SignalR 服務 登入Azure，進入https://portal.azure.com/#create/Microsoft.SignalRGalleryPackage 新增服務\n建完後，到key的選單，我們等等需要這個 Connection String\n建立 SignalR Server Hub 打開 Web Api 專案的 .csproj，加入以下程式碼\n\u0026lt;ItemGroup\u0026gt; \u0026lt;DotNetCliToolReference Include=\u0026#34;Microsoft.VisualStudio.Web.CodeGeneration.Tools\u0026#34; Version=\u0026#34;2.0.0\u0026#34; /\u0026gt; \u0026lt;DotNetCliToolReference Include=\u0026#34;Microsoft.Extensions.SecretManager.Tools\u0026#34; Version=\u0026#34;2.0.0\u0026#34; /\u0026gt; \u0026lt;/ItemGroup\u0026gt; 用 Nuget 安裝 Microsoft.Azure.SignalR\n在 Web Api 專案案右鍵，建立管理使用者密碼，\n建立 Azure:SignalR:ConnectionString ，把剛剛的連線字串貼上\nWeb Api 專案 Startup.","title":"製作婚禮現場即時留言版- Azure SignalR Service I (28)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day27)\n婚禮現場一定會有各種親朋好友視角的照片，以往都要一個一個傳在社群媒體之類的，本次來實作 chatbot 來廣收各種朋友拍的照片.. XD\n上傳到 Imgur Imgur是個圖床服務，如果是非商業使用，是完全免費的，串接起來也很容易，首先先到 https://api.imgur.com/oauth2/addclient 註冊一個 OAuth 服務\n會得到 client id 和 client secret\n回到 LineBotApp.cs，我們可以針對圖片類型的訊息做處理\nGetContentBytesAsync\npublic async Task\u0026lt;Stream\u0026gt; GetContentBytesAsync(string messageId) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), $\u0026#34;https://api-data.line.me/v2/bot/message/{messageId}/content\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var response = await httpClient.SendAsync(request); var results = await response.Content.ReadAsStreamAsync(); return results; } } } UploadToImgurAsync\n可以透過 Nuget 安裝 Imgur.API\npublic async Task\u0026lt;string\u0026gt; UploadToImgurAsync(Stream stream) { var apiClient = new ApiClient(\u0026#34;\u0026lt;Your Imgur ClientId\u0026gt;\u0026#34;); var httpClient = new HttpClient(); var imageEndpoint = new ImageEndpoint(apiClient, httpClient); var imageUpload = await imageEndpoint.UploadImageAsync(stream); return imageUpload.Link; } 接著試著在 Line 傳一張圖片試試看，這樣幾個步驟後就能成功取得圖片網址啦~ 後續可以將這網址存到資料庫或前幾篇講的Azure Table\n懶人包，本次學到了什麼? 接收 Line Image Message Type https://developers.line.biz/en/reference/messaging-api/#message-event 用 Message Id 取得存放在 Line Server 的圖片 https://developers.line.biz/en/reference/messaging-api/#get-content 使用 Imgur.API 來上傳圖片 https://github.com/DamienDennehy/Imgur.API ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-27/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day27)\n婚禮現場一定會有各種親朋好友視角的照片，以往都要一個一個傳在社群媒體之類的，本次來實作 chatbot 來廣收各種朋友拍的照片.. XD\n上傳到 Imgur Imgur是個圖床服務，如果是非商業使用，是完全免費的，串接起來也很容易，首先先到 https://api.imgur.com/oauth2/addclient 註冊一個 OAuth 服務\n會得到 client id 和 client secret\n回到 LineBotApp.cs，我們可以針對圖片類型的訊息做處理\nGetContentBytesAsync\npublic async Task\u0026lt;Stream\u0026gt; GetContentBytesAsync(string messageId) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), $\u0026#34;https://api-data.line.me/v2/bot/message/{messageId}/content\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var response = await httpClient.SendAsync(request); var results = await response.Content.ReadAsStreamAsync(); return results; } } } UploadToImgurAsync\n可以透過 Nuget 安裝 Imgur.API\npublic async Task\u0026lt;string\u0026gt; UploadToImgurAsync(Stream stream) { var apiClient = new ApiClient(\u0026#34;\u0026lt;Your Imgur ClientId\u0026gt;\u0026#34;); var httpClient = new HttpClient(); var imageEndpoint = new ImageEndpoint(apiClient, httpClient); var imageUpload = await imageEndpoint.","title":"收集婚禮現場照片- 將圖片用C#傳到 Imgur 服務 (27)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day26)\n以下是 Line 官方文件的流程，今天會依據這個圖的流程來實作 Line Pay 付款\nhttps://pay.line.me/jp/developers/apis/onlineApis?locale=zh_TW\nCheckout 流程 官方API文件可以從這裡看到\nRequest Body 的 C# Class\npublic class LinePayRequest { public int amount { get; set; } public string currency { get; set; } public string orderId { get; set; } public List\u0026lt;Package\u0026gt; packages { get; set; } public Options options { get; set; } public Redirecturls redirectUrls { get; set; } } public class Package { public string id { get; set; } public int amount { get; set; } public string name { get; set; } public List\u0026lt;Product\u0026gt; products { get; set; } } public class Product { public string id { get; set; } public string name { get; set; } public string imageUrl { get; set; } public int quantity { get; set; } public int price { get; set; } } public class Options { public Payment payment { get; set; } } public class Payment { public bool capture { get; set; } } public class Redirecturls { public string confirmUrl { get; set; } public string cancelUrl { get; set; } } 整個 Post 的 Body 可以這樣組成，channelId和channelSecret可以在前一篇申請sandbox時候取得\n但 Line Pay 在發 Request 的時候必須把 PostBody 加密，寫了個 method 處理\nstring Signature = HashLinePayRequest(channelSecret, apiUrl, body, orderId, channelSecret); private static string HashLinePayRequest(string channelSecret, string apiUrl, string body, string orderId, string key) { var request = channelSecret + apiUrl + body + orderId; key = key ?? \u0026quot;\u0026quot;; var encoding = new System.Text.UTF8Encoding(); byte[] keyByte = encoding.GetBytes(key); byte[] messageBytes = encoding.GetBytes(request); using (var hmacsha256 = new HMACSHA256(keyByte)) { byte[] hashmessage = hmacsha256.ComputeHash(messageBytes); return Convert.ToBase64String(hashmessage); } } 在 Post 的時候 Header 也要加一些 Line 的驗證資訊\nresult 可以用 JSON.NET轉強行別\nJsonConvert.DeserializeObject\u0026lt;LinePayResponse\u0026gt;(result) LinePayResponse.cs\npublic class LinePayResponse { public string returnCode { get; set; } public string returnMessage { get; set; } public Info info { get; set; } } public class Info { public Paymenturl paymentUrl { get; set; } public long transactionId { get; set; } public string paymentAccessToken { get; set; } } public class Paymenturl { public string web { get; set; } public string app { get; set; } } 在 Razor Page 導頁到 Line Server 進行付款\n實作效果\n登入Line或掃QR Code的付款畫面\nConfirm 流程 再來要做付完款的 callback 流程 官方API文件可以從這裡看到\nCallback 可以接收 Line 這四個參數\n在剛剛發送Request時我們設定callback的URL是 http://localhost:xxx/api/payment/confirm，我們開個Api來接他，能正確取得剛剛所指定的 OrderId，就能去做後續實作啦，標記該朋友已經付過禮金了 (本系列文章就不實作完成了)\n懶人包，本次學到了什麼? 看文件用C#串Line Pay https://pay.line.me/jp/developers/apis/onlineApis?locale=zh_TW ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-26/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day26)\n以下是 Line 官方文件的流程，今天會依據這個圖的流程來實作 Line Pay 付款\nhttps://pay.line.me/jp/developers/apis/onlineApis?locale=zh_TW\nCheckout 流程 官方API文件可以從這裡看到\nRequest Body 的 C# Class\npublic class LinePayRequest { public int amount { get; set; } public string currency { get; set; } public string orderId { get; set; } public List\u0026lt;Package\u0026gt; packages { get; set; } public Options options { get; set; } public Redirecturls redirectUrls { get; set; } } public class Package { public string id { get; set; } public int amount { get; set; } public string name { get; set; } public List\u0026lt;Product\u0026gt; products { get; set; } } public class Product { public string id { get; set; } public string name { get; set; } public string imageUrl { get; set; } public int quantity { get; set; } public int price { get; set; } } public class Options { public Payment payment { get; set; } } public class Payment { public bool capture { get; set; } } public class Redirecturls { public string confirmUrl { get; set; } public string cancelUrl { get; set; } } 整個 Post 的 Body 可以這樣組成，channelId和channelSecret可以在前一篇申請sandbox時候取得","title":"婚禮禮金用Line Pay II (26)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day25)\n婚禮最麻煩的就是收禮金了吧，當天還有可能遇到跑錯棚的(本人親自遇到，後來還要退錢給他XD)，婚禮結束還要很辛苦的算鈔票，我一直在想未來有沒有可能都用電子支付，讓婚禮結束後不要扛那麼多的現金，不要花那麼多時間計算。本篇將以 Line 的 Sendbox 進行串接測試，要實際使用的話要先註冊成商家，並通過 Line 的審核。在 Sandbox 有很清楚的 step by step 來說明如何串接\n註冊 Sandbox 至 https://pay.line.me/jp/developers/main/main?locale=zh_TW 註冊\n接著 Email 會收到測試的帳號\n接下來到商家後台，用上面的帳號密碼登入 (寫這篇文章的同時，email 裡面提供的商家後台連結不知道為什麼會出錯)，取得 client id與 client secret\nRazor Page 開發禮金表單 新增一個 Razor Page\nPaymentModel\n新增一個 Amount 屬性\n在 Razor 可以用 asp-for 來綁定 model\n表單驗證呢 ? 可以用 asp-validation-for\nasp-page-handler 可以在 .cs寫 function 驗證 model\n實作效果\n下一篇來繼續用 C# 來串 Line Pay ~\n懶人包，本次學到了什麼? Razor Page https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/razor-pages/razor-pages-start?view=aspnetcore-3.1\u0026amp;tabs=visual-studio Add validation to an ASP.NET Core Razor Page https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/razor-pages/validation?view=aspnetcore-3.1\u0026amp;tabs=visual-studio ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-25/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day25)\n婚禮最麻煩的就是收禮金了吧，當天還有可能遇到跑錯棚的(本人親自遇到，後來還要退錢給他XD)，婚禮結束還要很辛苦的算鈔票，我一直在想未來有沒有可能都用電子支付，讓婚禮結束後不要扛那麼多的現金，不要花那麼多時間計算。本篇將以 Line 的 Sendbox 進行串接測試，要實際使用的話要先註冊成商家，並通過 Line 的審核。在 Sandbox 有很清楚的 step by step 來說明如何串接\n註冊 Sandbox 至 https://pay.line.me/jp/developers/main/main?locale=zh_TW 註冊\n接著 Email 會收到測試的帳號\n接下來到商家後台，用上面的帳號密碼登入 (寫這篇文章的同時，email 裡面提供的商家後台連結不知道為什麼會出錯)，取得 client id與 client secret\nRazor Page 開發禮金表單 新增一個 Razor Page\nPaymentModel\n新增一個 Amount 屬性\n在 Razor 可以用 asp-for 來綁定 model\n表單驗證呢 ? 可以用 asp-validation-for\nasp-page-handler 可以在 .cs寫 function 驗證 model\n實作效果\n下一篇來繼續用 C# 來串 Line Pay ~\n懶人包，本次學到了什麼? Razor Page https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/razor-pages/razor-pages-start?view=aspnetcore-3.1\u0026amp;tabs=visual-studio Add validation to an ASP.","title":"婚禮禮金用Line Pay I (25)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day24)\n上一篇講到了用 QR Code 來做婚禮報到，如果能進一步來做自動報到就更方便了，LINE Beacon，是 LINE 推出的藍芽Sensor，可以讓 LINE Bot 接收設備附近的資訊，有時路過一些店家或捷運，他會推播給你一些訊息給你，這就是 Beacon 的技術\n但寫到此篇發現 Line Beacon 無法用簡單的手機或平板來模擬 (就我之前搞beacon的經驗，那時候開發都是用手機當 Sensor)，目前看到可以模擬的方式是用Raspberry Pi 來架設 https://github.com/line/line-simple-beacon\n故此篇內容會來講講怎麼申請和怎麼在程式裡面去擴充接收Line Beacon事件 LINE Business ID Edit descriptionmanager.line.biz\nWebhookEventType 增加 Beacon 事件\n在 Event 繼續擴充\nBeaconType\nenter : 當使用者進入Line Beacon 發送範圍內 banner：LINE for Business用戶限定，可讓用戶收到打開藍芽資訊，進而引導到加企業用戶好友及收到企業帳號的訊息 stay：LINE for Business用戶限定，偵測在區域內的使用者，每10秒會觸發一次\n接著就是在收到訊息後判斷是否為 Beacon 事件，並做實際處理啦\n本篇有點可惜，來不及買 Line Beacon 來玩，無法嘗試實際效果，之後有機會再拿來應用~\n懶人包，本次學到了什麼? Line Beacon https://developers.line.biz/en/docs/messaging-api/using-beacons/ ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-24/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day24)\n上一篇講到了用 QR Code 來做婚禮報到，如果能進一步來做自動報到就更方便了，LINE Beacon，是 LINE 推出的藍芽Sensor，可以讓 LINE Bot 接收設備附近的資訊，有時路過一些店家或捷運，他會推播給你一些訊息給你，這就是 Beacon 的技術\n但寫到此篇發現 Line Beacon 無法用簡單的手機或平板來模擬 (就我之前搞beacon的經驗，那時候開發都是用手機當 Sensor)，目前看到可以模擬的方式是用Raspberry Pi 來架設 https://github.com/line/line-simple-beacon\n故此篇內容會來講講怎麼申請和怎麼在程式裡面去擴充接收Line Beacon事件 LINE Business ID Edit descriptionmanager.line.biz\nWebhookEventType 增加 Beacon 事件\n在 Event 繼續擴充\nBeaconType\nenter : 當使用者進入Line Beacon 發送範圍內 banner：LINE for Business用戶限定，可讓用戶收到打開藍芽資訊，進而引導到加企業用戶好友及收到企業帳號的訊息 stay：LINE for Business用戶限定，偵測在區域內的使用者，每10秒會觸發一次\n接著就是在收到訊息後判斷是否為 Beacon 事件，並做實際處理啦\n本篇有點可惜，來不及買 Line Beacon 來玩，無法嘗試實際效果，之後有機會再拿來應用~\n懶人包，本次學到了什麼? Line Beacon https://developers.line.biz/en/docs/messaging-api/using-beacons/ ","title":"用Line Beacon來搞自動報到 (24)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day23)\n在上一篇我們實作了Liff App 的 QR Code 掃瞄功能後，我們要來將這個值傳到後端完成報到的動作\n在 Razor Page 一樣可以跟以往一樣把一些共用的 Template 抽出來，譬如 _Layout，一個蘿蔔一個坑，在有用到此 Template 最終就會 Render 在這些所埋的坑裡面\n_Layout.cshtml\nQrCode.cshtml\n今晚我想來點jQuery，預設 Razor Page 有載入 jQuery，我們直接用 jQuery Ajax 來跟後端溝通\nQrCode Api 的內容，其實就是前幾篇講的，針對 Azure Table 的操作\n實作畫面\nhttps://imgur.com/5qsSzRN\n懶人包，本次學到了什麼? Razor 的 RenderBody() vs RenderSection() http://coding.anyun.tw/2012/02/19/razor-learning-renderbody-renderpartial-and-rendersection/\njQuery Ajax https://api.jquery.com/jquery.ajax/\n如何新增/編輯一個結構化的 Azure Table 資料 https://docs.microsoft.com/en-us/azure/cosmos-db/tutorial-develop-table-dotnet?toc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fstorage%2Ftables%2Ftoc.json\u0026amp;bc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fbread%2Ftoc.json\n","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-23/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day23)\n在上一篇我們實作了Liff App 的 QR Code 掃瞄功能後，我們要來將這個值傳到後端完成報到的動作\n在 Razor Page 一樣可以跟以往一樣把一些共用的 Template 抽出來，譬如 _Layout，一個蘿蔔一個坑，在有用到此 Template 最終就會 Render 在這些所埋的坑裡面\n_Layout.cshtml\nQrCode.cshtml\n今晚我想來點jQuery，預設 Razor Page 有載入 jQuery，我們直接用 jQuery Ajax 來跟後端溝通\nQrCode Api 的內容，其實就是前幾篇講的，針對 Azure Table 的操作\n實作畫面\nhttps://imgur.com/5qsSzRN\n懶人包，本次學到了什麼? Razor 的 RenderBody() vs RenderSection() http://coding.anyun.tw/2012/02/19/razor-learning-renderbody-renderpartial-and-rendersection/\njQuery Ajax https://api.jquery.com/jquery.ajax/\n如何新增/編輯一個結構化的 Azure Table 資料 https://docs.microsoft.com/en-us/azure/cosmos-db/tutorial-develop-table-dotnet?toc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fstorage%2Ftables%2Ftoc.json\u0026amp;bc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fbread%2Ftoc.json","title":"用QR Code來搞報到 — Razor Page實做 II (23)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day22)\n婚禮當天必須請一些親戚來協助朋友報到，但前面都收集報名資訊了，用一些報到的機制方便統計也是挺潮的…看到 Liff App V2 有支援 QR Code 報到，剛好前面也有存報名的使用者資料，就能來判斷報到狀況，本次來實現一下這個功能。至於為啥不用 vue，要用 Razor Page，也沒為啥，鐵人賽就是順便玩玩沒玩過的 XD\n在 .NET Core Web API 專案加入 Razor Page 如果當初起始專案選擇是存 Web Api 專案，可以考慮重新一個Web Application的時候把相關檔案搬過來\nStartup.cs 要加入下列三段程式\nwwwroot 把一些靜態檔案搬過來\n開個資料夾叫 Pages 把 cdhtml 搬過來\n按右鍵 加入一個叫做 QRCode 的 Razor Page\n再來就是跟前幾篇實作一樣，加入 liff app SDK啦，說明如下：\nLine Liff SDK v2 Liff App 的 初始化 (記得要註冊一個 Liff App Id) 取得當前使用者 QR Code 掃描 實作畫面\n下一篇，我們就繼續實作，怎麼完成後端報到的功能\nRazor Page 對我來講好有復古的感覺，彷彿回到 WebForm 的時代… 懶人包，本次學到了什麼? Razor Page https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/razor-pages/razor-pages-start?view=aspnetcore-3.1\u0026amp;tabs=visual-studio Liff App Scan QR Code https://developers.line.biz/en/reference/liff/#scan-code ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-22/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day22)\n婚禮當天必須請一些親戚來協助朋友報到，但前面都收集報名資訊了，用一些報到的機制方便統計也是挺潮的…看到 Liff App V2 有支援 QR Code 報到，剛好前面也有存報名的使用者資料，就能來判斷報到狀況，本次來實現一下這個功能。至於為啥不用 vue，要用 Razor Page，也沒為啥，鐵人賽就是順便玩玩沒玩過的 XD\n在 .NET Core Web API 專案加入 Razor Page 如果當初起始專案選擇是存 Web Api 專案，可以考慮重新一個Web Application的時候把相關檔案搬過來\nStartup.cs 要加入下列三段程式\nwwwroot 把一些靜態檔案搬過來\n開個資料夾叫 Pages 把 cdhtml 搬過來\n按右鍵 加入一個叫做 QRCode 的 Razor Page\n再來就是跟前幾篇實作一樣，加入 liff app SDK啦，說明如下：\nLine Liff SDK v2 Liff App 的 初始化 (記得要註冊一個 Liff App Id) 取得當前使用者 QR Code 掃描 實作畫面\n下一篇，我們就繼續實作，怎麼完成後端報到的功能\nRazor Page 對我來講好有復古的感覺，彷彿回到 WebForm 的時代… 懶人包，本次學到了什麼?","title":"用QR Code來搞報到 - Razor Page實做 I (22)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day21)\n上一篇介紹如何主動式推播給使用者訊息，但Push Message發送到一定的量是要收費的，Line Notify是另一種主動式推播的解決方案，且是免費的，缺點就是收到訊息時候，不會顯示是從本身Chatbot的角度，而是Line的Chatbot，要發送Notify訊息，大概需要做以下事情，缺一不可\n註冊 Line Notify 服務 取得 client_id 取得 client_secret 取得驗證的 code 透過第2-4項取得 access token 註冊 Line Notify 服務 LINE Notify\n透過 ngrok http 8888 取得一個local的對外站台，貼到 Callback URL\n接著會驗證 Email，驗證通過後可以進到這個 Service\nClient ID 跟 Client Secret 取得了\n接著可以透過下列網址訂閱服務\n[https://notify-bot.line.me/oauth/authorize?response_type=code\u0026amp;scope=notify\u0026amp;response_mode=form_post\u0026amp;client_id=](https://notify-bot.line.me/oauth/authorize?response_type=code\u0026amp;scope=notify\u0026amp;response_mode=form_post\u0026amp;client_id=)\u0026lt;client_id\u0026gt;\u0026amp;redirect_uri=\u0026lt;client_secret\u0026gt;/\u0026amp;state=\u0026lt;驗證身分代碼，任意給\u0026gt; 可以看到可以從這個頁面綁定\nLine 會收到一個通知\n到我們剛剛 ngrok 的後台看看 (http://127.0.0.1:4040)，有接收到這個 Request，取得加密後的 code\n用剛剛取得的這三個資訊來取得 access token\npublic static async Task\u0026lt;string\u0026gt; GetAccessToken() { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), \u0026#34;https://notify-bot.line.me/oauth/token\u0026#34;)) { var redirect_uri = \u0026#34;https://f2fb2e2ee6d1.ngrok.io/\u0026#34;; var client_id = \u0026#34;XXXXX\u0026#34;; var client_secret = \u0026#34;XXXXX\u0026#34;; var code = \u0026#34;XXXXX\u0026#34;; request.Content = new StringContent($\u0026#34;grant_type=authorization_code\u0026amp;redirect_uri={redirect_uri}\u0026amp;client_id={client_id}\u0026amp;client_secret={client_secret}\u0026amp;code={code}\u0026#34;); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/x-www-form-urlencoded\u0026#34;); var response = await httpClient.SendAsync(request); var results = await response.Content.ReadAsStringAsync(); return results; } } } 透過 C# 發送\npublic static async Task PushNotify(string accessToken, string message) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), \u0026#34;https://notify-api.line.me/api/notify\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); request.Content = new StringContent($\u0026#34;message={message}\u0026#34;); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/x-www-form-urlencoded\u0026#34;); var response = await httpClient.SendAsync(request); } } } 如何使用\n實際上就能把這個程式埋在報名成功後，發送一個 Notify 訊息\nvar accessToken = await GetAccessToken(); await PushNotify(accessToken, $\u0026#34;Hi, 金城武 剛剛報名了婚禮，他當天會出席!\u0026#34;); 懶人包，本次學到了什麼? 透過 Line Notify 來發送主動式推播 https://notify-bot.line.me/doc/en/ ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-21/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day21)\n上一篇介紹如何主動式推播給使用者訊息，但Push Message發送到一定的量是要收費的，Line Notify是另一種主動式推播的解決方案，且是免費的，缺點就是收到訊息時候，不會顯示是從本身Chatbot的角度，而是Line的Chatbot，要發送Notify訊息，大概需要做以下事情，缺一不可\n註冊 Line Notify 服務 取得 client_id 取得 client_secret 取得驗證的 code 透過第2-4項取得 access token 註冊 Line Notify 服務 LINE Notify\n透過 ngrok http 8888 取得一個local的對外站台，貼到 Callback URL\n接著會驗證 Email，驗證通過後可以進到這個 Service\nClient ID 跟 Client Secret 取得了\n接著可以透過下列網址訂閱服務\n[https://notify-bot.line.me/oauth/authorize?response_type=code\u0026amp;scope=notify\u0026amp;response_mode=form_post\u0026amp;client_id=](https://notify-bot.line.me/oauth/authorize?response_type=code\u0026amp;scope=notify\u0026amp;response_mode=form_post\u0026amp;client_id=)\u0026lt;client_id\u0026gt;\u0026amp;redirect_uri=\u0026lt;client_secret\u0026gt;/\u0026amp;state=\u0026lt;驗證身分代碼，任意給\u0026gt; 可以看到可以從這個頁面綁定\nLine 會收到一個通知\n到我們剛剛 ngrok 的後台看看 (http://127.0.0.1:4040)，有接收到這個 Request，取得加密後的 code\n用剛剛取得的這三個資訊來取得 access token\npublic static async Task\u0026lt;string\u0026gt; GetAccessToken() { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), \u0026#34;https://notify-bot.","title":"當有人報名的時候接收通知 (Line Notify) (21)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day20)\n在上一篇我們儲存報名表單後，也有一併打使用者的ID儲存起來，透過這個ID，我們就能發送一些主動的推播訊息\n準備 Line Chatbot 的 Access Tokne 準備要發送的 UserId (可以從接收到的訊息及Liff App 取得使用者ID) 新增一個 .NET Core Console專案來發送\n官方的 Shell 範例\n一樣丟到 Curl To C# 做轉換，並做格式修改\n當然 Push Message 也支援各種前面介紹的格式，此篇就不再多做介紹了，可以參考本系列文的各種訊息格式教學\nstatic async System.Threading.Tasks.Task Main(string[] args) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), \u0026#34;https://api.line.me/v2/bot/message/multicast\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer \u0026lt;Your Access Token\u0026gt;\u0026#34;); PushMessage req = new PushMessage(); req.to = new List\u0026lt;string\u0026gt;() { \u0026#34;\u0026lt;User Id\u0026gt;\u0026#34; }; req.messages = new List\u0026lt;Message\u0026gt;() { new Message(){ type = \u0026#34;text\u0026#34;, text = \u0026#34;Hi, 提醒您 12/08 就是我們的人生大事啦，如果不能前往請記得提前告知！\u0026#34; } }; var postJson = JsonConvert.SerializeObject(req, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } }); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); } } } PushMessage.cs\npublic class PushMessage { public List\u0026lt;string\u0026gt; to { get; set; } public List\u0026lt;Message\u0026gt; messages { get; set; } = new List\u0026lt;Message\u0026gt;(); } public class Message { public string type { get; set; } public string text { get; set; } } 實際成果 要注意的是，Push Message 依據不同的方案是要收費的，詳細可以看這篇的Line@ 收費計算機 LINE 2.0 收費計算機：收費試算、推薦方案一按即知\n懶人包，本次學到了什麼? 如何透過C#發送主動推播的Line訊息 https://developers.line.biz/en/reference/messaging-api/#send-push-message ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-20/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day20)\n在上一篇我們儲存報名表單後，也有一併打使用者的ID儲存起來，透過這個ID，我們就能發送一些主動的推播訊息\n準備 Line Chatbot 的 Access Tokne 準備要發送的 UserId (可以從接收到的訊息及Liff App 取得使用者ID) 新增一個 .NET Core Console專案來發送\n官方的 Shell 範例\n一樣丟到 Curl To C# 做轉換，並做格式修改\n當然 Push Message 也支援各種前面介紹的格式，此篇就不再多做介紹了，可以參考本系列文的各種訊息格式教學\nstatic async System.Threading.Tasks.Task Main(string[] args) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), \u0026#34;https://api.line.me/v2/bot/message/multicast\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer \u0026lt;Your Access Token\u0026gt;\u0026#34;); PushMessage req = new PushMessage(); req.to = new List\u0026lt;string\u0026gt;() { \u0026#34;\u0026lt;User Id\u0026gt;\u0026#34; }; req.","title":"主動推播Line訊息給某個使用者(Push Message) (20)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day19)\nAzure Table Azure Table 是一個具有結構性的 NoSQL 服務，如果資料結構不具備太複雜的關聯關係，透過這種 Key-Value 的方式做儲存，用 JSON 反序列化回來是一個不錯且方便的方式。\n註冊 Azure Table 服務 如何註冊一個 Azure Table 服務，可以參考官方文件，建立完成後，可以透過以下路徑取得 Storage 的 Connection String\n在 .NET Core Web Api 儲存報名資訊 先從 Nuget 安裝 Microsoft.Azure.Cosmos.Table\n新增 AzureTableUtility.cs 並實作\n如何透過 SDK 新增 Azure Table\npublic static CloudTable CreateTableAsync(string tableName) { string storageConnectionString = \u0026#34;DefaultEndpointsProtocol=https;AccountName=kylewedding;AccountKey=2mVDAPWt6xZAlxY9xx6pxhzJMvp3vyULGql7PZz9tHEwJ0zc+1vtdt50ZcDwb9I862ez+OoHQGcCkOhU0OvAng==;EndpointSuffix=core.windows.net\u0026#34;; // Storage 的 連線字串，來源可以用appSetting.json搭配 IOption 注入 CloudStorageAccount storageAccount = CreateStorageAccountFromConnectionString(storageConnectionString); // 新增 Azure Table CloudTableClient tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration()); // 不存在則會新增，確認 Table 已存在可使用 await table.CreateIfNotExistsAsync() CloudTable table = tableClient.GetTableReference(tableName); return table; } public static CloudStorageAccount CreateStorageAccountFromConnectionString(string storageConnectionString) { CloudStorageAccount storageAccount; try { storageAccount = CloudStorageAccount.Parse(storageConnectionString); } catch (FormatException) { throw; } catch (ArgumentException) { throw; } return storageAccount; } 如何在 Azure Table 新增或更新資料\n這邊用泛型，來讓外部決定所要新增的 Model\npublic static async Task\u0026lt;ITableEntity\u0026gt; InsertOrMergeEntityAsync\u0026lt;T\u0026gt;(CloudTable table, ITableEntity entity) { try { // 新增或更新 TableOperation insertOrMergeOperation = TableOperation.InsertOrMerge(entity); TableResult result = await table.ExecuteAsync(insertOrMergeOperation); var insertedCustomer = result.Result as ITableEntity; if (result.RequestCharge.HasValue) { Console.WriteLine(\u0026#34;Request Charge of InsertOrMerge Operation: \u0026#34; + result.RequestCharge); } return insertedCustomer; } catch (StorageException e) { throw; } } 在 Web Api 新增一個 RegisterController\n[HttpPost] public async Task\u0026lt;IActionResult\u0026gt; Post(Users user) { CloudTable table = AzureTableUtility.CreateTableAsync(\u0026#34;Users\u0026#34;); user.PartitionKey = Guid.NewGuid().ToString(\u0026#34;N\u0026#34;); user.RowKey = Guid.NewGuid().ToString(\u0026#34;N\u0026#34;); var models = await AzureTableUtility.InsertOrMergeEntityAsync\u0026lt;Users\u0026gt;(table, user) as Users; return Ok(); } User.cs 的內容\n這是報名的資料，可以在前一天的 vue.js 實作轉成 C# 的 model\n另繼承 TableEntity 來讓 Azure Table SDK 能寫入\npublic class Users : TableEntity { public Users() { } public Users(string _partitionKey, string _rowKey) { PartitionKey = _partitionKey; RowKey = _rowKey; } public string LineUserId { get; set; } public string LineUserName { get; set; } public string fullName { get; set; } public string phoneNumber { get; set; } public int attendEvent { get; set; } public int inviteType { get; set; } public int relation { get; set; } public int attendPeople { get; set; } public int child { get; set; } public bool speical { get; set; } public string message { get; set; } } 透過 Postman 測試 Api 即可看到資料能正確寫入\n用 Azure Storage Explorer 登入 Azure帳號並選取 Azure Table 服務即可看到資料\n怎麼撈已經寫入的資料呢\n新增一個 Get 的 Api\n[HttpGet] public async Task\u0026lt;IActionResult\u0026gt; Get() { CloudTable table = AzureTableUtility.CreateTableAsync(\u0026#34;Users\u0026#34;); List\u0026lt;Users\u0026gt; linqQuery = table .CreateQuery\u0026lt;Users\u0026gt;() .ToList(); return Ok(linqQuery); } 執行結果，透過這種方式就可以去做一個已報名的後台來瀏覽啦 (本系列文章不會寫到後台)\n回到 Vue.js 如何串接 Vue.js 可透過 axios 來跟 Api 溝通\nnpm install axios --save 匯入 axios 模組 將報名表單的發送 post request 如果要打到 local API 可能會有 CORS 跨域問題，可以在主目錄下新增一個 vue.config.js 設定 Proxy\nmodule.exports = { devServer: { proxy: { \u0026#39;/api\u0026#39;: { target: \u0026#39;http://localhost:\u0026lt;Your Local Api Port\u0026gt;\u0026#39;, changeOrigin: true, ws: true, pathRewrite: { \u0026#39;^api\u0026#39;: \u0026#39;\u0026#39; } } } } } 懶人包，本次學到了什麼? 如何註冊 Azure Table https://azure.microsoft.com/zh-tw/free/ 用 Azure Storage Explorer 管理 https://azure.microsoft.com/en-ca/features/storage-explorer/ 如何使用 Microsoft.Azure.Cosmos.Table SDK https://www.nuget.org/packages/Microsoft.Azure.Cosmos.Table 如何新增/編輯一個結構化的 Azure Table 資料 https://docs.microsoft.com/en-us/azure/cosmos-db/tutorial-develop-table-dotnet?toc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fstorage%2Ftables%2Ftoc.json\u0026amp;bc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fbread%2Ftoc.json 如何用 vue.js + axios 來跟 Api 溝通 https://www.runoob.com/vue2/vuejs-ajax-axios.html ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-19/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day19)\nAzure Table Azure Table 是一個具有結構性的 NoSQL 服務，如果資料結構不具備太複雜的關聯關係，透過這種 Key-Value 的方式做儲存，用 JSON 反序列化回來是一個不錯且方便的方式。\n註冊 Azure Table 服務 如何註冊一個 Azure Table 服務，可以參考官方文件，建立完成後，可以透過以下路徑取得 Storage 的 Connection String\n在 .NET Core Web Api 儲存報名資訊 先從 Nuget 安裝 Microsoft.Azure.Cosmos.Table\n新增 AzureTableUtility.cs 並實作\n如何透過 SDK 新增 Azure Table\npublic static CloudTable CreateTableAsync(string tableName) { string storageConnectionString = \u0026#34;DefaultEndpointsProtocol=https;AccountName=kylewedding;AccountKey=2mVDAPWt6xZAlxY9xx6pxhzJMvp3vyULGql7PZz9tHEwJ0zc+1vtdt50ZcDwb9I862ez+OoHQGcCkOhU0OvAng==;EndpointSuffix=core.windows.net\u0026#34;; // Storage 的 連線字串，來源可以用appSetting.json搭配 IOption 注入 CloudStorageAccount storageAccount = CreateStorageAccountFromConnectionString(storageConnectionString); // 新增 Azure Table CloudTableClient tableClient = storageAccount.","title":"Line Liff App v2 — 用 Azure Table 儲存報名資訊 (19)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day18)\n在上一篇我們成功將 chatbot 透過 Liff App 後，跟所開發網站有一些連結後，本篇我們將要來實作前端的報名表單\n本篇實作內容放在這個 Git Repo：\nhttps://github.com/kyleap/vue-next-vee-validate-v4-line-liff-app-sample\n本篇將會學到：\n如何起始 vue-next (v3.0) 專案 如何在 vue.js 加入 Line Liff App，並取得使用者資訊 如何在 vue 做一個報名表單 怎麼依據使用者選取條件來顯示隱藏表單內容 在 vue 如何做表單驗證 Vue.js 的表單 本次範例用了 Google 風格的 CSS 框架 Materialize，使得我們的表單比較像 Google Form 的風格，在 index.html 加入相關 css/javascript CDN\n原始碼可以至 Github 看，實作的 UI 大概長這樣\n如果不參加喜帖，相對的也不用填那麼多資訊了，這時可以透過 v-if or v-show 來實現，兩者差異為 v-if 會實際產生 dom，v-show 儘會顯示隱藏\n透過 VeeValidate 來做表單必填驗證，請注意 vue 3.0 要使用 VeeValidate 的 v4 版本\nnpm install vee-validate@next --save 在 component import 相關模組\n撰寫一個必填的 method\n用 這個 directive，並在沒有值得時候用 呈現錯誤訊息\n這樣就能很容易的做到沒有值的時候提示必填\n在 App.vue 取得 Line 使用者資訊\n設定 Liff App Id\n透過 Liff SDK 取得使用者資訊\n給子元件使用 profile 這個 model\n建立 Liff App\nLiff App 怎麼建立上一篇介紹過了，我們用 ngrok 把本地的網站 Run 起來，建立進去測試看看\n把 liff App 的 URL 貼到 Line 裡面，在手機板呈現的效果如下：\n這一篇我們用 vue.js 完成了一個前端的報名頁面，下一篇我們要將這個前端的 data model 傳到我們的 API 並儲存起來，本篇程式碼 Repo https://github.com/kyleap/vue-next-vee-validate-v4-line-liff-app-sample\n懶人包，本次學到了什麼? Vue 3.0 Vue-Next https://github.com/vuejs/vue-next 用 Materialzecss CSS框架來美化表單 https://materializecss.com/about.html 用 VeeValidate v4 來做表單驗證 https://vee-validate.logaretm.com/v4 如何用 Vue.js 及 Line Liff App 取得使用者資訊 https://developers.line.biz/en/reference/liff/#get-profile ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-18/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day18)\n在上一篇我們成功將 chatbot 透過 Liff App 後，跟所開發網站有一些連結後，本篇我們將要來實作前端的報名表單\n本篇實作內容放在這個 Git Repo：\nhttps://github.com/kyleap/vue-next-vee-validate-v4-line-liff-app-sample\n本篇將會學到：\n如何起始 vue-next (v3.0) 專案 如何在 vue.js 加入 Line Liff App，並取得使用者資訊 如何在 vue 做一個報名表單 怎麼依據使用者選取條件來顯示隱藏表單內容 在 vue 如何做表單驗證 Vue.js 的表單 本次範例用了 Google 風格的 CSS 框架 Materialize，使得我們的表單比較像 Google Form 的風格，在 index.html 加入相關 css/javascript CDN\n原始碼可以至 Github 看，實作的 UI 大概長這樣\n如果不參加喜帖，相對的也不用填那麼多資訊了，這時可以透過 v-if or v-show 來實現，兩者差異為 v-if 會實際產生 dom，v-show 儘會顯示隱藏\n透過 VeeValidate 來做表單必填驗證，請注意 vue 3.0 要使用 VeeValidate 的 v4 版本","title":"Line Liff App v2 — 用 Vue.js 來做報名頁面 (18)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day17)\nLIFF 全名是 LINE Front-end Framework，一個可以在 LINE app 內運作網頁的機制，所以網頁工程師真的很通用阿，只要會網頁的語言就可以在各種平台實現，Liff App 可以在 line chatbot 裡面打開所開發的網頁，呈現效果大概會是以下這樣，我們本次用較新的 v2 來實現\n建立 vue 專案 npm install -g @vue/cli vue create hello-line-liff-app cd hello-line-liff-app npm run serve 這樣應該就能看到以下畫面\n本系列不會講 vue 怎麼發佈到 web server上，我們用 ngrok 來打通對外網址\nngrok http \u0026lt;your local vue.js port\u0026gt;-host-header=”localhost:\u0026lt;your local vue.js port\u0026gt;\u0026#34; 要新增一個 Line Liff App，得先新增一個 channel，性質為Line Login\n在 Line Login Liff 的 tab 新增一個 App，網址指向上面 ngrok 產生的網址\n把 https://liff.line.me 的網址複製，貼到 chatbot 的對話，點擊測試看看，會是以下畫面：\n那如何在 Liff App 抓到目前使用者的資訊呢? 我們可以載入 Liff App 的 SDK\nindex.html\n\u0026lt;script *charset*=\u0026#34;utf-8\u0026#34; *src*=\u0026#34;https://static.line-scdn.net/liff/edge/2/sdk.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; App.vue\ndata: 定義App.vue 的 model beforeCreate: init liff app (這邊要傳入上面建立的 liff app id) methods: init 成功後，取得使用者資訊\n我們把這兩個參數，把子元件 HelloLiffApp.vuew 傳\nHelloLiffApp.vue\n這個 component 接收兩個參數：msg 和 profile，並秀在 html 上，透過liff.getVersion() 可以取得目前 SDK 的版本\n更多 liff 的 function 可以看這篇 ！ 結果呈現\n本篇先寫到這，後續我們要來實作婚禮的報名表單！\n懶人包，本次學到了什麼? 如何建立一個 Liff App https://developers.line.biz/en/docs/liff/registering-liff-apps/ 如何使用 Line Liff App SDK v2 https://developers.line.biz/en/docs/liff/developing-liff-apps/#specify-cdn-path 如何取得在Line Liff App 取得使用者資訊 https://developers.line.biz/en/docs/liff/developing-liff-apps/#getting-user-profile ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-17/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day17)\nLIFF 全名是 LINE Front-end Framework，一個可以在 LINE app 內運作網頁的機制，所以網頁工程師真的很通用阿，只要會網頁的語言就可以在各種平台實現，Liff App 可以在 line chatbot 裡面打開所開發的網頁，呈現效果大概會是以下這樣，我們本次用較新的 v2 來實現\n建立 vue 專案 npm install -g @vue/cli vue create hello-line-liff-app cd hello-line-liff-app npm run serve 這樣應該就能看到以下畫面\n本系列不會講 vue 怎麼發佈到 web server上，我們用 ngrok 來打通對外網址\nngrok http \u0026lt;your local vue.js port\u0026gt;-host-header=”localhost:\u0026lt;your local vue.js port\u0026gt;\u0026#34; 要新增一個 Line Liff App，得先新增一個 channel，性質為Line Login\n在 Line Login Liff 的 tab 新增一個 App，網址指向上面 ngrok 產生的網址\n把 https://liff.line.me 的網址複製，貼到 chatbot 的對話，點擊測試看看，會是以下畫面：","title":"Line Liff App v2 - 用 Vue.js 來實作 (17)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day16)\nQnA Maker 是啥 QnA Maker 是微軟提供一個雲端的自然語言處理 (NLP) 處理服務，可輕鬆透過網站後台建立自己的知識庫，並能從使用者所輸入的語句，理解他的意圖，提供可能對應的知識庫答案。QnA Maker提供了免費的方案，但限制就是只能有3份知識庫、每秒只能調用Api 3次，每月50,000筆Request，對於一般使用或技術評估來講很夠用了。要在 Line 串接大概分為以下步驟：\n在Azure註冊 QnA Maker 新增一個 knowledge base 增加題目與回答 完成訓練 \u0026amp; 發佈 取得 Endpoint、EndpointKey，來讓程式串接 建立 QnA Maker 照著 https://www.qnamaker.ai/Create Step By Step 步驟，因為QnA Maker是Azure的服務，故您可以需要Azure的訂閱，由此註冊試用\nStep 1 在 Azure 新增 QnA Maker 服務\nStep 2 選取剛剛建立的QnA Maker服務，並選擇繁體中文\nStep 3 knowledge base 名稱\n步驟四可以匯入現有的資料集，來源可以是文件或者是線上的網站，這樣就不用重新建檔了，是不是很酷啊 :D\nStep 4 匯入資料集 (本次先跳過)\n開始瘋狂建題目吧！我們來試試建立：\u0026ldquo;新人怎麼認識的？\u0026rdquo; 回答我們設定 \u0026ldquo;我們是在打工餐廳認識的喔，那時的同事 put your hands up ??\u0026quot;，並點選Save And Train\n可以透過旁邊的模擬器測試，嘗試打類似的問話，都會對應的相同的回答\n在 .NET Core 串接 QnA Maker 在串接之前，記得要先點選 Publish\n又是得到一個熟悉的 curl 指令，可以繼續用 https://curl.olsh.me/ 來轉 C# 啦\nQnAMakerUtility.cs\n新增一個 Utility 來處理 QnA Maker，裡面的 Key 應該要移到appsetting.json比較好替換，最初幾篇有提到可以用 IOption 來注入，這邊就不再多提了。QnA Maker判斷出來會有個分數值，我們篩選最高的當作答案，另也定義一個選擇新參數為分數，如果有值的話，要大於這個分數我們才會回傳。\npublic class QnAMakerUtility { public async Task\u0026lt;QnAMakterAnswer\u0026gt; Get(string question, int? scope = null) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), \u0026#34;https://qna-maker-lab.azurewebsites.net/qnamaker/knowledgebases/OOOOO-2d0e-4512-ba3f-a58fdc4c37e1/generateAnswer\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, \u0026#34;EndpointKey 5fdcf6c7-eb76-4a53-9e3b-74570645bOOO\u0026#34;); request.Content = new StringContent(\u0026#34;{\u0026#39;question\u0026#39;:\u0026#39;\u0026#34; + question + \u0026#34;\u0026#39;}\u0026#34;); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); var qnaResponse = JsonConvert.DeserializeObject\u0026lt;QnAMakerResponse\u0026gt;(result); // 取得分數最高的回答 var answers = qnaResponse.answers; // 選擇性參數，大於多少分數才回傳，這樣看起來會比較準 if(scope.HasValue) { answers = answers.Where(c =\u0026gt; c.score \u0026gt;= scope.Value); } var answer = answers.OrderByDescending(c =\u0026gt; c.score).FirstOrDefault(); // 無法辨識 if (answer == null || answer.id == -1) return null; return answer; } } } } QnAMakerResponse\npublic class QnAMakerResponse { public IEnumerable\u0026lt;QnAMakterAnswer\u0026gt; answers { get; set; } public bool activeLearningEnabled { get; set; } } public class QnAMakterAnswer { public object[] questions { get; set; } public string answer { get; set; } public float score { get; set; } public int id { get; set; } public object[] metadata { get; set; } } 調整一下 IntenFactory.cs\n我們先判斷 Rule Base 的回覆 (字串完整比對)，有比對到就回傳了，如果沒有比對到，則丟到 AI Base (QnA Maker) 來判斷\n傳送 \u0026ldquo;新人怎麼認識的？\u0026rdquo; 分數是 100 分\n傳送 “當初怎麼交往的?” 分數是 26.53 分\n所以對話模型是要逐步訓練的，我們調整一下，提升交往關鍵字的分數，要記得重新訓練和發佈\n傳送 \u0026ldquo;交往的過程\u0026rdquo;，得到重新訓練後的分數 99 分了\n本篇就是 .NET Core 串接 QnA Maker 的過程，透過這種雲端語意辨識服務，我們再也不用在程式擴增一堆 Intent 實作，並且讓 Chatbot 聰明一點，使用者為什麼都可以知道它想問的意圖是什麼。如果要更聰明一點訓練，可以考慮使用微軟的 LUIS 服務，在 LUIS 可以針對使用者所傳的資訊拆解 Entity / Intent，透過訊息的收集，透過自行定義的訓練語意讓 Chatbot 理解且更準的內容，但本系列文章應該不會介紹到這塊 (後續還是介紹一些好玩的 Line服務)\n懶人包，本次學到了什麼? QnA Maker 服務 https://azure.microsoft.com/zh-tw/services/cognitive-services/qna-maker/ Rule Base vs AI Base https://www.tricentis.com/artificial-intelligence-software-testing/ai-approaches-rule-based-testing-vs-learning/ 如何在 Line 串接 QnA Maker https://docs.microsoft.com/en-us/azure/cognitive-services/qnamaker/quickstarts/quickstart-rest-curl ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-16/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day16)\nQnA Maker 是啥 QnA Maker 是微軟提供一個雲端的自然語言處理 (NLP) 處理服務，可輕鬆透過網站後台建立自己的知識庫，並能從使用者所輸入的語句，理解他的意圖，提供可能對應的知識庫答案。QnA Maker提供了免費的方案，但限制就是只能有3份知識庫、每秒只能調用Api 3次，每月50,000筆Request，對於一般使用或技術評估來講很夠用了。要在 Line 串接大概分為以下步驟：\n在Azure註冊 QnA Maker 新增一個 knowledge base 增加題目與回答 完成訓練 \u0026amp; 發佈 取得 Endpoint、EndpointKey，來讓程式串接 建立 QnA Maker 照著 https://www.qnamaker.ai/Create Step By Step 步驟，因為QnA Maker是Azure的服務，故您可以需要Azure的訂閱，由此註冊試用\nStep 1 在 Azure 新增 QnA Maker 服務\nStep 2 選取剛剛建立的QnA Maker服務，並選擇繁體中文\nStep 3 knowledge base 名稱\n步驟四可以匯入現有的資料集，來源可以是文件或者是線上的網站，這樣就不用重新建檔了，是不是很酷啊 :D\nStep 4 匯入資料集 (本次先跳過)\n開始瘋狂建題目吧！我們來試試建立：\u0026ldquo;新人怎麼認識的？\u0026rdquo; 回答我們設定 \u0026ldquo;我們是在打工餐廳認識的喔，那時的同事 put your hands up ??\u0026quot;，並點選Save And Train","title":"來點AI感，用QnA Maker來搞Line Chatbot回覆 (16)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day15)\n在上一篇我們介紹如何設定圖文選單，本篇要來介紹如何切換圖文選單，此部分只能用程式來達成，在 Line Business 後台只能設定固定的圖文選單起訖日，有了這功能就可以像網站一樣有網站地圖的概念\n今天實作要將 chatbot 隨機切換不同的圖文選單，給使用者不同的體驗，今天的範例圖片在這 https://i.imgur.com/pupYvXr.jpg\nCreateRichMenuAsync\n原本預設的 Rich Menu 是寫在 SetDefaultRichMenuAsync，我們做點重構，讓Rich Menu 的物件是從外部傳入，因為我們現在要設定多個選單\npublic async Task\u0026lt;Richmenu\u0026gt; CreateRichMenuAsync(CreateRichmenu richmenu) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), lineMessageApiBaseUrl)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var postJson = JsonConvert.SerializeObject(richmenu, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } }); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); var results = await response.Content.ReadAsStringAsync(); var richmenu_res = JsonConvert.DeserializeObject\u0026lt;Richmenu\u0026gt;(results); return richmenu_res; }; } } RichMenuUtility\n我們將這些定義 Rich Menu的程式抽到這邊\npublic static class RichMenuUtility { public static CreateRichmenu GetDefault() { var defaultRichMenu = new CreateRichmenu(); defaultRichMenu.size = new CreateSize(1200, 810); defaultRichMenu.selected = true; defaultRichMenu.name = \u0026#34;Default RichMenu\u0026#34;; defaultRichMenu.chatBarText = \u0026#34;Kyle\u0026#39;s Wedding\u0026#34;; defaultRichMenu.areas = new List\u0026lt;CreateArea\u0026gt;() { new CreateArea(boundx:0,boundy:0,width:800,height:810, _action: new MessageAction(\u0026#34;婚紗輪播\u0026#34;)) }; return defaultRichMenu; } public static CreateRichmenu GetSecondOne() { var defaultRichMenu = new CreateRichmenu(); defaultRichMenu.size = new CreateSize(1200, 810); defaultRichMenu.selected = true; defaultRichMenu.name = \u0026#34;Second One\u0026#34;; defaultRichMenu.chatBarText = \u0026#34;Kyle\u0026#39;s Wedding\u0026#34;; defaultRichMenu.areas = new List\u0026lt;CreateArea\u0026gt;() { new CreateArea(boundx:0,boundy:0,width:800,height:810, _action: new MessageAction(\u0026#34;婚紗輪播\u0026#34;)) }; return defaultRichMenu; } } 程式就不像 Line Business 後台那麼方便有一些範本，未來如果要封裝 SDK 我可能會考慮將後台的那些範本封裝起來，只要設定圖片.按鈕事件即可 InitRickMenu\n修改上一篇，我們 GetRichMenuListAsync 我們改回傳id即可，在設定好圖文選單時也 Add 到 ID 陣列裡，最後，我們隨機取得任一個 ID 做顯示的設定\n實際畫面就不 Demo 了，RichMenu會在每次進入聊天頁面的時候隨機切換，但因為會跑出我朋友的列表不方便 Demo XD…當然這需求很奇怪，實際上應該是做成像多層選單方式，去逐步切換，且可以返回主選單的概念，這樣就讓 Line Chatbot 也像網頁一樣有上一頁 下一頁啦～\n懶人包，本次學到了什麼? 如何上傳 多個 Rich Menu 圖片 https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image 如何切換 Rich Menu 圖片 https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-15/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day15)\n在上一篇我們介紹如何設定圖文選單，本篇要來介紹如何切換圖文選單，此部分只能用程式來達成，在 Line Business 後台只能設定固定的圖文選單起訖日，有了這功能就可以像網站一樣有網站地圖的概念\n今天實作要將 chatbot 隨機切換不同的圖文選單，給使用者不同的體驗，今天的範例圖片在這 https://i.imgur.com/pupYvXr.jpg\nCreateRichMenuAsync\n原本預設的 Rich Menu 是寫在 SetDefaultRichMenuAsync，我們做點重構，讓Rich Menu 的物件是從外部傳入，因為我們現在要設定多個選單\npublic async Task\u0026lt;Richmenu\u0026gt; CreateRichMenuAsync(CreateRichmenu richmenu) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), lineMessageApiBaseUrl)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var postJson = JsonConvert.SerializeObject(richmenu, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } }); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.","title":"切換 Line 的圖文選單 (15)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day14)\n在 Line 的官方帳號可以設定進入時的選單，讓使用者快速點選，設定方式可以從官方帳號後台(有既定的範本可以參考)，或者是用程式自己指定(可以自己控制圖片的座標\u0026amp;設定點選區塊)，以下分別介紹\n從官方帳號後台 這應該是最方便的方式了，Line 已經提供了一些範本和設計規範，只要照著範本製作一個圖片，並設定每個區塊要觸發的事件，即可完成一個圖文選單\n進入 https://manager.line.biz/ 選擇圖文選單 =\u0026gt; 建立 =\u0026gt; 設計規範 =\u0026gt; 下載範本，(或者直接點此下載)，下載後會是一個壓縮檔，今天來用這個範例做修改\n我們用 PhotoShop 改成我們要的圖片 (這就不介紹啦) https://imgur.com/ev4LQzp.jpg\n上傳圖片，並設定 A.B.C 所要觸發的文字\n這樣就完成了，是不是很方便阿\n從程式指定 Rich Menu 程式就沒有用 Business 後台直接上傳圖片來得那麼方便，但程式可以做到極彈性，它可以自訂圖片的座標來設定 click 的範圍，可以針對使用者來動態的切換 Rich Menu 等等…。Rich Menu 所牽扯的 Api 有點多，故先整理今天會實作的 Api 清單如下\nGet rich menu list：取得此 chatbot 已註冊的 Rich Menu 清單 Upload rich menu image：設定 Rich Menu 的圖片 Set default rich menu：設定所檢視的 Rich Menu\n新增 LineRichMenuUtility.cs 之前 Message API 我們做了 一個 Utility ，Rich Menu 我們也新增一個\npublic class LineRichMenuUtility { private readonly string accessToken; private static string lineMessageApiBaseUrl = \u0026#34;https://api.line.me/v2/bot/richmenu\u0026#34;; private static string lineMessageApiBaseUrlForAllUser = \u0026#34;https://api.line.me/v2/bot/user/all/richmenu\u0026#34;; public LineRichMenuUtility(IOptions\u0026lt;LineSetting\u0026gt; lineSetting) { accessToken = lineSetting.Value.ChannelAccessToken; } .... } Get rich menu list：取得此 chatbot 已註冊的 Rich Menu 清單\nRichMenuResponse 這個 class 內容來自於這個文件\npublic async Task\u0026lt;RichMenuResponse\u0026gt; GetRichMenuListAsync() { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}/list\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var response = await httpClient.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); var richMenuRes = JsonConvert.DeserializeObject\u0026lt;RichMenuResponse\u0026gt;(result); return richMenuRes; } } } Upload rich menu image：設定 Rich Menu 的圖片\npublic async Task UploadRichMenuImage(string imgUrl, string richMenuId) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $\u0026#34;lineMessageApiBaseUrl/{richMenuId}/content\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var imgBytes = GetImage(imgUrl); request.Content = new ByteArrayContent(imgBytes); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); var results = await response.Content.ReadAsStringAsync(); }; } } private byte[] GetImage(string url) { var request = (HttpWebRequest)WebRequest.Create(url); var response = (HttpWebResponse)request.GetResponse(); using (Stream dataStream = response.GetResponseStream()) { if (dataStream == null) return null; using (var sr = new BinaryReader(dataStream)) { byte[] bytes = sr.ReadBytes(100000000); return bytes; } } } Set default rich menu：設定預設的 Rich Menu\nCreateRichmenu 這個 class 內容來自於這個文件\n自己用程式定義的麻煩點就在這，要設長寬和這種座標與Action，但未來可以考慮將一些常用範本用程式封裝起來，直接使用\npublic async Task\u0026lt;Richmenu\u0026gt; SetDefaultRichMenuAsync() { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), lineMessageApiBaseUrl)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var defaultRichMenu = new CreateRichmenu(); defaultRichMenu.size = new CreateSize(1200, 810); defaultRichMenu.selected = true; defaultRichMenu.name = \u0026#34;Default RichMenu\u0026#34;; defaultRichMenu.chatBarText = \u0026#34;Kyle\u0026#39;s Wedding\u0026#34;; defaultRichMenu.areas = new List\u0026lt;CreateArea\u0026gt;() { new CreateArea(boundx:0,boundy:0,width:800,height:810, _action: new MessageAction(\u0026#34;婚紗輪播\u0026#34;)) }; var postJson = JsonConvert.SerializeObject(defaultRichMenu, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } }); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); var results = await response.Content.ReadAsStringAsync(); var richmenu = JsonConvert.DeserializeObject\u0026lt;Richmenu\u0026gt;(results); return richmenu; }; } } Set default rich menu：設定所\u0026quot;指定\u0026quot;的 Rich Menu\npublic async Task SetDefaultRichMenuAsync(string richId) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $\u0026#34;{lineMessageApiBaseUrlForAllUser}/{richId}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var response = await httpClient.SendAsync(request); var results = await response.Content.ReadAsStringAsync(); } } } 回到 Api Controller，我們在 app 啟動的時候給設定 Rich Menu\nInitRichMenu 實作\npublic async Task InitRickMenu() { var rickResponse = await lineRichMenuUtility.GetRichMenuListAsync(); if(rickResponse.richmenus.Count == 0) { // 沒有取到任何 Rich Menu，設定預設 var richMenu = await lineRichMenuUtility.SetDefaultRichMenuAsync(); await lineRichMenuUtility.UploadRichMenuImage(\u0026#34;https://i.imgur.com/ev4LQzp.jpg\u0026#34;, richMenu.richMenuId); } else { // 取得預設並設定該 Rich Menu var defaultRich = rickResponse.richmenus.First(c =\u0026gt; c.name == \u0026#34;Default RichMenu\u0026#34;); await lineRichMenuUtility.SetDefaultRichMenuAsync(defaultRich.richMenuId); } 懶人包，本次學到了什麼? 如何取得所有 Rich Menu https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-list 如何上傳 Rich Menu 的圖片 https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image 如何設定 Rich Menu 圖片 https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-14/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day14)\n在 Line 的官方帳號可以設定進入時的選單，讓使用者快速點選，設定方式可以從官方帳號後台(有既定的範本可以參考)，或者是用程式自己指定(可以自己控制圖片的座標\u0026amp;設定點選區塊)，以下分別介紹\n從官方帳號後台 這應該是最方便的方式了，Line 已經提供了一些範本和設計規範，只要照著範本製作一個圖片，並設定每個區塊要觸發的事件，即可完成一個圖文選單\n進入 https://manager.line.biz/ 選擇圖文選單 =\u0026gt; 建立 =\u0026gt; 設計規範 =\u0026gt; 下載範本，(或者直接點此下載)，下載後會是一個壓縮檔，今天來用這個範例做修改\n我們用 PhotoShop 改成我們要的圖片 (這就不介紹啦) https://imgur.com/ev4LQzp.jpg\n上傳圖片，並設定 A.B.C 所要觸發的文字\n這樣就完成了，是不是很方便阿\n從程式指定 Rich Menu 程式就沒有用 Business 後台直接上傳圖片來得那麼方便，但程式可以做到極彈性，它可以自訂圖片的座標來設定 click 的範圍，可以針對使用者來動態的切換 Rich Menu 等等…。Rich Menu 所牽扯的 Api 有點多，故先整理今天會實作的 Api 清單如下\nGet rich menu list：取得此 chatbot 已註冊的 Rich Menu 清單 Upload rich menu image：設定 Rich Menu 的圖片 Set default rich menu：設定所檢視的 Rich Menu\n新增 LineRichMenuUtility.cs 之前 Message API 我們做了 一個 Utility ，Rich Menu 我們也新增一個","title":"Line 的圖文選單 (14)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day13)\nChatbot 有分為兩種類型，一個是任務型導向，另一個是聊天型導向，聊天型導向最典型的就是 Siri，講什麼他都會回答你，這背後所支撐的是海量數據訓練出來的，在我們設計應用 Chatbot 時，多半會偏向任務型導向，使用者用你的 Chatbot 不是要跟你聊天，是要快速的用你這服務達到任務，所以在設計時，如果定位 Chatbot 是 \u0026ldquo;人\u0026rdquo; ，是非常可怕的設計，只會降低使用者的觀感。\n在設計 Line Chatbot 時，如果使用者在對話過程中迷路了，不妨設計一些快捷鍵引導到他的目標吧，這個在 Line 裡面實作叫 Quick Reply，我們今天將做在無法辨識使用者的意圖時，回傳一些引導的快捷鍵\nIntenFactory.cs\n之前有設定一個 DefaultIntent.cs 我們將修改這個實作\n當聽不懂的時候 回傳三個快捷鍵\npublic async Task ReplyAsync(string replyToken) { if (!string.IsNullOrEmpty(replyText)) { await lineMessageUtility.ReplyMessageAsync(replyToken, replyText); } else { // 回傳歡迎詞 var unknow = new TextMessage() { Text = $@\u0026#34;不好意思，我不太清楚您的意思，或許你可以用下列快速鍵 (僅支援手機)\u0026#34; }; // Quick Reply unknow.QuickReply = new QuickReply(); unknow.QuickReply.Items.Add(new QuickReplyItem() { action = new MessageAction(\u0026#34;電子喜帖\u0026#34;), }); unknow.QuickReply.Items.Add(new QuickReplyItem() { action = new MessageAction(\u0026#34;婚紗輪播\u0026#34;), }); unknow.QuickReply.Items.Add(new QuickReplyItem() { action = new MessageAction(\u0026#34;交通資訊\u0026#34;), }); await lineMessageUtility.ReplyMessageAsync(replyToken, new List\u0026lt;IMessage\u0026gt; { unknow }); } } TextMessage 要多一個 QuickReply 的屬性\npublic class QuickReply { public List\u0026lt;QuickReplyItem\u0026gt; Items = new List\u0026lt;QuickReplyItem\u0026gt;(); } public class QuickReplyItem { public string type =\u0026gt; \u0026#34;action\u0026#34;; public string imageUrl { get; set; } public IAction action { get; set; } } 實作效果 懶人包，本次學到了什麼? 商用聊天機器人 UX 的 13 個設計原則(heuristics) https://medium.com/as-a-product-designer/13-heuristics-for-commercial-chatbot-ux-design-58c1aa191c77 如何使用 Quick Reply https://developers.line.biz/en/docs/messaging-api/using-quick-reply/ ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-13/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day13)\nChatbot 有分為兩種類型，一個是任務型導向，另一個是聊天型導向，聊天型導向最典型的就是 Siri，講什麼他都會回答你，這背後所支撐的是海量數據訓練出來的，在我們設計應用 Chatbot 時，多半會偏向任務型導向，使用者用你的 Chatbot 不是要跟你聊天，是要快速的用你這服務達到任務，所以在設計時，如果定位 Chatbot 是 \u0026ldquo;人\u0026rdquo; ，是非常可怕的設計，只會降低使用者的觀感。\n在設計 Line Chatbot 時，如果使用者在對話過程中迷路了，不妨設計一些快捷鍵引導到他的目標吧，這個在 Line 裡面實作叫 Quick Reply，我們今天將做在無法辨識使用者的意圖時，回傳一些引導的快捷鍵\nIntenFactory.cs\n之前有設定一個 DefaultIntent.cs 我們將修改這個實作\n當聽不懂的時候 回傳三個快捷鍵\npublic async Task ReplyAsync(string replyToken) { if (!string.IsNullOrEmpty(replyText)) { await lineMessageUtility.ReplyMessageAsync(replyToken, replyText); } else { // 回傳歡迎詞 var unknow = new TextMessage() { Text = $@\u0026#34;不好意思，我不太清楚您的意思，或許你可以用下列快速鍵 (僅支援手機)\u0026#34; }; // Quick Reply unknow.QuickReply = new QuickReply(); unknow.QuickReply.Items.Add(new QuickReplyItem() { action = new MessageAction(\u0026#34;電子喜帖\u0026#34;), }); unknow.","title":"各種訊息都玩過了，來聊聊使用者體驗 (Quick Reply) (13)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day12)\n這篇要來緩一下，做第一次的重構，寫到現在會發現 LineBotApp.cs 這個 class開始複雜了，程式碼越來越長，在OnMessageAsync.OnPostbackAsync 開始有相同的 Intent 重複定義\n我們把取得 Intent 封裝成一個工廠\nIIntentFactory.cs 定義一個工廠介面\npublic interface IIntentFactory { IReplyIntent GetIntent(string intentName, string userId); } IntentFactory.cs 實作這個介面\npublic class IntentFactory : IIntentFactory { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; public IntentFactory(LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; } public IReplyIntent GetIntent(string intentName, string userId) { var intents = new Dictionary\u0026lt;string, IReplyIntent\u0026gt;() { { \u0026#34;Welcome\u0026#34; , new Welcome(lineMessageUtility,lineProfileUtility,userId)}, { \u0026#34;電子喜帖\u0026#34; , new WeddingInvitationFromImage(lineMessageUtility,lineProfileUtility)}, { \u0026#34;電子喜帖(FlexMsg)\u0026#34; , new WeddingInvitation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;前導影片\u0026#34; , new WeddingTrailer(lineMessageUtility,lineProfileUtility)}, { \u0026#34;婚宴地點\u0026#34; , new WeddingLocation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;婚紗輪播\u0026#34; , new PhotoCarousel(lineMessageUtility,lineProfileUtility)}, { \u0026#34;交通資訊\u0026#34; , new Directions(lineMessageUtility,lineProfileUtility)}, { \u0026#34;從高鐵\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;至 北門站 搭乘松山新店線至 南京復興站 步行7分鐘\u0026#34;)}, { \u0026#34;從捷運\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;捷運台北小巨蛋站6分鐘/南京復興站只需7分鐘\u0026#34;)}, { \u0026#34;從機場\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;酒店距離松山國際機場僅 5 分鐘車程\u0026#34;)}, { \u0026#34;停車資訊\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;酒店的停車場為室內停車場，位於B5及B6\u0026#34;)}, { \u0026#34;開車提醒\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;? 當天有紅酒享用，切記開車不喝酒，快快樂樂平平安安的回家~ ?\u0026#34;)}, { \u0026#34;Default\u0026#34; ,new DefaultIntent(lineMessageUtility,lineProfileUtility)}, }; return intents.ContainsKey(intentName) ? intents[intentName] : intents[\u0026#34;Default\u0026#34;]; } } LinebotApp.cs 注入IIntentFactory，原本的 lineMessageUtility.lineProfileUtility 職責改由工廠裡面使用，LineBotApp 不再處理回覆的職責，這邊也不需要注入了\n記得要在 Startup.cs 注入\nLineBotApp.cs 重構後\n這樣就乾淨多了，也不用相同的 intent 重複寫了兩次，重構到這篇，覺得還夠用，之後實作開始複雜我們再來繼續重構\nprotected virtual async Task OnMessageAsync(Event ev) { if (ev.message.Type.Equals(LineMessageType.text)) { var intent = intentFactory.GetIntent(ev.message.Text, ev.source.userId); await intent.ReplyAsync(ev.replyToken); } } protected virtual async Task OnFollowAsync(Event ev) { var intent = intentFactory.GetIntent(intentName: \u0026#34;Welcome\u0026#34;, ev.source.userId); await intent.ReplyAsync(ev.replyToken); } protected virtual async Task OnUnfollowAsync(Event ev) { // 可以記錄誰封鎖了這個Bot await Task.CompletedTask; } protected virtual async Task OnUnPostbackAsync(Event ev) { var intent = intentFactory.GetIntent(ev.postback.data, ev.source.userId); await intent.ReplyAsync(ev.replyToken); } 懶人包，本次學到了什麼? [Design Pattern] 簡單工廠模式 (Simple Factory Pattern) https://dotblogs.com.tw/joysdw12/2013/06/23/design-pattern-simple-factory-pattern ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-12/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day12)\n這篇要來緩一下，做第一次的重構，寫到現在會發現 LineBotApp.cs 這個 class開始複雜了，程式碼越來越長，在OnMessageAsync.OnPostbackAsync 開始有相同的 Intent 重複定義\n我們把取得 Intent 封裝成一個工廠\nIIntentFactory.cs 定義一個工廠介面\npublic interface IIntentFactory { IReplyIntent GetIntent(string intentName, string userId); } IntentFactory.cs 實作這個介面\npublic class IntentFactory : IIntentFactory { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; public IntentFactory(LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; } public IReplyIntent GetIntent(string intentName, string userId) { var intents = new Dictionary\u0026lt;string, IReplyIntent\u0026gt;() { { \u0026#34;Welcome\u0026#34; , new Welcome(lineMessageUtility,lineProfileUtility,userId)}, { \u0026#34;電子喜帖\u0026#34; , new WeddingInvitationFromImage(lineMessageUtility,lineProfileUtility)}, { \u0026#34;電子喜帖(FlexMsg)\u0026#34; , new WeddingInvitation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;前導影片\u0026#34; , new WeddingTrailer(lineMessageUtility,lineProfileUtility)}, { \u0026#34;婚宴地點\u0026#34; , new WeddingLocation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;婚紗輪播\u0026#34; , new PhotoCarousel(lineMessageUtility,lineProfileUtility)}, { \u0026#34;交通資訊\u0026#34; , new Directions(lineMessageUtility,lineProfileUtility)}, { \u0026#34;從高鐵\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;至 北門站 搭乘松山新店線至 南京復興站 步行7分鐘\u0026#34;)}, { \u0026#34;從捷運\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;捷運台北小巨蛋站6分鐘/南京復興站只需7分鐘\u0026#34;)}, { \u0026#34;從機場\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;酒店距離松山國際機場僅 5 分鐘車程\u0026#34;)}, { \u0026#34;停車資訊\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;酒店的停車場為室內停車場，位於B5及B6\u0026#34;)}, { \u0026#34;開車提醒\u0026#34; , new DefaultIntent(lineMessageUtility,lineProfileUtility,\u0026#34;?","title":"Hold on Hold on 重構一下！ (12)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day11)\n如果前面介紹的訊息類型都不夠怎麼辦？就用 Flex Message 做個客製化的訊息吧，看看官網這些 showcase，4不4很酷阿\n設計魂發作 這些效果，主要都是用 JSON 字串組出來的，官網也很貼心的提供 Simulator 來讓你用GUI的方式來設定自己客製的 Flex Message，之前電子喜帖是用圖片的方式，我們來改成 Flex Message 吧。\n我在 Simulator 拉的訊息圖片如下 (其實蠻好上手的)\n加入行事曆希望引導到 Google Map\n透過 UriAction 即可解決\nhttps://calendar.google.com/calendar/u/0/r/eventedit?text=Kyle%27s%20Wedding\u0026amp;dates=20201208T100000Z/20201208T140000Z\u0026amp;details\u0026amp;location=%E5%8F%B0%E5%8C%97%E6%9D%B1%E6%96%B9%E6%96%87%E8%8F%AF\n點選右上角 View as JSON 就能取得完整的 JSON 字串\n回到 .NET 發送吧，在 LineReplyMessageUtility.cs 實作 ReplyMessageByJsonAsync\npublic async Task ReplyMessageByJsonAsync(string replyToken, string jsonString) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); LineMessageReq req = new LineMessageReq(); req.ReplyToken = replyToken; req.Messages.Add(new FlexMessage() { Contents = JsonConvert.DeserializeObject(jsonString) }); var postJson = JsonConvert.SerializeObject(req, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, Formatting = Formatting.Indented }).Replace(\u0026#34;\\\u0026#34;\u0026#34;,@\u0026#34;\u0026#34;\u0026#34;\u0026#34;); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); } } } FlexMessage 繼承 IMessage\npublic class FlexMessage : IMessage { public LineMessageType Type =\u0026gt; LineMessageType.flex; public string AltText =\u0026gt; \u0026#34;Flex Message\u0026#34;; public dynamic Contents { get; set; } } OnMessageAsync\n實作 WeddingInvitation.cs\n複製的 JSON 字串記得將雙引號取代成兩個雙引號\nawait lineMessageUtility.ReplyMessageByJsonAsync(replyToken, jsonString); 像 FlexMessage 這種複雜的 JSON 我建議就不要用啥C# 型別來組了，只會讓程式碼更噁心而已，如果要動態修改裡面的內容，其實也只是要改字串而已，譬如：\nvar name = \u0026#34;kyle\u0026#34;; var jsonContent = $\u0026#34;hello world, {name}\u0026#34;; 實作效果\n懶人包，本次學到了什麼? 用 Flex Message Simulator 來拉花式訊息，取得 JSON https://developers.line.biz/en/docs/messaging-api/message-types/#template-messages 如何將 Flex Message 的 JSON 做發送 https://developers.line.biz/en/reference/messaging-api/#flex-message 加入 Google 行事曆的小技巧 ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-11/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day11)\n如果前面介紹的訊息類型都不夠怎麼辦？就用 Flex Message 做個客製化的訊息吧，看看官網這些 showcase，4不4很酷阿\n設計魂發作 這些效果，主要都是用 JSON 字串組出來的，官網也很貼心的提供 Simulator 來讓你用GUI的方式來設定自己客製的 Flex Message，之前電子喜帖是用圖片的方式，我們來改成 Flex Message 吧。\n我在 Simulator 拉的訊息圖片如下 (其實蠻好上手的)\n加入行事曆希望引導到 Google Map\n透過 UriAction 即可解決\nhttps://calendar.google.com/calendar/u/0/r/eventedit?text=Kyle%27s%20Wedding\u0026amp;dates=20201208T100000Z/20201208T140000Z\u0026amp;details\u0026amp;location=%E5%8F%B0%E5%8C%97%E6%9D%B1%E6%96%B9%E6%96%87%E8%8F%AF\n點選右上角 View as JSON 就能取得完整的 JSON 字串\n回到 .NET 發送吧，在 LineReplyMessageUtility.cs 實作 ReplyMessageByJsonAsync\npublic async Task ReplyMessageByJsonAsync(string replyToken, string jsonString) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); LineMessageReq req = new LineMessageReq(); req.","title":"設計師魂發作，改造電子喜帖 (11)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day10)\n在上一篇我們用 Template Message 中的 ImageCarousel 實作輪播後，我們再來練習一篇 Template Message，實作到達婚禮現場的各種交通方式資訊，也是先來看看今天的成果\n上一篇有介紹 Action 的 7 種類型，前面幾個操作都是用 MessageAction 來引島使用者，而當點選如何前往時，會直接跳出之前實作的\u0026quot;地址導覽\u0026quot; 的 intent，這就是我們今天會帶到的 postback event\nCarouselTemplate.cs — 實作ITemplate來完成第二種 Template Message 上一篇輪播圖片，這一篇我們來輪播自訂的樣板，CarouselTemplate，一樣繼承 ITemplate 實作\npublic class CarouselTemplate : ITemplate { public CarouselTemplate() { } public CarouselTemplate(List\u0026lt;ColumnMultipleAction\u0026gt; columns) { Columns = columns; } public string Type =\u0026gt; \u0026#34;carousel\u0026#34;; public List\u0026lt;ColumnMultipleAction\u0026gt; Columns { get; set; } } ColumnMultipleAction — 每個樣本的內容 可以設定圖片/標題/副標題…等資訊，\nThumbnailImageUrl: 縮圖URL ImageAspectRatio: 圖片的比例 rectangle(1.51:1) square(1:1) ImageSize: cover(不適應地方會裁切掉) contain(包含全部圖片) ImageBackgroundColor: 圖片區塊的背景色 (loading時候會看到) Title: 標題 Text: 副標題 ImageUrl: 圖片網址 DefaultAction: 預設的Action (這樣就不用一個一個塞進 Actions 裡面) Actions: 如我們前一篇文章所說的，7種Action public class ColumnMultipleAction { /// \u0026lt;summary\u0026gt; /// Max width: 1024px /// Max file size: 1 MB /// JPEG or PNG /// \u0026lt;/summary\u0026gt; public string ThumbnailImageUrl { get; set; } public ImageAspectRatioType ImageAspectRatio { get; set; } = ImageAspectRatioType.rectangle; public ImageSizeType ImageSize { get; set; } = ImageSizeType.cover; public string ImageBackgroundColor { get; set; } = \u0026#34;#FFFFFF\u0026#34;; public string Title { get; set; } public string Text { get; set; } public string ImageUrl { get; set; } public Defaultaction DefaultAction { get; set; } public List\u0026lt;IAction\u0026gt; Actions { get; set; } } 為了開發方便，陸續可以定義一些 Type\nStringEnumConverter 主要是 JSON.NET 的功能，可以在我們從 Enum 轉JSON的時候，轉成字串，而不是該 Enum 的 Index Value\n[JsonConverter(typeof(StringEnumConverter))] public enum ImageAspectRatioType { /// \u0026lt;summary\u0026gt; /// 1.51:1 /// \u0026lt;/summary\u0026gt; rectangle, /// \u0026lt;summary\u0026gt; /// 1:1 /// \u0026lt;/summary\u0026gt; square } [JsonConverter(typeof(StringEnumConverter))] public enum ImageSizeType { cover, contain } Directions.cs - 來實作如何前往 Intent public class Directions : IReplyIntent { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; public Directions(LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; } public async Task ReplyAsync(string replyToken) { var carouselTemplate = new CarouselTemplate(); var columns = new List\u0026lt;ColumnMultipleAction\u0026gt;(); // 輪播兩種 Template, 三種 Action columns.Add(new ColumnMultipleAction() { ThumbnailImageUrl = \u0026#34;https://imgur.com/2u6F3dV.jpg\u0026#34;, Title = \u0026#34;搭乘大眾運輸\u0026#34;, Text = \u0026#34;交通便利好方便~\u0026#34;, Actions = new List\u0026lt;IAction\u0026gt;() { new MessageAction(\u0026#34;從高鐵\u0026#34;), new MessageAction(\u0026#34;從捷運\u0026#34;), new MessageAction(\u0026#34;從機場\u0026#34;) } }); columns.Add(new ColumnMultipleAction() { ThumbnailImageUrl = \u0026#34;https://imgur.com/FGitN5M.jpg\u0026#34;, Title = \u0026#34;自行開車前往\u0026#34;, Text = \u0026#34;切記酒後不開車~\u0026#34;, Actions = new List\u0026lt;IAction\u0026gt;() { new PostbackAction(\u0026#34;如何前往\u0026#34;, \u0026#34;婚宴地點\u0026#34;), new MessageAction(\u0026#34;停車資訊\u0026#34;), new MessageAction(\u0026#34;開車提醒\u0026#34;) } }); carouselTemplate.Columns = columns; await lineMessageUtility.ReplyTemplateMessageAsync(replyToken, carouselTemplate); } } OnMessageAsync\nDefaultIntent 增加一個選擇性參數，當有要傳訊息的時候回傳，這樣我們就不用定義到那麼多的 Intent\npublic class DefaultIntent : IReplyIntent { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; private readonly string replyText; public DefaultIntent( LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility, string _replyText = \u0026#34;\u0026#34;) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; replyText = _replyText; } public async Task ReplyAsync(string replyToken) { if(!string.IsNullOrEmpty(replyText)) { await lineMessageUtility.ReplyMessageAsync(replyToken, replyText); } else { await Task.CompletedTask; } } } Postback Event 要來擴充 Event 了，OnMessage Event已經不夠我們使用，在如何前往那邊我們有塞一個 Action 叫做 PostbackAction，data 可以帶一些程式要溝通的資料，如果想傳的資料太多甚至你可以傳個JSON字串，在反序列化回來成物件，我這邊帶的是 “婚宴地點” 這個字串，用來判斷我要執行這個 Intent\npublic class PostbackAction : IAction { public PostbackAction(string text, string data, string label = \u0026#34;\u0026#34;) { Text = text; Data = data; Label = string.IsNullOrEmpty(label) ? text : label; } public string Data { get; set; } public string Text { get; set; } public ActionType Type =\u0026gt; ActionType.postback; public string Label { get; set; } } 回到 LineBotApp.cs 的 RunAsync\nWebhook 所接收的 Event 要多 postback 這個 class\n實作 OnUnPostbackAsync\nev.postback.data 即可取得上面給的 data ，本範例是 \u0026ldquo;婚宴地點\u0026rdquo;\nNOTE:看到這邊，我們開始嗅到第一個壞味道了，Intent開始定義兩邊，且開始多和複雜，沒關係，我們找篇文章來做重構的動作 JOKE:然後也會發現，透過引導的方式，感覺不用 AI 的技術，也能讓 chatbot 有流程對話，我懂你的感覺 (可以開始包裝我大AI時代了) 有啥限制嗎? CarouselTemplate 在傳送的時候最多只能設定10個輪播 (其實太多也不好，很亂) URL 最大 1000 字元 一定要是 HTTPS 且加密規範 TLS 1.2 以上 檔案大小上限 200 MB 圖片需為 JPEG or PNG 懶人包，本次學到了什麼? 如何用 CarouselTemplate 來發送多張樣本圖片 https://developers.line.biz/en/reference/messaging-api/#carousel 如何用 Postback Event 來帶一些參數 https://developers.line.biz/en/reference/messaging-api/#postback-event 透過 StringEnumConverter 來讓JSON轉換時，將Enum轉成字串，而不是index value https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Converters_StringEnumConverter.htm ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-10/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day10)\n在上一篇我們用 Template Message 中的 ImageCarousel 實作輪播後，我們再來練習一篇 Template Message，實作到達婚禮現場的各種交通方式資訊，也是先來看看今天的成果\n上一篇有介紹 Action 的 7 種類型，前面幾個操作都是用 MessageAction 來引島使用者，而當點選如何前往時，會直接跳出之前實作的\u0026quot;地址導覽\u0026quot; 的 intent，這就是我們今天會帶到的 postback event\nCarouselTemplate.cs — 實作ITemplate來完成第二種 Template Message 上一篇輪播圖片，這一篇我們來輪播自訂的樣板，CarouselTemplate，一樣繼承 ITemplate 實作\npublic class CarouselTemplate : ITemplate { public CarouselTemplate() { } public CarouselTemplate(List\u0026lt;ColumnMultipleAction\u0026gt; columns) { Columns = columns; } public string Type =\u0026gt; \u0026#34;carousel\u0026#34;; public List\u0026lt;ColumnMultipleAction\u0026gt; Columns { get; set; } } ColumnMultipleAction — 每個樣本的內容 可以設定圖片/標題/副標題…等資訊，\nThumbnailImageUrl: 縮圖URL ImageAspectRatio: 圖片的比例 rectangle(1.51:1) square(1:1) ImageSize: cover(不適應地方會裁切掉) contain(包含全部圖片) ImageBackgroundColor: 圖片區塊的背景色 (loading時候會看到) Title: 標題 Text: 副標題 ImageUrl: 圖片網址 DefaultAction: 預設的Action (這樣就不用一個一個塞進 Actions 裡面) Actions: 如我們前一篇文章所說的，7種Action public class ColumnMultipleAction { /// \u0026lt;summary\u0026gt; /// Max width: 1024px /// Max file size: 1 MB /// JPEG or PNG /// \u0026lt;/summary\u0026gt; public string ThumbnailImageUrl { get; set; } public ImageAspectRatioType ImageAspectRatio { get; set; } = ImageAspectRatioType.","title":"婚禮現場交通資訊一覽表 (10)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day9)\n這篇會開始進入一些較複雜的訊息格式，我們要來做一個婚紗輪播，讓各種參加喜宴的朋友瀏覽，今天先來看成果\nTemplate message 在 Line 的 Template 有分為四種，可以用來引導使用者做下一步的動作\nButtons Confirm Carousel Image carousel Template Message 發送的結構如下\n本次要用 ImageCarousel 來實作\n擴充 LineReplyMessageUtility.cs 我們在發送的 class 的擴充\npublic async Task ReplyTemplateMessageAsync(string replyToken, ImageCarouselTemplate template) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); LineMessageReq req = new LineMessageReq(); req.ReplyToken = replyToken; req.Messages.Add(new TemplateMessageBase() { Template = template }); var postJson = JsonConvert.SerializeObject(req, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, Formatting = Formatting.Indented }); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); } } } TemplateMessageBase.cs 定義一個 ITemplate 來用來接受上面四種訊息的格式，要特別注意 TemplateMessage 尚未支援桌面版的 Line，故可以加一些提示文字\npublic class TemplateMessageBase : IMessage { public LineMessageType Type =\u0026gt; LineMessageType.template; public string AltText =\u0026gt; \u0026#34;此訊息不支援桌面版的Line\u0026#34;; public ITemplate Template { get; set; } } 定義 ImageCarouselTemplate 來實作 ITemplate\n一樣看文件將 JSON 轉成 Class\npublic class ImageCarouselTemplate : ITemplate { public string Type =\u0026gt; \u0026#34;image_carousel\u0026#34;; public List\u0026lt;ColumnAction\u0026gt; Columns { get; set; } } ColumnAction.cs\npublic class ColumnAction { public string ImageUrl { get; set; } public IAction Action { get; set; } } IAction 是什麼呢? 又是一個介面 看到我們又定義一個介面，代表又是包含多種實作了，在文章一開始有提到，做 chatbot 引導很重要，在 Line 裡面可以設計一些按鈕，來做一些操作，目前類型共提供 7 種，但本系列文就不一一寫文章介紹 Action 了，相信大家也很會看文件轉 C# 了，遇到功能需要時再來實作\nPostback action：觸發回傳Api事件，這也會是特別的一個 WebHook 事件，同時可以帶一些參數，蠻實用的，我們後幾篇文章再來詳細介紹這個 Message action：引導使用者輸入設定的文字 URI action：網址類型，點擊了會在 Line 開啟所設定的 URL，甚至類型是 tel:{photo number} 也行，會直接引導撥打電話，line:// 開頭會引導開啟 Line 的 Liff App (我們之後文章就會實作到啦~) Datetime picker action：日期選擇器，讓使用者直接選一個日期與時間 Camera action：開啟相機 Camera roll action：開啟相簿 Location action：偵測目前使用者的位置，並讓他們設定實際位置並回傳 以本章需求為例，我們希望可以點擊婚紗的圖片原始檔，我們繼承 IAction 實作一個 UriAction\npublic class UriAction : IAction { /// \u0026lt;param name=\u0026#34;uri\u0026#34;\u0026gt;網址\u0026lt;/param\u0026gt; /// \u0026lt;param name=\u0026#34;label\u0026#34;\u0026gt;顯示文字\u0026lt;/param\u0026gt; public UriAction(string uri, string label) { Uri = uri; Label = label; } public string Uri { get; set; } public ActionType Type =\u0026gt; ActionType.uri; public string Label { get; set; } } 實作婚紗輪播 Intent\npublic class PhotoCarousel : IReplyIntent { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; public PhotoCarousel(LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; } public async Task ReplyAsync(string replyToken) { var imageCarouselTemplate = new ImageCarouselTemplate(); var columns = new List\u0026lt;ColumnAction\u0026gt;(); var photos = new List\u0026lt;string\u0026gt;() { \u0026#34;https://imgur.com/ZChLjeG.jpg\u0026#34;, \u0026#34;https://imgur.com/uElHkw8.jpg\u0026#34;, \u0026#34;https://imgur.com/fbQ5gu4.jpg\u0026#34;, \u0026#34;https://imgur.com/9Gwzi36.jpg\u0026#34;, \u0026#34;https://imgur.com/Y7uBXxb.jpg\u0026#34;, \u0026#34;https://imgur.com/XaCigCi.jpg\u0026#34;, \u0026#34;https://imgur.com/dDcoMPI.jpg\u0026#34;, \u0026#34;https://imgur.com/vn6ZK6f.jpg\u0026#34; }; // 隨機取得三筆 var selected = photos.OrderBy(x =\u0026gt; new Random().Next()).Take(3); foreach (var photo in selected) { columns.Add(new ColumnAction() { ImageUrl = photo, Action = new UriAction(photo, \u0026#34;Learn more\u0026#34;) }); } imageCarouselTemplate.Columns = columns; await lineMessageUtility.ReplyTemplateMessageAsync(replyToken, imageCarouselTemplate); } } LineBotApp.cs\n有啥限制嗎? ImageCarouselTemplate 在傳送的時候最多只能傳10張照片 URL 最大 1000 字元 一定要是 HTTPS 且加密規範 TLS 1.2 以上 檔案大小上限 200 MB 圖片需為 JPEG or PNG 懶人包，本次學到了什麼? 理解 Line 的 Template message 4 種類型 https://developers.line.biz/en/docs/messaging-api/message-types/#template-messages 如何用 ImageCarouselTemplate 來發送多張輪播圖片 https://developers.line.biz/en/reference/messaging-api/#carousel 學到了 Line 有 7 種 Action 類型，來引導使用者點擊，更快速完成目標 https://developers.line.biz/en/reference/messaging-api/#action-objects 如何使用 Linq 取得陣列的隨機項目之小技巧 https://stackoverflow.com/questions/4615410/random-element-of-listt-from-linq-sql ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-9/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day9)\n這篇會開始進入一些較複雜的訊息格式，我們要來做一個婚紗輪播，讓各種參加喜宴的朋友瀏覽，今天先來看成果\nTemplate message 在 Line 的 Template 有分為四種，可以用來引導使用者做下一步的動作\nButtons Confirm Carousel Image carousel Template Message 發送的結構如下\n本次要用 ImageCarousel 來實作\n擴充 LineReplyMessageUtility.cs 我們在發送的 class 的擴充\npublic async Task ReplyTemplateMessageAsync(string replyToken, ImageCarouselTemplate template) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); LineMessageReq req = new LineMessageReq(); req.ReplyToken = replyToken; req.Messages.Add(new TemplateMessageBase() { Template = template }); var postJson = JsonConvert.","title":"來做一些婚紗輪播 (9)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day8)\n籌備婚禮期間甚至婚禮當天(暈)，常常被問的就是問地點在哪了，Line 本身能直接傳送地圖資訊，可以直接引導到 Google Map 進行導航，在 Line 可以透過 LocationMessage 發送\n定義 LocationMessage.cs\npublic class LocationMessage : IMessage { public LineMessageType Type =\u0026gt; LineMessageType.location; public string Title { get; set; } public string Address { get; set; } public decimal Latitude { get; set; } public decimal Longitude { get; set; } } 假設我們今天辦在東方文華 (當然沒那麼有錢)\n地址為 台灣台北市敦化北路 158 號\n經緯度可以在 google map 看到為 @25.0557416,121.5481485\n實作 WeddingLocation.cs\npublic async Task ReplyAsync(string replyToken) { var locationName = \u0026#34;臺北文華東方酒店\u0026#34;; var msg = new TextMessage() { Text = $\u0026#34;我們在 {locationName} 舉辦唷!\u0026#34; }; var locationMessage = new LocationMessage() { Title = $\u0026#34;Kyle\u0026#39;s Wedding - {locationName}\u0026#34;, Address = \u0026#34;台灣台北市敦化北路 158 號\u0026#34;, Latitude = Convert.ToDecimal(25.0557416), Longitude = Convert.ToDecimal(121.5481485) }; await lineMessageUtility.ReplyMessageAsync(replyToken, new List\u0026lt;IMessage\u0026gt; { msg, locationMessage }); } OnMessageAsync\nprotected virtual async Task OnMessageAsync(Event ev) { if (ev.message.Type.Equals(LineMessageType.text)) { // 完整比對使用者輸入的訊息，決定要回傳什麼 var msg = ev.message.Text; var intents = new Dictionary\u0026lt;string, IReplyIntent\u0026gt;() { { \u0026#34;電子喜帖\u0026#34; ,new WeddingInvitation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;前導影片\u0026#34; ,new WeddingTrailer(lineMessageUtility,lineProfileUtility)}, { \u0026#34;婚宴地點\u0026#34; ,new WeddingLocation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;Default\u0026#34; ,new DefaultIntent(lineMessageUtility,lineProfileUtility)}, }; var intent = intents.ContainsKey(ev.message.Text) ? intents[msg] : intents[\u0026#34;Default\u0026#34;]; await intent.ReplyAsync(ev.replyToken); } } 實作效果\n懶人包，本次學到了什麼? 如何回覆 LocationMessage https://developers.line.biz/en/reference/messaging-api/#image-message ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-8/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day8)\n籌備婚禮期間甚至婚禮當天(暈)，常常被問的就是問地點在哪了，Line 本身能直接傳送地圖資訊，可以直接引導到 Google Map 進行導航，在 Line 可以透過 LocationMessage 發送\n定義 LocationMessage.cs\npublic class LocationMessage : IMessage { public LineMessageType Type =\u0026gt; LineMessageType.location; public string Title { get; set; } public string Address { get; set; } public decimal Latitude { get; set; } public decimal Longitude { get; set; } } 假設我們今天辦在東方文華 (當然沒那麼有錢)\n地址為 台灣台北市敦化北路 158 號\n經緯度可以在 google map 看到為 @25.0557416,121.5481485\n實作 WeddingLocation.cs\npublic async Task ReplyAsync(string replyToken) { var locationName = \u0026#34;臺北文華東方酒店\u0026#34;; var msg = new TextMessage() { Text = $\u0026#34;我們在 {locationName} 舉辦唷!","title":"一鍵導航婚宴地點辦在哪 (8)"},{"content":"上一篇我們用 ImageMessage 來傳送圖片，這一篇我們要來實作傳送婚禮的前導預告片 (回想起來那時候我怎麼那麼閒XD )，我當時自己是從淘寶買一些婚禮的影片範本，買回來用 Adobe Effect 來改，或者是網路上可能會有一些免費的 template 也是可以拿來應用\n上傳 Video Line VideoMessage 一樣要透過URL來發送，我們一樣傳到 Imgur，就能產生一個網址，這是我當時所購買的影片範本 https://i.imgur.com/n8QsXTk.mp4\n定義 VideoMessage.cs\npublic class VideoMessage : IMessage { public LineMessageType Type =\u0026gt; LineMessageType.video; public string OriginalContentUrl { get; set; } public string PreviewImageUrl { get; set; } } 一樣在 OnMessageAsync 增加一個 前導影片 的實作\n實作內容\npublic class WeddingTrailer : IReplyIntent { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; public WeddingTrailer(LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; } public async Task ReplyAsync(string replyToken) { var imageMessage = new VideoMessage() { OriginalContentUrl = \u0026#34;https://i.imgur.com/n8QsXTk.mp4\u0026#34;, PreviewImageUrl = \u0026#34;https://i.imgur.com/oLvTjtu.png\u0026#34; }; await lineMessageUtility.ReplyMessageAsync(replyToken, new List\u0026lt;IMessage\u0026gt; { imageMessage }); } } 實作效果 實際上沒有回傳那麼慢，會是很順的，只是手機螢幕錄影時，我忘記我有在 Visual Studio 設中斷點，所以卡住了 QQ\nAudioMessage Line 也有提供聲音檔的 Message Type (AudioMessage)，因為沒需求這篇就不實作了，但基本上很多程式都是共用的，所以只要：\n定義AudioMessage的Class (JSON to C# Class) 增加 Intent 的 key 繼承我們自己定義的 IReplyIntent 介面，並實作 Audio 回覆內容 有啥限制嗎? ImageMessage 在傳送的時候要注意有以下限制\nURL 最大 1000 字元 一定要是 HTTPS 且加密規範 TLS 1.2 以上 檔案大小上限 200 MB 如果太寬或太高的影片可能會有裁切掉的風險 懶人包，本次學到了什麼? 如何回覆 Video/Audio Message https://developers.line.biz/en/reference/messaging-api/#image-message 不會做影片，但知道跟程式一樣可以買範本來改? 雖然 Adobe Effect 也搞了好一陣子….隔行如隔山阿 XD ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-7/","summary":"上一篇我們用 ImageMessage 來傳送圖片，這一篇我們要來實作傳送婚禮的前導預告片 (回想起來那時候我怎麼那麼閒XD )，我當時自己是從淘寶買一些婚禮的影片範本，買回來用 Adobe Effect 來改，或者是網路上可能會有一些免費的 template 也是可以拿來應用\n上傳 Video Line VideoMessage 一樣要透過URL來發送，我們一樣傳到 Imgur，就能產生一個網址，這是我當時所購買的影片範本 https://i.imgur.com/n8QsXTk.mp4\n定義 VideoMessage.cs\npublic class VideoMessage : IMessage { public LineMessageType Type =\u0026gt; LineMessageType.video; public string OriginalContentUrl { get; set; } public string PreviewImageUrl { get; set; } } 一樣在 OnMessageAsync 增加一個 前導影片 的實作\n實作內容\npublic class WeddingTrailer : IReplyIntent { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; public WeddingTrailer(LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; } public async Task ReplyAsync(string replyToken) { var imageMessage = new VideoMessage() { OriginalContentUrl = \u0026#34;https://i.","title":"有聲有色，傳送婚禮前導預告影片 (7)"},{"content":"今天要來做電子喜帖功能，節能減碳，環保愛地球\n主要傳送是用 Line 的 ImageMessage\n這部分跟程式無關 XD 只是分享 template 網站，設計苦手的工程師其實可以考慮一些素材網站，或 template 網站，付點錢就能拿到質感極佳的設計\nhttps://www.greetingsisland.com/\n將圖片傳到圖床 (imgur) Line 發送圖片時候是以 URL，通常我會傳到 imgur 平台產生一個 https 的連結，從上面網站製作好範例喜帖如下：\nOnMessageAsync 在前幾篇我們有介紹的OnMessageAsync，我們在這實作接收到文字訊息時候要回傳的訊息，考量到再來使用者的意圖 (intent) 可能會越來越多，我們用一個簡單的 Dictionary 來做處理\n使用者的意圖 (intent) 是在做chatbot NLP 或使用 chatbot 一些服務時很常看見的名詞，簡單來講就是當使用者打出這句話時，是想做什麼事\n定義一個介面，包含 ReplyAsync 方法，需要傳入回覆所需 replyToken\npublic interface IReplyIntent { Task ReplyAsync(string replyToken); } 接收文字\nprotected virtual async Task OnMessageAsync(Event ev) { if (ev.message.Type.Equals(LineMessageType.text)) { // 完整比對使用者輸入的訊息，決定要回傳什麼 var msg = ev.message.Text; var intents = new Dictionary\u0026lt;string, IReplyIntent\u0026gt;() { { \u0026#34;電子喜帖\u0026#34; ,new WeddingInvitation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;Default\u0026#34; ,new DefaultIntent(lineMessageUtility,lineProfileUtility)}, }; var intent = intents.ContainsKey(ev.message.Text) ? intents[msg] : intents[\u0026#34;Default\u0026#34;]; await intent.ReplyAsync(ev.replyToken); } } 定義 ImageMessage.cs\npublic class ImageMessage : IMessage { public LineMessageType Type =\u0026gt; LineMessageType.image; public string OriginalContentUrl { get; set; } public string PreviewImageUrl { get; set; } } 新增 WeddingInvitation.cs 並實作\npublic class WeddingInvitation: IReplyIntent { private readonly LineReplyMessageUtility lineMessageUtility; private readonly LineProfileUtility lineProfileUtility; public WeddingInvitation(LineReplyMessageUtility _lineMessageUtility, LineProfileUtility _lineProfileUtility) { lineMessageUtility = _lineMessageUtility; lineProfileUtility = _lineProfileUtility; } public async Task ReplyAsync(string replyToken) { var imageMessage = new ImageMessage() { OriginalContentUrl = \u0026#34;https://i.imgur.com/Li0UUul.png\u0026#34;, PreviewImageUrl = \u0026#34;https://i.imgur.com/Li0UUul.png\u0026#34; }; await lineMessageUtility.ReplyMessageAsync(replyToken, new List \u0026lt; IMessage \u0026gt; { imageMessage }); } } 實作效果 有啥限制嗎? ImageMessage 在傳送的時候要注意有以下限制\nURL 最大 1000 字元 一定要是 HTTPS 且加密規範 TLS 1.2 以上 檔案大小上限 200 MB 圖片需為 JPEG or PNG 懶人包，本次學到了什麼? 如何針對多個意圖切分多個實作 如何回覆 ImageMessage https://developers.line.biz/en/reference/messaging-api/#image-message ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-6/","summary":"今天要來做電子喜帖功能，節能減碳，環保愛地球\n主要傳送是用 Line 的 ImageMessage\n這部分跟程式無關 XD 只是分享 template 網站，設計苦手的工程師其實可以考慮一些素材網站，或 template 網站，付點錢就能拿到質感極佳的設計\nhttps://www.greetingsisland.com/\n將圖片傳到圖床 (imgur) Line 發送圖片時候是以 URL，通常我會傳到 imgur 平台產生一個 https 的連結，從上面網站製作好範例喜帖如下：\nOnMessageAsync 在前幾篇我們有介紹的OnMessageAsync，我們在這實作接收到文字訊息時候要回傳的訊息，考量到再來使用者的意圖 (intent) 可能會越來越多，我們用一個簡單的 Dictionary 來做處理\n使用者的意圖 (intent) 是在做chatbot NLP 或使用 chatbot 一些服務時很常看見的名詞，簡單來講就是當使用者打出這句話時，是想做什麼事\n定義一個介面，包含 ReplyAsync 方法，需要傳入回覆所需 replyToken\npublic interface IReplyIntent { Task ReplyAsync(string replyToken); } 接收文字\nprotected virtual async Task OnMessageAsync(Event ev) { if (ev.message.Type.Equals(LineMessageType.text)) { // 完整比對使用者輸入的訊息，決定要回傳什麼 var msg = ev.message.Text; var intents = new Dictionary\u0026lt;string, IReplyIntent\u0026gt;() { { \u0026#34;電子喜帖\u0026#34; ,new WeddingInvitation(lineMessageUtility,lineProfileUtility)}, { \u0026#34;Default\u0026#34; ,new DefaultIntent(lineMessageUtility,lineProfileUtility)}, }; var intent = intents.","title":"環保愛地球，回傳電子喜帖 (6)"},{"content":"我想 Line 豐富的貼圖，應該是很多人喜歡用它來做 Chatbot 的一個原因吧 接續上一篇，我們試著在歡迎詞後，傳送一個禮貌又不尷尬的貼圖\n在 LineReplyMessageUtility.cs 擴充回傳貼圖 我們接續前一篇 LineReplyMessageUtility 這個類別再實作一個回覆貼圖的method\npublic async Task ReplyStickerMessageAsync(string replyToken, string packageId, string stickerId) { using(var httpClient = new HttpClient()) { using(var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $ \u0026#34;{lineMessageApiBaseUrl}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $ \u0026#34;Bearer {accessToken}\u0026#34;); LineMessageReq req = new LineMessageReq(); req.ReplyToken = replyToken; req.Messages.Add(new StickerMessage() { PackageId = packageId, StickerId = stickerId }); var postJson = JsonConvert.SerializeObject(req, new JsonSerializerSettings { ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, Formatting = Formatting.Indented }); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); } } } StickerMessage.cs\npublic class StickerMessage: IMessage { public LineMessageType Type =\u0026gt; LineMessageType.sticker; public string PackageId { get; set; } public string StickerId { get; set; } } ReplyStickerMessageAsync 傳了兩個參數：packageId, stickerId\n這個定義在這份文件裡頭，我們在 OnFollow 事件增加回傳貼圖\nawait lineMessageUtility.ReplyMessageAsync(ev.replyToken, $@\u0026#34;Hi {user.displayName}, 感謝您加入婚禮小助理！\u0026#34;); await lineMessageUtility.ReplyStickerMessageAsync(ev.replyToken, \u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;); (X) 結果發現沒作用，原因是 ReplyToken 只能用一次，且也要注意有30秒左右的時效性\n做點調整 ReplyMessageAsync 改接 IMessage 的 List\n修改歡迎詞的程式\nprotected virtual async Task OnFollowAsync(Event ev) { // 取得使用者的資訊 var user = await lineProfileUtility.GetUserProfile(ev.source.userId); // 回傳歡迎詞 var welcome = new TextMessage() { Text = $@\u0026#34;Hi {user.displayName}, 感謝您加入婚禮小助理！\u0026#34; }; var sticker = new StickerMessage() { PackageId = \u0026#34;11539\u0026#34;, StickerId = \u0026#34;52114136\u0026#34; }; await lineMessageUtility.ReplyMessageAsync(ev.replyToken, new List\u0026lt;IMessage\u0026gt; { welcome, sticker }); } 實際成果\n懶人包，本次學到了什麼? 如何回覆 Line 貼圖 https://developers.line.biz/en/reference/messaging-api/#sticker-message IList 介面 https://docs.microsoft.com/zh-tw/dotnet/api/system.collections.ilist?view=netcore-3.1 ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-5/","summary":"我想 Line 豐富的貼圖，應該是很多人喜歡用它來做 Chatbot 的一個原因吧 接續上一篇，我們試著在歡迎詞後，傳送一個禮貌又不尷尬的貼圖\n在 LineReplyMessageUtility.cs 擴充回傳貼圖 我們接續前一篇 LineReplyMessageUtility 這個類別再實作一個回覆貼圖的method\npublic async Task ReplyStickerMessageAsync(string replyToken, string packageId, string stickerId) { using(var httpClient = new HttpClient()) { using(var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $ \u0026#34;{lineMessageApiBaseUrl}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $ \u0026#34;Bearer {accessToken}\u0026#34;); LineMessageReq req = new LineMessageReq(); req.ReplyToken = replyToken; req.Messages.Add(new StickerMessage() { PackageId = packageId, StickerId = stickerId }); var postJson = JsonConvert.SerializeObject(req, new JsonSerializerSettings { ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, Formatting = Formatting.","title":"回傳不文謅謅的 Line 貼圖 (5)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day4)\n本篇開始我們要來逐步實踐我們的婚禮 Chatbot 了！\n我們預期在加入好友的時候 (OnFollow) 事件時回傳歡迎詞：Hi UserName, 感謝您加入 Kyle 的婚禮小助理！\n取得使用者的資訊 記得上一篇講 Webhook Event 時的結構嗎? 裡面有個 userId\n查看文件後，可以透過 access token 和 userId 來取得用戶資訊\n建立一個 LineProfileUtility.cs 取得使用者資訊 我們建立一個 Class 來實作取得使用者相關程式，讓未來不同的情境可以調用，以下是文件取得使用者範例的Sample\ncurl -v -X GET https://api.line.me/v2/bot/profile/{userId} \\ -H \u0026#39;Authorization: Bearer {channel access token}\u0026#39; 有個小技巧可以把 curl 轉成 C# 的 HttpClinet ，將以上的資訊貼到 https://curl.olsh.me/ 即可轉換完成，再做一些微調就好了\n轉換後如下：\nusing (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), \u0026#34;[https://api.line.me/v2/bot/profile/{userId](https://api.line.me/v2/bot/profile/{userId)}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer {channel access token}\u0026#34;); var response = await httpClient.SendAsync(request); } } access token 在幾天前的文章，我們已經定義在 appsetting.json 了，在建構的時候我們注入進來\n新增一個 GetUserProfile function，實作如下\npublic async Task\u0026lt;UserProfile\u0026gt; GetUserProfile(string userId) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}/{userId}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); var response = await httpClient.SendAsync(request); if(response.StatusCode != HttpStatusCode.OK) { // 這邊未來應該要 log 起來 throw new Exception(\u0026#34;get_profile_error\u0026#34;); } var result = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject\u0026lt;UserProfile\u0026gt;(result); } } } UserProfile.cs 定義如下\npublic class UserProfile { public string userId { get; set; } public string displayName { get; set; } public string pictureUrl { get; set; } public string statusMessage { get; set; } } JSON.NET 轉換\n在 .NET Core 3.1 其實有內建 System.Text.Json 來做JSON的序列化跟反序列化處理，但個人還是習慣用 JSON.NET\nJsonConvert.DeserializeObject\u0026lt;UserProfile\u0026gt;(result) 您可在專案以下打開 Nuget 套件管理\n安裝 JSON.NET 回來\n在 OnFollow 事件取得使用者資訊\n建立一個 LineReplyMessageUtility.cs 來回覆訊息 我們建立一個 Class 來實作回覆使用者相關程式，以下是官方文件回覆使用者範例的Sample\ncurl -v -X POST https://api.line.me/v2/bot/message/reply \\ -H \u0026#39;Content-Type: application/json\u0026#39; \\ -H \u0026#39;Authorization: Bearer {channel access token}\u0026#39; \\ -d \u0026#39;{ \u0026#34;replyToken\u0026#34;:\u0026#34;nHuyWiB7yP5Zw52FIkcQobQuGDXCTA\u0026#34;, \u0026#34;messages\u0026#34;:[ { \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;Hello, user\u0026#34; }, { \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;May I help you?\u0026#34; } ] }\u0026#39; 用 https://curl.olsh.me/ 轉成 C#\nusing (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), \u0026#34;https://api.line.me/v2/bot/message/reply\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer {channel access token}\u0026#34;); request.Content = new StringContent(\u0026#34;{\\n \\\u0026#34;replyToken\\\u0026#34;:\\\u0026#34;nHuyWiB7yP5Zw52FIkcQobQuGDXCTA\\\u0026#34;,\\n \\\u0026#34;messages\\\u0026#34;:[\\n {\\n \\\u0026#34;type\\\u0026#34;:\\\u0026#34;text\\\u0026#34;,\\n \\\u0026#34;text\\\u0026#34;:\\\u0026#34;Hello, user\\\u0026#34;\\n },\\n {\\n \\\u0026#34;type\\\u0026#34;:\\\u0026#34;text\\\u0026#34;,\\n \\\u0026#34;text\\\u0026#34;:\\\u0026#34;May I help you?\\\u0026#34;\\n }\\n ]\\n}\u0026#34;); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); } } 做點調整：將 StringContent 變成class，並用 JSON.NET序列化，完整實作如下：\nprivate readonly string accessToken; private static string lineMessageApiBaseUrl = \u0026#34;[https://api.line.me/v2/bot/message/reply](https://api.line.me/v2/bot/message/reply)\u0026#34;; public LineReplyMessageUtility(IOptions\u0026lt;LineSetting\u0026gt; lineSetting) { accessToken = lineSetting.Value.ChannelAccessToken; } public async Task ReplyMessageAsync(string replyToken, params string[] messages) { using (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;POST\u0026#34;), $\u0026#34;{lineMessageApiBaseUrl}\u0026#34;)) { request.Headers.TryAddWithoutValidation(\u0026#34;Authorization\u0026#34;, $\u0026#34;Bearer {accessToken}\u0026#34;); LineMessageReq req = new LineMessageReq(); req.ReplyToken = replyToken; foreach (var message in messages) { req.Messages.Add(new TextMessage() { Text = message }); } var postJson = JsonConvert.SerializeObject(req, new JsonSerializerSettings { ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() //轉小寫 }, Formatting = Formatting.Indented }); request.Content = new StringContent(postJson); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(\u0026#34;application/json\u0026#34;); var response = await httpClient.SendAsync(request); } } } LineMessageReq 這邊定義一個 IMessage 的介面，之後不同的訊息類型，將會以此來繼承\npublic class LineMessageReq { public string ReplyToken { get; set; } public List\u0026lt;IMessage\u0026gt; Messages { get; set; } = new List\u0026lt;IMessage\u0026gt;(); } TextMessage Class\npublic class TextMessage : IMessage { public string Text { get; set; } public LineMessageType Type =\u0026gt; LineMessageType.text; } 於 OnFollowAsync 加入 Reply\nprotected virtual async Task OnFollowAsync(Event ev) { // 取得使用者的資訊 var user = await lineProfileUtility.GetUserProfile(ev.source.userId); // 回傳歡迎詞 await lineMessageUtility.ReplyMessageAsync(ev.replyToken, $@\u0026#34;Hi {user.displayName}, 感謝您加入婚禮小助理！\u0026#34;) } 如何測試加入好友事件呢? 其實只要封鎖再解除封鎖就好了\n實作效果\n懶人包，本次學到了什麼? 如何取得 Line 使用者資訊 https://developers.line.biz/en/docs/social-api/getting-user-profiles/ 如何回覆 Line 使用者訊息 https://developers.line.biz/en/reference/messaging-api/#send-reply-message 如何把 curl 轉成 C# HttpClient https://curl.olsh.me/ C# 的 介面 interface (C# Reference) JSON.NET https://www.newtonsoft.com/json ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-4/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day4)\n本篇開始我們要來逐步實踐我們的婚禮 Chatbot 了！\n我們預期在加入好友的時候 (OnFollow) 事件時回傳歡迎詞：Hi UserName, 感謝您加入 Kyle 的婚禮小助理！\n取得使用者的資訊 記得上一篇講 Webhook Event 時的結構嗎? 裡面有個 userId\n查看文件後，可以透過 access token 和 userId 來取得用戶資訊\n建立一個 LineProfileUtility.cs 取得使用者資訊 我們建立一個 Class 來實作取得使用者相關程式，讓未來不同的情境可以調用，以下是文件取得使用者範例的Sample\ncurl -v -X GET https://api.line.me/v2/bot/profile/{userId} \\ -H \u0026#39;Authorization: Bearer {channel access token}\u0026#39; 有個小技巧可以把 curl 轉成 C# 的 HttpClinet ，將以上的資訊貼到 https://curl.olsh.me/ 即可轉換完成，再做一些微調就好了\n轉換後如下：\nusing (var httpClient = new HttpClient()) { using (var request = new HttpRequestMessage(new HttpMethod(\u0026#34;GET\u0026#34;), \u0026#34;[https://api.line.me/v2/bot/profile/{userId](https://api.line.me/v2/bot/profile/{userId)}\u0026#34;)) { request.","title":"回覆第一個Line的訊息-歡迎詞 (4)"},{"content":"第12 屆iT邦幫忙鐵人賽系列文章 (Day3)\n先來看看文件 在 Line 的文件裡有定義 Webhook Event Objects 傳過來的 JSON 的結構\n可以看到它會傳一個 events 過來，裡面包含幾個 properties\nreplyToken : 用來回覆該使用者的Token type：使用者傳送訊息的類型 (message/video/sticker…) mode: Channel是否有效 source:使用者/群組的資訊 … 以上都可以從上方的文件去查看 properties 的用途\n將 Line Event Request 轉成 C# Class 在上一篇我們 Web Api 是用 dynamic 來接收 Request，在VS有個功能是很快的把 JSON 結構轉成 C# 的 Class，這個技巧在串接異質 Api 蠻常用的，或者是可以用 json2csharp.com 這種服務來處理。\n複製官方的JSON Sample，在 VS 點選編輯 =\u0026gt; 選擇性貼上 =\u0026gt; 貼上 JSON\n結果會長這樣，通常我不會在一個 .cs 放多個類別，但為了文章好呈現故直接貼上來，拆一拆檔案的工也還好\n做點加工，把 Event 的 Type 一些文件改成 C# 的 Enum，讓開發更順暢，新增一個 WebhookEventType.cs\npublic enum WebhookEventType { Message, Follow, Unfollow, } Event class 就會改為如下\npublic class Event { public WebhookEventType type { get; set; } public string replyToken { get; set; } public WebhookEventSource source { get; set; } public long timestamp { get; set; } public string mode { get; set; } public LineMessage message { get; set; } } 再做一次，我們將訊息的類型也定義 LineMessageType.cs\npublic enum LineMessageType { text, sticker, image, video, audio, template, flex, location } Message Class 就會改為如下\npublic class Message { public string id { get; set; } public string type { get; set; } public LineMessageType text { get; set; } } 試著從Line Develop Console後台，將 Chatbot 加入好友後，發送第一個訊息，並將 Controller 的 Api 的參數改成剛剛建立的 WebhookEvent Class，可以開始用強型別開發了\n針對不同事件，做不同流程 讓 Controller 不要那麼複雜，我們新增一個 LineBotApp.cs，定義一個 RunAsync來處理各種事件\n有型別的好處就是在switch(e.type)按下 VS 就會自動展開所有 Enums\n針對不同事件，加入實作的 function，我們用 virtual 保留未來繼承這個 LineBotApp.cs 可以 override 的彈性\nOnMessageAsync 可以再針對使用者所傳的類型處理\n其他 Events 上面實作了 Message/Follow/UnFollow的Event，還有其他事件，為求簡單，故等我們有必要用到的時候再來實做它吧\nUnsend event：當使用者收回訊息的時候 Join event：當 Bot加入群組的時候 Leave event：當 Bot離開群組的時候 Member join event：當有新成員加入群組的時候 Member leave event：當有新成員離開群組的時候 Postback event：當使用者有點選Bot設計的流程按鈕的事件 (這應該會蠻常用) Video viewing complete event：影片看完時候的 Beacon event：觸發Beacon的時候 Account link event：當使用者Line帳號跟Bot連接時 (通常用於異質系統與Line Bot連結) Device link event：裝置連結 ( IoT相關，Line Things API) Device unlink event：裝置取消連結 ( IoT相關，Line Things API)\n懶人包，本次學到了什麼? 理解 Line Webhook 所定義的 Event 類型 Line Webhook Event Objects 學會看文件，從官方JSON Sample 轉成 C# 的 Class (by VS or https://json2csharp.com/) C# Enum 的用法 Enumeration types (C# reference) C# 裡的 virual class [C#] 利用 interface(介面) abstract(抽象) override(覆寫) inherit(繼承) 實作簡單範例 ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-3/","summary":"第12 屆iT邦幫忙鐵人賽系列文章 (Day3)\n先來看看文件 在 Line 的文件裡有定義 Webhook Event Objects 傳過來的 JSON 的結構\n可以看到它會傳一個 events 過來，裡面包含幾個 properties\nreplyToken : 用來回覆該使用者的Token type：使用者傳送訊息的類型 (message/video/sticker…) mode: Channel是否有效 source:使用者/群組的資訊 … 以上都可以從上方的文件去查看 properties 的用途\n將 Line Event Request 轉成 C# Class 在上一篇我們 Web Api 是用 dynamic 來接收 Request，在VS有個功能是很快的把 JSON 結構轉成 C# 的 Class，這個技巧在串接異質 Api 蠻常用的，或者是可以用 json2csharp.com 這種服務來處理。\n複製官方的JSON Sample，在 VS 點選編輯 =\u0026gt; 選擇性貼上 =\u0026gt; 貼上 JSON\n結果會長這樣，通常我不會在一個 .cs 放多個類別，但為了文章好呈現故直接貼上來，拆一拆檔案的工也還好\n做點加工，把 Event 的 Type 一些文件改成 C# 的 Enum，讓開發更順暢，新增一個 WebhookEventType.","title":"用.NET Core 接收Line的訊息事件 (3)"},{"content":"基本設定 關於如何註冊一個串接自己Web Api 的 Line Chatbot 網路上已經很多的教學文了，大概分為以下幾個步驟\nhttps://developers.line.biz/en/\n申請一個Provider 申請一個Channel (一個Provider可以有多個Channel) 於Channel取得 Channel Secret 及 Access Token Use webhook 啟用 Auto-reply messages 關閉 設定 Webhook URL (需為https) 起始 Web API 專案與 Line Server 做連結 建立一個 .NET Core 3.1 的 Web Api 專案\n建立一個 Line Controller，先用 dynamic 接收，看看 Request 過來的是什麼\n執行看看，weatherforecast 為初始專案的 Sample，此Local Api 的 Port 為 44300\n透過 ngrok 讓 Line Server 跟 Local Web API 做連結\n安裝 node.js npm install ngrok -g ngrok -v 本文章所使用的版本為 2.3.35\n執行 ngrok http 44300 -host-header=”localhost:44300\u0026quot; -region ap\n44300要替換成你本地執行Run起來的Port\n-region ap 為切換 ngrok 的 server，預設為us，在今年 Line 開始阻擋來至於 ngrok us 的 server 了，所以我們要將 Region 切到 Asia/Pacific (ap)\nRun 起來大概會長這樣\n我們將 https://44a914411bf2.ap.ngrok.io 這串加上Route api/line 後貼到 Line Developer Console，點選 Verify\n進入 http://127.0.0.1:4040/ ，可以看到Line Server POST 過來的 Request\n把這個POST 的 Body 移到 Postman 方便測試\n也可以直接用我建好的 Collection 匯入 https://www.getpostman.com/collections/8a93e0b6ce3ff66f8c8b\n透過 Postman 測試，在VS下中斷點，驗證可接收到\n驗證是否為有效的Line來源 API 有了，但我們要如何驗證這個 Request 是來自 Line Server 來阻擋惡意攻擊呢? 在 Line Server 所傳過來的 Header 會包含這個項目 **X-Line-Signature ，**將 POST Body 跟 Chanel Secret 用做 HMAC-SHA256 演算法Hash過後，比對過後如果相同，表示該 Request 是來自 Line Server\n*Verifying Signatures 官方參考文件*\n在 Web Api 實作，我們先新建一個 Authorization Filter 來驗證 Line Signature，.NET Core Web Api 的 Reuqest pipeline 如下：\n新增一個 Filter 叫 LineVerifySignatureFilter.cs 繼承 IAuthorizationFilter 實作，Channel Secret 要替換\npublic class LineVerifySignatureFilter : IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context) { /* * Ensure the requestBody can be read multiple times. * [https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestrewindextensions.enablebuffering?view=aspnetcore-3.1](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestrewindextensions.enablebuffering?view=aspnetcore-3.1) */ context.HttpContext.Request.EnableBuffering(); string requestBody = new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync().Result; context.HttpContext.Request.Body.Position = 0; var xLineSignature = context.HttpContext.Request.Headers[\u0026#34;X-Line-Signature\u0026#34;].ToString(); var channelSecret = Encoding.UTF8.GetBytes(\u0026#34;{Channel Secret}\u0026#34;); var body = Encoding.UTF8.GetBytes(requestBody); using (HMACSHA256 hmac = new HMACSHA256(channelSecret)) { var hash = hmac.ComputeHash(body, 0, body.Length); var xLineBytes = Convert.FromBase64String(xLineSignature); if (SlowEquals(xLineBytes, hash) == false) { context.Result = new ForbidResult(); } } } private static bool SlowEquals(byte[] a, byte[] b) { uint diff = (uint)a.Length ^ (uint)b.Length; for (int i = 0; i \u0026lt; a.Length \u0026amp;\u0026amp; i \u0026lt; b.Length; i++) diff |= (uint)(a[i] ^ b[i]); return diff == 0; } } 新增一個 Line LineVerifySignatureAttribute.cs 透過 TypeFilterAttribute 來例化\npublic class LineVerifySignatureAttribute : TypeFilterAttribute { public LineVerifySignatureAttribute() : base(typeof(LineVerifySignatureFilter)) { } } 在 Line Api Route 加上 [LineVerifySignature] 進行驗證，這樣程式就乾淨多了\n把 Channel Secret/Access Token 移到 appsettings.json 通常我們會把這兩個資訊寫在設定檔，以便使用跟替換，在 .NET Core 可以使用 IOption 的方式來從設定檔注入\n在 appsettings.json 增加區塊，放Channel Secret/Access Token\n增加一個 LineSetting.cs，我們要用強型別來開發\nstartup.cs 增加 services.AddOptions()\n回到前面建立的 LineVerifySignatureFilter.cs，注入 LineSetting及使用 ChannelSecret 即可完成\n懶人包，本次學到了什麼? 如何申請一個 Line Chatbot - 參考上方6步驟 LINE Developers 如何建立一個 .NET Core Web Api 專案 Building Your First Web API with ASP.NET Core MVC and Visual Studio 用 CLI 建立 .NET Core 如何將 Web API 跟 Line Server 連結起來 Install ngrok by npm 如何驗證有效的Line Request Line Signature Validation 如何使用 .NET Core 的 Filters \u0026amp; Attributes Filters in ASP.NET Core 如何在.NET Core讀取設定檔 Options pattern in ASP.NET Core ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-2/","summary":"基本設定 關於如何註冊一個串接自己Web Api 的 Line Chatbot 網路上已經很多的教學文了，大概分為以下幾個步驟\nhttps://developers.line.biz/en/\n申請一個Provider 申請一個Channel (一個Provider可以有多個Channel) 於Channel取得 Channel Secret 及 Access Token Use webhook 啟用 Auto-reply messages 關閉 設定 Webhook URL (需為https) 起始 Web API 專案與 Line Server 做連結 建立一個 .NET Core 3.1 的 Web Api 專案\n建立一個 Line Controller，先用 dynamic 接收，看看 Request 過來的是什麼\n執行看看，weatherforecast 為初始專案的 Sample，此Local Api 的 Port 為 44300\n透過 ngrok 讓 Line Server 跟 Local Web API 做連結\n安裝 node.js npm install ngrok -g ngrok -v 本文章所使用的版本為 2.","title":"用.NET Core接收第一個Line訊息 (2)"},{"content":"寫在開始之前，目的與動機 距離上次參加鐵人賽已經是 2013-2014 的事，很慶幸自己還是活在這行且保有一些技術的熱情還沒死掉。2018 年完成了人生的婚禮，因為個性屬於比較喜歡搞一些特別的，把自己的想法用技術實踐，所以從做影片到還製作婚禮活動官網到婚禮現場都規劃一些比較不一樣的體驗，身為一個技術宅寫一些程式是必須的，所以那時候做了一個當時很流行的 Line Chatbot，整個婚禮報名流程/推播訊息都是透過Line Chatbot完成，幾個朋友回饋都覺得比起一些問卷網站，這種方式還蠻新奇的，後續一些辦婚禮的朋友都還來詢問當時 Line 的一些功能是怎麼用的，也想透過這種方式來玩。2年也過去了，Line 的 Chatbot 也改版了不少，故這次想要藉由鐵人賽，再逐步的好好再把 Line 的文件看過一遍，並重新開發，透過這過程也分享一些當時的一些尚未做的想法實踐\n先來聊聊一個基本的 Line 對話的流程大概會遇到什麼事情\n使用者發送訊息，向 Line Server 發送一個 Request Line Server 會知道是哪個 Chatbot 發送的，經過格式處理後會發送到該Chatbot作者所設定的 Webhook Webhook (自己所開發的Web Api)，能接收到 Line Server 所傳的結構化Request，能知道使用者發送了什麼訊息，應該要做什麼相對應的處理 回傳相對應處理的訊息，告訴 Line Server，我們要回覆什麼 透過 Line Server 再回傳給使用者 目標 在這個 Line Chatbot 我們將實作婚禮前的資訊查詢，快速建立各種能理解聊天意圖的回覆，宴客報名… 等等，透過這些功能的實作，我們將會學到，如何用 Line 來傳各式各樣的豐富訊息，學到如何開發一個網頁來搭配 Line Liff 來做一些小程序，在婚禮會現場會嘗試用 QR Code/iBeacon 搭配婚禮的報到，嘗試用 Line Pay 來做紅包的電子支付，婚禮現場做一些互動的功能…\n所用到的技術預計採用 .NET Core 3.1、Html/CSS/JavaScript、Line API 、Azure的一些服務…等等 (但寫這篇文章時完全還沒實作XD…只能說待訂)\n也發現在 Line 官方都沒有 .NET 的 SDK，但台灣有董大偉老師所封裝的LineBotSDK ，日本有pierre3所開源的 LineMessagingApi，但在這 30 天過程也會希望從 Line 的官方文件出發，逐步的封裝成自己的 C# 的函式庫，方便自己未來修改，也放到 Nuget Server/Github 提供給需要的人。\n索引 這邊下面會陸續把文章索引加上來，供未來連結用，也希望自己能完成30的挑戰!\nDay 1 用.NET Core與Line Bot製作婚禮機器人 Day 2 用.NET Core接收第一個Line訊息 Day 3 用.NET Core接收Line的訊息事件 Day 4 回覆第一個Line的訊息-歡迎詞 Day 5 回傳不文謅謅的 Line 貼圖 Day 6 環保愛地球，回傳電子喜帖 Day 7 有聲有色，傳送婚禮前導預告影片 Day 8 一鍵導航婚宴地點辦在哪 Day 9 來做一些婚紗輪播 Day 10 婚禮現場交通資訊一覽表 Day 11 設計師魂發作，改造電子喜帖 Day 12 Hold on Hold on 重構一下！ Day 13 各種訊息都玩過了，來聊聊使用者體驗 (Quick Reply) Day 14 Line 的圖文選單 Day 15 切換 Line 的圖文選單 Day 16 來點AI感，用QnA Maker來搞Line Chatbot回覆 Day 17 Line Liff App v2 - 用 Vue.js 來實作 Day 18 Line Liff App v2 — 用 Vue.js 來做報名頁面 — 用 Vue.js 來做報名頁面 Day 19 Line Liff App v2 — 用 Azure Table 儲存報名資訊 Day 20 主動推播Line訊息給某個使用者(Push Message) Day 21 當有人報名的時候接收通知 (Line Notify) Day 22 用QR Code來搞報到 - Razor Page實做 I Day 23 用QR Code來搞報到 — Razor Page實做 II Day 24 用Line Beacon來搞自動報到 Day 25 婚禮禮金用Line Pay I Day 26 婚禮禮金用Line Pay I Day 27 收集婚禮現場照片- 將圖片用C#傳到 Imgur 服務 Day 28 製作婚禮現場即時留言版- Azure SignalR Service I Day 29 製作婚禮現場即時留言版- Azure SignalR Service II Day 30 鐵人賽完賽整理\u0026amp;開源 ","permalink":"https://kyleap.github.io/it-ironman-line-chatbot-wedding-1/","summary":"寫在開始之前，目的與動機 距離上次參加鐵人賽已經是 2013-2014 的事，很慶幸自己還是活在這行且保有一些技術的熱情還沒死掉。2018 年完成了人生的婚禮，因為個性屬於比較喜歡搞一些特別的，把自己的想法用技術實踐，所以從做影片到還製作婚禮活動官網到婚禮現場都規劃一些比較不一樣的體驗，身為一個技術宅寫一些程式是必須的，所以那時候做了一個當時很流行的 Line Chatbot，整個婚禮報名流程/推播訊息都是透過Line Chatbot完成，幾個朋友回饋都覺得比起一些問卷網站，這種方式還蠻新奇的，後續一些辦婚禮的朋友都還來詢問當時 Line 的一些功能是怎麼用的，也想透過這種方式來玩。2年也過去了，Line 的 Chatbot 也改版了不少，故這次想要藉由鐵人賽，再逐步的好好再把 Line 的文件看過一遍，並重新開發，透過這過程也分享一些當時的一些尚未做的想法實踐\n先來聊聊一個基本的 Line 對話的流程大概會遇到什麼事情\n使用者發送訊息，向 Line Server 發送一個 Request Line Server 會知道是哪個 Chatbot 發送的，經過格式處理後會發送到該Chatbot作者所設定的 Webhook Webhook (自己所開發的Web Api)，能接收到 Line Server 所傳的結構化Request，能知道使用者發送了什麼訊息，應該要做什麼相對應的處理 回傳相對應處理的訊息，告訴 Line Server，我們要回覆什麼 透過 Line Server 再回傳給使用者 目標 在這個 Line Chatbot 我們將實作婚禮前的資訊查詢，快速建立各種能理解聊天意圖的回覆，宴客報名… 等等，透過這些功能的實作，我們將會學到，如何用 Line 來傳各式各樣的豐富訊息，學到如何開發一個網頁來搭配 Line Liff 來做一些小程序，在婚禮會現場會嘗試用 QR Code/iBeacon 搭配婚禮的報到，嘗試用 Line Pay 來做紅包的電子支付，婚禮現場做一些互動的功能…\n所用到的技術預計採用 .NET Core 3.1、Html/CSS/JavaScript、Line API 、Azure的一些服務…等等 (但寫這篇文章時完全還沒實作XD…只能說待訂)\n也發現在 Line 官方都沒有 .NET 的 SDK，但台灣有董大偉老師所封裝的LineBotSDK ，日本有pierre3所開源的 LineMessagingApi，但在這 30 天過程也會希望從 Line 的官方文件出發，逐步的封裝成自己的 C# 的函式庫，方便自己未來修改，也放到 Nuget Server/Github 提供給需要的人。","title":"用.NET Core與Line Bot製作婚禮機器人 (1)"},{"content":"新創事業為何如此艱難? 經濟部中小企業處提供數據：創業一年內就倒閉的機率高達90%，而存活下來的10%中，又有90%會在五年內倒閉。為何做產品那麼難? 有沒有一個框架能遵循，降低風險，精實創業是一個可以參考的方法論\n精實計畫淬鍊成三個步驟 文件化你的初始計畫 識別風險 有系統的試驗計劃 新創會失敗在於跟顧客脫鉤，落於太自我的創新上，直接「走出辦公室」跟顧客面對面吧\n精實畫布收斂想法 先寫下願景，譬如阿里巴巴「讓天下沒有難做的生意」、馬斯克「讓人人都可上月球」，沒有願景則無法往下細拆策略目標 精實畫布是一個迅速、簡明、具可攜性的方式，用來產出「商業模式」\n圖片來源：https://ryanisagoodguy.blogspot.com/2016/03/lean-canvas.html\nStep 1~2 — 問題與顧客區隔 列出三項痛點，並目前這些痛點有何既有替代方案，識別哪些人會接觸到此服務，並試著專注於可能的早期採用者，主流顧客在早期產品通常無法滿足\nStep 3 — 獨特價值主張 「銷售」是一種對話，不管什麼身分.職位其實都是在做銷售 e.g 面試過程其實就是在銷售給面試官；而面試官其實在銷售給優秀人才。擬定好故事，從問題出發，擬定好如何在30秒內闡述獨特主張\nStep 4 — 解決方案 針對問題，「草擬」解決方案，為何是草擬？因為會隨著市場變化或經實驗及訪談，會重新排序優先順序，甚至會被新問題取代\nStep 5 — 通路 無法建立接觸「足夠」顧客的途徑，是初期創業失敗的原因，個人認為2C早期應至少接觸上百位顧客，2B需找出2~3個指標客戶。\n免費的最貴，本書作者 Eric Ries 以他這本書為例實踐精實創業，他一天花5塊錢美金，以每次點擊 5 分錢的成本吸引 100 次點擊\n通道要Pull (SEO.Blog…) or Push (活動.電話行銷…)，作為學習，直接銷售最有效；作為營利，直接銷售超過你的薪資整體薪酬才有意義\n避免太早建立策略型夥伴，同理銷售人員只能執行你的計劃，非建立計畫。故初期一定得親自販售你的產品 病毒式行銷前提是產品「值得」這樣做\nStep 6~7 — 收益來源 \u0026amp; 成本結構 思考定價策略：成本導向、競爭導向、需求導向、差異訂價法、特殊的訂價法(SaaS產品定價)…\n成本如何擬定：\n訪談30–50顧客將耗費多少成本 開發MVP將耗費多少成本 (e.g 員工薪資 x 1.6 ~2.0倍) 固定成本(薪資…等).變動成本(雲端費用…等) Step 8 — 關鍵指標 顧客獲取 顧客啟用 顧客續約 收益 轉介 延伸閱讀：從 AARRR 模型幫助對焦商業目標與設計策略\n## Step 9 — 不公平競爭優勢 不公平競爭優勢代表無法讓人複製( Copy-Cat)的東西，可以往建立生態系為目標，當個有影響力的公司.產品，可往以下幾個面向思考：掌握內幕訊息、建立強力團隊、打造個人品牌、社群機制、既有顧客數、指標客戶\n辨識風險，降低風險\n產品風險 (Product Risk) 如何降低產品風險：確認有值得解決問題，定義最小的可能解決方案，小規模驗證MVP、大規模驗證\n顧問風險 (Customer Risk) 顧客風險：識別誰有痛苦，早期採用者，推播式通道，逐步擴產Pull通道\n市場風險 (Market Risk) 市場風險：識別競業．口頭訪談測試定價，客戶行為測試定價，最佳化成本結構，讓商業模式有效運作\n##　價值交付循環 擬定畫布完，開始做實驗，可用以下循環做產品的持續交付\n圖片來源: https://novapoints.com/2017/10/27/summary-of-eric-ries-the-startup-way/\n點子 - 做可行性的假設 避免太含糊的假設，避免太含糊地假設：e.g 做一個「好銷售」的功能，但如何定義「好銷售」\n可利用下列公式來擬訂假設\n可否證的假設 = [具體的可重複動作] 將 [預期的可量測結果]\n一篇部落格文章可驅使 100 個人註冊\n組織團隊 - 建立與開發產品 打破職責與部門標籤，可分為問題團隊.解決方案團隊即可，盡可能從最小團隊開始，約 2~3 人，其中三個必要功能：開發.設計.行銷。早期初創產品避免外包，因會限制快速反覆迭代與經學習的能力\n測量 - 定性\u0026amp;定量\n如果有許多不確定性存在，不須用大量資料來讓不確定性降低，但有許多確定性存在，需要用大量資料讓不確定性顯著降低。譬如訪談5個都不可行，那也不用堅持此商業模式，該改變方向，累積到一定的認可，得用大量的資料來與關鍵指標做驗證\n測量 - 訪談 「走出辦公室」為精實創業的重點，一定要跟客戶面對面，避免用問卷調查或焦點團體法。問卷調查時常會引導顧客進你預設的假設，焦點訪談法會移轉到群體思維，無法挖掘顧客內心真正的需求。\n別問顧客要什麼，要評估他們現行做什麼\n小節 我一直覺得創業家需要很大的勇氣，24小時都會陷入於如何讓產品更好，腦袋一直想著又燒多少錢，回歸到員工應該都要有創業的思維，如何避免浪費，自己對公司/產品帶來的價值是什麼，透過方法論是一個不錯的方式，精實創業這本書提供了一個很好的框架來遵循，但任何方法論運用需先清楚理解，不應該被方法論綁死，將「原則」跟「戰略」分開。「原則」引導你做什麼； 「戰略」告訴你如何做\n","permalink":"https://kyleap.github.io/2020-10-21-kyle-bite-book-lean-startup/","summary":"新創事業為何如此艱難? 經濟部中小企業處提供數據：創業一年內就倒閉的機率高達90%，而存活下來的10%中，又有90%會在五年內倒閉。為何做產品那麼難? 有沒有一個框架能遵循，降低風險，精實創業是一個可以參考的方法論\n精實計畫淬鍊成三個步驟 文件化你的初始計畫 識別風險 有系統的試驗計劃 新創會失敗在於跟顧客脫鉤，落於太自我的創新上，直接「走出辦公室」跟顧客面對面吧\n精實畫布收斂想法 先寫下願景，譬如阿里巴巴「讓天下沒有難做的生意」、馬斯克「讓人人都可上月球」，沒有願景則無法往下細拆策略目標 精實畫布是一個迅速、簡明、具可攜性的方式，用來產出「商業模式」\n圖片來源：https://ryanisagoodguy.blogspot.com/2016/03/lean-canvas.html\nStep 1~2 — 問題與顧客區隔 列出三項痛點，並目前這些痛點有何既有替代方案，識別哪些人會接觸到此服務，並試著專注於可能的早期採用者，主流顧客在早期產品通常無法滿足\nStep 3 — 獨特價值主張 「銷售」是一種對話，不管什麼身分.職位其實都是在做銷售 e.g 面試過程其實就是在銷售給面試官；而面試官其實在銷售給優秀人才。擬定好故事，從問題出發，擬定好如何在30秒內闡述獨特主張\nStep 4 — 解決方案 針對問題，「草擬」解決方案，為何是草擬？因為會隨著市場變化或經實驗及訪談，會重新排序優先順序，甚至會被新問題取代\nStep 5 — 通路 無法建立接觸「足夠」顧客的途徑，是初期創業失敗的原因，個人認為2C早期應至少接觸上百位顧客，2B需找出2~3個指標客戶。\n免費的最貴，本書作者 Eric Ries 以他這本書為例實踐精實創業，他一天花5塊錢美金，以每次點擊 5 分錢的成本吸引 100 次點擊\n通道要Pull (SEO.Blog…) or Push (活動.電話行銷…)，作為學習，直接銷售最有效；作為營利，直接銷售超過你的薪資整體薪酬才有意義\n避免太早建立策略型夥伴，同理銷售人員只能執行你的計劃，非建立計畫。故初期一定得親自販售你的產品 病毒式行銷前提是產品「值得」這樣做\nStep 6~7 — 收益來源 \u0026amp; 成本結構 思考定價策略：成本導向、競爭導向、需求導向、差異訂價法、特殊的訂價法(SaaS產品定價)…\n成本如何擬定：\n訪談30–50顧客將耗費多少成本 開發MVP將耗費多少成本 (e.g 員工薪資 x 1.6 ~2.0倍) 固定成本(薪資…等).變動成本(雲端費用…等) Step 8 — 關鍵指標 顧客獲取 顧客啟用 顧客續約 收益 轉介 延伸閱讀：從 AARRR 模型幫助對焦商業目標與設計策略","title":"精實執行精實創業指南 - 從初始計畫演化為有效計畫"}]