dsh-gufa-code 0.0.0-stage → 1.0.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.
Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -2
  3. package/client.js +11548 -0
  4. package/cordis.patch.yml +8 -0
  5. package/docs/icon-180.png +0 -0
  6. package/docs/icon.svg +18 -0
  7. package/docs/market-submission.md +32 -0
  8. package/docs/screenshot-editor.jpg +0 -0
  9. package/docs/screenshot-ide.jpg +0 -0
  10. package/host/assets.js +31 -0
  11. package/host/chat.js +523 -0
  12. package/host/context.js +438 -0
  13. package/host/dev.js +35 -0
  14. package/host/fs.js +306 -0
  15. package/host/git.js +532 -0
  16. package/host/project.js +305 -0
  17. package/host/ssh.js +54 -0
  18. package/host/terminal-pty.js +237 -0
  19. package/host/terminal.js +947 -0
  20. package/icon.svg +18 -0
  21. package/index.js +63 -0
  22. package/locale/en.json +121 -0
  23. package/locale/zh.json +121 -0
  24. package/package.json +66 -4
  25. package/scripts/build-client.mjs +38 -0
  26. package/scripts/selftest-client.mjs +457 -0
  27. package/scripts/selftest-host.mjs +197 -0
  28. package/scripts/selftest-http.mjs +284 -0
  29. package/scripts/selftest-router.mjs +101 -0
  30. package/scripts/selftest-terminal-client.mjs +314 -0
  31. package/scripts/selftest-terminal-pty.mjs +190 -0
  32. package/scripts/selftest-terminal.mjs +351 -0
  33. package/scripts/test-chat-render.mjs +702 -0
  34. package/scripts/test-chat.mjs +761 -0
  35. package/scripts/test-editor.mjs +516 -0
  36. package/scripts/verify-workspace-sessions.mjs +135 -0
  37. package/src/client/00-core.js +1686 -0
  38. package/src/client/10-icons.js +205 -0
  39. package/src/client/20-editor.js +1318 -0
  40. package/src/client/30-terminal.js +1510 -0
  41. package/src/client/40-chat.js +1184 -0
  42. package/src/client/46-gitgraph.js +278 -0
  43. package/src/client/50-panels.js +916 -0
  44. package/src/client/55-backdrop.js +508 -0
  45. package/src/client/60-dock.js +662 -0
  46. package/src/client/85-shortcuts.js +516 -0
  47. package/src/client/90-app.js +2272 -0
  48. package/src/client/99-main.js +449 -0
  49. package/vendor/monaco/LICENSE +21 -0
  50. package/vendor/monaco/ThirdPartyNotices.txt +448 -0
  51. package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  52. package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  53. package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  54. package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  55. package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  56. package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  57. package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  58. package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  59. package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  60. package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  61. package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  62. package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  63. package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  64. package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  65. package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  66. package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  67. package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  68. package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  69. package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  70. package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  71. package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  72. package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  73. package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  74. package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  75. package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  76. package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  77. package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  78. package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  79. package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  80. package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  81. package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  82. package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  83. package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  84. package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  85. package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  86. package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  87. package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  88. package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  89. package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  90. package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  91. package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  92. package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  93. package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  94. package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  95. package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  96. package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  97. package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  98. package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  99. package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  100. package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  101. package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  102. package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  103. package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  104. package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  105. package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  106. package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  107. package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  108. package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  109. package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  110. package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  111. package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  112. package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  113. package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  114. package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  115. package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  116. package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  117. package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  118. package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  119. package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  120. package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  121. package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  122. package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  123. package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  124. package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  125. package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  126. package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  127. package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  128. package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  129. package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  130. package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  131. package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  132. package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  133. package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  134. package/vendor/monaco/vs/editor/editor.main.css +8 -0
  135. package/vendor/monaco/vs/editor/editor.main.js +798 -0
  136. package/vendor/monaco/vs/language/css/cssMode.js +13 -0
  137. package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  138. package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  139. package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  140. package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  141. package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  142. package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  143. package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  144. package/vendor/monaco/vs/loader.js +11 -0
  145. package/vendor/monaco/vs/nls.messages.de.js +21 -0
  146. package/vendor/monaco/vs/nls.messages.es.js +21 -0
  147. package/vendor/monaco/vs/nls.messages.fr.js +19 -0
  148. package/vendor/monaco/vs/nls.messages.it.js +19 -0
  149. package/vendor/monaco/vs/nls.messages.ja.js +21 -0
  150. package/vendor/monaco/vs/nls.messages.ko.js +19 -0
  151. package/vendor/monaco/vs/nls.messages.ru.js +21 -0
  152. package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
  153. package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
  154. package/vendor/xterm/LICENSE +21 -0
  155. package/vendor/xterm/LICENSE.addon-fit +19 -0
  156. package/vendor/xterm/addon-fit.js +2 -0
  157. package/vendor/xterm/xterm.css +285 -0
  158. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,508 @@
1
+ /**
2
+ * 55-backdrop:把「梁圣」皮肤(@leon___/dsh-client-liang-intensity-skin)的背景图渐变
3
+ * 复刻到古法编程覆盖层里。
4
+ *
5
+ * 皮肤的做法(见其 client.js):
6
+ * - 在中心列挂一个 `.liang-skin-backdrop`,背景 = `var(--liang-page)`;
7
+ * - 内部放一个 `<img class="liang-skin-sequence-frame">`,src 指向皮肤 assets 里的人像
8
+ * (按强度档位切换 stage-XX.webp),右侧 `min(42vw,720px)`、`object-fit:cover`,
9
+ * 并用 `mask-image: linear-gradient(90deg, transparent → black)` 做「左透明→右实」的渐变;
10
+ * - 透明度由 `--liang-portrait-opacity` 控制,可选 `data-mask="on"` 时再叠一层渐变遮罩。
11
+ *
12
+ * 古法编程是 `shell.overlay` 覆盖层,会盖住皮肤背景。本模块读取皮肤当前的背景节点/人像/透明度/
13
+ * 遮罩状态,在覆盖层里用同样的渐变规则重建一份背景,让 IDE 也能看到同一张背景图与渐变,
14
+ * 并通过标题栏的「背景」按钮随时开关(偏好持久化)。
15
+ *
16
+ * 皮肤关闭 / 找不到背景节点时不渲染(IDE 保持纯净实色)。
17
+ */
18
+
19
+ /** 背景开关偏好('0' = 关闭)。 */
20
+ const GUFA_BACKDROP_PREF_KEY = 'gufa-code:backdrop:v1'
21
+
22
+ G.backdropPref = {
23
+ get() {
24
+ try {
25
+ return localStorage.getItem(GUFA_BACKDROP_PREF_KEY) !== '0'
26
+ } catch {
27
+ return true
28
+ }
29
+ },
30
+ set(value) {
31
+ try {
32
+ localStorage.setItem(GUFA_BACKDROP_PREF_KEY, value === true ? '1' : '0')
33
+ } catch {
34
+ /* 隐私模式忽略 */
35
+ }
36
+ },
37
+ }
38
+
39
+ /** 背景浓度偏好:null = 跟随皮肤自身的透明度;数字 = 0–100 的独立无级浓度。 */
40
+ const GUFA_BACKDROP_STRENGTH_KEY = 'gufa-code:backdrop-strength:v1'
41
+
42
+ G.backdropStrengthPref = {
43
+ get() {
44
+ try {
45
+ const raw = localStorage.getItem(GUFA_BACKDROP_STRENGTH_KEY)
46
+ if (raw === null || raw === '') return null
47
+ const value = Number.parseFloat(raw)
48
+ return Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : null
49
+ } catch {
50
+ return null
51
+ }
52
+ },
53
+ set(value) {
54
+ try {
55
+ if (value === null || value === undefined) localStorage.removeItem(GUFA_BACKDROP_STRENGTH_KEY)
56
+ else localStorage.setItem(GUFA_BACKDROP_STRENGTH_KEY, String(Math.max(0, Math.min(100, Number(value)))))
57
+ } catch {
58
+ /* 隐私模式忽略 */
59
+ }
60
+ },
61
+ }
62
+
63
+ /** 滑动变祖皮肤的偏好存储(皮肤源码里的常量),用于开启「31 档细分」。 */
64
+ const SKIN_SETTINGS_KEY = 'dsh-liang-intensity-skin.settings.v1'
65
+ const SKIN_SETTINGS_MARKER = 'gufa-code:skin-fine-stops:v1'
66
+ const SKIN_DEFAULT_VALUES = Object.freeze({
67
+ bindEffort: true,
68
+ maskEnabled: true,
69
+ xianxiaPortraits: false,
70
+ particlesEnabled: false,
71
+ motionEnabled: false,
72
+ fineStopsEnabled: false,
73
+ hideSlider: false,
74
+ })
75
+
76
+ /**
77
+ * 开启滑动变祖皮肤的「31 档细分」(fineStopsEnabled)。
78
+ *
79
+ * 皮肤默认行为:绑定思考等级时,松手会把**外观滑块**吸附到模型真实推理档位
80
+ * (5 档就是 0/7.5/15/22.5/30),所以进度条和背景会「弹」到档位上。
81
+ * 打开 31 档细分后:外观可停在 0–30 的任意整数级(松手只吸附到最近整数级),
82
+ * 进度条与背景停在松手位置,模型仍提交最近的真实推理档位 —— 正是想要的行为。
83
+ *
84
+ * 只干预一次(写 marker,之后用户在皮肤设置里再关掉不会被反复打开);
85
+ * 写入后派发 StorageEvent,让皮肤的内存 store 立即生效,无需刷新。
86
+ * @returns {{applied:boolean, reason?:string}}
87
+ */
88
+ G.ensureSkinFineStops = function ensureSkinFineStops() {
89
+ try {
90
+ if (localStorage.getItem(SKIN_SETTINGS_MARKER) === '1') return { applied: false, reason: 'marked' }
91
+ let record = null
92
+ try {
93
+ const raw = localStorage.getItem(SKIN_SETTINGS_KEY)
94
+ record = raw === null ? null : JSON.parse(raw)
95
+ } catch {
96
+ record = null
97
+ }
98
+ const versioned = record !== null && typeof record === 'object' && record.version === 1 && record.values !== null && typeof record.values === 'object'
99
+ const legacyBind = localStorage.getItem('dsh-liang-intensity-skin.bind-effort') !== '0'
100
+ const legacyMask = localStorage.getItem('dsh-liang-intensity-skin.mask') !== '0'
101
+ const values = versioned
102
+ ? { ...SKIN_DEFAULT_VALUES, ...record.values }
103
+ : { ...SKIN_DEFAULT_VALUES, bindEffort: legacyBind, maskEnabled: legacyMask }
104
+ localStorage.setItem(SKIN_SETTINGS_MARKER, '1')
105
+ if (values.fineStopsEnabled === true) return { applied: false, reason: 'already-enabled' }
106
+ values.fineStopsEnabled = true
107
+ const payload = JSON.stringify({ version: 1, values })
108
+ const previous = localStorage.getItem(SKIN_SETTINGS_KEY)
109
+ localStorage.setItem(SKIN_SETTINGS_KEY, payload)
110
+ try {
111
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof StorageEvent === 'function') {
112
+ window.dispatchEvent(new StorageEvent('storage', {
113
+ key: SKIN_SETTINGS_KEY,
114
+ oldValue: previous,
115
+ newValue: payload,
116
+ storageArea: localStorage,
117
+ }))
118
+ }
119
+ } catch (error) {
120
+ console.error('[gufa-code] 通知皮肤设置失败(刷新后仍会生效)', error)
121
+ }
122
+ return { applied: true }
123
+ } catch (error) {
124
+ return { applied: false, reason: String(error) }
125
+ }
126
+ }
127
+
128
+ /** 读取当前「31 档细分」是否开启(诊断探针用)。 */
129
+ G.readSkinFineStops = function readSkinFineStops() {
130
+ try {
131
+ const raw = localStorage.getItem(SKIN_SETTINGS_KEY)
132
+ const record = raw === null ? null : JSON.parse(raw)
133
+ return record !== null && typeof record === 'object' && record.values !== null && typeof record.values === 'object' && record.values.fineStopsEnabled === true
134
+ } catch {
135
+ return false
136
+ }
137
+ }
138
+
139
+ /** 解析 transform 矩阵里的缩放(取 a 分量;非矩阵时返回 1)。 */
140
+ function gufaTransformScale(transform) {
141
+ if (typeof transform !== 'string' || transform === '' || transform === 'none') return 1
142
+ const matrix = /matrix\(([^)]+)\)/.exec(transform)
143
+ if (matrix === null) return 1
144
+ const parts = matrix[1].split(',').map((part) => Number.parseFloat(part))
145
+ return parts.length >= 4 && Number.isFinite(parts[0]) && parts[0] > 0 ? parts[0] : 1
146
+ }
147
+
148
+ /**
149
+ * 读取当前皮肤背景状态。纯 DOM 读取,不修改皮肤节点。
150
+ * @returns {{available:boolean, pageColor?:string, pageImage?:string, portraitSrc?:string, portraitOpacity?:number, portraitScale?:number, mask?:boolean, media?:string}}
151
+ */
152
+ G.readSkinBackdropMeta = function readSkinBackdropMeta() {
153
+ if (typeof document === 'undefined' || typeof window === 'undefined') return { available: false }
154
+ const body = document.body
155
+ if (body === null || body === undefined || typeof body.hasAttribute !== 'function') return { available: false }
156
+ if (!body.hasAttribute('data-liang-skin')) return { available: false }
157
+ if (typeof document.querySelector !== 'function') return { available: false }
158
+ const node = document.querySelector('.liang-skin-backdrop')
159
+ if (node === null || node === undefined) return { available: false }
160
+ let style
161
+ try {
162
+ style = window.getComputedStyle(node)
163
+ } catch {
164
+ return { available: false }
165
+ }
166
+ const pageColor = style.backgroundColor !== '' && style.backgroundColor !== 'rgba(0, 0, 0, 0)' ? style.backgroundColor : ''
167
+ const pageImage = style.backgroundImage !== 'none' && style.backgroundImage !== '' ? style.backgroundImage : ''
168
+ const img = typeof node.querySelector === 'function' ? node.querySelector('img') : null
169
+ let portraitSrc = ''
170
+ let portraitOpacity = 1
171
+ let portraitScale = 1
172
+ if (img !== null && img !== undefined) {
173
+ portraitSrc = img.currentSrc || img.src || ''
174
+ try {
175
+ const imgStyle = window.getComputedStyle(img)
176
+ if (imgStyle.display === 'none') portraitSrc = ''
177
+ const opacity = Number.parseFloat(imgStyle.opacity)
178
+ if (Number.isFinite(opacity)) portraitOpacity = Math.max(0, Math.min(1, opacity))
179
+ portraitScale = Math.max(0.1, Math.min(3, gufaTransformScale(imgStyle.transform)))
180
+ } catch {
181
+ /* 忽略样式读取失败 */
182
+ }
183
+ }
184
+ return {
185
+ available: true,
186
+ pageColor,
187
+ pageImage,
188
+ portraitSrc,
189
+ portraitOpacity,
190
+ portraitScale,
191
+ mask: typeof node.getAttribute === 'function' && node.getAttribute('data-mask') === 'on',
192
+ media: (typeof node.getAttribute === 'function' && node.getAttribute('data-media')) || 'color',
193
+ }
194
+ }
195
+
196
+ /**
197
+ * 订阅皮肤背景状态:皮肤强度变化会换人像、改透明度,这里每 1.5s 轮询一次并在变化时重渲染。
198
+ * @returns {ReturnType<typeof G.readSkinBackdropMeta>}
199
+ */
200
+ G.useSkinBackdrop = function useSkinBackdrop() {
201
+ const [meta, setMeta] = useState(() => G.readSkinBackdropMeta())
202
+ useEffect(() => {
203
+ let disposed = false
204
+ const sync = () => {
205
+ if (disposed) return
206
+ const next = G.readSkinBackdropMeta()
207
+ setMeta((previous) => (JSON.stringify(previous) === JSON.stringify(next) ? previous : next))
208
+ }
209
+ sync()
210
+ const timer = window.setInterval(sync, 350)
211
+ window.addEventListener('resize', sync)
212
+ return () => {
213
+ disposed = true
214
+ window.clearInterval(timer)
215
+ window.removeEventListener('resize', sync)
216
+ }
217
+ }, [])
218
+ return meta
219
+ }
220
+
221
+ /**
222
+ * 背景层:页面底色 + 右侧人像(带左→右渐变遮罩)+ 可选遮罩。
223
+ * 同时注册一个空 overlay 探针(`gufa-code-backdrop-live-<state>`),便于 Client Inspect
224
+ * 验证「背景层已挂载 / 人像已加载」,无需截图。
225
+ */
226
+ G.SkinBackdrop = function SkinBackdrop(props) {
227
+ const meta = props.meta
228
+ const [imgState, setImgState] = useState('pending')
229
+ const [layers, setLayers] = useState(() => ({
230
+ current: meta !== null && meta !== undefined && meta.available === true && typeof meta.portraitSrc === 'string' ? meta.portraitSrc : '',
231
+ previous: '',
232
+ }))
233
+ const enabled = props.enabled !== false
234
+ const active = meta !== null && meta !== undefined && meta.available === true && enabled === true
235
+ const portraitSrc = active && typeof meta.portraitSrc === 'string' ? meta.portraitSrc : ''
236
+
237
+ // 人像源变化(皮肤换档)时保留旧图层做交叉淡出,避免硬切
238
+ useEffect(() => {
239
+ setLayers((previous) => (previous.current === portraitSrc ? previous : { current: portraitSrc, previous: previous.current }))
240
+ }, [portraitSrc])
241
+ useEffect(() => {
242
+ if (layers.previous === '') return undefined
243
+ const timer = window.setTimeout(() => setLayers((previous) => (previous.previous === '' ? previous : { ...previous, previous: '' })), 520)
244
+ return () => window.clearTimeout(timer)
245
+ }, [layers.previous])
246
+ useEffect(() => {
247
+ if (!active) return undefined
248
+ if (G.ctx === null || G.ctx === undefined || typeof G.ctx.inject !== 'function') return undefined
249
+ const state = imgState === 'ok' ? 'img-ok' : imgState === 'fail' ? 'img-fail' : meta.portraitSrc ? 'img-loading' : 'color-only'
250
+ let dispose = () => {}
251
+ let disposeScope
252
+ try {
253
+ disposeScope = G.ctx.inject(['slots'], (scope) => {
254
+ try {
255
+ const removal = scope.slots.register({ name: 'shell.overlay', id: `gufa-code-backdrop-live-${state}`, order: 996 }, () => null)
256
+ dispose = typeof removal === 'function' ? removal : () => {}
257
+ } catch (error) {
258
+ console.error('[gufa-code] 注册背景探针失败', error)
259
+ }
260
+ })
261
+ } catch (error) {
262
+ console.error('[gufa-code] 注入背景探针失败', error)
263
+ return undefined
264
+ }
265
+ return () => {
266
+ try {
267
+ dispose()
268
+ } catch {
269
+ /* ignore */
270
+ }
271
+ try {
272
+ disposeScope?.()
273
+ } catch {
274
+ /* ignore */
275
+ }
276
+ }
277
+ }, [active, imgState, meta])
278
+ if (!active) return null
279
+ const pageStyle = {}
280
+ if (typeof meta.pageColor === 'string' && meta.pageColor !== '') pageStyle.backgroundColor = meta.pageColor
281
+ if (typeof meta.pageImage === 'string' && meta.pageImage !== '') pageStyle.backgroundImage = meta.pageImage
282
+ pageStyle.backgroundSize = 'cover'
283
+ pageStyle.backgroundPosition = 'center'
284
+ // 浓度:用户拖动过滑杆就用无级值,否则跟随皮肤自身透明度
285
+ const strength = typeof props.strength === 'number' && Number.isFinite(props.strength)
286
+ ? Math.max(0, Math.min(1, props.strength / 100))
287
+ : Math.max(0, Math.min(1, meta.portraitOpacity))
288
+ const portraitStyle = {
289
+ opacity: strength,
290
+ transform: `translateY(-50%) scale(${meta.portraitScale})`,
291
+ }
292
+ const leavingStyle = {
293
+ opacity: 0,
294
+ transform: `translateY(-50%) scale(${meta.portraitScale})`,
295
+ }
296
+ return h(
297
+ 'div',
298
+ { className: 'gufa-backdrop', 'aria-hidden': 'true' },
299
+ h('div', { className: 'gufa-backdrop-page', style: pageStyle }),
300
+ h('div', { className: 'gufa-backdrop-glow', 'aria-hidden': 'true' }),
301
+ layers.previous !== ''
302
+ ? h('img', { key: `prev:${layers.previous}`, className: 'gufa-backdrop-portrait', src: layers.previous, alt: '', draggable: false, style: leavingStyle, 'data-leaving': '' })
303
+ : null,
304
+ layers.current !== ''
305
+ ? h('img', {
306
+ key: `cur:${layers.current}`,
307
+ className: 'gufa-backdrop-portrait',
308
+ src: layers.current,
309
+ alt: '',
310
+ draggable: false,
311
+ style: portraitStyle,
312
+ onLoad: () => setImgState('ok'),
313
+ onError: () => setImgState('fail'),
314
+ })
315
+ : null,
316
+ h('div', { className: 'gufa-backdrop-fade' }),
317
+ meta.mask === true ? h('div', { className: 'gufa-backdrop-scrim' }) : null,
318
+ h(G.BackdropParticles, { strength }),
319
+ )
320
+ }
321
+
322
+ /**
323
+ * 背景粒子层:按「滑动变祖」原版粒子参数复刻(小颗粒、数量多、加色发光、从右下方涌向人像)。
324
+ * 原版用 WebGL 点精灵 + 加色混合;这里用 2D canvas + 预渲染光点 sprite + 'lighter' 混合近似,
325
+ * 性能足够(每帧 drawImage,30fps)。
326
+ */
327
+ G.BackdropParticles = function BackdropParticles(props) {
328
+ const canvasRef = useRef(null)
329
+ const strengthRaw = typeof props.strength === 'number' && Number.isFinite(props.strength) ? props.strength : 1
330
+ const strength = Math.max(0, Math.min(1, strengthRaw))
331
+ useEffect(() => {
332
+ const canvas = canvasRef.current
333
+ if (canvas === null || canvas === undefined) return undefined
334
+ const context = canvas.getContext('2d')
335
+ if (context === null || context === undefined) return undefined
336
+ let reduced = false
337
+ try {
338
+ reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
339
+ } catch {
340
+ reduced = false
341
+ }
342
+ if (reduced) return undefined
343
+
344
+ const dpr = Math.min(1.5, window.devicePixelRatio || 1)
345
+ let width = 0
346
+ let height = 0
347
+ const resize = () => {
348
+ width = Math.max(1, Math.floor(canvas.clientWidth * dpr))
349
+ height = Math.max(1, Math.floor(canvas.clientHeight * dpr))
350
+ canvas.width = width
351
+ canvas.height = height
352
+ sprite = makeSprite()
353
+ for (const particle of particles) spawn(particle, true)
354
+ }
355
+
356
+ /* 预渲染光点 sprite(暖金色核心 + 外圈微光) */
357
+ let sprite = null
358
+ const makeSprite = () => {
359
+ const size = 64
360
+ const off = document.createElement('canvas')
361
+ off.width = size
362
+ off.height = size
363
+ const ctx = off.getContext('2d')
364
+ if (ctx === null) return null
365
+ const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2)
366
+ gradient.addColorStop(0, 'rgba(255,241,214,1)')
367
+ gradient.addColorStop(0.25, 'rgba(240,196,120,0.85)')
368
+ gradient.addColorStop(0.55, 'rgba(210,150,80,0.35)')
369
+ gradient.addColorStop(1, 'rgba(150,90,50,0)')
370
+ ctx.fillStyle = gradient
371
+ ctx.fillRect(0, 0, size, size)
372
+ return off
373
+ }
374
+
375
+ const TAU = Math.PI * 2
376
+ /* 与皮肤原版一致的群体参数:数量随「背景浓度」放大,尺寸 2~5.2 CSS px,寿命 3~7s */
377
+ const activeCount = Math.round(300 + strength * 720)
378
+ const anchorX = 0.58
379
+ const anchorY = 0.92
380
+ const targetX = 0.5
381
+ const targetY = 0.4
382
+ const particles = Array.from({ length: activeCount }, () => ({ x: 0, y: 0, vx: 0, vy: 0, size: 0, life: 0, maxLife: 1, phase: 0 }))
383
+ const clamp01 = (value) => Math.max(0, Math.min(1, value))
384
+ const spawn = (particle, randomLife) => {
385
+ const spread = width * 0.05
386
+ particle.x = anchorX * width + (Math.random() - 0.5) * spread * 2
387
+ particle.y = anchorY * height + (Math.random() - 0.5) * spread * 1.4
388
+ particle.vx = (Math.random() - 0.5) * 90 * dpr
389
+ particle.vy = (-30 - Math.random() * 110) * dpr
390
+ particle.maxLife = 3 + Math.random() * 4
391
+ particle.life = randomLife ? Math.random() * particle.maxLife : 0
392
+ particle.size = (2 + Math.random() * 3.2) * dpr
393
+ particle.phase = Math.random() * TAU
394
+ }
395
+ for (const particle of particles) spawn(particle, true)
396
+
397
+ const pointer = { x: -1e4, y: -1e4, down: false }
398
+ let last = 0
399
+ let raf = 0
400
+ const tick = (now) => {
401
+ raf = window.requestAnimationFrame(tick)
402
+ if (now - last < 33) return
403
+ const dt = Math.min(0.048, (last === 0 ? 0.016 : (now - last) / 1000))
404
+ last = now
405
+ context.clearRect(0, 0, width, height)
406
+ context.globalCompositeOperation = 'lighter'
407
+ const levelFactor = 0.45 + 0.55 * strength
408
+ for (const particle of particles) {
409
+ particle.life += dt
410
+ if (particle.life > particle.maxLife) spawn(particle, false)
411
+ const curl = Math.sin(particle.y * 0.0016 + now * 0.00025 + particle.phase)
412
+ particle.vx += curl * width * 0.02 * dt + (Math.random() - 0.5) * width * 0.05 * dt
413
+ particle.vy -= height * 0.008 * dt
414
+ particle.vy += (Math.random() - 0.5) * height * 0.03 * dt
415
+ const dxp = pointer.x - particle.x
416
+ const dyp = pointer.y - particle.y
417
+ const distance = Math.hypot(dxp, dyp)
418
+ if (pointer.x > -1e3 && distance < width * 0.34 && distance > 0.001) {
419
+ const falloff = 1 - distance / (width * 0.34)
420
+ const sign = pointer.down === true ? -1 : 1
421
+ particle.vx -= (dxp / distance) * falloff * 900 * dt * sign
422
+ particle.vy -= (dyp / distance) * falloff * 900 * dt * sign
423
+ }
424
+ const toX = targetX * width - particle.x
425
+ const toY = targetY * height - particle.y
426
+ const toDistance = Math.hypot(toX, toY) || 1
427
+ const pull = 60 * dt * levelFactor * dpr
428
+ particle.vx += (toX / toDistance) * pull
429
+ particle.vy += (toY / toDistance) * pull
430
+ const damping = Math.max(0, 1 - 0.8 * dt)
431
+ particle.vx *= damping
432
+ particle.vy *= damping
433
+ particle.x += particle.vx * dt
434
+ particle.y += particle.vy * dt
435
+ if (particle.y < -height * 0.15 || particle.x < -width * 0.1 || particle.x > width * 1.1) spawn(particle, false)
436
+ const lifeRatio = particle.life / particle.maxLife
437
+ const fade = Math.min(1, lifeRatio / 0.18) * Math.pow(Math.sin(Math.PI * clamp01(lifeRatio)), 0.7)
438
+ const twinkle = 0.55 + 0.45 * Math.sin(particle.phase + now * 0.0016)
439
+ const alpha = Math.max(0, Math.min(1, (0.3 + 0.42 * levelFactor) * twinkle * fade))
440
+ if (alpha <= 0.01 || sprite === null) continue
441
+ const size = particle.size * (0.8 + 0.4 * levelFactor)
442
+ context.globalAlpha = alpha * 0.9
443
+ context.drawImage(sprite, particle.x - size * 1.6, particle.y - size * 1.6, size * 3.2, size * 3.2)
444
+ }
445
+ context.globalAlpha = 1
446
+ context.globalCompositeOperation = 'source-over'
447
+ }
448
+ const onPointerMove = (event) => {
449
+ const rect = canvas.getBoundingClientRect()
450
+ pointer.x = (event.clientX - rect.left) * dpr
451
+ pointer.y = (event.clientY - rect.top) * dpr
452
+ }
453
+ const onPointerLeave = () => {
454
+ pointer.x = -1e4
455
+ pointer.y = -1e4
456
+ }
457
+ const onPointerDown = () => { pointer.down = true }
458
+ const onPointerUp = () => { pointer.down = false }
459
+ window.addEventListener('pointermove', onPointerMove)
460
+ window.addEventListener('pointerdown', onPointerDown)
461
+ window.addEventListener('pointerup', onPointerUp)
462
+ window.addEventListener('pointerleave', onPointerLeave)
463
+ window.addEventListener('resize', resize)
464
+ resize()
465
+ raf = window.requestAnimationFrame(tick)
466
+ try {
467
+ void G.api('dev.log', { message: `particles-start count=${activeCount} size=2-5.2css dpr=${dpr} canvas=${width}x${height}` }).catch(() => {})
468
+ } catch {
469
+ /* ignore */
470
+ }
471
+ return () => {
472
+ if (raf !== 0) window.cancelAnimationFrame(raf)
473
+ window.removeEventListener('pointermove', onPointerMove)
474
+ window.removeEventListener('pointerdown', onPointerDown)
475
+ window.removeEventListener('pointerup', onPointerUp)
476
+ window.removeEventListener('pointerleave', onPointerLeave)
477
+ window.removeEventListener('resize', resize)
478
+ }
479
+ }, [strength])
480
+ return h('canvas', { ref: canvasRef, className: 'gufa-backdrop-particles', 'aria-hidden': 'true', style: { mixBlendMode: 'screen' } })
481
+ }
482
+
483
+ /** 背景浓度滑杆(无级、连续;双击恢复「跟随皮肤」)。 */
484
+ G.BackdropStrengthSlider = function BackdropStrengthSlider(props) {
485
+ const t = G.useT()
486
+ const raw = typeof props.value === 'number' && Number.isFinite(props.value) ? props.value : 100
487
+ const value = Math.max(0, Math.min(100, Math.round(raw)))
488
+ return h(
489
+ 'label',
490
+ { className: 'gufa-strength', title: `${t('app.backdropStrength')} — ${t('app.backdropStrengthHint')}` },
491
+ h(G.Icon, { name: 'image', size: 12 }),
492
+ h('input', {
493
+ className: 'gufa-strength-range',
494
+ type: 'range',
495
+ min: 0,
496
+ max: 100,
497
+ step: 1,
498
+ value,
499
+ 'aria-label': t('app.backdropStrength'),
500
+ onChange: (event) => props.onChange?.(Number(event.target.value)),
501
+ onDoubleClick: () => props.onReset?.(),
502
+ }),
503
+ h('span', { className: 'gufa-strength-value' }, `${value}%`),
504
+ )
505
+ }
506
+
507
+ /** 纯函数暴露(测试/控制台排查)。 */
508
+ G.__backdrop = { read: G.readSkinBackdropMeta }