n8nのINode仕様ではalwaysOutputDataはparameters内でなくノード オブジェクト直下のプロパティ。前回の修正でparameters内に置いて しまい効果が無かった(0件マッチ時に後続Codeノードが実行されない 問題が未解決のままだった)ため、正しい位置へ移動。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
455 lines
25 KiB
JSON
455 lines
25 KiB
JSON
{
|
||
"name": "HC-WA: LINEWORKS応答受信",
|
||
"nodes": [
|
||
{
|
||
"id": "webhook-lineworks-response",
|
||
"name": "Webhook",
|
||
"type": "n8n-nodes-base.webhook",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
200,
|
||
400
|
||
],
|
||
"webhookId": "healthcheck-lineworks-response",
|
||
"parameters": {
|
||
"httpMethod": "POST",
|
||
"path": "healthcheck-lineworks-response",
|
||
"responseMode": "responseNode",
|
||
"options": {
|
||
"rawBody": true
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "dt-get-config",
|
||
"name": "設定値一括取得",
|
||
"type": "n8n-nodes-base.dataTable",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
420,
|
||
400
|
||
],
|
||
"parameters": {
|
||
"operation": "get",
|
||
"dataTableId": {
|
||
"__rl": true,
|
||
"mode": "id",
|
||
"value": "bNkadTyDgDepYx2p"
|
||
},
|
||
"returnAll": true
|
||
}
|
||
},
|
||
{
|
||
"id": "code-verify-and-identify",
|
||
"name": "署名検証・対象レコード特定",
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
640,
|
||
400
|
||
],
|
||
"parameters": {
|
||
"jsCode": "const crypto = require(\"crypto\");\nfunction normalizeSignature(value) {\n return String(value || \"\").trim().replace(/^sha256=/i, \"\");\n}\nfunction safeEqual(a, b) {\n const ab = Buffer.from(String(a), \"utf8\");\n const bb = Buffer.from(String(b), \"utf8\");\n if (ab.length !== bb.length) return false;\n return crypto.timingSafeEqual(ab, bb);\n}\nfunction verifySignature(rawBody, headerSignature, botSecret) {\n const headerSig = normalizeSignature(headerSignature);\n if (!headerSig || !botSecret) return false;\n const payload = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody), \"utf8\");\n const expected = crypto.createHmac(\"sha256\", botSecret).update(payload).digest(\"base64\");\n return safeEqual(headerSig, expected);\n}\n\nconst configRows = $('設定値一括取得').all().map((item) => item.json);\nconst config = Object.fromEntries(configRows.map((row) => [row.configKey, row.configValue]));\n\nconst item = $('Webhook').first();\nconst headers = item.json.headers || {};\nconst rawBody = item.binary && item.binary.data\n ? Buffer.from(item.binary.data.data, \"base64\")\n : Buffer.from(JSON.stringify(item.json.body || {}), \"utf8\");\n\nif (!verifySignature(rawBody, headers[\"x-works-signature\"], config.LINEWORKS_BOT_SECRET)) {\n throw new Error(\"Unauthorized: signature mismatch\");\n}\n\nconst body = item.json.body || {};\nconst source = body.source || {};\nconst content = body.content || {};\nconst targetEmail = source.userId;\n\nasync function pleasanterPost(path, reqBody) {\n return this.helpers.httpRequest({\n method: \"POST\",\n url: `${config.PLEASANTER_BASE_URL_PROD}${path}`,\n body: { ApiVersion: 1.1, ApiKey: config.PLEASANTER_API_KEY_PROD, ...reqBody },\n json: true,\n });\n}\n\nconst userRes = await pleasanterPost.call(this, \"api/users/get\", {\n View: { ApiGetMailAddresses: true },\n Where: { MailAddress: targetEmail },\n});\nconst userId = userRes.Response.Data[0]?.UserId;\nif (!userId) throw new Error(`対象者が見つかりません: ${targetEmail}`);\n\nconst recordRes = await pleasanterPost.call(this, `api/items/${config.HEALTHCHECK_SITE_ID}/get`, {\n View: {\n ColumnFilterHash: { ClassC: String(userId) },\n ColumnFilterSearchTypes: { ClassC: \"ExactMatch\" },\n },\n});\nconst candidates = (recordRes.Response.Data || []).filter((r) => r.Status !== 900 && r.Status !== 910);\nif (candidates.length !== 1) {\n throw new Error(`対象レコードを一意に特定できません: ${candidates.length}件`);\n}\nconst record = candidates[0];\n\nreturn [{\n json: {\n config,\n resultId: record.ResultId,\n currentStatus: record.Status,\n contentType: content.type,\n text: content.type === \"text\" ? content.text : null,\n fileId: content.type === \"file\" ? content.fileId : null,\n },\n}];"
|
||
}
|
||
},
|
||
{
|
||
"id": "dt-get-state",
|
||
"name": "待機状態取得",
|
||
"type": "n8n-nodes-base.dataTable",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
860,
|
||
400
|
||
],
|
||
"alwaysOutputData": true,
|
||
"parameters": {
|
||
"operation": "get",
|
||
"dataTableId": {
|
||
"__rl": true,
|
||
"mode": "id",
|
||
"value": "jqMDa2YZTI4f0iQ7"
|
||
},
|
||
"filters": {
|
||
"conditions": [
|
||
{
|
||
"keyName": "resultId",
|
||
"keyValue": "={{ $json.resultId }}"
|
||
}
|
||
]
|
||
},
|
||
"returnAll": true
|
||
}
|
||
},
|
||
{
|
||
"id": "code-decide-action",
|
||
"name": "分岐処理・アクション決定",
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1080,
|
||
400
|
||
],
|
||
"parameters": {
|
||
"jsCode": "function matchProcessByLabel(processes, text) {\n const trimmed = String(text ?? \"\").trim();\n return processes.find((p) => (p.label || p.DisplayName || p.Name) === trimmed) || null;\n}\nfunction getValidationColumnNames(process) {\n if (!Array.isArray(process.ValidateInputs)) return [];\n return process.ValidateInputs.map((v) => v.ColumnName).filter(Boolean);\n}\nfunction classifyAwaitInput(process) {\n const columnNames = getValidationColumnNames(process);\n if (columnNames.length === 0) return { awaitInput: \"none\", column: null };\n const column = columnNames[0];\n if (column.startsWith(\"Date\")) return { awaitInput: \"date\", column };\n if (column.startsWith(\"Attachments\")) return { awaitInput: \"file\", column };\n return { awaitInput: \"none\", column: null };\n}\n\nconst ERA_INFO = { \"令和\": 2018, \"平成\": 1988, \"昭和\": 1925, \"大正\": 1911 };\nconst ERA_ALIASES = { R: \"令和\", H: \"平成\", S: \"昭和\", T: \"大正\" };\nfunction pad2(value) { return String(value).padStart(2, \"0\"); }\nfunction finalizeDateParts(year, month, day) {\n if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {\n throw new Error(\"日付形式で回答してください(例: 2026-03-01)\");\n }\n const date = new Date(year, month - 1, day);\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n throw new Error(\"存在しない日付です\");\n }\n return `${year}-${pad2(month)}-${pad2(day)}`;\n}\nfunction tryParseEraDate(compact) {\n const match = compact.match(/^(令和|平成|昭和|大正|R|H|S|T)(.+)$/i);\n if (!match) return null;\n let era = match[1];\n if (/^[RHST]$/i.test(era)) era = ERA_ALIASES[era.toUpperCase()] || era;\n if (!ERA_INFO[era]) return null;\n const normalized = match[2].replace(/年/g, \"-\").replace(/月/g, \"-\").replace(/日/g, \"\").replace(/[.\\/]/g, \"-\");\n const parts = normalized.split(\"-\").filter((part) => part.length > 0);\n if (parts.length < 3) throw new Error(\"月と日まで入力してください(例: 令和6年3月1日)\");\n const eraYear = Number(parts[0]);\n const month = Number(parts[1]);\n const day = Number(parts[2]);\n if (!Number.isFinite(eraYear) || !Number.isFinite(month) || !Number.isFinite(day)) {\n throw new Error(\"日付形式で回答してください(例: 令和6年3月1日)\");\n }\n return finalizeDateParts(ERA_INFO[era] + eraYear, month, day);\n}\nfunction tryParseMonthDay(compact) {\n const match = compact.match(/^(\\d{1,2})(?:月|\\/|-|\\.)(\\d{1,2})(?:日)?$/);\n if (!match) return null;\n const currentYear = new Date().getFullYear();\n return finalizeDateParts(currentYear, Number(match[1]), Number(match[2]));\n}\nfunction parseDateInput(value) {\n const trimmed = String(value ?? \"\").trim();\n if (!trimmed) throw new Error(\"日付が空です\");\n const compact = trimmed.replace(/\\s+/g, \"\");\n const eraResult = tryParseEraDate(compact);\n if (eraResult) return eraResult;\n const monthDayResult = tryParseMonthDay(compact);\n if (monthDayResult) return monthDayResult;\n const normalized = compact.replace(/年/g, \"-\").replace(/月/g, \"-\").replace(/日/g, \"\").replace(/[.\\/]/g, \"-\");\n const isoParts = normalized.split(\"-\").filter((part) => part.length > 0);\n if (isoParts.length === 3 && isoParts[0].length >= 4) {\n return finalizeDateParts(Number(isoParts[0]), Number(isoParts[1]), Number(isoParts[2]));\n }\n const parsed = new Date(trimmed);\n if (Number.isNaN(parsed.getTime())) {\n throw new Error(\"日付形式で回答してください(例: 2026-03-01 や 令和6年3月1日)\");\n }\n return finalizeDateParts(parsed.getFullYear(), parsed.getMonth() + 1, parsed.getDate());\n}\n\nconst identity = $('署名検証・対象レコード特定').item.json;\nconst { config, resultId, contentType, text } = identity;\n\nconst stateRows = $('待機状態取得').all().map((item) => item.json);\nconst state = stateRows[0] || null;\n\nasync function pleasanterPost(path, body) {\n return this.helpers.httpRequest({\n method: \"POST\",\n url: `${config.PLEASANTER_BASE_URL_PROD}${path}`,\n body: { ApiVersion: 1.1, ApiKey: config.PLEASANTER_API_KEY_PROD, ...body },\n json: true,\n });\n}\n\nfunction buildResult(action, extra, nextState) {\n const fallbackNext = state\n ? { awaitInput: state.awaitInput, awaitProcessId: state.awaitProcessId, awaitColumn: state.awaitColumn }\n : { awaitInput: \"none\", awaitProcessId: \"\", awaitColumn: \"\" };\n const next = nextState || fallbackNext;\n return [{\n json: {\n action,\n resultId,\n config,\n targetEmail: state ? state.targetEmail : null,\n processId: null,\n messageText: null,\n ...extra,\n nextAwaitInput: next.awaitInput,\n nextAwaitProcessId: String(next.awaitProcessId ?? \"\"),\n nextAwaitColumn: next.column !== undefined ? (next.column || \"\") : (next.awaitColumn || \"\"),\n },\n }];\n}\n\nif (!state || ![\"none\", \"date\", \"file\"].includes(state.awaitInput)) {\n return buildResult(\"error\", {\n messageText: `待機状態が不正です(resultId=${resultId})。healthcheck_bot_stateの該当行を確認してください。`,\n }, { awaitInput: \"none\", awaitProcessId: \"\", awaitColumn: \"\" });\n}\n\nif (state.awaitInput === \"none\") {\n const pendingProcesses = JSON.parse(state.pendingProcesses || \"[]\");\n const matched = matchProcessByLabel(pendingProcesses, text);\n\n if (!matched) {\n // 不一致 → フォールバック再送(HC-SUBが現在Statusの選択肢案内を再送する)\n return buildResult(\"execute\", { processId: null }, { awaitInput: \"none\", awaitProcessId: \"\", awaitColumn: \"\" });\n }\n\n // HC-SUBが保存するpendingProcessesは{processId, label, tooltip}のみでValidateInputsを\n // 持たないため、追加入力の要否判定にはサイト設定(getsite)からフルのProcess定義を\n // 引き直す必要がある(README.md記載の設計注記のとおり)。\n const siteRes = await pleasanterPost.call(this, `api/items/${config.HEALTHCHECK_SITE_ID}/getsite`, {});\n const siteSettings = siteRes.Response.Data.SiteSettings;\n const processes = siteSettings.Processes || [];\n const fullProcess = processes.find((p) => p.Id === matched.processId);\n if (!fullProcess) {\n throw new Error(`ProcessId ${matched.processId} がサイト設定内に見つかりません`);\n }\n const classification = classifyAwaitInput(fullProcess);\n\n if (classification.awaitInput === \"none\") {\n return buildResult(\"execute\", { processId: matched.processId }, { awaitInput: \"none\", awaitProcessId: \"\", awaitColumn: \"\" });\n }\n\n const promptText = classification.awaitInput === \"date\"\n ? \"日付を入力してください(例: 2026-03-01)\"\n : \"ファイルを送信してください\";\n return buildResult(\"send\", { messageText: promptText }, {\n awaitInput: classification.awaitInput,\n awaitProcessId: String(matched.processId),\n column: classification.column || \"\",\n });\n}\n\nif (state.awaitInput === \"date\") {\n try {\n const parsedDate = parseDateInput(text);\n await pleasanterPost.call(this, `api/items/${resultId}/update`, {\n DateHash: { [state.awaitColumn]: parsedDate },\n });\n return buildResult(\"execute\", { processId: Number(state.awaitProcessId) }, { awaitInput: \"none\", awaitProcessId: \"\", awaitColumn: \"\" });\n } catch (err) {\n return buildResult(\"send\", { messageText: err.message }, {\n awaitInput: \"date\", awaitProcessId: state.awaitProcessId, column: state.awaitColumn,\n });\n }\n}\n\n// state.awaitInput === \"file\"\nif (contentType !== \"file\") {\n return buildResult(\"send\", { messageText: \"ファイルを送信してください\" }, {\n awaitInput: \"file\", awaitProcessId: state.awaitProcessId, column: state.awaitColumn,\n });\n}\n\n// TODO(要フォローアップ、未実装): LINEWORKSファイル添付のダウンロード\n// (GET https://www.worksapis.com/v1.0/bots/{botId}/attachments/{fileId})には\n// LINEWORKS Bot APIのアクセストークン(JWTアサーション経由)が必要だが、プロジェクトルールにより\n// JWT署名は専用のJWTノード(このワークフローではaction:\"send\"経路のノードのみに存在し、\n// このCodeノードより後段に位置する)でしか行えない。そのため本Codeノード内では\n// トークンを取得する手段がなく、ダウンロード〜Pleasanter添付アップロード\n// (Pleasanter公式マニュアルapi-attachment系、具体的なエンドポイントも実装時要確認)は未実装。\n// フォローアップタスクでグラフ構成の見直し(ファイル受信専用のJWT/Tokenペアを追加する等)を検討すること。\nreturn buildResult(\"send\", {\n messageText: \"現在、ファイル添付の処理は準備中です。しばらくお待ちいただくか、担当者にご連絡ください。\",\n}, { awaitInput: \"file\", awaitProcessId: state.awaitProcessId, column: state.awaitColumn });"
|
||
}
|
||
},
|
||
{
|
||
"id": "dt-update-state",
|
||
"name": "状態更新",
|
||
"type": "n8n-nodes-base.dataTable",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
1300,
|
||
400
|
||
],
|
||
"parameters": {
|
||
"operation": "update",
|
||
"dataTableId": {
|
||
"__rl": true,
|
||
"mode": "id",
|
||
"value": "jqMDa2YZTI4f0iQ7"
|
||
},
|
||
"filters": {
|
||
"conditions": [
|
||
{
|
||
"keyName": "resultId",
|
||
"keyValue": "={{ $json.resultId }}"
|
||
}
|
||
]
|
||
},
|
||
"columns": {
|
||
"mappingMode": "defineBelow",
|
||
"value": {
|
||
"awaitInput": "={{ $json.nextAwaitInput }}",
|
||
"awaitProcessId": "={{ $json.nextAwaitProcessId }}",
|
||
"awaitColumn": "={{ $json.nextAwaitColumn }}"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "if-action-execute",
|
||
"name": "IF: action==execute",
|
||
"type": "n8n-nodes-base.if",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1520,
|
||
400
|
||
],
|
||
"parameters": {
|
||
"conditions": {
|
||
"options": {
|
||
"caseSensitive": true,
|
||
"leftValue": "",
|
||
"typeValidation": "loose",
|
||
"version": 2
|
||
},
|
||
"conditions": [
|
||
{
|
||
"id": "cond-execute",
|
||
"leftValue": "={{ $('分岐処理・アクション決定').item.json.action === 'execute' }}",
|
||
"rightValue": true,
|
||
"operator": {
|
||
"type": "boolean",
|
||
"operation": "true",
|
||
"singleValue": true
|
||
}
|
||
}
|
||
],
|
||
"combinator": "and"
|
||
},
|
||
"options": {}
|
||
}
|
||
},
|
||
{
|
||
"id": "execute-hc-sub",
|
||
"name": "HC-SUB実行",
|
||
"type": "n8n-nodes-base.executeWorkflow",
|
||
"typeVersion": 1.2,
|
||
"position": [
|
||
1740,
|
||
260
|
||
],
|
||
"parameters": {
|
||
"workflowId": {
|
||
"__rl": true,
|
||
"mode": "id",
|
||
"value": "XRqcykbG2LuAjGG2"
|
||
},
|
||
"workflowInputs": {
|
||
"mappingMode": "defineBelow",
|
||
"value": {
|
||
"resultId": "={{ $('分岐処理・アクション決定').item.json.resultId }}",
|
||
"processId": "={{ $('分岐処理・アクション決定').item.json.processId }}"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "if-action-send",
|
||
"name": "IF: action==send",
|
||
"type": "n8n-nodes-base.if",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1740,
|
||
540
|
||
],
|
||
"parameters": {
|
||
"conditions": {
|
||
"options": {
|
||
"caseSensitive": true,
|
||
"leftValue": "",
|
||
"typeValidation": "loose",
|
||
"version": 2
|
||
},
|
||
"conditions": [
|
||
{
|
||
"id": "cond-send",
|
||
"leftValue": "={{ $('分岐処理・アクション決定').item.json.action === 'send' }}",
|
||
"rightValue": true,
|
||
"operator": {
|
||
"type": "boolean",
|
||
"operation": "true",
|
||
"singleValue": true
|
||
}
|
||
}
|
||
],
|
||
"combinator": "and"
|
||
},
|
||
"options": {}
|
||
}
|
||
},
|
||
{
|
||
"id": "code-jwt-claims",
|
||
"name": "JWTクレーム組み立て",
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1960,
|
||
460
|
||
],
|
||
"parameters": {
|
||
"jsCode": "const decision = $('分岐処理・アクション決定').item.json;\nconst config = decision.config;\nconst now = Math.floor(Date.now() / 1000);\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\nasync function pleasanterPost(path, body) {\n return this.helpers.httpRequest({\n method: 'POST',\n url: `${config.PLEASANTER_BASE_URL_PROD}${path}`,\n body: { ApiVersion: 1.1, ApiKey: config.PLEASANTER_API_KEY_PROD, ...body },\n json: true,\n });\n}\n\n// TODO(Task 12で要確認・HC-SUBと同一の暫定実装): 484184(LINEWORKS_BOT_MASTER_SITE_ID)の\n// 実データ構造が未調査のため、SiteSettings.BotIds または Processes[].BotId のいずれかを想定する。\nconst botMasterRes = await pleasanterPost.call(this, `api/items/${config.LINEWORKS_BOT_MASTER_SITE_ID}/getsite`, {});\nconst botMasterSiteSettings = botMasterRes.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)から解決できませんでした。');\n}\n\nconst apiUrl = `https://www.worksapis.com/v1.0/bots/${botId}/users/${decision.targetEmail}/messages`;\n\nconst messageContent = {\n type: 'text',\n text: decision.messageText || '',\n};\n\nreturn [{\n json: {\n jwtClaims,\n apiUrl,\n messageContent,\n config,\n resultId: decision.resultId,\n },\n}];"
|
||
}
|
||
},
|
||
{
|
||
"id": "jwt-sign",
|
||
"name": "Sign JWT",
|
||
"type": "n8n-nodes-base.jwt",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
2180,
|
||
460
|
||
],
|
||
"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": [
|
||
2400,
|
||
460
|
||
],
|
||
"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()しない、ヘッダーも手動指定しない)。\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 [{ json: { result: 'ok', resultId: claims.resultId, lineworksResponse: sendRes } }];"
|
||
}
|
||
},
|
||
{
|
||
"id": "respond-ok",
|
||
"name": "Respond to Webhook(成功)",
|
||
"type": "n8n-nodes-base.respondToWebhook",
|
||
"typeVersion": 1.1,
|
||
"position": [
|
||
2620,
|
||
320
|
||
],
|
||
"parameters": {
|
||
"respondWith": "json",
|
||
"responseBody": "={{ JSON.stringify({ result: \"ok\" }) }}",
|
||
"options": {}
|
||
}
|
||
},
|
||
{
|
||
"id": "respond-error",
|
||
"name": "Respond to Webhook(異常系)",
|
||
"type": "n8n-nodes-base.respondToWebhook",
|
||
"typeVersion": 1.1,
|
||
"position": [
|
||
1960,
|
||
680
|
||
],
|
||
"parameters": {
|
||
"respondWith": "json",
|
||
"responseBody": "={{ JSON.stringify({ result: \"error\", message: $('分岐処理・アクション決定').item.json.messageText || \"unknown error\" }) }}",
|
||
"options": {}
|
||
}
|
||
}
|
||
],
|
||
"connections": {
|
||
"Webhook": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "設定値一括取得",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"設定値一括取得": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "署名検証・対象レコード特定",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"署名検証・対象レコード特定": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "待機状態取得",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"待機状態取得": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "分岐処理・アクション決定",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"分岐処理・アクション決定": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "状態更新",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"状態更新": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "IF: action==execute",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"IF: action==execute": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "HC-SUB実行",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
],
|
||
[
|
||
{
|
||
"node": "IF: action==send",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"HC-SUB実行": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Respond to Webhook(成功)",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"IF: action==send": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "JWTクレーム組み立て",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
],
|
||
[
|
||
{
|
||
"node": "Respond to Webhook(異常系)",
|
||
"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": "Respond to Webhook(成功)",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
}
|
||
},
|
||
"settings": {
|
||
"executionOrder": "v1"
|
||
}
|
||
}
|