0%

3X-UI 最新版配置 VLESS + Reality + Vision 教程

本文记录一次在 3X-UI v3.2.8 中配置 VLESS + Reality + Vision 的完整过程,包含端口放行检查、入站创建、Reality 参数、Sniffing、Client 创建与连通性测试。

说明:本文用于个人学习与自用环境记录。请遵守服务器所在地、网络所在地及服务商的使用规则。


1. 安装 3X-UI 面板

1.1 更新系统并安装基础工具

在服务器中执行:

1
apt-get update && apt-get install -y wget curl

1.2 放行面板和相关端口

先放行 HTTP 端口:

1
ufw allow 80

再放行 3X-UI 面板端口范围:

1
ufw allow 55555:56666/tcp

说明:这里后续会将 3X-UI 面板端口设置为 55555,因此需要提前放行该端口。端口范围 55555:56666/tcp 也便于后续测试或调整。

1.3 执行官方安装脚本

执行:

1
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)

安装过程中按以下方式选择:

1
2
3
4
是否自选端口:输入 y
端口:输入 55555
面板证书类型:输入 2
后续选项(如果有):直接回车跳过

安装完成后,终端会显示面板访问地址、用户名和密码。请立即保存这些信息。

1.4 浏览器登录 3X-UI

打开浏览器,访问:

1
http://你的服务器公网IP:55555

然后使用安装脚本输出的用户名和密码登录。

登录后建议立即完成以下安全设置:

1
2
3
4
修改默认用户名和密码
修改面板访问路径
确认面板端口不是代理端口
只开放必要端口

2. 配置目标

最终目标配置为:

1
2
3
4
5
6
7
8
9
10
Protocol: VLESS
Port: 443
Transmission: RAW / TCP
Security: Reality
Flow: xtls-rprx-vision
uTLS / Fingerprint: chrome
SNI: www.amd.com
Target: www.amd.com:443
Sniffing: HTTP, TLS, QUIC
Client: 绑定到 vless-reality-vision 入站

3. 初始状态:进入 Add Inbound

最开始进入 3X-UI 的 Inbounds → Add Inbound 页面时,默认协议可能是 http

初始 Add Inbound 界面

这里不要使用 http 作为最终代理协议,需要改为:

1
Protocol: vless

4. 检查服务器本机防火墙是否放行 443

在服务器上执行:

1
2
sudo iptables -L -n -v
sudo iptables -S | grep 443

当看到类似下面结果时,说明服务器本机 UFW / iptables 已经允许 TCP 443:

1
-A ufw-user-input -p tcp -m tcp --dport 443 -j ACCEPT

截图记录:

UFW / iptables 已放行 443

此时只能说明服务器本机防火墙放行了 443,还不能证明公网能访问 443。


5. 未配置 3X-UI 入站前,公网测试 443 会失败

在 Windows PowerShell 中测试:

1
Test-NetConnection 你的服务器公网IP -Port 443

如果此时 3X-UI 还没有创建监听 443 的入站,会出现:

1
TcpTestSucceeded : False

截图记录:

未配置入站前 443 测试失败

这个结果是正常的,因为服务器虽然放行了 443,但还没有程序监听 443。


6. 创建 VLESS 入站:Basics 页面

进入:

1
Inbounds → Add Inbound → Basics

填写:

1
2
3
4
5
6
7
8
Enabled: 开启
Remark: vless-reality-vision
Protocol: vless
Address: 留空
Port: 443
Total Flow: 0
Traffic Reset: Never
Duration: 留空

截图记录:

Basics 页面配置 VLESS 和 443

说明:

  • Address 留空表示监听所有 IP。
  • Port 使用 443,前提是服务器没有其他服务占用 443。
  • 如果 443 被 Nginx、Caddy、Apache 等占用,可以换成其他端口,例如 8443,但云服务器安全组也要对应放行。

7. Protocol 页面

进入:

1
Add Inbound → Protocol

保持:

1
2
3
Decryption: none
Encryption: none
Selected: None

截图记录:

Protocol 页面保持 none

注意:

  • 这里的 X25519 authML-KEM-768 auth 不需要点。
  • Reality 密钥是在后面的 Security 页面配置,不是在这里配置。

8. Stream 页面

进入:

1
Add Inbound → Stream

设置:

1
2
3
4
5
6
Transmission: RAW
Proxy Protocol: 关闭
HTTP Obfuscation: 关闭
External Proxy: 关闭
Sockopt: 关闭
TCP Masks: 不添加

截图记录:

Stream 页面使用 RAW

说明:

  • RAW 可以理解为新版界面中的 TCP / 原始传输方式。
  • 本教程目标是 VLESS + RAW/TCP + Reality + Vision

9. Security 页面:Reality 配置

进入:

1
Add Inbound → Security

选择:

1
Security: Reality

推荐配置:

1
2
3
4
5
6
7
8
9
10
11
12
Show: 关闭
Xver: 0
uTLS: chrome
Target: www.amd.com:443
SNI: www.amd.com
Max Time Diff(ms): 0
Min Client Ver: 建议留空
Max Client Ver: 建议留空
Short IDs: 建议只保留一个,便于排查
SpiderX: /
Public Key: 点击 Get New Cert 自动生成
Private Key: 点击 Get New Cert 自动生成

截图记录:

Security 页面 Reality 配置

重要提醒:

  • 截图中显示了 Private Key,正式长期使用前建议重新生成一组 Reality Key。
  • 以后不要公开 Private Key、UUID、客户端分享链接等敏感信息。
  • TargetSNI 要对应,例如:
1
2
Target: www.amd.com:443
SNI: www.amd.com

也可以使用其他真实 HTTPS 站点,例如:

1
2
Target: www.microsoft.com:443
SNI: www.microsoft.com

10. Sniffing 页面

进入:

1
Add Inbound → Sniffing

建议设置:

1
2
3
4
5
6
7
8
9
Enabled: 开启
HTTP: 勾选
TLS: 勾选
QUIC: 勾选
FAKEDNS: 不勾选,或新手先关闭
Metadata only: 关闭
Route only: 关闭
IPs excluded: 留空
Domains excluded: 留空

截图记录:

Sniffing 页面配置

说明:

  • HTTP / TLS / QUIC 可开启。
  • FAKEDNS 属于更复杂的 DNS 分流配置,新手阶段建议先关闭,避免增加排查难度。

11. Advanced 页面

进入:

1
Add Inbound → Advanced

新手保持默认即可。

最后点击:

1
Create

创建完成后,服务器应开始监听 443。


12. 检查 443 是否监听成功

在服务器上执行:

1
sudo ss -lntp | grep ':443'

理想结果类似:

1
LISTEN 0 4096 0.0.0.0:443 0.0.0.0:* users:(("xray",pid=xxxx,fd=xx))

然后在 Windows PowerShell 中执行:

1
Test-NetConnection 你的服务器公网IP -Port 443

当出现下面结果时,说明公网 TCP 443 已经连通:

1
TcpTestSucceeded : True

本次配置中最终测试结果为:

1
2
3
4
5
6
ComputerName     : 你的服务器公网IP
RemoteAddress : 你的服务器公网IP
RemotePort : 443
InterfaceAlias : 以太网
SourceAddress : 10.x.x.x
TcpTestSucceeded : True

13. 创建 Client 客户端

新版 3X-UI 中,创建入站后还需要创建客户端。

进入:

1
Clients

最开始可能显示 Clients 0

Clients 页面为空

点击:

1
+ Add Clients

14. Add Client 页面配置

填写:

1
2
3
4
5
6
7
8
9
10
11
Email: demo-vless
UUID: 自动生成
Total Sent/Received(GB): 0
Expiry: 留空
Start After First Use: 关闭
Auto Renew: 0
Flow: xtls-rprx-vision
Comment: 可留空
Group: 可留空
Attached inbounds: vless-reality-vision
Enabled: 开启

截图记录:

Add Client 页面配置

说明:

  • Flow 必须选择:
1
xtls-rprx-vision
  • Attached inbounds 必须绑定前面创建的:
1
vless-reality-vision
  • Hysteria AuthPassword 是通用字段或其他协议使用的字段,本教程使用 VLESS 时不用重点处理。

最后点击:

1
Create

15. 导出客户端链接

创建 Client 后,在客户端行的 Actions 中查找:

1
二维码 / QR Code / Copy Link / Share / Export

复制 VLESS 链接。

链接中应包含以下关键字段:

1
2
3
4
5
6
7
8
9
vless://
服务器IP:443
security=reality
type=tcp 或 type=raw
flow=xtls-rprx-vision
sni=www.amd.com
fp=chrome
pbk=
sid=

如果缺少 flow=xtls-rprx-vision,客户端可能没有启用 Vision。


16. Windows v2rayN 导入方式

Windows 使用 v2rayN 时:

1
服务器 → 从剪贴板导入分享链接 → 选中节点 → 设为活动服务器 → 启动系统代理

导入后检查节点参数:

1
2
3
4
5
6
7
8
9
10
地址: 你的服务器公网IP
端口: 443
协议: VLESS
传输: TCP / RAW
安全: Reality
SNI: www.amd.com
Fingerprint: chrome
Public Key: 与 3X-UI 的 Public Key 一致
Short ID: 与 3X-UI 的 Short ID 一致
Flow: xtls-rprx-vision

17. Android v2rayNG 导入方式

Android 使用 v2rayNG 时:

1
右上角 + → 从剪贴板导入 → 选择新节点 → 点击右下角启动

如果连接失败,优先检查:

1
2
3
4
5
6
7
Flow 是否为 xtls-rprx-vision
SNI 是否为 www.amd.com
Fingerprint 是否为 chrome
Public Key 是否正确
Short ID 是否正确
端口是否为 443
服务器安全组是否放行 TCP 443

18. 常见问题排查

18.1 TcpTestSucceeded 为 False

可能原因:

1
2
3
4
5
1. 3X-UI 入站没有创建成功
2. Xray 没有监听 443
3. 服务器本机防火墙没有放行 443
4. 云服务器安全组没有放行 443
5. 443 被其他程序占用

排查命令:

1
2
3
4
sudo ss -lntp | grep ':443'
sudo lsof -iTCP:443 -sTCP:LISTEN -P -n
sudo ufw status
sudo iptables -S | grep 443

18.2 端口通,但客户端连不上

优先检查客户端参数是否一致:

1
2
3
4
5
6
security=reality
flow=xtls-rprx-vision
sni=www.amd.com
fp=chrome
pbk=Public Key
sid=Short ID

18.3 客户端提示 TLS handshake timeout

可能原因:

1
2
3
4
5
6
1. SNI 与服务端 Reality 的 SNI 不一致
2. Public Key 错误
3. Short ID 错误
4. Flow 没有设置为 xtls-rprx-vision
5. 客户端版本过旧,不支持 Reality / Vision
6. Min Client Ver / Max Client Ver 限制导致客户端被拒绝

18.4 443 被其他程序占用

检查:

1
sudo lsof -iTCP:443 -sTCP:LISTEN -P -n

如果看到 nginxapachecaddy 等占用 443,需要:

1
2
3
1. 改用其他端口,例如 8443;或
2. 停止占用 443 的服务;或
3. 做更复杂的反向代理 / 分流配置。

19. 最终配置汇总

本次可用配置汇总如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Inbound Remark: vless-reality-vision
Protocol: vless
Port: 443
Transmission: RAW
Security: Reality
uTLS: chrome
Target: www.amd.com:443
SNI: www.amd.com
SpiderX: /
Sniffing: HTTP, TLS, QUIC
Client Email: demo-vless
Client Flow: xtls-rprx-vision
Attached Inbound: vless-reality-vision

20. 安全建议

  1. 面板默认账号密码必须修改。
  2. 建议修改面板访问路径。
  3. 面板端口不要和代理端口混用。
  4. 不要公开 Reality Private Key、UUID、分享链接、订阅链接。
  5. 如果截图或链接曾经公开,建议重新生成 Client UUID 和 Reality Key。
  6. 服务器只开放必要端口,例如 SSH、面板端口、代理端口。
  7. 定期更新 3X-UI 和 Xray-core。

服务器 Codex Device Code 切换账户登录说明

本文记录在服务器环境中使用 codex-proxy login --device-auth 退出旧账户并登录新账户的流程。

这篇文章和《服务器 Codex 反向代理使用说明》是同一套使用场景:服务器本身无法直接访问 OpenAI,需要先通过本地 Windows 电脑的 v2rayN、Clash、Clash Verge 等代理建立 SSH 反向隧道,再让服务器上的 Codex 通过 codex-proxy 访问外网。

适用场景:

  1. 服务器已经安装 codexcodex-proxy
  2. 本地电脑已经建立 SSH 反向代理。
  3. 需要从旧 ChatGPT/Codex 账户切换到新账户。
  4. 服务器没有浏览器,需要使用 device code 在本地浏览器完成登录。

1. 前提条件

开始前,建议先确认上一篇文章中的反向代理链路已经可用。

本地 Windows PowerShell 中需要保持 SSH 反向隧道窗口打开:

1
ssh -N -T -o ExitOnForwardFailure=yes -R 43897:127.0.0.1:7897 your_user@your_server_ip

其中:

  • 43897:服务器端监听端口;
  • 7897:本地代理端口,例如 v2rayN、Clash、Clash Verge 等;
  • your_user:服务器用户名;
  • your_server_ip:服务器 IP 地址。

注意:这个 PowerShell 窗口需要一直保持打开,关闭后服务器上的反向代理会断开。

2. 检查当前 Codex 状态

登录服务器后,先检查当前用户、Codex 命令位置和认证文件:

1
2
3
4
5
6
7
whoami
which codex
which codex-proxy
codex-proxy --version

ls -la ~/.codex
ls -l ~/.codex/auth.json 2>/dev/null || echo "no auth.json"

如果需要查看 auth.json 的基本结构,但不暴露 token,可以执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python3 - <<'PY'
import json, os

p = os.path.expanduser("~/.codex/auth.json")

if not os.path.exists(p):
print("auth.json not found")
else:
with open(p) as f:
data = json.load(f)

print("auth.json exists")
print("top-level keys:", list(data.keys()))
PY

3. 退出当前 Codex 账户

优先使用官方退出命令:

1
codex-proxy logout

然后检查认证文件是否仍然存在:

1
ls -l ~/.codex/auth.json 2>/dev/null || echo "auth.json removed"

如果退出后仍然存在认证问题,可以手动备份并删除 auth.json

1
2
3
mkdir -p ~/.codex_auth_backup
cp ~/.codex/auth.json ~/.codex_auth_backup/auth.json.$(date +%Y%m%d_%H%M%S) 2>/dev/null || true
rm -f ~/.codex/auth.json

4. 清理旧的 Codex 后台进程

如果之前启动过 codex app-server,建议在重新登录前清理旧进程。

先查看当前用户下的 Codex 相关进程:

1
ps -u "$(whoami)" -o pid=,cmd= | grep '[c]odex' || echo "no codex process"

如果存在旧的 app-server 进程,可以只清理当前用户下的相关进程:

1
2
3
4
5
ps -u "$(whoami)" -o pid=,cmd= \
| grep 'codex app-server' \
| grep -v grep \
| awk '{print $1}' \
| xargs -r kill -9

不要在多用户服务器上直接使用不带用户限制的 pkill -f codex,否则可能误匹配其他用户的 Codex 进程。

再次检查是否还有残留进程:

1
ps -u "$(whoami)" -o pid=,cmd= | grep '[c]odex' || echo "no codex process"

5. 检查反向代理是否可用

在服务器上测试 OpenAI 是否能通过反向代理访问:

1
curl -I -x http://127.0.0.1:43897 https://api.openai.com

如果能看到 HTTP 响应,说明反向代理可用。

如果连接失败,先回到本地 Windows PowerShell,确认 SSH 反向隧道仍然打开:

1
ssh -N -T -o ExitOnForwardFailure=yes -R 43897:127.0.0.1:7897 your_user@your_server_ip

也可以在本地电脑测试本地代理端口:

1
curl.exe -I -x http://127.0.0.1:7897 https://www.google.com

6. 使用 Device Code 登录新账户

在服务器上执行:

1
codex-proxy login --device-auth

终端一般会输出:

  • 一个登录链接;
  • 一个一次性 device code。

操作步骤:

  1. 在本地浏览器中打开终端显示的登录链接。
  2. 登录你要切换到的新 ChatGPT/Codex 账户。
  3. 输入服务器终端显示的 device code。
  4. 等待服务器终端显示登录成功。

建议使用浏览器无痕窗口,避免浏览器自动复用旧账户。

7. 检查新账户登录状态

登录完成后,在服务器上执行:

1
ls -l ~/.codex/auth.json

继续检查认证文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
python3 - <<'PY'
import json, os

p = os.path.expanduser("~/.codex/auth.json")

print("exists:", os.path.exists(p))

if os.path.exists(p):
with open(p) as f:
data = json.load(f)

print("keys:", list(data.keys()))
PY

如果 auth.json 存在,并且能正常读取,说明新的本地认证文件已经生成。

8. 启动 Codex

进入你的项目目录:

1
cd /path/to/your/project

通过代理启动 Codex:

1
codex-proxy

不要直接执行:

1
codex

因为直接运行 codex 可能不会带上你在 codex-proxy 中配置的代理环境变量。

9. 一键完整流程

如果只是想快速完成“退出旧账户并登录新账户”,可以直接执行下面的完整命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
echo "== current =="
whoami
which codex || true
which codex-proxy || true
codex-proxy --version || true
ls -l ~/.codex/auth.json 2>/dev/null || echo "no auth.json"

echo "== logout =="
codex-proxy logout || true
mkdir -p ~/.codex_auth_backup
cp ~/.codex/auth.json ~/.codex_auth_backup/auth.json.$(date +%Y%m%d_%H%M%S) 2>/dev/null || true
rm -f ~/.codex/auth.json

echo "== kill old app-server =="
ps -u "$(whoami)" -o pid=,cmd= \
| grep 'codex app-server' \
| grep -v grep \
| awk '{print $1}' \
| xargs -r kill -9
ps -u "$(whoami)" -o pid=,cmd= | grep '[c]odex' || echo "no codex process"

echo "== test proxy =="
curl -I -x http://127.0.0.1:43897 https://api.openai.com

echo "== login new account =="
codex-proxy login --device-auth

echo "== check auth =="
ls -l ~/.codex/auth.json

10. 常见问题

10.1 codex-proxy login --device-auth 无法打开登录页面

可能原因:

  1. 服务器代理不可用。
  2. SSH 反向隧道未启动。
  3. 本地代理端口不正确。
  4. 本地代理软件未开启 HTTP 代理端口。

检查命令:

1
curl -I -x http://127.0.0.1:43897 https://api.openai.com

如果该命令失败,优先修复代理。

10.2 登录后仍然是旧账户

可能原因:

  1. 旧的 auth.json 未删除。
  2. 旧的 codex app-server 进程仍在运行。
  3. 登录时浏览器自动使用了旧账户。

处理方法:

1
2
3
4
5
6
7
8
codex-proxy logout || true
rm -f ~/.codex/auth.json
ps -u "$(whoami)" -o pid=,cmd= \
| grep 'codex app-server' \
| grep -v grep \
| awk '{print $1}' \
| xargs -r kill -9
codex-proxy login --device-auth

登录时建议使用浏览器无痕窗口,确保登录的是新账户。

10.3 auth.json 不存在

可能原因:

  1. device-code 登录没有完成。
  2. 登录过程中网络中断。
  3. 没有使用 codex-proxy login --device-auth
  4. 登录成功后文件写入失败。

重新登录:

1
codex-proxy login --device-auth

10.4 启动 Codex 后网络错误

确认不是直接运行了 codex

1
codex-proxy

同时确认代理可用:

1
curl -I -x http://127.0.0.1:43897 https://api.openai.com

如果 codex-proxy 仍然无法访问网络,可以回到《服务器 Codex 反向代理使用说明》中重新检查 SSH 反向隧道、代理端口和 codex-proxy 包装脚本。

11. 日常使用流程

第一步:本地电脑打开 SSH 反向隧道

1
ssh -N -T -o ExitOnForwardFailure=yes -R 43897:127.0.0.1:7897 your_user@your_server_ip

保持该窗口不关闭。

第二步:登录服务器并进入项目目录

1
ssh your_user@your_server_ip
1
cd /path/to/your/project

第三步:启动 Codex

1
codex-proxy

第四步:需要换号时重新登录

1
2
3
codex-proxy logout
rm -f ~/.codex/auth.json
codex-proxy login --device-auth

12. 最小命令汇总

本地电脑

1
ssh -N -T -o ExitOnForwardFailure=yes -R 43897:127.0.0.1:7897 your_user@your_server_ip

服务器测试代理

1
curl -I -x http://127.0.0.1:43897 https://api.openai.com

服务器切换账户

1
2
3
codex-proxy logout || true
rm -f ~/.codex/auth.json
codex-proxy login --device-auth

服务器启动 Codex

1
codex-proxy

服务器 Codex 反向代理使用说明

1. 使用场景

服务器无法直接访问外网,但本地 Windows 电脑可以通过 v2rayN、Clash 等代理访问外网。

目标链路如下:

1
2
3
4
5
6
7
服务器上的 Codex

服务器 127.0.0.1:代理端口
↓ SSH 反向隧道
本地电脑 127.0.0.1:7897

外网

该配置只在当前用户账户下生效,不修改服务器全局代理,不影响其他 Linux 用户。

2. 前提条件

2.1 本地电脑

本地电脑需要已经有可用代理,例如:

1
127.0.0.1:7897

可在 Windows PowerShell 中测试:

1
curl.exe -I -x http://127.0.0.1:7897 https://www.google.com

如果返回 HTTP/2 200 或类似结果,说明本地代理可用。

2.2 服务器

服务器需要能够通过 SSH 登录:

1
ssh your_user@your_server_ip

3. 在本地电脑建立 SSH 反向代理

在 Windows PowerShell 中执行:

1
ssh -N -T -o ExitOnForwardFailure=yes -R 43897:127.0.0.1:7897 your_user@your_server_ip

参数说明:

1
2
3
4
5
6
-N:只建立隧道,不进入远程 shell
-T:不分配伪终端
-o ExitOnForwardFailure=yes:如果端口转发失败则直接退出
-R 43897:127.0.0.1:7897:
在服务器上监听 127.0.0.1:43897
并转发到本地电脑的 127.0.0.1:7897

执行后窗口看起来会停住,这是正常现象。该窗口用于保持 SSH 隧道连接,不要关闭。

如需中断隧道,在该 PowerShell 窗口按:

1
Ctrl + C

4. 在服务器上测试代理是否可用

另开一个 PowerShell 窗口,登录服务器:

1
ssh your_user@your_server_ip

在服务器中执行:

1
curl -I -x http://127.0.0.1:43897 https://www.google.com

如果出现类似结果:

1
2
3
HTTP/1.1 200 Connection established

HTTP/2 200

说明反向代理已经成功。

也可以测试 OpenAI:

1
curl -I -x http://127.0.0.1:43897 https://api.openai.com

5. 检查服务器端监听端口

在服务器上执行:

1
ss -lntp | grep 43897

正常情况下应看到类似:

1
LISTEN 0 128 127.0.0.1:43897 0.0.0.0:*

重点是监听地址应为:

1
127.0.0.1:43897

不要是:

1
0.0.0.0:43897

127.0.0.1 表示该端口只在服务器本机可访问,不暴露给外部网络。

注意:同一台服务器上的其他 Linux 用户理论上也可能访问 127.0.0.1:43897。因此建议使用随机高位端口,并且不要公开端口号。

6. 安装 Node.js 和 npm

先检查服务器是否已有 Node.js 和 npm:

1
2
node -v
npm -v

如果能正常显示版本号,可以跳过本节。

如果没有 Node.js,且没有 sudo 权限,建议使用 nvm 安装到当前用户目录。

通过代理安装 nvm:

1
2
3
HTTPS_PROXY=http://127.0.0.1:43897 \
HTTP_PROXY=http://127.0.0.1:43897 \
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash

加载 nvm:

1
source ~/.bashrc

安装 Node.js LTS:

1
2
nvm install --lts
nvm use --lts

再次检查:

1
2
node -v
npm -v

7. 配置 npm 只安装到当前用户目录

不要使用系统级全局安装路径,避免影响服务器其他用户。

在服务器中执行:

1
2
mkdir -p ~/.npm-global
npm config set prefix "$HOME/.npm-global"

将当前用户的 npm bin 目录加入 PATH:

1
2
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

检查 npm 全局安装目录:

1
npm config get prefix

期望输出类似:

1
/home/your_user/.npm-global

8. 通过代理安装 Codex CLI

在服务器中执行:

1
2
3
4
HTTPS_PROXY=http://127.0.0.1:43897 \
HTTP_PROXY=http://127.0.0.1:43897 \
ALL_PROXY=http://127.0.0.1:43897 \
npm install -g @openai/codex

安装完成后检查:

1
2
which codex
codex --version

期望结果类似:

1
/home/your_user/.npm-global/bin/codex

如果 which codex 指向 /home/your_user/.npm-global/bin/codex,说明 Codex 安装在当前用户目录下,不影响其他用户。

9. 创建只对 Codex 生效的代理启动脚本

不要把代理变量直接写入 ~/.bashrc,否则当前账户下的其他程序也会默认走代理。

推荐创建单独脚本。注意同时设置大写和小写代理变量,因为 Codex 及其子进程可能读取不同大小写的环境变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mkdir -p ~/bin
cat > ~/bin/codex-proxy <<'EOF'
#!/usr/bin/env bash
set -e

export HTTP_PROXY=http://127.0.0.1:43897
export HTTPS_PROXY=http://127.0.0.1:43897
export ALL_PROXY=http://127.0.0.1:43897
export NO_PROXY=localhost,127.0.0.1,::1

export http_proxy="$HTTP_PROXY"
export https_proxy="$HTTPS_PROXY"
export all_proxy="$ALL_PROXY"
export no_proxy="$NO_PROXY"

exec "$HOME/.npm-global/bin/codex" "$@"
EOF

赋予执行权限:

1
chmod +x ~/bin/codex-proxy

~/bin 加入当前用户 PATH:

1
2
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

检查:

1
which codex-proxy

期望结果:

1
/home/your_user/bin/codex-proxy

9.1 给 Codex app-server 添加代理包装

Codex 可能会自动拉起 codex app-servercodex app-server proxy 子进程。实际使用中,这些子进程不一定继承当前终端手动 export 的代理变量,并且自动启动命令会优先从:

1
~/.local/bin/codex

查找 codex。因此建议额外放一个同样的包装脚本到 ~/.local/bin/codex,让自动拉起的 app-server 也走代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mkdir -p ~/.local/bin
cat > ~/.local/bin/codex <<'EOF'
#!/usr/bin/env bash
set -e

export HTTP_PROXY=http://127.0.0.1:43897
export HTTPS_PROXY=http://127.0.0.1:43897
export ALL_PROXY=http://127.0.0.1:43897
export NO_PROXY=localhost,127.0.0.1,::1

export http_proxy="$HTTP_PROXY"
export https_proxy="$HTTPS_PROXY"
export all_proxy="$ALL_PROXY"
export no_proxy="$NO_PROXY"

exec "$HOME/.npm-global/bin/codex" "$@"
EOF

chmod +x ~/.local/bin/codex

创建后检查:

1
2
~/.local/bin/codex --version
codex-proxy --version

10. 登录 Codex

使用代理脚本启动登录:

1
codex-proxy login

如果支持设备码登录,也可以使用:

1
codex-proxy login --device-auth

服务器无浏览器时,终端一般会显示登录链接或设备码。将链接复制到本地浏览器完成登录即可。

11. 启动 Codex

进入项目目录:

1
cd /path/to/project

启动 Codex:

1
codex-proxy

不要直接运行:

1
codex

因为直接运行 codex 不会自动带上代理环境变量。

12. Docker 容器内使用 Codex

如果 Codex 或开发环境在 Docker 容器内,不要修改 Docker daemon 全局代理。

建议只给自己的容器传入代理变量。

启动容器时:

1
2
3
4
5
6
docker run -it \
--add-host=host.docker.internal:host-gateway \
-e HTTP_PROXY=http://host.docker.internal:43897 \
-e HTTPS_PROXY=http://host.docker.internal:43897 \
-e ALL_PROXY=http://host.docker.internal:43897 \
your_image bash

如果使用 docker compose,只在自己的服务中添加:

1
2
3
4
5
6
7
8
9
services:
dev:
image: your_image
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
HTTP_PROXY: http://host.docker.internal:43897
HTTPS_PROXY: http://host.docker.internal:43897
ALL_PROXY: http://host.docker.internal:43897

不要修改:

1
/etc/docker/daemon.json

否则可能影响服务器其他用户的 Docker 网络行为。

13. 多用户服务器注意事项

13.1 Codex 程序隔离

如果 Codex 安装在:

1
/home/your_user/.npm-global/bin/codex

并且只将路径写入:

1
/home/your_user/.bashrc

则其他用户默认无法直接通过 codex 命令访问你的 Codex。

检查方式:

1
2
which codex
ls -l $(which codex)

期望结果:

1
/home/your_user/.npm-global/bin/codex

13.2 代理端口风险

SSH 反向代理监听在服务器本机:

1
127.0.0.1:43897

外部机器不能直接访问,但同一台服务器上的其他用户理论上可能访问该本机端口。

降低风险的方法:

  1. 使用随机高位端口,例如 5917347281 等。
  2. 不要将代理端口写入公共文档或共享脚本。
  3. 不要将代理环境变量写入全局配置。
  4. 只在 ~/bin/codex-proxy 中使用代理。
  5. 使用完后关闭 SSH 隧道。

例如将端口换为 59173

本地电脑执行:

1
ssh -N -T -o ExitOnForwardFailure=yes -R 59173:127.0.0.1:7897 your_user@your_server_ip

服务器脚本中对应修改为:

1
2
3
export HTTP_PROXY=http://127.0.0.1:59173
export HTTPS_PROXY=http://127.0.0.1:59173
export ALL_PROXY=http://127.0.0.1:59173

14. 权限加固

如果不希望其他用户浏览你的用户目录,可执行:

1
2
chmod 700 ~
chmod 700 ~/.npm-global ~/bin

检查权限:

1
2
ls -ld ~
ls -ld ~/.npm-global ~/bin

15. 常见问题

15.1 PowerShell 执行 SSH 后卡住

正常。

如果使用了:

1
ssh -N -R 43897:127.0.0.1:7897 your_user@your_server_ip

-N 表示只建立隧道,不打开远程 shell,因此不会出现服务器命令行提示符。

该窗口需要保持打开,关闭后隧道断开。

15.2 curl 测试返回 Connection refused

可能原因:

  1. 本地代理端口不是 7897
  2. 本地 v2rayN 或 Clash 没有开启。
  3. SSH 反向隧道没有成功。
  4. 服务器端口 43897 被占用。
  5. 本地代理只监听了其他地址或端口。

检查本地代理端口是否正确。

15.3 curl 测试返回 Could not resolve host

可能是代理没有生效,或者命令没有指定 -x

应使用:

1
curl -I -x http://127.0.0.1:43897 https://www.google.com

15.4 npm install 仍然无法联网

使用显式代理变量安装:

1
2
3
4
HTTPS_PROXY=http://127.0.0.1:43897 \
HTTP_PROXY=http://127.0.0.1:43897 \
ALL_PROXY=http://127.0.0.1:43897 \
npm install -g @openai/codex

必要时也可以设置 npm 代理:

1
2
npm config set proxy http://127.0.0.1:43897
npm config set https-proxy http://127.0.0.1:43897

如果后续不想让 npm 默认走代理,可以删除:

1
2
npm config delete proxy
npm config delete https-proxy

15.5 codex-proxy 提示 command not found

检查 ~/bin 是否加入 PATH:

1
echo $PATH

重新执行:

1
2
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

检查脚本是否存在:

1
ls -l ~/bin/codex-proxy

15.6 codex 命令找不到

检查 Codex 是否安装成功:

1
npm list -g --depth=0

检查 npm 全局 bin 目录:

1
npm bin -g

检查 PATH:

1
echo $PATH

确保包含:

1
/home/your_user/.npm-global/bin

15.7 Codex 主进程有代理,但 codex_apps 或 MCP 仍然失败

如果报错中出现类似:

1
2
3
4
MCP startup failed
codex_apps
https://chatgpt.com/backend-api/wham/apps
tls handshake eof

先确认代理链路本身是否可用:

1
curl -v -x http://127.0.0.1:43897 https://chatgpt.com/backend-api/wham/apps

如果能看到 HTTP/1.1 200 Connection established,并且最终返回 HTTP/2 405 或其他 HTTP 响应,说明代理链路是通的。

此时重点检查是否有旧的无代理 codex app-server 进程残留:

1
ps -u "$(whoami)" -o pid=,cmd= | grep 'codex app-server' | grep -v grep

如果存在旧进程,可以只清理当前用户下的 app-server:

1
2
3
4
5
ps -u "$(whoami)" -o pid=,cmd= \
| grep 'codex app-server' \
| grep -v grep \
| awk '{print $1}' \
| xargs -r kill -9

不要直接使用不带用户限制的 pkill -f codex,多用户服务器上可能匹配到其他人的 Codex 进程并出现无权限错误。

清理后重新运行:

1
codex-proxy

16. 日常使用流程

第一步:本地电脑打开 SSH 隧道

1
ssh -N -T -o ExitOnForwardFailure=yes -R 43897:127.0.0.1:7897 your_user@your_server_ip

保持该窗口不关闭。

第二步:另开终端登录服务器

1
ssh your_user@your_server_ip

第三步:进入项目目录

1
cd /path/to/project

第四步:启动 Codex

1
codex-proxy

第五步:使用结束后关闭隧道

回到保持隧道的 PowerShell 窗口,按:

1
Ctrl + C

17. 最小命令汇总

本地电脑

1
ssh -N -T -o ExitOnForwardFailure=yes -R 43897:127.0.0.1:7897 your_user@your_server_ip

服务器测试代理

1
curl -I -x http://127.0.0.1:43897 https://www.google.com

服务器安装 Codex

1
2
3
4
5
6
7
8
9
mkdir -p ~/.npm-global
npm config set prefix "$HOME/.npm-global"
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

HTTPS_PROXY=http://127.0.0.1:43897 \
HTTP_PROXY=http://127.0.0.1:43897 \
ALL_PROXY=http://127.0.0.1:43897 \
npm install -g @openai/codex

创建 Codex 代理启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mkdir -p ~/bin
cat > ~/bin/codex-proxy <<'EOF'
#!/usr/bin/env bash
set -e

export HTTP_PROXY=http://127.0.0.1:43897
export HTTPS_PROXY=http://127.0.0.1:43897
export ALL_PROXY=http://127.0.0.1:43897
export NO_PROXY=localhost,127.0.0.1,::1

export http_proxy="$HTTP_PROXY"
export https_proxy="$HTTPS_PROXY"
export all_proxy="$ALL_PROXY"
export no_proxy="$NO_PROXY"

exec "$HOME/.npm-global/bin/codex" "$@"
EOF

chmod +x ~/bin/codex-proxy
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

创建 Codex app-server 包装脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mkdir -p ~/.local/bin
cat > ~/.local/bin/codex <<'EOF'
#!/usr/bin/env bash
set -e

export HTTP_PROXY=http://127.0.0.1:43897
export HTTPS_PROXY=http://127.0.0.1:43897
export ALL_PROXY=http://127.0.0.1:43897
export NO_PROXY=localhost,127.0.0.1,::1

export http_proxy="$HTTP_PROXY"
export https_proxy="$HTTPS_PROXY"
export all_proxy="$ALL_PROXY"
export no_proxy="$NO_PROXY"

exec "$HOME/.npm-global/bin/codex" "$@"
EOF

chmod +x ~/.local/bin/codex

启动 Codex

1
codex-proxy

vscode devcontainer中codex插件代理问题记录

问题环境:

  • 系统:Windows + Docker Desktop + VS Code Dev Container
  • 容器:road_ortho
  • 项目路径:/code/road_ortho_code/road_lunwen_code/merge_tiffs_lunwen_code_newest
  • 代理端口:宿主机 7897
  • 容器访问宿主机代理地址:http://host.docker.internal:7897

问题现象:

在 VS Code 的 Codex 插件中反复出现网络请求失败,日志类似:

1
2
3
4
Error fetching error="TypeError: fetch failed" url=https://ab.chatgpt.com/v1/initialize
Error fetching error="TypeError: fetch failed" url=https://chatgpt.com/ces/v1/rgstr
Error fetching error="TypeError: fetch failed" url=/wham/accounts/check
Error fetching error="TypeError: fetch failed" url=/wham/tasks/list?limit=20&task_filter=current

同时还可能出现:

1
unsupported feature enablement `workspace_dependencies`

以及 Git 或 VS Code Remote 日志中出现:

1
2
Failed to connect to 127.0.0.1 port 7897
Failed to establish a socket connection to proxies: PROXY 127.0.0.1:7897

问题原因

核心原因:容器内的 127.0.0.1 不是 Windows 宿主机。

在 Windows 上代理软件监听:

1
127.0.0.1:7897

但是 Codex 插件运行在 Docker 容器中。容器内部访问 127.0.0.1:7897 时,访问的是容器自己,而不是 Windows 宿主机,因此会连接失败。

在 Docker Desktop 环境中,容器访问宿主机应使用:

1
host.docker.internal

因此容器内正确代理地址是:

1
http://host.docker.internal:7897

需要修改的位置

这个问题不是只改一个环境变量即可,因为 VS Code Codex 插件涉及多层进程:

  • VS Code Remote Server
  • VS Code extensionHost
  • OpenAI ChatGPT/Codex 插件
  • Codex app-server
  • Git

所以需要同时处理以下位置。

1. VS Code Remote Server 环境变量

文件:

1
/root/.vscode-server/server-env-setup

内容:

1
2
3
4
5
6
7
8
export HTTP_PROXY=http://host.docker.internal:7897
export HTTPS_PROXY=http://host.docker.internal:7897
export ALL_PROXY=http://host.docker.internal:7897
export http_proxy=http://host.docker.internal:7897
export https_proxy=http://host.docker.internal:7897
export all_proxy=http://host.docker.internal:7897
export NO_PROXY=localhost,127.0.0.1,::1
export no_proxy=localhost,127.0.0.1,::1

2. VS Code Remote settings

文件:

1
/root/.vscode-server/data/Machine/settings.json

需要包含:

1
2
3
4
5
{
"http.proxy": "http://host.docker.internal:7897",
"http.proxyStrictSSL": false,
"http.proxySupport": "off"
}

其中:

  • http.proxy:设置容器可访问的代理地址
  • http.proxySupport: off:避免 VS Code Remote 继续把宿主机的 127.0.0.1:7897 代理注入到容器内部

3. Git 全局代理

在容器中执行:

1
2
git config --global http.proxy http://host.docker.internal:7897
git config --global https.proxy http://host.docker.internal:7897

检查:

1
git config --global --get-regexp proxy

正确结果应类似:

1
2
http.proxy http://host.docker.internal:7897
https.proxy http://host.docker.internal:7897

4. Codex 插件启动脚本

Codex 插件自带的启动文件位置类似:

1
/root/.vscode-server/extensions/openai.chatgpt-版本号-linux-x64/bin/linux-x86_64/codex

需要将其替换为 wrapper,使 Codex app-server 启动时继承正确代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh
export HTTP_PROXY=http://host.docker.internal:7897
export HTTPS_PROXY=http://host.docker.internal:7897
export ALL_PROXY=http://host.docker.internal:7897
export http_proxy=http://host.docker.internal:7897
export https_proxy=http://host.docker.internal:7897
export all_proxy=http://host.docker.internal:7897
export NO_PROXY=localhost,127.0.0.1,::1
export no_proxy=localhost,127.0.0.1,::1
if command -v /usr/local/bin/codex >/dev/null 2>&1; then
exec /usr/local/bin/codex "$@"
fi
exec "$(dirname "$0")/codex.real" "$@"

其中:

  • 原始插件二进制保存为:codex.real
  • 优先使用 npm 安装的稳定版:/usr/local/bin/codex
  • 如果没有 npm 版本,则回退到插件自带的 codex.real

一键修复脚本

已经在项目中保存脚本:

1
/mnt/f/code/road_ortho/road_ortho_code/road_lunwen_code/merge_tiffs_lunwen_code_newest/scripts/fix_codex_vscode_proxy.sh

以后再次出现类似问题,直接在项目根目录执行:

1
./scripts/fix_codex_vscode_proxy.sh

脚本默认参数:

1
2
3
CONTAINER_NAME=road_ortho
PROXY_URL=http://host.docker.internal:7897
RESTART_VSCODE=1

如果容器名或代理端口变化,可以这样运行:

1
CONTAINER_NAME=road_ortho PROXY_URL=http://host.docker.internal:7897 ./scripts/fix_codex_vscode_proxy.sh

如果只想写配置,不想立即停止 VS Code 相关进程:

1
RESTART_VSCODE=0 ./scripts/fix_codex_vscode_proxy.sh

完整脚本代码

文件名:

1
scripts/fix_codex_vscode_proxy.sh

完整内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env bash
set -euo pipefail

CONTAINER_NAME="${CONTAINER_NAME:-road_ortho}"
PROXY_URL="${PROXY_URL:-http://host.docker.internal:7897}"
RESTART_VSCODE="${RESTART_VSCODE:-1}"

if ! docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
echo "Container not found: $CONTAINER_NAME" >&2
echo "Set CONTAINER_NAME=... if your container name changed." >&2
exit 1
fi

echo "Fixing VS Code Codex proxy in container: $CONTAINER_NAME"
echo "Proxy: $PROXY_URL"

docker exec \
-e PROXY_URL="$PROXY_URL" \
-e RESTART_VSCODE="$RESTART_VSCODE" \
"$CONTAINER_NAME" \
sh -s <<'EOF'
set -eu

proxy="${PROXY_URL:-http://host.docker.internal:7897}"
restart="${RESTART_VSCODE:-1}"
vscode_root="/root/.vscode-server"

echo "Writing $vscode_root/server-env-setup"
mkdir -p "$vscode_root"
cat > "$vscode_root/server-env-setup" <<ENVEOF
export HTTP_PROXY=$proxy
export HTTPS_PROXY=$proxy
export ALL_PROXY=$proxy
export http_proxy=$proxy
export https_proxy=$proxy
export all_proxy=$proxy
export NO_PROXY=localhost,127.0.0.1,::1
export no_proxy=localhost,127.0.0.1,::1
ENVEOF
chmod 644 "$vscode_root/server-env-setup"

echo "Writing VS Code machine settings"
mkdir -p "$vscode_root/data/Machine"
python3 - <<PYEOF
import json
from pathlib import Path

p = Path("/root/.vscode-server/data/Machine/settings.json")
try:
data = json.loads(p.read_text())
except FileNotFoundError:
data = {}
except json.JSONDecodeError:
backup = p.with_suffix(p.suffix + ".bak_proxyfix")
backup.write_text(p.read_text())
data = {}

data["http.proxy"] = "$proxy"
data["http.proxyStrictSSL"] = False
data["http.proxySupport"] = "off"
p.write_text(json.dumps(data, indent="\t", ensure_ascii=False))
PYEOF

echo "Writing Git proxy"
git config --global http.proxy "$proxy"
git config --global https.proxy "$proxy"

echo "Patching OpenAI ChatGPT/Codex extension launchers"
for codex_bin in /root/.vscode-server/extensions/openai.chatgpt-*/bin/linux-x86_64/codex; do
[ -e "$codex_bin" ] || continue
codex_dir="$(dirname "$codex_bin")"

if [ ! -f "$codex_dir/codex.real" ]; then
cp "$codex_bin" "$codex_dir/codex.real"
fi

cat > "$codex_bin" <<SH_EOF
#!/bin/sh
export HTTP_PROXY=$proxy
export HTTPS_PROXY=$proxy
export ALL_PROXY=$proxy
export http_proxy=$proxy
export https_proxy=$proxy
export all_proxy=$proxy
export NO_PROXY=localhost,127.0.0.1,::1
export no_proxy=localhost,127.0.0.1,::1
if command -v /usr/local/bin/codex >/dev/null 2>&1; then
exec /usr/local/bin/codex "\$@"
fi
exec "\$(dirname "\$0")/codex.real" "\$@"
SH_EOF
chmod +x "$codex_bin"
echo "Patched: $codex_bin"
done

echo "Current proxy config:"
git config --global --get-regexp proxy || true
cat "$vscode_root/server-env-setup"

if [ "$restart" = "1" ]; then
echo "Stopping VS Code server/extensionHost/Codex app-server processes."
# VS Code will restart these when the window/dev-container reconnects.
pkill -f "openai.chatgpt.*codex" 2>/dev/null || true
pkill -f "codex app-server" 2>/dev/null || true
pkill -f "bootstrap-fork --type=extensionHost" 2>/dev/null || true
pkill -f "out/server-main.js" 2>/dev/null || true
fi
EOF

echo
echo "Done."
if [ "$RESTART_VSCODE" = "1" ]; then
echo "Now run 'Reload Window' in VS Code, or reconnect the Dev Container."
fi

修复后需要做的操作

执行脚本后,需要在 VS Code 中执行:

1
Reload Window

或者重新连接 Dev Container。

原因是:

1
/root/.vscode-server/server-env-setup

只会在新的 VS Code Remote Server / extensionHost 启动时读取。旧进程不会自动更新已经继承的环境变量。

验证方法

查看 Codex 进程

1
docker exec road_ortho sh -lc 'ps aux | grep -E "openai.chatgpt|codex|extensionHost" | grep -v grep'

查看 Codex app-server 环境变量

1
docker exec road_ortho sh -lc 'pid=$(pgrep -f "codex app-server" | head -n 1); tr "\0" "\n" < /proc/$pid/environ | grep -i proxy'

正确结果应包含:

1
2
3
4
HTTP_PROXY=http://host.docker.internal:7897
HTTPS_PROXY=http://host.docker.internal:7897
ALL_PROXY=http://host.docker.internal:7897
NO_PROXY=localhost,127.0.0.1,::1

查看 Git 代理

1
docker exec road_ortho git config --global --get-regexp proxy

正确结果:

1
2
http.proxy http://host.docker.internal:7897
https.proxy http://host.docker.internal:7897

测试容器访问代理

1
docker exec road_ortho curl -I --max-time 8 -x http://host.docker.internal:7897 https://chatgpt.com

如果能返回 HTTP 响应,说明容器可以通过宿主机代理访问外网。

注意事项

unsupported feature enablement workspace_dependencies 不一定是代理问题。

这个错误表示当前 Codex app-server 不支持 VS Code 插件尝试启用的某个 experimental feature。它可能和插件版本、Codex CLI 版本有关。

但是如果同时出现:

1
2
3
TypeError: fetch failed
Failed to connect to 127.0.0.1 port 7897
PROXY 127.0.0.1:7897

则优先处理代理问题。

总结

这个问题的关键不是代理端口本身,而是 Docker 容器中的地址语义:

1
127.0.0.1:7897

在容器中表示容器自己,不是 Windows 宿主机。

因此容器内所有 VS Code、Codex、Git 相关代理都应统一改为:

1
http://host.docker.internal:7897

并在修改后重启 VS Code Remote / Dev Container,使新环境变量生效。

PWA小游戏发布到GitHub Pages并在iPhone离线使用

这里记录一个完整流程:把一个普通的 HTML/CSS/JS 小功能改造成 PWA 离线应用,发布到 GitHub Pages,然后在 iPhone Safari 中“添加到主屏幕”。第一次联网打开后,后面断网也可以从主屏幕图标进入。

示例地址:

1
https://ouwenwu.github.io/minesweeper/

其中 minesweeper 是仓库名。以后换成自己的项目时,把它替换为自己的仓库名即可。

一、核心思路

PWA 不需要打包成 iOS 安装包,也不需要 Xcode。它的核心是:

  • 用 HTML/CSS/JS 写好网页功能
  • 加一个 manifest.webmanifest,让系统知道应用名称、图标、启动方式
  • 加一个 service-worker.js,缓存页面、JS、CSS、图标等资源
  • 第一次通过 HTTPS 访问 GitHub Pages
  • 在 iPhone Safari 中点击“分享” -> “添加到主屏幕”
  • 之后从主屏幕图标打开,已经缓存的资源可以离线使用

注意:第一次必须联网访问一次,离线能力是在第一次访问时缓存下来的。

二、项目结构

一个最小结构可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
my-game/
├── index.html
├── package.json
├── public/
│ ├── manifest.webmanifest
│ ├── service-worker.js
│ └── icons/
│ ├── icon-192.png
│ ├── icon-512.png
│ └── apple-touch-icon.png
├── src/
│ ├── main.js
│ └── styles.css
└── vite.config.js

如果不是 Vite 项目,也可以直接用静态 HTML,只要最后发布出去的是 index.htmlmanifest.webmanifestservice-worker.js 和相关资源即可。

三、添加 manifest.webmanifest

public/manifest.webmanifest 中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "扫雷",
"short_name": "扫雷",
"description": "可离线游玩的扫雷小游戏",
"start_url": "./",
"scope": "./",
"display": "standalone",
"background_color": "#c8d2e1",
"theme_color": "#f3f6fb",
"orientation": "portrait",
"icons": [
{
"src": "./icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "./icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

图标至少准备:

  • public/icons/icon-192.png
  • public/icons/icon-512.png
  • public/icons/apple-touch-icon.png

其中 apple-touch-icon.png 是 iPhone 添加到主屏幕后显示的图标,建议使用 180x180 或更高分辨率的 PNG。

四、在 index.html 中引入 PWA 配置

index.html<head> 中加入:

1
2
3
4
5
6
7
8
<meta name="theme-color" content="#f3f6fb" />
<meta name="description" content="可离线游玩的扫雷小游戏" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="扫雷" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="manifest" href="./manifest.webmanifest" />
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png" />

这里使用 ./manifest.webmanifest./icons/...,是为了让项目部署到 GitHub Pages 的子路径时也能正常加载。

五、添加 service-worker.js

public/service-worker.js 中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const CACHE_NAME = 'minesweeper-pwa-v1';
const APP_SHELL = [
'./',
'./index.html',
'./manifest.webmanifest',
'./icons/icon-192.png',
'./icons/icon-512.png',
'./icons/apple-touch-icon.png',
];

self.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then(async (cache) => {
const shellUrls = APP_SHELL.map((path) => new URL(path, self.registration.scope));
await cache.addAll(shellUrls);

const indexUrl = new URL('./index.html', self.registration.scope);
const indexResponse = await fetch(indexUrl);
const indexHtml = await indexResponse.clone().text();
await cache.put(indexUrl, indexResponse);

const assetUrls = [...indexHtml.matchAll(/(?:src|href)="([^"]+)"/g)]
.map((match) => new URL(match[1], indexUrl))
.filter((url) => url.origin === self.location.origin);

await cache.addAll(assetUrls);
})
.then(() => self.skipWaiting()),
);
});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((cacheNames) =>
Promise.all(cacheNames.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name))),
)
.then(() => self.clients.claim()),
);
});

self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return;

event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) return cachedResponse;

return fetch(event.request)
.then((networkResponse) => {
const shouldCache =
networkResponse.ok &&
new URL(event.request.url).origin === self.location.origin &&
['document', 'script', 'style', 'image', 'manifest'].includes(event.request.destination);

if (shouldCache) {
const responseCopy = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, responseCopy));
}

return networkResponse;
})
.catch(() => caches.match(new URL('./index.html', self.registration.scope)));
}),
);
});

CACHE_NAME 是缓存版本号。以后修改了 JS、CSS、图标或缓存策略,建议把它改成新的名字,例如:

1
const CACHE_NAME = 'minesweeper-pwa-v2';

这样 iPhone 下次联网打开时会更新缓存。

六、注册 service worker

在入口 JS 中加入:

1
2
3
4
5
6
7
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register(`${import.meta.env.BASE_URL}service-worker.js`).catch((error) => {
console.info('Service worker registration failed:', error);
});
});
}

如果不是 Vite 项目,可以写成:

1
2
3
4
5
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./service-worker.js');
});
}

七、Vite 项目配置相对路径

GitHub Pages 的项目站点通常是子路径,例如:

1
https://ouwenwu.github.io/minesweeper/

为了避免资源路径从域名根目录开始找,Vite 项目建议加 vite.config.js

1
2
3
4
5
import { defineConfig } from 'vite';

export default defineConfig({
base: './',
});

这样构建后的 JS、CSS 会使用相对路径,部署到 /minesweeper/ 这种子路径也能正常访问。

八、本地构建验证

安装依赖:

1
npm install

构建:

1
npm run build

本地预览:

1
npm run preview

确认 dist/ 中有这些文件:

1
2
3
4
5
6
dist/index.html
dist/manifest.webmanifest
dist/service-worker.js
dist/icons/icon-192.png
dist/icons/icon-512.png
dist/icons/apple-touch-icon.png

九、创建 GitHub 仓库

新建一个独立仓库,例如:

1
minesweeper

不要直接放到 ouwenwu.github.io 这个 Hexo 根站仓库里,避免影响原来的博客。独立仓库发布后访问地址是:

1
https://ouwenwu.github.io/minesweeper/

十、提交源码到 main 分支

在项目目录中执行:

1
2
3
4
5
6
git init
git add .
git commit -m "Add minesweeper PWA"
git branch -M main
git remote add origin https://github.com/ouwenwu/minesweeper.git
git push -u origin main

如果你的仓库名不是 minesweeper,把命令里的仓库地址改成自己的:

1
git remote add origin https://github.com/你的用户名/你的仓库名.git

十一、把 dist 发布到 gh-pages 分支

先确保已经构建:

1
npm run build

然后把 dist/ 单独推送到 gh-pages 分支:

1
2
git subtree split --prefix dist -b gh-pages
git push -u origin gh-pages

如果之前已经有本地 gh-pages 分支,更新时可以先删除本地临时分支再重新生成:

1
2
3
git branch -D gh-pages
git subtree split --prefix dist -b gh-pages
git push origin gh-pages --force

这里的 gh-pages 分支只放构建后的静态文件,main 分支保留源码。

十二、配置 GitHub Pages

进入仓库:

1
https://github.com/ouwenwu/minesweeper

然后:

  1. 点击 Settings
  2. 左侧点击 Pages
  3. Source 选择 Deploy from a branch
  4. Branch 选择 gh-pages
  5. 文件夹选择 / (root)
  6. 点击 Save

等待一两分钟后访问:

1
https://ouwenwu.github.io/minesweeper/

如果能打开页面,说明 GitHub Pages 已经发布成功。

十三、iPhone 添加到主屏幕

在 iPhone 上操作:

  1. 用 Safari 打开 https://ouwenwu.github.io/minesweeper/
  2. 等页面完整加载
  3. 点击 Safari 底部的“分享”按钮
  4. 选择“添加到主屏幕”
  5. 名称可以保持“扫雷”
  6. 点击“添加”

之后桌面会出现一个类似 app 的图标。点击它会以独立窗口打开,不像普通网页那样显示 Safari 地址栏。

十四、离线测试

第一次添加后,建议联网状态下从主屏幕图标打开一次,停留几秒,让 service worker 完成安装和缓存。

然后测试:

  1. 关闭 Wi-Fi 和蜂窝网络
  2. 从 iPhone 主屏幕点击应用图标
  3. 如果页面正常打开,说明离线缓存成功

如果第一次离线打不开,通常是缓存还没安装完成。重新联网打开一次,停留几秒,再断网测试。

十五、更新版本

修改代码后,按这个流程更新:

1
2
3
4
5
6
7
npm run build
git add .
git commit -m "Update PWA"
git push origin main
git branch -D gh-pages
git subtree split --prefix dist -b gh-pages
git push origin gh-pages --force

如果改动涉及缓存资源,记得更新 public/service-worker.js 里的缓存名:

1
const CACHE_NAME = 'minesweeper-pwa-v2';

iPhone 下次联网打开后会拿到新缓存。如果仍然看到旧页面,可以删除主屏幕图标后重新添加,或者在 Safari 中清理网站数据后再访问。

十六、常见问题

1. 为什么不能直接把源码作为 Pages 发布?

Vite 项目的开发版 index.html 通常引用 /src/main.js,浏览器不能像 Vite dev server 那样直接处理模块和打包。因此应发布 npm run build 生成的 dist/

2. 为什么推荐独立仓库?

因为 ouwenwu.github.io 已经是 Hexo 博客根站。小游戏放到独立仓库后,会发布到:

1
https://ouwenwu.github.io/minesweeper/

这样不会影响博客首页:

1
https://ouwenwu.github.io/

3. iPhone 能不能完全不联网使用?

第一次必须联网打开一次,因为需要下载网页资源并安装 service worker。只要缓存成功,后面就可以离线打开已经缓存的应用。

4. 这是不是 iOS 原生 app?

不是。它是 PWA,本质上还是网页应用,但可以添加到主屏幕,并且支持离线缓存。优点是不需要 Xcode 和 Apple 开发者账号;缺点是不能使用所有原生 iOS 能力。

十七、最终效果

完成后,可以把这个地址分享给别人:

1
https://ouwenwu.github.io/minesweeper/

对方在 iPhone Safari 打开后,也可以通过“分享” -> “添加到主屏幕”安装成一个离线可用的小应用。

高斯泼溅形状

为什么是椭球

一维高斯分布函数

一维高斯分布(Gaussian Distribution),通常被称为正态分布(Normal Distribution),其概率密度函数(PDF)的数学方程如下:

当概率取某个值时,x也对应的为一个常数值

二维高斯分布函数

二维高斯分布(Bivariate Gaussian Distribution 或 2D Normal Distribution)是一维高斯分布在二维平面上的推广。它的图像不再是一条平面曲线,而是一个三维空间中的“山包”或“钟形曲面”。

矩阵形式:若二维随机向量 $\mathbf{x} = [x, y]^T$ 服从均值向量为 $\mathbf{\mu}$,协方差矩阵为 $\mathbf{\Sigma}$ 的正态分布,其概率密度函数为:

其中:

  • 是二维变量
  • 是均值向量,决定了中心点的位置
  • $\mathbf{\Sigma} = \begin{bmatrix} \sigma_x^2 & \rho\sigma_x\sigma_y \ \rho\sigma_x\sigma_y & \sigma_y^2 \end{bmatrix}$是协方差矩阵,决定了分布的形状、方向和散度

当概率取某个常数值时:

此时xy的分布是一个椭圆,当概率值从[0,1]发生变化时,则x和y组成很多个椭圆,整体来说构成一个实心的椭圆面

三维高斯分布函数

在二维中,高斯分布是一个平面上的“山包”(等高线是椭圆)。 到了三维中,高斯分布描述的是三维空间中的一个“云团”“雾状球体”。它的等值面(密度相同点的集合)是三维空间中的椭球体 (Ellipsoid)

设三维随机向量 $\mathbf{x} = [x, y, z]^T$,其概率密度函数为:

其中:

  • $\mathbf{x}$ - 输入变量 (Input Vector): 三维空间中的一个点。 $\mathbf{x} = \begin{bmatrix} x \cr y \cr z \end{bmatrix}$

  • $\mathbf{\mu}$ - 均值向量 (Mean Vector): 这个三维“云团”中心点的位置坐标。 $\mathbf{\mu} = \begin{bmatrix} \mu_x \cr \mu_y \cr \mu_z \end{bmatrix}$

  • $\mathbf{\Sigma}$ - 3x3 协方差矩阵 (Covariance Matrix):

    这是最核心的部分,它定义了椭球体的形状、大小和方向。

    • 对角线元素 ($\sigma_x^2, \sigma_y^2, \sigma_z^2$):也就是方差,决定了椭球体沿 XYZ 三个轴向的拉伸程度(“胖瘦”)。
    • 非对角线元素 ():也就是协方差,表示了分布的x轴、y轴、z轴之间的关系,决定了椭球体在空间中的旋转姿态。如果它们全为 0,说明分布的xyz是独立的,椭球体的轴就与坐标轴平行。

    同时可以用来表示分布的特性

    • 各向同性:在各个方向具有相同的扩散程度(梯度)$\mathbf{\Sigma} = \begin{bmatrix} \sigma^2 & 0 & 0 \cr 0 & \sigma^2 & 0 \cr 0 & 0 & \sigma^2 \end{bmatrix}$,三个方向的方差相同,互不相关,则表示为一个正球体
    • 各向异性:在各个方向的均值不同,且方向相关(进行了旋转),因此高斯椭球具有各向异性,可以表示空间中的各种分布

当概率值取某个常数时$(\mathbf{x} - \mathbf{\mu})^T \mathbf{\Sigma}^{-1} (\mathbf{x} - \mathbf{\mu})=constant$,x的分布为椭球体,则当概率从[0,1]发生变化时,组成很多个椭球体,则所有的分布构成一个实心的椭球,这个就是高斯椭球

椭球形状的控制

上面已经推断出协方差矩阵控制了椭球的中心和各方向上的方向,根据椭球在空间上可以通过球进行仿射变换得到推出椭球的协方差矩阵可以通过球的协方差矩阵通过变换得到,即任意高斯可以看作标准高斯通过仿射变换得到。

对于高斯分布,假设其每个x均经过的仿射变换,那么其新的分布为

仿射变换可以分解为旋转和缩放,即$A=RS$,,反之,通过协方差矩阵之后可以通过特征值分求出R和S,即将高斯椭球的协方差矩阵用R、S进行表示,R作为旋转矩阵又可以通过四元数q进行表示,即最终的椭球,我们通过q、s进行表示

所以对于椭球的形状共有4元数+缩放3+中心3共10个参数

椭球的颜色

球谐函数

要理解球谐函数,你只需要理解一个概念:傅里叶变换(Fourier Transform)

  1. 一维的情况(傅里叶级数): 想象一段复杂的声波(音乐)。我们知道,这段复杂的波形其实是由无数个简单的正弦波(Sine waves)叠加而成的。
    • 低频正弦波决定了整体的大调。
    • 高频正弦波决定了细节和音色。
    • 结论: 任何一维周期函数,都可以分解为一组正弦波的加权和。
  2. 二维的情况(球谐函数): 现在,把目光从“一根线(声波)”移到“一个球(地球仪或灯光球)”上。 如果我想描述一个球体表面的颜色分布、凹凸起伏或者是围绕球体的光照强度,该怎么办?
    • 球谐函数就是定义在球表面上的“正弦波”
    • 结论: 任何定义在球表面上的函数(比如地球的地形、环境光照),都可以分解为一组球谐函数的加权和。

一句话总结定义: 球谐函数就是球面上的基函数(Basis Functions),就像积木一样,通过组合不同形状的积木(球谐函数),你可以拼出任何复杂的球面信号。

球谐函数公式

对于一个球谐函数有4个输入分别代表

  • : 指定的角度,也就是一个球谐函数可以求出空间中任意角度的值y,y代表的是这个球谐函数在这个角度的l,即长度
  • l(Degree, 阶数): 代表频率或细节程度、l越大球表面的褶皱越多,能表现的细节越明显。l=0是光滑的球;
  • m(order,阶次): 代表方向和纹理分布,范围m为整数,且. 正负代表了是否旋转90度

部件的作用:

  • :归一化常数,使得l和m不管怎么变,球谐函数在球面上的积分都等于1
  • :伴随勒让德多项式,这是核心形状生成器,专门负责切分南北(纬度)方向的形状

下面是l取3时m的所有取值所得出的表面图像:

m_3

椭球的颜色

我们要让椭球的颜色真实,就不能让椭球的颜色在各个角度看都意义,因此引入球谐函数,因为球谐函数恰好定义了每个不同的角度的值,我们可以将这个值作为颜色,也就是上图中不同方向的值不一样,代表颜色的占比不必要。

但是不能用一个球谐函数去代表颜色,因为实际上各个方向的并不规则,与傅里叶变换一样,可以用多个正余弦函数去拟合场景中的任意波形,因此,可以考虑多个球谐函数的叠加,去拟合空间中的任意椭球,也就是颜色的公式如下:

在高斯椭球颜色的渲染中,选择3阶即l等于3,其中的每个y都可以通过公式计算出来,所有的c为参数。且可以得出对于l,他的参数为2*l+1当最高取3时,所有的参数量为,同时,这仅能代表一个颜色值,对于真实的RGB,则需要个参数

高斯椭球参数

  • 形状参数:4个四元数+3个缩放+3个中心位置参数 =10
  • 颜色参数:16·3=48
  • 不透明度:1个$\alpha$

一共10+48+1=59个参数

简易刷铁机的制作

原料:

任意方框: 泥土、玻璃、石头均可以

收集装置

首先搭建下面的装置,一格深的装置,两个小箱子组成的大箱子——箱子*2 方块*15

收集1

接下来放置9个漏斗,漏斗连接到箱子上,方法是shift+右键点击对应方块,即可将漏斗连接到方块上、将红色线圈内的漏斗连接到箱子,其余漏斗依次连接

检验是否安装成果: 向漏斗里面丢东西,最后可以在箱子里面找到

漏斗安装

铁傀儡处死装置

在收集装置基础上向上搭建20格,并形成通道,部分区域可简化,以节省方块,当然也可以想底下挖,将收集装置位于-20处以节省方块,需要方块16*20=320,告示牌*9

玻璃通道

在距收集装置2格处放置告示牌,悬浮告示牌放置需要将光标指向告示牌右侧 shift+右键单机(下面的截图均高了一格)

告示牌放置方法

告示牌安置结果

在告示牌安置上一个放置岩浆,上图红框范围

岩浆放置结果

铁傀儡生成

搭建铁傀儡生成后的移动平台,利用水流将铁傀儡推到处死装置里

搭建9*9的方格,并在方格外侧添加围栏,挡住水的流动

铁傀儡流动平台

先在中心添加栅栏门,挡住水流向下的移动,在四个角的方块上放置水源,将铁傀儡向中心推动

![水流装置](水流装置.p

僵尸放置轨道

在水流上方四格处,放置一个横着的9格,并搭建为如图所示,其中最上方为半砖

僵尸行动轨道

在僵尸过道两侧建立有3*3*2空间的空间的村民房

村民房

放入床并封顶,注意床要顶格放,否则僵尸吓不到村民,如果夜晚村民睡觉了,说明距离太远了,以及注意床的方向,床的方向不对,村民不会睡觉

村民床

在最底下可以放置灵魂篝火,以加快效率

灵魂篝火

注意:

铁傀儡生成规则

本篇介绍我的世界的各种特性

铁傀儡刷新机制:村民被吓+区域内5个以上村民,僵尸吓村民表现为晚上上床后立马起床

一、ContextCapture先降低图像分辨率做空三再恢复继续做

首先下采样图像分辨率,一般为30%

下降分辨率

提交空三

image-20250804100937058

二、已有图像初始位姿,计算准确位姿

针对于:手持激光扫描仪由于时间同步或标定或者其他问题导致的图像位姿不准,但已有较好的初值,需要计算更为准确的位姿

将初始位姿转为blockexchange 的xml格式

这个格式是COntextCapture、Photoscan等软件的通用格式

image-20250804142715704

contextCapture导入xml结果

在已有位姿上提交空三

contextCapture提交空三

最后导出block,在metashape中生成稀疏点云即可