Use 比特骆驼 logo for PWA/brand marks, remove install button, docs Ubuntu 22.04 only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 22:07:11 +08:00
parent 86d3c94f32
commit 8813fd3e37
15 changed files with 66 additions and 192 deletions
+21 -9
View File
@@ -8,6 +8,15 @@
仓库:<https://git.bz121.com/dekun/eth_hedge_sim.git>
## 运行环境
| 项 | 约定 |
|----|------|
| 操作系统 | **Ubuntu 22.04** LTS |
| 进程管理 | PM2`eth-hedge-api` |
| 安装目录 | `/opt/eth_hedge_sim` |
| 端口 | **5155** |
## 文档
- [开发方案](docs/开发方案.md)
@@ -21,11 +30,10 @@
- 反代:[`https://dc.hyf2.cc`](https://dc.hyf2.cc) → 本机 `5155`PM2: `eth-hedge-api`
- 登录账号:见服务器 `/opt/eth_hedge_sim/.env``AUTH_USERNAME` / `AUTH_PASSWORD`
- 可在「系统设置」修改用户名/密码;前端固定同源 API,无 API 地址配置项
- 支持电脑 / 平板安装为独立应用(PWA)
## 一键部署 / 更新(禁止 scp 传代码)
服务器目录`/opt/eth_hedge_sim`
目标机:**Ubuntu 22.04**目录 `/opt/eth_hedge_sim`
更新方式:**只允许 `git pull`**,然后构建并 `pm2 startOrReload` 本项目进程。
### 推荐:curl 一键管理器(菜单)
@@ -47,30 +55,33 @@ curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/mana
bash /opt/eth_hedge_sim/deploy/manage.sh
```
### 机触发远程更新
### 从开发机触发远程更新(可选)
在能 SSH 到 Ubuntu 服务器的机器上:
```bash
pip install paramiko
set DEPLOY_PASS=*** # Windows PowerShell: $env:DEPLOY_PASS='***'
export DEPLOY_PASS='***'
python scripts/deploy_remote.py
```
PM2 进程名:`eth-hedge-api`(端口 **5155**)。禁止 `pm2 restart all`
## 本地开发
## 服务器上本地开发 / 调试(Ubuntu 22.04
```bash
python -m venv .venv
.venv\Scripts\activate
cd /opt/eth_hedge_sim
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
copy .env.example .env
cp .env.example .env
python scripts/smoke_market.py
cd backend
uvicorn app.main:app --host 0.0.0.0 --port 5155 --reload
# 另开终端
cd frontend
cd /opt/eth_hedge_sim/frontend
npm ci
npm run dev
```
@@ -81,3 +92,4 @@ npm run dev
- 永续市价;期权只吃买卖一;滑点 = 1×手续费
- 仓位:永续 1 ETH,期权 2 ETH 名义
- 部署更新只用 git pull,不用 scp
- 部署环境固定为 Ubuntu 22.04
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# deploy/lib/common.sh — 比特骆驼自动化对冲系统 部署公共函数
# 目标系统: Ubuntu 22.04 LTS
set -e
set -u
if [ -n "${BASH_VERSION:-}" ]; then
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# 比特骆驼自动化对冲系统 部署管理器(工程 eth_hedge_sim
# 部署环境: Ubuntu 22.04 LTS
#
# 新服务器(免克隆):
# curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash
+3 -1
View File
@@ -191,6 +191,8 @@ frontend/src/components/
## 4. 部署相关文件
目标环境:**Ubuntu 22.04** + PM2。目录默认 `/opt/eth_hedge_sim`
### 4.1 `deploy/ecosystem.config.cjs`(示意)
```js
@@ -252,7 +254,7 @@ module.exports = {
| 允许 | 禁止 |
|------|------|
| 复制 OKX 行情订阅、签名、深度解析到本仓库 | 修改 `C:\Users\dekun\Desktop\crypto_monitor` 任何文件 |
| 复制 OKX 行情订阅、签名、深度解析到本仓库 | 修改现网 `crypto_monitor` 仓库内任何文件 |
| 复制「买一流动性检查」思路后重写 | 把本项目塞进现网 monorepo 一起 PM2 |
| 新建本仓库的 `.env` | 默认使用现网交易 Key 做模拟(模拟不需要交易权限) |
+2 -2
View File
@@ -43,13 +43,13 @@
| 阶段 | 形态 | 说明 |
|------|------|------|
| 现在(内部) | Git + `deploy/manage.sh` + PM2 | 已具备,继续打磨成「可复制安装」 |
| 商用 v1 | **一键部署脚本** + 环境检查 + 健康检查 | 客户 Ubuntu 机器上跑通 |
| 商用 v1 | **一键部署脚本** + 环境检查 + 健康检查 | 客户 **Ubuntu 22.04** 机器上跑通 |
| 商用 v2 | **Docker Compose**(可选) | 降低环境差异;与脚本二选一或并存 |
| 商用 v3 | 离线包 / 镜像导出 | 内网客户、无法拉 Git 的场景 |
### 2.2 客户侧必备条件
- 一台独立 Linux 主机(建议独立 IP
- 一台独立 **Ubuntu 22.04** 主机(建议独立 IP
- 出网访问目标交易所 API(OKX 等)
- 域名或 IP + HTTPS(可用反代)
- 客户自备交易所 API Key(只读行情 / 实盘交易按档位要求)
+4 -4
View File
@@ -14,8 +14,8 @@
|----|------|
| 产品名 | **比特骆驼自动化对冲系统** |
| 工程名 / 目录名 | `eth_hedge_sim` |
| 本机路径 | `C:\Users\dekun\Desktop\新建文件夹\eth_hedge_sim` |
| 云服务器 | **单独一台 Ubuntu**(或同机不同目录/不同 PM2 进程名,且与现网隔离) |
| 部署操作系统 | **Ubuntu 22.04** LTS |
| 云服务器 | **单独一台 Ubuntu 22.04**(或同机不同目录/不同 PM2 进程名,且与现网隔离) |
| 部署 | PM2 |
| 行情 | OKX **实盘只读** APIREST + WebSocket |
| 成交(默认) | **本地模拟撮合 + 本地虚拟资金**(非 OKX 模拟盘) |
@@ -150,7 +150,7 @@ OKX 实盘只读行情 (WS/REST)
---
## 6. 部署(Ubuntu + PM2
## 6. 部署(Ubuntu 22.04 + PM2
### 6.1 建议目录
@@ -221,4 +221,4 @@ TZ=Asia/Shanghai
## 9. 一句话
**独立仓 `eth_hedge_sim`(产品:比特骆驼自动化对冲系统):OKX/币安真行情只读 + 本地虚拟资金撮合(永续市价、期权只买吃买卖一、滑点=1×手续费)+ 四页前端 + Ubuntu/PM2 单独部署;可抄现网思路,但不改现网代码、不共用现网进程。**
**独立仓 `eth_hedge_sim`(产品:比特骆驼自动化对冲系统):OKX/币安真行情只读 + 本地虚拟资金撮合(永续市价、期权只买吃买卖一、滑点=1×手续费)+ 四页前端 + Ubuntu 22.04 / PM2 单独部署;可抄现网思路,但不改现网代码、不共用现网进程。**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 571 KiB

+10 -3
View File
@@ -1,7 +1,6 @@
import type { ReactNode } from "react";
import { NavLink, Navigate, Route, Routes } from "react-router-dom";
import { clearSession, getToken, getUsername } from "./api/client";
import InstallBadge from "./components/InstallBadge";
import LoginPage from "./pages/Login";
import PlanPage from "./pages/Plan";
import TradesPage from "./pages/Trades";
@@ -14,8 +13,16 @@ function Shell({ children }: { children: ReactNode }) {
<div className="app-shell">
<header className="topnav">
<div className="topnav-side left">
<div className="brand"></div>
<InstallBadge />
<div className="brand">
<img
className="brand-mark"
src="/icons/brand.png"
alt=""
width={28}
height={28}
/>
<span></span>
</div>
</div>
<nav className="topnav-center">
<NavLink to="/plan" className={({ isActive }) => (isActive ? "active" : "")}>
-118
View File
@@ -1,118 +0,0 @@
import { useEffect, useState } from "react";
import {
BeforeInstallPromptEvent,
isAppleTouchDevice,
isStandaloneDisplay,
} from "../pwa/install";
/**
* Desktop/tablet install affordance:
* - Chrome/Edge/Android: native install via beforeinstallprompt
* - iPad/iOS: tip to Add to Home Screen
* - Already installed: show 已安装标识
*/
export default function InstallBadge() {
const [standalone, setStandalone] = useState(isStandaloneDisplay);
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(
null,
);
const [tipOpen, setTipOpen] = useState(false);
const [busy, setBusy] = useState(false);
const apple = isAppleTouchDevice();
useEffect(() => {
const mq = window.matchMedia("(display-mode: standalone)");
const sync = () => setStandalone(isStandaloneDisplay());
mq.addEventListener?.("change", sync);
sync();
const onBip = (e: Event) => {
e.preventDefault();
setDeferred(e as BeforeInstallPromptEvent);
};
const onInstalled = () => {
setDeferred(null);
setStandalone(true);
setTipOpen(false);
};
window.addEventListener("beforeinstallprompt", onBip);
window.addEventListener("appinstalled", onInstalled);
return () => {
mq.removeEventListener?.("change", sync);
window.removeEventListener("beforeinstallprompt", onBip);
window.removeEventListener("appinstalled", onInstalled);
};
}, []);
if (standalone) {
return (
<span className="install-badge installed" title="已安装为独立应用">
</span>
);
}
const canNativeInstall = Boolean(deferred);
async function onInstall() {
if (!deferred) {
setTipOpen((v) => !v);
return;
}
setBusy(true);
try {
await deferred.prompt();
await deferred.userChoice;
setDeferred(null);
} finally {
setBusy(false);
}
}
return (
<div className="install-wrap">
<button
type="button"
className="btn ghost install-btn"
disabled={busy}
onClick={() => void onInstall()}
title={
canNativeInstall
? "安装到电脑或平板"
: apple
? "添加到主屏幕(平板/手机)"
: "安装说明"
}
>
{canNativeInstall ? (busy ? "安装中…" : "安装应用") : "安装应用"}
</button>
{tipOpen && !canNativeInstall ? (
<div className="install-tip" role="dialog" aria-label="安装说明">
{apple ? (
<>
<p>iPad / iPhone Safari </p>
<p className="muted"></p>
</>
) : (
<>
<p>
Chrome / Edge
</p>
<p>
/
</p>
<p className="muted"> HTTPS localhost</p>
</>
)}
<button
type="button"
className="btn ghost"
onClick={() => setTipOpen(false)}
>
</button>
</div>
) : null}
</div>
);
}
+7 -2
View File
@@ -1,7 +1,6 @@
import { FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
import { login, setSession } from "../api/client";
import InstallBadge from "../components/InstallBadge";
export default function LoginPage() {
const nav = useNavigate();
@@ -29,8 +28,14 @@ export default function LoginPage() {
<div className="login-wrap">
<form className="login-box" onSubmit={onSubmit}>
<div className="login-title-row">
<img
className="login-brand-mark"
src="/icons/brand.png"
alt="比特骆驼"
width={48}
height={48}
/>
<h1></h1>
<InstallBadge />
</div>
{err ? <div className="err">{err}</div> : null}
<div className="field">
+17 -53
View File
@@ -81,70 +81,35 @@ input {
}
.brand {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--accent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.install-wrap {
position: relative;
.brand-mark,
.login-brand-mark {
flex-shrink: 0;
border-radius: 6px;
object-fit: cover;
background: #1a1a1a;
}
.install-btn {
font-size: 12px;
padding: 4px 10px;
border: 1px solid var(--line);
color: var(--accent);
}
.install-badge {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
padding: 3px 8px;
border-radius: 4px;
white-space: nowrap;
}
.install-badge.installed {
color: #0b0e11;
background: var(--accent);
}
.install-tip {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 30;
width: min(320px, calc(100vw - 24px));
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--bg-elev);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.install-tip p {
margin: 0 0 8px;
font-size: 12px;
line-height: 1.5;
color: var(--text);
}
.install-tip p.muted,
.install-tip .muted {
color: var(--muted);
.login-brand-mark {
border-radius: 10px;
}
@media (max-width: 900px) {
.topnav-side.left .brand {
max-width: 11em;
.topnav-side.left .brand span {
max-width: 9em;
overflow: hidden;
text-overflow: ellipsis;
}
}
@@ -406,9 +371,8 @@ input {
.login-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}