feat: add Feishu user authorization flow documentation and update frontend guidelines

- 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.
This commit is contained in:
yuxuanhui
2026-08-31 09:18:03 +08:00
parent 3d838866cd
commit dcd6d44960
15 changed files with 3817 additions and 732 deletions
@@ -0,0 +1,144 @@
---
id: 20260822-extension-slot-portal-three-column-grid
title: 用生命周期插槽、稳定布局接缝和 CSS Grid 扩展宿主三列工作台
created: 2026-08-22
updated: 2026-08-24
status: candidate
scope: global
category: frontend-architecture
confidence: medium
last_verified: 2026-08-24
promotion_target: pattern
projects:
- oh-story-dsh
tags:
- host-extension
- session-slot
- react-portal
- css-grid
- container-responsive
- native-chat
---
# 用生命周期插槽、稳定布局接缝和 CSS Grid 扩展宿主三列工作台
## Trigger
插件需要在不修改宿主源码、不复制宿主 Chat 的前提下,把文件树、编辑器等工作区与宿主原生会话界面组成三列布局;宿主提供的官方扩展点在视觉上是 overlay,但真正需要改造的是 Session 内已有内容区的排版。
## Context
`oh-story-dsh` 需要在 DeepSeek Harness(DSH)中提供“文件树 / 编辑器 / 官方 Chat”三列创作工作台,同时继续使用 DSH 原生 Chat、streaming、tools、Todo、approvals、history 和 Composer。
这个实现没有修改 DSH Host 源码,也没有把整个工作台直接画成覆盖宿主内容的浮层。它把两个职责不同的接缝组合起来:
1. 通过官方 `shell.overlay` 注册 Session 级子插槽 `oh-story.workspace`,取得 `SessionProvider`、`sessionId`、`useSession` 和 Session 生命周期内的 Store。
2. 子插槽组件定位 DSH 明确作为稳定地址接缝的 `conversation.session`,再用 React portal 把工作台挂进该会话容器。
3. CSS 只在工作台 portal 确实存在时,把 conversation scroller 切成三列 Grid;文件树占第 1 列,编辑器占第 2 列,仍然挂载着的官方 Chat 和 Composer 占第 3 列。
关键不是“用 Grid 画三列”,而是先把状态生命周期、DOM 布局落点和原生能力所有权分开,再用最小的布局改造把三者拼起来。
这也说明应先区分两类需求:`conversation.view` 适合增加一个与 Chat 并列、切换显示的整页 Tab;当文件树、编辑器和原生 Chat 必须同时可见时,单独注册 view 不够,需要让新增工作区进入 Chat 当前视图的共同布局上下文。
## Evidence
- 仓库:[worldwonderer/oh-story-dsh](https://github.com/worldwonderer/oh-story-dsh)。2026-08-22 初次核对提交 [`8fa6786`](https://github.com/worldwonderer/oh-story-dsh/commit/8fa6786aa9d107caf3072c66ab5df334f07b69c0);2026-08-24 再次浅克隆并核对 HEAD [`fce73ca`](https://github.com/worldwonderer/oh-story-dsh/commit/fce73cafff535ab80316b74e427c539351759fbc),关键架构仍一致。
- [`docs/ARCHITECTURE.md`](https://github.com/worldwonderer/oh-story-dsh/blob/8fa6786aa9d107caf3072c66ab5df334f07b69c0/docs/ARCHITECTURE.md):明确 Browser entry 使用 `shell.overlay`,把文件树和编辑器 portal 到稳定的 `conversation.session` 布局接缝,并保留官方 conversation view。
- [`packages/dsh-plugin/src/client/index.tsx#L796-L895`](https://github.com/worldwonderer/oh-story-dsh/blob/fce73cafff535ab80316b74e427c539351759fbc/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`](https://github.com/worldwonderer/oh-story-dsh/blob/fce73cafff535ab80316b74e427c539351759fbc/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`](https://github.com/worldwonderer/oh-story-dsh/blob/fce73cafff535ab80316b74e427c539351759fbc/packages/dsh-plugin/src/client/plugin.css#L352-L358) 定义 medium/compact 列轨;源码未发现拖拽分隔条或 pointer/mouse move 调宽逻辑,因此当前列宽由 `clamp()` 和容器断点决定。
- [`scripts/native-dsh-smoke.ts`](https://github.com/worldwonderer/oh-story-dsh/blob/8fa6786aa9d107caf3072c66ab5df334f07b69c0/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`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-layout/src/client/index.ts) 将 `shell.overlay` 声明为 root-scoped list slot,官方 [`ui-conversation` SlotMap](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/contract/slots.ts) 将 `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
1. 先判断产品需要的是“切换到插件整页”还是“插件 UI 与宿主原生视图同时可见”。前者优先使用 view/tab slot;只有后者才需要组合生命周期 slot 与布局 seam。
2. 画清所有权边界:插件只拥有新增工作区,宿主继续拥有 Chat、Composer、审批、工具调用和历史等原生能力。
3. 把接入拆成两种接缝:
- 生命周期接缝:只负责获得 Session 上下文、Store 和卸载边界;
- 布局接缝:只负责让新增 UI 与宿主现有 UI 参与同一个排版上下文。
4. 用官方扩展槽声明 Session scoped child slot,不自行订阅全局 Session,也不把跨 Session 的编辑状态放进单例 Store。
5. 只把 portal 挂到宿主明确承诺稳定的地址节点。若只能找到内部类名或偶然 DOM 层级,先补宿主契约或适配层,不要把猜测当扩展 API。
6. 在稳定内容容器上建立 Grid,显式给新增区域和原生区域分配列;不要复制或重新挂载宿主 Chat。
7. 用 portal 内容自身作为布局启用标记,例如父容器 `:has(.plugin-surface)`。这样插件未加载、切换 Session 或卸载时,不需要额外 JavaScript class toggle 就能恢复原布局。
8. Grid 轨道同时表达最小可用宽度、弹性和上限:窄导航列适合 `clamp()`,主编辑区适合 `minmax(0, 1fr)`,需要保障可用性的原生 Chat 适合带下限和上限的 `clamp()`。
9. 给可收缩 Grid 子项设置 `min-width: 0`,给内部滚动区域设置 `min-height: 0` 和明确 overflow;否则内容的固有尺寸可能撑破轨道。
10. 宿主 Composer 若与 Chat 流不是同一个 Grid item,应单独放入 Chat 列,并为消息流预留 Composer 高度;滚动时验证其可见性,而不只验证静态截图。
11. 响应式判断优先观察实际布局容器宽度,而不是浏览器 viewport。插件可能运行在侧栏、分屏或可变宽宿主中,`ResizeObserver + data-layout` 比 viewport media query 更贴近真实约束。
12. 几何回归至少检查: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` 和 `medium` confidence。
## Failed approaches
- 把 `shell.overlay` 的名字直接理解成最终视觉实现:会漏掉它在本项目中实际承担的 Session 生命周期和子插槽声明职责。
- 为了获得三列而复制或替换官方 Chat:会接管 streaming、工具、审批、历史和 Composer 等宿主内部状态,扩大维护边界。
- 只把工作台 portal 进会话但不改变共同父容器的布局:它仍只是会话中的普通内容或覆盖层,不能与原生 Chat 形成真正并列的三列。
- 用 viewport media query 推导列宽:宿主内部 conversation 容器可能与窗口宽度不同,分屏或侧栏变化时会得到错误布局。
- 只做视觉截图、不测 DOM 几何和滚动:容易遗漏 Composer 跨列、末尾消息被遮挡、窄列低于可用宽度等回归。
## Examples
下面是该模式的结构化伪代码,不是可直接复制的 DSH API:
```tsx
registerLifecycleSlot({
children: { workspace: { scope: "session" } },
}, ({ SessionProvider }) => (
<SessionProvider>{() => renderSlot("workspace")}</SessionProvider>
));
function WorkspaceBridge() {
const target = findDocumentedConversationSeam();
return target ? createPortal(<WorkspaceSurface />, target) : null;
}
```
```css
.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 或宿主扩展布局检查清单。