Files
crypto_okx/lib/env/shared_env_lib.py
dekun a1abe159fa Initial standalone crypto_okx with one-click deploy.
Add deploy/manage.sh bootstrap for git.bz121.com/dekun/crypto_okx and point docs at this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 20:00:59 +08:00

191 lines
6.2 KiB
Python

"""Local AI env helpers (standalone project)."""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
from lib.env.env_file_lib import apply_env_updates, env_get_all, load_env_file_into_environ, read_env_lines
from lib.env.env_schema import (
_field_type,
_hot_reload,
_is_sensitive,
_mask_value,
_restart_required,
parse_env_example_schema,
validate_env_updates,
)
from lib.paths import REPO_ROOT
AI_ENV_FIELDS: list[tuple[str, str, str]] = [
("AI_PROVIDER", "AI 提供方", "openai 或 ollama"),
("OPENAI_API_BASE", "API 地址", "OpenAI 兼容接口"),
("OPENAI_API_KEY", "API 密钥", "留空表示不修改"),
("OPENAI_MODEL", "云端模型", ""),
("OLLAMA_API", "Ollama 地址", "本地服务 URL"),
("AI_MODEL", "Ollama 模型", ""),
("AI_TIMEOUT_SECONDS", "请求超时(秒)", "默认 120"),
]
AI_ENV_KEYS = frozenset(k for k, _l, _n in AI_ENV_FIELDS)
def local_env_path() -> str:
return str(REPO_ROOT / ".env")
def local_example_path() -> str:
return str(REPO_ROOT / ".env.example")
def instance_example_path(exchange_key: str = "okx") -> str:
return local_example_path()
def _schema_field_map(example_path: str) -> dict[str, dict[str, Any]]:
out: dict[str, dict[str, Any]] = {}
for group in parse_env_example_schema(example_path):
for field in group.get("fields") or []:
out[field["key"]] = dict(field)
return out
def _build_field(
key: str,
label: str,
note: str,
schema: dict[str, dict[str, Any]],
values: dict[str, str],
) -> dict[str, Any]:
meta = schema.get(key) or {}
schema_default = meta.get("default") or ""
val = values.get(key, "")
if val == "" and schema_default:
val = schema_default
masked = _mask_value(key, val)
ftype = meta.get("type") or _field_type(key, val or schema_default)
return {
"key": key,
"label": label,
"note": note or meta.get("note") or "",
"default": val,
"type": ftype,
"sensitive": meta.get("sensitive", _is_sensitive(key)),
"restart_required": meta.get("restart_required", _restart_required(key)),
"hot_reload": meta.get("hot_reload", _hot_reload(key)),
"current": masked["value"] if not _is_sensitive(key) else "",
"masked": masked["masked"],
"tail": masked.get("tail") or "",
"has_value": masked["has_value"],
}
def build_ai_env_payload(env_path: str | None = None, example_path: str | None = None) -> dict[str, Any]:
env_path = env_path or local_env_path()
example_path = example_path or local_example_path()
schema = _schema_field_map(example_path)
values = env_get_all(read_env_lines(env_path))
fields = [
_build_field(key, label, note, schema, values)
for key, label, note in AI_ENV_FIELDS
]
sync_status = ai_sync_status()
return {
"title": "AI 复盘",
"fields": fields,
"sync_status": sync_status,
}
def ai_sync_status() -> dict[str, Any]:
"""Standalone project: no multi-instance hub sync."""
path = local_env_path()
if not os.path.isfile(path):
return {"all_synced": False, "instances": {"local": {"ok": False, "msg": "缺少 .env"}}}
return {"all_synced": True, "instances": {"local": {"ok": True, "mismatched_keys": []}}}
def _ai_validate_groups(example_path: str) -> list[dict[str, Any]]:
schema = _schema_field_map(example_path)
fields: list[dict[str, Any]] = []
for key, _label, _note in AI_ENV_FIELDS:
if key in schema:
fields.append(schema[key])
else:
fields.append(
{
"key": key,
"type": _field_type(key, ""),
"sensitive": _is_sensitive(key),
"restart_required": _restart_required(key),
"hot_reload": _hot_reload(key),
}
)
return [{"title": "AI 复盘", "fields": fields}]
def validate_ai_env_updates(updates: dict[str, str], example_path: str | None = None) -> tuple[dict[str, str], list[str]]:
example_path = example_path or local_example_path()
groups = _ai_validate_groups(example_path)
filtered = {k: v for k, v in (updates or {}).items() if k in AI_ENV_KEYS}
unknown = [k for k in (updates or {}) if k not in AI_ENV_KEYS]
errors = [f"未知配置项: {k}" for k in unknown]
clean, val_errors = validate_env_updates(groups, filtered)
errors.extend(val_errors)
return clean, errors
def apply_ai_env_to_all(updates: dict[str, str]) -> dict[str, Any]:
"""Write AI keys to local .env only."""
clean, errors = validate_ai_env_updates(updates)
if errors:
return {"ok": False, "errors": errors, "changed": {}}
if not clean:
return {"ok": True, "changed": {}, "restart_required": False}
path = local_env_path()
changed_keys = apply_env_updates(path, clean)
if changed_keys:
load_env_file_into_environ(path)
restart_required = any(
(not _hot_reload(k)) or _restart_required(k) for k in changed_keys
)
return {
"ok": True,
"changed": {"local": list(changed_keys)},
"restart_required": restart_required,
}
def restart_local_pm2() -> dict[str, Any]:
"""Restart this app via PM2 if configured."""
return _restart_pm2_app("crypto_okx")
def _restart_pm2_app(app_name: str) -> dict[str, Any]:
try:
proc = subprocess.run(
["pm2", "restart", app_name, "--update-env"],
capture_output=True,
text=True,
timeout=120,
)
return {
"ok": proc.returncode == 0,
"msg": (proc.stdout or proc.stderr or "").strip()[:500],
"returncode": proc.returncode,
}
except FileNotFoundError:
return {"ok": False, "msg": "未找到 pm2 命令"}
except subprocess.TimeoutExpired:
return {"ok": False, "msg": "pm2 restart 超时"}
except Exception as e:
return {"ok": False, "msg": str(e)}