From 6454e1b46befd28965ed36bb6248443ff1a8dbbd Mon Sep 17 00:00:00 2001 From: kosukesuenaga Date: Wed, 24 Dec 2025 11:36:34 +0900 Subject: [PATCH] 20251224 --- .gitignore | 23 +- _test/test_dev.sh | 16 - api-gateway/openapi.yaml | 319 +++++++++++++++++- cloudbuild_dev.yaml | 196 ----------- cloudbuild_prod.yaml | 193 ----------- functions/generate_minutes/.gcloudignore | 3 + functions/generate_minutes/package.json | 5 +- functions/generate_minutes/serverConfig.ts | 4 +- functions/generate_minutes/src/apiRouter.ts | 117 ++++--- functions/generate_minutes/src/logics/ai.ts | 95 +++++- functions/generate_minutes/src/logics/file.ts | 5 +- .../generate_minutes/src/logics/fuzzyMatch.ts | 8 +- .../src/logics/googleDrive.ts | 104 ++++-- .../generate_minutes/src/logics/hubspot.ts | 22 +- .../generate_minutes/src/logics/process.ts | 53 ++- .../generate_minutes/src/logics/storage.ts | 21 +- .../generate_minutes/src/stores/errorCodes.ts | 33 +- terraform/prod/{initial => IAM}/main.tf | 32 +- terraform/prod/scheduler/main.tf | 29 +- 19 files changed, 667 insertions(+), 611 deletions(-) delete mode 100755 _test/test_dev.sh delete mode 100755 cloudbuild_dev.yaml delete mode 100755 cloudbuild_prod.yaml rename terraform/prod/{initial => IAM}/main.tf (60%) diff --git a/.gitignore b/.gitignore index cce9094..65534ab 100755 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,10 @@ -handle-company-webhook/ - terraform.* .terraform* -IAM/ - -test/ - -venv/ -__pycache__/ -*.csv - -request.json - node_modules/ dist/ -.env_dev -.env -.env_prod -credentials.json -credentials_dev.json -package-lock.json \ No newline at end of file +.env* +credentials* +package-lock.json +*.sh +log/ \ No newline at end of file diff --git a/_test/test_dev.sh b/_test/test_dev.sh deleted file mode 100755 index e17dc07..0000000 --- a/_test/test_dev.sh +++ /dev/null @@ -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" diff --git a/api-gateway/openapi.yaml b/api-gateway/openapi.yaml index c056fc8..2060434 100755 --- a/api-gateway/openapi.yaml +++ b/api-gateway/openapi.yaml @@ -5,10 +5,6 @@ info: version: '1.0.0' schemes: - 'https' -host: 'crate-minutes-gw-a8slsa47.an.gateway.dev' -x-google-endpoints: - - name: 'crate-minutes-gw-a8slsa47.an.gateway.dev' - allowCors: True paths: /create-minutes: post: @@ -70,6 +66,321 @@ paths: Access-Control-Allow-Headers: type: string 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: APIKeyHeader: type: apiKey diff --git a/cloudbuild_dev.yaml b/cloudbuild_dev.yaml deleted file mode 100755 index 757cb07..0000000 --- a/cloudbuild_dev.yaml +++ /dev/null @@ -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', - ] diff --git a/cloudbuild_prod.yaml b/cloudbuild_prod.yaml deleted file mode 100755 index 87f56d4..0000000 --- a/cloudbuild_prod.yaml +++ /dev/null @@ -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', - ] diff --git a/functions/generate_minutes/.gcloudignore b/functions/generate_minutes/.gcloudignore index 8fbd543..e773e5a 100644 --- a/functions/generate_minutes/.gcloudignore +++ b/functions/generate_minutes/.gcloudignore @@ -19,6 +19,9 @@ node_modules .env_prod deploy_function_dev.sh +deploy_function_prod.sh + +files/ package-lock.json diff --git a/functions/generate_minutes/package.json b/functions/generate_minutes/package.json index beeb1e8..dcae153 100644 --- a/functions/generate_minutes/package.json +++ b/functions/generate_minutes/package.json @@ -5,8 +5,8 @@ "scripts": { "build": "tsc", "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", - "watch": "concurrently \"dotenv -e .env_dev -- npm run build -- --watch\" \"dotenv -e .env_dev -- nodemon --watch ./dist/ --exec npm run debug\"" + "debug": "dotenv -e .env_prod -- node --inspect node_modules/.bin/functions-framework --source=dist/index.js --target=helloHttp", + "watch": "concurrently \"dotenv -e .env_prod -- npm run build -- --watch\" \"dotenv -e .env_prod -- nodemon --watch ./dist/ --exec npm run debug\"" }, "devDependencies": { "@google-cloud/functions-framework": "^3.0.0", @@ -29,6 +29,7 @@ "express": "^4.21.2", "fast-fuzzy": "^1.12.0", "googleapis": "^105.0.0", + "marked": "^17.0.1", "zod": "^4.1.13" } } diff --git a/functions/generate_minutes/serverConfig.ts b/functions/generate_minutes/serverConfig.ts index f547d2f..2509d54 100644 --- a/functions/generate_minutes/serverConfig.ts +++ b/functions/generate_minutes/serverConfig.ts @@ -1,6 +1,6 @@ -export const GEMINI_MODEL_ID = "gemini-2.5-flash"; -export const DEBUG = true; +export const GEMINI_MODEL_ID = "gemini-2.5-pro"; +export const DEBUG = false; export const CLOUD_STORAGE_MASTER_FOLDER_NAME = "master"; export const CLOUD_STORAGE_LOG_FOLDER_NAME = "new_request_log"; diff --git a/functions/generate_minutes/src/apiRouter.ts b/functions/generate_minutes/src/apiRouter.ts index 03efbaf..60a190d 100644 --- a/functions/generate_minutes/src/apiRouter.ts +++ b/functions/generate_minutes/src/apiRouter.ts @@ -1,12 +1,15 @@ import express from "express"; import zlib from "zlib"; 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 { 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"; -import { googleDriveController } from "./logics/googleDrive"; +import path from "path"; +import fs from "fs"; +import { fuzzyMatchController } from "./logics/fuzzyMatch"; + const router = express.Router(); @@ -30,8 +33,7 @@ router.post("/miitel", async (req, res) => { return res.status(200).send("ok"); } catch(err) { - responseError(err, res); - return; + return responseError(err, res); } }); @@ -41,18 +43,19 @@ router.post("/dailyBatch", async (req, res) => { console.log("Starting daily batch process..."); // export companies to GCS 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'); // export owners to GCS 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'); res.status(200).send("Daily batch executed."); } catch (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) => { console.log(req.body); 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); - const log = await storageController.loadFromGCS("request_log", meetingId + ".json.gz"); - console.log(log) - res.send(log); + const log = await storageController.loadFromGCS(CLOUD_STORAGE_LOG_FOLDER_NAME, meetingId + ".json.gz"); + if(!log) throw Error(); + 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`); if(!log) throw Error(); const params = MiiTelWebhookSchema.safeParse(JSON.parse(log)); + console.log(params); if(!params.success) throw createCustomError("ZOD_FAILED"); params.data.video.title = newTitle; // console.log(params.data.video) - await processRequest(params.data.video); + // await processRequest(params.data.video); res.send(log); } catch(error) { @@ -92,28 +98,39 @@ router.post("/reExecute", async (req, res) => { // 過去のログを全て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("/logUpload", async (req, res) => { +// try { +// const list = await storageController.getFileList(); +// if(!list) throw createCustomError("GET_FILES_FAILED"); +// console.log("Total files to process:", list.length); +// const failedFiles: string[] = []; +// let count = 0; +// const tmplist = list.slice(1600,1800); +// for(const l of tmplist){ +// console.log(l); +// count++; +// console.log(`Processing file ${count} of ${tmplist.length}`); +// const fileName = l.split('/')[1] +// const log = await storageController.loadFromGCS('request_log', fileName); +// if(!log) { +// failedFiles.push(fileName); +// continue; +// }; +// const parsedLog = MiiTelWebhookSchema.safeParse(JSON.parse(log)); +// 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) => { // console.log(req.body); @@ -126,20 +143,32 @@ router.post("/logUpload", async (req, res) => { 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); + await testProcess(); res.send("ok"); } catch (error) { - console.error("Error in /test endpoint:", error); - res.status(500).send("Error in /test endpoint"); + console.error(error); + 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; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/ai.ts b/functions/generate_minutes/src/logics/ai.ts index 5ba41ad..c16f019 100644 --- a/functions/generate_minutes/src/logics/ai.ts +++ b/functions/generate_minutes/src/logics/ai.ts @@ -8,20 +8,91 @@ const aiClient = new GoogleGenAI({ export const aiController = { generateMinutes: async(text: string): Promise => { const prompt = ` - あなたは議事録作成のプロフェッショナルです。以下の「文字起こし結果」は営業マンが録音した商談の文字起こしです。以下の制約条件に従い、最高の商談報告の議事録を作成してください。 +あなたは、流通・小売・飲食業界向けにシステムを提供する「データコム株式会社」の優秀な営業アシスタントです。 +以下の[文字起こし結果]をもとに、関係者(社内および顧客・パートナー)に共有するための、正確で可読性の高い議事録を作成してください。 - 制約条件: - 1. 文字起こし結果にはAIによる書き起こしミスがある可能性を考慮してください。 - 2. 冒頭に主要な「決定事項」と「アクションアイテム」をまとめてください。 - 3. 議論のポイントを議題ごとに要約してください。 - 4. 見出しや箇条書きを用いて、情報が探しやすい構造で簡潔かつ明瞭に記述してください。 - 5. 要約は500文字以内に収めてください。 - 6. 箇条書き形式で簡潔にまとめてください。 - 7. マークダウン記法は使わず、各項目を「■」や「・」等を使って見やすくしてください。 +# 前提条件 +* **当社(データコム株式会社):** システム開発会社。店舗分析、DX、業務効率化システムなどを提案・提供する立場。 +* **相手:** + * パターンA(エンドユーザー): 小売業(スーパー等)や飲食業の経営層・現場担当者。現場の課題や予算について話す。 + * パターンB(パートナー): システム会社、代理店、POSメーカー等。協業、API連携、紹介案件について話す。 +* **入力データ:** 対面会議のスマホ録音が含まれるため、話者ラベル(Speaker A, B等)は不正確です。必ず「発言内容」から誰が話しているかを判断してください。 - 文字起こし結果: - ${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 { const response = await aiClient.models.generateContent({ diff --git a/functions/generate_minutes/src/logics/file.ts b/functions/generate_minutes/src/logics/file.ts index 42837f0..ae97481 100644 --- a/functions/generate_minutes/src/logics/file.ts +++ b/functions/generate_minutes/src/logics/file.ts @@ -5,9 +5,8 @@ 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}`; + createMinutesFileName: (title: string, hostName: string, meetingDateStr: string): string => { + const fileName = `${meetingDateStr} ${title.replace('/', '')} ${hostName}`; return fileName; }, extractCompanyNameFromTitle: (title: string) => { diff --git a/functions/generate_minutes/src/logics/fuzzyMatch.ts b/functions/generate_minutes/src/logics/fuzzyMatch.ts index 3d90584..fad7f7f 100644 --- a/functions/generate_minutes/src/logics/fuzzyMatch.ts +++ b/functions/generate_minutes/src/logics/fuzzyMatch.ts @@ -15,13 +15,13 @@ export const fuzzyMatchController = { if(!parsedCompanies.success) return null; 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, - name: fuzzyMatchController.normalizeCompanyName(c.name), + name: c.name, })); // Exact Match - const exactMatchedCompany = fuzzyMatchController.searchExactMatchedCompany(normalizedCompanyName, normalizedCompanies); + const exactMatchedCompany = fuzzyMatchController.searchExactMatchedCompany(normalizedCompanyName, companies); // console.log(exactMatchedCompanyId); if(exactMatchedCompany) return exactMatchedCompany; @@ -56,7 +56,7 @@ export const fuzzyMatchController = { }, searchExactMatchedCompany: (companyName: string, companies: Company[]): Company | null => { for(const company of companies) { - if(companyName === company.name) return company; + if(companyName === fuzzyMatchController.normalizeCompanyName(company.name)) return company; }; return null; }, diff --git a/functions/generate_minutes/src/logics/googleDrive.ts b/functions/generate_minutes/src/logics/googleDrive.ts index 509b3bd..eff15a7 100644 --- a/functions/generate_minutes/src/logics/googleDrive.ts +++ b/functions/generate_minutes/src/logics/googleDrive.ts @@ -1,12 +1,12 @@ import { docs_v1, drive_v3, google, sheets_v4 } from "googleapis"; 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 { Readable } from "stream"; 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 MAX_RETRY = 3; export const LogRowDataSchema = z.object({ timestamp: z.string(), @@ -49,10 +49,27 @@ export const googleDriveController = { const docs = google.docs({ version: "v1", auth: auth }); return docs; }, + checkConnection: async (driveClient: drive_v3.Drive): Promise => { + 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 => { try { - // console.log("Uploading file to Google Drive:", filePath); const response = await driveClient.files.create({ requestBody: { name: fileName, @@ -62,6 +79,7 @@ export const googleDriveController = { mimeType: contentType, body: fs.createReadStream(filePath), }, + supportsAllDrives: true, }); if(!response.data.id) return null; return response.data.id; @@ -123,6 +141,7 @@ export const googleDriveController = { const file = await driveClient.files.create({ requestBody, + supportsAllDrives: true, // fields: 'id', }); @@ -134,6 +153,35 @@ export const googleDriveController = { return null; } }, + + createMinutesDocument: async(driveClient: drive_v3.Drive, folderId: string, fileName: string, htmlText: string): Promise => { + 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 deleteFile: async (driveClient: drive_v3.Drive, fileId: string) => { try { @@ -147,31 +195,31 @@ export const googleDriveController = { console.error('Error deleting file:', error); } }, - addContentToDocs: async (docsClient: docs_v1.Docs, documentId: string, content: string): Promise => { - 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; - } - }, + // addContentToDocs: async (docsClient: docs_v1.Docs, documentId: string, content: string): Promise => { + // 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; + // } + // }, getLogSheetId: async (driveClient: drive_v3.Drive, sheetsClient: sheets_v4.Sheets, folderId: string, fileName: string): Promise => { try { diff --git a/functions/generate_minutes/src/logics/hubspot.ts b/functions/generate_minutes/src/logics/hubspot.ts index e48aa1c..05a5c21 100644 --- a/functions/generate_minutes/src/logics/hubspot.ts +++ b/functions/generate_minutes/src/logics/hubspot.ts @@ -8,7 +8,7 @@ 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(), @@ -19,9 +19,16 @@ export type Company = z.infer; export type Owner = z.infer; export const hubspotController = { - check: async() => { - const response = await hubspotClient.crm.companies.getAll(); - console.log(response.length); + check: async(): Promise => { + try { + 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 => { try { @@ -33,9 +40,9 @@ export const hubspotController = { 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, - })); + id: company.id, + name: company.properties.name ?? '', + })); allCompanies.push(...companies); if(response.paging && response.paging.next && response.paging.next.after) { @@ -46,6 +53,7 @@ export const hubspotController = { } return allCompanies; } catch (error) { + console.error("Error fetching companies:", error); return null; } }, diff --git a/functions/generate_minutes/src/logics/process.ts b/functions/generate_minutes/src/logics/process.ts index 0e56103..9675127 100644 --- a/functions/generate_minutes/src/logics/process.ts +++ b/functions/generate_minutes/src/logics/process.ts @@ -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 { hubspotController, OwnerSchema } from "./hubspot"; import { fuzzyMatchController } from "./fuzzyMatch"; +import { marked } from "marked"; const VideoInfoSchema = z.looseObject({ id: z.string(), @@ -63,8 +64,9 @@ export const processRequest = async (videoInfo: VideoInfo) => { const sheetsClient = googleDriveController.getSheetsClient(googleAuth); const jstStartsAt = dateController.convertToJst(startsAt); + const meetingDateStr = dateController.getFormattedDate(jstStartsAt, "yyyy年MM月dd日"); 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}`; @@ -74,23 +76,21 @@ 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 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"); // ===== Generate Minutes ===== const minutes = await callFunctionWithRetry(() => aiController.generateMinutes(speechRecognition)); + console.log(minutes); if (!minutes) throw createCustomError("AI_GENERATION_FAILED"); - let content = `会議履歴URL:${videoUrl}\n`; - content += `担当者:${hostName}\n\n`; - content += minutes; - + const html = await marked.parse(minutes); + let content = `

会議履歴URL:${videoUrl}

`; + content += `

担当者:${hostName}

`; + content += html; // ===== 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"); - const addContentResult = await callFunctionWithRetry(() => googleDriveController.addContentToDocs(docsClient, documentId, content)); - if(!addContentResult) throw createCustomError("UPLOAD_MINUTES_FAILED"); - // ===== Create Meeting Log at Hubspot ===== 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 matchedCompany = await fuzzyMatchController.searchMatchedCompany(extractedCompanyName); 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"); } @@ -113,10 +113,9 @@ export const processRequest = async (videoInfo: VideoInfo) => { 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, + meetingDate: meetingDateStr, title: title, matchedCompanyName: matchedCompany?.name ?? '', 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 => { try { const videoId = videoInfo.id; const title = videoInfo.title; @@ -144,7 +143,7 @@ export const logUploadProcess = async (videoInfo: VideoInfo) => { const hostName = videoInfo.host.user_name; const speechRecognition = videoInfo.speech_recognition.raw; - if (accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return; + if (accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return true; // ===== Init ===== const googleAuth = await googleDriveController.getAuth(); @@ -153,8 +152,9 @@ export const logUploadProcess = async (videoInfo: VideoInfo) => { const sheetsClient = googleDriveController.getSheetsClient(googleAuth); const jstStartsAt = dateController.convertToJst(startsAt); + const meetingDateStr = dateController.getFormattedDate(jstStartsAt, "yyyy年MM月dd日"); 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}`; @@ -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")); 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) { throw error; } diff --git a/functions/generate_minutes/src/logics/storage.ts b/functions/generate_minutes/src/logics/storage.ts index f6ea454..1f27bdb 100644 --- a/functions/generate_minutes/src/logics/storage.ts +++ b/functions/generate_minutes/src/logics/storage.ts @@ -16,7 +16,7 @@ export const storageController = { }, loadFromGCS: async(folder: string, filename: string): Promise => { const file = bucket.file(`${folder}/${filename}`); - // console.log("loading file:", file.name); + console.log("loading file:", `${folder}/${filename}`); try { const [data] = await file.download(); return zlib.gunzipSync(data).toString("utf-8"); @@ -46,15 +46,20 @@ export const storageController = { }, getFileList: async(): Promise => { try { - const files = await bucket.getFiles({ + const results = await bucket.getFiles({ prefix: 'request_log/', }); - const list = []; - for(const f of files[0]) { - // console.log(f.name) - list.push(f.name); - } - return list; + const files = results[0]; + files.sort((a, b) => { + if(!a.metadata.timeCreated || !b.metadata.timeCreated) return 0; + const timeA = new Date(a.metadata.timeCreated).getTime(); + const timeB = new Date(b.metadata.timeCreated).getTime(); + return timeA - timeB; + }); + // for(const f of files[0]) { + // list.push(f.name); + // } + return files.map((f) => f.name); } catch(error) { return null; } diff --git a/functions/generate_minutes/src/stores/errorCodes.ts b/functions/generate_minutes/src/stores/errorCodes.ts index de2c313..eda47ef 100644 --- a/functions/generate_minutes/src/stores/errorCodes.ts +++ b/functions/generate_minutes/src/stores/errorCodes.ts @@ -2,27 +2,36 @@ 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 Drive関連 // 議事録(Google Docs)の作成/アップロード失敗 - CREATE_NEW_DOCUMENT_FAILED: { code: "E3002", message: "ドキュメント作成に失敗しました", statusCode: 500 }, - UPLOAD_MINUTES_FAILED: { code: "E3003", message: "議事録のアップロードに失敗しました", statusCode: 500 }, + CONNECT_GOOGLE_DRIVE_FAILED: { code: "E2001", 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_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 }, 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; export type ErrorKey = keyof typeof ERROR_DEFINITIONS; diff --git a/terraform/prod/initial/main.tf b/terraform/prod/IAM/main.tf similarity index 60% rename from terraform/prod/initial/main.tf rename to terraform/prod/IAM/main.tf index 6d5566d..06e8c12 100755 --- a/terraform/prod/initial/main.tf +++ b/terraform/prod/IAM/main.tf @@ -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用サービスアカウント resource "google_service_account" "gateway_sa" { project = var.project_id @@ -62,17 +46,17 @@ resource "google_project_iam_member" "gateway_role" { } -# cloud build用サービスアカウント -resource "google_service_account" "cloudbuild_sa" { +# Scheduler実行用サービスアカウント +resource "google_service_account" "cf_scheduler_sa" { project = var.project_id - account_id = "mrt-cloudbuild-sa" - display_name = "Cloud Build 用サービスアカウント" + account_id = "mrt-scheduler-sa" + display_name = "Cloud Functions 起動用サービスアカウント" } # 権限を SA に付与 -resource "google_project_iam_member" "cloudbuild_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"]) +resource "google_project_iam_member" "scheduler_role" { + for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"]) project = var.project_id role = each.value - member = "serviceAccount:${google_service_account.cloudbuild_sa.email}" -} \ No newline at end of file + member = "serviceAccount:${google_service_account.cf_scheduler_sa.email}" +} diff --git a/terraform/prod/scheduler/main.tf b/terraform/prod/scheduler/main.tf index e7441a4..0833182 100755 --- a/terraform/prod/scheduler/main.tf +++ b/terraform/prod/scheduler/main.tf @@ -10,37 +10,22 @@ variable "region" { variable "function_name" { 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 に付与 -resource "google_project_iam_member" "scheduler_role" { - 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}" -} - - -# 毎月1日0時に Function を実行する Scheduler ジョブ -resource "google_cloud_scheduler_job" "monthly_cf_trigger" { +# 毎日3時に Function を実行する Scheduler ジョブ +resource "google_cloud_scheduler_job" "daily_cf_trigger" { project = var.project_id - name = "monthly-cf-trigger" - description = "Invoke Cloud Function on the 1st of each month at 00:00" + name = "daily-cf-trigger" + description = "Invoke Cloud Function everyday at 03:00" region = var.region - schedule = "0 0 1 * *" + schedule = "0 3 * * *" time_zone = "Asia/Tokyo" 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" oidc_token { service_account_email = google_service_account.cf_scheduler_sa.email