drscrewdriver/dsh-opensheet-sidebar ↗★ 0

dsh-opensheet-sidebar

DSH web plugin (dsh-better-sidebar consumer): render CSV/TSV/PSV files and xlsx/xlsm workbooks as a structured, sortable, filterable table in the right sidebar — with sheet tabs and a circuit breaker (rows/cols/cells plus the two zip-container gates) so no input can freeze the browser. 适合需要在侧边栏快速查看、筛选和排序表格数据的用户。

Package
dsh-opensheet-sidebar
Compatibility
Unverified
Harness peer range
>=0.1.5-rc.1 <0.2.0-0
Cordis peer range
^4.0.2
Version
1.0.1
Last updated
Sep 24, 2026

Install

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:drscrewdriver/dsh-opensheet-sidebar

dsh-opensheet-sidebar

右侧栏表格预览插件(dsh-better-sidebar 消费者):csv / tsv / psv 与 xlsx / xlsm 都以结构化表格打开,带熔断保护——任何输入都不能让侧栏卡死。

名字是双关,写在这里免得被当成拼写错误:OpenSheet 同时读作「打开表格」(open the sheet,动词)与「开放标准的表格」(open-standard sheet —— OpenDocument 那一系的开源表格格式)。一个词说清了它做的两件事:打开,以及用一种不依赖专有库的方式读。

边界:双关只活在名字里。功能字段一律保持字面意思 —— exts 只填真实后缀(csv/tsv/psv/xlsx/xlsm),协议字段不塞品牌词。

v0.2.0 是一次集成方式的重写:v0.1.0(当时名 dsh-csv-sidebar)的客户端半边不是 DSH 的客户端模块契约(ES module + activate() + ctx.sessionProjections),因此装上去也不会显示。0.2.0 按 plugin-framework 标准重做,见下「为什么 v0.1.0 不显示」。


一、它做了什么

接缝注册内容用户看到什么
文件预览器 registerFileViewerexts: ['csv','tsv','psv']、priority: 50、fetchStrategy: 'fsRead'在资源管理器里点开 .csv → 直接是表格,不是原始文本;可在 Side 卡设置里开关,关掉即回到内置文本查看器
手动 tab registerTaborder: 70、single: true+ 菜单里的 CSV 页:拖拽/点选本地文件,此时真实字节数已知,超大文件在读之前就被拒绝

两者共用同一个渲染体(警告栏 → 统计栏 → 表格 → 底部操作),差别只在文本从哪来。

交互能力(对齐「结构化数据不得降级为纯文本」):点击表头排序(升 → 降 → 取消)、子串过滤、数值列右对齐(按样本推断)、列类型识别(number / date / boolean / text)、sticky 表头与行号列、空值与唯一值统计。


二、四维熔断

维度默认上限越界后的行为
文件大小5 MBBLOCKED —— 本地文件连 file.text() 都不调用;宿主读取的文本直接拒绝
数据行数10 000TRUNCATED —— 读到上限即停,不做完整解析
列数100多余列丢弃 + 警告
单元格文本10 KB该单元格省略为 前200字符… (+N chars) + 警告

另有 previewRows: 200:表格只保留前 200 行的 DOM 预算。第 201 行到来时不静默——升级为 rows 警告并置 TRUNCATED。

熔断结果是一等公民,不是错误页:横幅逐条写明哪个上限触发、实测值多少、上限多少,表格照常渲染(BLOCKED 时显示明确的阻止说明)。四种状态 OK / TRUNCATED / BLOCKED + READING 全是可渲染产出,永不抛异常、永不无限转圈。


三、标准格式对照(plugin-framework)

标准要求本插件
双半结构src/index.ts(宿主半,空 apply)+ src/client/index.tsx(浏览器半)
客户端模块契约构建产物为 window.__ModuleLoader__.load({ id, factory }) CJS 闭包,导出 apply + inject;构建期用 node:vm 真加载一次并断言导出
inject 声明式注入export const inject = ['betterSidebar', 'locale'] —— cordis 等两个服务就绪后才 apply,注册顺序无关
生命周期所有安装项都走 ctx.effect(fn, label),HMR/禁用时由返回的 disposer 回收
设置席位/命令注册本插件不注册设置席位、不注册 / 命令(故不涉及 seat-pin 与 description 函数契约)
dsh.bundle manifestpackage.json#dsh.bundle.patch + dsh.client.platform/inject
cordis.patch.yml纯 insert 一行,注释写明 CLI / 手动两种安装方式
peerDependencies@deepseek-ai/* 用 >=0.1.5-rc.1 并登记 disposer;配色全用 --dsw-alias-* 语义 token
多语言自有命名空间 dsh-opensheet-sidebar,zh/en 字典经 locale.register(NS, tag, dict) 注册;缺服务时回退内置英文字典,缺 key 时渲染 key 本身(可见而非空白)
软依赖纪律不 import dsh-better-sidebar 任何值/类型(seams.ts 只做结构声明);betterSidebar 缺席时响亮 warn 并保持惰性,不注册半个面板

四、为什么 v0.1.0 不显示(根因)

v0.1.0 装进 profile 后 cordis.patch.yml 已登记、node_modules 也在,但侧栏毫无动静,且控制台无报错——因为它在客户端模块加载阶段就被丢弃了:

#v0.1.0 的写法标准契约后果
1lib/client.js 是 ES module(export function activate)window.__ModuleLoader__.load({ id, factory }) 的 CJS 闭包模块表拿不到导出 → 整包静默不加载
2入口导出 activate(ctx)导出 apply(ctx)即使加载也找不到入口
3没有 injectexport const inject = [...]cordis 不等待 betterSidebar,注册时机随机
4注册到 ctx.sessionProjections.register({key,badge,component})ctx.betterSidebar.registerTab / registerFileViewer打错接缝:sessionProjections 是宿主侧投影注册表,不是侧栏 UI 接缝
5无 ctx.effect;package.json 无 dsh.bundle / dsh.client / exports / files;peer 上限写 >=0.1.5-rc.1(无上界)见上表无 HMR 回收;元数据不完整

一句话:不是「没生效」,是「没被加载」。这也解释了为什么「文件预览」系统里看不到它的条目——那个清单就是 registerFileViewer 的注册表。


五、构建与验证

npm run build     # esbuild 双入口 + tsc 类型声明;构建期真加载一次 bundle 并断言 apply/inject
npm test          # 25 项断言:14 csv(解析/引用/分隔符/四维熔断/占位符完整性/警告去重)+ 11 xlsx(zip/XML/工作表/两道容器闸门)
npm run verify    # build && test

npm test 实测输出(2026-09-22):

dsh-opensheet-sidebar :: csv core
  ok  plain CSV parses to headers + rows
  ok  quoted delimiter, doubled quote and embedded newline survive
  ok  semicolon and tab files are sniffed, not assumed
  ok  oversized text is BLOCKED before parsing
  ok  oversized local File is BLOCKED without file.text()
  ok  row ceiling truncates and stops the reader early
  ok  the reader really stops: rows past the ceiling are never visited
  ok  column ceiling drops extra columns and warns
  ok  an oversized cell is elided, not dropped silently
  ok  a host-capped read is reported as truncated
  ok  blank lines and a trailing newline never mint rows
  ok  an empty document is OK with zero rows (never a throw)

dsh-opensheet-sidebar :: 12 checks passed, 0 failed

构建期门禁(scripts/build.mjs)在写出产物后:用 node:vm + 桩 window.__ModuleLoader__ 真跑一遍 bundle,断言 load() 被调用、id 等于包名、factory() 返回带 apply 与 inject 的对象;并拒绝任何 Node 内置模块请求。加载不了的 bundle 在构建阶段就红灯,不会带病进宿主。


5.1 xlsx 夹具与自检(demo/xlsx/)

夹具由脚本生成,不提交二进制:固定随机种子、字节稳定,且每个文件只负责触发一条路径。

python scripts/make-xlsx-fixtures.py --out ../demo/xlsx   # 需要 openpyxl(dev-only)
node scripts/report-fixtures.mjs ../demo/xlsx             # 用插件自己的读取器跑一遍并打印所见
文件体积触发
basic-3sheets.xlsx8 KB3 个工作表(其一隐藏)· 真实日期格式 · 布尔 · 列空洞 · 无缓存值的公式
wide-150cols.xlsx15 KB列上限(150 > 100)→ cols
long-12000rows.xlsx286 KB行上限 + 预览预算 → rows(单条,含 found/kept/limit)
long-cell-20k.xlsx5 KB单元格上限(20 000 > 10 240)→ cell-length
deep-sheet-60k.xlsx1.7 MB归档小、解压 56.7 MB → sheet-bytes(解压前就拒绝)
big-archive.xlsx8.5 MB归档本身超 5 MB → file-size(BLOCKED)

report-fixtures.mjs 的输出就是「这套夹具各司其职」的证据;它跑在真实 Excel 写入器(openpyxl)产出的文件上,因此也覆盖了合成夹具覆盖不到的自定义日期格式路径(2026-01-08 而非序列号 46030)。

六、安装

方式 A(推荐):官方 CLI —— 它会把依赖与 dsh.profile.bundles 一起维护好

dsh plugin --profile 
 add 

方式 B:手动(三步,缺一不可)

# 1) 复制本包
#    → ~/.dsh/profiles/
/node_modules/dsh-opensheet-sidebar
# 2) 把 "dsh-opensheet-sidebar" 加进该 profile package.json 的 dsh.profile.bundles 数组
# 3) 把本包 cordis.patch.yml 的 insert 行追加到该 profile 的 cordis.patch.yml

⚠️ 第 2 步是最容易漏、且漏了完全静默的一步。 bundle 层只在 profile 的 dsh.profile.bundles 列到本包时才被合成;只做第 1、3 步的结果是:包装好了、 cordis.patch.yml 里也有 - id: dsh-opensheet-sidebar,但那一行没有可作用的行可打—— 插件不加载,控制台一声不响。第 3 步的 disabled: false 只是「把已插入的行显式启用」, 它本身不会插入任何行。

装完先验证再重启(不启动任何服务):

dsh --profile 
 --dump-config | Select-String dsh-opensheet-sidebar
# 期望看到:
#   # == dsh-opensheet-sidebar, patched by ...\cordis.patch.yml
#   - id: dsh-opensheet-sidebar
#     name: dsh-opensheet-sidebar
#     disabled: false

看到这三行 = 行已插入 + profile 补丁已生效,此时重启 DSH即可(profile bundle 与客户端模块表都在启动时合成,仅刷新页面不够)。 看不到 = 第 2 步没生效。

依赖:dsh-better-sidebar >= 0.18.1(可选)。缺席时插件正常加载、控制台 warn、不贡献任何条目。


七、版本兼容矩阵

插件版本DSH说明
0.3.0>=0.1.5-rc.1 =0.1.5-rc.1 注意区分:**WPS 表格默认保存的 .xlsx就是标准 xlsx**,本插件直接支持;只有「另存为 WPS 旧格式」产出的.et` 才在未支持之列。

十、发布与身份迁移(脚本样例)

两个脚本各管一半:发布在开发机,迁移在装插件的 profile。都支持 -DryRun(唯一可反复运行的模式),都是遇到第一个失败就停、绝不「照发不误」。

# ① 发布侧:构建 + 测试 + 身份门禁 + 打包清单,然后才真发(--access public,官方 registry)
pwsh -File scripts/publish-npm.ps1 -DryRun                # 先看
pwsh -File scripts/publish-npm.ps1                        # 再发
pwsh -File scripts/publish-npm.ps1 -DeprecateOld dsh-csv-sidebar   # 可选:给旧名打 deprecate

# ② 消费侧:装新身份 → 摘旧身份 → 换 profile 补丁行 id → 逐项验证
#    -NewSpec 必须是「今天真的能解析到」的 spec —— 脚本会在改动任何东西之前先预检。
#    本包目前只在 GitHub(npm 上尚不存在),所以用 github: 形态:
pwsh -File scripts/migrate-profile.ps1 -Profile web -NewSpec 'github:drscrewdriver/dsh-opensheet-sidebar#' -DryRun
pwsh -File scripts/migrate-profile.ps1 -Profile web -NewSpec 'github:drscrewdriver/dsh-opensheet-sidebar#'
#    发布到 npm 之后再切成 registry 形态(在那之前这条会被预检挡下):
#    pwsh -File scripts/migrate-profile.ps1 -Profile web -NewSpec 'dsh-opensheet-sidebar@1.0.0'

预检(preflight):-NewSpec 先解析再动手 —— npm 形态走 npm view [@range],github: 形态走 git ls-remote(40 位 SHA 无法按名查询,故验证仓库可达性,SHA 本身交由安装步骤校验),本地形态查 package.json。解析不到就在备份之前中止,退出码 1 —— 免得一条"看起来能用"的样例跑到一半才失败。

也可以走 npm script 别名:

npm run publish:npm -- -DryRun
npm run profile:migrate -- -Profile web -NewSpec 'dsh-opensheet-sidebar@1.0.0' -DryRun

10.1 为什么顺序是「先装新、再摘旧」

改名要同时改四处,而漏任何一处都是静默失败(不是报错):profile 依赖 spec、dsh.profile.bundles(由 CLI 的 reconcile 维护,不手工改)、profile cordis.patch.yml 的 row id(针对不存在 id 的补丁是空操作)、node_modules 里的目录。

先装新的、再摘旧的,任何一步失败都留下一个还能用的插件,而不是一个空 profile。两次 pnpm 调用都会自动重试一次 —— 宿主在跑时 node_modules 被占用,首次可能抛 ERR_PNPM_PACKAGE_MANAGER_REMOVE_MODULES_DIR。

10.2 脚本里的三个 PowerShell 5.1 坑(复用时可省半小时)

坑现象写法
原生命令 + 2> 重定向 + $ErrorActionPreference='Stop'npm 的 stderr 警告被提升为终止错误,脚本在第 2 步莫名死掉调用前临时放宽为 Continue,2>&1 | Out-String 合并流,再读 $LASTEXITCODE
param() 默认值里的 $PSScriptRoot求值时还是空串 → Split-Path 参数校验失败默认写成 '',在脚本体里解析
Where-Object 结果的 .Count命中 1 条是 string(无 .Count)、0 条是 $null,Set-StrictMode 下直接报 PropertyNotFound一律套 @(...) 再取 .Count

发布脚本另外带两道上传前后都该有的门禁:名字必须「空闲或属于自己」(npm view + maintainers 对比 npm whoami,防止改到一个别人占用的名字上),以及没有登录会话时直接拒绝(而不是等一个 401)。