dsh-wallpaper_share 26.8.22-T

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/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "dsh-wallpaper_share",
3
+ "description": "Wallpaper Engine ↔ DeepSeek Harness wallpaper sync (display-only): the page background follows the current Wallpaper Engine wallpaper, with a wallpaper_share conversation-view tab (monitor selector, power toggle, focus mode, render-mode toggle, transparency/blur/shadow sliders)",
4
+ "version": "26.8.22-T",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "dsh": {
13
+ "bundle": {
14
+ "patch": "./cordis.patch.yml"
15
+ },
16
+ "client": {
17
+ "inject": [
18
+ "@deepseek-ai/dsh-client-runtime",
19
+ "@deepseek-ai/dsh-client-ui-theme"
20
+ ],
21
+ "platform": "web"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsdown",
26
+ "bundle": "tsdown",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "license": "GPL-3.0",
30
+ "keywords": [
31
+ "dsh",
32
+ "deepseek-harness",
33
+ "wallpaper-engine",
34
+ "wallpaper",
35
+ "background",
36
+ "plugin"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/YRN-playmaker/dsh-wallpaper_share.git"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "@types/react": "~18.3.1",
45
+ "tsdown": "^0.22.0",
46
+ "typescript": "^5.6.0"
47
+ },
48
+ "files": [
49
+ "lib/index.js",
50
+ "lib/client.js",
51
+ "lib/client.js.map",
52
+ "cordis.patch.yml",
53
+ "README.md",
54
+ "README.zh-CN.md",
55
+ "LICENSE",
56
+ "tools/scene-renderer/"
57
+ ]
58
+ }
@@ -0,0 +1,68 @@
1
+ # scene-renderer(参考 renderer / 协议契约)
2
+
3
+ `scene-renderer.mjs` 是 `dsh-wallpaper_share` 的 **参考 renderer**:它实现
4
+ `SceneAdapter` 约定的进程协议,用于端到端验证「scene.pkg → renderer 子进程 →
5
+ offscreen 帧 → Node 中继 → WebSocket → 浏览器 canvas」这条管线能稳定跑 30fps。
6
+
7
+ > ⚠️ **它不是真正的 Wallpaper Engine Scene renderer**。它输出的是按场景配色
8
+ > 生成的 RGBA 诊断动画,不是 `scene.pkg` 的真实渲染画面。真实 scene 渲染需要你
9
+ > 提供 renderer(见下文「如何接入真 renderer」)。
10
+
11
+ ## 协议(与 `src/scene/SceneProtocol.ts` 一致)
12
+
13
+ **控制(stdin,换行分隔 JSON)**
14
+
15
+ ```json
16
+ {"cmd":"load","scene":"C:\\...\\scene.pkg","assets":"C:\\...\\wallpaper_engine\\assets","width":1920,"height":1080,"fps":30,"quality":80}
17
+ {"cmd":"pause"}
18
+ {"cmd":"resume"}
19
+ {"cmd":"resize","width":2560,"height":1440}
20
+ {"cmd":"ping"}
21
+ {"cmd":"stop"}
22
+ ```
23
+
24
+ **帧(stdout,二进制)**
25
+
26
+ ```
27
+ [4B LE uint32 payloadLen][payload]
28
+ payload = [1B format][4B LE width][4B LE height][编码/像素字节]
29
+ format: 0=JPEG 1=WebP 2=RGBA 3=BGRA
30
+ ```
31
+
32
+ 本参考 renderer 使用 `format=2`(RGBA)。真 renderer 建议输出 `format=0`(JPEG)
33
+ 或 `1`(WebP)以降低带宽。
34
+
35
+ **状态(stderr)**
36
+
37
+ - `[VERSION]reference-0.1.0` — 启动时自报版本
38
+ - `[STATUS]{"fps":29.8,"frame":893}` — 每秒心跳
39
+ - 其余行 = 人类日志
40
+
41
+ ## 运行方式
42
+
43
+ `dsh-wallpaper_share` 会用它**当前宿主 node 进程**直接执行本脚本
44
+ (`process.execPath` + 本文件),无需额外安装。也可手动测试:
45
+
46
+ ```bash
47
+ node tools/scene-renderer/scene-renderer.mjs
48
+ # 然后粘贴一行 load 命令并回车
49
+ ```
50
+
51
+ ## 如何接入真 renderer
52
+
53
+ 把 `CONFIG.sceneRendererPath` 指向一个实现了上述协议的独立可执行文件即可
54
+ (例如 WSL2 里封装的 linux-wallpaperengine 离屏渲染封装)。真 renderer 需要:
55
+
56
+ 1. 读取 `scene`(scene.pkg)与 `assets`(WE engine assets 目录);
57
+ 2. 离屏渲染(不弹窗)到 framebuffer;
58
+ 3. 把每帧按上面格式写到 **stdout**;
59
+ 4. 响应 `pause/resume/resize/stop/ping`;
60
+ 5. stderr 输出 `[VERSION]` 与(可选)`[STATUS]` 心跳。
61
+
62
+ 只要遵守协议,`SceneAdapter` / 帧传输 / 浏览器 canvas / fallback / diag 全部无需改动。
63
+
64
+ ## 许可证
65
+
66
+ 本参考 renderer 为 MIT,可随 `dsh-wallpaper_share` 一起发布。真 renderer 若是
67
+ GPL,请作为**独立组件**由用户自行提供(`CONFIG.sceneRendererPath`),不要复制其
68
+ 源码进本仓库,以保持主项目 MIT。
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scene-renderer.mjs —— dsh-wallpaper_share 的「参考 renderer」。
4
+ *
5
+ * ⚠ 重要:这不是真正的 Wallpaper Engine Scene renderer。它是一个
6
+ * 协议参考实现 / 管线验证桩,用来端到端验证:
7
+ * spawn → stdin JSON 控制 → 二进制帧 stdout → Node 中继 → WebSocket
8
+ * → 浏览器 canvas → 30fps 稳定显示。
9
+ * 它输出的是「按场景配色生成的诊断动画」(RGBA),不是 scene.pkg 的
10
+ * 真实渲染画面。真实 scene 渲染由用户通过 CONFIG.sceneRendererPath
11
+ * 提供的 renderer(如 GPL 的 linux-wallpaperengine 离屏封装)完成。
12
+ *
13
+ * 协议(与 src/scene/SceneProtocol.ts 一致):
14
+ * stdin 换行分隔 JSON 命令:load/pause/resume/resize/stop/ping
15
+ * stdout 帧:`[4B LE payloadLen][1B format][4B LE width][4B LE height][像素/编码字节]`
16
+ * format 2 = RGBA(本参考 renderer 使用)
17
+ * stderr 日志 + `[VERSION]reference-0.1.0` + `[STATUS]{"fps":..,"frame":..}`
18
+ */
19
+ import { readFileSync, existsSync } from 'node:fs'
20
+ import { dirname, join } from 'node:path'
21
+
22
+ const VERSION = 'reference-0.1.0'
23
+ const FORMAT_RGBA = 2
24
+ const STATUS_INTERVAL_MS = 1000
25
+
26
+ // ---- 状态 ----
27
+ let state = 'idle' // idle | running | paused
28
+ let scenePath = ''
29
+ let assetsPath = ''
30
+ let width = 640
31
+ let height = 360
32
+ let fps = 30
33
+ let frame = 0
34
+ let t0 = Date.now()
35
+ let framesThisSecond = 0
36
+ let fpsMeasured = 0
37
+ let colorA = [15, 16, 20]
38
+ let colorB = [46, 160, 67]
39
+ let gradient = null // 预计算背景渐变(rows)
40
+
41
+ // ---- 工具 ----
42
+ function log(line) {
43
+ process.stderr.write('[SceneRenderer] ' + line + '\n')
44
+ }
45
+ function status(obj) {
46
+ process.stderr.write('[STATUS]' + JSON.stringify(obj) + '\n')
47
+ }
48
+
49
+ function sendFrame(rgba) {
50
+ const header = Buffer.alloc(9)
51
+ header[0] = FORMAT_RGBA
52
+ header.writeUInt32LE(width, 1)
53
+ header.writeUInt32LE(height, 5)
54
+ const len = header.length + rgba.length
55
+ const out = Buffer.alloc(4 + len)
56
+ out.writeUInt32LE(len, 0)
57
+ header.copy(out, 4)
58
+ rgba.copy(out, 4 + header.length)
59
+ process.stdout.write(out)
60
+ }
61
+
62
+ // ---- 场景读取(只做元数据,不做真实渲染) ----
63
+ function parseSchemeColor(v) {
64
+ try {
65
+ const parts = String(v).trim().split(/\s+/).map(Number)
66
+ if (parts.length < 3 || parts.some((n) => !Number.isFinite(n))) return null
67
+ const max = Math.max(...parts)
68
+ const scale = max > 1.5 ? 1 : 255
69
+ return parts.slice(0, 3).map((n) => Math.max(0, Math.min(255, Math.round(n * scale))))
70
+ } catch {
71
+ return null
72
+ }
73
+ }
74
+
75
+ function readSceneMeta(pkg) {
76
+ const dir = dirname(pkg)
77
+ const meta = { title: '', scheme: null, textureW: 0, textureH: 0, entries: 0 }
78
+ try {
79
+ const project = JSON.parse(readFileSync(join(dir, 'project.json'), 'utf8'))
80
+ meta.title = typeof project.title === 'string' ? project.title : ''
81
+ if (project.general?.properties?.schemecolor?.value !== undefined) {
82
+ meta.scheme = parseSchemeColor(project.general.properties.schemecolor.value)
83
+ }
84
+ } catch { /* project.json 不可用 */ }
85
+ try {
86
+ // 粗略扫描 scene.pkg 内最大 JPEG 尺寸(只读尺寸,不解码)
87
+ const b = readFileSync(pkg)
88
+ let pos = 0
89
+ while (pos < b.length - 9) {
90
+ if (b[pos] === 0xff && b[pos + 1] === 0xd8 && b[pos + 2] === 0xff) {
91
+ let scan = pos + 2
92
+ for (let g = 0; scan < b.length - 9 && g < 64; g++) {
93
+ if (b[scan] !== 0xff) { scan++; continue }
94
+ const marker = b[scan + 1]
95
+ if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7)) { scan += 2; continue }
96
+ const len = b.readUInt16BE(scan + 2)
97
+ if (len < 2) break
98
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
99
+ const h = b.readUInt16BE(scan + 5)
100
+ const w = b.readUInt16BE(scan + 7)
101
+ if (w * h > meta.textureW * meta.textureH) { meta.textureW = w; meta.textureH = h }
102
+ break
103
+ }
104
+ scan += 2 + len
105
+ }
106
+ const eoi = b.indexOf(Buffer.from([0xff, 0xd9]), scan)
107
+ pos = eoi >= 0 ? eoi + 1 : b.length
108
+ continue
109
+ }
110
+ pos++
111
+ }
112
+ } catch { /* scene.pkg 不可读 */ }
113
+ return meta
114
+ }
115
+
116
+ // ---- 诊断动画渲染 ----
117
+ function rebuildGradient() {
118
+ gradient = Buffer.alloc(height * 4)
119
+ for (let y = 0; y < height; y++) {
120
+ const k = y / Math.max(1, height - 1)
121
+ const r = Math.round(colorA[0] + (colorB[0] - colorA[0]) * k)
122
+ const g = Math.round(colorA[1] + (colorB[1] - colorA[1]) * k)
123
+ const b = Math.round(colorA[2] + (colorB[2] - colorA[2]) * k)
124
+ gradient.writeUInt8(r, y * 4)
125
+ gradient.writeUInt8(g, y * 4 + 1)
126
+ gradient.writeUInt8(b, y * 4 + 2)
127
+ gradient.writeUInt8(255, y * 4 + 3)
128
+ }
129
+ }
130
+
131
+ function renderFrame() {
132
+ if (gradient === null) rebuildGradient()
133
+ const buf = Buffer.alloc(width * height * 4)
134
+ // 背景:逐行渐变
135
+ for (let y = 0; y < height; y++) {
136
+ const src = y * 4
137
+ const dstStart = y * width * 4
138
+ for (let x = 0; x < width; x++) {
139
+ gradient.copy(buf, dstStart + x * 4, src, src + 4)
140
+ }
141
+ }
142
+ const t = frame / Math.max(1, fps)
143
+ // 移动的高亮横条(正弦上下扫)
144
+ const barY = Math.round(height / 2 + Math.sin(t * 1.3) * (height / 3))
145
+ for (let x = 0; x < width; x++) {
146
+ for (let dy = -3; dy <= 3; dy++) {
147
+ const y = barY + dy
148
+ if (y < 0 || y >= height) continue
149
+ const i = (y * width + x) * 4
150
+ buf[i] = Math.min(255, buf[i] + 90)
151
+ buf[i + 1] = Math.min(255, buf[i + 1] + 70)
152
+ buf[i + 2] = Math.min(255, buf[i + 2] + 50)
153
+ }
154
+ }
155
+ // 轨道圆点
156
+ const cx = width / 2 + Math.cos(t * 0.9) * (Math.min(width, height) * 0.3)
157
+ const cy = height / 2 + Math.sin(t * 0.7) * (Math.min(width, height) * 0.25)
158
+ const radius = Math.max(4, Math.round(Math.min(width, height) * 0.04))
159
+ for (let y = Math.max(0, Math.floor(cy - radius)); y < Math.min(height, Math.ceil(cy + radius)); y++) {
160
+ for (let x = Math.max(0, Math.floor(cx - radius)); x < Math.min(width, Math.ceil(cx + radius)); x++) {
161
+ const dx = x - cx
162
+ const dy = y - cy
163
+ if (dx * dx + dy * dy <= radius * radius) {
164
+ const i = (y * width + x) * 4
165
+ buf[i] = 255
166
+ buf[i + 1] = 255
167
+ buf[i + 2] = 255
168
+ }
169
+ }
170
+ }
171
+ // 底部帧进度条
172
+ const progress = ((frame % (fps * 3)) / (fps * 3)) * width
173
+ for (let y = height - 6; y < height; y++) {
174
+ for (let x = 0; x < width; x++) {
175
+ const i = (y * width + x) * 4
176
+ if (x < progress) {
177
+ buf[i] = 255; buf[i + 1] = 255; buf[i + 2] = 255
178
+ } else {
179
+ buf[i] = 60; buf[i + 1] = 60; buf[i + 2] = 60
180
+ }
181
+ }
182
+ }
183
+ return buf
184
+ }
185
+
186
+ function tick() {
187
+ if (state !== 'running') return
188
+ const rgba = renderFrame()
189
+ sendFrame(rgba)
190
+ frame++
191
+ framesThisSecond++
192
+ }
193
+
194
+ // ---- 命令处理 ----
195
+ function handleLine(line) {
196
+ line = line.trim()
197
+ if (line === '') return
198
+ let cmd
199
+ try { cmd = JSON.parse(line) } catch { log('ignoring non-JSON stdin: ' + line); return }
200
+ switch (cmd.cmd) {
201
+ case 'load': {
202
+ scenePath = cmd.scene ?? ''
203
+ assetsPath = cmd.assets ?? ''
204
+ width = clampInt(cmd.width, 160, 1920, 640)
205
+ height = clampInt(cmd.height, 90, 1080, 360)
206
+ fps = clampInt(cmd.fps, 5, 60, 30)
207
+ const meta = readSceneMeta(scenePath)
208
+ if (meta.scheme !== null) { colorA = [Math.round(meta.scheme[0] * 0.25), Math.round(meta.scheme[1] * 0.25), Math.round(meta.scheme[2] * 0.25)]; colorB = meta.scheme }
209
+ gradient = null
210
+ frame = 0
211
+ log('Scene loaded: ' + (meta.title || scenePath) + ' (texture ' + meta.textureW + 'x' + meta.textureH + ', assets ' + (existsSync(join(assetsPath, 'shaders')) ? 'ok' : 'missing') + ')')
212
+ log('Streaming ' + width + 'x' + height + ' @' + fps + 'fps (RGBA diagnostic animation, NOT real scene render)')
213
+ state = 'running'
214
+ restartTick()
215
+ status({ fps: 0, frame: 0 })
216
+ break
217
+ }
218
+ case 'pause': state = 'paused'; log('paused'); break
219
+ case 'resume': state = 'running'; t0 = Date.now(); log('resumed'); break
220
+ case 'resize':
221
+ width = clampInt(cmd.width, 160, 3840, width)
222
+ height = clampInt(cmd.height, 90, 2160, height)
223
+ gradient = null
224
+ log('resized to ' + width + 'x' + height)
225
+ break
226
+ case 'ping': status({ fps: fpsMeasured, frame }); break
227
+ case 'stop': log('stopping'); state = 'idle'; setTimeout(() => process.exit(0), 20); break
228
+ default: log('unknown cmd: ' + (cmd.cmd ?? ''))
229
+ }
230
+ }
231
+
232
+ function clampInt(v, min, max, dflt) {
233
+ const n = Math.round(Number(v))
234
+ if (!Number.isFinite(n)) return dflt
235
+ return Math.max(min, Math.min(max, n))
236
+ }
237
+
238
+ // ---- 启动 ----
239
+ process.stderr.write('[VERSION]' + VERSION + '\n')
240
+ log('reference scene renderer started (pid ' + process.pid + ')')
241
+
242
+ let tickTimer = null
243
+ function restartTick() {
244
+ if (tickTimer !== null) clearInterval(tickTimer)
245
+ tickTimer = setInterval(tick, 1000 / fps)
246
+ }
247
+
248
+ // stdin 换行缓冲
249
+ let stdinBuf = ''
250
+ process.stdin.on('data', (chunk) => {
251
+ stdinBuf += chunk.toString('utf8')
252
+ let nl = stdinBuf.indexOf('\n')
253
+ while (nl >= 0) {
254
+ handleLine(stdinBuf.slice(0, nl))
255
+ stdinBuf = stdinBuf.slice(nl + 1)
256
+ nl = stdinBuf.indexOf('\n')
257
+ }
258
+ })
259
+ process.stdin.on('end', () => process.exit(0))
260
+ process.on('SIGTERM', () => process.exit(0))
261
+
262
+ // 帧循环(load 后按实际 fps 重建)+ 每秒状态上报
263
+ restartTick()
264
+ setInterval(() => {
265
+ fpsMeasured = framesThisSecond
266
+ framesThisSecond = 0
267
+ if (state === 'running') status({ fps: fpsMeasured, frame })
268
+ }, STATUS_INTERVAL_MS)