资讯详情 Cindy Desktop 中 vendored drawio 查看器的集成、更新与源码级实现解析
📅 2026/10/26 1:45:28
【免费下载链接】cindyConsider it done. The open-source AI agent that works out of the box · 想到就能做到。开源、开箱即用的 AI Agent。项目地址https://gitcode.com/GitHub_Trending/cind/cindy点击查看免费下载Cindy 桌面端的文件浏览器支持直接预览.drawio图形文件而这一能力依赖仓库内apps/desktop/src/renderer/vendor/drawio/目录中随包分发的 drawio viewer。本文以该目录的 README 为主线完整讲解 vendor 的边界与许可条款、版本号的自动解析方式、更新操作流程并顺着 DrawioPreview.tsx 与 FileBodyView.tsx 的调用链说明离线加载、data-mxgraph参数、零宽挂载陷阱以及与 Electron CSP 的相互制约。读完可以独立完成一次 viewer 升级并能在预览失败时按源码路径定位问题。为什么要以 vendor 方式内置 viewerREADME 给出的定位非常明确这是用于在工作区文件浏览里预览.drawio文件的官方 viewer 脚本直接 vendor 进仓库以避免运行时从外网拉取。这一决策同时满足三个约束离线可用桌面端不能假设用户此刻能访问公网DrawioPreview.tsx的头部注释也重申了viewer 离线工作vendor 目录里有Vite 通过?url出 URL不联网。可审计的版本与许可文件随安装包分发版本与许可证必须写进第三方声明因此版本号需要可被脚本解析而不是靠人工记忆。不污染主包体积viewer 本体约 3.6MB磁盘上viewer-static.min.js实测约 3.7MB若被静态 import 会直接进入 renderer 主 bundle因此实现上改为首次使用时动态注入script。目录构成与许可边界vendor 目录只有三个文件职责边界清晰文件作用是否随包分发viewer-static.min.js上游发布产物drawio 的只读 viewer 脚本是renderer 资源LICENSEApache-2.0 全文Copyright (c) JGraph Ltd是合规材料README.md来源、版本、许可、更新方式与使用入口说明仓库内文档README 记录的元信息可以直接在仓库中复核来源上游 drawio 项目的发布产物viewer-static.min.js。版本30.0.4以 min.js 内嵌的VERSION字符串为准。在 bundle 中确实存在mxClient{VERSION:30.0.4,...}这样的内联常量这也是自动化脚本唯一可靠的版本来源。LicenseApache-2.0见同目录LICENSE。附加限制说明上游对其 icon set / stencil / 模板素材另有未经书面许可不得用于 Atlassian 产品的附加条款本目录只 vendor viewer代码本体不包含也不分发那些素材因此该限制对本产品不适用。这条素材 vs 代码本体的区分是合规上最容易被忽略的一点。仓库把它同时写进了两处README 的说明段以及生成脚本中bundledComponent追加的免责文字——Only the viewer JavaScript (viewer-static.min.js) is redistributed. Upstream icon sets / stencils / templates carry an additional no-Atlassian-use restriction; none of those assets are redistributed by this product.见 generate-third-party-notices.mjs。需要特别提醒因为不随包分发 stencil 与 icon 素材某些依赖上游素材库的自定义形状在预览时可能只显示基础图形。这是只分发代码本体的直接后果而不是 viewer 加载失败。版本号如何被自动解析进第三方声明README 明确写了版本号自动从 min.js 解析无需手改。这一承诺由 scripts/generate-third-party-notices.mjs 实现逻辑很短但很关键// drawio viewer — vendored 进仓库的 .drawio 文件预览脚本(renderer 资源随包分发) try { const drawioDir path.join(DESKTOP_DIR, src, renderer, vendor, drawio); const drawioLicense normalizeNoticeText( fs.readFileSync(path.join(drawioDir, LICENSE), utf8), ); const drawioVersion /VERSION:([\d.])/.exec( fs.readFileSync(path.join(drawioDir, viewer-static.min.js), utf8), )?.[1] || vendored; entries.push( bundledComponent({ name: drawio viewer (vendored), version: drawioVersion, license: Apache-2.0, licenseText: drawioLicense \n\nOnly the viewer JavaScript (viewer-static.min.js) is redistributed. Upstream icon sets / stencils / templates carry an additional no-Atlassian-use restriction; none of those assets are redistributed by this product., }), ); } catch { /* vendored 目录被移除时自动跳过 */ }从源码可以读出几个工程约束版本提取是正则而非 JSON 解析/VERSION:([\d.])/匹配 bundle 里的VERSION:30.0.4。若上游改变压缩产物中的这段形态例如改成单引号或变量间接赋值正则失配会静默回退为字符串vendored声明仍能生成但版本信息退化。升级后务必检查生成的声明文本。失败即跳过整个块包在try/catch中LICENSE或 min.js 缺失时不抛错只跳过该条目。这意味着如果有人误删 vendor 目录构建不会失败但产物会缺少一条许可声明——需要靠 review 或声明文件的 diff 发现。许可全文被规范化后内嵌normalizeNoticeText负责统一换行与空白最终写入docs/legal/notices/下的声明与 SBOM 文件生成物头部标注本文件由 scripts/generate-third-party-notices.mjs 自动生成请勿手改。当前仓库中已提交的声明可以直接印证结果例如 docs/legal/notices/THIRD-PARTY-NOTICES.txt 与各平台文件desktop-macos.txt、desktop-win.txt、desktop-linux.txt均包含drawio viewer (vendored) 30.0.4 License: Apache-2.0SPDX 清单 docs/legal/notices/sbom/desktop-linux.spdx.json 中同样有name: drawio viewer (vendored)条目。至于 vendored 资源的许可归口说明见 docs/legal/README.md 中把vendor/drawio/LICENSE归类为vendored 前端资源的上游许可证随资源分发。更新流程替换文件 重新生成声明README 的更新方式只有两句但落地时建议按下面四步执行避免只完成一半第一步取新版产物。从上游仓库或 diagrams.net 发布产物中获取新版viewer-static.min.js。第二步覆盖同名文件。用新文件覆盖apps/desktop/src/renderer/vendor/drawio/viewer-static.min.js。注意保持文件名不变——DrawioPreview.tsx通过固定路径/vendor/drawio/viewer-static.min.js?url引用它改名会直接导致构建期解析失败。若上游 LICENSE 有变更同步更新同目录LICENSE。第三步重新生成第三方声明。pnpm licenses:generate该命令在根 package.json 中定义为node scripts/generate-third-party-notices.mjs会重建docs/legal/notices/下的声明文本与 SBOM。版本号无需手改。第四步核对与回归。建议至少确认三件事# 1) 新产物里确实内嵌了版本串(正则的来源) grep -o VERSION:[0-9.]* apps/desktop/src/renderer/vendor/drawio/viewer-static.min.js | head -1 # 2) 声明中版本已被刷新 grep -n drawio viewer (vendored) docs/legal/notices/THIRD-PARTY-NOTICES.txt # 3) 声明相关测试仍通过 node --test scripts/__tests__/third-party-notices.test.mjs第 1 步的输出应与声明中的版本一致当前为30.0.4若第 1 步为空说明新版压缩形态不再匹配正则声明会退化成vendored此时应更新生成脚本中的正则而不是手改声明文件。使用入口从文件树到预览组件的调用链README 只授权一个入口apps/desktop/src/renderer/features/cc-agent/workdir-browse/DrawioPreview.tsx—— 首次打开.drawio文件时动态注入script加载约 3.6MB不进 main bundle后续复用。业务方不要直接 import 本目录。业务方不要直接 import是硬性约束min.js 是 UMD 风格的全局脚本没有 ES 模块导出直接import既拿不到GraphViewer还会把它打进主包。正确姿势只有经过DrawioPreview这一层封装。文件类型识别是否走 drawio 分支由 lib/drawioExt.ts 判定export function isDrawioPath(filePath: string): boolean { const lastSlash Math.max(filePath.lastIndexOf(/), filePath.lastIndexOf(\\)); const name filePath.slice(lastSlash 1); const dot name.lastIndexOf(.); const ext dot 0 ? : name.slice(dot).toLowerCase(); return ext .drawio; }三个细节值得注意同时处理/与\因此 Windows 路径与 POSIX 路径都能识别先剥离目录再找扩展名避免目录名中带点号造成误判例如a.b/notes不会被当成有扩展名大小写不敏感toLowerCase().DRAWIO同样命中。文本分支截流而非二进制分支FileBodyView.tsx 中的关键实现是.drawio文件本质是 XML 文本无 NULL 字节因此内容读取会落在text分支而用户期望看到的是图形而不是原始 XML所以在进入PlaintextEditor/ markdown 渲染之前先把它截走if (isDrawioPath(relPath)) { return ( DrawioPreview workdir{workdir} relPath{relPath} xmlContent{content.content} size{content.size} mtimeMs{content.mtimeMs} / ); }源码注释同时说明了为什么在binary分支刻意不处理.drawio理论上加密 drawio 才会进 binary极罕见退回UnrenderablePlaceholder即可。外部拖拽文件同样受支持dropExternalFile.ts 把 drawio 与文本、图片、PDF 一起列入可预览类型export function isDroppedFilePreviewSupported(filePath: string): boolean { return ( isTextPreviewSupported(filePath) || isImagePath(filePath) || isPdfPath(filePath) || isDrawioPath(filePath) ); }这解释了为什么从系统文件管理器拖入一个.drawio也能直接预览——它复用同一套isDrawioPath判定避免了文件树里能预览、拖进来不能预览的行为割裂。渲染实现剖析DrawioPreview的核心是一个useEffect加一个模块级 Promise。拆开看有五个关键设计点。1. 懒加载与模块级 Promise 复用// 模块级 promise 复用 —— 第一次注入 script 就开始 race,后续 mount 共享同一个 // resolve。失败时清空,允许下一次重试(网络盘读不到 / SHA 损坏等极端场景)。 let viewerLoadPromise: PromiseDrawioGraphViewer | null null; function loadViewerOnce(): PromiseDrawioGraphViewer { if (window.GraphViewer) return Promise.resolve(window.GraphViewer); if (viewerLoadPromise) return viewerLoadPromise; viewerLoadPromise new PromiseDrawioGraphViewer((resolve, reject) { const script document.createElement(script); script.src viewerUrl; script.async true; script.onload () { if (window.GraphViewer) { resolve(window.GraphViewer); } else { viewerLoadPromise null; reject(new Error(drawio viewer loaded but GraphViewer is undefined)); } }; script.onerror () { viewerLoadPromise null; reject(new Error(failed to load drawio viewer-static.min.js)); }; document.head.appendChild(script); }); return viewerLoadPromise; }设计意图逐条对应script.src viewerUrl中的viewerUrl来自import viewerUrl from /vendor/drawio/viewer-static.min.js?url。Vite 的?url后缀把文件作为独立资源发射并返回其 URL而不是把源码内联进模块图——这就是不进 main bundle的实现方式。顺带说明vite.renderer.config.ts 中自定义的assetsInlineLimit只对*-worklet.js返回false其余资产维持默认阈值行为3.6MB 的脚本远超阈值本来也不会被内联。if (window.GraphViewer)是幂等短路多次挂载不会重复插脚本。viewerLoadPromise作为模块级缓存让并发打开多个.drawio标签页时共享同一次加载。失败时把缓存置null使下一次打开可以重试——注释举的例子是网络盘读不到、SHA 损坏等极端场景。onload之后仍要再检查window.GraphViewer脚本加载成功不等于全局对象就位这种情况会明确抛drawio viewer loaded but GraphViewer is undefined而不是留一个空白画布。2.data-mxgraph参数逐项说明viewer 的对外契约是一个带data-mxgraphJSON 的容器 divDrawioPreview按此构造const host document.createElement(div); host.className mxgraph; host.style.maxWidth 100%; host.setAttribute( data-mxgraph, JSON.stringify({ xml: xmlContent, highlight: #0000ff, nav: true, resize: true, dark-mode: auto, check-visible-state: false, }), ); container.appendChild(host); viewer.createViewerForElement(host);参数取值作用与影响xml文件原文由父层useFileContent读出图形数据源。直接内联避免预览器二次读取文件highlight#0000ff高亮色用于搜索/定位等交互反馈navtrue打开平移、缩放等导航交互resizetrue容器尺寸变化时自适应重排dark-modeauto跟随 OS/应用主题注释说明浅色仍是浅图深色用对应反色check-visible-statefalse关键项见下一节3.check-visible-state: falseReact 动态挂载下的零宽陷阱源码给出了这个参数存在的真实原因值得完整引用其含义drawio 默认会检查host.offsetWidth0 宽时挂MutationObserver等 parent attribute 变化才渲染。React 动态挂载 flex 布局下host 刚挂上去就是 0 宽parent attribute 也不会再变observer 永远等不到 → viewer 静默卡住不渲染。设为false让 drawio 立即渲染布局到位后 resize handler 会自适应宽度。也就是说这个参数是避免永久白屏的必需项而非性能优化。它同时解释了为什么外层容器用了flex min-h-full items-center justify-center p-6且内层ref容器是max-w-full——布局由 React 侧负责绘制时机由 viewer 侧立即触发两者解耦。4. 用createViewerForElement而不是processElements类型声明里对两个 API 的差异写得很直白interface DrawioGraphViewer { /** drawio 源码里这个参数是 class name 字符串(默认 mxgraph),不是 scope 元素。 * 传 DOM 元素会被 toString 成 [object HTMLDivElement] → 找不到任何元素 → 静默失败。 */ processElements: (className?: string) void; /** 给单个已含>useEffect(() { let cancelled false; const container containerRef.current; if (!container) return; container.replaceChildren(); setState({ kind: loading }); (async () { try { const viewer await loadViewerOnce(); if (cancelled) return; // ...构造 host 并渲染 if (!cancelled) setState({ kind: rendered }); } catch (err) { if (cancelled) return; log.warn(drawio render failed, { relPath, error: String(err) }); setState({ kind: error, message: String(err) }); } })(); return () { cancelled true; }; }, [workdir, relPath, xmlContent]);要点清理旧画布container.replaceChildren()在每次 effect 执行时先清空避免切换文件后残留上一张图。异步取消cancelled标志防止组件卸载或快速切换文件后对旧实例setState。重渲染触发条件依赖数组是[workdir, relPath, xmlContent]因此文件内容变化外部编辑保存会触发重绘而size/mtimeMs只用于降级占位卡展示不参与绘制。降级路径出错时渲染 UnrenderablePlaceholder透传workdir、relPath、size、mtimeMs让用户仍能通过在系统中打开等方式处理文件而不是只看到一个报错。已知限制replaceChildren()直接丢弃旧 DOM没有调用 viewer 的销毁接口源码中createViewerForElement的返回类型也只是unknown。从代码结构看频繁切换多个大型.drawio文件时可能存在 viewer 内部对象留存这属于可观察的推断而非已确认的问题。加载态还有一处 i18n 文案ccAgent.workdirBrowse.fileBody.drawioLoading在 zh-CN/common.json 中为正在加载 draw.io 预览…同时提供了defaultValue兜底因此即使 locale 缺键也不会露出 key 名。为什么不 fetch 文件xdt-file://与 CORSDrawioPreview.tsx的头部注释解释了它为什么坚持只接收xmlContentprop而不是像图片预览那样自己去读文件文件内容XML 文本由父层FileBodyView经useFileContent已读好直接传xmlContentprop。不走fetch(xdt-file://...)—— 该协议corsEnabled: falsefetch API 会被跨源拦掉TypeError: Failed to fetch同时也省一次冗余读取。仓库里可以找到对应的协议注册证据schemePrivileges.test.ts 断言各自定义 scheme含xdt-file的权限集合为standard: true、secure: true、supportFetchAPI: true、bypassCSP: false、corsEnabled: false。其中corsEnabled: false正是 renderer 侧跨源fetch被拒的原因测试注释也提到该组合对xdt-modelmodel-viewerfetch与xdt-fileFBXLoader / drawio viewer fetch是load-bearing的。这里有一个值得注意的实现取向差异协议层保留了supportFetchAPI: true但 drawio 预览的实际代码路径选择完全绕开fetch改由已经读过内容的上层直接传字符串。好处是一石二鸟——既避开 CORS 限制又省掉一次磁盘或远端读取。这也解释了为什么DrawioPreviewProps里xmlContent: string是必填而非可选。与 Electron 安全边界的相互作用内置一个 3.6MB 的第三方脚本不只是打包问题还牵动 CSP。script-src中的unsafe-evalapps/desktop/src/main/security/csp.ts 明确记录了这条权衡const scriptSrc isDev ? [self, unsafe-inline, unsafe-eval, wasm-unsafe-eval] : [self, unsafe-eval, wasm-unsafe-eval];注释说明vendored drawio viewer calls real JS eval() from withincreateViewerForElementand runs in THIS renderer; without unsafe-eval any.drawiofile preview deterministically throws EvalError and degrades to the unrenderable placeholder。同时强调这是有意为之的取舍——unsafe-eval只让代码能到达已存在的eval()sink而注入内联脚本与远程脚本这两条主路径在 prod 中依然被堵死script-src中没有unsafe-inline也没有远程源。对应的回归断言在 csp.test.tsit(script-src blocks inline remote script (the primary XSS hardening) but allows eval for vendored drawio, () { const d parsePolicy(buildContentSecurityPolicy(PROD_CTX)); expect(d[script-src]).toEqual([self, unsafe-eval, wasm-unsafe-eval]); expect(d[script-src]).not.toContain(unsafe-inline); expect(d[script-src]).toContain(unsafe-eval); });这条测试是升级 viewer 时不要顺手删unsafe-eval的护栏一旦删除.drawio预览会稳定失败并降级为占位卡而其他功能看起来一切正常很容易被误判为 viewer 版本问题。源码注释还写明了后续收紧方向把 drawio 移入独立iframe sandbox并配置自有 CSP从而让 prod 可以移除unsafe-eval。self与内置的双向约束prod 的script-src只有self没有远程源。这与 vendor 决策互为因果脚本必须与 renderer 同源因此不能改回运行时从 CDN 拉取否则会被 CSP 直接拦截。style-src中的字体域同一份 CSP 的style-src包含https://fonts.googleapis.com注释写的原因是drawio viewer loads Google Font stylesheets for sketch-font diagrams。这意味着在完全离线的环境下手绘风格sketch图元的字体会回退但样式表加载失败不会阻断图形绘制。定位这类图能出来、字体不对的反馈时可以先区分是离线字体回退还是 viewer 未加载——后者会走UnrenderablePlaceholder前者不会。常见问题排查表现象首要怀疑点源码定位打开.drawio显示原始 XML走了 text 分支isDrawioPath未命中drawioExt.ts、FileBodyView.tsx预览区域空白但无报错check-visible-state为真导致零宽不渲染或误用processElements传了 DOM 元素DrawioPreview.tsx控制台出现EvalErrorCSPscript-src缺少unsafe-evalcsp.ts、csp.test.ts提示 drawio viewer loaded but GraphViewer is undefined脚本加载成功但全局对象未就位产物异常/被拦截loadViewerOnce的onload分支提示 failed to load drawio viewer-static.min.js资源 URL 失效或文件缺失?url导入与 vendor 目录TypeError: Failed to fetch试图对xdt-file://做fetchschemePrivileges.test.ts、DrawioPreview.tsx头部注释声明中版本变成vendored新版 bundle 不匹配VERSION:x.y.z正则generate-third-party-notices.mjs部分自定义形状不显示未分发上游 stencil / icon 素材README 的附加限制说明手绘字体样式异常离线无法访问fonts.googleapis.comcsp.ts 的style-src排查时的通用起点是DrawioPreview的日志失败分支会调用log.warn(drawio render failed, { relPath, error })logger 名称为drawio可据此在日志中过滤。维护清单改动这类代码时该守住什么保持唯一入口外部只经DrawioPreview不要让业务组件直接importvendor 目录README 明确要求。若需要在别处预览 drawio复用isDrawioPath 组件而不是再写一套 loader。坚持?url懒加载任何改成静态import的写法都会把 3.6MB 塞进主包违背 README 记录的设计意图。坚持传入xmlContent不要再引入fetch(xdt-file://...)corsEnabled: false决定了它必然失败。升级后三件事核对VERSION串、跑pnpm licenses:generate并检查声明 diff、手动打开一个含手绘风格与自定义形状的.drawio验证渲染。不要删unsafe-eval除非完成 iframe 隔离方案否则会直接破坏预览功能。延伸阅读vendor 目录说明与许可边界apps/desktop/src/renderer/vendor/drawio/README.md、apps/desktop/src/renderer/vendor/drawio/LICENSE预览组件与类型声明apps/desktop/src/renderer/features/cc-agent/workdir-browse/DrawioPreview.tsx文件类型判定与路由apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/drawioExt.ts、apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileBodyView.tsx拖拽入口apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/dropExternalFile.ts合规生成与产物scripts/generate-third-party-notices.mjs、docs/legal/notices/THIRD-PARTY-NOTICES.txt、docs/legal/README.md安全边界apps/desktop/src/main/security/csp.ts、apps/desktop/src/main/__tests__/schemePrivileges.test.ts赞分享【免费下载链接】cindyConsider it done. The open-source AI agent that works out of the box · 想到就能做到。开源、开箱即用的 AI Agent。项目地址https://gitcode.com/GitHub_Trending/cind/cindy点击查看免费下载相关推荐告别手动升级drawio-desktop自动更新系统全解析Electron-updater实战指南告别手动升级drawio desktop自动更新系统全解析Electron updater实战指南 drawio desktop是一款基于Electron构桌面应用图形学Cindy 桌面端 Windows 独立更新器 cindy-updater 实战解析CLI 契约、更新阶段与失败重试机制Cindy 桌面端 Windows 独立更新器 cindy updater 实战解析CLI 契约、更新阶段与失败重试机制 cindy updater 是 CiSumatraPDF 仓库中的 MuPDF Android 集成指南从嵌入查看器到源码级定制SumatraPDF 仓库中的 MuPDF Android 集成指南从嵌入查看器到源码级定制 本篇技术指南以 MuPDF 官方 Android 使用文档 e桌面应用上一篇Minimal Mistakes 单页布局加宽指南classes: wide 的配置方法与底层实现原理下一篇Biome Markdown 格式化器如何对齐有序列表项内的缩进内容CommonMark Example 249 测试用例深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考