- Added new documentation for the Feishu user authorization flow, detailing the backend protocol, authorization initiation, callback handling, and token management. - Updated frontend index to link to the new authorization flow documentation for better accessibility and guidance on UI design consistency.
13 KiB
id, title, created, updated, status, scope, category, confidence, last_verified, promotion_target, projects, tags
| id | title | created | updated | status | scope | category | confidence | last_verified | promotion_target | projects | tags | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 20260822-extension-slot-portal-three-column-grid | 用生命周期插槽、稳定布局接缝和 CSS Grid 扩展宿主三列工作台 | 2026-08-22 | 2026-08-24 | candidate | global | frontend-architecture | medium | 2026-08-24 | pattern |
|
|
用生命周期插槽、稳定布局接缝和 CSS Grid 扩展宿主三列工作台
Trigger
插件需要在不修改宿主源码、不复制宿主 Chat 的前提下,把文件树、编辑器等工作区与宿主原生会话界面组成三列布局;宿主提供的官方扩展点在视觉上是 overlay,但真正需要改造的是 Session 内已有内容区的排版。
Context
oh-story-dsh 需要在 DeepSeek Harness(DSH)中提供“文件树 / 编辑器 / 官方 Chat”三列创作工作台,同时继续使用 DSH 原生 Chat、streaming、tools、Todo、approvals、history 和 Composer。
这个实现没有修改 DSH Host 源码,也没有把整个工作台直接画成覆盖宿主内容的浮层。它把两个职责不同的接缝组合起来:
- 通过官方
shell.overlay注册 Session 级子插槽oh-story.workspace,取得SessionProvider、sessionId、useSession和 Session 生命周期内的 Store。 - 子插槽组件定位 DSH 明确作为稳定地址接缝的
conversation.session,再用 React portal 把工作台挂进该会话容器。 - CSS 只在工作台 portal 确实存在时,把 conversation scroller 切成三列 Grid;文件树占第 1 列,编辑器占第 2 列,仍然挂载着的官方 Chat 和 Composer 占第 3 列。
关键不是“用 Grid 画三列”,而是先把状态生命周期、DOM 布局落点和原生能力所有权分开,再用最小的布局改造把三者拼起来。
这也说明应先区分两类需求:conversation.view 适合增加一个与 Chat 并列、切换显示的整页 Tab;当文件树、编辑器和原生 Chat 必须同时可见时,单独注册 view 不够,需要让新增工作区进入 Chat 当前视图的共同布局上下文。
Evidence
- 仓库:worldwonderer/oh-story-dsh。2026-08-22 初次核对提交
8fa6786;2026-08-24 再次浅克隆并核对 HEADfce73ca,关键架构仍一致。 docs/ARCHITECTURE.md:明确 Browser entry 使用shell.overlay,把文件树和编辑器 portal 到稳定的conversation.session布局接缝,并保留官方 conversation view。packages/dsh-plugin/src/client/index.tsx#L796-L895:apply()在shell.overlay下声明{ "oh-story.workspace": { kind: "single", scope: "session" } };WorkbenchSeat通过SessionProvider渲染子插槽;CreativeSplitBridge查找[data-conversation-scroll] > [data-slot='conversation.session']并调用createPortal()。该入口没有注册或替换conversation.view。- 同一文件中的
CreativeSplitBridge使用ResizeObserver读取 conversation scroller 的clientWidth,以<620、<900和其余宽度写入compact、medium、wide容器布局状态;卸载时移除 CSS 变量和data-oh-story-layout。 packages/dsh-plugin/src/client/plugin.css#L1-L75:只有当conversation.session中存在.oh-story-split-surface时,:has()选择器才把 scroller 设为 Grid。wide 列轨为clamp(184px, 16%, 200px) minmax(240px, 1fr) clamp(408px, 40%, 520px);tree、editor、官方 Chat 分别进入第 1、2、3 列。- 同一 CSS 把
[data-composer-seat]放到第 3 列并设为 sticky,同时给[data-chat-flow]留出 Composer 高度,避免原生 Composer 覆盖最终消息;min-width: 0、min-height: 0和各列 overflow 规则负责允许 Grid 子项正确收缩和滚动。 plugin.css#L352-L358定义 medium/compact 列轨;源码未发现拖拽分隔条或 pointer/mouse move 调宽逻辑,因此当前列宽由clamp()和容器断点决定。scripts/native-dsh-smoke.ts:原生 DSH smoke test 读取 tree、editor、Chat 和 Composer 的 bounding box,验证三列顺序、列宽至少 120px、Composer 位于 Chat 列内,并在滚动回归中检查 Composer 不消失。- 2026-08-22 用户明确纠正:该项目“没有修改 DSH Host,也不是单纯浮层”;这是本条要保留的关键架构辨析。
- 2026-08-22 本次 capture 通过浅克隆核对当前提交、上述源码和测试代码;没有实际运行仓库的原生 DSH smoke test。
- 2026-08-24 用户提供该项目实际 UI 截图,视觉上与源码一致:左侧项目/文件树、中间 Markdown 预览、右侧原生消息流与 Composer 同时出现。截图证明最终呈现,不单独证明交互和卸载行为。
- 2026-08-24 Context7 将官方 DSH 解析为
/deepseek-ai/deepseek-harness;官方ui-layout将shell.overlay声明为 root-scoped list slot,官方ui-conversationSlotMap 将conversation.view声明为 session-scoped list slot,验证两者职责边界。 - 项目侧完整复核记录:
dsh-lark-cli/.scratch/research/oh-story-dsh-three-column-layout.md。
Root cause
已验证:shell.overlay 在此实现中承担的是官方扩展注册和 Session 生命周期桥接,不是最终三列布局的空间父节点。真正的布局落点是 conversation.session 所在的稳定 DOM 接缝。
已验证:官方 Chat 和 Composer 没有被插件重写。插件保留它们现有的挂载和状态所有权,只通过 Grid placement 把它们排到第 3 列;因此 Chat 内部能力仍由 DSH 实现。
已验证:三列布局的启用条件与 portal 内容共存。.oh-story-split-surface 消失后,:has() 不再命中,scroller 会自然退出插件 Grid 规则;组件清理逻辑同时移除容器状态。
已验证:该方案不是“利用 shell.overlay 直接画出三栏”。shell.overlay 只提供 root registration、SessionProvider 和自定义 Session slot;三栏空间关系发生在 portal target 所在的 conversation scroller。
已验证:注册新的 conversation.view 会得到一个与 Chat 并列、按 active id 单独渲染的视图,不能直接实现“工作区与原生 Chat 同时可见”。并存需求与切换需求必须使用不同扩展策略。
推断:这套设计稳定的原因是把“在哪里取得上下文”和“在哪里参与布局”拆成两层。扩展 API 的名字或默认视觉形式不必成为最终布局结构,只要宿主另有明确、稳定、可寻址的布局接缝。
推断:保留官方 Chat 的 DOM 和状态,比复制一套 Chat UI 或代理内部状态更能降低宿主升级时的兼容成本;但 portal 目标和 CSS 选择器仍属于宿主契约,必须由文档和回归测试保护。
Preferred action
- 先判断产品需要的是“切换到插件整页”还是“插件 UI 与宿主原生视图同时可见”。前者优先使用 view/tab slot;只有后者才需要组合生命周期 slot 与布局 seam。
- 画清所有权边界:插件只拥有新增工作区,宿主继续拥有 Chat、Composer、审批、工具调用和历史等原生能力。
- 把接入拆成两种接缝:
- 生命周期接缝:只负责获得 Session 上下文、Store 和卸载边界;
- 布局接缝:只负责让新增 UI 与宿主现有 UI 参与同一个排版上下文。
- 用官方扩展槽声明 Session scoped child slot,不自行订阅全局 Session,也不把跨 Session 的编辑状态放进单例 Store。
- 只把 portal 挂到宿主明确承诺稳定的地址节点。若只能找到内部类名或偶然 DOM 层级,先补宿主契约或适配层,不要把猜测当扩展 API。
- 在稳定内容容器上建立 Grid,显式给新增区域和原生区域分配列;不要复制或重新挂载宿主 Chat。
- 用 portal 内容自身作为布局启用标记,例如父容器
:has(.plugin-surface)。这样插件未加载、切换 Session 或卸载时,不需要额外 JavaScript class toggle 就能恢复原布局。 - Grid 轨道同时表达最小可用宽度、弹性和上限:窄导航列适合
clamp(),主编辑区适合minmax(0, 1fr),需要保障可用性的原生 Chat 适合带下限和上限的clamp()。 - 给可收缩 Grid 子项设置
min-width: 0,给内部滚动区域设置min-height: 0和明确 overflow;否则内容的固有尺寸可能撑破轨道。 - 宿主 Composer 若与 Chat 流不是同一个 Grid item,应单独放入 Chat 列,并为消息流预留 Composer 高度;滚动时验证其可见性,而不只验证静态截图。
- 响应式判断优先观察实际布局容器宽度,而不是浏览器 viewport。插件可能运行在侧栏、分屏或可变宽宿主中,
ResizeObserver + data-layout比 viewport media query 更贴近真实约束。 - 几何回归至少检查:tree/editor/Chat 的左右顺序、每列最小宽度、Composer 是否完全位于 Chat 列、长 Chat 滚动时 Composer 是否保持可见,以及插件卸载后宿主是否恢复原布局。
Boundaries
- 该模式只适用于宿主提供官方生命周期扩展点,并明确承诺某个 DOM 节点或
data-slot是稳定布局接缝的情况;不能把任意 DOM 查询合理化为公共 API。 shell.overlay、conversation.session、SessionProvider和具体选择器是 DSH 当前扩展契约,不是 React 或插件系统的通用命名。:has()需要目标浏览器版本支持;若宿主兼容范围包含旧浏览器,需要等价的受控 class/data attribute 退出机制。display: contents会改变元素生成布局盒的方式,并可能影响可访问性、定位和浏览器兼容;应用前必须在宿主实际浏览器上验证。- 三列最小宽度和 620/900 断点来自当前创作工作台,不应原样复制到内容密度、字体和宿主宽度不同的产品。
- portal 保留 React 上下文,但不会自动保证宿主 CSS、焦点层级、z-index、滚动和可访问性正确;这些仍需在真实宿主中做集成测试。
- 当前证据验证了源码设计和已有 smoke test 的检查项,但本次没有运行需要真实 DSH 环境的 smoke test,因此保留
candidate和mediumconfidence。
Failed approaches
- 把
shell.overlay的名字直接理解成最终视觉实现:会漏掉它在本项目中实际承担的 Session 生命周期和子插槽声明职责。 - 为了获得三列而复制或替换官方 Chat:会接管 streaming、工具、审批、历史和 Composer 等宿主内部状态,扩大维护边界。
- 只把工作台 portal 进会话但不改变共同父容器的布局:它仍只是会话中的普通内容或覆盖层,不能与原生 Chat 形成真正并列的三列。
- 用 viewport media query 推导列宽:宿主内部 conversation 容器可能与窗口宽度不同,分屏或侧栏变化时会得到错误布局。
- 只做视觉截图、不测 DOM 几何和滚动:容易遗漏 Composer 跨列、末尾消息被遮挡、窄列低于可用宽度等回归。
Examples
下面是该模式的结构化伪代码,不是可直接复制的 DSH API:
registerLifecycleSlot({
children: { workspace: { scope: "session" } },
}, ({ SessionProvider }) => (
<SessionProvider>{() => renderSlot("workspace")}</SessionProvider>
));
function WorkspaceBridge() {
const target = findDocumentedConversationSeam();
return target ? createPortal(<WorkspaceSurface />, target) : null;
}
.conversation-scroller:has(.workspace-surface) {
display: grid;
grid-template-columns:
clamp(var(--tree-min), 16%, var(--tree-max))
minmax(0, 1fr)
clamp(var(--chat-min), 40%, var(--chat-max));
}
.workspace-tree { grid-column: 1; min-width: 0; }
.workspace-editor { grid-column: 2; min-width: 0; }
.native-chat,
.native-composer { grid-column: 3; min-width: 0; }
Promotion record
- Not promoted. 当前只有
oh-story-dsh一个项目的源码和测试证据;待在第二个宿主扩展中独立复用并通过真实集成测试后,再考虑提升为通用 pattern 或宿主扩展布局检查清单。