Compare commits

46 Commits

Author SHA1 Message Date
Windmill Bot
19f64db8ed Auto-sync: 2026-03-20 09:00:01 2026-03-20 09:00:01 +00:00
Windmill Bot
d29dcc2b61 Auto-sync: 2026-03-13 06:30:01 2026-03-13 06:30:01 +00:00
Windmill Bot
280c12ccdf Auto-sync: 2026-03-05 06:30:01 2026-03-05 06:30:01 +00:00
Windmill Bot
ee88aa8c32 Auto-sync: 2026-03-05 06:00:01 2026-03-05 06:00:01 +00:00
Windmill Bot
5d3dd18224 Auto-sync: 2026-03-03 17:00:01 2026-03-03 17:00:01 +00:00
Windmill Bot
6f099e3665 Auto-sync: 2026-03-03 07:30:01 2026-03-03 07:30:01 +00:00
Windmill Bot
c5b49c015d Auto-sync: 2026-03-03 06:00:01 2026-03-03 06:00:01 +00:00
Windmill Bot
873d834dad Auto-sync: 2026-03-03 05:00:01 2026-03-03 05:00:01 +00:00
Windmill Bot
72483e045b Auto-sync: 2026-03-03 03:00:01 2026-03-03 03:00:01 +00:00
Windmill Bot
2a81b7ea35 Auto-sync: 2026-03-02 12:00:01 2026-03-02 12:00:01 +00:00
Windmill Bot
4666fa23fc Auto-sync: 2026-03-02 11:30:01 2026-03-02 11:30:01 +00:00
Windmill Bot
27854af3a7 Auto-sync: 2026-03-02 08:00:02 2026-03-02 08:00:02 +00:00
Windmill Bot
e646a87e6b Auto-sync: 2026-03-02 05:30:01 2026-03-02 05:30:01 +00:00
Windmill Bot
434fa33670 Auto-sync: 2026-03-02 05:00:01 2026-03-02 05:00:01 +00:00
Windmill Bot
65846cf6f6 Auto-sync: 2026-03-02 04:30:01 2026-03-02 04:30:01 +00:00
Windmill Bot
bdd1f5c689 Auto-sync: 2026-03-02 04:00:01 2026-03-02 04:00:01 +00:00
Windmill Bot
33a4f5ad7b Auto-sync: 2026-03-01 17:29:03 2026-03-01 17:29:03 +00:00
Windmill Bot
dcca6ee056 sync branch: インフラファイルを除外、ワークフロー定義のみ追跡 2026-03-02 02:24:04 +09:00
Windmill Bot
9e75903b39 Auto-sync: 2026-03-01 17:00:01 2026-03-01 17:00:01 +00:00
Windmill Bot
148d2cb025 Auto-sync: 2026-02-28 05:00:03 2026-02-28 05:00:03 +00:00
Windmill Bot
2911a489a2 Auto-sync: 2026-02-25 12:30:01 2026-02-25 12:30:01 +00:00
Windmill Bot
7b31410ef4 Auto-sync: 2026-02-25 07:14:42 2026-02-25 07:14:42 +00:00
Windmill Bot
f49ee2ab95 Add mail_filter flow (manual export from Windmill API) 2026-02-25 10:31:36 +09:00
Windmill Bot
6aa109b628 Auto-sync: 2026-02-21 07:00:01 2026-02-21 07:00:01 +00:00
Windmill Bot
1876548656 Auto-sync: 2026-02-21 06:30:12 2026-02-21 06:30:12 +00:00
Windmill Bot
1180d86091 Auto-sync: 2026-02-21 04:00:01 2026-02-21 04:00:01 +00:00
Windmill Bot
a89f54569d Auto-sync: 2026-02-21 00:00:01 2026-02-21 00:00:01 +00:00
Windmill Bot
0601cccce3 Auto-sync: 2026-02-19 08:30:01 2026-02-19 08:30:01 +00:00
Windmill Bot
909666a57d Auto-sync: 2026-02-19 08:00:01 2026-02-19 08:00:01 +00:00
Windmill Bot
f36aad0203 Auto-sync: 2026-02-19 07:50:01 2026-02-19 07:50:01 +00:00
Windmill Bot
3b8ddc8f2d Auto-sync: 2026-02-19 07:40:01 2026-02-19 07:40:01 +00:00
Akira
d59e55a54e 手順書更新 2026-02-19 16:13:43 +09:00
Akira
53f84e2647 スクリーンショットを確認しました。
test_git_sync.py
 に IndentationError(インデントエラー) があります。ファイルを確認します。

Analyzed
test_git_sync.py#L1-4
3行目の return のインデントが print より深くなっています。修正します。

Edited
test_git_sync.py
+1
-2
修正しました。return のインデントが print と揃っていなかったのが原因です。

ローカルからGiteaにプッシュして、サーバー側で git pull した後、Windmill上で再度テストを実行してみてください:

bash
git add .
git commit -m "Fix indentation in test_git_sync.py"
git push origin main
その後、サーバーのコンテナ内(またはホスト側)で再度 wmill sync push するか、Windmill UIから直接スクリプトを編集しても大丈夫です。
2026-02-19 15:45:18 +09:00
Akira
86c0180c3f おっしゃる通り、説明が不十分でした。整理させてください。
/workspace はコンテナ内のパスです。
docker-compose.yml
 の - .:/workspace によって、サーバーのホスト上の ~/windmill/(リポジトリルート)がコンテナ内の /workspace にマウントされています。
a.sh
 はWindmillのWorkerコンテナ内で実行されるので、スクリプト内で使う /workspace はコンテナ内のパスとして正しいです。

ただし、問題は
wmill.yaml
 がリポジトリルートではなく workflows/ サブディレクトリにある ことです。コンテナ内で言うと /workspace/workflows/ に
wmill.yaml
 があるので、スクリプトの cd /workspace を cd /workspace/workflows に変更する必要があります。

修正します:

Analyzed
a.sh#L1-63
なるほど、ポイントは以下の通りです:

cd /workspace → コンテナ内ではリポジトリルートです。しかし
wmill.yaml
 はここにはなく、workflows/ の中にあります。
wmill sync pull は
wmill.yaml
 がある場所で実行しなければ動きません。
git 操作(add, commit, push)はリポジトリルート(/workspace)で行う必要があります。
つまり、wmill sync pull は /workspace/workflows で、git 操作は /workspace で行う、という二段構えが必要です。修正します:

Edited
a.sh
+9
-5
修正しました。最終的なスクリプトの流れを確認します:

Analyzed
a.sh#L1-67
修正後のスクリプトは以下の流れになっています:

cd /workspace/workflows →
wmill.yaml
 がある場所で wmill sync pull を実行(Windmill DB → ファイルに書き出し)
cd /workspace → Gitリポジトリルートに戻って git add / commit / push(ファイル → Gitea)
ここで重要な補足:

/workspace はサーバーのホスト上のパスではなく、コンテナ内のパスです。
docker-compose.yml
 の - .:/workspace により、ホスト上の ~/windmill/(docker-composeを実行した場所)がコンテナ内の /workspace にマウントされます。
つまり、コンテナ内の /workspace/workflows/ = ホスト上の ~/windmill/workflows/ です。
a.sh
 はWindmillのWorkerコンテナ内で実行されるので、コンテナ内パス(/workspace)を使うのが正しいです。
この変更をGiteaにプッシュしますか?プッシュ後、サーバー側で git pull して再度お試しください。
2026-02-19 15:25:08 +09:00
Akira
f8e9c95403 ご指摘のコードブロックを削除した理由は、それが重複しており、かつサーバー環境(コンテナ内)では動作しないパスを含んでいたためです。
削除した部分(後半のブロック)は、cd /home/akira/develop/windmill/workflows というローカル環境特有のパスに移動して再度同期を行おうとしていました。 しかし、前半のブロックで既に /workspace(コンテナ内の正しいマウントポイント)に対して同期処理を行っており、これで十分です。 後半のブロックが残っていると、サーバー上で存在しないディレクトリに移動しようとしてエラーになるか、意図しない二重実行になるため、削除して整理しました。

これでスクリプトはシンプルになり、サーバー環境に適したものになりました。 変更をGiteaにプッシュしましたので、SERVER_SETUP.md の手順に従ってサーバー上で設定を進めてください。
2026-02-19 15:03:25 +09:00
Akira
793846e7f5 Google OAUTH関連 2026-02-19 14:50:31 +09:00
Akira
29d2dbbb57 DATABASE_PASSWORD 2026-02-19 14:18:23 +09:00
Akira
0acac8799d password 2026-02-19 14:11:22 +09:00
Akira
39a850b064 versionのパスワード 2026-02-19 14:10:27 +09:00
Akira
90e805a360 存在しないバージョン言われた 2026-02-19 14:04:02 +09:00
Akira
f700a3454e バージョンがlastestだと起動しない 2026-02-19 13:54:09 +09:00
Akira
0d1278b9ac compose見ました — 原因は完全に特定できました。
👉 Windmillに BASE_URL が設定されていません。

これがあると:

OAuth callback が壊れる

redirect_uri_mismatch

/api/oauth/callback/google が見つからない

全部起きます。

これは Windmill + Traefik の典型トラブルです。
2026-02-19 11:30:35 +09:00
Akira
639ac23efa Prepare for VPS migration: Create server specific docker-compose.yml and rename dev config 2026-02-18 15:13:53 +09:00
Bot
297299c3f8 Auto-sync Fri Feb 13 10:45:00 UTC 2026 2026-02-13 10:45:00 +00:00
Bot
2f2ae074f5 Auto-sync Fri Feb 13 10:42:01 UTC 2026 2026-02-13 10:42:01 +00:00
Bot
2c96d29c6f Auto-sync Fri Feb 13 10:39:01 UTC 2026 2026-02-13 10:39:01 +00:00
57 changed files with 1703 additions and 661 deletions

10
.env
View File

@@ -1,9 +1,14 @@
DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable
DATABASE_PASSWORD=DbForWindMillPassword
WM_VERSION=1.638.0
DATABASE_URL=postgres://postgres:${DATABASE_PASSWORD}@db/windmill?sslmode=disable
# For Enterprise Edition, use:
# WM_IMAGE=ghcr.io/windmill-labs/windmill-ee:main
WM_IMAGE=ghcr.io/windmill-labs/windmill:main
WM_IMAGE=ghcr.io/windmill-labs/windmill:${WM_VERSION}
GOOGLE_OAUTH_CLIENT_ID=976427934311-6oj0l38ptn6ui2hoj37qbs137lcnu6kg.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=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
# To use another port than :80, setup the Caddyfile and the caddy section of the docker-compose to your needs: https://caddyserver.com/docs/getting-started
# To have caddy take care of automatic TLS
@@ -11,3 +16,4 @@ WM_IMAGE=ghcr.io/windmill-labs/windmill:main
# To rotate logs, set the following variables:
#LOG_MAX_SIZE=10m
#LOG_MAX_FILE=3
WINDMILL_TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh

9
.gitignore vendored
View File

@@ -46,3 +46,12 @@ workflows/.wmill/tmp/
!workflows/g/
!workflows/wmill.yaml
!workflows/wmill-lock.yaml
# sync ブランチではインフラファイルを追跡しない
docker-compose.yml
docker-compose-dev.yml
Caddyfile
SERVER_SETUP.md
env.host
sync_to_git.sh
mcp/

View File

@@ -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
}

View File

@@ -1,224 +0,0 @@
version: "3.7"
x-logging: &default-logging
driver: "json-file"
options:
max-size: "${LOG_MAX_SIZE:-20m}"
max-file: "${LOG_MAX_FILE:-10}"
compress: "true"
services:
db:
deploy:
# To use an external database, set replicas to 0 and set DATABASE_URL to the external database url in the .env file
replicas: 1
image: postgres:16
shm_size: 1g
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
expose:
- 5432
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: windmill
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
interval: 10s
timeout: 5s
retries: 5
logging: *default-logging
windmill_server:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 1
restart: unless-stopped
expose:
- 8000
- 2525
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=server
depends_on:
db:
condition: service_healthy
volumes:
- worker_logs:/tmp/windmill/logs
- /home/akira/develop/windmill:/workspace
logging: *default-logging
windmill_worker:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 3
resources:
limits:
cpus: "1"
memory: 2048M
# for GB, use syntax '2Gi'
restart: unless-stopped
# Uncomment to enable PID namespace isolation (recommended for security)
# Requires privileged mode for --mount-proc flag
# See: https://www.windmill.dev/docs/advanced/security_isolation
# privileged: true
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=default
# If running with non-root/non-windmill UID (e.g., user: "1001:1001"),
# add: - HOME=/tmp
# Uncomment to enable PID namespace isolation (requires privileged: true above)
# - ENABLE_UNSHARE_PID=true
depends_on:
db:
condition: service_healthy
# to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
# mount the docker socket to allow to run docker containers from within the workers
- /var/run/docker.sock:/var/run/docker.sock
- worker_dependency_cache:/tmp/windmill/cache
- worker_logs:/tmp/windmill/logs
# mount the windmill workspace directory for git sync workflow
- /home/akira/develop/windmill:/workspace
logging: *default-logging
## This worker is specialized for "native" jobs. Native jobs run in-process and thus are much more lightweight than other jobs
windmill_worker_native:
# Use ghcr.io/windmill-labs/windmill-ee:main for the ee
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 1
resources:
limits:
cpus: "1"
memory: 2048M
# for GB, use syntax '2Gi'
restart: unless-stopped
# Uncomment to enable PID namespace isolation (recommended for security)
# Requires privileged mode for --mount-proc flag
# See: https://www.windmill.dev/docs/advanced/security_isolation
# privileged: true
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=native
- NUM_WORKERS=8
- SLEEP_QUEUE=200
# Uncomment to enable PID namespace isolation (requires privileged: true above)
# - ENABLE_UNSHARE_PID=true
depends_on:
db:
condition: service_healthy
volumes:
- worker_logs:/tmp/windmill/logs
logging: *default-logging
# This worker is specialized for reports or scraping jobs. It is assigned the "reports" worker group which has an init script that installs chromium and can be targeted by using the "chromium" worker tag.
# windmill_worker_reports:
# image: ${WM_IMAGE}
# pull_policy: always
# deploy:
# replicas: 1
# resources:
# limits:
# cpus: "1"
# memory: 2048M
# # for GB, use syntax '2Gi'
# restart: unless-stopped
# # Uncomment to enable PID namespace isolation (recommended for security)
# # Requires privileged mode for --mount-proc flag
# # See: https://www.windmill.dev/docs/advanced/security_isolation
# # privileged: true
# environment:
# - DATABASE_URL=${DATABASE_URL}
# - MODE=worker
# - WORKER_GROUP=reports
# # Uncomment to enable PID namespace isolation (requires privileged: true above)
# # - ENABLE_UNSHARE_PID=true
# depends_on:
# db:
# condition: service_healthy
# # to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
# volumes:
# # mount the docker socket to allow to run docker containers from within the workers
# - /var/run/docker.sock:/var/run/docker.sock
# - worker_dependency_cache:/tmp/windmill/cache
# - worker_logs:/tmp/windmill/logs
# The indexer powers full-text job and log search, an EE feature.
windmill_indexer:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 0 # set to 1 to enable full-text job and log search
restart: unless-stopped
expose:
- 8002
environment:
- PORT=8002
- DATABASE_URL=${DATABASE_URL}
- MODE=indexer
depends_on:
db:
condition: service_healthy
volumes:
- windmill_index:/tmp/windmill/search
- worker_logs:/tmp/windmill/logs
logging: *default-logging
# Combined extra services: LSP, Multiplayer, and Debugger
# Each service can be enabled/disabled via environment variables:
# - ENABLE_LSP=true (default) - Language Server Protocol for code intelligence
# - ENABLE_MULTIPLAYER=false - Real-time collaboration (Enterprise Edition)
# - ENABLE_DEBUGGER=false - Interactive debugging via DAP WebSocket
windmill_extra:
image: ghcr.io/windmill-labs/windmill-extra:latest
pull_policy: always
restart: unless-stopped
expose:
- 3001 # LSP
- 3002 # Multiplayer
- 3003 # Debugger
environment:
- ENABLE_LSP=true
- ENABLE_MULTIPLAYER=false # Set to true to enable multiplayer (Enterprise Edition)
- ENABLE_DEBUGGER=true # Set to true to enable debugger
- DEBUGGER_PORT=3003 # Debugger service port
- ENABLE_NSJAIL=false # Set to true for nsjail sandboxing (requires privileged: true)
- REQUIRE_SIGNED_DEBUG_REQUESTS=false # Set to true to require JWT tokens for debug sessions
- WINDMILL_BASE_URL=http://windmill_server:8000
volumes:
- lsp_cache:/pyls/.cache
logging: *default-logging
caddy:
image: ghcr.io/windmill-labs/caddy-l4:latest
restart: unless-stopped
# Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
# - ./certs:/certs # Provide custom certificate files like cert.pem and key.pem to enable HTTPS - See the corresponding section in the Caddyfile
ports:
# To change the exposed port, simply change 80:80 to <desired_port>:80. No other changes needed
- 80:80
- 25:25
# - 443:443 # Uncomment to enable HTTPS handling by Caddy
environment:
- BASE_URL=":80"
# - BASE_URL=":443" # uncomment and comment line above to enable HTTPS via custom certificate and key files
# - BASE_URL=mydomain.com # Uncomment and comment line above to enable HTTPS handling by Caddy
logging: *default-logging
volumes:
db_data: null
worker_dependency_cache: null
worker_logs: null
worker_memory: null
windmill_index: null
lsp_cache: null
caddy_data: null

View File

@@ -1,43 +0,0 @@
#!/bin/bash
# Windmill Workflow Git Auto-Sync Script
# このスクリプトは、Windmillワークフローを自動的にGitにコミットします
set -e
# 色付き出力
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== Windmill Workflow Git Sync ===${NC}"
# ディレクトリに移動
cd /home/akira/develop/windmill/workflows
# PATHを設定
export PATH=~/.npm-global/bin:$PATH
# 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"
echo -e "${GREEN}✓ Changes committed to Git${NC}"
else
echo -e "${GREEN}✓ No changes detected${NC}"
fi
echo -e "${GREEN}=== Sync Complete ===${NC}"

View File

@@ -0,0 +1 @@
admins

View File

@@ -0,0 +1 @@
{"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"}

View File

@@ -0,0 +1 @@
admins

View File

@@ -0,0 +1 @@
{"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"}

View 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: []

View File

@@ -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

View File

@@ -0,0 +1 @@
# py: 3.12

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -0,0 +1 @@
# py: 3.12

View File

@@ -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}")

View 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

View File

@@ -0,0 +1 @@
# py: 3.12

View File

@@ -0,0 +1,2 @@
def main():
print('こんにちは、世界')

View 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

View File

@@ -0,0 +1 @@
# py: 3.12

View File

@@ -0,0 +1,3 @@
def main():
from datetime import datetime
print(datetime.now().strftime('%H:%M:%S'))

View 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

View 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: []

View 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

View 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 ""

View 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

View 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: []

View 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.640.0

View 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")

View File

@@ -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

View 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

View 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: []

View 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

View 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

View 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

View 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();
}

View File

@@ -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=="],
}
}

View File

@@ -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: []

View File

@@ -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"] });
}

View 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

View File

@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>

View 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 };
}

View 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: []

View File

@@ -1,7 +1,7 @@
description: ''
args: {}
cron_version: v2
email: antigravity@keinafarm.com
email: akiracraftwork@gmail.com
enabled: true
is_flow: true
no_flow_overlap: false
@@ -10,7 +10,7 @@ on_failure_times: 1
on_recovery_extra_args: {}
on_recovery_times: 1
on_success_extra_args: {}
schedule: 0 */2 * * * *
schedule: 0 */30 * * * *
script_path: u/antigravity/git_sync
timezone: Asia/Tokyo
ws_error_handler_muted: false

View File

@@ -1,22 +1,58 @@
#!/bin/bash
set -x
export WM_BASE_URL="http://windmill_server:8000"
export WM_WORKSPACE="admins"
export PATH=$HOME/.npm-global/bin:$PATH
set -e
export PATH=/usr/bin:/usr/local/bin:/usr/sbin:/sbin:/bin:$PATH
echo "=== START SYNC ==="
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
RED="\033[0;31m"
NC="\033[0m"
echo -e "${GREEN}=== Windmill Workflow Git Sync ===${NC}"
REPO_ROOT="/workspace"
WMILL_DIR="${REPO_ROOT}/workflows"
if ! command -v wmill &> /dev/null; then
echo -e "${YELLOW}Installing windmill-cli...${NC}"
npm install -g windmill-cli
export PATH=$(npm prefix -g)/bin:$PATH
fi
wmill sync pull --token "$WM_TOKEN" --base-url "$WM_BASE_URL" --workspace "$WM_WORKSPACE" --skip-variables --skip-secrets --skip-resources --yes --verbose || exit 1
git config --global --add safe.directory "$REPO_ROOT"
git config --global user.email "bot@keinafarm.net"
git config --global user.name "Windmill Bot"
git config --global --add safe.directory /workspace
git config --global user.email "bot@example.com"
git config --global user.name "Bot"
# 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
git add .
git commit -m "Auto-sync $(date)" || echo "No changes"
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 "=== END SYNC ==="
echo -e "${YELLOW}Pulling from Windmill...${NC}"
cd "$WMILL_DIR"
wmill sync pull --config-dir /workspace/wmill_config --skip-variables --skip-secrets --skip-resources --yes || exit 1
cd "$REPO_ROOT"
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}"
echo -e "${YELLOW}Pushing to Gitea (sync branch)...${NC}"
git push origin sync || {
echo -e "${RED}Failed to push.${NC}"
exit 1
}
echo -e "${GREEN}Changes pushed to Gitea (sync branch)${NC}"
else
echo -e "${GREEN}No changes detected${NC}"
fi
echo -e "${GREEN}=== Sync Complete ===${NC}"

View File

@@ -1,5 +1,5 @@
summary: Git Sync Workflow
description: Automatically sync Windmill workflows to Git repository
description: Automatically sync Windmill workflows to Git repository (sync branch)
value:
modules:
- id: a
@@ -9,9 +9,4 @@ value:
input_transforms: {}
lock: ''
language: bash
schema:
$schema: 'https://json-schema.org/draft/2020-12/schema'
type: object
order: []
properties: {}
required: []
schema: null

View File

@@ -1,3 +0,0 @@
export async function main() {
return "Hello, World!"
}

View File

@@ -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: []

View File

@@ -1,4 +0,0 @@
def main():
print("Hello from Git Sync Test")
return {"status": "success"}

View File

@@ -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: []

View File

@@ -1,10 +1,32 @@
version: v2
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+change_account.deno.ts: 3c592cac27e9cdab0de6ae19270bcb08c7fa54355ad05253a12de2351894346b
u/admin/alexa_speak: e5bef63ab682e903715056cf24b4a94e87a14d4db60d8d29cd7c579359b56c72
u/admin/hub_sync: aaf9fd803fa229f3029d1bb02bbe3cc422fce680cad39c4eec8dd1da115de102
u/antigravity/git_sync__flow+__flow_hash: 747f089a941b4fede4e17d92132c523be583291cdbbea7f523421409f443f6f0
u/antigravity/git_sync__flow+a.sh: 615cae3132332c6b63ebc41d99bebe582577f9bb99102a9587c1f8cce56b853a
u/akiracraftwork/hourly_chime__flow+__flow_hash: 79974bee69ff196e45a08b74e9539d8a3b50885ef0abba6907a00530809984fa
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+a.ts: 53669a285c16d4ba322888755a33424521f769e9ebf64fc1f0cb21f9952b5958
u/antigravity/test_git_sync: 6461260a743de38a8c37d4b6083d481a73a6fde8c17cad1976d6635dca11362c
u/antigravity/test_git_sync: 3aa9e66ad8c87f1c2718d41d78ce3b773ce20743e4a1011396edbe2e7f88ac51