Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 36 additions & 11 deletions client/src/pages/GameDeploymentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface GameInfo {
url: string
docs?: string
system?: string[]
login_anonymous?: boolean
supportedOnCurrentPlatform?: boolean
currentPlatform?: string
panelCompatibleOnCurrentPlatform?: boolean
Expand All @@ -59,6 +60,14 @@ interface Games {
[key: string]: GameInfo
}

const quoteSteamCMDArgument = (value: string, platform?: string): string => {
if (platform === 'Windows') {
return `'${value.replace(/'/g, "''")}'`
}

return `'${value.replace(/'/g, "'\\''")}'`
}

// 辅助函数:判断是否为 Windows 平台
const isWindowsPlatform = (systemInfo: any): boolean => {
// 优先使用 rawPlatform(原始平台标识)
Expand Down Expand Up @@ -2543,17 +2552,24 @@ const GameDeploymentPage: React.FC = () => {
useEffect(() => {
if (showInstallModal && selectedGame) {
const forceInstallDir = `force_install_dir "${installPath.trim()}"`
const steamLoginArgs = [
'login',
quoteSteamCMDArgument(steamUsername.trim(), selectedGame.info.currentPlatform),
...(steamPassword.trim()
? [quoteSteamCMDArgument(steamPassword.trim(), selectedGame.info.currentPlatform)]
: [])
]

const loginCommand = useAnonymous
? 'login anonymous'
: `login ${steamUsername.trim()} ${steamPassword.trim()}`
: steamLoginArgs.join(' ')

const appUpdateCommand = validateGameIntegrity
? `app_update ${selectedGame.info.appid} validate`
: `app_update ${selectedGame.info.appid}`

// force_install_dir 必须在 login 之前,否则 SteamCMD 会报错
const fullCommand = `steamcmd +${forceInstallDir} +${loginCommand} +${appUpdateCommand} +quit`
const fullCommand = `+${forceInstallDir} +${loginCommand} +${appUpdateCommand} +quit`
setSteamcmdCommand(fullCommand)
}
}, [showInstallModal, selectedGame, useAnonymous, steamUsername, steamPassword, validateGameIntegrity, installPath])
Expand Down Expand Up @@ -2628,6 +2644,12 @@ const GameDeploymentPage: React.FC = () => {
// 打开安装对话框的通用函数
const openInstallModal = async (gameKey: string, gameInfo: GameInfo) => {
const defaultInstanceName = gameInfo.game_nameCN
const shouldUseAnonymous = gameInfo.login_anonymous !== false
setUseAnonymous(shouldUseAnonymous)
if (shouldUseAnonymous) {
setSteamUsername('')
setSteamPassword('')
}

// 检查是否存在同名实例
try {
Expand Down Expand Up @@ -2909,11 +2931,11 @@ const GameDeploymentPage: React.FC = () => {
return
}

if (!useAnonymous && (!steamUsername.trim() || !steamPassword.trim())) {
if (!useAnonymous && !steamUsername.trim()) {
addNotification({
type: 'error',
title: '参数错误',
message: '请填写Steam账户信息'
message: '请填写Steam用户名'
})
return
}
Expand Down Expand Up @@ -2946,7 +2968,7 @@ const GameDeploymentPage: React.FC = () => {
instanceName: instanceName.trim(),
useAnonymous,
steamUsername: useAnonymous ? undefined : steamUsername.trim(),
steamPassword: useAnonymous ? undefined : steamPassword.trim(),
steamPassword: useAnonymous || !steamPassword.trim() ? undefined : steamPassword.trim(),
steamcmdCommand: steamcmdCommand.trim(),
existingInstanceId: currentExistingInstanceId || undefined,
updateInstanceInfo: currentUpdateInstanceInfo,
Expand Down Expand Up @@ -5763,15 +5785,18 @@ const GameDeploymentPage: React.FC = () => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Steam密码
Steam密码(可选)
</label>
<input
type="password"
value={steamPassword}
onChange={(e) => setSteamPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="输入Steam密码"
placeholder="留空则在终端中输入"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
留空时 SteamCMD 会在终端提示输入密码和 Steam Guard 码
</p>
</div>
</div>
)}
Expand Down Expand Up @@ -5844,18 +5869,18 @@ const GameDeploymentPage: React.FC = () => {

<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
SteamCMD 安装命令
SteamCMD 安装参数
</label>
<textarea
value={steamcmdCommand}
onChange={(e) => setSteamcmdCommand(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono text-sm"
placeholder="SteamCMD 命令将在这里显示,您可以修改后执行"
placeholder="SteamCMD 安装参数将在这里显示,您可以修改后执行"
rows={4}
readOnly={false}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
您可以修改此命令来自定义安装参数,修改后的命令将用于实际安装
面板会自动调用已配置的 steamcmd.exe 或 steamcmd.sh,请只保留 +force_install_dir、+login、+app_update 等参数
</p>
</div>
</div>
Expand All @@ -5875,7 +5900,7 @@ const GameDeploymentPage: React.FC = () => {
disabled={
!installPath.trim() ||
!instanceName.trim() ||
(!useAnonymous && (!steamUsername.trim() || !steamPassword.trim()))
(!useAnonymous && !steamUsername.trim())
}
className="px-4 py-2 disabled:bg-gray-400 text-white rounded-lg transition-colors flex items-center space-x-2 bg-blue-600 hover:bg-blue-700"
>
Expand Down
95 changes: 95 additions & 0 deletions docs/内网穿透插件需求说明.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 内网穿透插件需求说明

## 背景

家用主机、NAS、宿舍网络、运营商 CGNAT 等没有公网入口的环境中,游戏服务端即使启动成功,也不方便让外部玩家连接。

这个问题不是泰拉瑞亚特有的。Minecraft、Palworld、Valheim、Project Zomboid、Factorio 等游戏只要运行在不可直连的网络里,都会遇到类似的端口暴露问题。因此内网穿透能力应当作为跨游戏的可选工具实现,而不是绑定到某一个游戏部署流程。

## 目标

提供一个可选工具,让没有公网 IP 的设备也能把游戏服务端端口暴露给外部玩家。插件只负责网络入口,不改变游戏实例本身的部署、配置和启动逻辑。

## 支持对象

第一阶段建议支持:

- frp 客户端模式:适合已有 frps 服务器的用户。
- EasyTier 组网模式:适合希望多节点虚拟局域网互通的用户。

后续可以扩展到其他工具,但需要满足同一套安全边界:配置可审计、启动命令受控、敏感信息脱敏、进程可停止。

## MVP 能力

- 从实例列表选择一个游戏实例,读取实例名称、工作目录和端口信息。
- 支持手动填写本地地址、本地端口、远端端口、远端服务器、认证 token 或组网密钥。
- 生成 frpc 或 EasyTier 配置文件,默认保存到 `server/data/tunnels/` 下。
- 通过实例管理创建一个独立的“穿透实例”,让用户用现有实例启停能力管理隧道进程。
- 显示连接说明,例如 `远端地址:远端端口` 或 EasyTier 虚拟 IP。
- 不要求游戏实例和穿透实例同生命周期绑定,避免穿透失败影响游戏服务本身。

## 非目标

- 不把 frp/EasyTier 变成游戏部署前置依赖。
- 不替用户申请公网服务器或托管中继。
- 不绕过游戏本身的账号、白名单、RCON、反作弊或服务端安全配置。
- 不默认启用 UPnP 或自动开放所有端口。
- 不在仓库内提交 frp、EasyTier 等第三方二进制文件。

## 推荐实现方式

### 当前可复用能力

项目已有插件体系和示例插件,可以直接复用:

- 插件目录:`server/data/plugins/`。
- 插件配置:`plugin.json`,支持 `enabled`、`hasWebInterface`、`entryPoint`、`category` 等字段。
- 插件 API 客户端:`gsm3-api.js`,请求前缀为 `/api/plugin-api`。
- 实例 API:`getInstances()`、`createInstance()`、`startInstance()`、`stopInstance()`、`restartInstance()`、`deleteInstance()`。
- 文件 API:`mkdir()`、`writeFile()`、`readFile()`、`exists()`、`listFiles()` 等,可用于写入 `server/data/tunnels/` 下的配置文件。
- 终端 API:可查看终端会话和活跃进程,MVP 阶段只作为状态辅助,不直接拼接任意命令。

### 插件优先

先做成 `server/data/plugins/tunnel-helper` 这类可选插件:

- `plugin.json` 默认 `enabled: false`,避免默认安装后自动出现网络暴露入口。
- `index.html` 只做配置生成、实例创建和状态查看。
- 复用 `gsm3-api.js` 的实例 API、文件 API 和终端/进程列表 API。

这种方式与当前插件体系匹配,风险较低,也不会把第三方网络工具耦合进核心部署页。

### 核心 API 补强

如果后续要做成完整的一键体验,核心需要补一个受限的后台进程/工具管理能力:

- 管理受信任工具定义:工具名称、版本、下载地址、校验和、支持平台。
- 统一保存配置:配置文件、敏感字段、脱敏展示、备份恢复。
- 统一管理进程:启动、停止、重启、日志、健康状态、退出码。
- 限制命令注入:不要让插件拼接任意 shell 字符串后直接执行。
- 与实例端口关联:从游戏配置或实例元数据中读取端口,减少重复输入。

## 安全与维护约束

- 所有后端接口必须加认证中间件。
- 持久化数据放在 `server/data` 下,兼容开发和打包路径。
- token、密钥、服务端地址等敏感字段不要出现在日志和普通列表展示中。
- 下载第三方工具必须记录来源、版本、哈希校验和失败回滚策略。
- Windows、Linux、Docker 三种运行方式的路径、权限和停止信号要分别验证。
- 默认生成的配置应当最小权限暴露,只映射用户明确选择的端口。

## 下一步实现计划

1. 创建 `server/data/plugins/tunnel-helper/`,复制示例插件的 `gsm3-api.js`,新增默认关闭的 `plugin.json`。
2. 实现单页表单:选择已有游戏实例,选择 `frp` 或 `EasyTier`,填写本地端口、远端地址、远端端口和认证字段。
3. 用文件 API 生成配置:frp 写入 `tunnels/<name>/frpc.toml`,EasyTier 写入 `tunnels/<name>/easytier.json` 或启动参数说明。
4. 用实例 API 创建穿透实例:`workingDirectory` 指向隧道配置目录,`startCommand` 引用用户已安装的 `frpc` 或 `easytier-core` 可执行文件,`stopCommand` 使用 `ctrl+c`。
5. 增加连接结果展示:保存配置路径、实例名称、远端连接地址、关联的游戏实例和端口。
6. 增加敏感字段处理:表单默认隐藏 token,列表展示脱敏;后续如需服务端日志脱敏,再补核心 API。
7. 再评估一键下载:确认工具版本、平台包、哈希校验和更新策略后,再考虑下载器和工具管理 API。

## PR 拆分建议

1. 通用内网穿透插件需求说明和界面草图。
2. `tunnel-helper` 插件 MVP:生成配置并创建受管实例。
3. 核心进程/工具 API 补强,完善二进制下载、校验、日志和状态检测。
65 changes: 65 additions & 0 deletions docs/泰拉瑞亚部署需求说明.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 泰拉瑞亚部署需求说明

## 背景

Steam 游戏部署列表中缺少原版泰拉瑞亚,用户需要回到文档手动下载、上传、解压和交互式配置。

本需求只覆盖泰拉瑞亚本身的部署入口、启动命令和 `serverconfig.txt` 可视化配置。没有公网入口、端口暴露、CGNAT 等问题统一放到通用的内网穿透插件需求中处理。

## 已落地的最小改进

### Steam 部署入口

在 `server/data/games/installgame.json` 中新增 `Terraria`:

- 使用 SteamCMD 专用服务器列表中的 Terraria AppID:`105600`。
- 默认端口:`7777/TCP`。
- 默认不使用匿名登录,安装时需要填写已拥有 Terraria 的 Steam 用户名。密码可留空,让 SteamCMD 在终端中交互式输入密码和 Steam Guard 码。
- 清单内提供 Windows/Linux 默认启动命令,避免实例创建后回落为 `none`。
- 关联现有泰拉瑞亚开服文档。
- 在提示中建议用 `serverconfig.txt` 启动,避免首次启动进入交互式建世界流程。

推荐 Linux 实例启动命令:

```bash
./TerrariaServer.bin.x86_64 -config serverconfig.txt
```

Windows 可改为:

```powershell
.\TerrariaServer.exe -config serverconfig.txt
```

### 可视化配置模板

在 `server/data/gameconfig/泰拉瑞亚.yml` 中新增 `serverconfig.txt` 模板:

- 世界路径、自动建图、种子、世界名、难度。
- 最大玩家数、端口、密码、MOTD。
- 世界目录、封禁列表、作弊防护、语言、UPnP、NPC 同步、优先级。
- 旅途模式权限项。

模板使用现有 `properties` 解析器。Terraria 的 `0/1` 开关用 `select` 表达,避免保存成 `true/false` 后与服务端配置格式不一致。

## 使用提示

### 安装命令

SteamCMD 登录参数经过终端 shell 执行。包含 `$`、空格、`&`、引号等字符的密码不能直接裸写在命令行中,否则可能被 shell 解释。

推荐在密码输入框留空,生成如下命令后在终端交互式输入密码和 Steam Guard 码:

```powershell
.\steamcmd.exe +force_install_dir "D:\Games\Terraria" +login xiwangly2 +app_update 105600 validate +quit
```

### 外部连接

Terraria 默认监听 `7777/TCP`。外部玩家能否连接取决于防火墙、路由器端口转发、公网 IP 或内网穿透能力。跨游戏的端口暴露能力不放在泰拉瑞亚部署需求中实现,详见 `docs/内网穿透插件需求说明.md`。

## PR 拆分建议

1. 泰拉瑞亚 Steam 安装清单。
2. 泰拉瑞亚 `serverconfig.txt` 可视化配置模板。
3. SteamCMD 登录参数转义、密码可选输入、日志脱敏。
Loading
Loading