mathruffian-dot/opencode-lazy-packs/05-sheets-gas

Claude Code and agent skill by mathruffian-dot/opencode-lazy-packs

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

用 Google 試算表當資料庫、Apps Script 網頁應用程式當後端,讓網頁作品「關掉還記得住」。零安裝、不用 clasp、不用 Node。說「用試算表存資料」「GAS 後端」「表單收資料」「讓網頁能存東西」「做課堂回饋牆」「做線上報名表」時載入。

Install

npx vskill@latest install mathruffian-dot/opencode-lazy-packs/05-sheets-gas

SKILL.md

Google Sheets + Apps Script 網頁應用程式(懶人包 #05)

前面幾包做出來的網頁,關掉就什麼都沒了。 這一包讓作品記得住事情:學生填的、投的、交的,全部進到一份 Google 試算表。


這包負責什麼、不負責什麼

負責

  • 用一份 Google 試算表當資料庫
  • 用 Apps Script 的「網頁應用程式」同時當前端網頁和後端
  • 部署成一個學生可以直接開的網址,縮短並產出 QR Code
  • 改版之後網址不變(QR Code 不用重印)

不負責

  • 多人即時同步(文字雲、搶答、即時排行榜)→ 見下方「什麼時候不該走這條路」
  • 學生登入、身分驗證、成績系統
  • clasp、Node.js、任何要在電腦上安裝的東西——這包全程零安裝

⚠️ 先講清楚:這包為什麼不用 clasp

網路上大多數的 GAS 教學都在教 clasp(Google 官方的命令列工具,可以把本機的程式碼推上 Apps Script)。 本包刻意不走那條路。理由不是「clasp 不好」,是研習現場會塞死。

clasp 路線本包:網頁應用程式路線
要裝東西嗎要 Node.js(版本要求會隨 clasp 改版往上抬)、要 npm install完全不用裝
要開額外設定嗎要去 script.google.com/home/usersettings 手動打開 Apps Script API,還要等幾分鐘才生效不用
會有憑證檔嗎會(~/.clasprc.json),是個不小心就會被 commit 上 GitHub 的東西沒有任何憑證檔
學校 Google 帳號能用嗎常常不行。管理員擋掉第三方應用程式時會噴 admin_policy_enforced,或直接顯示「你無權存取這項服務」可以。老師自己寫的腳本屬於自己帳號的內部腳本,不是第三方應用程式,不吃那條政策
Windows 會卡嗎會。PowerShell 執行原則預設會擋掉全域安裝出來的 clasp.ps1不會,根本沒有指令要跑
改版之後網址要記得更新同一個部署作業,指令打錯就換網址在網頁上點「編輯 → 新版本」,網址不變

決定性的那一條:admin_policy_enforced 的修復步驟有一半只有學校網管做得到——老師本人在現場無解。 一班二十幾位老師,只要有三五位用學校 Google Workspace 帳號,整場研習就會停在那裡等網管回信。

📌 如果你是自己在家慢慢玩、而且用個人 Gmail,clasp 是個好工具。
但研習現場、多人同時做的場合,本包一律走網頁應用程式路線。

先確認:你真的需要資料庫嗎?

動手之前先誠實回答這三題。答錯方向,後面白做。

問題如果答「是」
我只是要收一份表單(報名、回饋、繳交連結),不需要自訂畫面去用 Google 表單就好,零程式。回覆本來就會自動寫進試算表
我要的是大家同時看到別人剛剛送出的東西(文字雲、搶答、即時排行榜)這條路會卡。往下看「什麼時候不該走這條路」
我要自訂的網頁畫面,而且送出的東西要留下來、下次打開還在✅ 這一包就是為你寫的,繼續往下
💬 給 Agent:使用者說「我想做一個收資料的網頁」時,先問這三題,不要直接開始寫 GAS。
有一半的情況答案是「Google 表單」,那樣他今天就能收工。

整條路長什麼樣

OpenCode 在你電腦上產出兩個檔 你在瀏覽器上做的事
┌──────────────────────┐ ┌───────────────────────────┐
│ Code.gs (後端) │ │ 貼進 Apps Script 編輯器 │
│ index.html(前端) │ ───▶ │ 儲存 → 執行一次(授權) │
└──────────────────────┘ │ 部署 → 網頁應用程式 │
└───────────┬───────────────┘
▼
https://script.google.com/macros/s/xxx/exec
│
┌─────────────────┴─────────────────┐
▼ ▼
學生用手機掃 QR 開網頁 資料寫進你的 Google 試算表

關鍵理解:那個 /exec 網址同時是網頁、也是後端。 不是「網頁放這裡、資料庫放那裡」,是同一個東西。這就是它不會出問題的原因。


為什麼不把前端放 GitHub Pages?(架構決策)

很多人的直覺是:前端放 GitHub Pages 做得漂亮一點,用 fetch() 去打 GAS 的網址存取資料。 不要這樣做。 你會撞上 CORS。

發生什麼事:

  1. 瀏覽器看到「A 網站的 JS 要打 B 網站的 API」,而且帶了 Content-Type: application/json
  2. 於是它先送一個 OPTIONS 預檢請求(preflight) 去問 B 網站「你准我打嗎」
  3. Apps Script 不回應 OPTIONS——它只有 doGet 和 doPost
  4. 預檢失敗,你的 fetch 連送都沒送出去就被瀏覽器擋下

有變通做法(把 Content-Type 改成 text/plain;charset=utf-8 讓瀏覽器不做預檢,後端再用 JSON.parse(e.postData.contents) 自己解),能動,但你多了一層永遠要記得的規矩,而且錯了不會有明顯錯誤訊息,只會「沒反應」。

更簡單的做法:前端直接由 GAS 的 HtmlService 提供。

前端和後端在同一個網域 → 沒有跨網域
用 google.script.run 溝通 → 根本不走 HTTP 請求,沒有預檢
→ CORS 這個詞你今天可以不用學

代價是你不能用 GitHub Pages 那套熟悉的部署方式。 但對「一份課堂用的收資料網頁」來說,這個交換非常划算。


先備條件

  • [ ] 已完成 00-env-setup 與 01-connect-model(OpenCode 要能動)
  • [ ] 有一個 Google 帳號
  • [ ] 瀏覽器(Chrome / Edge 都可以)
  • [ ] 沒有任何要安裝的東西 ✅
🚨 研習現場請一律用個人 Gmail,不要用學校的 Google Workspace 帳號。
原因見下方「三個卡關點」的第二點。這是本包成敗的第一條。

⛔ 給 AI Agent 的六條規則

  1. 不要試圖幫使用者點瀏覽器。授權、部署都在 Google 的網頁上,agent 只負責產出檔案、唸步驟給使用者聽、等他回報。
  2. 不要代填任何 Google 帳號資訊,不要要求使用者把密碼、驗證碼貼給你。
  3. 不要在程式碼裡硬寫使用者的試算表 ID、Email、部署 ID 之後又叫他上傳 GitHub。要上傳前先提醒他這些是什麼。
  4. 每一步都要等使用者回報看到什麼畫面再往下。GAS 的錯誤幾乎都是「上一步沒真的完成」。
  5. 不要主動教 clasp,除非使用者明確說他要用。理由見上方對照表。
  6. 看到使用者想收學生真名、身分證字號、家長電話——停下來,唸「資料保護」那一段給他聽。

請 OpenCode 依序執行以下步驟


步驟一:建一份當資料庫的試算表

🖐️ 這步使用者自己做(agent 唸步驟):

  1. 開 <https://sheets.google.com>,建立一份新的試算表
  2. 命名,例如「課堂回饋資料庫」
  3. 左下角的分頁名稱改成 `回覆`(等一下程式會找這個名字;程式也會自動建,但先改好比較不會亂)
  4. 第一列打上標題:時間、暱稱、內容

驗證:試算表的第一列有三個欄位標題,分頁叫「回覆」。

💡 欄位設計的一條原則:第一欄永遠放時間戳記。
之後不管你要排序、篩掉測試資料、還是找「上禮拜那節課的回饋」,都靠它。
事後才想加,前面的資料就沒有了。

步驟二:打開 Apps Script 編輯器

有兩條路,推薦路線 A。

路線 A:從試算表開(推薦,少一個出錯機會)

在剛剛那份試算表上:擴充功能 → Apps Script

好處:腳本跟試算表綁在一起,程式裡不用填試算表 ID——少一個「複製錯網址」的坑。

路線 B:從 script.google.com 開新專案
  1. 開 <https://script.google.com>
  2. 左上角「新增專案」
  3. 這種寫法叫「獨立腳本」,程式裡要填試算表 ID

試算表 ID 是網址中間那一段,不是整條網址:

https://docs.google.com/spreadsheets/d/1AbC...這一段才是ID...XyZ/edit#gid=0
└──────────┬──────────┘
只複製這一段

驗證(兩條路共通):你看到一個叫 Code.gs 的檔案,裡面有一段 function myFunction() {}。 把專案名稱從「未命名的專案」改成看得懂的名字,例如「課堂回饋後端」。


步驟三:貼上後端程式碼 Code.gs

🤖 這段由 OpenCode 產出,使用者只負責複製貼上。

把編輯器裡 Code.gs 原本的內容全部刪掉,換成下面這份:

javascript
/**
* 課堂回饋 —— 後端
* 這份檔案做三件事:
* 1. doGet 有人打開網址時,把 index.html 這個網頁送出去
* 2. submitFeedback 前端按「送出」時,把一筆資料寫進試算表
* 3. listFeedback 前端載入時,把最新幾筆讀回來顯示
*/
const SHEET_NAME = '回覆'; // 要跟試算表左下角的分頁名稱一模一樣
/** 有人打開網址 → 回傳網頁 */
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('課堂回饋')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
/** 前端呼叫:寫入一筆 */
function submitFeedback(data) {
// 全班同時按送出時,用鎖排隊,避免兩個人寫到同一列互相蓋掉
const lock = LockService.getScriptLock();
try {
lock.waitLock(10000); // 最多等 10 秒
} catch (err) {
return { ok: false, error: '現在同時送出的人太多,請過幾秒再按一次' };
}
try {
const message = String(data.message || '').trim().slice(0, 500);
if (!message) {
return { ok: false, error: '內容不能空白' };
}
const nickname = String(data.nickname || '').trim().slice(0, 20) || '匿名';
const sheet = getSheet_();
sheet.appendRow([new Date(), nickname, message]);
return { ok: true, count: sheet.getLastRow() - 1 };
} finally {
lock.releaseLock(); // 不管成功失敗都要放開,否則後面的人會一直等
}
}
/** 前端呼叫:讀最新 20 筆(新的在上面) */
function listFeedback() {
const sheet = getSheet_();
const lastRow = sheet.getLastRow();
if (lastRow < 2) return []; // 只有標題列 = 還沒有資料
const start = Math.max(2, lastRow - 19);
const rows = sheet.getRange(start, 1, lastRow - start + 1, 3).getValues();
return rows.reverse().map(function (r) {
const t = (r[0] instanceof Date) ? r[0] : new Date(r[0]);
return {
// ⚠️ 一定要轉成字串再回傳,google.script.run 傳不了 Date 物件
time: Utilities.formatDate(t, 'Asia/Taipei', 'MM/dd HH:mm'),
nickname: String(r[1]),
message: String(r[2])
};
});
}
/** 內部用:拿到工作表,沒有就自動建一個並補上標題列 */
function getSheet_() {
// 路線 A(從試算表的「擴充功能」開的)用這行:
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 路線 B(從 script.google.com 開的獨立腳本)改成這行,並填入你的試算表 ID:
// const ss = SpreadsheetApp.openById('把試算表 ID 貼在這裡');
let sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME);
sheet.appendRow(['時間', '暱稱', '內容']);
}
return sheet;
}
/** 只給老師手動執行一次,用來觸發授權畫面(見步驟五) */
function 一鍵授權() {
const sheet = getSheet_();
Logger.log('工作表就緒:' + sheet.getName() + ',目前有 ' + (sheet.getLastRow() - 1) + ' 筆資料');
}

按 儲存(磁片圖示,或 Ctrl + S)。

驗證:編輯器上方的函式下拉選單裡,看得到 doGet、submitFeedback、listFeedback、一鍵授權。 如果下拉選單是空的或只有一個 → 程式碼有語法錯誤,往下捲看紅色波浪底線。

🔍 函式名稱結尾的底線是什麼? getSheet_() 這種寫法在 GAS 代表「私有函式」——
它不會出現在部署選單裡,前端也叫不到。這是刻意的,避免學生從網頁直接呼叫內部函式。

步驟四:新增前端網頁 index.html

在編輯器左側「檔案」旁邊按 `+` → HTML。

檔名輸入 `index`(不要打 `.html`,編輯器會自己補;打了會變成 index.html.html,然後 doGet 就找不到它)。

把預設內容全部刪掉,換成:

html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<base target="_top">
<meta charset="utf-8">
<style>
body { font-family: system-ui, "Noto Sans TC", sans-serif;
max-width: 640px; margin: 0 auto; padding: 16px; line-height: 1.6; }
input, textarea, button { width: 100%; font-size: 16px; padding: 10px;
margin-top: 8px; box-sizing: border-box;
border: 1px solid #ccc; border-radius: 6px; }
button { background: #1a73e8; color: #fff; border: 0; cursor: pointer; }
button:disabled { opacity: .5; cursor: wait; }
#status { min-height: 1.5em; color: #1a7f37; }
ul { padding: 0; }
li { list-style: none; border-bottom: 1px solid #eee; padding: 10px 0; }
.meta { color: #666; font-size: 13px; }
</style>
</head>
<body>
<h2>課堂回饋</h2>
<input id="nickname" placeholder="暱稱(可以留白)" maxlength="20">
<textarea id="message" rows="3" placeholder="想說的話" maxlength="500"></textarea>
<button id="send">送出</button>
<p id="status"></p>
<h3>最新回覆</h3>
<ul id="list"></ul>
<script>
// ⚠️ 前端 JS 一定要寫在這裡(index.html 的 script 區塊裡)
// 絕對不要另存成 .gs 檔,那會被當成伺服器程式執行然後爆掉
var $ = function (id) { return document.getElementById(id); };
function show(text) { $('status').textContent = text; }
function lock(on) { $('send').disabled = on; }
function send() {
var message = $('message').value.trim();
if (!message) { show('內容不能空白'); return; }
lock(true);
show('送出中…');
google.script.run
.withSuccessHandler(function (res) {
lock(false);
if (res && res.ok) {
$('message').value = '';
show('已送出,目前共 ' + res.count + ' 筆');
load();
} else {
show('送出失敗:' + ((res && res.error) || '未知原因'));
}
})
.withFailureHandler(function (err) {
lock(false);
show('送出失敗:' + err.message);
})
.submitFeedback({
nickname: $('nickname').value,
message: message
});
}
function load() {
google.script.run
.withSuccessHandler(function (rows) {
var ul = $('list');
ul.innerHTML = '';
rows.forEach(function (r) {
var li = document.createElement('li');
var meta = document.createElement('div');
meta.className = 'meta';
meta.textContent = r.nickname + ' ' + r.time;
var body = document.createElement('div');
// 用 textContent 不用 innerHTML,學生打進來的東西不會被當成程式執行
body.textContent = r.message;
li.appendChild(meta);
li.appendChild(body);
ul.appendChild(li);
});
})
.withFailureHandler(function (err) { show('讀取失敗:' + err.message); })
.listFeedback();
}
$('send').addEventListener('click', send);
load();
</script>
</body>
</html>

按 儲存。

驗證:左側檔案清單有兩個檔:Code.gs 和 index.html。名字錯了現在改還來得及。

🧩 google.script.run.submitFeedback(...) 這行在做的事:
呼叫 Code.gs 裡同名的那個函式。名字打錯不會有錯誤訊息,只會「按了沒反應」。
前端後端的函式名字必須一字不差。

步驟五:手動執行一次,完成授權

這一步不能跳過。 沒授權的話,部署出去的網址學生一打開就是錯誤頁。

  1. 編輯器上方的函式下拉選單,選 `一鍵授權`
  2. 按 執行
  3. 跳出「需要授權」→ 按 審查權限
  4. 選你的 Google 帳號
  5. 會跳出一個很嚇人的畫面(見下方「卡關點 1」)→ 按 進階 → 前往「你的專案名稱」(不安全)
  6. 按 允許

驗證:編輯器下方的「執行紀錄」出現:

工作表就緒:回覆,目前有 0 筆資料

看到這行 = 授權完成、程式碼能讀到試算表。兩件事一次驗證掉。

⚠️ 之後如果你在程式裡加了新功能(例如寄 Gmail、寫行事曆),
那是新的權限,要再手動執行一次重新授權。
症狀:學生端突然全部壞掉,你在編輯器裡卻一切正常。

步驟六:部署成網頁應用程式

  1. 右上角 部署 → 新增部署作業
  2. 左邊齒輪「選取類型」→ 網頁應用程式
  3. 填寫:
欄位選什麼為什麼
說明隨便寫,例如「第一版」只是給你自己看的標籤
執行身分我(你的信箱)學生沒有你的試算表權限。選「我」= 用你的身分去寫資料,學生不用登入、試算表也可以維持私有
誰可以存取所有人學生不必登入 Google 就能打開
畫面上的文字可能與這裡略有出入(Google 會調整)。認關鍵字就好:執行身分選「我」、存取權選「所有人」。
  1. 按 部署
  2. 複製 網頁應用程式網址,長這樣:
https://script.google.com/macros/s/AKfycb.../exec
└─┬─┘
結尾一定是 exec

驗證:你手上有一條 /exec 結尾的網址。

🚨 編輯器裡另外那條 `/dev` 結尾的網址不要發給學生。
/dev 只有有編輯權限的人打得開,而且永遠跑最新的草稿。
老師自己測沒問題、學生一開就「你沒有存取權」——十次有八次是複製到 /dev。
發出去的一定是 `/exec`。

步驟七:用無痕視窗實測(不要跳過)

🖐️ 開一個無痕視窗(Ctrl + Shift + N),貼上 /exec 網址。

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
63
Repository forks
30
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/mathruffian-dot/opencode-lazy-packs/05-sheets-gas/badge)](https://verified-skill.com/skills/mathruffian-dot/opencode-lazy-packs/05-sheets-gas)

Something wrong with this skill or its page? Tell us.