a
This commit is contained in:
parent
3b0ff0227e
commit
fe60f12ee0
|
|
@ -81,7 +81,8 @@ async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> Lis
|
||||||
return out
|
return out
|
||||||
|
|
||||||
memberships = UserAccountMembership.list_for_user(int(user["id"]))
|
memberships = UserAccountMembership.list_for_user(int(user["id"]))
|
||||||
account_ids = [int(m.get("account_id")) for m in (memberships or []) if m.get("account_id") is not None]
|
membership_map = {int(m.get("account_id")): (m.get("role") or "viewer") for m in (memberships or []) if m.get("account_id") is not None}
|
||||||
|
account_ids = list(membership_map.keys())
|
||||||
for aid in account_ids:
|
for aid in account_ids:
|
||||||
r = Account.get(int(aid))
|
r = Account.get(int(aid))
|
||||||
if not r:
|
if not r:
|
||||||
|
|
@ -94,6 +95,7 @@ async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> Lis
|
||||||
"name": r.get("name") or "",
|
"name": r.get("name") or "",
|
||||||
"status": r.get("status") or "active",
|
"status": r.get("status") or "active",
|
||||||
"use_testnet": bool(use_testnet),
|
"use_testnet": bool(use_testnet),
|
||||||
|
"role": membership_map.get(int(aid), "viewer"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|
@ -167,8 +169,10 @@ async def update_account(account_id: int, payload: AccountUpdate, _admin: Dict[s
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{account_id}/credentials")
|
@router.put("/{account_id}/credentials")
|
||||||
async def update_credentials(account_id: int, payload: AccountCredentialsUpdate, _admin: Dict[str, Any] = Depends(get_admin_user)):
|
async def update_credentials(account_id: int, payload: AccountCredentialsUpdate, user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
try:
|
try:
|
||||||
|
if (user.get("role") or "user") != "admin":
|
||||||
|
require_account_owner(int(account_id), user)
|
||||||
row = Account.get(int(account_id))
|
row = Account.get(int(account_id))
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=404, detail="账号不存在")
|
raise HTTPException(status_code=404, detail="账号不存在")
|
||||||
|
|
@ -213,11 +217,44 @@ async def trading_status_for_account(account_id: int, user: Dict[str, Any] = Dep
|
||||||
try:
|
try:
|
||||||
raw = run_supervisorctl(["status", program])
|
raw = run_supervisorctl(["status", program])
|
||||||
running, pid, state = parse_supervisor_status(raw)
|
running, pid, state = parse_supervisor_status(raw)
|
||||||
return {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}
|
# 仅 owner/admin 可看 tail(便于自助排障)
|
||||||
|
stderr_tail = ""
|
||||||
|
try:
|
||||||
|
is_admin = (user.get("role") or "user") == "admin"
|
||||||
|
role = UserAccountMembership.get_role(int(user["id"]), int(account_id)) if not is_admin else "admin"
|
||||||
|
if is_admin or role == "owner":
|
||||||
|
if state in {"FATAL", "EXITED", "BACKOFF"}:
|
||||||
|
stderr_tail = tail_supervisor(program, "stderr", 120)
|
||||||
|
except Exception:
|
||||||
|
stderr_tail = ""
|
||||||
|
|
||||||
|
resp = {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}
|
||||||
|
if stderr_tail:
|
||||||
|
resp["stderr_tail"] = stderr_tail
|
||||||
|
return resp
|
||||||
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.get("/{account_id}/trading/tail")
|
||||||
|
async def trading_tail_for_account(
|
||||||
|
account_id: int,
|
||||||
|
stream: str = "stderr",
|
||||||
|
lines: int = 200,
|
||||||
|
user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
读取该账号交易进程日志尾部(用于排障)。仅 owner/admin 可读。
|
||||||
|
"""
|
||||||
|
require_account_owner(int(account_id), user)
|
||||||
|
program = program_name_for_account(int(account_id))
|
||||||
|
try:
|
||||||
|
out = tail_supervisor(program, stream=stream, lines=lines)
|
||||||
|
return {"program": program, "stream": stream, "lines": lines, "tail": out}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"读取交易进程日志失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{account_id}/trading/start")
|
@router.post("/{account_id}/trading/start")
|
||||||
async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
|
async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
require_account_owner(int(account_id), user)
|
require_account_owner(int(account_id), user)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ sys.path.insert(0, str(project_root / 'backend'))
|
||||||
sys.path.insert(0, str(project_root / 'trading_system'))
|
sys.path.insert(0, str(project_root / 'trading_system'))
|
||||||
|
|
||||||
from database.models import TradingConfig, Account
|
from database.models import TradingConfig, Account
|
||||||
from api.auth_deps import get_current_user, get_account_id, require_admin
|
from api.auth_deps import get_current_user, get_account_id, require_admin, require_account_owner
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -140,13 +140,13 @@ async def get_all_configs(
|
||||||
"value": _mask(api_key or ""),
|
"value": _mask(api_key or ""),
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"category": "api",
|
"category": "api",
|
||||||
"description": "币安API密钥(账号私有,仅脱敏展示;仅管理员可修改)",
|
"description": "币安API密钥(账号私有,仅脱敏展示;账号 owner/admin 可修改)",
|
||||||
}
|
}
|
||||||
result["BINANCE_API_SECRET"] = {
|
result["BINANCE_API_SECRET"] = {
|
||||||
"value": "",
|
"value": "",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"category": "api",
|
"category": "api",
|
||||||
"description": "币安API密钥Secret(账号私有,不回传明文;仅管理员可修改)",
|
"description": "币安API密钥Secret(账号私有,不回传明文;账号 owner/admin 可修改)",
|
||||||
}
|
}
|
||||||
result["USE_TESTNET"] = {
|
result["USE_TESTNET"] = {
|
||||||
"value": bool(use_testnet),
|
"value": bool(use_testnet),
|
||||||
|
|
@ -502,9 +502,14 @@ async def update_config(
|
||||||
):
|
):
|
||||||
"""更新配置"""
|
"""更新配置"""
|
||||||
try:
|
try:
|
||||||
|
# 非管理员:必须是该账号 owner 才允许修改配置
|
||||||
|
if (user.get("role") or "user") != "admin":
|
||||||
|
require_account_owner(account_id, user)
|
||||||
|
|
||||||
# API Key/Secret/Testnet:写入 accounts 表(账号私有)
|
# API Key/Secret/Testnet:写入 accounts 表(账号私有)
|
||||||
if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||||||
require_admin(user)
|
if (user.get("role") or "user") != "admin":
|
||||||
|
require_account_owner(account_id, user)
|
||||||
try:
|
try:
|
||||||
if key == "BINANCE_API_KEY":
|
if key == "BINANCE_API_KEY":
|
||||||
Account.update_credentials(account_id, api_key=str(item.value or ""))
|
Account.update_credentials(account_id, api_key=str(item.value or ""))
|
||||||
|
|
@ -594,13 +599,17 @@ async def update_configs_batch(
|
||||||
):
|
):
|
||||||
"""批量更新配置"""
|
"""批量更新配置"""
|
||||||
try:
|
try:
|
||||||
|
# 非管理员:必须是该账号 owner 才允许修改配置
|
||||||
|
if (user.get("role") or "user") != "admin":
|
||||||
|
require_account_owner(account_id, user)
|
||||||
updated_count = 0
|
updated_count = 0
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for item in configs:
|
for item in configs:
|
||||||
try:
|
try:
|
||||||
if item.key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
if item.key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||||||
require_admin(user)
|
if (user.get("role") or "user") != "admin":
|
||||||
|
require_account_owner(account_id, user)
|
||||||
# 验证配置值
|
# 验证配置值
|
||||||
if item.type == 'number':
|
if item.type == 'number':
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,13 @@ class Account:
|
||||||
api_key = decrypt_str(row.get("api_key_enc") or "")
|
api_key = decrypt_str(row.get("api_key_enc") or "")
|
||||||
api_secret = decrypt_str(row.get("api_secret_enc") or "")
|
api_secret = decrypt_str(row.get("api_secret_enc") or "")
|
||||||
except Exception:
|
except Exception:
|
||||||
# 兼容:无 cryptography 或未配 master key 时,先按明文兜底
|
# 兼容:无 cryptography 或未配 master key 时:
|
||||||
api_key = row.get("api_key_enc") or ""
|
# - 若库里是明文,仍可工作
|
||||||
api_secret = row.get("api_secret_enc") or ""
|
# - 若库里是 enc:v1 密文但未配 ATS_MASTER_KEY,则不能解密,也不能把密文当作 Key 使用
|
||||||
|
api_key_raw = row.get("api_key_enc") or ""
|
||||||
|
api_secret_raw = row.get("api_secret_enc") or ""
|
||||||
|
api_key = "" if str(api_key_raw).startswith("enc:v1:") else str(api_key_raw)
|
||||||
|
api_secret = "" if str(api_secret_raw).startswith("enc:v1:") else str(api_secret_raw)
|
||||||
use_testnet = bool(row.get("use_testnet") or False)
|
use_testnet = bool(row.get("use_testnet") or False)
|
||||||
return api_key, api_secret, use_testnet
|
return api_key, api_secret, use_testnet
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
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 [accountTradingStatus, setAccountTradingStatus] = useState(null)
|
||||||
|
const [accountTradingErr, setAccountTradingErr] = useState('')
|
||||||
|
|
||||||
// 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航)
|
// 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航)
|
||||||
const [accountId, setAccountId] = useState(getCurrentAccountId())
|
const [accountId, setAccountId] = useState(getCurrentAccountId())
|
||||||
|
|
@ -212,8 +213,10 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
try {
|
try {
|
||||||
const res = await api.getAccountTradingStatus(accountId)
|
const res = await api.getAccountTradingStatus(accountId)
|
||||||
setAccountTradingStatus(res)
|
setAccountTradingStatus(res)
|
||||||
|
setAccountTradingErr('')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 非 owner/未配置 supervisor 时可能会失败,静默即可
|
setAccountTradingStatus(null)
|
||||||
|
setAccountTradingErr(error?.message || '获取交易进程状态失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,6 +404,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
const cur = getCurrentAccountId()
|
const cur = getCurrentAccountId()
|
||||||
|
|
||||||
if (cur !== accountId) setAccountId(cur)
|
if (cur !== accountId) setAccountId(cur)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
|
|
@ -795,6 +799,87 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
<div className="system-hint">
|
<div className="system-hint">
|
||||||
提示:若按钮报“无权限”,请让管理员在用户授权里把该账号分配为 owner;若报 supervisor 相关错误,请检查后端对 `/www/server/panel/plugin/supervisor` 的写权限与 supervisorctl 可执行权限。
|
提示:若按钮报“无权限”,请让管理员在用户授权里把该账号分配为 owner;若报 supervisor 相关错误,请检查后端对 `/www/server/panel/plugin/supervisor` 的写权限与 supervisorctl 可执行权限。
|
||||||
</div>
|
</div>
|
||||||
|
{accountTradingErr ? (
|
||||||
|
<div className="system-hint" style={{ color: '#b00020' }}>
|
||||||
|
{accountTradingErr}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{accountTradingStatus?.raw ? (
|
||||||
|
<details style={{ marginTop: '8px' }}>
|
||||||
|
<summary style={{ cursor: 'pointer' }}>查看原始状态输出</summary>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.raw || '')}</pre>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
|
{accountTradingStatus?.stderr_tail ? (
|
||||||
|
<details style={{ marginTop: '8px' }}>
|
||||||
|
<summary style={{ cursor: 'pointer' }}>查看最近错误日志(stderr)</summary>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail || '')}</pre>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 账号密钥(当前账号;owner/admin 可改) */}
|
||||||
|
<div className="system-section">
|
||||||
|
<div className="system-header">
|
||||||
|
<h3>账号密钥(当前账号)</h3>
|
||||||
|
<div className="system-status">
|
||||||
|
<span className="system-status-meta">说明:为安全起见,页面不会回显 Secret;填入后保存即可。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="accounts-form">
|
||||||
|
<label>
|
||||||
|
API KEY(留空=不改)
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={credForm.api_key}
|
||||||
|
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })}
|
||||||
|
placeholder="粘贴你的 Binance API KEY"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
API SECRET(留空=不改)
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={credForm.api_secret}
|
||||||
|
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })}
|
||||||
|
placeholder="粘贴你的 Binance API SECRET"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="accounts-inline">
|
||||||
|
<span>测试网</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!credForm.use_testnet}
|
||||||
|
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="accounts-form-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn primary"
|
||||||
|
disabled={systemBusy}
|
||||||
|
onClick={async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const payload = {}
|
||||||
|
if (credForm.api_key) payload.api_key = credForm.api_key
|
||||||
|
if (credForm.api_secret) payload.api_secret = credForm.api_secret
|
||||||
|
payload.use_testnet = !!credForm.use_testnet
|
||||||
|
await api.updateAccountCredentials(accountId, payload)
|
||||||
|
setMessage('密钥已更新(建议重启该账号交易进程)')
|
||||||
|
setCredForm({ api_key: '', api_secret: '', use_testnet: !!credForm.use_testnet })
|
||||||
|
} catch (e) {
|
||||||
|
setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
保存密钥
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 系统控制:清缓存 / 启停 / 重启(supervisor) */}
|
{/* 系统控制:清缓存 / 启停 / 重启(supervisor) */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user