@twinsearth/dsh-2xray 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -2
  4. package/bin/README.md +27 -0
  5. package/cordis.patch.yml +37 -0
  6. package/docs/AC-0.md +60 -0
  7. package/docs/EVIDENCE.md +135 -0
  8. package/examples/config.downgrade.json +20 -0
  9. package/examples/config.example.json +22 -0
  10. package/examples/config.multi.downgrade.json +39 -0
  11. package/examples/config.multi.json +46 -0
  12. package/icon.svg +9 -0
  13. package/index.js +89 -0
  14. package/lib/binary.js +133 -0
  15. package/lib/config.js +401 -0
  16. package/lib/control/client.js +100 -0
  17. package/lib/control/grpc.js +121 -0
  18. package/lib/control/protobuf.js +143 -0
  19. package/lib/health.js +109 -0
  20. package/lib/log.js +20 -0
  21. package/lib/paths.js +65 -0
  22. package/lib/process.js +154 -0
  23. package/lib/route.js +299 -0
  24. package/lib/share-link.js +132 -0
  25. package/lib/supervisor.js +687 -0
  26. package/lib/tools.js +106 -0
  27. package/lib/xray-config.js +225 -0
  28. package/locale/en.json +6 -0
  29. package/locale/zh.json +6 -0
  30. package/package.json +42 -4
  31. package/scripts/check-config.mjs +56 -0
  32. package/scripts/parse-share-link.mjs +23 -0
  33. package/scripts/probe-routing.mjs +66 -0
  34. package/scripts/release-check.mjs +104 -0
  35. package/test/binary.test.js +103 -0
  36. package/test/config.test.js +106 -0
  37. package/test/control.test.js +166 -0
  38. package/test/health.test.js +75 -0
  39. package/test/index.test.js +52 -0
  40. package/test/multi-node.test.js +111 -0
  41. package/test/route.test.js +158 -0
  42. package/test/share-link.test.js +113 -0
  43. package/test/supervisor.test.js +525 -0
  44. package/test/tools.test.js +88 -0
  45. package/test/xray-config.test.js +132 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-29
4
+
5
+ 首个发布版本(npm:`@twinsearth/dsh-2xray`)。
6
+
7
+ ### 核心能力
8
+
9
+ - **xray-core 进程托管**:优先 `ctx.subprocess`(Windows Job Object),缺失时退化为 `child_process`;固定端口、占用即失败、不静默换端口。
10
+ - **进程内流量接管**:调用 DSH 的 `@deepseek-ai/dsh-http-proxy` 策略入口 `installProxyFromEnvironment()`,让全局 `fetch` 与 `web_fetch` 读取同一份策略;不可用时退回内置 undici dispatcher 并显式标记 `web_fetch` 未覆盖。
11
+ - **就绪门禁**:TCP 可连 → 经代理 204 功能探测 → 才安装 dispatcher;停止时先摘路由再 terminate。
12
+ - **状态机**:`idle/resolving/starting/ready/degraded/backoff/failed/stopped`,写入 `status.json`(0600,不含密钥);退出码 23 与端口占用不自动重启。
13
+ - **VLESS + XTLS Vision + REALITY**:字段级校验(flow×security 组合、shortId、base64url 公钥、回环入站),`connIdle=600` 避免干净 FIN 导致的 `STREAM_CLOSED`。
14
+ - **多节点与运行时切换**:预置全部 outbound + balancer,切换走 `RoutingService.OverrideBalancerTarget`,不重启进程;零依赖 gRPC(`node:http2` + 手写 protobuf)。
15
+ - **模型工具**:`xray_status`、`xray_validate_config`、`xray_switch_node`、`xray_stats`(`ctx.inject(['tools'])` 可选注册)。
16
+ - **配置导入**:`vless://` 分享链接(`shareLink` / `shareLinks`)解析,支持二维码里同样的文本;`vmess/trojan/ss` 明确报不支持。
17
+
18
+ ### 验证
19
+
20
+ - 单元测试 96/96(`node --test --test-isolation=none test/*.test.js`)。
21
+ - 真实 DSH 运行时:`fetch` 经代理(absolute-form)、回环直连(origin-form)、恢复后回到直连。
22
+ - 真实 xray 1.4.5:`StatsService.QueryStats` 调用成功;`RoutingService` 返回 `12 unknown service`(版本边界,需 2024-09+)。
23
+
24
+ ### 已知限制
25
+
26
+ - 未与 Xray v26 真实二进制、真实服务端做过端到端验收;REALITY 字段靠源码规格保证。
27
+ - 设置页(含扫码导入)未实现;配置通过 `cordis.patch.yml`。
28
+ - 不下载 xray 二进制;可由发布物自带 `bin/xray`,但受 MPL-2.0 约束。
29
+ - builtin/SOCKS 模式下 `web_fetch` 不在接管范围。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TwinsEarth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,329 @@
1
- # Temporary Holding Version
1
+ # DSH-2Xray
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 一个 DSH bundle 插件,扮演 **xray-core 进程管理者 + 进程内流量路由者** 的双重角色:
4
+
5
+ 1. 生成 xray-core 客户端配置,启动并守护 `xray` 子进程;
6
+ 2. 在 xray 本地入站**通过真实探测之后**,把 DSH 进程的全局 HTTP(S) 出口接到该入站;
7
+ 3. 可选:通过零依赖 gRPC 控制面做多节点运行时切换与流量统计。
8
+
9
+ ```
10
+ apply(ctx, config)
11
+ │
12
+ ├─ config.js 配置归并 + 字段级校验(失败不进 failed 以外的状态)
13
+ ├─ xray-config.js 纯函数:多节点 outbound + balancer + VLESS Vision REALITY
14
+ ├─ supervisor.js 状态机:启动 → 探活 → 接管 → 守护 → 回收/退避
15
+ │ ├─ binary.js 解析 xray(插件 bin/ → 数据目录 → PATH)、探测版本
16
+ │ ├─ process.js ctx.subprocess(Windows Job Object)/ child_process 兜底
17
+ │ ├─ health.js TCP 探活 + 经代理的功能探测
18
+ │ ├─ route.js 与 DSH 代理策略层协作,或内置 undici dispatcher 兜底
19
+ │ └─ control/ 零依赖 gRPC 控制面(HTTP/2 + 手写 protobuf)
20
+ ├─ tools.js xray_status / xray_switch_node / xray_stats(可选)
21
+ └─ paths.js 数据目录、原子写、哈希(只记录配置哈希,不记录密钥)
22
+ ```
23
+
24
+ ## 关键设计决策(以及为什么不是骨架里的写法)
25
+
26
+ | 主题 | 结论 | 原因(已在 DSH 0.2.1-rc.3 上核实) |
27
+ |---|---|---|
28
+ | 全局接管方式 | **调用 `@deepseek-ai/dsh-http-proxy` 的 `installProxyFromEnvironment()`**,不自己 `setGlobalDispatcher()` | DSH 在第一个插件挂载前就装好了策略 dispatcher;`web_fetch` 只问 `proxyRouteFor()`。裸装 dispatcher 会造成「模型 API 进代理、web_fetch 仍直连」的双源真值分裂。 |
29
+ | 入站协议 | 默认 **HTTP**,SOCKS 只作为可选附加 | DSH 代理策略明确拒绝 `socks5://` URL(诊断为 “connecting directly”)。SOCKS 只能靠内置 dispatcher,且失去 `web_fetch` 覆盖。 |
30
+ | 清理钩子 | `ctx.effect(() => disposer)`,`apply` 返回它 | Cordis 没有 `ctx.on('dispose')`;`ctx.effect` 与 `apply` 返回值才是生命周期 seam。 |
31
+ | 子进程 | 优先 `ctx.subprocess` | Windows 上是 Job Object(`KILL_ON_JOB_CLOSE`)。Electron 关闭时对运行时发 `SIGTERM` ≈ `TerminateProcess`,cordis dispose 可能根本不执行;没有 Job Object 就会留下孤儿 xray 占着端口。 |
32
+ | 就绪顺序 | **先起进程 → TCP 探活 → 功能探测 → 才接管** | 反过来会出现「dispatcher 指向尚未监听的端口」,整个 DSH 的 fetch 全部 `ECONNREFUSED`。 |
33
+ | 停止顺序 | **先摘路由 → 再 terminate → 等退出** | 反过来存在「进程已死、dispatcher 还指着它」的窗口。 |
34
+ | 端口 | 固定端口 + 占用即失败,**不静默换端口** | 换端口后 DSH 仍指向旧端口,表现为持续 `ECONNREFUSED`,且用户无从判断。 |
35
+ | 多节点切换 | 预置全部 outbound + balancer + `RoutingService.OverrideBalancerTarget` | `HandlerService.AddOutbound` 要传 protobuf 编码的 `OutboundHandlerConfig`(TypedMessage sender/proxy settings);override 只传两个字符串,是最小可靠面。 |
36
+ | 控制面实现 | 零依赖:`node:http2` + 手写 protobuf 编解码 | 为一个 balancer 切换 + 统计查询给每个 profile 装 `@grpc/grpc-js` + proto-loader 不划算,也扩大供应链面。 |
37
+ | 重启 | 只对「意外崩溃」退避重启 | 退出码 23(配置错)不重启;端口占用不重启;上游不可达不重启 xray(进程是健康的)。 |
38
+ | 工具注册 | `ctx.inject(['tools'])` 可选注册 | 若声明 `inject = ['tools','settings']`,缺少任一服务的 profile 会整行不激活;代理本身不应该被工具可用性拖累。 |
39
+
40
+ ## 安装
41
+
42
+ 插件是一个 **Host-only bundle**,无需构建;除控制面外无运行时依赖。
43
+
44
+ 1. 安装:`npm i @twinsearth/dsh-2xray` 后由插件管理器启用;或在 DSH 插件页选择「从目录安装」,指向 `E:\GPT\dsh-2xray`。
45
+ 2. 把 xray 可执行文件放到下面任一位置(按解析优先级):
46
+ - 插件自己的 `bin/xray(.exe)`(随 bundle 分发时用;见 [bin/README.md](bin/README.md) 的 MPL-2.0 说明)
47
+ - 配置 `binary.path`
48
+ - 环境变量 `DSH_XRAY_BIN`
49
+ - `<DSH_HOME>\xray-proxy\bin\xray.exe`(`DSH_HOME` 默认 `%USERPROFILE%\.dsh`)
50
+ - 放进 `PATH`
51
+ 3. 编辑 profile 的 `cordis.patch.yml`(或 bundle 默认配置),填入 `server.*` 或 `nodes[]`,把 `enabled` 改成 `true`,重启 profile。
52
+
53
+ bundle 自带的 `cordis.patch.yml` 默认 `enabled: false`,避免安装即报错;它是**整块配置**,profile 层覆盖时必须重述要保留的字段。注册行的 `name` 必须等于包名(`@twinsearth/dsh-2xray`),否则 patch 会按「name mismatch」跳过。
54
+
55
+ ```yaml
56
+ - id: xray-proxy
57
+ name: '@twinsearth/dsh-2xray'
58
+ config:
59
+ enabled: true
60
+ server:
61
+ address: example.com
62
+ port: 443
63
+ uuid: 00000000-0000-0000-0000-000000000000
64
+ flow: xtls-rprx-vision
65
+ serverName: www.example.com
66
+ password: REPLACE_WITH_43_CHAR_BASE64URL_X25519_PUBLIC_KEY
67
+ shortId: ''
68
+ fingerprint: chrome
69
+ spiderX: /
70
+ inbound:
71
+ protocol: http
72
+ host: 127.0.0.1
73
+ port: 20808
74
+ takeover: auto
75
+ ```
76
+
77
+ ## 配置参考
78
+
79
+ | 字段 | 默认 | 说明 |
80
+ |---|---|---|
81
+ | `enabled` | `false` | 总开关 |
82
+ | `server.address` | — | 服务端域名或 IP(旧版单节点写法;多节点用 `nodes`) |
83
+ | `server.port` | `443` | 服务端端口 |
84
+ | `server.uuid` | — | VLESS uuid;非标准 UUID 但 ≤30 字节也可 |
85
+ | `server.flow` | `xtls-rprx-vision` | `""` / `xtls-rprx-vision` / `xtls-rprx-vision-udp443`;非空时 network 必须是 raw/tcp 且 security 必须是 reality/tls(`-test` 不校验,插件静态拦截) |
86
+ | `server.serverName` | — | REALITY/TLS 的 SNI |
87
+ | `server.password` | — | REALITY 公钥(当前字段名),43 字符 base64url |
88
+ | `server.publicKey` | — | REALITY 公钥旧字段名;与 `password` 同时写可跨版本,至少提供一个 |
89
+ | `server.shortId` | `""` | 偶数长度 hex,≤16 字符 |
90
+ | `server.fingerprint` | `chrome` | uTLS 指纹 |
91
+ | `server.spiderX` | `/` | 以 `/` 开头 |
92
+ | `server.network` | `raw` | `raw` / `tcp`(后者归一为 raw) |
93
+ | `server.security` | `reality` | `reality` / `tls` / `none` |
94
+ | `nodes` | `[]` | 节点数组;与 `server.*` 二选一。每项支持 `id`/`tag` 与全部 `server.*` 字段 |
95
+ | `shareLink` | `""` | 粘贴一条 `vless://` 分享链接(二维码里就是这段文本),解析后追加为节点 |
96
+ | `shareLinks` | `[]` | 多条分享链接;优先于 `shareLink`,与显式 `nodes` 合并(显式节点在前) |
97
+ | `activeNode` | 第一个节点 | 启动时钉住的 node id 或 outbound tag |
98
+ | `inbound.protocol` | `http` | `http` 或 `socks`;socks 只建议给非 Node 客户端 |
99
+ | `inbound.host` | `127.0.0.1` | 只允许回环地址 |
100
+ | `inbound.port` | `20808` | 本地入站端口 |
101
+ | `inbound.socks.enabled` | `false` | 额外开 SOCKS 入站 |
102
+ | `inbound.socks.port` | `20809` | 不能与 HTTP 入站相同 |
103
+ | `inbound.socks.udp` | `false` | undici 只实现 SOCKS CONNECT,开启只服务其它程序 |
104
+ | `takeover` | `auto` | `auto`:已有代理策略时不覆盖;`force`:强制覆盖;`off`:只起 xray 不接管 |
105
+ | `control.enabled` | `false` | 开启 gRPC 控制面(`api` 入站 + services) |
106
+ | `control.host` / `control.port` | `127.0.0.1` / `10085` | 只允许回环;端口不能与任何入站冲突 |
107
+ | `control.services` | Handler/Stats/Routing | 传给 xray 的 `api.services` |
108
+ | `control.stats` | `true` | 打开 `stats` 与 `policy.system` 流量计数 |
109
+ | `control.balancerTag` | `active` | 多节点 balancer 的 tag,也是切换目标 |
110
+ | `control.timeoutMs` | `3000` | 每次 gRPC 调用超时 |
111
+ | `noProxy` | `""` | 追加的 bypass 列表(回环由 DSH 强制加入);不支持 CIDR |
112
+ | `routing.bypassDomains` | 内网后缀 | 直连域名后缀列表,数组或逗号串 |
113
+ | `routing.blockAds` | `false` | 加 `geosite:category-ads-all` → block |
114
+ | `routing.geositeCn` | `false` | 国内域名直连(需要 geosite 数据) |
115
+ | `binary.path` | `""` | 显式 xray 路径 |
116
+ | `binary.dataDir` | `""` | 数据目录;默认 `<DSH_HOME>/xray-proxy` |
117
+ | `lifecycle.startTimeoutMs` | `15000` | 端口就绪预算 |
118
+ | `lifecycle.stopGraceMs` | `3000` | 终止宽限(也是 `ctx.subprocess` 的 graceMs) |
119
+ | `lifecycle.maxRestarts` | `5` | 崩溃重启上限,超过进 `restart-exhausted` |
120
+ | `lifecycle.backoffMs` / `backoffMaxMs` | `500` / `30000` | 指数退避 |
121
+ | `lifecycle.healthIntervalMs` | `30000` | 周期健康探测;`0` 关闭 |
122
+ | `lifecycle.unhealthyThreshold` | `3` | 连续失败后回收进程 |
123
+ | `functionalProbe.enabled` | `true` | 经本地入站请求一次 204;这是唯一能证明 uuid/flow/SNI/REALITY 全链路可用的检查 |
124
+ | `functionalProbe.url` | `http://www.gstatic.com/generate_204` | 探测目标 |
125
+ | `functionalProbe.timeoutMs` | `5000` | 探测超时 |
126
+ | `log.level` | `warning` | 传给 xray 的 `loglevel`;访问日志固定 `none` |
127
+
128
+ ## 多节点与 gRPC 控制面
129
+
130
+ `nodes[]` 与 `server.*` 二选一:前者为每个节点生成一个 outbound + 一个 balancer;后者保持单节点旧行为,配置字节不变。
131
+
132
+ ```yaml
133
+ - id: xray-proxy
134
+ name: '@twinsearth/dsh-2xray'
135
+ config:
136
+ enabled: true
137
+ activeNode: hk-01 # node id 或 outbound tag
138
+ nodes:
139
+ - id: hk-01
140
+ address: hk.example.com
141
+ port: 443
142
+ uuid: 11111111-2222-4333-8444-555555555555
143
+ flow: xtls-rprx-vision
144
+ serverName: www.example.com
145
+ password: REPLACE_WITH_43_CHAR_BASE64URL_X25519_PUBLIC_KEY
146
+ shortId: ''
147
+ - id: jp-01
148
+ address: jp.example.com
149
+ port: 443
150
+ uuid: 11111111-2222-4333-8444-555555555555
151
+ flow: xtls-rprx-vision
152
+ serverName: www.example.com
153
+ password: REPLACE_WITH_43_CHAR_BASE64URL_X25519_PUBLIC_KEY
154
+ shortId: ''
155
+ control:
156
+ enabled: true
157
+ port: 10085
158
+ stats: true
159
+ ```
160
+
161
+ 启动顺序:写配置 → 起进程 → TCP 就绪 → 204 功能探测 → 接管 dispatcher → `OverrideBalancerTarget(activeNode)` 钉住出口。切换节点只改 balancer override,进程、入站、dispatcher 都不动。
162
+
163
+ 模型可调用的工具(`ctx.tools.register` + 从安装目录加载 `@deepseek-ai/dsh-tools` 的 `defineTool`):
164
+
165
+ | 工具 | 作用 | 依赖 |
166
+ |---|---|---|
167
+ | `xray_status` | 与 `status.json` 相同的状态快照 | 无 |
168
+ | `xray_validate_config` | 用 `xray run -test` 预检当前配置;不启动/不重启进程;返回 { ok, reason, detail, configHash } | 无 |
169
+ | `xray_switch_node` | `{node}` → 钉住指定 outbound | `control` + `RoutingService` |
170
+ | `xray_stats` | 每节点累计上下行字节;`{reset:true}` 读后清零 | `control` + `StatsService` + `stats` |
171
+
172
+ **版本要求:Xray 2024-09 之后的版本(v1.8.24+ / v24.9+)。** 本机 1.4.5 实测 `RoutingService` 返回 `12 unknown service`,而 `StatsService` 在 1.4.5 已可用。版本过老时插件仍正常启动,切换工具会给出可读诊断并把 `status.control.reachable` 置为 `false`。
173
+
174
+ 注意:
175
+
176
+ - override 只存在于 xray 进程内存,重启后由插件按 `activeNode` 重新钉住;切换结果不写回配置(持久化需要设置页 `ctx.settings`,尚未实现)。
177
+ - API 入站只绑 `127.0.0.1`,但**无认证**:本机任意进程可查统计或切节点。需要更强隔离时用随机端口,或关闭 `control.enabled` 只保留静态多出站。
178
+ - `stats` 计数器是**自进程启动以来的累计值**,不是速率;速率需要调用方自行差分。
179
+
180
+ ## 配置导入:分享链接 / 二维码
181
+
182
+ 三种入口最终都归约为同一段 `vless://` 文本,所以解析器只有一份([lib/share-link.js](lib/share-link.js)),UI 只负责把文本拿到手。
183
+
184
+ | 方式 | 状态 | 实现 |
185
+ |---|---|---|
186
+ | 粘贴分享链接 | ✅ Host 侧已实现 | `shareLink: "vless://..."`,或 `node scripts/parse-share-link.mjs "<link>"` 生成 `nodes` JSON |
187
+ | 扫描屏幕二维码 | ⏳ 待设置页 | 浏览器 `getDisplayMedia()` 取帧 + `BarcodeDetector`(Chromium)或 jsQR 兜底 |
188
+ | 扫描图片二维码 | ⏳ 待设置页 | `<input type="file">` + `createImageBitmap()` + 同一个解码器 |
189
+
190
+ ```yaml
191
+ - id: xray-proxy
192
+ name: '@twinsearth/dsh-2xray'
193
+ config:
194
+ enabled: true
195
+ # 支持标准 Xray/v2rayN 导出格式:
196
+ # vless://uuid@host:443?security=reality&sni=...&fp=chrome&pbk=...&sid=...&spx=%2F&flow=xtls-rprx-vision#Hong%20Kong
197
+ shareLink: "vless://11111111-2222-4333-8444-555555555555@hk.example.com:443?security=reality&sni=www.example.com&fp=chrome&pbk=REPLACE_43_CHARS&sid=ab12&spx=%2F&flow=xtls-rprx-vision&type=tcp#Hong%20Kong"
198
+ control:
199
+ enabled: true
200
+ ```
201
+
202
+ 解析规则:
203
+
204
+ - `#` 片段名 → 节点 `id`(如 `Hong Kong` → outbound tag `proxy-Hong_Kong`),可用作 `activeNode`。
205
+ - `sni` → `serverName`;`pbk` → `password` 与 `publicKey` 双写(跨版本兼容);`sid` → `shortId`;`fp` → `fingerprint`;`spx` → `spiderX`。
206
+ - `security=none` 时清空 flow/SNI/shortId;`type=tcp` 归一为 `raw`。
207
+ - `vmess://` / `trojan://` / `ss://` 会明确报「暂不支持」,而不是生成一个 Xray 会拒绝的配置——当前 builder 只产出 VLESS 出站。
208
+
209
+ **订阅分组(订阅 URL 拉取多节点)未实现。** 它是独立的网络动作(SSRF、重定向、离线缓存、订阅格式),建议单独设计,不与本地导入混在一起。
210
+
211
+ 扫码能力依赖浏览器设置页(`dsh.client` + `configForms`/`slots`),当前仓库是 Host-only,所以只有链接字段与 CLI 可用。
212
+
213
+ ## 状态机与失败原因码
214
+
215
+ 对外状态:`idle` → `resolving` → `starting` → `ready`;异常路径为 `degraded` / `backoff` / `failed` / `stopped`。
216
+
217
+ 状态快照写入 `<dataDir>/status.json`(0600,不含密钥),字段包括:
218
+
219
+ `state`、`reason`、`detail`、`generation`、`restarts`、`port`、`binary{path,source,version}`、`process{pid,alive,exitCode,stderrTail}`、`route{installed,mode,proxy,webFetchCovered,sentinel,dispatcherIsOurs}`、`nodes[]`、`activeNode`、`control{enabled,reachable,activeTarget,lastError}`、`functionalProbe`、`configHash`、`startedAt`、`updatedAt`。
220
+
221
+ 原因码:
222
+
223
+ | reason | 含义 | 是否自动重启 |
224
+ |---|---|---|
225
+ | `disabled` | `enabled: false` | — |
226
+ | `config-invalid` | 字段校验失败(detail 带字段名) | 否,改配置后重载 |
227
+ | `binary-missing` / `binary-unusable` | 找不到 xray,或 `xray version` 不能运行 | 否 |
228
+ | `config-write-failed` | 数据目录不可写 | 否 |
229
+ | `port-busy` | 入站端口被占用 | 否(不换端口) |
230
+ | `config-rejected` | `xray run -test` 失败,或运行中以退出码 23 退出 | 否 |
231
+ | `start-timeout` | 进程活着但端口未监听 | 否 |
232
+ | `functional-probe-failed` | 入站通了但经代理的请求失败 | 否 |
233
+ | `route-install-failed` | xray 健康,但策略安装失败 → `degraded` | 进程保留 |
234
+ | `external-proxy-active` | `takeover: auto` 检测到启动器已有代理 → `degraded` | — |
235
+ | `crashed` | 运行中意外退出 → `backoff` | 是,指数退避 |
236
+ | `unhealthy` | 连续健康探测失败,回收后重启 | 是 |
237
+ | `restart-exhausted` | 超过 `maxRestarts` | 否 |
238
+ | `disposed` | 插件卸载/DSH 退出 | — |
239
+
240
+ ## 覆盖矩阵
241
+
242
+ | 流量 | 是否经 xray |
243
+ |---|---|
244
+ | 模型 API(`globalThis.fetch`,含 SSE) | ✅ |
245
+ | `web_fetch` | ✅(harness 模式);❌ builtin/SOCKS 模式(已知缺口) |
246
+ | `web_search`、MCP HTTP、账号登录、第三方插件 fetch | ✅ |
247
+ | 回环地址(DSH Web UI、本地服务) | ❌ 强制直连(DSH 策略内置,插件不重复实现) |
248
+ | `noProxy` 命中的域名 | ❌ |
249
+ | 内网 IP / `geoip:private` | ❌ 由 Xray routing 直连 |
250
+ | Electron 更新检查、内嵌视图(Chromium 网络栈) | ❌ |
251
+ | `ws` 远程访问 / api-gateway | ❌ 自行建连,不走 undici |
252
+ | OTLP 遥测(`node:http`) | ❌ |
253
+ | worker / PTC 脚本 | ❌ 宿主刻意不注入 |
254
+ | bash/curl/git 子进程 | ⚠️ 接管后,DSH 从当前策略派生子进程 overlay,新起的子进程会拿到无认证的 `http://127.0.0.1:<port>`;是否生效取决于该程序是否读 `HTTP_PROXY` |
255
+
256
+ ## 安全
257
+
258
+ - 生成的 xray 配置含 uuid 与 REALITY 公钥,写入 `<dataDir>/conf/xray-config.json`,POSIX 权限 0600、目录 0700;Windows 上 ACL 继承用户目录,插件不做 `icacls` 收紧(已知边界)。
259
+ - 只记录 `configHash`,不记录配置内容;错误信息只带 stderr 尾部。
260
+ - 入站只允许回环地址;`inbound.host: 0.0.0.0` 会被校验拒绝。
261
+ - 凭据不写进 `process.env`:`installProxyFromEnvironment` 收到的代理 URL 是无认证的 `http://127.0.0.1:<port>`。
262
+ - 控制面 API 无认证(仅回环);高隔离场景请关闭或换随机端口。
263
+
264
+ ## 已知限制
265
+
266
+ 1. **builtin/SOCKS 模式不覆盖 `web_fetch`**(它会自建 pinned-IP Agent)。默认 HTTP + harness 模式没有这个缺口。
267
+ 2. **没有设置页 UI**:v1 通过 `cordis.patch.yml` 配置。要做表单需走 `ctx.settings` + Schemastery + `configForms`,且可编辑字段必须是 volatile。
268
+ 3. **不下载 xray**:插件可以携带 `bin/xray`,但从不联网下载;供应链(版本、哈希、签名)留给发布流程。
269
+ 4. **重启会打断在途流**:Windows 上 `taskkill /F` 无法 drain;DSH 会看到 `TRANSPORT` 重试,或干净 FIN 导致的 `STREAM_CLOSED`(默认不重试)。xray `connIdle` 已设为 600s,尽量让 DSH 的 300s 空闲看门狗先触发。
270
+ 5. **端口变化必须重启 DSH**:插件不改端口,也不在运行中重装策略。
271
+ 6. **多实例**:每个 DSH profile 独立端口/配置;没有跨实例锁,端口冲突会明确报 `port-busy`。
272
+ 7. **控制面依赖较新 Xray**:`RoutingService` 需要 2024-09 之后的版本;更老的版本只有 `StatsService`。失败时插件不崩溃,但切换工具不可用。
273
+ 8. **API 无认证**:只绑回环,但本机任意进程都能调用。高隔离场景请关闭 `control.enabled` 或换随机端口。
274
+ 9. **切换不持久化**:`xray_switch_node` 只改进程内 override,重启后回到 `activeNode`;写回配置需要设置页/`ctx.settings`(未实现)。
275
+
276
+ ## 验证结果(诚实边界)
277
+
278
+ 已执行:
279
+
280
+ | 项 | 结果 |
281
+ |---|---|
282
+ | 单元测试 `node --test --test-isolation=none test/*.test.js` | **96/96 通过**(配置校验、单/多节点配置生成、分享链接解析、二进制解析优先级、路由安装/回滚/幂等、状态机启动/崩溃重启/退出码 23/端口占用/停止顺序/启动竞态、protobuf 编解码、gRPC 客户端对 mock h2 服务、工具注册与调用) |
283
+ | 真实运行时加载(`ELECTRON_RUN_AS_NODE=1 "E:\DS\DeepSeek Harness.exe"`) | 插件入口可导入;`ctx.effect` 注册与 dispose 正常;`state=idle → stopped` |
284
+ | 真实运行时路由探针(`scripts/probe-routing.mjs`) | 通过 `createRequire` 解析到安装目录里的 `@deepseek-ai/dsh-http-proxy`;`installProxyFromEnvironment` 安装成功;`fetch` 实测经假代理(absolute-form 请求行)、回环地址直连(origin-form)、`restore()` 后回到直连(ENOTFOUND) |
285
+ | 工具层解析 | `@deepseek-ai/dsh-tools` 同样从安装目录解析成功,`defineTool` 生成合法参数 schema |
286
+ | 真实 xray gRPC 探针 | 启动本机 xray 1.4.5 的 API 入站后,插件的零依赖客户端成功调用 `StatsService.QueryStats` 并解码出 inbound 计数;`RoutingService` 按预期返回 `12 unknown service` |
287
+ | 多节点配置结构校验 | 2 节点 + balancer + API 入站 + stats 的配置,被本机 xray 1.4.5(降级 stream 后)判为 `Configuration OK.` |
288
+ | xray 配置结构校验 | 单节点同款做法通过,证明 inbound/outbound/routing/policy 骨架合法 |
289
+
290
+ **未验证(必须在目标机执行)**:
291
+
292
+ - 与 Xray v26.10.x 真实二进制的 `run -test`(REALITY `password`、`network: raw` 字段只能靠源码规格保证);
293
+ - 多节点 `OverrideBalancerTarget` 在支持 `RoutingService` 的真实版本上的端到端切换(本机 1.4.5 没有该服务);
294
+ - 真实服务端的端到端连通(uuid/flow/SNI/shortId/公钥);
295
+ - Windows Job Object 在强杀 DSH 后的 xray 清理;
296
+ - `web_fetch` 实抓流量与 `proxyRouteFor()` 的一致性;
297
+ - 验收测试计划中的 83 条用例(见仓库根目录 `DSH-XRAY-PROXY-ACCEPTANCE-TEST-PLAN.md`)。
298
+
299
+ ## 排障
300
+
301
+ | 现象 | 处理 |
302
+ |---|---|
303
+ | `binary-missing` | 把 `xray.exe` 放进插件 `bin/`、`<DSH_HOME>\xray-proxy\bin\`,或设 `binary.path` |
304
+ | `config-rejected`,detail 含 `encryption` | 检查生成的配置未被手工改坏;`users[0].encryption` 必须为 `"none"` |
305
+ | `config-rejected`,detail 含 `password` | 用 `xray x25519` 重新生成,确认 43 字符 base64url |
306
+ | `functional-probe-failed` | 先用 `curl -x http://127.0.0.1:20808 https://example.com -v` 手工验证;确认服务端参数;离线环境可临时设 `functionalProbe.enabled: false` |
307
+ | `port-busy` | 释放端口(可能有上次残留的 xray),或改 `inbound.port` 后重启 DSH |
308
+ | `external-proxy-active` | 启动环境里已有 `HTTP(S)_PROXY`;需要覆盖时设 `takeover: force` |
309
+ | 模型请求突然 `ECONNREFUSED` | 查看 `status.json` 的 `state/reason`;若 `backoff`,等待重启或修复配置 |
310
+ | 状态一直 `degraded` + `route-install-failed` | 查日志里的模块解析诊断;必要时确认 DSH 安装目录结构 |
311
+ | `xray_switch_node` 报 `unknown service ... RoutingService` | xray 版本早于 2024-09;升级二进制,或关闭 `control.enabled` 只用 `activeNode` |
312
+
313
+ ## 开发
314
+
315
+ ```powershell
316
+ cd E:\GPT\dsh-2xray
317
+ node --test --test-isolation=none test/*.test.js
318
+ ```
319
+
320
+ > 在受限沙箱里不要用 `node --test test/`:它按文件 `spawn` 子进程并使用管道,会被拒绝(EPERM);`--test-isolation=none` 在进程内运行。
321
+
322
+ 可复跑的验证脚本:
323
+
324
+ | 脚本 | 用途 |
325
+ |---|---|
326
+ | `scripts/check-config.mjs` | 用真实 xray 二进制校验生成的配置;`--legacy` 兼容 1.8 之前的 `tcp` 命名 |
327
+ | `scripts/probe-routing.mjs` | 在 DSH 运行时验证「fetch 被接管 / 回环直连 / restore 后恢复」 |
328
+
329
+ 完整证据与未验证项见 [docs/EVIDENCE.md](docs/EVIDENCE.md);验收闸门 0 的接管方式声明见 [docs/AC-0.md](docs/AC-0.md)。
package/bin/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # bin/
2
+
3
+ Drop the xray-core executable here (`xray.exe` on Windows, `xray` elsewhere) to
4
+ let the bundle ship its own binary:
5
+
6
+ ```
7
+ dsh-2xray/bin/xray.exe
8
+ ```
9
+
10
+ Resolution order (see `lib/binary.js`):
11
+
12
+ 1. `config.binary.path`
13
+ 2. `DSH_XRAY_BIN`
14
+ 3. **this directory**
15
+ 4. `<dataDir>/bin/` (`%USERPROFILE%\.dsh\xray-proxy\bin`)
16
+ 5. `PATH`
17
+
18
+ ## Licensing
19
+
20
+ Xray-core is MPL-2.0. Redistributing the binary inside a plugin package is
21
+ allowed, but the MPL requires that the source of the covered files be made
22
+ available and that modifications stay under the MPL. If you vendor a binary,
23
+ record the exact version and its upstream release URL in this directory, and
24
+ keep the license notice. The plugin itself never downloads xray at runtime.
25
+
26
+ Binaries are intentionally git-ignored (`.gitignore`): commit the version
27
+ record, not a 15–30 MB blob.
@@ -0,0 +1,37 @@
1
+ # dsh-2xray: one xray-core child process plus the harness' proxy policy.
2
+ #
3
+ # 1. Put the xray binary at <DSH_HOME>/xray-proxy/bin/xray(.exe), or set
4
+ # binary.path, or expose `xray` on PATH.
5
+ # 2. Fill server.* below, then set enabled: true.
6
+ # 3. The row's config is a complete block: a later profile-level patch replaces
7
+ # it wholesale, so restate any field you want to keep.
8
+ - insert:
9
+ - id: xray-proxy
10
+ name: '@twinsearth/dsh-2xray'
11
+ config:
12
+ enabled: false
13
+ # server:
14
+ # address: example.com
15
+ # port: 443
16
+ # uuid: 00000000-0000-0000-0000-000000000000
17
+ # flow: xtls-rprx-vision
18
+ # serverName: www.example.com
19
+ # password: REPLACE_WITH_43_CHAR_BASE64URL_X25519_PUBLIC_KEY
20
+ # shortId: ''
21
+ # fingerprint: chrome
22
+ # spiderX: /
23
+ # network: raw
24
+ # security: reality
25
+ # inbound:
26
+ # protocol: http
27
+ # host: 127.0.0.1
28
+ # port: 20808
29
+ # takeover: auto
30
+ #
31
+ # Multi-node + runtime switching (needs Xray 2024-09+):
32
+ # nodes:
33
+ # - { id: hk-01, address: hk.example.com, port: 443, uuid: ..., flow: xtls-rprx-vision,
34
+ # serverName: www.example.com, password: ..., shortId: '' }
35
+ # - { id: jp-01, address: jp.example.com, ... }
36
+ # activeNode: hk-01
37
+ # control: { enabled: true, port: 10085, stats: true }
package/docs/AC-0.md ADDED
@@ -0,0 +1,60 @@
1
+ # AC-0 接管方式声明(供验收)
2
+
3
+ 对应 `DSH-XRAY-PROXY-ACCEPTANCE-TEST-PLAN.md` 的闸门 0:实现方必须先声明接管层级,否则 F 组用例无法判定。本实现选择:
4
+
5
+ ## 接管层级:M2(运行期策略安装,进程级)
6
+
7
+ ```
8
+ 插件 apply()
9
+ └─ 启动 xray 子进程
10
+ └─ TCP 就绪 + 功能探测通过
11
+ └─ 调用 @deepseek-ai/dsh-http-proxy 的 installProxyFromEnvironment()
12
+ (合成 env:http_proxy=https_proxy=http://127.0.0.1:<port>)
13
+ → 安装进程级 dispatcher + 更新 active policy
14
+ └─ ctx.effect 清理:先调 disposer 恢复策略,再 terminate 子进程
15
+ ```
16
+
17
+ - **不是** M1(启动期 env 注入):插件无法在 launcher 安装前运行;但通过调用同一个策略安装函数,效果与启动物理注入一致,且 `proxyRouteFor()` 与 dispatcher 同源。
18
+ - **不是** M2 的「逐调用点注入」:没有修改任何调用点(模型、web_fetch、搜索都未改)。
19
+ - **不是** M3(进程外前置代理)。
20
+
21
+ ## 覆盖声明
22
+
23
+ | 流量 | 覆盖 | 证据 |
24
+ |---|---|---|
25
+ | `globalThis.fetch`(模型 API、搜索、MCP HTTP、插件 fetch、账号) | ✅ | `scripts/probe-routing.mjs`:absolute-form 请求行到达假代理 |
26
+ | `web_fetch` | ✅(harness 模式) | 走 `proxyRouteFor()` 同一策略;未实抓验证 |
27
+ | 回环(DSH Web UI、本地服务) | ❌ 强制直连 | 探针:origin-form 请求行 |
28
+ | `noProxy` / 内网 IP | ❌ | Xray routing + 策略 bypass |
29
+ | `ws`、Electron 更新、OTel、worker | ❌ | 不在 undici dispatcher 路径上 |
30
+ | 子进程(bash/curl) | ⚠️ 取决于程序是否读 `HTTP_PROXY` | 由 DSH 的 `proxyEnvironmentForChild()` 派生,非本插件写 env |
31
+
32
+ ## 观测点对照(相对验收计划的硬要求)
33
+
34
+ | 要求字段 | 现状 |
35
+ |---|---|
36
+ | `state`(区分 degraded / failed) | ✅ `status.json` |
37
+ | `reason` code | ✅ 见 README 原因码表 |
38
+ | `functionalProbe` | ✅ `pass` / `fail:<status\|error>` / `skipped` / `pending` |
39
+ | `sentinelId` | ✅ `route.sentinel`(`harness-N`)/ 同时给 `route.dispatcherIsOurs` |
40
+ | `jobObject` | ❌ `ctx.subprocess` 不暴露 Job 句柄参数 → E-04 按计划「以缺席证明」:强杀 DSH 后 `xray.exe` 必须消失 |
41
+ | `generation` | ✅ |
42
+ | `restarts.totalSinceReady` | ✅ `restarts`(自进程启动累计) |
43
+ | `portSource` | ⚠️ 固定为 `config`(插件不自动选端口) |
44
+ | `configHash` | ✅ 生成配置的 sha256 |
45
+ | `processAlive` 与 `pid` 分离 | ✅ |
46
+
47
+ ## 控制面(v0.2 新增)
48
+
49
+ - 接管层级不变:仍是「运行期策略安装」,控制面只负责节点切换与统计,不改变流量入口。
50
+ - 节点切换通过 `RoutingService.OverrideBalancerTarget`(balancer 预置全部 outbound),**不**使用 `HandlerService.AddOutbound`,因此不需要在 Node 侧编码 `OutboundHandlerConfig`。
51
+ - 需要 Xray 2024-09+(v1.8.24+/v24.9+);更老版本上 `control.reachable=false`,工具返回明确错误,代理本身继续工作。
52
+ - 工具(`xray_status` / `xray_switch_node` / `xray_stats`)是可选能力:`tools` 服务缺失时静默跳过,不影响代理。
53
+ - 设置页写入仍未实现:切换结果不持久化,重启回落到 `activeNode`。
54
+
55
+ ## 与计划不一致而需要调整的用例
56
+
57
+ 1. **设置页写入相关用例**:v1 没有客户端 UI,也没有 `ctx.settings` 集成;这类用例本期标 N/A(配置只走 `cordis.patch.yml`)。
58
+ 2. **C 组端口自动分配**:本实现明确「占用即失败、不静默换端口」,用例预期应改为 `failed(port-busy)`。
59
+ 3. **D 组重启**:退出码 23 / 端口占用 / 上游不可达均不重启;仅意外崩溃与连续健康失败重启(指数退避,上限 `maxRestarts`)。
60
+ 4. **E 组优雅退出**:Windows 上不可得;判据为进程消失、端口可重新绑定、`status.json` 为 `stopped`。
@@ -0,0 +1,135 @@
1
+ # 证据记录(本机实测)
2
+
3
+ > 目的:把「已验证」与「未验证」分开。下面每条都可用给定命令复跑;无法复跑的结论不写。
4
+
5
+ ## 环境
6
+
7
+ | 项 | 值 |
8
+ |---|---|
9
+ | DSH | 0.2.1-rc.3 桌面版(`E:\DS\DeepSeek Harness.exe`,实现打包在 `app.asar`) |
10
+ | 运行时 | Node 24.21.0 / Electron 44.7.0(`ELECTRON_RUN_AS_NODE=1`) |
11
+ | 打包 undici | 8.11.2(`app.asar\dsh\node_modules\undici`) |
12
+ | 本机 xray | 仅 1.4.5(`E:\20221120\BaiduNetdiskDownload\v2rayN-Core\xray.exe`,不支持 REALITY) |
13
+
14
+ ## E1 单元测试
15
+
16
+ ```powershell
17
+ cd E:\GPT\dsh-2xray
18
+ node --test --test-isolation=none test/*.test.js
19
+ ```
20
+
21
+ 结果:**96/96 通过**,覆盖:
22
+
23
+ - 配置归并/校验(缺字段、flow×security 组合、shortId、base64url 公钥、回环限制、端口冲突、多节点 tag 重复、activeNode 回退、control 端口冲突)
24
+ - xray 配置生成(单节点旧形状、多节点 outbound + balancer、API 入站 / services / stats / routing 首条规则)
25
+ - 分享链接(vless:// REALITY/none、IPv6、默认值、非法协议、与 nodes 合并、生成 outbound)
26
+ - 二进制解析优先级(config → DSH_XRAY_BIN → 插件 bin/ → 数据目录 → PATH,缺省时列出候选)
27
+ - 路由管理(harness 安装/幂等/回滚、已有策略不覆盖、`force`、实例不一致回滚、builtin 兜底、SOCKS 强制 builtin)
28
+ - 状态机(启动顺序、缺二进制、退出码 23 不重启、崩溃退避重启、重启上限、停止顺序、启动中停止、端口占用、启动时钉住 balancer、switchNode 不重启进程)
29
+ - 控制面(protobuf 编解码、gRPC unary over h2c、非零状态码、连接拒绝、QueryStats/OverrideBalancerTarget)
30
+ - 工具(四个工具注册、调用透传、validateConfig 预检、缺少 dsh-tools 时静默降级)
31
+ - 健康探测(TCP、晚到监听、预算耗尽、端口占用、204 功能探测、可取消等待)
32
+
33
+ > 沙箱内不能 `node --test test/`:它按文件 spawn 子进程并走管道,会被拒绝(EPERM)。`--test-isolation=none` 在进程内跑。
34
+
35
+ ## E2 模块解析(M0 关键假设)
36
+
37
+ 探针(`.tmp/probe-resolve.mjs`,一次性):
38
+
39
+ ```
40
+ import undici => FAIL ERR_MODULE_NOT_FOUND
41
+ import @deepseek-ai/dsh-http-proxy => FAIL ERR_MODULE_NOT_FOUND
42
+ createRequire('E:/DS/resources/app.asar/dsh/package.json').resolve('@deepseek-ai/dsh-http-proxy')
43
+ => E:\DS\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-http-proxy\lib\index.js
44
+ dynamic import of resolved => ok exports=clearedProxyEnv,installProxyFromEnvironment,proxyEnvironmentForChild,proxyRouteFor
45
+ ```
46
+
47
+ 同样的解析方式也用于工具层:
48
+
49
+ ```
50
+ dsh-tools resolved: E:\DS\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-tools\lib\index.js
51
+ defineTool works: xray_probe {"type":"object","properties":{"node":{"type":"string","description":"node"}},"required":["node"]}
52
+ ```
53
+
54
+ 结论:插件**不能**用裸 `import('undici')` / `import('@deepseek-ai/dsh-http-proxy')`;必须用 `createRequire(<安装目录>/dsh/package.json)` 解析后按 file URL 动态 import。`lib/route.js` 的 `installationAnchors()` 按此实现,失败时退回 builtin。
55
+
56
+ ## E3 路由安装/恢复与真实 egress
57
+
58
+ ```powershell
59
+ ELECTRON_RUN_AS_NODE=1 "E:\DS\DeepSeek Harness.exe" scripts/probe-routing.mjs
60
+ ```
61
+
62
+ 输出:
63
+
64
+ ```
65
+ PASS route installed through the harness policy
66
+ PASS proxied fetch got the proxy answer
67
+ PASS proxy saw an absolute-form request line # "http://probe.invalid/x"
68
+ PASS loopback traffic bypassed the proxy # "/direct"
69
+ PASS post-restore fetch is direct again (ENOTFOUND)
70
+ all routing checks passed
71
+ ```
72
+
73
+ 这同时证明了三件事:`installProxyFromEnvironment` 装的是**同一个**模块实例(否则 `proxyRouteFor` 不会同意);全局 `fetch` 确实被接管;回环与恢复语义正确。
74
+
75
+ ## E4 插件入口在真实运行时加载
76
+
77
+ `ELECTRON_RUN_AS_NODE=1 "E:\DS\DeepSeek Harness.exe" .tmp\probe-index.mjs`(最小 cordis 形状的 ctx):
78
+
79
+ ```
80
+ effect registered: dsh-2xray/xray-supervisor
81
+ [info] state=idle reason=disabled
82
+ plugin name: dsh-2xray dispose type: function
83
+ [info] state=stopped reason=disposed
84
+ disposed cleanly
85
+ ```
86
+
87
+ 结论:ESM 图可加载;`ctx.effect` 注册与 disposer 正常;禁用时不动进程与网络。
88
+
89
+ ## E5 xray 配置结构校验(降级)
90
+
91
+ 本机二进制太旧(1.4.5,不支持 REALITY、也不认 `raw` 传输名),用 `--legacy` 把 stream 降级为 `tcp/none` 后:
92
+
93
+ ```powershell
94
+ node scripts/check-config.mjs --config examples/config.downgrade.json --xray "E:\20221120\BaiduNetdiskDownload\v2rayN-Core\xray.exe" --legacy
95
+ ```
96
+
97
+ 输出:`Reading config ... Configuration OK.`
98
+
99
+ 多节点(2 节点 + balancer + API 入站 + stats)同样通过:
100
+
101
+ ```powershell
102
+ node scripts/check-config.mjs --config examples/config.multi.downgrade.json --xray "E:\20221120\BaiduNetdiskDownload\v2rayN-Core\xray.exe" --legacy
103
+ ```
104
+
105
+ 这证明 inbound/outbound/routing/policy/balancer/api 骨架合法;**不**证明 REALITY 字段与 `network: raw` 在 v26 上合法。
106
+
107
+ ## E6 真实 xray 的 gRPC 控制面
108
+
109
+ 用本机 xray 1.4.5 启动 `examples/config.multi.downgrade.json` 生成的配置(API 入站 10085),再用插件的零依赖客户端调用:
110
+
111
+ ```
112
+ stats: [{"name":"inbound>>>api>>>traffic>>>uplink","value":188},
113
+ {"name":"inbound>>>api>>>traffic>>>downlink","value":54},
114
+ {"name":"inbound>>>http-in>>>traffic>>>uplink","value":0},
115
+ {"name":"inbound>>>http-in>>>traffic>>>downlink","value":0}]
116
+ balancer failed: 12 unknown service xray.app.router.command.RoutingService
117
+ snapshot: {"reachable":false,"lastError":"12: unknown service xray.app.router.command.RoutingService",...}
118
+ ```
119
+
120
+ 结论:
121
+
122
+ - HTTP/2 分帧、gRPC 状态码、protobuf 编解码与真实 Xray 对得上(StatsService 是长期存在的服务)。
123
+ - `RoutingService`(`GetBalancerInfo` / `OverrideBalancerTarget`)在 1.4.5 不存在,返回 `UNIMPLEMENTED(12)`。该服务进入主线的时间是 2024-03(PR #3189 一带),因此多节点运行时切换要求 **2024-09 之后的版本(v1.8.24+ / v24.9+)**,并在更老版本上降级为「随机 balancer + 明确诊断」。
124
+ - 多节点配置骨架已被 1.4.5 判为 `Configuration OK.`,见 E5。
125
+
126
+ ## E7 未验证项(必须在目标机执行)
127
+
128
+ | 项 | 为什么没做 | 复跑方式 |
129
+ |---|---|---|
130
+ | v26.10.x `run -test` | 本机无新二进制 | 下载对应 release,`node scripts/check-config.mjs --config real.json --xray .../xray.exe` |
131
+ | 多节点切换端到端 | 本机 1.4.5 无 `RoutingService` | 用 v24.9+ 启动多节点配置,调用 `xray_switch_node`,对照 xray 日志/出站 |
132
+ | 真实服务端连通 | 无可用服务端参数 | 配好 `server.*` 启动 DSH,看 `status.json` 的 `functionalProbe` |
133
+ | Windows Job Object 清理 | 需要以插件方式装进 DSH 并强杀进程 | 装 bundle → 启动 → 任务管理器结束 DSH → 确认 `xray.exe` 消失 |
134
+ | `web_fetch` 实抓一致性 | 需要 DSH 内真实调用 web_fetch | 让模型抓一个只经代理可达的页面,对照 xray 日志 |
135
+ | 83 条验收用例 | 属验收阶段 | 见仓库根目录 `DSH-XRAY-PROXY-ACCEPTANCE-TEST-PLAN.md` |