From 675cf4a321179ab19871fea1d9da7cb25a3817d8 Mon Sep 17 00:00:00 2001 From: Kenichiro NOGI Date: Sat, 5 Sep 2026 18:07:15 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20HC-SUB=E3=83=AF=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=95=E3=83=AD=E3=83=BC=E3=82=927=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=89=E6=A7=8B=E6=88=90=E3=81=AB=E3=83=AA=E3=83=93=E3=83=AB?= =?UTF-8?q?=E3=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新プロジェクトルール(Data Table最小限・Code集約)に合わせ、旧19ノード版 (個別キーごとのData Table Get×7、HTTP Requestノード×6)を廃止。 設定値一括取得→Pleasanter照会・選択肢組み立て→JWTクレーム組み立て→ Sign JWT→アクセストークン取得・LINEWORKS送信→状態更新の7ノードに集約。 デプロイは行わず、ローカルJSON作成・検証のみ(コントローラーが同一ID XRqcykbG2LuAjGG2へPUTする想定)。 Co-Authored-By: Claude Sonnet 5 --- NodeSrv/apps/healthcheck-survey-bot/README.md | 22 +- .../hc-sub-run-process-and-notify.json | 559 +----------------- 2 files changed, 46 insertions(+), 535 deletions(-) diff --git a/NodeSrv/apps/healthcheck-survey-bot/README.md b/NodeSrv/apps/healthcheck-survey-bot/README.md index 0533921d..05276c59 100644 --- a/NodeSrv/apps/healthcheck-survey-bot/README.md +++ b/NodeSrv/apps/healthcheck-survey-bot/README.md @@ -30,10 +30,24 @@ Task 5(processFlow.js)の`getValidationColumnNames`はこの構造(`proces - ワークフロー `HC-SUB: プロセス実行と案内送信`: `XRqcykbG2LuAjGG2` - ファイル: `workflows/hc-sub-run-process-and-notify.json`(Execute Workflow Trigger、入力`{resultId, processId}`) - - active化済み(Execute Workflow Triggerサブワークフローは呼び出し元から実行可能にするためactive必須、`asKvC5LSVg3q8tuU`と同じ運用) - - 秘密情報・SiteId等はすべて`workflow_config_values`(`bNkadTyDgDepYx2p`)から`Data Table`ノードで取得し式参照。ワークフローJSONに秘密値のハードコードなし - - 未検証の暫定実装あり(Task 12で要修正): ①484184(`LINEWORKS_BOT_MASTER_SITE_ID`)からのBotId解決ロジック(実データ構造未調査のため`SiteSettings.BotIds`または`Processes[].BotId`を仮定し、無ければ例外を投げる)、②`/api/users/get`のレスポンス形状(配列/単一オブジェクト両対応の防御的実装) - - Step 4(実行テスト・LINEWORKS実送信)は本タスクでは未実施。次回、捨てレコードでの疎通確認とユーザー確認が必要 + - **プロジェクト全体のn8nワークフロー構築ルール(2026-09-05改訂)に合わせて7ノード構成へ全面リビルド。旧19ノード版(個別キーごとのData Table Getノード×7、HTTP Requestノード×6を並べた構成)を置き換える。** 同じワークフローID`XRqcykbG2LuAjGG2`へのPUTで上書きする想定(デプロイはコントローラーが実施、本タスクではJSON作成・ローカル検証のみ) + - active化済み(Execute Workflow Triggerサブワークフローは呼び出し元から実行可能にするためactive必須、`asKvC5LSVg3q8tuU`と同じ運用)— 再デプロイ後も設定は維持される想定 + - ノード構成(7ノード、リニア接続): + 1. `Execute Workflow Trigger`(入力: `resultId`, `processId`) + 2. `Data Table`「設定値一括取得」: `workflow_config_values`(`bNkadTyDgDepYx2p`)を`filters`無し・`returnAll:true`で一括取得(個別キーごとのGetノードを廃止) + 3. `Code`「Pleasanter照会・選択肢組み立て」: 設定値のMap化、(任意)Process実行、レコード取得、サイト設定取得(getsite)、Botマスタ取得(484184)、対象者メール解決、選択肢組み立てまでを1ノードに集約。Pleasanter API呼び出しはすべて`this.helpers.httpRequest`(n8n Codeノード公式ヘルパー)で発行 + 4. `Code`「JWTクレーム組み立て」: JWTクレームJSON文字列の組み立てとLINEWORKS `button_template`のactions配列組み立てを1ノードに集約 + 5. `JWT`ノード「Sign JWT」(`operation: sign`, `algorithm: RS256`, Credential: 「LINEWORKS Bot Private Key (v4)」id `Hw0qlEaGfLPnQWp1`) + 6. `Code`「アクセストークン取得・LINEWORKS送信」: LINEWORKSアクセストークン取得とメッセージ送信を1ノードに集約(`this.helpers.httpRequest`) + 7. `Data Table`「状態更新」: `healthcheck_bot_state`(`jqMDa2YZTI4f0iQ7`)へ`insert` + - 秘密情報・SiteId等はすべて`workflow_config_values`(`bNkadTyDgDepYx2p`)から「設定値一括取得」1ノードで取得し、直後のCodeノードで`configKey`→`configValue`のMapに変換して参照。ワークフローJSONに秘密値のハードコードなし + - Execute Workflow Triggerの入力(`resultId`/`processId`)は、後続ノードの直接の入力でない箇所では必ず`$('Execute Workflow Trigger').item.json...`のようにノード名を明示して参照している(旧版にあった「Data Tableノード通過後に裸の`$json`を参照してしまうバグ」の再発防止) + - 未検証・要判断の暫定実装あり(Task 12以降で要確認): + ①484184(`LINEWORKS_BOT_MASTER_SITE_ID`)からのBotId解決ロジック(実データ構造未調査のため`SiteSettings.BotIds`または`Processes[].BotId`を仮定し、無ければ例外を投げる。「JWTクレーム組み立て」ノードに実装) + ②`/api/users/get`のレスポンス形状(配列/単一オブジェクト両対応の防御的実装、「Pleasanter照会・選択肢組み立て」ノードに実装) + ③LINEWORKS token/message呼び出しを`this.helpers.httpRequest`で行う際のform-urlencodedボディの正しい渡し方(`URLSearchParams`文字列+`Content-Type`ヘッダー明示という実装にしたが、n8n本体でこの組み合わせが期待通り動くかは実機未検証。ダメな場合は`n8n-nodes-base.httpRequest`ノードの`contentType: "form-urlencoded"`に戻す変更で対応) + ④`Data Table`Insertノードの`columns.mappingMode`/`value`/`schema`の形状(旧19ノード版から流用。旧版はデプロイ実績があるためスキーマ自体は疑わしくないが、実書き込みでの動作は本タスクでは未再検証) + - テスト実行(Step 4相当)は本タスクでは未実施。次回、捨てレコードでの疎通確認とユーザー確認が必要 - ワークフロー `HC-WP: Statusプッシュ通知`: `swXfpoDtwZDT3Hwk` - ファイル: `workflows/hc-wp-status-push.json` diff --git a/NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json b/NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json index c59934c5..ea1a56c0 100644 --- a/NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json +++ b/NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json @@ -24,8 +24,8 @@ } }, { - "id": "dt-get-pleasanter_base_url_prod", - "name": "PLEASANTER_BASE_URL取得", + "id": "dt-get-config", + "name": "設定値一括取得", "type": "n8n-nodes-base.dataTable", "typeVersion": 1, "position": [ @@ -39,314 +39,20 @@ "mode": "id", "value": "bNkadTyDgDepYx2p" }, - "filters": { - "conditions": [ - { - "keyName": "configKey", - "keyValue": "PLEASANTER_BASE_URL_PROD" - } - ] - }, "returnAll": true } }, { - "id": "dt-get-pleasanter_api_key_prod", - "name": "PLEASANTER_API_KEY取得", - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1, + "id": "code-pleasanter-query", + "name": "Pleasanter照会・選択肢組み立て", + "type": "n8n-nodes-base.code", + "typeVersion": 2, "position": [ 660, 300 ], "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "mode": "id", - "value": "bNkadTyDgDepYx2p" - }, - "filters": { - "conditions": [ - { - "keyName": "configKey", - "keyValue": "PLEASANTER_API_KEY_PROD" - } - ] - }, - "returnAll": true - } - }, - { - "id": "dt-get-healthcheck_site_id", - "name": "HEALTHCHECK_SITE_ID取得", - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1, - "position": [ - 880, - 300 - ], - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "mode": "id", - "value": "bNkadTyDgDepYx2p" - }, - "filters": { - "conditions": [ - { - "keyName": "configKey", - "keyValue": "HEALTHCHECK_SITE_ID" - } - ] - }, - "returnAll": true - } - }, - { - "id": "if-processid-exists", - "name": "IF processId exists", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1100, - 300 - ], - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "loose", - "version": 2 - }, - "conditions": [ - { - "id": "cond-processid-truthy", - "leftValue": "={{ !!$('Execute Workflow Trigger').item.json.processId }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - } - }, - { - "id": "http-process-run", - "name": "Process実行", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1320, - 300 - ], - "parameters": { - "method": "POST", - "url": "={{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('Execute Workflow Trigger').item.json.resultId }}/update", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ ApiVersion: 1.1, ApiKey: $('PLEASANTER_API_KEY取得').item.json.configValue, ProcessId: Number($('Execute Workflow Trigger').item.json.processId) }) }}", - "options": {} - } - }, - { - "id": "http-record-get", - "name": "レコード取得", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1540, - 300 - ], - "parameters": { - "method": "POST", - "url": "={{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('Execute Workflow Trigger').item.json.resultId }}/get", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ ApiVersion: 1.1, ApiKey: $('PLEASANTER_API_KEY取得').item.json.configValue }) }}", - "options": {} - } - }, - { - "id": "http-site-settings-get", - "name": "サイト設定取得", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1760, - 300 - ], - "parameters": { - "method": "POST", - "url": "={{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('HEALTHCHECK_SITE_ID取得').item.json.configValue }}/getsite", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ ApiVersion: 1.1, ApiKey: $('PLEASANTER_API_KEY取得').item.json.configValue }) }}", - "options": {} - } - }, - { - "id": "code-build-options", - "name": "選択肢組み立て", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1980, - 300 - ], - "parameters": { - "jsCode": "function isUnsetSentinel(value) {\n return typeof value === \"string\" && value.startsWith(\"1899\");\n}\nfunction fillTemplate(template, columns, valueHash) {\n const labelToColumnName = new Map();\n for (const column of columns) {\n if (column.LabelText) labelToColumnName.set(column.LabelText, column.ColumnName);\n }\n return template.replace(/\\{([^{}]+)\\}/g, (matched, label) => {\n const columnName = labelToColumnName.get(label);\n if (!columnName) return matched;\n const value = valueHash[columnName];\n if (value === undefined || value === null || value === \"\" || isUnsetSentinel(value)) return \"未設定\";\n return String(value);\n });\n}\nfunction extractProcessesForStatus(processes, status) {\n return processes.filter((p) => p.CurrentStatus === status || p.CurrentStatus === -1);\n}\n\nconst record = $('レコード取得').item.json.Response.Data;\nconst siteSettings = $('サイト設定取得').item.json.Response.Data.SiteSettings;\nconst columns = siteSettings.Columns || [];\nconst processes = siteSettings.Processes || [];\n\nconst valueHash = {\n ...record.ClassHash, ...record.NumHash, ...record.DateHash, ...record.DescriptionHash,\n};\n\nconst candidates = extractProcessesForStatus(processes, record.Status);\nconst options = candidates.map((p) => ({\n processId: p.Id,\n label: p.DisplayName || p.Name,\n tooltip: fillTemplate(p.ToolTip || \"\", columns, valueHash),\n}));\n\nreturn [{\n json: {\n resultId: record.ResultId,\n currentStatus: record.Status,\n options,\n classCUserId: record.ClassHash.ClassC,\n },\n}];" - } - }, - { - "id": "dt-get-lw_bot_client_id", - "name": "LW_BOT_CLIENT_ID取得", - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1, - "position": [ - 2200, - 300 - ], - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "mode": "id", - "value": "bNkadTyDgDepYx2p" - }, - "filters": { - "conditions": [ - { - "keyName": "configKey", - "keyValue": "LW_BOT_CLIENT_ID" - } - ] - }, - "returnAll": true - } - }, - { - "id": "dt-get-lw_bot_client_secret", - "name": "LW_BOT_CLIENT_SECRET取得", - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1, - "position": [ - 2420, - 300 - ], - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "mode": "id", - "value": "bNkadTyDgDepYx2p" - }, - "filters": { - "conditions": [ - { - "keyName": "configKey", - "keyValue": "LW_BOT_CLIENT_SECRET" - } - ] - }, - "returnAll": true - } - }, - { - "id": "dt-get-lw_bot_service_account", - "name": "LW_BOT_SERVICE_ACCOUNT取得", - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1, - "position": [ - 2640, - 300 - ], - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "mode": "id", - "value": "bNkadTyDgDepYx2p" - }, - "filters": { - "conditions": [ - { - "keyName": "configKey", - "keyValue": "LW_BOT_SERVICE_ACCOUNT" - } - ] - }, - "returnAll": true - } - }, - { - "id": "dt-get-lineworks_bot_master_site_id", - "name": "LINEWORKS_BOT_MASTER_SITE_ID取得", - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1, - "position": [ - 2860, - 300 - ], - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "mode": "id", - "value": "bNkadTyDgDepYx2p" - }, - "filters": { - "conditions": [ - { - "keyName": "configKey", - "keyValue": "LINEWORKS_BOT_MASTER_SITE_ID" - } - ] - }, - "returnAll": true - } - }, - { - "id": "http-bot-master-get", - "name": "Botマスタ取得", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3080, - 300 - ], - "parameters": { - "method": "POST", - "url": "={{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('LINEWORKS_BOT_MASTER_SITE_ID取得').item.json.configValue }}/getsite", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ ApiVersion: 1.1, ApiKey: $('PLEASANTER_API_KEY取得').item.json.configValue }) }}", - "options": {} - } - }, - { - "id": "http-target-user-get", - "name": "対象者メール解決", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3300, - 300 - ], - "parameters": { - "method": "POST", - "url": "={{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/users/get", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ ApiVersion: 1.1, ApiKey: $('PLEASANTER_API_KEY取得').item.json.configValue, View: { ApiGetMailAddresses: true }, Where: { UserId: $('選択肢組み立て').item.json.classCUserId } }) }}", - "options": {} + "jsCode": "function isUnsetSentinel(value) {\n return typeof value === \"string\" && value.startsWith(\"1899\");\n}\nfunction fillTemplate(template, columns, valueHash) {\n const labelToColumnName = new Map();\n for (const column of columns) {\n if (column.LabelText) labelToColumnName.set(column.LabelText, column.ColumnName);\n }\n return template.replace(/\\{([^{}]+)\\}/g, (matched, label) => {\n const columnName = labelToColumnName.get(label);\n if (!columnName) return matched;\n const value = valueHash[columnName];\n if (value === undefined || value === null || value === \"\" || isUnsetSentinel(value)) return \"未設定\";\n return String(value);\n });\n}\nfunction extractProcessesForStatus(processes, status) {\n return processes.filter((p) => p.CurrentStatus === status || p.CurrentStatus === -1);\n}\n\nconst configRows = $('設定値一括取得').all().map((item) => item.json);\nconst config = Object.fromEntries(configRows.map((row) => [row.configKey, row.configValue]));\n\nconst trigger = $('Execute Workflow Trigger').item.json;\nconst baseUrl = config.PLEASANTER_BASE_URL_PROD;\nconst apiKey = config.PLEASANTER_API_KEY_PROD;\n\nasync function pleasanterPost(path, body) {\n const res = await this.helpers.httpRequest({\n method: \"POST\",\n url: `${baseUrl}${path}`,\n body: { ApiVersion: 1.1, ApiKey: apiKey, ...body },\n json: true,\n });\n return res;\n}\n\nif (trigger.processId) {\n await pleasanterPost.call(this, `api/items/${trigger.resultId}/update`, { ProcessId: trigger.processId });\n}\n\nconst recordRes = await pleasanterPost.call(this, `api/items/${trigger.resultId}/get`, {});\nconst record = recordRes.Response.Data;\n\nconst siteRes = await pleasanterPost.call(this, `api/items/${config.HEALTHCHECK_SITE_ID}/getsite`, {});\nconst siteSettings = siteRes.Response.Data.SiteSettings;\nconst columns = siteSettings.Columns || [];\nconst processes = siteSettings.Processes || [];\n\nconst botMasterRes = await pleasanterPost.call(this, `api/items/${config.LINEWORKS_BOT_MASTER_SITE_ID}/getsite`, {});\n// TODO(Task 12): 484184(LINEWORKS_BOT_MASTER_SITE_ID)の実データ構造を見てBotIdの解決方法を確定させる。\n// 暫定: SiteSettings.BotIds配列 または Processes[].BotId のいずれかを想定し、\n// 生データのまま後段(JWTクレーム組み立て)へ渡して解決する。\nconst botMasterSiteSettings = botMasterRes.Response.Data.SiteSettings;\n\nconst valueHash = {\n ...record.ClassHash, ...record.NumHash, ...record.DateHash, ...record.DescriptionHash,\n};\n\nconst candidates = extractProcessesForStatus(processes, record.Status);\nconst options = candidates.map((p) => ({\n processId: p.Id,\n label: p.DisplayName || p.Name,\n tooltip: fillTemplate(p.ToolTip || \"\", columns, valueHash),\n}));\n\nconst userRes = await pleasanterPost.call(this, `api/users/get`, {\n View: { ApiGetMailAddresses: true },\n Where: { UserId: record.ClassHash.ClassC },\n});\n// Pleasanter /api/users/get のレスポンス形状は実機未検証のため、配列/単一\n// オブジェクトどちらでも動くように防御的に処理する。\nconst usersResponseData = userRes.Response.Data;\nconst userRecord = Array.isArray(usersResponseData) ? usersResponseData[0] : usersResponseData;\nconst targetEmail = userRecord && (userRecord.MailAddress\n || (Array.isArray(userRecord.MailAddresses) && userRecord.MailAddresses[0]));\nif (!targetEmail) {\n throw new Error('対象者のメールアドレスを解決できませんでした。');\n}\n\nreturn [{\n json: {\n resultId: record.ResultId,\n currentStatus: record.Status,\n options,\n targetEmail,\n config,\n botMasterSiteSettings,\n },\n}];" } }, { @@ -355,11 +61,11 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3520, + 880, 300 ], "parameters": { - "jsCode": "const now = Math.floor(Date.now() / 1000);\nconst jwtClaims = JSON.stringify({\n iss: $('LW_BOT_CLIENT_ID取得').item.json.configValue,\n sub: $('LW_BOT_SERVICE_ACCOUNT取得').item.json.configValue,\n iat: now,\n exp: now + 3600,\n aud: 'https://auth.worksmobile.com/oauth2/v2.0/token',\n});\n\n// --- Bot ID解決 ---\n// TODO(Task 12): 484184(LINEWORKS_BOT_MASTER_SITE_ID)の実データ構造は未調査\n// (このワークフローは新規追加のBrief上、実データ確認前に構築している)。\n// Task 12で508971のProcess⇔Bot対応表を設計する際、実データに合わせて\n// この関数を書き換えるか、Execute Workflow Triggerの入力にbotIdを追加して\n// 呼び出し元(WP/WA)から明示的に渡す方式へ変更すること。\n// 暫定実装: getsiteのSiteSettings直下にBotIds配列がある、または\n// Processes[].BotIdが定義されているケースを想定し、最初の1件を採用する。\n// どちらの構造も無ければ、誤ったBotへの送信を避けるため明示的にエラーとする。\nconst botMasterSiteSettings = $('Botマスタ取得').item.json.Response.Data.SiteSettings;\nconst candidateBotIds = botMasterSiteSettings.BotIds\n || (botMasterSiteSettings.Processes || []).map((p) => p.BotId).filter(Boolean);\nconst botId = candidateBotIds && candidateBotIds[0];\nif (!botId) {\n throw new Error('BotIdを484184(LINEWORKS_BOT_MASTER_SITE_ID)から解決できませんでした。Task 12でBot選択ロジックを実装してください。');\n}\n\n// --- 送信先メールアドレス解決 ---\n// Pleasanter /api/users/get のレスポンス形状は実機未検証のため、配列/単一\n// オブジェクトどちらでも動くように防御的に処理する。\nconst usersResponseData = $('対象者メール解決').item.json.Response.Data;\nconst userRecord = Array.isArray(usersResponseData) ? usersResponseData[0] : usersResponseData;\nif (!userRecord) {\n throw new Error('対象者メール解決レスポンスからユーザー情報を取得できませんでした。');\n}\nconst targetEmail = userRecord.MailAddress\n || (Array.isArray(userRecord.MailAddresses) && userRecord.MailAddresses[0])\n || null;\nif (!targetEmail) {\n throw new Error('対象者のメールアドレスを解決できませんでした。');\n}\n\nconst apiUrl = `https://www.worksapis.com/v1.0/bots/${botId}/users/${targetEmail}/messages`;\n\n// --- button_template組み立て ---\nconst options = $('選択肢組み立て').item.json.options;\nconst resultId = $('選択肢組み立て').item.json.resultId;\nconst actions = options.map((opt) => ({\n type: 'message',\n label: opt.label,\n postback: JSON.stringify({ resultId, processId: opt.processId }),\n displayText: opt.label,\n}));\n\nconst messageContent = {\n type: 'button_template',\n contentText: options.map((opt) => `${opt.label}: ${opt.tooltip}`).join('\\n') || '対応可能な操作がありません',\n actions,\n};\n\nreturn [{\n json: {\n jwtClaims,\n apiUrl,\n messageContent,\n targetEmail,\n resultId,\n },\n}];" + "jsCode": "const now = Math.floor(Date.now() / 1000);\nconst { config, botMasterSiteSettings, options, resultId, currentStatus, targetEmail } = $json;\n\nconst jwtClaims = JSON.stringify({\n iss: config.LW_BOT_CLIENT_ID,\n sub: config.LW_BOT_SERVICE_ACCOUNT,\n iat: now,\n exp: now + 3600,\n aud: 'https://auth.worksmobile.com/oauth2/v2.0/token',\n});\n\n// --- Bot ID解決 ---\n// TODO(Task 12): 484184(LINEWORKS_BOT_MASTER_SITE_ID)の実データ構造は未調査\n// (このワークフローは実データ確認前に構築している)。\n// Task 12で508971のProcess⇔Bot対応表を設計する際、実データに合わせて\n// この解決方法を書き換えるか、Execute Workflow Triggerの入力にbotIdを追加して\n// 呼び出し元(WP/WA)から明示的に渡す方式へ変更すること。\n// 暫定実装: getsiteのSiteSettings直下にBotIds配列がある、または\n// Processes[].BotIdが定義されているケースを想定し、最初の1件を採用する。\n// どちらの構造も無ければ、誤ったBotへの送信を避けるため明示的にエラーとする。\nconst candidateBotIds = botMasterSiteSettings.BotIds\n || (botMasterSiteSettings.Processes || []).map((p) => p.BotId).filter(Boolean);\nconst botId = candidateBotIds && candidateBotIds[0];\nif (!botId) {\n throw new Error('BotIdを484184(LINEWORKS_BOT_MASTER_SITE_ID)から解決できませんでした。Task 12でBot選択ロジックを実装してください。');\n}\n\nconst apiUrl = `https://www.worksapis.com/v1.0/bots/${botId}/users/${targetEmail}/messages`;\n\n// --- button_template組み立て ---\nconst actions = options.map((opt) => ({\n type: 'message',\n label: opt.label,\n postback: JSON.stringify({ resultId, processId: opt.processId }),\n displayText: opt.label,\n}));\n\nconst messageContent = {\n type: 'button_template',\n contentText: options.map((opt) => `${opt.label}: ${opt.tooltip}`).join('\\n') || '対応可能な操作がありません',\n actions,\n};\n\nreturn [{\n json: {\n jwtClaims,\n apiUrl,\n messageContent,\n targetEmail,\n resultId,\n currentStatus,\n options,\n config,\n },\n}];" } }, { @@ -368,7 +74,7 @@ "type": "n8n-nodes-base.jwt", "typeVersion": 1, "position": [ - 3740, + 1100, 300 ], "parameters": { @@ -387,75 +93,16 @@ } }, { - "id": "http-access-token", - "name": "アクセストークン取得", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, + "id": "code-lineworks-send", + "name": "アクセストークン取得・LINEWORKS送信", + "type": "n8n-nodes-base.code", + "typeVersion": 2, "position": [ - 3960, + 1320, 300 ], "parameters": { - "method": "POST", - "url": "https://auth.worksmobile.com/oauth2/v2.0/token", - "sendBody": true, - "contentType": "form-urlencoded", - "bodyParameters": { - "parameters": [ - { - "name": "assertion", - "value": "={{ $json.token }}" - }, - { - "name": "grant_type", - "value": "urn:ietf:params:oauth:grant-type:jwt-bearer" - }, - { - "name": "client_id", - "value": "={{ $('LW_BOT_CLIENT_ID取得').item.json.configValue }}" - }, - { - "name": "client_secret", - "value": "={{ $('LW_BOT_CLIENT_SECRET取得').item.json.configValue }}" - }, - { - "name": "scope", - "value": "bot" - } - ] - }, - "options": {} - } - }, - { - "id": "http-lineworks-send", - "name": "LINEWORKS送信", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 4180, - 300 - ], - "parameters": { - "method": "POST", - "url": "={{ $('JWTクレーム組み立て').item.json.apiUrl }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Authorization", - "value": "=Bearer {{ $json.access_token }}" - }, - { - "name": "Content-Type", - "value": "application/json;charset=UTF-8" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ content: $('JWTクレーム組み立て').item.json.messageContent }) }}", - "options": {} + "jsCode": "const claims = $('JWTクレーム組み立て').item.json;\nconst config = claims.config;\nconst assertion = $json.token;\n\n// LINEWORKS token/message呼び出しはx-www-form-urlencoded / JSONのどちらも\n// this.helpers.httpRequestのbodyに素の値を渡す形は実機未検証のため、\n// URLSearchParamsで明示的にエンコードしてContent-Typeも明示する。\nconst tokenBody = new URLSearchParams({\n assertion,\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n client_id: config.LW_BOT_CLIENT_ID,\n client_secret: config.LW_BOT_CLIENT_SECRET,\n scope: 'bot',\n}).toString();\n\nconst tokenRes = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://auth.worksmobile.com/oauth2/v2.0/token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: tokenBody,\n json: true,\n});\nconst accessToken = tokenRes.access_token;\nif (!accessToken) {\n throw new Error('LINEWORKSアクセストークンの取得に失敗しました。');\n}\n\nconst sendRes = await this.helpers.httpRequest({\n method: 'POST',\n url: claims.apiUrl,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json;charset=UTF-8',\n },\n body: { content: claims.messageContent },\n json: true,\n});\n\nreturn [{\n json: {\n resultId: claims.resultId,\n currentStatus: claims.currentStatus,\n options: claims.options,\n targetEmail: claims.targetEmail,\n lineworksResponse: sendRes,\n },\n}];" } }, { @@ -464,7 +111,7 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1, "position": [ - 4400, + 1540, 300 ], "parameters": { @@ -477,10 +124,10 @@ "columns": { "mappingMode": "defineBelow", "value": { - "resultId": "={{ $('選択肢組み立て').item.json.resultId }}", - "targetEmail": "={{ $('JWTクレーム組み立て').item.json.targetEmail }}", - "currentStatus": "={{ String($('選択肢組み立て').item.json.currentStatus) }}", - "pendingProcesses": "={{ JSON.stringify($('選択肢組み立て').item.json.options) }}", + "resultId": "={{ $json.resultId }}", + "targetEmail": "={{ $json.targetEmail }}", + "currentStatus": "={{ String($json.currentStatus) }}", + "pendingProcesses": "={{ JSON.stringify($json.options) }}", "awaitInput": "none", "awaitProcessId": "", "awaitColumn": "" @@ -545,164 +192,25 @@ "main": [ [ { - "node": "PLEASANTER_BASE_URL取得", + "node": "設定値一括取得", "type": "main", "index": 0 } ] ] }, - "PLEASANTER_BASE_URL取得": { + "設定値一括取得": { "main": [ [ { - "node": "PLEASANTER_API_KEY取得", + "node": "Pleasanter照会・選択肢組み立て", "type": "main", "index": 0 } ] ] }, - "PLEASANTER_API_KEY取得": { - "main": [ - [ - { - "node": "HEALTHCHECK_SITE_ID取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "HEALTHCHECK_SITE_ID取得": { - "main": [ - [ - { - "node": "IF processId exists", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF processId exists": { - "main": [ - [ - { - "node": "Process実行", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "レコード取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "Process実行": { - "main": [ - [ - { - "node": "レコード取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "レコード取得": { - "main": [ - [ - { - "node": "サイト設定取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "サイト設定取得": { - "main": [ - [ - { - "node": "選択肢組み立て", - "type": "main", - "index": 0 - } - ] - ] - }, - "選択肢組み立て": { - "main": [ - [ - { - "node": "LW_BOT_CLIENT_ID取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "LW_BOT_CLIENT_ID取得": { - "main": [ - [ - { - "node": "LW_BOT_CLIENT_SECRET取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "LW_BOT_CLIENT_SECRET取得": { - "main": [ - [ - { - "node": "LW_BOT_SERVICE_ACCOUNT取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "LW_BOT_SERVICE_ACCOUNT取得": { - "main": [ - [ - { - "node": "LINEWORKS_BOT_MASTER_SITE_ID取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "LINEWORKS_BOT_MASTER_SITE_ID取得": { - "main": [ - [ - { - "node": "Botマスタ取得", - "type": "main", - "index": 0 - } - ] - ] - }, - "Botマスタ取得": { - "main": [ - [ - { - "node": "対象者メール解決", - "type": "main", - "index": 0 - } - ] - ] - }, - "対象者メール解決": { + "Pleasanter照会・選択肢組み立て": { "main": [ [ { @@ -728,25 +236,14 @@ "main": [ [ { - "node": "アクセストークン取得", + "node": "アクセストークン取得・LINEWORKS送信", "type": "main", "index": 0 } ] ] }, - "アクセストークン取得": { - "main": [ - [ - { - "node": "LINEWORKS送信", - "type": "main", - "index": 0 - } - ] - ] - }, - "LINEWORKS送信": { + "アクセストークン取得・LINEWORKS送信": { "main": [ [ { @@ -761,4 +258,4 @@ "settings": { "executionOrder": "v1" } -} \ No newline at end of file +}