diff --git a/NodeSrv/apps/healthcheck-survey-bot/README.md b/NodeSrv/apps/healthcheck-survey-bot/README.md index fef1af0e..8092404b 100644 --- a/NodeSrv/apps/healthcheck-survey-bot/README.md +++ b/NodeSrv/apps/healthcheck-survey-bot/README.md @@ -27,3 +27,10 @@ Task 5(processFlow.js)の`getValidationColumnNames`はこの構造(`proces - projectId: `LOcxF69Gm4PvnkqA`(`org-master-sync`用プロジェクトを流用) - 列: `resultId`, `targetEmail`, `currentStatus`, `pendingProcesses`, `awaitInput`, `awaitProcessId`, `awaitColumn`(すべてstring型) - 注記: 設計上の8列目`updatedAt`はn8n Data Tableのシステム予約列名(`POST /data-tables`が`400 Column name "updatedAt" is reserved as a system column name.`を返す)のため、列定義には含めていない。各行の更新日時はn8nが自動管理する`updatedAt`メタデータで代替する。 + +- ワークフロー `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実送信)は本タスクでは未実施。次回、捨てレコードでの疎通確認とユーザー確認が必要 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 new file mode 100644 index 00000000..a28eedd4 --- /dev/null +++ b/NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json @@ -0,0 +1,764 @@ +{ + "name": "HC-SUB: プロセス実行と案内送信", + "nodes": [ + { + "id": "trigger-1", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [ + 220, + 300 + ], + "parameters": { + "workflowInputs": { + "values": [ + { + "name": "resultId" + }, + { + "name": "processId" + } + ] + } + } + }, + { + "id": "dt-get-pleasanter_base_url_prod", + "name": "PLEASANTER_BASE_URL取得", + "type": "n8n-nodes-base.dataTable", + "typeVersion": 1, + "position": [ + 440, + 300 + ], + "parameters": { + "operation": "get", + "dataTableId": { + "__rl": true, + "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, + "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": "={{ !!$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": {} + } + }, + { + "id": "code-jwt-claims", + "name": "JWTクレーム組み立て", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3520, + 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}];" + } + }, + { + "id": "jwt-sign", + "name": "Sign JWT", + "type": "n8n-nodes-base.jwt", + "typeVersion": 1, + "position": [ + 3740, + 300 + ], + "parameters": { + "operation": "sign", + "useJson": true, + "claimsJson": "={{ $json.jwtClaims }}", + "options": { + "algorithm": "RS256" + } + }, + "credentials": { + "jwtAuth": { + "id": "Hw0qlEaGfLPnQWp1", + "name": "LINEWORKS Bot Private Key (v4)" + } + } + }, + { + "id": "http-access-token", + "name": "アクセストークン取得", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3960, + 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": {} + } + }, + { + "id": "dt-state-insert", + "name": "状態更新", + "type": "n8n-nodes-base.dataTable", + "typeVersion": 1, + "position": [ + 4400, + 300 + ], + "parameters": { + "operation": "insert", + "dataTableId": { + "__rl": true, + "mode": "id", + "value": "jqMDa2YZTI4f0iQ7" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "resultId": "={{ $('選択肢組み立て').item.json.resultId }}", + "targetEmail": "={{ $('JWTクレーム組み立て').item.json.targetEmail }}", + "currentStatus": "={{ String($('選択肢組み立て').item.json.currentStatus) }}", + "pendingProcesses": "={{ JSON.stringify($('選択肢組み立て').item.json.options) }}", + "awaitInput": "none", + "awaitProcessId": "", + "awaitColumn": "" + }, + "schema": [ + { + "id": "resultId", + "displayName": "resultId", + "required": false, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "targetEmail", + "displayName": "targetEmail", + "required": false, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "currentStatus", + "displayName": "currentStatus", + "required": false, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "pendingProcesses", + "displayName": "pendingProcesses", + "required": false, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "awaitInput", + "displayName": "awaitInput", + "required": false, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "awaitProcessId", + "displayName": "awaitProcessId", + "required": false, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "awaitColumn", + "displayName": "awaitColumn", + "required": false, + "type": "string", + "canBeUsedToMatch": true + } + ] + } + } + } + ], + "connections": { + "Execute Workflow Trigger": { + "main": [ + [ + { + "node": "PLEASANTER_BASE_URL取得", + "type": "main", + "index": 0 + } + ] + ] + }, + "PLEASANTER_BASE_URL取得": { + "main": [ + [ + { + "node": "PLEASANTER_API_KEY取得", + "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 + } + ] + ] + }, + "対象者メール解決": { + "main": [ + [ + { + "node": "JWTクレーム組み立て", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWTクレーム組み立て": { + "main": [ + [ + { + "node": "Sign JWT", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sign JWT": { + "main": [ + [ + { + "node": "アクセストークン取得", + "type": "main", + "index": 0 + } + ] + ] + }, + "アクセストークン取得": { + "main": [ + [ + { + "node": "LINEWORKS送信", + "type": "main", + "index": 0 + } + ] + ] + }, + "LINEWORKS送信": { + "main": [ + [ + { + "node": "状態更新", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} \ No newline at end of file