From 7b31410ef43371da6fe6952aa00165e8f192c7e6 Mon Sep 17 00:00:00 2001 From: Windmill Bot Date: Wed, 25 Feb 2026 07:14:42 +0000 Subject: [PATCH] Auto-sync: 2026-02-25 07:14:42 --- wmill_config/activeWorkspace | 1 + wmill_config/remotes.ndjson | 1 + wmill_config/windmill/activeWorkspace | 1 + wmill_config/windmill/remotes.ndjson | 1 + workflows/f/mail/mail_filter__flow/flow.yaml | 293 +-------- .../メール取得・判定・通知.lock | 9 + .../メール取得・判定・通知.py | 561 ++++++++++++++++++ .../f/mail/mail_filter_schedule.schedule.yaml | 10 + workflows/u/antigravity/git_sync__flow/a.sh | 43 +- workflows/wmill-lock.yaml | 6 +- 10 files changed, 610 insertions(+), 316 deletions(-) create mode 100644 wmill_config/activeWorkspace create mode 100644 wmill_config/remotes.ndjson create mode 100644 wmill_config/windmill/activeWorkspace create mode 100644 wmill_config/windmill/remotes.ndjson create mode 100644 workflows/f/mail/mail_filter__flow/メール取得・判定・通知.lock create mode 100644 workflows/f/mail/mail_filter__flow/メール取得・判定・通知.py create mode 100644 workflows/f/mail/mail_filter_schedule.schedule.yaml diff --git a/wmill_config/activeWorkspace b/wmill_config/activeWorkspace new file mode 100644 index 0000000..7d8ca2e --- /dev/null +++ b/wmill_config/activeWorkspace @@ -0,0 +1 @@ +admins \ No newline at end of file diff --git a/wmill_config/remotes.ndjson b/wmill_config/remotes.ndjson new file mode 100644 index 0000000..e4be2ee --- /dev/null +++ b/wmill_config/remotes.ndjson @@ -0,0 +1 @@ +{"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"} diff --git a/wmill_config/windmill/activeWorkspace b/wmill_config/windmill/activeWorkspace new file mode 100644 index 0000000..7d8ca2e --- /dev/null +++ b/wmill_config/windmill/activeWorkspace @@ -0,0 +1 @@ +admins \ No newline at end of file diff --git a/wmill_config/windmill/remotes.ndjson b/wmill_config/windmill/remotes.ndjson new file mode 100644 index 0000000..e4be2ee --- /dev/null +++ b/wmill_config/windmill/remotes.ndjson @@ -0,0 +1 @@ +{"remote":"http://windmill_server:8000/","workspaceId":"admins","name":"admins","token":"CQKYm1bUwszHCT4Ww6TGyQX97XMs8qg8"} diff --git a/workflows/f/mail/mail_filter__flow/flow.yaml b/workflows/f/mail/mail_filter__flow/flow.yaml index 2be2c53..e9577c4 100644 --- a/workflows/f/mail/mail_filter__flow/flow.yaml +++ b/workflows/f/mail/mail_filter__flow/flow.yaml @@ -1,289 +1,18 @@ summary: メールフィルタリング -description: IMAPで新着メールを受信し、送信者ルール確認→LLM判定→LINE通知を行う。Keinasystemと連携。Gmail→Hotmail→Xserverの順で段階的に有効化する。 +description: >- + IMAPで新着メールを受信し、送信者ルール確認→LLM判定→LINE通知を行う。Keinasystemと連携。Gmail→Hotmail→Xserverの順で段階的に有効化する。 value: modules: - - id: a - value: - lock: '# 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' - type: rawscript - content: "import imaplib\nimport email\nimport email.header\nimport json\nimport\ - \ re\nimport ssl\nimport urllib.request\nimport urllib.parse\nfrom datetime\ - \ import datetime, timezone, timedelta\nfrom email.utils import parsedate_to_datetime\n\ - import wmill\n\nJST = timezone(timedelta(hours=9))\n\n# ============================================================\n\ - # アカウント設定\n# 新しいアカウントを追加する際は enabled: True にする\n# ============================================================\n\ - ACCOUNTS = [\n {\n \"name\": \"gmail\",\n \"account_code\"\ - : \"gmail\",\n \"host\": \"imap.gmail.com\",\n \"port\": 993,\n\ - \ \"user_var\": \"u/admin/GMAIL_IMAP_USER\",\n \"pass_var\"\ - : \"u/admin/GMAIL_IMAP_PASSWORD\",\n \"last_uid_var\": \"u/admin/MAIL_FILTER_GMAIL_LAST_UID\"\ - ,\n \"mailbox\": \"[Gmail]/All Mail\",\n \"enabled\": True,\n\ - \ },\n {\n \"name\": \"gmail_service\",\n \"account_code\"\ - : \"gmail_service\",\n \"host\": \"imap.gmail.com\",\n \"port\"\ - : 993,\n \"user_var\": \"u/admin/GMAIL2_IMAP_USER\",\n \"pass_var\"\ - : \"u/admin/GMAIL2_IMAP_PASSWORD\",\n \"last_uid_var\": \"u/admin/MAIL_FILTER_GMAIL2_LAST_UID\"\ - ,\n \"mailbox\": \"[Gmail]/All Mail\",\n \"enabled\": True,\n\ - \ },\n # Hotmail テスト後に有効化\n # {\n # \"name\": \"hotmail\"\ - ,\n # \"account_code\": \"hotmail\",\n # \"host\": \"outlook.office365.com\"\ - ,\n # \"port\": 993,\n # \"user_var\": \"u/admin/HOTMAIL_IMAP_USER\"\ - ,\n # \"pass_var\": \"u/admin/HOTMAIL_IMAP_PASSWORD\",\n # \"\ - last_uid_var\": \"u/admin/MAIL_FILTER_HOTMAIL_LAST_UID\",\n # \"enabled\"\ - : False,\n # },\n # Xserver (keinafarm.com) 6アカウント\n {\n \"\ - name\": \"xserver_akiracraftwork\",\n \"account_code\": \"xserver\"\ - ,\n \"host\": \"sv579.xserver.jp\",\n \"port\": 993,\n \ - \ \"user_var\": \"u/admin/XSERVER1_IMAP_USER\",\n \"pass_var\": \"\ - u/admin/XSERVER1_IMAP_PASSWORD\",\n \"last_uid_var\": \"u/admin/MAIL_FILTER_XSERVER1_LAST_UID\"\ - ,\n \"enabled\": True,\n },\n {\n \"name\": \"xserver_service\"\ - ,\n \"account_code\": \"xserver\",\n \"host\": \"sv579.xserver.jp\"\ - ,\n \"port\": 993,\n \"user_var\": \"u/admin/XSERVER2_IMAP_USER\"\ - ,\n \"pass_var\": \"u/admin/XSERVER2_IMAP_PASSWORD\",\n \"last_uid_var\"\ - : \"u/admin/MAIL_FILTER_XSERVER2_LAST_UID\",\n \"enabled\": True,\n\ - \ },\n {\n \"name\": \"xserver_midori\",\n \"account_code\"\ - : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ - : 993,\n \"user_var\": \"u/admin/XSERVER3_IMAP_USER\",\n \"\ - pass_var\": \"u/admin/XSERVER3_IMAP_PASSWORD\",\n \"last_uid_var\"\ - : \"u/admin/MAIL_FILTER_XSERVER3_LAST_UID\",\n \"enabled\": True,\n\ - \ },\n {\n \"name\": \"xserver_kouseiren\",\n \"account_code\"\ - : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ - : 993,\n \"user_var\": \"u/admin/XSERVER4_IMAP_USER\",\n \"\ - pass_var\": \"u/admin/XSERVER4_IMAP_PASSWORD\",\n \"last_uid_var\"\ - : \"u/admin/MAIL_FILTER_XSERVER4_LAST_UID\",\n \"enabled\": True,\n\ - \ },\n {\n \"name\": \"xserver_post\",\n \"account_code\"\ - : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ - : 993,\n \"user_var\": \"u/admin/XSERVER5_IMAP_USER\",\n \"\ - pass_var\": \"u/admin/XSERVER5_IMAP_PASSWORD\",\n \"last_uid_var\"\ - : \"u/admin/MAIL_FILTER_XSERVER5_LAST_UID\",\n \"enabled\": True,\n\ - \ },\n {\n \"name\": \"xserver_sales\",\n \"account_code\"\ - : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ - : 993,\n \"user_var\": \"u/admin/XSERVER6_IMAP_USER\",\n \"\ - pass_var\": \"u/admin/XSERVER6_IMAP_PASSWORD\",\n \"last_uid_var\"\ - : \"u/admin/MAIL_FILTER_XSERVER6_LAST_UID\",\n \"enabled\": True,\n\ - \ },\n]\n\n\ndef main():\n # 共通変数取得\n api_key = wmill.get_variable(\"\ - u/admin/KEINASYSTEM_API_KEY\")\n api_url = wmill.get_variable(\"u/admin/KEINASYSTEM_API_URL\"\ - ).rstrip(\"/\")\n gemini_key = wmill.get_variable(\"u/admin/GEMINI_API_KEY\"\ - )\n line_token = wmill.get_variable(\"u/admin/LINE_CHANNEL_ACCESS_TOKEN\"\ - )\n line_to = wmill.get_variable(\"u/admin/LINE_TO\")\n\n total_processed\ - \ = 0\n total_notified = 0\n\n for account in ACCOUNTS:\n if\ - \ not account[\"enabled\"]:\n continue\n print(f\"[{account['name']}]\ - \ 処理開始\")\n try:\n processed, notified = process_account(\n\ - \ account, api_key, api_url, gemini_key, line_token, line_to\n\ - \ )\n total_processed += processed\n total_notified\ - \ += notified\n print(f\"[{account['name']}] 処理完了: {processed}件処理,\ - \ {notified}件通知\")\n except Exception as e:\n print(f\"\ - [{account['name']}] エラー: {e}\")\n # 1アカウントが失敗しても他のアカウントは継続\n\n\ - \ return {\n \"total_processed\": total_processed,\n \"total_notified\"\ - : total_notified,\n }\n\n\ndef process_account(account, api_key, api_url,\ - \ gemini_key, line_token, line_to):\n user = wmill.get_variable(account[\"\ - user_var\"])\n password = wmill.get_variable(account[\"pass_var\"])\n\n\ - \ # 前回の最終UID取得\n try:\n last_uid_str = wmill.get_variable(account[\"\ - last_uid_var\"])\n last_uid = int(last_uid_str) if last_uid_str and\ - \ last_uid_str != \"0\" else None\n except Exception:\n last_uid\ - \ = None\n\n # IMAP接続\n ssl_ctx = ssl.create_default_context()\n \ - \ mail = imaplib.IMAP4_SSL(account[\"host\"], account[\"port\"], ssl_context=ssl_ctx)\n\ - \ mail.login(user, password)\n mailbox = account.get(\"mailbox\", \"\ - INBOX\")\n imap_mailbox = resolve_mailbox(mail, mailbox)\n\n try:\n\ - \ if last_uid is None:\n # 初回実行: 現在の最大UIDを記録して終了(既存メールは処理しない)\n\ - \ _, data = mail.uid(\"SEARCH\", None, \"ALL\")\n all_uids\ - \ = data[0].split() if data[0] else []\n max_uid = int(all_uids[-1])\ - \ if all_uids else 0\n wmill.set_variable(account[\"last_uid_var\"\ - ], str(max_uid))\n print(f\"[{account['name']}] 初回実行: 最大UID={max_uid}\ - \ を記録、既存メールはスキップ\")\n return 0, 0\n\n # last_uid より大きい UID\ - \ を検索\n search_criterion = f\"UID {last_uid + 1}:*\"\n _, data\ - \ = mail.uid(\"SEARCH\", None, search_criterion)\n raw_uids = data[0].split()\ - \ if data[0] else []\n new_uids = [u for u in raw_uids if int(u) >\ - \ last_uid]\n\n if not new_uids:\n print(f\"[{account['name']}]\ - \ 新着メールなし\")\n return 0, 0\n\n print(f\"[{account['name']}]\ - \ 新着{len(new_uids)}件\")\n\n processed = 0\n notified = 0\n \ - \ max_processed_uid = last_uid\n\n for uid_bytes in new_uids:\n\ - \ uid = int(uid_bytes)\n try:\n result\ - \ = process_message(\n mail, uid, account,\n \ - \ api_key, api_url, gemini_key, line_token, line_to\n \ - \ )\n processed += 1\n if result == \"notified\"\ - :\n notified += 1\n max_processed_uid =\ - \ max(max_processed_uid, uid)\n except Exception as e:\n \ - \ print(f\"[{account['name']}] UID={uid} 処理エラー: {e}\")\n \ - \ # 個別メッセージのエラーは継続、UIDは進めない\n\n # 処理済み最大UIDを保存(正常完了時のみ)\n \ - \ if max_processed_uid > last_uid:\n wmill.set_variable(account[\"\ - last_uid_var\"], str(max_processed_uid))\n\n return processed, notified\n\ - \ finally:\n mail.logout()\n\n\ndef process_message(mail, uid, account,\ - \ api_key, api_url, gemini_key, line_token, line_to):\n \"\"\"メッセージを1通処理。戻り値:\ - \ 'skipped' / 'not_important' / 'notified'\"\"\"\n account_code = account[\"\ - account_code\"]\n forwarding_map = account.get(\"forwarding_map\", {})\n\ - \n # メール取得\n _, data = mail.uid(\"FETCH\", str(uid), \"(RFC822)\")\n\ - \ if not data or not data[0]:\n return \"skipped\"\n\n raw_email\ - \ = data[0][1]\n msg = email.message_from_bytes(raw_email)\n\n # ヘッダー解析\n\ - \ message_id = msg.get(\"Message-ID\", \"\").strip()\n if not message_id:\n\ - \ message_id = f\"{account_code}-uid-{uid}\"\n\n sender_raw = msg.get(\"\ - From\", \"\")\n sender_email_addr = extract_email_address(sender_raw)\n\ - \ sender_domain = sender_email_addr.split(\"@\")[-1] if \"@\" in sender_email_addr\ - \ else \"\"\n\n subject = decode_header_value(msg.get(\"Subject\", \"(件名なし)\"\ - ))\n\n date_str = msg.get(\"Date\", \"\")\n try:\n received_at\ - \ = parsedate_to_datetime(date_str).isoformat()\n except Exception:\n \ - \ received_at = datetime.now(JST).isoformat()\n\n body_preview =\ - \ extract_body_preview(msg, max_chars=500)\n\n # 転送検出: To:ヘッダーのドメインが forwarding_map\ - \ に存在する場合は account_code を上書き\n if forwarding_map:\n to_raw = msg.get(\"\ - To\", \"\")\n if to_raw:\n to_addr = extract_email_address(to_raw)\n\ - \ to_domain = to_addr.split(\"@\")[-1] if \"@\" in to_addr else\ - \ \"\"\n if to_domain in forwarding_map:\n account_code\ - \ = forwarding_map[to_domain]\n print(f\" [転送検出] To:{to_addr}\ - \ → account: {account_code}\")\n\n print(f\" From: {sender_email_addr},\ - \ Subject: {subject[:50]}\")\n\n # --- ステップ1: 送信者ルール確認 ---\n rule_result\ - \ = call_api_get(api_key, api_url, \"/api/mail/sender-rule/\", {\n \ - \ \"email\": sender_email_addr,\n \"domain\": sender_domain,\n })\n\ - \n if rule_result.get(\"matched\"):\n rule = rule_result[\"rule\"\ - ]\n\n if rule == \"never_notify\":\n print(f\" → never_notify\ - \ ルール一致、スキップ\")\n return \"skipped\"\n\n elif rule == \"\ - always_notify\":\n print(f\" → always_notify ルール一致、即通知\")\n \ - \ result = post_email(api_key, api_url, {\n \"account\"\ - : account_code,\n \"message_id\": message_id,\n \ - \ \"sender_email\": sender_email_addr,\n \"sender_domain\"\ - : sender_domain,\n \"subject\": subject,\n \"\ - body_preview\": body_preview,\n \"received_at\": received_at,\n\ - \ \"llm_verdict\": \"important\",\n })\n \ - \ if result.get(\"feedback_url\"):\n send_line_notification(line_token,\ - \ line_to, account_code, sender_email_addr, subject, result[\"feedback_url\"\ - ])\n return \"notified\"\n return \"skipped\"\n\n\ - \ # --- ステップ2: LLM判定 ---\n context = call_api_get(api_key, api_url,\ - \ \"/api/mail/sender-context/\", {\n \"email\": sender_email_addr,\n\ - \ \"domain\": sender_domain,\n })\n verdict = judge_with_llm(gemini_key,\ - \ sender_email_addr, subject, body_preview, context)\n print(f\" → LLM判定:\ - \ {verdict}\")\n\n # --- ステップ3: Keinasystemに記録 ---\n result = post_email(api_key,\ - \ api_url, {\n \"account\": account_code,\n \"message_id\":\ - \ message_id,\n \"sender_email\": sender_email_addr,\n \"sender_domain\"\ - : sender_domain,\n \"subject\": subject,\n \"body_preview\"\ - : body_preview,\n \"received_at\": received_at,\n \"llm_verdict\"\ - : verdict,\n })\n\n if verdict == \"important\" and result.get(\"feedback_url\"\ - ):\n send_line_notification(line_token, line_to, account_code, sender_email_addr,\ - \ subject, result[\"feedback_url\"])\n return \"notified\"\n\n return\ - \ \"not_important\"\n\n\n# ============================================================\n\ - # メールボックス解決\n# ============================================================\n\ - \ndef resolve_mailbox(mail, mailbox):\n \"\"\"メールボックスを選択し SELECT する。\n\ - \ INBOX はそのまま、それ以外は指定名 -> \\\\All 属性でフォールバック。\n \"\"\"\n if mailbox\ - \ == \"INBOX\":\n typ, data = mail.select(\"INBOX\")\n if typ\ - \ != 'OK':\n raise Exception(f\"SELECT INBOX failed: {data}\")\n\ - \ return \"INBOX\"\n\n # まず指定名で試行\n imap_name = '\"' + mailbox\ - \ + '\"'\n typ, data = mail.select(imap_name)\n if typ == 'OK':\n \ - \ return imap_name\n\n # 失敗した場合: \\\\All 属性を持つメールボックスを自動検出\n print(f\"\ - \ [INFO] {mailbox} not found, searching for \\\\\\\\All mailbox...\")\n \ - \ typ2, mboxes = mail.list()\n if typ2 == 'OK':\n for mb in mboxes:\n\ - \ if not mb:\n continue\n mb_str = mb.decode()\ - \ if isinstance(mb, bytes) else mb\n if '\\\\\\\\All' in mb_str\ - \ or '\\\\All' in mb_str:\n # \"(attrs) \\\".\\\" \\\"name\\\ - \"\" 形式から名前を抽出\n parts = mb_str.rsplit('\"', 2)\n \ - \ if len(parts) >= 2 and parts[-2]:\n found = parts[-2]\n\ - \ else:\n found = mb_str.split()[-1].strip('\"\ - ')\n print(f\" [INFO] Found All Mail mailbox: {found}\")\n\ - \ imap_found = '\"' + found + '\"'\n typ3, data3\ - \ = mail.select(imap_found)\n if typ3 == 'OK':\n \ - \ return imap_found\n raise Exception(f\"Could not select any\ - \ All Mail mailbox (tried: {mailbox})\")\n\n\n# ============================================================\n\ - # APIヘルパー\n# ============================================================\n\ - \ndef _make_ssl_ctx():\n ctx = ssl.create_default_context()\n ctx.check_hostname\ - \ = False\n ctx.verify_mode = ssl.CERT_NONE\n return ctx\n\n\ndef call_api_get(api_key,\ - \ api_url, path, params):\n qs = urllib.parse.urlencode(params)\n url\ - \ = f\"{api_url}{path}?{qs}\"\n req = urllib.request.Request(url, headers={\"\ - X-API-Key\": api_key})\n with urllib.request.urlopen(req, context=_make_ssl_ctx(),\ - \ timeout=10) as resp:\n return json.loads(resp.read().decode(\"utf-8\"\ - ))\n\n\ndef post_email(api_key, api_url, data):\n url = f\"{api_url}/api/mail/emails/\"\ - \n payload = json.dumps(data).encode(\"utf-8\")\n req = urllib.request.Request(\n\ - \ url,\n data=payload,\n headers={\"X-API-Key\": api_key,\ - \ \"Content-Type\": \"application/json\"},\n method=\"POST\",\n \ - \ )\n try:\n with urllib.request.urlopen(req, context=_make_ssl_ctx(),\ - \ timeout=10) as resp:\n return json.loads(resp.read().decode(\"\ - utf-8\"))\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"\ - utf-8\")\n if e.code == 400 and \"message_id\" in body:\n \ - \ # 重複message_idは正常(再実行時の冦殁)\n print(f\" 重複メール、スキップ\")\n \ - \ return {}\n raise\n\n\nACCOUNT_LABELS = {\n \"gmail\"\ - : \"Gmail (メイン)\",\n \"gmail_service\": \"Gmail (サービス用)\",\n \"hotmail\"\ - : \"Hotmail\",\n \"xserver\": \"Xserver\",\n}\n\ndef send_line_notification(line_token,\ - \ line_to, account_code, sender_email_addr, subject, feedback_url):\n account_label\ - \ = ACCOUNT_LABELS.get(account_code, account_code)\n message = (\n \ - \ f\"\U0001F4E7 重要なメールが届きました\\n\\n\"\n f\"宛先: {account_label}\\\ - n\"\n f\"差出人: {sender_email_addr}\\n\"\n f\"件名: {subject}\\\ - n\\n\"\n f\"フィードバック:\\n{feedback_url}\"\n )\n payload = json.dumps({\n\ - \ \"to\": line_to,\n \"messages\": [{\"type\": \"text\", \"\ - text\": message}],\n }).encode(\"utf-8\")\n req = urllib.request.Request(\n\ - \ \"https://api.line.me/v2/bot/message/push\",\n data=payload,\n\ - \ headers={\n \"Authorization\": f\"Bearer {line_token}\"\ - ,\n \"Content-Type\": \"application/json\",\n },\n \ - \ method=\"POST\",\n )\n with urllib.request.urlopen(req, timeout=30)\ - \ as resp:\n resp.read()\n\n\n# ============================================================\n\ - # LLM判定(Gemini API)\n# ============================================================\n\ - \ndef judge_with_llm(gemini_key, sender_email_addr, subject, body_preview,\ - \ context):\n \"\"\"農家にとって重要なメールか判定。'important' または 'not_important' を返す。\"\ - \"\"\n\n context_text = \"\"\n total = context.get(\"total_notified\"\ - , 0)\n if total > 0:\n context_text = (\n f\"\\n\\n[この送信者の過去データ]\ - \ \"\n f\"通知済み{total}件: \"\n f\"重要{context.get('important',\ - \ 0)}件 / \"\n f\"普通{context.get('not_important', 0)}件 / \"\n \ - \ f\"通知不要{context.get('never_notify', 0)}件 / \"\n f\"\ - 未評価{context.get('no_feedback', 0)}件\"\n )\n\n user_message = (\n\ - \ f\"送信者: {sender_email_addr}\\n\"\n f\"件名: {subject}\\n\"\n\ - \ f\"本文冠頭:\\n{body_preview}\"\n f\"{context_text}\\n\\n\"\n\ - \ f\"このメールは農家にとって重要ですか?\\n\"\n f\"1: 重要(要確認)\\n\"\n f\"\ - 2: 重要でない(営業・通知等)\\n\"\n f\"数字1文字のみで答えてください。\"\n )\n\n payload\ - \ = json.dumps({\n \"system_instruction\": {\n \"parts\"\ - : [{\"text\": \"あなたは農家のメールフィルタリングアシスタントです。メールが重要かどうかを判定してください。\"}]\n \ - \ },\n \"contents\": [{\n \"role\": \"user\",\n \ - \ \"parts\": [{\"text\": user_message}]\n }],\n \"generationConfig\"\ - : {\n \"maxOutputTokens\": 10,\n \"temperature\": 0\n\ - \ }\n }).encode(\"utf-8\")\n\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={gemini_key}\"\ - \n req = urllib.request.Request(\n url,\n data=payload,\n\ - \ headers={\"Content-Type\": \"application/json\"},\n method=\"\ - POST\",\n )\n with urllib.request.urlopen(req, timeout=30) as resp:\n\ - \ result = json.loads(resp.read().decode(\"utf-8\"))\n answer\ - \ = result[\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"].strip()\n\ - \n return \"important\" if answer.startswith(\"1\") else \"not_important\"\ - \n\n\n# ============================================================\n# メール解析ヘルパー\n\ - # ============================================================\n\ndef extract_email_address(raw):\n\ - \ \"\"\"'Name ' または 'email@example.com' からアドレスを抽出\"\"\ - \"\n match = re.search(r'<([^>]+)>', raw)\n if match:\n return\ - \ match.group(1).strip().lower()\n return raw.strip().lower()\n\n\ndef\ - \ decode_header_value(value):\n \"\"\"MIMEエンコードされたヘッダー値をデコード\"\"\"\n \ - \ if not value:\n return \"\"\n parts = email.header.decode_header(value)\n\ - \ decoded = []\n for part, charset in parts:\n if isinstance(part,\ - \ bytes):\n decoded.append(part.decode(charset or \"utf-8\", errors=\"\ - replace\"))\n else:\n decoded.append(part)\n return \"\ - \".join(decoded)\n\n\ndef extract_body_preview(msg, max_chars=500):\n \"\ - \"\"メール本文の冠頭を抽出(テキスト優先、HTMLフォールバック)\"\"\"\n text_content = \"\"\n html_content\ - \ = \"\"\n\n if msg.is_multipart():\n for part in msg.walk():\n\ - \ ctype = part.get_content_type()\n if ctype == \"text/plain\"\ - \ and not text_content:\n charset = part.get_content_charset()\ - \ or \"utf-8\"\n try:\n text_content = part.get_payload(decode=True).decode(charset,\ - \ errors=\"replace\")\n except Exception:\n \ - \ pass\n elif ctype == \"text/html\" and not html_content:\n\ - \ charset = part.get_content_charset() or \"utf-8\"\n \ - \ try:\n html_content = part.get_payload(decode=True).decode(charset,\ - \ errors=\"replace\")\n except Exception:\n \ - \ pass\n else:\n charset = msg.get_content_charset() or \"utf-8\"\ - \n try:\n content = msg.get_payload(decode=True).decode(charset,\ - \ errors=\"replace\")\n if msg.get_content_type() == \"text/html\"\ - :\n html_content = content\n else:\n \ - \ text_content = content\n except Exception:\n pass\n\ - \n if text_content:\n # フッター・区切り線を除去\n text = re.sub(r'\\\ - n[-_=]{10,}\\n.*', '', text_content, flags=re.DOTALL)\n text = re.sub(r'\\\ - s+', ' ', text).strip()\n return text[:max_chars]\n\n if html_content:\n\ - \ # HTMLタグを除去\n text = re.sub(r'<[^>]+>', ' ', html_content)\n\ - \ text = re.sub(r'\\s+', ' ', text).strip()\n return text[:max_chars]\n\ - \n return \"\"\n" - language: python3 - input_transforms: {} - summary: メール取得・判定・通知 + - 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 + $schema: 'https://json-schema.org/draft/2020-12/schema' type: object order: [] properties: {} diff --git a/workflows/f/mail/mail_filter__flow/メール取得・判定・通知.lock b/workflows/f/mail/mail_filter__flow/メール取得・判定・通知.lock new file mode 100644 index 0000000..66c875f --- /dev/null +++ b/workflows/f/mail/mail_filter__flow/メール取得・判定・通知.lock @@ -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 \ No newline at end of file diff --git a/workflows/f/mail/mail_filter__flow/メール取得・判定・通知.py b/workflows/f/mail/mail_filter__flow/メール取得・判定・通知.py new file mode 100644 index 0000000..6bf0109 --- /dev/null +++ b/workflows/f/mail/mail_filter__flow/メール取得・判定・通知.py @@ -0,0 +1,561 @@ +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": "xserver", + "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": "xserver", + "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": "xserver", + "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": "xserver", + "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": "xserver", + "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": "xserver", + "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", {}) + + # メール取得 + _, 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:ヘッダーのドメインが forwarding_map に存在する場合は account_code を上書き + if forwarding_map: + 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 "" + if to_domain in forwarding_map: + account_code = forwarding_map[to_domain] + 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", + "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' からアドレスを抽出""" + 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 "" diff --git a/workflows/f/mail/mail_filter_schedule.schedule.yaml b/workflows/f/mail/mail_filter_schedule.schedule.yaml new file mode 100644 index 0000000..823c287 --- /dev/null +++ b/workflows/f/mail/mail_filter_schedule.schedule.yaml @@ -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 diff --git a/workflows/u/antigravity/git_sync__flow/a.sh b/workflows/u/antigravity/git_sync__flow/a.sh index 6654c51..3c75a17 100644 --- a/workflows/u/antigravity/git_sync__flow/a.sh +++ b/workflows/u/antigravity/git_sync__flow/a.sh @@ -1,69 +1,48 @@ #!/bin/bash set -e +export PATH=/usr/bin:/usr/local/bin:/usr/sbin:/sbin:/bin:$PATH -# 色付き出力 -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color +GREEN="" +YELLOW="" +RED="" +NC="" echo -e "${GREEN}=== Windmill Workflow Git Sync ===${NC}" -# リポジトリルート(コンテナ内: docker-compose.ymlの .:/workspace マウント) REPO_ROOT="/workspace" -# wmill.yamlがあるディレクトリ(Windmill CLIはここで実行する) WMILL_DIR="${REPO_ROOT}/workflows" -# Windmill CLIのセットアップ 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 -# 環境変数チェック -if [ -z "$WM_TOKEN" ]; then - echo -e "${RED}Error: WM_TOKEN is not set.${NC}" - exit 1 -fi -# WM_BASE_URLはWindmill内で自動設定される場合があるが、念のため -: "${WM_BASE_URL:=http://windmill_server:8000}" -# Workspaceは環境変数または引数で -: "${WM_WORKSPACE:=admins}" - -# Git設定(コンテナ内での一時設定) git config --global --add safe.directory "$REPO_ROOT" git config --global user.email "bot@keinafarm.net" git config --global user.name "Windmill Bot" -# 1. Windmill(DB) -> Local Disk(wmill.yamlがあるディレクトリで実行) echo -e "${YELLOW}Pulling from Windmill...${NC}" cd "$WMILL_DIR" -wmill sync pull --token "$WM_TOKEN" --base-url "$WM_BASE_URL" --workspace "$WM_WORKSPACE" --skip-variables --skip-secrets --skip-resources --yes || exit 1 +wmill sync pull --config-dir /workspace/wmill_config --skip-variables --skip-secrets --skip-resources --yes || exit 1 -# 2. Local Disk -> Git Remote(Gitリポジトリルートに戻ってgit操作) cd "$REPO_ROOT" 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') + TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") git commit -m "Auto-sync: ${TIMESTAMP}" - echo -e "${YELLOW}Pushing to Gitea...${NC}" - # リモートURLにトークンが含まれていない場合、プッシュに失敗する可能性がある - # ここでは既存のoriginを使用 git pull --rebase origin main || { echo -e "${RED}Failed to pull from remote. Trying push anyway...${NC}" } git push origin main || { - echo -e "${RED}Failed to push. Need credentials in git remote url or credential helper.${NC}" - echo -e "${YELLOW}Hint: git remote set-url origin https://@gitea.keinafarm.net/...${NC}" + echo -e "${RED}Failed to push.${NC}" exit 1 } - - echo -e "${GREEN}✓ Changes pushed to Gitea${NC}" + echo -e "${GREEN}Changes pushed to Gitea${NC}" else - echo -e "${GREEN}✓ No changes detected${NC}" + echo -e "${GREEN}No changes detected${NC}" fi echo -e "${GREEN}=== Sync Complete ===${NC}" diff --git a/workflows/wmill-lock.yaml b/workflows/wmill-lock.yaml index aeda18d..a4ca909 100644 --- a/workflows/wmill-lock.yaml +++ b/workflows/wmill-lock.yaml @@ -5,13 +5,15 @@ locks: '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/mail/mail_filter__flow+__flow_hash: 08d8ca9e024f743def5c1e8a90e424da3d9884628074f02c37dbb4c00599e9e9 + f/mail/mail_filter__flow+メール取得・判定・通知.py: 0b9cc3ff72d6f3445d46005a657903ae8f195104d1623b47079d13691811c602 f/shiraou/shiraou_notification__flow+__flow_hash: 94825ff4362b6e4b6d165f8e17a51ebf8e5ef4da3e0ec1407a94b614ecab19dd f/shiraou/shiraou_notification__flow+変更確認・line通知.py: ac80896991cce8132cfbf34d5dae20d3c09de5bc74a55c500e4c8705dd6a9d88 g/all/setup_app__app+__app_hash: d71add32e14e552d1a4c861c972a50d9598b07c0af201bbadec5b59bbd99d7e3 g/all/setup_app__app+change_account.deno.ts: 3c592cac27e9cdab0de6ae19270bcb08c7fa54355ad05253a12de2351894346b u/admin/hub_sync: aaf9fd803fa229f3029d1bb02bbe3cc422fce680cad39c4eec8dd1da115de102 - u/antigravity/git_sync__flow+__flow_hash: 4dd9119107cf74b0b98a4907f897f1b0158b76665353552d5e0aa9ba9a408d78 - u/antigravity/git_sync__flow+a.sh: a683bcb9ec70d52a573999b0564172f0600eaf71c5dbf610ad0b3b611580cf62 + u/antigravity/git_sync__flow+__flow_hash: 66cdf1feb6136bb87f65a050266840e7b074a136f4b752bd01dbe524eb8f05d7 + u/antigravity/git_sync__flow+a.sh: 3094bf5aed54e3232c6e0260fa0b3f3849f7fc19930ec2a8395fcfe437cdbe8f u/antigravity/hello_world_demo__flow+__flow_hash: 0adc341960f8196454876684f85fe14ef087ba470322d2aabc99b37bf61edac9 u/antigravity/hello_world_demo__flow+a.ts: 53669a285c16d4ba322888755a33424521f769e9ebf64fc1f0cb21f9952b5958 u/antigravity/test_git_sync: 3aa9e66ad8c87f1c2718d41d78ce3b773ce20743e4a1011396edbe2e7f88ac51