python -> node.js

This commit is contained in:
kosukesuenaga 2025-12-05 14:12:11 +09:00
parent 092f2ec0f3
commit 395fba645d
62 changed files with 726 additions and 1702 deletions

BIN
functions/generate_minutes/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -5,12 +5,12 @@
"scripts": {
"build": "tsc",
"start": "npm run build && functions-framework --target=helloHttp --port=8080 --source=dist/index.js",
"dev": "dotenv -e .env_dev -- nodemon --watch . --exec \"functions-framework --target=helloHttp --port=8080\"",
"debug": "dotenv -e .env_dev -- node --inspect node_modules/.bin/functions-framework --source=dist/index.js --target=helloHttp",
"watch": "concurrently \"dotenv -e .env_dev -- npm run build -- --watch\" \"dotenv -e .env_dev -- nodemon --watch ./dist/ --exec npm run debug\""
},
"devDependencies": {
"@google-cloud/functions-framework": "^3.0.0",
"@types/archiver": "^7.0.0",
"@types/express": "^4.17.0",
"@types/node": "^20.0.0",
"dotenv-cli": "^11.0.0",
@ -19,12 +19,14 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@google-cloud/local-auth": "^2.1.0",
"@google-cloud/storage": "^7.17.3",
"@google/genai": "^1.30.0",
"@hubspot/api-client": "^13.4.0",
"archiver": "^7.0.1",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"fast-fuzzy": "^1.12.0",
"googleapis": "^105.0.0",
"zod": "^4.1.13"
}

View file

@ -0,0 +1,24 @@
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";
export const OWNERS_FILE_NAME = "owners.json";
export const LEGAL_SUFFIX = /(株式会社|(株)|\(株\)|有限会社|合同会社|Inc\.?|Corp\.?|Co\.?Ltd\.?)/;
export const Y_FORMAT = 'yyyy';
export const YM_FORMAT = 'yyyyMM'
export const DATETIME_FORMAT = 'yyyy-MM-dd hh:mm:ss';
export const DATE_FORMAT = 'yyyy年MM月dd日';
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"]

View file

@ -1,28 +1,56 @@
import express from "express";
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 { CLOUD_STORAGE_LOG_FOLDER_NAME, CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME, OWNERS_FILE_NAME } from "../serverConfig";
const router = express.Router();
router.get("/hello", (req, res) => res.send("こんにちは!"));
// Process Request From Miitel Webhook
router.post("/miitel", async (req, res) => {
try {
const body = req.body;
const parsedBody = MiiTelWebhookSchema.safeParse(body);
if (!parsedBody.success) throw createCustomError("ZOD_FAILED");
router.post("/miitel", async(req, res) => {
const body = req.body;
// await storageController.saveToGCS("request_log",'test', JSON.stringify(req.body));
const videoInfo = parsedBody.data.video;
const gzipped = zlib.gzipSync(JSON.stringify(body));
await storageController.saveToGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, `${videoInfo.id}.json.gz`, gzipped, 'application/gzip');
const parsedBody = MiiTelWebhookSchema.safeParse(body);
if(!parsedBody.success) {
console.error("Invalid webhook body:", parsedBody.error);
return;
await processRequest(videoInfo);
res.status(200).send("ok");
} catch(err) {
res.status(400).send("Invalid webhook body");
}
console.log("miitel webhook received:", parsedBody.data.video.id);
await processRequest(parsedBody.data.video);
res.send("こんにちは!");
});
router.post("/getLog", async(req, res) => {
// Update Master Data And Check Google Drive Folder
router.post("/dailyBatch", async (req, res) => {
try {
console.log("Starting daily batch process...");
// export companies to GCS
const companies = await hubspotController.getCompanies();
if(!companies) throw createCustomError("GET_OWNERS_FAILED");
await storageController.saveToGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME, JSON.stringify(companies), 'application/json');
// export owners to GCS
const owners = await hubspotController.getOwners();
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) {
console.error("Error in daily batch:", error);
}
});
// Check Log By Meeting ID
router.post("/getLog", async (req, res) => {
console.log(req.body);
const meetingId = req.body.meetingId;
const exist = await storageController.existsInGCS("request_log", "test.json.gz");
@ -32,4 +60,33 @@ router.post("/getLog", async(req, res) => {
res.send(log);
});
// router.post("/deleteFile", async (req, res) => {
// console.log(req.body);
// const fileId = req.body.fileId;
// const googleAuth = await googleDriveController.getAuth();
// const driveClilent = googleDriveController.getDriveClient(googleAuth);
// await googleDriveController.deleteFile(driveClilent, fileId);
// res.send('ok');
// });
// router.post("/test", async (req, res) => {
// try {
// const googleAuth = await googleDriveController.getAuth();
// const driveClilent = googleDriveController.getDriveClient(googleAuth);
// const sheetsClient = googleDriveController.getSheetsClient(googleAuth);
// const folderId = await googleDriveController.searchFileIdByFileName(driveClilent, MINUTES_CREATION_HISTORY_FOLDER_ID, '2025');
// if(!folderId) throw new Error()
// // console.log(fileId);
// // const sheetId = await googleDriveController.getLogSheetId(driveClilent, sheetsClient, folderId, 'test1');
// // console.log('sheet id : ', sheetId);
// res.send("ok");
// } catch (error) {
// console.error("Error in /test endpoint:", error);
// res.status(500).send("Error in /test endpoint");
// }
// });
export default router;

View file

@ -21,5 +21,12 @@ export const dateController = {
return formatted.replace(/(y+)/g, (v) =>
date.getFullYear().toString().slice(-v.length)
);
}
},
getCurrentJstTime: (format: string) => {
const utcDate = new Date().toUTCString();
const jstDate = dateController.convertToJst(utcDate);
const jstStr = dateController.getFormattedDate(jstDate, format);
return jstStr;
// return dateController.getFormattedDate(utcDate, "yyyy/MM/dd hh:mm:ss");
},
};

View file

@ -0,0 +1,29 @@
import { Response } from "express";
import z from "zod";
import { ERROR_DEFINITIONS, ErrorKey } from "../stores/errorCodes";
const CustomErrorSchema = z.object({
code: z.string(),
message: z.string(),
statusCode:z.number(),
});
export type CustomError = z.infer<typeof CustomErrorSchema>;
export const createCustomError = (key: ErrorKey): CustomError => {
const errorInfo = ERROR_DEFINITIONS[key];
return CustomErrorSchema.parse(errorInfo);
};
export const responseError = (error: any, res: Response | null = null) => {
if (!CustomErrorSchema.safeParse(error).success) {
console.error(error);
console.error("========== Unknown Error ==========");
if(res) return res.status(500).send('Internal Server Error');
}
const parsedError = CustomErrorSchema.parse(error);
console.error("========== Custom Error ==========");
console.error(`Error Code: ${parsedError.code}\n Message: ${parsedError.message}`);
if(res) return res.status(parsedError.statusCode).send(parsedError.message);
}

View file

@ -0,0 +1,53 @@
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";
export const fileController = {
createMinutesFileName: (title: string, hostName: string, jstStartsAt: Date): string => {
const dateStr = dateController.getFormattedDate(jstStartsAt, "yyyy年MM月dd日");
const fileName = `${dateStr} ${title} ${hostName}`;
return fileName;
},
extractCompanyNameFromTitle: (title: string) => {
const normalizedTitle = title.replace("【", "").replace("】", "");
const companyName = normalizedTitle.split("様")[0];
return companyName
},
createMinutesContent: (videoUrl: string, hostName: string, minutes: string): string => {
let minutesContent = `会議履歴URL${videoUrl}\n`;
minutesContent += `担当者:${hostName}\n\n`;
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 }
});
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.pipe(output);
archive.append(JSON.stringify(body), { name: fileName + '.json' });
archive.finalize();
})
console.log("ZIP created");
return;
},
};

View file

@ -0,0 +1,62 @@
import { search } from "fast-fuzzy";
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";
export const fuzzyMatchController = {
searchMatchedCompany: async(companyName: string): Promise<Company | null> => {
try {
const companiesJson = await 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;
const normalizedCompanyName = fuzzyMatchController.normalizeCompanyName(companyName);
const normalizedCompanies: Company[] = parsedCompanies.data.map((c) => CompanySchema.parse({
id: c.id,
name: fuzzyMatchController.normalizeCompanyName(c.name),
}));
// Exact Match
const exactMatchedCompany = fuzzyMatchController.searchExactMatchedCompany(normalizedCompanyName, normalizedCompanies);
// console.log(exactMatchedCompanyId);
if(exactMatchedCompany) return exactMatchedCompany;
// Fuzzy Match
const results = search(
fuzzyMatchController.normalizeCompanyName(companyName),
parsedCompanies.data,
{
keySelector: (obj) => fuzzyMatchController.normalizeCompanyName(obj.name),
returnMatchData: true,
threshold: 0.8,
},
);
console.log("===== Search Results =====");
console.log(results);
if(results.length <= 0) return null;
if(results.length === 1) return results[0].item;
if(results.length > 1) {
// 同スコアが複数存在
if(results[0].score === results[1].score) return null;
// トップが単独の場合のみ
return results[0].item;
}
return null;
} catch(error) {
console.error(error);
return null;
}
},
normalizeCompanyName: (companyName: string) => {
return companyName.replace(LEGAL_SUFFIX, '');
},
searchExactMatchedCompany: (companyName: string, companies: Company[]): Company | null => {
for(const company of companies) {
if(companyName === company.name) return company;
};
return null;
},
};

View file

@ -1,36 +1,226 @@
import { authenticate } from "@google-cloud/local-auth";
import { JSONClient } from "google-auth-library/build/src/auth/googleauth";
import { google } from "googleapis";
import path from "path";
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 z from "zod";
const SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"]
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');
const MAX_RETRY = 3;
export const LogRowDataSchema = z.object({
timestamp: z.string(),
meetingDate: z.string(),
title: z.string(),
matchedCompanyName: z.string(),
ownerName: z.string(),
meetingUrl: z.string(),
documentUrl: z.string(),
hubspotUrl: z.string(),
});
export type LogRowData = z.infer<typeof LogRowDataSchema>
export const googleDriveController = {
getAuth: async():Promise<any> => {
const auth = await new google.auth.GoogleAuth({
keyFile: CREDENTIALS_PATH,
scopes: SCOPES,
});
return auth;
getAuth: async (): Promise<any> => {
try {
const credentials = JSON.parse(process.env.SEARVICE_ACCOUNT_CREDENTIALS || "{}");
console.log(credentials)
const auth = await new google.auth.GoogleAuth({
credentials: credentials,
scopes: SCOPES,
});
if (!auth) return null;
return auth;
} catch (error) {
console.error("Error obtaining Google Auth:", error);
return null;
}
},
checkConnection: async() => {
const auth = await googleDriveController.getAuth();
getDriveClient: (auth: any): drive_v3.Drive => {
// console.log("Google Drive client authenticated.");
const drive = google.drive({ version: "v3", auth: auth});
const folder = '1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw';
const res = await drive.files.list({
q: `'${folder}' in parents`,
const drive = google.drive({ version: "v3", auth: auth });
return drive;
},
getSheetsClient: (auth: any): sheets_v4.Sheets => {
const sheets = google.sheets({ version: "v4", auth: auth });
return sheets;
},
getDocsClient: (auth: any): docs_v1.Docs => {
const docs = google.docs({ version: "v1", auth: auth });
return docs;
},
uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string): Promise<any> => {
try {
console.log("Uploading file to Google Drive:", filePath);
const response = await driveClient.files.create({
requestBody: {
name: fileName,
parents: [folderId],
},
media: {
mimeType: "application/zip",
body: fs.createReadStream(filePath),
},
});
console.log("File uploaded, Id:", response.data.id);
fs.unlinkSync(filePath);
return response.data.id;
} catch (error) {
console.error("Error uploading file:", error);
fs.unlinkSync(filePath);
return null;
}
},
getFolderId: async (driveClient: drive_v3.Drive, folderId: string, fileName: string): Promise<string | null> => {
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<string | null> => {
try {
const params = googleDriveController.getSearchFileParamsByDebugMode(folderId);
const res = await driveClient.files.list(params);
console.log("Files:");
console.log(res.data.files);
if(!res.data.files) return null;
for(const file of res.data.files) {
if(fileName === file.name) {
if(!file.id) return null;
return file.id;
}
}
return null;
} catch (error) {
console.error('Error searching files:', error);
return null;
}
},
getSearchFileParamsByDebugMode: (folderId: string): drive_v3.Params$Resource$Files$List => {
if(DEBUG) {
return {
corpora: 'user',
q: `'${folderId}' in parents`,
pageSize: 10,
fields: "files(id, name)",
includeItemsFromAllDrives: true,
includeTeamDriveItems: true,
supportsAllDrives: true
}
}
return {
corpora: 'drive',
driveId: process.env.GOOGLE_DRIVE_FOLDER_ID,
q: `'${folderId}' in parents`,
pageSize: 10,
fields: "files(id, name)",
});
console.log("Files:");
console.log(res.data.files);
includeItemsFromAllDrives: true,
includeTeamDriveItems: true,
supportsAllDrives: true
}
},
uploadFile: async() => {
createNewFile: async (driveClient: drive_v3.Drive, folderId: string, fileName: string, mimeType: string): Promise<string | null> => {
try {
const requestBody = {
name: fileName,
parents: [folderId], // 作成したフォルダのIDを指定
mimeType: mimeType,
};
const file = await driveClient.files.create({
requestBody,
// fields: 'id',
});
console.log('File Id:', file.data);
if (!file.data.id) return null;
return file.data.id;
} catch (error) {
console.error('Error creating file:', error);
return null;
}
},
// CAUTION
deleteFile: async (driveClient: drive_v3.Drive, fileId: string) => {
try {
const body = { trashed: true }
const response = await driveClient.files.update({
fileId: fileId,
requestBody: body,
});
console.log('File deleted:', response.data);
} catch (error) {
console.error('Error deleting file:', error);
}
},
addContentToDocs: async (docsClient: docs_v1.Docs, documentId: string, content: string): Promise<boolean> => {
try {
const requestBody: docs_v1.Schema$BatchUpdateDocumentRequest = {
requests: [
{
insertText: {
text: content,
location: {
index: 1,
}
}
}
]
};
const response = await docsClient.documents.batchUpdate({
documentId: documentId,
requestBody: requestBody,
});
console.log('Content added to document:', response.data);
return true;
} catch (error) {
console.error('Error adding content to document:', error);
return false;
}
},
createNewFile: async() => {
getLogSheetId: async (driveClient: drive_v3.Drive, sheetsClient: sheets_v4.Sheets, folderId: string, fileName: string): Promise<string | null> => {
try {
const existsSheetId = await googleDriveController.searchFileIdByFileName(driveClient, folderId, fileName);
if(existsSheetId) return existsSheetId;
console.log('=== Create New Sheet ===')
const newSheetId = await googleDriveController.createNewFile(driveClient, folderId, fileName, SHEET_MIMETYPE);
if(!newSheetId) return null;
//
await googleDriveController.insertRowToSheet(sheetsClient, newSheetId, ['※シート名変更厳禁']);
await googleDriveController.insertRowToSheet(sheetsClient, newSheetId, LOG_SHEET_HEADER_VALUES);
return newSheetId;
} catch (error) {
console.error('Error searching files:', error);
return null;
}
},
insertRowToSheet: async (sheetsClient: sheets_v4.Sheets, sheetId: string, rowData: string[] ): Promise<boolean> => {
try {
const body = {
values: [rowData]
}
const params: sheets_v4.Params$Resource$Spreadsheets$Values$Append = {
spreadsheetId: sheetId,
range: 'Sheet1',
valueInputOption: 'USER_ENTERED',
insertDataOption: 'INSERT_ROWS',
requestBody: body,
}
await sheetsClient.spreadsheets.values.append(params);
return true;
} catch (error) {
console.log(error);
return false;
}
},
};

View file

@ -0,0 +1,116 @@
import { Client } from "@hubspot/api-client";
import { AssociationSpecAssociationCategoryEnum } from "@hubspot/api-client/lib/codegen/crm/objects/meetings/models/AssociationSpec";
import { PublicAssociationsForObject } from "@hubspot/api-client/lib/codegen/crm/objects/meetings";
import z, { email } from "zod";
const hubspotClient = new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN });
export const CompanySchema = z.object({
id: z.string(),
name: z.string(),
})
export const OwnerSchema = z.object({
id: z.string(),
email: z.string().optional().default(''),
});
export type Company = z.infer<typeof CompanySchema>;
export type Owner = z.infer<typeof OwnerSchema>;
export const hubspotController = {
check: async() => {
const response = await hubspotClient.crm.companies.getAll();
console.log(response.length);
},
getCompanies: async(): Promise<Company[] | null> => {
try {
const allCompanies: Company[] = [];
const limit = 100;
let after: string | undefined = undefined;
for(let i = 0; i < 1000; i++) {
console.log(`Fetching companies, iteration ${i+1}`);
const response = await hubspotClient.crm.companies.basicApi.getPage(limit, after);
// console.log(response.results);
const companies: Company[] = response.results.map((company) => CompanySchema.parse({
id: company.id,
name: company.properties.name,
}));
allCompanies.push(...companies);
if(response.paging && response.paging.next && response.paging.next.after) {
after = response.paging.next.after;
continue;
}
break;
}
return allCompanies;
} catch (error) {
return null;
}
},
getOwners: async(): Promise<Owner[] | null> => {
try {
const allOwners: Owner[] = [];
const limit = 100;
let after: string | undefined = undefined;
for(let i = 0; i < 1000; i++) {
console.log(`Fetching owners, iteration ${i+1}`);
const response = await hubspotClient.crm.owners.ownersApi.getPage(undefined,after,limit);
// console.log(response.results);
const owners: Owner[] = response.results.map((owner) => OwnerSchema.parse({
id: owner.id,
email: owner.email,
}));
allOwners.push(...owners);
if(response.paging && response.paging.next && response.paging.next.after) {
after = response.paging.next.after;
continue;
}
break;
}
return allOwners;
} catch (error) {
console.error("Error fetching owners:", error);
return null;
}
},
createMeetingLog: async(companyId: string, title: string, userId: string | null, minutes: string, startsAt: string, endsAt: string ): Promise<boolean> => {
try {
// 改行コードを変換
const minutes_html = minutes.replace("\n", "<br>")
const associations: PublicAssociationsForObject[] = [{
types: [
{associationCategory: AssociationSpecAssociationCategoryEnum.HubspotDefined, associationTypeId: 188},
],
to: {id: companyId},
}];
const properties = {
hs_timestamp: startsAt,
hs_meeting_title: title,
hubspot_owner_id: userId || '',
hs_meeting_body: minutes_html,
hs_meeting_start_time: startsAt,
hs_meeting_end_time: endsAt,
}
const result = await hubspotClient.crm.objects.meetings.basicApi.create({
associations: associations,
properties: properties,
});
return true;
} catch (error) {
console.error("Error creating HubSpot meeting log:", error);
return false;
}
},
searchOwnerIdByEmail: (email: string, owners: Owner[]): string | null => {
for(const owner of owners) {
if(email === owner.email) return owner.id;
}
return null;
},
};

View file

@ -1,7 +1,15 @@
import z from "zod";
import { aiController } from "./ai";
import { dateController } from "./date";
import { googleDriveController } from "./googleDrive";
import { googleDriveController, LogRowData, LogRowDataSchema } from "./googleDrive";
import { fileController } from "./file";
import path, { join } from "path";
import fs from "fs";
import { createCustomError, responseError } 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 { hubspotController, OwnerSchema } from "./hubspot";
import { fuzzyMatchController } from "./fuzzyMatch";
const VideoInfoSchema = z.looseObject({
id: z.string(),
@ -26,32 +34,101 @@ export const MiiTelWebhookSchema = z.object({
// export type MiiTelWebhook = z.infer<typeof MiiTelWebhookSchema>;
export const processRequest = async(videoInfo: VideoInfo) => {
const videoId = videoInfo.id;
const title = videoInfo.title;
const startsAt = videoInfo.starts_at;
const endsAt = videoInfo.ends_at;
const accessPermission = videoInfo.access_permission;
const host_id = videoInfo.host.login_id;
const host_name = videoInfo.host.user_name;
const speechRecognition = videoInfo.speech_recognition.raw;
console.log(startsAt);
const jstStartsAt = dateController.convertToJst(startsAt);
const jstEndsAt = dateController.convertToJst(endsAt);
googleDriveController.checkConnection();
// console.log(dateController.getFormattedDate(startsAtJst, "yyyy/MM/dd hh:mm:ss"));
// console.log(endsAt);
// console.log("Processing video:", host_id, host_name, title);
if(accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return;
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 || '';
const MIITEL_URL = process.env.MIITEL_URL || '';
const HUBSPOT_COMPANY_URL = process.env.HUBSPOT_COMPANY_URL || '';
// Save Request Log to Google Drive
// const minute = await aiController.generateMinutes(speechRecognition);
// console.log(minute);
const FILE_PATH = join(__dirname, "../files/");
};
export const processRequest = 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;
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');
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`;
content += minutes;
// ===== 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);
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");
const parsedOwners = z.array(OwnerSchema).safeParse(JSON.parse(ownersJson));
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);
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");
const currentYearMonth = dateController.getCurrentJstTime(YM_FORMAT);
const sheetId = await googleDriveController.getLogSheetId(driveClient, sheetsClient, yearFileId, currentYearMonth);
if(!sheetId) throw createCustomError("GET_SHEET_ID_FAILED");
const currentJstDateTimeStr = dateController.getCurrentJstTime(DATETIME_FORMAT);
const currentJstDateStr = dateController.getCurrentJstTime(DATE_FORMAT);
const rowData: LogRowData = LogRowDataSchema.parse({
timestamp: currentJstDateTimeStr,
meetingDate: currentJstDateStr,
title: title,
matchedCompanyName: matchedCompany?.name ?? '',
ownerName: hostName,
meetingUrl: videoUrl,
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;
}
};

View file

@ -1,19 +1,15 @@
import { Storage } from "@google-cloud/storage";
import zlib from "zlib";
const csClient = new Storage({
projectId: 'datacom-poc',
}
);
const BUCKET_NAME = "meeting-report-data";
const csClient = new Storage({projectId: process.env.PROJECT_ID});
const BUCKET_NAME = process.env.CLOUD_STORAGE_BUCKET_NAME || '';
const bucket = csClient.bucket(BUCKET_NAME);
export const storageController = {
saveToGCS: async(folder: string, filename: string, text: string) => {
const gzipped = zlib.gzipSync(text);
const file = bucket.file((`${folder}/${filename}.json.gz`));
await file.save(gzipped, {
contentType: 'application/gzip',
saveToGCS: async(folder: string, filename: string, content: any, contentType: string) => {
const file = bucket.file((`${folder}/${filename}`));
await file.save(content, {
contentType: contentType,
})
},
loadFromGCS: async(folder: string, filename: string): Promise<string | null> => {
@ -26,6 +22,16 @@ export const storageController = {
return null;
}
},
loadJsonFromGCS: async(folder: string, filename: string): Promise<string | null> => {
const file = bucket.file(`${folder}/${filename}`);
// console.log("loading file:", file.name);
try {
const [data] = await file.download();
return data.toString("utf-8");
} catch (err: any) {
return null;
}
},
existsInGCS: async(folder: string, filename: string): Promise<boolean> => {
const file = bucket.file((`${folder}/${filename}`));
console.log("checking file:", file.name);

View file

@ -0,0 +1,26 @@
// errorDefinitions.ts
export const ERROR_DEFINITIONS = {
ZOD_FAILED: { code: "E1003", message: "zodのチェックが失敗しました", statusCode: -1 },
// ログ ZIP の Google Drive アップロード失敗
UPLOAD_LOG_FAILED: { code: "E3001", message: "ログファイルのアップロードに失敗しました", statusCode: 500 },
// AI による議事録生成失敗
AI_GENERATION_FAILED: { code: "E2001", message: "AIによる議事録生成に失敗しました", statusCode: 500 },
// 議事録Google Docsの作成アップロード失敗
UPLOAD_MINUTES_FAILED: { code: "E3002", message: "議事録のアップロードに失敗しました", statusCode: 500 },
// オーナー情報の取得失敗
GET_OWNERS_FAILED: { code: "E3003", message: "オーナー情報の取得に失敗しました", statusCode: 500 },
GET_COMPANIES_FAILED: { code: "E3004", 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 },
} as const;
export type ErrorKey = keyof typeof ERROR_DEFINITIONS;