0.1.7 reads `package.json#icon` plus `locale/<lang>.json` to illustrate and label a plugin in the Plugins page and the `plugin_manager` list. The plugin had neither, so it rendered as a bare row (and every other third-party entry in this profile still does — ours is now the only one with a real mark). The icon follows the two icons DSH ships for this feature (packages/experimental/*/icon.svg): a 36x36, fill-only geometric mark with a blue gradient and no stroked outlines or text. It reads as a panel with a docked right rail — the sidebar this plugin is. The first cut used two blocks separated by a gap, but the rail shared the panel's light tone at the top, so the split leaned on a 3px gap that shrinks away at list size; a darker rail flush inside one panel survives any downscale. `locale/en.json` is the discovery baseline English needs to be present for the whole directory to be read — and `locale/zh.json` carries the Chinese title and blurb. The manifest's own `description` still advertised the terminal and the browser, which this plugin handed to DSH in 0.20.0; it is now user-visible as the fallback label, so it was corrected too. Every failure in this feature is silent: bad metadata only ever degrades to a plain text row. `tests/plugin-meta.spec.ts` therefore pins both ends — the manifest wiring and the packed tarball — plus the house style of the artwork, and was red/green verified (removing `icon.svg` from `files` fails it twice, once against the real tarball). Verified on the live 3080 instance: `readPluginMeta` resolves the icon to a `data:image/svg+xml` URI and the page renders it (`ourIconExact: 1`, no page errors).
33 KiB
dsh-better-sidebar 仓库规则(AGENTS)
本文只含项目全局开发规则(面向贡献者与 agent)。 消费插件接入 API 全参考(
ctx.betterSidebar服务、TabDescriptor / FileViewerDescriptor 全字段、声明式设置、原生右侧栏承载面、皮肤契约等)→ docs/external-plugin-guide.md;逐特性设计史(含实施偏差记录)→ docs/plans/。
1. 仓库硬约束(必须遵守)
- 禁止修改 DSH 源码:对官方 checkout(
~/.dsh/source/current)零写入。 - 代码改动必须走 PR:非文档改动在
feat/*/fix/*分支开发,gh pr create发起,review 合并后进 main;仅纯文档改动(README / AGENTS.md / docs/)允许直推 main。 - 挂载只走
cordis.patch.yml+ profile 机制(~/.dsh/profiles/<profile>/),插件作为独立包被 profile 引用,不反向侵入 DSH。 - 市场受管安装约束:
dependencies/peerDependencies/optionalDependencies一律不得出现cordis(按名硬拒,optional 无效),scripts不得含preinstall/install/postinstall/prepare。由tests/market-manifest.spec.ts守护。 - 缺能力时用 DSH 现成只读/公开 API 或插件自有路由(如
jobs.output事件回放:读会话事件日志而非动注册表);做不到先向用户说明取舍,不改 DSH。
2. CI 挂载冒烟(plugin-mount job / pnpm test:mount)
「npm 打包 → 真实挂载 → 无头渲染」门禁(证明打包产物在真实 DSH 挂载后不 crash):pnpm build && pnpm pack 产 tarball → scripts/e2e-mount.sh 装进全新 scratch profile(dsh plugin --profile web add <tarball>)并启动真实 dsh web(keyless,--port 0)→ tests/e2e/mount.e2e.ts(Playwright)断言 [data-dsh-better-sidebar] 挂载、无错误条/pageerror/console 错误,展开 DSH 原生右侧栏后经其 guide 页逐个打开本插件贡献的 tab 类型以及宿主自己的终端条目(必须恰好 1 条——多于 1 条即意味着插件又在遮蔽宿主),再经插件文件树(原生 files kind 接管)打开 seed 文件强制加载 editor chunk(client-editor.js),并跑 mermaid / README 预览与 sidechat 宿主路由烟测。browser 不在这个清单里,而是显式断言缺席:DSH 0.1.7 的 ui-sidebar-browser 只在 desktop profile 挂载(web-app 的 patch 里 disabled: ctx.get('profileContext')?.name !== 'desktop'),lane 全走 web profile,钉住「没有」比默默放过更能暴露宿主立场的变化。
本地:pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount(DSH_CMD 未设时 scripts/e2e-common.sh 回退的 npx 钉版是 DSH_NPX_SPEC,默认 @deepseek-ai/dsh@0.1.7-alpha.1——不钉就会解析 latest,而 latest 仍是插件已不支持的 0.1.5 线)。CI 钉 @deepseek-ai/dsh@0.1.7-alpha.1(npm alpha;latest 仍是 0.1.5-rc.2;peer 下限 ^0.1.7-alpha.1)。该步骤用 NODE_OPTIONS=--max-old-space-size=4096:钉版自身的传递依赖是浮动 ^ 范围,上游分阶段发布预发布版时(rc.2 于 2026-09-10 的 14:43–14:57 逐包上线)npm 会组出混合 peer 图,3062 条 ERESOLVE 后 OOM(exit 134);另一个失败模式是对未发布兄弟包 ETARGET,靠「钉一个已完整发布的版本」修掉。不要加 --legacy-peer-deps:它跳过的正是全局安装必须提供的 peer,@deepseek-ai/dsh-app-boot 在 boot 时 require 的 @deepseek-ai/cordis-plugin-group 是 peer 而非 dependency,加了它 CLI 直接 ERR_MODULE_NOT_FOUND(实测过一次,见 rc.2 计划 C 节)。ci-windows 的 Test 跑 pnpm test:windows(--maxWorkers=1)而非 pnpm test——多个 spec 真起进程(smoke / install-powershell / git),2 核 runner 上并行起 ConPTY / 冷启 powershell.exe 是超时与 worker 静默死亡的放大器;vitest.config.ts 的全局 testTimeout: 15_000(默认 5000 在 Windows 上对真起进程的用例太低,三个不同文件先后翻车)是配套的一半。e2e spec 命名 *.e2e.ts + vitest exclude 双保险;改 exclude 必须保留默认排除项(exclude 整体替换默认值)。
3. DSH 0.1.7-alpha.1 适配要点(0.1.7-alpha.1+ 基线)
v0.21.0-alpha.1 起仅支持 DSH 0.1.7-alpha.1+(peer 下限 ^0.1.7-alpha.1,CI 钉 @deepseek-ai/dsh@0.1.7-alpha.1)。下限必须动:semver 的预发布规则要求比较子与候选版本同 [major,minor,patch] 元组,^0.1.6-alpha.2 在数学上永远容纳不下任何 0.1.7-* 预发布版(实测 0.1.7-alpha.1 对 ^0.1.6-alpha.2 / >=0.1.6-alpha.2 全是 false),而 ^0.1.7-alpha.1 天然容纳 0.1.7 的 alpha/rc 全线。支持线取舍:0.1.6-alpha.2 及更早的用户停留在 v0.19.1(0.20.0 这一版从未发布到 npm)——本版破坏面足够大(dsh-settings 整体重写、26 个图标具名导出消失、会话格式 v3→v4),按本仓库先例不写运行时兼容层。发版:release.yml 按版本号是否含 - 自动选 alpha/latest dist-tag(0.21.0-alpha.1 含 - → alpha;npm latest 仍是 0.19.1)。
3.1 本版四类硬破坏(都是「不修就崩或静默失效」)
dsh-settings整体重写(packages/settings/settings):SettingsProvider.register(ns, schema)被删除,只剩SettingsForms的describe/update/replace/mutate/configure;命名空间就是插件 Loader 行的entry.options.id(不再是插件自选的字符串),schema 来自entry.fiber.runtime.Config,且describe()只列出「有Config且 fiber 处于 ACTIVE」的条目。本插件的适配:src/config.ts把用户偏好并进导出的Config(原先只是 4 个部署限额),并把每个偏好字段收集为 volatile schema。这一个标记就是「改设置不重挂插件」的全部机制:Loader 的equalExceptVolatile跳过 volatile 字段,纯 volatile 变更走_commitVolatile提交进运行中的引用并发loader/volatile-update,而普通字段(4 个限额)保持原来的重挂语义。必须用 DSH 自己的 schemastery(@deepseek-ai/schemastery),这是本版最容易踩空的一处。schemastery的meta.volatile只是标记;DSH 的构建额外在解析期把 volatile 字段包成 cosmokitVolatile引用(createVolatile),而 Loader 的_commitVolatile正是走volatileEntries/updateVolatile去提交那些引用。用公开的schemastery包:写入报告成功、revision 也涨,但有效值永远不变——静默丢弃每一次改设置(真机实测:settings.update200 +revision: 1,而describe().value仍是旧值)。另一个同等致命的坑:.volatile()走extra(),返回的是副本而不是原地修改,for (const f of ...) f.volatile()会把标记丢掉,随后宿主报Plugin entry "better-sidebar" has no volatile fields;必须收集返回值。src/index.ts的ownEntryId(ctx)运行时自发现自己那一行的 id(按options.name === 'dsh-better-sidebar'+entry.fiber === ctx.fiber匹配,回退到第一个启用的同名行),不允许硬编码better-sidebar:聚合包会用web-ui-better-sidebar之类的 id 挂同一个包。viewOf()=describe({redactSecrets:true}).find(d => d.ns === ns);写入 =update(ns, patch, expectedRevision)(SettingsConflictError仍导出、构造签名未变,409 分支保留)。ctx.effect(() => settings.configure({ auto: false }, ctx.fiber))—— 官方 README 给「自带设置页的插件」的写法;不这么做 Settings 会把同一批字段渲染两遍。settings/document-updated监听不到,不要写:它在 settings 服务自己的 context 上emit,而 cordis 的事件只向上冒泡到该 ctx 的祖先,插件的 fiber 是兄弟。本插件改用「客户端每次重新读取表单时重新求值」覆盖同样的触发点(settingsFace.get()里跑门控同步)。
ui-primitives图标整族改名:Icon<Name><14|16>→Icon<Name>Regular(1px 描边)/Icon<Name>Medium(1.3px)。0.1.6 的 81 个export const Icon*在 0.1.7 变成 184 个,旧的尺寸后缀名一个不剩(git grep -c Outline16 <tag> -- packages/client/ui-primitives/src= 0)。插件 26 个具名导入消失、17 个文件受影响。失败模式是 render 期而不是 load 期:插件 client bundle 是 CJS + external,产物是裸属性读,缺导出 =undefined→ React 抛Element type must be invalid,不是 ESM link error。改名必须同时钉回尺寸:0.1.6 的IconXxx14默认size = 14,0.1.7 的XxxRegular一律默认 16,所以原先依赖 14px 默认值的 5 处(TabBar.tsx/Sidebar.tsx/FileTree.tsx的IconCloseFillRegular、SubagentView.tsx两处IconRefreshOutlineRegular)必须显式传size={14},否则静默变大 2px。另有StateDot的 ongoing 态被整图重画(10px 像素矩阵 → 14px 圆环 spinner,默认尺寸10 → 14),签名兼容故无编译错误;插件选择跟随宿主观感。- 会话格式 v3 → v4(
packages/session/session-format-v3-to-v4):两处变化。source.kind === 'plugin'被硬拒(message-sources.ts抛format v4 message requires a producer-owned source kind),而插件的 sidechat 边界注入正是那个形状。改用plugin:dsh-better-sidebar:与 DSH 自己的 v3→v4 迁移对历史行产出的 kind 逐字一致(producerKind()对未知插件名返回`plugin:${plugin}`),所以新旧数据形状统一,历史 sidechat 会话不会丢。类型层面还需一条declare module '@deepseek-ai/dsh-llm'的MessageSourceMap增强(0.1.7 把它改成合并可扩展的封闭联合,没有共享 catch-all)。- tool 结果消息重写:
role: 'user'+ 嵌套type: 'tool-result'包装块 →role: 'tool'+ 顶层content/isError/toolCallId。插件的 4 个解析器原先靠candidate.type === 'tool-result'命中,在新形状下恒返回空串(静默空白)。现在四处都同时接受新旧形状(旧分支必须保留:历史日志里就是旧形状)。
- 客户端会话快照换字段:
SessionListState.subagentsByParent/jobsBySession被projectionsBySession取代,且setSubagentCatalogOpen这个按需观察 API 被整体删除(0.1.7 在连接建立时一次性加载所有 Session 的 projection,所以那套 observe/unobserve 机制应当删除而不是改名)。子代理目录改读projectionsBySession[sid]?.values.subagentCatalog({id, createdAt, mode: 'one-shot'|'continuable'|'unknown', label?},注意unknown是新值);后台任务列表没有替代字段,改由插件新增的jobs.list路由读ctx.jobs.list(sessionId)。同一版里ctx.jobs.list/get/read/kill的 caller 从Agent变成SessionId(插件因此可以彻底不碰ctx.agents来调注册表)。
3.2 本版的能力让出与收敛
- 只读文件预览交还宿主:0.1.7 的
ui-sidebar-documentpreview长齐了 code / excel(xlsx,xls,csv,tsv) / office(宿主侧转 PDF) / pdf / image / html / markdown / text,并带缩放与按目录自动刷新。插件删掉自己的image/pdf/binary-download三个 viewer,并在src/client/native/index.ts的editor.canOpen里拒绝认领这些扩展名(canOpen返回 false 就把地址让给内置text的fallback档):xlsx xls xlsb xlt xltx xltm ods ots fods csv tsv pdf png jpg jpeg gif webp svg bmp ico avif doc docx dot dotx ppt pptx保留在插件侧的三件:md/markdown(自带渲染器,用户认为优于内置)、html/htm(自带沙箱预览 +htmlViewerNoSandbox/htmlViewerDefaultUnsafe,宿主没有这两个开关)、以及codecatch-all(它是可编辑的 CodeMirror + 保存,内置那几个是只读预览)。未知二进制(.zip/.wasm)仍走code认领 → 宿主 fsRead 判 binary → head 重匹配无 sniffer →EditorHost.tsx的load.status === 'binary'兜底渲染下载面板,功能不回归。 - 宿主
browserkind 在 Web profile 不再挂载:packages/bundle/web-app/cordis.patch.yml给ui-sidebar-browser加了disabled: !!js "ctx.get('profileContext')?.name !== 'desktop'"。因此 e2e 的HOST_OWNED_TABS只剩['terminal'],并且显式断言 browser 条目缺席(钉住新真值,而不是默默放宽)。DSH 同时新增用户设置linkOpening决定正文链接进侧栏还是新标签页。 - 外链接管收敛:删掉
browserInterceptLinks/browserInterceptHttp/browserInterceptHttps三个偏好键(含 20 份词典的 key)。新行为是只认领声明了urlTarget的链接,其余一律不preventDefault、放行给宿主;认领成功但目标类型此刻不可用时window.open(url, '_blank', 'noopener,noreferrer')兜底。修掉了一个真实回归:0.1.6 那版把浏览器交还宿主时,openInSidebar仍兜底到'browser',而插件注册表里已无该类型、service.openTab对未注册类型静默return——默认配置下插件自绘 markdown(sidechat 转录 / 编辑器预览 / diff)里的 http 链接点了没反应。 - 文件树新增实时刷新:插件接管了
fileskind,所以宿主自己的按目录 watch 覆盖不到它。新增src/fs-watch.ts(宿主侧,每条连接一组fs.watch,150ms 去抖,上限 64 个句柄)+src/client/use-dir-watch.ts(一条/sidebar/ws/fs-watchsocket 携带展开目录集,目录折叠即退订),客户端收到通知后只丢弃并重列那一层缓存。路径解析沿用fs.tree的同一道 workspace fence。
3.3 既有缺陷(不是 0.1.7 引入,但这次一并修)
- 幽灵字段
.current:插件读ctx.sessions.list.getSnapshot().current,而SessionListState在两个 tag 里都没有这个字段(插件context-types.ts自造了它,所以 tsc 一直放行)。后果:src/client/native/surface.ts的onScreen判定恒 false(永远走*In分支),src/client/Sidebar.tsx的store.setSession(current)恒收到undefined(按会话持久化实际没绑上),use-host-feeds.ts的窄视口 park 门控恒假。改用 0.1.7 新增的ctx.sidebarRight.mounted(ObservableSnapshot<SessionId | undefined>,只在会话真变化时 set)。Pending队列与openTabIn/openResourceIn/closeIn必须保留:它们仍是唯一能写「未上屏会话 store」的路径(这三个方法在具体 class 上,不在ISidebarRight接口里,需结构化探测)。 package.json.files漏了lib/client-locale.js:chunk 名在 8 处镜像,files里只有client-editor.js/client-mermaid.js,于是 locale chunk 从来没进过 npm tarball(社区 PR #739 覆盖同一问题)。已补。- 测试 harness 的
windowshim 缺addEventListener:0.1.7 的ui-primitives在模块求值期就window.addEventListener('scroll'|'resize'|'blur'),而tests/browser-globals.ts的假 window 只有clearTimeout/setTimeout/innerWidth/innerHeight/getComputedStyle/location→TypeError: window.addEventListener is not a function,任何 importservice.ts的 spec 一个用例都跑不起来。已补addEventListener/removeEventListener/dispatchEvent/matchMedia。 - 测试期依赖闭包又长了一层:
ui-primitives@0.1.7的产物新裸 import@deepseek-ai/dsh-client-store与@deepseek-ai/dsh-util-workspace-path,而dsh-client-store自己的产物又裸 importzustand(×3)与immer,且这几个包都不声明dependencies。所以 devDependencies 要补的是 4 个(两个@deepseek-ai包 +zustand@~4.4.7+immer@^10.1.1),不是 2 个——只补前两个,vitest 一加载就Cannot find package 'zustand'。反过来不要预加dsh-workspace/dsh-agent-preset-registry:它们只是dsh-subagent的 optional peer,而该包 28 个lib/*.js里从不 import 它们(实测解包核对)。一律以pnpm peers check的实际输出为准。 - e2e 的 token 交换不能走 Node 全局
fetch:宿主静态 fallback 对纯 Node 的 GET 回 空体 405 且无Set-Cookie(在 0.1.5-rc.2 与 0.1.7-alpha.1 上都复现,属 harness 怪癖而非宿主回归),于是cookieHeader永远拿不到、后续/api种子请求全部未认证、挂载 lane 卡在拿不到 tab bar。改用 Playwright 自己的request.newContext()(exchangeLaunchCookieViaPlaywright,maxRedirects: 0以免跟随 303 丢掉Set-Cookie)。
3.4 宿主契约(0.1.5 线起沿用,均经真机挂载冒烟验证)
- 一次性 token 鉴权:就绪行
dsh web: http://127.0.0.1:<port>/?token=<43字符>(导航换签名 cookie,干净 URL 401)。e2e-mount.sh的 URL grep 必须延伸到空白([^ ]*,在/截断丢 token);e2e 统一走tests/e2e/host.ts(token 必选:parseLaunchUrl对裸 origin 直接抛错),带 stamps 导航走gotoPage()(先 addCookies 再直达——token 换 cookie 的 303 会丢弃同 URL 其它 query 参数)。插件/sidebar/*路由不受影响,同源 fetch 照旧。 - Remote gateway 斜杠 RPC(唯一方言):
POST /api/workspace/create,payload 恰为{args: {...}},args 按控制器 TS 参数名包装(workspace/create、session/create→{args:{request:{...}}};session/list参数名_request不可省略——{}也被拒args fields do not match the descriptor);envelopemethod与路径一致,点分路径 404。请求由tests/e2e/host-protocol.ts的rpcAttempt构造、tests/e2e-host-protocol.spec.ts锁定。 MarkdownTextlabels 嵌套契约:必填labels: { code: { copyLabel, copiedLabel }, footnotes }(漏传回退硬编码中文)。四个渲染点(mermaid.tsx / MarkdownHtml.tsx / TextEditor.tsx / SideChatView.tsx)统一走src/client/markdown-labels.tsx的markdownTextProps()。0.1.7 逐字未变。- 侧边对话转录走自有路由,不碰客户端宿主 RPC:
ctx.connection.api(含sessions.history)在 alpha.1 整体移除,继任session/follow|page又对origin:'subagent'会话强制 subagent 地址(普通{kind:'session'}被agent-busy拒)且分页throughSeq不得超当前游标。转录因此由sidechat.events插件路由供给(src/sidechat-routes.ts:live 读agent.session.snapshotEvents()、冷读sessionPersistence.inspect,服务端session/end-seed切割 +afterSeq增量)。0.1.5 起实时增量不在日志里:assistant/chunk事件被删除,进行中的模型增量改由agent/assistant-stream瞬时帧发布(start/chunk/end,不入会话日志),结算时才落assistant/message或assistant/attempt。插件在宿主侧src/assistant-live.ts折叠这些帧成有界缓冲。0.1.7 复查:packages/core/agent/src/runtime-types.ts逐字未变,无需调整。 dsh-subagent的SUBAGENT_DESCRIPTOR_VERSION仍是 3(0.1.7 复查:descriptor.ts两 tag 同值)。sidechat 种子的subagent/descriptor版本由宿主包盖章,插件不硬编码;测试断言跟随常量(tests/sidechat-routes.spec.ts),勿钉字面量。@deepseek-ai/dsh-client-runtime包已消亡(继任 seed 是裸名dsh-client-store,无/client子路径):peerDependencies、devDependencies、dsh.client.inject、chunk externals 白名单(src/client/chunk-loader.ts/tsdown.config.ts/tests/chunk-loader.spec.ts/tests/manifest-consistency.spec.ts四处同步)均已无该条目。- e2e scratch profile 的
minimumReleaseAgeExclude含'@deepseek-ai/*'(scripts/e2e-mount.sh/e2e-aggregate-mount.sh,与仓库根pnpm-workspace.yaml同策):alpha 版本常在发布后 24h 内跑 lane,pnpm 11 的minimumReleaseAge默认会拒装新鲜包。 - 插件开发树的 dsh- 传递 peer 需提升为 devDependencies*(alpha.3 首见):
dsh-subagent等 npm 包把dsh-attachment等 dsh-* 姊妹包声明为 peerDependencies(由宿主 bundle 树统一提供,宿主侧无此问题),插件仓库若只直接依赖其中一部分,其余 peer 在 pnpm 下会解析到树上残留的旧版——如dsh-attachment@0.1.1-rc.1缺admitPromptContent导出、dsh-subagent产物 import 它时测试加载即崩。因此 devDependencies 需涵盖 dev 树实际触达的全部 peer(attachment / scope / session-projection / system-prompt / user-approval / util-time 等,与直接依赖同款精确钉版);@deepseek-ai/cordispeer 自 alpha.3 起要求^4.0.2,0.1.7 起要求^4.0.3。适配新版本时先跑pnpm peers check,把新失配的传递 peer 一并提升进 devDependencies。0.1.7 新增的同类问题见 §3.3 第 12 条(ui-primitives的第二层裸 import)。例外:dsh-client-locale的 peer/devDep 允许落后于基线的历史已消除,rc.1 起上游恢复发版,0.1.7 起与 DSH 同 tuple。 - 右侧栏是 DSH 原生栏,插件只提供 tab 类型:聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走
ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))。插件的每个TabDescriptor注册成原生 tab 类型(kind = descriptor.id,extension带)+ 原生 tab 体(sidebar.right.pane.tabkeyed 槽,key =dsh-better-sidebar:<id>);editor类型同时认领dsh-resource://file/**(但按 §3.2 第 5 条的清单让出一部分),并接管内置files页面 kind(openTab('files')打开插件文件树,注销即复位)。ctx.sidebarRight.openTab/openResource/close只对在屏会话写入;跨会话用具体类上的openTabIn/openResourceIn/closeIn。原生 tab 的插件侧状态在src/client/native/tab-adapter.tsx。注册必须等服务、不能等槽声明:原生栏先声明sidebar.right.pane.tab再provide('sidebarRightTabs'),真机 profile 上槽声明回调里ctx.get('sidebarRightTabs')仍是 undefined(实测 3 秒后才出现),按槽触发注册会静默什么都不注册且永不重试——用ctx.inject(['sidebarRightTabs'], cb)驱动类型注册(tests/native-surface.spec.ts以「槽先触发、服务后到达」的顺序守护)。0.1.6-alpha.2 起:SidebarRightGuideEntry.id必填且同 provider 内唯一(缺它会静默清空整个承载面);同 provider 的 guide 条目是追加关系,chain那种「替换宿主条目」的语义只存在于conversation.chat.turnTail且已被上游改成list。0.1.7 起:sidebar-right本体的契约零破坏——槽名无改名、SidebarRightTabDefinition没有新的必填字段、只有新增的可选keepMounted与ISidebarRight.mounted,openTabIn/openResourceIn/closeIn在具体 class 上字节不变;tab.visible语义收紧为active && (...)(全局面板/后台会话时 body 不挂载);持久化校验收紧(非根 / 列轴 / 嵌套 / 3+ docked pane 的 split 一律判非法整份丢弃,0.1.6 的 sidebar 自己产不出这种布局);RightbarRoot现在为每个保留会话渲染一个隐藏 seat(SidebarSessionViews,未导出、插件拿不到)。原生 tab 体宿主.paneBody是有确定高度的块级滚动容器(非 flex 容器),native 适配层因此给每个 tab 体包一层height:100%的列 flex 宿主(sidebar.module.css的.nativeTabHost),tab 组件根继续用flex:1/height:100%。全局面板(根级mainkeyed 槽 +sidebar.panellist+ctx.layout.selectPanel/beginNavigation、rightbar改根级并新增rightbar.session)插件不接入,只做兼容。底部工作台的中心列锚点认[data-slot="main.conversation"](并跳过display: contents祖先,同时保留 alpha.1 的[data-slot="conversation"]);文件地址语法跟随 alpha.2(fileAddressFor一律 session 作用域、绝对路径保留前导/、parseFileAddress前缀解析并去?/#)。rc.1 起:宿主原生指南只在列出的条目 ≤ 4 条时渲染说明(上游MAX_DESCRIBED_ENTRIES = 4;更长的列表是整列丢弃,不是截断)——插件默认贡献 4 个 guide 条目(文件 / 文件变动 / 任务管理 / 侧边对话),与宿主的终端条目合计后可能超过 4 条,此时说明不渲染;插件不恢复旧的nativeGuideDesc通用兜底句。 - 自定义种子必须带 fork 标记对(
meta.isSeeded: true+inheritedEventCount):dsh-session 契约「只给 seed 不标 isSeeded,种子算重放历史而非继承前缀」——缺标记时Session.ownEvents()含整个种子,子会话的 inbox 折重放种子里的agent/inbox/spliced,继承父会话切割时刻未领取的 inbox 输入。宿主session.fork(api-session-controller)的调用即规范形态;回归由tests/sidechat-seed-validation.spec.ts(真实Session.create+ 对ownEvents()跑 inbox 折)守护。0.1.7 复查:Session.create的五个参数与函数体逐字相同;上游新增了buildForkSeed(events, boundary)导出,与本插件手工的buildSidechatInheritance职责重叠但收益不明确(插件还有 dangling-tool-call 退回结构化快照的自有需求),暂不替换。 dsh.plugin.json从来不是 DSH 契约:0.1.7 全树 grep 仍 0 命中。DSH 真正读的是package.json的dsh.bundle.patch+dsh.client(platform/inject/immediately/external)+exports["./client"]。dsh.plugin.json只服务第三方 registry(社区市场)通道,lib/client-registry.js同理;tests/manifest-consistency.spec.ts守护的是这条通道的形状,不是 DSH 契约。dsh.manifestVersion也仍是「只声明、零读」(0.1.7 的package-manifestREADME 自己写明「Current installers and loaders do not enforcedsh.manifestVersionorengines.dsh」)。0.1.7 的官方管控面新增:plugin-manager的多 registry 回退(registryPlan():配置源 →fallbackRegistries(默认含registry.npmmirror.com),network/timeout/not-found/no-matching-version会自动问下一个源;registry入参只存在于 Web 对话框与plugin_manager工具,CLI 没有 registry flag)、waitForInstall、以及插件展示元数据:package.json#icon(36×36、扁平填充、蓝色渐变,与 DSH 自带图标同风格)+locale/<lang>.json的meta.title/description。本插件已采纳(v0.21.0-alpha.1):icon.svg+locale/en.json+locale/zh.json,并由tests/plugin-meta.spec.ts守护「声明了就必须发布出去」。三条硬规则:icon 必须是包内相对路径且写进files;locale/en.json是发现基线(缺它整个目录都不读);exports必须暴露./locale/*.json与./package.json。所有失败都是静默的——元数据有问题只会退化成一行纯文字,没有任何报错。dsh.bundle.patch现在多了形状校验(非法值会throw,引导期跳过该 bundle),本插件是合法的单字符串。
4. npm 发版(GitHub Release → npm publish)
.github/workflows/release.yml 在 GitHub Release(tag vX.Y.Z)发布时自动发 npm:
- 前置:
package.json版本 bump 到X.Y.Z,CI 全绿后打 tag;tag 与版本不匹配直接失败。 - 流程:
pnpm build/typecheck/test→ 校验 tag →pnpm publish --provenance --access public。 - 认证:npm Trusted Publishing(OIDC),不配
NPM_TOKEN。一次性配置(npmjs.com package → Settings → Trusted Publishers):ProviderGitHub Actions、Orgomdsh-dev、RepoDSH-better-sidebar、Workflow filenamerelease.yml、Environment 留空。 - 调试:
workflow_dispatch+dry_run=true只打包不发版。
5. 开发规则速查
- 构建纯度门:client bundle 禁止 value-import
@dsh-external/*或非白名单@deepseek-ai/*(tsdown.config.ts拦截);import type {}被擦除不触发——类型可共享,运行时符号不行;跨插件交互走ctx.betterSidebar方法调用。 - 懒加载 chunk:重依赖(CodeMirror / mermaid / 19 份第三语言词典)在独立 bundle(
lib/client-<name>.js,0.20.0 起只剩editor/mermaid/locale),经/sidebar/bundle按需下发、globalThis.__dshChunks__物化(src/client/chunk-loader.ts),核心 bundle 禁止静态 importsrc/client/chunks/*。chunk 名在 8 处镜像(tsdown.config.ts/src/bundle-route.ts/src/client/chunk-loader.ts/package.json.files/scripts/package-registry.mjs/tests/{bundle-route,chunk-loader,chunk-artifact,manifest-consistency}.spec.ts)——删一个 chunk 必须同步全部。 - i18n:词典在
betterSidebar命名空间,跟随 DSHctx.locale;新增 zh key 必须同步src/client/locales-ja.ts的 ja 翻译(否则 ja 下回退 en)。渲染MarkdownText必须经markdownTextProps()(§3 第 3 条)。 - 皮肤契约:视觉值只消费
--dsw-alias-*/--dsw-font-*/--ds-*令牌,无硬编码颜色,插件没有任何豁免面——文件/文件夹图标是 DSH 官方FileTypeIcon的图形(宿主自己的调色板),插件画的每个 glyph(含内置 tab 彩色图标)颜色都来自令牌;tests/theme.spec.ts同时守护「图标模块零颜色字面量」「样式表每条color解析到令牌」「没有图标数据被做成 chunk」。契约全文与 titleBar 四方案模型见指南 §12,改动必须同步该节与tests/theme.spec.ts。 - 契约反向引用:皮肤契约被
src/client/shell-presets.ts、tests/e2e/mount.e2e.ts的注释以「指南 §12」引用——调整指南章节结构时同步检查这两处。 - 接入 API 即文档:
src/client/service.ts与src/client/builtins/的任何行为变更,必须同步 docs/external-plugin-guide.md(唯一权威接入文档,不再双份维护)。 - 文件/文件夹图标:回退链是「具体
names/exts注册 → catch-all(exts: [])→ 宿主FileTypeIcon」。插件不持有扩展名表、图标数据或图标 chunk(#429 的 563 条数据与fileIconTheme开关在 rc.2 适配时删除,因为宿主已导出官方图形)。语义雷区:宿主分类器覆盖任意路径,所以注册exts: []会接管所有未具体命中的行——只想补几个类型的插件必须用exts/names。 - 内置 tab 图标:五个内置类型(文件 / 文件变动 / 任务管理 / 侧边对话 / diff)的 glyph 集中在
src/client/builtins/tab-icons.tsx(颜色在tab-icons.module.css,全部--dsw-alias-*),三处消费面自动同步:底部工作台 tab 条、原生指南胶囊、原生 tab 芯片。原生芯片的图标是插件自己画的——宿主的SidebarRightTabDefinition没有 icon 字段,但sidebar.right.pane.tab.title槽就是芯片内容(NativeTabTitle渲染[glyph][title],glypharia-hidden以免改可访问名)。
6. 文档与测试地图
- 插件展示元数据:
package.json#icon+locale/<lang>.json(DSH 0.1.7+ 的 Plugins 页与plugin_manager列表),由tests/plugin-meta.spec.ts守护。 - 接入 API 全参考:docs/external-plugin-guide.md(消费插件开发者向;§0 原生栏承载面 / §4 Tab API / §5 FileViewer API / §7 服务方法 / §10 平台陷阱 / §11 已移除的自由窗口 / §12 皮肤契约 / §15 真实案例)。
- 设计文档:docs/plans/(30+ 份逐特性设计,含实施偏差记录)。
- 关键测试守护:
tests/service.spec.ts/builtins.spec.ts(注册表与内置清单:5 tab + 3 viewer——终端与浏览器已交还宿主,只读预览也按 §3.2 第 5 条让出,只剩 markdown / html / code)/market-manifest.spec.ts(市场约束,含pnpm pack后的产物面)/manifest-consistency.spec.ts(registry 通道形状)/e2e-host-protocol.spec.ts(RPC 双协议)/native-surface.spec.ts(原生右侧栏承载面,含 guide 条目 id 唯一性)/theme.spec.ts(皮肤契约,含空白面板胶囊几何)/plugin-list.spec.ts(推荐插件目录)/fs-search.spec.ts(host 文件名搜索)/fs-watch.spec.ts(文件树目录 watch 的去抖、句柄上限与错误路径)/smoke.spec.ts的 settings 段(按 entry id 读写表单、一次性 legacy 迁移、open 工具门控)。