Compare commits

..

2 Commits

Author SHA1 Message Date
Akira
f771e6bcf7 windmillワークフローの管理手順の変更仕様 2026-03-03 14:39:57 +09:00
Akira
d5bb7f24dd Claude Codeによる更新 2026-03-03 13:25:25 +09:00
3 changed files with 397 additions and 9 deletions

View File

@@ -97,7 +97,7 @@ Windmill ワークフロー
| `.env` | `alexa-api/`.gitignore 対象) | 実際の Cookie 保管 | Git にコミットしない | | `.env` | `alexa-api/`.gitignore 対象) | 実際の Cookie 保管 | Git にコミットしない |
| `auth4.js` | `alexa-api/` | Amazon 認証・Cookie 取得スクリプト | **ローカルのみで実行**Windowsブラウザ認証が必要 | | `auth4.js` | `alexa-api/` | Amazon 認証・Cookie 取得スクリプト | **ローカルのみで実行**Windowsブラウザ認証が必要 |
| `auth.js` / `auth2.js` / `auth3.js` | `alexa-api/` | auth4.js の旧バージョン | 参考用。実際は auth4.js を使う | | `auth.js` / `auth2.js` / `auth3.js` | `alexa-api/` | auth4.js の旧バージョン | 参考用。実際は auth4.js を使う |
| `test_tts.js` | `alexa-api/` | ローカルテスト用スクリプト | 直接 alexa.amazon.co.jp を叩いて動作確認 | | `test_tts.js` | `alexa-api/` | ローカルテスト用スクリプト | `.env` を読んで直接 alexa.amazon.co.jp を叩く。テスト対象デバイスはシリアル `G0922H08525302K5`オフィスの右エコーにハードコード。TABLET は一覧表示から除外。 |
--- ---
@@ -186,6 +186,8 @@ await main("G0922H08525302K5", "来客がありました");
} }
``` ```
- Amazon は **200 または 202** を返すどちらも成功として扱う。202 は非同期処理を示す)
**レスポンス(失敗)**: **レスポンス(失敗)**:
```json ```json
{ {
@@ -209,6 +211,29 @@ await main("G0922H08525302K5", "来客がありました");
] ]
``` ```
- `force=true` で**常にキャッシュを無効化**して最新一覧を取得する(`/speak` の5分キャッシュとは独立
- TABLET や Alexa アプリなど、Echo 以外のデバイスも含む全デバイスを返す
### デバイス検索ロジックfindDevice
`/speak``device` パラメータは以下の優先順位で検索する:
1. **シリアル番号完全一致**`serialNumber === device`
2. **アカウント名完全一致**(大文字小文字を無視)
3. **アカウント名部分一致**`includes()` 、大文字小文字を無視)
```javascript
// 例: "右エコー" でも "オフィスの右エコー" を見つけられる
```
### キャッシュ仕様
| 対象 | TTL | 備考 |
|------|-----|------|
| `customerId` | サーバー再起動まで永続 | Bootstrap API から取得 |
| デバイス一覧(`/speak` 経由) | 5分 | 期限切れ後は自動更新 |
| デバイス一覧(`/devices` 経由) | なし(毎回強制取得) | 診断・確認用途 |
--- ---
## 6. Alexa API の仕組み(重要な知識) ## 6. Alexa API の仕組み(重要な知識)
@@ -234,6 +259,16 @@ await main("G0922H08525302K5", "来客がありました");
→ シーケンス JSON を送信して読み上げ実行 → シーケンス JSON を送信して読み上げ実行
``` ```
### サーバー起動時の自動初期化
`app.listen()` の直後に非同期で初期化処理を実行する:
1. `getCustomerId()` を呼び出して `customerId` をキャッシュ(成功すると `[INFO] Customer ID: xxx` をログ出力)
2. `getDevices()` を呼び出してデバイス一覧をキャッシュ5分 TTL
3. `deviceType``A4ZXE` または `ASQZWP` で始まるデバイスEcho 系)のみをログ出力
**失敗してもサーバーは起動し続ける**`[WARN] Startup init failed:` と出力して続行)。ただし Cookie が無効な場合、その後の `/speak` リクエストも全て失敗する。
### POST /api/behaviors/preview のリクエスト構造 ### POST /api/behaviors/preview のリクエスト構造
```json ```json
@@ -318,11 +353,19 @@ AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth
### auth4.js のログインフロー ### auth4.js のログインフロー
1. `GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jp` 1. `GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jp`
2. hidden フィールドanti-csrftoken-a2z, appActionToken, workflowState 等)を抽出 - `openid.assoc_handle: 'amzn_dp_project_dee_jp'`**Alexa Japan 専用のハンドル**他のAmazonサービスとは異なる
3. POST でメール/パスワードを送信 - `openid.return_to: 'https://alexa.amazon.co.jp/api/apps/v1/token'` にリダイレクト先を指定
4. `alexa.amazon.co.jp/api/apps/v1/token` へのリダイレクトをたどる 2. hidden フィールドanti-csrftoken-a2z, appActionToken, workflowState 等)を HTML から抽出
3. POST でメール/パスワードを `rememberMe: 'true'` と一緒に送信長期Cookie取得のため重要
4. 3xx リダイレクトを最大10回たどる`alexa.amazon.co.jp/api/apps/v1/token` 等)
5. 取得した Cookieat-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を `.env` に保存 5. 取得した Cookieat-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を `.env` に保存
**成功判定**: Cookie に `at-acbjp` または `session-token` が含まれているかで判定。
**失敗時のエラー検出**:
- CAPTCHA が要求されている場合: `※ CAPTCHA が要求されています。しばらく待ってから再試行してください。`
- パスワードが間違っている場合: `※ パスワードが間違っている可能性があります。`
### Cookie の有効期限 ### Cookie の有効期限
数日〜数週間で期限切れになる。期限切れの症状: `/health` を叩くと Cookie 長は正常だが、`/speak` が 400 や 403 を返す。 数日〜数週間で期限切れになる。期限切れの症状: `/health` を叩くと Cookie 長は正常だが、`/speak` が 400 や 403 を返す。
@@ -479,6 +522,10 @@ curl -X POST \
| `AlexaAnnouncement` は別用途 | コンテンツでなくノード名が読まれる | | `AlexaAnnouncement` は別用途 | コンテンツでなくノード名が読まれる |
| レート制限 | 短時間連続リクエストで HTTP 429 または無音。通知用途では問題なし | | レート制限 | 短時間連続リクエストで HTTP 429 または無音。通知用途では問題なし |
| Gitea push がブロックされる | pre-receive フックでエラー。ファイル転送は scp を使う | | Gitea push がブロックされる | pre-receive フックでエラー。ファイル転送は scp を使う |
| 起動ログに Echo デバイスが出ない | deviceType が `A4ZXE` or `ASQZWP` で始まるもののみ表示。新デバイス追加時は確認を |
| test_tts.js のテスト対象が固定 | シリアル `G0922H08525302K5`(オフィスの右エコー)にハードコード。他デバイスでテストする場合は一時的に書き換える |
| auth4.js で CAPTCHA が出る | Amazon のレート制限。しばらく時間を置いてから再実行 |
| `/devices``/speak` のキャッシュが異なる | `/devices` は毎回最新取得、`/speak` は5分キャッシュ。新しいデバイス追加直後に `/speak` が失敗する場合、コンテナ再起動でキャッシュクリア |
--- ---
@@ -489,7 +536,7 @@ curl -X POST \
| ファイル | 説明 | | ファイル | 説明 |
|---------|------| |---------|------|
| `alexa-api/server.js` | Express API サーバー。Alexa への直接 HTTPS 実装 | | `alexa-api/server.js` | Express API サーバー。Alexa への直接 HTTPS 実装 |
| `alexa-api/Dockerfile` | node:20-alpine ベース | | `alexa-api/Dockerfile` | node:20-alpine ベース。**`npm install --omit=dev`** で devDependenciesalexa-remote2, alexa-cookie2を除外してビルド。コピーするのは `server.js` のみauth4.js 等はコンテナに含まれない) |
| `alexa-api/docker-compose.yml` | windmill_windmill-internal ネットワーク接続設定 | | `alexa-api/docker-compose.yml` | windmill_windmill-internal ネットワーク接続設定 |
| `alexa-api/auth4.js` | Amazon 認証・Cookie 取得(ローカルのみ) | | `alexa-api/auth4.js` | Amazon 認証・Cookie 取得(ローカルのみ) |
| `alexa-api/test_tts.js` | ローカルテスト用スクリプト | | `alexa-api/test_tts.js` | ローカルテスト用スクリプト |
@@ -554,3 +601,4 @@ var rawSequenceJson = JSON.stringify(sequenceObj).replace(
| 2026-03-02〜03 | 日本語TTS問題の調査・試行錯誤 | | 2026-03-02〜03 | 日本語TTS問題の調査・試行錯誤 |
| 2026-03-03 | `\uXXXX` エスケープで日本語TTS完全解決。server.js・test_tts.js に反映 | | 2026-03-03 | `\uXXXX` エスケープで日本語TTS完全解決。server.js・test_tts.js に反映 |
| 2026-03-03 | 本マスタードキュメント作成 | | 2026-03-03 | 本マスタードキュメント作成 |
| 2026-03-03 | findDevice 検索ロジック、キャッシュ仕様、起動初期化、auth4.js 詳細、Dockerfile 仕様を追記 |

View File

@@ -0,0 +1,301 @@
# マスタードキュメント - Windmillフロー管理 API一本化編
> **最終更新**: 2026-03-03
> **対象**: `windmill.keinafarm.net` / workspace `admins`
> **目的**: ローカルGitとサーバーGitの衝突を避けつつ、Windmill APIを唯一の運用経路に統一する
---
## 目次
1. [この文書の役割](#1-この文書の役割)
2. [運用方針(結論)](#2-運用方針結論)
3. [現状の課題と解決方針](#3-現状の課題と解決方針)
4. [管理対象と正本の定義](#4-管理対象と正本の定義)
5. [同期・反映の仕様](#5-同期反映の仕様)
6. [競合時の動作仕様](#6-競合時の動作仕様)
7. [実装計画](#7-実装計画)
8. [標準運用手順Runbook](#8-標準運用手順runbook)
9. [セキュリティ・監査方針](#9-セキュリティ監査方針)
10. [障害前提の復旧設計(必須)](#10-障害前提の復旧設計必須)
11. [Windmill依存を薄くする方針必須](#11-windmill依存を薄くする方針必須)
12. [受け入れ条件](#12-受け入れ条件)
13. [既知の注意点](#13-既知の注意点)
14. [更新履歴](#14-更新履歴)
---
## 1. この文書の役割
この文書は、次回セッション開始時にこれだけ読めば作業を継続できることを目的とした、**運用仕様 + 実装計画の単一ソース**である。
- 暗黙知を残さない
- 方針・手順・失敗時の扱いを固定化する
- API経由運用に必要な実装タスクを明文化する
---
## 2. 運用方針(結論)
### 採用する方式: API一本化Server First
1. ローカルリポジトリは、サーバー側Gitをリモートにしない
2. Windmillの実体変更は **Windmill REST API 経由のみ**
3. 作業開始時にAPIでサーバー状態を取り込み、サーバーが新しければローカルへ同期
4. ローカル変更はAPIでサーバーへ反映
5. サーバー側の Git Sync Workflow は定期記録用途として継続
6. ローカルGitコミットは手動またはAIで実施し、監査/履歴用途として扱う
---
## 3. 現状の課題と解決方針
### 課題
- Windmill側の自動Git記録WFと、ローカルGit運用が同一系統を触ると競合が発生する
- `wmill` CLIは制約があり使いにくく、運用が不安定
- サーバーとローカルのどちらが最新か判定が曖昧
### 解決
- 更新経路をWindmill APIに統一し、競合面を縮小する
- 「正本=Windmillサーバー」の原則を固定する
- `hash` / `parent_hash` を使った衝突検知を標準化する
---
## 4. 管理対象と正本の定義
### 正本
- **Windmillサーバー上のオブジェクト**
- scripts (`/w/{workspace}/scripts/*`)
- flows (`/w/{workspace}/flows/*`)
- schedules (`/w/{workspace}/schedules/*`)
### ローカルの位置づけ
- ローカルファイルは「編集用ワークツリー + 監査ログ」
- ローカルGitはサーバー同期の必須経路ではない
### 既存の主要オブジェクト(例)
- script: `u/admin/alexa_speak`
- flows: `f/app_custom/system_heartbeat`, `f/shiraou/shiraou_notification` など
---
## 5. 同期・反映の仕様
## 5.1 Pullサーバー -> ローカル)
### 目的
- 作業前にサーバー最新状態をローカルへ取り込む
### 判定ルール
- サーバー `updated_at` / `hash` とローカル管理メタ情報を比較
- サーバーが新しければローカルを更新
### 対象API
- `GET /api/w/{workspace}/scripts/get/p/{path}`
- `GET /api/w/{workspace}/flows/get/{path}`
- `GET /api/w/{workspace}/schedules/get/{path}`(必要時)
## 5.2 Pushローカル -> サーバー)
### 目的
- ローカル変更をサーバーへ安全反映
### スクリプト反映API標準
- `POST /api/w/{workspace}/scripts/create`
必須パラメータ(最小):
- `path`
- `parent_hash`直前取得したサーバーhash
- `summary`
- `description`
- `content`
- `schema`
- `language`TypeScriptは `bun`
- `kind`(通常 `script`
- `lock`(既存値継承)
### フロー反映API
- CE制約により `PUT` 更新が不可/不安定な場合があるため、原則:
1. `DELETE /flows/delete/{path}`
2. `POST /flows/create`
---
## 6. 競合時の動作仕様
### スクリプトPush競合
- `parent_hash` 不一致時は更新拒否(期待動作)
- 対応:
1. 最新をPull
2. 差分を再適用
3. 再Push
### フローPush競合
- 削除再作成前に最新を必ずPullしてローカル保存
- 競合が疑われる場合は自動実行せず手動確認にフォールバック
---
## 7. 実装計画
## Phase 0: 文書固定(完了)
- 本ドキュメント作成
## Phase 1: API運用コマンド整備次タスク
`wm-api.sh` へ追加:
1. `pull-script <path> <outfile>`
2. `push-script <path> <infile>`
3. `pull-flow <path> <outfile>`
4. `push-flow <json-file>`
5. `pull-all`scripts/flowsの一覧取得 + 一括保存)
6. `status-remote`ローカルとサーバーのhash比較
## Phase 2: メタ情報管理
- `state/remote_index.json` を導入し、`path -> hash/updated_at` を保持
- Pull前後で差分判定可能にする
## Phase 3: 標準化
- `docs/flow-manage` に操作例を固定
- 「作業開始時は必ずpull」を運用ルール化
## Phase 4: 半自動化(任意)
- AI/スクリプトで
- 変更検知
- Pull提案
- Push時の競合自動リカバリ
---
## 8. 標準運用手順Runbook
### 8.1 日常の更新(推奨)
1. `pull-all` でサーバー最新を取得
2. ローカル編集
3. `push-script` / `push-flow` で反映
4. 必要に応じてローカルGitにコミット
### 8.2 サーバーで変更されたものを取り込む
1. `status-remote` 実行
2. サーバー新規/更新分を `pull-*` で取得
3. ローカル履歴としてコミット(任意)
### 8.3 `alexa_speak` 更新例(現在の具体例)
1. `pull-script u/admin/alexa_speak scripts/alexa_speak.ts`
2. `scripts/alexa_speak.ts` を編集
3. `push-script u/admin/alexa_speak scripts/alexa_speak.ts`
4. Windmill UIで動作確認`device` がドロップダウン表示)
---
## 9. セキュリティ・監査方針
- APIトークンは環境変数またはローカル限定設定で管理
- リポジトリへ平文トークンをコミットしない
- 反映時刻・対象path・実行者をログ化
- サーバーGit Syncは監査証跡として維持
---
## 10. 障害前提の復旧設計(必須)
サーバー内部障害、API非冪等、通信断は防止不能とみなし、**復旧可能性を担保する**。
### 10.1 復旧の基本原則
1. Push前に対象オブジェクトをバックアップ保存必須
2. Push後に `post-verify`(再取得して期待値比較)を必須化
3. 失敗時は「再実行」ではなく「現状確認 -> 復旧」の順で実施
### 10.2 標準復旧手順
1. `get` で現状確認存在、content/value、version/hash
2. 期待状態との差分を判定
3. 必要ならバックアップから復元
4. 復元後に `post-verify`
5. 復旧ログを記録日時、path、操作者、原因、処置
### 10.3 フロー欠落時の緊急復旧
`delete -> create` の途中失敗でフローが消失した場合:
1. 直前バックアップJSONで即時 `flows/create`
2. 依存スケジュールの有効性を確認
3. 関連ジョブの手動実行で動作確認
---
## 11. Windmill依存を薄くする方針必須
Windmill API依存は避けられないため、依存点を最小化する。
### 11.1 使用APIの固定
- `scripts`: `list/get/create`
- `flows`: `list/get/create/delete`
- `schedules`: `list/get/create/delete`(必要時)
上記以外のAPIは原則使わない。
### 11.2 判定ロジックの優先順位
1. 実体JSON比較content/valueの比較を主判定
2. `hash` / `version_id` / `edited_at` は補助判定
3. 補助フィールド欠落時も動作継続できる実装にする
### 11.3 仕様変更検知
- 定期的に `smoke test` を実行してレスポンス形を検証
- 期待フィールド欠落時はPush停止fail closed
- 仕様差分を本ドキュメントの更新履歴に反映
---
## 12. 受け入れ条件
以下を満たせば「このプロジェクトはサーバーのワークフローを管理するためのもの」と言える状態:
1. ローカルから scripts/flows の Pull/Push がAPIで完結
2. サーバー更新がローカルで検知できるhash比較
3. 競合時の復旧手順が定義済み
4. 運用手順がこの文書だけで再現可能
---
## 13. 既知の注意点
1. `wm-api.sh` は現状 `bash` 前提だが、Windowsで改行がCRLFだと shebang 実行失敗する場合がある
2. フロー更新は環境により `PUT` できないため削除再作成を標準とする
3. API応答仕様はWindmillバージョン差で微差が出るため、初回導入時は `get` のレスポンス形を確認する
---
## 14. 更新履歴
| 日付 | 変更内容 |
|------|----------|
| 2026-03-03 | 初版作成API一本化方針、同期仕様、実装計画、Runbookを定義 |
| 2026-03-03 | 障害前提の復旧設計、Windmill依存を薄くする方針を必須要件として追記 |

View File

@@ -3,16 +3,55 @@
* 指定した Echo デバイスにテキストを読み上げさせる Windmill スクリプト * 指定した Echo デバイスにテキストを読み上げさせる Windmill スクリプト
* *
* パラメータ: * パラメータ:
* device - デバイス名またはシリアル番号(例: "リビングエコー1", "プレハブ" * device - ドロップダウンから選択するデバイス(内部的にはシリアル番号
* text - 読み上げるテキスト * text - 読み上げるテキスト
*/ */
const ALEXA_API_URL = "http://alexa_api:3500";
type DeviceOption = { value: string; label: string };
const FALLBACK_DEVICE_OPTIONS: DeviceOption[] = [
{ value: "G0922H085165007R", label: "プレハブ (G0922H085165007R)" },
{ value: "G8M2DB08522600RL", label: "リビングエコー1 (G8M2DB08522600RL)" },
{ value: "G8M2DB08522503WF", label: "リビングエコー2 (G8M2DB08522503WF)" },
{ value: "G0922H08525302K5", label: "オフィスの右エコー (G0922H08525302K5)" },
{ value: "G0922H08525302J9", label: "オフィスの左エコー (G0922H08525302J9)" },
{ value: "G8M2HN08534302XH", label: "寝室のエコー (G8M2HN08534302XH)" },
];
// Windmill Dynamic Select: 引数名 `device` に対応する `DynSelect_device` と `device()` を定義
export type DynSelect_device = string;
export async function device(): Promise<DeviceOption[]> {
try {
const res = await fetch(`${ALEXA_API_URL}/devices`);
if (!res.ok) return FALLBACK_DEVICE_OPTIONS;
const devices = (await res.json()) as Array<{
name?: string;
serial?: string;
family?: string;
}>;
const options = devices
.filter((d) => d.family === "ECHO" && d.serial)
.map((d) => ({
value: d.serial as string,
label: `${d.name ?? d.serial} (${d.serial})`,
}))
.sort((a, b) => a.label.localeCompare(b.label, "ja"));
return options.length > 0 ? options : FALLBACK_DEVICE_OPTIONS;
} catch {
return FALLBACK_DEVICE_OPTIONS;
}
}
export async function main( export async function main(
device: string, device: DynSelect_device,
text: string, text: string,
): Promise<{ ok: boolean; device: string; text: string }> { ): Promise<{ ok: boolean; device: string; text: string }> {
const ALEXA_API_URL = "http://alexa_api:3500";
const res = await fetch(`${ALEXA_API_URL}/speak`, { const res = await fetch(`${ALEXA_API_URL}/speak`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },