計画書Task9-11をCodeノード集約方式に全面改訂(基本ルール反映)

ユーザー指示により、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 <noreply@anthropic.com>
This commit is contained in:
Kenichiro NOGI 2026-09-05 18:02:26 +09:00
parent 3dccdefdcb
commit 935e58e384

View File

@ -18,6 +18,12 @@
- Pleasanter日本語ボディを含むリクエストはシェル引数に直書きせず、Writeツールでファイル化してから`curl --data-binary "@file"`で送る同ガイド7-6参照 - Pleasanter日本語ボディを含むリクエストはシェル引数に直書きせず、Writeツールでファイル化してから`curl --data-binary "@file"`で送る同ガイド7-6参照
- 具体的なProcess内容①日程通知〜③検査結果受取り等の本番仕様は本計画のスコープ外。本計画は「Process流用型フロー定義」の枠組みを動かすことがゴールで、テスト用Process1件で疎通確認する - 具体的なProcess内容①日程通知〜③検査結果受取り等の本番仕様は本計画のスコープ外。本計画は「Process流用型フロー定義」の枠組みを動かすことがゴールで、テスト用Process1件で疎通確認する
- 設計書8章の社員マスタ(504412)によるメールアドレス整合性チェック・フリガナ補完は、Bot対話フローと独立した別タスクとして扱う。本計画には含まない - 設計書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ワークフロー — プロセス実行+案内送信 ### 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:** **Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json` - 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に登録済みの設定値 - 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`ノードで呼び出す - 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ードを分けない
このワークフローで使うキー(今回追加登録済み): このワークフローで使うキー今回追加登録済み、値は取得済みの全行から後続Codeードでフィルタする:
- `PLEASANTER_BASE_URL_PROD`(既存) `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`
- `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値に揃えて一元管理する
**ノード構成:** **ード構成7ード:**
1. `Execute Workflow Trigger`(入力: `resultId`, `processId` 1. `Execute Workflow Trigger`(入力: `resultId`, `processId`
2. `Data Table`ード「PLEASANTER_BASE_URL取得」`configKey: "PLEASANTER_BASE_URL_PROD"` 2. `Data Table`ノード「設定値一括取得」(`operation: "get"`, `dataTableId: bNkadTyDgDepYx2p`, `filters`無し, `returnAll: true`
3. `Data Table`ード「PLEASANTER_API_KEY取得」`configKey: "PLEASANTER_API_KEY_PROD"` 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. `Data Table`ード「HEALTHCHECK_SITE_ID取得」`configKey: "HEALTHCHECK_SITE_ID"` 4. `Code`「JWTクレーム組み立て」: `iss`/`sub`に設定値Mapの`LW_BOT_CLIENT_ID`/`LW_BOT_SERVICE_ACCOUNT`を使ってJWTクレームJSON文字列を組み立てる。あわせて選択肢からLINEWORKS `button_template`のactions配列を組み立てる
5. `IF`: `processId`が存在するか 5. `JWT`ノード(`operation: sign`, `algorithm: RS256`, Credential: 「LINEWORKS Bot Private Key (v4)」、id `Hw0qlEaGfLPnQWp1`
- 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 }}}` 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`
- false分岐 → そのまま次へ(フォールバック再送のケース) 7. `Data Table`ノード「状態更新」: `healthcheck_bot_state`id: `jqMDa2YZTI4f0iQ7`)へ`resultId`をキーに`insert``currentStatus`, `pendingProcesses`=JSON化したoptions, `awaitInput: "none"`)。`updatedAt`列は存在しないTask 8参照ため送信対象に含めない
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 }}"}` Step 3のCodeードの中身骨格。各`this.helpers.httpRequest`呼び出しの`body`はTask 4〜7で確認したAPI形式に沿って埋める:
8. `Code`「選択肢組み立て」: Task 4の`fillTemplate`とTask 5の`extractProcessesForStatus`をそのまま貼り付けて使う
```javascript ```javascript
// Codeード「選択肢組み立て」の中身 // Codeード「Pleasanter照会・選択肢組み立て」の中身
function isUnsetSentinel(value) { function isUnsetSentinel(value) {
return typeof value === "string" && value.startsWith("1899"); 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); return processes.filter((p) => p.CurrentStatus === status || p.CurrentStatus === -1);
} }
const record = $('レコード取得').item.json.Response.Data; const configRows = $('設定値一括取得').all().map((item) => item.json);
const siteSettings = $('サイト設定取得').item.json.Response.Data.SiteSettings; 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 columns = siteSettings.Columns || [];
const processes = siteSettings.Processes || []; 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 = { const valueHash = {
...record.ClassHash, ...record.NumHash, ...record.DateHash, ...record.DescriptionHash, ...record.ClassHash, ...record.NumHash, ...record.DateHash, ...record.DescriptionHash,
}; };
@ -928,33 +957,27 @@ const options = candidates.map((p) => ({
tooltip: fillTemplate(p.ToolTip || "", columns, valueHash), tooltip: fillTemplate(p.ToolTip || "", columns, valueHash),
})); }));
const userRes = await pleasanterPost.call(this, `api/users/get`, {
View: { ApiGetMailAddresses: true },
Where: { UserId: record.ClassHash.ClassC },
});
return [{ return [{
json: { json: {
resultId: record.ResultId, resultId: record.ResultId,
currentStatus: record.Status, currentStatus: record.Status,
options, options,
classCUserId: record.ClassHash.ClassC, targetEmail: userRes.Response.Data[0]?.MailAddress,
config,
}, },
}]; }];
``` ```
`ToolTip`のキー名はTask 2の実機調査結果で確定させ、異なる場合はここだけ修正する `ToolTip`のキー名はTask 2の実機調査結果で確定させ、異なる場合はここだけ修正する。上記は骨格であり、実装時にPleasanter API実レスポンスの構造に合わせて調整する
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参照ため送信対象に含めない
- [ ] **Step 1: ワークフローJSON雛形を作成** - [ ] **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: デプロイ** - [ ] **Step 2: デプロイ**
@ -994,12 +1017,12 @@ git commit -m "feat: HC-SUBワークフロー(プロセス実行+案内送信)
- Consumes: Task 9のワークフローID`Execute Workflow`ノードで参照) - Consumes: Task 9のワークフローID`Execute Workflow`ノードで参照)
- Produces: Webhook `POST https://n8n32.next-hd.net/webhook/healthcheck-status-push``X-Api-Key`ヘッダー認証、body `{resultId, processId}` - 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` 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(...)`でワークフローを失敗させる) 3. `Code`「検証」: `X-Api-Key`ヘッダーとbodyの`resultId`/`processId`必須チェック(不正なら`throw new Error(...)`でワークフローを失敗させる)
4. `Execute Workflow`Task 9のワークフローIDを指定、入力: `resultId`, `processId` 4. `Execute Workflow`Task 9のワークフローIDを指定、入力: `resultId`, `processId`
5. `Respond to Webhook``{"result":"ok"}`を返す) 5. `Respond to Webhook``{"result":"ok"}`を返す)
@ -1009,7 +1032,8 @@ git commit -m "feat: HC-SUBワークフロー(プロセス実行+案内送信)
`workflows/hc-wp-status-push.json`を作成。Codeード「検証」の中身: `workflows/hc-wp-status-push.json`を作成。Codeード「検証」の中身:
```javascript ```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 || {}; const headers = $input.first().json.headers || {};
if (headers["x-api-key"] !== expectedApiKey) { if (headers["x-api-key"] !== expectedApiKey) {
throw new Error("Unauthorized: invalid API key"); 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 - 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`検証) - 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` 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着手時点で未登録なら先に登録する 2. `Data Table`ノード「設定値一括取得」(`operation: "get"`, `dataTableId: bNkadTyDgDepYx2p`, `filters`無し, `returnAll: true`
3. `Code`「署名検証・送信者解決」: Task 6の`verifySignature`を貼り付けて検証。失敗なら`throw`。成功したら`source.userId`(=メールアドレス)と、`content.type`に応じたテキスト/ファイル情報を抽出する 3. `Code`「署名検証・対象レコード特定」: Task 6の`verifySignature`で検証(失敗なら`throw`)。成功したら`this.helpers.httpRequest`で対象者メール→PleasanterUserId解決、508971の対象レコード検索`ColumnFilterHash`までをこの1ードで行う。0件/複数件は`throw`6章の異常系方針、自動判定しない
```javascript ```javascript
// Codeード「署名検証・送信者解決」の中身 // Codeード「署名検証・対象レコード特定」の中身
const crypto = require("crypto"); const crypto = require("crypto");
function normalizeSignature(value) { function normalizeSignature(value) {
return String(value || "").trim().replace(/^sha256=/i, ""); return String(value || "").trim().replace(/^sha256=/i, "");
@ -1097,7 +1123,8 @@ function verifySignature(rawBody, headerSignature, botSecret) {
return safeEqual(headerSig, expected); 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 item = $input.first();
const headers = item.json.headers || {}; 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(item.binary.data.data, "base64")
: Buffer.from(JSON.stringify(item.json.body || {}), "utf8"); : 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"); throw new Error("Unauthorized: signature mismatch");
} }
const body = item.json.body || {}; const body = item.json.body || {};
const source = body.source || {}; const source = body.source || {};
const content = body.content || {}; 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 [{ return [{
json: { json: {
targetEmail: source.userId, config,
resultId: record.ResultId,
currentStatus: record.Status,
contentType: content.type, contentType: content.type,
text: content.type === "text" ? content.text : null, text: content.type === "text" ? content.text : null,
fileId: content.type === "file" ? content.fileId : null, fileId: content.type === "file" ? content.fileId : null,
@ -1123,28 +1181,29 @@ return [{
}]; }];
``` ```
4. `Data Table`ード「PLEASANTER_BASE_URL取得」`configKey: "PLEASANTER_BASE_URL_PROD"` 4. `Data Table`ノード「待機状態取得」(`operation: get`, `filters.conditions: [{keyName:"resultId", keyValue:"={{ $json.resultId }}"}]`: `healthcheck_bot_state`から該当resultIdの行を取得
5. `Data Table`ード「PLEASANTER_API_KEY取得」`configKey: "PLEASANTER_API_KEY_PROD"` 5. `Code`「分岐処理・アクション決定」: `awaitInput``none`/`date`/`file`、または待機状態自体が無い場合に応じた全ロジックをここに集約する。以下を1つのCodeードで行う:
6. `Data Table`ード「HEALTHCHECK_SITE_ID取得」`configKey: "HEALTHCHECK_SITE_ID"` - 待機状態が無い、または`awaitInput`が想定外の値 → `action: "error"`
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を引く - `awaitInput: "none"`(選択肢待ち): `pendingProcesses`JSON文字列をパースし、Task 5の`matchProcessByLabel`相当で`text`と一致するか判定
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":"<Step7で解決したUserId>"},"ColumnFilterSearchTypes":{"ClassC":"ExactMatch"}}}` - 一致・追加入力対象列なし → `action: "execute"`, `processId`
- 取得結果を`Status`昇順以外(`900`/`910`除外)でフィルタし、`UpdatedTime`降順で1件選ぶ`Code`ードを挟む。0件/複数件は`throw`でワークフローを失敗させる6章の異常系方針通り、自動判定しない - 一致・追加入力対象列が`Date*`/`Attachments*` → `action: "send"`(追加入力を促すメッセージ)、次の待機状態(`awaitInput: "date"|"file"`, `awaitProcessId`, `awaitColumn`
9. `Data Table`ノード「待機状態取得」(`operation: get`: `healthcheck_bot_state`から`resultId`一致行を取得 - 不一致 → `action: "execute"`, `processId: null`フォールバック再送、HC-SUBが現在Statusの案内を再送する
10. `Switch`「awaitInput分岐」: `none` / `date` / `file` の3分岐 - `awaitInput: "date"`: Task 3の`parseDateInput`を実行
- **none分岐**: `Code`で`pendingProcesses`(JSON文字列)をパースし、Task 5の`matchProcessByLabel`相当のロジックで`text`と一致するか判定 - 成功 → `this.helpers.httpRequest`で該当列(`DateHash`をupdate → `action: "execute"`, `processId: awaitProcessId`, 待機状態を`none`に戻す
- 一致 → `Code`でTask 5の`classifyAwaitInput`相当のロジックを実行 - 失敗 → `action: "send"`(エラーメッセージ)、待機状態は維持
- `awaitInput: "none"``Execute Workflow`Task 9、`processId`=一致したProcessId - `awaitInput: "file"`: `contentType`が`file`でなければ`action: "send"`(再送要求)、待機状態維持。`file`なら`this.helpers.httpRequest`でLINEWORKSファイルダウンロード→Pleasanter添付アップロード具体的なエンドポイントは実装時にPleasanter公式マニュアル`api-attachment`系を確認する)→`action: "execute"`, `processId: awaitProcessId`、待機状態を`none`に戻す
- `awaitInput: "date"` / `"file"``Data Table`ノード(`operation: update`)で`awaitInput`/`awaitProcessId`/`awaitColumn`を保存 → `HTTP Request`で「日付を入力してください」等の追加メッセージを送信Task 9のLINEWORKS送信ード群と同じパターンを再利用、Data Table取得ードでconfig値を参照する点も同様 - 出力に必ず「次に`healthcheck_bot_state`へ書き込むべき状態」(`nextAwaitInput`, `nextAwaitProcessId`, `nextAwaitColumn`を含めるード6で無条件更新するため
- 不一致 → `Execute Workflow`Task 9、`processId: null`)でフォールバック再送 6. `Data Table`ノード「状態更新」(`operation: update`、`resultId`をキーに`nextAwaitInput`等を書き込む。無条件で1回呼ぶ
- **date分岐**: `Code`でTask 3の`parseDateInput`を実行 7. `IF`「`action == "execute"`」
- 成功 → `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":{"<awaitColumn>": "<parseDateInputの結果>"}}``Data Table`で`awaitInput`を`none`に戻す → `Execute Workflow`Task 9、`processId`=`awaitProcessId` - true分岐 → `Execute Workflow`Task 9、`processId`)→ `Respond to Webhook`(成功)
- 失敗 → エラーメッセージを再送(待機状態は維持、`Data Table`更新なし) - false分岐 → 次へ
- **file分岐**: `contentType`が`file`でなければ再送要求。`file`なら`HTTP Request`でLINEWORKSファイルダウンロード→`HTTP Request`でPleasanter添付アップロード具体的なエンドポイントは実装時にPleasanter公式マニュアル`api-attachment`系を確認する)→`Data Table`更新→`Execute Workflow`Task 9 8. `IF`「`action == "send"`」
11. `Respond to Webhook`(各分岐の末尾で`{"result":"ok"}`を返す) - 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を作成** - [ ] **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: デプロイ** - [ ] **Step 2: デプロイ**