dsh-long-plugins 2.2.3 → 2.3.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
@@ -1961,7 +1961,9 @@ window.__ModuleLoader__.load({
1961
1961
  if (!cancelled) setBalance(null);
1962
1962
  }
1963
1963
  };
1964
- load();
1964
+ // 首次拉取延迟到浏览器空闲(减轻首屏网络风暴), 60s 轮询不变
1965
+ const schedule = () => { try { if (window.requestIdleCallback) window.requestIdleCallback(() => load(), { timeout: 300 }); else setTimeout(load, 300) } catch (_) { load() } };
1966
+ schedule();
1965
1967
  const interval = setInterval(load, 60000);
1966
1968
  return () => {
1967
1969
  cancelled = true;
@@ -2003,7 +2005,9 @@ window.__ModuleLoader__.load({
2003
2005
  if (!cancelled) setCost(null);
2004
2006
  }
2005
2007
  };
2006
- load();
2008
+ // 首次拉取延迟到空闲(减轻首屏网络风暴), 60s 轮询不变
2009
+ const schedule = () => { try { if (window.requestIdleCallback) window.requestIdleCallback(() => load(), { timeout: 300 }); else setTimeout(load, 300) } catch (_) { load() } };
2010
+ schedule();
2007
2011
  const interval = setInterval(load, 60000);
2008
2012
  return () => {
2009
2013
  cancelled = true;
@@ -3133,6 +3137,7 @@ window.__ModuleLoader__.load({
3133
3137
  color: #ffffff !important;
3134
3138
  text-shadow: 0 1px 1px rgba(0,0,0,.5);
3135
3139
  }
3140
+ /* (移除 color/text-shadow 过渡: 会在收尾重渲染时短暂贴默认色导致"闪黑") */
3136
3141
  /* DSH 弹窗/浮层(提问/确认/重命名等):给不透明底+主题文字, 杜绝透明与会话内容重叠 */
3137
3142
  html[data-dsh-glass="on"] [class*="Radix"] [class*="Content"],
3138
3143
  html[data-dsh-glass="on"] [class*="DialogContent"],
@@ -3165,6 +3170,19 @@ window.__ModuleLoader__.load({
3165
3170
  html[data-dsh-glass="on"] [class*="lXshSW_item"] {
3166
3171
  color: var(--dsw-alias-label-primary) !important;
3167
3172
  }
3173
+ /* 排队消息/等待任务面板(_7yHdaG_dock/panel, 在 conversation.input.dock 内):不透明/磨砂底+可读文字 */
3174
+ html[data-dsh-glass="on"] [class*="_7yHdaG_panel"],
3175
+ html[data-dsh-glass="on"] [class*="_7yHdaG_dock"] {
3176
+ background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 88%, transparent) !important;
3177
+ color: var(--dsw-alias-label-primary) !important;
3178
+ backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
3179
+ -webkit-backdrop-filter: blur(var(--dsh-glass-blur, 16px)) !important;
3180
+ }
3181
+ html[data-dsh-glass="on"] [class*="_7yHdaG_header"],
3182
+ html[data-dsh-glass="on"] [class*="_7yHdaG_preview"],
3183
+ html[data-dsh-glass="on"] [class*="_7yHdaG_row"] {
3184
+ color: var(--dsw-alias-label-primary) !important;
3185
+ }
3168
3186
  /* 轮次/历史提问预览:默认隐藏(点开才显示),并去掉头部底色,避免底部露出白条 */
3169
3187
  .dsh-turn-preview:not(.open){display:none !important;}
3170
3188
  .dsh-turn-preview-head{background:transparent !important;}
@@ -3224,18 +3242,27 @@ window.__ModuleLoader__.load({
3224
3242
  backdrop-filter: none !important;
3225
3243
  -webkit-backdrop-filter: none !important;
3226
3244
  }
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;
3245
+ /* 输入框旁权限/功能选择下拉(盾牌 Full access 等):给不透明底+可读文字(高优先级, 压过 DSH 透明覆盖) */
3246
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_root_"] [class*="_list_"],
3247
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_root_"] [class*="_list_"] [class*="_viewport_"],
3248
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_list_"] [class*="_viewport_"] [class*="_itemWrap_"] {
3249
+ background-color: var(--dsw-specific-input-major, #e8edf3) !important;
3231
3250
  backdrop-filter: none !important;
3232
3251
  -webkit-backdrop-filter: none !important;
3233
3252
  }
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;
3253
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_item_"],
3254
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_itemLabel_"] {
3255
+ color: var(--dsw-alias-label-primary) !important;
3256
+ }
3257
+ /* 兜底: Radix 风格菜单/下拉(可能被渲染到 body 顶层) */
3258
+ html[data-dsh-glass="on"] [class*="Radix"] [class*="Menu"],
3259
+ html[data-dsh-glass="on"] [class*="radix-menu"],
3260
+ html[data-dsh-glass="on"] [class*="menuitem"],
3261
+ html[data-dsh-glass="on"] [role="menu"] {
3262
+ background-color: var(--dsw-specific-input-major, #e8edf3) !important;
3237
3263
  backdrop-filter: none !important;
3238
3264
  -webkit-backdrop-filter: none !important;
3265
+ color: var(--dsw-alias-label-primary) !important;
3239
3266
  }
3240
3267
  /* 玻璃开启 + 浅色系统主题:加深弱化标签/元信息文字,避免浅灰字叠在浅磨砂卡上看不清 */
3241
3268
  @media (prefers-color-scheme: light) {
@@ -3329,7 +3356,7 @@ window.__ModuleLoader__.load({
3329
3356
  if (!r.ok) throw new Error(b.error || ('HTTP ' + r.status))
3330
3357
  let list = []
3331
3358
  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 }))
3359
+ setState((s) => ({ ...s, loading: false, cfg: b.cfg, bgImage: b.bgImageUrl || '', bgList: list }))
3333
3360
  } catch (e) { setState((s) => ({ ...s, loading: false, error: glassText(e) })) }
3334
3361
  }, [])
3335
3362
  React.useEffect(() => { load(); }, [load])
@@ -3352,9 +3379,16 @@ window.__ModuleLoader__.load({
3352
3379
  try {
3353
3380
  const cfg = state.cfg || {}
3354
3381
  const num = (v, fb) => ((typeof v === 'number' && Number.isFinite(v) && v >= 0) ? v : fb)
3382
+ // 背景图: 仅当用户上传了新图(data-URI)才传; 未改(URL)省略以保持引用; 清空传 '' 清除
3383
+ const sBg = state.bgImage
3384
+ let bgImageOut
3385
+ if (sBg === '') bgImageOut = ''
3386
+ else if (typeof sBg === 'string' && sBg.startsWith('data:image/')) bgImageOut = sBg
3387
+ else bgImageOut = undefined
3355
3388
  const payload = {
3356
3389
  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),
3390
+ bgMask: num(cfg.bgMask, 0.28), bgColor: cfg.bgColor || '#1a2332', bgBlur: num(cfg.bgBlur, 0), bgOpacity: num(cfg.bgOpacity, 1),
3391
+ ...(bgImageOut !== undefined ? { bgImage: bgImageOut } : {}),
3358
3392
  bgTint: {
3359
3393
  light: { color: (cfg.bgTint?.light?.color) || '#1a2332', mask: num(cfg.bgTint?.light?.mask, 0.28) },
3360
3394
  dark: { color: (cfg.bgTint?.dark?.color) || '#1a2332', mask: num(cfg.bgTint?.dark?.mask, 0.28) },
@@ -3371,8 +3405,9 @@ window.__ModuleLoader__.load({
3371
3405
  const r = await fetch('/api/dsh-uploads/glass-config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
3372
3406
  const b = await r.json()
3373
3407
  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: '' }))
3408
+ const applyBg = (payload.bgImage && payload.bgImage.startsWith('data:')) ? payload.bgImage : ((state.bgImage && state.bgImage.startsWith('/')) ? state.bgImage : '')
3409
+ window.__dshGlassApply && window.__dshGlassApply({ ...payload, bgImage: applyBg })
3410
+ setState((s) => ({ ...s, saving: false, cfg: { ...(s.cfg || {}), ...payload }, bgImage: applyBg, error: '' }))
3376
3411
  } catch (e) { setState((s) => ({ ...s, saving: false, error: glassText(e) })) }
3377
3412
  }, [state])
3378
3413
  const cfg = state.cfg || {}
@@ -3586,18 +3621,15 @@ window.__ModuleLoader__.load({
3586
3621
  if (isInSkip(root)) return
3587
3622
  root.querySelectorAll('div,span,button,a,label,p,li,h1,h2,h3,h4').forEach((el) => {
3588
3623
  if (isInSkip(el)) return
3589
- const tag = el.tagName
3590
- const hasText = el.textContent && el.textContent.trim()
3591
- if (!hasText) return
3624
+ if (el.dataset && el.dataset.dshgl) return // 已处理过, 跳过
3592
3625
  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)
3626
+ const textLike = /^(BUTTON|A|SPAN|LABEL|LI|P|H1|H2|H3|H4)$/.test(el.tagName)
3594
3627
  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
3628
+ // 跳过有盒子背景的元素(用类名判断, 不再 getComputedStyle 以免每元素强制样式重算导致卡顿)
3629
+ if (/bubble|badge|chip|tag|pill|code-block|md-code|n_block|n_banner|n_plain|button/i.test(el.className)) return
3599
3630
  el.style.setProperty('color', '#ffffff', 'important')
3600
3631
  el.style.setProperty('text-shadow', '0 1px 1px rgba(0,0,0,.5)')
3632
+ if (el.dataset) el.dataset.dshgl = '1'
3601
3633
  })
3602
3634
  // 图标也变白(随 currentColor),用于工作区文件夹等单色图标;保留显式品牌色
3603
3635
  root.querySelectorAll('svg, [class*="icon"], [class*="Icon"]').forEach((el) => {
@@ -3622,15 +3654,14 @@ window.__ModuleLoader__.load({
3622
3654
  if (!scroll) return
3623
3655
  scroll.querySelectorAll('div,span,p,li,pre,code,button,a,label,h1,h2,h3,h4').forEach((el) => {
3624
3656
  if (isInSkip(el)) return
3625
- const hasText = el.textContent && el.textContent.trim()
3626
- if (!hasText) return
3657
+ if (el.dataset && el.dataset.dshgl) return // 已处理过, 跳过(只处理新内容)
3627
3658
  const ownText = Array.from(el.childNodes).some((n) => n.nodeType === 3 && n.textContent.trim())
3628
3659
  const textLike = /^(BUTTON|A|SPAN|LABEL|LI|P|H1|H2|H3|H4|PRE|CODE)$/.test(el.tagName)
3629
3660
  if (!textLike && !ownText) return
3630
- const bgbg = getComputedStyle(el).backgroundColor
3631
- if (bgbg && bgbg !== 'rgba(0, 0, 0, 0)' && bgbg !== 'transparent') return
3661
+ if (/bubble|badge|chip|tag|pill|code-block|md-code|n_block|n_banner|n_plain/i.test(el.className)) return
3632
3662
  el.style.setProperty('color', '#ffffff', 'important')
3633
3663
  el.style.setProperty('text-shadow', '0 1px 1px rgba(0,0,0,.5)')
3664
+ if (el.dataset) el.dataset.dshgl = '1'
3634
3665
  })
3635
3666
  // 在白字底上, 给过程内容配色区分, Error 用红色。
3636
3667
  // 文字常在标签的子元素里(白字已把它设白), 故连同文本子元素一起上色。
@@ -3726,6 +3757,8 @@ window.__ModuleLoader__.load({
3726
3757
  }
3727
3758
  // 主题相关的东西统一重算:背景罩 + 输入框。切主题时由 observer/mq 触发。
3728
3759
  // 写入前先断开观察器,写完再重连,避免自触发造成死循环。
3760
+ // 把非关键的"内容配色/白字"延迟到浏览器空闲时执行, 让页面先渲染出来(优化刷新加载时间)
3761
+ const defer = (fn) => { try { if (window.requestIdleCallback) window.requestIdleCallback(fn, { timeout: 120 }); else setTimeout(fn, 0) } catch (_) { fn() } }
3729
3762
  const applyTheme = () => {
3730
3763
  const obs = window.__dshGlassStyleObs
3731
3764
  if (obs) obs.disconnect()
@@ -3736,9 +3769,7 @@ window.__ModuleLoader__.load({
3736
3769
  const v = `rgba(${hexToRgb(bt.color)},${bt.mask})`
3737
3770
  root.style.setProperty('--dsh-glass-bg-zone', v)
3738
3771
  applyInputCard()
3739
- applyChromeText()
3740
- applySessionText()
3741
- fixDialogs()
3772
+ defer(() => { applyChromeText(); applySessionText(); fixDialogs() })
3742
3773
  } finally {
3743
3774
  if (obs) obs.observe(document.documentElement, { attributes: true, attributeFilter: ['style', 'class'] })
3744
3775
  }
@@ -3778,7 +3809,7 @@ window.__ModuleLoader__.load({
3778
3809
  // 侧边栏/顶栏 DOM 变化(会话列表更新等)时重算左栏/顶栏文字,防抖 120ms
3779
3810
  let chromeTimer = null
3780
3811
  const chromeRoots = [...document.querySelectorAll('#root [class*="sidebarCol"],#root [class*="wSkVaW_header"]')]
3781
- const chromeObs = new MutationObserver(() => { if (chromeTimer) clearTimeout(chromeTimer); chromeTimer = setTimeout(applyChromeText, 120) })
3812
+ const chromeObs = new MutationObserver(() => { if (chromeTimer) clearTimeout(chromeTimer); chromeTimer = setTimeout(applyChromeText, 30) })
3782
3813
  chromeRoots.forEach((r) => chromeObs.observe(r, { childList: true, subtree: true, characterData: true }))
3783
3814
  // 输入框卡片(uV2eYG_card/root)可能在启动后才挂载(会话/输入区加载晚),
3784
3815
  // 启动那一刻 applyInputCard 常找不到框 → 需刷新才生效。监听新增卡片节点时重算,防抖 100ms。
@@ -3796,7 +3827,7 @@ window.__ModuleLoader__.load({
3796
3827
  const sessionObs = new MutationObserver((muts) => {
3797
3828
  let hit = false
3798
3829
  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) }
3830
+ if (hit) { if (sessionTimer) clearTimeout(sessionTimer); sessionTimer = setTimeout(() => { applySessionText(); fixDialogs() }, 20) }
3800
3831
  })
3801
3832
  sessionObs.observe(document.body, { childList: true, subtree: true })
3802
3833
  }
@@ -3817,9 +3848,11 @@ window.__ModuleLoader__.load({
3817
3848
  }, 'dsh-long-plugins: glass applier')
3818
3849
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'glass-ui', order: 40, label: 'RA-Span' }, GlassSettingsSection))
3819
3850
  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(() => {})
3851
+ // 玻璃配置/背景图历史拉取延迟到空闲, 减轻首屏网络风暴(背景没用玻璃时几乎零成本)
3852
+ const loadGlass = () => { fetch('/api/dsh-uploads/glass-config', { cache: 'no-store' }).then((r) => r.json()).then((b) => {
3853
+ if (b && b.cfg) { window.__dshGlassApply && window.__dshGlassApply({ ...b.cfg, bgImage: b.bgImageUrl || '' }) }
3854
+ }).catch(() => {}) }
3855
+ try { if (window.requestIdleCallback) window.requestIdleCallback(loadGlass, { timeout: 500 }); else setTimeout(loadGlass, 500) } catch (_) { loadGlass() }
3823
3856
  }, 'dsh-long-plugins: glass boot')
3824
3857
  },
3825
3858
  }
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.3.0",
5
5
  "entry": {
6
6
  "name": "dsh-long-plugins",
7
7
  "inject": [
package/lib/index.js CHANGED
@@ -1632,15 +1632,48 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1632
1632
  const glassDir = resolve(homedir(), ".dsh-long-plugins");
1633
1633
  const glassFile = join(glassDir, "glass.json");
1634
1634
  const bgDir = join(glassDir, "backgrounds");
1635
+ const currentBgFile = join(bgDir, "current.uri");
1636
+ // 背景图文件引用: 配置里 bgImage 为 "current"(存 bgDir/current.uri) 或 ""(无) ;不再内嵌大图 data-URI
1637
+ 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; };
1638
+ const bgImageUrl = (cfg) => (cfg.bgImage === "current") ? "/api/dsh-uploads/glass-background" : "";
1639
+ // 只保留当前背景图(current.uri), 删除其它历史图片文件(旧图清理)
1640
+ async function pruneBackgrounds() {
1641
+ try {
1642
+ const names = await readdir(bgDir);
1643
+ for (const n of names) {
1644
+ if (n === "current.uri" || !n.endsWith(".uri")) continue;
1645
+ try { await unlink(join(bgDir, n)); } catch (_) {}
1646
+ }
1647
+ } catch (_) {}
1648
+ }
1635
1649
  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 }; }
1650
+ try {
1651
+ const raw = JSON.parse(await readFile(glassFile, "utf8"));
1652
+ const cfg = { ...GLASS_DEFAULTS, ...(raw && typeof raw === "object" ? raw : {}) };
1653
+ // 迁移旧版: bgImage 是内嵌 data-URI(大图) → 存成文件 + 改引用
1654
+ if (typeof cfg.bgImage === "string" && cfg.bgImage.startsWith("data:image/")) {
1655
+ try {
1656
+ await mkdir(bgDir, { recursive: true, mode: 0o700 });
1657
+ await writeFile(currentBgFile, cfg.bgImage, "utf8");
1658
+ cfg.bgImage = "current";
1659
+ await writeFile(glassFile, JSON.stringify(cfg, null, 2), "utf8");
1660
+ } catch (_) {}
1661
+ }
1662
+ return cfg;
1663
+ } catch { return { ...GLASS_DEFAULTS }; }
1638
1664
  }
1639
1665
  const glassConfig = async (req, res) => {
1640
1666
  try {
1641
1667
  requireTrusted(req);
1642
1668
  if (req.method === "GET" || req.method === "HEAD") {
1643
1669
  const cfg = await readGlassJSON();
1670
+ // 背景图只返回"是否存在 + 图片 URL", 不再返回内嵌大图 data-URI(减小响应)
1671
+ let imgMeta = { present: false, bytes: 0 };
1672
+ let imgUrl = "";
1673
+ if (cfg.bgImage === "current") {
1674
+ imgUrl = "/api/dsh-uploads/glass-background";
1675
+ try { imgMeta = { present: true, bytes: (await stat(currentBgFile)).size || 0 }; } catch (_) { imgMeta = { present: true, bytes: 0 }; }
1676
+ }
1644
1677
  sendJson(res, 200, {
1645
1678
  found: true,
1646
1679
  cfg: {
@@ -1651,9 +1684,10 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1651
1684
  bgTint: cfg.bgTint || { light: { color: "#1a2332", mask: 0.28 }, dark: { color: "#1a2332", mask: 0.28 } },
1652
1685
  bgColor: cfg.bgColor || "#1a2332", bgMask: cfg.bgMask ?? 0.28,
1653
1686
  bgBlur: cfg.bgBlur ?? 0, bgOpacity: cfg.bgOpacity ?? 1,
1654
- bgImage: cfg.bgImage ? { present: true, bytes: Math.round((cfg.bgImage.length * 3) / 4) } : { present: false },
1687
+ bgImage: imgMeta,
1655
1688
  },
1656
- bgImage: cfg.bgImage,
1689
+ bgImage: imgMeta,
1690
+ bgImageUrl: imgUrl,
1657
1691
  });
1658
1692
  return;
1659
1693
  }
@@ -1661,16 +1695,21 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1661
1695
  const body = await readJsonBody(req, 4 * 1024 * 1024);
1662
1696
  const current = await readGlassJSON();
1663
1697
  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)) {
1698
+ // 背景图改为"文件 + 引用": 用户上传的 data-URI 存入 current.uri, 配置只存 "current" 引用
1699
+ if (typeof next.bgImage === "string" && next.bgImage.startsWith("data:image/")) {
1668
1700
  try {
1669
1701
  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 (_) {}
1702
+ await writeFile(currentBgFile, next.bgImage, "utf8");
1703
+ next.bgImage = "current";
1704
+ } catch (_) { next.bgImage = ""; }
1705
+ } else if ("bgImage" in body && body.bgImage === "") {
1706
+ next.bgImage = ""; // 显式清除
1707
+ try { await unlink(currentBgFile); } catch (_) {}
1673
1708
  }
1709
+ // 只保留当前背景图, 删除历史旧图
1710
+ await pruneBackgrounds();
1711
+ await mkdir(glassDir, { recursive: true, mode: 0o700 });
1712
+ await writeFile(glassFile, JSON.stringify(next, null, 2), "utf8");
1674
1713
  sendJson(res, 200, { ok: true, saved: true });
1675
1714
  return;
1676
1715
  }
@@ -1691,14 +1730,32 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1691
1730
  catch { sendJson(res, 404, { error: "not found" }); return; }
1692
1731
  }
1693
1732
  let list = [];
1694
- try { list = (await readdir(bgDir)).filter((n) => n.endsWith(".uri")).sort().reverse().slice(0, 30); } catch (_) {}
1733
+ // 只保留当前背景图, 删除历史旧图; 列表排除当前图
1734
+ try { await pruneBackgrounds(); } catch (_) {}
1735
+ try { list = (await readdir(bgDir)).filter((n) => n.endsWith(".uri") && n !== "current.uri").sort().reverse().slice(0, 30); } catch (_) {}
1695
1736
  sendJson(res, 200, { list });
1696
1737
  } catch (error) {
1697
1738
  sendError(res, error, onError);
1698
1739
  }
1699
1740
  };
1741
+ // 返回当前背景图(文件存的 data-URI 解码为真实图片)
1742
+ const glassBackground = async (req, res) => {
1743
+ try {
1744
+ requireTrusted(req);
1745
+ const cfg = await readGlassJSON();
1746
+ if (cfg.bgImage !== "current") { sendJson(res, 404, { error: "no background" }); return; }
1747
+ const buf = await readFile(currentBgFile, "utf8");
1748
+ const d = decodeDataUri(buf);
1749
+ if (!d) { sendJson(res, 404, { error: "bad image" }); return; }
1750
+ const bytes = Buffer.from(d.base64, "base64");
1751
+ res.writeHead(200, { "content-type": d.mime, "cache-control": "no-store", "x-content-type-options": "nosniff" });
1752
+ res.end(bytes);
1753
+ } catch (error) {
1754
+ sendError(res, error, onError);
1755
+ }
1756
+ };
1700
1757
 
1701
- return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, glassConfig, glassBackgrounds };
1758
+ return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, glassConfig, glassBackgrounds, glassBackground };
1702
1759
  }
1703
1760
 
1704
1761
  /** 人类可读文件大小。 */
@@ -2609,6 +2666,12 @@ export async function apply(ctx, config = {}) {
2609
2666
  handler: handlers.glassBackgrounds,
2610
2667
  }), "dsh-long-plugins: glass-ui backgrounds route");
2611
2668
 
2669
+ ctx.effect(() => ctx.webServer.register({
2670
+ kind: "exact",
2671
+ path: "/api/dsh-uploads/glass-background",
2672
+ handler: handlers.glassBackground,
2673
+ }), "dsh-long-plugins: glass-ui background image route");
2674
+
2612
2675
  ctx.effect(() => ctx.webServer.register({
2613
2676
  kind: "exact",
2614
2677
  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.3.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",