Compare commits

..

14 Commits

Author SHA1 Message Date
b0685c879b 111 2026-02-26 22:28:43 +08:00
3f313283df fix: Update API base URL to include versioning
- Modify the base URL in the ApiClient constructor to append '/api/v1', ensuring compatibility with versioned API endpoints.
2026-02-26 22:27:25 +08:00
0aa51cc932 refactor: Simplify parameter parsing in install script
- Move the parameter parsing for force installation to a more logical position in the script, ensuring it executes before displaying the running mode.
- Remove redundant code related to force installation parameter parsing, improving script clarity and maintainability.
2026-02-26 21:27:23 +08:00
0f183b61a4 feat: Enhance installation script and download functionality
- Add support for a force installation mode in the install script, allowing users to overwrite existing configurations and databases.
- Improve database setup logic to ensure existing users and databases are only dropped during a force installation.
- Introduce a new API endpoint to list available download files, enhancing the user experience on the download page by only displaying existing files.
- Update HTML templates to reflect the availability of download files dynamically.
2026-02-26 21:22:39 +08:00
900cc5fa09 feat: Improve database setup process
- Update database setup to forcefully terminate connections before dropping the existing database and user, preventing errors during the process.
- Clarify comments regarding the database password and data overwriting in the setup script.
2026-02-26 21:09:16 +08:00
89948f76b7 feat: Update database setup and download handling
- Set a fixed database password and ensure old data is overwritten during setup.
- Enhance the download functionality with improved error handling for missing files, providing user-friendly HTML responses.
- Add instructions for placing client builds in the downloads directory in the admin panel.
2026-02-26 21:05:41 +08:00
a376a9e0f3 11 2026-02-26 20:49:56 +08:00
5d7eef29e8 feat: Install sqlx-cli if not present and run database migrations during setup 2026-02-25 22:26:02 +08:00
d0b6e37ae5 refactor: Remove unused import from authStore.ts 2026-02-25 22:22:15 +08:00
b6191990da 1 2026-02-25 22:13:29 +08:00
cf106cab58 11 2026-02-25 22:07:34 +08:00
fcec19117d Merge branch 'main' of https://gt.funmc.cn/xiaobai/FunConnect 2026-02-25 22:01:22 +08:00
13950a8d09 1 2026-02-25 22:00:35 +08:00
97e79f924a 修复错误 2026-02-25 22:00:15 +08:00
12 changed files with 401 additions and 59 deletions

View File

@@ -152,6 +152,9 @@ export default function Downloads() {
{building ? '构建中...' : '构建选中平台'} {building ? '构建中...' : '构建选中平台'}
</button> </button>
</div> </div>
<p className="text-sm text-gray-500 mb-4">
CI FunMC--windows-x64.exe <code className="bg-gray-100 px-1 rounded">downloads</code> DOWNLOADS_DIR /opt/funmc/downloads
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(platformInfo).map(([key, info]) => ( {Object.entries(platformInfo).map(([key, info]) => (

View File

@@ -1,5 +1,5 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware' import { persist } from 'zustand/middleware'
interface AuthState { interface AuthState {
token: string | null token: string | null

View File

@@ -5,7 +5,7 @@ export class ApiClient {
constructor(baseUrl: string) { constructor(baseUrl: string) {
this.http = axios.create({ this.http = axios.create({
baseURL: baseUrl.replace(/\/$/, '') + '/api', baseURL: baseUrl.replace(/\/$/, '') + '/api/v1',
timeout: 10000, timeout: 10000,
}); });
} }

185
docs/BUILD-CLIENT.md Normal file
View File

@@ -0,0 +1,185 @@
# 各平台客户端构建并放入下载目录
下载页提供的文件名由服务端配置 `CLIENT_VERSION` 决定(默认 `0.1.0`),格式为:
- `FunMC-<版本>-windows-x64.exe`
- `FunMC-<版本>-macos-arm64.dmg` / `FunMC-<版本>-macos-x64.dmg`
- `FunMC-<版本>-linux-x64.AppImage`
- `FunMC-<版本>-android.apk`
构建产物需**复制到服务器的下载目录**并**按上述文件名命名**,下载页才会显示「下载」按钮。
服务器下载目录:`/opt/funmc/downloads`(或环境变量 `DOWNLOADS_DIR`)。
---
## 1. 版本号一致
构建前请确认与服务器一致:
- 查看服务器:`grep CLIENT_VERSION /etc/funmc/server.env`(例如 `0.1.0`
- 下文中的 `VERSION` 请替换为该版本号。
---
## 2. Linux可在服务器本机执行
**在服务器或任意 Linux 机器上:**
```bash
cd /opt/funmc/src/client # 或你的项目 client 目录
npm install --registry https://registry.npmjs.org/
npm run dist:linux
```
产物在 `client/release/` 下,例如:
- `FunConnect-1.1.0-Linux-x64.AppImage`
复制并重命名为下载页期望的文件名后放入下载目录:
```bash
VERSION=0.1.0 # 与 CLIENT_VERSION 一致
cp release/FunConnect-*-Linux-x64.AppImage /opt/funmc/downloads/FunMC-${VERSION}-linux-x64.AppImage
```
---
## 3. Windows
**在 Windows 本机:**
```cmd
cd client
npm install
npm run dist:win
```
产物在 `client\release\`,例如:
- `FunConnect-1.1.0-Win-x64.exe`(或带 nsis 的安装包)
上传到服务器后重命名并放入下载目录(在服务器上执行,或本机重命名后上传):
```bash
# 在服务器上(假设已上传为 FunConnect-1.1.0-Win-x64.exe
VERSION=0.1.0
mv /path/to/FunConnect-1.1.0-Win-x64.exe /opt/funmc/downloads/FunMC-${VERSION}-windows-x64.exe
```
或用 SCP 从本机直接放到服务器并命名PowerShell 示例):
```powershell
scp client\release\FunConnect-1.1.0-Win-x64.exe root@你的服务器IP:/opt/funmc/downloads/FunMC-0.1.0-windows-x64.exe
```
---
## 4. macOS
**在 Mac 本机:**
```bash
cd client
npm install
npm run dist:mac
```
产物在 `client/release/`,例如:
- Apple Silicon: `FunConnect-1.1.0-Mac-arm64.dmg`
- Intel: `FunConnect-1.1.0-Mac-x64.dmg`
上传到服务器并重命名(在服务器上):
```bash
VERSION=0.1.0
mv /path/to/FunConnect-1.1.0-Mac-arm64.dmg /opt/funmc/downloads/FunMC-${VERSION}-macos-arm64.dmg
mv /path/to/FunConnect-1.1.0-Mac-x64.dmg /opt/funmc/downloads/FunMC-${VERSION}-macos-x64.dmg
```
---
## 5. Androidmobile/Expo + React Native
Android 客户端在 **`mobile/`** 目录,使用 **Expo** 构建。任选其一即可。
### 前置要求
- Node.js 18+
- **方式一EAS 云端)**Expo 账号([expo.dev](https://expo.dev) 注册)
- **方式二(本地)**Android Studio + Android SDK并配置好 `ANDROID_HOME`
### 方式一EAS Build推荐无需本机 Android 环境)
在项目根或 `mobile/` 下执行:
```bash
cd mobile
npm install
# 安装 EAS CLI 并登录
npm install -g eas-cli
eas login
# 构建 APK预览/内部分发,直接得到 .apk
eas build --platform android --profile preview
```
构建完成后在 Expo 网页或邮件中下载 **APK**,上传到服务器后重命名并放入下载目录:
```bash
VERSION=0.1.0
cp /path/to/下载的.apk /opt/funmc/downloads/FunMC-${VERSION}-android.apk
```
### 方式二:本地构建(需 Android Studio + SDK
```bash
cd mobile
npm install
# 生成原生 android/ 目录
npx expo prebuild
# 构建 Release APK
cd android && ./gradlew assembleRelease
```
APK 输出路径:
- `mobile/android/app/build/outputs/apk/release/app-release.apk`
复制到服务器下载目录并重命名:
```bash
VERSION=0.1.0
cp mobile/android/app/build/outputs/apk/release/app-release.apk /opt/funmc/downloads/FunMC-${VERSION}-android.apk
# 若在服务器上,可先 scp 上传再执行 cp/mv
```
---
## 6. 一键复制脚本示例(在服务器上使用)
在服务器上,若已把各平台构建产物上传到某目录(或本机刚构建好 Linux 版),可统一复制并重命名:
```bash
# 请先确认版本号与 /etc/funmc/server.env 中 CLIENT_VERSION 一致
VERSION=0.1.0
DOWNLOADS=/opt/funmc/downloads
# Linux本机刚构建时
cp -v /opt/funmc/src/client/release/FunConnect-*-Linux-x64.AppImage "$DOWNLOADS/FunMC-${VERSION}-linux-x64.AppImage" 2>/dev/null || true
# 若 Windows/macOS 已上传到 /opt/funmc/uploads/ 等目录,可类似:
# cp -v /opt/funmc/uploads/FunConnect-*-Win-x64.exe "$DOWNLOADS/FunMC-${VERSION}-windows-x64.exe" 2>/dev/null || true
# cp -v /opt/funmc/uploads/FunConnect-*-Mac-arm64.dmg "$DOWNLOADS/FunMC-${VERSION}-macos-arm64.dmg" 2>/dev/null || true
# cp -v /opt/funmc/uploads/FunConnect-*-Mac-x64.dmg "$DOWNLOADS/FunMC-${VERSION}-macos-x64.dmg" 2>/dev/null || true
```
---
## 7. 验证
- 在服务器上:`ls -la /opt/funmc/downloads`
- 浏览器打开:`http://你的服务器:3000/download`,有文件的平台会显示「下载」按钮,没有的显示「暂无」。

View File

@@ -1,7 +1,9 @@
#!/bin/bash #!/bin/bash
# #
# FunMC 一键部署脚本 # FunMC 一键部署脚本
# 用法: curl -fsSL https://fc.funmc.cn/install.sh | bash # 用法: bash install.sh [ -force ]
# - 无参数: 更新安装保留数据库与现有配置server.env / relay.env / credentials.txt
# - -force: 强制覆盖安装,清空数据库并重写所有配置
# #
set -e set -e
@@ -21,6 +23,15 @@ CONFIG_DIR="/etc/funmc"
DATA_DIR="/var/lib/funmc" DATA_DIR="/var/lib/funmc"
LOG_DIR="/var/log/funmc" LOG_DIR="/var/log/funmc"
# 先解析参数(必须在显示运行模式前执行)
FORCE_INSTALL=0
for arg in "$@"; do
if [ "$arg" = "-force" ] || [ "$arg" = "--force" ]; then
FORCE_INSTALL=1
break
fi
done
echo -e "${CYAN}" echo -e "${CYAN}"
echo "╔═══════════════════════════════════════════════════════════╗" echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ ║" echo "║ ║"
@@ -30,6 +41,12 @@ echo "║ 魔幻方开发 ║"
echo "║ ║" echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════╝" echo "╚═══════════════════════════════════════════════════════════╝"
echo -e "${NC}" echo -e "${NC}"
if [ "$FORCE_INSTALL" -eq 1 ]; then
echo -e "${YELLOW}运行模式: 强制覆盖安装(将清空数据库并重写配置)${NC}"
else
echo -e "${GREEN}运行模式: 更新安装(保留数据库与现有配置)${NC}"
fi
echo ""
# 检查 root 权限 # 检查 root 权限
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
@@ -99,39 +116,45 @@ install_nodejs() {
fi fi
} }
# 配置数据库 # 配置数据库-force 时强制删除并重建,否则仅确保库存在且不覆盖数据)
setup_database() { setup_database() {
echo -e "${YELLOW}[4/7] 配置数据库...${NC}" echo -e "${YELLOW}[4/7] 配置数据库...${NC}"
systemctl enable postgresql systemctl enable postgresql
systemctl start postgresql systemctl start postgresql
# 生成随机密码 if [ "$FORCE_INSTALL" -eq 1 ]; then
DB_PASSWORD=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 24) # 强制断开对 funmc 库的所有连接,再删除库和用户
sudo -u postgres psql -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'funmc' AND pid <> pg_backend_pid();" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "DROP DATABASE IF EXISTS funmc;" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "DROP USER IF EXISTS funmc;" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "CREATE USER funmc WITH PASSWORD '12345678';"
sudo -u postgres psql -d postgres -c "CREATE DATABASE funmc OWNER funmc;"
sudo -u postgres psql -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE funmc TO funmc;"
echo -e "${GREEN}✓ 数据库已强制重建(密码 12345678${NC}"
else
# 更新模式:不删除,仅确保用户和库存在(若已存在则跳过)
sudo -u postgres psql -d postgres -c "CREATE USER funmc WITH PASSWORD '12345678';" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "CREATE DATABASE funmc OWNER funmc;" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE funmc TO funmc;" 2>/dev/null || true
echo -e "${GREEN}✓ 数据库检查完成(未覆盖现有数据)${NC}"
fi
# 创建数据库和用户 # 配置 pg_hba.conf仅追加缺失项
sudo -u postgres psql -c "CREATE USER funmc WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true
sudo -u postgres psql -c "CREATE DATABASE funmc OWNER funmc;" 2>/dev/null || true
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE funmc TO funmc;"
# 配置 pg_hba.conf
PG_HBA=$(sudo -u postgres psql -t -c "SHOW hba_file;" | xargs) PG_HBA=$(sudo -u postgres psql -t -c "SHOW hba_file;" | xargs)
if ! grep -q "funmc" "$PG_HBA"; then if ! grep -q "funmc" "$PG_HBA"; then
echo "local funmc funmc md5" >> "$PG_HBA" echo "local funmc funmc md5" >> "$PG_HBA"
echo "host funmc funmc 127.0.0.1/32 md5" >> "$PG_HBA" echo "host funmc funmc 127.0.0.1/32 md5" >> "$PG_HBA"
systemctl reload postgresql systemctl reload postgresql
fi fi
echo -e "${GREEN}✓ 数据库配置完成${NC}"
echo "$DB_PASSWORD" > /tmp/funmc_db_password
} }
# 下载并编译 FunMC # 下载并编译 FunMC
build_funmc() { build_funmc() {
echo -e "${YELLOW}[5/7] 编译 FunMC...${NC}" echo -e "${YELLOW}[5/7] 编译 FunMC...${NC}"
# 创建目录 # 创建目录(含客户端下载目录)
mkdir -p $INSTALL_DIR $CONFIG_DIR $DATA_DIR $LOG_DIR mkdir -p $INSTALL_DIR $INSTALL_DIR/downloads $CONFIG_DIR $DATA_DIR $LOG_DIR
# 克隆或更新代码 # 克隆或更新代码
if [ -d "$INSTALL_DIR/src" ]; then if [ -d "$INSTALL_DIR/src" ]; then
@@ -146,32 +169,33 @@ build_funmc() {
source $HOME/.cargo/env source $HOME/.cargo/env
cargo build --release -p funmc-server -p funmc-relay-server cargo build --release -p funmc-server -p funmc-relay-server
# 复制二进制文件 # 复制二进制文件包名与二进制名不同server / relay-server
cp target/release/funmc-server $INSTALL_DIR/ cp target/release/server $INSTALL_DIR/funmc-server
cp target/release/funmc-relay-server $INSTALL_DIR/ cp target/release/relay-server $INSTALL_DIR/funmc-relay-server
# 编译管理面板前端 # 编译管理面板前端(使用官方 registry 避免镜像返回 HTML 导致 FETCH_ERROR
cd $INSTALL_DIR/src/admin-panel cd $INSTALL_DIR/src/admin-panel
npm install npm install --registry https://registry.npmjs.org/
npm run build npm run build
cp -r dist $INSTALL_DIR/admin-panel cp -r dist $INSTALL_DIR/admin-panel
echo -e "${GREEN}✓ FunMC 编译完成${NC}" echo -e "${GREEN}✓ FunMC 编译完成${NC}"
} }
# 配置服务 # 配置服务-force 时重写配置;否则若已有配置则保留仅做迁移与重启)
configure_services() { configure_services() {
echo -e "${YELLOW}[6/7] 配置服务...${NC}" echo -e "${YELLOW}[6/7] 配置服务...${NC}"
DB_PASSWORD=$(cat /tmp/funmc_db_password) WROTE_CONFIG=0
JWT_SECRET=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 48) if [ "$FORCE_INSTALL" -eq 1 ] || [ ! -f "$CONFIG_DIR/server.env" ]; then
ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 12) # 强制安装或首次安装:生成新配置
WROTE_CONFIG=1
# 获取服务器 IP DB_PASSWORD="12345678"
SERVER_IP=$(curl -s ifconfig.me || curl -s ipinfo.io/ip || hostname -I | awk '{print $1}') JWT_SECRET=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 48)
ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 12)
# 创建主配置文件 SERVER_IP=$(curl -s ifconfig.me || curl -s ipinfo.io/ip || hostname -I | awk '{print $1}')
cat > $CONFIG_DIR/server.env << EOF
cat > $CONFIG_DIR/server.env << EOF
# FunMC 服务端配置 # FunMC 服务端配置
DATABASE_URL=postgres://funmc:${DB_PASSWORD}@localhost/funmc DATABASE_URL=postgres://funmc:${DB_PASSWORD}@localhost/funmc
JWT_SECRET=${JWT_SECRET} JWT_SECRET=${JWT_SECRET}
@@ -192,16 +216,24 @@ ADMIN_PASSWORD=${ADMIN_PASSWORD}
# 客户端下载 # 客户端下载
CLIENT_DOWNLOAD_ENABLED=true CLIENT_DOWNLOAD_ENABLED=true
CLIENT_VERSION=${FUNMC_VERSION} CLIENT_VERSION=${FUNMC_VERSION}
DOWNLOADS_DIR=$INSTALL_DIR/downloads
EOF EOF
# 创建中继配置 cat > $CONFIG_DIR/relay.env << EOF
cat > $CONFIG_DIR/relay.env << EOF
RELAY_PORT=7900 RELAY_PORT=7900
JWT_SECRET=${JWT_SECRET} JWT_SECRET=${JWT_SECRET}
RUST_LOG=info RUST_LOG=info
EOF EOF
else
# 更新模式:保留现有配置,仅确保 DB_PASSWORD 等变量存在供后续迁移使用
DB_PASSWORD=$(grep DATABASE_URL "$CONFIG_DIR/server.env" 2>/dev/null | sed -n 's/.*:\/\/funmc:\([^@]*\)@.*/\1/p')
if [ -z "$DB_PASSWORD" ]; then
DB_PASSWORD="12345678"
fi
echo -e "${GREEN}✓ 保留现有配置(未覆盖 server.env / relay.env${NC}"
fi
# 创建 systemd 服务文件 # 创建 systemd 服务文件(始终更新以便安装路径等变更生效)
cat > /etc/systemd/system/funmc-server.service << EOF cat > /etc/systemd/system/funmc-server.service << EOF
[Unit] [Unit]
Description=FunMC API Server Description=FunMC API Server
@@ -214,7 +246,7 @@ WorkingDirectory=$INSTALL_DIR
EnvironmentFile=$CONFIG_DIR/server.env EnvironmentFile=$CONFIG_DIR/server.env
ExecStart=$INSTALL_DIR/funmc-server ExecStart=$INSTALL_DIR/funmc-server
Restart=always Restart=always
RestartSec=5 RestartSec=10
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
@@ -238,9 +270,14 @@ RestartSec=5
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
# 运行数据库迁移 # 安装 sqlx-cli若未安装运行数据库迁移
export PATH="$HOME/.cargo/bin:$PATH"
if ! command -v sqlx &> /dev/null; then
cargo install sqlx-cli --no-default-features --features postgres
fi
cd $INSTALL_DIR/src/server cd $INSTALL_DIR/src/server
DATABASE_URL="postgres://funmc:${DB_PASSWORD}@localhost/funmc" cargo sqlx migrate run export DATABASE_URL="postgres://funmc:${DB_PASSWORD}@localhost/funmc"
sqlx migrate run
# 启动服务 # 启动服务
systemctl daemon-reload systemctl daemon-reload
@@ -249,8 +286,9 @@ EOF
echo -e "${GREEN}✓ 服务配置完成${NC}" echo -e "${GREEN}✓ 服务配置完成${NC}"
# 保存凭据 # 仅强制/首次安装时写入凭据文件,更新模式不覆盖
cat > $CONFIG_DIR/credentials.txt << EOF if [ "$WROTE_CONFIG" -eq 1 ]; then
cat > $CONFIG_DIR/credentials.txt << EOF
====================================== ======================================
FunMC 服务端安装信息 FunMC 服务端安装信息
====================================== ======================================
@@ -269,7 +307,8 @@ JWT 密钥: ${JWT_SECRET}
请妥善保管此文件! 请妥善保管此文件!
====================================== ======================================
EOF EOF
chmod 600 $CONFIG_DIR/credentials.txt chmod 600 $CONFIG_DIR/credentials.txt
fi
} }
# 配置防火墙 # 配置防火墙

View File

@@ -9,7 +9,6 @@ use quinn::{Endpoint, ServerConfig, TransportConfig};
use quinn::crypto::rustls::QuicServerConfig; use quinn::crypto::rustls::QuicServerConfig;
use rcgen::{CertifiedKey, generate_simple_self_signed}; use rcgen::{CertifiedKey, generate_simple_self_signed};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

View File

@@ -0,0 +1,61 @@
#!/bin/bash
# 将 client/release 下的构建产物复制到服务器下载目录并命名为 FunMC-<版本>-<平台>.<后缀>
# 用法:
# bash scripts/copy-client-to-downloads.sh [版本号]
# bash scripts/copy-client-to-downloads.sh # 版本号从 /etc/funmc/server.env 的 CLIENT_VERSION 读取
# 需在项目根目录或指定 CLIENT_DIR、DOWNLOADS_DIR 运行。
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CLIENT_DIR="${CLIENT_DIR:-$REPO_ROOT/client}"
DOWNLOADS_DIR="${DOWNLOADS_DIR:-/opt/funmc/downloads}"
if [ -n "$1" ]; then
VERSION="$1"
elif [ -f /etc/funmc/server.env ]; then
VERSION=$(grep -E '^CLIENT_VERSION=' /etc/funmc/server.env | cut -d= -f2)
fi
if [ -z "$VERSION" ]; then
echo "用法: $0 <版本号> 或确保 /etc/funmc/server.env 中有 CLIENT_VERSION"
echo "示例: $0 0.1.0"
exit 1
fi
RELEASE="$CLIENT_DIR/release"
if [ ! -d "$RELEASE" ]; then
echo "未找到 $RELEASE,请先在 client 目录执行 npm run dist:linux 等构建"
exit 1
fi
mkdir -p "$DOWNLOADS_DIR"
# 复制并重命名Electron 产物: FunConnect-<package.json版本>-<平台>-<arch>.<ext>
copied=0
for f in "$RELEASE"/FunConnect-*-Linux-x64.AppImage; do
[ -f "$f" ] || continue
cp -v "$f" "$DOWNLOADS_DIR/FunMC-${VERSION}-linux-x64.AppImage"
copied=$((copied+1))
done
for f in "$RELEASE"/FunConnect-*-Win-x64.exe; do
[ -f "$f" ] || continue
cp -v "$f" "$DOWNLOADS_DIR/FunMC-${VERSION}-windows-x64.exe"
copied=$((copied+1))
done
for f in "$RELEASE"/FunConnect-*-Mac-arm64.dmg; do
[ -f "$f" ] || continue
cp -v "$f" "$DOWNLOADS_DIR/FunMC-${VERSION}-macos-arm64.dmg"
copied=$((copied+1))
done
for f in "$RELEASE"/FunConnect-*-Mac-x64.dmg; do
[ -f "$f" ] || continue
cp -v "$f" "$DOWNLOADS_DIR/FunMC-${VERSION}-macos-x64.dmg"
copied=$((copied+1))
done
if [ $copied -eq 0 ]; then
echo "未在 $RELEASE 中找到可复制的 FunConnect-* 文件,请先构建客户端"
exit 1
fi
echo "已复制 $copied 个文件到 $DOWNLOADS_DIR(版本 $VERSION"

View File

@@ -2,7 +2,7 @@ use axum::{
body::Body, body::Body,
extract::{Path, State}, extract::{Path, State},
http::{header, StatusCode}, http::{header, StatusCode},
response::{Html, IntoResponse, Response}, response::{Html, Response},
Json, Json,
}; };
use serde::Serialize; use serde::Serialize;
@@ -32,6 +32,24 @@ pub struct ClientBuild {
pub status: String, pub status: String,
} }
/// 列出 downloads 目录下可用的文件名(供下载页仅对存在的文件显示「下载」)
pub async fn list_download_files() -> Json<Vec<String>> {
let downloads_dir = std::env::var("DOWNLOADS_DIR").unwrap_or_else(|_| "./downloads".to_string());
let mut names = Vec::new();
if let Ok(mut rd) = tokio::fs::read_dir(&downloads_dir).await {
while let Ok(Some(entry)) = rd.next_entry().await {
if let Ok(meta) = entry.metadata().await {
if meta.is_file() {
if let Ok(name) = entry.file_name().into_string() {
names.push(name);
}
}
}
}
}
Json(names)
}
pub async fn get_client_config(State(state): State<Arc<AppState>>) -> Json<ClientConfig> { pub async fn get_client_config(State(state): State<Arc<AppState>>) -> Json<ClientConfig> {
let config = state.server_config.read().unwrap(); let config = state.server_config.read().unwrap();
@@ -70,7 +88,7 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
"http://localhost:3000".to_string() "http://localhost:3000".to_string()
}; };
let html = format!(r#"<!DOCTYPE html> let html = format!(r##"<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@@ -103,8 +121,8 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<div class="text-5xl mb-4">🪟</div> <div class="text-5xl mb-4">🪟</div>
<h3 class="text-xl font-semibold mb-2">Windows</h3> <h3 class="text-xl font-semibold mb-2">Windows</h3>
<p class="text-gray-500 text-sm mb-4">Windows 10/11</p> <p class="text-gray-500 text-sm mb-4">Windows 10/11</p>
<a href="{server_url}/api/v1/download/FunMC-{version}-windows-x64.exe" <a href="{server_url}/api/v1/download/FunMC-{version}-windows-x64.exe" data-download-file="FunMC-{version}-windows-x64.exe"
class="inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"> class="dl-link inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
下载 .exe 下载 .exe
</a> </a>
</div> </div>
@@ -117,12 +135,12 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<h3 class="text-xl font-semibold mb-2">macOS</h3> <h3 class="text-xl font-semibold mb-2">macOS</h3>
<p class="text-gray-500 text-sm mb-4">macOS 11+</p> <p class="text-gray-500 text-sm mb-4">macOS 11+</p>
<div class="space-y-2"> <div class="space-y-2">
<a href="{server_url}/api/v1/download/FunMC-{version}-macos-arm64.dmg" <a href="{server_url}/api/v1/download/FunMC-{version}-macos-arm64.dmg" data-download-file="FunMC-{version}-macos-arm64.dmg"
class="inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"> class="dl-link inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
Apple Silicon Apple Silicon
</a> </a>
<a href="{server_url}/api/v1/download/FunMC-{version}-macos-x64.dmg" <a href="{server_url}/api/v1/download/FunMC-{version}-macos-x64.dmg" data-download-file="FunMC-{version}-macos-x64.dmg"
class="inline-block w-full py-2 px-4 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm"> class="dl-link inline-block w-full py-2 px-4 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm">
Intel Mac Intel Mac
</a> </a>
</div> </div>
@@ -135,8 +153,8 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<div class="text-5xl mb-4">🐧</div> <div class="text-5xl mb-4">🐧</div>
<h3 class="text-xl font-semibold mb-2">Linux</h3> <h3 class="text-xl font-semibold mb-2">Linux</h3>
<p class="text-gray-500 text-sm mb-4">Ubuntu/Debian/Fedora</p> <p class="text-gray-500 text-sm mb-4">Ubuntu/Debian/Fedora</p>
<a href="{server_url}/api/v1/download/FunMC-{version}-linux-x64.AppImage" <a href="{server_url}/api/v1/download/FunMC-{version}-linux-x64.AppImage" data-download-file="FunMC-{version}-linux-x64.AppImage"
class="inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"> class="dl-link inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
下载 AppImage 下载 AppImage
</a> </a>
</div> </div>
@@ -151,8 +169,8 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<div class="text-center"> <div class="text-center">
<div class="text-4xl mb-3">🤖</div> <div class="text-4xl mb-3">🤖</div>
<h4 class="font-semibold mb-2">Android</h4> <h4 class="font-semibold mb-2">Android</h4>
<a href="{server_url}/api/v1/download/FunMC-{version}-android.apk" <a href="{server_url}/api/v1/download/FunMC-{version}-android.apk" data-download-file="FunMC-{version}-android.apk"
class="inline-block w-full py-2 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"> class="dl-link inline-block w-full py-2 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
下载 APK 下载 APK
</a> </a>
</div> </div>
@@ -186,8 +204,22 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
魔幻方开发 · FunMC 魔幻方开发 · FunMC
</div> </div>
</div> </div>
<script>
(function(){{
var apiBase = window.location.origin + '/api/v1';
fetch(apiBase + '/download/list').then(function(r){{ return r.json(); }}).then(function(list){{
var set = new Set(list || []);
document.querySelectorAll('a.dl-link[data-download-file]').forEach(function(a){{
var name = a.getAttribute('data-download-file');
if (!set.has(name)) {{
a.outerHTML = '<span class="inline-block w-full py-3 px-4 bg-gray-300 text-gray-500 rounded-lg cursor-not-allowed">暂无</span>';
}}
}});
}}).catch(function(){{}});
}})();
</script>
</body> </body>
</html>"#, </html>"##,
server_name = config.server_name, server_name = config.server_name,
server_url = server_url, server_url = server_url,
version = config.client_version, version = config.client_version,
@@ -256,11 +288,31 @@ pub async fn list_builds() -> Json<Vec<ClientBuild>> {
Json(builds) Json(builds)
} }
pub async fn download_file(Path(filename): Path<String>) -> Result<Response, StatusCode> { pub async fn download_file(Path(filename): Path<String>) -> Result<Response, Response> {
let downloads_dir = std::env::var("DOWNLOADS_DIR").unwrap_or_else(|_| "./downloads".to_string()); let downloads_dir = std::env::var("DOWNLOADS_DIR").unwrap_or_else(|_| "./downloads".to_string());
let file_path = std::path::Path::new(&downloads_dir).join(&filename); let file_path = std::path::Path::new(&downloads_dir).join(&filename);
let file = File::open(&file_path).await.map_err(|_| StatusCode::NOT_FOUND)?; let file = match File::open(&file_path).await {
Ok(f) => f,
Err(_) => {
let escaped = filename.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;");
let html = format!(r##"<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>文件不存在</title></head>
<body style="font-family:sans-serif;max-width:560px;margin:80px auto;padding:20px;">
<h1>文件不存在</h1>
<p>未找到 <strong>{}</strong>。</p>
<p>可能原因:该版本尚未构建或未上传到服务器。请联系管理员,或将构建好的客户端放入服务器的 <code>downloads</code> 目录后重试。</p>
<p><a href="/download">返回下载页</a></p>
</body>
</html>"##, escaped);
return Err(Response::builder()
.status(StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
.body(Body::from(html))
.unwrap());
}
};
let stream = ReaderStream::new(file); let stream = ReaderStream::new(file);
let body = Body::from_stream(stream); let body = Body::from_stream(stream);
@@ -289,6 +341,7 @@ pub async fn download_file(Path(filename): Path<String>) -> Result<Response, Sta
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
pub struct TriggerBuildBody { pub struct TriggerBuildBody {
#[allow(dead_code)]
pub platforms: Vec<String>, pub platforms: Vec<String>,
} }

View File

@@ -43,7 +43,7 @@ pub async fn list_friends(
match rows { match rows {
Ok(friends) => { Ok(friends) => {
let dtos: Vec<_> = friends.into_iter().map(|(id, username, avatar_seed, status)| { let dtos: Vec<_> = friends.into_iter().map(|(id, username, avatar_seed, status)| {
let is_online = state.presence.is_online(id); let is_online = state.presence.is_online(&id);
FriendDto { id, username, avatar_seed, is_online, status } FriendDto { id, username, avatar_seed, is_online, status }
}).collect(); }).collect();
(StatusCode::OK, Json(serde_json::json!(dtos))).into_response() (StatusCode::OK, Json(serde_json::json!(dtos))).into_response()

View File

@@ -59,6 +59,7 @@ pub fn router(_state: Arc<AppState>) -> Router<Arc<AppState>> {
.route("/admin/builds/trigger", post(download::trigger_build)) .route("/admin/builds/trigger", post(download::trigger_build))
// Download // Download
.route("/client-config", get(download::get_client_config)) .route("/client-config", get(download::get_client_config))
.route("/download/list", get(download::list_download_files))
.route("/download/:filename", get(download::download_file)) .route("/download/:filename", get(download::download_file))
// WebSocket signaling // WebSocket signaling
.route("/ws", get(ws_handler)) .route("/ws", get(ws_handler))

View File

@@ -38,7 +38,7 @@ pub async fn search_users(
"id": id, "id": id,
"username": username, "username": username,
"avatar_seed": avatar_seed, "avatar_seed": avatar_seed,
"is_online": state.presence.is_online(id), "is_online": state.presence.is_online(&id),
})).collect(); })).collect();
(StatusCode::OK, Json(serde_json::json!(dtos))).into_response() (StatusCode::OK, Json(serde_json::json!(dtos))).into_response()
} }

View File

@@ -6,6 +6,7 @@ use uuid::Uuid;
use funmc_shared::protocol::SignalingMessage; use funmc_shared::protocol::SignalingMessage;
use tokio::sync::mpsc; use tokio::sync::mpsc;
#[allow(dead_code)]
pub struct SignalingSession { pub struct SignalingSession {
pub user_id: Uuid, pub user_id: Uuid,
pub tx: mpsc::UnboundedSender<SignalingMessage>, pub tx: mpsc::UnboundedSender<SignalingMessage>,