本文按代码实际结构写,不是按设计稿。凡是本文与代码冲突的地方,以代码为准,并请提 issue。 历史设计文档(交接文档、实现粗规划、规划、独立窗口复刻规格等)已归档到仓库外,见第 8 节。
一个 DSH 插件包 = 一个 bundle row + 两个半体。两者没有共享模块,只共享字符串常量
(lib/constants.js)和一套 HTTP 契约。
| 半体 | 入口 | 由谁加载 | 运行环境 |
|---|---|---|---|
| Host | lib/index.js |
Cordis,应用启动时加载一次 | DSH 主进程(Node ESM) |
| Client | lib/client.js |
window.__ModuleLoader__.load({ id, factory }),无构建步骤 |
渲染进程(浏览器环境) |
Host 半体在 DSH 的 web 服务器上注册一个前缀路由 /session-navigator/api,所有端点都在
它下面(lib/index.js:333-337)。Client 半体往 DSH 的 UI 插槽里注册 5 个组件,并把浮窗
portal 到 <body> 上的固定层。
package.json
├─ dsh.bundle.patch = "./cordis.patch.yml"
│ └─ - insert: [{ id: session-navigator, name: 'dsh-session-navigator', config: {} }]
│ → Cordis 按包名 import,取 name / inject / apply
│ (lib/index.js:109 name, lib/index.js:114 inject, lib/index.js:123 apply)
└─ dsh.client.inject = "@deepseek-ai/dsh-client-ui-slots"
dsh.client.platform = "web"
→ 渲染进程把 lib/client.js 当作 out-of-tree client 模块执行
cordis.patch.yml 只做增量 insert,不覆盖任何已发布的行(补丁会替换目标行的整个
config,覆盖是危险的)。
apply(ctx) 做了七件事lib/index.js:123-353,顺序有意义:
:122-123)。inject = ["webServer"] 是唯一硬依赖:没有 HTTP 服务器的
profile 就没有浏览器半体,此时直接 return,让 loader 跳过这一行。:127-138):resolveDataDir() → dataPaths() → openDocuments()。
openAt(dir) 让迁移完成后能在不重启的情况下切到新目录。:140-184):
component:浏览器组件状态机(Electron 运行时);detached:独立窗口 helper 进程控制器(懒创建);bridge:浮窗 ↔ 独立窗口的命令总线(内存);theme:浮窗上报、窗口读取的调色板。api 对象(:187-273)。这是路由处理器唯一的依赖入口:
ctx / dataDir / paths / documents / migration / reopen / component / detached / bridge /
theme / capabilities / reader / content / indexBuilder / summarizer。dataDir 与 paths 是
getter,迁移后自动指向新值——内容索引的缓存文件也跟着走(content 用 getter 拿路径,见 api.md §4.14)。
summarizer 在字面量之后单独赋值(:250 附近):它要引用正在定义的 api,而且它的
summaries / settings 必须每次现读 documents——迁移会重新赋值那个变量,
闭包里抓一份旧引用就会写进旧目录。:275-296):reconcileTrash(api) 收尾”索引已写、payload 未搬”的中断;
reconcileMoves(api) 做同一件事的移动版(payload 已搬到目标、注册表还没跟上);
component.sweep() 删除上一次卸载时被占用、删不掉的目录。三者都 best-effort、不 await。:298-306),并把返回的 dispose 挂到 ctx.effect。:308-317):插件被禁用/重载时 api.detached.stop(),否则 helper 进程会
活过拥有它的代码。handle(api, req, res)(lib/index.js:2184-2231):
routeOf(req.url) // lib/http.js:88 剥掉 /session-navigator/api
↓ null → 404 bad-request
compileRoute 匹配表 // lib/index.js:2140 段数相等 + :param 解码
↓ 无 → 404 no route for <METHOD> <path>
迁移守卫 // 非 GET/HEAD 且非 /datadir* → migration-in-progress
assertSameOrigin(req) // 非 GET/HEAD:跨站 / 非 JSON → 403 / 415
readJsonBody(req) // 非 GET/HEAD,上限 8 MiB(lib/http.js:57)
handler(api, req, res, query, params)
↓ 正常 → ok(res, result) // { ok: true, result }
↓ 抛错 → statusFor(code) // lib/index.js:2310 错误码 → HTTP 状态
fail(res, status, code, message)
三条不变量:
{ ok: true, result } / { ok: false, error, code }(lib/http.js:22-48)。
客户端只对 code 分支,error 只给人看。workspace/name-conflict、
session/title-invalid)原样透出,不翻译,避免多一层会漂移的映射(errorCodeOf,lib/index.js:2274)。GET /bridge/events 自己接管响应,handle 在 headersSent 时不再写信封
(lib/index.js:2218)。Sec-Fetch-Site 与 Content-Type: application/json
这两个浏览器强制的信号拒绝跨站写(lib/index.js 的 assertSameOrigin(),见
api.md)。lib/
├─ index.js 2417 插件装配 + 路由表(76 条)+ 错误映射(唯一编排层)
├─ http.js 101 信封、body 解析、URL 切分
├─ constants.js 39 API_PREFIX / PLUGIN_ID / REPO_URL / FIXED_NODES / WORKSPACE_NODE_PREFIX
├─ capabilities.js 73 Cordis 服务的防御式探测
├─ paths.js 365 数据目录解析、`.location` 指针、原子写临时目录清理
├─ fs.js 52 存在与列举:宿主与组件安装共用的两个判断
│
├─ sources/ 读 DSH 的数据(只读,除 move 的受控重写)
│ ├─ heads.js 233 L2:sessions 目录遍历(按实际版本名取最新日志,并发 8,结果排序)
│ ├─ projcache.js 228 L1:storages/session_projcache 的每会话小 JSON
│ ├─ workspaces.js 127 workspace registry:优先活服务,回退 storages/workspace.json
│ ├─ logfacts.js 154 日志只回答一件事:这个会话有没有过一轮对话(空白会话),按 path|size|mtime 有界缓存
│ ├─ live.js 122 哪些会话真的在跑:只信 ctx.agents 的 status,绝不从 ctx.sessions 或日志推断
│ ├─ artifact.js 227 会话文件路径编码/解码、读日志、重写 header
│ ├─ content.js 389 会话内容索引:按 path|size|activityAt 判失效,逐份构建、可续查,总是落盘
│ └─ reader.js 313 三源合并 + TTL 缓存策略(/bootstrap 的数据源)
│
├─ domain/ 纯规则,无 IO,可直接单测
│ ├─ sessions.js 310 合并三源 → 扁平会话列表(header 定身份,cache 只填空)
│ ├─ tree.js 477 树投影:workspace 节点 + 固定节点 + 用户文件夹
│ ├─ tree-rules.js 487 规则表 R1–R5 的唯一实现(accepts / scope / ref 装饰)
│ ├─ folders.js 335 新建/重命名/移动/删除文件夹(改 tree draft)
│ ├─ shortcuts.js 259 快捷方式:引用而非移动
│ ├─ annotations.js 276 标签、备注、收藏/待办
│ ├─ settings.js 439 设置文档形状与归一化(搜索索引只剩 indexIdle 一个开关)
│ ├─ trash.js 210 回收站条目形状、恢复阶梯
│ ├─ subagents.js 107 子智能体绑定:isBound / descendantsOf / withDescendants / boundIds
│ ├─ export.js 681 日志 → Markdown / JSONL(surfaceOp 与压缩只报告、不过滤)
│ ├─ summaries.js 763 AI 字段的提示词契约、回答解析、预算、花费账本、待补清单(纯函数)
│ └─ session-state.js 68 单个会话的布尔位(给 DSH 自己的按钮用,含 subagent)
│
├─ store/ 自己的持久化
│ ├─ sidecar.js 238 原子写 + revision + 串行化 + mtime/size 缓存
│ └─ documents.js 151 五份侧车文档的注册表
│
├─ host/ 需要动文件系统或跨进程的操作
│ ├─ actions.js 701 改名/派生/归档/置顶 → 调 DSH 自己的服务
│ ├─ trash.js 496 删除/恢复/清空的字节搬运
│ ├─ move.js 777 跨 workspace 移动/复制(重写 header cwd + 换登记)
│ ├─ move-journal.js 101 移动的意图日志:崩在两次写之间时,启动收尾而不是留下孤儿
│ ├─ datadir.js 367 数据目录检查与七步迁移
│ ├─ bridge.js 170 命令环形缓冲 + 游标
│ ├─ session-sync.js 238 跟 DSH 保持同步:订阅 session/* 与 agent/* 事件,合并成一次刷新
│ ├─ storage-watch.js 146 盯 storages/ 目录的 mtime:DSH 改了投影缓存就重读,事件漏了也有兜底
│ ├─ theme.js 103 调色板令牌白名单与校验
│ ├─ native-reveal.js 107 koffi → `SHOpenFolderAndSelectItems`,"在文件夹中显示"
│ ├─ reveal-agent.js 203 常驻 PowerShell:省掉每次 reveal 一秒多的进程启动
│ ├─ update.js 210 更新检查:版本比较、GitHub release 解析、5 分钟缓存
│ ├─ open-link.js 161 链接白名单(repo / star / issues / releases)→ 系统浏览器(带降级链)
│ ├─ fsutil.js 22 psQuote / dshHomeOf:两条 PowerShell 命令行共用的引用与 home
│ ├─ maintenance.js 229 缓存清理:内存三件 + `.tmp` / 下载目录 / staging,跳过在装的组件
│ ├─ summarize.js 787 AI 字段队列:模型目录、路由、单飞 worker、重试与 token 落账(调 DSH 的 llm 服务)
│ └─ reset.js 209 清除数据:五份侧车 + 回收站净内容 + 隔离区 + `.tmp`,`DELETE` 把关
│
├─ component/ 可选 Electron 运行时
│ ├─ layout.js 226 路径、版本、SHA-256、镜像、体积
│ ├─ state.js 168 state.v1.json 生命周期机
│ ├─ downloader.js 339 curl 流式下载(断点续传、镜像、进度)
│ ├─ install.js 541 校验 → 解压 → 原子发布
│ └─ manager.js 688 单飞状态机(install/cancel/remove 共享一把锁)
│
├─ detached/
│ ├─ launcher.js 415 Host 侧:spawn / 重试 / 聚焦 / 关闭、net 控制通道
│ └─ main.cjs — 跑在 Electron 运行时里,拥有 BrowserWindow
│
└─ dsh/
├─ home.js 39 DSH home 解析(与 kernel 同规则)
└─ zstd.js 197 zstd 帧扫描器(只为跳过帧,不解码)
依赖方向是单向的:index → host / store / domain / sources;
host → domain + store;domain 不 import 任何 IO 模块;sources 不 import domain 的规则
(只 import 纯合并函数)。domain/* 全部是”传入 draft、返回 draft”,所以测试不需要文件系统。
AI 字段(标题 / 描述 / 标签)是插件里唯一会把会话内容送到模型的地方,也因此是唯一需要 llm
服务的功能。
inject 仍然只有 webServer(api.md §2),llm 与 sessionController 都用
getService(ctx, …) 现取。把它们写成硬依赖会让没有模型服务的 profile 整个插件都不加载,
而树的浏览、标签、导出都不需要模型。sessionController.modelCatalog()(与 dsh-orb 同源),
拿不到才回退到 llm.listProviders() / listModels() / resolveModelInfo()。理由是别在宿主
已经维护的目录之外再维护一份判断。domain/summaries.js 只拼字符串、解析回答与算账(纯函数,可单测),
host/summarize.js 才碰 LLM 与文件;客户端只认 state 里的几个数字。summaries.v1.json 只会换来 revision-conflict,
而写字段不是越并发越快的东西——它是用户在等的一小段文字。AbortSignal.timeout:一个卡住的 provider 不该让队列永远停在
“正在生成 3/12”。不设 maxTokens:推理模型把思考算进同一个预算,小上限买到的是空正文
而不是短回答(自带的 dsh-pet 插件在 whisper.ts 里记下了同一个坑);提示词只要三个短字段,
时长由超时兜住。tools: []):这是一次文本改写,给了工具模型就会去调它。有些模型
即使没有工具也会回一个 tool-call 块——那种块被当作”没有正文”处理而不是错误,旁边的文字照收;
两次都没有正文才报 llm/tool-only / llm/empty。系统提示词只有本插件这一份,DSH 的 agent
提示词不参与(pi-ai 适配器的 splitSystemPrompt() 只拼 options.system 与请求里的 system 消息)。existingTags 读 tagVocabulary(),最多 40 个进系统提示词,回答里的标签再由
alignTags() 按现有写法归一。理由是同义词比没有标签更糟——”导出”/”会话导出”/”导出功能”会让选择器
长出十几行意思一样的词,而这个库正是下一轮生成要参考的”已有标签”。extractJsonObject() 剥掉
围栏取第一个 { 到最后一个 },normalizeFields() 逐字段校验;缺字段就重来一次,而且第二次
请求会附上一句”上次的回答不是那个 JSON 对象,不要调用工具”(原话重复只会得到原样的错);
两次都缺则 ai/bad-answer,一个半成品都不写——半成品会让这个会话永远不再重试。DSH 的
GenerateOptions 没有 response-format 字段,所以这里能给的最硬保证就是”校验 + 重试 + 拒绝”。summaries.v1.json 是文档不是缓存(见 data-directory.md §1.1):它是花过钱换来的文字,
所以坏文件进 quarantine/、不随缓存清理消失,也不因为会话被删而删(钱已经花了)。设置页「关于」组的三个入口里有两个不能由客户端完成,这是它们单独放在 host/ 的原因:
api.github.com 会撞 CORS;独立窗口里
window.open 与 fetch 也被主进程限定。所以由 host/update.js 代问、代比较,
结果缓存 5 分钟——设置页每打开一次就问一次的话,未登录的 60 次/小时会很快用完。host/open-link.js 用系统浏览器打开,但只认四个名字
(repo / star / issues / releases),不收 URL。这条 API 挂在 DSH 的端口上、没有 token,
接受任意 URL 就等于送给每个网页一个”用系统浏览器打开这个地址”的按钮。
打开本身按平台走一条降级链(Windows:rundll32 url.dll,FileProtocolHandler → Start-Process,
macOS open,Linux xdg-open → gio open),认的是子进程退出码而不是”spawn 成功”——
后者会让一个起得来、却打不开任何东西的壳子也报成功,用户看到的就是”点了没反应”;全链失败才
报错,且错误消息里带着那个 URL,面板原样显示,至少还能复制。第三个(缓存清理,host/maintenance.js)固然是 host 的活,但它有一条同样值得记住的边界:
只清插件自己的缓存(内存里的三处与 .tmp / 下载目录 / staging),quarantine/ 与
DSH 自己的 storages/session_projcache 一律不碰;组件正在下载/校验/解压时跳过那两项。
浏览器缓存也不归它管:面板的 /ui/* 本来就不缓存,其余属于 DSH 的渲染进程和独立窗口。
设置页最后一组(「危险操作」)只有一个按钮,它删的是用户自己的东西:虚拟树(文件夹、工作区
摆放与顺序)、标签、备注、设置、摘要与它的花费账本、回收站索引,以及回收站里的会话目录本体。回收站是 move 不是 copy(host/trash.js),
那些目录曾经就是 DSH 的会话目录——这一步没有”再点一次还原”可退。
所以判断全在 host/reset.js,客户端只把用户打的字原样送上去:
DELETE(去掉首尾空白)是第一件检查的事,不是删到一半才检查;api.dataDir 必须过 isUsableDataDir()——越是要清空一个目录,越要先确认那是它自己的;trash/ / quarantine/ / .tmp/)保留目录本身:插件下次还得往里写;invalidateMemory(api):面板每张列表都是 memo 过的,不抖掉的话,被删掉的文件夹
会一直答到进程重启。页面上的红按钮与输入框只是礼貌。这条路由没有 token,真正的门必须在 host:
if (typed !== RESET_CONFIRMATION) throw codedError("confirm-required", "…"); // 400,一个字节都没动
if (!isUsableDataDir(dataDir)) throw codedError("reset-refused", "…"); // 409
apply(ctx) 注册了什么lib/client.js:14190-14363。inject = ["slots", "locale", "uiWorkspace"](:13067)。
插槽(order 决定与 DSH 自带条目的相对位置):
| 插槽 | id | order | 组件 | 作用 |
|---|---|---|---|---|
sidebar.footer.action |
session-navigator-footer |
40 | FooterAction |
侧栏底部入口,开/关浮窗 |
settings.section |
session-navigator |
510 | SettingsSection |
设置页分组(label 是 thunk,跟随语言);组序 打开方式 → 列表 → 会话界面 → 浮窗 → 浏览器组件 → 数据目录 → 关于 → 危险操作 |
settings.onboarding |
session-navigator-opener |
900 | 匿名 | 只为拿到 openSection 后立即 complete(),不留 UI |
conversation.session.header.actions |
session-navigator-header |
60 | SessionHeaderActions |
会话标题栏五个按钮(样式跟随设置 shell.header.style) |
sidebar.workspaces.session.menu.item |
session-navigator-rows |
500 | SessionMenuItems |
会话行 “…” 菜单两条 |
effect(随插件卸载自动清理):
reportTheme() + MutationObserver 监听 documentElement 的
class / data-theme / style(:12913-12926);subscribeBridge(...),处理 open-session / new-session / open-settings /
reveal / window-closed(:12928-12985);ctx.locale.register(NS, { zh, en })(:12986)。导出:apply / inject / panelStore / SettingsSection(:13066-13070)。
浮窗本体是 Panel(lib/client.js:6356,单函数约 5500 行,内部按功能分段),
它被 portal 到 <body> 上的固定层——侧栏有 overflow: hidden,原地渲染会被裁掉
(lib/client.js:879 的注释)。
按「这一屏的标签是不是固定的」分工:
t("setStarHint")):一屏固定表单,键名可读比字面量值钱。T(中文原文, params)(lib/client.js:2196 起的注释块下方):键就是中文原文。
三百多条散在渲染树里的短标签,命名键只会多一层间接;而且改字时字典未命中的后果是
「显示中文」而不是「在页面上印出 key」。插值统一 {0} / {1},和设置页的 {size} 风格互不干扰。T 读模块级的 panelLang,而 Panel 每次渲染都从 props 的翻译器刷新它:
panelLang = typeof t === "function" && t("langCode") === "en" ? "en" : "zh";(lib/client.js:6359)。
语言开关一动,DSH 用新的 t 重渲染插槽,下一帧就是新语言。代价是模块级表(FIXED_META、
COLUMNS)的 label 必须写成 get label() { return T("…"); }——直接赋值会在模块加载时求值、
永久冻在中文。英文表在 panelDict()(lib/client.js:13228)。
宿主发来的名字也属于同一类,只是来源不同:固定目录(所有会话 / 未分组 / 待办 / 收藏 / 回收站 / 根目录)
的 name 在 host 那边是标识符、写法就是中文,所以 nodeLabel() 不印它,而是取
FIXED_META[node.id].label。默认工作区连固定 id 都没有——注册表里它的标题是 default-workspace,
host 画成「默认工作区」——节点因此带一个 default: true 标志,nodeLabel() 看到标志就取
T("默认工作区");用户自己改过名的工作区保留原名,这也是判据用标志而不是字符串的原因。
独立窗口是另一侧:assets/detached 是静态页 + 冒充 DSH 的 shim,它读不到 DSH 的语言开关,
也不会跟着 DSH 重渲染。所以语言由面板在 POST /detached/open 里报给 host(uiLang(),
lib/client.js:2216),经控制器 lib/detached/launcher.js 放进 helper 的 env(DSH_SE_LANG),
helper 拼进页面 URL(?lang=,lib/detached/main.cjs 的 pageUrl()),shim 从 URL 读出 LANG,
之后每句都过 L(zh, en)。中途切换语言时控制器用控制通道广播 { type: "lang" },helper 直接
win.loadURL() reload——字典是页面加载时定死的,reload 是唯一能让新字典生效的方式。
index.html 仍然随中文首发(HTML 在任何脚本之前就被解析完),英文窗口的标题与启动语由 shim 事后改写。
test/panel-copy.test.mjs 守着这条链的可见后果:T() 的每个键都在表里、表里没有空值或中文、
占位符两边一致,以及渲染树里不许有裸中文字面量(注释与样式表除外)。
列表不是一棵树长出来的,是一串带(band)。ListPane 先把行分组,再统一画成
button.grp(标题行:折叠箭头、图标、名字、计数)+ 该带的行,行本身由 rowsOf() 生成:
groups.push({ key, title, icon, count, rows });
带是渲染前的数据结构,不是 CSS 分组,「这一带有什么」和「这一带画不画」因此在同一个对象里
回答。带按固定优先级排:置顶、搜索/筛选结果、特殊文件夹、容器(文件夹 / 工作区 / 快捷方式)、
所有会话 的行、已归档(lib/client.js:4599-4700 的九处 groups.push)。
所有会话 在没有搜索词、没有筛选时不是一条平带,而是一条带一个工作区:
workspaceBands(sessions, tree)(lib/client.js:3962)按 session.workspaceId 分桶,
标题取会话视图里的 workspaceTitle——跟树画的是同一个名字,视图过期时回落到 id;
顺序用 DFS 从整棵树取(进到 workspace 节点就记位置、不再下钻,所以拖进用户文件夹的工作区
仍留在文件夹的位置上),树不认识的工作区排在已知之后,没有工作区的沉到最末。
搜索与筛选一旦生效就退回单带(lib/client.js:4672 的 byWorkspace):那些列表回答
「哪几行匹配」,每行一条工作区标题只是复读。
折叠只改画不改数据。foldedBands 是 Panel 的 Set 状态(lib/client.js:6483,初值来自
布局记忆),键是 scope + "\u0000" + bandKey(lib/client.js:4337)——同一个「会话」带在
根目录和某个工作区里是两个开关,否则折一次全站没有行。折起来的带在建行之前短路:
rows: bandFolded("normal") ? null : rowsOf(normal)(lib/client.js:4690)。所以不是渲染完
再藏起来——被折的行既不在 DOM 里,也不进框选,删不掉看不见的东西。状态随布局记忆落盘
(foldedBands: [...foldedBands],lib/client.js:6672)。
┌─ L2 heads(sessions 目录 + zstd 第一帧) ← 真相:存在性、id、cwd
reader.snapshot() ←───┼─ L1 projcache(每会话小 JSON) ← 便宜:标题、时间、计数
└─ workspace registry(活服务 → 文件) ← 归属、归档、置顶
↓ 合并(domain/sessions.js)
↓ 叠加侧车(tree / annotations / trash index)
GET /bootstrap → 一次往返把面板需要的全部东西给客户端
客户端不重算任何规则:tree-rules.js 在节点上装饰 accepts / scope / ref /
hasWorkspace,客户端只读这些字段、只沿 children 走两件事(子树、父链)。
test/tree-mirror.test.mjs 守着这条线:客户端可以留一个”拖拽时先灰掉”的谓词,但不能有第二套规则表。
归属(workspaceId)在 domain/sessions.js 里定,且沿父链继承:DSH 的
workspace.sessionIds 只登记用户从工作区打开的会话,子代理是父会话创建的,永远不在里面。
所以一个 isSubagent 且没有登记的会话,按 parentSession 向上找最近一个已归属的祖先,
继承它的 workspaceId / workspaceTitle;整条链都没有归属时才退回到 cwd 与工作区
路径的精确比较。fork 不是子代理(只有 parentSession + isSeeded),不参与继承。
子智能体的操作也绑定在父会话上,规则同样只在一处:domain/subagents.js。它回答三个问题——
isBound(能不能单独动)、descendantsOf(父会话走了谁跟着走)、withDescendants(级联的实际
id 列表)。路由侧统一用 assertStandalone() 拒绝单独操作(subagent-unsupported),回收站三条
路由再用 withDescendants() 把整条委派链一起带走;归档不写子会话,靠 sessions.js 的
archivedUp 从祖先派生。客户端只消费 session-state 里的 subagent 位,不自己判断。
状态不在 /bootstrap 里。 一行会话是”进行中 / 等待审批 / 已完成 / 空闲”这件事,只有 DSH
自己的客户端知道:uiSession.sessionStatus 是一个 HostObservable,插件用 useSyncExternalStore
订阅它(useDshSessionStatus),再用 withDshStatus 把 pendingInteraction / running /
completionUnread / 运行中的子会话数折进列表行,statusOf 按 DSH 的阶梯取第一档。这样侧栏和面板
看的是同一份内存状态,不必等下一次扫描,也不会各自推断出不同的答案——日志只回答”有没有过一轮”,
不再回答”这轮是不是断了”。
dependencies 为空,peerDependencies 只有 Cordis(optional)。
React 由宿主提供,assets/vendor/ 里的两个文件只给独立窗口用。lib/client.js 直接是浏览器可执行的文件;node --check 只证明能解析,
行为靠 test/render-smoke.mjs 与几个真实渲染测试兜底。lib/client.js 是单文件,且是刻意的。模块加载器只给一个 factory,拆文件就要自己实现
模块系统。不要在”整理代码”时拆它。lib/host/actions.js)。两个受控例外:host/move.js 移动会话文件并重写 header 的 cwd
(必须同时走 registry 的写路径,否则下次启动会话就丢了),host/trash.js 把会话目录整体
移进/移出回收站(DSH 与插件对”它不在了”的理解因此天然一致)。两个例外都用”先记账、后动手”
兜住崩溃:回收站写 pending 条目,移动写 move-journal.v1.json,启动时各自对账。tree.v1.json / annotations.v1.json / settings.v1.json /
trash/index.v1.json 只存 DSH 不知道的东西;删掉它们不会丢会话。
move-journal.v1.json 是例外:它记录的是正在进行的一次移动,正常情况下是空文件(不存在)。/bootstrap 的 capabilities 会告诉客户端
哪些不可用),不能让整个插件挂掉。ctx.effect,否则热重载会漏掉清理(路由 dispose、主题 observer、桥订阅、
helper 进程)。| 文件 | 内容 |
|---|---|
| README.md | 索引、阅读顺序、三条最容易违反的约定 |
| api.md | 全部路由、信封、错误码 → 状态、revision 契约 |
| data-directory.md | 数据目录解析、侧车文件、原子写、七步迁移 |
| tree-and-placement.md | 树结构、R1–R5、节点 id、快捷方式、虚拟放置 |
| bridge-and-theme.md | 浮窗 ↔ 独立窗口的命令总线与调色板同步 |
| component.md | 浏览器组件:下载、校验、安装、独立窗口进程 |
| testing.md | 测试矩阵与怎么跑 |
历史文档(已移出仓库,归档在 C:\Users\mi\Desktop\project_old\docs-archive\:交接文档.md、
实现粗规划.md、规划.md、独立窗口复刻规格.md、重构方案-核心UI复用.md、
session-manager-prototype.html 等)。它们描述的是开发过程中的计划,与当前代码有多处不一致,
不要照着它们改代码——这正是这套文档改为”照代码写”的原因。