@twinsearth/dsh-2xray 0.2.5 → 0.2.7
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.
- package/CHANGELOG.md +23 -0
- package/README.md +30 -0
- package/lib/assets.js +42 -0
- package/lib/binary.js +6 -0
- package/lib/client.js +41 -3
- package/lib/config.js +44 -0
- package/lib/route.js +5 -1
- package/lib/schema.js +14 -0
- package/lib/supervisor.js +10 -0
- package/lib/xray-config.js +22 -1
- package/package.json +1 -1
- package/scripts/release-check.mjs +2 -1
- package/test/install-core.test.js +22 -0
- package/test/route.test.js +8 -0
- package/test/routing.test.js +71 -0
- package/tools/install-xray-core.mjs +128 -0
- package/tools/launch-with-browser-proxy.cmd +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.7 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- **路由模式**:`global` / `cn-direct`(国内 IP、域名直连)/ `whitelist`(仅白名单走 2Xray)。
|
|
8
|
+
- **黑白名单**:白名单默认 github.com、google.com、youtube.com、x.com、facebook.com;黑名单默认 deepseek.com、api.deepseek.com。黑名单优先,并并入 `noProxy`。
|
|
9
|
+
- **内核安装脚本** `tools/install-xray-core.mjs`:官方 release + `.dgst` SHA2-256 校验 + 解压到数据目录。
|
|
10
|
+
- 侧边栏「实时连接信息」默认展开日志。
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- macOS 自动发现补充 `~/Library/Application Support/v2rayN`、`/Applications` 等路径。
|
|
15
|
+
- 路由验证 URL 改为保留域 `dsh-2xray-verify.invalid`,不再被 `noProxy: deepseek.com` 绕过(route-instance-mismatch 根因)。
|
|
16
|
+
- 非 Windows 下入站端口 <1024 自动回落到 20808(避免把远端 556 当本地端口导致 EACCES)。
|
|
17
|
+
|
|
18
|
+
## 0.2.6 — 2026-10-29
|
|
19
|
+
|
|
20
|
+
### 内置浏览器支持
|
|
21
|
+
|
|
22
|
+
- 新增 `tools/launch-with-browser-proxy.cmd`:以 `--proxy-server=http://127.0.0.1:20808` 启动 DSH,让内置 Chromium 浏览器也走插件的 xray。
|
|
23
|
+
- 设置页与插件页卡片增加说明:Node 插件无法在运行时修改 Chromium 的 session 代理,必须带启动参数重启;系统代理/TUN 是另一条全机路径。
|
|
24
|
+
- 保留边界:插件仍只接管 DSH 进程内的 undici fetch。
|
|
25
|
+
|
|
3
26
|
## 0.2.5 — 2026-10-29
|
|
4
27
|
|
|
5
28
|
### 修复
|
package/README.md
CHANGED
|
@@ -328,6 +328,36 @@ v0.2.0 提供两个入口,互不替代:
|
|
|
328
328
|
| 状态一直 `degraded` + `route-install-failed` | 查日志里的模块解析诊断;必要时确认 DSH 安装目录结构 |
|
|
329
329
|
| `xray_switch_node` 报 `unknown service ... RoutingService` | xray 版本早于 2024-09;升级二进制,或关闭 `control.enabled` 只用 `activeNode` |
|
|
330
330
|
|
|
331
|
+
## 路由模式与黑白名单
|
|
332
|
+
|
|
333
|
+
- `global`(默认):全部流量走 2Xray。
|
|
334
|
+
- `cn-direct`:`geoip:cn` / `geosite:cn` 直连,其余走代理。插件会从 xray 二进制旁复制 `geoip.dat`/`geosite.dat` 到数据目录,并设置 `XRAY_LOCATION_ASSET`。
|
|
335
|
+
- `whitelist`:仅白名单域名走 2Xray,其余直连。
|
|
336
|
+
|
|
337
|
+
黑名单优先于白名单与分流,并会同时加入 DSH 的 `noProxy`(xray 重启时也不受影响)。默认白名单:github.com、google.com、youtube.com、x.com、facebook.com;默认黑名单:deepseek.com、api.deepseek.com。
|
|
338
|
+
|
|
339
|
+
## 内核安装(macOS / Linux)
|
|
340
|
+
|
|
341
|
+
npm 包不内嵌五平台内核。未找到内核时:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
node tools/install-xray-core.mjs 26.9.30
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
脚本下载官方 release 资产、校验 `.dgst` 的 SHA2-256、解压到 `~/.dsh/xray-proxy/bin/`。GitHub 不可达时设置 `DSH_XRAY_MIRROR` 指向镜像。macOS 也会自动搜索 `~/Library/Application Support/v2rayN/bin/xray/xray`。
|
|
348
|
+
|
|
349
|
+
## 内置浏览器(Chromium)
|
|
350
|
+
|
|
351
|
+
DSH 内置浏览器走 Chromium 网络栈,不读 undici dispatcher,插件无法在运行时改它的代理。支持方式只有启动参数:
|
|
352
|
+
|
|
353
|
+
```powershell
|
|
354
|
+
& "E:\DS\DeepSeek Harness.exe" --proxy-server=http://127.0.0.1:20808
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
或运行 `tools/launch-with-browser-proxy.cmd`(自动查找 DSH 路径并透传 `--proxy-server`)。
|
|
358
|
+
|
|
359
|
+
**必须先完全退出 DSH**:单实例再次启动只会聚焦旧窗口,不会应用参数。系统代理/TUN(v2rayN)也能让 Chromium 生效,但那是全机代理,与本插件无关。
|
|
360
|
+
|
|
331
361
|
## 开发
|
|
332
362
|
|
|
333
363
|
```powershell
|
package/lib/assets.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const FILES = ['geoip.dat', 'geosite.dat']
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `geoip:cn` / `geosite:cn` need data files next to the binary or in
|
|
8
|
+
* XRAY_LOCATION_ASSET. A borrowed v2rayN binary keeps them one directory up,
|
|
9
|
+
* so copy them into the managed data dir and point xray there.
|
|
10
|
+
*/
|
|
11
|
+
export function ensureXrayAssets({ binaryPath, dataDir, logger } = {}) {
|
|
12
|
+
if (!dataDir) return { dir: undefined, copied: [], missing: FILES }
|
|
13
|
+
try {
|
|
14
|
+
mkdirSync(dataDir, { recursive: true })
|
|
15
|
+
} catch (error) {
|
|
16
|
+
logger?.warn?.(`无法创建数据目录:${String(error)}`)
|
|
17
|
+
return { dir: dataDir, copied: [], missing: FILES }
|
|
18
|
+
}
|
|
19
|
+
const candidates = [dataDir, dirname(binaryPath ?? ''), dirname(dirname(binaryPath ?? ''))]
|
|
20
|
+
const copied = []
|
|
21
|
+
const missing = []
|
|
22
|
+
for (const name of FILES) {
|
|
23
|
+
const target = join(dataDir, name)
|
|
24
|
+
if (existsSync(target)) continue
|
|
25
|
+
const source = candidates.find((dir) => dir && existsSync(join(dir, name)))
|
|
26
|
+
if (!source) {
|
|
27
|
+
missing.push(name)
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
copyFileSync(join(source, name), target)
|
|
32
|
+
copied.push(name)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger?.warn?.(`复制 ${name} 失败:${String(error)}`)
|
|
35
|
+
missing.push(name)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (missing.length > 0) {
|
|
39
|
+
logger?.warn?.(`缺少 xray 数据文件 ${missing.join(', ')};cn-direct / geosite 规则可能启动失败,可从 v2rayN 的 bin/ 目录复制到 ${dataDir}`)
|
|
40
|
+
}
|
|
41
|
+
return { dir: dataDir, copied, missing }
|
|
42
|
+
}
|
package/lib/binary.js
CHANGED
|
@@ -117,6 +117,12 @@ export function defaultDiscoveryRoots() {
|
|
|
117
117
|
local,
|
|
118
118
|
home ? join(home, 'Desktop') : '',
|
|
119
119
|
home ? join(home, 'Downloads') : '',
|
|
120
|
+
// macOS: v2rayN keeps its core under Application Support, not ~/bin.
|
|
121
|
+
home ? join(home, 'Library', 'Application Support') : '',
|
|
122
|
+
home ? join(home, '.local', 'bin') : '',
|
|
123
|
+
'/Applications',
|
|
124
|
+
'/opt/homebrew/bin',
|
|
125
|
+
'/usr/local/bin',
|
|
120
126
|
'C:\\Program Files',
|
|
121
127
|
'C:\\Program Files (x86)',
|
|
122
128
|
'D:\\',
|
package/lib/client.js
CHANGED
|
@@ -7,6 +7,8 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
const PANEL_ID = 'xray'
|
|
8
8
|
const NS = 'xray-proxy'
|
|
9
9
|
const inject = ['slots']
|
|
10
|
+
const BROWSER_HINT =
|
|
11
|
+
'内置浏览器走 Chromium 网络栈,不受插件 dispatcher 影响:请用插件目录 tools/launch-with-browser-proxy.cmd 启动 DSH,或在快捷方式目标后追加 --proxy-server=http://127.0.0.1:20808(需完全退出 DSH 后重启才生效)。'
|
|
10
12
|
|
|
11
13
|
const C = {
|
|
12
14
|
page: { maxWidth: 980, margin: '0 auto', padding: '20px 24px 16px', color: 'var(--dsw-alias-label-primary, #0f1115)' },
|
|
@@ -174,7 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
function RuntimeCard({ status, logs, setLogs }) {
|
|
177
|
-
const [showLogs, setShowLogs] = React.useState(
|
|
179
|
+
const [showLogs, setShowLogs] = React.useState(true)
|
|
178
180
|
const inbound = status?.inbound
|
|
179
181
|
const inboundLabel = inbound
|
|
180
182
|
? `${String(inbound.protocol).toUpperCase()}:${inbound.port}${inbound.socks ? ` · SOCKS5:${inbound.socks.port}` : ''}`
|
|
@@ -311,6 +313,9 @@ window.__ModuleLoader__.load({
|
|
|
311
313
|
const [port, setPort] = React.useState(String(value.inbound?.port ?? 20808))
|
|
312
314
|
const [enabled, setEnabled] = React.useState(value.enabled === true)
|
|
313
315
|
const [takeover, setTakeover] = React.useState(value.takeover ?? 'auto')
|
|
316
|
+
const [routingMode, setRoutingMode] = React.useState(value.routing?.mode ?? 'global')
|
|
317
|
+
const [whitelist, setWhitelist] = React.useState((value.routing?.whitelist ?? []).join('\n'))
|
|
318
|
+
const [blacklist, setBlacklist] = React.useState((value.routing?.blacklist ?? []).join('\n'))
|
|
314
319
|
const [message, setMessage] = React.useState('')
|
|
315
320
|
const [kind, setKind] = React.useState('')
|
|
316
321
|
const [busy, setBusy] = React.useState(false)
|
|
@@ -409,10 +414,18 @@ window.__ModuleLoader__.load({
|
|
|
409
414
|
const save = async () => {
|
|
410
415
|
setBusy(true)
|
|
411
416
|
try {
|
|
417
|
+
const splitList = (text) =>
|
|
418
|
+
String(text)
|
|
419
|
+
.split(/[\n,]+/)
|
|
420
|
+
.map((entry) => entry.trim())
|
|
421
|
+
.filter(Boolean)
|
|
412
422
|
const fields = [
|
|
413
423
|
{ path: ['shareLink'], value: shareLink.trim() },
|
|
414
424
|
{ path: ['enabled'], value: enabled },
|
|
415
425
|
{ path: ['takeover'], value: takeover },
|
|
426
|
+
{ path: ['routing', 'mode'], value: routingMode },
|
|
427
|
+
{ path: ['routing', 'whitelist'], value: splitList(whitelist) },
|
|
428
|
+
{ path: ['routing', 'blacklist'], value: splitList(blacklist) },
|
|
416
429
|
]
|
|
417
430
|
if (binaryPath.trim()) fields.push({ path: ['binary', 'path'], value: binaryPath.trim() })
|
|
418
431
|
const numericPort = Number(port)
|
|
@@ -447,6 +460,29 @@ window.__ModuleLoader__.load({
|
|
|
447
460
|
h('label', { style: { fontSize: 12.5, display: 'flex', alignItems: 'center', gap: 6, marginTop: 10 } }, 'takeover',
|
|
448
461
|
h('select', { value: takeover, onChange: (event) => setTakeover(event.target.value), style: { padding: '4px 8px', borderRadius: 6, background: 'transparent', color: 'inherit' } },
|
|
449
462
|
h('option', { value: 'auto' }, 'auto'), h('option', { value: 'force' }, 'force'), h('option', { value: 'off' }, 'off')))),
|
|
463
|
+
h(Field, { label: '路由模式' },
|
|
464
|
+
h('select', {
|
|
465
|
+
value: routingMode,
|
|
466
|
+
onChange: (event) => setRoutingMode(event.target.value),
|
|
467
|
+
style: { ...C.input, width: 'auto' },
|
|
468
|
+
},
|
|
469
|
+
h('option', { value: 'global' }, '全局:全部走 2Xray'),
|
|
470
|
+
h('option', { value: 'cn-direct' }, '国内直连:国内 IP/域名不走 VPN,国外走'),
|
|
471
|
+
h('option', { value: 'whitelist' }, '白名单:仅白名单走 2Xray'))),
|
|
472
|
+
h(Field, { label: '白名单(强制走 2Xray,每行一个域名)' },
|
|
473
|
+
h('textarea', {
|
|
474
|
+
style: { ...C.input, minHeight: 58, fontFamily: 'ui-monospace, Consolas, monospace' },
|
|
475
|
+
value: whitelist,
|
|
476
|
+
placeholder: 'github.com\ngoogle.com\nyoutube.com\nx.com\nfacebook.com',
|
|
477
|
+
onChange: (event) => setWhitelist(event.target.value),
|
|
478
|
+
})),
|
|
479
|
+
h(Field, { label: '黑名单(强制直连,每行一个域名,优先于白名单)' },
|
|
480
|
+
h('textarea', {
|
|
481
|
+
style: { ...C.input, minHeight: 58, fontFamily: 'ui-monospace, Consolas, monospace' },
|
|
482
|
+
value: blacklist,
|
|
483
|
+
placeholder: 'deepseek.com\napi.deepseek.com',
|
|
484
|
+
onChange: (event) => setBlacklist(event.target.value),
|
|
485
|
+
})),
|
|
450
486
|
h(Field, { label: 'xray 可执行文件路径(留空自动查找)' },
|
|
451
487
|
h('input', { style: { ...C.input, fontFamily: 'ui-monospace, Consolas, monospace' }, value: binaryPath, placeholder: '例如 D:\\v2rayN\\...\\bin\\xray\\xray.exe', onChange: (event) => setBinaryPath(event.target.value) })),
|
|
452
488
|
h(Field, { label: '本地 HTTP 入站端口' },
|
|
@@ -463,7 +499,8 @@ window.__ModuleLoader__.load({
|
|
|
463
499
|
}
|
|
464
500
|
return h('div', { style: { ...C.card, marginTop: 8 } },
|
|
465
501
|
h('div', { style: C.cardHead }, h(GlobeIcon), '2Xray 连接'),
|
|
466
|
-
h(XrayConfigForm, { pageForm: form })
|
|
502
|
+
h(XrayConfigForm, { pageForm: form }),
|
|
503
|
+
h('p', { style: { ...C.muted, fontSize: 12, marginTop: 10 } }, BROWSER_HINT))
|
|
467
504
|
}
|
|
468
505
|
|
|
469
506
|
function SettingsSection({ controller }) {
|
|
@@ -473,7 +510,8 @@ window.__ModuleLoader__.load({
|
|
|
473
510
|
h('div', null,
|
|
474
511
|
h('h1', { style: C.title }, '2Xray'),
|
|
475
512
|
h('p', { style: C.sub }, '导入 VLESS 链接或本地二维码,保存后重启 DSH 生效。'))),
|
|
476
|
-
h('section', { style: C.card }, h(XrayConfigForm, { controller }))
|
|
513
|
+
h('section', { style: C.card }, h(XrayConfigForm, { controller })),
|
|
514
|
+
h('p', { style: { ...C.muted, fontSize: 12, marginTop: 4 } }, BROWSER_HINT))
|
|
477
515
|
}
|
|
478
516
|
|
|
479
517
|
function apply(ctx) {
|
package/lib/config.js
CHANGED
|
@@ -51,6 +51,9 @@ export const DEFAULT_CONFIG = {
|
|
|
51
51
|
takeover: 'auto',
|
|
52
52
|
noProxy: '',
|
|
53
53
|
routing: {
|
|
54
|
+
mode: 'global',
|
|
55
|
+
whitelist: ['github.com', 'google.com', 'youtube.com', 'x.com', 'facebook.com'],
|
|
56
|
+
blacklist: ['deepseek.com', 'api.deepseek.com'],
|
|
54
57
|
bypassDomains: ['localhost', 'local', 'lan', 'internal', 'corp', 'home.arpa'],
|
|
55
58
|
blockAds: false,
|
|
56
59
|
geositeCn: false,
|
|
@@ -139,6 +142,20 @@ function asStringList(value, fallback) {
|
|
|
139
142
|
return clone(fallback)
|
|
140
143
|
}
|
|
141
144
|
|
|
145
|
+
/** Accept URLs as well as bare domains; routing rules only understand hosts. */
|
|
146
|
+
function normalizeDomainList(entries) {
|
|
147
|
+
const result = []
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
let text = String(entry ?? '').trim().toLowerCase()
|
|
150
|
+
if (!text) continue
|
|
151
|
+
text = text.replace(/^[a-z][a-z0-9+.-]*:\/\//, '').split('/')[0].split(':')[0]
|
|
152
|
+
text = text.replace(/^\*\./, '').replace(/\.$/, '')
|
|
153
|
+
if (!/^[a-z0-9.-]+$/.test(text) || !text.includes('.')) continue
|
|
154
|
+
if (!result.includes(text)) result.push(text)
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
142
159
|
export function sanitizeTag(value) {
|
|
143
160
|
return String(value ?? '')
|
|
144
161
|
.trim()
|
|
@@ -316,6 +333,22 @@ export function normalizeConfig(raw) {
|
|
|
316
333
|
merged.routing.bypassDomains = asStringList(merged.routing.bypassDomains, DEFAULT_CONFIG.routing.bypassDomains)
|
|
317
334
|
merged.routing.blockAds = asBool(merged.routing.blockAds, false)
|
|
318
335
|
merged.routing.geositeCn = asBool(merged.routing.geositeCn, false)
|
|
336
|
+
merged.routing.mode = asString(merged.routing.mode, 'global')
|
|
337
|
+
merged.routing.whitelist = normalizeDomainList(
|
|
338
|
+
asStringList(merged.routing.whitelist, DEFAULT_CONFIG.routing.whitelist),
|
|
339
|
+
)
|
|
340
|
+
merged.routing.blacklist = normalizeDomainList(
|
|
341
|
+
asStringList(merged.routing.blacklist, DEFAULT_CONFIG.routing.blacklist),
|
|
342
|
+
)
|
|
343
|
+
if (merged.routing.blacklist.length > 0) {
|
|
344
|
+
// Bypass the plugin at the dispatcher level too: a blacklisted host must
|
|
345
|
+
// work even when xray is restarting.
|
|
346
|
+
const existing = String(merged.noProxy ?? '')
|
|
347
|
+
.split(/[,\s]+/)
|
|
348
|
+
.map((entry) => entry.trim())
|
|
349
|
+
.filter(Boolean)
|
|
350
|
+
merged.noProxy = [...new Set([...existing, ...merged.routing.blacklist])].join(',')
|
|
351
|
+
}
|
|
319
352
|
|
|
320
353
|
const control = merged.control
|
|
321
354
|
control.enabled = asBool(control.enabled, false)
|
|
@@ -344,11 +377,22 @@ export function normalizeConfig(raw) {
|
|
|
344
377
|
if (inbound.protocol !== 'http' && inbound.protocol !== 'socks') {
|
|
345
378
|
errors.push({ field: 'inbound.protocol', message: 'inbound.protocol 只支持 http 或 socks' })
|
|
346
379
|
}
|
|
380
|
+
if (!['global', 'cn-direct', 'whitelist'].includes(merged.routing.mode)) {
|
|
381
|
+
errors.push({ field: 'routing.mode', message: 'routing.mode 只支持 global、cn-direct、whitelist' })
|
|
382
|
+
}
|
|
383
|
+
if (merged.routing.mode === 'whitelist' && merged.routing.whitelist.length === 0) {
|
|
384
|
+
warnings.push('routing.mode=whitelist 但白名单为空:所有流量都会直连')
|
|
385
|
+
}
|
|
347
386
|
if (!LOOPBACK_HOSTS.has(inbound.host)) {
|
|
348
387
|
errors.push({ field: 'inbound.host', message: '入站必须绑定回环地址,127.0.0.1 / ::1 / localhost' })
|
|
349
388
|
}
|
|
350
389
|
if (!Number.isInteger(inbound.port) || inbound.port < 1 || inbound.port > 65535) {
|
|
351
390
|
errors.push({ field: 'inbound.port', message: '入站端口必须是 1..65535 的整数' })
|
|
391
|
+
} else if (inbound.port < 1024 && process.platform !== 'win32') {
|
|
392
|
+
// A remote node port (e.g. 556) must never become the local listener:
|
|
393
|
+
// binding <1024 needs root on macOS/Linux and xray fails with EACCES.
|
|
394
|
+
warnings.push(`入站端口 ${inbound.port} 是特权端口,macOS/Linux 下无法绑定;已改用 ${DEFAULT_CONFIG.inbound.port}`)
|
|
395
|
+
inbound.port = DEFAULT_CONFIG.inbound.port
|
|
352
396
|
}
|
|
353
397
|
if (socks.enabled) {
|
|
354
398
|
if (!LOOPBACK_HOSTS.has(socks.host)) {
|
package/lib/route.js
CHANGED
|
@@ -3,7 +3,11 @@ import { dirname, join } from 'node:path'
|
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
5
|
const PROXY_PACKAGE = '@deepseek-ai/dsh-http-proxy'
|
|
6
|
-
|
|
6
|
+
// Route verification must not depend on the data plane it checks: a real
|
|
7
|
+
// target such as api.deepseek.com can legitimately be listed in noProxy, which
|
|
8
|
+
// would make a working install look unproxied. A reserved .invalid host is
|
|
9
|
+
// never bypassed; proxyRouteFor only resolves the route, it never fetches.
|
|
10
|
+
export const PROBE_URL = 'https://dsh-2xray-verify.invalid/'
|
|
7
11
|
const DISPATCHER_SYMBOLS = [Symbol.for('undici.globalDispatcher.2'), Symbol.for('undici.globalDispatcher.1')]
|
|
8
12
|
|
|
9
13
|
export class RouteError extends Error {
|
package/lib/schema.js
CHANGED
|
@@ -93,4 +93,18 @@ export const Config = Schema.object({
|
|
|
93
93
|
.description('启用 gRPC 控制面(多节点切换与流量统计,需 xray 2024-09+)。')
|
|
94
94
|
.volatile(),
|
|
95
95
|
}),
|
|
96
|
+
routing: Schema.object({
|
|
97
|
+
mode: Schema.string()
|
|
98
|
+
.default('global')
|
|
99
|
+
.description('global:全部走代理;cn-direct:国内 IP/域名直连;whitelist:仅白名单走代理。')
|
|
100
|
+
.volatile(),
|
|
101
|
+
whitelist: Schema.array(Schema.string())
|
|
102
|
+
.default(['github.com', 'google.com', 'youtube.com', 'x.com', 'facebook.com'])
|
|
103
|
+
.description('强制走 2Xray 的域名(匹配子域名)。')
|
|
104
|
+
.volatile(),
|
|
105
|
+
blacklist: Schema.array(Schema.string())
|
|
106
|
+
.default(['deepseek.com', 'api.deepseek.com'])
|
|
107
|
+
.description('强制直连的域名(优先于白名单与分流)。')
|
|
108
|
+
.volatile(),
|
|
109
|
+
}),
|
|
96
110
|
})
|
package/lib/supervisor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { rmSync } from 'node:fs'
|
|
3
|
+
import { ensureXrayAssets } from './assets.js'
|
|
3
4
|
import { buildXrayConfig } from './xray-config.js'
|
|
4
5
|
import { resolveBinary, probeVersion, tail } from './binary.js'
|
|
5
6
|
import { httpProxyProbe, portIsFree, waitForPort } from './health.js'
|
|
@@ -222,6 +223,15 @@ export class Supervisor {
|
|
|
222
223
|
this.#binary = binary
|
|
223
224
|
if (signal.aborted) return
|
|
224
225
|
|
|
226
|
+
if (this.dataDir) {
|
|
227
|
+
const routing = this.config.routing ?? {}
|
|
228
|
+
if (routing.mode === 'cn-direct' || routing.geositeCn || routing.blockAds) {
|
|
229
|
+
const assets = ensureXrayAssets({ binaryPath: binary.path, dataDir: this.dataDir, logger: this.#logger })
|
|
230
|
+
process.env.XRAY_LOCATION_ASSET = this.dataDir
|
|
231
|
+
if (assets.copied.length > 0) this.#logger?.info?.(`已复制 xray 数据文件:${assets.copied.join(', ')}`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
225
235
|
const version = await probeVersion({
|
|
226
236
|
launcher: this.#launcher,
|
|
227
237
|
binaryPath: binary.path,
|
package/lib/xray-config.js
CHANGED
|
@@ -156,6 +156,19 @@ export function buildXrayConfig(config) {
|
|
|
156
156
|
if (routing.blockAds) {
|
|
157
157
|
rules.push({ type: 'field', domain: ['geosite:category-ads-all'], outboundTag: 'block' })
|
|
158
158
|
}
|
|
159
|
+
const blacklist = dedupe(routing.blacklist ?? [])
|
|
160
|
+
if (blacklist.length > 0) {
|
|
161
|
+
// Highest priority: these hosts must stay direct even in whitelist mode.
|
|
162
|
+
rules.push({ type: 'field', domain: blacklist.map((entry) => `domain:${entry}`), outboundTag: 'direct' })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const whitelist = dedupe(routing.whitelist ?? []).filter((entry) => !blacklist.includes(entry))
|
|
166
|
+
const mode = routing.mode ?? 'global'
|
|
167
|
+
const proxyTarget = nodes.length > 1 ? { balancerTag: control.balancerTag } : { outboundTag: nodes[0].tag }
|
|
168
|
+
if (mode === 'whitelist' && whitelist.length > 0) {
|
|
169
|
+
rules.push({ type: 'field', domain: whitelist.map((entry) => `domain:${entry}`), ...proxyTarget })
|
|
170
|
+
}
|
|
171
|
+
|
|
159
172
|
rules.push({ type: 'field', ip: PRIVATE_IP_RULES, outboundTag: 'direct' })
|
|
160
173
|
|
|
161
174
|
const bypassDomains = dedupe(routing.bypassDomains)
|
|
@@ -166,9 +179,12 @@ export function buildXrayConfig(config) {
|
|
|
166
179
|
rules.push({ type: 'field', domain: bypassDomains.map((entry) => `domain:${entry}`), outboundTag: 'direct' })
|
|
167
180
|
}
|
|
168
181
|
|
|
169
|
-
if (routing.geositeCn) {
|
|
182
|
+
if (mode === 'cn-direct' || routing.geositeCn) {
|
|
170
183
|
rules.push({ type: 'field', domain: ['geosite:cn'], outboundTag: 'direct' })
|
|
171
184
|
}
|
|
185
|
+
if (mode === 'cn-direct') {
|
|
186
|
+
rules.push({ type: 'field', ip: ['geoip:cn'], outboundTag: 'direct' })
|
|
187
|
+
}
|
|
172
188
|
|
|
173
189
|
const balancers = []
|
|
174
190
|
if (nodes.length > 1) {
|
|
@@ -180,6 +196,11 @@ export function buildXrayConfig(config) {
|
|
|
180
196
|
selector: nodes.map((node) => `^${escapeRegExp(node.tag)}$`),
|
|
181
197
|
strategy: { type: 'random' },
|
|
182
198
|
})
|
|
199
|
+
}
|
|
200
|
+
if (mode === 'whitelist') {
|
|
201
|
+
// Only the whitelist above goes through xray; everything else is direct.
|
|
202
|
+
rules.push({ type: 'field', network: 'tcp,udp', outboundTag: 'direct' })
|
|
203
|
+
} else if (nodes.length > 1) {
|
|
183
204
|
rules.push({ type: 'field', network: 'tcp,udp', balancerTag: control.balancerTag })
|
|
184
205
|
} else {
|
|
185
206
|
rules.push({ type: 'field', network: 'tcp,udp', outboundTag: nodes[0].tag })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/dsh-2xray",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "DSH-2Xray: supervises xray-core and routes DSH process-wide HTTP(S) traffic through its local inbound, with optional multi-node gRPC control plane",
|
|
6
6
|
"type": "module",
|
|
@@ -73,7 +73,8 @@ const files = walk(ROOT)
|
|
|
73
73
|
const outside = files.filter((file) => !isAllowed(file))
|
|
74
74
|
check('every packaged file is allow-listed', outside.length === 0, outside.join(', ') || `${files.length} files`)
|
|
75
75
|
|
|
76
|
-
for (const required of ['index.js', 'package.json', 'cordis.patch.yml', 'README.md', 'CHANGELOG.md', 'LICENSE', 'lib/supervisor.js', 'lib/control/grpc.js',
|
|
76
|
+
for (const required of ['index.js', 'package.json', 'cordis.patch.yml', 'README.md', 'CHANGELOG.md', 'LICENSE', 'lib/supervisor.js', 'lib/control/grpc.js',
|
|
77
|
+
'tools/launch-with-browser-proxy.cmd', 'locale/en.json', 'locale/zh.json', 'icon.svg']) {
|
|
77
78
|
check(`required file ${required}`, files.includes(required))
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { assetName, parseDigest, sha256, dataBinDir } from '../tools/install-xray-core.mjs'
|
|
4
|
+
|
|
5
|
+
test('asset names follow the official release naming per platform', () => {
|
|
6
|
+
assert.equal(assetName('win32', 'x64'), 'Xray-windows-64.zip')
|
|
7
|
+
assert.equal(assetName('darwin', 'arm64'), 'Xray-macos-arm64-v8a.zip')
|
|
8
|
+
assert.equal(assetName('darwin', 'x64'), 'Xray-macos-64.zip')
|
|
9
|
+
assert.equal(assetName('linux', 'x64'), 'Xray-linux-64.zip')
|
|
10
|
+
assert.throws(() => assetName('aix', 'ppc64'), /unsupported/)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('the .dgst parser reads the SHA2-256 line', () => {
|
|
14
|
+
const digest = 'SHA2-256: ' + 'a'.repeat(64) + '\n'
|
|
15
|
+
assert.equal(parseDigest(digest), 'a'.repeat(64))
|
|
16
|
+
assert.throws(() => parseDigest('SHA1: abc'), /SHA2-256/)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('sha256 hashes the downloaded bytes', () => {
|
|
20
|
+
assert.equal(sha256(Buffer.from('abc')).length, 64)
|
|
21
|
+
assert.equal(dataBinDir().endsWith('bin'), true)
|
|
22
|
+
})
|
package/test/route.test.js
CHANGED
|
@@ -156,3 +156,11 @@ test('a socks inbound always takes the builtin path', async () => {
|
|
|
156
156
|
assert.equal(snapshot.mode, 'builtin')
|
|
157
157
|
await route.restore()
|
|
158
158
|
})
|
|
159
|
+
|
|
160
|
+
test('the policy verification host is reserved and cannot be in noProxy', async () => {
|
|
161
|
+
const { PROBE_URL } = await import('../lib/route.js')
|
|
162
|
+
const url = new URL(PROBE_URL)
|
|
163
|
+
assert.equal(url.hostname, 'dsh-2xray-verify.invalid')
|
|
164
|
+
assert.equal(url.hostname.endsWith('.invalid'), true)
|
|
165
|
+
assert.notEqual(url.hostname, 'api.deepseek.com')
|
|
166
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { normalizeConfig } from '../lib/config.js'
|
|
4
|
+
import { buildXrayConfig } from '../lib/xray-config.js'
|
|
5
|
+
|
|
6
|
+
function base(root = {}) {
|
|
7
|
+
return normalizeConfig({
|
|
8
|
+
enabled: true,
|
|
9
|
+
server: {
|
|
10
|
+
address: 'example.com',
|
|
11
|
+
port: 443,
|
|
12
|
+
uuid: '11111111-2222-4333-8444-555555555555',
|
|
13
|
+
serverName: 'www.example.com',
|
|
14
|
+
password: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
15
|
+
shortId: 'ab12',
|
|
16
|
+
},
|
|
17
|
+
...root,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rulesOf(config) {
|
|
22
|
+
return buildXrayConfig(config).routing.rules
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('global mode keeps the proxy catch-all and private direct rule', () => {
|
|
26
|
+
const { config, errors } = base()
|
|
27
|
+
assert.deepEqual(errors, [])
|
|
28
|
+
const rules = rulesOf(config)
|
|
29
|
+
assert.ok(rules.some((rule) => Array.isArray(rule.ip) && rule.ip.includes('10.0.0.0/8')))
|
|
30
|
+
const catchAll = rules[rules.length - 1]
|
|
31
|
+
assert.equal(catchAll.network, 'tcp,udp')
|
|
32
|
+
assert.equal(catchAll.outboundTag, 'proxy')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('cn-direct adds geosite:cn and geoip:cn direct rules', () => {
|
|
36
|
+
const { config } = base({ routing: { mode: 'cn-direct' } })
|
|
37
|
+
const rules = rulesOf(config)
|
|
38
|
+
assert.ok(rules.some((rule) => rule.domain?.includes('geosite:cn')))
|
|
39
|
+
assert.ok(rules.some((rule) => rule.ip?.includes('geoip:cn')))
|
|
40
|
+
assert.equal(rules[rules.length - 1].outboundTag, 'proxy')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('whitelist mode proxies only listed domains and defaults to direct', () => {
|
|
44
|
+
const { config } = base({ routing: { mode: 'whitelist', whitelist: ['github.com', 'youtube.com'] } })
|
|
45
|
+
const rules = rulesOf(config)
|
|
46
|
+
const proxyRule = rules.find((rule) => rule.domain?.includes('domain:github.com'))
|
|
47
|
+
assert.ok(proxyRule)
|
|
48
|
+
assert.equal(proxyRule.outboundTag, 'proxy')
|
|
49
|
+
const catchAll = rules[rules.length - 1]
|
|
50
|
+
assert.equal(catchAll.outboundTag, 'direct')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('blacklisted domains go direct and are added to noProxy', () => {
|
|
54
|
+
const { config } = base({ routing: { blacklist: ['https://api.deepseek.com/anthropic'] } })
|
|
55
|
+
const rules = rulesOf(config)
|
|
56
|
+
assert.equal(rules[0].outboundTag, 'direct')
|
|
57
|
+
assert.ok(rules[0].domain.includes('domain:api.deepseek.com'))
|
|
58
|
+
assert.match(config.noProxy, /(^|,)api\.deepseek\.com(,|$)/)
|
|
59
|
+
assert.equal(config.routing.blacklist.includes('api.deepseek.com'), true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('whitelist wins over bypassDomains but loses to the blacklist', () => {
|
|
63
|
+
const { config } = base({
|
|
64
|
+
routing: { mode: 'whitelist', whitelist: ['github.com'], blacklist: ['github.com'], bypassDomains: ['github.com'] },
|
|
65
|
+
})
|
|
66
|
+
const rules = rulesOf(config)
|
|
67
|
+
const black = rules.findIndex((rule) => rule.domain?.includes('domain:github.com') && rule.outboundTag === 'direct')
|
|
68
|
+
const white = rules.findIndex((rule) => rule.domain?.includes('domain:github.com') && rule.outboundTag === 'proxy')
|
|
69
|
+
assert.ok(black >= 0)
|
|
70
|
+
assert.equal(white, -1)
|
|
71
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install an official Xray-core binary into the plugin data directory.
|
|
4
|
+
*
|
|
5
|
+
* The npm package does not embed five platform cores (that is ~150 MB). Instead
|
|
6
|
+
* this one-shot installer downloads the official release asset for the current
|
|
7
|
+
* platform, verifies the published SHA2-256 digest, extracts it, and places the
|
|
8
|
+
* binary where the plugin already looks: <dataDir>/bin/xray(.exe).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node tools/install-xray-core.mjs [version]
|
|
12
|
+
* XRAY_VERSION=26.9.30 node tools/install-xray-core.mjs
|
|
13
|
+
* DSH_XRAY_MIRROR=https://mirror.example/Xray-core/releases/download node tools/install-xray-core.mjs
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { homedir, tmpdir } from 'node:os'
|
|
18
|
+
import { basename, dirname, join } from 'node:path'
|
|
19
|
+
import { spawnSync } from 'node:child_process'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const DEFAULT_VERSION = '26.9.30'
|
|
23
|
+
|
|
24
|
+
export function assetName(platform = process.platform, arch = process.arch) {
|
|
25
|
+
if (platform === 'win32' && arch === 'x64') return 'Xray-windows-64.zip'
|
|
26
|
+
if (platform === 'win32' && arch === 'arm64') return 'Xray-windows-arm64-v8a.zip'
|
|
27
|
+
if (platform === 'darwin' && arch === 'arm64') return 'Xray-macos-arm64-v8a.zip'
|
|
28
|
+
if (platform === 'darwin' && arch === 'x64') return 'Xray-macos-64.zip'
|
|
29
|
+
if (platform === 'linux' && arch === 'x64') return 'Xray-linux-64.zip'
|
|
30
|
+
if (platform === 'linux' && arch === 'arm64') return 'Xray-linux-arm64-v8a.zip'
|
|
31
|
+
throw new Error(`unsupported platform: ${platform}/${arch}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseDigest(text) {
|
|
35
|
+
const match = String(text).match(/SHA2-256:\s*([0-9a-f]{64})/i)
|
|
36
|
+
if (!match) throw new Error('release digest has no SHA2-256 value')
|
|
37
|
+
return match[1].toLowerCase()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sha256(buffer) {
|
|
41
|
+
return createHash('sha256').update(buffer).digest('hex')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function dataBinDir() {
|
|
45
|
+
const home = homedir()
|
|
46
|
+
return join(home, '.dsh', 'xray-proxy', 'bin')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function download(url) {
|
|
50
|
+
const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(180000) })
|
|
51
|
+
if (!response.ok) throw new Error(`${url} -> HTTP ${response.status}`)
|
|
52
|
+
return Buffer.from(await response.arrayBuffer())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extract(zip, destination) {
|
|
56
|
+
const attempts =
|
|
57
|
+
process.platform === 'win32'
|
|
58
|
+
? [['tar', ['-xf', zip, '-C', destination]], ['powershell', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${zip}' -DestinationPath '${destination}' -Force`]]]
|
|
59
|
+
: [['unzip', ['-o', zip, '-d', destination]], ['tar', ['-xf', zip, '-C', destination]]]
|
|
60
|
+
let lastError
|
|
61
|
+
for (const [command, args] of attempts) {
|
|
62
|
+
const result = spawnSync(command, args, { stdio: 'ignore' })
|
|
63
|
+
if (result.status === 0) return
|
|
64
|
+
lastError = result.error ?? new Error(`${command} exited ${result.status}`)
|
|
65
|
+
}
|
|
66
|
+
throw lastError
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findBinary(dir, name) {
|
|
70
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
71
|
+
const full = join(dir, entry.name)
|
|
72
|
+
if (entry.isFile() && entry.name === name) return full
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
const nested = findBinary(full, name)
|
|
75
|
+
if (nested) return nested
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function main() {
|
|
82
|
+
const version = process.argv[2] || process.env.XRAY_VERSION || DEFAULT_VERSION
|
|
83
|
+
const asset = assetName()
|
|
84
|
+
const mirror = (process.env.DSH_XRAY_MIRROR || 'https://github.com/XTLS/Xray-core/releases/download').replace(/\/$/, '')
|
|
85
|
+
const url = `${mirror}/v${version.replace(/^v/, '')}/${asset}`
|
|
86
|
+
const targetDir = dataBinDir()
|
|
87
|
+
const targetName = process.platform === 'win32' ? 'xray.exe' : 'xray'
|
|
88
|
+
const target = join(targetDir, targetName)
|
|
89
|
+
|
|
90
|
+
if (existsSync(target)) {
|
|
91
|
+
console.log(`already present: ${target}`)
|
|
92
|
+
return target
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`downloading ${url}`)
|
|
96
|
+
const zip = await download(url)
|
|
97
|
+
console.log(`verifying ${asset}.dgst`)
|
|
98
|
+
const digestText = (await download(`${url}.dgst`)).toString('utf8')
|
|
99
|
+
const expected = parseDigest(digestText)
|
|
100
|
+
const actual = sha256(zip)
|
|
101
|
+
if (expected !== actual) throw new Error(`digest mismatch: expected ${expected}, got ${actual}`)
|
|
102
|
+
|
|
103
|
+
const workspace = mkdtempSync(join(tmpdir(), 'dsh-xray-core-'))
|
|
104
|
+
try {
|
|
105
|
+
const archive = join(workspace, asset)
|
|
106
|
+
writeFileSync(archive, zip)
|
|
107
|
+
const extracted = join(workspace, 'out')
|
|
108
|
+
mkdirSync(extracted, { recursive: true })
|
|
109
|
+
extract(archive, extracted)
|
|
110
|
+
const binary = findBinary(extracted, targetName)
|
|
111
|
+
if (!binary) throw new Error(`no ${targetName} inside ${asset}`)
|
|
112
|
+
mkdirSync(targetDir, { recursive: true })
|
|
113
|
+
copyFileSync(binary, target)
|
|
114
|
+
if (process.platform !== 'win32') chmodSync(target, 0o755)
|
|
115
|
+
console.log(`installed ${target} (${Math.round(statSync(target).size / 1048576)} MB, v${version})`)
|
|
116
|
+
return target
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(workspace, { recursive: true, force: true })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
123
|
+
main().catch((error) => {
|
|
124
|
+
console.error(`install failed: ${error.message ?? error}`)
|
|
125
|
+
console.error('If GitHub is unreachable, set DSH_XRAY_MIRROR to a mirror and retry.')
|
|
126
|
+
process.exitCode = 1
|
|
127
|
+
})
|
|
128
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
rem Launch DeepSeek Harness with the embedded Chromium browser pointed at the
|
|
4
|
+
rem plugin's local HTTP inbound. A Node plugin cannot change Chromium's proxy at
|
|
5
|
+
rem runtime, so the switch has to be present when the app starts.
|
|
6
|
+
rem
|
|
7
|
+
rem Usage:
|
|
8
|
+
rem launch-with-browser-proxy.cmd ["C:\path\to\DeepSeek Harness.exe"]
|
|
9
|
+
rem
|
|
10
|
+
rem Close DSH completely before running this: a second launch only focuses the
|
|
11
|
+
rem existing single instance and never applies the flag.
|
|
12
|
+
|
|
13
|
+
set "PORT=20808"
|
|
14
|
+
set "EXE=%~1"
|
|
15
|
+
if not defined EXE set "EXE=%DSH_EXE%"
|
|
16
|
+
|
|
17
|
+
if not defined EXE if exist "%LOCALAPPDATA%\Programs\DeepSeek Harness\DeepSeek Harness.exe" set "EXE=%LOCALAPPDATA%\Programs\DeepSeek Harness\DeepSeek Harness.exe"
|
|
18
|
+
if not defined EXE if exist "%ProgramFiles%\DeepSeek Harness\DeepSeek Harness.exe" set "EXE=%ProgramFiles%\DeepSeek Harness\DeepSeek Harness.exe"
|
|
19
|
+
if not defined EXE if exist "E:\DS\DeepSeek Harness.exe" set "EXE=E:\DS\DeepSeek Harness.exe"
|
|
20
|
+
|
|
21
|
+
if not defined EXE (
|
|
22
|
+
echo DeepSeek Harness executable not found.
|
|
23
|
+
echo Pass the path explicitly:
|
|
24
|
+
echo %~nx0 "C:\path\to\DeepSeek Harness.exe"
|
|
25
|
+
exit /b 1
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
echo Launching with embedded browser proxy: http://127.0.0.1:%PORT%
|
|
29
|
+
start "" "%EXE%" --proxy-server=http://127.0.0.1:%PORT%
|
|
30
|
+
endlocal
|