Files
AIC-Project/docs/superpowers/plans/2026-07-06-dev-server-config.md
2026-07-10 10:24:29 +08:00

519 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 探测 IP 可配置化 + 移除组播 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 删除实测无效的组播发现代码;开发机探测 IP 从代码写死改为设备本地配置 `persistentDataPath/dev_server.txt`,底包发现失败时弹 IMGUI 输入框录入并验证后持久化。
**Architecture:** 三个小单元:`DevServerConfig`(Base,dev_server.txt 读写)、`DevServerPrompt`(Base,纯 IMGUI 三态输入框)、`LoadDll` 协程编排两轮发现+弹框循环。`LocalCdnDiscovery.Discover``extraProbeIp` 参数(后台线程不能碰 persistentDataPath,配置由主线程读好传入);热更层 `DevServerDiscovery` 内联同语义配置读取(XWorld.Link 未引用 Base 程序集,沿用"内联+两边同步"惯例)。
**Tech Stack:** C#/.NET(netstandard2.1 共享协议)、Unity 客户端(IMGUI)、xUnit 服务端测试。
**Spec:** `docs/superpowers/specs/2026-07-06-dev-server-config-design.md`
**注意:** 项目根 `.git` 是空目录、非有效 git 仓库,省略 commit 步骤,以"运行测试/目视核对"作为每任务收尾。
**关键背景(执行者必读):**
- 前一个特性(组播叠加,plan `2026-07-06-multicast-discovery.md`)已实施但实测跨网段不通,本计划将其代码**全部删除**。删除时以"还原并改进"为准——不是逐字回滚,注释要更新为新语义(配置文件而非写死 IP)。
- `Server\Framework.Shared\Framework.Shared.csproj` 直接编译 `Client\Assets\Framework\Shared\**\*.cs`,共享协议常量改一处两边生效。**删 `DevDiscovery.MulticastGroup` 前必须先删掉它的最后一个使用点(`DevServerDiscovery.cs`),否则 Unity 编译会断**——任务顺序已按此安排,勿调换。
- `Client\Assets\Base\**` 是 AOT 底包程序集(Base.asmdef),不能引用热更程序集;`Client\Assets\Script\**` 是热更程序集(XWorld.Link.asmdef),其引用列表**没有 Base**,所以热更层也引用不到 Base 的新类。
- `Application.persistentDataPath` 只能在 Unity 主线程访问;两个发现器都跑在后台线程,配置读取必须在主线程完成(参数传入/构造函数存字段)。
- 代码注释用中文、全角标点;网络失败静默吞掉是本模块的既定风格。
- 新建 .cs 文件无需手写 .meta,Unity 编辑器打开时自动生成。
- 测试命令在仓库根执行,路径含 `#`,bash 中必须加引号:`cd "/d/UD/AI/AIC#Project"`
---
### Task 1: 删除服务端组播代码与测试
**Files:**
- Modify: `Server\Gateway\DevDiscoveryResponder.cs`
- Modify: `Server\Gateway.Tests\DevDiscoveryResponderTests.cs`
(共享常量 `DevDiscovery.MulticastGroup` 此任务**不删**——客户端 `DevServerDiscovery.cs` 还在用,Task 4 收尾时删。)
- [ ] **Step 1: 删响应器的组播 join**
`Server\Gateway\DevDiscoveryResponder.cs` 三处:
1. 删文件头 `using System.Net.NetworkInformation;`
2. 删构造函数中这一行:
```csharp
JoinMulticastOnAllNics(_udp, IPAddress.Parse(DevDiscovery.MulticastGroup));
```
3. 删整个 `JoinMulticastOnAllNics` 私有静态方法(含其上三行中文注释,从 `// 逐块 Up 状态非回环 IPv4 网卡加入组播组` 到方法闭括号)。
删后构造函数应恢复为:
```csharp
_udp = new UdpClient(AddressFamily.InterNetwork);
_udp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_udp.Client.Bind(new IPEndPoint(IPAddress.Any, listenPort));
Port = ((IPEndPoint)_udp.Client.LocalEndPoint).Port;
}
public void Start()
```
- [ ] **Step 2: 删组播测试**
`Server\Gateway.Tests\DevDiscoveryResponderTests.cs`:删整个 `MulticastQuery_ReceivesReply` 测试方法(`[Fact]` 到方法闭括号)。原有 3 条测试(`ValidQuery_ReceivesReply_WithAdvertisedAddresses``WrongToken_NoReply``GarbagePacket_Ignored_LoopStillAnswers`)与 `QueryAsync` 辅助方法不动。
- [ ] **Step 3: 跑 Gateway 测试确认绿**
```bash
cd "/d/UD/AI/AIC#Project" && dotnet test Server/Gateway.Tests
```
预期:全部通过,总数比之前少 1(42 条)。
---
### Task 2: 新建 DevServerConfig
**Files:**
- Create: `Client\Assets\Base\DevServerConfig.cs`
无自动化测试(Unity 客户端代码);正确性靠 Task 7 编辑器/真机验收。
- [ ] **Step 1: 写文件**
完整内容:
```csharp
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
// 内网联调配置:开发机探测 IP 的本地持久化(persistentDataPath/dev_server.txt,单行 IPv4)。
// 跨网段时广播/组播都出不了网段(路由器不转发),只能向已知开发机 IP 单播——该 IP 由本文件持久化,
// 首次由 DevServerPrompt 弹框录入并验证成功后写入(见 LoadDll.CoLocalDiscoverThenDownload)。
// 注意:Application.persistentDataPath 只能在主线程访问,故 Load/Save 只能在主线程调;
// 发现线程(后台)需要的值由调用方在主线程读好传入。
// 热更层 DevServerDiscovery 内联了同语义的读取(XWorld.Link 未引用 Base 程序集),改格式时两边须同步。
public static class DevServerConfig
{
public static string FilePath => Path.Combine(Application.persistentDataPath, "dev_server.txt");
// 读配置:文件不存在/IO 异常/内容不是合法 IPv4 → null(静默,等同"无配置")。
public static string Load()
{
try
{
string path = FilePath;
if (!File.Exists(path)) return null;
string line = File.ReadAllText(path).Trim();
if (line.Length == 0) return null;
if (!IPAddress.TryParse(line, out IPAddress ip)) return null;
if (ip.AddressFamily != AddressFamily.InterNetwork) return null; // 只认 IPv4
return ip.ToString();
}
catch (Exception)
{
return null;
}
}
// 写配置:单行覆盖;IO 异常静默吞(写不进只是下次还弹框,不致命)。
public static void Save(string ip)
{
try { File.WriteAllText(FilePath, ip ?? ""); }
catch (Exception) { }
}
}
```
- [ ] **Step 2: 目视核对**
所有类型(`Path`/`File`/`IPAddress`/`AddressFamily`/`Application`)均被文件顶部 using 覆盖;类在全局命名空间(与 `LocalCdnDiscovery` 一致)。
---
### Task 3: LocalCdnDiscovery 删组播、加 extraProbeIp 参数
**Files:**
- Modify: `Client\Assets\Base\LocalCdnDiscovery.cs`
- [ ] **Step 1: 删内联组播常量**
删这一行(const 块中):
```csharp
private const string MulticastGroup = "239.192.48.92"; // = DevDiscovery.MulticastGroup
```
- [ ] **Step 2: 删 DevExtraProbeIps 整块**
删字段及其上方全部 4 行注释(从 `// 跨网段内网联调用:``private static readonly string[] DevExtraProbeIps = { "192.168.130.40" };`)。
- [ ] **Step 3: 改 Discover 签名与方法体**
方法头注释与签名改为:
```csharp
// 阻塞式发现,须在后台线程调用(会最多阻塞 durationMs)。
// 在窗口内周期重发查询(向广播 + 127.0.0.1 单播),收到首个合法应答即返回其 ResourceBaseUrl(CDN 根);超时返回 null。
// extraProbeIp:额外单播探测目标(跨网段联调用,来自 dev_server.txt 或弹框输入,见 DevServerConfig/DevServerPrompt)。
// 广播/定向广播不被路由器转发(RFC 2644),组播亦已实测不通(路由器未开组播路由),跨网段只能靠单播已知 IP。
// 注意:本方法跑在后台线程,不能碰 Unity 主线程 API(如 persistentDataPath),配置由调用方在主线程读好传入。
public static string Discover(string token, int durationMs = 1500, string extraProbeIp = null)
```
方法体内四处改动:
1. 删 TTL 设置(共 3 行,`ReceiveTimeout = 250;` 之后):
```csharp
// 组播默认 TTL=1 出不了本网段;8 覆盖内网多级路由。单独 try/catch:个别设备设不上 TTL 时
// 仍能发 TTL=1 组播 + 全部原有路径,不能让它杀掉整个发现线程。
try { udp.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 8); } catch { }
```
2. targets 构造:把原 `foreach (string s in DevExtraProbeIps) ...` 两行替换为:
```csharp
if (!string.IsNullOrEmpty(extraProbeIp) && IPAddress.TryParse(extraProbeIp, out IPAddress extra))
targetList.Add(new IPEndPoint(extra, Port));
```
3. 删组播端点/网卡列表构建及日志(共 5 行,targets 的 Debug.Log 之后):
```csharp
// 组播端点不进 targets:发组播走哪块网卡由 IP_MULTICAST_IF 决定(默认只走默认路由网卡),
// 故须逐网卡切换后各发一次,覆盖编辑器/PC 多网卡;手机一般单网卡。
IPEndPoint multicastEp = new IPEndPoint(IPAddress.Parse(MulticastGroup), Port);
List<IPAddress> multicastNics = GetLocalIPv4Addresses();
UnityEngine.Debug.Log("[Discovery] multicast=" + multicastEp + " via " + multicastNics.Count + " nic(s)");
```
4. 删重发块内的逐网卡组播发送与零网卡兜底(targets foreach 之后、`nextSend = ...` 之前的 13 行):
```csharp
foreach (IPAddress nicAddr in multicastNics)
{
try
{
udp.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, nicAddr.GetAddressBytes());
udp.Send(query, query.Length, multicastEp);
}
catch { }
}
if (multicastNics.Count == 0)
{
try { udp.Send(query, query.Length, multicastEp); } catch { } // 枚举不到网卡时按默认网卡发
}
```
- [ ] **Step 4: 更新 catch 注释、删辅助方法**
1. `catch (Exception)` 内注释改为:
```csharp
// 忽略:发现失败由调用方处理(弹框输入或回退 127.0.0.1)
```
2. 删整个 `GetLocalIPv4Addresses()` 方法(含其上一行注释 `// 枚举本机所有 Up 状态非回环网卡的 IPv4 地址,供逐网卡发组播用。`)。`GetDirectedBroadcasts()``IsPrivateV4`、编解码方法全部不动。
- [ ] **Step 5: 目视核对**
全文不再出现 `Multicast`/`multicast`/`DevExtraProbeIps`/`192.168.130.40`;大括号配平;`extraProbeIp` 是唯一新增参数且带默认值(既有调用 `Discover("xw-dev", 1500)` 不受影响)。
---
### Task 4: DevServerDiscovery 删组播、内联配置读取;删共享组播常量
**Files:**
- Modify: `Client\Assets\Script\xmain\MiniGame\DevServerDiscovery.cs`
- Modify: `Client\Assets\Framework\Shared\Protocol\DevDiscovery.cs`
- [ ] **Step 1: 替换 DevExtraProbeIps 为配置字段**
删字段及其上方 4 行注释(从 `// 跨网段内网联调用:``private static readonly string[] DevExtraProbeIps = { "192.168.130.40" };`)。
字段区加一行(`private readonly string _token;` 之后):
```csharp
private readonly string _extraProbeIp;
```
构造函数改为:
```csharp
public DevServerDiscovery(string token)
{
_token = token;
// 跨网段联调:广播/定向广播不被路由器转发(RFC 2644),组播亦实测不通(路由器未开组播路由),
// 唯一可靠路径是向已知开发机 IP 单播。IP 来自 persistentDataPath/dev_server.txt——底包首启弹框
// 录入(见 Base 层 DevServerConfig/DevServerPrompt)。persistentDataPath 不能在后台线程访问,
// 故在主线程构造时读好存字段,Run(后台线程)只用字段。
_extraProbeIp = ReadConfiguredProbeIp();
}
```
- [ ] **Step 2: 内联配置读取方法**
`Stop()` 方法之后、类闭括号之前新增:
```csharp
// 读 dev_server.txt 首行并校验为 IPv4,失败一律 null。与 Base 层 DevServerConfig.Load() 语义一致——
// XWorld.Link 未引用 Base 程序集,按本项目"内联+两边同步"惯例复制最小实现,改格式时两边须同步。
private static string ReadConfiguredProbeIp()
{
try
{
string path = System.IO.Path.Combine(Application.persistentDataPath, "dev_server.txt");
if (!System.IO.File.Exists(path)) return null;
string line = System.IO.File.ReadAllText(path).Trim();
if (line.Length == 0) return null;
if (!IPAddress.TryParse(line, out IPAddress ip)) return null;
if (ip.AddressFamily != AddressFamily.InterNetwork) return null; // 只认 IPv4
return ip.ToString();
}
catch (Exception)
{
return null;
}
}
```
(`System.IO` 用全限定,避免新增 using;`Application`/`IPAddress`/`AddressFamily`/`Exception` 均已有 using 覆盖。)
- [ ] **Step 3: Run() 删组播、改 targets**
1. 删 TTL 设置(共 3 行,`ReceiveTimeout = 250;` 之后):
```csharp
// 组播默认 TTL=1 出不了本网段;8 覆盖内网多级路由。单独 try/catch:个别设备设不上 TTL 时
// 仍能发 TTL=1 组播 + 全部原有路径,不能让它杀掉整个发现线程。
try { udp.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 8); } catch { }
```
2. targets 构造:把原 `foreach (string s in DevExtraProbeIps) ...` 两行替换为:
```csharp
if (!string.IsNullOrEmpty(_extraProbeIp) && IPAddress.TryParse(_extraProbeIp, out IPAddress extra))
targetList.Add(new IPEndPoint(extra, DevDiscovery.Port));
```
3. 删组播端点/网卡列表构建及日志(targets 的 Debug.Log 之后共 5 行,内容同 Task 3 Step 3 第 3 点,仅 `DevDiscovery.MulticastGroup`/`DevDiscovery.Port`/`[DevTest]` 之别)。
4. 删重发块内逐网卡组播发送与零网卡兜底(13 行,内容同 Task 3 Step 3 第 4 点)。
5. 删整个 `GetLocalIPv4Addresses()` 方法(含其上一行注释)。
- [ ] **Step 4: 删共享协议组播常量**
`Client\Assets\Framework\Shared\Protocol\DevDiscovery.cs`:删 `MulticastGroup` 常量及其上两行注释:
```csharp
// 组播发现组:组织本地范围(RFC 2365, 239.192.0.0/14),配合发送端 TTL>1 可跨内网多级路由;
// 能否真正跨网段取决于路由器是否开启组播路由(PIM/IGMP Proxy)。
public const string MulticastGroup = "239.192.48.92";
```
删后 `DevDiscovery` 类只剩 `Port`/`ProtoVersion`/`Magic`/`TypeQuery`/`TypeReply`
- [ ] **Step 5: 全仓库确认无残留引用**
```bash
cd "/d/UD/AI/AIC#Project" && grep -rn "MulticastGroup\|239.192.48.92" --include="*.cs" Client/Assets/Base Client/Assets/Script Client/Assets/Framework Server/Gateway Server/Gateway.Tests
```
预期:无输出(docs/ 里的历史记录不算)。
- [ ] **Step 6: 服务端全量回归**
```bash
cd "/d/UD/AI/AIC#Project" && dotnet test Server/Server.sln
```
预期:全部通过(172 条:比组播版少 1 条测试)。
---
### Task 5: 新建 DevServerPrompt
**Files:**
- Create: `Client\Assets\Base\DevServerPrompt.cs`
- [ ] **Step 1: 写文件**
完整内容:
```csharp
using UnityEngine;
// 内网联调:发现失败时的开发机 IP 输入框(纯 IMGUI,风格同热更层 DevServerSwitchUI)。
// 只负责输入与三态状态机,不做网络/文件 IO——探测与写配置由 LoadDll.CoLocalDiscoverThenDownload 编排:
// [确定] → Confirmed,外部拿 InputIp 去探测;失败则外部调 ResetToWaiting(错误文案) 拨回弹框;
// [跳过] → Skipped,外部走 127.0.0.1 回退。
public sealed class DevServerPrompt : MonoBehaviour
{
public enum PromptState { Waiting, Confirmed, Skipped }
public PromptState State { get; private set; } = PromptState.Waiting;
public string InputIp = "";
public string Error = ""; // 非空则红字显示
// 外部探测失败后调用:显示错误并回到可输入状态。
public void ResetToWaiting(string error)
{
Error = error ?? "";
State = PromptState.Waiting;
}
private void OnGUI()
{
if (State != PromptState.Waiting) return;
const int W = 560, H = 190;
GUILayout.BeginArea(new Rect((Screen.width - W) / 2, (Screen.height - H) / 3, W, H), GUI.skin.box);
GUILayout.Label("内网发现失败:请输入开发机 IP(网关须 --lan --devToken xw-dev");
InputIp = GUILayout.TextField(InputIp ?? "", GUILayout.Height(44));
if (!string.IsNullOrEmpty(Error))
{
GUI.color = new Color(1f, 0.4f, 0.3f);
GUILayout.Label(Error);
GUI.color = Color.white;
}
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
if (GUILayout.Button("确定", GUILayout.Height(44))) { Error = ""; State = PromptState.Confirmed; }
if (GUILayout.Button("跳过", GUILayout.Height(44), GUILayout.Width(120))) State = PromptState.Skipped;
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
}
```
- [ ] **Step 2: 目视核对**
仅依赖 UnityEngine;无网络/文件 IO;`State` 外部只读(private set),状态回拨只经 `ResetToWaiting`
---
### Task 6: LoadDll 流程重写
**Files:**
- Modify: `Client\Assets\Base\LoadDll.cs`(`CoLocalDiscoverThenDownload` 方法,约 191-220 行,`#if !UNITY_WEBGL` 块内)
- [ ] **Step 1: 整体替换 CoLocalDiscoverThenDownload**
把方法(含其上两行注释)整体替换为:
```csharp
//内网热更:先 UDP 广播发现开发机 CDN(拿其 LAN IP),成功则改用它;失败弹输入框让开发者输入开发机 IP,
//探测成功写入 persistentDataPath/dev_server.txt(下次启动第 1 轮直接命中不再弹);[跳过]回退 127.0.0.1(真机需 adb reverse tcp:15081)。
//token 须与热更层 GlobalData.LanDiscoveryToken 及服务端 Gateway.Runner --devToken 一致(均为 "xw-dev")。
IEnumerator CoLocalDiscoverThenDownload()
{
Debug.Log($"[LoadDll] CoLocalDiscoverThenDownload begin");
//第 1 轮:广播/loopback/定向广播 + dev_server.txt 已存 IP(若有)。
//Discover 会阻塞(UDP 收发+超时),放后台线程跑,协程逐帧等它结束,避免卡加载界面;
//persistentDataPath 不能在后台线程访问,配置在主线程先读好传入。
string cdnRoot = null;
string savedIp = DevServerConfig.Load();
{
Thread th = new Thread(() => { cdnRoot = LocalCdnDiscovery.Discover("xw-dev", 1500, savedIp); }) { IsBackground = true };
th.Start();
while (th.IsAlive)
yield return null;
}
//第 1 轮失败 → 弹输入框(预填旧值,开发机换 IP 时可直接改);输入 IP 验证探测成功才写配置。
if (string.IsNullOrEmpty(cdnRoot))
{
DevServerPrompt prompt = gameObject.AddComponent<DevServerPrompt>();
prompt.InputIp = savedIp ?? "";
while (string.IsNullOrEmpty(cdnRoot))
{
while (prompt.State == DevServerPrompt.PromptState.Waiting)
yield return null;
if (prompt.State == DevServerPrompt.PromptState.Skipped)
break;
//Confirmed:先主线程校验格式,非法不发起探测直接回弹框。
string inputIp = (prompt.InputIp ?? "").Trim();
if (!System.Net.IPAddress.TryParse(inputIp, out _))
{
prompt.ResetToWaiting($"IP 格式不对:{inputIp}");
continue;
}
Thread th = new Thread(() => { cdnRoot = LocalCdnDiscovery.Discover("xw-dev", 1500, inputIp); }) { IsBackground = true };
th.Start();
while (th.IsAlive)
yield return null;
if (string.IsNullOrEmpty(cdnRoot))
{
prompt.ResetToWaiting($"探测失败:连不到 {inputIp}:48923(网关起了吗?防火墙放行 UDP 48923 了吗?)");
continue;
}
DevServerConfig.Save(inputIp);
Debug.Log($"[LoadDll] dev server ip saved: {inputIp}");
}
Destroy(prompt);
}
if (!string.IsNullOrEmpty(cdnRoot))
{
if (!cdnRoot.EndsWith("/"))
cdnRoot += "/";
//发现回复的 ResourceBaseUrl 是 CDN 根(http://<LAN-IP>:15081/)DLL 热更在其下的 XWorld/<platform>/。
UpdateServer = $"{cdnRoot}XWorld/{sPlatform}/";
Debug.Log($"[LoadDll] LAN CDN discovered: {UpdateServer}");
}
else
{
UpdateServer = $"http://127.0.0.1:15081/XWorld/{sPlatform}/";
Debug.LogWarning($"[LoadDll] LAN CDN discovery failed/skipped, fallback {UpdateServer} (真机需 adb reverse tcp:15081 tcp:15081,且开发机网关须 --lan --devToken xw-dev)");
}
yield return DownLoadAssets(this.StartGame);
}
```
要点:
- `Thread` 已有 using(原方法就用);`System.Net.IPAddress` 全限定,不新增 using。
- 探测成功但来源无法区分(广播/文件 IP),故第 1 轮成功**不**写文件——只有弹框输入并验证成功才 `Save`(spec 行为规格)。
- `Destroy(prompt)` 在跳出循环后统一执行(成功或跳过都销毁)。
- [ ] **Step 2: 目视核对**
方法内所有 `yield return` 都在协程主体(不在 lambda 内);`cdnRoot` 由后台线程赋值、主线程在 `th.IsAlive == false` 后才读(与原代码同模式);两处 `Thread` 变量分别在独立作用域(第 1 轮包在 `{}` 块里,无重名冲突)。
---
### Task 7: 端到端验证
**Files:** 无代码改动,纯验证。
- [ ] **Step 1: 服务端全量测试**
```bash
cd "/d/UD/AI/AIC#Project" && dotnet test Server/Server.sln
```
预期:172 条全部通过。
- [ ] **Step 2: 残留扫描**
```bash
cd "/d/UD/AI/AIC#Project" && grep -rn "Multicast\|239.192.48.92\|192.168.130.40\|DevExtraProbeIps\|GetLocalIPv4Addresses" --include="*.cs" Client/Assets/Base Client/Assets/Script Client/Assets/Framework Server/Gateway Server/Gateway.Tests
```
预期:无输出。
- [ ] **Step 3: 编辑器同机自测(需用户配合)**
起网关(`--lan --devToken xw-dev`)+ 编辑器运行:loopback 一轮即中,弹框不出现,`[LoadDll] LAN CDN discovered` 正常——与改前行为一致。
- [ ] **Step 4: 真机验收(需用户配合,重打 APK——Base 是 AOT 底包,DLL 热更交付不了)**
按 spec 验收标准:
1. 跨网段首启(先清 App 数据或删 dev_server.txt):第 1 轮失败 → 弹框 → 输入 192.168.130.40 → 探测成功 → 正常加载
2. 重启:不弹框,第 1 轮直接命中(日志 targets 里含 192.168.130.40)
3. [跳过]:回退 127.0.0.1,与现状一致
4. (可选)改 dev_server.txt 为错误 IP 模拟开发机换 IP:弹框预填旧值可改