UNPKG

cw-ai-backlog

Version:

AI-powered tool to generate backlog items from meeting documents and output to Google Sheets

542 lines (511 loc) 14.3 kB
const { google } = require('googleapis'); const path = require('path'); /** * 建立 Google Sheets API 客戶端 * @param {string} credentialsPath Google 憑證檔案路徑 * @returns {Object} Sheets API 客戶端 */ async function createSheetsClient(credentialsPath) { try { const auth = new google.auth.GoogleAuth({ keyFile: credentialsPath, scopes: [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ], }); const authClient = await auth.getClient(); const sheets = google.sheets({ version: 'v4', auth: authClient }); return sheets; } catch (error) { throw new Error(`建立 Google Sheets 客戶端失敗: ${error.message}`); } } /** * 從 Google Sheets URL 提取 Spreadsheet ID * @param {string} url Google Sheets URL * @returns {string} Spreadsheet ID */ function extractSpreadsheetId(url) { const match = url.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/); if (!match) { throw new Error('無效的 Google Sheets URL'); } return match[1]; } /** * 產生新的工作表名稱 * @returns {string} 工作表名稱 */ function generateSheetName() { const now = new Date(); const timestamp = now.toLocaleString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).replace(/[/\s:]/g, ''); return `Backlog_${timestamp}`; } /** * 建立新的工作表並寫入 backlog 資料 * @param {Object} sheets Google Sheets API 客戶端 * @param {string} spreadsheetId Spreadsheet ID * @param {Array} backlogItems Backlog 項目陣列 * @returns {Object} 建立結果 */ async function createBacklogSheet(sheets, spreadsheetId, backlogItems) { const sheetName = generateSheetName(); try { // 建立新工作表 await sheets.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, resource: { requests: [{ addSheet: { properties: { title: sheetName } } }] } }); // 準備表頭 const headers = [ 'Area', 'Iteration', 'BacklogLink', '標題', '工作類型', '功能描述', 'Acceptance Criteria', 'AssignedTo', 'Effort', 'Effort Analysis', '來源', 'MoSCoW', 'AI 建議' ]; // 準備資料行 const rows = [headers]; backlogItems.forEach(item => { const row = [ '', // Area - 留空給使用者填寫 '', // Iteration - 留空給使用者填寫 '', // BacklogLink - 留空給使用者填寫 item.title || '', item.type || '', item.description || '', Array.isArray(item.acceptance_criteria) ? item.acceptance_criteria.join('\n') : item.acceptance_criteria || '', '', // AssignedTo - 留空給使用者填寫 item.effort || '', item.effort_analysis || '', item.source_text || '', '', // MoSCoW - 留空給使用者填寫 item.ai_suggestions || '' // AI 建議 - 新增欄位 ]; rows.push(row); }); // 寫入資料到新工作表 await sheets.spreadsheets.values.update({ spreadsheetId: spreadsheetId, range: `${sheetName}!A1`, valueInputOption: 'RAW', resource: { values: rows } }); // 格式化表頭 await sheets.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, resource: { requests: [{ repeatCell: { range: { sheetId: await getSheetId(sheets, spreadsheetId, sheetName), startRowIndex: 0, endRowIndex: 1, startColumnIndex: 0, endColumnIndex: headers.length }, cell: { userEnteredFormat: { backgroundColor: { red: 0.2, green: 0.6, blue: 0.9 }, textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } } } }, fields: 'userEnteredFormat(backgroundColor,textFormat)' } }] } }); // 自動調整欄寬 await sheets.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, resource: { requests: [{ autoResizeDimensions: { dimensions: { sheetId: await getSheetId(sheets, spreadsheetId, sheetName), dimension: 'COLUMNS', startIndex: 0, endIndex: headers.length } } }] } }); // 設定特定欄位的寬度和自動換行 const sheetId = await getSheetId(sheets, spreadsheetId, sheetName); const columnWidthRequests = [ // Area - 欄位 0 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 0, endIndex: 1 }, properties: { pixelSize: 120 }, fields: 'pixelSize' } }, // Iteration - 欄位 1 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 1, endIndex: 2 }, properties: { pixelSize: 100 }, fields: 'pixelSize' } }, // BacklogLink - 欄位 2 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 2, endIndex: 3 }, properties: { pixelSize: 150 }, fields: 'pixelSize' } }, // Title - 欄位 3 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 3, endIndex: 4 }, properties: { pixelSize: 200 }, fields: 'pixelSize' } }, // Type - 欄位 4 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 4, endIndex: 5 }, properties: { pixelSize: 100 }, fields: 'pixelSize' } }, // Description - 欄位 5 (加寬) { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 5, endIndex: 6 }, properties: { pixelSize: 300 }, fields: 'pixelSize' } }, // Acceptance Criteria - 欄位 6 (加寬) { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 6, endIndex: 7 }, properties: { pixelSize: 300 }, fields: 'pixelSize' } }, // AssignedTo - 欄位 7 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 7, endIndex: 8 }, properties: { pixelSize: 120 }, fields: 'pixelSize' } }, // Effort - 欄位 8 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 8, endIndex: 9 }, properties: { pixelSize: 80 }, fields: 'pixelSize' } }, // Effort Analysis - 欄位 9 (加寬) { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 9, endIndex: 10 }, properties: { pixelSize: 250 }, fields: 'pixelSize' } }, // Source Text - 欄位 10 (加寬) { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 10, endIndex: 11 }, properties: { pixelSize: 250 }, fields: 'pixelSize' } }, // MoSCoW - 欄位 11 { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 11, endIndex: 12 }, properties: { pixelSize: 100 }, fields: 'pixelSize' } }, // AI 建議 - 欄位 12 (加寬) { updateDimensionProperties: { range: { sheetId: sheetId, dimension: 'COLUMNS', startIndex: 12, endIndex: 13 }, properties: { pixelSize: 300 }, fields: 'pixelSize' } } ]; // 執行欄寬設定 await sheets.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, resource: { requests: columnWidthRequests } }); // 設定自動換行(針對內容較長的欄位) await sheets.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, resource: { requests: [{ repeatCell: { range: { sheetId: sheetId, startRowIndex: 1, // 從第二行開始(跳過標題行) startColumnIndex: 5, // Description 欄位 endColumnIndex: 7 // 到 Acceptance Criteria 欄位 }, cell: { userEnteredFormat: { wrapStrategy: 'WRAP' } }, fields: 'userEnteredFormat.wrapStrategy' } }, { repeatCell: { range: { sheetId: sheetId, startRowIndex: 1, // 從第二行開始(跳過標題行) startColumnIndex: 9, // Effort Analysis 欄位 endColumnIndex: 11 // 到 Source Text 欄位 }, cell: { userEnteredFormat: { wrapStrategy: 'WRAP' } }, fields: 'userEnteredFormat.wrapStrategy' } }, { repeatCell: { range: { sheetId: sheetId, startRowIndex: 1, // 從第二行開始(跳過標題行) startColumnIndex: 12, // AI 建議欄位 endColumnIndex: 13 }, cell: { userEnteredFormat: { wrapStrategy: 'WRAP' } }, fields: 'userEnteredFormat.wrapStrategy' } }] } }); // 設定 MoSCoW 欄位的下拉選單 await sheets.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, resource: { requests: [{ setDataValidation: { range: { sheetId: sheetId, startRowIndex: 1, // 從第二行開始(跳過標題行) endRowIndex: 1000, // 設定足夠的行數 startColumnIndex: 11, // MoSCoW 欄位 endColumnIndex: 12 }, rule: { condition: { type: 'ONE_OF_LIST', values: [ { userEnteredValue: 'Must' }, { userEnteredValue: 'Should' }, { userEnteredValue: 'Could' }, { userEnteredValue: "Won't" } ] }, inputMessage: '請選擇優先級:Must (必須有)、Should (應該有)、Could (可以有)、Won\'t (這次不做)', showCustomUi: true, strict: true } } }] } }); return { sheetName, url: `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit#gid=${await getSheetId(sheets, spreadsheetId, sheetName)}`, rowsAdded: backlogItems.length }; } catch (error) { throw new Error(`建立 backlog 工作表失敗: ${error.message}`); } } /** * 取得工作表 ID * @param {Object} sheets Google Sheets API 客戶端 * @param {string} spreadsheetId Spreadsheet ID * @param {string} sheetName 工作表名稱 * @returns {number} Sheet ID */ async function getSheetId(sheets, spreadsheetId, sheetName) { const response = await sheets.spreadsheets.get({ spreadsheetId: spreadsheetId }); const sheet = response.data.sheets.find(s => s.properties.title === sheetName); return sheet ? sheet.properties.sheetId : 0; } /** * 將 backlog 項目寫入 Google Sheets * @param {Array} backlogItems Backlog 項目陣列 * @param {Object} config 配置物件 * @returns {Object} 寫入結果 */ async function writeBacklogToSheets(backlogItems, config) { if (!config.googleCredentials) { throw new Error(`請在配置中指定 Google 憑證檔案路徑 (googleCredentials) 🔧 如何取得 Google 憑證檔案: 1. 前往 Google Cloud Console: https://console.cloud.google.com/ 2. 建立新專案或選擇現有專案 3. 啟用 Google Sheets API 和 Google Drive API 4. 建立服務帳戶 (Service Account) 5. 下載 JSON 憑證檔案 6. 將服務帳戶的 email 加入到 Google Sheets 共享名單(編輯權限) 💡 或執行 ai-backlog --setup 進行互動式設定`); } if (!config.googleSheetUrl) { throw new Error('請在配置中指定 Google Sheets URL (googleSheetUrl)'); } const sheets = await createSheetsClient(config.googleCredentials); const spreadsheetId = extractSpreadsheetId(config.googleSheetUrl); const result = await createBacklogSheet(sheets, spreadsheetId, backlogItems); return result; } module.exports = { writeBacklogToSheets };