dsh-client-ui-skin-tianlight 2.0.3 → 2.0.4
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 +4 -0
- package/bin/cli.mjs +6 -0
- package/bin/installer.mjs +49 -24
- package/build-info.json +3 -3
- package/docs/THIRD-PARTY.md +161 -0
- package/lib/index.js +1 -1
- package/lib/media-types.js +44 -7
- package/lib/pet-chat.js +40 -0
- package/lib/scene-project.js +3 -1
- package/lib/we-wallpaper/host.mjs +32 -0
- package/lib/web-project.js +6 -2
- package/package.json +10 -1
- package/skin.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
>
|
|
7
7
|
> npm 包名 `dsh-client-ui-skin-tianlight`(v2.0.2 起;旧链接安装包名 `@dsh-external/dsh-client-ui-skin-tianlight`)· 布线行 `ui-skin-tianlight` · body 属性 `data-dsh-tianlight`
|
|
8
8
|
|
|
9
|
+
## v2.0.4 · 安全修复版(建议升级)
|
|
10
|
+
|
|
11
|
+
修复了 v2.0.3 验收中发现的问题,其中最重要的一条:Wallpaper Engine 网页壁纸的页面以前会以 DSH 源的身份运行,现在一律沙箱化。2.0.2 和 2.0.3 都受影响,请升级:先完全退出 DSH,再运行 `npx dsh-client-ui-skin-tianlight@latest`。另外 `npx` 不再顺带下载整套 DSH 依赖,安装快很多。详见 [RELEASE-NOTES-v2.0.4.md](RELEASE-NOTES-v2.0.4.md)。
|
|
12
|
+
|
|
9
13
|
## v2.0.3 · 一条命令安装
|
|
10
14
|
|
|
11
15
|
```bash
|
package/bin/cli.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
pruneBackups,
|
|
29
29
|
readJson,
|
|
30
30
|
removeInstalledFiles,
|
|
31
|
+
isUncPath,
|
|
31
32
|
resolveDshHome,
|
|
32
33
|
scanPatches,
|
|
33
34
|
writeJsonAtomic,
|
|
@@ -59,6 +60,7 @@ const M = {
|
|
|
59
60
|
home: (p) => `DSH 数据目录:${p}`,
|
|
60
61
|
noHome: (p) => `找不到 DSH 数据目录 ${p}\n 请先安装并启动一次 DeepSeek Harness;数据目录不在默认位置时用 --dsh-home <目录> 指定。`,
|
|
61
62
|
noProfiles: '这个 DSH 数据目录里还没有 profile。请先启动一次 DSH 再运行本命令。',
|
|
63
|
+
unc: '不支持网络路径(\\\\服务器\\共享)作为 DSH 数据目录,请使用本机磁盘上的目录。',
|
|
62
64
|
profileMissing: (p, list) => `没有名为 "${p}" 的 profile。现有:${list.join('、') || '(无)'}`,
|
|
63
65
|
profileAmbiguous: (list) => `有多个 profile:${list.join('、')}\n 请用 --profile <名称> 指定要装到哪一个。`,
|
|
64
66
|
profile: (p) => `目标 profile:${p}`,
|
|
@@ -120,6 +122,7 @@ const M = {
|
|
|
120
122
|
home: (p) => `DSH home: ${p}`,
|
|
121
123
|
noHome: (p) => `DSH home not found: ${p}\n Install and start DeepSeek Harness once first, or pass --dsh-home <dir>.`,
|
|
122
124
|
noProfiles: 'This DSH home has no profile yet. Start DSH once, then run this command again.',
|
|
125
|
+
unc: 'Network paths (\\\\server\\share) are not supported as the DSH home; use a local directory.',
|
|
123
126
|
profileMissing: (p, list) => `No profile named "${p}". Existing: ${list.join(', ') || '(none)'}`,
|
|
124
127
|
profileAmbiguous: (list) => `Several profiles exist: ${list.join(', ')}\n Pick one with --profile <name>.`,
|
|
125
128
|
profile: (p) => `Target profile: ${p}`,
|
|
@@ -186,7 +189,10 @@ const die = (line, code = 1) => {
|
|
|
186
189
|
|
|
187
190
|
// ---- shared steps -----------------------------------------------------------
|
|
188
191
|
function locate() {
|
|
192
|
+
const requestedHome = value('--dsh-home') ?? process.env.DSH_HOME ?? ''
|
|
193
|
+
if (isUncPath(requestedHome)) die(M.unc)
|
|
189
194
|
const dshHome = resolveDshHome({ dshHome: value('--dsh-home') })
|
|
195
|
+
if (isUncPath(dshHome)) die(M.unc)
|
|
190
196
|
say(M.home(dshHome))
|
|
191
197
|
if (existsSync(dshHome) === false) die(M.noHome(dshHome))
|
|
192
198
|
const profiles = listProfiles(dshHome)
|
package/bin/installer.mjs
CHANGED
|
@@ -59,6 +59,11 @@ export function writeJsonAtomic(file, value) {
|
|
|
59
59
|
|
|
60
60
|
// ---- locating DSH -----------------------------------------------------------
|
|
61
61
|
|
|
62
|
+
/** `\\server\share` / `//server/share` (and `\\?\UNC\...`): network locations are refused. */
|
|
63
|
+
export function isUncPath(value) {
|
|
64
|
+
return typeof value === 'string' && /^(?:\\\\|\/\/)/.test(value.trim())
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
export function resolveDshHome({ dshHome = null, env = process.env, home = homedir() } = {}) {
|
|
63
68
|
if (typeof dshHome === 'string' && dshHome.trim() !== '') return resolve(dshHome.trim())
|
|
64
69
|
if (typeof env.DSH_HOME === 'string' && env.DSH_HOME.trim() !== '') return resolve(env.DSH_HOME.trim())
|
|
@@ -401,37 +406,57 @@ export function pruneBackups(profileDir, keep = KEEP_BACKUPS) {
|
|
|
401
406
|
* backup folder (a link is just unlinked), then swap the staged copy in. If the swap
|
|
402
407
|
* fails the previous copy is put back.
|
|
403
408
|
*/
|
|
409
|
+
export const STAGING_PREFIX = `.${PKG_NAME}.tl-staging-`
|
|
410
|
+
|
|
411
|
+
/** v2.0.4 (L1): staging copies left behind by an interrupted/failed earlier run. */
|
|
412
|
+
export function cleanStaleStaging(modulesDir) {
|
|
413
|
+
const removed = []
|
|
414
|
+
if (existsSync(modulesDir) === false) return removed
|
|
415
|
+
for (const name of readdirSync(modulesDir)) {
|
|
416
|
+
if (name.startsWith(STAGING_PREFIX) === false) continue
|
|
417
|
+
safeRemoveTree(join(modulesDir, name))
|
|
418
|
+
removed.push(name)
|
|
419
|
+
}
|
|
420
|
+
return removed
|
|
421
|
+
}
|
|
422
|
+
|
|
404
423
|
export function installFiles({ srcRoot = SELF_ROOT, profileDir, backupDir, entries = packageEntries(srcRoot) }) {
|
|
405
424
|
const modules = join(profileDir, 'node_modules')
|
|
406
425
|
mkdirSync(modules, { recursive: true })
|
|
426
|
+
cleanStaleStaging(modules)
|
|
407
427
|
const target = join(modules, PKG_NAME)
|
|
408
|
-
const staging = join(modules,
|
|
409
|
-
rmSync(staging, { recursive: true, force: true })
|
|
410
|
-
mkdirSync(staging, { recursive: true })
|
|
411
|
-
for (const entry of entries) cpSync(join(srcRoot, entry), join(staging, entry), { recursive: true })
|
|
412
|
-
const stagedManifestPath = join(staging, 'package.json')
|
|
413
|
-
const stagedManifest = readJson(stagedManifestPath)
|
|
414
|
-
if (stagedManifest.private !== undefined) {
|
|
415
|
-
delete stagedManifest.private
|
|
416
|
-
writeFileSync(stagedManifestPath, JSON.stringify(stagedManifest, null, 2) + '\n', 'utf8')
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const before = inspectInstalled(profileDir)
|
|
428
|
+
const staging = join(modules, `${STAGING_PREFIX}${process.pid}`)
|
|
420
429
|
let parked = null
|
|
421
|
-
|
|
422
|
-
removeLink(target)
|
|
423
|
-
} else if (before.kind === 'dir') {
|
|
424
|
-
parked = join(backupDir, 'previous-package')
|
|
425
|
-
renameSync(target, parked)
|
|
426
|
-
}
|
|
430
|
+
let swapped = false
|
|
427
431
|
try {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
432
|
+
mkdirSync(staging, { recursive: true })
|
|
433
|
+
for (const entry of entries) cpSync(join(srcRoot, entry), join(staging, entry), { recursive: true })
|
|
434
|
+
const stagedManifestPath = join(staging, 'package.json')
|
|
435
|
+
const stagedManifest = readJson(stagedManifestPath)
|
|
436
|
+
if (stagedManifest.private !== undefined) {
|
|
437
|
+
delete stagedManifest.private
|
|
438
|
+
writeFileSync(stagedManifestPath, JSON.stringify(stagedManifest, null, 2) + '\n', 'utf8')
|
|
439
|
+
}
|
|
440
|
+
const before = inspectInstalled(profileDir)
|
|
441
|
+
if (before.kind === 'link') {
|
|
442
|
+
removeLink(target)
|
|
443
|
+
} else if (before.kind === 'dir') {
|
|
444
|
+
parked = join(backupDir, 'previous-package')
|
|
445
|
+
renameSync(target, parked)
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
renameSync(staging, target)
|
|
449
|
+
swapped = true
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (parked !== null && existsSync(target) === false) renameSync(parked, target)
|
|
452
|
+
throw error
|
|
453
|
+
}
|
|
454
|
+
return { previous: before, parked }
|
|
455
|
+
} finally {
|
|
456
|
+
// v2.0.4 (L1): whatever failed — copy, parking (EPERM on a locked file), swap —
|
|
457
|
+
// never leave the ~37 MB staging copy behind.
|
|
458
|
+
if (swapped === false) safeRemoveTree(staging)
|
|
433
459
|
}
|
|
434
|
-
return { previous: before, parked }
|
|
435
460
|
}
|
|
436
461
|
|
|
437
462
|
export function removeInstalledFiles({ profileDir, backupDir }) {
|
package/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"product": "dsh-client-ui-skin-tianlight",
|
|
4
|
-
"version": "2.0.
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-11-
|
|
4
|
+
"version": "2.0.4",
|
|
5
|
+
"commit": "37c4db1b350a47b6794262bbe246a259710177db",
|
|
6
|
+
"builtAt": "2026-11-13T17:10:57+08:00",
|
|
7
7
|
"source": "pack"
|
|
8
8
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# 第三方组件与许可
|
|
2
|
+
|
|
3
|
+
天光皮肤本体是 MIT(见根目录 `LICENSE`)。本文件列出随仓库分发(或作为实现依据)的第三方组件、
|
|
4
|
+
版本、许可与归属;发布包(`node tools/pack.mjs`)会带上 `lib/we-wallpaper/`、`vendor/we-wallpaper/`、
|
|
5
|
+
`vendor/webwallgl/` 及各自的许可文件。
|
|
6
|
+
|
|
7
|
+
## 1. dsh-we-wallpaper 宿主半(MIT)
|
|
8
|
+
|
|
9
|
+
v0.6.0 的 Wallpaper Engine 库集成把上游 **dsh-we-wallpaper** 的宿主半(MIT)**以源码形式内嵌**,
|
|
10
|
+
使皮肤能在**不填写任何目录**的前提下浏览本机 Wallpaper Engine 库。
|
|
11
|
+
|
|
12
|
+
| 项 | 值 |
|
|
13
|
+
|---|---|
|
|
14
|
+
| 上游 | <https://github.com/pbadgpmeb22791-sketch/dsh-we-wallpaper> |
|
|
15
|
+
| 版本 / 提交 | `v0.4.0` @ `61ccaa2`(把 v0.4.0 提升到仓库根的 restructure commit) |
|
|
16
|
+
| 归属 | Copyright (c) 2026 **dsh-we-wallpaper contributors** |
|
|
17
|
+
| 内嵌文件 | `lib/we-wallpaper/host.mjs`(单 ESM,仅 Node 内置模块;上游 `lib/index.js`,由上游 TS 源码构建) |
|
|
18
|
+
| 许可全文 | [`lib/we-wallpaper/LICENSE`](../lib/we-wallpaper/LICENSE) |
|
|
19
|
+
| 修改说明 | [`lib/we-wallpaper/README.md`](../lib/we-wallpaper/README.md)(grep `TIANLIGHT` 可定位全部改动) |
|
|
20
|
+
|
|
21
|
+
保留:无框架扫描器、无依赖 `.pkg`/TEX 解码器、HTTP 路由族。
|
|
22
|
+
未内嵌:上游 cordis/settings 插件接线与上游客户端半(由本皮肤自己的客户端渲染列表并播放壁纸)。
|
|
23
|
+
|
|
24
|
+
对本仓库的修改(摘要,详细见上表 README):去掉插件接线改为纯函数导出;同源校验放宽到 DSH Desktop
|
|
25
|
+
webview 与 `127.0.0.1/localhost`(跨站仍 403);运行状态/缓存从 `~/.dsh` 移到插件 gitignored `state/`
|
|
26
|
+
(`TIANLIGHT_WE_HOME` 可覆盖);wcap 路径指向 `vendor/we-wallpaper/wcap/`;本地 project 的 `.pkg` 也支持解出;
|
|
27
|
+
场景动态缓存 503 语义明确;扫描器结果映射为本皮肤的 `_wallpapers`/`_engines` 形状。
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
MIT License
|
|
31
|
+
|
|
32
|
+
Copyright (c) 2026 dsh-we-wallpaper contributors
|
|
33
|
+
|
|
34
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
35
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
36
|
+
in the Software without restriction, including without limitation the rights
|
|
37
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
38
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
39
|
+
furnished to do so, subject to the following conditions:
|
|
40
|
+
|
|
41
|
+
The above copyright notice and this permission notice shall be included in all
|
|
42
|
+
copies or substantial portions of the Software.
|
|
43
|
+
|
|
44
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
45
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
46
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
47
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
48
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
49
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
50
|
+
SOFTWARE.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 2. wcap(Unlicense / 公有领域)
|
|
54
|
+
|
|
55
|
+
可选的「场景动态缓存」(Windows-only)使用上游 dsh-we-wallpaper 随附的窗口捕获工具 `wcap`:
|
|
56
|
+
|
|
57
|
+
| 文件 | 来源 | 许可 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `vendor/we-wallpaper/wcap/wcap-x64.exe` | 上游 `tools/wcap/wcap-x64.exe` | Unlicense(public domain) |
|
|
60
|
+
| `vendor/we-wallpaper/wcap/scene-capture-desktop.ps1` | 上游 `tools/wcap/` | Unlicense |
|
|
61
|
+
| `vendor/we-wallpaper/wcap/send-window-capture-hotkey.ps1` | 上游 `tools/wcap/` | Unlicense |
|
|
62
|
+
| 许可全文 | 上游 `tools/wcap/LICENSE`,逐字内嵌 | [`vendor/we-wallpaper/wcap/LICENSE`](../vendor/we-wallpaper/wcap/LICENSE) |
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
This is free and unencumbered software released into the public domain.
|
|
66
|
+
|
|
67
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or distribute
|
|
68
|
+
this software, either in source code form or as a compiled binary, for any
|
|
69
|
+
purpose, commercial or non-commercial, and by any means.
|
|
70
|
+
|
|
71
|
+
In jurisdictions that recognize copyright laws, the author or authors of
|
|
72
|
+
this software dedicate any and all copyright interest in the software to the
|
|
73
|
+
public domain. We make this dedication for the benefit of the public at large
|
|
74
|
+
and to the detriment of our heirs and successors. We intend this dedication
|
|
75
|
+
to be an overt act of relinquishment in perpetuity of all present and future
|
|
76
|
+
rights to this software under copyright law.
|
|
77
|
+
|
|
78
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
79
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
80
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
81
|
+
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
82
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
83
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
84
|
+
|
|
85
|
+
For more information, please refer to <https://unlicense.org>.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> `wcap` 只在用户显式请求「生成动态」时被 `POST /api/we-wallpaper/scene-video/generate/<id>` 调用;
|
|
89
|
+
> 仓库测试套件用 `TIANLIGHT_WE_DISABLE_SCENE_VIDEO=1` 跳过它(不会在维护者桌面弹窗)。
|
|
90
|
+
|
|
91
|
+
## 3. RePKG(MIT,DXT 解码的实现依据)
|
|
92
|
+
|
|
93
|
+
`lib/we-wallpaper/host.mjs` 的 `pkg-tex` 解包/解码器在格式实现上参考并移植了 **RePKG** 的 DXT 解码
|
|
94
|
+
(LibSquish 派生的 C# 实现);上游 dsh-we-wallpaper 同样以此为实现依据。
|
|
95
|
+
|
|
96
|
+
| 项 | 值 |
|
|
97
|
+
|---|---|
|
|
98
|
+
| 项目 | <https://github.com/notscuffed/repkg> |
|
|
99
|
+
| 归属 | Copyright (c) **Xalcon @ mmowned.com**(见上游文件头) |
|
|
100
|
+
| 许可 | MIT |
|
|
101
|
+
| 使用方式 | 格式/算法参考与 DXT 解码移植;未整份复制其 C# 实现 |
|
|
102
|
+
|
|
103
|
+
RePKG 的 MIT 许可全文(v2.0.4 起逐字附在这里,随 npm 包分发):
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
MIT License
|
|
107
|
+
|
|
108
|
+
Copyright (c) Xalcon @ mmowned.com
|
|
109
|
+
|
|
110
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
111
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
112
|
+
in the Software without restriction, including without limitation the rights
|
|
113
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
114
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
115
|
+
furnished to do so, subject to the following conditions:
|
|
116
|
+
|
|
117
|
+
The above copyright notice and this permission notice shall be included in all
|
|
118
|
+
copies or substantial portions of the Software.
|
|
119
|
+
|
|
120
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
121
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
122
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
123
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
124
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
125
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
126
|
+
SOFTWARE.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 4. WebWallGL(MIT)
|
|
130
|
+
|
|
131
|
+
Wallpaper Engine 场景通道使用仓库内 `vendor/webwallgl/` 的 WebWallGL 3.0(可选依赖,不内联进
|
|
132
|
+
`lib/client.js`):
|
|
133
|
+
|
|
134
|
+
| 项 | 值 |
|
|
135
|
+
|---|---|
|
|
136
|
+
| 位置 | `vendor/webwallgl/webwallgl.global.min.js`(+ `README.upstream.md`) |
|
|
137
|
+
| 归属 | Copyright (c) 2026 **oneincase** `<462534624@qq.com>` |
|
|
138
|
+
| 许可 | MIT;全文见 [`vendor/webwallgl/LICENSE`](../vendor/webwallgl/LICENSE) |
|
|
139
|
+
|
|
140
|
+
## 4.1 二进制与压缩代码(v2.0.4 起公布校验值)
|
|
141
|
+
|
|
142
|
+
下面两个文件不是从本仓库源码构建的,包内无法审计其源码,只能按来源与校验值核对。`wcap-x64.exe` 未做代码签名,且只在用户显式点击「生成动态」时才会运行。
|
|
143
|
+
|
|
144
|
+
| 文件 | SHA-256 | 来源 |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `vendor/we-wallpaper/wcap/wcap-x64.exe` | `4bd124febf537f0f569bd5cb7486ddbb9e664b509f39e421e6949767829b6be6` | 上游 dsh-we-wallpaper `tools/wcap/wcap-x64.exe`,原样复制 |
|
|
147
|
+
| `vendor/webwallgl/webwallgl.global.min.js` | `2f86ab955f3377e2fa2cfd5ffa72f5813fe8f6e4a272295fed36f4c1e626b1e5` | WebWallGL 3.0 发布的压缩构建,原样复制(见 `vendor/webwallgl/README.upstream.md`) |
|
|
148
|
+
|
|
149
|
+
`tools/test.mjs` 会断言这两个校验值,文件被替换时测试会变红。
|
|
150
|
+
|
|
151
|
+
## 5. 默认演示素材
|
|
152
|
+
|
|
153
|
+
仓库完整版附带的 `assets/bg-4k.mp4` / `assets/cat.png` 是演示素材,
|
|
154
|
+
**不在 MIT 代码许可范围内**:桌宠形象与视频版权归各自权利方,替换或再分发前请确认你有使用权;
|
|
155
|
+
发布包默认**包含**这两个文件(`node tools/pack.mjs` 的随包演示素材白名单,Release 附件同源;
|
|
156
|
+
需要纯代码包时用 `node tools/pack.mjs --code-only`,此时只带代码、清单、`lib/`、`vendor/`、
|
|
157
|
+
`presets/` 与 `assets/README.md`)。
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
*本文件随 v0.6.0 新增;若后续再内嵌第三方代码,请在同一 PR 里补行并附许可全文。*
|
package/lib/index.js
CHANGED
|
@@ -73,7 +73,7 @@ const HOST_API = '2'
|
|
|
73
73
|
* manifest is stamped with it. The handshake no longer compares it, so a
|
|
74
74
|
* browser-half-only update can run against an unchanged host process.
|
|
75
75
|
*/
|
|
76
|
-
const BUILD = '2.0.
|
|
76
|
+
const BUILD = '2.0.4'
|
|
77
77
|
|
|
78
78
|
function readRoots() {
|
|
79
79
|
try {
|
package/lib/media-types.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// list is exactly how "the upload accepted a format the player cannot decode"
|
|
6
6
|
// bugs happen. One table, imported by every route.
|
|
7
7
|
|
|
8
|
-
import { statSync } from 'node:fs'
|
|
8
|
+
import { realpathSync, statSync } from 'node:fs'
|
|
9
9
|
import { dirname, resolve, sep } from 'node:path'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
@@ -198,7 +198,17 @@ export function isSafeSvgBuffer(buffer) {
|
|
|
198
198
|
if (SVG_NAMED_ENTITIES.has(entity[1]) === false) return { ok: false, reason: 'named entity' }
|
|
199
199
|
entity = entityPattern.exec(text)
|
|
200
200
|
}
|
|
201
|
-
|
|
201
|
+
// v2.0.4 (M1): a raw `>` inside a quoted attribute value (`alt=">"`) used to end
|
|
202
|
+
// the tag early so the attributes after it were never inspected. Two layers now:
|
|
203
|
+
// a document-wide raw scan that does not depend on where a tag ends, and a tag
|
|
204
|
+
// scanner whose attribute part consumes whole quoted strings.
|
|
205
|
+
if (/[\s/"']on[a-z]+\s*=/i.test(text)) return { ok: false, reason: 'inline event handler' }
|
|
206
|
+
for (const denied of SVG_DENY_ELEMENTS) {
|
|
207
|
+
if (new RegExp('<\\s*(?:[A-Za-z_][\\w.-]*:)?' + denied + '(?![\\w.:-])', 'i').test(text)) {
|
|
208
|
+
return { ok: false, reason: 'dangerous element: ' + denied }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const tagPattern = /<\s*\/?\s*([A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)((?:[^>"']|"[^"]*"|'[^']*')*?)>/g
|
|
202
212
|
let firstLocalName = ''
|
|
203
213
|
let match = tagPattern.exec(text)
|
|
204
214
|
while (match !== null) {
|
|
@@ -250,16 +260,43 @@ export function isSafeSvgBuffer(buffer) {
|
|
|
250
260
|
}
|
|
251
261
|
|
|
252
262
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
263
|
+
* v2.0.4 (M2): true when the REAL path of `target` (symlinks and Windows junctions
|
|
264
|
+
* resolved) is `root` itself or inside the real path of `root`. Lexical checks
|
|
265
|
+
* (`resolve()` + startsWith) cannot see links; this one can. A path that cannot be
|
|
266
|
+
* resolved (missing, permission) is reported as outside — callers fail closed.
|
|
267
|
+
*/
|
|
268
|
+
export function realPathInside(root, target) {
|
|
269
|
+
try {
|
|
270
|
+
const realRoot = realpathSync.native(root)
|
|
271
|
+
const realTarget = realpathSync.native(target)
|
|
272
|
+
const fold = (value) => (process.platform === 'win32' ? value.toLowerCase() : value)
|
|
273
|
+
const prefix = realRoot.endsWith(sep) ? realRoot : realRoot + sep
|
|
274
|
+
return fold(realTarget) === fold(realRoot) || fold(realTarget).startsWith(fold(prefix))
|
|
275
|
+
} catch {
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Resolves `name` inside `root` and proves the result stays inside it: `..` and
|
|
282
|
+
* separators are refused by SAFE_NAME plus a lexical prefix test, and an existing
|
|
283
|
+
* entry must also resolve (through any symlink/junction) to a real path inside the
|
|
284
|
+
* real root (v2.0.4; before that the comment claimed this but the code did not).
|
|
257
285
|
*/
|
|
258
286
|
export function safeJoin(root, name) {
|
|
259
287
|
if (typeof name !== 'string' || SAFE_NAME.test(name) === false) return null
|
|
260
288
|
const joined = resolve(root, name)
|
|
261
289
|
const prefix = root.endsWith(sep) ? root : root + sep
|
|
262
|
-
|
|
290
|
+
if (joined.startsWith(prefix) === false) return null
|
|
291
|
+
let exists = false
|
|
292
|
+
try {
|
|
293
|
+
statSync(joined)
|
|
294
|
+
exists = true
|
|
295
|
+
} catch {
|
|
296
|
+
exists = false
|
|
297
|
+
}
|
|
298
|
+
if (exists && realPathInside(root, joined) === false) return null
|
|
299
|
+
return joined
|
|
263
300
|
}
|
|
264
301
|
|
|
265
302
|
/** Card metadata: size is cheap (one stat) and lets the library show real weight. */
|
package/lib/pet-chat.js
CHANGED
|
@@ -31,6 +31,15 @@ export const PET_CHAT_MAX_CHARS = 4000
|
|
|
31
31
|
export const PET_CHAT_MAX_TOKENS = 600
|
|
32
32
|
/** Whole-call deadline; the browser adapter also races at 20 s. */
|
|
33
33
|
export const PET_CHAT_TIMEOUT_MS = 20000
|
|
34
|
+
/**
|
|
35
|
+
* v2.0.4 (M3): host-side call budget. The browser keeps its own (stricter, user
|
|
36
|
+
* configurable) quota, but any same-origin script could call this route directly,
|
|
37
|
+
* so the host enforces a ceiling of its own. Set just above the most the UI can ever
|
|
38
|
+
* spend (user daily limit max 120 + 12 autonomous thoughts) so normal use never hits
|
|
39
|
+
* it. Every accepted call counts, including failures and timeouts.
|
|
40
|
+
*/
|
|
41
|
+
export const PET_CHAT_HOST_PER_MINUTE = 8
|
|
42
|
+
export const PET_CHAT_HOST_PER_DAY = 140
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
45
|
* Fixed system prompt. It must not be assembled from Host or page data: the page
|
|
@@ -203,7 +212,33 @@ export function createPetChat(options = {}) {
|
|
|
203
212
|
? Math.floor(options.maxBodyBytes)
|
|
204
213
|
: PET_CHAT_MAX_BODY_BYTES
|
|
205
214
|
const maxChars = Number.isFinite(options.maxChars) && options.maxChars > 0 ? Math.floor(options.maxChars) : PET_CHAT_MAX_CHARS
|
|
215
|
+
const perMinute =
|
|
216
|
+
Number.isFinite(options.perMinute) && options.perMinute > 0 ? Math.floor(options.perMinute) : PET_CHAT_HOST_PER_MINUTE
|
|
217
|
+
const perDay = Number.isFinite(options.perDay) && options.perDay > 0 ? Math.floor(options.perDay) : PET_CHAT_HOST_PER_DAY
|
|
218
|
+
const now = typeof options.now === 'function' ? options.now : () => Date.now()
|
|
206
219
|
let busy = false
|
|
220
|
+
/** Accepted-call timestamps of the last minute + the per-local-day counter. */
|
|
221
|
+
let recent = []
|
|
222
|
+
let dayKey = ''
|
|
223
|
+
let dayCount = 0
|
|
224
|
+
const localDayKey = (ms) => {
|
|
225
|
+
const d = new Date(ms)
|
|
226
|
+
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
|
|
227
|
+
}
|
|
228
|
+
/** Reserve one call from the host budget; false when the minute or day is spent. */
|
|
229
|
+
const takeBudget = () => {
|
|
230
|
+
const t = now()
|
|
231
|
+
recent = recent.filter((stamp) => t - stamp < 60000)
|
|
232
|
+
const key = localDayKey(t)
|
|
233
|
+
if (key !== dayKey) {
|
|
234
|
+
dayKey = key
|
|
235
|
+
dayCount = 0
|
|
236
|
+
}
|
|
237
|
+
if (recent.length >= perMinute || dayCount >= perDay) return false
|
|
238
|
+
recent.push(t)
|
|
239
|
+
dayCount += 1
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
207
242
|
|
|
208
243
|
/**
|
|
209
244
|
* Handle one request. Never throws, never rejects; the response body is always
|
|
@@ -287,6 +322,11 @@ export function createPetChat(options = {}) {
|
|
|
287
322
|
respond(429, { ok: false, code: PET_CHAT_CODES.BUSY })
|
|
288
323
|
return
|
|
289
324
|
}
|
|
325
|
+
if (takeBudget() === false) {
|
|
326
|
+
req?.resume?.()
|
|
327
|
+
respond(429, { ok: false, code: PET_CHAT_CODES.RATE_LIMIT })
|
|
328
|
+
return
|
|
329
|
+
}
|
|
290
330
|
busy = true
|
|
291
331
|
acquired = true
|
|
292
332
|
controller = new AbortController()
|
package/lib/scene-project.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
14
14
|
import { join, resolve, sep } from 'node:path'
|
|
15
|
-
import { ROUTE, SAFE_NAME, fileSize } from './media-types.js'
|
|
15
|
+
import { ROUTE, SAFE_NAME, fileSize, realPathInside } from './media-types.js'
|
|
16
16
|
|
|
17
17
|
/** Extensions a scene renderer may legitimately request. No HTML/JS/EXE. */
|
|
18
18
|
export const SCENE_FILE_EXT = new Set([
|
|
@@ -81,6 +81,8 @@ export function sceneFilePath(projectDir, segments) {
|
|
|
81
81
|
current = resolve(current, segment)
|
|
82
82
|
if (current.startsWith(prefix) === false) return null
|
|
83
83
|
}
|
|
84
|
+
// v2.0.4 (M2): a junction/symlink inside the project must not lead outside it.
|
|
85
|
+
if (existsSync(current) && realPathInside(base, current) === false) return null
|
|
84
86
|
const ext = current.slice(current.lastIndexOf('.')).toLowerCase()
|
|
85
87
|
if (SCENE_FILE_EXT.has(ext) === false) return null
|
|
86
88
|
return current
|
|
@@ -30,6 +30,7 @@ import { fileURLToPath } from "node:url";
|
|
|
30
30
|
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
31
31
|
import { deflateSync } from "node:zlib";
|
|
32
32
|
import { parseRange } from "../http-media.js";
|
|
33
|
+
import { realPathInside } from "../media-types.js";
|
|
33
34
|
//#region src/pkg-tex.ts
|
|
34
35
|
/**
|
|
35
36
|
* Wallpaper Engine scene.pkg → background image extractor.
|
|
@@ -2184,6 +2185,9 @@ function resolveWebTarget(base, segments) {
|
|
|
2184
2185
|
const resolvedBase = resolve(base);
|
|
2185
2186
|
const target = resolve(join(resolvedBase, ...segments.length > 0 ? segments : ["index.html"]));
|
|
2186
2187
|
if (target !== resolvedBase && !target.startsWith(resolvedBase + sep)) return null;
|
|
2188
|
+
// v2.0.4 (M2): lexical containment alone lets a junction/symlink inside the
|
|
2189
|
+
// project point anywhere on disk; the REAL path must stay inside the real root.
|
|
2190
|
+
if (existsSync(target) && !realPathInside(resolvedBase, target)) return null;
|
|
2187
2191
|
return target;
|
|
2188
2192
|
}
|
|
2189
2193
|
/**
|
|
@@ -2235,6 +2239,33 @@ function pipeFileSafe(res, path, options) {
|
|
|
2235
2239
|
* @param abs - absolute file path.
|
|
2236
2240
|
* @param cacheControl - cache directive (default no-cache).
|
|
2237
2241
|
*/
|
|
2242
|
+
/**
|
|
2243
|
+
* v2.0.4 (H1): every HTML document this host serves is a wallpaper page from the
|
|
2244
|
+
* user's Wallpaper Engine library. Served from the DSH origin without a policy it
|
|
2245
|
+
* would run with the DSH origin's storage and APIs whenever it is opened directly or
|
|
2246
|
+
* embedded by any site (the read-only /web route deliberately accepts cross-site and
|
|
2247
|
+
* `Origin: null` requests so the skin's sandboxed frame can load its sub-resources).
|
|
2248
|
+
* `sandbox allow-scripts` makes the document an opaque origin no matter who loads it
|
|
2249
|
+
* — exactly what the skin's own `<iframe sandbox="allow-scripts">` already does, so
|
|
2250
|
+
* the wallpaper behaves the same in the skin — and frame-ancestors keeps other sites
|
|
2251
|
+
* from embedding it at all.
|
|
2252
|
+
*/
|
|
2253
|
+
export const WE_HTML_CSP =
|
|
2254
|
+
"default-src 'none'; script-src * blob: data: 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; " +
|
|
2255
|
+
"worker-src * blob: data:; style-src * blob: data: 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; " +
|
|
2256
|
+
"font-src * data: blob:; connect-src * data: blob:; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; " +
|
|
2257
|
+
"frame-ancestors 'self' dsh-app: dsh:; sandbox allow-scripts";
|
|
2258
|
+
/** Response headers that make a served HTML/XHTML/XML document inert to the DSH origin. */
|
|
2259
|
+
export function weDocumentHeaders(type) {
|
|
2260
|
+
const lower = String(type ?? "").toLowerCase();
|
|
2261
|
+
if (lower.startsWith("text/html") || lower.startsWith("application/xhtml") || lower.startsWith("application/xml") || lower.startsWith("text/xml")) {
|
|
2262
|
+
return {
|
|
2263
|
+
"content-security-policy": WE_HTML_CSP,
|
|
2264
|
+
"referrer-policy": "no-referrer"
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
return {};
|
|
2268
|
+
}
|
|
2238
2269
|
function serveFile(req, res, abs, cacheControl = "no-cache") {
|
|
2239
2270
|
const stat = statSync(abs, { throwIfNoEntry: false });
|
|
2240
2271
|
if (stat === void 0 || !stat.isFile()) {
|
|
@@ -2258,6 +2289,7 @@ function serveFile(req, res, abs, cacheControl = "no-cache") {
|
|
|
2258
2289
|
if (type === "image/svg+xml") {
|
|
2259
2290
|
headers["content-security-policy"] = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
2260
2291
|
}
|
|
2292
|
+
Object.assign(headers, weDocumentHeaders(type));
|
|
2261
2293
|
if (req.method === "HEAD") {
|
|
2262
2294
|
res.writeHead(200, {
|
|
2263
2295
|
...headers,
|
package/lib/web-project.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
19
19
|
import { basename, join, resolve, sep } from 'node:path'
|
|
20
|
-
import { ROUTE, SAFE_NAME } from './media-types.js'
|
|
20
|
+
import { ROUTE, SAFE_NAME, realPathInside } from './media-types.js'
|
|
21
21
|
import { servePath, sendText } from './http-media.js'
|
|
22
22
|
|
|
23
23
|
/** Extensions a web wallpaper page may load. No archives, no executables. */
|
|
@@ -102,7 +102,9 @@ export function webProjectFilePath(projectDir, segments) {
|
|
|
102
102
|
if (list.length === 0) {
|
|
103
103
|
const entry = projectEntryName(readProjectJson(base))
|
|
104
104
|
if (SAFE_NAME.test(entry) === false) return null
|
|
105
|
-
|
|
105
|
+
const entryPath = join(base, entry)
|
|
106
|
+
if (existsSync(entryPath) && realPathInside(base, entryPath) === false) return null
|
|
107
|
+
return entryPath
|
|
106
108
|
}
|
|
107
109
|
if (list.length > MAX_SEGMENTS) return null
|
|
108
110
|
let current = base
|
|
@@ -117,6 +119,8 @@ export function webProjectFilePath(projectDir, segments) {
|
|
|
117
119
|
current = resolve(current, segment)
|
|
118
120
|
if (current.startsWith(prefix) === false) return null
|
|
119
121
|
}
|
|
122
|
+
// v2.0.4 (M2): a junction/symlink inside the project must not lead outside it.
|
|
123
|
+
if (existsSync(current) && realPathInside(base, current) === false) return null
|
|
120
124
|
const ext = current.slice(current.lastIndexOf('.')).toLowerCase()
|
|
121
125
|
if (WEB_FILE_EXT.has(ext) === false) return null
|
|
122
126
|
return current
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-client-ui-skin-tianlight",
|
|
3
3
|
"description": "Tianlight skin for the dsh web GUI: Makoto Shinkai daylight skies, a looping video wallpaper, a wallpaper library with upload/rotation, optional WebWallGL scene support, and a draggable Mint chibi-cat desktop companion",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
"@deepseek-ai/dsh-api-remotes": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.2.2-0 || >=0.2.2-0 <0.3.0-0",
|
|
44
44
|
"@deepseek-ai/dsh-api-session-controller": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.2.2-0 || >=0.2.2-0 <0.3.0-0"
|
|
45
45
|
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@deepseek-ai/dsh-api-remotes": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@deepseek-ai/dsh-api-session-controller": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
46
54
|
"scripts": {
|
|
47
55
|
"build": "node tools/build.mjs",
|
|
48
56
|
"build:check": "node tools/build.mjs --check",
|
|
@@ -68,6 +76,7 @@
|
|
|
68
76
|
"assets",
|
|
69
77
|
"presets",
|
|
70
78
|
"cordis.patch.yml",
|
|
79
|
+
"docs/THIRD-PARTY.md",
|
|
71
80
|
"skin.json",
|
|
72
81
|
"build-info.json",
|
|
73
82
|
"LICENSE",
|
package/skin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "tianlight",
|
|
3
3
|
"name": "天光",
|
|
4
4
|
"nameEn": "TIANLIGHT",
|
|
5
|
-
"version": "2.0.
|
|
5
|
+
"version": "2.0.4",
|
|
6
6
|
"author": "local",
|
|
7
7
|
"tagline": "新海诚式天光 ・ 动态壁纸与可上传壁纸库 ・ 可选 WebWallGL 场景 ・ 薄荷小猫桌宠 ・ 职责通道(role pack → 桌宠气泡/选择) ・ 生活节奏与主动开口预算 ・ 可替换图片/≤32MiB 视频形象 ・ 液态玻璃三档 ・ AI 桌宠(严格权限) ・ 接入 DSH Agent(受控) ・ 桌宠即入口 ・ 兼容 DSH 0.2.0–0.2.2",
|
|
8
8
|
"description": "以新海诚天光为基调的沉浸式皮肤:全屏循环视频壁纸置于内容之下,半透明天光玻璃面板;壁纸库支持上传、隐藏、轮播、画面滤镜与标签检索;可选的 WebWallGL 通道实时渲染 Wallpaper Engine 场景;可拖拽的薄荷 Q 版小猫桌宠会靠近回应、陪你互动。v0.5.0 加入了宿主配置同步与 schema 迁移、模块开关、渲染预算、特效与氛围层、时段/专注联动、系统音频与 Now Playing、网页壁纸、动态分辨率与预裁剪缓存、可选转码、导入导出包与配方分享链接、排版增强、可读性护栏、键盘无障碍与 zh/en 双语;v0.6.0 接入 Wallpaper Engine 壁纸库;v0.7.0 新增职责通道:外部角色包可在桌宠旁显示带按钮的职责气泡(审批/完成/喝水等),并把用户选择(等会儿/静音/休息时再说)回传给职责引擎。本版只改浏览器半,无需重启 DSH(Ctrl+F5 即可)。同一份构建适配 DSH 0.2.0 / 0.2.1 / 0.2.2(选择器桥按运行时钩子分层降级)。 v1.0.0 正式版:桌宠升级为有生活节奏的角色智能体——虚拟时钟/每日作息/主动开口预算/跨会话记忆,职责提醒可被静默与预算闸门延迟并在窗口期自动回放;形象可替换为本地图片或 ≤32 MiB 视频;新增可选液态玻璃(真实/柔和/磨砂,低电量自动降级);新增「角色 / 生活」设置页与专注计时;无宿主路由改动,Ctrl+F5 即可。 v1.1.0:角色中枢(侧栏入口 + 全屏面板,一屏管生活/形象/玻璃/记忆)、AI 桌宠(本地大脑 + 可选 LLM 适配器,严格能力白名单与硬上限、审计与暂停开关)、形象支持本地上传图片/视频(≤32MiB)。 v1.2.0:桌宠右键直达角色中枢;新增受控 DSH Agent 接入(agentChat 权限默认关闭、显式授权、无自动触发、每日≤10 次/≤500 字/60s,可连接现有会话或新建桌宠会话)。 v1.3.0:角色入口收进桌宠——去掉设置页「角色 / 生活」与侧栏「角色」按钮,右键桌宠(或桌宠菜单「角色中枢」)打开全部功能;生活节奏设置已并入角色中枢。 v1.4.0:权限模型调整——「驱动 DSH」仍强烈永久禁止;直接联网/读写文件/执行命令/代审批/代发消息/安装插件改为用户可显式授予的高风险权限(默认关闭、可撤销、全程审计;皮肤不内置实现,需你注册能力适配器才会被调用)。 v1.5.0:恢复默认桌宠形象、液态玻璃默认开启并加预览(实时可见)、角色中枢新增天气卡(启用/城市/定位/刷新,显示当前天气)、一键功能自检、应用内反馈面板(复制/预填 GitHub Issue/下载 JSON)+ GitHub issue 模板。 v1.6.0:安全上限整体上调(主动开口 12/50、本地 chat 20/分 1200/天、输入 1000 字、LLM 日 60/上限 120、分钟 6、输出 1200 字、20s 超时、Agent 日 30/上限 100、输入 1000 字/120s);液态玻璃在 depth!=off 时统一应用到全部界面(侧栏/设置/壁纸库/中枢/对话框/菜单/列表框/输入区/气泡/胶囊/提示等),无壁纸场景时自动补一层玻璃兜底,关闭档零变化、低电量自动降级。 v1.6.1:液态玻璃升级 v2(OriginOS 风更厚磨砂、多层 specular 高光、1px 亮暗边、外柔光、细噪点与柔和回弹);修复侧栏「新会话」按钮深色块并玻璃化;DSH 主界面右上新增常驻天气胶囊(点击打开角色中枢天气卡)。 v1.6.2:移除壁纸左上角固定的「晨光/太阳」装饰(旧版每张壁纸重建场景都会绘制一个 top-left dawn burst,看起来像常驻小太阳);场景层只保留 veil 天空光渐变,切换壁纸不再出现任何固定角标(仅视觉变化)。 v1.6.3:显示名精简为「天光」(skin id / 包名 / data 属性 / 设置键均不变,旧用户无迁移成本);完成 DSH 0.2.0-rc.2 / 0.2.1-rc.5 / 0.2.2-rc.1 三线兼容性实证(compat-matrix 三行全 COMPATIBLE、hook-scan 全桥接钩子在位、upgrade-check 升级假设全部成立)。 v1.6.4(公开 v1.0.3 正式版):桌宠新增「AI 对话」菜单(输入条 + 气泡回复,识别当前替换的图片/视频形象,视频上方空间不足自动翻转);每个自定义形象可编辑一套动作名与回复(按 mediaId 生效,未改动跟随默认);角色中枢形象文件名常显列表(文件名/类型/大小,点击切换);完成全代码审查修复(默认文案冻结、WE 宿主 stream error、低电量异步泄漏、bridge 节点清理、转码超时兜底、宿主同源栅栏收紧、聊天视口翻转)。 v1.6.5(公开 v1.0.4 正式版):修复桌宠「AI 对话」关不掉的问题——新增面板「×」关闭按钮、点空白关闭、全局 Esc 关闭、[hidden] 真正隐藏,并支持桌宠靠近屏幕底部时点桌宠直接退出(菜单被裁到视口外也能退出)。仅浏览器半,无宿主路由改动,Ctrl+F5 即可。 v1.6.6(公开 v1.0.5 正式版):修复 AI 对话面板在桌宠默认靠右位置时「×」关闭按钮被视口裁掉的问题(面板水平夹取进视口,拖拽/窗口缩放后重算),并把顶部翻转阈值改为按面板实际高度计算(新增按钮行后不再裁到顶部),略增大关闭按钮点击区域。仅浏览器半;版本常量变更,Ctrl+F5 + 重启 DSH 一次可消除宿主半 stale 提示。 v1.6.7(公开 v1.0.6 正式版):壁纸面板默认视图新增一等「URL 地址」「ShaderToy 代码」快捷来源(点击直达编辑器,并修复 URL/Shader 应用假成功与卡死);桌宠「AI 对话」收到回复即自动收起输入面板、只留气泡回复;新增「☰」对话历史(与 AI 对话相邻):可查看历史、开启新对话、编辑已发消息、删除对话,数据存本机 localStorage。仅浏览器半;版本常量变更,Ctrl+F5 + 重启 DSH 一次可消除宿主半 stale 提示。 v1.6.8(公开 v1.0.7 正式版):桌宠新增「自主思考」智能体——按间隔做内心独白、可自主开口(受既有主动开口预算约束),聊天回复融入最近对话/时段/记忆/当前想法更像人;角色中枢新增「自主思考」卡片(开关/间隔/自主开口/现在想一下/最近想法/清空)。不新增权限与网络通道,不驱动 DSH Agent;仅浏览器半,Ctrl+F5 即可;版本常量变更,重启 DSH 一次可消除宿主半 stale 提示。 v2.0.0:桌宠 AI 对话接入 DSH 自带 LLM(宿主路由 _pet-chat;默认跟随 DSH 默认模型,仍须在角色中枢两步授权 llm;不建会话、不调工具、60/日、6/分、1200 字、20s 超时;失败按稳定错误码提示);版本号统一为 2.0.0(仓库 / DSH 安装 / GitHub 发布同名同号);并入 v1.6.8 之后的体检修复批次(设置同步合并与冲突保护、宿主/桥接安全收口、统一浮层层序、壁纸切换与性能、可读性/低电量、导出导入一致性);默认演示素材精简为 bg-4k.mp4(H.264 4K)与 cat.png,bg-main.mp4 改为可选自备首选槽。本版改了宿主半:升级后必须重启 DSH 一次 + Ctrl+F5,之后纯浏览器半更新仍只需 Ctrl+F5。",
|