2025/12/03

Using AI Turn My Requirement Into A Spec

LLM 最強大的就是你給予適當的內容,它可以幫你延伸很多你沒注意到的問題,最近常常把需求稍微梳理過後,加上一段 prompt 讓他整理成 spec,再將結果調整一下變可以開 redmine 的票。

widget 設定完成後,若使用者將 widget 的欄位刪除,有機率會造成計算或使用錯誤,經討論後端要先行驗證欄位是否存在,查找有問題後一律回覆 422 給前端

---

你的角色是網路工程師。請協助將我提供的功能簡述,轉換成規格(spec)格式。轉換後的 spec 需包含以下幾個內容:

- 功能標題
- 功能簡述
- 實作建議
- 測試建議
- 其他注意事項

允許使用 emoji 輔助
# 📌 Widget 欄位存在性驗證(後端)

## 📝 功能簡述

當使用者完成 widget 設定後,如果後續手動刪除了 widget 所需的欄位,可能導致運算錯誤或使用體驗異常。後端需在每次相關操作前進行欄位存在性驗證,若必填欄位不存在,統一回傳 HTTP 422 錯誤給前端。

---

## 🛠️ 實作建議

1. **欄位驗證邏輯**

   * 在 widget 運算、讀取前加入一段欄位檢查邏輯。
   * 驗證所有 widget 設定中宣告的欄位是否存在於資料來源(如資料庫欄位、配置檔或動態欄位列表)。
   * 若任一欄位不存在,立即中止流程並回傳 422。

2. **統一錯誤格式**

   * 使用統一格式,例如:
        ```json
        {
          "message": "missing field",
          "code": "CO-ME108",
          "status": false
        }
        ```
        **此 message 以及 code 為示意。**

---

## 🧪 測試建議

1. **正常情境**
   * 所有欄位都存在 → 回傳 200 並正常執行邏輯。

2. **單一欄位缺失**
   * 刪除一個 widget 必要欄位 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

3. **多個欄位缺失**
   * 模擬多個欄位被刪除 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

其實他寫了相當多建議,這是我整理過後的結果,那些建議不一定符合我們需求,但可以當作參考也是相當不錯。

Using AI Turn My Requirement Into A Spec

LLM 最強大的就是你給予適當的內容,它可以幫你延伸很多你沒注意到的問題,最近常常把需求稍微梳理過後,加上一段 prompt 讓他整理成 spec,再將結果調整一下變可以開 redmine 的票。

widget 設定完成後,若使用者將 widget 的欄位刪除,有機率會造成計算或使用錯誤,經討論後端要先行驗證欄位是否存在,查找有問題後一律回覆 422 給前端

---

你的角色是網路工程師。請協助將我提供的功能簡述,轉換成規格(spec)格式。轉換後的 spec 需包含以下幾個內容:

- 功能標題
- 功能簡述
- 實作建議
- 測試建議
- 其他注意事項

允許使用 emoji 輔助
# 📌 Widget 欄位存在性驗證(後端)

## 📝 功能簡述

當使用者完成 widget 設定後,如果後續手動刪除了 widget 所需的欄位,可能導致運算錯誤或使用體驗異常。後端需在每次相關操作前進行欄位存在性驗證,若必填欄位不存在,統一回傳 HTTP 422 錯誤給前端。

---

## 🛠️ 實作建議

1. **欄位驗證邏輯**

   * 在 widget 運算、讀取前加入一段欄位檢查邏輯。
   * 驗證所有 widget 設定中宣告的欄位是否存在於資料來源(如資料庫欄位、配置檔或動態欄位列表)。
   * 若任一欄位不存在,立即中止流程並回傳 422。

2. **統一錯誤格式**

   * 使用統一格式,例如:
        ```json
        {
          "message": "missing field",
          "code": "CO-ME108",
          "status": false
        }
        ```
        **此 message 以及 code 為示意。**

---

## 🧪 測試建議

1. **正常情境**
   * 所有欄位都存在 → 回傳 200 並正常執行邏輯。

2. **單一欄位缺失**
   * 刪除一個 widget 必要欄位 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

3. **多個欄位缺失**
   * 模擬多個欄位被刪除 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

其實他寫了相當多建議,這是我整理過後的結果,那些建議不一定符合我們需求,但可以當作參考也是相當不錯。

2025/10/08

Laravel Sail On WSL

緣由

我開發 PHP,不管是純 PHP 或 Laravel 都很喜歡用 Homestead,他優點是可以起一台 VM 就可以同時使用多個專案,而且透過修改一個簡單的 Homestead.yml 就可以做完全部環境的設定,但官方已經宣布不維護了,身為資訊人,你要嘛 fork 一份自己維護,不然就要跟著時代走。

不維護的原因也很簡單,VM 真的很肥很吃資源,現在 docker 實在是太香了,而且 Windows 引入 WSL 以後,Docker Desktop 也是秒級啟動,Laravel Sail 也是 base on Docker Compose,是時候跟 Homestead 分手了 。°(°¯᷄◠¯᷅°)°。。

SMB

WSL 有一個痛點,你用他預設掛載進去的目錄執行任何程式碼都慢到哭,目前我實作比較好的方式是透過 SMB 把你的工作目錄掛進去,跑起來跟在本機執行相差無幾,首先是在你要分享的目錄上按右鍵選內容,共用 -> 進階共用 -> 共用此資料夾,共用名稱建議字尾加個 $,這樣在網路芳鄰上會看不到這個目錄,像是 code$,這樣我打 \\HOST\\code$ 可以連到,在公網上是看不到的,另外是我在本機多設定了一組 smb/smb 帳號,這樣我在 demo 的時候也不會因為被看到密碼尷尬,SMB 沒設定的話預設是你這台 Windows 的登入帳號密碼。

WSL

我並沒有使用 Docker Desktop,因為他開機即啟動,而且無法手動 terminate,WSL 資源雖然吃不多,但我沒有要開發的時候並不需要你來吃我這份資源,我知道網路上有許多改機碼之類的方式可以達成這個目的,但官方沒給的我認為都是邪門歪道,我很不喜歡用非官方的方式解決問題,所以我自己起一台 Ubuntu wsl --install -d Ubuntu --name develop,之後再進去自己安裝 Docker,自由自在~

進去 WSL 以後我建議一律 sudo su,用 root 可以少遇到很多奇奇怪怪的權限問題,再來就是手動掛載 smb 了,我將指令放到 ~/.bashrc,因為 WSL 沒有固定 IP,所以你沒辦法把掛載寫死,要的話也是要寫一堆 script 去判斷東西,我是覺得沒必要。

~/.bashrc

# ~/.bashrc - SMB Mount Configuration with Dynamic Windows IP

_get_windows_ip() {
    # 方法 1:透過 /proc/net/route (最穩定)
    # 取得預設閘道,就是 Windows host IP
    local gateway_ip
    gateway_ip=$(awk '$1 == "00000000" {print strtonum("0x"substr($3,7,2))"."strtonum("0x"substr($3,5,2))"."strtonum("0x"substr($3,3,2))"."strtonum("0x"substr($3,1,2))}' /proc/net/route | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 2:透過 ip route (備選)
    gateway_ip=$(ip route show | awk '/^default via/ {print $3}' | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 3:透過 resolv.conf (Windows 10 相容)
    gateway_ip=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    echo "" >&2
    return 1
}

_mount_smb() {
    local smb_share="$1"
    local mount_point="$2"
    local smb_username="smb"
    local smb_password="smb"
    local smb_host
    
    # 檢查必要參數
    if [[ -z "$smb_share" ]] || [[ -z "$mount_point" ]]; then
        echo "Usage: _mount_smb <share> <mount_point>" >&2
        return 1
    fi
    
    # 檢查是否已掛載
    if mountpoint -q "$mount_point"; then
        echo "Already mounted at $mount_point"
        return 0
    fi
    
    # 確保掛載點存在
    [[ -d "$mount_point" ]] || mkdir -p "$mount_point"
    
    # 動態取得 Windows host IP
    smb_host=$(_get_windows_ip)
    
    if [[ -z "$smb_host" ]]; then
        echo "Error: Cannot determine Windows host IP" >&2
        return 1
    fi
    
    echo "Detected Windows host IP: $smb_host"
    echo "Attempting to mount //$smb_host/$smb_share to $mount_point"
    
    # 測試連線
    if ! timeout 3 bash -c "</dev/tcp/$smb_host/445" 2>/dev/null; then
        echo "Error: Cannot connect to $smb_host:445 (SMB port)" >&2
        return 1
    fi
    
    echo "SMB port is reachable, mounting..."
    
    # 執行掛載 (優先使用 SMB 3.0,失敗則試 2.1)
    if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
        -o username="$smb_username",password="$smb_password",vers=3.0,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
        echo "✓ SMB share mounted at $mount_point"
        return 0
    else
        echo "SMB 3.0 failed, trying SMB 2.1..."
        
        if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
            -o username="$smb_username",password="$smb_password",vers=2.1,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
            echo "✓ SMB share mounted at $mount_point (SMB 2.1)"
            return 0
        fi
        
        echo "✗ Failed to mount SMB share" >&2
        return 1
    fi
}

# 只在互動式 shell 中執行
if [[ $- == *i* ]] && [[ $SHLVL -eq 1 ]]; then
    _mount_smb "code$" ~/mnt/code
fi

這段程式碼簡單講就是把我在本機 share 的 \\HOST\\code$ 掛到 ~/mnt/code,需要多掛幾個多執行幾次 _mount_smb 即可,每次進入 WSL 便抓取 Host Gateway IP 來連結,目錄跟檔案權限都設定成 777 避免一堆怪事發生,反正開發機不怕。

實際測試

這邊獻上幾個環境跑 phpunit 測試結果,測試數量 1364:

Host

real 1m 37.34s
real 1m 37.83s
real 1m 36.08s


平均:1m 37.08s

Vagrant + SMB

real 3m58.906s
real 3m32.119s
real 3m17.965s


平均:3m 36.33s

WSL + SMB

real 1m48.497s
real 1m57.464s
real 1m50.013s


平均:1m51.991s

其實在 Vagrant 裡面即便使用 SMB 執行速度還是悲劇,我已經這樣操作很久了,但因為 Homestead 的便利性一直選擇忽視這個問題,反正也不是整天跑 full test,另外有趣的一件事情,在 WSL 裡面使用 docker build 一個 php 環境跑測試,我預計應該要掉個 3% 的速度,因為它是 Host -> WSL -> Docker Volume,多一層轉換理應要消耗吧,但沒想到跑了幾次的結果它居然跟在 Windows Host 跑的時間差不多,php.ini 除了調整 memory_limit = -1 以外都沒動,不確定是 Docker 預設拉取的 OS 是 Alpine Linux 效能很好,還是 PHP 官方的 container php.ini 的配置有優化,總之蠻意外的。

之後預計會使用 WSL + Docker + Laravel/Sail 開發測試專案啦,謝謝你 Homestead。

2025/09/13

Docker Compose

原本以為 docker composedocker-compose 指令官方的套件整合,後來發現其實是全新的東西。

差異說明

  • docker-compose:是原先獨立安裝的 Compose 工具,使用 Python 實作,指令是用 docker-compose(中間有連字號),這是 Docker Compose v1 的常用形式。

  • docker compose:是 Docker Compose v2 版本,整合進 Docker CLI 中,採用 Go 語言重寫,使得 Compose 命令成為 docker 命令的一部分,指令使用空格隔開,即 docker compose,這是官方現階段推薦的新作法,提升整合度與效能。

Compose 檔案名稱的使用

歷史上預設檔名是 docker-compose.yml,由於 docker-compose 指令而來,仍被廣泛使用以確保向後相容。

新推薦的檔名是 compose.yaml(或 compose.yml 同樣被接受),更符合 Compose 規範,且映射到 docker compose 指令。官方文件中多建議使用 compose.yaml 作為現代 Compose 的標準檔名。

也可以用 -f 選項指定自訂檔名,靈活在不同目錄或專案間使用多個 Compose 配置檔。

建議改動

  1. 版本號 version 已經不需要寫
  2. deploy 只在 Swarm 模式才有用
  3. 官方推薦改成 compose.yaml
  4. 支援 profiles(服務分組)
services:
  db:
    image: mysql:8
    profiles: ["dev"]

  redis:
    image: redis:6
    profiles: ["prod", "staging"]
docker compose --profile dev up # 這樣執行只有 profiles 含 dev 的 container 會啟動
  1. 官方不建議用 links,用 depends_on 取代
  2. 多檔案合併,常用於「開發」跟「生產」環境分離

compose.yml

services:
  app:
    image: nginx:alpine
    ports:
      - "80:80"
    environment:
      - APP_ENV=production

compose.override.yaml

services:
  app:
    environment:
      - APP_ENV=development
    ports:
      - "8080:80" # 開發改用 8080 port
    volumes:
      - ./src:/usr/share/nginx/html:ro
docker compose -f compose.yaml -f compose.override.yaml up
  1. docker compose cp 已支援
  2. docker compose config 支援扁平化配置檔顯示
  3. 支援 Include
# compose.yaml
include:
  - ./database/compose.yaml
  - ./monitoring/compose.yaml
  
services:
  web:
    image: nginx
  1. 支援單一 updown service
docker compose up web fpm -d
docker compose down fpm

但有 depends_on 的會被連帶啟動。

2025/07/22

Gcloud Common Commands

☁️ gcloud 常用指令小抄

🧑‍💼 account:帳號管理

功能 指令
登入 gcloud auth login
登出 gcloud auth revoke
查看目前帳號 gcloud config get-value account
設定預設帳號 gcloud config set account <ACCOUNT>
列出所有帳號 gcloud auth list

🏢 project:專案管理

功能 指令
列出所有可用的專案 gcloud projects list
查看目前使用中的專案 gcloud config get-value project
切換專案 gcloud config set project <PROJECT_ID>
查看目前所有 config gcloud config list

💻 compute:VM 實例管理

一般操作

功能 指令
列出所有 instance gcloud compute instances list
查看 instance 詳細資訊 gcloud compute instances describe <INSTANCE> --zone=<ZONE>
啟動 instance gcloud compute instances start <INSTANCE> --zone=<ZONE>
關閉 instance gcloud compute instances stop <INSTANCE> --zone=<ZONE>
刪除 instance gcloud compute instances delete <INSTANCE> --zone=<ZONE>

SSH & 檔案傳輸

功能 指令
SSH 連線 gcloud compute ssh <INSTANCE> --zone=<ZONE>
上傳檔案 gcloud compute scp <LOCAL> <INSTANCE>:<REMOTE> --zone=<ZONE>
下載檔案 gcloud compute scp <INSTANCE>:<REMOTE> <LOCAL> --zone=<ZONE>
rsync 資料夾 gcloud compute rsync <LOCAL_DIR> <INSTANCE>:<REMOTE_DIR> --zone=<ZONE>

📦 scp 常用參數

參數 說明
--recurse-r 遞迴傳輸資料夾
--compress 傳輸時壓縮(較快)
--scp-flag="-C" 額外傳入 scp flag,例如啟用壓縮

📦 storage:Cloud Storage 操作

功能 指令
列出 bucket gcloud storage buckets list
建立 bucket gcloud storage buckets create <BUCKET> --location=<REGION>
上傳檔案 gcloud storage cp <LOCAL> gs://<BUCKET>/
下載檔案 gcloud storage cp gs://<BUCKET>/<FILE> <LOCAL>
刪除檔案 gcloud storage rm gs://<BUCKET>/<FILE>
刪除 bucket gcloud storage buckets delete <BUCKET>

🔁 Cloud Storage rsync

gcloud storage 支援類似 rsync 的功能來同步本地與 GCS 目錄:

上傳資料夾到 bucket

gcloud storage rsync -r ./local-folder gs://my-bucket/remote-folder

從 bucket 下載資料夾

gcloud storage rsync -r gs://my-bucket/remote-folder ./local-folder

🛠️ 常用參數

參數 說明
-r 遞迴處理目錄(必要)
-d 刪除目標端中不存在的檔案(mirror 同步)
-n 模擬執行(dry run,不實際動作)
-x "<REGEX>" 排除符合正則的檔案

🧩 其他常用指令

設定與服務

功能 指令
查看目前所有 config gcloud config list
清除設定 gcloud config unset <PROPERTY>
啟用服務 gcloud services enable <SERVICE>
停用服務 gcloud services disable <SERVICE>

IAM 權限操作

功能 指令
查看 IAM 成員 gcloud projects get-iam-policy <PROJECT>
新增 IAM 成員與角色 gcloud projects add-iam-policy-binding <PROJECT> --member="user:<EMAIL>" --role="roles/<ROLE>"

🧠 小提醒

  • 使用 get-value 取得設定值比 list 簡潔:
gcloud config get-value project
gcloud config get-value account
gcloud config get-value compute/zone
gcloud config get-value compute/region

2025/06/02

Neovim Copilot Chat Plugin

Github Copilot 已經是我工作上的好幫手,它的好處是你付一次月租他可以支援所有你有支援的軟體,我安裝在 Jetbrains 全套、vscode、neovim,一魚多吃相當方便,這邊介紹一下 vim/neovim 用來跟 copilot 對話的好用插件 CopilotChat.nvim,當然你要使用之前必須先有 copilot 帳號,以及安裝開通 copilot.vim

網站中有許多 plugin 安裝的方式,選自己常用的安裝即可,在你的 vim/neovim 配置檔案要加入啟動的語法:

lua << EOF
    require("CopilotChat").setup { }
EOF

" 用 cc 叫出視窗
nnoremap <silent> cc :CopilotChatToggle<CR>

我習慣是盡量不太修改插件預設值,避免有轉換環境的痛苦,套用 cc 叫出視窗是我針對 CopilotChat 唯一的設定了,我們用撰寫一個 docker-compose.yml 當作範例。

nvim docker-compose.yml 建立一個檔案,進入 vim/neovim 按下 cc,可以看到 CopilotChat 視窗在左邊出現。

以下是常用的快捷鍵以及指令:

插入模式 普通模式 功能說明
<Tab> - 觸發或接受補全選單(tokens 自動完成)
<C-c> q 關閉聊天視窗
<C-l> <C-l> 重設並清空聊天視窗
<C-s> <CR> 提交當前的提示內容
- grr 切換目前游標所在行是否為「固定提示」
- grx 清除所有「固定提示」
<C-y> <C-y> 接受最近的一個差異(diff)
- gj 跳轉到最近差異的區塊
- gqa 將所有回覆加入 quickfix 清單
- gqd 將所有差異加入 quickfix 清單
- gy 將最近的差異複製(yank)到暫存器
- gd 顯示原始內容與最近差異的比對
- gi 顯示目前聊天的資訊
- gc 顯示目前聊天的上下文
- gh 顯示說明訊息
指令 說明
:CopilotChat <輸入內容>? 開啟聊天視窗,並可選擇性地輸入問題或指令
:CopilotChatOpen 開啟聊天視窗
:CopilotChatClose 關閉聊天視窗
:CopilotChatToggle 切換聊天視窗(開啟與關閉間切換)
:CopilotChatStop 停止目前的輸出(中斷正在生成的內容)
:CopilotChatReset 重設聊天視窗(清除目前對話)
:CopilotChatSave <名稱>? 儲存聊天記錄,可選擇性指定一個名稱
:CopilotChatLoad <名稱>? 載入先前儲存的聊天記錄
:CopilotChatPrompts 查看並選擇提示範本(例如:解釋代碼、重構等用途)
:CopilotChatModels 查看並選擇可使用的 AI 模型
:CopilotChatAgents 查看並選擇可用的 AI 助手代理(不同風格或角色)
:CopilotChat<PromptName> 使用特定提示範本開啟對話(例如 :CopilotChatExplain

現在位於左邊的 CC 視窗,輸入寫一份 docker-compose.yml,需要安裝 nginx 以及 php-fpm,先按 esc 回到 visual mode 以後再按下 enter

我們可以看到他幫我們產生了許多內容,下面包含了基礎的 nginx 配置,更下方還有測試用的 php 程式碼,但我需要的只有 docker-compose 的部分,所以我將 cursor 移到我需要的區塊,然後按下 ctrl + y,此時他便將該區塊的程式碼移到了右邊去。

我接著輸入 prompt,現在的 docker-compose 不需要使用 version,他會依照上文幫我重產內容。

我們可以使用 gd 快速看出差異。

按下 q 回來以後,一樣使用 ctrl + y 將改變套用,這樣就可以套用新的格式,假設你已經脫離上下文,需要透過 cc 給你的檔案建議呢,這時就要用 vim/neovim 的選取功能,將需要他幫忙的地方選取起來,再去問一下 cc。

一樣用 ctrl + y 套用結果,如果你是要他參考整個文件就得使用全選了,另外一種方式是在命令列輸入 :CopilotChat 將 nginx 改為 ubuntu 版本 #buffer,這樣他就會把該檔案的內容當作緩衝使用,我自己是覺得用選的比較快。

另外一個主動參考整個文件的方法就是在第一次打開視窗問問題時,加上 #buffer,例如:

#buffer 將 $name 取代為 $userName

這樣他便會去直接參考整個檔案。

當我們使用 :CopilotChatModels 時,他會列出所有你可以使用的 model,可以記一下編號,移動到最下面後可以填入該編號,便可以切換模型。

但這個變動只有在這次有效,要永久使用該模型可以設定 config。

lua << EOF
require("CopilotChat").setup {
    model = 'claude-3.7-sonnet'
}
EOF

2025/05/26

Homestead SSL Issue

Laravel Homestead 在使用 nginx 創建網頁 config 時是有幫他配置 ssl key 的,但為什麼我們使用 https 還是會被警告呢,因為我們的瀏覽器是不認這個憑證的,在 Windows 上可以手動安裝該憑證讓你測試的時候可以透過 https 連到你的測試機,有時候開發會遇到跨網站連結 https 的狀況,如果你是 http 的話可能會被擋下來。

配置 Homestead.yml

folders:
  - map: ./ssl
    to: /ssl
  - map: ./code
    to: /home/vagrant/code

sites:
  - map: ssl.test
    to: /home/vagrant/code/
    php: "8.2"

這是我添加的測試網站,將根目錄開一個 ssl 目錄,在 code 目錄下放一個有 echo hello world 的 PHP 檔案,修改本機的 hosts 將 ssl.test 指到該 ip。

vm 起來後,vagrant ssh 進去該 vm,執行 sudo cp /etc/ssl/certs/ca.homestead.homestead.pem /ssl,將憑證複製到 /ssl 下,exit 退出 vm。

前往該 ssl 目錄,執行以下步驟:

  1. 右鍵 點擊憑證檔,選擇 「安裝憑證」
  2. 點選 「下一步」
  3. 選擇 「將所有憑證放入以下的存放區」
  4. 點選 「瀏覽」
  5. 選擇 「受信任的根憑證授權單位」
  6. 點選 「下一步」
  7. 點選 「完成」

此時瀏覽 https://ssl.test 應該就不會有警告了,如果還是有的話可能是 nginx cache,重開 server 或瀏覽器應該就解決了,可以先用無痕驗證一下。

該憑證過期時間為十年,應該是蠻夠用的,過期的話要重產一個新的,但我想應該在過期之前就會重置測試機了。