ken_nogi/NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md
Kenichiro NOGI 3b904e6214 健康診断管理LINEWORKS連携の実装計画を追加
設計書(2026-09-05-healthcheck-lineworks-survey-n8n-design.md)を基に、
12タスクの実装計画を作成。純粋ロジック(日付パース/プレースホルダー
置換/Process抽出/署名検証)はNode.jsでTDD、n8nワークフロー構築は
既存のLINEWORKS通知送信ワークフロー(JWT署名→トークン取得→Bot API
送信)とdataTableノードの実機パターンを踏襲する形で具体化した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 11:25:45 +09:00

1280 lines
53 KiB
Markdown
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.

# 健康診断管理×LINEWORKS Bot連携 n8n実装 実装計画
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** プリザンター「健康診断管理」SiteId 508971のProcess機能を定義源として、LINEWORKS Botとの対話でStatusを段階的に進める仕組みをn8n上に構築する。
**Architecture:** n8nに3本のワークフローHC-SUB: プロセス実行案内送信の共通ロジックHC-WP: 担当者操作起点のプッシュ通知HC-WA: LINEWORKS応答受信の唯一の受信口を構築する。フロー定義は508971の`SiteSettings.Processes`現在の状況変更後の状況表示名ツールチップ入力検証タブをそのまま使い、新規マスタは作らない。会話の待機状態はn8n Data Table新規で保持する。純粋ロジック日付パース・プレースホルダー置換・Process抽出・署名検証はNode.jsモジュールとしてTDDで開発し、動作確認済みのコードをn8n Codeードへ書き写す。
**Tech Stack:** Node.js`node --test`によるユニットテスト、追加npmパッケージなし、n8nPublic API経由でのワークフロー・Data Table構築、LINE WORKS Bot API、Pleasanter API
## Global Constraints
- n8nはコンテナメモリ768MB制限。複数行データの一括展開・集約は行わない`NodeSrv/apps/n8n/docs/n8n-guide.md` 7-2参照
- n8n Data Table操作は「Clear→Insert」を直列に繋がない。後続ードは前段ードを`$('ノード名')`で明示的に再参照する同ガイド7-1参照
- サーバー環境の指定は本番のみ対象(`https://nextoffice.next-hd.co.jp/pleasanter/`)。テスト環境は今回のスコープ外
- n8nワークフローの構築・編集PUT/POSTは確認不要。**Webhookを実際に叩く・508971へ書き込みを伴うテスト実行は毎回ユーザーへ事前確認**同ガイド9章
- 508971は本番の健診データそのもの。検証は既存レコードを壊さない捨てレコードで行う
- Pleasanter日本語ボディを含むリクエストはシェル引数に直書きせず、Writeツールでファイル化してから`curl --data-binary "@file"`で送る同ガイド7-6参照
- 具体的なProcess内容①日程通知〜③検査結果受取り等の本番仕様は本計画のスコープ外。本計画は「Process流用型フロー定義」の枠組みを動かすことがゴールで、テスト用Process1件で疎通確認する
---
## File Structure
```
NodeSrv/apps/healthcheck-survey-bot/
package.json
README.md -- 実機調査結果、n8nリソースID一覧を記録
src/lib/
dateParser.js -- 日付文字列パース(和暦・月日省略対応)
templateFill.js -- ツールチップ文言の{ラベル名}プレースホルダー置換
processFlow.js -- Processes配列からの選択肢抽出・回答照合・追加入力種別判定
signatureVerify.js -- LINEWORKS Webhook署名検証(HMAC-SHA256)
test/
dateParser.test.js
templateFill.test.js
processFlow.test.js
signatureVerify.test.js
scripts/
n8n-api.js -- n8n Public API共通fetchヘルパー
deploy-workflow.js -- workflows/*.json を n8n へPUT/POSTするCLI
workflows/
hc-sub-run-process-and-notify.json
hc-wp-status-push.json
hc-wa-lineworks-response.json
```
n8n環境の接続情報URL・APIキー・既存Credentialは`NodeSrv/apps/n8n/docs/n8n-guide.md`参照。Pleasanter本番APIキーは`Pleasanter/config_production.json`参照。
---
### Task 1: プロジェクト雛形作成
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/package.json`
- Create: `NodeSrv/apps/healthcheck-survey-bot/README.md`
- Create: `NodeSrv/apps/healthcheck-survey-bot/.gitignore`
**Interfaces:**
- Produces: `node --test test/*.test.js`で実行できるテスト環境
- [ ] **Step 1: package.json作成**
```json
{
"name": "healthcheck-survey-bot",
"version": "0.1.0",
"private": true,
"type": "commonjs",
"scripts": {
"test": "node --test test/*.test.js",
"deploy-workflow": "node scripts/deploy-workflow.js"
}
}
```
- [ ] **Step 2: README.md作成**
```markdown
# healthcheck-survey-bot
健康診断管理(SiteId 508971)×LINEWORKS Bot連携。508971の`SiteSettings.Processes`
をフロー定義として使い、n8n上でBot対話型のStatus管理を行う。
設計書: `NodeSrv/docs/superpowers/specs/2026-09-05-healthcheck-lineworks-survey-n8n-design.md`
実装計画: `NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md`
## 実機調査メモ
(Task 2完了後、ここにPleasanterのProcess入力検証タブのJSON構造を記録する)
## n8nリソースID一覧
(Task 8〜11完了後、ここに作成したData Table ID・ワークフローIDを記録する)
```
- [ ] **Step 3: .gitignore作成**
```
node_modules/
```
- [ ] **Step 4: コミット**
```bash
git add NodeSrv/apps/healthcheck-survey-bot/package.json NodeSrv/apps/healthcheck-survey-bot/README.md NodeSrv/apps/healthcheck-survey-bot/.gitignore
git commit -m "feat: healthcheck-survey-botプロジェクト雛形を追加"
```
---
### Task 2: 実機調査 — Processの入力検証タブのJSON構造を確認
Pleasanter公式マニュアルにも構造の記載がなく、508971の既存Processesには入力検証を使った実例がないため、実機で確認する。**508971本番へのSiteSettings変更を伴うため、着手前に必ずユーザーへ確認する。**
**Files:**
- Modify: `NodeSrv/apps/healthcheck-survey-bot/README.md`(調査結果を追記)
**Interfaces:**
- Produces: Task 5processFlow.jsが前提とする、Process内で入力検証対象列を表すJSONキー名とその構造
- [ ] **Step 1: ユーザーへテストProcess作成を依頼**
以下をプリザンター508971の「プロセス」設定画面で新規作成してもらうよう、ユーザーに依頼する本番Statusに影響しないよう、現在の状況・変更後の状況は同じ値のままにしてもらう
- 名称: `テスト_入力検証構造確認`
- 表示名: `テスト`
- 現在の状況/変更後の状況: 触らず空欄のまま(全状況共通ボタンにしてもらう)
- 入力検証タブ: 「新規作成」→項目に`日付001`Date001を選択して保存
- Process自体を保存
- [ ] **Step 2: 最新のSiteSettingsを取得**
```bash
cd "C:\Users\k.nogi\#GitHub\ken_nogi\dev\Pleasanter"
node .claude/js/get-site-config.js --project="健康診断管理" --env=production
```
Expected: `健康診断管理/configs/production/site-508971_健康診断管理/sitesettings/site-508971_latest.json`が更新される
- [ ] **Step 3: Processes配列の該当エントリを確認**
`site-508971_latest.json`内の`Response.Data.SiteSettings.Processes`配列から、Step1で追加したProcess`Name`が`テスト_入力検証構造確認`のもの)を探し、入力検証設定に対応するキー(配列名・各要素のプロパティ名)を特定する。
- [ ] **Step 4: 発見した構造をREADME.mdに記録**
例(実際のキー名は実機確認結果に置き換える):
```markdown
## 実機調査メモ2026-09-05確認
Processの入力検証タブは`SiteSettings.Processes[].Validations`配列として保存される。
各要素は`{"Id": 1, "ColumnName": "Date001"}`形式(フィールド名は実機確認結果で確定)。
```
- [ ] **Step 5: テストProcessを削除**
ユーザーに508971の管理画面からテストProcessを削除してもらう本番のProcess一覧を汚さないため
- [ ] **Step 6: コミット**
```bash
git add NodeSrv/apps/healthcheck-survey-bot/README.md
git commit -m "docs: Process入力検証タブのJSON構造を調査結果として記録"
```
---
### Task 3: dateParser.js — 日付パースロジック
Express版`OldCode/express/modules/lineworksSurvey.js`の`parseDateInput`系ロジックを移植する。
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/src/lib/dateParser.js`
- Test: `NodeSrv/apps/healthcheck-survey-bot/test/dateParser.test.js`
**Interfaces:**
- Produces: `parseDateInput(value: string): string``"YYYY-MM-DD"`形式を返す。パース不能なら`Error`をthrow
- [ ] **Step 1: 失敗するテストを書く**
```javascript
// test/dateParser.test.js
const { test } = require("node:test");
const assert = require("node:assert");
const { parseDateInput } = require("../src/lib/dateParser");
test("ISO形式の日付をそのまま解釈する", () => {
assert.strictEqual(parseDateInput("2026-03-01"), "2026-03-01");
});
test("スラッシュ区切りの日付を解釈する", () => {
assert.strictEqual(parseDateInput("2026/3/1"), "2026-03-01");
});
test("和暦(令和)を西暦に変換する", () => {
assert.strictEqual(parseDateInput("令和6年3月1日"), "2024-03-01");
});
test("和暦の略記(R)を西暦に変換する", () => {
assert.strictEqual(parseDateInput("R6.3.1"), "2024-03-01");
});
test("月日のみの入力は今年として解釈する", () => {
const currentYear = new Date().getFullYear();
assert.strictEqual(parseDateInput("3/1"), `${currentYear}-03-01`);
});
test("空文字はエラーになる", () => {
assert.throws(() => parseDateInput(""), /日付が空です/);
});
test("存在しない日付はエラーになる", () => {
assert.throws(() => parseDateInput("2026-02-30"), /存在しない日付です/);
});
test("解釈不能な文字列はエラーになる", () => {
assert.throws(() => parseDateInput("あいうえお"));
});
```
- [ ] **Step 2: テストが失敗することを確認**
```bash
cd "C:\Users\k.nogi\#GitHub\ken_nogi\dev\NodeSrv\apps\healthcheck-survey-bot"
node --test test/dateParser.test.js
```
Expected: `Cannot find module '../src/lib/dateParser'`で失敗
- [ ] **Step 3: 実装を書く**
```javascript
// src/lib/dateParser.js
const ERA_INFO = {
"令和": 2018,
"平成": 1988,
"昭和": 1925,
"大正": 1911,
};
const ERA_ALIASES = { R: "令和", H: "平成", S: "昭和", T: "大正" };
function pad2(value) {
return String(value).padStart(2, "0");
}
function finalizeDateParts(year, month, day) {
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
throw new Error("日付形式で回答してください(例: 2026-03-01");
}
const date = new Date(year, month - 1, day);
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
throw new Error("存在しない日付です");
}
return `${year}-${pad2(month)}-${pad2(day)}`;
}
function tryParseEraDate(compact) {
const match = compact.match(/^(令和|平成|昭和|大正|R|H|S|T)(.+)$/i);
if (!match) return null;
let era = match[1];
if (/^[RHST]$/i.test(era)) {
era = ERA_ALIASES[era.toUpperCase()] || era;
}
if (!ERA_INFO[era]) return null;
const normalized = match[2]
.replace(/年/g, "-")
.replace(/月/g, "-")
.replace(/日/g, "")
.replace(/[.\/]/g, "-");
const parts = normalized.split("-").filter((part) => part.length > 0);
if (parts.length < 3) {
throw new Error("月と日まで入力してください(例: 令和6年3月1日");
}
const eraYear = Number(parts[0]);
const month = Number(parts[1]);
const day = Number(parts[2]);
if (!Number.isFinite(eraYear) || !Number.isFinite(month) || !Number.isFinite(day)) {
throw new Error("日付形式で回答してください(例: 令和6年3月1日");
}
return finalizeDateParts(ERA_INFO[era] + eraYear - 1, month, day);
}
function tryParseMonthDay(compact) {
const match = compact.match(/^(\d{1,2})(?:月|\/|-|\.)(\d{1,2})(?:日)?$/);
if (!match) return null;
const currentYear = new Date().getFullYear();
return finalizeDateParts(currentYear, Number(match[1]), Number(match[2]));
}
function parseDateInput(value) {
const trimmed = String(value ?? "").trim();
if (!trimmed) {
throw new Error("日付が空です");
}
const compact = trimmed.replace(/\s+/g, "");
const eraResult = tryParseEraDate(compact);
if (eraResult) return eraResult;
const monthDayResult = tryParseMonthDay(compact);
if (monthDayResult) return monthDayResult;
const normalized = compact
.replace(/年/g, "-")
.replace(/月/g, "-")
.replace(/日/g, "")
.replace(/[.\/]/g, "-");
const isoParts = normalized.split("-").filter((part) => part.length > 0);
if (isoParts.length === 3 && isoParts[0].length >= 4) {
return finalizeDateParts(Number(isoParts[0]), Number(isoParts[1]), Number(isoParts[2]));
}
const parsed = new Date(trimmed);
if (Number.isNaN(parsed.getTime())) {
throw new Error("日付形式で回答してください(例: 2026-03-01 や 令和6年3月1日");
}
return finalizeDateParts(parsed.getFullYear(), parsed.getMonth() + 1, parsed.getDate());
}
module.exports = { parseDateInput };
```
- [ ] **Step 4: テストが通ることを確認**
```bash
node --test test/dateParser.test.js
```
Expected: 8 tests、全てPASS
- [ ] **Step 5: コミット**
```bash
git add src/lib/dateParser.js test/dateParser.test.js
git commit -m "feat: 日付パースロジックを追加"
```
---
### Task 4: templateFill.js — ツールチップ文言のプレースホルダー置換
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/src/lib/templateFill.js`
- Test: `NodeSrv/apps/healthcheck-survey-bot/test/templateFill.test.js`
**Interfaces:**
- Consumes: なしTask 3とは独立
- Produces: `fillTemplate(template: string, columns: Array<{ColumnName: string, LabelText?: string}>, valueHash: Record<string, unknown>): string`
`columns`はPleasanter `getsite`の`SiteSettings.Columns`配列そのもの。`valueHash`はレコードの`ClassHash`/`NumHash`/`DateHash`/`DescriptionHash`を`{...ClassHash, ...NumHash, ...DateHash, ...DescriptionHash}`のようにマージしたフラットオブジェクト(呼び出し側で用意する)。日付の未設定センチネル値(`"1899-12-30..."`で始まる文字列)は「未設定」として扱う。
- [ ] **Step 1: 失敗するテストを書く**
```javascript
// test/templateFill.test.js
const { test } = require("node:test");
const assert = require("node:assert");
const { fillTemplate } = require("../src/lib/templateFill");
const columns = [
{ ColumnName: "Class003", LabelText: "検査機関" },
{ ColumnName: "Date001", LabelText: "検査日" },
];
test("プレースホルダーをレコード値で置換する", () => {
const result = fillTemplate(
"検査機関: {検査機関}\n日程: {検査日}",
columns,
{ Class003: "next健診クリニック", Date001: "2026-04-01T00:00:00" }
);
assert.strictEqual(result, "検査機関: next健診クリニック\n日程: 2026-04-01T00:00:00");
});
test("未設定の日付センチネル値は「未設定」に変換する", () => {
const result = fillTemplate("日程: {検査日}", columns, {
Date001: "1899-12-30T00:00:00",
});
assert.strictEqual(result, "日程: 未設定");
});
test("値が無い列は「未設定」に変換する", () => {
const result = fillTemplate("検査機関: {検査機関}", columns, {});
assert.strictEqual(result, "検査機関: 未設定");
});
test("対応するラベルが見つからないプレースホルダーはそのまま残す", () => {
const result = fillTemplate("不明: {存在しないラベル}", columns, {});
assert.strictEqual(result, "不明: {存在しないラベル}");
});
```
- [ ] **Step 2: テストが失敗することを確認**
```bash
node --test test/templateFill.test.js
```
Expected: `Cannot find module '../src/lib/templateFill'`で失敗
- [ ] **Step 3: 実装を書く**
```javascript
// src/lib/templateFill.js
function isUnsetSentinel(value) {
return typeof value === "string" && value.startsWith("1899");
}
function fillTemplate(template, columns, valueHash) {
const labelToColumnName = new Map();
for (const column of columns) {
if (column.LabelText) {
labelToColumnName.set(column.LabelText, column.ColumnName);
}
}
return template.replace(/\{([^{}]+)\}/g, (matched, label) => {
const columnName = labelToColumnName.get(label);
if (!columnName) {
return matched;
}
const value = valueHash[columnName];
if (value === undefined || value === null || value === "" || isUnsetSentinel(value)) {
return "未設定";
}
return String(value);
});
}
module.exports = { fillTemplate };
```
- [ ] **Step 4: テストが通ることを確認**
```bash
node --test test/templateFill.test.js
```
Expected: 4 tests、全てPASS
- [ ] **Step 5: コミット**
```bash
git add src/lib/templateFill.js test/templateFill.test.js
git commit -m "feat: ツールチップ文言のプレースホルダー置換ロジックを追加"
```
---
### Task 5: processFlow.js — Process抽出・回答照合・追加入力判定
Task 2で確認したJSON構造を前提に実装する。以下のコードでは、入力検証対象列が`process.Validations`(各要素`{ColumnName: string}`)に入っている前提で書いている。**Task 2の実機調査結果が異なる構造だった場合は、`getValidationColumnNames`関数のみをその構造に合わせて書き換える**(他の関数への影響はない)。
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/src/lib/processFlow.js`
- Test: `NodeSrv/apps/healthcheck-survey-bot/test/processFlow.test.js`
**Interfaces:**
- Consumes: なし
- Produces:
- `extractProcessesForStatus(processes: Array<Process>, status: number): Array<Process>`
- `matchProcessByLabel(processes: Array<Process>, text: string): Process | null`
- `classifyAwaitInput(process: Process): { awaitInput: "none" | "date" | "file", column: string | null }`
- [ ] **Step 1: 失敗するテストを書く**
```javascript
// test/processFlow.test.js
const { test } = require("node:test");
const assert = require("node:assert");
const {
extractProcessesForStatus,
matchProcessByLabel,
classifyAwaitInput,
} = require("../src/lib/processFlow");
const processes = [
{ Id: 1, Name: "了承", DisplayName: "了承", CurrentStatus: 100, ChangedStatus: 200 },
{ Id: 2, Name: "日程変更", DisplayName: "日程変更", CurrentStatus: 100, ChangedStatus: 150, Validations: [{ ColumnName: "Date001" }] },
{ Id: 3, Name: "受けた", DisplayName: "受けた", CurrentStatus: 200, ChangedStatus: 300 },
{ Id: 4, Name: "結果受取り", DisplayName: "受け取った", CurrentStatus: 300, ChangedStatus: 900, Validations: [{ ColumnName: "AttachmentsA" }] },
];
test("extractProcessesForStatus: 現在のStatusに一致するProcessのみ返す", () => {
const result = extractProcessesForStatus(processes, 100);
assert.strictEqual(result.length, 2);
assert.deepStrictEqual(result.map((p) => p.Id), [1, 2]);
});
test("extractProcessesForStatus: 一致するProcessが無ければ空配列", () => {
assert.deepStrictEqual(extractProcessesForStatus(processes, 999), []);
});
test("matchProcessByLabel: DisplayNameが完全一致するProcessを返す", () => {
const candidates = extractProcessesForStatus(processes, 100);
const matched = matchProcessByLabel(candidates, "日程変更");
assert.strictEqual(matched.Id, 2);
});
test("matchProcessByLabel: 前後の空白を無視して一致判定する", () => {
const candidates = extractProcessesForStatus(processes, 100);
const matched = matchProcessByLabel(candidates, " 了承 ");
assert.strictEqual(matched.Id, 1);
});
test("matchProcessByLabel: 一致しなければnull", () => {
const candidates = extractProcessesForStatus(processes, 100);
assert.strictEqual(matchProcessByLabel(candidates, "存在しない選択肢"), null);
});
test("classifyAwaitInput: Validationsが無いProcessはnone", () => {
assert.deepStrictEqual(classifyAwaitInput(processes[0]), { awaitInput: "none", column: null });
});
test("classifyAwaitInput: Date*列はdate", () => {
assert.deepStrictEqual(classifyAwaitInput(processes[1]), { awaitInput: "date", column: "Date001" });
});
test("classifyAwaitInput: Attachments*列はfile", () => {
assert.deepStrictEqual(classifyAwaitInput(processes[3]), { awaitInput: "file", column: "AttachmentsA" });
});
```
- [ ] **Step 2: テストが失敗することを確認**
```bash
node --test test/processFlow.test.js
```
Expected: `Cannot find module '../src/lib/processFlow'`で失敗
- [ ] **Step 3: 実装を書く**
```javascript
// src/lib/processFlow.js
function extractProcessesForStatus(processes, status) {
return processes.filter((p) => p.CurrentStatus === status || p.CurrentStatus === -1);
}
function matchProcessByLabel(processes, text) {
const trimmed = String(text ?? "").trim();
return processes.find((p) => (p.DisplayName || p.Name) === trimmed) || null;
}
function getValidationColumnNames(process) {
if (!Array.isArray(process.Validations)) return [];
return process.Validations.map((v) => v.ColumnName).filter(Boolean);
}
function classifyAwaitInput(process) {
const columnNames = getValidationColumnNames(process);
if (columnNames.length === 0) {
return { awaitInput: "none", column: null };
}
const column = columnNames[0];
if (column.startsWith("Date")) {
return { awaitInput: "date", column };
}
if (column.startsWith("Attachments")) {
return { awaitInput: "file", column };
}
return { awaitInput: "none", column: null };
}
module.exports = { extractProcessesForStatus, matchProcessByLabel, classifyAwaitInput };
```
- [ ] **Step 4: テストが通ることを確認**
```bash
node --test test/processFlow.test.js
```
Expected: 8 tests、全てPASS
- [ ] **Step 5: コミット**
```bash
git add src/lib/processFlow.js test/processFlow.test.js
git commit -m "feat: Process抽出・回答照合・追加入力判定ロジックを追加"
```
---
### Task 6: signatureVerify.js — LINEWORKS Webhook署名検証
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/src/lib/signatureVerify.js`
- Test: `NodeSrv/apps/healthcheck-survey-bot/test/signatureVerify.test.js`
**Interfaces:**
- Produces: `verifySignature(rawBody: Buffer | string, headerSignature: string, botSecret: string): boolean`
- [ ] **Step 1: 失敗するテストを書く**
```javascript
// test/signatureVerify.test.js
const { test } = require("node:test");
const assert = require("node:assert");
const crypto = require("node:crypto");
const { verifySignature } = require("../src/lib/signatureVerify");
test("正しい署名はtrueを返す", () => {
const secret = "test-secret";
const body = JSON.stringify({ hello: "world" });
const signature = crypto.createHmac("sha256", secret).update(body).digest("base64");
assert.strictEqual(verifySignature(body, signature, secret), true);
});
test("sha256=プレフィックス付き署名も検証できる", () => {
const secret = "test-secret";
const body = JSON.stringify({ hello: "world" });
const signature = crypto.createHmac("sha256", secret).update(body).digest("base64");
assert.strictEqual(verifySignature(body, `sha256=${signature}`, secret), true);
});
test("不正な署名はfalseを返す", () => {
assert.strictEqual(verifySignature("body", "invalid-signature", "secret"), false);
});
test("署名ヘッダーが空ならfalseを返す", () => {
assert.strictEqual(verifySignature("body", "", "secret"), false);
});
```
- [ ] **Step 2: テストが失敗することを確認**
```bash
node --test test/signatureVerify.test.js
```
Expected: `Cannot find module '../src/lib/signatureVerify'`で失敗
- [ ] **Step 3: 実装を書く**
```javascript
// src/lib/signatureVerify.js
const crypto = require("node:crypto");
function normalizeSignature(value) {
return String(value || "").trim().replace(/^sha256=/i, "");
}
function safeEqual(a, b) {
const ab = Buffer.from(String(a), "utf8");
const bb = Buffer.from(String(b), "utf8");
if (ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
function verifySignature(rawBody, headerSignature, botSecret) {
const headerSig = normalizeSignature(headerSignature);
if (!headerSig || !botSecret) return false;
const payload = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody), "utf8");
const expected = crypto.createHmac("sha256", botSecret).update(payload).digest("base64");
return safeEqual(headerSig, expected);
}
module.exports = { verifySignature };
```
- [ ] **Step 4: テストが通ることを確認**
```bash
node --test test/signatureVerify.test.js
```
Expected: 4 tests、全てPASS
- [ ] **Step 5: 全テストを通しで実行**
```bash
node --test test/*.test.js
```
Expected: 4ファイル・24テスト、全てPASS
- [ ] **Step 6: コミット**
```bash
git add src/lib/signatureVerify.js test/signatureVerify.test.js
git commit -m "feat: LINEWORKS Webhook署名検証ロジックを追加"
```
---
### Task 7: n8n Public API共通ヘルパーデプロイスクリプト
n8nワークフロー・Data Tableの作成/更新をコマンドから行うための共通スクリプト。APIキーは`NodeSrv/apps/n8n/docs/n8n-guide.md`記載の値を使う。
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/scripts/n8n-api.js`
- Create: `NodeSrv/apps/healthcheck-survey-bot/scripts/deploy-workflow.js`
**Interfaces:**
- Produces:
- `n8nApi.js`: `request(method: string, path: string, body?: object): Promise<{status: number, body: any}>`
- `deploy-workflow.js`: CLI `node scripts/deploy-workflow.js <workflows/xxx.json> [--id=<既存workflowId>]`
- [ ] **Step 1: n8n-api.js を作成**
```javascript
// scripts/n8n-api.js
const N8N_BASE_URL = "https://n8n32.next-hd.net/api/v1";
const N8N_API_KEY = process.env.N8N_API_KEY;
if (!N8N_API_KEY) {
throw new Error(
"環境変数 N8N_API_KEY が未設定です。NodeSrv/apps/n8n/docs/n8n-guide.md 2章のPublic API Keyを設定してください。"
);
}
async function request(method, path, body) {
const res = await fetch(`${N8N_BASE_URL}${path}`, {
method,
headers: {
"X-N8N-API-KEY": N8N_API_KEY,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = text;
}
return { status: res.status, body: json };
}
module.exports = { request };
```
- [ ] **Step 2: deploy-workflow.js を作成**
```javascript
// scripts/deploy-workflow.js
const fs = require("node:fs");
const path = require("node:path");
const { request } = require("./n8n-api");
async function main() {
const [, , filePath, ...rest] = process.argv;
if (!filePath) {
console.error("使い方: node scripts/deploy-workflow.js <workflows/xxx.json> [--id=<既存workflowId>]");
process.exit(1);
}
const idArg = rest.find((a) => a.startsWith("--id="));
const existingId = idArg ? idArg.slice("--id=".length) : null;
const fullPath = path.resolve(filePath);
const definition = JSON.parse(fs.readFileSync(fullPath, "utf8"));
const body = {
name: definition.name,
nodes: definition.nodes,
connections: definition.connections,
settings: definition.settings || {},
};
const { status, body: result } = existingId
? await request("PUT", `/workflows/${existingId}`, body)
: await request("POST", "/workflows", body);
console.log("HTTP status:", status);
console.log(JSON.stringify(result, null, 2));
if (status >= 200 && status < 300 && result.id) {
console.log(`\nワークフローID: ${result.id}`);
console.log("README.mdの「n8nリソースID一覧」へ記録すること。");
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
```
- [ ] **Step 3: 動作確認(既存ワークフローのダミー取得で疎通のみ確認)**
```bash
cd "C:\Users\k.nogi\#GitHub\ken_nogi\dev\NodeSrv\apps\healthcheck-survey-bot"
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node -e "
require('./scripts/n8n-api').request('GET', '/workflows?limit=1').then(r => console.log(r.status));
"
```
Expected: `200`が出力される
- [ ] **Step 4: コミット**
```bash
git add scripts/n8n-api.js scripts/deploy-workflow.js
git commit -m "feat: n8n Public API操作用の共通スクリプトを追加"
```
---
### Task 8: n8n Data Table「healthcheck_bot_state」作成
**Files:**
- Modify: `NodeSrv/apps/healthcheck-survey-bot/README.md`作成したテーブルIDを記録
**Interfaces:**
- Produces: n8n Data Tableテーブル名`healthcheck_bot_state`)。列: `resultId`(string), `targetEmail`(string), `currentStatus`(string), `pendingProcesses`(string, JSON), `awaitInput`(string), `awaitProcessId`(string), `awaitColumn`(string), `updatedAt`(string)
n8n Public APIの`POST /data-tables`の必須パラメータ(`projectId`要否等)は`n8n-guide.md`に記載が無いため、実機で確認しながら進める。
- [ ] **Step 1: 既存Data Table一覧からprojectIdを確認**
```bash
cd "C:\Users\k.nogi\#GitHub\ken_nogi\dev\NodeSrv\apps\healthcheck-survey-bot"
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node -e "
require('./scripts/n8n-api').request('GET', '/data-tables?limit=10').then(r => console.log(JSON.stringify(r.body, null, 2)));
"
```
Expected: 既存テーブル(`workflow_config_values`等)のリストと、それぞれの`projectId`が確認できる
- [ ] **Step 2: Data Table作成を試みる**
Step 1で確認した既存`projectId`のいずれか(`org-master-sync`用の`LOcxF69Gm4PvnkqA`等)を指定して作成を試す。
```bash
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node -e "
require('./scripts/n8n-api').request('POST', '/data-tables', {
name: 'healthcheck_bot_state',
projectId: 'LOcxF69Gm4PvnkqA',
columns: [
{ name: 'resultId', type: 'string' },
{ name: 'targetEmail', type: 'string' },
{ name: 'currentStatus', type: 'string' },
{ name: 'pendingProcesses', type: 'string' },
{ name: 'awaitInput', type: 'string' },
{ name: 'awaitProcessId', type: 'string' },
{ name: 'awaitColumn', type: 'string' },
{ name: 'updatedAt', type: 'string' },
],
}).then(r => console.log(r.status, JSON.stringify(r.body, null, 2)));
"
```
- [ ] **Step 2a: 作成に失敗した場合の代替手順**
`projectId`必須エラー等でAPI経由の作成が通らない場合は、n8n UI`https://n8n32.next-hd.net`、n8n-guide.md 2章のログイン情報から手動でData Tableを作成する。列構成はStep 2と同じにする。
- [ ] **Step 3: 作成結果を確認**
```bash
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node -e "
require('./scripts/n8n-api').request('GET', '/data-tables?limit=20').then(r => {
const t = r.body.data.find(d => d.name === 'healthcheck_bot_state');
console.log(JSON.stringify(t, null, 2));
});
"
```
Expected: 作成した8列のテーブルが表示される。表示された`id`を記録する
- [ ] **Step 4: README.mdへ記録**
```markdown
## n8nリソースID一覧
- Data Table `healthcheck_bot_state`: `<Step3で確認したid>`
```
- [ ] **Step 5: コミット**
```bash
git add NodeSrv/apps/healthcheck-survey-bot/README.md
git commit -m "docs: healthcheck_bot_state Data Table作成結果を記録"
```
---
### Task 9: HC-SUBワークフロー — プロセス実行+案内送信
WP・WAの両方から呼ばれる共通ロジック。「(任意)ProcessIdを実行→現在Statusの選択肢を組み立ててLINEWORKSへ送信→Data Table更新」を1本のExecute Workflow Triggerサブワークフローにまとめる。
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/workflows/hc-sub-run-process-and-notify.json`
- Modify: `NodeSrv/apps/healthcheck-survey-bot/README.md`
**Interfaces:**
- Consumes: Task 3〜6のロジックCodeードへ書き写す、Task 8のData Table ID
- Produces: n8n上のワークフローExecute Workflow Trigger、入力`{resultId: string, processId: string | null}`。WP・WAはこのワークフローIDを`Execute Workflow`ノードで呼び出す
**ノード構成:**
1. `Execute Workflow Trigger`(入力: `resultId`, `processId`
2. `IF`: `processId`が存在するか
- true分岐 → `HTTP Request`「Process実行」: `POST {PLEASANTER_BASE_URL}api/items/{{$json.resultId}}/update`、body `{"ApiVersion":1.1,"ApiKey":"<config_production.jsonのApiKey>","ProcessId": {{$json.processId}}}`
- false分岐 → そのまま次へ(フォールバック再送のケース)
3. `HTTP Request`「レコード取得」: `POST {PLEASANTER_BASE_URL}api/items/{{$('Execute Workflow Trigger').item.json.resultId}}/get`、body `{"ApiVersion":1.1,"ApiKey":"..."}`
4. `HTTP Request`「サイト設定取得」: `POST {PLEASANTER_BASE_URL}api/items/508971/getsite`、body `{"ApiVersion":1.1,"ApiKey":"..."}`
5. `Code`「選択肢組み立て」: Task 4の`fillTemplate`とTask 5の`extractProcessesForStatus`をそのまま貼り付けて使う
```javascript
// Codeード「選択肢組み立て」の中身
function isUnsetSentinel(value) {
return typeof value === "string" && value.startsWith("1899");
}
function fillTemplate(template, columns, valueHash) {
const labelToColumnName = new Map();
for (const column of columns) {
if (column.LabelText) labelToColumnName.set(column.LabelText, column.ColumnName);
}
return template.replace(/\{([^{}]+)\}/g, (matched, label) => {
const columnName = labelToColumnName.get(label);
if (!columnName) return matched;
const value = valueHash[columnName];
if (value === undefined || value === null || value === "" || isUnsetSentinel(value)) return "未設定";
return String(value);
});
}
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 columns = siteSettings.Columns || [];
const processes = siteSettings.Processes || [];
const valueHash = {
...record.ClassHash, ...record.NumHash, ...record.DateHash, ...record.DescriptionHash,
};
const candidates = extractProcessesForStatus(processes, record.Status);
const options = candidates.map((p) => ({
processId: p.Id,
label: p.DisplayName || p.Name,
tooltip: fillTemplate(p.ToolTip || "", columns, valueHash),
}));
return [{
json: {
resultId: record.ResultId,
currentStatus: record.Status,
options,
classCUserId: record.ClassHash.ClassC,
},
}];
```
`ToolTip`のキー名はTask 2の実機調査結果で確定させ、異なる場合はここだけ修正する
6. `HTTP Request`「対象者メール解決」: `POST {PLEASANTER_BASE_URL}api/users/get`、body `{"ApiVersion":1.1,"ApiKey":"...","View":{"ApiGetMailAddresses":true},"Where":{"UserId":{{$json.classCUserId}}}}`
7. `Code`「JWTクレーム組み立て」: LINEWORKS通知送信ワークフロー`Yprojk4JTl1vJPkf`)の`Validate & Prepare`ードと同じパターンでJWTクレームJSON文字列と、選択肢からLINEWORKS `button_template`のactions配列を組み立てる
8. `JWT`ノード(`operation: sign`, `algorithm: RS256`, Credential: 「LINEWORKS Bot Private Key (v4)」)
9. `HTTP Request`「アクセストークン取得」: `POST https://auth.worksmobile.com/oauth2/v2.0/token`form-urlencoded、`Yprojk4JTl1vJPkf`の`Get Access Token`ノードと同じパラメータ構成)
10. `HTTP Request`「LINEWORKS送信」: `POST https://www.worksapis.com/v1.0/bots/{BOT_ID}/users/{userId}/messages`、`button_template`形式のcontentを送信
11. `HTTP Request`n8n内部呼び出し、または`Data Table`ノード)「状態更新」: `healthcheck_bot_state`へ`resultId`をキーに`upsert``currentStatus`, `pendingProcesses`=JSON化したoptions, `awaitInput: "none"`, `updatedAt`
- [ ] **Step 1: ワークフローJSON雛形を作成**
`workflows/hc-sub-run-process-and-notify.json`に、上記11ードの`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 APIキーは`Pleasanter/config_production.json`の値をそのままCodeード内の定数として埋め込む既存の`LINEWORKS通知送信`ワークフローと同じ方式)。
- [ ] **Step 2: デプロイ**
```bash
cd "C:\Users\k.nogi\#GitHub\ken_nogi\dev\NodeSrv\apps\healthcheck-survey-bot"
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node scripts/deploy-workflow.js workflows/hc-sub-run-process-and-notify.json
```
Expected: HTTP status 200、ワークフローIDが出力される
- [ ] **Step 3: README.mdへワークフローIDを記録**
```markdown
- ワークフロー `HC-SUB: プロセス実行と案内送信`: `<Step2で確認したid>`
```
- [ ] **Step 4: ユーザーへ確認のうえ、テスト実行**
Task 2で作成・削除したテストProcessとは別に、動作確認用の捨てレコード・テストProcess`CurrentStatus`をnullまたは既存Statusのどれかにして`ChangedStatus`は同じ値、`processId`無しでの疎通確認から始めるを使い、n8n UIの「Test workflow」または`Execute Workflow`ード経由で1回実行し、LINEWORKSへメッセージが届くこと・`healthcheck_bot_state`に行が作られることを確認する。**実行前に必ずユーザーへ確認する。**
- [ ] **Step 5: コミット**
```bash
git add workflows/hc-sub-run-process-and-notify.json NodeSrv/apps/healthcheck-survey-bot/README.md
git commit -m "feat: HC-SUBワークフロー(プロセス実行+案内送信)を追加"
```
---
### Task 10: HC-WPワークフロー — Statusプッシュ通知担当者操作起点
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/workflows/hc-wp-status-push.json`
- Modify: `NodeSrv/apps/healthcheck-survey-bot/README.md`
**Interfaces:**
- Consumes: Task 9のワークフローID`Execute Workflow`ノードで参照)
- Produces: Webhook `POST https://n8n32.next-hd.net/webhook/healthcheck-status-push``X-Api-Key`ヘッダー認証、body `{resultId, processId}`
**ノード構成:**
1. `Webhook``httpMethod: POST`, `path: healthcheck-status-push`
2. `Code`「検証」: `X-Api-Key`ヘッダーとbodyの`resultId`/`processId`必須チェック(不正なら`throw new Error(...)`でワークフローを失敗させる)
3. `Execute Workflow`Task 9のワークフローIDを指定、入力: `resultId`, `processId`
4. `Respond to Webhook``{"result":"ok"}`を返す)
- [ ] **Step 1: ワークフローJSONを作成**
`workflows/hc-wp-status-push.json`を作成。Codeードの中身:
```javascript
const API_KEY = "<healthcheck-status-push用に新規発行するAPIキー文字列>";
const headers = $input.first().json.headers || {};
if (headers["x-api-key"] !== API_KEY) {
throw new Error("Unauthorized: invalid API key");
}
const body = $input.first().json.body || {};
if (!body.resultId || !body.processId) {
throw new Error("Bad Request: resultId, processId は必須です");
}
return [{ json: { resultId: body.resultId, processId: body.processId } }];
```
- [ ] **Step 2: デプロイ**
```bash
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node scripts/deploy-workflow.js workflows/hc-wp-status-push.json
```
- [ ] **Step 3: Webhook有効化**
```bash
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node -e "
require('./scripts/n8n-api').request('POST', '/workflows/<Step2で確認したid>/activate').then(r => console.log(r.status, JSON.stringify(r.body)));
"
```
- [ ] **Step 4: README.mdへ記録**
```markdown
- ワークフロー `HC-WP: Statusプッシュ通知`: `<id>`Webhook: `https://n8n32.next-hd.net/webhook/healthcheck-status-push`
```
- [ ] **Step 5: ユーザー確認のうえ疎通テスト**
```bash
curl -X POST "https://n8n32.next-hd.net/webhook/healthcheck-status-push" \
-H "X-Api-Key: <Step1で設定したAPIキー>" \
-H "Content-Type: application/json" \
-d '{"resultId": <テスト用ResultId>, "processId": null}'
```
Expected: `{"result":"ok"}`、かつLINEWORKSへ現在Statusの案内が届く**実行前に必ずユーザーへ確認する**
- [ ] **Step 6: コミット**
```bash
git add workflows/hc-wp-status-push.json NodeSrv/apps/healthcheck-survey-bot/README.md
git commit -m "feat: HC-WPワークフロー(Statusプッシュ通知)を追加"
```
---
### Task 11: HC-WAワークフロー — LINEWORKS応答受信唯一の受信口
**Files:**
- Create: `NodeSrv/apps/healthcheck-survey-bot/workflows/hc-wa-lineworks-response.json`
- Modify: `NodeSrv/apps/healthcheck-survey-bot/README.md`
**Interfaces:**
- 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`検証)
**ノード構成:**
1. `Webhook``httpMethod: POST`, `path: healthcheck-lineworks-response`, `options.rawBody: true`
2. `Code`「署名検証・送信者解決」: Task 6の`verifySignature`を貼り付けて検証。失敗なら`throw`。成功したら`source.userId`(=メールアドレス)と、`content.type`に応じたテキスト/ファイル情報を抽出する
```javascript
// Codeード「署名検証・送信者解決」の中身
const crypto = require("crypto");
function normalizeSignature(value) {
return String(value || "").trim().replace(/^sha256=/i, "");
}
function safeEqual(a, b) {
const ab = Buffer.from(String(a), "utf8");
const bb = Buffer.from(String(b), "utf8");
if (ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
function verifySignature(rawBody, headerSignature, botSecret) {
const headerSig = normalizeSignature(headerSignature);
if (!headerSig || !botSecret) return false;
const payload = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody), "utf8");
const expected = crypto.createHmac("sha256", botSecret).update(payload).digest("base64");
return safeEqual(headerSig, expected);
}
const LINEWORKS_BOT_SECRET = "<LINEWORKS Developer Consoleで確認するBot Secret>";
const item = $input.first();
const headers = item.json.headers || {};
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)) {
throw new Error("Unauthorized: signature mismatch");
}
const body = item.json.body || {};
const source = body.source || {};
const content = body.content || {};
return [{
json: {
targetEmail: source.userId,
contentType: content.type,
text: content.type === "text" ? content.text : null,
fileId: content.type === "file" ? content.fileId : null,
},
}];
```
3. `HTTP Request`「対象レコード検索」: `POST {PLEASANTER_BASE_URL}api/items/508971/get`、body `{"ApiVersion":1.1,"ApiKey":"...","View":{"ColumnFilterHash":{"ClassC":"<メールからPleasanterUserId解決した値>"},"ColumnFilterSearchTypes":{"ClassC":"ExactMatch"}}}`
- 前段として`api/users/get`で`targetEmail`からPleasanterUserIdを引く`HTTP Request`ードを1つ挟む
- 取得結果を`Status`昇順以外(`900`/`910`除外)でフィルタし、`UpdatedTime`降順で1件選ぶ`Code`ードを挟む。0件/複数件は`throw`でワークフローを失敗させる6章の異常系方針通り、自動判定しない
4. `HTTP Request``Data Table`ノード, `operation: get`)「待機状態取得」: `healthcheck_bot_state`から`resultId`一致行を取得
5. `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のード7〜10と同じLINEWORKS送信パターンを再利用
- 不一致 → `Execute Workflow`Task 9、`processId: null`)でフォールバック再送
- **date分岐**: `Code`でTask 3の`parseDateInput`を実行
- 成功 → `HTTP Request`「該当列update」: `POST api/items/{resultId}/update`、body `{"ApiVersion":1.1,"ApiKey":"...", "DateHash":{"<awaitColumn>": "<parseDateInputの結果>"}}``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
6. `Respond to Webhook`(各分岐の末尾で`{"result":"ok"}`を返す)
- [ ] **Step 1: ワークフローJSONを作成**
上記ノード構成に従い`workflows/hc-wa-lineworks-response.json`を作成する。`none`分岐・`date`分岐の`Code`ードにはTask 3・5のロジックをそのまま貼り付ける。
- [ ] **Step 2: デプロイ**
```bash
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node scripts/deploy-workflow.js workflows/hc-wa-lineworks-response.json
```
- [ ] **Step 3: Webhook有効化**
```bash
N8N_API_KEY="<n8n-guide.md 2章のAPIキー>" node -e "
require('./scripts/n8n-api').request('POST', '/workflows/<Step2で確認したid>/activate').then(r => console.log(r.status));
"
```
- [ ] **Step 4: LINEWORKS Developer Console側にBot Callback URLを設定**
対象Bot484184で管理しているBotのうち、健康診断管理で使うもののCallback URLを`https://n8n32.next-hd.net/webhook/healthcheck-lineworks-response`に設定する(**ユーザー確認必須**。他プロジェクトと同じBotを共用している場合、Callback URL変更が既存フローに影響しないか要確認
- [ ] **Step 5: README.mdへ記録**
```markdown
- ワークフロー `HC-WA: LINEWORKS応答受信`: `<id>`Webhook: `https://n8n32.next-hd.net/webhook/healthcheck-lineworks-response`
```
- [ ] **Step 6: コミット**
```bash
git add workflows/hc-wa-lineworks-response.json NodeSrv/apps/healthcheck-survey-bot/README.md
git commit -m "feat: HC-WAワークフロー(LINEWORKS応答受信)を追加"
```
---
### Task 12: 508971側クライアントスクリプト追加エンドツーエンド確認
**Files:**
- Modify: `Pleasanter/健康診断管理/configs/production/site-508971_健康診断管理/scripts/`配下に新規スクリプトファイルを追加
- Modify: 508971にテスト用Processを1つ追加`pleasanter-site-spec`スキルの標準フロー: `get-site-config.js`→編集→`build-desired-config.js`→`apply-site-config.js`ドライラン確認→ユーザー確認後にcurl実行
**Interfaces:**
- Consumes: Task 10のWebhook URL
- [ ] **Step 1: クライアントスクリプトを作成**
`Pleasanter/健康診断管理/configs/production/site-508971_健康診断管理/scripts/1_プロセス通知送信.js`を新規作成:
```javascript
const WEBHOOK_URL = "https://n8n32.next-hd.net/webhook/healthcheck-status-push";
const API_KEY = "<Task10 Step1で設定したAPIキー>";
$p.events.on_process = function (processId) {
sendStatusPush(processId);
};
async function sendStatusPush(processId) {
const resultId = $p.id();
try {
const res = await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY },
body: JSON.stringify({ resultId, processId }),
});
if (!res.ok) {
console.error("Status push failed:", res.status, await res.text());
}
} catch (error) {
console.error("Status push error:", error.message);
}
}
```
- [ ] **Step 2: 標準フローで508971へ反映**
```bash
cd "C:\Users\k.nogi\#GitHub\ken_nogi\dev\Pleasanter"
node .claude/js/build-desired-config.js --project="健康診断管理" --env=production site-508971
node .claude/js/apply-site-config.js --project="健康診断管理" --env=production
```
Expected: 差分・送信予定Body・curlコマンドが表示される常にドライラン。内容をユーザーに提示し、確認を得てから表示されたcurlコマンドを実行する
- [ ] **Step 3: テスト用Processを追加**
ユーザー確認のうえ、508971に検証用Process例: `CurrentStatus`を既存の`900`完了のまま、`ChangedStatus`も`900`、`OnClick`不要、実行種別「追加したボタン」を1つ追加する。Task 1のクライアントスクリプトと連携させ、押下時に`on_process`イベント経由でTask 10のWebhookが呼ばれることを確認する。
- [ ] **Step 4: エンドツーエンド確認**
1. 508971の捨てレコードでテストProcessボタンを押す → LINEWORKSに案内が届くことを確認
2. LINEWORKSで選択肢に回答する → `healthcheck_bot_state`の該当行が更新され、Processが実行されて捨てレコードのStatusが変わることを確認`get-site-config.js`相当で該当レコードを`api/items/{id}/get`し直して確認)
3. n8n Execution History`GET /executions?workflowId=<HC-WA id>&limit=5`)でエラーが出ていないことを確認
**すべて実データ・実LINEWORKS送信を伴うため、着手前に必ずユーザーへ確認する。**
- [ ] **Step 5: テスト用Processを削除**
確認完了後、ユーザーに508971からテスト用Processを削除してもらう本番のProcess一覧を汚さないため
- [ ] **Step 6: コミット**
```bash
cd "C:\Users\k.nogi\#GitHub\ken_nogi\dev"
git add "Pleasanter/健康診断管理/configs/production/site-508971_健康診断管理/scripts/1_プロセス通知送信.js"
git commit -m "feat: 508971にStatusプッシュ通知用クライアントスクリプトを追加"
```
---
## Self-Review
**Spec coverage:**
- 全体アーキテクチャWP/WA + Process流用→ Task 9〜11でカバー
- Data Table「bot_conversation_state」→ Task 8実装上は`healthcheck_bot_state`という名前にしたが、設計書の構造要件は満たす。理由: 既存のorg-master-sync用テーブルと並んだ一覧で識別しやすくするため
- Process定義ツールチップ・入力検証タブ→ Task 2実機調査・Task 5ロジック・Task 9利用
- 対象者解決メールLINEWORKS userId→ Task 9・11のPleasanter `api/users/get`呼び出し
- 日付・ファイル入力の追加往復 → Task 11の`date`/`file`分岐
- タイムアウト監視は「不要」という設計判断 → 本計画にタイムアウト監視ワークフローは含めていない(意図通り)
- エラーハンドリング(複数レコードヒット等)→ Task 11 Step3の異常系`throw`で失敗させる)
**Placeholder scan:** 「実装時に確認する」という記述がTask 8projectId要否・Task 11Pleasanter添付アップロードの正確なエンドポイントに残っている。これはn8n-guide.mdが明記していない未検証事項であり、調査ステップ自体をタスク内の手順として書いてあるため、内容のないプレースホルダーではなく「次に確認すべきこと」を伴う具体的な調査タスクとして扱う。
**Type consistency:** `extractProcessesForStatus`/`matchProcessByLabel`/`classifyAwaitInput`の関数名・戻り値の形は Task 5→Task 9→Task 11で一貫させた。`fillTemplate`も同様。
---
Plan complete and saved to `NodeSrv/docs/superpowers/plans/2026-09-05-healthcheck-lineworks-survey-n8n.md`. 実行方式を選んでほしい。
1. **Subagent-Driven推奨** — タスクごとに新しいsubagentを立てて実装、タスク間でレビューを挟む
2. **Inline Execution** — このセッション内で`executing-plans`を使い、チェックポイントを挟みながらまとめて実行