和 AI 一起寫程式:創意生活應用 WEEK 6 2026-10-12(一)09:10–12:00 E403
教它看照片、存進 Drive;讓 agent 替你下指令;本地模型也會上網搜
上週你的 LINE 待辦機器人已經在雲端跑了。今天三件事:①請 AI 幫它加「照片先存進 Google Drive」的本事(跟上週第二關一樣:看計畫、按同意、看對照、退回一次)②裝一個命令列工具 gws(Google Workspace CLI,Google 做的、在終端機裡讀寫你的雲端硬碟與試算表),然後讓 agent 替你下指令 ③在這台電腦跑一個本地模型(Gemma 4),而且讓它「不確定就自己上網搜」。
這一頁怎麼用:每一步有「指令或畫面 → 你應該看到 → 測一下 → 卡住了」四件,照順序做、做完打勾。勾選只存在這台瀏覽器裡;E403 的電腦有還原卡,重開機就全部沒了(今天裝的東西也一樣)。想帶走進度就截圖。指令全部是 PowerShell 寫法,按「複製」再貼。
- 第 0 段 先開背景下載——一坐下就貼;模型在另一個視窗下載,你接著做第 1 段
- 第 1 段 教它看照片、存進 Drive——09:45 還在改程式,就換成老師的對照答案往下走
- 第 2 段 裝 gws、讓 agent 替你下指令——第一次下課後開始;10:40 還在 Cloud Console,就停下來看老師示範
- 第 3 段 本地模型也會上網搜——第二次下課後開始
- 第 4 段 收尾與帶走——離開前一定要登出(含 gws)
- 卡關總表——出錯了先用關鍵字查這裡
鐘響 10:00、11:00 各休息一次。開場老師會請你舉手三樣:手機有收到 bot 回覆、試算表有多一列、日曆有多一個行程——這是上週的成果,也是今天的起點。
第 0 段 先開背景下載
這段做完你會有什麼:兩個下載在背景跑(本地模型 6.6 GB、Node.js+gws),你可以去做第 1 段。
開 PowerShell(開始功能表搜「PowerShell」,出現的 Windows PowerShell 就可以,不用系統管理員)。下面 A、B 兩塊整段複製、貼進去、按 Enter,貼完就去第 1 段;C 到第 2 段開頭再跑、D 到第 3 段開頭再跑。
-
Ollama(跑本地模型的程式)用 winget(Windows 內建的套件安裝指令)裝,不用管理員權限;裝完先把它的路徑加進這個視窗(第二行:PATH 是電腦找程式用的路徑清單,剛裝的程式要加進去才叫得到),第三行會另開一個 PowerShell 視窗下載
gemma4:e4b(Google 的 Gemma 4 模型,6.6 GB)——進度條在那個新視窗跑,你回這個視窗繼續貼 B。winget install --id Ollama.Ollama -e --accept-package-agreements --accept-source-agreements --silent $env:PATH += ";$env:LOCALAPPDATA\Programs\Ollama" Start-Process powershell -ArgumentList '-NoExit','-Command','ollama pull gemma4:e4b'
你應該看到winget 跑完說已成功安裝;接著跳出第二個 PowerShell 視窗,裡面出現pulling manifest(在讀模型的目錄清單),然後一條進度條慢慢往 6.6 GB 走。老師的電腦 36 MB/s 要 211 秒(約三分半);全班一起拉、教室網路會慢很多,先讓它跑。那個視窗不要關。測一下進度條有在動就過。不用等它跑完,直接貼 B。卡住了
- winget 說找不到或被擋→ 到官方網站 ollama.com 下載 Windows 安裝檔雙擊裝,裝完一樣跑第二、三行。
ollama不是指令、或第二個視窗一閃就關→ 第二行(加 PATH)沒貼到;重貼第二、三行。還是不行就關掉 PowerShell 重開一個(新開的視窗才會讀到剛裝好的程式)。- 進度條卡住不動→ 教室網路慢,先往下做;到第 3 段還沒好,兩人一組用已經拉完的那台。
-
gws 是用 Node.js(跑 JavaScript 的環境)寫的,所以先裝 Node.js,再用 npm(Node.js 附的套件安裝指令)裝 gws。第二行把 Node 的路徑加進這個視窗(跟 0-A 同一個道理)。
--allow-scripts那段是因為新版 npm 可能擋下套件的安裝腳本,這裡明確允許 gws 跑它的(它要靠那段下載執行檔)。winget install --id OpenJS.NodeJS.LTS -e --accept-package-agreements --accept-source-agreements --silent $env:PATH += ";$env:ProgramFiles\nodejs;$env:APPDATA\npm" npm install -g --allow-scripts=@googleworkspace/cli @googleworkspace/cli
你應該看到Node.js 裝的時候可能跳出「使用者帳戶控制」問你要不要允許,按「是」。npm 那行最後印出added … packages之類的一行。測一下沒有紅字就過。真正的驗證在 0-C(要新開視窗)。卡住了
- 「使用者帳戶控制」要管理員密碼→ 舉手,老師處理;先去做第 1 段。
npm不是指令→ 第二行(加 PATH)沒貼到,或 Node 裝到別的地方:關掉 PowerShell、重開一個(新視窗會讀到剛裝好的程式),只貼第三行。- npm 那行印 EACCES 或權限錯誤→ 重開 PowerShell 再跑一次;還是不行就舉手。
-
一定要新開一個 PowerShell 視窗(舊視窗的 PATH 還沒更新),貼這四行:
ollama --version node -v gws --version ollama list
你應該看到(2026-10-09 老師電腦實測的版本)ollama version is 0.40.1v24.20.0(v24 開頭就對,後面的數字會隨時間更新;電腦本來就裝過 Node 的會印別的版本,v18 以上都夠用)gws 0.22.5,下一行 This is not an officially supported Google product.(正常,不是錯誤)ollama list等 A 拉完會有一列gemma4:e4b,大小約 6.6 GB;還在拉就是空的,等一下再看。測一下前三行都印出版本號就過;第四行ollama list若說連不上(Warning: could not connect…)先不管,第 3 段開 Ollama app 之後再看。卡住了
ollama --version印 Warning: could not connect to a running Ollama instance、然後 client version is 0.40.1→ 裝好了,只是伺服器還沒起來;第 3 段開 Ollama app 就會起來。gws不是指令→ 你沒有新開視窗;或 0-B 的 npm 那行沒跑完。重開視窗再試;還是沒有就回 0-B 重跑第二行。node -v不是 v24→ 電腦本來就裝了別的版本;v18 以上都夠用,不用管。- 三個都說不是指令→ 你還在舊視窗。新裝的程式只有新開的視窗才找得到(舊視窗的 PATH 是它開啟那一刻抄的)。
-
原生的
gemma4:e4b不知道今天是哪一天,問它 2026 年的事會說「還沒發生」、連搜都不搜。下面這份 Modelfile(Ollama 的模型設定檔)用一段系統提示把規矩寫死:日期可能在訓練資料之後,不確定一律先搜、附來源。ollama create這一行就是用這份設定做出一個新名字gemma4-search(同一個模型+一段固定的開場指示),不會重新下載。下面這段整段一起貼:前半把 Modelfile 的內容寫成一個檔(就是上面說的那段規矩,放在你的使用者資料夾),後半用它做出
gemma4-search。$mf = @' FROM gemma4:e4b SYSTEM """你是研究助理。你有網路搜尋與讀網頁的工具。今天的日期可能在你的訓練資料之後,所以遇到「最近發生的事、某一年的得獎名單、版本號、價格、新聞」或任何你不確定的事實,一律先用搜尋工具查,查到再回答,並附上來源網址;不要以「還沒發生」為理由拒絕查詢。回答用繁體中文(台灣用語)。""" PARAMETER num_ctx 16384 '@ [IO.File]::WriteAllText("$env:USERPROFILE\Modelfile", $mf) ollama create gemma4-search -f "$env:USERPROFILE\Modelfile" ollama list你應該看到印幾行using existing layer、writing manifest,最後一行success(老師的電腦 1 秒;看到 success 才算);ollama list多一列gemma4-search:latest。測一下ollama list看得到gemma4-search就過。卡住了
- 貼完停在
>>不動→ 它在等你把整段貼完:中間那段以@'開頭、'@結尾,'@必須在一行的最前面。按一次 Enter;還是不動就按 Ctrl+C,整段重新複製再貼。 - 說找不到 gemma4:e4b→ 0-A 還沒拉完,
ollama list看一下。 - 顯示卡太小、模型跑不動→ 改拉小一號的
ollama pull gemma4:e2b,再把這段第二行的FROM gemma4:e4b改成FROM gemma4:e2b重貼。
只想看 Modelfile 本身(給好奇的人)
FROM gemma4:e4b SYSTEM """你是研究助理。你有網路搜尋與讀網頁的工具。今天的日期可能在你的訓練資料之後,所以遇到「最近發生的事、某一年的得獎名單、版本號、價格、新聞」或任何你不確定的事實,一律先用搜尋工具查,查到再回答,並附上來源網址;不要以「還沒發生」為理由拒絕查詢。回答用繁體中文(台灣用語)。""" PARAMETER num_ctx 16384
整份 install-commands.ps1(A~D 合在一起,想一次貼的用這個)
# W6 第 0 段:一進教室先貼的指令(PowerShell;2026-10-09 在 msi 實測版本) # A. 本地模型(另開一個視窗跑;6.6 GB,教室網路快的話 3–5 分鐘) winget install --id Ollama.Ollama -e --accept-package-agreements --accept-source-agreements --silent $env:PATH += ";$env:LOCALAPPDATA\Programs\Ollama" Start-Process powershell -ArgumentList '-NoExit','-Command','ollama pull gemma4:e4b' # B. 命令列工具(gws 需要 Node.js) winget install --id OpenJS.NodeJS.LTS -e --accept-package-agreements --accept-source-agreements --silent $env:PATH += ";$env:ProgramFiles\nodejs;$env:APPDATA\npm" npm install -g --allow-scripts=@googleworkspace/cli @googleworkspace/cli # C. 第 2 段開頭測一下(新開一個 PowerShell 視窗再跑,PATH 才會更新) ollama --version # 0.40.1 node -v # v24.x gws --version # gws 0.22.5 ollama list # 等 pull 完會看到 gemma4:e4b # D. 第 3 段開頭(A 拉完才跑得動):把「不確定就先搜」的規矩寫進模型設定,做出 gemma4-search(不會重新下載) $mf = @' FROM gemma4:e4b SYSTEM """你是研究助理。你有網路搜尋與讀網頁的工具。今天的日期可能在你的訓練資料之後,所以遇到「最近發生的事、某一年的得獎名單、版本號、價格、新聞」或任何你不確定的事實,一律先用搜尋工具查,查到再回答,並附上來源網址;不要以「還沒發生」為理由拒絕查詢。回答用繁體中文(台灣用語)。""" PARAMETER num_ctx 16384 '@ [IO.File]::WriteAllText("$env:USERPROFILE\Modelfile", $mf) ollama create gemma4-search -f "$env:USERPROFILE\Modelfile" ollama list - 貼完停在
第 1 段 教它看照片、存進 Drive
這段做完你會有什麼:手機傳一張照片給 bot,照片先存進你 Google Drive 的「LINE待辦照片」資料夾,試算表多一欄「照片連結」點得開。
這就是上週的第二關,多一個「先存進 Drive」。你的工作跟 W3、W5 一樣:看計畫、按同意、看改前改後對照、退回它一次。先選路線:
| 你手上有 | 走 | 怎麼改程式 |
|---|---|---|
| 自己的筆電,Antigravity 或 Codex 登得進 | 路線 A | 把程式存成檔案放進一個資料夾,agent 在資料夾裡改,它自己有 diff(改前改後對照)畫面;改完你把檔案內容貼回 Apps Script 編輯器。 |
| 教室電腦,只有瀏覽器 | 路線 B | 把程式整份貼給 ChatGPT/Gemini 網頁版請它改;沒有 diff 畫面,所以動手前先把舊程式存進一份 Google 文件,改完請 AI 列對照、自己對。 |
時間盒:09:45 還在改程式的,直接開這段最下面「老師的對照答案」,整份換掉你的 Code.gs(有 oauthScopes 清單的連 appsscript.json 一起換),從 1-7 貼回編輯器繼續——先讓照片進 Drive,改前改後對照回家再看。
-
開你上週那份試算表 → 擴充功能 → Apps Script。手機傳一句「明天下午三點要開會」給 bot。
你應該看到手機收到「✅ 記好了(試算表第 N 列)」,試算表多一列。測一下有回覆、有新列,就過。卡住了
- 手機完全沒反應→ 上週的卡:部署的「誰可以存取」要是「所有人」、LINE 後台的 Webhook 要開著。兩分鐘內修不好先舉手,別卡在這裡。
- bot 回「⚠️ 出錯了:…」→ 把那段整段貼給 AI 問原因;多半是金鑰或通行碼。
-
路線 B:在編輯器裡 Ctrl+A 全選 Code.gs、複製,開一份新的 Google 文件(取名「Code.gs 改前 W6」)貼進去。路線 A:在電腦建一個空資料夾(例如
line-bot-w6),把 Code.gs 的內容存成Code.gs(記事本貼上 → 另存新檔 → 存檔類型選「所有檔案」、檔名打Code.gs、編碼 UTF-8,才不會變成 Code.gs.txt);再把 appsscript.json 也存一份(它是這個專案的設定檔,裡面的 oauthScopes 就是程式要跟 Google 要的權限清單。怎麼看到它:編輯器左邊齒輪「專案設定」→ 勾「在編輯器中顯示 appsscript.json 資訊清單檔案」,檔案清單就會多出來)。然後用 Antigravity/Codex 開這個資料夾。你應該看到路線 B:Google 文件裡有整份程式。路線 A:資料夾裡有Code.gs(和appsscript.json),agent 左邊檔案列表看得到。測一下舊版在編輯器以外的地方有一份完整的,就過。卡住了
- 想在同一個 Apps Script 專案另開一個 .gs 檔存舊版→ 不行:兩份宣告同名常數,整個專案會載入失敗、bot 直接掛掉。存在 Google 文件或電腦上。
- 找不到 appsscript.json→ 專案設定那個勾沒勾;勾了還沒有就是你的專案用預設清單,1-6 會講怎麼辦。
-
這段=上週第二關那段,多加了一句「先存進 Drive」。路線 A 貼進 agent 的對話框;路線 B 貼進 ChatGPT/Gemini,並把整份 Code.gs 接在後面一起貼。
這是一支 LINE 待辦機器人的 Apps Script(Code.gs)。請幫我加上:使用者傳照片時,把照片下載下來交給 Gemini 判讀,整理成跟文字訊息一樣的欄位,再記進試算表。使用者傳照片時,先把照片存進我 Google Drive 的『LINE待辦照片』資料夾(沒有就建一個),把檔案的連結寫進試算表新的一欄『照片連結』,再交給 Gemini 判讀。也請加上「對方正在輸入…」的動畫,並且確定試算表真的寫進去了,才回覆「記好了」。金鑰繼續從指令碼屬性讀,不要寫進程式。改之前先列出實作計畫(會改哪些地方、分幾步、哪裡你不確定),等我同意再動手;做完給我改了什麼的摘要(改前/改後逐段對照),以及我可以怎麼測。
你應該看到它先列計畫、停下來問你,沒有動手。老師 10/09 用 Codex 跑這段,第一輪回來的是這樣(節錄;只是一個例子,你的不會逐字相同,路線 B 的 ChatGPT/Gemini 列法也會不一樣):① 實作計畫 預計分 5 步修改: 1. 擴充試算表欄位——將 HEADERS 加入新欄「照片連結」,建議放在「來源」之後、「日曆」之前…… 2. 照片先存入 Google Drive——新增尋找或建立「LINE待辦照片」資料夾的函式……不會把任何金鑰寫進程式。 3. 調整照片處理流程——改成:LINE 下載照片 → 儲存到 Drive → 取得連結 → 交給 Gemini 判讀 → 寫入試算表。 4. 更新 Apps Script 權限——在 appsscript.json 的 oauthScopes 增加 Google Drive 權限。下次執行或部署時,Google 可能要求重新授權。 5. 驗證…… 目前有 3 個需要確認的行為…… 這一輪尚未修改 Code.gs 或 appsscript.json。請回覆「同意」即可按上述預設開始。測一下看三件:有先列計畫、有說哪裡不確定、有說還沒動手(在等你同意)。三件都有=過。卡住了
- 它沒停、直接把程式改完丟給你→ 提示詞少貼了「等我同意再動手」那句,或它沒照做。回它:「請先不要動,先列計畫等我同意」;路線 B 就重開一段對話重貼。
- 它說程式已經會看照片了(上週第二關做過的人)→ 正常,請它只做「先存進 Drive、寫照片連結」那部分,計畫一樣要先列。
- 它問你要金鑰→ 不給。回它「金鑰在指令碼屬性,程式用 PROPS.getProperty 讀,你不需要」。
-
對著它的計畫看四件,都有才回「同意」(它問的問題也一併回答,沒意見就說「照你的預設」):
- 有沒有說金鑰繼續從指令碼屬性讀、不寫進程式?
- 流程是不是先存 Drive → 拿連結 → 再給 Gemini → 再寫表?
- 有沒有提到要加 Drive 權限、可能要重新授權?
- 有沒有老實說哪裡不確定(同名資料夾怎麼辦、連結要不要公開、不是待辦的照片要不要記)?
你應該看到回「同意」之後它才開始改,改完給你摘要與改前/改後對照。測一下四件都勾得到就回同意;少一件就先問它那一件。卡住了
- 計畫說要把連結設成「知道連結的任何人都能看」→ 不用。照片是你的,留私人就好;回它「連結保留私人權限」。
- 計畫要改一大堆跟照片無關的地方→ 越界。回它「只做照片存 Drive 這件事,其他不要動」。
-
路線 A 看 agent 的 diff 畫面(紅色減號=刪掉、綠色加號=加上);路線 B 看它列的對照,必要時拿 Google 文件裡的舊版比。上週四件,今天第五件是新的:
# 找什麼 在哪裡看、怎樣才對 1 先寫表、再回覆 saveAndReply裡appendTodoRow(...)要在replyLine(...)前面。agent 為了快常先回覆再寫表,那就是「說記好了其實沒記」。2 金鑰不在程式裡 整份搜一遍沒有長串英數字; GEMINI_API_KEY只能是PROPS.getProperty("GEMINI_API_KEY")。3 照片從 api-data.line.me 下載 downloadLineImage用的網域是https://api-data.line.me,不是api.line.me。4 存取值還是 ANYONE_ANONYMOUS appsscript.json 的 webapp.access;寫成ANYONELINE 就進不來。5 多了 Drive 權限(5a);先存再判讀(5b) 5a:appsscript.json 的 oauthScopes清單多一行https://www.googleapis.com/auth/drive。你的 appsscript.json 根本沒有 oauthScopes 這個清單?(上週在瀏覽器貼程式的多半是這樣)那就不用加,Apps Script 會自己從程式判斷、重新授權時多問你 Drive。5b:程式這邊要看到一個新函式(對照答案叫savePhotoToDrive),而且在askGeminiAboutImage之前被呼叫——先存、再判讀。你應該看到五件都對;照片那段變成「下載 → 存 Drive 拿連結 → Gemini 判讀 → 寫表(多一欄)→ 回覆」。測一下五件(第五件有 a、b 兩點)各指得出在哪一行,就過。指不出來就問它「第 N 件在哪裡?」卡住了
- 它把
api-data.line.me改成api.line.me→ 退回:「照片下載網域要用 api-data.line.me」。 - 它改成先回覆再寫表→ 退回:「要先 appendTodoRow 再 replyLine」。
- 它只給整份新程式、沒有對照→ 回它「請列改前/改後逐段對照,只列有改的段落」。
- 它把
-
就算它改得很好,今天還是練一次退回——「把 AI 的東西退回去改」是這門課要帶走的能力。理由可以很小(請它把英文註解改成中文也算)。用這個格式:
我不要___,因為___。請只改這一點,其他不要動;改完再列一次改前/改後對照。
你應該看到它只動你說的那一點,重新列對照。測一下對照裡只有你要的那個改動,就過。多改了別的就再退一次。卡住了
- 想不到理由→「註解請改成中文,因為下週同學要看得懂」「新函式請加一行註解說明它做什麼」都行。
-
Code.gs:在 Apps Script 編輯器 Ctrl+A 全選、貼上新版、Ctrl+S 儲存。appsscript.json:只有你的檔案裡有 oauthScopes 清單時才要動——照 AI 給的加一行 drive(或直接拿下面「老師的對照答案」那份整個換掉)。
你應該看到儲存後上方沒有紅色錯誤;函式選單(「執行」左邊那個下拉)能看到testSetup。測一下函式選單裡多了新函式(例如savePhotoToDrive),就過。卡住了
- 儲存就報「已宣告」之類的語法錯誤→ 你的專案裡還有一份舊的 .gs(同名常數撞到),刪掉那份;或貼的時候沒全選、新舊疊在一起,重貼。
- appsscript.json 看不到→ 1-2 說的那個勾;沒有清單就不用改。
-
函式選單選
testSetup→ 執行。因為程式多用了 Drive,Google 通常會再跳一次授權畫面:「Google 尚未驗證這個應用程式」→ 進階 → 前往…(不安全)→ 權限清單每一項都勾,多出來的那一項就是 Google 雲端硬碟 → 允許。你應該看到下方「執行紀錄」四行 ✅:LINE token(bot 名稱)、Google 日曆、Gemini、試算表(寫進第 N 列),最後一句「👉 打開試算表,親眼看到多了一列才算成功」。測一下打開試算表真的多一列(來源寫 testSetup)才算過。這裡還沒測到 Drive,1-10 才會。卡住了
- 沒跳授權畫面→ 不一定有錯(這個帳號可能早就給過 Drive 權限)。先做 1-10 傳照片:Drive 裡有檔就沒事;回覆「⚠️ 出錯了:You do not have permission…」才回頭看 1-5 第 5 件。
- 權限畫面少勾了一項→ 不會報錯,但資料不落地。到 Google 帳戶的「第三方應用程式與服務」把這個專案移除,重跑 testSetup,這次全勾。
- 執行紀錄說 You do not have permission to call DriveApp…→ appsscript.json 的 oauthScopes 清單沒加 drive 那行;加了存檔再跑。
- 執行紀錄說成功、表裡沒有那列→ 同第二條:授權沒勾滿。
-
存檔不等於上線:LINE 敲的
/exec網址跑的是「部署那一刻」的版本。右上「部署」→「管理部署作業」→ 鉛筆(編輯)→ 版本選「新版本」→ 部署。你應該看到版本號加一;/exec網址沒變(不用回 LINE 後台改);「誰可以存取」還是「所有人」。測一下回「管理部署作業」看一眼:誰可以存取=所有人,就過。卡住了
- 找不到「管理部署作業」→ 「部署」按鈕旁邊的小三角。
- 不小心按成「新增部署作業」→ 會產生一個新的 /exec 網址,LINE 後台還指著舊的;刪掉新的那個,回去用「管理部署作業」。
-
傳一張有日期的活動海報或便條紙(拍一張課表、海報都行;別傳有別人個資的)。
你應該看到手機回覆長這樣(對照答案的版本):✅ 記好了(試算表第 N 列) 📌 事項 🏷️ 分類 📅 日期 ⏰ 時間 🗓️ 日曆提醒建立中…(或:ℹ️ 沒有明確的日期和時間,這筆先不排日曆) 📷 照片已存進 Drive 的「LINE待辦照片」資料夾
試算表那一列:「來源」=照片,標題叫「照片連結」那一欄(對照答案放在第 8 欄 H)是一條https://drive.google.com/file/d/…的網址。測一下①點那條連結,開出來是你那張照片 ②開 Google 雲端硬碟,多一個「LINE待辦照片」資料夾,裡面有line-20261012-時分秒.jpg。兩個都有才過。卡住了
- bot 說這不是待辦、試算表沒新列,但 Drive 裡有照片→ 正常:程式是先存 Drive 再判讀,不是待辦就不記表。換一張有日期的海報再傳。
- 試算表有那列、但「照片連結」那欄空白,Drive 卻有檔→ AI 漏了把連結傳進寫表的函式(
saveAndReply→appendTodoRow那條路)。請它對照對照答案補上。 - 第 8 欄有連結、標題列卻沒有「照片連結」四個字→ 正常:程式只在空白表才寫標題列,你的表上週就有了。自己在 H1 打上「照片連結」就好。
- 回覆「⚠️ 出錯了:You do not have permission…」→ 1-8 的授權沒勾滿或 oauthScopes 沒加 drive;修好要再做一次 1-9。
- 手機沒變化、還是舊行為→ 忘了 1-9「新版本」。
- 傳照片完全沒回→ 看 Apps Script 左邊「執行紀錄」最新那筆的紅字,整段貼給 AI 問。
老師的對照答案(做完再看):改前/改後 diff
左邊 - 是上週版、右邊 + 是存 Drive 版。總共只改了這幾段;你的版本長得不一樣沒關係,五件對就好。
--- apps-script/Code.gs(W5 原版)
+++ Code.drive-reference.gs(W6 存 Drive 版)
@@ -22,7 +22,7 @@
const GEMINI_MODEL = PROPS.getProperty("GEMINI_MODEL") || "gemini-3.5-flash-lite";
const TZ = "Asia/Taipei";
const CATEGORIES = ["繳費", "會議", "課業", "生活", "其他"];
-const HEADERS = ["建立時間", "事項", "分類", "日期", "時間", "來源", "日曆"];
+const HEADERS = ["建立時間", "事項", "分類", "日期", "時間", "來源", "日曆", "照片連結"];
const GREETING = /^(hi|hello|hey|你好|嗨|哈囉|安安|早安|午安|晚安|謝謝|感謝|ok|好的|掰|bye|test|測試)$/i;
const HELP_TEXT = "你好!我是你的 AI 待辦小秘書 🤖\n\n直接傳給我:\n•「明天下午三點要開會」\n•「週五前記得繳房租」\n• 或拍一張活動海報、便條紙\n\n我會記進試算表;有日期和時間的,再幫你排進 Google 日曆。";
@@ -58,7 +58,8 @@
if (msg.type === "image") {
showLoading(event.source);
const image = downloadLineImage(msg.id);
- return saveAndReply(event.replyToken, askGeminiAboutImage(image), "照片");
+ const photoUrl = savePhotoToDrive(image); // 先把照片存進 Drive,拿到連結
+ return saveAndReply(event.replyToken, askGeminiAboutImage(image), "照片", photoUrl);
}
replyLine(event.replyToken, "目前我看得懂文字和照片,其他類型還不行喔。");
} catch (err) {
@@ -74,11 +75,11 @@
// ===== 3. 記錄、回覆、排日曆 =====
-function saveAndReply(replyToken, todo, source) {
+function saveAndReply(replyToken, todo, source, photoUrl) {
if (!todo.isTodo) return replyLine(replyToken, todo.chatReply || HELP_TEXT);
// ① 先寫進試算表:確定真的寫進去了,才能跟使用者說「記好了」
- const row = appendTodoRow(todo, source);
+ const row = appendTodoRow(todo, source, photoUrl || "");
const wantsCalendar = todo.date !== "未指定" && todo.time !== "未指定";
// ② 回覆手機:只講已經做完的事(回覆碼要在收到訊息後 1 分鐘內用掉)
@@ -86,6 +87,7 @@
reply += wantsCalendar
? "\n🗓️ 日曆提醒建立中,結果會寫在試算表的「日曆」欄"
: "\nℹ️ 沒有明確的日期和時間,這筆先不排日曆";
+ if (photoUrl) reply += "\n📷 照片已存進 Drive 的「LINE待辦照片」資料夾";
replyLine(replyToken, reply);
// ③ 最後才排日曆;成功或失敗都寫回試算表,不讓人誤以為做好了
@@ -103,18 +105,28 @@
return sheet;
}
-function appendTodoRow(todo, source) {
+function appendTodoRow(todo, source, photoUrl) {
const sheet = getSheet();
const now = Utilities.formatDate(new Date(), TZ, "yyyy-MM-dd HH:mm:ss");
// 全班同時傳訊息時,先排隊再寫,避免兩筆搶到同一列
const lock = LockService.getScriptLock();
lock.waitLock(10000);
try {
- sheet.appendRow([now, todo.summary, todo.category, todo.date, todo.time, source, ""]);
+ sheet.appendRow([now, todo.summary, todo.category, todo.date, todo.time, source, "", photoUrl || ""]);
return sheet.getLastRow();
} finally {
lock.releaseLock();
}
+}
+
+// 照片先存進 Google Drive 的「LINE待辦照片」資料夾(沒有就建一個),回傳檔案連結
+function savePhotoToDrive(blob) {
+ const folderName = "LINE待辦照片";
+ const found = DriveApp.getFoldersByName(folderName);
+ const folder = found.hasNext() ? found.next() : DriveApp.createFolder(folderName);
+ const stamp = Utilities.formatDate(new Date(), TZ, "yyyyMMdd-HHmmss");
+ const file = folder.createFile(blob.setName("line-" + stamp + ".jpg"));
+ return file.getUrl();
}
function createCalendarEvent(todo) {完整的 Code.gs(存 Drive 版,想整份換掉的用這個)
/**
* LINE AI 待辦小秘書(Level 2:文字+照片)
*
* 這份是正本:老師的示範 bot、學生 Level 2 的對照答案,都以這份為準。
* 一則訊息的旅程:
* LINE 送來訊息 → 請 Gemini 把口語整理成固定欄位 → 程式驗算欄位
* → 寫進試算表 → 回覆 LINE(只講已經做完的事)→ 排 Google 日曆,結果寫回試算表
*
* 金鑰不寫在程式裡,一律放「專案設定 → 指令碼屬性」:
* GEMINI_API_KEY 必填,到 Google AI Studio 申請
* LINE_CHANNEL_ACCESS_TOKEN 必填,LINE Developers 後台發的長效 token
* SHEET_ID 選填,程式不是從試算表「擴充功能 → Apps Script」開的才要填
* GEMINI_MODEL 選填,模型停用或想換模型時改這裡,不用動程式
*/
// ===== 1. 設定 =====
const PROPS = PropertiesService.getScriptProperties();
const GEMINI_API_KEY = PROPS.getProperty("GEMINI_API_KEY") || "";
const LINE_ACCESS_TOKEN = PROPS.getProperty("LINE_CHANNEL_ACCESS_TOKEN") || "";
const SHEET_ID = PROPS.getProperty("SHEET_ID") || "";
// 模型名稱會隨 Google 改版而停用:上課前到 https://ai.google.dev/gemini-api/docs/models 確認還在
const GEMINI_MODEL = PROPS.getProperty("GEMINI_MODEL") || "gemini-3.5-flash-lite";
const TZ = "Asia/Taipei";
const CATEGORIES = ["繳費", "會議", "課業", "生活", "其他"];
const HEADERS = ["建立時間", "事項", "分類", "日期", "時間", "來源", "日曆", "照片連結"];
const GREETING = /^(hi|hello|hey|你好|嗨|哈囉|安安|早安|午安|晚安|謝謝|感謝|ok|好的|掰|bye|test|測試)$/i;
const HELP_TEXT = "你好!我是你的 AI 待辦小秘書 🤖\n\n直接傳給我:\n•「明天下午三點要開會」\n•「週五前記得繳房租」\n• 或拍一張活動海報、便條紙\n\n我會記進試算表;有日期和時間的,再幫你排進 Google 日曆。";
// ===== 2. LINE 送訊息進來的入口 =====
function doPost(e) {
try {
const body = JSON.parse((e && e.postData && e.postData.contents) || "{}");
// 在 LINE 後台按 Verify 時,送來的 events 是空的,這裡就什麼都不做
(body.events || []).forEach(handleEvent);
} catch (err) {
console.error("讀不懂 LINE 送來的內容:", err);
}
return ContentService.createTextOutput("OK");
}
// 用瀏覽器打開 /exec 網址會看到這行字,可以確認部署成功
function doGet() {
return ContentService.createTextOutput("LINE AI 待辦小秘書運作中(Level 2)");
}
// 一次處理一則訊息;其中一則出錯,不影響同一批的其他訊息
function handleEvent(event) {
if (event.type !== "message" || !event.replyToken) return;
try {
const msg = event.message;
if (msg.type === "text") {
const text = msg.text.trim();
if (GREETING.test(text)) return replyLine(event.replyToken, HELP_TEXT); // 打招呼不必問 AI,直接回
showLoading(event.source);
return saveAndReply(event.replyToken, askGeminiAboutText(text), "文字");
}
if (msg.type === "image") {
showLoading(event.source);
const image = downloadLineImage(msg.id);
const photoUrl = savePhotoToDrive(image); // 先把照片存進 Drive,拿到連結
return saveAndReply(event.replyToken, askGeminiAboutImage(image), "照片", photoUrl);
}
replyLine(event.replyToken, "目前我看得懂文字和照片,其他類型還不行喔。");
} catch (err) {
console.error("處理訊息失敗:", err);
// 課堂版刻意把錯誤原因回給手機:學生可以整段貼給 AI,問它怎麼修
try {
replyLine(event.replyToken, "⚠️ 出錯了:" + err.message);
} catch (err2) {
console.error("連錯誤訊息都回不出去(多半是 LINE token 沒設定或貼錯):", err2);
}
}
}
// ===== 3. 記錄、回覆、排日曆 =====
function saveAndReply(replyToken, todo, source, photoUrl) {
if (!todo.isTodo) return replyLine(replyToken, todo.chatReply || HELP_TEXT);
// ① 先寫進試算表:確定真的寫進去了,才能跟使用者說「記好了」
const row = appendTodoRow(todo, source, photoUrl || "");
const wantsCalendar = todo.date !== "未指定" && todo.time !== "未指定";
// ② 回覆手機:只講已經做完的事(回覆碼要在收到訊息後 1 分鐘內用掉)
let reply = `✅ 記好了(試算表第 ${row} 列)\n\n📌 ${todo.summary}\n🏷️ ${todo.category}\n📅 ${todo.date} ⏰ ${todo.time}`;
reply += wantsCalendar
? "\n🗓️ 日曆提醒建立中,結果會寫在試算表的「日曆」欄"
: "\nℹ️ 沒有明確的日期和時間,這筆先不排日曆";
if (photoUrl) reply += "\n📷 照片已存進 Drive 的「LINE待辦照片」資料夾";
replyLine(replyToken, reply);
// ③ 最後才排日曆;成功或失敗都寫回試算表,不讓人誤以為做好了
if (wantsCalendar) getSheet().getRange(row, 7).setValue(createCalendarEvent(todo));
}
function getSheet() {
const ss = SHEET_ID ? SpreadsheetApp.openById(SHEET_ID) : SpreadsheetApp.getActiveSpreadsheet();
if (!ss) throw new Error("找不到試算表:請從試算表的「擴充功能 → Apps Script」開程式,或在指令碼屬性填 SHEET_ID");
const sheet = ss.getSheets()[0];
if (sheet.getLastRow() === 0) {
sheet.appendRow(HEADERS);
sheet.getRange(1, 1, 1, HEADERS.length).setFontWeight("bold").setBackground("#0E7C7B").setFontColor("#FFFFFF");
}
return sheet;
}
function appendTodoRow(todo, source, photoUrl) {
const sheet = getSheet();
const now = Utilities.formatDate(new Date(), TZ, "yyyy-MM-dd HH:mm:ss");
// 全班同時傳訊息時,先排隊再寫,避免兩筆搶到同一列
const lock = LockService.getScriptLock();
lock.waitLock(10000);
try {
sheet.appendRow([now, todo.summary, todo.category, todo.date, todo.time, source, "", photoUrl || ""]);
return sheet.getLastRow();
} finally {
lock.releaseLock();
}
}
// 照片先存進 Google Drive 的「LINE待辦照片」資料夾(沒有就建一個),回傳檔案連結
function savePhotoToDrive(blob) {
const folderName = "LINE待辦照片";
const found = DriveApp.getFoldersByName(folderName);
const folder = found.hasNext() ? found.next() : DriveApp.createFolder(folderName);
const stamp = Utilities.formatDate(new Date(), TZ, "yyyyMMdd-HHmmss");
const file = folder.createFile(blob.setName("line-" + stamp + ".jpg"));
return file.getUrl();
}
function createCalendarEvent(todo) {
try {
const start = new Date(`${todo.date}T${todo.time}:00+08:00`);
const end = new Date(start.getTime() + 30 * 60 * 1000);
const event = CalendarApp.getDefaultCalendar().createEvent(`【${todo.category}】${todo.summary}`, start, end, {
description: "由 LINE AI 待辦小秘書建立"
});
event.addPopupReminder(30);
return "已建立";
} catch (err) {
console.error("建立日曆失敗:", err);
return "失敗:" + err.message;
}
}
// ===== 4. 請 Gemini 把口語整理成固定欄位 =====
const JSON_SHAPE = `只輸出一個 JSON 物件,欄位固定如下:
{"isTodo": true 或 false,
"summary": "事項,簡短一句",
"category": "繳費、會議、課業、生活、其他 五選一",
"date": "YYYY-MM-DD,沒提到就填 未指定",
"time": "HH:mm(24 小時制),沒提到就填 未指定",
"chatReply": "不是待辦時,給使用者的一句簡短回覆;是待辦就填空字串"}`;
// 附上星期幾,AI 才算得準「下週五」是哪一天
function nowText() {
return Utilities.formatDate(new Date(), TZ, "yyyy-MM-dd EEEE HH:mm");
}
function askGeminiAboutText(text) {
const prompt = `你是待辦事項助理。現在是台北時間 ${nowText()}。
判斷使用者這句話是不是待辦、提醒或行程。
是的話,把「明天」「下週五」這類說法換算成實際日期;不是的話,isTodo 填 false。
${JSON_SHAPE}
使用者說:${text}`;
return checkTodo(callGemini([{ text: prompt }]));
}
function askGeminiAboutImage(blob) {
const prompt = `你是待辦事項助理。現在是台北時間 ${nowText()}。
這是一張照片,可能是活動海報、便條紙或繳費單。找出上面最重要的一件待辦或活動。
照片上沒寫的欄位就填 未指定,不要自己猜。
${JSON_SHAPE}`;
return checkTodo(callGemini([
{ text: prompt },
{ inlineData: { mimeType: blob.getContentType() || "image/jpeg", data: Utilities.base64Encode(blob.getBytes()) } }
]));
}
function callGemini(parts) {
if (!GEMINI_API_KEY) throw new Error("還沒設定 GEMINI_API_KEY(專案設定 → 指令碼屬性)");
const res = UrlFetchApp.fetch(`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`, {
method: "post",
contentType: "application/json",
headers: { "x-goog-api-key": GEMINI_API_KEY }, // 金鑰放標頭、不放網址,才不會留在各種紀錄裡
payload: JSON.stringify({
contents: [{ role: "user", parts: parts }],
generationConfig: { responseMimeType: "application/json" }
}),
muteHttpExceptions: true
});
const code = res.getResponseCode();
if (code !== 200) throw new Error(`Gemini 回應 ${code}:${res.getContentText().slice(0, 150)}`);
const data = JSON.parse(res.getContentText());
const cand = (data.candidates || [])[0];
const text = cand && cand.content && (cand.content.parts || []).map(p => p.text || "").join("");
if (!text) throw new Error(`Gemini 沒有給內容(原因:${cand ? cand.finishReason : "沒有任何答案"})`);
return JSON.parse(text.replace(/^```(json)?\s*|\s*```$/g, ""));
}
// ===== 5. 驗算:AI 給的欄位不直接照單全收 =====
function checkTodo(t) {
return {
isTodo: t.isTodo === true,
summary: String(t.summary || "").trim() || "(沒抓到事項)",
category: CATEGORIES.includes(t.category) ? t.category : "其他",
date: isRealDate(t.date) ? t.date : "未指定",
time: /^([01]\d|2[0-3]):[0-5]\d$/.test(t.time) ? t.time : "未指定",
chatReply: String(t.chatReply || "")
};
}
// 「2026-02-30」格式對、日子卻不存在:轉成日期再轉回來,對得上才算數
function isRealDate(s) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
const d = new Date(`${s}T12:00:00+08:00`);
return !isNaN(d.getTime()) && Utilities.formatDate(d, TZ, "yyyy-MM-dd") === s;
}
// ===== 6. LINE 相關 =====
function lineApi(path, payload, host) {
if (!LINE_ACCESS_TOKEN) throw new Error("還沒設定 LINE_CHANNEL_ACCESS_TOKEN(專案設定 → 指令碼屬性)");
const options = {
method: payload ? "post" : "get",
headers: { Authorization: "Bearer " + LINE_ACCESS_TOKEN },
muteHttpExceptions: true
};
if (payload) {
options.contentType = "application/json";
options.payload = JSON.stringify(payload);
}
return UrlFetchApp.fetch((host || "https://api.line.me") + path, options);
}
function replyLine(replyToken, text) {
const res = lineApi("/v2/bot/message/reply", {
replyToken: replyToken,
messages: [{ type: "text", text: text.slice(0, 5000) }]
});
if (res.getResponseCode() !== 200) console.error("LINE 回覆失敗:", res.getResponseCode(), res.getContentText());
}
// 「對方正在輸入…」動畫:只有一對一聊天看得到,沒開成也不影響功能
function showLoading(source) {
if (!source || source.type !== "user") return;
try {
lineApi("/v2/bot/chat/loading/start", { chatId: source.userId, loadingSeconds: 20 });
} catch (err) {
console.warn("輸入中動畫沒開成:", err);
}
}
// 照片要到另一個網域(api-data)下載,而且 LINE 不會一直保存,收到就要抓
function downloadLineImage(messageId) {
const res = lineApi(`/v2/bot/message/${messageId}/content`, null, "https://api-data.line.me");
if (res.getResponseCode() !== 200) throw new Error(`照片下載失敗(LINE 回應 ${res.getResponseCode()})`);
const blob = res.getBlob();
// 送給 Gemini 的請求有大小上限,照片轉成文字(Base64)還會再大三分之一,太大的先請使用者縮小
if (blob.getBytes().length > 10 * 1024 * 1024) throw new Error("照片太大(超過 10 MB),請截圖或壓縮後再傳");
return blob;
}
// ===== 7. 自我檢查:在編輯器上方選 testSetup,按「執行」 =====
function testSetup() {
const results = [];
const check = (name, fn) => {
try {
results.push(`✅ ${name}:${fn()}`);
} catch (err) {
results.push(`❌ ${name}:${err.message}`);
}
};
check("LINE token", () => {
const res = lineApi("/v2/bot/info");
if (res.getResponseCode() !== 200) throw new Error(`LINE 回應 ${res.getResponseCode()},token 可能貼錯或已經重發`);
return "bot 名稱「" + JSON.parse(res.getContentText()).displayName + "」";
});
check("Google 日曆", () => "可以使用「" + CalendarApp.getDefaultCalendar().getName() + "」");
let todo = null;
check("Gemini", () => {
todo = askGeminiAboutText("明天早上十點要交房租,記得轉帳給房東");
return JSON.stringify(todo);
});
check("試算表", () => {
if (!todo) throw new Error("Gemini 那關沒過,先不寫");
return `寫進第 ${appendTodoRow(todo, "testSetup")} 列`;
});
console.log(results.join("\n"));
console.log("👉 打開試算表,親眼看到多了一列才算成功。出現權限錯誤,或這裡說成功、表裡卻沒有,多半是授權畫面沒有全部勾選。");
}完整的 appsscript.json(存 Drive 版)
{
"timeZone": "Asia/Taipei",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"webapp": {
"executeAs": "USER_DEPLOYING",
"access": "ANYONE_ANONYMOUS"
},
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/script.external_request"
]
}第 2 段 裝 gws、讓 agent 替你下指令
這段做完你會有什麼:在終端機裡用 gws 讀自己的雲端硬碟;然後讓 agent 用它替你把「照片資料夾+待辦試算表」整理成一份 summary.txt。
gws 是 Google 自己出的命令列工具。它不是幫你「登入 Google」而已,而是要你先在 Google Cloud 開一個自己的專案、拿一把「用戶端憑證」(一個 JSON 檔,等於這支工具進你 Google 帳號的門票),gws 才有身分去敲 Google 的 API(應用程式介面)。所以前半段都在 Google Cloud Console(Google 雲端平台的管理網頁)按按鈕,後半段才回終端機。
老師 2026-10-09 用自己的帳號從頭走過一次:2-1 到 2-7 全程約 15 分鐘(含截圖),下面每一步的畫面都是那次拍的。兩個差別先講:①老師用的是學校帳號,新增專案時多了「組織」「父項資源」兩欄,你用個人 Gmail 不會有;②老師的同意頁按一次「繼續」就過了,個人 Gmail 用「外部」測試模式可能多一頁「Google 尚未驗證這個應用程式」的警告——遇到就按「進階」→「前往 …(不安全)」繼續,不確定就舉手。10:40 還在 Cloud Console 的,別硬撐:停下來看老師示範,這段回家也做得完,下週再對一次。
-
用跟 bot 同一個個人 Gmail登入
console.cloud.google.com(資料夾和試算表都在這個帳號的雲端硬碟裡)。上方的專案選單 → 「新增專案」→ 專案名稱自取(例如line-todo-w6)→ 建立。建好後右上角會跳一則通知「建立專案:你取的名字」,按通知裡的「選取專案」切換過去。新增專案:只要填「專案名稱」就能按「建立」。老師用學校帳號所以多了「組織」「父項資源」兩欄,個人 Gmail 沒有;上面黃色的配額提示不用理。 建好:右上「通知」出現「建立專案:…」,按「選取專案」;之後上方的專案名稱就是你的,「專案資訊」卡片裡也是。 你應該看到頁面上方的專案選單變成你剛取的名字,「專案資訊」卡片裡的專案名稱也是。測一下重新整理一次,專案名稱還是你的,就過。卡住了
- 要你同意服務條款、選國家→ 第一次用 Cloud Console 會問,選台灣、同意。
- 用學校帳號登進去了→ 右上頭像切換到個人 Gmail,否則等一下找不到你的資料夾。
- 通知關掉了、上方還是別的專案→ 點上方專案選單,在清單裡找你取的名字選它。
-
上方搜尋列打
Google Drive API,選它進到「產品詳細資料」頁,按藍色的「啟用」;再搜Google Sheets API,一樣按「啟用」。兩個都要。(從左上 ≡ 選單「API 和服務」→「程式庫」搜,會到同一頁。)Google Drive API:按「啟用」(旁邊的「試用這個 API」不用按)。 Google Sheets API:同樣按「啟用」。右上跳出來的 Gemini CLI 廣告關掉就好。 按過之後再回到同一頁:按鈕變成「管理」,旁邊多了「API 已啟用」(老師課後回去看的)。 你應該看到按下「啟用」後頁面轉到那個 API 的管理頁。回到「產品詳細資料」頁再看一次,原本的「啟用」變成「管理」、旁邊打勾寫「API 已啟用」(上圖)。測一下兩個 API 的頁面都顯示「管理」而不是「啟用」,就過。卡住了
- 搜到好幾個名字像的→ 要的是「Google Drive API」「Google Sheets API」,不是 Drive Activity、Drive Labels 那些。
- 啟用了但後面 gws 還是 403 說 API 沒啟用→ 剛啟用要等一兩分鐘;也確認你啟用的是「這個專案」(看上方專案名稱)。
-
上方搜尋列打
Google Auth Platform進去(舊版叫「OAuth 同意畫面」),總覽頁寫著「尚未設定 Google 驗證平台」→ 按「開始」。接著是四步:- 應用程式資訊:應用程式名稱自取(例如
line-todo-w6)、使用者支援電子郵件選你的 Gmail → 下一步 - 目標對象:選「外部」→ 下一步
- 聯絡資訊:填你的 Gmail → 下一步
- 完成:勾「我同意《Google API 服務:使用者資料政策》」→ 繼續 → 最下面藍色的「建立」
Google Auth Platform 總覽:按「開始」。 步驟 1:名稱、支援信箱 → 下一步。 步驟 2:選「外部」。老師的學校帳號兩個都能選;個人 Gmail 通常只有「外部」。 步驟 3:聯絡信箱 → 下一步。 步驟 4:勾同意 → 繼續。 四步都打勾後,按最下面的「建立」。 你應該看到畫面回到總覽,不再寫「尚未設定」;左側的「目標對象」「用戶端」可以點進去了(下兩步要用)。測一下左側點「目標對象」,頁面上有「測試使用者」區塊,就過。卡住了
- 搜不到「Google Auth Platform」→ 左上 ≡ 選單「API 和服務」→「OAuth 同意畫面」會轉到同一個地方。
- 「內部」「外部」兩個都能選→ 一樣選外部(學校帳號才會兩個都有)。
- 它說要送審(驗證)→ 不用。測試模式就夠今天用,下一步把自己加成測試使用者即可。
- 應用程式資訊:應用程式名稱自取(例如
-
左側「目標對象」→ 往下的「測試使用者」→「+ Add users」→ 右邊滑出的面板打你的 Gmail → 按「儲存」。沒做這步,等一下登入會被擋(Access blocked)。
「目標對象」→「測試使用者」→「+ Add users」,右邊滑出「新增使用者」面板。 打你的 Gmail、按「儲存」。信箱會先變成一個小標籤;沒出現在清單就再按一次「儲存」。 加好:上方寫「1 位使用者」,清單裡有你的信箱。 你應該看到「測試使用者」清單裡有你的信箱,上方寫「1 位使用者(1 位測試使用者…)/上限為 100 位」。測一下清單有你,就過。卡住了
- 按了「儲存」,信箱沒出現在清單→ 第一次按可能只是把信箱變成一個小標籤,再按一次「儲存」(老師實走就按了兩次)。
- 找不到「測試使用者」→ 你在 2-3 選到「內部」(那就不需要這步);或 2-3 最後的「建立」還沒按。
-
左側「用戶端」→「+ 建立用戶端」→ 應用程式類型選「電腦版應用程式」→ 名稱自取(例如
gws-desktop)→ 建立。會跳出一個「已建立 OAuth 用戶端」的視窗,裡面有用戶端 ID 和用戶端密碼:按「下載 JSON」,再按確定關掉。「用戶端」頁一開始是空的,按上方的「+ 建立用戶端」。 應用程式類型選「電腦版應用程式」。 名稱改成 gws-desktop(叫什麼都行);「Use this client for an AI-powered agent」不用勾 → 建立。關掉跳出視窗後,清單多了一列,下方提示「已建立 OAuth 用戶端」。 關掉視窗才想到要下載:點清單裡的名字進詳細頁,往下捲到「用戶端密鑰」,右邊的下載圖示(滑過去寫「下載 JSON」)隨時可以再下載。密碼這裡只顯示尾四碼。 你應該看到Downloads 多一個檔,名字是client_secret_開頭、.apps.googleusercontent.com.json結尾,中間一長串是你的用戶端 ID。測一下檔案在 Downloads,就過。這個檔等於你這個專案的鑰匙:不要傳給別人、不要截圖它的內容、不要貼進對話。(老師那張跳出視窗的截圖因為有密碼,這裡故意不放。)卡住了
- 關掉視窗才想到要下載→ 用戶端清單點你的用戶端名字 → 詳細頁往下捲到「用戶端密鑰」→ 右邊的下載圖示(上圖)。
- 「用戶端」頁進不去、或建立失敗→ 2-3 還沒做完(沒按到最後的「建立」),先回去。
- 「Use this client for an AI-powered agent」要不要勾→ 不用。
-
gws 固定到
%USERPROFILE%\.config\gws\client_secret.json找這把鑰匙。先看瀏覽器右上的下載清單,確認檔案真的在 Downloads,再貼這三行(第二行會從 Downloads 挑最新下載的那個 client_secret 開頭的檔,搬過去並改名;下載過兩次也沒關係):New-Item -ItemType Directory -Force "$env:USERPROFILE\.config\gws" | Out-Null Get-ChildItem "$env:USERPROFILE\Downloads\client_secret*.json" | Sort-Object LastWriteTime | Select-Object -Last 1 | Move-Item -Destination "$env:USERPROFILE\.config\gws\client_secret.json" -Force Get-ChildItem "$env:USERPROFILE\.config\gws"
你應該看到第三行印一張小表,Name欄只有一列client_secret.json(Length大約 400)。測一下有那個檔、名字一字不差,就過。卡住了
- 第三行列出來是空的→ Downloads 裡沒有 client_secret 開頭的檔:瀏覽器存到別的資料夾,或檔名不是 client_secret 開頭;用檔案總管找到它,拖進
%USERPROFILE%\.config\gws(在檔案總管上方的網址列貼這個路徑、按 Enter 就會開;名字有小數點的資料夾不是隱藏的,只是長得怪)並改名成client_secret.json。 - 以前也做過、Downloads 裡有好幾個 client_secret 檔→ 第二行會挑最新的那個;確定是今天這個專案下載的就好(不確定就回 2-5 再下載一次、重貼這三行)。
- 第三行列出來是空的→ Downloads 裡沒有 client_secret 開頭的檔:瀏覽器存到別的資料夾,或檔名不是 client_secret 開頭;用檔案總管找到它,拖進
-
把要用的 Google 服務限制在雲端硬碟與試算表兩個(登入本身還會附帶 openid、email、profile 三個基本身分權限,正常)。PowerShell 裡逗號清單一定要加引號,不然會被拆成兩個參數、gws 靜靜地只要最基本的權限。
gws auth login -s "drive,sheets"
它不會自己開瀏覽器,只印出「Open this URL in your browser to authenticate:」和一條很長的
https://accounts.google.com/o/oauth2/auth?…網址,然後停在那裡等。把整條網址複製、貼到瀏覽器 → 選你的個人 Gmail(2-4 加的那個)→ 看到「登入『你的應用程式名稱』」頁 → 繼續(⚠️ 老師只用學校帳號走過;個人 Gmail 可能先出現「Google 尚未驗證這個應用程式」→ 進階 → 前往…(不安全),也可能多一頁權限勾選框,全部勾選再繼續)→ 瀏覽器變成一行「You may now close this window.」→ 回終端機,它已經自己印出一段以"status": "success"結尾的 JSON。貼網址後:「登入『w06-gws-test』」(你的會是你取的應用程式名稱),右邊列它要拿的個人資訊。 往下捲,按「繼續」。 瀏覽器只剩一行「You may now close this window.」就是成功了,關掉分頁回終端機。 你應該看到終端機最後印出這樣一段(帳號與路徑是你的;中間 keyring、AES 那幾行是它在說「密碼已加密存好」,不用管):Open this URL in your browser to authenticate: https://accounts.google.com/o/oauth2/auth?… Using keyring backend: keyring { "account": "你的帳號@gmail.com", "credentials_file": "C:\\Users\\你\\.config\\gws\\credentials.enc", "encryption": "AES-256-GCM (key in OS keyring or local `.encryption_key`; …)", "message": "Authentication successful. Encrypted credentials saved.", "scopes": [ "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets", "openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile" ], "status": "success" }測一下"status": "success"而且scopes裡有 drive 與 spreadsheets 兩條,就過(下一步再用gws auth status驗一次)。卡住了
- 瀏覽器說 Access blocked:…has not completed the Google verification process→ 2-4 沒把自己加成測試使用者,或你選錯了 Google 帳號。加好再重跑這行。
- 瀏覽器說「Google 尚未驗證這個應用程式」→ 正常(你自己的測試應用程式本來就沒送審):按「進階」→「前往 …(不安全)」繼續。老師的學校帳號沒出現這頁,個人 Gmail 可能會。
- 它說找不到 client_secret.json→ 2-6 的路徑或檔名不對。
- 網址太長、貼到瀏覽器說網址無效→ 終端機把網址折成好幾行,複製時夾進了換行或空格:先貼到記事本接成一行再貼瀏覽器;或先把視窗拉到最寬,再從 https 拖到最後一個字元複製。
- 貼網址後瀏覽器說 localhost 拒絕連線→ 你把終端機關了,或等太久它放棄了;重跑這行、這次快一點。
- scopes 只有 openid、email、profile 三個→ 引號沒加、逗號清單被拆掉了。重跑
gws auth login -s "drive,sheets"。
-
gws auth status
你應該看到一段 JSON(老師 10/09 的真輸出,路徑與帳號換成你的;中間幾行省略):Using keyring backend: keyring { "auth_method": "oauth2", "client_config": "C:\\Users\\你\\.config\\gws\\client_secret.json", "client_config_exists": true, … "has_refresh_token": true, … "project_id": "你的專案 ID", "scope_count": 7, "scopes": [ "email", "profile", "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "openid" ], "storage": "encrypted", "token_cache_exists": true, "token_valid": true, "user": "你的帳號@gmail.com" }測一下has_refresh_token是 true、user是你的帳號、scopes 有 drive 也有 spreadsheets,就過。卡住了
- scopes 只有 openid、email、profile 三個→ 2-7 的引號沒加、逗號清單被拆掉了。重跑
gws auth login -s "drive,sheets"。 has_refresh_token是 false 或說沒登入→ 2-7 沒跑完(網址沒貼或沒按繼續),重跑。
- scopes 只有 openid、email、profile 三個→ 2-7 的引號沒加、逗號清單被拆掉了。重跑
-
兩條都貼。第一條列你雲端硬碟最近的 5 個檔,排成一張表;第二條加了條件,只找「LINE待辦照片」這個資料夾。
gws.cmd drive files list --params '{\"pageSize\":5,\"fields\":\"files(name,mimeType)\"}' --format table gws.cmd drive files list --params '{\"q\":\"name=''LINE待辦照片''\",\"fields\":\"files(name,mimeType)\"}'為什麼是
gws.cmd、引號前多一條反斜線、name=兩邊沒空格:Windows 內建的那版 PowerShell 把引號交給別的程式時會吃掉,這樣寫在新舊版都通(老師 10/09 兩版各跑過一次)。等一下讓 agent 下指令,它也可能撞到同一件事。你應該看到第一條印一張表,表頭是mimeType和name,底下 5 列是你雲端硬碟最近碰過的檔(每個人不同)。第二條像這樣:{ "files": [ { "mimeType": "application/vnd.google-apps.folder", "name": "LINE待辦照片" } ] }第 1 段還沒做到 1-10 的,第二條會印"files": []——正常,資料夾是 bot 收到第一張照片時才建的。測一下第一條有印出表格就過:終端機真的拿到你雲端硬碟的門票了。第二條找得到資料夾=第 1 段也成功了;找不到不影響這段。卡住了
- Invalid --params JSON: key must be a string→ 打成
gws而不是gws.cmd,或少了反斜線;整行從頭複製再貼,別手打。 - unexpected argument '=' found→ 條件裡多了空格(
name = …);照抄name=''LINE待辦照片''。 - 403:Google Drive API has not been used in project … or it is disabled→ 2-2 沒啟用、或啟用在別的專案;啟用後等一兩分鐘再跑。
- 403:insufficient authentication scopes→ 2-8 的 scopes 沒有 drive;重做 2-7(記得引號)。
- 重登過了還是 403→ 舊的通行證被快取住了:把
%USERPROFILE%\.config\gws\token_cache.json改名成別的(別刪),再跑一次。 - 第二條印 "files": []→ 第 1 段的 1-10 還沒成功;不影響這段,先往下。
- Invalid --params JSON: key must be a string→ 打成
-
在電腦建一個空資料夾(例如
gws-agent-w6),用 Antigravity 或 Codex 開它,貼這段(把試算表名稱填進去)。老師會先投影示範一次,看它列指令、停下來等你同意的那一刻。11:00 休息前還沒做到這步的,看老師示範就好;這步回家也做得了。我已經用 gws(Google Workspace CLI)登入好了,不要幫我重新登入。請用 gws 列出我 Drive「LINE待辦照片」資料夾的檔案、讀我的待辦試算表最後 5 列、整理成一份今日摘要存成 summary.txt(存在這個資料夾)。我的待辦試算表名稱是「___」。每一條指令執行前先告訴我它會做什麼,等我同意再跑。
路線 B(沒有 agent):把同一段貼給 ChatGPT/Gemini 請它寫出要跑的 gws 指令,你自己一條一條貼進 PowerShell;每條跑之前自己看一眼它要做什麼。指令裡的
<資料夾ID>、<試算表ID>別自己從一堆 JSON 裡挑:請 AI 一次給一條,你跑完把輸出貼回去、請它幫你挑出 ID 再給下一條。你應該看到它先列出要跑的指令,再一條一條執行。老師 10/09 用 Codex 實走,它列的是這四條(<資料夾ID>、<試算表ID>由前一條的結果代入;這是老師電腦新版 PowerShell 的寫法,跟 2-9 不一樣;你的 agent 會照它自己的環境寫):gws drive files list --params '{"q":"name = ''LINE待辦照片'' and mimeType = ''application/vnd.google-apps.folder'' and trashed = false","fields":"files(id,name,modifiedTime)","pageSize":1000}' gws drive files list --params '{"q":"''<資料夾ID>'' in parents and trashed = false","fields":"files(id,name,mimeType,size,modifiedTime)","pageSize":1000}' gws drive files list --params '{"q":"name = ''你的試算表名稱'' and mimeType = ''application/vnd.google-apps.spreadsheet'' and trashed = false","fields":"files(id,name,modifiedTime)","pageSize":1000}' gws sheets spreadsheets values get --params '{"spreadsheetId":"<試算表ID>","range":"A1:ZZZ10000","majorDimension":"ROWS"}'每條跑之前它會說這條要做什麼、等你同意(⚠️ 老師只用無頭的 Codex 驗過這份指令清單,Antigravity/Codex 桌面版的確認框沒彩排,以當天畫面為準)。資料夾找不到它就直接說「沒有這個資料夾」、跳過第二條。跑完資料夾裡出現summary.txt。老師那次示範表只有一列、照片資料夾還沒建,它寫出來的摘要長這樣:今日摘要 日期:2026-10-09 一、LINE待辦照片 Google Drive 中沒有名稱為「LINE待辦照片」的資料夾。 二、(你的試算表名稱) 第一個工作表:工作表1 資料列總數:1(不含標題;不足 5 列) 最後 5 列(實際僅 1 列): 1. 建立時間:2026-09-02 14:55:00 事項內容:明天下午三點要跟指導教授討論論文進度 分類:課業 日期:2026-09-03 時間:15:00
測一下打開 summary.txt:照片檔名對得上 Drive 資料夾、最後 5 列對得上試算表。對得上才過。卡住了
- 它沒問你就直接跑了→ 看它的設定,把指令執行改成「每條都先問」(各家名稱不同,找 approval/permission 這類字樣)。無頭(沒有視窗、純命令列)的模式本來就不會問,今天不要用那種。
- 它先讀一堆說明檔、半天不跑指令→ 有些 agent 裝了 Google Drive 外掛會先去讀外掛說明(老師第二次實走就多花了一分鐘)。在任務後面加一句「不要讀任何外掛說明檔,直接用 gws」再貼一次。
- 它說工作區唯讀、寫不了 summary.txt→ 你開的不是可寫的資料夾,或 agent 的權限設成唯讀;改開 Downloads 底下新建的資料夾,或請它把摘要印出來、你自己存檔。
- 它找不到試算表→ 名稱打錯一個字就找不到;去雲端硬碟把試算表名稱整個複製貼進提示詞。
- 它跑 gws 一直出現 Invalid --params JSON 或 unexpected argument→ 這台是 Windows 內建的舊版 PowerShell:回它「用 gws.cmd、JSON 的引號前加反斜線、JSON 裡不要有空格」,或請它照 2-9 那兩條的寫法。
- 它想跑
gws auth login→ 回它「已經登入了,直接用」。 - 它要跑的指令有 delete、update、create→ 不要同意,回它「只准讀」。
第 3 段 本地模型也會上網搜
這段做完你會有什麼:一個跑在你這台電腦上的模型,問它三道「要上網才答得出」的題目,它自己決定搜、附來源;而且你能說出「是誰在搜」。
先記三句,等一下親眼驗證:模型本身不會上網;會搜的是 Ollama app 幫它接的搜尋工具;模型只決定「要搜什麼」,拿到結果再回答。模型跑在你的電腦、沒有 API 費用;搜尋那一下走的是 Ollama 的線上搜尋服務(要網路;目前不登入也能用、沒收費,額度規則以後可能變)。
-
回第 0 段:
ollama list有gemma4:e4b→ 跑 0-D 的ollama create→ 再ollama list多一個gemma4-search。你應該看到兩列:gemma4:e4b、gemma4-search:latest。測一下有 gemma4-search 就過。卡住了
- 還沒拉完→ 兩人一組,用旁邊拉完的那台做這段。
-
開始功能表搜 Ollama 開它。第一次啟動會問要不要 Create an account/Sign in,選 No thanks 先本機用——不登入也能搜(老師連跑三次都通)。想登入免費帳號也可以(提高搜尋額度、能用雲端模型),不是必要。
你應該看到左邊側欄三項:Apps、Chat、Settings;中間一個草泥馬頭像和輸入框。測一下看到輸入框就過。卡住了
- 卡在 Loading… 或「Run Ollama」導覽頁→ 你(或 0-A)從指令列另外起過 Ollama 伺服器。按 Ctrl+Shift+Esc 開工作管理員 → 「詳細資料」頁,把
ollama app.exe與ollama.exe都「結束工作」,再開 app。 - Ollama 回 500、CUDA error→ 顯示卡同時載了別套模型(例如 LM Studio 開著);關掉其他跑模型的程式再試。
- 卡在 Loading… 或「Run Ollama」導覽頁→ 你(或 0-A)從指令列另外起過 Ollama 伺服器。按 Ctrl+Shift+Esc 開工作管理員 → 「詳細資料」頁,把
-
輸入框右下那顆就是模型選單。預設選的是雲端的 glm 模型,一定要換:點開、打
gemma4-search、選它。Chat 畫面:輸入框右下,左邊圓圈是地球圖示(網頁搜尋開關),右邊是模型選單。這張老師選的還是 gemma4:e4b,你要換成 gemma4-search。 你應該看到模型選單上顯示gemma4-search。測一下選單上的字是 gemma4-search(不是 gemma4:e4b、不是 glm),就過。卡住了
- 模型清單一直 Loading→ 跟 3-2 第一條一樣:兩個 ollama 程序都結束再開。
- 清單裡沒有 gemma4-search→ 0-D 沒跑成,回去跑;跑完 app 要重開才看得到。
-
輸入框旁的地球圖示=網頁搜尋開關,點一下讓它亮起來(上圖那個有藍色圈圈的狀態)。
你應該看到地球圖示有藍色外圈。測一下亮了就過。卡住了
- 地球圖示點不亮或是灰的→ Settings 裡「Cloud:Enable cloud models and web search」要開著(預設是開的)。
Settings 第一項 Cloud「Enable cloud models and web search」預設開著——地球圖示要靠它。 -
每一題:先把地球關掉問一次 → 開一個新對話、把地球打開再問一次。三道題(答案在下面摺疊,問完再開):
# 題目(照抄) 1 2026 年諾貝爾物理獎得主是誰?得獎理由是什麼? 2 2026 年諾貝爾生理醫學獎的三位得主是誰?研究什麼? 3 Ollama 目前最新的正式版本號是多少?什麼時候發布的? 你應該看到地球關:多半說不知道、尚未公布(它的知識停在訓練那天),也可能憑印象猜一個——重點是它拿不出可以核對的來源。第一題會等很久——模型要先載進顯示卡,老師的電腦等了兩分多鐘,之後每題幾秒。地球開:先出現「Searching for …」,接著「Thought for 幾秒」,然後答案附來源。老師 10/09 問第一題,它想了 7.9 秒就自己去搜,答出 Francis Halzen 與 IceCube:地球開、模型 gemma4-search:「Thought for 7.9 seconds」→ 根據網路搜尋結果,得主是弗朗西斯・哈爾岑(Francis Halzen)。 同一題答案的下半:三點重點說明(IceCube/冰立方微中子天文台)。你的答案字句會不一樣,看「得主對不對、有沒有來源」。 測一下三題「地球開」都有來源、得主與版本號對得上下面的答案,就過。第 3 題的答案會隨時間變,以老師課堂上公布的為準。時間不夠就第 1 題做完整比較,第 2、3 題只問「地球開」那次。卡住了
- 地球開了它還是答「2026 還沒到」、不搜→ 你選到的是
gemma4:e4b不是gemma4-search(原生模型沒有那段規矩,想 19 秒就認定還沒發生)。回 3-3 換模型。 - 沒出現 Searching、直接憑記憶答→ 它這次決定不搜(這顆小模型的判斷不是每次都穩)。開新對話再問一次,或在題目後面加一句「請先搜尋再回答」。
- 搜到了卻回「請告訴我你要我做什麼」→ 同上,重問一次。
- 回「Error: unknown tool: web_search」→ 地球沒亮(或 Settings 的 Cloud 開關被關了):按亮再問一次。
- 第一題等超過三分鐘→ 看工作管理員的 GPU 有沒有在動;沒有就回 3-2 第二條。
對答案(問完再開;查核日 2026-10-09)
# 標準答案 1 Francis Halzen 一人獨得;理由=對 IceCube 微中子觀測站的決定性貢獻、偵測到來自天體的高能微中子。2026-10-06 公布。 2 Karl Deisseroth、Peter Hegemann、Georg Nagel,各三分之一;光遺傳學(用光開關單一神經元)。2026-10-05 公布。 3 查核日當天是 v0.40.1(2026-10-07 發布)。⚠️ 會變,以老師上課公布的為準。 有空的再把同一題丟給你的 LINE bot,看它怎麼回——它接的是沒有搜尋的免費 Gemini。
- 地球開了它還是答「2026 還沒到」、不搜→ 你選到的是
-
地球開那次的答案上方有一行「Search results for 你的題目」,點它展開。
展開後是真的搜尋結果(title、url、content 一筆一筆列),第一筆就是 GitHub 的 releases 頁。這張是第 3 題、老師還沒換模型時拍的(右下還是 gemma4:e4b)。 你應該看到一串"title"、"url"、"content",而且答案裡的來源就在這裡面。測一下隨便點一個 url 開開看,真的是那個網頁,就過。這就是「搜的不是模型,是 app 接的工具」的證據。卡住了
- 沒有「Search results」那行→ 那次它沒搜,看 3-5 卡住了第二條。
那「模型只負責決定要搜什麼」看得到嗎?
Ollama app 把這一瞬間自動做掉了。另一個程式 LM Studio 會先停下來問你,剛好把它拍出來:模型說「我想呼叫 search,關鍵字是 2026年諾貝爾物理獎得主」,等人按 Proceed。關鍵字是模型決定的、按鈕是人按的、真的去搜的是工具。
LM Studio 的核准框(不是今天的主線,只拿來看機制):Model wants to call search → query「2026年諾貝爾物理獎得主」→ Proceed/Deny。想自己試這條路看 local-llm-search.html。 -
每一題「地球關/地球開」各一張(三組六張,或一題一張合在一起也行),加一句話:哪一次答對、你怎麼知道(來源在哪)。
你應該看到三組截圖存在桌面或你的雲端硬碟。測一下三組都有、那句話寫了,就過。卡住了
- 截圖快捷鍵→ Win+Shift+S 框選,貼到小畫家或直接貼進 Google 文件。
第 4 段 收尾與帶走
這段做完你會有什麼:今天的證據都在手上,教室電腦上沒留下你的帳號。
-
# 截什麼 哪一段 1 手機上「照片那筆」的回覆(有 📷 那行) 1-10 2 試算表那一列(來源=照片、「照片連結」欄有連結) 1-10 3 雲端硬碟的「LINE待辦照片」資料夾(裡面有檔) 1-10 4 summary.txt 打開的樣子(或 agent 列出的 gws 指令) 2-10 5 三組答案(地球關/開)+一句「哪個對、為什麼」 3-7 你應該看到五張(或五組)都在你自己的雲端硬碟或手機裡,不是只在教室電腦的桌面。測一下用手機或自己的帳號打開確認看得到,就過。做不完的先把有的傳走。卡住了
- 第 2 段沒做完→ 第 4 張改截「卡在哪一步的畫面」,下週補。
-
教室電腦有還原卡,重開機會清掉;但在那之前下一個人就坐在這台前面。離開前:①瀏覽器右上頭像 → 登出 Google(Cloud Console、雲端硬碟、Apps Script 都跟著登出)②LINE 官方帳號後台與 LINE Developers 登出 ③瀏覽器跳出「要儲存密碼嗎」一律按「不要」;有存到的到設定 → 密碼管理員刪掉 ④終端機貼下面三行:gws 登出、確認、把鑰匙檔那個資料夾整個刪掉(瀏覽器登出不會清掉 gws 存的登入憑證,這步一定要做)⑤關掉所有瀏覽器視窗與終端機。
gws auth logout gws auth status Remove-Item -Recurse -Force "$env:USERPROFILE\.config\gws"
你應該看到第一行印"message": "Logged out. All credentials and token caches removed.";第二行裡"auth_method": "none"、"token_cache_exists": false;重開瀏覽器打開 drive.google.com 要你重新登入。測一下drive.google.com 顯示登入頁、gws auth status說 none,就過。卡住了
- Remove-Item 說找不到路徑→ 2-6 沒做過(鑰匙檔不在這台),沒事。
- 回家想繼續用 gws→ 在自己電腦重做 2-5(下載 JSON)、2-6、2-7 就好,Cloud Console 那些設定都還在。
-
作業老師上課時會在 e學苑 開,繳交內容與方式到時候一起說明,這一頁不先寫。
回家想在自己電腦重做一遍:第 0 段整份指令與 Modelfile 都在這一頁,複製就好。想多試其他做法(LM Studio 讓模型自己呼叫搜尋工具、瀏覽器擴充代搜、用小程式自己接工具)的,看另一頁 我們試過的全部:local-llm-search.html——那是額外參考,不是今天的進度。
今天三件事做完,你手上有:一支會把照片存進雲端硬碟的 bot、一把能讓終端機和 agent 替你讀雲端硬碟的鑰匙、一個會自己上網查的本地模型——三樣都是你自己接起來的。
你應該看到這一頁的網址存在你自己的地方(書籤,或用 LINE 傳給自己)。測一下沒有要測的;把這一頁的網址記下來就好。卡住了
- local-llm-search.html 打不開→ 跟這一頁放在同一個地方,網址把 student.html 換成 local-llm-search.html;還是不行就下週再看。
卡關總表
上面每一步的「卡住了」全部集中在這裡,打關鍵字就過濾(例如 403、Loading、授權、api-data)。
| 步驟 | 症狀 → 處理 |
|---|
本頁內容來源:W6 內容規格與 2026-10-09 老師在自己電腦的實走紀錄(本地模型段、Drive 對照答案、第 2 段 Cloud Console 與 gws 全程、agent 下 gws 指令);第二關的提示詞與「看 diff 找四件」接自 W5 教師手記與〈LINE AI 待辦小秘書〉實作指南 Level 2。沒實走到的(個人 Gmail 的同意頁、Antigravity/Codex 桌面版跑 agent)在該步標明。