diff --git a/functions/generate_minutes/serverConfig.ts b/functions/generate_minutes/serverConfig.ts index 0fb6426..248cef1 100644 --- a/functions/generate_minutes/serverConfig.ts +++ b/functions/generate_minutes/serverConfig.ts @@ -1,10 +1,7 @@ -import { join } from "path"; export const GEMINI_MODEL_ID = "gemini-2.5-flash"; export const DEBUG = true; -export const CREDENTIALS_PATH = join(__dirname, process.env.SEARVICE_ACCOUNT_CREDENTIALS_FILE || ''); - export const CLOUD_STORAGE_MASTER_FOLDER_NAME = "master"; export const CLOUD_STORAGE_LOG_FOLDER_NAME = "request_logs"; export const COMPANIES_FILE_NAME = "companies.json"; diff --git a/functions/generate_minutes/src/apiRouter.ts b/functions/generate_minutes/src/apiRouter.ts index ad06446..0c04172 100644 --- a/functions/generate_minutes/src/apiRouter.ts +++ b/functions/generate_minutes/src/apiRouter.ts @@ -3,7 +3,7 @@ import zlib from "zlib"; import { storageController } from "./logics/storage"; import { MiiTelWebhookSchema, processRequest } from "./logics/process"; import { hubspotController } from "./logics/hubspot"; -import { createCustomError } from "./logics/error"; +import { createCustomError, responseError } from "./logics/error"; import { CLOUD_STORAGE_LOG_FOLDER_NAME, CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME, OWNERS_FILE_NAME } from "../serverConfig"; const router = express.Router(); @@ -20,14 +20,15 @@ router.post("/miitel", async (req, res) => { await storageController.saveToGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, `${videoInfo.id}.json.gz`, gzipped, 'application/gzip'); await processRequest(videoInfo); + // if(!result) throw res.status(200).send("ok"); } catch(err) { - res.status(400).send("Invalid webhook body"); + responseError(err, res) } }); -// Update Master Data And Check Google Drive Folder +// Refresh Master Data Everyday router.post("/dailyBatch", async (req, res) => { try { console.log("Starting daily batch process..."); @@ -41,7 +42,6 @@ router.post("/dailyBatch", async (req, res) => { if(!owners) throw createCustomError("GET_COMPANIES_FAILED"); await storageController.saveToGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME, JSON.stringify(owners), 'application/json'); - // check folders in Google Drive res.status(200).send("Daily batch executed."); } catch (error) { @@ -61,6 +61,29 @@ router.post("/getLog", async (req, res) => { }); +// Check Log By Meeting ID +router.post("/reExecute", async (req, res) => { + try { + console.log(req.body); + const meetingId = req.body.meetingId; + const newTitle = req.body.newTitle; + const log = await storageController.loadFromGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, `${meetingId}.json.gz`); + if(!log) throw Error(); + const params = MiiTelWebhookSchema.safeParse(JSON.parse(log)); + if(!params.success) throw createCustomError("ZOD_FAILED"); + params.data.video.title = newTitle; + // console.log(params.data.video) + + await processRequest(params.data.video); + + res.send(log); + } catch(error) { + console.log("===== Route Log =====") + console.log(error); + res.status(400).send("Failed"); + } +}); + // router.post("/deleteFile", async (req, res) => { // console.log(req.body); diff --git a/functions/generate_minutes/src/logics/file.ts b/functions/generate_minutes/src/logics/file.ts index 39411db..bced88b 100644 --- a/functions/generate_minutes/src/logics/file.ts +++ b/functions/generate_minutes/src/logics/file.ts @@ -24,30 +24,32 @@ export const fileController = { minutesContent += minutes; return minutesContent; }, - createZip: async (body: any, outputPath: string, fileName: string) => { - console.log(outputPath); - await new Promise((resolve, reject) => { - const output = fs.createWriteStream(outputPath); - const archive = archiver('zip', { - zlib: { level: 9 } - }); + createZip: async (body: any, outputPath: string, fileName: string): Promise => { + try { + await new Promise((resolve, reject) => { + const output = fs.createWriteStream(outputPath); + const archive = archiver('zip', { + zlib: { level: 9 } + }); - output.on('close', () => { - console.log(archive.pointer() + ' total bytes'); - console.log('archiver has been finalized and the output file descriptor has closed.'); - resolve(true); - }); + output.on('close', () => { + // console.log(archive.pointer() + ' total bytes'); + // console.log('archiver has been finalized and the output file descriptor has closed.'); + resolve(true); + }); - archive.on('error', (err) => { - reject(err); - }); + archive.on('error', (err) => { + reject(err); + }); - archive.pipe(output); - archive.append(JSON.stringify(body), { name: fileName + '.json' }); - archive.finalize(); - }) - console.log("ZIP created"); - return; + archive.pipe(output); + archive.append(JSON.stringify(body), { name: fileName + '.json' }); + archive.finalize(); + }) + return true; + } catch(error) { + return false; + } }, }; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/googleDrive.ts b/functions/generate_minutes/src/logics/googleDrive.ts index 50bd69c..a8ede92 100644 --- a/functions/generate_minutes/src/logics/googleDrive.ts +++ b/functions/generate_minutes/src/logics/googleDrive.ts @@ -1,8 +1,10 @@ import { docs_v1, drive_v3, google, sheets_v4 } from "googleapis"; import fs from "fs"; -import { CREDENTIALS_PATH, DEBUG, FOLDER_MIMETYPE, LOG_SHEET_HEADER_VALUES, SHEET_MIMETYPE } from "../../serverConfig"; +import { DEBUG, LOG_SHEET_HEADER_VALUES, SHEET_MIMETYPE } from "../../serverConfig"; import z from "zod"; +const GOOGLE_DRIVE_FOLDER_ID = process.env.GOOGLE_DRIVE_FOLDER_ID; + const SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"] const MAX_RETRY = 3; @@ -23,7 +25,6 @@ export const googleDriveController = { getAuth: async (): Promise => { try { const credentials = JSON.parse(process.env.SEARVICE_ACCOUNT_CREDENTIALS || "{}"); - console.log(credentials) const auth = await new google.auth.GoogleAuth({ credentials: credentials, scopes: SCOPES, @@ -49,20 +50,20 @@ export const googleDriveController = { return docs; }, - uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string): Promise => { + uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string, contentType: string): Promise => { try { - console.log("Uploading file to Google Drive:", filePath); + // console.log("Uploading file to Google Drive:", filePath); const response = await driveClient.files.create({ requestBody: { name: fileName, parents: [folderId], }, media: { - mimeType: "application/zip", + mimeType: contentType, body: fs.createReadStream(filePath), }, }); - console.log("File uploaded, Id:", response.data.id); + // console.log("File uploaded, Id:", response.data.id); fs.unlinkSync(filePath); return response.data.id; } catch (error) { @@ -71,25 +72,12 @@ export const googleDriveController = { return null; } }, - getFolderId: async (driveClient: drive_v3.Drive, folderId: string, fileName: string): Promise => { - try { - const existsFolderId = await googleDriveController.searchFileIdByFileName(driveClient, folderId, fileName); - if(existsFolderId) return existsFolderId; - console.log('=== Create New Folder ===') - const newFolderId = googleDriveController.createNewFile(driveClient, folderId, fileName, FOLDER_MIMETYPE); - if(!newFolderId) return null; - return newFolderId; - } catch (error) { - console.error('Error searching files:', error); - return null; - } - }, searchFileIdByFileName: async (driveClient: drive_v3.Drive, folderId: string, fileName: string): Promise => { try { const params = googleDriveController.getSearchFileParamsByDebugMode(folderId); const res = await driveClient.files.list(params); - console.log("Files:"); - console.log(res.data.files); + // console.log("Files:"); + // console.log(res.data.files); if(!res.data.files) return null; for(const file of res.data.files) { @@ -118,7 +106,7 @@ export const googleDriveController = { } return { corpora: 'drive', - driveId: process.env.GOOGLE_DRIVE_FOLDER_ID, + driveId: GOOGLE_DRIVE_FOLDER_ID, q: `'${folderId}' in parents`, pageSize: 10, fields: "files(id, name)", diff --git a/functions/generate_minutes/src/logics/process.ts b/functions/generate_minutes/src/logics/process.ts index 1b5764e..7aef059 100644 --- a/functions/generate_minutes/src/logics/process.ts +++ b/functions/generate_minutes/src/logics/process.ts @@ -5,9 +5,9 @@ import { googleDriveController, LogRowData, LogRowDataSchema } from "./googleDri import { fileController } from "./file"; import path, { join } from "path"; import fs from "fs"; -import { createCustomError, responseError } from "./error"; +import { createCustomError } from "./error"; import { storageController } from "./storage"; -import { CLOUD_STORAGE_MASTER_FOLDER_NAME, DATE_FORMAT, DATETIME_FORMAT, DOCUMENT_MIMETYPE, OWNERS_FILE_NAME, Y_FORMAT, YM_FORMAT } from "../../serverConfig"; +import { CLOUD_STORAGE_MASTER_FOLDER_NAME, DATE_FORMAT, DATETIME_FORMAT, DOCUMENT_MIMETYPE, OWNERS_FILE_NAME, YM_FORMAT } from "../../serverConfig"; import { hubspotController, OwnerSchema } from "./hubspot"; import { fuzzyMatchController } from "./fuzzyMatch"; @@ -32,8 +32,6 @@ export const MiiTelWebhookSchema = z.object({ video: VideoInfoSchema, }); -// export type MiiTelWebhook = z.infer; - const GOOGLE_DRIVE_FOLDER_ID = process.env.GOOGLE_DRIVE_FOLDER_ID || ''; const MIITEL_REQUEST_LOG_FOLDER_ID = process.env.MIITEL_REQUEST_LOG_FOLDER_ID || ''; const MINUTES_CREATION_HISTORY_FOLDER_ID = process.env.MINUTES_CREATION_HISTORY_FOLDER_ID || ''; @@ -43,6 +41,8 @@ const HUBSPOT_COMPANY_URL = process.env.HUBSPOT_COMPANY_URL || ''; const FILE_PATH = join(__dirname, "../files/"); +let outputPath = ''; + export const processRequest = async (videoInfo: VideoInfo) => { try { const videoId = videoInfo.id; @@ -50,36 +50,35 @@ export const processRequest = async (videoInfo: VideoInfo) => { const startsAt = videoInfo.starts_at; const endsAt = videoInfo.ends_at; const accessPermission = videoInfo.access_permission; - const hostId = videoInfo.host.login_id; const hostName = videoInfo.host.user_name; - const speechRecognition = videoInfo.speech_recognition.raw; + if (accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return; + + // ===== Init ===== + const googleAuth = await googleDriveController.getAuth(); + const driveClient = googleDriveController.getDriveClient(googleAuth); + const docsClient = googleDriveController.getDocsClient(googleAuth); + const sheetsClient = googleDriveController.getSheetsClient(googleAuth); + const jstStartsAt = dateController.convertToJst(startsAt); const jstEndsAt = dateController.convertToJst(endsAt); const fileName = fileController.createMinutesFileName(title, hostName, jstStartsAt); const videoUrl = `${MIITEL_URL}app/video/${videoId}`; - if (accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return; - - // - const googleAuth = await googleDriveController.getAuth(); - const driveClient = googleDriveController.getDriveClient(googleAuth); - const docsClient = googleDriveController.getDocsClient(googleAuth); - const sheetsClient = googleDriveController.getSheetsClient(googleAuth); // ===== Save Request Log to Google Drive ===== if (!fs.existsSync(FILE_PATH)) fs.mkdirSync(FILE_PATH, { recursive: true }); - const outputPath = path.join(FILE_PATH, fileName + '.zip'); - await fileController.createZip(videoInfo, outputPath, fileName); - - const logFileId = await googleDriveController.uploadFile(driveClient, outputPath, MIITEL_REQUEST_LOG_FOLDER_ID, fileName + '.zip'); + outputPath = path.join(FILE_PATH, fileName + '.zip'); + const createZip = await fileController.createZip(videoInfo, outputPath, fileName); + if(!createZip) throw createCustomError("CREATE_ZIP_FILE_FAILED"); + + const logFileId = await googleDriveController.uploadFile(driveClient, outputPath, MIITEL_REQUEST_LOG_FOLDER_ID, `${fileName}.zip`, "application/zip"); if(!logFileId) throw createCustomError("UPLOAD_LOG_FAILED"); // ===== Generate Minutes ===== const minutes = await aiController.generateMinutes(speechRecognition); - console.log(minutes); if (!minutes) throw createCustomError("AI_GENERATION_FAILED"); let content = `会議履歴URL:${videoUrl}\n`; content += `担当者:${hostName}\n\n`; @@ -87,11 +86,12 @@ export const processRequest = async (videoInfo: VideoInfo) => { // ===== Upload To Google Drive ===== - const documentId = await googleDriveController.createNewFile(driveClient, GOOGLE_DRIVE_FOLDER_ID, title, DOCUMENT_MIMETYPE); - if (!documentId) throw createCustomError("UPLOAD_MINUTES_FAILED"); - const result = await googleDriveController.addContentToDocs(docsClient, documentId, minutes); + const documentId = await googleDriveController.createNewFile(driveClient, GOOGLE_DRIVE_FOLDER_ID, fileName, DOCUMENT_MIMETYPE); + if (!documentId) throw createCustomError("CREATE_NEW_DOCUMENT_FAILED"); + const result = await googleDriveController.addContentToDocs(docsClient, documentId, content); if(!result) throw createCustomError("UPLOAD_MINUTES_FAILED"); + // ===== Create Meeting Log at Hubspot ===== const ownersJson = await storageController.loadJsonFromGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME); if(!ownersJson) throw createCustomError("GET_OWNERS_FAILED"); @@ -99,18 +99,14 @@ export const processRequest = async (videoInfo: VideoInfo) => { if(!parsedOwners.success) throw createCustomError("ZOD_FAILED"); const ownerId = hubspotController.searchOwnerIdByEmail(hostId, parsedOwners.data); - - const companyName = fileController.extractCompanyNameFromTitle(title); - const matchedCompany = await fuzzyMatchController.searchMatchedCompany(companyName); + const extractedCompanyName = fileController.extractCompanyNameFromTitle(title); + const matchedCompany = await fuzzyMatchController.searchMatchedCompany(extractedCompanyName); if(matchedCompany) await hubspotController.createMeetingLog(matchedCompany.id, title, ownerId, minutes, startsAt, endsAt); - // ===== Apeend Log To SpreadSheet ===== - const currentYear = dateController.getCurrentJstTime(Y_FORMAT); - const yearFileId = await googleDriveController.getFolderId(driveClient, MINUTES_CREATION_HISTORY_FOLDER_ID, currentYear); - if(!yearFileId) throw createCustomError("GET_FOLDER_ID_FAILED"); + // ===== Apeend Log To SpreadSheet ===== const currentYearMonth = dateController.getCurrentJstTime(YM_FORMAT); - const sheetId = await googleDriveController.getLogSheetId(driveClient, sheetsClient, yearFileId, currentYearMonth); + const sheetId = await googleDriveController.getLogSheetId(driveClient, sheetsClient, MINUTES_CREATION_HISTORY_FOLDER_ID, currentYearMonth); if(!sheetId) throw createCustomError("GET_SHEET_ID_FAILED"); const currentJstDateTimeStr = dateController.getCurrentJstTime(DATETIME_FORMAT); @@ -125,10 +121,11 @@ export const processRequest = async (videoInfo: VideoInfo) => { documentUrl: `https://docs.google.com/document/d/${documentId}/edit`, hubspotUrl: matchedCompany ? `${HUBSPOT_COMPANY_URL}/${matchedCompany.id}` : '', }); - await googleDriveController.insertRowToSheet(sheetsClient, sheetId, Object.values(rowData)); - return; - } catch (error) { - responseError(error); - return; + const insertResult = await googleDriveController.insertRowToSheet(sheetsClient, sheetId, Object.values(rowData)); + if(!insertResult) throw createCustomError("INSERT_ROW_FAILED"); + fs.unlinkSync(outputPath); + } catch (error) { + fs.unlinkSync(outputPath); + throw error; } }; \ No newline at end of file diff --git a/functions/generate_minutes/src/stores/errorCodes.ts b/functions/generate_minutes/src/stores/errorCodes.ts index 3533169..3ae530f 100644 --- a/functions/generate_minutes/src/stores/errorCodes.ts +++ b/functions/generate_minutes/src/stores/errorCodes.ts @@ -9,18 +9,17 @@ export const ERROR_DEFINITIONS = { AI_GENERATION_FAILED: { code: "E2001", message: "AIによる議事録生成に失敗しました", statusCode: 500 }, // 議事録(Google Docs)の作成/アップロード失敗 - UPLOAD_MINUTES_FAILED: { code: "E3002", message: "議事録のアップロードに失敗しました", statusCode: 500 }, + CREATE_NEW_DOCUMENT_FAILED: { code: "E3002", message: "ドキュメント作成に失敗しました", statusCode: 500 }, + UPLOAD_MINUTES_FAILED: { code: "E3003", message: "議事録のアップロードに失敗しました", statusCode: 500 }, // オーナー情報の取得失敗 - GET_OWNERS_FAILED: { code: "E3003", message: "オーナー情報の取得に失敗しました", statusCode: 500 }, - GET_COMPANIES_FAILED: { code: "E3004", message: "会社情報の取得に失敗しました", statusCode: 500 }, + GET_OWNERS_FAILED: { code: "E3004", message: "オーナー情報の取得に失敗しました", statusCode: 500 }, + GET_COMPANIES_FAILED: { code: "E3005", message: "会社情報の取得に失敗しました", statusCode: 500 }, - // 議事録作成履歴スプレッドシートの取得失敗 - GET_MINUTES_HISTORY_FAILED: { code: "E3005", message: "議事録作成履歴の取得に失敗しました", statusCode: 500 }, - - - GET_FOLDER_ID_FAILED: { code: "E3006", message: "フォルダID取得に失敗しました", statusCode: 500 }, - GET_SHEET_ID_FAILED: { code: "E3007", message: "スプレッドシートID取得に失敗しました", statusCode: 500 }, + GET_FOLDER_ID_FAILED: { code: "E3007", message: "フォルダID取得に失敗しました", statusCode: 500 }, + GET_SHEET_ID_FAILED: { code: "E3008", message: "スプレッドシートID取得に失敗しました", statusCode: 500 }, + CREATE_ZIP_FILE_FAILED: { code: "E3009", message: "ZIPファイルの作成に失敗しました", statusCode: 500 }, + INSERT_ROW_FAILED: { code: "E3009", message: "シートへのデータ追加に失敗しました", statusCode: 500 }, } as const; export type ErrorKey = keyof typeof ERROR_DEFINITIONS;