跳转至

Workbench 项目

✦ Workbench · 个人开发工作台 🐹 Go + SQLite · 单二进制交付 📁 上传 · 管理 · 分享 · 文件全链路 📝 折叠 · 目录 · 宽屏表格 · 预览增强 🐞 v1.1.4 · mermaid 渲染修复 · 分栏预览同步 📱 移动端适配 · 数据完全自持 ✦ Workbench · 个人开发工作台 🐹 Go + SQLite · 单二进制交付 📁 上传 · 管理 · 分享 · 文件全链路 📝 折叠 · 目录 · 宽屏表格 · 预览增强 🐞 v1.1.4 · mermaid 渲染修复 · 分栏预览同步 📱 移动端适配 · 数据完全自持
Personal Dev Workbench
Workbench
🛠️ 个人开发工作台 🐹 Go + SQLite ⚡ 单二进制 📱 移动端可用
把个人数据全攥在自己手里,想加什么功能就加什么功能。不是又一个"演示项目",而是打开浏览器就能干活的东西。

我给自己定的目标一直没变:做一个能真正天天用的个人开发工作台。它叫 Workbench,跑在 Go + SQLite 上,前端是 Vite + Vue3 + TypeScript。文件浏览、上传与管理、Todo 看板、用户与组织管理、分享下载,全在一个二进制里。

一路迭代到 v1.1.4:文件从"只能看"补成"能上传、能建目录、能改名移动",软链接有了独立身份,手机浏览器也能正常用了,Markdown 预览则长出了折叠、目录和宽屏表格。最新的 v1.1.4 没有堆新功能,只把一件天天遇到的事修干净——mermaid 图从"时好时坏"变成"打开就能画",顺带补掉侧栏折叠按钮被视口左缘裁掉的老毛病。这些都不是规划出来的,是每天用着用着发现"这里少一块",然后补上的。

为什么不用现成的

市面上不是没有类似的东西。但用别人的工具,总有几个绕不开的坎:

  • 数据不在自己手里。笔记、任务、文件散落在各家云端,想导出、想迁移,麻烦。
  • 改不动。想要个"按组织隔离文件"的功能,得等官方排期,或者自己 fork 一坨。
  • 太重。为了看个文件、记个任务,要起一整套服务,杀鸡用牛刀。

所以干脆自己写。自己写的好处是:每一个功能都是"我需要的",而不是"别人觉得我需要的"。

技术选型:Go + SQLite 就够了

选型上我几乎没纠结。

后端 Go ——单二进制、无依赖、部署就是 scp 一个文件过去。存储 SQLite ——一个 workbench.db 文件搞定所有数据,WAL 模式,备份就是复制文件。前端 Vite + Vue3 + TS ——组件化开发,//go:embed 把构建产物直接嵌进二进制,前端后端一个包。

这套组合最大的好处是 心智负担极低。没有微服务、没有消息队列、没有容器编排,一个进程跑完所有事。对个人工具来说,简单就是最大的可靠。

现在它能做什么

技术栈

🐹Go 🗄️SQLite ⚡Vite 🟢Vue3 🔷TypeScript 🧩Element Plus 📝marked 🧼DOMPurify 🎨highlight.js 📈Mermaid 📦golang-migrate 🌍xgo 跨平台 🔐HMAC-SHA256 📋slog

从"能用"到"好用":几个关键决策

账号体系:从字符串主键到整数自增

最早我图省事,用业务名(比如 yiiewang)当主键。后来发现这是个坑——改名、重名、跨组织引用,全都会出问题。最后老老实实迁到整数自增 id,业务名只当展示字段。

教训:主键就该是"无意义"的,有意义的字段永远不该当主键。

权限模型:用户、组织、功能三层

一开始只有"登录/未登录"两态,后来加了组织,再后来加了角色(admin / org_admin / user)和功能开关。现在每个用户在每个组织有独立的功能开关,文件按用户物理隔离。

这套模型不是一次设计出来的,是 被需求推着长出来的。每次加一层,都对应一个真实的使用场景。

安全:默认严格

路径穿越、符号链接逃逸、CORS 沙箱、Token 鉴权……这些不是"锦上添花",是 默认就该做对 的事。个人工具暴露在公网上,一个洞就是整个数据目录。

预览:一条链路,两处复用

Markdown 预览原本有两套实现:文件预览走 CDN 加载 marked + DOMPurify,Todo 编辑器预览走 npm 的 marked 且完全不做净化。同一个功能两套代码,既是重复也是隐患(后者等于把 v-html 直接暴露给任意输入)。

后来收敛成一个 MarkdownPreview 组件:marked → DOMPurify → v-html,文件预览和 Todo 预览都用它,只靠一个 compact prop 区分尺寸。收敛之后,宽屏、表格增强、折叠、目录这些改动只写一遍,两处同时生效。

教训:同一件事有两份实现时,落后的那份迟早会变成安全漏洞。

沙箱:HTML 预览为什么用 srcdoc

HTML 文件是任意内容,必须隔离在 iframe 里,所以 sandbox 不能给 allow-same-origin——否则页面能读到父页面的 localStorage,登录 token 就漏了。

但这样 #hash 锚点全都点不动:用 blob: URL 作 src,Chrome 直接拒绝(Not allowed to load local resource);换成 srcdoc,href="#id" 又会被解析成导航到 about:srcdoc#id,触发整页重载,表现就是"点了没反应"。

最后的做法是 srcdoc + 注入一小段脚本:拦截页内锚点点击,改成 scrollIntoView。脚本跑在独立源里,碰不到父页面。安全边界没退让,功能也没丢——多数时候这两件事并不冲突,只是要找对方案。

踩过的坑

  • SQLite 迁移:ALTER TABLE ADD COLUMN 不能带外键,旧库升级时索引依赖新列会直接崩。最后上了 golang-migrate 版本化迁移,每个 schema 变更加一个脚本。
  • 前端状态:KeepAlive + onMounted 的组合下,任何依赖 onMounted 的"模式切换重置"逻辑都会因组件复用而不执行,必须在路由 watch 里显式收敛。
  • 构建链路://go:embed 要求 frontend/dist 存在,fresh clone 必须先 make all 才能编译,否则直接报错。
  • !important 会盖掉内联样式:移动端抽屉的媒体查询写了 width: 280px !important,把折叠态的内联 width: 0 一起盖掉,抽屉再也收不起来。显隐这类状态最好交给 transform 这类不会与布局宽度打架的属性。
  • 全局注册函数被自己覆盖:有个组件把包装函数又写回 window.openShareModal,而包装函数内部正是调用 window.openShareModal——访问过那个页面后再点分享就是无限递归。window 上的全局入口只能有一个注册方。
  • 渲染链路上游会污染下游:mermaid 代码块先被 highlight.js 的 highlightAuto 插了一堆 <span>,而 mermaid.run() 拿的正是节点 innerHTML —— 这些标签被当成源码文本,直接报 Syntax error in text。修法是 mermaid 块原样输出(只转义 &、<、>)。两个库共用一条渲染链时,上游的"美化"就是下游的"污染",顺序必须显式约定,不能靠默认。
  • 迭代残留比代码债更难发现:noUnusedLocals 一直是 false,于是没人调用的导出、永远不会被触发的 emit 链路、写了但没逻辑对应的 CSS 类,就这么一轮轮攒下来。后来开启这个开关,一次清出六处未使用符号。能给编译器管的检查,别交给自觉。

写在最后

写这个工作台,最大的收获不是代码,是 想清楚了一个问题:工具应该长在人的工作流里,而不是让人去适应工具。

它现在还在持续迭代,当前版本 v1.1.4(Release 与变更记录)。如果你也想要一个"数据在自己手里、想改就改"的个人工具,欢迎来 GitHub 看看,或者直接体验在线实例。

板块里还有示例代码和效率工具——返回实验室概览。


最后更新:2026-10-26(v1.1.4)

评论