dsh-deepmoyu 2.3.0 → 2.4.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.
Files changed (90) hide show
  1. package/AGENTS.md +93 -153
  2. package/CHANGELOG.md +36 -0
  3. package/CONTRIBUTING.md +51 -0
  4. package/CONTRIBUTORS.md +3 -7
  5. package/NOTICE.md +3 -3
  6. package/README.md +143 -81
  7. package/SECURITY.md +21 -0
  8. package/assets/screenshots/memory.png +0 -0
  9. package/assets/screenshots/menu.png +0 -0
  10. package/assets/screenshots/workspace.png +0 -0
  11. package/client.js +1404 -441
  12. package/cordis.patch.yml +1 -1
  13. package/docs/ASSETS.md +25 -0
  14. package/docs/DEPLOYMENT.md +102 -0
  15. package/docs/PRIVACY.md +32 -0
  16. package/docs/RELEASE_AUDIT.md +70 -0
  17. package/docs/RELEASING.md +94 -0
  18. package/docs/TEST_MATRIX.md +77 -0
  19. package/docs/assets-permissions.json +20 -0
  20. package/docs/tool-evidence.json +196 -0
  21. package/index.js +1095 -285
  22. package/lib/asr.py +41 -35
  23. package/lib/background-output.mjs +64 -17
  24. package/lib/background-tools.mjs +1 -0
  25. package/lib/companion.mjs +43 -0
  26. package/lib/config-policy.mjs +20 -0
  27. package/lib/favops.py +9 -2
  28. package/lib/ids.mjs +41 -0
  29. package/lib/login-session.mjs +34 -0
  30. package/lib/login.mjs +5 -4
  31. package/lib/mcp.mjs +3 -2
  32. package/lib/memory-center.mjs +74 -0
  33. package/lib/mode.mjs +6 -7
  34. package/lib/model-relay.mjs +5 -4
  35. package/lib/open-directory.mjs +93 -0
  36. package/lib/pace.mjs +92 -12
  37. package/lib/persona.mjs +46 -0
  38. package/lib/product-status.mjs +123 -0
  39. package/lib/prompts.mjs +40 -5
  40. package/lib/pv-production.mjs +280 -0
  41. package/lib/pv-session.mjs +316 -0
  42. package/lib/pv.mjs +398 -306
  43. package/lib/pv.py +84 -22
  44. package/lib/pv_cover.py +108 -26
  45. package/lib/pv_editorial.py +336 -0
  46. package/lib/pv_verify.py +267 -0
  47. package/lib/python-env.mjs +61 -0
  48. package/lib/reply.mjs +10 -3
  49. package/lib/run-settings.mjs +6 -0
  50. package/lib/sticker-automation.mjs +5 -4
  51. package/lib/sticker.mjs +135 -25
  52. package/lib/tool-scope.mjs +65 -0
  53. package/lib/update.mjs +265 -0
  54. package/lib/uvfetch.mjs +165 -153
  55. package/lib/verify.mjs +111 -127
  56. package/marketplace/pyyxzi__dsh-deepmoyu.yml +7 -0
  57. package/package.json +23 -12
  58. package/presets/deepmoyu.patch.yml +1 -1
  59. package/prompts/duty.md +4 -3
  60. package/screenshots.json +5 -0
  61. package/selfcheck.mjs +21 -6
  62. package/vendor/bilibili-mcp/requirements.in +9 -0
  63. package/vendor/bilibili-mcp/requirements.txt +1936 -11
  64. package/vendor/bilibili-mcp/runtime_guard.py +35 -10
  65. package/lib/__pycache__/pv.cpython-310.pyc +0 -0
  66. package/lib/__pycache__/pv_cover.cpython-310.pyc +0 -0
  67. package/tests/automation-routes.test.mjs +0 -183
  68. package/tests/automation.test.mjs +0 -79
  69. package/tests/background-tools.test.mjs +0 -182
  70. package/tests/background-window.test.mjs +0 -75
  71. package/tests/dup-cross-channel.test.mjs +0 -158
  72. package/tests/manual-browsing.test.mjs +0 -671
  73. package/tests/mcp-restart.test.mjs +0 -131
  74. package/tests/memory-chain.test.mjs +0 -413
  75. package/tests/model-relay.test.mjs +0 -116
  76. package/tests/persona.test.mjs +0 -67
  77. package/tests/pet-run-routes.test.mjs +0 -127
  78. package/tests/pet-run-settings.test.mjs +0 -38
  79. package/tests/pet-session.test.mjs +0 -125
  80. package/tests/pv-stage1.test.mjs +0 -203
  81. package/tests/sticker-routes.test.mjs +0 -141
  82. package/tests/sticker-upload-routes.test.mjs +0 -97
  83. package/tests/sticker-upload.test.mjs +0 -122
  84. package/tests/sticker.test.mjs +0 -169
  85. package/tests/test_guard_cross_channel.py +0 -141
  86. package/tests/test_pv_cover.py +0 -95
  87. package/tests/test_pv_stage1.py +0 -250
  88. package/tests/test_reply_pictures.py +0 -255
  89. package/tests/token-ledger.test.mjs +0 -78
  90. package/tests/verify_pv_sample.py +0 -91
package/client.js CHANGED
@@ -14,9 +14,9 @@
14
14
  * slots.inject('settings.section', () => slots.register(
15
15
  * { name: 'settings.section', id: 'dafeiyu', order: 30, label: '刷B站' }, …)) // ✗ 已移除
16
16
  *
17
- * 现在的入口只有一条:**点桌宠 → 弹菜单 → ⚙️ 设置 → 弹窗**(PetSettingsModal)。
18
- * 弹窗九项侧栏:偏好与桌宠 / 运行方式 / 互动与投递 / 表情收藏 / 学习与记忆 /
19
- * token 账本 / 运行日志 / 日报视频 / 账号与环境;仅构建当前页,账本独立并支持截图。
17
+ * 现在的入口只有一条:**点桌宠 → 弹菜单 → 工作台 → 弹窗**(PetSettingsModal)。
18
+ * 弹窗六组导航:运行方式 / 互动与投递 / 记忆与素材 / 视频工作室 / 记录与用量 /
19
+ * 偏好与设置;表情、账本、回顾与环境通过子页访问,仅构建当前页。
20
20
  * ⇒ `inject` 也跟着清空成 `[]`,apply() 只做两件事:挂样式、挂桌宠。
21
21
  * ⚠️ 别再加回 settings.section —— selfcheck.mjs 会直接判红。
22
22
  *
@@ -66,6 +66,7 @@ window.__ModuleLoader__.load({
66
66
  const error = new Error('HTTP ' + res.status + why + (message ? ':' + message : ''))
67
67
  error.status = res.status
68
68
  error.code = detail?.code
69
+ error.sessionId = detail?.sessionId
69
70
  throw error
70
71
  }
71
72
  const jget = async (p) => asJson(await fetch(API + p, { credentials: 'same-origin' }))
@@ -232,7 +233,7 @@ window.__ModuleLoader__.load({
232
233
  border:1.5px solid rgba(255,255,255,.9);background:#9db8c8;box-shadow:0 1px 4px rgba(2,12,22,.35);
233
234
  pointer-events:none;z-index:2;}
234
235
  [data-bd-pet-mood][data-mood=curious]{background:#56c8d2;box-shadow:0 0 8px rgba(86,200,210,.8);animation:bdMoodBreathe 3.2s ease-in-out infinite;}
235
- [data-bd-pet-mood][data-mood=content]{background:#8fd08f;box-shadow:0 0 8px rgba(143,208,143,.7);}
236
+ [data-bd-pet-mood][data-mood=content]{background:#f0ac91;box-shadow:0 0 8px rgba(240,172,145,.5);}
236
237
  [data-bd-pet-mood][data-mood=glum]{background:#a39ad9;box-shadow:0 0 6px rgba(163,154,217,.55);}
237
238
  [data-bd-pet-mood][data-mood=attention]{background:#ffb14d;box-shadow:0 0 10px rgba(255,177,77,.9);animation:bdMoodPulse 1.6s ease-in-out infinite;}
238
239
  @keyframes bdMoodBreathe{0%,100%{transform:scale(1);}50%{transform:scale(1.2);}}
@@ -477,7 +478,77 @@ window.__ModuleLoader__.load({
477
478
  [data-bd-page-content]{display:flex;flex-direction:column;gap:22px;min-width:0;}
478
479
  [data-bd-pghead]{padding:0 1px 2px;align-items:flex-start;gap:8px;}
479
480
  [data-bd-pghead] b{color:#224b5d;font-size:17px;font-weight:600;}
480
- [data-bd-pghead] p{color:#66808d;font-size:11px;line-height:1.7;margin:6px 0 0;}
481
+ [data-bd-pghead] p{color:#526d79;font-size:12px;line-height:1.7;margin:6px 0 0;}
482
+ [data-bd-today]{display:flex;flex-direction:column;gap:18px;}
483
+ [data-bd-today-hero]{display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:154px;padding:22px 24px;border:1px solid #d9e7ee;border-radius:16px;overflow:hidden;background:linear-gradient(135deg,#fffdf9 0%,#f4f9fb 62%,#edf4ff 100%);box-shadow:0 12px 30px rgba(65,111,131,.08);}
484
+ [data-bd-today-hero-copy]{min-width:0;}
485
+ [data-bd-today-hero] h1{margin:8px 0 6px;color:#204b5e;font:600 25px/1.35 'Noto Serif SC','Source Han Serif SC','SimSun',serif;letter-spacing:.02em;}
486
+ [data-bd-today-hero] p{max-width:560px;margin:0;color:#4b6c83;font-size:12px;line-height:1.8;}
487
+ [data-bd-today-hero] [data-bd-eyebrow],[data-bd-today-hero] [data-bd-note]{color:#4b6c83;}
488
+ [data-bd-today-fish]{display:flex;align-items:center;justify-content:center;flex:none;width:92px;height:92px;border-radius:50%;font-size:52px;background:#fff;border:1px solid #d9e7ee;box-shadow:0 8px 20px rgba(65,111,131,.1);transform:rotate(-7deg);}
489
+ [data-bd-today-fish] img{width:100%;height:100%;object-fit:contain;}
490
+ [data-bd-today-grid]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;}
491
+ [data-bd-today-grid]>[data-bd-today-card]{min-height:148px;}
492
+ [data-bd-today-card]{display:flex;flex-direction:column;gap:8px;min-height:148px;padding:13px 14px;border-radius:10px;border:1px solid #d9e7ee;background:#fff;}
493
+ [data-bd-today-current]{display:flex;align-items:center;min-height:38px;padding:9px 10px;border-radius:8px;background:#f6fafc;color:#4f6975;font-size:12px;line-height:1.55;}
494
+ [data-bd-today-next] strong{font-size:18px;color:#214f63;}
495
+ [data-bd-today-trust]{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}
496
+ [data-bd-today-trust] strong{font-size:18px;color:#214f63;}
497
+ [data-bd-today-trust] span{font-size:11px;color:#718792;}
498
+ [data-bd-today-wide]{grid-column:1/-1;}
499
+ [data-bd-timeline]{display:flex;gap:0;position:relative;padding:6px 4px 2px;}
500
+ [data-bd-timeline]:before{content:'';position:absolute;left:10px;right:10px;top:14px;height:1px;background:#d9e7ee;}
501
+ [data-bd-timeline-item]{position:relative;z-index:1;display:flex;flex:1;flex-direction:column;gap:8px;color:#8aa0a9;font-size:11px;line-height:1.5;}
502
+ [data-bd-timeline-item] i{display:block;width:10px;height:10px;border-radius:50%;background:#d9e7ee;border:2px solid #fff;box-sizing:content-box;}
503
+ [data-bd-timeline-item][data-on] i{background:#63a6c1;box-shadow:0 0 0 3px rgba(99,166,193,.13);}
504
+ [data-bd-timeline-item][data-on]{color:#4f6975;}
505
+ [data-bd-run-plan-grid]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;}
506
+ [data-bd-run-plan-grid] div{display:flex;flex-direction:column;gap:4px;padding:9px 10px;border-radius:8px;background:#f6fafc;min-width:0;}
507
+ [data-bd-run-plan-grid] span{font-size:11px;color:#526d79;}
508
+ [data-bd-run-plan-grid] strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#315b6c;font-size:12px;font-weight:600;}
509
+ [data-bd-log-summary] p{margin:0;color:#4f6975;font-size:12px;line-height:1.7;}
510
+ [data-bd-pet-modal] [data-bd-recovery-copy]>span,
511
+ [data-bd-pet-modal] [data-bd-ledger-heading] p,
512
+ [data-bd-pet-modal] [data-bd-ledger-number]>span,
513
+ [data-bd-pet-modal] [data-bd-ledger-hero]>p,
514
+ [data-bd-pet-modal] [data-bd-tok-kpis] small,
515
+ [data-bd-pet-modal] [data-bd-ledger-sectionhead]>span,
516
+ [data-bd-pet-modal] [data-bd-tok-chip]{color:#526d79;}
517
+ [data-bd-companion-settings]{display:flex;flex-direction:column;gap:10px;margin-top:12px;padding-top:14px;border-top:1px solid #d9e7ee;}
518
+ [data-bd-pet-reaction]{position:absolute;right:0;top:0;display:flex;align-items:center;gap:4px;max-width:124px;box-sizing:border-box;padding:5px 9px;border-radius:13px 13px 13px 3px;background:#fff8ef;border:1px solid #eed9bf;color:#986027;font-size:16px;box-shadow:0 4px 14px #54320712;animation:bdReactionIn .3s ease-out;pointer-events:none;}
519
+ [data-bd-pet-reaction] small{font:500 10px/1.4 'Microsoft YaHei',sans-serif;color:#71523a;white-space:nowrap;}
520
+ [data-bd-pet-fish][data-reaction=celebrate] [data-bd-pet-img]{animation:bdFishCelebrate .7s ease-in-out 2;}
521
+ [data-bd-pet-fish][data-reaction=curious] [data-bd-pet-img]{animation:bdFishPeek .8s ease-in-out;}
522
+ [data-bd-pet-fish][data-reaction=learned] [data-bd-pet-reaction]{background:#edf4ff;border-color:#c5d6ed;color:#4d72a3;}
523
+ [data-bd-pet-fish][data-reduced-motion],[data-bd-pet-fish][data-reduced-motion] *{animation:none!important;transition:none!important;}
524
+ @keyframes bdReactionIn{from{opacity:0;translate:0 5px}to{opacity:1;translate:0 0}}
525
+ @keyframes bdFishCelebrate{0%,100%{transform:rotate(0)}35%{transform:rotate(-6deg) translateY(-5px)}70%{transform:rotate(5deg)}}
526
+ @keyframes bdFishPeek{0%,100%{transform:rotate(0)}50%{transform:rotate(-5deg) translateY(-3px)}}
527
+ @media(prefers-reduced-motion:reduce){[data-bd-pet-fish],[data-bd-pet-fish] *{animation:none!important;transition:none!important;}}
528
+ [data-bd-week-recovery]{display:grid;grid-template-columns:1.05fr .95fr;gap:14px;}
529
+ [data-bd-week-card],[data-bd-recovery-card]{display:flex;flex-direction:column;gap:10px;padding:14px;border:1px solid #d9e7ee;border-radius:12px;background:#fff;min-width:0;}
530
+ [data-bd-week-days]{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:5px;align-items:end;min-height:74px;}
531
+ [data-bd-week-day]{display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0;text-align:center;color:#526d79;font-size:10px;}
532
+ [data-bd-week-day] i{display:block;min-height:4px;border-radius:4px 4px 2px 2px;background:#d9e7ee;}
533
+ [data-bd-week-day][data-on] i{background:#6ca8bd;}
534
+ [data-bd-week-day][data-missing] i{background:repeating-linear-gradient(135deg,#eef3f5,#eef3f5 3px,#dce7eb 3px,#dce7eb 5px);}
535
+ [data-bd-week-day] b{font-size:10px;color:#315b6c;overflow:hidden;text-overflow:ellipsis;}
536
+ [data-bd-week-themes]{display:flex;gap:5px;flex-wrap:wrap;}
537
+ [data-bd-week-themes] span{max-width:100%;padding:3px 7px;border-radius:999px;background:#f2f7f9;color:#56727e;font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
538
+ [data-bd-week-change]{display:flex;flex-direction:column;gap:6px;padding-top:8px;border-top:1px solid #e5edef;}
539
+ [data-bd-week-change-row]{display:grid;grid-template-columns:42px minmax(0,1fr);gap:7px;align-items:start;}
540
+ [data-bd-week-change-row]>b{font-size:10px;color:#526d79;}
541
+ [data-bd-week-change-chips]{display:flex;gap:4px;flex-wrap:wrap;}
542
+ [data-bd-week-change-chips] span{padding:2px 6px;border-radius:5px;background:#f4f8fa;color:#55717d;font-size:9px;}
543
+ [data-bd-recovery-list]{display:flex;flex-direction:column;gap:7px;}
544
+ [data-bd-recovery-row]{display:grid;grid-template-columns:8px minmax(0,1fr) auto;gap:8px;align-items:start;padding:8px 9px;border-radius:8px;background:#f6fafc;}
545
+ [data-bd-recovery-row]>i{width:8px;height:8px;margin-top:4px;border-radius:50%;background:#93aab3;}
546
+ [data-bd-recovery-row][data-state="running"]>i,[data-bd-recovery-row][data-state="ready"]>i{background:#4ca079;}
547
+ [data-bd-recovery-row][data-state="failed"]>i,[data-bd-recovery-row][data-state="interrupted"]>i,[data-bd-recovery-row][data-state="blocked"]>i{background:#d36555;}
548
+ [data-bd-recovery-row][data-state="paused"]>i,[data-bd-recovery-row][data-state="held"]>i{background:#d19746;}
549
+ [data-bd-recovery-copy]{display:flex;flex-direction:column;gap:2px;min-width:0;}
550
+ [data-bd-recovery-copy] strong{font-size:11px;color:#315b6c;}
551
+ [data-bd-recovery-copy] span{font-size:10px;line-height:1.45;color:#78909a;overflow-wrap:anywhere;}
481
552
  [data-bd-pet-modal] [data-bd-ico]{display:none;}
482
553
  [data-bd-pet-grid],[data-bd-grid2]{grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;min-width:0;}
483
554
  [data-bd-page-content]>[data-bd-pet-grid],[data-bd-pet-body]>[data-bd-pet-grid]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;align-items:start;}
@@ -515,9 +586,9 @@ window.__ModuleLoader__.load({
515
586
  [data-bd-pet-modal] [data-bd-btn][data-primary] [data-bd-ico]{color:#fff;background:transparent;}
516
587
  [data-bd-pet-modal] [data-bd-msg]{background:#dceffb;color:#24536e;border:1px solid #b9d8eb;}
517
588
  [data-bd-pet-modal] [data-bd-msg][data-warn]{background:#fff2da;border-color:#e8c98f;color:#805311;}
518
- [data-bd-pet-modal] [data-bd-msg][data-good]{background:#e4f5ed;border-color:#b6dbcb;color:#256447;}
589
+ [data-bd-pet-modal] [data-bd-msg][data-good]{background:#edf4ff;border-color:#bfd3ed;color:#355a85;}
519
590
  [data-bd-pet-modal] [data-bd-badge],[data-bd-pet-card] [data-bd-pet-badge]{background:#e1f0f9;color:#365b75;}
520
- [data-bd-pet-modal] [data-bd-badge][data-live],[data-bd-pet-card] [data-bd-pet-badge][data-live]{background:#d9f0e5;color:#256447;}
591
+ [data-bd-pet-modal] [data-bd-badge][data-live],[data-bd-pet-card] [data-bd-pet-badge][data-live]{background:#e5efff;color:#355a85;}
521
592
  [data-bd-pet-modal] [data-bd-badge][data-warn],[data-bd-pet-card] [data-bd-pet-badge][data-warn]{background:#fff0d6;color:#805311;}
522
593
  [data-bd-pet-modal] [data-bd-pre]{color:#365b75;background:#edf6fc;border:1px solid #d2e5f0;}
523
594
  [data-bd-pet-modal] [data-bd-sub]{border-color:#cbdde4;margin-left:3px;padding-left:14px;gap:12px;}
@@ -554,7 +625,7 @@ window.__ModuleLoader__.load({
554
625
  /* ═══════════ 独立 token 账本:海盐蓝 / 暖白 / 编辑式数字排版 ═══════════ */
555
626
  [data-bd-ledger]{gap:20px;}
556
627
  [data-bd-ledger-heading]{display:flex;align-items:center;justify-content:space-between;gap:16px;}
557
- [data-bd-eyebrow]{font-family:'Cascadia Code','Consolas',monospace;font-size:9px;letter-spacing:.16em;color:#66818e;}
628
+ [data-bd-eyebrow]{font-family:'Cascadia Code','Consolas',monospace;font-size:10px;letter-spacing:.16em;color:#526d79;}
558
629
  [data-bd-ledger-heading] h2{font-family:'Noto Serif SC','Source Han Serif SC','SimSun',serif;font-size:27px;line-height:1.35;font-weight:600;letter-spacing:.035em;color:#173f52;margin:7px 0 5px;}
559
630
  [data-bd-ledger-heading] p{font-size:12px;color:#617882;margin:0;}
560
631
  [data-bd-ledger-summary]{padding:25px 28px 18px;border:1px solid #cadde3;border-top:3px solid #216b87;border-radius:3px 3px 12px 12px;background:#fffefb;
@@ -630,6 +701,13 @@ window.__ModuleLoader__.load({
630
701
  [data-bd-pet-tabs]{padding:0 9px;}
631
702
  [data-bd-pet-tab]{padding:10px 9px;}
632
703
  [data-bd-pet-body]{padding:20px 14px 24px;}
704
+ [data-bd-today-hero]{padding:18px;min-height:132px;}
705
+ [data-bd-today-hero] h1{font-size:21px;}
706
+ [data-bd-today-fish]{width:68px;height:68px;font-size:38px;}
707
+ [data-bd-today-grid]{grid-template-columns:minmax(0,1fr);}
708
+ [data-bd-today-wide]{grid-column:auto;}
709
+ [data-bd-run-plan-grid]{grid-template-columns:repeat(2,minmax(0,1fr));}
710
+ [data-bd-week-recovery]{grid-template-columns:minmax(0,1fr);}
633
711
  [data-bd-page-content]>[data-bd-pet-grid],[data-bd-pet-body]>[data-bd-pet-grid]{columns:1;}
634
712
  [data-bd-pet-grid],[data-bd-grid2]{grid-template-columns:minmax(0,1fr);}
635
713
  [data-bd-pet-modal] [data-bd-sec]{padding:20px 17px;}
@@ -661,14 +739,14 @@ window.__ModuleLoader__.load({
661
739
  [data-bd-settings-title] p{margin-top:4px;font-size:11px;}
662
740
  [data-bd-settings-layout]{display:grid;grid-template-columns:172px minmax(0,1fr);flex:1;min-height:0;}
663
741
  [data-bd-pet-tabs]{display:flex;flex-direction:column;align-items:stretch;gap:4px;padding:18px 12px;border:0;border-right:1px solid #dbe5e8;background:#f0f5f5;overflow-y:auto;min-height:0;}
664
- [data-bd-nav-group]{font-size:9px;letter-spacing:.12em;color:#82959b;padding:13px 13px 5px;}
742
+ [data-bd-nav-group]{font-size:10px;letter-spacing:.12em;color:#526d79;padding:13px 13px 5px;}
665
743
  [data-bd-nav-group]:first-child{padding-top:0;}
666
744
  [data-bd-pet-tab]{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:3px;white-space:normal;text-align:left;min-height:48px;padding:9px 13px;border-radius:8px;font-size:12px;line-height:1.4;color:#53707e;}
667
- [data-bd-pet-tab] small{font-size:9px;font-weight:400;letter-spacing:.02em;color:#78909a;}
745
+ [data-bd-pet-tab] small{font-size:10px;font-weight:400;letter-spacing:.02em;color:#526d79;}
668
746
  [data-bd-pet-tab]:hover{background:#e5edef;color:#255c73;}
669
747
  [data-bd-pet-tab][data-on]{box-shadow:none;background:#dcebef;color:#20536b;}
670
- [data-bd-pet-tab][data-on] small{color:#537e8f;}
671
- [data-bd-nav-foot]{margin-top:auto;padding:20px 13px 4px;color:#8c9b9f;font:9px/1.8 'Consolas',monospace;letter-spacing:.07em;}
748
+ [data-bd-pet-tab][data-on] small{color:#3d6172;}
749
+ [data-bd-nav-foot]{margin-top:auto;padding:20px 13px 4px;color:#526d79;font:10px/1.8 'Consolas',monospace;letter-spacing:.07em;}
672
750
  [data-bd-settings-main]{display:flex;flex-direction:column;min-width:0;min-height:0;}
673
751
  [data-bd-pet-body]{padding:26px;min-height:0;}
674
752
  [data-bd-pghead] b{font:600 24px/1.4 'Noto Serif SC','Source Han Serif SC','SimSun',serif;letter-spacing:.035em;color:#204b5e;}
@@ -676,7 +754,7 @@ window.__ModuleLoader__.load({
676
754
  [data-bd-pet-modal] [data-bd-sec]{padding:20px;gap:13px;}
677
755
  [data-bd-pet-modal] [data-bd-h] b{font-size:13px;}
678
756
  [data-bd-segments]{display:inline-flex;align-self:flex-start;gap:4px;padding:4px;border:1px solid #d4e1e6;border-radius:10px;background:#edf3f3;max-width:100%;}
679
- [data-bd-segments] button{border:0;border-radius:7px;padding:8px 17px;color:#64808b;background:transparent;font:inherit;font-size:11px;cursor:pointer;}
757
+ [data-bd-segments] button{border:0;border-radius:7px;padding:8px 17px;color:#526d79;background:transparent;font:inherit;font-size:11px;cursor:pointer;}
680
758
  [data-bd-segments] button[aria-pressed="true"]{background:#fffefb;color:#235d76;box-shadow:0 1px 4px #233d4a14;}
681
759
  [data-bd-pet-modal] [data-bd-learn-list]{height:360px;min-height:180px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;padding:2px 7px 2px 0;}
682
760
  [data-bd-pet-modal] [data-bd-learn-list]>[data-bd-sec]{padding:17px;flex:none;background:#f9fbfa;}
@@ -695,6 +773,33 @@ window.__ModuleLoader__.load({
695
773
  [data-bd-memory]>[data-bd-pet-grid]{margin:16px 0;}
696
774
  [data-bd-memory]>[data-bd-pet-grid]>[data-bd-sec]{padding:12px!important;gap:9px!important;}
697
775
  [data-bd-memory]>[data-bd-note]{margin-top:10px;}
776
+ [data-bd-memory-overview]{background:linear-gradient(135deg,#fffdf9 0%,#f5fafb 100%)!important;}
777
+ [data-bd-memory-grid]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px;}
778
+ [data-bd-memory-tile]{display:flex;flex-direction:column;gap:5px;min-width:0;padding:10px;border:1px solid #dce8ec;border-radius:9px;background:rgba(255,255,255,.72);}
779
+ [data-bd-memory-tile] strong{font-size:12px;color:#2a5668;}
780
+ [data-bd-memory-tile] span{font-size:10px;line-height:1.5;color:#718792;min-height:30px;}
781
+ [data-bd-memory-detail]{margin-top:2px;border-top:1px solid #e1ebee;padding-top:8px;}
782
+ [data-bd-memory-detail]>summary{cursor:pointer;color:#4d7384;font-size:12px;font-weight:600;}
783
+ [data-bd-memory-trends]{display:flex;flex-direction:column;gap:5px;margin-top:9px;}
784
+ [data-bd-memory-trend]{display:grid;grid-template-columns:minmax(90px,1fr) auto auto;gap:8px;align-items:center;padding:6px 8px;border-radius:7px;background:#f6fafc;color:#496875;font-size:11px;}
785
+ [data-bd-memory-trend] b{font-size:10px;font-weight:500;color:#6c8792;}
786
+ [data-bd-memory-trend] em{font-style:normal;font-size:9px;color:#b17640;}
787
+ [data-bd-memory-quotes]{display:flex;flex-direction:column;gap:4px;margin-top:10px;}
788
+ [data-bd-memory-quotes] blockquote{margin:0;padding:6px 8px;border-left:2px solid #e1a07e;color:#5b7079;font-size:11px;line-height:1.5;background:#fffaf7;}
789
+ [data-bd-memory-results]{display:flex;flex-direction:column;gap:9px;min-height:120px;max-height:380px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;padding:3px 5px 3px 0;}
790
+ [data-bd-memory-result]{display:flex;flex-direction:column;gap:6px;border:1px solid #dae6ec;border-radius:9px;padding:12px;background:#fbfdfe;}
791
+ [data-bd-memory-result] p{margin:0;color:#355c6d;font-size:12px;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere;}
792
+ [data-bd-memory-result] time{white-space:nowrap;}
793
+ [data-bd-memory-result]:focus-within{border-color:#8fb5c9;}
794
+ @media(max-width:650px){
795
+ [data-bd-memory-grid]{grid-template-columns:repeat(2,minmax(0,1fr));}
796
+ [data-bd-memory-tile] span{min-height:24px;}
797
+ [data-bd-memory-trend]{grid-template-columns:1fr auto;}
798
+ [data-bd-memory-trend] em{grid-column:2;grid-row:1;}
799
+ [data-bd-memory-overview] [data-bd-kpi]{gap:7px;}
800
+ [data-bd-memory-result] time{width:100%;}
801
+ [data-bd-memory-results]{max-height:340px;}
802
+ }
698
803
  [data-bd-pet-card]{background:#fafcf9;border-color:#ccdde4;backdrop-filter:none;-webkit-backdrop-filter:none;}
699
804
  [data-bd-pet-card][data-bd-menu]{width:238px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;}
700
805
  [data-bd-menu] [data-bd-pet-cardhead]{padding:13px 15px;color:#748a95;}
@@ -753,8 +858,16 @@ window.__ModuleLoader__.load({
753
858
  [data-bd-sticker-itemfoot]{display:flex;align-items:center;gap:5px;min-height:29px;padding:0 8px 9px;font-size:9px;color:#7a8f8c;}
754
859
  [data-bd-sticker-itemfoot]>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
755
860
  [data-bd-pet-modal] [data-bd-sticker-itemfoot] [data-bd-btn]{font-size:9px;padding:3px 6px;color:#8a6e63;background:transparent;border-color:#decfc6;}
861
+ [data-bd-sticker-item][data-pending]{border-color:#d9bd83;background:#fffcf3;}
862
+ [data-bd-sticker-status]{color:#8e682b!important;}
863
+ [data-bd-sticker-reviewbar]{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;padding:11px 13px;border:1px solid #e1cea6;border-left:3px solid #c49b53;border-radius:7px;background:#fffcf3;color:#765a30;font-size:11px;line-height:1.7;}
864
+ [data-bd-sticker-reviewbar]>span{flex:1 1 190px;}
865
+ [data-bd-sticker-reviewactions]{display:flex;flex-wrap:wrap;gap:5px;}
866
+ [data-bd-pet-modal] [data-bd-sticker-reviewactions] [data-bd-btn]{font-size:10px;padding:5px 9px;}
867
+ [data-bd-sticker-item]>[data-bd-sticker-reviewactions]{padding:0 8px 10px;}
868
+ [data-bd-pet-modal] [data-bd-sticker-reviewactions] [data-primary]{color:#355a85;background:#e5efff;border-color:#bfd3ed;}
756
869
  [data-bd-sticker-empty]{display:grid;place-items:center;height:100%;text-align:center;font-size:11px;color:#7b9095;line-height:1.8;padding:20px;box-sizing:border-box;}
757
- [data-bd-sticker-detail]{display:grid;grid-template-columns:144px minmax(0,1fr);gap:19px;padding:17px;background:#edf4f2;border:1px solid #d1e0db;border-radius:9px;color:#44685f;}
870
+ [data-bd-sticker-detail]{display:grid;grid-template-columns:144px minmax(0,1fr);gap:19px;padding:17px;background:#f4f7fc;border:1px solid #d4dfed;border-radius:9px;color:#44617d;}
758
871
  [data-bd-sticker-detail] p{font-size:11px;line-height:1.7;overflow-wrap:anywhere;margin:8px 0;}
759
872
  [data-bd-sticker-detail] [data-bd-sticker-thumb]{border-radius:7px;}
760
873
  [data-bd-sticker-detail] [data-bd-grow]{overflow-wrap:anywhere;}
@@ -785,16 +898,122 @@ window.__ModuleLoader__.load({
785
898
  @media(max-width:660px){[data-bd-sticker-list]{height:360px;}[data-bd-sticker-grid]{grid-template-columns:repeat(auto-fill,minmax(116px,1fr));gap:10px;}[data-bd-sticker-detail]{grid-template-columns:90px minmax(0,1fr);gap:12px;padding:13px;}[data-bd-basic-grid]>[data-bd-sec]>textarea{flex:none!important;}}
786
899
  @media(prefers-reduced-motion:reduce){[data-bd-pet-modal]{animation:none;}[data-bd-preview]{animation:none;}[data-bd-pet-mood]{animation:none;}[data-bd-tok-bar] i,[data-bd-btn],[data-bd-sw]{transition:none;}}
787
900
 
901
+ /* ────────────── 视频工作室:明亮编辑台 ────────────── */
902
+ [data-bd-settings-page="video"]{color:#26354a;}
903
+ [data-bd-settings-page="video"] [data-bd-pet-grid]{gap:16px;}
904
+ [data-bd-settings-page="video"] [data-bd-sec]{background:linear-gradient(145deg,#fffefd 0%,#f6f8ff 100%);border:1px solid #d9e2ee;border-radius:16px;box-shadow:0 10px 28px rgba(48,67,108,.08);padding:19px 20px;}
905
+ [data-bd-settings-page="video"] [data-bd-sec]:hover{border-color:#c6d3e4;box-shadow:0 12px 32px rgba(48,67,108,.11);}
906
+ [data-bd-settings-page="video"] [data-bd-h]{color:#6a7890;font-size:11px;letter-spacing:.08em;text-transform:uppercase;}
907
+ [data-bd-settings-page="video"] [data-bd-h] b{color:#1d2c47;font-size:15px;letter-spacing:.01em;}
908
+ [data-bd-settings-page="video"] [data-bd-note]{color:#69778d;line-height:1.65;}
909
+ [data-bd-settings-page="video"] [data-bd-label]{color:#31445f;font-weight:600;}
910
+ [data-bd-settings-page="video"] [data-bd-input]{background:#fff;color:#26354a;border-color:#cfdbe8;box-shadow:0 2px 0 rgba(62,86,130,.03);}
911
+ [data-bd-settings-page="video"] [data-bd-input]::placeholder{color:#8a98a8;}
912
+ [data-bd-settings-page="video"] [data-bd-input]:focus{border-color:#5b63d4;outline:3px solid rgba(91,99,212,.14);}
913
+ [data-bd-settings-page="video"] [data-bd-segments]{padding:4px;background:#f0f3ff;border:1px solid #d9def7;border-radius:12px;gap:4px;}
914
+ [data-bd-settings-page="video"] [data-bd-segments] [data-bd-btn],
915
+ [data-bd-settings-page="video"] [data-bd-segments] button{border-color:transparent;background:transparent;color:#5f6d86;border-radius:9px;}
916
+ [data-bd-settings-page="video"] [data-bd-segments] [aria-pressed="true"]{background:#fff;color:#4f58bf;border-color:#cfd3f6;box-shadow:0 3px 10px rgba(78,86,180,.12);font-weight:700;}
917
+ [data-bd-settings-page="video"] [data-bd-btn][data-primary]{background:#ed6a57;border-color:#ed6a57;color:#fff;box-shadow:0 7px 16px rgba(237,106,87,.22);}
918
+ [data-bd-settings-page="video"] [data-bd-btn][data-primary]:hover:not(:disabled){background:#d95646;border-color:#d95646;box-shadow:0 9px 20px rgba(237,106,87,.28);}
919
+ [data-bd-settings-page="video"] [data-bd-btn]:not([data-primary]){background:#fff;border-color:#cfdbe8;color:#425570;}
920
+ [data-bd-settings-page="video"] [data-bd-btn]:not([data-primary]):hover:not(:disabled){background:#f3f6ff;border-color:#aebce2;color:#4f58bf;}
921
+ [data-bd-settings-page="video"] [data-bd-msg]{background:#eef5ff;border:1px solid #d0e0f4;color:#3d5e83;}
922
+ [data-bd-settings-page="video"] [data-bd-msg][data-warn]{background:#fff0ec;border-color:#f1c6bb;color:#a1483e;}
923
+ [data-bd-settings-page="video"] [data-bd-video-progress]{position:relative;padding:12px 14px 12px 18px;background:linear-gradient(90deg,#fff1e9,#f0f3ff);border-color:#f0c9bd;color:#42516c;font-weight:600;overflow:hidden;}
924
+ [data-bd-settings-page="video"] [data-bd-video-progress]::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:#ed6a57;}
925
+ [data-bd-settings-page="video"] [data-bd-video-stage="starting"],[data-bd-settings-page="video"] [data-bd-video-stage="authoring"],[data-bd-settings-page="video"] [data-bd-video-stage="plan"],[data-bd-settings-page="video"] [data-bd-video-stage="cover"],[data-bd-settings-page="video"] [data-bd-video-stage="render"],[data-bd-settings-page="video"] [data-bd-video-stage="verify"],[data-bd-settings-page="video"] [data-bd-video-stage="publish"]{background:#fff1e9;color:#b85b4d;border:1px solid #f0c9bd;}
926
+ [data-bd-settings-page="video"] [data-bd-video-stage="done"]{background:#eef1ff;color:#4e58b7;border:1px solid #cfd4f4;}
927
+ [data-bd-settings-page="video"] [data-bd-video-stage="failed"],[data-bd-settings-page="video"] [data-bd-video-stage="interrupted"],[data-bd-settings-page="video"] [data-bd-video-stage="stopped"]{background:#fff0ec;color:#a1483e;border:1px solid #f1c6bb;}
928
+ [data-bd-settings-page="video"] [data-bd-empty]{padding:28px 18px;border:1px dashed #cad7e4;border-radius:12px;background:#fafcff;color:#76859a;}
929
+ [data-bd-settings-page="video"] [data-bd-video-intro]{border-top:3px solid #ed6a57;}
930
+ [data-bd-settings-page="video"] [data-bd-video-studio]>[data-bd-sec]:nth-child(2){background:linear-gradient(145deg,#fbfcff 0%,#f8f5ff 100%);}
931
+ [data-bd-settings-page="video"] [data-bd-video-studio]>[data-bd-sec]:nth-child(3){background:linear-gradient(145deg,#fffdf9 0%,#fff8f3 100%);}
932
+ [data-bd-settings-page="video"] [data-bd-video-studio]>[data-bd-sec]:nth-child(4){background:linear-gradient(145deg,#fffefd 0%,#f6f8ff 100%);}
933
+
934
+ /* Shared sky palette and compact navigation; avoid blur in the host workspace. */
935
+ [data-bd-pet-mask]{background:rgba(29,70,101,.17);}
936
+ [data-bd-pet-modal]{background:#f7fbff;border:1px solid #cce3f3;border-radius:20px;box-shadow:0 24px 80px #205c8426;color:#24465f;}
937
+ [data-bd-settings-head]{padding:18px 24px;border-color:#daeaf5;background:#ffffff;}
938
+ [data-bd-settings-title] b{font:650 23px/1.2 'Bahnschrift','Segoe UI Variable Text',sans-serif;letter-spacing:.025em;color:#185981;}
939
+ [data-bd-settings-title] p{margin-top:5px;color:#58768e;font-size:11px;}
940
+ [data-bd-settings-layout]{grid-template-columns:174px minmax(0,1fr);}
941
+ [data-bd-pet-tabs]{background:#eff7fe;border-color:#d6e9f7;padding:18px 12px;gap:8px;}
942
+ [data-bd-nav-group]{color:#466b84;font-size:10px;letter-spacing:.06em;padding:8px 12px 2px;}
943
+ [data-bd-pet-tab]{min-height:53px;padding:11px 12px;border:1px solid transparent;border-radius:11px;color:#41627b;transition:background .14s ease,border-color .14s ease;}
944
+ [data-bd-pet-tab] small{color:#466b84;line-height:1.5;font-size:10px;}
945
+ [data-bd-pet-tab]:hover{background:#e4f2ff;border-color:#d4e9f9;}
946
+ [data-bd-pet-tab][data-on]{background:#fff;border-color:#bbddf5;color:#126a9a;box-shadow:0 3px 12px #3073a010;}
947
+ [data-bd-pet-tab][data-on] small{color:#46718c;}
948
+ [data-bd-nav-foot]{color:#55748d;letter-spacing:.08em;}
949
+ [data-bd-settings-main]{background:#f7fbff;}
950
+ [data-bd-pet-body]{padding:22px 26px 26px;scrollbar-color:#bedbef transparent;scroll-behavior:auto;}
951
+ [data-bd-pghead] b{font:600 22px/1.45 'Microsoft YaHei UI','PingFang SC',sans-serif;letter-spacing:0;color:#24465f;}
952
+ [data-bd-pghead] p{font-size:12px;color:#54738b;line-height:1.7;margin-top:5px;}
953
+ [data-bd-page-content]{gap:18px;}
954
+ [data-bd-workbench-subnav]{margin-bottom:20px;display:flex;gap:6px;width:fit-content;flex-wrap:wrap;}
955
+ [data-bd-segments]{border-color:#d5e7f5;background:#edf6fd;padding:4px;border-radius:10px;}
956
+ [data-bd-segments] button{color:#466b84;padding:8px 13px;min-height:34px;}
957
+ [data-bd-segments] button[aria-pressed="true"]{background:#fff;color:#126a9a;box-shadow:0 1px 5px #1b639516;}
958
+ [data-bd-pet-modal] [data-bd-sec]{background:#fff;border-color:#d7e8f4;border-radius:13px;box-shadow:0 3px 12px #24649204;padding:18px;gap:12px;}
959
+ [data-bd-pet-modal] [data-bd-h] b{color:#2a4d65;}
960
+ [data-bd-pet-modal] [data-bd-note]{color:#537289;line-height:1.7;}
961
+ [data-bd-pet-modal] [data-bd-btn]{border-color:#c5dfef;border-radius:9px;background:#fff;color:#2d678b;box-shadow:none;}
962
+ [data-bd-pet-modal] [data-bd-btn]:hover:not(:disabled){background:#eff7ff;border-color:#9fc9e4;box-shadow:none;}
963
+ [data-bd-pet-modal] [data-bd-btn][data-primary]{background:#137eaf;border-color:#137eaf;color:#fff;box-shadow:0 4px 12px #177baa16;}
964
+ [data-bd-pet-modal] [data-bd-btn][data-primary]:hover:not(:disabled){background:#096c9d;border-color:#096c9d;}
965
+ [data-bd-pet-modal] [data-bd-input]{background:#fbfdff;border-color:#c8e0f0;color:#274a62;border-radius:9px;}
966
+ [data-bd-pet-modal] [data-bd-input]:focus{outline:2px solid #8ec9ee;outline-offset:1px;border-color:#64afd9;}
967
+ [data-bd-pet-modal] [data-bd-sw][data-on]{background:#2389bb;}
968
+ [data-bd-pet-modal] [data-bd-msg][data-good]{background:#edf7ff;border-color:#bbdef5;color:#23678d;}
969
+ [data-bd-pet-modal] [data-bd-badge][data-live]{background:#e7f4ff;color:#1b6a96;}
970
+ [data-bd-run-launches]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;}
971
+ [data-bd-run-launch]{display:flex;flex-direction:column;gap:13px;padding:19px;border:1px solid #c2dff2;border-radius:15px;background:linear-gradient(140deg,#fff 25%,#edf7ff);min-width:0;box-shadow:0 5px 20px #286f9c08;}
972
+ [data-bd-run-launch] h2{margin:0;color:#1a5c84;font-size:18px;font-weight:600;}
973
+ [data-bd-pet-modal] [data-bd-run-launch] [data-bd-ico]{display:block;flex:none;width:21px;height:21px;color:#2387b8;}
974
+ [data-bd-run-launch] p{margin:0;color:#526f85;font-size:12px;line-height:1.7;}
975
+ [data-bd-launch-meta]{display:flex;flex-direction:column;gap:4px;margin-top:auto;color:#466b84;font-size:11px;}
976
+ [data-bd-launch-meta] span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
977
+ [data-bd-run-launch] [data-bd-btn]{width:100%;min-height:39px;margin-top:2px;font-size:13px;}
978
+ [data-bd-run-details]{border:1px solid #d7e8f4;border-radius:12px;background:#fff;padding:14px 16px;}
979
+ [data-bd-run-details]>summary{cursor:pointer;color:#3b6681;font-size:12px;}
980
+ [data-bd-run-details][open]>summary{margin-bottom:14px;}
981
+ [data-bd-run-details]>[data-bd-sec]{margin-bottom:14px;}
982
+ [data-bd-week-recovery]>div:only-child{grid-column:1/-1;}
983
+ [data-bd-pet-modal] [data-bd-settings-page="video"] [data-bd-pet-grid]>[data-bd-sec]{background:#fff;border-color:#d7e8f4;box-shadow:0 3px 12px #24649204;}
984
+ [data-bd-pet-modal] [data-bd-video-stage]:not([data-warn]){background:#e8f5ff;color:#216c94;border-color:#bfdff5;}
985
+ [data-bd-settings-page="video"] [data-bd-video-intro]{border-top-color:#50aada;}
986
+ [data-bd-settings-page="video"] [data-bd-segments]{background:#edf6fd;border-color:#d5e7f5;}
987
+ [data-bd-settings-page="video"] [data-bd-segments] [aria-pressed="true"]{color:#126a9a;border-color:#b6d9ef;}
988
+ [data-bd-settings-page="video"] [data-bd-video-progress]{background:#eef8ff;border-color:#b6d9ef;}
989
+ [data-bd-settings-page="video"] [data-bd-video-progress]::before{background:#50aada;}
990
+ [data-bd-pet-card][data-bd-menu]{box-sizing:border-box;background:#f5fbff;border-color:#b6dcf3;border-radius:17px;box-shadow:0 14px 40px #245c7e24;padding-top:3px;}
991
+ [data-bd-menu] [data-bd-pet-cardhead]{padding:13px 16px 12px;border-color:#d5eaf8;background:#fff;}
992
+ [data-bd-menu] [data-bd-pet-cardhead] b{font-size:14px;color:#225d81;}
993
+ [data-bd-menu] [data-bd-pet-item]{border-radius:10px;color:#285f82;min-height:38px;}
994
+ [data-bd-menu] [data-bd-pet-item]:hover:not(:disabled){background:#e3f2ff;}
995
+ [data-bd-menu] [data-bd-menu-key="session"],[data-bd-menu] [data-bd-menu-key="background"]{width:calc(100% - 24px);margin:9px 12px;padding:11px 12px;background:#e7f4ff;border:1px solid #c3e1f5;}
996
+ [data-bd-menu] [data-bd-menu-key="session"] span>div,[data-bd-menu] [data-bd-menu-key="background"] span>div{font-size:13px;font-weight:600;color:#176b9a;}
997
+ [data-bd-menu] [data-bd-pet-item] i{font-size:10px;line-height:1.5;color:#4b718a;}
998
+ [data-bd-menu-token]{background:#eaf5ff;border-color:#cce5f6;padding:11px 15px;}
999
+ [data-bd-menu-token] span,[data-bd-menu-token] small{color:#466b84;}
1000
+ [data-bd-menu-token] b{font-size:16px;color:#226c98;}
1001
+ [data-bd-preview]{background:#f5fbff;border-color:#b6dcf3;box-shadow:0 12px 32px #245c7e1c;}
1002
+ @media(max-width:760px){[data-bd-settings-layout]{grid-template-columns:minmax(0,1fr);}[data-bd-settings-head]{padding:14px 18px;}[data-bd-pet-tabs]{padding:9px 12px;gap:6px;}[data-bd-pet-tab]{min-height:39px;white-space:nowrap;padding:8px 12px;}[data-bd-pghead] b{font-size:20px;}[data-bd-pet-body]{padding:18px;}[data-bd-run-launches]{gap:12px;}[data-bd-run-launch]{padding:14px;}[data-bd-run-launch] h2{font-size:15px;}}
1003
+ @media(max-width:480px){[data-bd-run-launches]{grid-template-columns:minmax(0,1fr);}[data-bd-run-launch]{gap:9px;}[data-bd-run-launch] p{font-size:11px;}}
1004
+ @media(prefers-reduced-motion:reduce){[data-bd-pet-tab],[data-bd-pet-modal] *{scroll-behavior:auto!important;transition:none!important;}}
788
1005
  `
789
1006
 
790
1007
  /* ────────────── 小组件 ────────────── */
791
1008
 
792
- function Switch({ on, onChange, disabled }) {
1009
+ function Switch({ on, onChange, disabled, label }) {
793
1010
  return h('button', {
794
1011
  'data-bd-sw': '',
795
1012
  'data-on': on ? '' : undefined,
796
1013
  disabled: Boolean(disabled),
797
1014
  title: on ? '已开' : '已关',
1015
+ 'aria-label': label || (on ? '关闭此选项' : '打开此选项'),
1016
+ 'aria-pressed': Boolean(on),
798
1017
  onClick: () => { if (!disabled) onChange(!on) },
799
1018
  }, h('i'))
800
1019
  }
@@ -804,14 +1023,14 @@ window.__ModuleLoader__.load({
804
1023
  h('div', { 'data-bd-grow': '' },
805
1024
  h('div', { 'data-bd-label': '' }, label),
806
1025
  note ? h('div', { 'data-bd-note': '' }, note) : null),
807
- h(Switch, { on, onChange, disabled }))
1026
+ h(Switch, { on, onChange, disabled, label }))
808
1027
  }
809
1028
 
810
1029
  /* ⭐ 2026-10-26:文本输入统一走「本地草稿 + 失焦/回车才提交」。
811
1030
  * GPT-SoVITS 那四个输入框以前 value 直接绑 /pv 轮询值、每敲一键就 POST 一次 ——
812
1031
  * 既违反本文件自己立的"输入框必须绑本地草稿"的规矩(轮询会把正在打的字冲掉),
813
1032
  * 又让一条路径打 30 多个请求。草稿领先服务端时显示草稿;轮询带回同值后自动交回控制权。 */
814
- function DraftInput({ value, onCommit, width, placeholder, type = 'text', inputMode }) {
1033
+ function DraftInput({ value, onCommit, width, placeholder, label, type = 'text', inputMode }) {
815
1034
  const [draft, setDraft] = React.useState(null)
816
1035
  const server = String(value ?? '')
817
1036
  const shown = draft !== null && draft !== server ? draft : server
@@ -821,7 +1040,7 @@ window.__ModuleLoader__.load({
821
1040
  // 不清草稿:等轮询带回新值(draft === server)才交回控制权,避免提交瞬间闪回旧值
822
1041
  }
823
1042
  return h('input', {
824
- 'data-bd-input': '', style: { width }, type, placeholder,
1043
+ 'data-bd-input': '', 'aria-label': label || placeholder || '配置值', style: { width }, type, placeholder,
825
1044
  ...(inputMode ? { inputMode } : {}),
826
1045
  value: shown,
827
1046
  onChange: (e) => setDraft(e.target.value),
@@ -835,6 +1054,11 @@ window.__ModuleLoader__.load({
835
1054
  // ⭐ 2026-10-19(用户报"没有开关这一项,只能让 ai 帮我开"):读不到配置时界面只显示"读设置中…",
836
1055
  // 用户会以为是开关坏了 ✗ ⇒ 数一下连续失败次数,交给界面把话说清楚(版本太旧 / 没重启 / 读不到)
837
1056
  const [fails, setFails] = React.useState(0)
1057
+ const localEpoch = React.useRef(0)
1058
+ const requestSeq = React.useRef(0)
1059
+ const inFlight = React.useRef(false)
1060
+ const externalSet = React.useRef(null)
1061
+ if (!externalSet.current) externalSet.current = (next) => { localEpoch.current += 1; setSt(next) }
838
1062
  /* ⭐ rev 增量(2026-10-27):服务端签名没变时只回一行 {unchanged:true} ——
839
1063
  前端连 setState 都不做,5 秒轮询从"每趟全量 payload + 全树重渲染"变成几乎白嫖。
840
1064
  ⚠️ 本地乐观更新(patch / petShow 切换)改的是 st、rev 没变 ⇒ 不会被 unchanged 那趟冲掉,
@@ -845,22 +1069,27 @@ window.__ModuleLoader__.load({
845
1069
  if (paused) return undefined
846
1070
  let alive = true
847
1071
  const tick = async () => {
1072
+ if (inFlight.current) return
1073
+ inFlight.current = true
1074
+ const seq = ++requestSeq.current
1075
+ const epoch = localEpoch.current
848
1076
  try {
849
1077
  const d = await jget('/status' + (revRef.current ? '?rev=' + encodeURIComponent(revRef.current) : ''))
850
- if (!alive) return
1078
+ if (!alive || seq !== requestSeq.current || epoch !== localEpoch.current) return
851
1079
  if (d && d.ok) {
852
1080
  setFails(0)
853
1081
  if (d.unchanged) return
854
1082
  if (d.rev) revRef.current = String(d.rev)
855
1083
  setSt(d)
856
1084
  } else setFails((n) => n + 1)
857
- } catch { if (alive) setFails((n) => n + 1) }
1085
+ } catch { if (alive && seq === requestSeq.current && epoch === localEpoch.current) setFails((n) => n + 1) }
1086
+ finally { if (seq === requestSeq.current) inFlight.current = false }
858
1087
  }
859
1088
  tick()
860
1089
  const t = setInterval(tick, intervalMs || 5000)
861
- return () => { alive = false; clearInterval(t) }
1090
+ return () => { alive = false; requestSeq.current += 1; inFlight.current = false; clearInterval(t) }
862
1091
  }, [intervalMs, paused])
863
- return [st, setSt, fails]
1092
+ return [st, externalSet.current, fails]
864
1093
  }
865
1094
 
866
1095
  /* 🗑 旧的全页设置 Panel 已退役(2026-10-27):它的配置项早已搬进 PetSettingsModal,
@@ -1038,9 +1267,10 @@ window.__ModuleLoader__.load({
1038
1267
  * 现在使用者指定了本体形象图,就走后端静态路由 `/api/bili-duty/pet/image`(白名单只给 pet)。
1039
1268
  * 图挂了(路由不在/文件丢了)自动退回 CSS 那条鱼 —— 失败点仍然是有的,但兜住了。
1040
1269
  * aura 由父级按 /status 算好传进来:idle 空闲 / run 鲸鱼摸鱼在刷 / bg 后台轮 / err 没登录或读不到状态 */
1041
- function PetFish({ aura, bubbles, title }) {
1270
+ function PetFish({ aura, bubbles, title, reaction, reducedMotion }) {
1042
1271
  const [imgOk, setImgOk] = React.useState(true)
1043
- return h('div', { 'data-bd-pet-fish': '', 'data-aura': aura || 'idle', title: title || '' },
1272
+ return h('div', { 'data-bd-pet-fish': '', 'data-aura': aura || 'idle', 'data-reaction': reaction?.kind,
1273
+ 'data-reduced-motion': reducedMotion ? '' : undefined, title: title || '' },
1044
1274
  imgOk
1045
1275
  ? h('img', {
1046
1276
  'data-bd-pet-img': '', src: PET_IMG_URL, alt: '鲸鱼摸鱼', draggable: false,
@@ -1056,7 +1286,8 @@ window.__ModuleLoader__.load({
1056
1286
  h('i', { key: 'eye', 'data-bd-pet-eye': '' }, h('i', null)),
1057
1287
  h('i', { key: 'mouth', 'data-bd-pet-mouth': '' }),
1058
1288
  ],
1059
- bubbles.map((b) => h('i', {
1289
+ reaction ? h('span', { 'data-bd-pet-reaction': '', 'aria-hidden': true }, reaction.symbol, h('small', {}, reaction.label)) : null,
1290
+ (reducedMotion ? [] : bubbles).map((b) => h('i', {
1060
1291
  'data-bd-pet-bubble': '', key: b.id,
1061
1292
  style: { '--dx': b.dx + 'px', '--s': b.s + 'px', '--t': b.t + 's', '--d': b.d + 's' },
1062
1293
  })))
@@ -1241,22 +1472,22 @@ window.__ModuleLoader__.load({
1241
1472
  : (budget > 0 ? Math.max(0, budget - used) : null)
1242
1473
  const fmtTok = (n) => (Number(n) || 0).toLocaleString()
1243
1474
  const rows = [
1244
- ['在刷与否', fy.active ? '在刷~' : '歇着'],
1245
- ['第几轮', fy.goal && fy.goal.rounds != null ? `${fy.goal.rounds} / ${fy.goal.maxRounds || '?'} 轮` : fy.source === 'manual' ? '手动刷站' : '—'],
1246
- ['今天挑了几条', `${Number(fy.pickedToday ?? (st && st.pickedToday) ?? 0)} / ${Number(st && st.config && st.config.perDay) || 0}`],
1247
- ['今天发了几条说说', `${Number(fy.momentsToday ?? 0)} / ${Number(fy.momentPerDay ?? 0)}`],
1248
- ['这轮烧的 token', !measured ? '未计量' : budget > 0
1475
+ ['会话状态', fy.active ? '运行中' : '空闲'],
1476
+ ['运行轮次', fy.goal && fy.goal.rounds != null ? `${fy.goal.rounds} / ${fy.goal.maxRounds || '?'} 轮` : fy.source === 'manual' ? '手动刷站' : '—'],
1477
+ ['今日精选', `${Number(fy.pickedToday ?? (st && st.pickedToday) ?? 0)} / ${Number(st && st.config && st.config.perDay) || 0}`],
1478
+ ['今日动态', `${Number(fy.momentsToday ?? 0)} / ${Number(fy.momentPerDay ?? 0)}`],
1479
+ ['本次 token', !measured ? '未计量' : budget > 0
1249
1480
  ? `${used.toLocaleString()} / ${budget.toLocaleString()}${left != null ? `(剩 ${left.toLocaleString()})` : ''}`
1250
1481
  : `${used.toLocaleString()} / 不限`],
1251
- ['今天烧的 token', fmtTok(st && st.tokensToday)],
1252
- ['一共烧的 token', fmtTok(st && st.tokensTotal) + (Number(st && st.tokensTotal) > 0 ? '(' + tokTitle(st && st.tokensTotal) + ')' : '')],
1253
- ['后台轮', st ? (st.bgBusy ? '跑着呢' : '闲着') : '—'],
1254
- ['登录', st ? (st.loggedIn ? '已登录' : '没登录') : (fails >= 3 ? '读不到状态' : '读取中…')],
1255
- ['在用模型', String(fy.modelActive || fy.model || (st && st.modelNow) || '—')],
1482
+ ['今日 token', fmtTok(st && st.tokensToday)],
1483
+ ['累计 token', fmtTok(st && st.tokensTotal)],
1484
+ ['后台状态', st ? (st.bgBusy ? '运行中' : '空闲') : '—'],
1485
+ ['登录状态', st ? (st.loggedIn ? '已登录' : '未登录') : (fails >= 3 ? '状态读取失败' : '读取中…')],
1486
+ ['当前模型', String(fy.modelActive || fy.model || (st && st.modelNow) || '—')],
1256
1487
  ]
1257
1488
  return h('div', { 'data-bd-pet-card': '' },
1258
- h('div', { 'data-bd-pet-cardhead': '' }, h('b', {}, '状态卡'),
1259
- h('span', {}, '每 5 秒刷新' + (Number(st && st.activeDays) > 0 ? ' · 连着干活 ' + st.activeDays + ' 天' : ''))),
1489
+ h('div', { 'data-bd-pet-cardhead': '' }, h('b', {}, '运行状态'),
1490
+ h('span', {}, '每 5 秒刷新' + (Number(st && st.activeDays) > 0 ? ' · 连续运行 ' + st.activeDays + ' 天' : ''))),
1260
1491
  h('div', { 'data-bd-pet-status': '' },
1261
1492
  rows.map(([k, v]) => h('div', { 'data-bd-pet-kv': '', key: k }, h('span', {}, k), h('b', {}, v)))))
1262
1493
  }
@@ -1273,6 +1504,17 @@ window.__ModuleLoader__.load({
1273
1504
  return { ...(sourceSessionId ? { sourceSessionId } : {}), ...(owner?.workspaceId ? { workspaceId: owner.workspaceId } : {}) }
1274
1505
  } catch { return {} }
1275
1506
  }
1507
+ // The server exposes only verified studio packages; keep legacy date playback separate.
1508
+ function pvGalleryRows(value) {
1509
+ const productions = Array.isArray(value?.productions) ? value.productions : []
1510
+ const videos = Array.isArray(value?.videos) ? value.videos : []
1511
+ return [
1512
+ ...productions.filter((item) => item?.id && item.file && (item.stage === 'done' || (!item.stage && item.status === 'done')) && item.verification !== false && item.verification?.ok !== false && item.verified !== false)
1513
+ .map((item) => ({ ...item, key: 'studio:' + item.id })),
1514
+ ...videos.filter((item) => item?.file && /^\d{4}-\d{2}-\d{2}$/.test(String(item.date || '')))
1515
+ .map((item) => ({ ...item, key: 'legacy:' + item.date, legacy: true })),
1516
+ ]
1517
+ }
1276
1518
  function PetIcon({ name }) {
1277
1519
  const paths = {
1278
1520
  play: ['M8 5l11 7-11 7z'], moon: ['M20.9 13A9 9 0 0 1 11 3.1 9 9 0 1 0 20.9 13z'],
@@ -1295,12 +1537,109 @@ window.__ModuleLoader__.load({
1295
1537
  h('img', { 'data-bd-settings-avatar': '', src: PET_AVATAR_URL, alt: '', draggable: false, onError: () => setImgOk(false) }))
1296
1538
  }
1297
1539
 
1540
+ function petTabStops(container) {
1541
+ if (!container?.querySelectorAll) return []
1542
+ return Array.from(container.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),a[href],summary,[tabindex="0"]')).filter((el) => {
1543
+ if (el.tabIndex < 0 || !el.getClientRects().length) return false
1544
+ for (let parent = el.parentElement; parent && parent !== container; parent = parent.parentElement) {
1545
+ if (parent.tagName === 'DETAILS' && !parent.open) {
1546
+ const summary = Array.from(parent.children).find((child) => child.tagName === 'SUMMARY')
1547
+ if (!summary?.contains(el)) return false
1548
+ }
1549
+ }
1550
+ return true
1551
+ })
1552
+ }
1553
+
1554
+ function PetCompanionSettings({ st, busy, patch }) {
1555
+ const cfg = st?.config || {}
1556
+ const initial = () => ({ chatterFrequency: cfg.chatterFrequency || 'normal', notificationLevel: cfg.notificationLevel || 'normal',
1557
+ focusMode: cfg.focusMode === true, quietHoursEnabled: cfg.quietHoursEnabled === true,
1558
+ quietHoursStart: cfg.quietHoursStart || '23:00', quietHoursEnd: cfg.quietHoursEnd || '08:00', petReducedMotion: cfg.petReducedMotion === true })
1559
+ const [draft, setDraft] = React.useState(initial)
1560
+ const update = (key, value) => setDraft(old => ({ ...old, [key]: value }))
1561
+ const toggle = (key, title, note) => ToggleRow({ label: title, note, on: draft[key], disabled: Boolean(busy), onChange: value => update(key, value) })
1562
+ return h('div', { 'data-bd-companion-settings': '' },
1563
+ h('div', { 'data-bd-h': '' }, h('b', {}, '陪伴节奏')),
1564
+ h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '主动闲聊频率'),
1565
+ h('select', { 'data-bd-input': '', value: draft.chatterFrequency, disabled: Boolean(busy), onChange: e => update('chatterFrequency', e.target.value) },
1566
+ ...[['rare', '低频 · 约 4–6 小时'], ['normal', '标准 · 约 2–3.5 小时'], ['lively', '高频 · 约 45–90 分钟']].map(([value, label]) => h('option', { key: value, value }, label)))),
1567
+ h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '主动提醒范围'),
1568
+ h('select', { 'data-bd-input': '', value: draft.notificationLevel, disabled: Boolean(busy), onChange: e => update('notificationLevel', e.target.value) },
1569
+ ...[['minimal', '重要结果与待办'], ['normal', '标准 · 结果、学习与精选'], ['all', '全部 · 包含运行状态变化']].map(([value, label]) => h('option', { key: value, value }, label)))),
1570
+ toggle('focusMode', '工作专注模式', '暂停闲聊与一般提醒。任务继续运行,结果和错误记录保留。'),
1571
+ toggle('quietHoursEnabled', '夜间安静时段', '仅限制主动提醒,不影响会话、后台和定时任务。'),
1572
+ draft.quietHoursEnabled ? h('div', { 'data-bd-row': '' },
1573
+ h('label', { 'data-bd-field': '' }, '安静开始', h('input', { 'data-bd-input': '', type: 'time', value: draft.quietHoursStart, disabled: Boolean(busy), onChange: e => update('quietHoursStart', e.target.value) })),
1574
+ h('label', { 'data-bd-field': '' }, '安静结束', h('input', { 'data-bd-input': '', type: 'time', value: draft.quietHoursEnd, disabled: Boolean(busy), onChange: e => update('quietHoursEnd', e.target.value) })),
1575
+ h('span', { 'data-bd-note': '' }, draft.quietHoursStart === draft.quietHoursEnd ? '起止时间相同,安静时段不生效。' : '按本地时间执行,支持跨午夜。')) : null,
1576
+ toggle('petReducedMotion', '减少桌宠动效', '停用浮动、泡泡和事件动画,并遵循系统的减少动态效果设置。'),
1577
+ h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, '当前:' + (st?.companion?.label || '自然陪伴')),
1578
+ h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: () => patch(draft, '陪伴节奏') }, '保存陪伴节奏')))
1579
+ }
1580
+
1298
1581
  /* ⭐ 设置大弹窗(2026-10-26 桌宠化改造):从桌宠 ⚙️ 进来的居中弹窗。
1299
- * 九项侧栏,运行方式与日志各有内部分段;图库、学习与视频按所在页读取;
1582
+ * 六组主导航与组内子页,运行方式与日志各有内部分段;图库、学习与视频按所在页读取;
1300
1583
  * 端点与保存语义与旧设置页一致(/config、/feiyu、/pv、/login/*、/install*、/run)。
1301
1584
  * ⭐ 2026-10-27:旧的全页 Panel 已退役删掉 —— 它独有过的活动流水、出手节奏读数、
1302
1585
  * 判重账本概览、一键诊断都搬进了这张弹窗;弹窗现在是设置的**唯一**入口,
1303
1586
  * 回归断言由 devtools/render-modal.cjs 盯(debugParts 只剩 PetSettingsModal / PetRoot / PetFish)。 */
1587
+ function PetSendReviewPanel({ st, patch, busy, say }) {
1588
+ const [filter, setFilter] = React.useState('pending')
1589
+ const [working, setWorking] = React.useState('')
1590
+ const [error, setError] = React.useState('')
1591
+ const [snapshot, setSnapshot] = React.useState(null)
1592
+ const alive = React.useRef(true)
1593
+ const lock = React.useRef(false)
1594
+ React.useEffect(() => { alive.current = true; return () => { alive.current = false } }, [])
1595
+ React.useEffect(() => { refresh() }, [st?.sendReview?.rev])
1596
+ const data = snapshot || st?.sendReview || {}
1597
+ const rows = Array.isArray(data.items) ? data.items : []
1598
+ const labels = { pending: '待审核', approving: '正在发送', sent: '已发送', rejected: '已拒绝', failed: '发送失败', unknown: '结果待核对' }
1599
+ const refresh = async () => {
1600
+ try { const result = await jget('/send-review'); if (result?.ok === false) throw new Error(result.error || '预览队列读取失败'); if (alive.current) { setSnapshot(result); setError('') } }
1601
+ catch (e) { if (alive.current) setError(String(e?.message || e)) }
1602
+ }
1603
+ const decide = async (row, decision) => {
1604
+ if (lock.current || row.state !== 'pending') return
1605
+ lock.current = true; setWorking(row.id); setError('')
1606
+ try {
1607
+ const result = await jpost('/send-review', { id: row.id, decision, expectedHash: row.hash })
1608
+ if (!alive.current) return
1609
+ if (result.ok === false) throw new Error(result.error || '审核操作未完成')
1610
+ setSnapshot(result)
1611
+ say(decision === 'reject' ? '已拒绝,该内容未发送。' : '已发送,其余内容保持待审核状态。', 'good')
1612
+ } catch (e) {
1613
+ if (alive.current) { await refresh(); setError(String(e?.message || e)) }
1614
+ } finally { lock.current = false; if (alive.current) setWorking('') }
1615
+ }
1616
+ const visible = rows.filter(row => filter === 'all' || row.state === 'pending')
1617
+ return h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-send-review': '' },
1618
+ h('div', { 'data-bd-h': '' }, h(PetIcon, { name: 'key' }), h('b', {}, '发送预览与审核'),
1619
+ Number(data.pending) > 0 ? h('span', { 'data-bd-badge': '' }, data.pending + ' 条待审') : null),
1620
+ ToggleRow({ label: '发送前审核', on: Boolean(st?.config?.sendReview), disabled: Boolean(busy || working),
1621
+ note: '开启后,鲸鱼摸鱼模式的评论、回复、私信、动态和成果投递进入待审队列。每次批准仅发送所选内容,发送权限、判重、节奏和风控限制保持生效。',
1622
+ onChange: value => patch({ sendReview: value }, '发送预览') }),
1623
+ h('div', { 'data-bd-note': '' }, '默认关闭。关闭仅影响后续内容,已有待审内容仍需审核。发送结果不确定时标记为“待核对”,不自动重试。未指定配图的评论按纯文字审核;指定表情时,发送内容绑定预览图片的实际字节。'),
1624
+ h('div', { 'data-bd-row': '' },
1625
+ h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '发送预览筛选' },
1626
+ ...[['pending', '待审核'], ['all', '全部记录']].map(([key, label]) => h('button', { key, type: 'button', 'aria-pressed': filter === key, onClick: () => setFilter(key) }, label))),
1627
+ h('span', { 'data-bd-grow': '' }), h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(working), onClick: refresh }, '刷新预览')),
1628
+ error || data.error ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, error || data.error) : null,
1629
+ h('div', { 'data-bd-send-review-list': '', tabIndex: 0, 'aria-label': '发送预览内容', style: { maxHeight: '380px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '12px' } },
1630
+ visible.length ? visible.map(row => h('article', { key: row.id, 'data-bd-send-review-item': '', style: { padding: '12px', border: '1px solid #dbe5ef', borderRadius: '10px', background: '#f9fbfe' } },
1631
+ h('div', { 'data-bd-row': '' }, h('b', { 'data-bd-grow': '' }, row.kind || '发送内容'), h('span', { 'data-bd-badge': '', 'data-warn': row.state === 'unknown' ? '' : undefined }, labels[row.state] || row.state)),
1632
+ h('div', { 'data-bd-note': '' }, [row.args?.bvid, row.args?.opus_id && '动态 ' + row.args.opus_id, (row.args?.uid || row.targetUid) && '收件 UID ' + (row.args?.uid || row.targetUid), row.args?.rpid && row.args.rpid !== '0' && '回复 ' + row.args.rpid, row.createdAt && petFmtTime(row.createdAt)].filter(Boolean).join(' · ')),
1633
+ h('p', { style: { margin: '8px 0', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', lineHeight: '1.65', fontSize: '12px' } }, String(row.args?.text || '')),
1634
+ Array.isArray(row.attachments) && row.attachments.length ? h('div', { 'data-bd-row': '' }, ...row.attachments.map(item => h('img', { key: item.hash, src: item.imageUrl, alt: '待发送表情:' + item.name, style: { width: '80px', height: '80px', objectFit: 'contain', border: '1px solid #dbe5ef', borderRadius: '8px' } }))) : null,
1635
+ Array.isArray(row.args?.images) && row.args.images.length ? h('div', { 'data-bd-note': '' }, '动态附图:' + row.args.images.join('、')) : null,
1636
+ row.result?.error || row.result?.hint || row.error ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, String(row.error || row.result?.hint || row.result?.error)) : null,
1637
+ row.state === 'pending' ? h('div', { 'data-bd-row': '' },
1638
+ h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(working), onClick: () => decide(row, 'approve'), 'aria-label': '批准并发送:' + row.id }, working === row.id ? '正在处理…' : '批准并发送'),
1639
+ h('button', { 'data-bd-btn': '', disabled: Boolean(working), onClick: () => decide(row, 'reject'), 'aria-label': '拒绝发送:' + row.id }, '拒绝')) : row.state === 'unknown' ? h('div', { 'data-bd-note': '' }, '发送结果不确定。请在 B站核对;系统不自动重试。') : null))
1640
+ : h('div', { 'data-bd-empty': '' }, filter === 'pending' ? '暂无待审核内容。启用发送前审核后,新内容将进入此队列。' : '暂无发送审核记录。')))
1641
+ }
1642
+
1304
1643
  function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say, patch, login, loginState, loginTone, onLoginStart, onInstall, onCheck, onInstallAsr, copyText, sw }) {
1305
1644
  const cfg = (st && st.config) || {}
1306
1645
  const [asrModelDraft, setAsrModelDraft] = React.useState(null)
@@ -1316,6 +1655,18 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1316
1655
  const [learnRefresh, setLearnRefresh] = React.useState(0)
1317
1656
  const [mirrorNote, setMirrorNote] = React.useState('')
1318
1657
  const [soulBusy, setSoulBusy] = React.useState('')
1658
+ const [memoryQuery, setMemoryQuery] = React.useState('')
1659
+ const [memorySource, setMemorySource] = React.useState('all')
1660
+ const [memoryOffset, setMemoryOffset] = React.useState(0)
1661
+ const [memoryData, setMemoryData] = React.useState(null)
1662
+ const [memoryError, setMemoryError] = React.useState('')
1663
+ const [memoryRefresh, setMemoryRefresh] = React.useState(0)
1664
+ const [memoryWorking, setMemoryWorking] = React.useState('')
1665
+ const memoryRequest = React.useRef(0)
1666
+ const memoryActionLock = React.useRef(false)
1667
+ const soulRef = React.useRef(null)
1668
+ const tasteRef = React.useRef(null)
1669
+ const learnQueueRef = React.useRef(null)
1319
1670
  const alive = React.useRef(true)
1320
1671
  const learnInFlight = React.useRef(new Set())
1321
1672
  const learnPulling = React.useRef(false)
@@ -1392,6 +1743,20 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1392
1743
  }
1393
1744
  }, [view, learnState, learnKind, learnFilter, learnOffset, learnRefresh])
1394
1745
 
1746
+ React.useEffect(() => {
1747
+ if (view !== 'learning') return
1748
+ let dead = false
1749
+ const seq = ++memoryRequest.current
1750
+ const timer = setTimeout(async () => {
1751
+ try {
1752
+ const r = await jget('/memory?source=' + encodeURIComponent(memorySource) + '&filter=' + encodeURIComponent(memoryQuery) + '&offset=' + memoryOffset + '&limit=20')
1753
+ if (!r || r.ok === false || !Array.isArray(r.items)) throw new Error(r?.error || '记忆中心接口尚未加载,完整重启 DSH 后重试')
1754
+ if (!dead && alive.current && seq === memoryRequest.current) { setMemoryData(r); setMemoryError('') }
1755
+ } catch (e) { if (!dead && alive.current && seq === memoryRequest.current) setMemoryError(String(e?.message || e)) }
1756
+ }, memoryQuery ? 220 : 0)
1757
+ return () => { dead = true; clearTimeout(timer); memoryRequest.current += 1 }
1758
+ }, [view, memorySource, memoryQuery, memoryOffset, memoryRefresh, st?.rev])
1759
+
1395
1760
  const icon = (name) => h(PetIcon, { name })
1396
1761
  const sectionHead = (_name, title, extra) => h('div', { 'data-bd-h': '' }, h('b', {}, title), extra || null)
1397
1762
  const safeSay = (text, tone) => { if (typeof say === 'function') say(String(text || ''), tone) }
@@ -1411,12 +1776,13 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1411
1776
  }
1412
1777
  const switchControl = (on, disabled, onChange) => h('button', {
1413
1778
  'data-bd-sw': '', 'data-on': on ? '' : undefined, disabled: Boolean(disabled),
1779
+ 'aria-label': view === 'learning' ? '自动审核待审内容' : '启用本地转写',
1414
1780
  type: 'button', 'aria-pressed': Boolean(on), onClick: () => onChange(!on),
1415
1781
  }, h('i', {}))
1416
1782
  const saveConfig = async (values, label) => {
1417
1783
  try {
1418
1784
  const result = await patch(values, label)
1419
- if (result === false) throw new Error(label + '没有保存成功')
1785
+ if (result === false) throw new Error(label + '保存失败')
1420
1786
  return true
1421
1787
  } catch (e) {
1422
1788
  safeSay(String(e && e.message || e), 'warn')
@@ -1428,7 +1794,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1428
1794
  const label = (SOUL_SECTION_LABELS || []).find((x) => x[0] === section)
1429
1795
  const title = label ? label[1] : section
1430
1796
  if (typeof window !== 'undefined' && typeof window.confirm === 'function'
1431
- && !window.confirm(`清空「${title}」这一节?\n\n只清空这一节,其他内容保留。`)) return
1797
+ && !window.confirm(`确认清空「${title}」?\n\n仅清空所选章节,其他内容保留。`)) return
1432
1798
  if (soulBusy) return
1433
1799
  setSoulBusy(section)
1434
1800
  setBusy('soul')
@@ -1461,8 +1827,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1461
1827
  if (!alive.current) return
1462
1828
  if (sameLearnView(view)) setLearnQ(r)
1463
1829
  setLearnError('')
1464
- const text = decision === 'approve' ? '已批准,这条内容会作为后续参考'
1465
- : decision === 'reject' ? '已拒绝,记录仍保留在审计历史中' : '已忘掉,这条学习已撤销'
1830
+ const text = decision === 'approve' ? '已批准,该记录将作为后续参考'
1831
+ : decision === 'reject' ? '已拒绝,审计记录保留' : '已移除该学习记录,审计记录保留'
1466
1832
  safeSay(text, 'good')
1467
1833
  } catch (e) {
1468
1834
  if (alive.current) { setLearnError(String(e && e.message || e)); safeSay(String(e && e.message || e), 'warn') }
@@ -1519,14 +1885,14 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1519
1885
  const onDiag = async () => {
1520
1886
  setBusy('diag')
1521
1887
  try {
1522
- const r = check(await jget('/diag'), '诊断包没取到')
1888
+ const r = check(await jget('/diag'), '诊断信息读取失败')
1523
1889
  const text = String(r.text || '')
1524
1890
  try {
1525
1891
  await navigator.clipboard.writeText(text)
1526
- safeSay(`诊断包已复制到剪贴板(${text.length} 字)—— 直接去粘贴就行;凭证不在里面,可以放心贴`, 'good')
1892
+ safeSay(`诊断信息已复制到剪贴板(${text.length} 字),不包含登录凭证。`, 'good')
1527
1893
  } catch {
1528
1894
  try { console.log('[dsh-deepmoyu] 诊断包\n' + text) } catch { /* 无所谓 */ }
1529
- safeSay(`剪贴板没复制上 —— 诊断包(${text.length} 字)已经打到浏览器控制台(F12)里,从那儿复制`, 'warn')
1895
+ safeSay(`剪贴板写入失败。诊断信息(${text.length} 字)已输出至浏览器控制台(F12)。`, 'warn')
1530
1896
  }
1531
1897
  } catch (e) { safeSay(String(e && e.message || e), 'warn') }
1532
1898
  finally { setBusy('') }
@@ -1534,10 +1900,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1534
1900
 
1535
1901
  const so = st && st.soul
1536
1902
  const filled = so && Array.isArray(so.filled) ? so.filled : []
1537
- const soulCard = h('details', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-memory': '' },
1538
- h('summary', {}, 'soul · 她自己的四节账'),
1903
+ const soulCard = h('details', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-memory': '', ref: soulRef },
1904
+ h('summary', {}, 'soul · 角色记忆'),
1539
1905
  h('div', { 'data-bd-note': '' }, so
1540
- ? (so.empty ? '这本账目前是空的,由鲸鱼摸鱼自己在冲浪过程中记录。这里只提供逐节清除。'
1906
+ ? (so.empty ? '暂无角色记忆。记录由鲸鱼摸鱼模式生成,此处可按章节清除。'
1541
1907
  : `${Number(so.chars) || 0}/${Number(so.budget) || 900} 字${so.updatedAt ? ',上次更新 ' + formatTime(so.updatedAt) : ''}`)
1542
1908
  : '正在读取 soul 状态…'),
1543
1909
  so && so.text ? h('pre', { 'data-bd-pre': '', 'data-bd-soul': '' }, String(so.text)) : null,
@@ -1549,13 +1915,118 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1549
1915
  h('button', {
1550
1916
  'data-bd-btn': '', 'data-sm': '', disabled: Boolean(busy) || soulBusy === key || !filled.includes(key),
1551
1917
  onClick: () => clearSoul(key),
1552
- }, icon('spark'), soulBusy === key ? '清除中…' : '清除这一节')))),
1918
+ }, icon('spark'), soulBusy === key ? '清除中…' : '清除章节')))),
1553
1919
  so && so.file ? h('div', { 'data-bd-note': '' }, '文件:' + String(so.file)) : null
1554
1920
  )
1555
1921
 
1556
1922
  const learnInfo = (st && st.learn) || {}
1923
+ const tasteInfo = (st && st.taste) || {}
1557
1924
  const kinds = Array.isArray(learnInfo.kinds) ? learnInfo.kinds : []
1558
1925
  const labels = learnInfo.labels && typeof learnInfo.labels === 'object' ? learnInfo.labels : {}
1926
+ const tasteRows = Array.isArray(tasteInfo.rows) ? tasteInfo.rows : []
1927
+ const tasteTop = Array.isArray(tasteInfo.top) ? tasteInfo.top : []
1928
+ const tasteLines = Array.isArray(tasteInfo.lines) ? tasteInfo.lines : []
1929
+ const focusLearning = (state) => {
1930
+ setLearnState(state); setLearnFilter(''); setLearnKind(''); setLearnOffset(0); setLearnQ(null)
1931
+ learnQueueRef.current?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' })
1932
+ }
1933
+ const memoryOverviewCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-memory-overview': '' },
1934
+ h('div', { 'data-bd-row': '' },
1935
+ sectionHead('spark', '记忆中心'),
1936
+ h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, '记录概览')),
1937
+ h('div', { 'data-bd-note': '' }, '学习经验经审核后作为参考;soul 保存角色记忆;口味统计基于已发送内容;判重历史用于识别重复操作。各类记录分别管理。'),
1938
+ h('div', { 'data-bd-kpi': '' },
1939
+ h('div', {}, h('b', {}, String(Number(learnInfo.approved) || 0)), h('span', {}, '已批准经验')),
1940
+ h('div', {}, h('b', {}, String(Number(learnInfo.pending) || 0)), h('span', {}, '待审经验')),
1941
+ h('div', {}, h('b', {}, String((so && so.filled || []).length)), h('span', {}, '已记录章节'))),
1942
+ h('div', { 'data-bd-memory-grid': '' },
1943
+ h('div', { 'data-bd-memory-tile': 'learning' },
1944
+ h('strong', {}, '学习'),
1945
+ h('span', {}, `共 ${Number(learnInfo.total) || 0} 条 · 批准 ${Number(learnInfo.approved) || 0} · 待审 ${Number(learnInfo.pending) || 0}`),
1946
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => focusLearning('pending') }, '待审记录')),
1947
+ h('div', { 'data-bd-memory-tile': 'soul' },
1948
+ h('strong', {}, 'soul'),
1949
+ h('span', {}, so ? (so.empty ? '暂无记录' : `${Number(so.chars) || 0}/${Number(so.budget) || 900} 字 · ${String(so.updatedAt || '').trim() ? '已更新' : '暂无更新记录'}`) : '状态读取中'),
1950
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => { if (soulRef.current) { soulRef.current.open = true; soulRef.current.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' }) } } }, '心情记录')),
1951
+ h('div', { 'data-bd-memory-tile': 'taste' },
1952
+ h('strong', {}, '口味'),
1953
+ h('span', {}, `样本 ${Number(tasteInfo.sample) || 0} · 已核对赞数 ${Number(tasteInfo.checked) || 0}`),
1954
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => { if (tasteRef.current) tasteRef.current.open = !tasteRef.current.open } }, '口味统计')),
1955
+ h('div', { 'data-bd-memory-tile': 'seen' },
1956
+ h('strong', {}, '判重'),
1957
+ h('span', {}, `已浏览 ${Number(st?.seen?.videos) || 0} 个视频 · 已回复 ${Number(st?.seen?.replied) || 0} 条评论`),
1958
+ h('span', { 'data-bd-note': '' }, '用于识别重复操作'))),
1959
+ h('details', { 'data-bd-memory-detail': '', 'data-bd-taste-detail': '', ref: tasteRef },
1960
+ h('summary', {}, tasteInfo.enough ? '口味趋势' : '口味趋势(暂不满足分析条件)'),
1961
+ tasteLines.length ? h('div', { 'data-bd-memory-lines': '' }, ...tasteLines.map((line, i) => h('div', { key: i, 'data-bd-note': '' }, line))) : null,
1962
+ tasteRows.length ? h('div', { 'data-bd-memory-trends': '' }, ...tasteRows.slice(0, 5).map((row) => {
1963
+ const marker = row.skew || row.low ? h('em', {}, row.skew ? '单条样本偏斜' : '赞数较少') : null
1964
+ return h('div', { key: row.key, 'data-bd-memory-trend': '' },
1965
+ h('span', {}, row.label || row.key), h('b', {}, `${row.n} 次 · 平均 ${row.avg} 赞`), marker)
1966
+ })) : h('div', { 'data-bd-note': '' }, '暂无满足分析条件的样本。'),
1967
+ tasteTop.length ? h('div', { 'data-bd-memory-quotes': '' }, h('div', { 'data-bd-h': '' }, h('b', {}, '已获赞的自身发言')), ...tasteTop.map((row) => h('blockquote', { key: row.rpid }, `“${String(row.text || '').slice(0, 100)}” · ${Number(row.likes) || 0} 赞`))) : null),
1968
+ h('div', { 'data-bd-row': '' },
1969
+ h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, tasteInfo.updatedAt ? '口味统计更新时间:' + formatTime(tasteInfo.updatedAt) : '口味统计暂无更新记录。'),
1970
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => focusLearning('all') }, '查看完整学习记录')))
1971
+ const changeMemory = async (row, action) => {
1972
+ if (memoryActionLock.current || memoryWorking || busy) return
1973
+ const note = action === 'revoke' ? '确认撤销该记录的批准?状态将改为拒绝,并从下一轮提示词中移除。'
1974
+ : row.source === 'taste' ? '确认从口味统计中移除该样本?已发送的评论、判重记录和历史审计保留。'
1975
+ : '确认从当前记忆中移除该记录?历史版本、审计、B站内容和判重记录保留。'
1976
+ if (typeof window.confirm === 'function' && !window.confirm(note + '\n\n' + String(row.text || '').slice(0, 160))) return
1977
+ const key = row.source + ':' + row.id
1978
+ memoryActionLock.current = true
1979
+ setMemoryWorking(key)
1980
+ try {
1981
+ const result = await jpost('/memory', { source: row.source, id: row.id, version: row.version, action })
1982
+ if (!result || result.ok === false) throw new Error(result?.error || '记忆操作未完成')
1983
+ if (!alive.current) return
1984
+ safeSay(result.auditRetained === false ? (result.warning || '当前记忆已更新,但操作审计归档失败,请查看运行日志。')
1985
+ : action === 'revoke' ? '已撤销批准,该记录不再注入后续提示词,审计保留。' : '已从当前记忆移除,历史审计保留。', result.auditRetained === false ? 'warn' : 'good')
1986
+ setMemoryRefresh((n) => n + 1); setLearnRefresh((n) => n + 1)
1987
+ const fresh = await jget('/status')
1988
+ if (alive.current && fresh?.ok) setSt(fresh)
1989
+ } catch (e) { if (alive.current) {
1990
+ const text = String(e?.message || e)
1991
+ safeSay(text.includes('MEMORY_CHANGED_REFRESH_REQUIRED') ? '记录已发生变化,正在重新读取。请核对更新后的内容。' : text, 'warn')
1992
+ setMemoryError(text.includes('MEMORY_CHANGED_REFRESH_REQUIRED') ? '记录已变化,本次操作未执行。正在重新读取。' : text)
1993
+ setMemoryRefresh((n) => n + 1)
1994
+ } }
1995
+ finally { memoryActionLock.current = false; if (alive.current) setMemoryWorking('') }
1996
+ }
1997
+ const memoryLabels = { all: '全部记忆', learning: '学习经验', soul: '心情记录', voice: '语气札记', taste: '口味样本', seen: '浏览历史', history: '版本与审计' }
1998
+ const memoryItems = Array.isArray(memoryData?.items) ? memoryData.items : []
1999
+ const memoryExplorerCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-memory-explorer': '' },
2000
+ sectionHead('list', '记忆检索'),
2001
+ h('div', { 'data-bd-note': '' }, '支持检索学习、心情、语气、口味、浏览记录和近期版本审计。浏览判重与历史版本为只读;撤销批准或移除记录均保留审计,公开内容保持不变。'),
2002
+ h('div', { 'data-bd-row': '' },
2003
+ h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '记忆搜索'),
2004
+ h('input', { 'data-bd-input': '', 'aria-label': '搜索所有记忆', placeholder: '按正文、BV号、来源或决定理由搜索', value: memoryQuery, maxLength: 160,
2005
+ onChange: (e) => { setMemoryQuery(e.target.value); setMemoryOffset(0); setMemoryData(null) } })),
2006
+ h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '记忆来源'),
2007
+ h('select', { 'data-bd-input': '', value: memorySource, 'aria-label': '记忆来源', onChange: (e) => { setMemorySource(e.target.value); setMemoryOffset(0); setMemoryData(null) } },
2008
+ ...Object.entries(memoryLabels).map(([key, label]) => h('option', { key, value: key }, label))))),
2009
+ memoryError ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, memoryError) : null,
2010
+ h('div', { 'data-bd-memory-results': '', tabIndex: 0, 'aria-label': '统一记忆搜索结果' },
2011
+ !memoryData ? h('div', { 'data-bd-empty': '', role: 'status' }, memoryError ? '记忆读取失败,可重新读取。' : '正在读取记忆记录…')
2012
+ : !memoryItems.length ? h('div', { 'data-bd-empty': '' }, memoryQuery ? '无匹配的记忆记录。' : '该来源暂无记录。')
2013
+ : memoryItems.map((row) => h('article', { key: row.source + ':' + row.id, 'data-bd-memory-result': '' },
2014
+ h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-badge': '' }, memoryLabels[row.source] || row.source),
2015
+ row.label ? h('strong', { 'data-bd-label': '', 'data-bd-grow': '' }, row.label) : null,
2016
+ h('time', { 'data-bd-note': '' }, formatTime(row.at) || '时间未记录')),
2017
+ h('p', {}, row.text),
2018
+ row.from ? h('div', { 'data-bd-note': '' }, '来源:' + row.from) : null,
2019
+ row.why ? h('div', { 'data-bd-note': '' }, '决定理由:' + row.why) : null,
2020
+ row.state ? h('span', { 'data-bd-note': '' }, '当前状态:' + ({ pending: '待审', approved: '已批准', rejected: '已拒绝' }[row.state] || row.state)) : null,
2021
+ Array.isArray(row.actions) && row.actions.length ? h('div', { 'data-bd-row': '' },
2022
+ row.actions.includes('revoke') ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(memoryWorking) || Boolean(busy), onClick: () => changeMemory(row, 'revoke') }, '撤销批准') : null,
2023
+ row.actions.includes('forget') ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(memoryWorking) || Boolean(busy), onClick: () => changeMemory(row, 'forget') }, memoryWorking === row.source + ':' + row.id ? '正在移除…' : '从当前记忆移除') : null)
2024
+ : h('span', { 'data-bd-note': '' }, '只读历史记录')))),
2025
+ h('div', { 'data-bd-row': '' },
2026
+ h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, memoryData ? `匹配 ${Number(memoryData.matched) || 0} 条 · 当前 ${memoryItems.length ? memoryOffset + 1 : 0}–${memoryOffset + memoryItems.length};审计范围为最近 100 条` : '按需读取,离页停止'),
2027
+ h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: memoryOffset <= 0 || Boolean(memoryWorking), onClick: () => { setMemoryOffset(Math.max(0, memoryOffset - 20)); setMemoryData(null) } }, '前 20 条'),
2028
+ h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: !memoryData?.more || Boolean(memoryWorking), onClick: () => { setMemoryOffset(memoryOffset + 20); setMemoryData(null) } }, '后 20 条'),
2029
+ h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(memoryWorking), onClick: () => setMemoryRefresh((n) => n + 1) }, '重新读取记忆')))
1559
2030
  const q = learnQ && sameLearnView(learnView) ? learnQ : null
1560
2031
  const items = q && Array.isArray(q.items) ? q.items : []
1561
2032
  const sourceLinks = (one) => {
@@ -1591,9 +2062,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1591
2062
  h('div', { 'data-bd-row': '' },
1592
2063
  canApprove ? h('button', { 'data-bd-btn': '', 'data-sm': '', 'data-primary': '', disabled: pending, onClick: () => decideLearn(one.id, 'approve') }, icon('spark'), one.state === 'rejected' ? '重新批准' : '批准') : null,
1593
2064
  canReject ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: pending, onClick: () => decideLearn(one.id, 'reject') }, icon('dots'), one.state === 'approved' ? '改为拒绝' : '拒绝') : null,
1594
- h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: pending, onClick: () => decideLearn(one.id, 'forget') }, icon('gear'), '忘记')))
2065
+ h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: pending, onClick: () => decideLearn(one.id, 'forget') }, icon('gear'), '移除记录')))
1595
2066
  }) : null
1596
- const learnCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
2067
+ const learnCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-learning-queue': '', ref: learnQueueRef },
1597
2068
  h('div', { 'data-bd-row': '' }, sectionHead('list', '学习队列'),
1598
2069
  h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, `待审 ${Number(learnInfo.pending) || 0} · 已批准 ${Number(learnInfo.approved) || 0} · 已拒绝 ${Number(learnInfo.rejected) || 0}`)),
1599
2070
  /* 判重账本概览(2026-10-27 从旧设置页搬进来):「刷过不再刷、回过不再回」记了多少得有地方看 */
@@ -1601,24 +2072,24 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1601
2072
  const sn = st && st.seen
1602
2073
  if (!sn) return null
1603
2074
  return h('div', { 'data-bd-note': '' },
1604
- `判重账本:刷过 ${Number(sn.videos) || 0} 个视频(今天新记 ${Number(sn.videosToday) || 0} 个)· `
1605
- + `回过 ${Number(sn.replied) || 0} 条评论 —— 同一个不会再来一遍`)
2075
+ `判重账本:已浏览 ${Number(sn.videos) || 0} 个视频(今日新增 ${Number(sn.videosToday) || 0} 个)· `
2076
+ + `已回复 ${Number(sn.replied) || 0} 条评论`)
1606
2077
  })(),
1607
- h('div', { 'data-bd-note': '' }, '批准后才会注入后续行为;例句、来源、决定理由和版本都保留,忘记只撤销当前记录并留下审计痕迹。'),
2078
+ h('div', { 'data-bd-note': '' }, '已批准内容可注入后续提示词。记录保留例句、来源、决定理由和版本;移除当前记录后仍保留审计。'),
1608
2079
  h('div', { 'data-bd-row': '' },
1609
2080
  ...[['pending', '待审'], ['approved', '已批准'], ['rejected', '已拒绝'], ['all', '全部']].map(([value, label]) => h('button', {
1610
2081
  key: value, 'data-bd-btn': '', 'data-sm': '', 'data-primary': learnState === value ? '' : undefined,
1611
2082
  onClick: () => { setLearnState(value); setLearnOffset(0); setLearnQ(null) },
1612
2083
  }, label)),
1613
- h('select', { 'data-bd-input': '', style: { width: '130px', flex: '0 1 130px' }, value: learnKind, onChange: (e) => { setLearnKind(e.target.value); setLearnOffset(0); setLearnQ(null) } },
2084
+ h('select', { 'data-bd-input': '', 'aria-label': '学习记录类别', style: { width: '130px', flex: '0 1 130px' }, value: learnKind, onChange: (e) => { setLearnKind(e.target.value); setLearnOffset(0); setLearnQ(null) } },
1614
2085
  h('option', { value: '' }, '全部类别'), ...kinds.map((kind) => h('option', { key: kind, value: kind }, labels[kind] || kind)))),
1615
2086
  h('input', {
1616
- 'data-bd-input': '', value: learnFilter, maxLength: 160, placeholder: '筛正文、例句、来源或决定理由',
2087
+ 'data-bd-input': '', value: learnFilter, maxLength: 160, placeholder: '按正文、例句、来源或决定理由筛选',
1617
2088
  'aria-label': '筛选学习记录', onChange: (e) => { setLearnFilter(e.target.value); setLearnOffset(0); setLearnQ(null) },
1618
2089
  }),
1619
2090
  learnError ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '学习记录读取失败:' + learnError) : null,
1620
2091
  !q ? h('div', { 'data-bd-list-empty': '', role: 'status' }, '正在读取学习记录…')
1621
- : !items.length ? h('div', { 'data-bd-list-empty': '' }, learnFilter ? '没有匹配的学习记录' : '这个状态还没有学习记录')
2092
+ : !items.length ? h('div', { 'data-bd-list-empty': '' }, learnFilter ? '无匹配的学习记录' : '该状态暂无学习记录')
1622
2093
  : h('div', { 'data-bd-learn-list': '', tabIndex: 0, 'aria-label': '学习记录列表' }, ...learnRows),
1623
2094
  q ? h('div', { 'data-bd-row' : '' },
1624
2095
  h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, `显示 ${items.length ? learnOffset + 1 : 0}–${learnOffset + items.length} / ${q.matched == null ? items.length : q.matched} 条`),
@@ -1633,16 +2104,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1633
2104
  h('div', { 'data-bd-row': '', 'data-bd-toggle': '' },
1634
2105
  h('div', { 'data-bd-grow': '' },
1635
2106
  h('div', { 'data-bd-label': '' }, '自动审核待审内容'),
1636
- h('div', { 'data-bd-note': '' }, '由模型逐条判断;证据不足会拒绝或保留待审,不会代替你发 B站 内容。')),
2107
+ h('div', { 'data-bd-note': '' }, '模型逐条审核学习记录,证据不足时拒绝或保留待审。此过程不发送 B站内容。')),
1637
2108
  switchControl(readSwitch('learnAutoReview'), Boolean(busy), (v) => saveConfig({ learnAutoReview: v }, '自动审核学习队列'))),
1638
2109
  h('details', { 'data-bd-advanced': '' }, h('summary', {}, '模型与审核参数'), h('div', {},
1639
2110
  h('div', { 'data-bd-row': '' },
1640
2111
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '审核模型'),
1641
- reviewModels.length ? h('select', { 'data-bd-input': '', value: reviewModelDraft === null ? '' : reviewModelDraft, onChange: (e) => setReviewModelDraft(e.target.value) },
2112
+ reviewModels.length ? h('select', { 'data-bd-input': '', 'aria-label': '审核模型', value: reviewModelDraft === null ? '' : reviewModelDraft, onChange: (e) => setReviewModelDraft(e.target.value) },
1642
2113
  h('option', { value: '' }, '跟随后台模型'),
1643
2114
  reviewModelDraft && !reviewModels.some((m) => m.id === reviewModelDraft) ? h('option', { value: reviewModelDraft }, reviewModelDraft + '(清单外)') : null,
1644
2115
  reviewModels.map((m) => h('option', { key: m.id, value: m.id }, m.label || m.id)))
1645
- : h('input', { 'data-bd-input': '', value: reviewModelDraft === null ? '' : reviewModelDraft, placeholder: '留空 = 跟随后台模型', onChange: (e) => setReviewModelDraft(e.target.value) })),
2116
+ : h('input', { 'data-bd-input': '', 'aria-label': '审核模型', value: reviewModelDraft === null ? '' : reviewModelDraft, placeholder: '留空 = 跟随后台模型', onChange: (e) => setReviewModelDraft(e.target.value) })),
1646
2117
  h('div', { 'data-bd-row': '' },
1647
2118
  h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '每批最多'), h('input', { 'data-bd-input': '', type: 'number', min: 1, max: 50, value: reviewBatchDraft ?? '20', onChange: (e) => setReviewBatchDraft(e.target.value) })),
1648
2119
  h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '最长分钟'), h('input', { 'data-bd-input': '', type: 'number', min: 1, max: 60, value: reviewMinutesDraft ?? '10', onChange: (e) => setReviewMinutesDraft(e.target.value) })),
@@ -1662,14 +2133,14 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1662
2133
  safeSay(r.started ? '模型审核已开始,完成后会写入运行日志。' : (r.message || '当前没有待审核内容。'), r.started ? 'good' : '')
1663
2134
  } catch (e) { safeSay(String(e && e.message || e), 'warn') }
1664
2135
  finally { setBusy('') }
1665
- } }, reviewStatus.running ? '审核中…' : '现在审核一批'),
2136
+ } }, reviewStatus.running ? '审核中…' : '开始批量审核'),
1666
2137
  reviewStatus.running ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: async () => {
1667
2138
  setBusy('learn-review-stop')
1668
2139
  try { const r = check(await jpost('/learn/review', { action: 'stop' }), '停止模型审核失败'); if (r.learnReview) setSt((old) => ({ ...old, learnReview: r.learnReview })); safeSay('模型审核已请求停止。', 'good') }
1669
2140
  catch (e) { safeSay(String(e && e.message || e), 'warn') } finally { setBusy('') }
1670
2141
  } }, '停止审核') : null),
1671
2142
  h('div', { 'data-bd-row': '' },
1672
- h('span', { 'data-bd-badge': '', 'data-live': reviewStatus.running ? '' : undefined }, reviewStatus.running ? '审核中' : reviewStatus.complete ? '本批完成' : reviewStatus.lastError ? '本批未完成' : '待命'),
2143
+ h('span', { 'data-bd-badge': '', 'data-live': reviewStatus.running ? '' : undefined }, reviewStatus.running ? '审核中' : reviewStatus.complete ? '本批完成' : reviewStatus.lastError ? '本批未完成' : '未运行'),
1673
2144
  h('span', { 'data-bd-badge': '' }, '全部待审 ' + Number(reviewStatus.pending ?? learnInfo.pending ?? 0)),
1674
2145
  h('span', { 'data-bd-badge': '' }, '本批批准 ' + Number(reviewStatus.approved || 0)),
1675
2146
  h('span', { 'data-bd-badge': '' }, '本批拒绝 ' + Number(reviewStatus.rejected || 0)),
@@ -1677,23 +2148,79 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1677
2148
  reviewStatus.model ? h('div', { 'data-bd-note': '' }, (reviewStatus.running ? '正在使用:' : '最近使用:') + reviewStatus.model) : null,
1678
2149
  reviewStatus.lastError ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '审核提示:' + reviewStatus.lastError) : null,
1679
2150
  !reviewStatus.running && Number(reviewStatus.nextRetryAt) > 0 ? h('div', { 'data-bd-note': '' }, '自动重试最早:' + formatTime(reviewStatus.nextRetryAt) + (reviewStatus.enabled ? '' : '(自动审核未开启)')) : null,
1680
- h('div', { 'data-bd-note': '' }, reviewStatus.lastFinishedAt ? '上次结束:' + formatTime(reviewStatus.lastFinishedAt) : '尚未运行;打开自动审核后,下一次检查会处理待审队列。'))
2151
+ h('div', { 'data-bd-note': '' }, reviewStatus.lastFinishedAt ? '上次结束:' + formatTime(reviewStatus.lastFinishedAt) : '暂无审核记录。启用自动审核后,将在下次检查时处理待审队列。'))
2152
+
2153
+ /* 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)
2154
+ * 只在这里查/升级;普通鱼工具拿不到升级能力(换版本要重启宿主,不能让会话模型自己决定)。
2155
+ * 状态只在展开这一卡时按需拉取,不并进 /status 轮询。 */
2156
+ const [updateState, setUpdateState] = React.useState(null)
2157
+ const [updateError, setUpdateError] = React.useState('')
2158
+ const [updateBusy, setUpdateBusy] = React.useState('')
2159
+ const updateAlive = React.useRef(true)
2160
+ React.useEffect(() => () => { updateAlive.current = false }, [])
2161
+ const pullUpdate = async (force = false) => {
2162
+ try {
2163
+ const r = await jget('/update' + (force ? '?force=1' : ''))
2164
+ if (!updateAlive.current) return
2165
+ if (!r || r.ok === false && !r.current) throw new Error((r && r.error) || '检查更新失败')
2166
+ setUpdateState(r)
2167
+ setUpdateError(r.error || '')
2168
+ } catch (e) { if (updateAlive.current) setUpdateError(String(e && e.message || e)) }
2169
+ }
2170
+ React.useEffect(() => { pullUpdate() }, [])
2171
+ const updateCard = (() => {
2172
+ const u = updateState || {}
2173
+ const install = u.install || {}
2174
+ const checking = updateBusy === 'check'
2175
+ const upgrading = u.upgrading || updateBusy === 'upgrade'
2176
+ const canUpgrade = Boolean(u.canUpgrade) && !u.blocker && !upgrading
2177
+ const badges = []
2178
+ if (u.current) badges.push(h('span', { key: 'cur', 'data-bd-badge': '' }, 'v' + u.current))
2179
+ if (u.latest && u.latest !== u.current) badges.push(h('span', { key: 'latest', 'data-bd-badge': '', 'data-live': '' }, '最新 v' + u.latest))
2180
+ if (install.mode) badges.push(h('span', { key: 'mode', 'data-bd-badge': '' }, { registry: 'registry 安装', link: '目录联接', source: '源码检出', unknown: '未知安装' }[install.mode] || install.mode))
2181
+ return h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-update': '' },
2182
+ sectionHead('pulse', '检查更新'),
2183
+ h('div', { 'data-bd-row': '' }, ...badges),
2184
+ h('div', { 'data-bd-note': '' }, u.ok === false
2185
+ ? '暂时查不到最新版本:' + (u.error || updateError || '稍后再试')
2186
+ : (u.hint || '点「检查更新」问一下 registry 上有没有新版本。')),
2187
+ u.upgrading ? h('div', { 'data-bd-msg': '', role: 'status' }, '正在安装新版本…完成后需要完全重启 DSH。') : null,
2188
+ u.blocker ? h('div', { 'data-bd-note': '' }, '升级暂不可用:' + u.blocker) : null,
2189
+ h('div', { 'data-bd-row': '' },
2190
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy) || checking || upgrading, onClick: async () => {
2191
+ setUpdateBusy('check')
2192
+ try { await pullUpdate(true); safeSay('已检查更新。', 'good') }
2193
+ catch (e) { safeSay(String(e && e.message || e), 'warn') }
2194
+ finally { if (updateAlive.current) setUpdateBusy('') }
2195
+ } }, checking ? '正在检查…' : '检查更新'),
2196
+ h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy) || !canUpgrade, title: !canUpgrade && install.reason ? install.reason : '从 registry 安装新版本(会重启宿主后生效)', onClick: async () => {
2197
+ if (typeof window.confirm === 'function' && !window.confirm('从 registry 安装 ' + (u.latest || '新版本') + '?\n\n升级完成后需要完全重启 DSH 才生效。运行数据(凭证、日报、图库、账本)都在家目录,不受影响。')) return
2198
+ setUpdateBusy('upgrade')
2199
+ try {
2200
+ const r = check(await jpost('/upgrade', { confirm: true }), '升级失败')
2201
+ if (r.ok === false) { safeSay(r.error || '升级没有开始', 'warn'); if (r.install) setUpdateState(r) }
2202
+ else { safeSay('已开始升级到 ' + (r.to || '新版本') + ',完成后完全重启 DSH。', 'good'); setUpdateState((old) => ({ ...(old || {}), upgrading: true })) }
2203
+ } catch (e) { safeSay(String(e && e.message || e), 'warn') }
2204
+ finally { if (updateAlive.current) setUpdateBusy('') }
2205
+ } }, upgrading ? '正在升级…' : '升级到最新版')),
2206
+ h('div', { 'data-bd-note': '' }, '只对「registry 安装」开放一键升级。本机分叉(目录联接)、源码检出或不在 profile 里的安装会锁定按钮并说明手动升级办法——2026-10-21 的自动升级曾把本机分叉整包换掉。'))
2207
+ })()
1681
2208
 
1682
2209
  const mirror = (st && st.mirror) || {}
1683
2210
  const mirrorCard = h('section', { 'data-bd-sec': '' },
1684
2211
  sectionHead('dots', '记录镜像'),
1685
2212
  h('div', { 'data-bd-note': '' }, mirror.error ? '镜像出错:' + mirror.error
1686
2213
  : mirror.dir ? `复制到:${mirror.dir}${mirror.lastAt ? ' · 上次 ' + formatTime(mirror.lastAt) + '(' + ((mirror.lastCopied || []).length) + ' 个)' : ''}`
1687
- : '还没有可用镜像目录;可以填写目录,或让插件从 out/digest junction 自动推断。'),
2214
+ : '镜像目录未配置。可指定目录,或根据 out/digest 目录联接自动推断。'),
1688
2215
  h('div', { 'data-bd-row': '' },
1689
- h('input', { 'data-bd-input': '', value: mirrorDirDraft === null ? '' : mirrorDirDraft, placeholder: '留空 = 自动推断镜像目录', onChange: (e) => setMirrorDirDraft(e.target.value) }),
2216
+ h('input', { 'data-bd-input': '', 'aria-label': '记录镜像目录', value: mirrorDirDraft === null ? '' : mirrorDirDraft, placeholder: '留空 = 自动推断镜像目录', onChange: (e) => setMirrorDirDraft(e.target.value) }),
1690
2217
  h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(busy), onClick: () => saveConfig({ mirrorDir: String(mirrorDirDraft || '').trim() }, '镜像目录') }, '保存目录')),
1691
2218
  h('div', { 'data-bd-row': '' },
1692
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy) || !mirror.dir, onClick: syncMirror }, icon('dots'), '现在同步'),
2219
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy) || !mirror.dir, onClick: syncMirror }, icon('dots'), '同步记录'),
1693
2220
  h('button', { 'data-bd-btn': '', disabled: Boolean(busy) || !mirror.dir, onClick: openMirror }, icon('gear'), '打开目录'),
1694
2221
  mirror.dir ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(busy), onClick: () => copyText(mirror.dir) }, '复制路径') : null),
1695
2222
  mirrorNote ? h('div', { 'data-bd-msg': '' }, mirrorNote) : null,
1696
- h('div', { 'data-bd-note': '' }, '镜像是给人看的脱敏快照,不会复制登录凭证。'))
2223
+ h('div', { 'data-bd-note': '' }, '镜像保存脱敏记录快照,不包含登录凭证。'))
1697
2224
 
1698
2225
  const accountCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
1699
2226
  sectionHead('key', '账号与环境'),
@@ -1702,16 +2229,19 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1702
2229
  h('span', { 'data-bd-badge': '', 'data-live': st && st.venvReady ? '' : undefined }, st && st.venvReady ? '主环境就绪' : '主环境未就绪'),
1703
2230
  h('span', { 'data-bd-badge': '', 'data-live': st && st.dshCli && st.dshCli.ok ? '' : undefined }, st && st.dshCli && st.dshCli.ok ? 'dsh命令正常' : 'dsh命令待检查'),
1704
2231
  h('span', { 'data-bd-badge': '' }, 'v' + String((st && st.version) || '?'))),
1705
- h('div', { 'data-bd-note': '' }, '首次使用先安装主依赖,再扫码登录;测一下会实际检查登录和搜索。'),
2232
+ typeof st?.environment?.upgradeHint === 'string' && st.environment.upgradeHint.trim()
2233
+ ? h('div', { 'data-bd-msg': '', 'data-warn': '', 'data-bd-environment-upgrade': '', role: 'status', 'aria-live': 'polite', 'aria-label': '环境升级说明' }, st.environment.upgradeHint)
2234
+ : null,
2235
+ h('div', { 'data-bd-note': '' }, '首次使用需安装主依赖并扫码登录。连接检查将验证登录状态和搜索接口。'),
1706
2236
  h('div', { 'data-bd-row': '' },
1707
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: onInstall }, icon('gear'), '装主依赖'),
1708
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: onCheck }, icon('pulse'), '测一下'),
2237
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: onInstall }, icon('gear'), '安装主依赖'),
2238
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: onCheck }, icon('pulse'), '检查连接'),
1709
2239
  h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: onLoginStart }, icon('key'), '扫码登录'),
1710
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy), title: '把版本/环境/最近错误/日志尾部拼成一段文本复制走 —— 出问题直接把剪贴板贴出来', onClick: onDiag }, icon('dots'), busy === 'diag' ? '打包中…' : '一键诊断')),
2240
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy), title: '复制版本、环境、近期错误和日志摘要,用于故障排查', onClick: onDiag }, icon('dots'), busy === 'diag' ? '正在生成…' : '生成诊断信息')),
1711
2241
  login ? h('div', { 'data-bd-qr': '' },
1712
2242
  login.qr ? h('img', { src: login.qr, alt: 'B站扫码登录二维码' }) : h('div', { 'data-bd-msg': '', 'data-warn': '' }, '二维码没有生成,请先安装主依赖。'),
1713
2243
  h('div', { 'data-bd-grow': '' },
1714
- h('div', { 'data-bd-note': '' }, '手机 B站打开扫一扫,确认时选择记住我或长期登录。'),
2244
+ h('div', { 'data-bd-note': '' }, '使用手机 B站扫码,并确认登录;可选择记住登录状态。'),
1715
2245
  h('div', { 'data-bd-msg': '', 'data-warn': loginTone === 'warn' ? '' : undefined, 'data-good': loginTone === 'good' ? '' : undefined }, loginState || '等待扫码…'))) : null,
1716
2246
  loginState && !login ? h('div', { 'data-bd-msg': '', 'data-warn': loginTone === 'warn' ? '' : undefined, 'data-good': loginTone === 'good' ? '' : undefined }, loginState) : null,
1717
2247
  st && st.installLog ? h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, '主依赖日志:' + st.installLog), h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => copyText(st.installLog) }, '复制路径')) : null,
@@ -1724,11 +2254,11 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1724
2254
  switchControl(readSwitch('transcribe'), Boolean(busy), (v) => saveConfig({ transcribe: v }, '本地转写'))),
1725
2255
  h('div', { 'data-bd-row': '' },
1726
2256
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, 'Whisper 模型'),
1727
- h('select', { 'data-bd-input': '', style: { width: '140px' }, value: asrModelDraft === null ? 'small' : asrModelDraft, onChange: (e) => setAsrModelDraft(e.target.value) },
2257
+ h('select', { 'data-bd-input': '', 'aria-label': 'Whisper 模型', style: { width: '140px' }, value: asrModelDraft === null ? 'small' : asrModelDraft, onChange: (e) => setAsrModelDraft(e.target.value) },
1728
2258
  ...['tiny', 'base', 'small', 'medium'].map((name) => h('option', { key: name, value: name }, name)))),
1729
2259
  h('div', { 'data-bd-row': '' },
1730
2260
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '主 Python 路径'),
1731
- h('input', { 'data-bd-input': '', value: pythonPathDraft === null ? '' : pythonPathDraft, onChange: (e) => setPythonPathDraft(e.target.value), placeholder: 'python 或 venv/python.exe' })),
2261
+ h('input', { 'data-bd-input': '', 'aria-label': '主 Python 路径', value: pythonPathDraft === null ? '' : pythonPathDraft, onChange: (e) => setPythonPathDraft(e.target.value), placeholder: 'python 或 venv/python.exe' })),
1732
2262
  h('div', { 'data-bd-row': '' },
1733
2263
  h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => saveConfig({ asrModel: String(asrModelDraft || 'small'), pythonPath: String(pythonPathDraft || '').trim() }, 'ASR参数') }, '保存 ASR 参数'),
1734
2264
  h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: onInstallAsr }, icon('gear'), st && st.asrReady ? '转写已安装' : '安装转写')),
@@ -1740,21 +2270,21 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1740
2270
  st && st.asrTail ? h('pre', { 'data-bd-pre': '' }, String(st.asrTail).slice(-1600)) : null)
1741
2271
 
1742
2272
  const groundCard = h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
1743
- sectionHead('pulse', '它的地盘与发言复核'),
1744
- h('div', { 'data-bd-kv': '' }, '家目录:' + String((st && st.home) || '')),
2273
+ sectionHead('pulse', '本地数据'),
2274
+ h('div', { 'data-bd-kv': '' }, '数据目录:' + String((st && st.home) || '')),
1745
2275
  h('div', { 'data-bd-kv': '' }, '报告路径:' + String((st && st.report) || '')),
1746
2276
  h('div', { 'data-bd-row' : '' },
1747
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => openFolder('home', '地盘') }, icon('pulse'), '打开地盘'),
2277
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => openFolder('home', '数据目录') }, icon('pulse'), '打开数据目录'),
1748
2278
  h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => openFolder('out', '产物目录') }, icon('gear'), '打开产物目录'),
1749
- st && st.home ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(busy), onClick: () => copyText(st.home) }, '复制地盘路径') : null,
2279
+ st && st.home ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(busy), onClick: () => copyText(st.home) }, '复制数据目录路径') : null,
1750
2280
  st && st.report ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(busy), onClick: () => copyText(st.report) }, '复制报告路径') : null),
1751
- h('div', { 'data-bd-note': '' }, '这里保留的是插件运行地盘、输出路径和发言复核入口;不会打开或改写登录凭证。'))
2281
+ h('div', { 'data-bd-note': '' }, '管理插件数据和输出目录。登录凭证保持独立存储。'))
1752
2282
 
1753
2283
  return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': view === 'learning' ? 'learning' : 'other' },
1754
2284
  h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, view === 'learning' ? '学习与记忆' : '账号与环境'),
1755
- h('p', {}, view === 'learning' ? '审核在后台进行。这里查看进度、管理记录与她的小心事。' : '登录、依赖与本地记录。环境状态集中在这里。'))),
2285
+ h('p', {}, view === 'learning' ? '管理学习审核、角色记忆、口味统计和历史记录。' : '管理账号登录、运行依赖和本地数据。'))),
1756
2286
  h('div', { 'data-bd-pet-grid': '', 'data-bd-other': '' },
1757
- ...(view === 'learning' ? [reviewCard, learnCard, soulCard] : [accountCard, asrCard, mirrorCard, groundCard])))
2287
+ ...(view === 'learning' ? [memoryOverviewCard, memoryExplorerCard, reviewCard, learnCard, soulCard] : [accountCard, updateCard, asrCard, mirrorCard, groundCard])))
1758
2288
  }
1759
2289
 
1760
2290
 
@@ -1774,8 +2304,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1774
2304
  const partialCount = rounds.filter(partial).length
1775
2305
  const background = st.backgroundRun || {}
1776
2306
  const current = background.active ? background.usage?.total : st.feiyu?.active ? st.feiyu.tokens?.thisRun : undefined
1777
- const currentLabel = background.active ? (background.task === 'learn-review' ? '本趟审核' : '本趟后台') : st.feiyu?.active ? '本趟会话' : '当前任务'
1778
- const endLabels = { user: '手动结束', exit: '已结束', error: '错误退出', 'token-budget': '本趟预算用完', 'total-budget': '累计上限已到', 'time-limit': '到达时限', 'spawn-error': '启动失败' }
2307
+ const currentLabel = background.active ? (background.task === 'learn-review' ? '本次审核' : '本次后台') : st.feiyu?.active ? '本次会话' : '当前任务'
2308
+ const endLabels = { user: '手动结束', exit: '已结束', error: '错误退出', 'token-budget': '已达单次预算', 'total-budget': '已达累计上限', 'time-limit': '到达时限', 'spawn-error': '启动失败' }
1779
2309
  const warnEnds = ['error', 'token-budget', 'total-budget', 'time-limit', 'spawn-error']
1780
2310
  const cols = []
1781
2311
  const dayValues = st.tokenDays && typeof st.tokenDays === 'object' ? st.tokenDays : {}
@@ -1796,32 +2326,32 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1796
2326
  return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'ledger', 'data-bd-ledger': '', 'data-bd-ledger-focus': focus ? '' : undefined },
1797
2327
  h('div', { 'data-bd-ledger-heading': '' },
1798
2328
  h('div', {}, h('span', { 'data-bd-eyebrow': '' }, 'DAFEIYU / TOKEN JOURNAL'),
1799
- h('h2', {}, 'token 账本'), h('p', {}, '每一趟认真摸鱼,都有据可查。')),
2329
+ h('h2', {}, 'token 账本'), h('p', {}, '查看累计用量、每日趋势和任务明细。')),
1800
2330
  h('button', { 'data-bd-btn': '', type: 'button', 'aria-pressed': focus, onClick: () => setFocus((old) => !old) }, focus ? '退出截图视图' : '截图视图')),
1801
2331
  h('section', { 'data-bd-ledger-summary': '', 'aria-label': 'token 用量总览' },
1802
2332
  h('div', { 'data-bd-ledger-hero': '' },
1803
2333
  h('span', { 'data-bd-ledger-label': '' }, '累计用量'),
1804
2334
  h('div', { 'data-bd-ledger-number': '' }, h('b', { 'data-bd-ledger-total': '' }, fmt(total)), h('span', {}, 'tokens')),
1805
2335
  h('div', { 'data-bd-ledger-meta': '' },
1806
- known(total) ? h('span', { 'data-bd-tok-chip': '', title: '称号只是显示,不影响行为' }, '称号 · ' + tokTitle(total)) : null,
1807
- Number(st.activeDays) > 0 ? h('span', {}, '连着干活 ' + st.activeDays + ' 天') : null),
2336
+ known(total) ? h('span', { 'data-bd-tok-chip': '', title: '称号仅用于显示,不影响运行行为' }, '称号 · ' + tokTitle(total)) : null,
2337
+ Number(st.activeDays) > 0 ? h('span', {}, '连续运行 ' + st.activeDays + ' 天') : null),
1808
2338
  h('p', {}, '已记录的真实用量,不含未计量任务。')),
1809
2339
  h('div', { 'data-bd-tok-kpis': '' },
1810
2340
  h('div', {}, h('span', {}, '今日用量'), h('b', {}, fmt(today)), h('small', {}, '本地时间 00:00 起')),
1811
2341
  h('div', {}, h('span', {}, currentLabel), h('b', {}, fmt(current)), h('small', {}, background.active && Number(background.unknownRequests) > 0 ? '另有 ' + background.unknownRequests + ' 次响应用量缺失' : background.active || st.feiyu?.active ? '正在记录中' : '当前没有运行任务')),
1812
- h('div', {}, h('span', {}, '累计上限'), h('b', {}, cap > 0 ? fmt(cap) : '不限'), h('small', {}, '已保存的总闸额度'))),
2342
+ h('div', {}, h('span', {}, '累计上限'), h('b', {}, cap > 0 ? fmt(cap) : '不限'), h('small', {}, '已保存的累计限额'))),
1813
2343
  cap > 0 ? h('div', { 'data-bd-ledger-budget': '' },
1814
2344
  h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-grow': '' }, '累计上限进度'), h('b', {}, ratio === null ? '—' : ratio + '%')),
1815
2345
  h('div', { 'data-bd-tok-bar': '', role: 'progressbar', 'aria-label': '累计 token 上限使用比例', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': ratio === null ? undefined : Math.min(100, ratio) },
1816
2346
  h('i', { style: { width: (ratio === null ? 0 : Math.min(100, ratio)) + '%' } })),
1817
- h('p', {}, '到顶会自动暂停会话刷;后台单轮预算在「后台刷」页单独设置。')) : null),
2347
+ h('p', {}, '达到累计上限后暂停会话刷。后台单次预算在运行方式中单独设置。')) : null),
1818
2348
  h('section', { 'data-bd-ledger-trend': '', 'aria-label': '最近 14 天 token 用量' },
1819
2349
  h('div', { 'data-bd-ledger-sectionhead': '' }, h('h3', {}, '最近 14 天'), h('span', {}, '每日已计量用量 · tokens')),
1820
2350
  cols.some((c) => c.value > 0) ? h('div', { 'data-bd-tok-days': '' }, cols.map((c) => h('div', { key: c.key, 'data-zero': c.value > 0 ? undefined : '', title: `${c.key} · ${fmt(c.value)} tokens` },
1821
2351
  h('div', { 'data-bd-tok-daytrack': '' }, h('i', { style: { height: Math.max(3, Math.round(c.value / dayMax * 100)) + '%' } })),
1822
2352
  h('span', {}, c.label)))) : h('div', { 'data-bd-tok-empty': '' }, '最近 14 天还没有已计量用量。')),
1823
- h('section', { 'data-bd-ledger-tasks': '', 'aria-label': '每趟任务明细' },
1824
- h('div', { 'data-bd-ledger-sectionhead': '' }, h('h3', {}, '每趟任务'),
2353
+ h('section', { 'data-bd-ledger-tasks': '', 'aria-label': '任务用量明细' },
2354
+ h('div', { 'data-bd-ledger-sectionhead': '' }, h('h3', {}, '任务明细'),
1825
2355
  h('span', {}, `${rounds.length} 条记录${unknownCount ? ' · ' + unknownCount + ' 条未计量' : ''}${partialCount ? ' · ' + partialCount + ' 条部分计量' : ''}`)),
1826
2356
  h('div', { 'data-bd-tok-list': '' }, shownRounds.length ? shownRounds.map((r) => h('div', { 'data-bd-tokrow': '', key: r.id },
1827
2357
  h('time', { title: r.startedAt || '' }, petFmtTime(r.startedAt) || '—'),
@@ -1832,16 +2362,16 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1832
2362
  h('em', { 'data-unknown': unmetered(r) ? '' : undefined }, unmetered(r) ? '未计量' : fmt(r.tokens.total)),
1833
2363
  !unmetered(r) ? h('small', {}, partial(r) ? '部分计量 · 另有 ' + r.unknownRequests + ' 次缺失' : '输入 ' + fmt(r.tokens.input) + ' / 输出 ' + fmt(r.tokens.output)) : h('small', {}, '不按 0 计算')),
1834
2364
  r.stopReason ? h('i', { 'data-bd-tok-chip': '', 'data-warn': warnEnds.includes(String(r.stopReason)) ? '' : undefined, title: String(r.stopReason) }, endLabels[r.stopReason] || String(r.stopReason)) : null))
1835
- : h('div', { 'data-bd-tok-empty': '' }, '还没有任务记录 —— 开一次会话刷或后台刷,这里就会记下来。')),
2365
+ : h('div', { 'data-bd-tok-empty': '' }, '暂无任务记录。会话和后台任务运行后将记录用量。')),
1836
2366
  focus && rounds.length > shownRounds.length ? h('p', { 'data-bd-note': '' }, '截图视图展示最近 4 条;退出后查看全部 ' + rounds.length + ' 条记录。') : null),
1837
2367
  h('p', { 'data-bd-ledger-footnote': '' }, '计量说明:累计数只含已知用量,任务明细独立记账。部分计量保留已知数及缺失次数;未计量不等于没有消耗。'),
1838
2368
  !focus ? h('section', { 'data-bd-ledger-controls': '' },
1839
2369
  h('div', { 'data-bd-ledger-sectionhead': '' }, h('h3', {}, '预算与计数'), h('span', {}, '仅保存后生效')),
1840
2370
  h('div', { 'data-bd-row': '' },
1841
- h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '累计最多用多少 token(0 = 不限)'),
2371
+ h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '累计 token 上限(0 = 不限)'),
1842
2372
  h('input', { 'data-bd-input': '', type: 'number', min: 0, max: 50000000, step: 100000, value: budgetDraft ?? '0', onChange: (e) => setBudgetDraft(e.target.value) })),
1843
2373
  h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: saveBudget }, '保存上限'),
1844
- h('button', { 'data-bd-btn': '', 'data-bd-danger': '', disabled: Boolean(busy), title: '累计和任务明细一起清零,上限不动;操作前需确认', onClick: clear }, '清零计数'))) : null)
2374
+ h('button', { 'data-bd-btn': '', 'data-bd-danger': '', disabled: Boolean(busy), title: '清零累计用量和任务明细,保留累计上限;操作需确认', onClick: clear }, '清零计数'))) : null)
1845
2375
  }
1846
2376
 
1847
2377
  function PetStickerThumb({ item, large = false }) {
@@ -1863,6 +2393,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1863
2393
  const [draft, setDraft] = React.useState({})
1864
2394
  const [query, setQuery] = React.useState('')
1865
2395
  const [source, setSource] = React.useState('all')
2396
+ const [review, setReview] = React.useState('approved')
1866
2397
  const [name, setName] = React.useState('')
1867
2398
  const [url, setUrl] = React.useState('')
1868
2399
  const [description, setDescription] = React.useState('')
@@ -1887,8 +2418,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1887
2418
  if (reading.current || writing.current) return
1888
2419
  reading.current = true; setLoading(true)
1889
2420
  try {
1890
- const r = await jget('/stickers')
1891
- if (!r?.ok) throw new Error(r?.error || '图库暂时读不到')
2421
+ const r = await jget('/stickers?status=all')
2422
+ if (!r?.ok) throw new Error(r?.error || '图库读取失败')
1892
2423
  if (alive.current) { setData(r); setError(''); routeMissingRef.current = false; setRouteMissing(false) }
1893
2424
  } catch (e) {
1894
2425
  if (alive.current) {
@@ -1959,15 +2490,19 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1959
2490
  if (kind === 'upload') setUploadError('')
1960
2491
  try {
1961
2492
  const r = await jpost('/stickers/' + kind, body)
1962
- if (!r?.ok) throw new Error(r?.error || '没有完成')
2493
+ if (!r?.ok) throw new Error(r?.error || '操作未完成')
1963
2494
  if (!alive.current) return
1964
- say(kind === 'collect' ? '已收进表情库。' : kind === 'upload' ? '已上传到表情库。' : '已从收集区移除。', 'good')
2495
+ say(kind === 'approve' ? '已批准,表情已加入可用图库。' : kind === 'reject' ? '已拒绝并移除该待审表情。'
2496
+ : r.status === 'pending' ? '已加入待审队列,批准后可使用。'
2497
+ : kind === 'collect' ? '已加入表情库。' : kind === 'upload' ? '已上传至表情库。' : '已从收集区移除。', 'good')
1965
2498
  if (kind === 'collect') { setName(''); setUrl(''); setDescription('') }
1966
- if (kind === 'upload') { clearUpload(); setUploadName(''); setUploadDescription(''); setSource('collected'); setQuery(''); setSelected('collected:' + (r.name || body.name)) }
1967
- if (kind === 'forget' && selected === 'collected:' + body.name) setSelected('')
2499
+ if (kind === 'upload') { clearUpload(); setUploadName(''); setUploadDescription('') }
2500
+ if (kind === 'upload' || kind === 'collect') { setSource('collected'); setReview(r.status === 'pending' ? 'pending' : 'approved'); setQuery(''); setSelected('collected:' + (r.name || body.name)) }
2501
+ if (kind === 'approve') setReview('approved')
2502
+ if ((kind === 'forget' || kind === 'reject') && selected === 'collected:' + body.name) setSelected('')
1968
2503
  } catch (e) {
1969
2504
  if (alive.current) {
1970
- const message = e?.status === 404 ? '当前 DSH 后端尚未加载这个图库功能。请完全退出并重新打开 DSH 后重试。' : String(e?.message ?? e)
2505
+ const message = e?.status === 404 && e?.code !== 'STICKER_NOT_FOUND' ? '当前 DSH 后端尚未加载对应的图库接口。请完全退出并重新打开 DSH 后重试。' : String(e?.message ?? e)
1971
2506
  if (kind === 'upload') setUploadError(message)
1972
2507
  say(message, 'warn')
1973
2508
  }
@@ -1977,12 +2512,12 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1977
2512
  const upload = (event) => {
1978
2513
  event?.preventDefault?.()
1979
2514
  if (working || !uploadFile) return
1980
- if (!uploadName.trim()) { setUploadError('请给表情起一个名字。'); return }
2515
+ if (!uploadName.trim()) { setUploadError('请输入表情名称。'); return }
1981
2516
  mutate('upload', { name: uploadName.trim(), filename: uploadFile.name, dataBase64: uploadFile.dataBase64, description: uploadDescription.trim() })
1982
2517
  }
1983
2518
  const collect = (event) => {
1984
2519
  event?.preventDefault?.()
1985
- if (!name.trim() || !/^https?:\/\//i.test(url.trim())) { say('填一个好叫的名字和 http(s) 图片链接。', 'warn'); return }
2520
+ if (!name.trim() || !/^https?:\/\//i.test(url.trim())) { say('请输入表情名称和 http(s) 图片链接。', 'warn'); return }
1986
2521
  mutate('collect', { name: name.trim(), url: url.trim(), description: description.trim() })
1987
2522
  }
1988
2523
  const forget = (item) => {
@@ -1990,70 +2525,90 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
1990
2525
  if (typeof window.confirm !== 'function' || !window.confirm('从收集区移除「' + item.name + '」?内置表情不会改变。')) return
1991
2526
  mutate('forget', { name: item.name })
1992
2527
  }
2528
+ const decide = (kind, item) => {
2529
+ if (item.source !== 'collected' || item.status !== 'pending' || !/^[a-f0-9]{64}$/.test(item.hash || '')) return
2530
+ mutate(kind, { name: item.name, expectedHash: item.hash })
2531
+ }
1993
2532
  const rows = Array.isArray(data?.stickers) ? data.stickers : []
2533
+ const pendingCount = data?.counts?.pending ?? rows.filter((item) => item.status === 'pending').length
1994
2534
  const q = query.trim().toLowerCase()
1995
- const shown = rows.filter((item) => (source === 'all' || item.source === source) && (!q || [item.name, item.description, ...(Array.isArray(item.tags) ? item.tags : [])].some((v) => String(v || '').toLowerCase().includes(q))))
2535
+ const shown = rows.filter((item) => (review === 'all' || (item.status || 'approved') === review) && (source === 'all' || item.source === source) && (!q || [item.name, item.description, ...(Array.isArray(item.tags) ? item.tags : [])].some((v) => String(v || '').toLowerCase().includes(q))))
1996
2536
  const current = rows.find((item) => item.source + ':' + item.name === selected)
1997
2537
  const disabled = Boolean(busy || working || loading)
1998
2538
  const input = (label, key, max, suffix, min = 0) => h('label', { 'data-bd-sticker-number': '' }, h('span', {}, label),
1999
2539
  h('span', {}, h('input', { 'data-bd-input': '', type: 'number', min, max, step: 1, 'aria-label': label, value: field(key), onChange: (e) => setDraft((old) => ({ ...old, [key]: e.target.value })) }), h('small', {}, suffix)))
2540
+ const reviewActions = (item) => item.source === 'collected' && item.status === 'pending' ? h('div', { 'data-bd-sticker-reviewactions': '' },
2541
+ h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: disabled || !/^[a-f0-9]{64}$/.test(item.hash || ''), 'aria-label': '批准表情:' + item.name, onClick: () => decide('approve', item) }, '批准使用'),
2542
+ h('button', { 'data-bd-btn': '', disabled: disabled || !/^[a-f0-9]{64}$/.test(item.hash || ''), 'aria-label': '拒绝表情:' + item.name, onClick: () => decide('reject', item) }, '拒绝并移除')) : null
2000
2543
  return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'stickers' },
2001
- h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, '表情收藏'), h('p', {}, '逛评论区,遇见喜欢的就收一张。让每次配图都有点自己的意思。'))),
2544
+ h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, '表情收藏'), h('p', {}, '管理表情收集、上传、审核和评论配图。'))),
2002
2545
  h('div', { 'data-bd-pet-grid': '', 'data-bd-sticker-controls': '' },
2003
- h('section', { 'data-bd-sec': '' }, h('div', { 'data-bd-h': '' }, h('b', {}, '自己收表情')),
2004
- ToggleRow({ label: '从评论区自动收集', on: Boolean(cfg.stickerAutoCollect), disabled, note: '翻评论时,按概率收取观众附带的图片;同图不重复,库满就停。', onChange: (v) => patch({ stickerAutoCollect: v }, '自动收表情') }),
2546
+ h('section', { 'data-bd-sec': '' }, h('div', { 'data-bd-h': '' }, h('b', {}, '表情收集')),
2547
+ ToggleRow({ label: '从评论区自动收集', on: Boolean(cfg.stickerAutoCollect), disabled, note: '读取评论时,按设定概率收集附图。相同图片不重复收集,达到容量后停止。', onChange: (v) => patch({ stickerAutoCollect: v }, '自动收集表情') }),
2548
+ ToggleRow({ label: '新表情人工审核', on: Boolean(cfg.stickerReview), disabled, note: '新收集和上传的图片经批准后方可配图。关闭仅影响后续新增图片,已有待审图片仍需审核。', onChange: (v) => patch({ stickerReview: v }, '表情人工审核') }),
2005
2549
  input('收集概率', 'stickerCollectProbability', 100, '%'), input('收集冷却(秒)', 'stickerCollectCooldown', 86400, '秒'), input('收集容量', 'stickerCapacity', 60, '张', 1),
2006
2550
  h('button', { 'data-bd-btn': '', disabled, onClick: () => save('collect') }, '保存收集参数')),
2007
- h('section', { 'data-bd-sec': '' }, h('div', { 'data-bd-h': '' }, h('b', {}, '发言时配一张')),
2008
- ToggleRow({ label: '新评论自动配图', on: Boolean(cfg.stickerAutoSend), disabled, note: '有话想说并已允许评论时,从库里选一张。只对支持图片的新评论生效。', onChange: (v) => patch({ stickerAutoSend: v }, '自动配表情') }),
2551
+ h('section', { 'data-bd-sec': '' }, h('div', { 'data-bd-h': '' }, h('b', {}, '评论配图')),
2552
+ ToggleRow({ label: '新评论自动配图', on: Boolean(cfg.stickerAutoSend), disabled, note: '评论权限已开启时,从可用图库选择配图。仅适用于支持图片的新评论。', onChange: (v) => patch({ stickerAutoSend: v }, '自动配图') }),
2009
2553
  input('配图概率', 'stickerSendProbability', 100, '%'), input('配图冷却(秒)', 'stickerSendCooldown', 86400, '秒'),
2010
- h('p', { 'data-bd-note': '' }, '0% 不主动配图,100% 每次符合条件都尝试。指定了表情时优先使用指定项;不会开启评论或私信权限。'),
2554
+ h('p', { 'data-bd-note': '' }, '0% 表示不自动配图,100% 表示每次符合条件时尝试配图。指定表情优先,评论和私信权限独立控制。'),
2011
2555
  h('button', { 'data-bd-btn': '', disabled, onClick: () => save('send') }, '保存配图参数'))),
2012
2556
  h('section', { 'data-bd-sec': '', 'data-bd-sticker-upload-card': '' },
2013
2557
  h('div', { 'data-bd-h': '' }, h('b', {}, '上传本地表情')),
2014
- h('p', { 'data-bd-note': '', style: { margin: 0 } }, '支持 PNG、JPEG、GIF、WebP,单张不超过 5 MB。上传后放进「自己收集」,可以预览、配图或移除。'),
2558
+ h('p', { 'data-bd-note': '', style: { margin: 0 } }, '支持 PNG、JPEG、GIF、WebP,单张不超过 5 MB。上传后归入收集表情;启用人工审核时进入待审队列,批准后可配图。'),
2015
2559
  h('form', { 'data-bd-sticker-upload': '', onSubmit: upload },
2016
2560
  h('div', { 'data-bd-sticker-upload-preview': '', 'aria-label': '待上传图片预览' }, uploadPreview ? h('img', { src: uploadPreview, alt: uploadName || '待上传表情' }) : h('span', {}, '图片预览')),
2017
2561
  h('div', { 'data-bd-sticker-upload-fields': '' },
2018
2562
  h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '选择本地图片'),
2019
2563
  h('input', { ref: fileInput, type: 'file', accept: 'image/png,image/jpeg,image/gif,image/webp,.png,.jpg,.jpeg,.gif,.webp', 'aria-label': '本地表情图片', disabled: Boolean(busy || working), onChange: chooseFile })),
2020
- routeMissing ? h('div', { 'data-bd-note': '', role: 'status' }, '图库接口尚未加载,重启 DSH 后即可上传;下面可以手动重试连接。') : null,
2564
+ routeMissing ? h('div', { 'data-bd-note': '', role: 'status' }, '图库接口尚未加载。请重启 DSH 后重试连接。') : null,
2021
2565
  uploadFile ? h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-sticker-fileinfo': '', 'data-bd-grow': '' }, uploadFile.name + ' · ' + (uploadFile.size < 1024 * 1024 ? Math.max(1, Math.round(uploadFile.size / 1024)) + ' KB' : (uploadFile.size / 1024 / 1024).toFixed(2) + ' MB')),
2022
2566
  h('button', { 'data-bd-btn': '', type: 'button', disabled: Boolean(working), onClick: clearUpload }, '取消选择')) : null,
2023
- h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '表情名称'), h('input', { 'data-bd-input': '', 'aria-label': '上传表情名称', value: uploadName, maxLength: 40, placeholder: '默认用文件名,可以改个好叫的', onChange: (e) => setUploadName(e.target.value) })),
2024
- h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '适合什么场景(可选)'), h('input', { 'data-bd-input': '', 'aria-label': '上传表情描述', value: uploadDescription, maxLength: 160, placeholder: '例:悄悄围观,或者开心得冒泡', onChange: (e) => setUploadDescription(e.target.value) })),
2567
+ h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '表情名称'), h('input', { 'data-bd-input': '', 'aria-label': '上传表情名称', value: uploadName, maxLength: 40, placeholder: '默认使用文件名,可自定义', onChange: (e) => setUploadName(e.target.value) })),
2568
+ h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '使用场景(可选)'), h('input', { 'data-bd-input': '', 'aria-label': '上传表情描述', value: uploadDescription, maxLength: 160, placeholder: '例如:表达好奇、赞同或祝贺', onChange: (e) => setUploadDescription(e.target.value) })),
2025
2569
  uploadError ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, uploadError) : null,
2026
2570
  h('div', { 'data-bd-row': '' }, h('button', { 'data-bd-btn': '', 'data-primary': '', type: 'submit', disabled: disabled || routeMissing || !uploadFile || !uploadName.trim() }, working === 'upload' ? '正在上传…' : working === 'read-file' ? '正在读取…' : '上传到表情库'))))),
2027
2571
  h('section', { 'data-bd-sec': '' },
2028
- h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, h('b', {}, '我的表情库')),
2029
- h('span', { 'data-bd-sticker-count': '' }, data ? '内置 ' + (data.counts?.builtin ?? rows.filter((x) => x.source === 'builtin').length) + ' · 收集 ' + (data.counts?.collected ?? rows.filter((x) => x.source === 'collected').length) + ' / ' + (data.capacity ?? field('stickerCapacity')) : error ? '未连接' : '读取中'),
2572
+ h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, h('b', {}, '表情库')),
2573
+ h('span', { 'data-bd-sticker-count': '' }, data ? '内置 ' + (data.counts?.builtin ?? rows.filter((x) => x.source === 'builtin').length) + ' · 收集 ' + (data.counts?.collected ?? rows.filter((x) => x.source === 'collected').length) + ' / ' + (data.capacity ?? field('stickerCapacity')) + ' · 待审 ' + pendingCount : error ? '未连接' : '读取中'),
2030
2574
  h('button', { 'data-bd-btn': '', disabled, onClick: pull }, '刷新图库')),
2031
- h('div', { 'data-bd-row': '' }, h('input', { 'data-bd-input': '', 'data-bd-grow': '', placeholder: '找名字、描述或标签', 'aria-label': '搜索表情', value: query, onChange: (e) => setQuery(e.target.value) }),
2032
- h('select', { 'data-bd-input': '', 'aria-label': '表情来源', value: source, onChange: (e) => setSource(e.target.value) }, h('option', { value: 'all' }, '全部来源'), h('option', { value: 'builtin' }, '内置表情'), h('option', { value: 'collected' }, '自己收集'))),
2575
+ pendingCount ? h('div', { 'data-bd-sticker-reviewbar': '', role: 'status' }, h('span', {}, pendingCount + ' 张表情待审核。批准前不可用于发送,待审图片占用收集容量。'),
2576
+ h('button', { 'data-bd-btn': '', disabled, onClick: () => { setReview('pending'); setSource('all'); setQuery(''); setSelected('') } }, '查看待审核')) : null,
2577
+ h('div', { 'data-bd-row': '' }, h('input', { 'data-bd-input': '', 'data-bd-grow': '', placeholder: '按名称、描述或标签搜索', 'aria-label': '搜索表情', value: query, onChange: (e) => setQuery(e.target.value) }),
2578
+ h('select', { 'data-bd-input': '', 'aria-label': '表情审核状态', value: review, onChange: (e) => { setReview(e.target.value); setSelected('') } }, h('option', { value: 'approved' }, '可用图库'), h('option', { value: 'pending' }, '待审核(' + pendingCount + ')'), h('option', { value: 'all' }, '全部状态')),
2579
+ h('select', { 'data-bd-input': '', 'aria-label': '表情来源', value: source, onChange: (e) => setSource(e.target.value) }, h('option', { value: 'all' }, '全部来源'), h('option', { value: 'builtin' }, '内置表情'), h('option', { value: 'collected' }, '收集表情'))),
2033
2580
  error ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, '图库读取失败:' + error, h('button', { 'data-bd-btn': '', onClick: pull }, '重试')) : null,
2034
2581
  !data && error ? null : h('div', { 'data-bd-sticker-list': '', role: 'region', 'aria-label': '已有表情', tabIndex: 0 },
2035
- shown.length ? h('div', { 'data-bd-sticker-grid': '' }, shown.map((item) => h('article', { 'data-bd-sticker-item': item.source, key: item.source + ':' + item.name },
2582
+ shown.length ? h('div', { 'data-bd-sticker-grid': '' }, shown.map((item) => h('article', { 'data-bd-sticker-item': item.source, 'data-pending': item.status === 'pending' ? '' : undefined, key: item.source + ':' + item.name },
2036
2583
  h('button', { 'data-bd-sticker-open': '', 'aria-label': '查看表情:' + item.name, onClick: () => setSelected(item.source + ':' + item.name) }, h(PetStickerThumb, { item })),
2037
- h('div', { 'data-bd-sticker-caption': '' }, h('b', { title: item.name }, item.name), h('small', {}, item.source === 'builtin' ? '内置 · 只读' : '自己收集')),
2584
+ h('div', { 'data-bd-sticker-caption': '' }, h('b', { title: item.name }, item.name), h('small', { 'data-bd-sticker-status': item.status === 'pending' ? '' : undefined }, item.source === 'builtin' ? '内置 · 只读' : item.status === 'pending' ? '待审核 · 暂不发送' : '收集表情 · 可用')),
2038
2585
  h('div', { 'data-bd-sticker-itemfoot': '' }, h('span', {}, Array.isArray(item.tags) ? item.tags.slice(0, 2).join(' · ') : ''),
2039
- item.removable && item.source === 'collected' ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled, 'aria-label': '移除表情:' + item.name, onClick: () => forget(item) }, '移除') : null))))
2040
- : h('div', { 'data-bd-sticker-empty': '' }, data ? q || source !== 'all' ? '没有找到匹配的表情。' : '还没有表情。可以手动收一张,或开启自动收集后去逛评论区。' : '正在打开表情库…')),
2586
+ item.removable && item.source === 'collected' && item.status !== 'pending' ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled, 'aria-label': '移除表情:' + item.name, onClick: () => forget(item) }, '移除') : null),
2587
+ reviewActions(item))))
2588
+ : h('div', { 'data-bd-sticker-empty': '' }, data ? q || source !== 'all' ? '无匹配的表情。' : review === 'pending' ? '暂无待审核表情。' : '暂无可用表情。可上传图片、手动收集或启用自动收集。' : '正在读取表情库…')),
2041
2589
  current ? h('div', { 'data-bd-sticker-detail': '' }, h(PetStickerThumb, { item: current, large: true }),
2042
2590
  h('div', {}, h('div', { 'data-bd-row': '' }, h('b', { 'data-bd-grow': '' }, current.name), h('button', { 'data-bd-btn': '', onClick: () => setSelected('') }, '收起预览')),
2043
- h('p', {}, current.description || '还没有描述'), h('p', {}, '来源:' + (current.from || (current.source === 'builtin' ? '内置表情库' : '手动收集'))),
2044
- current.addedAt ? h('p', {}, '收于 ' + petFmtTime(current.addedAt)) : null,
2045
- Array.isArray(current.tags) && current.tags.length ? h('p', {}, current.tags.join(' · ')) : null)) : null,
2591
+ h('p', {}, current.description || '暂无描述'), h('p', {}, '来源:' + (current.from || (current.source === 'builtin' ? '内置表情库' : '手动收集'))),
2592
+ current.addedAt ? h('p', {}, '收集时间:' + petFmtTime(current.addedAt)) : null,
2593
+ Array.isArray(current.tags) && current.tags.length ? h('p', {}, current.tags.join(' · ')) : null,
2594
+ current.status === 'pending' ? h('p', { 'data-bd-sticker-status': '' }, '待审核,批准后加入可用图库。') : null,
2595
+ reviewActions(current))) : null,
2046
2596
  h('div', { 'data-bd-note': '' }, data?.automation?.collectPending ? '正在收取 ' + data.automation.collectPending + ' 张,稍后刷新可查看。' : '自动收集保留来源并按图片内容去重。内置区只读,移除只影响自己收集的表情。')),
2047
- h('details', { 'data-bd-sec': '', 'data-bd-advanced': '' }, h('summary', {}, '手动收一张'),
2597
+ h('details', { 'data-bd-sec': '', 'data-bd-advanced': '' }, h('summary', {}, '手动收集'),
2048
2598
  h('form', { 'data-bd-sticker-form': '', onSubmit: collect },
2049
- h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '给它起个名字'), h('input', { 'data-bd-input': '', 'aria-label': '表情名称', value: name, maxLength: 40, placeholder: '例:悄悄探头', onChange: (e) => setName(e.target.value) })),
2599
+ h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '表情名称'), h('input', { 'data-bd-input': '', 'aria-label': '表情名称', value: name, maxLength: 40, placeholder: '例如:好奇', onChange: (e) => setName(e.target.value) })),
2050
2600
  h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '图片链接'), h('input', { 'data-bd-input': '', 'aria-label': '图片链接', type: 'url', value: url, placeholder: 'https://…', onChange: (e) => setUrl(e.target.value) })),
2051
- h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '适合什么场景'), h('input', { 'data-bd-input': '', 'aria-label': '表情描述', value: description, maxLength: 160, placeholder: '例:发现有趣的小细节时', onChange: (e) => setDescription(e.target.value) })),
2052
- h('button', { 'data-bd-btn': '', 'data-primary': '', type: 'submit', disabled }, working === 'collect' ? '正在收…' : '收进表情库'))))
2601
+ h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '使用场景'), h('input', { 'data-bd-input': '', 'aria-label': '表情描述', value: description, maxLength: 160, placeholder: '例如:表达对细节的兴趣', onChange: (e) => setDescription(e.target.value) })),
2602
+ h('button', { 'data-bd-btn': '', 'data-primary': '', type: 'submit', disabled }, working === 'collect' ? '正在收集…' : '加入表情库'))))
2053
2603
  }
2054
2604
 
2055
- function PetSettingsModal({ onClose, toast, ctx, status, initialTab = 'basic' }) {
2605
+ function PetSettingsModal({ onClose, toast, ctx, status, initialTab = 'run' }) {
2056
2606
  const [tab, setTab] = React.useState(initialTab)
2607
+ const activeGroup = ({ stickers: 'learning', ledger: 'logs', review: 'logs', other: 'basic' })[tab] || tab
2608
+ React.useEffect(() => {
2609
+ const body = modalRef.current?.querySelector('[data-bd-pet-body]')
2610
+ if (body) body.scrollTop = 0
2611
+ }, [tab])
2057
2612
  const [runMode, setRunMode] = React.useState('session')
2058
2613
  const [logMode, setLogMode] = React.useState('events')
2059
2614
  const [runDrafts, setRunDrafts] = React.useState({})
@@ -2076,6 +2631,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2076
2631
  const [dmMode, setDmMode] = React.useState(null)
2077
2632
  const [persona, setPersona] = React.useState(null)
2078
2633
  const [momentStyle, setMomentStyle] = React.useState(null)
2634
+ const [interactionStyle, setInteractionStyle] = React.useState(null)
2079
2635
  const [momentPerDay, setMomentPerDay] = React.useState(null)
2080
2636
  const [modelId, setModelId] = React.useState(null)
2081
2637
  const [tokenBudget, setTokenBudget] = React.useState(null)
@@ -2093,12 +2649,24 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2093
2649
  const [loginState, setLoginState] = React.useState('')
2094
2650
  const [loginTone, setLoginTone] = React.useState('')
2095
2651
  const loginTimer = React.useRef(null)
2652
+ const loginGeneration = React.useRef(0)
2653
+ const loginPolling = React.useRef(false)
2096
2654
  const alive = React.useRef(true)
2097
2655
  /* 🎬 视频那一卡的独立轮询:跑起来 3 秒、闲着 15 秒 */
2098
2656
  const [pv, setPv] = React.useState(null)
2099
- const [pvDown, setPvDown] = React.useState(false)
2657
+ const [pvDown, setPvDown] = React.useState('')
2100
2658
  const [pvBusy, setPvBusy] = React.useState('')
2101
2659
  const [pvDay, setPvDay] = React.useState('')
2660
+ const [pvMode, setPvMode] = React.useState('daily')
2661
+ const [pvBrief, setPvBrief] = React.useState('')
2662
+ const [pvError, setPvError] = React.useState('')
2663
+ const [pvSelection, setPvSelection] = React.useState('')
2664
+ const [pvTargetDurationSec, setPvTargetDurationSec] = React.useState(60)
2665
+ const [videoModelId, setVideoModelId] = React.useState(null)
2666
+ const [pvFailOpen, setPvFailOpen] = React.useState(false)
2667
+ const [pvPollEpoch, setPvPollEpoch] = React.useState(0)
2668
+ const pvRouteMissing = React.useRef(false)
2669
+ const pvCreateLock = React.useRef(false)
2102
2670
  const [settingLogs, setSettingLogs] = React.useState(null)
2103
2671
  const [settingLogError, setSettingLogError] = React.useState('')
2104
2672
  const [settingLogFilter, setSettingLogFilter] = React.useState('')
@@ -2109,34 +2677,53 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2109
2677
  const [actLive, setActLive] = React.useState(null)
2110
2678
  const pvRunning = Boolean(pv && pv.running)
2111
2679
  React.useEffect(() => {
2112
- if (tab !== 'video') return
2680
+ if (tab !== 'video' || pvRouteMissing.current) return
2113
2681
  let dead = false
2114
2682
  let timer = null
2683
+ let failures = 0
2115
2684
  const pull = async () => {
2685
+ let running = pvRunning
2116
2686
  try {
2117
2687
  const r = await jget('/pv')
2118
- if (!dead && r) {
2119
- if (r.ok === false) setPvDown(true)
2120
- else { setPvDown(false); setPv(r) }
2121
- }
2122
- } catch { /* 抖一下就算了,下次接着拉 */ }
2123
- if (dead) return
2124
- timer = setTimeout(pull, pvRunning ? 3000 : 15000)
2688
+ if (dead) return
2689
+ if (!r || r.ok === false) throw new Error(r?.error || '视频服务没有返回有效状态')
2690
+ failures = 0
2691
+ running = Boolean(r.running)
2692
+ setPvDown('')
2693
+ setPv(r)
2694
+ setVideoModelId((old) => old === null ? String(r.modelId || '') : old)
2695
+ setPvSelection((old) => old || pvGalleryRows(r)[0]?.key || '')
2696
+ } catch (e) {
2697
+ if (dead) return
2698
+ failures++
2699
+ if (Number(e?.status) === 404) {
2700
+ pvRouteMissing.current = true
2701
+ setPvDown('视频接口尚未加载。请完全重启 DSH 后点「重新读取」;已暂停自动重试。')
2702
+ } else setPvDown(String(e?.message ?? e))
2703
+ }
2704
+ if (dead || pvRouteMissing.current) return
2705
+ timer = setTimeout(pull, failures ? Math.min(60000, 5000 * 2 ** (failures - 1)) : running ? 3000 : 15000)
2125
2706
  }
2126
2707
  pull()
2127
2708
  return () => { dead = true; if (timer) clearTimeout(timer) }
2128
- }, [tab, pvRunning])
2709
+ }, [tab, pvPollEpoch])
2129
2710
  /* ⭐ 活动流水轮询:只拉增量(sinceUid = 已收到的最后一条 uid),报文一直很小。
2130
2711
  同一次工具调用先 call 后 done(共用一个 id)⇒ 原地改写那一行,不是再追加一条。 */
2131
2712
  const activityCursor = React.useRef('')
2713
+ const activityEpoch = React.useRef(0)
2714
+ const activityRequest = React.useRef(null)
2132
2715
  React.useEffect(() => {
2133
2716
  if (tab !== 'logs') return
2134
2717
  let dead = false
2135
2718
  let sinceUid = activityCursor.current
2136
2719
  const pull = async () => {
2720
+ if (dead || activityRequest.current) return
2721
+ if (sinceUid !== activityCursor.current) sinceUid = activityCursor.current
2722
+ const token = { epoch: activityEpoch.current }
2723
+ activityRequest.current = token
2137
2724
  try {
2138
2725
  const r = await jget('/activity?sinceUid=' + encodeURIComponent(sinceUid))
2139
- if (dead || !r) return
2726
+ if (dead || activityRequest.current !== token || token.epoch !== activityEpoch.current || !r) return
2140
2727
  // 「此刻在不在想」不跟增量走,是当场的状态 —— 每趟都要更新
2141
2728
  if (r.live) setActLive(r.live)
2142
2729
  if (Array.isArray(r.rows) && r.rows.length) {
@@ -2157,14 +2744,19 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2157
2744
  })
2158
2745
  }
2159
2746
  } catch { /* 抖一下就算了,下次接着拉 */ }
2747
+ finally { if (activityRequest.current === token) activityRequest.current = null }
2160
2748
  }
2161
2749
  pull()
2162
2750
  const t = setInterval(pull, 2500)
2163
- return () => { dead = true; clearInterval(t) }
2751
+ return () => { dead = true; activityEpoch.current += 1; activityRequest.current = null; clearInterval(t) }
2164
2752
  }, [tab])
2165
2753
 
2166
2754
  const onClearAct = async () => {
2167
- try { await jpost('/activity', { clear: true }); setActs([]); activityCursor.current = '' } catch (e) { say(String(e?.message ?? e), 'warn') }
2755
+ try {
2756
+ await jpost('/activity', { clear: true })
2757
+ activityEpoch.current += 1; activityCursor.current = ''
2758
+ setActs([]); setActLive(null)
2759
+ } catch (e) { say(String(e?.message ?? e), 'warn') }
2168
2760
  }
2169
2761
 
2170
2762
  const logRev = React.useRef('')
@@ -2190,6 +2782,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2190
2782
  /* ⚠️ 扫码轮询的 timer 必须能停(切走/关弹窗后还跑会在已卸载组件上 setState) */
2191
2783
  React.useEffect(() => () => {
2192
2784
  alive.current = false
2785
+ loginGeneration.current += 1
2786
+ loginPolling.current = false
2193
2787
  if (loginTimer.current) { clearInterval(loginTimer.current); loginTimer.current = null }
2194
2788
  }, [])
2195
2789
 
@@ -2197,7 +2791,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2197
2791
  React.useEffect(() => {
2198
2792
  const previous = document.activeElement
2199
2793
  const node = modalRef.current
2200
- if (node?.querySelector) node.querySelector('[data-bd-pet-tab][data-on]')?.focus()
2794
+ if (node?.querySelector) (node.querySelector('[data-bd-pet-tab][data-on]') || node.querySelector('[data-bd-pet-close]'))?.focus()
2201
2795
  return () => { if (previous?.isConnected && previous.focus) previous.focus() }
2202
2796
  }, [Boolean(cfg)])
2203
2797
  const lim = (st && st.limits) || {}
@@ -2219,7 +2813,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2219
2813
  if (dmUid === null) setDmUid(cfg.dmUid ? String(cfg.dmUid) : '')
2220
2814
  if (dmMode === null) setDmMode(cfg.dmMode || 'off')
2221
2815
  if (persona === null) setPersona(cfg.persona || '')
2222
- if (momentStyle === null) setMomentStyle(cfg.momentStyle || '感想')
2816
+ if (momentStyle === null) setMomentStyle(cfg.momentStyle || '随心情')
2817
+ if (interactionStyle === null) setInteractionStyle(cfg.interactionStyle || 'companion')
2223
2818
  if (momentPerDay === null) setMomentPerDay(String(cfg.momentPerDay ?? 1))
2224
2819
  if (modelId === null) setModelId(cfg.modelId || '')
2225
2820
  if (tokenBudget === null) setTokenBudget(String(cfg.tokenBudget ?? 0))
@@ -2264,23 +2859,29 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2264
2859
  const sw = (k) => Boolean(cfg && cfg[k])
2265
2860
 
2266
2861
  const onLoginStart = async () => {
2862
+ const generation = ++loginGeneration.current
2863
+ loginPolling.current = false
2864
+ if (loginTimer.current) { clearInterval(loginTimer.current); loginTimer.current = null }
2267
2865
  setBusy('login')
2268
2866
  setLoginTone('')
2269
2867
  setLoginState('正在出码…')
2270
2868
  try {
2271
2869
  const r = await jpost('/login/start', {})
2870
+ if (!alive.current || generation !== loginGeneration.current) return
2272
2871
  if (!r?.ok) { setLoginTone('warn'); setLoginState(r?.error || '出码失败'); return }
2273
2872
  setLogin({ key: r.key, qr: r.qr })
2274
2873
  if (!r.qr) { setLoginTone('warn'); setLoginState('没画出二维码 —— 先点上面的「装主依赖」(venv 里要有 qrcode)') }
2275
2874
  else setLoginState('用手机 B站 扫这张码')
2276
- if (loginTimer.current) { clearInterval(loginTimer.current); loginTimer.current = null }
2277
2875
  let n = 0
2278
- const stop = () => { clearInterval(timer); if (loginTimer.current === timer) loginTimer.current = null }
2876
+ const stop = () => { clearInterval(timer); if (loginTimer.current === timer) loginTimer.current = null; loginPolling.current = false }
2279
2877
  const timer = setInterval(async () => {
2280
- if (!alive.current) { stop(); return }
2878
+ if (!alive.current || generation !== loginGeneration.current || loginTimer.current !== timer) { stop(); return }
2879
+ if (loginPolling.current) return
2880
+ loginPolling.current = true
2281
2881
  n += 1
2282
2882
  try {
2283
2883
  const p = await jpost('/login/poll', { key: r.key })
2884
+ if (!alive.current || generation !== loginGeneration.current || loginTimer.current !== timer) return
2284
2885
  if (p?.state === 'ok') {
2285
2886
  stop(); setLogin(null); setLoginTone('good'); setLoginState('登录成功')
2286
2887
  setSt((old) => ({ ...old, loggedIn: true }))
@@ -2291,17 +2892,19 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2291
2892
  setLoginState('没登上:' + (p?.message || '响应里没有 SESSDATA —— 再点一次「扫码登录」'))
2292
2893
  } else { setLoginTone(''); setLoginState('等扫码…') }
2293
2894
  } catch { /* 抖动就下次再问 */ }
2895
+ finally { if (generation === loginGeneration.current && loginTimer.current === timer) loginPolling.current = false }
2294
2896
  if (n > 90) { stop(); setLogin(null); setLoginTone('warn'); setLoginState('超时了,再点一次「扫码登录」') }
2295
2897
  }, 2000)
2296
2898
  loginTimer.current = timer
2297
- } catch (e) { setLoginTone('warn'); setLoginState(String(e?.message ?? e)) }
2298
- finally { setBusy('') }
2899
+ } catch (e) { if (alive.current && generation === loginGeneration.current) { setLoginTone('warn'); setLoginState(String(e?.message ?? e)) } }
2900
+ finally { if (generation === loginGeneration.current) setBusy('') }
2299
2901
  }
2300
2902
  const onInstall = async () => {
2301
2903
  setBusy('install'); say('')
2302
2904
  try {
2303
2905
  const r = await jpost('/install', {})
2304
- say(r?.hint || '已经在后台开始装了', '')
2906
+ if (!r || r.ok === false) throw new Error(r?.error || '依赖安装请求失败')
2907
+ say(r.hint || '依赖安装请求已接受。', 'good')
2305
2908
  } catch (e) { say(String(e?.message ?? e), 'warn') }
2306
2909
  finally { setBusy('') }
2307
2910
  }
@@ -2309,217 +2912,297 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2309
2912
  setBusy('check'); say('')
2310
2913
  try {
2311
2914
  const r = await jpost('/check', {})
2312
- if (r && r.ok) say('能用!' + (r.steps || []).join('、'))
2313
- else say('还不能用:' + (r && r.error ? r.error : '没拿到结果'))
2915
+ if (!r?.ok) throw new Error(r?.error || '连接检查未返回有效结果')
2916
+ say('连接检查通过。' + (r.steps || []).join('、'), 'good')
2314
2917
  } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
2315
2918
  }
2316
2919
  const onInstallAsr = async () => {
2317
2920
  setBusy('asr'); say('')
2318
2921
  try {
2319
2922
  const r = await jpost('/install-asr', {})
2320
- say(r?.started ? '开始装转写依赖 —— 进度显示在下面(要下模型,慢一点正常)' : (r?.hint || '没起来'),
2923
+ if (!r || r.ok === false) throw new Error(r?.error || '转写依赖安装请求失败')
2924
+ say(r.started ? '转写依赖安装已开始,进度显示在环境日志中。' : (r.hint || '转写依赖未启动安装。'),
2321
2925
  r?.started ? 'good' : 'warn')
2322
2926
  } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
2323
2927
  }
2324
- /* 🎬 视频动作:先拦日期格式,再 POST /pv */
2325
- const onPvMake = async (force) => {
2326
- const day = (pvDay || (pv && pv.yesterday) || '').trim()
2327
- if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) { say('先选一天(YYYY-MM-DD)', 'warn'); return }
2928
+ const openPvSession = async (sessionId) => {
2929
+ if (!alive.current) return false
2930
+ try {
2931
+ const workspace = ctx && typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null
2932
+ if (!workspace || typeof workspace.openSession !== 'function') throw new Error('当前 DSH 没有可用的会话工作区')
2933
+ await workspace.openSession(sessionId)
2934
+ return alive.current
2935
+ } catch (e) {
2936
+ if (!alive.current) return false
2937
+ setPvError('制作会话已创建(' + sessionId + '),但无法自动打开:' + String(e?.message ?? e))
2938
+ return false
2939
+ }
2940
+ }
2941
+ const onPvMake = async () => {
2942
+ if (pvCreateLock.current) return
2943
+ const date = (pvDay || (pv && pv.yesterday) || '').trim()
2944
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { setPvError('请选择有效日期(YYYY-MM-DD)'); return }
2945
+ pvCreateLock.current = true
2328
2946
  setPvBusy('make')
2947
+ setPvError('')
2329
2948
  try {
2330
- const r = await jpost('/pv', { action: 'make', date: day, force: force === true })
2331
- if (!r || r.ok === false) { say('没做成:' + ((r && r.error) || '未知原因'), 'warn'); return }
2332
- if (r.skipped) { say(`这天已经有视频了:${r.file}(要重做就再点一次「重做」)`, 'warn'); return }
2333
- say(`开始做 ${r.date} 的视频了 —— 一条约 2–6 分钟,做完这张卡会自己变成播放器。`, 'good')
2334
- setPv((old) => ({ ...(old || {}), running: true, stage: 'plan', date: r.date }))
2335
- } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setPvBusy('') }
2336
- }
2337
- const onPvStop = async () => {
2949
+ const r = await jpost('/pv/session-new', {
2950
+ ...newSessionContext(ctx), date, mode: pvMode, brief: pvBrief.trim(),
2951
+ targetDurationSec: Number(pvTargetDurationSec), model: String(videoModelId ?? pv?.modelId ?? cfg?.videoModelId ?? '').trim(),
2952
+ })
2953
+ if (!alive.current) return
2954
+ if (!r?.ok || !r.sessionId) {
2955
+ const error = String(r?.error || '制作会话创建失败')
2956
+ if (r?.sessionId) {
2957
+ setPvError(error + '。正在打开已部分创建的会话。')
2958
+ toast(error, 'warn')
2959
+ if (await openPvSession(r.sessionId)) onClose()
2960
+ } else setPvError(error)
2961
+ return
2962
+ }
2963
+ const createdSession = { date, mode: pvMode, brief: pvBrief.trim(), createdAt: new Date().toISOString(), stage: 'authoring', ...r }
2964
+ setPv((old) => ({ ...(old || {}), sessions: [createdSession, ...(old?.sessions || []).filter((item) => item.sessionId !== r.sessionId)] }))
2965
+ if (await openPvSession(r.sessionId)) onClose()
2966
+ } catch (e) {
2967
+ if (!alive.current) return
2968
+ const error = String(e?.message ?? e)
2969
+ if (e?.sessionId) {
2970
+ setPvError(error + '。正在打开已部分创建的会话。')
2971
+ toast(error, 'warn')
2972
+ if (await openPvSession(e.sessionId)) onClose()
2973
+ } else setPvError(error)
2974
+ } finally { pvCreateLock.current = false; if (alive.current) setPvBusy('') }
2975
+ }
2976
+ const onPvStop = async (sessionId) => {
2977
+ if (!sessionId || pvCreateLock.current) return
2338
2978
  setPvBusy('stop')
2979
+ setPvError('')
2339
2980
  try {
2340
- const r = await jpost('/pv', { action: 'stop' })
2341
- say(r && r.stopped ? '停下了。' : '它本来就没在做。', r && r.stopped ? 'warn' : '')
2342
- } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setPvBusy('') }
2981
+ const r = await jpost('/pv', { action: 'stop', sessionId })
2982
+ if (!alive.current) return
2983
+ if (!r?.ok) throw new Error(r?.error || '没能停止制作')
2984
+ if (r.stopped) setPv((old) => ({ ...(old || {}), sessions: (old?.sessions || []).map((item) => item.sessionId === sessionId ? { ...item, stage: 'stopped', error: '' } : item) }))
2985
+ say(r.stopped ? '已停止这条制作会话。' : (r.hint || '这条会话当前没有运行中的制作。'), r.stopped ? 'warn' : '')
2986
+ } catch (e) { if (alive.current) setPvError(String(e?.message ?? e)) } finally { if (alive.current) setPvBusy('') }
2343
2987
  }
2344
2988
  const onPvRefresh = async () => {
2345
- try { const r = await jget('/pv'); if (r) setPv(r) } catch { /* 下次轮询会补 */ }
2989
+ try {
2990
+ const result = await jget('/pv')
2991
+ if (!alive.current) return
2992
+ if (!result || result.ok === false) throw new Error(result?.error || '视频服务没有返回有效状态')
2993
+ setPv(result)
2994
+ setPvDown('')
2995
+ setVideoModelId((old) => old === null ? String(result.modelId || '') : old)
2996
+ setPvSelection((old) => old || pvGalleryRows(result)[0]?.key || '')
2997
+ if (pvRouteMissing.current) {
2998
+ pvRouteMissing.current = false
2999
+ setPvPollEpoch((old) => old + 1)
3000
+ }
3001
+ } catch (e) {
3002
+ if (!alive.current) return
3003
+ if (Number(e?.status) === 404) {
3004
+ pvRouteMissing.current = true
3005
+ setPvDown('视频接口尚未加载。请完全重启 DSH 后点「重新读取」;已暂停自动重试。')
3006
+ } else setPvDown(String(e?.message ?? e))
3007
+ }
3008
+ }
3009
+ const onPvOpenFolder = async () => {
3010
+ try {
3011
+ const r = await jpost('/open', { what: 'pv' })
3012
+ if (!alive.current) return
3013
+ say(r?.ok === false ? (r.error || '打开成片目录失败') : '已请求系统打开成片目录。', r?.ok === false ? 'warn' : 'good')
3014
+ } catch (e) { if (alive.current) say(String(e?.message ?? e), 'warn') }
2346
3015
  }
2347
- const onPvRedo = () => onPvMake(true)
2348
3016
  const copyText = async (text) => {
2349
3017
  const v = String(text || '')
2350
3018
  try { await navigator.clipboard.writeText(v); toast('已复制:' + v, 'good') }
2351
3019
  catch { toast('复制失败 —— 路径是:' + v) }
2352
3020
  }
2353
3021
 
2354
- const tabDefs = [['basic', '偏好与桌宠', '口吻 / 选片 / 报告'], ['run', '运行方式', '会话 / 后台 / 定时'], ['speech', '互动与投递', '发言 / 私信 / 节奏'],
2355
- ['stickers', '表情收藏', '图库 / 收集 / 配图'], ['learning', '学习与记忆', '自动审核 / 记录'], ['ledger', 'token 账本', '用量 / 预算'], ['logs', '运行日志', '实时动态 / 事件'], ['video', '日报视频', '制作 / 配音'], ['other', '账号与环境', '登录 / 依赖 / 镜像']]
3022
+ const tabDefs = [['run', '运行方式', '会话 / 后台 / 定时'], ['speech', '互动与投递', '权限 / 发送审核'],
3023
+ ['learning', '记忆与素材', '学习记忆 / 表情收藏'], ['video', '视频工作室', '制作 / 成片'],
3024
+ ['logs', '记录与用量', '日志 / 用量 / 回顾'], ['basic', '偏好与设置', '桌宠 / 账号 / 环境']]
3025
+ const subTabs = {
3026
+ learning: [['learning', '学习与记忆'], ['stickers', '表情收藏']],
3027
+ logs: [['logs', '运行日志'], ['ledger', 'token 账本'], ['review', '数据回顾']],
3028
+ basic: [['basic', '偏好与桌宠'], ['other', '账号与环境']],
3029
+ }
2356
3030
  const modalShell = (body) => h('div', { 'data-bd-pet-mask': '', onPointerDown: (e) => { if (e.target === e.currentTarget) onClose() } },
2357
3031
  h('div', { 'data-bd-pet-modal': '', role: 'dialog', 'aria-modal': true, 'aria-label': '鲸鱼摸鱼设置', ref: modalRef,
2358
3032
  onKeyDown: (e) => {
2359
3033
  if (e.key === 'Escape') { e.stopPropagation(); onClose() }
2360
3034
  if (e.key === 'Tab' && e.currentTarget.querySelectorAll) {
2361
- const nodes = Array.from(e.currentTarget.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),a[href],summary,[tabindex="0"]')).filter((el) => el.getClientRects().length)
3035
+ const nodes = petTabStops(e.currentTarget)
2362
3036
  const first = nodes[0], last = nodes[nodes.length - 1]
2363
3037
  if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus() }
2364
3038
  else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus() }
2365
3039
  }
2366
3040
  } },
2367
3041
  h('header', { 'data-bd-settings-head': '' }, h(SettingsAvatar),
2368
- h('div', { 'data-bd-settings-title': '' }, h('b', {}, '鲸鱼摸鱼 · 设置'), h('p', {}, '慢慢逛,调成你喜欢的样子。')),
3042
+ h('div', { 'data-bd-settings-title': '' }, h('b', {}, 'DeepMoYu'), h('p', {}, '鲸鱼摸鱼工作台')),
2369
3043
  h('button', { 'data-bd-pet-close': '', onClick: onClose, title: '关闭(ESC)', 'aria-label': '关闭设置' }, '×')),
2370
3044
  h('div', { 'data-bd-settings-layout': '' },
2371
3045
  h('nav', { 'data-bd-pet-tabs': '', role: 'tablist', 'aria-orientation': 'vertical', 'aria-label': '设置分类', onKeyDown: (e) => {
2372
3046
  if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) return
2373
3047
  e.preventDefault()
2374
- const current = tabDefs.findIndex(([key]) => key === tab)
3048
+ const current = tabDefs.findIndex(([key]) => key === activeGroup)
2375
3049
  const forward = e.key === 'ArrowRight' || e.key === 'ArrowDown'
2376
3050
  const next = e.key === 'Home' ? 0 : e.key === 'End' ? tabDefs.length - 1 : (current + (forward ? 1 : -1) + tabDefs.length) % tabDefs.length
2377
3051
  setTab(tabDefs[next][0]); e.currentTarget.querySelectorAll('[role="tab"]')[next]?.focus()
2378
3052
  } }, ...tabDefs.flatMap(([key, label, note], i) => [
2379
- i === 0 || i === 4 || i === 7 ? h('div', { key: 'group-' + key, 'data-bd-nav-group': '', role: 'presentation' }, i === 0 ? '日常使用' : i === 4 ? '记录与成长' : '制作与维护') : null,
2380
- h('button', { key, id: 'bd-tab-' + key, 'data-bd-pet-tab': '', role: 'tab', 'aria-selected': tab === key, 'aria-controls': 'bd-settings-content', tabIndex: tab === key ? 0 : -1,
2381
- 'data-on': tab === key ? '' : undefined, onClick: () => setTab(key) }, label, h('small', {}, note)),
2382
- ]), h('div', { 'data-bd-nav-foot': '' }, 'DAFEIYU', h('br'), '小小控制室 / LOCAL')),
3053
+ i === 0 || i === 2 || i === 4 ? h('div', { key: 'group-' + key, 'data-bd-nav-group': '', role: 'presentation' }, i === 0 ? '运行与互动' : i === 2 ? '内容管理' : '记录与配置') : null,
3054
+ h('button', { key, id: 'bd-tab-' + key, 'data-bd-pet-tab': '', role: 'tab', 'aria-selected': activeGroup === key, 'aria-controls': 'bd-settings-content', tabIndex: activeGroup === key ? 0 : -1,
3055
+ 'data-on': activeGroup === key ? '' : undefined, onClick: () => setTab(key) }, label, h('small', {}, note)),
3056
+ ]), h('div', { 'data-bd-nav-foot': '' }, 'DEEP MOYU', h('br'), 'LOCAL WORKSPACE')),
2383
3057
  h('main', { 'data-bd-settings-main': '' },
2384
3058
  msg ? h('div', { style: { margin: '14px 26px 0' }, role: msgTone === 'warn' ? 'alert' : 'status' },
2385
3059
  h('div', { 'data-bd-msg': '', 'data-warn': msgTone === 'warn' ? '' : undefined, 'data-good': msgTone === 'good' ? '' : undefined }, msg)) : null,
2386
- h('div', { 'data-bd-pet-body': '', id: 'bd-settings-content', role: 'tabpanel', 'aria-labelledby': 'bd-tab-' + tab }, body)))))
3060
+ h('div', { 'data-bd-pet-body': '', id: 'bd-settings-content', role: 'tabpanel', 'aria-labelledby': 'bd-tab-' + activeGroup },
3061
+ subTabs[activeGroup] ? h('div', { 'data-bd-workbench-subnav': '', 'data-bd-segments': '', role: 'group', 'aria-label': tabDefs.find(([key]) => key === activeGroup)?.[1] + '分类' },
3062
+ ...subTabs[activeGroup].map(([key, label]) => h('button', { key, type: 'button', 'data-bd-page-target': key, 'aria-pressed': tab === key, onClick: () => setTab(key) }, label))) : null,
3063
+ body)))))
2387
3064
 
2388
3065
  if (!cfg) {
2389
3066
  return h('div', { 'data-bd-pet-mask': '' },
2390
- h('div', { 'data-bd-pet-modal': '' },
3067
+ h('div', { 'data-bd-pet-modal': '', role: 'dialog', 'aria-modal': true, 'aria-label': '鲸鱼摸鱼设置', ref: modalRef,
3068
+ onKeyDown: (e) => {
3069
+ if (e.key === 'Escape') { e.stopPropagation(); onClose() }
3070
+ if (e.key === 'Tab') { e.preventDefault(); e.currentTarget.querySelector('[data-bd-pet-close]')?.focus() }
3071
+ } },
2391
3072
  h('div', { 'data-bd-pet-tabs': '' },
2392
3073
  h('span', { 'data-bd-pet-tab': '', style: { cursor: 'default' } }, '鲸鱼摸鱼设置'),
2393
- h('button', { 'data-bd-pet-close': '', onClick: onClose }, '✕')),
3074
+ h('button', { 'data-bd-pet-close': '', 'aria-label': '关闭设置', onClick: onClose }, '✕')),
2394
3075
  h('div', { 'data-bd-pet-body': '' },
2395
- h('div', { 'data-bd-note': '' }, fails >= 3
2396
- ? '读不到插件配置(已经试了 ' + fails + ' 次)—— 完全退出 DSH Desktop 再打开试试。'
2397
- : '读设置中…'))))
3076
+ h('div', { 'data-bd-note': '', role: fails >= 3 ? 'alert' : 'status' }, fails >= 3
3077
+ ? '插件配置读取失败(重试 ' + fails + ' 次)。请检查 DSH 运行状态。'
3078
+ : '正在读取配置…'))))
2398
3079
  }
2399
3080
 
2400
3081
  /* 每项只在一页出现;旧 Panel 仅供历史回归工具使用,不挂入弹窗。 */
2401
3082
  const icon = (name) => h(PetIcon, { name })
2402
3083
  const heading = (_name, title) => h('div', { 'data-bd-h': '' }, h('b', {}, title))
2403
3084
  const pvCard = () => {
2404
- if (pvDown) return h('section', { 'data-bd-sec': '' }, heading('film', '视频暂时不可用'),
2405
- h('div', { 'data-bd-msg': '', 'data-warn': '' }, '读不到视频服务,请稍后重试。'),
2406
- h('button', { 'data-bd-btn': '', onClick: onPvRefresh }, '重新读取'))
2407
- const videos = Array.isArray(pv && pv.videos) ? pv.videos : []
2408
- const latest = videos.length ? videos[0] : null
2409
- const shown = (pv && pv.file && pv.stage === 'done') ? pv.file : (latest ? latest.file : '')
2410
- const shownDay = (pv && pv.stage === 'done' && pv.date) ? pv.date : (latest ? latest.date : '')
2411
- const deps = pv ? pv.deps : null
2412
- const stageTxt = pv && pv.running
2413
- ? (pv.stage === 'plan' ? '正在整理分镜…' : '正在渲染画面…')
2414
- : ''
2415
- return h('div', { 'data-bd-sec': '' },
2416
- h('div', { 'data-bd-h': '' }, icon('film'), h('b', {}, '冲浪日报视频'),
2417
- pv && pv.running ? h('span', { 'data-bd-badge': '', 'data-live': '' }, stageTxt) : null,
2418
- deps === false ? h('span', { 'data-bd-badge': '', 'data-warn': '' }, '依赖没装') : null,
2419
- videos.length ? h('span', { 'data-bd-badge': '' }, `${videos.length} 条成品`) : null),
2420
- h('div', { 'data-bd-note': '' },
2421
- '把某一天的冲浪情况做成一条由鲸鱼摸鱼讲解配音的短片,存进当天文件夹。做在本地,不发布。'),
2422
- deps === false ? h('div', { 'data-bd-msg': '', 'data-warn': '' },
2423
- '渲染要的三样东西没装齐(edge-tts / imageio-ffmpeg / Pillow)—— 到「账号与环境」点一次「装主依赖」即可。') : null,
2424
- h('div', { 'data-bd-row': '' },
2425
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '做哪一天'),
2426
- h('input', {
2427
- 'data-bd-input': '', style: { width: '150px' }, type: 'date',
2428
- value: pvDay || (pv && pv.yesterday) || '', onChange: (e) => setPvDay(e.target.value),
2429
- })),
2430
- h('div', { 'data-bd-row': '' },
2431
- h('button', {
2432
- 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(pvBusy) || pvRunning || deps === false,
2433
- onClick: () => onPvMake(false),
2434
- }, pvRunning ? '正在做…' : '做这一天的视频'),
2435
- pvRunning ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(pvBusy), onClick: onPvStop }, '停下') : null,
2436
- shown ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(pvBusy) || pvRunning, onClick: onPvRedo }, '重做这天') : null,
2437
- h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(pvBusy), onClick: onPvRefresh }, '刷新')),
2438
- pv && pv.running ? h('div', { 'data-bd-msg': '' },
2439
- `${pv.date}:${stageTxt}已 ${Math.round((Number(pv.ms) || 0) / 1000)} 秒。这一步要占几分钟 CPU,可以先干别的,做完这里会自己出画面。`) : null,
2440
- pv && !pv.running && pv.stage === 'failed' ? h('div', { 'data-bd-msg': '', 'data-warn': '' },
2441
- `上一次没做成:${pv.error || '未知原因'}` + (pv.log ? `(日志:${pv.log})` : '')) : null,
2442
- pv && !pv.running && pv.stage === 'stopped' ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '上一次是手动停下的。') : null,
2443
- shown
2444
- ? h('div', { 'data-bd-field': '' },
2445
- h('video', {
2446
- src: `/api/bili-duty/pv/video?day=${encodeURIComponent(shownDay)}&t=${Number(latest && latest.at ? Date.parse(latest.at) : 0) || 0}`,
2447
- controls: true, preload: 'metadata',
2448
- style: { width: '100%', borderRadius: '10px', background: '#000', display: 'block' },
2449
- }),
2450
- h('div', { 'data-bd-row': '' },
2451
- h('span', { 'data-bd-note': '', 'data-bd-grow': '' },
2452
- `${shownDay} · ${latest && latest.mb != null ? `${latest.mb} MB` : ''} · ${shown}`),
2453
- h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => copyText(shown) }, '复制路径')))
2454
- : h('div', { 'data-bd-empty': '' },
2455
- pvRunning ? '第一条正在做,做完会出现在这里' : '还没有成品 —— 选个日期点「做这一天的视频」'),
2456
- videos.length > 1 ? h('div', { 'data-bd-kv': '' },
2457
- '以前做的:' + videos.slice(0, 6).map((v) => `${v.date}${v.mb != null ? `(${v.mb}MB)` : ''}`).join(' · ')) : null,
2458
- h('div', { 'data-bd-field': '' },
2459
- h('div', { 'data-bd-row': '', 'data-bd-toggle': '' },
2460
- h('div', { 'data-bd-grow': '' },
2461
- h('div', { 'data-bd-label': '' }, '每天自动做「昨天」的'),
2462
- h('div', { 'data-bd-note': '' }, '到了下面的时间点,如果昨天的成品还没有,就自己补一条。')),
2463
- h(Switch, {
2464
- on: Boolean(pv && pv.auto), disabled: Boolean(pvBusy),
2465
- onChange: async (v) => {
2466
- // 乐观更新 + 失败回滚(同 Panel 那套:等轮询回来才动,人会以为没点上)
2467
- setPv((old) => ({ ...(old || {}), auto: v }))
2468
- const okSaved = await patch({ pvAuto: v }, '每天自动做视频')
2469
- if (!okSaved) setPv((old) => ({ ...(old || {}), auto: !v }))
2470
- },
2471
- })),
2472
- h('div', { 'data-bd-row': '' },
2473
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '每天几点做'),
2474
- h('input', {
2475
- 'data-bd-input': '', 'data-bd-num': '', style: { width: '96px' }, type: 'time',
2476
- value: (pv && pv.at) || '10:30',
2477
- onChange: (e) => {
2478
- const v = e.target.value
2479
- if (!/^\d{2}:\d{2}$/.test(v)) return
2480
- setPv((old) => ({ ...(old || {}), at: v }))
2481
- patch({ pvAt: v }, '自动做视频的时间')
2482
- },
2483
- })),
2484
- h('div', { 'data-bd-row': '' },
2485
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '配音音色'),
2486
- h('select', {
2487
- 'data-bd-input': '', style: { width: '230px' },
2488
- value: (pv && pv.voice) || 'zh-CN-XiaoyiNeural',
2489
- onChange: (e) => patch({ pvVoice: e.target.value }, '配音音色'),
2490
- },
2491
- h('option', { value: 'zh-CN-XiaoyiNeural' }, '晓伊(默认 · 偏软)'),
2492
- h('option', { value: 'zh-CN-XiaoxiaoNeural' }, '晓晓(更清亮)'),
2493
- h('option', { value: 'zh-CN-XiaochenNeural' }, '晓辰(偏成熟)'))),
2494
- /* 参考音色三件套:DraftInput(本地草稿、失焦/回车才提交),语义与 Panel 相同 */
2495
- h('div', { 'data-bd-row': '' },
2496
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, 'GPT-SoVITS 目录'),
2497
- h(DraftInput, {
2498
- width: '300px', type: 'text', placeholder: 'D:\\...\\GPT-SoVITS',
2499
- value: (pv && pv.gsvRepo) || '', onCommit: (v) => patch({ pvGsvRepo: v }, 'GPT-SoVITS 目录'),
2500
- })),
2501
- h('div', { 'data-bd-row': '' },
2502
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '参考音频(3~10 秒)'),
2503
- h(DraftInput, {
2504
- width: '300px', type: 'text', placeholder: 'D:\\...\\voice-ds\\ref\\dwg_0031_ref.wav',
2505
- value: (pv && pv.gsvRef) || '', onCommit: (v) => patch({ pvGsvRef: v }, '参考音频'),
2506
- })),
2507
- h('div', { 'data-bd-row': '' },
2508
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '参考音频的原话'),
2509
- h(DraftInput, {
2510
- width: '300px', type: 'text', placeholder: '那段音频里说的那句话(逐字)',
2511
- value: (pv && pv.gsvPrompt) || '', onCommit: (v) => patch({ pvGsvPrompt: v }, '参考音频原话'),
2512
- })),
3085
+ const deps = pv?.deps
3086
+ const sessions = Array.isArray(pv?.sessions) ? pv.sessions : []
3087
+ const gallery = pvGalleryRows(pv)
3088
+ const selected = gallery.find((item) => item.key === pvSelection)
3089
+ const modeLabels = { daily: '冲浪日报', rant: '有话想说', highlights: '精选安利', custom: '自定主题' }
3090
+ const stageLabels = { starting: '正在创建会话', authoring: '正在编排视频', plan: '整理分镜', cover: '制作封面', render: '渲染画面', verify: '校验音视频', publish: '整理成片', done: '制作完成', failed: '制作失败', interrupted: '制作中断', stopped: '已停止' }
3091
+ const stageName = (stage) => stageLabels[stage] || '等待制作'
3092
+ const currentModel = videoModelId === null ? String(pv?.modelId || cfg.videoModelId || '') : videoModelId
3093
+ const models = Array.isArray(st?.models) ? st.models : []
3094
+ const saveModel = async (value) => {
3095
+ const previous = currentModel
3096
+ setVideoModelId(value)
3097
+ if (!(await patch({ videoModelId: value }, '制作模型'))) setVideoModelId(previous)
3098
+ }
3099
+ const selectedSession = selected && (selected.sessionId || sessions.find((item) => item.productionId === selected.id)?.sessionId)
3100
+ return h('div', { 'data-bd-pet-grid': '', 'data-bd-video-studio': '' },
3101
+ h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-video-intro': '' },
3102
+ heading('film', '开始一条新视频'),
3103
+ h('div', { 'data-bd-note': '' }, '基于主题或日报素材生成本地视频,包含配音、字幕与角色口型动画。'),
3104
+ pvDown ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, '视频状态读取失败:' + (typeof pvDown === 'string' ? pvDown : '服务暂时不可用') + '。可点「刷新」重试。') : null,
3105
+ pvError ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, pvError) : null,
3106
+ deps === false ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, '渲染依赖尚未装齐(edge-tts / imageio-ffmpeg / Pillow)。到「账号与环境」点「装主依赖」,完成后回来刷新。') : null,
3107
+ h('div', { 'data-bd-field': '' },
3108
+ h('span', { 'data-bd-label': '', id: 'bd-video-mode-label' }, '视频形式'),
3109
+ h('div', { 'data-bd-segments': '', role: 'group', 'aria-labelledby': 'bd-video-mode-label', style: { flexWrap: 'wrap' } },
3110
+ ...Object.entries(modeLabels).map(([value, label]) => h('button', { key: value, type: 'button', 'aria-pressed': pvMode === value, disabled: pvBusy === 'make', onClick: () => setPvMode(value) }, label)))),
3111
+ h('label', { 'data-bd-field': '' },
3112
+ h('span', { 'data-bd-label': '' }, '创作要求'),
3113
+ h('textarea', { 'data-bd-input': '', rows: 3, maxLength: 2000, value: pvBrief, placeholder: pvMode === 'daily' ? '留空会从当天公开素材里挑一条主线;也可以指定重点和偏好的画风…' : '例如:为什么收藏了那么多教程,还是没学会?用30秒做个反差小实验,纸张朱红风…', onChange: (e) => setPvBrief(e.target.value), style: { width: '100%', resize: 'vertical', minHeight: '82px' } })),
3114
+ h('div', { 'data-bd-row': '', style: { flexWrap: 'wrap' } },
3115
+ h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '日期'),
3116
+ h('input', { 'data-bd-input': '', type: 'date', value: pvDay || pv?.yesterday || '', onChange: (e) => setPvDay(e.target.value) })),
3117
+ h('label', { 'data-bd-field': '', 'data-bd-grow': '' }, h('span', { 'data-bd-label': '' }, '目标时长'),
3118
+ h('select', { 'data-bd-input': '', value: pvTargetDurationSec, onChange: (e) => setPvTargetDurationSec(Number(e.target.value)) },
3119
+ ...[30, 60, 90, 150].map((seconds) => h('option', { key: seconds, value: seconds }, seconds + ' 秒')))),
3120
+ h('label', { 'data-bd-field': '', 'data-bd-grow': '', style: { minWidth: '200px' } }, h('span', { 'data-bd-label': '' }, '制作模型'),
3121
+ models.length ? h('select', { 'data-bd-input': '', value: currentModel, disabled: pvBusy === 'make', onChange: (e) => saveModel(e.target.value) },
3122
+ h('option', { value: '' }, '使用当前会话 / 宿主默认模型'),
3123
+ currentModel && !models.some((model) => model.id === currentModel) ? h('option', { value: currentModel }, currentModel + '(当前清单外)') : null,
3124
+ ...models.map((model) => h('option', { key: model.id, value: model.id }, model.label || model.id)))
3125
+ : h('input', { 'data-bd-input': '', value: currentModel, disabled: pvBusy === 'make', placeholder: 'provider/model;留空使用默认模型', onChange: (e) => setVideoModelId(e.target.value), onBlur: (e) => saveModel(e.target.value.trim()) }))),
3126
+ h('div', { 'data-bd-note': '' }, '制作模型会保存为下次默认值。制作由当前会话中的模型调用工具异步完成;提交后会保持可见并等待封面、画面和音视频校验全部完成,再在原会话里反馈结果。'),
2513
3127
  h('div', { 'data-bd-row': '' },
2514
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '跑 GPT-SoVITS 的 Python'),
2515
- h(DraftInput, {
2516
- width: '300px', type: 'text', placeholder: '留空 = 用上面那个 python',
2517
- value: (pv && pv.gsvPython) || '', onCommit: (v) => patch({ pvGsvPython: v }, 'GPT-SoVITS 的 Python'),
2518
- })),
2519
- h('div', { 'data-bd-note': '' },
2520
- '默认走 edge-tts(需联网);参考音色配置齐全后使用本地 GPT-SoVITS,速度取决于设备。')))
3128
+ h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(pvBusy) || !pv || Boolean(pvDown) || deps === false, onClick: onPvMake }, pvBusy === 'make' ? '正在创建制作会话…' : '新开会话制作'),
3129
+ h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(pvBusy), onClick: onPvRefresh }, '刷新'))),
3130
+ h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
3131
+ h('div', { 'data-bd-h': '' }, icon('film'), h('b', {}, '成片放映室'), gallery.length ? h('span', { 'data-bd-badge': '' }, gallery.length + ' 条成品') : null),
3132
+ gallery.length ? h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '选择成片'),
3133
+ h('select', { 'data-bd-input': '', value: pvSelection, onChange: (e) => setPvSelection(e.target.value) },
3134
+ !selected ? h('option', { value: pvSelection }, '请选择一条成片') : null,
3135
+ ...gallery.map((item) => h('option', { key: item.key, value: item.key }, item.date + ' · ' + (item.title || (item.legacy ? '历史日报' : modeLabels[item.mode] || '视频')))))) : null,
3136
+ selected ? h('div', { 'data-bd-field': '' },
3137
+ h('video', { key: selected.key, src: selected.legacy ? API + '/pv/video?day=' + encodeURIComponent(selected.date) : API + '/pv/video?id=' + encodeURIComponent(selected.id), controls: true, preload: 'metadata', style: { width: '100%', borderRadius: '12px', background: '#f3f6fb', border: '1px solid #d7e1eb', display: 'block' } }),
3138
+ h('div', { 'data-bd-row': '', style: { flexWrap: 'wrap' } },
3139
+ h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, selected.date + ' · ' + (selected.legacy ? '历史成片' : '校验通过') + (selected.size ? ' · ' + (selected.size / 1048576).toFixed(1) + ' MB' : '')),
3140
+ selectedSession ? h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: async () => { if (await openPvSession(selectedSession)) onClose() } }, '回到制作会话') : null,
3141
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => copyText(selected.file) }, '复制成片路径'),
3142
+ h('a', { 'data-bd-btn': '', 'data-sm': '', href: selected.legacy ? API + '/pv/video?day=' + encodeURIComponent(selected.date) : API + '/pv/video?id=' + encodeURIComponent(selected.id) + '&download=1', download: 'DeepMoYu-' + selected.date + '.mp4', style: { textDecoration: 'none' } }, '下载成片'),
3143
+ pv?.out ? h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: onPvOpenFolder }, '打开成片目录') : null))
3144
+ : h('div', { 'data-bd-empty': '' }, gallery.length ? '尚未选择成片。' : '暂无已通过音视频校验的成片。')),
3145
+ h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
3146
+ heading('film', '最近的制作会话'),
3147
+ pvRunning ? h('div', { 'data-bd-msg': '', 'data-bd-video-progress': '', role: 'status' }, '制作进行中 · ' + (pv?.date || '') + ' · ' + stageName(pv?.stage) + ' · 已运行 ' + Math.round((Number(pv?.ms) || 0) / 1000) + ' 秒。完成并通过校验后,会回到当前制作会话反馈;对话不会提前结束。') : null,
3148
+ sessions.length ? h('div', { 'data-bd-field': '', style: { maxHeight: '320px', overflowY: 'auto' } },
3149
+ ...sessions.slice(0, 12).map((session) => h('div', { key: session.sessionId, 'data-bd-field': '', style: { padding: '10px 0', borderBottom: '1px solid #e1e9e8' } },
3150
+ h('div', { 'data-bd-row': '' },
3151
+ h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, session.date + ' · ' + (modeLabels[session.mode] || '视频')),
3152
+ h('span', { 'data-bd-badge': '', 'data-bd-video-stage': session.stage, 'data-warn': ['failed', 'interrupted'].includes(session.stage) ? '' : undefined }, stageName(session.stage)),
3153
+ ['starting', 'authoring', 'plan', 'cover', 'render', 'verify', 'publish'].includes(session.stage) ? h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(pvBusy), 'aria-label': '停止制作会话:' + session.sessionId, onClick: () => onPvStop(session.sessionId) }, '停止制作') : null,
3154
+ h('button', { 'data-bd-btn': '', 'data-sm': '', disabled: Boolean(pvBusy), 'aria-label': '沿用制作参数:' + session.sessionId, onClick: () => {
3155
+ if (session.date) setPvDay(String(session.date))
3156
+ if (session.mode) setPvMode(String(session.mode))
3157
+ setPvBrief(String(session.brief || ''))
3158
+ setPvTargetDurationSec(Number(session.targetDurationSec) || 60)
3159
+ say('已把这条的日期、形式、要求和时长填回上面的草稿。', 'good')
3160
+ } }, '沿用参数'),
3161
+ h('button', { 'data-bd-btn': '', 'data-sm': '', 'aria-label': '回到制作会话:' + session.sessionId, onClick: async () => { if (await openPvSession(session.sessionId)) onClose() } }, '回到会话')),
3162
+ h('div', { 'data-bd-note': '' }, (session.brief ? session.brief + ' · ' : '') + (session.model || '默认模型') + (session.createdAt ? ' · ' + petFmtTime(session.createdAt) : '')),
3163
+ session.error ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, String(session.error)) : null)))
3164
+ : h('div', { 'data-bd-empty': '' }, '开始制作后,新会话会记在这里,随时可以回去接着聊。'),
3165
+ !pvRunning && pv?.stage === 'failed' ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, '上一次制作失败:' + (pv.error || '未知原因')) : null,
3166
+ (() => {
3167
+ const failures = Array.isArray(pv?.failures) ? pv.failures : []
3168
+ if (!failures.length) return null
3169
+ return h('div', { 'data-bd-field': '' },
3170
+ h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '未完成的记录(' + failures.length + ')'),
3171
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setPvFailOpen(!pvFailOpen) }, pvFailOpen ? '收起' : '看详情')),
3172
+ pvFailOpen ? failures.slice(0, 12).map((item, i) => h('div', { key: String(item.at || '') + ':' + i, 'data-bd-field': '' },
3173
+ h('div', { 'data-bd-note': '' }, (item.date || '—') + ' · ' + stageName(item.stage) + ' · ' + String(item.error || '未知原因')),
3174
+ item.log ? h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => copyText(item.log) }, '复制日志路径') : null)) : null)
3175
+ })()),
3176
+ h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
3177
+ heading('clock', '每天的制作安排'),
3178
+ h('div', { 'data-bd-row': '', 'data-bd-toggle': '' },
3179
+ h('div', { 'data-bd-grow': '' }, h('div', { 'data-bd-label': '' }, '每天自动做「昨天」的'),
3180
+ h('div', { 'data-bd-note': '' }, '到点后,使用所选制作模型创建可见的制作会话,整理昨天的公开记录并完成视频;已有成品时跳过。')),
3181
+ h(Switch, { on: Boolean(pv?.auto), label: '每天自动制作视频', disabled: Boolean(pvBusy), onChange: async (value) => {
3182
+ setPv((old) => ({ ...(old || {}), auto: value }))
3183
+ if (!(await patch({ pvAuto: value }, '每天自动做视频'))) setPv((old) => ({ ...(old || {}), auto: !value }))
3184
+ } })),
3185
+ h('label', { 'data-bd-row': '' }, h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '每天几点做'),
3186
+ h('input', { 'data-bd-input': '', type: 'time', style: { width: '110px' }, value: pv?.at || '10:30', onChange: (e) => {
3187
+ const value = e.target.value
3188
+ if (!/^\d{2}:\d{2}$/.test(value)) return
3189
+ setPv((old) => ({ ...(old || {}), at: value }))
3190
+ patch({ pvAt: value }, '自动做视频的时间')
3191
+ } }))),
3192
+ h('section', { 'data-bd-sec': '', 'data-bd-span': '2' },
3193
+ heading('film', '配音与本地环境'),
3194
+ h('label', { 'data-bd-row': '' }, h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '配音音色'),
3195
+ h('select', { 'data-bd-input': '', style: { width: '230px' }, value: pv?.voice || 'zh-CN-XiaoyiNeural', onChange: (e) => patch({ pvVoice: e.target.value }, '配音音色') },
3196
+ h('option', { value: 'zh-CN-XiaoyiNeural' }, '晓伊(默认 · 偏软)'), h('option', { value: 'zh-CN-XiaoxiaoNeural' }, '晓晓(更清亮)'), h('option', { value: 'zh-CN-XiaochenNeural' }, '晓辰(偏成熟)'))),
3197
+ ...[
3198
+ ['GPT-SoVITS 目录', 'gsvRepo', 'pvGsvRepo', 'D:\\...\\GPT-SoVITS'],
3199
+ ['参考音频(3~10 秒)', 'gsvRef', 'pvGsvRef', 'D:\\...\\voice-ds\\ref\\dwg_0031_ref.wav'],
3200
+ ['参考音频的原话', 'gsvPrompt', 'pvGsvPrompt', '那段音频里说的那句话(逐字)'],
3201
+ ['跑 GPT-SoVITS 的 Python', 'gsvPython', 'pvGsvPython', '留空 = 使用账号与环境中的 Python'],
3202
+ ].map(([label, key, configKey, placeholder]) => h('div', { key, 'data-bd-row': '' }, h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, label),
3203
+ h(DraftInput, { width: '300px', type: 'text', label, placeholder, value: pv?.[key] || '', onCommit: (value) => patch({ [configKey]: value }, label) }))),
3204
+ h('div', { 'data-bd-note': '' }, '默认走 edge-tts(需联网);参考音色配置齐全后使用本地 GPT-SoVITS,速度取决于设备。')))
2521
3205
  }
2522
-
2523
3206
  const settingLogRows = tab === 'logs' && logMode === 'events' && Array.isArray(settingLogs) ? settingLogs.filter((e) => {
2524
3207
  const q = String(settingLogFilter || '').trim().toLowerCase()
2525
3208
  if (!q) return true
@@ -2615,6 +3298,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2615
3298
  }
2616
3299
  const startRun = async (mode) => {
2617
3300
  if (runLock.current) return
3301
+ if (mode === 'session' && st.toolScope?.checked && !st.toolScope.supported) { say('当前 DSH 不支持会话工具隔离,无法启动会话运行。', 'warn'); return }
2618
3302
  let options
2619
3303
  try { options = readRun(mode) } catch (e) { say(String(e?.message ?? e), 'warn'); return }
2620
3304
  runLock.current = true
@@ -2638,8 +3322,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2638
3322
  const workspace = ctx && typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null
2639
3323
  if (workspace && typeof workspace.openSession === 'function') await workspace.openSession(sessionId)
2640
3324
  } catch { /* 已启动的会话仍可从侧栏打开 */ }
2641
- say('已在新会话开始刷,可以从会话列表查看过程。', 'good')
2642
- } else say('后台已开始,运行进度会显示在这里。', 'good')
3325
+ say('会话任务已启动。', 'good')
3326
+ } else say('后台任务已启动。', 'good')
2643
3327
  try { const fresh = await jget('/status'); if (fresh?.ok) setSt(fresh) } catch { /* 下次轮询补上 */ }
2644
3328
  } catch (e) { say(String(e?.message ?? e), 'warn') }
2645
3329
  finally { runLock.current = false; setBusy('') }
@@ -2651,7 +3335,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2651
3335
  try {
2652
3336
  const r = await jpost('/feiyu', { action, ...(st.feiyu?.sessionId ? { sessionId: st.feiyu.sessionId } : {}) })
2653
3337
  if (!r?.ok) throw new Error(r?.error || r?.hint || '操作没有完成')
2654
- say(action === 'stop' ? '已暂停。' : action === 'resume' ? '已接着刷。' : '已收台。', 'good')
3338
+ say(action === 'stop' ? '任务已暂停。' : action === 'resume' ? '任务已继续。' : '任务已清除。', 'good')
2655
3339
  const fresh = await jget('/status')
2656
3340
  if (fresh?.ok) setSt(fresh)
2657
3341
  } catch (e) { say(String(e?.message ?? e), 'warn') }
@@ -2675,7 +3359,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2675
3359
  const tokens = fy.tokens || {}
2676
3360
  return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': mode },
2677
3361
  h('div', { 'data-bd-pet-grid': '' },
2678
- h('section', { 'data-bd-sec': '' }, heading('sliders', '下次怎么刷'),
3362
+ h('section', { 'data-bd-sec': '' }, heading('sliders', '运行参数'),
2679
3363
  h('label', { 'data-bd-field': '' }, h('span', { 'data-bd-label': '' }, '使用的模型'), modelField(mode)),
2680
3364
  /* ⭐ 后台「时段」任务(2026-10-28 使用者要求):填了 start–end,段内就**一直刷**——
2681
3365
  * 一轮接一轮接力到时段结束;DSH 重启 / 提前收工 / 崩溃都会被每分钟的调度钩子
@@ -2698,16 +3382,13 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2698
3382
  h('input', { 'data-bd-input': '', type: 'number', min: 0, max: 5000000, step: 1000,
2699
3383
  value: options.tokenBudget, onChange: (e) => updateRun(mode, 'tokenBudget', e.target.value) })),
2700
3384
  h('div', { 'data-bd-note': '' }, session
2701
- ? '轮数与 token 在每轮结束后检查,可能略超预算;到达时限会暂停续轮,当前轮可能继续到结束。'
2702
- + '⭐ 它刷着的时候你随时插一句话,当前那轮就会作废重来 —— 想下指令(挑点别的、停下、换个口味)就大方打字,不用等它问。'
3385
+ ? '轮数与 token 预算在每轮结束后检查;到达时限后暂停续轮。会话内的新消息按宿主中断规则处理。'
2703
3386
  : (String(options.windowStart || '') && String(options.windowEnd || '')
2704
3387
  ? `时段任务:${options.windowStart}–${options.windowEnd} 内一直刷,一轮接一轮到时段结束;中途关掉 DSH,重新打开也会在时段内自动接着刷。手动「停止」后本时段不再自动拉起。token 预算是软目标,不能精确截断。`
2705
- : '填上「从几点刷到几点」就是常设时段任务(推荐);留空则按下面的分钟数刷一轮,提前做完会自动接着刷到点。token 预算交给后台代理作任务目标,不能按精确用量自动截断。')),
3388
+ : '后台时段为空时,按单次运行时长执行;提前结束会在剩余时长内续接。后台 token 预算为软目标,不能按精确用量自动截断。')),
2706
3389
  h('div', { 'data-bd-row': '' },
2707
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => saveRun(mode) }, '保存默认设置'),
2708
- h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy) || live, onClick: () => startRun(mode) },
2709
- icon(session ? 'play' : 'moon'), busy === 'start-' + mode ? '启动中…' : session ? '新开会话,开始刷' : '开始后台刷')),
2710
- h('div', { 'data-bd-note': '' }, '两个模式分别保存。修改参数不会改变正在运行的任务。')),
3390
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => saveRun(mode) }, '保存默认设置')),
3391
+ h('div', { 'data-bd-note': '' }, '两种方式分别保存。模型、时长与预算用于下一次启动;后台时段保存后生效。运行中任务不受草稿影响。')),
2711
3392
  h('section', { 'data-bd-sec': '' }, heading('pulse', '运行状态'),
2712
3393
  h('span', { 'data-bd-badge': '', 'data-live': live ? '' : undefined },
2713
3394
  live ? '运行中' : session && (fy.goal?.phase === 'paused' || fy.goal?.phase === 'blocked') ? '已暂停' : '空闲'),
@@ -2730,15 +3411,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2730
3411
  session && fy.source === 'manual' && live ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('stop') }, '停止手动刷站') : null,
2731
3412
  session && fy.goal ? h('div', { 'data-bd-row': '' },
2732
3413
  live ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('stop') }, '暂停')
2733
- : h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('resume') }, '接着刷'),
2734
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('clear') }, '收台')) : null,
3414
+ : h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('resume') }, '继续运行'),
3415
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => controlRun('clear') }, '清除任务')) : null,
2735
3416
  !session && live ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: async () => {
2736
3417
  setBusy('stop-bg')
2737
3418
  try { const r = await jpost('/pet/stop', {}); if (!r?.ok) throw new Error(r?.error || '没有停下'); say('已停止后台任务。', 'good') }
2738
3419
  catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
2739
3420
  } }, '停止后台任务') : null,
2740
3421
  !session && st.lastRunAt ? h('div', { 'data-bd-note': '' }, '上次运行:' + petFmtTime(st.lastRunAt)) : null,
2741
- h('div', { 'data-bd-note': '' }, '详细过程可在左侧「运行日志」中查看。'),
3422
+ h('div', { 'data-bd-note': '' }, '执行明细保存在「记录与用量」中。'),
2742
3423
  /* ⏸️ 免跑牌期间如实说(st.bgWindow.holdUntil 由宿主 /status 提供) */
2743
3424
  !session && st.bgWindow && Number(st.bgWindow.holdUntil) > Date.now()
2744
3425
  ? h('div', { 'data-bd-note': '' }, `本时段已停止自动拉起(到 ${new Date(st.bgWindow.holdUntil).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })});点「开始后台刷」可立刻恢复。`)
@@ -2784,16 +3465,27 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2784
3465
  onChange: (v) => patch({ petShow: v }, '显示桌宠'),
2785
3466
  }),
2786
3467
  ToggleRow({
2787
- label: '空闲时自言自语', on: !(st && st.config && st.config.idleChatter === false), disabled: Boolean(busy),
2788
- note: '闲着的时候偶尔冒一句观察(两三个小时一条)——嫌吵就关成安静模式。',
3468
+ label: '闲聊提示', on: !(st && st.config && st.config.idleChatter === false), disabled: Boolean(busy),
3469
+ note: '按已配置频率显示闲聊,频率由陪伴节奏控制。',
2789
3470
  onChange: (v) => patch({ idleChatter: v }, '闲聊泡泡'),
2790
- })),
3471
+ }), h(PetCompanionSettings, { st, busy, patch })),
2791
3472
  () => h('div', { 'data-bd-sec': '' },
2792
- heading('spark', '刷什么 · 用什么口气'),
2793
- h('div', { 'data-bd-note': '' }, '只在「鲸鱼摸鱼模式」预设里生效,留空用预设自带的那套口吻。'),
3473
+ heading('spark', '角色与表达'),
3474
+ h('div', { 'data-bd-note': '' }, '人格与陪伴设置仅用于鲸鱼摸鱼模式。留空使用默认设定。'),
3475
+ h('div', { 'data-bd-field': '' },
3476
+ h('span', { 'data-bd-label': '' }, '陪伴档位'),
3477
+ h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '陪伴档位' },
3478
+ ...[['quiet', '安静陪伴'], ['companion', '一起冲浪'], ['creator', '创作者'], ['steward', '自动管家']].map(([key, label]) => h('button', {
3479
+ key, type: 'button', 'aria-pressed': (interactionStyle === null ? 'companion' : interactionStyle) === key,
3480
+ disabled: Boolean(busy), onClick: () => setInteractionStyle(key),
3481
+ }, label)))),
3482
+ h('div', { 'data-bd-note': '' }, ({ quiet: '少打断,多观察;适合把鱼放在旁边一起待着。', companion: '有趣的就拉你一起看,偶尔吐槽,不抢你的注意力。', creator: '主动找反差、细节和能留下来的判断,适合做内容。', steward: '更在意连续性、收尾和下一步,适合让它替你照看日常。' }[(interactionStyle === null ? 'companion' : interactionStyle)])),
3483
+ h('div', { 'data-bd-row': '' },
3484
+ h('span', { 'data-bd-grow': '' }),
3485
+ h('button', { 'data-bd-btn': '', 'data-sm': '', 'data-primary': '', disabled: Boolean(busy), onClick: () => patch({ interactionStyle: interactionStyle || 'companion' }, '陪伴档位') }, '保存档位')),
2794
3486
  h('textarea', {
2795
- 'data-bd-input': '', rows: 4, value: persona === null ? '' : persona,
2796
- placeholder: '例:像个爱折腾的技术宅,说人话,看到夸大宣传会忍不住吐槽。',
3487
+ 'data-bd-input': '', rows: 4, 'aria-label': '大肥鱼的人格设定', value: persona === null ? '' : persona,
3488
+ placeholder: '例如:关注技术内容,表达简洁,保留个人观点。',
2797
3489
  onChange: (e) => setPersona(e.target.value),
2798
3490
  }),
2799
3491
  h('div', { 'data-bd-row': '' },
@@ -2808,14 +3500,14 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2808
3500
  h('div', { 'data-bd-row': '' },
2809
3501
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '一天最多挑几条'),
2810
3502
  h('input', {
2811
- 'data-bd-input': '', 'data-bd-num': '', type: 'number',
3503
+ 'data-bd-input': '', 'data-bd-num': '', type: 'number', 'aria-label': '一天最多挑几条',
2812
3504
  min: numLim('perDay', 0, 50)[0], max: numLim('perDay', 0, 50)[1],
2813
3505
  value: perDay === null ? '5' : perDay, onChange: (e) => setPerDay(e.target.value),
2814
3506
  })),
2815
3507
  h('div', { 'data-bd-row': '' },
2816
3508
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '报告详细程度'),
2817
3509
  h('select', {
2818
- 'data-bd-input': '', style: { width: '140px' },
3510
+ 'data-bd-input': '', 'aria-label': '报告详细程度', style: { width: '140px' },
2819
3511
  value: rl === null ? 'full' : rl, onChange: (e) => setRl(e.target.value),
2820
3512
  },
2821
3513
  h('option', { value: 'full' }, '完整'),
@@ -2830,7 +3522,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2830
3522
  heading('dots', '长期偏好'),
2831
3523
  h('div', { 'data-bd-note': '' }, '关键词与参考文件提供长期背景,不限制她刷什么。参考路径不改变新会话所属工作区。'),
2832
3524
  h('textarea', {
2833
- 'data-bd-input': '', rows: 3, placeholder: '一行一个',
3525
+ 'data-bd-input': '', 'aria-label': '长期偏好关键词', rows: 3, placeholder: '一行一个',
2834
3526
  value: kws === null ? '' : kws, onChange: (e) => setKws(e.target.value),
2835
3527
  }),
2836
3528
  h('input', {
@@ -2845,22 +3537,58 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2845
3537
  await patch({ keywords: list, workspace: String(wsDir || '').trim() }, '长期偏好')
2846
3538
  },
2847
3539
  }, '保存'))),
2848
- () => h('div', { 'data-bd-sec': '' },
2849
- heading('pulse', '出手的节奏'),
2850
- h('div', { 'data-bd-row': '' },
2851
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '节奏倍数(0.3 快 / 1 正常 / 3 慢三倍)'),
2852
- h('input', {
2853
- 'data-bd-input': '', 'data-bd-num': '', type: 'number', step: 0.1,
2854
- min: numLim('paceFactor', 0.3, 3)[0], max: numLim('paceFactor', 0.3, 3)[1],
2855
- value: paceFactor === null ? '1' : paceFactor, onChange: (e) => setPaceFactor(e.target.value),
2856
- })),
2857
- h('div', { 'data-bd-note': '' },
2858
- '倍数只影响「下次该等多久」,不会把已经钉下的间隔缩短 —— 想让它立刻变快,等手上这一条发完再调。'),
2859
- h('div', { 'data-bd-row': '' },
2860
- h('button', {
2861
- 'data-bd-btn': '', disabled: Boolean(busy),
2862
- onClick: () => patch({ paceFactor: clampField('paceFactor', paceFactor, 0.3, 3) }, '出手节奏'),
2863
- }, '保存'))),
3540
+ () => {
3541
+ const info = st && st.pace && st.pace.explanation
3542
+ const saved = Number(cfg.paceFactor ?? info?.factor ?? 1)
3543
+ const draftText = paceFactor === null ? String(saved) : paceFactor
3544
+ const draft = Number(draftText)
3545
+ const [lo, hi] = numLim('paceFactor', 0.3, 3)
3546
+ const valid = String(draftText).trim() !== '' && Number.isFinite(draft) && draft >= lo && draft <= hi
3547
+ const changed = valid && draft !== saved
3548
+ const presets = info?.presets || { normal: 1, responsive: 0.5, cautious: 1.5 }
3549
+ // 规则与基础秒数来自 paceExplain;本地只预览草稿,不另起请求、不改发送账。
3550
+ const range = (v, factor) => `${Math.round(v.baseMinSec * 1000 * factor) / 1000}–${Math.round(v.baseMaxSec * 1000 * factor) / 1000} 秒`
3551
+ const rules = info ? [
3552
+ ['小时上限', info.notes.hour + ' 防止一小时的出手集中在一小会儿。'],
3553
+ ['连发刹车', `所有发送合计 ${info.burst.windowSec / 60} 分钟最多 ${info.burst.max} 次;达到 ${info.burst.extraAfterCount} 次还会额外等 ${info.burst.extraWaitSec / 60} 分钟。` + info.notes.burst],
3554
+ ['额度停牌', info.notes.quota + ` 再撞按 ${info.quota.parkSec.map((sec) => sec / 60).join(' → ')} 分钟递增,最晚只停到本地下一个 00:05;额度不是手速问题。`],
3555
+ ['风控冷却', Object.entries(info.risk.cooldownSecByCode).map(([code, sec]) => `${code}:${sec / 60} 分钟`).join(';') + '。' + info.notes.risk + ' 避免硬试续封。'],
3556
+ ] : []
3557
+ return h('section', { 'data-bd-sec': '', 'data-bd-pace-editor': '' },
3558
+ heading('pulse', '发送节奏'),
3559
+ h('div', { 'data-bd-note': '' }, `已保存 ${saved} 倍。预设只填草稿,按「保存节奏」才生效;不会自动改掉原设置。`),
3560
+ h('div', { 'data-bd-row': '', 'aria-label': '节奏快捷预设' },
3561
+ ...[['normal', '标准'], ['responsive', '轻快'], ['cautious', '谨慎']].map(([key, label]) => h('button', {
3562
+ key, 'data-bd-btn': '', 'data-sm': '', 'data-bd-pace-preset': key,
3563
+ 'aria-pressed': valid && draft === presets[key], disabled: Boolean(busy),
3564
+ onClick: () => setPaceFactor(String(presets[key])),
3565
+ }, `${label} ${presets[key]}`))),
3566
+ h('div', { 'data-bd-row': '' },
3567
+ h('label', { 'data-bd-label': '', 'data-bd-grow': '', htmlFor: 'bd-pace-factor' }, `自定倍数(${lo}–${hi};越小间隔越短)`),
3568
+ h('input', {
3569
+ id: 'bd-pace-factor', 'aria-label': '节奏倍数', 'aria-invalid': !valid || undefined,
3570
+ 'data-bd-input': '', 'data-bd-num': '', type: 'number', step: 'any', min: lo, max: hi,
3571
+ value: draftText, onChange: (e) => setPaceFactor(e.target.value),
3572
+ })),
3573
+ !valid ? h('div', { 'data-bd-msg': '', 'data-warn': '' }, `请填 ${lo}–${hi} 之间的数字;空白或越界草稿不会保存。`) : null,
3574
+ h('div', { 'data-bd-note': '' }, info?.notes.interval || '保存只影响下一次成功发送后新钉下的间隔与随机抖动;已钉下的等待不会重算。'),
3575
+ info ? h('div', { 'data-bd-sub': '', 'data-bd-pace-preview': '' },
3576
+ h('div', { 'data-bd-h': '' }, changed ? '未保存草稿预览(不是当前倒计时)' : '间隔范围(不是当前倒计时)'),
3577
+ ...Object.entries(info.kinds).map(([key, v]) => h('div', { key, 'data-bd-note': '', 'data-bd-pace-range': key },
3578
+ h('b', {}, v.label),
3579
+ `:基础 ${range(v, 1)} · 已保存 ${range(v, saved)}`,
3580
+ changed ? ` · 草稿 ${range(v, draft)}` : '',
3581
+ ` · 每滚动小时最多 ${v.hourCap} 条`)))
3582
+ : h('div', { 'data-bd-note': '' }, '节奏说明尚未就绪;不会猜基础范围,完整重启 DSH 后重试。'),
3583
+ h('div', { 'data-bd-row': '' },
3584
+ h('button', {
3585
+ 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy) || !valid,
3586
+ onClick: () => valid && patch({ paceFactor: draft }, '出手节奏'),
3587
+ }, '保存节奏')),
3588
+ info ? h('details', {}, h('summary', { 'data-bd-label': '' }, '保留的刹车:小时 / 连发 / 额度 / 风控'),
3589
+ h('div', { 'data-bd-sub': '' }, ...rules.map(([label, note]) => h('div', { key: label, 'data-bd-note': '' }, h('b', {}, label + ':'), note)))) : null,
3590
+ h('div', { 'data-bd-note': '' }, info?.notes.safety || '预设是本地节奏偏好,不是 B站 承诺的安全频率;不会缩短小时、连发、额度或风控闸。'))
3591
+ },
2864
3592
 
2865
3593
  /* ── 📡 它刚才在干嘛(2026-10-27 从旧设置页搬进来):实时流水是桌宠化时唯一没跟进弹窗的
2866
3594
  * 主力功能,这里补上。2.5 秒增量轮询 + 「正在想」呼吸条(模型读字幕的几十秒里流水
@@ -2874,177 +3602,366 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
2874
3602
  ? h('div', { 'data-bd-thinking': '' },
2875
3603
  h('i', { 'data-bd-dot': '' }),
2876
3604
  h('span', { 'data-bd-grow': '' }, actLive.round
2877
- ? `第 ${actLive.round}${actLive.maxRounds ? '/' + actLive.maxRounds : ''} 轮 · 正在想(翻字幕、挑片子、琢磨怎么冒泡)`
2878
- : '正在想(翻字幕、挑片子、琢磨怎么冒泡)'),
3605
+ ? `第 ${actLive.round}${actLive.maxRounds ? '/' + actLive.maxRounds : ''} 轮 · 模型处理中`
3606
+ : '模型处理中'),
2879
3607
  actLive.since
2880
3608
  ? h('time', {}, '已跑 ' + fmtSpanMs(actLive.since))
2881
3609
  : null)
2882
3610
  : null,
2883
3611
  h('div', { 'data-bd-act': '' },
2884
3612
  acts.length === 0
2885
- ? h('div', { 'data-bd-empty': '' }, '还没动静~开冲之后这儿会一条条冒出来:搜了啥、挑了哪条、读到第几秒')
3613
+ ? h('div', { 'data-bd-empty': '' }, '暂无活动记录。')
2886
3614
  : acts.map((a) => h('div', { 'data-bd-actline': '', 'data-kind': a.kind || 'info', key: a.id || a.seq },
2887
3615
  h('time', {}, petFmtTime(a.t)),
2888
3616
  h('span', {}, a.text),
2889
3617
  a.kind === 'call' ? h('em', { 'data-bd-running': '' }, '进行中') : null))),
2890
- h('div', { 'data-bd-note': '' }, '它每做一件事都会在这里说一句人话;更完整的来龙去脉看下面的「近期运行日志」。'))
3618
+ h('div', { 'data-bd-note': '' }, '活动列表显示最近的工具调用与执行结果。'))
2891
3619
  ]
2892
3620
  const pageHead = (title, note) => h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, title), h('p', {}, note)))
3621
+ const todayPage = () => {
3622
+ const cfgNow = st?.config || {}
3623
+ const fyNow = st?.feiyu || {}
3624
+ const bgNow = st?.backgroundRun || {}
3625
+ const learning = st?.learn || {}
3626
+ const soul = st?.soul || {}
3627
+ const activityRows = (Array.isArray(st?.activity) ? st.activity : []).filter(Boolean)
3628
+ const latest = activityRows[activityRows.length - 1]
3629
+ const active = Boolean(fyNow.active || st?.bgBusy)
3630
+ const modeLabel = fyNow.active ? '会话运行中' : st?.bgBusy ? (bgNow.task === 'learn-review' ? '学习审核中' : '后台运行中') : '空闲'
3631
+ const modeTone = active ? 'live' : undefined
3632
+ const publicAllowed = cfgNow.dmMode !== 'only'
3633
+ const sent = [cfgNow.autoComment && publicAllowed, cfgNow.replyToComments && publicAllowed, cfgNow.autoReplyDm, cfgNow.autoMoment,
3634
+ cfgNow.dmMode !== 'off' && Number(cfgNow.dmUid) > 0].filter(Boolean).length
3635
+ const trustLabel = cfgNow.sendReview ? '发送前预览' : sent === 0 ? '只观察' : '按通道授权'
3636
+ const next = !st?.venvReady
3637
+ ? { label: '主依赖未安装', note: '视频读取依赖 Python 环境与 B站工具链。', tab: 'other' }
3638
+ : !st?.loggedIn
3639
+ ? { label: 'B站未登录', note: '登录凭证仅保存在本机。', tab: 'other' }
3640
+ : active
3641
+ ? { label: '任务运行中', note: '活动与运行事件已记录。', tab: 'logs' }
3642
+ : Number(st?.sendReview?.unknown) > 0
3643
+ ? { label: '核对待确认的发送', note: `${st.sendReview.unknown} 条发送结果不确定,请先去 B站核对,避免重复发送。`, tab: 'speech' }
3644
+ : Number(st?.sendReview?.pending) > 0
3645
+ ? { label: '待审核发送内容', note: `${st.sendReview.pending} 条内容待审。审批按单条内容执行。`, tab: 'speech' }
3646
+ : st?.attention?.reasons?.length
3647
+ ? { label: '待处理事项', note: String(st.attention.reasons[0]), tab: String(st.attention.reasons[0]).includes('学习') ? 'learning' : 'speech' }
3648
+ : !st?.ranToday && !st?.lastRunAt
3649
+ ? { label: '尚无运行记录', note: '运行参数与启动入口位于「运行方式」。', tab: 'run' }
3650
+ : learning.pending > 0
3651
+ ? { label: '待审核学习记录', note: `${learning.pending} 条经验待审。`, tab: 'learning' }
3652
+ : { label: '无待处理事项', note: '当前配置与运行记录均可查看。', tab: 'basic' }
3653
+ const recentText = latest ? String(latest.text || latest.summary || '').replace(/\s+/g, ' ').trim() : ''
3654
+ const summary = active
3655
+ ? `任务正在运行。${recentText ? '最近动静:' + recentText.slice(0, 80) : '还没有收到新的活动记录,可打开日志查看。'}`
3656
+ : st?.lastRunAt
3657
+ ? `最近运行:${petFmtTime(st.lastRunAt)}。今日精选 ${Number(st.pickedToday) || 0} 条。`
3658
+ : '暂无今日运行记录。'
3659
+ const soulTotal = Array.isArray(soul.filled) ? soul.filled.length : 0
3660
+ return h('div', { 'data-bd-today': '' },
3661
+ h('section', { 'data-bd-today-hero': '' },
3662
+ h('div', { 'data-bd-today-hero-copy': '' },
3663
+ h('span', { 'data-bd-eyebrow': '' }, 'DEEP MOYU · DAILY DESK'),
3664
+ h('h1', {}, '运行概览'),
3665
+ h('p', {}, summary),
3666
+ h('div', { 'data-bd-row': '' },
3667
+ h('span', { 'data-bd-badge': '', 'data-live': modeTone }, modeLabel),
3668
+ h('span', { 'data-bd-note': '' }, '陪伴档位:' + ({ quiet: '安静陪伴', companion: '一起冲浪', creator: '创作者', steward: '自动管家' }[cfgNow.interactionStyle] || '一起冲浪')),
3669
+ )),
3670
+ h('div', { 'data-bd-today-fish': '', 'aria-hidden': true }, h('img', { src: PET_IMG_URL, alt: '', draggable: false }))),
3671
+ h('div', { 'data-bd-today-grid': '' },
3672
+ h('section', { 'data-bd-today-card': '', 'data-bd-today-status': '' },
3673
+ h('div', { 'data-bd-h': '' }, icon('pulse'), h('b', {}, '今日运行统计')),
3674
+ h('div', { 'data-bd-kpi': '' },
3675
+ h('div', {}, h('b', {}, String(st?.ranToday ?? 0)), h('span', {}, '今日轮次')),
3676
+ h('div', {}, h('b', {}, String(st?.pickedToday ?? 0)), h('span', {}, '今日精选')),
3677
+ h('div', {}, h('b', {}, st?.tokensToday === null || st?.tokensToday === undefined ? '—' : Number(st.tokensToday).toLocaleString()), h('span', {}, '今日 token'))),
3678
+ h('div', { 'data-bd-today-current': '' }, latest ? h('span', {}, recentText || '刚刚有一条新动静') : h('span', {}, '还没有新的活动记录')),
3679
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab(active ? 'logs' : 'run') }, active ? '打开实时流水' : '安排一轮冲浪')),
3680
+ h('section', { 'data-bd-today-card': '', 'data-bd-today-next': '' },
3681
+ h('div', { 'data-bd-h': '' }, icon('spark'), h('b', {}, '相关事项')),
3682
+ h('strong', {}, next.label), h('div', { 'data-bd-note': '' }, next.note),
3683
+ h('button', { 'data-bd-btn': '', 'data-primary': '', onClick: () => {
3684
+ if (next.tab === 'basic') modalRef.current?.querySelector('[data-bd-basic-grid]')?.scrollIntoView?.({ block: 'start', behavior: cfgNow.petReducedMotion ? 'auto' : 'smooth' })
3685
+ else setTab(next.tab)
3686
+ } }, '查看详情')),
3687
+ h('section', { 'data-bd-today-card': '' },
3688
+ h('div', { 'data-bd-h': '' }, icon('key'), h('b', {}, '发送安全档位')),
3689
+ h('div', { 'data-bd-today-trust': '' }, h('strong', {}, trustLabel), h('span', {}, sent ? `已授权 ${sent} 个投递通道` : '自动投递开关保持关闭')),
3690
+ st?.sendReview ? h('div', { 'data-bd-note': '' }, st.sendReview.ok === false ? '预览队列暂时读不到,请到互动与投递页重试。' : `${Number(st.sendReview.pending) || 0} 条待审 · ${Number(st.sendReview.unknown) || 0} 条结果待核对`) : null,
3691
+ h('div', { 'data-bd-note': '' }, '节奏、判重、冷却和额度闸始终生效。需要改变权限时,去互动与投递页逐项确认。'),
3692
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab('speech') }, '检查投递权限')),
3693
+ h('section', { 'data-bd-today-card': '' },
3694
+ h('div', { 'data-bd-h': '' }, icon('spark'), h('b', {}, '记忆统计')),
3695
+ h('div', { 'data-bd-kpi': '' },
3696
+ h('div', {}, h('b', {}, String(learning.approved || 0)), h('span', {}, '已批准经验')),
3697
+ h('div', {}, h('b', {}, String(learning.pending || 0)), h('span', {}, '待审经验')),
3698
+ h('div', {}, h('b', {}, String(soulTotal)), h('span', {}, '有内容的心情节'))),
3699
+ h('div', { 'data-bd-note': '' }, soul.empty ? '暂无角色记忆。' : (soul.updatedAt ? '最近更新:' + petFmtTime(soul.updatedAt) : '角色记忆已读取。')),
3700
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab('learning') }, '打开记忆中心')),
3701
+ h('section', { 'data-bd-today-card': '', 'data-bd-today-wide': '' },
3702
+ h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, icon('list'), h('b', {}, '今天的节奏')),
3703
+ h('span', { 'data-bd-badge': '', 'data-live': active ? '' : undefined }, active ? '进行中' : st?.schedule?.inWindow ? '定时窗口内' : '未运行')),
3704
+ h('div', { 'data-bd-timeline': '' },
3705
+ h('div', { 'data-bd-timeline-item': '', 'data-on': st?.loggedIn ? '' : undefined }, h('i', {}), h('span', {}, st?.loggedIn ? '账号已连接' : '等待登录')),
3706
+ h('div', { 'data-bd-timeline-item': '', 'data-on': st?.ranToday || st?.lastRunAt ? '' : undefined }, h('i', {}), h('span', {}, st?.ranToday || st?.lastRunAt ? `今天已留下 ${st?.ranToday || 0} 轮记录` : '还没有今天的冲浪记录')),
3707
+ h('div', { 'data-bd-timeline-item': '', 'data-on': active ? '' : undefined }, h('i', {}), h('span', {}, active ? '任务执行中' : '当前未运行')))))
3708
+ )
3709
+ }
2893
3710
  const basicPage = () => {
2894
3711
  /* 🧭 新手三步(2026-10-28):齐了这张卡自己消失 —— 不用任何人去关它 */
2895
3712
  const steps = [
2896
- { done: Boolean(st && st.venvReady), label: '装主依赖(Python 环境)', view: 'other' },
3713
+ { done: Boolean(st && st.venvReady), label: '安装主依赖(Python 环境)', view: 'other' },
2897
3714
  { done: Boolean(st && st.loggedIn), label: '扫码登录 B站', view: 'other' },
2898
- { done: Boolean(Number(st && st.ranToday) || (st && st.lastRunAt)), label: '开第一把刷', view: 'run' },
3715
+ { done: Boolean(Number(st && st.ranToday) || (st && st.lastRunAt)), label: '首次运行', view: 'run' },
2899
3716
  ]
2900
3717
  const todo = steps.filter((s) => !s.done)
2901
3718
  return h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'basic' },
2902
- pageHead('偏好与桌宠', '选什么,怎么说,记录多少。每张卡独立保存。'),
3719
+ pageHead('偏好与桌宠', '角色偏好、桌宠显示与报告设置。'),
2903
3720
  todo.length ? h('section', { 'data-bd-sec': '', 'data-bd-onboard': '' },
2904
- h('div', { 'data-bd-h': '' }, icon('spark'), h('b', {}, '先把鱼安顿好(三步,齐了这张卡自己消失)')),
3721
+ h('div', { 'data-bd-h': '' }, icon('spark'), h('b', {}, '初始化状态')),
2905
3722
  ...steps.map((s) => h('div', { 'data-bd-row': '', key: s.label },
2906
3723
  h('span', { 'data-bd-badge': '', 'data-live': s.done ? '' : undefined }, s.done ? '✓ 已完成' : '待办'),
2907
3724
  h('span', { 'data-bd-grow': '', style: { fontWeight: s.done ? 400 : 600 } }, s.label),
2908
- s.done ? null : h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab(s.view) }, '去处理')))) : null,
3725
+ s.done ? null : h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab(s.view) }, '配置')))) : null,
2909
3726
  h('div', { 'data-bd-pet-grid': '', 'data-bd-basic-grid': '' }, basicCards[1](), basicCards[3](), basicCards[0](), basicCards[2]()))
2910
3727
  }
2911
3728
  const speechPage = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'speech' },
2912
- pageHead('互动与投递', '公开冒泡和私信往来,各有自己的开关。'),
3729
+ pageHead('互动与投递', '管理公开发言、私信投递、自动回复和发送限制。'),
3730
+ h(PetSendReviewPanel, { st, patch, busy, say }),
2913
3731
  h('div', { 'data-bd-pet-grid': '' },
2914
3732
  h('section', { 'data-bd-sec': '' }, heading('spark', '公开发言'), ToggleRow({
2915
- label: '自动发说说', on: sw('autoMoment'), disabled: Boolean(busy),
2916
- note: '把这一轮的想法发成 B站 动态。默认关;关着的时候它一次都不会发。',
2917
- onChange: (v) => patch({ autoMoment: v }, '自动发说说'),
3733
+ label: '自动发布动态', on: sw('autoMoment'), disabled: Boolean(busy),
3734
+ note: '允许根据当前内容发布 B站动态。默认关闭,是否发布由角色自主判断。',
3735
+ onChange: (v) => patch({ autoMoment: v }, '自动发布动态'),
2918
3736
  }),
2919
3737
  sw('autoMoment') ? h('div', { 'data-bd-sub': '' },
2920
3738
  h('div', { 'data-bd-row': '' },
2921
3739
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '口吻'),
2922
3740
  h('select', {
2923
- 'data-bd-input': '', 'data-bd-num': '', style: { width: '104px' },
2924
- value: momentStyle === null ? '感想' : momentStyle, onChange: (e) => setMomentStyle(e.target.value),
3741
+ 'data-bd-input': '', 'data-bd-num': '', 'aria-label': '动态口吻', style: { width: '104px' },
3742
+ value: momentStyle === null ? '随心情' : momentStyle, onChange: (e) => setMomentStyle(e.target.value),
2925
3743
  },
2926
- ['感想', '吐槽', '安利', '冷知识'].map((s) => h('option', { key: s, value: s }, s)))),
3744
+ ['随心情', '感想', '吐槽', '安利', '冷知识'].map((s) => h('option', { key: s, value: s }, s)))),
2927
3745
  h('div', { 'data-bd-row': '' },
2928
- h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '每天动态目标'),
3746
+ h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '每天上限'),
2929
3747
  h('input', {
2930
- 'data-bd-input': '', 'data-bd-num': '', type: 'number',
3748
+ 'data-bd-input': '', 'data-bd-num': '', 'aria-label': '每日动态上限', type: 'number',
2931
3749
  min: numLim('momentPerDay', 0, 50)[0], max: numLim('momentPerDay', 0, 50)[1],
2932
3750
  value: momentPerDay === null ? '1' : momentPerDay, onChange: (e) => setMomentPerDay(e.target.value),
2933
3751
  })),
2934
- h('div', { 'data-bd-note': '' }, '每天要发够这个量(不是"最多"——是当日目标;0 = 永远不发;后端上限 50)。')) : null,
3752
+ h('div', { 'data-bd-note': '' }, '每日发布上限不代表任务量。0 表示不发布,最大 50;无合适内容时可不发布。')) : null,
2935
3753
  ToggleRow({
2936
3754
  label: '自动评论视频', on: sw('autoComment'), disabled: Boolean(busy),
2937
- note: '看完有具体想说的话时再评论,也可以安静看完。按自己的口气说,不用 @ 任何人。',
3755
+ note: '读取视频后,根据内容判断是否评论。评论遵循角色设定,不要求 @ 用户。',
2938
3756
  onChange: (v) => patch({ autoComment: v }, '自动评论'),
2939
3757
  }),
2940
3758
  sw('autoComment') ? h('div', { 'data-bd-sub': '' },
2941
- h('div', { 'data-bd-note': '' }, '没有每日条数上限;是否评论由她自己决定。同一个视频只评一次,发送仍按节奏闸执行。'),
3759
+ h('div', { 'data-bd-note': '' }, '无固定每日条数上限;是否评论由角色自主判断。同一个视频只评论一次,发送受节奏、冷却和判重限制。'),
2942
3760
  dmMode === 'only' ? h('div', { 'data-bd-msg': '', 'data-warn': '' },
2943
- '评论开关开着但不生效:投递方式是「只发私信」,公开评论整个关掉了~') : null) : null,
3761
+ '当前选择「仅私信」,公开评论已停用;自动评论开关保持开启但不执行。') : null) : null,
2944
3762
  ToggleRow({
2945
3763
  label: '自动回复评论', on: sw('replyToComments'), disabled: Boolean(busy),
2946
- note: '有人回了你的评论,它想回就回 —— 不必每条都搭话。每轮先看一眼有没有人回你。',
3764
+ note: '每轮检查收到的回复,由角色判断是否回复,无需逐条回复。',
2947
3765
  onChange: (v) => patch({ replyToComments: v }, '自动回复评论'),
2948
3766
  }),
2949
3767
  sw('replyToComments') ? h('div', { 'data-bd-sub': '' },
2950
- h('div', { 'data-bd-note': '' }, '想回就回,没有条数上限,也可以不理。同一条只回一次 —— 它记着账本,不会对着一个人刷屏。')) : null,
2951
- h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => patch({ momentStyle: momentStyle || '感想', momentPerDay: clampField('momentPerDay', momentPerDay, 0, 50) }, '动态设置') }, '保存动态设置')),
3768
+ h('div', { 'data-bd-note': '' }, '无固定条数上限;是否回复由角色自主判断。同一条评论只回复一次,避免对同一对象连续重复回复。')) : null,
3769
+ h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => patch({ momentStyle: momentStyle || '随心情', momentPerDay: clampField('momentPerDay', momentPerDay, 0, 50) }, '动态设置') }, '保存动态设置')),
2952
3770
  h('section', { 'data-bd-sec': '' }, heading('key', '私信与投递'), ToggleRow({
2953
3771
  label: '私信投递', on: dmMode !== 'off' && dmMode !== null, disabled: Boolean(busy),
2954
- note: '可选择私信或同时投递;填写你的收件 UID 后保存。',
3772
+ note: '将精选结果发送到指定 UID,可选择仅私信或同时投递。',
2955
3773
  onChange: (v) => setDmMode(v ? 'both' : 'off'),
2956
3774
  }),
2957
3775
  dmMode !== 'off' && dmMode !== null ? h('div', { 'data-bd-sub': '' },
2958
3776
  h('div', { 'data-bd-row': '' },
2959
3777
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '收件人 UID'),
2960
3778
  h('input', {
2961
- 'data-bd-input': '', style: { width: '150px' }, placeholder: '数字 UID',
3779
+ 'data-bd-input': '', 'aria-label': '收件人 UID', type: 'text', inputMode: 'numeric', pattern: '[0-9]*', style: { width: '150px' }, placeholder: '数字 UID',
2962
3780
  value: dmUid === null ? '' : dmUid, onChange: (e) => setDmUid(e.target.value),
2963
3781
  })),
2964
3782
  h('div', { 'data-bd-row': '' },
2965
3783
  h('span', { 'data-bd-label': '', 'data-bd-grow': '' }, '方式'),
2966
3784
  h('select', {
2967
- 'data-bd-input': '', style: { width: '150px' },
3785
+ 'data-bd-input': '', 'aria-label': '投递方式', style: { width: '150px' },
2968
3786
  value: dmMode || 'off', onChange: (e) => setDmMode(e.target.value),
2969
3787
  },
2970
- h('option', { value: 'off' }, '只发评论区'),
2971
- h('option', { value: 'only' }, '只发私信'),
2972
- h('option', { value: 'both' }, '两边都发')))) : null,
3788
+ h('option', { value: 'off' }, '仅评论区'),
3789
+ h('option', { value: 'only' }, '仅私信'),
3790
+ h('option', { value: 'both' }, '评论区与私信')))) : null,
2973
3791
  /* 自动回复私信:跟「私信投递」是两件事(那是往外发成果,这是回别人发来的) */
2974
3792
  ToggleRow({
2975
3793
  label: '自动回复私信', on: sw('autoReplyDm'), disabled: Boolean(busy),
2976
- note: '别人私信你,它就按自己那套脾气回一句。想回就回,不回也不算欠着。',
3794
+ note: '根据新收到的私信判断是否回复,遵循角色设定,允许不回复。',
2977
3795
  onChange: (v) => patch({ autoReplyDm: v }, '自动回复私信'),
2978
3796
  }),
2979
3797
  sw('autoReplyDm') ? h('div', { 'data-bd-sub': '' },
2980
3798
  h('div', { 'data-bd-note': '' },
2981
- `没有每日条数上限 —— 想回就回。同一条私信只回一次;新鲜事只回 ${DM_STALE_DAYS} 天内的,晾着的旧消息不去翻。`),
3799
+ `无固定每日条数上限;是否回复由角色自主判断。同一条私信只回复一次,仅处理 ${DM_STALE_DAYS} 天内的消息。`),
2982
3800
  h('div', { 'data-bd-row': '' },
2983
3801
  h('div', { 'data-bd-grow': '' },
2984
- h('div', { 'data-bd-label': '' }, '只回我关注的人'),
2985
- h('div', { 'data-bd-note': '' }, '开着更安全:陌生人私信多半是广告引流,一律不回;关掉就是谁都回。')),
3802
+ h('div', { 'data-bd-label': '' }, '仅回复已关注的用户'),
3803
+ h('div', { 'data-bd-note': '' }, '开启后,仅回复账号已关注的用户;关闭后可处理其他用户的新私信。是否回复仍由角色判断。')),
2986
3804
  h(Switch, {
2987
- on: Boolean(sw('dmReplyFollowedOnly')), disabled: Boolean(busy),
2988
- onChange: (v) => patch({ dmReplyFollowedOnly: v }, '只回我关注的人'),
3805
+ on: Boolean(sw('dmReplyFollowedOnly')), label: '仅回复已关注的用户', disabled: Boolean(busy),
3806
+ onChange: (v) => patch({ dmReplyFollowedOnly: v }, '仅回复已关注的用户'),
2989
3807
  }))) : null,
2990
3808
  h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: () => patch({ dmMode: dmMode || 'off', dmUid: String(dmUid || '').trim() }, '私信投递设置') }, '保存投递设置')),
2991
- basicCards[4](), h('section', { 'data-bd-sec': '' }, heading('pulse', '此刻能不能发'), (() => {
3809
+ basicCards[4](), h('section', { 'data-bd-sec': '' }, heading('pulse', '发送状态'), (() => {
2992
3810
  const pc = st && st.pace
2993
- if (!pc || pc.ok === false) return h('div', { 'data-bd-note': '' }, '节奏读不到(可能要重启一次让新版生效)')
3811
+ if (!pc || pc.ok === false) return h('div', { 'data-bd-note': '' }, '发送节奏读取失败。可重启 DSH 后重新加载。')
2994
3812
  const cd = pc.cooldown
2995
3813
  const qt = pc.quota
2996
- const ks = pc.kinds || {}
2997
- /* 🗨️ 先说人话(2026-10-28):把节奏账翻译成一句有期待感的总结,技术读数放后面 */
2998
- const waits = Object.values(ks).map((v) => Number(v.waitSec) || 0)
2999
- const maxWait = waits.length ? Math.max(0, ...waits) : 0
3814
+ const ks = pc.explanation?.kinds || pc.kinds || {}
3815
+ const ready = Object.entries(ks).filter(([, v]) => Number(v.waitSec) <= 0).map(([, v]) => v.label)
3816
+ /* 当前可发送时间只取后端账本;草稿预览不能把已钉下的等待重算。 */
3817
+ const waits = Object.values(ks).map((v) => Number(v.waitSec) || 0).filter((sec) => sec > 0)
3818
+ const firstWait = waits.length ? Math.min(...waits) : 0
3000
3819
  const summary = cd
3001
- ? `她在小黑屋冷静,约 ${Math.ceil((Number(cd.waitSec) || 0) / 60)} 分钟后回来 —— 这段时间别催。`
3820
+ ? `风控冷却中,预计 ${Math.ceil((Number(cd.waitSec) || 0) / 60)} 分钟后可重新评估发送。`
3002
3821
  : qt && Object.keys(qt).length
3003
- ? '评论额度用完了,她先安静看片,到点自己再探。'
3004
- : maxWait > 0
3005
- ? `下一次出手大约 ${Math.ceil(maxWait / 60)} 分钟后 —— 现在她在安静看片。`
3006
- : '现在出手很顺,她想发就发。'
3822
+ ? '视频评论额度暂满,其他通道按各自限制独立判断。'
3823
+ : ready.length > 0 && ready.length === Object.keys(ks).length
3824
+ ? '当前各通道均满足时间与额度条件,实际发送仍需相应权限。'
3825
+ : ready.length
3826
+ ? '部分通道满足发送条件,其余通道的等待时间见下方。'
3827
+ : firstWait > 0
3828
+ ? `预计最早 ${Math.ceil(firstWait / 60)} 分钟后有通道可用,具体限制按通道执行。`
3829
+ : '正在读取发送状态。'
3007
3830
  const qRow = qt && Object.keys(qt).length
3008
3831
  ? (() => {
3009
3832
  const q = Object.values(qt)[0] || {}
3010
3833
  const m = Math.max(1, Math.ceil((Number(q.waitSec) || 0) / 60))
3011
3834
  const strike = Number(q.strikes) || 1
3012
3835
  return h('div', { 'data-bd-msg': '', 'data-warn': '' },
3013
- `📉 评论额度满了(12014):先歇 ${m} 分钟,到点自动再探一次`
3014
- + `(第 ${strike} 次,再撞会停更久:45 分 → 90 分 → 180 分封顶)。`
3015
- + '这不是风控,是账号的评论额度用完了(越撞停得越久)—— 回私信、回动态评论、发动态都不受影响。')
3836
+ `视频评论额度已满(12014),本次暂停 ${m} 分钟后允许重新评估`
3837
+ + `(第 ${strike} 次触发,连续触发的暂停时长为 45 分 → 90 分 → 180 分)。`
3838
+ + '该额度仅限制视频评论及其回复;私信、动态评论回复和动态发布不受此项额度影响。')
3016
3839
  })()
3017
3840
  : null
3018
3841
  const rows = Object.entries(ks).map(([k, v]) => {
3019
3842
  const wait = Number(v.waitSec) || 0
3020
- return h('div', { 'data-bd-kv': '', key: k },
3021
- `${v.label || k}:${wait > 0 ? '还要等 ' + (wait >= 90 ? Math.ceil(wait / 60) + ' 分钟' : wait + ' 秒') : '现在可以'}`
3022
- + ` · 间隔 ${v.gapSec}~${Number(v.gapSec) + Number(v.jitterSec)} 秒 · 每小时最多 ${v.hourCap} 条`)
3843
+ const reason = { cooldown: '风控冷却', quota: '视频评论额度限制', burst: '连续发送限制', hour: '滚动小时上限', gap: '已保存的发送间隔' }[v.blockedBy]
3844
+ return h('div', { 'data-bd-kv': '', 'data-bd-pace-current': k, key: k },
3845
+ `${v.label || k}:${wait > 0 ? '需等待 ' + (wait >= 90 ? Math.ceil(wait / 60) + ' 分钟' : wait + ' 秒') : '当前可发送'}`
3846
+ + (wait > 0 && reason ? `(${reason})` : '')
3847
+ + ` · 滚动小时已用 ${v.usedThisHour || 0}/${v.hourCap} 条`)
3023
3848
  })
3024
3849
  const burst = pc.burst || {}
3025
3850
  return h('div', { 'data-bd-sub': '' },
3026
3851
  h('div', { 'data-bd-msg': '', 'data-good': !cd && !(qt && Object.keys(qt).length) ? '' : undefined }, summary),
3027
3852
  cd ? h('div', { 'data-bd-msg': '', 'data-warn': '' },
3028
- `🚧 正在冷却:${cd.why || '被风控拦了'}`
3029
- + `(${cd.code || '?'})—— 还要等 ${Math.ceil((Number(cd.waitSec) || 0) / 60)} 分钟。`
3030
- + '这段别让它试探性再发,风控是越试越久的。') : null,
3853
+ `风控冷却:${cd.why || '触发平台风控'}`
3854
+ + `(${cd.code || '?'}),剩余约 ${Math.ceil((Number(cd.waitSec) || 0) / 60)} 分钟。`
3855
+ + '冷却期间暂停发送,不进行试探性重试。') : null,
3031
3856
  qRow,
3032
3857
  h('div', { 'data-bd-msg': '', 'data-good': '' },
3033
- `现在可以发:${Object.entries(ks).filter(([, v]) => Number(v.waitSec) <= 0).map(([, v]) => v.label).join('、') || '都还得等等'}`),
3858
+ `可发送通道:${ready.join('、') || '暂无可发送通道'}`),
3034
3859
  ...rows,
3035
3860
  h('div', { 'data-bd-note': '' },
3036
- `连发刹车:${burst.windowMin || 10} 分钟里最多 ${burst.max || 10} 次出手(不管哪一类,已经用了 ${burst.used || 0} 次)—— `
3037
- + '风控看的是"这个号在动",不看动的是哪一种。'))
3861
+ `连续发送限制:每 ${burst.windowMin || 10} 分钟,各通道合计最多 ${burst.max || 10} 次(已使用 ${burst.used || 0} 次)。`
3862
+ + '此限制按账号合并计算。发送结果未知时应先核对,不重复发送。'))
3038
3863
  })(),
3039
3864
  h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: async () => {
3040
3865
  setBusy('verify'); try { const r = await jpost('/verify', {}); if (!r?.ok) throw new Error(r?.error || '复核失败'); const rows = Array.isArray(r.checked) ? r.checked : []; say(rows.length ? '复核 ' + rows.length + ' 条,公开可见 ' + rows.filter(x => x.visible === true).length + ' 条。' : r.text || '没有需要复核的发言', 'good') } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
3041
3866
  } }, '复核发言'))))
3867
+ const weeklyRecoveryCard = ({ showReview = true, showRecovery = true } = {}) => {
3868
+ const retro = st?.retrospective || {}
3869
+ const days = Array.isArray(retro.days) ? retro.days : []
3870
+ const totals = retro.totals || {}
3871
+ const themes = Array.isArray(retro.themes) ? retro.themes : []
3872
+ const changes = retro.changes || {}
3873
+ const recovery = st?.recovery || {}
3874
+ const rows = Array.isArray(recovery.rows) ? recovery.rows : []
3875
+ const maxPicks = Math.max(1, ...days.map((row) => row.picks == null ? 0 : Number(row.picks) || 0))
3876
+ const changeGroups = changes.available
3877
+ ? [['新增', changes.new], ['延续', changes.continued], ['减少', changes.decreased]].map(([label, items]) => h('div', { key: label, 'data-bd-week-change-row': '' },
3878
+ h('b', {}, label),
3879
+ h('div', { 'data-bd-week-change-chips': '' }, ...(Array.isArray(items) && items.length ? items : [{ clue: '无', current: 0, previous: 0, sources: [] }]).map((item) => h('span', {
3880
+ key: item.clue, title: (item.sources || []).map((source) => `${source.day} ${source.bvid} ${source.title}`).join('\n'),
3881
+ }, item.clue === '无' ? '无' : `${item.clue} ${item.previous}→${item.current}`)))))
3882
+ : [h('div', { key: 'unavailable', 'data-bd-note': '' }, '两个 7 天窗口没有同时具备日报证据,暂不生成变化判断。')]
3883
+ return h('section', { 'data-bd-week-recovery': '', 'aria-label': '七天回顾与任务恢复' },
3884
+ showReview ? h('div', { 'data-bd-week-card': '' },
3885
+ h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, icon('list'), h('b', {}, '最近 7 天')), h('span', { 'data-bd-badge': '' }, `${Number(totals.activeDays) || 0}/7 天有记录`)),
3886
+ h('div', { 'data-bd-kpi': '' },
3887
+ h('div', {}, h('b', {}, Number(totals.reportDays) > 0 ? String(Number(totals.picks) || 0) : '—'), h('span', {}, '精选')),
3888
+ h('div', {}, h('b', {}, String(Number(totals.runs) || 0)), h('span', {}, '已记任务')),
3889
+ h('div', {}, h('b', {}, Number(totals.tokenDays) > 0 ? Number(totals.tokens || 0).toLocaleString() : '—'), h('span', {}, 'token'))),
3890
+ days.length ? h('div', { 'data-bd-week-days': '', 'aria-label': '七天精选数量' }, ...days.map((row) => h('div', { key: row.day, 'data-bd-week-day': '', 'data-on': row.report || (row.tokens ?? 0) > 0 || row.runs ? '' : undefined, 'data-missing': row.picks == null ? '' : undefined, title: `${row.day} · 精选 ${row.picks == null ? '无日报记录' : row.picks} · 任务 ${row.runs || 0} · token ${row.tokenKnown ? row.tokens : '无计量记录'}` },
3891
+ h('b', {}, row.picks == null ? '—' : String(row.picks)), h('i', { style: { height: row.picks == null ? '8px' : Math.max(4, Math.round((Number(row.picks) || 0) / maxPicks * 42)) + 'px' } }), h('span', {}, String(row.day || '').slice(5))))) : h('div', { 'data-bd-note': '' }, '还没有七天记录。'),
3892
+ themes.length ? h('div', { 'data-bd-week-themes': '' }, ...themes.map((theme) => h('span', { key: theme.title, title: `${theme.title} · ${(theme.sources || []).map((source) => source.day + ' ' + source.bvid).join('、')}` }, theme.title + (theme.count > 1 ? ` ×${theme.count}` : '')))) : h('div', { 'data-bd-note': '' }, retro.note || '有日报后,这里会按真实精选标题显示最近主题。'),
3893
+ h('div', { 'data-bd-week-change': '' },
3894
+ h('div', { 'data-bd-row': '' }, h('b', { 'data-bd-grow': '' }, '主题变化'), h('span', { 'data-bd-note': '' }, `${changes.label || '标题线索 / 非兴趣结论'} · 本周日报 ${Number(changes.coverage?.currentReportDays) || 0} 天 / 上周 ${Number(changes.coverage?.previousReportDays) || 0} 天`)),
3895
+ ...changeGroups),
3896
+ h('div', { 'data-bd-note': '' }, '数据来自本地日报、当前任务账及 token 日账;不含已轮转任务。')) : null,
3897
+ showRecovery ? h('div', { 'data-bd-recovery-card': '' },
3898
+ h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, icon('pulse'), h('b', {}, '恢复中心')), recovery.needsAction ? h('span', { 'data-bd-badge': '', 'data-warn': '' }, '需要处理') : h('span', { 'data-bd-badge': '', 'data-live': '' }, '状态清楚')),
3899
+ h('div', { 'data-bd-recovery-list': '' }, ...(rows.length ? rows : [{ id: 'loading', state: 'scheduled', label: '正在读取任务状态', detail: '稍后自动刷新', action: 'run', actionLabel: '查看运行方式' }]).map((row) => h('div', { key: row.id, 'data-bd-recovery-row': '', 'data-state': row.state || '' },
3900
+ h('i', {}), h('div', { 'data-bd-recovery-copy': '' }, h('strong', {}, row.label || '任务状态'), h('span', {}, row.detail || '')),
3901
+ h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => {
3902
+ if (row.action === 'background') { setRunMode('background'); setTab('run'); return }
3903
+ setTab(row.action === 'video' ? 'video' : row.action === 'logs' ? 'logs' : 'run')
3904
+ } }, row.actionLabel || '查看')))),
3905
+ h('div', { 'data-bd-note': '' }, '运行状态由宿主确认。视频仅在音视频校验通过后标记完成。')) : null)
3906
+ }
3907
+ const runPlan = () => {
3908
+ const c = cfg || {}
3909
+ const f = st?.feiyu || {}
3910
+ const bg = st?.backgroundRun || {}
3911
+ const sessionOn = Boolean(f.active)
3912
+ const backgroundOn = Boolean(st?.bgBusy)
3913
+ const windowOn = Boolean(st?.bgWindow?.start && st?.bgWindow?.end)
3914
+ const active = sessionOn || backgroundOn
3915
+ const mode = sessionOn ? '当前会话' : backgroundOn ? (bg.task === 'learn-review' ? '后台学习审核' : '后台连续冲浪') : windowOn ? '定时窗口' : '未安排'
3916
+ const schedule = c.scheduleEnabled ? (c.scheduleAllDay ? '全天检查' : `${c.scheduleStart || '18:00'}–${c.scheduleEnd || '09:00'}`) : '未开启'
3917
+ const selectedBackground = backgroundOn || (!sessionOn && runMode === 'background')
3918
+ const budgetValue = sessionOn ? f.tokens?.budget : backgroundOn ? bg.tokenBudget : selectedBackground ? c.backgroundTokenBudget : c.sessionTokenBudget
3919
+ const budget = Number(budgetValue) > 0 ? Number(budgetValue).toLocaleString() + ' token / 趟' : '本趟不限(受总闸约束)'
3920
+ return h('section', { 'data-bd-sec': '', 'data-bd-run-plan': '' },
3921
+ h('div', { 'data-bd-row': '' }, h('div', { 'data-bd-h': '', 'data-bd-grow': '' }, icon('pulse'), h('b', {}, '运行计划')), h('span', { 'data-bd-badge': '', 'data-live': active ? '' : undefined }, active ? '正在执行' : '待命')),
3922
+ h('div', { 'data-bd-run-plan-grid': '' },
3923
+ h('div', {}, h('span', {}, '现在'), h('strong', {}, mode)),
3924
+ h('div', {}, h('span', {}, '下一步'), h('strong', {}, sessionOn ? '按当前目标续轮' : backgroundOn ? '按时段或本趟时限运行' : Number(st?.bgWindow?.holdUntil) > Date.now() ? '本时段已手动停止自动拉起' : windowOn ? '到窗口时自动接力' : '等待手动开始')),
3925
+ h('div', {}, h('span', {}, '模型'), h('strong', {}, sessionOn ? f.modelActive || f.model || '当前会话模型' : backgroundOn ? bg.model || '后台模型' : (selectedBackground ? c.backgroundModelId : c.sessionModelId) || '沿用宿主默认')),
3926
+ h('div', {}, h('span', {}, '预算'), h('strong', {}, budget)),
3927
+ h('div', {}, h('span', {}, '定时'), h('strong', {}, schedule)),
3928
+ h('div', {}, h('span', {}, '完成反馈'), h('strong', {}, selectedBackground ? '桌宠提醒 + 运行日志' : '当前会话 + 运行日志'))),
3929
+ h('div', { 'data-bd-note': '' }, active ? '这项任务不会因为切换设置页而停止;后台任务完成或失败后会回写结果。' : '当前会话、后台连续刷、定时窗口分别保存,互相不会覆盖参数。'))
3930
+ }
3042
3931
  const runView = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'run' },
3043
- pageHead('运行方式', '会话、后台分开保存。定时规则归在后台方式里。'),
3044
- h('div', { 'data-bd-segments': '', 'aria-label': '运行方式' }, ...[['session','新开会话刷'], ['background','后台与定时']].map(([mode, label]) => h('button', { key: mode, 'aria-pressed': runMode === mode, onClick: () => setRunMode(mode) }, label))),
3045
- runPage(runMode), runMode === 'background' ? scheduleCard() : null)
3932
+ pageHead('运行方式', '会话运行、后台运行与定时计划。'),
3933
+ st.toolScope?.checked && !st.toolScope.supported ? h('div', { 'data-bd-msg': '', 'data-warn': '', role: 'alert' }, '当前 DSH 不支持会话工具隔离。会话运行不可用,后台运行和本地管理仍可使用。') : null,
3934
+ h('div', { 'data-bd-run-launches': '' }, ...['session', 'background'].map(mode => {
3935
+ const isSession = mode === 'session', options = runSettings(mode)
3936
+ const active = isSession ? Boolean(st.feiyu?.active) : Boolean(st.bgBusy)
3937
+ const title = isSession ? '新开会话刷' : '后台刷'
3938
+ return h('section', { key: mode, 'data-bd-run-launch': mode },
3939
+ h('div', { 'data-bd-row': '' }, icon(isSession ? 'play' : 'moon'), h('h2', { 'data-bd-grow': '' }, title), h('span', { 'data-bd-badge': '', 'data-live': active ? '' : undefined }, active ? '运行中' : '未运行')),
3940
+ h('p', {}, isSession ? '在独立会话中运行,保留对话与执行进度。' : '在后台执行,支持连续时段与每日定时计划。'),
3941
+ h('div', { 'data-bd-launch-meta': '' }, h('span', { title: options.model || '' }, options.model || (isSession ? '当前会话模型' : '宿主默认模型')), h('span', {}, Number(options.tokenBudget) > 0 ? Number(options.tokenBudget).toLocaleString() + ' token' : '不设单次 token 上限')),
3942
+ h('button', { 'data-bd-btn': '', 'data-primary': '', 'data-bd-launch-action': mode, disabled: Boolean(busy) || active || (isSession && st.toolScope?.checked && !st.toolScope.supported),
3943
+ onClick: () => startRun(mode) }, busy === 'start-' + mode ? '正在启动…' : isSession ? '新开会话刷' : '开始后台刷'))
3944
+ })),
3945
+ h('div', { 'data-bd-segments': '', role: 'group', 'aria-label': '运行方式' }, ...[['session','会话参数'], ['background','后台与定时']].map(([mode, label]) => h('button', { key: mode, 'aria-pressed': runMode === mode, onClick: () => setRunMode(mode) }, label))),
3946
+ runPage(runMode), runMode === 'background' ? scheduleCard() : null,
3947
+ h('details', { 'data-bd-run-details': '', open: Boolean(st.recovery?.needsAction) }, h('summary', {}, '运行计划与恢复'), runPlan(), weeklyRecoveryCard({ showReview: false })))
3046
3948
  const logsPage = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'logs' },
3047
- pageHead('运行日志', '实时动态与运行事件,保留完整上下文。列表内滚动查看。'),
3949
+ pageHead('运行日志', '近期活动、工具调用与运行事件。'),
3950
+ h('section', { 'data-bd-sec': '', 'data-bd-log-summary': '' },
3951
+ h('div', { 'data-bd-h': '' }, icon('pulse'), h('b', {}, '近期活动摘要')),
3952
+ h('p', {}, (() => {
3953
+ const rows = Array.isArray(acts) ? acts : []
3954
+ if (!rows.length) return '暂无近期活动记录。'
3955
+ const tools = new Map()
3956
+ for (const row of rows) if (['call', 'done', 'fail'].includes(row.kind)) tools.set(row.id || row.uid || row.seq, row)
3957
+ const latest = [...tools.values()]
3958
+ const done = latest.filter(row => row.kind === 'done').length
3959
+ const running = latest.filter(row => row.kind === 'call').length
3960
+ const failed = latest.filter(row => row.kind === 'fail').length
3961
+ return `最近 ${rows.length} 条可见记录中,工具动作已完成 ${done} 次、进行中 ${running} 次、失败 ${failed} 次。`
3962
+ + (st.feiyu?.active || st.bgBusy ? '任务仍在运行。' : '当前没有运行中的刷站任务。') + '这里是最近片段,不是全天合计。'
3963
+ })()),
3964
+ h('div', { 'data-bd-row': '' }, h('span', { 'data-bd-note': '', 'data-bd-grow': '' }, st.petEvent?.text || '暂无任务结果。'), h('button', { 'data-bd-btn': '', 'data-sm': '', onClick: () => setTab('review') }, '数据回顾'))),
3048
3965
  h('div', { 'data-bd-row': '' },
3049
3966
  h('div', { 'data-bd-segments': '', 'aria-label': '日志类型' }, ...[['events','运行事件'], ['activity','实时动态']].map(([mode, label]) => h('button', { key: mode, 'aria-pressed': logMode === mode, onClick: () => setLogMode(mode) }, label))),
3050
3967
  h('span', { 'data-bd-grow': '' }),
@@ -3052,8 +3969,8 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3052
3969
  st.bgBusy ? h('button', { 'data-bd-btn': '', disabled: Boolean(busy), onClick: async () => { setBusy('stop-bg'); try { const r = await jpost('/pet/stop', {}); if (!r?.ok) throw new Error(r?.error || '停止失败'); say('已请求停止后台任务', 'good') } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') } } }, '停止后台任务') : null),
3053
3970
  logMode === 'events' ? settingLogCard() : basicCards[5]())
3054
3971
  const videoPage = () => h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'video' },
3055
- h('div', { 'data-bd-pghead': '' }, icon('film'), h('div', {}, h('b', {}, '视频'),
3056
- h('p', {}, '制作日报、查看成品和配置配音。'))), pvCard())
3972
+ h('div', { 'data-bd-pghead': '' }, icon('film'), h('div', {}, h('b', {}, '视频工作室'),
3973
+ h('p', {}, '视频制作、制作记录与本地成片。'))), pvCard())
3057
3974
  const otherPage = () => h(PetOtherSettings, { view: tab === 'learning' ? 'learning' : 'environment', st, setSt, busy, setBusy, say, patch, login, loginState,
3058
3975
  loginTone, onLoginStart, onInstall, onCheck, onInstallAsr, copyText, sw })
3059
3976
  const ledgerPage = () => h(PetTokenLedger, {
@@ -3064,7 +3981,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3064
3981
  try { const fresh = await jget('/status'); if (fresh?.ok) setSt(fresh) } catch { /* 下次状态轮询会补上 */ }
3065
3982
  },
3066
3983
  })
3067
- return modalShell(tab === 'basic' ? basicPage() : tab === 'run' ? runView() : tab === 'speech' ? speechPage() : tab === 'logs' ? logsPage()
3984
+ return modalShell(tab === 'review' ? h('div', { 'data-bd-page-content': '', 'data-bd-settings-page': 'review' }, pageHead('数据回顾', '近期精选、用量与主题记录。'), todayPage(), weeklyRecoveryCard({ showRecovery: false })) : tab === 'basic' ? basicPage() : tab === 'run' ? runView() : tab === 'speech' ? speechPage() : tab === 'logs' ? logsPage()
3068
3985
  : tab === 'stickers' ? h(PetStickersPanel, { st, patch, busy, say }) : tab === 'video' ? videoPage() : tab === 'ledger' ? ledgerPage() : otherPage())
3069
3986
 
3070
3987
  }
@@ -3100,25 +4017,25 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3100
4017
  const row = (label, text) => h('div', { 'data-bd-preview-row': '', key: label }, h('span', {}, label), h('b', {}, text))
3101
4018
  const enabled = (v) => v ? '已开' : '已关'
3102
4019
  const activity = (Array.isArray(st?.activity) ? st.activity : []).slice(-3)
3103
- const title = { session: '新开会话刷', background: '后台刷一轮', logs: '最近的动静', status: '鲸鱼摸鱼状态', settings: '设置工作台', hide: '藏一会儿' }[kind]
4020
+ const title = { session: '新开会话刷', background: '后台刷', logs: '运行记录', status: '运行状态', settings: '工作台', hide: '隐藏桌宠' }[kind]
3104
4021
  const body = kind === 'session' ? [
3105
4022
  row('模型', cfg.sessionModelId || cfg.modelId || '沿用当前会话模型'), row('本趟预算', petFmtWan(cfg.sessionTokenBudget ?? cfg.tokenBudget)), row('最多轮数', value(cfg.sessionMaxRounds ?? cfg.maxRounds, ' 轮')),
3106
- h('p', { key: 'note' }, '点击后新开会话,沿用当前工作区与权限。双击鱼也能开始。'),
4023
+ h('p', { key: 'note' }, '继承来源工作区与权限,采用已保存的会话参数。'),
3107
4024
  ] : kind === 'background' ? [
3108
4025
  row('现在', st?.bgBusy ? bg.task === 'learn-review' ? '正在审核学习队列' : '后台运行中' : '空闲'), row('模型', bg.active ? bg.model : cfg.backgroundModelId || '沿用宿主默认模型'),
3109
4026
  row('刷多久', (cfg.backgroundWindowStart && cfg.backgroundWindowEnd) ? cfg.backgroundWindowStart + '–' + cfg.backgroundWindowEnd + ' 内一直刷' : value(cfg.backgroundMinutes, ' 分钟')), row('本轮 token', st?.bgBusy ? tokenValue : '尚未运行'),
3110
- h('p', { key: 'note' }, '点击展开后台参数与启动按钮。运行过程可在日志中查看。'),
4027
+ h('p', { key: 'note' }, '后台参数与启动控制;执行过程保存在运行日志中。'),
3111
4028
  ] : kind === 'logs' ? [
3112
4029
  ...(activity.length ? activity.map((item, i) => h('p', { key: i, 'data-bd-preview-line': '' }, String(item.text || item.summary || '活动记录').slice(0, 180))) : [h('p', { key: 'empty' }, '状态快照里还没有近期动态。点击可看完整运行日志。')]),
3113
- h('p', { key: 'note' }, '进入后可切换运行事件与实时动态。悬停预览沿用已有状态。'),
4030
+ h('p', { key: 'note' }, '包含运行事件与实时活动。预览使用已有状态快照。'),
3114
4031
  ] : kind === 'status' ? [
3115
4032
  row('登录', !st ? fails >= 3 ? '暂时读不到' : '读取中' : st.loggedIn ? '已登录' : '未登录'), row('当前', fy.active ? '会话里刷着呢' : st?.bgBusy ? '后台跑着呢' : '歇着'), row('今天精选', value(st?.pickedToday ?? fy.pickedToday, ' 条')),
3116
4033
  row('本轮 token', tokenValue), h('p', { key: 'note' }, tokenNote),
3117
4034
  ] : kind === 'settings' ? [
3118
- h('p', { key: 'pages' }, '偏好与桌宠、运行方式、互动与投递、表情收藏、学习与记忆、token 账本、运行日志、日报视频、账号与环境 九个页签 —— 左侧栏按「日常使用 / 记录与成长 / 制作与维护」分组。'),
4035
+ h('p', { key: 'pages' }, '运行方式、互动与投递、记忆与素材、视频工作室、记录与用量、偏好与设置,共六组。'),
3119
4036
  row('自动评论', enabled(cfg.autoComment)), row('自动收表情', enabled(cfg.stickerAutoCollect)), row('自动配图', enabled(cfg.stickerAutoSend)),
3120
- h('p', { key: 'note' }, '点击打开工作台,每张卡独立保存。'),
3121
- ] : [h('p', { key: 'hide' }, '把鱼暂时藏起来,运行中的任务会继续。'), h('p', { key: 'return' }, '右下角会留下一个小圆点,点一下就能叫回来。')]
4037
+ h('p', { key: 'note' }, '各配置项独立保存。'),
4038
+ ] : [h('p', { key: 'hide' }, '隐藏桌宠不影响正在运行的任务。'), h('p', { key: 'return' }, '右下角保留桌宠恢复入口。')]
3122
4039
  return h('aside', { ref: box, id: 'bd-menu-preview-' + kind, 'data-bd-preview': kind, role: 'note', 'aria-label': title + '预览', style: placement ? { position: 'fixed', ...placement } : { position: 'fixed', visibility: 'hidden', width: 296 }, onMouseEnter: onEnter, onMouseLeave: onLeave },
3123
4040
  h('small', { 'data-bd-preview-eyebrow': '' }, '鲸鱼摸鱼 · 速览'), h('b', { 'data-bd-preview-title': '' }, title), ...body)
3124
4041
  }
@@ -3136,7 +4053,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3136
4053
  const [panel, setPanel] = React.useState(null) // null | menu | bg | log | status | settings(互斥)
3137
4054
  const [dragging, setDragging] = React.useState(false)
3138
4055
  const [sessBusy, setSessBusy] = React.useState(false)
3139
- const [settingsTab, setSettingsTab] = React.useState('basic')
4056
+ const [settingsTab, setSettingsTab] = React.useState('run')
3140
4057
  const [bubbles, setBubbles] = React.useState([])
3141
4058
  const [toasts, toast] = useToasts()
3142
4059
  const drag = React.useRef(null)
@@ -3144,6 +4061,9 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3144
4061
  const hiddenRef = React.useRef(false)
3145
4062
  const closeSettings = React.useRef(() => setPanel(null)).current
3146
4063
  const [preview, setPreview] = React.useState(null)
4064
+ const [reaction, setReaction] = React.useState(null)
4065
+ const companion = st?.companion || { silenced: Boolean(st?.config?.focusMode), notificationLevel: st?.config?.notificationLevel || 'normal', reducedMotion: Boolean(st?.config?.petReducedMotion) }
4066
+ const reducedMotion = Boolean(companion.reducedMotion)
3147
4067
  const previewTimers = React.useRef({ open: null, close: null })
3148
4068
  const cancelPreviewTimers = () => {
3149
4069
  clearTimeout(previewTimers.current.open); clearTimeout(previewTimers.current.close)
@@ -3180,9 +4100,10 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3180
4100
  * 只在「跨越」的那一刻响一次(ref 记上次值),不会每 5 秒重复吵人。 */
3181
4101
  const lastTitleRef = React.useRef('')
3182
4102
  React.useEffect(() => {
4103
+ if (!st || st.tokensTotal === null || st.tokensTotal === undefined || !Number.isFinite(Number(st.tokensTotal))) return
3183
4104
  const total = Number(st && st.tokensTotal) || 0
3184
4105
  const title = tokTitle(total)
3185
- if (total > 0 && lastTitleRef.current && title !== lastTitleRef.current) {
4106
+ if (total > 0 && lastTitleRef.current && title !== lastTitleRef.current && !companion.silenced && companion.notificationLevel !== 'minimal') {
3186
4107
  toast(`称号升级:${lastTitleRef.current} → ${title}(一共吃下 ${total.toLocaleString()} token)`, 'good')
3187
4108
  }
3188
4109
  lastTitleRef.current = title
@@ -3204,15 +4125,23 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3204
4125
  const mood = (st && st.mood) || 'neutral'
3205
4126
  /* 🗨️ 桌宠说话:宿主把收工小结/闲聊/提醒写进 st.petEvent —— at 变了就冒 toast。
3206
4127
  * 首见不冒(ref 从 0 起步),免得一打开就播昨天旧账。 */
3207
- const lastPetEventRef = React.useRef(0)
4128
+ const lastPetEventRef = React.useRef(null)
3208
4129
  React.useEffect(() => {
3209
4130
  const ev = st && st.petEvent
3210
- if (!ev || !Number(ev.at)) return
3211
- if (lastPetEventRef.current && Number(ev.at) !== lastPetEventRef.current) {
3212
- toast(String(ev.text || ''), ev.tone === 'warn' ? 'warn' : ev.tone === 'good' ? 'good' : 'info')
4131
+ if (!st) return
4132
+ if (!ev || !Number(ev.at)) { if (lastPetEventRef.current === null) lastPetEventRef.current = 0; return }
4133
+ if (lastPetEventRef.current !== null && Number(ev.at) !== lastPetEventRef.current) {
4134
+ const important = ev.tone === 'warn' || ev.tone === 'good' || ev.kind === 'complete'
4135
+ if (important || (!companion.silenced && companion.notificationLevel !== 'minimal')) {
4136
+ toast(String(ev.text || ''), ev.tone === 'warn' ? 'warn' : ev.tone === 'good' ? 'good' : 'info')
4137
+ }
4138
+ if (important || !companion.silenced) setReaction(ev.tone === 'warn'
4139
+ ? { kind: 'concerned', symbol: '…', label: '需要看一眼' }
4140
+ : ev.tone === 'good' ? { kind: 'celebrate', symbol: '✦', label: '收好这一趟' }
4141
+ : { kind: 'curious', symbol: '♪', label: '冒个小泡' })
3213
4142
  }
3214
4143
  lastPetEventRef.current = Number(ev.at)
3215
- }, [st && st.petEvent && st.petEvent.at])
4144
+ }, [Boolean(st), st && st.petEvent && st.petEvent.at])
3216
4145
  /* 需要你:出现新的待办时提一句(同一条只提一次,事由变了再说一遍) */
3217
4146
  const lastAttentionRef = React.useRef('')
3218
4147
  React.useEffect(() => {
@@ -3221,6 +4150,39 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3221
4150
  lastAttentionRef.current = key
3222
4151
  }, [attention.join('|')])
3223
4152
 
4153
+ // Compare real observations. The first snapshot never replays yesterday's milestones.
4154
+ const previousPetSnapshot = React.useRef(null)
4155
+ React.useEffect(() => {
4156
+ if (!st) return
4157
+ const current = { approved: Number(st.learn?.approved) || 0, picked: Number(st.pickedToday) || 0,
4158
+ running: Boolean(st.feiyu?.active), background: Boolean(st.bgBusy), day: String(st.today || ''),
4159
+ cooldown: String(st.pace?.cooldown?.until || '') }
4160
+ const before = previousPetSnapshot.current
4161
+ previousPetSnapshot.current = current
4162
+ if (!before || companion.silenced) return
4163
+ let cue = null, message = '', important = false
4164
+ if (current.cooldown && current.cooldown !== before.cooldown) {
4165
+ cue = { kind: 'concerned', symbol: '☂', label: '先等一等' }
4166
+ message = '发送进入冷却,先只看不发。具体等待时间在「互动与投递」里。'; important = true
4167
+ } else if (current.approved > before.approved) {
4168
+ cue = { kind: 'learned', symbol: '✧', label: '又懂了一点' }
4169
+ message = `新批准了 ${current.approved - before.approved} 条经验,来源和撤销入口都在记忆中心。`
4170
+ } else if (current.day === before.day && current.picked > before.picked) {
4171
+ cue = { kind: 'curious', symbol: '!', label: '有新精选' }
4172
+ message = `今天的精选多了 ${current.picked - before.picked} 条,理由留在报告里。`
4173
+ } else if (current.running !== before.running || current.background !== before.background) {
4174
+ cue = current.running || current.background ? { kind: 'working', symbol: '≈', label: '出发找好玩的' } : { kind: 'resting', symbol: '·', label: '歇一会儿' }
4175
+ if (companion.notificationLevel === 'all') message = current.running || current.background ? '开始冲浪了,过程在运行日志里。' : '运行状态变了,暂停或结束的原因可以在运行方式里查看。'
4176
+ }
4177
+ if (cue) setReaction(cue)
4178
+ if (message && (important || companion.notificationLevel !== 'minimal')) toast(message, important ? 'warn' : 'info')
4179
+ }, [st, companion.silenced, companion.notificationLevel])
4180
+ React.useEffect(() => {
4181
+ if (!reaction) return
4182
+ const timer = setTimeout(() => setReaction(null), 4800)
4183
+ return () => clearTimeout(timer)
4184
+ }, [reaction])
4185
+
3224
4186
  /* 吐泡泡:隔 5~9 秒吐 1~2 个上浮消散;页面不可见或桌宠被藏时不吐(白画一场) */
3225
4187
  React.useEffect(() => {
3226
4188
  let dead = false
@@ -3251,14 +4213,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3251
4213
  * 失败逐级 toast(error 与 hint 都要说出来);期间按钮 disabled + 转圈,绝不双发。 */
3252
4214
  const runNewSession = async () => {
3253
4215
  if (sessLock.current) return
4216
+ if (st?.toolScope?.checked && !st.toolScope.supported) { toast('当前 DSH 不支持会话工具隔离,无法启动会话运行。', 'warn'); return }
3254
4217
  sessLock.current = true
3255
4218
  setSessBusy(true)
3256
4219
  try {
3257
4220
  let r1 = null
3258
4221
  try { r1 = await jpost('/pet/session-new', newSessionContext(ctx)) }
3259
- catch (e) { toast('开不了会话:' + String(e?.message ?? e), 'warn'); return }
4222
+ catch (e) { toast('创建会话失败:' + String(e?.message ?? e), 'warn'); return }
3260
4223
  if (!r1 || r1.ok === false) {
3261
- toast('开不了会话:' + ((r1 && r1.error) || '未知原因') + (r1 && r1.hint ? ' —— ' + r1.hint : ''), 'warn')
4224
+ toast('创建会话失败:' + ((r1 && r1.error) || '未知原因') + (r1 && r1.hint ? ';' + r1.hint : ''), 'warn')
3262
4225
  return
3263
4226
  }
3264
4227
  /* 切过去看它刷:uiWorkspace 是宿主服务,可能没有/名字可能不同 ⇒ 全程防御式;
@@ -3275,7 +4238,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3275
4238
  toast('开刷没成:' + ((r2 && r2.error) || '未知原因') + (r2 && r2.hint ? ' —— ' + r2.hint : ''), 'warn')
3276
4239
  return
3277
4240
  }
3278
- toast('开冲!它在新会话里刷起来了 —— 点鱼随时看状态', 'good')
4241
+ toast('会话任务已启动。', 'good')
3279
4242
  setPanel(null)
3280
4243
  } finally { sessLock.current = false; setSessBusy(false) }
3281
4244
  }
@@ -3385,7 +4348,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3385
4348
  if (!pos) return null
3386
4349
  if (hidden) {
3387
4350
  return h('button', {
3388
- 'data-bd-pet-dot': '', type: 'button', title: '鲸鱼摸鱼藏这儿了 · 点一下叫回来',
4351
+ 'data-bd-pet-dot': '', type: 'button', title: '显示鲸鱼摸鱼桌宠',
3389
4352
  style: { right: '18px', bottom: '18px' }, onClick: showPet,
3390
4353
  })
3391
4354
  }
@@ -3400,11 +4363,11 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3400
4363
  : 'idle'
3401
4364
  const moodLabel = { curious: '好奇', content: '满足', glum: '有点蔫', neutral: '平静' }[mood] || '平静'
3402
4365
  const auraTitle = {
3403
- idle: '鲸鱼摸鱼:歇着(点我弹菜单)',
3404
- run: '在刷~(点我看状态卡)',
3405
- bg: '后台那轮跑着呢…',
3406
- attention: '需要你:' + (attention[0] || '有要你拿主意的事'),
3407
- err: st && st.loggedIn === false ? '还没登录 B站 —— 菜单里进设置扫码' : '读不到状态(插件没起来?)',
4366
+ idle: '鲸鱼摸鱼 · 空闲',
4367
+ run: '鲸鱼摸鱼 · 会话运行中',
4368
+ bg: '鲸鱼摸鱼 · 后台运行中',
4369
+ attention: '待处理:' + (attention[0] || '存在待处理事项'),
4370
+ err: st && st.loggedIn === false ? 'B站未登录' : '插件状态暂不可用',
3408
4371
  }[aura]
3409
4372
  /* 卡片默认从鱼上方弹出;上方装不下就翻到下方 —— JS 算好挂属性,CSS 消费。
3410
4373
  * 判据按「实际可用空间」算,不能写死 y<380:窗口矮的时候(笔记本 + 分屏)
@@ -3436,15 +4399,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3436
4399
  h('span', {}, h('div', {}, label), note ? h('i', {}, note) : null),
3437
4400
  spinning ? h('i', { 'data-bd-pet-spin': '' }) : null)
3438
4401
  return h('div', { 'data-bd-pet-card': '', 'data-bd-menu': '', style: { maxHeight: Math.max(80, (flip ? spaceBelow : spaceAbove) - 8) + 'px', maxWidth: Math.max(160, vw - 20) + 'px' } },
3439
- h('div', { 'data-bd-pet-cardhead': '' }, h('b', {}, '鲸鱼摸鱼'), h('span', {}, '右键菜单 · 双击开刷')),
4402
+ h('div', { 'data-bd-pet-cardhead': '' }, h('b', {}, 'DeepMoYu'), h('span', {}, '鲸鱼摸鱼')),
3440
4403
  /* 副标题:将用的模型/预算 —— 取设置里的 modelId/tokenBudget(/status 的 config 镜像) */
3441
4404
  item('session', 'play', '新开会话刷',
3442
4405
  sessBusy ? '正在开会话…' : `${modelTxt} · 预算 ${petFmtWan(cfg.sessionTokenBudget ?? cfg.tokenBudget)}`,
3443
4406
  () => { setPanel(null); runNewSession() }, sessBusy, sessBusy),
3444
- item('background', 'moon', '后台刷一轮', '', () => setPanel('bg')),
3445
- item('logs', 'list', '运行日志', '', () => { setSettingsTab('logs'); setPanel('settings') }),
3446
- item('status', 'pulse', '状态卡', '', () => setPanel('status')),
3447
- item('settings', 'gear', '设置', '', () => { setSettingsTab('basic'); setPanel('settings') }),
4407
+ item('background', 'moon', '后台刷', '运行时长与定时参数', () => setPanel('bg')),
4408
+ item('logs', 'list', '运行记录', '', () => { setSettingsTab('logs'); setPanel('settings') }),
4409
+ item('status', 'pulse', '运行状态', '', () => setPanel('status')),
4410
+ item('settings', 'gear', '工作台', '', () => { setSettingsTab('run'); setPanel('settings') }),
3448
4411
  h('hr', { 'data-bd-pet-sep': '' }),
3449
4412
  item('hide', 'hide', '隐藏桌宠', '', hidePet),
3450
4413
  h('footer', { 'data-bd-menu-token': '' },
@@ -3463,11 +4426,11 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3463
4426
  onPointerUp: onUp, onPointerCancel: onUp,
3464
4427
  onDoubleClick: onDbl, onContextMenu: onCtx,
3465
4428
  }, [
3466
- h(PetFish, { key: 'fish', aura, bubbles, title: auraTitle }),
4429
+ h(PetFish, { key: 'fish', aura, bubbles, title: auraTitle, reaction, reducedMotion }),
3467
4430
  /* 🎨⭐ 心情/需要-你小灯:attention 时是急促的琥珀,平时按 soul 的心情换颜色 —— 纯显示层 */
3468
4431
  h('i', {
3469
4432
  'data-bd-pet-mood': '', 'data-mood': attention.length ? 'attention' : mood,
3470
- title: attention.length ? `需要你:${attention[0]}` : `心情:${moodLabel}(来自她的小账本,只是显示)`,
4433
+ title: attention.length ? `待处理:${attention[0]}` : `心情:${moodLabel}(来源:角色记忆)`,
3471
4434
  }),
3472
4435
  /* ⚠️ 卡片与面板必须挂在 [data-bd-pet] 里面,不能是它的兄弟:
3473
4436
  [data-bd-pet-card] 的 bottom:calc(100% + 14px) 要拿 128px 的鱼盒当参照,
@@ -3479,7 +4442,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3479
4442
  panel === 'log' ? h(PetLogPanel, { toast }) : null,
3480
4443
  panel === 'status' ? h(PetStatusCard, { st, fails }) : null,
3481
4444
  panel === 'settings' ? h(StablePetSettingsModal, { onClose: closeSettings, toast, ctx, status: [st, setSt, fails], initialTab: settingsTab }) : null,
3482
- h('div', { key: 'toast', 'data-bd-pet-toast': '' },
4445
+ h('div', { key: 'toast', 'data-bd-pet-toast': '', role: 'status', 'aria-live': 'polite', 'aria-atomic': false },
3483
4446
  toasts.map((t) => h('div', { key: t.id, 'data-tone': t.tone || undefined }, t.text))),
3484
4447
  ]))
3485
4448
  }
@@ -3534,7 +4497,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
3534
4497
  return {
3535
4498
  /* ⚠️ 2026-10-26 起 **不再往 slots 注册任何东西**(使用者要求「鲸鱼摸鱼的设置不要出现在
3536
4499
  * DSH 的默认设置里」)。整页设置搬进了桌宠的 ⚙️ 设置弹窗(PetSettingsModal 的
3537
- * 五个 tab,按功能分组且不重复挂旧设置页),所以 inject 也清空。
4500
+ * 六组导航与组内子页,按功能分组且不重复挂旧设置页),所以 inject 也清空。
3538
4501
  * 改这条要连带改 selfcheck.mjs 的 slot 断言与 render-panel.cjs 的取组件方式。 */
3539
4502
  inject: [],
3540
4503
  apply(ctx) {