dsh-remote-agent 0.8.1 → 0.9.0
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 +370 -0
- package/README.en.md +95 -4
- package/README.md +147 -40
- package/cordis.patch.yml +48 -1
- package/docs/workspace-share.md +141 -0
- package/lib/client.js +392 -13
- package/lib/env-check.js +244 -0
- package/lib/hub/index.js +89 -1
- package/lib/hub/node-registry.js +96 -2
- package/lib/hub/registry-ingest.js +15 -0
- package/lib/hub/tools.js +26 -1
- package/lib/hub/ui-routes.js +157 -1
- package/lib/index.js +56 -7
- package/lib/node/fs-service.js +20 -2
- package/lib/node/index.js +86 -1
- package/lib/node/runner.js +23 -1
- package/lib/node/share-service.js +213 -0
- package/lib/node/transport.js +54 -1
- package/lib/protocol.js +18 -0
- package/lib/relay.js +18 -1
- package/lib/setup-routes.js +51 -5
- package/package.json +1 -1
- package/scripts/check-doc-drift.mjs +169 -3
package/lib/client.js
CHANGED
|
@@ -310,6 +310,198 @@ window.__ModuleLoader__.load({
|
|
|
310
310
|
* 我是副机 → 填主机地址(或粘配对码)+ 工作区,写 `role: worker`
|
|
311
311
|
* 同机自测 → 写 `role: both`(单机验证用)
|
|
312
312
|
*/
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 工作区共享面板(2026-10-17)。
|
|
316
|
+
*
|
|
317
|
+
* ═══════════════════════════════════════════════════════════════════════
|
|
318
|
+
* 这是什么
|
|
319
|
+
* ═══════════════════════════════════════════════════════════════════════
|
|
320
|
+
*
|
|
321
|
+
* 把副机的一个工作区目录通过 SMB 共享出去,主机 `net use X: \\副机\dsh-work`
|
|
322
|
+
* 映射成盘符 —— 这样可以用本地 IDE 直接打开副机的项目。
|
|
323
|
+
*
|
|
324
|
+
* ═══════════════════════════════════════════════════════════════════════
|
|
325
|
+
* ⚠ 为什么这个面板的 UI 必须"啰嗦"
|
|
326
|
+
* ═══════════════════════════════════════════════════════════════════════
|
|
327
|
+
*
|
|
328
|
+
* 这是本插件**唯一主动放宽安全边界**的功能。开启后:
|
|
329
|
+
* · roots / denyPaths 全部失效(Windows 内核直读,插件拦不住)
|
|
330
|
+
* · 「清单先于字节」失效(.env / .git 密钥 / 中间产物全部对主机可见)
|
|
331
|
+
* · 新增凭证面(主机需要副机 Windows 账号)
|
|
332
|
+
*
|
|
333
|
+
* 所以按用户要求做**二次确认**:
|
|
334
|
+
* 1. 点「共享工作区」→ 先展示风险说明 + 只读/读写选择
|
|
335
|
+
* 2. 必须勾选「我知道风险」才能点「生成共享命令」
|
|
336
|
+
* 3. 生成后给**可复制的命令**(需要管理员,本插件不代执行)
|
|
337
|
+
*
|
|
338
|
+
* 写权限(read/write)默认 read,选 write 要再确认一次 —— 最小权限原则。
|
|
339
|
+
*/
|
|
340
|
+
function SharePanel(props) {
|
|
341
|
+
const nodes = (props.nodes || []).filter(function (n) { return n.online })
|
|
342
|
+
const nodeS = React.useState(nodes.length > 0 ? nodes[0].nodeId : '')
|
|
343
|
+
const nodeId = nodeS[0]
|
|
344
|
+
const setNodeId = nodeS[1]
|
|
345
|
+
const accessS = React.useState('read')
|
|
346
|
+
const access = accessS[0]
|
|
347
|
+
const setAccess = accessS[1]
|
|
348
|
+
const ackS = React.useState(false)
|
|
349
|
+
const ack = ackS[0]
|
|
350
|
+
const setAck = ackS[1]
|
|
351
|
+
const busyS = React.useState(false)
|
|
352
|
+
const busy = busyS[0]
|
|
353
|
+
const setBusy = busyS[1]
|
|
354
|
+
const noteS = React.useState('')
|
|
355
|
+
const note = noteS[0]
|
|
356
|
+
const setNote = noteS[1]
|
|
357
|
+
const planS = React.useState(null)
|
|
358
|
+
const plan = planS[0]
|
|
359
|
+
const setPlan = planS[1]
|
|
360
|
+
const probeS = React.useState(null)
|
|
361
|
+
const probe = probeS[0]
|
|
362
|
+
const setProbe = probeS[1]
|
|
363
|
+
|
|
364
|
+
function doProbe() {
|
|
365
|
+
if (nodeId === '') return
|
|
366
|
+
apiGet('/share?node=' + encodeURIComponent(nodeId))
|
|
367
|
+
.then(function (r) { setProbe(r || null) })
|
|
368
|
+
.catch(function (e) { setNote('探测失败:' + ((e && e.message) || e)) })
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function doPlan() {
|
|
372
|
+
if (nodeId === '' || !ack) return
|
|
373
|
+
setBusy(true)
|
|
374
|
+
setNote('')
|
|
375
|
+
apiPost('/share', {
|
|
376
|
+
action: 'plan',
|
|
377
|
+
node: nodeId,
|
|
378
|
+
dir: '',
|
|
379
|
+
access: access,
|
|
380
|
+
confirm: 'yes',
|
|
381
|
+
})
|
|
382
|
+
.then(function (r) {
|
|
383
|
+
if (r && r.ok) { setPlan(r); setNote('') } else { setNote((r && r.message) || '生成失败') }
|
|
384
|
+
})
|
|
385
|
+
.catch(function (e) { setNote('生成失败:' + ((e && e.message) || e)) })
|
|
386
|
+
.then(function () { setBusy(false) })
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const warnBox = h('div', { className: 'dra-err', style: { marginBottom: '8px' } },
|
|
390
|
+
h('div', { style: { fontWeight: '600', marginBottom: '4px' } },
|
|
391
|
+
'⚠ 开启共享会绕过本插件的安全边界'),
|
|
392
|
+
h('div', null, '共享出去的文件由 Windows 内核直接读写,本插件**无法**再拦:'),
|
|
393
|
+
h('div', { className: 'dra-dim', style: { marginLeft: '12px' } },
|
|
394
|
+
'· roots / denyPaths 全部失效'),
|
|
395
|
+
h('div', { className: 'dra-dim', style: { marginLeft: '12px' } },
|
|
396
|
+
'· 中间产物、日志、.env、.git 里的密钥全部对主机可见'),
|
|
397
|
+
h('div', { className: 'dra-dim', style: { marginLeft: '12px' } },
|
|
398
|
+
'· 主机需要副机的 Windows 账号(新增凭证面)'),
|
|
399
|
+
h('div', { style: { marginTop: '4px' } },
|
|
400
|
+
'只共享工作区目录,别共享整个盘。'),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if (nodes.length === 0) {
|
|
404
|
+
return h('div', null,
|
|
405
|
+
h('div', { className: 'dra-sec' }, '工作区共享(SMB)'),
|
|
406
|
+
h('div', { className: 'dra-empty' }, '没有在线副机 —— 共享需要在副机上开启。'),
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return h('div', null,
|
|
411
|
+
h('div', { className: 'dra-sec' }, '工作区共享(SMB)'),
|
|
412
|
+
h('div', { className: 'dra-dim', style: { marginBottom: '8px' } },
|
|
413
|
+
'把副机的工作区映射成本地盘(net use X: \\\\副机\\...),可以用本地 IDE 直接打开。'),
|
|
414
|
+
warnBox,
|
|
415
|
+
|
|
416
|
+
h('div', { className: 'dra-row' },
|
|
417
|
+
h('span', { className: 'dra-k' }, '副机'),
|
|
418
|
+
h('select', {
|
|
419
|
+
className: 'dra-in',
|
|
420
|
+
value: nodeId,
|
|
421
|
+
onChange: function (e) { setNodeId(e.target.value); setPlan(null); setProbe(null) },
|
|
422
|
+
}, nodes.map(function (n) {
|
|
423
|
+
return h('option', { key: n.nodeId, value: n.nodeId }, n.nodeId)
|
|
424
|
+
})),
|
|
425
|
+
h('button', { className: 'dra-btn', disabled: busy, onClick: doProbe }, '探测当前状态'),
|
|
426
|
+
),
|
|
427
|
+
|
|
428
|
+
probe
|
|
429
|
+
? h('div', { className: 'dra-dim', style: { marginLeft: '20px' } },
|
|
430
|
+
probe.ok === false
|
|
431
|
+
? ('探测失败:' + (probe.message || ''))
|
|
432
|
+
: ('管理员:' + (probe.admin ? '是' : '否(生成命令后需用管理员 PowerShell 执行)')
|
|
433
|
+
+ ' | 已有 dsh-work 共享:' + ((probe.shares || []).length || 0) + ' 个'))
|
|
434
|
+
: null,
|
|
435
|
+
|
|
436
|
+
h('div', { className: 'dra-row' },
|
|
437
|
+
h('span', { className: 'dra-k' }, '权限'),
|
|
438
|
+
h('select', {
|
|
439
|
+
className: 'dra-in',
|
|
440
|
+
value: access,
|
|
441
|
+
onChange: function (e) { setAccess(e.target.value); setPlan(null) },
|
|
442
|
+
},
|
|
443
|
+
h('option', { value: 'read' }, '只读(主机能看、不能改)—— 推荐'),
|
|
444
|
+
h('option', { value: 'write' }, '读写(主机能看能改)'),
|
|
445
|
+
),
|
|
446
|
+
),
|
|
447
|
+
access === 'write'
|
|
448
|
+
? h('div', { className: 'dra-dim', style: { marginLeft: '20px', color: 'var(--dsw-alias-state-error-primary, currentColor)' } },
|
|
449
|
+
'⚠ 读写模式下主机可以修改/删除副机的文件。')
|
|
450
|
+
: null,
|
|
451
|
+
|
|
452
|
+
h('div', { className: 'dra-row', style: { marginTop: '8px' } },
|
|
453
|
+
h('input', {
|
|
454
|
+
type: 'checkbox',
|
|
455
|
+
checked: ack,
|
|
456
|
+
onChange: function (e) { setAck(e.target.checked) },
|
|
457
|
+
}),
|
|
458
|
+
h('span', { className: 'dra-dim' }, '我知道上面的风险,确认开启共享'),
|
|
459
|
+
),
|
|
460
|
+
h('div', { className: 'dra-actions' },
|
|
461
|
+
h('button', {
|
|
462
|
+
className: 'dra-btn',
|
|
463
|
+
disabled: busy || !ack || nodeId === '',
|
|
464
|
+
onClick: doPlan,
|
|
465
|
+
}, busy ? '生成中…' : '生成共享命令'),
|
|
466
|
+
note ? h('span', { className: 'dra-dim' }, note) : null,
|
|
467
|
+
),
|
|
468
|
+
|
|
469
|
+
plan && plan.ok
|
|
470
|
+
? h('div', { style: { marginTop: '8px' } },
|
|
471
|
+
h('div', { className: 'dra-sec' }, '① 在副机上执行(需要管理员 PowerShell)'),
|
|
472
|
+
h('pre', {
|
|
473
|
+
className: 'dra-mono',
|
|
474
|
+
style: {
|
|
475
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
476
|
+
padding: '8px',
|
|
477
|
+
borderRadius: '6px',
|
|
478
|
+
whiteSpace: 'pre-wrap',
|
|
479
|
+
wordBreak: 'break-all',
|
|
480
|
+
userSelect: 'all',
|
|
481
|
+
},
|
|
482
|
+
}, plan.shareCommand),
|
|
483
|
+
h('div', { className: 'dra-dim' }, '共享名:' + plan.shareName
|
|
484
|
+
+ ' | 目录:' + plan.dir + ' | 权限:' + (plan.access === 'write' ? '读写' : '只读')),
|
|
485
|
+
|
|
486
|
+
h('div', { className: 'dra-sec' }, '② 在主机上映射(普通 PowerShell 即可)'),
|
|
487
|
+
h('pre', {
|
|
488
|
+
className: 'dra-mono',
|
|
489
|
+
style: {
|
|
490
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
491
|
+
padding: '8px',
|
|
492
|
+
borderRadius: '6px',
|
|
493
|
+
whiteSpace: 'pre-wrap',
|
|
494
|
+
wordBreak: 'break-all',
|
|
495
|
+
userSelect: 'all',
|
|
496
|
+
},
|
|
497
|
+
}, plan.mountCommand),
|
|
498
|
+
h('div', { className: 'dra-dim' },
|
|
499
|
+
'执行后主机上会出现 X: 盘,就是副机的 ' + plan.dir + '。'),
|
|
500
|
+
)
|
|
501
|
+
: null,
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
313
505
|
function SetupView(props) {
|
|
314
506
|
const cur = (props.setup && props.setup.current) || {}
|
|
315
507
|
const patchFile = (props.setup && props.setup.patchFile) || ''
|
|
@@ -327,13 +519,19 @@ window.__ModuleLoader__.load({
|
|
|
327
519
|
hubUrl: cur.hubUrl || '',
|
|
328
520
|
relayUrl: cur.relayUrl || '',
|
|
329
521
|
nodeId: cur.nodeId || '',
|
|
330
|
-
roots: (cur.roots && cur.roots.length ? cur.roots.join(', ') :
|
|
522
|
+
roots: (cur.roots && cur.roots.length ? cur.roots.join(', ') : defaultRootsHint()),
|
|
331
523
|
transportMode: cur.transportMode === 'relay' ? 'relay' : 'direct',
|
|
332
524
|
pair: '',
|
|
333
525
|
})
|
|
334
526
|
const f = f1[0]
|
|
335
527
|
const setF = f1[1]
|
|
336
528
|
|
|
529
|
+
// 高级选项折叠(2026-10-17,开箱即用):节点 ID 默认取主机名,
|
|
530
|
+
// 90% 的用户不需要改它。折叠起来少一个"必须理解"的字段。
|
|
531
|
+
const advS = React.useState(false)
|
|
532
|
+
const showAdv = advS[0]
|
|
533
|
+
const setShowAdv = advS[1]
|
|
534
|
+
|
|
337
535
|
const b1 = React.useState(false)
|
|
338
536
|
const busy = b1[0]
|
|
339
537
|
const setBusy = b1[1]
|
|
@@ -357,6 +555,34 @@ window.__ModuleLoader__.load({
|
|
|
357
555
|
setF(function (prev) { const n = Object.assign({}, prev); n[k] = v; return n })
|
|
358
556
|
}
|
|
359
557
|
|
|
558
|
+
// ── 工作区默认值(2026-10-17,开箱即用)──────────────────────────
|
|
559
|
+
//
|
|
560
|
+
// 此前工作区是**空**的,必须用户自己填一个绝对路径。小白不知道填什么:
|
|
561
|
+
// 填相对路径会被拒(roots 必须是绝对路径)、填 `C:\` 太宽(安全边界)、
|
|
562
|
+
// 填一个不存在的目录任务会全报 workspace-denied。
|
|
563
|
+
//
|
|
564
|
+
// 现在给一个**安全且大概率可用**的默认值:
|
|
565
|
+
// · Windows → `D:\work`(有 D 盘用 D,没有用 C:\Users\<name>\work)
|
|
566
|
+
// · 其他 → `~/work`
|
|
567
|
+
//
|
|
568
|
+
// 这只是**表单预填**,真正的目录创建由服务端在写入配置时做(见
|
|
569
|
+
// setup-routes 的 ensureRoots)—— 面板不该直接碰文件系统。
|
|
570
|
+
//
|
|
571
|
+
// 注意:不能写 `C:\` 或 `~` 这种"整盘/整家目录"的默认值 ——
|
|
572
|
+
// roots 是安全边界,默认值必须是个**具体子目录**。
|
|
573
|
+
function defaultRootsHint() {
|
|
574
|
+
// 面板跑在浏览器里,拿不到服务端的 os.homedir()。
|
|
575
|
+
// 用 userAgent 判平台足够(这是预填提示,不是安全判定)。
|
|
576
|
+
try {
|
|
577
|
+
const ua = (navigator && navigator.userAgent) || ''
|
|
578
|
+
if (/Windows/i.test(ua)) return 'D:\\work'
|
|
579
|
+
if (/Mac/i.test(ua)) return '~/work'
|
|
580
|
+
return '~/work'
|
|
581
|
+
} catch {
|
|
582
|
+
return ''
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
360
586
|
// 粘贴配对码时自动解出 hubUrl / relayUrl / nodeId
|
|
361
587
|
// 码的格式见 lib/pairing.js(dshra1:// + base64url(JSON))。
|
|
362
588
|
function applyPair(code) {
|
|
@@ -573,18 +799,36 @@ window.__ModuleLoader__.load({
|
|
|
573
799
|
}),
|
|
574
800
|
),
|
|
575
801
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
802
|
+
// ── ② 本机标识:折叠为高级选项(2026-10-17,开箱即用)──────────
|
|
803
|
+
//
|
|
804
|
+
// 节点 ID 默认取主机名(lib/node/index.js 的 sanitizeNodeId),
|
|
805
|
+
// 90% 的用户不需要改。此前它是**平铺的必看字段**,小白会卡在
|
|
806
|
+
// "节点 ID 是什么?我要填什么?"。
|
|
807
|
+
//
|
|
808
|
+
// 折叠后:不展开就完全不用理解它;要改的人点一下就能改。
|
|
809
|
+
h('div', { className: 'dra-sec' },
|
|
810
|
+
h('span', null, '② 本机标识'),
|
|
811
|
+
h('span', {
|
|
812
|
+
className: 'dra-dim',
|
|
813
|
+
style: { marginLeft: '8px', cursor: 'pointer', textDecoration: 'underline' },
|
|
814
|
+
onClick: function () { setShowAdv(!showAdv) },
|
|
815
|
+
}, showAdv ? '收起' : '默认用主机名(点此自定义)'),
|
|
585
816
|
),
|
|
817
|
+
showAdv
|
|
818
|
+
? h('div', null,
|
|
819
|
+
h('div', { className: 'dra-row' },
|
|
820
|
+
h('span', { className: 'dra-k' }, '节点 ID'),
|
|
821
|
+
h('input', {
|
|
822
|
+
className: 'dra-input',
|
|
823
|
+
placeholder: '留空 = 自动用本机主机名',
|
|
824
|
+
value: f.nodeId,
|
|
825
|
+
onChange: function (e) { set('nodeId', e.target.value) },
|
|
826
|
+
}),
|
|
827
|
+
),
|
|
828
|
+
)
|
|
829
|
+
: null,
|
|
586
830
|
|
|
587
|
-
h('div', { className: 'dra-sec' }, '③
|
|
831
|
+
h('div', { className: 'dra-sec' }, '③ 允许任务在哪些目录干活(已预填,一般不用改)'),
|
|
588
832
|
h('div', { className: 'dra-row' },
|
|
589
833
|
h('span', { className: 'dra-k' }, '工作区'),
|
|
590
834
|
h('input', {
|
|
@@ -594,7 +838,9 @@ window.__ModuleLoader__.load({
|
|
|
594
838
|
onChange: function (e) { set('roots', e.target.value) },
|
|
595
839
|
}),
|
|
596
840
|
),
|
|
597
|
-
h('div', { className: 'dra-dim' },
|
|
841
|
+
h('div', { className: 'dra-dim' },
|
|
842
|
+
'任务只能在这些目录内动作 —— 这是安全边界,别填整个盘。'
|
|
843
|
+
+ '目录不存在时写入配置会自动创建。'),
|
|
598
844
|
|
|
599
845
|
// ⚠ 提示必须放在**按钮上方**。
|
|
600
846
|
//
|
|
@@ -680,6 +926,42 @@ window.__ModuleLoader__.load({
|
|
|
680
926
|
const showCfg = showCfgS[0]
|
|
681
927
|
const setShowCfg = showCfgS[1]
|
|
682
928
|
|
|
929
|
+
// ── 环境体检(2026-10-17)───────────────────────────────────────────
|
|
930
|
+
//
|
|
931
|
+
// 与命令行 `dsh-remote-setup --doctor` 共用 lib/env-check.js 的同一份逻辑。
|
|
932
|
+
//
|
|
933
|
+
// 为什么面板上必须有:这个插件最贵的失败全是**静默**的 —— 绑 loopback
|
|
934
|
+
// 导致副机永远连不上、inject 缺服务导致 hub 半死、roots 不存在导致任务
|
|
935
|
+
// 全被拒。这些在主机侧都"看起来正常",用户只能猜。
|
|
936
|
+
//
|
|
937
|
+
// 三档处置(产品决策):能自动修的标「可自动修」;需要装东西的只给
|
|
938
|
+
// 命令/链接(**不代下载**);需要人判断的只报清楚。
|
|
939
|
+
//
|
|
940
|
+
// ⚠ 顺序是硬要求:state 与 runEnvCheck 都必须定义在**按钮之前** ——
|
|
941
|
+
// `const` 有 TDZ,反过来会在渲染时 ReferenceError(第一版就是这样,
|
|
942
|
+
// 靠人工核对行号才发现)。所以 state → 函数 → 按钮 → 渲染区,不能乱。
|
|
943
|
+
//
|
|
944
|
+
// 只在点「体检」时拉一次,不进 4 秒轮询:体检含系统探测,没必要反复跑。
|
|
945
|
+
const envS = React.useState(null)
|
|
946
|
+
const envCheck = envS[0]
|
|
947
|
+
const setEnvCheck = envS[1]
|
|
948
|
+
const envBusyS = React.useState(false)
|
|
949
|
+
const envBusy = envBusyS[0]
|
|
950
|
+
const setEnvBusy = envBusyS[1]
|
|
951
|
+
|
|
952
|
+
const runEnvCheck = function () {
|
|
953
|
+
if (envBusy) return
|
|
954
|
+
setEnvBusy(true)
|
|
955
|
+
apiGet('/env-check')
|
|
956
|
+
.then(function (r) { setEnvCheck(r || null) })
|
|
957
|
+
.catch(function (err) { setNote('体检失败:' + (err.message || err)) })
|
|
958
|
+
.then(function () { setEnvBusy(false) })
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// 首次自动体检的闸门。用 ref(不是 state)—— 它只控制"要不要跑一次",
|
|
962
|
+
// 改了不需要重渲染。
|
|
963
|
+
const autoChecked = React.useRef(false)
|
|
964
|
+
|
|
683
965
|
// 副机接入卡片(生成配对码)
|
|
684
966
|
const pairS = React.useState(null)
|
|
685
967
|
const pair = pairS[0]
|
|
@@ -970,6 +1252,19 @@ window.__ModuleLoader__.load({
|
|
|
970
1252
|
tasks: tk.tasks || [],
|
|
971
1253
|
})
|
|
972
1254
|
})
|
|
1255
|
+
// ── 首次自动体检(2026-10-17,开箱即用)──────────────────
|
|
1256
|
+
//
|
|
1257
|
+
// 此前体检要用户主动点「体检」按钮 —— 但小白不知道要点。
|
|
1258
|
+
// 而这个插件最贵的失败全是静默的(绑 loopback 副机连不上、
|
|
1259
|
+
// inject 缺服务 hub 半死、roots 不存在任务全被拒),不主动
|
|
1260
|
+
// 检查就等于"永远不知道哪里错了"。
|
|
1261
|
+
//
|
|
1262
|
+
// 只在**第一次**成功加载时自动跑一次(autoChecked 是 ref,
|
|
1263
|
+
// 不触发重渲染)。之后 4 秒轮询不会重复体检(体检含系统探测)。
|
|
1264
|
+
if (!autoChecked.current) {
|
|
1265
|
+
autoChecked.current = true
|
|
1266
|
+
runEnvCheck()
|
|
1267
|
+
}
|
|
973
1268
|
})
|
|
974
1269
|
.catch(function (err) {
|
|
975
1270
|
if (disposed) return
|
|
@@ -1161,6 +1456,16 @@ window.__ModuleLoader__.load({
|
|
|
1161
1456
|
},
|
|
1162
1457
|
}, showPair ? '收起接入' : '副机接入'),
|
|
1163
1458
|
h('button', { className: 'dra-btn', onClick: refresh }, '刷新'),
|
|
1459
|
+
// ── 环境体检 ─────────────────────────────────────────────────
|
|
1460
|
+
//
|
|
1461
|
+
// 这个插件的失败大多是**静默**的(绑 loopback 副机连不上、inject 缺
|
|
1462
|
+
// 服务 hub 半死、roots 不存在任务全被拒),主机侧都"看起来正常"。
|
|
1463
|
+
// 体检把这些问题逐条列出来,每条带修法 —— 能自动修的标出来。
|
|
1464
|
+
h('button', {
|
|
1465
|
+
className: 'dra-btn',
|
|
1466
|
+
disabled: envBusy,
|
|
1467
|
+
onClick: runEnvCheck,
|
|
1468
|
+
}, envBusy ? '体检中…' : '体检'),
|
|
1164
1469
|
// ── 清理任务视图 ─────────────────────────────────────────────
|
|
1165
1470
|
//
|
|
1166
1471
|
// 为什么面板上需要这个按钮:hub 侧任务视图会累积(副机的任务记录
|
|
@@ -1294,10 +1599,71 @@ window.__ModuleLoader__.load({
|
|
|
1294
1599
|
),
|
|
1295
1600
|
)
|
|
1296
1601
|
|
|
1602
|
+
const envSection = envCheck === null
|
|
1603
|
+
? null
|
|
1604
|
+
: h('div', null,
|
|
1605
|
+
h('div', { className: 'dra-sec' }, '环境体检'),
|
|
1606
|
+
h('div', { className: 'dra-dim', style: { marginBottom: '6px' } },
|
|
1607
|
+
(envCheck.failed === 0
|
|
1608
|
+
? '核心项全部通过'
|
|
1609
|
+
: '共 ' + envCheck.failed + ' 项需要处理')
|
|
1610
|
+
+ (envCheck.autoFixable > 0 ? '(其中 ' + envCheck.autoFixable + ' 项可自动修)' : '')),
|
|
1611
|
+
(envCheck.items || []).map(function (it) {
|
|
1612
|
+
const mark = it.ok ? '✓' : (it.severity === 'warn' ? '⚠' : '✗')
|
|
1613
|
+
return h('div', { key: it.id, className: 'dra-env-item' },
|
|
1614
|
+
h('div', { className: 'dra-row', style: { alignItems: 'baseline' } },
|
|
1615
|
+
h('span', {
|
|
1616
|
+
className: 'dra-mono',
|
|
1617
|
+
style: { color: it.ok ? 'var(--dsw-alias-state-success-primary, currentColor)' : 'var(--dsw-alias-state-error-primary, currentColor)' },
|
|
1618
|
+
}, mark),
|
|
1619
|
+
h('span', { style: { flex: '1' } }, it.label),
|
|
1620
|
+
it.autoFix ? h('span', { className: 'dra-badge' }, '可自动修') : null,
|
|
1621
|
+
),
|
|
1622
|
+
it.detail
|
|
1623
|
+
? h('div', { className: 'dra-dim', style: { paddingLeft: '20px' } }, it.detail)
|
|
1624
|
+
: null,
|
|
1625
|
+
!it.ok && it.fix
|
|
1626
|
+
? h('div', { className: 'dra-dim', style: { paddingLeft: '20px', marginTop: '2px' } },
|
|
1627
|
+
'↳ 怎么修:' + it.fix)
|
|
1628
|
+
: null,
|
|
1629
|
+
)
|
|
1630
|
+
}),
|
|
1631
|
+
)
|
|
1632
|
+
|
|
1297
1633
|
const errBox = state.error
|
|
1298
1634
|
? h('div', { className: 'dra-err' }, '读取失败:' + state.error)
|
|
1299
1635
|
: null
|
|
1300
1636
|
|
|
1637
|
+
// ── 可达性横幅(2026-10-17)─────────────────────────────────────────
|
|
1638
|
+
//
|
|
1639
|
+
// 这是本插件**最贵的一个静默失败**:DSH 的 webserver 默认绑 127.0.0.1
|
|
1640
|
+
// (它面向"本机浏览器访问自己"),而本插件的用法是跨机 —— 副机拿
|
|
1641
|
+
// `ws://<主机IP>:19387/...` 拨入。绑 loopback 时:
|
|
1642
|
+
//
|
|
1643
|
+
// · hub 自己一切正常(healthz 200、面板能开、工具都在)
|
|
1644
|
+
// · 副机**永远连不上**(ECONNREFUSED / connect timeout)
|
|
1645
|
+
// · 主机侧没有任何异常 → 用户只能猜"地址错了?token 错了?防火墙?"
|
|
1646
|
+
//
|
|
1647
|
+
// 实测三种用户会踩:全新安装(绝大多数)、装过改 lanBind 的插件又卸载
|
|
1648
|
+
// (托管块残留)、自己改错。
|
|
1649
|
+
//
|
|
1650
|
+
// 所以这里渲染成**红色横幅 + 具体修法**。日志用户不会看,面板会看。
|
|
1651
|
+
const reach = caps && caps.reachability ? caps.reachability : null
|
|
1652
|
+
const reachBox = reach && reach.loopback === true
|
|
1653
|
+
? h('div', { className: 'dra-err', style: { marginBottom: '10px' } },
|
|
1654
|
+
h('div', { style: { fontWeight: '600', marginBottom: '4px' } },
|
|
1655
|
+
'⚠ 副机连不上:本机 Web 服务只监听 ' + (reach.bound || '127.0.0.1')),
|
|
1656
|
+
h('div', null, '现在监听在 ', h('code', null, reach.origin || ''),
|
|
1657
|
+
',只有本机能访问。副机用任何地址都连不上。'),
|
|
1658
|
+
h('div', { style: { marginTop: '6px' } }, '修法:在 profile 的 ',
|
|
1659
|
+
h('code', null, 'cordis.patch.yml'), ' 里给 ', h('code', null, 'webserver'),
|
|
1660
|
+
' 行加 ', h('code', null, "host: '0.0.0.0'"), '(保留 port),然后重启 DSH。'),
|
|
1661
|
+
reach.hint
|
|
1662
|
+
? h('div', { className: 'dra-dim', style: { marginTop: '4px' } }, reach.hint)
|
|
1663
|
+
: null,
|
|
1664
|
+
)
|
|
1665
|
+
: null
|
|
1666
|
+
|
|
1301
1667
|
const cards = h('div', { className: 'dra-cards' },
|
|
1302
1668
|
h('div', { className: 'dra-card' },
|
|
1303
1669
|
h('div', { className: 'dra-card-n' }, String(counts.online || 0)),
|
|
@@ -1723,7 +2089,20 @@ window.__ModuleLoader__.load({
|
|
|
1723
2089
|
return h('div', { className: 'dra-root' },
|
|
1724
2090
|
h(StyleTag),
|
|
1725
2091
|
head,
|
|
1726
|
-
h('div', { className: 'dra-body' },
|
|
2092
|
+
h('div', { className: 'dra-body' },
|
|
2093
|
+
errBox,
|
|
2094
|
+
reachBox,
|
|
2095
|
+
cfgSection,
|
|
2096
|
+
envSection,
|
|
2097
|
+
pairSection,
|
|
2098
|
+
cards,
|
|
2099
|
+
nodeSection,
|
|
2100
|
+
// 工作区共享(2026-10-17):**放在派发表单之前** —— 它是"配好之后
|
|
2101
|
+
// 想用本地 IDE 打开副机文件"的进阶需求,不该抢首屏位置,但也不该
|
|
2102
|
+
// 埋在任务列表下面。二次确认在组件内部。
|
|
2103
|
+
h(SharePanel, { nodes: state.nodes }),
|
|
2104
|
+
dispatchForm,
|
|
2105
|
+
taskSection),
|
|
1727
2106
|
)
|
|
1728
2107
|
}
|
|
1729
2108
|
|