未コミットを一括コミット

This commit is contained in:
akira
2026-04-04 09:15:09 +09:00
parent ef7c9d3c21
commit 555940d8f4
26 changed files with 6367 additions and 6367 deletions

View File

@@ -1,53 +1,53 @@
---
description: 新しいWindmillスクリプトを作成する
---
# 新しいWindmillスクリプトの作成
Windmillに新しいスクリプトを追加する手順。
## 手順
1. **スクリプトファイルの作成**: スクリプトは以下のディレクトリ構造に従って作成する
- ユーザースクリプト: `u/<username>/<script_name>/`
- フォルダスクリプト: `f/<folder_name>/<script_name>/`
2. **ファイル構成**: 各スクリプトフォルダには以下のファイルが必要
- `script.yaml` — メタデータ(言語、概要、スキーマなど)
- スクリプト本体(例: `script.py`, `script.ts`, `script.sh`
3. **script.yaml のテンプレート**
```yaml
summary: '<スクリプトの説明>'
description: '<詳細な説明>'
lock: []
schema:
$schema: 'https://json-schema.org/draft/2020-12/schema'
type: object
properties:
param1:
type: string
description: '<パラメータの説明>'
required:
- param1
kind: script
tag: ''
```
4. **Pythonスクリプト (`script.py`) のテンプレート**
```python
def main(param1: str):
"""
スクリプトの説明
"""
return {"result": f"Hello, {param1}!"}
```
5. **TypeScriptスクリプト (`script.ts`) のテンプレート**
```typescript
export async function main(param1: string): Promise<any> {
return { result: `Hello, ${param1}!` };
}
```
6. **サーバーに反映**: `/windmill-push` ワークフローを実行してpush
---
description: 新しいWindmillスクリプトを作成する
---
# 新しいWindmillスクリプトの作成
Windmillに新しいスクリプトを追加する手順。
## 手順
1. **スクリプトファイルの作成**: スクリプトは以下のディレクトリ構造に従って作成する
- ユーザースクリプト: `u/<username>/<script_name>/`
- フォルダスクリプト: `f/<folder_name>/<script_name>/`
2. **ファイル構成**: 各スクリプトフォルダには以下のファイルが必要
- `script.yaml` — メタデータ(言語、概要、スキーマなど)
- スクリプト本体(例: `script.py`, `script.ts`, `script.sh`
3. **script.yaml のテンプレート**
```yaml
summary: '<スクリプトの説明>'
description: '<詳細な説明>'
lock: []
schema:
$schema: 'https://json-schema.org/draft/2020-12/schema'
type: object
properties:
param1:
type: string
description: '<パラメータの説明>'
required:
- param1
kind: script
tag: ''
```
4. **Pythonスクリプト (`script.py`) のテンプレート**
```python
def main(param1: str):
"""
スクリプトの説明
"""
return {"result": f"Hello, {param1}!"}
```
5. **TypeScriptスクリプト (`script.ts`) のテンプレート**
```typescript
export async function main(param1: string): Promise<any> {
return { result: `Hello, ${param1}!` };
}
```
6. **サーバーに反映**: `/windmill-push` ワークフローを実行してpush

View File

@@ -1,39 +1,39 @@
---
description: Windmillサーバーからワークフロー情報を取得する
---
# Windmill Pull ワークフロー
Windmillサーバーから最新のスクリプトやフローの情報を取得する手順。
wm-api.sh を使用してREST API経由で取得する。
// turbo-all
1. サーバーバージョンを確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh version
```
2. スクリプト一覧を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh scripts
```
3. フロー一覧を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh flows
```
4. 特定のスクリプトの詳細を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh get-script <path>
```
5. 特定のフローの詳細を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh get-flow <path>
```
## 注意
- wmill CLIはCE版ではグローバルAPI認証の制限があるため使用できない
- 代わりに `wm-api.sh` を使用してREST APIで直接操作する
---
description: Windmillサーバーからワークフロー情報を取得する
---
# Windmill Pull ワークフロー
Windmillサーバーから最新のスクリプトやフローの情報を取得する手順。
wm-api.sh を使用してREST API経由で取得する。
// turbo-all
1. サーバーバージョンを確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh version
```
2. スクリプト一覧を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh scripts
```
3. フロー一覧を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh flows
```
4. 特定のスクリプトの詳細を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh get-script <path>
```
5. 特定のフローの詳細を取得
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh get-flow <path>
```
## 注意
- wmill CLIはCE版ではグローバルAPI認証の制限があるため使用できない
- 代わりに `wm-api.sh` を使用してREST APIで直接操作する

View File

@@ -1,35 +1,35 @@
---
description: ローカルのワークフローをWindmillサーバーにpushする
---
# Windmill Push ワークフロー
ローカルで作成・編集したスクリプトやフローをWindmillサーバーに反映する手順。
wm-api.sh を使用してREST API経由でpushする。
// turbo-all
1. 現在のサーバー接続を確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh version
```
2. 既存のスクリプト一覧を確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh scripts
```
3. 既存のフロー一覧を確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh flows
```
4. スクリプトを作成する場合 (JSONファイルを用意して実行)
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh create-script <script.json>
```
## 注意
- wmill CLIはCE版ではグローバルAPI認証の制限があるため使用できない
- 代わりに `wm-api.sh` を使用してREST APIで直接操作する
- Windmill MCP経由でも操作可能
---
description: ローカルのワークフローをWindmillサーバーにpushする
---
# Windmill Push ワークフロー
ローカルで作成・編集したスクリプトやフローをWindmillサーバーに反映する手順。
wm-api.sh を使用してREST API経由でpushする。
// turbo-all
1. 現在のサーバー接続を確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh version
```
2. 既存のスクリプト一覧を確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh scripts
```
3. 既存のフロー一覧を確認
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh flows
```
4. スクリプトを作成する場合 (JSONファイルを用意して実行)
```bash
cd /home/akira/develop/windmill_workflow && ./wm-api.sh create-script <script.json>
```
## 注意
- wmill CLIはCE版ではグローバルAPI認証の制限があるため使用できない
- 代わりに `wm-api.sh` を使用してREST APIで直接操作する
- Windmill MCP経由でも操作可能

58
.gitignore vendored
View File

@@ -1,29 +1,29 @@
# Windmill secrets and sensitive data
*.secret.*
variables/
resources/
# Environment / secrets
.env
.env.local
# Python
.venv/
__pycache__/
*.pyc
# wmill CLI
wmill-lock.yaml
# Node
node_modules/
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
*.swp
*.swo
# Windmill secrets and sensitive data
*.secret.*
variables/
resources/
# Environment / secrets
.env
.env.local
# Python
.venv/
__pycache__/
*.pyc
# wmill CLI
wmill-lock.yaml
# Node
node_modules/
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
*.swp
*.swo

4
.serena/.gitignore vendored
View File

@@ -1,2 +1,2 @@
/cache
/project.local.yml
/cache
/project.local.yml

View File

@@ -1,152 +1,152 @@
# the name by which the project can be referenced within Serena
project_name: "windmill_workflow"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- python
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
#
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}
# the name by which the project can be referenced within Serena
project_name: "windmill_workflow"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- python
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
#
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}

246
CLAUDE.md
View File

@@ -1,123 +1,123 @@
# Windmill Workflow プロジェクト
白皇集落営農組合 統合システム向けの Windmill ワークフロー管理リポジトリ。
## 環境
| 項目 | 値 |
|------|-----|
| Windmillサーバー | https://windmill.keinafarm.net |
| ワークスペース | `admins` |
| APIトークン | `wm-api.sh` に設定済み |
| Gitリモート | https://gitea.keinafarm.net/akira/windmill_workflow.git |
## 重要な制約
- **`wmill` CLIは使用不可**CE版のグローバルAPI認証制限`wm-api.sh` または curl で直接REST APIを叩く
- **フローの PUT更新は 405**Windmill CE版の制限→ DELETE してから POST で再作成する
- **`wmill.get_state()` はインラインフロースクリプトで永続化されない**→ 状態管理は必ず `wmill.get_variable()` / `set_variable()` を使う
- **`python3` コマンドは Windows環境で失敗する**→ curl の出力は grep等で直接処理する
## ディレクトリ構成
```
windmill_workflow/
├── flows/ # フロー定義JSON
│ ├── system_heartbeat.flow.json # Windmill自己診断フロー
│ ├── shiraou_notification.flow.json # 白皇集落 変更通知フロー
│ └── mail_filter.flow.json # メールフィルタリングフロー
├── docs/
│ └── shiraou/ # 白皇集落営農組合関連ドキュメント
│ ├── 19_windmill_通知ワークフロー連携仕様.md # API仕様書
│ └── 20_マスタードキュメント_Windmill通知ワークフロー編.md # マスタードキュメント
├── .agent/workflows/ # エージェント作業手順
│ ├── windmill-push.md # サーバーへのpush手順
│ ├── windmill-pull.md # サーバーからのpull手順
│ └── windmill-new-script.md # 新規スクリプト作成手順
├── wm-api.sh # Windmill REST APIヘルパー
└── wmill.yaml # wmill設定defaultTs: bun
```
## 登録済みワークフロー
| パス | 概要 | スケジュール |
|------|------|-------------|
| `f/app_custom/system_heartbeat` | Windmill自己診断 | なし(手動) |
| `f/shiraou/shiraou_notification` | 白皇集落営農 変更通知 | 5分毎JST |
| `f/mail/mail_filter` | メールフィルタリングIMAP→LLM→LINE | 10分毎JST予定 |
| `u/antigravity/git_sync` | Git同期 | 30分毎 |
## wm-api.sh コマンド一覧
```bash
./wm-api.sh version # サーバーバージョン確認
./wm-api.sh flows # フロー一覧
./wm-api.sh schedules # スケジュール一覧
./wm-api.sh get-flow <path> # フロー詳細取得
./wm-api.sh create-flow <json-file> # フロー作成
./wm-api.sh create-schedule <json-file> # スケジュール作成
./wm-api.sh run-flow <path> # フロー手動実行
./wm-api.sh job-status <job-id> # ジョブ状態確認
```
## フローのデプロイ手順
```bash
# 1. flows/*.flow.json を編集
# 2. 削除して再作成PUTは405のため
curl -sk -X DELETE \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
"https://windmill.keinafarm.net/api/w/admins/flows/delete/<path>"
curl -sk -X POST \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
-H "Content-Type: application/json" \
-d @flows/<file>.flow.json \
"https://windmill.keinafarm.net/api/w/admins/flows/create"
# 3. コミット&プッシュ
git add flows/<file>.flow.json
git commit -m "..."
git push origin main
```
## Windmill Variables
| 変数パス | Secret | 説明 |
|---------|--------|------|
| `u/admin/NOTIFICATION_API_KEY` | ✅ | shiraou APIキー |
| `u/admin/LINE_CHANNEL_ACCESS_TOKEN` | ✅ | LINE Messaging APIトークン |
| `u/admin/LINE_TO` | ✅ | LINE通知先IDユーザーまたはグループ |
| `u/admin/SHIRAOU_LAST_CHECKED_AT` | ❌ | 前回確認時刻(ワークフローが自動更新) |
| `u/admin/KEINASYSTEM_API_KEY` | ✅ | Keinasystem MAIL_API_KEY.envと同じ値 |
| `u/admin/KEINASYSTEM_API_URL` | ❌ | `https://keinafarm.net` |
| `u/admin/GEMINI_API_KEY` | ✅ | Google Gemini API キーLLM判定用 |
| `u/admin/GMAIL_IMAP_USER` | ✅ | GmailアカウントのIMAPユーザー名メールアドレス |
| `u/admin/GMAIL_IMAP_PASSWORD` | ✅ | GmailのアプリパスワードIMAPパスワード |
| `u/admin/MAIL_FILTER_GMAIL_LAST_UID` | ❌ | Gmail最終処理UIDワークフローが自動更新 |
| `u/admin/HOTMAIL_IMAP_USER` | ✅ | Hotmail IMAPユーザー名有効化時に登録 |
| `u/admin/HOTMAIL_IMAP_PASSWORD` | ✅ | Hotmail IMAPパスワード有効化時に登録 |
| `u/admin/MAIL_FILTER_HOTMAIL_LAST_UID` | ❌ | Hotmail最終処理UID有効化時に登録 |
| `u/admin/XSERVER_IMAP_USER` | ✅ | Xserver IMAPユーザー名有効化時に登録 |
| `u/admin/XSERVER_IMAP_PASSWORD` | ✅ | Xserver IMAPパスワード有効化時に登録 |
| `u/admin/MAIL_FILTER_XSERVER_LAST_UID` | ❌ | Xserver最終処理UID有効化時に登録 |
## マスタードキュメント
- [白皇集落 Windmill通知ワークフロー](docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md)
## メールフィルタリング — アカウント有効化手順
Gmail → Hotmail → Xserver の順で段階的に有効化する。
### Gmail 初期設定
1. GoogleアカウントでIMAPを有効化Googleアカウント設定 → セキュリティ → アプリパスワード)
2. Windmill Variables に `GMAIL_IMAP_USER`, `GMAIL_IMAP_PASSWORD` を登録
3. フローを手動実行(初回: 既存メールスキップ、最大UIDを記録
4. スケジュール登録10分毎
### Hotmail/Xserver 追加時
1. Windmill Variables に対応する変数を登録
2. `flows/mail_filter.flow.json` の該当アカウントの `"enabled": false``true` に変更
3. フローを DELETE → POST で再デプロイ
# Windmill Workflow プロジェクト
白皇集落営農組合 統合システム向けの Windmill ワークフロー管理リポジトリ。
## 環境
| 項目 | 値 |
|------|-----|
| Windmillサーバー | https://windmill.keinafarm.net |
| ワークスペース | `admins` |
| APIトークン | `wm-api.sh` に設定済み |
| Gitリモート | https://gitea.keinafarm.net/akira/windmill_workflow.git |
## 重要な制約
- **`wmill` CLIは使用不可**CE版のグローバルAPI認証制限`wm-api.sh` または curl で直接REST APIを叩く
- **フローの PUT更新は 405**Windmill CE版の制限→ DELETE してから POST で再作成する
- **`wmill.get_state()` はインラインフロースクリプトで永続化されない**→ 状態管理は必ず `wmill.get_variable()` / `set_variable()` を使う
- **`python3` コマンドは Windows環境で失敗する**→ curl の出力は grep等で直接処理する
## ディレクトリ構成
```
windmill_workflow/
├── flows/ # フロー定義JSON
│ ├── system_heartbeat.flow.json # Windmill自己診断フロー
│ ├── shiraou_notification.flow.json # 白皇集落 変更通知フロー
│ └── mail_filter.flow.json # メールフィルタリングフロー
├── docs/
│ └── shiraou/ # 白皇集落営農組合関連ドキュメント
│ ├── 19_windmill_通知ワークフロー連携仕様.md # API仕様書
│ └── 20_マスタードキュメント_Windmill通知ワークフロー編.md # マスタードキュメント
├── .agent/workflows/ # エージェント作業手順
│ ├── windmill-push.md # サーバーへのpush手順
│ ├── windmill-pull.md # サーバーからのpull手順
│ └── windmill-new-script.md # 新規スクリプト作成手順
├── wm-api.sh # Windmill REST APIヘルパー
└── wmill.yaml # wmill設定defaultTs: bun
```
## 登録済みワークフロー
| パス | 概要 | スケジュール |
|------|------|-------------|
| `f/app_custom/system_heartbeat` | Windmill自己診断 | なし(手動) |
| `f/shiraou/shiraou_notification` | 白皇集落営農 変更通知 | 5分毎JST |
| `f/mail/mail_filter` | メールフィルタリングIMAP→LLM→LINE | 10分毎JST予定 |
| `u/antigravity/git_sync` | Git同期 | 30分毎 |
## wm-api.sh コマンド一覧
```bash
./wm-api.sh version # サーバーバージョン確認
./wm-api.sh flows # フロー一覧
./wm-api.sh schedules # スケジュール一覧
./wm-api.sh get-flow <path> # フロー詳細取得
./wm-api.sh create-flow <json-file> # フロー作成
./wm-api.sh create-schedule <json-file> # スケジュール作成
./wm-api.sh run-flow <path> # フロー手動実行
./wm-api.sh job-status <job-id> # ジョブ状態確認
```
## フローのデプロイ手順
```bash
# 1. flows/*.flow.json を編集
# 2. 削除して再作成PUTは405のため
curl -sk -X DELETE \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
"https://windmill.keinafarm.net/api/w/admins/flows/delete/<path>"
curl -sk -X POST \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
-H "Content-Type: application/json" \
-d @flows/<file>.flow.json \
"https://windmill.keinafarm.net/api/w/admins/flows/create"
# 3. コミット&プッシュ
git add flows/<file>.flow.json
git commit -m "..."
git push origin main
```
## Windmill Variables
| 変数パス | Secret | 説明 |
|---------|--------|------|
| `u/admin/NOTIFICATION_API_KEY` | ✅ | shiraou APIキー |
| `u/admin/LINE_CHANNEL_ACCESS_TOKEN` | ✅ | LINE Messaging APIトークン |
| `u/admin/LINE_TO` | ✅ | LINE通知先IDユーザーまたはグループ |
| `u/admin/SHIRAOU_LAST_CHECKED_AT` | ❌ | 前回確認時刻(ワークフローが自動更新) |
| `u/admin/KEINASYSTEM_API_KEY` | ✅ | Keinasystem MAIL_API_KEY.envと同じ値 |
| `u/admin/KEINASYSTEM_API_URL` | ❌ | `https://keinafarm.net` |
| `u/admin/GEMINI_API_KEY` | ✅ | Google Gemini API キーLLM判定用 |
| `u/admin/GMAIL_IMAP_USER` | ✅ | GmailアカウントのIMAPユーザー名メールアドレス |
| `u/admin/GMAIL_IMAP_PASSWORD` | ✅ | GmailのアプリパスワードIMAPパスワード |
| `u/admin/MAIL_FILTER_GMAIL_LAST_UID` | ❌ | Gmail最終処理UIDワークフローが自動更新 |
| `u/admin/HOTMAIL_IMAP_USER` | ✅ | Hotmail IMAPユーザー名有効化時に登録 |
| `u/admin/HOTMAIL_IMAP_PASSWORD` | ✅ | Hotmail IMAPパスワード有効化時に登録 |
| `u/admin/MAIL_FILTER_HOTMAIL_LAST_UID` | ❌ | Hotmail最終処理UID有効化時に登録 |
| `u/admin/XSERVER_IMAP_USER` | ✅ | Xserver IMAPユーザー名有効化時に登録 |
| `u/admin/XSERVER_IMAP_PASSWORD` | ✅ | Xserver IMAPパスワード有効化時に登録 |
| `u/admin/MAIL_FILTER_XSERVER_LAST_UID` | ❌ | Xserver最終処理UID有効化時に登録 |
## マスタードキュメント
- [白皇集落 Windmill通知ワークフロー](docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md)
## メールフィルタリング — アカウント有効化手順
Gmail → Hotmail → Xserver の順で段階的に有効化する。
### Gmail 初期設定
1. GoogleアカウントでIMAPを有効化Googleアカウント設定 → セキュリティ → アプリパスワード)
2. Windmill Variables に `GMAIL_IMAP_USER`, `GMAIL_IMAP_PASSWORD` を登録
3. フローを手動実行(初回: 既存メールスキップ、最大UIDを記録
4. スケジュール登録10分毎
### Hotmail/Xserver 追加時
1. Windmill Variables に対応する変数を登録
2. `flows/mail_filter.flow.json` の該当アカウントの `"enabled": false``true` に変更
3. フローを DELETE → POST で再デプロイ

View File

@@ -1,6 +1,6 @@
# alexa-api/.env
# このファイルをコピーして .env を作成し、ALEXA_COOKIE に値を設定する
# .env は Git にコミットしない(.gitignore 参照)
# Amazon Cookieauth.js を実行して取得)
ALEXA_COOKIE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# alexa-api/.env
# このファイルをコピーして .env を作成し、ALEXA_COOKIE に値を設定する
# .env は Git にコミットしない(.gitignore 参照)
# Amazon Cookieauth.js を実行して取得)
ALEXA_COOKIE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@@ -1,2 +1,2 @@
node_modules/
.env
node_modules/
.env

View File

@@ -1,14 +1,14 @@
FROM node:20-alpine
WORKDIR /app
# 依存パッケージのみ先にコピー(キャッシュ活用)
COPY package*.json ./
RUN npm install --omit=dev
# ソースをコピー
COPY server.js .
EXPOSE 3500
CMD ["node", "server.js"]
FROM node:20-alpine
WORKDIR /app
# 依存パッケージのみ先にコピー(キャッシュ活用)
COPY package*.json ./
RUN npm install --omit=dev
# ソースをコピー
COPY server.js .
EXPOSE 3500
CMD ["node", "server.js"]

View File

@@ -1,70 +1,70 @@
/**
* alexa-api/auth.js
* ローカル PC で実行して Amazon Cookie を取得するスクリプト。
*
* 使い方:
* cd alexa-api
* npm install alexa-cookie2
* node auth.js
*
* → ブラウザで http://localhost:3456 を開く
* → Amazon にログイン
* → コンソールに Cookie が表示される
* → その値を Windmill Variable "u/admin/ALEXA_COOKIE" に登録
*/
const AlexaCookie = require('alexa-cookie2');
const PROXY_PORT = 3456;
console.log('==============================================');
console.log(' Alexa Cookie 取得ツール');
console.log('==============================================');
console.log(`\n認証プロキシを起動中... (port ${PROXY_PORT})`);
console.log('\n【手順】');
console.log(` 1. ブラウザで http://localhost:${PROXY_PORT} を開く`);
console.log(' 2. Amazon アカウントにログインamazon.co.jp');
console.log(' 3. ログイン完了後、このコンソールに Cookie が表示される\n');
AlexaCookie.generateAlexaCookie(
'',
{
amazonPage: 'amazon.co.jp',
acceptLanguage: 'ja-JP',
setupProxy: true,
proxyPort: PROXY_PORT,
proxyOwnIp: '127.0.0.1',
proxyListenBind: '0.0.0.0',
logger: (msg) => {
if (!msg.includes('verbose') && !msg.includes('DEBUG')) {
console.log('[auth]', msg);
}
},
},
(err, cookie) => {
// alexa-cookie2 はブラウザを開くよう促すメッセージも err として渡してくる
if (err) {
const msg = err.message || String(err);
if (msg.includes('Please open')) {
// これは実際のエラーではなく「ブラウザで開いて」という指示
console.log('\n>>> ブラウザで http://localhost:3456/ を開いて Amazon にログインしてください <<<\n');
// プロキシを生かしたまま待機process.exit しない)
return;
}
console.error('\n[ERROR] 認証失敗:', msg);
process.exit(1);
}
console.log('\n==============================================');
console.log(' Cookie 取得成功!');
console.log('==============================================');
console.log('\n以下の値を Windmill Variable に登録してください:');
console.log(' パス: u/admin/ALEXA_COOKIE');
console.log(' Secret: ONチェックを入れる');
console.log('\n--- Cookie ---');
console.log(cookie);
console.log('--- ここまで ---\n');
process.exit(0);
}
);
/**
* alexa-api/auth.js
* ローカル PC で実行して Amazon Cookie を取得するスクリプト。
*
* 使い方:
* cd alexa-api
* npm install alexa-cookie2
* node auth.js
*
* → ブラウザで http://localhost:3456 を開く
* → Amazon にログイン
* → コンソールに Cookie が表示される
* → その値を Windmill Variable "u/admin/ALEXA_COOKIE" に登録
*/
const AlexaCookie = require('alexa-cookie2');
const PROXY_PORT = 3456;
console.log('==============================================');
console.log(' Alexa Cookie 取得ツール');
console.log('==============================================');
console.log(`\n認証プロキシを起動中... (port ${PROXY_PORT})`);
console.log('\n【手順】');
console.log(` 1. ブラウザで http://localhost:${PROXY_PORT} を開く`);
console.log(' 2. Amazon アカウントにログインamazon.co.jp');
console.log(' 3. ログイン完了後、このコンソールに Cookie が表示される\n');
AlexaCookie.generateAlexaCookie(
'',
{
amazonPage: 'amazon.co.jp',
acceptLanguage: 'ja-JP',
setupProxy: true,
proxyPort: PROXY_PORT,
proxyOwnIp: '127.0.0.1',
proxyListenBind: '0.0.0.0',
logger: (msg) => {
if (!msg.includes('verbose') && !msg.includes('DEBUG')) {
console.log('[auth]', msg);
}
},
},
(err, cookie) => {
// alexa-cookie2 はブラウザを開くよう促すメッセージも err として渡してくる
if (err) {
const msg = err.message || String(err);
if (msg.includes('Please open')) {
// これは実際のエラーではなく「ブラウザで開いて」という指示
console.log('\n>>> ブラウザで http://localhost:3456/ を開いて Amazon にログインしてください <<<\n');
// プロキシを生かしたまま待機process.exit しない)
return;
}
console.error('\n[ERROR] 認証失敗:', msg);
process.exit(1);
}
console.log('\n==============================================');
console.log(' Cookie 取得成功!');
console.log('==============================================');
console.log('\n以下の値を Windmill Variable に登録してください:');
console.log(' パス: u/admin/ALEXA_COOKIE');
console.log(' Secret: ONチェックを入れる');
console.log('\n--- Cookie ---');
console.log(cookie);
console.log('--- ここまで ---\n');
process.exit(0);
}
);

View File

@@ -1,76 +1,76 @@
/**
* auth2.js - alexa-remote2 自身の認証フローを使う
* alexa-cookie2 より確実(ライブラリ内蔵の OAuth プロキシを使用)
*
* 使い方:
* node auth2.js
* → ブラウザで http://localhost:3001/ を開いて Amazon にログイン
* → 成功するとコンソールに Cookie が出力される → .env に保存
*/
const AlexaRemote = require('alexa-remote2');
const fs = require('fs');
const path = require('path');
const PORT = 3001;
console.log('==============================================');
console.log(' Alexa 認証ツール (alexa-remote2 内蔵プロキシ)');
console.log('==============================================');
console.log(`\nプロキシ起動中... (port ${PORT})`);
console.log(`\n【手順】ブラウザで http://localhost:${PORT}/ を開いて Amazon にログイン\n`);
const alexa = new AlexaRemote();
alexa.init(
{
cookie: null,
alexaServiceHost: 'alexa.amazon.co.jp',
amazonPage: 'amazon.co.jp',
acceptLanguage: 'ja-JP',
useWsMqtt: false,
setupProxy: true,
proxyOwnIp: '127.0.0.1',
proxyPort: PORT,
proxyListenBind: '0.0.0.0',
logger: console.log,
onSucess: (refreshedCookie) => {
// 認証成功時にリフレッシュされた Cookie を受け取る
console.log('\n[onSucess] Cookie refreshed');
},
},
(err, result) => {
if (err) {
const msg = err.message || String(err);
if (msg.includes('open') && (msg.includes('http://') || msg.includes('localhost'))) {
console.log(`\n>>> ブラウザで http://localhost:${PORT}/ を開いてください <<<\n`);
// プロキシを生かしたまま待機
return;
}
console.error('\n[ERROR]', msg);
return;
}
// 認証成功
console.log('\n==============================================');
console.log(' 認証成功!');
console.log('==============================================');
// alexa-remote2 内部の Cookie を取得
const cookie = alexa.cookie || alexa._options?.cookie;
if (cookie) {
const cookieStr = typeof cookie === 'string' ? cookie : JSON.stringify(cookie);
console.log('\n以下を alexa-api/.env の ALEXA_COOKIE に設定してください:\n');
console.log('ALEXA_COOKIE=' + cookieStr);
// .env に自動保存
const envPath = path.join(__dirname, '.env');
fs.writeFileSync(envPath, 'ALEXA_COOKIE=' + cookieStr + '\n');
console.log(`\n.env に自動保存しました: ${envPath}`);
} else {
console.log('Cookie を取得できませんでした。alexa オブジェクト:', Object.keys(alexa));
}
process.exit(0);
}
);
/**
* auth2.js - alexa-remote2 自身の認証フローを使う
* alexa-cookie2 より確実(ライブラリ内蔵の OAuth プロキシを使用)
*
* 使い方:
* node auth2.js
* → ブラウザで http://localhost:3001/ を開いて Amazon にログイン
* → 成功するとコンソールに Cookie が出力される → .env に保存
*/
const AlexaRemote = require('alexa-remote2');
const fs = require('fs');
const path = require('path');
const PORT = 3001;
console.log('==============================================');
console.log(' Alexa 認証ツール (alexa-remote2 内蔵プロキシ)');
console.log('==============================================');
console.log(`\nプロキシ起動中... (port ${PORT})`);
console.log(`\n【手順】ブラウザで http://localhost:${PORT}/ を開いて Amazon にログイン\n`);
const alexa = new AlexaRemote();
alexa.init(
{
cookie: null,
alexaServiceHost: 'alexa.amazon.co.jp',
amazonPage: 'amazon.co.jp',
acceptLanguage: 'ja-JP',
useWsMqtt: false,
setupProxy: true,
proxyOwnIp: '127.0.0.1',
proxyPort: PORT,
proxyListenBind: '0.0.0.0',
logger: console.log,
onSucess: (refreshedCookie) => {
// 認証成功時にリフレッシュされた Cookie を受け取る
console.log('\n[onSucess] Cookie refreshed');
},
},
(err, result) => {
if (err) {
const msg = err.message || String(err);
if (msg.includes('open') && (msg.includes('http://') || msg.includes('localhost'))) {
console.log(`\n>>> ブラウザで http://localhost:${PORT}/ を開いてください <<<\n`);
// プロキシを生かしたまま待機
return;
}
console.error('\n[ERROR]', msg);
return;
}
// 認証成功
console.log('\n==============================================');
console.log(' 認証成功!');
console.log('==============================================');
// alexa-remote2 内部の Cookie を取得
const cookie = alexa.cookie || alexa._options?.cookie;
if (cookie) {
const cookieStr = typeof cookie === 'string' ? cookie : JSON.stringify(cookie);
console.log('\n以下を alexa-api/.env の ALEXA_COOKIE に設定してください:\n');
console.log('ALEXA_COOKIE=' + cookieStr);
// .env に自動保存
const envPath = path.join(__dirname, '.env');
fs.writeFileSync(envPath, 'ALEXA_COOKIE=' + cookieStr + '\n');
console.log(`\n.env に自動保存しました: ${envPath}`);
} else {
console.log('Cookie を取得できませんでした。alexa オブジェクト:', Object.keys(alexa));
}
process.exit(0);
}
);

View File

@@ -1,76 +1,76 @@
/**
* auth3.js - メール/パスワードで直接認証2FA なしのアカウント向け)
*
* 使い方:
* AMAZON_EMAIL="xxx@xxx.com" AMAZON_PASSWORD="yourpass" node auth3.js
*
* 成功すると .env を更新して終了します。
*/
const AlexaRemote = require('alexa-remote2');
const fs = require('fs');
const path = require('path');
const email = process.env.AMAZON_EMAIL;
const password = process.env.AMAZON_PASSWORD;
if (!email || !password) {
console.error('[ERROR] 環境変数 AMAZON_EMAIL と AMAZON_PASSWORD を設定してください');
console.error(' 例: AMAZON_EMAIL="xxx@xxx.com" AMAZON_PASSWORD="pass" node auth3.js');
process.exit(1);
}
console.log(`[INFO] ${email} でログイン試行中...`);
const alexa = new AlexaRemote();
alexa.init(
{
email,
password,
alexaServiceHost: 'alexa.amazon.co.jp',
amazonPage: 'amazon.co.jp',
acceptLanguage: 'ja-JP',
useWsMqtt: false,
setupProxy: false,
logger: (msg) => {
if (!msg.includes('verbose') && !msg.includes('Bearer')) {
console.log('[alexa]', msg);
}
},
onSucess: (refreshedCookie) => {
saveCookie(refreshedCookie, 'onSucess refresh');
},
},
(err) => {
if (err) {
console.error('[ERROR] 認証失敗(詳細):', err);
console.error('\n考えられる原因:');
console.error(' - パスワードが違う');
console.error(' - Amazon が CAPTCHA を要求している(後で再試行)');
console.error(' - 2FA が実際は有効になっている');
process.exit(1);
}
// 認証成功
const cookie = alexa.cookie;
saveCookie(cookie, 'init success');
process.exit(0);
}
);
function saveCookie(cookie, source) {
if (!cookie) {
console.error(`[${source}] Cookie が空です`);
return;
}
const cookieStr = typeof cookie === 'string' ? cookie : JSON.stringify(cookie);
const envPath = path.join(__dirname, '.env');
fs.writeFileSync(envPath, 'ALEXA_COOKIE=' + cookieStr + '\n');
console.log('\n==============================================');
console.log(' 認証成功!');
console.log('==============================================');
console.log('.env を更新しました:', envPath);
console.log('Cookie length:', cookieStr.length);
}
/**
* auth3.js - メール/パスワードで直接認証2FA なしのアカウント向け)
*
* 使い方:
* AMAZON_EMAIL="xxx@xxx.com" AMAZON_PASSWORD="yourpass" node auth3.js
*
* 成功すると .env を更新して終了します。
*/
const AlexaRemote = require('alexa-remote2');
const fs = require('fs');
const path = require('path');
const email = process.env.AMAZON_EMAIL;
const password = process.env.AMAZON_PASSWORD;
if (!email || !password) {
console.error('[ERROR] 環境変数 AMAZON_EMAIL と AMAZON_PASSWORD を設定してください');
console.error(' 例: AMAZON_EMAIL="xxx@xxx.com" AMAZON_PASSWORD="pass" node auth3.js');
process.exit(1);
}
console.log(`[INFO] ${email} でログイン試行中...`);
const alexa = new AlexaRemote();
alexa.init(
{
email,
password,
alexaServiceHost: 'alexa.amazon.co.jp',
amazonPage: 'amazon.co.jp',
acceptLanguage: 'ja-JP',
useWsMqtt: false,
setupProxy: false,
logger: (msg) => {
if (!msg.includes('verbose') && !msg.includes('Bearer')) {
console.log('[alexa]', msg);
}
},
onSucess: (refreshedCookie) => {
saveCookie(refreshedCookie, 'onSucess refresh');
},
},
(err) => {
if (err) {
console.error('[ERROR] 認証失敗(詳細):', err);
console.error('\n考えられる原因:');
console.error(' - パスワードが違う');
console.error(' - Amazon が CAPTCHA を要求している(後で再試行)');
console.error(' - 2FA が実際は有効になっている');
process.exit(1);
}
// 認証成功
const cookie = alexa.cookie;
saveCookie(cookie, 'init success');
process.exit(0);
}
);
function saveCookie(cookie, source) {
if (!cookie) {
console.error(`[${source}] Cookie が空です`);
return;
}
const cookieStr = typeof cookie === 'string' ? cookie : JSON.stringify(cookie);
const envPath = path.join(__dirname, '.env');
fs.writeFileSync(envPath, 'ALEXA_COOKIE=' + cookieStr + '\n');
console.log('\n==============================================');
console.log(' 認証成功!');
console.log('==============================================');
console.log('.env を更新しました:', envPath);
console.log('Cookie length:', cookieStr.length);
}

View File

@@ -1,190 +1,190 @@
/**
* auth4.js - Amazon Japan OpenID フローを正しく再現するカスタム認証スクリプト
* alexa-cookie2 の古いエンドポイント問題を回避して直接フォームを処理する
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const EMAIL = process.env.AMAZON_EMAIL;
const PASSWORD = process.env.AMAZON_PASSWORD;
if (!EMAIL || !PASSWORD) {
console.error('[ERROR] 環境変数 AMAZON_EMAIL と AMAZON_PASSWORD を設定してください');
process.exit(1);
}
const ALEXA_LOGIN_URL =
'https://www.amazon.co.jp/ap/signin?' +
new URLSearchParams({
'openid.assoc_handle': 'amzn_dp_project_dee_jp',
'openid.mode': 'checkid_setup',
'openid.ns': 'http://specs.openid.net/auth/2.0',
'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
'openid.return_to': 'https://alexa.amazon.co.jp/api/apps/v1/token',
'pageId': 'amzn_dp_project_dee_jp',
}).toString();
const USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
let cookieJar = {};
function setCookies(setCookieHeaders) {
if (!setCookieHeaders) return;
const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
for (const h of headers) {
const [kv] = h.split(';');
const [k, v] = kv.trim().split('=');
if (k && v !== undefined) cookieJar[k.trim()] = v.trim();
}
}
function getCookieHeader() {
return Object.entries(cookieJar)
.map(([k, v]) => `${k}=${v}`)
.join('; ');
}
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const reqOpts = {
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
method: options.method || 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept-Language': 'ja-JP,ja;q=0.9',
'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8',
'Cookie': getCookieHeader(),
...(options.headers || {}),
},
};
const req = https.request(reqOpts, (res) => {
setCookies(res.headers['set-cookie']);
let body = '';
res.on('data', (d) => (body += d));
res.on('end', () => {
resolve({ status: res.statusCode, headers: res.headers, body });
});
});
req.on('error', reject);
if (options.body) req.write(options.body);
req.end();
});
}
// HTML の hidden フィールドを抽出
function extractHiddenFields(html) {
const fields = {};
const re = /<input[^>]+type=["']?hidden["']?[^>]*>/gi;
let match;
while ((match = re.exec(html)) !== null) {
const tag = match[0];
const name = (tag.match(/name=["']([^"']+)["']/) || [])[1];
const value = (tag.match(/value=["']([^"']*)["']/) || ['', ''])[1];
if (name) fields[name] = value;
}
return fields;
}
// フォームの action URL を抽出
function extractFormAction(html) {
const m = html.match(/id="ap_login_form"[^>]+action="([^"]+)"/);
if (m) return m[1].replace(/&amp;/g, '&');
const m2 = html.match(/name="signIn"[^>]+action="([^"]+)"/);
if (m2) return m2[1].replace(/&amp;/g, '&');
return null;
}
async function main() {
console.log('[1] ログインページ取得中...');
const page1 = await request(ALEXA_LOGIN_URL);
console.log(` Status: ${page1.status}, Cookies: ${Object.keys(cookieJar).join(', ')}`);
if (page1.status !== 200) {
console.error(`[ERROR] ログインページ取得失敗: ${page1.status}`);
process.exit(1);
}
// フォーム情報を抽出
const action = extractFormAction(page1.body);
const hidden = extractHiddenFields(page1.body);
if (!action) {
console.error('[ERROR] ログインフォームが見つかりません。HTMLを確認します:');
console.error(page1.body.substring(0, 500));
process.exit(1);
}
console.log(`[2] フォーム送信先: ${action}`);
console.log(` Hidden fields: ${Object.keys(hidden).join(', ')}`);
// フォームデータ構築
const formData = new URLSearchParams({
...hidden,
email: EMAIL,
password: PASSWORD,
rememberMe: 'true',
}).toString();
console.log('[3] 認証送信中...');
const page2 = await request(action, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': ALEXA_LOGIN_URL,
},
body: formData,
});
console.log(` Status: ${page2.status}`);
console.log(` Location: ${page2.headers.location || '(none)'}`);
console.log(` Cookies after login: ${Object.keys(cookieJar).join(', ')}`);
// リダイレクトをたどる
let current = page2;
let redirectCount = 0;
while (current.status >= 300 && current.status < 400 && current.headers.location && redirectCount < 10) {
const loc = current.headers.location;
const nextUrl = loc.startsWith('http') ? loc : `https://www.amazon.co.jp${loc}`;
console.log(`[${4 + redirectCount}] Redirect → ${nextUrl.substring(0, 80)}...`);
current = await request(nextUrl);
console.log(` Status: ${current.status}, New Cookies: ${Object.keys(cookieJar).join(', ')}`);
redirectCount++;
}
// 成功判定: at-acbjp または session-token が含まれているか
const cookie = getCookieHeader();
const hasAlexaToken = cookie.includes('at-acbjp') || cookie.includes('session-token');
if (!hasAlexaToken) {
console.error('[ERROR] 認証に失敗しました。取得できたCookieに認証トークンが含まれていません。');
console.error('取得済みCookieキー:', Object.keys(cookieJar).join(', '));
if (current.body.includes('captcha') || current.body.includes('CAPTCHA')) {
console.error('※ CAPTCHA が要求されています。しばらく待ってから再試行してください。');
}
if (current.body.includes('password') && current.body.includes('error')) {
console.error('※ パスワードが間違っている可能性があります。');
}
process.exit(1);
}
// .env に保存
const envPath = path.join(__dirname, '.env');
fs.writeFileSync(envPath, `ALEXA_COOKIE=${cookie}\n`);
console.log('\n==============================================');
console.log(' 認証成功!');
console.log('==============================================');
console.log(`.env を保存しました: ${envPath}`);
console.log(`Cookie 長さ: ${cookie.length} 文字`);
}
main().catch((err) => {
console.error('[FATAL]', err);
process.exit(1);
});
/**
* auth4.js - Amazon Japan OpenID フローを正しく再現するカスタム認証スクリプト
* alexa-cookie2 の古いエンドポイント問題を回避して直接フォームを処理する
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const EMAIL = process.env.AMAZON_EMAIL;
const PASSWORD = process.env.AMAZON_PASSWORD;
if (!EMAIL || !PASSWORD) {
console.error('[ERROR] 環境変数 AMAZON_EMAIL と AMAZON_PASSWORD を設定してください');
process.exit(1);
}
const ALEXA_LOGIN_URL =
'https://www.amazon.co.jp/ap/signin?' +
new URLSearchParams({
'openid.assoc_handle': 'amzn_dp_project_dee_jp',
'openid.mode': 'checkid_setup',
'openid.ns': 'http://specs.openid.net/auth/2.0',
'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
'openid.return_to': 'https://alexa.amazon.co.jp/api/apps/v1/token',
'pageId': 'amzn_dp_project_dee_jp',
}).toString();
const USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
let cookieJar = {};
function setCookies(setCookieHeaders) {
if (!setCookieHeaders) return;
const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
for (const h of headers) {
const [kv] = h.split(';');
const [k, v] = kv.trim().split('=');
if (k && v !== undefined) cookieJar[k.trim()] = v.trim();
}
}
function getCookieHeader() {
return Object.entries(cookieJar)
.map(([k, v]) => `${k}=${v}`)
.join('; ');
}
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const reqOpts = {
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
method: options.method || 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept-Language': 'ja-JP,ja;q=0.9',
'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8',
'Cookie': getCookieHeader(),
...(options.headers || {}),
},
};
const req = https.request(reqOpts, (res) => {
setCookies(res.headers['set-cookie']);
let body = '';
res.on('data', (d) => (body += d));
res.on('end', () => {
resolve({ status: res.statusCode, headers: res.headers, body });
});
});
req.on('error', reject);
if (options.body) req.write(options.body);
req.end();
});
}
// HTML の hidden フィールドを抽出
function extractHiddenFields(html) {
const fields = {};
const re = /<input[^>]+type=["']?hidden["']?[^>]*>/gi;
let match;
while ((match = re.exec(html)) !== null) {
const tag = match[0];
const name = (tag.match(/name=["']([^"']+)["']/) || [])[1];
const value = (tag.match(/value=["']([^"']*)["']/) || ['', ''])[1];
if (name) fields[name] = value;
}
return fields;
}
// フォームの action URL を抽出
function extractFormAction(html) {
const m = html.match(/id="ap_login_form"[^>]+action="([^"]+)"/);
if (m) return m[1].replace(/&amp;/g, '&');
const m2 = html.match(/name="signIn"[^>]+action="([^"]+)"/);
if (m2) return m2[1].replace(/&amp;/g, '&');
return null;
}
async function main() {
console.log('[1] ログインページ取得中...');
const page1 = await request(ALEXA_LOGIN_URL);
console.log(` Status: ${page1.status}, Cookies: ${Object.keys(cookieJar).join(', ')}`);
if (page1.status !== 200) {
console.error(`[ERROR] ログインページ取得失敗: ${page1.status}`);
process.exit(1);
}
// フォーム情報を抽出
const action = extractFormAction(page1.body);
const hidden = extractHiddenFields(page1.body);
if (!action) {
console.error('[ERROR] ログインフォームが見つかりません。HTMLを確認します:');
console.error(page1.body.substring(0, 500));
process.exit(1);
}
console.log(`[2] フォーム送信先: ${action}`);
console.log(` Hidden fields: ${Object.keys(hidden).join(', ')}`);
// フォームデータ構築
const formData = new URLSearchParams({
...hidden,
email: EMAIL,
password: PASSWORD,
rememberMe: 'true',
}).toString();
console.log('[3] 認証送信中...');
const page2 = await request(action, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': ALEXA_LOGIN_URL,
},
body: formData,
});
console.log(` Status: ${page2.status}`);
console.log(` Location: ${page2.headers.location || '(none)'}`);
console.log(` Cookies after login: ${Object.keys(cookieJar).join(', ')}`);
// リダイレクトをたどる
let current = page2;
let redirectCount = 0;
while (current.status >= 300 && current.status < 400 && current.headers.location && redirectCount < 10) {
const loc = current.headers.location;
const nextUrl = loc.startsWith('http') ? loc : `https://www.amazon.co.jp${loc}`;
console.log(`[${4 + redirectCount}] Redirect → ${nextUrl.substring(0, 80)}...`);
current = await request(nextUrl);
console.log(` Status: ${current.status}, New Cookies: ${Object.keys(cookieJar).join(', ')}`);
redirectCount++;
}
// 成功判定: at-acbjp または session-token が含まれているか
const cookie = getCookieHeader();
const hasAlexaToken = cookie.includes('at-acbjp') || cookie.includes('session-token');
if (!hasAlexaToken) {
console.error('[ERROR] 認証に失敗しました。取得できたCookieに認証トークンが含まれていません。');
console.error('取得済みCookieキー:', Object.keys(cookieJar).join(', '));
if (current.body.includes('captcha') || current.body.includes('CAPTCHA')) {
console.error('※ CAPTCHA が要求されています。しばらく待ってから再試行してください。');
}
if (current.body.includes('password') && current.body.includes('error')) {
console.error('※ パスワードが間違っている可能性があります。');
}
process.exit(1);
}
// .env に保存
const envPath = path.join(__dirname, '.env');
fs.writeFileSync(envPath, `ALEXA_COOKIE=${cookie}\n`);
console.log('\n==============================================');
console.log(' 認証成功!');
console.log('==============================================');
console.log(`.env を保存しました: ${envPath}`);
console.log(`Cookie 長さ: ${cookie.length} 文字`);
}
main().catch((err) => {
console.error('[FATAL]', err);
process.exit(1);
});

View File

@@ -1,19 +1,19 @@
services:
alexa-api:
build: .
container_name: alexa_api
restart: unless-stopped
env_file:
- .env
environment:
- PORT=3500
networks:
- windmill_windmill-internal
# 外部には公開しないWindmill ワーカーから内部ネットワーク経由でのみアクセス)
# デバッグ時は以下のコメントを外す:
# ports:
# - "127.0.0.1:3500:3500"
networks:
windmill_windmill-internal:
external: true
services:
alexa-api:
build: .
container_name: alexa_api
restart: unless-stopped
env_file:
- .env
environment:
- PORT=3500
networks:
- windmill_windmill-internal
# 外部には公開しないWindmill ワーカーから内部ネットワーク経由でのみアクセス)
# デバッグ時は以下のコメントを外す:
# ports:
# - "127.0.0.1:3500:3500"
networks:
windmill_windmill-internal:
external: true

File diff suppressed because it is too large Load Diff

View File

@@ -1,130 +1,130 @@
/**
* test_tts.js - TTS API テスト
* node test_tts.js
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const envContent = fs.readFileSync(path.join(__dirname, '.env'), 'utf8');
const COOKIE_STR = envContent.match(/ALEXA_COOKIE=(.+)/)[1].trim();
function makeRequest(url, options = {}, extraCookies = '') {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const allCookies = COOKIE_STR + (extraCookies ? '; ' + extraCookies : '');
const reqOpts = {
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
method: options.method || 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ja-JP,ja;q=0.9',
'Cookie': allCookies,
...(options.headers || {}),
},
};
const req = https.request(reqOpts, (res) => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
});
req.on('error', reject);
if (options.body) req.write(options.body);
req.end();
});
}
async function main() {
// 1. CSRFトークン取得
console.log('[1] CSRF token取得...');
const langRes = await makeRequest('https://alexa.amazon.co.jp/api/language');
const setCookies = langRes.headers['set-cookie'] || [];
const csrfCookieStr = setCookies.find(c => c.startsWith('csrf='));
const csrfToken = csrfCookieStr ? csrfCookieStr.split('=')[1].split(';')[0] : null;
console.log(' CSRF token:', csrfToken);
console.log(' Status:', langRes.status);
if (!csrfToken) { console.error('CSRF token not found!'); process.exit(1); }
// 2. デバイス一覧取得
console.log('[2] デバイス一覧取得...');
const devRes = await makeRequest('https://alexa.amazon.co.jp/api/devices-v2/device?cached=false');
console.log(' Status:', devRes.status);
const devices = JSON.parse(devRes.body).devices || [];
console.log(' Device count:', devices.length);
// デバイス一覧表示
devices.filter(d => d.deviceFamily !== 'TABLET').forEach(d => {
console.log(` - ${d.accountName} (type=${d.deviceType}, serial=${d.serialNumber})`);
});
// プレハブを探す
const target = devices.find(d => d.serialNumber === 'G0922H08525302K5'); // オフィスの右エコー(以前成功したデバイス)
console.log('\nTarget device:', target ? `${target.accountName}` : 'NOT FOUND');
if (!target) { process.exit(1); }
// 2.5. カスタマーIDを取得
const bootstrapRes = await makeRequest('https://alexa.amazon.co.jp/api/bootstrap');
const bootstrap = JSON.parse(bootstrapRes.body);
const customerId = bootstrap.authentication?.customerId;
console.log(' Customer ID:', customerId);
// 3. TTSリクエスト新Cookie + Alexa.Speak + locale:'ja-JP' + 日本語テキスト)
const sequenceObj = {
'@type': 'com.amazon.alexa.behaviors.model.Sequence',
startNode: {
'@type': 'com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode',
type: 'Alexa.Speak',
operationPayload: {
deviceType: target.deviceType,
deviceSerialNumber: target.serialNumber,
customerId: customerId,
locale: 'ja-JP',
textToSpeak: '\u3053\u308c\u306f\u65e5\u672c\u8a9e\u306e\u30c6\u30b9\u30c8\u3067\u3059', // 「これは日本語のテストです」
},
},
};
// non-ASCII を \uXXXX に強制エスケープ
// Amazon のパーサーが sequenceJson 内の raw UTF-8 を処理できない場合の回避策
const rawSequenceJson = JSON.stringify(sequenceObj).replace(
/[\u0080-\uffff]/g,
c => '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)
);
const bodyObj = {
behaviorId: 'PREVIEW',
sequenceJson: rawSequenceJson,
status: 'ENABLED',
};
const body = JSON.stringify(bodyObj);
console.log('\n[3] TTS送信...');
// 送信内容確認textToSpeakの部分が\uXXXXエスケープになっているか
const ttsIdx = body.indexOf('textToSpeak');
console.log(' textToSpeak部分:', body.substring(ttsIdx, ttsIdx + 80));
const ttsRes = await makeRequest('https://alexa.amazon.co.jp/api/behaviors/preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'csrf': csrfToken,
'Referer': 'https://alexa.amazon.co.jp/spa/index.html',
'Origin': 'https://alexa.amazon.co.jp',
},
body,
}, `csrf=${csrfToken}`);
console.log('TTS status:', ttsRes.status);
console.log('TTS response:', ttsRes.body.substring(0, 500));
if (ttsRes.status === 200 || ttsRes.status === 202) {
console.log('\n成功エコーがしゃべるはずです。');
}
}
main().catch(console.error);
/**
* test_tts.js - TTS API テスト
* node test_tts.js
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const envContent = fs.readFileSync(path.join(__dirname, '.env'), 'utf8');
const COOKIE_STR = envContent.match(/ALEXA_COOKIE=(.+)/)[1].trim();
function makeRequest(url, options = {}, extraCookies = '') {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const allCookies = COOKIE_STR + (extraCookies ? '; ' + extraCookies : '');
const reqOpts = {
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
method: options.method || 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ja-JP,ja;q=0.9',
'Cookie': allCookies,
...(options.headers || {}),
},
};
const req = https.request(reqOpts, (res) => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
});
req.on('error', reject);
if (options.body) req.write(options.body);
req.end();
});
}
async function main() {
// 1. CSRFトークン取得
console.log('[1] CSRF token取得...');
const langRes = await makeRequest('https://alexa.amazon.co.jp/api/language');
const setCookies = langRes.headers['set-cookie'] || [];
const csrfCookieStr = setCookies.find(c => c.startsWith('csrf='));
const csrfToken = csrfCookieStr ? csrfCookieStr.split('=')[1].split(';')[0] : null;
console.log(' CSRF token:', csrfToken);
console.log(' Status:', langRes.status);
if (!csrfToken) { console.error('CSRF token not found!'); process.exit(1); }
// 2. デバイス一覧取得
console.log('[2] デバイス一覧取得...');
const devRes = await makeRequest('https://alexa.amazon.co.jp/api/devices-v2/device?cached=false');
console.log(' Status:', devRes.status);
const devices = JSON.parse(devRes.body).devices || [];
console.log(' Device count:', devices.length);
// デバイス一覧表示
devices.filter(d => d.deviceFamily !== 'TABLET').forEach(d => {
console.log(` - ${d.accountName} (type=${d.deviceType}, serial=${d.serialNumber})`);
});
// プレハブを探す
const target = devices.find(d => d.serialNumber === 'G0922H08525302K5'); // オフィスの右エコー(以前成功したデバイス)
console.log('\nTarget device:', target ? `${target.accountName}` : 'NOT FOUND');
if (!target) { process.exit(1); }
// 2.5. カスタマーIDを取得
const bootstrapRes = await makeRequest('https://alexa.amazon.co.jp/api/bootstrap');
const bootstrap = JSON.parse(bootstrapRes.body);
const customerId = bootstrap.authentication?.customerId;
console.log(' Customer ID:', customerId);
// 3. TTSリクエスト新Cookie + Alexa.Speak + locale:'ja-JP' + 日本語テキスト)
const sequenceObj = {
'@type': 'com.amazon.alexa.behaviors.model.Sequence',
startNode: {
'@type': 'com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode',
type: 'Alexa.Speak',
operationPayload: {
deviceType: target.deviceType,
deviceSerialNumber: target.serialNumber,
customerId: customerId,
locale: 'ja-JP',
textToSpeak: '\u3053\u308c\u306f\u65e5\u672c\u8a9e\u306e\u30c6\u30b9\u30c8\u3067\u3059', // 「これは日本語のテストです」
},
},
};
// non-ASCII を \uXXXX に強制エスケープ
// Amazon のパーサーが sequenceJson 内の raw UTF-8 を処理できない場合の回避策
const rawSequenceJson = JSON.stringify(sequenceObj).replace(
/[\u0080-\uffff]/g,
c => '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)
);
const bodyObj = {
behaviorId: 'PREVIEW',
sequenceJson: rawSequenceJson,
status: 'ENABLED',
};
const body = JSON.stringify(bodyObj);
console.log('\n[3] TTS送信...');
// 送信内容確認textToSpeakの部分が\uXXXXエスケープになっているか
const ttsIdx = body.indexOf('textToSpeak');
console.log(' textToSpeak部分:', body.substring(ttsIdx, ttsIdx + 80));
const ttsRes = await makeRequest('https://alexa.amazon.co.jp/api/behaviors/preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'csrf': csrfToken,
'Referer': 'https://alexa.amazon.co.jp/spa/index.html',
'Origin': 'https://alexa.amazon.co.jp',
},
body,
}, `csrf=${csrfToken}`);
console.log('TTS status:', ttsRes.status);
console.log('TTS response:', ttsRes.body.substring(0, 500));
if (ttsRes.status === 200 || ttsRes.status === 202) {
console.log('\n成功エコーがしゃべるはずです。');
}
}
main().catch(console.error);

View File

@@ -1,459 +1,459 @@
# マスタードキュメント - Windmill通知ワークフロー編
> **最終更新**: 2026-02-21
> **対象システム**: windmill.keinafarm.netワークスペース: admins
> **目的**: このドキュメントだけでWindmill通知ワークフローの全容を把握できること
---
## 目次
1. [機能概要](#1-機能概要)
2. [フロー設計](#2-フロー設計)
3. [変更履歴取得API仕様](#3-変更履歴取得api仕様)
4. [LINE通知仕様](#4-line通知仕様)
5. [Windmill設定仕様](#5-windmill設定仕様)
6. [状態管理仕様](#6-状態管理仕様)
7. [設計判断と制約](#7-設計判断と制約)
8. [運用手順](#8-運用手順)
9. [ソースファイル索引](#9-ソースファイル索引)
10. [更新履歴](#更新履歴)
---
## 1. 機能概要
### 目的
`shiraou.keinafarm.net`(白皇集落営農組合 統合システムで発生した予約・実績の変更を、LINE Messaging API 経由で管理者に通知する。
### ユーザーフロー
```
統合システム上で予約・実績の変更が発生
└→ Windmill が5分毎にポーリング
└→ 変更があればLINEにプッシュ通知
└→ 管理者がLINEで変更内容を確認
```
### 通知される操作一覧
| 区分 | 操作 | 説明 |
|------|------|------|
| 予約 | `create` | 予約が作成された |
| 予約 | `update` | 予約の日時・機械が変更された |
| 予約 | `cancel` | 予約がキャンセルされた |
| 実績 | `create` | 実績が登録された |
| 実績 | `update` | 実績が修正された |
| 実績 | `delete` | 実績が削除された |
---
## 2. フロー設計
### Windmillフロー情報
| 項目 | 値 |
|------|-----|
| パス | `f/shiraou/shiraou_notification` |
| 概要 | 白皇集落営農 変更通知 |
| ステップ数 | 1単一Pythonスクリプト |
| スケジュール | `0 */5 * * * *`5分毎、JST |
| スケジュールパス | `f/shiraou/shiraou_notification_every_5min` |
### 実行フロー(擬似コード)
```python
# Step 1: シークレット・前回実行時刻を取得
api_key = get_variable("u/admin/NOTIFICATION_API_KEY")
line_token = get_variable("u/admin/LINE_CHANNEL_ACCESS_TOKEN")
line_to = get_variable("u/admin/LINE_TO")
last_checked = get_variable("u/admin/SHIRAOU_LAST_CHECKED_AT") # 空なら初回
since = last_checked or (now() - 10)
# Step 2: 変更履歴を取得
response = GET "https://shiraou.keinafarm.net/reservations/api/changes/?since={since}"
headers: { "X-API-Key": api_key }
# Step 3: 変更があればLINE通知
if response.reservations or response.usages:
message = format_message(response)
POST "https://api.line.me/v2/bot/message/push"
body: { "to": line_to, "messages": [{"type": "text", "text": message}] }
# Step 4: 前回実行時刻を更新(正常完了時のみ)
set_variable("u/admin/SHIRAOU_LAST_CHECKED_AT", response.checked_at)
```
### エラー時の挙動
- API呼び出し失敗、LINE送信失敗のいずれでも例外が発生
- 例外が発生した場合、`SHIRAOU_LAST_CHECKED_AT` は更新されない
- 次回実行時に同じ `since` で再試行される(変更の取り漏れ防止)
---
## 3. 変更履歴取得API仕様
### エンドポイント
```
GET https://shiraou.keinafarm.net/reservations/api/changes/
```
### 認証
```
X-API-Key: <NOTIFICATION_API_KEY>
```
APIキーが不正な場合は `401 Unauthorized` が返る。
### クエリパラメータ
| パラメータ | 型 | 必須 | 説明 |
|-----------|-----|------|------|
| `since` | ISO8601文字列 | 必須 | この日時以降の変更を取得する |
**`since` の形式例**:
- `2026-02-21T10:00:00+09:00`(タイムゾーン付き、推奨)
- `2026-02-21T10:00:00`ナイーブ、JSTとして扱われる
### レスポンス200 OK
```json
{
"checked_at": "2026-02-21T12:00:00+09:00",
"since": "2026-02-21T10:00:00+09:00",
"reservations": [
{
"operation": "create",
"reservation_id": 123,
"user_name": "田中太郎",
"machine_name": "トラクター",
"start_at": "2026-02-25T09:00:00+09:00",
"end_at": "2026-02-25T12:00:00+09:00",
"operated_at": "2026-02-21T11:30:00+09:00",
"operator_name": "田中太郎",
"reason": ""
}
],
"usages": [
{
"operation": "update",
"usage_id": 456,
"user_name": "山田次郎",
"machine_name": "コンバイン",
"amount": 4.0,
"unit": "時間",
"start_at": "2026-02-20T08:00:00+09:00",
"end_at": "2026-02-20T12:00:00+09:00",
"operated_at": "2026-02-21T11:55:00+09:00",
"operator_name": "管理者A",
"reason": "記録ミスのため修正"
}
]
}
```
### 変更なし時のレスポンス
```json
{
"checked_at": "2026-02-21T12:05:00+09:00",
"since": "2026-02-21T12:00:00+09:00",
"reservations": [],
"usages": []
}
```
### エラーレスポンス
| ステータス | 原因 |
|-----------|------|
| `401 Unauthorized` | APIキーが不正または未設定 |
| `400 Bad Request` | `since` パラメータが欠落または不正な日時形式 |
---
## 4. LINE通知仕様
### 使用API
LINE Messaging API - Push Message
```
POST https://api.line.me/v2/bot/message/push
Authorization: Bearer <LINE_CHANNEL_ACCESS_TOKEN>
Content-Type: application/json
```
### リクエストボディ
```json
{
"to": "<LINE_TO>",
"messages": [
{
"type": "text",
"text": "<フォーマット済みメッセージ>"
}
]
}
```
### メッセージフォーマット
```
📋 営農システム 変更通知
🟢 予約作成
機械: トラクター
利用者: 田中太郎
日時: 2026-02-25 09:00 〜 2026-02-25 12:00
🔴 予約キャンセル
機械: 田植機
利用者: 佐藤花子
日時: 2026-02-22 08:00 〜 2026-02-22 17:00
🔵 実績修正
機械: コンバイン
利用者: 山田次郎
利用量: 4.0時間
日: 2026-02-20
理由: 記録ミスのため修正
```
### アイコン規則
| アイコン | 意味 |
|---------|------|
| 🟢 | 作成create / 予約作成 / 実績登録) |
| 🔵 | 変更update / 予約変更 / 実績修正) |
| 🔴 | 削除・キャンセルcancel / delete |
### 通知先の種別
`LINE_TO` にはユーザーIDまたはグループIDを設定する。
| 種別 | ID形式 |
|------|-------|
| ユーザー | `U` で始まる文字列 |
| グループ | `C` で始まる文字列 |
---
## 5. Windmill設定仕様
### Windmill Variablesシークレット
以下の変数を Windmill UIVariables ページ)で作成・管理する。
| 変数パス | Secret | 説明 | 取得元 |
|---------|--------|------|-------|
| `u/admin/NOTIFICATION_API_KEY` | ✅ | shiraou.keinafarm.net のAPIキー | Djangoサーバー側 `NOTIFICATION_API_KEY` 環境変数と同一値 |
| `u/admin/LINE_CHANNEL_ACCESS_TOKEN` | ✅ | LINE Messaging API チャネルアクセストークン | LINE Developers Console |
| `u/admin/LINE_TO` | ✅ | 通知先のLINEユーザーIDまたはグループID | LINE webhook / profile API |
| `u/admin/SHIRAOU_LAST_CHECKED_AT` | ❌ | 前回確認時刻(ワークフローが自動更新) | ワークフローが自動管理(初期値: 空文字) |
### Django側の設定shiraou.keinafarm.net
`docker-compose.yml` に以下の環境変数を追加:
```yaml
environment:
- NOTIFICATION_API_KEY=<NOTIFICATION_API_KEYと同一の値>
```
APIキー生成コマンド:
```bash
openssl rand -hex 32
```
---
## 6. 状態管理仕様
### 状態変数: `SHIRAOU_LAST_CHECKED_AT`
| 項目 | 内容 |
|------|------|
| 格納場所 | Windmill Variable `u/admin/SHIRAOU_LAST_CHECKED_AT` |
| 型 | ISO8601文字列例: `2026-02-21T15:30:00+09:00` |
| 初期値 | 空文字(初回実行時は `現在時刻 - 10分` を使用) |
| 更新タイミング | フロー正常完了時のみ、APIレスポンスの `checked_at` を保存 |
| 参照タイミング | フロー実行開始時、`since` パラメータとして使用 |
### 重複通知防止の仕組み
```
実行1: since=T0, checked_at=T1 → LAST_CHECKED_AT = T1
実行2: since=T1, checked_at=T2 → T1以降の変更のみ取得
```
- `since``checked_at`APIが確認した時刻を使うことで、変更の取りこぼしが発生しない
- `since`(リクエストに渡した時刻)ではなく `checked_at`(サーバーが確認した時刻)を保存するのがポイント
### 旧実装との違い(トラブルシュート記録)
| | 旧実装 | 現実装 |
|---|--------|--------|
| 状態保存方法 | `wmill.get_state()` / `set_state()` | `wmill.get_variable()` / `set_variable()` |
| 問題 | フローのインラインスクリプトでは実行をまたいで保存されない | - |
| 症状 | 毎回 `since = 現在 - 10分` になり、毎回通知が飛ぶ | 正常動作 |
---
## 7. 設計判断と制約
### 絶対に変えてはいけない制約
1. **`SHIRAOU_LAST_CHECKED_AT` には `checked_at` を保存すること**`since` を保存しない)
- `checked_at`: APIサーバーが「この時刻まで確認した」という保証付きの時刻
- 同じ変更が2度通知されることを防ぐ
2. **状態更新は正常完了後のみ行うこと**
- API呼び出し失敗・LINE送信失敗時は `SHIRAOU_LAST_CHECKED_AT` を更新しない
- 次回実行で同じ範囲を再取得し、通知の取り漏れを防ぐ
3. **`wmill.get_state()` は使用しないこと**
- Windmillのインラインフロースクリプトでは実行をまたいで保存されない
- 状態管理は必ず Windmill Variable を使うこと
### 設計判断
| 判断 | 理由 |
|------|------|
| 単一ステップフロー | 状態管理を1か所に集約するため。`get_state()`/`set_state()` のスコープ問題を回避 |
| SSL検証スキップ | shiraou.keinafarm.net が自己署名証明書の可能性があるため |
| タイムアウト 30秒 | 農業用途で多少の応答遅延を許容しつつ、無限待機を防ぐ |
| 5分ポーリング間隔 | 農業機械の予約用途では数分の遅延は許容範囲。リアルタイム不要 |
---
## 8. 運用手順
### フローを手動実行
```bash
curl -sk -X POST \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
-H "Content-Type: application/json" \
-d '{}' \
"https://windmill.keinafarm.net/api/w/admins/jobs/run/f/f/shiraou/shiraou_notification"
```
### 動作確認curlで直接API呼び出し
```bash
# 変更なし確認
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-02-21T11:59:00%2B09:00"
# 広い範囲で変更取得(初期確認用)
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00"
```
### フローの更新デプロイ手順
```bash
cd /path/to/windmill_workflow
# 1. flows/shiraou_notification.flow.json を編集
# 2. 既存フローを削除して再作成PUTは405のため
curl -sk -X DELETE \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
"https://windmill.keinafarm.net/api/w/admins/flows/delete/f/shiraou/shiraou_notification"
curl -sk -X POST \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
-H "Content-Type: application/json" \
-d @flows/shiraou_notification.flow.json \
"https://windmill.keinafarm.net/api/w/admins/flows/create"
# 3. スケジュールは再作成不要(フローの削除・再作成でも維持される)
```
### APIキーローテーション手順
1. Djangoサーバー側で新しいキーを生成: `openssl rand -hex 32`
2. `docker-compose.yml``NOTIFICATION_API_KEY` を更新してデプロイ
3. Windmill UI で `u/admin/NOTIFICATION_API_KEY` の値を同じ新しいキーに更新
4. フローを手動実行して動作確認
### 過去のジョブ結果確認
```bash
curl -sk -H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
"https://windmill.keinafarm.net/api/w/admins/jobs/list?per_page=10&script_path_exact=f/shiraou/shiraou_notification&is_flow=true"
```
---
## 9. ソースファイル索引
### フロー定義
| ファイル | 説明 |
|---------|------|
| [flows/shiraou_notification.flow.json](../../flows/shiraou_notification.flow.json) | フロー本体。単一Pythonステップでポーリング・通知・状態更新を実行 |
**フロー構造**:
```json
{
"path": "f/shiraou/shiraou_notification",
"value": {
"modules": [
{
"id": "a",
"value": {
"type": "rawscript",
"language": "python3",
"content": "..."
}
}
]
}
}
```
### ヘルパースクリプト
| ファイル | 説明 |
|---------|------|
| [wm-api.sh](../../wm-api.sh) | Windmill REST API操作ヘルパー。フロー作成・スケジュール管理に使用 |
**主要コマンド**:
```
create-flow <file> JSONファイルからフローを作成
create-schedule <file> JSONファイルからスケジュールを作成
flows フロー一覧取得
schedules スケジュール一覧取得
```
### ドキュメント
| ファイル | 説明 |
|---------|------|
| [docs/shiraou/19_windmill_通知ワークフロー連携仕様.md](19_windmill_通知ワークフロー連携仕様.md) | 仕様書。API仕様・メッセージフォーマットの原典 |
| [docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md](20_マスタードキュメント_Windmill通知ワークフロー編.md) | 本ドキュメント |
### エージェントワークフロー
| ファイル | 説明 |
|---------|------|
| [.agent/workflows/windmill-push.md](../../.agent/workflows/windmill-push.md) | Windmillへのpush手順。wmill CLIの制限とAPI代替の経緯を記録 |
| [.agent/workflows/windmill-new-script.md](../../.agent/workflows/windmill-new-script.md) | 新規スクリプト作成手順 |
---
## 更新履歴
| 日付 | 変更内容 |
|------|---------|
| 2026-02-21 | 初版作成。フロー登録・スケジュール設定・状態管理バグ修正を含む |
# マスタードキュメント - Windmill通知ワークフロー編
> **最終更新**: 2026-02-21
> **対象システム**: windmill.keinafarm.netワークスペース: admins
> **目的**: このドキュメントだけでWindmill通知ワークフローの全容を把握できること
---
## 目次
1. [機能概要](#1-機能概要)
2. [フロー設計](#2-フロー設計)
3. [変更履歴取得API仕様](#3-変更履歴取得api仕様)
4. [LINE通知仕様](#4-line通知仕様)
5. [Windmill設定仕様](#5-windmill設定仕様)
6. [状態管理仕様](#6-状態管理仕様)
7. [設計判断と制約](#7-設計判断と制約)
8. [運用手順](#8-運用手順)
9. [ソースファイル索引](#9-ソースファイル索引)
10. [更新履歴](#更新履歴)
---
## 1. 機能概要
### 目的
`shiraou.keinafarm.net`(白皇集落営農組合 統合システムで発生した予約・実績の変更を、LINE Messaging API 経由で管理者に通知する。
### ユーザーフロー
```
統合システム上で予約・実績の変更が発生
└→ Windmill が5分毎にポーリング
└→ 変更があればLINEにプッシュ通知
└→ 管理者がLINEで変更内容を確認
```
### 通知される操作一覧
| 区分 | 操作 | 説明 |
|------|------|------|
| 予約 | `create` | 予約が作成された |
| 予約 | `update` | 予約の日時・機械が変更された |
| 予約 | `cancel` | 予約がキャンセルされた |
| 実績 | `create` | 実績が登録された |
| 実績 | `update` | 実績が修正された |
| 実績 | `delete` | 実績が削除された |
---
## 2. フロー設計
### Windmillフロー情報
| 項目 | 値 |
|------|-----|
| パス | `f/shiraou/shiraou_notification` |
| 概要 | 白皇集落営農 変更通知 |
| ステップ数 | 1単一Pythonスクリプト |
| スケジュール | `0 */5 * * * *`5分毎、JST |
| スケジュールパス | `f/shiraou/shiraou_notification_every_5min` |
### 実行フロー(擬似コード)
```python
# Step 1: シークレット・前回実行時刻を取得
api_key = get_variable("u/admin/NOTIFICATION_API_KEY")
line_token = get_variable("u/admin/LINE_CHANNEL_ACCESS_TOKEN")
line_to = get_variable("u/admin/LINE_TO")
last_checked = get_variable("u/admin/SHIRAOU_LAST_CHECKED_AT") # 空なら初回
since = last_checked or (now() - 10)
# Step 2: 変更履歴を取得
response = GET "https://shiraou.keinafarm.net/reservations/api/changes/?since={since}"
headers: { "X-API-Key": api_key }
# Step 3: 変更があればLINE通知
if response.reservations or response.usages:
message = format_message(response)
POST "https://api.line.me/v2/bot/message/push"
body: { "to": line_to, "messages": [{"type": "text", "text": message}] }
# Step 4: 前回実行時刻を更新(正常完了時のみ)
set_variable("u/admin/SHIRAOU_LAST_CHECKED_AT", response.checked_at)
```
### エラー時の挙動
- API呼び出し失敗、LINE送信失敗のいずれでも例外が発生
- 例外が発生した場合、`SHIRAOU_LAST_CHECKED_AT` は更新されない
- 次回実行時に同じ `since` で再試行される(変更の取り漏れ防止)
---
## 3. 変更履歴取得API仕様
### エンドポイント
```
GET https://shiraou.keinafarm.net/reservations/api/changes/
```
### 認証
```
X-API-Key: <NOTIFICATION_API_KEY>
```
APIキーが不正な場合は `401 Unauthorized` が返る。
### クエリパラメータ
| パラメータ | 型 | 必須 | 説明 |
|-----------|-----|------|------|
| `since` | ISO8601文字列 | 必須 | この日時以降の変更を取得する |
**`since` の形式例**:
- `2026-02-21T10:00:00+09:00`(タイムゾーン付き、推奨)
- `2026-02-21T10:00:00`ナイーブ、JSTとして扱われる
### レスポンス200 OK
```json
{
"checked_at": "2026-02-21T12:00:00+09:00",
"since": "2026-02-21T10:00:00+09:00",
"reservations": [
{
"operation": "create",
"reservation_id": 123,
"user_name": "田中太郎",
"machine_name": "トラクター",
"start_at": "2026-02-25T09:00:00+09:00",
"end_at": "2026-02-25T12:00:00+09:00",
"operated_at": "2026-02-21T11:30:00+09:00",
"operator_name": "田中太郎",
"reason": ""
}
],
"usages": [
{
"operation": "update",
"usage_id": 456,
"user_name": "山田次郎",
"machine_name": "コンバイン",
"amount": 4.0,
"unit": "時間",
"start_at": "2026-02-20T08:00:00+09:00",
"end_at": "2026-02-20T12:00:00+09:00",
"operated_at": "2026-02-21T11:55:00+09:00",
"operator_name": "管理者A",
"reason": "記録ミスのため修正"
}
]
}
```
### 変更なし時のレスポンス
```json
{
"checked_at": "2026-02-21T12:05:00+09:00",
"since": "2026-02-21T12:00:00+09:00",
"reservations": [],
"usages": []
}
```
### エラーレスポンス
| ステータス | 原因 |
|-----------|------|
| `401 Unauthorized` | APIキーが不正または未設定 |
| `400 Bad Request` | `since` パラメータが欠落または不正な日時形式 |
---
## 4. LINE通知仕様
### 使用API
LINE Messaging API - Push Message
```
POST https://api.line.me/v2/bot/message/push
Authorization: Bearer <LINE_CHANNEL_ACCESS_TOKEN>
Content-Type: application/json
```
### リクエストボディ
```json
{
"to": "<LINE_TO>",
"messages": [
{
"type": "text",
"text": "<フォーマット済みメッセージ>"
}
]
}
```
### メッセージフォーマット
```
📋 営農システム 変更通知
🟢 予約作成
機械: トラクター
利用者: 田中太郎
日時: 2026-02-25 09:00 〜 2026-02-25 12:00
🔴 予約キャンセル
機械: 田植機
利用者: 佐藤花子
日時: 2026-02-22 08:00 〜 2026-02-22 17:00
🔵 実績修正
機械: コンバイン
利用者: 山田次郎
利用量: 4.0時間
日: 2026-02-20
理由: 記録ミスのため修正
```
### アイコン規則
| アイコン | 意味 |
|---------|------|
| 🟢 | 作成create / 予約作成 / 実績登録) |
| 🔵 | 変更update / 予約変更 / 実績修正) |
| 🔴 | 削除・キャンセルcancel / delete |
### 通知先の種別
`LINE_TO` にはユーザーIDまたはグループIDを設定する。
| 種別 | ID形式 |
|------|-------|
| ユーザー | `U` で始まる文字列 |
| グループ | `C` で始まる文字列 |
---
## 5. Windmill設定仕様
### Windmill Variablesシークレット
以下の変数を Windmill UIVariables ページ)で作成・管理する。
| 変数パス | Secret | 説明 | 取得元 |
|---------|--------|------|-------|
| `u/admin/NOTIFICATION_API_KEY` | ✅ | shiraou.keinafarm.net のAPIキー | Djangoサーバー側 `NOTIFICATION_API_KEY` 環境変数と同一値 |
| `u/admin/LINE_CHANNEL_ACCESS_TOKEN` | ✅ | LINE Messaging API チャネルアクセストークン | LINE Developers Console |
| `u/admin/LINE_TO` | ✅ | 通知先のLINEユーザーIDまたはグループID | LINE webhook / profile API |
| `u/admin/SHIRAOU_LAST_CHECKED_AT` | ❌ | 前回確認時刻(ワークフローが自動更新) | ワークフローが自動管理(初期値: 空文字) |
### Django側の設定shiraou.keinafarm.net
`docker-compose.yml` に以下の環境変数を追加:
```yaml
environment:
- NOTIFICATION_API_KEY=<NOTIFICATION_API_KEYと同一の値>
```
APIキー生成コマンド:
```bash
openssl rand -hex 32
```
---
## 6. 状態管理仕様
### 状態変数: `SHIRAOU_LAST_CHECKED_AT`
| 項目 | 内容 |
|------|------|
| 格納場所 | Windmill Variable `u/admin/SHIRAOU_LAST_CHECKED_AT` |
| 型 | ISO8601文字列例: `2026-02-21T15:30:00+09:00` |
| 初期値 | 空文字(初回実行時は `現在時刻 - 10分` を使用) |
| 更新タイミング | フロー正常完了時のみ、APIレスポンスの `checked_at` を保存 |
| 参照タイミング | フロー実行開始時、`since` パラメータとして使用 |
### 重複通知防止の仕組み
```
実行1: since=T0, checked_at=T1 → LAST_CHECKED_AT = T1
実行2: since=T1, checked_at=T2 → T1以降の変更のみ取得
```
- `since``checked_at`APIが確認した時刻を使うことで、変更の取りこぼしが発生しない
- `since`(リクエストに渡した時刻)ではなく `checked_at`(サーバーが確認した時刻)を保存するのがポイント
### 旧実装との違い(トラブルシュート記録)
| | 旧実装 | 現実装 |
|---|--------|--------|
| 状態保存方法 | `wmill.get_state()` / `set_state()` | `wmill.get_variable()` / `set_variable()` |
| 問題 | フローのインラインスクリプトでは実行をまたいで保存されない | - |
| 症状 | 毎回 `since = 現在 - 10分` になり、毎回通知が飛ぶ | 正常動作 |
---
## 7. 設計判断と制約
### 絶対に変えてはいけない制約
1. **`SHIRAOU_LAST_CHECKED_AT` には `checked_at` を保存すること**`since` を保存しない)
- `checked_at`: APIサーバーが「この時刻まで確認した」という保証付きの時刻
- 同じ変更が2度通知されることを防ぐ
2. **状態更新は正常完了後のみ行うこと**
- API呼び出し失敗・LINE送信失敗時は `SHIRAOU_LAST_CHECKED_AT` を更新しない
- 次回実行で同じ範囲を再取得し、通知の取り漏れを防ぐ
3. **`wmill.get_state()` は使用しないこと**
- Windmillのインラインフロースクリプトでは実行をまたいで保存されない
- 状態管理は必ず Windmill Variable を使うこと
### 設計判断
| 判断 | 理由 |
|------|------|
| 単一ステップフロー | 状態管理を1か所に集約するため。`get_state()`/`set_state()` のスコープ問題を回避 |
| SSL検証スキップ | shiraou.keinafarm.net が自己署名証明書の可能性があるため |
| タイムアウト 30秒 | 農業用途で多少の応答遅延を許容しつつ、無限待機を防ぐ |
| 5分ポーリング間隔 | 農業機械の予約用途では数分の遅延は許容範囲。リアルタイム不要 |
---
## 8. 運用手順
### フローを手動実行
```bash
curl -sk -X POST \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
-H "Content-Type: application/json" \
-d '{}' \
"https://windmill.keinafarm.net/api/w/admins/jobs/run/f/f/shiraou/shiraou_notification"
```
### 動作確認curlで直接API呼び出し
```bash
# 変更なし確認
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-02-21T11:59:00%2B09:00"
# 広い範囲で変更取得(初期確認用)
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00"
```
### フローの更新デプロイ手順
```bash
cd /path/to/windmill_workflow
# 1. flows/shiraou_notification.flow.json を編集
# 2. 既存フローを削除して再作成PUTは405のため
curl -sk -X DELETE \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
"https://windmill.keinafarm.net/api/w/admins/flows/delete/f/shiraou/shiraou_notification"
curl -sk -X POST \
-H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
-H "Content-Type: application/json" \
-d @flows/shiraou_notification.flow.json \
"https://windmill.keinafarm.net/api/w/admins/flows/create"
# 3. スケジュールは再作成不要(フローの削除・再作成でも維持される)
```
### APIキーローテーション手順
1. Djangoサーバー側で新しいキーを生成: `openssl rand -hex 32`
2. `docker-compose.yml``NOTIFICATION_API_KEY` を更新してデプロイ
3. Windmill UI で `u/admin/NOTIFICATION_API_KEY` の値を同じ新しいキーに更新
4. フローを手動実行して動作確認
### 過去のジョブ結果確認
```bash
curl -sk -H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \
"https://windmill.keinafarm.net/api/w/admins/jobs/list?per_page=10&script_path_exact=f/shiraou/shiraou_notification&is_flow=true"
```
---
## 9. ソースファイル索引
### フロー定義
| ファイル | 説明 |
|---------|------|
| [flows/shiraou_notification.flow.json](../../flows/shiraou_notification.flow.json) | フロー本体。単一Pythonステップでポーリング・通知・状態更新を実行 |
**フロー構造**:
```json
{
"path": "f/shiraou/shiraou_notification",
"value": {
"modules": [
{
"id": "a",
"value": {
"type": "rawscript",
"language": "python3",
"content": "..."
}
}
]
}
}
```
### ヘルパースクリプト
| ファイル | 説明 |
|---------|------|
| [wm-api.sh](../../wm-api.sh) | Windmill REST API操作ヘルパー。フロー作成・スケジュール管理に使用 |
**主要コマンド**:
```
create-flow <file> JSONファイルからフローを作成
create-schedule <file> JSONファイルからスケジュールを作成
flows フロー一覧取得
schedules スケジュール一覧取得
```
### ドキュメント
| ファイル | 説明 |
|---------|------|
| [docs/shiraou/19_windmill_通知ワークフロー連携仕様.md](19_windmill_通知ワークフロー連携仕様.md) | 仕様書。API仕様・メッセージフォーマットの原典 |
| [docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md](20_マスタードキュメント_Windmill通知ワークフロー編.md) | 本ドキュメント |
### エージェントワークフロー
| ファイル | 説明 |
|---------|------|
| [.agent/workflows/windmill-push.md](../../.agent/workflows/windmill-push.md) | Windmillへのpush手順。wmill CLIの制限とAPI代替の経緯を記録 |
| [.agent/workflows/windmill-new-script.md](../../.agent/workflows/windmill-new-script.md) | 新規スクリプト作成手順 |
---
## 更新履歴
| 日付 | 変更内容 |
|------|---------|
| 2026-02-21 | 初版作成。フロー登録・スケジュール設定・状態管理バグ修正を含む |

File diff suppressed because it is too large Load Diff

View File

@@ -1,148 +1,148 @@
Alexa TTS API マスタードキュメント
最終更新: 2026-03-03 状態: サーバーからの日本語TTS未解決調査中
------------
2026/03/03 10:24 akira記録
akiraが下記の変更をしましたので、内容を読んでください。
1) 構成とサーバーへのファイル受け渡し方法を変更しました
/home/claude/windmill_workflow
に、https://gitea.keinafarm.net/akira/windmill_workflow.gitをcloneしました
これにより、
C:\Users\akira\Develop\windmill_workflow
とのやり取りはgiteaを使って出来るようになります。
2) docker compose up -dで、「 Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP を参照したまま 502/504 エラーになる」のは、原因があるはずです。(他のコンテナで、問題になっていないので)
調査して、Traefik 再起動が不必要になるようにしたいです
------------
概要
Windmill から家中の Echo デバイスに任意のテキストを読み上げさせる API サーバー。Node.js + Express で実装し、Docker コンテナとして Windmill サーバー上で稼働する。
⚠ 現在の問題: ローカル PC からは日本語TTS動作確認済み。しかしサーバーkeinafarm.netのコンテナからリクエストすると日本語文字が Amazon 側で除去されて発話されない。原因は Amazon の IP ベースフィルタリング海外IPからは日本語 textToSpeak を無視する模様)。調査継続中。
ファイル構成
ファイル 場所 役割 備考
server.js alexa-api/(リポジトリ) Express API サーバー本体 本番コード。変更したらビルド・再デプロイが必要
Dockerfile alexa-api/(リポジトリ) Docker イメージ定義 node:20-alpine ベース。server.js と package*.json をコピー
docker-compose.yml alexa-api/(リポジトリ) コンテナ起動設定 windmill_windmill-internal ネットワーク接続。外部ポート非公開
package.json / package-lock.json alexa-api/(リポジトリ) npm 依存関係 本番: express のみ。devDeps に alexa-remote2不使用
.env.example alexa-api/(リポジトリ) 環境変数テンプレート ALEXA_COOKIE=xxx の形式
.env alexa-api/(リポジトリ、.gitignore 対象) 実際の Cookie 保管 Git にコミットしない。ローカル作業後に scp でサーバーへ転送
auth4.js alexa-api/(リポジトリ) Amazon 認証・Cookie 取得スクリプト ローカルのみで実行Windows PC
auth.js / auth2.js / auth3.js alexa-api/(リポジトリ) auth4.js の旧バージョン 参考用。実際は auth4.js を使う
test_tts.js alexa-api/(リポジトリ) ローカルテスト用スクリプト 直接 alexa.amazon.co.jp を叩いて動作確認
サーバー上のファイル場所: /home/claude/alexa-api/git リポジトリとは別にコピーして管理)
サーバーへのデプロイ手順
server.js や Dockerfile、package.json を変更した場合は以下の手順でサーバーに反映する。
Step 1: ローカルでファイルを編集
リポジトリc:\Users\akira\Develop\windmill_workflow\alexa-api\)でファイルを編集する。
Step 2: scp でサーバーに転送
変更したファイルをサーバーに scp で転送する:
# server.js を変更した場合 scp alexa-api/server.js keinafarm-claude:/home/claude/alexa-api/server.js # Dockerfile や package.json を変更した場合 scp alexa-api/Dockerfile keinafarm-claude:/home/claude/alexa-api/Dockerfile scp alexa-api/package.json keinafarm-claude:/home/claude/alexa-api/package.json scp alexa-api/package-lock.json keinafarm-claude:/home/claude/alexa-api/package-lock.json # .env を更新した場合Cookie 更新時など) scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env
Step 3: サーバーでビルドして再起動
⚠ 重要: docker compose restart はイメージをリビルドしない。server.js 等を変更した場合は必ず build + up -d を実行すること。
# SSH でサーバーに接続してビルド+起動 ssh keinafarm-claude cd /home/claude/alexa-api # イメージをビルドserver.js 等の変更を反映) sudo docker compose build # コンテナを再作成して起動 sudo docker compose up -d # Traefik を再起動(コンテナ再作成後は必須) sudo docker restart traefik
Step 4: 動作確認
# ヘルスチェックWindmill ワーカーコンテナ内から) curl http://alexa_api:3500/health # ログ確認 sudo docker logs alexa_api -f # デバイス一覧確認 curl http://alexa_api:3500/devices
Cookie だけ更新する場合server.js 変更なし)
# .env をサーバーに転送 scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env # コンテナを再起動restart で OK → イメージのリビルド不要) ssh keinafarm-claude 'sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart' # ※ Traefik の再起動は不要(コンテナ再作成しないため)
Traefik 再起動が必要な理由
docker compose up -d はコンテナを「再作成」するdocker compose restart は既存コンテナを再起動するだけ)。コンテナが再作成されると Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP を参照したまま 502/504 エラーになる。
対処: sudo docker restart traefik で Traefik に新しい IP を再検出させる。
この問題は Traefik の設定で watch: true にすれば自動解消できるが、現状はコンテナ再作成のたびに手動で Traefik を再起動する運用としている。
docker-compose.yml の内容
services: alexa-api: build: . container_name: alexa_api restart: unless-stopped env_file: - .env environment: - PORT=3500 networks: - windmill_windmill-internal # 外部には公開しないWindmill ワーカーから内部ネットワーク経由でのみアクセス) # デバッグ時は以下のコメントを外す: # ports: # - "127.0.0.1:3500:3500" networks: windmill_windmill-internal: external: true
認証方法auth4.js
Amazon Japan OpenID フローを自前で実装。ローカル PCWindowsでのみ実行する
# ローカルPC の alexa-api ディレクトリで実行 cd alexa-api AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth4.js
成功すると alexa-api/.env が生成または更新される。
ログインフローの概要:
GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jp
hidden フィールドanti-csrftoken-a2z, appActionToken, workflowState 等)を抽出
POST でメール/パスワードを送信
alexa.amazon.co.jp/api/apps/v1/token へのリダイレクトをたどる
取得した Cookieat-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を .env に保存
TTS の仕組みserver.js
alexa-remote2 は使わない直接 API 実装。Endpoints:
POST /speak — { device: "デバイス名 or serial", text: "しゃべる内容" }
GET /devices — デバイス一覧
GET /health — ヘルスチェック
内部の API 呼び出し順序:
GET /api/language → Set-Cookie: csrf=XXXXX を取得(毎リクエストごと)
GET /api/bootstrap → customerId を取得(キャッシュ: 永続A1AE8HXD8IJ61L
GET /api/devices-v2/device → デバイス一覧5分キャッシュ
POST /api/behaviors/preview にシーケンス JSON を送信
POST /api/behaviors/preview のボディ構造:
{ behaviorId: "PREVIEW", sequenceJson: JSON.stringify({ "@type": "com.amazon.alexa.behaviors.model.Sequence", startNode: { "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", type: "Alexa.Speak", operationPayload: { deviceType: "...", deviceSerialNumber: "...", customerId: "A1AE8HXD8IJ61L", locale: "ja-JP", // ← 重要(下記参照) textToSpeak: "発話内容" } } }), status: "ENABLED" }
ヘッダーに csrf: XXXXX と Cookie に csrf=XXXXX の両方が必要。Content-Length は Buffer.byteLength で計算(マルチバイト文字対応)。
⚠ locale パラメータについて(重要・未解決)
locale 値 ローカル PC から サーバーkeinafarm.netから
""(空文字) ✅ 日本語・英語・漢字全て発話 ❌ 英語TTSになり日本語部分が発話されない
"ja-JP" ❌ 一瞬音が出るだけ(失敗) ❌ 日本語文字が Amazon 側で除去され英字のみ発話
現在 server.js では locale: "ja-JP" に設定している。
仮説: Amazon が海外IPkeinafarm.net = 非日本IPからのリクエストを IP ベースでフィルタリングし、textToSpeak の日本語文字を除去している。Alexa.TextCommand は同じ問題がない(異なる API パス)。
確認済み事実: alexa_api の server.js ログには日本語テキストが正しく届いている。除去は Amazon サーバー側で発生。
次の調査候補:
SSML の <lang xml:lang="ja-JP"> タグで強制的に日本語 TTS を指定できるか
Alexa.TextCommand で「次を読み上げて:{text}」形式が使えるか
ローカルブリッジ方式(ユーザーのローカル PC で小さなプロキシサーバーを動かし、クラウドサーバーからローカル経由で alexa.amazon.co.jp を叩く)
デバイス一覧Echo デバイスのみ)
名前 deviceType serialNumber
プレハブ A4ZXE0RM7LQ7A G0922H085165007R
リビングエコー1 ASQZWP4GPYUT7 G8M2DB08522600RL
リビングエコー2 ASQZWP4GPYUT7 G8M2DB08522503WF
オフィスの右エコー A4ZXE0RM7LQ7A G0922H08525302K5
オフィスの左エコー A4ZXE0RM7LQ7A G0922H08525302J9
寝室のエコー ASQZWP4GPYUT7 G8M2HN08534302XH
Windmill スクリプトu/admin/alexa_speak
export async function main(device: string, text: string) { const res = await fetch("http://alexa_api:3500/speak", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device, text }), }); if (!res.ok) throw new Error("alexa-api error " + res.status); return res.json(); }
device はデバイス名日本語またはシリアル番号で指定可能。Windmill ワーカーから http://alexa_api:3500 でアクセスwindmill_windmill-internal ネットワーク経由)。
Cookie の更新手順
Cookie は数日〜数週間で期限切れ。切れたら:
# 1. ローカル PC で Cookie を取得 cd alexa-api AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth4.js # → alexa-api/.env が更新される # 2. サーバーに .env を転送 scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env # 3. コンテナを再起動restart で OK、リビルド不要 ssh keinafarm-claude 'sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart' # ※ Traefik 再起動は不要(コンテナ再作成なし)
既知の問題・落とし穴
docker compose restart ≠ リビルド: server.js を変更しても restart ではコンテナ内のコードは古いまま。build + up -d が必要。
コンテナ再作成後は Traefik 再起動必須: up -d でコンテナ再作成すると Docker 内部 IP が変わり Traefik が 502/504 を返す。sudo docker restart traefik で解消。
alexa-remote2 は使えない: 取得した Cookie 文字列を受け付けない(内部で再認証しようとして失敗)。直接 API 実装が必要。
CSRF トークンは Cookie と ヘッダーの両方に必要: csrf ヘッダーだけ、または Cookie だけでは認証失敗。
operationPayload に customerId 必須: ないと 400 エラー。
レート制限: 短時間に連続リクエストすると HTTP 429 または 200 で音が出ない。通常の通知用途では問題なし。
git push がブロックされる: Gitea の pre-receive フックremote: Gitea: User permission denied for writingで push が失敗する。根本原因は未調査。ファイル転送は scp で行っている。
firstRunCompleted: false はデバイス設定の未完了フラグ: TTS には直接影響しないroot cause ではなかった)。
サーバー上の運用コマンド一覧
# コンテナ状態確認 sudo docker ps | grep alexa # リアルタイムログ確認 sudo docker logs alexa_api -f # コンテナ停止 sudo docker compose -f /home/claude/alexa-api/docker-compose.yml stop # ビルド+起動(コード変更後) cd /home/claude/alexa-api sudo docker compose build sudo docker compose up -d sudo docker restart traefik # Cookie 更新時(再起動のみ) sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart
Alexa TTS API マスタードキュメント
最終更新: 2026-03-03 状態: サーバーからの日本語TTS未解決調査中
------------
2026/03/03 10:24 akira記録
akiraが下記の変更をしましたので、内容を読んでください。
1) 構成とサーバーへのファイル受け渡し方法を変更しました
/home/claude/windmill_workflow
に、https://gitea.keinafarm.net/akira/windmill_workflow.gitをcloneしました
これにより、
C:\Users\akira\Develop\windmill_workflow
とのやり取りはgiteaを使って出来るようになります。
2) docker compose up -dで、「 Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP を参照したまま 502/504 エラーになる」のは、原因があるはずです。(他のコンテナで、問題になっていないので)
調査して、Traefik 再起動が不必要になるようにしたいです
------------
概要
Windmill から家中の Echo デバイスに任意のテキストを読み上げさせる API サーバー。Node.js + Express で実装し、Docker コンテナとして Windmill サーバー上で稼働する。
⚠ 現在の問題: ローカル PC からは日本語TTS動作確認済み。しかしサーバーkeinafarm.netのコンテナからリクエストすると日本語文字が Amazon 側で除去されて発話されない。原因は Amazon の IP ベースフィルタリング海外IPからは日本語 textToSpeak を無視する模様)。調査継続中。
ファイル構成
ファイル 場所 役割 備考
server.js alexa-api/(リポジトリ) Express API サーバー本体 本番コード。変更したらビルド・再デプロイが必要
Dockerfile alexa-api/(リポジトリ) Docker イメージ定義 node:20-alpine ベース。server.js と package*.json をコピー
docker-compose.yml alexa-api/(リポジトリ) コンテナ起動設定 windmill_windmill-internal ネットワーク接続。外部ポート非公開
package.json / package-lock.json alexa-api/(リポジトリ) npm 依存関係 本番: express のみ。devDeps に alexa-remote2不使用
.env.example alexa-api/(リポジトリ) 環境変数テンプレート ALEXA_COOKIE=xxx の形式
.env alexa-api/(リポジトリ、.gitignore 対象) 実際の Cookie 保管 Git にコミットしない。ローカル作業後に scp でサーバーへ転送
auth4.js alexa-api/(リポジトリ) Amazon 認証・Cookie 取得スクリプト ローカルのみで実行Windows PC
auth.js / auth2.js / auth3.js alexa-api/(リポジトリ) auth4.js の旧バージョン 参考用。実際は auth4.js を使う
test_tts.js alexa-api/(リポジトリ) ローカルテスト用スクリプト 直接 alexa.amazon.co.jp を叩いて動作確認
サーバー上のファイル場所: /home/claude/alexa-api/git リポジトリとは別にコピーして管理)
サーバーへのデプロイ手順
server.js や Dockerfile、package.json を変更した場合は以下の手順でサーバーに反映する。
Step 1: ローカルでファイルを編集
リポジトリc:\Users\akira\Develop\windmill_workflow\alexa-api\)でファイルを編集する。
Step 2: scp でサーバーに転送
変更したファイルをサーバーに scp で転送する:
# server.js を変更した場合 scp alexa-api/server.js keinafarm-claude:/home/claude/alexa-api/server.js # Dockerfile や package.json を変更した場合 scp alexa-api/Dockerfile keinafarm-claude:/home/claude/alexa-api/Dockerfile scp alexa-api/package.json keinafarm-claude:/home/claude/alexa-api/package.json scp alexa-api/package-lock.json keinafarm-claude:/home/claude/alexa-api/package-lock.json # .env を更新した場合Cookie 更新時など) scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env
Step 3: サーバーでビルドして再起動
⚠ 重要: docker compose restart はイメージをリビルドしない。server.js 等を変更した場合は必ず build + up -d を実行すること。
# SSH でサーバーに接続してビルド+起動 ssh keinafarm-claude cd /home/claude/alexa-api # イメージをビルドserver.js 等の変更を反映) sudo docker compose build # コンテナを再作成して起動 sudo docker compose up -d # Traefik を再起動(コンテナ再作成後は必須) sudo docker restart traefik
Step 4: 動作確認
# ヘルスチェックWindmill ワーカーコンテナ内から) curl http://alexa_api:3500/health # ログ確認 sudo docker logs alexa_api -f # デバイス一覧確認 curl http://alexa_api:3500/devices
Cookie だけ更新する場合server.js 変更なし)
# .env をサーバーに転送 scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env # コンテナを再起動restart で OK → イメージのリビルド不要) ssh keinafarm-claude 'sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart' # ※ Traefik の再起動は不要(コンテナ再作成しないため)
Traefik 再起動が必要な理由
docker compose up -d はコンテナを「再作成」するdocker compose restart は既存コンテナを再起動するだけ)。コンテナが再作成されると Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP を参照したまま 502/504 エラーになる。
対処: sudo docker restart traefik で Traefik に新しい IP を再検出させる。
この問題は Traefik の設定で watch: true にすれば自動解消できるが、現状はコンテナ再作成のたびに手動で Traefik を再起動する運用としている。
docker-compose.yml の内容
services: alexa-api: build: . container_name: alexa_api restart: unless-stopped env_file: - .env environment: - PORT=3500 networks: - windmill_windmill-internal # 外部には公開しないWindmill ワーカーから内部ネットワーク経由でのみアクセス) # デバッグ時は以下のコメントを外す: # ports: # - "127.0.0.1:3500:3500" networks: windmill_windmill-internal: external: true
認証方法auth4.js
Amazon Japan OpenID フローを自前で実装。ローカル PCWindowsでのみ実行する
# ローカルPC の alexa-api ディレクトリで実行 cd alexa-api AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth4.js
成功すると alexa-api/.env が生成または更新される。
ログインフローの概要:
GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jp
hidden フィールドanti-csrftoken-a2z, appActionToken, workflowState 等)を抽出
POST でメール/パスワードを送信
alexa.amazon.co.jp/api/apps/v1/token へのリダイレクトをたどる
取得した Cookieat-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を .env に保存
TTS の仕組みserver.js
alexa-remote2 は使わない直接 API 実装。Endpoints:
POST /speak — { device: "デバイス名 or serial", text: "しゃべる内容" }
GET /devices — デバイス一覧
GET /health — ヘルスチェック
内部の API 呼び出し順序:
GET /api/language → Set-Cookie: csrf=XXXXX を取得(毎リクエストごと)
GET /api/bootstrap → customerId を取得(キャッシュ: 永続A1AE8HXD8IJ61L
GET /api/devices-v2/device → デバイス一覧5分キャッシュ
POST /api/behaviors/preview にシーケンス JSON を送信
POST /api/behaviors/preview のボディ構造:
{ behaviorId: "PREVIEW", sequenceJson: JSON.stringify({ "@type": "com.amazon.alexa.behaviors.model.Sequence", startNode: { "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", type: "Alexa.Speak", operationPayload: { deviceType: "...", deviceSerialNumber: "...", customerId: "A1AE8HXD8IJ61L", locale: "ja-JP", // ← 重要(下記参照) textToSpeak: "発話内容" } } }), status: "ENABLED" }
ヘッダーに csrf: XXXXX と Cookie に csrf=XXXXX の両方が必要。Content-Length は Buffer.byteLength で計算(マルチバイト文字対応)。
⚠ locale パラメータについて(重要・未解決)
locale 値 ローカル PC から サーバーkeinafarm.netから
""(空文字) ✅ 日本語・英語・漢字全て発話 ❌ 英語TTSになり日本語部分が発話されない
"ja-JP" ❌ 一瞬音が出るだけ(失敗) ❌ 日本語文字が Amazon 側で除去され英字のみ発話
現在 server.js では locale: "ja-JP" に設定している。
仮説: Amazon が海外IPkeinafarm.net = 非日本IPからのリクエストを IP ベースでフィルタリングし、textToSpeak の日本語文字を除去している。Alexa.TextCommand は同じ問題がない(異なる API パス)。
確認済み事実: alexa_api の server.js ログには日本語テキストが正しく届いている。除去は Amazon サーバー側で発生。
次の調査候補:
SSML の <lang xml:lang="ja-JP"> タグで強制的に日本語 TTS を指定できるか
Alexa.TextCommand で「次を読み上げて:{text}」形式が使えるか
ローカルブリッジ方式(ユーザーのローカル PC で小さなプロキシサーバーを動かし、クラウドサーバーからローカル経由で alexa.amazon.co.jp を叩く)
デバイス一覧Echo デバイスのみ)
名前 deviceType serialNumber
プレハブ A4ZXE0RM7LQ7A G0922H085165007R
リビングエコー1 ASQZWP4GPYUT7 G8M2DB08522600RL
リビングエコー2 ASQZWP4GPYUT7 G8M2DB08522503WF
オフィスの右エコー A4ZXE0RM7LQ7A G0922H08525302K5
オフィスの左エコー A4ZXE0RM7LQ7A G0922H08525302J9
寝室のエコー ASQZWP4GPYUT7 G8M2HN08534302XH
Windmill スクリプトu/admin/alexa_speak
export async function main(device: string, text: string) { const res = await fetch("http://alexa_api:3500/speak", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device, text }), }); if (!res.ok) throw new Error("alexa-api error " + res.status); return res.json(); }
device はデバイス名日本語またはシリアル番号で指定可能。Windmill ワーカーから http://alexa_api:3500 でアクセスwindmill_windmill-internal ネットワーク経由)。
Cookie の更新手順
Cookie は数日〜数週間で期限切れ。切れたら:
# 1. ローカル PC で Cookie を取得 cd alexa-api AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth4.js # → alexa-api/.env が更新される # 2. サーバーに .env を転送 scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env # 3. コンテナを再起動restart で OK、リビルド不要 ssh keinafarm-claude 'sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart' # ※ Traefik 再起動は不要(コンテナ再作成なし)
既知の問題・落とし穴
docker compose restart ≠ リビルド: server.js を変更しても restart ではコンテナ内のコードは古いまま。build + up -d が必要。
コンテナ再作成後は Traefik 再起動必須: up -d でコンテナ再作成すると Docker 内部 IP が変わり Traefik が 502/504 を返す。sudo docker restart traefik で解消。
alexa-remote2 は使えない: 取得した Cookie 文字列を受け付けない(内部で再認証しようとして失敗)。直接 API 実装が必要。
CSRF トークンは Cookie と ヘッダーの両方に必要: csrf ヘッダーだけ、または Cookie だけでは認証失敗。
operationPayload に customerId 必須: ないと 400 エラー。
レート制限: 短時間に連続リクエストすると HTTP 429 または 200 で音が出ない。通常の通知用途では問題なし。
git push がブロックされる: Gitea の pre-receive フックremote: Gitea: User permission denied for writingで push が失敗する。根本原因は未調査。ファイル転送は scp で行っている。
firstRunCompleted: false はデバイス設定の未完了フラグ: TTS には直接影響しないroot cause ではなかった)。
サーバー上の運用コマンド一覧
# コンテナ状態確認 sudo docker ps | grep alexa # リアルタイムログ確認 sudo docker logs alexa_api -f # コンテナ停止 sudo docker compose -f /home/claude/alexa-api/docker-compose.yml stop # ビルド+起動(コード変更後) cd /home/claude/alexa-api sudo docker compose build sudo docker compose up -d sudo docker restart traefik # Cookie 更新時(再起動のみ) sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart

File diff suppressed because it is too large Load Diff

View File

@@ -1,105 +1,105 @@
# 引き継ぎ - `u/admin/alexa_speak` API反映後にUIドロップダウンが変わらない件
> **作成日**: 2026-03-04
> **対象**: `windmill.keinafarm.net` / workspace `admins`
> **対象スクリプト**: `u/admin/alexa_speak`
> **目的**: 別端末から同じ事象に遭遇しても、原因切り分けと復旧をすぐ実施できるようにする
---
## 1. 事象の概要
`u/admin/alexa_speak` を Windmill API`create-script`)で更新した直後、
- Scriptタブ上のコードは更新済み
- `schema` 上も `device``dynselect-device`
- しかし Inputフォームは `Device` がテキスト入力のまま(ドロップダウンにならない)
という状態になった。
---
## 2. 当日の時系列(要点)
1. 既存スクリプトを取得し、ローカル `scripts/alexa_speak.ts`Dynamic Select実装ありをAPIで反映
2. サーバー再取得で `content` 一致を確認(更新自体は成功)
3. UIを開くと `Device` が入力欄のままで、ドロップダウン化されていない
4. `schema.device``format: dynselect-device`, `originalType: DynSelect_device` に更新して再反映
5. それでも UI は直ちには変わらず
6. Windmill UIで `Edit` に入り、`Deploy` を1回実施
7. 直後にドロップダウン表示へ反映
---
## 3. 確認できた事実
- API反映は成功しているhash更新
- 中間: `a6010687183a199d`
- 最終: `318d78f45a084e32`
- 最終状態では以下がAPIで確認済み
- `schema.properties.device.format = "dynselect-device"`
- `schema.properties.device.originalType = "DynSelect_device"`
- UI反映は API反映だけでは即時にならず、`Edit -> Deploy` 後に反映された
---
## 4. 想定される原因
Windmill CE 側で、API経由更新時にフォームUIメタ情報入力ウィジェット解決の再計算または再適用が即時反映されないケースがある。
実務上は「API更新後にUIで1回Deploy」が回避策として有効。
---
## 5. 再現時の標準対応手順Runbook
### 5.1 APIでスクリプト更新
```bash
cd /home/akira/develop/windmill_workflow
./wm-api.sh get-script u/admin/alexa_speak > /tmp/remote_alexa_speak.json
# parent_hash を含む payload を作成して create-script
./wm-api.sh create-script /tmp/alexa_speak_push.json
```
### 5.2 APIで反映確認
```bash
./wm-api.sh get-script u/admin/alexa_speak
```
確認ポイント:
- `hash` が更新されている
- `content` が想定コードになっている
- `schema.properties.device.format``dynselect-device`
- `schema.properties.device.originalType``DynSelect_device`
### 5.3 UI反映されない場合
1. `u/admin/alexa_speak` を最新リビジョンで開く
2. ハードリロード(`Ctrl + Shift + R`
3. 変化がなければ `Edit -> Deploy` を1回実施
4. Inputフォームの `Device` がドロップダウン化されたことを確認
---
## 6. 補足(今回の最終状態)
- スクリプト: `u/admin/alexa_speak`
- 期待UI:
- `Device`: ドロップダウンdynselect
- `Text`: テキスト入力
- 前提:
- `alexa_api` コンテナが稼働
- `http://alexa_api:3500/devices` が取得可能
---
## 7. 引き継ぎメモ
- 「API反映成功」と「UIフォーム反映成功」は同時とは限らない
- 引き継ぎ時は、必ず以下をセットで確認する
1. APIレスポンスの `hash``schema`
2. UI表示必要なら `Edit -> Deploy`
# 引き継ぎ - `u/admin/alexa_speak` API反映後にUIドロップダウンが変わらない件
> **作成日**: 2026-03-04
> **対象**: `windmill.keinafarm.net` / workspace `admins`
> **対象スクリプト**: `u/admin/alexa_speak`
> **目的**: 別端末から同じ事象に遭遇しても、原因切り分けと復旧をすぐ実施できるようにする
---
## 1. 事象の概要
`u/admin/alexa_speak` を Windmill API`create-script`)で更新した直後、
- Scriptタブ上のコードは更新済み
- `schema` 上も `device``dynselect-device`
- しかし Inputフォームは `Device` がテキスト入力のまま(ドロップダウンにならない)
という状態になった。
---
## 2. 当日の時系列(要点)
1. 既存スクリプトを取得し、ローカル `scripts/alexa_speak.ts`Dynamic Select実装ありをAPIで反映
2. サーバー再取得で `content` 一致を確認(更新自体は成功)
3. UIを開くと `Device` が入力欄のままで、ドロップダウン化されていない
4. `schema.device``format: dynselect-device`, `originalType: DynSelect_device` に更新して再反映
5. それでも UI は直ちには変わらず
6. Windmill UIで `Edit` に入り、`Deploy` を1回実施
7. 直後にドロップダウン表示へ反映
---
## 3. 確認できた事実
- API反映は成功しているhash更新
- 中間: `a6010687183a199d`
- 最終: `318d78f45a084e32`
- 最終状態では以下がAPIで確認済み
- `schema.properties.device.format = "dynselect-device"`
- `schema.properties.device.originalType = "DynSelect_device"`
- UI反映は API反映だけでは即時にならず、`Edit -> Deploy` 後に反映された
---
## 4. 想定される原因
Windmill CE 側で、API経由更新時にフォームUIメタ情報入力ウィジェット解決の再計算または再適用が即時反映されないケースがある。
実務上は「API更新後にUIで1回Deploy」が回避策として有効。
---
## 5. 再現時の標準対応手順Runbook
### 5.1 APIでスクリプト更新
```bash
cd /home/akira/develop/windmill_workflow
./wm-api.sh get-script u/admin/alexa_speak > /tmp/remote_alexa_speak.json
# parent_hash を含む payload を作成して create-script
./wm-api.sh create-script /tmp/alexa_speak_push.json
```
### 5.2 APIで反映確認
```bash
./wm-api.sh get-script u/admin/alexa_speak
```
確認ポイント:
- `hash` が更新されている
- `content` が想定コードになっている
- `schema.properties.device.format``dynselect-device`
- `schema.properties.device.originalType``DynSelect_device`
### 5.3 UI反映されない場合
1. `u/admin/alexa_speak` を最新リビジョンで開く
2. ハードリロード(`Ctrl + Shift + R`
3. 変化がなければ `Edit -> Deploy` を1回実施
4. Inputフォームの `Device` がドロップダウン化されたことを確認
---
## 6. 補足(今回の最終状態)
- スクリプト: `u/admin/alexa_speak`
- 期待UI:
- `Device`: ドロップダウンdynselect
- `Text`: テキスト入力
- 前提:
- `alexa_api` コンテナが稼働
- `http://alexa_api:3500/devices` が取得可能
---
## 7. 引き継ぎメモ
- 「API反映成功」と「UIフォーム反映成功」は同時とは限らない
- 引き継ぎ時は、必ず以下をセットで確認する
1. APIレスポンスの `hash``schema`
2. UI表示必要なら `Edit -> Deploy`

View File

@@ -1,361 +1,361 @@
# マスタードキュメント - 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. **運用単位は「workflow packageflow + schedules」を基本**とする
5. ローカル変更はAPIでサーバーへ反映
6. サーバー側の Git Sync Workflow は定期記録用途として継続
7. ローカル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だけ、flowだけは補助用途
- 標準運用は **workflow package 単位** とする
- `flow` 本体
- その `flow path` に紐づく `schedules``is_flow=true` かつ `script_path == flow.path`
### 既存の主要オブジェクト(例)
- 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}`
### workflow package Pull標準
1. `GET /flows/get/{flow_path}` で flow 本体取得
2. `GET /schedules/list` で schedule 一覧取得
3. `script_path == flow_path` の schedule 群を抽出
4. ローカルへ一括保存flow + schedules
## 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`
### schedule反映APIworkflow package の一部)
- workflow package Push時は、対象 flow に紐づく schedule も同時同期する
- 原則:
1. サーバー現行 schedule`script_path == flow_path`)一覧取得
2. ローカル定義との差分計算(追加・更新・削除)
3. `DELETE /schedules/delete/{path}``POST /schedules/create` で収束
---
## 6. 競合時の動作仕様
### スクリプトPush競合
- `parent_hash` 不一致時は更新拒否(期待動作)
- 対応:
1. 最新をPull
2. 差分を再適用
3. 再Push
### フローPush競合
- 削除再作成前に最新を必ずPullしてローカル保存
- 競合が疑われる場合は自動実行せず手動確認にフォールバック
- **Preflight必須**: Push直前に `remote_index` の既知hashとサーバー現在hashを比較し、不一致ならPush中断fail closed
- Push後は `post-verify`再取得して期待JSON一致確認を必須化
### schedulePush競合
- `schedule.path` 重複や同名上書きに注意
- workflow package Push時は、対象 flow の schedule 群をまとめて同期し、中途半端な状態を残さない
- 失敗時は flow と schedules の両方を再取得して整合を確認
- schedule 同期も Preflight/`post-verify` の対象に含める
---
## 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: workflow package 対応(次タスク)
`wm-api.sh` へ追加:
1. `pull-workflow <flow_path>`flow + schedules 一括取得)
2. `push-workflow <workflow-dir>`flow反映 + schedules差分同期
3. `status-workflow [flow_path]`workflow単位の差分表示
4. `pull-all-workflows`flow全件を workflow package で取得)
## Phase 3: メタ情報管理
- `state/remote_index.json` を拡張し、`scripts/flows/schedules` を保持
- `state/workflow_index.json` を導入し、`workflow -> flow_hash + schedule_hashes` を保持
## Phase 4: 標準化
- `docs/flow-manage` に操作例を固定
- 「作業開始時は必ずpull」を運用ルール化
## Phase 5: 半自動化(任意)
- AI/スクリプトで
- 変更検知
- Pull提案
- Push時の競合自動リカバリ
---
## 8. 標準運用手順Runbook
### 8.1 日常の更新(推奨)
1. `pull-all` でサーバー最新を取得
2. ローカル編集
3. workflow修正時は `push-workflow` で反映flow + schedules
4. script単体修正時のみ `push-script` を使う
5. 必要に応じてローカルGitにコミット
### 8.2 サーバーで変更されたものを取り込む
1. `status-remote` 実行
2. workflow単位の変更は `status-workflow` / `pull-workflow` で取得
3. 単体変更は `pull-*` で取得
4. ローカル履歴としてコミット(任意)
### 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` がドロップダウン表示)
5. API反映後にUIが変わらない場合は `Edit -> Deploy` を1回実行して再確認
関連引き継ぎ文書:
- `docs/flow-manage/11_引き継ぎ_alexa_speak_API反映後にUIドロップダウンが変わらない件.md`
---
## 9. セキュリティ・監査方針
- APIトークンは環境変数またはローカル限定設定で管理
- リポジトリへ平文トークンをコミットしない
- 反映時刻・対象path・実行者をログ化
- サーバーGit Syncは監査証跡として維持
---
## 10. 障害前提の復旧設計(必須)
サーバー内部障害、API非冪等、通信断は防止不能とみなし、**復旧可能性を担保する**。
### 10.1 復旧の基本原則
1. Push前に対象オブジェクトをバックアップ保存必須
2. Push後に `post-verify`(再取得して期待値比較)を必須化
3. Push直前に `preflight hash check` を必須化し、不一致時はPush停止fail closed
4. 失敗時は「再実行」ではなく「現状確認 -> 復旧」の順で実施
### 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. 関連ジョブの手動実行で動作確認
### 10.4 delete -> create 運用ガード(必須)
1. `push-flow` / `push-workflow` 前に対象flowのバックアップJSONを必ず保存
2. Preflightで hash 不一致なら自動Pushしない手動レビューへフォールバック
3. Push後に flow と schedules を再取得し、ローカル期待値と一致確認
4. 不一致時は即時にバックアップから復元し、復旧ログを残す
---
## 11. Windmill依存を薄くする方針必須
Windmill API依存は避けられないため、依存点を最小化する。
### 11.1 使用APIの固定
- `scripts`: `list/get/create`
- `flows`: `list/get/create/delete`
- `schedules`: `list/get/create/delete`workflow package 同期で常用)
上記以外のAPIは原則使わない。
### 11.2 判定ロジックの優先順位
1. 実体JSON比較content/valueの比較を主判定
2. `hash` / `version_id` / `edited_at` は補助判定
3. 補助フィールド欠落時も動作継続できる実装にする
### 11.3 仕様変更検知
- 定期的に `smoke test` を実行してレスポンス形を検証
- 期待フィールド欠落時はPush停止fail closed
- 仕様差分を本ドキュメントの更新履歴に反映
---
## 12. 受け入れ条件
以下を満たせば「このプロジェクトはサーバーのワークフローを管理するためのもの」と言える状態:
1. ローカルから workflow packageflow + schedulesの Pull/Push がAPIで完結
2. サーバー更新が workflow 単位でローカル検知できる
3. 競合時の復旧手順が flow/schedule 両方で定義済み
4. 運用手順がこの文書だけで再現可能
---
## 13. 既知の注意点
1. `wm-api.sh``bash` 前提。WindowsのCRLF混入で shebang 実行失敗し得るため、`.gitattributes``*.sh text eol=lf` を固定し、実行は `bash wm-api.sh ...` を標準とする
2. フロー更新は環境により `PUT` できないため削除再作成を標準とする
3. 削除再作成は `version_id/hash/edited_at` を更新するため、Preflight hash check がないと競合上書きを見落とす可能性がある
4. 1つのflowに複数scheduleが紐づくことがあるため、`script_path` ベースで束ねて管理する
5. API応答仕様はWindmillバージョン差で微差が出るため、初回導入時は `get` のレスポンス形を確認する
---
## 14. 更新履歴
| 日付 | 変更内容 |
|------|----------|
| 2026-03-03 | 初版作成API一本化方針、同期仕様、実装計画、Runbookを定義 |
# マスタードキュメント - 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. **運用単位は「workflow packageflow + schedules」を基本**とする
5. ローカル変更はAPIでサーバーへ反映
6. サーバー側の Git Sync Workflow は定期記録用途として継続
7. ローカル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だけ、flowだけは補助用途
- 標準運用は **workflow package 単位** とする
- `flow` 本体
- その `flow path` に紐づく `schedules``is_flow=true` かつ `script_path == flow.path`
### 既存の主要オブジェクト(例)
- 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}`
### workflow package Pull標準
1. `GET /flows/get/{flow_path}` で flow 本体取得
2. `GET /schedules/list` で schedule 一覧取得
3. `script_path == flow_path` の schedule 群を抽出
4. ローカルへ一括保存flow + schedules
## 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`
### schedule反映APIworkflow package の一部)
- workflow package Push時は、対象 flow に紐づく schedule も同時同期する
- 原則:
1. サーバー現行 schedule`script_path == flow_path`)一覧取得
2. ローカル定義との差分計算(追加・更新・削除)
3. `DELETE /schedules/delete/{path}``POST /schedules/create` で収束
---
## 6. 競合時の動作仕様
### スクリプトPush競合
- `parent_hash` 不一致時は更新拒否(期待動作)
- 対応:
1. 最新をPull
2. 差分を再適用
3. 再Push
### フローPush競合
- 削除再作成前に最新を必ずPullしてローカル保存
- 競合が疑われる場合は自動実行せず手動確認にフォールバック
- **Preflight必須**: Push直前に `remote_index` の既知hashとサーバー現在hashを比較し、不一致ならPush中断fail closed
- Push後は `post-verify`再取得して期待JSON一致確認を必須化
### schedulePush競合
- `schedule.path` 重複や同名上書きに注意
- workflow package Push時は、対象 flow の schedule 群をまとめて同期し、中途半端な状態を残さない
- 失敗時は flow と schedules の両方を再取得して整合を確認
- schedule 同期も Preflight/`post-verify` の対象に含める
---
## 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: workflow package 対応(次タスク)
`wm-api.sh` へ追加:
1. `pull-workflow <flow_path>`flow + schedules 一括取得)
2. `push-workflow <workflow-dir>`flow反映 + schedules差分同期
3. `status-workflow [flow_path]`workflow単位の差分表示
4. `pull-all-workflows`flow全件を workflow package で取得)
## Phase 3: メタ情報管理
- `state/remote_index.json` を拡張し、`scripts/flows/schedules` を保持
- `state/workflow_index.json` を導入し、`workflow -> flow_hash + schedule_hashes` を保持
## Phase 4: 標準化
- `docs/flow-manage` に操作例を固定
- 「作業開始時は必ずpull」を運用ルール化
## Phase 5: 半自動化(任意)
- AI/スクリプトで
- 変更検知
- Pull提案
- Push時の競合自動リカバリ
---
## 8. 標準運用手順Runbook
### 8.1 日常の更新(推奨)
1. `pull-all` でサーバー最新を取得
2. ローカル編集
3. workflow修正時は `push-workflow` で反映flow + schedules
4. script単体修正時のみ `push-script` を使う
5. 必要に応じてローカルGitにコミット
### 8.2 サーバーで変更されたものを取り込む
1. `status-remote` 実行
2. workflow単位の変更は `status-workflow` / `pull-workflow` で取得
3. 単体変更は `pull-*` で取得
4. ローカル履歴としてコミット(任意)
### 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` がドロップダウン表示)
5. API反映後にUIが変わらない場合は `Edit -> Deploy` を1回実行して再確認
関連引き継ぎ文書:
- `docs/flow-manage/11_引き継ぎ_alexa_speak_API反映後にUIドロップダウンが変わらない件.md`
---
## 9. セキュリティ・監査方針
- APIトークンは環境変数またはローカル限定設定で管理
- リポジトリへ平文トークンをコミットしない
- 反映時刻・対象path・実行者をログ化
- サーバーGit Syncは監査証跡として維持
---
## 10. 障害前提の復旧設計(必須)
サーバー内部障害、API非冪等、通信断は防止不能とみなし、**復旧可能性を担保する**。
### 10.1 復旧の基本原則
1. Push前に対象オブジェクトをバックアップ保存必須
2. Push後に `post-verify`(再取得して期待値比較)を必須化
3. Push直前に `preflight hash check` を必須化し、不一致時はPush停止fail closed
4. 失敗時は「再実行」ではなく「現状確認 -> 復旧」の順で実施
### 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. 関連ジョブの手動実行で動作確認
### 10.4 delete -> create 運用ガード(必須)
1. `push-flow` / `push-workflow` 前に対象flowのバックアップJSONを必ず保存
2. Preflightで hash 不一致なら自動Pushしない手動レビューへフォールバック
3. Push後に flow と schedules を再取得し、ローカル期待値と一致確認
4. 不一致時は即時にバックアップから復元し、復旧ログを残す
---
## 11. Windmill依存を薄くする方針必須
Windmill API依存は避けられないため、依存点を最小化する。
### 11.1 使用APIの固定
- `scripts`: `list/get/create`
- `flows`: `list/get/create/delete`
- `schedules`: `list/get/create/delete`workflow package 同期で常用)
上記以外のAPIは原則使わない。
### 11.2 判定ロジックの優先順位
1. 実体JSON比較content/valueの比較を主判定
2. `hash` / `version_id` / `edited_at` は補助判定
3. 補助フィールド欠落時も動作継続できる実装にする
### 11.3 仕様変更検知
- 定期的に `smoke test` を実行してレスポンス形を検証
- 期待フィールド欠落時はPush停止fail closed
- 仕様差分を本ドキュメントの更新履歴に反映
---
## 12. 受け入れ条件
以下を満たせば「このプロジェクトはサーバーのワークフローを管理するためのもの」と言える状態:
1. ローカルから workflow packageflow + schedulesの Pull/Push がAPIで完結
2. サーバー更新が workflow 単位でローカル検知できる
3. 競合時の復旧手順が flow/schedule 両方で定義済み
4. 運用手順がこの文書だけで再現可能
---
## 13. 既知の注意点
1. `wm-api.sh``bash` 前提。WindowsのCRLF混入で shebang 実行失敗し得るため、`.gitattributes``*.sh text eol=lf` を固定し、実行は `bash wm-api.sh ...` を標準とする
2. フロー更新は環境により `PUT` できないため削除再作成を標準とする
3. 削除再作成は `version_id/hash/edited_at` を更新するため、Preflight hash check がないと競合上書きを見落とす可能性がある
4. 1つのflowに複数scheduleが紐づくことがあるため、`script_path` ベースで束ねて管理する
5. API応答仕様はWindmillバージョン差で微差が出るため、初回導入時は `get` のレスポンス形を確認する
---
## 14. 更新履歴
| 日付 | 変更内容 |
|------|----------|
| 2026-03-03 | 初版作成API一本化方針、同期仕様、実装計画、Runbookを定義 |
| 2026-03-03 | 障害前提の復旧設計、Windmill依存を薄くする方針を必須要件として追記 |
| 2026-03-03 | 運用単位を workflow packageflow + schedulesへ変更し、実装計画とRunbookを更新 |
| 2026-03-03 | `delete -> create` と hash 管理の運用ガードpreflight / fail closed / post-verifyおよびCRLF対策を追記 |

View File

@@ -1,309 +1,309 @@
# Windmill 通知ワークフロー連携仕様
> **作成日**: 2026-02-21
> **対象システム**: 白皇集落営農組合 統合システム (`shiraou.keinafarm.net`)
> **目的**: 予約・実績の変更をLINEで管理者に通知するWindmillワークフローの実装仕様
---
## 1. 概要
統合システム側が「変更履歴取得API」を提供する。
Windmillは定期的にこのAPIをポーリングし、変更があればLINEへ通知する。
```
Windmill定期実行
└→ GET https://shiraou.keinafarm.net/reservations/api/changes/?since=<前回実行時刻>
└→ 変更一覧(予約・実績)を取得
└→ 変更があればLINE Messaging APIへ通知
└→ 前回実行時刻を更新
```
---
## 2. 変更履歴取得API
### エンドポイント
```
GET https://shiraou.keinafarm.net/reservations/api/changes/
```
### 認証
`X-API-Key` ヘッダーにAPIキーを指定する統合システム管理者から取得
```
X-API-Key: <NOTIFICATION_API_KEY>
```
APIキーが不正な場合は `401 Unauthorized` が返る。
### クエリパラメータ
| パラメータ | 型 | 必須 | 説明 |
|-----------|-----|------|------|
| `since` | ISO8601文字列 | 必須 | この日時以降の変更を取得する |
**`since` の形式例**:
- `2026-02-21T10:00:00` ナイーブ、JSTとして扱われる
- `2026-02-21T10:00:00+09:00` (タイムゾーン付き、推奨)
### レスポンス200 OK
```json
{
"checked_at": "2026-02-21T12:00:00+09:00",
"since": "2026-02-21T10:00:00+09:00",
"reservations": [
{
"operation": "create",
"reservation_id": 123,
"user_name": "田中太郎",
"machine_name": "トラクター",
"start_at": "2026-02-25T09:00:00+09:00",
"end_at": "2026-02-25T12:00:00+09:00",
"operated_at": "2026-02-21T11:30:00+09:00",
"operator_name": "田中太郎",
"reason": ""
},
{
"operation": "cancel",
"reservation_id": 120,
"user_name": "佐藤花子",
"machine_name": "田植機",
"start_at": "2026-02-22T08:00:00+09:00",
"end_at": "2026-02-22T17:00:00+09:00",
"operated_at": "2026-02-21T11:45:00+09:00",
"operator_name": "佐藤花子",
"reason": ""
}
],
"usages": [
{
"operation": "update",
"usage_id": 456,
"user_name": "山田次郎",
"machine_name": "コンバイン",
"amount": 4.0,
"unit": "時間",
"start_at": "2026-02-20T08:00:00+09:00",
"end_at": "2026-02-20T12:00:00+09:00",
"operated_at": "2026-02-21T11:55:00+09:00",
"operator_name": "管理者A",
"reason": "記録ミスのため修正"
}
]
}
```
### operation の値一覧
**予約reservations**:
| 値 | 意味 |
|----|------|
| `create` | 予約が作成された |
| `update` | 予約の日時・機械が変更された |
| `cancel` | 予約がキャンセルされた |
**実績usages**:
| 値 | 意味 |
|----|------|
| `create` | 実績が登録された |
| `update` | 実績が修正された |
| `delete` | 実績が削除された |
### 変更なしの場合
`reservations``usages` が両方空配列になる。通知は不要。
```json
{
"checked_at": "2026-02-21T12:05:00+09:00",
"since": "2026-02-21T12:00:00+09:00",
"reservations": [],
"usages": []
}
```
### エラーレスポンス
| ステータス | 原因 |
|-----------|------|
| `401 Unauthorized` | APIキーが不正または未設定 |
| `400 Bad Request` | `since` パラメータが欠落または不正な日時形式 |
---
## 3. Windmillワークフロー設計
### 3.1 スケジュール
- **実行間隔**: 5分毎`*/5 * * * *`
- 農業機械の予約という用途上、数分の遅延は許容範囲
### 3.2 状態管理(前回実行時刻)
Windmillの **Resource** または **State Variable** に前回実行時刻を保存する。
```
変数名: last_checked_at
初期値: (初回実行時は現在時刻 - 10分 を使用)
```
### 3.3 ワークフロー全体フロー(擬似コード)
```python
# 1. 前回実行時刻を取得
last_checked = get_state("last_checked_at") or (now() - 10 minutes)
# 2. 変更履歴を取得
response = GET "https://shiraou.keinafarm.net/reservations/api/changes/"
params: { since: last_checked.isoformat() }
headers: { "X-API-Key": NOTIFICATION_API_KEY }
# 3. 変更があればLINEに通知
if response.reservations or response.usages:
message = format_line_message(response)
send_line_message(LINE_CHANNEL_ACCESS_TOKEN, LINE_USER_ID, message)
# 4. 前回実行時刻を更新
set_state("last_checked_at", response.checked_at)
```
### 3.4 LINEメッセージのフォーマット例
```python
def format_line_message(data):
lines = ["📋 営農システム 変更通知\n"]
for r in data["reservations"]:
start = r["start_at"][:16].replace("T", " ")
end = r["end_at"][:16].replace("T", " ")
if r["operation"] == "create":
icon = "🟢"
label = "予約作成"
elif r["operation"] == "update":
icon = "🔵"
label = "予約変更"
elif r["operation"] == "cancel":
icon = "🔴"
label = "予約キャンセル"
lines.append(f"{icon} {label}")
lines.append(f" 機械: {r['machine_name']}")
lines.append(f" 利用者: {r['user_name']}")
lines.append(f" 日時: {start}{end}")
if r["reason"]:
lines.append(f" 理由: {r['reason']}")
lines.append("")
for u in data["usages"]:
start = u["start_at"][:16].replace("T", " ")
if u["operation"] == "create":
icon = "🟢"
label = "実績登録"
elif u["operation"] == "update":
icon = "🔵"
label = "実績修正"
elif u["operation"] == "delete":
icon = "🔴"
label = "実績削除"
lines.append(f"{icon} {label}")
lines.append(f" 機械: {u['machine_name']}")
lines.append(f" 利用者: {u['user_name']}")
lines.append(f" 利用量: {u['amount']}{u['unit']}")
lines.append(f" 日: {start[:10]}")
if u["reason"]:
lines.append(f" 理由: {u['reason']}")
lines.append("")
return "\n".join(lines).strip()
```
**出力例**:
```
📋 営農システム 変更通知
🟢 予約作成
機械: トラクター
利用者: 田中太郎
日時: 2026-02-25 09:00 〜 2026-02-25 12:00
🔴 予約キャンセル
機械: 田植機
利用者: 佐藤花子
日時: 2026-02-22 08:00 〜 2026-02-22 17:00
🔵 実績修正
機械: コンバイン
利用者: 山田次郎
利用量: 4.0時間
日: 2026-02-20
理由: 記録ミスのため修正
```
---
## 4. Windmill側の環境変数シークレット
| 変数名 | 説明 | 設定場所 |
|--------|------|---------|
| `NOTIFICATION_API_KEY` | 統合システムのAPIキー | Windmill Secret |
| `LINE_CHANNEL_ACCESS_TOKEN` | LINE Messaging API チャネルアクセストークン | Windmill Secret |
| `LINE_USER_ID` / `LINE_GROUP_ID` | 通知先のユーザーIDまたはグループID | Windmill Secret |
---
## 5. 統合システム側の設定django側の作業
本番サーバーの `docker-compose.yml` に以下の環境変数を追加し、デプロイする。
```yaml
environment:
- NOTIFICATION_API_KEY=<任意の強いランダム文字列>
```
**APIキーの生成例**:
```bash
openssl rand -hex 32
```
---
## 6. 動作確認方法
### curlで直接テスト
```bash
# 変更なし直近1分
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-02-21T11:59:00%2B09:00"
# 広い範囲で変更を取得(初期確認用)
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00"
# APIキーなし → 401が返ることを確認
curl "https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00"
```
### ダッシュボードで変更確認
管理者アカウントで `https://shiraou.keinafarm.net/reservations/dashboard/` にアクセスすると、
直近30件の予約操作履歴・実績操作ログを確認できる。
Windmillワークフローのデバッグ時に「APIが何を返すべきか」の確認に使える。
---
## 7. 注意事項
- `since` に渡す時刻は **前回の `checked_at`** を使う(`since` ではなく `checked_at` を保存すること)
- 同一の変更が2回通知されないよう、状態管理を確実に行う
- ワークフローがエラーで終了した場合、`last_checked_at` を更新しないようにすること(次回実行時に再取得される)
- APIキーは定期的にローテーションすること変更時は統合システム側の環境変数も同時に更新
# Windmill 通知ワークフロー連携仕様
> **作成日**: 2026-02-21
> **対象システム**: 白皇集落営農組合 統合システム (`shiraou.keinafarm.net`)
> **目的**: 予約・実績の変更をLINEで管理者に通知するWindmillワークフローの実装仕様
---
## 1. 概要
統合システム側が「変更履歴取得API」を提供する。
Windmillは定期的にこのAPIをポーリングし、変更があればLINEへ通知する。
```
Windmill定期実行
└→ GET https://shiraou.keinafarm.net/reservations/api/changes/?since=<前回実行時刻>
└→ 変更一覧(予約・実績)を取得
└→ 変更があればLINE Messaging APIへ通知
└→ 前回実行時刻を更新
```
---
## 2. 変更履歴取得API
### エンドポイント
```
GET https://shiraou.keinafarm.net/reservations/api/changes/
```
### 認証
`X-API-Key` ヘッダーにAPIキーを指定する統合システム管理者から取得
```
X-API-Key: <NOTIFICATION_API_KEY>
```
APIキーが不正な場合は `401 Unauthorized` が返る。
### クエリパラメータ
| パラメータ | 型 | 必須 | 説明 |
|-----------|-----|------|------|
| `since` | ISO8601文字列 | 必須 | この日時以降の変更を取得する |
**`since` の形式例**:
- `2026-02-21T10:00:00` ナイーブ、JSTとして扱われる
- `2026-02-21T10:00:00+09:00` (タイムゾーン付き、推奨)
### レスポンス200 OK
```json
{
"checked_at": "2026-02-21T12:00:00+09:00",
"since": "2026-02-21T10:00:00+09:00",
"reservations": [
{
"operation": "create",
"reservation_id": 123,
"user_name": "田中太郎",
"machine_name": "トラクター",
"start_at": "2026-02-25T09:00:00+09:00",
"end_at": "2026-02-25T12:00:00+09:00",
"operated_at": "2026-02-21T11:30:00+09:00",
"operator_name": "田中太郎",
"reason": ""
},
{
"operation": "cancel",
"reservation_id": 120,
"user_name": "佐藤花子",
"machine_name": "田植機",
"start_at": "2026-02-22T08:00:00+09:00",
"end_at": "2026-02-22T17:00:00+09:00",
"operated_at": "2026-02-21T11:45:00+09:00",
"operator_name": "佐藤花子",
"reason": ""
}
],
"usages": [
{
"operation": "update",
"usage_id": 456,
"user_name": "山田次郎",
"machine_name": "コンバイン",
"amount": 4.0,
"unit": "時間",
"start_at": "2026-02-20T08:00:00+09:00",
"end_at": "2026-02-20T12:00:00+09:00",
"operated_at": "2026-02-21T11:55:00+09:00",
"operator_name": "管理者A",
"reason": "記録ミスのため修正"
}
]
}
```
### operation の値一覧
**予約reservations**:
| 値 | 意味 |
|----|------|
| `create` | 予約が作成された |
| `update` | 予約の日時・機械が変更された |
| `cancel` | 予約がキャンセルされた |
**実績usages**:
| 値 | 意味 |
|----|------|
| `create` | 実績が登録された |
| `update` | 実績が修正された |
| `delete` | 実績が削除された |
### 変更なしの場合
`reservations``usages` が両方空配列になる。通知は不要。
```json
{
"checked_at": "2026-02-21T12:05:00+09:00",
"since": "2026-02-21T12:00:00+09:00",
"reservations": [],
"usages": []
}
```
### エラーレスポンス
| ステータス | 原因 |
|-----------|------|
| `401 Unauthorized` | APIキーが不正または未設定 |
| `400 Bad Request` | `since` パラメータが欠落または不正な日時形式 |
---
## 3. Windmillワークフロー設計
### 3.1 スケジュール
- **実行間隔**: 5分毎`*/5 * * * *`
- 農業機械の予約という用途上、数分の遅延は許容範囲
### 3.2 状態管理(前回実行時刻)
Windmillの **Resource** または **State Variable** に前回実行時刻を保存する。
```
変数名: last_checked_at
初期値: (初回実行時は現在時刻 - 10分 を使用)
```
### 3.3 ワークフロー全体フロー(擬似コード)
```python
# 1. 前回実行時刻を取得
last_checked = get_state("last_checked_at") or (now() - 10 minutes)
# 2. 変更履歴を取得
response = GET "https://shiraou.keinafarm.net/reservations/api/changes/"
params: { since: last_checked.isoformat() }
headers: { "X-API-Key": NOTIFICATION_API_KEY }
# 3. 変更があればLINEに通知
if response.reservations or response.usages:
message = format_line_message(response)
send_line_message(LINE_CHANNEL_ACCESS_TOKEN, LINE_USER_ID, message)
# 4. 前回実行時刻を更新
set_state("last_checked_at", response.checked_at)
```
### 3.4 LINEメッセージのフォーマット例
```python
def format_line_message(data):
lines = ["📋 営農システム 変更通知\n"]
for r in data["reservations"]:
start = r["start_at"][:16].replace("T", " ")
end = r["end_at"][:16].replace("T", " ")
if r["operation"] == "create":
icon = "🟢"
label = "予約作成"
elif r["operation"] == "update":
icon = "🔵"
label = "予約変更"
elif r["operation"] == "cancel":
icon = "🔴"
label = "予約キャンセル"
lines.append(f"{icon} {label}")
lines.append(f" 機械: {r['machine_name']}")
lines.append(f" 利用者: {r['user_name']}")
lines.append(f" 日時: {start}{end}")
if r["reason"]:
lines.append(f" 理由: {r['reason']}")
lines.append("")
for u in data["usages"]:
start = u["start_at"][:16].replace("T", " ")
if u["operation"] == "create":
icon = "🟢"
label = "実績登録"
elif u["operation"] == "update":
icon = "🔵"
label = "実績修正"
elif u["operation"] == "delete":
icon = "🔴"
label = "実績削除"
lines.append(f"{icon} {label}")
lines.append(f" 機械: {u['machine_name']}")
lines.append(f" 利用者: {u['user_name']}")
lines.append(f" 利用量: {u['amount']}{u['unit']}")
lines.append(f" 日: {start[:10]}")
if u["reason"]:
lines.append(f" 理由: {u['reason']}")
lines.append("")
return "\n".join(lines).strip()
```
**出力例**:
```
📋 営農システム 変更通知
🟢 予約作成
機械: トラクター
利用者: 田中太郎
日時: 2026-02-25 09:00 〜 2026-02-25 12:00
🔴 予約キャンセル
機械: 田植機
利用者: 佐藤花子
日時: 2026-02-22 08:00 〜 2026-02-22 17:00
🔵 実績修正
機械: コンバイン
利用者: 山田次郎
利用量: 4.0時間
日: 2026-02-20
理由: 記録ミスのため修正
```
---
## 4. Windmill側の環境変数シークレット
| 変数名 | 説明 | 設定場所 |
|--------|------|---------|
| `NOTIFICATION_API_KEY` | 統合システムのAPIキー | Windmill Secret |
| `LINE_CHANNEL_ACCESS_TOKEN` | LINE Messaging API チャネルアクセストークン | Windmill Secret |
| `LINE_USER_ID` / `LINE_GROUP_ID` | 通知先のユーザーIDまたはグループID | Windmill Secret |
---
## 5. 統合システム側の設定django側の作業
本番サーバーの `docker-compose.yml` に以下の環境変数を追加し、デプロイする。
```yaml
environment:
- NOTIFICATION_API_KEY=<任意の強いランダム文字列>
```
**APIキーの生成例**:
```bash
openssl rand -hex 32
```
---
## 6. 動作確認方法
### curlで直接テスト
```bash
# 変更なし直近1分
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-02-21T11:59:00%2B09:00"
# 広い範囲で変更を取得(初期確認用)
curl -H "X-API-Key: <キー>" \
"https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00"
# APIキーなし → 401が返ることを確認
curl "https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00"
```
### ダッシュボードで変更確認
管理者アカウントで `https://shiraou.keinafarm.net/reservations/dashboard/` にアクセスすると、
直近30件の予約操作履歴・実績操作ログを確認できる。
Windmillワークフローのデバッグ時に「APIが何を返すべきか」の確認に使える。
---
## 7. 注意事項
- `since` に渡す時刻は **前回の `checked_at`** を使う(`since` ではなく `checked_at` を保存すること)
- 同一の変更が2回通知されないよう、状態管理を確実に行う
- ワークフローがエラーで終了した場合、`last_checked_at` を更新しないようにすること(次回実行時に再取得される)
- APIキーは定期的にローテーションすること変更時は統合システム側の環境変数も同時に更新

View File

@@ -25,5 +25,5 @@
"required": []
}
}

View File

@@ -1 +1 @@
defaultTs: bun
defaultTs: bun