diff --git a/functions/generate_minutes/package.json b/functions/generate_minutes/package.json index 09b0bba..beeb1e8 100644 --- a/functions/generate_minutes/package.json +++ b/functions/generate_minutes/package.json @@ -23,6 +23,7 @@ "@google/genai": "^1.30.0", "@hubspot/api-client": "^13.4.0", "archiver": "^7.0.1", + "cerceis-lib": "^2.5.0", "concurrently": "^9.2.1", "dotenv": "^17.2.3", "express": "^4.21.2", diff --git a/functions/generate_minutes/serverConfig.ts b/functions/generate_minutes/serverConfig.ts index 248cef1..f547d2f 100644 --- a/functions/generate_minutes/serverConfig.ts +++ b/functions/generate_minutes/serverConfig.ts @@ -3,7 +3,7 @@ export const GEMINI_MODEL_ID = "gemini-2.5-flash"; export const DEBUG = true; export const CLOUD_STORAGE_MASTER_FOLDER_NAME = "master"; -export const CLOUD_STORAGE_LOG_FOLDER_NAME = "request_logs"; +export const CLOUD_STORAGE_LOG_FOLDER_NAME = "new_request_log"; export const COMPANIES_FILE_NAME = "companies.json"; export const OWNERS_FILE_NAME = "owners.json"; @@ -18,4 +18,7 @@ export const FOLDER_MIMETYPE = 'application/vnd.google-apps.folder'; export const DOCUMENT_MIMETYPE = 'application/vnd.google-apps.document'; export const SHEET_MIMETYPE = 'application/vnd.google-apps.spreadsheet'; -export const LOG_SHEET_HEADER_VALUES = ["タイムスタンプ","商談日", "タイトル", "登録先企業","担当者", "ミーティングURL", "議事録URL", "HubSpot会社概要URL"] \ No newline at end of file +export const LOG_SHEET_HEADER_VALUES = ["タイムスタンプ","商談日", "タイトル", "登録先企業","担当者", "ミーティングURL", "議事録URL", "HubSpot会社概要URL"] + +export const MAX_RETRY_COUNT = 3; +export const ROOP_DELAY_MS = 5000; \ No newline at end of file diff --git a/functions/generate_minutes/src/apiRouter.ts b/functions/generate_minutes/src/apiRouter.ts index 0c04172..6077066 100644 --- a/functions/generate_minutes/src/apiRouter.ts +++ b/functions/generate_minutes/src/apiRouter.ts @@ -5,6 +5,7 @@ import { MiiTelWebhookSchema, processRequest } from "./logics/process"; import { hubspotController } from "./logics/hubspot"; 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"; +import { Delay } from "cerceis-lib"; const router = express.Router(); @@ -78,13 +79,36 @@ router.post("/reExecute", async (req, res) => { res.send(log); } catch(error) { - console.log("===== Route Log =====") console.log(error); res.status(400).send("Failed"); } }); +// 過去のログを全てGoogle Driveへアップロード +router.post("/logUpload", async (req, res) => { + try { + const list = await storageController.getFileList(); + if(!list) throw createCustomError("GET_FILES_FAILED"); + for(const l of list){ + console.log(l); + const fileName = l.split('/')[1] + const log = await storageController.loadFromGCS('request_log', fileName); + if(!log) throw createCustomError("GET_FILES_FAILED"); + // console.log(log); + const parsedLog = MiiTelWebhookSchema.safeParse(JSON.parse(log)); + if(!parsedLog.success) throw createCustomError("ZOD_FAILED"); + console.log(parsedLog.data.video.title); + + await Delay(500); + } + res.send('ok'); + } catch(error) { + console.log(error); + res.status(400).send("Failed"); + } +}); + // router.post("/deleteFile", async (req, res) => { // console.log(req.body); // const fileId = req.body.fileId; diff --git a/functions/generate_minutes/src/logics/ai.ts b/functions/generate_minutes/src/logics/ai.ts index ee37ec6..5ba41ad 100644 --- a/functions/generate_minutes/src/logics/ai.ts +++ b/functions/generate_minutes/src/logics/ai.ts @@ -6,7 +6,7 @@ const aiClient = new GoogleGenAI({ }); export const aiController = { - generateMinutes: async(text: string) => { + generateMinutes: async(text: string): Promise => { const prompt = ` あなたは議事録作成のプロフェッショナルです。以下の「文字起こし結果」は営業マンが録音した商談の文字起こしです。以下の制約条件に従い、最高の商談報告の議事録を作成してください。 @@ -28,6 +28,7 @@ export const aiController = { model: process.env.GEMINI_MODEL_ID || "gemini-2.5-flash", contents: prompt, }) + if(!response.text) return null; console.log("AI Response:", response.text); return response.text; } catch (error) { diff --git a/functions/generate_minutes/src/logics/error.ts b/functions/generate_minutes/src/logics/error.ts index 0a742b2..0983399 100644 --- a/functions/generate_minutes/src/logics/error.ts +++ b/functions/generate_minutes/src/logics/error.ts @@ -1,6 +1,8 @@ import { Response } from "express"; import z from "zod"; import { ERROR_DEFINITIONS, ErrorKey } from "../stores/errorCodes"; +import { Delay } from "cerceis-lib"; +import { MAX_RETRY_COUNT, ROOP_DELAY_MS } from "../../serverConfig"; const CustomErrorSchema = z.object({ code: z.string(), @@ -27,3 +29,18 @@ export const responseError = (error: any, res: Response | null = null) => { if(res) return res.status(parsedError.statusCode).send(parsedError.message); } + +export const callFunctionWithRetry = async (fn: () => Promise): Promise => { + for(let retryCount = 0; retryCount <= MAX_RETRY_COUNT; retryCount++) { + try { + const result = await fn(); + if(!result) throw Error(); + return result; + } catch(error) { + if(retryCount === MAX_RETRY_COUNT) return null; + console.warn(`\n\n========== リトライ${retryCount + 1}回目 ==========\n\n`); + await Delay(ROOP_DELAY_MS); + } + } + return null; +}; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/file.ts b/functions/generate_minutes/src/logics/file.ts index bced88b..42837f0 100644 --- a/functions/generate_minutes/src/logics/file.ts +++ b/functions/generate_minutes/src/logics/file.ts @@ -1,8 +1,5 @@ -import { create } from "domain"; import { dateController } from "./date"; -import path, { join } from "path"; import archiver from "archiver"; -import { googleDriveController } from "./googleDrive"; import fs from "fs"; diff --git a/functions/generate_minutes/src/logics/fuzzyMatch.ts b/functions/generate_minutes/src/logics/fuzzyMatch.ts index 211b172..3d90584 100644 --- a/functions/generate_minutes/src/logics/fuzzyMatch.ts +++ b/functions/generate_minutes/src/logics/fuzzyMatch.ts @@ -3,12 +3,13 @@ import { storageController } from "./storage"; import { CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME, LEGAL_SUFFIX } from "../../serverConfig"; import { Company, CompanySchema } from "./hubspot"; import z from "zod"; +import { callFunctionWithRetry } from "./error"; export const fuzzyMatchController = { searchMatchedCompany: async(companyName: string): Promise => { try { - const companiesJson = await storageController.loadJsonFromGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME); + const companiesJson = await callFunctionWithRetry(() => storageController.loadJsonFromGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME)); if(!companiesJson) return null; const parsedCompanies = z.array(CompanySchema).safeParse(JSON.parse(companiesJson)); if(!parsedCompanies.success) return null; diff --git a/functions/generate_minutes/src/logics/googleDrive.ts b/functions/generate_minutes/src/logics/googleDrive.ts index a8ede92..509b3bd 100644 --- a/functions/generate_minutes/src/logics/googleDrive.ts +++ b/functions/generate_minutes/src/logics/googleDrive.ts @@ -50,7 +50,7 @@ export const googleDriveController = { return docs; }, - uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string, contentType: 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); const response = await driveClient.files.create({ @@ -63,12 +63,10 @@ export const googleDriveController = { body: fs.createReadStream(filePath), }, }); - // console.log("File uploaded, Id:", response.data.id); - fs.unlinkSync(filePath); + if(!response.data.id) return null; return response.data.id; } catch (error) { console.error("Error uploading file:", error); - fs.unlinkSync(filePath); return null; } }, @@ -179,7 +177,7 @@ export const googleDriveController = { try { const existsSheetId = await googleDriveController.searchFileIdByFileName(driveClient, folderId, fileName); if(existsSheetId) return existsSheetId; - console.log('=== Create New Sheet ===') + // console.log('=== Create New Sheet ===') const newSheetId = await googleDriveController.createNewFile(driveClient, folderId, fileName, SHEET_MIMETYPE); if(!newSheetId) return null; // diff --git a/functions/generate_minutes/src/logics/process.ts b/functions/generate_minutes/src/logics/process.ts index 7aef059..0e56103 100644 --- a/functions/generate_minutes/src/logics/process.ts +++ b/functions/generate_minutes/src/logics/process.ts @@ -5,7 +5,7 @@ import { googleDriveController, LogRowData, LogRowDataSchema } from "./googleDri import { fileController } from "./file"; import path, { join } from "path"; import fs from "fs"; -import { createCustomError } from "./error"; +import { callFunctionWithRetry, createCustomError } from "./error"; import { storageController } from "./storage"; import { CLOUD_STORAGE_MASTER_FOLDER_NAME, DATE_FORMAT, DATETIME_FORMAT, DOCUMENT_MIMETYPE, OWNERS_FILE_NAME, YM_FORMAT } from "../../serverConfig"; import { hubspotController, OwnerSchema } from "./hubspot"; @@ -74,11 +74,11 @@ export const processRequest = async (videoInfo: VideoInfo) => { 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"); + const logFileId = await callFunctionWithRetry(() => 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); + const minutes = await callFunctionWithRetry(() => aiController.generateMinutes(speechRecognition)); if (!minutes) throw createCustomError("AI_GENERATION_FAILED"); let content = `会議履歴URL:${videoUrl}\n`; content += `担当者:${hostName}\n\n`; @@ -86,14 +86,14 @@ export const processRequest = async (videoInfo: VideoInfo) => { // ===== Upload To Google Drive ===== - const documentId = await googleDriveController.createNewFile(driveClient, GOOGLE_DRIVE_FOLDER_ID, fileName, DOCUMENT_MIMETYPE); + const documentId = await callFunctionWithRetry(() => 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"); + const addContentResult = await callFunctionWithRetry(() => googleDriveController.addContentToDocs(docsClient, documentId, content)); + if(!addContentResult) throw createCustomError("UPLOAD_MINUTES_FAILED"); // ===== Create Meeting Log at Hubspot ===== - const ownersJson = await storageController.loadJsonFromGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME); + const ownersJson = await callFunctionWithRetry(() => storageController.loadJsonFromGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME)); if(!ownersJson) throw createCustomError("GET_OWNERS_FAILED"); const parsedOwners = z.array(OwnerSchema).safeParse(JSON.parse(ownersJson)); if(!parsedOwners.success) throw createCustomError("ZOD_FAILED"); @@ -101,12 +101,15 @@ export const processRequest = async (videoInfo: VideoInfo) => { const extractedCompanyName = fileController.extractCompanyNameFromTitle(title); const matchedCompany = await fuzzyMatchController.searchMatchedCompany(extractedCompanyName); - if(matchedCompany) await hubspotController.createMeetingLog(matchedCompany.id, title, ownerId, minutes, startsAt, endsAt); + if(matchedCompany) { + const createLogResult = await callFunctionWithRetry(() => hubspotController.createMeetingLog(matchedCompany.id, title, ownerId, minutes, startsAt, endsAt)); + if(!createLogResult) throw createCustomError("CREATE_MEETING_LOG_FAILED"); + } // ===== Apeend Log To SpreadSheet ===== const currentYearMonth = dateController.getCurrentJstTime(YM_FORMAT); - const sheetId = await googleDriveController.getLogSheetId(driveClient, sheetsClient, MINUTES_CREATION_HISTORY_FOLDER_ID, currentYearMonth); + const sheetId = await callFunctionWithRetry(() => googleDriveController.getLogSheetId(driveClient, sheetsClient, MINUTES_CREATION_HISTORY_FOLDER_ID, currentYearMonth)); if(!sheetId) throw createCustomError("GET_SHEET_ID_FAILED"); const currentJstDateTimeStr = dateController.getCurrentJstTime(DATETIME_FORMAT); @@ -121,11 +124,49 @@ export const processRequest = async (videoInfo: VideoInfo) => { documentUrl: `https://docs.google.com/document/d/${documentId}/edit`, hubspotUrl: matchedCompany ? `${HUBSPOT_COMPANY_URL}/${matchedCompany.id}` : '', }); - const insertResult = await googleDriveController.insertRowToSheet(sheetsClient, sheetId, Object.values(rowData)); + const insertResult = await callFunctionWithRetry(() => googleDriveController.insertRowToSheet(sheetsClient, sheetId, Object.values(rowData))); if(!insertResult) throw createCustomError("INSERT_ROW_FAILED"); fs.unlinkSync(outputPath); } catch (error) { fs.unlinkSync(outputPath); throw error; } +}; + +export const logUploadProcess = async (videoInfo: VideoInfo) => { + try { + const videoId = videoInfo.id; + const title = videoInfo.title; + 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}`; + + + // ===== Save Request Log to Google Drive ===== + if (!fs.existsSync(FILE_PATH)) fs.mkdirSync(FILE_PATH, { recursive: true }); + 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 callFunctionWithRetry(() => googleDriveController.uploadFile(driveClient, outputPath, MIITEL_REQUEST_LOG_FOLDER_ID, `${fileName}.zip`, "application/zip")); + if(!logFileId) throw createCustomError("UPLOAD_LOG_FAILED"); + } catch(error) { + throw error; + } }; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/storage.ts b/functions/generate_minutes/src/logics/storage.ts index 8275145..f6ea454 100644 --- a/functions/generate_minutes/src/logics/storage.ts +++ b/functions/generate_minutes/src/logics/storage.ts @@ -1,5 +1,7 @@ import { Storage } from "@google-cloud/storage"; +import { Files } from "@google/genai"; import zlib from "zlib"; +import { CLOUD_STORAGE_LOG_FOLDER_NAME } from "../../serverConfig"; const csClient = new Storage({projectId: process.env.PROJECT_ID}); const BUCKET_NAME = process.env.CLOUD_STORAGE_BUCKET_NAME || ''; @@ -42,4 +44,19 @@ export const storageController = { return false; } }, + getFileList: async(): Promise => { + try { + const files = await bucket.getFiles({ + prefix: 'request_log/', + }); + const list = []; + for(const f of files[0]) { + // console.log(f.name) + list.push(f.name); + } + return list; + } catch(error) { + return null; + } + } }; diff --git a/functions/generate_minutes/src/stores/errorCodes.ts b/functions/generate_minutes/src/stores/errorCodes.ts index 3ae530f..de2c313 100644 --- a/functions/generate_minutes/src/stores/errorCodes.ts +++ b/functions/generate_minutes/src/stores/errorCodes.ts @@ -20,6 +20,9 @@ export const ERROR_DEFINITIONS = { 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 }, + + GET_FILES_FAILED: { code: "E3010", message: "ファイルの取得に失敗しました", statusCode: 500 }, + CREATE_MEETING_LOG_FAILED: { code: "E3011", message: "ミーティングログ作成に失敗しました", statusCode: 500 }, } as const; export type ErrorKey = keyof typeof ERROR_DEFINITIONS;