This commit is contained in:
kosukesuenaga 2025-12-24 11:36:34 +09:00
parent 1259ba76c9
commit 6454e1b46b
19 changed files with 667 additions and 611 deletions

23
.gitignore vendored
View file

@ -1,23 +1,10 @@
handle-company-webhook/
terraform.* terraform.*
.terraform* .terraform*
IAM/
test/
venv/
__pycache__/
*.csv
request.json
node_modules/ node_modules/
dist/ dist/
.env_dev .env*
.env credentials*
.env_prod package-lock.json
credentials.json *.sh
credentials_dev.json log/
package-lock.json

View file

@ -1,16 +0,0 @@
#!/bin/bash
# APIエンドポイントURL
API_URL="https://sales-tool-gw-dev-ex1cujb.an.gateway.dev/trigger-minutes-workflow-from-miitel"
# APIキーヘッダーに付与する場合
API_KEY="AIzaSyBVJOtvJTB4noAfUGEyMhCRqsF5yfypENc"
# リクエストボディ
JSON_FILE="request.json"
# curlコマンド実行
curl -X POST "$API_URL" \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-d @"$JSON_FILE"

View file

@ -5,10 +5,6 @@ info:
version: '1.0.0' version: '1.0.0'
schemes: schemes:
- 'https' - 'https'
host: 'crate-minutes-gw-a8slsa47.an.gateway.dev'
x-google-endpoints:
- name: 'crate-minutes-gw-a8slsa47.an.gateway.dev'
allowCors: True
paths: paths:
/create-minutes: /create-minutes:
post: post:
@ -70,6 +66,321 @@ paths:
Access-Control-Allow-Headers: Access-Control-Allow-Headers:
type: string type: string
default: 'Content-Type, x-api-key' default: 'Content-Type, x-api-key'
/miitel:
post:
description: 'Miitel Webhook Processer'
operationId: 'miitel'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes/api/miitel
path_translation: CONSTANT_ADDRESS
jwt_audience: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes
deadline: 600
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
400:
description: 'Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
security:
- APIKeyHeader: []
/dailyBatch:
post:
description: 'get companies and owners'
operationId: 'dailyBatch'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes/api/dailyBatch
path_translation: CONSTANT_ADDRESS
jwt_audience: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes
deadline: 600
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
400:
description: 'Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
security:
- APIKeyHeader: []
/getLog:
post:
description: 'get log'
operationId: 'getLog'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes/api/getLog
path_translation: CONSTANT_ADDRESS
jwt_audience: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes
deadline: 600
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
400:
description: 'Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
security:
- APIKeyHeader: []
/reExecute:
post:
description: ''
operationId: 'reExecute'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes/api/reExecute
path_translation: CONSTANT_ADDRESS
jwt_audience: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes
deadline: 600
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
400:
description: 'Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
security:
- APIKeyHeader: []
/test:
post:
description: 'test'
operationId: 'test'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes/api/test
path_translation: CONSTANT_ADDRESS
jwt_audience: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes
deadline: 600
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
400:
description: 'Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
/alertTest:
post:
description: 'alertTest'
operationId: 'alertTest'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes/api/alertTest
path_translation: CONSTANT_ADDRESS
jwt_audience: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/generate_minutes
deadline: 600
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
security:
- APIKeyHeader: []
options:
summary: 'CORS support'
operationId: 'test-options'
responses:
204:
description: 'CORS preflight'
headers:
Access-Control-Allow-Origin:
type: string
default: '*'
Access-Control-Allow-Methods:
type: string
default: 'GET, POST, OPTIONS'
Access-Control-Allow-Headers:
type: string
default: 'Content-Type, x-api-key'
securityDefinitions: securityDefinitions:
APIKeyHeader: APIKeyHeader:
type: apiKey type: apiKey

View file

@ -1,196 +0,0 @@
substitutions:
_ENV: 'dev'
_CF_SERVICE_ACCOUNT: 'mrt-cloudfunctions-sa-devtest'
_CW_SERVICE_ACCOUNT: 'mrt-cloudworkflows-sa-devtest'
options:
logging: CLOUD_LOGGING_ONLY
steps:
# 会社一覧取得
- id: 'gcloud functions deploy mrt-export-companies-to-gcs'
name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-companies-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-companies-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 担当者一覧取得
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-owners-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-owners-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシート作成
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-log-sheet'
args: [
'functions',
'deploy',
'mrt-create-log-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー呼び出し関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/trigger-minutes-workflow-from-miitel'
args: [
'functions',
'deploy',
'mrt-trigger-minutes-workflow-from-miitel',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 議事録作成関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/generate-meeting-minutes'
args: [
'functions',
'deploy',
'mrt-generate-meeting-minutes',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--timeout=10m',
'--quiet',
]
waitFor: ['-']
# 議事録をドライブへアップロードする関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/upload-minutes-to-drive'
args: [
'functions',
'deploy',
'mrt-upload-minutes-to-drive',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# Hubspot連携関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-hubspot-meeting-log'
args: [
'functions',
'deploy',
'mrt-create-hubspot-meeting-log',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシートへ記録
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/append-log-to-sheet'
args: [
'functions',
'deploy',
'mrt-append-log-to-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー
- name: gcr.io/cloud-builders/gcloud
dir: 'workflows/workflow-create-minutes'
args:
[
'workflows',
'deploy',
'mrt-workflow-create-minutes',
'--location=asia-northeast1',
'--source=main.yaml',
'--service-account=$_CW_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--quiet',
]

View file

@ -1,193 +0,0 @@
substitutions:
_ENV: 'prod'
_CF_SERVICE_ACCOUNT: 'mrt-cloudfunctions-sa'
_CW_SERVICE_ACCOUNT: 'mrt-cloudworkflows-sa'
steps:
# 会社一覧取得
- id: 'gcloud functions deploy mrt-export-companies-to-gcs'
name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-companies-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-companies-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 担当者一覧取得
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-owners-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-owners-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシート作成
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-log-sheet'
args: [
'functions',
'deploy',
'mrt-create-log-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー呼び出し関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/trigger-minutes-workflow-from-miitel'
args: [
'functions',
'deploy',
'mrt-trigger-minutes-workflow-from-miitel',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 議事録作成関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/generate-meeting-minutes'
args: [
'functions',
'deploy',
'mrt-generate-meeting-minutes',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--timeout=10m',
'--quiet',
]
waitFor: ['-']
# 議事録をドライブへアップロードする関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/upload-minutes-to-drive'
args: [
'functions',
'deploy',
'mrt-upload-minutes-to-drive',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# Hubspot連携関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-hubspot-meeting-log'
args: [
'functions',
'deploy',
'mrt-create-hubspot-meeting-log',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシートへ記録
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/append-log-to-sheet'
args: [
'functions',
'deploy',
'mrt-append-log-to-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー
- name: gcr.io/cloud-builders/gcloud
dir: 'workflows/workflow-create-minutes'
args:
[
'workflows',
'deploy',
'mrt-workflow-create-minutes',
'--location=asia-northeast1',
'--source=main.yaml',
'--service-account=$_CW_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--quiet',
]

View file

@ -19,6 +19,9 @@ node_modules
.env_prod .env_prod
deploy_function_dev.sh deploy_function_dev.sh
deploy_function_prod.sh
files/
package-lock.json package-lock.json

View file

@ -5,8 +5,8 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"start": "npm run build && functions-framework --target=helloHttp --port=8080 --source=dist/index.js", "start": "npm run build && functions-framework --target=helloHttp --port=8080 --source=dist/index.js",
"debug": "dotenv -e .env_dev -- node --inspect node_modules/.bin/functions-framework --source=dist/index.js --target=helloHttp", "debug": "dotenv -e .env_prod -- 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\"" "watch": "concurrently \"dotenv -e .env_prod -- npm run build -- --watch\" \"dotenv -e .env_prod -- nodemon --watch ./dist/ --exec npm run debug\""
}, },
"devDependencies": { "devDependencies": {
"@google-cloud/functions-framework": "^3.0.0", "@google-cloud/functions-framework": "^3.0.0",
@ -29,6 +29,7 @@
"express": "^4.21.2", "express": "^4.21.2",
"fast-fuzzy": "^1.12.0", "fast-fuzzy": "^1.12.0",
"googleapis": "^105.0.0", "googleapis": "^105.0.0",
"marked": "^17.0.1",
"zod": "^4.1.13" "zod": "^4.1.13"
} }
} }

View file

@ -1,6 +1,6 @@
export const GEMINI_MODEL_ID = "gemini-2.5-flash"; export const GEMINI_MODEL_ID = "gemini-2.5-pro";
export const DEBUG = true; export const DEBUG = false;
export const CLOUD_STORAGE_MASTER_FOLDER_NAME = "master"; export const CLOUD_STORAGE_MASTER_FOLDER_NAME = "master";
export const CLOUD_STORAGE_LOG_FOLDER_NAME = "new_request_log"; export const CLOUD_STORAGE_LOG_FOLDER_NAME = "new_request_log";

View file

@ -1,12 +1,15 @@
import express from "express"; import express from "express";
import zlib from "zlib"; import zlib from "zlib";
import { storageController } from "./logics/storage"; import { storageController } from "./logics/storage";
import { MiiTelWebhookSchema, processRequest } from "./logics/process"; import { logUploadProcess, MiiTelWebhookSchema, processRequest, testProcess } from "./logics/process";
import { hubspotController } from "./logics/hubspot"; import { hubspotController } from "./logics/hubspot";
import { createCustomError, responseError } 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"; import { CLOUD_STORAGE_LOG_FOLDER_NAME, CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME, OWNERS_FILE_NAME } from "../serverConfig";
import { Delay } from "cerceis-lib"; import { Delay } from "cerceis-lib";
import { googleDriveController } from "./logics/googleDrive"; import path from "path";
import fs from "fs";
import { fuzzyMatchController } from "./logics/fuzzyMatch";
const router = express.Router(); const router = express.Router();
@ -30,8 +33,7 @@ router.post("/miitel", async (req, res) => {
return res.status(200).send("ok"); return res.status(200).send("ok");
} catch(err) { } catch(err) {
responseError(err, res); return responseError(err, res);
return;
} }
}); });
@ -41,18 +43,19 @@ router.post("/dailyBatch", async (req, res) => {
console.log("Starting daily batch process..."); console.log("Starting daily batch process...");
// export companies to GCS // export companies to GCS
const companies = await hubspotController.getCompanies(); const companies = await hubspotController.getCompanies();
if(!companies) throw createCustomError("GET_OWNERS_FAILED"); if(!companies) throw createCustomError("GET_COMPANIES_FAILED");
await storageController.saveToGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME, JSON.stringify(companies), 'application/json'); await storageController.saveToGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, COMPANIES_FILE_NAME, JSON.stringify(companies), 'application/json');
// export owners to GCS // export owners to GCS
const owners = await hubspotController.getOwners(); const owners = await hubspotController.getOwners();
if(!owners) throw createCustomError("GET_COMPANIES_FAILED"); if(!owners) throw createCustomError("GET_OWNERS_FAILED");
await storageController.saveToGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME, JSON.stringify(owners), 'application/json'); await storageController.saveToGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME, JSON.stringify(owners), 'application/json');
res.status(200).send("Daily batch executed."); res.status(200).send("Daily batch executed.");
} catch (error) { } catch (error) {
console.error("Error in daily batch:", error); console.error("Error in daily batch:", error);
return res.status(400).send("Error executing daily batch.");
} }
}); });
@ -60,11 +63,13 @@ router.post("/dailyBatch", async (req, res) => {
router.post("/getLog", async (req, res) => { router.post("/getLog", async (req, res) => {
console.log(req.body); console.log(req.body);
const meetingId = req.body.meetingId; const meetingId = req.body.meetingId;
const exist = await storageController.existsInGCS("request_log", "test.json.gz"); const exist = await storageController.existsInGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, `${meetingId}.json.gz`);
console.log("Log exists:", exist); console.log("Log exists:", exist);
const log = await storageController.loadFromGCS("request_log", meetingId + ".json.gz"); const log = await storageController.loadFromGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, meetingId + ".json.gz");
console.log(log) if(!log) throw Error();
res.send(log); const params = MiiTelWebhookSchema.parse(JSON.parse(log));
// console.log(params)
res.send(params);
}); });
@ -77,11 +82,12 @@ router.post("/reExecute", async (req, res) => {
const log = await storageController.loadFromGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, `${meetingId}.json.gz`); const log = await storageController.loadFromGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, `${meetingId}.json.gz`);
if(!log) throw Error(); if(!log) throw Error();
const params = MiiTelWebhookSchema.safeParse(JSON.parse(log)); const params = MiiTelWebhookSchema.safeParse(JSON.parse(log));
console.log(params);
if(!params.success) throw createCustomError("ZOD_FAILED"); if(!params.success) throw createCustomError("ZOD_FAILED");
params.data.video.title = newTitle; params.data.video.title = newTitle;
// console.log(params.data.video) // console.log(params.data.video)
await processRequest(params.data.video); // await processRequest(params.data.video);
res.send(log); res.send(log);
} catch(error) { } catch(error) {
@ -92,28 +98,39 @@ router.post("/reExecute", async (req, res) => {
// 過去のログを全てGoogle Driveへアップロード // 過去のログを全てGoogle Driveへアップロード
router.post("/logUpload", async (req, res) => { // router.post("/logUpload", async (req, res) => {
try { // try {
const list = await storageController.getFileList(); // const list = await storageController.getFileList();
if(!list) throw createCustomError("GET_FILES_FAILED"); // if(!list) throw createCustomError("GET_FILES_FAILED");
for(const l of list){ // console.log("Total files to process:", list.length);
console.log(l); // const failedFiles: string[] = [];
const fileName = l.split('/')[1] // let count = 0;
const log = await storageController.loadFromGCS('request_log', fileName); // const tmplist = list.slice(1600,1800);
if(!log) throw createCustomError("GET_FILES_FAILED"); // for(const l of tmplist){
// console.log(log); // console.log(l);
const parsedLog = MiiTelWebhookSchema.safeParse(JSON.parse(log)); // count++;
if(!parsedLog.success) throw createCustomError("ZOD_FAILED"); // console.log(`Processing file ${count} of ${tmplist.length}`);
console.log(parsedLog.data.video.title); // const fileName = l.split('/')[1]
// const log = await storageController.loadFromGCS('request_log', fileName);
await Delay(500); // if(!log) {
} // failedFiles.push(fileName);
res.send('ok'); // continue;
} catch(error) { // };
console.log(error); // const parsedLog = MiiTelWebhookSchema.safeParse(JSON.parse(log));
res.status(400).send("Failed"); // if(!parsedLog.success) throw createCustomError("ZOD_FAILED");
} // console.log(parsedLog.data.video.title);
}); // const result = await logUploadProcess(parsedLog.data.video);
// if(!result) failedFiles.push(fileName);
// await Delay(500);
// }
// const outputPath = path.join(__dirname, "../log/", 'failedFiles.json');
// fs.writeFileSync(outputPath, JSON.stringify(failedFiles, null, 2));
// res.send('ok');
// } catch(error) {
// console.log(error);
// res.status(400).send("Failed");
// }
// });
// router.post("/deleteFile", async (req, res) => { // router.post("/deleteFile", async (req, res) => {
// console.log(req.body); // console.log(req.body);
@ -126,20 +143,32 @@ router.post("/logUpload", async (req, res) => {
router.post("/test", async (req, res) => { router.post("/test", async (req, res) => {
try { try {
await testProcess();
// 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"); res.send("ok");
} catch (error) { } catch (error) {
console.error("Error in /test endpoint:", error); console.error(error);
res.status(500).send("Error in /test endpoint"); res.status(400).send("Error in /test endpoint");
} }
}); });
router.post("/alertTest", async (_req, res) => {
res.status(500).send("Error");
});
// router.post("/debug", async (req, res) => {
// try {
// const a = await fuzzyMatchController.searchMatchedCompany("Aコープ九");
// console.log(a);
// res.send("ok");
// } catch (error) {
// console.error(error);
// res.status(400).send("Error in /test endpoint");
// }
// });
export default router; export default router;

View file

@ -8,20 +8,91 @@ const aiClient = new GoogleGenAI({
export const aiController = { export const aiController = {
generateMinutes: async(text: string): Promise<string | null> => { generateMinutes: async(text: string): Promise<string | null> => {
const prompt = ` const prompt = `
[]
: #
1. AIによる書き起こしミスがある可能性を考慮してください * **:** DX
2. * **:**
3. * A:
4. * B: POSメーカー等API連携
5. 500 * **:** Speaker A, B等
6.
7. 使使
#
${text}
`
| | | |
| :--- | :--- | :--- |
| **** | | |
| **** | ID-POS | |
| | ArmBox | |
| | RV | |
| | MS-View | |
| | AWS | |
| | CustomerJournal (CJ) | / |
| | Tiramisu | |
| | TerraMap | |
| | d@Journal | |
| | d3 | |
| | D-PLAN | |
| | PV | |
| | FreshO2 | |
| | Point View | |
| | Retail View | |
| **** | RFP | |
| | | |
| | | |
| | DWH | |
| | POS | |
| | CUBIC | |
| | NOCC | |
| ****| | |
| | | |
#
1. **:** :Tiramisuが
2. **:**
*
*
3. **:**
*
* 調
4. **:**
#
##
* **:** / / etc.
* **:**
* **:** : / :
* **:** 300
##
*
* ...
## ToDo
| | | |
| :--- | :--- | :--- |
| | ... | ... |
| | ... | ... |
##
### 1
* **:**
* **:**
* **:**
### 2
...
##
*
---
[]:
${text}
`;
try { try {
const response = await aiClient.models.generateContent({ const response = await aiClient.models.generateContent({

View file

@ -5,9 +5,8 @@ import fs from "fs";
export const fileController = { export const fileController = {
createMinutesFileName: (title: string, hostName: string, jstStartsAt: Date): string => { createMinutesFileName: (title: string, hostName: string, meetingDateStr: string): string => {
const dateStr = dateController.getFormattedDate(jstStartsAt, "yyyy年MM月dd日"); const fileName = `${meetingDateStr} ${title.replace('/', '')} ${hostName}`;
const fileName = `${dateStr} ${title} ${hostName}`;
return fileName; return fileName;
}, },
extractCompanyNameFromTitle: (title: string) => { extractCompanyNameFromTitle: (title: string) => {

View file

@ -15,13 +15,13 @@ export const fuzzyMatchController = {
if(!parsedCompanies.success) return null; if(!parsedCompanies.success) return null;
const normalizedCompanyName = fuzzyMatchController.normalizeCompanyName(companyName); const normalizedCompanyName = fuzzyMatchController.normalizeCompanyName(companyName);
const normalizedCompanies: Company[] = parsedCompanies.data.map((c) => CompanySchema.parse({ const companies: Company[] = parsedCompanies.data.map((c) => CompanySchema.parse({
id: c.id, id: c.id,
name: fuzzyMatchController.normalizeCompanyName(c.name), name: c.name,
})); }));
// Exact Match // Exact Match
const exactMatchedCompany = fuzzyMatchController.searchExactMatchedCompany(normalizedCompanyName, normalizedCompanies); const exactMatchedCompany = fuzzyMatchController.searchExactMatchedCompany(normalizedCompanyName, companies);
// console.log(exactMatchedCompanyId); // console.log(exactMatchedCompanyId);
if(exactMatchedCompany) return exactMatchedCompany; if(exactMatchedCompany) return exactMatchedCompany;
@ -56,7 +56,7 @@ export const fuzzyMatchController = {
}, },
searchExactMatchedCompany: (companyName: string, companies: Company[]): Company | null => { searchExactMatchedCompany: (companyName: string, companies: Company[]): Company | null => {
for(const company of companies) { for(const company of companies) {
if(companyName === company.name) return company; if(companyName === fuzzyMatchController.normalizeCompanyName(company.name)) return company;
}; };
return null; return null;
}, },

View file

@ -1,12 +1,12 @@
import { docs_v1, drive_v3, google, sheets_v4 } from "googleapis"; import { docs_v1, drive_v3, google, sheets_v4 } from "googleapis";
import fs from "fs"; import fs from "fs";
import { DEBUG, LOG_SHEET_HEADER_VALUES, SHEET_MIMETYPE } from "../../serverConfig"; import { DEBUG, DOCUMENT_MIMETYPE, LOG_SHEET_HEADER_VALUES, SHEET_MIMETYPE } from "../../serverConfig";
import z from "zod"; import z from "zod";
import { Readable } from "stream";
const GOOGLE_DRIVE_FOLDER_ID = process.env.GOOGLE_DRIVE_FOLDER_ID; 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 SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"]
const MAX_RETRY = 3;
export const LogRowDataSchema = z.object({ export const LogRowDataSchema = z.object({
timestamp: z.string(), timestamp: z.string(),
@ -49,10 +49,27 @@ export const googleDriveController = {
const docs = google.docs({ version: "v1", auth: auth }); const docs = google.docs({ version: "v1", auth: auth });
return docs; return docs;
}, },
checkConnection: async (driveClient: drive_v3.Drive): Promise<boolean> => {
try {
const res = await driveClient.files.list({
corpora: 'drive',
driveId: GOOGLE_DRIVE_FOLDER_ID,
pageSize: 1,
fields: "files(id, name)",
includeItemsFromAllDrives: true,
includeTeamDriveItems: true,
supportsAllDrives: true
});
console.log("Google Drive connection check successful:", res.data);
return true;
} catch (error) {
console.error("Error checking Google Drive connection:", error);
return false;
}
},
uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string, contentType: string): Promise<string | null> => { uploadFile: async (driveClient: drive_v3.Drive, filePath: string, folderId: string, fileName: string, contentType: string): Promise<string | null> => {
try { try {
// console.log("Uploading file to Google Drive:", filePath);
const response = await driveClient.files.create({ const response = await driveClient.files.create({
requestBody: { requestBody: {
name: fileName, name: fileName,
@ -62,6 +79,7 @@ export const googleDriveController = {
mimeType: contentType, mimeType: contentType,
body: fs.createReadStream(filePath), body: fs.createReadStream(filePath),
}, },
supportsAllDrives: true,
}); });
if(!response.data.id) return null; if(!response.data.id) return null;
return response.data.id; return response.data.id;
@ -123,6 +141,7 @@ export const googleDriveController = {
const file = await driveClient.files.create({ const file = await driveClient.files.create({
requestBody, requestBody,
supportsAllDrives: true,
// fields: 'id', // fields: 'id',
}); });
@ -134,6 +153,35 @@ export const googleDriveController = {
return null; return null;
} }
}, },
createMinutesDocument: async(driveClient: drive_v3.Drive, folderId: string, fileName: string, htmlText: string): Promise<string | null> => {
try {
const requestBody = {
name: fileName,
parents: [folderId], // 作成したフォルダのIDを指定
mimeType: DOCUMENT_MIMETYPE,
};
const media = {
mimeType: 'text/html',
body: Readable.from([htmlText])
};
const file = await driveClient.files.create({
requestBody,
media,
supportsAllDrives: true,
// fields: 'id',
});
console.log('File Id:', file.data);
if (!file.data.id) return null;
return file.data.id;
} catch(err) {
console.error('Error creating file:', err);
return null;
}
},
// CAUTION // CAUTION
deleteFile: async (driveClient: drive_v3.Drive, fileId: string) => { deleteFile: async (driveClient: drive_v3.Drive, fileId: string) => {
try { try {
@ -147,31 +195,31 @@ export const googleDriveController = {
console.error('Error deleting file:', error); console.error('Error deleting file:', error);
} }
}, },
addContentToDocs: async (docsClient: docs_v1.Docs, documentId: string, content: string): Promise<boolean> => { // addContentToDocs: async (docsClient: docs_v1.Docs, documentId: string, content: string): Promise<boolean> => {
try { // try {
const requestBody: docs_v1.Schema$BatchUpdateDocumentRequest = { // const requestBody: docs_v1.Schema$BatchUpdateDocumentRequest = {
requests: [ // requests: [
{ // {
insertText: { // insertText: {
text: content, // text: content,
location: { // location: {
index: 1, // index: 1,
} // }
} // }
} // }
] // ]
}; // };
const response = await docsClient.documents.batchUpdate({ // const response = await docsClient.documents.batchUpdate({
documentId: documentId, // documentId: documentId,
requestBody: requestBody, // requestBody: requestBody,
}); // });
console.log('Content added to document:', response.data); // console.log('Content added to document:', response.data);
return true; // return true;
} catch (error) { // } catch (error) {
console.error('Error adding content to document:', error); // console.error('Error adding content to document:', error);
return false; // return false;
} // }
}, // },
getLogSheetId: async (driveClient: drive_v3.Drive, sheetsClient: sheets_v4.Sheets, folderId: string, fileName: string): Promise<string | null> => { getLogSheetId: async (driveClient: drive_v3.Drive, sheetsClient: sheets_v4.Sheets, folderId: string, fileName: string): Promise<string | null> => {
try { try {

View file

@ -8,7 +8,7 @@ const hubspotClient = new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN
export const CompanySchema = z.object({ export const CompanySchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
}) });
export const OwnerSchema = z.object({ export const OwnerSchema = z.object({
id: z.string(), id: z.string(),
@ -19,9 +19,16 @@ export type Company = z.infer<typeof CompanySchema>;
export type Owner = z.infer<typeof OwnerSchema>; export type Owner = z.infer<typeof OwnerSchema>;
export const hubspotController = { export const hubspotController = {
check: async() => { check: async(): Promise<boolean | null> => {
const response = await hubspotClient.crm.companies.getAll(); try {
console.log(response.length); const response = await hubspotClient.crm.companies.getAll();
console.log(response.length);
console.log("HubSpot connection check successful.");
return true;
} catch (error) {
console.error("HubSpot connection check failed:", error);
return false;
}
}, },
getCompanies: async(): Promise<Company[] | null> => { getCompanies: async(): Promise<Company[] | null> => {
try { try {
@ -33,9 +40,9 @@ export const hubspotController = {
const response = await hubspotClient.crm.companies.basicApi.getPage(limit, after); const response = await hubspotClient.crm.companies.basicApi.getPage(limit, after);
// console.log(response.results); // console.log(response.results);
const companies: Company[] = response.results.map((company) => CompanySchema.parse({ const companies: Company[] = response.results.map((company) => CompanySchema.parse({
id: company.id, id: company.id,
name: company.properties.name, name: company.properties.name ?? '',
})); }));
allCompanies.push(...companies); allCompanies.push(...companies);
if(response.paging && response.paging.next && response.paging.next.after) { if(response.paging && response.paging.next && response.paging.next.after) {
@ -46,6 +53,7 @@ export const hubspotController = {
} }
return allCompanies; return allCompanies;
} catch (error) { } catch (error) {
console.error("Error fetching companies:", error);
return null; return null;
} }
}, },

View file

@ -10,6 +10,7 @@ import { storageController } from "./storage";
import { CLOUD_STORAGE_MASTER_FOLDER_NAME, DATE_FORMAT, DATETIME_FORMAT, DOCUMENT_MIMETYPE, OWNERS_FILE_NAME, 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 { hubspotController, OwnerSchema } from "./hubspot";
import { fuzzyMatchController } from "./fuzzyMatch"; import { fuzzyMatchController } from "./fuzzyMatch";
import { marked } from "marked";
const VideoInfoSchema = z.looseObject({ const VideoInfoSchema = z.looseObject({
id: z.string(), id: z.string(),
@ -63,8 +64,9 @@ export const processRequest = async (videoInfo: VideoInfo) => {
const sheetsClient = googleDriveController.getSheetsClient(googleAuth); const sheetsClient = googleDriveController.getSheetsClient(googleAuth);
const jstStartsAt = dateController.convertToJst(startsAt); const jstStartsAt = dateController.convertToJst(startsAt);
const meetingDateStr = dateController.getFormattedDate(jstStartsAt, "yyyy年MM月dd日");
const jstEndsAt = dateController.convertToJst(endsAt); const jstEndsAt = dateController.convertToJst(endsAt);
const fileName = fileController.createMinutesFileName(title, hostName, jstStartsAt); const fileName = fileController.createMinutesFileName(title, hostName, meetingDateStr);
const videoUrl = `${MIITEL_URL}app/video/${videoId}`; const videoUrl = `${MIITEL_URL}app/video/${videoId}`;
@ -74,23 +76,21 @@ export const processRequest = async (videoInfo: VideoInfo) => {
const createZip = await fileController.createZip(videoInfo, outputPath, fileName); const createZip = await fileController.createZip(videoInfo, outputPath, fileName);
if(!createZip) throw createCustomError("CREATE_ZIP_FILE_FAILED"); 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")); const logFileId = await callFunctionWithRetry(() => googleDriveController.uploadFile(driveClient, outputPath, MIITEL_REQUEST_LOG_FOLDER_ID, fileName + '.zip', "application/zip"));
if(!logFileId) throw createCustomError("UPLOAD_LOG_FAILED"); if(!logFileId) throw createCustomError("UPLOAD_LOG_FAILED");
// ===== Generate Minutes ===== // ===== Generate Minutes =====
const minutes = await callFunctionWithRetry(() => aiController.generateMinutes(speechRecognition)); const minutes = await callFunctionWithRetry(() => aiController.generateMinutes(speechRecognition));
console.log(minutes);
if (!minutes) throw createCustomError("AI_GENERATION_FAILED"); if (!minutes) throw createCustomError("AI_GENERATION_FAILED");
let content = `会議履歴URL${videoUrl}\n`; const html = await marked.parse(minutes);
content += `担当者:${hostName}\n\n`; let content = `<p>会議履歴URL<a href="${videoUrl}">${videoUrl}</a></p>`;
content += minutes; content += `<p>担当者:${hostName}</p>`;
content += html;
// ===== Upload To Google Drive ===== // ===== Upload To Google Drive =====
const documentId = await callFunctionWithRetry(() => googleDriveController.createNewFile(driveClient, GOOGLE_DRIVE_FOLDER_ID, fileName, DOCUMENT_MIMETYPE)); const documentId = await callFunctionWithRetry(() => googleDriveController.createMinutesDocument(driveClient, GOOGLE_DRIVE_FOLDER_ID, fileName, content));
if (!documentId) throw createCustomError("CREATE_NEW_DOCUMENT_FAILED"); if (!documentId) throw createCustomError("CREATE_NEW_DOCUMENT_FAILED");
const addContentResult = await callFunctionWithRetry(() => googleDriveController.addContentToDocs(docsClient, documentId, content));
if(!addContentResult) throw createCustomError("UPLOAD_MINUTES_FAILED");
// ===== Create Meeting Log at Hubspot ===== // ===== Create Meeting Log at Hubspot =====
const ownersJson = await callFunctionWithRetry(() => storageController.loadJsonFromGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME)); const ownersJson = await callFunctionWithRetry(() => storageController.loadJsonFromGCS(CLOUD_STORAGE_MASTER_FOLDER_NAME, OWNERS_FILE_NAME));
@ -102,7 +102,7 @@ export const processRequest = async (videoInfo: VideoInfo) => {
const extractedCompanyName = fileController.extractCompanyNameFromTitle(title); const extractedCompanyName = fileController.extractCompanyNameFromTitle(title);
const matchedCompany = await fuzzyMatchController.searchMatchedCompany(extractedCompanyName); const matchedCompany = await fuzzyMatchController.searchMatchedCompany(extractedCompanyName);
if(matchedCompany) { if(matchedCompany) {
const createLogResult = await callFunctionWithRetry(() => hubspotController.createMeetingLog(matchedCompany.id, title, ownerId, minutes, startsAt, endsAt)); const createLogResult = await callFunctionWithRetry(() => hubspotController.createMeetingLog(matchedCompany.id, title, ownerId, content, startsAt, endsAt));
if(!createLogResult) throw createCustomError("CREATE_MEETING_LOG_FAILED"); if(!createLogResult) throw createCustomError("CREATE_MEETING_LOG_FAILED");
} }
@ -113,10 +113,9 @@ export const processRequest = async (videoInfo: VideoInfo) => {
if(!sheetId) throw createCustomError("GET_SHEET_ID_FAILED"); if(!sheetId) throw createCustomError("GET_SHEET_ID_FAILED");
const currentJstDateTimeStr = dateController.getCurrentJstTime(DATETIME_FORMAT); const currentJstDateTimeStr = dateController.getCurrentJstTime(DATETIME_FORMAT);
const currentJstDateStr = dateController.getCurrentJstTime(DATE_FORMAT);
const rowData: LogRowData = LogRowDataSchema.parse({ const rowData: LogRowData = LogRowDataSchema.parse({
timestamp: currentJstDateTimeStr, timestamp: currentJstDateTimeStr,
meetingDate: currentJstDateStr, meetingDate: meetingDateStr,
title: title, title: title,
matchedCompanyName: matchedCompany?.name ?? '', matchedCompanyName: matchedCompany?.name ?? '',
ownerName: hostName, ownerName: hostName,
@ -133,7 +132,7 @@ export const processRequest = async (videoInfo: VideoInfo) => {
} }
}; };
export const logUploadProcess = async (videoInfo: VideoInfo) => { export const logUploadProcess = async (videoInfo: VideoInfo): Promise<boolean | null> => {
try { try {
const videoId = videoInfo.id; const videoId = videoInfo.id;
const title = videoInfo.title; const title = videoInfo.title;
@ -144,7 +143,7 @@ export const logUploadProcess = async (videoInfo: VideoInfo) => {
const hostName = videoInfo.host.user_name; const hostName = videoInfo.host.user_name;
const speechRecognition = videoInfo.speech_recognition.raw; const speechRecognition = videoInfo.speech_recognition.raw;
if (accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return; if (accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return true;
// ===== Init ===== // ===== Init =====
const googleAuth = await googleDriveController.getAuth(); const googleAuth = await googleDriveController.getAuth();
@ -153,8 +152,9 @@ export const logUploadProcess = async (videoInfo: VideoInfo) => {
const sheetsClient = googleDriveController.getSheetsClient(googleAuth); const sheetsClient = googleDriveController.getSheetsClient(googleAuth);
const jstStartsAt = dateController.convertToJst(startsAt); const jstStartsAt = dateController.convertToJst(startsAt);
const meetingDateStr = dateController.getFormattedDate(jstStartsAt, "yyyy年MM月dd日");
const jstEndsAt = dateController.convertToJst(endsAt); const jstEndsAt = dateController.convertToJst(endsAt);
const fileName = fileController.createMinutesFileName(title, hostName, jstStartsAt); const fileName = fileController.createMinutesFileName(title, hostName, meetingDateStr);
const videoUrl = `${MIITEL_URL}app/video/${videoId}`; const videoUrl = `${MIITEL_URL}app/video/${videoId}`;
@ -166,6 +166,27 @@ export const logUploadProcess = async (videoInfo: VideoInfo) => {
const logFileId = await callFunctionWithRetry(() => 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"); if(!logFileId) throw createCustomError("UPLOAD_LOG_FAILED");
fs.unlinkSync(outputPath);
return true;
} catch(error) {
console.log(error);
fs.unlinkSync(outputPath);
return false;
}
};
export const testProcess = async () => {
try {
// Google Drive 接続確認
const googleAuth = await googleDriveController.getAuth();
const driveClilent = googleDriveController.getDriveClient(googleAuth);
const driveResponse = await googleDriveController.checkConnection(driveClilent);
if(!driveResponse) throw createCustomError("CONNECT_GOOGLE_DRIVE_FAILED");
// Hubspot 接続確認
const hubspotResponse = await hubspotController.check();
if(!hubspotResponse) throw createCustomError("CONNECT_HUBSPOT_FAILED");
return;
} catch(error) { } catch(error) {
throw error; throw error;
} }

View file

@ -16,7 +16,7 @@ export const storageController = {
}, },
loadFromGCS: async(folder: string, filename: string): Promise<string | null> => { loadFromGCS: async(folder: string, filename: string): Promise<string | null> => {
const file = bucket.file(`${folder}/${filename}`); const file = bucket.file(`${folder}/${filename}`);
// console.log("loading file:", file.name); console.log("loading file:", `${folder}/${filename}`);
try { try {
const [data] = await file.download(); const [data] = await file.download();
return zlib.gunzipSync(data).toString("utf-8"); return zlib.gunzipSync(data).toString("utf-8");
@ -46,15 +46,20 @@ export const storageController = {
}, },
getFileList: async(): Promise<string[] | null> => { getFileList: async(): Promise<string[] | null> => {
try { try {
const files = await bucket.getFiles({ const results = await bucket.getFiles({
prefix: 'request_log/', prefix: 'request_log/',
}); });
const list = []; const files = results[0];
for(const f of files[0]) { files.sort((a, b) => {
// console.log(f.name) if(!a.metadata.timeCreated || !b.metadata.timeCreated) return 0;
list.push(f.name); const timeA = new Date(a.metadata.timeCreated).getTime();
} const timeB = new Date(b.metadata.timeCreated).getTime();
return list; return timeA - timeB;
});
// for(const f of files[0]) {
// list.push(f.name);
// }
return files.map((f) => f.name);
} catch(error) { } catch(error) {
return null; return null;
} }

View file

@ -2,27 +2,36 @@
export const ERROR_DEFINITIONS = { export const ERROR_DEFINITIONS = {
ZOD_FAILED: { code: "E1003", message: "zodのチェックが失敗しました", statusCode: -1 }, ZOD_FAILED: { code: "E1003", message: "zodのチェックが失敗しました", statusCode: -1 },
// ログ ZIP の Google Drive アップロード失敗
UPLOAD_LOG_FAILED: { code: "E3001", message: "ログファイルのアップロードに失敗しました", statusCode: 500 }, // Google Drive関連
// AI による議事録生成失敗
AI_GENERATION_FAILED: { code: "E2001", message: "AIによる議事録生成に失敗しました", statusCode: 500 },
// 議事録Google Docsの作成アップロード失敗 // 議事録Google Docsの作成アップロード失敗
CREATE_NEW_DOCUMENT_FAILED: { code: "E3002", message: "ドキュメント作成に失敗しました", statusCode: 500 }, CONNECT_GOOGLE_DRIVE_FAILED: { code: "E2001", message: "ファイル一覧取得に失敗しました", statusCode: 500 },
UPLOAD_MINUTES_FAILED: { code: "E3003", message: "議事録のアップロードに失敗しました", statusCode: 500 }, GET_FOLDER_ID_FAILED: { code: "E2002", message: "フォルダID取得に失敗しました", statusCode: 500 },
GET_SHEET_ID_FAILED: { code: "E2003", message: "スプレッドシートID取得に失敗しました", statusCode: 500 },
CREATE_NEW_DOCUMENT_FAILED: { code: "E2004", message: "ドキュメント作成に失敗しました", statusCode: 500 },
UPLOAD_MINUTES_FAILED: { code: "E2005", message: "議事録のアップロードに失敗しました", statusCode: 500 },
UPLOAD_LOG_FAILED: { code: "E2006", message: "ログファイルのアップロードに失敗しました", statusCode: 500 },
INSERT_ROW_FAILED: { code: "E2007", message: "シートへのデータ追加に失敗しました", statusCode: 500 },
// Hubspot関連
// オーナー情報の取得失敗 // オーナー情報の取得失敗
CONNECT_HUBSPOT_FAILED: { code: "E3001", message: "ファイル一覧取得に失敗しました", statusCode: 500 },
GET_OWNERS_FAILED: { code: "E3004", message: "オーナー情報の取得に失敗しました", statusCode: 500 }, GET_OWNERS_FAILED: { code: "E3004", message: "オーナー情報の取得に失敗しました", statusCode: 500 },
GET_COMPANIES_FAILED: { code: "E3005", message: "会社情報の取得に失敗しました", statusCode: 500 }, GET_COMPANIES_FAILED: { code: "E3005", message: "会社情報の取得に失敗しました", 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 },
GET_FILES_FAILED: { code: "E3010", message: "ファイルの取得に失敗しました", statusCode: 500 }, GET_FILES_FAILED: { code: "E3010", message: "ファイルの取得に失敗しました", statusCode: 500 },
CREATE_MEETING_LOG_FAILED: { code: "E3011", message: "ミーティングログ作成に失敗しました", statusCode: 500 }, CREATE_MEETING_LOG_FAILED: { code: "E3011", message: "ミーティングログ作成に失敗しました", statusCode: 500 },
// AI による議事録生成失敗
AI_GENERATION_FAILED: { code: "E4001", message: "AIによる議事録生成に失敗しました", statusCode: 500 },
CREATE_ZIP_FILE_FAILED: { code: "E3007", message: "ZIPファイルの作成に失敗しました", statusCode: 500 },
} as const; } as const;
export type ErrorKey = keyof typeof ERROR_DEFINITIONS; export type ErrorKey = keyof typeof ERROR_DEFINITIONS;

View file

@ -30,22 +30,6 @@ resource "google_project_iam_member" "cf_sa_role" {
} }
# Cloud Workflows用サービスアカウント
resource "google_service_account" "workflows_sa" {
project = var.project_id
account_id = "mrt-cloudworkflows-sa"
display_name = "Cloud Workflows SA"
}
# SA
resource "google_project_iam_member" "wf_cf_role" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.workflows_sa.email}"
}
# API Gateway用サービスアカウント # API Gateway用サービスアカウント
resource "google_service_account" "gateway_sa" { resource "google_service_account" "gateway_sa" {
project = var.project_id project = var.project_id
@ -62,17 +46,17 @@ resource "google_project_iam_member" "gateway_role" {
} }
# cloud build用サービスアカウント # Scheduler実行用サービスアカウント
resource "google_service_account" "cloudbuild_sa" { resource "google_service_account" "cf_scheduler_sa" {
project = var.project_id project = var.project_id
account_id = "mrt-cloudbuild-sa" account_id = "mrt-scheduler-sa"
display_name = "Cloud Build 用サービスアカウント" display_name = "Cloud Functions 起動用サービスアカウント"
} }
# SA # SA
resource "google_project_iam_member" "cloudbuild_role" { resource "google_project_iam_member" "scheduler_role" {
for_each = toset(["roles/cloudbuild.builds.builder","roles/storage.objectAdmin", "roles/artifactregistry.writer", "roles/developerconnect.readTokenAccessor", "roles/cloudfunctions.developer","roles/workflows.admin", "roles/iam.serviceAccountUser"]) for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id project = var.project_id
role = each.value role = each.value
member = "serviceAccount:${google_service_account.cloudbuild_sa.email}" member = "serviceAccount:${google_service_account.cf_scheduler_sa.email}"
} }

View file

@ -10,37 +10,22 @@ variable "region" {
variable "function_name" { variable "function_name" {
type = string type = string
default = "mrt-create-log-sheet" default = "generate-minutes"
} }
# Scheduler実行用サービスアカウント
resource "google_service_account" "cf_scheduler_sa" {
project = var.project_id
account_id = "mrt-scheduler-sa"
display_name = "Cloud Functions 起動用サービスアカウント"
}
# SA # 3 Function Scheduler
resource "google_project_iam_member" "scheduler_role" { resource "google_cloud_scheduler_job" "daily_cf_trigger" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cf_scheduler_sa.email}"
}
# 10 Function Scheduler
resource "google_cloud_scheduler_job" "monthly_cf_trigger" {
project = var.project_id project = var.project_id
name = "monthly-cf-trigger" name = "daily-cf-trigger"
description = "Invoke Cloud Function on the 1st of each month at 00:00" description = "Invoke Cloud Function everyday at 03:00"
region = var.region region = var.region
schedule = "0 0 1 * *" schedule = "0 3 * * *"
time_zone = "Asia/Tokyo" time_zone = "Asia/Tokyo"
http_target { http_target {
uri = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}" uri = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}/api/dailyBatch"
http_method = "POST" http_method = "POST"
oidc_token { oidc_token {
service_account_email = google_service_account.cf_scheduler_sa.email service_account_email = google_service_account.cf_scheduler_sa.email