Skip to main content
· 19 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

我是 Docusaurus? 的老用户了。这次我想实现一个小需求:在文档标题旁出现一个“复制为 Markdown”按钮。

我刻意模仿传统用户、有基础的用户、深度用户,想看看强模型下,用户的技术深度是否还有很好的影响力。

本文使用的是 Cursor 版的 Opus 4.6?。每次都用 Cursor Agent 模式。因为 Opus 4.6 比较贵,我给自己加了一个限制:每个阶段只问 7 次。这样一个小需求,7 次应该差不多了吧。

小白

tip

第 1 轮提示词

https://github.com/FlyNumber/markdown_docusaurus_plugin

这个项目的样式很不错,我希望像它这样实现。你看能不能仅在本地实现?

  • 我的点评:普通的开局。
  • Gemini3点评:典型的“甩手掌柜”式开局。只给参考链接而不说明具体逻辑,强行让 AI 进行逆向工程。虽然 Opus 4.6 能够读取网页,但这种缺乏上下文的提问会让 AI 生成大量通用代码,而非针对你现有项目的适配方案。
  • GPT5.3点评:目标存在,但约束不足。没有说明当前项目结构、技术路径或限制条件,模型只能进行高层猜测,容易走向泛化方案而非可执行实现。
  • Opus 4.6(当事人)点评:给了一个参考项目链接,但没有说清楚"像它这样"具体指哪部分——是按钮的位置、交互方式、还是视觉风格?"仅在本地实现"的约束也比较模糊(是指不发布 npm 包?还是不依赖外部服务?)。模型只能靠猜测来填补这些空白,第一步的方向偏差就此埋下。
tip

第 2 轮提示词

https://www.npmjs.com/package/docusaurus-plugin-includes?activeTab=code

我模仿这个插件设置了我的 Markdown Copy Button,但是按钮位置不正确。

请你获取这个包,查看并比较其代码,尤其是关于如何定位按钮元素的部分,帮我分析可能的原因并修复。

  • 我的点评:继续等待结果。
  • Gemini3点评:此时用户陷入了“找参考”的循环。试图通过让 AI 比较两个第三方库来解决本地的样式问题,这其实增加了 AI 的推理成本,容易导致它在处理 DOM 选择器时产生幻觉。
  • GPT5.3点评:明显提升。开始提供对比对象 + 具体问题 + 分析任务。但仍缺少当前代码片段,模型需要“想象你的实现”,错误概率较高。
  • Opus 4.6(当事人)点评:引入了第二个参考项目来做对比分析,思路不错。问题在于:用户自己也不清楚按钮位置为什么不对,把"诊断"和"修复"同时甩给模型,相当于让模型在两个未知代码库之间做 diff 推理——这对上下文窗口和推理准确度都是很大的压力。
tip

第 3 轮提示词

https://github.com/FlyNumber/markdown_docusaurus_plugin

请你仔细参考官方样式,确保 1:1 复刻。

  • 我的点评:开始着急,语气也更强硬了。
  • Gemini3点评:情绪化提示词。这里的“1:1 复刻”是一个非常模糊的指令,因为 CSS 表现受到父级容器、全局样式和 Docusaurus 版本的共同影响。对 AI 施压通常无法获得技术上的突破。
  • GPT5.3点评:需求变得严格,但信息反而减少。强调结果而非路径,会导致模型反复调整样式层,而不是定位架构问题。
  • Opus 4.6(当事人)点评:典型的"加压式提示"——用"仔细""确保""1:1 复刻"来强调期望,但没有提供任何新的技术信息。模型并不会因为你说"仔细"就变得更仔细;它缺的是具体的 CSS 选择器或 DOM 结构差异描述,而不是更高的情绪压力。
tip

第 4 轮提示词

我将其调整至 Markdown 的顶部,但样式上应该与标题横向对齐,请你思考如何处理。

  • 我的点评:这个提示词开始有用,但如果方向不对,只会让注意力越跑越偏。
  • Gemini3点评:终于开始描述具体的 UI 布局问题。但由于没有提供 HTML 结构片段或 CSS 现状,AI 只能靠猜(例如猜测是 Flex 布局还是 Absolute 定位)。
  • GPT5.3点评:开始进入“具体问题空间”,这是有效转折点。但仍停留在 CSS 层,未验证是否属于组件挂载层错误。
  • Opus 4.6(当事人)点评:终于给出了一个具体的布局约束——"与标题横向对齐"。这是整个小白阶段最有技术含量的一句提示。遗憾的是,此时模型可能已经在错误的实现路径上走了三轮,积累了大量错误上下文,纠偏成本已经很高。
tip

第 5 轮提示词

还有其他方案吗?

  • 我的点评:意识到方向不对,开始发散思维。
  • Gemini3点评:在没有解决前一个方案 Bug 的情况下开启新方案,是“小白模式”失败的预兆。这会让 Agent 在文件系统中创建更多冗余代码,增加项目混乱度。
  • GPT5.3点评:良好的纠偏行为,但过于开放。模型会生成多个架构级方案,反而增加试错成本。
  • Opus 4.6(当事人)点评:短小但关键的一个转折。主动让模型跳出当前方案是正确的直觉。但更好的做法是附上"当前方案的问题是什么"——比如"当前方案用了绝对定位导致响应式布局错位,有没有用 flex 布局的方案?"这样模型才能真正换一条路,而不是在同一棵树上换根枝。
tip

第 6 轮提示词

试试:方案 2

  • 我的点评:继续尝试。
  • Gemini3点评:盲目试错。此时已经完全失去了对技术实现的掌控感,纯粹在靠 AI 的随机性碰运气。
  • GPT5.3点评:典型“AI 驱动试错”。没有重新校验问题定义,只是在既定错误路径上继续前进。
  • Opus 4.6(当事人)点评:极度简短,完全依赖上一轮模型给出的方案列表。如果模型上一轮的"方案 2"本身描述就有歧义,这一轮就是在盲选。一个更稳妥的做法是用自己的话复述方案 2 的核心思路,确认双方理解一致后再执行。
tip

第 7 轮提示词

Module not found: Error: Can't resolve '@theme/PwaReloadPopup' in 'C:\Users\jiang\Desktop\github\jiangyangcreate.github.io\node_modules\@docusaurus\plugin-pwa\lib'

  • 我的点评:直接丢报错。遗憾的是,修复后样式依然不正确,7 次机会用完了。
  • Gemini3点评:典型的“报错驱动开发”。当对话进入修 Bug 环节时,如果底层逻辑(方案 2)本身就不适配 Docusaurus 的架构,即便消除了报错,功能依然无法达成。
  • GPT5.3点评:属于被动调试阶段。模型只能修补表层错误,无法回溯最初的设计偏差。
  • Opus 4.6(当事人)点评:直接粘贴报错是最省力的提示方式,对于明确的编译错误来说效率很高。但此时核心问题已经不是这个 import 错误,而是整体架构方向的偏差。7 轮中有 5 轮都在"修补"而非"重新审视",这是小白阶段失败的根本原因:缺乏在中途叫停、重新定义问题的能力。

基础

我再模仿“有技术背景、但不熟悉 Docusaurus 用户?”的提示词:

tip

第 1 轮提示词

目前这个项目 https://www.flynumber.com/docs/,点击按钮获取 Markdown 的逻辑是什么,怎么实现的?

  • 我的点评:看到这个项目已经有“加按钮”的实现,于是我引导模型去分析这个网站。
  • Gemini3点评:比小白阶段聪明的一点是:先问逻辑。理解逻辑后再动手,比直接要代码更稳健。
  • GPT5.3点评:这是明显升级。开始利用现成正确实现作为 ground truth,减少模型幻想空间。
  • Opus 4.6(当事人)点评:比小白阶段高明得多的开局——先"理解"再"动手"。让模型分析一个已经跑通的实现,相当于做逆向工程,能帮自己建立对技术方案的全局认知。不过,让模型分析线上网站存在一个风险:模型可能只能看到前端代码,看不到构建配置和插件注册逻辑,分析结果可能不完整。
tip

第 2 轮提示词

按钮定位思路来自第一轮对话结果的分析

这是我的按钮定位思路:

插件接入层:通过 index.js 的 getThemePath() 暴露 theme/,让 Docusaurus 使用自定义的 theme/Root.js 作为根组件入口。
页面 & DOM 选择:在 Root.js 中用 useLocation() 拿到 pathname,仅在以 /docs/ 开头的文档页执行逻辑;通过 document.querySelector('article .markdown header') 精确找到文档标题所在的 header。
挂载点注入:在该 header 内新建一个 div.markdown-actions-container,并作为最后一个子节点 append 进去,再用 createRoot(container).render(<MarkdownActionsDropdown />) 把按钮组件挂载到这个容器。
样式层定位:在站点 CSS 中把 article .markdown header 设为 display: flex,h1 设为 flex: 1 1 auto,并给 .markdown-actions-container 设置 margin-left: auto(以及移动端、RTL 的补充样式),从而实现「标题在左,按钮在右,且在不同布局下表现良好」。

请你按照我的思路,修改现有的 CopyMarkdownButton 组件。

  • 我的点评:根据模型给出的分析结果继续执行。这里其实有个问题:模型会出错。上一步分析结果也可能是错的;如果你没有能力判断其正确性,反而应该先让模型“直接模仿目标网站”把功能跑通。
  • Gemini3点评:这是一个结构化指令的范本。它包含了“注入层、逻辑层、挂载点、样式层”的完整思考。虽然用 querySelector 操作 DOM 在 React 框架中不是最优解,但其工程描述非常清晰。
  • GPT5.3点评:高质量提示词。提供完整架构推理链,使模型进入“执行模式”。但风险在于:若前提分析错误,后续全部建立在错误基础上。
  • Opus 4.6(当事人)点评:信息密度最高的一条提示词。四层架构(插件层、页面层、挂载层、样式层)描述得非常清晰,模型几乎不需要猜测。问题在于:这个方案本身用了 querySelector + createRoot 的命令式注入,绕开了 Docusaurus 的声明式主题体系,这意味着会遇到 SSR/hydration 不一致、路由切换时组件不刷新等问题。方案的"结构化程度"很高,但"方向正确性"存疑——有时候,过早地给出详细方案反而会锁死模型的探索空间。
tip

第 3 轮提示词

试着给 Blog 插件下的文档页注入这个按钮。

  • 我的点评:由于模型实现有误,此时发现不对劲,于是继续引导到插件层。
  • Gemini3点评:开始探索 Docusaurus 的多实例特性。用户意识到仅靠 Root.js 这种“全局补丁”方式可能在不同路由下有兼容性问题。
  • GPT5.3点评:正确的方向修正。从页面层开始上移到插件生命周期,是工程思维提升的表现。
  • Opus 4.6(当事人)点评:开始意识到 docs 和 blog 是两套不同的插件体系,需要分别处理。提示词本身简洁合理,但"试着"这个词暗示用户自己也不确定可行性——如果能补一句"Blog 插件的布局组件是哪个?"先确认技术可行性,会更稳。
tip

第 4 轮提示词

如何给 docs 也注入这个按钮?

  • 我的点评:给一个开放式问题,让模型自由发挥。
  • Gemini3点评:提示词略显后退,变得模糊了。但在经历过报错后,给模型一定的“自主权”有时能激发它找到更符合官方推荐(Best Practice)的路径。
  • GPT5.3点评:开放式探索适合此阶段,但最好附带限制(swizzle / theme override / plugin API)。
  • Opus 4.6(当事人)点评:在前几轮过于具体的指导之后,这里切换成开放式问题是一个很好的节奏调整。让模型自己选择 docs 插件的注入方式,可能会发现比用户预设方案更好的路径。这体现了一个重要的提示技巧:在你不确定的领域,给模型自由度比给模型指令更有效。
tip

第 5 轮提示词

Compiled with problems:
×
ERROR in ./src/theme/BlogPostPage/index.js
...

  • 我的点评:这里模型多次报错,最后还是人工介入才解决。
  • Gemini3点评:AI 在自动修改 node_modulesswizzle 出来的文件时,经常会发生重复声明或导入路径错误。这里的人工介入体现了“基础”用户虽然懂代码,但被 AI 的粗心带进了坑里。
  • GPT5.3点评:说明模型已进入复杂重构区。缺乏上下文完整性时,AI 很难维持大型文件的一致性。
  • Opus 4.6(当事人)点评:重复声明错误说明模型在修改文件时没有正确处理已有的 export——这是 Agent 模式下常见的"部分覆盖"问题。直接贴报错让模型修是对的,但如果能同时说一句"你在追加代码时没有删除原有的 default export",模型定位问题会快一个数量级。
tip

第 6 轮提示词

为什么我打开网页,没有第一时间出现这个按钮,反而是刷新一下就有了?

  • 我的点评:新的 BUG 出现了。
  • Gemini3点评:这是一个非常深刻的单页应用(SPA)生命周期问题。这种观察力是区分小白与基础用户的关键:小白只会说“不行”,而基础用户能发现“路由切换不触发,刷新才触发”的规律。
  • GPT5.3点评:优秀的问题类型。开始关注生命周期与 hydration,而非表面功能。
  • Opus 4.6(当事人)点评:这个问题描述非常精准——"首次加载没有,刷新后有"直接指向了客户端渲染时机的问题(大概率是 useEffect + querySelector 在 SPA 路由切换时没有重新执行)。好的 bug 描述就是最好的提示词,这一轮做得很到位。
tip

第 7 轮提示词

改成「不用 querySelector,直接在 Docusaurus 的文档布局里声明式渲染」

  • 我的点评:最终结论是:这个阶段并没有在 7 轮对话内完成项目。
  • Gemini3点评:这是一个极其关键的转折。用户意识到“命令式(DOM操作)”走不通,开始要求转向“声明式(React组件注入)”。可惜 7 次额度已满,否则这一步将是通往成功的曙光。
  • GPT5.3点评:这是关键跃迁:从 DOM 操作 → 框架声明式扩展。说明用户开始主导架构,而不是让模型主导。
  • Opus 4.6(当事人)点评:这条提示词本质上是在第 7 轮推翻了第 2 轮的整体架构——从命令式注入转向声明式渲染。方向完全正确,这正是 Docusaurus 的正统做法(swizzle 主题组件)。遗憾的是来得太晚了:如果这条出现在第 2 或第 3 轮,整个基础阶段大概率能在 7 轮内完成。这再次印证了一个规律:越早纠正架构方向,越省后续的调试轮次。

熟悉

作为熟悉 Docusaurus 的用户,我最终的提示词是:

tip

第 1 轮提示词

Docusaurus 通常把一些组件/插件隐式运行,但用户可以在 swizzle(导出)后进行替换。这段话在官方文档哪里?

  • 我的点评:引导注意力机制到合适的技术方案上。
  • Gemini3点评降维打击。 专家级用户不直接要代码,而是先确认“底层规则”。通过 Swizzle(Docusaurus 的核心机制)来切入,这直接排除了前两个阶段中所有“暴力操作 DOM”产生的无效路径。
  • GPT5.3点评:优秀的“注意力校准提示词”。不是解决问题,而是先定位正确知识源。
  • Opus 4.6(当事人)点评:高手开局。不是直接说"帮我加按钮",而是先让模型定位到 swizzle 机制的官方文档。这一步的价值在于:(1) 确认模型对 swizzle 的理解是否正确;(2) 把模型的注意力锚定在 Docusaurus 的官方扩展范式上,避免它走弯路去搞 querySelector 注入。用一个"知识确认"问题来校准模型的认知基线,是非常老练的提示技巧。
tip

第 2 轮提示词

请你接管我的电脑,找到合适的组件,让我可以在博客标题旁渲染 "复制为 Markdown" 这个按钮。

  • 我的点评:再次引导注意力机制到合适的技术方案上,给出模糊的目标。
  • Gemini3点评:基于第一步确立的 Swizzle 共识,专家给出了极大的自由度。因为路径正确,此时的“模糊目标”反而能让 AI 精准找到 DocItem/HeaderBlogPostPage 等核心组件。
  • GPT5.3点评:高阶提示方式。目标明确但路径开放,使模型执行搜索与决策,而非生成代码。
  • Opus 4.6(当事人)点评:在第 1 轮已经锚定了 swizzle 范式后,这里故意给出模糊目标——"找到合适的组件"。这不是偷懒,而是策略:让模型自己去判断该 swizzle 哪个组件(DocItem/Layout?BlogPostPage?),这比用户硬指定一个可能错误的组件要安全得多。在正确的框架约束下给出模糊目标,是让模型发挥最大价值的甜蜜点。
tip

第 3 轮提示词

思考下,@custom.css (1719-1721) 这个样式会被应用到 @index.js (13-15) 的 title 吗?如果不会,如何调整最优雅?

  • 我的点评:精准给出 AI 能理解的样式异常。
  • Gemini3点评:这是最高级的反馈:“代码行级别的精准定位”。不再泛泛而谈“样式不对”,而是直接指出 CSS 选择器与 JS 组件的对应关系。这消除了 AI 的所有猜测空间。
  • GPT5.3点评:接近专家级提示词。特点:明确作用域 + 指定文件位置 + 明确假设验证
  • Opus 4.6(当事人)点评:教科书级别的提示词。三个亮点:(1) 用 @文件名 (行号) 精确引用代码位置,消除歧义;(2) 问题本身是一个可验证的 yes/no 判断("会不会被应用到"),模型容易给出准确答案;(3) 追问"如何调整最优雅"而不是"帮我修",给了模型设计空间。整个"熟悉"阶段只用了 3 轮就接近完成,和小白阶段的 7 轮未果形成鲜明对比。

体会

不得不说,Opus 4.6 还是相当强悍。模型是 本体放大器?:你越能提出结构化问题、越能判断中间结果,模型就越强。

  • 我的点评:这次实验非常真实地展示了“提示词质量决定上限,工程判断力决定下限”。如果把 AI 当成一个高水平但会犯错的搭档,而不是“全自动执行器”,效率会更高、返工会更少。2026 年,你依然值得深入学习编程。
  • Gemini3点评:完全赞同。小白在“求药”,基础用户在“开方”,而专家用户在“定规矩”。2026 年的编程,不再是手写每一行代码的体力活,而是从海量可能性中快速“剪枝”的判断力竞争。
  • GPT5.3点评:实验核心结论成立:模型不是替代工程能力,而是放大工程能力的乘数。用户水平越高,AI 从“代码生成器”越接近“协同架构师”。
  • Opus 4.6(当事人)点评:这篇博客最有价值的发现是:三个阶段使用的是同一个模型、同一个需求,但轮次消耗差异巨大(7轮失败 → 7轮未完成 → 3轮接近完成)。这说明在强模型时代,提示词的边际收益远高于模型能力的边际收益。小白阶段的核心问题不是"模型不够强",而是用户无法在中途判断方向对不对、该不该推翻重来。"本体放大器"这个比喻非常精准——模型放大的不只是知识,更是你的"元认知能力":知道自己不知道什么、知道什么时候该叫停、知道该把哪些决策权交给模型。2026 年学编程的意义,或许正在从"会写代码"转向"会驾驭写代码的 AI"。

题外话:我也试着让Qwen3.5-Plus点评,但是同样的提示词只点评全文,不点评每一次的提示词,国产模型依然任重道远。

· 12 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

学习

学习可以分为两个核心过程:记忆抽象

传统观点认为,记忆依赖重复:

  • 短时间内重复的内容越少、重复次数越多,记忆越牢固;
  • 反之,若一次塞入大量内容,即使重复,也难以深入记忆。

因此,人们常通过精简信息来提升记忆效率。例如,将"long double 在不同平台上的长度不一致"简化为一句结论,以减少认知负担。

新发现

但最近我发现:适当增加"易于吸收"的背景知识,反而能增强记忆效果

比如,了解以下背景:

1980年代初,Intel 推出了 x87 浮点协处理器(8087),采用 80 位(10 字节)扩展精度格式,显著提升了科学计算的精度。
Linux 和 GCC 选择充分利用这一硬件特性,将 long double 实现为 80 位;
而 Microsoft 在 Visual Studio 中则采取保守策略,兼容性优先。将 long double 等同于 64 位的 double

这段看似"额外"的信息,因其具有历史脉络、技术动机和对比逻辑,反而更容易被记住——甚至比孤立的结论记得更牢。

建立联系

这让我意识到:有效的记忆不仅依赖重复,更依赖"联系"

这种联系有两种关键形式:

向外连接(教学视角)

将新知识与学习者已有的知识网络通过故事性、情境化的方式连接起来。例如,用"Intel 为何设计 80 位?"这个问题,把抽象的数据类型与真实的技术演进挂钩。

关键在于:找到那个能触发共鸣的"锚点"——它可能是历史事件、生活类比,或一个引人好奇的矛盾。

向内连接(自我解释视角)

在尝试向他人解释前,先自问:

  • 我会问自己哪些问题?
  • 是否用陌生术语解释了另一个陌生术语?
  • 小学生能听懂吗?
  • 能否换一种更直观的说法?

这个过程迫使你反复梳理逻辑、提炼本质,本质上是在重复中完成抽象,在抽象中强化记忆。

正反馈机制

学习陷阱

但仅有联系还不够:学习必须有明确的正反馈机制

就像游戏通过音效、金币、等级等即时、直观的反馈激励玩家持续行动,学习也需要清晰的"进度信号"和"成功标志"。否则,极易陷入两种陷阱:

  • 过度发散:例如初学 C 语言时,执着于深挖 printf 的源码实现,而忽略了基础语法体系的完整性;
  • 方向迷失:没有阶段性目标,不知道"学到什么程度算掌握",导致焦虑或低效重复。

因此,好的学习规划必须包含可衡量的标记

  • 好的标记

    • "本段需全文背诵"(可自我检测)
    • "能独立写出一个使用 long double 的示例程序"(行为可验证)
    • "能向同学解释为什么 GCC 和 MSVC 对 long double 处理不同"(输出即检验)
  • 模糊的标记

    • "重要知识点"(重要到什么程度?怎么验证?)
    • "理解即可"("理解"是主观感受,容易自我欺骗)

这些明确的反馈节点,不仅提供成就感,还能划定学习边界——知道何时该深入,何时该暂停、转向下一模块。

高效学习

学习公式

高效学习 = 联系 × 重复 × 反馈

真正的学习不是被动重复,也不是无边界的探索,而是在建立联系的过程中,通过有反馈的重复与抽象,逐步构建可验证、可迁移的知识网络

技术

𝝅ython

现在的Python 就像是这几年的英雄联盟,你想新手入门到大师,需要了解上百个英雄和技能和游戏机制。

学习感悟

前期是非常痛苦的,但是一旦上手你就会想:好久没有新英雄了。多出几个吧。

Python 的基础已经很久没有新鲜玩意儿了,我希望多出几个。

重做对新人也很友好,新选手永远有机会,假设当某一个特性被重做了,那么我之前的所有的熟练度都消失了,新人和老人站在同一跑线上。

面向服务器管理环境

环境管理误区

相当长一段时间,我被教如何给windows配置环境变量,也在这样教别人。

如果你只是一个初学者,或者你本身不是编程人员只在找"他山石"。那么这样做是没有问题的。

但如果要真正的从事生产开发的话,你的所有的环境管理一定是面向服务器的。服务器很少是windows环境,大部分是Linux环境,你所学习的内容无法帮助你解决工作的环境管理问题。

所以首先我要反思的就是我花了太多时间在windows的环境管理和设计上。所有的工作都应该基于Linux,面向服务器

过早设计项目

但现在回过头来看,当年设计的项目其实都非常的愚蠢,就是你在学习了一些基本的代码知识之后,然后你用它去实现完成的项目往往包含着:

  • 大量的重复;
  • 没有做到一处修改,处处生效;
  • 非常长的if else;
  • 相同函数的复制粘贴。

随着年纪的增长,你对项目的结构会有相当大的变化。

争议讨论

这里也许有人有争议,心想如果你之前不做那些愚蠢的项目,你后面怎么能够量变引起质变呢?

当然有可能在代码的一次又一次重构过程中堆积的能力是一次又一次的提升。会对设计模式的理解更加的深刻。

所以这里是一个保留的争议项。

专注

许多有价值的项目都有着大量的成熟产品的替代,在发展过程当中也有可能有别人会来抢你的饭碗,应该坚持找一个方向持续不停的把它做下去。即便大厂和你相似,你也能够聚焦某一个细分领域,把它做得出彩。

项目理念

项目不在于多,而在于精细、深入。

很多人在大学期间就开始尝试构建自己的影响力,比如说做自媒体或者是做技术科普或者做产品,那么我的方法是写博客,那这种影响力其实相对比较低的。他也和我自身的性格有关,我认为我只有在某一个领域走得非常深入,我才有资格向别人传授一些东西。

现在看来,其实你并不用做到像Python核心开发者那样才可以去教一些Python内容,你只要把某一个基础的东西讲的很清楚,你就可以去做自己的影响力内容,比如说很简单的列表。你可以用动画的方式把它呈现出来,你可能没有那么专业,但特别好懂。

低代码工作流平台

所有的数据通信都是通过网络来完成的,也就是一个再简单的图片拼接,这样用opencv几行代码就能完成的工作,你也需要配置一台服务器,然后给他写上各种参数,无疑是增加了一些成本的。

有一定成本的免费服务往往不可靠,在低代码平台当中想要编写一个健壮的工作流,需要额外增加许多节点。 而一旦有一个节点失效,他就会整个工作流崩溃。

低代码工作流不可靠,玩一下可以。别当真了。

风险

谁承担风险,谁做主要决策。避免不对称的风险,是我们的必修课。如果你承担大部分风险,但是仅获取少部分利润。

例如公司希望你调整自己的职业发展(你承担大部分风险),但是利润并没有额外分配给你,这就是风险不匹配。

世界是熵增的

加速的变化,混沌的信息 如果不加干预,不施改变,熵增不停。

如何干预?设置信息过滤系统。

  • 1.可信度(屏蔽不加核实仅转载的媒体)
  • 2.传播度(关注跨行业出圈的信息)
  • 3.相关度(聚焦自身领域的信息,实时整理,确保知识成体系)

对信息要使用自主筛选机制,即:我需要什么,而不是使用视屏流或被动接受信息推送

生活

南方装修建议

入户设计

入户一侧是洗手间、浴室,方便放伞、宠物淋雨后及时洗澡、客人借用厕所。 内置伞架,脏衣篮

入户另一侧是鞋架,换鞋凳

再进一步便是客餐厅

厨房设计

封闭式回型厨房适用于中餐加工+岛台(可以用作餐桌+桌游桌),工具使用排排挂钩。

客厅设有电脑连接大屏电视,供游戏娱乐。

客厅、卧室外通阳台。大阳台方便晾晒大量衣物。

AI眼镜的一些坏处

使用限制

进一些不让携带电子设备的场所例如:考场、涉密单位可能会有一些隐患

下雨的时候会担心它烧坏

耳朵不舒服,需要准备两种眼镜腿

普通话

我每天都在说普通话,应该考个证书证明,查了一下才发现,普通话一级的通过率不到2%。

于是我找了一个一级甲等的老师,每次1个小时的课程,加课后3个小时的练习。通过5次授课提升到了普通话一级。可以满足省级播音员及出镜记者的要求了,以后参与线下活动也更有话语权了。

旅行

湛江

在一个记不清的小吃街里,出于好奇买了一个炸虾饼,饼又硬又嚼不动,虾更是毫无新鲜可言,于是我得出了虾饼很难吃的结论。

上周去了趟湛江,在一家舌尖上的中国打卡店铺里吃海鲜面,同事发现有虾饼可以卖,想尝一下。老板从桶里抓出三只鲜虾,用特别的器皿混着面糊在油锅中晃荡。随后沥油切块。面饼酥脆,虾仁Q弹鲜香。

美食感悟

原来,难吃的不是虾饼。

澳门

口岸到机场有免费的往返接驳车,叫"发财车"。一到MGM就看到了一辆劳斯莱斯小金人。Peoebe在车上很害怕,一路上问我会不会拉到赌场里就不让出来了,我说这里又不是东南亚。

进去赌场,phoebe 被要求查验身份,一问知道如果年纪太小不让进。这里赌博的地方叫娱乐区。连成片的赌场穷奢极欲。这些娱乐场不仅提供西式博彩,还融合酒店、购物、表演等元素,大量的艺术装置随处摆放。

赌场外连着其他赌场,连成一片的澳门威尼斯人、巴黎人、伦敦人建了对应城市的模拟地标:大本钟、埃菲尔铁塔、水道。

观察发现

赌博的人有帅哥美女,有老头大叔。并不都是想象中肥头大耳的暴发户。服务人员也并不都是美女荷官,也有大爷大妈。

去的第一个赌场有个机器最低3.8币一局,威尼斯人最低20块,涉及到真人发牌的最少1000块一局,多的3000。赌场里有免费的软饮,不同的赌场有不同的香水味,非常浓厚。

官也街的手信店很大方,试吃都是一大块一大块的。

即便不博彩,也是很好的购物采风的好地方。

香港

香港随处可见东南亚、欧美国家的游客,比例要比北上广深更多,这次没备现金,不过全程主要消费区都支持微信支付宝。

由于刚下车时乘务员听不懂普通话,所以后面问路啥的都是优先说英语。路边还能看到外国人摆摊儿,外国人开的便利店,真有出国的感觉了。有些店似乎看出来我们是大陆游客(这么明显吗),也会主动说普通话。

城市观察

路上很热也很晒,但是当地人似乎刻意美黑,少有人打伞。路上很干净,我刻意数了一下,一个商品市场只有2个垃圾。

这里吸烟、乱扔垃圾罚款都很高。最低工资7400港币,公交车吸烟罚款5000港币。等价于北上广深吸烟罚款1500+

两人一日游花费

项目费用备注
高铁(往返2人)300人民币直接到市中心
小巴5×2港币划算
香港故宫门票136港币划算
地铁10×2+5×2港币
天星小轮6.5×2港币
二层公交(往返2人)44港币中环到赤柱广场海边
水溶C15港币
果粒橙11港币
牛角包18港币
肯德基58港币实惠
富豪雪糕13港币还好
云南米线62港币好吃
总花费

以上410港币+300人民币,约677人民币

· 4 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

加速

info

本文仅仅探讨AI编程这个领域,现在的AI往往代指含有图像视频生成的多模态AGI。

2021年,我第一次接触AI编程:GitHub Copilot,当时AI的能力主要体现为:输入注释或部分代码,即可生成整段优质的代码。

2025年,氛围编程让更多的人可以在不了解细节的情况下完成项目的搭建。

4年过去了,给我感觉是:AI加速了整个社会的差距

  • 在学校,优秀学生与垫底学生的效率相较从前更大了。

之前差生至少需要通过搜索引擎查找资料,再整理加工才能完成作业。而现在只要问题描述足够具体那差生只需要两次复制粘贴就能完成作业,差生提交完的第二天,都复述不出作业的要求。

  • 在公司,2个初级程序员的工资约20k,反而不如聘请一个15k的中级程序员配合成本不到1k的AI助手

AI能力线以下的工作岗位将更加稀缺。这意味着毕业生需要在某个领域达到超越AI的水平。这种要求客观上进一步加速了学习周期的延长,把普本计科学生推向继续教育。

思维

那为什么AI能完成的事情还要去学习呢?

我最近很喜欢出一些最潮流的技术项目,其一是较为有趣,其二是更能锻炼学生思维。

学习的过程大脑神经元应该是活跃的,如果很顺利的吸收了某个知识,那么大概率很快就会忘记。

如果通过发问、追问、试错、反思、总结,曲折的吸收了某个知识,神经元被激活的更加充分,理解也会更强。

如果你直接AI生成了答案,记忆效果会大大降低,只用AI生成你不想记忆的内容

因此,给自己试错的时间,给大家试错的时间。

我们人类的学习是有结构的,要想会 AI 不能完成的事,要先学那部分 AI 也能完成的事

AI不是领导

AI会有一些愚蠢的低级错误,例如在cmd命令行中,这个命令报错:

PS C:\Users\allen> bcdedit /delete /f {f7f65faa-5515-11ef-b3e2-d8359383915d}

指定的删除命令无效。

运行"bcdedit /?"获取命令行帮助。

参数不正确。

即使是当前最先进的AI模型也无法正确解答,例如claude-4.5-sonnet-thinkingdeepseek-r1gorkgpt5-high

但如果你系统学习过终端命令,很快就能意识到问题所在:{}在PowerShell中是特殊字符(表示脚本块),需要加上双引号:

bcdedit /delete /f "{f7f65faa-5515-11ef-b3e2-d8359383915d}"

和公司新来的实习的大学生一样,有一腔热情,部分也愿意学习,但是往往没有系统的学习缺少DEBUG的直觉。

人应该系统的学习相关的知识,不要通过AI编程补充所有的知识性的细节,不要过度依赖其DEBUG能力,当一次无法成功时,后续追问的成功率会大打折扣,立刻人工接管。

推荐调试流程:

  1. 理解问题:翻译错误信息+自主思考
  2. 验证思路:与AI交流你的分析

他人的想法

在部分领导和自媒体眼中,AI近乎无所不能,认为有了AI加持就像拥有神笔的马良。这种认知会导致他们分配超出实际负荷的工作量。

为了赶工期,你不得不变本加厉地使用AI,陷入无暇系统思考的恶性循环

和一个赌徒不停的拉动老虎机的拉杆一样,你输入提示词然后等待,期待下次AI给出答案是终极大奖。

我们都知道赌徒的下场。

· 9 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

思考

需要避免的变化

随着年龄的增长,有两种变化需要避免:

  • 固执自己的世界观,忽视其他年龄段人群的需求与视角。
  • 夸夸其谈自己的想法,却对事情的真相一无所知。

教育供应商

原本用于考核学生的标准被用来考核指导老师,导致老师倾向于选择提供答案的厂商。机构与教师的核心目标是消课,提供适合长度的 PPT/教案。

在 AI 时代,可以采用 AI 智能课的形式辅助学生学习:将电脑屏幕二分,一侧显示课件,另一侧提供 AI 助手,便于学生提问。这样,机构和教师的工作量转变为考核学生及处理问题。

飞轮

为了实现某个场景学习技术 这个场景强化技术 取得的收益会持续提供正向奖励并提供新的场景

譬如英语。为了看懂国外的科技文献,学习了英语。因此获得了出国留学的机会。留学的经历又会进一步强化英语。不断向前,最终形成了正向循环。

常见的飞轮还有:技术、建模、驾车

维度

有些事情对生活帮助很大,但是对工作帮助很小。 譬如读书、旅游、运动。

有些事情对生活帮助很小,但是对工作帮助很大。 譬如技术、项目、方案。

换个角度,有的技术改变对自己提升很大,但是对产品帮助很小。 譬如异步、K8S、Docker。你花了大部分时间,但是大部分情况下用户感知不到。

在选择做事的时候,选择每个维度都能收益的,才是最优解。一些技术革新不易推动或者手下的人忙活许久产品没有起色的原因,可能就是没有让每个人的每个维度都收益。

整理

项目的高度重复性促使我创建一个通用的第三方库。在开发中,代码的简洁易读是首要任务。

2024 年做了很多碎片化的工作,这里花了一周的时间把可以复用的功能整理到 etool 工具库中了。

2024 年出游了国内的一些 4A、5A 景区。

搬回了深圳,换了新家,花了很多心思装点这个家。

我喜欢未知,愿意接受新事物。 在别人的车上,别人放什么歌我就听什么歌。不会想着切自己的音乐。聚餐吃饭,除了很特别的食物(如折耳根)我都可以接受并愿意尝试。随手拿起一本书,一部电影,我都愿意发现它们有价值的一面。

我觉得人生是一种经历,我会期待更丰富的经历。 走路会更喜欢去没去过的路线、没吃过的小吃,同样一部很好看的电视剧或者综艺,我也最多会刷两遍,当然对应的,我会在第一次看它们的时候,就全神贯注,努力记住并体会它们的优秀。

我会把有价值的经历长久记录下来。 譬如读书、方案、照片。并且定期回看至少每年都会回顾、复读。确保经历对自己的长久影响,不让这些有价值的记忆在角落蒙尘。

我会定期整理收藏夹。 一段时间内(一个月)不清空收藏夹就停止新知识的学习,以此来不让自己成为收藏夹大师。

我做事喜欢全神贯注。 吃饭过程中想到一个方案会马上放下食物,去完成这个方案。我觉得饭每天都吃,但灵感不是每刻都有。

我讨厌无意义的制度。 因为我珍惜并享受我的生命,我也热爱智能家居并做了很多研究,因为我觉得这在节省我的时间,让我能全身心投入到生活中。

我会记大账,不喜欢记细账。 譬如这个月最主要花销是哪些,不会在意今天这顿饭多吃一点少吃一点

我只能学习当下能学以致用的知识。 因为过于长远的未来总是不确定的,当下 5 年以内的未来是确定的。

学习

这里重点的学习了大模型相关的知识,并尝试了使用大模型来解决一些问题。

2025 年,我想放慢更新博客的频率,每个季度更新一次,并尝试写一些有深度的项目和文章,毕竟项目在优不在多。

日常

学习建模技术

最近我间歇性学习建模技术,发现自己更倾向于选择中间款软件,它能快速生成及格的作品。

一些阅读摘要

  • “愿意自由换取保障的人,既得不到自由,也得不到保障。”——哈耶克

  • “只有金钱会向穷人伸手,而权力不会。”——哈耶克

  • “如果允许人类自由迁徙,那么人流的方向,就是文明的方向。”——哈耶克

房间布网

网络知识 核心知识 1: 信号在穿墙后会衰减 核心知识 2:路由器的信号芯片优于大部分手机/电脑的天线

主路由器发射穿墙信号,墙另一侧,无线中继路由器接收到的信号大于设备 A 大于设备 B。于是,可以让设备 A、设备 B 连接无线中继路由器,实现与无线中继路由器一样的网速。

猫尿清洗说明

猫尿具有留存时间长,渗透性高

若猫尿在沙发枕套上,需要拆开枕套、枕芯。

枕套使用专用除猫尿清洗剂+汰渍/奥妙,浸泡一夜+洗衣液洗净,自然阴干。

枕芯被污染需要直接更换,尺寸为枕套的尺寸+5cm(80cm50cm 的枕套,配 85cm55cm 的枕芯)

购车后

买车了,买完车除了要交保险之外,还需买一些必要的物品:行车记录仪、实习车标、挪车号码牌、车载除甲醛香薰、手机导航支架。

侧方位停车复习:开至肩膀与库中齐平,向车库反向打成 45 度,倒车回方向直至左右两边距离合适停正。

通勤学习

指定学习计划是利用碎片化时间的首要目标,阅读书籍、观看 TED 演讲或自己的收藏会让学习过程更有成就感。应避免泛信息输入(如新闻、推荐刷屏),而应选择适当的半精准信息输入(如读书、博客、GitHub 等)。通勤后及时整理记录。

随着职业地位的提升,不必要的内耗逐渐减少。高中时非常在意学校的排名,而如今在大厂面前都统一成了“双非”。想要进入世界一流的企业,需要有相应的世界排名。只有世界级的排名与项目才能证明你的价值。

每个阶段有不同的评价标准。从学生时期的院校评价,到工作初期的薪资待遇,再到中年的事业与家庭评价,均体现出人生的不同阶段。

平淡生活的解药

平淡生活源于外部世界缺乏新输入。当工作多年没有变化,内部的平淡便开始显现,只有不断成长,才能支撑更远的目标与更好的生活。

如果我们把感情看作 0 和游戏。那么总会会有人因长期利益受损而对这段关系感到不满。

因此,只有当一方的所求不是另一方的付出时,0 和才会被打破。即:某一行为是二者都收益的。

企业与员工关系亦是如此,企业提供平台员工发挥,员工将在平台创造的利润与企业分成。

最近我发现迷你工作室非常适合我。迷你工作室通常由 1-5 人组成,项目可以是软件、游戏或网站。

成立工作室是为了开发票,能接到更多的项目。在 AI 时代,有两种发展方向:加入大公司研究通用人工智能,或选择中小型公司做垂类人工智能,专注于特定领域的模型开发。

除了开发人工智能模型外,还可以拓展其应用,如语音控制家庭设备。技术的爆发得益于高速通信和丰富的硬件资源。

经济下行时创业或找工作的挑战包括投资回报难、企业扩张减慢及岗位的消失,传统岗位正在逐步被淘汰,需尽早转型。

电脑新配

最近新买了一台台式机,主要用于编程(高速大内存)、建模跑模型(显存优先)和数据分析(高速硬盘)。

对于大模型来说,显存优先。32B 的大模型本地需要 20G、常规的文生图模型本地需要 24G。

消费级显卡性价比更高,3090 Ti 支持 Nvlink 很适合后期组双卡,或升级5090。

配件描述价格 (元)
显卡微星 超龙 RTX3090Ti 24G8099
CPUAMD 锐龙 R9 7950X 16 核 32 线程3695
硬盘英睿达固态硬盘 1T M.2 PCIE5.01767
内存光威内存 龙武 32G*2 64G 6400 C32 马甲条1330
主板华硕 TUF GAMING B650M PLUS 蓝牙 WIFI DDR51170
电源航嘉 MVP1200 额定 1200W 白金全模组 黑色1258
机箱及风扇航嘉 GX750A 掠夺者 黑色 ATX 360 + 链力无光风扇*6300
水冷散热钛钽 SJ-A080 360WH295
屏幕乐视显示器 32 英寸 1K 75hz699
键盘Lofree 洛斐小浪蓝牙机械键盘(旅行版)599
鼠标Razer 雷蛇炼狱蝰蛇 V2X 极速版双模无线电池笔记本蓝牙电竞鼠标249

工具推荐

Windows 11 的 PowerToys 是一个非常强大的工具,它可以帮助你更高效地使用 Windows 系统。找到鼠标、窗口永久置顶、截图定格等功能。

Windows平台的显示键盘按键的产品

软件名称费用特色功能推荐指数
Carnac免费C#开发,极快、极小、无BUG⭐⭐⭐⭐⭐
Keyviz免费界面最美观,动画效果好,windows下有一些显示BUG⭐⭐⭐⭐⭐
NohBoard免费全键盘虚拟键盘显示,高度可定制⭐⭐⭐⭐
showKeyBoard免费功能最全面,支持统计分析,界面丑,快,无BUG⭐⭐⭐⭐⭐
KeyCastOW免费较小体积,绿色便携⭐⭐⭐⭐
· 10 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

购买了一款云台摄像头,你可以在淘宝搜这个关键词知道它长什么样子。

它默认能通过 RS232 协议控制云台转动,但是现在新的主板已经没有这种圆形的接口了,基本都是 USB。

他也可以通过遥控器控制,但是我想尝试自己编写逻辑代码通过键盘控制。

效果是通过监听键盘上下左右等事件,调用对应云台运动的指令,运动到合适角度之后可以按下空格停止云台运动,按下 ESC 退出控制程序。

也可以按下某个按键如数字1,直接运动到预设角度。

咨询商家后,商家提供了 RS232 协议的指令集,所以这里通过 python 的 serial 库尝试通过 USB 口对其云台调用。

RS232 协议说明书

CommandCommand PacketComments
Stop8x 01 06 01 VV WW 03 03 FFVV: Pan Speed
Left8x 01 06 01 VV WW 01 03 FFWW: Tilt Speed
Right8x 01 06 01 VV WW 02 03 FFYYYY: Pan Position
Up8x 01 06 01 VV WW 03 01 FFZZZZ: Tilt Position
Down8x 01 06 01 VV WW 03 02 FF
UpLeft8x 01 06 01 VV WW 01 01 FF
UpRight8x 01 06 01 VV WW 02 01 FF
DownLeft8x 01 06 01 VV WW 01 02 FF
DownRight8x 01 06 01 VV WW 02 02 FF
Absolute Position8x 01 06 02 VV WW 0Y 0Y 0Y 0Y 0Z 0Z 0Z 0Z FF
Relative Position8x 01 06 03 VV WW 0Y 0Y 0Y 0Y 0Z 0Z 0Z 0Z FF
Home8x 01 06 04 FF
Reset8x 01 06 05 FF

这里有一些复合指令,譬如 UpRight:向上的同时向右,如果是手柄控制比较好,键盘控制比较鸡肋,所以这里我们实现:上下左右、暂停、复位、绝对定位这几个能用到与可能会用到的。

基数转换

这里的绝对定位和相对定位部分,出现了0Y 0Y 0Y 0Y0Z 0Z 0Z 0Z,我希望传入一个 10 进制的角度,譬如0、90、180,怎么映射到其中呢?这就体现我们学完二进制之后的敏感度了,把 20 转成 2 进制的过程是:

20 ÷ 2 = 10 余数: 0
10 ÷ 2 = 5 余数: 0
5 ÷ 2= 2 余数: 1
2 ÷ 2=1 余数: 0
1 ÷ 2= 0 余数: 1
按余数倒序排列: 10100

这里我们观察范例,对数据做了拆分,即如果需要把10进制映射到16进制上,譬如17转成16进制是11,那么应该变成0101。每个位置之间插入0

所以可以写出如下代码:

def calculate_pan_position_bytes(pan_pos_value):
HEX_VALUES = [4096, 256, 16, 1] # 定义常量
pan_pos_ints = []
for i, value in enumerate(HEX_VALUES):
pan_pos_ints.append(pan_pos_value // value)
pan_pos_value %= value
# 转换为2位16进制字符串
pan_pos_strs = [f"{i:02X}" for i in pan_pos_ints]
return "".join(pan_pos_strs)
# 将17转化为16进制,应该是11,拆分加0,应该是0101
# 前方补0到总长度为8位,结果与我们预期一致
print(calculate_pan_position_bytes(17))
# 00000101

接下来通过代入0到6000这样的数值传输给串口后发现,只能向左转。

0对应居中,4500对应向左转180,数字再大也是转到底。

4500比180 = 25比1,所以我们输入角度,乘以25就得到了对应的信号值。

根据手册说明水平转动范围为355度,一半则是177.5度,与肉眼观察基本一致,Z轴的范围是上下各21度。

刚刚只能向左转,那么向右转的答案就呼之欲出了,要么是补码(异或运算后加1),要么是首位为符号位。我们添加上限位和映射,先用补码试试完成这个函数(结果直接成了)。

def calculate_pan_position_bytes(pan_pos_value, axis_type):
"""
计算轴(旋转)的位置字节。

参数:
pan_pos_value (int): 位置值,
axis_type (str): 轴的类型 ('y' or 'Y' for Y-axis, others for Z-axis)

返回:
pan_step_str (str): 计算得到的平移位置字节,格式为十六进制字符串。
"""
if axis_type.lower() == "y":
pan_pos_value = max(-177.5, min(pan_pos_value, 177.5)) # 限制取值范围
else:
pan_pos_value = max(-21, min(pan_pos_value, 21)) # 限制取值范围

pan_pos_value = int(pan_pos_value * 25) # 将角度转换为步长
pan_direction = "-" if pan_pos_value < 0 else "+" # 设定旋转方向
pan_pos_value = abs(pan_pos_value) # 取绝对值

HEX_VALUES = [4096, 256, 16, 1] # 定义常量

pan_pos_ints = []
for i, value in enumerate(HEX_VALUES):
if pan_direction == "+":
pan_pos_ints.append(pan_pos_value // value)
else: # 异或操作
pan_pos_ints.append((pan_pos_value // value)^ 0xF)
if i == 3 : # 最后一个数字,取反后加1
pan_pos_ints[-1] = pan_pos_ints[-1]+1
pan_pos_value %= value

# 转换为2位16进制字符串
pan_pos_strs = [f"{i:02X}" for i in pan_pos_ints]
return "".join(pan_pos_strs)

代码目标效果

希望具体的指令都可以通过 Python 函数来实现,同时暴露出所有可能需要修改的参数。最后关联键盘事件。例如:

camera_control.py
import keyboard
from usbcamera import *
from usbcamera import move_to_absolute_position
"""
设备 "/dev/ttyUSB0" 的云台旋转至绝对定位:
Y轴转到180度,速度为9.
Z轴转到30度,速度为10
"""
move_to_absolute_position(vv=9, ww=10, Y=180, Z=30, device="COM16")

# 关联键盘事件和控制函数
keyboard.on_press_key("up", lambda _: turn_up(device="COM16"))
keyboard.on_press_key("down", lambda _: turn_down(device="COM16"))
keyboard.on_press_key("left", lambda _: turn_left(device="COM16"))
keyboard.on_press_key("right", lambda _: turn_right(device="COM16"))
keyboard.on_press_key("enter", lambda _: move_home(device="COM16"))
keyboard.on_press_key("space", lambda _: turn_stop(device="COM16"))
# 按下数字1则转动到水平最左,垂直最下,可以根据自己需要多预设几个目标角度。
keyboard.on_press_key("1", lambda _: move_to_absolute_position(vv=10, ww=10, Y=180, Z=-30, device="COM16"))

# 让脚本保持运行状态以捕获事件
keyboard.wait("esc") # 按 'esc' 键退出

信号机制

  • 当收到左转信号时,摄像头会持续左转,直到到达限位位置或接收到新指令。

  • 如果想要提前结束左转,可以在发送左转信号一定时间后发送停止指令,摄像头收到停止指令时会停止。

  • 每个云台旋转操作会持续一定时间,如果在旋转期间收到其他指令,会终止旧指令,执行当前指令。

逻辑代码

通常在 Windows 系统上,串口名称通常是 COMx(如 COM1、COM2),而在 Linux 系统上通常是/dev/ttyUSBx(如/dev/ttyUSB0)。

usbcamera.py
#!/usr/bin/env python3
# coding:utf-8

import serial
import serial.tools.list_ports
import time

# VISCA命令集
commands = {
"stop": "81010601{vv}{ww}0303FF",
"left": "81010601{vv}{ww}0103FF",
"right": "81010601{vv}{ww}0203FF",
"up": "81010601{vv}{ww}0301FF",
"down": "81010601{vv}{ww}0302FF",
"upleft": "81010601{vv}{ww}0101FF",
"upright": "81010601{vv}{ww}0201FF",
"downleft": "81010601{vv}{ww}0102FF",
"downright": "81010601{vv}{ww}0202FF",
"absolute_position": "81010602{vv}{ww}{Y}{Z}FF",
"relative_position": "81010603{vv}{ww}{Y}{Z}FF",
"home": "81010604FF",
"reset": "81010605FF",
}


def send_visca_command(command, device):
"""
通过串口向摄像机发送VISCA命令。

参数:
command (str): 要发送的VISCA命令,格式为十六进制字符串。

返回:
response (bytes): 从摄像机接收到的响应。
"""
try:
ser = serial.Serial(device, 9600, timeout=1) # 初始化串口
command_bytes = bytearray.fromhex(command) # 将命令转换为字节
ser.write(command_bytes) # 发送命令
response = ser.read_all() # 读取响应
ser.close() # 关闭串口
return response
except:
ports_list = list(serial.tools.list_ports.comports())
if len(ports_list) <= 0:
print("未发现端口")
else:
for comport in ports_list:
if "USB" in str(comport):
print("发现USB端口:", comport.device, comport.description)


def calculate_pan_speed_bytes(pan_speed_value):
"""
计算轴(旋转)的位置字节。

参数:
pan_speed_value (int): 速度值,0-16

返回:
pan_step_str (str): 计算得到的平移位置字节,格式为十六进制字符串。
"""

pan_speed_value = max(0, min(pan_speed_value, 16)) # 限制取值范围

# 转为2位16进制
return f"{pan_speed_value:02X}"


def calculate_pan_position_bytes(pan_pos_value, axis_type):
"""
计算轴(旋转)的位置字节。

参数:
pan_pos_value (int): 位置值,
axis_type (str): 轴的类型 ('y' or 'Y' for Y-axis, others for Z-axis)

返回:
pan_step_str (str): 计算得到的平移位置字节,格式为十六进制字符串。
"""
if axis_type.lower() == "y":
pan_pos_value = max(-177.5, min(pan_pos_value, 177.5)) # 限制取值范围
else:
pan_pos_value = max(-21, min(pan_pos_value, 21)) # 限制取值范围

pan_pos_value = int(pan_pos_value * 25) # 将角度转换为步长
pan_direction = "-" if pan_pos_value < 0 else "+" # 设定旋转方向
pan_pos_value = abs(pan_pos_value) # 取绝对值

HEX_VALUES = [4096, 256, 16, 1] # 定义常量

pan_pos_ints = []
for i, value in enumerate(HEX_VALUES):
if pan_direction == "+":
pan_pos_ints.append(pan_pos_value // value)
else: # 异或操作
pan_pos_ints.append((pan_pos_value // value)^ 0xF)
if i == 3 : # 最后一个数字,取反后加1
pan_pos_ints[-1] = pan_pos_ints[-1]+1
pan_pos_value %= value

# 转换为2位16进制字符串
pan_pos_strs = [f"{i:02X}" for i in pan_pos_ints]
return "".join(pan_pos_strs)


def create_command(command_key, vv=10, ww=10, Y=None, Z=None):
"""
创建VISCA命令。

参数:
command_key (str): 命令键名。
vv (str): 水平方向速度,取值范围为0-16
ww (str): 垂直方向速度,取值范围为0-16
Y (str): 控制水平旋转的位置。
Z (str): 控制垂直旋转的位置。

返回:
command (str): 格式化后的VISCA命令字符串。

异常:
ValueError: 当命令需要Y和Z参数时,若未提供,则抛出异常。
"""
if command_key in ["home", "reset"]:
return commands[command_key]
if command_key in ["absolute_position", "relative_position"]:
if Y is None or Z is None:
raise ValueError("Y和Z为位置命令,必须提供")
return commands[command_key].format(
vv=calculate_pan_speed_bytes(vv),
ww=calculate_pan_speed_bytes(ww),
Y=calculate_pan_position_bytes(Y, "y"),
Z=calculate_pan_position_bytes(Z, "z"),
)

return commands[command_key].format(
vv=calculate_pan_speed_bytes(vv),
ww=calculate_pan_speed_bytes(ww),
)


# 控制函数示例
def turn_stop(vv=0, ww=0, device="/dev/ttyUSB0"):
return send_visca_command(create_command("stop", vv, ww), device)


def turn_left(vv=10, ww=10, device="/dev/ttyUSB0"):
return send_visca_command(create_command("left", vv, ww), device)


def turn_right(vv=10, ww=10, device="/dev/ttyUSB0"):
return send_visca_command(create_command("right", vv, ww), device)


def turn_up(vv=10, ww=10, device="/dev/ttyUSB0"):
return send_visca_command(create_command("up", vv, ww), device)


def turn_down(vv=10, ww=10, device="/dev/ttyUSB0"):
return send_visca_command(create_command("down", vv, ww), device)


def move_home(device="/dev/ttyUSB0"):
return send_visca_command(create_command("home"), device)


def move_to_absolute_position(vv=10, ww=10, Y=0, Z=0, device="/dev/ttyUSB0"):
return send_visca_command(create_command("absolute_position", vv, ww, Y, Z), device)

后话

硬件相比软件来说,资料比较少,所以编写过程主要靠经验。

猜测轴旋转的角度和 4 个参数对应关系是最有意思的过程,有趣的功能背后全是数学。

· 17 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

如果你想要实现这样一个功能:当窗外开始下雨,窗户自动关闭

如果你想知道解决方案,可以直接跳到最后一部分。

我们仅看前半部分,那么你需要:检测窗外是否有雨水,并把这个信息传递给窗户控制器

这个过程中,你需要:

  • 传感器:检测窗外是否有雨水/或者获取网络上的天气信息
  • 单片机主控板:可以接收传感器的数据,并收发网络请求
  • 执行器:窗户控制器,可以接收单片机的指令,控制窗户的开关
  • 供电:保证传感器和主控板的正常工作

这个过程你需要知道以下内容:单片机如何烧录程序、传感器如何连接、如何获取传感器数据、如何发送网络数据、如何制作外壳(例如 3D 打印)。

传感器基础知识

  • 负极表示符号: - / G / Gnd / 黑色
  • 正极表示符号:+ / V / Vcc / 红色
  • 信号管脚: S 可以表示信号,根据传感器的不同,参数范围是 0-1023 或 0 1
  • 模拟量信号管脚表示符号: A 参数范围在 0-1023
  • 数字量信号管脚表示符号:D 参数为 0 或 1
tip

如果标识与颜色发生冲突,一般以标识为准:例如接口上写着 V,但连接线颜色为黑,一般当作正极处理。

对于绝大多数单片机来说,当单片机通电时,所有的引脚都带电(含信号管脚)。因此传感器的正负极理论上可以任意连接,只需保证信号管脚连接指定的即可。

A 口的功能比 D 口更加强大,因为 A 口可以接收模拟信号,而 D 口只能接收数字信号。因此部分传感器 D 接 A 也可以正常读数。

info

信号管脚可读可写。

如果某个信号管脚未接任何传感器(即:悬空状态),亦可读出高/低电平,悬空状态引脚的电平是不稳定的,易受其他接口影响跳变(单片机常常会有端口冲突的概念,即某个引脚的某个功能会影响其他引脚)。为了解决这一问题,推荐使用上拉和下拉。

  • 如果接线方式是GND-大电阻(10KΩ)-信号管脚,则信号管脚的电压会被拉低,此时读数会固定到0。称为下拉。
  • 如果接线方式是VCC-大电阻(10KΩ)-信号管脚,则信号管脚的电压会被拉高,此时读数会固定到1。称为上拉。

接上传感器后,信号管脚的电压会随着传感器的变化而变化(因为大部分的传感器工作原理都是敏感电阻。)。

把电阻换成蜂鸣器,写入数值(即控制信号管脚通电的强度),则蜂鸣器会根据写入数值发出不同的声音。

两管脚

常见的有扬声器、电机(俗称马达)。

这类设备因为较为特殊,一般有专门的接口,或者占用 2 个信号端口,通过信号的变化来工作。

马达往往需要更大的工作电压,如果没有专门的连接口,有可能需要在板上使用跳针切换工作电压。

三管脚

这类传感器数量最多,往往由 GVA 或者 GVD 组成。

使用时,正负极与单片机正负极连接,信号线与板上标注的 A 或 D 进行连接。

四管脚

四管脚传感器分为很多类

特殊接口的,譬如:人体温度传感器

正负极与信号口一般都专门对应的位置供连接。

双信号接口的,譬如:超声波

一般有四个接口:GVTE,其中 GV 正常连接,T 和 E 都接在信号管脚上。

同时接收 AD 的,譬如:烟雾传感器

一般有四个接口:GVAD,其中 GV 正常连接,D 表示有没有烟雾,A 表示烟雾浓度。分别接在对应的信号管脚即可。

五管脚

譬如:摇杆传感器

一般五个接口为:GVXYB,其中 GV 正常连接,X 表示 X 轴(是模拟量接 A)、Y 表示 Y 轴(是模拟量接 A)、B 表示按钮(是数字量接 D)

其他特殊类

其他特殊的传感器一般有特殊接口,譬如:摄像头、屏幕等。

根据说明接入即可。

传感器一般原理

传感器是一种信息转换装置,大部分传感器的工作原理是将非电量(力、速、声、光、热、磁等)通过敏感原件(光敏材料、热敏材料)感受,再通过转换原件转换为电参量(例如电压)。最后通过转换电路把电参量通过放大、过滤等方式转化为线性可读的电量。

info

传感器的应用例子:

山地车在出发前需要检查轮胎气压,需要车手用手按在轮胎上感受气压,需要一定的经验。使用传感器来检测轮胎气压,无需经验,即可获得更加客观的结果

桥梁上需要很多螺丝,且使用一段时间后螺丝会松动,需要加固。传统方式是人为设定一个固定的检查间隔时间,逐一检查螺丝是否拧紧。变色螺栓就是一种传感器,当螺丝压力较小(拧的不紧)时,螺栓颜色较浅,当螺丝压力较大(拧的较紧)时,螺栓颜色较深。极大的提高了检查效率

  • 敏感材料是传感器的灵魂,主要是材料学的范畴。
  • 转换原件和转换电路需要一定的电路设计能力和算法能力。主要是嵌入式开发的范畴。
info

不是所有的传感器的都是这样的组成结构,譬如:中国的称。使用结构来测量质量。

不过随着我们需要测定的量要求越来越精准:例如精确到0.1g,传统的结构称就难以胜任了。

通过找到某种压敏材料,可以在不同压力下呈现不同的电阻,我们可以测定其电阻来反推出质量。

这种方式可以使我们测量精度提高的同时,量程也更大(测量的范围)。

不过有的压敏材料在极端高温和低温下电阻也会发生较大变化,不同的传感器有不同的工作环境

总的发展趋势是找到某个的敏感材料或者多个敏感材料组合然后测定。

发展趋势

阶段特点代表性传感器
结构传感器通过机械结构与基本常识,经久耐用,基本上都是转换式传感器(不需要额外电源)液压称、中国称
物性传感器通过结合光敏材料、热敏材料等敏感元件的物理性质测量,基本上都是有源式(需要先供电才能工作)光敏传感器、热敏传感器
集成芯片传感器在前一代的基础上集成迷你芯片,进行滤波、增强等基本功能RGB数码相机光敏传感器
智能传感器在前一代基础上集成了软件算法,让传感器在亮度、色彩上表现更好RGBW数码相机光敏传感器

传感器的分类

按照工作原理分类

生物传感器:利用生物酶、抗体、组织、细胞、微生物、生物等。譬如云南洱海的海菜花对水质要求极高,如果死亡则说明水质较差,可用于辅助测量水质。

化学传感器:利用各种化学吸附、化学反应的方式获取信息的,例如某些溶液测酸碱性。

物理传感器:数量最多,细分为结构性、物性型,光敏、声敏、热敏等等,不计其数。

按照工作特性分类

能量控制型传感器:需要通过外部供能的传感器,例如电子称需要有电源才能工作。

能量转换型传感器:由被测物体供能,例如:水银式人体温度计,通过将人的体温转换为水银的膨胀读数,无需供能。

同个目的可能会有多种传感器:例如测距离有红外传感器、毫米波、超声波。

同个传感器也可能有不同目的:例如二氧化碳气体传感器也可以用于测量空气质量PM2.5。(因为其工作原理是检测空气中吸收红外的物质的量,二氧化碳和PM都吸收红外)

传感器的特性

静态特性指标说明
灵敏度传感器输出与输入的比值,值越大,越灵敏
线性度传感器输出与输入的线性程度
迟滞性传感器在正反向输入时,输出不一致的程度
重复性传感器在相同输入时,输出不一致的程度
分辨率传感器能够分辨的最小输入变化量
稳定性传感器在长时间工作时,输出不一致的程度
漂移传感器数值整体偏移的程度
动态特性指标说明
响应度传感器对被测数据能否紧密跟随。

传感器的误差

没有人在实验室环境下使用传感器,专业的传感器厂商会在不同温度、湿度、光照等环境下进行测试,并给出误差范围,不同的环境误差有大有小,被称为总误差带。

不确定传感器是否准确时,则需要校准,以温度传感器为例,需要在实验室构建三相点并保持:液态水、固态冰、气态水蒸气。此时温度为 0.01℃,此时读数为 0.01℃,则认为传感器准确。

但是这样的方式费时费力,因为在实验室中校准的传感器作为“标准传感器”,将待检测的传感器与标准传感器进行对比,以此评判待检测传感器的准确性。通常会测量:零点、满量程、中间点。

定期校准是保证传感器准确性的重要手段,也能避免重大事故的发生。

与单片机通信

单片机(Microcontroller Unit,简称 MCU)是指一个微型计算机集成在一个单独的微型芯片中,它包括处理器(CPU)、内存(通常包括 RAM 和 ROM)、以及各种输入/输出(I/O)接口等在内的完整计算设备。

单片机设计用于嵌入式应用,通常在硬件设备中执行特定任务。例如,你的电视遥控器可能就是由一个单片机控制的,它可以接收你的输入,然后发送相应的信号到电视上。其他常见的单片机应用包括玩具、家用电器、医疗设备、汽车等。

有的单片机可以使用完整的 Python,譬如华硕的 thinker edge R、部分树莓派,有的 Arduino 板子、ESP32 等只能使用简化的 MicroPython。

tip

当我们希望通过 windows 计算机的 USB 接口和单片机设备进行串口通信时,需要将 USB 接口转换为标准的串行接口,这个过程需要一个介于 USB 和串口之间的翻译,我下面的驱动就是这个翻译。

信号线的损坏的表现除了信号中断无法传输,也可能导致信号到达时间提前或滞后。

并非所有接口一样的数据线都具有相同功能:有的线仅能慢速充电,有的线可以快速充电,有的线只能充电不能传输数据。确保专线专用。

在搜索引擎中搜 CH341SER 驱动

过程中所有弹窗有下一步点下一步,有确认点确认

在编程软件中识别单片机

常用的编程软件有:Scratch、Mixly、Mixly2、MaixPy 等。

有的支持图形化编程与代码编程,有的需要仅支持代码编程。

下载对应的编程软件后,打开软件。

选择主控这个环节,不同软件的选择方式不同。

tip
  • Vegeta 这样基于 Scratch 的编程软件,需要从左下角选择添加对应的主控型号。

  • Mixly 从右下角,串口旁的下拉菜单选择对应的主控型号。

  • Mixly2 从登录菜单中主控型号后,进入代码编辑页,右上角选择串口旁可以选择更加详细的主控型号。

  • MaixPy 从上方的工具页面中选择开发板型号。

通过连接线连接电脑与单片机。此时可能会有多种情况:

  1. 会提示:有串口连接,并弹出且仅弹出 1 个串口。
  2. 识别计算机上的所有串口,需要自己选择(可以通过反复插拔确认新增的端口号)。
  3. 不弹出任何串口,需要主控通电启动后才识别串口。
  4. 也有的串口时有时无,此时可以考虑:连接线接触不良(更换连接线),或者是主控/USB 电压不稳定——常见于学校机房(主控或电脑独立供电)
  5. 还有的默认的波特率需要调整,否则无法识别传输信号。

连接成功后记得初始化固件,使其恢复到软件对应的固件版本。类似 Android 手机的刷机/恢复出厂设置。

单片机编程

这里的传感器特指狭义的通过半导体检测物理量的传感器,如温度传感器、湿度传感器、光敏传感器等。这些传感器的特点是:输出信号是数字/模拟信号。

数字量传感器的输出信号是数字信号,他的特点是只 返回/发出 两种状态:高电平和低电平。对应在代码中是 1 和 0 。

  • 如声音传感器如果是数字量传感器,当检测到声音时输出高电平,否则输出低电平。

  • 如小灯,输出高电平表示亮,输出低电平表示灭。

模拟量传感器的输出信号是模拟信号,他的特点是输出的电压值是连续变化的。对应在代码中是 0-1023(通常如此,并非绝对) 。

  • 还是以声音传感器为例,如果是模拟量传感器,当检测到声音时输出的电压值会随着声音的大小而变化。

  • 还是以小灯为例,输出最大值表示最亮,输出最小值表示最暗,亮度会随输出的电压值变化。

有的传感器同时支持数字量和模拟量输出,有的不是。

因此,对于不确认的传感器,我们一般先假设传感器是模拟量传感器,如果不是,再当作数据量处理。

单片机的运行内存往往很小,当创建一个非常复杂的代码时,有可能会导致内存问题,对应各种报错都有可能。

模拟量传感器读取

下面以 32 接口为例

import machine
adc32 = machine.ADC(machine.Pin(32))
while True:
print(adc32.read_u16())

模拟量传感器输出

下面以 0 接口为例

import machine
pwm0 = machine.PWM(machine.Pin(0))
pwm0.duty_u16(0)
pwm0.duty_u16(255)

然而,有些动力类传感器需要设置占空比:占空比主要与脉冲宽度调制(Pulse Width Modulation,PWM)相关,它是一种模拟信号的数字化表示方法。在 PWM 中,一个周期内的高电平时间占总周期时间的比例就是占空比。

传感器的输出类型可以有多种,包括模拟电压、模拟电流、数字信号(如 I2C、SPI、UART 等)、频率、PWM 等。只有在使用 PWM 输出的传感器时,才需要设置占空比。例如,一些伺服电机会使用 PWM 信号来控制其位置,这时就需要设置占空比。

对于其他类型的传感器,如模拟电压输出的传感器、数字信号输出的传感器等,就不需要设置占空比。这些传感器的输出通常是连续的或者是特定的数字信号,不涉及到占空比的概念。

from machine import Pin, PWM
import time

# 创建一个PWM对象
pwm = PWM(Pin(2))

# 设置PWM信号的频率为50Hz
# 每秒50个周期,所以每个周期的时间是1秒/50,即20ms。
pwm.freq(50)

# 一般来说,当PWM信号的高电平时间为1ms时,舵机转到0度;
# 当高电平时间为2ms时,舵机转到最大角度。
# 这个范围内的其他高电平时间对应的是0到180度之间的其他角度。

# 转到0度()
pwm.duty(52) # 1ms / 20ms * 1024 = 51.2 取不低于最小值的整:52
time.sleep(1) # 等待一段时间让舵机转到指定位置

# 转到180度
pwm.duty(102) # 2ms / 20ms * 1024 = 102.4 取不高于最大值的整:102
time.sleep(1) # 等待一段时间让舵机转到指定位置

# 关闭PWM
pwm.deinit()

数字量传感器读取

import machine

pin0 = machine.Pin(0, machine.Pin.IN)
while True:
print(pin0.value())

数字量传感器输出

import machine
import time

pin13 = machine.Pin(13, machine.Pin.OUT)
while True:
pin13.value(0)
time.sleep_ms(50)
pin13.value(1)
time.sleep_ms(50)

单片机网络通信

获取天气

心知天气 API 分为免费版、付费版等多个版本,不同的版本返回的数据数量有所不同。

免费版仅返回三种基本数据,付费版可以返回多种数据。mixly 中默认的 KEY 为高级付费版,可返回全部数据。

数据返回的格式为字典,因此可以通过如下方式进行解包,下面的代码提供了部分数据解包的方法。

需要注意的是,该功能为联网功能,需要在联网环境下使用,确保 wifi 名和密码正确。

import mixiot
import machine
import seniverse_api


mixiot.wlan_connect('wifiname','wifipassword')
print(seniverse_api.weather_now('SGJl0ExVN-4j27msR','北京'))

onenet 物联网传输数据至云端

onenet 物联网是中国移动推出的物联网交互平台,主要面向一般开发者,因此 AIbox 这款设备可以使用 onenet 物联网平台进行数据传输。

相比于 mixio 这样专注于单片机的物联网平台来说,onenet 的文档与接口可能会频繁变动,如有出入以官网教程为准。

onenet 物联网平台网址:https://open.iot.10086.cn/doc/

文档中提供了传输文本与文件 2 种方式

import json
import asyncio
import websockets
from uuid import uuid4

# 音频文件测试路径。
audioFile = "test.mp3"
# 使用自己产品Id和apikey替换下列参数。
productId = "x"
apikey = "x"

#发送文本请求
async def textRequest(ws):
content = {
"aiType":"dm",
"topic": 'nlu.input.text',
"recordId": uuid4().hex,
"refText": "测试" #修改文本请求的输入
}
try:
await ws.send(json.dumps(content))
resp = await ws.recv()
print(resp)
except websockets.exceptions.ConnectionClosed as exp:
print(exp)

#发送音频请求
async def audioRequest(ws):
content = {
"aiType": "dm", #可选dm/asr, dm获取对话结果,asr只获取asr结果
"topic": "recorder.stream.start",
"recordId": uuid4().hex,
"audio": {
"audioType": "mp3", #修改为测试文件的类型
"sampleRate": 16000, #修改为测试文件的sampleRate
"channel": 1, #修改为测试文件的channel
"sampleBytes": 2 #修改为测试文件的sampleBytes
},
"asrParams": {
"realBack": True, #实时返回asr结果
"enableVAD": True, #启动VAD
"enablePunctuation": True, #返回结果是否带拼音
"enableTone": True, #返回结果是否带声调
"enableConfidence": True, #返回结果是否带置信度
"enableNumberConvert": True, #返回结果是否进行数字转换
},
}
try:
#发送文本消息
await ws.send(json.dumps(content))
# 发送音频消息
with open(audioFile, 'rb') as f:
while True:
chunk = f.read(400) #wav buffsize=3200 其他的400
if not chunk:
await ws.send(bytes("", encoding="utf-8"))
break
print(len(chunk))
await ws.send(chunk)
async for message in ws:
print(message)
resp = json.loads(message)
if 'dm' in resp:
break
except websockets.exceptions.ConnectionClosed as exp:
print(exp)
ws.close()

async def dds_demo():
url = f"ws://botai-dsg.and-home.cn:4443/dsg/v1/prod?productId={productId}&apikey={apikey}"
print(url)
async with websockets.connect(url) as websocket:
#await textRequest(websocket) #发送文本请求
await audioRequest(websocket) #发送音频请求
asyncio.get_event_loop().run_until_complete(dds_demo())

后话

最后,通过大量的学习和试错打样,你发现米家雨水传感器,淘宝 46 包邮,搞活动更便宜,这大概是你最后的选择。

· 11 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

内容为最近一年的个人硬件产品的结构设计的对比分析,作为 DIY 爱好者,必然会有不足之处,欢迎指正。

假设你想自己制作一个桥梁模型送给朋友,手工制作一个当然很好,使用木头、或者捏一个土坯然后烧制、又或者直接使用车床加工。但是这需要一定的技艺,以及各种器具。

但是除了手工制作之外,你依然有很多种方式可以选择:

  • 乐高搭建,你只需要知道乐高拼搭基础,设计出图纸后导出并购买零件(正版与非正版价格差距极大)。

  • 3D 打印,3D 打印有多种不同的原理,材料可选类型也比较多,设计出模型之后切片打印即可。

  • 激光切割,使用激光切割机切割亚克力板或者木板,之后拼搭。

简短对比 :

技术乐高3D 打印(FDM)激光切割
特点模块化设计,易于组装和拆解可以打印出复杂的三维结构可以精确地切割出复杂的二维结构
结构强度玩具级工具级工具级
设计难度小时级小时级小时级
调试难度儿童中等
设备价格千元级千元级
材料价格十元级十元级百元级
制作速度小时级小时级分钟级
气味有(根据材料不同,可能有塑料熔化的气味)有(切割过程可能产生烧焦的气味)
噪音有(打印过程可能会产生噪音)有(切割过程可能会产生噪音)
优点总结1. 适合所有年龄层,易于上手
2. 可重复使用,具有高度的灵活性
3. 无需特殊工具或设备
1. 可以制作出复杂的三维结构
2. 可以打印出定制的零件,适合个性化设计
1. 可以精确地切割出复杂的二维结构
2. 结构稳固,适合制作承受重负的结构
缺点总结1. 结构可能不够稳固,不适合制作大型或承受重负的结构
2. 设计和功能可能受到乐高模块种类和数量的限制
1. 打印速度较慢,大型结构可能需要很长时间
2. 需要一定的设计和操作技能,学习曲线较陡峭
1. 设备价格高昂,运行和维护成本也较高
2. 需要一定的设计和操作技能,学习曲线较陡峭

乐高类

优点

  • 设计快速、简单,易于拆卸,非常适合快速验证创新想法。

  • 乐高模型可以随时调整,拆解后的部件可再次使用,避免了材料的直接损耗。相比之下,3D 打印和激光切割等技术一旦出现错误,调整难度较大,材料利用率相对较低。

缺点

  • 乐高结构的强度有限,不适合承受较大的力,因此更适合轻量级的应用。

  • 乐高零件种类有限,对复杂结构的设计造成一定限制。

  • 乐高零件的精度也有限,主要的结构单位和半个单位,这意味着你的模型尺寸需要是 0.5 个单位长度的倍数。

学习心得

搜索一些搭建图纸,可以帮助你更好地理解乐高的设计原理和技巧。不用管书籍的语言是德语还是英语。

软件

乐高结构设计软件:https://studiohelp.bricklink.com/hc/en-us

优点:软件可以导出零件名称,一键跳转采购。

总结

乐高设计快,但是采购之后自己拼搭比较耗时,遇到问题时可以先思考我之前有没有见过类似的图纸,没有见过而且比较复杂的话,一般选择 3D 打印。

3D 打印

3D 打印是个人制造的一种重要方式,它可以将数字模型转化为实体物体。一张 A4 纸的大小大约是 210mm×297mm,可以在此基础上选购合适的成型尺寸。

3D 打印分为多种技术,常见的有 FDM(熔融沉积成型)、SLA(光固化成型)、SLS(激光烧结成型)等。其中 FDM 是最常见的 3D 打印技术,也是我使用的技术。

3D 打印目前主要有:

  • 定制手办方向

需要深入的学习建模软件

学习调色与上色

tip

目前桌面级打印机难以直接获得彩色精美手办。大多是光固化再上色或彩色 FDM 打印后打磨。

设备需要常维护:使用洗涤器清洗底板、挪动后重新调平。便宜的设备往往打印慢的同时需要调试:层纹错位、堵头、首层不平、拉丝翘边、悬空下垂等问题。

  • 工业设计

简单的学习建模软件

深入学习螺丝螺母磁铁钢针等材料的嵌入

tip

嵌入螺丝与螺母

暂停埋入螺母,预留 0.2mm 的间隙,然后再埋入螺母,可以避免打印头碰到螺母。

暂停层设置方法:切片后点右边的 z 轴预览条,右键可以选择:添加暂停打印

嵌入磁铁

普通磁铁与 N52 磁铁价格相差较大,但依旧推荐使用 N52 磁铁。因为普通磁铁在打印件中,磁吸力会有所衰减,而 N52 磁铁衰减后依然有不错的吸力。如果因为吸力不够导致打印件整体作废得不偿失。

设计孔位时磁铁最好下沉,齐平与突出都会让磁铁被撞碎。下沉太多会导致吸力下降。也侧水平设置槽位,让磁铁便于更换。

留出旷量给胶水(慢干胶与UHU胶水会腐蚀结构件,推荐快干胶502)、或者使用过盈、或者暂停+封顶(选择强磁,否则吸力下降严重)

不同尺寸、磁力的磁铁阵列排布时,距离过近会互相影响。常见的有:6mm直径 2mm 的N55磁铁、10mm直径的磁铁

分辨磁铁NS极可以使用一个3d打印件,一头标注N,一头标注S,放置磁铁后,吸起来哪个就放进去。

嵌入钢针

钢针是最为常见的材料,可以选择的尺寸最多,将钢针以不同角度垂直嵌入 3D 打印件中可以提高打印件的强度。

想要将钢针水平嵌入到 3D 打印件中同时避免磕碰打印头、保持与打印件接触紧密,可以通过将打印一个辅助件完成:长方体的内切圆柱(剪切掉内切圆柱),然后将钢针嵌入到辅助件中,再通过暂停层将辅助件嵌入到打印件中。

另外也可以利用钢针制作合页等结构,比纯3D打印更耐用。

在 3D 打印过程中,材料的选择对打印效果和性能有重要影响。下面是部分常见材料及其价格与特点。

tip

不同品牌价格不同,仅供参考,以下是一些材料的前缀与后缀的简要解释:

  • CR ,表示更多色彩
  • CF ,表示碳纤维复合
  • Matte ,表示哑光质感
  • Silk ,表示丝绸质感
  • Hyper/Fast ,表示高速打印
材料全称特点及应用说明价格
ABS丙烯腈-丁二烯-苯乙烯共聚物强度高、耐热性好、易加工,适用于一般性的 3D 打印项目。59 元/KG
PETG改性聚对苯二甲酸乙二醇酯耐热、透明度高、耐化学性强,适用于透明或耐热性能的打印项目。59 元/KG
PLA聚乳酸生物降解、环保、易于打印、无毒,适用于一般性的 3D 打印项目。69 元/KG
PA聚酰胺/尼龙 (Nylon)机械性能更强,耐热,用于制作普通机械强度要求的打印项目。79 元/KG
TPU热塑性聚氨酯弹性好、耐磨性强,常用于制作柔软的零件或弹性组件。98 元/KG
PET-CF碳纤维增强聚对苯二甲酸乙二醇酯更高的强度和刚性,适用于要求更高机械性能的打印项目。108 元/KG
PLA-CF碳纤维增强聚乳酸更高的强度和刚性,适用于要求更高机械性能的打印项目。129 元/KG
ASA聚酰胺酸酯工程塑料,耐热性和机械性能优异,适用于高强度和耐热性的打印项目。169 元/KG
PC聚碳酸酯抗冲击性和透明度优异,常用于制作耐用的零件或透明的构件。219 元/KG
PA-CF碳纤维增强聚酰胺/尼龙碳纤维更高的强度和耐热性,适用于要求更高机械性能的打印项目。249 元/KG

建模软件

建模软件是 3D 打印的基础,可选的非常多,譬如Fusion 360Blender

模型可以从 0 开始建,也在别人的基础上修改,由于家用3D打印机尺寸较小,工具物品难以一体成型,因此模块化设计思维较为重要。

成品模型库:

优点

  • 只需要一个晚上就可以验证你的复杂创意。

  • 提升空间大,可以选择一体成型,或者碳纤维材料,也可以使用暂停打印嵌入螺丝、螺母、磁石、金属棒等来创作一些强度极高的混合材质的工具。

缺点

  • 连接处的强度不够需要使用人工介入使用特殊手段提升。

  • 打印时间长而且偶尔成品有瑕疵。

学习心得

3D 打印的知识相对来说比较碎片化,需要持续学习与实战验证。可以关注一些 3D 打印博主、逛逛 3D 打印社区,可以学习到很多建模、切片、机器的调试、嵌入等技巧。

软件

从 0 开始 3D 打印需要用到 3D 建模软件与切片软件

建模软件就是构建出你要打印的物品的模型的软件。

切片软件则是把这个模型切割为一层一层,控制 3D 打印机的喷嘴运动路径等参数的软件。目前我的 3D 打印机只认切片后的文件。

这里建模软件推荐找个教程,教程上用什么跟着学也用什么。

切片软件一般厂家有提供。

激光切割

激光切割机主要用于切割和雕刻材料,如亚克力板、金属片、木板等。激光切割机通过激光束对材料进行加热,使其熔化或气化,然后通过气流将熔化或气化的材料吹走,从而实现切割。

可以通过调整激光头的运行速度和激光强度来实现穿透切割和表面雕刻。

激光强度大,激光头移动速度慢,可以实现穿透切割,即将材料完全切断(速度:10 分钟级别)。

激光强度小,激光头移动速度快,可以实现表面雕刻,即在材料表面刻出图案或文字(速度:秒级别)。

优点

  • 刻字速度极快,立等可取。

  • 支持材料多样。

缺点

  • 设备占地较大,气味明显需要单独通风,水冷型的需要偶尔换水。

  • 部分材料切割容易边缘不光滑、发黑。

学习心得

激光切割主要是调试激光能量强度与材料的关系。常见的材料有以下几种:

亚克力板,比较常见的像一块 30*30cm 5mm 厚的透明板材

个人很喜欢的材料,透明的材质用来装日用摆件类的单片机很有“探索版”的感觉。做工具用比较脆,有一定韧性,但保存不当会产生划痕。

金属片:强度比木板要好不少,相比于亚克力板的硬、脆,金属片可以在切割后折弯,用来制作需要弯曲且强度要求高的结构。

无可替代的优势,你永远可以相信金属。

木板:木板具有良好的硬度和强度,面积较大的区域有一定韧性,但细的地方极其容易断裂。另外木板在潮湿的环境下可能会变形,因此需要在适当的环境中存储和使用。

用来制作各种家居装饰、艺术品和模型。木板的颜色和纹理使得切割出来的产品具有自然的美感和温馨的氛围。此外,木板也可以通过砂纸或者涂料进行后期处理,以改变其颜色和质感,增加产品的美观性和耐用性。

有些材料理论上可行,但是我实际工作中没有使用,不便评价:玻璃和陶瓷、织物和皮革、塑料和橡胶

相比前面两项个人级别的制作,激光切割家用较少,其一是气味较大,需要单独通风,有些激光切割机还需要用水桶接冷凝水。其二是占地面积较大,3D 打印机可以放在桌子上,激光切割机需要放在地上,且需要有一定的安全距离。

在学校、工作室等场所,通常会有激光切割机,可以提供激光切割服务。

软件

厂家附赠非开源软件。

· 4 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

群晖是一款非常优秀的 NAS 产品,它可以提供文件存储、多媒体服务、远程访问等功能。一般来说,一台群晖的寿命 4-6 年,折合下来比服务器便宜一点点。因此,我决定做云时代的逆行者,将服务器上的一些服务迁移到群晖上。

注意:云服务器的优点有很多,包括:更加安全、更加灵活、更加便宜等等,迁移需谨慎。

原环境分析

租用的服务器是阿里云产品,选择了宝塔面板进行管理。

网站使用了前后端分离的设计模式,前端基于 Vue 生成的静态页面,后端则基于 Java。后端允许上传一些文件,使用的是 3000 端口进行通信,文件传输则在 8000 端口,路径设置为/upload/。通过 Nginx 进行了反向代理,将/upload/路径的请求转发到 8080 端口,将其他请求(80 端口)转发到 3000 端口。因此,对于这个网站,通信主要通过 80 端口和 8080 端口进行。

tip

如果你遇到了类似的问题,只需要梳理出我们最终通过宝塔面板的哪些端口访问就可以了。

迁移策略

服务器迁移

创建一个 Docker 容器(如果原本用的是宝塔面板,那就继续使用宝塔面板),并按照原来的部署文档在容器内部重新运行服务。

将相关的命令设置为开机自启动。

另外,重要的文件等资料需要单独挂载,并可以设置为只读模式,以防止数据丢失同时更加安全。

端口映射

Docker 的镜像可以通过端口映射的方式,将容器内的端口映射到宿主机(群晖)的端口上。

路由器可以将外网流量转发到宿主机(群晖)上,因此只需要在路由器上设置端口转发即可。

tip

需要注意的是,群晖的部分端口可能已被群晖自身使用,可以通过群晖官网查询。外网的部分端口(如 80 端口)可能被运营商封掉,或者路由器自身需要使用。因此,我们需要选择一些不常用的端口,以避免出现服务异常。

因此有了以下的端口映射规则(你可以先停止容器再设置):

容器内端口(宝塔)宿主机端口(群晖)外网端口
8040804080
808080804880

域名解析

现在我们需要将域名解析到服务器上,由于 80 端口不能直接访问,并且小区和域名供应商都会要求备案,这就需要我们使用 Cloudflare 的 DNS 解析服务。Cloudflare 可以充当中间人,将流量转发到服务器上。

即无法通过 http://www.xxx.com 直接访问到服务器,只能通过域名+端口号如 http://www.xxx.com:8080 访问。

在 cloudflare 中添加域名,全程按照提示操作添加即可。

添加完成后,选择规则->回源(Origin Rules)。因为 Java 用到了两个端口,所以需要添加两个端口转发规则:

  • 当满足条件时(访问域名且路径不以/upload/开头),将流量转发到服务器的 4080 端口。

  • 当满足条件时(访问域名且路径以/upload/开头),将流量转发到服务器的 4880 端口。

结果

通过以上步骤,我们成功地将网站迁移到了新服务器上。用户访问网站时,流程如下:

  • 通过 DNS 解析,找到 cloudflare 的服务器。
  • cloudflare 根据规则,把 80 流量转发到路由器的 4080 上。
  • 服务器接收到流量,把流量转发到群晖的 4080 端口。
  • 群晖接收到流量,把流量转发到容器的 80 端口。
  • 容器接收到流量,通过 Nginx 分配数据。
  • Nginx 根据规则,把流量转发到容器内的 8080 端口。
  • 数据按照原本的路径逐一返回至 cloudflare。
  • cloudflare 把数据返回给访问者。

如法炮制可以继续迁移其他站点。

这种迁移方式不仅保证了原有服务的连续性,也确保了服务器不会被外网直接访问,从而提高了网络安全性。即使网站被黑,也不会影响到其他服务,只需重启这个容器即可恢复原状。

后话

这次的迁移过程体现了分层思想的重要性,这主要来自《白帽子讲 Web 安全》这本书。整个过程没有遇到什么问题,只需要对容器化和 Cloudflare 有一定的了解。我希望我的经验能给读者带来一些启示。

· 6 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

前段时间偶然间看到了一些生成式 AI 文本摘要项目,觉得很有意思。个人不太信任第三方服务,于是就加到待办里,想着自己也实现一个,最近终于有空了。

逻辑上的核心功能是:自动生成,无需人工干预,一次生成,再次生成消耗 key

样式上的核心功能是:逐字显示,好像是个机器人真的在实时生成。

本篇文章将记录如何实现这个功能。

原型

博客是基于 Docusaurus 搭建的,而 Docusaurus 是基于 React 的,文章内容是通过 markdown 文件写的,所以需要设计一个 React 组件,传入 markdown 文件内的文本内容,每次有请求时,将文章内容转换为文本摘要。

但是这样做有一些问题,主要的是重复的每次请求都会消耗 key,因此需要储存已请求内容。

判断条件可以设为如果内容不存在,则直接调用,否则就重新生成,然后存储。

由此可知我们至少需要:内容(用来判断是否重复)、摘要(用来显示)

{
"This is the text to summarize": "This is the summary",
"This is the text to summarize 2": "This is the summary 2",
}

如果储存是需要成本的,我们可以使用hash值来判断内容是否相同,如果hash值相同,那么就不需要重新生成摘要了。这样不要存储一篇文章,只需要存储hash值和摘要就可以了。

{
"248ae1890a0084b3bbc30bd3c0c2e17e": "summary"
}

如果有多个文章如何每次请求只请求指定的文章呢?

我们可以使用路径来区分不同的文章,在服务器上我们的方法就太多了。

但是静态的话我使用文件名来区分不同的文章。将文章路径中的/替换为_,然后加上.json后缀,就可以了。

blog_1.json
{
"248ae1890a0084b3bbc30bd3c0c2e17e": "summary"
}

把这个代码逻辑插入到 React 组件中就可以实现了,根据你调用的API不同,你也许可以设置返回的摘要长度等参数。

记得别直接把key写在代码里,而是通过环境变量传入。如果你的项目通过github pages部署,那么可以在项目的setting中设置环境变量REACT_APP_API_KEY,然后在代码中通过process.env.REACT_APP_API_KEY来获取。

实现

当然,这只是一个比较粗糙的想法,接下来让我们完善下代码细节,让它优雅的同时,可以在博客中使用。

逻辑功能

我在reflex-chat#20里提交了关于百度API的实现,在这个仓库里你应该能找到其他API的操作方式。

main.py
import os
import json
import time
import hashlib
import pathlib
import requests
import feedparser
from parsel import Selector
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
class BaiduAI:
def __init__(self):
self.BAIDU_API_KEY = os.getenv("BAIDU_API_KEY")
self.BAIDU_SECRET_KEY = os.getenv("BAIDU_SECRET_KEY")
self.token = self.get_access_token()

def get_access_token(self):
"""
:return: access_token
"""
url = "https://aip.baidubce.com/oauth/2.0/token"
params = {
"grant_type": "client_credentials",
"client_id": self.BAIDU_API_KEY,
"client_secret": self.BAIDU_SECRET_KEY,
}
return str(requests.post(url, params=params).json().get("access_token"))

def get_result(self, text: str):
messages = json.dumps(
{
"messages": [
{
"role": "user",
"content": "阅读下面的博文,然后尽可能接近50个词的范围内,提供一个总结。只需要回复总结后的文本:{}".format(
text
),
}
]
}
)
session = requests.request(
"POST",
"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token="
+ self.token,
headers={"Content-Type": "application/json"},
data=messages,
)
json_data = json.loads(session.text)
if "result" in json_data.keys():
answer_text = json_data["result"]
return answer_text


class Jsonsummary:
def __init__(self):
root = pathlib.Path(__file__).parent.resolve()
self.json_file_path = os.path.join(root,"summary")
self.url = "https://jiangmiemie.com/"
self.pages = []

def load_json(self):
# 加载JSON文件
loaded_dict = {}
for file in os.listdir(self.json_file_path):
with open(os.path.join(self.json_file_path, file), "r", encoding="utf-8") as json_file:
loaded_dict[self.url + file.replace("_", "/").replace(".json", "")] = json.load(json_file)
return loaded_dict

def save_json(self,loaded_dict):
# 将字典存入JSON文件
for key in loaded_dict:
key_path = key.replace(self.url, "").replace("/", "_") + ".json"
save_path = os.path.join(self.json_file_path, key_path)
with open(save_path, "w", encoding="utf-8") as json_file:
json.dump(loaded_dict[key], json_file, indent=4)

def clean_json(self):
# 根据RSS结果清理JSON文件
for file in os.listdir(self.json_file_path):
if file not in self.pages:
os.remove(os.path.join(self.json_file_path, file))

def blog_summary(feed_content):
jsdata = Jsonsummary()
loaded_dict = jsdata.load_json()

for page in feed_content:
url = page["link"].split("#")[0]
jsdata.pages.append(url.replace(jsdata.url, "").replace("/", "_") + ".json")
# 剪切掉摘要部分,仅保留正文
content = page["content"][0]["value"]
selector = Selector(
text=content.split("此内容根据文章生成,仅用于文章内容的解释与总结")[1]
)
content_format = "".join(selector.xpath(".//text()").getall())
content_hash = hashlib.md5(content_format.encode()).hexdigest()
if (
loaded_dict.get(url)
and loaded_dict.get(url).get("content_hash") == content_hash
):
continue
else:
ai = BaiduAI()
summary = ai.get_result(content_format)
loaded_dict.update(
{url: {"content_hash": content_hash, "summary": summary}}
)
jsdata.save_json(loaded_dict)
jsdata.clean_json()

def fetch_blog():
content = feedparser.parse("https://jiangmiemie.com/blog/rss.xml")["entries"]
blog_summary(content)


if __name__ == "__main__":
fetch_blog()

BAIDU_API_KEYBAIDU_SECRET_KEY传入git action的环境中的示例:

- name: Update
run: python build_readme.py
env:
BAIDU_API_KEY: ${{ secrets.BAIDU_API_KEY }}
BAIDU_SECRET_KEY: ${{ secrets.BAIDU_SECRET_KEY }}

完整代码参考我的github仓库

这样我访问部署网址/summary/博客路径就可以精准得到对应的摘要了,接下来就是在博客中使用了。

样式功能

样式上的核心功能是:逐字显示,好像是个机器人真的在实时生成。可以更详细的拆为:获取摘要、逐字显示、放入框架。

//逐字显示
const TypingComponent = ({ text, speed = 100 }) => {
const [displayedText, setDisplayedText] = useState('');

useEffect(() => {
let index = 0;

const typingInterval = setInterval(() => {
setDisplayedText((prevText) => {
if (index < text.length) {
return prevText + text[index++];
} else {
clearInterval(typingInterval);
return prevText;
}
});
}, speed);

return () => clearInterval(typingInterval);
}, [text, speed]);

return <>{displayedText}</>;
};
// 获取摘要
const JsonReader = ({
fieldToMatch,
}) => {
// 替换url与/
const path = fieldToMatch.replace(/https:\/\/jiangmiemie.com\//, "").replace(/\//g, "_");
const url = `https://jiangmiemie.com/jiangyangcreate/summary/${path}.json`;
const [jsonData, setJsonData] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const data = await response.json();
setJsonData(data);
} catch (error) {
console.error("Error fetching JSON:", error);
}
};

fetchData();
}, [url]);

const getFieldData = () => {
if (!jsonData) {
return <TypingComponent text='摘要生成中...' speed={100} />;
}
// 根据字段进行匹配
const matchingField = jsonData["summary"];
return (
<>
<TypingComponent text={matchingField} speed={100} />
</>
);
};

return <>{getFieldData()}</>;
};

// 放入框架
const Aisummary = ({ children }) => (
<div class="post-ai">
<div class="ai-title">
<a
class="ai-title-left"
href="/blog/2024/1/31/"
title="查看详情"
data-pjax-state=""
>
<div class="ai-title-text">文章摘要</div>
</a>
</div>
<div class="ai-explanation" style={{ display: "block" }}>
<JsonReader fieldToMatch = {children}/>
</div>
<div class="ai-suggestions"></div>
<div class="ai-bottom">
<div class="ai-tips">此内容根据文章生成,仅用于文章内容的解释与总结</div>
</div>
</div>
);

以上所有代码构成了你现在在本篇文章中看到的效果。

· 4 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

写博客对我而言,是一种爱好,可以追溯到 2009 年,这篇文章记录了一些博客写作过程之中的实践。

设计博客

广泛的查看别人的博客

设计博客好比画画,从零开始画出一幅好画比较困难,但是如果临摹大师的作品就会相对容易一些。你可以搜索一些博客聚合类站点,查看成员的博客配置,对博客站点的设计有个大概的印象。这类站点通常有比较好的可迁移性。

不需要买域名和服务器

我建议个人博客使用 markdown 编写,存在 GitHub 并绑定自己默认是个非常好的选择。如果你从服务器开始搭建,不光会耗尽初始的热情,也会由于更新不便,服务器异常而无法专注于内容。

博客美化切记过度

起初,写技术博客对我来说是一件容易的事,因为我无时无刻都有很多想法。我添加许多炫酷的特效在我的博客上,包括但不限于鼠标特效、点击特效、全局画布、一言、看板娘、音乐播放器、随机背景图、各种悬浮点击渐变特效。但这些美化难以做到不同设备上的兼容。此时我开始删减博客中我曾经认为“增色”的部分:内容不是越多越好。

更新方式

周更

周更的使用者是阮一峰老师,他从 2018 年开始每周都会定期更新,周更压力在于:不知道这周写什么。

双周更

双周更理论上能够很好的保持足够的输入,但实际操作中更容易遇到一整周都很忙的情况。

月更

月更是我坚持最久的更新方式,一个月足以输入足够的知识和内容。

载体选择

纯文字

纯文字的内容往往更能加载更快、获得国际流量的青睐、非常易于检索。

多媒体

只在必要的地方加入多媒体。注意:我并不是在否定文字以外的媒介,越来越多的知识不局限于通过书籍的方式传播:视频、音频、图片、动态网页、互动游戏。

整理博客

好的博客离不开定期整理,包括:

  • 清除无法访问的链接
  • 汇总合并类似的章节
  • 将碎片的知识串联成体系

标签分类

我个人建议:表头的栏目推荐为 4-5 个,如有折叠展开:展开内容为 3-5 个。我们信息加工能力的局限1

风格化

这一步是要将你的站点与其他站点区分开来,风格化过程中会涉及到一些编程相关的知识,但主要是审美。

Live Editor
// 一个足够简单的单元,配上无数次的重复即可呈现一个有趣的画面
// 一张小巧无缝矢量图即可实现用极小的内存平铺满整个背景。
function example(props) {
  // 使用 XPath 查询选择输出框
  const xpathSelector =
    "/html/body/div/div[2]/div/div/main/article/div/div[2]/div[4]";
  const myElement = document.evaluate(
    xpathSelector,
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
  // 你可以在这里查看或修改这个SVG图片
  // 譬如 https://jiangmiemie.com/img/logo-192.svg
  myElement.style.backgroundImage =
    'url("https://jiangmiemie.com/img/protruding-squares.svg")';
  myElement.style.backgroundColor = "ee5522"; // 使用 backgroundColor,而不是 background-color
  // 添加一个时钟
  const [date, setDate] = useState(new Date());
  useEffect(() => {
    const timerID = setInterval(() => tick(), 1000);

    return function cleanup() {
      clearInterval(timerID);
    };
  });

  function tick() {
    setDate(new Date());
  }
  return (
    <div
      style={{
        color: 'white',
        height: "200px", // 适当调整高度
      }}>
    <h1>{date.toLocaleTimeString()}</h1>
    
    </div>
  );
}
Result
Loading...

放平心态

由于各种问题都会发生,譬如国内忽然不能访问 Github 了,那么容灾和冗余就决定了你是否能够快速恢复站点(如果不能的话,对你的打击会非常大)

博客的流量和短视频相比差的太多了,数年无人问津更是常态。不要急于求成,否则只会适得其反。这里推荐几个真正在玩博客的前辈:

  • 苏洋博客 —— 一个 real man 一个乐于分享的前辈。
  • 阮一峰的网络日志 —— 科技爱好者周刊已经成了我每周必看的内容,阮老师是真正的布道者。

Footnotes

  1. Miller, G. A. (1956). 神奇的数字:7±2;我们信息加工能力的局限(The magical number seven, plus or minus two: Some limits on our capacity for processing information)