""" AI Control Service — Phase 24 C (2026-04-03 ogt) Telegram /ai 指令動態控制 AI Router 狀態 - Redis 狀態優先於 env var - OPENCLAW_TG_USER_WHITELIST 白名單保護 Redis Keys: ai:control:use_router "true"/"false" (TTL: 30天) ai:control:primary_provider legacy compatibility; only "ollama"/"ollama_gcp_a" ai:control:disabled: "1" disabled / paid provider "0" enabled only under a run-owned expiring lease Usage: /ai status — 顯示所有 Provider 狀態 + 當前路由模式 /ai primary

— legacy compatibility; production always starts at GCP-A /ai enable

— 啟用特定 Provider /ai disable

— 停用特定 Provider /ai cost — 顯示費用統計 /ai router on — 啟用 AIRouter (覆蓋 env var) /ai router off — 停用 AIRouter (回滾到舊 fallback chain) """ import hashlib import json import re from typing import Any import structlog logger = structlog.get_logger(__name__) # Redis Key 常數 _AI_ROUTER_KEY = "ai:control:use_router" _PRIMARY_PROVIDER_KEY = "ai:control:primary_provider" _DISABLED_KEY_PREFIX = "ai:control:disabled:" _PAID_CANARY_LOCK_KEY = "ai:control:paid_canary:activation_lock" _PAID_CANARY_ACTIVE_RECEIPT_KEY = "ai:control:paid_canary:active_receipt" _PAID_CANARY_EXECUTION_CLAIM_PREFIX = "ai:control:paid_canary:execution_claim:" _PAID_CANARY_OWNER_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,191}$") _PAID_CANARY_MIN_LEASE_SECONDS = 60 _PAID_CANARY_MAX_LEASE_SECONDS = 900 _ACQUIRE_PAID_CANARY_LEASE_LUA = r""" if redis.call('EXISTS', KEYS[1]) == 1 then return {0, 'activation_lock_held'} end if redis.call('EXISTS', KEYS[4]) == 1 then return {0, 'persistent_activation_already_recorded'} end if redis.call('GET', KEYS[2]) == '0' or redis.call('GET', KEYS[3]) == '0' then return {0, 'paid_provider_already_enabled'} end redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]) redis.call('SET', KEYS[2], '0', 'EX', ARGV[2]) redis.call('SET', KEYS[3], '0', 'EX', ARGV[2]) return {1, 'activation_lease_acquired'} """ _ROLLBACK_PAID_CANARY_LUA = r""" local lock_owner = redis.call('GET', KEYS[1]) local active_owner = redis.call('GET', KEYS[4]) if lock_owner and lock_owner ~= ARGV[1] then return {0, 'activation_owner_mismatch'} end if active_owner and string.sub(active_owner, 1, string.len(ARGV[1]) + 1) ~= ARGV[1] .. '|' then return {0, 'active_receipt_owner_mismatch'} end redis.call('SET', KEYS[2], '1') redis.call('SET', KEYS[3], '1') redis.call('DEL', KEYS[1]) redis.call('DEL', KEYS[4]) return {1, 'paid_canary_disabled'} """ _READ_PAID_CANARY_CONTROL_LUA = r""" return { redis.call('GET', KEYS[1]) or '', redis.call('TTL', KEYS[1]), redis.call('GET', KEYS[2]) or '', redis.call('TTL', KEYS[2]), redis.call('GET', KEYS[3]) or '', redis.call('TTL', KEYS[3]), redis.call('GET', KEYS[4]) or '' } """ _ACQUIRE_PAID_CANARY_EXECUTION_CLAIM_LUA = r""" if redis.call('EXISTS', KEYS[1]) == 1 then return {0, 'execution_claim_held'} end redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]) return {1, 'execution_claim_acquired'} """ _RELEASE_PAID_CANARY_EXECUTION_CLAIM_LUA = r""" if redis.call('GET', KEYS[1]) ~= ARGV[1] then return {0, 'execution_claim_owner_mismatch'} end redis.call('DEL', KEYS[1]) return {1, 'execution_claim_released'} """ _SET_PAID_PROVIDER_STATE_LUA = r""" redis.call('SET', KEYS[2], '1') redis.call('SET', KEYS[3], '1') redis.call('DEL', KEYS[1]) redis.call('DEL', KEYS[4]) return {1, 'paired_paid_providers_disabled'} """ _PAID_PROVIDER_ADMISSION_LUA = r""" local claude_state = redis.call('GET', KEYS[2]) local gemini_state = redis.call('GET', KEYS[3]) if claude_state ~= '0' or gemini_state ~= '0' then return {0, 'paired_paid_state_not_enabled', ''} end local lock_owner = redis.call('GET', KEYS[1]) local active_owner = redis.call('GET', KEYS[4]) if lock_owner then local expected_prefix = ARGV[1] .. '.attempt-' local run_matches = ARGV[1] ~= '' and string.sub(lock_owner, 1, string.len(expected_prefix)) == expected_prefix local lease_live = redis.call('TTL', KEYS[1]) > 0 and redis.call('TTL', KEYS[2]) > 0 and redis.call('TTL', KEYS[3]) > 0 local promoted_pending_release = active_owner and string.sub(active_owner, 1, string.len(lock_owner) + 1) == lock_owner .. '|' and redis.call('TTL', KEYS[2]) == -1 and redis.call('TTL', KEYS[3]) == -1 if run_matches and (lease_live or promoted_pending_release) then return {1, 'run_owned_canary_admitted', lock_owner} end return {0, 'run_owned_canary_not_admitted', lock_owner} end if active_owner then return {0, 'legacy_persistent_activation_blocked', active_owner} end return {0, 'paid_activation_receipt_missing', ''} """ # Free-provider/router compatibility controls retain the historical 30-day # expiry. Paid providers are executable only during a run-owned expiring lease. _CONTROL_KEY_TTL = 30 * 24 * 3600 # 30 days PAID_PROVIDERS = {"claude", "gemini"} VALID_PROVIDERS = { "ollama", "ollama_gcp_a", "ollama_gcp_b", "ollama_local", "claude", "gemini", } VALID_PRIMARY_PROVIDERS = {"ollama", "ollama_gcp_a"} SHADOW_METADATA_ONLY_PROVIDERS = { "nvidia", "nemotron", "openclaw", "openclaw_nemo", "ollama_tool", } async def _get_redis(): from src.core.redis_client import get_redis return get_redis() async def get_ai_router_enabled() -> bool | None: """ 從 Redis 取得 AIRouter 啟用狀態。 回傳 None 表示未設定,使用 env var (USE_AI_ROUTER)。 """ try: r = await _get_redis() val = await r.get(_AI_ROUTER_KEY) if val is None: return None return val.decode() == "true" if isinstance(val, bytes) else val == "true" except Exception as e: logger.warning("ai_control_redis_read_failed", key=_AI_ROUTER_KEY, error=str(e)) return None async def set_ai_router_enabled(enabled: bool) -> bool: """設定 AIRouter 啟用狀態到 Redis""" try: r = await _get_redis() await r.set(_AI_ROUTER_KEY, "true" if enabled else "false", ex=_CONTROL_KEY_TTL) logger.info("ai_control_router_set", enabled=enabled) return True except Exception as e: logger.error("ai_control_router_set_failed", error=str(e)) return False async def get_primary_provider() -> str | None: """從 Redis 取得主要 Provider,None 表示使用 Router 智慧路由""" try: r = await _get_redis() val = await r.get(_PRIMARY_PROVIDER_KEY) if val is None: return None return val.decode() if isinstance(val, bytes) else val except Exception: return None async def set_primary_provider(provider: str) -> bool: """Retain only the fixed GCP-A-compatible legacy override.""" if provider not in VALID_PRIMARY_PROVIDERS: return False try: r = await _get_redis() await r.set(_PRIMARY_PROVIDER_KEY, provider, ex=_CONTROL_KEY_TTL) logger.info("ai_control_primary_set", provider=provider) return True except Exception as e: logger.error("ai_control_primary_set_failed", error=str(e)) return False async def clear_primary_provider() -> bool: """清除主要 Provider 設定(回歸智慧路由)""" try: r = await _get_redis() await r.delete(_PRIMARY_PROVIDER_KEY) return True except Exception: return False async def is_provider_disabled( provider: str, *, run_id: str | None = None, ) -> bool: """檢查 Provider 是否被停用""" try: r = await _get_redis() if provider in PAID_PROVIDERS: result = await r.eval( _PAID_PROVIDER_ADMISSION_LUA, 4, *_paid_canary_keys(), str(run_id or ""), ) admitted = int(result[0]) == 1 admission_status = _decode_redis_scalar(result[1]) admission_receipt = _decode_redis_scalar(result[2]) if not admitted: return True if admission_status == "run_owned_canary_admitted": return not _valid_paid_canary_owner(admission_receipt) # Unknown admission states are not forward-compatible for paid # execution. A server-side script change must be paired with an # explicit client policy update. return True val = await r.get(f"{_DISABLED_KEY_PREFIX}{provider}") # Free Ollama lanes retain their historical missing-key=enabled # behaviour. Paid providers are admitted only by the paired, # receipt-backed transaction above. return val is not None except Exception: # Paid-provider disable truth is fail-closed. Callers can catch this to # publish an unavailable readback, but must never re-add a paid lane. if provider in PAID_PROVIDERS: raise return False async def set_provider_disabled(provider: str, disabled: bool) -> bool: """設定 Provider 啟用/停用""" if provider not in VALID_PROVIDERS: return False if provider in PAID_PROVIDERS and not disabled: logger.warning( "ai_control_paid_provider_generic_enable_blocked", provider=provider, reason="run_owned_paid_canary_required", ) return False try: r = await _get_redis() key = f"{_DISABLED_KEY_PREFIX}{provider}" if provider in PAID_PROVIDERS: result = await r.eval( _SET_PAID_PROVIDER_STATE_LUA, 4, *_paid_canary_keys(), ) if int(result[0]) != 1: logger.warning( "ai_control_paid_provider_set_blocked", provider=provider, disabled=disabled, status=_decode_redis_scalar(result[1]), ) return False elif disabled: await r.set(key, "1", ex=_CONTROL_KEY_TTL) else: await r.delete(key) logger.info("ai_control_provider_set", provider=provider, disabled=disabled) return True except Exception as e: logger.error("ai_control_provider_set_failed", error=str(e)) return False def _paid_canary_keys() -> tuple[str, str, str, str]: return ( _PAID_CANARY_LOCK_KEY, f"{_DISABLED_KEY_PREFIX}claude", f"{_DISABLED_KEY_PREFIX}gemini", _PAID_CANARY_ACTIVE_RECEIPT_KEY, ) def _valid_paid_canary_owner(owner: str) -> bool: return bool(_PAID_CANARY_OWNER_PATTERN.fullmatch(str(owner or ""))) def _paid_canary_execution_claim_key(run_id: str) -> str: digest = hashlib.sha256(str(run_id).encode("utf-8")).hexdigest() return f"{_PAID_CANARY_EXECUTION_CLAIM_PREFIX}{digest}" def _decode_redis_scalar(value: Any) -> str: if isinstance(value, bytes): return value.decode("utf-8", errors="replace") return str(value or "") async def acquire_paid_provider_canary_lease( owner: str, *, ttl_seconds: int = 300, ) -> bool: """Acquire one run-owned, crash-safe paid-provider activation lease.""" if not _valid_paid_canary_owner(owner): return False ttl = int(ttl_seconds) if not _PAID_CANARY_MIN_LEASE_SECONDS <= ttl <= _PAID_CANARY_MAX_LEASE_SECONDS: return False try: redis = await _get_redis() result = await redis.eval( _ACQUIRE_PAID_CANARY_LEASE_LUA, 4, *_paid_canary_keys(), owner, ttl, ) acquired = int(result[0]) == 1 logger.info( "ai_control_paid_canary_lease_acquire", acquired=acquired, status=_decode_redis_scalar(result[1]), ttl_seconds=ttl, ) return acquired except Exception as exc: logger.error( "ai_control_paid_canary_lease_acquire_failed", error_type=type(exc).__name__, ) return False async def acquire_paid_provider_canary_execution_claim( run_id: str, owner: str, *, ttl_seconds: int = 120, ) -> bool: """Serialize one Gate 5 authorization before any provider preflight.""" if not _valid_paid_canary_owner(run_id) or not _valid_paid_canary_owner(owner): return False ttl = int(ttl_seconds) if not _PAID_CANARY_MIN_LEASE_SECONDS <= ttl <= _PAID_CANARY_MAX_LEASE_SECONDS: return False try: redis = await _get_redis() result = await redis.eval( _ACQUIRE_PAID_CANARY_EXECUTION_CLAIM_LUA, 1, _paid_canary_execution_claim_key(run_id), owner, ttl, ) acquired = int(result[0]) == 1 logger.info( "ai_control_paid_canary_execution_claim_acquire", acquired=acquired, status=_decode_redis_scalar(result[1]), ttl_seconds=ttl, ) return acquired except Exception as exc: logger.error( "ai_control_paid_canary_execution_claim_acquire_failed", error_type=type(exc).__name__, ) return False async def release_paid_provider_canary_execution_claim( run_id: str, owner: str, ) -> bool: """CAS-release only the caller's pre-execution claim.""" if not _valid_paid_canary_owner(run_id) or not _valid_paid_canary_owner(owner): return False try: redis = await _get_redis() result = await redis.eval( _RELEASE_PAID_CANARY_EXECUTION_CLAIM_LUA, 1, _paid_canary_execution_claim_key(run_id), owner, ) released = int(result[0]) == 1 logger.info( "ai_control_paid_canary_execution_claim_release", released=released, status=_decode_redis_scalar(result[1]), ) return released except Exception as exc: logger.error( "ai_control_paid_canary_execution_claim_release_failed", error_type=type(exc).__name__, ) return False async def promote_paid_provider_canary_activation( owner: str, *, completion_record_id: str, ) -> bool: """Reject persistent enablement without a separate bounded authorization. A Gate 5 receipt authorizes one synthetic comparison only. Keeping this compatibility function fail closed prevents an internal caller from turning that receipt into future paid-alert traffic. """ _ = (owner, completion_record_id) logger.warning( "ai_control_paid_canary_persistent_promotion_blocked", reason="separate_time_and_cost_bounded_authorization_required", ) return False async def rollback_paid_provider_canary_activation(owner: str) -> bool: """Fail closed without overwriting another run's activation state.""" if not _valid_paid_canary_owner(owner): return False try: redis = await _get_redis() result = await redis.eval( _ROLLBACK_PAID_CANARY_LUA, 4, *_paid_canary_keys(), owner, ) rolled_back = int(result[0]) == 1 logger.info( "ai_control_paid_canary_activation_rollback", rolled_back=rolled_back, status=_decode_redis_scalar(result[1]), ) return rolled_back except Exception as exc: logger.error( "ai_control_paid_canary_activation_rollback_failed", error_type=type(exc).__name__, ) return False async def get_paid_provider_canary_control_readback( *, expected_owner: str | None = None, expected_completion_record_id: str | None = None, ) -> dict[str, Any]: """Return a content-free, atomic readback of paid canary control state.""" if expected_owner is not None and not _valid_paid_canary_owner(expected_owner): raise ValueError("paid_canary_owner_invalid") if expected_completion_record_id is not None and not _valid_paid_canary_owner( expected_completion_record_id ): raise ValueError("paid_canary_completion_record_id_invalid") redis = await _get_redis() result = await redis.eval( _READ_PAID_CANARY_CONTROL_LUA, 4, *_paid_canary_keys(), ) lock_owner = _decode_redis_scalar(result[0]) lock_ttl = int(result[1]) claude_value = _decode_redis_scalar(result[2]) claude_ttl = int(result[3]) gemini_value = _decode_redis_scalar(result[4]) gemini_ttl = int(result[5]) active_receipt = _decode_redis_scalar(result[6]) active_owner, separator, completion_record_id = active_receipt.partition("|") active_completion_receipt_present = bool( separator and _valid_paid_canary_owner(completion_record_id) ) active_completion_receipt_matches = bool( expected_completion_record_id and completion_record_id == expected_completion_record_id ) claude_disabled = claude_value != "0" gemini_disabled = gemini_value != "0" lock_matches = bool(expected_owner and lock_owner == expected_owner) active_matches = bool(expected_owner and active_owner == expected_owner) leased_enabled = bool( lock_matches and not active_receipt and not claude_disabled and not gemini_disabled and lock_ttl > 0 and claude_ttl > 0 and gemini_ttl > 0 ) legacy_persistent_state_detected = bool( active_receipt or ( not claude_disabled and not gemini_disabled and claude_ttl == -1 and gemini_ttl == -1 ) ) disabled_verified = bool( claude_disabled and gemini_disabled and not lock_owner and not active_receipt ) return { "claude_disabled": claude_disabled, "gemini_disabled": gemini_disabled, "lock_present": bool(lock_owner), "lock_owner_matches": lock_matches, "lock_ttl_seconds": lock_ttl, "active_receipt_present": bool(active_receipt), "active_receipt_owner_matches": active_matches, "active_completion_receipt_present": active_completion_receipt_present, "active_completion_receipt_matches": active_completion_receipt_matches, "leased_enabled": leased_enabled, "persistent_promoted": False, "persistent_enabled": False, "legacy_persistent_state_detected": legacy_persistent_state_detected, "disabled_verified": disabled_verified, } async def get_status_summary() -> str: """ 取得 AI 控制狀態摘要 (Telegram 格式 HTML) """ from src.core.config import get_settings settings = get_settings() # AIRouter 狀態 redis_router = await get_ai_router_enabled() if redis_router is not None: router_status = "✅ ON (Redis 覆蓋)" if redis_router else "❌ OFF (Redis 覆蓋)" else: router_status = "✅ ON (env)" if settings.USE_AI_ROUTER else "❌ OFF (env)" # Primary Provider primary = await get_primary_provider() primary_str = f"🎯 {primary}" if primary else "🤖 智慧路由 (AIRouter 決定)" # Provider 狀態:只讀既有 health/control receipts,不呼叫模型或端點。 ollama_specs = ( ("ollama_gcp_a", "GCP-A", "OLLAMA_URL"), ("ollama_gcp_b", "GCP-B", "OLLAMA_SECONDARY_URL"), ("ollama_local", "111", "OLLAMA_FALLBACK_URL"), ) ollama_health = {provider: "not_observed" for provider, _, _ in ollama_specs} try: from src.services.ollama_health_monitor import make_cache_key health_redis = await _get_redis() for provider, _, setting_name in ollama_specs: endpoint = str(getattr(settings, setting_name, "") or "").strip() if not endpoint: ollama_health[provider] = "not_configured" continue try: raw = await health_redis.get(make_cache_key(endpoint)) if raw: if isinstance(raw, bytes): raw = raw.decode() receipt = json.loads(raw) ollama_health[provider] = str( receipt.get("status") or "invalid_receipt" ) except Exception as exc: ollama_health[provider] = "readback_unavailable" logger.warning( "ai_control_ollama_health_receipt_read_failed", provider=provider, error_type=type(exc).__name__, ) except Exception as exc: logger.warning( "ai_control_ollama_health_readback_unavailable", error_type=type(exc).__name__, ) provider_lines = [] for provider, label, _ in ollama_specs: try: control = "disabled" if await is_provider_disabled(provider) else "enabled" except Exception: control = "readback_unavailable" health = ollama_health[provider] if control == "disabled": icon = "⏸️" elif health == "healthy": icon = "🟢" elif health in {"slow", "degraded"}: icon = "🟠" elif health == "offline": icon = "🔴" else: icon = "⚪" provider_lines.append( f" {icon} {label} | health={health} | control={control}" ) paid_control: dict[str, Any] = {} route_lease = "readback_unavailable" try: paid_control = await get_paid_provider_canary_control_readback() if paid_control.get("legacy_persistent_state_detected") is True: route_lease = "legacy_persistent_state_detected" elif ( paid_control.get("lock_present") is True and paid_control.get("claude_disabled") is False and paid_control.get("gemini_disabled") is False and int(paid_control.get("lock_ttl_seconds") or 0) > 0 ): route_lease = "active_run_owned_lease" elif paid_control.get("disabled_verified") is True: route_lease = "待單次 run-owned lease" else: route_lease = "inconsistent_fail_closed" except Exception as exc: logger.warning( "ai_control_paid_control_status_readback_unavailable", error_type=type(exc).__name__, ) authentication: dict[str, dict[str, Any]] = {} try: from src.services.ai_rate_limiter import get_ai_rate_limiter limiter = get_ai_rate_limiter() for provider in ("claude", "gemini"): try: authentication[provider] = ( await limiter.get_provider_authentication_status(provider) ) except Exception: authentication[provider] = { "configured": None, "authenticated": None, "authentication_verified": False, "verification_status": "authentication_readback_unavailable", } except Exception as exc: logger.warning( "ai_control_paid_authentication_status_readback_unavailable", error_type=type(exc).__name__, ) def _truth_text(value: Any) -> str: if value is True: return "true" if value is False: return "false" return "unknown" for provider, label in (("claude", "Claude"), ("gemini", "Gemini")): auth = authentication.get(provider) or {} configured = auth.get("configured") mode = str( getattr(settings, f"{provider.upper()}_EXECUTION_MODE", "shadow") ).lower() percent = int( getattr(settings, f"{provider.upper()}_CANARY_PERCENT", 0) or 0 ) if configured is True and auth.get("authenticated") is False: icon = "⚠️" elif configured is True and route_lease == "待單次 run-owned lease": icon = "🧪" elif configured is True and route_lease == "active_run_owned_lease": icon = "🟢" elif configured is True: icon = "⚠️" else: icon = "⚪" provider_lines.append( f" {icon} {label} | configured={_truth_text(configured)} | " f"authenticated={_truth_text(auth.get('authenticated'))} | " "verification=" f"{auth.get('verification_status', 'authentication_readback_unavailable')} | " f"canary={mode} {percent}% | route_lease={route_lease}" ) provider_lines.extend( f" ⚪ {provider} (shadow metadata only)" for provider in sorted(SHADOW_METADATA_ONLY_PROVIDERS) ) # 費用統計 cost_lines = [] try: from src.services.ai_rate_limiter import get_ai_rate_limiter limiter = get_ai_rate_limiter() for provider in ("claude", "gemini"): usage = await limiter.get_usage_stats(provider) cost = float( (usage.get("total_cost_usd") or {}).get("current") or 0.0 ) accounting = usage.get("accounting") or {} receipt_backed = bool( usage.get("readback_status") == "verified" and accounting.get("complete") is True and accounting.get("status") == "receipt_backed" and accounting.get("degraded") is not True ) cost_lines.append( f" 💰 {provider}: ${cost:.6f} | " f"receipt={'verified' if receipt_backed else 'degraded'}" ) except Exception as exc: logger.warning( "ai_control_status_cost_readback_unavailable", error_type=type(exc).__name__, ) cost_lines.append(" (accounting receipt readback unavailable)") providers_str = "\n".join(provider_lines) costs_str = "\n".join(cost_lines) if cost_lines else " (尚無費用記錄)" return ( f"🤖 AI Router 控制面板\n\n" f"路由模式: {router_status}\n" f"主要 Provider: {primary_str}\n\n" f"Provider 狀態:\n{providers_str}\n\n" f"費用統計:\n{costs_str}\n\n" f"/ai primary <provider> 切換\n" f"/ai router on/off 開關 AIRouter\n" f"/ai enable/disable <ollama provider> 控制 Ollama\n" f"Claude/Gemini 僅由單次 run-owned canary lease 啟用" ) async def handle_ai_command(text: str) -> str: """ 處理 /ai 指令,回傳 Telegram HTML 回應。 Args: text: 完整訊息文字 (e.g., "/ai status", "/ai primary gemini") Returns: HTML 格式回應字串 """ parts = text.strip().split() if len(parts) < 2: return await get_status_summary() sub = parts[1].lower() if sub == "status": return await get_status_summary() elif sub == "router": if len(parts) < 3: return "用法: /ai router on/ai router off" action = parts[2].lower() if action in ("on", "true", "enable"): ok = await set_ai_router_enabled(True) return "✅ AIRouter 已啟用 (Redis 覆蓋)" if ok else "❌ 寫入 Redis 失敗" elif action in ("off", "false", "disable"): ok = await set_ai_router_enabled(False) return "⚠️ AIRouter 已停用,回滾舊 fallback chain (Redis 覆蓋)" if ok else "❌ 寫入 Redis 失敗" else: return f"❌ 未知動作: {action},請用 on/off" elif sub == "primary": if len(parts) < 3: current = await get_primary_provider() if current: return f"當前主要 Provider: {current}\n用法: /ai primary <provider>/ai primary auto" return "當前: 🤖 智慧路由\n用法: /ai primary <provider>" provider = parts[2].lower() if provider == "auto": ok = await clear_primary_provider() return "✅ 已清除主要 Provider,恢復智慧路由" if ok else "❌ 操作失敗" if provider not in VALID_PRIMARY_PROVIDERS: return ( f"❌ Provider 不可改寫 production 起點: {provider}\n" "production 固定從 ollama_gcp_a 開始" ) ok = await set_primary_provider(provider) return f"✅ 主要 Provider 已設為 {provider}" if ok else "❌ 寫入 Redis 失敗" elif sub == "enable": if len(parts) < 3: return "用法: /ai enable <provider>" provider = parts[2].lower() if provider not in VALID_PROVIDERS: return f"❌ 未知 Provider: {provider}" if provider in PAID_PROVIDERS: return ( "❌ Claude/Gemini 付費路由只能由 run-owned canary 驗證流程啟用;" "一般 /ai enable 已 fail closed" ) ok = await set_provider_disabled(provider, False) return f"✅ {provider} 已啟用" if ok else "❌ 操作失敗" elif sub == "disable": if len(parts) < 3: return "用法: /ai disable <provider>" provider = parts[2].lower() if provider not in VALID_PROVIDERS: return f"❌ 未知 Provider: {provider}" ok = await set_provider_disabled(provider, True) if ok and provider in PAID_PROVIDERS: return "⚠️ Claude/Gemini 已成對停用,activation lease/receipt 已清除" return f"⚠️ {provider} 已停用" if ok else "❌ 操作失敗" elif sub == "cost": lines = ["💰 AI 費用 receipt\n"] try: from src.services.ai_rate_limiter import get_ai_rate_limiter limiter = get_ai_rate_limiter() total = 0.0 all_receipt_backed = True for provider in ("claude", "gemini"): usage = await limiter.get_usage_stats(provider) cost = float( (usage.get("total_cost_usd") or {}).get("current") or 0.0 ) accounting = usage.get("accounting") or {} receipt_backed = bool( usage.get("readback_status") == "verified" and accounting.get("complete") is True and accounting.get("status") == "receipt_backed" and accounting.get("degraded") is not True ) total += cost all_receipt_backed = all_receipt_backed and receipt_backed lines.append( f" {provider}: ${cost:.6f} | " f"receipt={'verified' if receipt_backed else 'degraded'}" ) lines.extend( [ f"\n 總計: ${total:.6f}", ( " ✅ receipt-backed accounting" if all_receipt_backed else " ⚠️ accounting receipt degraded;禁止假性估算" ), ] ) except Exception as exc: logger.warning( "ai_control_cost_readback_unavailable", error_type=type(exc).__name__, ) lines.append( " ⚠️ accounting readback unavailable;未以舊 Redis key 或估算值替代。" ) return "\n".join(lines) elif sub == "help": return ( "/ai 指令說明\n\n" "/ai status — 顯示所有狀態\n" "/ai router on/off — 開關 AIRouter\n" "/ai primary <p> — 設主要 Provider\n" "/ai primary auto — 恢復智慧路由\n" "/ai enable <p> — 啟用 Provider\n" "/ai disable <p> — 停用 Provider\n" "/ai cost — 費用統計\n\n" f"可控 Provider: {', '.join(sorted(VALID_PROVIDERS))}\n" "Legacy providers 僅保留 shadow metadata;Claude/Gemini 受 persistent paid gate 控制" ) else: return f"❌ 未知子指令: {sub}\n輸入 /ai help 查看說明"