From 935e58e38444611761d9295431b0e71f3e4bf497 Mon Sep 17 00:00:00 2001 From: Kenichiro NOGI Date: Sat, 5 Sep 2026 18:02:26 +0900 Subject: [PATCH] =?UTF-8?q?=E8=A8=88=E7=94=BB=E6=9B=B8Task9-11=E3=82=92Cod?= =?UTF-8?q?e=E3=83=8E=E3=83=BC=E3=83=89=E9=9B=86=E7=B4=84=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E3=81=AB=E5=85=A8=E9=9D=A2=E6=94=B9=E8=A8=82(?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E3=83=AB=E3=83=BC=E3=83=AB=E5=8F=8D=E6=98=A0?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ユーザー指示により、n8nワークフロー構築の基本ルールを確定: (1)Data Table利用は最小限(config値取得は1回のreturnAll取得+ Codeノードでフィルタ、複数のGetノードを並べない)、(2)入口は Webhook+Codeでノード数を抑える、(3)外部システムへのHTTP Requestノードは使ってよい(Codeノード内this.helpers.httpRequest への無理な集約は不要)。JWT署名・Data Table読み書きのみ専用 ノードを維持。 これによりHC-SUB 19→7ノード、HC-WA 60→11ノード目安に再設計。 Task11で発生した「認証チェーンを分岐ごとに複製」問題も、 Codeノード1つに分岐ロジックを集約することで解消。 Co-Authored-By: Claude Sonnet 5 --- ...-09-05-healthcheck-lineworks-survey-n8n.md | 187 ++++++++++++------ 1 file changed, 123 insertions(+), 64 deletions(-) diff --git a/NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md b/NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md index d160fd95..c664585f 100644 --- a/NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md +++ b/NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md @@ -18,6 +18,12 @@ - Pleasanter日本語ボディを含むリクエストはシェル引数に直書きせず、Writeツールでファイル化してから`curl --data-binary "@file"`で送る(同ガイド7-6参照) - 具体的なProcess内容(①日程通知〜③検査結果受取り等の本番仕様)は本計画のスコープ外。本計画は「Process流用型フロー定義」の枠組みを動かすことがゴールで、テスト用Process1件で疎通確認する - 設計書8章の社員マスタ(504412)によるメールアドレス整合性チェック・フリガナ補完は、Bot対話フローと独立した別タスクとして扱う。本計画には含まない +- **【n8nワークフロー構築の基本ルール、必ず守ること】** 以下3点を踏まえてノード数の肥大を避ける(`NodeSrv/apps/n8n/docs/n8n-guide.md` 8-1参照): + - **Data Table利用は最小限にする。**`workflow_config_values`からの設定値取得は1回の`Data Table`Getノード(`filters`無し、`returnAll:true`)にまとめ、後続のCodeノードで`configKey`→`configValue`のMapに変換して使う。「1キー=1回のGet」でノードを積み上げない(n8n-guide.md 7-2の注意は619件規模のマスタデータ展開の話であり、config値十数件程度の一括取得には当てはまらない) + - **入口はWebhookノード+Codeノードで処理し、なるべくノード数を抑える。** データ整形・分岐判定・業務ロジック(ツールチップ置換、Process抽出、日付パース等)はCodeノードにまとめる + - **外部システム(Pleasanter、LINEWORKS等)へのHTTP Requestノードは普通に使ってよい。** 個々のAPI呼び出しをCodeノード内の`this.helpers.httpRequest`に無理に押し込む必要はない + - **JWT署名(Credential使用)だけは専用の`n8n-nodes-base.jwt`ノードを残す**(秘密鍵を安全に扱う既存の実証済みパターンのため) + - **Data Tableの読み書き自体(`workflow_config_values`の取得、`healthcheck_bot_state`の取得・更新)は専用の`Data Table`ノードのまま残す**(Codeノードから直接Data Tableを操作する手段が無いため) --- @@ -859,7 +865,7 @@ git commit -m "docs: healthcheck_bot_state Data Table作成結果を記録" ### Task 9: HC-SUBワークフロー — プロセス実行+案内送信 -WP・WAの両方から呼ばれる共通ロジック。「(任意)ProcessIdを実行→現在Statusの選択肢を組み立ててLINEWORKSへ送信→Data Table更新」を1本のExecute Workflow Triggerサブワークフローにまとめる。 +WP・WAの両方から呼ばれる共通ロジック。「(任意)ProcessIdを実行→現在Statusの選択肢を組み立ててLINEWORKSへ送信→Data Table更新」を1本のExecute Workflow Triggerサブワークフローにまとめる。**Global Constraintsのn8nワークフロー構築ルール(Data Table最小限、Webhook+Codeでノード数抑制、HTTP Requestノードは可)に従い、Pleasanter API呼び出し群・選択肢組み立てを1つのCodeノードに集約する。** **Files:** - Create: `NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json` @@ -869,29 +875,25 @@ WP・WAの両方から呼ばれる共通ロジック。「(任意)ProcessIdを - Consumes: Task 3〜6のロジック(Codeノードへ書き写す)、Task 8のData Table ID、`workflow_config_values`(id `bNkadTyDgDepYx2p`、既存のorg-master-sync用n8n Data Table)に登録済みの設定値 - Produces: n8n上のワークフロー(Execute Workflow Trigger、入力`{resultId: string, processId: string | null}`)。WP・WAはこのワークフローIDを`Execute Workflow`ノードで呼び出す -**秘密情報・設定値の扱い方針(重要):** org-master-sync系ワークフロー(`n8n-guide.md` 6章参照)と同じ方式に統一する。**PleasanterのApiKeyやLINEWORKS Client Secret等をCodeノードに直接ハードコードしない。** 代わりに`workflow_config_values`(Data Table、id `bNkadTyDgDepYx2p`)から`Data Table`ノード(`operation: "get"`、`filters.conditions: [{keyName: "configKey", keyValue: "<キー名>"}]`)で都度取得し、後続ノードで`{{ $('取得ノード名').item.json.configValue }}`のように参照する。`n8n-guide.md` 7-2の「設定値取得は1キー=1回のGetに限定する」原則に従い、キーごとに個別のGetノードを用意する。 +**秘密情報・設定値の扱い方針(重要):** **PleasanterのApiKeyやLINEWORKS Client Secret等をCodeノードに直接ハードコードしない。** 代わりに`workflow_config_values`(Data Table、id `bNkadTyDgDepYx2p`)を**1回の`Data Table`Getノード(`filters`無し、`returnAll:true`)でまとめて取得**し、直後のCodeノードで`configKey`→`configValue`のMapに変換して使う(個別キーごとにGetノードを分けない)。 -このワークフローで使うキー(今回追加登録済み): -- `PLEASANTER_BASE_URL_PROD`(既存) -- `PLEASANTER_API_KEY_PROD`(既存) -- `LW_BOT_CLIENT_ID` / `LW_BOT_CLIENT_SECRET` / `LW_BOT_SERVICE_ACCOUNT`(今回追加。値はLINEWORKS通知送信ワークフローが使っているBotと同じ認証情報) -- `HEALTHCHECK_SITE_ID`(=508971)/ `LINEWORKS_BOT_MASTER_SITE_ID`(=484184)/ `HEALTHCHECK_DATA_TABLE_ID`(=`jqMDa2YZTI4f0iQ7`)(今回追加、SiteId自体は秘密ではないがconfig値に揃えて一元管理する) +このワークフローで使うキー(今回追加登録済み、値は取得済みの全行から後続Codeノードでフィルタする): +`PLEASANTER_BASE_URL_PROD` / `PLEASANTER_API_KEY_PROD` / `LW_BOT_CLIENT_ID` / `LW_BOT_CLIENT_SECRET` / `LW_BOT_SERVICE_ACCOUNT` / `HEALTHCHECK_SITE_ID`(=508971)/ `LINEWORKS_BOT_MASTER_SITE_ID`(=484184)/ `HEALTHCHECK_DATA_TABLE_ID`(=`jqMDa2YZTI4f0iQ7`) -**ノード構成:** +**ノード構成(7ノード):** 1. `Execute Workflow Trigger`(入力: `resultId`, `processId`) -2. `Data Table`ノード「PLEASANTER_BASE_URL取得」(`configKey: "PLEASANTER_BASE_URL_PROD"`) -3. `Data Table`ノード「PLEASANTER_API_KEY取得」(`configKey: "PLEASANTER_API_KEY_PROD"`) -4. `Data Table`ノード「HEALTHCHECK_SITE_ID取得」(`configKey: "HEALTHCHECK_SITE_ID"`) -5. `IF`: `processId`が存在するか - - true分岐 → `HTTP Request`「Process実行」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('Execute Workflow Trigger').item.json.resultId }}/update`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}","ProcessId": {{ $('Execute Workflow Trigger').item.json.processId }}}` - - false分岐 → そのまま次へ(フォールバック再送のケース) -6. `HTTP Request`「レコード取得」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('Execute Workflow Trigger').item.json.resultId }}/get`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}"}` -7. `HTTP Request`「サイト設定取得」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('HEALTHCHECK_SITE_ID取得').item.json.configValue }}/getsite`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}"}` -8. `Code`「選択肢組み立て」: Task 4の`fillTemplate`とTask 5の`extractProcessesForStatus`をそのまま貼り付けて使う +2. `Data Table`ノード「設定値一括取得」(`operation: "get"`, `dataTableId: bNkadTyDgDepYx2p`, `filters`無し, `returnAll: true`) +3. `Code`「Pleasanter照会・選択肢組み立て」: 設定値のMap化、(任意)Process実行、レコード取得、サイト設定取得(getsite)、Botマスタ取得(484184)、対象者メール解決、選択肢組み立て(Task 4の`fillTemplate`とTask 5の`extractProcessesForStatus`をそのまま使う)までを1つのCodeノードにまとめる。Pleasanter APIへの各呼び出しは`await this.helpers.httpRequest({method:"POST", url, body, json:true})`で行う(n8n Codeノードの公式ヘルパー、外部HTTPリクエストを直接発行できる) +4. `Code`「JWTクレーム組み立て」: `iss`/`sub`に設定値Mapの`LW_BOT_CLIENT_ID`/`LW_BOT_SERVICE_ACCOUNT`を使ってJWTクレームJSON文字列を組み立てる。あわせて選択肢からLINEWORKS `button_template`のactions配列を組み立てる +5. `JWT`ノード(`operation: sign`, `algorithm: RS256`, Credential: 「LINEWORKS Bot Private Key (v4)」、id `Hw0qlEaGfLPnQWp1`) +6. `Code`「アクセストークン取得・LINEWORKS送信」: `POST https://auth.worksmobile.com/oauth2/v2.0/token`(form-urlencoded、`client_id`/`client_secret`は設定値Mapの`LW_BOT_CLIENT_ID`/`LW_BOT_CLIENT_SECRET`)でトークン取得後、続けて`POST https://www.worksapis.com/v1.0/bots/{BOT_ID}/users/{userId}/messages`(`button_template`形式)へ送信するところまでを1つのCodeノードで行う(いずれも`this.helpers.httpRequest`) +7. `Data Table`ノード「状態更新」: `healthcheck_bot_state`(id: `jqMDa2YZTI4f0iQ7`)へ`resultId`をキーに`insert`(`currentStatus`, `pendingProcesses`=JSON化したoptions, `awaitInput: "none"`)。`updatedAt`列は存在しない(Task 8参照)ため送信対象に含めない + +Step 3のCodeノードの中身(骨格。各`this.helpers.httpRequest`呼び出しの`body`はTask 4〜7で確認したAPI形式に沿って埋める): ```javascript -// Codeノード「選択肢組み立て」の中身 +// Codeノード「Pleasanter照会・選択肢組み立て」の中身 function isUnsetSentinel(value) { return typeof value === "string" && value.startsWith("1899"); } @@ -912,11 +914,38 @@ function extractProcessesForStatus(processes, status) { return processes.filter((p) => p.CurrentStatus === status || p.CurrentStatus === -1); } -const record = $('レコード取得').item.json.Response.Data; -const siteSettings = $('サイト設定取得').item.json.Response.Data.SiteSettings; +const configRows = $('設定値一括取得').all().map((item) => item.json); +const config = Object.fromEntries(configRows.map((row) => [row.configKey, row.configValue])); + +const trigger = $('Execute Workflow Trigger').item.json; +const baseUrl = config.PLEASANTER_BASE_URL_PROD; +const apiKey = config.PLEASANTER_API_KEY_PROD; + +async function pleasanterPost(path, body) { + const res = await this.helpers.httpRequest({ + method: "POST", + url: `${baseUrl}${path}`, + body: { ApiVersion: 1.1, ApiKey: apiKey, ...body }, + json: true, + }); + return res; +} + +if (trigger.processId) { + await pleasanterPost.call(this, `api/items/${trigger.resultId}/update`, { ProcessId: trigger.processId }); +} + +const recordRes = await pleasanterPost.call(this, `api/items/${trigger.resultId}/get`, {}); +const record = recordRes.Response.Data; + +const siteRes = await pleasanterPost.call(this, `api/items/${config.HEALTHCHECK_SITE_ID}/getsite`, {}); +const siteSettings = siteRes.Response.Data.SiteSettings; const columns = siteSettings.Columns || []; const processes = siteSettings.Processes || []; +const botMasterRes = await pleasanterPost.call(this, `api/items/${config.LINEWORKS_BOT_MASTER_SITE_ID}/getsite`, {}); +// TODO(Task 12): 484184の実データ構造を見てBotIdの解決方法を確定させる + const valueHash = { ...record.ClassHash, ...record.NumHash, ...record.DateHash, ...record.DescriptionHash, }; @@ -928,33 +957,27 @@ const options = candidates.map((p) => ({ tooltip: fillTemplate(p.ToolTip || "", columns, valueHash), })); +const userRes = await pleasanterPost.call(this, `api/users/get`, { + View: { ApiGetMailAddresses: true }, + Where: { UserId: record.ClassHash.ClassC }, +}); + return [{ json: { resultId: record.ResultId, currentStatus: record.Status, options, - classCUserId: record.ClassHash.ClassC, + targetEmail: userRes.Response.Data[0]?.MailAddress, + config, }, }]; ``` -(`ToolTip`のキー名はTask 2の実機調査結果で確定させ、異なる場合はここだけ修正する) - -9. `Data Table`ノード「LW_BOT_CLIENT_ID取得」(`configKey: "LW_BOT_CLIENT_ID"`) -10. `Data Table`ノード「LW_BOT_CLIENT_SECRET取得」(`configKey: "LW_BOT_CLIENT_SECRET"`) -11. `Data Table`ノード「LW_BOT_SERVICE_ACCOUNT取得」(`configKey: "LW_BOT_SERVICE_ACCOUNT"`) -12. `Data Table`ノード「LINEWORKS_BOT_MASTER_SITE_ID取得」(`configKey: "LINEWORKS_BOT_MASTER_SITE_ID"`) -13. `HTTP Request`「Botマスタ取得」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('LINEWORKS_BOT_MASTER_SITE_ID取得').item.json.configValue }}/getsite`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}"}`(484184からBotId一覧を取得。どのBotを使うかの解決方法はTask 12で個別Process設計時に確定するため、ここでは1件目のBotを暫定使用するか、`Execute Workflow Trigger`の入力に`botId`を追加して呼び出し元から渡す形にするか、実装時に判断する) -14. `HTTP Request`「対象者メール解決」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/users/get`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}","View":{"ApiGetMailAddresses":true},"Where":{"UserId":{{ $('選択肢組み立て').item.json.classCUserId }}}}` -15. `Code`「JWTクレーム組み立て」: LINEWORKS通知送信ワークフロー(`Yprojk4JTl1vJPkf`)の`Validate & Prepare`ノードと同じパターンでJWTクレームJSON文字列を組み立てる。`iss`/`sub`には`{{ $('LW_BOT_CLIENT_ID取得').item.json.configValue }}` / `{{ $('LW_BOT_SERVICE_ACCOUNT取得').item.json.configValue }}`を使う(Codeノードにこれらの値を直接書かない)。あわせて選択肢からLINEWORKS `button_template`のactions配列を組み立てる -16. `JWT`ノード(`operation: sign`, `algorithm: RS256`, Credential: 「LINEWORKS Bot Private Key (v4)」、id `Hw0qlEaGfLPnQWp1`) -17. `HTTP Request`「アクセストークン取得」: `POST https://auth.worksmobile.com/oauth2/v2.0/token`(form-urlencoded、`Yprojk4JTl1vJPkf`の`Get Access Token`ノードと同じパラメータ構成。`client_id`/`client_secret`は`{{ $('LW_BOT_CLIENT_ID取得').item.json.configValue }}` / `{{ $('LW_BOT_CLIENT_SECRET取得').item.json.configValue }}`を参照) -18. `HTTP Request`「LINEWORKS送信」: `POST https://www.worksapis.com/v1.0/bots/{BOT_ID}/users/{userId}/messages`、`button_template`形式のcontentを送信 -19. `Data Table`ノード「状態更新」: `healthcheck_bot_state`(id: `jqMDa2YZTI4f0iQ7`)へ`resultId`をキーに`upsert`(`currentStatus`, `pendingProcesses`=JSON化したoptions, `awaitInput: "none"`)。`updatedAt`列は存在しない(Task 8参照)ため送信対象に含めない +(`ToolTip`のキー名はTask 2の実機調査結果で確定させ、異なる場合はここだけ修正する。上記は骨格であり、実装時にPleasanter API実レスポンスの構造に合わせて調整する) - [ ] **Step 1: ワークフローJSON雛形を作成** -`workflows/hc-sub-run-process-and-notify.json`に、上記19ノードの`nodes`配列と`connections`を、Task 7の`n8n-api.js`が期待する`{name, nodes, connections, settings}`形式で書く。各`HTTP Request`ノードの`parameters`は本タスク内の説明文の通りのURL・bodyを設定する(`n8n-nodes-base.httpRequest`, `n8n-nodes-base.code`, `n8n-nodes-base.jwt`, `n8n-nodes-base.executeWorkflowTrigger`, `n8n-nodes-base.if`, `n8n-nodes-base.dataTable`の各ノードタイプを使う)。**秘密情報(PleasanterのApiKey、LINEWORKS Client Secret等)はCodeノードやHTTP Requestノードのパラメータに直接値として書かず、必ず対応する`Data Table`取得ノードの式(`{{ $('ノード名').item.json.configValue }}`)で参照する。** +`workflows/hc-sub-run-process-and-notify.json`に、上記7ノードの`nodes`配列と`connections`を、Task 7の`n8n-api.js`が期待する`{name, nodes, connections, settings}`形式で書く(`n8n-nodes-base.code`, `n8n-nodes-base.jwt`, `n8n-nodes-base.executeWorkflowTrigger`, `n8n-nodes-base.dataTable`の各ノードタイプを使う)。**秘密情報はCodeノードに直接値として書かず、必ず「設定値一括取得」ノードから得たMapを経由して参照する。** - [ ] **Step 2: デプロイ** @@ -994,12 +1017,12 @@ git commit -m "feat: HC-SUBワークフロー(プロセス実行+案内送信) - Consumes: Task 9のワークフローID(`Execute Workflow`ノードで参照) - Produces: Webhook `POST https://n8n32.next-hd.net/webhook/healthcheck-status-push`(`X-Api-Key`ヘッダー認証、body `{resultId, processId}`) -**秘密情報の扱い**: Webhook認証キーはCodeノードに直接書かず、`workflow_config_values`(`bNkadTyDgDepYx2p`)の`HEALTHCHECK_WP_API_KEY`キーを`Data Table`ノードで取得して比較する(Task 9と同じ方針)。 +**秘密情報の扱い**: Webhook認証キーはCodeノードに直接書かず、`workflow_config_values`(`bNkadTyDgDepYx2p`)から`Data Table`ノードで取得して比較する(Task 9と同じ方針。1回の`returnAll:true`取得+後続Codeノードでキーを引く)。 **ノード構成:** 1. `Webhook`(`httpMethod: POST`, `path: healthcheck-status-push`) -2. `Data Table`ノード「HEALTHCHECK_WP_API_KEY取得」(`configKey: "HEALTHCHECK_WP_API_KEY"`) +2. `Data Table`ノード「設定値一括取得」(`operation: "get"`, `dataTableId: bNkadTyDgDepYx2p`, `filters`無し, `returnAll: true`) 3. `Code`「検証」: `X-Api-Key`ヘッダーとbodyの`resultId`/`processId`必須チェック(不正なら`throw new Error(...)`でワークフローを失敗させる) 4. `Execute Workflow`(Task 9のワークフローIDを指定、入力: `resultId`, `processId`) 5. `Respond to Webhook`(`{"result":"ok"}`を返す) @@ -1009,7 +1032,8 @@ git commit -m "feat: HC-SUBワークフロー(プロセス実行+案内送信) `workflows/hc-wp-status-push.json`を作成。Codeノード「検証」の中身: ```javascript -const expectedApiKey = $('HEALTHCHECK_WP_API_KEY取得').item.json.configValue; +const configRows = $('設定値一括取得').all().map((item) => item.json); +const expectedApiKey = configRows.find((row) => row.configKey === "HEALTHCHECK_WP_API_KEY")?.configValue; const headers = $input.first().json.headers || {}; if (headers["x-api-key"] !== expectedApiKey) { throw new Error("Unauthorized: invalid API key"); @@ -1071,14 +1095,16 @@ git commit -m "feat: HC-WPワークフロー(Statusプッシュ通知)を追加" - Consumes: Task 6の署名検証ロジック、Task 3の日付パース、Task 5の`matchProcessByLabel`/`classifyAwaitInput`、Task 9のワークフローID - Produces: Webhook `POST https://n8n32.next-hd.net/webhook/healthcheck-lineworks-response`(LINE WORKS本体からの直接コールバック、`x-works-signature`検証) -**ノード構成:** +**Global Constraintsのn8nワークフロー構築ルールに従い、分岐ロジック・Pleasanter/LINEWORKS API呼び出しは可能な限りCodeノードに集約する。** Data Tableの読み書き(設定値取得・待機状態の取得/更新)のみ専用ノードを使う。JWT署名も専用ノードのまま。 + +**ノード構成(目安11ノード):** 1. `Webhook`(`httpMethod: POST`, `path: healthcheck-lineworks-response`, `options.rawBody: true`) -2. `Data Table`ノード「LINEWORKS_BOT_SECRET取得」(`configKey: "LINEWORKS_BOT_SECRET"`。値はLINEWORKS Developer ConsoleでBot Secretを確認後、`workflow_config_values`へ別途登録する。Task 11着手時点で未登録なら先に登録する) -3. `Code`「署名検証・送信者解決」: Task 6の`verifySignature`を貼り付けて検証。失敗なら`throw`。成功したら`source.userId`(=メールアドレス)と、`content.type`に応じたテキスト/ファイル情報を抽出する +2. `Data Table`ノード「設定値一括取得」(`operation: "get"`, `dataTableId: bNkadTyDgDepYx2p`, `filters`無し, `returnAll: true`) +3. `Code`「署名検証・対象レコード特定」: Task 6の`verifySignature`で検証(失敗なら`throw`)。成功したら`this.helpers.httpRequest`で対象者メール→PleasanterUserId解決、508971の対象レコード検索(`ColumnFilterHash`)までをこの1ノードで行う。0件/複数件は`throw`(6章の異常系方針、自動判定しない) ```javascript -// Codeノード「署名検証・送信者解決」の中身 +// Codeノード「署名検証・対象レコード特定」の中身 const crypto = require("crypto"); function normalizeSignature(value) { return String(value || "").trim().replace(/^sha256=/i, ""); @@ -1097,7 +1123,8 @@ function verifySignature(rawBody, headerSignature, botSecret) { return safeEqual(headerSig, expected); } -const LINEWORKS_BOT_SECRET = $('LINEWORKS_BOT_SECRET取得').item.json.configValue; +const configRows = $('設定値一括取得').all().map((item) => item.json); +const config = Object.fromEntries(configRows.map((row) => [row.configKey, row.configValue])); const item = $input.first(); const headers = item.json.headers || {}; @@ -1105,17 +1132,48 @@ const rawBody = item.binary && item.binary.data ? Buffer.from(item.binary.data.data, "base64") : Buffer.from(JSON.stringify(item.json.body || {}), "utf8"); -if (!verifySignature(rawBody, headers["x-works-signature"], LINEWORKS_BOT_SECRET)) { +if (!verifySignature(rawBody, headers["x-works-signature"], config.LINEWORKS_BOT_SECRET)) { throw new Error("Unauthorized: signature mismatch"); } const body = item.json.body || {}; const source = body.source || {}; const content = body.content || {}; +const targetEmail = source.userId; + +async function pleasanterPost(path, reqBody) { + return this.helpers.httpRequest({ + method: "POST", + url: `${config.PLEASANTER_BASE_URL_PROD}${path}`, + body: { ApiVersion: 1.1, ApiKey: config.PLEASANTER_API_KEY_PROD, ...reqBody }, + json: true, + }); +} + +const userRes = await pleasanterPost.call(this, "api/users/get", { + View: { ApiGetMailAddresses: true }, + Where: { MailAddress: targetEmail }, +}); +const userId = userRes.Response.Data[0]?.UserId; +if (!userId) throw new Error(`対象者が見つかりません: ${targetEmail}`); + +const recordRes = await pleasanterPost.call(this, `api/items/${config.HEALTHCHECK_SITE_ID}/get`, { + View: { + ColumnFilterHash: { ClassC: String(userId) }, + ColumnFilterSearchTypes: { ClassC: "ExactMatch" }, + }, +}); +const candidates = (recordRes.Response.Data || []).filter((r) => r.Status !== 900 && r.Status !== 910); +if (candidates.length !== 1) { + throw new Error(`対象レコードを一意に特定できません: ${candidates.length}件`); +} +const record = candidates[0]; return [{ json: { - targetEmail: source.userId, + config, + resultId: record.ResultId, + currentStatus: record.Status, contentType: content.type, text: content.type === "text" ? content.text : null, fileId: content.type === "file" ? content.fileId : null, @@ -1123,28 +1181,29 @@ return [{ }]; ``` -4. `Data Table`ノード「PLEASANTER_BASE_URL取得」(`configKey: "PLEASANTER_BASE_URL_PROD"`) -5. `Data Table`ノード「PLEASANTER_API_KEY取得」(`configKey: "PLEASANTER_API_KEY_PROD"`) -6. `Data Table`ノード「HEALTHCHECK_SITE_ID取得」(`configKey: "HEALTHCHECK_SITE_ID"`) -7. `HTTP Request`「対象者UserId解決」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/users/get`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}","View":{"ApiGetMailAddresses":true}}`でtargetEmailからPleasanterUserIdを引く -8. `HTTP Request`「対象レコード検索」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{{ $('HEALTHCHECK_SITE_ID取得').item.json.configValue }}/get`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}","View":{"ColumnFilterHash":{"ClassC":""},"ColumnFilterSearchTypes":{"ClassC":"ExactMatch"}}}` - - 取得結果を`Status`昇順以外(`900`/`910`除外)でフィルタし、`UpdatedTime`降順で1件選ぶ`Code`ノードを挟む。0件/複数件は`throw`でワークフローを失敗させる(6章の異常系方針通り、自動判定しない) -9. `Data Table`ノード「待機状態取得」(`operation: get`): `healthcheck_bot_state`から`resultId`一致行を取得 -10. `Switch`「awaitInput分岐」: `none` / `date` / `file` の3分岐 - - **none分岐**: `Code`で`pendingProcesses`(JSON文字列)をパースし、Task 5の`matchProcessByLabel`相当のロジックで`text`と一致するか判定 - - 一致 → `Code`でTask 5の`classifyAwaitInput`相当のロジックを実行 - - `awaitInput: "none"` → `Execute Workflow`(Task 9、`processId`=一致したProcessId) - - `awaitInput: "date"` / `"file"` → `Data Table`ノード(`operation: update`)で`awaitInput`/`awaitProcessId`/`awaitColumn`を保存 → `HTTP Request`で「日付を入力してください」等の追加メッセージを送信(Task 9のLINEWORKS送信ノード群と同じパターンを再利用、Data Table取得ノードでconfig値を参照する点も同様) - - 不一致 → `Execute Workflow`(Task 9、`processId: null`)でフォールバック再送 - - **date分岐**: `Code`でTask 3の`parseDateInput`を実行 - - 成功 → `HTTP Request`「該当列update」: `POST {{ $('PLEASANTER_BASE_URL取得').item.json.configValue }}api/items/{resultId}/update`、body `{"ApiVersion":1.1,"ApiKey":"{{ $('PLEASANTER_API_KEY取得').item.json.configValue }}", "DateHash":{"": ""}}` → `Data Table`で`awaitInput`を`none`に戻す → `Execute Workflow`(Task 9、`processId`=`awaitProcessId`) - - 失敗 → エラーメッセージを再送(待機状態は維持、`Data Table`更新なし) - - **file分岐**: `contentType`が`file`でなければ再送要求。`file`なら`HTTP Request`でLINEWORKSファイルダウンロード→`HTTP Request`でPleasanter添付アップロード(具体的なエンドポイントは実装時にPleasanter公式マニュアル`api-attachment`系を確認する)→`Data Table`更新→`Execute Workflow`(Task 9) -11. `Respond to Webhook`(各分岐の末尾で`{"result":"ok"}`を返す) +4. `Data Table`ノード「待機状態取得」(`operation: get`, `filters.conditions: [{keyName:"resultId", keyValue:"={{ $json.resultId }}"}]`): `healthcheck_bot_state`から該当resultIdの行を取得 +5. `Code`「分岐処理・アクション決定」: `awaitInput`(`none`/`date`/`file`、または待機状態自体が無い場合)に応じた全ロジックをここに集約する。以下を1つのCodeノードで行う: + - 待機状態が無い、または`awaitInput`が想定外の値 → `action: "error"` + - `awaitInput: "none"`(選択肢待ち): `pendingProcesses`(JSON文字列)をパースし、Task 5の`matchProcessByLabel`相当で`text`と一致するか判定 + - 一致・追加入力対象列なし → `action: "execute"`, `processId` + - 一致・追加入力対象列が`Date*`/`Attachments*` → `action: "send"`(追加入力を促すメッセージ)、次の待機状態(`awaitInput: "date"|"file"`, `awaitProcessId`, `awaitColumn`) + - 不一致 → `action: "execute"`, `processId: null`(フォールバック再送、HC-SUBが現在Statusの案内を再送する) + - `awaitInput: "date"`: Task 3の`parseDateInput`を実行 + - 成功 → `this.helpers.httpRequest`で該当列(`DateHash`)をupdate → `action: "execute"`, `processId: awaitProcessId`, 待機状態を`none`に戻す + - 失敗 → `action: "send"`(エラーメッセージ)、待機状態は維持 + - `awaitInput: "file"`: `contentType`が`file`でなければ`action: "send"`(再送要求)、待機状態維持。`file`なら`this.helpers.httpRequest`でLINEWORKSファイルダウンロード→Pleasanter添付アップロード(具体的なエンドポイントは実装時にPleasanter公式マニュアル`api-attachment`系を確認する)→`action: "execute"`, `processId: awaitProcessId`、待機状態を`none`に戻す + - 出力に必ず「次に`healthcheck_bot_state`へ書き込むべき状態」(`nextAwaitInput`, `nextAwaitProcessId`, `nextAwaitColumn`)を含める(ノード6で無条件更新するため) +6. `Data Table`ノード「状態更新」(`operation: update`、`resultId`をキーに`nextAwaitInput`等を書き込む。無条件で1回呼ぶ) +7. `IF`「`action == "execute"`」 + - true分岐 → `Execute Workflow`(Task 9、`processId`)→ `Respond to Webhook`(成功) + - false分岐 → 次へ +8. `IF`「`action == "send"`」 + - true分岐 → `Code`「JWTクレーム組み立て」(設定値Mapの`LW_BOT_CLIENT_ID`/`LW_BOT_SERVICE_ACCOUNT`を使用)→ `JWT`ノード(Credential: 「LINEWORKS Bot Private Key (v4)」、id `Hw0qlEaGfLPnQWp1`)→ `Code`「アクセストークン取得・LINEWORKS送信」(Task 9のノード6と同じ`this.helpers.httpRequest`パターン)→ `Respond to Webhook`(成功) + - false分岐 → `Respond to Webhook`(`action == "error"`のケース。エラー内容を返す) - [ ] **Step 1: ワークフローJSONを作成** -上記ノード構成に従い`workflows/hc-wa-lineworks-response.json`を作成する。`none`分岐・`date`分岐の`Code`ノードにはTask 3・5のロジックをそのまま貼り付ける。 +上記ノード構成に従い`workflows/hc-wa-lineworks-response.json`を作成する。ノード5の`Code`ノードにはTask 3・5のロジックをそのまま貼り付ける。 - [ ] **Step 2: デプロイ**