From 71ca8d7f6621e36cf029e6d191fb000e5c7fb69c Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 25 May 2026 16:37:51 +0800 Subject: [PATCH 1/2] Separate Google Voice alert config --- .github/workflows/sync-cloud-run-env.yml | 80 ++++++++--------------- README.md | 32 +++------ requirements.txt | 2 +- runtime_config_support.py | 44 +++---------- tests/test_request_handling.py | 9 +-- tests/test_runtime_config_support.py | 33 +++------- tests/test_sync_cloud_run_env_workflow.sh | 39 ++++------- 7 files changed, 71 insertions(+), 168 deletions(-) diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 2aa574a..40a745f 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -51,14 +51,9 @@ jobs: LONGBRIDGE_MIN_RESERVED_CASH_USD: ${{ vars.LONGBRIDGE_MIN_RESERVED_CASH_USD }} LONGBRIDGE_RESERVED_CASH_RATIO: ${{ vars.LONGBRIDGE_RESERVED_CASH_RATIO }} LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }} - CRISIS_ALERT_GOOGLE_VOICE_TO: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_TO }} - CRISIS_ALERT_SMTP_FROM: ${{ vars.CRISIS_ALERT_SMTP_FROM }} - CRISIS_ALERT_SMTP_HOST: ${{ vars.CRISIS_ALERT_SMTP_HOST }} - CRISIS_ALERT_SMTP_PORT: ${{ vars.CRISIS_ALERT_SMTP_PORT }} - CRISIS_ALERT_SMTP_USERNAME: ${{ vars.CRISIS_ALERT_SMTP_USERNAME }} - CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME }} - CRISIS_ALERT_SMTP_STARTTLS: ${{ vars.CRISIS_ALERT_SMTP_STARTTLS }} - CRISIS_ALERT_SMTP_SSL: ${{ vars.CRISIS_ALERT_SMTP_SSL }} + CRISIS_ALERT_GOOGLE_VOICE_GATEWAY: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GATEWAY }} + CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER }} + CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME }} # Optional strategy overrides; leave unset to inherit the UsEquityStrategies profile defaults. INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }} QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }} @@ -67,7 +62,7 @@ jobs: LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }} GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }} TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} - CRISIS_ALERT_SMTP_PASSWORD: ${{ secrets.CRISIS_ALERT_SMTP_PASSWORD }} + CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD }} steps: - name: Check whether env sync is enabled id: config @@ -302,8 +297,16 @@ jobs: "LONGBRIDGE_FRACTIONAL_SHARES_ENABLED" "LONGBRIDGE_ORDER_QUANTITY_STEP" "LONGBRIDGE_MIN_ORDER_NOTIONAL_USD" + "CRISIS_ALERT_GOOGLE_VOICE_TO" + "CRISIS_ALERT_SMTP_FROM" + "CRISIS_ALERT_SMTP_HOST" + "CRISIS_ALERT_SMTP_PORT" + "CRISIS_ALERT_SMTP_USERNAME" + "CRISIS_ALERT_SMTP_PASSWORD" + "CRISIS_ALERT_SMTP_STARTTLS" + "CRISIS_ALERT_SMTP_SSL" ) - remove_secret_vars=() + remove_secret_vars=("CRISIS_ALERT_SMTP_PASSWORD") if [ -n "${TELEGRAM_TOKEN_SECRET_NAME:-}" ]; then secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest") @@ -313,14 +316,15 @@ jobs: remove_secret_vars+=("TELEGRAM_TOKEN") fi - if [ -n "${CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME:-}" ]; then - secret_pairs+=("CRISIS_ALERT_SMTP_PASSWORD=${CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME}:latest") - elif [ -n "${CRISIS_ALERT_SMTP_PASSWORD:-}" ]; then - env_pairs+=("CRISIS_ALERT_SMTP_PASSWORD=${CRISIS_ALERT_SMTP_PASSWORD}") - remove_secret_vars+=("CRISIS_ALERT_SMTP_PASSWORD") + if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME:-}" ]; then + secret_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME}:latest") + remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD") + elif [ -n "${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD:-}" ]; then + env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD}") + remove_secret_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD") else - remove_env_vars+=("CRISIS_ALERT_SMTP_PASSWORD") - remove_secret_vars+=("CRISIS_ALERT_SMTP_PASSWORD") + remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD") + remove_secret_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD") fi secret_pairs+=("LONGPORT_APP_KEY=${LONGPORT_APP_KEY_SECRET_NAME}:latest") @@ -377,46 +381,16 @@ jobs: remove_env_vars+=("LONGBRIDGE_RESERVED_CASH_RATIO") fi - if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_TO:-}" ]; then - env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_TO=${CRISIS_ALERT_GOOGLE_VOICE_TO}") + if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_GATEWAY:-}" ]; then + env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GATEWAY=${CRISIS_ALERT_GOOGLE_VOICE_GATEWAY}") else - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_TO") + remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GATEWAY") fi - if [ -n "${CRISIS_ALERT_SMTP_FROM:-}" ]; then - env_pairs+=("CRISIS_ALERT_SMTP_FROM=${CRISIS_ALERT_SMTP_FROM}") + if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER:-}" ]; then + env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER}") else - remove_env_vars+=("CRISIS_ALERT_SMTP_FROM") - fi - - if [ -n "${CRISIS_ALERT_SMTP_HOST:-}" ]; then - env_pairs+=("CRISIS_ALERT_SMTP_HOST=${CRISIS_ALERT_SMTP_HOST}") - else - remove_env_vars+=("CRISIS_ALERT_SMTP_HOST") - fi - - if [ -n "${CRISIS_ALERT_SMTP_PORT:-}" ]; then - env_pairs+=("CRISIS_ALERT_SMTP_PORT=${CRISIS_ALERT_SMTP_PORT}") - else - remove_env_vars+=("CRISIS_ALERT_SMTP_PORT") - fi - - if [ -n "${CRISIS_ALERT_SMTP_USERNAME:-}" ]; then - env_pairs+=("CRISIS_ALERT_SMTP_USERNAME=${CRISIS_ALERT_SMTP_USERNAME}") - else - remove_env_vars+=("CRISIS_ALERT_SMTP_USERNAME") - fi - - if [ -n "${CRISIS_ALERT_SMTP_STARTTLS:-}" ]; then - env_pairs+=("CRISIS_ALERT_SMTP_STARTTLS=${CRISIS_ALERT_SMTP_STARTTLS}") - else - remove_env_vars+=("CRISIS_ALERT_SMTP_STARTTLS") - fi - - if [ -n "${CRISIS_ALERT_SMTP_SSL:-}" ]; then - env_pairs+=("CRISIS_ALERT_SMTP_SSL=${CRISIS_ALERT_SMTP_SSL}") - else - remove_env_vars+=("CRISIS_ALERT_SMTP_SSL") + remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER") fi if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then diff --git a/README.md b/README.md index 4401342..ebe3718 100644 --- a/README.md +++ b/README.md @@ -70,14 +70,9 @@ Telegram notifications include structured execution and heartbeat messages, with | `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | No | Set to `true` to log raw LongBridge position quantity and available quantity for troubleshooting. | | `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | No | Optional LongBridge-side strategy plugin mount JSON. The plugin artifact controls mode; platform config must not set `mode`. | -| `CRISIS_ALERT_GOOGLE_VOICE_TO` | No | Comma/semicolon/newline-separated Google Voice SMS gateway recipients, usually ending in `@txt.voice.google.com`. | -| `CRISIS_ALERT_SMTP_FROM` | No | SMTP sender address for Google Voice alerts. | -| `CRISIS_ALERT_SMTP_HOST` | No | SMTP host for Google Voice alerts. | -| `CRISIS_ALERT_SMTP_PORT` | No | SMTP port; defaults to `587`. | -| `CRISIS_ALERT_SMTP_USERNAME` | No | Optional SMTP username. | -| `CRISIS_ALERT_SMTP_PASSWORD` | No | Optional SMTP password. For Cloud Run, prefer `CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME` in env sync. | -| `CRISIS_ALERT_SMTP_STARTTLS` | No | Whether to use STARTTLS; defaults to `true`. | -| `CRISIS_ALERT_SMTP_SSL` | No | Whether to use implicit SMTP SSL; defaults to `false`. | +| `CRISIS_ALERT_GOOGLE_VOICE_GATEWAY` | No | Comma/semicolon/newline-separated Google Voice SMS gateway recipients, usually ending in `@txt.voice.google.com`. | +| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER` | No | Gmail address used to send Google Voice gateway alerts. | +| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` | No | Gmail App Password for Google Voice gateway alerts. For Cloud Run, prefer `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME` in env sync. | | `LONGBRIDGE_MIN_RESERVED_CASH_USD` | No | Platform-level minimum cash reserve in USD. Defaults to `0`; the effective reserve is the max of this floor, `LONGBRIDGE_RESERVED_CASH_RATIO * total equity`, and any strategy-provided reserve. | | `LONGBRIDGE_RESERVED_CASH_RATIO` | No | Platform-level minimum cash reserve ratio in `[0,1]`. Defaults to `0`; it can raise but not lower a strategy-provided reserve. | | `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` | No | Safe-haven/cash-sweep target values below this USD amount are kept as cash instead of buying BOXX/BIL. Default `1000`. | @@ -88,7 +83,7 @@ Telegram notifications include structured execution and heartbeat messages, with Strategy allocation can still target fractional dollar values and fractional position weights. The LongBridge execution layer now keeps a whole-share-only rule for every broker order: sell sizing floors to whole shares, buy sizing floors to whole shares, and fractional orders are skipped rather than downgraded. When a target value is zero, sell sizing uses the sellable position quantity instead of re-deriving shares from current price, so liquidation targets do not leave a residual share because of quote drift. -When `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal Telegram cycle message still includes the compact plugin line. If the plugin signal escalates beyond `no_action` (for example `canonical_route=true_crisis`, `suggested_action=defend`/`blocked`, or `would_trade_if_enabled=true`), the service also sends an independent crisis Google Voice notification when the `CRISIS_ALERT_*` SMTP settings are complete. +When `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal Telegram cycle message still includes the compact plugin line. If the plugin signal escalates beyond `no_action` (for example `canonical_route=true_crisis`, `suggested_action=defend`/`blocked`, or `would_trade_if_enabled=true`), the service also sends an independent crisis Google Voice notification when the `CRISIS_ALERT_GOOGLE_VOICE_*` settings are complete. Google Voice alert results are written into the runtime report. Duplicate suppression uses stable plugin alert keys and stores markers under `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` when set, otherwise `EXECUTION_REPORT_GCS_URI`, with a local `/tmp` marker fallback. Secret Manager must contain the secret named by `LONGPORT_SECRET_NAME` (default: `longport_token_paper`), where the **latest version = active access token**. The app refreshes it when expiry is within 30 days. @@ -129,10 +124,9 @@ Recommended setup: - `TELEGRAM_TOKEN_SECRET_NAME` (recommended: `longbridge-telegram-token`) - `NOTIFY_LANG` - `GLOBAL_TELEGRAM_CHAT_ID` - - Optional crisis Google Voice alerts: `CRISIS_ALERT_GOOGLE_VOICE_TO`, `CRISIS_ALERT_SMTP_FROM`, `CRISIS_ALERT_SMTP_HOST`, `CRISIS_ALERT_SMTP_PORT`, `CRISIS_ALERT_SMTP_USERNAME`, `CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`, `CRISIS_ALERT_SMTP_STARTTLS`, `CRISIS_ALERT_SMTP_SSL` - **Repository Secrets (shared):** - Optional fallback only: `TELEGRAM_TOKEN` - - Optional fallback only: `CRISIS_ALERT_SMTP_PASSWORD` + - Optional fallback only: `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` - **GitHub Environment: `longbridge-paper`** - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `RUNTIME_TARGET_JSON`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `LONGBRIDGE_MIN_RESERVED_CASH_USD`, `LONGBRIDGE_RESERVED_CASH_RATIO`, `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (leave unset to inherit platform and strategy defaults) @@ -243,14 +237,9 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换 | `LONGBRIDGE_DRY_RUN_ONLY` | 否 | 设为 `true` 时,该部署保持 dry-run。 | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | 否 | 设为 `true` 时输出 LongBridge 原始持仓数量和可卖数量,便于排查。 | | `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | 否 | 可选的 LongBridge 侧策略插件挂载 JSON。插件 artifact 自带模式;平台配置不要设置 `mode`。 | -| `CRISIS_ALERT_GOOGLE_VOICE_TO` | 否 | Google Voice 短信网关收件人,通常以 `@txt.voice.google.com` 结尾,支持逗号、分号或换行分隔。 | -| `CRISIS_ALERT_SMTP_FROM` | 否 | Google Voice 告警的 SMTP 发件人。 | -| `CRISIS_ALERT_SMTP_HOST` | 否 | Google Voice 告警 SMTP host。 | -| `CRISIS_ALERT_SMTP_PORT` | 否 | SMTP 端口,默认 `587`。 | -| `CRISIS_ALERT_SMTP_USERNAME` | 否 | 可选 SMTP 用户名。 | -| `CRISIS_ALERT_SMTP_PASSWORD` | 否 | 可选 SMTP 密码。Cloud Run env sync 建议配置 `CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`。 | -| `CRISIS_ALERT_SMTP_STARTTLS` | 否 | 是否使用 STARTTLS,默认 `true`。 | -| `CRISIS_ALERT_SMTP_SSL` | 否 | 是否使用隐式 SMTP SSL,默认 `false`。 | +| `CRISIS_ALERT_GOOGLE_VOICE_GATEWAY` | 否 | Google Voice 短信网关收件人,通常以 `@txt.voice.google.com` 结尾,支持逗号、分号或换行分隔。 | +| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER` | 否 | Google Voice 通知使用的 Gmail 地址。 | +| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` | 否 | Google Voice 通知使用的 Gmail App Password。Cloud Run env sync 建议配置 `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME`。 | | `LONGBRIDGE_MIN_RESERVED_CASH_USD` | 否 | 平台级最低预留现金 USD。默认 `0`;实际预留取该下限、`LONGBRIDGE_RESERVED_CASH_RATIO * 总资产` 和策略预留中的最大值。 | | `LONGBRIDGE_RESERVED_CASH_RATIO` | 否 | 平台级最低预留现金比例,取值 `[0,1]`。默认 `0`;只会抬高,不会降低策略预留。 | | `LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` | 否 | `BOXX`/`BIL` 等避险现金替代标的目标金额低于该 USD 门槛时保留现金,不买入。默认 `1000`。 | @@ -261,7 +250,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换 策略分配层仍然可以按目标金额和目标比例计算出小数仓位;LongBridge 执行层只提交整数股订单,因为实测账户的 OpenAPI `submit_order` 会拒绝碎股委托数量。目标市值为 0 时,卖出数量直接按可卖整数股持仓计算,不再用当前报价反推股数,避免因报价漂移留下 1 股残仓。 -如果 `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` 挂载了 `crisis_response_shadow` 插件,常规策略周期 Telegram 仍会包含插件摘要行。当插件信号升级到非 `no_action`(例如 `canonical_route=true_crisis`、`suggested_action=defend`/`blocked`,或 `would_trade_if_enabled=true`)时,只要 `CRISIS_ALERT_*` SMTP 配置完整,服务还会额外发一封独立 Google Voice 危机通知。 +如果 `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` 挂载了 `crisis_response_shadow` 插件,常规策略周期 Telegram 仍会包含插件摘要行。当插件信号升级到非 `no_action`(例如 `canonical_route=true_crisis`、`suggested_action=defend`/`blocked`,或 `would_trade_if_enabled=true`)时,只要 `CRISIS_ALERT_GOOGLE_VOICE_*` 配置完整,服务还会额外发送一条独立 Google Voice 危机通知。 Google Voice 告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `longport_token_paper`),**最新版本 = 当前有效的 access token**。Token 到期前 30 天会自动刷新。 @@ -302,10 +291,9 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo - `TELEGRAM_TOKEN_SECRET_NAME`(建议:`longbridge-telegram-token`) - `NOTIFY_LANG` - `GLOBAL_TELEGRAM_CHAT_ID` - - 可选危机插件 Google Voice 告警:`CRISIS_ALERT_GOOGLE_VOICE_TO`、`CRISIS_ALERT_SMTP_FROM`、`CRISIS_ALERT_SMTP_HOST`、`CRISIS_ALERT_SMTP_PORT`、`CRISIS_ALERT_SMTP_USERNAME`、`CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`、`CRISIS_ALERT_SMTP_STARTTLS`、`CRISIS_ALERT_SMTP_SSL` - **仓库级 Secrets(共享):** - 仅保留为 fallback:`TELEGRAM_TOKEN` - - 仅保留为 fallback:`CRISIS_ALERT_SMTP_PASSWORD` + - 仅保留为 fallback:`CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` - **GitHub Environment: `longbridge-paper`** - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`RUNTIME_TARGET_JSON`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`LONGBRIDGE_MIN_RESERVED_CASH_USD`、`LONGBRIDGE_RESERVED_CASH_RATIO`、`LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(不填则继承平台和策略默认值) diff --git a/requirements.txt b/requirements.txt index 46d3776..8d1fe21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@4305a3c01151ced7e78b39519959444309326cd7 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@2de98b250b437ecb2076197b34de291df080e760 us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@03e23c5d6a620ca9ecb763550452f9ca3870481a pandas requests diff --git a/runtime_config_support.py b/runtime_config_support.py index 91ea71b..a02f74b 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -54,14 +54,9 @@ class PlatformRuntimeSettings: strategy_config_path: str | None = None strategy_config_source: str | None = None strategy_plugin_mounts_json: str | None = None - crisis_alert_google_voice_to: tuple[str, ...] = () - crisis_alert_smtp_from: str | None = None - crisis_alert_smtp_host: str | None = None - crisis_alert_smtp_port: int = 587 - crisis_alert_smtp_username: str | None = None - crisis_alert_smtp_password: str | None = None - crisis_alert_smtp_starttls: bool = True - crisis_alert_smtp_ssl: bool = False + crisis_alert_google_voice_gateway: tuple[str, ...] = () + crisis_alert_google_voice_gmail_user: str | None = None + crisis_alert_google_voice_gmail_app_password: str | None = None runtime_target: RuntimeTarget | None = None @@ -158,14 +153,11 @@ def load_platform_runtime_settings( os.getenv("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON") or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON") ), - crisis_alert_google_voice_to=_split_env_list(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_TO")), - crisis_alert_smtp_from=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_FROM")), - crisis_alert_smtp_host=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_HOST")), - crisis_alert_smtp_port=_resolve_positive_int_env("CRISIS_ALERT_SMTP_PORT", default=587), - crisis_alert_smtp_username=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_USERNAME")), - crisis_alert_smtp_password=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_PASSWORD")), - crisis_alert_smtp_starttls=_resolve_bool_env("CRISIS_ALERT_SMTP_STARTTLS", default=True), - crisis_alert_smtp_ssl=_resolve_bool_env("CRISIS_ALERT_SMTP_SSL", default=False), + crisis_alert_google_voice_gateway=_split_env_list(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GATEWAY")), + crisis_alert_google_voice_gmail_user=_first_non_empty(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER")), + crisis_alert_google_voice_gmail_app_password=_first_non_empty( + os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD") + ), runtime_target=runtime_target, ) @@ -212,26 +204,6 @@ def _first_non_empty(*raw_values: str | None) -> str | None: return None -def _resolve_bool_env(name: str, *, default: bool) -> bool: - raw_value = os.getenv(name) - if raw_value is None or str(raw_value).strip() == "": - return bool(default) - return resolve_bool_value(raw_value) - - -def _resolve_positive_int_env(name: str, *, default: int) -> int: - raw_value = os.getenv(name) - if raw_value is None or str(raw_value).strip() == "": - return int(default) - try: - value = int(raw_value) - except (TypeError, ValueError): - raise ValueError(f"{name} must be a positive integer, got {raw_value!r}") from None - if value <= 0: - raise ValueError(f"{name} must be a positive integer, got {raw_value!r}") - return value - - def _split_env_list(raw_value: str | None) -> tuple[str, ...]: if raw_value is None: return () diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index e8f6f7b..ff90e56 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -66,12 +66,9 @@ def run(self, *args, **kwargs): tg_token=None, tg_chat_id="shared-chat-id", dry_run_only=False, - crisis_alert_smtp_host=None, - crisis_alert_smtp_port=587, - crisis_alert_smtp_username=None, - crisis_alert_smtp_password=None, - crisis_alert_smtp_starttls=True, - crisis_alert_smtp_ssl=False, + crisis_alert_google_voice_gateway=(), + crisis_alert_google_voice_gmail_user=None, + crisis_alert_google_voice_gmail_app_password=None, runtime_target=build_runtime_target( platform_id="longbridge", strategy_profile="soxl_soxx_trend_income", diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 1e55eb4..4cd3c43 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -119,14 +119,9 @@ def test_load_platform_runtime_settings_uses_defaults_with_explicit_strategy_pro self.assertIsNone(settings.feature_snapshot_path) self.assertIsNone(settings.strategy_config_path) self.assertIsNone(settings.strategy_plugin_mounts_json) - self.assertEqual(settings.crisis_alert_google_voice_to, ()) - self.assertIsNone(settings.crisis_alert_smtp_from) - self.assertIsNone(settings.crisis_alert_smtp_host) - self.assertEqual(settings.crisis_alert_smtp_port, 587) - self.assertIsNone(settings.crisis_alert_smtp_username) - self.assertIsNone(settings.crisis_alert_smtp_password) - self.assertTrue(settings.crisis_alert_smtp_starttls) - self.assertFalse(settings.crisis_alert_smtp_ssl) + self.assertEqual(settings.crisis_alert_google_voice_gateway, ()) + self.assertIsNone(settings.crisis_alert_google_voice_gmail_user) + self.assertIsNone(settings.crisis_alert_google_voice_gmail_app_password) def test_load_platform_runtime_settings_prefers_runtime_target_json(self): with patch.dict( @@ -279,27 +274,17 @@ def test_crisis_alert_google_voice_config_is_loaded_from_env(self): os.environ, { "RUNTIME_TARGET_JSON": runtime_target_json(SAMPLE_STRATEGY_PROFILE), - "CRISIS_ALERT_GOOGLE_VOICE_TO": "gateway@txt.voice.google.com", - "CRISIS_ALERT_SMTP_FROM": "smtp-from@example.com", - "CRISIS_ALERT_SMTP_HOST": "smtp.example.com", - "CRISIS_ALERT_SMTP_PORT": "465", - "CRISIS_ALERT_SMTP_USERNAME": "bot", - "CRISIS_ALERT_SMTP_PASSWORD": "secret", - "CRISIS_ALERT_SMTP_STARTTLS": "false", - "CRISIS_ALERT_SMTP_SSL": "true", + "CRISIS_ALERT_GOOGLE_VOICE_GATEWAY": "gateway@txt.voice.google.com", + "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER": "sender@gmail.com", + "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD": "secret", }, clear=True, ): settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") - self.assertEqual(settings.crisis_alert_google_voice_to, ("gateway@txt.voice.google.com",)) - self.assertEqual(settings.crisis_alert_smtp_from, "smtp-from@example.com") - self.assertEqual(settings.crisis_alert_smtp_host, "smtp.example.com") - self.assertEqual(settings.crisis_alert_smtp_port, 465) - self.assertEqual(settings.crisis_alert_smtp_username, "bot") - self.assertEqual(settings.crisis_alert_smtp_password, "secret") - self.assertFalse(settings.crisis_alert_smtp_starttls) - self.assertTrue(settings.crisis_alert_smtp_ssl) + self.assertEqual(settings.crisis_alert_google_voice_gateway, ("gateway@txt.voice.google.com",)) + self.assertEqual(settings.crisis_alert_google_voice_gmail_user, "sender@gmail.com") + self.assertEqual(settings.crisis_alert_google_voice_gmail_app_password, "secret") def test_income_layer_overrides_are_loaded_from_env(self): with patch.dict( diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 21c60e2..aeacda5 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -36,7 +36,7 @@ grep -Fq 'Timed out waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy grep -Fq 'ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}' "$workflow_file" grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}' "$workflow_file" grep -Fq 'TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_PASSWORD: ${{ secrets.CRISIS_ALERT_SMTP_PASSWORD }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD }}' "$workflow_file" grep -Fq 'TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}' "$workflow_file" grep -Fq 'LONGPORT_APP_KEY_SECRET_NAME: ${{ vars.LONGPORT_APP_KEY_SECRET_NAME }}' "$workflow_file" grep -Fq 'LONGPORT_APP_SECRET_SECRET_NAME: ${{ vars.LONGPORT_APP_SECRET_SECRET_NAME }}' "$workflow_file" @@ -48,14 +48,9 @@ grep -Fq 'LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_P grep -Fq 'LONGBRIDGE_MIN_RESERVED_CASH_USD: ${{ vars.LONGBRIDGE_MIN_RESERVED_CASH_USD }}' "$workflow_file" grep -Fq 'LONGBRIDGE_RESERVED_CASH_RATIO: ${{ vars.LONGBRIDGE_RESERVED_CASH_RATIO }}' "$workflow_file" grep -Fq 'LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_TO: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_TO }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_FROM: ${{ vars.CRISIS_ALERT_SMTP_FROM }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_HOST: ${{ vars.CRISIS_ALERT_SMTP_HOST }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_PORT: ${{ vars.CRISIS_ALERT_SMTP_PORT }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_USERNAME: ${{ vars.CRISIS_ALERT_SMTP_USERNAME }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_STARTTLS: ${{ vars.CRISIS_ALERT_SMTP_STARTTLS }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_SSL: ${{ vars.CRISIS_ALERT_SMTP_SSL }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_GATEWAY: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GATEWAY }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME }}' "$workflow_file" grep -Fq 'INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}' "$workflow_file" grep -Fq 'QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}' "$workflow_file" grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}' "$workflow_file" @@ -84,7 +79,7 @@ grep -Fq 'if [ "${REQUIRES_SNAPSHOT_MANIFEST_PATH:-}" = "true" ] && [ -z "${LONG grep -Fq '&& [ "${CONFIG_SOURCE_POLICY:-}" = "env_only" ] \' "$workflow_file" grep -Fq '&& [ -z "${LONGBRIDGE_STRATEGY_CONFIG_PATH:-}" ]; then' "$workflow_file" grep -Fq 'secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest")' "$workflow_file" -grep -Fq 'secret_pairs+=("CRISIS_ALERT_SMTP_PASSWORD=${CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME}:latest")' "$workflow_file" +grep -Fq 'secret_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("LONGPORT_APP_KEY=${LONGPORT_APP_KEY_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("LONGPORT_APP_SECRET=${LONGPORT_APP_SECRET_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'LONGPORT_SECRET_NAME=${LONGPORT_SECRET_NAME}' "$workflow_file" @@ -98,22 +93,14 @@ grep -Fq 'LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD=${LONGBRIDGE_SAFE_ grep -Fq 'remove_env_vars+=("LONGBRIDGE_MIN_RESERVED_CASH_USD")' "$workflow_file" grep -Fq 'remove_env_vars+=("LONGBRIDGE_RESERVED_CASH_RATIO")' "$workflow_file" grep -Fq 'remove_env_vars+=("LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD")' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_TO=${CRISIS_ALERT_GOOGLE_VOICE_TO}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_FROM=${CRISIS_ALERT_SMTP_FROM}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_HOST=${CRISIS_ALERT_SMTP_HOST}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_PORT=${CRISIS_ALERT_SMTP_PORT}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_USERNAME=${CRISIS_ALERT_SMTP_USERNAME}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_PASSWORD=${CRISIS_ALERT_SMTP_PASSWORD}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_STARTTLS=${CRISIS_ALERT_SMTP_STARTTLS}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_SMTP_SSL=${CRISIS_ALERT_SMTP_SSL}' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_TO")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_FROM")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_HOST")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_PORT")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_USERNAME")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_PASSWORD")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_STARTTLS")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_SSL")' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_GATEWAY=${CRISIS_ALERT_GOOGLE_VOICE_GATEWAY}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD}' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GATEWAY")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")' "$workflow_file" +grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_TO"' "$workflow_file" +grep -Fq '"CRISIS_ALERT_SMTP_HOST"' "$workflow_file" grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}' "$workflow_file" grep -Fq 'INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}' "$workflow_file" grep -Fq 'QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}' "$workflow_file" From 5da99484e97080749f4d245a1c3ac3fdfa97a4db Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 25 May 2026 17:11:38 +0800 Subject: [PATCH 2/2] Sync Google Voice dependency pins --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8d1fe21..b64e315 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@2de98b250b437ecb2076197b34de291df080e760 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@03e23c5d6a620ca9ecb763550452f9ca3870481a +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@34a2c74deb7aafef0f6f4b278444d7b0efb76794 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@861eaedc1caecf1e33ff12f55bbe9af87a221df1 pandas requests pytz