<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Chronicle-IRAS的博客</title><description>写代码，也写生活。</description><link>https://blog.chronicleiras.top/</link><language>zh-CN</language><lastBuildDate>Fri, 06 Nov 2026 04:08:31 GMT</lastBuildDate><item><title>CHRONICLE：把滚动叙事做成一条可回退的时间线</title><link>https://blog.chronicleiras.top/blog/chronicle/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/chronicle/</guid><description>从 8 个场景、12 个视口高度到 GSAP ScrollTrigger 与减少动态效果，记录一个滚动驱动叙事的实现取舍。</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/chronicle&quot;&gt;CHRONICLE&lt;/a&gt; 是一个滚动驱动的交互叙事页面。它没有把“滚动”当成页面进度条，而是把 8 个场景组织成一条有边界、可回退、可重播的时间线。&lt;/p&gt;
&lt;p&gt;这篇文章不讨论它最终好不好看，而是记录几个工程问题：场景之间如何衔接、动画和滚动如何解耦、以及为什么“能回退”比“能播放”难得多。&lt;/p&gt;
&lt;h2 id=&quot;场景不是页面而是状态&quot;&gt;场景不是页面，而是状态&lt;/h2&gt;
&lt;p&gt;项目把体验拆成 Prelude、Welcome、Composer、Currently、Profile、Space、Destinations、Goodbye 等场景。每个场景有自己的背景、文字、角色或 WebGL 画布，滚动只负责触发边界。&lt;/p&gt;
&lt;p&gt;一个关键选择是：&lt;strong&gt;滚动距离和动画时长分离&lt;/strong&gt;。场景用 &lt;code&gt;svh&lt;/code&gt; 定义滚动区间，动画时长则写在 &lt;code&gt;sceneRegistry.ts&lt;/code&gt; 里。Composer 甚至预留了 200svh，其中 50svh 用于编辑，角色在这段时间里隐藏。&lt;/p&gt;
&lt;p&gt;这带来一个直接结果：用户继续滚动，不会把动画“拖快”或“拖慢”；动画按自己的时间线播放，滚动只决定何时开始。&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;部分&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/app/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;固定视口舞台、场景、滚动区间、可访问性与输入门禁&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/scenes/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;场景内容与局部动画钩子，不直接监听滚动&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/transitions/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;角色、文字、传送门、内容交接与重启编排&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/features/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Composer、文字冲击、星空/黑洞等独立能力&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/stores/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;当前场景、减少动态效果、Composer 就绪状态&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;可回退才是真正的时间线&quot;&gt;可回退，才是真正的时间线&lt;/h2&gt;
&lt;p&gt;很多滚动页面只实现了“向下播放”。CHRONICLE 为每个边界准备了前进时间线，以及可选的返回时间线。Prelude 返回是 3.3 秒，Welcome 是 0.35 秒的简化边界，Profile 返回是 1.8 秒，传送门进出各 1.25 秒。&lt;/p&gt;
&lt;p&gt;当边界触发后，播放会独立于滚动输入完成；主内容在过渡期间设为 inert，滚轮、触摸和滚动键被暂时阻止。到达目标场景后，文档才移动到该场景的阅读/编辑区间，输入恢复。&lt;/p&gt;
&lt;p&gt;这解决了一个常见问题：用户快速滚动时，动画播放到一半又被新的滚动事件打断。这里用“过渡期间锁输入”换取了确定性。&lt;/p&gt;
&lt;h2 id=&quot;composer-与角色编排&quot;&gt;Composer 与角色编排&lt;/h2&gt;
&lt;p&gt;Composer 是一个可编辑的节点图。它使用 React Flow，但禁用了滚轮缩放、背景平移和自动适配；只有节点和边可编辑，滚轮继续驱动页面叙事。&lt;/p&gt;
&lt;p&gt;角色不是视频，而是注册表驱动的姿态层。DeepSeek、Grok、Gemini、Claude、GPT 等角色由 &lt;code&gt;src/components/CharacterCast.tsx&lt;/code&gt; 和角色注册表管理。Scene 03 到 02 的拉拽动画使用 DS_PULL / GROK_PULL 姿态，并重复“用力”的过程。&lt;/p&gt;
&lt;p&gt;这类页面最容易失控的地方是“每个场景都写一套动画”。项目把场景内容、过渡编排和共享能力拆开，至少让新增场景时不用复制整段滚动逻辑。&lt;/p&gt;
&lt;h2 id=&quot;可访问性与降级&quot;&gt;可访问性与降级&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;非活动场景设为 inert，并从辅助技术中隐藏。&lt;/li&gt;
&lt;li&gt;阅读/编辑区间到达后，键盘焦点回到可用内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prefers-reduced-motion&lt;/code&gt; 下，边界简化、时长缩短到 0.35s。&lt;/li&gt;
&lt;li&gt;没有 WebGL 时保留 CSS 艺术与原生效果；WebGL 1 也能工作。&lt;/li&gt;
&lt;li&gt;首次访问的 Loading 有 6 秒超时兜底，不会因为资源失败卡死。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些不是“加分项”。当一个页面把滚动、动画和输入都锁在一起时，降级路径就是可用性的一部分。&lt;/p&gt;
&lt;h2 id=&quot;部署边界&quot;&gt;部署边界&lt;/h2&gt;
&lt;p&gt;项目用 Vite 构建，&lt;code&gt;base: &apos;./&apos;&lt;/code&gt;，同一份产物既能部署在域名根路径，也能部署在 GitHub Pages 的子路径。&lt;code&gt;dist/&lt;/code&gt; 由 GitHub Actions 发布，不依赖仓库名硬编码。&lt;/p&gt;
&lt;p&gt;这类叙事页面通常不需要服务端；真正的复杂度在客户端的时间线、资源预热和状态回收。CHRONICLE 把场景保持在固定视口中，非活动场景暂停效果，也是在控制长期运行的成本。&lt;/p&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;CHRONICLE 值得看的地方不是“用了多少库”，而是它把滚动叙事拆成了可管理的几层：场景、边界、时间线、输入门禁和降级。只要其中一层没有边界，整个页面就会变成一次不可回退的演示。&lt;/p&gt;
&lt;p&gt;如果你要做类似的滚动体验，先回答三个问题：动画能否独立完成？返回时能否恢复？减少动态效果时还剩下什么？这三个问题比选哪个动画库更重要。&lt;/p&gt;</content:encoded><category>前端</category><category>工程实践</category></item><item><title>用蓝图生成 Qt 工程：把 AI 代码放进可审查的流水线</title><link>https://blog.chronicleiras.top/blog/qt-blueprint-generator/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/qt-blueprint-generator/</guid><description>从蓝图数据模型、确定性生成到候选代码审核，记录 Qt Blueprint Generator 的架构取舍与当前边界。</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/qt-blueprint-generator&quot;&gt;Qt Blueprint Generator&lt;/a&gt; 是一个用蓝图描述 Qt 6 Widgets 应用的桌面工具。它的目标不是做完整的 IDE，而是把“设计意图”变成可审查、可构建、可导出的工程。&lt;/p&gt;
&lt;h2 id=&quot;为什么用蓝图作主数据源&quot;&gt;为什么用蓝图作主数据源&lt;/h2&gt;
&lt;p&gt;如果让 AI 直接改代码，最大的问题是：改动不可比较，也难以判断哪些部分是契约、哪些部分是实现。这个项目把蓝图作为主数据源，节点、端口、连线和属性先落成结构化数据，再由确定性生成器维护公共契约与工程骨架。&lt;/p&gt;
&lt;p&gt;一个蓝图工程包含两个文件：&lt;code&gt;blueprint.json&lt;/code&gt; 保存语义，&lt;code&gt;layout.json&lt;/code&gt; 保存画布位置。移动节点只改变布局，不会改变生成的中间表示；这样评审时可以只关注语义差异。&lt;/p&gt;
&lt;h2 id=&quot;生成流程&quot;&gt;生成流程&lt;/h2&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;蓝图设计 → 校验 → 生成 Candidate → 人工 Review&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        → Accept / Reject → Build → Export&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Candidate&lt;/strong&gt;：AI 生成的代码先进入候选区，不会直接覆盖正式工程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review&lt;/strong&gt;：逐文件对照当前实现与候选代码，可以选择接受、编辑后接受或拒绝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt;：构建需要显式触发，使用真实的 CMake 工具链。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export&lt;/strong&gt;：导出可以独立重新构建的工程，而不是只保留当前工作区。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这条流程的核心约束是：AI 不拥有最终写入权。公共契约、程序入口、构建配置等受保护内容由确定性系统维护；生成失败也不会自动清空旧工程。&lt;/p&gt;
&lt;h2 id=&quot;确定性与-ai-的边界&quot;&gt;确定性与 AI 的边界&lt;/h2&gt;
&lt;p&gt;项目把职责拆成几层：&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;/th&gt;&lt;th&gt;职责&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Qt 编辑器&lt;/td&gt;&lt;td&gt;画布、属性、文档生命周期、候选审核&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;生成服务&lt;/td&gt;&lt;td&gt;校验、写候选、接受授权、脚手架与构建&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;目标插件&lt;/td&gt;&lt;td&gt;把蓝图语义 lowering 成具体目标工程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;协议工具&lt;/td&gt;&lt;td&gt;Blueprint v2 与插件声明的离线校验&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;外部代码的导入也是同样的思路：C/C++ 文件以只读黑盒模块接入，绑定接口契约并校验 SHA-256；编辑器不假设自己可以安全地重写这些文件。&lt;/p&gt;
&lt;h2 id=&quot;当前能力与限制&quot;&gt;当前能力与限制&lt;/h2&gt;
&lt;p&gt;Phase 1 的运行时只接受一条线性链：&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Start → 零个或多个 Logic Module → 可选的唯一末端 UI Page → End&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Decision、External Code、多 End 等节点仍然可以编辑，但不属于这一阶段的运行时生成范围。节点编辑能力不等于运行时能力，这是 README 里反复强调的边界。&lt;/p&gt;
&lt;p&gt;插件平台仍是 Draft：当前只实现了 TypeScript/Node 的声明校验与最小内存 Registry，不执行插件代码，也没有接入 Qt GUI。长期方向是“编辑器和目标插件分离”，但在协议冻结前，不把提案当成已交付能力。&lt;/p&gt;
&lt;h2 id=&quot;构建与测试&quot;&gt;构建与测试&lt;/h2&gt;
&lt;p&gt;项目使用 C++17、Qt 6 Widgets / Network、CMake 和 Ninja。Windows 下建议把构建目录放在纯 ASCII 路径，避免 MinGW 与 AutoMOC 在中文路径上出问题。&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;cmake &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;S &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;B $buildRoot &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;G Ninja &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;DBUILD_TESTING&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;ON &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;DCMAKE_PREFIX_PATH&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;$qtRoot&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;cmake &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;build $buildRoot&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;ctest &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;test-dir&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; $buildRoot &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;C Debug &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;output&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;failure&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Join-Path&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; $buildRoot &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;BlueprintEditor.exe&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CTest 使用 Fake AI 或离线网络替身，不读取真实密钥，也不产生模型费用；需要调用真实模型的集成程序不会注册进自动测试。&lt;/p&gt;
&lt;h2 id=&quot;复盘&quot;&gt;复盘&lt;/h2&gt;
&lt;p&gt;这个项目最有价值的部分不是“让 AI 写代码”，而是把 AI 放进一条有边界、可回滚的流水线：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结构化输入优先于自然语言提示。&lt;/li&gt;
&lt;li&gt;生成结果先成为候选，再经过人工确认。&lt;/li&gt;
&lt;li&gt;契约、入口和构建配置由确定性系统维护。&lt;/li&gt;
&lt;li&gt;明确写出“当前不支持什么”，比模糊地承诺“未来都可以”更重要。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码与文档采用 MIT License；Qt 和其他第三方依赖仍遵循各自许可证。&lt;/p&gt;</content:encoded><category>工程实践</category><category>前端</category></item><item><title>StudyLoop：本地优先的学习系统，以及 AI 失败时的退路</title><link>https://blog.chronicleiras.top/blog/studyloop/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/studyloop/</guid><description>用 IndexedDB、三个学习模块和可替换的 QuestionProvider，把学习计划、每日巩固与间隔复习串成一个闭环。</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/online_restudy&quot;&gt;StudyLoop&lt;/a&gt; 是一个本地优先的个人学习系统。它没有把“学习”简化成一个番茄钟，而是拆成三个互相连接、但边界清楚的模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;学习计划&lt;/strong&gt;：接下来学什么，每天做什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每日巩固&lt;/strong&gt;：今天学的内容是否真的理解了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复习计划&lt;/strong&gt;：已学过的内容，之后怎样记牢。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三个模块共享知识库，但不混用任务。学习任务只记录完成情况；巩固题和复习题才影响掌握度。这个边界是整套设计里最重要的部分。&lt;/p&gt;
&lt;h2 id=&quot;为什么本地优先&quot;&gt;为什么本地优先&lt;/h2&gt;
&lt;p&gt;应用默认单用户、免登录。学习任务、笔记、题目、计划和答题记录都保存在浏览器 IndexedDB；关闭页面、重启设备后仍然存在。&lt;/p&gt;
&lt;p&gt;联网只用于可选的 AI 出题服务。没有配置 AI 或断网时，系统仍可使用本地模板题与已缓存的题目。复习计划创建时会预生成并缓存基础题目，因此已建立的计划不会因为网络中断而失效。&lt;/p&gt;
&lt;p&gt;这带来两个实际问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;导出/导入不是附属功能&lt;/strong&gt;：浏览器存储可能被清理，数据必须能完整导出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 不是主存储&lt;/strong&gt;：模型失败不能把用户输入一起清空。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;三个模块如何连接&quot;&gt;三个模块如何连接&lt;/h2&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;学习计划（安排新内容）&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;   ↓ 完成今日学习任务&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;每日巩固（记录实际所学并即时出题）&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;   ↓ 用户确认纳入长期记忆&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;复习计划（按目标天数间隔复习）&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;   ↓ 暴露薄弱点&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;每日巩固&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流转必须由用户确认。勾选“已学习”不会自动生成大量题目，也不会自动修改复习计划。这个约束看起来保守，但能避免 AI 把一次误操作放大成几十道题。&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;概念&lt;/th&gt;&lt;th&gt;所属模块&lt;/th&gt;&lt;th&gt;是否影响掌握度&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;学习任务&lt;/td&gt;&lt;td&gt;学习计划&lt;/td&gt;&lt;td&gt;否，只记录完成&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;当日巩固会话&lt;/td&gt;&lt;td&gt;每日巩固&lt;/td&gt;&lt;td&gt;是，形成初始掌握度&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;长期复习会话&lt;/td&gt;&lt;td&gt;复习计划&lt;/td&gt;&lt;td&gt;是，更新间隔与下次日期&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;笔记与知识点&lt;/td&gt;&lt;td&gt;共享知识库&lt;/td&gt;&lt;td&gt;被答题记录间接更新&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;ai-的边界&quot;&gt;AI 的边界&lt;/h2&gt;
&lt;p&gt;后端是一个本地 Fastify 代理，读取 &lt;code&gt;.env.local&lt;/code&gt; 里的 DeepSeek 配置。API Key 不会进入前端代码或 IndexedDB；日志和导出文件也不应包含密钥。&lt;/p&gt;
&lt;p&gt;出题层用统一的 &lt;code&gt;QuestionProvider&lt;/code&gt; 接口隔离：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;本地模板提供器&lt;/strong&gt;：无需联网，从定义句、列表、步骤、对比和代码块生成基础题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DeepSeek 提供器&lt;/strong&gt;：负责知识点提取、题型选择、干扰项和解析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未来本地模型&lt;/strong&gt;：可以接 WebLLM，但不进入 MVP。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DeepSeek 返回 JSON，经过 Zod schema 二次校验。空内容、截断或校验失败时只重试一次；仍失败就切到本地模板，并明确告诉用户。这里的原则是：&lt;strong&gt;AI 可以失败，但学习流程不能卡死&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&quot;复习计划不是每天随机出题&quot;&gt;复习计划不是“每天随机出题”&lt;/h2&gt;
&lt;p&gt;复习计划创建时，用户先确认知识点，再选择 3–60 天的目标、开始日期和轻量/标准/强化三档强度。系统预生成阶段日程和基础题目，首次打开当天时恢复“今日到期复习”，不需要网页一直运行。&lt;/p&gt;
&lt;p&gt;答题后更新掌握度和下次复习日期；错题会在后续日期重新出现。主观题先展示参考答案，再由用户选择“不会 / 模糊 / 掌握 / 轻松”。&lt;/p&gt;
&lt;p&gt;这也解释了为什么不能把学习任务和复习任务合成一个“待办数字”：学习任务是新内容，复习任务是旧内容，它们的逾期处理方式不同。&lt;/p&gt;
&lt;h2 id=&quot;值得复用的设计&quot;&gt;值得复用的设计&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据先于 AI&lt;/strong&gt;：用户输入、计划、答题记录都在本地；AI 只是可替换的题目来源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider 隔离&lt;/strong&gt;：本地规则和云端模型实现同一个接口，失败可切换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema 二次校验&lt;/strong&gt;：模型输出不是“看起来像 JSON”就直接用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户确认边界&lt;/strong&gt;：模块之间不自动流转，避免错误扩散。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可解释的复习&lt;/strong&gt;：用户能看到“为什么今天复习这些内容”，而不是只看到一个日期。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;StudyLoop 的工程价值不在“接了 DeepSeek”，而在于它把 AI 放在一个可失败、可替换、可审计的位置。对个人学习工具来说，这比多一个模型名称重要得多。&lt;/p&gt;
&lt;p&gt;项目仍在迭代；上面的模块边界和数据流来自公开的 README 与设计文档，具体实现以仓库为准。&lt;/p&gt;</content:encoded><category>前端</category><category>后端</category><category>工程实践</category></item><item><title>FlashDuel：2–4 人实时反应游戏，服务端如何管住房间与回合</title><link>https://blog.chronicleiras.top/blog/flashduel/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/flashduel/</guid><description>从 Socket.IO 房间、三种小游戏到服务端状态机，拆解一个实时多人反应游戏的前后端边界。</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/flashduel&quot;&gt;FlashDuel&lt;/a&gt; 是一个 2–4 人联机反应对抗游戏。它有三种模式：FlashStrike 测反应速度、ColorSniper 测视觉搜索、MemoryChain 测序列记忆。&lt;/p&gt;
&lt;p&gt;实时多人游戏最容易出问题的地方，不是画面，而是“谁在什么时候看到了什么、服务端相信谁”。这个项目把客户端表现和服务端游戏逻辑分开，结构值得看一眼。&lt;/p&gt;
&lt;h2 id=&quot;三种模式同一套回合&quot;&gt;三种模式，同一套回合&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模式&lt;/th&gt;&lt;th&gt;玩家做什么&lt;/th&gt;&lt;th&gt;主要能力&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;FlashStrike&lt;/td&gt;&lt;td&gt;信号出现后尽快点击，提前点击判抢跑&lt;/td&gt;&lt;td&gt;反应速度&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ColorSniper&lt;/td&gt;&lt;td&gt;在多个目标中找出指定颜色&lt;/td&gt;&lt;td&gt;视觉搜索&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MemoryChain&lt;/td&gt;&lt;td&gt;按顺序复刻颜色序列&lt;/td&gt;&lt;td&gt;序列记忆&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;三种模式共享房间、玩家、回合和结果流程。服务端目录里能看到 &lt;code&gt;Room&lt;/code&gt;、&lt;code&gt;RoomManager&lt;/code&gt;、&lt;code&gt;GameEngine&lt;/code&gt; 和 &lt;code&gt;BaseMiniGame&lt;/code&gt;，以及三种模式各自的实现。这个拆分让“房间生命周期”和“小游戏规则”不会缠在一起。&lt;/p&gt;
&lt;h2 id=&quot;前后端边界&quot;&gt;前后端边界&lt;/h2&gt;
&lt;p&gt;客户端使用 React 19、TypeScript、Vite、Zustand、Tailwind CSS 和 Socket.IO Client。服务端使用 Node.js、Express、Socket.IO、TypeScript 和 Pino。&lt;/p&gt;
&lt;p&gt;一个合理的职责划分是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户端&lt;/strong&gt;：渲染界面、收集输入、展示倒计时/结果，不拥有最终判定权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端&lt;/strong&gt;：维护房间与玩家状态、推进回合、判定抢跑与得分、广播结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Socket 事件&lt;/strong&gt;：只传必要动作和状态，不把整个游戏状态交给客户端随意修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反应类游戏尤其不能只相信客户端时间。抢跑判定、点击顺序和回合开始时间需要以服务端为准；客户端时钟只用于显示。否则延迟、改时间或重放请求都能轻易破坏公平性。&lt;/p&gt;
&lt;h2 id=&quot;房间状态机&quot;&gt;房间状态机&lt;/h2&gt;
&lt;p&gt;实时房间至少要回答这些问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;谁创建了房间，房间号如何加入？&lt;/li&gt;
&lt;li&gt;玩家人数不足/已满时，开始按钮是否可用？&lt;/li&gt;
&lt;li&gt;一局结束后，是回到大厅、自动重开，还是解散？&lt;/li&gt;
&lt;li&gt;玩家断线后，房间保留多久？是否允许重连？&lt;/li&gt;
&lt;li&gt;重复的开始/加入事件如何幂等处理？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;服务端的 &lt;code&gt;RoomManager&lt;/code&gt; 和 &lt;code&gt;Room&lt;/code&gt; 分层，适合把这些状态收敛在服务端；客户端只根据广播的状态渲染。项目里还有 &lt;code&gt;GameEngine&lt;/code&gt; 与各小游戏的测试，说明规则层可以脱离网络单独验证。&lt;/p&gt;
&lt;h2 id=&quot;延迟与体验&quot;&gt;延迟与体验&lt;/h2&gt;
&lt;p&gt;实时游戏不需要“零延迟”，但需要&lt;strong&gt;一致的延迟处理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;回合开始时间由服务端广播，客户端只做本地倒计时展示。&lt;/li&gt;
&lt;li&gt;输入带客户端序号/时间，服务端用自己的接收时间做最终排序。&lt;/li&gt;
&lt;li&gt;对提前点击、重复点击、超时未点击分别定义结果。&lt;/li&gt;
&lt;li&gt;断线时不要把玩家直接“判负”到不可恢复；至少要让房间状态可解释。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些是设计原则，不是对项目内部实现的逐行描述；具体事件名和判定以仓库代码为准。&lt;/p&gt;
&lt;h2 id=&quot;部署&quot;&gt;部署&lt;/h2&gt;
&lt;p&gt;README 给出的部署路径很直接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端 &lt;code&gt;npm run build&lt;/code&gt; 后用 &lt;code&gt;pm2&lt;/code&gt; 托管 &lt;code&gt;dist/index.js&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;客户端 &lt;code&gt;npm run build&lt;/code&gt; 生成静态资源，交给 Nginx 或由后端静态托管。&lt;/li&gt;
&lt;li&gt;项目使用 MIT License。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这类项目不需要复杂微服务。一个 Socket.IO 服务 + 一个静态客户端，已经足够支撑 2–4 人的房间；真正的成本在状态管理和异常路径。&lt;/p&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;FlashDuel 的看点不是“又做了一个小游戏”，而是把房间、回合、小游戏规则拆开，并让服务端拥有判定权。对实时多人项目来说，先想清楚状态机，再想动画和音效，通常更省事。&lt;/p&gt;</content:encoded><category>前端</category><category>后端</category></item><item><title>ESP32 监控中台：Vue 3 的实时视频界面</title><link>https://blog.chronicleiras.top/blog/monitor/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/monitor/</guid><description>从多路视频流、端口管理到权限与部署，记录实验室监控中心的前端结构与工程取舍。</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/monitor&quot;&gt;monitor&lt;/a&gt; 是一个面向 ESP32-S3 摄像头集群的实验室监控前端。它把多路视频、设备端口和系统配置放在同一个 Vue 3 应用里，后端通过 HTTP API 管理节点，视频流则由每台设备独立推送。&lt;/p&gt;
&lt;h2 id=&quot;技术栈与页面&quot;&gt;技术栈与页面&lt;/h2&gt;
&lt;p&gt;项目使用 Vue 3（Composition API + &lt;code&gt;&amp;#x3C;script setup&gt;&lt;/code&gt;）、TypeScript、Vite、Element Plus、Pinia、Vue Router 和 Axios，视频解码使用 jmuxer。&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;页面&lt;/th&gt;&lt;th&gt;职责&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;实时预览&lt;/td&gt;&lt;td&gt;多路 MJPEG / WebSocket 视频流，支持展开与关闭&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;历史录像&lt;/td&gt;&lt;td&gt;录像文件查询与回放&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;通讯终端&lt;/td&gt;&lt;td&gt;节点端口状态、启停管理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;系统配置&lt;/td&gt;&lt;td&gt;设备调度与系统参数（管理员）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;登录/角色&lt;/td&gt;&lt;td&gt;访客只读、管理员可操作&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;侧边栏在桌面端常驻，移动端切换为抽屉导航；主题偏好保存在本地。&lt;/p&gt;
&lt;h2 id=&quot;视频流websocket-与-jmuxer&quot;&gt;视频流：WebSocket 与 jmuxer&lt;/h2&gt;
&lt;p&gt;H.264 流通过 WebSocket 以二进制帧到达，前端把每一帧交给 jmuxer 解码到 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt;。关键不是“能播”，而是断流、重连和组件卸载时不要留下定时器。&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;ws &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; WebSocket&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(props.streamUrl)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;ws.binaryType &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &apos;arraybuffer&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;ws.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;onmessage&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  jmuxer.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;feed&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;({ video: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; Uint8Array&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(event.data) })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;ws.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;onclose&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; (destroyed) &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;return&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  reconnectTimer &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; window.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;setTimeout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(connect, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;2000&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组件卸载时会清空首次连接与重连定时器、摘掉 &lt;code&gt;onclose&lt;/code&gt; 后主动关闭 WebSocket，并销毁 jmuxer；否则离开页面后仍可能继续建连。MJPEG 模式则直接使用 HTTP 流地址，不经过 jmuxer。&lt;/p&gt;
&lt;h2 id=&quot;配置集中在一个文件&quot;&gt;配置集中在一个文件&lt;/h2&gt;
&lt;p&gt;最容易出问题的不是请求本身，而是地址散落在各处。项目把 API 与流地址收敛到 &lt;code&gt;src/config.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; API_BASE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;meta&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;.env.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;VITE_API_BASE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &apos;http://&amp;#x3C;YOUR_SERVER_IP&gt;:3000/api&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; STREAM_HOST&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;meta&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;.env.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;VITE_STREAM_HOST&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &apos;&amp;#x3C;YOUR_SERVER_IP&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;节点只保存端口，URL 由 &lt;code&gt;streamHttpUrl(port)&lt;/code&gt; / &lt;code&gt;streamWsUrl(port)&lt;/code&gt; 拼接。换服务器只需要改 &lt;code&gt;.env&lt;/code&gt;，不用搜索替换业务代码。&lt;/p&gt;
&lt;h2 id=&quot;节点与端口管理&quot;&gt;节点与端口管理&lt;/h2&gt;
&lt;p&gt;节点模型很克制：&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;interface&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; DeviceNode&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;  id&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;  udp&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; number&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;  http&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; number&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pinia 里的 &lt;code&gt;usedPorts&lt;/code&gt; 会把所有节点的 UDP/HTTP 端口收进一个集合，启动新节点前检查冲突；界面上 UDP 范围是 9001–9050，HTTP 范围是 9051–9100，已占用端口会标红。启停、重置等操作只对管理员开放，普通访客只能看流。&lt;/p&gt;
&lt;h2 id=&quot;权限客户端门禁不是安全边界&quot;&gt;权限：客户端门禁不是安全边界&lt;/h2&gt;
&lt;p&gt;项目用 &lt;code&gt;VITE_ADMIN_PASSWORD&lt;/code&gt; 做前端管理员门禁，登录状态存在 &lt;code&gt;localStorage&lt;/code&gt;。这适合实验室内网演示，但必须明确：&lt;strong&gt;构建进前端的密码不是授权机制&lt;/strong&gt;。任何能拿到前端资源的人都能绕过它，真正的启停与配置接口必须在服务端校验身份。&lt;/p&gt;
&lt;p&gt;如果要把这套系统放到不可信网络，至少需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端会话或 Token 校验；&lt;/li&gt;
&lt;li&gt;对启停、重置、配置等写接口做权限控制与审计；&lt;/li&gt;
&lt;li&gt;不再把管理员密码放进 &lt;code&gt;VITE_*&lt;/code&gt; 变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;部署&quot;&gt;部署&lt;/h2&gt;
&lt;p&gt;仓库提供了一个 Windows 一键脚本：构建后通过 SSH 清理旧目录，SCP 上传 &lt;code&gt;dist/&lt;/code&gt;，再更新 Nginx 权限。脚本里的服务器地址是占位值，部署前必须替换；生产环境更建议把部署步骤放进 CI，而不是把凭据留在批处理文件旁边。&lt;/p&gt;
&lt;h2 id=&quot;复盘&quot;&gt;复盘&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;视频类页面的重点在生命周期管理：断流重连、卸载清理、重复建连。&lt;/li&gt;
&lt;li&gt;配置集中化能显著降低部署与排错成本。&lt;/li&gt;
&lt;li&gt;角色分离要前后端一起做；前端门禁只负责界面状态。&lt;/li&gt;
&lt;li&gt;监控系统没有“差不多能看”这一说，端口冲突和权限边界都要在代码里写清楚。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>前端</category><category>工程实践</category></item><item><title>万能导：把多平台知识库迁回 Markdown</title><link>https://blog.chronicleiras.top/blog/wandao/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/wandao/</guid><description>飞书、语雀、印象笔记、Notion、Obsidian 等平台的内容，如何尽量保留目录、图片与附件，迁回本地 Markdown。</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/wandao&quot;&gt;wandao（万能导）&lt;/a&gt; 解决的是一个很具体的问题：知识散落在不同平台，导出后目录没了、图片断了、附件丢了，最后还是要手动整理。&lt;/p&gt;
&lt;p&gt;它的定位是“多平台知识库 Markdown 导入导出工具”。不是做另一个笔记应用，而是把已有内容迁回本地，或者把本地 Markdown 导入到目标平台。&lt;/p&gt;
&lt;h2 id=&quot;支持的范围&quot;&gt;支持的范围&lt;/h2&gt;
&lt;p&gt;README 列出的平台覆盖了常见的知识库和内容平台：飞书、语雀、阿里云 Thoughts、印象笔记、有道云笔记、为知笔记、OneNote、知识星球、ima 知识库、钉钉文档、WPS、息流、Obsidian、Notion、知乎、CSDN、微信公众号和本地 Markdown。&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方向&lt;/th&gt;&lt;th&gt;目标&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;导出&lt;/td&gt;&lt;td&gt;把有权访问的平台内容导出为本地 Markdown&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;导入&lt;/td&gt;&lt;td&gt;把本地 Markdown 导入支持的平台&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;保真&lt;/td&gt;&lt;td&gt;尽量保留目录结构、正文格式、图片与附件&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;迁移&lt;/td&gt;&lt;td&gt;知识备份、平台迁移、学习资料整理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这类工具最难的不是“调通一个接口”，而是处理不同平台的目录、图片、附件和格式差异。README 把“保留目录结构、图片、附件”放在核心位置，说明作者很清楚真正的成本在哪里。&lt;/p&gt;
&lt;h2 id=&quot;为什么用桌面端&quot;&gt;为什么用桌面端&lt;/h2&gt;
&lt;p&gt;项目提供 Windows x64 与 macOS arm64 桌面端。相比纯命令行，桌面端更适合处理登录态、平台选择、任务进度和失败重试。&lt;/p&gt;
&lt;p&gt;任务中心会保留历史进度与失败原因，平台中心展示已接入平台，插件中心负责搜索、安装和更新平台插件。这个结构比“一个脚本跑完所有平台”更容易维护，也更适合非技术用户。&lt;/p&gt;
&lt;h2 id=&quot;ai-辅助学习&quot;&gt;AI 辅助学习&lt;/h2&gt;
&lt;p&gt;项目里有一个我比较喜欢的设计：把课程资料、项目文档和源码导出到同一目录，再配合“项目学习导师”提示词交给支持本地文件的 AI。&lt;/p&gt;
&lt;p&gt;这样 AI 看到的不是零散截图，而是“教学文档 + 真实代码”的组合。对于理解一个陌生项目，这比只读 README 更有效。&lt;/p&gt;
&lt;h2 id=&quot;边界与许可&quot;&gt;边界与许可&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;只应导出自己有权访问的内容。&lt;/li&gt;
&lt;li&gt;平台插件会随平台变化而更新，导出结果不保证 100% 完整。&lt;/li&gt;
&lt;li&gt;项目使用 AGPL-3.0；如果要在网络服务中修改并分发，需要遵守许可证。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你也经历过“从某个平台导出后图片全裂开”，这个项目值得看一眼。它不承诺完美迁移，但至少把重复复制粘贴这件事自动化了。&lt;/p&gt;</content:encoded><category>工程实践</category><category>后端</category></item><item><title>用 Astro 内容集合管理博客文章</title><link>https://blog.chronicleiras.top/blog/astro-content-collections/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/astro-content-collections/</guid><description>从 schema 校验、目录约定到构建期过滤，记录内容集合在个人博客里的实际用法与取舍。</description><pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;内容集合（Content Collections）解决的问题很具体：把散落在目录里的 Markdown 变成有类型、可校验、可查询的数据。对个人博客来说，它的价值不在“功能多”，而在“坏数据会在构建期失败”。&lt;/p&gt;
&lt;h2 id=&quot;为什么需要内容集合&quot;&gt;为什么需要内容集合&lt;/h2&gt;
&lt;p&gt;没有 schema 的时候，frontmatter 只是一段 YAML。字段拼错、日期写成中文、标签随意新增，都要等到页面上出现空白时才被发现。内容集合把这些检查提前到构建阶段，并给出具体字段名。&lt;/p&gt;
&lt;h2 id=&quot;目录与图片&quot;&gt;目录与图片&lt;/h2&gt;
&lt;p&gt;文章以目录为单位，图片与正文放在一起。这样迁移时不需要额外维护一份资源映射，相对路径也不会因为文章换位置而失效。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.chronicleiras.top/uploads/astro-content-collections-diagram.png&quot; alt=&quot;Markdown 经 schema 校验后生成静态页面与索引的流程示意&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;schema-示例&quot;&gt;schema 示例&lt;/h2&gt;
&lt;p&gt;下面是一段刻意加长的代码，用来检查代码块的横向滚动与复制按钮：&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; blog&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; defineCollection&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  loader: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;glob&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;({ base: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;./src/content/blog&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, pattern: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;**/index.{md,mdx}&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;  schema&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: ({ &lt;/span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;image&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; z.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;object&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    title: z.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;min&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;80&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    description: z.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;min&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;20&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;160&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    slug: z.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;regex&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;[a-z0-9]&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#DBEDFF&quot;&gt;(?:-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;[a-z0-9]&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#DBEDFF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;*$&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    pubDate: z.coerce.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    tags: z.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;array&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(z.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;enum&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(tagNames)).&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;default&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;([]),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    draft: z.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;boolean&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;default&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    cover: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;image&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;optional&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;字段对照&quot;&gt;字段对照&lt;/h2&gt;































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;/th&gt;&lt;th&gt;必填&lt;/th&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;默认值&lt;/th&gt;&lt;th&gt;校验规则&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;th&gt;示例&lt;/th&gt;&lt;th&gt;备注&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;title&lt;/td&gt;&lt;td&gt;是&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;1–80 字符&lt;/td&gt;&lt;td&gt;标题与 SEO&lt;/td&gt;&lt;td&gt;你好，世界&lt;/td&gt;&lt;td&gt;中文建议不超过 40 字&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;description&lt;/td&gt;&lt;td&gt;是&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;20–160 字符&lt;/td&gt;&lt;td&gt;摘要与 meta&lt;/td&gt;&lt;td&gt;一篇示例文章&lt;/td&gt;&lt;td&gt;必须能独立成立&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;slug&lt;/td&gt;&lt;td&gt;是&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;kebab-case&lt;/td&gt;&lt;td&gt;URL&lt;/td&gt;&lt;td&gt;hello-world&lt;/td&gt;&lt;td&gt;全局唯一&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pubDate&lt;/td&gt;&lt;td&gt;是&lt;/td&gt;&lt;td&gt;date&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;可解析日期&lt;/td&gt;&lt;td&gt;排序与 RSS&lt;/td&gt;&lt;td&gt;2026-10-18&lt;/td&gt;&lt;td&gt;按站点时区过滤&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;updatedDate&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;date&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;≥ pubDate&lt;/td&gt;&lt;td&gt;更新时间&lt;/td&gt;&lt;td&gt;2026-10-20&lt;/td&gt;&lt;td&gt;不替换发布日期&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;tags&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;string[]&lt;/td&gt;&lt;td&gt;[]&lt;/td&gt;&lt;td&gt;注册表内&lt;/td&gt;&lt;td&gt;分类&lt;/td&gt;&lt;td&gt;前端&lt;/td&gt;&lt;td&gt;最多 3 个&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;draft&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;boolean&lt;/td&gt;&lt;td&gt;false&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;草稿过滤&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;构建时排除&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;cover&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;image&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;本地图片&lt;/td&gt;&lt;td&gt;封面&lt;/td&gt;&lt;td&gt;/uploads/astro-content-collections-cover.png&lt;/td&gt;&lt;td&gt;需同时提供 coverAlt&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;构建期过滤&quot;&gt;构建期过滤&lt;/h2&gt;
&lt;p&gt;草稿与未来日期在构建时被排除，因此它们不会进入首页、列表、RSS 或搜索索引。日期按 &lt;code&gt;Asia/Shanghai&lt;/code&gt; 的日历日比较，避免 &lt;code&gt;YYYY-MM-DD&lt;/code&gt; 被解析成 UTC 午夜后，在当天早上被误判为未来。&lt;sup&gt;&lt;a href=&quot;https://blog.chronicleiras.top/blog/astro-content-collections/#user-content-fn-1&quot; id=&quot;user-content-user-content-fnref-1&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;user-content-footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;内容集合不是银弹，但它把“内容约定”变成了可执行代码。对一个人维护的博客来说，这比多一套后台更有用。&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;user-content-footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-1&quot;&gt;
&lt;p&gt;具体实现见 &lt;code&gt;src/lib/posts.ts&lt;/code&gt; 中的 &lt;code&gt;isPublished()&lt;/code&gt;。 &lt;a href=&quot;https://blog.chronicleiras.top/blog/astro-content-collections/#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>前端</category><category>工程实践</category><category>后端</category></item><item><title>onCss：让 AI 生成 UI 代码，再把预览关进 iframe</title><link>https://blog.chronicleiras.top/blog/oncss/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/oncss/</guid><description>一个 Vue 3 + Express 的 AI UI 生成器，如何用结构化 schema、mock 模式和 iframe 预览控制不确定性。</description><pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/onCss&quot;&gt;onCss&lt;/a&gt; 是一个可视化 CSS / UI 代码生成工具。用户选择技术栈、组件类型和视觉风格，输入自然语言需求，后端调用 AI 生成结构化代码，前端在编辑器里展示并在 iframe 中预览。&lt;/p&gt;
&lt;p&gt;它吸引我的地方不是“AI 写页面”，而是它把生成结果约束成一组可校验的文件，而不是一段不可执行的聊天文本。&lt;/p&gt;
&lt;h2 id=&quot;一次生成的完整链路&quot;&gt;一次生成的完整链路&lt;/h2&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;前端表单&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  → POST /api/generate-code&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  → Zod 校验请求参数&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  → AI_MODE=mock ? mockGenerator&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                 : promptBuilder → AI → parseAiJson → Zod&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  → 统一 data&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  → CodeMirror 展示 / iframe 预览&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端只做一件事：代理 AI，并把结果变成稳定结构。它不存历史记录，不做用户系统，也不在线构建项目。历史记录放在前端 &lt;code&gt;localStorage&lt;/code&gt;，预览由前端 iframe 承担。&lt;/p&gt;
&lt;p&gt;这个边界让 MVP 很容易替换模型：只要返回结构不变，前端不需要关心背后是 DeepSeek、其他 OpenAI-compatible 服务，还是本地 mock。&lt;/p&gt;
&lt;h2 id=&quot;为什么要求-json而不是直接返回代码&quot;&gt;为什么要求 JSON，而不是直接返回代码&lt;/h2&gt;
&lt;p&gt;如果让模型自由返回一段 Markdown，前端就要猜哪段是 Vue、哪段是 CSS、入口文件是哪个。onCss 的响应结构明确到文件级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;framework&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;styleLibrary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;componentType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt; / &lt;code&gt;description&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;files[]&lt;/code&gt;：&lt;code&gt;filename&lt;/code&gt;、&lt;code&gt;language&lt;/code&gt;、&lt;code&gt;code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;previewEntry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notes[]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后端用 Zod 校验 AI 返回的 JSON，解析失败或结构不符就返回 &lt;code&gt;AI_RESPONSE_INVALID&lt;/code&gt;。这不是为了“类型好看”，而是为了让前端可以安全地渲染编辑器、下载文件和选择预览入口。&lt;/p&gt;
&lt;h2 id=&quot;mock-模式不是玩具&quot;&gt;mock 模式不是玩具&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AI_MODE=mock&lt;/code&gt; 时不调用外部 API，直接返回本地生成结果。它的价值在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有 API Key 也能调试前端表单、编辑器和预览。&lt;/li&gt;
&lt;li&gt;CI 可以稳定测试接口，不依赖模型输出。&lt;/li&gt;
&lt;li&gt;真实 AI 失败时，至少有一个可预期的最小响应。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后端配置里 &lt;code&gt;AI_MODE&lt;/code&gt; 默认就是 mock。对个人项目来说，这比“必须先配置 Key 才能启动”更友好。&lt;/p&gt;
&lt;h2 id=&quot;iframe-预览的安全边界&quot;&gt;iframe 预览的安全边界&lt;/h2&gt;
&lt;p&gt;生成的是可执行 HTML/CSS/JS。把这段内容直接注入主页面，会带来样式污染和脚本风险；放进 iframe 至少建立了文档边界。&lt;/p&gt;
&lt;p&gt;仍需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iframe 应使用 sandbox，按需要开放 &lt;code&gt;allow-scripts&lt;/code&gt;，不要同时开放 &lt;code&gt;allow-same-origin&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;预览内容不会自动获得主站权限。&lt;/li&gt;
&lt;li&gt;代码下载和复制只处理生成的文件，不要把用户输入拼进可执行上下文。&lt;/li&gt;
&lt;li&gt;后端错误信息不要回传 API Key、完整 Prompt 或内部堆栈。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些是生成式 UI 工具的基本边界，不是可选的“安全加固”。&lt;/p&gt;
&lt;h2 id=&quot;技术栈与取舍&quot;&gt;技术栈与取舍&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;/th&gt;&lt;th&gt;技术&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;前端&lt;/td&gt;&lt;td&gt;Vue 3 + TypeScript + Vite + Pinia + CodeMirror 6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;后端&lt;/td&gt;&lt;td&gt;Node.js + Express + TypeScript&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AI&lt;/td&gt;&lt;td&gt;OpenAI-compatible API&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;预览&lt;/td&gt;&lt;td&gt;iframe + 前端拼装&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;第一版不做的能力也很明确：数据库、账号、项目管理、在线构建、多模型调度、额度系统。对一个小工具来说，这些“以后再说”比提前做架构更重要。&lt;/p&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;onCss 的工程价值在于把 AI 的不确定性收进三个盒子：结构化请求、JSON schema、iframe 预览。模型仍然可能生成错误代码，但错误至少是可定位、可编辑、可替换的。&lt;/p&gt;
&lt;p&gt;如果你在做类似的生成工具，先定义输出结构，再选模型；反过来通常会得到一堆无法自动处理的“看起来像代码”的文本。&lt;/p&gt;</content:encoded><category>前端</category><category>后端</category><category>工程实践</category></item><item><title>PocketJS：没有 DOM 的组件运行时</title><link>https://blog.chronicleiras.top/blog/pocketjs/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/pocketjs/</guid><description>把 Solid、Vue Vapor 与 Octane 组件编译成同一棵原生树，在没有 DOM、CSS 引擎和 WebView 的设备上直接绘制像素。</description><pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/pocketjs&quot;&gt;PocketJS&lt;/a&gt; 是我最近看到的最“反常规”的前端项目之一：它让现代组件代码运行在没有 DOM、没有 CSS 引擎、没有 WebView 的环境里。&lt;/p&gt;
&lt;p&gt;不是把浏览器塞进设备，而是把组件编译成一棵原生树，由 Rust 核心做 flexbox 布局并绘制像素。&lt;/p&gt;
&lt;h2 id=&quot;三种框架一个运行时&quot;&gt;三种框架，一个运行时&lt;/h2&gt;
&lt;p&gt;PocketJS 支持 Solid、Vue Vapor 和 Octane。三者最终编译到同一棵原生树，运行在同一个 QuickJS guest 上。&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;框架&lt;/th&gt;&lt;th&gt;状态与生命周期&lt;/th&gt;&lt;th&gt;源码形式&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Solid&lt;/td&gt;&lt;td&gt;&lt;code&gt;solid-js&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JSX&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Vue Vapor&lt;/td&gt;&lt;td&gt;&lt;code&gt;vue&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JSX 与 &lt;code&gt;&amp;#x3C;script setup&gt;&lt;/code&gt; SFC&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Octane&lt;/td&gt;&lt;td&gt;&lt;code&gt;octane&lt;/code&gt;&lt;/td&gt;&lt;td&gt;编译期 hooks 与 JSX&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;框架只影响应用代码，不影响下面的运行时、输入、动画和原生边界。&lt;/p&gt;
&lt;h2 id=&quot;从组件到像素&quot;&gt;从组件到像素&lt;/h2&gt;
&lt;p&gt;一个 Solid 计数器在 PocketJS 里大概是这样：&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; { createSignal, Show } &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;solid-js&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; { mount } &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;@pocketjs/framework/solid&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; { Text, View } &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;@pocketjs/framework/solid/components&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; Counter&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;setCount&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; createSignal&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;View&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;w-full h-full flex-col items-center gap-4 p-4 bg-slate-50&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Text&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;text-xl text-slate-950 font-bold&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;Count: {&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;()}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Text&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;View&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;        class&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;px-4 py-2 rounded-xl bg-blue-600&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;        focusable&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;        onPress&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{() &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; setCount&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      &gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Text&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;text-base text-white font-bold&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;Press Circle&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Text&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;View&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Show&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; when&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Text&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;text-sm text-emerald-600&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;Reactive on real hardware.&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Text&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Show&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;View&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;mount&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Counter&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; /&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码没有 DOM 节点，也没有 CSS 解析。&lt;code&gt;class&lt;/code&gt; 字面量在构建期被编译成样式表，运行时只做查表。&lt;/p&gt;
&lt;h2 id=&quot;样式与动画&quot;&gt;样式与动画&lt;/h2&gt;
&lt;p&gt;PocketJS 接受一个固定的 Tailwind 子集。构建时把 class 编译成 baked style table，运行时没有 CSS parser、cascade、specificity 或 reflow。&lt;/p&gt;
&lt;p&gt;动画也是同一套思路：关键帧时间线和弹簧曲线被烘焙进样式表，由 Rust 核心按自己的时钟推进。也就是说，一屏动画可以在&lt;strong&gt;没有逐帧 JavaScript&lt;/strong&gt; 的情况下运行。&lt;/p&gt;
&lt;h2 id=&quot;目标准入&quot;&gt;目标准入&lt;/h2&gt;
&lt;p&gt;一个 bundle 可以服务不同密度的设备。应用在 &lt;code&gt;pocket.json&lt;/code&gt; 里声明视口和所需 API，目标 profile 必须满足声明才能编译和打包。&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;pocket&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; check&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --target&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; psp&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;pocket&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; check&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --target&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; vita&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一个 bundle，不需要为不同设备改组件。&lt;/p&gt;
&lt;h2 id=&quot;为什么值得关注&quot;&gt;为什么值得关注&lt;/h2&gt;
&lt;p&gt;PocketJS 把前端开发者熟悉的组件模型，带到了资源受限、没有浏览器的手持设备上。它没有试图模拟 Web，而是重新定义了“组件到像素”的路径。&lt;/p&gt;
&lt;p&gt;这条路当然有边界：固定样式子集、目标设备能力、QuickJS/Rust 的调试成本。但它至少证明了一件事——组件化不一定等于 DOM。&lt;/p&gt;</content:encoded><category>前端</category><category>工程实践</category></item><item><title>用 Windows 窗口播放 Bad Apple：Rust 与一次批量窗口实验</title><link>https://blog.chronicleiras.top/blog/bad-apple-windows/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/bad-apple-windows/</guid><description>把每个窗口当成像素，用 DeferWindowPos、WS_EX_TOOLWINDOW 和帧预处理，做一个高吞吐的 Windows 窗口动画。</description><pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Chronicle-IRAS/bad_apple_virus&quot;&gt;bad_apple_virus&lt;/a&gt; 是一个“用 Windows 窗口播放 Bad Apple”的实验。每个窗口是一个像素，窗口的显示、隐藏、移动和缩放组成帧动画。&lt;/p&gt;
&lt;p&gt;它不是病毒，只是借用了老式恶作剧程序的视觉语言。项目已经归档，但里面的性能取舍仍然值得看。&lt;/p&gt;
&lt;h2 id=&quot;把窗口当像素&quot;&gt;把窗口当像素&lt;/h2&gt;
&lt;p&gt;普通动画操作一个画布；这个项目操作的是大量真实窗口。每个窗口有位置、大小、可见性，还要走 Windows 的窗口管理器。瓶颈通常不在“画”，而在系统调用和重绘。&lt;/p&gt;
&lt;p&gt;README 里列出的关键优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DeferWindowPos&lt;/code&gt;：批量提交窗口位置/大小变更，而不是逐窗口 &lt;code&gt;SetWindowPos&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WS_EX_TOOLWINDOW&lt;/code&gt;：让窗口不出现在任务栏。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SWP_NOREDRAW&lt;/code&gt;：移动/缩放时避免不必要的重绘。&lt;/li&gt;
&lt;li&gt;只显示/隐藏/移动需要变化的窗口。&lt;/li&gt;
&lt;li&gt;Rust 负责调度与数据处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些点合起来，才把“窗口动画”从幻灯片变成可实时播放的实验。&lt;/p&gt;
&lt;h2 id=&quot;帧数据不是实时算的&quot;&gt;帧数据不是实时算的&lt;/h2&gt;
&lt;p&gt;项目包含一个 Python 预处理脚本，把输入视频转换成 &lt;code&gt;boxes.bin&lt;/code&gt;：每一帧的窗口边界被压缩成空间优化的表示。&lt;/p&gt;
&lt;p&gt;这个分工很合理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;视频解码和边界提取放在离线脚本。&lt;/li&gt;
&lt;li&gt;Rust 运行时只读帧数据，负责窗口操作。&lt;/li&gt;
&lt;li&gt;不需要在每一帧里做图像识别。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对实时程序来说，把能离线做的事情移出主循环，往往比换更快的语言更有效。&lt;/p&gt;
&lt;h2 id=&quot;为什么-deferwindowpos-重要&quot;&gt;为什么 &lt;code&gt;DeferWindowPos&lt;/code&gt; 重要&lt;/h2&gt;
&lt;p&gt;窗口位置调整不是纯内存操作。每次 &lt;code&gt;SetWindowPos&lt;/code&gt; 都可能触发窗口管理器的布局、重绘和消息处理。大量窗口逐条调整时，开销会累积。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DeferWindowPos&lt;/code&gt; 的思路是先收集一组变更，再一次性提交。它减少的是往返次数，而不是单次调用的成本。对于“每一帧几百个窗口都要动”的场景，这比 micro-optimize 单个函数更有意义。&lt;/p&gt;
&lt;p&gt;同样，&lt;code&gt;SWP_NOREDRAW&lt;/code&gt; 避免在移动过程中反复绘制；窗口最终位置由下一帧统一处理。&lt;/p&gt;
&lt;h2 id=&quot;边界与代价&quot;&gt;边界与代价&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;平台绑定&lt;/strong&gt;：完全依赖 Windows 窗口 API，不能在 Linux/macOS 直接运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;窗口数量有限&lt;/strong&gt;：分辨率越高，需要的窗口越多，系统压力越大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互会被干扰&lt;/strong&gt;：大量窗口会抢占焦点/遮挡桌面，只适合演示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;项目已归档&lt;/strong&gt;：作者明确表示没有带宽继续维护；fork 是预期路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是病毒&lt;/strong&gt;：README 明确说明它只是视觉上像老式恶作剧程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;可以复用的经验&quot;&gt;可以复用的经验&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;批量 API 往往比循环调用单个 API 更值得优化。&lt;/li&gt;
&lt;li&gt;把解码/预处理移出实时循环。&lt;/li&gt;
&lt;li&gt;只更新发生变化的实体，不要每帧重建全部状态。&lt;/li&gt;
&lt;li&gt;先测量瓶颈，再决定是否换语言；Rust 很快，但错误的调用方式仍然会慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;这是一个“用错工具做对事情”的项目：用窗口系统做像素缓冲区，听起来荒唐，但它把批量提交、最小更新和离线预处理都用上了。作为性能实验，它比很多正经动画 demo 更有信息量。&lt;/p&gt;</content:encoded><category>后端</category><category>工程实践</category></item><item><title>把个人博客当作工程项目：DeskBox 的 P0–P4 复盘</title><link>https://blog.chronicleiras.top/blog/deskbox-blog-engineering/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/deskbox-blog-engineering/</guid><description>从 Astro 静态站、内容校验到 SEO、搜索与多智能体协作，记录这个博客原型的工程化过程。</description><pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这个博客本身也是一个长期维护的工程项目：&lt;a href=&quot;https://github.com/Chronicle-IRAS/deskbox-blog&quot;&gt;deskbox-blog&lt;/a&gt;。它没有数据库、没有后台，内容与代码都在 Git 里；目标是让写作、构建和发布尽量可预测。&lt;/p&gt;
&lt;h2 id=&quot;技术选择astro--内容集合&quot;&gt;技术选择：Astro + 内容集合&lt;/h2&gt;
&lt;p&gt;站点使用 Astro 7 构建期生成静态 HTML，内容放在 &lt;code&gt;src/content/blog/&lt;/code&gt;，由 Content Collections + Zod 校验 frontmatter。文章目录、封面和正文图片放在一起，迁移时不会出现资源路径失配。&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;src/content/blog/2026/10/hello-world/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── index.md&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── cover.jpg&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;└── diagram.png&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;slug&lt;/code&gt; 在 frontmatter 里显式声明，构建期检查唯一性；标签不是自由字符串，而是集中注册表，未知标签会直接让构建失败。坏数据在构建期暴露，比上线后看到空白页要好。&lt;/p&gt;
&lt;h2 id=&quot;内容管线&quot;&gt;内容管线&lt;/h2&gt;
&lt;p&gt;新增文章由 &lt;code&gt;npm run new&lt;/code&gt; 生成目录与 frontmatter 模板：&lt;/p&gt;
&lt;pre style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;&lt;span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;文章标题&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --slug&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; my-post&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --tags&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; 前端,工程实践&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;草稿默认 &lt;code&gt;draft: true&lt;/code&gt;：&lt;code&gt;npm run dev&lt;/code&gt; 和本地预览可以看到，&lt;code&gt;npm run build&lt;/code&gt; 会排除。发布前把 &lt;code&gt;draft&lt;/code&gt; 改为 &lt;code&gt;false&lt;/code&gt;，再跑完整检查。&lt;/p&gt;
&lt;p&gt;内容规则包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt; 20–160 字；&lt;/li&gt;
&lt;li&gt;标签最多 3 个，必须已注册；&lt;/li&gt;
&lt;li&gt;有 &lt;code&gt;cover&lt;/code&gt; 必须有 &lt;code&gt;coverAlt&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updatedDate&lt;/code&gt; 不能早于 &lt;code&gt;pubDate&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slug&lt;/code&gt; 只允许小写 ASCII 与连字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;阅读与分发&quot;&gt;阅读与分发&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;文章页有粘性目录、代码复制、上一篇/下一篇和系列导航。&lt;/li&gt;
&lt;li&gt;搜索使用构建后的 Pagefind 索引，只在 &lt;code&gt;/search/&lt;/code&gt; 加载。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/rss.xml&lt;/code&gt; 输出全文，&lt;code&gt;/sitemap-index.xml&lt;/code&gt; 与 &lt;code&gt;robots.txt&lt;/code&gt; 由构建生成。&lt;/li&gt;
&lt;li&gt;文章页输出 &lt;code&gt;BlogPosting&lt;/code&gt; JSON-LD，首页输出 &lt;code&gt;WebSite&lt;/code&gt; + &lt;code&gt;Person&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;每篇文章构建期生成 1200×630 的 OG 图；非文章页面使用默认 OG。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;性能与安全&quot;&gt;性能与安全&lt;/h2&gt;
&lt;p&gt;最初移动端 LCP 超过 1.5 秒预算。后来的处理是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内联首屏 CSS，去掉渲染阻塞请求；&lt;/li&gt;
&lt;li&gt;内联主题初始化脚本，避免 &lt;code&gt;&amp;#x3C;head&gt;&lt;/code&gt; 里的额外往返；&lt;/li&gt;
&lt;li&gt;给封面增加 &lt;code&gt;widths/sizes&lt;/code&gt;，移动端下载更小的图。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;复测结果（Slow 4G + 4× CPU）：首页 LCP 从 1661ms 降到 1014ms，长文从 1812ms 降到 1291ms，CLS 保持 0。&lt;/p&gt;
&lt;p&gt;安全方面，站点没有第三方脚本。CSP 不使用 &lt;code&gt;script-src &apos;unsafe-inline&apos;&lt;/code&gt;，而是从构建产物提取内联脚本的 SHA-256 哈希；&lt;code&gt;npm run check:csp&lt;/code&gt; 会校验哈希、策略与实际生成的 headers 配置一致。评论启用时，CSP 自动加入 &lt;code&gt;giscus.app&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;多智能体协作的教训&quot;&gt;多智能体协作的教训&lt;/h2&gt;
&lt;p&gt;这个原型由 Claude Code、omp、agy 三条轨道并行完成，各自使用独立 worktree 和端口。回头看不复杂，但踩过的坑很具体：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要假设主工作区在哪个分支&lt;/strong&gt;。合并前先检查 &lt;code&gt;git branch&lt;/code&gt; 与 &lt;code&gt;git status&lt;/code&gt;；有他人未提交改动时，用独立 worktree 验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dev 通过不等于上线通过&lt;/strong&gt;。Pagefind、CSP headers 和缓存规则都必须在 &lt;code&gt;build + preview&lt;/code&gt; 下验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态 DOM 的样式要放全局&lt;/strong&gt;。Astro scoped CSS 不会作用于运行时插入的搜索结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSP 从构建产物生成&lt;/strong&gt;，不要手写哈希；评论/分析的开关也必须进入生成器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享契约要写下来&lt;/strong&gt;。&lt;code&gt;PostSummary&lt;/code&gt;、&lt;code&gt;BaseLayout&lt;/code&gt; props、&lt;code&gt;data-pagefind-body&lt;/code&gt; 这些边界一旦模糊，集成阶段就会重复返工。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;当前状态&quot;&gt;当前状态&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单元测试与 E2E 覆盖内容排序、SEO、搜索、评论、导航和链接。&lt;/li&gt;
&lt;li&gt;文章由示例内容与真实开源项目笔记组成。&lt;/li&gt;
&lt;li&gt;评论使用 Giscus，默认点击后加载；分析默认关闭。&lt;/li&gt;
&lt;li&gt;托管与域名仍未确定，生产部署不在这一阶段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把博客当作工程项目，不意味着要写得像企业系统。它只是让每个决定都有记录、每次修改都能回滚，然后把省下来的注意力留给写作。&lt;/p&gt;</content:encoded><category>前端</category><category>工程实践</category></item><item><title>最近的阅读笔记</title><link>https://blog.chronicleiras.top/blog/reading-notes/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/reading-notes/</guid><description>三本技术之外的书，以及它们共同指向的一个问题：如何与注意力相处。</description><pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近读的三本书都不厚，但都指向同一个问题：注意力放在哪里，生活就长成什么样。&lt;/p&gt;
&lt;h2 id=&quot;关于专注&quot;&gt;关于专注&lt;/h2&gt;
&lt;p&gt;《深度工作》里有一句话大意是：专注不是一种天赋，而是一种需要被保护的安排。我把它理解成两件事：给重要的事留出连续的时间，以及减少那些看起来只需要一分钟的打断。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们不是缺少时间，而是缺少不被切碎的时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这句话被我抄在便签上。它没有提供方法，但提供了一个判断标准：一件事值不值得做，先看它会不会把整块时间切成碎片。&lt;/p&gt;
&lt;h2 id=&quot;关于记录&quot;&gt;关于记录&lt;/h2&gt;
&lt;p&gt;另一本书谈的是记录。作者建议不要追求完整的日记，而是每天留下三行：发生了什么、当时的判断、事后看是否成立。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记下当时在做什么&lt;/li&gt;
&lt;li&gt;记下为什么停下来&lt;/li&gt;
&lt;li&gt;一周之后回看一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我试了两周，最大的收获不是留下了多少素材，而是发现自己反复在同样的问题上犹豫。记录把这种重复暴露了出来。&lt;/p&gt;
&lt;h2 id=&quot;读完之后&quot;&gt;读完之后&lt;/h2&gt;
&lt;p&gt;书读完之后，能留下来的通常不是观点，而是某个具体的动作。对我来说，这次留下的动作是：在开始一件事之前，先确认它需要多长的不被打断的时间。&lt;/p&gt;
&lt;p&gt;如果连这个问题都回答不了，那可能还没准备好开始。&lt;/p&gt;</content:encoded><category>阅读</category></item><item><title>你好，世界</title><link>https://blog.chronicleiras.top/blog/hello-world/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/hello-world/</guid><description>第一篇示例文章，用来验证没有封面、没有目录时正文是否仍然安静好读。</description><pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这里是一个个人博客的起点。写下第一篇文章，不是为了宣布什么，只是给以后留一个可以对照的坐标。&lt;/p&gt;
&lt;p&gt;我打算在这里记录两类东西：做过的项目和技术上的判断，以及读书、生活里值得停一下的瞬间。它们不一定完整，但尽量具体。&lt;/p&gt;
&lt;p&gt;写作的规则也很简单：先把话说明白，再考虑修辞。如果一篇文章需要靠形容词撑场面，那多半是内容还不够。&lt;/p&gt;
&lt;p&gt;所以就从这里开始。下一篇见。&lt;/p&gt;</content:encoded><category>生活</category></item><item><title>把构建时间当作一种反馈</title><link>https://blog.chronicleiras.top/blog/post-01/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-01/</guid><description>构建慢下来之后，最先暴露的不是工具问题，而是项目里那些没有被说清楚的依赖关系。</description><pubDate>Sat, 10 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;项目变大之后，构建时间会从“等待”变成“信号”。它开始告诉你，哪些模块被反复读取，哪些边界没有被划清。&lt;/p&gt;
&lt;h2 id=&quot;先看依赖图&quot;&gt;先看依赖图&lt;/h2&gt;
&lt;p&gt;最有效的一步不是换构建工具，而是把依赖图打印出来。很多看起来无关的页面，实际上通过一个共享入口连在一起，任何改动都会让整张图失效。&lt;/p&gt;
&lt;p&gt;把这类隐式依赖显式化之后，构建时间往往会自己降下来。&lt;/p&gt;
&lt;h2 id=&quot;再看缓存边界&quot;&gt;再看缓存边界&lt;/h2&gt;
&lt;p&gt;缓存不是越细越好。粒度太细会带来大量无效命中，粒度太粗又等于没有。比较稳妥的起点是：按内容单元切分，按构建阶段复用。&lt;/p&gt;
&lt;p&gt;最后，别把构建时间当成一个需要消灭的数字。它更像体温，重要的是变化趋势，而不是绝对值。&lt;/p&gt;</content:encoded><category>前端</category></item><item><title>小团队里的代码评审</title><link>https://blog.chronicleiras.top/blog/post-02/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-02/</guid><description>评审不是找错，而是让变更在合并前获得一次共同理解；记录几条我们实际用过的规则。</description><pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;小团队的代码评审很容易走向两个极端：要么变成形式上的“看过了”，要么变成逐行挑刺的消耗战。&lt;/p&gt;
&lt;h2 id=&quot;先看意图&quot;&gt;先看意图&lt;/h2&gt;
&lt;p&gt;评审的第一步是确认作者想解决什么问题。如果从 diff 开始看，很容易把注意力放在命名和格式上，而错过方案本身的问题。&lt;/p&gt;
&lt;p&gt;我们的做法是：提交说明里必须有一句“这样做是因为……”，评审先回应这句话。&lt;/p&gt;
&lt;h2 id=&quot;再看待改动的边界&quot;&gt;再看待改动的边界&lt;/h2&gt;
&lt;p&gt;一个变更如果同时改了行为、结构和格式，评审者很难判断哪部分才是重点。拆成几个小提交之后，讨论会具体很多。&lt;/p&gt;
&lt;h2 id=&quot;最后才是细节&quot;&gt;最后才是细节&lt;/h2&gt;
&lt;p&gt;格式问题交给工具。人只看工具判断不了的东西：命名是否表达了意图，错误处理是否完整，边界条件是否想清楚。&lt;/p&gt;
&lt;p&gt;评审的目标不是让代码变得完美，而是让下一位维护者少一次猜测。&lt;/p&gt;</content:encoded><category>工程实践</category></item><item><title>CSS 逻辑属性的几个使用场景</title><link>https://blog.chronicleiras.top/blog/post-03/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-03/</guid><description>从 margin-inline 到 inset-inline，逻辑属性让组件在不同书写方向下少写一层特判。</description><pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;逻辑属性刚出现时，我以为它只和阿拉伯语、希伯来语这类从右到左的语言有关。实际用下来，它在普通的中文页面里也能减少不少特判。&lt;/p&gt;
&lt;h2 id=&quot;水平方向&quot;&gt;水平方向&lt;/h2&gt;
&lt;p&gt;把 &lt;code&gt;margin-left&lt;/code&gt; / &lt;code&gt;margin-right&lt;/code&gt; 换成 &lt;code&gt;margin-inline-start&lt;/code&gt; / &lt;code&gt;margin-inline-end&lt;/code&gt;，组件就不再假设文字从左往右排。&lt;/p&gt;
&lt;p&gt;对于图标和文字的间距，&lt;code&gt;gap&lt;/code&gt; 通常更直接；只有在需要单侧偏移时，逻辑属性才真正体现价值。&lt;/p&gt;
&lt;h2 id=&quot;定位&quot;&gt;定位&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;inset-inline-start&lt;/code&gt; 和 &lt;code&gt;inset-inline-end&lt;/code&gt; 可以替代 &lt;code&gt;left&lt;/code&gt; / &lt;code&gt;right&lt;/code&gt;。写浮层、角标和关闭按钮时，这个替换几乎是无成本的。&lt;/p&gt;
&lt;h2 id=&quot;什么时候不用&quot;&gt;什么时候不用&lt;/h2&gt;
&lt;p&gt;如果布局本身有明确的物理方向，比如图表坐标轴或者固定位置的装饰线，继续使用物理属性反而更清楚。&lt;/p&gt;
&lt;p&gt;逻辑属性是表达“相对于文字方向”的工具，不是所有场景都适用。&lt;/p&gt;</content:encoded><category>前端</category></item><item><title>一次依赖升级的复盘</title><link>https://blog.chronicleiras.top/blog/post-04/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-04/</guid><description>把升级拆成可回滚的小步，比一次性追平版本更重要；这次我们踩过的坑与留下的检查项。</description><pubDate>Fri, 02 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这次升级原本计划一个下午完成，最后花了三天。问题不在依赖本身，而在我们对“升级完成”的定义太模糊。&lt;/p&gt;
&lt;h2 id=&quot;先升级工具链&quot;&gt;先升级工具链&lt;/h2&gt;
&lt;p&gt;运行时依赖和构建工具要分开升。混在一起时，一旦构建失败，很难判断是哪个变化导致的。&lt;/p&gt;
&lt;p&gt;我们先升级编译器和类型检查，再升级运行时库，每一步都保留一个可以回滚的提交。&lt;/p&gt;
&lt;h2 id=&quot;再看破坏性变更&quot;&gt;再看破坏性变更&lt;/h2&gt;
&lt;p&gt;变更日志里的 breaking change 只是起点。真正麻烦的是行为变化：默认值改了、警告变成了错误、废弃 API 还能用但语义不同。&lt;/p&gt;
&lt;h2 id=&quot;留下的检查项&quot;&gt;留下的检查项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级前记录当前构建时间与产物体积&lt;/li&gt;
&lt;li&gt;每步升级后跑一次完整测试&lt;/li&gt;
&lt;li&gt;出现无法解释的差异时，先回滚再定位&lt;/li&gt;
&lt;li&gt;升级提交单独存在，不和功能混在一起&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;升级结束之后，我们把这份清单放进了仓库。下一次不应该再靠记忆。&lt;/p&gt;</content:encoded><category>工程实践</category></item><item><title>为什么我把动画删掉了</title><link>https://blog.chronicleiras.top/blog/post-05/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-05/</guid><description>页面里的过渡效果从七处减到两处之后，阅读节奏反而更稳定，交互反馈也更清楚。</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;做减法比做加法难。这次清理页面动画，起因是有读者说“看完有点累”。我一开始以为是排版问题，后来把录屏放慢才意识到：页面一直在动。&lt;/p&gt;
&lt;h2 id=&quot;哪些动画其实没必要&quot;&gt;哪些动画其实没必要&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;段落进入时的淡入&lt;/li&gt;
&lt;li&gt;卡片悬停时的轻微上浮&lt;/li&gt;
&lt;li&gt;滚动到某个位置才出现的标题&lt;/li&gt;
&lt;li&gt;导航切换时的滑动指示器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些效果单看都不夸张，叠在一起却让眼睛始终在追东西。&lt;/p&gt;
&lt;h2 id=&quot;留下的两处&quot;&gt;留下的两处&lt;/h2&gt;
&lt;p&gt;只保留了两类：颜色和边框的过渡，以及移动菜单的展开。它们的共同点是，反馈的是用户刚刚做出的动作。&lt;/p&gt;
&lt;h2 id=&quot;结果&quot;&gt;结果&lt;/h2&gt;
&lt;p&gt;页面没有变快多少，但阅读时的停顿变少了。动画应该回答问题，而不是制造问题。&lt;/p&gt;</content:encoded><category>前端</category></item><item><title>用检查清单替代口头约定</title><link>https://blog.chronicleiras.top/blog/post-06/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-06/</guid><description>把“记得检查”写成可执行的清单，团队里的交接成本会明显下降，遗漏也更少。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;口头约定的问题是，它默认所有人当时的理解一致，而且下次还记得。现实通常不是这样。&lt;/p&gt;
&lt;h2 id=&quot;从一次交接开始&quot;&gt;从一次交接开始&lt;/h2&gt;
&lt;p&gt;我们把发布前的口头提醒写成了清单，只用了半天。写的过程本身就是一次对齐：原来每个人对“检查过了”的定义并不一样。&lt;/p&gt;
&lt;h2 id=&quot;清单应该多长&quot;&gt;清单应该多长&lt;/h2&gt;
&lt;p&gt;太长没人看，太短没有用。我们的经验是七到十项，每一项都能在一次操作里完成，并且有明确的通过标准。&lt;/p&gt;
&lt;h2 id=&quot;谁来维护&quot;&gt;谁来维护&lt;/h2&gt;
&lt;p&gt;清单不是写完就结束。每次出现遗漏，先问一个问题：是清单里没有这一项，还是这一项写得不够具体。然后把修正加回去。&lt;/p&gt;
&lt;p&gt;口头约定依赖记忆，检查清单依赖流程。前者适合熟人，后者适合长期维护。&lt;/p&gt;</content:encoded><category>工程实践</category></item><item><title>没有标签的一篇文章</title><link>https://blog.chronicleiras.top/blog/post-07/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-07/</guid><description>用来验证无标签状态下的列表行高、间距与元信息对齐是否仍然成立。</description><pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;标签是一个可选项，不是所有文章都需要归类。这篇示例文章故意不设置标签，用来检查列表行在没有标签时的排版。&lt;/p&gt;
&lt;p&gt;没有标签之后，元信息区只剩阅读时间。它仍然应该与上方标题保持稳定的间距，不能因为少了一个元素就向上塌陷。&lt;/p&gt;
&lt;p&gt;这类边界情况在真实写作里一定会出现，与其等上线后再修，不如在原型阶段就用示例内容覆盖。&lt;/p&gt;</content:encoded></item><item><title>当一篇技术文章写到三千字之后：关于结构、节奏与收尾的一次不完全复盘</title><link>https://blog.chronicleiras.top/blog/post-08/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/post-08/</guid><description>长文难的不是写满，而是让每一节都值得存在；记录一次删掉四成内容之后的编辑过程。</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;初稿写完时是五千字。我原本以为接下来只需要润色，结果真正的工作才开始：删掉四成，重排顺序，然后重新写结尾。&lt;/p&gt;
&lt;h2 id=&quot;结构不是目录&quot;&gt;结构不是目录&lt;/h2&gt;
&lt;p&gt;一开始的目录按“背景、方案、实现、结果”排列，看起来很完整。读第二遍时发现，读者最关心的问题被埋在第三节，而前两节有一半内容是在为自己铺垫。&lt;/p&gt;
&lt;p&gt;把结论提前之后，后面的章节反而更容易写，因为它们不再需要承担“制造悬念”的任务。&lt;/p&gt;
&lt;h2 id=&quot;节奏靠段落&quot;&gt;节奏靠段落&lt;/h2&gt;
&lt;p&gt;长文最容易出现的问题是每段都差不多长。视觉上很整齐，读起来却没有呼吸。调整的办法不是加小标题，而是让段落承担不同的功能：有的推进论点，有的给出例子，有的负责停顿。&lt;/p&gt;
&lt;h2 id=&quot;结尾不必总结&quot;&gt;结尾不必总结&lt;/h2&gt;
&lt;p&gt;我删掉了原来的“总结”小节。该说的在前文已经说完，再复述一遍只是让文章显得更长。新的结尾只有三句话，回到最初的问题，然后停下。&lt;/p&gt;
&lt;p&gt;删掉四成之后，文章并没有变薄，反而更清楚了。&lt;/p&gt;</content:encoded><category>阅读</category></item><item><title>长标题与长 slug 的排版测试</title><link>https://blog.chronicleiras.top/blog/a-very-long-slug-for-testing-list-layout-and-line-wrapping-behavior/</link><guid isPermaLink="true">https://blog.chronicleiras.top/blog/a-very-long-slug-for-testing-list-layout-and-line-wrapping-behavior/</guid><description>这篇文章用一个很长的英文 slug 和较长的标题，检查列表、卡片与归档场景下的换行表现。</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇示例文章的主要作用是压力测试。它的 slug 很长，URL 会明显超过常见文章；标题也比平均长度更长。&lt;/p&gt;
&lt;h2 id=&quot;列表里的表现&quot;&gt;列表里的表现&lt;/h2&gt;
&lt;p&gt;在文章列表中，标题应该自然换行，不被截断。日期列保持固定宽度，摘要最多两行，元信息在换行后仍然对齐。&lt;/p&gt;
&lt;h2 id=&quot;url-与分享&quot;&gt;URL 与分享&lt;/h2&gt;
&lt;p&gt;过长的 slug 不适合真实写作，但在原型中保留一篇，可以提前暴露路由、分页和复制链接时的边界问题。&lt;/p&gt;
&lt;p&gt;如果以后需要支持中文 slug，再单独评估编码与兼容性，不在 P0 处理。&lt;/p&gt;</content:encoded><category>前端</category></item></channel></rss>