release: bump version to 0.57.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled

This commit is contained in:
rayd1o
2026-05-14 01:02:17 +08:00
parent f14ff6ec0f
commit dd176a6ae6
18 changed files with 541 additions and 68 deletions

View File

@@ -275,38 +275,24 @@ curl http://localhost:8000/health
在常见的 WSL2 开发环境下Windows 通常可以直接通过 `localhost` 访问 WSL 中的服务。 在常见的 WSL2 开发环境下Windows 通常可以直接通过 `localhost` 访问 WSL 中的服务。
### 4. 如果需要让局域网设备访问,再做 Windows 端口转发 ### 4. 如果需要让局域网设备访问,使用临时 Windows relay
注意:下面的命令必须在“以管理员身份运行”的 PowerShell 中执行 `./planet.sh start --allow-lan` 在 WSL 下会优先使用临时 Windows relayWSL 和 Windows 本机 `localhost` 仍使用 `3000` / `8000`,局域网设备访问 Windows 局域网 IP 的 `3000` / `8000` 时由 relay 转发。relay 由脚本启动和停止;如果 WSL 目标端口断开relay 会自动退出并释放 Windows 局域网监听端口
先把 Windows 对外网卡上的 `3000` / `8000` 转发到 Windows 本机 `127.0.0.1` 如果以前手动配置过持久 `portproxy`,脚本会在启动前检测 `3000` / `8000` 残留规则,并请求管理员 PowerShell 自动删除。若自动请求被取消,可以手动清理,避免 `iphlpsvc` 继续占用 `3000` / `8000`
```powershell ```powershell
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=3000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=3000
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=8000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=8000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=3000 connectaddress=127.0.0.1 connectport=3000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8000 connectaddress=127.0.0.1 connectport=8000
``` ```
再放行 Windows 防火墙: 脚本会检测 Windows 防火墙是否已放行 `3000` / `8000`。如果缺少规则,会触发一次 Windows UAC 管理员 PowerShell 请求来自动创建。若自动请求被取消,也可以手动执行
```powershell ```powershell
New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000 New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000
New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000 New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000
``` ```
检查转发规则是否生效:
```powershell
netsh interface portproxy show all
```
预期能看到:
- `0.0.0.0:3000 -> 127.0.0.1:3000`
- `0.0.0.0:8000 -> 127.0.0.1:8000`
### 5. 查 Windows 局域网 IP并让其他设备访问 ### 5. 查 Windows 局域网 IP并让其他设备访问
在 Windows PowerShell 中执行: 在 Windows PowerShell 中执行:
@@ -329,7 +315,7 @@ ipconfig
### 6. 常见现象与判断 ### 6. 常见现象与判断
- WSL 中 `curl localhost:3000` 能通,但 Windows 访问 `WSL 的局域网 IP:3000` 不通:这是正常现象之一,优先验证 Windows 的 `localhost:3000` - WSL 中 `curl localhost:3000` 能通,但 Windows 访问 `WSL 的局域网 IP:3000` 不通:这是正常现象之一,优先验证 Windows 的 `localhost:3000`
- Windows `localhost:3000` 能通,但局域网设备访问 `Windows 局域网 IP:3000` 不通:通常缺少 `portproxy` 或防火墙放行 - Windows `localhost:3000` 能通,但局域网设备访问 `Windows 局域网 IP:3000` 不通:通常是 Windows 防火墙、网络配置或旧 `portproxy` 残留
- `whoami /groups``S-1-5-32-544` 显示 `deny only`:说明当前 PowerShell 不是提权管理员窗口 - `whoami /groups``S-1-5-32-544` 显示 `deny only`:说明当前 PowerShell 不是提权管理员窗口
### 7. 本项目一次性验证顺序 ### 7. 本项目一次性验证顺序
@@ -340,8 +326,8 @@ ipconfig
2. WSL 中执行 `curl http://localhost:8000/health` 2. WSL 中执行 `curl http://localhost:8000/health`
3. Windows 中执行 `curl http://localhost:3000` 3. Windows 中执行 `curl http://localhost:3000`
4. Windows 中执行 `curl http://localhost:8000/health` 4. Windows 中执行 `curl http://localhost:8000/health`
5. 管理员 PowerShell 配置 `portproxy` 和防火墙 5. 按脚本提示完成 Windows 防火墙 UAC 请求;如果曾手动配置 `portproxy`,先清理旧规则
6. 用手机或其他电脑访问 `http://<Windows局域网IP>:3000/earth` 6. 用手机或其他电脑访问 Windows 对外端口,例如 `http://<Windows局域网IP>:3000/earth`
## 启动容错参数 ## 启动容错参数

View File

@@ -1 +1 @@
0.56.0 0.57.0

View File

@@ -8,6 +8,22 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`bugfix + 小功能混合) - `improvement` -> `+0.0.1`bugfix + 小功能混合)
- `bugfix` -> `+0.0.1` - `bugfix` -> `+0.0.1`
## [0.57.0] — 2026-05-14
Released: 2026-05-14
### Highlights
- 新增 WSL `--allow-lan` 临时 Windows relay保持本机 `localhost:3000` / `localhost:8000` 不变,同时用 Windows 局域网 IP 暴露相同端口。
- 启动脚本会检测旧 `netsh interface portproxy` 冲突并请求管理员 PowerShell 清理,避免 `svchost.exe / iphlpsvc` 持久占用 3000/8000。
- 修复 Vite CJS Node API deprecated warning将前端配置迁移到 ESM并让脚本按实际后端端口注入代理目标。
### Added / Fixed / Improved
- 新增 [scripts/windows-lan-relay.ps1](/home/ray/dev/linkong/planet/scripts/windows-lan-relay.ps1),在 Windows 侧启动随 WSL 服务健康状态自动退出的 TCP relay。
- `planet.sh --allow-lan` 仅在 WSL + PowerShell 可用时启用 Windows relay并自动检查 Windows 防火墙规则;非 WSL 环境保持原有路径。
- 更新中英文 README、FAQ 和运维文档,说明旧 portproxy 清理、UAC 防火墙授权、同端口 localhost/LAN 访问和故障恢复方式。
---
## [0.56.0] — 2026-05-13 ## [0.56.0] — 2026-05-13
Released: 2026-05-13 Released: 2026-05-13

View File

@@ -55,12 +55,12 @@ tasklist /svc /fi "PID eq 4700"
For temporary troubleshooting, you can stop IP Helper from Administrator PowerShell: For temporary troubleshooting, you can stop IP Helper from Administrator PowerShell:
```powershell ```powershell
Stop-Service iphlpsvc Stop-Service iphlpsvc -Force
``` ```
This may affect networking, proxying, or forwarding features. Do not disable it long-term unless you know why it is safe. If the Windows forwarding rule must stay, use a different Planet backend port. This may affect networking, proxying, or forwarding features. Do not disable it long-term unless you know why it is safe. New `--allow-lan` runs a temporary Windows relay for `3000` / `8000`, so persistent portproxy is no longer required.
If the script prints `failed-stop-service` or `failed-stop-process`, the current shell does not have permission to clear the Windows listener. Startup stops immediately instead of launching the backend into the same port conflict. If the script prints `failed-stop-service`, `failed-stop-process`, or Vite reports `Port 3000 is already in use` followed by `Windows listener ... services=iphlpsvc`, an old Windows listener still owns the port. Startup stops immediately and prints the recovery order: use Administrator PowerShell to inspect `netsh interface portproxy show all`, delete the matching `listenport` rule, confirm the PID and services with `netstat` / `tasklist` if no portproxy rule exists, and temporarily run `Stop-Service iphlpsvc -Force` when appropriate. After old rules are gone, rerun `./planet.sh restart --allow-lan`; the script starts temporary relays while LAN devices still use `3000` / `8000`.
### Which startup flags change default ports? ### Which startup flags change default ports?
@@ -110,15 +110,18 @@ The flag must be written as `--allow-lan`. `allowlan` or `--allowlan` is not rec
If `ss -ltnp` shows the frontend listening on `0.0.0.0:3000`, but `Test-NetConnection <Windows LAN IP> -Port 3000` still fails from Windows PowerShell, the problem is usually Windows-side forwarding or firewall policy rather than Vite or `.zshrc`. If `ss -ltnp` shows the frontend listening on `0.0.0.0:3000`, but `Test-NetConnection <Windows LAN IP> -Port 3000` still fails from Windows PowerShell, the problem is usually Windows-side forwarding or firewall policy rather than Vite or `.zshrc`.
For traditional WSL NAT networking, configure portproxy and firewall from Administrator PowerShell: For traditional WSL NAT networking, `./planet.sh start --allow-lan` starts temporary Windows relays so Windows still exposes `3000` / `8000`. Before startup, the script checks for stale `portproxy` rules and requests Administrator PowerShell to delete them. It also checks Windows Firewall and, when inbound allow rules for `3000` / `8000` are missing, triggers a UAC Administrator PowerShell request to create them. If the automatic request is canceled, clean up manually:
```powershell ```powershell
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=3000 connectaddress=127.0.0.1 connectport=3000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=3000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8000 connectaddress=127.0.0.1 connectport=8000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=8000
New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000 New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000
New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000 New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000
``` ```
LAN devices should use the Windows external port, for example `http://<Windows LAN IP>:3000/earth`.
If `wslinfo --networking-mode` prints `mirrored`, also check Hyper-V firewall. Even when ordinary Windows Firewall rules exist, Hyper-V firewall can still block external devices from reaching WSL. From Administrator PowerShell, allow the required ports: If `wslinfo --networking-mode` prints `mirrored`, also check Hyper-V firewall. Even when ordinary Windows Firewall rules exist, Hyper-V firewall can still block external devices from reaching WSL. From Administrator PowerShell, allow the required ports:
```powershell ```powershell

View File

@@ -177,7 +177,7 @@ Frontend startup now has an additional pre-start cleanup retry layer:
- `PORT_PRESTART_RETRIES`: defaults to 3 attempts. - `PORT_PRESTART_RETRIES`: defaults to 3 attempts.
- `PORT_PRESTART_RETRY_INTERVAL`: defaults to 2 seconds. - `PORT_PRESTART_RETRY_INTERVAL`: defaults to 2 seconds.
`kill_port_if_requested()` first cleans listener PIDs visible in the current environment. It only checks for Windows-side listeners when the script detects WSL, no local listener PID is visible, and the port still cannot bind. In that WSL-only path it tries to stop the owning Windows service or force-stop the owning process through PowerShell. If permissions are missing, or a system service such as `iphlpsvc` refuses to stop, the script prints the Windows listener details and stops startup immediately instead of launching the service into the same port error. Non-WSL environments do not run the Windows cleanup path. At that point, use Administrator PowerShell to clear the portproxy/service ownership, or choose another port. `kill_port_if_requested()` first cleans listener PIDs visible in the current environment. It only checks for Windows-side listeners when the script detects WSL, no local listener PID is visible, and the port still cannot bind. In that WSL-only path it tries to stop the owning Windows service or force-stop the owning process through PowerShell. If permissions are missing, or a system service such as `iphlpsvc` refuses to stop, the script prints the Windows listener details and Administrator PowerShell recovery commands, then stops startup immediately instead of launching the service into the same port error. If the frontend Vite process only discovers `Port 3000 is already in use` after launch, the script prints the same Windows listener recovery commands. Non-WSL environments do not run the Windows cleanup path. On WSL, `--allow-lan` uses temporary Windows relays to expose `3000` / `8000`, so old persistent portproxy rules should be removed.
## Issue 4: `restart` Behavior ## Issue 4: `restart` Behavior

View File

@@ -120,7 +120,7 @@ Useful for:
- Demoing Earth from a phone or tablet - Demoing Earth from a phone or tablet
- Other LAN machines reaching the same dev instance - Other LAN machines reaching the same dev instance
`--allow-lan` only makes the frontend and backend listen on `0.0.0.0`. When Planet runs in WSL, Windows can usually reach it through `localhost`, but other LAN machines hitting `http://<Windows LAN IP>:3000` still need Windows port forwarding and firewall rules. `--allow-lan` only makes the frontend and backend listen on `0.0.0.0`. When Planet runs in WSL, Windows can usually reach it through `localhost`, but other LAN machines reaching the Windows LAN IP still need Windows port forwarding and firewall rules.
Diagnose in this order: Diagnose in this order:
@@ -131,16 +131,18 @@ curl http://localhost:8000/health
ss -ltnp | grep -E ':3000|:8000' ss -ltnp | grep -E ':3000|:8000'
``` ```
If WSL shows `0.0.0.0:3000` / `0.0.0.0:8000` but the LAN IP still fails, configure Windows from an elevated PowerShell: If WSL services are running but the LAN IP still fails, let `./planet.sh start --allow-lan` launch temporary Windows relays. The relays keep Windows exposed on `3000` / `8000` and exit automatically when the WSL target port goes away. The script checks stale `portproxy` rules and requests Administrator PowerShell to delete them, then checks Windows Firewall and triggers a UAC Administrator PowerShell request when inbound allow rules are missing. Manual fallback commands:
```powershell ```powershell
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=3000 connectaddress=127.0.0.1 connectport=3000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=3000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8000 connectaddress=127.0.0.1 connectport=8000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=8000
New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000 New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000
New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000 New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000
``` ```
LAN devices should use the Windows external port, for example `http://<Windows LAN IP>:3000/earth`.
## AI Provider Environment and Builds ## AI Provider Environment and Builds
AI Provider runtime configuration lives in two places: AI Provider runtime configuration lives in two places:

View File

@@ -55,12 +55,12 @@ tasklist /svc /fi "PID eq 4700"
临时排障可以在管理员 PowerShell 中停止 IP Helper 临时排障可以在管理员 PowerShell 中停止 IP Helper
```powershell ```powershell
Stop-Service iphlpsvc Stop-Service iphlpsvc -Force
``` ```
这可能影响部分网络、代理或转发能力。长期不推荐禁用该服务;如果必须保留 Windows 转发,改用不同后端端口更稳 这可能影响部分网络、代理或转发能力。长期不推荐禁用该服务;新版本 `--allow-lan` 会用临时 Windows relay 暴露 `3000` / `8000`,不再需要保留持久 portproxy
如果脚本输出 `failed-stop-service``failed-stop-process`说明当前权限无法清理 Windows listener。脚本会停止启动避免后端再次遇到同一端口冲突 如果脚本输出 `failed-stop-service``failed-stop-process`或 Vite 报 `Port 3000 is already in use` 后显示 `Windows listener ... services=iphlpsvc`,说明旧的 Windows listener 仍在占用端口。脚本会停止启动,并提示用管理员 PowerShell 检查 `netsh interface portproxy show all`,删除对应 `listenport` 规则;如果没有 portproxy 规则,再用 `netstat` / `tasklist` 确认服务,必要时临时 `Stop-Service iphlpsvc -Force`。清理旧规则后重新运行 `./planet.sh restart --allow-lan`,脚本会启动临时 relay局域网仍访问 `3000` / `8000`
### 默认端口冲突时应该改哪些参数? ### 默认端口冲突时应该改哪些参数?
@@ -112,15 +112,18 @@ curl http://localhost:8000/health
如果 `ss -ltnp` 显示前端已经监听 `0.0.0.0:3000`,但 Windows PowerShell 中 `Test-NetConnection <Windows局域网IP> -Port 3000` 仍失败,问题通常不在 Vite 或 `.zshrc`,而是在 Windows 侧转发或防火墙。 如果 `ss -ltnp` 显示前端已经监听 `0.0.0.0:3000`,但 Windows PowerShell 中 `Test-NetConnection <Windows局域网IP> -Port 3000` 仍失败,问题通常不在 Vite 或 `.zshrc`,而是在 Windows 侧转发或防火墙。
传统 WSL NAT 场景下,管理员 PowerShell 中配置 portproxy 和防火墙 传统 WSL NAT 场景下,`./planet.sh start --allow-lan` 会启动临时 Windows relay让 Windows 对外仍使用 `3000` / `8000`。脚本会在启动前检测并请求管理员 PowerShell 删除旧 `portproxy`,也会检测 Windows 防火墙规则;如果缺少 `3000` / `8000` 入站放行,会触发一次 UAC 管理员 PowerShell 请求来自动创建。若自动请求被取消,可以手动清理
```powershell ```powershell
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=3000 connectaddress=127.0.0.1 connectport=3000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=3000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8000 connectaddress=127.0.0.1 connectport=8000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=8000
New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000 New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000
New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000 New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000
``` ```
局域网设备访问 Windows 对外端口,例如 `http://<Windows局域网IP>:3000/earth`
如果 `wslinfo --networking-mode` 输出 `mirrored`,还需要检查 Hyper-V firewall。普通 Windows 防火墙规则存在时Hyper-V firewall 仍可能拦截外部设备进入 WSL。管理员 PowerShell 中按端口放行: 如果 `wslinfo --networking-mode` 输出 `mirrored`,还需要检查 Hyper-V firewall。普通 Windows 防火墙规则存在时Hyper-V firewall 仍可能拦截外部设备进入 WSL。管理员 PowerShell 中按端口放行:
```powershell ```powershell

View File

@@ -155,7 +155,7 @@ wait_for_port_release() {
- `PORT_PRESTART_RETRIES`:默认 3 次。 - `PORT_PRESTART_RETRIES`:默认 3 次。
- `PORT_PRESTART_RETRY_INTERVAL`:默认 2 秒。 - `PORT_PRESTART_RETRY_INTERVAL`:默认 2 秒。
`kill_port_if_requested()` 优先清理当前环境能找到的监听 PID只有检测到当前运行在 WSL 且没有可杀 PID、但端口仍不可绑定时才会检查 Windows 侧 listener并尝试通过 PowerShell 停止对应服务或强制结束对应进程。若没有权限,或 `iphlpsvc` 这类系统服务拒绝停止,脚本会打印 Windows listener 详情立即停止启动,不再继续拉起服务碰同一个端口错误。非 WSL 环境不会尝试 Windows 清理路径。此时需要用管理员 PowerShell 清理 portproxy/服务占用,或改用其他端口 `kill_port_if_requested()` 优先清理当前环境能找到的监听 PID只有检测到当前运行在 WSL 且没有可杀 PID、但端口仍不可绑定时才会检查 Windows 侧 listener并尝试通过 PowerShell 停止对应服务或强制结束对应进程。若没有权限,或 `iphlpsvc` 这类系统服务拒绝停止,脚本会打印 Windows listener 详情和管理员 PowerShell 处理命令,然后立即停止启动,不再继续拉起服务碰同一个端口错误。前端 Vite 启动后才发现 `Port 3000 is already in use` 时,也会打印同一套 Windows listener 处理命令。非 WSL 环境不会尝试 Windows 清理路径。`--allow-lan` 在 WSL 下会使用临时 Windows relay 暴露 `3000` / `8000`,所以旧的持久 portproxy 规则应清理掉
## 问题三:端口检测用 Python ## 问题三:端口检测用 Python

View File

@@ -120,7 +120,7 @@
- 手机或平板演示 Earth - 手机或平板演示 Earth
- 局域网其他机器访问同一开发实例 - 局域网其他机器访问同一开发实例
`--allow-lan` 只负责让前端和后端监听 `0.0.0.0`。WSL 中运行时Windows 本机一般可以通过 `localhost` 访问,但局域网其他机器访问 `http://<Windows局域网IP>:3000` 还需要 Windows 端口转发和防火墙放行。 `--allow-lan` 只负责让前端和后端监听 `0.0.0.0`。WSL 中运行时Windows 本机一般可以通过 `localhost` 访问,但局域网其他机器访问 Windows 局域网 IP 还需要 Windows 端口转发和防火墙放行。
建议按顺序排查: 建议按顺序排查:
@@ -131,16 +131,18 @@ curl http://localhost:8000/health
ss -ltnp | grep -E ':3000|:8000' ss -ltnp | grep -E ':3000|:8000'
``` ```
如果看到 `0.0.0.0:3000` `0.0.0.0:8000`,但局域网 IP 仍访问失败,在管理员 PowerShell 中配置 如果看到 WSL 内部服务已经启动,但局域网 IP 仍访问失败,让 `./planet.sh start --allow-lan` 启动临时 Windows relay。relay 会让 Windows 对外继续使用 `3000` / `8000`,并在 WSL 目标端口断开后自动退出。脚本会检测并请求管理员 PowerShell 删除旧 `portproxy`,也会检测 Windows 防火墙规则;缺少入站放行时会触发一次 UAC 管理员 PowerShell 请求自动创建。手动兜底命令如下
```powershell ```powershell
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=3000 connectaddress=127.0.0.1 connectport=3000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=3000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8000 connectaddress=127.0.0.1 connectport=8000 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=8000
New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000 New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000
New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000 New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000
``` ```
局域网设备访问 Windows 对外端口,例如 `http://<Windows局域网IP>:3000/earth`
## AI Provider 环境变量与构建 ## AI Provider 环境变量与构建
AI Provider 运行期配置可以放在两处: AI Provider 运行期配置可以放在两处:

View File

@@ -16,12 +16,13 @@
## Current Version ## Current Version
- `main` 当前主线历史推导到:`0.16.5` - `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.56.0` - `dev` 当前开发分支历史推导到:`0.57.0`
## Timeline ## Timeline
| Version | Type | Branch | Commit | Summary | | Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `0.57.0` | feature | `dev` | `pending` | WSL `--allow-lan` 新增临时 Windows relay保持 localhost 与局域网同用 3000/8000并自动处理旧 portproxy、防火墙授权和 Vite ESM 配置 |
| `0.56.0` | feature | `dev` | `pending` | 修复 Earth 卫星 ECI/TEME 到 ECF 坐标转换和闭合预测轨道,调校真实高度压缩上限,统一 BGP 光晕色调,并更新超算图标与 Earth HUD/新闻体验 | | `0.56.0` | feature | `dev` | `pending` | 修复 Earth 卫星 ECI/TEME 到 ECF 坐标转换和闭合预测轨道,调校真实高度压缩上限,统一 BGP 光晕色调,并更新超算图标与 Earth HUD/新闻体验 |
| `0.55.0` | feature | `dev` | `pending` | Earth 卫星新增真实高度压缩显示开关轨迹和预测轨道跟随高度模式切换并补齐设置面板、FAQ、用户手册和开发者文档 | | `0.55.0` | feature | `dev` | `pending` | Earth 卫星新增真实高度压缩显示开关轨迹和预测轨道跟随高度模式切换并补齐设置面板、FAQ、用户手册和开发者文档 |
| `0.54.0` | feature | `dev` | `pending` | 新增 Earth 轨迹显示设置并迁移到桌面/移动设置面板,持久化轨迹偏好,同时优化卫星呼吸闪烁参数和算力中心图标资产 | | `0.54.0` | feature | `dev` | `pending` | 新增 Earth 轨迹显示设置并迁移到桌面/移动设置面板,持久化轨迹偏好,同时优化卫星呼吸闪烁参数和算力中心图标资产 |

View File

@@ -19,6 +19,7 @@
"zustand": "^4.4.7", "zustand": "^4.4.7",
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.0",
"@types/react": "^18.2.45", "@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
@@ -226,6 +227,8 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
"@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="],
@@ -458,6 +461,8 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],

View File

@@ -1,6 +1,6 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.56.0", "version": "0.57.0",
"private": true, "private": true,
"packageManager": "bun@1", "packageManager": "bun@1",
"dependencies": { "dependencies": {
@@ -20,6 +20,7 @@
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.45", "@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@types/node": "^24.0.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.10" "vite": "^5.0.10"

View File

@@ -2,9 +2,13 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"skipLibCheck": true, "skipLibCheck": true,
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tooling.tsbuildinfo",
"types": ["node"],
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.mts"]
} }

View File

@@ -2,6 +2,12 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { fileURLToPath } from 'url'
const backendPort = process.env.PLANET_BACKEND_PORT || '8000'
const backendHttpTarget = `http://localhost:${backendPort}`
const backendWsTarget = `ws://localhost:${backendPort}`
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@@ -42,18 +48,18 @@ export default defineConfig({
port: 3000, port: 3000,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8000', target: backendHttpTarget,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
'/ws': { '/ws': {
target: 'ws://localhost:8000', target: backendWsTarget,
ws: true, ws: true,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
'/health': { '/health': {
target: 'http://localhost:8000', target: backendHttpTarget,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },

365
planet.sh
View File

@@ -120,6 +120,9 @@ FRONTEND_PID_FILE="$PLANET_STATE_DIR/frontend.pid"
FRONTEND_LOG_FILE="$PLANET_STATE_DIR/frontend.log" FRONTEND_LOG_FILE="$PLANET_STATE_DIR/frontend.log"
PLANET_PORT_STATE_FILE="$PLANET_STATE_DIR/ports.env" PLANET_PORT_STATE_FILE="$PLANET_STATE_DIR/ports.env"
FRONTEND_VITE_ENTRY="$SCRIPT_DIR/frontend/node_modules/vite/bin/vite.js" FRONTEND_VITE_ENTRY="$SCRIPT_DIR/frontend/node_modules/vite/bin/vite.js"
WINDOWS_LAN_RELAY_SCRIPT="$SCRIPT_DIR/scripts/windows-lan-relay.ps1"
WINDOWS_FRONTEND_RELAY_PID_FILE="$PLANET_STATE_DIR/windows_frontend_relay.pid"
WINDOWS_BACKEND_RELAY_PID_FILE="$PLANET_STATE_DIR/windows_backend_relay.pid"
MOTION_AGENT_PID_FILE="$PLANET_STATE_DIR/motion_agent.pid" MOTION_AGENT_PID_FILE="$PLANET_STATE_DIR/motion_agent.pid"
MOTION_AGENT_LOG_FILE="$PLANET_STATE_DIR/motion_agent.log" MOTION_AGENT_LOG_FILE="$PLANET_STATE_DIR/motion_agent.log"
AI_PROVIDER_BUILD_STAMP_FILE="$PLANET_CACHE_DIR/aiprovider_build.sha256" AI_PROVIDER_BUILD_STAMP_FILE="$PLANET_CACHE_DIR/aiprovider_build.sha256"
@@ -133,6 +136,10 @@ START_RUN_COMPLETED=0
STARTED_BACKEND_THIS_RUN=0 STARTED_BACKEND_THIS_RUN=0
STARTED_FRONTEND_THIS_RUN=0 STARTED_FRONTEND_THIS_RUN=0
STARTED_MOTION_AGENT_THIS_RUN=0 STARTED_MOTION_AGENT_THIS_RUN=0
WINDOWS_LAN_RELAY_ENABLED=0
LAN_PUBLIC_FRONTEND_PORT="$DEFAULT_FRONTEND_PORT"
LAN_PUBLIC_BACKEND_PORT="$DEFAULT_BACKEND_PORT"
WINDOWS_LAN_RELAY_LISTEN_ADDRESS=""
AI_PROVIDER_RUNTIME_ENV_NAMES=( AI_PROVIDER_RUNTIME_ENV_NAMES=(
SERVICE_NAME SERVICE_NAME
SERVICE_VERSION SERVICE_VERSION
@@ -639,17 +646,246 @@ EOF
log_lan_access_notes() { log_lan_access_notes() {
local frontend_port="$1" local frontend_port="$1"
local backend_port="$2" local backend_port="$2"
local public_frontend_port="${3:-$frontend_port}"
local public_backend_port="${4:-$backend_port}"
local recommended_lan_ip="" local recommended_lan_ip=""
if recommended_lan_ip="$(get_recommended_lan_ipv4)"; then if recommended_lan_ip="$(get_recommended_lan_ipv4)"; then
log_note "推荐访问地址: http://${recommended_lan_ip}:${frontend_port}" log_note "推荐访问地址: http://${recommended_lan_ip}:${public_frontend_port}"
log_note "后端健康检查: http://${recommended_lan_ip}:${backend_port}/health" log_note "后端健康检查: http://${recommended_lan_ip}:${public_backend_port}/health"
else else
log_note "前端已对局域网开放,请使用本机局域网 IP 访问 :${frontend_port}" log_note "前端已对局域网开放,请使用本机局域网 IP 访问 :${public_frontend_port}"
log_note "后端已对局域网开放,请使用本机局域网 IP 访问 :${backend_port}/health" log_note "后端已对局域网开放,请使用本机局域网 IP 访问 :${public_backend_port}/health"
fi fi
} }
cleanup_windows_lan_relays() {
local pid_file
local pid
for pid_file in "$WINDOWS_FRONTEND_RELAY_PID_FILE" "$WINDOWS_BACKEND_RELAY_PID_FILE"; do
if [ -f "$pid_file" ]; then
if pid="$(read_pid_file "$pid_file")"; then
terminate_process_tree TERM "$pid"
sleep 0.3
terminate_process_tree KILL "$pid"
fi
remove_pid_file "$pid_file"
fi
done
}
start_windows_lan_relay() {
local name="$1"
local public_port="$2"
local target_port="$3"
local pid_file="$4"
local target_host="$5"
local listen_address="$6"
local script_path
local relay_pid
script_path="$(wslpath -w "$WINDOWS_LAN_RELAY_SCRIPT" 2>/dev/null || true)"
if [ -z "$script_path" ]; then
log_warn "无法转换 Windows relay 脚本路径,跳过 ${name} 局域网 relay"
return 1
fi
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$script_path" \
-ListenAddress "$listen_address" \
-ListenPort "$public_port" \
-TargetHost "$target_host" \
-TargetPort "$target_port" \
> /dev/null 2>&1 &
relay_pid=$!
write_pid_file "$pid_file" "$relay_pid"
sleep 0.5
if ! kill -0 "$relay_pid" 2>/dev/null; then
remove_pid_file "$pid_file"
log_warn "${name} Windows relay 未能监听端口 ${public_port}"
print_windows_port_listener_details "$public_port" || true
return 1
fi
log_success "${name} Windows relay 已启动: ${listen_address}:${public_port} -> ${target_host}:${target_port}"
}
start_windows_lan_relays() {
local target_host="127.0.0.1"
[ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ] || return 0
cleanup_windows_lan_relays
start_windows_lan_relay "前端" "$LAN_PUBLIC_FRONTEND_PORT" "$FRONTEND_PORT" "$WINDOWS_FRONTEND_RELAY_PID_FILE" "$target_host" "$WINDOWS_LAN_RELAY_LISTEN_ADDRESS" || return 1
start_windows_lan_relay "后端" "$LAN_PUBLIC_BACKEND_PORT" "$BACKEND_PORT" "$WINDOWS_BACKEND_RELAY_PID_FILE" "$target_host" "$WINDOWS_LAN_RELAY_LISTEN_ADDRESS" || return 1
}
run_windows_admin_powershell_script() {
local script_path="$1"
local warn_context="$2"
local script_win_path=""
chmod 600 "$script_path" 2>/dev/null || true
script_win_path="$(wslpath -w "$script_path" 2>/dev/null || true)"
if [ -z "$script_win_path" ]; then
log_warn "${warn_context},但无法生成管理员 PowerShell 请求"
return 1
fi
powershell.exe -NoProfile -Command "Start-Process powershell.exe -Verb RunAs -Wait -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \"${script_win_path}\"'" >/dev/null 2>&1
}
windows_firewall_missing_ports() {
local frontend_port="$1"
local backend_port="$2"
command -v powershell.exe >/dev/null 2>&1 || return 1
powershell.exe -NoProfile -Command "
\$ErrorActionPreference = 'SilentlyContinue'
\$ports = @([int]${frontend_port}, [int]${backend_port})
function Test-PortAllowed {
param([int]\$Port)
\$displayName = 'WSL Planet ' + \$Port
\$rule = Get-NetFirewallRule -DisplayName \$displayName -ErrorAction SilentlyContinue |
Where-Object { \$_.Enabled -eq 'True' -and \$_.Direction -eq 'Inbound' -and \$_.Action -eq 'Allow' } |
Select-Object -First 1
if (-not \$rule) { return \$false }
\$filter = \$rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue |
Where-Object { \$_.Protocol -in @('TCP', 'Any') -and \$_.LocalPort -eq [string]\$Port } |
Select-Object -First 1
if (\$filter) {
return \$true
}
return \$false
}
\$missing = @()
foreach (\$port in \$ports) {
if (-not (Test-PortAllowed \$port)) { \$missing += \$port }
}
if (\$missing.Count -gt 0) {
'missing=' + (\$missing -join ',')
}
" 2>/dev/null | tr -d '\r' | sed -nE 's/^missing=//p' | tail -n 1
}
request_windows_firewall_rules_if_needed() {
local frontend_port="$1"
local backend_port="$2"
local missing_ports=""
local script_path=""
[ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ] || return 0
case "${PLANET_WINDOWS_FIREWALL_AUTO_PROMPT:-1}" in
0|false|no|off)
return 0
;;
esac
missing_ports="$(windows_firewall_missing_ports "$frontend_port" "$backend_port" || true)"
[ -n "$missing_ports" ] || return 0
script_path="$PLANET_STATE_DIR/windows_firewall_planet.ps1"
mkdir -p "$PLANET_STATE_DIR"
cat > "$script_path" <<EOF
\$ErrorActionPreference = 'Stop'
\$ports = @(${missing_ports})
foreach (\$port in \$ports) {
\$displayName = "WSL Planet \$port"
\$exists = Get-NetFirewallRule -DisplayName \$displayName -ErrorAction SilentlyContinue |
Where-Object { \$_.Enabled -eq 'True' -and \$_.Direction -eq 'Inbound' -and \$_.Action -eq 'Allow' }
if (-not \$exists) {
New-NetFirewallRule -DisplayName \$displayName -Direction Inbound -Action Allow -Protocol TCP -LocalPort \$port | Out-Null
}
}
Write-Host "Planet firewall rules are ready for ports: \$((\$ports -join ', '))"
Start-Sleep -Seconds 2
EOF
log_warn "检测到 Windows 防火墙可能未放行端口 ${missing_ports},即将请求管理员 PowerShell 添加规则"
run_windows_admin_powershell_script "$script_path" "检测到 Windows 防火墙缺少端口 ${missing_ports} 放行规则" || {
log_warn "管理员 PowerShell 未完成防火墙规则创建;局域网设备可能仍无法访问"
return 1
}
}
windows_portproxy_conflict_ports() {
local frontend_port="$1"
local backend_port="$2"
command -v powershell.exe >/dev/null 2>&1 || return 1
powershell.exe -NoProfile -Command "
\$ErrorActionPreference = 'SilentlyContinue'
\$ports = @([int]${frontend_port}, [int]${backend_port})
\$lines = @(netsh interface portproxy show v4tov4)
\$found = @()
foreach (\$line in \$lines) {
if (\$line -match '^\\s*(\\S+)\\s+(\\d+)\\s+(\\S+)\\s+(\\d+)\\s*$') {
\$listenPort = [int]\$Matches[2]
if (\$ports -contains \$listenPort) { \$found += \$listenPort }
}
}
if (\$found.Count -gt 0) {
'conflict=' + ((\$found | Sort-Object -Unique) -join ',')
}
" 2>/dev/null | tr -d '\r' | sed -nE 's/^conflict=//p' | tail -n 1
}
write_windows_portproxy_cleanup_script() {
local script_path="$1"
local frontend_port="$2"
local backend_port="$3"
cat > "$script_path" <<EOF
\$ErrorActionPreference = 'Continue'
\$ports = @([int]${frontend_port}, [int]${backend_port})
\$lines = @(netsh interface portproxy show v4tov4)
\$removed = @()
foreach (\$line in \$lines) {
if (\$line -match '^\\s*(\\S+)\\s+(\\d+)\\s+(\\S+)\\s+(\\d+)\\s*$') {
\$listenAddress = \$Matches[1]
\$listenPort = [int]\$Matches[2]
if (\$ports -contains \$listenPort) {
netsh interface portproxy delete v4tov4 listenaddress=\$listenAddress listenport=\$listenPort | Out-Null
\$removed += (\$listenAddress + ':' + \$listenPort)
}
}
}
Write-Host "Planet removed stale portproxy rules: \$((\$removed -join ', '))"
Start-Sleep -Seconds 2
EOF
}
request_windows_portproxy_cleanup_if_needed() {
local frontend_port="$1"
local backend_port="$2"
local conflict_ports=""
local script_path=""
[ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ] || return 0
case "${PLANET_WINDOWS_PORTPROXY_CLEANUP_PROMPT:-1}" in
0|false|no|off)
return 0
;;
esac
conflict_ports="$(windows_portproxy_conflict_ports "$frontend_port" "$backend_port" || true)"
[ -n "$conflict_ports" ] || return 0
script_path="$PLANET_STATE_DIR/windows_portproxy_cleanup_planet.ps1"
mkdir -p "$PLANET_STATE_DIR"
write_windows_portproxy_cleanup_script "$script_path" "$frontend_port" "$backend_port"
log_warn "检测到旧 Windows portproxy 占用端口 ${conflict_ports},即将请求管理员 PowerShell 删除"
run_windows_admin_powershell_script "$script_path" "检测到旧 portproxy 占用端口 ${conflict_ports}" || {
log_warn "管理员 PowerShell 未完成旧 portproxy 清理;端口 ${conflict_ports} 可能仍被 iphlpsvc 占用"
return 1
}
}
log_motion_agent_access_notes() { log_motion_agent_access_notes() {
local motion_agent_port="$1" local motion_agent_port="$1"
local lan_enabled="${2:-0}" local lan_enabled="${2:-0}"
@@ -1351,7 +1587,7 @@ fail_unreleased_port() {
if [ -z "$(collect_port_pids "$port" || true)" ]; then if [ -z "$(collect_port_pids "$port" || true)" ]; then
log_error "端口 ${port} 当前环境内未发现占用进程,但端口仍不可用,请检查宿主机或外部环境占用" log_error "端口 ${port} 当前环境内未发现占用进程,但端口仍不可用,请检查宿主机或外部环境占用"
print_port_listener_details "$port" print_port_listener_details "$port"
log_note "如果显示 Windows listener如 svchost.exe / iphlpsvc请优先用管理员 PowerShell 检查 portproxynetsh interface portproxy show all删除冲突转发或改用其他端口。" log_note "如果显示 Windows listener如 svchost.exe / iphlpsvc请优先用管理员 PowerShell 检查 portproxynetsh interface portproxy show all删除冲突转发后重新启动,--allow-lan 会改用临时 Windows relay。"
else else
log_error "端口 ${port} 清理失败,请检查占用进程" log_error "端口 ${port} 清理失败,请检查占用进程"
if command -v lsof >/dev/null 2>&1; then if command -v lsof >/dev/null 2>&1; then
@@ -1360,7 +1596,7 @@ fail_unreleased_port() {
ss -ltnp "( sport = :${port} )" 2>/dev/null || true ss -ltnp "( sport = :${port} )" 2>/dev/null || true
fi fi
print_windows_port_listener_details "$port" || true print_windows_port_listener_details "$port" || true
log_note "请手动停止占用进程,或改用其他端口。" log_note "请手动停止占用进程后重试。"
fi fi
exit 1 exit 1
@@ -1395,6 +1631,48 @@ retry_wait_for_port_release() {
return 1 return 1
} }
print_windows_cleanup_recovery_steps() {
local port="$1"
local service_name="$2"
local port_flag
local command_example
case "$service_name" in
前端)
port_flag="-f"
command_example="./planet.sh restart ${port_flag} ${port} --allow-lan"
;;
后端)
port_flag="-b"
command_example="./planet.sh restart ${port_flag} ${port} --allow-lan"
;;
"AI Provider")
port_flag="-a"
command_example="./planet.sh restart ${port_flag} ${port}"
;;
"Motion Agent")
port_flag="--motion-agent-port"
command_example="./planet.sh restart ${port_flag} ${port} --allow-lan"
;;
*)
port_flag=""
command_example="./planet.sh restart --allow-lan"
;;
esac
log_note "Windows 侧端口被 svchost.exe / iphlpsvc 占用时,通常是管理员 PowerShell 中创建过持久 portproxy 规则,例如 listenport=${port}"
log_note "该规则会由 Windows IP Helper 持续监听端口,项目停止后也不会自动消失;下次启动前必须先删除,否则会与 Vite/后端绑定同一端口冲突。"
log_note "请打开“管理员 PowerShell”优先检查并删除冲突 portproxy"
printf "${DIM} netsh interface portproxy show all${NC}\n"
printf "${DIM} netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=%s${NC}\n" "$port"
log_note "如果没有 portproxy 规则,再确认 PID/服务并临时停止对应服务:"
printf "${DIM} netstat -ano | findstr :%s${NC}\n" "$port"
printf "${DIM} tasklist /svc /fi \"PID eq <PID>\"${NC}\n"
printf "${DIM} Stop-Service iphlpsvc -Force${NC}\n"
log_note "如需局域网仍使用 ${port},请删除旧 portproxy 后重新运行 --allow-lan脚本会使用临时 Windows relay 暴露该端口。"
log_note "重新启动示例:${command_example}"
}
force_cleanup_external_port_listener() { force_cleanup_external_port_listener() {
local port="$1" local port="$1"
local service_name="$2" local service_name="$2"
@@ -1487,6 +1765,7 @@ EOF
log_warn "Windows 侧未发现端口 ${port} 监听,但端口仍不可绑定" log_warn "Windows 侧未发现端口 ${port} 监听,但端口仍不可绑定"
elif [ "$saw_cleanup_failure" -eq 1 ]; then elif [ "$saw_cleanup_failure" -eq 1 ]; then
log_warn "Windows 侧监听清理失败,端口 ${port} 仍不可绑定" log_warn "Windows 侧监听清理失败,端口 ${port} 仍不可绑定"
print_windows_cleanup_recovery_steps "$port" "$service_name"
fi fi
return 1 return 1
} }
@@ -1885,6 +2164,48 @@ wait_for_port_release() {
return 1 return 1
} }
get_windows_lan_ipv4() {
command -v powershell.exe >/dev/null 2>&1 || return 1
powershell.exe -NoProfile -Command "
\$ErrorActionPreference = 'SilentlyContinue'
\$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' |
Where-Object { \$_.NextHop -and \$_.NextHop -ne '0.0.0.0' } |
Sort-Object RouteMetric, InterfaceMetric |
Select-Object -First 1
if (\$route) {
\$ip = Get-NetIPAddress -AddressFamily IPv4 -InterfaceIndex \$route.InterfaceIndex |
Where-Object { \$_.IPAddress -notmatch '^(127|169\\.254)\\.' } |
Select-Object -First 1 -ExpandProperty IPAddress
if (\$ip) { \$ip }
}
" 2>/dev/null | tr -d '\r' | sed -n '1p'
}
prepare_windows_lan_relay_ports() {
local listen_address=""
[ "$FRONTEND_LAN_ENABLED" -eq 1 ] || return 0
is_wsl_environment || return 0
command -v powershell.exe >/dev/null 2>&1 || return 0
[ -f "$WINDOWS_LAN_RELAY_SCRIPT" ] || return 0
listen_address="$(get_windows_lan_ipv4 || true)"
if [ -z "$listen_address" ]; then
log_warn "未能识别 Windows 局域网 IP跳过 Windows 局域网 relay本机 localhost 仍可使用 ${FRONTEND_PORT}/${BACKEND_PORT}"
return 0
fi
WINDOWS_LAN_RELAY_ENABLED=1
LAN_PUBLIC_FRONTEND_PORT="$FRONTEND_PORT"
LAN_PUBLIC_BACKEND_PORT="$BACKEND_PORT"
WINDOWS_LAN_RELAY_LISTEN_ADDRESS="$listen_address"
cleanup_windows_lan_relays
request_windows_portproxy_cleanup_if_needed "$LAN_PUBLIC_FRONTEND_PORT" "$LAN_PUBLIC_BACKEND_PORT" || true
log_note "WSL 局域网 relay: Windows ${WINDOWS_LAN_RELAY_LISTEN_ADDRESS}:${LAN_PUBLIC_FRONTEND_PORT}/${LAN_PUBLIC_BACKEND_PORT} -> localhost:${FRONTEND_PORT}/${BACKEND_PORT}"
}
kill_port_if_requested() { kill_port_if_requested() {
local port="$1" local port="$1"
local service_name="$2" local service_name="$2"
@@ -2071,6 +2392,7 @@ start_frontend_with_retry() {
local frontend_port="$1" local frontend_port="$1"
local frontend_port_requested="${2:-0}" local frontend_port_requested="${2:-0}"
local frontend_lan_enabled="${3:-0}" local frontend_lan_enabled="${3:-0}"
local backend_port="${4:-$DEFAULT_BACKEND_PORT}"
local retry=1 local retry=1
while [ "$retry" -le "$FRONTEND_MAX_RETRIES" ]; do while [ "$retry" -le "$FRONTEND_MAX_RETRIES" ]; do
@@ -2087,7 +2409,7 @@ start_frontend_with_retry() {
frontend_args+=(--host 0.0.0.0) frontend_args+=(--host 0.0.0.0)
fi fi
frontend_args+=(--port "$frontend_port" --strictPort) frontend_args+=(--port "$frontend_port" --strictPort)
nohup "$FRONTEND_RUNTIME_BIN" "${frontend_args[@]}" > "$FRONTEND_LOG_FILE" 2>&1 & PLANET_BACKEND_PORT="$backend_port" nohup "$FRONTEND_RUNTIME_BIN" "${frontend_args[@]}" > "$FRONTEND_LOG_FILE" 2>&1 &
FRONTEND_PID=$! FRONTEND_PID=$!
write_pid_file "$FRONTEND_PID_FILE" "$FRONTEND_PID" write_pid_file "$FRONTEND_PID_FILE" "$FRONTEND_PID"
@@ -2114,6 +2436,7 @@ start_frontend_service() {
local frontend_port="$1" local frontend_port="$1"
local frontend_port_requested="$2" local frontend_port_requested="$2"
local frontend_lan_enabled="${3:-0}" local frontend_lan_enabled="${3:-0}"
local backend_port="${4:-$DEFAULT_BACKEND_PORT}"
if [ "$frontend_port_requested" -eq 1 ]; then if [ "$frontend_port_requested" -eq 1 ]; then
kill_port_if_requested "$frontend_port" "前端" kill_port_if_requested "$frontend_port" "前端"
@@ -2130,11 +2453,14 @@ start_frontend_service() {
else else
set_wait_detail "启动 Vite 开发服务器" set_wait_detail "启动 Vite 开发服务器"
fi fi
if ! start_frontend_with_retry "$frontend_port" "$frontend_port_requested" "$frontend_lan_enabled"; then if ! start_frontend_with_retry "$frontend_port" "$frontend_port_requested" "$frontend_lan_enabled" "$backend_port"; then
stop_wait_session stop_wait_session
log_error "前端启动失败,已重试 ${FRONTEND_MAX_RETRIES}" log_error "前端启动失败,已重试 ${FRONTEND_MAX_RETRIES}"
tail -10 "$FRONTEND_LOG_FILE" tail -10 "$FRONTEND_LOG_FILE"
print_port_listener_details "$frontend_port" print_port_listener_details "$frontend_port"
if frontend_log_indicates_port_conflict "$FRONTEND_LOG_FILE"; then
print_windows_cleanup_recovery_steps "$frontend_port" "前端"
fi
exit 1 exit 1
fi fi
stop_wait_session stop_wait_session
@@ -2485,6 +2811,7 @@ cleanup_failed_start() {
clear_wait_spinner clear_wait_spinner
log_warn "启动未完成,清理本轮已拉起的本地进程" log_warn "启动未完成,清理本轮已拉起的本地进程"
cleanup_windows_lan_relays
if [ "$STARTED_MOTION_AGENT_THIS_RUN" -eq 1 ]; then if [ "$STARTED_MOTION_AGENT_THIS_RUN" -eq 1 ]; then
cleanup_motion_agent_processes TERM cleanup_motion_agent_processes TERM
@@ -2605,6 +2932,7 @@ PY
# Top-level commands # Top-level commands
start() { start() {
parse_service_args "$@" parse_service_args "$@"
prepare_windows_lan_relay_ports
cleanup_exit_containers cleanup_exit_containers
START_RUN_ACTIVE=1 START_RUN_ACTIVE=1
@@ -2617,7 +2945,7 @@ start() {
start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" "$AI_PROVIDER_PORT" start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" "$AI_PROVIDER_PORT"
STARTED_BACKEND_THIS_RUN=1 STARTED_BACKEND_THIS_RUN=1
start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED" "$FRONTEND_LAN_ENABLED" start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED" "$FRONTEND_LAN_ENABLED" "$BACKEND_PORT"
STARTED_FRONTEND_THIS_RUN=1 STARTED_FRONTEND_THIS_RUN=1
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
start_motion_agent_service "$MOTION_AGENT_PORT" "$MOTION_AGENT_DRY_RUN" "$FRONTEND_LAN_ENABLED" start_motion_agent_service "$MOTION_AGENT_PORT" "$MOTION_AGENT_DRY_RUN" "$FRONTEND_LAN_ENABLED"
@@ -2625,24 +2953,37 @@ start() {
fi fi
write_port_state "$BACKEND_PORT" "$FRONTEND_PORT" "$AI_PROVIDER_PORT" "$MOTION_AGENT_PORT" write_port_state "$BACKEND_PORT" "$FRONTEND_PORT" "$AI_PROVIDER_PORT" "$MOTION_AGENT_PORT"
if [ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ]; then
start_windows_lan_relays
request_windows_firewall_rules_if_needed "$LAN_PUBLIC_FRONTEND_PORT" "$LAN_PUBLIC_BACKEND_PORT" || true
fi
START_RUN_COMPLETED=1 START_RUN_COMPLETED=1
START_RUN_ACTIVE=0 START_RUN_ACTIVE=0
log_success "启动完成" log_success "启动完成"
if [ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ]; then
log_note "智能星球计划: http://localhost:${LAN_PUBLIC_FRONTEND_PORT}/earth"
log_note "智能星球仪表盘: http://localhost:${LAN_PUBLIC_FRONTEND_PORT}/admin"
log_note "AI Playground: http://localhost:${LAN_PUBLIC_FRONTEND_PORT}/playground"
log_note "智能星球开发文档: http://localhost:${LAN_PUBLIC_BACKEND_PORT}/docs"
log_note "局域网入口: http://${WINDOWS_LAN_RELAY_LISTEN_ADDRESS}:${LAN_PUBLIC_FRONTEND_PORT}/earth"
else
log_note "智能星球计划: http://localhost:${FRONTEND_PORT}/earth" log_note "智能星球计划: http://localhost:${FRONTEND_PORT}/earth"
log_note "智能星球仪表盘: http://localhost:${FRONTEND_PORT}/admin" log_note "智能星球仪表盘: http://localhost:${FRONTEND_PORT}/admin"
log_note "AI Playground: http://localhost:${FRONTEND_PORT}/playground" log_note "AI Playground: http://localhost:${FRONTEND_PORT}/playground"
log_note "智能星球开发文档: http://localhost:${BACKEND_PORT}/docs" log_note "智能星球开发文档: http://localhost:${BACKEND_PORT}/docs"
fi
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
log_motion_agent_access_notes "$MOTION_AGENT_PORT" "$FRONTEND_LAN_ENABLED" log_motion_agent_access_notes "$MOTION_AGENT_PORT" "$FRONTEND_LAN_ENABLED"
fi fi
if [ "$FRONTEND_LAN_ENABLED" -eq 1 ]; then if [ "$FRONTEND_LAN_ENABLED" -eq 1 ] && [ "$WINDOWS_LAN_RELAY_ENABLED" -eq 0 ]; then
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT" log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT" "$LAN_PUBLIC_FRONTEND_PORT" "$LAN_PUBLIC_BACKEND_PORT"
fi fi
} }
stop() { stop() {
log_warn "停止服务" log_warn "停止服务"
cleanup_windows_lan_relays
stop_backend_service stop_backend_service
stop_ai_provider_service stop_ai_provider_service
stop_frontend_service stop_frontend_service
@@ -2688,7 +3029,7 @@ restart() {
if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then
stop_frontend_service stop_frontend_service
sleep 1 sleep 1
start_frontend_service "$FRONTEND_PORT" 1 "$FRONTEND_LAN_ENABLED" start_frontend_service "$FRONTEND_PORT" 1 "$FRONTEND_LAN_ENABLED" "$BACKEND_PORT"
fi fi
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "planet" name = "planet"
version = "0.56.0" version = "0.57.0"
description = "智能星球计划 - 态势感知系统" description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [

View File

@@ -0,0 +1,103 @@
param(
[string]$ListenAddress = "0.0.0.0",
[Parameter(Mandatory = $true)][int]$ListenPort,
[Parameter(Mandatory = $true)][string]$TargetHost,
[Parameter(Mandatory = $true)][int]$TargetPort,
[int]$IdleExitSeconds = 8
)
$ErrorActionPreference = "Stop"
Add-Type -TypeDefinition @"
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
public static class PlanetTcpRelay
{
public static async Task RunAsync(string listenAddress, int listenPort, string targetHost, int targetPort, int idleExitSeconds)
{
IPAddress address;
if (!IPAddress.TryParse(listenAddress, out address))
{
address = IPAddress.Any;
}
var listener = new TcpListener(address, listenPort);
listener.Start();
var lastHealthyAt = DateTime.UtcNow;
try
{
while (true)
{
if (await CanConnectAsync(targetHost, targetPort))
{
lastHealthyAt = DateTime.UtcNow;
}
else if ((DateTime.UtcNow - lastHealthyAt).TotalSeconds >= idleExitSeconds)
{
break;
}
while (listener.Pending())
{
var client = await listener.AcceptTcpClientAsync();
_ = Task.Run(() => HandleClientAsync(client, targetHost, targetPort));
}
await Task.Delay(200);
}
}
finally
{
listener.Stop();
}
}
private static async Task<bool> CanConnectAsync(string host, int port)
{
using (var client = new TcpClient())
{
var connectTask = client.ConnectAsync(host, port);
var completed = await Task.WhenAny(connectTask, Task.Delay(500));
if (completed != connectTask)
{
return false;
}
try
{
await connectTask;
return client.Connected;
}
catch
{
return false;
}
}
}
private static async Task HandleClientAsync(TcpClient client, string targetHost, int targetPort)
{
using (client)
using (var upstream = new TcpClient())
{
try
{
await upstream.ConnectAsync(targetHost, targetPort);
var clientStream = client.GetStream();
var upstreamStream = upstream.GetStream();
var toUpstream = clientStream.CopyToAsync(upstreamStream);
var toClient = upstreamStream.CopyToAsync(clientStream);
await Task.WhenAny(toUpstream, toClient);
}
catch
{
}
}
}
}
"@
[PlanetTcpRelay]::RunAsync($ListenAddress, $ListenPort, $TargetHost, $TargetPort, $IdleExitSeconds).GetAwaiter().GetResult()

2
uv.lock generated
View File

@@ -757,7 +757,7 @@ wheels = [
[[package]] [[package]]
name = "planet" name = "planet"
version = "0.56.0" version = "0.57.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },