dsh-long-plugins 1.3.10 → 2.1.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/README.md CHANGED
@@ -124,6 +124,26 @@ MIT
124
124
 
125
125
  ## Changelog / 更新记录
126
126
 
127
+ ### v2.1.0
128
+ - 增强:毛玻璃界面(玻璃拟态)进一步打磨。
129
+ - 主题感知磨砂:磨砂卡片颜色跟随系统深色/浅色主题自动反转(浅色=白磨砂深字 / 深色=深磨砂浅字)。
130
+ - 会话区内容反底色全透明化:去掉代码块/提示/气泡等反底色小块,统一融入磨砂卡片。
131
+ - 会话区「启用玻璃」开关:可单独禁用会话区磨砂(还原原生)。
132
+ - 会话区/输入区各自独立「罩色 + 罩强度 + 透明度」调节。
133
+ - 新增「背景图罩」:背景图上叠一层半透明罩色,深度可调。
134
+ - 手机端/窄屏(≤768px)自动禁用毛玻璃,还原 DSH 原生。
135
+ - 输入卡片/底部白色层透明化,更透背景。
136
+ - 修复:输入框下拉/悬浮菜单(`/` 命令、`@` 提及)背景透明,改为不透明随主题色。
137
+ - 修复:玻璃界面自定义背景图上传超大文件报 `Unexpected token '<'` 错误——上传上限提升到 4MiB。
138
+
139
+ ### v2.0.0
140
+ - 新增:毛玻璃界面(玻璃拟态)。为 DSH 网页叠加自定义毛玻璃效果,将界面打磨成"玻璃"质感。
141
+ - **自定义背景图**:上传一张背景图铺满整个页面(含左边栏、顶部、会话区),实现桌面壁纸式的背景;左边栏 / 顶部半透明透出背景图,界面更通透。
142
+ - **分区独立磨砂与罩色**:会话区、输入区各自独立设置「罩色 + 罩强度」,互不影响,实现"玻璃卡片"的分区质感;会话窗口磨砂玻璃悬浮卡片效果。
143
+ - **磨砂模糊**:全局磨砂模糊强度可调(0–80px),文字清晰可读。
144
+ - **跟随系统主题**:界面文字/内容颜色跟随系统深色/浅色主题,不强制改色,保证任意主题下可读。
145
+ - **设置面板「毛玻璃界面」**:总开关、背景图上传、磨砂模糊、会话区/输入区各自的罩色与罩强度,实时保存生效。
146
+
127
147
  ### v1.3.10
128
148
  - 新增 `.xlsx` 浏览器端预览:用 SheetJS 解析并显示为表格(行列标头、合并单元格、多工作表切换),预览支持 − / + / 适合宽度缩放,默认整表适配;浏览器端处理,不占用 NAS 本地资源。
129
149
  - 工作区浏览、工作区文件、上传文件列表中的 `.xlsx` 均接入该预览页。
package/client/client.js CHANGED
@@ -3034,6 +3034,349 @@ window.__ModuleLoader__.load({
3034
3034
  },
3035
3035
  }
3036
3036
 
3037
+ // ===== dsh-long-plugins: 毛玻璃界面 (glass UI) =====
3038
+ // 简洁方案:共用一张背景图铺满 body;左边栏 + 顶部各加一层半透明"罩";
3039
+ // 会话窗口(centerCol)做成磨砂玻璃悬浮卡片(backdrop-filter blur + 半透明背景)。
3040
+ const GLASS_CSS = `
3041
+ html[data-dsh-glass="on"] body { position: relative; }
3042
+ /* 方案A:背景图铺满整页(含左栏/顶栏/底部,全透明透出);仅会话滚动区做磨砂玻璃卡片。 */
3043
+ /* 左栏内部容器(或其它用此变量的)也透明,让背景图透到左栏。只覆盖左栏专用变量,避免影响设置面板。 */
3044
+ html[data-dsh-glass="on"] body {
3045
+ --dsw-specific-sidebar-fill: transparent !important;
3046
+ }
3047
+ /* 左栏 / 顶栏 / 底部:完全透明,让 frame 背景图连成一张透出(不做罩/不做磨砂) */
3048
+ html[data-dsh-glass="on"] #root [class*="sidebarCol"],
3049
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_header"],
3050
+ html[data-dsh-glass="on"] #root [class*="centerCol"],
3051
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_root"] {
3052
+ background-color: transparent !important;
3053
+ backdrop-filter: none;
3054
+ -webkit-backdrop-filter: none;
3055
+ }
3056
+ /* 左栏内部根 / 底部容器也用透明(这些是 DSH 当前版本哈希类名;背景图要透到左栏与底部) */
3057
+ html[data-dsh-glass="on"] #root [class*="hHd-Xa_root"],
3058
+ html[data-dsh-glass="on"] #root [class*="FJxK0a_root"] {
3059
+ background-color: transparent !important;
3060
+ }
3061
+ /* 会话滚动区:磨砂玻璃悬浮卡片(居中收窄,不占满宽度,四周透背景图) */
3062
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"] {
3063
+ background: linear-gradient(var(--dsh-glass-session-white, rgba(255,255,255,0.5)), var(--dsh-glass-session-white, rgba(255,255,255,0.5))) var(--dsh-glass-session-bg, rgba(26,35,50,0.45)) !important;
3064
+ backdrop-filter: blur(var(--dsh-glass-blur, 16px));
3065
+ -webkit-backdrop-filter: blur(var(--dsh-glass-blur, 16px));
3066
+ border-radius: 16px;
3067
+ overflow: auto;
3068
+ margin: 0 auto !important;
3069
+ /* 柔和阴影 + 边缘渐隐,让磨砂卡片边缘过渡自然(悬浮玻璃质感) */
3070
+ box-shadow: 0 8px 40px rgba(0,0,0,0.28), 0 2px 12px rgba(0,0,0,0.18);
3071
+ }
3072
+ /* 滚动条:细、半透明、圆角,且左移贴在内容区(不占卡片最右边缘),让右边缘过渡柔和 */
3073
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"]::-webkit-scrollbar { width: 8px; }
3074
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"]::-webkit-scrollbar-track { background: transparent; }
3075
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"]::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.35); border-radius: 8px; }
3076
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"]::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.5); }
3077
+ /* 会话区禁用玻璃:scrollBody 还原为透明、无磨砂、无圆角/阴影(保持 DSH 原生会话区外观) */
3078
+ html[data-dsh-glass="on"][data-dsh-session-glass="off"] #root [class*="wSkVaW_scrollBody"] {
3079
+ background-color: transparent !important;
3080
+ background-image: none !important;
3081
+ backdrop-filter: none !important;
3082
+ -webkit-backdrop-filter: none !important;
3083
+ border-radius: 0 !important;
3084
+ box-shadow: none !important;
3085
+ }
3086
+ /* 底部输入卡片:深色半透明磨砂(参考图:深色玻璃卡片 + 白字,与背景区分开) */
3087
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] {
3088
+ background: linear-gradient(var(--dsh-glass-input-white, rgba(255,255,255,0.3)), var(--dsh-glass-input-white, rgba(255,255,255,0.3))) var(--dsh-glass-input-bg, rgba(26,35,50,0.6)) !important;
3089
+ backdrop-filter: blur(var(--dsh-glass-blur, 16px));
3090
+ -webkit-backdrop-filter: blur(var(--dsh-glass-blur, 16px));
3091
+ }
3092
+
3093
+ html[data-dsh-glass="off"] #root [class*="sidebarCol"],
3094
+ html[data-dsh-glass="off"] #root [class*="header"],
3095
+ html[data-dsh-glass="off"] #root [class*="centerCol"],
3096
+ html[data-dsh-glass="off"] #root [class*="wSkVaW_root"],
3097
+ html[data-dsh-glass="off"] #root [class*="wSkVaW_scrollBody"],
3098
+ html[data-dsh-glass="off"] #root [class*="uV2eYG_card"] { background-color: transparent !important; backdrop-filter: none; }
3099
+ /* 用户消息气泡:浅色半透明底(随主题),与 AI 消息(透明/磨砂)区分 */
3100
+ html[data-dsh-glass="on"] #root [class*="ngdEzaw_bubble"]:not([class*="button"]) {
3101
+ background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 62%, transparent) !important;
3102
+ }
3103
+ /* 会话区所有内容元素背景透明化:去掉"反底色"小块衬底,统一融入磨砂卡片 */
3104
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"] *:not([class*="button"]):not([class*="bubble"]):not([class*="_menu"]) {
3105
+ background-color: transparent !important;
3106
+ }
3107
+ /* 会话区内容衬底(代码块/提示/气泡/引用等)玻璃开启时透明化,融入磨砂卡片,去掉浅色小块衬底 */
3108
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"] [class*="n_block"],
3109
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"] [class*="n_bannerWrap"],
3110
+
3111
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"] [class*="n_plain"],
3112
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"] [class*="n_copyButton"],
3113
+ html[data-dsh-glass="on"] #root [class*="wSkVaW_scrollBody"] [class*="md-code-block"] {
3114
+ background-color: transparent !important;
3115
+ }
3116
+ /* uV2eYG_add: composer 里随内容变化的白色衬底,玻璃开启时透明化 */
3117
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_add"] {
3118
+ background-color: transparent !important;
3119
+ }
3120
+ /* 输入框下拉/悬浮菜单(如 / 命令、@ 提及):背景给不透明随主题色,避免叠在会话磨砂卡上重叠看不清 */
3121
+ html[data-dsh-glass="on"] #root [class*="_menu"][class*="_menu"][class*="_menu"][class*="_menu"] {
3122
+ background-color: var(--dsw-specific-input-major, #e8edf3) !important;
3123
+ background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 97%, transparent) !important;
3124
+ backdrop-filter: none !important;
3125
+ -webkit-backdrop-filter: none !important;
3126
+ }
3127
+ /* 输入框旁权限/功能选择下拉(盾牌 Full access 等 Radix 风格菜单):与 / 命令菜单同样处理 */
3128
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_root_"]:has([class*="_list_"]) {
3129
+ background-color: var(--dsw-specific-input-major, #e8edf3) !important;
3130
+ background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 97%, transparent) !important;
3131
+ backdrop-filter: none !important;
3132
+ -webkit-backdrop-filter: none !important;
3133
+ }
3134
+ html[data-dsh-glass="on"] #root [class*="uV2eYG_card"] [class*="_list_"] {
3135
+ background-color: var(--dsw-specific-input-major, #e8edf3) !important;
3136
+ background-color: color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) 97%, transparent) !important;
3137
+ backdrop-filter: none !important;
3138
+ -webkit-backdrop-filter: none !important;
3139
+ }
3140
+ .dsh-glass-section{display:flex;flex-direction:column;gap:16px;min-width:0;padding:4px 2px 24px;color:var(--dsw-alias-label-primary)}
3141
+ .dsh-glass-head{display:flex;align-items:center;justify-content:space-between;gap:16px}
3142
+ .dsh-glass-head h2{margin:0;font-size:20px;line-height:28px}
3143
+ .dsh-glass-head p{margin:4px 0 0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
3144
+ .dsh-glass-switch{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;user-select:none}
3145
+ .dsh-glass-switch input{width:34px;height:20px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer}
3146
+ .dsh-glass-card{display:grid;gap:12px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-input-major)}
3147
+ .dsh-glass-zone-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
3148
+ .dsh-glass-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
3149
+ .dsh-glass-row label{font-size:13px;color:var(--dsw-alias-label-primary);flex:none}
3150
+ .dsh-glass-range{flex:1;min-width:160px;max-width:320px;accent-color:var(--dsw-alias-state-business-primary)}
3151
+ .dsh-glass-val{flex:none;font-size:12px;color:var(--dsw-alias-label-secondary);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}
3152
+ .dsh-glass-swatches{display:flex;gap:8px;flex-wrap:wrap}
3153
+ .dsh-glass-swatch{width:26px;height:26px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;padding:0;transition:transform .12s}
3154
+ .dsh-glass-swatch:hover{transform:scale(1.12)}
3155
+ .dsh-glass-swatch.on{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
3156
+ .dsh-glass-colorpicker{display:inline-flex;align-items:center;gap:8px;padding:5px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;cursor:pointer}
3157
+ .dsh-glass-colorpicker input[type=color]{width:30px;height:24px;border:none;background:none;padding:0;cursor:pointer}
3158
+ .dsh-glass-bg{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
3159
+ .dsh-glass-file{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font-size:12px;cursor:pointer}
3160
+ .dsh-glass-file input{display:none}
3161
+ .dsh-glass-actions{display:flex;gap:8px;align-items:center;justify-content:flex-end}
3162
+ .dsh-glass-actions button{padding:7px 16px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;cursor:pointer}
3163
+ .dsh-glass-actions .primary{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:#fff}
3164
+ .dsh-glass-actions button:disabled{opacity:.55;cursor:not-allowed}
3165
+ .dsh-glass-error{padding:10px 12px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);font-size:12px}
3166
+ .dsh-glass-note{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}
3167
+ `
3168
+ const GLASS_SWATCHES = ['#0f1420', '#14161a', '#1a2530', '#26324d', '#153a3a', '#3b2a4a', '#4a1f1f', '#2d3748']
3169
+ const glassText = (e) => (e && e.message) ? e.message : String(e)
3170
+ function glassImageInput(value, onChange) {
3171
+ return React.createElement('label', { className: 'dsh-glass-file' },
3172
+ React.createElement('span', null, '上传背景图'),
3173
+ React.createElement('input', { type: 'file', accept: 'image/png,image/jpeg,image/webp,image/gif', onChange: (e) => {
3174
+ const f = e.target.files && e.target.files[0]
3175
+ if (!f) return
3176
+ const reader = new FileReader()
3177
+ reader.onload = () => onChange(String(reader.result || ''))
3178
+ reader.readAsDataURL(f)
3179
+ } }),
3180
+ value ? React.createElement('span', { style: { width: 22, height: 22, borderRadius: 4, backgroundImage: 'url(' + value + ')', backgroundSize: 'cover', display: 'inline-block' } }) : null,
3181
+ )
3182
+ }
3183
+ // 会话区/输入区共用设置卡:罩色 + 罩强度
3184
+ const GlassZoneCard = ({ title, zone, onChange, showGlassToggle }) => {
3185
+ const z = zone || { color: '#1a2332', mask: 0.45, enabled: true }
3186
+ const setPatch = (p) => onChange({ ...z, ...p })
3187
+ return React.createElement('div', { className: 'dsh-glass-card' },
3188
+ React.createElement('div', { className: 'dsh-glass-zone-title' }, title),
3189
+ showGlassToggle ? React.createElement('div', { className: 'dsh-glass-row' },
3190
+ React.createElement('label', { className: 'dsh-glass-switch' }, React.createElement('input', { type: 'checkbox', checked: z.enabled !== false, onChange: (e) => setPatch({ enabled: e.target.checked }) }), title + ' 启用玻璃'),
3191
+ ) : null,
3192
+ React.createElement('div', { className: 'dsh-glass-row' },
3193
+ React.createElement('label', null, title + ' 罩色'),
3194
+ React.createElement('div', { className: 'dsh-glass-swatches' },
3195
+ GLASS_SWATCHES.map((c) => React.createElement('button', { key: c, type: 'button', className: 'dsh-glass-swatch' + ((z.color || '#1a2332') === c ? ' on' : ''), style: { background: c }, 'aria-label': '颜色 ' + c, onClick: () => setPatch({ color: c }) })),
3196
+ ),
3197
+ React.createElement('label', { className: 'dsh-glass-colorpicker' }, '色盘', React.createElement('input', { type: 'color', value: z.color || '#1a2332', onChange: (e) => setPatch({ color: e.target.value }) })),
3198
+ ),
3199
+ React.createElement('div', { className: 'dsh-glass-row' },
3200
+ React.createElement('label', null, title + ' 罩强度'),
3201
+ React.createElement('input', { type: 'range', className: 'dsh-glass-range', min: 0, max: 0.95, step: 0.01, value: z.mask ?? 0.45, onChange: (e) => setPatch({ mask: Number(e.target.value) }) }),
3202
+ React.createElement('span', { className: 'dsh-glass-val' }, `${Math.round((Number(z.mask ?? 0.45)) * 100)}%`),
3203
+ ),
3204
+ React.createElement('div', { className: 'dsh-glass-row' },
3205
+ React.createElement('label', null, title + ' 透明度'),
3206
+ React.createElement('input', { type: 'range', className: 'dsh-glass-range', min: 0, max: 1, step: 0.01, value: z.opacity ?? 0.5, onChange: (e) => setPatch({ opacity: Number(e.target.value) }) }),
3207
+ React.createElement('span', { className: 'dsh-glass-val' }, `${Math.round((Number(z.opacity ?? 0.5)) * 100)}%`),
3208
+ ),
3209
+ )
3210
+ }
3211
+ const GlassSettingsSection = () => {
3212
+ const [state, setState] = React.useState({ loading: true, saving: false, error: '', cfg: null, bgImage: '' })
3213
+ const load = React.useCallback(async () => {
3214
+ setState((s) => ({ ...s, loading: true, error: '' }))
3215
+ try {
3216
+ const r = await fetch('/api/dsh-uploads/glass-config', { cache: 'no-store' })
3217
+ const b = await r.json()
3218
+ if (!r.ok) throw new Error(b.error || ('HTTP ' + r.status))
3219
+ setState((s) => ({ ...s, loading: false, cfg: b.cfg, bgImage: b.bgImage || '' }))
3220
+ } catch (e) { setState((s) => ({ ...s, loading: false, error: glassText(e) })) }
3221
+ }, [])
3222
+ React.useEffect(() => { load(); }, [load])
3223
+ const patch = React.useCallback((p) => setState((s) => ({ ...s, cfg: { ...(s.cfg || {}), ...p } })), [])
3224
+ const patchZone = React.useCallback((key, p) => setState((s) => ({ ...s, cfg: { ...(s.cfg || {}), [key]: p } })), [])
3225
+ const save = React.useCallback(async () => {
3226
+ setState((s) => ({ ...s, saving: true, error: '' }))
3227
+ try {
3228
+ const cfg = state.cfg || {}
3229
+ const payload = {
3230
+ enabled: !!cfg.enabled, blur: Number(cfg.blur) || 0,
3231
+ bgImage: state.bgImage || cfg.bgImage || '', bgMask: Number(cfg.bgMask) || 0.28, bgColor: cfg.bgColor || '#1a2332',
3232
+ zone: {
3233
+ session: { enabled: cfg.session && cfg.session.enabled !== false, color: (cfg.session && cfg.session.color) || '#1a2332', mask: Number((cfg.session && cfg.session.mask)) || 0.45, opacity: Number((cfg.session && cfg.session.opacity)) || 0.5 },
3234
+ input: { color: (cfg.input && cfg.input.color) || '#1a2332', mask: Number((cfg.input && cfg.input.mask)) || 0.6, opacity: Number((cfg.input && cfg.input.opacity)) || 0.55 },
3235
+ },
3236
+ }
3237
+ const r = await fetch('/api/dsh-uploads/glass-config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
3238
+ const b = await r.json()
3239
+ if (!r.ok) throw new Error(b.error || ('HTTP ' + r.status))
3240
+ window.__dshGlassApply && window.__dshGlassApply(payload)
3241
+ setState((s) => ({ ...s, saving: false, cfg: { ...(s.cfg || {}), ...payload }, bgImage: payload.bgImage, error: '' }))
3242
+ } catch (e) { setState((s) => ({ ...s, saving: false, error: glassText(e) })) }
3243
+ }, [state])
3244
+ const cfg = state.cfg || {}
3245
+ if (state.loading) return React.createElement('div', { className: 'dsh-glass-section' }, '加载中…')
3246
+ return React.createElement('div', { className: 'dsh-glass-section' },
3247
+ React.createElement('div', { className: 'dsh-glass-head' },
3248
+ React.createElement('div', null, React.createElement('h2', null, '毛玻璃界面'), React.createElement('p', null, '共用一张背景图铺满整页,仅消息滚动区做居中磨砂玻璃卡片,其余区域透出背景图。')),
3249
+ React.createElement('label', { className: 'dsh-glass-switch' }, React.createElement('input', { type: 'checkbox', checked: !!cfg.enabled, onChange: (e) => patch({ enabled: e.target.checked }) }), cfg.enabled ? '已开启' : '已关闭'),
3250
+ ),
3251
+ state.error ? React.createElement('div', { className: 'dsh-glass-error' }, state.error) : null,
3252
+ React.createElement('div', { className: 'dsh-glass-card' },
3253
+ React.createElement('div', { className: 'dsh-glass-row' },
3254
+ React.createElement('label', null, '磨砂模糊强度'),
3255
+ React.createElement('input', { type: 'range', className: 'dsh-glass-range', min: 0, max: 80, value: cfg.blur ?? 20, onChange: (e) => patch({ blur: Number(e.target.value) }) }),
3256
+ React.createElement('span', { className: 'dsh-glass-val' }, `${Number(cfg.blur ?? 20)} px`),
3257
+ ),
3258
+ React.createElement(GlassZoneCard, { title: '会话区', zone: cfg.session, onChange: (p) => patchZone('session', p), showGlassToggle: true }),
3259
+ React.createElement(GlassZoneCard, { title: '输入区', zone: cfg.input, onChange: (p) => patchZone('input', p) }),
3260
+ React.createElement('div', { className: 'dsh-glass-row' },
3261
+ React.createElement('label', null, '共用背景图'),
3262
+ React.createElement('div', { className: 'dsh-glass-bg' }, glassImageInput(state.bgImage, (v) => setState((s) => ({ ...s, bgImage: v })))),
3263
+ ),
3264
+ React.createElement('div', { className: 'dsh-glass-row' },
3265
+ React.createElement('label', null, '背景图罩'),
3266
+ React.createElement('input', { type: 'range', className: 'dsh-glass-range', min: 0, max: 0.95, step: 0.01, value: cfg.bgMask ?? 0.28, onChange: (e) => patch({ bgMask: Number(e.target.value) }) }),
3267
+ React.createElement('span', { className: 'dsh-glass-val' }, `${Math.round((Number(cfg.bgMask ?? 0.28)) * 100)}%`),
3268
+ ),
3269
+ React.createElement('p', { className: 'dsh-glass-note' }, '提示:背景图铺满整页(可加背景图罩);会话区/输入区各自使用自己的罩色、罩强度与透明度;左栏/顶部透出背景图。'),
3270
+ React.createElement('div', { className: 'dsh-glass-actions' },
3271
+ React.createElement('button', { type: 'button', onClick: load }, '重置'),
3272
+ React.createElement('button', { type: 'button', className: 'primary', disabled: state.saving, onClick: save }, state.saving ? '保存中…' : '保存生效'),
3273
+ ),
3274
+ ),
3275
+ )
3276
+ }
3277
+ const glassPlugin = {
3278
+ inject: ['slots'],
3279
+ apply(ctx) {
3280
+ const hexToRgb = (hex) => { const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(String(hex || '')); return m ? `${parseInt(m[1],16)},${parseInt(m[2],16)},${parseInt(m[3],16)}` : '15,20,32' }
3281
+ ctx.effect(() => {
3282
+ const style = document.createElement('style')
3283
+ style.dataset.plugin = 'dsh-long-plugins'
3284
+ style.dataset.pluginCss = 'dsh-long-plugins/glass'
3285
+ style.textContent = GLASS_CSS
3286
+ document.head.appendChild(style)
3287
+ return () => style.remove()
3288
+ }, 'dsh-long-plugins: glass styles')
3289
+ ctx.effect(() => {
3290
+ const root = document.documentElement
3291
+
3292
+ // 磨砂卡片与底部输入卡片对齐:宽度取输入卡片宽度,translateX 补偿使左右边界对齐输入卡。
3293
+ const centerScroll = () => {
3294
+ const el = document.querySelector('#root [class*="wSkVaW_scrollBody"]')
3295
+ if (!el) return
3296
+ const card = document.querySelector('#root [class*="uV2eYG_card"]')
3297
+ const r = el.getBoundingClientRect()
3298
+ const target = card ? card.getBoundingClientRect() : null
3299
+ // 不改磨砂卡片宽度(保留默认 width:min() 原始宽度),仅水平居中到输入卡片中心。
3300
+ const diff = (target ? (target.left + target.right) / 2 : window.innerWidth / 2) - (r.left + r.right) / 2
3301
+ el.style.transform = 'translateX(' + Math.round(diff) + 'px)'
3302
+ }
3303
+ window.addEventListener('resize', centerScroll)
3304
+ window.__dshGlassCenter = centerScroll
3305
+ // win 风格文字:给左栏/顶栏内的可见文字元素加白色+深色描边;跳过弹窗(modal/overlay/dialog/settings/panel)内元素。
3306
+
3307
+ window.__dshGlassApply = (cfg) => {
3308
+ const c = cfg || {}
3309
+ // 手机端/窄屏(<=768px):不开启毛玻璃,还原 DSH 原生样式(清背景图/罩变量/磨砂)。
3310
+ if (window.innerWidth <= 768) {
3311
+ root.setAttribute('data-dsh-glass', 'off')
3312
+ root.setAttribute('data-dsh-session-glass', 'off')
3313
+ root.style.setProperty('--dsh-glass-blur', '')
3314
+ root.style.setProperty('--dsh-glass-session-bg', '')
3315
+ root.style.setProperty('--dsh-glass-input-bg', '')
3316
+ root.style.setProperty('--dsh-glass-bg-zone', '')
3317
+ const b = document.body
3318
+ b.style.backgroundImage = 'none'; b.style.backgroundSize = ''; b.style.backgroundPosition = ''; b.style.backgroundRepeat = ''
3319
+ const fr = document.querySelector('#root > div > div') || document.querySelector('#root > div')
3320
+ if (fr) { fr.style.backgroundColor = ''; fr.style.backgroundImage = 'none'; fr.style.backgroundSize = ''; fr.style.backgroundPosition = ''; fr.style.backgroundRepeat = '' }
3321
+ return
3322
+ }
3323
+ root.setAttribute('data-dsh-glass', c.enabled ? 'on' : 'off')
3324
+ const sessOn = !(c.zone && c.zone.session && c.zone.session.enabled === false)
3325
+ root.setAttribute('data-dsh-session-glass', sessOn ? 'on' : 'off')
3326
+ const blur = Number(c.blur) || 16
3327
+ root.style.setProperty('--dsh-glass-blur', blur + 'px')
3328
+ const zs = (c.zone && c.zone.session) || { color: '#1a2332', mask: 0.45, opacity: 0.5 }
3329
+ const zi = (c.zone && c.zone.input) || { color: '#1a2332', mask: 0.6, opacity: 0.55 }
3330
+ root.style.setProperty('--dsh-glass-session-bg', `rgba(${hexToRgb(zs.color)},${Math.min(0.95, Math.max(0, Number(zs.mask) || 0.45))})`)
3331
+ root.style.setProperty('--dsh-glass-input-bg', `rgba(${hexToRgb(zi.color)},${Math.min(0.95, Math.max(0, Number(zi.mask) || 0.6))})`)
3332
+ const sa = Math.round(Math.min(1, Math.max(0, Number(zs.opacity) || 0.5)) * 100)
3333
+ root.style.setProperty('--dsh-glass-session-white', `color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) ${sa}%, transparent)`)
3334
+ const ia = Math.round(Math.min(1, Math.max(0, Number(zi.opacity) === 0 ? 0 : (Number(zi.opacity) || 0.3))) * 100)
3335
+ root.style.setProperty('--dsh-glass-input-white', `color-mix(in srgb, var(--dsw-specific-input-major, #e8edf3) ${ia}%, transparent)`)
3336
+ // 背景图罩:整页背景图上叠一层半透明罩色
3337
+ const bgc = c.bgColor || '#1a2332'
3338
+ const bgm = Math.min(0.95, Math.max(0, Number(c.bgMask) || 0.28))
3339
+ root.style.setProperty('--dsh-glass-bg-zone', `rgba(${hexToRgb(bgc)},${bgm})`)
3340
+
3341
+ const body = document.body
3342
+ const frame = document.querySelector('#root > div > div') || document.querySelector('#root > div')
3343
+ const applyBg = (el) => {
3344
+ if (!el) return
3345
+ if (c.enabled && c.bgImage) {
3346
+ // 背景图 + 罩色:先叠加一层背景罩(linear-gradient),再铺背景图,实现"背景图上有罩"。
3347
+ const gz = root.style.getPropertyValue('--dsh-glass-bg-zone') || 'rgba(26,35,50,0.28)'
3348
+ el.style.backgroundImage = `linear-gradient(${gz},${gz}), url('${c.bgImage}')`
3349
+ el.style.backgroundSize = 'cover'
3350
+ el.style.backgroundPosition = 'center'
3351
+ el.style.backgroundRepeat = 'no-repeat'
3352
+ } else {
3353
+ el.style.backgroundImage = 'none'
3354
+ el.style.backgroundSize = ''
3355
+ el.style.backgroundPosition = ''
3356
+ el.style.backgroundRepeat = ''
3357
+ }
3358
+ }
3359
+ // body 背景色也设透明,避免 body 白色露底形成"白色遮盖层"(背景图 cover 铺底即可)。
3360
+ if (c.enabled && c.bgImage) { body.style.backgroundColor = 'transparent' } else { body.style.backgroundColor = '' }
3361
+ applyBg(body)
3362
+ if (frame) { frame.style.backgroundColor = 'transparent'; applyBg(frame) }
3363
+ centerScroll()
3364
+ }
3365
+ return () => {
3366
+ window.__dshGlassApply = undefined
3367
+ window.removeEventListener('resize', centerScroll)
3368
+ window.__dshGlassCenter = undefined
3369
+ }
3370
+ }, 'dsh-long-plugins: glass applier')
3371
+ ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'glass-ui', order: 40, label: '毛玻璃界面' }, GlassSettingsSection))
3372
+ ctx.effect(() => {
3373
+ fetch('/api/dsh-uploads/glass-config', { cache: 'no-store' }).then((r) => r.json()).then((b) => {
3374
+ if (b && b.cfg) { window.__dshGlassApply && window.__dshGlassApply({ enabled: b.cfg.enabled, blur: b.cfg.blur, mask: b.cfg.mask, color: b.cfg.color, bgImage: b.bgImage }) }
3375
+ }).catch(() => {})
3376
+ }, 'dsh-long-plugins: glass boot')
3377
+ },
3378
+ }
3379
+
3037
3380
  const inject = Array.from(new Set([
3038
3381
  ...uploadPlugin.inject,
3039
3382
  ...skillDocsPlugin.inject,
@@ -3041,6 +3384,7 @@ window.__ModuleLoader__.load({
3041
3384
  ...mobilePlugin.inject,
3042
3385
  ...workspaceFilesPlugin.inject,
3043
3386
  ...turnRulerPlugin.inject,
3387
+ ...glassPlugin.inject,
3044
3388
  ]))
3045
3389
 
3046
3390
  function apply(ctx) {
@@ -3057,6 +3401,7 @@ window.__ModuleLoader__.load({
3057
3401
  safeApply('mobile-hamburger', (c) => mobilePlugin.apply(c))
3058
3402
  safeApply('workspace-files', (c) => workspaceFilesPlugin.apply(c))
3059
3403
  safeApply('turn-ruler', (c) => turnRulerPlugin.apply(c))
3404
+ safeApply('glass', (c) => glassPlugin.apply(c))
3060
3405
  }
3061
3406
 
3062
3407
  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": "1.3.9",
4
+ "version": "2.1.0",
5
5
  "entry": {
6
6
  "name": "dsh-long-plugins",
7
7
  "inject": [
package/lib/index.js CHANGED
@@ -190,20 +190,24 @@ export function isTrustedUploadRequest(req, trustedHosts = []) {
190
190
  }
191
191
 
192
192
  /** Read a JSON request body (bounded; 1 MiB to fit large edited documents). */
193
- function readJsonBody(request) {
193
+ function readJsonBody(request, maxBytes = 1024 * 1024) {
194
194
  return new Promise((resolvePromise, rejectPromise) => {
195
195
  let size = 0;
196
+ let tooBig = false;
196
197
  const chunks = [];
197
198
  request.on("data", (chunk) => {
199
+ if (tooBig) return;
198
200
  size += chunk.length;
199
- if (size > 1024 * 1024) {
200
- rejectPromise(new Error("body too large"));
201
- request.destroy();
201
+ if (size > maxBytes) {
202
+ tooBig = true;
203
+ chunks.length = 0;
204
+ rejectPromise(new HttpError(413, "body too large"));
202
205
  return;
203
206
  }
204
207
  chunks.push(chunk);
205
208
  });
206
209
  request.on("end", () => {
210
+ if (tooBig) return;
207
211
  try {
208
212
  resolvePromise(JSON.parse(Buffer.concat(chunks).toString("utf8")));
209
213
  } catch (error) {
@@ -666,6 +670,36 @@ async function officePreviewHtml(name, buffer) {
666
670
  return null;
667
671
  }
668
672
 
673
+ /** 仅保留毛玻璃配置允许字段并 clamp,避免写入任意/超大内容。背景图只收 data:image/*;base64 且限 2MB。 */
674
+ function sanitizeGlass(input) {
675
+ const out = {};
676
+ if ("enabled" in input) out.enabled = input.enabled === true;
677
+ if ("blur" in input) { const n = Number(input.blur); out.blur = Number.isFinite(n) ? Math.max(0, Math.min(80, Math.round(n))) : undefined; }
678
+ if ("mask" in input) { const n = Number(input.mask); out.mask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
679
+ if ("color" in input) out.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(input.color)) ? String(input.color) : undefined;
680
+ const sanitizeUri = (v) => { const s = String(v ?? ""); return (/^data:image\/(png|jpe?g|webp|gif);base64,/.test(s) && s.length <= 2 * 1024 * 1024 + 256) ? s : undefined; };
681
+ if ("bgImage" in input) out.bgImage = sanitizeUri(input.bgImage);
682
+ if ("bgColor" in input) out.bgColor = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(input.bgColor)) ? String(input.bgColor) : undefined;
683
+ if ("bgMask" in input) { const n = Number(input.bgMask); out.bgMask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
684
+ // 会话区/输入区各自独立罩色+罩强度
685
+ if (input.zone && typeof input.zone === "object") {
686
+ const zo = {};
687
+ for (const key of ["session", "input"]) {
688
+ const z = input.zone[key];
689
+ if (z && typeof z === "object") {
690
+ const o = {};
691
+ if ("enabled" in z) o.enabled = z.enabled === true;
692
+ if ("color" in z) o.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(z.color)) ? String(z.color) : undefined;
693
+ if ("mask" in z) { const n = Number(z.mask); o.mask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
694
+ if ("opacity" in z) { const n = Number(z.opacity); o.opacity = Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : undefined; }
695
+ zo[key] = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
696
+ }
697
+ }
698
+ if (Object.keys(zo).length) out.zone = zo;
699
+ }
700
+ return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== undefined));
701
+ }
702
+
669
703
  export function createHandlers(options = {}) {
670
704
  const root = resolve(options.root || resolveUploadRoot());
671
705
  const maxFileBytes = positiveInteger(options.maxFileBytes, resolveMaxFileBytes());
@@ -1554,7 +1588,53 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1554
1588
  }
1555
1589
  };
1556
1590
 
1557
- return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset };
1591
+ // ---- 毛玻璃界面 (glass UI) 配置:读写 ~/.dsh-long-plugins/glass.json ----
1592
+ // 背景图以 data-URI 存进配置,随配置持久化。enabled=总开关;blur=磨砂模糊;mask=整页罩强度;
1593
+ // color=叠加/罩色;bgImage=共用背景图。顶栏/左栏的"罩"与整页共用 mask+color(不再单独存,简化)。
1594
+ const GLASS_DEFAULTS = {
1595
+ enabled: false, blur: 20, bgImage: "", bgColor: "#1a2332", bgMask: 0.28,
1596
+ zone: { session: { color: "#1a2332", mask: 0.45, opacity: 0.5 }, input: { color: "#1a2332", mask: 0.6, opacity: 0.55 } },
1597
+ };
1598
+ const glassDir = resolve(homedir(), ".dsh-long-plugins");
1599
+ const glassFile = join(glassDir, "glass.json");
1600
+ async function readGlassJSON() {
1601
+ try { const raw = JSON.parse(await readFile(glassFile, "utf8")); return { ...GLASS_DEFAULTS, ...(raw && typeof raw === "object" ? raw : {}) }; }
1602
+ catch { return { ...GLASS_DEFAULTS }; }
1603
+ }
1604
+ const glassConfig = async (req, res) => {
1605
+ try {
1606
+ requireTrusted(req);
1607
+ if (req.method === "GET" || req.method === "HEAD") {
1608
+ const cfg = await readGlassJSON();
1609
+ sendJson(res, 200, {
1610
+ found: true,
1611
+ cfg: {
1612
+ enabled: cfg.enabled, blur: cfg.blur,
1613
+ session: cfg.zone?.session || { enabled: true, color: "#1a2332", mask: 0.45, opacity: 0.5 },
1614
+ input: cfg.zone?.input || { color: "#1a2332", mask: 0.6, opacity: 0.55 },
1615
+ bgColor: cfg.bgColor || "#1a2332", bgMask: cfg.bgMask ?? 0.28,
1616
+ bgImage: cfg.bgImage ? { present: true, bytes: Math.round((cfg.bgImage.length * 3) / 4) } : { present: false },
1617
+ },
1618
+ bgImage: cfg.bgImage,
1619
+ });
1620
+ return;
1621
+ }
1622
+ if (req.method === "POST") {
1623
+ const body = await readJsonBody(req, 4 * 1024 * 1024);
1624
+ const current = await readGlassJSON();
1625
+ const next = { ...current, ...sanitizeGlass(typeof body === "object" && body !== null ? body : {}) };
1626
+ await mkdir(glassDir, { recursive: true, mode: 0o700 });
1627
+ await writeFile(glassFile, JSON.stringify(next, null, 2), "utf8");
1628
+ sendJson(res, 200, { ok: true, saved: true });
1629
+ return;
1630
+ }
1631
+ methodNotAllowed(res, ["GET", "HEAD", "POST"]);
1632
+ } catch (error) {
1633
+ sendError(res, error, onError);
1634
+ }
1635
+ };
1636
+
1637
+ return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, glassConfig };
1558
1638
  }
1559
1639
 
1560
1640
  /** 人类可读文件大小。 */
@@ -2453,6 +2533,12 @@ export async function apply(ctx, config = {}) {
2453
2533
  handler: handlers.xlsxPreviewAsset,
2454
2534
  }), "dsh-long-plugins: xlsx-preview vendor assets");
2455
2535
 
2536
+ ctx.effect(() => ctx.webServer.register({
2537
+ kind: "exact",
2538
+ path: "/api/dsh-uploads/glass-config",
2539
+ handler: handlers.glassConfig,
2540
+ }), "dsh-long-plugins: glass-ui config route");
2541
+
2456
2542
  ctx.effect(() => ctx.webServer.register({
2457
2543
  kind: "exact",
2458
2544
  path: "/api/dsh-uploads/workspace-browse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-long-plugins",
3
- "version": "1.3.10",
3
+ "version": "2.1.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",