本篇適合:
- 小企業、組織,有「大量發通知」、「印收據」的需求,且需要便於管理的工具。
- 對 生成式 AI 應用有興趣的朋友
- 就算你不會寫程式,也有機會快速上手!
本篇目標:
- 學習如何與AI互動解決內心真正的問題
- 製作出一個自動化收據開立系統!
筆者雖然過去寫過一些小程式,但比起真正企業的工程師大概還差一大截。這幾個月來生成式 AI (Generative Artifical Intelligence,後文簡稱 GAI)的快速發展,實在令不少人跌破眼鏡。
一直都知道「Google Apps Script」的存在,是一個可以允許你控制和自動化Google文件、試算表、簡報工具,但一來不知道應用場景,一來更不知道怎麼使用,要重新學習一個東西也相當需要門檻(仿間也出了不少工具書、付費課程)。
現在有了 GAI,叫 AI 教你一步一步做吧!以下比較不像是教你怎麼做,而是教你怎麼請AI教你做!
初始指令
首先,我們需要定義任務。切記,這個「任務」你需要好好審慎思考。
錯誤指令:「我想用 Google 做一個自動化收據系統」
當然,AI 可能也會給你一個答案,但它可能並不完全符合你的想像情境。
對,「想像情境」。有想像,且試圖去「描述想像」,才會有機會讓 AI 正確教你,且做出來符合使用者要求的東西。
以下給你參考我的指令(很多人會稱之為「Prompt」,而這個動作叫做「Prompt Engineering」):
1 2 3 4 5 6 7 8 9 10 11
| 我想使用 Google App Script 搭配google spreadsheet, google doc 做自動生成收據系統, 現在我有一份 spreadsheet 表單和google doc 收據(名為「收據模板」,儲存在同一層中的「收據製作」資料夾內),
請使用 app script 達成以下情況:
1. 當 spreadsheet 中偵測到新增一筆數據的「收據製作(J欄)」呈現「未處理」,則開始準備製作收據。切記,未處理改成已處理的動作,並不會觸發收據製作。 2. 使用我的 google doc(可以先副本一份) 將相對應的欄位填入,並將檔案轉成 PDF , PDF 文件名稱:改成收據 + 姓名 + _ + 序號,PDF名稱與doc 副本檔案名稱一致,PDF 檔案儲存到「收據製作」資料夾裡面的「收據」資料夾 3. 製作完成後,spreadsheet 在 K 欄對應該筆完成收據製作的資料,寫「自動程式已完成收據製作」
請你一步一步教我怎麼操作,使用繁體中文回覆
|
同時,現在大部分的 AI 都已經支援圖片上傳,也請你上傳幾個相關的圖片,讓 AI 自己去辨識揣摩。
Google Apps Script 使用
先寫在前面,AI 認識的「Google Apps Script」,可能是幾年前的 Google Apps Script,因此他對你的指示是有可能有所落差,不過應該會大同小異,你可以切記這點。
而這邊簡單介紹一下這個工具,可以從 Google 試算表中上層選單「擴充功能 > Apps Script」進入畫面。
進入畫面後,就會看到輸入程式碼的區塊,此時你可以把 AI 給你的程式碼直接挑到主要區塊當中,把預設的「funciton myFunction()
」覆蓋掉。如果你從來沒寫過程式,請勇敢做這件事,它不會被你玩壞的!儘管玩!(很重要XD)
接著帶你介紹介面,左邊欄位的時鐘號誌是「觸發器(Trigger)」,當你做的自動化工具是「因為某個動作」或「到了某個時間」就要觸發,通常會使用這個工具,有時候我知道了我需要有這樣的功能,會直接在 Prompt 裡面告訴 AI 說我需要使用觸發器。
(當然,AI 有時候也會自己判斷是否需要觸發器)
觸發器下面是「執行項目」,也就是可以看到所有程式碼執行情況的地方。比起其他程式碼,我覺得 Apps Script 更難去針對錯誤解決問題。因此你的 Prompt 可以多加「能否幫我把程式碼做一些log讓我可以看?」
這裡的 log 不是數學的對數,在程式語言中指的是日誌(logging),通常會有視窗(或檔案)記錄程式執行的過程,幫助開發者理解程式的運行狀況,診斷錯誤和問題。因此,我在 Prompt 裡增加「能否幫我把程式碼做一些log讓我可以看?」後,我就可以在「執行項目」裏看到他執行的過程,讓我更好理解發生什麼事、程式執行到哪。
調整細部工作內容、調整錯誤
雷點 1 - 把自己當白癡,跟著 AI 一步一步做
「切記,跟著 AI 一步一步做,不要漏掉!」
AI 可能出錯,但常常我自己的錯是漏掉 AI 給的步驟,因此請記得鉅細靡遺的嘗試一步一步來,不會的話,也可以反覆詢問AI。
雷點 2 - AI 可能出錯,貼錯誤訊息給他!
貼錯誤訊息,或說「無法成功,請重新思考一次」,都是一種方式,不過,如果能有錯誤訊息,會更能讓 AI 知道怎麼改善!
我的任務
最後,直接附上我的成果。
首先準備好一份已經處理好的 Spreadsheet 表格,以及一份收據模板:
在這邊,根據 Claude 最後給我的 code,設定的觸發條件是當 J 欄(收據製作)文字是「未電子開立」且 K欄沒有任何值,我就會開始進行收據製作,並也會轉成 PDF 檔存放到指定的位址!
如果想要直接使用我的程式碼,你需要準備上面兩份資料,搭配我的 Prompt 就可以使用!(記得相關的資料夾也要設定喔!詳見我給 AI 的指令)
Claude 回覆與程式碼
- 設置步驟:
a. 將上面的代碼添加到您的腳本中。
b. 運行 createEditTrigger 函數來創建安裝的觸發器。
c. 在腳本編輯器中,轉到 “觸發器” 頁面(左側邊欄時鐘圖標)。
d. 您應該能看到一個新的觸發器被創建。
- 授予權限:
a. 在觸發器頁面,點擊新創建的觸發器。
b. 在彈出的窗口中,您可能需要再次授予權限。
c. 仔細閱讀權限請求,然後點擊 “允許”。
- 測試:現在,當您在試算表中編輯 J 列時,應該會觸發 onEditTrigger 函數,而不是原來的 onEdit。這個函數應該有足夠的權限來訪問 Drive。
錯誤處理和日誌
- 為了更好地捕獲可能的錯誤,我們可以在 generateReceipt 函數中添加更多的錯誤處理和日誌記錄
這些更改應該能幫助解決權限問題,並提供更詳細的錯誤信息。如果您仍然遇到問題:
- 檢查觸發器是否正確創建和授權。
- 運行 testDrivePermissions 函數(如之前提供的),確保基本的 Drive 操作正常。
- 如果問題持續,請查看執行日誌並分享詳細的錯誤信息。
如果您需要進一步的幫助或有任何疑問,請告訴我。我們會繼續調試直到問題解決。
程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
| function createEditTrigger() { var ss = SpreadsheetApp.getActive(); ScriptApp.newTrigger('onEditTrigger') .forSpreadsheet(ss) .onEdit() .create(); Logger.log('Edit trigger created successfully'); }
var pendingRows = [];
function onEditTrigger(e) { Logger.log('onEditTrigger executed'); var sheet = e.source.getActiveSheet(); var range = e.range; var startRow = range.getRow(); var numRows = range.getNumRows(); for (var i = 0; i < numRows; i++) { var currentRow = startRow + i; var jCellValue = sheet.getRange(currentRow, 10).getValue().toString().trim(); var kCellValue = sheet.getRange(currentRow, 11).getValue(); if (jCellValue === "未電子開立" && kCellValue === "") { Logger.log('Row ' + currentRow + ' needs processing'); sheet.getRange(currentRow, 11).setValue("等待處理..."); pendingRows.push(currentRow); } } if (pendingRows.length > 0) { Logger.log('Starting to process ' + pendingRows.length + ' rows'); processPendingRows(sheet); } }
function processPendingRows(sheet) { while (pendingRows.length > 0) { var row = pendingRows.shift(); Logger.log('Processing row: ' + row); sheet.getRange(row, 11).setValue("製作中..."); try { generateReceipt(sheet, row); sheet.getRange(row, 11).setValue("自動程式已完成收據製作"); Logger.log('Receipt generated successfully for row ' + row); } catch (error) { Logger.log('Error processing row ' + row + ': ' + error.toString()); sheet.getRange(row, 11).setValue("錯誤: " + error.toString()); } } }
function formatAmount(amount) { var num = parseFloat(amount); if (isNaN(num)) { return amount; } return num.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ","); }
function generateReceipt(sheet, row) { Logger.log('Starting generateReceipt for row: ' + row); try { var data = sheet.getRange(row, 1, 1, 11).getValues()[0]; Logger.log('Retrieved data: ' + JSON.stringify(data)); var [序號, 姓名, 開立日期, 證號, 項目, 用途, 總額, 地址, 電話, 收據狀態] = data; var formattedDate = Utilities.formatDate(new Date(開立日期), Session.getScriptTimeZone(), "yyyy/MM/dd"); Logger.log('Formatted date: ' + formattedDate);
var formattedAmount = formatAmount(總額); Logger.log('Formatted amount: ' + formattedAmount);
var templateFile = DriveApp.getFilesByName("收據模板").next(); Logger.log('Template file found: ' + templateFile.getName()); var newDoc = templateFile.makeCopy("收據_" + 姓名 + "_" + 序號, DriveApp.getFoldersByName("收據製作").next()); Logger.log('New document created: ' + newDoc.getName()); var doc = DocumentApp.openById(newDoc.getId()); var body = doc.getBody(); Logger.log('Replacing placeholders in the document'); body.replaceText("{{序號}}", 序號); body.replaceText("{{姓名}}", 姓名); body.replaceText("{{開立日期}}", formattedDate); body.replaceText("{{證號}}", 證號); body.replaceText("{{項目}}", 項目); body.replaceText("{{用途}}", 用途); body.replaceText("{{總額}}", formattedAmount); body.replaceText("{{地址}}", 地址); body.replaceText("{{電話}}", 電話); doc.saveAndClose(); Logger.log('Document saved and closed'); var pdf = DriveApp.getFileById(newDoc.getId()).getAs("application/pdf"); var receiptFolder = DriveApp.getFoldersByName("收據製作").next(); var pdfFolder; var subFolders = receiptFolder.getFoldersByName("收據"); if (subFolders.hasNext()) { pdfFolder = subFolders.next(); } else { pdfFolder = receiptFolder.createFolder("收據"); } Logger.log('PDF will be saved in folder: ' + pdfFolder.getName()); var pdfFile = pdfFolder.createFile(pdf); pdfFile.setName("收據_" + 姓名 + "_" + 序號 + ".pdf"); Logger.log('PDF created: ' + pdfFile.getName()); sheet.getRange(row, 11).setValue("自動程式已完成收據製作"); Logger.log('Receipt generated successfully, spreadsheet updated'); } catch (error) { Logger.log('Error in generateReceipt: ' + error.toString()); Logger.log('Error stack: ' + error.stack); sheet.getRange(row, 11).setValue("錯誤: " + error.toString()); } }
function manualTest() { var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); var testRow = 2; Logger.log('Starting manual test for row: ' + testRow); generateReceipt(sheet, testRow); Logger.log('Manual test completed'); }
|