建立以歐噴資料庫為資料來源的純前端 Web 服務
給 AI agent 閱讀的操作指南。協助開發者從需求出發,建立一個以歐噴 API 為資料來源的純前端 web 服務。
你的角色
你是一個全端助理,負責從零開始生出一個可上線的 web 服務。這個服務:
- 以純前端(HTML + JavaScript,無後端)為主
- 使用者透過歐噴 Device Auth 取得 API Token,直接從瀏覽器呼叫歐噴 API
- 部署到 GitHub Pages(或任何靜態托管平台)
Phase 1:確認需求
在寫任何程式碼之前,先與使用者確認以下事項:
- 服務目的:這個服務要解決什麼問題?給誰用?
- 核心查詢:使用者會輸入什麼、期待看到什麼結果?(例:輸入統一編號 → 看到公司基本資料)
- 關鍵欄位:需要顯示哪些欄位?
- 互動方式:一次查詢?列表瀏覽?篩選搜尋?
不要假設需求,直接問清楚。
Phase 2:取得 Token 並搜尋資料集
步驟 1:先取得 Bearer Token
搜尋私有資料集需要登入。在搜尋之前,先引導使用者完成 Device Auth,取得 Bearer Token:
// 發起授權(device_name 可填服務名稱)
const resp = await fetch('https://data.openfun.tw/api/v1/auth/device', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ device_name: '開發者驗證' })
});
const data = await resp.json();
// 引導使用者前往 data.verification_uri_complete 授權
// 然後輪詢 /api/v1/auth/token 直到取得 access_token
或使用 MCP tool openfun_guide 了解取得 Token 的方式。
取得 token 後,後續所有 API 呼叫都帶上 Authorization: Bearer {token}。
步驟 2:搜尋相關資料集
優先使用 MCP tool openfun_search 搜尋;若無法使用,則呼叫搜尋 API:
GET https://data.openfun.tw/api/v1/search?q={關鍵字}
Authorization: Bearer {token}
搜尋停損規則:每個功能需求最多試 2 個關鍵字變體。若都沒有找到相關資料集,直接記錄為「歐噴資料庫目前無對應資料集」並告知使用者,不繼續換關鍵字重試。
步驟 3:確認資料集
- 列出找到的候選資料集(名稱、描述、主要欄位)
- 對「沒找到」的功能需求,明確告訴使用者哪些功能無法從歐噴取得
- 對每個候選資料集查看 skill.md 了解如何查詢:
GET https://data.openfun.tw/datasets/{slug}/skill.md - 與使用者確認要採用哪些資料集、哪些功能暫時跳過,再開始實作
Phase 3:實作
技術架構
- 單一 HTML 檔(
index.html)+ 可選的style.css - 純 JavaScript(無框架,或用 Alpine.js 等輕量選項)
- Token 存在
sessionStorage(關分頁自動清除) - 直接從瀏覽器打歐噴 API(支援 CORS)
Device Auth 流程
歐噴採用 RFC 8628 Device Authorization Grant,適合無後端的 web 應用。
Step 1:發起授權,取得 user_code
const resp = await fetch('https://data.openfun.tw/api/v1/auth/device', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
device_name: 'KYC Service', // 顯示在授權頁的服務名稱(可自訂)
requested_identity_scope: ['email', 'display_name'], // 申請取得的使用者資訊欄位(可省略)
// 若只需特定資料集,可指定 scope → 使用者核准後發永久 Token(無 scope 則 24 小時)
// requested_dataset_scope: ['tw.gov.nhi~entity~pbrs-meeting'], // 最多 3 個 dataset slug
// requested_category_scope: ['tw.nhi'], // 最多 3 個 category slug
// 兩者可同時指定,存取控制為聯集
})
});
const data = await resp.json();
// data.user_code → 使用者輸入的驗證碼(例:ABCD-1234)
// data.verification_uri → 使用者前往授權的網址
// data.verification_uri_complete → 預填 code 的網址(可直接開啟)
// data.device_code → 輪詢用(不給使用者看)
// data.interval → polling 間隔秒數(5)
// data.expires_in → 1800 秒後失效
// data.has_scope → true 代表本次申請帶有資料集/分類 scope
requested_identity_scope:填寫後,授權頁會向使用者說明「此服務將取得哪些身份資訊」。 可填['display_name'](只取暱稱)、['email', 'display_name'](取 email 和暱稱)或省略(不取身份資訊)。
限定 scope 換取永久 Token:若指定了
requested_dataset_scope或requested_category_scope, token 核發後的expires_in為null(永久有效),但只能存取指定範圍內的資料集。 適合長期運行的服務或 CI/CD 流程,不需要使用者定期重新授權。 scope 僅接受公開資料集或已存在的 category slug,最多各 3 個。
Step 2:引導使用者授權
// 建議直接用 verification_uri_complete(預填 code,使用者只需點確認)
window.open(data.verification_uri_complete, '_blank');
Step 3:輪詢等待授權完成
const pollInterval = setInterval(async () => {
const tokenResp = await fetch('https://data.openfun.tw/api/v1/auth/token', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
grant_type: 'device_code',
device_code: data.device_code
})
});
const tokenData = await tokenResp.json();
if (tokenData.error === 'authorization_pending') return; // 繼續等
if (tokenData.error) {
clearInterval(pollInterval);
showError('授權失敗:' + tokenData.error);
return;
}
// 成功:tokenData.access_token
clearInterval(pollInterval);
sessionStorage.setItem('openfun_token', tokenData.access_token);
// tokenData.expires_in = 86400(24 小時)或 null(永久,當申請時有帶 scope)
onAuthenticated(tokenData.access_token);
}, data.interval * 1000);
呼叫歐噴 API
async function openfunFetch(path, params = {}) {
const token = sessionStorage.getItem('openfun_token');
if (!token) throw new Error('尚未登入');
const url = new URL('https://data.openfun.tw' + path);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const resp = await fetch(url, {
headers: {'Authorization': 'Bearer ' + token}
});
if (resp.status === 401) {
sessionStorage.removeItem('openfun_token');
throw new Error('Token 已過期,請重新登入');
}
if (!resp.ok) throw new Error('API 錯誤:' + resp.status);
return resp.json();
}
// 使用範例:查詢資料集
const result = await openfunFetch('/api/v1/datasets/{slug}/records', {
'欄位名稱': '查詢值',
per_page: 20
});
// result.records → 資料列陣列
// result.total → 總筆數
頁面結構範本
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服務名稱</title>
</head>
<body>
<!-- 登入區(未登入時顯示) -->
<div id="auth-section">
<h2>請先登入歐噴資料庫</h2>
<button onclick="startAuth()">使用歐噴帳號登入</button>
<div id="auth-status"></div>
</div>
<!-- 主功能區(登入後顯示) -->
<div id="app-section" style="display:none">
<!-- 查詢表單 -->
<input id="query-input" type="text" placeholder="請輸入查詢條件">
<button onclick="doSearch()">查詢</button>
<!-- 結果區 -->
<div id="results"></div>
</div>
<script>
// 頁面載入時檢查 token
window.addEventListener('load', () => {
const token = sessionStorage.getItem('openfun_token');
if (token) showApp();
});
function showApp() {
document.getElementById('auth-section').style.display = 'none';
document.getElementById('app-section').style.display = 'block';
}
async function startAuth() {
// 實作 Device Auth 流程(見上方)
}
async function doSearch() {
// 呼叫 openfunFetch() 查詢資料
}
</script>
</body>
</html>
Phase 4:部署到 GitHub Pages
- 建立 GitHub repo(public 或 private + Pages 設定)
- 把
index.html(與其他靜態檔)推上mainbranch - 到 repo Settings → Pages → Source 選
mainbranch - 幾分鐘後即可透過
https://{username}.github.io/{repo-name}/存取
注意事項
- Token 有效期:預設 24 小時;若申請時指定了
requested_dataset_scope或requested_category_scope,則核發永久 Token(expires_in: null),但只能存取指定的資料集/分類範圍 - sessionStorage vs localStorage:sessionStorage 關分頁即清除(較安全);若希望跨分頁保留登入狀態,改用 localStorage,但請告知使用者
- 歐噴 CORS:已支援跨網域請求,瀏覽器直打 API 不需 proxy
- 資料集權限:使用者只能查詢自己有權限存取的資料集;無權限的資料集 API 會回 403
- 不要在 URL query string 放 token:token 只放 Authorization header 或 sessionStorage
- 顯示使用者名稱:取得 token 後可呼叫
GET /api/v1/me取得{email, display_name}
快速參考
| 項目 | 端點 |
|---|---|
| 發起 Device Auth | POST https://data.openfun.tw/api/v1/auth/device |
| Polling token | POST https://data.openfun.tw/api/v1/auth/token |
| 查詢使用者資訊 | GET https://data.openfun.tw/api/v1/me |
| 查詢資料 | GET https://data.openfun.tw/api/v1/datasets/{slug}/records |
| 搜尋資料集 | GET https://data.openfun.tw/api/v1/search?q={keyword} |
| 讀 skill.md | GET https://data.openfun.tw/datasets/{slug}/skill.md |