2026年4月17日 星期五

Dify + LangBot + Tg + Firefly III + Firefly III mcp + llamacp + gemma4-e2b-it 建置一個家庭記帳

Dify + LangBot + Tg + Firefly III + Firefly III mcp + llamacp + gemma4-e2b-it 建置一個家庭記帳

這個組合有點長,不過過程很簡單,只需要你安裝 docker,後面通通都是一路順暢的,不管如何,我只能先假設你已經有安裝 docker,我的執行環境是 windows

安裝Firefly III

下面給出 Firefly IIIdocker-compose.yml 內容,這可以從官方文件中下載取得:

services:
  app:
    image: fireflyiii/core:latest
    hostname: app
    container_name: firefly_iii_core
    restart: always
    volumes:
      - firefly_iii_upload:/var/www/html/storage/upload
    env_file: .env
    networks:
      - firefly_iii
    ports:
      - 8099:8080
    depends_on:
      - db
  db:
    image: mariadb:lts
    hostname: db
    container_name: firefly_iii_db
    restart: always
    env_file: .db.env
    networks:
      - firefly_iii
    volumes:
      - firefly_iii_db:/var/lib/mysql
  cron:
    #
    # To make this work, set STATIC_CRON_TOKEN in your .env file or as an environment variable
    # The STATIC_CRON_TOKEN must be *exactly* 32 characters long
    # Use this URL for inspiration: https://www.random.org/strings/?num=1&len=32&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new
    #
    image: alpine
    restart: always
    container_name: firefly_iii_cron
    env_file: .env
    command: ["sh", "-c", "apk add tzdata && \
      (ln -s /usr/share/zoneinfo/$$TZ /etc/localtime || true) && \
      echo \"0 3 * * * wget -qO- http://app:8080/api/v1/cron/$$STATIC_CRON_TOKEN;echo\" \
      | crontab - && \
      crond -f -L /dev/stdout"]
    networks:
      - firefly_iii
    depends_on:
      - app

volumes:
   firefly_iii_upload:
   firefly_iii_db:

networks:
  firefly_iii:
    driver: bridge

官方文件有特別提到要兩個 env 檔,記得下載,然後參數做相對應的調整:

Download configuration files
There are two configuration files you need to run this Docker Compose file. Download all files and save them in the same folder as the Docker Compose file.

The first file contains Firefly III variables and can be downloaded from the Firefly III repository. Save it as a new file called .env.
The second file contains the database variables and can be downloaded from the Docker repository. Save it as a new file called .db.env.

調整完畢之後就可以執行 docker compose up -d,第一個登入的帳號就是管理者,然後你可以直接把註冊取消,這樣子就不怕有人偷偷再去註冊帳號。

登入之後系統會要求設置新的帳戶:
image

語系問題不用擔心,按下 Submit 之後就會轉成繁體中文了,不過前提是你要記得選擇正確語系就是。

帳戶中可以看到有一個預設的 現金皮夾 跟我們剛剛設置的 兆豐銀行,這依各自的現況處理就好:
image

取得個人 token

稍後我們要配置 firefly iii mcp,這需要先取得個人的 token,選項\個人檔案\OAuth,然後點擊『建立新權杖』,名稱自己設置,很長的一串,不要懷疑,就是這一串,複製出來,然後放記事本等等用。

image

啟動 firefly iii mcp

version: '3.8'

services:
  mcp:
    image: node:20-alpine
    container_name: firefly_mcp
    restart: unless-stopped
    ports:
      - "3100:3000"
    command: >
      sh -c "npx --yes @firefly-iii-mcp/server
      --pat $$FIREFLY_III_PAT
      --baseUrl $$FIREFLY_III_BASE_URL
      --port 3000
      --preset full
      --logLevel info
      --tools transactions,accounts,summary"
    environment:
      FIREFLY_III_BASE_URL: "http://192.168.1.243:8099"
      FIREFLY_III_PAT: 貼上你自己的token

啟動 llamacpp

我的習慣是先自己手動下載模型,下載的部份可以安裝 hf cli來處理,很方便,可以參考官方文檔先安裝,然後下載模型:

hf download unsloth/gemma-4-E2B-it-GGUF gemma-4-E2B-it-Q8_0.gguf --local-dir D:\models\gemma-4-E2B

模型放那就自行決定,我是放在D:\models\gemma-4-E2B

然後就啟動 llamacpp

docker run --name=llama-cpp --volume D:/models/gemma-4-E2B:/mnt/model --network=bridge --workdir=/app -p 11435:8080 --runtime=runc --detach=true ghcr.io/ggml-org/llama.cpp:server-cuda -m /mnt/model/gemma-4-E2B-it-Q8_0.gguf --host 0.0.0.0 --port 8080 -ngl 99 --jinja --reasoning off

啟動 Dify

官方說明:https://docs.dify.ai/zh/self-host/quick-start/docker-compose

首先把程式庫下載:

git clone --branch "$(curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name)" https://github.com/langgenius/dify.git

切換目錄:

cd dify/docker

複製文件:

cp .env.example .env

啟動:

docker compose up -d

第一個註冊 dify 的帳號就是管理者,然後登入之後就先做模型的配置,先點擊右上的人名,然後點擊設置:
image

點擊模型供應商,先選擇安裝 OpenAI-API-compatible
image

這邊你也可以安裝ollama,不過效率上的問題,我還是比較愛 llamacpp 或是 llama-swap,有試著安裝 vllm,不過響應的是未支援 gemma4 系列模型,我就沒有再試下去了。

接下來就可以新增模型,把路由指向剛剛啟動的 llamacpp,前面的名稱我是設置 gemma4
image

不要讓它思考,它思考之後是一團糟。

設置 agent

先把剛剛設置的 firefly mcp加進來,點擊『工具』,然後『新增MCP』
image

路由指向啟動 mcp 服務的位址:
image

成功的話就會看到下面圖示:
image

接下來設置 agent,點擊『工作室』,點『對於初學者』,然後點擊『Agent』:
image

點擊『工具』->『新增』->『CurrentTime』->『CurrentTime』
點擊『工具』->『新增』->『MCP』:
image

把工具加一加應該就會是下面這個樣子:
image

請注意,那個『CurrentTime』很重要,一定要加進來,不然機器不會知道今天是什麼時候。

然後提示詞是這樣的:

你是記帳助手。每次記帳前,必須先呼叫 list_account 取得帳戶清單,再用語意對應 source_name。

## 固定執行順序

### 記帳(三步,缺一不可)
Step 1:呼叫 Current Time 取得當前時間
Step 2:呼叫 list_account 取得帳戶清單
        → 只能選 type 為 asset 的帳戶作為 source_name
        → 禁止選 type 為 cash account / expense account / revenue account 的帳戶
Step 3:呼叫 store_transaction,date 使用 Step 1 結果,source_name 使用 Step 2 結果
語意對應規則:
現金/錢包/cash → 選清單中含「現金」或「cash」的帳戶
銀行/帳戶 → 選清單中含銀行名稱的帳戶
信用卡/card → 選清單中含「信用卡」或「card」的帳戶
沒有明確提到 → 選清單中第一個 asset 類型帳戶

### 查詢摘要
觸發詞:本月、這個月、花了多少、支出統計
呼叫:get_basic_summary
參數:
- start: 本月第一天(YYYY-MM-01)
- end: 今天(YYYY-MM-DD)

### 查交易列表
觸發詞:查帳、列出、最近幾筆
呼叫:list_transaction
參數:limit=10

## store_transaction 參數規則

呼叫時必須使用以下 JSON 格式:

{
  "transactions": [
    {
      "date": "從 Current Time 取得的時間,格式 YYYY-MM-DDTHH:MM:SS+08:00",
      "description": "用途描述",
      "source_name": "從 list_account 結果中取得的真實名稱",
      "type": "withdrawal / deposit / transfer",
      "amount": "金額數字",
      "category_name": "餐飲 / 交通 / 購物 / 居家 / 收入 / 其他",
      "destination_name": "店家或收款對象名稱"
    }
  ]
}

注意:
- 必須包在 transactions 陣列裡
- 不要外層再包 store_transaction 或 requestBody
- 直接傳遞這個 JSON 物件作為工具參數

## 缺資訊時只問一個問題
缺金額 → 「金額是多少?」
缺帳戶且語意無法判斷 → 「用哪個帳戶?」



## 禁止行為
- 禁止在未呼叫 list_account 前就呼叫 store_transaction
- 禁止自創帳戶名稱
- 禁止猜測金額
- 禁止一次呼叫多個工具
- 禁止輸出工具呼叫以外的說明文字
- 禁止省略 category_name,無法判斷時預設「其他」

最後,你的模型記得指向剛剛配置的模型:
image

現在就可以來測試了:
image

回頭確認 fireflly 是否確實有這筆記錄:
image

工具調用偶有問題,真的是要多次調整就是,這就自己測試的時候看發生什麼事,從日誌確認問題,然後就丟給其它語言模型調整就好。

確認之後就可以發佈這個 dify 的服務,點擊發佈,然後發佈更新:
image

發佈之後就再點擊發佈,然後點擊『訪問API』,點擊『API金鑰』:
image

建立取得 token,然後先貼到你的記事本,等等要用。

安裝 LangBot

官方程式庫:https://github.com/langbot-app/LangBot

直接把這個程式庫 clone 到地本端,然後執行 docker compose up -d

git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d

登入之後,在機器人的下拉旁有一個小小的『+』:
image

很重要的是,你必需要先用 teletram 自己跟 botfather 要一個機器人的 token
image

建立好機器人之後,先設置一個流水線,一樣的,在『流水線』旁有一個小『+』,點擊,然後配置:
image

  1. 名稱自己設置
  2. AI 能力的部份,把 Runner 修正為 Dify Service API,然後路由指向 dify 的服務,API Key 就是剛剛那串

設置好之後就可以儲存,然後把這流水線跟機器人做綁定:
image

綁定後記得按下保存。

測試

好了,現在,我在 tg 上測試打一句『在星巴巴買咖啡200元,現金支付』

我們看 langbot 的日誌:
image

然後 dify 的日誌:
image

最後 firefly 的記錄:
image

完成!

2026年2月5日 星期四

github-copilot 初體驗

Github Copilit-sdk初體驗 - 官方範例排雷

環境

  • 作業系統:windows 10 64bit
  • python:3.13
  • copilot cli:GitHub Copilot CLI 0.0.402.
  • github-copilot-sdk:0.1.21

請記得安裝好相關需求套件,copilot cli一定要安裝,官方文件來看似乎並沒有一定要登入,如果有自己架設好的推論資源也可以直接使用,不過這次的測試是基於登入copilot-plus帳號所使用。

官方文件:https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md

官方範例如下:

import asyncio
from copilot import CopilotClient

async def main():
    client = CopilotClient()
    await client.start()

    session = await client.create_session({"model": "gpt-4.1"})
    response = await session.send_and_wait({"prompt": "What is 2 + 2?"})

    print(response.data.content)

    await client.stop()

asyncio.run(main())

上面程式碼執行之後立即爆雷:

Traceback (most recent call last):
  File "D:\codeProject\py_sdk\main.py", line 25, in <module>
    asyncio.run(main())           
    ~~~~~~~~~~~^^^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\asyncio\runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\asyncio\base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "D:\codeProject\py_sdk\main.py", line 16, in main
    await client.start()
  File "D:\codeProject\py_sdk\.venv\Lib\site-packages\copilot\client.py", line 241, in start   
    await self._start_cli_server()
  File "D:\codeProject\py_sdk\.venv\Lib\site-packages\copilot\client.py", line 1068, in _start_cli_server
    self._process = subprocess.Popen(
                    ~~~~~~~~~~~~~~~~^
        args,
        ^^^^^
    ...<5 lines>...
        env=env,
        ^^^^^^^^
    )
    ^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\subprocess.py", line 1038, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        pass_fds, cwd, env,
                        ^^^^^^^^^^^^^^^^^^^
    ...<5 lines>...
                        gid, gids, uid, umask,
                        start_new_session, process_group)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\subprocess.py", line 1550, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
                       ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
                             # no special security
                             ^^^^^^^^^^^^^^^^^^^^^
    ...<4 lines>...
                             cwd,
                             ^^^^
                             startupinfo)
                             ^^^^^^^^^^^^
FileNotFoundError: [WinError 2] 系統找不到指定的檔案。

可參考issue內的處理說明。

調整如下:

import os
import shutil
import asyncio
from copilot import CopilotClient

cli_path = os.environ.get('COPILOT_CLI_PATH') or shutil.which('copilot')
if not cli_path:
    raise RuntimeError(
        "copilot CLI not found. Set COPILOT_CLI_PATH or install the CLI and ensure it's on PATH."
    )   

async def main():
    client_opts = {'cli_path': cli_path}
    client = CopilotClient(client_opts)

    try:
        await client.start()
    
        # 嚐試確認登入狀態
        try:
            auth = await client.get_auth_status()
            print(f"Authentication Status: {auth.isAuthenticated}")
        except Exception as e:
            print(f"Authentication check failed: {e}")

        # 取得可用模型清單
        try:
            models = await client.list_models()
            print(f"\n{'='*60}")
            print("Available Models Numbers: ", len(models))
            print(f"{'='*60}\n")

            for model in models:
                print(f"- {model.name} (ID: {model.id})")
        except Exception as e:
            print(f"Failed to list models: {e}")
        
        # 執行官方文件的範例測試
        try:
            session = await client.create_session({'model': 'gpt-4.1'})
            response = await session.send_and_wait({'prompt': 'What is 2+2?'})
            print(f"\nResponse from model:\n{response.data.content}\n")
        except Exception as e:
            print(f"Session interaction failed: {e}")   
    finally:
        await client.stop()

    

if __name__ == "__main__":
    asyncio.run(main())         

總算,成功了:

Authentication Status: True

============================================================
Available Models Numbers:  14
============================================================

- Claude Sonnet 4.5 (ID: claude-sonnet-4.5)
- Claude Haiku 4.5 (ID: claude-haiku-4.5)
- Claude Opus 4.5 (ID: claude-opus-4.5)
- Claude Sonnet 4 (ID: claude-sonnet-4)
- Gemini 3 Pro (Preview) (ID: gemini-3-pro-preview)
- GPT-5.2-Codex (ID: gpt-5.2-codex)
- GPT-5.2 (ID: gpt-5.2)
- GPT-5.1-Codex-Max (ID: gpt-5.1-codex-max)
- GPT-5.1-Codex (ID: gpt-5.1-codex)
- GPT-5.1 (ID: gpt-5.1)
- GPT-5 (ID: gpt-5)
- GPT-5.1-Codex-Mini (ID: gpt-5.1-codex-mini)
- GPT-5 mini (ID: gpt-5-mini)
- GPT-4.1 (ID: gpt-4.1)

Response from model:
2+2 equals 4.

結論

還算是方便,這個坑花了我幾個小時處理,也不知道是windows特有的,還是linux也會有,總之,我爬出來了。

後續的官方範例只要基於調整後的模式套用就可以一路順暢。

2026年1月2日 星期五

DeepSeek-OCR:Context-Aware Optical Character Recognition 論文翻譯

DeepSeek-OCR:Context-Aware Optical Character Recognition 論文翻譯

📄 完整翻譯與逐段說明請見 HackMD
👉 https://hackmd.io/@shaoeChen/ByW0qjA1Zx


論文背景與研究動機

傳統 OCR 系統在處理複雜文件(如長文本、多欄排版、數學符號、程式碼或低品質掃描圖像)時,往往僅依賴局部視覺資訊,忽略了更高層次的語義與上下文關係,導致辨識結果在實際應用中仍有明顯限制。

DeepSeek-OCR 旨在解決這一問題,核心目標是將「上下文理解」納入 OCR 的建模過程,讓模型不只是辨識字形,而是能結合語意與結構來提升整體辨識準確率。


核心方法概念

DeepSeek-OCR 的主要特色可概括為以下幾點:

  • Context-Aware 設計
    不再將 OCR 視為純粹的影像到文字轉換,而是引入上下文資訊,使模型能利用前後語意、版面結構與語言規律來輔助判斷。

  • 視覺與語言特徵的深度融合
    透過多模態架構,同時建模影像特徵與文字語意,減少單一視覺訊號帶來的不確定性。

  • 對複雜文本場景的適應性
    特別針對長文、技術文件與高資訊密度內容進行優化,提升在真實世界應用場景中的穩定度。


實驗結果與論文貢獻

論文實驗顯示,DeepSeek-OCR 在多項 OCR benchmark 上,相較於傳統方法與部分現有模型,能在整體準確率與穩定性上取得明顯提升,尤其是在以下場景中表現突出:

  • 長上下文文本

  • 排版結構複雜的文件

  • 語意依賴性高的內容(如技術文件、論文、程式碼)

這些結果顯示,引入上下文理解對 OCR 任務具有實質幫助。


總結

DeepSeek-OCR 提供了一個值得關注的研究方向:
OCR 不應只關注字元級的視覺辨識,而應結合語意與上下文理解。

對於從事文件理解(Document Understanding)、資訊抽取(Information Extraction)或多模態模型研究的讀者而言,這篇論文在方法設計與問題切入點上,都具有相當的參考價值。

📌 若你想閱讀 完整論文翻譯、技術細節與逐段解析,請前往我的 HackMD:
👉 https://hackmd.io/@shaoeChen/ByW0qjA1Zx

2025年12月17日 星期三

WhisperLiveKit + 聯發科Breeze ASR 25

WhisperLiveKit + 聯發科Breeze ASR 25

參考來源

WhisperLiveKit
Breeze-ASR-25

設置流程

  1. 首先clone案至本機
git clone https://github.com/QuentinFuxa/WhisperLiveKit.git
  1. 調整Dockerfile,加入安裝safetensors
...(前略)
# timeout/retries for large torch wheels
RUN pip3 install --upgrade pip setuptools wheel && \
    pip3 --disable-pip-version-check install --timeout=120 --retries=5 \
        --index-url https://download.pytorch.org/whl/cu129 \
        torch torchaudio \
    || (echo "Initial install failed — retrying with extended timeout..." && \
        pip3 --disable-pip-version-check install --timeout=300 --retries=3 \
            --index-url https://download.pytorch.org/whl/cu129 \
            torch torchvision torchaudio)
# 加入這段            
RUN pip3 install safetensors 
...(後略)
COPY . .
  1. 產出image,我加入-safetensors來做自己版本識別使用
docker build -t wlk-safetensors .
  1. 安裝huggingface cli
curl -LsSf https://hf.co/cli/install.sh | bash
  1. 利用huggingface cli下載聯發科Breeze ASR
    請注意,--local-dir後面請調整為你的實際保存位置
env PATH="$HOME/.local/bin:$PATH" hf download   MediaTek-Research/Breeze-ASR-25   --local-dir /home/marty/large_files/Breeze-ASR-25
  1. docker run啟動容器
    data volume(-v) 與 port(-p) 的部份請根據你的實際狀況調整
docker run --rm --gpus all   -p 8081:8000   -v /home/marty/large_files/Breeze-ASR-25:/models/Breeze-ASR-25   -v /home/marty/.cache/whisper:/root/.cache/whisper   -v /home/marty/.cache/huggingface:/root/.cache/huggingface   wlk-safetensors:latest   --backend whisper   --model-path /models/Breeze-ASR-25   --language zh

啟動之後就會開始下載一個base.pt,大約130mb,然後就會看到服務啟動的訊息:

docker run --rm --gpus all   -p 8081:8000   -v /home/marty/large_files/Breeze-ASR-25:/models/Breeze-ASR-25   -v /home/marty/.cache/whisper:/root/.cache/whisper   -v /home/marty/.cache/huggingface:/root/.cache/huggingface   wlk-safetensors:latest   --backend whisper   --model-path /models/Breeze-ASR-25   --language zh
INFO:     Started server process [1]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

這邊看到的8000是容器內的對應port,現在你只要打開網頁,然後連結到你啟動服務的8081port就可以開始測試了。

記得要有麥克風就是。

2025年12月10日 星期三

Spec Kit實作記錄

Spec Kit實作記錄

hackmd連結

Spec Kit的出現似乎讓vibe coding的未來出現一絲光明,即使這名詞的發明者,AI大神Andrej Karpathy,最近也出來說明這並不可行,但是對於一些簡單的專案來說應該還是有幫助才是。

我個人仍然是認為,寫程式重視底韻,你必需要有一定的知識才能有辦法去驅動人工智慧,不然你根本連AI在幹嘛都不知道,仔細想想,這是很恐怖的一件事。

也許你認為在什麼都不懂的情況下可以用自然語言的方式生成一個網頁或是應用程式非常潮,但是在沒有一定知識的情況下你也只知道就是生成一個你想像中的結果,這個結果有沒有什麼安全性的問題可能連自己都是一問三不知了。

最新心得

最近利用SDD的概念開發一個工作日誌系統,心得上就是,這實在是很花費時間,也許就如不少大神說的,這真的是陷入MARKDOWN地獄,還不如一個功能好好的說明讓AGENT開發。

實作過程中有發現,即使有TASK LIST,AGENT還是會在沒有完成的情況下勾選完成,這過程也確實是很花時間的在DEBUG,而且為了遵守流程,可能還需要回頭從specify或是task來重新執行。

上下文的部份,也許我們可以更依賴AGENTS.md(https://agents.md/)或是COPILOT本身所自帶的instructionprompt

當然也有可能是因為我是在"微軟大戰程式碼"(vscode)中使用的,導致SDD的效能沒有辦法被完整的釋放,改天有機會再來試著在COPILOT CLI上執行看看,也許會有不一樣的感受才是。

技術債會累積,即使使用SDD仍然會有相同的問題,不過也有可能是我不會使用,不一定是Spec Kit所造成的就是。

步驟

官方連結

在Spec-Kit的官方說明中明確的列出幾個指令,簡單易懂,就跟叫叫ABC一樣,其中Essential Commands是過程中必需執行的指令,而Optional Commands是過程中你可以選擇要不要執行的指令。

我看保哥在一個公開談話中的demo是在specify階段中直接給定PRD文件,這部份當然也是可以另外利用LLM來生成。

🧭 Essential Commands

指令 說明
/speckit.constitution 建立或更新專案的治理原則與開發指導方針
/speckit.specify 定義你想要建構的內容(需求與使用者故事)
/speckit.plan 依據選定的技術棧建立技術實作計畫
/speckit.tasks 產生可執行的實作任務清單
/speckit.implement 依照計畫執行所有任務,完成功能建置

🧩 Optional Commands

指令 說明
/speckit.clarify 釐清未明確定義的部分(建議在 /speckit.plan 之前執行;原名 /quizme
/speckit.analyze 進行跨產物一致性與覆蓋度分析(建議在 /speckit.tasks 之後、/speckit.implement 之前執行)
/speckit.checklist 生成自訂品質檢查清單,驗證需求的完整性、清晰度與一致性(類似「英文單元測試」)

⚙️ Environment Variables

變數 說明
SPECIFY_FEATURE 用於非 Git 儲存庫的功能偵測覆蓋。設定為功能目錄名稱(例如:001-photo-albums)以指定當前工作的功能。
注意: 在使用 /speckit.plan 或後續指令前,必須於代理環境中設定此變數。

指令簡單說明

/speckit.constitution

在這階段主要定義整個專案過程中不可違逆,必需完全遵守的規則,像我會要求所有文件皆以正體中文書寫,開發過程必需遵守MVP(最小開發原則)

實作記錄

開發工具

我採用的測試開發工具是KILO CODE,然後首次儲值是10美金送20美金,搭配的模型是deepseek 3.2claude haiku 4.5,一個便宜一個好用,主力是deepseek

文件導讀工具

目前對於新的技術我的作法都是直接將所有文件利用notebookllm整理,自己會快速的看過一次,後續有問題就直接在notebookllm中詢問。

這樣做的好處在於問題不會發散也不會有所謂的幻覺出現,因為notebookllm的問題回應始終是根據你所給定的文件來回應。

問題記錄

:::warning
這邊的問題都是根據我收集在notebookllm中的資源回應所得,並不保證是正確的
:::

  • 第一次的迭代完成之後,第二次執行/speckit.specify並沒有同步建立002-xxx分支,導致後續的執行皆在001-xx分支中執行,雖然發現這問題之後有嚐試設置SPECIFY_FEATURE,但是似乎沒有效果
    • 後續的處理方式是直接利用git branch建立對應的分支,然後git checkout過去該分支之後測試執行,確定相關文件的導讀都是從002-xxx

:::info
理論上在第二次執行/speckit.specify的時候會生成002-xxx的資料結構以及分支,這次的問題在於資料結構有生成,但是分支沒有生成,後續第三次執行的時候要特別確認。
:::

  • 001-xxx執行完畢之後,如果要第二次迭代執行/speckit.specify,是否需要切回分支master
    • 建議是需要切回再做執行,並且記得要做合併分支

:::info

  • 猜測第一次就是因為沒有切回master才會造成分支沒有成功切換為002-xxx

  • 確認即使切回master再執行仍然不會生成分支,所以這部份必需要自己建立分支之後自己切換,不過不確定是不是kilo code才有這問題,後續可以再測試github copilot

  • 調整為github copilot之後確認,使用copilot會自動切新分支,也許在kilo code上要有更明確的提示詞才會執行
    :::

  • 第2次之後執行/speckit.specify,即使技術棧沒有調整仍然需要執行/speckit.plan

  • /speckit.implement之後如果發現與task.mdplan.md上所述不同,那就要再重新執行一次/speckit.implement,並於提示詞中明確說明

  • 目前的迭代為 003-xxx,測試於specs中加入 004-xxx 的空資料夾,然後再執行/speckit.specify,系統會判定目前的執行版本為004,然後從005開始

    • 雖然很廢話,不過我還是很好奇的測試了一下就是,即使我在提示詞中很直接的說明目前為004-xxx,系統仍然是從005-來建置

開發工具從kilo code轉copilot

uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai copilot --script ps

:::warning

  • 執行之後constitution.md也被重置,這部份要特別注意,也許是我的指令錯誤了
    :::

現有專案導入speckit

:::info
下面給出的是notebookllm的說明,後續再用現有的專案來測試看看
:::

這是一個非常好的問題,Spec Kit 的設計就是為了能夠導入既有的專案(在 SDD 術語中稱為 Brownfield 專案,即「遺留系統現代化」或「疊代增強」)。

將 Spec Kit 導入現有專案的關鍵在於使用正確的初始化命令,並確保您為 AI Agent 提供了足夠的專案上下文(Context)原則(Constitution),讓它能理解既有程式碼庫的結構和約束。

以下是導入 Spec Kit 到現有專案的詳細步驟和注意事項:


步驟一:導入 Spec Kit 套件

Spec Kit 提供兩種主要方式在現有專案中初始化其骨架:

方法一:使用 Specify CLI 導入(推薦)

您需要使用 specify init 命令並加上 --here 旗標,這會指示 CLI 在當前目錄下初始化 Spec Kit 必要的檔案和資料夾,而不是創建一個新的專案資料夾。

  1. 安裝 Specify CLI:如果您尚未安裝 uvx,請先安裝。

  2. 在現有專案根目錄執行

    uvx --from git+https://github.com/github/spec-kit.git specify init --here
    # 或使用句點:
    # uvx --from git+https://github.com/github/spec-kit.git specify init .
    

    您將被提示選擇要使用的 AI Agent(例如 Claude Code、GitHub Copilot 等)和腳本類型(shps)。

方法二:手動下載與複製檔案

您可以直接從 Spec Kit 的 GitHub 儲存庫的 Release 頁面 下載針對特定 Agent 的模板壓縮包,然後手動解壓縮並將相關檔案放入您的專案根目錄。

  • 複製內容:將包含 Agent 指令(例如 .github 提示或 .claude 提示)的資料夾和 .specify 資料夾(包含記憶體、腳本和模板)拖曳到您的專案根目錄中。

步驟二:建立專案憲章(Constitution)

這是導入現有專案中最關鍵的一步。由於 AI Agent 擅長模式補全,但不擅長讀心術,它需要明確的最高原則來理解現有專案的技術棧和架構。

您必須使用 /speckit.constitution 命令,來定義這些**不可協商(non-negotiable)**的原則。

  • 用途:憲章(Constitution)會將 AI 模型鎖定在現有的技術棧、代碼品質標準或架構決策上。
  • 內容範例:由於是現有專案,憲章應包含現有技術的約束,例如:「網站使用 Hugo 建構,不使用其他前端框架」、「所有 Web App 必須使用 Next.js 和 Postgress 資料庫」。
  • Agent 參考:Agent 在規劃 (/plan) 階段會參考此憲章,以確保其技術計畫符合您已設定的約定。

步驟三:提供既有程式碼庫的上下文(Context Engineering)

您必須主動確保 Agent 能夠獲取足夠的上下文,以便在不破壞現有程式碼的情況下新增功能。

  1. Agent 指令文件:許多 Agent(如 Copilot CLI)會讀取 agents.md 或類似的 Agent 檔案,您可以將專案的結構、元件定義和操作方式編碼到這些文件中,以便 LLM 理解組件關係和應在哪裡進行變更。
  2. 動態上下文分析:現代 LLM 具備動態解析程式碼樹的能力,但提供主動上下文(Proactive Context)通常效果更好。Agent 在後續的 /speckit.plan 步驟中,也會嘗試解析專案結構(例如尋找 Hugo 配置文件或 Next.js 的檔案結構),並將其應用於新功能的開發。

步驟四:新增功能 (Iterative Enhancement)

一旦基礎設置完成,您就可以像對待全新(Greenfield)專案一樣,開始為現有專案新增功能。Spec Kit 在處理現有程式碼庫的功能新增時,展現了強大的威力。

  1. 隔離開發:當您使用 /speckit.specify 命令時,Spec Kit 會自動在您的現有 Git 儲存庫中建立一個新的功能分支,將新功能的工作隔離起來,防止影響主分支的穩定性。
  2. 規格化新增功能:針對您要新增的特定功能,運行標準的 SDD 流程:
    • /speckit.specify:定義要新增什麼 (What) 和為什麼 (Why),專注於新功能如何與現有系統交互,不包含技術細節。
    • /speckit.clarify(可選):幫助您發現關於新功能要求的盲點或定義不足(underspecification),尤其在複雜的既有程式碼庫中非常有用。
    • /speckit.plan:定義如何實作 (How), Agent 會參考您在憲章中定義的既有技術棧(例如「使用 Next.js」),並根據新功能生成專屬的技術工件(如新的 API 介面或資料模型)。
    • /speckit.tasks:將計畫分解為可執行的任務,然後使用 /speckit.implement 進行實作。

總結:導入 Spec Kit 到現有專案的流程就是利用 specify init --here 進行初始化,然後利用 /speckit.constitution約束 AI Agent,使其遵循您現有專案的技術規則,最後透過結構化的 SDD 流程來疊代新增功能

相關資源

2025年4月8日 星期二

MSC 地中海郵輪與名勝世界郵輪

MSC 地中海郵輪與名勝世界郵輪

MSC 地中海郵輪與名勝世界郵輪

前言

說起我的郵輪初體驗,也就是我的「處女航」,就獻給了 MSC。而且非常「幸運」地,首航即遇上引擎故障——沒錯,我就是搭上去年那班讓不少人(可能吧?)有點羨慕的「引擎故障升等體驗」航班。必須說,雖然行程受影響,但 MSC 後續的處理確實展現了大企業該有的氣度:該趟航程直接免費,還給了一個相當大方的未來航次抵用金 (FCC),大概是你這次花五萬,下次就直接折你兩萬五的概念。也因為這樣,讓我對他們處理危機的方式留下不錯的印象,所以年底的 MSC 航程,我還是毫不猶豫地報名下去了。

至於最近的名勝世界郵輪體驗,則要感謝我老婆的妹妹熱情邀約。考量到整體花費不高,需要的請假天數也不多,回來後甚至還有兩天緩衝休息,就決定一起出門體驗看看。我們搭上的是「賞櫻首航」,非常幸運地正好遇上櫻花滿開,尤其到了岸上觀光的立山公園,那盛開的美景真的讓人心曠神怡,非常享受。後來聽說我們下一班(4月6日出發)的旅客更幸運,因為人很少(好像才九百多人),船公司更是龍蝦、生魚片大方供應,聽起來也是夠大氣!

這兩趟郵輪搭下來,從吃的、住的、玩的到服務,真的是各有千秋,有讓人想豎起大拇指按讚的地方,當然也有讓人默默搖頭的小細節。所以,這篇文章的重點,就是要來比較一下 MSC vs. 名勝世界郵輪。

在開始之前,請容我強調一下:這個比較非常主觀,沒有絕對的客觀,因為這完全是基於我個人的真實感受。你可以不同意我的看法,畢竟每個人的標準和在意的點都不同,但這就是我最直接的體驗分享。

接下來,我會用表格的方式,盡量把我還記得住的各個項目,做一個詳細的比較。如果硬要先給個簡單結論,我會說:名勝世界郵輪的 CP 值真的高到破表,而且「中文優先」的環境非常加分;但我個人實在無法接受它五樓公共區域那瀰漫的菸味。

那麼,就讓我們開始這次的比較吧!

比較總表

比較面向 MSC 地中海郵輪 名勝世界郵輪 (Resorts World Cruises) 你的觀察/備註
整體印象/氛圍 奢華感、歐式風情、國際化 親民、亞洲熟悉感、"中文優先" RWC 可能更貼近台灣旅客習慣
價格定位/CP值 感覺較高價 CP值高、價格相對親民 RWC CP值勝出
語言服務 國際語言為主 "中文優先" (服務人員、廣播) RWC 對中文旅客極友善
自助餐 品項多、變化少、口味偏西式 品項相對少、每日變化、口味合台灣胃 RWC 口味/變化勝,MSC 品項多
主餐廳 傳聞出餐慢、菜色與自助餐相似? 可能有龍蝦夜 早餐與自助餐有區隔 (佳) RWC 主餐廳感覺較用心?
飲水提供 付費瓶裝水 / 自助餐水 (口感差) 免費飲水機 (水有味需過濾) RWC 提供免費飲水是優點
娛樂活動 強調視覺 (天幕/名店街),互動性可能較弱 大廳活動多 (電影/遊戲/參與度高),感覺更吸引人 RWC 活動感覺更親民、互動性高
賭場 感覺是重點設施,吸引不少客人 (視讀者是否在意賭場)
客艙 (內艙) 基本體驗差異不大 基本體驗差異不大 (平手)
服務品質 主觀感覺較佳 中文溝通便利 MSC 服務感受好,RWC 語言無礙
兒童設施 有<2歲空間 (玩具舊)、有室內泳池 僅2-12歲空間,無室內泳池 MSC 對帶小小孩 & 壞天氣時較友善
游泳池 有室內泳池 無室內泳池 MSC 勝
吸菸政策/執行 (未遇問題) 嚴重缺點: 5樓室內吸菸嚴重,氣味擴散,管理待加強 RWC 的重大負面問題 (MSC 在此項因無負面描述而勝出)
岸上觀光 通常不包含 優點:包含免費岸上觀光 RWC 包含岸上觀光是優勢
WiFi 日計費 日計費 (平手 - 計費方式類似)
下船效率 排超久 可能因人較少排隊較快? (主觀感受) (RWC 略佔優勢?)

優缺點總結

附帶一提,在MSC的所有廣播都是先英文、日文、韓文,最後才是中文;在名勝世界的時候絕對都是先中文再英文,那種感覺是完全不一樣的。

MSC 地中海郵輪

優點:
  • 歐式奢華體驗
  • 服務品質感受佳
  • 有室內泳池
  • 有<2歲兒童空間
缺點:
  • 價格可能較高
  • 餐飲口味/變化較不合台灣胃
  • 飲水不便
  • 中文服務可能較少 (廣播順序可見一斑)

名勝世界郵輪 (Resorts World Cruises)

優點:
  • CP值高
  • "中文優先"環境
  • 餐飲口味/變化適合台灣人
  • 免費飲水機
  • 含岸上觀光
  • 活動互動性高
缺點:
  • 室內吸菸問題嚴重
  • 無室內泳池
  • 無<2歲兒童空間

給讀者的選擇建議

推薦「名勝世界郵輪」給:

  • 追求高 CP 值、預算考量者。
  • 重視中文溝通便利性。
  • 偏好符合亞洲/台灣口味餐飲。
  • 希望行程包含岸上觀光。
  • 能接受(或本身是)吸菸環境的旅客

推薦「MSC 地中海郵輪」給:

  • 偏好歐式氛圍與較高服務質感。
  • 需要室內泳池或帶<2歲幼兒同行。
  • 對室內菸味零容忍的旅客
  • 對餐飲口味接受度較廣,或願意嘗試付費餐廳。

2025年3月4日 星期二

Dify + Whisper Asr Webservice

Dify + Whisper Asr Webservice

這邊假設你已經成功啟動 dify 的服務

Whisper Asr Webservice

Whisper Asr Webservice 是一個很方便的工具,因為作者已經幫忙把語音轉文字的服務 API 化,對於應用端來說只要做常規的 API 呼叫就可以滿足應用上的需求。

啟動 Whisper Asr Webservice

啟動服務最快的方法就是使用 Docker:

docker run -d -p 9000:9000 -e ASR_MODEL=base -e ASR_ENGINE=openai_whisper \
onerahmet/openai-whisper-asr-webservice:latest
docker run -d --gpus all -p 9000:9000 -e ASR_MODEL=base -e ASR_ENGINE=openai_whisper \
onerahmet/openai-whisper-asr-webservice:latest-gpu

不過要注意自己的 CUDA 版本是否有對應,如果最新版的無法啟動的話就要降版,這取決於你的硬體設備。

OpenAPI 設置

成功啟動 docker container 之後,可以直接連接到 http://你的ip:9000/docs ,沒意外的話你會看到下圖:

openapi image

點擊紅框處的 openapi.json,系統會開啟一個新的視窗顯示一堆 JSON 格式的字串,複製出來後進行以下處理:

  1. infopaths 中間加入 servers
  2. 修正 requestBody 中的 content-type,改為 multipart/form-data; boundary=----WebKitFormBoundarydzemBAPhdeDfTCfR
更多關於模型的選擇與細部配置請參考 Whisper Asr Webservice 官方文件說明。

Dify - 自定義工具設置

進入 Dify 後,點擊「工具」→「建立自定義工具」,貼上剛剛修改的 OpenAPI json 即可建立成功。

custom tool

Dify - 應用流程開發

現在可以在 workflow 中取用自定義的語音轉文字工具:

workflow

設定參數:

params

測試應用:

test

將文字結果轉成問答集:

問題:客戶在聯絡退稅服務時需要提供哪些信息? 答案:...(省略部分內容)... 問題:退稅作業需要多久時間? 答案:退稅大約需要一個星期。 ...(更多問答內容)...

若有興趣,可以將問答轉為主持人與來賓對話,發展成 Podcast 節目;若是公司會議紀錄,也能每日播放當作內部溝通素材。