api call retry

This commit is contained in:
kosukesuenaga 2025-12-08 14:22:40 +09:00
parent c004f6c34f
commit bb072cc91c
11 changed files with 126 additions and 23 deletions

View file

@ -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",

View file

@ -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"]
export const LOG_SHEET_HEADER_VALUES = ["タイムスタンプ","商談日", "タイトル", "登録先企業","担当者", "ミーティングURL", "議事録URL", "HubSpot会社概要URL"]
export const MAX_RETRY_COUNT = 3;
export const ROOP_DELAY_MS = 5000;

View file

@ -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;

View file

@ -6,7 +6,7 @@ const aiClient = new GoogleGenAI({
});
export const aiController = {
generateMinutes: async(text: string) => {
generateMinutes: async(text: string): Promise<string | null> => {
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) {

View file

@ -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 <T>(fn: () => Promise<T>): Promise<T | null> => {
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;
};

View file

@ -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";

View file

@ -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<Company | null> => {
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;

View file

@ -50,7 +50,7 @@ export const googleDriveController = {
return docs;
},
uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string, contentType: string): Promise<any> => {
uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string, contentType: string): Promise<string | null> => {
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;
//

View file

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

View file

@ -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<string[] | null> => {
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;
}
}
};

View file

@ -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;