a
This commit is contained in:
parent
1fdcb9c8b7
commit
6d48dc98d2
|
|
@ -67,6 +67,19 @@ def require_account_access(account_id: int, user: Dict[str, Any]) -> int:
|
||||||
raise HTTPException(status_code=403, detail="无权访问该账号")
|
raise HTTPException(status_code=403, detail="无权访问该账号")
|
||||||
|
|
||||||
|
|
||||||
|
def require_account_owner(account_id: int, user: Dict[str, Any]) -> int:
|
||||||
|
"""
|
||||||
|
账号“拥有者”权限:用于启停交易进程等高危操作。
|
||||||
|
"""
|
||||||
|
aid = int(account_id or 1)
|
||||||
|
if (user.get("role") or "user") == "admin":
|
||||||
|
return aid
|
||||||
|
role = UserAccountMembership.get_role(int(user["id"]), aid)
|
||||||
|
if role == "owner":
|
||||||
|
return aid
|
||||||
|
raise HTTPException(status_code=403, detail="需要该账号 owner 权限")
|
||||||
|
|
||||||
|
|
||||||
def get_admin_user(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
def get_admin_user(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||||
return require_admin(user)
|
return require_admin(user)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,14 @@ from typing import Optional, List, Dict, Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from database.models import Account, UserAccountMembership
|
from database.models import Account, UserAccountMembership
|
||||||
from api.auth_deps import get_current_user, get_admin_user
|
from api.auth_deps import get_current_user, get_admin_user, require_account_access, require_account_owner
|
||||||
|
|
||||||
|
from api.supervisor_account import (
|
||||||
|
ensure_account_program,
|
||||||
|
run_supervisorctl,
|
||||||
|
parse_supervisor_status,
|
||||||
|
program_name_for_account,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -104,7 +111,24 @@ async def create_account(payload: AccountCreate, _admin: Dict[str, Any] = Depend
|
||||||
use_testnet=bool(payload.use_testnet),
|
use_testnet=bool(payload.use_testnet),
|
||||||
status=payload.status,
|
status=payload.status,
|
||||||
)
|
)
|
||||||
return {"success": True, "id": int(aid), "message": "账号已创建"}
|
# 自动为该账号生成 supervisor program 配置(失败不影响账号创建)
|
||||||
|
sup = ensure_account_program(int(aid))
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"id": int(aid),
|
||||||
|
"message": "账号已创建",
|
||||||
|
"supervisor": {
|
||||||
|
"ok": bool(sup.ok),
|
||||||
|
"program": sup.program,
|
||||||
|
"program_dir": sup.program_dir,
|
||||||
|
"ini_path": sup.ini_path,
|
||||||
|
"supervisor_conf": sup.supervisor_conf,
|
||||||
|
"reread": sup.reread,
|
||||||
|
"update": sup.update,
|
||||||
|
"error": sup.error,
|
||||||
|
"note": "如需自动启停:请确保 backend 进程有写入 program_dir 权限,并允许执行 supervisorctl(可选 sudo -n)。",
|
||||||
|
},
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"创建账号失败: {e}")
|
raise HTTPException(status_code=500, detail=f"创建账号失败: {e}")
|
||||||
|
|
||||||
|
|
@ -160,3 +184,74 @@ async def update_credentials(account_id: int, payload: AccountCredentialsUpdate,
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"更新账号密钥失败: {e}")
|
raise HTTPException(status_code=500, detail=f"更新账号密钥失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{account_id}/trading/ensure-program")
|
||||||
|
async def ensure_trading_program(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
# 允许管理员或该账号 owner 执行(owner 用于“我重建配置再启动”)
|
||||||
|
if (user.get("role") or "user") != "admin":
|
||||||
|
require_account_owner(int(account_id), user)
|
||||||
|
sup = ensure_account_program(int(account_id))
|
||||||
|
if not sup.ok:
|
||||||
|
raise HTTPException(status_code=500, detail=sup.error or "生成 supervisor 配置失败")
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"program": sup.program,
|
||||||
|
"ini_path": sup.ini_path,
|
||||||
|
"program_dir": sup.program_dir,
|
||||||
|
"supervisor_conf": sup.supervisor_conf,
|
||||||
|
"reread": sup.reread,
|
||||||
|
"update": sup.update,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{account_id}/trading/status")
|
||||||
|
async def trading_status_for_account(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
# 有访问权即可查看状态
|
||||||
|
require_account_access(int(account_id), user)
|
||||||
|
program = program_name_for_account(int(account_id))
|
||||||
|
try:
|
||||||
|
raw = run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = parse_supervisor_status(raw)
|
||||||
|
return {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"读取交易进程状态失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{account_id}/trading/start")
|
||||||
|
async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
require_account_owner(int(account_id), user)
|
||||||
|
program = program_name_for_account(int(account_id))
|
||||||
|
try:
|
||||||
|
out = run_supervisorctl(["start", program])
|
||||||
|
raw = run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = parse_supervisor_status(raw)
|
||||||
|
return {"message": "已启动", "output": out, "status": {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"启动交易进程失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{account_id}/trading/stop")
|
||||||
|
async def trading_stop_for_account(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
require_account_owner(int(account_id), user)
|
||||||
|
program = program_name_for_account(int(account_id))
|
||||||
|
try:
|
||||||
|
out = run_supervisorctl(["stop", program])
|
||||||
|
raw = run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = parse_supervisor_status(raw)
|
||||||
|
return {"message": "已停止", "output": out, "status": {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"停止交易进程失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{account_id}/trading/restart")
|
||||||
|
async def trading_restart_for_account(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
require_account_owner(int(account_id), user)
|
||||||
|
program = program_name_for_account(int(account_id))
|
||||||
|
try:
|
||||||
|
out = run_supervisorctl(["restart", program])
|
||||||
|
raw = run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = parse_supervisor_status(raw)
|
||||||
|
return {"message": "已重启", "output": out, "status": {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"重启交易进程失败: {e}")
|
||||||
|
|
||||||
|
|
|
||||||
270
backend/api/supervisor_account.py
Normal file
270
backend/api/supervisor_account.py
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
"""
|
||||||
|
Supervisor 多账号托管(宝塔插件兼容)
|
||||||
|
|
||||||
|
目标:
|
||||||
|
- 根据 account_id 自动生成一个 supervisor program 配置文件(.ini)
|
||||||
|
- 自动定位 supervisord.conf 的 include 目录(尽量不要求你手填路径)
|
||||||
|
- 提供 supervisorctl 的常用调用封装(reread/update/status/start/stop/restart)
|
||||||
|
|
||||||
|
重要说明:
|
||||||
|
- 本模块只写入“程序配置文件”,不包含任何 API Key/Secret
|
||||||
|
- trading_system 进程通过 ATS_ACCOUNT_ID 选择自己的账号配置
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CANDIDATE_CONFS = [
|
||||||
|
"/www/server/panel/plugin/supervisor/supervisord.conf",
|
||||||
|
"/www/server/panel/plugin/supervisor/supervisor.conf",
|
||||||
|
"/etc/supervisor/supervisord.conf",
|
||||||
|
"/etc/supervisord.conf",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_project_root() -> Path:
|
||||||
|
# backend/api/supervisor_account.py -> api -> backend -> project_root
|
||||||
|
return Path(__file__).resolve().parents[2].parent
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_supervisor_conf_path() -> Optional[Path]:
|
||||||
|
p = (os.getenv("SUPERVISOR_CONF") or "").strip()
|
||||||
|
if p:
|
||||||
|
pp = Path(p)
|
||||||
|
return pp if pp.exists() else pp # 允许不存在时也返回,便于报错信息
|
||||||
|
for cand in DEFAULT_CANDIDATE_CONFS:
|
||||||
|
try:
|
||||||
|
cp = Path(cand)
|
||||||
|
if cp.exists():
|
||||||
|
return cp
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_include_dir_from_conf(conf_path: Path) -> Optional[Path]:
|
||||||
|
"""
|
||||||
|
尝试解析 supervisord.conf 的 [include] files=... 目录。
|
||||||
|
常见格式:
|
||||||
|
[include]
|
||||||
|
files = /path/to/conf.d/*.ini
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
text = conf_path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
in_include = False
|
||||||
|
for raw in text.splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith(";") or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if re.match(r"^\[include\]\s*$", line, flags=re.I):
|
||||||
|
in_include = True
|
||||||
|
continue
|
||||||
|
if in_include and line.startswith("[") and line.endswith("]"):
|
||||||
|
break
|
||||||
|
if not in_include:
|
||||||
|
continue
|
||||||
|
m = re.match(r"^files\s*=\s*(.+)$", line, flags=re.I)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
val = (m.group(1) or "").strip().strip('"').strip("'")
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
# 只取第一个 pattern(即使写了多个用空格分隔)
|
||||||
|
first = val.split()[0]
|
||||||
|
p = Path(first)
|
||||||
|
if not p.is_absolute():
|
||||||
|
p = (conf_path.parent / p).resolve()
|
||||||
|
return p.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_supervisor_program_dir() -> Path:
|
||||||
|
"""
|
||||||
|
获取 supervisor program 配置目录(优先级):
|
||||||
|
1) SUPERVISOR_PROGRAM_DIR
|
||||||
|
2) 从 supervisord.conf 的 [include] files= 解析
|
||||||
|
3) 兜底:/www/server/panel/plugin/supervisor(你当前看到的目录)
|
||||||
|
"""
|
||||||
|
env_dir = (os.getenv("SUPERVISOR_PROGRAM_DIR") or "").strip()
|
||||||
|
if env_dir:
|
||||||
|
return Path(env_dir)
|
||||||
|
|
||||||
|
conf = _detect_supervisor_conf_path()
|
||||||
|
if conf and conf.exists():
|
||||||
|
inc = _parse_include_dir_from_conf(conf)
|
||||||
|
if inc:
|
||||||
|
return inc
|
||||||
|
|
||||||
|
return Path("/www/server/panel/plugin/supervisor")
|
||||||
|
|
||||||
|
|
||||||
|
def program_name_for_account(account_id: int) -> str:
|
||||||
|
tmpl = (os.getenv("SUPERVISOR_TRADING_PROGRAM_TEMPLATE") or "auto_sys_acc{account_id}").strip()
|
||||||
|
try:
|
||||||
|
return tmpl.format(account_id=int(account_id))
|
||||||
|
except Exception:
|
||||||
|
return f"auto_sys_acc{int(account_id)}"
|
||||||
|
|
||||||
|
|
||||||
|
def ini_filename_for_program(program_name: str) -> str:
|
||||||
|
safe = re.sub(r"[^a-zA-Z0-9_\-:.]+", "_", program_name).strip("_") or "auto_sys"
|
||||||
|
return f"{safe}.ini"
|
||||||
|
|
||||||
|
|
||||||
|
def render_program_ini(account_id: int, program_name: str) -> str:
|
||||||
|
project_root = _get_project_root()
|
||||||
|
python_bin = sys.executable # 使用 backend 当前虚拟环境,通常已包含 trading_system 依赖
|
||||||
|
|
||||||
|
# 日志目录可通过环境变量覆盖
|
||||||
|
log_dir = Path(os.getenv("TRADING_LOG_DIR", str(project_root / "logs"))).expanduser()
|
||||||
|
out_log = log_dir / f"trading_{int(account_id)}.out.log"
|
||||||
|
err_log = log_dir / f"trading_{int(account_id)}.err.log"
|
||||||
|
|
||||||
|
# 默认不自动启动,避免“创建账号=立刻下单”
|
||||||
|
autostart = (os.getenv("TRADING_AUTOSTART_DEFAULT", "false") or "false").lower() == "true"
|
||||||
|
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
f"[program:{program_name}]",
|
||||||
|
f"directory={project_root}",
|
||||||
|
f"command={python_bin} -m trading_system.main",
|
||||||
|
"autostart=" + ("true" if autostart else "false"),
|
||||||
|
"autorestart=true",
|
||||||
|
"startsecs=3",
|
||||||
|
"stopasgroup=true",
|
||||||
|
"killasgroup=true",
|
||||||
|
"",
|
||||||
|
f'environment=ATS_ACCOUNT_ID="{int(account_id)}",PYTHONUNBUFFERED="1"',
|
||||||
|
"",
|
||||||
|
f"stdout_logfile={out_log}",
|
||||||
|
f"stderr_logfile={err_log}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_program_ini(program_dir: Path, filename: str, content: str) -> Path:
|
||||||
|
program_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target = program_dir / filename
|
||||||
|
tmp = program_dir / (filename + ".tmp")
|
||||||
|
tmp.write_text(content, encoding="utf-8")
|
||||||
|
os.replace(str(tmp), str(target))
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def _build_supervisorctl_cmd(args: list[str]) -> list[str]:
|
||||||
|
supervisorctl_path = os.getenv("SUPERVISORCTL_PATH", "supervisorctl")
|
||||||
|
supervisor_conf = (os.getenv("SUPERVISOR_CONF") or "").strip()
|
||||||
|
use_sudo = (os.getenv("SUPERVISOR_USE_SUDO", "false") or "false").lower() == "true"
|
||||||
|
|
||||||
|
if not supervisor_conf:
|
||||||
|
conf = _detect_supervisor_conf_path()
|
||||||
|
supervisor_conf = str(conf) if conf else ""
|
||||||
|
|
||||||
|
cmd: list[str] = []
|
||||||
|
if use_sudo:
|
||||||
|
cmd += ["sudo", "-n"]
|
||||||
|
cmd += [supervisorctl_path]
|
||||||
|
if supervisor_conf:
|
||||||
|
cmd += ["-c", supervisor_conf]
|
||||||
|
cmd += args
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def run_supervisorctl(args: list[str], timeout_sec: int = 10) -> str:
|
||||||
|
cmd = _build_supervisorctl_cmd(args)
|
||||||
|
try:
|
||||||
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=int(timeout_sec))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise RuntimeError("supervisorctl 超时")
|
||||||
|
|
||||||
|
out = (res.stdout or "").strip()
|
||||||
|
err = (res.stderr or "").strip()
|
||||||
|
combined = "\n".join([s for s in [out, err] if s]).strip()
|
||||||
|
if res.returncode != 0:
|
||||||
|
raise RuntimeError(combined or f"supervisorctl failed (exit={res.returncode})")
|
||||||
|
return combined or out
|
||||||
|
|
||||||
|
|
||||||
|
def parse_supervisor_status(raw: str) -> Tuple[bool, Optional[int], str]:
|
||||||
|
if "RUNNING" in raw:
|
||||||
|
m = re.search(r"\bpid\s+(\d+)\b", raw)
|
||||||
|
pid = int(m.group(1)) if m else None
|
||||||
|
return True, pid, "RUNNING"
|
||||||
|
for state in ["STOPPED", "FATAL", "EXITED", "BACKOFF", "STARTING", "UNKNOWN"]:
|
||||||
|
if state in raw:
|
||||||
|
return False, None, state
|
||||||
|
return False, None, "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EnsureProgramResult:
|
||||||
|
ok: bool
|
||||||
|
program: str
|
||||||
|
ini_path: str
|
||||||
|
program_dir: str
|
||||||
|
supervisor_conf: str
|
||||||
|
reread: str = ""
|
||||||
|
update: str = ""
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_account_program(account_id: int) -> EnsureProgramResult:
|
||||||
|
aid = int(account_id)
|
||||||
|
program = program_name_for_account(aid)
|
||||||
|
program_dir = get_supervisor_program_dir()
|
||||||
|
ini_name = ini_filename_for_program(program)
|
||||||
|
ini_text = render_program_ini(aid, program)
|
||||||
|
conf = _detect_supervisor_conf_path()
|
||||||
|
conf_s = str(conf) if conf else (os.getenv("SUPERVISOR_CONF") or "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = write_program_ini(program_dir, ini_name, ini_text)
|
||||||
|
reread_out = ""
|
||||||
|
update_out = ""
|
||||||
|
try:
|
||||||
|
reread_out = run_supervisorctl(["reread"])
|
||||||
|
update_out = run_supervisorctl(["update"])
|
||||||
|
except Exception as e:
|
||||||
|
# 写文件成功但 supervisorctl 失败也要给出可诊断信息
|
||||||
|
return EnsureProgramResult(
|
||||||
|
ok=False,
|
||||||
|
program=program,
|
||||||
|
ini_path=str(path),
|
||||||
|
program_dir=str(program_dir),
|
||||||
|
supervisor_conf=conf_s,
|
||||||
|
reread=reread_out,
|
||||||
|
update=update_out,
|
||||||
|
error=f"写入配置成功,但执行 supervisorctl reread/update 失败: {e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return EnsureProgramResult(
|
||||||
|
ok=True,
|
||||||
|
program=program,
|
||||||
|
ini_path=str(path),
|
||||||
|
program_dir=str(program_dir),
|
||||||
|
supervisor_conf=conf_s,
|
||||||
|
reread=reread_out,
|
||||||
|
update=update_out,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return EnsureProgramResult(
|
||||||
|
ok=False,
|
||||||
|
program=program,
|
||||||
|
ini_path="",
|
||||||
|
program_dir=str(program_dir),
|
||||||
|
supervisor_conf=conf_s,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -189,6 +189,14 @@ class UserAccountMembership:
|
||||||
)
|
)
|
||||||
return bool(row)
|
return bool(row)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_role(user_id: int, account_id: int) -> str:
|
||||||
|
row = db.execute_one(
|
||||||
|
"SELECT role FROM user_account_memberships WHERE user_id = %s AND account_id = %s",
|
||||||
|
(int(user_id), int(account_id)),
|
||||||
|
)
|
||||||
|
return (row.get("role") if isinstance(row, dict) else None) or ""
|
||||||
|
|
||||||
|
|
||||||
class TradingConfig:
|
class TradingConfig:
|
||||||
"""交易配置模型"""
|
"""交易配置模型"""
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
const [systemStatus, setSystemStatus] = useState(null)
|
const [systemStatus, setSystemStatus] = useState(null)
|
||||||
const [backendStatus, setBackendStatus] = useState(null)
|
const [backendStatus, setBackendStatus] = useState(null)
|
||||||
const [systemBusy, setSystemBusy] = useState(false)
|
const [systemBusy, setSystemBusy] = useState(false)
|
||||||
|
const [accountTradingStatus, setAccountTradingStatus] = useState(null)
|
||||||
|
|
||||||
// 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航)
|
// 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航)
|
||||||
const [accountId, setAccountId] = useState(getCurrentAccountId())
|
const [accountId, setAccountId] = useState(getCurrentAccountId())
|
||||||
|
|
@ -207,6 +208,71 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadAccountTradingStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.getAccountTradingStatus(accountId)
|
||||||
|
setAccountTradingStatus(res)
|
||||||
|
} catch (error) {
|
||||||
|
// 非 owner/未配置 supervisor 时可能会失败,静默即可
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAccountTradingEnsure = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.ensureAccountTradingProgram(accountId)
|
||||||
|
setMessage(`已生成/刷新 supervisor 配置:${res.program || ''}`)
|
||||||
|
await loadAccountTradingStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('生成 supervisor 配置失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAccountTradingStart = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.startAccountTrading(accountId)
|
||||||
|
setMessage(res.message || '交易进程已启动')
|
||||||
|
await loadAccountTradingStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('启动交易进程失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAccountTradingStop = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.stopAccountTrading(accountId)
|
||||||
|
setMessage(res.message || '交易进程已停止')
|
||||||
|
await loadAccountTradingStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('停止交易进程失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAccountTradingRestart = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.restartAccountTrading(accountId)
|
||||||
|
setMessage(res.message || '交易进程已重启')
|
||||||
|
await loadAccountTradingStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('重启交易进程失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClearCache = async () => {
|
const handleClearCache = async () => {
|
||||||
setSystemBusy(true)
|
setSystemBusy(true)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
|
|
@ -287,10 +353,12 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
checkFeasibility()
|
checkFeasibility()
|
||||||
loadSystemStatus()
|
loadSystemStatus()
|
||||||
loadBackendStatus()
|
loadBackendStatus()
|
||||||
|
loadAccountTradingStatus()
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
loadSystemStatus()
|
loadSystemStatus()
|
||||||
loadBackendStatus()
|
loadBackendStatus()
|
||||||
|
loadAccountTradingStatus()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
|
|
@ -322,6 +390,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
checkFeasibility()
|
checkFeasibility()
|
||||||
loadSystemStatus()
|
loadSystemStatus()
|
||||||
loadBackendStatus()
|
loadBackendStatus()
|
||||||
|
loadAccountTradingStatus()
|
||||||
}, [accountId])
|
}, [accountId])
|
||||||
|
|
||||||
// 顶部导航切换账号时(localStorage更新),这里做一个轻量同步
|
// 顶部导航切换账号时(localStorage更新),这里做一个轻量同步
|
||||||
|
|
@ -692,6 +761,38 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
<p>修改配置后,交易系统将在下次扫描时自动使用新配置</p>
|
<p>修改配置后,交易系统将在下次扫描时自动使用新配置</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 我的交易进程(按账号;owner/admin 可启停) */}
|
||||||
|
<div className="system-section">
|
||||||
|
<div className="system-header">
|
||||||
|
<h3>我的交易进程(当前账号)</h3>
|
||||||
|
<div className="system-status">
|
||||||
|
<span className={`system-status-badge ${accountTradingStatus?.running ? 'running' : 'stopped'}`}>
|
||||||
|
{accountTradingStatus?.running ? '运行中' : '未运行/未知'}
|
||||||
|
</span>
|
||||||
|
{accountTradingStatus?.pid ? <span className="system-status-meta">PID: {accountTradingStatus.pid}</span> : null}
|
||||||
|
{accountTradingStatus?.program ? <span className="system-status-meta">程序: {accountTradingStatus.program}</span> : null}
|
||||||
|
{accountTradingStatus?.state ? <span className="system-status-meta">状态: {accountTradingStatus.state}</span> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="system-actions">
|
||||||
|
<button type="button" className="system-btn" onClick={handleAccountTradingEnsure} disabled={systemBusy} title="为该账号生成/刷新 supervisor program 配置(需要 owner/admin)">
|
||||||
|
生成配置
|
||||||
|
</button>
|
||||||
|
<button type="button" className="system-btn" onClick={handleAccountTradingStop} disabled={systemBusy} title="停止该账号交易进程(需要 owner/admin)">
|
||||||
|
停止
|
||||||
|
</button>
|
||||||
|
<button type="button" className="system-btn" onClick={handleAccountTradingStart} disabled={systemBusy} title="启动该账号交易进程(需要 owner/admin)">
|
||||||
|
启动
|
||||||
|
</button>
|
||||||
|
<button type="button" className="system-btn primary" onClick={handleAccountTradingRestart} disabled={systemBusy} title="重启该账号交易进程(需要 owner/admin)">
|
||||||
|
重启
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="system-hint">
|
||||||
|
提示:若按钮报“无权限”,请让管理员在用户授权里把该账号分配为 owner;若报 supervisor 相关错误,请检查后端对 `/www/server/panel/plugin/supervisor` 的写权限与 supervisorctl 可执行权限。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 系统控制:清缓存 / 启停 / 重启(supervisor) */}
|
{/* 系统控制:清缓存 / 启停 / 重启(supervisor) */}
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<div className="system-section">
|
<div className="system-section">
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,60 @@ export const api = {
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 交易进程(按账号;需要 owner 或 admin)
|
||||||
|
getAccountTradingStatus: async (accountId) => {
|
||||||
|
const response = await fetch(buildUrl(`/api/accounts/${accountId}/trading/status`), { headers: withAccountHeaders() })
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '获取交易进程状态失败' }))
|
||||||
|
throw new Error(error.detail || '获取交易进程状态失败')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
startAccountTrading: async (accountId) => {
|
||||||
|
const response = await fetch(buildUrl(`/api/accounts/${accountId}/trading/start`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '启动交易进程失败' }))
|
||||||
|
throw new Error(error.detail || '启动交易进程失败')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
stopAccountTrading: async (accountId) => {
|
||||||
|
const response = await fetch(buildUrl(`/api/accounts/${accountId}/trading/stop`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '停止交易进程失败' }))
|
||||||
|
throw new Error(error.detail || '停止交易进程失败')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
restartAccountTrading: async (accountId) => {
|
||||||
|
const response = await fetch(buildUrl(`/api/accounts/${accountId}/trading/restart`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '重启交易进程失败' }))
|
||||||
|
throw new Error(error.detail || '重启交易进程失败')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
ensureAccountTradingProgram: async (accountId) => {
|
||||||
|
const response = await fetch(buildUrl(`/api/accounts/${accountId}/trading/ensure-program`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '生成/刷新 supervisor 配置失败' }))
|
||||||
|
throw new Error(error.detail || '生成/刷新 supervisor 配置失败')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
|
||||||
// 配置管理
|
// 配置管理
|
||||||
getConfigs: async () => {
|
getConfigs: async () => {
|
||||||
const response = await fetch(buildUrl('/api/config'), { headers: withAccountHeaders() });
|
const response = await fetch(buildUrl('/api/config'), { headers: withAccountHeaders() });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user