Compare commits
31 Commits
chore/harn
...
sync
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19f64db8ed | ||
|
|
d29dcc2b61 | ||
|
|
280c12ccdf | ||
|
|
ee88aa8c32 | ||
|
|
5d3dd18224 | ||
|
|
6f099e3665 | ||
|
|
c5b49c015d | ||
|
|
873d834dad | ||
|
|
72483e045b | ||
|
|
2a81b7ea35 | ||
|
|
4666fa23fc | ||
|
|
27854af3a7 | ||
|
|
e646a87e6b | ||
|
|
434fa33670 | ||
|
|
65846cf6f6 | ||
|
|
bdd1f5c689 | ||
|
|
33a4f5ad7b | ||
|
|
dcca6ee056 | ||
|
|
9e75903b39 | ||
|
|
148d2cb025 | ||
|
|
2911a489a2 | ||
|
|
7b31410ef4 | ||
|
|
f49ee2ab95 | ||
|
|
6aa109b628 | ||
|
|
1876548656 | ||
|
|
1180d86091 | ||
|
|
a89f54569d | ||
|
|
0601cccce3 | ||
|
|
909666a57d | ||
|
|
f36aad0203 | ||
|
|
3b8ddc8f2d |
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(curl -m 10 -s -o /dev/null -w \"%{http_code}\" -H \"Connection: Upgrade\" -H \"Upgrade: websocket\" https://windmill.keinafarm.net/ws/)",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" -t claude@keinafarm.net \"cat /home/windmill/windmill/docker-compose.yml\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker exec windmill_server cat /workspace/docker-compose.yml\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker compose --project-directory /home/windmill/windmill version 2>&1\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker compose --project-directory /home/windmill/windmill ps 2>&1\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker exec windmill_server cat /workspace/.env\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker network inspect traefik-net --format ''{{json .Name}}'' && docker network connect traefik-net windmill-windmill_extra-1 && echo ''Connected successfully''\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"docker exec windmill_server cat /workspace/docker-compose.yml > /tmp/windmill-compose.yml && cat /tmp/windmill-compose.yml | wc -l\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"grep -A 20 ''windmill_extra:'' /tmp/windmill-compose-fixed.yml | grep -E ''\\(traefik-net|windmill-lsp.service|windmill-debug.service\\)''\")",
|
|
||||||
"Bash(ssh -i \"/c/Users/akira/.ssh/ssh-key-20241206.pem\" akira@keinafarm.net \"grep -n ''windmill-lsp\\\\|windmill-debug\\\\|traefik-net'' /tmp/windmill-compose-fixed.yml\")",
|
|
||||||
"Bash(ssh -i:*)",
|
|
||||||
"WebFetch(domain:github.com)",
|
|
||||||
"Bash(python -c \"from mcp.server.fastmcp import FastMCP; print\\(''mcp OK''\\)\")",
|
|
||||||
"Bash(pip install mcp httpx)",
|
|
||||||
"Bash(WINDMILL_TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh python -c \":*)",
|
|
||||||
"Read(//c/Users/akira/.claude/**)",
|
|
||||||
"Bash(git add .gitignore mcp/)",
|
|
||||||
"Bash(git commit:*)",
|
|
||||||
"mcp__windmill__windmill_list_flows"
|
|
||||||
],
|
|
||||||
"additionalDirectories": [
|
|
||||||
"C:\\Users\\akira\\Develop\\windmill",
|
|
||||||
"C:\\Users\\akira\\.claude\\projects\\c--Users-akira-Develop-windmill\\memory\\"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"enabledMcpjsonServers": [
|
|
||||||
"windmill"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
1
.env
1
.env
@@ -16,3 +16,4 @@ GOOGLE_OAUTH_CLIENT_SECRET=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
|
|||||||
# To rotate logs, set the following variables:
|
# To rotate logs, set the following variables:
|
||||||
#LOG_MAX_SIZE=10m
|
#LOG_MAX_SIZE=10m
|
||||||
#LOG_MAX_FILE=3
|
#LOG_MAX_FILE=3
|
||||||
|
WINDMILL_TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh
|
||||||
|
|||||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -29,9 +29,6 @@ Thumbs.db
|
|||||||
*.bak
|
*.bak
|
||||||
*~
|
*~
|
||||||
|
|
||||||
# MCP server config (contains API tokens)
|
|
||||||
.mcp.json
|
|
||||||
|
|
||||||
# Resolved markdown files (generated by editor)
|
# Resolved markdown files (generated by editor)
|
||||||
*.resolved
|
*.resolved
|
||||||
*.resolved.*
|
*.resolved.*
|
||||||
@@ -49,11 +46,12 @@ workflows/.wmill/tmp/
|
|||||||
!workflows/g/
|
!workflows/g/
|
||||||
!workflows/wmill.yaml
|
!workflows/wmill.yaml
|
||||||
!workflows/wmill-lock.yaml
|
!workflows/wmill-lock.yaml
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# Windmill workflow definitions (managed by git_sync on sync branch)
|
# sync ブランチではインフラファイルを追跡しない
|
||||||
# Local working directory should not track these on main
|
docker-compose.yml
|
||||||
u/
|
docker-compose-dev.yml
|
||||||
workflows/f/
|
Caddyfile
|
||||||
workflows/u/
|
SERVER_SETUP.md
|
||||||
workflows/g/
|
env.host
|
||||||
|
sync_to_git.sh
|
||||||
|
mcp/
|
||||||
|
|||||||
31
AGENTS.md
31
AGENTS.md
@@ -1,31 +0,0 @@
|
|||||||
# Project AGENTS (windmill)
|
|
||||||
|
|
||||||
## Startup
|
|
||||||
- Load shared global rules from:
|
|
||||||
- `C:/Users/akira/.codex/shared/GLOBAL_RULES.md`
|
|
||||||
- Apply project-local rules in this file after shared rules.
|
|
||||||
|
|
||||||
## Required Read Order
|
|
||||||
1. `HARNESS.md`
|
|
||||||
2. `INDEX.md`
|
|
||||||
3. `HANDOFF.md`
|
|
||||||
4. `SERVER_SETUP.md`
|
|
||||||
|
|
||||||
## Working Mode (mandatory)
|
|
||||||
- This repository uses harness workflow.
|
|
||||||
- Every request must be framed in 3 lines:
|
|
||||||
- Purpose
|
|
||||||
- Target
|
|
||||||
- Done
|
|
||||||
- Follow `INDEX.md` work sequence and `HARNESS.md` validation policy.
|
|
||||||
|
|
||||||
## Safety Rules (mandatory)
|
|
||||||
- Work locally first.
|
|
||||||
- Before edits, ensure backup branch exists: `backup/2026-03-05-before-harness`.
|
|
||||||
- Implement on working branch (current default: `chore/harness-introduce`).
|
|
||||||
- If unexpected unrelated changes are detected, stop and ask.
|
|
||||||
|
|
||||||
## Recording Rules
|
|
||||||
- Record each task in `state/tasks/<task_id>.md`.
|
|
||||||
- Accumulate regression cases in `tests/golden/`.
|
|
||||||
|
|
||||||
35
Caddyfile
35
Caddyfile
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
layer4 {
|
|
||||||
:25 {
|
|
||||||
proxy {
|
|
||||||
to windmill_server:2525
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{$BASE_URL} {
|
|
||||||
bind {$ADDRESS}
|
|
||||||
|
|
||||||
# LSP - Language Server Protocol for code intelligence (windmill_extra:3001)
|
|
||||||
reverse_proxy /ws/* http://windmill_extra:3001
|
|
||||||
|
|
||||||
# Multiplayer - Real-time collaboration, Enterprise Edition (windmill_extra:3002)
|
|
||||||
# Uncomment and set ENABLE_MULTIPLAYER=true in docker-compose.yml
|
|
||||||
# reverse_proxy /ws_mp/* http://windmill_extra:3002
|
|
||||||
|
|
||||||
# Debugger - Interactive debugging via DAP WebSocket (windmill_extra:3003)
|
|
||||||
# Set ENABLE_DEBUGGER=true in docker-compose.yml to enable
|
|
||||||
handle_path /ws_debug/* {
|
|
||||||
reverse_proxy http://windmill_extra:3003
|
|
||||||
}
|
|
||||||
|
|
||||||
# Search indexer, Enterprise Edition (windmill_indexer:8002)
|
|
||||||
# reverse_proxy /api/srch/* http://windmill_indexer:8002
|
|
||||||
|
|
||||||
# Default: Windmill server
|
|
||||||
reverse_proxy /* http://windmill_server:8000
|
|
||||||
|
|
||||||
# TLS with custom certificates
|
|
||||||
# tls /certs/cert.pem /certs/key.pem
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
:80 {
|
|
||||||
# LSP - Language Server Protocol for code intelligence
|
|
||||||
reverse_proxy /ws/* http://windmill_extra:3001
|
|
||||||
|
|
||||||
# Debugger - Interactive debugging via DAP WebSocket
|
|
||||||
handle_path /ws_debug/* {
|
|
||||||
reverse_proxy http://windmill_extra:3003
|
|
||||||
}
|
|
||||||
|
|
||||||
# Default: Windmill server
|
|
||||||
reverse_proxy /* http://windmill_server:8000
|
|
||||||
}
|
|
||||||
112
HANDOFF.md
112
HANDOFF.md
@@ -1,112 +0,0 @@
|
|||||||
# Windmill 作業引き継ぎメモ
|
|
||||||
|
|
||||||
> 作成: 2026-02-25(keinasystem_t02 セッションから移管)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 現在の状態
|
|
||||||
|
|
||||||
### git_sync フロー(修正済み ✅)
|
|
||||||
|
|
||||||
**問題**: `u/antigravity/git_sync` フローが `wmill sync pull` で認証エラー
|
|
||||||
**原因**: `$WM_TOKEN`(ジョブトークン)はワークスペーススコープのため、
|
|
||||||
wmill CLIが内部で呼ぶ `/api/users/whoami`(グローバルAPI)で401
|
|
||||||
**解決**:
|
|
||||||
- `/home/windmill/windmill/wmill_config/remotes.ndjson` を作成(永続設定)
|
|
||||||
- グローバルスコープトークン `CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8`(ラベル: `git-sync`)を使用
|
|
||||||
- フロースクリプトを `--config-dir /workspace/wmill_config` を使う形に修正
|
|
||||||
- 動作確認: Success: True (2秒) ✅
|
|
||||||
|
|
||||||
### windmill.keinafarm.net 外部アクセス(504 未修正 ❌)
|
|
||||||
|
|
||||||
- 外部からアクセスすると 30秒後に 504 Gateway Timeout
|
|
||||||
- サーバー内部からは `http://localhost:8000` で正常にアクセスできる
|
|
||||||
- **Caddyfile がこのディレクトリにある** → 原因調査・修正が必要
|
|
||||||
|
|
||||||
### Windmill MCP サーバー(未着手 ⬜)
|
|
||||||
|
|
||||||
- LLM(Claude)が直接 Windmill を操作できるようにする
|
|
||||||
- 外部アクセスが直れば `https://windmill.keinafarm.net` に接続
|
|
||||||
- 直らない場合は SSH トンネル経由
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 重要な情報
|
|
||||||
|
|
||||||
### サーバー接続
|
|
||||||
|
|
||||||
```
|
|
||||||
SSH: root@keinafarm.net
|
|
||||||
Windmill内部URL: http://localhost:8000
|
|
||||||
Windmillサーバー上のパス: /home/windmill/windmill/
|
|
||||||
```
|
|
||||||
|
|
||||||
### API トークン
|
|
||||||
|
|
||||||
| トークン | スコープ | 用途 |
|
|
||||||
|---------|---------|------|
|
|
||||||
| `qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh` | ワークスペース(admins) | 通常のAPI操作 |
|
|
||||||
| `CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8` | グローバル | git-sync用(wmill CLI) |
|
|
||||||
|
|
||||||
### 2つの Git リポジトリ(別物!)
|
|
||||||
|
|
||||||
| リポジトリ | パス | 用途 |
|
|
||||||
|-----------|------|------|
|
|
||||||
| `windmill.git` (Gitea) | サーバー `/home/windmill/windmill/` | wmill CLI で自動同期 |
|
|
||||||
| `windmill_workflow.git` (Gitea) | ローカル `C:\Users\akira\Develop\windmill` | このディレクトリ |
|
|
||||||
|
|
||||||
### wmill_config(永続設定)
|
|
||||||
|
|
||||||
```
|
|
||||||
サーバーパス: /home/windmill/windmill/wmill_config/
|
|
||||||
コンテナ内パス: /workspace/wmill_config/
|
|
||||||
remotes.ndjson: {"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"}
|
|
||||||
activeWorkspace: admins
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 次にやること(優先順)
|
|
||||||
|
|
||||||
### 1. windmill.keinafarm.net 504 を修正
|
|
||||||
|
|
||||||
`Caddyfile` を確認して、windmill へのリバースプロキシ設定を見直す。
|
|
||||||
タイムアウト設定が足りない可能性が高い。
|
|
||||||
|
|
||||||
```
|
|
||||||
# 確認コマンド(サーバー内部では正常)
|
|
||||||
ssh root@keinafarm.net "curl -s http://localhost:8000/api/version"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Windmill MCP サーバーを実装
|
|
||||||
|
|
||||||
**方針**: カスタム軽量 Python MCP サーバー(6〜8 tools)
|
|
||||||
|
|
||||||
実装する tools:
|
|
||||||
- `windmill_list_flows` — フロー一覧
|
|
||||||
- `windmill_get_flow` — フローのスクリプト取得
|
|
||||||
- `windmill_run_flow` — フローをトリガー
|
|
||||||
- `windmill_list_recent_jobs` — 最近のジョブ一覧(成功/失敗)
|
|
||||||
- `windmill_get_job_logs` — ジョブの詳細ログ
|
|
||||||
- `windmill_list_scripts` — スクリプト一覧
|
|
||||||
- `windmill_get_script` — スクリプト取得
|
|
||||||
|
|
||||||
既存実装: `rothnic/windmill-mcp`(GitHub, スター0, 更新停止)→ 使わず自作
|
|
||||||
|
|
||||||
### 3. mail_filter.flow.json をコミット
|
|
||||||
|
|
||||||
ローカルの `windmill_workflow` に `f/mail/mail_filter.flow.json` が未コミット状態。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 参考: wmill 設定ファイル形式
|
|
||||||
|
|
||||||
```
|
|
||||||
~/.config/windmill/remotes.ndjson (1行1ワークスペース)
|
|
||||||
{"remote":"http://...../","workspaceId":"...","name":"local_alias","token":"..."}
|
|
||||||
|
|
||||||
~/.config/windmill/activeWorkspace (プレーンテキスト)
|
|
||||||
local_alias
|
|
||||||
```
|
|
||||||
|
|
||||||
`wmill sync pull --config-dir /path/to/config` で任意ディレクトリを指定可能。
|
|
||||||
70
HARNESS.md
70
HARNESS.md
@@ -1,70 +0,0 @@
|
|||||||
# HARNESS.md
|
|
||||||
|
|
||||||
## 目的
|
|
||||||
- このリポジトリ(Windmill運用)で、Codex / Claude Code / ローカルLLMの協調作業を安全かつ再現可能にする。
|
|
||||||
- 大きい指示文より、コンテキスト設計・制約・検証・記録を優先する。
|
|
||||||
|
|
||||||
## 役割分担
|
|
||||||
- Codex: 要件分解、変更方針、レビュー観点、ドキュメント整備。
|
|
||||||
- Claude Code: 実装、コマンド実行、検証、差分反映。
|
|
||||||
- ローカルLLM: 下書き生成、軽量リファクタ提案、代替案提示。
|
|
||||||
|
|
||||||
## 依頼フォーマット(必須3行)
|
|
||||||
1. Purpose: 何を達成するか
|
|
||||||
2. Target: どのファイル/機能/環境か
|
|
||||||
3. Done: 何を確認できたら完了か
|
|
||||||
|
|
||||||
## 作業スコープ
|
|
||||||
- 主対象: `workflows/`, `mcp/`, `docker-compose-dev.yml`, `docker-compose.yml`, `sync_to_git.sh`, `HANDOFF.md`, `SERVER_SETUP.md`
|
|
||||||
- 原則: 1タスク1責務。仕様変更と大規模改修を同時に進めない。
|
|
||||||
- 仕様/挙動変更時は、同タスクでドキュメント更新まで行う。
|
|
||||||
|
|
||||||
## コンテキスト方針
|
|
||||||
- 長い会話ログは渡さない。
|
|
||||||
- 毎回渡す情報は最小化する:
|
|
||||||
- 依頼3行(Purpose/Target/Done)
|
|
||||||
- 関連ファイルのパス
|
|
||||||
- 失敗コマンド + ログ先頭5行 + 末尾5行
|
|
||||||
- 現在の前提・制約
|
|
||||||
- トークン圧迫時は要約スナップショットへ圧縮する。
|
|
||||||
|
|
||||||
## 安全レール
|
|
||||||
- 破壊的操作(削除、履歴改変、本番反映)は事前承認必須。
|
|
||||||
- シークレットをプロンプト/ログ/コミットに含めない。
|
|
||||||
- タスクに無関係なパスは編集しない。
|
|
||||||
- 想定外の差分を検知したら作業停止して確認する。
|
|
||||||
|
|
||||||
## 検証ポリシー(Windmill向け固定)
|
|
||||||
- L1 構成検証:
|
|
||||||
- `docker compose -f docker-compose-dev.yml config`
|
|
||||||
- L2 サービス検証(必要時):
|
|
||||||
- `docker compose -f docker-compose-dev.yml up -d db windmill_server`
|
|
||||||
- `docker compose -f docker-compose-dev.yml ps`
|
|
||||||
- `docker compose -f docker-compose-dev.yml exec -T windmill_server curl -fsS http://localhost:8000/api/version`
|
|
||||||
- L3 変更対象別検証:
|
|
||||||
- `mcp/` 変更時: `docker compose build windmill_mcp` と `docker compose up -d windmill_mcp`
|
|
||||||
- `workflows/` 変更時: `wmill sync` 関連手順を `SERVER_SETUP.md` / `HANDOFF.md` に沿って確認
|
|
||||||
- 失敗時は必ずログを残す:
|
|
||||||
- `docker compose -f docker-compose-dev.yml logs --tail=200 windmill_server`
|
|
||||||
|
|
||||||
## 品質ゲート
|
|
||||||
- 変更内容に対応するL1/L2/L3結果を記録して完了とする。
|
|
||||||
- サイレントな挙動変更は禁止(テスト/手順書更新を伴うこと)。
|
|
||||||
- 運用手順に影響する変更は `HANDOFF.md` または `SERVER_SETUP.md` を同時更新する。
|
|
||||||
|
|
||||||
## 記録ルール
|
|
||||||
- タスクごとに `state/tasks/<task_id>.md` を作成。
|
|
||||||
- 最低記録項目:
|
|
||||||
- task_id
|
|
||||||
- 依頼3行
|
|
||||||
- 変更ファイル
|
|
||||||
- 実行コマンドと結果
|
|
||||||
- 未解決リスク/次アクション
|
|
||||||
|
|
||||||
## ゴールデンデータセット
|
|
||||||
- 再発した不具合は `tests/golden/` に再現手順/入出力を追加する。
|
|
||||||
- 修正時は可能な限り先に再現ケースを追加してから直す。
|
|
||||||
|
|
||||||
## 復旧方針
|
|
||||||
- 作業前にバックアップブランチを作る。
|
|
||||||
- 緊急時はバックアップブランチに即時復帰できる状態を維持する。
|
|
||||||
60
INDEX.md
60
INDEX.md
@@ -1,60 +0,0 @@
|
|||||||
# INDEX.md
|
|
||||||
|
|
||||||
## Start Here
|
|
||||||
1. `AGENTS.md`(存在する場合)
|
|
||||||
2. `HARNESS.md`
|
|
||||||
3. `HANDOFF.md`
|
|
||||||
4. `SERVER_SETUP.md`
|
|
||||||
5. 今回タスクの3行(Purpose/Target/Done)
|
|
||||||
|
|
||||||
## このリポジトリで優先的に見る場所
|
|
||||||
- 運用メモ:
|
|
||||||
- `HANDOFF.md`
|
|
||||||
- `SERVER_SETUP.md`
|
|
||||||
- 実装・設定:
|
|
||||||
- `workflows/`
|
|
||||||
- `mcp/`
|
|
||||||
- `docker-compose-dev.yml`
|
|
||||||
- `docker-compose.yml`
|
|
||||||
- `sync_to_git.sh`
|
|
||||||
- 実行時状態:
|
|
||||||
- `.env`(読み取りのみ・値の露出禁止)
|
|
||||||
- `.mcp.json`(読み取りのみ・値の露出禁止)
|
|
||||||
- 記録:
|
|
||||||
- `state/tasks/`
|
|
||||||
- `tests/golden/`
|
|
||||||
|
|
||||||
## 作業手順(固定)
|
|
||||||
1. 依頼3行を確定する。
|
|
||||||
2. 変更対象を最小ファイルに絞る。
|
|
||||||
3. 小さい差分で実装する。
|
|
||||||
4. 検証を実行する(HARNESSのL1/L2/L3)。
|
|
||||||
5. `state/tasks/<task_id>.md` に記録する。
|
|
||||||
6. 必要なら `HANDOFF.md` / `SERVER_SETUP.md` を更新する。
|
|
||||||
|
|
||||||
## Agent入力テンプレート
|
|
||||||
- Purpose: ...
|
|
||||||
- Target: ...
|
|
||||||
- Done: ...
|
|
||||||
- Constraints: ...
|
|
||||||
- Relevant files:
|
|
||||||
- path1
|
|
||||||
- path2
|
|
||||||
- Verification commands:
|
|
||||||
- cmd1
|
|
||||||
- cmd2
|
|
||||||
|
|
||||||
## 検証コマンド(このrepo既定)
|
|
||||||
```powershell
|
|
||||||
docker compose -f docker-compose-dev.yml config
|
|
||||||
docker compose -f docker-compose-dev.yml up -d db windmill_server
|
|
||||||
docker compose -f docker-compose-dev.yml ps
|
|
||||||
docker compose -f docker-compose-dev.yml exec -T windmill_server curl -fsS http://localhost:8000/api/version
|
|
||||||
docker compose -f docker-compose-dev.yml logs --tail=200 windmill_server
|
|
||||||
```
|
|
||||||
|
|
||||||
## 完了条件(DoD)
|
|
||||||
- 要求された挙動を満たす。
|
|
||||||
- 検証結果が記録されている。
|
|
||||||
- 運用手順影響がある場合、関連ドキュメントが更新されている。
|
|
||||||
- 未解決リスクと次アクションが明示されている。
|
|
||||||
155
SERVER_SETUP.md
155
SERVER_SETUP.md
@@ -1,155 +0,0 @@
|
|||||||
# Windmill サーバー設定手順 (VPS移行版)
|
|
||||||
|
|
||||||
本番環境(VPS)へのデプロイ手順です。
|
|
||||||
既にTraefikが稼働している環境(`traefik-net` ネットワークが存在する環境)を前提としています。
|
|
||||||
|
|
||||||
## 前提条件
|
|
||||||
|
|
||||||
- サーバー上でTraefikが稼働しており、`traefik-net` ネットワークが存在すること。
|
|
||||||
- ドメイン `windmill.keinafarm.net` がサーバーのIPに向けられていること。
|
|
||||||
|
|
||||||
## ステップ1: リポジトリの準備
|
|
||||||
|
|
||||||
サーバー上の任意の場所(例: `/home/windmill/windmill`)にリポジトリをクローンします。
|
|
||||||
**重要**: WindmillのGit同期機能を使用するため、このディレクトリパスは重要です。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /home/windmill
|
|
||||||
cd /home/windmill
|
|
||||||
git clone https://gitea.keinafarm.net/akira/windmill.git windmill
|
|
||||||
cd windmill
|
|
||||||
```
|
|
||||||
|
|
||||||
## ステップ2: 環境変数の設定
|
|
||||||
|
|
||||||
`.env` ファイルを作成し、本番用の設定を行います。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env .env.production
|
|
||||||
nano .env
|
|
||||||
```
|
|
||||||
|
|
||||||
以下の内容を確認・修正してください:
|
|
||||||
- `DATABASE_URL`: `postgres://postgres:あなたの強力なパスワード@db/windmill?sslmode=disable`
|
|
||||||
- `POSTGRES_PASSWORD`: 上記と同じパスワード
|
|
||||||
- `WM_IMAGE`: `ghcr.io/windmill-labs/windmill:main`
|
|
||||||
|
|
||||||
## ステップ3: 起動
|
|
||||||
|
|
||||||
`docker-compose.yml` は本番用に構成されています(Traefik連携済み)。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## ステップ4: Git同期用ワークフローのセットアップ
|
|
||||||
|
|
||||||
Windmill上で「登録されたワークフローをGitに保存する」機能を有効にする手順です。
|
|
||||||
`git_sync` フローが定期実行されると、Windmill DB上のスクリプト/フローの変更がGiteaリポジトリに自動コミット&プッシュされます。
|
|
||||||
|
|
||||||
### 4-1. Windmill APIトークンの取得
|
|
||||||
|
|
||||||
1. ブラウザで `https://windmill.keinafarm.net` にログイン
|
|
||||||
2. 左下の **Settings** → **Account** をクリック
|
|
||||||
3. **Tokens** セクションで **Create token** をクリック
|
|
||||||
4. Label(例: `git-sync`)を入力し、作成
|
|
||||||
5. 表示されたトークンをコピーしておく(後のステップで使用)
|
|
||||||
|
|
||||||
### 4-2. ワークフロー定義の取り込み(初回のみ)
|
|
||||||
|
|
||||||
リポジトリの `workflows/` にある定義ファイルをWindmill DBに取り込みます。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Windmillサーバーコンテナに入る
|
|
||||||
docker exec -it windmill_server /bin/bash
|
|
||||||
|
|
||||||
# コンテナ内で実行:windmill-cli をインストール
|
|
||||||
npm install -g windmill-cli
|
|
||||||
|
|
||||||
# wmill.yamlがあるディレクトリに移動して sync push
|
|
||||||
cd /workspace/workflows
|
|
||||||
wmill sync push \
|
|
||||||
--token "<4-1で取得したトークン>" \
|
|
||||||
--base-url "http://localhost:8000" \
|
|
||||||
--workspace admins \
|
|
||||||
--yes
|
|
||||||
|
|
||||||
exit
|
|
||||||
```
|
|
||||||
|
|
||||||
> **注意**: `wmill sync push` はディスク→DBへの反映です。
|
|
||||||
> 逆に `wmill sync pull` はDB→ディスクへの反映です。
|
|
||||||
> スケジュールされた `git_sync` フローが `sync pull` を実行するため、
|
|
||||||
> **UIで直接スクリプトを修正した場合、次回の sync pull で正しくディスクにも反映されます。**
|
|
||||||
|
|
||||||
### 4-3. Gitea認証情報の設定(git push用)
|
|
||||||
|
|
||||||
`git_sync` フローが Gitea へ `git push` できるよう、サーバー上のリモートURLにGiteaのアクセストークンを含めます。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# サーバーのホスト側で実行
|
|
||||||
cd ~/windmill
|
|
||||||
|
|
||||||
# 現在のリモートURLを確認
|
|
||||||
git remote -v
|
|
||||||
|
|
||||||
# Giteaのアクセストークンを含んだURLに変更
|
|
||||||
git remote set-url origin https://<username>:<giteaトークン>@gitea.keinafarm.net/akira/windmill.git
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Giteaトークンの作成方法**: Gitea(`https://gitea.keinafarm.net`)にログイン →
|
|
||||||
> 右上アバター → Settings → Applications → Generate New Token
|
|
||||||
|
|
||||||
### 4-4. WM_TOKEN Variable の設定
|
|
||||||
|
|
||||||
WindmillのWeb画面で、`git_sync` フローが使用する変数を登録します。
|
|
||||||
|
|
||||||
1. 左メニューの **Variables** をクリック
|
|
||||||
2. **+ Variable** をクリック
|
|
||||||
3. 以下を入力:
|
|
||||||
- **Path**: `u/antigravity/wm_token`
|
|
||||||
- **Value**: 4-1で取得したWindmill APIトークン
|
|
||||||
- **Is Secret**: ✅ オン
|
|
||||||
4. **Save** をクリック
|
|
||||||
|
|
||||||
> **注意**: `git_sync` フローのスクリプト(`a.sh`)内で `$WM_TOKEN` として参照されます。
|
|
||||||
> フローのInput設定で、この変数が正しく紐づけられていることを確認してください。
|
|
||||||
|
|
||||||
### 4-5. git_sync フローの手動実行テスト
|
|
||||||
|
|
||||||
1. Windmill UI で **`u/antigravity/git_sync`** フローを開く
|
|
||||||
2. **Run** ボタンで手動実行
|
|
||||||
3. **Runs** ページで実行ログを確認
|
|
||||||
4. 成功すれば、Giteaリポジトリに自動コミットが作成されているはず
|
|
||||||
|
|
||||||
### 4-6. スケジュール実行の確認
|
|
||||||
|
|
||||||
`git_sync.schedule.yaml` により、2分ごとに自動実行されるスケジュールが登録されています。
|
|
||||||
左メニューの **Schedules** から、スケジュールが有効になっていることを確認してください。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## トラブルシューティング
|
|
||||||
|
|
||||||
### ディスク上のファイルが古い内容に戻る
|
|
||||||
`git_sync` フローが `wmill sync pull`(DB→ディスク)を実行するため、UIで修正した内容がディスクに上書きされます。
|
|
||||||
スクリプトの修正は **Windmill UI上で直接編集** するのが確実です。
|
|
||||||
|
|
||||||
### git push が失敗する
|
|
||||||
```bash
|
|
||||||
# サーバー上でリモートURLにトークンが含まれているか確認
|
|
||||||
cd ~/windmill
|
|
||||||
git remote -v
|
|
||||||
# https://<user>:<token>@gitea.keinafarm.net/... の形式であること
|
|
||||||
```
|
|
||||||
|
|
||||||
### 開発環境(ローカル)での起動
|
|
||||||
ローカルで起動する場合は `docker-compose-dev.yml` を使用します:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose-dev.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### ログ確認
|
|
||||||
```bash
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
x-logging: &default-logging
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "${LOG_MAX_SIZE:-20m}"
|
|
||||||
max-file: "${LOG_MAX_FILE:-10}"
|
|
||||||
compress: "true"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
windmill-internal:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
deploy:
|
|
||||||
replicas: 1
|
|
||||||
image: postgres:16
|
|
||||||
shm_size: 1g
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- db_data:/var/lib/postgresql/data
|
|
||||||
expose:
|
|
||||||
- 5432
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
|
||||||
POSTGRES_DB: windmill
|
|
||||||
healthcheck:
|
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
logging: *default-logging
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
|
|
||||||
windmill_server:
|
|
||||||
image: ${WM_IMAGE}
|
|
||||||
container_name: windmill_server
|
|
||||||
pull_policy: if_not_present
|
|
||||||
deploy:
|
|
||||||
replicas: 1
|
|
||||||
restart: unless-stopped
|
|
||||||
expose:
|
|
||||||
- 8000
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- MODE=server
|
|
||||||
- BASE_URL=http://localhost
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- worker_logs:/tmp/windmill/logs
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
windmill_worker:
|
|
||||||
image: ${WM_IMAGE}
|
|
||||||
pull_policy: if_not_present
|
|
||||||
deploy:
|
|
||||||
replicas: 1
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "1"
|
|
||||||
memory: 2048M
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- MODE=worker
|
|
||||||
- WORKER_GROUP=default
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- worker_dependency_cache:/tmp/windmill/cache
|
|
||||||
- worker_logs:/tmp/windmill/logs
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
windmill_worker_native:
|
|
||||||
image: ${WM_IMAGE}
|
|
||||||
pull_policy: if_not_present
|
|
||||||
deploy:
|
|
||||||
replicas: 1
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "1"
|
|
||||||
memory: 2048M
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- MODE=worker
|
|
||||||
- WORKER_GROUP=native
|
|
||||||
- NUM_WORKERS=8
|
|
||||||
- SLEEP_QUEUE=200
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- worker_logs:/tmp/windmill/logs
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
windmill_extra:
|
|
||||||
image: ghcr.io/windmill-labs/windmill-extra:${WM_VERSION}
|
|
||||||
pull_policy: if_not_present
|
|
||||||
restart: unless-stopped
|
|
||||||
expose:
|
|
||||||
- 3001
|
|
||||||
- 3003
|
|
||||||
environment:
|
|
||||||
- ENABLE_LSP=true
|
|
||||||
- ENABLE_MULTIPLAYER=false
|
|
||||||
- ENABLE_DEBUGGER=true
|
|
||||||
- DEBUGGER_PORT=3003
|
|
||||||
- ENABLE_NSJAIL=false
|
|
||||||
- REQUIRE_SIGNED_DEBUG_REQUESTS=false
|
|
||||||
- WINDMILL_BASE_URL=http://windmill_server:8000
|
|
||||||
volumes:
|
|
||||||
- lsp_cache:/pyls/.cache
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
caddy:
|
|
||||||
image: caddy:2.9-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile.local:/etc/caddy/Caddyfile
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
depends_on:
|
|
||||||
- windmill_server
|
|
||||||
- windmill_extra
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data: null
|
|
||||||
worker_dependency_cache: null
|
|
||||||
worker_logs: null
|
|
||||||
lsp_cache: null
|
|
||||||
caddy_data: null
|
|
||||||
caddy_config: null
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
x-logging: &default-logging
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "${LOG_MAX_SIZE:-20m}"
|
|
||||||
max-file: "${LOG_MAX_FILE:-10}"
|
|
||||||
compress: "true"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
traefik-net:
|
|
||||||
external: true # サーバー上の既存Traefikネットワーク
|
|
||||||
windmill-internal:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
deploy:
|
|
||||||
replicas: 1
|
|
||||||
image: postgres:16
|
|
||||||
shm_size: 1g
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- db_data:/var/lib/postgresql/data
|
|
||||||
expose:
|
|
||||||
- 5432
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
|
||||||
POSTGRES_DB: windmill
|
|
||||||
healthcheck:
|
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
logging: *default-logging
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
|
|
||||||
windmill_server:
|
|
||||||
image: ${WM_IMAGE}
|
|
||||||
container_name: windmill_server
|
|
||||||
pull_policy: if_not_present
|
|
||||||
deploy:
|
|
||||||
replicas: 1
|
|
||||||
restart: unless-stopped
|
|
||||||
expose:
|
|
||||||
- 8000
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- MODE=server
|
|
||||||
- BASE_URL=https://windmill.keinafarm.net
|
|
||||||
- OAUTH_REDIRECT_BASE_URL=https://windmill.keinafarm.net
|
|
||||||
- GOOGLE_OAUTH_ENABLED=true
|
|
||||||
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
|
|
||||||
- GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET}
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- worker_logs:/tmp/windmill/logs
|
|
||||||
# Git同期のために、カレントディレクトリ(リポジトリルート)を/workspaceにマウント
|
|
||||||
# これにより、コンテナ内から .git ディレクトリにアクセス可能となり、git pushが可能になる
|
|
||||||
- .:/workspace
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
# HTTPSルーター
|
|
||||||
- "traefik.http.routers.windmill.rule=Host(`windmill.keinafarm.net`)"
|
|
||||||
- "traefik.http.routers.windmill.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.windmill.tls=true"
|
|
||||||
- "traefik.http.routers.windmill.tls.certresolver=letsencrypt"
|
|
||||||
- "traefik.http.services.windmill.loadbalancer.server.port=8000"
|
|
||||||
# HTTPからHTTPSへのリダイレクト
|
|
||||||
- "traefik.http.routers.windmill-http.rule=Host(`windmill.keinafarm.net`)"
|
|
||||||
- "traefik.http.routers.windmill-http.entrypoints=web"
|
|
||||||
- "traefik.http.routers.windmill-http.middlewares=windmill-https-redirect"
|
|
||||||
- "traefik.http.middlewares.windmill-https-redirect.redirectscheme.scheme=https"
|
|
||||||
networks:
|
|
||||||
- traefik-net
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
windmill_worker:
|
|
||||||
image: ${WM_IMAGE}
|
|
||||||
pull_policy: if_not_present
|
|
||||||
deploy:
|
|
||||||
replicas: 3
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "1"
|
|
||||||
memory: 2048M
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- MODE=worker
|
|
||||||
- WORKER_GROUP=default
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- worker_dependency_cache:/tmp/windmill/cache
|
|
||||||
- worker_logs:/tmp/windmill/logs
|
|
||||||
# WorkerからもGit同期が必要な場合に備えてマウント
|
|
||||||
- .:/workspace
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
windmill_worker_native:
|
|
||||||
image: ${WM_IMAGE}
|
|
||||||
pull_policy: if_not_present
|
|
||||||
deploy:
|
|
||||||
replicas: 1
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "1"
|
|
||||||
memory: 2048M
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- MODE=worker
|
|
||||||
- WORKER_GROUP=native
|
|
||||||
- NUM_WORKERS=8
|
|
||||||
- SLEEP_QUEUE=200
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- worker_logs:/tmp/windmill/logs
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
windmill_extra:
|
|
||||||
image: ghcr.io/windmill-labs/windmill-extra:${WM_VERSION}
|
|
||||||
pull_policy: if_not_present
|
|
||||||
restart: unless-stopped
|
|
||||||
expose:
|
|
||||||
- 3001
|
|
||||||
- 3002
|
|
||||||
- 3003
|
|
||||||
environment:
|
|
||||||
- ENABLE_LSP=true
|
|
||||||
- ENABLE_MULTIPLAYER=false
|
|
||||||
- ENABLE_DEBUGGER=true
|
|
||||||
- DEBUGGER_PORT=3003
|
|
||||||
- ENABLE_NSJAIL=false
|
|
||||||
- REQUIRE_SIGNED_DEBUG_REQUESTS=false
|
|
||||||
- WINDMILL_BASE_URL=http://windmill_server:8000
|
|
||||||
volumes:
|
|
||||||
- lsp_cache:/pyls/.cache
|
|
||||||
networks:
|
|
||||||
- windmill-internal
|
|
||||||
logging: *default-logging
|
|
||||||
labels:
|
|
||||||
# LSPなどのWebSocket用設定(Caddyfileの代替)
|
|
||||||
- "traefik.enable=true"
|
|
||||||
# LSPへのルーティング (/ws/* -> 3001)
|
|
||||||
- "traefik.http.routers.windmill-lsp.rule=Host(`windmill.keinafarm.net`) && PathPrefix(`/ws/`)"
|
|
||||||
- "traefik.http.routers.windmill-lsp.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.windmill-lsp.tls=true"
|
|
||||||
- "traefik.http.services.windmill-lsp.loadbalancer.server.port=3001"
|
|
||||||
# Debuggerへのルーティング (/ws_debug/* -> 3003)
|
|
||||||
- "traefik.http.routers.windmill-debug.rule=Host(`windmill.keinafarm.net`) && PathPrefix(`/ws_debug/`)"
|
|
||||||
- "traefik.http.routers.windmill-debug.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.windmill-debug.tls=true"
|
|
||||||
- "traefik.http.services.windmill-debug.loadbalancer.server.port=3003"
|
|
||||||
|
|
||||||
windmill_mcp:
|
|
||||||
build:
|
|
||||||
context: ./mcp
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: windmill_mcp
|
|
||||||
restart: unless-stopped
|
|
||||||
expose:
|
|
||||||
- 8001
|
|
||||||
environment:
|
|
||||||
- WINDMILL_TOKEN=${WINDMILL_TOKEN}
|
|
||||||
- WINDMILL_URL=https://windmill.keinafarm.net
|
|
||||||
- WINDMILL_WORKSPACE=admins
|
|
||||||
- MCP_TRANSPORT=sse
|
|
||||||
- MCP_HOST=0.0.0.0
|
|
||||||
- MCP_PORT=8001
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
# HTTPS ルーター
|
|
||||||
- "traefik.http.routers.windmill-mcp.rule=Host(`windmill-mcp.keinafarm.net`)"
|
|
||||||
- "traefik.http.routers.windmill-mcp.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.windmill-mcp.tls=true"
|
|
||||||
- "traefik.http.routers.windmill-mcp.tls.certresolver=letsencrypt"
|
|
||||||
- "traefik.http.services.windmill-mcp.loadbalancer.server.port=8001"
|
|
||||||
# HTTP → HTTPS リダイレクト
|
|
||||||
- "traefik.http.routers.windmill-mcp-http.rule=Host(`windmill-mcp.keinafarm.net`)"
|
|
||||||
- "traefik.http.routers.windmill-mcp-http.entrypoints=web"
|
|
||||||
- "traefik.http.routers.windmill-mcp-http.middlewares=windmill-https-redirect"
|
|
||||||
networks:
|
|
||||||
- traefik-net
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data: null
|
|
||||||
worker_dependency_cache: null
|
|
||||||
worker_logs: null
|
|
||||||
lsp_cache: null
|
|
||||||
5
env.host
5
env.host
@@ -1,5 +0,0 @@
|
|||||||
WM_IMAGE=ghcr.io/windmill-labs/windmill:main
|
|
||||||
POSTGRES_PASSWORD=MyS3cur3P@ssw0rd!2024
|
|
||||||
DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/windmill
|
|
||||||
LOG_MAX_SIZE=20m
|
|
||||||
LOG_MAX_FILE=10
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Windmill MCP Server の設定
|
|
||||||
# このファイルを .env にコピーして値を設定してください
|
|
||||||
|
|
||||||
# Windmill のベース URL(デフォルト: https://windmill.keinafarm.net)
|
|
||||||
WINDMILL_URL=https://windmill.keinafarm.net
|
|
||||||
|
|
||||||
# Windmill API トークン(必須)
|
|
||||||
# Windmill の「設定 > トークン」から作成してください
|
|
||||||
WINDMILL_TOKEN=your_token_here
|
|
||||||
|
|
||||||
# 対象ワークスペース(デフォルト: admins)
|
|
||||||
WINDMILL_WORKSPACE=admins
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
COPY windmill_mcp.py .
|
|
||||||
|
|
||||||
ENV MCP_TRANSPORT=sse
|
|
||||||
ENV MCP_HOST=0.0.0.0
|
|
||||||
ENV MCP_PORT=8001
|
|
||||||
|
|
||||||
CMD ["python", "windmill_mcp.py"]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"windmill": {
|
|
||||||
"command": "python",
|
|
||||||
"args": ["windmill_mcp.py"],
|
|
||||||
"cwd": "/path/to/mcp",
|
|
||||||
"env": {
|
|
||||||
"WINDMILL_TOKEN": "your_api_token_here",
|
|
||||||
"WINDMILL_URL": "https://windmill.keinafarm.net",
|
|
||||||
"WINDMILL_WORKSPACE": "admins"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
mcp>=1.0.0
|
|
||||||
httpx>=0.27.0
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Windmill MCP Server - Claude が Windmill を直接操作できるようにする"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import httpx
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
|
||||||
|
|
||||||
WINDMILL_URL = os.environ.get("WINDMILL_URL", "https://windmill.keinafarm.net")
|
|
||||||
WINDMILL_TOKEN = os.environ.get("WINDMILL_TOKEN", "")
|
|
||||||
WINDMILL_WORKSPACE = os.environ.get("WINDMILL_WORKSPACE", "admins")
|
|
||||||
MCP_HOST = os.environ.get("MCP_HOST", "127.0.0.1")
|
|
||||||
MCP_PORT = int(os.environ.get("MCP_PORT", "8001"))
|
|
||||||
|
|
||||||
if not WINDMILL_TOKEN:
|
|
||||||
print("Error: WINDMILL_TOKEN 環境変数が設定されていません", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
mcp = FastMCP("windmill", host=MCP_HOST, port=MCP_PORT)
|
|
||||||
|
|
||||||
|
|
||||||
def _headers() -> dict:
|
|
||||||
return {"Authorization": f"Bearer {WINDMILL_TOKEN}"}
|
|
||||||
|
|
||||||
|
|
||||||
def _api(path: str) -> str:
|
|
||||||
return f"{WINDMILL_URL}/api/w/{WINDMILL_WORKSPACE}/{path}"
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_list_flows(per_page: int = 20) -> str:
|
|
||||||
"""Windmill のフロー一覧を取得する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
per_page: 取得件数(最大100)
|
|
||||||
"""
|
|
||||||
resp = httpx.get(
|
|
||||||
_api("flows/list"),
|
|
||||||
headers=_headers(),
|
|
||||||
params={"per_page": min(per_page, 100)},
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
flows = resp.json()
|
|
||||||
if not flows:
|
|
||||||
return "フローが見つかりませんでした"
|
|
||||||
lines = [
|
|
||||||
f"- {f['path']}: {f.get('summary', '(概要なし)')}" for f in flows
|
|
||||||
]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_get_flow(path: str) -> str:
|
|
||||||
"""指定したパスのフロー定義(スクリプト含む)を取得する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: フローのパス (例: u/antigravity/git_sync)
|
|
||||||
"""
|
|
||||||
resp = httpx.get(_api(f"flows/get/{path}"), headers=_headers(), timeout=30)
|
|
||||||
resp.raise_for_status()
|
|
||||||
return json.dumps(resp.json(), indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_run_flow(path: str, args: str = "{}") -> str:
|
|
||||||
"""フローをトリガーして実行する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: フローのパス (例: u/antigravity/git_sync)
|
|
||||||
args: JSON形式の入力引数 (例: {"key": "value"})
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
args_dict = json.loads(args)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
return f"Error: argsのJSON形式が不正です: {e}"
|
|
||||||
|
|
||||||
resp = httpx.post(
|
|
||||||
_api(f"jobs/run/f/{path}"),
|
|
||||||
headers=_headers(),
|
|
||||||
json=args_dict,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
job_id = resp.text.strip().strip('"')
|
|
||||||
return (
|
|
||||||
f"フローを開始しました。\n"
|
|
||||||
f"ジョブID: {job_id}\n"
|
|
||||||
f"詳細URL: {WINDMILL_URL}/run/{job_id}?workspace={WINDMILL_WORKSPACE}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_list_recent_jobs(
|
|
||||||
limit: int = 20,
|
|
||||||
success_only: bool = False,
|
|
||||||
failure_only: bool = False,
|
|
||||||
script_path_filter: str = "",
|
|
||||||
) -> str:
|
|
||||||
"""最近のジョブ一覧を取得する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
limit: 取得件数(最大100)
|
|
||||||
success_only: Trueにすると成功ジョブのみ表示
|
|
||||||
failure_only: Trueにすると失敗ジョブのみ表示
|
|
||||||
script_path_filter: パスで絞り込む (例: u/antigravity/git_sync)
|
|
||||||
"""
|
|
||||||
params: dict = {"per_page": min(limit, 100)}
|
|
||||||
if success_only:
|
|
||||||
params["success"] = "true"
|
|
||||||
if failure_only:
|
|
||||||
params["success"] = "false"
|
|
||||||
if script_path_filter:
|
|
||||||
params["script_path_filter"] = script_path_filter
|
|
||||||
|
|
||||||
resp = httpx.get(_api("jobs/list"), headers=_headers(), params=params, timeout=30)
|
|
||||||
resp.raise_for_status()
|
|
||||||
jobs = resp.json()
|
|
||||||
if not jobs:
|
|
||||||
return "ジョブが見つかりませんでした"
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
for j in jobs:
|
|
||||||
success = j.get("success")
|
|
||||||
if success is True:
|
|
||||||
status = "[OK]"
|
|
||||||
elif success is False:
|
|
||||||
status = "[FAIL]"
|
|
||||||
else:
|
|
||||||
status = "[RUNNING]"
|
|
||||||
path = j.get("script_path", "unknown")
|
|
||||||
started = (j.get("started_at") or "")[:19] or "pending"
|
|
||||||
job_id = j.get("id", "")
|
|
||||||
lines.append(f"{status} [{started}] {path} (ID: {job_id})")
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_get_job_logs(job_id: str) -> str:
|
|
||||||
"""ジョブの詳細情報とログを取得する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job_id: ジョブのID(windmill_list_recent_jobs で確認できる)
|
|
||||||
"""
|
|
||||||
resp = httpx.get(_api(f"jobs_u/get/{job_id}"), headers=_headers(), timeout=30)
|
|
||||||
resp.raise_for_status()
|
|
||||||
job = resp.json()
|
|
||||||
|
|
||||||
success = job.get("success")
|
|
||||||
if success is True:
|
|
||||||
state = "成功 [OK]"
|
|
||||||
elif success is False:
|
|
||||||
state = "失敗 [FAIL]"
|
|
||||||
else:
|
|
||||||
state = "実行中 [RUNNING]"
|
|
||||||
|
|
||||||
result_parts = [
|
|
||||||
f"ジョブID: {job_id}",
|
|
||||||
f"パス: {job.get('script_path', 'N/A')}",
|
|
||||||
f"状態: {state}",
|
|
||||||
f"開始: {job.get('started_at', 'N/A')}",
|
|
||||||
f"終了: {job.get('created_at', 'N/A')}",
|
|
||||||
]
|
|
||||||
|
|
||||||
log_resp = httpx.get(
|
|
||||||
_api(f"jobs_u/getlogs/{job_id}"), headers=_headers(), timeout=30
|
|
||||||
)
|
|
||||||
if log_resp.status_code == 200:
|
|
||||||
result_parts.append("\n--- ログ ---")
|
|
||||||
result_parts.append(log_resp.text)
|
|
||||||
|
|
||||||
result_val = job.get("result")
|
|
||||||
if result_val is not None:
|
|
||||||
result_parts.append("\n--- 実行結果 ---")
|
|
||||||
result_parts.append(
|
|
||||||
json.dumps(result_val, indent=2, ensure_ascii=False)
|
|
||||||
if isinstance(result_val, (dict, list))
|
|
||||||
else str(result_val)
|
|
||||||
)
|
|
||||||
|
|
||||||
return "\n".join(result_parts)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_create_flow(path: str, summary: str, flow_definition: str, description: str = "") -> str:
|
|
||||||
"""新しいフローを作成する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: フローのパス (例: u/admin/my_flow)
|
|
||||||
summary: フローの概要
|
|
||||||
flow_definition: フローの定義 (JSON形式の文字列)
|
|
||||||
description: フローの詳細説明 (省略可)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
flow_value = json.loads(flow_definition)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
return f"Error: flow_definitionのJSON形式が不正です: {e}"
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"path": path,
|
|
||||||
"summary": summary,
|
|
||||||
"description": description,
|
|
||||||
"value": flow_value,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = httpx.post(
|
|
||||||
_api("flows/create"),
|
|
||||||
headers=_headers(),
|
|
||||||
json=payload,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
return (
|
|
||||||
f"フローを作成しました。\n"
|
|
||||||
f"パス: {path}\n"
|
|
||||||
f"URL: {WINDMILL_URL}/flows/edit/{path}?workspace={WINDMILL_WORKSPACE}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_update_flow(path: str, summary: str, flow_definition: str, description: str = "") -> str:
|
|
||||||
"""既存のフローを更新する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: フローのパス (例: u/admin/my_flow)
|
|
||||||
summary: フローの概要
|
|
||||||
flow_definition: フローの定義 (JSON形式の文字列)
|
|
||||||
description: フローの詳細説明 (省略可)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
flow_value = json.loads(flow_definition)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
return f"Error: flow_definitionのJSON形式が不正です: {e}"
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"path": path,
|
|
||||||
"summary": summary,
|
|
||||||
"description": description,
|
|
||||||
"value": flow_value,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = httpx.post(
|
|
||||||
_api(f"flows/update/{path}"),
|
|
||||||
headers=_headers(),
|
|
||||||
json=payload,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
return (
|
|
||||||
f"フローを更新しました。\n"
|
|
||||||
f"パス: {path}\n"
|
|
||||||
f"URL: {WINDMILL_URL}/flows/edit/{path}?workspace={WINDMILL_WORKSPACE}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_create_script(
|
|
||||||
path: str, language: str, content: str, summary: str = "", description: str = ""
|
|
||||||
) -> str:
|
|
||||||
"""新しいスクリプトを作成する(既存パスの場合は新バージョンを登録する)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: スクリプトのパス (例: u/admin/my_script)
|
|
||||||
language: 言語 (python3, deno, bun, bash など)
|
|
||||||
content: スクリプトのソースコード
|
|
||||||
summary: スクリプトの概要 (省略可)
|
|
||||||
description: スクリプトの詳細説明 (省略可)
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"path": path,
|
|
||||||
"language": language,
|
|
||||||
"content": content,
|
|
||||||
"summary": summary,
|
|
||||||
"description": description,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = httpx.post(
|
|
||||||
_api("scripts/create"),
|
|
||||||
headers=_headers(),
|
|
||||||
json=payload,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
hash_val = resp.text.strip().strip('"')
|
|
||||||
return (
|
|
||||||
f"スクリプトを作成しました。\n"
|
|
||||||
f"パス: {path}\n"
|
|
||||||
f"ハッシュ: {hash_val}\n"
|
|
||||||
f"URL: {WINDMILL_URL}/scripts/edit/{path}?workspace={WINDMILL_WORKSPACE}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_list_scripts(per_page: int = 20) -> str:
|
|
||||||
"""Windmill のスクリプト一覧を取得する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
per_page: 取得件数(最大100)
|
|
||||||
"""
|
|
||||||
resp = httpx.get(
|
|
||||||
_api("scripts/list"),
|
|
||||||
headers=_headers(),
|
|
||||||
params={"per_page": min(per_page, 100)},
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
scripts = resp.json()
|
|
||||||
if not scripts:
|
|
||||||
return "スクリプトが見つかりませんでした"
|
|
||||||
lines = [
|
|
||||||
f"- {s['path']} [{s.get('language', '?')}]: {s.get('summary', '(概要なし)')}"
|
|
||||||
for s in scripts
|
|
||||||
]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def windmill_get_script(path: str) -> str:
|
|
||||||
"""指定したパスのスクリプトのソースコードを取得する
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: スクリプトのパス (例: u/antigravity/test_git_sync)
|
|
||||||
"""
|
|
||||||
resp = httpx.get(_api(f"scripts/get/{path}"), headers=_headers(), timeout=30)
|
|
||||||
resp.raise_for_status()
|
|
||||||
script = resp.json()
|
|
||||||
|
|
||||||
result_parts = [
|
|
||||||
f"パス: {script.get('path', 'N/A')}",
|
|
||||||
f"言語: {script.get('language', 'N/A')}",
|
|
||||||
f"概要: {script.get('summary', 'N/A')}",
|
|
||||||
"",
|
|
||||||
"--- コード ---",
|
|
||||||
script.get("content", "(コードなし)"),
|
|
||||||
]
|
|
||||||
return "\n".join(result_parts)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
|
||||||
mcp.run(transport=transport)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Task Record Template
|
|
||||||
|
|
||||||
- task_id: TASK-YYYYMMDD-001
|
|
||||||
- date: YYYY-MM-DD
|
|
||||||
|
|
||||||
## Request (3 lines)
|
|
||||||
- Purpose:
|
|
||||||
- Target:
|
|
||||||
- Done:
|
|
||||||
|
|
||||||
## Changed Files
|
|
||||||
- path/to/file
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
- cmd:
|
|
||||||
- result: success/fail
|
|
||||||
- notes:
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
- L1:
|
|
||||||
- L2:
|
|
||||||
- L3:
|
|
||||||
|
|
||||||
## Risks / Next Actions
|
|
||||||
-
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Windmill Workflow Git Auto-Sync Script
|
|
||||||
# このスクリプトは、Windmillワークフローを自動的にGitにコミットします
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# 色付き出力
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'#!/bin/bash
|
|
||||||
|
|
||||||
# Windmill Workflow Git Auto-Sync Script for Gitea
|
|
||||||
# このスクリプトは、Windmillワークフローを自動的にGiteaにコミット&プッシュします
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# 色付き出力
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${GREEN}=== Windmill Workflow Git Sync (Gitea) ===${NC}"
|
|
||||||
|
|
||||||
# 作業ディレクトリに移動
|
|
||||||
cd /workspace
|
|
||||||
|
|
||||||
# PATHを設定
|
|
||||||
export PATH=~/.npm-global/bin:$PATH
|
|
||||||
|
|
||||||
# Git設定(safe.directoryエラー対策)
|
|
||||||
git config --global --add safe.directory /workspace
|
|
||||||
git config --global user.email "bot@example.com"
|
|
||||||
git config --global user.name "Windmill Bot"
|
|
||||||
|
|
||||||
# Windmillから最新を取得
|
|
||||||
echo -e "${YELLOW}Pulling from Windmill...${NC}"
|
|
||||||
wmill sync pull --skip-variables --skip-secrets --skip-resources --yes
|
|
||||||
|
|
||||||
# 変更があるか確認
|
|
||||||
if [[ -n $(git status --porcelain) ]]; then
|
|
||||||
echo -e "${YELLOW}Changes detected, committing to Git...${NC}"
|
|
||||||
|
|
||||||
# 変更をステージング
|
|
||||||
git add -A
|
|
||||||
|
|
||||||
# コミット
|
|
||||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
git commit -m "Auto-sync: ${TIMESTAMP}
|
|
||||||
|
|
||||||
Synced workflows from Windmill workspace"
|
|
||||||
|
|
||||||
|
|
||||||
# Giteaにプッシュ
|
|
||||||
echo -e "${YELLOW}Pushing to Gitea...${NC}"
|
|
||||||
git push origin main || {
|
|
||||||
echo -e "${RED}Failed to push to Gitea. Check credentials.${NC}"
|
|
||||||
# トークンや認証情報が設定されていない場合のヒント
|
|
||||||
echo -e "${YELLOW}Hint: Ensure you have set up git credentials or use a token in the remote URL.${NC}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo -e "${GREEN}✓ Changes pushed to Gitea${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${GREEN}✓ No changes detected${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}=== Sync Complete ===${NC}"
|
|
||||||
1
wmill_config/activeWorkspace
Normal file
1
wmill_config/activeWorkspace
Normal file
@@ -0,0 +1 @@
|
|||||||
|
admins
|
||||||
1
wmill_config/remotes.ndjson
Normal file
1
wmill_config/remotes.ndjson
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"}
|
||||||
1
wmill_config/windmill/activeWorkspace
Normal file
1
wmill_config/windmill/activeWorkspace
Normal file
@@ -0,0 +1 @@
|
|||||||
|
admins
|
||||||
1
wmill_config/windmill/remotes.ndjson
Normal file
1
wmill_config/windmill/remotes.ndjson
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"}
|
||||||
57
workflows/f/app_custom/system_heartbeat__flow/flow.yaml
Normal file
57
workflows/f/app_custom/system_heartbeat__flow/flow.yaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
summary: Windmill Heartbeat - システム自己診断
|
||||||
|
description: Windmillの動作確認用ワークフロー。UUID生成、時刻取得、計算チェック、HTTPヘルスチェック、年度判定を行い、全ステップの正常性を検証する。
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
summary: 'Step1: 診断データ生成'
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline step1:_診断データ生成.py'
|
||||||
|
input_transforms: {}
|
||||||
|
lock: '!inline step1:_診断データ生成.lock'
|
||||||
|
language: python3
|
||||||
|
- id: b
|
||||||
|
summary: 'Step2: データ検証'
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline step2:_データ検証.py'
|
||||||
|
input_transforms:
|
||||||
|
step1_result:
|
||||||
|
type: javascript
|
||||||
|
expr: results.a
|
||||||
|
lock: '!inline step2:_データ検証.lock'
|
||||||
|
language: python3
|
||||||
|
- id: c
|
||||||
|
summary: 'Step3: HTTPヘルスチェック'
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline step3:_httpヘルスチェック.py'
|
||||||
|
input_transforms:
|
||||||
|
verification_result:
|
||||||
|
type: javascript
|
||||||
|
expr: results.b
|
||||||
|
lock: '!inline step3:_httpヘルスチェック.lock'
|
||||||
|
language: python3
|
||||||
|
- id: d
|
||||||
|
summary: 'Step4: 年度判定 & 最終レポート'
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline step4:_年度判定_&_最終レポート.py'
|
||||||
|
input_transforms:
|
||||||
|
http_check:
|
||||||
|
type: javascript
|
||||||
|
expr: results.c
|
||||||
|
step1_data:
|
||||||
|
type: javascript
|
||||||
|
expr: results.a
|
||||||
|
verification:
|
||||||
|
type: javascript
|
||||||
|
expr: results.b
|
||||||
|
lock: '!inline step4:_年度判定_&_最終レポート.lock'
|
||||||
|
language: python3
|
||||||
|
schema:
|
||||||
|
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
||||||
|
type: object
|
||||||
|
order: []
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""診断データを生成する"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
run_id = str(uuid.uuid4())
|
||||||
|
check_value = 2 + 2
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"timestamp": now.isoformat(),
|
||||||
|
"run_id": run_id,
|
||||||
|
"check": check_value,
|
||||||
|
"python_version": __import__('sys').version
|
||||||
|
}
|
||||||
|
print(f"[Step1] 診断データ生成完了")
|
||||||
|
print(f" run_id: {run_id}")
|
||||||
|
print(f" timestamp: {now.isoformat()}")
|
||||||
|
print(f" check: {check_value}")
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# py: 3.12
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def main(step1_result: dict):
|
||||||
|
"""Step1の結果を検証する"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# 計算チェック
|
||||||
|
if step1_result.get("check") != 4:
|
||||||
|
errors.append(f"計算エラー: expected 4, got {step1_result.get('check')}")
|
||||||
|
|
||||||
|
# run_idの存在チェック
|
||||||
|
if not step1_result.get("run_id"):
|
||||||
|
errors.append("run_idが存在しない")
|
||||||
|
|
||||||
|
# timestampの存在チェック
|
||||||
|
if not step1_result.get("timestamp"):
|
||||||
|
errors.append("timestampが存在しない")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
error_msg = "; ".join(errors)
|
||||||
|
print(f"[Step2] 検証失敗: {error_msg}")
|
||||||
|
raise Exception(f"検証失敗: {error_msg}")
|
||||||
|
|
||||||
|
print(f"[Step2] データ検証OK")
|
||||||
|
print(f" 計算チェック: 2+2={step1_result['check']} ✓")
|
||||||
|
print(f" run_id: {step1_result['run_id']} ✓")
|
||||||
|
print(f" timestamp: {step1_result['timestamp']} ✓")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"verification": "PASS",
|
||||||
|
"step1_data": step1_result
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# py: 3.12
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import urllib.request
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
def main(verification_result: dict):
|
||||||
|
"""Windmillサーバー自身へのHTTPチェック"""
|
||||||
|
url = "https://windmill.keinafarm.net/api/version"
|
||||||
|
|
||||||
|
# SSL検証をスキップ(自己署名証明書対応)
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
with urllib.request.urlopen(req, context=ctx, timeout=10) as response:
|
||||||
|
status_code = response.status
|
||||||
|
body = response.read().decode('utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Step3] HTTPチェック失敗: {e}")
|
||||||
|
raise Exception(f"HTTPヘルスチェック失敗: {e}")
|
||||||
|
|
||||||
|
print(f"[Step3] HTTPヘルスチェックOK")
|
||||||
|
print(f" URL: {url}")
|
||||||
|
print(f" Status: {status_code}")
|
||||||
|
print(f" Version: {body}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"http_check": "PASS",
|
||||||
|
"status_code": status_code,
|
||||||
|
"server_version": body
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# py: 3.12
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def main(step1_data: dict, verification: dict, http_check: dict):
|
||||||
|
"""年度判定と最終診断レポートを生成"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# 日本の年度判定(4月始まり)
|
||||||
|
fiscal_year = now.year if now.month >= 4 else now.year - 1
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"status": "ALL OK",
|
||||||
|
"fiscal_year": fiscal_year,
|
||||||
|
"diagnostics": {
|
||||||
|
"data_generation": "PASS",
|
||||||
|
"data_verification": verification.get("verification", "UNKNOWN"),
|
||||||
|
"http_health": http_check.get("http_check", "UNKNOWN"),
|
||||||
|
"server_version": http_check.get("server_version", "UNKNOWN")
|
||||||
|
},
|
||||||
|
"run_id": step1_data.get("run_id"),
|
||||||
|
"started_at": step1_data.get("timestamp"),
|
||||||
|
"completed_at": now.isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("========================================")
|
||||||
|
print(" Windmill Heartbeat - 診断レポート")
|
||||||
|
print("========================================")
|
||||||
|
print(f" Status: {report['status']}")
|
||||||
|
print(f" 年度: {fiscal_year}年度")
|
||||||
|
print(f" Run ID: {report['run_id']}")
|
||||||
|
print(f" Server: {report['diagnostics']['server_version']}")
|
||||||
|
print(f" 開始: {report['started_at']}")
|
||||||
|
print(f" 完了: {report['completed_at']}")
|
||||||
|
print(" ────────────────────────────────────")
|
||||||
|
print(f" データ生成: PASS ✓")
|
||||||
|
print(f" データ検証: {report['diagnostics']['data_verification']} ✓")
|
||||||
|
print(f" HTTP確認: {report['diagnostics']['http_health']} ✓")
|
||||||
|
print("========================================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return report
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# py: 3.12
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def main(
|
||||||
|
task_contract: dict[str, Any],
|
||||||
|
steps: list[dict[str, Any]],
|
||||||
|
context: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Standalone Windmill runner flow for Butler delegated step execution.
|
||||||
|
|
||||||
|
This file is intentionally self-contained so it can be pasted or synced to
|
||||||
|
Windmill without requiring the Butler repository on the worker.
|
||||||
|
"""
|
||||||
|
timeout_sec = _resolve_timeout_sec(task_contract)
|
||||||
|
resolved_context = dict(context or {})
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for idx, raw_step in enumerate(steps):
|
||||||
|
result = _execute_step(raw_step, idx, timeout_sec, resolved_context)
|
||||||
|
|
||||||
|
if raw_step.get("kind") in {"cmd", "check"} and result["exit_code"] != 0:
|
||||||
|
err_type = _classify_error(str(raw_step.get("value", "")), result["stderr"] or result["stdout"])
|
||||||
|
if err_type == "transient":
|
||||||
|
time.sleep(30)
|
||||||
|
result = _execute_step(raw_step, idx, timeout_sec, resolved_context)
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
if result["exit_code"] != 0:
|
||||||
|
evidence = _build_evidence(results)
|
||||||
|
evidence["ok"] = False
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"summary": _failure_summary(raw_step, result),
|
||||||
|
"failed_step_index": idx,
|
||||||
|
"step_results": results,
|
||||||
|
"evidence": evidence,
|
||||||
|
}
|
||||||
|
|
||||||
|
evidence = _build_evidence(results)
|
||||||
|
evidence["ok"] = True
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"summary": f"Executed {len(results)} step(s) successfully.",
|
||||||
|
"step_results": results,
|
||||||
|
"evidence": evidence,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_timeout_sec(task_contract: dict[str, Any]) -> int:
|
||||||
|
constraints = task_contract.get("constraints", {})
|
||||||
|
max_minutes = constraints.get("max_minutes", 1)
|
||||||
|
try:
|
||||||
|
return max(1, int(max_minutes) * 60)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 60
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_step(
|
||||||
|
step: dict[str, Any],
|
||||||
|
step_index: int,
|
||||||
|
timeout_sec: int,
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
kind = str(step.get("kind", "")).strip()
|
||||||
|
value = str(step.get("value", "") or "")
|
||||||
|
|
||||||
|
if kind == "wait":
|
||||||
|
started = time.perf_counter()
|
||||||
|
seconds = _parse_wait_seconds(value)
|
||||||
|
time.sleep(seconds)
|
||||||
|
duration_ms = int((time.perf_counter() - started) * 1000)
|
||||||
|
return _step_result(step_index, kind, value, 0, "", "", duration_ms)
|
||||||
|
|
||||||
|
if kind == "mcp_call":
|
||||||
|
return _execute_mcp_call(step, step_index, timeout_sec, context)
|
||||||
|
|
||||||
|
started = time.perf_counter()
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
value,
|
||||||
|
shell=True,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=timeout_sec,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
duration_ms = int((time.perf_counter() - started) * 1000)
|
||||||
|
return _step_result(
|
||||||
|
step_index,
|
||||||
|
kind,
|
||||||
|
value,
|
||||||
|
proc.returncode,
|
||||||
|
proc.stdout,
|
||||||
|
proc.stderr,
|
||||||
|
duration_ms,
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
duration_ms = int((time.perf_counter() - started) * 1000)
|
||||||
|
stdout = exc.stdout if isinstance(exc.stdout, str) else ""
|
||||||
|
return _step_result(
|
||||||
|
step_index,
|
||||||
|
kind,
|
||||||
|
value,
|
||||||
|
124,
|
||||||
|
stdout,
|
||||||
|
f"timeout after {timeout_sec}s",
|
||||||
|
duration_ms,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_mcp_call(
|
||||||
|
step: dict[str, Any],
|
||||||
|
step_index: int,
|
||||||
|
timeout_sec: int,
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Placeholder for future Windmill-side MCP execution.
|
||||||
|
|
||||||
|
The first real connectivity test uses `check` steps, so we keep the
|
||||||
|
deployment artifact dependency-free for now and fail explicitly if a flow
|
||||||
|
attempts `mcp_call`.
|
||||||
|
"""
|
||||||
|
_ = timeout_sec, context
|
||||||
|
server = str(step.get("server", "") or "").strip()
|
||||||
|
tool = str(step.get("tool", "") or "").strip()
|
||||||
|
return _step_result(
|
||||||
|
step_index,
|
||||||
|
"mcp_call",
|
||||||
|
tool,
|
||||||
|
1,
|
||||||
|
"",
|
||||||
|
f"mcp_call is not supported in the standalone Windmill runner yet (server={server}, tool={tool})",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _step_result(
|
||||||
|
step_index: int,
|
||||||
|
kind: str,
|
||||||
|
value: str,
|
||||||
|
exit_code: int,
|
||||||
|
stdout: str,
|
||||||
|
stderr: str,
|
||||||
|
duration_ms: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"step_index": step_index,
|
||||||
|
"kind": kind,
|
||||||
|
"value": value,
|
||||||
|
"exit_code": exit_code,
|
||||||
|
"stdout": stdout,
|
||||||
|
"stderr": stderr,
|
||||||
|
"duration_ms": duration_ms,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_evidence(results: list[dict[str, Any]]) -> dict[str, Any]:
|
||||||
|
executed_commands = [str(result.get("value", "")) for result in results]
|
||||||
|
key_outputs: list[str] = []
|
||||||
|
error_lines: list[str] = []
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
stdout = str(result.get("stdout", "") or "")
|
||||||
|
stderr = str(result.get("stderr", "") or "")
|
||||||
|
if stdout:
|
||||||
|
key_outputs.extend(stdout.splitlines()[:5])
|
||||||
|
if stderr:
|
||||||
|
lines = stderr.splitlines()
|
||||||
|
error_lines.extend(lines[:5])
|
||||||
|
if len(lines) > 5:
|
||||||
|
error_lines.extend(lines[-5:])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"executed_commands": executed_commands,
|
||||||
|
"key_outputs": key_outputs,
|
||||||
|
"error_head_tail": "\n".join(error_lines) if error_lines else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _failure_summary(step: dict[str, Any], result: dict[str, Any]) -> str:
|
||||||
|
kind = str(step.get("kind", "") or "")
|
||||||
|
stderr = str(result.get("stderr", "") or "")
|
||||||
|
stdout = str(result.get("stdout", "") or "")
|
||||||
|
if kind == "mcp_call":
|
||||||
|
return stderr or stdout or "mcp_call failed."
|
||||||
|
return stderr or stdout or f"{kind} step failed."
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_error(command: str, output: str) -> str:
|
||||||
|
lowered = (command + "\n" + output).lower()
|
||||||
|
transient_markers = [
|
||||||
|
"timeout",
|
||||||
|
"timed out",
|
||||||
|
"temporarily unavailable",
|
||||||
|
"connection reset",
|
||||||
|
"connection aborted",
|
||||||
|
"connection refused",
|
||||||
|
"503",
|
||||||
|
"502",
|
||||||
|
"rate limit",
|
||||||
|
]
|
||||||
|
for marker in transient_markers:
|
||||||
|
if marker in lowered:
|
||||||
|
return "transient"
|
||||||
|
return "permanent"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_wait_seconds(value: str) -> float:
|
||||||
|
normalized = value.strip().lower()
|
||||||
|
if re.fullmatch(r"\d+(\.\d+)?s", normalized):
|
||||||
|
return float(normalized[:-1])
|
||||||
|
if re.fullmatch(r"\d+(\.\d+)?", normalized):
|
||||||
|
return float(normalized)
|
||||||
|
raise ValueError(f"Invalid wait value: {value}")
|
||||||
47
workflows/f/butler/execute_task_steps__flow/flow.yaml
Normal file
47
workflows/f/butler/execute_task_steps__flow/flow.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
summary: Butler generic runner - delegated step execution
|
||||||
|
description: >-
|
||||||
|
Receives a serialized TaskContract and resolved step list from Butler,
|
||||||
|
executes steps server-side with Butler-compatible semantics
|
||||||
|
(cmd/check/wait/retry), and returns ok/summary/step_results/evidence.
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
summary: Execute Butler task steps
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline execute_butler_task_steps.py'
|
||||||
|
input_transforms:
|
||||||
|
context:
|
||||||
|
type: javascript
|
||||||
|
expr: flow_input.context
|
||||||
|
steps:
|
||||||
|
type: javascript
|
||||||
|
expr: flow_input.steps
|
||||||
|
task_contract:
|
||||||
|
type: javascript
|
||||||
|
expr: flow_input.task_contract
|
||||||
|
lock: '!inline execute_butler_task_steps.lock'
|
||||||
|
language: python3
|
||||||
|
schema:
|
||||||
|
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
||||||
|
type: object
|
||||||
|
order:
|
||||||
|
- task_contract
|
||||||
|
- steps
|
||||||
|
- context
|
||||||
|
properties:
|
||||||
|
context:
|
||||||
|
type: object
|
||||||
|
description: 'Execution context (target, payload)'
|
||||||
|
default: {}
|
||||||
|
steps:
|
||||||
|
type: array
|
||||||
|
description: Resolved SOP step list
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
task_contract:
|
||||||
|
type: object
|
||||||
|
description: Serialized Butler TaskContract
|
||||||
|
required:
|
||||||
|
- task_contract
|
||||||
|
- steps
|
||||||
1
workflows/f/dev/konnnichiha__flow/a.lock
Normal file
1
workflows/f/dev/konnnichiha__flow/a.lock
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# py: 3.12
|
||||||
2
workflows/f/dev/konnnichiha__flow/a.py
Normal file
2
workflows/f/dev/konnnichiha__flow/a.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def main():
|
||||||
|
print('こんにちは、世界')
|
||||||
12
workflows/f/dev/konnnichiha__flow/flow.yaml
Normal file
12
workflows/f/dev/konnnichiha__flow/flow.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
summary: Print greeting
|
||||||
|
description: ''
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline a.py'
|
||||||
|
input_transforms: {}
|
||||||
|
lock: '!inline a.lock'
|
||||||
|
language: python3
|
||||||
|
schema: null
|
||||||
1
workflows/f/dev/textout__flow/a.lock
Normal file
1
workflows/f/dev/textout__flow/a.lock
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# py: 3.12
|
||||||
3
workflows/f/dev/textout__flow/a.py
Normal file
3
workflows/f/dev/textout__flow/a.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
def main():
|
||||||
|
from datetime import datetime
|
||||||
|
print(datetime.now().strftime('%H:%M:%S'))
|
||||||
12
workflows/f/dev/textout__flow/flow.yaml
Normal file
12
workflows/f/dev/textout__flow/flow.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
summary: Display current time on startup
|
||||||
|
description: ''
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline a.py'
|
||||||
|
input_transforms: {}
|
||||||
|
lock: '!inline a.lock'
|
||||||
|
language: python3
|
||||||
|
schema: null
|
||||||
19
workflows/f/mail/mail_filter__flow/flow.yaml
Normal file
19
workflows/f/mail/mail_filter__flow/flow.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
summary: メールフィルタリング
|
||||||
|
description: >-
|
||||||
|
IMAPで新着メールを受信し、送信者ルール確認→LLM判定→LINE通知を行う。Keinasystemと連携。Gmail→Hotmail→Xserverの順で段階的に有効化する。
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
summary: メール取得・判定・通知
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline メール取得・判定・通知.py'
|
||||||
|
input_transforms: {}
|
||||||
|
lock: '!inline メール取得・判定・通知.lock'
|
||||||
|
language: python3
|
||||||
|
schema:
|
||||||
|
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
||||||
|
type: object
|
||||||
|
order: []
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
9
workflows/f/mail/mail_filter__flow/メール取得・判定・通知.lock
Normal file
9
workflows/f/mail/mail_filter__flow/メール取得・判定・通知.lock
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# py: 3.12
|
||||||
|
anyio==4.12.1
|
||||||
|
certifi==2026.1.4
|
||||||
|
h11==0.16.0
|
||||||
|
httpcore==1.0.9
|
||||||
|
httpx==0.28.1
|
||||||
|
idna==3.11
|
||||||
|
typing-extensions==4.15.0
|
||||||
|
wmill==1.642.0
|
||||||
575
workflows/f/mail/mail_filter__flow/メール取得・判定・通知.py
Normal file
575
workflows/f/mail/mail_filter__flow/メール取得・判定・通知.py
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
import imaplib
|
||||||
|
import email
|
||||||
|
import email.header
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from email.utils import parsedate_to_datetime
|
||||||
|
import wmill
|
||||||
|
|
||||||
|
JST = timezone(timedelta(hours=9))
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# アカウント設定
|
||||||
|
# 新しいアカウントを追加する際は enabled: True にする
|
||||||
|
# ============================================================
|
||||||
|
ACCOUNTS = [
|
||||||
|
{
|
||||||
|
"name": "gmail",
|
||||||
|
"account_code": "gmail",
|
||||||
|
"host": "imap.gmail.com",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/GMAIL_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/GMAIL_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_GMAIL_LAST_UID",
|
||||||
|
"mailbox": "[Gmail]/All Mail",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gmail_service",
|
||||||
|
"account_code": "gmail_service",
|
||||||
|
"host": "imap.gmail.com",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/GMAIL2_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/GMAIL2_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_GMAIL2_LAST_UID",
|
||||||
|
"mailbox": "[Gmail]/All Mail",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
# Hotmail テスト後に有効化
|
||||||
|
# {
|
||||||
|
# "name": "hotmail",
|
||||||
|
# "account_code": "hotmail",
|
||||||
|
# "host": "outlook.office365.com",
|
||||||
|
# "port": 993,
|
||||||
|
# "user_var": "u/admin/HOTMAIL_IMAP_USER",
|
||||||
|
# "pass_var": "u/admin/HOTMAIL_IMAP_PASSWORD",
|
||||||
|
# "last_uid_var": "u/admin/MAIL_FILTER_HOTMAIL_LAST_UID",
|
||||||
|
# "enabled": False,
|
||||||
|
# },
|
||||||
|
# Xserver (keinafarm.com) 6アカウント
|
||||||
|
{
|
||||||
|
"name": "xserver_akiracraftwork",
|
||||||
|
"account_code": "xserver1",
|
||||||
|
"host": "sv579.xserver.jp",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/XSERVER1_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/XSERVER1_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_XSERVER1_LAST_UID",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xserver_service",
|
||||||
|
"account_code": "xserver2",
|
||||||
|
"host": "sv579.xserver.jp",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/XSERVER2_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/XSERVER2_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_XSERVER2_LAST_UID",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xserver_midori",
|
||||||
|
"account_code": "xserver3",
|
||||||
|
"host": "sv579.xserver.jp",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/XSERVER3_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/XSERVER3_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_XSERVER3_LAST_UID",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xserver_kouseiren",
|
||||||
|
"account_code": "xserver4",
|
||||||
|
"host": "sv579.xserver.jp",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/XSERVER4_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/XSERVER4_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_XSERVER4_LAST_UID",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xserver_post",
|
||||||
|
"account_code": "xserver5",
|
||||||
|
"host": "sv579.xserver.jp",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/XSERVER5_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/XSERVER5_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_XSERVER5_LAST_UID",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xserver_sales",
|
||||||
|
"account_code": "xserver6",
|
||||||
|
"host": "sv579.xserver.jp",
|
||||||
|
"port": 993,
|
||||||
|
"user_var": "u/admin/XSERVER6_IMAP_USER",
|
||||||
|
"pass_var": "u/admin/XSERVER6_IMAP_PASSWORD",
|
||||||
|
"last_uid_var": "u/admin/MAIL_FILTER_XSERVER6_LAST_UID",
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 共通変数取得
|
||||||
|
api_key = wmill.get_variable("u/admin/KEINASYSTEM_API_KEY")
|
||||||
|
api_url = wmill.get_variable("u/admin/KEINASYSTEM_API_URL").rstrip("/")
|
||||||
|
gemini_key = wmill.get_variable("u/admin/GEMINI_API_KEY")
|
||||||
|
line_token = wmill.get_variable("u/admin/LINE_CHANNEL_ACCESS_TOKEN")
|
||||||
|
line_to = wmill.get_variable("u/admin/LINE_TO")
|
||||||
|
|
||||||
|
total_processed = 0
|
||||||
|
total_notified = 0
|
||||||
|
|
||||||
|
for account in ACCOUNTS:
|
||||||
|
if not account["enabled"]:
|
||||||
|
continue
|
||||||
|
print(f"[{account['name']}] 処理開始")
|
||||||
|
try:
|
||||||
|
processed, notified = process_account(
|
||||||
|
account, api_key, api_url, gemini_key, line_token, line_to
|
||||||
|
)
|
||||||
|
total_processed += processed
|
||||||
|
total_notified += notified
|
||||||
|
print(f"[{account['name']}] 処理完了: {processed}件処理, {notified}件通知")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{account['name']}] エラー: {e}")
|
||||||
|
# 1アカウントが失敗しても他のアカウントは継続
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_processed": total_processed,
|
||||||
|
"total_notified": total_notified,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def process_account(account, api_key, api_url, gemini_key, line_token, line_to):
|
||||||
|
user = wmill.get_variable(account["user_var"])
|
||||||
|
password = wmill.get_variable(account["pass_var"])
|
||||||
|
|
||||||
|
# 前回の最終UID取得
|
||||||
|
try:
|
||||||
|
last_uid_str = wmill.get_variable(account["last_uid_var"])
|
||||||
|
last_uid = int(last_uid_str) if last_uid_str and last_uid_str != "0" else None
|
||||||
|
except Exception:
|
||||||
|
last_uid = None
|
||||||
|
|
||||||
|
# IMAP接続
|
||||||
|
ssl_ctx = ssl.create_default_context()
|
||||||
|
mail = imaplib.IMAP4_SSL(account["host"], account["port"], ssl_context=ssl_ctx)
|
||||||
|
mail.login(user, password)
|
||||||
|
mailbox = account.get("mailbox", "INBOX")
|
||||||
|
imap_mailbox = resolve_mailbox(mail, mailbox)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if last_uid is None:
|
||||||
|
# 初回実行: 現在の最大UIDを記録して終了(既存メールは処理しない)
|
||||||
|
_, data = mail.uid("SEARCH", None, "ALL")
|
||||||
|
all_uids = data[0].split() if data[0] else []
|
||||||
|
max_uid = int(all_uids[-1]) if all_uids else 0
|
||||||
|
wmill.set_variable(account["last_uid_var"], str(max_uid))
|
||||||
|
print(f"[{account['name']}] 初回実行: 最大UID={max_uid} を記録、既存メールはスキップ")
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
# last_uid より大きい UID を検索
|
||||||
|
search_criterion = f"UID {last_uid + 1}:*"
|
||||||
|
_, data = mail.uid("SEARCH", None, search_criterion)
|
||||||
|
raw_uids = data[0].split() if data[0] else []
|
||||||
|
new_uids = [u for u in raw_uids if int(u) > last_uid]
|
||||||
|
|
||||||
|
if not new_uids:
|
||||||
|
print(f"[{account['name']}] 新着メールなし")
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
print(f"[{account['name']}] 新着{len(new_uids)}件")
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
notified = 0
|
||||||
|
max_processed_uid = last_uid
|
||||||
|
|
||||||
|
for uid_bytes in new_uids:
|
||||||
|
uid = int(uid_bytes)
|
||||||
|
try:
|
||||||
|
result = process_message(
|
||||||
|
mail, uid, account,
|
||||||
|
api_key, api_url, gemini_key, line_token, line_to
|
||||||
|
)
|
||||||
|
processed += 1
|
||||||
|
if result == "notified":
|
||||||
|
notified += 1
|
||||||
|
max_processed_uid = max(max_processed_uid, uid)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{account['name']}] UID={uid} 処理エラー: {e}")
|
||||||
|
# 個別メッセージのエラーは継続、UIDは進めない
|
||||||
|
|
||||||
|
# 処理済み最大UIDを保存(正常完了時のみ)
|
||||||
|
if max_processed_uid > last_uid:
|
||||||
|
wmill.set_variable(account["last_uid_var"], str(max_processed_uid))
|
||||||
|
|
||||||
|
return processed, notified
|
||||||
|
finally:
|
||||||
|
mail.logout()
|
||||||
|
|
||||||
|
|
||||||
|
def process_message(mail, uid, account, api_key, api_url, gemini_key, line_token, line_to):
|
||||||
|
"""メッセージを1通処理。戻り値: 'skipped' / 'not_important' / 'notified'"""
|
||||||
|
account_code = account["account_code"]
|
||||||
|
forwarding_map = account.get("forwarding_map", {})
|
||||||
|
recipient_map = {
|
||||||
|
"akira@keinafarm.com": "xserver1",
|
||||||
|
"service@keinafarm.com": "xserver2",
|
||||||
|
"midori@keinafarm.com": "xserver3",
|
||||||
|
"kouseiren@keinafarm.com": "xserver4",
|
||||||
|
"post@keinafarm.com": "xserver5",
|
||||||
|
"sales@keinafarm.com": "xserver6",
|
||||||
|
}
|
||||||
|
|
||||||
|
# メール取得
|
||||||
|
_, data = mail.uid("FETCH", str(uid), "(RFC822)")
|
||||||
|
if not data or not data[0]:
|
||||||
|
return "skipped"
|
||||||
|
|
||||||
|
raw_email = data[0][1]
|
||||||
|
msg = email.message_from_bytes(raw_email)
|
||||||
|
|
||||||
|
# ヘッダー解析
|
||||||
|
message_id = msg.get("Message-ID", "").strip()
|
||||||
|
if not message_id:
|
||||||
|
message_id = f"{account_code}-uid-{uid}"
|
||||||
|
|
||||||
|
sender_raw = msg.get("From", "")
|
||||||
|
sender_email_addr = extract_email_address(sender_raw)
|
||||||
|
sender_domain = sender_email_addr.split("@")[-1] if "@" in sender_email_addr else ""
|
||||||
|
|
||||||
|
subject = decode_header_value(msg.get("Subject", "(件名なし)"))
|
||||||
|
|
||||||
|
date_str = msg.get("Date", "")
|
||||||
|
try:
|
||||||
|
received_at = parsedate_to_datetime(date_str).isoformat()
|
||||||
|
except Exception:
|
||||||
|
received_at = datetime.now(JST).isoformat()
|
||||||
|
|
||||||
|
body_preview = extract_body_preview(msg, max_chars=500)
|
||||||
|
|
||||||
|
# 宛先補正: To:ヘッダーから account_code を補正(転送/重複受信時の誤判定防止)
|
||||||
|
to_raw = msg.get("To", "")
|
||||||
|
if to_raw:
|
||||||
|
to_addr = extract_email_address(to_raw)
|
||||||
|
to_domain = to_addr.split("@")[-1] if "@" in to_addr else ""
|
||||||
|
mapped = forwarding_map.get(to_addr) or forwarding_map.get(to_domain) or recipient_map.get(to_addr)
|
||||||
|
if mapped:
|
||||||
|
account_code = mapped
|
||||||
|
print(f" [宛先補正] To:{to_addr} → account: {account_code}")
|
||||||
|
|
||||||
|
print(f" From: {sender_email_addr}, Subject: {subject[:50]}")
|
||||||
|
|
||||||
|
# --- ステップ1: 送信者ルール確認 ---
|
||||||
|
rule_result = call_api_get(api_key, api_url, "/api/mail/sender-rule/", {
|
||||||
|
"email": sender_email_addr,
|
||||||
|
"domain": sender_domain,
|
||||||
|
})
|
||||||
|
|
||||||
|
if rule_result.get("matched"):
|
||||||
|
rule = rule_result["rule"]
|
||||||
|
|
||||||
|
if rule == "never_notify":
|
||||||
|
print(f" → never_notify ルール一致、スキップ")
|
||||||
|
return "skipped"
|
||||||
|
|
||||||
|
elif rule == "always_notify":
|
||||||
|
print(f" → always_notify ルール一致、即通知")
|
||||||
|
result = post_email(api_key, api_url, {
|
||||||
|
"account": account_code,
|
||||||
|
"message_id": message_id,
|
||||||
|
"sender_email": sender_email_addr,
|
||||||
|
"sender_domain": sender_domain,
|
||||||
|
"subject": subject,
|
||||||
|
"body_preview": body_preview,
|
||||||
|
"received_at": received_at,
|
||||||
|
"llm_verdict": "important",
|
||||||
|
})
|
||||||
|
if result.get("feedback_url"):
|
||||||
|
send_line_notification(line_token, line_to, account_code, sender_email_addr, subject, result["feedback_url"])
|
||||||
|
return "notified"
|
||||||
|
return "skipped"
|
||||||
|
|
||||||
|
# --- ステップ2: LLM判定 ---
|
||||||
|
context = call_api_get(api_key, api_url, "/api/mail/sender-context/", {
|
||||||
|
"email": sender_email_addr,
|
||||||
|
"domain": sender_domain,
|
||||||
|
})
|
||||||
|
verdict = judge_with_llm(gemini_key, sender_email_addr, subject, body_preview, context)
|
||||||
|
print(f" → LLM判定: {verdict}")
|
||||||
|
|
||||||
|
# --- ステップ3: Keinasystemに記録 ---
|
||||||
|
result = post_email(api_key, api_url, {
|
||||||
|
"account": account_code,
|
||||||
|
"message_id": message_id,
|
||||||
|
"sender_email": sender_email_addr,
|
||||||
|
"sender_domain": sender_domain,
|
||||||
|
"subject": subject,
|
||||||
|
"body_preview": body_preview,
|
||||||
|
"received_at": received_at,
|
||||||
|
"llm_verdict": verdict,
|
||||||
|
})
|
||||||
|
|
||||||
|
if verdict == "important" and result.get("feedback_url"):
|
||||||
|
send_line_notification(line_token, line_to, account_code, sender_email_addr, subject, result["feedback_url"])
|
||||||
|
return "notified"
|
||||||
|
|
||||||
|
return "not_important"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# メールボックス解決
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def resolve_mailbox(mail, mailbox):
|
||||||
|
"""メールボックスを選択し SELECT する。
|
||||||
|
INBOX はそのまま、それ以外は指定名 -> \\All 属性でフォールバック。
|
||||||
|
"""
|
||||||
|
if mailbox == "INBOX":
|
||||||
|
typ, data = mail.select("INBOX")
|
||||||
|
if typ != 'OK':
|
||||||
|
raise Exception(f"SELECT INBOX failed: {data}")
|
||||||
|
return "INBOX"
|
||||||
|
|
||||||
|
# まず指定名で試行
|
||||||
|
imap_name = '"' + mailbox + '"'
|
||||||
|
typ, data = mail.select(imap_name)
|
||||||
|
if typ == 'OK':
|
||||||
|
return imap_name
|
||||||
|
|
||||||
|
# 失敗した場合: \\All 属性を持つメールボックスを自動検出
|
||||||
|
print(f" [INFO] {mailbox} not found, searching for \\\\All mailbox...")
|
||||||
|
typ2, mboxes = mail.list()
|
||||||
|
if typ2 == 'OK':
|
||||||
|
for mb in mboxes:
|
||||||
|
if not mb:
|
||||||
|
continue
|
||||||
|
mb_str = mb.decode() if isinstance(mb, bytes) else mb
|
||||||
|
if '\\\\All' in mb_str or '\\All' in mb_str:
|
||||||
|
# "(attrs) \".\" \"name\"" 形式から名前を抽出
|
||||||
|
parts = mb_str.rsplit('"', 2)
|
||||||
|
if len(parts) >= 2 and parts[-2]:
|
||||||
|
found = parts[-2]
|
||||||
|
else:
|
||||||
|
found = mb_str.split()[-1].strip('"')
|
||||||
|
print(f" [INFO] Found All Mail mailbox: {found}")
|
||||||
|
imap_found = '"' + found + '"'
|
||||||
|
typ3, data3 = mail.select(imap_found)
|
||||||
|
if typ3 == 'OK':
|
||||||
|
return imap_found
|
||||||
|
raise Exception(f"Could not select any All Mail mailbox (tried: {mailbox})")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# APIヘルパー
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _make_ssl_ctx():
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
def call_api_get(api_key, api_url, path, params):
|
||||||
|
qs = urllib.parse.urlencode(params)
|
||||||
|
url = f"{api_url}{path}?{qs}"
|
||||||
|
req = urllib.request.Request(url, headers={"X-API-Key": api_key})
|
||||||
|
with urllib.request.urlopen(req, context=_make_ssl_ctx(), timeout=10) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def post_email(api_key, api_url, data):
|
||||||
|
url = f"{api_url}/api/mail/emails/"
|
||||||
|
payload = json.dumps(data).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=payload,
|
||||||
|
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, context=_make_ssl_ctx(), timeout=10) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode("utf-8")
|
||||||
|
if e.code == 400 and "message_id" in body:
|
||||||
|
# 重複message_idは正常(再実行時の冦殁)
|
||||||
|
print(f" 重複メール、スキップ")
|
||||||
|
return {}
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNT_LABELS = {
|
||||||
|
"gmail": "Gmail (メイン)",
|
||||||
|
"gmail_service": "Gmail (サービス用)",
|
||||||
|
"hotmail": "Hotmail",
|
||||||
|
"xserver1": "Xserver (akira@keinafarm.com)",
|
||||||
|
"xserver2": "Xserver (service@keinafarm.com)",
|
||||||
|
"xserver3": "Xserver (midori@keinafarm.com)",
|
||||||
|
"xserver4": "Xserver (kouseiren@keinafarm.com)",
|
||||||
|
"xserver5": "Xserver (post@keinafarm.com)",
|
||||||
|
"xserver6": "Xserver (sales@keinafarm.com)",
|
||||||
|
"xserver": "Xserver",
|
||||||
|
}
|
||||||
|
|
||||||
|
def send_line_notification(line_token, line_to, account_code, sender_email_addr, subject, feedback_url):
|
||||||
|
account_label = ACCOUNT_LABELS.get(account_code, account_code)
|
||||||
|
message = (
|
||||||
|
f"📧 重要なメールが届きました\n\n"
|
||||||
|
f"宛先: {account_label}\n"
|
||||||
|
f"差出人: {sender_email_addr}\n"
|
||||||
|
f"件名: {subject}\n\n"
|
||||||
|
f"フィードバック:\n{feedback_url}"
|
||||||
|
)
|
||||||
|
payload = json.dumps({
|
||||||
|
"to": line_to,
|
||||||
|
"messages": [{"type": "text", "text": message}],
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://api.line.me/v2/bot/message/push",
|
||||||
|
data=payload,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {line_token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
resp.read()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# LLM判定(Gemini API)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def judge_with_llm(gemini_key, sender_email_addr, subject, body_preview, context):
|
||||||
|
"""農家にとって重要なメールか判定。'important' または 'not_important' を返す。"""
|
||||||
|
|
||||||
|
context_text = ""
|
||||||
|
total = context.get("total_notified", 0)
|
||||||
|
if total > 0:
|
||||||
|
context_text = (
|
||||||
|
f"\n\n[この送信者の過去データ] "
|
||||||
|
f"通知済み{total}件: "
|
||||||
|
f"重要{context.get('important', 0)}件 / "
|
||||||
|
f"普通{context.get('not_important', 0)}件 / "
|
||||||
|
f"通知不要{context.get('never_notify', 0)}件 / "
|
||||||
|
f"未評価{context.get('no_feedback', 0)}件"
|
||||||
|
)
|
||||||
|
|
||||||
|
user_message = (
|
||||||
|
f"送信者: {sender_email_addr}\n"
|
||||||
|
f"件名: {subject}\n"
|
||||||
|
f"本文冠頭:\n{body_preview}"
|
||||||
|
f"{context_text}\n\n"
|
||||||
|
f"このメールは農家にとって重要ですか?\n"
|
||||||
|
f"1: 重要(要確認)\n"
|
||||||
|
f"2: 重要でない(営業・通知等)\n"
|
||||||
|
f"数字1文字のみで答えてください。"
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"system_instruction": {
|
||||||
|
"parts": [{"text": "あなたは農家のメールフィルタリングアシスタントです。メールが重要かどうかを判定してください。"}]
|
||||||
|
},
|
||||||
|
"contents": [{
|
||||||
|
"role": "user",
|
||||||
|
"parts": [{"text": user_message}]
|
||||||
|
}],
|
||||||
|
"generationConfig": {
|
||||||
|
"maxOutputTokens": 10,
|
||||||
|
"temperature": 0
|
||||||
|
}
|
||||||
|
}).encode("utf-8")
|
||||||
|
|
||||||
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={gemini_key}"
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
result = json.loads(resp.read().decode("utf-8"))
|
||||||
|
answer = result["candidates"][0]["content"]["parts"][0]["text"].strip()
|
||||||
|
|
||||||
|
return "important" if answer.startswith("1") else "not_important"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# メール解析ヘルパー
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def extract_email_address(raw):
|
||||||
|
"""'Name <email@example.com>' または 'email@example.com' からアドレスを抽出"""
|
||||||
|
match = re.search(r'<([^>]+)>', raw)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip().lower()
|
||||||
|
return raw.strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def decode_header_value(value):
|
||||||
|
"""MIMEエンコードされたヘッダー値をデコード"""
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
parts = email.header.decode_header(value)
|
||||||
|
decoded = []
|
||||||
|
for part, charset in parts:
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
decoded.append(part.decode(charset or "utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
decoded.append(part)
|
||||||
|
return "".join(decoded)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_body_preview(msg, max_chars=500):
|
||||||
|
"""メール本文の冠頭を抽出(テキスト優先、HTMLフォールバック)"""
|
||||||
|
text_content = ""
|
||||||
|
html_content = ""
|
||||||
|
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
ctype = part.get_content_type()
|
||||||
|
if ctype == "text/plain" and not text_content:
|
||||||
|
charset = part.get_content_charset() or "utf-8"
|
||||||
|
try:
|
||||||
|
text_content = part.get_payload(decode=True).decode(charset, errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif ctype == "text/html" and not html_content:
|
||||||
|
charset = part.get_content_charset() or "utf-8"
|
||||||
|
try:
|
||||||
|
html_content = part.get_payload(decode=True).decode(charset, errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
charset = msg.get_content_charset() or "utf-8"
|
||||||
|
try:
|
||||||
|
content = msg.get_payload(decode=True).decode(charset, errors="replace")
|
||||||
|
if msg.get_content_type() == "text/html":
|
||||||
|
html_content = content
|
||||||
|
else:
|
||||||
|
text_content = content
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if text_content:
|
||||||
|
# フッター・区切り線を除去
|
||||||
|
text = re.sub(r'\n[-_=]{10,}\n.*', '', text_content, flags=re.DOTALL)
|
||||||
|
text = re.sub(r'\s+', ' ', text).strip()
|
||||||
|
return text[:max_chars]
|
||||||
|
|
||||||
|
if html_content:
|
||||||
|
# HTMLタグを除去
|
||||||
|
text = re.sub(r'<[^>]+>', ' ', html_content)
|
||||||
|
text = re.sub(r'\s+', ' ', text).strip()
|
||||||
|
return text[:max_chars]
|
||||||
|
|
||||||
|
return ""
|
||||||
10
workflows/f/mail/mail_filter_schedule.schedule.yaml
Normal file
10
workflows/f/mail/mail_filter_schedule.schedule.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
args: {}
|
||||||
|
cron_version: v2
|
||||||
|
email: akiracraftwork@gmail.com
|
||||||
|
enabled: true
|
||||||
|
is_flow: true
|
||||||
|
no_flow_overlap: false
|
||||||
|
schedule: 0 */10 * * * *
|
||||||
|
script_path: f/mail/mail_filter
|
||||||
|
timezone: Asia/Tokyo
|
||||||
|
ws_error_handler_muted: false
|
||||||
18
workflows/f/shiraou/shiraou_notification__flow/flow.yaml
Normal file
18
workflows/f/shiraou/shiraou_notification__flow/flow.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
summary: 白皇集落営農 変更通知
|
||||||
|
description: shiraou.keinafarm.net の予約・実績変更をポーリングし、変更があればLINEで管理者に通知する。5分毎に実行。
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
summary: 変更確認・LINE通知
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline 変更確認・line通知.py'
|
||||||
|
input_transforms: {}
|
||||||
|
lock: '!inline 変更確認・line通知.lock'
|
||||||
|
language: python3
|
||||||
|
schema:
|
||||||
|
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
||||||
|
type: object
|
||||||
|
order: []
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# py: 3.12
|
||||||
|
anyio==4.12.1
|
||||||
|
certifi==2026.1.4
|
||||||
|
h11==0.16.0
|
||||||
|
httpcore==1.0.9
|
||||||
|
httpx==0.28.1
|
||||||
|
idna==3.11
|
||||||
|
typing-extensions==4.15.0
|
||||||
|
wmill==1.640.0
|
||||||
132
workflows/f/shiraou/shiraou_notification__flow/変更確認・line通知.py
Normal file
132
workflows/f/shiraou/shiraou_notification__flow/変更確認・line通知.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
import wmill
|
||||||
|
|
||||||
|
JST = timezone(timedelta(hours=9))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# シークレット取得
|
||||||
|
api_key = wmill.get_variable("u/admin/NOTIFICATION_API_KEY")
|
||||||
|
line_token = wmill.get_variable("u/admin/LINE_CHANNEL_ACCESS_TOKEN")
|
||||||
|
line_to = wmill.get_variable("u/admin/LINE_TO")
|
||||||
|
|
||||||
|
# 前回実行時刻を取得(初回は現在時刻 - 10分)
|
||||||
|
try:
|
||||||
|
last_checked = wmill.get_variable("u/admin/SHIRAOU_LAST_CHECKED_AT")
|
||||||
|
if not last_checked:
|
||||||
|
last_checked = None
|
||||||
|
except Exception:
|
||||||
|
last_checked = None
|
||||||
|
|
||||||
|
if last_checked:
|
||||||
|
since = last_checked
|
||||||
|
else:
|
||||||
|
since = (datetime.now(JST) - timedelta(minutes=10)).isoformat()
|
||||||
|
|
||||||
|
print(f"[通知] 変更確認: since={since}")
|
||||||
|
|
||||||
|
# API呼び出し
|
||||||
|
ssl_ctx = ssl.create_default_context()
|
||||||
|
ssl_ctx.check_hostname = False
|
||||||
|
ssl_ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
params = urllib.parse.urlencode({"since": since})
|
||||||
|
url = f"https://shiraou.keinafarm.net/reservations/api/changes/?{params}"
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, headers={"X-API-Key": api_key})
|
||||||
|
with urllib.request.urlopen(req, context=ssl_ctx, timeout=30) as resp:
|
||||||
|
data = json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
checked_at = data["checked_at"]
|
||||||
|
reservations = data.get("reservations", [])
|
||||||
|
usages = data.get("usages", [])
|
||||||
|
|
||||||
|
print(f"[通知] checked_at={checked_at}, 予約={len(reservations)}件, 実績={len(usages)}件")
|
||||||
|
|
||||||
|
# 変更があればLINE通知(エラー時は状態を更新しない)
|
||||||
|
if reservations or usages:
|
||||||
|
message = _format_message(reservations, usages)
|
||||||
|
_send_line(line_token, line_to, message)
|
||||||
|
print("[通知] LINE送信完了")
|
||||||
|
else:
|
||||||
|
print("[通知] 変更なし、通知スキップ")
|
||||||
|
|
||||||
|
# 正常完了時のみ状態更新
|
||||||
|
wmill.set_variable("u/admin/SHIRAOU_LAST_CHECKED_AT", checked_at)
|
||||||
|
print(f"[通知] last_checked_at更新: {checked_at}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"since": since,
|
||||||
|
"checked_at": checked_at,
|
||||||
|
"reservations_count": len(reservations),
|
||||||
|
"usages_count": len(usages),
|
||||||
|
"notified": bool(reservations or usages),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _format_message(reservations, usages):
|
||||||
|
lines = ["\U0001f4cb 営農システム 変更通知\n"]
|
||||||
|
|
||||||
|
OP_R = {
|
||||||
|
"create": ("\U0001f7e2", "予約作成"),
|
||||||
|
"update": ("\U0001f535", "予約変更"),
|
||||||
|
"cancel": ("\U0001f534", "予約キャンセル"),
|
||||||
|
}
|
||||||
|
OP_U = {
|
||||||
|
"create": ("\U0001f7e2", "実績登録"),
|
||||||
|
"update": ("\U0001f535", "実績修正"),
|
||||||
|
"delete": ("\U0001f534", "実績削除"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for r in reservations:
|
||||||
|
start = r["start_at"][:16].replace("T", " ")
|
||||||
|
end = r["end_at"][:16].replace("T", " ")
|
||||||
|
icon, label = OP_R.get(r["operation"], ("\u26aa", r["operation"]))
|
||||||
|
lines += [
|
||||||
|
f"{icon} {label}",
|
||||||
|
f" 機械: {r['machine_name']}",
|
||||||
|
f" 利用者: {r['user_name']}",
|
||||||
|
f" 日時: {start} \uff5e {end}",
|
||||||
|
]
|
||||||
|
if r.get("reason"):
|
||||||
|
lines.append(f" 理由: {r['reason']}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for u in usages:
|
||||||
|
start = u["start_at"][:16].replace("T", " ")
|
||||||
|
icon, label = OP_U.get(u["operation"], ("\u26aa", u["operation"]))
|
||||||
|
lines += [
|
||||||
|
f"{icon} {label}",
|
||||||
|
f" 機械: {u['machine_name']}",
|
||||||
|
f" 利用者: {u['user_name']}",
|
||||||
|
f" 利用量: {u['amount']}{u['unit']}",
|
||||||
|
f" 日: {start[:10]}",
|
||||||
|
]
|
||||||
|
if u.get("reason"):
|
||||||
|
lines.append(f" 理由: {u['reason']}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _send_line(token, to, message):
|
||||||
|
payload = json.dumps({
|
||||||
|
"to": to,
|
||||||
|
"messages": [{"type": "text", "text": message}],
|
||||||
|
}).encode("utf-8")
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://api.line.me/v2/bot/message/push",
|
||||||
|
data=payload,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
return resp.read().decode("utf-8")
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
args: {}
|
||||||
|
cron_version: v2
|
||||||
|
email: akiracraftwork@gmail.com
|
||||||
|
enabled: true
|
||||||
|
is_flow: true
|
||||||
|
no_flow_overlap: false
|
||||||
|
schedule: 0 */5 * * * *
|
||||||
|
script_path: f/shiraou/shiraou_notification
|
||||||
|
timezone: Asia/Tokyo
|
||||||
|
ws_error_handler_muted: false
|
||||||
10
workflows/f/weather/weather_sync.schedule.yaml
Normal file
10
workflows/f/weather/weather_sync.schedule.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
args: {}
|
||||||
|
cron_version: v2
|
||||||
|
email: akiracraftwork@gmail.com
|
||||||
|
enabled: true
|
||||||
|
is_flow: true
|
||||||
|
no_flow_overlap: false
|
||||||
|
schedule: 0 0 6 * * *
|
||||||
|
script_path: f/weather/weather_sync
|
||||||
|
timezone: Asia/Tokyo
|
||||||
|
ws_error_handler_muted: false
|
||||||
18
workflows/f/weather/weather_sync__flow/flow.yaml
Normal file
18
workflows/f/weather/weather_sync__flow/flow.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
summary: Weather Sync - 気象データ日次同期
|
||||||
|
description: Open-Meteo から昨日の気象データを取得し、Keinasystem DB に保存する。毎朝6時実行。
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
summary: 気象データ取得・同期
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline 気象データ取得・同期.py'
|
||||||
|
input_transforms: {}
|
||||||
|
lock: '!inline 気象データ取得・同期.lock'
|
||||||
|
language: python3
|
||||||
|
schema:
|
||||||
|
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
||||||
|
type: object
|
||||||
|
order: []
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
12
workflows/f/weather/weather_sync__flow/気象データ取得・同期.lock
Normal file
12
workflows/f/weather/weather_sync__flow/気象データ取得・同期.lock
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# py: 3.12
|
||||||
|
anyio==4.12.1
|
||||||
|
certifi==2026.2.25
|
||||||
|
charset-normalizer==3.4.4
|
||||||
|
h11==0.16.0
|
||||||
|
httpcore==1.0.9
|
||||||
|
httpx==0.28.1
|
||||||
|
idna==3.11
|
||||||
|
requests==2.32.5
|
||||||
|
typing-extensions==4.15.0
|
||||||
|
urllib3==2.6.3
|
||||||
|
wmill==1.646.0
|
||||||
72
workflows/f/weather/weather_sync__flow/気象データ取得・同期.py
Normal file
72
workflows/f/weather/weather_sync__flow/気象データ取得・同期.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import wmill
|
||||||
|
import requests
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
LATITUDE = 33.213
|
||||||
|
LONGITUDE = 133.133
|
||||||
|
TIMEZONE = "Asia/Tokyo"
|
||||||
|
|
||||||
|
OPEN_METEO_URL = "https://archive-api.open-meteo.com/v1/archive"
|
||||||
|
DAILY_VARS = [
|
||||||
|
"temperature_2m_mean",
|
||||||
|
"temperature_2m_max",
|
||||||
|
"temperature_2m_min",
|
||||||
|
"sunshine_duration",
|
||||||
|
"precipitation_sum",
|
||||||
|
"wind_speed_10m_max",
|
||||||
|
"surface_pressure_min",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
api_key = wmill.get_variable("u/admin/KEINASYSTEM_API_KEY")
|
||||||
|
base_url = wmill.get_variable("u/admin/KEINASYSTEM_API_URL").rstrip("/")
|
||||||
|
sync_url = f"{base_url}/api/weather/sync/"
|
||||||
|
|
||||||
|
yesterday = (datetime.date.today() - datetime.timedelta(days=1)).isoformat()
|
||||||
|
print(f"Fetching weather data for {yesterday} ...")
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"latitude": LATITUDE,
|
||||||
|
"longitude": LONGITUDE,
|
||||||
|
"start_date": yesterday,
|
||||||
|
"end_date": yesterday,
|
||||||
|
"daily": DAILY_VARS,
|
||||||
|
"timezone": TIMEZONE,
|
||||||
|
}
|
||||||
|
resp = requests.get(OPEN_METEO_URL, params=params, timeout=30)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise Exception(f"Open-Meteo API error: {resp.status_code} {resp.text[:300]}")
|
||||||
|
|
||||||
|
daily = resp.json().get("daily", {})
|
||||||
|
dates = daily.get("time", [])
|
||||||
|
if not dates:
|
||||||
|
print("No data returned from Open-Meteo.")
|
||||||
|
return {"status": "no_data"}
|
||||||
|
|
||||||
|
sunshine_raw = daily.get("sunshine_duration", [])
|
||||||
|
records = []
|
||||||
|
for i, d in enumerate(dates):
|
||||||
|
sun_sec = sunshine_raw[i]
|
||||||
|
records.append({
|
||||||
|
"date": d,
|
||||||
|
"temp_mean": daily["temperature_2m_mean"][i],
|
||||||
|
"temp_max": daily["temperature_2m_max"][i],
|
||||||
|
"temp_min": daily["temperature_2m_min"][i],
|
||||||
|
"sunshine_h": round(sun_sec / 3600, 2) if sun_sec is not None else None,
|
||||||
|
"precip_mm": daily["precipitation_sum"][i],
|
||||||
|
"wind_max": daily["wind_speed_10m_max"][i],
|
||||||
|
"pressure_min": daily["surface_pressure_min"][i],
|
||||||
|
})
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": api_key,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
post_resp = requests.post(sync_url, json=records, headers=headers, timeout=30)
|
||||||
|
if post_resp.status_code not in (200, 201):
|
||||||
|
raise Exception(f"Keinasystem sync error: {post_resp.status_code} {post_resp.text[:300]}")
|
||||||
|
|
||||||
|
result = post_resp.json()
|
||||||
|
print(f"Sync complete: {result}")
|
||||||
|
return result
|
||||||
21
workflows/u/admin/alexa_speak.script.yaml
Normal file
21
workflows/u/admin/alexa_speak.script.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
summary: Echo デバイスに TTS で読み上げ
|
||||||
|
description: 指定した Echo デバイスにテキストを読み上げさせる
|
||||||
|
lock: '!inline u/admin/alexa_speak.script.lock'
|
||||||
|
kind: script
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
device:
|
||||||
|
type: object
|
||||||
|
description: ''
|
||||||
|
default: null
|
||||||
|
format: dynselect-device
|
||||||
|
originalType: DynSelect_device
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
description: ''
|
||||||
|
default: null
|
||||||
|
originalType: string
|
||||||
|
required:
|
||||||
|
- device
|
||||||
|
- text
|
||||||
69
workflows/u/admin/alexa_speak.ts
Normal file
69
workflows/u/admin/alexa_speak.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* alexa_speak.ts
|
||||||
|
* 指定した Echo デバイスにテキストを読み上げさせる Windmill スクリプト
|
||||||
|
*
|
||||||
|
* パラメータ:
|
||||||
|
* device - ドロップダウンから選択するデバイス(内部的にはシリアル番号)
|
||||||
|
* text - 読み上げるテキスト
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ALEXA_API_URL = "http://alexa_api:3500";
|
||||||
|
|
||||||
|
type DeviceOption = { value: string; label: string };
|
||||||
|
|
||||||
|
const FALLBACK_DEVICE_OPTIONS: DeviceOption[] = [
|
||||||
|
{ value: "G0922H085165007R", label: "プレハブ (G0922H085165007R)" },
|
||||||
|
{ value: "G8M2DB08522600RL", label: "リビングエコー1 (G8M2DB08522600RL)" },
|
||||||
|
{ value: "G8M2DB08522503WF", label: "リビングエコー2 (G8M2DB08522503WF)" },
|
||||||
|
{ value: "G0922H08525302K5", label: "オフィスの右エコー (G0922H08525302K5)" },
|
||||||
|
{ value: "G0922H08525302J9", label: "オフィスの左エコー (G0922H08525302J9)" },
|
||||||
|
{ value: "G8M2HN08534302XH", label: "寝室のエコー (G8M2HN08534302XH)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Windmill Dynamic Select: 引数名 `device` に対応する `DynSelect_device` と `device()` を定義
|
||||||
|
export type DynSelect_device = string;
|
||||||
|
|
||||||
|
export async function device(): Promise<DeviceOption[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${ALEXA_API_URL}/devices`);
|
||||||
|
if (!res.ok) return FALLBACK_DEVICE_OPTIONS;
|
||||||
|
|
||||||
|
const devices = (await res.json()) as Array<{
|
||||||
|
name?: string;
|
||||||
|
serial?: string;
|
||||||
|
family?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const options = devices
|
||||||
|
.filter((d) => d.family === "ECHO" && d.serial)
|
||||||
|
.map((d) => ({
|
||||||
|
value: d.serial as string,
|
||||||
|
label: `${d.name ?? d.serial} (${d.serial})`,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label, "ja"));
|
||||||
|
|
||||||
|
return options.length > 0 ? options : FALLBACK_DEVICE_OPTIONS;
|
||||||
|
} catch {
|
||||||
|
return FALLBACK_DEVICE_OPTIONS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function main(
|
||||||
|
device: DynSelect_device,
|
||||||
|
text: string,
|
||||||
|
): Promise<{ ok: boolean; device: string; text: string }> {
|
||||||
|
const res = await fetch(`${ALEXA_API_URL}/speak`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ device, text }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(
|
||||||
|
`alexa-api error ${res.status}: ${JSON.stringify(body)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"windmill-cli": "1.566.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//bun.lock
|
|
||||||
{
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"workspaces": {
|
|
||||||
"": {
|
|
||||||
"dependencies": {
|
|
||||||
"windmill-cli": "1.566.1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages": {
|
|
||||||
"@ayonli/jsext": ["@ayonli/jsext@1.9.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sudo-prompt": "^9.2.1", "ws": "^8.17.0", "zod": "^3.23.8" } }, "sha512-hIu6lQhoLr5e26lmt+vzopuZffaAyb623r4+8HlN/rhXgm2ywHslzk7UHiATdfDbfPjBARkB6cfXjVEi3aav6g=="],
|
|
||||||
|
|
||||||
"@deno/shim-deno": ["@deno/shim-deno@0.18.2", "", { "dependencies": { "@deno/shim-deno-test": "^0.5.0", "which": "^4.0.0" } }, "sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA=="],
|
|
||||||
|
|
||||||
"@deno/shim-deno-test": ["@deno/shim-deno-test@0.5.0", "", {}, "sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w=="],
|
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="],
|
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="],
|
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="],
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
|
|
||||||
|
|
||||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
|
||||||
|
|
||||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
|
||||||
|
|
||||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
|
||||||
|
|
||||||
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
|
||||||
|
|
||||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
|
||||||
|
|
||||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
|
||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
|
||||||
|
|
||||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
||||||
|
|
||||||
"default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="],
|
|
||||||
|
|
||||||
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
|
|
||||||
|
|
||||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
|
||||||
|
|
||||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
|
||||||
|
|
||||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
|
||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
|
||||||
|
|
||||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
|
||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
|
||||||
|
|
||||||
"es-main": ["es-main@1.4.0", "", {}, "sha512-/rYhbfGK/1E6L7TcoUqmrWbSnOlMoxahiZInSYKbhIZ4/dbclHtXEcrViu4Az9IzYNBT8LcXpPszfS47zbGpwA=="],
|
|
||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
|
|
||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
|
||||||
|
|
||||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
|
||||||
|
|
||||||
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
|
||||||
|
|
||||||
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
|
||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
|
||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
|
||||||
|
|
||||||
"get-port": ["get-port@7.1.0", "", {}, "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw=="],
|
|
||||||
|
|
||||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
|
||||||
|
|
||||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
|
||||||
|
|
||||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
|
||||||
|
|
||||||
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
|
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
|
||||||
|
|
||||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
|
||||||
|
|
||||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
|
||||||
|
|
||||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
|
||||||
|
|
||||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
|
||||||
|
|
||||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
|
||||||
|
|
||||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
|
||||||
|
|
||||||
"jszip": ["jszip@3.7.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "set-immediate-shim": "~1.0.1" } }, "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg=="],
|
|
||||||
|
|
||||||
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
|
||||||
|
|
||||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
|
||||||
|
|
||||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
|
||||||
|
|
||||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
|
||||||
|
|
||||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
|
||||||
|
|
||||||
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
|
||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
|
||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
|
||||||
|
|
||||||
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
|
||||||
|
|
||||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
|
||||||
|
|
||||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
|
||||||
|
|
||||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
|
||||||
|
|
||||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
|
||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
|
||||||
|
|
||||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
|
||||||
|
|
||||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
|
||||||
|
|
||||||
"raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="],
|
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
|
||||||
|
|
||||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
|
||||||
|
|
||||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
||||||
|
|
||||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
|
||||||
|
|
||||||
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
|
||||||
|
|
||||||
"set-immediate-shim": ["set-immediate-shim@1.0.1", "", {}, "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ=="],
|
|
||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
|
||||||
|
|
||||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
|
||||||
|
|
||||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
|
||||||
|
|
||||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
|
||||||
|
|
||||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
|
||||||
|
|
||||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
|
||||||
|
|
||||||
"sudo-prompt": ["sudo-prompt@9.2.1", "", {}, "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw=="],
|
|
||||||
|
|
||||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
|
||||||
|
|
||||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
|
||||||
|
|
||||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
|
||||||
|
|
||||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
|
||||||
|
|
||||||
"windmill-cli": ["windmill-cli@1.566.1", "", { "dependencies": { "@ayonli/jsext": "*", "@deno/shim-deno": "~0.18.0", "diff": "*", "es-main": "*", "esbuild": "*", "express": "*", "get-port": "7.1.0", "jszip": "3.7.1", "minimatch": "*", "open": "*", "ws": "*" }, "bin": { "wmill": "esm/src/main.js" } }, "sha512-dyhcg/fBjOw1GvXxsFI/L+UGgoKTXUBzzVIF7p7HMcNUkD302Uf2l2MwnbJeyYT3czJ8L2oz46/5w2Rq2u/Vhg=="],
|
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
|
||||||
|
|
||||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
|
||||||
|
|
||||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
||||||
|
|
||||||
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
|
||||||
|
|
||||||
"raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
|
||||||
|
|
||||||
"readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
|
||||||
|
|
||||||
"string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
summary: Synchronize Hub Resource types with instance
|
|
||||||
description: >-
|
|
||||||
Sync latest resource types from hub to share to every workspace. Recommended
|
|
||||||
to run at least once. On a schedule by default.
|
|
||||||
lock: '!inline u/admin/hub_sync.script.lock'
|
|
||||||
kind: script
|
|
||||||
schema:
|
|
||||||
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
|
||||||
type: object
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import * as wmill from "windmill-cli@1.566.1"
|
|
||||||
|
|
||||||
export async function main() {
|
|
||||||
await wmill.hubPull({ workspace: "admins", token: process.env["WM_TOKEN"], baseUrl: process.env["BASE_URL"] });
|
|
||||||
}
|
|
||||||
16
workflows/u/akiracraftwork/hourly_chime.schedule.yaml
Normal file
16
workflows/u/akiracraftwork/hourly_chime.schedule.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
description: ''
|
||||||
|
args: {}
|
||||||
|
cron_version: v2
|
||||||
|
email: akiracraftwork@gmail.com
|
||||||
|
enabled: true
|
||||||
|
is_flow: true
|
||||||
|
no_flow_overlap: false
|
||||||
|
on_failure_exact: false
|
||||||
|
on_failure_times: 1
|
||||||
|
on_recovery_extra_args: {}
|
||||||
|
on_recovery_times: 1
|
||||||
|
on_success_extra_args: {}
|
||||||
|
schedule: 0 0 * * * *
|
||||||
|
script_path: u/akiracraftwork/hourly_chime
|
||||||
|
timezone: Asia/Tokyo
|
||||||
|
ws_error_handler_muted: false
|
||||||
5
workflows/u/akiracraftwork/hourly_chime__flow/a.lock
Normal file
5
workflows/u/akiracraftwork/hourly_chime__flow/a.lock
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
//bun.lock
|
||||||
|
<empty>
|
||||||
29
workflows/u/akiracraftwork/hourly_chime__flow/a.ts
Normal file
29
workflows/u/akiracraftwork/hourly_chime__flow/a.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export async function main(
|
||||||
|
device: string = "オフィスの右エコー",
|
||||||
|
prefix: string = "現在時刻は",
|
||||||
|
suffix: string = "です"
|
||||||
|
) {
|
||||||
|
const now = new Date();
|
||||||
|
const hhmm = new Intl.DateTimeFormat("ja-JP", {
|
||||||
|
timeZone: "Asia/Tokyo",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
}).format(now); // 例: 09:30
|
||||||
|
|
||||||
|
const [h, m] = hhmm.split(":");
|
||||||
|
const text = `${prefix}${Number(h)}時${Number(m)}分${suffix}`;
|
||||||
|
|
||||||
|
const res = await fetch("http://alexa_api:3500/speak", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ device, text }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(`alexa-api error ${res.status}: ${body}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, device, text };
|
||||||
|
}
|
||||||
88
workflows/u/akiracraftwork/hourly_chime__flow/flow.yaml
Normal file
88
workflows/u/akiracraftwork/hourly_chime__flow/flow.yaml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
summary: 鳩時計機能
|
||||||
|
description: 毎正時にAlexaで時刻を読み上げる。失敗時はLINEで通知。
|
||||||
|
value:
|
||||||
|
modules:
|
||||||
|
- id: a
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: '!inline a.ts'
|
||||||
|
input_transforms:
|
||||||
|
device:
|
||||||
|
type: static
|
||||||
|
value: オフィスの右エコー
|
||||||
|
prefix:
|
||||||
|
type: static
|
||||||
|
value: 現在時刻は
|
||||||
|
suffix:
|
||||||
|
type: static
|
||||||
|
value: です
|
||||||
|
lock: '!inline a.lock'
|
||||||
|
language: bun
|
||||||
|
failure_module:
|
||||||
|
id: failure
|
||||||
|
summary: エラー時LINE通知
|
||||||
|
value:
|
||||||
|
type: rawscript
|
||||||
|
content: |
|
||||||
|
import * as wmill from "windmill-client";
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
const token = await wmill.getVariable("u/admin/LINE_CHANNEL_ACCESS_TOKEN");
|
||||||
|
const to = await wmill.getVariable("u/admin/LINE_TO");
|
||||||
|
|
||||||
|
const message = [
|
||||||
|
"\u26a0\ufe0f \u9ce9\u6642\u8a08\u30a8\u30e9\u30fc",
|
||||||
|
"",
|
||||||
|
"Alexa TTS API \u304c\u5931\u6557\u3057\u307e\u3057\u305f\u3002",
|
||||||
|
"Cookie\u306e\u671f\u9650\u5207\u308c\u306e\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002",
|
||||||
|
"",
|
||||||
|
"\u5bfe\u51e6: auth4.js \u3067 Cookie \u3092\u518d\u53d6\u5f97\u3057\u3066\u304f\u3060\u3055\u3044\u3002"
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const res = await fetch("https://api.line.me/v2/bot/message/push", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
to: to,
|
||||||
|
messages: [{ type: "text", text: message }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(`LINE API error ${res.status}: ${body}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { notified: true };
|
||||||
|
}
|
||||||
|
input_transforms: {}
|
||||||
|
lock: |
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"windmill-client": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//bun.lock
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"windmill-client": "latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"windmill-client": ["windmill-client@1.661.0", "", {}, "sha512-vEosrP1NKVHJMi6gEnKnvd3QrNeoy0W0PYqAIIKvg0B4K4ejpw9zbvrytVvoSb7XC3Fb9PzYdvGFqdfaVCCTvg=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
language: bun
|
||||||
|
schema:
|
||||||
|
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
||||||
|
type: object
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
description: ''
|
description: ''
|
||||||
args: {}
|
args: {}
|
||||||
cron_version: v2
|
cron_version: v2
|
||||||
email: antigravity@keinafarm.com
|
email: akiracraftwork@gmail.com
|
||||||
enabled: true
|
enabled: true
|
||||||
is_flow: true
|
is_flow: true
|
||||||
no_flow_overlap: false
|
no_flow_overlap: false
|
||||||
@@ -10,7 +10,7 @@ on_failure_times: 1
|
|||||||
on_recovery_extra_args: {}
|
on_recovery_extra_args: {}
|
||||||
on_recovery_times: 1
|
on_recovery_times: 1
|
||||||
on_success_extra_args: {}
|
on_success_extra_args: {}
|
||||||
schedule: 0 */2 * * * *
|
schedule: 0 */30 * * * *
|
||||||
script_path: u/antigravity/git_sync
|
script_path: u/antigravity/git_sync
|
||||||
timezone: Asia/Tokyo
|
timezone: Asia/Tokyo
|
||||||
ws_error_handler_muted: false
|
ws_error_handler_muted: false
|
||||||
|
|||||||
@@ -1,68 +1,58 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
export PATH=/usr/bin:/usr/local/bin:/usr/sbin:/sbin:/bin:$PATH
|
||||||
|
|
||||||
# 色付き出力
|
GREEN="\033[0;32m"
|
||||||
GREEN='\033[0;32m'
|
YELLOW="\033[1;33m"
|
||||||
YELLOW='\033[1;33m'
|
RED="\033[0;31m"
|
||||||
RED='\033[0;31m'
|
NC="\033[0m"
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${GREEN}=== Windmill Workflow Git Sync ===${NC}"
|
echo -e "${GREEN}=== Windmill Workflow Git Sync ===${NC}"
|
||||||
|
|
||||||
# リポジトリルート(コンテナ内: docker-compose.ymlの .:/workspace マウント)
|
|
||||||
REPO_ROOT="/workspace"
|
REPO_ROOT="/workspace"
|
||||||
# wmill.yamlがあるディレクトリ(Windmill CLIはここで実行する)
|
|
||||||
WMILL_DIR="${REPO_ROOT}/workflows"
|
WMILL_DIR="${REPO_ROOT}/workflows"
|
||||||
|
|
||||||
# Windmill CLIのセットアップ
|
|
||||||
if ! command -v wmill &> /dev/null; then
|
if ! command -v wmill &> /dev/null; then
|
||||||
echo -e "${YELLOW}Installing windmill-cli...${NC}"
|
echo -e "${YELLOW}Installing windmill-cli...${NC}"
|
||||||
npm install -g windmill-cli
|
npm install -g windmill-cli
|
||||||
|
export PATH=$(npm prefix -g)/bin:$PATH
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 環境変数チェック
|
|
||||||
if [ -z "$WM_TOKEN" ]; then
|
|
||||||
echo -e "${RED}Error: WM_TOKEN is not set.${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# WM_BASE_URLはWindmill内で自動設定される場合があるが、念のため
|
|
||||||
: "${WM_BASE_URL:=http://windmill_server:8000}"
|
|
||||||
# Workspaceは環境変数または引数で
|
|
||||||
: "${WM_WORKSPACE:=admins}"
|
|
||||||
|
|
||||||
# Git設定(コンテナ内での一時設定)
|
|
||||||
git config --global --add safe.directory "$REPO_ROOT"
|
git config --global --add safe.directory "$REPO_ROOT"
|
||||||
git config --global user.email "bot@keinafarm.net"
|
git config --global user.email "bot@keinafarm.net"
|
||||||
git config --global user.name "Windmill Bot"
|
git config --global user.name "Windmill Bot"
|
||||||
|
|
||||||
# 1. Windmill(DB) -> Local Disk(wmill.yamlがあるディレクトリで実行)
|
# sync ブランチを使用
|
||||||
|
CURRENT_BRANCH=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)
|
||||||
|
if [ "$CURRENT_BRANCH" != "sync" ]; then
|
||||||
|
echo -e "${YELLOW}Switching to sync branch...${NC}"
|
||||||
|
git -C "$REPO_ROOT" fetch origin sync
|
||||||
|
git -C "$REPO_ROOT" checkout sync
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Pulling from origin/sync...${NC}"
|
||||||
|
git -C "$REPO_ROOT" pull --rebase origin sync || {
|
||||||
|
echo -e "${RED}Failed to pull from remote. Continuing...${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
echo -e "${YELLOW}Pulling from Windmill...${NC}"
|
echo -e "${YELLOW}Pulling from Windmill...${NC}"
|
||||||
cd "$WMILL_DIR"
|
cd "$WMILL_DIR"
|
||||||
wmill sync pull --token "$WM_TOKEN" --base-url "$WM_BASE_URL" --workspace "$WM_WORKSPACE" --skip-variables --skip-secrets --skip-resources --yes || exit 1
|
wmill sync pull --config-dir /workspace/wmill_config --skip-variables --skip-secrets --skip-resources --yes || exit 1
|
||||||
|
|
||||||
# 2. Local Disk -> Git Remote(Gitリポジトリルートに戻ってgit操作)
|
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
if [[ -n $(git status --porcelain) ]]; then
|
if [[ -n $(git status --porcelain) ]]; then
|
||||||
echo -e "${YELLOW}Changes detected, committing to Git...${NC}"
|
echo -e "${YELLOW}Changes detected, committing to Git...${NC}"
|
||||||
|
|
||||||
git add -A
|
git add -A
|
||||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
git commit -m "Auto-sync: ${TIMESTAMP}"
|
git commit -m "Auto-sync: ${TIMESTAMP}"
|
||||||
|
echo -e "${YELLOW}Pushing to Gitea (sync branch)...${NC}"
|
||||||
echo -e "${YELLOW}Pushing to Gitea...${NC}"
|
git push origin sync || {
|
||||||
# リモートの変更を先に取り込む(ローカルPCからのpushがある場合に備えて)
|
echo -e "${RED}Failed to push.${NC}"
|
||||||
git pull --rebase origin main || {
|
|
||||||
echo -e "${RED}Failed to pull from remote. Trying push anyway...${NC}"
|
|
||||||
}
|
|
||||||
git push origin main || {
|
|
||||||
echo -e "${RED}Failed to push. Need credentials in git remote url or credential helper.${NC}"
|
|
||||||
echo -e "${YELLOW}Hint: git remote set-url origin https://<token>@gitea.keinafarm.net/...${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
echo -e "${GREEN}Changes pushed to Gitea (sync branch)${NC}"
|
||||||
echo -e "${GREEN}✓ Changes pushed to Gitea${NC}"
|
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}✓ No changes detected${NC}"
|
echo -e "${GREEN}No changes detected${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}=== Sync Complete ===${NC}"
|
echo -e "${GREEN}=== Sync Complete ===${NC}"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
summary: Git Sync Workflow
|
summary: Git Sync Workflow
|
||||||
description: Automatically sync Windmill workflows to Git repository
|
description: Automatically sync Windmill workflows to Git repository (sync branch)
|
||||||
value:
|
value:
|
||||||
modules:
|
modules:
|
||||||
- id: a
|
- id: a
|
||||||
@@ -9,9 +9,4 @@ value:
|
|||||||
input_transforms: {}
|
input_transforms: {}
|
||||||
lock: ''
|
lock: ''
|
||||||
language: bash
|
language: bash
|
||||||
schema:
|
schema: null
|
||||||
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
|
||||||
type: object
|
|
||||||
order: []
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export async function main() {
|
|
||||||
return "Hello, World!"
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
summary: hello_world_demo
|
|
||||||
description: ''
|
|
||||||
value:
|
|
||||||
modules:
|
|
||||||
- id: a
|
|
||||||
value:
|
|
||||||
type: rawscript
|
|
||||||
content: '!inline a.ts'
|
|
||||||
input_transforms: {}
|
|
||||||
lock: '!inline a.lock'
|
|
||||||
language: bun
|
|
||||||
schema:
|
|
||||||
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
|
||||||
type: object
|
|
||||||
order: []
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
def main():
|
|
||||||
print("Hello from Git Sync Test")
|
|
||||||
return {"status": "success"}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
summary: Test script for Git auto-sync
|
|
||||||
description: Test workflow for Git auto-sync
|
|
||||||
lock: '!inline u/antigravity/test_git_sync.script.lock'
|
|
||||||
kind: script
|
|
||||||
schema:
|
|
||||||
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
|
||||||
type: object
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
@@ -1,10 +1,32 @@
|
|||||||
version: v2
|
version: v2
|
||||||
locks:
|
locks:
|
||||||
|
f/app_custom/system_heartbeat__flow+__flow_hash: 658d12c41ff4fed8cc458803c725da51f0ef477ea605ac1617a8cbc27a94f1fe
|
||||||
|
'f/app_custom/system_heartbeat__flow+step1:_診断データ生成.py': 5dac5515801ac73afa433b242e8af9989ecdc18b9798522d627aa8d88bc07bc8
|
||||||
|
'f/app_custom/system_heartbeat__flow+step2:_データ検証.py': d7f4e6e04ed116ba3836cb32793a0187a69359a3f2a807b533030b01d42bed39
|
||||||
|
'f/app_custom/system_heartbeat__flow+step3:_httpヘルスチェック.py': 5d3bce0ddb4f521444bf01bc80670e7321933ad09f935044f4d6123c658ca7a8
|
||||||
|
'f/app_custom/system_heartbeat__flow+step4:_年度判定_&_最終レポート.py': 6889bfac9a629fa42cf0505cbc945ba3782c59e1697b8493ce6101ef5ffa8b32
|
||||||
|
f/butler/execute_task_steps__flow+__flow_hash: 4b331a51d9f4bd6fbfc4714a859a08df86184f81fd902a382725541c002bdca8
|
||||||
|
f/butler/execute_task_steps__flow+execute_butler_task_steps.py: 90e90680a89ff3e7bd05d6c32513e9893b0c2064ae1c9e3dc3e2f3e05bad2166
|
||||||
|
f/dev/hello_world__flow+__flow_hash: 08a256433d5978b05d08e2ba6cfa8e4324c23be4875c9775777d683f32c6015e
|
||||||
|
f/dev/hello_world__flow+a.py: 63bf18351b5b0e81067254a03c9811e6bb388c890ad72e18092ac5ec2690a456
|
||||||
|
f/dev/konnnichiha__flow+__flow_hash: 0d40e9e9fe2cf6944028d671b6facb9e0598d41abc3682993d5339800188b8f1
|
||||||
|
f/dev/konnnichiha__flow+a.py: 932c967ebcf32abf2e923458c22d63973933b9b4451d0495846b2b720ff25d6d
|
||||||
|
f/dev/textout__flow+__flow_hash: 869322134a2ea15f54c3b35adf533a495b407d946ddd0b0e9c20d77316479c8b
|
||||||
|
f/dev/textout__flow+a.py: c4062ee04d2177a398ab3eb23dee0536088d183e8cf22f1d890b05a1bd6e518c
|
||||||
|
f/mail/mail_filter__flow+__flow_hash: 5790f99e6189a6ed1acabf57f9e6777fb1dc8a334facc1d1b1d26a08be8558a0
|
||||||
|
f/mail/mail_filter__flow+メール取得・判定・通知.py: b105f1a8414e7ee395f0e3ec1b9515766b4cb630d1fe5205b0493170a727237e
|
||||||
|
f/shiraou/shiraou_notification__flow+__flow_hash: 94825ff4362b6e4b6d165f8e17a51ebf8e5ef4da3e0ec1407a94b614ecab19dd
|
||||||
|
f/shiraou/shiraou_notification__flow+変更確認・line通知.py: ac80896991cce8132cfbf34d5dae20d3c09de5bc74a55c500e4c8705dd6a9d88
|
||||||
|
f/weather/weather_sync__flow+__flow_hash: 8af44676b2a175c1cc105028682f18e4bfbf7bf9de2722263a7d85c13c825f08
|
||||||
|
f/weather/weather_sync__flow+気象データ取得・同期.py: 86c9953ec7346601eaa13c681e2db5c01c9a5b4b45a3c47e8667ad3c47557029
|
||||||
g/all/setup_app__app+__app_hash: d71add32e14e552d1a4c861c972a50d9598b07c0af201bbadec5b59bbd99d7e3
|
g/all/setup_app__app+__app_hash: d71add32e14e552d1a4c861c972a50d9598b07c0af201bbadec5b59bbd99d7e3
|
||||||
g/all/setup_app__app+change_account.deno.ts: 3c592cac27e9cdab0de6ae19270bcb08c7fa54355ad05253a12de2351894346b
|
g/all/setup_app__app+change_account.deno.ts: 3c592cac27e9cdab0de6ae19270bcb08c7fa54355ad05253a12de2351894346b
|
||||||
|
u/admin/alexa_speak: e5bef63ab682e903715056cf24b4a94e87a14d4db60d8d29cd7c579359b56c72
|
||||||
u/admin/hub_sync: aaf9fd803fa229f3029d1bb02bbe3cc422fce680cad39c4eec8dd1da115de102
|
u/admin/hub_sync: aaf9fd803fa229f3029d1bb02bbe3cc422fce680cad39c4eec8dd1da115de102
|
||||||
u/antigravity/git_sync__flow+__flow_hash: 747f089a941b4fede4e17d92132c523be583291cdbbea7f523421409f443f6f0
|
u/akiracraftwork/hourly_chime__flow+__flow_hash: 79974bee69ff196e45a08b74e9539d8a3b50885ef0abba6907a00530809984fa
|
||||||
u/antigravity/git_sync__flow+a.sh: 615cae3132332c6b63ebc41d99bebe582577f9bb99102a9587c1f8cce56b853a
|
u/akiracraftwork/hourly_chime__flow+a.ts: b27320279be1d14184a210632e15d0e89d701243545d2d73cdd20e11dd413c53
|
||||||
|
u/antigravity/git_sync__flow+__flow_hash: 5a7194ef6bf1ce5529e70ae74fdb4cd05a0da662c78bfa355bb7e98698689ae6
|
||||||
|
u/antigravity/git_sync__flow+a.sh: ac7fdc83548f305fed33389129b79439e0c40077ed39a410477c77d08dca0ca9
|
||||||
u/antigravity/hello_world_demo__flow+__flow_hash: 0adc341960f8196454876684f85fe14ef087ba470322d2aabc99b37bf61edac9
|
u/antigravity/hello_world_demo__flow+__flow_hash: 0adc341960f8196454876684f85fe14ef087ba470322d2aabc99b37bf61edac9
|
||||||
u/antigravity/hello_world_demo__flow+a.ts: 53669a285c16d4ba322888755a33424521f769e9ebf64fc1f0cb21f9952b5958
|
u/antigravity/hello_world_demo__flow+a.ts: 53669a285c16d4ba322888755a33424521f769e9ebf64fc1f0cb21f9952b5958
|
||||||
u/antigravity/test_git_sync: 6461260a743de38a8c37d4b6083d481a73a6fde8c17cad1976d6635dca11362c
|
u/antigravity/test_git_sync: 3aa9e66ad8c87f1c2718d41d78ce3b773ce20743e4a1011396edbe2e7f88ac51
|
||||||
|
|||||||
Reference in New Issue
Block a user