cw-ai-backlog
Version:
AI-powered tool to generate backlog items from meeting documents and output to Google Sheets
233 lines (199 loc) • 6.72 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const pdfParse = require('pdf-parse');
const mammoth = require('mammoth');
const axios = require('axios');
const { google } = require('googleapis');
/**
* 解析檔案內容
* @param {string} input 檔案路徑或 Google Docs 連結
* @param {Object} config 配置物件
* @returns {string} 解析後的文字內容
*/
async function parseFile(input, config) {
// 判斷是 Google Docs 連結還是本地檔案
if (input.includes('docs.google.com')) {
return await parseGoogleDocs(input, config);
} else {
return await parseLocalFile(input);
}
}
/**
* 解析 Google Docs
* @param {string} url Google Docs URL
* @param {Object} config 配置物件
* @returns {string} 文字內容
*/
async function parseGoogleDocs(url, config) {
try {
// 提取文件 ID
const docId = extractGoogleDocId(url);
if (config.googleCredentials) {
// 使用 Google Docs API
return await parseWithGoogleAPI(docId, config.googleCredentials);
} else {
// 嘗試使用公開存取
return await parseGoogleDocsPublic(docId);
}
} catch (error) {
if (error.message.includes('Google Docs API has not been used')) {
throw new Error(`Google Docs API 未啟用。請按照以下步驟操作:
1. 前往 Google Cloud Console:
https://console.developers.google.com/apis/api/docs.googleapis.com/overview?project=${error.message.match(/project (\d+)/)?.[1] || 'YOUR_PROJECT_ID'}
2. 點擊「啟用」按鈕來啟用 Google Docs API
3. 確保您的 service account 有權限存取該文件:
- 在 Google Docs 中點擊「分享」
- 新增您的 service account email
- 設定權限為「檢視者」或「編輯者」
4. 重新執行指令`);
}
throw new Error(`無法解析 Google Docs: ${error.message}
💡 解決方案:
1. 如果是私人文件,請設定 googleCredentials 或將文件設為公開
2. 如果是公開文件,請確認文件設為「知道連結的使用者可以檢視」
3. 執行 ai-backlog --setup 進行完整設定`);
}
}
/**
* 從 Google Docs URL 提取文件 ID
* @param {string} url Google Docs URL
* @returns {string} 文件 ID
*/
function extractGoogleDocId(url) {
const match = url.match(/\/document\/d\/([a-zA-Z0-9-_]+)/);
if (!match) {
throw new Error('無效的 Google Docs URL');
}
return match[1];
}
/**
* 使用 Google Docs API 解析文件
* @param {string} docId 文件 ID
* @param {string} credentialsPath 憑證檔案路徑
* @returns {string} 文字內容
*/
async function parseWithGoogleAPI(docId, credentialsPath) {
try {
const credentials = require(path.resolve(credentialsPath));
const auth = new google.auth.GoogleAuth({
credentials,
scopes: [
'https://www.googleapis.com/auth/documents.readonly',
'https://www.googleapis.com/auth/drive.readonly'
]
});
const docs = google.docs({ version: 'v1', auth });
const doc = await docs.documents.get({ documentId: docId });
return extractTextFromGoogleDoc(doc.data);
} catch (error) {
if (error.message.includes('Google Docs API has not been used')) {
throw new Error(`❌ Google Docs API 未啟用
🔧 解決步驟:
1. 前往 Google Cloud Console 啟用 Google Docs API:
https://console.developers.google.com/apis/api/docs.googleapis.com/overview?project=${error.message.match(/project (\d+)/)?.[1] || 'YOUR_PROJECT_ID'}
2. 點擊「啟用」按鈕
3. 等待幾分鐘讓 API 生效,然後重試`);
}
if (error.message.includes('insufficient authentication scopes')) {
throw new Error(`❌ Service Account 權限不足
🔧 解決步驟:
1. 確保您的 Service Account 有以下權限:
- Google Docs API 存取權限
- Google Drive API 存取權限
2. 在 Google Docs 中分享文件給您的 Service Account:
${credentials.client_email || 'your-service-account@project.iam.gserviceaccount.com'}
3. 設定權限為「檢視者」或「編輯者」`);
}
if (error.code === 404) {
throw new Error(`❌ 找不到文件或無權限存取
🔧 可能原因:
1. 文件 ID 不正確
2. Service Account 沒有存取文件的權限
3. 文件已被刪除或移動
💡 解決方法:
1. 確認 Google Docs URL 正確
2. 在文件中點擊「分享」→ 新增:${credentials.client_email || 'your-service-account@project.iam.gserviceaccount.com'}
3. 設定權限為「檢視者」或「編輯者」`);
}
throw error;
}
}
/**
* 嘗試解析公開的 Google Docs
* @param {string} docId 文件 ID
* @returns {string} 文字內容
*/
async function parseGoogleDocsPublic(docId) {
try {
// 嘗試使用公開的純文字匯出 URL
const exportUrl = `https://docs.google.com/document/d/${docId}/export?format=txt`;
const response = await axios.get(exportUrl);
return response.data;
} catch (error) {
throw new Error('無法存取 Google Docs,請確認文件為公開或提供 API 憑證');
}
}
/**
* 從 Google Docs API 回應中提取文字
* @param {Object} docData Google Docs API 回應
* @returns {string} 文字內容
*/
function extractTextFromGoogleDoc(docData) {
let text = '';
if (docData.body && docData.body.content) {
for (const element of docData.body.content) {
if (element.paragraph) {
for (const paragraphElement of element.paragraph.elements) {
if (paragraphElement.textRun) {
text += paragraphElement.textRun.content;
}
}
}
}
}
return text;
}
/**
* 解析本地檔案
* @param {string} filePath 檔案路徑
* @returns {string} 文字內容
*/
async function parseLocalFile(filePath) {
if (!await fs.pathExists(filePath)) {
throw new Error(`檔案不存在: ${filePath}`);
}
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case '.pdf':
return await parsePDF(filePath);
case '.docx':
return await parseDocx(filePath);
case '.txt':
case '.md':
return await fs.readFile(filePath, 'utf-8');
default:
throw new Error(`不支援的檔案格式: ${ext}`);
}
}
/**
* 解析 PDF 檔案
* @param {string} filePath PDF 檔案路徑
* @returns {string} 文字內容
*/
async function parsePDF(filePath) {
const dataBuffer = await fs.readFile(filePath);
const data = await pdfParse(dataBuffer);
return data.text;
}
/**
* 解析 Word 檔案
* @param {string} filePath Word 檔案路徑
* @returns {string} 文字內容
*/
async function parseDocx(filePath) {
const result = await mammoth.extractRawText({ path: filePath });
return result.value;
}
module.exports = {
parseFile
};