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
JavaScript
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
};