dsh-long-plugins 2.2.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js CHANGED
@@ -529,17 +529,27 @@ window.__ModuleLoader__.load({
529
529
  }
530
530
  }
531
531
  }
532
- window.addEventListener('dragenter', onDragEnter, true)
533
- window.addEventListener('dragover', onDragOver, true)
534
- window.addEventListener('dragleave', onDragLeave, true)
535
- window.addEventListener('drop', onDrop, true)
536
- window.addEventListener('paste', onPaste, true)
532
+ const dragOn = !!(window.__dshLongMod && window.__dshLongMod('uploadDragDrop'))
533
+ const pasteOn = !!(window.__dshLongMod && window.__dshLongMod('uploadPaste'))
534
+ if (dragOn) {
535
+ window.addEventListener('dragenter', onDragEnter, true)
536
+ window.addEventListener('dragover', onDragOver, true)
537
+ window.addEventListener('dragleave', onDragLeave, true)
538
+ window.addEventListener('drop', onDrop, true)
539
+ }
540
+ if (pasteOn) {
541
+ window.addEventListener('paste', onPaste, true)
542
+ }
537
543
  return () => {
538
- window.removeEventListener('dragenter', onDragEnter, true)
539
- window.removeEventListener('dragover', onDragOver, true)
540
- window.removeEventListener('dragleave', onDragLeave, true)
541
- window.removeEventListener('drop', onDrop, true)
542
- window.removeEventListener('paste', onPaste, true)
544
+ if (dragOn) {
545
+ window.removeEventListener('dragenter', onDragEnter, true)
546
+ window.removeEventListener('dragover', onDragOver, true)
547
+ window.removeEventListener('dragleave', onDragLeave, true)
548
+ window.removeEventListener('drop', onDrop, true)
549
+ }
550
+ if (pasteOn) {
551
+ window.removeEventListener('paste', onPaste, true)
552
+ }
543
553
  }
544
554
  }, [controller, sessionId])
545
555
 
@@ -1367,25 +1377,52 @@ window.__ModuleLoader__.load({
1367
1377
  }
1368
1378
 
1369
1379
  function apply(ctx) {
1370
- const controller = new FileDraftController(ctx)
1371
- ctx.effect(() => () => controller.dispose(), 'dsh-upload-manager: draft-file state')
1372
- ctx.effect(() => ctx.inputTriggers.registerSource({
1373
- trigger: '@',
1374
- name: SOURCE,
1375
- order: 10_000,
1376
- candidates: async () => [],
1377
- onPick: () => undefined,
1378
- codec: {
1379
- clipboardText: () => '',
1380
- serialize: async (ref) => {
1381
- const file = controller.fileForRef(ref)
1382
- if (!file) throw new Error('待发送文件已失效,请重新上传')
1383
- controller.markSerializing(ref)
1384
- return serializedFile(file)
1380
+ const mod = window.__dshLongMod || (() => true)
1381
+ const anyUpload = mod('uploadAttach') || mod('uploadDragDrop') || mod('uploadPaste')
1382
+ if (anyUpload) {
1383
+ const controller = new FileDraftController(ctx)
1384
+ ctx.effect(() => () => controller.dispose(), 'dsh-upload-manager: draft-file state')
1385
+ ctx.effect(() => ctx.inputTriggers.registerSource({
1386
+ trigger: '@',
1387
+ name: SOURCE,
1388
+ order: 10_000,
1389
+ candidates: async () => [],
1390
+ onPick: () => undefined,
1391
+ codec: {
1392
+ clipboardText: () => '',
1393
+ serialize: async (ref) => {
1394
+ const file = controller.fileForRef(ref)
1395
+ if (!file) throw new Error('待发送文件已失效,请重新上传')
1396
+ controller.markSerializing(ref)
1397
+ return serializedFile(file)
1398
+ },
1385
1399
  },
1386
- },
1387
- }), 'dsh-upload-manager: hidden file reference codec')
1388
-
1400
+ }), 'dsh-upload-manager: hidden file reference codec')
1401
+ // 输入框附件上传(回形针) + 待发送文件栏:由 uploadAttach 控制
1402
+ if (mod('uploadAttach')) {
1403
+ ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
1404
+ name: 'conversation.input.left',
1405
+ id: 'local-file-upload',
1406
+ order: -20,
1407
+ label: '上传文件',
1408
+ }, (props) => React.createElement(UploadControl, { ...props, controller })))
1409
+ ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
1410
+ name: 'conversation.input.dock',
1411
+ id: 'local-file-upload-rail',
1412
+ order: 80,
1413
+ label: '待发送文件',
1414
+ }, (props) => React.createElement(PendingFileRail, { ...props, controller })))
1415
+ }
1416
+ // 拖放上传(遮罩+window 监听) / 粘贴上传:由各自开关控制
1417
+ if (mod('uploadDragDrop') || mod('uploadPaste')) {
1418
+ ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
1419
+ name: 'conversation.input.dock',
1420
+ id: 'local-file-upload-dropzone',
1421
+ order: 90,
1422
+ label: '拖放/粘贴上传',
1423
+ }, (props) => React.createElement(DragDropOverlay, { ...props, controller })))
1424
+ }
1425
+ }
1389
1426
  ctx.effect(() => {
1390
1427
  const style = document.createElement('style')
1391
1428
  style.dataset.plugin = 'dsh-file-uploads'
@@ -1393,41 +1430,24 @@ window.__ModuleLoader__.load({
1393
1430
  document.head.appendChild(style)
1394
1431
  return () => style.remove()
1395
1432
  }, 'dsh-upload-manager: styles')
1396
-
1397
- ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
1398
- name: 'conversation.input.left',
1399
- id: 'local-file-upload',
1400
- order: -20,
1401
- label: '上传文件',
1402
- }, (props) => React.createElement(UploadControl, { ...props, controller })))
1403
-
1404
- ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
1405
- name: 'conversation.input.dock',
1406
- id: 'local-file-upload-rail',
1407
- order: 80,
1408
- label: '待发送文件',
1409
- }, (props) => React.createElement(PendingFileRail, { ...props, controller })))
1410
-
1411
- ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
1412
- name: 'conversation.input.dock',
1413
- id: 'local-file-upload-dropzone',
1414
- order: 90,
1415
- label: '拖放上传',
1416
- }, (props) => React.createElement(DragDropOverlay, { ...props, controller })))
1417
-
1418
- ctx.slots.inject('settings.section', () => ctx.slots.register({
1419
- name: 'settings.section',
1420
- id: 'uploaded-files',
1421
- order: 30,
1422
- label: '上传文件',
1423
- }, UploadSettingsSection))
1424
-
1425
- ctx.slots.inject('settings.section', () => ctx.slots.register({
1426
- name: 'settings.section',
1427
- id: 'output-files',
1428
- order: 35,
1429
- label: '输出文件',
1430
- }, WorkspaceFilesSection))
1433
+ // 上传文件预览/管理设置区:由 uploadPreview 控制
1434
+ if (mod('uploadPreview')) {
1435
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
1436
+ name: 'settings.section',
1437
+ id: 'uploaded-files',
1438
+ order: 30,
1439
+ label: '上传文件',
1440
+ }, UploadSettingsSection))
1441
+ }
1442
+ // 输出文件(工作区)设置区:由 workspace 控制
1443
+ if (mod('workspace')) {
1444
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
1445
+ name: 'settings.section',
1446
+ id: 'output-files',
1447
+ order: 35,
1448
+ label: '输出文件',
1449
+ }, WorkspaceFilesSection))
1450
+ }
1431
1451
  }
1432
1452
  return { apply, inject }
1433
1453
  })()
@@ -1961,7 +1981,9 @@ window.__ModuleLoader__.load({
1961
1981
  if (!cancelled) setBalance(null);
1962
1982
  }
1963
1983
  };
1964
- load();
1984
+ // 首次拉取延迟到浏览器空闲(减轻首屏网络风暴), 60s 轮询不变
1985
+ const schedule = () => { try { if (window.requestIdleCallback) window.requestIdleCallback(() => load(), { timeout: 300 }); else setTimeout(load, 300) } catch (_) { load() } };
1986
+ schedule();
1965
1987
  const interval = setInterval(load, 60000);
1966
1988
  return () => {
1967
1989
  cancelled = true;
@@ -2003,7 +2025,9 @@ window.__ModuleLoader__.load({
2003
2025
  if (!cancelled) setCost(null);
2004
2026
  }
2005
2027
  };
2006
- load();
2028
+ // 首次拉取延迟到空闲(减轻首屏网络风暴), 60s 轮询不变
2029
+ const schedule = () => { try { if (window.requestIdleCallback) window.requestIdleCallback(() => load(), { timeout: 300 }); else setTimeout(load, 300) } catch (_) { load() } };
2030
+ schedule();
2007
2031
  const interval = setInterval(load, 60000);
2008
2032
  return () => {
2009
2033
  cancelled = true;
@@ -2058,18 +2082,21 @@ window.__ModuleLoader__.load({
2058
2082
  const inject = ["slots", "locale"];
2059
2083
  /** Register the token-usage card into the sidebar footer. */
2060
2084
  function apply(ctx) {
2061
- injectMobileCss();
2085
+ injectMobileCss(); // 移动端布局 CSS 始终注入(与余额开关解耦)
2062
2086
  ctx.effect(() => ctx.locale.register(NS, {
2063
2087
  zh,
2064
2088
  en
2065
2089
  }), "dsh-token-usage: dictionaries");
2066
- ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
2067
- name: "conversation.composer.dock",
2068
- id: "token-balance",
2069
- order: -10,
2070
- locale: NS,
2071
- inject: () => ({})
2072
- }, BalanceChip));
2090
+ // 余额 chip:仅当"余额"模块开启时注册(禁用余额时保留移动布局)
2091
+ if (!(window.__dshLongModules && window.__dshLongModules.balance === false)) {
2092
+ ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
2093
+ name: "conversation.composer.dock",
2094
+ id: "token-balance",
2095
+ order: -10,
2096
+ locale: NS,
2097
+ inject: () => ({})
2098
+ }, BalanceChip));
2099
+ }
2073
2100
  }
2074
2101
  return { apply, inject }
2075
2102
  })()
@@ -2433,6 +2460,11 @@ window.__ModuleLoader__.load({
2433
2460
  .dsh-turn-preview{width:min(360px,88vw);height:min(70vh,520px);top:50%!important;left:50%!important;transform:translate(-50%,-50%)!important}
2434
2461
 
2435
2462
  }
2463
+ /* 会话导航开关(off 时隐藏轮次刻尺/手机把手):与 DSH 原生回合导航避免冲突 */
2464
+ html[data-dsh-turnruler="off"] .dsh-turn-ruler,
2465
+ html[data-dsh-turnruler="off"] .dsh-turn-phone-tab {
2466
+ display: none !important;
2467
+ }
2436
2468
  `
2437
2469
  const turnRulerPlugin = {
2438
2470
  inject: [],
@@ -2463,6 +2495,11 @@ window.__ModuleLoader__.load({
2463
2495
  let scrollAnim = 0
2464
2496
  let scrollTarget = -1
2465
2497
  let scrollFrom = 0
2498
+ // 会话导航开关:从 glass 配置读取 turnRuler;false=off 隐藏刻尺(避免与 DSH 原生回合导航冲突)
2499
+ ;(() => {
2500
+ const setVis = (en) => { try { document.documentElement.setAttribute('data-dsh-turnruler', en ? 'on' : 'off') } catch (_) {} }
2501
+ fetch('/api/dsh-uploads/glass-config', { cache: 'no-store' }).then((r) => r.json()).then((b) => { if (b && b.cfg) setVis((b.cfg.modules && b.cfg.modules.turnRuler) !== false) }).catch(() => {})
2502
+ })()
2466
2503
 
2467
2504
  const ensureRuler = () => {
2468
2505
  if (ruler && ruler.isConnected) return ruler
@@ -3133,6 +3170,7 @@ window.__ModuleLoader__.load({
3133
3170
  color: #ffffff !important;
3134
3171
  text-shadow: 0 1px 1px rgba(0,0,0,.5);
3135
3172
  }
3173
+ /* (移除 color/text-shadow 过渡: 会在收尾重渲染时短暂贴默认色导致"闪黑") */
3136
3174
  /* DSH 弹窗/浮层(提问/确认/重命名等):给不透明底+主题文字, 杜绝透明与会话内容重叠 */
3137
3175
  html[data-dsh-glass="on"] [class*="Radix"] [class*="Content"],
3138
3176
  html[data-dsh-glass="on"] [class*="DialogContent"],
@@ -3152,10 +3190,10 @@ window.__ModuleLoader__.load({
3152
3190
  backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
3153
3191
  -webkit-backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
3154
3192
  }
3155
- /* 任务/todo 面板(lXshSW_root, 在 conversation.input.dock 内):不透明底+可读文字, 不透明与会话重叠 */
3193
+ /* 任务/todo 面板(lXshSW_root, 在 conversation.input.dock 内):半透明磨砂底+可读文字 */
3156
3194
  html[data-dsh-glass="on"] [class*="lXshSW_root"],
3157
3195
  html[data-dsh-glass="on"] [class*="lXshSW_body"] {
3158
- background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 76%, transparent) !important;
3196
+ background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 62%, transparent) !important;
3159
3197
  color: var(--dsw-alias-label-primary) !important;
3160
3198
  backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
3161
3199
  -webkit-backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
@@ -3165,6 +3203,19 @@ window.__ModuleLoader__.load({
3165
3203
  html[data-dsh-glass="on"] [class*="lXshSW_item"] {
3166
3204
  color: var(--dsw-alias-label-primary) !important;
3167
3205
  }
3206
+ /* 排队消息/等待任务面板(_7yHdaG_dock/panel, 在 conversation.input.dock 内):不透明/磨砂底+可读文字 */
3207
+ html[data-dsh-glass="on"] [class*="_7yHdaG_panel"],
3208
+ html[data-dsh-glass="on"] [class*="_7yHdaG_dock"] {
3209
+ background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 62%, transparent) !important;
3210
+ color: var(--dsw-alias-label-primary) !important;
3211
+ backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
3212
+ -webkit-backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
3213
+ }
3214
+ html[data-dsh-glass="on"] [class*="_7yHdaG_header"],
3215
+ html[data-dsh-glass="on"] [class*="_7yHdaG_preview"],
3216
+ html[data-dsh-glass="on"] [class*="_7yHdaG_row"] {
3217
+ color: var(--dsw-alias-label-primary) !important;
3218
+ }
3168
3219
  /* 轮次/历史提问预览:默认隐藏(点开才显示),并去掉头部底色,避免底部露出白条 */
3169
3220
  .dsh-turn-preview:not(.open){display:none !important;}
3170
3221
  .dsh-turn-preview-head{background:transparent !important;}
@@ -3224,18 +3275,27 @@ window.__ModuleLoader__.load({
3224
3275
  backdrop-filter: none !important;
3225
3276
  -webkit-backdrop-filter: none !important;
3226
3277
  }
3227
- /* 输入框旁权限/功能选择下拉(盾牌 Full access 等 Radix 风格菜单):与 / 命令菜单同样处理 */
3228
- html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_root_"]:has([class*="_list_"]) {
3229
- background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 76%, transparent) !important;
3230
- background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 97%, transparent) !important;
3278
+ /* 输入框旁权限/功能选择下拉(盾牌 Full access 等):给不透明底+可读文字(高优先级, 压过 DSH 透明覆盖) */
3279
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_root_"] [class*="_list_"],
3280
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_root_"] [class*="_list_"] [class*="_viewport_"],
3281
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_list_"] [class*="_viewport_"] [class*="_itemWrap_"] {
3282
+ background-color: var(--dsw-specific-input-major, #e8edf3) !important;
3231
3283
  backdrop-filter: none !important;
3232
3284
  -webkit-backdrop-filter: none !important;
3233
3285
  }
3234
- html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_list_"] {
3235
- background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 76%, transparent) !important;
3236
- background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 97%, transparent) !important;
3286
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_item_"],
3287
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_itemLabel_"] {
3288
+ color: var(--dsw-alias-label-primary) !important;
3289
+ }
3290
+ /* 兜底: Radix 风格菜单/下拉(可能被渲染到 body 顶层) */
3291
+ html[data-dsh-glass="on"] [class*="Radix"] [class*="Menu"],
3292
+ html[data-dsh-glass="on"] [class*="radix-menu"],
3293
+ html[data-dsh-glass="on"] [class*="menuitem"],
3294
+ html[data-dsh-glass="on"] [role="menu"] {
3295
+ background-color: var(--dsw-specific-input-major, #e8edf3) !important;
3237
3296
  backdrop-filter: none !important;
3238
3297
  -webkit-backdrop-filter: none !important;
3298
+ color: var(--dsw-alias-label-primary) !important;
3239
3299
  }
3240
3300
  /* 玻璃开启 + 浅色系统主题:加深弱化标签/元信息文字,避免浅灰字叠在浅磨砂卡上看不清 */
3241
3301
  @media (prefers-color-scheme: light) {
@@ -3245,6 +3305,8 @@ window.__ModuleLoader__.load({
3245
3305
  --dsw-alias-label-tertiary: #50545b !important;
3246
3306
  }
3247
3307
  }
3308
+ `
3309
+ const GLASS_LAYOUT_CSS = `
3248
3310
  .dsh-glass-section{display:flex;flex-direction:column;gap:16px;min-width:0;padding:4px 2px 24px;color:var(--dsw-alias-label-primary)}
3249
3311
  .dsh-glass-head{display:flex;align-items:center;justify-content:space-between;gap:16px}
3250
3312
  .dsh-glass-head h2{margin:0;font-size:20px;line-height:28px}
@@ -3319,6 +3381,80 @@ window.__ModuleLoader__.load({
3319
3381
  ),
3320
3382
  )
3321
3383
  }
3384
+ // 「dsh-long」设置区:各模块开关(独立、移动端干净),避免把开关塞进 RA-Span
3385
+ const DSH_LONG_MODULES = [
3386
+ ['glass', 'RA-Span'],
3387
+ ['turnRuler', '会话导航(轮次刻尺)'],
3388
+ ['uploadAttach', '附件上传(回形针)'],
3389
+ ['uploadDragDrop', '附件拖放上传'],
3390
+ ['uploadPaste', '附件粘贴上传'],
3391
+ ['uploadPreview', '上传文件预览/管理'],
3392
+ ['skillDocs', '技能文档'],
3393
+ ['balance', '账户余额'],
3394
+ ['workspace', '输出文件预览/管理'],
3395
+ ['mobile', '移动端布局'],
3396
+ ]
3397
+ const DshLongSettingsSection = () => {
3398
+ const [state, setState] = React.useState({ loading: true, saving: false, error: '', modules: null, patches: null })
3399
+ const load = React.useCallback(async () => {
3400
+ setState((s) => ({ ...s, loading: true, error: '', patches: null }))
3401
+ try {
3402
+ const [r, pr] = await Promise.all([
3403
+ fetch('/api/dsh-uploads/glass-config', { cache: 'no-store' }),
3404
+ fetch('/api/dsh-uploads/patch-status', { cache: 'no-store' }),
3405
+ ])
3406
+ const b = await r.json()
3407
+ if (!r.ok) throw new Error(b.error || ('HTTP ' + r.status))
3408
+ let patches = null
3409
+ try { const pb = await pr.json(); patches = pb.patches } catch (_) {}
3410
+ setState((s) => ({ ...s, loading: false, modules: b.cfg.modules || {}, patches }))
3411
+ } catch (e) { setState((s) => ({ ...s, loading: false, error: String((e && e.message) || e) })) }
3412
+ }, [])
3413
+ React.useEffect(() => { load(); }, [load])
3414
+ const setMod = React.useCallback((k, v) => setState((s) => ({ ...s, modules: { ...(s.modules || {}), [k]: v } })), [])
3415
+ const save = React.useCallback(async () => {
3416
+ setState((s) => ({ ...s, saving: true, error: '' }))
3417
+ try {
3418
+ const r = await fetch('/api/dsh-uploads/glass-config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ modules: state.modules }) })
3419
+ const b = await r.json()
3420
+ if (!r.ok) throw new Error(b.error || ('HTTP ' + r.status))
3421
+ window.__dshLongModules = state.modules
3422
+ try { localStorage.setItem('dsh-long:modules', JSON.stringify(state.modules)) } catch (e) {}
3423
+ setState((s) => ({ ...s, saving: false, error: '' }))
3424
+ } catch (e) { setState((s) => ({ ...s, saving: false, error: String((e && e.message) || e) })) }
3425
+ }, [state.modules])
3426
+ const PATCH_LABELS = { trustedHosts: 'privileged 信任 trustedHosts', loopback: '反代域名视为 loopback', heartbeat: 'WebSocket 心跳' }
3427
+ const pstat = (st) => st === '已打' ? { text: '已打', cls: 'ok' } : (st === '原生无需' ? { text: '原生无需', cls: 'na' } : { text: '未打', cls: 'warn' })
3428
+ if (state.loading) return React.createElement('div', { className: 'dsh-glass-section' }, '加载中…')
3429
+ return React.createElement('div', { className: 'dsh-glass-section' },
3430
+ React.createElement('div', { className: 'dsh-glass-head' },
3431
+ React.createElement('div', null, React.createElement('h2', null, 'dsh-long'), React.createElement('p', null, '各模块开关:关掉某模块即禁用其功能(若新版 DSH 自带某项,可关掉插件对应功能避免冲突)。')),
3432
+ ),
3433
+ state.error ? React.createElement('div', { className: 'dsh-glass-error' }, state.error) : null,
3434
+ React.createElement('div', { className: 'dsh-glass-card' },
3435
+ DSH_LONG_MODULES.map(([k, label]) => React.createElement('div', { key: k, className: 'dsh-glass-row' },
3436
+ React.createElement('label', null, label),
3437
+ React.createElement('label', { className: 'dsh-glass-switch' }, React.createElement('input', { type: 'checkbox', checked: (state.modules ? state.modules[k] !== false : true), onChange: (e) => setMod(k, e.target.checked) }), (state.modules ? state.modules[k] !== false : true) ? '已开启' : '已关闭'),
3438
+ )),
3439
+ React.createElement('div', { className: 'dsh-glass-actions' },
3440
+ React.createElement('button', { type: 'button', onClick: load }, '重置'),
3441
+ React.createElement('button', { type: 'button', className: 'primary', disabled: state.saving, onClick: save }, state.saving ? '保存中…' : '保存生效'),
3442
+ ),
3443
+ ),
3444
+ state.patches ? React.createElement('div', { className: 'dsh-glass-card' },
3445
+ React.createElement('div', { className: 'dsh-glass-zone-title' }, '补丁状态(只读)'),
3446
+ typeof state.patches === 'string' ? React.createElement('p', { className: 'dsh-glass-note' }, state.patches)
3447
+ : Object.keys(PATCH_LABELS).map((k) => {
3448
+ const st = state.patches[k]
3449
+ const info = pstat(st)
3450
+ return React.createElement('div', { key: k, className: 'dsh-glass-row' },
3451
+ React.createElement('label', null, PATCH_LABELS[k]),
3452
+ React.createElement('span', { className: 'dsh-glass-val' + (info.cls === 'ok' ? '' : (info.cls === 'na' ? ' dsh-glass-na' : ' dsh-glass-warn')) }, info.text),
3453
+ )
3454
+ })
3455
+ ) : null,
3456
+ )
3457
+ }
3322
3458
  const GlassSettingsSection = () => {
3323
3459
  const [state, setState] = React.useState({ loading: true, saving: false, error: '', cfg: null, bgImage: '', bgList: [] })
3324
3460
  const load = React.useCallback(async () => {
@@ -3329,7 +3465,7 @@ window.__ModuleLoader__.load({
3329
3465
  if (!r.ok) throw new Error(b.error || ('HTTP ' + r.status))
3330
3466
  let list = []
3331
3467
  try { const rb = await fetch('/api/dsh-uploads/glass-backgrounds', { cache: 'no-store' }); const jb = await rb.json(); list = jb.list || [] } catch (_) {}
3332
- setState((s) => ({ ...s, loading: false, cfg: b.cfg, bgImage: b.bgImage || '', bgList: list }))
3468
+ setState((s) => ({ ...s, loading: false, cfg: b.cfg, bgImage: b.bgImageUrl || '', bgList: list }))
3333
3469
  } catch (e) { setState((s) => ({ ...s, loading: false, error: glassText(e) })) }
3334
3470
  }, [])
3335
3471
  React.useEffect(() => { load(); }, [load])
@@ -3352,9 +3488,16 @@ window.__ModuleLoader__.load({
3352
3488
  try {
3353
3489
  const cfg = state.cfg || {}
3354
3490
  const num = (v, fb) => ((typeof v === 'number' && Number.isFinite(v) && v >= 0) ? v : fb)
3491
+ // 背景图: 仅当用户上传了新图(data-URI)才传; 未改(URL)省略以保持引用; 清空传 '' 清除
3492
+ const sBg = state.bgImage
3493
+ let bgImageOut
3494
+ if (sBg === '') bgImageOut = ''
3495
+ else if (typeof sBg === 'string' && sBg.startsWith('data:image/')) bgImageOut = sBg
3496
+ else bgImageOut = undefined
3355
3497
  const payload = {
3356
3498
  enabled: !!cfg.enabled, blur: num(cfg.blur, 0),
3357
- bgImage: state.bgImage || cfg.bgImage || '', bgMask: num(cfg.bgMask, 0.28), bgColor: cfg.bgColor || '#1a2332', bgBlur: num(cfg.bgBlur, 0), bgOpacity: num(cfg.bgOpacity, 1),
3499
+ bgMask: num(cfg.bgMask, 0.28), bgColor: cfg.bgColor || '#1a2332', bgBlur: num(cfg.bgBlur, 0), bgOpacity: num(cfg.bgOpacity, 1),
3500
+ ...(bgImageOut !== undefined ? { bgImage: bgImageOut } : {}),
3358
3501
  bgTint: {
3359
3502
  light: { color: (cfg.bgTint?.light?.color) || '#1a2332', mask: num(cfg.bgTint?.light?.mask, 0.28) },
3360
3503
  dark: { color: (cfg.bgTint?.dark?.color) || '#1a2332', mask: num(cfg.bgTint?.dark?.mask, 0.28) },
@@ -3371,8 +3514,9 @@ window.__ModuleLoader__.load({
3371
3514
  const r = await fetch('/api/dsh-uploads/glass-config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
3372
3515
  const b = await r.json()
3373
3516
  if (!r.ok) throw new Error(b.error || ('HTTP ' + r.status))
3374
- window.__dshGlassApply && window.__dshGlassApply(payload)
3375
- setState((s) => ({ ...s, saving: false, cfg: { ...(s.cfg || {}), ...payload }, bgImage: payload.bgImage, error: '' }))
3517
+ const applyBg = (payload.bgImage && payload.bgImage.startsWith('data:')) ? payload.bgImage : ((state.bgImage && state.bgImage.startsWith('/')) ? state.bgImage : '')
3518
+ window.__dshGlassApply && window.__dshGlassApply({ ...payload, bgImage: applyBg })
3519
+ setState((s) => ({ ...s, saving: false, cfg: { ...(s.cfg || {}), ...payload }, bgImage: applyBg, error: '' }))
3376
3520
  } catch (e) { setState((s) => ({ ...s, saving: false, error: glassText(e) })) }
3377
3521
  }, [state])
3378
3522
  const cfg = state.cfg || {}
@@ -3466,7 +3610,7 @@ window.__ModuleLoader__.load({
3466
3610
  const style = document.createElement('style')
3467
3611
  style.dataset.plugin = 'dsh-long-plugins'
3468
3612
  style.dataset.pluginCss = 'dsh-long-plugins/glass'
3469
- style.textContent = GLASS_CSS
3613
+ style.textContent = GLASS_CSS // 玻璃视觉规则(仅 RA-Span 开启时注入)
3470
3614
  document.head.appendChild(style)
3471
3615
  return () => style.remove()
3472
3616
  }, 'dsh-long-plugins: glass styles')
@@ -3586,18 +3730,15 @@ window.__ModuleLoader__.load({
3586
3730
  if (isInSkip(root)) return
3587
3731
  root.querySelectorAll('div,span,button,a,label,p,li,h1,h2,h3,h4').forEach((el) => {
3588
3732
  if (isInSkip(el)) return
3589
- const tag = el.tagName
3590
- const hasText = el.textContent && el.textContent.trim()
3591
- if (!hasText) return
3733
+ if (el.dataset && el.dataset.dshgl) return // 已处理过, 跳过
3592
3734
  const ownText = Array.from(el.childNodes).some((n) => n.nodeType === 3 && n.textContent.trim())
3593
- const textLike = /^(BUTTON|A|SPAN|LABEL|LI|P|H1|H2|H3|H4)$/.test(tag)
3735
+ const textLike = /^(BUTTON|A|SPAN|LABEL|LI|P|H1|H2|H3|H4)$/.test(el.tagName)
3594
3736
  if (!textLike && !ownText) return
3595
- // 跳过有盒子背景的元素(badge/徽章/药丸等, deepseek 的 HARNESS 徽章)
3596
- // 这类文字颜色应随其盒子背景走主题, 不强制白字(否则白底徽章+白字不可见)
3597
- const bgbg = getComputedStyle(el).backgroundColor
3598
- if (bgbg && bgbg !== 'rgba(0, 0, 0, 0)' && bgbg !== 'transparent') return
3737
+ // 跳过有盒子背景的元素(用类名判断, 不再 getComputedStyle 以免每元素强制样式重算导致卡顿)
3738
+ if (/bubble|badge|chip|tag|pill|code-block|md-code|n_block|n_banner|n_plain|button/i.test(el.className)) return
3599
3739
  el.style.setProperty('color', '#ffffff', 'important')
3600
3740
  el.style.setProperty('text-shadow', '0 1px 1px rgba(0,0,0,.5)')
3741
+ if (el.dataset) el.dataset.dshgl = '1'
3601
3742
  })
3602
3743
  // 图标也变白(随 currentColor),用于工作区文件夹等单色图标;保留显式品牌色
3603
3744
  root.querySelectorAll('svg, [class*="icon"], [class*="Icon"]').forEach((el) => {
@@ -3622,15 +3763,14 @@ window.__ModuleLoader__.load({
3622
3763
  if (!scroll) return
3623
3764
  scroll.querySelectorAll('div,span,p,li,pre,code,button,a,label,h1,h2,h3,h4').forEach((el) => {
3624
3765
  if (isInSkip(el)) return
3625
- const hasText = el.textContent && el.textContent.trim()
3626
- if (!hasText) return
3766
+ if (el.dataset && el.dataset.dshgl) return // 已处理过, 跳过(只处理新内容)
3627
3767
  const ownText = Array.from(el.childNodes).some((n) => n.nodeType === 3 && n.textContent.trim())
3628
3768
  const textLike = /^(BUTTON|A|SPAN|LABEL|LI|P|H1|H2|H3|H4|PRE|CODE)$/.test(el.tagName)
3629
3769
  if (!textLike && !ownText) return
3630
- const bgbg = getComputedStyle(el).backgroundColor
3631
- if (bgbg && bgbg !== 'rgba(0, 0, 0, 0)' && bgbg !== 'transparent') return
3770
+ if (/bubble|badge|chip|tag|pill|code-block|md-code|n_block|n_banner|n_plain/i.test(el.className)) return
3632
3771
  el.style.setProperty('color', '#ffffff', 'important')
3633
3772
  el.style.setProperty('text-shadow', '0 1px 1px rgba(0,0,0,.5)')
3773
+ if (el.dataset) el.dataset.dshgl = '1'
3634
3774
  })
3635
3775
  // 在白字底上, 给过程内容配色区分, Error 用红色。
3636
3776
  // 文字常在标签的子元素里(白字已把它设白), 故连同文本子元素一起上色。
@@ -3726,19 +3866,22 @@ window.__ModuleLoader__.load({
3726
3866
  }
3727
3867
  // 主题相关的东西统一重算:背景罩 + 输入框。切主题时由 observer/mq 触发。
3728
3868
  // 写入前先断开观察器,写完再重连,避免自触发造成死循环。
3869
+ // 把非关键的"内容配色/白字"延迟到浏览器空闲时执行, 让页面先渲染出来(优化刷新加载时间)
3870
+ const defer = (fn) => { try { if (window.requestIdleCallback) window.requestIdleCallback(fn, { timeout: 120 }); else setTimeout(fn, 0) } catch (_) { fn() } }
3729
3871
  const applyTheme = () => {
3730
3872
  const obs = window.__dshGlassStyleObs
3731
3873
  if (obs) obs.disconnect()
3732
3874
  try {
3733
3875
  const tk = dshDark() ? 'dark' : 'light'
3734
3876
  root.setAttribute('data-dsh-theme', tk)
3877
+ root.setAttribute('data-dsh-turnruler', ((c.modules && c.modules.turnRuler) !== false) ? 'on' : 'off')
3878
+ window.__dshLongModules = c.modules || {}
3879
+ try { localStorage.setItem('dsh-long:modules', JSON.stringify(window.__dshLongModules)) } catch (e) {}
3735
3880
  const bt = bgTint(tk)
3736
3881
  const v = `rgba(${hexToRgb(bt.color)},${bt.mask})`
3737
3882
  root.style.setProperty('--dsh-glass-bg-zone', v)
3738
3883
  applyInputCard()
3739
- applyChromeText()
3740
- applySessionText()
3741
- fixDialogs()
3884
+ defer(() => { applyChromeText(); applySessionText(); fixDialogs() })
3742
3885
  } finally {
3743
3886
  if (obs) obs.observe(document.documentElement, { attributes: true, attributeFilter: ['style', 'class'] })
3744
3887
  }
@@ -3778,7 +3921,7 @@ window.__ModuleLoader__.load({
3778
3921
  // 侧边栏/顶栏 DOM 变化(会话列表更新等)时重算左栏/顶栏文字,防抖 120ms
3779
3922
  let chromeTimer = null
3780
3923
  const chromeRoots = [...document.querySelectorAll('#root [class*="sidebarCol"],#root [class*="wSkVaW_header"]')]
3781
- const chromeObs = new MutationObserver(() => { if (chromeTimer) clearTimeout(chromeTimer); chromeTimer = setTimeout(applyChromeText, 120) })
3924
+ const chromeObs = new MutationObserver(() => { if (chromeTimer) clearTimeout(chromeTimer); chromeTimer = setTimeout(applyChromeText, 30) })
3782
3925
  chromeRoots.forEach((r) => chromeObs.observe(r, { childList: true, subtree: true, characterData: true }))
3783
3926
  // 输入框卡片(uV2eYG_card/root)可能在启动后才挂载(会话/输入区加载晚),
3784
3927
  // 启动那一刻 applyInputCard 常找不到框 → 需刷新才生效。监听新增卡片节点时重算,防抖 100ms。
@@ -3796,7 +3939,7 @@ window.__ModuleLoader__.load({
3796
3939
  const sessionObs = new MutationObserver((muts) => {
3797
3940
  let hit = false
3798
3941
  for (const m of muts) { for (const node of m.addedNodes) { if (node.nodeType === 1 && inScroll(node)) { hit = true; break } } if (hit) break }
3799
- if (hit) { if (sessionTimer) clearTimeout(sessionTimer); sessionTimer = setTimeout(() => { applySessionText(); fixDialogs() }, 150) }
3942
+ if (hit) { if (sessionTimer) clearTimeout(sessionTimer); sessionTimer = setTimeout(() => { applySessionText(); fixDialogs() }, 20) }
3800
3943
  })
3801
3944
  sessionObs.observe(document.body, { childList: true, subtree: true })
3802
3945
  }
@@ -3817,9 +3960,11 @@ window.__ModuleLoader__.load({
3817
3960
  }, 'dsh-long-plugins: glass applier')
3818
3961
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'glass-ui', order: 40, label: 'RA-Span' }, GlassSettingsSection))
3819
3962
  ctx.effect(() => {
3820
- fetch('/api/dsh-uploads/glass-config', { cache: 'no-store' }).then((r) => r.json()).then((b) => {
3821
- if (b && b.cfg) { window.__dshGlassApply && window.__dshGlassApply({ ...b.cfg, bgImage: b.bgImage }) }
3822
- }).catch(() => {})
3963
+ // 玻璃配置/背景图历史拉取延迟到空闲, 减轻首屏网络风暴(背景没用玻璃时几乎零成本)
3964
+ const loadGlass = () => { fetch('/api/dsh-uploads/glass-config', { cache: 'no-store' }).then((r) => r.json()).then((b) => {
3965
+ if (b && b.cfg) { window.__dshGlassApply && window.__dshGlassApply({ ...b.cfg, bgImage: b.bgImageUrl || '' }) }
3966
+ }).catch(() => {}) }
3967
+ try { if (window.requestIdleCallback) window.requestIdleCallback(loadGlass, { timeout: 500 }); else setTimeout(loadGlass, 500) } catch (_) { loadGlass() }
3823
3968
  }, 'dsh-long-plugins: glass boot')
3824
3969
  },
3825
3970
  }
@@ -3834,7 +3979,17 @@ window.__ModuleLoader__.load({
3834
3979
  ...glassPlugin.inject,
3835
3980
  ]))
3836
3981
 
3982
+ // 模块开关缓存:页面加载时同步读取(让 main apply 的 modEnabled 门控生效);
3983
+ // 配置加载(applyTheme)/保存(dsh-long 保存)后更新。开关在下次刷新生效。
3984
+ try {
3985
+ const _m = localStorage.getItem('dsh-long:modules')
3986
+ if (_m) window.__dshLongModules = JSON.parse(_m)
3987
+ } catch (_) {}
3988
+ // 全局模块开关查询(供各组件/apply 内部判断)
3989
+ window.__dshLongMod = (n) => (window.__dshLongModules ? window.__dshLongModules[n] !== false : true)
3990
+
3837
3991
  function apply(ctx) {
3992
+ const modEnabled = (n) => (window.__dshLongModules ? window.__dshLongModules[n] !== false : true)
3838
3993
  const safeApply = (label, fn) => {
3839
3994
  try {
3840
3995
  fn(ctx)
@@ -3842,13 +3997,24 @@ window.__ModuleLoader__.load({
3842
3997
  console.error('[dsh-long-plugins] ' + label + ' apply failed:', error)
3843
3998
  }
3844
3999
  }
3845
- safeApply('upload', (c) => uploadPlugin.apply(c))
3846
- safeApply('skill-docs', (c) => skillDocsPlugin.apply(c))
3847
- safeApply('token-usage', (c) => tokenUsagePlugin.apply(c))
3848
- safeApply('mobile-hamburger', (c) => mobilePlugin.apply(c))
3849
- safeApply('workspace-files', (c) => workspaceFilesPlugin.apply(c))
3850
- safeApply('turn-ruler', (c) => turnRulerPlugin.apply(c))
3851
- safeApply('glass', (c) => glassPlugin.apply(c))
4000
+ // 「dsh-long」设置区(模块开关+补丁状态)始终注册——独立于任何模块开关, 否则禁用 RA-Span 会连带它一起消失
4001
+ ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-long', order: 30, label: 'dsh-long' }, DshLongSettingsSection))
4002
+ // .dsh-glass-* 布局类(设置区排版) CSS 始终注入(不随 glass 开关丢失; 体积小)
4003
+ ctx.effect(() => {
4004
+ const style = document.createElement('style')
4005
+ style.dataset.plugin = 'dsh-long-plugins'
4006
+ style.dataset.pluginCss = 'dsh-long-plugins/glass-layout'
4007
+ style.textContent = GLASS_LAYOUT_CSS
4008
+ document.head.appendChild(style)
4009
+ return () => style.remove()
4010
+ }, 'dsh-long-plugins: glass layout css (always)')
4011
+ safeApply('upload', (c) => uploadPlugin.apply(c)) // 上传子功能(预览/附件/拖放/粘贴)在插件内部按模块开关
4012
+ if (modEnabled('skillDocs')) safeApply('skill-docs', (c) => skillDocsPlugin.apply(c))
4013
+ safeApply('token-usage', (c) => tokenUsagePlugin.apply(c)) // 余额 chip 开关在其内部; MOBILE_CSS 始终注入
4014
+ if (modEnabled('mobile')) safeApply('mobile-hamburger', (c) => mobilePlugin.apply(c))
4015
+ if (modEnabled('workspace')) safeApply('workspace-files', (c) => workspaceFilesPlugin.apply(c))
4016
+ if (modEnabled('turnRuler')) safeApply('turn-ruler', (c) => turnRulerPlugin.apply(c))
4017
+ if (modEnabled('glass')) safeApply('glass', (c) => glassPlugin.apply(c))
3852
4018
  }
3853
4019
 
3854
4020
  exports.apply = apply
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-long-plugins",
3
3
  "description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, Markdown-to-Word (md2docx), and polished file preview (Word & PowerPoint real preview with zoom/pan, rendered Markdown), plus an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Markdown 转 Word(md2docx)、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。",
4
- "version": "2.2.3",
4
+ "version": "2.4.0",
5
5
  "entry": {
6
6
  "name": "dsh-long-plugins",
7
7
  "inject": [
package/lib/index.js CHANGED
@@ -1628,19 +1628,54 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1628
1628
  bgTint: { light: { color: "#1a2332", mask: 0.28 }, dark: { color: "#1a2332", mask: 0.28 } },
1629
1629
  inputBox: { light: { color: "", opacity: 1 }, dark: { color: "", opacity: 1 } },
1630
1630
  zone: { session: { color: "#1a2332", mask: 0.45, opacity: 0.5 }, input: { color: "#1a2332", mask: 0.6, opacity: 0.55 } },
1631
+ // 各模块开关(「dsh-long」设置区):false=禁用该模块功能(避免与 DSH 新版自带功能冲突/按需关闭)
1632
+ modules: { glass: true, uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: true },
1631
1633
  };
1632
1634
  const glassDir = resolve(homedir(), ".dsh-long-plugins");
1633
1635
  const glassFile = join(glassDir, "glass.json");
1634
1636
  const bgDir = join(glassDir, "backgrounds");
1637
+ const currentBgFile = join(bgDir, "current.uri");
1638
+ // 背景图文件引用: 配置里 bgImage 为 "current"(存 bgDir/current.uri) 或 ""(无) ;不再内嵌大图 data-URI
1639
+ const decodeDataUri = (s) => { const m = /^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(String(s || "")); return m ? { mime: m[1], base64: m[2] } : null; };
1640
+ const bgImageUrl = (cfg) => (cfg.bgImage === "current") ? "/api/dsh-uploads/glass-background" : "";
1641
+ // 只保留当前背景图(current.uri), 删除其它历史图片文件(旧图清理)
1642
+ async function pruneBackgrounds() {
1643
+ try {
1644
+ const names = await readdir(bgDir);
1645
+ for (const n of names) {
1646
+ if (n === "current.uri" || !n.endsWith(".uri")) continue;
1647
+ try { await unlink(join(bgDir, n)); } catch (_) {}
1648
+ }
1649
+ } catch (_) {}
1650
+ }
1635
1651
  async function readGlassJSON() {
1636
- try { const raw = JSON.parse(await readFile(glassFile, "utf8")); return { ...GLASS_DEFAULTS, ...(raw && typeof raw === "object" ? raw : {}) }; }
1637
- catch { return { ...GLASS_DEFAULTS }; }
1652
+ try {
1653
+ const raw = JSON.parse(await readFile(glassFile, "utf8"));
1654
+ const cfg = { ...GLASS_DEFAULTS, ...(raw && typeof raw === "object" ? raw : {}) };
1655
+ // 迁移旧版: bgImage 是内嵌 data-URI(大图) → 存成文件 + 改引用
1656
+ if (typeof cfg.bgImage === "string" && cfg.bgImage.startsWith("data:image/")) {
1657
+ try {
1658
+ await mkdir(bgDir, { recursive: true, mode: 0o700 });
1659
+ await writeFile(currentBgFile, cfg.bgImage, "utf8");
1660
+ cfg.bgImage = "current";
1661
+ await writeFile(glassFile, JSON.stringify(cfg, null, 2), "utf8");
1662
+ } catch (_) {}
1663
+ }
1664
+ return cfg;
1665
+ } catch { return { ...GLASS_DEFAULTS }; }
1638
1666
  }
1639
1667
  const glassConfig = async (req, res) => {
1640
1668
  try {
1641
1669
  requireTrusted(req);
1642
1670
  if (req.method === "GET" || req.method === "HEAD") {
1643
1671
  const cfg = await readGlassJSON();
1672
+ // 背景图只返回"是否存在 + 图片 URL", 不再返回内嵌大图 data-URI(减小响应)
1673
+ let imgMeta = { present: false, bytes: 0 };
1674
+ let imgUrl = "";
1675
+ if (cfg.bgImage === "current") {
1676
+ imgUrl = "/api/dsh-uploads/glass-background";
1677
+ try { imgMeta = { present: true, bytes: (await stat(currentBgFile)).size || 0 }; } catch (_) { imgMeta = { present: true, bytes: 0 }; }
1678
+ }
1644
1679
  sendJson(res, 200, {
1645
1680
  found: true,
1646
1681
  cfg: {
@@ -1651,9 +1686,12 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1651
1686
  bgTint: cfg.bgTint || { light: { color: "#1a2332", mask: 0.28 }, dark: { color: "#1a2332", mask: 0.28 } },
1652
1687
  bgColor: cfg.bgColor || "#1a2332", bgMask: cfg.bgMask ?? 0.28,
1653
1688
  bgBlur: cfg.bgBlur ?? 0, bgOpacity: cfg.bgOpacity ?? 1,
1654
- bgImage: cfg.bgImage ? { present: true, bytes: Math.round((cfg.bgImage.length * 3) / 4) } : { present: false },
1689
+ turnRuler: (cfg.modules && cfg.modules.turnRuler) !== false,
1690
+ modules: { glass: true, uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: true, ...(cfg.modules || {}) },
1691
+ bgImage: imgMeta,
1655
1692
  },
1656
- bgImage: cfg.bgImage,
1693
+ bgImage: imgMeta,
1694
+ bgImageUrl: imgUrl,
1657
1695
  });
1658
1696
  return;
1659
1697
  }
@@ -1661,16 +1699,27 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1661
1699
  const body = await readJsonBody(req, 4 * 1024 * 1024);
1662
1700
  const current = await readGlassJSON();
1663
1701
  const next = { ...current, ...sanitizeGlass(typeof body === "object" && body !== null ? body : {}) };
1664
- await mkdir(glassDir, { recursive: true, mode: 0o700 });
1665
- await writeFile(glassFile, JSON.stringify(next, null, 2), "utf8");
1666
- // 背景图若有变更且是 data-URI, 存入 backgrounds/ 供"选择旧图"使用
1667
- if (next.bgImage && current.bgImage !== next.bgImage && /^data:image\//.test(next.bgImage)) {
1702
+ // 各模块开关表:body.modules 里有的键按布尔合并
1703
+ if (body && typeof body.modules === "object" && body.modules !== null) {
1704
+ const m = { glass: true, uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: true, ...(current.modules || {}) };
1705
+ for (const k of Object.keys(body.modules)) m[k] = body.modules[k] !== false;
1706
+ next.modules = m;
1707
+ }
1708
+ // 背景图改为"文件 + 引用": 用户上传的 data-URI 存入 current.uri, 配置只存 "current" 引用
1709
+ if (typeof next.bgImage === "string" && next.bgImage.startsWith("data:image/")) {
1668
1710
  try {
1669
1711
  await mkdir(bgDir, { recursive: true, mode: 0o700 });
1670
- const name = Date.now() + ":" + Math.random().toString(36).slice(2, 8) + ".uri";
1671
- await writeFile(join(bgDir, name), next.bgImage, "utf8");
1672
- } catch (_) {}
1712
+ await writeFile(currentBgFile, next.bgImage, "utf8");
1713
+ next.bgImage = "current";
1714
+ } catch (_) { next.bgImage = ""; }
1715
+ } else if ("bgImage" in body && body.bgImage === "") {
1716
+ next.bgImage = ""; // 显式清除
1717
+ try { await unlink(currentBgFile); } catch (_) {}
1673
1718
  }
1719
+ // 只保留当前背景图, 删除历史旧图
1720
+ await pruneBackgrounds();
1721
+ await mkdir(glassDir, { recursive: true, mode: 0o700 });
1722
+ await writeFile(glassFile, JSON.stringify(next, null, 2), "utf8");
1674
1723
  sendJson(res, 200, { ok: true, saved: true });
1675
1724
  return;
1676
1725
  }
@@ -1691,14 +1740,64 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1691
1740
  catch { sendJson(res, 404, { error: "not found" }); return; }
1692
1741
  }
1693
1742
  let list = [];
1694
- try { list = (await readdir(bgDir)).filter((n) => n.endsWith(".uri")).sort().reverse().slice(0, 30); } catch (_) {}
1743
+ // 只保留当前背景图, 删除历史旧图; 列表排除当前图
1744
+ try { await pruneBackgrounds(); } catch (_) {}
1745
+ try { list = (await readdir(bgDir)).filter((n) => n.endsWith(".uri") && n !== "current.uri").sort().reverse().slice(0, 30); } catch (_) {}
1695
1746
  sendJson(res, 200, { list });
1696
1747
  } catch (error) {
1697
1748
  sendError(res, error, onError);
1698
1749
  }
1699
1750
  };
1751
+ // 返回当前背景图(文件存的 data-URI 解码为真实图片)
1752
+ const glassBackground = async (req, res) => {
1753
+ try {
1754
+ requireTrusted(req);
1755
+ const cfg = await readGlassJSON();
1756
+ if (cfg.bgImage !== "current") { sendJson(res, 404, { error: "no background" }); return; }
1757
+ const buf = await readFile(currentBgFile, "utf8");
1758
+ const d = decodeDataUri(buf);
1759
+ if (!d) { sendJson(res, 404, { error: "bad image" }); return; }
1760
+ const bytes = Buffer.from(d.base64, "base64");
1761
+ res.writeHead(200, { "content-type": d.mime, "cache-control": "no-store", "x-content-type-options": "nosniff" });
1762
+ res.end(bytes);
1763
+ } catch (error) {
1764
+ sendError(res, error, onError);
1765
+ }
1766
+ };
1767
+ // 补丁状态:只读检测 DSH 核心各补丁的「已打 / 未打 / 原生无需」,供 dsh-long 设置区显示
1768
+ const patchStatus = async (req, res) => {
1769
+ try {
1770
+ requireTrusted(req);
1771
+ // 定位 DSH 核心 dsh-client-connection 目录(探测常见路径)
1772
+ let DIR = "";
1773
+ const candidates = [
1774
+ "/volume1/npm/global/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib",
1775
+ "/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib",
1776
+ "/volume1/dsh/.dsh/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib",
1777
+ "/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection/lib",
1778
+ ];
1779
+ for (const c of candidates) { try { await stat(join(c, "index.js")); DIR = c; break; } catch (_) {} }
1780
+ if (!DIR) { sendJson(res, 200, { ok: true, patches: null, error: "未找到 DSH 核心 dsh-client-connection 目录" }); return; }
1781
+ const IDX = join(DIR, "index.js");
1782
+ const CLI = join(DIR, "client.js");
1783
+ const idx = await readFile(IDX, "utf8");
1784
+ let cli = "";
1785
+ try { cli = await readFile(CLI, "utf8"); } catch (_) {}
1786
+ const mark1 = "PRIVILEGED_METHODS.has(method) && !isTrustedApiRequest(request, trustedHosts)";
1787
+ const mark2 = "isLoopback: pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname) || [";
1788
+ const mark3 = "WEBSOCKET_HEARTBEAT_MS";
1789
+ const patches = {
1790
+ trustedHosts: idx.includes(mark1) ? "已打" : "未打",
1791
+ loopback: cli.includes(mark2) ? "已打" : "未打",
1792
+ heartbeat: idx.includes(mark3) ? "已打" : (idx.includes("socket.ping()") ? "原生无需" : "未打"),
1793
+ };
1794
+ sendJson(res, 200, { ok: true, patches, dir: DIR });
1795
+ } catch (error) {
1796
+ sendError(res, error, onError);
1797
+ }
1798
+ };
1700
1799
 
1701
- return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, glassConfig, glassBackgrounds };
1800
+ return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, glassConfig, glassBackgrounds, glassBackground, patchStatus };
1702
1801
  }
1703
1802
 
1704
1803
  /** 人类可读文件大小。 */
@@ -2609,6 +2708,18 @@ export async function apply(ctx, config = {}) {
2609
2708
  handler: handlers.glassBackgrounds,
2610
2709
  }), "dsh-long-plugins: glass-ui backgrounds route");
2611
2710
 
2711
+ ctx.effect(() => ctx.webServer.register({
2712
+ kind: "exact",
2713
+ path: "/api/dsh-uploads/glass-background",
2714
+ handler: handlers.glassBackground,
2715
+ }), "dsh-long-plugins: glass-ui background image route");
2716
+
2717
+ ctx.effect(() => ctx.webServer.register({
2718
+ kind: "exact",
2719
+ path: "/api/dsh-uploads/patch-status",
2720
+ handler: handlers.patchStatus,
2721
+ }), "dsh-long-plugins: patch status route");
2722
+
2612
2723
  ctx.effect(() => ctx.webServer.register({
2613
2724
  kind: "exact",
2614
2725
  path: "/api/dsh-uploads/workspace-browse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-long-plugins",
3
- "version": "2.2.3",
3
+ "version": "2.4.0",
4
4
  "description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, Markdown-to-Word (md2docx), and polished file preview (Word & PowerPoint real preview with zoom/pan, rendered Markdown), plus an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Markdown 转 Word(md2docx)、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,11 +42,16 @@ if grep -q 'WEBSOCKET_HEARTBEAT_MS' "$IDX"; then
42
42
  echo " 已打过,跳过 ✓"
43
43
  exit 0
44
44
  fi
45
+ # DSH 已原生心跳(如 v0.1.2-alpha 网关定期 ping):检测到已是 ping 心跳即跳过,避免重复/冲突
46
+ if grep -q 'socket\.ping()' "$IDX"; then
47
+ echo " 检测到 DSH 已有 WebSocket ping 心跳(原生或已打过),跳过 ✓"
48
+ exit 0
49
+ fi
45
50
 
46
- # 待替换的原始代码未在 -> 提示人工核对
51
+ # 待替换的原始代码未在 -> 可能是 DSH 已改逻辑(如 v0.1.2-alpha 会话视图/网关重构):温和跳过而不是硬失败
47
52
  if ! grep -q 'var WebSocketDownlinks = class {' "$IDX"; then
48
- echo " 警告:未找到 WebSocketDownlinks 类,可能 DSH 已改逻辑,请人工核对" >&2
49
- exit 1
53
+ echo " 提示:未找到 WebSocketDownlinks 类,DSH 可能已改逻辑(如新版原生心跳),跳过本补丁(不再硬失败)" >&2
54
+ exit 0
50
55
  fi
51
56
 
52
57
  cp "$IDX" "$IDX.bak-heartbeat-$(date +%Y%m%d-%H%M)"
@@ -119,6 +119,16 @@ pnpm install # 或用探测到的 pnpm 绝对路径
119
119
  # md2docxScript: /your/path/md2docx.py # 可选,默认用包内 lib/md2docx.py
120
120
  ```
121
121
 
122
+ ### 4.5 设置插件默认:回合导航 + RA-Span 关闭(避免与 DSH 新版自带功能冲突 / 默认更简洁)
123
+ > DSH ≥ v0.1.2-alpha 已自带「紧凑回合导航」。新机默认把插件 `modules.turnRuler` 置 false(避免轮次刻尺冲突),`modules.glass`(RA-Span)也默认 false(要玻璃桌面可在「dsh-long」设置区手动开)。如目标 DSH 无原生回合导航,可在「dsh-long」手动开回。
124
+ ```sh
125
+ GJ="${HOME:-/root}/.dsh-long-plugins/glass.json"
126
+ mkdir -p "$(dirname "$GJ")"
127
+ [ -f "$GJ" ] || echo '{"enabled":false,"blur":20,"bgImage":"","bgColor":"#1a2332","bgMask":0.28,"bgTint":{"light":{"color":"#1a2332","mask":0.28},"dark":{"color":"#1a2332","mask":0.28}},"inputBox":{"light":{"color":"","opacity":1},"dark":{"color":"","opacity":1}},"zone":{"session":{"color":"#1a2332","mask":0.45,"opacity":0.5},"input":{"color":"#1a2332","mask":0.6,"opacity":0.55}},"modules":{"glass":false,"uploadAttach":true,"uploadDragDrop":true,"uploadPaste":true,"uploadPreview":true,"skillDocs":true,"balance":true,"mobile":true,"workspace":true,"turnRuler":false}}' > "$GJ"
128
+ node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));p.modules=p.modules||{};p.modules.glass=false;p.modules.turnRuler=false;fs.writeFileSync(process.argv[1],JSON.stringify(p,null,2)+'\n')" "$GJ" \
129
+ && echo " ✅ 已默认关闭插件 RA-Span(modules.glass=false) 与回合导航(modules.turnRuler=false)"
130
+ ```
131
+
122
132
  ## 工作目录 / 上传目录匹配(重要决策点)
123
133
 
124
134
  上传/工作区根**不是写死的**,靠环境变量推导:
@@ -176,6 +186,7 @@ setsid nohup <node绝对路径> --expose-internals --max-old-space-size=8192 \
176
186
  > **md2docx 工具需新建会话后 agent 才加载**(会话工具集创建时固定),验证时开新会话。
177
187
  6. 窄屏(手机)下预览窗按钮自动换行不截断。
178
188
  7. 用 `dsh --profile <p> --dump-config` 核对组合树正确。
189
+ 8. **「dsh-long」设置区**:设置面板出现「dsh-long」节,含**各模块开关**(RA-Span/会话导航/附件上传/拖放/粘贴/上传预览/技能文档/余额/移动端/输出文件)与**「补丁状态(只读)」**——显示 3 个 DSH 核心补丁的已打/未打/原生无需。模块开关存于 `~/.dsh-long-plugins/glass.json` 的 `modules` 字段,保存后下次刷新生效。
179
190
 
180
191
  ## 硬性安全边界(必须遵守)
181
192
  - **不主动 `git push` / 打 tag / 发 Release**;需要发版本时停下用 `ask_user_question` 等用户确认。
@@ -60,6 +60,19 @@ sh <插件目录>/patches/dsh-client-connection-heartbeat.sh
60
60
  cd "$DSH_HOME/profiles/web" && pnpm install
61
61
  ```
62
62
 
63
+ ### 5.5 升级后自动禁用插件「回合导航」(避免与 DSH 新版自带回合导航冲突)
64
+ > DSH ≥ v0.1.2-alpha 已自带「紧凑回合导航」。若插件 turn-ruler 仍开启,两者同时操纵会话 DOM 可能冲突→前端崩。升级流程里**自动把插件 `modules.turnRuler` 置为 false**;如目标版无原生回合导航,可在「dsh-long」设置区手动开回来。
65
+ ```sh
66
+ GJ="${HOME:-/root}/.dsh-long-plugins/glass.json"
67
+ if [ -f "$GJ" ]; then
68
+ node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));p.modules=p.modules||{};p.modules.turnRuler=false;fs.writeFileSync(process.argv[1],JSON.stringify(p,null,2)+'\n')" "$GJ" \
69
+ && echo " ✅ 已自动关闭插件回合导航 (modules.turnRuler=false,可在 dsh-long 手动开回)" \
70
+ || echo " ⚠️ 未能更新插件配置(可手动在 dsh-long 关掉「会话导航」)"
71
+ else
72
+ echo " (未找到插件配置, 跳过; 可手动在 dsh-long 关掉「会话导航」)"
73
+ fi
74
+ ```
75
+
63
76
  ### 6. 重启 dsh + 强刷浏览器
64
77
  ```sh
65
78
  /volume1/dsh/start.sh # 或按当前环境的重启方式
@@ -73,34 +86,6 @@ cd "$DSH_HOME/profiles/web" && pnpm install
73
86
  4. 提问/审批窗口:弹出一个问题,等 60s+ 不再自己消失(心跳补丁生效)。
74
87
  5. 插件设置面板「上传文件」「输出文件」「技能文档」都在。
75
88
  6. `grep -q WEBSOCKET_HEARTBEAT_MS <核心解压路径>/dsh-client-connection/lib/index.js` → 心跳在位。
76
- 7. **RA-Span 玻璃观感**:左栏/顶栏文字图标、会话区过程内容配色、输入框、提问卡、耗时统计等是否仍正常(见下文"RA-Span 升级后需重新校准")。
77
-
78
- ## RA-Span(统一桌面/玻璃)升级后需重新校准 ⚠️
79
-
80
- dsh-long-plugins 的「RA-Span(统一桌面/玻璃)」**大量依赖 DSH 内部混淆类名**(`o3BgMG_*`/`CY-8Ka_*`/`QWLzLg_*`/`M8wy4a_*`/`uV2eYG_*`/`wSkVaW_*`/`osXY9a_*`/`p-xYUq_*` 等)与 DSH 内部 DOM 结构。DSH 升级重打包后**这些类名前缀会变**,导致插件里靠类名压样式的规则/JS **静默失效**(不报错,只是观感/配色不对)。
81
-
82
- 升级流程会**同步插件代码(逻辑保住)**,但**不会自动重抓新的类名**,所以升级后必须**人工复核**玻璃观感。
83
-
84
- ### 升级后重点复核 + 处理
85
- 1. 浏览器强刷(Ctrl/Cmd+Shift+R)。
86
- 2. 逐项看 RA-Span 是否仍正常:
87
- - 左栏/顶栏文字与图标(白字+深影、品牌区不被白化)
88
- - 会话区过程内容配色(Bash 绿 / Read 蓝 / Edit 橙 / Write 紫 / Think 琥珀、错误红、工具输出水绿)
89
- - 状态/统计行余额(走 `data-slot-conversation="composer.dock"` 的相对稳)
90
- - 输入框(无重影、背景/文字随主题、可调色)
91
- - 「新会话」按钮去白底
92
- 3. 若某项观感/配色不对 → **新类名变了**。让用户在对应元素「右键 → 检查」,把高亮元素及父级的 class 发我,把 `client/client.js` 里对应的 `[class*="..."]` 前缀更新成新类名(只改选择器,不动逻辑)。
93
- 4. 稳定项无需动:用 `data-slot-*` 的(状态栏)、注入 `<head>` 的 CSS、`data-dsh-theme` 属性。
94
-
95
- ### 各依赖类名的功能清单(升级后最可能失效)
96
- | 功能 | 依赖类名 | 失效表现 |
97
- |---|---|---|
98
- | 工具名配色(Bash/Read/Edit/Write) | `o3BgMG_title` / `CY-8Ka_title` | 标签不变色 |
99
- | Think 配色 | `QWLzLg_*` | Think 不琥珀 |
100
- | 提问/弹窗不透明 | `M8wy4a_card` | 弹窗透明重叠 |
101
- | 输入框 | `uV2eYG_*` | 重影/背景不随主题 |
102
- | 耗时统计 | `osXY9a_*` / `p-xYUq_*` | 统计字淡/重影 |
103
- | 左栏「新会话」 | `newSession` | 白底回来 |
104
89
 
105
90
  ## 硬性安全边界(必须遵守)
106
91
  - **不主动 push / 打 tag / 发 Release**;需要发布版本时停下用 ask_user_question 等确认。
@@ -114,5 +99,4 @@ dsh-long-plugins 的「RA-Span(统一桌面/玻璃)」**大量依赖 DSH 内
114
99
  | 升级后设置页"未暴露命名空间" | 补丁2 被覆盖 | 重跑 apply-dsh-patches.sh |
115
100
  | 升级后提问窗口自己消失 | 心跳补丁(补丁3)被覆盖 | 重跑 apply-dsh-patches.sh 或装插件补丁脚本 |
116
101
  | 升级后插件不显示 | 插件安装副本未同步 | 步骤 5 `pnpm install` |
117
- | 升级后 RA-Span 观感/配色不对(输入框重影、标签不变色、弹窗透明、统计字淡等) | DSH 混淆类名变了 | 见 RA-Span 校准节:强刷 + 逐项检查,让用户右键给出新类名,更新 `client/client.js` 对应选择器 |
118
102
  | 按钮全没了/界面异常 | 浏览器缓存了旧前端 | 强刷 Ctrl/Cmd+Shift+R |