ken_nogi/NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json
Kenichiro NOGI df0c836573 fix(healthcheck-survey-bot): LINEWORKS token交換のhttpRequestボディ指定を修正
URLSearchParamsを.toString()した上でjson:trueを併用すると
Content-Typeがapplication/jsonに上書きされうる不整合があり、
LINEWORKSアクセストークン取得が失敗する恐れがあったため、
生のURLSearchParamsをbodyに渡しjson:trueと手動Content-Typeを除去。
レスポンスはtypeofで文字列/オブジェクトを判定しJSON.parseする形に変更。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:13:43 +09:00

262 lines
13 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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-config",
"name": "設定値一括取得",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1,
"position": [
440,
300
],
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "id",
"value": "bNkadTyDgDepYx2p"
},
"returnAll": true
}
},
{
"id": "code-pleasanter-query",
"name": "Pleasanter照会・選択肢組み立て",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
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 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}];"
}
},
{
"id": "code-jwt-claims",
"name": "JWTクレーム組み立て",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
300
],
"parameters": {
"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}];"
}
},
{
"id": "jwt-sign",
"name": "Sign JWT",
"type": "n8n-nodes-base.jwt",
"typeVersion": 1,
"position": [
1100,
300
],
"parameters": {
"operation": "sign",
"useJson": true,
"claimsJson": "={{ $json.jwtClaims }}",
"options": {
"algorithm": "RS256"
}
},
"credentials": {
"jwtAuth": {
"id": "Hw0qlEaGfLPnQWp1",
"name": "LINEWORKS Bot Private Key (v4)"
}
}
},
{
"id": "code-lineworks-send",
"name": "アクセストークン取得・LINEWORKS送信",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
300
],
"parameters": {
"jsCode": "const claims = $('JWTクレーム組み立て').item.json;\nconst config = claims.config;\nconst assertion = $json.token;\n\n// LINEWORKS tokenエンドポイントはx-www-form-urlencodedのみ受け付ける。\n// this.helpers.httpRequestはbodyにURLSearchParamsインスタンスを渡すと\n// application/x-www-form-urlencodedへの変換とContent-Type設定を自動で\n// 行うため、素のURLSearchParamsを渡す.toString()しない、ヘッダーも手動指定しない)。\n// jsonオプションは指定しないため、レスポンスは自前でJSON.parseする。\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});\n\nconst tokenRes = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://auth.worksmobile.com/oauth2/v2.0/token',\n body: tokenBody,\n});\nconst tokenJson = typeof tokenRes === 'string' ? JSON.parse(tokenRes) : tokenRes;\nconst accessToken = tokenJson.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}];"
}
},
{
"id": "dt-state-insert",
"name": "状態更新",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1,
"position": [
1540,
300
],
"parameters": {
"operation": "insert",
"dataTableId": {
"__rl": true,
"mode": "id",
"value": "jqMDa2YZTI4f0iQ7"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"resultId": "={{ $json.resultId }}",
"targetEmail": "={{ $json.targetEmail }}",
"currentStatus": "={{ String($json.currentStatus) }}",
"pendingProcesses": "={{ JSON.stringify($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": "設定値一括取得",
"type": "main",
"index": 0
}
]
]
},
"設定値一括取得": {
"main": [
[
{
"node": "Pleasanter照会・選択肢組み立て",
"type": "main",
"index": 0
}
]
]
},
"Pleasanter照会・選択肢組み立て": {
"main": [
[
{
"node": "JWTクレーム組み立て",
"type": "main",
"index": 0
}
]
]
},
"JWTクレーム組み立て": {
"main": [
[
{
"node": "Sign JWT",
"type": "main",
"index": 0
}
]
]
},
"Sign JWT": {
"main": [
[
{
"node": "アクセストークン取得・LINEWORKS送信",
"type": "main",
"index": 0
}
]
]
},
"アクセストークン取得・LINEWORKS送信": {
"main": [
[
{
"node": "状態更新",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}