Files

107 lines
7.7 KiB
Markdown
Raw Permalink Normal View History

---
id: 20260824-composed-slot-before-dom-seam
title: 先查宿主组合包 SlotMap,再退回 DOM seam
created: 2026-08-24
updated: 2026-08-24
status: candidate
scope: global
category: api-discovery
confidence: high
last_verified: 2026-08-24
promotion_target: pattern
projects:
- dsh-lark-cli
tags:
- host-extension
- slotmap
- declaration-merging
- native-tab
- api-discovery
- dsh
---
# 先查宿主组合包 SlotMap,再退回 DOM seam
## Trigger
需要判断一个模块化 Web 宿主的插件能否原生增加页面、Tab、导航项或面板,但当前插件仓库和它直接依赖的 runtime 类型里没有找到对应扩展 API。
## Context
调研 DeepSeek Harness(DSH)插件能否增加与 Chat、Trajectory 并列的 Session 顶部 Tab 时,先检索当前插件仓库和直接安装的 `@deepseek-ai/dsh-client-runtime`,只看到了 `root`、`shell.overlay` 以及仓库已有的 DOM Portal 路径,一度推断“没有原生 Tab API”。
继续检查实际 DSH Web Host 组合的 UI packages 后,`@deepseek-ai/dsh-client-ui-conversation` 的 TypeScript declaration merging 明确补充了 `conversation.view`:这是 `kind: 'list'`、`scope: 'session'` 的视图环,一个注册项就是一个顶部 Tab。第一方 `ui-trajectory` 插件正是通过同一 slot 注册 Trajectory。
这次有意义的复利点不是记住一个 slot 名,而是:扩展契约可能由“视觉表面的所有者包”声明,不在核心 runtime,也不一定出现在业务插件的直接依赖树中。否定一个扩展能力前,必须检查宿主最终组合和第一方同类贡献者。
## Evidence
- 2026-08-24,本机 `dsh --version` 返回 `0.1.0-rc.7`;Web UI packages 为 `0.1.0-rc.8`。
- 官方 `ui-conversation` SlotMap 在 [`packages/client/ui-conversation/src/client/contract/slots.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/contract/slots.ts) 声明 `conversation.view`,注释说明它是 “one list entry per view tab”。
- 官方会话组装在 [`packages/client/ui-conversation/src/client/apply.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/apply.ts) 遍历 `slots.entries('conversation.view')`,从 entry 的 `id` 和 `label` 生成 Tab;Chat 以 `id: 'chat'`、`order: 0` 注册。
- 第一方 [`packages/client/ui-trajectory/src/client/index.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-trajectory/src/client/index.ts) 使用 `ctx.slots.inject('conversation.view', ...)` 注册 `id: 'trajectory'`、`order: 10` 的 Trajectory Tab。
- 当前 `dsh-lark-cli` 只注册 `sidebar.footer.action` 和 `shell.overlay`,再通过 React Portal 使用 Conversation DOM seam;它证明了另一种布局扩展方式,但不能证明原生 Tab slot 不存在。
- 本次同时核对了已安装包的 `.d.ts`、编译后 `client.js`、官方中文 README、Context7 官方库 `/deepseek-ai/deepseek-harness` 和 GitHub `master` 原始源码。
- 项目内完整调研记录:`dsh-lark-cli/.scratch/research/dsh-plugin-custom-tab.md`。
## Root cause
已验证:DSH 的 `SlotMap` 使用 TypeScript declaration merging,由各 UI owner package 分散声明。`dsh-client-runtime` 只声明内建 `root` slot;`conversation.view` 由 `dsh-client-ui-conversation` 声明,因此只查 runtime 会得到不完整的扩展面。
已验证:宿主最终装配包含的包可以多于业务插件的直接依赖。当前项目没有直接安装 `dsh-client-ui-conversation`,但全局 DSH Web bundle 已组合该包及 `ui-trajectory`,所以项目局部 `node_modules` 不是宿主能力全集。
已验证:slot 类型声明只能证明 seat 存在;宿主如何把 seat 投影为 UI,需要继续核对 owner 实现。`ui-conversation` 的 header/render 代码证明 `id`、`order`、`label` 会形成 Tab,active id 会选择唯一视图。
推断:在其他可组合插件宿主中,同类误判也容易发生:核心 SDK 暴露注册机制,具体贡献点由 feature package 增补;从业务仓库向内搜索会漏掉最终装配才拥有的契约。
## Preferred action
1. 先确定目标 UI 的所有者:Tab 属于 Conversation、Settings、Sidebar 还是根 Layout;不要默认所有扩展点都由 runtime 声明。
2. 枚举实际宿主装配的 feature/client packages。优先读取 bundle manifest、profile dump、全局安装目录或已发布 package metadata,而不是只看业务插件的直接依赖。
3. 搜索所有 `SlotMap` declaration merging、slot declaration 和 `slots.register` 调用,区分:
- `single`:替换整个表面;
- `list`:可追加贡献;
- `keyed` / `chain`:按 key 或选择器扩展;
- slot 的 `root` / `session` scope。
4. 用三类一手证据闭环:
- owner package 的契约声明;
- owner 如何把注册项投影为实际 UI;
- 第一方同类插件的最小注册示例。
5. 分别核对“当前部署版本”和“上游当前版本”。前者回答现在能否工作,后者回答接口是否仍存在;不要用其中一个替代另一个。
6. 只有确认没有满足需求的 additive slot 后,才评估 DOM seam、Portal、CSS 重排或宿主 fork。若已有原生 slot,用它承担生命周期、排序、卸载和 active state。
7. 实现时把 owner package 加入插件的 client inject,并按宿主版本固定 peer/dev dependency;RC API 必须做真实 Web profile smoke test。
## Boundaries
- 该方法适用于由多个包组合、扩展契约分散声明的插件宿主。单体应用或拥有集中式完整 schema 的 SDK 不需要扫描全部 feature packages。
- 发现内部 slot 不等于它是稳定公共 API。至少需要 package 导出、类型声明、owner 文档或第一方插件用例之一;仅从 minified DOM、私有类名或未导出符号推断时仍按内部实现处理。
- `conversation.view` 适合“切换到插件拥有的整页视图”。若需求是文件树、编辑器与官方 Chat 同时可见,原生 Tab 会隐藏非 active view,仍应评估 [[20260822-extension-slot-portal-three-column-grid|生命周期 slot + 稳定布局 seam + Portal]]。
- `conversation.view`、`dsh.client.inject` 和具体 order 值是 DSH 当前契约,不应推广成其他宿主的通用命名。
- 当前只完成源码、类型、包版本和上游 master 核验,没有为 `dsh-lark-cli` 实际注册新 Tab,也没有运行浏览器集成测试。实现层面的兼容性结论仍需 smoke test。
## Failed approaches
- 只检索业务仓库和直接依赖的 runtime:会把“当前项目没有导入 owner package”误判成“宿主没有扩展点”。
- 从 `root` 或 `conversation.session` 是 `single` slot 推导不能新增 Tab:这混淆了“替换整棵表面”和其内部声明的 additive child slot。
- 看到当前项目已经使用 DOM Portal,就把它当作该视觉需求的唯一方案:既有实现只能证明一条可行路径,不能穷举宿主扩展面。
- 只搜 `tab`、`route`、`router`:slot 系统可能把 Tab 表达成抽象的 `view` contribution,应同时沿 UI owner、slot ledger 和第一方插件反向定位。
## Examples
DSH 当前原生 Session Tab 的最小形态:
```tsx
ctx.slots.inject('conversation.view', () => ctx.slots.register({
name: 'conversation.view',
id: 'plugin-view',
order: -10,
label: () => 'Plugin View',
}, PluginView))
```
验证顺序应是:`SlotMap declaration` → `host tab projection` → `first-party trajectory registration` → `target version smoke test`。
## Promotion record
- Not promoted. 当前已有一次高置信度纠错和一套可复用调查流程;待在另一个分包式宿主扩展调研中独立复用后,再考虑提升为通用 host-extension discovery pattern。