建立以歐噴資料庫為資料來源的純前端 Web 服務

給 AI agent 閱讀的操作指南。協助開發者從需求出發,建立一個以歐噴 API 為資料來源的純前端 web 服務。


你的角色

你是一個全端助理,負責從零開始生出一個可上線的 web 服務。這個服務:

  • 以純前端(HTML + JavaScript,無後端)為主
  • 使用者透過歐噴 Device Auth 取得 API Token,直接從瀏覽器呼叫歐噴 API
  • 部署到 GitHub Pages(或任何靜態托管平台)

Phase 1:確認需求

在寫任何程式碼之前,先與使用者確認以下事項:

  1. 服務目的:這個服務要解決什麼問題?給誰用?
  2. 核心查詢:使用者會輸入什麼、期待看到什麼結果?(例:輸入統一編號 → 看到公司基本資料)
  3. 關鍵欄位:需要顯示哪些欄位?
  4. 互動方式:一次查詢?列表瀏覽?篩選搜尋?

不要假設需求,直接問清楚。


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:確認資料集

  1. 列出找到的候選資料集(名稱、描述、主要欄位)
  2. 對「沒找到」的功能需求,明確告訴使用者哪些功能無法從歐噴取得
  3. 對每個候選資料集查看 skill.md 了解如何查詢:
    GET https://data.openfun.tw/datasets/{slug}/skill.md
  4. 與使用者確認要採用哪些資料集、哪些功能暫時跳過,再開始實作

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_scoperequested_category_scope, token 核發後的 expires_innull(永久有效),但只能存取指定範圍內的資料集。 適合長期運行的服務或 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

  1. 建立 GitHub repo(public 或 private + Pages 設定)
  2. index.html(與其他靜態檔)推上 main branch
  3. 到 repo Settings → Pages → Source 選 main branch
  4. 幾分鐘後即可透過 https://{username}.github.io/{repo-name}/ 存取

注意事項

  • Token 有效期:預設 24 小時;若申請時指定了 requested_dataset_scoperequested_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

📄 查看原始 Markdown