@lemoncat7/dsh-theme-xiaohei 0.4.4 → 0.4.6

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 (61) hide show
  1. package/README.md +18 -91
  2. package/docs/releases/0.4.5.md +21 -0
  3. package/docs/releases/0.4.6.md +17 -0
  4. package/docs/releases/README.md +14 -0
  5. package/docs/startup-audit-20261013.md +47 -0
  6. package/lib/bloub-heixiu/layout.d.ts +16 -0
  7. package/lib/bloub-heixiu/layout.js +14 -0
  8. package/lib/bloub-heixiu/runtime.js +41 -10
  9. package/lib/chrome/conversation-messages.js +17 -4
  10. package/lib/chrome/conversation-submissions.d.ts +3 -0
  11. package/lib/chrome/conversation-submissions.js +68 -0
  12. package/lib/chrome.js +2 -0
  13. package/lib/client.companion.js +100 -0
  14. package/lib/client.decorations.js +51 -0
  15. package/lib/client.js +113 -845
  16. package/lib/client.pose-chin-dark.js +4 -0
  17. package/lib/client.pose-chin-light.js +4 -0
  18. package/lib/client.pose-doze-dark.js +4 -0
  19. package/lib/client.pose-doze-light.js +4 -0
  20. package/lib/client.pose-halfpeek-dark.js +4 -0
  21. package/lib/client.pose-halfpeek-light.js +4 -0
  22. package/lib/client.pose-peek-dark.js +4 -0
  23. package/lib/client.pose-peek-light.js +4 -0
  24. package/lib/client.pose-seated-dark.js +4 -0
  25. package/lib/client.pose-seated-light.js +4 -0
  26. package/lib/client.rig-chin-dark.js +4 -0
  27. package/lib/client.rig-chin-light.js +4 -0
  28. package/lib/client.rig-halfpeek-dark.js +4 -0
  29. package/lib/client.rig-halfpeek-light.js +4 -0
  30. package/lib/client.rig-peek-dark.js +4 -0
  31. package/lib/client.rig-peek-light.js +4 -0
  32. package/lib/client.rig-seated-dark.js +4 -0
  33. package/lib/client.rig-seated-light.js +4 -0
  34. package/lib/companion-entry.d.ts +3 -0
  35. package/lib/companion-entry.js +3 -0
  36. package/lib/composer-draft-preview.d.ts +4 -0
  37. package/lib/composer-draft-preview.js +8 -0
  38. package/lib/decorations-entry.d.ts +3 -0
  39. package/lib/decorations-entry.js +18 -0
  40. package/lib/deferred-companion.d.ts +5 -0
  41. package/lib/deferred-companion.js +9 -0
  42. package/lib/deferred-decorations.d.ts +3 -0
  43. package/lib/deferred-decorations.js +51 -0
  44. package/lib/host-dom.d.ts +1 -1
  45. package/lib/host-dom.js +6 -2
  46. package/lib/plugin.js +13 -6
  47. package/lib/portal.js +1 -1
  48. package/lib/scene/character-assets.d.ts +13 -0
  49. package/lib/scene/character-assets.js +26 -0
  50. package/lib/scene/character-motion.js +14 -7
  51. package/lib/scene/ids.d.ts +5 -0
  52. package/lib/scene/ids.js +5 -0
  53. package/lib/scene/runtime.d.ts +1 -1
  54. package/lib/scene/runtime.js +2 -15
  55. package/lib/scene/styles.d.ts +1 -3
  56. package/lib/scene/styles.js +2 -8
  57. package/lib/scene/wallpaper-character-styles.js +1 -5
  58. package/lib/scene/wallpaper-character.d.ts +2 -1
  59. package/lib/scene/wallpaper-character.js +24 -15
  60. package/lib/sidebar-glass.js +1 -1
  61. package/package.json +4 -1
package/README.md CHANGED
@@ -1,108 +1,35 @@
1
1
  # Xiaohei Night for DSH
2
2
 
3
- > 当前版本 **0.4.3**,已验证宿主 **DSH 0.2.0-rc.2**(宿主仍为 RC)。详见 [兼容说明](docs/dsh-020-compatibility.md) 和 [发布说明](docs/releases/0.4.3.md)。
3
+ 基于 DeepSeek Harness 官方主题接口的非官方粉丝主题。以低饱和天空、日夜光线和小黑角色提供陪伴感,保留原生工作台布局。
4
4
 
5
- `Xiaohei Night` 是一个基于 DeepSeek Harness 官方主题注册接口开发的非官方粉丝主题插件。主题以低饱和天空、日夜光线和坐在小平台上的人形小黑表达陪伴感,保留原生工作台布局。
5
+ [![npm](https://img.shields.io/npm/v/%40lemoncat7%2Fdsh-theme-xiaohei)](https://www.npmjs.com/package/@lemoncat7/dsh-theme-xiaohei)
6
6
 
7
- ## 兼容性
7
+ ## 功能
8
8
 
9
- 正式版 `0.4.3` 适配并验证 DeepSeek Harness `0.2.0-rc.2`,开发依赖锁定该宿主版本,需要 Node.js `^22.19.0` 或 `>=24.0.0`。旧宿主请保留 `0.3.5`。保留侧栏操作与仅思考消息显示修复,不修改官方核心代码。“跟随系统”读取当前设备/浏览器配色偏好,不按服务器时间切换。
10
-
11
- ## 当前范围
12
-
13
- - 保留 DSH 原有布局、密度和交互层级。
14
- - 使用半透明墨黑表面,让背景可见但不压过对话内容。
15
- - 使用青绿色作为唯一交互强调色,错误、成功、警告保留独立语义色。
16
- - 背景与人物分层:保留日间/夜间的柔和天空;人物使用独立高清、无损 WebP,不随整张壁纸放大。坐姿、托腮和打盹统一为 720×1019,探头素材宽 360;最大显示尺寸仍有约 3 倍像素密度。姿势按需解码后再显示,保留轮廓与五官细节。
17
- - 根据真实剩余空间决定姿势:右侧宽裕时坐在小平台上,中等窗口扶着侧栏探头;细侧栏空位使用更窄的半脸偷看。没有安全空间时暂时隐藏,不覆盖消息、头像或侧栏按钮,不修改业务布局。
18
- - 坐姿下,用户空闲 30 秒后托腮、3 分钟后闭眼打盹;鼠标、键盘、触摸点击或滚轮操作后恢复。页面隐藏或不再使用坐姿时暂停计时,返回后重新开始。最多一个空闲计时器,睡着后不继续轮询,不绑定 AI 状态,不读取输入内容。姿势以 220ms 淡化过渡,不是连续骨骼动画。
19
- - 耳根固定、耳尖连续摆动,坐姿和托腮的尾巴通过 7 个关节点与网格蒙皮连续变形,鼠标靠近可触发联动。眨眼提取原图睫毛笔触并连续变形,瞳孔平滑跟随鼠标;身体与四肢没有完整骨骼绑定,并非完整 Live2D。短暂动作最高 60 帧,视线停稳后停止绘制,后台与减少动态模式暂停。
20
- - 装饰背景使用稳定的大视口高度;姿势切换不横穿会话,不读取会话内容或执行状态;流式文本高度变化不触发布局重测。软键盘兼容性仍需 iOS / Android 真机验收。
21
- - bloub 嘿咻保留 alpha.171 略宽于高的小黑团轮廓和圆润奶白眼圈。单击依次眨眼回应、轻跳、歪头、感叹号惊讶;双击或键盘 Shift+Enter 轮换散开重聚、蛋形、环绕、六边形、单眼眨眼、睁大眼、感叹号、警觉、休眠、思考、通知。每隔随机 20–40 秒自动选择一种待机动作(含眨眼、休眠,不含思考、通知、感叹号),不连续重复;打字、飞行或其他动作尚未结束时顺延。后台/离屏/减少动态时取消调度,恢复后重新等待,不补播。
22
- - 嘿咻通过可选的官方 `sessions.list` / `binding().session` 只读订阅当前会话:运行中显示三点思考(代表本轮处理中,不细分推理/输出/工具),观察到本轮结束时提醒一次,新执行错误显示感叹号。提醒表示本轮结束,不承诺任务成功,手动停止也可能触发。初次加载历史、切换会话、重新加载历史不会补发完成提醒。通知徽点采用暖金色和真正透明的缺口;不申请浏览器通知权限、不发送消息、不操作会话、不关联壁纸。
23
- - 新会话将同一只嘿咻停在输入框上方、Logo 左边;输入区落到底部时,用可打断的约 950 ms 飞行动画转移到输入框左侧。手机给 Logo 左侧留出空间并缩小角色;普通窗口缩放不触发跨区域飞行。仅使用现有 Hero 品牌元素和输入框位置,不读取或改变会话业务数据。
24
- - 慢浮动周期 14 秒,宽屏横向 ±7 px、纵向 ±10 px;窄屏自动减小幅度。视线跟随窗口内鼠标,身体只在近处轻微靠近(宽屏最多 5 / 3.5 px),约 480 ms 柔和回位。CSS transform 合成浮动和跟随,不增加持续 JS 帧计算或逐帧布局测量;除短暂换位飞行外,点击区域保持固定。减少动态模式关闭浮动、跟随、飞行和自动动作;不改发送键。
25
- - 嘿咻初始只有 10 个 SVG 节点;首次触发特殊动作才一次性建立粒子/轨道池及通知缺口,之后固定复用(总计 102 节点),帧循环不新增节点、不测量布局,结束后隐藏特效。小尺寸变形中的眼圈适当收窄,避免相互粘连。空闲间歇唤醒,离屏、隐藏和减少动态时暂停,卸载释放监听、观察器、帧请求和定时器。宽屏使用框外空位,窄屏使用框内左侧预留位,不改发送键。原项目 MIT 许可与固定源码版本见 `THIRD_PARTY_NOTICES.md`;观察及改动记录见 `docs/heixiu-motion-reference.md`。
26
- - 对话双方保留静态身份头像;消息气泡使用更舒适的横向留白。
27
- - 左侧栏只有一层独立磨砂材质,目录与会话不再各自模糊。展开目录、Hover、选中会话使用逐级增强的中性底色;移除拼接空间框和底部卡片,原生滚动、拖拽和菜单保持不变。
28
- - 侧栏展开时,内容等待列宽到位后统一淡入,避免文字先出现再被展开裁切。切换期间暂停装饰角色布局和绘制;侧栏品牌金属画面缓存复用,不再反复创建 WebGL 和编译着色器。
29
- - 新会话使用统一圆角控件;发送键嘿咻继续保留完整帧眨眼。左栏行宽、缩进、滚动条预留和收起布局交给 DSH 管理,避免主题强行计算宽度。
30
- - 支持减少动态、增强对比度、强制颜色和打印模式降级。
31
- - 主题通过 DSH Theme Runtime 覆盖 Light / Dark / System 的语义 token,不替换用户的主题偏好;卸载后由 DSH 恢复原始 token。
9
+ - Light / Dark / System 主题,语义颜色与半透明材质。
10
+ - 随空间调整的角色姿态与轻量互动,不遮挡消息或侧栏操作。
11
+ - 可拖动交互的嘿咻装饰,以及会话运行和结束的只读状态提示。
12
+ - 响应式布局,减少动态、增强对比度、强制颜色和打印模式降级。
32
13
 
33
14
  ## 安装
34
15
 
35
- 通过 npm 安装正式版:
36
-
37
- ```bash
38
- dsh plugin --profile web add @lemoncat7/dsh-theme-xiaohei@0.3.5
39
- ```
40
-
41
- 也可在源码目录运行 `npm pack` 生成完整包后安装:
42
-
43
- ```bash
44
- dsh plugin --profile web add ./lemoncat7-dsh-theme-xiaohei-0.3.5.tgz
45
- ```
46
-
47
- 卸载:
48
-
49
- ```bash
50
- dsh plugin --profile web remove @lemoncat7/dsh-theme-xiaohei
51
- ```
52
-
53
- ## 开发与验证
54
-
55
- 需要 Node.js 22.19+ 或 24+:
56
-
57
- ```bash
58
- npm install
59
- npm test
60
- npm run pack:check
61
- ```
62
-
63
- 使用 Docker 24 构建可安装包:
16
+ 适配 DeepSeek Harness **0.2.0-rc.2**(宿主仍为 RC),需要 Node.js **^22.19.0 或 >=24.0.0**。其他宿主版本请先查阅[兼容说明](docs/dsh-020-compatibility.md)和对应版本的发布说明。
64
17
 
65
18
  ```bash
66
- npm run pack:docker
19
+ dsh plugin --profile web add @lemoncat7/dsh-theme-xiaohei@latest
67
20
  ```
68
21
 
69
- ### 代码边界
70
-
71
- - `src/index.ts` 只负责 Host 首帧加载装饰,保持自包含并在客户端插件启动前运行。
72
- - `src/plugin.ts` 通过 DSH 生命周期装配主题 token、Chrome、场景与交互行为。
73
- - `src/chrome/` 管理控件和公共插件表面;`src/scene/` 分离场景样式与运行时 DOM。
74
- - `src/host-contract.ts` 集中维护宿主插槽选择器;`src/host-dom.ts` 为运行期模块共享一个批处理 DOM 观察器。左栏只在尺寸或节点变化时测量布局,流式文本更新不重复强制布局。
75
- - `src/scene/character-layout.ts` 只计算安全位置,`character-idle.ts` 只管理空闲期限,`character-poses.ts` 统一素材目录;`wallpaper-character.ts` 负责生命周期、按需解码和呈现,卸载时释放监听与计时器。
76
- - `src/vendor/bloub/` 保留上游动画计算,`src/bloub-heixiu/` 分离 SVG 呈现、布局、样式和生命周期。`signals.ts` 可选地只读订阅当前会话执行状态,不读取会话内容,不操作会话。
77
- - 旧树洞/落叶挂载器已移除,历史图片保留供版本回溯;背景、材质和业务状态各自独立。
78
-
79
- ## 扩展工具接入主题
80
-
81
- SSH、伙伴、Git 等后续工具不需要复制主题 CSS。插件根节点使用公开的框架属性即可继承小黑主题:
82
-
83
- ```html
84
- <section
85
- data-xiaohei-frame="module"
86
- data-xiaohei-frame-ornament="spirit-knot"
87
- data-xiaohei-module-kind="ssh"
88
- >
89
- <header data-xiaohei-frame-header>
90
- <strong>远程主机</strong>
91
- <div data-xiaohei-frame-actions><!-- 工具按钮 --></div>
92
- </header>
93
- <!-- 模块内容 -->
94
- </section>
95
- ```
22
+ 安装或升级后重启对应的 DSH Profile;`web` 请按实际使用的 Profile 调整。
96
23
 
97
- - `data-xiaohei-frame="module"`:完整区域框裱;紧凑控件可使用 `compact`。
98
- - `data-xiaohei-frame-ornament="spirit-knot"`:兼容既有扩展的猫尾灵珠挂饰入口;挂饰固定在框角,悬停时提供轻微转动反馈。
99
- - `data-xiaohei-module-kind`:保留模块身份,方便主题以后为 SSH、伙伴、Git 等提供差异化细节。
100
- - `data-xiaohei-frame-header` 与 `data-xiaohei-frame-actions`:统一标题栏和操作区对齐。
24
+ 安装后在 DSH 的主题设置中选择 Xiaohei Night;「跟随系统」采用当前设备或浏览器的配色偏好。卸载后由 DSH 恢复原始主题。
101
25
 
102
- ## 视觉资产
26
+ ## 使用边界
103
27
 
104
- 背景使用内置的日夜天空 WebP、CSS 渐变及按需加载的人物透明素材,不加载 Three.js、外部 CDN 或角色模型。品牌头像、消息静态头像、加载动画与发送键嘿咻作为内联资源随插件交付;嘿咻交互使用 SVG。
28
+ 主题不修改官方核心代码,不读取对话正文,也不代替业务插件提供布局或授权能力。角色状态提示只表示本轮结束,不保证任务成功。人物动画不是完整 Live2D;移动端软键盘兼容仍需真机验收。
105
29
 
106
- ## License
30
+ ## 文档
107
31
 
108
- 代码使用 MIT License。角色及相关作品权利归原权利方所有,本项目与官方无关。
32
+ - [效果、开发与扩展接入](docs/usage.md)
33
+ - [第三方许可](THIRD_PARTY_NOTICES.md)
34
+ - [版本与发布说明](https://github.com/lemoncat7/dsh-theme-xiaohei/releases)
35
+ - [发布维护约定](docs/releases/README.md)
@@ -0,0 +1,21 @@
1
+ # 0.4.5 · 对话输入预览与主题按需加载
2
+
3
+ 兼容宿主:DeepSeek Harness 0.2.0-rc.2。本版本不调整宿主兼容范围。
4
+
5
+ ## 对话体验
6
+
7
+ - 输入时显示草稿气泡,跟随正式对话内容列对齐;清空、切换会话后同步更新。
8
+ - 适配宿主发送后的提前回显及插话消息,统一气泡、头像和浅深色样式,保留附件及排队操作。
9
+ - 覆盖窄屏、长文本与安全文本渲染;草稿预览不会代替实际发送。
10
+
11
+ ## 启动与人物资源
12
+
13
+ - 壁纸随主题入口同步安装,不等待人物下载,减少首屏只有纯色背景的等待。
14
+ - 装饰和人物控制器使用 DSH 原生异步分包;按当前姿态、主题及动作设置加载对应资源,支持请求去重与缓存。
15
+ - 保留原始素材,新增 44 张有损 WebP 运行时副本,资源从约 4.44 MB 降至 0.68 MB,减少约 85%;尺寸和透明通道保持不变。
16
+ - 构建校验源图与运行时副本的 SHA256,防止发布过期素材;生成 10 个姿态和 8 个动作分包。
17
+ - 动作资源加载失败时保留静态人物;卸载及姿态变化时丢弃失效的异步结果。
18
+
19
+ 验证:构建通过,113 项测试通过、1 项原有跳过;草稿预览、发送回显及头像、首屏壁纸的浅深色与窄屏/宽屏浏览器回归通过。
20
+
21
+ 验证环境为 Web / DSH 0.2.0-rc.2;未进行 Desktop 实机回归。上述优化不代表所有插件加载和服务端等待均已消除。详细测量及部署记录见 `docs/startup-audit-20261013.md`。
@@ -0,0 +1,17 @@
1
+ # 0.4.6 · 对话首屏与人物交互性能优化
2
+
3
+ 适配 DeepSeek Harness **0.2.0-rc.2**;需要 Node.js **^22.19.0 或 >=24.0.0**。不调整宿主兼容范围。
4
+
5
+ ## 本次改进
6
+
7
+ - 输入区人物引擎拆为独立分包,在对话输入区出现并完成首屏绘制后加载,减少主题入口的同步初始化工作。
8
+ - 合并宿主 DOM 变化通知,跳过不影响输入区位置的流式正文更新、人物自身效果和历史滚动,减少重复布局测量。
9
+ - 人物挂载后延后一帧定位,先读取宿主布局再写入样式;通过输入区尺寸计算人物位置,避免写样式后立即测量造成同步布局。
10
+ - 保留欢迎页、窄屏、会话切换、窗口缩放、减少动画和卸载清理行为,不修改对话内容、发送逻辑或用户设置。
11
+
12
+ ## 验证与升级
13
+
14
+ - 构建及单元测试:119 项通过、1 项原有跳过,无失败。
15
+ - 隔离浏览器回归覆盖 375 / 1280 像素、浅深主题和减少动画,共 8 种组合;检查流式更新、历史滚动、输入区替换、人物定位和清理,不向生产会话发送消息。
16
+ - 无数据迁移;升级后重启对应 Profile 并刷新页面,以加载新的异步分包。
17
+ - 本次验证针对 Web,未进行 Desktop 实机回归;优化不代表服务端或其他插件的等待均已消除。
@@ -0,0 +1,14 @@
1
+ # 发布说明维护
2
+
3
+ [返回项目介绍](../../README.md) · [GitHub Releases](https://github.com/lemoncat7/dsh-theme-xiaohei/releases)
4
+
5
+ README 只维护当前功能、安装方法、适用边界和文档入口,不逐版追加更新日志。详细配置放在使用文档中。
6
+
7
+ 每次发布:
8
+
9
+ 1. 在本目录维护 `<version>.md`,记录该版变更、兼容要求、迁移注意事项和实际验证范围;历史说明不代表当前行为。
10
+ 2. 将对应说明同步到 GitHub Release 正文,使用有效的绝对文档链接;不要只放一句更新摘要或指向 README。
11
+ 3. 核对版本标签、npm 版本与附件的一致性,优先使用已验证的同一安装包,不重新构建替换旧版本附件。
12
+ 4. 同一版本复用已有 Release;发布后检查不再遗留重复草稿。补发历史版本不能将它设成 Latest,撤回版本不能自动重新发布。
13
+
14
+ 本次从 README 迁出的历史段落保存在对应版本文件中。尚未正式发布的说明文件不代表该版本可用,请以 GitHub Release 和 npm 实际记录为准。
@@ -0,0 +1,47 @@
1
+ # 刷新加载排查(2026-10-13)
2
+
3
+ 环境:正式容器 DSH 0.2.0-rc.2,本机 Chromium 无头浏览器,直连 127.0.0.1:3080。
4
+ 只加载页面和刷新,没有发送消息或修改业务配置。不能将此结果直接等同于公网代理或 Desktop 的耗时。
5
+
6
+ ## 观察
7
+
8
+ - 三组首开/刷新侧栏就绪时间(毫秒):16775/1238/1266、11645/5038/1120、2213/2025/1268。第三组开启 CPU 采样,有额外测量开销。
9
+ - 两个主要插件聚合脚本解码大小为 10584847 和 11332146 字节。直接 HTTP 响应传输大小接近原文大小;缓存有命中,也存在刷新后重传。尚未验证公网反代压缩与缓存策略。
10
+ - 安装的主题 client.js 约 6.5 MiB;主题生成的背景、角色图片以 base64 字符串打进脚本。不能单凭包大小断言它是全部瓶颈。
11
+ - 慢刷新样本首页 responseStart 为 3775 ms,后续插件下载约 100–140 ms。存在服务侧响应等待,不只是浏览器执行慢。
12
+ - 慢首开样本前端出现 2028 ms 和 4988 ms 长任务。快首开 CPU 样本包含模块 materialize、图片 onload、文本匹配及布局测量;聚合脚本归属仍需进一步拆分定位。
13
+
14
+ ## 本轮边界
15
+
16
+ 修复发送后待确认消息缺少头像,共享正式用户头像规则。106 项测试通过,1 项原有跳过;浏览器覆盖浅深色、375/1280 宽度及队列不添加头像。
17
+
18
+ 没有修改宿主源码、清除用户缓存、禁用业务插件或提前隐藏加载状态。启动优化应单独验证:图片资源按需加载并独立缓存、装饰初始化延后,以及服务/反代响应与压缩配置。需同时保留 Desktop 兼容和原视觉效果。
19
+
20
+ ## 后续实施:装饰原生分包
21
+
22
+ - 使用 DSH rc.2 官方 `require.async('./client.decorations.js')` 与 `chunk` 注册协议,没有增加自定义图片服务、跨域请求或宿主修改。Desktop 使用同一载体协议;本轮实机验证是 Web,未声称已运行 Desktop 回归。
23
+ - 主题启动脚本从约 6.5 MiB 降至约 450 KiB。壁纸、动作和角色原图保留在约 5.7 MiB 自包含装饰分包中,不降质;不是每张图片各自请求。
24
+ - 共享场景 ID 拆到无资源依赖模块,避免侧栏仅引用 ID 却把整套图片拉进主包。
25
+ - 主界面侧栏提交后才调度下载;下载/初始化失败只影响装饰。卸载取消定时/空闲调度,异步返回后检查销毁状态。
26
+ - 构建先写 chunk、再写 client.js,符合 DSH 根据入口修订生成所有分包缓存地址的约定。
27
+ - 正式验证:侧栏 4787/1741/1165 ms;装饰资源三次都成功,后两次 transferSize=0(缓存命中)。首屏插件聚合资源约 21.96 MB → 15.70 MB。首开有 CPU 采样开销且服务负载有波动,不作为固定提速比例。
28
+ - 109 项测试通过、1 项原有跳过,另有气泡、头像、窄屏与浅深色浏览器回归。正式服务 API HTTP 200。
29
+ - 备份:`dsh-docker/backups/theme-submission-9Kd164aL`;部署构建:`theme-submission-1791884068942`。
30
+
31
+ 剩余:服务侧响应等待、核心布局测量长任务仍存在,本轮不改宿主源码。不能将所有刷新延迟标记为已消除。
32
+
33
+ ## 首屏背景修正
34
+
35
+ 用户反馈角色分包完成前只有纯色背景。两张壁纸合计实际仅约 17 KiB,因此将壁纸 CSS 与角色 CSS 分离:壁纸随主题同步安装,不再使用空闲调度;角色和动作仍在独立分包内。入口约 474 KiB,未回退到原来的 6.5 MiB。浏览器测试覆盖浅深色、375/1280、没有角色分包时背景图解码成功及卸载清理。109 项测试通过,1 项原有跳过。
36
+
37
+ ## 人物有损压缩与逐资源加载
38
+
39
+ - 原始素材保留,使用 `scripts/compress-character-assets.py` 生成 WebP quality 82 运行时副本。44 张素材从 4,440,792 字节降到 676,052 字节,减少约 85%;尺寸和透明通道保持一致,构建校验源文件及副本 SHA256。显示尺寸下对比了浅深色 seated/chin 素材。
40
+ - 将资源拆为 10 个姿态分包和 8 个动作分包。入口仍约 474 KiB;人物控制器从约 5.7 MiB 降到约 22 KiB;单个姿态约 37–83 KiB,动作约 22–46 KiB。
41
+ - 仅加载当前姿态、当前主题的图片;启用动作且角色可见时才加载对应动作层。减少动态效果设置下不请求动作分包。请求去重、成功缓存,失效异步结果不覆盖当前姿态;动作失败保留静态图。
42
+ - 壁纸仍随入口同步安装,不等待人物素材。继续使用 DSH 原生分包协议,不新增资源服务或修改宿主。
43
+ - 全套测试 113 通过、1 项原有跳过;最终补充的 18 个分包完整性断言及资源加载测试 3 项通过。`git diff --check` 通过。
44
+ - 正式容器浏览器实测:初始只请求 `pose-seated-light`;切深色才请求 `pose-seated-dark`;切回浅色无新请求;关闭减少动态效果后才请求 `rig-seated-light`,动作层成功显示。容器 `running healthy`、重启次数 0。未进行 Desktop 实机验证。
45
+ - 部署构建:`theme-submission-1791886119221`;备份:`dsh-docker/backups/theme-submission-O30fGwxx`。
46
+
47
+ 本轮降低的是人物素材传输、解析和解码压力,不代表服务侧等待或所有插件的启动耗时均已消除。
@@ -15,4 +15,20 @@ export declare function resolveHeixiuWelcome(hero: {
15
15
  left: number;
16
16
  top: number;
17
17
  } | undefined;
18
+ /** Mirror the owned absolute-position CSS without measuring after style writes.
19
+ * The containing block is the composer's padding box (inside its border).
20
+ */
21
+ export declare function resolveHeixiuBox(composer: {
22
+ left: number;
23
+ top: number;
24
+ height: number;
25
+ }, border: {
26
+ left: number;
27
+ top: number;
28
+ }, dock: ReturnType<typeof resolveHeixiuDock>, welcome: ReturnType<typeof resolveHeixiuWelcome>): {
29
+ left: number;
30
+ top: number;
31
+ width: number;
32
+ height: number;
33
+ };
18
34
  //# sourceMappingURL=layout.d.ts.map
@@ -9,4 +9,18 @@ export function resolveHeixiuWelcome(hero, composer, sidebarRight, viewportWidth
9
9
  const left = Math.max(sidebarRight + 6, hero.left - size - (size === 52 ? 10 : 24));
10
10
  return { size, left: left - composer.left, top: hero.top + hero.height / 2 - size / 2 - composer.top };
11
11
  }
12
+ /** Mirror the owned absolute-position CSS without measuring after style writes.
13
+ * The containing block is the composer's padding box (inside its border).
14
+ */
15
+ export function resolveHeixiuBox(composer, border, dock, welcome) {
16
+ const size = welcome?.size ?? dock.size;
17
+ if (welcome)
18
+ return { left: composer.left + border.left + welcome.left,
19
+ top: composer.top + border.top + welcome.top, width: size, height: size };
20
+ if (dock.mode === 'compact')
21
+ return { left: composer.left + border.left + 4,
22
+ top: composer.top + border.top + 6, width: size, height: size };
23
+ return { left: composer.left + border.left - 18 - size,
24
+ top: composer.top + composer.height / 2 - size / 2, width: size, height: size };
25
+ }
12
26
  //# sourceMappingURL=layout.js.map
@@ -4,7 +4,7 @@ import { STATE_BY_ID } from '../vendor/bloub/states.js';
4
4
  import { subscribeXiaoheiHostDom } from '../host-dom.js';
5
5
  import { XIAOHEI_HOST_SELECTORS } from '../host-contract.js';
6
6
  import { createHeixiuRenderer } from './renderer.js';
7
- import { resolveHeixiuDock, resolveHeixiuWelcome } from './layout.js';
7
+ import { resolveHeixiuDock, resolveHeixiuWelcome, resolveHeixiuBox } from './layout.js';
8
8
  import { resolveHeixiuFollow, resolveHeixiuGaze } from './follow.js';
9
9
  import { createHeixiuFlight } from './flight.js';
10
10
  import { heixiuAmbientDelay, heixiuAmbientIndex } from './ambient.js';
@@ -184,6 +184,7 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
184
184
  const next = doc.querySelector(XIAOHEI_HOST_SELECTORS.composerCard);
185
185
  const nextSidebar = doc.querySelector(XIAOHEI_HOST_SELECTORS.sidebarShell);
186
186
  const nextHero = doc.querySelector(XIAOHEI_HOST_SELECTORS.composerHeroMark);
187
+ let mounted = false;
187
188
  if (composer !== next || sidebar !== nextSidebar || hero !== nextHero) {
188
189
  composer?.removeAttribute('data-heixiu-dock');
189
190
  ro?.disconnect();
@@ -202,6 +203,7 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
202
203
  ro?.observe(hero);
203
204
  if (heroRow)
204
205
  ro?.observe(heroRow);
206
+ mounted = true;
205
207
  }
206
208
  if (!composer) {
207
209
  button.remove();
@@ -213,9 +215,17 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
213
215
  // The native trailing toolbar must stay last: DSH and the existing send
214
216
  // skin use its structural position. The overlay must not replace it.
215
217
  const toolbar = [...composer.children].reverse().find(child => child !== button);
218
+ button.hidden = true;
216
219
  composer.insertBefore(button, toolbar ?? null);
217
220
  io?.observe(button);
218
221
  dirty = true;
222
+ mounted = true;
223
+ }
224
+ // Let the host lay out the new dock before reading geometry. Do not force
225
+ // a synchronous layout of the entire newly mounted conversation here.
226
+ if (mounted) {
227
+ scheduleLayout();
228
+ return;
219
229
  }
220
230
  if (!dirty)
221
231
  return;
@@ -223,16 +233,21 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
223
233
  const wasFlying = flight.active;
224
234
  const viewportChanged = viewportWidth !== win.innerWidth || viewportHeight !== win.innerHeight;
225
235
  const origin = flight.capture(bounds);
226
- follow(0, 0);
227
236
  const rect = composer.getBoundingClientRect();
237
+ const border = { left: composer.clientLeft || 0, top: composer.clientTop || 0 };
228
238
  const sidebarRight = sidebar?.getBoundingClientRect().right ?? 0;
229
239
  const dock = resolveHeixiuDock(rect.left, sidebarRight, win.innerWidth);
230
240
  const heroRect = hero?.getBoundingClientRect();
231
241
  const welcome = heroRect && resolveHeixiuWelcome(heroRect, rect, sidebarRight, win.innerWidth);
242
+ const target = resolveHeixiuBox(rect, border, dock, welcome || undefined);
243
+ // All geometry reads are complete. Everything below only writes.
244
+ follow(0, 0);
232
245
  if (welcome) {
233
- button.style.setProperty('--heixiu-welcome-left', `${welcome.left}px`);
234
- button.style.setProperty('--heixiu-welcome-top', `${welcome.top}px`);
235
- button.style.setProperty('--heixiu-welcome-size', `${welcome.size}px`);
246
+ for (const [key, value] of Object.entries(welcome)) {
247
+ const property = `--heixiu-welcome-${key}`, css = `${value}px`;
248
+ if (button.style.getPropertyValue(property) !== css)
249
+ button.style.setProperty(property, css);
250
+ }
236
251
  }
237
252
  const mode = welcome ? 'welcome' : dock.mode;
238
253
  const fly = !viewportChanged && (wasFlying || !!dockMode && dockMode !== mode && (dockMode === 'welcome' || mode === 'welcome'));
@@ -241,8 +256,8 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
241
256
  viewportHeight = win.innerHeight;
242
257
  if (composer.dataset.heixiuDock !== mode)
243
258
  composer.dataset.heixiuDock = mode;
244
- // One read after layout configuration; never inside the animation loop.
245
- bounds = flight.target(button.getBoundingClientRect());
259
+ bounds = target;
260
+ button.hidden = false;
246
261
  visible = rect.width > 0 && rect.height > 0 && bounds.top + bounds.height > 0 && bounds.top < win.innerHeight;
247
262
  syncPlayback();
248
263
  if (allowed() && fly)
@@ -369,7 +384,23 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
369
384
  else if (next.running && (!actionUntil || button.dataset.actionSource !== 'session'))
370
385
  playState('thinking', 'session', true);
371
386
  }) ?? (() => { });
372
- const unsubscribe = subscribeXiaoheiHostDom(doc, scheduleLayout);
387
+ const unsubscribe = subscribeXiaoheiHostDom(doc, records => {
388
+ // Streamed paragraphs and our own SVG effects do not replace host slots.
389
+ // Parent removals still reach us, so session/composer replacement is safe.
390
+ if (records.length && records.every(record => {
391
+ const target = record.target.nodeType === 1 ? record.target : record.target.parentElement;
392
+ return target?.closest('[data-chat-flow], .xiaohei-bloub');
393
+ }))
394
+ return;
395
+ scheduleLayout();
396
+ });
397
+ const scrolled = (event) => {
398
+ const target = event.target;
399
+ // Captured scrolls from history/code blocks cannot move a fixed composer.
400
+ if (target === doc || target === win || target instanceof win.Element &&
401
+ (target.contains(composer) || target.contains(sidebar)))
402
+ resized();
403
+ };
373
404
  doc.addEventListener('pointermove', move, { passive: true });
374
405
  doc.addEventListener('pointerleave', leave);
375
406
  doc.addEventListener('visibilitychange', syncPlayback);
@@ -378,7 +409,7 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
378
409
  button.addEventListener('dblclick', doubleClick);
379
410
  button.addEventListener('keydown', keydown);
380
411
  win.addEventListener('resize', resized, { passive: true });
381
- win.addEventListener('scroll', resized, { passive: true, capture: true });
412
+ win.addEventListener('scroll', scrolled, { passive: true, capture: true });
382
413
  motion.addEventListener('change', syncPlayback);
383
414
  scheduleLayout();
384
415
  return () => {
@@ -398,7 +429,7 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
398
429
  button.removeEventListener('dblclick', doubleClick);
399
430
  button.removeEventListener('keydown', keydown);
400
431
  win.removeEventListener('resize', resized);
401
- win.removeEventListener('scroll', resized, true);
432
+ win.removeEventListener('scroll', scrolled, true);
402
433
  motion.removeEventListener('change', syncPlayback);
403
434
  heroRow?.removeAttribute('data-heixiu-hero-row');
404
435
  composer?.removeAttribute('data-heixiu-dock');
@@ -34,12 +34,14 @@ ${thinkingOnlyRow} > [data-slot='conversation.chat.node'] > div:first-child {
34
34
 
35
35
  /* Message portraits are deliberately static and only identify the speaker. */
36
36
  #root [data-chat-flow-kind='assistant-step'],
37
+ #root div[data-submission-echo],
37
38
  #root [data-chat-flow-kind='user'] {
38
39
  position: relative;
39
40
  overflow: visible;
40
41
  }
41
42
 
42
43
  #root [data-chat-flow-kind='assistant-step']::before,
44
+ #root div[data-submission-echo]::after,
43
45
  #root [data-chat-flow-kind='user']::after {
44
46
  position: absolute;
45
47
  inset-block-start: 2px;
@@ -70,6 +72,7 @@ ${thinkingOnlyRow} > [data-slot='conversation.chat.node'] > div:first-child {
70
72
  background-size: 76px auto, auto;
71
73
  }
72
74
 
75
+ #root div[data-submission-echo]::after,
73
76
  #root [data-chat-flow-kind='user']::after {
74
77
  content: '我';
75
78
  inset-inline-end: -50px;
@@ -123,8 +126,10 @@ ${thinkingOnlyRow} > [data-slot='conversation.chat.node'] > div:first-child {
123
126
  > div:first-child > div:first-child > div:last-child:not([data-slot]):not(:has(img)),
124
127
  #root [data-chat-flow-kind='user'] [data-time-hover-root]
125
128
  > div:first-child > div:last-child:not(:has(img)),
126
- #root [data-pending-steering]
127
- > div:first-child > div:last-child:not(:has(img)) {
129
+ #root :is(div[data-submission-echo], [data-pending-steering])
130
+ > div:first-child > div:first-child:not([data-message-attachments]),
131
+ #root :is(div[data-submission-echo], [data-pending-steering])
132
+ > div:first-child > [data-message-attachments] + div {
128
133
  box-sizing: border-box;
129
134
  padding: 14px 28px !important;
130
135
  color: var(--xiaohei-conversation-text) !important;
@@ -229,6 +234,7 @@ html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='assistant-step
229
234
  }
230
235
 
231
236
  html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='assistant-step']::before,
237
+ html[data-xiaohei-appearance='light'] #root div[data-submission-echo]::after,
232
238
  html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='user']::after {
233
239
  border-color: rgb(42 48 52 / 11%);
234
240
  color: rgb(48 54 58 / 72%);
@@ -238,6 +244,7 @@ html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='user']::after
238
244
  0 6px 16px rgb(34 40 43 / 9%);
239
245
  }
240
246
 
247
+ html[data-xiaohei-appearance='light'] #root div[data-submission-echo]::after,
241
248
  html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='user']::after {
242
249
  background-image:
243
250
  linear-gradient(145deg, rgb(255 255 255 / 74%), transparent 58%),
@@ -250,12 +257,14 @@ html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='user']::after
250
257
  padding-inline-start: 38px;
251
258
  }
252
259
 
260
+ #root div[data-submission-echo],
253
261
  #root [data-chat-flow-kind='user'] {
254
262
  box-sizing: border-box;
255
263
  padding-inline-end: 38px;
256
264
  }
257
265
 
258
266
  #root [data-chat-flow-kind='assistant-step']::before,
267
+ #root div[data-submission-echo]::after,
259
268
  #root [data-chat-flow-kind='user']::after {
260
269
  inline-size: 32px;
261
270
  block-size: 36px;
@@ -267,6 +276,7 @@ html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='user']::after
267
276
  background-size: 62px auto, auto;
268
277
  }
269
278
 
279
+ #root div[data-submission-echo]::after,
270
280
  #root [data-chat-flow-kind='user']::after {
271
281
  inset-inline-end: 0;
272
282
  block-size: 32px;
@@ -285,8 +295,10 @@ html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='user']::after
285
295
  > div:first-child > div:first-child > div:last-child:not([data-slot]):not(:has(img)),
286
296
  #root [data-chat-flow-kind='user'] [data-time-hover-root]
287
297
  > div:first-child > div:last-child:not(:has(img)),
288
- #root [data-pending-steering]
289
- > div:first-child > div:last-child:not(:has(img)) {
298
+ #root :is(div[data-submission-echo], [data-pending-steering])
299
+ > div:first-child > div:first-child:not([data-message-attachments]),
300
+ #root :is(div[data-submission-echo], [data-pending-steering])
301
+ > div:first-child > [data-message-attachments] + div {
290
302
  padding-inline: 20px !important;
291
303
  }
292
304
 
@@ -302,6 +314,7 @@ html[data-xiaohei-appearance='light'] #root [data-chat-flow-kind='user']::after
302
314
 
303
315
  @media (prefers-contrast: more), (forced-colors: active) {
304
316
  #root [data-chat-flow-kind='assistant-step']::before,
317
+ #root div[data-submission-echo]::after,
305
318
  #root [data-chat-flow-kind='user']::after {
306
319
  display: none;
307
320
  }
@@ -0,0 +1,3 @@
1
+ /** Local draft and native submission presentation; Host owns their lifetime. */
2
+ export declare const XIAOHEI_CONVERSATION_SUBMISSIONS_CSS = "\n#root [data-xiaohei-draft-preview] {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n box-sizing: border-box;\n inline-size: calc(100% - 2 * (var(--dsh-composer-side-clearance, 24px) + 16px));\n max-inline-size: var(--dsh-chat-content-width, 748px);\n margin-inline: auto;\n min-inline-size: 0;\n padding: 4px 0 8px;\n gap: 4px;\n color: var(--xiaohei-conversation-text);\n}\n#root .xiaohei-draft-label {\n font-size: 12px;\n padding-inline: 10px;\n}\n#root .xiaohei-draft-bubble {\n max-inline-size: min(calc(var(--dsh-chat-content-width, 748px) * .702), 82%);\n min-inline-size: 0;\n max-block-size: min(160px, 22dvh);\n overflow: auto;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n padding: 10px 14px;\n border: 1px dashed var(--xiaohei-conversation-edge);\n border-radius: 18px 9px 18px 18px;\n background: var(--xiaohei-conversation-user);\n font-size: 14px;\n line-height: 1.6;\n}\n#root .xiaohei-draft-bubble:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n}\n@media (width <= 760px) {\n #root [data-xiaohei-draft-preview] {\n padding-inline-end: 38px;\n }\n}\n\n#root :is(div[data-submission-echo], [data-pending-steering]) {\n min-inline-size: 0;\n color: var(--xiaohei-conversation-text);\n}\n\n/* Do not dim the whole row: text and attachments must remain readable while\n * the Host keeps edit/remove/steer disabled until its queue acknowledgement. */\n#root li[data-submission-echo] {\n opacity: 1;\n color: var(--xiaohei-conversation-text);\n background: var(--xiaohei-conversation-user);\n border-color: var(--xiaohei-conversation-edge);\n}\n\n#root [data-submission-echo] [role='status'] {\n color: var(--xiaohei-conversation-text);\n flex-shrink: 0;\n white-space: nowrap;\n}\n\n#root li[data-submission-echo] button:disabled {\n cursor: not-allowed;\n}\n";
3
+ //# sourceMappingURL=conversation-submissions.d.ts.map
@@ -0,0 +1,68 @@
1
+ /** Local draft and native submission presentation; Host owns their lifetime. */
2
+ export const XIAOHEI_CONVERSATION_SUBMISSIONS_CSS = `
3
+ #root [data-xiaohei-draft-preview] {
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: flex-end;
7
+ box-sizing: border-box;
8
+ inline-size: calc(100% - 2 * (var(--dsh-composer-side-clearance, 24px) + 16px));
9
+ max-inline-size: var(--dsh-chat-content-width, 748px);
10
+ margin-inline: auto;
11
+ min-inline-size: 0;
12
+ padding: 4px 0 8px;
13
+ gap: 4px;
14
+ color: var(--xiaohei-conversation-text);
15
+ }
16
+ #root .xiaohei-draft-label {
17
+ font-size: 12px;
18
+ padding-inline: 10px;
19
+ }
20
+ #root .xiaohei-draft-bubble {
21
+ max-inline-size: min(calc(var(--dsh-chat-content-width, 748px) * .702), 82%);
22
+ min-inline-size: 0;
23
+ max-block-size: min(160px, 22dvh);
24
+ overflow: auto;
25
+ white-space: pre-wrap;
26
+ overflow-wrap: anywhere;
27
+ padding: 10px 14px;
28
+ border: 1px dashed var(--xiaohei-conversation-edge);
29
+ border-radius: 18px 9px 18px 18px;
30
+ background: var(--xiaohei-conversation-user);
31
+ font-size: 14px;
32
+ line-height: 1.6;
33
+ }
34
+ #root .xiaohei-draft-bubble:focus-visible {
35
+ outline: 2px solid currentColor;
36
+ outline-offset: 2px;
37
+ }
38
+ @media (width <= 760px) {
39
+ #root [data-xiaohei-draft-preview] {
40
+ padding-inline-end: 38px;
41
+ }
42
+ }
43
+
44
+ #root :is(div[data-submission-echo], [data-pending-steering]) {
45
+ min-inline-size: 0;
46
+ color: var(--xiaohei-conversation-text);
47
+ }
48
+
49
+ /* Do not dim the whole row: text and attachments must remain readable while
50
+ * the Host keeps edit/remove/steer disabled until its queue acknowledgement. */
51
+ #root li[data-submission-echo] {
52
+ opacity: 1;
53
+ color: var(--xiaohei-conversation-text);
54
+ background: var(--xiaohei-conversation-user);
55
+ border-color: var(--xiaohei-conversation-edge);
56
+ }
57
+
58
+ #root [data-submission-echo] [role='status'] {
59
+ color: var(--xiaohei-conversation-text);
60
+ flex-shrink: 0;
61
+ white-space: nowrap;
62
+ }
63
+
64
+ #root li[data-submission-echo] button:disabled {
65
+ cursor: not-allowed;
66
+ }
67
+ `;
68
+ //# sourceMappingURL=conversation-submissions.js.map
package/lib/chrome.js CHANGED
@@ -4,6 +4,7 @@ import { XIAOHEI_COMPOSER_SEND_HEIXIU_CSS } from './chrome/composer-send-heixiu.
4
4
  import { XIAOHEI_CONTROL_PRIMITIVES_CSS } from './chrome/controls.js';
5
5
  import { XIAOHEI_CONVERSATION_CSS } from './chrome/conversation.js';
6
6
  import { XIAOHEI_CONVERSATION_MESSAGES_CSS } from './chrome/conversation-messages.js';
7
+ import { XIAOHEI_CONVERSATION_SUBMISSIONS_CSS } from './chrome/conversation-submissions.js';
7
8
  import { XIAOHEI_FRAME_SYSTEM_CSS } from './chrome/frames.js';
8
9
  import { XIAOHEI_IDENTITY_CSS } from './chrome/identity.js';
9
10
  import { XIAOHEI_OVERLAY_CSS } from './chrome/overlays.js';
@@ -25,6 +26,7 @@ export const XIAOHEI_CHROME_CSS = [
25
26
  XIAOHEI_WORKSPACE_CSS,
26
27
  XIAOHEI_CONVERSATION_CSS,
27
28
  XIAOHEI_CONVERSATION_MESSAGES_CSS,
29
+ XIAOHEI_CONVERSATION_SUBMISSIONS_CSS,
28
30
  XIAOHEI_COMPOSER_CSS,
29
31
  XIAOHEI_COMPOSER_SEND_HEIXIU_CSS,
30
32
  XIAOHEI_OVERLAY_CSS,