dsh-bridge-gateway 0.1.1 → 0.1.3

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,19 @@
2
2
 
3
3
  本项目 `dsh-bridge-gateway` 是基于 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) 的移植增强分支,在保留原版能力之外,重点新增「公网直连网关」。
4
4
 
5
+ ## [0.1.3] - 待发布
6
+
7
+ ### 修复
8
+ - 修复移动端对话时底部输入框横向超出屏幕右缘的问题:约束输入卡片与编辑器最大宽度不超视口,长内容自动折行。
9
+
10
+ ## [0.1.2] - 待发布
11
+
12
+ ### 新增
13
+ - 公网直连网关新增插件 host 反向代理:`/points-checkin/*` 路径转发到本机 points-checkin bridge,使积分签到插件在公网 HTTPS 网关页面下同步可用。
14
+
15
+ ### 修复
16
+ - 修复移动端模型/权限选择下拉在底部输入栏被挤出视口、仅剩右下角横条的问题:改为固定在输入栏上方的底部浮层。
17
+
5
18
  ## [0.1.1] - 待发布
6
19
 
7
20
  ### 修复
package/client/client.js CHANGED
@@ -3902,6 +3902,27 @@ function injectMobileStyles() {
3902
3902
  background: var(--dsw-alias-bg-layer-2, #f4f4f7) !important;
3903
3903
  }
3904
3904
 
3905
+ /* \u8F93\u5165\u6846\u6A2A\u5411\u4E0D\u6EA2\u51FA\uFF1A\u5361\u7247\u53CA\u5176\u7F29\u5BB9\u5668\u6700\u5927\u5BBD\u5EA6\u4E0D\u8D85\u8FC7\u89C6\u53E3\uFF0C\u5185\u90E8\u7F16\u8F91\u5668\u53EF\u6536\u7F29\u3001\u957F\u5185\u5BB9\u81EA\u52A8\u6298\u884C */
3906
+ div[class*="uV2eYG_scrollBody"] {
3907
+ max-width: 100vw !important;
3908
+ overflow-x: hidden !important;
3909
+ }
3910
+ div[class*="uV2eYG_card"] {
3911
+ width: 100% !important;
3912
+ max-width: 100% !important;
3913
+ min-width: 0 !important;
3914
+ box-sizing: border-box !important;
3915
+ }
3916
+ div[class*="uV2eYG_card"] div,
3917
+ div[class*="uV2eYG_card"] textarea,
3918
+ div[class*="uV2eYG_card"] [contenteditable="true"] {
3919
+ max-width: 100% !important;
3920
+ min-width: 0 !important;
3921
+ overflow-wrap: anywhere !important;
3922
+ word-break: break-word !important;
3923
+ box-sizing: border-box !important;
3924
+ }
3925
+
3905
3926
  /* \u8F93\u5165\u6846\u5E95\u90E8\u5DE5\u5177\u680F\uFF1A\u5F39\u6027\u81EA\u9002\u5E94\uFF0C\u5F7B\u5E95\u675C\u7EDD\u6743\u9650\u9009\u62E9\u5668(Full access)\u4E0E\u6A21\u578B\u9009\u62E9\u5668\u91CD\u53E0\u78B0\u649E */
3906
3927
  div[class*="uV2eYG_row"] {
3907
3928
  display: flex !important;
@@ -3963,7 +3984,7 @@ function injectMobileStyles() {
3963
3984
  min-width: 0 !important;
3964
3985
  }
3965
3986
 
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 */
3987
+ /* \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
3988
  div[class*="_7KE1Ra_root"] > div[class*="dropdown"],
3968
3989
  div[class*="_7KE1Ra_root"] > div[class*="listbox"],
3969
3990
  div[class*="_7KE1Ra_root"] > div[class*="popup"],
@@ -3973,15 +3994,20 @@ function injectMobileStyles() {
3973
3994
  div[class*="Sh0Q9G_root"] > div[class*="listbox"],
3974
3995
  div[class*="Sh0Q9G_root"] > div[class*="popup"],
3975
3996
  div[class*="Sh0Q9G_root"] > div[class*="menu"] {
3997
+ position: fixed !important;
3998
+ left: 8px !important;
3999
+ right: 8px !important;
4000
+ top: auto !important;
4001
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
4002
+ width: auto !important;
3976
4003
  min-width: 0 !important;
3977
4004
  max-width: calc(100vw - 16px) !important;
3978
- width: auto !important;
3979
- max-height: 55vh !important;
4005
+ max-height: 48vh !important;
3980
4006
  overflow-x: auto !important;
3981
4007
  overflow-y: auto !important;
3982
- left: auto !important;
3983
- right: auto !important;
3984
- top: calc(100% + 6px) !important;
4008
+ z-index: 10005 !important;
4009
+ transform: none !important;
4010
+ margin: 0 !important;
3985
4011
  box-sizing: border-box !important;
3986
4012
  -webkit-overflow-scrolling: touch !important;
3987
4013
  }
@@ -4233,7 +4259,7 @@ function injectMobileStyles() {
4233
4259
  pointer-events: auto !important;
4234
4260
  }
4235
4261
 
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 */
4262
+ /* \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
4263
  div[class*="uV2eYG_overlayAnchor"] > div[class*="dropdown"],
4238
4264
  div[class*="uV2eYG_overlayAnchor"] > div[class*="popup"],
4239
4265
  div[class*="uV2eYG_overlayAnchor"] > div[class*="menu"],
@@ -4241,13 +4267,22 @@ function injectMobileStyles() {
4241
4267
  div[class*="uV2eYG_overlayAnchor"] > div[role="listbox"],
4242
4268
  div[class*="overlayLayer"] > div[class*="dropdown"],
4243
4269
  div[class*="overlayLayer"] > div[class*="popup"] {
4244
- max-width: calc(100vw - 16px) !important;
4270
+ position: fixed !important;
4271
+ left: 8px !important;
4272
+ right: 8px !important;
4273
+ top: auto !important;
4274
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
4275
+ width: auto !important;
4245
4276
  min-width: 0 !important;
4246
- max-height: 60vh !important;
4277
+ max-width: calc(100vw - 16px) !important;
4278
+ max-height: 50vh !important;
4247
4279
  overflow-x: auto !important;
4248
4280
  overflow-y: auto !important;
4281
+ z-index: 10005 !important;
4282
+ transform: none !important;
4283
+ margin: 0 !important;
4249
4284
  box-sizing: border-box !important;
4250
- transform-origin: left top !important;
4285
+ transform-origin: left bottom !important;
4251
4286
  }
4252
4287
  }
4253
4288
 
package/client/index.js CHANGED
@@ -3500,6 +3500,27 @@ function injectMobileStyles() {
3500
3500
  background: var(--dsw-alias-bg-layer-2, #f4f4f7) !important;
3501
3501
  }
3502
3502
 
3503
+ /* 输入框横向不溢出:卡片及其缩容器最大宽度不超过视口,内部编辑器可收缩、长内容自动折行 */
3504
+ div[class*="uV2eYG_scrollBody"] {
3505
+ max-width: 100vw !important;
3506
+ overflow-x: hidden !important;
3507
+ }
3508
+ div[class*="uV2eYG_card"] {
3509
+ width: 100% !important;
3510
+ max-width: 100% !important;
3511
+ min-width: 0 !important;
3512
+ box-sizing: border-box !important;
3513
+ }
3514
+ div[class*="uV2eYG_card"] div,
3515
+ div[class*="uV2eYG_card"] textarea,
3516
+ div[class*="uV2eYG_card"] [contenteditable="true"] {
3517
+ max-width: 100% !important;
3518
+ min-width: 0 !important;
3519
+ overflow-wrap: anywhere !important;
3520
+ word-break: break-word !important;
3521
+ box-sizing: border-box !important;
3522
+ }
3523
+
3503
3524
  /* 输入框底部工具栏:弹性自适应,彻底杜绝权限选择器(Full access)与模型选择器重叠碰撞 */
3504
3525
  div[class*="uV2eYG_row"] {
3505
3526
  display: flex !important;
@@ -3561,7 +3582,7 @@ function injectMobileStyles() {
3561
3582
  min-width: 0 !important;
3562
3583
  }
3563
3584
 
3564
- /* 移动端:模型/权限选择弹层不超出屏幕(从 180px 触发器中弹出时强制约束视口、横向可滚动兜底) */
3585
+ /* 移动端:模型/权限选择弹层固定为输入栏上方的底部浮层,保证完整可见(修复真机下拉只剩右下角横条、内容被挤出视口) */
3565
3586
  div[class*="_7KE1Ra_root"] > div[class*="dropdown"],
3566
3587
  div[class*="_7KE1Ra_root"] > div[class*="listbox"],
3567
3588
  div[class*="_7KE1Ra_root"] > div[class*="popup"],
@@ -3571,15 +3592,20 @@ function injectMobileStyles() {
3571
3592
  div[class*="Sh0Q9G_root"] > div[class*="listbox"],
3572
3593
  div[class*="Sh0Q9G_root"] > div[class*="popup"],
3573
3594
  div[class*="Sh0Q9G_root"] > div[class*="menu"] {
3595
+ position: fixed !important;
3596
+ left: 8px !important;
3597
+ right: 8px !important;
3598
+ top: auto !important;
3599
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
3600
+ width: auto !important;
3574
3601
  min-width: 0 !important;
3575
3602
  max-width: calc(100vw - 16px) !important;
3576
- width: auto !important;
3577
- max-height: 55vh !important;
3603
+ max-height: 48vh !important;
3578
3604
  overflow-x: auto !important;
3579
3605
  overflow-y: auto !important;
3580
- left: auto !important;
3581
- right: auto !important;
3582
- top: calc(100% + 6px) !important;
3606
+ z-index: 10005 !important;
3607
+ transform: none !important;
3608
+ margin: 0 !important;
3583
3609
  box-sizing: border-box !important;
3584
3610
  -webkit-overflow-scrolling: touch !important;
3585
3611
  }
@@ -3831,7 +3857,7 @@ function injectMobileStyles() {
3831
3857
  pointer-events: auto !important;
3832
3858
  }
3833
3859
 
3834
- /* 兜底:Portal 渲染到 overlayAnchor 里的模型/权限选择弹层也强制约束在屏幕内,杜绝整体跑出视口 */
3860
+ /* 兜底:Portal 渲染到 overlayAnchor 里的模型/权限选择弹层也固定为底部浮层,杜绝整体跑出视口只剩横条 */
3835
3861
  div[class*="uV2eYG_overlayAnchor"] > div[class*="dropdown"],
3836
3862
  div[class*="uV2eYG_overlayAnchor"] > div[class*="popup"],
3837
3863
  div[class*="uV2eYG_overlayAnchor"] > div[class*="menu"],
@@ -3839,13 +3865,22 @@ function injectMobileStyles() {
3839
3865
  div[class*="uV2eYG_overlayAnchor"] > div[role="listbox"],
3840
3866
  div[class*="overlayLayer"] > div[class*="dropdown"],
3841
3867
  div[class*="overlayLayer"] > div[class*="popup"] {
3842
- max-width: calc(100vw - 16px) !important;
3868
+ position: fixed !important;
3869
+ left: 8px !important;
3870
+ right: 8px !important;
3871
+ top: auto !important;
3872
+ bottom: calc(64px + var(--dsh-mobile-safe-bottom)) !important;
3873
+ width: auto !important;
3843
3874
  min-width: 0 !important;
3844
- max-height: 60vh !important;
3875
+ max-width: calc(100vw - 16px) !important;
3876
+ max-height: 50vh !important;
3845
3877
  overflow-x: auto !important;
3846
3878
  overflow-y: auto !important;
3879
+ z-index: 10005 !important;
3880
+ transform: none !important;
3881
+ margin: 0 !important;
3847
3882
  box-sizing: border-box !important;
3848
- transform-origin: left top !important;
3883
+ transform-origin: left bottom !important;
3849
3884
  }
3850
3885
  }
3851
3886
 
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.3",
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",