@yezack/dsh-remote-mobile 1.10.0 → 1.10.2

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.
@@ -2,19 +2,17 @@
2
2
  * DSH 底座运行模式探测器
3
3
  *
4
4
  * 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
5
- * 1. 若宿主仅监听本地回环地址 (127.0.0.1 / localhost),则外部网络无法直连,
6
- * 自适应判定为网关模式 ('gateway'),由插件启动独立安全反代网关 (0.0.0.0:3081);
5
+ * 1. 默认使用代理模式 ('proxy'):宿主仅监听本地回环地址 (127.0.0.1 / localhost),
6
+ * 由插件启动独立安全接入代理 (0.0.0.0:3081);
7
7
  * 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
8
- * 判定为直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
9
- * 3. 支持用户通过 options.mode ('auto' | 'gateway' | 'direct') 强制覆盖。
8
+ * 自动回退直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
9
+ * 3. 用户可通过 options.mode ('proxy' | 'direct') 显式选择;不再提供 auto 模式。
10
10
  */
11
- export type GatewayMode = 'gateway' | 'direct';
11
+ export type ProxyMode = 'proxy' | 'direct';
12
12
  export interface ServerModeDetectionResult {
13
13
  /** 最终生效的运行模式 */
14
- mode: GatewayMode;
15
- /** 是否为自动探测得出 */
16
- isAuto: boolean;
17
- /** 底座核心监听的 host(未配置时默认 127.0.0.1,在 gateway 模式下已安全规范化为回环 target) */
14
+ mode: ProxyMode;
15
+ /** 底座核心监听的 host(未配置时默认 127.0.0.1,在 proxy 模式下已安全规范化为回环 target) */
18
16
  coreHost: string;
19
17
  /** 底座核心监听的端口(默认 3080) */
20
18
  corePort: number;
@@ -28,7 +26,7 @@ export interface ServerModeDetectionResult {
28
26
  */
29
27
  export declare function isLoopbackHost(host?: string | null): boolean;
30
28
  /**
31
- * 规范化网关内部转发的目标核心 Host(仅在 gateway 模式下作为 proxy target 使用)
29
+ * 规范化安全接入代理内部转发的目标核心 Host(仅在 proxy 模式下作为 proxy target 使用)
32
30
  * 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
33
31
  */
34
32
  export declare function normalizeGatewayTargetHost(host: string): string;
@@ -36,7 +34,7 @@ export declare function normalizeGatewayTargetHost(host: string): string;
36
34
  * 探测底座运行姿态并裁决插件的工作模式
37
35
  *
38
36
  * @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
39
- * @param configuredMode 用户显式配置的运行模式 ('auto' | 'gateway' | 'direct')
37
+ * @param configuredMode 用户显式配置的运行模式 ('proxy' | 'direct')
40
38
  * @param defaultCorePort 底座默认核心端口(3080)
41
39
  */
42
- export declare function detectServerMode(ctx: any, configuredMode?: 'auto' | 'gateway' | 'direct', defaultCorePort?: number): ServerModeDetectionResult;
40
+ export declare function detectServerMode(ctx: any, configuredMode?: 'proxy' | 'direct', defaultCorePort?: number): ServerModeDetectionResult;
@@ -2,11 +2,11 @@
2
2
  * DSH 底座运行模式探测器
3
3
  *
4
4
  * 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
5
- * 1. 若宿主仅监听本地回环地址 (127.0.0.1 / localhost),则外部网络无法直连,
6
- * 自适应判定为网关模式 ('gateway'),由插件启动独立安全反代网关 (0.0.0.0:3081);
5
+ * 1. 默认使用代理模式 ('proxy'):宿主仅监听本地回环地址 (127.0.0.1 / localhost),
6
+ * 由插件启动独立安全接入代理 (0.0.0.0:3081);
7
7
  * 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
8
- * 判定为直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
9
- * 3. 支持用户通过 options.mode ('auto' | 'gateway' | 'direct') 强制覆盖。
8
+ * 自动回退直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
9
+ * 3. 用户可通过 options.mode ('proxy' | 'direct') 显式选择;不再提供 auto 模式。
10
10
  */
11
11
  /**
12
12
  * 判断指定 host 是否属于本地回环地址(支持 localhost, 127.*, ::1, 0:0:0:0:0:0:0:1 等完整形态)
@@ -29,7 +29,7 @@ export function isLoopbackHost(host) {
29
29
  return false;
30
30
  }
31
31
  /**
32
- * 规范化网关内部转发的目标核心 Host(仅在 gateway 模式下作为 proxy target 使用)
32
+ * 规范化安全接入代理内部转发的目标核心 Host(仅在 proxy 模式下作为 proxy target 使用)
33
33
  * 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
34
34
  */
35
35
  export function normalizeGatewayTargetHost(host) {
@@ -48,10 +48,10 @@ export function normalizeGatewayTargetHost(host) {
48
48
  * 探测底座运行姿态并裁决插件的工作模式
49
49
  *
50
50
  * @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
51
- * @param configuredMode 用户显式配置的运行模式 ('auto' | 'gateway' | 'direct')
51
+ * @param configuredMode 用户显式配置的运行模式 ('proxy' | 'direct')
52
52
  * @param defaultCorePort 底座默认核心端口(3080)
53
53
  */
54
- export function detectServerMode(ctx, configuredMode = 'auto', defaultCorePort = 3080) {
54
+ export function detectServerMode(ctx, configuredMode = 'proxy', defaultCorePort = 3080) {
55
55
  const webServer = (typeof ctx?.get === 'function' ? ctx.get('webServer') : undefined) || ctx?.webServer;
56
56
  const webStartup = (typeof ctx?.get === 'function' ? ctx.get('webStartup') : undefined) || ctx?.webStartup;
57
57
  // 提取宿主底座的 host 与 port 配置(保持原始字面量用于模式判定)
@@ -60,26 +60,13 @@ export function detectServerMode(ctx, configuredMode = 'auto', defaultCorePort =
60
60
  const rawCoreHost = String(coreHostRaw || '127.0.0.1').trim();
61
61
  const corePort = Number(corePortRaw) || defaultCorePort;
62
62
  const isLoopback = isLoopbackHost(rawCoreHost);
63
- let finalMode = 'direct';
64
- const isAuto = configuredMode === 'auto';
65
- if (configuredMode === 'gateway') {
66
- if (!isLoopback && rawCoreHost !== '0.0.0.0' && rawCoreHost !== '::') {
67
- throw new Error(`[dsh-remote-mobile] 无法在具体外部 IP ("${rawCoreHost}") 上启用网关模式 (mode: 'gateway')。网关模式要求底座监听在本地回环 (127.0.0.1);若需直接对外开放请使用 direct 模式。`);
68
- }
69
- finalMode = 'gateway';
70
- }
71
- else if (configuredMode === 'direct') {
72
- finalMode = 'direct';
73
- }
74
- else {
75
- // auto 自适应模式:底座为回环则启用网关,底座已对外开放(0.0.0.0 / 具体外部IP)则直连
76
- finalMode = isLoopback ? 'gateway' : 'direct';
77
- }
78
- // 仅对最终生效的 gateway 模式的目标核心地址执行 proxyTarget 规范化
79
- const coreHost = finalMode === 'gateway' ? normalizeGatewayTargetHost(rawCoreHost) : rawCoreHost;
63
+ // proxy 是默认模式,但只在核心回环时成立;核心已对外监听时回退 direct,
64
+ // 以兼容旧版 webserver 配置或用户显式绑定的具体网卡 IP。
65
+ const finalMode = configuredMode === 'direct' || !isLoopback ? 'direct' : 'proxy';
66
+ // 仅对最终生效的 proxy 模式的目标核心地址执行 proxyTarget 规范化
67
+ const coreHost = finalMode === 'proxy' ? normalizeGatewayTargetHost(rawCoreHost) : rawCoreHost;
80
68
  return {
81
69
  mode: finalMode,
82
- isAuto,
83
70
  coreHost,
84
71
  corePort,
85
72
  isLoopback,
@@ -1,19 +1,19 @@
1
1
  /**
2
- * DSH 移动端远程独立安全网关服务 (GatewayServer)
2
+ * DSH 移动端远程独立安全接入代理服务 (GatewayServer)
3
3
  *
4
4
  * 架构定位:
5
5
  * 解决 DSH 0.2.1+ 底层强行禁止 0.0.0.0 监听的问题。
6
6
  * DSH 官方 WebServer 继续保持纯粹、安全的 127.0.0.1:3080 本地回环监听(零外部暴露风险);
7
- * 由本服务在 0.0.0.0:3081 启动专用安全反代网关,接纳局域网与 Tailscale 流量,
7
+ * 由本服务在 0.0.0.0:3081 启动专用安全接入代理,接纳局域网与 Tailscale 流量,
8
8
  * 执行端到端 RSA 加密、扫码配对、慢哈希密码认证与防暴力破解 IP 熔断,
9
9
  * 并将合法的流量全双工代理至底座核心服务,彻底解决换 Wi-Fi 崩溃、无法兼顾多网卡与本机回环失效的痛点。
10
10
  */
11
11
  import http, { type IncomingMessage, type ServerResponse } from 'node:http';
12
12
  import type { SessionStore } from '../auth/token.js';
13
13
  export interface GatewayServerOptions {
14
- /** 网关监听端口,默认 3081 */
14
+ /** 安全接入代理监听端口,默认 3081 */
15
15
  port?: number;
16
- /** 网关监听网络地址,默认 0.0.0.0 */
16
+ /** 安全接入代理监听网络地址,默认 0.0.0.0 */
17
17
  host?: string;
18
18
  /** DSH 宿主核心服务端口,默认 3080 */
19
19
  corePort: number;
@@ -42,13 +42,13 @@ export declare class GatewayServer {
42
42
  private lastError;
43
43
  private logger?;
44
44
  constructor(options: GatewayServerOptions, store: SessionStore, gateMiddleware: (req: IncomingMessage, res: ServerResponse, next?: () => void) => boolean, logger?: any);
45
- /** 获取网关当前运行状态契约 */
45
+ /** 获取安全接入代理当前运行状态契约 */
46
46
  getStatus(): GatewayServerStatus;
47
47
  /** 获取网关最近发生的异常信息 */
48
48
  getLastError(): string | null;
49
- /** 获取网关实际生效绑定的端口 */
49
+ /** 获取安全接入代理实际生效绑定的端口 */
50
50
  getBoundPort(): number;
51
- /** 获取网关监听的 host */
51
+ /** 获取安全接入代理监听的 host */
52
52
  getHost(): string;
53
53
  /** 获取内部转发的目标核心端口 */
54
54
  getCorePort(): number;
@@ -73,14 +73,14 @@ export declare class GatewayServer {
73
73
  /** 请求是否携带反向代理透传头 */
74
74
  private hasForwardedHeaders;
75
75
  /**
76
- * 启动网关服务(带端口冲突自愈探测与状态机落定)
76
+ * 启动安全接入代理服务(带端口冲突自愈探测与状态机落定)
77
77
  */
78
78
  start(): Promise<{
79
79
  port: number;
80
80
  host: string;
81
81
  }>;
82
82
  /**
83
- * 优雅关闭网关服务,立即释放端口与所有客户端 Socket
83
+ * 优雅关闭安全接入代理,立即释放端口与所有客户端 Socket
84
84
  */
85
85
  close(): Promise<void>;
86
86
  /**
@@ -1,10 +1,10 @@
1
1
  /**
2
- * DSH 移动端远程独立安全网关服务 (GatewayServer)
2
+ * DSH 移动端远程独立安全接入代理服务 (GatewayServer)
3
3
  *
4
4
  * 架构定位:
5
5
  * 解决 DSH 0.2.1+ 底层强行禁止 0.0.0.0 监听的问题。
6
6
  * DSH 官方 WebServer 继续保持纯粹、安全的 127.0.0.1:3080 本地回环监听(零外部暴露风险);
7
- * 由本服务在 0.0.0.0:3081 启动专用安全反代网关,接纳局域网与 Tailscale 流量,
7
+ * 由本服务在 0.0.0.0:3081 启动专用安全接入代理,接纳局域网与 Tailscale 流量,
8
8
  * 执行端到端 RSA 加密、扫码配对、慢哈希密码认证与防暴力破解 IP 熔断,
9
9
  * 并将合法的流量全双工代理至底座核心服务,彻底解决换 Wi-Fi 崩溃、无法兼顾多网卡与本机回环失效的痛点。
10
10
  */
@@ -46,7 +46,7 @@ export class GatewayServer {
46
46
  this.gateMiddleware = gateMiddleware;
47
47
  this.logger = logger;
48
48
  }
49
- /** 获取网关当前运行状态契约 */
49
+ /** 获取安全接入代理当前运行状态契约 */
50
50
  getStatus() {
51
51
  return this.status;
52
52
  }
@@ -54,11 +54,11 @@ export class GatewayServer {
54
54
  getLastError() {
55
55
  return this.lastError;
56
56
  }
57
- /** 获取网关实际生效绑定的端口 */
57
+ /** 获取安全接入代理实际生效绑定的端口 */
58
58
  getBoundPort() {
59
59
  return this.boundPort;
60
60
  }
61
- /** 获取网关监听的 host */
61
+ /** 获取安全接入代理监听的 host */
62
62
  getHost() {
63
63
  return this.host;
64
64
  }
@@ -113,7 +113,7 @@ export class GatewayServer {
113
113
  req.headers['x-real-ip']);
114
114
  }
115
115
  /**
116
- * 启动网关服务(带端口冲突自愈探测与状态机落定)
116
+ * 启动安全接入代理服务(带端口冲突自愈探测与状态机落定)
117
117
  */
118
118
  async start() {
119
119
  if (this.server && this.status === 'running') {
@@ -149,7 +149,7 @@ export class GatewayServer {
149
149
  const addr = server.address();
150
150
  this.boundPort = addr && typeof addr === 'object' ? addr.port : currentPort;
151
151
  this.status = 'running';
152
- this.logger?.info?.(`[dsh-remote-mobile] 独立安全反向代理网关已就绪: http://${this.host}:${this.boundPort} -> 127.0.0.1:${this.corePort}`);
152
+ this.logger?.info?.(`[dsh-remote-mobile] 安全接入代理已就绪: http://${this.host}:${this.boundPort} -> ${this.coreHost}:${this.corePort}`);
153
153
  return { port: this.boundPort, host: this.host };
154
154
  }
155
155
  catch (err) {
@@ -166,10 +166,10 @@ export class GatewayServer {
166
166
  }
167
167
  this.status = 'error';
168
168
  this.lastError = `尝试端口均被占用 (${this.configuredPort} ~ ${currentPort})`;
169
- throw new Error(`[dsh-remote-mobile] 启动安全网关失败:${this.lastError}`);
169
+ throw new Error(`[dsh-remote-mobile] 启动安全接入代理失败:${this.lastError}`);
170
170
  }
171
171
  /**
172
- * 优雅关闭网关服务,立即释放端口与所有客户端 Socket
172
+ * 优雅关闭安全接入代理,立即释放端口与所有客户端 Socket
173
173
  */
174
174
  async close() {
175
175
  if (!this.server) {
@@ -215,7 +215,7 @@ export class GatewayServer {
215
215
  return;
216
216
  }
217
217
  // 网关已完成 Host 白名单校验,通知共享门禁跳过重复 Host 校验
218
- // (gatewayHost 可能是自定义域名,不属于核心的 trustedHosts / 网卡 IP)
218
+ // (proxyHost 可能是自定义域名,不属于核心的 trustedHosts / 网卡 IP)
219
219
  ;
220
220
  req.__dsh_rm_host_allowed__ = true;
221
221
  // 2. 门禁中间件校验与原因标记获取
package/lib/index.d.ts CHANGED
@@ -31,6 +31,7 @@ export * from './auth/token.js';
31
31
  export * from './auth/gate.js';
32
32
  export * from './routes/api.js';
33
33
  export * from './bridge/compat.js';
34
+ export * from './bridge/directory-picker.js';
34
35
  export * from './styles/style-snippets.js';
35
36
  export * from './gateway/detector.js';
36
37
  export * from './gateway/server.js';
package/lib/index.js CHANGED
@@ -12,17 +12,30 @@
12
12
  * 7. 配置持久化于插件自有文件 ~/.dsh/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
13
13
  * 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
14
14
  */
15
+ import { createRequire } from 'node:module';
16
+ import { installStartupFailureHint } from './startup-hint.js';
15
17
  import { SessionStore } from './auth/token.js';
16
18
  import { createGlobalAuthGate } from './auth/gate.js';
17
19
  import { createRoutes } from './routes/api.js';
20
+ import { registerWebRoutes } from './routes/register.js';
18
21
  import { StyleSnippetStore } from './styles/style-snippets.js';
19
22
  import { getLocalTailscaleIp, getLocalLanIp } from './auth/tailscale.js';
20
23
  import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, takeOverConnectionAuth, injectTrustedHosts, } from './bridge/compat.js';
24
+ import { bindDirectoryPickerRuntime, getDirectoryPickerStatus } from './bridge/directory-picker.js';
21
25
  import { detectServerMode } from './gateway/detector.js';
22
26
  import { GatewayServer } from './gateway/server.js';
23
27
  import { generateGatewaySecret, setGatewaySecret, clearGatewaySecret } from './gateway/secret.js';
28
+ // 该 hook 必须在模块顶层注册,才能捕获 webserver 在插件 apply 前的启动失败。
29
+ installStartupFailureHint();
24
30
  /** Cordis 插件名称标识 */
25
31
  export const name = 'dsh-remote-mobile';
32
+ /** 服务端 Banner 使用的插件版本(从安装包 package.json 读取,避免与客户端版本常量重复维护) */
33
+ let pluginVersion = '';
34
+ try {
35
+ const require = createRequire(import.meta.url);
36
+ pluginVersion = String(require('../package.json').version || '');
37
+ }
38
+ catch { }
26
39
  /** DSH 设置命名空间 */
27
40
  export const REMOTE_MOBILE_SETTINGS_NAMESPACE = 'dsh-remote-mobile';
28
41
  /** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
@@ -100,12 +113,10 @@ export function apply(ctx, config = {}) {
100
113
  }
101
114
  // 挂载 HTML 模板层的样式与 Polyfill 注入钩子(在 Gzip 压缩前注入)
102
115
  mountIndexInjections(ctx, styleStore);
103
- // 注册插件专属的 HTTP & SSE API 路由
116
+ // 注册插件专属的 HTTP & SSE API 路由。registerWebRoutes 会通过 ctx.effect
117
+ // 收集 DSH WebServer 返回的撤销函数,保证插件重新激活/热重载时不残留路由。
104
118
  if (typeof ctx.webServer.register === 'function') {
105
- const routes = createRoutes(store, styleStore);
106
- for (const route of routes) {
107
- ctx.webServer.register(route);
108
- }
119
+ registerWebRoutes(ctx, createRoutes(store, styleStore));
109
120
  }
110
121
  }
111
122
  // 4. 向 Cordis 容器桥接注册 remoteWebUiPairing 服务(延迟裁决:与其他远程接入
@@ -121,10 +132,18 @@ export function apply(ctx, config = {}) {
121
132
  * takeOverConnectionAuth 内部通过 __dsh_rm_auth_takeover__ 标记保证幂等,
122
133
  * 同一 connection 实例不会被重复接管。
123
134
  */
135
+ let connectionAuthAttached = false;
124
136
  const attachConnectionAuth = (conn) => {
125
- if (conn) {
137
+ if (!conn)
138
+ return false;
139
+ try {
126
140
  takeOverConnectionAuth(conn, store);
141
+ connectionAuthAttached = Boolean(conn.__dsh_rm_auth_takeover__);
142
+ }
143
+ catch {
144
+ connectionAuthAttached = false;
127
145
  }
146
+ return connectionAuthAttached;
128
147
  };
129
148
  // 1) 优先通过 ctx.get 安全获取(未注入时不抛错返回 undefined)
130
149
  try {
@@ -170,9 +189,8 @@ export function apply(ctx, config = {}) {
170
189
  }
171
190
  catch { }
172
191
  }
173
- // 6. 探测 DSH 底座网络暴露姿态,自适应裁决是否拉起独立安全反向代理网关
174
- // 配置错误(如 mode: 'gateway' + 具体 LAN IP)不能把整个插件 apply 抛崩;
175
- // 捕获后回退到 direct 模式,让用户仍可通过 3080 本机/局域网访问并修改配置。
192
+ // 6. 探测 DSH 底座网络暴露姿态:默认 proxy;核心已对外监听时回退 direct。
193
+ // 检测逻辑本身不抛错,异常时仍保留兜底,避免插件 apply 崩溃。
176
194
  let modeFallbackError = '';
177
195
  let detection;
178
196
  try {
@@ -180,37 +198,44 @@ export function apply(ctx, config = {}) {
180
198
  }
181
199
  catch (err) {
182
200
  modeFallbackError = err?.message || String(err);
183
- ctx.logger?.error?.('[dsh-remote-mobile] 网关模式不可用,已回退 direct 模式:', err);
201
+ ctx.logger?.error?.('[dsh-remote-mobile] 安全接入代理配置不可用,已回退 direct 模式:', err);
184
202
  detection = detectServerMode(ctx, 'direct', ctx.webServer?.port ?? 3080);
185
203
  }
186
- // 强制 gateway 且底座已监听通配地址时,输出安全提醒:核心 3080 仍暴露,
187
- // 网关只是附加入口,不能替代核心侧的 Host/Origin 防护。
188
- const modeWarning = detection.mode === 'gateway' && !detection.isLoopback
189
- ? `底座本身已监听 ${detection.rawCoreHost},3080 仍是对外入口;网关 3081 为附加入口,建议优先恢复 127.0.0.1 监听。`
190
- : '';
204
+ // proxy 是默认模式,但核心已对外监听时会回退 direct:
205
+ // - 旧版 webserver 配置(0.0.0.0 / 具体网卡 IP)继续可用;
206
+ // - direct + 回环核心时,外部手机无法直连,仅适合同机反向代理。
207
+ bindDirectoryPickerRuntime(ctx, () => detection.isLoopback);
208
+ const requestedMode = store.getOptions().mode ?? 'proxy';
209
+ const legacyDirectFallback = requestedMode === 'proxy' && detection.mode === 'direct' && !detection.isLoopback;
210
+ const directLoopbackWarning = detection.mode === 'direct' && detection.isLoopback;
211
+ const modeWarning = legacyDirectFallback
212
+ ? `检测到底座监听 ${detection.rawCoreHost},已自动使用 direct 模式;3080 直接对外,如需 proxy 请恢复 127.0.0.1 监听。`
213
+ : directLoopbackWarning
214
+ ? 'direct 模式 + 回环核心:外部手机无法直连,仅适合同机反向代理。'
215
+ : '';
191
216
  let gatewayServer = null;
192
217
  let gatewayReadyPromise = null;
193
- if (detection.mode === 'gateway') {
218
+ if (detection.mode === 'proxy') {
194
219
  const opts = store.getOptions();
195
220
  gatewayServer = new GatewayServer({
196
- port: opts.gatewayPort ?? 3081,
197
- host: opts.gatewayHost ?? '0.0.0.0',
221
+ port: opts.proxyPort ?? 3081,
222
+ host: opts.proxyHost ?? '0.0.0.0',
198
223
  corePort: detection.corePort,
199
224
  coreHost: detection.coreHost,
200
225
  }, store, gateMiddleware, ctx.logger);
201
226
  // 初始标记为正在启动中
202
- store.setActiveGatewayInfo('gateway', opts.gatewayPort ?? 3081, detection.corePort, false);
227
+ store.setActiveGatewayInfo('proxy', opts.proxyPort ?? 3081, detection.corePort, false);
203
228
  gatewayReadyPromise = gatewayServer.start().then(({ port }) => {
204
229
  if (gatewayServer?.getServer()) {
205
230
  setGatewaySecret(gatewayServer.getServer(), gatewaySecret);
206
231
  }
207
- store.setActiveGatewayInfo('gateway', port, detection.corePort, true);
232
+ store.setActiveGatewayInfo('proxy', port, detection.corePort, true);
208
233
  syncTrustedHosts();
209
234
  return port;
210
235
  }).catch((err) => {
211
- store.setActiveGatewayInfo('gateway', opts.gatewayPort ?? 3081, detection.corePort, false, err?.message || String(err));
212
- ctx.logger?.error?.('[dsh-remote-mobile] 独立安全反向代理网关启动失败:', err);
213
- return opts.gatewayPort ?? 3081;
236
+ store.setActiveGatewayInfo('proxy', opts.proxyPort ?? 3081, detection.corePort, false, err?.message || String(err));
237
+ ctx.logger?.error?.('[dsh-remote-mobile] 安全接入代理启动失败:', err);
238
+ return opts.proxyPort ?? 3081;
214
239
  });
215
240
  }
216
241
  else {
@@ -245,7 +270,7 @@ export function apply(ctx, config = {}) {
245
270
  if (disposed || printed)
246
271
  return;
247
272
  printed = true;
248
- // 等待网关就绪以获得准确的对外端口(避免端口自增探测竞态)
273
+ // 等待安全接入代理就绪以获得准确的对外端口(避免端口自增探测竞态)
249
274
  try {
250
275
  if (gatewayReadyPromise) {
251
276
  await Promise.race([
@@ -279,21 +304,34 @@ export function apply(ctx, config = {}) {
279
304
  const activePort = store.getActivePort();
280
305
  const activeMode = store.getActiveMode();
281
306
  const corePort = store.getCorePort();
307
+ const coreHost = detection.coreHost;
282
308
  const opts = store.getOptions();
283
- const gwStatus = store.getGatewayStatus();
309
+ const proxyHost = opts.proxyHost ?? '0.0.0.0';
310
+ const proxyStatus = store.getGatewayStatus();
311
+ const versionLabel = pluginVersion ? `v${pluginVersion}` : 'unknown';
312
+ const coreLabel = detection.isLoopback ? '回环' : '对外监听';
313
+ const modeLabel = activeMode === 'proxy'
314
+ ? 'proxy(默认)'
315
+ : legacyDirectFallback
316
+ ? 'direct(旧配置回退)'
317
+ : 'direct(显式配置)';
284
318
  const divider = '─'.repeat(72);
285
319
  console.log(`\n┌${divider}`);
286
- console.log(`│ [dsh-remote-mobile] 官方临时 Token 鉴权已接管,PC/移动端访问无需携带官方token`);
320
+ console.log(connectionAuthAttached
321
+ ? `│ [dsh-remote-mobile] ${versionLabel} 官方临时 Token 鉴权已接管,PC/移动端访问无需携带官方token`
322
+ : `│ [dsh-remote-mobile] ${versionLabel} ⚠️ 官方临时 Token 鉴权接管未完成,请检查 DSH 版本或启动日志`);
323
+ console.log(`│ DSH 核心: ${detection.rawCoreHost}:${corePort} (${coreLabel})`);
324
+ console.log(`│ 运行模式: ${modeLabel}`);
287
325
  if (modeFallbackError) {
288
- console.log(`│ ⚠️ 网关配置不可用,已回退 direct 模式: ${modeFallbackError}`);
326
+ console.log(`│ ⚠️ 安全接入代理配置不可用,已回退 direct 模式: ${modeFallbackError}`);
289
327
  }
290
328
  if (modeWarning) {
291
329
  console.log(`│ ⚠️ ${modeWarning}`);
292
330
  }
293
- if (activeMode === 'gateway') {
294
- if (gwStatus.running) {
295
- console.log(`│ 独立安全反代网关已激活 (0.0.0.0:${activePort} -> 127.0.0.1:${corePort})`);
296
- console.log(`│ 手机/远程设备请访问安全网关地址:`);
331
+ if (activeMode === 'proxy') {
332
+ if (proxyStatus.running) {
333
+ console.log(`│ 安全接入代理已激活 (${proxyHost}:${activePort} -> ${coreHost}:${corePort})`);
334
+ console.log(`│ 手机/远程设备请访问安全接入代理地址:`);
297
335
  if (lanIp && lanIp !== '127.0.0.1') {
298
336
  console.log(`│ • 局域网: http://${lanIp}:${activePort} (同 Wi-Fi 局域网直连)`);
299
337
  }
@@ -301,17 +339,22 @@ export function apply(ctx, config = {}) {
301
339
  console.log(`│ • 虚拟私网: http://${tsIp}:${activePort} (Tailscale 私网异地直连)`);
302
340
  }
303
341
  console.log(`│ 电脑本机浏览器访问控制台:`);
304
- console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
342
+ console.log(`│ • 核心直连: http://127.0.0.1:${corePort}`);
343
+ if (proxyHost === '0.0.0.0' || proxyHost === '::' || proxyHost === '127.0.0.1' || proxyHost === 'localhost') {
344
+ const localProxyHost = proxyHost === '::' ? '[::1]' : '127.0.0.1';
345
+ console.log(`│ • 代理入口: http://${localProxyHost}:${activePort}`);
346
+ console.log(`│ 本机直连不校验配对码/长期密码`);
347
+ }
305
348
  }
306
349
  else {
307
- console.log(`│ ⚠️ 独立安全反代网关未成功启动: ${gwStatus.error || '端口占用'}`);
308
- console.log(`│ 外部手机设备当前暂时无法连接,请检查 3081 端口占用`);
350
+ console.log(`│ ⚠️ 安全接入代理未成功启动: ${proxyStatus.error || '端口占用'}`);
351
+ console.log(`│ 外部手机设备当前暂时无法连接,请检查 ${opts.proxyPort ?? 3081} 端口占用`);
309
352
  console.log(`│ 电脑本机浏览器仍可直接访问:`);
310
353
  console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
311
354
  }
312
355
  }
313
356
  else {
314
- console.log(`│ 远程安全网关已就绪,支持手机浏览器访问:`);
357
+ console.log(`│ 直连模式已启用(无独立安全接入代理),支持手机浏览器访问:`);
315
358
  console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
316
359
  if (lanIp && lanIp !== '127.0.0.1') {
317
360
  console.log(`│ • 局域网: http://${lanIp}:${corePort} (同 Wi-Fi 局域网直连)`);
@@ -320,10 +363,17 @@ export function apply(ctx, config = {}) {
320
363
  console.log(`│ • 虚拟私网: http://${tsIp}:${corePort} (Tailscale 私网异地直连)`);
321
364
  }
322
365
  }
366
+ const dirPickerStatus = getDirectoryPickerStatus();
367
+ if (dirPickerStatus.needsWarning) {
368
+ console.log(`│ ⚠️ 移动端选目录提示: 当前未开启网页内目录选择器,手机点「选择文件夹」会在电脑弹窗`);
369
+ console.log(`│ 可在 Web「设置」->「远程与移动端」顶部一键开启网页目录选择器`);
370
+ }
323
371
  console.log(`│ 访问与配置说明:`);
324
372
  console.log(`│ - 手机/远程设备打开对应地址即可直接访问`);
325
373
  console.log(`│ - 在本机打开 Web 界面 ->「设置」->「远程与移动端」进行安全管理:`);
326
374
  console.log(`│ 可开启免密直连(局域网/Tailscale)、查看 6 位临时配对码或设置长期密码`);
375
+ console.log(`│ - 高级接入参数: settings.json 中的 mode / proxyPort / proxyHost / trustedHosts`);
376
+ console.log(`│ 修改后需要重启 DSH`);
327
377
  console.log(`│ - 项目文档: https://github.com/IceApriler/dsh-remote-mobile`);
328
378
  console.log(`└${divider}\n`);
329
379
  ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (运行模式: ${activeMode}, 对外端口: ${activePort})`);
@@ -359,6 +409,7 @@ export * from './auth/token.js';
359
409
  export * from './auth/gate.js';
360
410
  export * from './routes/api.js';
361
411
  export * from './bridge/compat.js';
412
+ export * from './bridge/directory-picker.js';
362
413
  export * from './styles/style-snippets.js';
363
414
  export * from './gateway/detector.js';
364
415
  export * from './gateway/server.js';
@@ -1,16 +1,19 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { type SessionStore } from '../auth/token.js';
3
3
  import { type StyleSnippetStore } from '../styles/style-snippets.js';
4
- export declare function createRoutes(store: SessionStore, styleStore?: StyleSnippetStore): ({
5
- method: string;
4
+ /**
5
+ * DSH 0.2.1+ WebServer 的路由契约:`kind` 决定注册到 exact 还是 prefixes 表。
6
+ * 当前所有接口都是字面路径,因此统一使用 exact;`method` 保留给旧宿主兼容。
7
+ */
8
+ export interface WebRoute {
9
+ kind: 'exact' | 'prefix';
10
+ /** 旧版 DSH 使用;DSH 0.2.1+ 的 WebServer 不按 method 分派。 */
11
+ method?: 'GET' | 'POST';
6
12
  path: string;
7
- handler: (_req: IncomingMessage, res: ServerResponse) => Promise<void>;
8
- } | {
9
- method: string;
10
- path: string;
11
- handler: (req: IncomingMessage, res: ServerResponse) => Promise<ServerResponse<IncomingMessage> | undefined>;
12
- } | {
13
- method?: undefined;
14
- path: string;
15
- handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
16
- })[];
13
+ handler: (req: IncomingMessage, res: ServerResponse) => unknown;
14
+ }
15
+ export interface CreateRoutesOptions {
16
+ /** 测试环境注入的自定义 cordis.patch.yml 路径(避免污染宿主配置) */
17
+ patchFilePath?: string;
18
+ }
19
+ export declare function createRoutes(store: SessionStore, styleStore?: StyleSnippetStore, routeOptions?: CreateRoutesOptions): WebRoute[];