Compare commits
46 Commits
dd9799d8b3
...
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 | ||
|
|
d59e55a54e | ||
|
|
53f84e2647 | ||
|
|
86c0180c3f | ||
|
|
f8e9c95403 | ||
|
|
793846e7f5 | ||
|
|
29d2dbbb57 | ||
|
|
0acac8799d | ||
|
|
39a850b064 | ||
|
|
90e805a360 | ||
|
|
f700a3454e | ||
|
|
0d1278b9ac | ||
|
|
639ac23efa | ||
|
|
297299c3f8 | ||
|
|
2f2ae074f5 | ||
|
|
2c96d29c6f |
12
.env
12
.env
@@ -1,13 +1,19 @@
|
||||
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
|
||||
|
||||
# To rotate logs, set the following variables:
|
||||
#LOG_MAX_SIZE=10m
|
||||
#LOG_MAX_FILE=3
|
||||
#LOG_MAX_FILE=3
|
||||
WINDMILL_TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -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/
|
||||
|
||||
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,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
|
||||
@@ -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}"
|
||||
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: ''
|
||||
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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,4 +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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user