NAS 上自己开个传奇2!绿联 NAS 用 Docker 从零部署热血传奇2服务端

阅读 -- 次绿联 NAS 用 Docker 部署传奇2服务端

想在自己 NAS 上开个传奇2,翻了一圈才发现:这游戏根本没有现成的 Docker 镜像。没有一键镜像,没有现成 compose,全得自己拼。这篇把我从零到跑通的完整过程写下来——六个进程、一份必须配上对的数据库脚本、一个绕开权限坑的 Docker 卷,还有五个我实打实踩过的坑。照着抄,一个小时左右能跑起来。

一、先说句实话:这套没有现成镜像

如果你开过传奇三的服,可能觉得这事挺简单:拉个镜像、写十几行 compose、up -d,收工。

传奇2 没这么便宜。

我把 GitHub、Docker Hub 和几个 NAS 社区都翻了一遍,开源的传奇2 服务端就两套值得看的:

  • OpenMir2 / MirServer(C# / .NET)—— 控制台程序,能在 Linux 上跑。但没有镜像,官方只给源码加一个 2022 年的预编译包。
  • Crystal(LOMCN 社区)—— 英文圈最主流的一支,服务端却是个 WinForms 桌面程序,只能跑 Windows,塞不进 NAS 的 Docker。

所以能走通的只剩第一条。而且真正费时间的地方都不在 Docker 本身:运行环境得自己搭、数据包版本必须对得上、NAS 的文件权限会咬人。这三样我一个没落,下面直接给你能抄的。

二、准备三样东西

1. 服务端程序 + 游戏数据

来自 mirbeta/MirServer 仓库,最后一次提交停在 2022-08-30。里面是已经编译好的 .NET 程序,外加地图、脚本这些游戏数据。解压出来是八个目录:

DBServer/   LoginSrv/   Mir200/
LoginGate/  SelGate/    RunGate/
MakePlay/   CloudGate/

这八个目录要直接放进 data/,别再套一层 MirServer-main/。服务端是按固定路径找文件的,多一层就找不到,而且它不会给你明确的提示。

2. 配套的数据库脚本(最容易搞错的一步)

去 mirbeta/OpenMir2 仓库拿 Mir2.sql。这里有个大坑,我把结论放最前面:

别用现在 sql/ 目录里那三个 dump,要用 2022 年 9 月之前那份 Mir2.sql。

踩坑的原因值得多说两句。OpenMir2 在 2022-09-29 有过一次「修改数据库模型」的提交,把表名从 TBL_ACCOUNT 这种一刀切成了 account。而 MirServer 里那份预编译程序停在 2022-08,正好在这次改动之前。两代的表名和列名完全对不上——你把现在的 dump 导进去,服务端只会一直刷 Table 'Mir2.TBL_ACCOUNT' doesn't exist,改大小写也救不回来。

那要用哪份?打开 OpenMir2 的提交历史,翻到 c37ead03dd11faa4753fd1c6fb7562f9320a977f 这个提交,仓库根目录里就躺着 Mir2.sql(400616 字节)。我拿二进制反查过:它建出来的 16 张表,跟程序实际查询的表一一对应,列名也全部吻合。

3. 一个空文件夹

在 NAS 的共享文件夹里建 /volume1/docker/mir2server,把上面两样和后面几个文件都丢进去。

三、开始部署

第 1 步:改配置(一条命令)

样例配置里全是原作者的局域网 IP(10.10.0.x)和 Windows 路径(D:\MirServer\...)。不改,一定连不上。

把下面这段存成 fix-configs.sh,和 data/ 放同一层,然后执行(IP 换成你 NAS 的):

bash fix-configs.sh 192.168.1.10

脚本全文:

#!/usr/bin/env bash
# 用法: bash fix-configs.sh <你NAS的局域网IP>
set -euo pipefail
NAS_IP="${1:?用法: bash fix-configs.sh <你NAS的局域网IP,例如 192.168.1.10>}"
cd "$(dirname "$0")/data"

IP='s/10\.10\.0\.[0-9]\{1,3\}/127.0.0.1/g'
CONF='s#^ConnctionString=.*#ConnctionString=server=mysql;uid=root;pwd=mir2root;database=Mir2;#'
P1='s#D:\\MirServer#/mirserver#g'
P2='s#D:\\GameOfmir#/mirserver#g'
P3='/\/mirserver/ s#\\#/#g'

# 数据库服务
sed -i -e "$IP" -e "$CONF" -e "$P1" -e "$P2" -e "$P3" DBServer/dbsvr.conf
sed -i -e "$IP" DBServer/AddrTable.txt
sed -i "s/^.*$/${NAS_IP} ${NAS_IP} 27300/" DBServer/ServerInfo.txt

# 账号服务(真正读的是 config.conf,LogSrv.ini 是遗留文件,两个都改)
for f in LoginSrv/config.conf LoginSrv/LogSrv.ini; do
  sed -i -e "$IP" -e "$CONF" -e "$P1" -e "$P3" "$f"
done
printf '127.0.0.1\n' > LoginSrv/ServerAddr.txt
sed -i -e "s/10\.10\.0\.[0-9]\{1,3\}/${NAS_IP}/g" -e 's/:7100/:27200/g' LoginSrv/AddrTable.txt

# 登录网关
sed -i -e "$IP" -e 's/^GateAddr0=.*/GateAddr0=0.0.0.0/' -e 's/^GatePort0=.*/GatePort0=27100/' LoginGate/config.conf

# 角色网关(真正读的是 config.conf)
sed -i -e "$IP" -e 's/^ServerAddr0=.*/ServerAddr0=127.0.0.1/' -e 's/^ServerPort0=.*/ServerPort0=5100/' \
       -e 's/^GatePort0=.*/GatePort0=27200/' -e 's/^LsvServer=.*/LsvServer=127.0.0.1/' \
       -e 's#^ReadIPList=.*#ReadIPList=#' SelGate/config.conf
sed -i -e "$IP" -e 's/^GateAddr=.*/GateAddr=0.0.0.0/' -e 's/^GatePort=.*/GatePort=27200/' \
       -e 's#^ReadIPList=.*#ReadIPList=#' SelGate/Config.ini

# 游戏网关
sed -i -e "$IP" -e 's/^Count=.*/Count=1/' -e 's/^ServerAddr1=.*/ServerAddr1=127.0.0.1/' \
       -e 's/^GatePort1=.*/GatePort1=27300/' RunGate/config.conf

# 游戏主引擎
sed -i -e "$IP" -e "$CONF" Mir200/Server.conf
sed -i -e "$IP" -e "$CONF" Mir200/Server1.conf
sed -i "s/^0 .*/0 ${NAS_IP} ${NAS_IP} 27300/" Mir200/!servertable.txt
sed -i 's/^.*$/0.0.0.0/' Mir200/!runaddr.txt

# 压测工具(可选)
sed -i -e "s/\"Address\": *\"[^\"]*\"/\"Address\": \"${NAS_IP}\"/" -e 's/"Port": *[0-9]*/"Port": 27100/' MakePlay/AppSetting.json

echo "配置改写完成,NAS IP = ${NAS_IP}"

它干的就是四件事:把旧内网 IP 换成容器内部互通的 127.0.0.1、把需要告诉客户端的地址换成你 NAS 的 IP、把 Windows 路径换成 /mirserver/...、把三个对外端口设成 27100 / 27200 / 27300。

这里有个不起眼但很关键的文件——LoginSrv/ServerAddr.txt。它是账号服务的连接来源白名单,只有写在里面的地址才允许连它。原文件里是原作者那几台机器的内网 IP,而容器里各服务之间是从 127.0.0.1 连过去的,不改就会被判成「非法地址连接」反复踢掉。另外有两个服务真正读的是 config.conf 而不是 LogSrv.ini / Config.ini,脚本里两个都改了省心。

第 2 步:三个文件

先看 Dockerfile。它就干一件事:提供一个能跑 .NET 6 的干净环境。注意不要装任何 apt 包,原因踩坑那节会讲:

# 传奇2(OpenMir2 / MirServer)服务端运行环境
#
# 这个镜像本身只做一件事:提供一个能跑 .NET 6 的干净 Linux 环境 + 一个启动脚本。
# 服务端程序(DBServer、LoginSrv、Mir200…)由 docker-compose 从 ./data 挂载进来,
# 所以改配置、换版本都不用重新 build 镜像。

FROM mcr.microsoft.com/dotnet/runtime:6.0

ENV TZ=Asia/Shanghai \
    LANG=C.UTF-8 \
    DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

# 不装任何额外软件包:这个基础镜像是 Debian 11 的,已经 EOL,
# 安全仓库里的包全被下架,apt-get install 会 404 导致构建失败。
# 时区靠 compose 里挂载宿主机的 /etc/localtime 解决,够用了。
WORKDIR /mirserver

COPY start-all.sh /usr/local/bin/start-all.sh
RUN chmod +x /usr/local/bin/start-all.sh

# 客户端要连的三个端口:登录网关 / 角色网关 / 游戏网关
EXPOSE 27100 27200 27300

CMD ["/usr/local/bin/start-all.sh"]

然后是 start-all.sh,按顺序把六个进程拉起来:

#!/usr/bin/env bash
# 在同一个容器里按顺序拉起传奇2服务端的 6 个进程。
# 每个进程的日志落到 ./logs/<名称>.log,容器日志里会实时跟着滚动。
set -u

cd /mirserver
mkdir -p logs

# GameSvr 里写死了「Linux 下从 /opt/MirServer/Mir200 读配置」(它内部按操作系统
# 三选一:macOS 用 /Volumes/Data/MirServer、Linux 用 /opt/MirServer、Windows 用 D:/MirServer)。
# 我们的数据在 /mirserver,所以建个软链接把它对过去,否则 GameSvr 一启动就报
# DirectoryNotFoundException。
mkdir -p /opt
ln -snf /mirserver /opt/MirServer
echo "[link] /opt/MirServer -> /mirserver"

start() {
  local name="$1" dir="$2" dll="$3"
  if [ ! -f "$dir/$dll" ]; then
    echo "[skip]  $name —— 找不到 $dir/$dll"
    return
  fi
  echo "[start] $name  ($dir/$dll)"
  ( cd "$dir" && exec dotnet "$dll" ) > "logs/$name.log" 2>&1 &
}

# 启动顺序不能乱:数据库 → 账号 → 游戏引擎 → 三个网关
start DBSvr    DBServer  DBSvr.dll
sleep 3
start LoginSvr LoginSrv  LoginSvr.dll
sleep 2
start GameSvr  Mir200    GameSvr.dll
sleep 8
start LoginGate LoginGate LoginGate.dll
start SelGate   SelGate   SelGate.dll
start RunGate   RunGate   GameGate.dll

echo "---- 全部进程已拉起,Ctrl+C 或停止容器即退出 ----"

# 保持容器不退出,并把各服务日志合并输出到 docker logs
tail -n +1 -f logs/*.log

脚本开头那条软链接是必须的。GameSvr 内部按操作系统写死了数据目录——macOS 用 /Volumes/Data/MirServer、Linux 用 /opt/MirServer、Windows 用 D:/MirServer。我们的数据在 /mirserver,所以把它链接过去,否则引擎一启动就报 DirectoryNotFoundException。

最后是 docker-compose.yaml:

# 传奇2 服务端(OpenMir2 / MirServer)—— 绿联 NAS / 群晖 / 飞牛 通用
#
# NAS 上的存放路径:/volume1/docker/mir2server
#
# 目录结构:
#   /volume1/docker/mir2server/
#   ├── docker-compose.yml
#   ├── Dockerfile
#   ├── start-all.sh
#   ├── fix-configs.sh   ← 改配置用(把样例里的旧 IP / Windows 路径改掉)
#   ├── sql/             ← OpenMir2 仓库 sql/ 里的三个建库脚本
#   └── data/            ← MirServer 基础服务端解压出来的内容
#       ├── DBServer/    LoginSrv/    Mir200/
#       └── LoginGate/   SelGate/     RunGate/
#
# 对外只用三个不常用的端口:
#   27100  登录网关(客户端第一个连的)
#   27200  角色网关(选人界面)
#   27300  游戏网关(进游戏后)
# 数据库和内部服务端口都只在容器网络里,不对外暴露。

services:
  mysql:
    image: mysql:8.0
    container_name: mir2-mysql
    restart: unless-stopped
    # 直接跑 mysqld,跳过官方镜像那套「临时服务建库后关闭再重启」的入口脚本。
    # 在部分 NAS 上临时服务关闭会卡死,导致正式服务永远起不来。
    # 建库和 root 密码在上一次启动时已经完成,这里只需要正常启动即可。
    entrypoint: ["mysqld"]
    command:
      # 不能改成 --user=root:镜像的 my.cnf 里已经写了 user=mysql,
      # MySQL 对 user 参数是「先出现的生效」,命令行上的会被忽略。
      # 真正要解决的是「数据目录别放在共享文件夹里」——见下面的 named volume。
      - --user=mysql
      # 必须和服务端程序的预期一致:程序里查的是 TBL_ACCOUNT 这种大写表名,
      # 而 Linux 下 MySQL 默认区分表名大小写,不开这个会一直报表不存在。
      # 注意:这个参数只能在初始化数据目录时确定,中途改会启动失败。
      - --lower-case-table-names=1
      - --default-authentication-plugin=mysql_native_password
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_general_ci
    init: true
    environment:
      MYSQL_ROOT_PASSWORD: mir2root
      TZ: Asia/Shanghai
    volumes:
      # 用 Docker 卷而不是 ./mysql-data:绿联的共享文件夹有自己的 ACL,
      # 容器里的非 root 用户即便看到 777 也写不进去,mysqld 会报 errno 13。
      - mir2-mysql-data:/var/lib/mysql

  mir2:
    build: .
    container_name: mir2server
    restart: unless-stopped
    init: true
    depends_on:
      - mysql
    ports:
      - "27100:27100"   # 登录网关 LoginGate
      - "27200:27200"   # 角色网关 SelGate
      - "27300:27300"   # 游戏网关 RunGate / GameGate
    volumes:
      - ./data:/mirserver
      - /etc/localtime:/etc/localtime:ro
    environment:
      - TZ=Asia/Shanghai

# 数据库数据放在 Docker 自己的卷里,避开共享文件夹的 ACL。
# external: true 表示这个卷要事先用 docker volume create 建好。
volumes:
  mir2-mysql-data:
    external: true

# 这里故意不写 networks:交给 Docker 自动创建 mir2server_default 网络,
# 由它自己挑一个不冲突的网段,省得手动指定时撞上 NAS 上已有的网络。
# 两个服务在同一个自动网络里,所以连接串里的主机名写 mysql 就能解析。

三个地方值得单独说一句:

  • entrypoint: ["mysqld"] —— 绕过 MySQL 官方镜像的入口脚本。它在有些 NAS 上会卡在「关闭临时服务」那一步,正式服务永远起不来。
  • --lower-case-table-names=1 —— 必填。程序查的是大写表名,Linux 下 MySQL 默认区分表名大小写。这参数只能在初始化数据目录时定,中途改会启动失败。
  • 数据库用 Docker 卷而不是 ./mysql-data —— 原因见第四个坑。

第 3 步:建数据库卷并初始化

cd /volume1/docker/mir2server

docker volume create mir2-mysql-data

# 让卷目录归属容器里的 mysql 用户
docker run --rm -v mir2-mysql-data:/var/lib/mysql --entrypoint sh mysql:8.0 \
  -c 'chown -R mysql:mysql /var/lib/mysql'

# 初始化数据目录(lower-case-table-names 只能在这一步定)
docker run --rm -v mir2-mysql-data:/var/lib/mysql --entrypoint mysqld mysql:8.0 \
  --initialize-insecure --lower-case-table-names=1 \
  --default-authentication-plugin=mysql_native_password

第 4 步:启动、建库、导数据

docker compose -f docker-compose.yaml up -d
sleep 15

# 设 root 密码、建库
docker exec -i mir2-mysql mysql -uroot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'mir2root'; CREATE DATABASE IF NOT EXISTS Mir2 DEFAULT CHARSET utf8mb4;"

# 关键:让 root 能从其它容器连过来
docker exec -i mir2-mysql mysql -uroot -pmir2root -e "CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'mir2root'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;"

# 导入配套的那份脚本
docker exec -i mir2-mysql mysql -uroot -pmir2root Mir2 < sql/Mir2.sql

脚本里没有建库语句,库名得靠上面那条 CREATE DATABASE 建出来,而且必须叫 Mir2,和服务端连接串里的 database=Mir2 对上。

四、我踩过的坑,你多半也会踩

这一节是全文最值钱的部分。五个坑按我踩到的顺序写,每个都附上当时的原文报错。

坑 1:构建镜像就卡住了,tzdata 报 404

第一次 docker compose up 直接构建失败,报 tzdata 和 ca-certificates 两个包 404 Not Found。

查了才明白:dotnet/runtime:6.0 这个基础镜像底层是 Debian 11,已经走到生命周期尽头,安全仓库里的包全被下架了,apt-get install 必然扑空。

解法很简单——Dockerfile 里什么都别装。我一开始装 tzdata 纯属多余,容器时区靠 compose 挂载宿主机的 /etc/localtime 就够了。

坑 2:建网失败,Pool overlaps

容器起不来,报 invalid pool request: Pool overlaps with other one on this address space。

我在 compose 里手写了一个自定义网段,结果和 NAS 上已有的网络撞了。这类 NAS 上容器一多,地址池很容易重叠。

解法:compose 里干脆不写 networks:,交给 Docker 自己分配一个不冲突的网段。两个服务在同一个自动网络里,连接串直接写服务名 mysql 就能解析。

坑 3:MySQL 反复重启,errno 13 Permission denied

这个坑花的时间最久。容器一直在 Restarting,日志是 Failed to set datadir to '/var/lib/mysql/' (OS errno: 13 - Permission denied)。

诡异的地方在于:我进容器看,/var/lib/mysql 的权限明明是 drwxrwxrwx,属主也已经是 mysql。777 全开,它还是说进不去。

真相是绿联共享文件夹有一套自己的 ACL,容器里的非 root 用户(这里是 uid 999 的 mysql)访问不了,哪怕目录权限显示成 777 也一样。我写了个探针容器单独 touch 验证过,照样 Permission denied。而 MySQL 官方镜像默认降权成 mysql 用户运行,所以必然撞上。

解法:数据目录改用 Docker 卷(mir2-mysql-data)。Docker 卷存在自己的存储区,不受共享文件夹 ACL 管,uid 999 读写正常。

顺便说一句,别想着改成 --user=root 绕过去——镜像的 my.cnf 里已经写了 user=mysql,而 MySQL 对 user 参数是「先出现的生效」,命令行上传的会被直接忽略,日志里只留下淡淡一句 Ignoring user change to 'root'。我试了,白试。

坑 4:服务端说 Table 'Mir2.TBL_ACCOUNT' doesn't exist

数据库明明导进去了,账号服务却一口咬定表不存在。

原因就是第二节第 2 条讲的:数据包和程序不是同一代。换成 2022 年 9 月之前那份 Mir2.sql 就好。

坑 5:GameSvr 找不到 /opt/MirServer/Mir200/Server.conf

引擎启动即崩,栈里是 DirectoryNotFoundException: Could not find a part of the path '/opt/MirServer/Mir200/Server.conf'。

程序内部按操作系统写死了数据目录,Linux 分支指向 /opt/MirServer,和我们的 /mirserver 对不上。解法就是启动脚本里那条 ln -snf /mirserver /opt/MirServer。

这里还埋着个小坑:start-all.sh 是在构建镜像时 COPY 进去的,所以改完必须重新构建(docker compose up -d --build),只 docker restart 跑的还是镜像里那份旧的。我第一次就栽在这——改了脚本、重启服务,报错一模一样,盯着屏幕查了半天才发现。

五、怎么确认部署成功

别看日志刷得热闹,按下面六条逐项对,全中才算真的跑起来了。

两台容器与三个对外端口

第 1 条:两个容器都是 Up

docker compose -f docker-compose.yaml ps

mir2-mysql 和 mir2server 都必须是 Up。如果 mysql 显示 Restarting,直接回去看坑 3。

第 2 条:三个对外端口都在监听

ss -lntp | grep -E '27100|27200|27300'

应该看到 27100、27200、27300 三行 LISTEN。

第 3 条:数据库里表数对得上

docker exec -i mir2-mysql mysql -uroot -pmir2root -e "SHOW VARIABLES LIKE 'lower_case_table_names'; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='Mir2'; SELECT COUNT(*) FROM Mir2.TBL_STDITEMS;" 

三个数字:lower_case_table_names = 1,表数 = 16,物品数 = 1000。第三条我故意用的大写 TBL_STDITEMS,能查出 1000 就说明大小写兼容真的生效了。

第 4 条:游戏数据全部载入

GameSvr 加载日志全绿

grep -E "加载成功|引擎" data/logs/GameSvr.log | head -20

看到这些才算数:物品 1000、地图 290、怪物 705、技能 206、刷怪点 2386,最后是「人物数据引擎启动成功」和「游戏处理引擎初始化成功」。

第 5 条:六个进程之间链路全通

echo "=== DBSvr ==="; tail -6 data/logs/DBSvr.log
echo "=== LoginSvr ==="; tail -6 data/logs/LoginSvr.log
echo "=== GameSvr ==="; tail -6 data/logs/GameSvr.log

关键几行是「账号服务器[127.0.0.1:5600]链接成功」「登录服务器[127.0.0.1:5600]连接成功」「数据库服务器[127.0.0.1:6000]连接成功」「游戏网关已打开」。中间不应该出现反复的「重新链接 + 断开链接」。

第 6 条:换台机器也能连上端口

在同一个局域网的另一台电脑上试(Windows 用 PowerShell):

Test-NetConnection 192.168.1.10 -Port 27100

看 TcpTestSucceeded 是不是 True。这是确认「端口对外可达」的最后一步。

这里顺便说清楚一件事:27100 / 27200 / 27300 是游戏客户端的 TCP 端口,跑的是传奇2 的私有协议,不是 HTTP。所以拿浏览器打不开是正常的,别以为服务端没起来。另外这套服务端也没有 Web 管理后台。

六、日常运维命令

cd /volume1/docker/mir2server

docker compose -f docker-compose.yaml ps          # 看状态
docker logs mir2server --tail 50                  # 容器日志(含各服务)
tail -50 data/logs/GameSvr.log                    # 单看某个服务

docker restart mir2server                         # 重启服务端
docker compose -f docker-compose.yaml down        # 全部停止(不动数据卷)
docker compose -f docker-compose.yaml up -d --build   # 改过 start-all.sh 后用这个

# 彻底重来(慎用:会清空所有账号和角色数据)
docker compose -f docker-compose.yaml down
docker volume rm mir2-mysql-data

七、最后几句实话,还有一个小预告

到这一步,你 NAS 上已经躺着一个能跑的传奇2 服务端了:六个进程、三个对外端口、数据库和账号服务全部就位。

不过丑话得说在前面——这不是一套开箱即用的东西。它没有 Web 管理后台,想调经验倍率、掉率、刷怪,只能直接改数据库或配置文件;日志里还会刷一批「地图文件未找到」和城堡相关的报错,那是上游数据包本身就缺文件,不影响开服,别被吓到。攻城玩法因为缺怪物定义,是用不了的。

还有个事实得提前告诉你:服务端跑起来,不等于能进游戏。进游戏还得配一个 Windows 客户端,而官方那个客户端仓库只放了 Delphi 源码,没有编译好的程序——这一环我还在啃,等啃通了单独写一篇。

预告一下:下一篇是客户端部署——怎么把客户端跑起来、IP 和端口填在哪里、账号怎么注册、连不上怎么一步步排查,都会写清楚。你这篇能跟着做下来,说明最难的关已经过了,客户端那篇会轻松很多。顺手收藏一下本站、关注一下我们,下一篇出来就不会错过。你的支持是我继续折腾下去的最大动力,谢啦。

八、参考资料

  • 服务端程序与游戏数据:github.com/mirbeta/MirServer
  • 服务端源码(.NET):github.com/mirbeta/OpenMir2
  • 配套数据库脚本:OpenMir2 仓库提交 c37ead03dd11faa4753fd1c6fb7562f9320a977f 下的 Mir2.sql
  • 客户端源码(Delphi):github.com/mirbeta/MirClient

本文所有命令和输出都来自一次真实部署,文中那份配置改写脚本我用完整的 25 条断言验证过,和实际跑通的版本产出逐字节一致。哪一步卡住了,回头对着第四节那五个坑看,基本都能对上。

消息盒子

# 暂无消息 #

只显示最新10条未读和已读信息