# 架构说明 ## 两半职责 - **宿主半(`lib/`)**:注册 `dsh-session-dustbin` 设置命名空间(空 schema,只为让可配置页有键可派发),并挂载三条 loopback 路由。 - **浏览器半(`src/client/` → `lib/client.js`)**:注册 locale 字典、把「会话纸篓」卡片注册进 `settings.plugin.item`(keyed = 命名空间),并在 loopback 浏览器里安装侧边栏菜单注入。 ## 数据来源(为什么不需要解压日志) 平台的 `@deepseek-ai/dsh-session-projection-cache` 会为每个会话持久化一份**头帧**: ``` ~/.dsh/storages/session_projcache/sessions/.json record.identity.createdAt / cwd record.rows.title.val ← 会话标题(含用户重命名) record.rows.sessionListMetadata.val ← blank / lastPromptAt record.rows.sessionStats.val ← turns / steps ... ``` 列表行需要的信息全在这里,因此 `lib/sessions.js` **从不读取 `session.jsonl.zstd`**(多帧 zstd 容器,整包解码是上一代实现卡住宿主的原因)。头帧缺失时才退化为日志文件 mtime。 ## 删除链路 ``` POST /api/dsh-session-dustbin/sessions/delete { sessionId } ├─① registry.archiveSession(id) live 内存 + 持久化 + 广播;客户端 sessionVisible() 立即隐藏 ├─② rm ~/.dsh/sessions/<项目>// 数据清除 ├─③ rm ~/.dsh/storages/session_projcache/sessions/.json └─④ 修剪 workspace.json 的 archivedSessionIds / 各 workspace.sessionIds ``` 为什么必须先归档:平台没有「删除会话」RPC,也没有「取消归档」入口;归档是唯一能让侧边栏**立即**收走该行的内存操作。归档集合是 registry-global 的,`sessionIds` 槽位保留,因此不能靠「在哪个列表里」判断归档态,必须读归档集合本身。 为什么运行中的 host 可能回写标记:registry 的内存状态才是权威,它后续的 `setState` 会用内存快照覆盖文件。列表读取因此按**目录存在性**过滤(目录已删 → 不列出),并把这类 id 归入 `orphans`,卡片以「N 条归档标记已无对应数据」提示;重启后彻底干净。 ## 侧边栏菜单注入 平台会话菜单在 `dsh-client-ui-workspace` 中是写死数组(`rename` / `fork` / `archive`),没有 slot。注入依赖平台 Menu 原语的稳定 DOM 契约: | 目标 | 选择器 | |---|---| | 顶层菜单 portal | `div[role="menu"]`(子菜单在 `div[class*="itemWrap"]` 内,会被跳过) | | 菜单滚动体 | `div[role="presentation"]` | | 单个菜单项 | `div[class*="itemWrap"] > button[role="menuitem"]` + `span[class*="itemIcon"]` / `span[class*="itemLabel"]` | | 当前会话行 | `[role="treeitem"][class*="menuOpen"]`(只有菜单打开的那一行带 `menuOpen`) | 判定「这是会话菜单」需要同时满足:存在打开状态的会话行 + 菜单项里既有「重命名/Rename」又有「归档/Archive/分支/Fork」。这样工作区/分组菜单(同一原语)不会被误改。 注入项是**深拷贝最后一项(归档会话)**,只替换标签、图标、颜色与点击行为,从而继承平台样式。会话行 DOM 不含 id,因此点击后用标题通过 list 桥反查 id。 ## 客户端 bundle 契约 `build-client.mjs` 产出: ```js window.__ModuleLoader__.load({ id: '@local/dsh-session-dustbin', factory: (require) => { /* CJS 形态,导出 { apply, inject } */ } }) ``` `react` / `react-dom/client` / `react/jsx-runtime` / `@deepseek-ai/*` 保持 `require(...)` 由 loader 解析;CSS Modules 由构建脚本内联为「scoped class map + 自注入 `