dream-num/dsh-univer-office
dream-num/dsh-univer-office为 DeepSeek Harness 打造一个真正的办公环境。Univer Office 插件将电子表格、文档、幻灯片、画布、多维表格等汇聚到同一个运行时——数据互联、修改经过校验、变更按版本管理,并以隔离工作树支持多 Agent 协作。
将code-server(VSCode网页版)打包安装到dsh内的插件,快速实现专业的文件编辑。
源码仓库地址见package.json的repository/homepage字段。
## ⚠️ 扩展市场说明(重要)
- code-server 的扩展商店是 Open VSX,不是微软 Visual Studio Marketplace;
- 微软 Marketplace 的条款禁止第三方产品(含 code-server)使用其 API,所以 code-server 无法查询微软市场的扩展列表;
- 因此微软商业/专有扩展(如 GitHub Copilot、Remote-SSH 等 Remote 系列、Azure 系列、IntelliCode)在商店里找不到——这是微软发行策略,不是缺失;
- 微软开源系扩展(Python、TypeScript 调试、ESLint 等)在 Open VSX 有镜像,搜索正常可装;
- 需要微软专有扩展时:从 Marketplace 网页下载.vsix,用code-server --install-extension(或放入--extensions-dir)手动安装,即可在插件列表使用。
静态 profile 插件(npm 包形态,host + client bundle),把 code-server 发行版里的 VS Code server 树 作为平台无关依赖随插件安装(打包期产物 vendor/vscode → @jinsiyu/dshcs-vscode-server, 无安装脚本、无 postinstall);code-server 的 Node 服务层已由插件自带的 `lib/launcher.mjs` 取代 (它直接驱动 /lib/vscode/out/server-main.js 的 loadCodeWithNls() / createServer() / handleRequest() / handleUpgrade(),并补上 /healthz、/manifest.json、/_static/*、/proxy/:port 这几条 code-server 原本提供的 HTTP 面); 原生模块(node-pty / @vscode/sqlite3 / spdlog …)由 @jinsiyu/dshcs-* 子包按真名直接挂在插件依赖上、按 os/cpu 自动选中 —— 无需全局 npm 安装、无需配置 `bin`、无需改 profile 配置、无需第二条安装命令、无需 argon2/C++ 工具链。
打包形态:插件不随包分发 argon2 与 code-server 的 136 个运行时依赖
(express / proxy-agent / js-yaml / pem / limiter …);IDE 由插件自己的 launcher 拉起内置的 VS Code 树,
服务方式见下方「服务方式(serve)」。依据与实测证据见 docs/analysis-code-server-as-dsh-plugin.md。「问 DSH」对话框:面板是 lib/client.js 里手写的 React 组件,渲染器直接 require DSH 页面模块表里的react-dom/client与@deepseek-ai/dsh-client-ui-primitives(与界面同一份实例 ⇒ 排版、代码高亮、
公式都一致,而且不可能版本错配),授权也在同一个对话框里就地处理 —— 详见
「与 DSH 的协同:编辑器桥」。整条链上没有任何构建步骤。
判定一律是能力是否被声明,不做版本号比较。支持范围只有一代:DSH ≥ 0.2.0-rc.2 (web 与 desktop 同一代 —— 两套界面都已升到 0.2.0-rc.2)。此前为 rc 线 0.1.5-rc.x 与 alpha 线 0.1.6-alpha.2…0.1.7-rc.x 并存而写的兼容分支已全部删除:旧座位 settings.plugin.item、 旧数据通道 settingsScope、会话列表快照上的 current 兜底与 recentWorkspaceId 兜底都不再支持 (见「设置」与「旧版 DSH」两节):
DSH 版本 · 载体 · 入口 ≥ 0.2.0-rc.2(web 与 desktop 同一代)—— 判据是有没有 sidebarRight / sidebarRightTabs 服务,不按版本号硬判 · 右侧栏标签(kind=code-server,标签名 Code Server),并认领文件地址(见下) · ① DSH 官方的产物 chip / 「交付」卡片预览 / 正文里的文件名(0.2.5 起,走官方 openFile → 文件地址 → 本 tab);② 右侧栏「开始」页的 Code Server 入口框;③ 设置区(位置见「设置」)→ 「在右侧栏打开」 更早(无右侧栏服务) · 不受支持:除设置区的一条提示外不提供任何入口 · 无(设置区显示升级提示)
ctx.get('sidebarRightTabs') / ctx.get('sidebarRight') 同步探测;服务可能晚于本插件就绪,则 ctx.inject(['sidebarRightTabs','sidebarRight'], …) 等待, 2.5 s 内仍未就绪即判定旧版 DSH(不按版本号硬判,也不影响插件激活)。 0.2.4 起判定可逆、且注册不再依赖 ctx 属性访问(desktop 上曾因此静默不注册、表现为"设置卡正常但侧栏没有入口"):
ctx.,再回退 ctx.get(name)——两种上下文形态都能注册;inject 迟迟不回调时,1.5 s 后用同步服务兜底注册;{sidebar:true} 让 host 恢复;/api/code-server/ui-mode { sidebar:false }(在上面的 10 s 宽限之后),host 据此回收自动预启动的实例并停止预启动(用户手动启动的实例不受影响);服务随后才出现时会再上报 {sidebar:true} 撤销;
自动把右侧栏从"与对话并排"切到全屏(铺满窗口)——IDE 在窄栏里太挤。 只影响"打开那一刻":随时点右侧栏的「退出全屏」不会被抢回去;切走再切回、再次打开文件 tab 会重新切全屏。 不想要就在设置卡片里关掉(fullscreenOnOpen=false)。
ctx.sidebarRight 只有 isExpanded/toggleExpanded(展开/收起),push ⟷ fullscreen 记在 ui-sidebar-right 自己的 store 里(actions.setMode,只发给它的 seat 内部组件); ctx.layout.openRightbar(track, fullscreen) 也不是控制面,而是 seat 用来汇报 presentation 的通道 (上游源码注释:*the occupant reports it; nothing else writes it*)。 所以本插件做的是"用户那个动作本身":closest('[data-sidebar-right-panel]') 定位自己所在面板, 再点面板 chrome 上的 [data-sidebar-right-mode="fullscreen"] 按钮(与手点完全同一条路径, 含窄视口下的连带处理)。按钮找不到时保持原模式并 console.warn 一条,绝不影响面板渲染。
未保存的编辑缓冲区、终端、调试会话都留在原处(见下方「为什么切标签不再重载」)。
打开 IDE 用右侧栏「开始」页的 Code Server 入口框,或直接点官方的产物 chip / 交付卡片 / 正文文件名; 诊断看 DSH host 日志里的 [code-server] 输出(/api/code-server/status 仍返回 env 供脚本排查)。 旧版的 windowedOpen(窗口化打开)、reserveComposer 已在 0.2.6 移除:旧设置文档里残留的这两个键不会报错,只是被忽略(不再出现在 schema 里)。 需要在新标签页用 IDE 时,从设置卡/空态提示里复制完整地址(含路径令牌,http://127.0.0.1://; serve: dsh 时为 DSH 的 /code-server/)—— 少了令牌那一段会 404。
DSH 用资源地址命名文件,openFile 只负责把地址交给右侧栏去认领:
官方产物 chip / 「交付」卡片预览 / 正文内联提及
→ openFile(path, { line? }) (ui-chat 提供)
→ dsh-resource://file/session/<sessionId>/<path> (或 …/file/absolute/<path>)
→ ctx.sidebarRight.openResource(address)
→ 由注册了匹配 patterns 的 tab 类型认领(优先级 extension(3) > builtin(2) > fallback(1),
同带内按 pattern 长度、再按注册顺序)本插件注册时带上:
字段 · 值 · 作用 patterns · ['dsh-resource://file/**'] · 认领文件地址(含 : 的 pattern 按整址 glob 匹配) priority · 'extension' · 高于官方纯文本预览的 fallback——DSH 源码注释写明后者是"VS Code 文本编辑器在编辑器中的位次,任何更具体的类型都应当击败它" canOpen · 见下 · 按「认领类型」设置否决,未认领的地址由官方预览兜底 title · 地址末段(=文件名) · tab chip 显示文件名;页面 tab(sidebar://code-server)仍是 Code Server
claimExtensions,0.2.11 起):不再区分作用域 —— dsh-resource://file/session/… 与 …/file/absolute/… 一视同仁,只看扩展名。 文本框语法(分号分隔,,/空白/换行也认;写 py、.py、*.py 等价;大小写不敏感):
* = 其余类型也认领(兜底);py = 认领 .py;!md = 不认领 .md(排除优先于认领与 *);① DSH 预览渲染得好的四类 —— md markdown html htm png jpg jpeg gif webp bmp ico svg pdf; ② 可执行文件与二进制产物 —— exe com msi msix msixbundle appx appxbundle dll sys scr cpl ocx drv efi mui、 obj o a lib pdb class jar pyc pyo wasm node、so dylib ko elf bin out、apk ipa deb rpm dmg iso img cab; ③ Office 与版式文档 —— doc docx docm dot dotx dotm docb rtf odt、xls xlsx xlsm xlsb xlt xltx xltm xla xlam ods、 ppt pptx pptm pot potx potm pps ppsx ppam odp、vsd vsdx vssx vstx vsdm vssm vstm one onetoc2 mpt mpp pub msg xps oxps odg。 其余(代码、json/yaml、txt、日志、无扩展名如 Makefile、未知扩展名)都进 IDE;清空文本框 = 不认领任何文件(只保留页面 tab)。
bat cmd ps1 sh py js…)与 csv / tsv 仍然进 IDE —— 它们是可编辑的文本。
*;!md;!markdown;!html;!htm;!png;!jpg;!jpeg;!gif;!webp;!bmp;!ico;!svg;!pdf即可(Office 与可执行文件重新进 IDE,预览友好那几类仍留给 DSH)。
PREVIEW_FRIENDLY_EXTENSIONS / EXECUTABLE_EXTENSIONS / OFFICE_EXTENSIONS),默认值就是它们的并集;设置卡里的「实际规则」摘要会点名"含可执行文件、Office 文档"。
下面依次是语法提示、「实际规则」摘要、三组说明,以及折成多行的默认值代码块 —— 换行与分号在解析器里等价,那段连换行一起复制回输入框,得到的策略与默认值逐字相同(有单测钉住)。
py;ts,无 * → 其余不认领)、兜底(*)、兜底加排除(默认)。lib/claim-types.js(host 的 Config 默认值与客户端 canOpen 共用同一份,随包发布,不会两边漂移);单测 scripts/test-claim-types.mjs。
useTabInfo().tab.navigation.address 解析出会话与路径(lib/client.js 的"地址语法"段,与 DSH parseFileAddress 同语义),相对路径按该会话 cwd 展开成绝对路径, 再把绝对路径 + 可选 line 交给 host 的 /api/code-server/open-file;内建扩展 (dshcs-open-file)在 workbench 里 showTextDocument(带行号时定位到该行)。
contentId 就是地址:同址幂等、异址必新开),而 replaceTab 只有发起方(产品自己的 openFile/openResource)能传 —— 所以插件改成:新 tab 的 body 挂载时把同窗格里旧的 code-server tab 关掉。 于是连点两个文件时,你看到的是同一个 tab 在换内容(chip 标题跟着变),而不是越开越多。 两条刻意留的边界:① 只收同窗格的 —— 多窗格是用户主动切分的布局,官方自己也是"每窗格一份" (SidebarRightTabDefinition.multiple 的注释:"one page per kind in each pane"); ② 只有"首次可见"的那个新 tab 负责收 —— 标签页恢复/激活顺序不可控,若每个可见的都收别人, 关掉一个会让下一个变可见,互相收成乒乓(用 ref 钉住"每次挂载只收一次")。 这些 tab 本来就共用同一个常驻 workbench(IDE 是单实例),关掉一个不会重载它: 常驻 iframe 由 lib/client.js 的"常驻 IDE 面"段持有,tab 只是它的停靠宿主(Element.moveBefore)。 回归:scripts/test-client-bundle-tabs.mjs —— 对入口渲染两个 tab,断言旧的被关、新的还在、 跨窗格不动、不可见时不动、缺 actions 的老 DSH 也不崩(负向对照:摘掉合并调用 → 该用例 FAIL)。
?folder= 指定工作区),所以"让 workbench 定位到某个文件"只能由树内的扩展完成; host 写信号文件、扩展轮询并 showTextDocument,失败保留重试(实例尚未就绪时也不会丢)。
过去的坑:DSH 的右侧栏(ui-dockkit)TabPanel 只渲染当前激活标签的 body (TabPanel.tsx:412 → renderTab(active))——切到别的标签 = React 卸载该 body = iframe 被移出文档 = 浏览上下文销毁,切回来就是一次完整的 VS Code 重载(未保存的缓冲区丢失)。把标签浮动成独立面板只是绕开它, 并没有解决。
现在的做法(客户端 `lib/client.js` 的常驻面段,0.2.2):插件把 iframe 从 React 手里接管,做成单例常驻面:
场景 · 动作 · 结果 标签激活 · host.moveBefore(frame, null) 移进当前可见的停靠位 · 状态保持型原子移动,不重载 标签失活 / 收起侧栏 · 移回文档级 park 容器(离屏、保留最后停靠尺寸、inert + aria-hidden) · 面不销毁,后台继续跑 工作区 / 端口变化 · 显式设置 src · 这是唯一正常的"重载"入口
appendChild 移动 iframe 会让内部计时器归零(等价重载),而 Element.moveBefore()(Chromium ≥133)保持状态(计时器 1→2 连续)。
moveBefore 缺失、或宿主已被 React 摘除而抛 HierarchyRequestError: invalid hierarchy(passive effect cleanup 晚于 DOM 卸载)时,退回 appendChild——会重载一次,但绝不丢帧; 状态里 degraded/lastMoveError 明示,界面据此提示"常驻不可用"。
visibility全部正常,面板却一片白(连续两张截图哈希相同,确认没有新帧);translateZ(0)、opacity 微调无效, display:none → 强制重排 → 还原(同一个 JS 任务内)可恢复,且 iframe 文档不重载、内部状态不变、无可见闪烁。 触发条件未能复现:探针页里 moveBefore 停放 337 s(超过 Chrome 对不可见跨源 iframe 的 ~5 min 节流窗口) 后移回、且关掉修复,仍正常绘制。因此把它当兜底保留:每次「停放 → 停靠」补一次 nudgeRepaint()(surfaceSnapshot().nudgeCount 计数,setNudgeEnabled(false) 可现场 A/B)。
keepResident(默认 true)时,宿主在插件启动后就把面建好并停在停放区,首次点开标签无需冷启动等待;preload 不会把正在使用的面拽走。
window.__dshcsSurface(snapshot() / setParkStrategy('offscreen'|'behind') /dock() / park() / nudge() / setNudgeEnabled(false) / destroy())。
实测记录(DSH web GUI,sidebar 标签间真实鼠标切换):切走 → docked:false、iframe 仍为同一节点、内部探针存活、 degraded:false;切回 → docked:true、src 不变、IDE 画面与编辑状态保持(无整页重载)。 完整证据与探针脚本见 docs/analysis-code-server-as-dsh-plugin.md。
方式 · 说明 · 需要 `loopback`(默认) · 插件自己起一个回环端口(默认 `port: 0` = 每次启动由系统分配随机端口),右侧栏 iframe 跨源直连;URL 带随机路径令牌(http://127.0.0.1://,见下「回环端口的安全模型」);进程可被 adopt(DSH host 重启后接管) · 无 `dsh` · IDE 挂到 DSH 自己的 HTTP 端口上的 /code-server/*(HTTP prefix 路由)+ /code-server/-(WS 精确路由),转发到 launcher 的命名管道;没有额外端口;每条请求(含 WS 握手)先过 ctx.connection.requestRejection() —— 与 /api 同一套 Host/Origin fence + 浏览器 cookie 认证 · DSH 提供 webServer 服务(web profile);desktop 无此服务 → 自动回退 loopback
loopback 是 desktop 端唯一的通路(无 webServer、无同源挂载),所以它单独加固了一层:
port 默认 0 → 由系统分配空闲端口,launcher 把实际端口写进 $DSH_HOME/code-server/endpoint.json,host 读回(因此端口每次都变、也不存在"8090 被占用"这类冲突)。要固定地址就显式配 port。
[0-9A-Za-z_-],24 字节随机),写在$DSH_HOME/code-server/path-token(用户 profile 下,默认 ACL 仅本人可读),成为 URL 的路径前缀。 没有这个前缀的请求一律 404(不泄露"这里跑着 IDE"),前缀不带结尾斜杠会 302 补上。
?tkn= → 302 + Set-Cookie: vscode-tkn; SameSite=Lax;