SwitchHosts 5.0

<p>一转眼,<a target="_blank" rel="noopener noreferrer nofollow" href="https://switchhosts.app">SwitchHosts</a> 已经存在了 15 年了,距离上一个大版本更新也过去 5 年了。最近抽空把 SwitchHosts 升级到了 5.0,最大的变化是将底层从 Electron 换成了 Tauri,安装包的体积小了很多。</p><p>这个过程的大部分工作都是用 AI 完成的,在这儿记录一下。</p><img src="https://s.oldj.net/uploads/2026/05/25/sh_light.png" width="978" data-align="center"><p></p><h2>动机</h2><p>这次升级的主要动机是想解决 SwitchHosts 体积过大的问题。</p><p>之前的版本是基于 Electron 实现的,打包后安装文件有大几十兆,且随着 Electron 版本的提升,这个体积还在不断变大,因为它内部依赖的 Chromium 等在不断变大。</p><p>Electron 是一个很好的框架,我还有一些其他项目也是基于 Electron 实现的,我很喜欢它。但 SwitchHosts 只是一个小工具,功能也很简单,几十兆的安装包对它来说有点重了,在 GitHub 的 issues 中也经常有人诟病这一点。</p><p>在这个版本之前我已经有很长一段时间没有对 SwitchHosts 做实质性的更新,原因之一就是考虑到后续可能要换架构,因此不太想再在原来的代码上做太多改动。</p><p>还有一个升级的动机则有点神奇:去年年底,SwitchHosts 收到了一笔来自 <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.warp.dev/">Warp</a> 的赞助。虽然金额不多,却也是一个意外之喜了。</p><p>既然还有人在关注和使用 SwitchHosts,我想我也应该继续完善它,于是,就有了 v5 这个版本。</p><p></p><h2>方案</h2><p>SwitchHosts 需要支持 Windows、macOS、Linux 三个平台,我没有足够的精力同时维护三套代码,因此首选还是跨平台技术或者框架。</p><p>考虑到要尽可能复用之前的 UI,技术方案上还是需要选择 WebView 界面或类似的方案,因此最终可用的选项并不是太多,其中最为成熟的就是 <a target="_blank" rel="noopener noreferrer nofollow" href="https://tauri.app/">Tauri</a> 了。</p><p>我也考察过一些其他方案,其中 <a target="_blank" rel="noopener noreferrer nofollow" href="https://wails.io/">Wails</a> 看起来不错,也有一些成功案例,不过它的生态似乎还是小了一些,同时它的 v3 还处在 Alpha 阶段,用在正式产品中有一定风险,如果我先用 v2 开发,可能不久之后又要再做一次迁移升级到 v3。</p><p>新出现的 <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/blackboardsh/electrobun">Electrobun</a> 也很有吸引力,它最大的优点是类似 Electron 可以使用 JS/TS 来写后台代码,不像 Tauri/Wails 那样需要使用 Rust/Go。不过它才发布没多久,看评价可能还有不少奇怪的 bug 或适配问题。</p><p>Tauri 当然也不完美,对前端来说最大的难点是一旦深入或者要做什么定制就要和 Rust 打交道,它打包体积小的代价是不同平台上可能会有不同的表现,推特上就有不少使用 Tauri 遇到大坑然后不得不放弃的经历分享。</p><p>不过,考虑到 SwitchHosts 只是一个功能比较简单的小工具,不会涉及太多复杂的平台相关的操作,在考察一圈之后,我最终还是选择了 Tauri。</p><p></p><h2>过程</h2><p>这次升级,大致上有两个阶段,分别是界面升级和架构升级。</p><p><strong>界面升级</strong></p><p>在投入 Tauri 之前,我先发了一个 v4.3.0 版,这个版本仍然是 Electron 架构,唯一的目标是先把 UI 框架从 <a target="_blank" rel="noopener noreferrer nofollow" href="https://chakra-ui.com/">Chakra</a> 升级到 <a target="_blank" rel="noopener noreferrer nofollow" href="https://mantine.dev/">Mantine</a>,主要原因是我在其他项目中大量使用 Mantine,对它更熟悉,也更喜欢它的设计风格。</p><p><strong>架构升级</strong></p><p>第二阶段则是重头戏,从 Electron 迁移到 Tauri。</p><p>迁移的工作基本都是由 AI (Claude Opus 4.6/4.7 + GPT-5.5)完成的,在动手之前我和 AI 聊了很久,先让它理解这个项目,然后评估迁移到 Tauri 的可行性,以及可能会遇到什么问题。</p><p>大体上的方案就是界面部分基本不动,但原本 Electron 的 main 进程的逻辑迁移到 Rust 实现。虽然理论上大部分工作可以放到渲染进程中用 JS/TS 完成,后台只负责基础的数据读写等工作以尽量避免写 Rust,但有 AI 的帮助,我还是选择了把所有后台逻辑(原 main 进程的逻辑)转为了 Rust。</p><p>当把所有能想到的问题都考虑到,确认迁移能顺利完成之后,我再让 AI 根据结论,制定一个详细的迁移计划,包括分成几个步骤,每步骤要做什么,验收标准是什么,然后将这些步骤保存为多个 Markdown 文档。</p><p>接着,我人工检查了这些计划文档,继续向 AI 提问、修改,直到我自己看不出明显问题,然后交给 AI,让 AI 自己反复审核这些计划文档,寻找可能的风险点以及没有考虑周全的地方,直到 Claude Opus 基本没有发现问题,又让 GPT 再检查了几遍,确保没有明显的疏漏。</p><p>计划确定后,就是执行的步骤了。这个过程很简单,让 AI 读取计划文档,一步一步地执行,每完成一个步骤,就将进展写入一个专门的 <code>progress.md</code> 文件,这样一方面方便自己随时检查,另一方面也是为了在 AI 流程中断或者新开对话的时候能快速继续工作。</p><p>大概是前期计划比较充分,加上现在 AI 确实已经足够智能,迁移工作大体上很顺利,断断续续地搞了几天,核心迁移就基本完成,新的 Tauri 版能跑起来了。</p><p>再接下来,就是一些琐碎的工作,各种细节的处理,新版本 UI 的改造,一些小功能的调整,以及反复检查是否存在 bug 或潜在问题。相对于迁移来说这部分的难度小了很多,但耗时却更长一些。</p><p></p><h2>成果</h2><p>最终,Tauri 版打包后的安装文件只有数兆,其中最小的 Windows 版只有 2.7M,较大的 macOS 通用版也不过 7.6M,体积是 Electron 版的几十分之一。</p><p>不过,在运行的时候,Tauri 版仍然会占用一百至数百兆的内存,比 Electron 版略少一些,但不像安装包那样有数量级的变化。这些内存基本上是由 WebView 进程使用的,因为 Tauri 的界面本质上是 Web 页面,除非彻底换成原生方案,不然大概没有特别好的办法来进一步降低内存占用。</p><p>现在从体积上来说 SwitchHosts 是一个小工具了。后续我会在保持轻量的前提下,继续完善它。</p><p></p><h2>小结</h2><p>最近几年 AI 的发展可谓日新月异,即使是在一两年之前,我也无法想象仅靠 Vibe Coding 的方式就能完成如此复杂的工作。</p><p>这个迁移很复杂,工作量也很大,且我对 Rust 并不熟悉,如果没有 AI 的帮助,我不会有勇气采用这么激进的迁移方案,即使采用,耗时可能也会是现在的数倍甚至十倍。AI 已经深刻改变了程序员的工作方式。</p><p>使用 AI 进行复杂工作是可能的,但要注意先制定好详细的计划,并将这些计划按阶段写成文档,然后让 AI 一步一步地执行。因为 AI 的上下文窗口大小有限,如果不将计划写下来就直接开始,AI 很可能执行到后面就忘了前面。</p><p>最后,喜欢各位新老用户喜欢 SwitchHosts 的新版本。❤️</p>

2026/5/26
阅读更多

开源本站博客系统 - Publa

<p>自从 2010 年注册了域名 oldj.net 并架设了这个博客,一转眼已经 16 年了,这么长的时间足够发生很多事,比如杨过和小龙女都已经再次重逢了,本站也经历过了 N 次重构甚至推倒重来。最近,在 AI 的帮助之下,我重写了站点的后台,并决定将这个博客系统开源。</p><p></p><h2>历程</h2><p>这个博客最早的版本是使用 Django 写的,后台使用了 Django 自带的 admin,前台则使用 Django 的模板输出 HTML。</p><p>一段时间之后,我有点懒得继续改进,便迁移到了 WordPress。不得不说,WordPress 的功能非常完善,插件也很多,使用起来非常方便。</p><p>再后来,我担心自己维护的 WordPress 有安全问题,便一度将站点迁移到了 WordPress.com 官方服务。</p><p>不过,又一段时间后,我发现官方服务有不少限制,比如想在文章中插入数学公式需要升级高级会员,成本实在有点高,加上官方服务位于海外,访问总是有一点慢,于是又迁回了自已部署的 WordPress。</p><p>但很快,我发现使用 WordPress 时,如果启用了某些功能或者某些插件,站点仍然需要从海外下载文件,整个博客的访问速度也因此被拖慢,于是又起了自建的心思,花了一些时间,用 egg.js 重写了博客系统。</p><p>不过 egg.js 本质上来说仍是上一代框架,或者说是基于后端视角开发的框架,它很适合写后台服务或者传统网站,但对 React 生态的支持则几乎没有。此时,React 等前端框架的边界不断拓展,除了写前端代码,还出现了 Next.js 这样的框架,让它能在服务端渲染 React 组件,解决了之前 React 站点对搜索引擎不友好的问题。</p><p>于是,我又开始折腾,用 Next.js 重写了这个博客站点。但这次我只用 Next.js 写了博客的前台页面,后台则改为继续使用 Django,Next.js 通过 API 向 Django 读取数据并渲染。</p><p>随着时间的流逝,我又逐渐厌倦了在 Django Admin 中写博客的体验,一直计划着再次改进这个博客,只是一直没抽出时间。</p><p>转眼到了 2026 年,AI 的发展如火如荼,Codex、Claude 等模型在写代码方面的表现已经非常出色,也深刻地改变了我日常写代码的方式。我决定花一些时间,在 AI 的帮助下完成这个博客系统后台的改造。</p><p>于是,就有了现在这个博客系统,我叫它 <a target="_blank" rel="noopener noreferrer nofollow" href="https://publa.app">Publa</a>(读音:<code>/ˈpʌb.la/</code>,帕布啦)。</p><p></p><img src="https://s.oldj.net/uploads/2026/06/03/image2.png" data-align="center"><p></p><h2>设计理念</h2><p>Publa 不是一个大而全的博客系统,它的目标用户是个人或小团队。在设计和开发过程中,我一直秉承着以下设计理念:</p><p></p><p><strong>轻量</strong></p><p>不要做得太重太复杂,要让部署尽可能简单,比如可以直接在 Vercel 上部署,或者只需要一个 Docker 文件就能部署。</p><p></p><p><strong>动态</strong></p><p>现在有很多静态博客系统,它们使用 Markdown 编写内容,最后生成静态文件并发布上线。</p><p>这些博客系统都很不错,且因为是纯静态,运行和维护的成本极低。但缺点也在于纯静态,比如作为静态站点,它们天然不支持评论,如果要让访客发布评论,通常还需要使用第三方工具,无论是体验还是数据,都比较割裂。</p><p>如果说评论还能用第三方工具搞定,另外一些比如定时发布、历史记录、站内搜索等等功能,静态博客系统就基本没有办法了。</p><p>我希望这个新的博客系统自身就能支持评论、留言、定时发布、站内搜索等功能,因此做成动态便是更好的选择。</p><p></p><p><strong>易于自定义</strong></p><p>Publa 内置了浅色和深色两个主题,同时支持自建主题,或者添加自定义 CSS。</p><p>同时,在后台设置中,还开放了自定义 HTML 片断的功能,支持在 <code>&lt;head&gt;</code>末尾、 <code>&lt;body&gt;</code> 开头或末尾等位置插入自定义 HTML。</p><p>因此,理论上来说,你可以根据偏好,自定义 Publa 博客前台页面的所有外观样式。</p><p></p><p><strong>易于迁移</strong></p><p>Publa 博客的数据可以一键导出为 JSON 文件,你也可以根据这个格式规范,将其他博客的内容转为 JSON 并导入 Publa。</p><p>这个导出功能,也可以用作站点的数据备份。</p><p></p><p><strong>所见即所得编辑器</strong></p><p>Markdown 是一项伟大的技术,我也用 Markdown 写了多年的博客和各类文档。不过,当我开始编写 Publa 后台时,我还是添加了一个基于 TipTap 的所见即所得编辑器。我已经用这个编辑器写了几篇文章,目前感觉良好,尤其是在要插入图片等场景,所见即所得编辑器会更加直观易用。</p><p>当然,如果你偏好手写 Markdown,或者想发布已经用 Markdown 格式写好的内容,Publa 后台也支持直接输入 Markdown。</p><p>顺便,Publa 的编辑器支持内容自动保存,写作的时候,每隔 5 秒会将编辑器中的内容保存到 localStorage,每隔 30 秒会自动将内容保存到云端草稿,如果连续三次自动保存失败(比如网络中断)会在顶部显示通知;同时,后台还会定期为变化的内容保存历史版本,后续如有需要可随时查看或恢复到之前的版本。基本上,你可以放心地在 Publa 后台撰写文章,而不用担心内容丢失。</p><p></p><p><strong>支持添加自定义页面</strong></p><p>除了博客文章,有时我们还需要添加一些自定义页面,比如「关于」页面。</p><p>在 Publa 中,你可以任意添加自定义页面,并指定访问路径,只要这个路径与已有页面不重复即可。自定义页面支持复用当前页面框架(页头、页脚),可用于快速添加一些常见的功能页面,也支持不使用任何模板,由你直接输入整个页面的 HTML,添加完全自定义的页面。</p><p></p><h2>一些特色</h2><p>除了以上设计理念外,Publa 还有一些值得提一下的特色功能。</p><p></p><p><strong>支持附件上传</strong></p><p>写博客少不了配图,有时可能还要向用户提供一些文件以供下载。Publa 内置支持 AWS S3、Cloudflare R2、阿里云 OSS、腾讯云 COS,如果你有这些平台的账号,可在 Publa 后台添加配置,之后便可以直接在附件管理页面上传和查看附件了。也可以直接在编辑器中粘贴图片,Publa 会自动将图片上传到你配置的平台。</p><p>Publa 会管理通过它上传的文件,如果你在 Publa 后台删除了文件,对应的存储平台上的文件也会同时删除。</p><p></p><p><strong>一键将图片设为 1/2 尺寸</strong></p><p>这是我之前写博客时的一个痛点。</p><p>随着高分屏的普及,我们给屏幕截图时,得到的图片尺寸通常是截图区域的两倍大小,插入到博客中时,我们希望它能以实际尺寸的 1/2 大小进行显示。</p><p>在大多数富文本编辑器中,作者只能拖拽调整图片尺寸,然后凭感觉调一个差不多的尺寸。或者在 Markdown 编辑器中,先查看一下原图的尺寸,再人工计算一下,输入一个 1/2 的宽度。</p><p>在 Publa 的编辑器中,我专门优化了这个流程,插入图片后,点击选中图片,图片上方便会浮现一个快捷工具栏,其中便有一个将图片一键设为 1/2 尺寸的按钮。如果你使用高分屏并经常要在文章中插入屏幕截图,那么这个功能对你应该会很有用。</p><p></p><img src="https://s.oldj.net/uploads/2026/04/16/half_size.png" width="948" data-align="center"><p></p><p><strong>支持添加自定义跳转</strong></p><p>在 Publa 中,你可以添加自定义跳转规则,将任意访问路径跳转到指定的路径。</p><p>如果你之前在使用其他博客系统,它的文章的路径结构和 Publa 不同,那么你可能会需要这个功能,用于将文章的老路径跳转到新路径,以便用户访问老路径时能正确跳到新的页面。</p><p>比如老博客系统中文章路径是 <code>/article/{slug}</code> 这样的形式,但 Publa 中文章路径是 <code>/posts/{slug}</code> ,那么便可以设置一条类似下图的跳转规则,以便平滑迁移。</p><img src="https://s.oldj.net/uploads/2026/04/16/redirect_rules.png" width="347" data-align="center"><p></p><p><strong>支持 SQLite 和 PostgreSQL</strong></p><p>既然是动态博客,后台数据库自然少不了。Publa 同时支持 SQLite 和 PostgreSQL 两种数据库,你可以根据需要选择。</p><p>由于支持 SQLite,如果你想开发调试 Publa 非常简单,只需从 GitHub 上下载源码,安装依赖,然后使用 <code>npm run dev</code> 即可启动测试服务器,在没有指定数据库的情况下,Publa 会自动创建并使用本地 SQLite 以便尽快开始开发调试工作。</p><p>值得一提的是,Publa 不仅支持本地 SQLite,还支持 <a target="_blank" rel="noopener noreferrer nofollow" href="https://turso.tech/">Turso</a> 提供的在线 SQLite 服务。Turso 有足够小项目使用的免费额度,因此,对大多数个人博客或小团队博客来说,可以将 Publa 部署在 Vercel 上并使用 Turso 提供的数据库,完全零成本运行。</p><p></p><h2>小结</h2><p>以上便是对博客系统 Publa 的介绍。</p><p>你现在看到的这个博客就是基于 Publa 的,我已经使用这个系统一段时间了,将持续使用并不断改进完善它,如果你也想架设一个动态博客,不妨试一试 <a target="_blank" rel="noopener noreferrer nofollow" href="https://publa.app">Publa</a>。</p><p>Publa 基于 MIT 协议开源,任何人都可以免费下载和使用它。</p><p>当然,Publa 还很年轻,且之前只有我一位用户,因此可能有一些一直没被发现的 bug,如果你发现了问题,欢迎在 GitHub 上给我<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/oldj/publa">提 issue</a>!</p><p>最后,希望 <a target="_blank" rel="noopener noreferrer nofollow" href="https://publa.app">Publa</a> 能帮你更好地完成博客写作!❤️</p><p></p>

2026/5/12
阅读更多

当写作软件遇上 AI:Scrivener 论坛上一场持续三年的产品哲学之争

<p>老牌写作软件 Scrivener 的论坛上有一篇关于是否要支持 AI 的<a target="_blank" rel="noopener noreferrer nofollow" href="https://forum.literatureandlatte.com/t/adding-ai-tools-to-scrivener/133222">帖子</a>,我花了一些时间仔细地把这篇帖子以及所有回复读了一遍。读完之后,我发现这不是一个简单的“要不要 AI”的技术讨论,更是一场关于写作工具本质、创作者身份认同、以及技术边界的长期争论,也是 AI 为我们这个时代带来的冲击的一个剪影,值得写一篇博客记录一下。</p><h2>背景</h2><p>从 2023 年 3 月开始,Scrivener 的官方论坛上发生了一场马拉松式的辩论,主题很直白:要不要给 Scrivener 加上 AI 功能?</p><p>这条讨论一直延续到 2025 年底,公开可见的帖子超过 300 条,参与者里有职业作家、程序员、学术研究者,当然还有 Literature &amp; Latte 公司的官方代表。</p><p>三年时间里,AI 从一个新鲜话题变成了行业标配,但 Scrivener 的立场却始终清晰而克制。</p><h2>官方的态度</h2><p>官方代表的回复让我印象深刻,他们没有用那种“我们永远不会”的绝对化措辞,也没有跟风喊“AI 是未来”,而是用一种近乎工程师式的冷静,把问题拆解成了几个层面。</p><p>2023 年 3 月,官方代表 kewms 说得很直接:“We are not interested in any form of subscription model.”(我们对任何形式的订阅模式都不感兴趣)这句话几乎提前封死了“把大模型月费捆绑进 Scrivener”的可能性。</p><p>2024 年 5 月,另一位官方代表 AmberV 把理由说得更清楚:他们不愿意把用户的草稿发送到第三方服务器,而消费级硬件上的本地模型“还不够有用”。这不是对新技术的忽视或恐惧,而是对隐私伦理和产品质量的双重坚持。</p><p>他们把 AI 工具类比为参考文献软件,Scrivener 可以提供接口,让你用自己选的工具,但不会替你选择、绑死、然后为它的价格变动和服务质量背书。</p><p>到 2025 年 11 月,官方给出了一句几乎可以当作政策结论的话:“是否使用 AI 工具,以及使用哪一种,最好留给用户自己决定。”</p><p>简单来说,官方温和而坚定地拒绝了在软件中集成 AI 服务。</p><h2>用户的分歧</h2><p>然而,讨论并不只是一场“挺 AI”对“反 AI”的二元对立。</p><p>支持者里,最有代表性的诉求不是“让 AI 替我写小说”,而是一些听起来很实际的需求:</p><ul><li><p>能不能帮我检查一下,这把枪在第一卷里到底有没有上膛?(长篇小说/系列小说的连贯性检查)</p></li><li><p>能不能把所有角色的对话按风格分类统计一下?</p></li><li><p>能不能快速识别出世界观设定里的前后矛盾?</p></li><li><p>能不能帮我起草一个复杂的正则表达式?</p></li></ul><p>这些需求的共同点是:它们不是要 AI “代写”,而是要 AI “帮忙分析或整理”。</p><p>反对者的担忧也不是单纯的技术恐惧,他们在意的是:</p><ul><li><p><strong>隐私</strong>:未完成的草稿被送去训练,等于创意还没成型就被“偷走”了。</p></li><li><p><strong>署名</strong>:如果 AI 参与了创作,读者怎么知道哪些是你写的?出版合同里写的“100% 我的作品”还算数吗?</p></li><li><p><strong>产品定位</strong>:Scrivener 的价值就在于它是“少数仍然让人自己写”的空间,一旦加入 AI,这个定位就模糊了。</p></li><li><p><strong>成本转嫁</strong>:如果 AI 功能需要订阅,那 Scrivener 还是那个“买断制、属于你”的工具吗?</p></li></ul><p>自然地,也有一些中间派,他们不反对 AI,但坚持认为更好的做法是:让操作系统或第三方工具来提供 AI 能力,Scrivener 只需要保持开放接口,而不是自己深度绑定某个模型。</p><h2>一个隐含共识</h2><p>读完整个讨论,我发现帖子中似乎存在一个隐含的共识:几乎没有人真正支持“让 AI 直接替作者写正文”。</p><p>即便是最积极的支持者,也只是把需求限定在头脑风暴、查询、校对、梳理、节奏分析这些辅助性工作上。反对者也基本承认,拼写检查、语法建议这类功能迟早会渗透到操作系统层面。</p><p>换句话说,真正被广泛排斥的不是“AI 辅助”,而是“把去作者化的生成机制塞进写作工具的核心”。</p><p>如果要做一个类比,大概是这样:人们可以接受用计算器做算术题,甚至能接受带计算器进入考场,但不接受用 AI 直接回答试卷。</p><h2>真正的分歧</h2><p>梳理下来,核心分歧其实有三条:</p><p><strong>第一,隐私的边界在哪里?</strong><br />支持者认为,只要透明告知、可以关闭,把数据发到云端就是可接受的;官方和反对者则认为,“别人(别的软件)也这么干”不代表这就是对的,尤其是当这些数据是还没发表的创作草稿时。</p><p><strong>第二,外接工具够不够用?</strong><br />官方认为,Alt+Tab 切换到另一个 AI 工具,或者用 Grammarly、ProWritingAid 这类第三方服务,已经是可行甚至更干净的方案;支持者则觉得这会打断沉浸感,制造重复的拷贝粘贴。</p><p><strong>第三,AI 到底是“不可逆的大势”还是“被过度营销的泡沫”?</strong><br />有人说“AI 会变得无处不在,不适应就会落后”;也有人说“我不认为 AI 行业能撑过五年,成本、能耗、法律风险都没算清楚”。</p><p>这三条分歧显然短时间内不会有结论,帖子中直到最后也还没有真正收敛。</p><h2>一点感想</h2><p>Scrivener 很克制,这种克制在这个时代显得格外珍贵。</p><p>不是因为 AI 不好,而是因为在一个“人人都在加 AI”的环境里,却有一个工具明确说“我们不替你做价值判断,你自己决定”,这本身就是一种立场。</p><p>这也符合 Scrivener 一直以来的产品哲学,它不会猜你想写什么,但会给你足够的空间和结构,让你把想写的东西组织好。它的价值不在于“替你做决定”,而在于“让你的决定更容易实现”。它不是一个“智能”工具,而是一个“强大”工具。</p><p>从这个角度看,官方的立场就很好理解了:AI 可以存在,但它应该在 Scrivener 的边界之外,由用户自己选择、自己控制、自己承担风险。Scrivener 只需要保持开放,而不是成为某个 AI 服务的前端。</p><h2>未完待续</h2><p>当然,故事还没结束。</p><p>Windows 侧会不会出现类似 Mac 上 Apple Intelligence 的“官方允许但不深度绑定”的路径?一些用户想要的“非生成式、但高度语义化”的功能,能否在不依赖大模型的前提下实现?版权、训练集伦理、输出的可版权性,这些法律和行业规范层面的问题,什么时候能有定论?</p><p>这些问题,到目前为止的讨论里仍然悬而未决。</p><p>但至少有一点是清楚的:Scrivener 不会因为“别人都在加 AI”就跟风,也不会因为“市场需要”就放弃自己的产品哲学。</p><p>在一个充满焦虑和跟风的时代,这种克制是一种稀缺的品质。</p><p></p>

2026/5/2
阅读更多

使用 GitHub Action 自动更新 SSL 证书

<p>之前写过一篇<a target="_blank" rel="noopener noreferrer nofollow" href="https://oldj.net/posts/ssl-cert-with-acme-sh">使用 acme.sh 申请 SSL 证书</a>的帖子,最近把这个流程放到了 GitHub Action 上自动化完成,在这儿记录一下要点。</p><p>GitHub Action 是托管在 GitHub 上的自动化服务,免费账户的私有仓库每月有 2000 分钟的运行额度,可以用来做很多运维、CI 相关的工作。</p><p>要使用 GitHub Action,只需在项目的根目录下的 <code>.github/workflows</code> 中创建一个 <code>.yml</code> 文件即可,比如新建一个 <code>.github/workflows/renew-ssl.yml</code> 。</p><p></p><h2>基本设置</h2><p>这个 action 的基本结构类似下面这样:</p><pre><code class="language-yaml">name: Renew SSL Certificate on: schedule: - cron: '0 20 * * 0' # 每周一 UTC 20:00(北京时间 04:00) workflow_dispatch: # 支持手动触发 permissions: contents: read jobs: renew-cert: runs-on: ubuntu-latest steps: - name: Task name run: | YOUR SCRIPT</code></pre><p></p><p>代码很直白,这儿就不多解释了,下面继续看最主要的任务步骤(steps)。</p><p></p><h2>第一步:安装 acme.sh</h2><p>我们要使用 <a target="_blank" rel="noopener noreferrer nofollow" href="http://acme.sh">acme.sh</a> 来申请 SSL 证书,因此第一步需要安装 acme.sh。在以上 YML 文件的 <code>steps</code> 部分,添加以下代码:</p><pre><code class="language-yaml"> - name: Install acme.sh run: | curl https://get.acme.sh | sh -s email=${{ secrets.ACME_EMAIL }}</code></pre><p></p><h2>第二步:申请证书</h2><p>接下来,则是使用 <a target="_blank" rel="noopener noreferrer nofollow" href="http://acme.sh">acme.sh</a> 申请域名证书。这儿我们申请了一个泛域名证书:</p><pre><code class="language-yaml"> - name: Issue or Renew Certificate id: issue-cert env: Ali_Key: ${{ secrets.ALI_KEY }} Ali_Secret: ${{ secrets.ALI_SECRET }} run: | ~/.acme.sh/acme.sh --issue \ -d oldj.net \ -d '*.oldj.net' \ --dns dns_ali \ --keylength ec-256</code></pre><p></p><p>你需要将上面代码中的 <code>oldj.net</code> 替换为你自己的域名。申请泛域名证书时,需要同时传入 <code>your-domain.com</code> 和 <code>*.your-domain.com</code>。</p><p>我的域名是在阿里云上,因此 <code>--dns</code> 参数设置的是 <code>dns_ali</code> 。如果你的域名在其他注册商那里,需要把这个参数改为对应的值,具体可查看 <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/acmesh-official/acme.sh/wiki/dnsapi">acme.sh 的文档</a>。</p><p>注意,这儿用到了环境变量 <code>secrets.ALI_KEY</code> 和 <code>secrets.ALI_SECRET</code> ,这两个值需要你去阿里云后台生成,然后在 GitHub 项目仓库的后台添加。</p><p>阿里云后台具体位置是 AccessKey 管理页面,建议创建一个子账号,只授予 <code>AliyunDNSFullAccess</code> 权限即可。</p><p>GitHub 添加 Secrets 的位置是:仓库页面 → Settings → Secrets and variables → Actions → New repository secret。</p><p></p><h2>第三步:上传到服务器</h2><p>证书需要上传到服务器才能发挥作用,在这儿我们使用 SSH 的方式连接服务器。为了能顺利连接服务器,需要你准备一对 SSH 密钥,将公钥上传到服务器的 <code>~./ssh/authorized_keys</code> 下,同时像上面那样将密钥添加到 GitHub Secrets 中,名称是 <code>SSH_PRIVATE_KEY</code> 。</p><p>接下来,再在 <code>renew-ssl.yml</code> 中继续添加以下代码:</p><pre><code class="language-yaml"> - name: Setup SSH run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" &gt; ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa echo "StrictHostKeyChecking no" &gt; ~/.ssh/config</code></pre><p></p><p>再接下来是正式的上传步骤:</p><pre><code> - name: Deploy to Server-001 if: steps.issue-cert.outcome == 'success' continue-on-error: true run: | scp ~/.acme.sh/oldj.net_ecc/fullchain.cer \ ~/.acme.sh/oldj.net_ecc/oldj.net.key \ ${{ secrets.SERVER_USER_GATEWAY }}@${{ secrets.SERVER_HOST_GATEWAY }}:${{ secrets.CERT_REMOTE_DIR }}/ ssh ${{ secrets.SERVER_USER_GATEWAY }}@${{ secrets.SERVER_HOST_GATEWAY }} "sudo nginx -s reload"</code></pre><p></p><p>注意 <code>SERVER_USER_GATEWAY</code> 和 <code>SERVER_HOST_GATEWAY</code> 分别是你的服务器的用户名和地址(IP 或域名),<code>CERT_REMOTE_DIR</code> 是你准备把证书上传到的服务器文件夹,需要先创建对应的文件夹。这三个变量也像上面一样添加到 GitHub Secrets 中。</p><p>步骤的第一行 <code>if: steps.issue-cert.outcome == 'success'</code> 是要确保前面申请证书成功才会执行后续的操作。</p><p>最后一行的 <code>sudo nginx -s reload</code> 作用是让服务器上的 Nginx 重新加载配置以及证书,没有这一句,即使证书文件已经上传了,Nginx 仍会继续使用老证书,直到下一次重载或重启。</p><p>到这儿,证书的申请 → 上传 → 应用流程就基本完整了,如果你有多台服务器在使用这个证书,可以继续添加上传任务。</p><p></p><h2>更新腾讯云上的证书</h2><p>我在使用腾讯云 CDN,所以也研究了一下如何自动更新腾讯云的上传证书。其他云服务商应该也有类似的接口。</p><p>另外,腾讯云的 EdgeOne 已经可以自动申请和更新免费证书了,无需再自己上传,不过传统的 CDN 服务目前似乎还需要自己管理免费证书。</p><p>相关代码如下:</p><pre><code> - name: Upload to Tencent Cloud if: steps.issue-cert.outcome == 'success' continue-on-error: true run: | pip install tccli # 配置腾讯云 CLI tccli configure set secretId ${{ secrets.TENCENT_SECRET_ID }} tccli configure set secretKey ${{ secrets.TENCENT_SECRET_KEY }} tccli configure set region ap-guangzhou CERT=$(cat ~/.acme.sh/oldj.net_ecc/fullchain.cer | base64 -w 0) KEY=$(cat ~/.acme.sh/oldj.net_ecc/oldj.net.key | base64 -w 0) # 上传新证书 RESULT=$(tccli ssl UploadCertificate \ --CertificatePublicKey "$(cat ~/.acme.sh/oldj.net_ecc/fullchain.cer)" \ --CertificatePrivateKey "$(cat ~/.acme.sh/oldj.net_ecc/oldj.net.key)" \ --Alias "oldj.net-wildcard-$(date +%Y%m%d)") NEW_CERT_ID=$(echo "$RESULT" | jq -r '.CertificateId') echo "New certificate ID: $NEW_CERT_ID" # 如果有旧证书 ID,自动替换关联资源 if [ -n "${{ secrets.TENCENT_OLD_CERT_ID }}" ]; then tccli ssl UpdateCertificateInstance \ --OldCertificateId ${{ secrets.TENCENT_OLD_CERT_ID }} \ --ResourceTypes '["cdn"]' \ --CertificateId "$NEW_CERT_ID" echo "Associated resources updated from ${{ secrets.TENCENT_OLD_CERT_ID }} to $NEW_CERT_ID" fi # 自动更新 Secret 中的证书 ID,供下次续期使用 echo "$NEW_CERT_ID" | gh secret set TENCENT_OLD_CERT_ID echo "TENCENT_OLD_CERT_ID updated to: $NEW_CERT_ID" env: GH_TOKEN: ${{ secrets.GH_PAT }} GH_REPO: ${{ github.repository }}</code></pre><p></p><p>请注意将代码中的证书路径(本例中是 <code>oldj.net_ecc</code>)替换为你的实际路径。</p><p>这段代码中使用了腾讯云的 <code>tccli</code> 来实现证书上传和更新工作,和上面类似,你需要先设置一些 Secrets:</p><ul><li><p><code>TENCENT_SECRET_ID</code> 腾讯云 API SecretId</p></li><li><p><code>TENCENT_SECRET_KEY</code> 腾讯云 API SecretKey</p></li><li><p><code>TENCENT_OLD_CERT_ID</code> 当前正在使用的证书 ID(如果仅上传,不需要自动替换老证书,可不填此项)</p></li></ul><p>其中 <code>TENCENT_SECRET_*</code>可以前往腾讯云 API 密钥管理页面生成,建议创建子账号,授予 <code>QcloudSSLFullAccess</code> 权限,以及你需要更新的资源的权限,比如 <code>QcloudCDNFullAccess</code> 。</p><p>上面的 <code>--ResourceTypes</code> 用于指定要更新的资源,这儿我只写了 CDN,你可以根据需要调整。</p><p>首次运行前,需要你先设置一下要更新的证书 ID。由于每次自动更新证书之后,证书 ID 都会变化,为了实现完全自动化,这儿添加了一个 <code>GH_PAT</code> 参数,用于记录你的 <code>GitHub Personal Access Token</code> 。</p><p>这个 <code>GitHub Personal Access Token</code> 的创建方式如下:</p><ol><li><p>进入 GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens</p></li><li><p>选择对应的组织以及仓库,权限勾选 Secrets → Read and Write</p></li><li><p>生成后,将 Token 存入仓库 Secret 中,名称是 <code>GH_PAT</code></p></li></ol><p>这样自动更新腾讯云上的证书的流程便也自动化了。</p><p></p>

2026/4/10
阅读更多

AB 工作法

<h2>什么是 AB 工作法</h2> <p>想象一下,你有两个工作台:</p> <p><strong>A 工作台</strong>上摆着你最重要的那个大项目——可能是一个复杂的架构设计,可能是一本书的核心章节,或者是一个需要深度思考的研究课题。这个工作需要你全神贯注,不被打扰。</p> <p><strong>B 工作台</strong>上则堆着其他各种各样的事情——紧急但不复杂的需求、日常维护工作、突发的小任务、需要回复的邮件等等。这些事情重要但不需要长时间的深度投入。</p> <p>AB 工作法的核心思路是:将各项工作放合适的工作台上,然后你的注意力在这两个工作台之间定期切换。比如上午在 A 工作台专注做深度工作,下午切换到 B 工作台处理各种杂事,或者今天做 A,明天做 B,关键是有节奏地轮换,而不是随机跳跃。</p> <p>这个方法特别适合那些每天要处理大量不同类型事务的人,——你需要推进重要的长期项目,同时又不能让其他事情堆积成山。如果你的工作是流水线式的,按先来后到依次处理就好,可能用不上这个方法。</p> <h2>它要解决什么问题</h2> <p>我们先来看一个常见的困境。</p> <p>假设你是一位程序员,你的待处理工作列表很长,每项工作的重要性、紧急程度、耗时都不同。你决定按串行方式处理,每次挑一项最重要的处理,完成之后再挑选下一项。这个策略通常运行良好,但偶尔也会遇到麻烦:某项工作特别耗时,你在上面花了一两周,其他耗时短的小任务就都卡住了,有些原本不紧急的需求因此变得紧急起来。</p> <p>这背后是两个相互冲突的需求:</p> <p>一方面,<strong>我们需要专注</strong>。频繁切换任务的成本很高,因为大脑需要时间重新加载上下文,频繁切换不仅降低效率,还会消耗大量精力,导致精神疲劳。</p> <p>另一方面,<strong>我们又不能长时间陷入一件事</strong>。如果完全沉浸在一个大项目中,其他重要但耗时短的任务会被无限期推迟,最终可能演变成紧急问题,甚至引发更大的麻烦。</p> <p>AB 工作法试图在这两者之间找到平衡。</p> <h2>核心机制</h2> <p>回到“两个工作台”的比喻。AB 工作法的运作方式是:</p> <p><strong>建立两个工作台</strong></p> <p>你需要把所有工作分类放到两个工作台上:</p> <ul> <li> <p><strong>A 工作台</strong>:存放那些需要深度工作的任务——最重要、最复杂、预计耗时较长的工作。这些任务摆在 A 工作台上,等待你在专注时段处理。</p> </li> <li> <p><strong>B 工作台</strong>:存放其他所有事务——重要但不太复杂的任务、紧急但耗时短的需求、日常维护工作、突发事项等等。这些任务堆在 B 工作台上,可以在较短时间内逐个处理。</p> </li> </ul> <p><strong>定期轮换</strong></p> <p>你需要设定一个固定的轮换周期。在 A 时段,你只在 A 工作台工作,专注处理深度任务;在 B 时段,你切换到 B 工作台,灵活处理各种事务。这种有节奏的切换,既保证了主要任务的持续推进,又确保了其他事务不会被长期搁置。</p> <p><strong>保持纪律</strong></p> <p>关键是严格遵守“在哪个工作台就做哪个工作台的事”。在 A 工作台时,即使 B 工作台上有看起来很紧急的事,也要记下来留到 B 时段处理。在 B 工作台时,即使突然对 A 任务有了灵感,也要克制住冲动,留到 A 时段再说。</p> <h2>具体怎么做</h2> <h3>第一步:给两个工作台分配任务</h3> <p>拿出你的工作列表,开始分类:</p> <p><strong>哪些任务该放到 A 工作台?</strong></p> <p>答案是那些“如果不专注投入就很难推进”的工作。对程序员来说,可能是复杂的架构重构;对作家来说,可能是书的核心章节;对研究者来说,可能是关键实验的设计与实施。判断标准是:这个任务需要长时间的连续思考吗?需要进入“心流”状态才能做好吗?如果答案是肯定的,就放到 A 工作台。</p> <p><strong>哪些任务该放到 B 工作台?</strong></p> <p>答案是其他所有工作——包括那些重要但不太复杂的任务、紧急但耗时短的需求、日常维护工作、突发事项等等。不用担心 B 工作台上堆得太满太杂,这个工作台本来就是为处理多样化任务设计的。</p> <h3>第二步:设定轮换节奏</h3> <p>根据你的工作性质和个人习惯,选择一个合适的轮换周期:</p> <p><strong>每半天轮换</strong>:上午在 A 工作台,下午在 B 工作台,或者上午在 B 工作台,下午在 A 工作台。这是最常见的节奏,适合工作环境相对安静、任务复杂度高的情况。</p> <p><strong>每天轮换</strong>:今天在 A 工作台,明天在 B 工作台。如果你的深度任务需要更长的连续时间来建立思考状态,这种方式可能更合适。</p> <p><strong>每个时段轮换</strong>:每个工作时段(2-4 小时)切换一次工作台。适合工作环境多变、突发事件较多的情况。</p> <p><strong>每 2 小时轮换</strong>:节奏更快,但不建议更短——太频繁的切换会让上下文切换成本抵消掉收益。</p> <p><strong>根据效率曲线设置</strong>:选择你每天最高效的时间段使用 A 工作台,其他时段使用 B 工作台。比如,如果你在上午 10 点到下午 3 点精力最充沛、思维最清晰,就把这个时段固定为 A 时段,其余时间处理 B 任务。这种方式能最大化利用你的黄金工作时段。</p> <p><strong>关于时间分配比例</strong></p> <p>A、B 的时间分配不一定是 1:1。如果你的日常事务相对较少,而深度任务需要大量投入,时间分配可以是 2:1、3:1,甚至可以一周的五个工作日,四天都在 A 工作台,剩下一天在 B 工作台。关键是根据实际工作量来设定合理的比例。</p> <p>但有一点很重要:<strong>一旦定好节奏,就不要轻易改动</strong>。频繁调整计划本身就是一种低效的行为,会削弱这个方法的效果。</p> <h3>第三步:严格遵守工作台纪律</h3> <p>这是 AB 工作法成败的关键。</p> <p><strong>在 A 工作台时</strong>:坚决拒绝处理 B 工作台上的事项,即使它们看起来很紧急。你可以把它们记在便签上,但要留到 B 时段再处理。这不是不负责任,而是在保护你的深度工作状态。</p> <p><strong>在 B 工作台时</strong>:不要被 A 工作台上的任务吸引回去,即使你突然有了灵感。把灵感记下来,留到 A 时段再展开。这同样是在保护你的工作模式——B 时段的价值在于灵活机动,如果被大任务拖住,就失去了意义。</p> <p>这种纪律看似僵化,实际上是在保护两种工作模式的完整性。你可以把自己想象成两个不同的人:A 工作台上的你是专注的深度思考者,B 工作台上的你是灵活的问题解决者。</p> <p><strong>真正的紧急情况例外</strong></p> <p>当然,如果遇到真正重要且紧急的突发事件——比如生产系统崩溃、客户的关键问题、或者团队成员急需你的帮助——无论你当前在哪个工作台,都可以立刻停下来去处理。</p> <p>但需要记住:<strong>这种打断应该是罕见的例外,而不是常态</strong>。如果你发现自己经常因为“紧急情况”打断工作,可能需要重新审视:这些事情真的都是既重要又紧急吗?还是因为缺乏边界,让所有事情都变成了紧急?</p> <p>一个简单的判断标准是:如果这件事推迟 2 小时(或推迟到下一个工作台时段)会造成严重后果,那就是真正的紧急情况。如果只是“看起来紧急”但推迟几小时也无妨,那就记下来,留到合适的时段处理。</p> <h3>第四步:动态调整两个工作台</h3> <p>AB 工作法不是一成不变的,需要根据实际情况动态调整:</p> <p><strong>当 A 工作台清空时</strong>:可以从 B 工作台选出最重要、最复杂的任务,把它移到 A 工作台。这样确保你始终有深度任务在推进。</p> <p><strong>当 B 任务需要升级时</strong>:在执行过程中,如果发现某个 B 任务的重要性和复杂度超出了最初的预期,可以把它从 B 工作台移到 A 工作台,作为深度任务来处理。</p> <p><strong>当 A 任务需要降级时</strong>:有时候你可能会发现某个 A 任务其实没那么复杂,或者可以拆分成小块处理,这时也可以把它或者它拆分后的一部分移到 B 工作台。</p> <p>关键是保持两个工作台的动态平衡,确保 A 工作台上始终有需要深度工作的任务,B 工作台上有足够的机动任务。</p> <h3>特殊情况:B 工作台清空了怎么办?</h3> <p>这是一个有意思的问题。如果到了 B 时段,却发现 B 工作台上的事情都处理完了,你有两个合理的选择:</p> <p><strong>选择一:转到 A 工作台</strong></p> <p>既然没有其他事务需要处理,将这段时间用于推进 A 工作台上的任务是很自然的想法,这样可以加快深度任务的进度。</p> <p>不过需要注意的是,如果你经常这样做,要警惕 AB 工作法是否正在退化成单线程工作模式。偶尔为之没问题,但如果成为常态,可能需要重新审视你的时间分配比例。</p> <p><strong>选择二:做“元工作”或休息</strong></p> <p>“元工作”是指那些不属于具体任务,但对工作系统本身有益的活动:整理工作笔记、回顾近期进展、学习新技能、优化工作流程、清理工作环境、或者进行一些前瞻性思考。这些活动往往在忙碌时被忽视,但长期来看对提升工作质量和效率很有帮助。</p> <p>如果找不到合适的“元工作”,那就干脆休息一下,——散散步、冥想、或者做一些轻松的事情,为下一个 A 时段储备精力。</p> <p>最重要的是,不要因为 B 工作台暂时清空就感到焦虑或内疚。这恰恰说明你的工作节奏是健康的,——你既在推进重要的长期项目,又及时处理了其他事务。这正是 AB 工作法追求的理想状态。</p> <h2>为什么 AB 工作法有效</h2> <p>AB 工作法的有效性来自于它对人类认知特性的尊重和对工作现实的妥协:</p> <p><strong>它承认专注的价值</strong></p> <p>通过为深度任务分配专门的时间块,我们能够进入心流状态。在 A 工作台上,你不需要担心其他事情,可以全身心投入到复杂的思考中。这种不被打扰的专注,是完成复杂工作的必要条件。</p> <p><strong>它承认灵活的必要</strong></p> <p>通过 B 工作台的存在,我们不会因为过度专注而忽视其他重要事项。那些看似琐碎但不可或缺的工作——回复邮件、处理临时需求、日常维护——也能得到及时处理,不会堆积成山。</p> <p><strong>它提供了心理缓冲</strong></p> <p>知道“我只需要在这个时段专注于 A 工作台,其他事情稍后会有专门时间处理”,这种确定性能够显著降低焦虑。你不会因为“还有很多其他事没做”而分心,也不会因为“一直在做杂事”而焦虑。</p> <p><strong>它创造了自然的休息点</strong></p> <p>工作台切换本身就是一种认知上的休息。从深度思考切换到处理多个小任务,或者反过来,都能让大脑的不同区域得到交替使用和恢复。这比连续 8 小时做同类工作要健康得多。</p> <h2>适合谁用</h2> <p>AB 工作法特别适合以下人群:</p> <ul> <li> <p>需要同时推进长期项目和处理日常事务的知识工作者</p> </li> <li> <p>工作内容多样、优先级复杂的管理者、创业者或独立开发者</p> </li> <li> <p>需要在创造性工作和事务性工作之间切换的专业人士</p> </li> <li> <p>容易陷入“只做紧急事,忽视重要事”或“只做重要事,忽视紧急事”两个极端的人</p> </li> </ul> <h2>局限性</h2> <p>当然,AB 工作法也有它的局限性:</p> <p>如果你的工作本质上是单线程的(比如流水线工人、客服人员),这个方法可能过于复杂。如果你的工作环境完全不可控、频繁被打断,可能需要先改善工作环境,再尝试这个方法。如果你正处于某个项目的冲刺期,需要全力以赴,暂时放弃 AB 结构、全力投入也是合理的选择。</p> <h2>一些实践建议</h2> <p><strong>从简单的节奏开始</strong></p> <p>如果你是第一次尝试 AB 工作法,建议从每天轮换开始,而不是每小时。今天在 A 工作台,明天在 B 工作台。这样更容易建立习惯,也更容易感受到效果。等习惯养成后,再尝试更短的轮换周期。</p> <p><strong>用工具标记当前工作台</strong></p> <p>可以使用日历、番茄钟或专门的时间管理工具来标记你当前在哪个工作台。视觉提示能够帮助你更好地保持纪律。有些人会在桌面上放两个不同颜色的便签纸,A 时段翻开绿色,B 时段翻开黄色,这种物理提示也很有效。</p> <p><strong>每周回顾与调整</strong></p> <p>每周花 15 分钟回顾一下:A 工作台上的任务推进得如何?B 工作台是否有积压?两个工作台的任务划分是否合理?轮换节奏是否合适?根据实际情况调整策略。这种定期回顾能帮你不断优化自己的工作节奏。</p> <p><strong>让同事知道你的节奏</strong></p> <p>如果你在团队中工作,让同事知道你的工作方式。比如在日历上标注“深度工作时段(A 工作台)”,让他们知道这段时间最好不要打扰你。大多数同事会理解并尊重这种安排,因为他们自己可能也有类似的需求。</p> <p><strong>保持弹性</strong></p> <p>AB 工作法是一个框架,不是枷锁。真正的紧急情况出现时,当然可以打破规则。关键是这种打破应该是例外,而不是常态。如果你发现自己经常打破规则,可能需要重新审视你的时间分配或任务分类。</p> <h2>扩展:可以有更多工作台吗?</h2> <p>AB 工作法的核心是“两个工作台”,但这不是唯一的可能。如果你的工作情况更复杂,完全可以扩展为三个、四个甚至更多工作台。</p> <p><strong>什么时候需要更多工作台?</strong></p> <p>如果你的工作内容跨越多个几乎不相关的领域,每个领域都有自己的深度任务和日常事务,那么多个工作台可能更合适。</p> <p>比如,假设你同时负责产品开发和市场推广两个领域,你可以采用 ABC 工作法:</p> <ul> <li> <p><strong>A 工作台</strong>:产品开发方面的深度工作——架构设计、核心功能开发、技术难题攻关</p> </li> <li> <p><strong>B 工作台</strong>:市场推广方面的深度工作——营销策略规划、重要内容创作、关键活动策划</p> </li> <li> <p><strong>C 工作台</strong>:所有其他日常事务——邮件回复、会议、临时需求、日常维护</p> </li> </ul> <p>然后制定一个轮换策略,比如:周一周二在 A 工作台,周三周四在 B 工作台,周五在 C 工作台。或者每天上午在 A 工作台,下午在 B 工作台,晚上处理 C 工作台的事情。</p> <p><strong>需要注意的是</strong></p> <p>工作台越多,轮换的复杂度就越高,上下文切换的成本也会增加。大多数情况下,两个工作台就足够了。只有当你确实需要在多个完全不同的领域之间切换,并且每个领域都有足够的工作量时,才考虑增加工作台数量。</p> <p>一个简单的判断标准:如果你发现自己在某个“深度工作台”上经常没事可做,或者某两个工作台上的任务其实可以合并处理,那就说明工作台分得太细了,不如简化回两个工作台的结构。</p> <h2>写在最后</h2> <p>AB 工作法的本质,是在专注与灵活、深度与广度、长期与短期之间寻找平衡。它不是什么神奇的生产力秘诀,而是一种务实的工作组织方式。</p> <p>想象你面前真的有两个工作台。A 工作台干净整洁,只摆着少量需要全神贯注的大项目;B 工作台上堆着各种各样的事情,等待你灵活处理。你在这两个工作台之间有节奏地切换,既不会因为过度专注而忽视其他事务,也不会因为琐事缠身而无法推进重要项目。</p> <p>在这个信息过载、任务繁杂的时代,我们既需要能够深入思考的专注时光,也需要能够快速响应的灵活机制。AB 工作法提供了一个简单但有效的框架,让这两种看似矛盾的需求能够在同一个人的工作节奏中和谐共存。</p> <p>如果你也面临着“重要的大项目总是被琐事打断”或“专注于大项目时其他事情都荒废了”的困扰,不妨试试 AB 工作法。给自己两周时间实验,你可能会发现一种全新的工作节奏。</p>

2026/3/25
阅读更多

使用 uv 管理 Python 依赖

<p>我有一个运行了好几年的 Django 项目,之前一直在使用默认的 pip 管理和安装依赖,最近切换到了 <a href="https://github.com/astral-sh/uv">uv</a>,感觉还不错,在这儿记录一下。</p> <h2 id="uv">什么是 uv</h2> <p>根据官网的介绍,uv 是一个 Python 包以及项目管理器,非常快,使用 Rust 开发。</p> <p>安装和管理依赖只是它的功能之一,除此之外,它还可以创建虚拟环境,即可以取代 pip + virtualenv 的功能。</p> <p>我测试了一下,uv 确实比 pip 快了很多。在使用相同的镜像源,且都是纯净的 docker 环境下,使用 pip 安装项目的依赖花了约 88 秒,但使用 uv 只用了 13 秒。</p> <p>不过,安装依赖并不是一个高频操作,多花一点时间一般不是什么痛点,uv 更吸引人的是它简化了很多工作,比如内置了 Python 多版本安装以及虚拟环境管理,且能保证环境的可复现性,这就让 Python 项目的开发和发布工作简单了很多。</p> <h2 id="uv_1">uv 的安装和初始化</h2> <p>在 macOS 或 Linux 上,可以使用以下命令安装 uv:</p> <pre><code class="language-bash">curl -LsSf https://astral.sh/uv/install.sh | sh </code></pre> <p>Windows 上的命令如下:</p> <pre><code class="language-bash"># On Windows. powershell -ExecutionPolicy ByPass -c &quot;irm https://astral.sh/uv/install.ps1 | iex&quot; </code></pre> <p>安装完成之后,可以使用以下命令初始化一个新项目:</p> <pre><code class="language-bash">uv init my-project </code></pre> <p>如果你的项目已经存在,也可以直接进入项目根目录,执行以下命令:</p> <pre><code class="language-bash">uv init </code></pre> <p>如果项目中已经有 <code>requirements.txt</code>,想改用 uv 进行管理,可以在项目根目录执行以下命令:</p> <pre><code class="language-bash">uv pip install -r requirements.txt </code></pre> <p>也可以直接执行:</p> <pre><code class="language-bash">uv sync </code></pre> <p>初始化后,uv 会在项目根目录下生成一个 <code>pyproject.toml</code> 文件,其中包含了项目的基本信息以及依赖项。</p> <p>可以运行以下命令,锁定依赖项:</p> <pre><code class="language-bash">uv lock </code></pre> <p>这个命令将会在项目根目录下生成 <code>uv.lock</code> 文件,类似 Node.js 的 <code>package-lock.json</code>,其中包含了项目依赖的各个第三方库以及版本号,确保下次安装时安装的包相同。</p> <h2 id="_1">虚拟环境</h2> <p>Python 默认是安装在系统中的,使用 pip 安装依赖时,默认也会全局安装,在很多情况下,尤其是需要维护多个项目时,这显然不是我们期望的,此时可以使用虚拟环境。</p> <p>在 uv 中使用虚拟环境很简单。</p> <p>首先,可以使用 uv 安装多个不同版本的 Python:</p> <pre><code class="language-bash">uv python install 3.10 3.11 3.12 </code></pre> <p>然后,可以通过类似下面的命令安装虚拟环境:</p> <pre><code class="language-bash">uv venv --python 3.12.0 </code></pre> <p>可以直接在项目根目录下执行这个命令,执行成功之后,项目根目录下会生成一个 <code>.venv</code> 文件夹,包含这个环境的所有信息,之后安装的包也会保存在这个文件夹下,记得将这个文件夹添加到 <code>.gitignore</code> 中。</p> <p>如果你熟悉 Node.js,会发现这个 <code>.venv</code> 文件夹和 Node.js 的 <code>node_modules</code> 文件夹功能类似,且它更进一步,不仅包含依赖,还能包含当前项目所需的 Python 本身。</p> <p>使用以下命令可以用虚拟环境中的 Python 执行指定脚本:</p> <pre><code class="language-bash">uv run example.py </code></pre> <p>如果你在终端中访问项目,可以使用以下命令激活当前 Python 虚拟环境:</p> <pre><code class="language-bash">source .venv/bin/activate </code></pre> <p>激活虚拟环境之后,可以直接用类似 <code>python example.py</code> 的方式来运行项目中的脚本。</p> <p>使用现代 IDE(比如 PyCharm、VSCode 等)打开这个项目时,IDE 一般都能自动识别项目中的 <code>.venv</code> 虚拟环境。</p> <h2 id="_2">安装依赖</h2> <p>配置好环境后,就可以使用类似下面的命令安装依赖了:</p> <pre><code>uv add django </code></pre> <p>这信命令会下载对应的包并安装在 <code>.venv</code>中,安装成功之后,会修改 <code>pyproject.toml</code> 和 <code>uv.lock</code> 文件。</p> <p>如果你刚将代码从仓库中拉到本地,项目中已经有了 <code>pyproject.toml</code> 和 <code>uv.lock</code>,那么只需执行以下命令即可安装所有依赖:</p> <pre><code class="language-bash">uv sync </code></pre> <p>注意,这个命令会根据 <code>pyproject.toml</code> 解析和下载依赖,有可能会改进 <code>uv.lock</code>。如果是在生产环境,你希望<strong>严格</strong>按照 <code>uv.lock</code> 中的版本安装依赖,可以使用以下命令:</p> <pre><code class="language-bash">uv sync --frozen </code></pre> <h2 id="docker-uv">在 docker 中使用 uv</h2> <p>如果你的项目需要使用 docker 发布,还有一些额外需要注意的事项。</p> <p>如果你的服务器在国内,那么可能需要使用国内 pypi 镜像,uv 中要指定镜像很简单,设置相应的环境变量即可,例如下面设置使用了阿里云的镜像:</p> <pre><code class="language-bash">ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ ENV UV_TRUSTED_HOST=mirrors.aliyun.com </code></pre> <p>在 docker 中安装依赖时,大体上有两种方式,一种是使用 <code>uv sync</code> 命令,如下所示:</p> <pre><code class="language-bash">WORKDIR /code/ COPY pyproject.toml uv.lock /code/ # 安装依赖 RUN uv sync --frozen --no-dev ENV PATH=&quot;/code/.venv/bin:$PATH&quot; </code></pre> <p>这种方式会在当前目录下创建虚拟环境,所有依赖都将安装到 <code>.venv</code> 目录下,因此需要将对应的目录加入 <code>PATH</code>。这种方式安装的依赖将严格遵守 <code>uv.lock</code> 中的版本限制,最为可靠。</p> <p>或者,也可以选择将依赖直接安装到系统环境中:</p> <pre><code class="language-bash">RUN uv pip install --no-cache-dir --system . </code></pre> <p>注意那个 <code>--system</code> 参数,这种方式会将各依赖包安装到全局目录,如果你的 docker 中只有这一个 Python 项目,且不想使用虚拟环境,也可以使用这种方式安装。不过,这种方式安装时虽然也会参考 <code>uv.lock</code>,但并不保证各依赖的版本和 <code>uv.lock</code> 中严格相同。</p> <h2 id="_3">小结</h2> <p>Python 发布迄今已有三十余年,一开始并没有第三方包的安装和管理工具,这和 Node.js 一发布就自带 npm 不同。如果你使用 Python 的时间较早,可能还会记得曾经有一个叫 easy_install 的工具用于安装 Python 的第三方包。</p> <p>约 2008 年,pip 发布,随后在 2014 年被 Python 官方集成到 3.4 版中(以及 2.7.9 中),Python 这才有了一个官方的依赖管理工具。不过 pip 并不完美,主要是依赖解析能力较弱,无法保证每次安装后的环境完全相同,同时安装速度也有一些慢,因此后续又出现了一些新的依赖管理工具,比如 poetry、uv 等。</p> <p>目前,开发 Python 项目的最佳实践是为每个项目创建独立的虚拟环境,将项目所需的依赖安装在该环境中,并通过依赖管理文件记录依赖,以确保隔离、可复现和可移植性。这些工作都可以使用 uv 完成,如果你正在开发或维护一个 Python 项目,不妨试试 uv。</p>

2025/11/29
阅读更多

富文本框架体验

<p>由于项目中要使用富文本编辑器,过去一段时间我深入研究了一下几个知名的富文本框架,在这儿写一下体验。</p> <p>这不是一个全面的分析,仅代表个人观点。</p> <h2 id="quill">Quill</h2> <p>我最开始尝试的是 <a href="https://quilljs.com/">Quill</a> 项目,因为它看起来比较简单同时又足够强大,更重要的是它还有一个 <a href="https://pub.dev/packages/flutter_quill">Flutter 版本</a>,可用于移动端应用的富文本开发。</p> <p>实际体验上,Quill 确实比较容易上手,根据文档很容易就能写出一个基本可用的富文本编辑器来。当然,如果要添加更多复杂的或自定义功能,就要继续深入研究了。</p> <p>Quill 的数据结构比较特别,它自定义了一个名为 <a href="https://quilljs.com/docs/delta">Delta</a> 的 JSON 格式用于描述文档,这种格式概念很简单,比如只有 <code>insert</code>、<code>delete</code>、<code>retain</code> 三种操作,但理论上足以描述任意复杂的文档格式以及修改。</p> <p>在 Delta 的基础上,Quill 的文档可以被认为是一个“流”,只需一个数字就可以表示文档的任意位置,这让使用代码来查找、修改文档的操作变得非常简单。</p> <p>一切看起来都很美好,不过当我继续深入,想实现一些更复杂的功能之时,逐渐发现 Quill 存在一些较为严重的不足。</p> <p>第一个也是最大的不足,是 Quill 缺少“装饰器”设计。</p> <p>所谓“装饰器”,是给文档中指定内容临时添加一些样式,但又不会影响文档数据的功能。</p> <p>举例来说,实现关键词搜索时,我们需要把文档中所有匹配的关键词全部高亮显示(比如背景显示为黄色),但这个高亮样式只是临时性的,需要与富文本中原本就有的样式区分开来,添加这种装饰性的样式时,不需要触发文档的 <code>onChange</code> 事件,此时如果获取文档的内容,得到的数据中也不应该包含装饰性的样式。</p> <p>缺少了装饰器功能,如果想高亮匹配的搜索关键词,就只能直接修改文档数据,在文档的原始数据中为这些关键词添加样式,随后在搜索结束时再去除这些样式。这便需要开发者自行添加和维护这些临时样式的状态,并区分哪些修改是真正的修改可以保存,哪些则只是装饰性修改不需要保存,显然,这会让代码逻辑变得复杂,且由于不是框架底层原生支持,性能上也会差很多。</p> <p>第二个问题则和 Delta 格式有关。</p> <p>使用了一段时间之后,我发现 Delta 这种格式用于描述纯文本或简单富文本的修改确实很方便,生成的 JSON 也很容易阅读,但如果要描述带有复杂嵌套结构的 HTML,Delta 反而会让问题复杂化。</p> <p>同时,应该是实现上还存在 bug,当文档中有一些格式存在重叠、嵌套时,如果对它们再次格式化,可能会让内容变得混乱。对此我在 Quill 的 GitHub 仓库中提交了一个 <a href="https://github.com/slab/quill/issues/4574">issue</a> ,不过暂时没有得到回应。</p> <h2 id="slate">Slate</h2> <p>随后,我又尝试了 <a href="http://slatejs.org/">Slate</a> ,一个基于 React 的富文本框架。</p> <p>Slate 非常强大,且相较 Quill 自由度更高,基本上可以用它实现任何想要的功能。它自带装饰器(Decorations),因此,诸如高亮搜索关键词等功能的实现都变得很容易。对应地,它的概念也比 Quill 多了一些,学习成本稍高。</p> <p>我用 Slate 基本实现了整个需求,原本已经基本准备正式使用它了,不过也许是我对它的理解还不够,遇到了一些始终没能处理好的小问题,虽然不影响主要功能,但却让编辑器在体验上总是差了那么一点。</p> <p>比如,有时候选中了文档内容,鼠标点在空白处时不会取消选中,要再点一次才行。还有一些中文输入法似乎与它存在兼容问题,一些操作会让整个编辑器崩溃,我研究了很久也找不到修复或者处理方法,只能在外层容器捕获错误,提示用户刷新页面。</p> <h2 id="prosemirror">ProseMirror</h2> <p>最后,我决定转向 <a href="https://prosemirror.net/">ProseMirror</a> 。</p> <p>ProseMirror 是一个老牌富文本框架,它的作者还写过知名的代码编辑器 CodeMirror。ProseMirror 非常强大,但因为学习曲线相对陡峭,因此劝退了很多人,包括之前的我也一度先尝试 Quill、Slate 等方案。</p> <p>也有一些基于 ProseMirror 的富文本框架,比如 <a href="https://tiptap.dev/">TipTap</a> ,这也从另一个角度展示了 ProseMirror 的强大。</p> <p>ProseMirror 的文档对新手不太友好,且它有一些自已的概念,比如文档的结构由 Schema 定义和描述,因此需要花一些时间才能上手,不过,上手之后会发现它的文档其实非常详细,基本可以从中找到需要的一切信息。</p> <p>在使用 ProseMirror 将需求重新实现之后,我发现在使用 Slate 遇到的那些恼人的小问题要么没有了,要么有解决方案,而且在处理有较多装饰器的长文档时,ProseMirror 的性能似乎更好一些。</p> <p>当然,ProseMirror 也不能说十全十美,不过,在深度使用了几个流行的富文本编辑器框架之后,个人觉得 ProseMirror 是综合来说最强大也最值得学习的。</p> <h2 id="_1">小结</h2> <p>富文本编辑器是前端开发中最复杂也最难的主题之一,幸运的是多数情况下我们不需要完全从头开始开发,而是可以选择一个基础框架,基于它进行二次开发。</p> <p>网上有很多富文本编辑器框架的评测,你可以根据自己的需求以及实际情况进行选择。</p> <p>如果你希望尽快完成项目,且你的文档结构不会很复杂,也没有装饰器等需求,可以考虑 Quill。</p> <p>如果你的需求比较复杂,且在使用 React,可以考虑 Slate。</p> <p>最后,如果你的需求很复杂,且你的时间不那么紧张,推荐选择 ProseMirror。</p>

2025/6/21
阅读更多

使用函数计算运行定时任务

<p>上一篇博客提到我在<a href="/article/2025/04/20/deploy-website-with-dokploy">使用 Dokploy 部署网站服务</a>,但 Dokploy 不支持定时任务,于是只能创建普通服务,并在内部使用脚本定时执行命令。最近发现,将这些定时任务放在函数计算中执行可能是更好的选择。</p> <p>我的服务都跑在阿里云上,下面介绍的也是阿里云函数计算。</p> <h2 id="_1">创建函数</h2> <p>要创建一个函数,在阿里云函数界面后台,点击创建函数按钮即可,如下图所示:</p> <p><center> <img src="https://s.oldj.net/upload/2025/05/01/01.png" width="560" alt="" /> </center></p> <p>在随后的界面中,选择“任务函数”类型。</p> <p><center> <img src="https://s.oldj.net/upload/2025/05/01/02.png" width="315" alt="" /> </center></p> <p>然后,函数代码部分可根据需要选择类型,比如可以使用 ACR 中的 Docker 镜像。</p> <p><center> <img src="https://s.oldj.net/upload/2025/05/01/03.png" width="648" alt="" /> </center></p> <p>需要注意的是,无论是上传代码还是使用 Docker 镜像,都要确保对应的代码能提供一个 HTTP 服务,因为定时任务执行的入口即是这个 HTTP 服务。</p> <p>其他还有环境变量等配置,根据你的实际情况填写即可。</p> <h2 id="_2">函数入口</h2> <p>使用函数计算的定时任务,需要你的代码提供一个 HTTP 服务,定时任务执行时,会以 POST 的方式请求 <code>/invoke</code> 路径,即类似下面这样的请求:</p> <pre><code class="language-bash">curl -X &quot;POST&quot; &quot;http://localhost:8050/invoke&quot; \ -H 'Content-Type: application/json' \ -d $'{ &quot;payload&quot;: &quot;YOUR_PAYLOAD&quot;, &quot;triggerName&quot;: &quot;trigger-name&quot;, &quot;triggerTime&quot;: &quot;2025-04-27T03:12:45Z&quot; }' </code></pre> <p>当然,真实的请求还有很多 HTTP 头信息。</p> <p>你需要在代码中实现 <code>/invoke</code> 接口,并在其中执行定时任务。在函数计算后台,可以设置超时时间等属性。</p> <p>注意其中的 <code>payload</code> 字段,后面在设置定时触发器时,可以自定义传入的 <code>palyload</code> 信息。</p> <h2 id="_3">设置定时触发器</h2> <p>添加函数之后,即可在配置界面设置触发器。</p> <p><center> <img src="https://s.oldj.net/upload/2025/05/01/04.png" width="553" alt="" /> </center></p> <p>函数计算支持多种触发器,在这儿,我们选择定时触发器即可。</p> <p><center> <img src="https://s.oldj.net/upload/2025/05/01/05.png" width="790" alt="" /> </center></p> <p>其中最后一个字段“触发消息”,其中填写的内容即是上面 <code>payload</code> 参数的值。注意这儿传递的是普通字符串,而不是 JSON,收到之后可根据需要做一个解析。</p> <p>如果你在同一个函数中有多个用途不同的触发器,可以通过 <code>payload</code> 参数进行区分。</p> <p>设置好之后,在函数详情界面可以看到类似下面的图示。</p> <p><center> <img src="https://s.oldj.net/upload/2025/05/01/06.png" width="974" alt="" /> </center></p> <p>如果一切顺利,定时任务就添加成功了,稍后可以在日志页面看到执行记录。</p> <p><center> <img src="https://s.oldj.net/upload/2025/05/01/07.png" width="1600" alt="" /> </center></p> <h2 id="_4">更新函数</h2> <p>如果你的函数使用的是 ACR 中的 Docker 镜像,当推送了新的镜像时,函数计算的版本不会自动更新,需要你登录网站后台手动修改,或者调用函数计算的 API 进行设置。</p> <p>每次手动修改是一件很麻烦的事,建议使用 API,以便和现有的发布流程结合起来。你可以先安装 <a href="https://help.aliyun.com/zh/cli/">aliyun-cli</a> 命令行工具,然后执行类似下面的命令:</p> <pre><code class="language-bash">aliyun fc PUT /2023-03-30/functions/YOUR_FC --region cn-shanghai --header &quot;Content-Type=application/json;&quot; --body &quot;{\&quot;tracingConfig\&quot;:{},\&quot;customContainerConfig\&quot;:{\&quot;image\&quot;:\&quot;registry-vpc.cn-shanghai.aliyuncs.com/XXX/YYY:1.2.3\&quot;}}&quot; </code></pre> <p>请注意将其中的参数值替换为你的项目中的值。</p> <h2 id="_5">小结</h2> <p>有一些定时任务(比如清理老数据、备份用户数据等)比较耗费资源,将它们迁移到函数计算中可以减少主服务器的负担,是一个不错的实践。</p> <p>函数计算是按量收费的,多数情况下,定时任务使用函数计算应该比专门买一台服务器划算,不过也不要大意,请做好优化,同时注意关注每日的用量。</p>

2025/5/2
阅读更多

使用 Dokploy 部署网站服务

<p>之前的几年我一直在使用 <a href="https://oldj.net/article/2022/04/17/install-k3s-and-rancher/">K3s + Rancher</a> 的组合来管理网站服务,不过前段时间迁移到了 <a href="https://dokploy.com/zh-Hans">Dokploy</a>,在这儿记录一下要点。</p> <h2 id="_1">为什么迁移?</h2> <p>K3s + Rancher 的组合挺好,几年来一直运行稳定,不过对像我这样的非专业运维来说还是有点太复杂了,事实上几年来,我一直只在使用这个组合的一些最基础的功能。</p> <p>去年看到有人介绍 Dokploy,了解了一下之后,发现它非常适合我的使用场景,同时又足够简单,于是花了一点时间做了研究,并最终决定迁移到 Dokploy。</p> <p>除了 Dokploy 之外,还有 <a href="https://www.coolify.io/">Coolify</a> 等产品也不错,而且功能更多一些,读者朋友如果有需要也可以试一试。</p> <h2 id="_2">云服务还是自托管?</h2> <p>Dokploy 提供了云服务,订阅之后可通过他们的云服务管理自己的服务器。</p> <p>云服务听起来是个不错的选择,可以减少自己运维的时间成本,我也花了 $4.5 订阅了一个月体验了一番。不过 Dokploy 的云服务在海外,我的服务器在国内,两者之间通讯不畅,因此体验并不是很好。</p> <p>最后,我选择了自托管服务,将 Dokploy 和网站服务安装在同一个网络中。</p> <h2 id="dokploy">安装 Dokploy</h2> <p>Dokploy 的安装很简单,在一台干净的服务器上运行以下命令即可:</p> <pre><code class="language-bash">curl -sSL https://dokploy.com/install.sh | sh </code></pre> <p>为了确保 Dokploy 能顺利运行,这台服务器建议至少要 2 CPU + 2 G 内存。</p> <p>如果你的服务器在国内,安装时可能耗时较长,可以添加国内的 docker 镜像,比如修改 <code>/etc/docker/daemon.json</code> 文件,添加以下内容:</p> <pre><code class="language-json">{ &quot;registry-mirrors&quot;: [ &quot;https://docker.1ms.run&quot; ] } </code></pre> <p>安装完成之后,即可通过 <code>http://{服务器 IP}:3000</code> 的形式访问 Dokploy 后台。</p> <h2 id="_3">添加服务器</h2> <p>Dokploy 成功安装后,马上就可以开始创建应用。不过,这时创建的应用会和 Dokploy 安装在同一台服务器上,你也可以在 Dokploy 后台添加新的服务器,并将应用添加到新服务器上。</p> <p>个人建议用一台服务器专门运行 Dokploy,然后在 Remote Servers 面板中添加其他服务器。</p> <p>添加服务器之后,还需要在 Actions 菜单中点击 Setup Server,并根据提示进行设置。</p> <p><center> <img src="https://s.oldj.net/upload/2025/04/20/setup_server.png" width="420" alt="" /> </center></p> <p>其中 Deployments 那个步骤可能耗时会很长,可以考虑点击 Modify Script,将脚本复制到对应的服务器上手动执行。</p> <p><center> <img src="https://s.oldj.net/upload/2025/04/20/setup_server_deployments.png" width="886" alt="" /> </center></p> <h2 id="_4">添加服务</h2> <p>添加完服务器之后,就可以添加项目,随后在项目中添加服务了。</p> <p>添加服务这儿,最重要的一个设置是 Provider,即设置代码的来源。</p> <p>Dokploy 支持多种常见的源,比如 Github,配置好之后只需向指定仓库和分支推送代码,Dokploy 就会自动拉取并构建代码,就像 Vercel 一样。</p> <p>对小项目来说,这样的方式自然是很方便的,不过也可以用 Docker 作为 Provider,并使用第三方镜像服务。这样主要有两个好处:</p> <ol> <li>镜像的构建工作在第三方执行,不会占用线上服务器资源;</li> <li>第三方构建镜像时可以打上版本号 tag,后续回滚操作将会很方便。</li> </ol> <p>我使用的是阿里云的容器镜像服务,填写方式类似下图:</p> <p><center> <img src="https://s.oldj.net/upload/2025/04/20/provider_docker.png" width="865" alt="" /> </center></p> <h2 id="_5">更新服务</h2> <p>Dokploy 提供了丰富的 API,几乎所有操作都可以通过 API 完成。当某个服务需要更新时,可以登录网站手动修改相关值,也可以使用 API 更新。</p> <p>比如,如果一个服务的 Provider 是 Docker,可以用类似下面的请求进行修改:</p> <pre><code class="language-shell">curl -X &quot;POST&quot; &quot;https://your-dokploy/api/application.saveDockerProvider&quot; \ -H 'x-api-key: $YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d $'{ &quot;applicationId&quot;: &quot;$APP_ID&quot;, &quot;dockerImage&quot;: &quot;$DOCKER_URL&quot; }' </code></pre> <p>有几个注意点:</p> <ol> <li>授权头信息是 <code>x-api-key: xxx...</code>,而不是常见的 <code>Authorization: Bearer xxx...</code> 。</li> <li><code>applicationId</code> 的值在 URL 中,在界面上暂时没有显示。</li> </ol> <p>比如某个服务的地址是 <code>https://your-dokploy/dashboard/project/aaa/services/application/bbb</code>,地址最后的 <code>bbb</code> 就是 <code>applicationId</code>。</p> <p>通过 API 的方式,可以很方便地将服务的发布、回滚等操作集中到一处管理,或者与你现有的服务集成。</p> <h2 id="dokploy_1">升级 Dokploy</h2> <p>Dokploy 本身也在不断迭代更新,一段时间之后,你可能需要升级 Dokploy。</p> <p>Dokploy 后台提供了自助升级服务,不过由于网络原因,在国内服务器上这个升级可能会失败,也可以登录到服务器后使用以下命令升级:</p> <pre><code class="language-bash">curl -sSL https://dokploy.com/install.sh | sh -s update </code></pre> <h2 id="_6">使用小结</h2> <p>使用 Dokploy 已经有一段时间了,整体而言还是很满意的,相对其他方案它很容易上手,且足够稳定,可用于生产环境。</p> <p>不足是暂时还不支持定时任务,不过可以通过启动一个普通服务并在其中运行定时脚本的方式解决。</p> <p>如果你有类似的需求,不妨也试一试 Dokploy。</p>

2025/4/20
阅读更多

Electron 中的 Kiosk 窗口

<p>最近在产品中用到了 Electron 中的 Kiosk 模式,记录一下要点。</p> <h2>什么是 Kiosk 模式?</h2> <p>Kiosk 模式是一种专门为限制用户操作而设计的应用运行模式,通常用于构建锁定的全屏应用程序,禁止用户访问系统其他功能或退出应用。在这种模式下,应用程序占据整个屏幕,并且用户无法通过常见的方式(如键盘快捷键、窗口控制按钮等)退出或切换到其他应用。</p> <p>Kiosk 模式的主要用途是为用户提供一个专注且受限的操作环境,避免对系统的其他部分产生干扰。</p> <h2>哪些场景下需要使用 Kiosk 模式?</h2> <p>Kiosk 模式被广泛应用于以下场景:</p> <ul> <li>公共信息亭:自助服务终端,如银行 ATM、自助点餐机、自助售票机。</li> <li>展览展示:在博物馆、展览会、零售店中,用于展示信息或广告内容的屏幕。</li> <li>教育场景:限制学生只能使用特定的教学应用,避免访问其他不必要的内容。</li> <li>会议或演讲:锁定演示内容,避免误操作或退出。</li> <li>数字标牌:作为广告屏幕或公告牌,循环播放内容。</li> </ul> <p>当然,我在开发的是日常效率软件,并不属于以上场景。我用到 Kiosk 模式的场景主要如下。</p> <h3>图几截图软件</h3> <p>我开发并维护着一个截图软件<a href="https://tuji.app">图几</a>,它有三种截图模式:全屏截图、窗口截图、区域截图。</p> <p>其中区域截图的交互方式是:用户点击截图按钮(或按下截图快捷键),先生成当前屏幕的截图,随后显示一个全屏无边框窗口,在窗口中显示将刚刚生成的屏幕截图,同时允许用户在窗口上进行框选等操作。</p> <p>这个无边框窗口就需要使用 Kiosk 模式,以免用户无意中切换窗口。当然,等用户完成或取消截图时,需要再退出或关闭对应的 Kiosk 窗口。</p> <h3>WonderPen 写作软件</h3> <p><a href="https://wonderpen.app">WonderPen 写作软件</a>最近添加了<em>小黑屋</em>模式,进入这种模式后,软件将全屏显示,屏蔽一切干扰,在完成预设的写作目标之前,将无法退出或切换到其他软件。</p> <p>这个禁止退出的小黑屋,自然也使用了 Kiosk 模式。</p> <h2>Electron 中的 Kiosk 模式</h2> <p>在 Electron 中,将一个窗口设为 Kiosk 模式非常简单,在创建窗口时设置 <code>kisok</code> 属性为 <code>true</code> 即可。</p> <p>有时,我们的窗口在创建时需要以普通模式显示,然后再在一定条件下切换为 Kiosk 模式,只需用类似下面的代码切换即可:</p> <pre><code class="language-js">win.setKiosk(flag) </code></pre> <p>其中 <code>flag</code> 是一个布尔值。</p> <p>你还可以使用 <code>win.isKiosk()</code> 方法判断当前窗口是否为 Kiosk 模式。</p> <p>在实践过程中,我发现很多时候只设置 Kiosk 属性还不太够,还需要设置 <code>frame</code> 等属性。以下是一个示例:</p> <pre><code class="language-js">const win = new BrowserWindow({ // 其他属性... closable: false, maximizable: false, minimizable: false, resizable: false, fullscreen: false, fullscreenable: false, frame: false, skipTaskbar: true, alwaysOnTop: true, useContentSize: true. autoHideMenuBar: true. movable: false. thickFrame: false. titleBarStyle: 'default', paintWhenInitiallyHidden: false, roundedCorners: false, enableLargerThanScreen: true, acceptFirstMouse: true, kiosk: true, // 其他属性... }) </code></pre> <p>即使这样设置之后,在 macOS 上有时仍会出现 Docker 栏和顶部系统菜单栏出现在 Kiosk 窗口上方的情况,因此还需要进一步设置 alwaysOnTop 的属性为 <code>screen-saver</code>,代码如下:</p> <pre><code class="language-js">win.setAlwaysOnTop(true, 'screen-saver', 1) </code></pre> <p>在 Windows 和 macOS 中,alwaysOnTop 的窗口有多种极别,按层级由低到高分别是:</p> <ul> <li>normal</li> <li>floating</li> <li>torn-off-menu</li> <li>modal-panel</li> <li>main-menu</li> <li>status</li> <li>pop-up-menu</li> <li>screen-saver</li> </ul> <p>如果只是简单地 <code>win.setAlwaysOnTop(true)</code> ,则窗口的级别只是 <code>floating</code>,仍有可能被其他系统组件遮挡。</p> <p>另外需要注意,在 macOS 下,太高的级别会挡住系统自带输入法的候选字窗口,如果你的 Kiosk 窗口需要用户输入,并且可能使用系统自带输入法的话,这个级别不能高于 <code>modal-panel</code>。</p> <h2>一些其他注意点</h2> <p>Kiosk 模式只对当前窗口有效,一个窗口只能覆盖一个屏幕,若用户有多个显示器,则需先检测显示器数量,然后创建多个 Kiosk 窗口分别覆盖。</p> <p>设置 Kiosk 模式后,用户仍可以使用 <code>Cmd+Q</code> 这样的快捷键退出应用,因此需要在代码中监听窗口的 <code>close</code> 事件,并检查是否处在 Kiosk 状态,如是则阻止退出。代码类似下面这样:</p> <pre><code class="language-typescript">win.on('close', async (e: Electron.Event) =&gt; { if (win.isKiosk()) { e.preventDefault() return } // 其他逻辑 } </code></pre> <p>Windows 下退出 Kiosk 模式后,窗口的大小可能会变成全屏大小,如希望退出时恢复原大小,可以在进入 Kiosk 模式之前先记住窗口大小,退出后再设置为原大小。</p> <p>Kiosk 模式并不能阻止用户重启计算机。如果希望重启计算机后能自动恢复 Kiosk 状态,可以将软件设置为随系统启动,并且启动时自动进入 Kiosk 模式。</p>

2024/11/28
阅读更多