dsh-bridge-gateway 0.1.1 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  本项目 `dsh-bridge-gateway` 是基于 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) 的移植增强分支,在保留原版能力之外,重点新增「公网直连网关」。
4
4
 
5
+ ## [0.1.2] - 待发布
6
+
7
+ ### 新增
8
+ - 公网直连网关新增插件 host 反向代理:`/points-checkin/*` 路径转发到本机 points-checkin bridge,使积分签到插件在公网 HTTPS 网关页面下同步可用。
9
+
10
+ ### 修复
11
+ - 修复移动端模型/权限选择下拉在底部输入栏被挤出视口、仅剩右下角横条的问题:改为固定在输入栏上方的底部浮层。
12
+
5
13
  ## [0.1.1] - 待发布
6
14
 
7
15
  ### 修复
package/client/client.js CHANGED
@@ -3963,7 +3963,7 @@ function injectMobileStyles() {
3963
3963
  min-width: 0 !important;
3964
3964
  }
3965
3965
 
3966
- /* \u79FB\u52A8\u7AEF\uFF1A\u6A21\u578B/\u6743\u9650\u9009\u62E9\u5F39\u5C42\u4E0D\u8D85\u51FA\u5C4F\u5E55\uFF08\u4ECE 180px \u89E6\u53D1\u5668\u4E2D\u5F39\u51FA\u65F6\u5F3A\u5236\u7EA6\u675F\u89C6\u53E3\u3001\u6A2A\u5411\u53EF\u6EDA\u52A8\u515C\u5E95\uFF09 */
3966
+ /* \u79FB\u52A8\u7AEF\uFF1A\u6A21\u578B/\u6743\u9650\u9009\u62E9\u5F39\u5C42\u56FA\u5B9A\u4E3A\u8F93\u5165\u680F\u4E0A\u65B9\u7684\u5E95\u90E8\u6D6E\u5C42\uFF0C\u4FDD\u8BC1\u5B8C\u6574\u53EF\u89C1\uFF08\u4FEE\u590D\u771F\u673A\u4E0B\u62C9\u53EA\u5269\u53F3\u4E0B\u89D2\u6A2A\u6761\u3001\u5185\u5BB9\u88AB\u6324\u51FA\u89C6\u53E3\uFF09 */
3967
3967
  div[class*="_7KE1Ra_root"] > div[class*="dropdown"],
3968
3968
  div[class*="_7KE1Ra_root"] > div[class*="listbox"],
3969
3969
  div[class*="_7KE1Ra_root"] > div[class*="popup"],
@@ -3973,15 +3973,20 @@ function injectMobileStyles() {
3973
3973
  div[class*="Sh0Q9G_root"] > div[class*="listbox"],
3974
3974
  div[class*="Sh0Q9G_root"] > div[class*="popup"],
3975
3975
  div[class*="Sh0Q9G_root"] > div[class*="menu"] {
3976
+ position: fixed !important;
3977
+ left: 8px !important;
3978
+ right: 8px !important;
3979
+ top: auto !important;
3980
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
3981
+ width: auto !important;
3976
3982
  min-width: 0 !important;
3977
3983
  max-width: calc(100vw - 16px) !important;
3978
- width: auto !important;
3979
- max-height: 55vh !important;
3984
+ max-height: 48vh !important;
3980
3985
  overflow-x: auto !important;
3981
3986
  overflow-y: auto !important;
3982
- left: auto !important;
3983
- right: auto !important;
3984
- top: calc(100% + 6px) !important;
3987
+ z-index: 10005 !important;
3988
+ transform: none !important;
3989
+ margin: 0 !important;
3985
3990
  box-sizing: border-box !important;
3986
3991
  -webkit-overflow-scrolling: touch !important;
3987
3992
  }
@@ -4233,7 +4238,7 @@ function injectMobileStyles() {
4233
4238
  pointer-events: auto !important;
4234
4239
  }
4235
4240
 
4236
- /* \u515C\u5E95\uFF1APortal \u6E32\u67D3\u5230 overlayAnchor \u91CC\u7684\u6A21\u578B/\u6743\u9650\u9009\u62E9\u5F39\u5C42\u4E5F\u5F3A\u5236\u7EA6\u675F\u5728\u5C4F\u5E55\u5185\uFF0C\u675C\u7EDD\u6574\u4F53\u8DD1\u51FA\u89C6\u53E3 */
4241
+ /* \u515C\u5E95\uFF1APortal \u6E32\u67D3\u5230 overlayAnchor \u91CC\u7684\u6A21\u578B/\u6743\u9650\u9009\u62E9\u5F39\u5C42\u4E5F\u56FA\u5B9A\u4E3A\u5E95\u90E8\u6D6E\u5C42\uFF0C\u675C\u7EDD\u6574\u4F53\u8DD1\u51FA\u89C6\u53E3\u53EA\u5269\u6A2A\u6761 */
4237
4242
  div[class*="uV2eYG_overlayAnchor"] > div[class*="dropdown"],
4238
4243
  div[class*="uV2eYG_overlayAnchor"] > div[class*="popup"],
4239
4244
  div[class*="uV2eYG_overlayAnchor"] > div[class*="menu"],
@@ -4241,13 +4246,22 @@ function injectMobileStyles() {
4241
4246
  div[class*="uV2eYG_overlayAnchor"] > div[role="listbox"],
4242
4247
  div[class*="overlayLayer"] > div[class*="dropdown"],
4243
4248
  div[class*="overlayLayer"] > div[class*="popup"] {
4244
- max-width: calc(100vw - 16px) !important;
4249
+ position: fixed !important;
4250
+ left: 8px !important;
4251
+ right: 8px !important;
4252
+ top: auto !important;
4253
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
4254
+ width: auto !important;
4245
4255
  min-width: 0 !important;
4246
- max-height: 60vh !important;
4256
+ max-width: calc(100vw - 16px) !important;
4257
+ max-height: 50vh !important;
4247
4258
  overflow-x: auto !important;
4248
4259
  overflow-y: auto !important;
4260
+ z-index: 10005 !important;
4261
+ transform: none !important;
4262
+ margin: 0 !important;
4249
4263
  box-sizing: border-box !important;
4250
- transform-origin: left top !important;
4264
+ transform-origin: left bottom !important;
4251
4265
  }
4252
4266
  }
4253
4267
 
package/client/index.js CHANGED
@@ -3561,7 +3561,7 @@ function injectMobileStyles() {
3561
3561
  min-width: 0 !important;
3562
3562
  }
3563
3563
 
3564
- /* 移动端:模型/权限选择弹层不超出屏幕(从 180px 触发器中弹出时强制约束视口、横向可滚动兜底) */
3564
+ /* 移动端:模型/权限选择弹层固定为输入栏上方的底部浮层,保证完整可见(修复真机下拉只剩右下角横条、内容被挤出视口) */
3565
3565
  div[class*="_7KE1Ra_root"] > div[class*="dropdown"],
3566
3566
  div[class*="_7KE1Ra_root"] > div[class*="listbox"],
3567
3567
  div[class*="_7KE1Ra_root"] > div[class*="popup"],
@@ -3571,15 +3571,20 @@ function injectMobileStyles() {
3571
3571
  div[class*="Sh0Q9G_root"] > div[class*="listbox"],
3572
3572
  div[class*="Sh0Q9G_root"] > div[class*="popup"],
3573
3573
  div[class*="Sh0Q9G_root"] > div[class*="menu"] {
3574
+ position: fixed !important;
3575
+ left: 8px !important;
3576
+ right: 8px !important;
3577
+ top: auto !important;
3578
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
3579
+ width: auto !important;
3574
3580
  min-width: 0 !important;
3575
3581
  max-width: calc(100vw - 16px) !important;
3576
- width: auto !important;
3577
- max-height: 55vh !important;
3582
+ max-height: 48vh !important;
3578
3583
  overflow-x: auto !important;
3579
3584
  overflow-y: auto !important;
3580
- left: auto !important;
3581
- right: auto !important;
3582
- top: calc(100% + 6px) !important;
3585
+ z-index: 10005 !important;
3586
+ transform: none !important;
3587
+ margin: 0 !important;
3583
3588
  box-sizing: border-box !important;
3584
3589
  -webkit-overflow-scrolling: touch !important;
3585
3590
  }
@@ -3831,7 +3836,7 @@ function injectMobileStyles() {
3831
3836
  pointer-events: auto !important;
3832
3837
  }
3833
3838
 
3834
- /* 兜底:Portal 渲染到 overlayAnchor 里的模型/权限选择弹层也强制约束在屏幕内,杜绝整体跑出视口 */
3839
+ /* 兜底:Portal 渲染到 overlayAnchor 里的模型/权限选择弹层也固定为底部浮层,杜绝整体跑出视口只剩横条 */
3835
3840
  div[class*="uV2eYG_overlayAnchor"] > div[class*="dropdown"],
3836
3841
  div[class*="uV2eYG_overlayAnchor"] > div[class*="popup"],
3837
3842
  div[class*="uV2eYG_overlayAnchor"] > div[class*="menu"],
@@ -3839,13 +3844,22 @@ function injectMobileStyles() {
3839
3844
  div[class*="uV2eYG_overlayAnchor"] > div[role="listbox"],
3840
3845
  div[class*="overlayLayer"] > div[class*="dropdown"],
3841
3846
  div[class*="overlayLayer"] > div[class*="popup"] {
3842
- max-width: calc(100vw - 16px) !important;
3847
+ position: fixed !important;
3848
+ left: 8px !important;
3849
+ right: 8px !important;
3850
+ top: auto !important;
3851
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
3852
+ width: auto !important;
3843
3853
  min-width: 0 !important;
3844
- max-height: 60vh !important;
3854
+ max-width: calc(100vw - 16px) !important;
3855
+ max-height: 50vh !important;
3845
3856
  overflow-x: auto !important;
3846
3857
  overflow-y: auto !important;
3858
+ z-index: 10005 !important;
3859
+ transform: none !important;
3860
+ margin: 0 !important;
3847
3861
  box-sizing: border-box !important;
3848
- transform-origin: left top !important;
3862
+ transform-origin: left bottom !important;
3849
3863
  }
3850
3864
  }
3851
3865
 
package/lib/index.js CHANGED
@@ -223,6 +223,9 @@ function getDshLoopbackCookie(targetPort) {
223
223
  }
224
224
  }
225
225
 
226
+ /** points-checkin 本机 host bridge 的候选端口(与其 client 端探测顺序一致) */
227
+ const POINTS_CHECKIN_PORTS = [27182, 27183, 27184, 27185, 27186, 27187, 27188, 27189, 27190, 27191];
228
+
226
229
  /** 把请求头中的 Host 和 Origin 改写成 loopback,让 DSH 的安全栅栏放行 */
227
230
  function loopbackHeaders(headers, targetPort) {
228
231
  const authority = `127.0.0.1:${targetPort}`;
@@ -260,14 +263,16 @@ class ProxyServer {
260
263
  * @param {object} [opts.logger]
261
264
  * @param {{ key: string|Buffer, cert: string|Buffer }} [opts.tls] 若提供则启用 HTTPS(公网直连网关)
262
265
  * @param {string} [opts.tag] 日志标签(如 '网关')
266
+ * @param {Array<{prefix:string,targetPort:number,stripPrefix?:boolean}>} [opts.extraTargets] 额外的本机插件 host 反代目标(如 points-checkin 的本地 bridge),鉴权通过后按前缀转发
263
267
  */
264
- constructor({ localPort, targetPort, authManager, logger, tls, tag = '' }) {
268
+ constructor({ localPort, targetPort, authManager, logger, tls, tag = '', extraTargets = [] }) {
265
269
  this.localPort = localPort;
266
270
  this.targetPort = targetPort;
267
271
  this.authManager = authManager;
268
272
  this.logger = logger;
269
273
  this.tls = tls ?? null;
270
274
  this.tag = tag ? `[${tag}] ` : '';
275
+ this.extraTargets = Array.isArray(extraTargets) ? extraTargets : [];
271
276
  this.server = null;
272
277
  this.clientSockets = new Set();
273
278
  this.activeConnections = 0;
@@ -417,6 +422,55 @@ class ProxyServer {
417
422
  }
418
423
  }
419
424
 
425
+ // 4.3 认证通过:可选插件 host 反向代理(如 points-checkin 本地 bridge,供远程 HTTPS 网关的页面同源调用)
426
+ if (this.extraTargets && this.extraTargets.length) {
427
+ const extra = this.extraTargets.find(
428
+ (t) => pathname === t.prefix || pathname.startsWith(`${t.prefix}/`)
429
+ );
430
+ if (extra) {
431
+ const qIndex = (req.url || '').indexOf('?');
432
+ const qs = qIndex >= 0 ? (req.url || '').slice(qIndex) : '';
433
+ const upstreamPath = extra.stripPrefix
434
+ ? (pathname.slice(extra.prefix.length) || '/') + qs
435
+ : req.url;
436
+ const headers = loopbackHeaders(req.headers, extra.targetPort);
437
+ const extraReq = httpRequest(
438
+ {
439
+ host: '127.0.0.1',
440
+ port: extra.targetPort,
441
+ method: req.method,
442
+ path: upstreamPath,
443
+ headers,
444
+ agent: false,
445
+ },
446
+ (extraRes) => {
447
+ const out = { ...extraRes.headers };
448
+ delete out['transfer-encoding'];
449
+ delete out['content-length'];
450
+ res.writeHead(extraRes.statusCode ?? 502, out);
451
+ extraRes.pipe(res);
452
+ res.on('close', () => extraRes.destroy());
453
+ extraRes.on('error', () => {
454
+ if (!res.writableEnded) res.destroy();
455
+ });
456
+ }
457
+ );
458
+ extraReq.on('error', (err) => {
459
+ this.logger.error('插件 host 代理请求失败(%s): %s', extra.prefix, err.message);
460
+ if (!res.headersSent) {
461
+ res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
462
+ res.end(
463
+ `dsh-bridge: 无法连接插件 host 服务 (127.0.0.1:${extra.targetPort}) — ${err.message}`
464
+ );
465
+ } else {
466
+ res.destroy();
467
+ }
468
+ });
469
+ req.pipe(extraReq);
470
+ return;
471
+ }
472
+ }
473
+
420
474
  // 5. 认证通过:正常执行反向代理转发
421
475
  const headers = loopbackHeaders(req.headers, this.targetPort);
422
476
  const proxyReq = httpRequest(
@@ -577,14 +631,53 @@ class BridgeService {
577
631
  return this.getStatus();
578
632
  }
579
633
 
634
+ // 探测 points-checkin 本机 host bridge 端口(供网关反代 /points-checkin/*,实现远程 HTTPS 下积分签到可用)
635
+ async _resolvePointsCheckinPort() {
636
+ if (this._pcPortResolved) return this._pcPort ?? null;
637
+ this._pcPortResolved = true;
638
+ this._pcPort = null;
639
+ for (const port of POINTS_CHECKIN_PORTS) {
640
+ const ok = await new Promise((resolve) => {
641
+ const req = httpGet(
642
+ { host: '127.0.0.1', port, path: '/ping', timeout: 500, headers: { 'User-Agent': 'dsh-bridge-gateway' } },
643
+ (res) => {
644
+ const chunks = [];
645
+ res.on('data', (c) => chunks.push(c));
646
+ res.on('end', () => {
647
+ try {
648
+ const body = JSON.parse(Buffer.concat(chunks).toString());
649
+ resolve(body?.ok === true && body.plugin === 'points-checkin');
650
+ } catch {
651
+ resolve(false);
652
+ }
653
+ });
654
+ }
655
+ );
656
+ req.on('error', () => resolve(false));
657
+ req.on('timeout', () => {
658
+ req.destroy();
659
+ resolve(false);
660
+ });
661
+ });
662
+ if (ok) {
663
+ this._pcPort = port;
664
+ this.logger?.info?.('已发现 points-checkin host bridge: 127.0.0.1:%d', port);
665
+ break;
666
+ }
667
+ }
668
+ return this._pcPort ?? null;
669
+ }
670
+
580
671
  async startProxy() {
581
672
  if (this.proxy) return this.proxy;
582
673
 
674
+ const pcPort = await this._resolvePointsCheckinPort();
583
675
  this.proxy = new ProxyServer({
584
676
  localPort: this.proxyPort,
585
677
  targetPort: this.dshPort,
586
678
  authManager: this.authManager,
587
679
  logger: this.logger,
680
+ extraTargets: pcPort ? [{ prefix: '/points-checkin', targetPort: pcPort, stripPrefix: true }] : [],
588
681
  });
589
682
 
590
683
  await this.proxy.start();
@@ -886,6 +979,7 @@ class BridgeService {
886
979
  }
887
980
 
888
981
  const gatewayAuth = this._refreshGatewayAuth();
982
+ const pcPort = await this._resolvePointsCheckinPort();
889
983
  this.gateway = new ProxyServer({
890
984
  localPort: port,
891
985
  targetPort: this.dshPort,
@@ -893,6 +987,7 @@ class BridgeService {
893
987
  logger: this.logger,
894
988
  tls: { key: tls.key, cert: tls.cert },
895
989
  tag: '公网直连',
990
+ extraTargets: pcPort ? [{ prefix: '/points-checkin', targetPort: pcPort, stripPrefix: true }] : [],
896
991
  });
897
992
 
898
993
  await this.gateway.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bridge-gateway",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "以 dsh-bridge 为根基、融合 remote-gateway 能力的 DSH 远程访问网关:局域网 / Cloudflare 隧道 / 自建隧道之外,新增「公网直连网关」(0.0.0.0 监听 + 自带 HTTPS 自签证书 + 强制登录门禁),解决移动端外网直连操控电脑。",
5
5
  "releaseNotes": "【dsh-bridge-gateway】以 dsh-bridge 为根基、融合 remote-gateway 能力的 DSH 远程访问网关\n• ➕ 公网访问区:隧道 / 直连网关 二选一,网关全部配置项可在界面配置\n• ➕ 直连网关:0.0.0.0:<端口> 自带 HTTPS(自签证书) + 强制 public_only 门禁,复用 AuthManager,链路打通到 DSH loopback\n• 📱 修复移动端选择模型时界面溢出屏幕的问题\n• 默认不启用,安装后原有局域网/隧道/IM 行为完全不变",
6
6
  "type": "module",