Files
dsh_shenxian/dsh-server-docs/06-工作台UI规范.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

332 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 短视频工作台 UI 规范
> **【本工作区约定 · 2026-09-10 入库 · 适用范围 2026-09-10 明确】**
> **地位**:**关键文档 / 强制基线**——**本工作区内我开发的一切前端页面必须遵循**本文档的 Token、组件与交互约定;冲突时以本文档为准。
> **适用范围(用户明确)**:**除「dsh 自带功能」与「dsh 自带插件页面」外,其余所有功能插件开发(含其 client 面 section)、门户插件、门户页面(desktop / admin / skills / plugins 等)一律参考本规范 + 使用 UI skill。** 即:dsh 官方 UI 与官方插件渲染的部分不在约束内(不改它们),我自己写的页面/插件全部在约束内。
> **权威源**:`E:\ProgramData\AI技能\mcn-short-video\project\短视频脚本创作\V1.0\mcn-work-shop\docs\工作台UI规范.md`(源侧更新后回拷本文件)
> ⚠️ **该路径 2026-09-12 实测已不存在**(glob 无结果;`E:\ProgramData\AI技能\mcn-short-video\` 目录仍在,其下已无 `工作台UI规范.md`)→ **本副本当前即唯一有效版本**。源侧若恢复,需重新建立回拷关系并补同步。
> **UI skill(按场景选对,用错场景等于没用)**:开发/改前端页面时**必须加载并使用对应技能**做视觉判断 ——
> - **`impeccable`** → **本工作区绝大多数页面**(工具类后台、设置面板、管理页、表单、空态)。**用 Operate 模式**。它的描述明确覆盖 settings / empty states / a11y / 信息层级 / 动效,与本规范同向。
> ⚠️ 它自带的机械检测器**未随包提供**(`scripts/detect.mjs` 会报 `bundled detector not found`)→ 改为**人工逐条过 `reference/craft-floor.md`** 的 Verify 清单与 Refuse 禁令(2026-09-12 实测的确抓出了真问题,见下)。
> - **`taste-skill`**(frontmatter name 为 `design-taste-frontend`)→ **仅用于落地页 / 作品集 / 整站重设计**。
> ⚠️ 它**自述「Not dashboards, not data tables, not multi-step product UI」** → **本平台的管理页 / 设置面板不在其射程内**,不要拿它做这类页面。
> - **冲突时以本文档的实测 Token 数值为准**(本文档是实测基线,技能是通用审美)。
> - 位置:`E:\ProgramData\.workbuddy\skills\<name>\`(2026-09-12 起两个技能已装入全局技能目录,并已过安全审计)。Skill 工具加载不到时**直接读文件**即可。
> **改文档规则**:UI 决策若伴随"为什么这样改",回写源侧文件并回拷,保持两端一致。
> **同步时间戳与回拷流程(档案 19 §D8)**:本文档是**回拷副本**,权威源见上文"权威源"路径。
> - 最近同步:**2026-09-10**(入库)→ 之后如有改动请在此追加一行日期;
> - **2026-09-12**:修订「UI skill」条款 —— 原表述要求"必须同时使用 impeccable + taste-skill",但 taste-skill 自述**不覆盖** dashboards / data tables / 多步产品 UI,与本工作区页面类型不符 → 改为**按场景分流**(impeccable 管本工作区绝大多数页面;taste-skill 仅限落地页 / 作品集 / 重设计)。
> - ⚠️ **本次未回写源侧**:权威源路径(`E:\ProgramData\AI技能\mcn-short-video\...\工作台UI规范.md`)在本机**不存在**(2026-09-12 实测 glob 无结果)→ 修订**只落在本副本**;源侧恢复后需补回拷。
> - 回拷流程:① 改源侧 → ② 回拷到本文件 → ③ 在上一行追加同步日期 → ④ 走文档库对账脚本确认双端一致
> (`bash scripts/docs-sync-check.sh`,0 = 全绿)。
> **定位**:本文件是前端 UI 的**设计规范基线**(配色 / 字体 / 圆角 / 布局 / 组件 / 交互),供新增页面、调整样式时对照,避免重复踩坑。
---
## 1. 设计语言
亮色主题、工具类后台风格。三条主线:
1. **信息密度优先**——表格/列表是主体,行高紧凑(表格 td `7px 10px`,字号 15px)。
2. **克制配色**——蓝(`#2f6fed`)作主色承担交互反馈,橙(`#e8590c`)只用于强调数值/警示,其余靠灰度拉开层次。
3. **轻量质感**——圆角 8/12px、阴影 `0 1px 3px rgba(0,0,0,.08)`,不用重投影和渐变。
---
## 2. 设计 Token
### 2.1 色彩
| 变量 | 值 | 用途 |
|---|---|---|
| `--bg` | `#f5f6f8` | 页面底色 |
| `--panel` | `#ffffff` | 卡片/表格/弹窗面板 |
| `--border` | `#e3e6ea` | 统一描边 |
| `--text` | `#24292f` | 主文本 |
| `--dim` | `#6b7280` | 次要文本、说明、占位 |
| `--primary` | `#2f6fed` | 主色(按钮/激活态/链接 hover) |
| `--primary-soft` | `#eaf1fe` | 主色浅底(hover 行、激活 tab 底) |
| `--accent` | `#e8590c` | 强调(榜单得分、警示) |
| `--danger` | `#d93026` | 危险操作(删除) |
| `--shadow` | `0 1px 3px rgba(0,0,0,.08)` | 卡片/顶栏阴影 |
**扩展色(未进变量,直接写在规则里)**
| 场景 | 值 |
|---|---|
| 表头底色 | `#f6f8fa` |
| 链接蓝 | `#4a90e2` |
| 主色 hover 深一档 | `#2a5fd0` |
| 图标垂直微调 | `vertical-align: -0.18em` |
| 文件类型色 | md `#8250df` / json `#cf222e` / xlsx `#1a7f37` / txt·csv `#57606a` |
| 半透明主色底 | `rgba(47,111,237,.07)`(首页统计条)、`.16`(分隔线) |
**功能图标亮色映射**(未列出的跟随 currentColor):
`trending-up` 橙 · `users` 蓝 · `pen` 粉 `#ec4899` · `flask` 青 `#0ea5e9` · `lightbulb` 黄 `#d99a2b` · `refresh` 青绿 `#0f7f8f` · `chart` 玫红 `#d6336c` · `wrench` 灰 `#5b6470`
### 2.2 字体
```css
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
font-size: 16px; /* body 基准 */
```
代码/JSON 预览:`Consolas, monospace`。
**字号阶梯**(实测归纳,不是硬 token):
| 级别 | 字号 | 用例 |
|---|---|---|
| 特大 | 26px / 21px | 首页统计数字、面包屑主标题 |
| 标题 | 18px / 17px | 弹窗标题、区块标题、卡片标题 |
| 正文 | 16px / 15px | 正文、按钮、表格、输入框 |
| 次要 | 14px / 13px | 说明文字、数值列、徽章、tab(小) |
### 2.3 圆角 / 阴影 / 层级
| 类型 | 值 | 用例 |
|---|---|---|
| 圆角 | 6px | 小控件(btn-sm、侧栏切换组、树节点) |
| | **8px** | 默认(按钮、输入框、表格容器、信息卡、评分块) |
| | 10px | 徽章、榜单容器、卡片 |
| | **12px** | 大卡片(首页 data-card、func-card、统计条) |
| | 14px | 弹窗 |
| 阴影 | `var(--shadow)` | 卡片、顶栏 |
| | `0 8px 30px rgba(0,0,0,.18)` | 弹窗 |
| | `0 4px 12px rgba(47,111,237,.15)` | 卡片 hover(主色扩散) |
| z-index | 10 顶栏 / 100 弹窗遮罩 / 200 Toast | 表头 sticky 为 1 |
### 2.4 间距
没有 token 化,实际使用序列:**2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 18 / 20 / 24px**。常用组合:
- 卡片内边距 `16px 18px`;信息卡 `14px 16px`;表格单元 `7px 10px`;表头 `8px 10px`
- 页面外边距 `20px 24px`;首页/详情内容 `24px`;区块间距 14~24px
- 工具条内部 gap 8~10px;表单行 gap 10px
---
## 3. 布局框架
```
┌─ header.topbar(55px,sticky,白底 + 底部 1px 边框 + shadow,z-index 10)
│ brand(logo) · crumbs(面包屑) · top-actions(首页/帮助/设置,右侧)
├─ main#view height: calc(100vh - 55px) overflow: auto ← 唯一滚动容器
│ .home(max-width 960) / .page(max-width 1440)
└─ 弹窗层 .modal-mask(fixed inset 0,z-index 100)
```
**滚动(坑点,勿改)**
| 规则 | 原因 |
|---|---|
| `html, body { overflow: hidden }` | 消除顶栏 1px 边框引发的第二层滚动条 |
| `main#view { height: calc(100vh - 55px) }` | **55px 是实测值**(padding 20 + 行高 34 + 边框 1)。写成 54px 底部会溢出 1px,触发第二层滚动条 |
| 长内容滚动交给内部容器(`.rw-sec-body`、`.rw-content`、`.file-tree`) | 保持页面级不滚 |
**内容宽度**:首页 `.home` max-width **960px**;功能页 `.page` max-width **1440px**(padding 20 24);弹窗 `.rw-modal` 宽 `min(1600px, 100% - 100px)`、高 `calc(100vh - 60px)`。
---
## 4. 组件规范
### 4.1 按钮
| 类 | 外观 | 用例 |
|---|---|---|
| `.btn` | 白底 + border + `6px 14px` + r8 + 15px;hover 边框/文字变 primary;`:disabled` opacity .5 | 默认 |
| `.btn-primary` | primary 底白字;hover `#2a5fd0` | 主行动 |
| `.btn-ghost` | 透明底 + border | 次要/弹窗内 |
| `.btn-danger` | danger 系 | 删除 |
| `.btn-sm` | `3px 10px` / 14px / r6 | 表格功能列 |
| `.btn-icon` | `5px 9px`,纯图标 | 图标按钮 |
| `.btn-busy` | primary-soft 底 + primary 字 + opacity .8,内含图标旋转动画 | 异步进行中 |
| `.btn-import` | 白底 + `#d0d4da` 边 + `#495057` 字 | **保存类**(浅灰底深字) |
| `.btn-view` | 白底 + primary 边 + primary 字 | **查看类** |
> 功能列成对出现时:**保存=浅灰底深字(.btn-import),查看=白底蓝字(.btn-view)**,靠底色深浅区分而非都用蓝。
顶栏三按钮用 `.ghost`(白底 + border,`6px 14px`,r8)。
### 4.2 输入框
`.input`(`6px 10px`,min-width 200px)· `.input-select`(`6px 8px`,筛选下拉更窄)。均 1px border + r8 + 15px + 白底。
### 4.3 表格
```css
.table-wrap overflow auto + 1px border + r8 + 白底
.table width 100%, border-collapse collapse, 15px
.table th sticky top 0, 背景 #f6f8fa, 8px 10px, 600, nowrap, z-index 1
.table td 7px 10px, nowrap, 底部 1px border
tbody tr:hover 背景 var(--primary-soft)
```
| 修饰类 | 作用 |
|---|---|
| `.num`(td/th) | **右对齐** + `font-size: 13px` + `font-variant-numeric: tabular-nums`(等宽数字,必须加,否则数字跳动) |
| `.th-wide` | 数值列表头 min-width 90px + 右对齐 |
| `.sortable` | cursor pointer,hover 变 primary |
| `.cell-content` | max-width 220px + 省略号 |
| `.cell-loc` | 账号定位列缩窄 110px |
| `.th-act` / `.cell-act` | 功能列居中 |
| `.title-plain` | 视频标题:默认同正文色,**hover 才变蓝+下划线**(避免整列花掉) |
| `.link` / `.link.strong` | `#4a90e2`;`.strong` 加粗 |
**列宽策略(关键)**
| 场景 | 做法 |
|---|---|
| 榜单表格 `.table-rank` | **必须 `table-layout: fixed`**——auto 布局下 100% 表格的富余空间会按比例摊薄,**min-width 压不住**;fixed 下 width 才是硬约束,账号列自动吃剩余宽度 |
| 普通数据表格 | auto 布局,靠 `.cell-content` max-width + 省略号收口 |
**行高对齐(关键)**:榜单三个 tab 的行高必须统一,否则切换时整表跳动——
- 表格 `.table-rank td { height: 42px }`(功能列 `.btn-sm` 撑到 42px,其他 tab 原本只有 34.7px)
- 首页预览 `.rank-preview-list .rank-row:not(.rank-head) { min-height: 40px }`
### 4.4 Tab(两套并存,注意覆盖)
| 位置 | 样式 |
|---|---|
| 顶栏 | 胶囊式:r8,hover 浅蓝底蓝字,active **蓝底白字 + 600** |
| 数据页(**重新定义同名 `.tabs/.tab`**) | 下划线式:无背景无圆角,`border-bottom: 2px transparent` + `margin-bottom: -1px`(压住容器分隔线),active **蓝字 + 蓝色下划线** |
> ⚠️ 同名类二次定义,**数据页规则在后会覆盖顶栏规则**。新增页面若在下划线式区域用 `.tab`,拿到的是下划线式;需要胶囊式请另起类名。
榜单 tab `.rank-tab`:r8 8px 0 0(上圆角),active 浅蓝底 + 蓝字 + 底部 2px 蓝线。
首页预览里需 `align-self: center`(全局 `.rank-tabs` 用 stretch,会让 tab 组相对兄弟控件偏上约 5px)。
### 4.5 卡片
| 类 | 规格 |
|---|---|
| `.data-card`(首页入口) | r12,`16px 18px`,shadow,hover **上浮** `translateY(-2px)` + 蓝边 + `0 4px 12px rgba(47,111,237,.15)`,transition .15s |
| `.info-card`(信息区) | r8,`14px 16px`,margin-bottom 14px;标题 15px/600 + 底部 1px 分隔线 |
| `.recent-card`(脚本快捷卡) | r10,`14px 16px`,min-height **172px**(保证网格等高),hover 蓝边 + 淡影 |
| `.func-card` | r12,`16px 18px` |
### 4.6 徽章 / 标签
`.badge`:`2px 8px`,r10,13px。
- `.badge-signed` 浅蓝底蓝字(签约/内部)
- `.badge-ext` `#fff4e5` 底 + accent 橙字
- `.rw-ver-new` primary 底白字 r9 12px(最新版本标记,附在版本条最右)
- `.recent-card-score` 12px/600 r10
### 4.7 弹窗
```css
.modal-mask fixed inset 0, rgba(0,0,0,.35), flex 居中, z-index 100
.modal 白底 r14, 22px 26px, width 520px, max-width 92vw, shadow 0 8px 30px
.req-modal 760px × min(840px, 92vh) /* AI创作 */
.rw-modal min(1600px, 100% - 100px) × calc(100vh - 60px) /* 脚本对比/查看 */
```
> ⚠️ `.modal-mask[hidden] { display: none }` **必须显式写**——`.modal-mask` 是 `display:flex`,会覆盖 HTML `hidden` 属性导致弹窗关不掉。
**对比弹窗三段式(`.rw-*`)**:`.rw-stack`(flex row, gap 12)→ `.rw-sec`(flex 1, min-width 0, r8, overflow hidden)→ 内分 `.rw-sec-head`(8px 12px,白底,底部边框)/ `.rw-sec-body`(12px 14px,overflow auto)/ `.rw-sec-foot`(**min-height 46px**,右对齐,顶部边框)。
> `.rw-sec-foot` 的 min-height 是为了**空 foot(源脚本列占位)与其他列等高**,不能删。
### 4.8 Toast
固定在底部居中(`bottom: 32px`,`translateX(-50%)`),深底 `#24292f` 白字,`10px 18px` r8,z-index 200,**显示 1.8 秒后自动消失**。用于复制成功、保存结果等轻量反馈;失败重试类提示走行内 `.toolbar-note` / `.empty`。
### 4.9 空状态
`.empty-state`:纵向居中,`padding: 56px 20px`,gap 10px——图标(dim + opacity .45)→ 标题(17px/600)→ 说明(14px/dim)→ 行动按钮(margin-top 10px)。
轻量占位可用 `.empty`(`padding: 30px`,居中,dim)。
### 4.10 分页 / 批量
`.pager` 右对齐(gap 12,`padding: 12px 4px`);`.list-footer` 用 space-between 把批量操作条放左、分页放右;批量条 `.list-batchbar` 出现时与分页同行。
### 4.11 评分条
`.score-row`:名称 `.score-name` 固定 **88px** → 进度条 `.score-bar`(flex 1,height 6px,r3,底 `#eef1f5`)→ 分值 `.score-val` 固定 **36px** 右对齐。三档宽度固定,保证多行对齐。
### 4.12 图标
```js
svgIcon(name, size = 16, color) ;生成 class="ic" 的 SVG
```
- Lucide 风格:`viewBox 0 0 24 24`,`stroke-width: 2`,`fill: none`,`stroke: currentColor`
- 静态 HTML 用 `<i data-icon="home">` 占位,`initIcons()` 统一替换
- `.ic { vertical-align: -0.18em; flex-shrink: 0 }` ——**垂直微调 -0.18em,去掉会和文字基线错开**
---
## 5. 关键交互约定
| 约定 | 说明 |
|---|---|
| **数值一律右对齐 + 等宽数字** | `text-align: right` + `font-variant-numeric: tabular-nums`,列表头同步右对齐 |
| **字段对齐用 grid 列轨道共享** | 账号信息/视频详情的多行字段,用同一个 grid 让列跨行对齐;每行独立 flex 会因内容长短把后续字段顶偏(R44/R45 教训) |
| **超宽字段移出共享网格** | 如「账号定位」整行留在网格内会把 4 条轨道撑成等宽(各 232.5px),右侧大片留白 |
| **滚动联动单向** | AI 脚本列滚动驱动分镜列对齐,**不做双向耦合**(双向会互相抖动) |
| **长文本截断三件套** | `overflow: hidden` + `text-overflow: ellipsis` + `white-space: nowrap`,并挂 `title` 显示全文 |
| **hover 才提示可点** | 列表中的标题类文本用 `.title-plain`(默认同正文色,hover 变蓝+下划线),避免整列都是蓝字 |
---
## 6. 已知坑与规避
| # | 坑 | 规避 |
|---|---|---|
| 1 | `main#view` 写成 `calc(100vh - 54px)` → 底部溢出 1px,出现第二层滚动条 | 用 **55px**(padding 20 + 行高 34 + 边框 1) |
| 2 | 榜单表格用 auto 布局 → 列宽被富余空间摊薄,`min-width` 失效 | 加 `.table-rank { table-layout: fixed }` + 显式列宽 |
| 3 | 榜单三 tab 行高不一致 → 切换时整表跳动 | 表格 td 固定 42px;首页预览行 min-height 40px |
| 4 | `.modal-mask` 的 `display:flex` 覆盖 `hidden` → 弹窗关不掉 | 显式写 `.modal-mask[hidden] { display: none }` |
| 5 | 数值列没加 `tabular-nums` → 滚动/翻页时数字左右跳 | 数值列统一加 |
| 6 | 图标与文字基线错开 | `.ic` 的 `vertical-align: -0.18em` 不能删 |
| 7 | 首页 `.rank-tabs` 相对兄弟控件偏上 ~5px | 预览场景加 `align-self: center; margin-bottom: 0` |
---
## 7. UI 改动的生效链路与验收(2026-09-12 实战确立,**所有 UI 改动必走**)
> **起因**:v0.2.6 把「功能管理」列表改卡片后,**磁盘、服务端、实例三处都验过是新的,用户却一直看到旧 UI** —— 排查耗了 40+ 步。
> 根因不是"改错地方",而是**链路上有两道时序/缓存**:client bundle 只在**实例启动时**加载;而浏览器按 URL 里的 `rev` 决定要不要重新拉。
> ⇒ 为让**所有** UI 改动能被稳定验收,确立以下机制。
### 7.1 生效链路(改完到"用户看见",要过四关)
| 关 | 环节 | 关键条件 |
|---|---|---|
| ① | 改源码 → `npm pack` → 打包 | **必须升版本号** —— 同版本改内容会让 pnpm 判定"无需更新",白改 |
| ② | 铺到用户 profile(`ensure-biz-plugins.cjs` 等) | 改完立刻生效(**磁盘层**) |
| ③ | **实例加载 bundle** | **必须重启实例** —— client bundle 在**实例启动时**加载;不重启则实例内存里仍是旧代码 |
| ④ | 浏览器取到新代码 | 由 URL 里的 `rev`(**内容 hash**)决定;平台已加 `no-cache` 兜底 |
### 7.2 平台侧机制(2026-09-12 落地,**勿删**)
- **`Cache-Control: no-cache`**:平台代理层对实例的 `/plugins/` 响应统一加此头 —— **允许缓存但每次回源校验**,bundle 一变浏览器就能拿到新的。
- 实现位置:`src/supervisor/proxy.ts`,搜索 `平台缓存治理`
- ⚠️ `/assets/` 另有 nginx `location ~* ^/assets/` 的 `expires 30d` 会覆盖此头 —— 但 `/assets/` 是 dsh 构建产物(**文件名带 hash**),30d 合理,**无需处理**
- **`rev` 由 dsh 官方按内容计算**(`dsh-client-modules`:`framedHash("combo", [sourceBytes, sourceMap])`),平台**不得改**(红线 R2);且 dsh **会拒绝 `rev` 不匹配的请求**(官方注释:*mismatched revisions are rejected instead of serving newer bytes*)—— 这恰好成了"实例是否已换新"的**判据**。
### 7.3 验收口径(三段式,缺一不可)
| 查什么 | 怎么查 | 期望 |
|---|---|---|
| **磁盘** | `grep -c <新代码特征> <profile>/node_modules/<pkg>/lib/*.js` | ≥ 1 |
| **实例已加载** | 抓实例页 HTML,比对 URL 里的 **`rev`** | **与改前不同**(变了 = 实例加载的是新内容) |
| **服务端返回** | 用 HTML 里那条**完整** bundle URL 请求 | `HTTP 200` + 含新代码特征 |
### 7.4 四个常见误判(都是本次踩过的)
1. **拿单包 URL 请求** → 404(`/plugins/??` 只认**完整包列表**,不是单包)
2. **去服务器上 `/opt/dshs/poc/**` 的源码副本里 grep** → 那是**源码**、不是实例读取路径(实例读 `profile/node_modules`)→ 会误判成"根本没改"
3. **只看"实例是否重启过"** → 还要看 HTML 里 `rev` 是否真的变了
4. **取实例页 HTML 漏参数** → 必须 `curl -L --compressed -c jar -b jar -H "Accept: text/html"`(缺一即被 gzip 拦截,拿到空内容)
5. **client bundle 的 `inject` 写成数组** → **必须是函数**(原包写法 `inject: () => ({})`,返回「要注入的服务映射」对象)。写成数组 `["@deepseek-ai/dsh-client-runtime", …]` 会被 dsh 理解为「**在等这些服务**」⇒ 条目**永久 pending**:
```
web boot: 1 entry did not activate
<pkg>: pending (waiting for services: @deepseek-ai/dsh-client-runtime, …)
```
⚠️ **这个错只在浏览器里暴露**——服务端日志显示 `[xxx] loaded` 一切正常,`inject` 类型错误**完全看不出来**(2026-09-12 实测)。
6. **整合包的 client bundle 必须注册「包名自己」** → 拼接多个原包时,它们各自 `load({id: "<原包名>"})`,**包名本身不会自动注册**;dsh 按包名找 ⇒ 报 `loaded without registering "<包名>" via __ModuleLoader__.load`。
正确做法:子包的 `load` 改为「收集」,再由本包 `load({id: 包名, factory})` 统一注册,**并在 factory 内驱动子包 factory**(否则子包 UI 永不注册)。