api call retry
This commit is contained in:
parent
c004f6c34f
commit
bb072cc91c
11 changed files with 126 additions and 23 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
@ -19,3 +19,6 @@ 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 MAX_RETRY_COUNT = 3;
|
||||
export const ROOP_DELAY_MS = 5000;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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,7 +124,7 @@ 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) {
|
||||
|
|
@ -129,3 +132,41 @@ export const processRequest = async (videoInfo: VideoInfo) => {
|
|||
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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue