Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
pull_request:

jobs:
test:
python-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -21,3 +21,50 @@ jobs:
DATABASE_URL: sqlite:///./data/test.db
CHROMA_DIR: ./data/test_chroma
run: pytest -q

frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.19.0"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Install Chromium for browser smoke
run: npx playwright install --with-deps chromium
- name: Run browser smoke
run: npm run test:smoke

deno-api:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
RATE_LIMIT_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.4.5
- name: Type-check API and tests
run: deno check supabase/functions/api/index.ts supabase/functions/api/*_test.ts
- name: Run Deno API tests
run: deno test --allow-env --allow-net --allow-read supabase/functions/api/
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@

Hands-on tutorial project for learning **LangChain**, **RESTful API**, **Dify**, and **WebHooks**.

本機使用 FastAPI + LangChain + SQLite;正式網站使用 GitHub Pages + Supabase Edge Functions + Supabase Database。預設 `LLM_PROVIDER=mock`,**不需要 API Key** 就能開始練 REST 與 RAG 流程。
本機使用 FastAPI + LangChain + SQLite。預設 `LLM_PROVIDER=mock`,**不需要 API Key** 就能開始練 REST 與 RAG 流程。

> ⚠️ **線上 API 已停用(2026-09-21)**:原本的 Supabase 專案已移除,[GitHub Pages 網站](https://frobel0520.github.io/AI-Agent-Tutorial/)只剩前端畫面,筆記、RAG、WebHook 與 Dify 功能在線上都無法使用。要實際練習請看下方「快速開始(本機)」。Supabase 相關程式與[部署文件](deploy/github-supabase-deploy.md)保留,供日後重建。

## 學習路徑

| Phase | 主題 | 文件 |
|-------|------|------|
| 1 | RESTful API + LangChain RAG | [docs/01-rest-api.md](docs/01-rest-api.md), [docs/02-langchain.md](docs/02-langchain.md) |
| 2 | WebHook | [docs/03-webhook.md](docs/03-webhook.md) |
| 3 | Dify 整合 | [docs/04-dify.md](docs/04-dify.md) |

## 需求
| 3 | Dify 整合 | [docs/04-dify.md](docs/04-dify.md) |

## 開發流程(SDLC)

本專案沿用 Planning → SA → SD/ADR → Task DAG → CI/Release Gate 的交付流程:

- [Project Plan](docs/01-project-plan.md)
- [System Analysis](docs/02-system-analysis.md)
- [System Design / ADR](docs/03-system-design.md)
- [MVP Release Gate](docs/04-mvp-release-gate.md)

功能開發使用 feature/<task-id>-<slug> 分支,依序經過 dev、main,並以 GitHub Actions 部署 GitHub Pages 與 Supabase Edge Function。

## 需求

- Python 3.11+
- (選用)Docker Desktop — Ollama 本機模型、Dify 自架
Expand Down Expand Up @@ -85,6 +98,8 @@ pytest -q

## 上線部署(GitHub Pages + Supabase)

> 目前**未部署**:Supabase 專案已移除,repository variables 已清空,`Deploy Supabase Edge Function` workflow 已停用。以下是重建時的步驟;重建後重新設定 variables、啟用 workflow,網站的停用提示就會自動消失。

正式環境不使用 Render。架構如下:

- GitHub Pages:發布 `frontend/` 的 React + Vite 學習台(建置產物為 `frontend/dist`)
Expand Down
4 changes: 4 additions & 0 deletions deploy/github-supabase-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ React 前端原始碼在 `frontend/src/`。GitHub Actions 會先執行 `npm ci`
1. 在 Supabase 建立專案。
2. SQL Editor 執行 `supabase/schema.sql`。
3. 若資料表已存在,再執行 `supabase/migrations/20260830000000_enable_rls_for_edge_api.sql` 與 `20260831000000_add_dify_access.sql`。
4. **一律執行** `supabase/migrations/20260909000000_add_edge_rate_limit.sql`(API 限流;`schema.sql` 沒有包含)。

這個專案的瀏覽器請求全部經過 Edge Function;RLS migration 會阻擋瀏覽器直接讀寫資料表。Edge Function 使用 server-only service role key,因此該 key 絕不能放在 GitHub Pages。

Expand All @@ -38,6 +39,7 @@ React 前端原始碼在 `frontend/src/`。GitHub Actions 會先執行 `npm ci`
| `GOOGLE_API_KEY` | 使用 Gemini 時 | Google AI API key |
| `GEMINI_MODEL` | 使用 Gemini 時 | 有效的 Gemini model 名稱 |
| `WEBHOOK_SECRET` | 否 | 訂閱未提供個別 secret 時使用 |
| `WEBHOOK_ALLOWED_URLS` | 使用 WebHook 時 | 允許送出的 HTTPS 網址,逗號分隔、完全相同才放行;留空則註冊回 503、既有訂閱送出記為失敗 |
| `CORS_ORIGINS` | 否 | 逗號分隔的允許來源;空白時為公開教學模式 |
| `DIFY_API_BASE` | 使用 Dify 時 | Dify 的 `/v1` API URL |
| `DIFY_API_KEY` | 使用 Dify 時 | Dify Chat App API key |
Expand All @@ -54,6 +56,8 @@ supabase functions deploy api

## 3. 部署 Edge Function

> ⚠️ push 到 `main` 且改到 `supabase/functions/**` 時,`.github/workflows/supabase-functions.yml` 會**自動部署** Edge Function,但**不會**執行 migration。含新 migration 的 PR 進 `main` 之前,先在 SQL Editor 執行該 migration(目前是 `20260909000000_add_edge_rate_limit.sql`),並設定好 `WEBHOOK_ALLOWED_URLS`。限流表不存在時 API 會 fail closed,除了 `GET /health` 以外都回 503。

手動部署:

```powershell
Expand Down
6 changes: 6 additions & 0 deletions deploy/supabase-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
1. Supabase Dashboard → **SQL Editor**
2. 貼上並執行本 repo 的 `supabase/schema.sql`
3. 若資料表原本已存在,再執行 `supabase/migrations/20260830000000_enable_rls_for_edge_api.sql`
4. **一律執行** `supabase/migrations/20260909000000_add_edge_rate_limit.sql`(API 限流;`schema.sql` 沒有包含)

RLS migration 會阻止 `anon` 與 `authenticated` 直接讀寫這四張表;Edge Function 使用 server-only service role key 執行資料庫操作。

> ⚠️ 限流 migration 必須在部署新版 Edge Function **之前**完成。限流表不存在時 API 會 fail closed,除了 `GET /health` 以外的路由都回 503。

## Step 3 — 設定 Function Secrets

在 Supabase Dashboard → **Edge Functions → Secrets** 設定:
Expand All @@ -34,8 +37,11 @@ RLS migration 會阻止 `anon` 與 `authenticated` 直接讀寫這四張表;Ed
SUPABASE_SERVICE_ROLE_KEY=<Project Settings → API 的 service role key>
LLM_PROVIDER=mock
WEBHOOK_SECRET=<一組隨機字串>
WEBHOOK_ALLOWED_URLS=https://webhook.site/<your-id>
```

`WEBHOOK_ALLOWED_URLS` 是允許送出 WebHook 的 HTTPS 網址清單,逗號分隔,必須**完全相同**才放行(不支援萬用字元,不可含 port、帳密或 `#`)。留空時 WebHook 功能停用:註冊回 503,既有訂閱每次送出都記為失敗。只放你信任、且不會解析到內網的 endpoint。

若要使用 Groq:

```env
Expand Down
118 changes: 118 additions & 0 deletions docs/01-project-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Project Plan — AI-Agent-Tutorial

| 項目 | 內容 |
|---|---|
| 文件版本 | 1.0 |
| 文件狀態 | SDLC baseline |
| 更新日期 | 2026-09-09 |
| 產品定位 | 給初學者的 LangChain、REST API、WebHook、Dify 實作教學 |
| 預算約束 | 以 USD 0 的託管方案為預設,付費服務只能是明確的選配 |
| 正式部署 | GitHub Pages + Supabase |

## 1. 目標與成功條件

本專案要讓沒有先備程式經驗的學習者,能沿著學習路徑完成:

1. 建立與測試 REST API。
2. 建立筆記並理解簡易 RAG 問答。
3. 建立 WebHook、觀察事件 payload,理解簽章與事件紀錄。
4. 使用 Google 登入後,讓被授權的帳號呼叫 Dify。
5. 在不使用 Render 的前提下,從 GitHub Pages 完成正式網站部署。

成功條件不是只有「頁面能開」,而是每個學習步驟都有可重現的輸入、輸出、驗收方式與失敗說明。

## 2. 範圍

### In scope

- React + Vite 學習台,部署到 GitHub Pages。
- Supabase Auth、Postgres、RLS 與 Edge Function API。
- 本機 FastAPI 路線,作為 REST/LangChain/Dify 的教學與除錯環境。
- Google SSO、Dify 存取授權與伺服器端密鑰邊界。
- WebHook 註冊、簽章、事件接收與事件紀錄。
- 可在免費額度內運作的 LLM/Dify 整合方式。
- 可由 CI 與 release gate 重複執行的驗收流程。

### Out of scope

- Render、長駐付費 VM 或自建高可用 Dify 叢集。
- 把 Dify API key、Supabase service-role key 或 LLM key 放進前端。
- 以本專案取代正式企業級 webhook broker、queue 或 SIEM。
- 在沒有需求與驗收條件前,先做大型前端重構或資料庫換代。

## 3. 現況基線

| 層 | 目前責任 | 主要位置 |
|---|---|---|
| 學習台 | React/Vite UI、Google 登入、教學步驟 | frontend/ |
| 正式 API | Supabase Edge Function,提供 health、notes、ask、webhooks、events、dify/ask | supabase/functions/api/ |
| 正式資料 | Supabase Postgres、RLS、Dify access table | supabase/ |
| 本機 API | FastAPI、SQLite、LangChain、本機 Dify/Ollama 教學路線 | src/、static/ |
| 部署 | GitHub Actions 發布 Pages 與 Supabase Edge Function | .github/workflows/ |

正式網站不依賴 Render。static/ 保留為本機 FastAPI 的舊版回退頁面與教學素材,不等於正式網站的第二套部署目標。

## 4. 需求基線

| ID | 需求 | 驗收結果 |
|---|---|---|
| FR-01 | 訪客可開啟學習台並看到目前學習步驟 | 頁面載入成功,API 狀態可辨識 |
| FR-02 | 使用者可建立、查詢共用教學筆記 | 明確標示共用沙盒、受共享額度限制;不宣稱個人資料隔離 |
| FR-03 | 使用者可送出問題並看到 RAG 回答與來源 | 失敗時顯示可理解的錯誤 |
| FR-04 | WebHook 事件可被送出、接收、驗證並留下紀錄 | 簽章錯誤與逾時可觀察 |
| FR-05 | Google 登入後,只有被授權帳號可以呼叫 Dify | 未登入為 401,未授權為 403 |
| FR-06 | push 到 main 可觸發正式部署 | Pages 與 Edge Function workflow 各自可驗證 |
| FR-07 | 文件與實際路由、環境變數、部署方式一致 | release gate 不允許明顯過時說明 |

## 5. 非功能需求

| 類別 | 基線要求 |
|---|---|
| 成本 | 預設不需要 Render;服務選擇需符合目前免費額度 |
| 安全 | 前端只拿公開 URL;敏感 key 只存在 Supabase secrets 或本機環境 |
| 身分 | 受保護功能使用 Supabase Auth;授權判斷在伺服器端完成 |
| 可用性 | API 失敗、Dify 未設定、tunnel 中斷都要有明確狀態 |
| 效能 | 教學資料量小時保持簡單;超過基線前不引入不必要的基礎設施 |
| 可測試性 | Python 測試、前端 build、部署設定檢查可在 CI 重跑 |
| 可維運性 | 每次 release 有 smoke、rollback 方式與已知限制 |

## 6. 里程碑

| Milestone | 內容 | 狀態 |
|---|---|---|
| M0 | GitHub Pages + Supabase baseline、React runtime、Dify access | 已完成,基準為 origin/main |
| M1 | 建立本文件、SA、SD/ADR、Task DAG、Release Gate | 本分支進行中 |
| M2 | 收斂 Auth、CORS、輸入限制與 webhook SSRF/replay 風險 | 待排程 |
| M3 | 補齊前端 runtime smoke、CI frontend gate 與 secrets 檢查 | 待排程 |
| M4 | 依驗收證據發布下一個可教學版本 | 待排程 |

## 7. 風險與處置

| 風險 | 影響 | 處置 |
|---|---|---|
| Edge API 的公開路由可能被濫用 | 資料、LLM 額度與 webhook 受影響 | M2 定義訪客/登入者/管理者權限矩陣並實作 |
| WebHook 目標 URL 可能形成 SSRF | 伺服器被利用存取內網 | 只允許安全目標,阻擋 private/link-local/metadata 位址並限制 redirect |
| Dify 使用免費方案或本機 tunnel | URL/額度不穩定 | UI 顯示狀態;文件記錄 tunnel 生命週期與替代路線 |
| React build 通過但瀏覽器白屏 | 使用者無法學習 | 加入 runtime smoke、錯誤邊界與部署後 smoke |
| 依賴未鎖定或 workflow 使用 latest | build 漂移 | 固定 Node/Supabase CLI/Python 依賴版本 |

## 8. Git 與交付規則

- production branch:main;只接受通過 gate 的變更。
- integration branch:dev;需要跨功能整合或 preview 時使用。
- task branch:feature/<task-id>-<slug>;修 bug 用 fix/<slug>,測試用 test/<slug>。
- 變更流程:feature → dev → main → GitHub Pages/Supabase deploy。
- 每個 task contract 必須有 Goal、Input contract、Output artifact、Out of scope、Acceptance checks、depends_on 與 evidence path。
- 不直接在 main 上開發;不使用 reset --hard 丟棄工作。
- secrets 不進 Git;本機與 CI 都以範例檔提供名稱,不提供真值。

## 9. Definition of Done

一個功能只有在下列條件都成立時才算完成:

- 需求、SA、SD 或 ADR 已更新,且與實作一致。
- 驗收案例至少涵蓋成功、未授權/錯誤與邊界情境。
- 相關 Python test、frontend build、必要的 browser smoke 與設定檢查通過。
- 沒有未處理的 critical/high 風險;若有明確 waiver,需記錄期限與 owner。
- release note、部署步驟、rollback 與已知限制已更新。
- PR 只包含單一可理解的 task,並保留可追溯的 evidence path。
106 changes: 106 additions & 0 deletions docs/02-system-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# System Analysis — AI-Agent-Tutorial

| 項目 | 內容 |
|---|---|
| 文件版本 | 1.0 |
| 文件狀態 | SA baseline |
| 更新日期 | 2026-09-09 |
| 對應計畫 | docs/01-project-plan.md |

## 1. 系統邊界

~~~text
學習者瀏覽器
├─ GitHub Pages React/Vite
│ ├─ Supabase Auth(Google)
│ └─ Supabase Edge Function API
│ ├─ Postgres + RLS
│ ├─ LLM provider
│ └─ Dify API(只有授權帳號)
└─ 本機 FastAPI(教學/除錯路線,不是正式託管)
~~~

瀏覽器是不可信的 client。任何「只有登入者可以」或「只有被授權者可以」的規則,都必須在 Supabase Edge Function 或資料庫 RLS 再判斷一次。

## 2. Actors

| Actor | 目標 | 信任等級 |
|---|---|---|
| 訪客 | 閱讀教學、查看公開狀態 | 不可信 |
| Google 使用者 | 建立筆記、使用受保護的學習功能 | 已驗證但仍不可信 |
| Dify 授權使用者 | 呼叫 Dify workflow | 已驗證且通過 allowlist |
| WebHook sender | 向接收端送出簽章事件 | 以 secret/簽章驗證 |
| Maintainer | 管理 secrets、授權帳號、發布版本 | 受控管理者 |
| GitHub Actions | 建置與部署 | CI workload identity/repository secrets |

## 3. 主要用例與驗收

| Use case | 前置條件 | 主要流程 | 失敗驗收 |
|---|---|---|---|
| UC-01 開啟學習台 | Pages 可用 | 載入 React,取得 API health | API 失敗時不得白屏,顯示目前狀態 |
| UC-02 Google 登入 | Supabase Auth provider 已設定 | 登入、回到 callback、保存 session | 取消登入可返回教學台,不洩漏 token |
| UC-03 建立筆記 | 共用教學沙盒額度可用 | POST notes、回傳 note id、刷新列表 | 欄位超限 4xx、額度超限 429 |
| UC-04 RAG 問答 | 有可檢索的筆記 | POST ask、回傳 answer 與 sources | LLM timeout/額度耗盡顯示可理解錯誤 |
| UC-05 WebHook | 有安全且允許的 target | 建立訂閱、送事件、驗證簽章、寫 event log | 無效簽章、逾時、重播可被拒絕或去重 |
| UC-06 Dify 問答 | Google session + dify_access | Edge 驗證 JWT、查 allowlist、伺服器端呼叫 Dify | 401/403 與 Dify 502 分開呈現 |
| UC-07 發布 | PR 通過 gate | merge main、Pages/Edge workflows 部署 | 必要設定缺失時 workflow fail fast |

## 4. 介面與資料契約

| 介面 | Client | 認證基線 | 輸出重點 |
|---|---|---|---|
| GET /health | Pages/維運 | 公開狀態可接受 | API、provider、Dify 設定狀態 |
| GET/POST/PUT/DELETE /notes | Pages | 共用公開教學沙盒,Edge 共享限流 | 筆記資料與 note id |
| POST /ask | Pages | 公開但有共享額度 | answer、sources、錯誤碼 |
| GET/POST/DELETE /webhooks | Pages | Supabase JWT + dify_access 操作員 | subscription id;不回傳 secret |
| GET /events | Pages | Supabase JWT + dify_access 操作員 | 共用教學事件紀錄 |
| POST /hooks/incoming | sender/Edge | HMAC;timestamp/nonce 防重播仍待實作 | accepted event、拒絕原因 |
| POST /dify/ask | Pages | Supabase JWT + dify_access | Dify answer、provider error |

所有 request body 都需要長度、格式與數量上限。錯誤回應應保持一致,至少包含可供 UI 顯示的 code 與不含 secret 的 message。

## 5. 身分與授權矩陣

| 能力 | 訪客 | 已登入 | Dify allowlist | Maintainer |
|---|---:|---:|---:|---:|
| 看公開教學與 health | ✓ | ✓ | ✓ | ✓ |
| 讀取/寫入共用教學筆記 | 依 quota | 依 quota | 依 quota | 依 quota |
| 呼叫一般 RAG | 依 quota | 依 quota | 依 quota | 依 quota |
| 管理 webhook subscription/查看事件 | ✗ | ✗ | ✓ | 需列入 allowlist |
| 呼叫 Dify | ✗ | ✗ | ✓ | ✓ |
| 授權 Dify 帳號 | ✗ | ✗ | ✗ | ✓ |
| 讀取 secrets | ✗ | ✗ | ✗ | 僅 Supabase/CI secret store |

此矩陣描述 SEC-QA 增量的目標行為,只有部署並驗收後才能宣稱線上已生效。一般筆記與 RAG 刻意保留公開沙盒政策,不具使用者資料隔離;勿放私人資料。所有操作員共用訂閱與事件紀錄。

## 6. 資料與所有權

| 資料 | 儲存處 | owner/保護規則 |
|---|---|---|
| auth user | Supabase Auth | Supabase 管理 |
| notes | Postgres notes | 共用教學資料;RLS 阻止瀏覽器直連,Edge service role 依路由政策存取 |
| dify_access | Postgres dify_access | 只有管理流程可新增/撤銷 |
| webhook_subscriptions | Postgres | allowlist 操作員共用管理;target 需通過可信 URL policy |
| event_logs | Postgres | 僅 allowlist 操作員查閱 |
| Dify/LLM key | Supabase Function Secrets | 永不回傳前端或寫入 log |

## 7. 邊界、威脅與非功能案例

| 情境 | 期待行為 |
|---|---|
| 沒有 Authorization header | 受保護路由回 401,不進入下游服務 |
| JWT 有效但沒有 dify_access | Dify 回 403,不消耗 Dify 額度 |
| 使用者提供內網 webhook URL | 建立請求回 4xx,不發出 outbound request |
| webhook 重送相同 event | 以 idempotency/nonce policy 去重或明確拒絕 |
| Dify/LLM timeout | bounded timeout、可辨識的 502/504,不卡住 request |
| 超長 note 或 question | 4xx,不把未限制輸入送到 LLM |
| CORS 來自未知 origin | 不授權 credentialed request |
| 前端 JS runtime exception | Error Boundary/fallback 顯示診斷,不呈現全白頁 |

## 8. SA Exit Criteria

- Actors、trust boundary、route contract、auth matrix 已被 SD 引用。
- 每個高風險流程至少有一個拒絕案例。
- 未決定的公開/登入策略被列入 task,而不是默認為安全。
- 資料 owner、RLS、secret boundary 已有明確責任位置。
- 能從本文件產出 feature task contract 與可重跑的 acceptance checks。
Loading
Loading