본문으로 건너뛰기
L

dsh-layout

lingtian303/dsh-layout

Layout plugin for DSH: customize the structure, order, width and visibility of the left sidebar, right sidebar, conversation area and settings page. Drag-sort and resize the three columns, per-layer teardown of sidebar/conversation/right/settings, grid slot arrangement, draw-a-frame region relocation, entry consolidation into More menus, conditional linkage rules, multiple named layout schemes, and a fullscreen layout whiteboard with a four-layer structure tree. Config persists host-side; dangerous configs have multi-layer recovery entries.

설치

dsh plugin --profile web add github:lingtian303/dsh-layout

README

dsh-layout(布局)

npm 版本 许可证

DSH 界面布局插件:把左栏、右栏、会话区、设置页的结构、顺序、宽度和显隐交给用户自己调。

  • 它不产出内容,只调整界面。
  • 入口在「插件 → 已安装 → 布局 → 详情页」,编辑面板是全局浮层:在哪个页面打开就停在哪个页面,识别、预览、调整都对着当前界面。
  • 改的是宿主已有的界面,不重写任何面板、不接管会话逻辑。

能做什么

区域可调项
三栏结构左栏 / 会话区 / 右栏 拖动排序(含左右互换)、各自宽度、会话区最小宽度、逐个隐藏任一栏(含会话区)、隐藏底部条
侧栏内容品牌行、新建会话、面板导航、工作区、底部操作 五块的排序与显隐;拆解:品牌标识 / 收起按钮 / 状态按钮行 / 账号菜单 四个单元独立显隐
面板导航左栏里每个面板图标的排序与显隐(列表从当前界面实时读取)
界面结构识别并列出所有界面页面:原生区域 / 原生页面 / 各插件贡献的面板页,按「官方登记 / 第三方登记 / 自动识别」三档标注;识别异常自动记录,可一键导出待复核清单
拆解覆盖24 个拆解单元的状态一览:此刻在场(这一秒 DOM 里命中)/ 上次观测(带时分)/ 上次未出现 / 从未识别 / 无快照;不在目标页面上时用区域快照说明看到的是哪一次结构,用于发现结构失配或空槽位
外部插件拆解通用扫描第三方插件页面(不针对具体插件):自动识别页面区域与成组单元,按「可隐藏 / 可排序 / 可调尺寸 / 只读」分级;分类模板归出语义组并支持整组隐藏;插件可用 data-dshl-declare 自声明结构(声明 > 登记 > 自动三档),人工可「登记此页结构」固化;签名失效自动清理配置,候选清单可导出
插件来源读宿主 slots 注册表:按槽位列出「哪个插件注册了什么」,已渲染条目可精确隐藏/排序(如侧栏面板图标、会话头工具、侧栏底部动作)。清单支持搜索过滤(槽位键 / 条目 id / 注册者 / 组件名,槽位命中时整组保留)与组内拖拽排序(拖动行或点箭头,两者共用同一套 0..n-1 序号,顺序由配置决定);入口类只排序,页面主体、单槽与弹层只读;未渲染条目只列出
槽位编排把容器改成网格:列数(2 / 3)、间距、面板导航可只留图标。容器来源两类:宿主布局容器(侧栏面板导航、侧栏底部、右栏标签栏、设置页导航)与任意 ≥2 条目的列表槽位出口;浮层/菜单类槽位与零尺寸容器自动排除
区域搬移把单元搬到界面上任意位置:点「开始画框」,在界面上拖出一个框,框的左上角就是落位、框宽就是宽度。框比源窄会自动放大到源宽度;框压住容器里已有的内容时自动往旁边空白处让一让(拖拽时用虚线露出真实落点),四周占满则留在原位并标「位置冲突」。多条搬移之间也互相避让,不会叠在一起。源高度按内容自然展开。已有框可原地微调:框内拖平移、拖右边缘改宽、方向键 1px(Shift 10px)、+/- 改宽度、Enter 应用;纯键盘也能建框(无框时按方向键即生成)。旧配置(按目标容器 + 开头/结尾)保留并标「需重画」,可逐条重画或一键清空
联动条件规则:条件(当前页面 / 侧栏折叠 / 区域在场 / 单元在场)成立时叠加动作(隐藏单元 / 容器排列),条件不命中就不生效、基础配置不受影响。配置同步:面板导航与插件来源的排序、隐藏双向同步,同一排图标不再出现两套顺序互相覆盖
入口收纳把不常用的入口收进各区域的「更多」菜单:侧栏面板、会话视图标签、右栏开始页卡片用原生菜单;会话头工具按钮直接注入宿主自带的「更多操作」菜单(按钮区不多出第二个「更多」);设置页导航、插件页第三方插件、设置页页头操作没有原生「更多」,由本插件授予一个同款(插锚点 + 抄原生控件样式 + 宿主 Menu)。入口清单全部自动发现(新装的插件 / 新注册的视图 / 新按钮不用登记),编辑面板「入口收纳」分页按七区管理开关,收纳状态持久化;入口身份按宿主稳定键(槽位条目键 / 列表 key / 开始页 kind / 按钮组件名)匹配,宿主改文案不影响已收纳的入口
会话区会话内容与输入框的最大宽度;拆解:会话头 / 消息区 / 输入区三块独立显隐,会话头可与正文换前后,输入区可提到消息区上方;会话头内部再拆标题行、视图标签行、会话名、操作组,标题行与标签行可换序;消息区拆出轮次导航;输入区拆出任务面板与状态条
右栏拆解:标签栏 / 内容区独立显隐,标签栏可放到内容区下方
设置页设置面板宽度、高度、左侧导航宽度、水平/垂直偏移;拆解:导航列(含标题)可隐藏或移到右侧,页头与选项区可各自隐藏
布局白板全屏工作台:左侧四栏列出全部结构(区域 4–6 / 容器 13–15 / 槽位全部 / 控件含第三方插件单元),中间画布把框描在真实界面上(描图态)或用结构快照画骨架(抽象态,跨页可用),右侧属性面板按选中节点给可调项。四栏与属性面板是可开合抽屉,描图态默认收起、不遮挡界面;四层各带独立显隐开关;选中区域后可继续下钻点选它内部的容器与控件(例:侧栏 → 面板导航 → 某个插件入口)
编辑模式在界面上直接拖动列分隔条改宽度;浮动工具条切换预设
预设标准 / 紧缩侧栏 / 专注 / 右栏左置 / 宽屏(起点模板,点一下即应用)
布局方案把当前整套配置存成命名方案(上限 20 套),随时切换、覆盖、改名、删除。切换是整套替换:三栏、侧栏、拆解、槽位编排、入口收纳、联动规则全部跟着换。预设是模板、方案是自己的,并列在「布局结构」分页
数据JSON 导出、导入、恢复默认;持久化在宿主侧:配置与方案写进当前 profile 的 cordis.patch.yml(- id: layout 节),换浏览器、换 Web 端、清缓存都不会丢;浏览器 localStorage 只作启动渲染缓存,宿主不可用时降级为唯一存储

宽度、顺序、显隐都是即时生效,不需要重启;「启用自定义布局」关闭后立刻回到 DSH 默认布局(配置保留)。

危险配置(隐藏侧栏/面板入口)会触发常驻恢复按钮与编辑浮层快捷键,见 安全与恢复。

安全与恢复(重要)

布局设置可能把「回到插件页」的入口藏起来,甚至可以把三栏全部隐藏、只剩空白页。恢复入口按「不占位、按需出现、位置不固定」设计:

场景恢复入口的表现
还有可见区域「布局」小按钮依附在现有控件旁边的空档里(工具栏按钮之间、面板图标旁),视觉上像那一排的一部分;没有可用锚点时才退化为留白随机落点
控件位置变化 / 新插件出现遮挡守卫盯着 DOM:按钮被盖住就自动换一个落点
三栏全部隐藏(只剩空白页)屏幕正中并排三枚按钮:恢复默认 / 插件页 / 编辑模式,配一行快捷键提示。插件页会顺手把被隐藏的会话区放出来,否则切过去也是空白
任何状态Ctrl/⌘ + Alt + L 打开编辑面板;再加 Shift 直接恢复默认
浏览器端http://127.0.0.1:60403/#dsh-layout=reset

落点算法不认识任何具体插件:只用浏览器通用几何(元素矩形、可见性、是否可交互),所以用户新装的第三方插件不管把控件放在哪、用什么框架,都参与同一套计算。详见落点设计。

另外两条自动防护:

  • 设置面板的偏移量在应用时会自动夹取,保证面板始终完整留在屏幕内(滑块值保留,只是视觉上受限)。
  • 关闭「启用自定义布局」会立即移除全部覆盖样式,不残留任何改变。

安装

已发布到 npm(dsh-layout)。通过 DSH 插件管理器安装:

plugin_manager install_bundle,target 为 dsh-layout@0.9.1(npm 包名@版本)

插件市场索引暂未收录本插件,市场内搜索不到属正常,按上面的 target 直接装即可。

开发或本地调试时,可改为安装本地目录(本仓库根目录即插件包),target 填仓库克隆到的本地路径:

plugin_manager install_bundle,target 为本仓库的本地目录(示例:E:\dsh-layout)

安装后 bundle 名为 dsh-layout,行 id 为 layout。npm 安装的升级走插件管理器换新版本号;本地目录为 link 安装,更换 client.js 后刷新页面即可生效,无需重装。

入口与操作路径

  1. 左栏点「插件」,在「已安装插件」里找到「布局」。
  2. 打开它的详情页,页面底部有「布局」区:识别状态、启用开关、「打开编辑面板」按钮。
  3. 点「打开编辑面板」(或按 Ctrl/⌘+Alt+L),面板浮在当前页面上,可以贴左或贴右;面板里调参数,或打开「编辑模式」在界面上直接拖列宽。
  4. 面板顶部「打开白板」进全屏工作台:点画布上的框或左抽屉里的行选中结构,右抽屉给它的可调项,改动即时生效。
  5. 面板内按类型分六页:布局结构 / 侧栏 / 入口收纳 / 区域拆解 / 槽位编排 / 识别与外部;分页状态会记住,下次打开停在原页。

面板不依赖插件详情页:切到会话页、插件页、设置页时它都留在屏幕上,识别与预览跟着当前页面走。设置页打开时面板浮在弹层之上,可直接调设置页几何。

实现方式

  • 客户端插件(client.js,浏览器模块)向 plugins.detail.section 注册详情页入口(开关 + 概览 + 打开面板),向 shell.overlay 注册全局编辑面板与编辑模式层;面板用 react-dom 的 createPortal 渲染到 document.body,以浮在设置弹层之上。
  • 用一个 DOM 注解器识别宿主外壳:#root > [data-slot="root"] > 框架、[data-slot="sidebar"|"main"|"rightbar"|"shell.bottom"]、侧栏块、面板行、[data-conversation-content]、[data-shortcut-modal="settings"],给它们打上 data-dshl-* 标记。
  • 识别层只看得见「此刻渲染在 DOM 里」的那一层界面;官方区域(会话区 / 右栏 / 设置页)每次可见时把单元清单与时间写入 dsh-layout/area-snapshots/v1,离开后用「上次观测 hh:mm」说明卡片对应的结构。
  • 由配置生成一段覆盖样式(style[data-dsh-layout="dynamic"]),用 !important 覆盖宿主的网格列宽、块顺序、面板顺序、容器排列、会话内容宽度与设置面板几何。宿主自身逻辑(打开/关闭右栏、侧栏折叠、路由、快捷键)保持不变。
  • 槽位编排给可排列容器打 data-dshl-arrange="<容器 id>",只生成网格声明(display:grid、grid-template-columns、gap、min-width:0),不动物件本身;容器每轮随识别重算,浮层/菜单类槽位与零尺寸容器不进清单。
  • 区域搬移是「投影」:源节点加 data-dshl-teleport 走 fixed 定位,落位与宽度由用户画的框给出(左上角 = 位置,框宽 = 宽度,高度随内容)。框宽不足时放大到源原始宽;框与「目标容器内部已被占用的内容」求矩形相交,冲突则四方向由近到远找第一个不相交的位置,四周占满则标 conflict 留在原位。障碍层级自适应下钻(区域里只有一层撑满的包裹时会继续往里找真正的并列内容块)。源节点始终留在原 React 树里,因此事件与宿主逻辑不受影响。
  • 联动是叠加层:computePlan 把基础配置与「命中规则的动作」合成一份 plan,样式与投影只认 plan;条件随刷新重算,所以切页面、折叠侧栏时规则即时生效/失效,不留下残余规则。配置同步在写配置时双向镜像两侧的 order / hide。
  • 入口收纳按「收纳域」工作:有原生「更多」的三区(侧栏面板、会话视图、右栏开始页)往宿主的 actions 槽位注册菜单行;没有的四区(设置页导航、插件页第三方组、会话头工具按钮、设置页页头操作)采用「授予式」——在目标容器里插一个外来锚点,运行时抄原生控件的 className 与内部结构,菜单用宿主 Menu primitives,所以外观与原生一致。被收纳的入口分别用 display:none、data-dshl-stored-view/guide/section/button 标记或 [data-plugin-package] 属性收起;入口清单全部自动发现:侧栏 sidebar.panellist、视图 conversation.view、设置页 settings.section 槽位条目,开始页卡片、插件页卡片、会话头工具行、设置页页头按钮读 DOM。按钮类入口优先用槽位条目键(稳定身份),没有的按显示名(label: 键)。
  • 配置存 localStorage(键 dsh-layout/config/v1),读取时做完整校验与范围夹取;导入文件同样走这一层校验。
  • 白板拆成独立 chunk(client.board.js):主 bundle 已七千行,白板再加一千行会难以维护。DSH 的 client-modules loader 原生支持包内异步 chunk,chunk 用 { id: 'dsh-layout', chunk: 'client.board.js', factory } 注册,主 bundle 在第一次打开白板时 require.async('./client.board.js') 按需加载;chunk 经 require('dsh-layout').boardHost 取回主 bundle 的状态与配置写入函数,不复制第二份状态。
  • 白板的结构树把七套既有词汇(BLOCKS / PART_UNITS / surfaces / slotSources / external.units / arrange / relocate)收敛成四层:区域 → 容器 → 槽位 → 控件;四栏只是这棵树按层级过滤的视图。
  • 白板不造结构:宿主三栏是写死的 CSS 网格,白板只取用宿主固定区域并改它的约束(order / width / min / hide),不存像素矩形;只有设置弹窗这类浮层保留像素几何。
  • 跨页靠几何快照(dsh-layout/board-geometry/v1):每页存归一化到 0..1 的节点矩形与结构签名(上限 8 页,按观测时间淘汰),目标页不在 DOM 里时用它画骨架;骨架态会冻结该页快照并比对结构签名,对不上时出「骨架可能已过期」提示 + 一键重抓。
  • 区域下钻用几何包含算直接下级(子节点中心落在父矩形内、且不被更小的同级包住),不维护父子表——宿主结构随时变,单独维护必然漂移。
  • 控件层长列表用固定行高窗口化(每桶独立滚动区、只渲染视口 ±8 行):全展开 451 行实测一次性渲染 79ms,窗口化后 DOM 只留 29 行。

详细设计见 docs/details/注释/布局引擎.md。

已知限制

  • 界面结构识别分三档:官方预登记(DSH 升级后需复核)、第三方逐步登记、未登记的自动识别。未登记的页面只会判为「第三方 / 未知」,不会误判成官方;若某页曾由登记认出、现在掉到自动档,卡片会提示「签名变化,待复核」。
  • 维护方式:识别异常自动写 dsh-layout/surface-anomalies/v1,卡片「待复核清单」可一键导出 JSON(含实测签名、观测时间、命中次数),照签名更新登记表即可;页面恢复正常登记后异常自动清除。
  • 只在宽度 ≥768px 的常规三栏布局上覆盖几何;窄屏抽屉形态交给宿主。
  • 侧栏折叠后(窗口窄于 1024px 自动收起,或手动收起)沿用宿主默认几何,本插件的列序/宽度在折叠态不强行覆盖,展开后恢复。
  • 侧栏折叠 + 隐藏侧栏同时成立时,宿主轨道会留下一条空轨道;展开后正常。属极端组合,恢复按钮始终可用。
  • 右栏宽度超过可视区时会按配置挤压中栏,可能触发横向溢出;建议不超过窗口宽度的 60%。
  • 依赖宿主 DOM 的 data-slot 等结构标记;宿主大版本改版后若识别不到,详情页入口会显示「未识别界面结构」,此时不会改动界面。
  • 插件来源读取的是 slots 服务的 entries/snapshot 与 React fiber 两处内部接口:宿主升级后可能失效。失效时「插件来源」卡片显示降级说明,页面内部的几何拆解与其它功能不受影响;未渲染的条目没有 DOM,只能列出不能操作;registrant 对动态加载的插件常是同一个宿主 id,不能当插件身份用。
  • 槽位编排只做容器级排列:列数 2 / 3,行宽由容器均分,列越窄越容易截断标签(设置页导航列默认 188px 时 2 列会截断,先调「导航宽度」更好用);过长的文字依赖宿主自己的省略号(没有的话可能溢出,已补 min-width:0)。「只留图标」只对侧栏面板导航开放,其它容器可能藏到控件,因此不提供。浮层/菜单类槽位不参与排列,已配置但此刻不在场的容器在卡片里标「此刻不在场」。
  • 区域搬移用投影而不是真搬 DOM:节点留在原 React 树里只改定位,所以宿主列表增删条目不会撞 React 协调器。落位由用户画的框给出,框只决定位置与宽度、高度随内容——代价是搬过去的块不让目标容器让位,压在容器内容上时会盖住它;这一点由「碰撞检测 + 向空白处让一让」承接,四周占满时会在面板上标「位置冲突」而不是悄悄挪走。源被改宽后高度会变,帧循环最多追 120 帧做二次校正。弹层/菜单类条目沿用只读规则不参与搬移。
  • 联动只做「条件 → 一个动作」:条件覆盖当前页面、侧栏折叠、区域在场、单元在场四类,动作覆盖隐藏与容器排列;动作在基础配置之上叠加,同一条规则不会改基础配置。配置同步依赖宿主 slots 注册表(拿不到就禁用);规则里隐藏弹层类单元与隐藏基础配置有同样的风险,恢复默认可一键清空。
  • 入口收纳按七区工作,依赖宿主的菜单 primitives 与 slots 注册表:侧栏「插件」、视图 chat、开始页「文件」是宿主保留入口,不参与;收纳只收起显示位置,不卸载插件、不改宿主数据。按钮类入口中没挂槽位的按钮按显示名匹配(宿主改文案会失配);授予式的锚点依赖宿主类名与容器结构,宿主大版本改版后可能退化为不出现锚点,不影响其它功能。某一区不在场时只列已配置条目并标注「此刻不在场」。
  • 配置按客户端来源存本地:桌面端与浏览器端的 localStorage 相互独立,可用导出/导入在两端同步。
  • 布局白板:描图态用真实元素矩形画框,目标页不在屏幕上时退化为结构骨架(几何快照);骨架态会冻结该页快照并与真实结构比对签名,差异 ≥3 个节点时出提示条 + 一键重抓(停留期间结构再变不会实时刷新提示)。白板是独立 chunk,加载失败时降级为提示 + 编辑面板,不影响任何现有功能。
  • 白板分页:控件层条目可达数百(第三方插件单元 + 槽位条目 + 按钮入口),按来源分桶、每桶 40 条并各带「显示更多」,全展开后走窗口化渲染(固定行高,只渲染视口附近的行)。
  • 白板顶部条让位:窗口栏是 Electron 原生 titleBarOverlay(本机高度 40),那一带 DOM 收不到点击,所以浮层条按「页面头栏底边 + 6」运行时定位,不写死像素。绝对定位居中不能用 left:50% + translateX(-50%)——那样可用宽度只有父级一半,内容会被挤成两行(实测踩过)。
  • 运行时测量只写 CSS 变量:顶部条/底部条在窄屏会换行变高,抽屉要跟着让位。测量结果曾写进 React state,窄屏下形成「测量 → 重渲染 → 再测量」的正反馈,打爆成 React #185、白板整块崩溃。现在改写成 --dshl-head-h / --dshl-foot-h 等 CSS 变量,抽屉用 calc() 引用。
  • 区域下钻靠几何包含:判定用「整框包含 + 面积降序 + 排除祖先」,不是中心点包含(后者会把容器判成子级)。完全重叠的兄弟节点可能漏列,四栏清单可兜底。
  • 收纳身份判空要判「非空字符串」:运行时发现的入口条目常常没有 id 字段(undefined),只写 id !== '' 会让这类条目互相全等、整批误显示为「已收纳」(实测 74 条)。
  • 恢复按钮落点避开窗口栏:三条坐标产出口(依附 / 落白 / 兜底)都要过安全区校验。漏掉时依附算法会把按钮放到 y=11 —— elementFromPoint 会命中,但 Electron 原生窗口栏吃掉真实点击。
  • 中英文字典成对维护:zh 漏键不会报错,只会静默回退显示英文(实测「切方案」动作显示成 Switch layout scheme)。改完跑本地键集合对比脚本(.tmp/probe-i18n-keys.py,诊断脚本不入库)对比键集合,要求双向 0 缺失。
  • 方案表读取的写回条件:只能因为「按上限截断」写回,不能因为「存储比内存长」写回——后者会因规范化丢弃脏条目而误判,把方案表覆盖成残缺版本。
  • 宿主 cordis.patch.yml 优先级高于 localStorage:往 localStorage 注入测试数据来调试会被宿主值覆盖,容易误判成代码 bug;改那份文件前必须备份。
  • 搬移的落点由框决定,框只认几何:位置与宽度来自用户画的框,目标容器只在运行时做碰撞判定、不进配置。旧配置(按目标容器 + 开头/结尾)没有位置信息,换算不出框,所以保留并标「需重画」,可逐条重画或一键清空——不静默丢弃。
  • 槽位条目可操作性排除不可渲染节点:<style>/<script> 有 fiber 但没有布局盒,不排除会混进搬移源清单。
  • 白板与编辑面板互斥但有返回:打开白板会关掉编辑面板并退出编辑模式;关闭白板时若原本是从面板进来的,面板会自动恢复。Esc 逐层退出:白板 → 编辑模式 → 面板。
  • 与其它同样改外壳布局的插件(如 better-sidebar)可能叠加;本插件的覆盖后写入,优先级更高。

目录

dsh-layout/
├── package.json          # bundle 清单:dsh.bundle.patch + dsh.client
├── cordis.patch.yml      # 插入插件行 id: layout
├── index.js              # Host 半边:声明可持久化的配置节(schemastery Config + settings.configure)
├── client.js             # Client 半边:注解器、样式引擎、全局编辑面板、编辑模式层、白板宿主面
├── client.board.js       # 布局白板(按需加载的包内 chunk):结构树、四栏、画布、属性面板
├── locale/zh.json|en.json# 插件列表里的标题与描述
├── icon.svg              # 插件图标
├── docs/                 # 计划、索引、架构与注释文档、验证截图
└── .tmp/                 # 诊断脚本与中间产物(不入库)

관련 플러그인