@marktrue/dsh-mobile-remote 3.2.1-alpha.3 → 3.2.1-rc.1-app
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/DSH-Remote-v3.2.1.apk +4 -0
- package/README.md +6 -253
- package/manifest.json +21 -0
- package/package.json +12 -65
- package/CHANGELOG.md +0 -810
- package/CONTRIBUTING.md +0 -59
- package/FAQ.md +0 -111
- package/cordis.patch.yml +0 -14
- package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +0 -122
- package/docs/01-PRD.md +0 -148
- package/docs/02-architecture.md +0 -249
- package/docs/03-api.md +0 -816
- package/docs/04-security.md +0 -96
- package/docs/05-test-cases.md +0 -326
- package/docs/06-install-run.md +0 -308
- package/docs/07-user-manual.md +0 -65
- package/docs/08-/346/211/251/345/261/225/345/274/200/345/217/221/346/214/207/345/215/227.md +0 -86
- package/docs/09-compatibility.md +0 -195
- package/docs/10-release.md +0 -927
- package/docs/screenshots/01-home.png +0 -0
- package/docs/screenshots/02-notifications.png +0 -0
- package/docs/screenshots/03-sessions.png +0 -0
- package/docs/screenshots/04-chat.png +0 -0
- package/docs/screenshots/05-settings.png +0 -0
- package/docs/screenshots/06-new-session.png +0 -0
- package/docs/screenshots/pgy-appstore.jpg +0 -0
- package/docs/screenshots/pgy-mobile-app.jpg +0 -0
- package/docs/screenshots/pgy-pc-client.jpg +0 -0
- package/lib/account-usage.js +0 -145
- package/lib/app-alignment.js +0 -112
- package/lib/app-carrier-store.js +0 -1972
- package/lib/app-carrier.js +0 -602
- package/lib/client.js +0 -706
- package/lib/desktop-form.js +0 -738
- package/lib/endpoint-registry.js +0 -162
- package/lib/git-read-bundled-provider.js +0 -255
- package/lib/git-read-service.js +0 -322
- package/lib/index.js +0 -9292
- package/lib/plugin-content-digest.js +0 -80
- package/lib/plugin-self-update.js +0 -758
- package/lib/plugin-update-controller.js +0 -95
- package/lib/plugin-update-transaction.js +0 -322
- package/lib/plugin-update.js +0 -309
- package/lib/runtime-dependency-snapshot.js +0 -345
- package/lib/session-title-cache.js +0 -123
- package/lib/session-title-refresh.js +0 -261
- package/lib/update-fetch.js +0 -24
- package/lib/update-lock-arbiter.js +0 -158
- package/lib/update-supervisor.js +0 -312
- package/node_modules/ansi-regex/index.d.ts +0 -37
- package/node_modules/ansi-regex/index.js +0 -10
- package/node_modules/ansi-regex/license +0 -9
- package/node_modules/ansi-regex/package.json +0 -55
- package/node_modules/ansi-regex/readme.md +0 -78
- package/node_modules/ansi-styles/index.d.ts +0 -345
- package/node_modules/ansi-styles/index.js +0 -163
- package/node_modules/ansi-styles/license +0 -9
- package/node_modules/ansi-styles/package.json +0 -56
- package/node_modules/ansi-styles/readme.md +0 -152
- package/node_modules/camelcase/index.d.ts +0 -63
- package/node_modules/camelcase/index.js +0 -76
- package/node_modules/camelcase/license +0 -9
- package/node_modules/camelcase/package.json +0 -43
- package/node_modules/camelcase/readme.md +0 -99
- package/node_modules/cliui/CHANGELOG.md +0 -76
- package/node_modules/cliui/LICENSE.txt +0 -14
- package/node_modules/cliui/README.md +0 -115
- package/node_modules/cliui/index.js +0 -354
- package/node_modules/cliui/package.json +0 -65
- package/node_modules/color-convert/CHANGELOG.md +0 -54
- package/node_modules/color-convert/LICENSE +0 -21
- package/node_modules/color-convert/README.md +0 -68
- package/node_modules/color-convert/conversions.js +0 -839
- package/node_modules/color-convert/index.js +0 -81
- package/node_modules/color-convert/package.json +0 -48
- package/node_modules/color-convert/route.js +0 -97
- package/node_modules/color-name/LICENSE +0 -8
- package/node_modules/color-name/README.md +0 -11
- package/node_modules/color-name/index.js +0 -152
- package/node_modules/color-name/package.json +0 -28
- package/node_modules/decamelize/index.js +0 -13
- package/node_modules/decamelize/license +0 -21
- package/node_modules/decamelize/package.json +0 -38
- package/node_modules/decamelize/readme.md +0 -48
- package/node_modules/dijkstrajs/.travis.yml +0 -4
- package/node_modules/dijkstrajs/CONTRIBUTING.md +0 -8
- package/node_modules/dijkstrajs/LICENSE.md +0 -19
- package/node_modules/dijkstrajs/README.md +0 -18
- package/node_modules/dijkstrajs/dijkstra.js +0 -165
- package/node_modules/dijkstrajs/package.json +0 -60
- package/node_modules/dijkstrajs/test/dijkstra.test.js +0 -96
- package/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
- package/node_modules/emoji-regex/README.md +0 -73
- package/node_modules/emoji-regex/es2015/index.js +0 -6
- package/node_modules/emoji-regex/es2015/text.js +0 -6
- package/node_modules/emoji-regex/index.d.ts +0 -23
- package/node_modules/emoji-regex/index.js +0 -6
- package/node_modules/emoji-regex/package.json +0 -50
- package/node_modules/emoji-regex/text.js +0 -6
- package/node_modules/find-up/index.d.ts +0 -137
- package/node_modules/find-up/index.js +0 -89
- package/node_modules/find-up/license +0 -9
- package/node_modules/find-up/package.json +0 -53
- package/node_modules/find-up/readme.md +0 -156
- package/node_modules/get-caller-file/LICENSE.md +0 -6
- package/node_modules/get-caller-file/README.md +0 -41
- package/node_modules/get-caller-file/index.d.ts +0 -2
- package/node_modules/get-caller-file/index.js +0 -22
- package/node_modules/get-caller-file/index.js.map +0 -1
- package/node_modules/get-caller-file/package.json +0 -42
- package/node_modules/is-fullwidth-code-point/index.d.ts +0 -17
- package/node_modules/is-fullwidth-code-point/index.js +0 -50
- package/node_modules/is-fullwidth-code-point/license +0 -9
- package/node_modules/is-fullwidth-code-point/package.json +0 -42
- package/node_modules/is-fullwidth-code-point/readme.md +0 -39
- package/node_modules/locate-path/index.d.ts +0 -83
- package/node_modules/locate-path/index.js +0 -65
- package/node_modules/locate-path/license +0 -9
- package/node_modules/locate-path/package.json +0 -45
- package/node_modules/locate-path/readme.md +0 -122
- package/node_modules/p-limit/index.d.ts +0 -38
- package/node_modules/p-limit/index.js +0 -57
- package/node_modules/p-limit/license +0 -9
- package/node_modules/p-limit/package.json +0 -52
- package/node_modules/p-limit/readme.md +0 -101
- package/node_modules/p-locate/index.d.ts +0 -64
- package/node_modules/p-locate/index.js +0 -52
- package/node_modules/p-locate/license +0 -9
- package/node_modules/p-locate/package.json +0 -53
- package/node_modules/p-locate/readme.md +0 -90
- package/node_modules/p-try/index.d.ts +0 -39
- package/node_modules/p-try/index.js +0 -9
- package/node_modules/p-try/license +0 -9
- package/node_modules/p-try/package.json +0 -42
- package/node_modules/p-try/readme.md +0 -58
- package/node_modules/path-exists/index.d.ts +0 -28
- package/node_modules/path-exists/index.js +0 -23
- package/node_modules/path-exists/license +0 -9
- package/node_modules/path-exists/package.json +0 -39
- package/node_modules/path-exists/readme.md +0 -52
- package/node_modules/pngjs/.eslintignore +0 -2
- package/node_modules/pngjs/.eslintrc.json +0 -19
- package/node_modules/pngjs/.prettierignore +0 -3
- package/node_modules/pngjs/LICENSE +0 -20
- package/node_modules/pngjs/README.md +0 -429
- package/node_modules/pngjs/browser.js +0 -16096
- package/node_modules/pngjs/coverage/lcov-report/base.css +0 -354
- package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +0 -881
- package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +0 -554
- package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +0 -77
- package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +0 -647
- package/node_modules/pngjs/coverage/lcov-report/constants.js.html +0 -176
- package/node_modules/pngjs/coverage/lcov-report/crc.js.html +0 -200
- package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +0 -593
- package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +0 -152
- package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +0 -143
- package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +0 -611
- package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +0 -359
- package/node_modules/pngjs/coverage/lcov-report/index.html +0 -426
- package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +0 -365
- package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +0 -230
- package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +0 -248
- package/node_modules/pngjs/coverage/lcov-report/packer.js.html +0 -467
- package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +0 -128
- package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +0 -575
- package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +0 -404
- package/node_modules/pngjs/coverage/lcov-report/parser.js.html +0 -950
- package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +0 -116
- package/node_modules/pngjs/coverage/lcov-report/png.js.html +0 -662
- package/node_modules/pngjs/coverage/lcov-report/prettify.css +0 -101
- package/node_modules/pngjs/coverage/lcov-report/prettify.js +0 -1007
- package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/node_modules/pngjs/coverage/lcov-report/sorter.js +0 -169
- package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +0 -584
- package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +0 -215
- package/node_modules/pngjs/coverage/lcov.info +0 -2177
- package/node_modules/pngjs/lib/bitmapper.js +0 -267
- package/node_modules/pngjs/lib/bitpacker.js +0 -158
- package/node_modules/pngjs/lib/chunkstream.js +0 -189
- package/node_modules/pngjs/lib/constants.js +0 -32
- package/node_modules/pngjs/lib/crc.js +0 -40
- package/node_modules/pngjs/lib/filter-pack.js +0 -171
- package/node_modules/pngjs/lib/filter-parse-async.js +0 -24
- package/node_modules/pngjs/lib/filter-parse-sync.js +0 -21
- package/node_modules/pngjs/lib/filter-parse.js +0 -177
- package/node_modules/pngjs/lib/format-normaliser.js +0 -93
- package/node_modules/pngjs/lib/interlace.js +0 -95
- package/node_modules/pngjs/lib/packer-async.js +0 -50
- package/node_modules/pngjs/lib/packer-sync.js +0 -56
- package/node_modules/pngjs/lib/packer.js +0 -129
- package/node_modules/pngjs/lib/paeth-predictor.js +0 -16
- package/node_modules/pngjs/lib/parser-async.js +0 -165
- package/node_modules/pngjs/lib/parser-sync.js +0 -108
- package/node_modules/pngjs/lib/parser.js +0 -290
- package/node_modules/pngjs/lib/png-sync.js +0 -12
- package/node_modules/pngjs/lib/png.js +0 -194
- package/node_modules/pngjs/lib/sync-inflate.js +0 -168
- package/node_modules/pngjs/lib/sync-reader.js +0 -45
- package/node_modules/pngjs/package.json +0 -73
- package/node_modules/qrcode/README.md +0 -770
- package/node_modules/qrcode/bin/qrcode +0 -159
- package/node_modules/qrcode/helper/to-sjis-browser.js +0 -2
- package/node_modules/qrcode/helper/to-sjis.js +0 -105
- package/node_modules/qrcode/lib/browser.js +0 -76
- package/node_modules/qrcode/lib/can-promise.js +0 -7
- package/node_modules/qrcode/lib/core/alignment-pattern.js +0 -83
- package/node_modules/qrcode/lib/core/alphanumeric-data.js +0 -59
- package/node_modules/qrcode/lib/core/bit-buffer.js +0 -37
- package/node_modules/qrcode/lib/core/bit-matrix.js +0 -65
- package/node_modules/qrcode/lib/core/byte-data.js +0 -30
- package/node_modules/qrcode/lib/core/error-correction-code.js +0 -135
- package/node_modules/qrcode/lib/core/error-correction-level.js +0 -50
- package/node_modules/qrcode/lib/core/finder-pattern.js +0 -22
- package/node_modules/qrcode/lib/core/format-info.js +0 -29
- package/node_modules/qrcode/lib/core/galois-field.js +0 -69
- package/node_modules/qrcode/lib/core/kanji-data.js +0 -54
- package/node_modules/qrcode/lib/core/mask-pattern.js +0 -234
- package/node_modules/qrcode/lib/core/mode.js +0 -167
- package/node_modules/qrcode/lib/core/numeric-data.js +0 -43
- package/node_modules/qrcode/lib/core/polynomial.js +0 -62
- package/node_modules/qrcode/lib/core/qrcode.js +0 -495
- package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +0 -56
- package/node_modules/qrcode/lib/core/regex.js +0 -31
- package/node_modules/qrcode/lib/core/segments.js +0 -330
- package/node_modules/qrcode/lib/core/utils.js +0 -63
- package/node_modules/qrcode/lib/core/version-check.js +0 -9
- package/node_modules/qrcode/lib/core/version.js +0 -163
- package/node_modules/qrcode/lib/index.js +0 -12
- package/node_modules/qrcode/lib/renderer/canvas.js +0 -63
- package/node_modules/qrcode/lib/renderer/png.js +0 -78
- package/node_modules/qrcode/lib/renderer/svg-tag.js +0 -81
- package/node_modules/qrcode/lib/renderer/svg.js +0 -19
- package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +0 -85
- package/node_modules/qrcode/lib/renderer/terminal/terminal.js +0 -49
- package/node_modules/qrcode/lib/renderer/terminal.js +0 -9
- package/node_modules/qrcode/lib/renderer/utf8.js +0 -71
- package/node_modules/qrcode/lib/renderer/utils.js +0 -99
- package/node_modules/qrcode/lib/server.js +0 -138
- package/node_modules/qrcode/license +0 -10
- package/node_modules/qrcode/package.json +0 -77
- package/node_modules/require-directory/.jshintrc +0 -67
- package/node_modules/require-directory/.npmignore +0 -1
- package/node_modules/require-directory/.travis.yml +0 -3
- package/node_modules/require-directory/LICENSE +0 -22
- package/node_modules/require-directory/README.markdown +0 -184
- package/node_modules/require-directory/index.js +0 -86
- package/node_modules/require-directory/package.json +0 -40
- package/node_modules/require-main-filename/CHANGELOG.md +0 -26
- package/node_modules/require-main-filename/LICENSE.txt +0 -14
- package/node_modules/require-main-filename/README.md +0 -26
- package/node_modules/require-main-filename/index.js +0 -18
- package/node_modules/require-main-filename/package.json +0 -35
- package/node_modules/set-blocking/CHANGELOG.md +0 -26
- package/node_modules/set-blocking/LICENSE.txt +0 -14
- package/node_modules/set-blocking/README.md +0 -31
- package/node_modules/set-blocking/index.js +0 -7
- package/node_modules/set-blocking/package.json +0 -42
- package/node_modules/string-width/index.d.ts +0 -29
- package/node_modules/string-width/index.js +0 -47
- package/node_modules/string-width/license +0 -9
- package/node_modules/string-width/package.json +0 -56
- package/node_modules/string-width/readme.md +0 -50
- package/node_modules/strip-ansi/index.d.ts +0 -17
- package/node_modules/strip-ansi/index.js +0 -4
- package/node_modules/strip-ansi/license +0 -9
- package/node_modules/strip-ansi/package.json +0 -54
- package/node_modules/strip-ansi/readme.md +0 -46
- package/node_modules/which-module/LICENSE +0 -13
- package/node_modules/which-module/README.md +0 -58
- package/node_modules/which-module/index.js +0 -9
- package/node_modules/which-module/package.json +0 -41
- package/node_modules/wrap-ansi/index.js +0 -186
- package/node_modules/wrap-ansi/license +0 -9
- package/node_modules/wrap-ansi/package.json +0 -61
- package/node_modules/wrap-ansi/readme.md +0 -97
- package/node_modules/y18n/CHANGELOG.md +0 -35
- package/node_modules/y18n/LICENSE +0 -13
- package/node_modules/y18n/README.md +0 -109
- package/node_modules/y18n/index.js +0 -188
- package/node_modules/y18n/package.json +0 -39
- package/node_modules/yargs/CHANGELOG.md +0 -420
- package/node_modules/yargs/LICENSE +0 -21
- package/node_modules/yargs/README.md +0 -140
- package/node_modules/yargs/build/lib/apply-extends.d.ts +0 -2
- package/node_modules/yargs/build/lib/apply-extends.js +0 -65
- package/node_modules/yargs/build/lib/argsert.d.ts +0 -2
- package/node_modules/yargs/build/lib/argsert.js +0 -65
- package/node_modules/yargs/build/lib/command.d.ts +0 -64
- package/node_modules/yargs/build/lib/command.js +0 -416
- package/node_modules/yargs/build/lib/common-types.d.ts +0 -36
- package/node_modules/yargs/build/lib/common-types.js +0 -25
- package/node_modules/yargs/build/lib/completion-templates.d.ts +0 -2
- package/node_modules/yargs/build/lib/completion-templates.js +0 -50
- package/node_modules/yargs/build/lib/completion.d.ts +0 -21
- package/node_modules/yargs/build/lib/completion.js +0 -135
- package/node_modules/yargs/build/lib/is-promise.d.ts +0 -1
- package/node_modules/yargs/build/lib/is-promise.js +0 -9
- package/node_modules/yargs/build/lib/levenshtein.d.ts +0 -1
- package/node_modules/yargs/build/lib/levenshtein.js +0 -58
- package/node_modules/yargs/build/lib/middleware.d.ts +0 -10
- package/node_modules/yargs/build/lib/middleware.js +0 -57
- package/node_modules/yargs/build/lib/obj-filter.d.ts +0 -1
- package/node_modules/yargs/build/lib/obj-filter.js +0 -14
- package/node_modules/yargs/build/lib/parse-command.d.ts +0 -11
- package/node_modules/yargs/build/lib/parse-command.js +0 -36
- package/node_modules/yargs/build/lib/process-argv.d.ts +0 -2
- package/node_modules/yargs/build/lib/process-argv.js +0 -31
- package/node_modules/yargs/build/lib/usage.d.ts +0 -49
- package/node_modules/yargs/build/lib/usage.js +0 -540
- package/node_modules/yargs/build/lib/validation.d.ts +0 -34
- package/node_modules/yargs/build/lib/validation.js +0 -330
- package/node_modules/yargs/build/lib/yargs.d.ts +0 -274
- package/node_modules/yargs/build/lib/yargs.js +0 -1190
- package/node_modules/yargs/build/lib/yerror.d.ts +0 -4
- package/node_modules/yargs/build/lib/yerror.js +0 -11
- package/node_modules/yargs/index.js +0 -40
- package/node_modules/yargs/locales/be.json +0 -46
- package/node_modules/yargs/locales/de.json +0 -46
- package/node_modules/yargs/locales/en.json +0 -51
- package/node_modules/yargs/locales/es.json +0 -46
- package/node_modules/yargs/locales/fi.json +0 -49
- package/node_modules/yargs/locales/fr.json +0 -53
- package/node_modules/yargs/locales/hi.json +0 -49
- package/node_modules/yargs/locales/hu.json +0 -46
- package/node_modules/yargs/locales/id.json +0 -50
- package/node_modules/yargs/locales/it.json +0 -46
- package/node_modules/yargs/locales/ja.json +0 -51
- package/node_modules/yargs/locales/ko.json +0 -49
- package/node_modules/yargs/locales/nb.json +0 -44
- package/node_modules/yargs/locales/nl.json +0 -49
- package/node_modules/yargs/locales/nn.json +0 -44
- package/node_modules/yargs/locales/pirate.json +0 -13
- package/node_modules/yargs/locales/pl.json +0 -49
- package/node_modules/yargs/locales/pt.json +0 -45
- package/node_modules/yargs/locales/pt_BR.json +0 -48
- package/node_modules/yargs/locales/ru.json +0 -46
- package/node_modules/yargs/locales/th.json +0 -46
- package/node_modules/yargs/locales/tr.json +0 -48
- package/node_modules/yargs/locales/zh_CN.json +0 -48
- package/node_modules/yargs/locales/zh_TW.json +0 -47
- package/node_modules/yargs/package.json +0 -92
- package/node_modules/yargs/yargs.js +0 -14
- package/node_modules/yargs-parser/CHANGELOG.md +0 -601
- package/node_modules/yargs-parser/LICENSE.txt +0 -14
- package/node_modules/yargs-parser/README.md +0 -449
- package/node_modules/yargs-parser/index.js +0 -1032
- package/node_modules/yargs-parser/lib/tokenize-arg-string.js +0 -40
- package/node_modules/yargs-parser/package.json +0 -46
package/lib/desktop-form.js
DELETED
|
@@ -1,738 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// 桌面形态自适应(docs/design/desktop-form-adaptation.md §3):私有网卡地址筛选、LAN 桥
|
|
3
|
-
// 端点选择与端口持久化。
|
|
4
|
-
//
|
|
5
|
-
// 为什么单独成模块:自适应里真正有风险的是「选」这一步——多网卡时绑哪块、状态文件坏了
|
|
6
|
-
// 怎么办、候选端口全被占时会不会漂到区间外、显式端口被占时会不会偷偷改端口。这些必须能
|
|
7
|
-
// 脱离 ctx / HTTP / 真实 listen 单独钉死,所以这里只做纯决策 + 状态文件读写;真正的
|
|
8
|
-
// `net.createServer().listen()` 探测由接线方以 `canBind` 注入。
|
|
9
|
-
//
|
|
10
|
-
// 约定:
|
|
11
|
-
// - 失败一律返回 `{ ok:false, reason }`(reason 是稳定词表),不抛异常;状态文件坏掉
|
|
12
|
-
// 只应让本次重新选端口,绝不能让插件启动失败;
|
|
13
|
-
// - 不写 stdout:日志只经注入的 `log(message)` 回调(warn 级),避免污染宿主日志格式;
|
|
14
|
-
// - 自动模式安全默认:只挑 RFC1918 私有 IPv4,找不到就不开桥,绝不回退 0.0.0.0。
|
|
15
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
-
import { networkInterfaces } from "node:os";
|
|
17
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 自动模式的候选端口,顺序即探测顺序。跳过 web 宿主惯用的 3080;用满 3089 后报
|
|
21
|
-
* `no-free-port` 而不是随机挑高位端口——端口要与手机端配对,漂到区间外等于不可预期。
|
|
22
|
-
* @type {readonly number[]}
|
|
23
|
-
*/
|
|
24
|
-
export const BRIDGE_PORT_CANDIDATES = Object.freeze([3081, 3082, 3083, 3084, 3085, 3086, 3087, 3088, 3089]);
|
|
25
|
-
|
|
26
|
-
/** 状态文件 schema 版本;字段语义变化必须递增,旧/未知 schema 一律不信任(宁可重选端口)。 */
|
|
27
|
-
const BRIDGE_STATE_SCHEMA = 1;
|
|
28
|
-
|
|
29
|
-
/** profileKey 会拼进文件名:只放行安全字符,挡住 `../` 一类路径穿越。 */
|
|
30
|
-
const PROFILE_KEY_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
31
|
-
|
|
32
|
-
/** 严格解析点分十进制 IPv4;非 IPv4(IPv6、畸形串、越界)返回 null。 */
|
|
33
|
-
function parseIpv4(address) {
|
|
34
|
-
if (typeof address !== "string") return null;
|
|
35
|
-
const parts = address.split(".");
|
|
36
|
-
if (parts.length !== 4) return null;
|
|
37
|
-
const octets = [];
|
|
38
|
-
for (const part of parts) {
|
|
39
|
-
if (!/^\d{1,3}$/.test(part)) return null;
|
|
40
|
-
const value = Number(part);
|
|
41
|
-
if (value > 255) return null;
|
|
42
|
-
octets.push(value);
|
|
43
|
-
}
|
|
44
|
-
return octets;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** RFC1918 私有地址:10/8、172.16/12、192.168/16(回环/链路本地/公网自然不在此列)。 */
|
|
48
|
-
function isPrivateIpv4(address) {
|
|
49
|
-
const octets = parseIpv4(address);
|
|
50
|
-
if (!octets) return false;
|
|
51
|
-
const [first, second] = octets;
|
|
52
|
-
if (first === 10) return true;
|
|
53
|
-
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
54
|
-
return first === 192 && second === 168;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** 回环(127/8)与链路本地(169.254/16):即便 internal 标记缺失也不当作局域网地址。 */
|
|
58
|
-
function isLoopbackOrLinkLocal(address) {
|
|
59
|
-
const octets = parseIpv4(address);
|
|
60
|
-
if (!octets) return false;
|
|
61
|
-
return octets[0] === 127 || (octets[0] === 169 && octets[1] === 254);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Node 22 起 family 是字符串 "IPv4",老版本是数字 4:两种都认,其余一律不算 IPv4。 */
|
|
65
|
-
function isIpv4Family(family) {
|
|
66
|
-
return family === "IPv4" || family === 4;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 端口必须是 1..65535 的整数(0 表示"随机端口",不是可用配置值)。 */
|
|
70
|
-
function isValidPort(port) {
|
|
71
|
-
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** 状态文件路径:`<dshHome>/mobile-remote/lan-bridge/<profileKey>.json`。 */
|
|
75
|
-
function bridgeStatePath(dshHome, profileKey) {
|
|
76
|
-
return join(dshHome, "mobile-remote", "lan-bridge", `${profileKey}.json`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 校验并拼状态文件路径;不安全/缺失的输入返回 null(调用方据此返回稳定 reason)。 */
|
|
80
|
-
function safeStatePath(dshHome, profileKey) {
|
|
81
|
-
if (typeof dshHome !== "string" || dshHome.length === 0) return null;
|
|
82
|
-
if (!isSafeProfileKey(profileKey)) return null;
|
|
83
|
-
return bridgeStatePath(dshHome, profileKey);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function isSafeProfileKey(profileKey) {
|
|
87
|
-
return (
|
|
88
|
-
typeof profileKey === "string" &&
|
|
89
|
-
PROFILE_KEY_PATTERN.test(profileKey) &&
|
|
90
|
-
profileKey !== "." &&
|
|
91
|
-
profileKey !== ".."
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** 尽力同步目录项(平台不支持时忽略:正确性不依赖它,只有崩溃恢复语义有差异)。 */
|
|
96
|
-
function fsyncDir(dir) {
|
|
97
|
-
try {
|
|
98
|
-
const fd = openSync(dir, "r");
|
|
99
|
-
try {
|
|
100
|
-
fsyncSync(fd);
|
|
101
|
-
} finally {
|
|
102
|
-
closeSync(fd);
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
// 某些文件系统不允许对目录 fsync:忽略
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** 清理写在半途的临时文件;失败无伤大雅(残缺 .tmp 不会被读侧当作状态)。 */
|
|
110
|
-
function removeTempFile(file) {
|
|
111
|
-
try {
|
|
112
|
-
if (existsSync(file)) rmSync(file, { force: true });
|
|
113
|
-
} catch {
|
|
114
|
-
// 清理失败不改变本次写入结果
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 列出可作为局域网桥绑定地址的私有 IPv4,按系统网卡顺序去重。
|
|
120
|
-
* @param {NodeJS.Dict<Array<{ address: string, family: string|number, internal: boolean }>>} [interfaces]
|
|
121
|
-
* 默认取 `os.networkInterfaces()`;注入形态与它一致(测试用假网卡表)。
|
|
122
|
-
* @returns {string[]} 私有 IPv4 地址(可能是空数组:没有任何可用的局域网网卡)
|
|
123
|
-
*/
|
|
124
|
-
export function listPrivateIpv4(interfaces = networkInterfaces()) {
|
|
125
|
-
return listPrivateEntries(interfaces).map((entry) => entry.address);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** 虚拟网卡名字特征:这些网卡的私有地址通常手机不可达(WSL/Hyper-V/Docker/VMware/VirtualBox/隧道)。 */
|
|
129
|
-
const VIRTUAL_INTERFACE_PATTERN = /vmnet|vethernet|virtualbox|vmware|hyper-?v|wsl|docker|veth|virbr|utun|tailscale|zerotier|hamachi/i;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* 私有 IPv4 的"手机可达性"排序(越小越优先):
|
|
133
|
-
* 0 = 家庭/办公局域网常见段(192.168.x / 10.x)——手机最可能在同网段;
|
|
134
|
-
* 1 = 172.16-31(常被 Docker/WSL/NAT/组网虚拟网占用);
|
|
135
|
-
* 2 = Tailscale CGNAT(100.64/10);
|
|
136
|
-
* 3 = 其它私有段。
|
|
137
|
-
*
|
|
138
|
-
* 为什么需要排序而不是"取第一个":多网卡机器(Windows 的 WSL/Hyper-V、WSL 内的 docker0)
|
|
139
|
-
* 常把手机不可达的私有地址排在 OS 顺序前面(真机验收实测:WSL 下 172.17.255.124 排在
|
|
140
|
-
* 192.168.50.142 之前),直接取首个会把桥绑到手机连不上的地址,而诊断仍显示 listening=true。
|
|
141
|
-
* @param {string} address
|
|
142
|
-
* @returns {0|1|2|3}
|
|
143
|
-
*/
|
|
144
|
-
export function rankPrivateIpv4(address) {
|
|
145
|
-
const octets = parseIpv4(address);
|
|
146
|
-
if (!octets) return 3;
|
|
147
|
-
const [first, second] = octets;
|
|
148
|
-
if (first === 10 || (first === 192 && second === 168)) return 0;
|
|
149
|
-
if (first === 172 && second >= 16 && second <= 31) return 1;
|
|
150
|
-
if (first === 100 && second >= 64 && second <= 127) return 2;
|
|
151
|
-
return 3;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 带网卡名的私有地址条目(排序需要网卡名;对外 listPrivateIpv4 仍只给地址)。 */
|
|
155
|
-
function listPrivateEntries(interfaces = networkInterfaces()) {
|
|
156
|
-
const found = [];
|
|
157
|
-
const seen = new Set();
|
|
158
|
-
const table = interfaces && typeof interfaces === "object" ? interfaces : {};
|
|
159
|
-
for (const [name, entries] of Object.entries(table)) {
|
|
160
|
-
if (!Array.isArray(entries)) continue;
|
|
161
|
-
for (const entry of entries) {
|
|
162
|
-
if (!entry || typeof entry !== "object") continue;
|
|
163
|
-
if (entry.internal === true) continue;
|
|
164
|
-
if (!isIpv4Family(entry.family)) continue;
|
|
165
|
-
if (typeof entry.address !== "string") continue;
|
|
166
|
-
if (isLoopbackOrLinkLocal(entry.address)) continue;
|
|
167
|
-
if (!isPrivateIpv4(entry.address)) continue;
|
|
168
|
-
if (seen.has(entry.address)) continue;
|
|
169
|
-
seen.add(entry.address);
|
|
170
|
-
found.push({ address: entry.address, name: String(name ?? "") });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return found;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 首选私有 IPv4(自动模式的绑定地址),没有则 null。
|
|
178
|
-
*
|
|
179
|
-
* 规则:先排除"名字像虚拟网卡"的候选(仅当还有别的候选时),再按 rankPrivateIpv4 取最优、
|
|
180
|
-
* 同 rank 保持网卡顺序。全被判定为虚拟网卡时回退到全部候选——宁可绑一个不确定的地址,
|
|
181
|
-
* 也不要因为名字规则误判而拒绝开桥。
|
|
182
|
-
* @param {NodeJS.Dict<Array<{ address: string, family: string|number, internal: boolean }>>} [interfaces]
|
|
183
|
-
* @returns {string|null}
|
|
184
|
-
*/
|
|
185
|
-
export function pickPrivateAddress(interfaces = networkInterfaces()) {
|
|
186
|
-
const entries = listPrivateEntries(interfaces);
|
|
187
|
-
if (entries.length === 0) return null;
|
|
188
|
-
const physical = entries.filter((entry) => !VIRTUAL_INTERFACE_PATTERN.test(entry.name));
|
|
189
|
-
const pool = physical.length > 0 ? physical : entries;
|
|
190
|
-
let best = pool[0];
|
|
191
|
-
let bestRank = rankPrivateIpv4(best.address);
|
|
192
|
-
for (const entry of pool) {
|
|
193
|
-
const rank = rankPrivateIpv4(entry.address);
|
|
194
|
-
if (rank < bestRank) {
|
|
195
|
-
best = entry;
|
|
196
|
-
bestRank = rank;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return best.address;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 读取持久化的桥端点(设计 §3.3:重启后复用端口,手机不必重扫码)。
|
|
204
|
-
*
|
|
205
|
-
* 状态文件只是优化,坏了必须能自愈:坏 JSON、缺字段、端口越界、未知 schema、路径不安全
|
|
206
|
-
* 或不可读,一律返回 null,由上层重新选端口。
|
|
207
|
-
* @param {{ dshHome?: string, profileKey?: string }} [input]
|
|
208
|
-
* @returns {{ port: number, host: string } | null}
|
|
209
|
-
*/
|
|
210
|
-
export function readPersistedBridge(options = {}) {
|
|
211
|
-
const { dshHome, profileKey } = options ?? {};
|
|
212
|
-
const file = safeStatePath(dshHome, profileKey);
|
|
213
|
-
if (!file) return null;
|
|
214
|
-
let raw;
|
|
215
|
-
try {
|
|
216
|
-
raw = readFileSync(file, "utf8");
|
|
217
|
-
} catch {
|
|
218
|
-
return null; // 不存在 / 不可读:都视为无持久化状态
|
|
219
|
-
}
|
|
220
|
-
let parsed;
|
|
221
|
-
try {
|
|
222
|
-
parsed = JSON.parse(raw);
|
|
223
|
-
} catch {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
227
|
-
if (parsed.schema !== BRIDGE_STATE_SCHEMA) return null;
|
|
228
|
-
if (!isValidPort(parsed.port)) return null;
|
|
229
|
-
if (typeof parsed.host !== "string" || parsed.host.length === 0) return null;
|
|
230
|
-
return { port: parsed.port, host: parsed.host };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* 原子持久化桥端点(schema:1、0600)。
|
|
235
|
-
*
|
|
236
|
-
* 为什么是"同目录 tmp + rename":崩溃/并发时读侧要么看到旧状态、要么看到完整新状态,
|
|
237
|
-
* 不会读到写了一半的 JSON 而误判端口。0600 是安全契约(文件里有内网地址),显式
|
|
238
|
-
* chmod 是因为 open 的 mode 会被进程 umask 影响。
|
|
239
|
-
* @param {{ dshHome?: string, profileKey?: string, port?: number, host?: string }} [input]
|
|
240
|
-
* @returns {{ ok: true } | { ok: false, reason: string }} reason ∈
|
|
241
|
-
* missing-dsh-home / bad-profile-key / invalid-port / invalid-host /
|
|
242
|
-
* mkdir-failed / write-failed / rename-failed
|
|
243
|
-
*/
|
|
244
|
-
export function persistBridge(options = {}) {
|
|
245
|
-
const { dshHome, profileKey, port, host } = options ?? {};
|
|
246
|
-
if (typeof dshHome !== "string" || dshHome.length === 0) return { ok: false, reason: "missing-dsh-home" };
|
|
247
|
-
if (!isSafeProfileKey(profileKey)) return { ok: false, reason: "bad-profile-key" };
|
|
248
|
-
if (!isValidPort(port)) return { ok: false, reason: "invalid-port" };
|
|
249
|
-
if (typeof host !== "string" || host.length === 0) return { ok: false, reason: "invalid-host" };
|
|
250
|
-
const file = bridgeStatePath(dshHome, profileKey);
|
|
251
|
-
const dir = dirname(file);
|
|
252
|
-
try {
|
|
253
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
254
|
-
} catch {
|
|
255
|
-
return { ok: false, reason: "mkdir-failed" };
|
|
256
|
-
}
|
|
257
|
-
const tmp = `${file}.${process.pid}-${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
258
|
-
const payload = `${JSON.stringify({ schema: BRIDGE_STATE_SCHEMA, port, host, at: new Date().toISOString() }, null, 2)}\n`;
|
|
259
|
-
try {
|
|
260
|
-
const fd = openSync(tmp, "wx", 0o600);
|
|
261
|
-
try {
|
|
262
|
-
writeFileSync(fd, payload, "utf8");
|
|
263
|
-
fsyncSync(fd);
|
|
264
|
-
} finally {
|
|
265
|
-
closeSync(fd);
|
|
266
|
-
}
|
|
267
|
-
chmodSync(tmp, 0o600);
|
|
268
|
-
} catch {
|
|
269
|
-
removeTempFile(tmp);
|
|
270
|
-
return { ok: false, reason: "write-failed" };
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
renameSync(tmp, file);
|
|
274
|
-
} catch {
|
|
275
|
-
removeTempFile(tmp);
|
|
276
|
-
return { ok: false, reason: "rename-failed" };
|
|
277
|
-
}
|
|
278
|
-
fsyncDir(dir);
|
|
279
|
-
return { ok: true };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* 选择 LAN 桥的绑定端点(host + port)。
|
|
284
|
-
*
|
|
285
|
-
* 优先级(设计 §3.3/§3.4,显式永远覆盖自动):
|
|
286
|
-
* 1. host:显式 `explicitHost` > `pickAddress()`(自动模式只绑私有 IPv4);
|
|
287
|
-
* 两者都没有 → `no-host`;自动模式无私有地址 → `no-private-address`(不回退 0.0.0.0)。
|
|
288
|
-
* 2. port:显式 `explicitPort`(portSource=config;绑定失败即 `port-in-use`,**不自动改
|
|
289
|
-
* 端口**——用户给的端口是契约)> `persisted`(portSource=persisted)> 按 `candidates`
|
|
290
|
-
* 顺序探测(portSource=probe)> `no-free-port`(禁止向候选区间外漂移)。
|
|
291
|
-
*
|
|
292
|
-
* `canBind(host, port)` 是注入的异步谓词(生产实现用 net.createServer 试 listen 后 close;
|
|
293
|
-
* 测试用假实现)。它抛异常时该端口按不可用处理——探针失败不该拖垮启动。
|
|
294
|
-
* @param {{
|
|
295
|
-
* candidates?: number[],
|
|
296
|
-
* explicitPort?: number,
|
|
297
|
-
* explicitHost?: string,
|
|
298
|
-
* persisted?: { port?: unknown, host?: string } | null,
|
|
299
|
-
* canBind?: (host: string, port: number) => boolean | Promise<boolean>,
|
|
300
|
-
* pickAddress?: () => string | null,
|
|
301
|
-
* log?: (message: string) => void,
|
|
302
|
-
* }} [input]
|
|
303
|
-
* @returns {Promise<{ ok: true, host: string, port: number, portSource: "config"|"persisted"|"probe" }
|
|
304
|
-
* | { ok: false, reason: "no-host"|"no-private-address"|"invalid-explicit-port"|"port-in-use"|"no-free-port"|"missing-can-bind" }>}
|
|
305
|
-
*/
|
|
306
|
-
export async function selectBridgeEndpoint(options = {}) {
|
|
307
|
-
const {
|
|
308
|
-
candidates = BRIDGE_PORT_CANDIDATES,
|
|
309
|
-
explicitPort,
|
|
310
|
-
explicitHost,
|
|
311
|
-
persisted,
|
|
312
|
-
canBind,
|
|
313
|
-
pickAddress,
|
|
314
|
-
log = () => {},
|
|
315
|
-
} = options ?? {};
|
|
316
|
-
// 注入的日志回调也必须当成不可信输入:它抛异常不能改变选端点的结果。
|
|
317
|
-
const warn = (message) => {
|
|
318
|
-
try {
|
|
319
|
-
log(message);
|
|
320
|
-
} catch {
|
|
321
|
-
// 日志失败不影响决策
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
let host;
|
|
326
|
-
if (typeof explicitHost === "string" && explicitHost.length > 0) {
|
|
327
|
-
host = explicitHost;
|
|
328
|
-
} else {
|
|
329
|
-
if (typeof pickAddress !== "function") return { ok: false, reason: "no-host" };
|
|
330
|
-
let picked;
|
|
331
|
-
try {
|
|
332
|
-
picked = pickAddress();
|
|
333
|
-
} catch {
|
|
334
|
-
picked = null;
|
|
335
|
-
}
|
|
336
|
-
if (typeof picked !== "string" || picked.length === 0) return { ok: false, reason: "no-private-address" };
|
|
337
|
-
host = picked;
|
|
338
|
-
}
|
|
339
|
-
// 配置值先校验:非法端口没必要再去看绑定谓词是否可用。
|
|
340
|
-
if (explicitPort !== undefined && explicitPort !== null && !isValidPort(explicitPort)) {
|
|
341
|
-
return { ok: false, reason: "invalid-explicit-port" };
|
|
342
|
-
}
|
|
343
|
-
if (typeof canBind !== "function") return { ok: false, reason: "missing-can-bind" };
|
|
344
|
-
|
|
345
|
-
/** 探测某个端点是否可绑;谓词异常按不可用处理(fail-closed 到下一个候选)。 */
|
|
346
|
-
const bindable = async (port) => {
|
|
347
|
-
try {
|
|
348
|
-
return Boolean(await canBind(host, port));
|
|
349
|
-
} catch (err) {
|
|
350
|
-
warn(`mobile-remote: 桥端口探测异常(${host}:${port}):${err?.message ?? err}`);
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
if (explicitPort !== undefined && explicitPort !== null) {
|
|
356
|
-
if (await bindable(explicitPort)) return { ok: true, host, port: explicitPort, portSource: "config" };
|
|
357
|
-
return { ok: false, reason: "port-in-use" };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const tried = new Set();
|
|
361
|
-
const persistedPort = isValidPort(persisted?.port) ? persisted.port : null;
|
|
362
|
-
if (persistedPort !== null) {
|
|
363
|
-
tried.add(persistedPort);
|
|
364
|
-
if (await bindable(persistedPort)) return { ok: true, host, port: persistedPort, portSource: "persisted" };
|
|
365
|
-
warn(`mobile-remote: 持久化桥端口 ${persistedPort} 不可用,按候选序列重新探测`);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const list = Array.isArray(candidates) ? candidates : BRIDGE_PORT_CANDIDATES;
|
|
369
|
-
for (const port of list) {
|
|
370
|
-
if (!isValidPort(port)) continue;
|
|
371
|
-
if (tried.has(port)) continue; // 持久化端口刚试过,不重复探测
|
|
372
|
-
tried.add(port);
|
|
373
|
-
if (await bindable(port)) return { ok: true, host, port, portSource: "probe" };
|
|
374
|
-
}
|
|
375
|
-
warn(`mobile-remote: 候选端口全部不可用(no-free-port):${host}`);
|
|
376
|
-
return { ok: false, reason: "no-free-port" };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// ── 运行形态证据(#36 follow-up,issue note 1509 §3)────────────────────────
|
|
380
|
-
//
|
|
381
|
-
// 为什么不能只看 profile 名/目录后缀:名字与目录都是磁盘上可被复制/伪造的弱事实。
|
|
382
|
-
// 0.2.1-rc.2 的真实 Desktop 链路(本机实机核验)是:
|
|
383
|
-
// 1. Electron 主进程(`@deepseek-ai/dsh-desktop`)用 **Electron 可执行文件 + Node 模式**
|
|
384
|
-
// (`ELECTRON_RUN_AS_NODE=1`)spawn `@deepseek-ai/dsh-desktop-host` 宿主进程;
|
|
385
|
-
// 2. 该宿主进程硬编码 `runProfile({ profile: "desktop", resolvedProfile: {...} })`,
|
|
386
|
-
// 于是插件进程里的 `profileContext.name === "desktop"`、`dir === <home>/profiles/desktop`;
|
|
387
|
-
// 3. 该版本 Desktop profile 的 `startedBundles` 只是用户 profile 清单里的
|
|
388
|
-
// `dsh-base`/`dsh-web-app`/实验 bundle,**没有 Desktop 专属 marker**(不能当证据)。
|
|
389
|
-
// 因此可信 Desktop 证据 = env(既有)> 服务(既有)> profileContext 身份 +
|
|
390
|
-
// **Electron 宿主运行标记**(`process.versions.electron`,Electron 在 Node 模式下仍保留该字段,
|
|
391
|
-
// 已在本机对同版本 Desktop 运行时实测)。任何一项缺失都保持 unknown/fail-closed。
|
|
392
|
-
|
|
393
|
-
/** 形态证据来源(诊断 `formSource` 的稳定词表)。 */
|
|
394
|
-
export const FORM_SOURCE = Object.freeze({
|
|
395
|
-
ENVIRONMENT: "environment",
|
|
396
|
-
SERVICE: "desktopBrowserAccess",
|
|
397
|
-
PROFILE: "desktop-profile",
|
|
398
|
-
UNKNOWN: "unknown",
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* 平台可靠的目录规范化:解析 symlink/junction、去掉 `.`/`..` 与重复分隔符、统一大小写盘符。
|
|
403
|
-
* 不存在/不可读返回 null(调用方据此 fail-closed,绝不拿原始字符串硬猜)。
|
|
404
|
-
* @param {unknown} value
|
|
405
|
-
* @param {{ realpath?: (path: string) => string }} [options]
|
|
406
|
-
* @returns {string|null}
|
|
407
|
-
*/
|
|
408
|
-
export function canonicalHostPath(value, { realpath = realpathSync.native } = {}) {
|
|
409
|
-
if (typeof value !== "string" || value === "") return null;
|
|
410
|
-
// N4:宿主事实应为绝对路径;相对路径会按 cwd 解析而产生伪等价,直接 fail-closed。
|
|
411
|
-
if (!isAbsolute(value)) return null;
|
|
412
|
-
try {
|
|
413
|
-
return realpath(value);
|
|
414
|
-
} catch {
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* 平台目录等价判断:Windows 覆盖盘符/大小写/分隔符/junction(realpath 解析 + 大小写不敏感),
|
|
421
|
-
* 其它平台按 realpath 结果精确比较。任一侧不可规范化 → false(多义即不可信)。
|
|
422
|
-
* @param {string} left
|
|
423
|
-
* @param {string} right
|
|
424
|
-
* @param {{ canonicalize?: (path: string) => string|null, platform?: string }} [options]
|
|
425
|
-
* @returns {boolean}
|
|
426
|
-
*/
|
|
427
|
-
export function sameHostPath(left, right, { canonicalize = canonicalHostPath, platform = process.platform } = {}) {
|
|
428
|
-
const a = canonicalize(left);
|
|
429
|
-
const b = canonicalize(right);
|
|
430
|
-
if (typeof a !== "string" || typeof b !== "string" || a === "" || b === "") return false;
|
|
431
|
-
return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/** Desktop 宿主进程入口的**精确后缀**(Windows DSH 0.2.1-rc.2 实机核验的 argv[1] 形状)。 */
|
|
435
|
-
export const DESKTOP_HOST_ENTRY_SUFFIX = "@deepseek-ai/dsh-desktop-host/lib/index.js";
|
|
436
|
-
|
|
437
|
-
/** 仅做分隔符规范化(用于 marker 后缀匹配;不解析文件系统、不展开 symlink)。 */
|
|
438
|
-
function normalizeEntryPath(value) {
|
|
439
|
-
return String(value ?? "").replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/** 该启动参数是否精确指向 Desktop 宿主进程入口(必须是完整路径段后缀,不接受子串)。 */
|
|
443
|
-
export function isDesktopHostEntry(value) {
|
|
444
|
-
const entry = normalizeEntryPath(value);
|
|
445
|
-
if (entry === "" || !entry.startsWith("/") && !/^[A-Za-z]:\//.test(entry) && !entry.startsWith("//")) return false;
|
|
446
|
-
return entry.endsWith(`/${DESKTOP_HOST_ENTRY_SUFFIX}`);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* 在进程启动 argv 的**启动器控制前缀**(argv[1..3])中查找 Desktop 宿主入口。
|
|
451
|
-
*
|
|
452
|
-
* 为什么不用 `argv.includes(marker)`:应用参数(用户可传)也在 argv 里,任意位置匹配会把
|
|
453
|
-
* 用户参数当宿主证据;只检查 launch 前缀(argv[0] 是可执行文件,Node flags 在
|
|
454
|
-
* process.execArgv,Windows 实机核验见 test/desktop-form.test.mjs 的 argv 布局断言)。
|
|
455
|
-
* @param {string[]} [argv]
|
|
456
|
-
* @returns {string} 命中的原始 argv 项;未命中返回 ""
|
|
457
|
-
*/
|
|
458
|
-
export function desktopHostEntryOf(argv = process.argv) {
|
|
459
|
-
const list = Array.isArray(argv) ? argv : [];
|
|
460
|
-
const limit = Math.min(3, list.length - 1);
|
|
461
|
-
for (let index = 1; index <= limit; index += 1) {
|
|
462
|
-
if (isDesktopHostEntry(list[index])) return String(list[index]);
|
|
463
|
-
}
|
|
464
|
-
return "";
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* 解析当前(插件进程的)运行形态证据。
|
|
469
|
-
*
|
|
470
|
-
* 优先级(与 issue note 1509 一致):
|
|
471
|
-
* 1. 既有 `DSH_DESKTOP=1` 环境信号 → environment;
|
|
472
|
-
* 2. 既有 `desktopBrowserAccess` 服务信号 → desktopBrowserAccess;
|
|
473
|
-
* 3. 宿主提供的**当前活跃** `profileContext` + 经 Windows 实机证据核验的启动 marker:
|
|
474
|
-
* `profileContext.name==="desktop"` + canonical `dir===<home>/profiles/desktop` +
|
|
475
|
-
* `process.versions.electron` 非空 + 进程 argv 启动前缀含精确的
|
|
476
|
-
* `@deepseek-ai/dsh-desktop-host/lib/index.js` 入口 → desktop-profile;
|
|
477
|
-
* 4. 其它一律 cli + source unknown(附稳定 reason,便于诊断;绝不开启自动 LAN)。
|
|
478
|
-
*
|
|
479
|
-
* 不采信:basename、任意 `profiles/desktop` 目录(profileContext 之外)、cwd、
|
|
480
|
-
* `updateProfileDir`、`DSH_PROFILE`/shell 调用、祖先 package.json 依赖声明、其它非活跃安装。
|
|
481
|
-
*
|
|
482
|
-
* @param {{
|
|
483
|
-
* environment?: Record<string, string|undefined>,
|
|
484
|
-
* versions?: Record<string, string|undefined>,
|
|
485
|
-
* profileContext?: { name?: unknown, dir?: unknown, home?: unknown } | null,
|
|
486
|
-
* hasDesktopBrowserAccess?: boolean,
|
|
487
|
-
* argv?: string[],
|
|
488
|
-
* hostEntry?: string,
|
|
489
|
-
* canonicalize?: (path: string) => string|null,
|
|
490
|
-
* platform?: string,
|
|
491
|
-
* }} [input]
|
|
492
|
-
* @returns {{ form: "desktop"|"cli", source: string, reason: string }}
|
|
493
|
-
* reason ∈ ""(可信)、profile-context-missing、profile-context-invalid、
|
|
494
|
-
* profile-not-desktop、desktop-launch-evidence-missing、desktop-host-marker-missing、
|
|
495
|
-
* profile-dir-untrusted
|
|
496
|
-
*/
|
|
497
|
-
export function resolveRuntimeFormEvidence({
|
|
498
|
-
environment = process.env,
|
|
499
|
-
versions = process.versions,
|
|
500
|
-
profileContext,
|
|
501
|
-
hasDesktopBrowserAccess = false,
|
|
502
|
-
argv,
|
|
503
|
-
hostEntry,
|
|
504
|
-
canonicalize,
|
|
505
|
-
platform = process.platform,
|
|
506
|
-
} = {}) {
|
|
507
|
-
if (String(environment?.DSH_DESKTOP ?? "") === "1") {
|
|
508
|
-
return { form: "desktop", source: FORM_SOURCE.ENVIRONMENT, reason: "" };
|
|
509
|
-
}
|
|
510
|
-
if (hasDesktopBrowserAccess === true) {
|
|
511
|
-
return { form: "desktop", source: FORM_SOURCE.SERVICE, reason: "" };
|
|
512
|
-
}
|
|
513
|
-
if (!profileContext || typeof profileContext !== "object") {
|
|
514
|
-
return { form: "cli", source: FORM_SOURCE.UNKNOWN, reason: "profile-context-missing" };
|
|
515
|
-
}
|
|
516
|
-
const name = profileContext.name;
|
|
517
|
-
const dir = profileContext.dir;
|
|
518
|
-
const home = profileContext.home;
|
|
519
|
-
if (typeof name !== "string" || name === "" || typeof dir !== "string" || dir === "" || typeof home !== "string" || home === "") {
|
|
520
|
-
return { form: "cli", source: FORM_SOURCE.UNKNOWN, reason: "profile-context-invalid" };
|
|
521
|
-
}
|
|
522
|
-
if (name !== "desktop") {
|
|
523
|
-
// profileContext 是宿主的可信事实:它明说了当前活跃 profile 不是 desktop → 不猜。
|
|
524
|
-
return { form: "cli", source: FORM_SOURCE.UNKNOWN, reason: "profile-not-desktop" };
|
|
525
|
-
}
|
|
526
|
-
// 仅 name==="desktop" 不足以授权(note 1509):必须有该版本可核验的 Desktop 启动证据。
|
|
527
|
-
// 证据 1:进程确实运行在 Electron 运行时里(Node 模式也会保留该字段)。
|
|
528
|
-
const electron = typeof versions?.electron === "string" ? versions.electron : "";
|
|
529
|
-
if (electron === "") {
|
|
530
|
-
return { form: "cli", source: FORM_SOURCE.UNKNOWN, reason: "desktop-launch-evidence-missing" };
|
|
531
|
-
}
|
|
532
|
-
// 证据 2(W2):启动 argv 前缀必须精确指向 Desktop 私有宿主入口
|
|
533
|
-
// `@deepseek-ai/dsh-desktop-host/lib/index.js`。只靠 Electron 运行时不足以证明宿主身份
|
|
534
|
-
// (任意 Electron 应用都可能承载 dsh profile);startedBundles 在本版本没有 Desktop marker。
|
|
535
|
-
const resolvedHostEntry = typeof hostEntry === "string" ? hostEntry : desktopHostEntryOf(argv ?? process.argv);
|
|
536
|
-
if (!isDesktopHostEntry(resolvedHostEntry)) {
|
|
537
|
-
return { form: "cli", source: FORM_SOURCE.UNKNOWN, reason: "desktop-host-marker-missing" };
|
|
538
|
-
}
|
|
539
|
-
// 目录必须是宿主管理根下的 desktop 实际目录(平台可靠等价,覆盖盘符/大小写/分隔符/junction)。
|
|
540
|
-
if (!sameHostPath(dir, join(home, "profiles", "desktop"), { canonicalize, platform })) {
|
|
541
|
-
return { form: "cli", source: FORM_SOURCE.UNKNOWN, reason: "profile-dir-untrusted" };
|
|
542
|
-
}
|
|
543
|
-
return { form: "desktop", source: FORM_SOURCE.PROFILE, reason: "" };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// ── 手机端点视图(#36 follow-up,issue note 1509 §5)─────────────────────────
|
|
547
|
-
//
|
|
548
|
-
// 唯一权威:**真实监听事实** + 显式配置。绝不把"私有 IP + 回环 webserver 端口"拼成
|
|
549
|
-
// 看似 LAN 的地址(#36 实机误导 URL 的根因)。内部 worker/监督器 locator 不经过这里。
|
|
550
|
-
|
|
551
|
-
/** 回环主机名:localhost、127/8、IPv6 ::1、IPv4-mapped 回环(URL.hostname 可能带方括号)。 */
|
|
552
|
-
export function isLoopbackHostname(value) {
|
|
553
|
-
let host = String(value ?? "").trim().toLowerCase();
|
|
554
|
-
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
555
|
-
if (host === "localhost" || host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
|
|
556
|
-
if (host.startsWith("::ffff:")) {
|
|
557
|
-
// IPv4-mapped:点分形式递归;URL 规范化后的十六进制形式(如 ::ffff:7f00:1)解析最后 32 位。
|
|
558
|
-
const mapped = host.slice("::ffff:".length);
|
|
559
|
-
if (mapped.includes(".")) return isLoopbackHostname(mapped);
|
|
560
|
-
const parts = mapped.split(":");
|
|
561
|
-
if (parts.length === 2 && parts.every((part) => /^[0-9a-f]{1,4}$/.test(part))) {
|
|
562
|
-
const address = ((parseInt(parts[0], 16) << 16) | parseInt(parts[1], 16)) >>> 0;
|
|
563
|
-
return ((address >>> 24) & 0xff) === 127;
|
|
564
|
-
}
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return false;
|
|
568
|
-
const first = Number(host.split(".")[0]);
|
|
569
|
-
return first === 127;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/** 未指定/通配主机名:0.0.0.0、::、0:0:0:0:0:0:0:0。 */
|
|
573
|
-
export function isUnspecifiedHostname(value) {
|
|
574
|
-
let host = String(value ?? "").trim().toLowerCase();
|
|
575
|
-
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
576
|
-
return host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0";
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/** 取 URL 的 hostname(小写、去 IPv6 方括号);非 http(s)/不可解析返回 ""。 */
|
|
580
|
-
function originHostname(origin) {
|
|
581
|
-
try {
|
|
582
|
-
const url = new URL(String(origin));
|
|
583
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
|
|
584
|
-
return String(url.hostname ?? "").toLowerCase();
|
|
585
|
-
} catch {
|
|
586
|
-
return "";
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** 端点不可用时的稳定 hint(可执行、不含本机路径/凭据)。 */
|
|
591
|
-
function endpointHint(reason, fallback = "") {
|
|
592
|
-
switch (reason) {
|
|
593
|
-
case "bridge-starting":
|
|
594
|
-
return "LAN 桥正在启动,请稍候;连接页会自动重试(也可点「刷新连接信息」)。";
|
|
595
|
-
case "loopback-only":
|
|
596
|
-
return "电脑端 webserver 只监听回环地址且 LAN 桥未监听,手机无法连接。请在有局域网的桌面形态下运行,或显式配置 lanBridge.enabled/port;也可配置 publicOrigin/受信代理地址。";
|
|
597
|
-
case "bridge-failed":
|
|
598
|
-
return fallback !== "" ? fallback : "LAN 桥启动失败;请在电脑诊断页查看 lanBridge.reason 后重试。";
|
|
599
|
-
case "bridge-disabled":
|
|
600
|
-
return fallback !== "" ? fallback : "LAN 桥已按显式配置关闭;如手机需要连接请显式配置 lanBridge.enabled:true。";
|
|
601
|
-
case "form-unknown":
|
|
602
|
-
return fallback !== "" ? fallback : "宿主形态无法可信判定,未自动开启 LAN 桥;请显式配置 lanBridge.enabled:true。";
|
|
603
|
-
case "no-private-address":
|
|
604
|
-
return fallback !== "" ? fallback : "未找到私有局域网地址;请连接局域网或显式配置 lanBridge.host。";
|
|
605
|
-
case "no-free-port":
|
|
606
|
-
return fallback !== "" ? fallback : "候选端口 3081-3089 均不可用;请释放端口或显式配置 lanBridge.port。";
|
|
607
|
-
case "ipv6-listener-unsupported":
|
|
608
|
-
return "当前监听为 IPv6-only,暂不生成 IPv4 手机地址;请显式配置 IPv4 host(如 0.0.0.0 或具体局域网地址)。";
|
|
609
|
-
case "address-unavailable":
|
|
610
|
-
return "未找到可用的局域网 IPv4 地址;请连接局域网或配置 publicOrigin/受信代理地址。";
|
|
611
|
-
default:
|
|
612
|
-
return fallback;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/** 视图对象工厂(统一 shape,避免调用方各拼一套)。 */
|
|
617
|
-
function endpointView(state, source, origins, reason) {
|
|
618
|
-
return {
|
|
619
|
-
state,
|
|
620
|
-
source,
|
|
621
|
-
origins,
|
|
622
|
-
reason,
|
|
623
|
-
hint: state === "ready" ? "" : endpointHint(reason),
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* 计算"手机可用端点"视图(纯函数;真实监听事实/显式配置来自调用方)。
|
|
629
|
-
*
|
|
630
|
-
* 选择顺序(note 1509 §5):
|
|
631
|
-
* 1. 显式 `publicOrigin`(合法、非回环/通配);
|
|
632
|
-
* 2. **已真实监听**的 LAN 桥:具体 IP 只发布该 IP;通配 0.0.0.0 才枚举适用 IPv4 网卡;
|
|
633
|
-
* 绑到回环的桥不算手机端点;
|
|
634
|
-
* 3. webserver 实际可对外的监听端点(具体非回环 IP,或通配时枚举 IPv4 网卡);
|
|
635
|
-
* 4. 否则 `starting`(桥正在启动)或 `unavailable`(稳定 reason)。
|
|
636
|
-
* IPv6-only 监听(host 为 `::`)不捏造 IPv4 候选:返回 ipv6-listener-unsupported。
|
|
637
|
-
*
|
|
638
|
-
* @param {{
|
|
639
|
-
* configuredOrigin?: string|null,
|
|
640
|
-
* bridge?: { phase?: string, listening?: boolean, host?: string, port?: number, reason?: string, hint?: string },
|
|
641
|
-
* webServer?: { host?: string, port?: number },
|
|
642
|
-
* addresses?: string[],
|
|
643
|
-
* }} [input]
|
|
644
|
-
* @returns {{ state: "ready"|"starting"|"unavailable", source: "configured"|"lan-bridge"|"web-lan"|"none",
|
|
645
|
-
* origins: string[], reason: string, hint: string }}
|
|
646
|
-
*/
|
|
647
|
-
export function buildPhoneEndpointView({ configuredOrigin, bridge, webServer, addresses = [] } = {}) {
|
|
648
|
-
const cleanAddresses = [];
|
|
649
|
-
for (const value of Array.isArray(addresses) ? addresses : []) {
|
|
650
|
-
const address = String(value ?? "").trim();
|
|
651
|
-
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(address)) continue;
|
|
652
|
-
if (isLoopbackHostname(address) || isUnspecifiedHostname(address)) continue;
|
|
653
|
-
if (!cleanAddresses.includes(address)) cleanAddresses.push(address);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// 1) 显式对外基址:合法、非回环、非通配才认。
|
|
657
|
-
if (typeof configuredOrigin === "string" && configuredOrigin !== "") {
|
|
658
|
-
const hostname = originHostname(configuredOrigin);
|
|
659
|
-
if (hostname !== "" && !isLoopbackHostname(hostname) && !isUnspecifiedHostname(hostname)) {
|
|
660
|
-
return endpointView("ready", "configured", [configuredOrigin], "");
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const bridgeState = bridge && typeof bridge === "object" ? bridge : {};
|
|
665
|
-
const bridgePhase = typeof bridgeState.phase === "string"
|
|
666
|
-
? bridgeState.phase
|
|
667
|
-
: (bridgeState.listening === true ? "listening" : "pending");
|
|
668
|
-
const bridgePort = Number(bridgeState.port);
|
|
669
|
-
const bridgeListening = bridgePhase === "listening" && Number.isInteger(bridgePort) && bridgePort >= 1 && bridgePort <= 65535;
|
|
670
|
-
|
|
671
|
-
// 2) 已真实监听的 LAN 桥:只按实际 bind 事实生成。
|
|
672
|
-
if (bridgeListening) {
|
|
673
|
-
const host = String(bridgeState.host ?? "").trim();
|
|
674
|
-
if (!isLoopbackHostname(host)) {
|
|
675
|
-
if (isUnspecifiedHostname(host)) {
|
|
676
|
-
if (host.includes(":")) return endpointView("unavailable", "none", [], "ipv6-listener-unsupported");
|
|
677
|
-
const origins = cleanAddresses.map((address) => `http://${address}:${bridgePort}`);
|
|
678
|
-
return origins.length > 0
|
|
679
|
-
? endpointView("ready", "lan-bridge", origins, "")
|
|
680
|
-
: endpointView("unavailable", "none", [], "address-unavailable");
|
|
681
|
-
}
|
|
682
|
-
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) {
|
|
683
|
-
return endpointView("ready", "lan-bridge", [`http://${host}:${bridgePort}`], "");
|
|
684
|
-
}
|
|
685
|
-
// N1:具体 IPv6 监听(非 :: 通配、非回环)同样不能捏造 IPv4 候选。
|
|
686
|
-
if (host.includes(":")) return endpointView("unavailable", "none", [], "ipv6-listener-unsupported");
|
|
687
|
-
}
|
|
688
|
-
// 桥绑在回环上:手机不可达,继续看 webserver 的真实监听面。
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// 3) webserver 实际监听面(桌面版恒回环;web 版可能 0.0.0.0/具体网卡)。
|
|
692
|
-
const web = webServer && typeof webServer === "object" ? webServer : {};
|
|
693
|
-
const webHost = String(web.host ?? "").trim();
|
|
694
|
-
const webPort = Number(web.port);
|
|
695
|
-
const webPortValid = Number.isInteger(webPort) && webPort >= 1 && webPort <= 65535;
|
|
696
|
-
if (webPortValid && webHost !== "" && !isLoopbackHostname(webHost)) {
|
|
697
|
-
if (isUnspecifiedHostname(webHost)) {
|
|
698
|
-
if (webHost.includes(":")) return endpointView("unavailable", "none", [], "ipv6-listener-unsupported");
|
|
699
|
-
const origins = cleanAddresses.map((address) => `http://${address}:${webPort}`);
|
|
700
|
-
return origins.length > 0
|
|
701
|
-
? endpointView("ready", "web-lan", origins, "")
|
|
702
|
-
: endpointView("unavailable", "none", [], "address-unavailable");
|
|
703
|
-
}
|
|
704
|
-
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(webHost)) {
|
|
705
|
-
return endpointView("ready", "web-lan", [`http://${webHost}:${webPort}`], "");
|
|
706
|
-
}
|
|
707
|
-
// N1:具体 IPv6 监听(如 fe80::1/2001:db8::1)返回稳定 ipv6-listener-unsupported,
|
|
708
|
-
// 而不是落到 loopback-only/form-unknown 等误导性通用原因。
|
|
709
|
-
if (webHost.includes(":")) return endpointView("unavailable", "none", [], "ipv6-listener-unsupported");
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// 4) 都不可达:桥正在启动 → starting;否则稳定 reason(优先反映桥决策原因)。
|
|
713
|
-
if (bridgePhase === "starting" || bridgePhase === "pending") {
|
|
714
|
-
return endpointView("starting", "none", [], "bridge-starting");
|
|
715
|
-
}
|
|
716
|
-
const bridgeReason = typeof bridgeState.reason === "string" && bridgeState.reason !== "" ? bridgeState.reason : "";
|
|
717
|
-
const bridgeHint = typeof bridgeState.hint === "string" ? bridgeState.hint : "";
|
|
718
|
-
// 桥"listening 但绑回环"不是失败,只是手机不可达 → 落到最后的 loopback-only;
|
|
719
|
-
// 真正启动失败时保留桥决策的稳定 reason(listen-error/no-free-port/auth-not-configured…),
|
|
720
|
-
// 便于连接页直接显示可执行原因;没有具体原因才退回 bridge-failed。
|
|
721
|
-
if (bridgePhase === "failed") {
|
|
722
|
-
const reason = bridgeReason !== "" ? bridgeReason : "bridge-failed";
|
|
723
|
-
return { state: "unavailable", source: "none", origins: [], reason, hint: endpointHint(reason, bridgeHint) };
|
|
724
|
-
}
|
|
725
|
-
if (bridgePhase === "disabled") {
|
|
726
|
-
return { state: "unavailable", source: "none", origins: [], reason: "bridge-disabled", hint: endpointHint("bridge-disabled", bridgeHint) };
|
|
727
|
-
}
|
|
728
|
-
if (bridgePhase === "skipped" && bridgeReason !== "") {
|
|
729
|
-
return { state: "unavailable", source: "none", origins: [], reason: bridgeReason, hint: endpointHint(bridgeReason, bridgeHint) };
|
|
730
|
-
}
|
|
731
|
-
return {
|
|
732
|
-
state: "unavailable",
|
|
733
|
-
source: "none",
|
|
734
|
-
origins: [],
|
|
735
|
-
reason: "loopback-only",
|
|
736
|
-
hint: endpointHint("loopback-only"),
|
|
737
|
-
};
|
|
738
|
-
}
|